wirepod-vector-sdk-audio 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anki_vector/__init__.py +43 -0
- anki_vector/animation.py +272 -0
- anki_vector/annotate.py +590 -0
- anki_vector/audio.py +212 -0
- anki_vector/audio_stream.py +335 -0
- anki_vector/behavior.py +1135 -0
- anki_vector/camera.py +670 -0
- anki_vector/camera_viewer/__init__.py +121 -0
- anki_vector/color.py +88 -0
- anki_vector/configure/__main__.py +331 -0
- anki_vector/connection.py +838 -0
- anki_vector/events.py +420 -0
- anki_vector/exceptions.py +185 -0
- anki_vector/faces.py +819 -0
- anki_vector/lights.py +210 -0
- anki_vector/mdns.py +131 -0
- anki_vector/messaging/__init__.py +45 -0
- anki_vector/messaging/alexa_pb2.py +36 -0
- anki_vector/messaging/alexa_pb2_grpc.py +3 -0
- anki_vector/messaging/behavior_pb2.py +40 -0
- anki_vector/messaging/behavior_pb2_grpc.py +3 -0
- anki_vector/messaging/client.py +33 -0
- anki_vector/messaging/cube_pb2.py +113 -0
- anki_vector/messaging/cube_pb2_grpc.py +3 -0
- anki_vector/messaging/extensions_pb2.py +25 -0
- anki_vector/messaging/extensions_pb2_grpc.py +3 -0
- anki_vector/messaging/external_interface_pb2.py +169 -0
- anki_vector/messaging/external_interface_pb2_grpc.py +1267 -0
- anki_vector/messaging/messages_pb2.py +431 -0
- anki_vector/messaging/messages_pb2_grpc.py +3 -0
- anki_vector/messaging/nav_map_pb2.py +33 -0
- anki_vector/messaging/nav_map_pb2_grpc.py +3 -0
- anki_vector/messaging/protocol.py +33 -0
- anki_vector/messaging/response_status_pb2.py +27 -0
- anki_vector/messaging/response_status_pb2_grpc.py +3 -0
- anki_vector/messaging/settings_pb2.py +72 -0
- anki_vector/messaging/settings_pb2_grpc.py +3 -0
- anki_vector/messaging/shared_pb2.py +54 -0
- anki_vector/messaging/shared_pb2_grpc.py +3 -0
- anki_vector/motors.py +127 -0
- anki_vector/nav_map.py +409 -0
- anki_vector/objects.py +1782 -0
- anki_vector/opengl/__init__.py +103 -0
- anki_vector/opengl/assets/LICENSE.txt +21 -0
- anki_vector/opengl/assets/cube.jpg +0 -0
- anki_vector/opengl/assets/cube.mtl +9 -0
- anki_vector/opengl/assets/cube.obj +1000 -0
- anki_vector/opengl/assets/vector.mtl +67 -0
- anki_vector/opengl/assets/vector.obj +13220 -0
- anki_vector/opengl/opengl.py +864 -0
- anki_vector/opengl/opengl_vector.py +620 -0
- anki_vector/opengl/opengl_viewer.py +689 -0
- anki_vector/photos.py +145 -0
- anki_vector/proximity.py +176 -0
- anki_vector/reserve_control/__main__.py +36 -0
- anki_vector/robot.py +930 -0
- anki_vector/screen.py +201 -0
- anki_vector/status.py +322 -0
- anki_vector/touch.py +119 -0
- anki_vector/user_intent.py +186 -0
- anki_vector/util.py +1132 -0
- anki_vector/version.py +15 -0
- anki_vector/viewer.py +403 -0
- anki_vector/vision.py +202 -0
- anki_vector/world.py +899 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/METADATA +80 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/RECORD +71 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/WHEEL +5 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/licenses/LICENSE.txt +180 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/top_level.txt +1 -0
- wirepod_vector_sdk_audio-0.9.0.dist-info/zip-safe +1 -0
anki_vector/annotate.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
# Copyright (c) 2019 Anki, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License in the file LICENSE.txt or at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Camera image annotation.
|
|
16
|
+
|
|
17
|
+
.. image:: ../images/annotate.png
|
|
18
|
+
|
|
19
|
+
This module defines an :class:`ImageAnnotator` class used by
|
|
20
|
+
:class:`anki_vector.camera.CameraImage` and
|
|
21
|
+
:class:`anki_vector.camera.CameraComponent` to add annotations
|
|
22
|
+
to camera images received by the robot.
|
|
23
|
+
|
|
24
|
+
This can include the location of cubes and faces that the robot currently sees,
|
|
25
|
+
along with user-defined custom annotations.
|
|
26
|
+
|
|
27
|
+
The ImageAnnotator instance can be accessed as
|
|
28
|
+
:attr:`anki_vector.camera.CameraComponent.image_annotator`.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# __all__ should order by constants, event classes, other classes, functions.
|
|
32
|
+
__all__ = ['DEFAULT_OBJECT_COLORS',
|
|
33
|
+
'RESAMPLE_MODE_NEAREST', 'RESAMPLE_MODE_BILINEAR',
|
|
34
|
+
'AnnotationPosition', 'ImageText', 'Annotator',
|
|
35
|
+
'ObjectAnnotator', 'FaceAnnotator', 'TextAnnotator', 'ImageAnnotator',
|
|
36
|
+
'add_img_box_to_image', 'add_polygon_to_image', 'annotator']
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
from enum import Enum
|
|
40
|
+
import collections
|
|
41
|
+
import functools
|
|
42
|
+
import sys
|
|
43
|
+
from typing import Callable, Iterable, Tuple, Union
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from PIL import Image, ImageDraw
|
|
47
|
+
import PIL
|
|
48
|
+
except ImportError:
|
|
49
|
+
sys.exit("Cannot import from PIL: Do `pip3 install --user Pillow` to install")
|
|
50
|
+
except SyntaxError:
|
|
51
|
+
sys.exit("SyntaxError: possible if accidentally importing old Python 2 version of PIL")
|
|
52
|
+
|
|
53
|
+
from . import faces
|
|
54
|
+
from . import objects
|
|
55
|
+
from . import util
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
DEFAULT_OBJECT_COLORS = {
|
|
59
|
+
objects.LightCube: 'yellow',
|
|
60
|
+
objects.CustomObject: 'purple',
|
|
61
|
+
'default': 'red'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#: Fastest resampling mode, use nearest pixel
|
|
65
|
+
RESAMPLE_MODE_NEAREST = Image.NEAREST
|
|
66
|
+
#: Slower, but smoother, resampling mode - linear interpolation from 2x2 grid of pixels
|
|
67
|
+
RESAMPLE_MODE_BILINEAR = Image.BILINEAR
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
PILLOW_VERSION = tuple(map(int, PIL.__version__.split('.')))
|
|
71
|
+
|
|
72
|
+
class AnnotationPosition(Enum):
|
|
73
|
+
"""Specifies where the annotation must be rendered."""
|
|
74
|
+
LEFT = 1
|
|
75
|
+
RIGHT = 2
|
|
76
|
+
TOP = 4
|
|
77
|
+
BOTTOM = 8
|
|
78
|
+
|
|
79
|
+
#: Top left position
|
|
80
|
+
TOP_LEFT = TOP | LEFT
|
|
81
|
+
|
|
82
|
+
#: Bottom left position
|
|
83
|
+
BOTTOM_LEFT = BOTTOM | LEFT
|
|
84
|
+
|
|
85
|
+
#: Top right position
|
|
86
|
+
TOP_RIGHT = TOP | RIGHT
|
|
87
|
+
|
|
88
|
+
#: Bottom right position
|
|
89
|
+
BOTTOM_RIGHT = BOTTOM | RIGHT
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ImageText: # pylint: disable=too-few-public-methods
|
|
93
|
+
"""ImageText represents some text that can be applied to an image.
|
|
94
|
+
|
|
95
|
+
The class allows the text to be placed at various positions inside a
|
|
96
|
+
bounding box within the image itself.
|
|
97
|
+
|
|
98
|
+
.. testcode::
|
|
99
|
+
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
from PIL import ImageDraw
|
|
104
|
+
except ImportError:
|
|
105
|
+
sys.exit("run `pip3 install --user Pillow numpy` to run this example")
|
|
106
|
+
|
|
107
|
+
import anki_vector
|
|
108
|
+
from anki_vector import annotate
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Define an annotator using the annotator decorator
|
|
112
|
+
@annotate.annotator
|
|
113
|
+
def clock(image, scale, annotator=None, world=None, **kw):
|
|
114
|
+
d = ImageDraw.Draw(image)
|
|
115
|
+
bounds = (0, 0, image.width, image.height)
|
|
116
|
+
text = annotate.ImageText(time.strftime("%H:%m:%S"),
|
|
117
|
+
position=annotate.AnnotationPosition.TOP_LEFT,
|
|
118
|
+
outline_color="black")
|
|
119
|
+
text.render(d, bounds)
|
|
120
|
+
|
|
121
|
+
with anki_vector.Robot(show_viewer=True, enable_face_detection=True, enable_custom_object_detection=True) as robot:
|
|
122
|
+
robot.camera.image_annotator.add_static_text("text", "Vec-Cam", position=annotate.AnnotationPosition.TOP_RIGHT)
|
|
123
|
+
robot.camera.image_annotator.add_annotator("clock", clock)
|
|
124
|
+
|
|
125
|
+
time.sleep(3)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
:param text: The text to display; may contain newlines
|
|
129
|
+
:param position: Where on the screen to render the text
|
|
130
|
+
- such as AnnotationPosition.TOP_LEFT or AnnotationPosition.BOTTOM_RIGHT
|
|
131
|
+
:param align: Text alignment for multi-line strings
|
|
132
|
+
:param color: Color to use for the text - see :mod:`PIL.ImageColor`
|
|
133
|
+
:param font: ImageFont to use (None for a default font)
|
|
134
|
+
:param line_spacing: The vertical spacing for multi-line strings
|
|
135
|
+
:param outline_color: Color to use for the outline - see
|
|
136
|
+
:mod:`PIL.ImageColor` - use None for no outline.
|
|
137
|
+
:param full_outline: True if the outline should surround the text,
|
|
138
|
+
otherwise a cheaper drop-shadow is displayed. Only relevant if
|
|
139
|
+
outline_color is specified.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, text: str, position: int = AnnotationPosition.BOTTOM_RIGHT, align: str = "left", color: str = "white",
|
|
143
|
+
font=None, line_spacing: int = 3, outline_color: str = None, full_outline: bool = True):
|
|
144
|
+
self.text = text
|
|
145
|
+
self.position = position
|
|
146
|
+
self.align = align
|
|
147
|
+
self.color = color
|
|
148
|
+
self.font = font
|
|
149
|
+
self.line_spacing = line_spacing
|
|
150
|
+
self.outline_color = outline_color
|
|
151
|
+
self.full_outline = full_outline
|
|
152
|
+
|
|
153
|
+
def render(self, draw: ImageDraw.ImageDraw, bounds: tuple) -> ImageDraw.ImageDraw:
|
|
154
|
+
"""Renders the text onto an image within the specified bounding box."""
|
|
155
|
+
|
|
156
|
+
(bx1, by1, bx2, by2) = bounds
|
|
157
|
+
|
|
158
|
+
# Use textsize for Pillow versions before 8.0.0, and textbbox for 8.0.0 and later
|
|
159
|
+
if PILLOW_VERSION < (8, 0, 0):
|
|
160
|
+
text_width, text_height = draw.textsize(self.text, font=self.font)
|
|
161
|
+
else:
|
|
162
|
+
dummy_position = (0, 0) # Dummy position for textbbox calculation
|
|
163
|
+
text_bbox = draw.textbbox(dummy_position, self.text, font=self.font)
|
|
164
|
+
text_width = text_bbox[2] - text_bbox[0]
|
|
165
|
+
text_height = text_bbox[3] - text_bbox[1]
|
|
166
|
+
|
|
167
|
+
if self.position.value & AnnotationPosition.TOP.value:
|
|
168
|
+
y = by1
|
|
169
|
+
else:
|
|
170
|
+
y = by2 - text_height
|
|
171
|
+
|
|
172
|
+
if self.position.value & AnnotationPosition.LEFT.value:
|
|
173
|
+
x = bx1
|
|
174
|
+
else:
|
|
175
|
+
x = bx2 - text_width
|
|
176
|
+
|
|
177
|
+
def _draw_text(pos, color):
|
|
178
|
+
draw.text(pos, self.text, font=self.font, fill=color,
|
|
179
|
+
align=self.align, spacing=self.line_spacing)
|
|
180
|
+
|
|
181
|
+
if self.outline_color is not None:
|
|
182
|
+
if self.full_outline:
|
|
183
|
+
_draw_text((x - 1, y), self.outline_color)
|
|
184
|
+
_draw_text((x + 1, y), self.outline_color)
|
|
185
|
+
_draw_text((x, y - 1), self.outline_color)
|
|
186
|
+
_draw_text((x, y + 1), self.outline_color)
|
|
187
|
+
else:
|
|
188
|
+
_draw_text((x + 1, y + 1), self.outline_color)
|
|
189
|
+
|
|
190
|
+
_draw_text((x, y), self.color)
|
|
191
|
+
|
|
192
|
+
return draw
|
|
193
|
+
|
|
194
|
+
def add_img_box_to_image(draw: ImageDraw.ImageDraw, box: util.ImageRect, color: str, text: Union[ImageText, Iterable[ImageText]] = None) -> None:
|
|
195
|
+
"""Draw a box on an image and optionally add text.
|
|
196
|
+
|
|
197
|
+
This will draw the outline of a rectangle to the passed in image
|
|
198
|
+
in the specified color and optionally add one or more pieces of text
|
|
199
|
+
along the inside edge of the rectangle.
|
|
200
|
+
|
|
201
|
+
:param draw: The drawable surface to write on
|
|
202
|
+
:param box: The ImageBox defining the rectangle to draw
|
|
203
|
+
:param color: A color string suitable for use with PIL - see :mod:`PIL.ImageColor`
|
|
204
|
+
:param text: The text to display - may be a single ImageText instance,
|
|
205
|
+
or any iterable (eg a list of ImageText instances) to display multiple pieces of text.
|
|
206
|
+
"""
|
|
207
|
+
x1, y1 = box.x_top_left, box.y_top_left
|
|
208
|
+
x2, y2 = (box.x_top_left + box.width), (box.y_top_left + box.height)
|
|
209
|
+
draw.rectangle([x1, y1, x2, y2], outline=color)
|
|
210
|
+
if text is not None:
|
|
211
|
+
if isinstance(text, collections.abc.Iterable):
|
|
212
|
+
for t in text:
|
|
213
|
+
t.render(draw, (x1, y1, x2, y2))
|
|
214
|
+
else:
|
|
215
|
+
text.render(draw, (x1, y1, x2, y2))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def add_polygon_to_image(draw: ImageDraw.ImageDraw, poly_points: list, scale: float, line_color: str, fill_color: str = None) -> None:
|
|
219
|
+
"""Draw a polygon on an image
|
|
220
|
+
|
|
221
|
+
This will draw a polygon on the passed-in image in the specified
|
|
222
|
+
colors and scale.
|
|
223
|
+
|
|
224
|
+
:param draw: The drawable surface to write on
|
|
225
|
+
:param poly_points: A sequence of points representing the polygon,
|
|
226
|
+
where each point has float members (x, y)
|
|
227
|
+
:param scale: Scale to multiply each point to match the image scaling
|
|
228
|
+
:param line_color: The color for the outline of the polygon. The string value
|
|
229
|
+
must be a color string suitable for use with PIL - see :mod:`PIL.ImageColor`
|
|
230
|
+
:param fill_color: The color for the inside of the polygon. The string value
|
|
231
|
+
must be a color string suitable for use with PIL - see :mod:`PIL.ImageColor`
|
|
232
|
+
"""
|
|
233
|
+
if len(poly_points) < 2:
|
|
234
|
+
# Need at least 2 points to draw any lines
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Convert poly_points to the PIL format and scale them to the image
|
|
238
|
+
pil_poly_points = []
|
|
239
|
+
for pt in poly_points:
|
|
240
|
+
pil_poly_points.append((pt.x * scale, pt.y * scale))
|
|
241
|
+
|
|
242
|
+
draw.polygon(pil_poly_points, fill=fill_color, outline=line_color)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _find_key_for_cls(d, cls):
|
|
246
|
+
for c in cls.__mro__:
|
|
247
|
+
result = d.get(c, None)
|
|
248
|
+
if result:
|
|
249
|
+
return result
|
|
250
|
+
return d['default']
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class Annotator:
|
|
254
|
+
"""Annotation base class
|
|
255
|
+
|
|
256
|
+
Subclasses of Annotator handle applying a single annotation to an image.
|
|
257
|
+
"""
|
|
258
|
+
#: int: The priority of the annotator - Annotators with higher numbered
|
|
259
|
+
#: priorities are applied first.
|
|
260
|
+
priority = 100
|
|
261
|
+
|
|
262
|
+
def __init__(self, img_annotator, priority=None):
|
|
263
|
+
#: :class:`ImageAnnotator`: The object managing camera annotations
|
|
264
|
+
self.img_annotator = img_annotator
|
|
265
|
+
|
|
266
|
+
#: :class:`~anki_vector.world.World`: The world object for the robot who owns the camera
|
|
267
|
+
self.world = img_annotator.world
|
|
268
|
+
|
|
269
|
+
#: bool: Set enabled to false to prevent the annotator being called
|
|
270
|
+
self.enabled = True
|
|
271
|
+
|
|
272
|
+
if priority is not None:
|
|
273
|
+
self.priority = priority
|
|
274
|
+
|
|
275
|
+
def apply(self, image: Image.Image, scale: float):
|
|
276
|
+
"""Applies the annotation to the image."""
|
|
277
|
+
# should be overriden by a subclass
|
|
278
|
+
raise NotImplementedError()
|
|
279
|
+
|
|
280
|
+
def __hash__(self):
|
|
281
|
+
return id(self)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class ObjectAnnotator(Annotator): # pylint: disable=too-few-public-methods
|
|
285
|
+
"""Adds object annotations to an Image.
|
|
286
|
+
|
|
287
|
+
This handles :class:`anki_vector.objects.LightCube`,
|
|
288
|
+
:class:`anki_vector.objects.Charger` and
|
|
289
|
+
:class:`anki_vector.objects.CustomObject`.
|
|
290
|
+
"""
|
|
291
|
+
priority = 100
|
|
292
|
+
object_colors = DEFAULT_OBJECT_COLORS
|
|
293
|
+
|
|
294
|
+
def __init__(self, img_annotator, object_colors=None):
|
|
295
|
+
super().__init__(img_annotator)
|
|
296
|
+
if object_colors is not None:
|
|
297
|
+
self.object_colors = object_colors
|
|
298
|
+
|
|
299
|
+
def apply(self, image: Image.Image, scale: float) -> None:
|
|
300
|
+
draw = ImageDraw.Draw(image)
|
|
301
|
+
for obj in self.world.visible_objects:
|
|
302
|
+
color = _find_key_for_cls(self.object_colors, obj.__class__)
|
|
303
|
+
text = self._label_for_obj(obj)
|
|
304
|
+
box = obj.last_observed_image_rect
|
|
305
|
+
if scale != 1:
|
|
306
|
+
box.scale_by(scale)
|
|
307
|
+
add_img_box_to_image(draw, box, color, text=text)
|
|
308
|
+
|
|
309
|
+
def _label_for_obj(self, obj): # pylint: disable=no-self-use
|
|
310
|
+
"""Fetch a label to display for the object.
|
|
311
|
+
|
|
312
|
+
Override or replace to customize.
|
|
313
|
+
"""
|
|
314
|
+
return ImageText(obj.descriptive_name)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class FaceAnnotator(Annotator): # pylint: disable=too-few-public-methods
|
|
318
|
+
"""Adds annotations of currently detected faces to a camera image.
|
|
319
|
+
|
|
320
|
+
This handles the display of :class:`anki_vector.faces.Face` objects.
|
|
321
|
+
"""
|
|
322
|
+
priority = 100
|
|
323
|
+
box_color = 'green'
|
|
324
|
+
|
|
325
|
+
def __init__(self, img_annotator, box_color=None):
|
|
326
|
+
super().__init__(img_annotator)
|
|
327
|
+
if box_color is not None:
|
|
328
|
+
self.box_color = box_color
|
|
329
|
+
|
|
330
|
+
def apply(self, image: Image.Image, scale: float) -> None:
|
|
331
|
+
draw = ImageDraw.Draw(image)
|
|
332
|
+
for obj in self.world.visible_faces:
|
|
333
|
+
text = self._label_for_face(obj)
|
|
334
|
+
box = obj.last_observed_image_rect
|
|
335
|
+
if scale != 1:
|
|
336
|
+
box.scale_by(scale)
|
|
337
|
+
add_img_box_to_image(draw, box, self.box_color, text=text)
|
|
338
|
+
add_polygon_to_image(draw, obj.left_eye, scale, self.box_color)
|
|
339
|
+
add_polygon_to_image(draw, obj.right_eye, scale, self.box_color)
|
|
340
|
+
add_polygon_to_image(draw, obj.nose, scale, self.box_color)
|
|
341
|
+
add_polygon_to_image(draw, obj.mouth, scale, self.box_color)
|
|
342
|
+
|
|
343
|
+
def _label_for_face(self, obj): # pylint: disable=no-self-use
|
|
344
|
+
"""Fetch a label to display for the face.
|
|
345
|
+
|
|
346
|
+
Override or replace to customize.
|
|
347
|
+
"""
|
|
348
|
+
label_text = ""
|
|
349
|
+
expression = faces.Expression(obj.expression).name
|
|
350
|
+
|
|
351
|
+
if obj.name:
|
|
352
|
+
label_text = f"Name:{obj.name}"
|
|
353
|
+
if expression != "UNKNOWN":
|
|
354
|
+
label_text += f"\nExpression:{expression}"
|
|
355
|
+
if obj.expression_score:
|
|
356
|
+
# if there is a specific known expression, then also show the score
|
|
357
|
+
# (display a % to make it clear the value is out of 100)
|
|
358
|
+
label_text += f"\nScore:{sum(obj.expression_score)}"
|
|
359
|
+
|
|
360
|
+
return ImageText(label_text + "\n" + f"Face Id:{obj.face_id}")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TextAnnotator(Annotator): # pylint: disable=too-few-public-methods
|
|
364
|
+
"""Adds simple text annotations to a camera image.
|
|
365
|
+
"""
|
|
366
|
+
priority = 50
|
|
367
|
+
|
|
368
|
+
def __init__(self, img_annotator, text):
|
|
369
|
+
super().__init__(img_annotator)
|
|
370
|
+
self.text = text
|
|
371
|
+
|
|
372
|
+
def apply(self, image: Image.Image, scale: int) -> None:
|
|
373
|
+
d = ImageDraw.Draw(image)
|
|
374
|
+
self.text.render(d, (0, 0, image.width, image.height))
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class _AnnotatorHelper(Annotator): # pylint: disable=too-few-public-methods
|
|
378
|
+
def __init__(self, img_annotator, wrapped):
|
|
379
|
+
super().__init__(img_annotator)
|
|
380
|
+
self._wrapped = wrapped
|
|
381
|
+
|
|
382
|
+
def apply(self, image: Image.Image, scale: int) -> None:
|
|
383
|
+
self._wrapped(image, scale, world=self.world, img_annotator=self.img_annotator)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def annotator(f):
|
|
387
|
+
"""A decorator for converting a regular function/method into an Annotator.
|
|
388
|
+
|
|
389
|
+
The wrapped function should have a signature of
|
|
390
|
+
``(image, scale, img_annotator=None, world=None, **kw)``
|
|
391
|
+
"""
|
|
392
|
+
@functools.wraps(f)
|
|
393
|
+
def wrapper(img_annotator):
|
|
394
|
+
return _AnnotatorHelper(img_annotator, f)
|
|
395
|
+
return wrapper
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class ImageAnnotator:
|
|
399
|
+
"""ImageAnnotator applies annotations to the camera image received from the robot.
|
|
400
|
+
|
|
401
|
+
This is instantiated by :class:`anki_vector.world.World` and is accessible as
|
|
402
|
+
:class:`anki_vector.camera.CameraComponent.image_annotator`.
|
|
403
|
+
|
|
404
|
+
By default it defines two active annotators named ``objects`` and ``faces``.
|
|
405
|
+
|
|
406
|
+
The ``objects`` annotator adds a box around each object (such as light cubes)
|
|
407
|
+
that the robot can see. The ``faces`` annotator adds a box around each person's
|
|
408
|
+
face that the robot can recognize.
|
|
409
|
+
|
|
410
|
+
Custom annotations can be defined by calling :meth:`add_annotator` with
|
|
411
|
+
a name of your choosing and an instance of a :class:`Annotator` subclass,
|
|
412
|
+
or use a regular function wrapped with the :func:`annotator` decorator.
|
|
413
|
+
|
|
414
|
+
Individual annotations can be disabled and re-enabled using the
|
|
415
|
+
:meth:`disable_annotator` and :meth:`enable_annotator` methods.
|
|
416
|
+
|
|
417
|
+
All annotations can be disabled by setting the
|
|
418
|
+
:attr:`annotation_enabled` property to False.
|
|
419
|
+
|
|
420
|
+
E.g. to disable face annotations, call
|
|
421
|
+
``robot.camera.image_annotator.disable_annotator('faces')``
|
|
422
|
+
|
|
423
|
+
Annotators each have a priority number associated with them. Annotators
|
|
424
|
+
with a larger priority number are rendered first and may be overdrawn by those
|
|
425
|
+
with a lower/smaller priority number.
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
.. testcode::
|
|
429
|
+
|
|
430
|
+
from PIL import ImageDraw
|
|
431
|
+
|
|
432
|
+
import anki_vector
|
|
433
|
+
from anki_vector import annotate
|
|
434
|
+
import time
|
|
435
|
+
|
|
436
|
+
@annotate.annotator
|
|
437
|
+
def clock(image, scale, annotator=None, world=None, **kw):
|
|
438
|
+
d = ImageDraw.Draw(image)
|
|
439
|
+
bounds = (0, 0, image.width, image.height)
|
|
440
|
+
text = annotate.ImageText(time.strftime("%H:%m:%S"),
|
|
441
|
+
position=annotate.AnnotationPosition.TOP_LEFT,
|
|
442
|
+
outline_color="black")
|
|
443
|
+
text.render(d, bounds)
|
|
444
|
+
|
|
445
|
+
with anki_vector.Robot(show_viewer=True) as robot:
|
|
446
|
+
# Add a custom annotator to the camera feed
|
|
447
|
+
robot.camera.image_annotator.add_annotator("custom-annotator", clock)
|
|
448
|
+
time.sleep(5)
|
|
449
|
+
# Disable the custom annotator
|
|
450
|
+
robot.camera.image_annotator.disable_annotator("custom-annotator")
|
|
451
|
+
time.sleep(5)
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
def __init__(self, world, **kw):
|
|
455
|
+
super().__init__(**kw)
|
|
456
|
+
#: :class:`anki_vector.world.World`: World object that created the annotator.
|
|
457
|
+
self.world = world
|
|
458
|
+
|
|
459
|
+
self._annotators = {}
|
|
460
|
+
self._sorted_annotators = []
|
|
461
|
+
self.add_annotator('objects', ObjectAnnotator(self))
|
|
462
|
+
self.add_annotator('faces', FaceAnnotator(self))
|
|
463
|
+
|
|
464
|
+
#: If this attribute is set to false, the :meth:`annotate_image` method
|
|
465
|
+
#: will continue to provide a scaled image, but will not apply any annotations.
|
|
466
|
+
self.annotation_enabled = True
|
|
467
|
+
|
|
468
|
+
def _sort_annotators(self):
|
|
469
|
+
self._sorted_annotators = sorted(self._annotators.values(),
|
|
470
|
+
key=lambda an: an.priority, reverse=True)
|
|
471
|
+
|
|
472
|
+
def add_annotator(self, name: str, new_annotator: Union[Annotator, Callable[..., Annotator]]) -> None:
|
|
473
|
+
"""Adds a new annotator for display.
|
|
474
|
+
|
|
475
|
+
Annotators are enabled by default.
|
|
476
|
+
|
|
477
|
+
:param name: An arbitrary name for the annotator; must not
|
|
478
|
+
already be defined
|
|
479
|
+
:param new_annotator: The annotator to add may either by an instance of Annotator,
|
|
480
|
+
or a factory callable that will return an instance of Annotator.
|
|
481
|
+
The callable will be called with an ImageAnnotator instance as its first argument.
|
|
482
|
+
|
|
483
|
+
Raises:
|
|
484
|
+
:class:`ValueError` if the annotator is already defined.
|
|
485
|
+
"""
|
|
486
|
+
if name in self._annotators:
|
|
487
|
+
raise ValueError('Annotator "%s" is already defined' % (name))
|
|
488
|
+
if not isinstance(new_annotator, Annotator):
|
|
489
|
+
new_annotator = new_annotator(self)
|
|
490
|
+
self._annotators[name] = new_annotator
|
|
491
|
+
self._sort_annotators()
|
|
492
|
+
|
|
493
|
+
def remove_annotator(self, name: str) -> None:
|
|
494
|
+
"""Remove an annotator.
|
|
495
|
+
|
|
496
|
+
:param name: The name of the annotator to remove as passed to :meth:`add_annotator`.
|
|
497
|
+
|
|
498
|
+
Raises:
|
|
499
|
+
KeyError if the annotator isn't registered
|
|
500
|
+
"""
|
|
501
|
+
del self._annotators[name]
|
|
502
|
+
self._sort_annotators()
|
|
503
|
+
|
|
504
|
+
def get_annotator(self, name: str) -> None:
|
|
505
|
+
"""Return a named annotator.
|
|
506
|
+
|
|
507
|
+
:param name: The name of the annotator to return
|
|
508
|
+
|
|
509
|
+
Raises:
|
|
510
|
+
KeyError if the annotator isn't registered
|
|
511
|
+
"""
|
|
512
|
+
return self._annotators[name]
|
|
513
|
+
|
|
514
|
+
def disable_annotator(self, name: str) -> None:
|
|
515
|
+
"""Disable a named annotator.
|
|
516
|
+
|
|
517
|
+
Leaves the annotator as registered, but does not include its output
|
|
518
|
+
in the annotated image.
|
|
519
|
+
|
|
520
|
+
:param name: The name of the annotator to disable
|
|
521
|
+
"""
|
|
522
|
+
if name in self._annotators:
|
|
523
|
+
self._annotators[name].enabled = False
|
|
524
|
+
|
|
525
|
+
def enable_annotator(self, name: str) -> None:
|
|
526
|
+
"""Enabled a named annotator.
|
|
527
|
+
|
|
528
|
+
(re)enable an annotator if it was previously disabled.
|
|
529
|
+
|
|
530
|
+
:param name: The name of the annotator to enable
|
|
531
|
+
"""
|
|
532
|
+
self._annotators[name].enabled = True
|
|
533
|
+
|
|
534
|
+
def add_static_text(self, name: str, text: Union[str, ImageText], color: str = 'white', position: int = AnnotationPosition.TOP_LEFT) -> None:
|
|
535
|
+
"""Add some static text to annotated images.
|
|
536
|
+
|
|
537
|
+
This is a convenience method to create a :class:`TextAnnnotator`
|
|
538
|
+
and add it to the image.
|
|
539
|
+
|
|
540
|
+
:param name: An arbitrary name for the annotator; must not
|
|
541
|
+
already be defined
|
|
542
|
+
:param text: The text to display
|
|
543
|
+
may be a plain string, or an ImageText instance
|
|
544
|
+
:param color: Used if text is a string; defaults to white
|
|
545
|
+
:param position: Used if text is a string; defaults to TOP_LEFT
|
|
546
|
+
"""
|
|
547
|
+
if isinstance(text, str):
|
|
548
|
+
text = ImageText(text, position=position, color=color)
|
|
549
|
+
self.add_annotator(name, TextAnnotator(self, text))
|
|
550
|
+
|
|
551
|
+
def annotate_image(self, image: Image.Image, scale: float = None, fit_size: Tuple[int, int] = None, resample_mode: int = RESAMPLE_MODE_NEAREST) -> Image.Image:
|
|
552
|
+
"""Called by :class:`~anki_vector.camera.CameraComponent` to annotate camera images.
|
|
553
|
+
|
|
554
|
+
:param image: The image to annotate
|
|
555
|
+
:param scale: If set then the base image will be scaled by the
|
|
556
|
+
supplied multiplier. Cannot be combined with fit_size
|
|
557
|
+
:param fit_size: If set, then scale the image to fit inside
|
|
558
|
+
the supplied (width, height) dimensions. The original aspect
|
|
559
|
+
ratio will be preserved. Cannot be combined with scale.
|
|
560
|
+
:param resample_mode: The resampling mode to use when scaling the
|
|
561
|
+
image. Should be either :attr:`RESAMPLE_MODE_NEAREST` (fast) or
|
|
562
|
+
:attr:`RESAMPLE_MODE_BILINEAR` (slower, but smoother).
|
|
563
|
+
"""
|
|
564
|
+
if scale is not None and scale != 1:
|
|
565
|
+
image = image.resize((int(image.width * scale), int(image.height * scale)),
|
|
566
|
+
resample=resample_mode)
|
|
567
|
+
|
|
568
|
+
elif fit_size is not None and fit_size != (image.width, image.height):
|
|
569
|
+
img_ratio = image.width / image.height
|
|
570
|
+
fit_width, fit_height = fit_size
|
|
571
|
+
fit_ratio = fit_width / fit_height
|
|
572
|
+
if img_ratio > fit_ratio:
|
|
573
|
+
fit_height = int(fit_width / img_ratio)
|
|
574
|
+
elif img_ratio < fit_ratio:
|
|
575
|
+
fit_width = int(fit_height * img_ratio)
|
|
576
|
+
scale = fit_width / image.width
|
|
577
|
+
image = image.resize((fit_width, fit_height))
|
|
578
|
+
|
|
579
|
+
else:
|
|
580
|
+
scale = 1
|
|
581
|
+
image = image.copy()
|
|
582
|
+
|
|
583
|
+
if not self.annotation_enabled:
|
|
584
|
+
return image
|
|
585
|
+
|
|
586
|
+
for an in self._sorted_annotators:
|
|
587
|
+
if an.enabled:
|
|
588
|
+
an.apply(image, scale)
|
|
589
|
+
|
|
590
|
+
return image
|