From c67121fbc79218661545ec13e7ddfc51cfc1f05b Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Mon, 17 Nov 2025 17:14:24 +0100 Subject: [PATCH 01/11] draw box shape according to the model --- .../app_bricks/object_detection/__init__.py | 22 +++++++++++++++++-- src/arduino/app_utils/image.py | 13 +++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/arduino/app_bricks/object_detection/__init__.py b/src/arduino/app_bricks/object_detection/__init__.py index 93f2e290..774a848d 100644 --- a/src/arduino/app_bricks/object_detection/__init__.py +++ b/src/arduino/app_bricks/object_detection/__init__.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA # # SPDX-License-Identifier: MPL-2.0 +from typing import Any from PIL import Image from arduino.app_utils import brick, Logger, draw_bounding_boxes @@ -20,8 +21,19 @@ class ObjectDetection(EdgeImpulseRunnerFacade): """ def __init__(self, confidence: float = 0.3): + """Initialize the ObjectDetection module. + + Args: + confidence (float): Minimum confidence threshold for detections. Default is 0.3 (30%). + + Raises: + ValueError: If model information cannot be retrieved. + """ self.confidence = confidence super().__init__() + self._model_info = self.get_model_info() + if not self._model_info: + raise ValueError("Failed to retrieve model information. Ensure the Edge Impulse service is running.") def detect_from_file(self, image_path: str, confidence: float = None) -> dict | None: """Process a local image file to detect and identify objects. @@ -38,7 +50,7 @@ def detect_from_file(self, image_path: str, confidence: float = None) -> dict | ret = super().infer_from_file(image_path) return self._extract_detection(ret, confidence) - def detect(self, image_bytes, image_type: str = "jpg", confidence: float = None) -> dict: + def detect(self, image_bytes, image_type: str = "jpg", confidence: float = None) -> dict[str, list[Any]] | None: """Process an in-memory image to detect and identify objects. Args: @@ -65,7 +77,13 @@ def draw_bounding_boxes(self, image: Image.Image | bytes, detections: dict) -> I Image with bounding boxes and key points drawn. None if no detection or invalid image. """ - return draw_bounding_boxes(image, detections) + if self._model_info.model_type == "object_detection": + return draw_bounding_boxes(image, detections) + elif self._model_info.model_type == "constrained_object_detection": + return draw_bounding_boxes(image, detections, draw_centroid=True) + else: + logger.error("Model type not supported for drawing bounding boxes.") + return None def _extract_detection(self, item, confidence: float = None): if not item: diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 8870f9b1..32e7369f 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -94,7 +94,9 @@ def draw_colored_dot(draw, x, y, color, size): draw.ellipse(bounding_box, fill=color) -def draw_bounding_boxes(image: Image.Image | bytes, detection: dict, draw: ImageDraw.ImageDraw = None) -> Image.Image | None: +def draw_bounding_boxes( + image: Image.Image | bytes, detection: dict, draw: ImageDraw.ImageDraw = None, draw_centroid: bool = False +) -> Image.Image | None: """Draw bounding boxes on an image using PIL. The thickness of the box and font size are scaled based on image size. @@ -104,6 +106,8 @@ def draw_bounding_boxes(image: Image.Image | bytes, detection: dict, draw: Image detection (dict): A dictionary containing detection results with keys 'class_name', 'bounding_box_xyxy', and 'confidence'. draw (ImageDraw.ImageDraw, optional): An existing ImageDraw object to use. If None, a new one is created. + draw_centroid (bool, optional): If True, draws a dot at the centroid of the bounding box instead of the box + itself. Defaults to False. """ if isinstance(image, bytes): image_box = Image.open(io.BytesIO(image)) @@ -163,7 +167,12 @@ def draw_bounding_boxes(image: Image.Image | bytes, detection: dict, draw: Image x2_text = x1 + text_width + label_hpad * 2 # Draw bounding box - draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_thickness) + if draw_centroid: + centroid_x = int((x1 + x2) / 2) + centroid_y = int((y1 + y2) / 2) + draw_colored_dot(draw, centroid_x, centroid_y, box_color, size=max(3, box_thickness * 2)) + else: + draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_thickness) # Draw label background (dark gray, semi-transparent) on overlay label_bg_color = (0, 0, 0, 128) overlay = Image.new("RGBA", image_box.size, (0, 0, 0, 0)) From 46af52ecdb5605ad3c7bf757bd3c3c20383afb53 Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 11:21:16 +0100 Subject: [PATCH 02/11] add hollow dot --- src/arduino/app_utils/image.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 32e7369f..7e0e49b1 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -94,6 +94,21 @@ def draw_colored_dot(draw, x, y, color, size): draw.ellipse(bounding_box, fill=color) +def draw_hollow_dot(draw, x, y, size): + """Draws a hollow dot (circle) on a PIL Image at the specified coordinate. + + Args: + draw: An ImageDraw object from PIL. + x: The x-coordinate of the center of the dot. + y: The y-coordinate of the center of the dot. + size: The radius of the dot (in pixels). + """ + # Calculate the bounding box for the outer circle + inner_size = int(size * 0.5) + bounding_box_inner = (x - inner_size, y - inner_size, x + inner_size, y + inner_size) + draw.ellipse(bounding_box_inner, fill=(0, 0, 0, 0)) + + def draw_bounding_boxes( image: Image.Image | bytes, detection: dict, draw: ImageDraw.ImageDraw = None, draw_centroid: bool = False ) -> Image.Image | None: @@ -170,7 +185,7 @@ def draw_bounding_boxes( if draw_centroid: centroid_x = int((x1 + x2) / 2) centroid_y = int((y1 + y2) / 2) - draw_colored_dot(draw, centroid_x, centroid_y, box_color, size=max(3, box_thickness * 2)) + draw_hollow_dot(draw, centroid_x, centroid_y, size=max(3, box_thickness * 2)) else: draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_thickness) # Draw label background (dark gray, semi-transparent) on overlay From 7d2b399ef3459e2fcfb59f857ee5360522eb4823 Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 11:36:30 +0100 Subject: [PATCH 03/11] remove fill --- src/arduino/app_utils/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 7e0e49b1..b3fa24b2 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -106,7 +106,7 @@ def draw_hollow_dot(draw, x, y, size): # Calculate the bounding box for the outer circle inner_size = int(size * 0.5) bounding_box_inner = (x - inner_size, y - inner_size, x + inner_size, y + inner_size) - draw.ellipse(bounding_box_inner, fill=(0, 0, 0, 0)) + draw.ellipse(bounding_box_inner) def draw_bounding_boxes( From 9f98df8fbc64bb5d1dd52a69601ddfc63de71d3a Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 11:48:45 +0100 Subject: [PATCH 04/11] outline color --- src/arduino/app_utils/image.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index b3fa24b2..42e42c8b 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -94,19 +94,20 @@ def draw_colored_dot(draw, x, y, color, size): draw.ellipse(bounding_box, fill=color) -def draw_hollow_dot(draw, x, y, size): +def draw_hollow_dot(draw, x, y, color, size): """Draws a hollow dot (circle) on a PIL Image at the specified coordinate. Args: draw: An ImageDraw object from PIL. x: The x-coordinate of the center of the dot. y: The y-coordinate of the center of the dot. + color: A color value that PIL understands (e.g., "red", (255, 0, 0), "#FF0000"). This is the outline color. size: The radius of the dot (in pixels). """ # Calculate the bounding box for the outer circle - inner_size = int(size * 0.5) + inner_size = int(size * 1) bounding_box_inner = (x - inner_size, y - inner_size, x + inner_size, y + inner_size) - draw.ellipse(bounding_box_inner) + draw.ellipse(bounding_box_inner, outline=color) def draw_bounding_boxes( @@ -185,7 +186,7 @@ def draw_bounding_boxes( if draw_centroid: centroid_x = int((x1 + x2) / 2) centroid_y = int((y1 + y2) / 2) - draw_hollow_dot(draw, centroid_x, centroid_y, size=max(3, box_thickness * 2)) + draw_hollow_dot(draw, centroid_x, centroid_y, box_color, size=max(3, box_thickness * 2)) else: draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_thickness) # Draw label background (dark gray, semi-transparent) on overlay From b21da8ad63fb9268be370ac3358b7ee9e62107cb Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 12:04:05 +0100 Subject: [PATCH 05/11] improve --- src/arduino/app_utils/image.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 42e42c8b..42a4b3a8 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -104,10 +104,8 @@ def draw_hollow_dot(draw, x, y, color, size): color: A color value that PIL understands (e.g., "red", (255, 0, 0), "#FF0000"). This is the outline color. size: The radius of the dot (in pixels). """ - # Calculate the bounding box for the outer circle - inner_size = int(size * 1) - bounding_box_inner = (x - inner_size, y - inner_size, x + inner_size, y + inner_size) - draw.ellipse(bounding_box_inner, outline=color) + bounding_box = (x - size, y - size, x + size, y + size) + draw.ellipse(bounding_box, outline=color, width=2) def draw_bounding_boxes( @@ -186,7 +184,7 @@ def draw_bounding_boxes( if draw_centroid: centroid_x = int((x1 + x2) / 2) centroid_y = int((y1 + y2) / 2) - draw_hollow_dot(draw, centroid_x, centroid_y, box_color, size=max(3, box_thickness * 2)) + draw_hollow_dot(draw, centroid_x, centroid_y, box_color, size=10) else: draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_thickness) # Draw label background (dark gray, semi-transparent) on overlay From ed32f691aff5b1ad5f2384158b4f0682de45583c Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 14:34:34 +0100 Subject: [PATCH 06/11] refactor + fix tests --- .../app_bricks/object_detection/__init__.py | 12 ++-- src/arduino/app_utils/image.py | 67 ++++++++----------- .../objectdetection/test_objectdetection.py | 6 ++ 3 files changed, 38 insertions(+), 47 deletions(-) diff --git a/src/arduino/app_bricks/object_detection/__init__.py b/src/arduino/app_bricks/object_detection/__init__.py index 774a848d..09697afa 100644 --- a/src/arduino/app_bricks/object_detection/__init__.py +++ b/src/arduino/app_bricks/object_detection/__init__.py @@ -4,7 +4,7 @@ from typing import Any from PIL import Image -from arduino.app_utils import brick, Logger, draw_bounding_boxes +from arduino.app_utils import brick, Logger, draw_bounding_boxes, Shape from arduino.app_internal.core import EdgeImpulseRunnerFacade logger = Logger("ObjectDetection") @@ -75,15 +75,13 @@ def draw_bounding_boxes(self, image: Image.Image | bytes, detections: dict) -> I Returns: Image with bounding boxes and key points drawn. - None if no detection or invalid image. """ + shape = None if self._model_info.model_type == "object_detection": - return draw_bounding_boxes(image, detections) + shape = Shape.RECTANGLE elif self._model_info.model_type == "constrained_object_detection": - return draw_bounding_boxes(image, detections, draw_centroid=True) - else: - logger.error("Model type not supported for drawing bounding boxes.") - return None + shape = Shape.CIRCLE + return draw_bounding_boxes(image, detections, shape=shape) def _extract_detection(self, item, confidence: float = None): if not item: diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 42a4b3a8..0c7ccbb6 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -8,6 +8,12 @@ logger = Logger(__name__) + +class Shape: + RECTANGLE = "rectangle" + CIRCLE = "circle" + + # Define a mapping of confidence ranges to colors for bounding boxes CONFIDENCE_MAP = { (0, 20): "#FF0976", # Pink @@ -40,7 +46,7 @@ def _read(file_path: str) -> bytes: def get_image_type(image_bytes: bytes | Image.Image) -> str | None: - """Detect the type of an image from bytes or a PIL Image object. + """Detect the type of image from bytes or a PIL Image object. Returns: str: The image type in lowercase (e.g., 'jpeg', 'png'). @@ -60,7 +66,7 @@ def get_image_type(image_bytes: bytes | Image.Image) -> str | None: return None -def get_image_bytes(image: str | Image.Image | bytes) -> bytes: +def get_image_bytes(image: str | Image.Image | bytes) -> bytes | None: """Convert different type of image objects to bytes.""" if image is None: return None @@ -78,38 +84,27 @@ def get_image_bytes(image: str | Image.Image | bytes) -> bytes: return None -def draw_colored_dot(draw, x, y, color, size): +def draw_dot(draw, x, y, size, fill_color=None, outline_color=None): """Draws a large colored dot on a PIL Image at the specified coordinate. Args: draw: An ImageDraw object from PIL. x: The x-coordinate of the center of the dot. y: The y-coordinate of the center of the dot. - color: A color value that PIL understands (e.g., "red", (255, 0, 0), "#FF0000"). size: The radius of the dot (in pixels). + fill_color: The fill color of the dot. Default is None (no fill). + outline_color: The outline color of the dot. Default is None (no outline). """ # Calculate the bounding box for the circle bounding_box = (x - size, y - size, x + size, y + size) - # Draw a filled ellipse (which looks like a circle if the bounding box is a square) - draw.ellipse(bounding_box, fill=color) - - -def draw_hollow_dot(draw, x, y, color, size): - """Draws a hollow dot (circle) on a PIL Image at the specified coordinate. - - Args: - draw: An ImageDraw object from PIL. - x: The x-coordinate of the center of the dot. - y: The y-coordinate of the center of the dot. - color: A color value that PIL understands (e.g., "red", (255, 0, 0), "#FF0000"). This is the outline color. - size: The radius of the dot (in pixels). - """ - bounding_box = (x - size, y - size, x + size, y + size) - draw.ellipse(bounding_box, outline=color, width=2) + draw.ellipse(bounding_box, fill=fill_color, outline=outline_color) def draw_bounding_boxes( - image: Image.Image | bytes, detection: dict, draw: ImageDraw.ImageDraw = None, draw_centroid: bool = False + image: Image.Image | bytes, + detection: dict, + draw: ImageDraw.ImageDraw = None, + shape: Shape = Shape.RECTANGLE, ) -> Image.Image | None: """Draw bounding boxes on an image using PIL. @@ -120,7 +115,7 @@ def draw_bounding_boxes( detection (dict): A dictionary containing detection results with keys 'class_name', 'bounding_box_xyxy', and 'confidence'. draw (ImageDraw.ImageDraw, optional): An existing ImageDraw object to use. If None, a new one is created. - draw_centroid (bool, optional): If True, draws a dot at the centroid of the bounding box instead of the box + shape (Shape, optional): Shape of the bounding box. Defaults to rectangle. itself. Defaults to False. """ if isinstance(image, bytes): @@ -134,6 +129,10 @@ def draw_bounding_boxes( if not detection or "detection" not in detection: return None + if shape not in (Shape.RECTANGLE, Shape.CIRCLE): + logger.warning(f"Unsupported shape '{shape}'. Defaulting to rectangle.") + shape = Shape.RECTANGLE + detection = detection["detection"] # Scale font size and box thickness based on image size and number of detections @@ -181,17 +180,15 @@ def draw_bounding_boxes( x2_text = x1 + text_width + label_hpad * 2 # Draw bounding box - if draw_centroid: - centroid_x = int((x1 + x2) / 2) - centroid_y = int((y1 + y2) / 2) - draw_hollow_dot(draw, centroid_x, centroid_y, box_color, size=10) + if shape == Shape.CIRCLE: + draw_dot(draw, int((x1 + x2) / 2), int((y1 + y2) / 2), 10, outline_color=box_color) else: - draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_thickness) + draw.rectangle((x1, y1, x2, y2), outline=box_color, width=box_thickness) # Draw label background (dark gray, semi-transparent) on overlay label_bg_color = (0, 0, 0, 128) overlay = Image.new("RGBA", image_box.size, (0, 0, 0, 0)) overlay_draw = ImageDraw.Draw(overlay) - overlay_draw.rectangle([x1, y1_text, x2_text, y2_text], fill=label_bg_color, outline=None) + overlay_draw.rectangle((x1, y1_text, x2_text, y2_text), fill=label_bg_color, outline=None) image_box = image_box.convert("RGBA") image_box = Image.alpha_composite(image_box, overlay) draw = ImageDraw.Draw(image_box) @@ -204,7 +201,6 @@ def draw_bounding_boxes( def draw_anomaly_markers( image: Image.Image | bytes, detection: dict, - draw: ImageDraw.ImageDraw = None, ) -> Image.Image | None: """Draw bounding boxes on an image using PIL. @@ -214,10 +210,6 @@ def draw_anomaly_markers( image (Image.Image|bytes): The image to draw on, can be a PIL Image or bytes. detection (dict): A dictionary containing detection results with keys 'class_name', 'bounding_box_xyxy', and 'score'. - draw (ImageDraw.ImageDraw, optional): An existing ImageDraw object to use. If None, a new one is created. - label_above_box (bool, optional): If True, labels are drawn above the bounding box. Defaults to False. - colours (list, optional): List of colors to use for bounding boxes. Defaults to a predefined palette. - text_color (str, optional): Color of the text labels. Defaults to "white". """ if isinstance(image, bytes): image_box = Image.open(io.BytesIO(image)) @@ -227,9 +219,6 @@ def draw_anomaly_markers( if image_box.mode != "RGBA": image_box = image_box.convert("RGBA") - if draw is None: - draw = ImageDraw.Draw(image_box) - max_anomaly_score = detection.get("anomaly_max_score", 0.0) if not detection or "detection" not in detection: @@ -262,10 +251,8 @@ def draw_anomaly_markers( temp_layer = Image.new("RGBA", image_box.size, (0, 0, 0, 0)) temp_draw = ImageDraw.Draw(temp_layer) - temp_draw.rectangle([x1, y1, x2, y2], fill=fill_color_with_alpha) - temp_draw.rectangle([x1, y1, x2, y2], outline=outline_color, width=box_thickness) + temp_draw.rectangle((x1, y1, x2, y2), fill=fill_color_with_alpha) + temp_draw.rectangle((x1, y1, x2, y2), outline=outline_color, width=box_thickness) image_box = Image.alpha_composite(image_box, temp_layer) - draw = ImageDraw.Draw(image_box) - return image_box diff --git a/tests/arduino/app_bricks/objectdetection/test_objectdetection.py b/tests/arduino/app_bricks/objectdetection/test_objectdetection.py index f98f779f..da041ab6 100644 --- a/tests/arduino/app_bricks/objectdetection/test_objectdetection.py +++ b/tests/arduino/app_bricks/objectdetection/test_objectdetection.py @@ -9,6 +9,11 @@ from arduino.app_bricks.object_detection import ObjectDetection +class ModelInfo: + def __init__(self, model_type: str): + self.model_type = model_type + + @pytest.fixture(autouse=True) def mock_dependencies(monkeypatch: pytest.MonkeyPatch): """Mock external dependencies in __init__. @@ -19,6 +24,7 @@ def mock_dependencies(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr("arduino.app_internal.core.load_brick_compose_file", lambda cls: fake_compose) monkeypatch.setattr("arduino.app_internal.core.resolve_address", lambda host: "127.0.0.1") monkeypatch.setattr("arduino.app_internal.core.parse_docker_compose_variable", lambda x: [(None, None), (None, "8100")]) + monkeypatch.setattr("arduino.app_bricks.object_detection.ObjectDetection.get_model_info", lambda self: ModelInfo("object-detection")) @pytest.fixture From f9eaaefd06a3eb29db9200f1d284cb231af9a73d Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 14:49:04 +0100 Subject: [PATCH 07/11] set width --- src/arduino/app_utils/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 0c7ccbb6..4fcc32ce 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -97,7 +97,7 @@ def draw_dot(draw, x, y, size, fill_color=None, outline_color=None): """ # Calculate the bounding box for the circle bounding_box = (x - size, y - size, x + size, y + size) - draw.ellipse(bounding_box, fill=fill_color, outline=outline_color) + draw.ellipse(bounding_box, fill=fill_color, outline=outline_color, width=2) def draw_bounding_boxes( From 77a2c649d1383d630a16354ebcfd6cbc011a44ec Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 15:14:41 +0100 Subject: [PATCH 08/11] remove method --- src/arduino/app_utils/image.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 4fcc32ce..3f5b21a2 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -84,22 +84,6 @@ def get_image_bytes(image: str | Image.Image | bytes) -> bytes | None: return None -def draw_dot(draw, x, y, size, fill_color=None, outline_color=None): - """Draws a large colored dot on a PIL Image at the specified coordinate. - - Args: - draw: An ImageDraw object from PIL. - x: The x-coordinate of the center of the dot. - y: The y-coordinate of the center of the dot. - size: The radius of the dot (in pixels). - fill_color: The fill color of the dot. Default is None (no fill). - outline_color: The outline color of the dot. Default is None (no outline). - """ - # Calculate the bounding box for the circle - bounding_box = (x - size, y - size, x + size, y + size) - draw.ellipse(bounding_box, fill=fill_color, outline=outline_color, width=2) - - def draw_bounding_boxes( image: Image.Image | bytes, detection: dict, @@ -181,7 +165,10 @@ def draw_bounding_boxes( # Draw bounding box if shape == Shape.CIRCLE: - draw_dot(draw, int((x1 + x2) / 2), int((y1 + y2) / 2), 10, outline_color=box_color) + center_x = int((x1 + x2) / 2) + center_y = int((y1 + y2) / 2) + bounding_box = (center_x - 10, center_y - 10, center_x + 10, center_y + 10) + draw.ellipse(bounding_box, outline=box_color, width=2) else: draw.rectangle((x1, y1, x2, y2), outline=box_color, width=box_thickness) # Draw label background (dark gray, semi-transparent) on overlay From 018cc6835009a9cbc506f47e496e5791a63d492e Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 15:30:13 +0100 Subject: [PATCH 09/11] set radius --- src/arduino/app_utils/image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index 3f5b21a2..e02663fb 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -167,7 +167,8 @@ def draw_bounding_boxes( if shape == Shape.CIRCLE: center_x = int((x1 + x2) / 2) center_y = int((y1 + y2) / 2) - bounding_box = (center_x - 10, center_y - 10, center_x + 10, center_y + 10) + radius = 10 + bounding_box = (center_x - radius, center_y - radius, center_x + radius, center_y + radius) draw.ellipse(bounding_box, outline=box_color, width=2) else: draw.rectangle((x1, y1, x2, y2), outline=box_color, width=box_thickness) From 167ec20d113065688718bbfaba864ca235bb2bf3 Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 15:36:48 +0100 Subject: [PATCH 10/11] add check --- src/arduino/app_bricks/object_detection/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/arduino/app_bricks/object_detection/__init__.py b/src/arduino/app_bricks/object_detection/__init__.py index 09697afa..04fb018a 100644 --- a/src/arduino/app_bricks/object_detection/__init__.py +++ b/src/arduino/app_bricks/object_detection/__init__.py @@ -75,7 +75,11 @@ def draw_bounding_boxes(self, image: Image.Image | bytes, detections: dict) -> I Returns: Image with bounding boxes and key points drawn. + None if input image or detections are invalid. """ + if not image or not detections: + return None + shape = None if self._model_info.model_type == "object_detection": shape = Shape.RECTANGLE From e89639a1cec748ec213af24c83d73ec5c7be619f Mon Sep 17 00:00:00 2001 From: Stefano Torneo Date: Tue, 18 Nov 2025 17:15:43 +0100 Subject: [PATCH 11/11] revert change --- src/arduino/app_utils/image.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/arduino/app_utils/image.py b/src/arduino/app_utils/image.py index e02663fb..48f2f25c 100644 --- a/src/arduino/app_utils/image.py +++ b/src/arduino/app_utils/image.py @@ -189,6 +189,7 @@ def draw_bounding_boxes( def draw_anomaly_markers( image: Image.Image | bytes, detection: dict, + draw: ImageDraw.ImageDraw = None, ) -> Image.Image | None: """Draw bounding boxes on an image using PIL. @@ -198,6 +199,10 @@ def draw_anomaly_markers( image (Image.Image|bytes): The image to draw on, can be a PIL Image or bytes. detection (dict): A dictionary containing detection results with keys 'class_name', 'bounding_box_xyxy', and 'score'. + draw (ImageDraw.ImageDraw, optional): An existing ImageDraw object to use. If None, a new one is created. + label_above_box (bool, optional): If True, labels are drawn above the bounding box. Defaults to False. + colours (list, optional): List of colors to use for bounding boxes. Defaults to a predefined palette. + text_color (str, optional): Color of the text labels. Defaults to "white". """ if isinstance(image, bytes): image_box = Image.open(io.BytesIO(image)) @@ -207,6 +212,9 @@ def draw_anomaly_markers( if image_box.mode != "RGBA": image_box = image_box.convert("RGBA") + if draw is None: + draw = ImageDraw.Draw(image_box) + max_anomaly_score = detection.get("anomaly_max_score", 0.0) if not detection or "detection" not in detection: @@ -239,8 +247,10 @@ def draw_anomaly_markers( temp_layer = Image.new("RGBA", image_box.size, (0, 0, 0, 0)) temp_draw = ImageDraw.Draw(temp_layer) - temp_draw.rectangle((x1, y1, x2, y2), fill=fill_color_with_alpha) - temp_draw.rectangle((x1, y1, x2, y2), outline=outline_color, width=box_thickness) + temp_draw.rectangle([x1, y1, x2, y2], fill=fill_color_with_alpha) + temp_draw.rectangle([x1, y1, x2, y2], outline=outline_color, width=box_thickness) image_box = Image.alpha_composite(image_box, temp_layer) + draw = ImageDraw.Draw(image_box) + return image_box