From f433ed79b6ba8e22a447b82c82d08ff3dd34d4ce Mon Sep 17 00:00:00 2001 From: Lillian Hwang-Geddes Date: Fri, 21 Nov 2025 09:51:16 -0500 Subject: [PATCH 1/6] change get_image to get_images --- src/motion_detector.py | 25 ++++++++++++++++++++----- tests/fakecam.py | 3 ++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/motion_detector.py b/src/motion_detector.py index 4b23d79..12bbb85 100644 --- a/src/motion_detector.py +++ b/src/motion_detector.py @@ -172,7 +172,10 @@ async def get_classifications( **kwargs, ) -> List[Classification]: # Grab and grayscale 2 images - input1 = await self.camera.get_image(mime_type=CameraMimeType.JPEG) + images = await self.camera.get_images() + if len(images) == 0: + raise ValueError("No images returned by get_images") + input1 = images[0] if input1.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: raise ValueError( "image mime type must be PNG or JPEG, not ", input1.mime_type @@ -181,7 +184,10 @@ async def get_classifications( img1, _, _ = self.crop_image(img1) gray1 = cv2.cvtColor(np.array(img1), cv2.COLOR_BGR2GRAY) - input2 = await self.camera.get_image() + camera_images = await self.camera.get_images() + if len(camera_images) == 0: + raise ValueError("No images were returned by get_images") + input2 = camera_images[0] if input2.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: raise ValueError( "image mime type must be PNG or JPEG, not ", input2.mime_type @@ -222,7 +228,10 @@ async def get_detections( **kwargs, ) -> List[Detection]: # Grab and grayscale 2 images - input1 = await self.camera.get_image(mime_type=CameraMimeType.JPEG) + images = await self.camera.get_images() + if len(images) == 0: + raise ValueError("No images returned by get_images") + input1 = images[0] if input1.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: raise ValueError( "image mime type must be PNG or JPEG, not ", input1.mime_type @@ -231,7 +240,10 @@ async def get_detections( img1, width, height = self.crop_image(img1) gray1 = cv2.cvtColor(np.array(img1), cv2.COLOR_BGR2GRAY) - input2 = await self.camera.get_image() + camera_images = await self.camera.get_images() + if len(camera_images) == 0: + raise ValueError("No images were returned by get_images") + input2 = camera_images[0] if input2.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: raise ValueError( "image mime type must be PNG or JPEG, not ", input2.mime_type @@ -302,7 +314,10 @@ async def capture_all_from_camera( # pylint: disable=too-many-positional-argume "is not the configured 'cam_name':", self.cam_name, ) - img = await self.camera.get_image(mime_type=CameraMimeType.JPEG) + imgs = await self.camera.get_images() + if len(imgs) == 0 and (return_image or return_classifications or return_detections): + raise ValueError("No images returned by get_images") + img = imgs[0] if return_image: result.image = img if return_classifications: diff --git a/tests/fakecam.py b/tests/fakecam.py index 37600be..91fdc8c 100644 --- a/tests/fakecam.py +++ b/tests/fakecam.py @@ -22,7 +22,8 @@ async def get_image(self, mime_type: str = "") -> Coroutine[Any, Any, ViamImage] return pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG) async def get_images(self) -> Coroutine[Any, Any, Tuple[List[NamedImage] | ResponseMetadata]]: - raise NotImplementedError + self.count +=1 + return [pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG)] async def get_properties(self) -> Coroutine[Any, Any, GetPropertiesResponse]: raise NotImplementedError From d19f33943834f6043de4b6b048686b9f289b70a2 Mon Sep 17 00:00:00 2001 From: Lillian Hwang-Geddes Date: Fri, 21 Nov 2025 11:47:22 -0500 Subject: [PATCH 2/6] tests --- tests/fakecam.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/fakecam.py b/tests/fakecam.py index 91fdc8c..200a9b6 100644 --- a/tests/fakecam.py +++ b/tests/fakecam.py @@ -22,8 +22,9 @@ async def get_image(self, mime_type: str = "") -> Coroutine[Any, Any, ViamImage] return pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG) async def get_images(self) -> Coroutine[Any, Any, Tuple[List[NamedImage] | ResponseMetadata]]: - self.count +=1 - return [pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG)] + #self.count +=1 + #return [pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG)] + raise NotImplementedError async def get_properties(self) -> Coroutine[Any, Any, GetPropertiesResponse]: raise NotImplementedError From 35fd2b80f6ef67c9b104a4754d9804d14e0f4fd5 Mon Sep 17 00:00:00 2001 From: Lillian Hwang-Geddes Date: Fri, 21 Nov 2025 14:01:51 -0500 Subject: [PATCH 3/6] add back fake camera --- tests/fakecam.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/fakecam.py b/tests/fakecam.py index 200a9b6..91fdc8c 100644 --- a/tests/fakecam.py +++ b/tests/fakecam.py @@ -22,9 +22,8 @@ async def get_image(self, mime_type: str = "") -> Coroutine[Any, Any, ViamImage] return pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG) async def get_images(self) -> Coroutine[Any, Any, Tuple[List[NamedImage] | ResponseMetadata]]: - #self.count +=1 - #return [pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG)] - raise NotImplementedError + self.count +=1 + return [pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG)] async def get_properties(self) -> Coroutine[Any, Any, GetPropertiesResponse]: raise NotImplementedError From f2c7d8b977e32982f3c705ed4c880d6631d7bffa Mon Sep 17 00:00:00 2001 From: Lillian Hwang-Geddes Date: Fri, 21 Nov 2025 14:24:47 -0500 Subject: [PATCH 4/6] validate config --- src/motion_detector.py | 6 +++--- tests/test_motiondetector.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/motion_detector.py b/src/motion_detector.py index 12bbb85..f9cbc1c 100644 --- a/src/motion_detector.py +++ b/src/motion_detector.py @@ -1,5 +1,5 @@ import math -from typing import Any, ClassVar, Dict, List, Mapping, Optional, Sequence +from typing import Any, ClassVar, Dict, List, Mapping, Optional, Sequence, Tuple import cv2 import numpy as np @@ -51,7 +51,7 @@ def new_service( # Validates JSON Configuration @classmethod - def validate_config(cls, config: ServiceConfig) -> Sequence[str]: + def validate_config(cls, config: ServiceConfig) -> Tuple[Sequence[str], Sequence[str]]: validate_cam_name = config.attributes.fields["cam_name"].string_value validate_camera_name = config.attributes.fields["camera_name"].string_value @@ -126,7 +126,7 @@ def validate_config(cls, config: ServiceConfig) -> Sequence[str]: raise ValueError("x1_rel should be less than x2_rel") if y1_rel > y2_rel: raise ValueError("y1_rel should be less than y2_rel") - return [source_cam] + return [source_cam], [] # Handles attribute reconfiguration def reconfigure( diff --git a/tests/test_motiondetector.py b/tests/test_motiondetector.py index 3d955b5..e882338 100644 --- a/tests/test_motiondetector.py +++ b/tests/test_motiondetector.py @@ -44,7 +44,7 @@ def test_empty(self): with pytest.raises( ValueError, match=pytest.source_camera_name_none_defined_error_message ): - response = md.validate_config(config=empty_config) + response, _ = md.validate_config(config=empty_config) # For each way to specify a valid min/max size, have a test that checks it's valid. @parameterized.expand( @@ -74,7 +74,7 @@ def test_valid(self, unused_test_name, extra_config_values): raw_config = {"cam_name": "test"} raw_config.update(extra_config_values) config = make_component_config(raw_config) - response = md.validate_config(config=config) + response, _ = md.validate_config(config=config) assert response == ["test"] # For each type of invalid config, test that the expected error is raised. @@ -120,7 +120,7 @@ def test_invalid(self, error_message, extra_config_values): raw_config.update(extra_config_values) config = make_component_config(raw_config) with pytest.raises(ValueError, match=error_message): - response = md.validate_config(config=config) + response, _ = md.validate_config(config=config) def test_empty_config_name(self): md = getMD() @@ -129,7 +129,7 @@ def test_empty_config_name(self): with pytest.raises( ValueError, match=pytest.source_camera_name_none_defined_error_message ): - response = md.validate_config(config=config) + response, _ = md.validate_config(config=config) # For each way to specify a valid camera name, test that the return is valid. @parameterized.expand( @@ -141,7 +141,7 @@ def test_empty_config_name(self): def test_valid_camera_names(self, unused_test_name, cam_config): md = getMD() config = make_component_config(cam_config) - response = md.validate_config(config=config) + response, _ = md.validate_config(config=config) assert response == ["test"] # For each way to spedify an invalid camera name, test that the expected error is raised. @@ -168,7 +168,7 @@ def test_invalid_camera_names(self, unused_test_name, cam_config, error_message) md = getMD() config = make_component_config(cam_config) with pytest.raises(ValueError, match=error_message): - response = md.validate_config(config=config) + response, _ = md.validate_config(config=config) class TestMotionDetector: From cb055c5883e1c7514558ff1a4bd9eaa49869d8e1 Mon Sep 17 00:00:00 2001 From: Lillian Hwang-Geddes Date: Tue, 2 Dec 2025 12:06:34 -0500 Subject: [PATCH 5/6] take into account metadata --- src/motion_detector.py | 21 +++++++++++---------- tests/fakecam.py | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/motion_detector.py b/src/motion_detector.py index f9cbc1c..a5012ad 100644 --- a/src/motion_detector.py +++ b/src/motion_detector.py @@ -172,8 +172,8 @@ async def get_classifications( **kwargs, ) -> List[Classification]: # Grab and grayscale 2 images - images = await self.camera.get_images() - if len(images) == 0: + images, _ = await self.camera.get_images() + if images is None or len(images) == 0: raise ValueError("No images returned by get_images") input1 = images[0] if input1.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: @@ -184,8 +184,8 @@ async def get_classifications( img1, _, _ = self.crop_image(img1) gray1 = cv2.cvtColor(np.array(img1), cv2.COLOR_BGR2GRAY) - camera_images = await self.camera.get_images() - if len(camera_images) == 0: + camera_images, _ = await self.camera.get_images() + if camera_images is None or len(camera_images) == 0: raise ValueError("No images were returned by get_images") input2 = camera_images[0] if input2.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: @@ -228,8 +228,8 @@ async def get_detections( **kwargs, ) -> List[Detection]: # Grab and grayscale 2 images - images = await self.camera.get_images() - if len(images) == 0: + images, _ = await self.camera.get_images() + if images is None or len(images) == 0: raise ValueError("No images returned by get_images") input1 = images[0] if input1.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: @@ -240,8 +240,8 @@ async def get_detections( img1, width, height = self.crop_image(img1) gray1 = cv2.cvtColor(np.array(img1), cv2.COLOR_BGR2GRAY) - camera_images = await self.camera.get_images() - if len(camera_images) == 0: + camera_images, _ = await self.camera.get_images() + if camera_images is None or len(camera_images) == 0: raise ValueError("No images were returned by get_images") input2 = camera_images[0] if input2.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: @@ -314,8 +314,9 @@ async def capture_all_from_camera( # pylint: disable=too-many-positional-argume "is not the configured 'cam_name':", self.cam_name, ) - imgs = await self.camera.get_images() - if len(imgs) == 0 and (return_image or return_classifications or return_detections): + imgs, _ = await self.camera.get_images() + if (imgs is None or len(imgs) == 0) and \ + (return_image or return_classifications or return_detections): raise ValueError("No images returned by get_images") img = imgs[0] if return_image: diff --git a/tests/fakecam.py b/tests/fakecam.py index 91fdc8c..ddca911 100644 --- a/tests/fakecam.py +++ b/tests/fakecam.py @@ -23,7 +23,7 @@ async def get_image(self, mime_type: str = "") -> Coroutine[Any, Any, ViamImage] async def get_images(self) -> Coroutine[Any, Any, Tuple[List[NamedImage] | ResponseMetadata]]: self.count +=1 - return [pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG)] + return [pil.pil_to_viam_image(self.images[self.count%2], CameraMimeType.JPEG)], None async def get_properties(self) -> Coroutine[Any, Any, GetPropertiesResponse]: raise NotImplementedError From aec3af624655b59ae97f31181b3aa3ea6d16528d Mon Sep 17 00:00:00 2001 From: Lillian Hwang-Geddes Date: Wed, 17 Dec 2025 14:35:54 -0500 Subject: [PATCH 6/6] change variable name --- src/motion_detector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/motion_detector.py b/src/motion_detector.py index a5012ad..f90ecf8 100644 --- a/src/motion_detector.py +++ b/src/motion_detector.py @@ -240,10 +240,10 @@ async def get_detections( img1, width, height = self.crop_image(img1) gray1 = cv2.cvtColor(np.array(img1), cv2.COLOR_BGR2GRAY) - camera_images, _ = await self.camera.get_images() - if camera_images is None or len(camera_images) == 0: + camera_images2, _ = await self.camera.get_images() + if camera_images2 is None or len(camera_images2) == 0: raise ValueError("No images were returned by get_images") - input2 = camera_images[0] + input2 = camera_images2[0] if input2.mime_type not in [CameraMimeType.JPEG, CameraMimeType.PNG]: raise ValueError( "image mime type must be PNG or JPEG, not ", input2.mime_type