Home Assistant Unofficial Reference 2024.12.1
image_processing.py
Go to the documentation of this file.
1 """Person detection using Sighthound cloud service."""
2 
3 from __future__ import annotations
4 
5 import io
6 import logging
7 from pathlib import Path
8 
9 from PIL import Image, ImageDraw, UnidentifiedImageError
10 import simplehound.core as hound
11 import voluptuous as vol
12 
14  PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA,
15  ImageProcessingEntity,
16 )
17 from homeassistant.const import (
18  ATTR_ENTITY_ID,
19  CONF_API_KEY,
20  CONF_ENTITY_ID,
21  CONF_NAME,
22  CONF_SOURCE,
23 )
24 from homeassistant.core import HomeAssistant, split_entity_id
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
28 import homeassistant.util.dt as dt_util
29 from homeassistant.util.pil import draw_box
30 
31 _LOGGER = logging.getLogger(__name__)
32 
33 EVENT_PERSON_DETECTED = "sighthound.person_detected"
34 
35 ATTR_BOUNDING_BOX = "bounding_box"
36 ATTR_PEOPLE = "people"
37 CONF_ACCOUNT_TYPE = "account_type"
38 CONF_SAVE_FILE_FOLDER = "save_file_folder"
39 CONF_SAVE_TIMESTAMPTED_FILE = "save_timestamped_file"
40 DATETIME_FORMAT = "%Y-%m-%d_%H:%M:%S"
41 DEV = "dev"
42 PROD = "prod"
43 
44 PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
45  {
46  vol.Required(CONF_API_KEY): cv.string,
47  vol.Optional(CONF_ACCOUNT_TYPE, default=DEV): vol.In([DEV, PROD]),
48  vol.Optional(CONF_SAVE_FILE_FOLDER): cv.isdir,
49  vol.Optional(CONF_SAVE_TIMESTAMPTED_FILE, default=False): cv.boolean,
50  }
51 )
52 
53 
55  hass: HomeAssistant,
56  config: ConfigType,
57  add_entities: AddEntitiesCallback,
58  discovery_info: DiscoveryInfoType | None = None,
59 ) -> None:
60  """Set up the platform."""
61  # Validate credentials by processing image.
62  api_key = config[CONF_API_KEY]
63  account_type = config[CONF_ACCOUNT_TYPE]
64  api = hound.cloud(api_key, account_type)
65  try:
66  api.detect(b"Test")
67  except hound.SimplehoundException as exc:
68  _LOGGER.error("Sighthound error %s setup aborted", exc)
69  return
70 
71  if save_file_folder := config.get(CONF_SAVE_FILE_FOLDER):
72  save_file_folder = Path(save_file_folder)
73 
74  entities = []
75  for camera in config[CONF_SOURCE]:
76  sighthound = SighthoundEntity(
77  api,
78  camera[CONF_ENTITY_ID],
79  camera.get(CONF_NAME),
80  save_file_folder,
81  config[CONF_SAVE_TIMESTAMPTED_FILE],
82  )
83  entities.append(sighthound)
84  add_entities(entities)
85 
86 
88  """Create a sighthound entity."""
89 
90  _attr_should_poll = False
91  _attr_unit_of_measurement = ATTR_PEOPLE
92 
93  def __init__(
94  self, api, camera_entity, name, save_file_folder, save_timestamped_file
95  ):
96  """Init."""
97  self._api_api = api
98  self._camera_camera = camera_entity
99  if name:
100  self._name_name = name
101  else:
102  camera_name = split_entity_id(camera_entity)[1]
103  self._name_name = f"sighthound_{camera_name}"
104  self._state_state = None
105  self._last_detection_last_detection = None
106  self._image_width_image_width = None
107  self._image_height_image_height = None
108  self._save_file_folder_save_file_folder = save_file_folder
109  self._save_timestamped_file_save_timestamped_file = save_timestamped_file
110 
111  def process_image(self, image):
112  """Process an image."""
113  detections = self._api_api.detect(image)
114  people = hound.get_people(detections)
115  self._state_state = len(people)
116  if self._state_state > 0:
117  self._last_detection_last_detection = dt_util.now().strftime(DATETIME_FORMAT)
118 
119  metadata = hound.get_metadata(detections)
120  self._image_width_image_width = metadata["image_width"]
121  self._image_height_image_height = metadata["image_height"]
122  for person in people:
123  self.fire_person_detected_eventfire_person_detected_event(person)
124  if self._save_file_folder_save_file_folder and self._state_state > 0:
125  self.save_imagesave_image(image, people, self._save_file_folder_save_file_folder)
126 
127  def fire_person_detected_event(self, person):
128  """Send event with detected total_persons."""
129  self.hasshass.bus.fire(
130  EVENT_PERSON_DETECTED,
131  {
132  ATTR_ENTITY_ID: self.entity_identity_id,
133  ATTR_BOUNDING_BOX: hound.bbox_to_tf_style(
134  person["boundingBox"], self._image_width_image_width, self._image_height_image_height
135  ),
136  },
137  )
138 
139  def save_image(self, image, people, directory):
140  """Save a timestamped image with bounding boxes around targets."""
141  try:
142  img = Image.open(io.BytesIO(bytearray(image))).convert("RGB")
143  except UnidentifiedImageError:
144  _LOGGER.warning("Sighthound unable to process image, bad data")
145  return
146  draw = ImageDraw.Draw(img)
147 
148  for person in people:
149  box = hound.bbox_to_tf_style(
150  person["boundingBox"], self._image_width_image_width, self._image_height_image_height
151  )
152  draw_box(draw, box, self._image_width_image_width, self._image_height_image_height)
153 
154  latest_save_path = directory / f"{self._name}_latest.jpg"
155  img.save(latest_save_path)
156 
157  if self._save_timestamped_file_save_timestamped_file:
158  timestamp_save_path = directory / f"{self._name}_{self._last_detection}.jpg"
159  img.save(timestamp_save_path)
160  _LOGGER.debug("Sighthound saved file %s", timestamp_save_path)
161 
162  @property
163  def camera_entity(self):
164  """Return camera entity id from process pictures."""
165  return self._camera_camera
166 
167  @property
168  def name(self):
169  """Return the name of the sensor."""
170  return self._name_name
171 
172  @property
173  def state(self):
174  """Return the state of the entity."""
175  return self._state_state
176 
177  @property
179  """Return the attributes."""
180  if not self._last_detection_last_detection:
181  return {}
182  return {"last_person": self._last_detection_last_detection}
def __init__(self, api, camera_entity, name, save_file_folder, save_timestamped_file)
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
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))
Definition: pil.py:18