You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

util.py 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. # Copyright (c) 2016-2017 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. # http://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. '''Utility classes and functions.'''
  15. # __all__ should order by constants, event classes, other classes, functions.
  16. # (util keeps class related functions close to their associated class)
  17. __all__ = ['Angle', 'degrees', 'radians',
  18. 'ImageBox',
  19. 'Distance', 'distance_mm', 'distance_inches', 'Matrix44',
  20. 'Pose', 'pose_quaternion', 'pose_z_angle',
  21. 'Position', 'Quaternion',
  22. 'Rotation', 'rotation_quaternion', 'rotation_z_angle',
  23. 'angle_z_to_quaternion',
  24. 'Speed', 'speed_mmps',
  25. 'Timeout', 'Vector2', 'Vector3']
  26. import collections
  27. import math
  28. import time
  29. from ._clad import _clad_to_engine_anki
  30. class ImageBox(collections.namedtuple('ImageBox', 'top_left_x top_left_y width height')):
  31. '''Defines a bounding box within an image frame.
  32. This is used when objects, faces and pets are observed to denote where in
  33. the robot's camera view the object, face or pet actually appears. It's then
  34. used by the :mod:`cozmo.annotate` module to show an outline of a box around
  35. the object, face or pet.
  36. .. py:attribute:: width
  37. float - The width of the box.
  38. .. py:attribute:: height
  39. float - The height of the box.
  40. '''
  41. __slots__ = ()
  42. @classmethod
  43. def _create_from_clad_rect(cls, img_rect):
  44. return cls(img_rect.x_topLeft, img_rect.y_topLeft,
  45. img_rect.width, img_rect.height)
  46. @property
  47. def left_x(self):
  48. """float: The x coordinate of the left of the box."""
  49. return self.top_left_x
  50. @property
  51. def right_x(self):
  52. """float: The x coordinate of the right of the box."""
  53. return self.top_left_x + self.width
  54. @property
  55. def top_y(self):
  56. """float: The y coordinate of the top of the box."""
  57. return self.top_left_y
  58. @property
  59. def bottom_y(self):
  60. """float: The y coordinate of the bottom of the box."""
  61. return self.top_left_y + self.height
  62. @property
  63. def center(self):
  64. """(float, float): The x,y coordinates of the center of the box."""
  65. cen_x = self.top_left_x + (self.width * 0.5)
  66. cen_y = self.top_left_y + (self.height * 0.5)
  67. return cen_x, cen_y
  68. def __mul__(self, other):
  69. return ImageBox(self[0] * other, self[1] * other, self[2] * other, self[3] * other)
  70. class Angle:
  71. '''Represents an angle.
  72. Use the :func:`degrees` or :func:`radians` convenience methods to generate
  73. an Angle instance.
  74. Args:
  75. radians (float): The number of radians the angle should represent
  76. (cannot be combined with ``degrees``)
  77. degrees (float): The number of degress the angle should represent
  78. (cannot be combined with ``radians``)
  79. '''
  80. __slots__ = ('_radians')
  81. def __init__(self, radians=None, degrees=None):
  82. if radians is None and degrees is None:
  83. raise ValueError("Expected either the degrees or radians keyword argument")
  84. if radians and degrees:
  85. raise ValueError("Expected either the degrees or radians keyword argument, not both")
  86. if degrees is not None:
  87. radians = degrees * math.pi / 180
  88. self._radians = float(radians)
  89. def __repr__(self):
  90. return "<%s %.2f radians (%.2f degrees)>" % (self.__class__.__name__, self.radians, self.degrees)
  91. def __add__(self, other):
  92. if not isinstance(other, Angle):
  93. raise TypeError("Unsupported type for + expected Angle")
  94. return radians(self.radians + other.radians)
  95. def __sub__(self, other):
  96. if not isinstance(other, Angle):
  97. raise TypeError("Unsupported type for - expected Angle")
  98. return radians(self.radians - other.radians)
  99. def __mul__(self, other):
  100. if not isinstance(other, (int, float)):
  101. raise TypeError("Unsupported type for * expected number")
  102. return radians(self.radians * other)
  103. def __truediv__(self, other):
  104. if not isinstance(other, (int, float)):
  105. raise TypeError("Unsupported type for / expected number")
  106. return radians(self.radians / other)
  107. def _cmp_int(self, other):
  108. if not isinstance(other, Angle):
  109. raise TypeError("Unsupported type for comparison expected Angle")
  110. return self.radians - other.radians
  111. def __eq__(self, other):
  112. return self._cmp_int(other) == 0
  113. def __ne__(self, other):
  114. return self._cmp_int(other) != 0
  115. def __gt__(self, other):
  116. return self._cmp_int(other) > 0
  117. def __lt__(self, other):
  118. return self._cmp_int(other) < 0
  119. def __ge__(self, other):
  120. return self._cmp_int(other) >= 0
  121. def __le__(self, other):
  122. return self._cmp_int(other) <= 0
  123. @property
  124. def radians(self):
  125. '''float: The angle in radians.'''
  126. return self._radians
  127. @property
  128. def degrees(self):
  129. '''float: The angle in degrees.'''
  130. return self._radians / math.pi * 180
  131. @property
  132. def abs_value(self):
  133. """:class:`cozmo.util.Angle`: The absolute value of the angle.
  134. If the Angle is positive then it returns a copy of this Angle, otherwise it returns -Angle.
  135. """
  136. return Angle(radians = abs(self._radians))
  137. def degrees(degrees):
  138. '''Returns an :class:`cozmo.util.Angle` instance set to the specified number of degrees.'''
  139. return Angle(degrees=degrees)
  140. def radians(radians):
  141. '''Returns an :class:`cozmo.util.Angle` instance set to the specified number of radians.'''
  142. return Angle(radians=radians)
  143. class Distance:
  144. '''Represents a distance.
  145. The class allows distances to be returned in either millimeters or inches.
  146. Use the :func:`distance_inches` or :func:`distance_mm` convenience methods to generate
  147. a Distance instance.
  148. Args:
  149. distance_mm (float): The number of millimeters the distance should
  150. represent (cannot be combined with ``distance_inches``).
  151. distance_inches (float): The number of inches the distance should
  152. represent (cannot be combined with ``distance_mm``).
  153. '''
  154. __slots__ = ('_distance_mm')
  155. def __init__(self, distance_mm=None, distance_inches=None):
  156. if distance_mm is None and distance_inches is None:
  157. raise ValueError("Expected either the distance_mm or distance_inches keyword argument")
  158. if distance_mm and distance_inches:
  159. raise ValueError("Expected either the distance_mm or distance_inches keyword argument, not both")
  160. if distance_inches is not None:
  161. distance_mm = distance_inches * 25.4
  162. self._distance_mm = distance_mm
  163. def __repr__(self):
  164. return "<%s %.2f mm (%.2f inches)>" % (self.__class__.__name__, self.distance_mm, self.distance_inches)
  165. def __add__(self, other):
  166. if not isinstance(other, Distance):
  167. raise TypeError("Unsupported operand for + expected Distance")
  168. return distance_mm(self.distance_mm + other.distance_mm)
  169. def __sub__(self, other):
  170. if not isinstance(other, Distance):
  171. raise TypeError("Unsupported operand for - expected Distance")
  172. return distance_mm(self.distance_mm - other.distance_mm)
  173. def __mul__(self, other):
  174. if not isinstance(other, (int, float)):
  175. raise TypeError("Unsupported operand for * expected number")
  176. return distance_mm(self.distance_mm * other)
  177. def __truediv__(self, other):
  178. if not isinstance(other, (int, float)):
  179. raise TypeError("Unsupported operand for / expected number")
  180. return distance_mm(self.distance_mm / other)
  181. @property
  182. def distance_mm(self):
  183. '''float: The distance in millimeters'''
  184. return self._distance_mm
  185. @property
  186. def distance_inches(self):
  187. '''float: The distance in inches'''
  188. return self._distance_mm / 25.4
  189. def distance_mm(distance_mm):
  190. '''Returns an :class:`cozmo.util.Distance` instance set to the specified number of millimeters.'''
  191. return Distance(distance_mm=distance_mm)
  192. def distance_inches(distance_inches):
  193. '''Returns an :class:`cozmo.util.Distance` instance set to the specified number of inches.'''
  194. return Distance(distance_inches=distance_inches)
  195. class Speed:
  196. '''Represents a speed.
  197. This class allows speeds to be measured in millimeters per second.
  198. Use :func:`speed_mmps` convenience methods to generate
  199. a Speed instance.
  200. Args:
  201. speed_mmps (float): The number of millimeters per second the speed
  202. should represent.
  203. '''
  204. __slots__ = ('_speed_mmps')
  205. def __init__(self, speed_mmps=None):
  206. if speed_mmps is None:
  207. raise ValueError("Expected speed_mmps keyword argument")
  208. self._speed_mmps = speed_mmps
  209. def __repr__(self):
  210. return "<%s %.2f mmps>" % (self.__class__.__name__, self.speed_mmps)
  211. def __add__(self, other):
  212. if not isinstance(other, Speed):
  213. raise TypeError("Unsupported operand for + expected Speed")
  214. return speed_mmps(self.speed_mmps + other.speed_mmps)
  215. def __sub__(self, other):
  216. if not isinstance(other, Speed):
  217. raise TypeError("Unsupported operand for - expected Speed")
  218. return speed_mmps(self.speed_mmps - other.speed_mmps)
  219. def __mul__(self, other):
  220. if not isinstance(other, (int, float)):
  221. raise TypeError("Unsupported operand for * expected number")
  222. return speed_mmps(self.speed_mmps * other)
  223. def __truediv__(self, other):
  224. if not isinstance(other, (int, float)):
  225. raise TypeError("Unsupported operand for / expected number")
  226. return speed_mmps(self.speed_mmps / other)
  227. @property
  228. def speed_mmps(self):
  229. '''float: The speed in millimeters per second (mmps).'''
  230. return self._speed_mmps
  231. def speed_mmps(speed_mmps):
  232. '''Returns an :class:`cozmo.util.Speed` instance set to the specified millimeters per second speed'''
  233. return Speed(speed_mmps=speed_mmps)
  234. class Pose:
  235. '''Represents where an object is in the world.
  236. Use the :func:'pose_quaternion' to return pose in the form of
  237. position and rotation defined by a quaternion
  238. Use the :func:'pose_z_angle' to return pose in the form of
  239. position and rotation defined by rotation about the z axis
  240. When the engine is initialized, and whenever Cozmo is de-localized (i.e.
  241. whenever Cozmo no longer knows where he is - e.g. when he's picked up)
  242. Cozmo creates a new pose starting at (0,0,0) with no rotation, with
  243. origin_id incremented to show that these poses cannot be compared with
  244. earlier ones. As Cozmo drives around, his pose (and the pose of other
  245. objects he observes - e.g. faces, cubes etc.) is relative to this initial
  246. position and orientation.
  247. The coordinate space is relative to Cozmo, where Cozmo's origin is the
  248. point on the ground between Cozmo's two front wheels:
  249. The X axis is Cozmo's forward direction
  250. The Y axis is to Cozmo's left
  251. The Z axis is up
  252. Only poses of the same origin_id can safely be compared or operated on
  253. '''
  254. __slots__ = ('_position', '_rotation', '_origin_id', '_is_accurate')
  255. def __init__(self, x, y, z, q0=None, q1=None, q2=None, q3=None,
  256. angle_z=None, origin_id=-1, is_accurate=True):
  257. self._position = Position(x,y,z)
  258. self._rotation = Quaternion(q0,q1,q2,q3,angle_z)
  259. self._origin_id = origin_id
  260. self._is_accurate = is_accurate
  261. @classmethod
  262. def _create_from_clad(cls, pose):
  263. return cls(pose.x, pose.y, pose.z,
  264. q0=pose.q0, q1=pose.q1, q2=pose.q2, q3=pose.q3,
  265. origin_id=pose.originID)
  266. @classmethod
  267. def _create_default(cls):
  268. return cls(0.0, 0.0, 0.0,
  269. q0=1.0, q1=0.0, q2=0.0, q3=0.0,
  270. origin_id=-1)
  271. def __repr__(self):
  272. return "<%s %s %s origin_id=%d>" % (self.__class__.__name__, self.position, self.rotation, self.origin_id)
  273. def __add__(self, other):
  274. if not isinstance(other, Pose):
  275. raise TypeError("Unsupported operand for + expected Pose")
  276. pos = self.position + other.position
  277. rot = self.rotation + other.rotation
  278. return pose_quaternion(pos.x, pos.y, pos.z, rot.q0, rot.q1, rot.q2, rot.q3)
  279. def __sub__(self, other):
  280. if not isinstance(other, Pose):
  281. raise TypeError("Unsupported operand for - expected Pose")
  282. pos = self.position - other.position
  283. rot = self.rotation - other.rotation
  284. return pose_quaternion(pos.x, pos.y, pos.z, rot.q0, rot.q1, rot.q2, rot.q3)
  285. def __mul__(self, other):
  286. if not isinstance(other, (int, float)):
  287. raise TypeError("Unsupported operand for * expected number")
  288. pos = self.position * other
  289. rot = self.rotation * other
  290. return pose_quaternion(pos.x, pos.y, pos.z, rot.q0, rot.q1, rot.q2, rot.q3)
  291. def __truediv__(self, other):
  292. if not isinstance(other, (int, float)):
  293. raise TypeError("Unsupported operand for / expected number")
  294. pos = self.position / other
  295. rot = self.rotation / other
  296. return pose_quaternion(pos.x, pos.y, pos.z, rot.q0, rot.q1, rot.q2, rot.q3)
  297. def define_pose_relative_this(self, new_pose):
  298. '''Creates a new pose such that new_pose's origin is now at the location of this pose.
  299. Args:
  300. new_pose (:class:`cozmo.util.Pose`): The pose which origin is being changed.
  301. Returns:
  302. A :class:`cozmo.util.pose` object for which the origin was this pose's origin.
  303. '''
  304. if not isinstance(new_pose, Pose):
  305. raise TypeError("Unsupported type for new_origin, must be of type Pose")
  306. x,y,z = self.position.x_y_z
  307. angle_z = self.rotation.angle_z
  308. new_x,new_y,new_z = new_pose.position.x_y_z
  309. new_angle_z = new_pose.rotation.angle_z
  310. cos_angle = math.cos(angle_z.radians)
  311. sin_angle = math.sin(angle_z.radians)
  312. res_x = x + (cos_angle * new_x) - (sin_angle * new_y)
  313. res_y = y + (sin_angle * new_x) + (cos_angle * new_y)
  314. res_z = z + new_z
  315. res_angle = angle_z + new_angle_z
  316. return Pose(res_x, res_y, res_z, angle_z=res_angle, origin_id=self._origin_id)
  317. def encode_pose(self):
  318. x, y, z = self.position.x_y_z
  319. q0, q1, q2, q3 = self.rotation.q0_q1_q2_q3
  320. return _clad_to_engine_anki.PoseStruct3d(x, y, z, q0, q1, q2, q3, self.origin_id)
  321. def invalidate(self):
  322. '''Mark this pose as being invalid (unusable)'''
  323. self._origin_id = -1
  324. def is_comparable(self, other_pose):
  325. '''Are these two poses comparable.
  326. Poses are comparable if they're valid and having matching origin IDs.
  327. Args:
  328. other_pose (:class:`cozmo.util.Pose`): The other pose to compare against.
  329. Returns:
  330. bool: True if the two poses are comparable, False otherwise.
  331. '''
  332. return (self.is_valid and other_pose.is_valid and
  333. (self.origin_id == other_pose.origin_id))
  334. @property
  335. def is_valid(self):
  336. '''bool: Returns True if this is a valid, usable pose.'''
  337. return self.origin_id >= 0
  338. @property
  339. def position(self):
  340. ''':class:`cozmo.util.Position`: The position component of this pose.'''
  341. return self._position
  342. @property
  343. def rotation(self):
  344. ''':class:`cozmo.util.Rotation`: The rotation component of this pose.'''
  345. return self._rotation
  346. def to_matrix(self):
  347. """Convert the Pose to a Matrix44.
  348. Returns:
  349. :class:`cozmo.util.Matrix44`: A matrix representing this Pose's
  350. position and rotation.
  351. """
  352. return self.rotation.to_matrix(*self.position.x_y_z)
  353. @property
  354. def origin_id(self):
  355. '''int: An ID maintained by the engine which represents which coordinate frame this pose is in.'''
  356. return self._origin_id
  357. @origin_id.setter
  358. def origin_id(self, value):
  359. '''Allows this to be changed later in case it was not originally defined.'''
  360. if not isinstance(value, int):
  361. raise TypeError("The type of origin_id must be int")
  362. self._origin_id = value
  363. @property
  364. def is_accurate(self):
  365. '''bool: Returns True if this pose is valid and accurate.
  366. Poses are marked as inaccurate if we detect movement via accelerometer,
  367. or if they were observed from far enough away that we're less certain
  368. of the exact pose.
  369. '''
  370. return self.is_valid and self._is_accurate
  371. def pose_quaternion(x, y, z, q0, q1, q2, q3, origin_id=0):
  372. '''Returns a :class:`cozmo.util.Pose` instance set to the pose given in quaternion format.'''
  373. return Pose(x, y, z, q0=q0, q1=q1, q2=q2, q3=q3, origin_id=origin_id)
  374. def pose_z_angle(x, y, z, angle_z, origin_id=0):
  375. '''Returns a :class:`cozmo.util.Pose` instance set to the pose given in z angle format.'''
  376. return Pose(x, y, z, angle_z=angle_z, origin_id=origin_id)
  377. class Matrix44:
  378. """A 4x4 Matrix for representing the rotation and/or position of an object in the world.
  379. Can be generated from a :class:`Quaternion` for a pure rotation matrix, or
  380. combined with a position for a full translation matrix, as done by
  381. :meth:`Pose.to_matrix`.
  382. """
  383. __slots__ = ('m00', 'm10', 'm20', 'm30',
  384. 'm01', 'm11', 'm21', 'm31',
  385. 'm02', 'm12', 'm22', 'm32',
  386. 'm03', 'm13', 'm23', 'm33')
  387. def __init__(self,
  388. m00, m10, m20, m30,
  389. m01, m11, m21, m31,
  390. m02, m12, m22, m32,
  391. m03, m13, m23, m33):
  392. self.m00 = m00
  393. self.m10 = m10
  394. self.m20 = m20
  395. self.m30 = m30
  396. self.m01 = m01
  397. self.m11 = m11
  398. self.m21 = m21
  399. self.m31 = m31
  400. self.m02 = m02
  401. self.m12 = m12
  402. self.m22 = m22
  403. self.m32 = m32
  404. self.m03 = m03
  405. self.m13 = m13
  406. self.m23 = m23
  407. self.m33 = m33
  408. def __repr__(self):
  409. return ("<%s: "
  410. "%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f "
  411. "%.1f %.1f %.1f %.1f %.1f %.1f %.1f %.1f>" % (
  412. self.__class__.__name__, *self.in_row_order))
  413. @property
  414. def tabulated_string(self):
  415. """str: A multi-line string formatted with tabs to show the matrix contents."""
  416. return ("%.1f\t%.1f\t%.1f\t%.1f\n"
  417. "%.1f\t%.1f\t%.1f\t%.1f\n"
  418. "%.1f\t%.1f\t%.1f\t%.1f\n"
  419. "%.1f\t%.1f\t%.1f\t%.1f" % self.in_row_order)
  420. @property
  421. def in_row_order(self):
  422. """tuple of 16 floats: The contents of the matrix in row order."""
  423. return self.m00, self.m01, self.m02, self.m03,\
  424. self.m10, self.m11, self.m12, self.m13,\
  425. self.m20, self.m21, self.m22, self.m23,\
  426. self.m30, self.m31, self.m32, self.m33
  427. @property
  428. def in_column_order(self):
  429. """tuple of 16 floats: The contents of the matrix in column order."""
  430. return self.m00, self.m10, self.m20, self.m30,\
  431. self.m01, self.m11, self.m21, self.m31,\
  432. self.m02, self.m12, self.m22, self.m32,\
  433. self.m03, self.m13, self.m23, self.m33
  434. @property
  435. def forward_xyz(self):
  436. """tuple of 3 floats: The x,y,z components representing the matrix's forward vector."""
  437. return self.m00, self.m01, self.m02
  438. @property
  439. def left_xyz(self):
  440. """tuple of 3 floats: The x,y,z components representing the matrix's left vector."""
  441. return self.m10, self.m11, self.m12
  442. @property
  443. def up_xyz(self):
  444. """tuple of 3 floats: The x,y,z components representing the matrix's up vector."""
  445. return self.m20, self.m21, self.m22
  446. @property
  447. def pos_xyz(self):
  448. """tuple of 3 floats: The x,y,z components representing the matrix's position vector."""
  449. return self.m30, self.m31, self.m32
  450. def set_forward(self, x, y, z):
  451. """Set the x,y,z components representing the matrix's forward vector.
  452. Args:
  453. x (float): The X component.
  454. y (float): The Y component.
  455. z (float): The Z component.
  456. """
  457. self.m00 = x
  458. self.m01 = y
  459. self.m02 = z
  460. def set_left(self, x, y, z):
  461. """Set the x,y,z components representing the matrix's left vector.
  462. Args:
  463. x (float): The X component.
  464. y (float): The Y component.
  465. z (float): The Z component.
  466. """
  467. self.m10 = x
  468. self.m11 = y
  469. self.m12 = z
  470. def set_up(self, x, y, z):
  471. """Set the x,y,z components representing the matrix's up vector.
  472. Args:
  473. x (float): The X component.
  474. y (float): The Y component.
  475. z (float): The Z component.
  476. """
  477. self.m20 = x
  478. self.m21 = y
  479. self.m22 = z
  480. def set_pos(self, x, y, z):
  481. """Set the x,y,z components representing the matrix's position vector.
  482. Args:
  483. x (float): The X component.
  484. y (float): The Y component.
  485. z (float): The Z component.
  486. """
  487. self.m30 = x
  488. self.m31 = y
  489. self.m32 = z
  490. class Quaternion:
  491. '''Represents the rotation of an object in the world. Can be generated with
  492. quaternion to define its rotation in 3d space, or with only a z axis rotation
  493. to define things limited to the x-y plane like Cozmo.
  494. Use the :func:`rotation_quaternion` to return rotation defined by a quaternion.
  495. Use the :func:`rotation_angle_z` to return rotation defined by an angle in the z axis.
  496. '''
  497. __slots__ = ('_q0', '_q1', '_q2', '_q3')
  498. def __init__(self, q0=None, q1=None, q2=None, q3=None, angle_z=None):
  499. is_quaternion = not (q0 is None) and not (q1 is None) and not (q2 is None) and not (q3 is None)
  500. if not is_quaternion and angle_z is None:
  501. raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword arguments")
  502. if is_quaternion and angle_z:
  503. raise ValueError("Expected either the q0 q1 q2 and q3 or angle_z keyword argument, not both")
  504. if angle_z is not None:
  505. if not isinstance(angle_z, Angle):
  506. raise TypeError("Unsupported type for angle_z expected Angle")
  507. q0,q1,q2,q3 = angle_z_to_quaternion(angle_z)
  508. self._q0, self._q1, self._q2, self._q3 = q0, q1, q2, q3
  509. def __repr__(self):
  510. return ("<%s q0: %.2f q1: %.2f q2: %.2f q3: %.2f (angle_z: %s)>" %
  511. (self.__class__.__name__, self.q0, self.q1, self.q2, self.q3, self.angle_z))
  512. def to_matrix(self, pos_x=0.0, pos_y=0.0, pos_z=0.0):
  513. """Convert the Quaternion to a 4x4 matrix representing this rotation.
  514. A position can also be provided to generate a full translation matrix.
  515. Args:
  516. pos_x (float): The x component for the position.
  517. pos_y (float): The y component for the position.
  518. pos_z (float): The z component for the position.
  519. Returns:
  520. :class:`cozmo.util.Matrix44`: A matrix representing this Quaternion's
  521. rotation, with the provided position (which defaults to 0,0,0).
  522. """
  523. # See https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
  524. q0q0 = self.q0 * self.q0
  525. q1q1 = self.q1 * self.q1
  526. q2q2 = self.q2 * self.q2
  527. q3q3 = self.q3 * self.q3
  528. q0x2 = self.q0 * 2.0 # saves 2 multiplies
  529. q0q1x2 = q0x2 * self.q1
  530. q0q2x2 = q0x2 * self.q2
  531. q0q3x2 = q0x2 * self.q3
  532. q1x2 = self.q1 * 2.0 # saves 1 multiply
  533. q1q2x2 = q1x2 * self.q2
  534. q1q3x2 = q1x2 * self.q3
  535. q2q3x2 = 2.0 * self.q2 * self.q3
  536. m00 = (q0q0 + q1q1 - q2q2 - q3q3)
  537. m01 = (q1q2x2 + q0q3x2)
  538. m02 = (q1q3x2 - q0q2x2)
  539. m10 = (q1q2x2 - q0q3x2)
  540. m11 = (q0q0 - q1q1 + q2q2 - q3q3)
  541. m12 = (q0q1x2 + q2q3x2)
  542. m20 = (q0q2x2 + q1q3x2)
  543. m21 = (q2q3x2 - q0q1x2)
  544. m22 = (q0q0 - q1q1 - q2q2 + q3q3)
  545. return Matrix44(m00, m10, m20, pos_x,
  546. m01, m11, m21, pos_y,
  547. m02, m12, m22, pos_z,
  548. 0.0, 0.0, 0.0, 1.0)
  549. #These are only for angle_z because quaternion addition/subtraction is not relevant here
  550. def __add__(self, other):
  551. if not isinstance(other, Quaternion):
  552. raise TypeError("Unsupported operand for + expected Quaternion")
  553. return rotation_z_angle(self.angle_z + other.angle_z)
  554. def __sub__(self, other):
  555. if not isinstance(other, Quaternion):
  556. raise TypeError("Unsupported operand for - expected Quaternion")
  557. return rotation_z_angle(self.angle_z - other.angle_z)
  558. def __mul__(self, other):
  559. if not isinstance(other, (int,float)):
  560. raise TypeError("Unsupported operand for * expected number")
  561. return rotation_z_angle(self.angle_z * other)
  562. def __truediv__(self, other):
  563. if not isinstance(other, (int,float)):
  564. raise TypeError("Unsupported operand for / expected number")
  565. return rotation_z_angle(self.angle_z / other)
  566. @property
  567. def q0(self):
  568. '''float: The q0 (w) value of the quaternion.'''
  569. return self._q0
  570. @property
  571. def q1(self):
  572. '''float: The q1 (i) value of the quaternion.'''
  573. return self._q1
  574. @property
  575. def q2(self):
  576. '''float: The q2 (j) value of the quaternion.'''
  577. return self._q2
  578. @property
  579. def q3(self):
  580. '''float: The q3 (k) value of the quaternion.'''
  581. return self._q3
  582. @property
  583. def q0_q1_q2_q3(self):
  584. '''tuple of float: Contains all elements of the quaternion (q0,q1,q2,q3)'''
  585. return self._q0,self._q1,self._q2,self._q3
  586. @property
  587. def angle_z(self):
  588. '''class:`Angle`: The z Euler component of the object's rotation.
  589. Defined as the rotation in the z axis.
  590. '''
  591. q0,q1,q2,q3 = self.q0_q1_q2_q3
  592. return Angle(radians=math.atan2(2*(q1*q2+q0*q3), 1-2*(q2**2+q3**2)))
  593. @property
  594. def euler_angles(self):
  595. '''tuple of float: Euler angles of an object.
  596. Returns the pitch, yaw, roll Euler components of the object's
  597. rotation defined as rotations in the x, y, and z axis respectively.
  598. It interprets the rotations performed in the order: Z, Y, X
  599. '''
  600. # convert to matrix
  601. matrix = self.to_matrix()
  602. # normalize the magnitudes of cos(roll)*sin(pitch) (i.e. m12) and
  603. # cos(roll)*cos(pitch) (ie. m22), to isolate cos(roll) to be compared
  604. # against -sin(roll) (m02). Unfortunately, this omits results with an
  605. # absolute angle larger than 90 degrees on roll.
  606. absolute_cos_roll = math.sqrt(matrix.m12*matrix.m12+matrix.m22*matrix.m22)
  607. near_gimbal_lock = absolute_cos_roll < 1e-6
  608. if not near_gimbal_lock:
  609. # general case euler decomposition
  610. pitch = math.atan2(matrix.m22, matrix.m12)
  611. yaw = math.atan2(matrix.m00, matrix.m01)
  612. roll = math.atan2(absolute_cos_roll, -matrix.m02)
  613. else:
  614. # special case euler angle decomposition near gimbal lock
  615. pitch = math.atan2(matrix.m11, -matrix.m21)
  616. yaw = 0
  617. roll = math.atan2(absolute_cos_roll, -matrix.m02)
  618. # adjust roll to be consistent with how we orient the device
  619. roll = math.pi * 0.5 - roll
  620. if roll > math.pi:
  621. roll -= math.pi * 2
  622. return pitch, yaw, roll
  623. class Rotation(Quaternion):
  624. '''An alias for :class:`Quaternion`'''
  625. __slots__ = ()
  626. def rotation_quaternion(q0, q1, q2, q3):
  627. '''Returns a :class:`Rotation` instance set by a quaternion.'''
  628. return Quaternion(q0=q0, q1=q1, q2=q2, q3=q3)
  629. def rotation_z_angle(angle_z):
  630. '''Returns a class:`Rotation` instance set by an angle in the z axis'''
  631. return Quaternion(angle_z=angle_z)
  632. def angle_z_to_quaternion(angle_z):
  633. '''This function converts an angle in the z axis (Euler angle z component) to a quaternion.
  634. Args:
  635. angle_z (:class:`cozmo.util.Angle`): The z axis angle.
  636. Returns:
  637. q0,q1,q2,q3 (float, float, float, float): A tuple with all the members
  638. of a quaternion defined by angle_z.
  639. '''
  640. #Define the quaternion to be converted from a Euler angle (x,y,z) of 0,0,angle_z
  641. #These equations have their original equations above, and simplified implemented
  642. # q0 = cos(x/2)*cos(y/2)*cos(z/2) + sin(x/2)*sin(y/2)*sin(z/2)
  643. q0 = math.cos(angle_z.radians/2)
  644. # q1 = sin(x/2)*cos(y/2)*cos(z/2) - cos(x/2)*sin(y/2)*sin(z/2)
  645. q1 = 0
  646. # q2 = cos(x/2)*sin(y/2)*cos(z/2) + sin(x/2)*cos(y/2)*sin(z/2)
  647. q2 = 0
  648. # q3 = cos(x/2)*cos(y/2)*sin(z/2) - sin(x/2)*sin(y/2)*cos(z/2)
  649. q3 = math.sin(angle_z.radians/2)
  650. return q0,q1,q2,q3
  651. class Vector2:
  652. '''Represents a 2D Vector (type/units aren't specified)
  653. Args:
  654. x (float): X component
  655. y (float): Y component
  656. '''
  657. __slots__ = ('_x', '_y')
  658. def __init__(self, x, y):
  659. self._x = x
  660. self._y = y
  661. def set_to(self, rhs):
  662. """Copy the x and y components of the given vector.
  663. Args:
  664. rhs (:class:`Vector2`): The right-hand-side of this assignment - the
  665. source vector to copy into this vector.
  666. """
  667. self._x = rhs.x
  668. self._y = rhs.y
  669. @property
  670. def x(self):
  671. '''float: The x component.'''
  672. return self._x
  673. @property
  674. def y(self):
  675. '''float: The y component.'''
  676. return self._y
  677. @property
  678. def x_y(self):
  679. '''tuple (float, float): The X, Y elements of the Vector2 (x,y)'''
  680. return self._x, self._y
  681. def __repr__(self):
  682. return "<%s x: %.2f y: %.2f>" % (self.__class__.__name__, self.x, self.y)
  683. def __add__(self, other):
  684. if not isinstance(other, Vector2):
  685. raise TypeError("Unsupported operand for + expected Vector2")
  686. return Vector2(self.x + other.x, self.y + other.y)
  687. def __sub__(self, other):
  688. if not isinstance(other, Vector2):
  689. raise TypeError("Unsupported operand for - expected Vector2")
  690. return Vector2(self.x - other.x, self.y - other.y)
  691. def __mul__(self, other):
  692. if not isinstance(other, (int, float)):
  693. raise TypeError("Unsupported operand for * expected number")
  694. return Vector2(self.x * other, self.y * other)
  695. def __truediv__(self, other):
  696. if not isinstance(other, (int, float)):
  697. raise TypeError("Unsupported operand for / expected number")
  698. return Vector2(self.x / other, self.y / other)
  699. class Vector3:
  700. '''Represents a 3D Vector (type/units aren't specified)
  701. Args:
  702. x (float): X component
  703. y (float): Y component
  704. z (float): Z component
  705. '''
  706. __slots__ = ('_x', '_y', '_z')
  707. def __init__(self, x, y, z):
  708. self._x = x
  709. self._y = y
  710. self._z = z
  711. def set_to(self, rhs):
  712. """Copy the x, y and z components of the given vector.
  713. Args:
  714. rhs (:class:`Vector3`): The right-hand-side of this assignment - the
  715. source vector to copy into this vector.
  716. """
  717. self._x = rhs.x
  718. self._y = rhs.y
  719. self._z = rhs.z
  720. @property
  721. def x(self):
  722. '''float: The x component.'''
  723. return self._x
  724. @property
  725. def y(self):
  726. '''float: The y component.'''
  727. return self._y
  728. @property
  729. def z(self):
  730. '''float: The z component.'''
  731. return self._z
  732. @property
  733. def x_y_z(self):
  734. '''tuple (float, float, float): The X, Y, Z elements of the Vector3 (x,y,z)'''
  735. return self._x, self._y, self._z
  736. def __repr__(self):
  737. return "<%s x: %.2f y: %.2f z: %.2f>" % (self.__class__.__name__, self.x, self.y, self.z)
  738. def __add__(self, other):
  739. if not isinstance(other, Vector3):
  740. raise TypeError("Unsupported operand for + expected Vector3")
  741. return Vector3(self.x + other.x, self.y + other.y, self.z + other.z)
  742. def __sub__(self, other):
  743. if not isinstance(other, Vector3):
  744. raise TypeError("Unsupported operand for - expected Vector3")
  745. return Vector3(self.x - other.x, self.y - other.y, self.z - other.z)
  746. def __mul__(self, other):
  747. if not isinstance(other, (int, float)):
  748. raise TypeError("Unsupported operand for * expected number")
  749. return Vector3(self.x * other, self.y * other, self.z * other)
  750. def __truediv__(self, other):
  751. if not isinstance(other, (int, float)):
  752. raise TypeError("Unsupported operand for / expected number")
  753. return Vector3(self.x / other, self.y / other, self.z / other)
  754. class Position(Vector3):
  755. '''Represents the position of an object in the world.
  756. A position consists of its x, y and z values in millimeters.
  757. Args:
  758. x (float): X position in millimeters
  759. y (float): Y position in millimeters
  760. z (float): Z position in millimeters
  761. '''
  762. __slots__ = ()
  763. class Timeout:
  764. '''Utility class to keep track of a timeout condition.
  765. This measures a timeout from the point in time that the class
  766. is instantiated.
  767. Args:
  768. timeout (float): Amount of time (in seconds) allotted to pass before
  769. considering the timeout condition to be met.
  770. use_inf (bool): If True, then :attr:`remaining` will return
  771. :const:`math.inf` if `timeout` is None, else it will return
  772. `None`.
  773. '''
  774. def __init__(self, timeout=None, use_inf=False):
  775. self.start = time.time()
  776. self.timeout = timeout
  777. self.use_inf = use_inf
  778. @property
  779. def is_timed_out(self):
  780. '''bool: True if the timeout has expired.'''
  781. if self.timeout is None:
  782. return False
  783. return time.time() - self.start > self.timeout
  784. @property
  785. def remaining(self):
  786. '''float: The number of seconds remaining before reaching the timeout.
  787. Will return a number of zero or higher, even if the timer has
  788. since expired (it will never return a negative value).
  789. Will return None or math.inf (if ``use_inf`` was passed as ``True``
  790. to the constructor) if the original timeout was ``None``.
  791. '''
  792. if self.timeout is None:
  793. return math.inf if self.use_inf else None
  794. return max(0, self.timeout - (time.time() - self.start))