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/util.py
ADDED
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
# Copyright (c) 2018 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
|
+
"""
|
|
16
|
+
Utility functions and classes for the Vector SDK.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# __all__ should order by constants, event classes, other classes, functions.
|
|
20
|
+
__all__ = ['Angle',
|
|
21
|
+
'BaseOverlay',
|
|
22
|
+
'Component',
|
|
23
|
+
'Distance',
|
|
24
|
+
'ImageRect',
|
|
25
|
+
'Matrix44',
|
|
26
|
+
'Pose',
|
|
27
|
+
'Position',
|
|
28
|
+
'Quaternion',
|
|
29
|
+
'RectangleOverlay',
|
|
30
|
+
'Speed',
|
|
31
|
+
'Vector2',
|
|
32
|
+
'Vector3',
|
|
33
|
+
'angle_z_to_quaternion',
|
|
34
|
+
'block_while_none',
|
|
35
|
+
'degrees',
|
|
36
|
+
'distance_mm',
|
|
37
|
+
'distance_inches',
|
|
38
|
+
'get_class_logger',
|
|
39
|
+
'parse_command_args',
|
|
40
|
+
'radians',
|
|
41
|
+
'setup_basic_logging',
|
|
42
|
+
'speed_mmps']
|
|
43
|
+
|
|
44
|
+
import argparse
|
|
45
|
+
import configparser
|
|
46
|
+
from functools import wraps
|
|
47
|
+
import logging
|
|
48
|
+
import math
|
|
49
|
+
import os
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
import sys
|
|
52
|
+
import time
|
|
53
|
+
from typing import Callable, Union
|
|
54
|
+
|
|
55
|
+
from .exceptions import VectorConfigurationException, VectorPropertyValueNotReadyException
|
|
56
|
+
from .messaging import protocol
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
from PIL import Image, ImageDraw
|
|
60
|
+
except ImportError:
|
|
61
|
+
sys.exit("Cannot import from PIL: Do `pip3 install --user Pillow` to install")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_command_args(parser: argparse.ArgumentParser = None):
|
|
65
|
+
"""
|
|
66
|
+
Parses command line arguments.
|
|
67
|
+
|
|
68
|
+
Attempts to read the robot serial number from the command line arguments. If no serial number
|
|
69
|
+
is specified, we next attempt to read the robot serial number from environment variable ANKI_ROBOT_SERIAL.
|
|
70
|
+
If ANKI_ROBOT_SERIAL is specified, the value will be used as the robot's serial number.
|
|
71
|
+
|
|
72
|
+
.. code-block:: python
|
|
73
|
+
|
|
74
|
+
import anki_vector
|
|
75
|
+
|
|
76
|
+
import argparse
|
|
77
|
+
|
|
78
|
+
parser = argparse.ArgumentParser()
|
|
79
|
+
parser.add_argument("--new_param")
|
|
80
|
+
args = anki_vector.util.parse_command_args(parser)
|
|
81
|
+
|
|
82
|
+
:param parser: To add new command line arguments,
|
|
83
|
+
pass an argparse parser with the new options
|
|
84
|
+
already defined. Leave empty to use the defaults.
|
|
85
|
+
"""
|
|
86
|
+
if parser is None:
|
|
87
|
+
parser = argparse.ArgumentParser()
|
|
88
|
+
parser.add_argument("-s", "--serial", nargs='?', default=os.environ.get('ANKI_ROBOT_SERIAL', None))
|
|
89
|
+
return parser.parse_args()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def block_while_none(interval: float = 0.1, max_iterations: int = 50):
|
|
93
|
+
"""Use this to denote a property that may need some delay before it appears.
|
|
94
|
+
|
|
95
|
+
:param interval: how often to check if the property is no longer None
|
|
96
|
+
:param max_iterations: how many times to check the property before raising an error
|
|
97
|
+
|
|
98
|
+
This will raise a :class:`VectorControlTimeoutException` if the property cannot be retrieved
|
|
99
|
+
before :attr:`max_iterations`.
|
|
100
|
+
"""
|
|
101
|
+
def blocker(func: Callable):
|
|
102
|
+
@wraps(func)
|
|
103
|
+
def wrapped(*args, **kwargs):
|
|
104
|
+
iterations = 0
|
|
105
|
+
result = func(*args, **kwargs)
|
|
106
|
+
while result is None:
|
|
107
|
+
time.sleep(interval)
|
|
108
|
+
iterations += 1
|
|
109
|
+
if iterations > max_iterations:
|
|
110
|
+
raise VectorPropertyValueNotReadyException()
|
|
111
|
+
result = func(*args, **kwargs)
|
|
112
|
+
return result
|
|
113
|
+
return wrapped
|
|
114
|
+
return blocker
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def setup_basic_logging(custom_handler: logging.Handler = None,
|
|
118
|
+
general_log_level: str = None,
|
|
119
|
+
target: object = None):
|
|
120
|
+
"""Helper to perform basic setup of the Python logger.
|
|
121
|
+
|
|
122
|
+
:param custom_handler: provide an external logger for custom logging locations
|
|
123
|
+
:param general_log_level: 'DEBUG', 'INFO', 'WARN', 'ERROR' or an equivalent
|
|
124
|
+
constant from the :mod:`logging` module. If None then a
|
|
125
|
+
value will be read from the VECTOR_LOG_LEVEL environment variable.
|
|
126
|
+
:param target: The stream to send the log data to; defaults to stderr
|
|
127
|
+
"""
|
|
128
|
+
if general_log_level is None:
|
|
129
|
+
general_log_level = os.environ.get('VECTOR_LOG_LEVEL', logging.INFO)
|
|
130
|
+
|
|
131
|
+
handler = custom_handler
|
|
132
|
+
if handler is None:
|
|
133
|
+
handler = logging.StreamHandler(stream=target)
|
|
134
|
+
formatter = logging.Formatter("%(asctime)s.%(msecs)03d %(name)+25s %(levelname)+7s %(message)s",
|
|
135
|
+
"%H:%M:%S")
|
|
136
|
+
handler.setFormatter(formatter)
|
|
137
|
+
|
|
138
|
+
class LogCleanup(logging.Filter): # pylint: disable=too-few-public-methods
|
|
139
|
+
def filter(self, record):
|
|
140
|
+
# Drop 'anki_vector' from log messages
|
|
141
|
+
record.name = '.'.join(record.name.split('.')[1:])
|
|
142
|
+
# Indent past informational chunk
|
|
143
|
+
record.msg = record.msg.replace("\n", f"\n{'':48}")
|
|
144
|
+
return True
|
|
145
|
+
handler.addFilter(LogCleanup())
|
|
146
|
+
|
|
147
|
+
vector_logger = logging.getLogger('anki_vector')
|
|
148
|
+
if not vector_logger.handlers:
|
|
149
|
+
vector_logger.addHandler(handler)
|
|
150
|
+
vector_logger.setLevel(general_log_level)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_class_logger(module: str, obj: object) -> logging.Logger:
|
|
154
|
+
"""Helper to create logger for a given class (and module).
|
|
155
|
+
|
|
156
|
+
.. testcode::
|
|
157
|
+
|
|
158
|
+
import anki_vector
|
|
159
|
+
|
|
160
|
+
logger = anki_vector.util.get_class_logger("module_name", "object_name")
|
|
161
|
+
|
|
162
|
+
:param module: The name of the module to which the object belongs.
|
|
163
|
+
:param obj: the object that owns the logger.
|
|
164
|
+
"""
|
|
165
|
+
return logging.getLogger(".".join([module, type(obj).__name__]))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Vector2:
|
|
169
|
+
"""Represents a 2D Vector (type/units aren't specified).
|
|
170
|
+
|
|
171
|
+
:param x: X component
|
|
172
|
+
:param y: Y component
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
__slots__ = ('_x', '_y')
|
|
176
|
+
|
|
177
|
+
def __init__(self, x: float, y: float):
|
|
178
|
+
self._x = float(x)
|
|
179
|
+
self._y = float(y)
|
|
180
|
+
|
|
181
|
+
def set_to(self, rhs):
|
|
182
|
+
"""Copy the x and y components of the given Vector2 instance.
|
|
183
|
+
|
|
184
|
+
:param rhs: The right-hand-side of this assignment - the
|
|
185
|
+
source Vector2 to copy into this Vector2 instance.
|
|
186
|
+
"""
|
|
187
|
+
self._x = float(rhs.x)
|
|
188
|
+
self._y = float(rhs.y)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def x(self) -> float:
|
|
192
|
+
"""The x component."""
|
|
193
|
+
return self._x
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def y(self) -> float:
|
|
197
|
+
"""The y component."""
|
|
198
|
+
return self._y
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def x_y(self):
|
|
202
|
+
"""tuple (float, float): The X, Y elements of the Vector2 (x,y)"""
|
|
203
|
+
return self._x, self._y
|
|
204
|
+
|
|
205
|
+
def __repr__(self):
|
|
206
|
+
return "<%s x: %.2f y: %.2f>" % (self.__class__.__name__, self.x, self.y)
|
|
207
|
+
|
|
208
|
+
def __add__(self, other):
|
|
209
|
+
if not isinstance(other, Vector2):
|
|
210
|
+
raise TypeError("Unsupported operand for + expected Vector2")
|
|
211
|
+
return Vector2(self.x + other.x, self.y + other.y)
|
|
212
|
+
|
|
213
|
+
def __sub__(self, other):
|
|
214
|
+
if not isinstance(other, Vector2):
|
|
215
|
+
raise TypeError("Unsupported operand for - expected Vector2")
|
|
216
|
+
return Vector2(self.x - other.x, self.y - other.y)
|
|
217
|
+
|
|
218
|
+
def __mul__(self, other):
|
|
219
|
+
if not isinstance(other, (int, float)):
|
|
220
|
+
raise TypeError("Unsupported operand for * expected number")
|
|
221
|
+
return Vector2(self.x * other, self.y * other)
|
|
222
|
+
|
|
223
|
+
def __truediv__(self, other):
|
|
224
|
+
if not isinstance(other, (int, float)):
|
|
225
|
+
raise TypeError("Unsupported operand for / expected number")
|
|
226
|
+
return Vector2(self.x / other, self.y / other)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Vector3:
|
|
230
|
+
"""Represents a 3D Vector (type/units aren't specified).
|
|
231
|
+
|
|
232
|
+
:param x: X component
|
|
233
|
+
:param y: Y component
|
|
234
|
+
:param z: Z component
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
__slots__ = ('_x', '_y', '_z')
|
|
238
|
+
|
|
239
|
+
def __init__(self, x: float, y: float, z: float):
|
|
240
|
+
self._x = float(x)
|
|
241
|
+
self._y = float(y)
|
|
242
|
+
self._z = float(z)
|
|
243
|
+
|
|
244
|
+
def set_to(self, rhs):
|
|
245
|
+
"""Copy the x, y and z components of the given Vector3 instance.
|
|
246
|
+
|
|
247
|
+
:param rhs: The right-hand-side of this assignment - the
|
|
248
|
+
source Vector3 to copy into this Vector3 instance.
|
|
249
|
+
"""
|
|
250
|
+
self._x = float(rhs.x)
|
|
251
|
+
self._y = float(rhs.y)
|
|
252
|
+
self._z = float(rhs.z)
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def x(self) -> float:
|
|
256
|
+
"""The x component."""
|
|
257
|
+
return self._x
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def y(self) -> float:
|
|
261
|
+
"""The y component."""
|
|
262
|
+
return self._y
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def z(self) -> float:
|
|
266
|
+
"""The z component."""
|
|
267
|
+
return self._z
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def magnitude_squared(self) -> float:
|
|
271
|
+
"""float: The magnitude of the Vector3 instance"""
|
|
272
|
+
return self._x**2 + self._y**2 + self._z**2
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def magnitude(self) -> float:
|
|
276
|
+
"""The magnitude of the Vector3 instance"""
|
|
277
|
+
return math.sqrt(self.magnitude_squared)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def normalized(self):
|
|
281
|
+
"""A Vector3 instance with the same direction and unit magnitude"""
|
|
282
|
+
mag = self.magnitude
|
|
283
|
+
if mag == 0:
|
|
284
|
+
return Vector3(0, 0, 0)
|
|
285
|
+
return Vector3(self._x / mag, self._y / mag, self._z / mag)
|
|
286
|
+
|
|
287
|
+
def dot(self, other):
|
|
288
|
+
"""The dot product of this and another Vector3 instance"""
|
|
289
|
+
if not isinstance(other, Vector3):
|
|
290
|
+
raise TypeError("Unsupported argument for dot product, expected Vector3")
|
|
291
|
+
return self._x * other.x + self._y * other.y + self._z * other.z
|
|
292
|
+
|
|
293
|
+
def cross(self, other):
|
|
294
|
+
"""The cross product of this and another Vector3 instance"""
|
|
295
|
+
if not isinstance(other, Vector3):
|
|
296
|
+
raise TypeError("Unsupported argument for cross product, expected Vector3")
|
|
297
|
+
|
|
298
|
+
return Vector3(
|
|
299
|
+
self._y * other.z - self._z * other.y,
|
|
300
|
+
self._z * other.x - self._x * other.z,
|
|
301
|
+
self._x * other.y - self._y * other.x)
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def x_y_z(self):
|
|
305
|
+
"""tuple (float, float, float): The X, Y, Z elements of the Vector3 (x,y,z)"""
|
|
306
|
+
return self._x, self._y, self._z
|
|
307
|
+
|
|
308
|
+
def __repr__(self):
|
|
309
|
+
return f"<{self.__class__.__name__} x: {self.x:.2f} y: {self.y:.2f} z: {self.z:.2f}>"
|
|
310
|
+
|
|
311
|
+
def __add__(self, other):
|
|
312
|
+
if not isinstance(other, Vector3):
|
|
313
|
+
raise TypeError("Unsupported operand for +, expected Vector3")
|
|
314
|
+
return Vector3(self.x + other.x, self.y + other.y, self.z + other.z)
|
|
315
|
+
|
|
316
|
+
def __sub__(self, other):
|
|
317
|
+
if not isinstance(other, Vector3):
|
|
318
|
+
raise TypeError("Unsupported operand for -, expected Vector3")
|
|
319
|
+
return Vector3(self.x - other.x, self.y - other.y, self.z - other.z)
|
|
320
|
+
|
|
321
|
+
def __mul__(self, other):
|
|
322
|
+
if not isinstance(other, (int, float)):
|
|
323
|
+
raise TypeError("Unsupported operand for * expected number")
|
|
324
|
+
return Vector3(self.x * other, self.y * other, self.z * other)
|
|
325
|
+
|
|
326
|
+
def __truediv__(self, other):
|
|
327
|
+
if not isinstance(other, (int, float)):
|
|
328
|
+
raise TypeError("Unsupported operand for / expected number")
|
|
329
|
+
return Vector3(self.x / other, self.y / other, self.z / other)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class Angle:
|
|
333
|
+
"""Represents an angle.
|
|
334
|
+
|
|
335
|
+
Use the :func:`degrees` or :func:`radians` convenience methods to generate
|
|
336
|
+
an Angle instance.
|
|
337
|
+
|
|
338
|
+
:param radians: The number of radians the angle should represent
|
|
339
|
+
(cannot be combined with ``degrees``)
|
|
340
|
+
:param degrees: The number of degress the angle should represent
|
|
341
|
+
(cannot be combined with ``radians``)
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
__slots__ = ('_radians')
|
|
345
|
+
|
|
346
|
+
def __init__(self, radians: float = None, degrees: float = None): # pylint: disable=redefined-outer-name
|
|
347
|
+
if radians is None and degrees is None:
|
|
348
|
+
raise ValueError("Expected either the degrees or radians keyword argument")
|
|
349
|
+
if radians and degrees:
|
|
350
|
+
raise ValueError("Expected either the degrees or radians keyword argument, not both")
|
|
351
|
+
|
|
352
|
+
if degrees is not None:
|
|
353
|
+
radians = degrees * math.pi / 180
|
|
354
|
+
self._radians = float(radians)
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def radians(self) -> float: # pylint: disable=redefined-outer-name
|
|
358
|
+
"""The angle in radians."""
|
|
359
|
+
return self._radians
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def degrees(self) -> float: # pylint: disable=redefined-outer-name
|
|
363
|
+
"""The angle in degrees."""
|
|
364
|
+
return self._radians / math.pi * 180
|
|
365
|
+
|
|
366
|
+
def __repr__(self):
|
|
367
|
+
return f"<{self.__class__.__name__} Radians: {self.radians:.2f} Degrees: {self.degrees:.2f}>"
|
|
368
|
+
|
|
369
|
+
def __add__(self, other):
|
|
370
|
+
if not isinstance(other, Angle):
|
|
371
|
+
raise TypeError("Unsupported type for + expected Angle")
|
|
372
|
+
return Angle(radians=(self.radians + other.radians))
|
|
373
|
+
|
|
374
|
+
def __sub__(self, other):
|
|
375
|
+
if not isinstance(other, Angle):
|
|
376
|
+
raise TypeError("Unsupported type for - expected Angle")
|
|
377
|
+
return Angle(radians=(self.radians - other.radians))
|
|
378
|
+
|
|
379
|
+
def __mul__(self, other):
|
|
380
|
+
if not isinstance(other, (int, float)):
|
|
381
|
+
raise TypeError("Unsupported type for * expected number")
|
|
382
|
+
return Angle(radians=(self.radians * other))
|
|
383
|
+
|
|
384
|
+
def __truediv__(self, other):
|
|
385
|
+
if not isinstance(other, (int, float)):
|
|
386
|
+
raise TypeError("Unsupported type for / expected number")
|
|
387
|
+
return radians(self.radians / other)
|
|
388
|
+
|
|
389
|
+
def _cmp_int(self, other):
|
|
390
|
+
if not isinstance(other, Angle):
|
|
391
|
+
raise TypeError("Unsupported type for comparison expected Angle")
|
|
392
|
+
return self.radians - other.radians
|
|
393
|
+
|
|
394
|
+
def __eq__(self, other):
|
|
395
|
+
return self._cmp_int(other) == 0
|
|
396
|
+
|
|
397
|
+
def __ne__(self, other):
|
|
398
|
+
return self._cmp_int(other) != 0
|
|
399
|
+
|
|
400
|
+
def __gt__(self, other):
|
|
401
|
+
return self._cmp_int(other) > 0
|
|
402
|
+
|
|
403
|
+
def __lt__(self, other):
|
|
404
|
+
return self._cmp_int(other) < 0
|
|
405
|
+
|
|
406
|
+
def __ge__(self, other):
|
|
407
|
+
return self._cmp_int(other) >= 0
|
|
408
|
+
|
|
409
|
+
def __le__(self, other):
|
|
410
|
+
return self._cmp_int(other) <= 0
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def abs_value(self):
|
|
414
|
+
""":class:`anki_vector.util.Angle`: The absolute value of the angle.
|
|
415
|
+
|
|
416
|
+
If the Angle is positive then it returns a copy of this Angle, otherwise it returns -Angle.
|
|
417
|
+
"""
|
|
418
|
+
return Angle(radians=abs(self._radians))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def angle_z_to_quaternion(angle_z: Angle):
|
|
422
|
+
"""This function converts an angle in the z axis (Euler angle z component) to a quaternion.
|
|
423
|
+
|
|
424
|
+
:param angle_z: The z axis angle.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
q0, q1, q2, q3 (float, float, float, float): A tuple with all the members
|
|
428
|
+
of a quaternion defined by angle_z.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
# Define the quaternion to be converted from a Euler angle (x,y,z) of 0,0,angle_z
|
|
432
|
+
# These equations have their original equations above, and simplified implemented
|
|
433
|
+
# q0 = cos(x/2)*cos(y/2)*cos(z/2) + sin(x/2)*sin(y/2)*sin(z/2)
|
|
434
|
+
q0 = math.cos(angle_z.radians / 2)
|
|
435
|
+
# q1 = sin(x/2)*cos(y/2)*cos(z/2) - cos(x/2)*sin(y/2)*sin(z/2)
|
|
436
|
+
q1 = 0
|
|
437
|
+
# q2 = cos(x/2)*sin(y/2)*cos(z/2) + sin(x/2)*cos(y/2)*sin(z/2)
|
|
438
|
+
q2 = 0
|
|
439
|
+
# q3 = cos(x/2)*cos(y/2)*sin(z/2) - sin(x/2)*sin(y/2)*cos(z/2)
|
|
440
|
+
q3 = math.sin(angle_z.radians / 2)
|
|
441
|
+
return q0, q1, q2, q3
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def degrees(degrees: float) -> Angle: # pylint: disable=redefined-outer-name
|
|
445
|
+
"""An Angle instance set to the specified number of degrees."""
|
|
446
|
+
return Angle(degrees=degrees)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def radians(radians: float) -> Angle: # pylint: disable=redefined-outer-name
|
|
450
|
+
"""An Angle instance set to the specified number of radians."""
|
|
451
|
+
return Angle(radians=radians)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class Matrix44:
|
|
455
|
+
"""A 4x4 Matrix for representing the rotation and/or position of an object in the world.
|
|
456
|
+
|
|
457
|
+
Can be generated from a :class:`Quaternion` for a pure rotation matrix, or
|
|
458
|
+
combined with a position for a full translation matrix, as done by
|
|
459
|
+
:meth:`Pose.to_matrix`.
|
|
460
|
+
"""
|
|
461
|
+
__slots__ = ('m00', 'm10', 'm20', 'm30',
|
|
462
|
+
'm01', 'm11', 'm21', 'm31',
|
|
463
|
+
'm02', 'm12', 'm22', 'm32',
|
|
464
|
+
'm03', 'm13', 'm23', 'm33')
|
|
465
|
+
|
|
466
|
+
def __init__(self,
|
|
467
|
+
m00: float, m10: float, m20: float, m30: float,
|
|
468
|
+
m01: float, m11: float, m21: float, m31: float,
|
|
469
|
+
m02: float, m12: float, m22: float, m32: float,
|
|
470
|
+
m03: float, m13: float, m23: float, m33: float):
|
|
471
|
+
self.m00 = float(m00)
|
|
472
|
+
self.m10 = float(m10)
|
|
473
|
+
self.m20 = float(m20)
|
|
474
|
+
self.m30 = float(m30)
|
|
475
|
+
|
|
476
|
+
self.m01 = float(m01)
|
|
477
|
+
self.m11 = float(m11)
|
|
478
|
+
self.m21 = float(m21)
|
|
479
|
+
self.m31 = float(m31)
|
|
480
|
+
|
|
481
|
+
self.m02 = float(m02)
|
|
482
|
+
self.m12 = float(m12)
|
|
483
|
+
self.m22 = float(m22)
|
|
484
|
+
self.m32 = float(m32)
|
|
485
|
+
|
|
486
|
+
self.m03 = float(m03)
|
|
487
|
+
self.m13 = float(m13)
|
|
488
|
+
self.m23 = float(m23)
|
|
489
|
+
self.m33 = float(m33)
|
|
490
|
+
|
|
491
|
+
def __repr__(self):
|
|
492
|
+
return ("<%s: "
|
|
493
|
+
"%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f "
|
|
494
|
+
"%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f>" % (
|
|
495
|
+
self.__class__.__name__, *self.in_row_order))
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def tabulated_string(self) -> str:
|
|
499
|
+
"""A multi-line string formatted with tabs to show the matrix contents."""
|
|
500
|
+
return ("%.1f\t%.1f\t%.1f\t%.1f\n"
|
|
501
|
+
"%.1f\t%.1f\t%.1f\t%.1f\n"
|
|
502
|
+
"%.1f\t%.1f\t%.1f\t%.1f\n"
|
|
503
|
+
"%.1f\t%.1f\t%.1f\t%.1f" % self.in_row_order)
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def in_row_order(self):
|
|
507
|
+
"""tuple of 16 floats: The contents of the matrix in row order."""
|
|
508
|
+
return self.m00, self.m01, self.m02, self.m03,\
|
|
509
|
+
self.m10, self.m11, self.m12, self.m13,\
|
|
510
|
+
self.m20, self.m21, self.m22, self.m23,\
|
|
511
|
+
self.m30, self.m31, self.m32, self.m33
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def in_column_order(self):
|
|
515
|
+
"""tuple of 16 floats: The contents of the matrix in column order."""
|
|
516
|
+
return self.m00, self.m10, self.m20, self.m30,\
|
|
517
|
+
self.m01, self.m11, self.m21, self.m31,\
|
|
518
|
+
self.m02, self.m12, self.m22, self.m32,\
|
|
519
|
+
self.m03, self.m13, self.m23, self.m33
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def forward_xyz(self):
|
|
523
|
+
"""tuple of 3 floats: The x,y,z components representing the matrix's forward vector."""
|
|
524
|
+
return self.m00, self.m01, self.m02
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def left_xyz(self):
|
|
528
|
+
"""tuple of 3 floats: The x,y,z components representing the matrix's left vector."""
|
|
529
|
+
return self.m10, self.m11, self.m12
|
|
530
|
+
|
|
531
|
+
@property
|
|
532
|
+
def up_xyz(self):
|
|
533
|
+
"""tuple of 3 floats: The x,y,z components representing the matrix's up vector."""
|
|
534
|
+
return self.m20, self.m21, self.m22
|
|
535
|
+
|
|
536
|
+
@property
|
|
537
|
+
def pos_xyz(self):
|
|
538
|
+
"""tuple of 3 floats: The x,y,z components representing the matrix's position vector."""
|
|
539
|
+
return self.m30, self.m31, self.m32
|
|
540
|
+
|
|
541
|
+
def set_forward(self, x: float, y: float, z: float):
|
|
542
|
+
"""Set the x,y,z components representing the matrix's forward vector.
|
|
543
|
+
|
|
544
|
+
:param x: The X component.
|
|
545
|
+
:param y: The Y component.
|
|
546
|
+
:param z: The Z component.
|
|
547
|
+
"""
|
|
548
|
+
self.m00 = float(x)
|
|
549
|
+
self.m01 = float(y)
|
|
550
|
+
self.m02 = float(z)
|
|
551
|
+
|
|
552
|
+
def set_left(self, x: float, y: float, z: float):
|
|
553
|
+
"""Set the x,y,z components representing the matrix's left vector.
|
|
554
|
+
|
|
555
|
+
:param x: The X component.
|
|
556
|
+
:param y: The Y component.
|
|
557
|
+
:param z: The Z component.
|
|
558
|
+
"""
|
|
559
|
+
self.m10 = float(x)
|
|
560
|
+
self.m11 = float(y)
|
|
561
|
+
self.m12 = float(z)
|
|
562
|
+
|
|
563
|
+
def set_up(self, x: float, y: float, z: float):
|
|
564
|
+
"""Set the x,y,z components representing the matrix's up vector.
|
|
565
|
+
|
|
566
|
+
:param x: The X component.
|
|
567
|
+
:param y: The Y component.
|
|
568
|
+
:param z: The Z component.
|
|
569
|
+
"""
|
|
570
|
+
self.m20 = float(x)
|
|
571
|
+
self.m21 = float(y)
|
|
572
|
+
self.m22 = float(z)
|
|
573
|
+
|
|
574
|
+
def set_pos(self, x: float, y: float, z: float):
|
|
575
|
+
"""Set the x,y,z components representing the matrix's position vector.
|
|
576
|
+
|
|
577
|
+
:param x: The X component.
|
|
578
|
+
:param y: The Y component.
|
|
579
|
+
:param z: The Z component.
|
|
580
|
+
"""
|
|
581
|
+
self.m30 = float(x)
|
|
582
|
+
self.m31 = float(y)
|
|
583
|
+
self.m32 = float(z)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class Quaternion:
|
|
587
|
+
"""Represents the rotation of an object in the world."""
|
|
588
|
+
|
|
589
|
+
__slots__ = ('_q0', '_q1', '_q2', '_q3')
|
|
590
|
+
|
|
591
|
+
def __init__(self, q0: float = None, q1: float = None, q2: float = None, q3: float = None, angle_z: Angle = None):
|
|
592
|
+
is_quaternion = q0 is not None and q1 is not None and q2 is not None and q3 is not None
|
|
593
|
+
|
|
594
|
+
if not is_quaternion and angle_z is None:
|
|
595
|
+
raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword arguments")
|
|
596
|
+
if is_quaternion and angle_z:
|
|
597
|
+
raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword argument,"
|
|
598
|
+
"not both")
|
|
599
|
+
if angle_z is not None:
|
|
600
|
+
if not isinstance(angle_z, Angle):
|
|
601
|
+
raise TypeError("Unsupported type for angle_z expected Angle")
|
|
602
|
+
q0, q1, q2, q3 = angle_z_to_quaternion(angle_z)
|
|
603
|
+
|
|
604
|
+
self._q0 = float(q0)
|
|
605
|
+
self._q1 = float(q1)
|
|
606
|
+
self._q2 = float(q2)
|
|
607
|
+
self._q3 = float(q3)
|
|
608
|
+
|
|
609
|
+
@property
|
|
610
|
+
def q0(self) -> float:
|
|
611
|
+
"""The q0 (w) value of the quaternion."""
|
|
612
|
+
return self._q0
|
|
613
|
+
|
|
614
|
+
@property
|
|
615
|
+
def q1(self) -> float:
|
|
616
|
+
"""The q1 (i) value of the quaternion."""
|
|
617
|
+
return self._q1
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def q2(self) -> float:
|
|
621
|
+
"""The q2 (j) value of the quaternion."""
|
|
622
|
+
return self._q2
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def q3(self) -> float:
|
|
626
|
+
"""The q3 (k) value of the quaternion."""
|
|
627
|
+
return self._q3
|
|
628
|
+
|
|
629
|
+
@property
|
|
630
|
+
def angle_z(self) -> Angle:
|
|
631
|
+
"""An Angle instance representing the z Euler component of the object's rotation.
|
|
632
|
+
|
|
633
|
+
Defined as the rotation in the z axis.
|
|
634
|
+
"""
|
|
635
|
+
q0, q1, q2, q3 = self.q0_q1_q2_q3
|
|
636
|
+
return Angle(radians=math.atan2(2 * (q1 * q2 + q0 * q3), 1 - 2 * (q2**2 + q3**2)))
|
|
637
|
+
|
|
638
|
+
@property
|
|
639
|
+
def q0_q1_q2_q3(self):
|
|
640
|
+
"""tuple of float: Contains all elements of the quaternion (q0,q1,q2,q3)"""
|
|
641
|
+
return self._q0, self._q1, self._q2, self._q3
|
|
642
|
+
|
|
643
|
+
def to_matrix(self, pos_x: float = 0.0, pos_y: float = 0.0, pos_z: float = 0.0):
|
|
644
|
+
"""Convert the Quaternion to a 4x4 matrix representing this rotation.
|
|
645
|
+
|
|
646
|
+
A position can also be provided to generate a full translation matrix.
|
|
647
|
+
|
|
648
|
+
:param pos_x: The x component for the position.
|
|
649
|
+
:param pos_y: The y component for the position.
|
|
650
|
+
:param pos_z: The z component for the position.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
:class:`anki_vector.util.Matrix44`: A matrix representing this Quaternion's
|
|
654
|
+
rotation, with the provided position (which defaults to 0,0,0).
|
|
655
|
+
"""
|
|
656
|
+
# See https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
|
|
657
|
+
q0q0 = self.q0 * self.q0
|
|
658
|
+
q1q1 = self.q1 * self.q1
|
|
659
|
+
q2q2 = self.q2 * self.q2
|
|
660
|
+
q3q3 = self.q3 * self.q3
|
|
661
|
+
|
|
662
|
+
q0x2 = self.q0 * 2.0 # saves 2 multiplies
|
|
663
|
+
q0q1x2 = q0x2 * self.q1
|
|
664
|
+
q0q2x2 = q0x2 * self.q2
|
|
665
|
+
q0q3x2 = q0x2 * self.q3
|
|
666
|
+
q1x2 = self.q1 * 2.0 # saves 1 multiply
|
|
667
|
+
q1q2x2 = q1x2 * self.q2
|
|
668
|
+
q1q3x2 = q1x2 * self.q3
|
|
669
|
+
q2q3x2 = 2.0 * self.q2 * self.q3
|
|
670
|
+
|
|
671
|
+
m00 = (q0q0 + q1q1 - q2q2 - q3q3)
|
|
672
|
+
m01 = (q1q2x2 + q0q3x2)
|
|
673
|
+
m02 = (q1q3x2 - q0q2x2)
|
|
674
|
+
|
|
675
|
+
m10 = (q1q2x2 - q0q3x2)
|
|
676
|
+
m11 = (q0q0 - q1q1 + q2q2 - q3q3)
|
|
677
|
+
m12 = (q0q1x2 + q2q3x2)
|
|
678
|
+
|
|
679
|
+
m20 = (q0q2x2 + q1q3x2)
|
|
680
|
+
m21 = (q2q3x2 - q0q1x2)
|
|
681
|
+
m22 = (q0q0 - q1q1 - q2q2 + q3q3)
|
|
682
|
+
|
|
683
|
+
return Matrix44(m00, m10, m20, float(pos_x),
|
|
684
|
+
m01, m11, m21, float(pos_y),
|
|
685
|
+
m02, m12, m22, float(pos_z),
|
|
686
|
+
0.0, 0.0, 0.0, 1.0)
|
|
687
|
+
|
|
688
|
+
def __repr__(self):
|
|
689
|
+
return (f"<{self.__class__.__name__} q0: {self.q0:.2f} q1: {self.q1:.2f}"
|
|
690
|
+
f" q2: {self.q2:.2f} q3: {self.q3:.2f} {self.angle_z}>")
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class Position(Vector3):
|
|
694
|
+
"""Represents the position of an object in the world.
|
|
695
|
+
|
|
696
|
+
A position consists of its x, y and z values in millimeters.
|
|
697
|
+
|
|
698
|
+
:param x: X position in millimeters
|
|
699
|
+
:param y: Y position in millimeters
|
|
700
|
+
:param z: Z position in millimeters
|
|
701
|
+
"""
|
|
702
|
+
__slots__ = ()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
class Pose:
|
|
706
|
+
"""Represents where an object is in the world.
|
|
707
|
+
|
|
708
|
+
Whenever Vector is delocalized (i.e. whenever Vector no longer knows
|
|
709
|
+
where he is - e.g. when he's picked up), Vector creates a new pose starting at
|
|
710
|
+
(0,0,0) with no rotation, with origin_id incremented to show that these poses
|
|
711
|
+
cannot be compared with earlier ones. As Vector drives around, his pose (and the
|
|
712
|
+
pose of other objects he observes - e.g. faces, his LightCube, charger, etc.) is relative to this
|
|
713
|
+
initial position and orientation.
|
|
714
|
+
|
|
715
|
+
The coordinate space is relative to Vector, where Vector's origin is the
|
|
716
|
+
point on the ground between Vector's two front wheels. The X axis is Vector's forward direction,
|
|
717
|
+
the Y axis is to Vector's left, and the Z axis is up.
|
|
718
|
+
|
|
719
|
+
Only poses of the same origin_id can safely be compared or operated on.
|
|
720
|
+
|
|
721
|
+
.. testcode::
|
|
722
|
+
|
|
723
|
+
import anki_vector
|
|
724
|
+
from anki_vector.util import degrees, Pose
|
|
725
|
+
|
|
726
|
+
with anki_vector.Robot() as robot:
|
|
727
|
+
pose = Pose(x=50, y=0, z=0, angle_z=anki_vector.util.Angle(degrees=0))
|
|
728
|
+
robot.behavior.go_to_pose(pose)
|
|
729
|
+
"""
|
|
730
|
+
__slots__ = ('_position', '_rotation', '_origin_id')
|
|
731
|
+
|
|
732
|
+
def __init__(self, x: float, y: float, z: float, q0: float = None, q1: float = None, q2: float = None, q3: float = None,
|
|
733
|
+
angle_z: Angle = None, origin_id: int = -1):
|
|
734
|
+
self._position = Position(x, y, z)
|
|
735
|
+
self._rotation = Quaternion(q0, q1, q2, q3, angle_z)
|
|
736
|
+
self._origin_id = origin_id
|
|
737
|
+
|
|
738
|
+
@property
|
|
739
|
+
def position(self) -> Position:
|
|
740
|
+
"""The position component of this pose."""
|
|
741
|
+
return self._position
|
|
742
|
+
|
|
743
|
+
@property
|
|
744
|
+
def rotation(self) -> Quaternion:
|
|
745
|
+
"""The rotation component of this pose."""
|
|
746
|
+
return self._rotation
|
|
747
|
+
|
|
748
|
+
@property
|
|
749
|
+
def origin_id(self) -> int:
|
|
750
|
+
"""An ID maintained by the robot which represents which coordinate frame this pose is in."""
|
|
751
|
+
return self._origin_id
|
|
752
|
+
|
|
753
|
+
def __repr__(self):
|
|
754
|
+
return (f"<{self.__class__.__name__}: {self._position}"
|
|
755
|
+
f" {self._rotation} <Origin Id: {self._origin_id}>>")
|
|
756
|
+
|
|
757
|
+
def define_pose_relative_this(self, new_pose):
|
|
758
|
+
"""Creates a new pose such that new_pose's origin is now at the location of this pose.
|
|
759
|
+
|
|
760
|
+
:param anki_vector.util.Pose new_pose: The pose which origin is being changed.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
A :class:`anki_vector.util.Pose` object for which the origin was this pose's origin.
|
|
764
|
+
"""
|
|
765
|
+
if not isinstance(new_pose, Pose):
|
|
766
|
+
raise TypeError("Unsupported type for new_origin, must be of type Pose")
|
|
767
|
+
x, y, z = self.position.x_y_z
|
|
768
|
+
angle_z = self.rotation.angle_z
|
|
769
|
+
new_x, new_y, new_z = new_pose.position.x_y_z
|
|
770
|
+
new_angle_z = new_pose.rotation.angle_z
|
|
771
|
+
|
|
772
|
+
cos_angle = math.cos(angle_z.radians)
|
|
773
|
+
sin_angle = math.sin(angle_z.radians)
|
|
774
|
+
res_x = x + (cos_angle * new_x) - (sin_angle * new_y)
|
|
775
|
+
res_y = y + (sin_angle * new_x) + (cos_angle * new_y)
|
|
776
|
+
res_z = z + new_z
|
|
777
|
+
res_angle = angle_z + new_angle_z
|
|
778
|
+
return Pose(res_x,
|
|
779
|
+
res_y,
|
|
780
|
+
res_z,
|
|
781
|
+
angle_z=res_angle,
|
|
782
|
+
origin_id=self._origin_id)
|
|
783
|
+
|
|
784
|
+
@property
|
|
785
|
+
def is_valid(self) -> bool:
|
|
786
|
+
"""True if this is a valid, usable pose."""
|
|
787
|
+
return self.origin_id >= 0
|
|
788
|
+
|
|
789
|
+
def is_comparable(self, other_pose) -> bool:
|
|
790
|
+
"""Checks whether these two poses are comparable.
|
|
791
|
+
|
|
792
|
+
Poses are comparable if they're valid and having matching origin IDs.
|
|
793
|
+
|
|
794
|
+
:param other_pose: The other pose to compare against. Type is Pose.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
True if the two poses are comparable, False otherwise.
|
|
798
|
+
"""
|
|
799
|
+
return (self.is_valid and other_pose.is_valid
|
|
800
|
+
and (self.origin_id == other_pose.origin_id))
|
|
801
|
+
|
|
802
|
+
def to_matrix(self) -> Matrix44:
|
|
803
|
+
"""Convert the Pose to a Matrix44.
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
A matrix representing this Pose's position and rotation.
|
|
807
|
+
"""
|
|
808
|
+
return self.rotation.to_matrix(*self.position.x_y_z)
|
|
809
|
+
|
|
810
|
+
def to_proto_pose_struct(self) -> protocol.PoseStruct:
|
|
811
|
+
"""Converts the Pose into the robot's messaging pose format.
|
|
812
|
+
"""
|
|
813
|
+
return protocol.PoseStruct(
|
|
814
|
+
x=self._position.x,
|
|
815
|
+
y=self._position.y,
|
|
816
|
+
z=self._position.z,
|
|
817
|
+
q0=self._rotation.q0,
|
|
818
|
+
q1=self._rotation.q1,
|
|
819
|
+
q2=self._rotation.q2,
|
|
820
|
+
q3=self._rotation.q3,
|
|
821
|
+
origin_id=self._origin_id)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
class ImageRect:
|
|
825
|
+
'''Defines a bounding box within an image frame.
|
|
826
|
+
|
|
827
|
+
This is used when objects and faces are observed to denote where in
|
|
828
|
+
the robot's camera view the object or face actually appears. It's then
|
|
829
|
+
used by the annotate module to show an outline of a box around
|
|
830
|
+
the object or face.
|
|
831
|
+
'''
|
|
832
|
+
|
|
833
|
+
__slots__ = ('_x_top_left', '_y_top_left', '_width', '_height')
|
|
834
|
+
|
|
835
|
+
def __init__(self, x_top_left: float, y_top_left: float, width: float, height: float):
|
|
836
|
+
self._x_top_left = float(x_top_left)
|
|
837
|
+
self._y_top_left = float(y_top_left)
|
|
838
|
+
self._width = float(width)
|
|
839
|
+
self._height = float(height)
|
|
840
|
+
|
|
841
|
+
@property
|
|
842
|
+
def x_top_left(self) -> float:
|
|
843
|
+
"""The top left x value of where the object was last visible within Vector's camera view."""
|
|
844
|
+
return self._x_top_left
|
|
845
|
+
|
|
846
|
+
@property
|
|
847
|
+
def y_top_left(self) -> float:
|
|
848
|
+
"""The top left y value of where the object was last visible within Vector's camera view."""
|
|
849
|
+
return self._y_top_left
|
|
850
|
+
|
|
851
|
+
@property
|
|
852
|
+
def width(self) -> float:
|
|
853
|
+
"""The width of the object from when it was last visible within Vector's camera view."""
|
|
854
|
+
return self._width
|
|
855
|
+
|
|
856
|
+
@property
|
|
857
|
+
def height(self) -> float:
|
|
858
|
+
"""The height of the object from when it was last visible within Vector's camera view."""
|
|
859
|
+
return self._height
|
|
860
|
+
|
|
861
|
+
def scale_by(self, scale_multiplier: Union[int, float]) -> None:
|
|
862
|
+
"""Scales the image rectangle by the multiplier provided."""
|
|
863
|
+
if not isinstance(scale_multiplier, (int, float)):
|
|
864
|
+
raise TypeError("Unsupported operand for * expected number")
|
|
865
|
+
self._x_top_left *= scale_multiplier
|
|
866
|
+
self._y_top_left *= scale_multiplier
|
|
867
|
+
self._width *= scale_multiplier
|
|
868
|
+
self._height *= scale_multiplier
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
class Distance:
|
|
872
|
+
"""Represents a distance.
|
|
873
|
+
|
|
874
|
+
The class allows distances to be returned in either millimeters or inches.
|
|
875
|
+
|
|
876
|
+
Use the :func:`distance_inches` or :func:`distance_mm` convenience methods to generate
|
|
877
|
+
a Distance instance.
|
|
878
|
+
|
|
879
|
+
:param distance_mm: The number of millimeters the distance should
|
|
880
|
+
represent (cannot be combined with ``distance_inches``).
|
|
881
|
+
:param distance_inches: The number of inches the distance should
|
|
882
|
+
represent (cannot be combined with ``distance_mm``).
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
__slots__ = ('_distance_mm')
|
|
886
|
+
|
|
887
|
+
def __init__(self, distance_mm: float = None, distance_inches: float = None): # pylint: disable=redefined-outer-name
|
|
888
|
+
if distance_mm is None and distance_inches is None:
|
|
889
|
+
raise ValueError("Expected either the distance_mm or distance_inches keyword argument")
|
|
890
|
+
if distance_mm and distance_inches:
|
|
891
|
+
raise ValueError("Expected either the distance_mm or distance_inches keyword argument, not both")
|
|
892
|
+
|
|
893
|
+
if distance_inches is not None:
|
|
894
|
+
distance_mm = distance_inches * 25.4
|
|
895
|
+
self._distance_mm = float(distance_mm)
|
|
896
|
+
|
|
897
|
+
def __repr__(self):
|
|
898
|
+
return "<%s %.2f mm (%.2f inches)>" % (self.__class__.__name__, self.distance_mm, self.distance_inches)
|
|
899
|
+
|
|
900
|
+
def __add__(self, other):
|
|
901
|
+
if not isinstance(other, Distance):
|
|
902
|
+
raise TypeError("Unsupported operand for + expected Distance")
|
|
903
|
+
return distance_mm(self.distance_mm + other.distance_mm)
|
|
904
|
+
|
|
905
|
+
def __sub__(self, other):
|
|
906
|
+
if not isinstance(other, Distance):
|
|
907
|
+
raise TypeError("Unsupported operand for - expected Distance")
|
|
908
|
+
return distance_mm(self.distance_mm - other.distance_mm)
|
|
909
|
+
|
|
910
|
+
def __mul__(self, other):
|
|
911
|
+
if not isinstance(other, (int, float)):
|
|
912
|
+
raise TypeError("Unsupported operand for * expected number")
|
|
913
|
+
return distance_mm(self.distance_mm * other)
|
|
914
|
+
|
|
915
|
+
def __truediv__(self, other):
|
|
916
|
+
if not isinstance(other, (int, float)):
|
|
917
|
+
raise TypeError("Unsupported operand for / expected number")
|
|
918
|
+
return distance_mm(self.distance_mm / other)
|
|
919
|
+
|
|
920
|
+
@property
|
|
921
|
+
def distance_mm(self) -> float: # pylint: disable=redefined-outer-name
|
|
922
|
+
"""The distance in millimeters"""
|
|
923
|
+
return self._distance_mm
|
|
924
|
+
|
|
925
|
+
@property
|
|
926
|
+
def distance_inches(self) -> float: # pylint: disable=redefined-outer-name
|
|
927
|
+
return self._distance_mm / 25.4
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def distance_mm(distance_mm: float): # pylint: disable=redefined-outer-name
|
|
931
|
+
"""Returns an :class:`anki_vector.util.Distance` instance set to the specified number of millimeters."""
|
|
932
|
+
return Distance(distance_mm=distance_mm)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def distance_inches(distance_inches: float): # pylint: disable=redefined-outer-name
|
|
936
|
+
"""Returns an :class:`anki_vector.util.Distance` instance set to the specified number of inches."""
|
|
937
|
+
return Distance(distance_inches=distance_inches)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class Speed:
|
|
941
|
+
"""Represents a speed.
|
|
942
|
+
|
|
943
|
+
This class allows speeds to be measured in millimeters per second.
|
|
944
|
+
|
|
945
|
+
The maximum speed is 220 mm/s and is clamped internally.
|
|
946
|
+
|
|
947
|
+
Use :func:`speed_mmps` convenience methods to generate
|
|
948
|
+
a Speed instance.
|
|
949
|
+
|
|
950
|
+
:param speed_mmps: The number of millimeters per second the speed
|
|
951
|
+
should represent.
|
|
952
|
+
"""
|
|
953
|
+
|
|
954
|
+
__slots__ = ('_speed_mmps')
|
|
955
|
+
|
|
956
|
+
def __init__(self, speed_mmps: float = None): # pylint: disable=redefined-outer-name
|
|
957
|
+
if speed_mmps is None:
|
|
958
|
+
raise ValueError("Expected speed_mmps keyword argument")
|
|
959
|
+
self._speed_mmps = float(speed_mmps)
|
|
960
|
+
|
|
961
|
+
def __repr__(self):
|
|
962
|
+
return "<%s %.2f mmps>" % (self.__class__.__name__, self.speed_mmps)
|
|
963
|
+
|
|
964
|
+
def __add__(self, other):
|
|
965
|
+
if not isinstance(other, Speed):
|
|
966
|
+
raise TypeError("Unsupported operand for + expected Speed")
|
|
967
|
+
return speed_mmps(self.speed_mmps + other.speed_mmps)
|
|
968
|
+
|
|
969
|
+
def __sub__(self, other):
|
|
970
|
+
if not isinstance(other, Speed):
|
|
971
|
+
raise TypeError("Unsupported operand for - expected Speed")
|
|
972
|
+
return speed_mmps(self.speed_mmps - other.speed_mmps)
|
|
973
|
+
|
|
974
|
+
def __mul__(self, other):
|
|
975
|
+
if not isinstance(other, (int, float)):
|
|
976
|
+
raise TypeError("Unsupported operand for * expected number")
|
|
977
|
+
return speed_mmps(self.speed_mmps * other)
|
|
978
|
+
|
|
979
|
+
def __truediv__(self, other):
|
|
980
|
+
if not isinstance(other, (int, float)):
|
|
981
|
+
raise TypeError("Unsupported operand for / expected number")
|
|
982
|
+
return speed_mmps(self.speed_mmps / other)
|
|
983
|
+
|
|
984
|
+
@property
|
|
985
|
+
def speed_mmps(self: float) -> float: # pylint: disable=redefined-outer-name
|
|
986
|
+
"""The speed in millimeters per second (mmps)."""
|
|
987
|
+
return self._speed_mmps
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
def speed_mmps(speed_mmps: float): # pylint: disable=redefined-outer-name
|
|
991
|
+
""":class:`anki_vector.util.Speed` instance set to the specified millimeters per second speed."""
|
|
992
|
+
return Speed(speed_mmps=speed_mmps)
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
class BaseOverlay:
|
|
996
|
+
"""A base overlay is used as a base class for other forms of overlays that can be drawn on top of an image.
|
|
997
|
+
|
|
998
|
+
:param line_thickness: The thickness of the line being drawn.
|
|
999
|
+
:param line_color: The color of the line to be drawn.
|
|
1000
|
+
"""
|
|
1001
|
+
|
|
1002
|
+
def __init__(self, line_thickness: int, line_color: tuple):
|
|
1003
|
+
self._line_thickness: int = line_thickness
|
|
1004
|
+
self._line_color: tuple = line_color
|
|
1005
|
+
|
|
1006
|
+
@property
|
|
1007
|
+
def line_thickness(self) -> int:
|
|
1008
|
+
"""The thickness of the line being drawn."""
|
|
1009
|
+
return self._line_thickness
|
|
1010
|
+
|
|
1011
|
+
@property
|
|
1012
|
+
def line_color(self) -> tuple:
|
|
1013
|
+
"""The color of the line to be drawn."""
|
|
1014
|
+
return self._line_color
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
class RectangleOverlay(BaseOverlay):
|
|
1018
|
+
"""A rectangle that can be drawn on top of a given image.
|
|
1019
|
+
|
|
1020
|
+
:param width: The width of the rectangle to be drawn.
|
|
1021
|
+
:param height: The height of the rectangle to be drawn.
|
|
1022
|
+
:param line_thickness: The thickness of the line being drawn.
|
|
1023
|
+
:param line_color: The color of the line to be drawn.
|
|
1024
|
+
"""
|
|
1025
|
+
|
|
1026
|
+
# @TODO Implement overlay using an ImageRect rather than a raw width & height
|
|
1027
|
+
def __init__(self, width: int, height: int, line_thickness: int = 5, line_color: tuple = (255, 0, 0)):
|
|
1028
|
+
super().__init__(line_thickness, line_color)
|
|
1029
|
+
self._width: int = width
|
|
1030
|
+
self._height: int = height
|
|
1031
|
+
|
|
1032
|
+
@property
|
|
1033
|
+
def width(self) -> int:
|
|
1034
|
+
"""The width of the rectangle to be drawn."""
|
|
1035
|
+
return self._width
|
|
1036
|
+
|
|
1037
|
+
@property
|
|
1038
|
+
def height(self) -> int:
|
|
1039
|
+
"""The height of the rectangle to be drawn."""
|
|
1040
|
+
return self._height
|
|
1041
|
+
|
|
1042
|
+
def apply_overlay(self, image: Image.Image) -> None:
|
|
1043
|
+
"""Draw a rectangle on top of the given image."""
|
|
1044
|
+
d = ImageDraw.Draw(image)
|
|
1045
|
+
|
|
1046
|
+
image_width, image_height = image.size
|
|
1047
|
+
remaining_width = image_width - self.width
|
|
1048
|
+
remaining_height = image_height - self.height
|
|
1049
|
+
x1, y1 = remaining_width // 2, remaining_height // 2
|
|
1050
|
+
x2, y2 = (image_width - (remaining_width // 2)), (image_height - (remaining_height // 2))
|
|
1051
|
+
|
|
1052
|
+
for i in range(0, self.line_thickness):
|
|
1053
|
+
d.rectangle([x1 + i, y1 + i, x2 - i, y2 - i], outline=self.line_color)
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
class Component:
|
|
1057
|
+
""" Base class for all components."""
|
|
1058
|
+
|
|
1059
|
+
def __init__(self, robot):
|
|
1060
|
+
self.logger = get_class_logger(__name__, self)
|
|
1061
|
+
self._robot = robot
|
|
1062
|
+
|
|
1063
|
+
@property
|
|
1064
|
+
def robot(self):
|
|
1065
|
+
return self._robot
|
|
1066
|
+
|
|
1067
|
+
@property
|
|
1068
|
+
def conn(self):
|
|
1069
|
+
return self._robot.conn
|
|
1070
|
+
|
|
1071
|
+
@property
|
|
1072
|
+
def force_async(self):
|
|
1073
|
+
return self._robot.force_async
|
|
1074
|
+
|
|
1075
|
+
@property
|
|
1076
|
+
def grpc_interface(self):
|
|
1077
|
+
"""A direct reference to the connected aiogrpc interface.
|
|
1078
|
+
"""
|
|
1079
|
+
return self._robot.conn.grpc_interface
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def read_configuration(serial: str, name: str, logger: logging.Logger) -> dict:
|
|
1083
|
+
"""Open the default conf file, and read it into a :class:`configparser.ConfigParser`
|
|
1084
|
+
If :code:`serial is not None`, this method will try to find a configuration with serial
|
|
1085
|
+
number :code:`serial`, and raise an exception otherwise. If :code:`serial is None` and
|
|
1086
|
+
:code:`name is not None`, this method will try to find a configuration which matches
|
|
1087
|
+
the provided name, and raise an exception otherwise. If both :code:`serial is None` and
|
|
1088
|
+
:code:`name is None`, this method will return a configuration if exactly `1` exists, but
|
|
1089
|
+
if multiple configurations exists, it will raise an exception.
|
|
1090
|
+
|
|
1091
|
+
:param serial: Vector's serial number
|
|
1092
|
+
:param name: Vector's name
|
|
1093
|
+
"""
|
|
1094
|
+
home = Path.home() / ".anki_vector"
|
|
1095
|
+
conf_file = str(home / "sdk_config.ini")
|
|
1096
|
+
parser = configparser.ConfigParser(strict=False)
|
|
1097
|
+
parser.read(conf_file)
|
|
1098
|
+
|
|
1099
|
+
sections = parser.sections()
|
|
1100
|
+
if not sections:
|
|
1101
|
+
raise VectorConfigurationException('Could not find the sdk configuration file. Please run `python3 -m anki_vector.configure` or `python3 -m anki_vector.configure_pod` to set up your Vector for SDK usage.')
|
|
1102
|
+
elif (serial is None) and (name is None):
|
|
1103
|
+
if len(sections) == 1:
|
|
1104
|
+
serial = sections[0]
|
|
1105
|
+
logger.warning("No serial number or name provided. Automatically selecting {}".format(serial))
|
|
1106
|
+
else:
|
|
1107
|
+
raise VectorConfigurationException("Found multiple robot serial numbers. "
|
|
1108
|
+
"Please provide the serial number or name of the Robot you want to control.\n\n"
|
|
1109
|
+
"Example: ./01_hello_world.py --serial {{robot_serial_number}}")
|
|
1110
|
+
|
|
1111
|
+
config = {k.lower(): v for k, v in parser.items()}
|
|
1112
|
+
|
|
1113
|
+
if serial is not None:
|
|
1114
|
+
serial = serial.lower()
|
|
1115
|
+
try:
|
|
1116
|
+
return config[serial]
|
|
1117
|
+
except KeyError:
|
|
1118
|
+
raise VectorConfigurationException("Could not find matching robot info for given serial number: {}. "
|
|
1119
|
+
"Please check your serial number is correct.\n\n"
|
|
1120
|
+
"Example: ./01_hello_world.py --serial {{robot_serial_number}}", serial)
|
|
1121
|
+
else:
|
|
1122
|
+
for keySerial in config:
|
|
1123
|
+
for key in config[keySerial]:
|
|
1124
|
+
if config[keySerial][key] == name:
|
|
1125
|
+
return config[keySerial]
|
|
1126
|
+
if config[keySerial][key].lower() == name.lower():
|
|
1127
|
+
logger.warning("Using case-insensitive name match found in config. Set 'name' field to match 'Vector-A1B2' format.")
|
|
1128
|
+
return config[keySerial]
|
|
1129
|
+
|
|
1130
|
+
raise VectorConfigurationException("Could not find matching robot info for given name: {}. "
|
|
1131
|
+
"Please check your name is correct.\n\n"
|
|
1132
|
+
"Example: ./01_hello_world.py --name {{robot_name}}", name)
|