1 """Support for the DOODS service."""
3 from __future__
import annotations
10 from PIL
import Image, ImageDraw, UnidentifiedImageError
11 from pydoods
import PyDOODS
12 import voluptuous
as vol
16 PLATFORM_SCHEMA
as IMAGE_PROCESSING_PLATFORM_SCHEMA,
17 ImageProcessingEntity,
34 _LOGGER = logging.getLogger(__name__)
36 ATTR_MATCHES =
"matches"
37 ATTR_SUMMARY =
"summary"
38 ATTR_TOTAL_MATCHES =
"total_matches"
39 ATTR_PROCESS_TIME =
"process_time"
41 CONF_AUTH_KEY =
"auth_key"
42 CONF_DETECTOR =
"detector"
43 CONF_LABELS =
"labels"
46 CONF_BOTTOM =
"bottom"
49 CONF_FILE_OUT =
"file_out"
51 AREA_SCHEMA = vol.Schema(
53 vol.Optional(CONF_BOTTOM, default=1): cv.small_float,
54 vol.Optional(CONF_LEFT, default=0): cv.small_float,
55 vol.Optional(CONF_RIGHT, default=1): cv.small_float,
56 vol.Optional(CONF_TOP, default=0): cv.small_float,
57 vol.Optional(CONF_COVERS, default=
True): cv.boolean,
61 LABEL_SCHEMA = vol.Schema(
63 vol.Required(CONF_NAME): cv.string,
64 vol.Optional(CONF_AREA): AREA_SCHEMA,
65 vol.Optional(CONF_CONFIDENCE): vol.Range(min=0, max=100),
69 PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
71 vol.Required(CONF_URL): cv.string,
72 vol.Required(CONF_DETECTOR): cv.string,
73 vol.Required(CONF_TIMEOUT, default=90): cv.positive_int,
74 vol.Optional(CONF_AUTH_KEY, default=
""): cv.string,
75 vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]),
76 vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100),
77 vol.Optional(CONF_LABELS, default=[]): vol.All(
78 cv.ensure_list, [vol.Any(cv.string, LABEL_SCHEMA)]
80 vol.Optional(CONF_AREA): AREA_SCHEMA,
88 add_entities: AddEntitiesCallback,
89 discovery_info: DiscoveryInfoType |
None =
None,
91 """Set up the Doods client."""
92 url = config[CONF_URL]
93 auth_key = config[CONF_AUTH_KEY]
94 detector_name = config[CONF_DETECTOR]
95 timeout = config[CONF_TIMEOUT]
97 doods = PyDOODS(url, auth_key, timeout)
98 response = doods.get_detectors()
99 if not isinstance(response, dict):
100 _LOGGER.warning(
"Could not connect to doods server: %s", url)
104 for server_detector
in response[
"detectors"]:
105 if server_detector[
"name"] == detector_name:
106 detector = server_detector
111 "Detector %s is not supported by doods server %s", detector_name, url
118 camera[CONF_ENTITY_ID],
119 camera.get(CONF_NAME),
124 for camera
in config[CONF_SOURCE]
129 """Doods image processing service client."""
131 def __init__(self, hass, camera_entity, name, doods, detector, config):
132 """Initialize the DOODS entity."""
139 self.
_name_name = f
"Doods {name}"
148 if detector[
"width"]
and detector[
"height"]:
149 self.
_width_width = detector[
"width"]
150 self.
_height_height = detector[
"height"]
155 confidence = config[CONF_CONFIDENCE]
158 labels = config[CONF_LABELS]
162 if isinstance(label, dict):
163 label_name = label[CONF_NAME]
164 if label_name
not in detector[
"labels"]
and label_name !=
"*":
165 _LOGGER.warning(
"Detector does not support label %s", label_name)
169 if not (label_confidence := label.get(CONF_CONFIDENCE)):
170 label_confidence = confidence
171 if label_name
not in dconfig
or dconfig[label_name] > label_confidence:
172 dconfig[label_name] = label_confidence
175 label_area = label.get(CONF_AREA)
176 self.
_label_areas_label_areas[label_name] = [0, 0, 1, 1]
180 label_area[CONF_TOP],
181 label_area[CONF_LEFT],
182 label_area[CONF_BOTTOM],
183 label_area[CONF_RIGHT],
185 self.
_label_covers_label_covers[label_name] = label_area[CONF_COVERS]
187 if label
not in detector[
"labels"]
and label !=
"*":
188 _LOGGER.warning(
"Detector does not support label %s", label)
192 if label
not in dconfig
or dconfig[label] > confidence:
193 dconfig[label] = confidence
196 dconfig[
"*"] = confidence
201 if area_config := config.get(CONF_AREA):
203 area_config[CONF_TOP],
204 area_config[CONF_LEFT],
205 area_config[CONF_BOTTOM],
206 area_config[CONF_RIGHT],
208 self.
_covers_covers = area_config[CONF_COVERS]
218 """Return camera entity id from process pictures."""
223 """Return the name of the image processor."""
224 return self.
_name_name
228 """Return the state of the entity."""
233 """Return device specific state attributes."""
235 ATTR_MATCHES: self.
_matches_matches,
237 label: len(values)
for label, values
in self.
_matches_matches.items()
244 img = Image.open(io.BytesIO(bytearray(image))).convert(
"RGB")
245 img_width, img_height = img.size
246 draw = ImageDraw.Draw(img)
249 if self.
_area_area != [0, 0, 1, 1]:
251 draw, self.
_area_area, img_width, img_height,
"Detection Area", (0, 255, 255)
254 for label, values
in matches.items():
257 box_label = f
"{label.capitalize()} Detection Area"
268 for instance
in values:
269 box_label = f
'{label} {instance["score"]:.1f}%'
281 _LOGGER.debug(
"Saving results image to %s", path)
282 os.makedirs(os.path.dirname(path), exist_ok=
True)
286 """Process the image."""
288 img = Image.open(io.BytesIO(bytearray(image))).convert(
"RGB")
289 except UnidentifiedImageError:
290 _LOGGER.warning(
"Unable to process image, bad data")
292 img_width, img_height = img.size
294 if self.
_aspect_aspect
and abs((img_width / img_height) - self.
_aspect_aspect) > 0.1:
297 "The image aspect: %s and the detector aspect: %s differ by more"
300 (img_width / img_height),
305 start = time.monotonic()
310 "doods detect: %s response: %s duration: %s",
313 time.monotonic() - start,
319 if not response
or "error" in response:
320 if "error" in response:
321 _LOGGER.error(response[
"error"])
327 for detection
in response[
"detections"]:
328 score = detection[
"confidence"]
335 label = detection[
"label"]
344 boxes[0] < self.
_area_area[0]
345 or boxes[1] < self.
_area_area[1]
346 or boxes[2] > self.
_area_area[2]
347 or boxes[3] > self.
_area_area[3]
351 boxes[0] > self.
_area_area[2]
352 or boxes[1] > self.
_area_area[3]
353 or boxes[2] < self.
_area_area[0]
354 or boxes[3] < self.
_area_area[1]
376 if label
not in matches:
378 matches[label].append({
"score":
float(score),
"box": boxes})
382 if total_matches
and self.
_file_out_file_out:
384 for path_template
in self.
_file_out_file_out:
385 if isinstance(path_template, template.Template):
387 path_template.render(camera_entity=self.
_camera_entity_camera_entity)
390 paths.append(path_template)
394 "Not saving image(s), no detections found or no output file configured"
def extra_state_attributes(self)
def process_image(self, image)
def _save_image(self, image, matches, paths)
def __init__(self, hass, camera_entity, name, doods, detector, config)
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
web.Response get(self, web.Request request, str config_key)
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
tuple[str, str] split_entity_id(str entity_id)
None draw_box(ImageDraw draw, tuple[float, float, float, float] box, int img_width, int img_height, str text="", tuple[int, int, int] color=(255, 255, 0))