3232import time
3333import sys
3434
35- from . import connection , util
35+ from . import annotate , connection , util
3636from .exceptions import VectorCameraFeedException
3737from .messaging import protocol
3838
@@ -60,6 +60,78 @@ def _convert_to_pillow_image(image_data: bytes) -> Image.Image:
6060 return image
6161
6262
63+ class CameraImage :
64+ """A single image from the robot's camera.
65+ This wraps a raw image and provides an :meth:`annotate_image` method
66+ that can resize and add dynamic annotations to the image, such as
67+ marking up the location of objects and faces.
68+
69+ .. testcode::
70+
71+ import anki_vector
72+
73+ with anki_vector.Robot() as robot:
74+ image = robot.camera.capture_single_image()
75+ print(f"Displaying image with id {image.image_id}, received at {image.image_recv_time}")
76+ image.raw_image.show()
77+
78+ :param raw_image: The raw unprocessed image from the camera.
79+ :param image_annotator: The image annotation object.
80+ :param image_id: An image number that increments on every new image received.
81+ """
82+
83+ def __init__ (self , raw_image : Image .Image , image_annotator : annotate .ImageAnnotator , image_id : int ):
84+
85+ self ._raw_image = raw_image
86+ self ._image_annotator = image_annotator
87+ self ._image_id = image_id
88+ self ._image_recv_time = time .time ()
89+
90+ @property
91+ def raw_image (self ) -> Image .Image :
92+ """The raw unprocessed image from the camera."""
93+ return self ._raw_image
94+
95+ @property
96+ def image_id (self ) -> int :
97+ """An image number that increments on every new image received."""
98+ return self ._image_id
99+
100+ @property
101+ def image_recv_time (self ) -> float :
102+ """The time the image was received and processed by the SDK."""
103+ return self ._image_recv_time
104+
105+ def annotate_image (self , scale : float = None , fit_size : tuple = None , resample_mode : int = annotate .RESAMPLE_MODE_NEAREST ) -> Image .Image :
106+ """Adds any enabled annotations to the image.
107+ Optionally resizes the image prior to annotations being applied. The
108+ aspect ratio of the resulting image always matches that of the raw image.
109+
110+ .. testcode::
111+
112+ import anki_vector
113+
114+ with anki_vector.Robot() as robot:
115+ image = robot.camera.capture_single_image()
116+ annotated_image = image.annotate_image()
117+ annotated_image.show()
118+
119+ :param scale: If set then the base image will be scaled by the
120+ supplied multiplier. Cannot be combined with fit_size
121+ :param fit_size: If set, then scale the image to fit inside
122+ the supplied (width, height) dimensions. The original aspect
123+ ratio will be preserved. Cannot be combined with scale.
124+ :param resample_mode: The resampling mode to use when scaling the
125+ image. Should be either :attr:`~anki_vector.annotate.RESAMPLE_MODE_NEAREST`
126+ (fast) or :attr:`~anki_vector.annotate.RESAMPLE_MODE_BILINEAR` (slower,
127+ but smoother).
128+ """
129+ return self ._image_annotator .annotate_image (self ._raw_image ,
130+ scale = scale ,
131+ fit_size = fit_size ,
132+ resample_mode = resample_mode )
133+
134+
63135class CameraComponent (util .Component ):
64136 """Represents Vector's camera.
65137
@@ -75,22 +147,27 @@ class CameraComponent(util.Component):
75147 with anki_vector.Robot() as robot:
76148 robot.camera.init_camera_feed()
77149 image = robot.camera.latest_image
78- image.show()
150+ image.raw_image. show()
79151
80152 :param robot: A reference to the owner Robot object.
81153 """
82154
155+ #: callable: The factory function that returns an
156+ #: :class:`annotate.ImageAnnotator` class or subclass instance.
157+ annotator_factory = annotate .ImageAnnotator
158+
83159 def __init__ (self , robot ):
84160 super ().__init__ (robot )
85161
86- self ._latest_image : Image .Image = None
162+ self ._image_annotator : annotate .ImageAnnotator = self .annotator_factory (self .robot .world )
163+ self ._latest_image : CameraImage = None
87164 self ._latest_image_id : int = None
88165 self ._camera_feed_task : asyncio .Task = None
89166 self ._enabled = False
90167
91168 @property
92169 @util .block_while_none ()
93- def latest_image (self ) -> Image . Image :
170+ def latest_image (self ) -> CameraImage :
94171 """:class:`Image.Image`: The most recently processed image received from the robot.
95172
96173 The resolution of latest_image is 640x360.
@@ -104,7 +181,7 @@ def latest_image(self) -> Image.Image:
104181 with anki_vector.Robot() as robot:
105182 robot.camera.init_camera_feed()
106183 image = robot.camera.latest_image
107- image.show()
184+ image.raw_image. show()
108185 """
109186 if not self ._camera_feed_task :
110187 raise VectorCameraFeedException ()
@@ -126,13 +203,31 @@ def latest_image_id(self) -> int:
126203 with anki_vector.Robot() as robot:
127204 robot.camera.init_camera_feed()
128205 image = robot.camera.latest_image
129- image.show()
206+ image.raw_image. show()
130207 print(f"latest_image_id: {robot.camera.latest_image_id}")
131208 """
132209 if not self ._camera_feed_task :
133210 raise VectorCameraFeedException ()
134211 return self ._latest_image_id
135212
213+ @property
214+ def image_annotator (self ) -> annotate .ImageAnnotator :
215+ """The image annotator used to add annotations to the raw camera images.
216+
217+ .. testcode::
218+
219+ import time
220+ import anki_vector
221+
222+ with anki_vector.Robot(show_viewer=True) as robot:
223+ # Annotations (enabled by default) are displayed on the camera feed
224+ time.sleep(5)
225+ # Disable all annotations
226+ robot.camera.image_annotator.annotation_enabled = False
227+ time.sleep(5)
228+ """
229+ return self ._image_annotator
230+
136231 def init_camera_feed (self ) -> None :
137232 """Begin camera feed task.
138233
@@ -143,7 +238,7 @@ def init_camera_feed(self) -> None:
143238 with anki_vector.Robot() as robot:
144239 robot.camera.init_camera_feed()
145240 image = robot.camera.latest_image
146- image.show()
241+ image.raw_image. show()
147242 """
148243 if not self ._camera_feed_task or self ._camera_feed_task .done ():
149244 self ._enabled = True
@@ -203,11 +298,14 @@ def image_streaming_enabled(self) -> bool:
203298 def _unpack_image (self , msg : protocol .CameraFeedResponse ) -> None :
204299 """Processes raw data from the robot into a more useful image structure."""
205300 image = _convert_to_pillow_image (msg .data )
206- self .robot .viewer .enqueue_frame (image )
207301
208- self ._latest_image = image
302+ self ._latest_image = CameraImage ( image , self . _image_annotator , msg . image_id )
209303 self ._latest_image_id = msg .image_id
210304
305+ if self ._image_annotator .annotation_enabled :
306+ image = self ._image_annotator .annotate_image (image )
307+ self .robot .viewer .enqueue_frame (image )
308+
211309 async def _request_and_handle_images (self ) -> None :
212310 """Queries and listens for camera feed events from the robot.
213311 Received events are parsed by a helper function."""
@@ -224,7 +322,7 @@ async def _request_and_handle_images(self) -> None:
224322 self .logger .debug ('Camera feed task was cancelled. This is expected during disconnection.' )
225323
226324 @connection .on_connection_thread ()
227- async def capture_single_image (self ) -> Image . Image :
325+ async def capture_single_image (self ) -> CameraImage :
228326 """Request to capture a single image from the robot's camera.
229327
230328 This call requests the robot to capture an image and returns the
@@ -239,12 +337,14 @@ async def capture_single_image(self) -> Image.Image:
239337
240338 with anki_vector.Robot() as robot:
241339 image = robot.camera.capture_single_image()
242- image.show()
340+ image.raw_image. show()
243341 """
244342 if self ._enabled :
245343 return self ._latest_image
246344 req = protocol .CaptureSingleImageRequest ()
247345 res = await self .grpc_interface .CaptureSingleImage (req )
248346 if res and res .data :
249- return _convert_to_pillow_image (res .data )
347+ image = _convert_to_pillow_image (res .data )
348+ return CameraImage (image , self ._image_annotator , res .image_id )
349+
250350 self .logger .error ('Failed to capture a single image' )
0 commit comments