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.
Files changed (71) hide show
  1. anki_vector/__init__.py +43 -0
  2. anki_vector/animation.py +272 -0
  3. anki_vector/annotate.py +590 -0
  4. anki_vector/audio.py +212 -0
  5. anki_vector/audio_stream.py +335 -0
  6. anki_vector/behavior.py +1135 -0
  7. anki_vector/camera.py +670 -0
  8. anki_vector/camera_viewer/__init__.py +121 -0
  9. anki_vector/color.py +88 -0
  10. anki_vector/configure/__main__.py +331 -0
  11. anki_vector/connection.py +838 -0
  12. anki_vector/events.py +420 -0
  13. anki_vector/exceptions.py +185 -0
  14. anki_vector/faces.py +819 -0
  15. anki_vector/lights.py +210 -0
  16. anki_vector/mdns.py +131 -0
  17. anki_vector/messaging/__init__.py +45 -0
  18. anki_vector/messaging/alexa_pb2.py +36 -0
  19. anki_vector/messaging/alexa_pb2_grpc.py +3 -0
  20. anki_vector/messaging/behavior_pb2.py +40 -0
  21. anki_vector/messaging/behavior_pb2_grpc.py +3 -0
  22. anki_vector/messaging/client.py +33 -0
  23. anki_vector/messaging/cube_pb2.py +113 -0
  24. anki_vector/messaging/cube_pb2_grpc.py +3 -0
  25. anki_vector/messaging/extensions_pb2.py +25 -0
  26. anki_vector/messaging/extensions_pb2_grpc.py +3 -0
  27. anki_vector/messaging/external_interface_pb2.py +169 -0
  28. anki_vector/messaging/external_interface_pb2_grpc.py +1267 -0
  29. anki_vector/messaging/messages_pb2.py +431 -0
  30. anki_vector/messaging/messages_pb2_grpc.py +3 -0
  31. anki_vector/messaging/nav_map_pb2.py +33 -0
  32. anki_vector/messaging/nav_map_pb2_grpc.py +3 -0
  33. anki_vector/messaging/protocol.py +33 -0
  34. anki_vector/messaging/response_status_pb2.py +27 -0
  35. anki_vector/messaging/response_status_pb2_grpc.py +3 -0
  36. anki_vector/messaging/settings_pb2.py +72 -0
  37. anki_vector/messaging/settings_pb2_grpc.py +3 -0
  38. anki_vector/messaging/shared_pb2.py +54 -0
  39. anki_vector/messaging/shared_pb2_grpc.py +3 -0
  40. anki_vector/motors.py +127 -0
  41. anki_vector/nav_map.py +409 -0
  42. anki_vector/objects.py +1782 -0
  43. anki_vector/opengl/__init__.py +103 -0
  44. anki_vector/opengl/assets/LICENSE.txt +21 -0
  45. anki_vector/opengl/assets/cube.jpg +0 -0
  46. anki_vector/opengl/assets/cube.mtl +9 -0
  47. anki_vector/opengl/assets/cube.obj +1000 -0
  48. anki_vector/opengl/assets/vector.mtl +67 -0
  49. anki_vector/opengl/assets/vector.obj +13220 -0
  50. anki_vector/opengl/opengl.py +864 -0
  51. anki_vector/opengl/opengl_vector.py +620 -0
  52. anki_vector/opengl/opengl_viewer.py +689 -0
  53. anki_vector/photos.py +145 -0
  54. anki_vector/proximity.py +176 -0
  55. anki_vector/reserve_control/__main__.py +36 -0
  56. anki_vector/robot.py +930 -0
  57. anki_vector/screen.py +201 -0
  58. anki_vector/status.py +322 -0
  59. anki_vector/touch.py +119 -0
  60. anki_vector/user_intent.py +186 -0
  61. anki_vector/util.py +1132 -0
  62. anki_vector/version.py +15 -0
  63. anki_vector/viewer.py +403 -0
  64. anki_vector/vision.py +202 -0
  65. anki_vector/world.py +899 -0
  66. wirepod_vector_sdk_audio-0.9.0.dist-info/METADATA +80 -0
  67. wirepod_vector_sdk_audio-0.9.0.dist-info/RECORD +71 -0
  68. wirepod_vector_sdk_audio-0.9.0.dist-info/WHEEL +5 -0
  69. wirepod_vector_sdk_audio-0.9.0.dist-info/licenses/LICENSE.txt +180 -0
  70. wirepod_vector_sdk_audio-0.9.0.dist-info/top_level.txt +1 -0
  71. wirepod_vector_sdk_audio-0.9.0.dist-info/zip-safe +1 -0
@@ -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