From 03af8568bfba61201cdb52d18d977a09b1c8dfcf Mon Sep 17 00:00:00 2001 From: jodur Date: Sun, 17 Jan 2021 10:33:56 +0100 Subject: [PATCH 1/3] Add image scale option It is now possible to scale the image with a factor (ratio is preserved) that is received from the camera entity. Advantages: - Smaller snapshot file - Faster processing by deepstack --- .../deepstack_object/image_processing.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/custom_components/deepstack_object/image_processing.py b/custom_components/deepstack_object/image_processing.py index 2c9c79a..da38a05 100644 --- a/custom_components/deepstack_object/image_processing.py +++ b/custom_components/deepstack_object/image_processing.py @@ -76,6 +76,7 @@ CONF_ROI_X_MIN = "roi_x_min" CONF_ROI_Y_MAX = "roi_y_max" CONF_ROI_X_MAX = "roi_x_max" +CONF_SCALE= "scale" CONF_CUSTOM_MODEL = "custom_model" DATETIME_FORMAT = "%Y-%m-%d_%H-%M-%S" @@ -86,6 +87,7 @@ DEFAULT_ROI_Y_MAX = 1.0 DEFAULT_ROI_X_MIN = 0.0 DEFAULT_ROI_X_MAX = 1.0 +DEAULT_SCALE = 1.0 DEFAULT_ROI = ( DEFAULT_ROI_Y_MIN, DEFAULT_ROI_X_MIN, @@ -127,6 +129,7 @@ vol.Optional(CONF_ROI_X_MIN, default=DEFAULT_ROI_X_MIN): cv.small_float, vol.Optional(CONF_ROI_Y_MAX, default=DEFAULT_ROI_Y_MAX): cv.small_float, vol.Optional(CONF_ROI_X_MAX, default=DEFAULT_ROI_X_MAX): cv.small_float, + vol.Optional(CONF_SCALE, default=DEAULT_SCALE): vol.All(vol.Coerce(float, vol.Range(min=0.1, max=1))), vol.Optional(CONF_SAVE_FILE_FOLDER): cv.isdir, vol.Optional(CONF_SAVE_TIMESTAMPTED_FILE, default=False): cv.boolean, vol.Optional(CONF_SHOW_BOXES, default=True): cv.boolean, @@ -223,6 +226,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): roi_x_min=config[CONF_ROI_X_MIN], roi_y_max=config[CONF_ROI_Y_MAX], roi_x_max=config[CONF_ROI_X_MAX], + scale=config[CONF_SCALE], show_boxes=config[CONF_SHOW_BOXES], save_file_folder=save_file_folder, save_timestamped_file=config.get(CONF_SAVE_TIMESTAMPTED_FILE), @@ -234,7 +238,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ObjectClassifyEntity(ImageProcessingEntity): - """Perform a face classification.""" + """Perform a object classification.""" def __init__( self, @@ -249,6 +253,7 @@ def __init__( roi_x_min, roi_y_max, roi_x_max, + scale, show_boxes, save_file_folder, save_timestamped_file, @@ -291,24 +296,40 @@ def __init__( "y_max": roi_y_max, "x_max": roi_x_max, } - + self._scale = scale self._show_boxes = show_boxes self._last_detection = None self._image_width = None self._image_height = None self._save_file_folder = save_file_folder self._save_timestamped_file = save_timestamped_file + self._image=None def process_image(self, image): """Process an image.""" - self._image_width, self._image_height = Image.open( - io.BytesIO(bytearray(image)) - ).size + _LOGGER.debug((f"Open image")) + self._image= Image.open(io.BytesIO(bytearray(image)) + ) + + self._image_width, self._image_height = self._image.size + + #resize image if different then default + if self._scale!=DEAULT_SCALE: + try: + newsize=(self._image_width*self._scale , self._image_width*self._scale) + self._image.thumbnail(newsize,Image.ANTIALIAS) + self._image_width,self._image_height =self._image.size + with io.BytesIO() as output: + self._image.save(output,format="JPEG") + image=output.getvalue() + _LOGGER.debug((f"Image scaled with : {self._scale} W={self._image_width} H={self._image_height}")) + except Exception as err: + _LOGGER.error(f"Could not resize to scale : {self._scale} \r\n {err}") self._state = None self._objects = [] # The parsed raw data self._targets_found = [] saved_image_path = None - + try: predictions = self._dsobject.detect(image) except ds.DeepstackException as exc: @@ -344,7 +365,7 @@ def process_image(self, image): if self._save_file_folder and self._state > 0: saved_image_path = self.save_image( - image, self._targets_found, self._save_file_folder, + self._targets_found, self._save_file_folder, ) # Fire events @@ -396,13 +417,13 @@ def device_state_attributes(self) -> Dict: attr[CONF_SAVE_TIMESTAMPTED_FILE] = self._save_timestamped_file return attr - def save_image(self, image, targets, directory) -> str: + def save_image(self, targets, directory) -> str: """Draws the actual bounding box of the detected objects. Returns: saved_image_path, which is the path to the saved timestamped file if configured, else the default saved image. """ try: - img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") + img = self._image.convert("RGB") except UnidentifiedImageError: _LOGGER.warning("Deepstack unable to process image, bad data") return From 5941c4cdd2d810b99a48400b0847f13dc11656b3 Mon Sep 17 00:00:00 2001 From: jodur Date: Wed, 20 Jan 2021 10:18:28 +0100 Subject: [PATCH 2/3] Adjustment after remarks Rob --- README.md | 1 + .../deepstack_object/image_processing.py | 54 +++++++++++-------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7cf815f..987c87b 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Configuration variables: - **roi_x_max**: (optional, default 1), range 0-1, must be more than roi_x_min - **roi_y_min**: (optional, default 0), range 0-1, must be less than roi_y_max - **roi_y_max**: (optional, default 1), range 0-1, must be more than roi_y_min +- **scale**: (optional, default 1.0), range 0.1-1.0, scaling factor for the images that are pulled from the camera stream. This results in smaller images files, especially when using hires-cameras. - **source**: Must be a camera. - **targets**: The list of target object names and/or `object_type`, default `person`. Optionally a `confidence` can be set for this target, if not the default confidence is used. Note the minimum possible confidence is 10%. diff --git a/custom_components/deepstack_object/image_processing.py b/custom_components/deepstack_object/image_processing.py index da38a05..59cd7b0 100644 --- a/custom_components/deepstack_object/image_processing.py +++ b/custom_components/deepstack_object/image_processing.py @@ -76,7 +76,7 @@ CONF_ROI_X_MIN = "roi_x_min" CONF_ROI_Y_MAX = "roi_y_max" CONF_ROI_X_MAX = "roi_x_max" -CONF_SCALE= "scale" +CONF_SCALE = "scale" CONF_CUSTOM_MODEL = "custom_model" DATETIME_FORMAT = "%Y-%m-%d_%H-%M-%S" @@ -129,7 +129,9 @@ vol.Optional(CONF_ROI_X_MIN, default=DEFAULT_ROI_X_MIN): cv.small_float, vol.Optional(CONF_ROI_Y_MAX, default=DEFAULT_ROI_Y_MAX): cv.small_float, vol.Optional(CONF_ROI_X_MAX, default=DEFAULT_ROI_X_MAX): cv.small_float, - vol.Optional(CONF_SCALE, default=DEAULT_SCALE): vol.All(vol.Coerce(float, vol.Range(min=0.1, max=1))), + vol.Optional(CONF_SCALE, default=DEAULT_SCALE): vol.All( + vol.Coerce(float, vol.Range(min=0.1, max=1)) + ), vol.Optional(CONF_SAVE_FILE_FOLDER): cv.isdir, vol.Optional(CONF_SAVE_TIMESTAMPTED_FILE, default=False): cv.boolean, vol.Optional(CONF_SHOW_BOXES, default=True): cv.boolean, @@ -303,33 +305,33 @@ def __init__( self._image_height = None self._save_file_folder = save_file_folder self._save_timestamped_file = save_timestamped_file - self._image=None + self._image = None def process_image(self, image): """Process an image.""" - _LOGGER.debug((f"Open image")) - self._image= Image.open(io.BytesIO(bytearray(image)) - ) - + self._image = Image.open(io.BytesIO(bytearray(image))) + _LOGGER.debug((f"Open image")) self._image_width, self._image_height = self._image.size - - #resize image if different then default - if self._scale!=DEAULT_SCALE: - try: - newsize=(self._image_width*self._scale , self._image_width*self._scale) - self._image.thumbnail(newsize,Image.ANTIALIAS) - self._image_width,self._image_height =self._image.size - with io.BytesIO() as output: - self._image.save(output,format="JPEG") - image=output.getvalue() - _LOGGER.debug((f"Image scaled with : {self._scale} W={self._image_width} H={self._image_height}")) - except Exception as err: - _LOGGER.error(f"Could not resize to scale : {self._scale} \r\n {err}") + + # resize image if different then default + if self._scale != DEAULT_SCALE: + newsize = (self._image_width * self._scale, self._image_width * self._scale) + self._image.thumbnail(newsize, Image.ANTIALIAS) + self._image_width, self._image_height = self._image.size + with io.BytesIO() as output: + self._image.save(output, format="JPEG") + image = output.getvalue() + _LOGGER.debug( + ( + f"Image scaled with : {self._scale} W={self._image_width} H={self._image_height}" + ) + ) + self._state = None self._objects = [] # The parsed raw data self._targets_found = [] saved_image_path = None - + try: predictions = self._dsobject.detect(image) except ds.DeepstackException as exc: @@ -365,7 +367,8 @@ def process_image(self, image): if self._save_file_folder and self._state > 0: saved_image_path = self.save_image( - self._targets_found, self._save_file_folder, + self._targets_found, + self._save_file_folder, ) # Fire events @@ -432,7 +435,12 @@ def save_image(self, targets, directory) -> str: roi_tuple = tuple(self._roi_dict.values()) if roi_tuple != DEFAULT_ROI and self._show_boxes: draw_box( - draw, roi_tuple, img.width, img.height, text="ROI", color=GREEN, + draw, + roi_tuple, + img.width, + img.height, + text="ROI", + color=GREEN, ) for obj in targets: From 6245cd136df01aa5205f8bb809166b2a96a6d122 Mon Sep 17 00:00:00 2001 From: jodur Date: Wed, 20 Jan 2021 19:10:53 +0100 Subject: [PATCH 3/3] removed unnessar debug --- custom_components/deepstack_object/image_processing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/deepstack_object/image_processing.py b/custom_components/deepstack_object/image_processing.py index 59cd7b0..b233150 100644 --- a/custom_components/deepstack_object/image_processing.py +++ b/custom_components/deepstack_object/image_processing.py @@ -310,7 +310,6 @@ def __init__( def process_image(self, image): """Process an image.""" self._image = Image.open(io.BytesIO(bytearray(image))) - _LOGGER.debug((f"Open image")) self._image_width, self._image_height = self._image.size # resize image if different then default