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.

world.py 51KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141
  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. '''The "world" represents the robot's known view of its environment.
  15. This view includes objects, faces and pets it knows about and can currently
  16. "see" with its camera, along with what actions or behaviors the robot is
  17. current performing and the images coming back from the camera (if any).
  18. Almost all events emitted by the robot itself, objects, faces, pets and the
  19. camera can be observed directly on the :class:`World` object, which is
  20. itself accessible as :attr:`cozmo.robot.Robot.world`.
  21. For example, if you only need to know whether a particular cube has been
  22. tapped, you can call the :meth:`~cozmo.event.Dispatcher.wait_for` method
  23. directly on that cube's :class:`cozmo.objects.LightCube` instance. Eg::
  24. my_cube.wait_for(cozmo.objects.EvtObjectTapped)
  25. If, however, you want to wait for any cube to be tapped, you could instead
  26. call the :meth:`~cozmo.event.Dispatcher.wait_for` method on the
  27. :class:`World` object instead. Eg::
  28. robot.world.wait_for(cozmo.objects.EvtObjectTapped)
  29. In either case, ``wait_for`` will return the instance of the event's
  30. :class:`~cozmo.objects.EvtObjectTapped` class, which includes a
  31. :attr:`~cozmo.objects.EvtObjectTapped.obj` attribute, which identifies
  32. exactly which cube has been tapped.
  33. The :class:`World` object also has a :class:`cozmo.camera.Camera` instance
  34. associated with it. It emits :class:`EvtNewCameraImage` objects whenever
  35. a new camera image is available (generally up to 15 times per second),
  36. which includes the raw image from the camera, as well as an annotated version
  37. showing where faces, pets and objects have been observed.
  38. .. Note:: The camera must first be enabled to receive images by setting
  39. :attr:`~cozmo.camera.Camera.image_stream_enabled` to ``True``.
  40. '''
  41. # __all__ should order by constants, event classes, other classes, functions.
  42. __all__ = ['EvtNewCameraImage',
  43. 'CameraImage', 'World']
  44. import asyncio
  45. import collections
  46. import time
  47. from . import logger
  48. from . import annotate
  49. from . import event
  50. from . import faces
  51. from . import nav_memory_map
  52. from . import objects
  53. from . import pets
  54. from . import util
  55. from . import _clad
  56. from ._clad import _clad_to_engine_iface, _clad_to_game_cozmo
  57. class EvtNewCameraImage(event.Event):
  58. '''Dispatched when a new camera image is received and processed from the robot's camera.'''
  59. image = 'A CameraImage object'
  60. class World(event.Dispatcher):
  61. '''Represents the state of the world, as known to a Cozmo robot.'''
  62. #: callable: The factory function that returns a
  63. #: :class:`faces.Face` class or subclass instance.
  64. face_factory = faces.Face
  65. #: callable: The factory function that returns a
  66. #: :class:`pets.Pet` class or subclass instance.
  67. pet_factory = pets.Pet
  68. #: callable: The factory function that returns an
  69. #: :class:`objects.LightCube` class or subclass instance.
  70. light_cube_factory = objects.LightCube
  71. #: callable: The factory function that returns an
  72. #: :class:`objects.Charger` class or subclass instance.
  73. charger_factory = objects.Charger
  74. #: callable: The factory function that returns an
  75. #: :class:`objects.CustomObject` class or subclass instance.
  76. custom_object_factory = objects.CustomObject
  77. #: callable: The factory function that returns an
  78. #: :class:`annotate.ImageAnnotator` class or subclass instance.
  79. annotator_factory = annotate.ImageAnnotator
  80. def __init__(self, conn, robot, **kw):
  81. """
  82. @type conn: cozmo.conn.CozmoConnection
  83. @type robot: cozmo.robot.Robot
  84. """
  85. super().__init__(**kw)
  86. #: :class:`cozmo.conn.CozmoConnection`: The underlying connection to a device.
  87. self.conn = conn
  88. #: :class:`cozmo.annotate.ImageAnnotator`: The image annotator used
  89. #: to add annotations to the raw camera images.
  90. self.image_annotator = self.annotator_factory(self)
  91. #: :class:`cozmo.robot.Robot`: The primary robot
  92. self.robot = robot # type: cozmo.robot.Robot
  93. self.custom_objects = {}
  94. #: :class:`CameraImage`: The latest image received, or None.
  95. self.latest_image = None # type: CameraImage
  96. self.light_cubes = {}
  97. #: :class:`cozmo.objects.Charger`: Cozmo's charger.
  98. #: ``None`` if no charger connected or known about yet.
  99. self.charger = None # type: cozmo.objects.Charger
  100. self._last_image_number = -1
  101. self._objects = {}
  102. self._visible_object_counts = collections.defaultdict(int)
  103. self._visible_face_count = 0
  104. self._visible_pet_count = 0
  105. self._faces = {}
  106. self._pets = {}
  107. self._active_behavior = None
  108. self._active_action = None
  109. self._nav_memory_map = None # type: nav_memory_map.NavMemoryMapGrid
  110. self._pending_nav_memory_map = None # type: nav_memory_map.NavMemoryMapGrid
  111. self._init_light_cubes()
  112. #### Private Methods ####
  113. def _init_light_cubes(self):
  114. # Initialize 3 cubes, but don't assign object IDs yet - they aren't
  115. # fixed and will be sent over from the Engine on connection for any
  116. # connected / known cubes.
  117. self.light_cubes = {
  118. objects.LightCube1Id: self.light_cube_factory(objects.LightCube1Id, self.conn, self, dispatch_parent=self),
  119. objects.LightCube2Id: self.light_cube_factory(objects.LightCube2Id, self.conn, self, dispatch_parent=self),
  120. objects.LightCube3Id: self.light_cube_factory(objects.LightCube3Id, self.conn, self, dispatch_parent=self),
  121. }
  122. def _allocate_object_from_msg(self, msg):
  123. if msg.objectFamily == _clad_to_game_cozmo.ObjectFamily.LightCube:
  124. cube = self.light_cubes.get(msg.objectType)
  125. if not cube:
  126. logger.error('Received invalid cube objecttype=%s msg=%s', msg.objectType, msg)
  127. return
  128. cube.object_id = msg.objectID
  129. self._objects[cube.object_id] = cube
  130. cube._robot = self.robot # XXX this will move if/when we have multi-robot support
  131. logger.debug('Allocated object_id=%d to light cube %s', msg.objectID, cube)
  132. return cube
  133. elif msg.objectFamily == _clad_to_game_cozmo.ObjectFamily.Charger:
  134. charger = self.charger_factory(self.conn, self, msg.objectID, dispatch_parent=self)
  135. if self.charger:
  136. logger.error('Allocating multiple chargers: existing charger=%s msg=%s', self.charger, msg)
  137. self.charger = charger
  138. self._objects[charger.object_id] = charger
  139. charger._robot = self.robot # XXX this will move if/when we have multi-robot support
  140. logger.debug('Allocated object_id=%s to Charger %s', msg.objectID, charger)
  141. return charger
  142. elif msg.objectFamily == _clad_to_game_cozmo.ObjectFamily.CustomObject:
  143. # obj is the base object type for this custom object. We make instances of this for every
  144. # unique object_id we see of this custom object type.
  145. obj = self.custom_objects.get(msg.objectType)
  146. if not obj:
  147. logger.error('Received a custom object type: %s that has not been defined yet. Msg=%s' %
  148. (msg.objectType, msg))
  149. return
  150. custom_object = self.custom_object_factory(self.conn, self, obj.object_type,
  151. obj.x_size_mm, obj.y_size_mm, obj.z_size_mm,
  152. obj.marker_width_mm, obj.marker_height_mm,
  153. obj.is_unique, dispatch_parent=self)
  154. custom_object.object_id = msg.objectID
  155. self._objects[custom_object.object_id] = custom_object
  156. logger.debug('Allocated object_id=%s to CustomObject %s', msg.objectID, custom_object)
  157. return custom_object
  158. def _allocate_face_from_msg(self, msg):
  159. face = self.face_factory(self.conn, self, self.robot, dispatch_parent=self)
  160. face.face_id = msg.faceID
  161. self._faces[face.face_id] = face
  162. logger.debug('Allocated face_id=%s to face=%s', face.face_id, face)
  163. return face
  164. def _allocate_pet_from_msg(self, msg):
  165. pet = self.pet_factory(self.conn, self, self.robot, dispatch_parent=self)
  166. pet.pet_id = msg.petID
  167. self._pets[pet.pet_id] = pet
  168. logger.debug('Allocated pet_id=%s to pet=%s', pet.pet_id, pet)
  169. return pet
  170. def _update_visible_obj_count(self, obj, inc):
  171. obscls = objects.ObservableObject
  172. for cls in obj.__class__.__mro__:
  173. self._visible_object_counts[cls] += inc
  174. if cls == obscls:
  175. break
  176. #### Properties ####
  177. @property
  178. def active_behavior(self):
  179. '''bool: True if the robot is currently executing a behavior.'''
  180. return self._active_behavior
  181. @property
  182. def active_action(self):
  183. '''bool: True if Cozmo is currently executing an action.'''
  184. return self._active_action
  185. @property
  186. def visible_objects(self):
  187. '''generator: yields each object that Cozmo can currently see.
  188. For faces, see :meth:`visible_faces`.
  189. For pets, see :meth:`visible_pets`.
  190. Returns:
  191. A generator yielding :class:`cozmo.objects.BaseObject` instances
  192. '''
  193. for id, obj in self._objects.items():
  194. if obj.is_visible:
  195. yield obj
  196. def visible_object_count(self, object_type=None):
  197. '''Returns the number of objects that Cozmo can currently see.
  198. Args:
  199. object_type (:class:`~cozmo.objects.ObservableObject` subclass):
  200. Which type of object to count. If None, return the total
  201. number of currently visible objects.
  202. Returns:
  203. int: The number of objects that Cozmo can currently see.
  204. '''
  205. if object_type is None:
  206. object_type = objects.ObservableObject
  207. return self._visible_object_counts[object_type]
  208. @property
  209. def visible_faces(self):
  210. '''generator: yields each face that Cozmo can currently see.
  211. Returns:
  212. A generator yielding :class:`cozmo.faces.Face` instances
  213. '''
  214. for obj in self._faces.values():
  215. if obj.is_visible:
  216. yield obj
  217. def visible_face_count(self):
  218. '''Returns the number of faces that Cozmo can currently see.
  219. Returns:
  220. int: The number of faces currently visible.
  221. '''
  222. return self._visible_face_count
  223. @property
  224. def visible_pets(self):
  225. '''generator: yields each pet that Cozmo can currently see.
  226. Returns:
  227. A generator yielding :class:`cozmo.pets.Pet` instances
  228. '''
  229. for obj in self._pets.values():
  230. if obj.is_visible:
  231. yield obj
  232. def visible_pet_count(self):
  233. '''Returns the number of pets that Cozmo can currently see.
  234. Returns:
  235. int: The number of pets currently visible.
  236. '''
  237. return self._visible_pet_count
  238. def get_light_cube(self, cube_id):
  239. """Returns the light cube with the given cube ID
  240. Args:
  241. cube_id (int): The light cube ID - should be one of
  242. :attr:`~cozmo.objects.LightCube1Id`,
  243. :attr:`~cozmo.objects.LightCube2Id` and
  244. :attr:`~cozmo.objects.LightCube3Id`. Note: the cube_id is not
  245. the same thing as the object_id.
  246. Returns:
  247. :class:`cozmo.objects.LightCube`: The LightCube object with that cube_id
  248. Raises:
  249. :class:`ValueError` if the cube_id is invalid.
  250. """
  251. if cube_id not in objects.LightCubeIDs:
  252. raise ValueError("Invalid cube_id %s" % cube_id)
  253. cube = self.light_cubes.get(cube_id)
  254. # Only return the cube if it has an object_id
  255. if cube.object_id is not None:
  256. return cube
  257. return None
  258. @property
  259. def connected_light_cubes(self):
  260. '''generator: yields each LightCube that Cozmo is currently connected to.
  261. Returns:
  262. A generator yielding :class:`cozmo.objects.LightCube` instances
  263. '''
  264. for cube_id in objects.LightCubeIDs:
  265. cube = self.light_cubes.get(cube_id)
  266. if cube and cube.is_connected:
  267. yield cube
  268. @property
  269. def nav_memory_map(self):
  270. """Returns the latest navigation memory map for Cozmo.
  271. Returns:
  272. :class:`~cozmo.nav_memory_map.NavMemoryMapGrid`: Current navigation
  273. memory map. This will be none unless you've previously called
  274. :meth:`~cozmo.world.request_nav_memory_map` with a positive
  275. frequency to request the data be sent over from the engine.
  276. """
  277. return self._nav_memory_map
  278. #### Private Event Handlers ####
  279. def _recv_msg_robot_observed_object(self, evt, *, msg):
  280. #The engine still sends observed messages for fixed custom objects, this is a bug
  281. if evt.msg.objectType == _clad_to_game_cozmo.ObjectType.CustomFixedObstacle:
  282. return
  283. obj = self._objects.get(msg.objectID)
  284. if not obj:
  285. obj = self._allocate_object_from_msg(msg)
  286. if obj:
  287. obj.dispatch_event(evt)
  288. def _recv_msg_robot_observed_face(self, evt, *, msg):
  289. if msg.faceID < 0:
  290. # this face is being tracked, but is not yet recognized - ignore
  291. return
  292. face = self._faces.get(msg.faceID)
  293. if not face:
  294. face = self._allocate_face_from_msg(msg)
  295. if face:
  296. face.dispatch_event(evt)
  297. def _recv_msg_robot_changed_observed_face_id(self, evt, *, msg):
  298. old_face = self._faces.get(msg.oldID)
  299. if old_face:
  300. old_face.dispatch_event(evt)
  301. def _recv_msg_robot_renamed_enrolled_face(self, evt, *, msg):
  302. face = self._faces.get(msg.faceID)
  303. if face:
  304. face.dispatch_event(evt)
  305. def _recv_msg_robot_erased_enrolled_face(self, evt, *, msg):
  306. face = self._faces.get(msg.faceID)
  307. if face:
  308. face.dispatch_event(evt)
  309. def _recv_msg_robot_observed_pet(self, evt, *, msg):
  310. pet = self._pets.get(msg.petID)
  311. if not pet:
  312. pet = self._allocate_pet_from_msg(msg)
  313. if pet:
  314. pet.dispatch_event(evt)
  315. def _dispatch_object_event(self, evt, msg):
  316. obj = self._objects.get(msg.objectID)
  317. if not obj:
  318. logger.warning('%s event received for unknown object ID %s', type(msg).__name__, msg.objectID)
  319. return
  320. obj.dispatch_event(evt)
  321. def _recv_msg_object_tapped(self, evt, *, msg):
  322. self._dispatch_object_event(evt, msg)
  323. def _recv_msg_object_moved(self, evt, *, msg):
  324. self._dispatch_object_event(evt, msg)
  325. def _recv_msg_object_stopped_moving(self, evt, *, msg):
  326. self._dispatch_object_event(evt, msg)
  327. def _recv_msg_object_power_level(self, evt, *, msg):
  328. self._dispatch_object_event(evt, msg)
  329. def _recv_msg_object_connection_state(self, evt, *, msg):
  330. self._dispatch_object_event(evt, msg)
  331. def _recv_msg_connected_object_states(self, evt, *, msg):
  332. # This is received on startup as a response to RequestConnectedObjects.
  333. for object_state in msg.objects:
  334. obj = self._objects.get(object_state.objectID)
  335. if not obj:
  336. obj = self._allocate_object_from_msg(object_state)
  337. if obj:
  338. obj._handle_connected_object_state(object_state)
  339. def _recv_msg_located_object_states(self, evt, *, msg):
  340. # This is received on startup as a response to RequestLocatedObjectStates.
  341. # It's also automatically sent from Engine whenever poses are rejiggered.
  342. updated_objects = set()
  343. for object_state in msg.objects:
  344. obj = self._objects.get(object_state.objectID)
  345. if not obj:
  346. obj = self._allocate_object_from_msg(object_state)
  347. if obj:
  348. obj._handle_located_object_state(object_state)
  349. updated_objects.add(object_state.objectID)
  350. # ensure that all objects not received have invalidated poses
  351. for id, obj in self._objects.items():
  352. if (id not in updated_objects) and obj.pose.is_valid:
  353. obj.pose.invalidate()
  354. def _recv_msg_robot_deleted_located_object(self, evt, *, msg):
  355. obj = self._objects.get(msg.objectID)
  356. if obj is None:
  357. logger.warning("Ignoring deleted_located_object for unknown object ID %s", msg.objectID)
  358. else:
  359. logger.debug("Invalidating pose for deleted located object %s" % obj)
  360. obj.pose.invalidate()
  361. def _recv_msg_robot_delocalized(self, evt, *, msg):
  362. # Invalidate the pose for every object
  363. logger.info("Robot delocalized - invalidating poses for all objects")
  364. for obj in self._objects.values():
  365. obj.pose.invalidate()
  366. def _recv_msg_memory_map_message_begin(self, evt, *, msg):
  367. if self._pending_nav_memory_map is not None:
  368. logger.error("NavMemoryMap unexpected begin - restarting map")
  369. self._pending_nav_memory_map = nav_memory_map.NavMemoryMapGrid(
  370. msg.originId, msg.rootDepth,
  371. msg.rootSize_mm, msg.rootCenterX,
  372. msg.rootCenterY)
  373. def _recv_msg_memory_map_message(self, evt, *, msg):
  374. if self._pending_nav_memory_map is not None:
  375. for quad in msg.quadInfos:
  376. self._pending_nav_memory_map._add_quad(quad.content, quad.depth)
  377. else:
  378. logger.error("NavMemoryMap message without begin - ignoring")
  379. def _recv_msg_memory_map_message_end(self, evt, *, msg):
  380. if self._pending_nav_memory_map is not None:
  381. # The pending map is now the latest complete map
  382. self._nav_memory_map = self._pending_nav_memory_map
  383. self._pending_nav_memory_map = None
  384. self.dispatch_event(nav_memory_map.EvtNewNavMemoryMap,
  385. nav_memory_map=self._nav_memory_map)
  386. else:
  387. logger.error("NavMemoryMap end without begin - ignoring")
  388. #### Public Event Handlers ####
  389. def recv_evt_object_tapped(self, event, *, obj, tap_count, tap_duration, **kw):
  390. pass
  391. def recv_evt_behavior_started(self, evt, *, behavior, **kw):
  392. self._active_behavior = behavior
  393. def recv_evt_behavior_stopped(self, evt, *, behavior, **kw):
  394. self._active_behavior = None
  395. def recv_evt_action_started(self, evt, *, action, **kw):
  396. self._active_action = action
  397. def recv_evt_action_completed(self, evt, *, action, **kw):
  398. self._active_action = None
  399. def recv_evt_new_raw_camera_image(self, evt, *, image, **kw):
  400. self._last_image_number += 1
  401. processed_image = CameraImage(image, self.image_annotator, self._last_image_number)
  402. self.latest_image = processed_image
  403. self.dispatch_event(EvtNewCameraImage, image=processed_image)
  404. def recv_evt_object_appeared(self, evt, *, obj, **kw):
  405. self._update_visible_obj_count(obj, 1)
  406. def recv_evt_object_vanished(self, evt, *, obj, **kw):
  407. self._update_visible_obj_count(obj, -1)
  408. def recv_evt_face_appeared(self, evt, *, face, **kw):
  409. self._visible_face_count += 1
  410. def recv_evt_face_disappeared(self, evt, *, face, **kw):
  411. self._visible_face_count -= 1
  412. def recv_evt_pet_appeared(self, evt, *, pet, **kw):
  413. self._visible_pet_count += 1
  414. def recv_evt_pet_disappeared(self, evt, *, pet, **kw):
  415. self._visible_pet_count -= 1
  416. #### Event Wrappers ####
  417. def _find_visible_object(self, object_type):
  418. for visible_object in self.visible_objects:
  419. if (object_type is None) or isinstance(visible_object, object_type):
  420. return visible_object
  421. return None
  422. async def wait_for_observed_light_cube(self, timeout=None, include_existing=True):
  423. '''Waits for one of the light cubes to be observed by the robot.
  424. Args:
  425. timeout (float): Number of seconds to wait for a cube to be
  426. observed, or None for indefinite
  427. include_existing (bool): Specifies whether to include light cubes
  428. that are already visible.
  429. Returns:
  430. The :class:`cozmo.objects.LightCube` object that was observed.
  431. '''
  432. if include_existing:
  433. obj = self._find_visible_object(objects.LightCube)
  434. if obj:
  435. return obj
  436. filter = event.Filter(objects.EvtObjectObserved,
  437. obj=lambda obj: isinstance(obj, objects.LightCube))
  438. evt = await self.wait_for(filter, timeout=timeout)
  439. return evt.obj
  440. async def wait_for_observed_face(self, timeout=None, include_existing=True):
  441. '''Waits for a face to be observed by the robot.
  442. Args:
  443. timeout (float): Number of seconds to wait for a face to be
  444. observed, or None for indefinite
  445. include_existing (bool): Specifies whether to include faces
  446. that are already visible.
  447. Returns:
  448. The :class:`cozmo.faces.Face` object that was observed.
  449. '''
  450. if include_existing:
  451. face = next(self.visible_faces, None)
  452. if face:
  453. return face
  454. filter = event.Filter(faces.EvtFaceObserved)
  455. evt = await self.wait_for(filter, timeout=timeout)
  456. return evt.face
  457. async def wait_for_observed_pet(self, timeout=None, include_existing=True):
  458. '''Waits for a pet to be observed by the robot.
  459. Args:
  460. timeout (float): Number of seconds to wait for a pet to be
  461. observed, or None for indefinite
  462. include_existing (bool): Specifies whether to include pets
  463. that are already visible.
  464. Returns:
  465. The :class:`cozmo.pets.Pet` object that was observed.
  466. '''
  467. if include_existing:
  468. pet = next(self.visible_pets, None)
  469. if pet:
  470. return pet
  471. filter = event.Filter(pets.EvtPetObserved)
  472. evt = await self.wait_for(filter, timeout=timeout)
  473. return evt.pet
  474. async def wait_for_observed_charger(self, timeout=None, include_existing=True):
  475. '''Waits for a charger to be observed by the robot.
  476. Args:
  477. timeout (float): Number of seconds to wait for a charger to be
  478. observed, or None for indefinite
  479. include_existing (bool): Specifies whether to include chargers
  480. that are already visible.
  481. Returns:
  482. The :class:`cozmo.objects.Charger` object that was observed.
  483. '''
  484. if include_existing:
  485. obj = self._find_visible_object(objects.Charger)
  486. if obj:
  487. return obj
  488. filter = event.Filter(objects.EvtObjectObserved,
  489. obj=lambda obj: isinstance(obj, objects.Charger))
  490. evt = await self.wait_for(filter, timeout=timeout)
  491. return evt.obj
  492. async def wait_until_observe_num_objects(self, num, object_type=None, timeout=None,
  493. include_existing=True):
  494. '''Waits for a certain number of unique objects to be seen at least once.
  495. This method waits for a number of unique objects to be seen, but not
  496. necessarily concurrently. That is, if cube 1 appears to the camera and
  497. then moves out of view to be replaced by cube 2, then that will count
  498. as 2 observed objects.
  499. To wait for multiple objects to be visible simultaneously, see
  500. :meth:`wait_until_num_objects_visible`.
  501. Args:
  502. num (float): The number of unique objects to wait for.
  503. object_type (class:`cozmo.objects.ObservableObject`): If provided
  504. this will cause only the selected object types to be counted.
  505. timeout (float): Maximum amount of time in seconds to wait for the
  506. requested number of objects to be observed.
  507. include_existing (bool): Specifies whether to include objects
  508. that are already visible.
  509. Returns:
  510. A list of length <= num of the unique objects
  511. class:`cozmo.objects.ObservableObject` observed during this wait.
  512. '''
  513. #Filter by object type if provided
  514. filter = objects.EvtObjectAppeared
  515. if object_type:
  516. if not issubclass(object_type, objects.ObservableObject):
  517. raise TypeError("Expected object_type to be ObservableObject")
  518. filter = event.Filter(objects.EvtObjectAppeared,
  519. obj=lambda obj: isinstance(obj, object_type))
  520. objs_seen = set()
  521. # If requested, add any objects that can already be seen (they won't create observed events)
  522. if include_existing:
  523. for visible_object in self.visible_objects:
  524. if (object_type is None) or isinstance(visible_object, object_type):
  525. objs_seen.add(visible_object)
  526. #Wait until we see a certain number of unique objects
  527. timeout = util.Timeout(timeout)
  528. while len(objs_seen) < num and not timeout.is_timed_out:
  529. try:
  530. evt = await self.wait_for(filter, timeout=timeout.remaining)
  531. objs_seen.add(evt.obj)
  532. except asyncio.TimeoutError:
  533. # on timeout, return the set of objects seen so far.
  534. return list(objs_seen)
  535. return list(objs_seen)
  536. async def wait_until_num_objects_visible(self, num, object_type=None, timeout=None):
  537. '''Waits for at least a specific number of objects to be seen concurrently.
  538. Unlike :meth:`wait_until_observe_num_objects` which returns when
  539. several objects have become visible, but not necessarily
  540. simultaneously, this method will only return if the specific
  541. number of objects are visible to the camera at the same time
  542. (as defined by :const:`objects.OBJECT_VISIBILITY_TIMEOUT`).
  543. Args:
  544. num (float): The number of unique objects to wait for.
  545. object_type (class:`cozmo.objects.ObservableObject`): If provided
  546. this will cause only the selected object types to be counted.
  547. timeout (float): Maximum amount of time in seconds to wait for the
  548. requested number of objects to be observed.
  549. Returns:
  550. int: The number of objects seen (num or higher).
  551. Raises:
  552. asyncio.TimeoutError if the required count wasn't seen.
  553. '''
  554. count = self.visible_object_count(object_type)
  555. timeout = util.Timeout(timeout)
  556. while count < num and not timeout.is_timed_out:
  557. await self.wait_for(objects.EvtObjectAppeared, timeout=timeout.remaining)
  558. count = self.visible_object_count(object_type)
  559. if count < num:
  560. raise asyncio.TimeoutError()
  561. return count
  562. #### Commands ####
  563. def send_available_objects(self):
  564. # XXX description for this?
  565. msg = _clad_to_engine_iface.SendAvailableObjects(enable=True)
  566. self.conn.send_msg(msg)
  567. def _remove_custom_marker_object_instances(self):
  568. for id, obj in list(self._objects.items()):
  569. if isinstance(obj, objects.CustomObject):
  570. logger.info("Removing CustomObject instance: id %s = obj '%s'", id, obj)
  571. del self._objects[id]
  572. def _remove_fixed_custom_object_instances(self):
  573. for id, obj in list(self._objects.items()):
  574. if isinstance(obj, objects.FixedCustomObject):
  575. logger.info("Removing FixedCustomObject instance: id %s = obj '%s'", id, obj)
  576. del self._objects[id]
  577. async def delete_all_custom_objects(self):
  578. """Causes the robot to forget about all custom (fixed + marker) objects it currently knows about.
  579. Note: This includes all fixed custom objects, and all custom marker object instances,
  580. BUT this does NOT remove the custom marker object definitions, so Cozmo
  581. will continue to add new objects if he sees the markers again. To remove
  582. the definitions for those objects use: :meth:`undefine_all_custom_marker_objects`
  583. """
  584. msg = _clad_to_engine_iface.DeleteAllCustomObjects()
  585. self.conn.send_msg(msg)
  586. # suppression for _MsgRobotDeletedAllCustomObjects "no-member" on pylint
  587. #pylint: disable=no-member
  588. await self.wait_for(_clad._MsgRobotDeletedAllCustomObjects)
  589. self._remove_custom_marker_object_instances()
  590. self._remove_fixed_custom_object_instances()
  591. async def delete_custom_marker_objects(self):
  592. """Causes the robot to forget about all custom marker objects it currently knows about.
  593. Note: This removes custom marker object instances only, it does NOT remove
  594. fixed custom objects, nor does it remove the custom marker object definitions, so Cozmo
  595. will continue to add new objects if he sees the markers again. To remove
  596. the definitions for those objects use: :meth:`undefine_all_custom_marker_objects`
  597. """
  598. msg = _clad_to_engine_iface.DeleteCustomMarkerObjects()
  599. self.conn.send_msg(msg)
  600. #pylint: disable=no-member
  601. await self.wait_for(_clad._MsgRobotDeletedCustomMarkerObjects)
  602. self._remove_custom_marker_object_instances()
  603. async def delete_fixed_custom_objects(self):
  604. """Causes the robot to forget about all fixed custom objects it currently knows about.
  605. Note: This removes fixed custom objects only, it does NOT remove
  606. the custom marker object instances or definitions.
  607. """
  608. msg = _clad_to_engine_iface.DeleteFixedCustomObjects()
  609. self.conn.send_msg(msg)
  610. #pylint: disable=no-member
  611. await self.wait_for(_clad._MsgRobotDeletedFixedCustomObjects)
  612. self._remove_fixed_custom_object_instances()
  613. async def undefine_all_custom_marker_objects(self):
  614. """Remove all custom marker object definitions, and any instances of them in the world."""
  615. msg = _clad_to_engine_iface.UndefineAllCustomMarkerObjects()
  616. self.conn.send_msg(msg)
  617. #pylint: disable=no-member
  618. await self.wait_for(_clad._MsgRobotDeletedCustomMarkerObjects)
  619. self._remove_custom_marker_object_instances()
  620. # Remove all custom object definitions / archetypes
  621. self.custom_objects.clear()
  622. async def _wait_for_defined_custom_object(self, custom_object_archetype):
  623. try:
  624. #pylint: disable=no-member
  625. msg = await self.wait_for(_clad._MsgDefinedCustomObject, timeout=5)
  626. except asyncio.TimeoutError as e:
  627. logger.error("Failed (Timed Out) to define: %s", custom_object_archetype)
  628. return None
  629. msg = msg.msg # get the internal message
  630. if msg.success:
  631. type_id = custom_object_archetype.object_type.id
  632. self.custom_objects[type_id] = custom_object_archetype
  633. logger.info("Defined: %s", custom_object_archetype)
  634. return custom_object_archetype
  635. else:
  636. logger.error("Failed to define Custom Object %s", custom_object_archetype)
  637. return None
  638. async def define_custom_box(self, custom_object_type,
  639. marker_front, marker_back,
  640. marker_top, marker_bottom,
  641. marker_left, marker_right,
  642. depth_mm, width_mm, height_mm,
  643. marker_width_mm, marker_height_mm,
  644. is_unique=True):
  645. '''Defines a cuboid of custom size and binds it to a specific custom object type.
  646. The engine will now detect the markers associated with this object and send an
  647. object_observed message when they are seen. The markers must be placed in the center
  648. of their respective sides. All 6 markers must be unique.
  649. Args:
  650. custom_object_type (:class:`cozmo.objects.CustomObjectTypes`): the
  651. object type you are binding this custom object to
  652. marker_front (:class:`cozmo.objects.CustomObjectMarkers`): the marker
  653. affixed to the front of the object
  654. marker_back (:class:`cozmo.objects.CustomObjectMarkers`): the marker
  655. affixed to the back of the object
  656. marker_top (:class:`cozmo.objects.CustomObjectMarkers`): the marker
  657. affixed to the top of the object
  658. marker_bottom (:class:`cozmo.objects.CustomObjectMarkers`): the marker
  659. affixed to the bottom of the object
  660. marker_left (:class:`cozmo.objects.CustomObjectMarkers`): the marker
  661. affixed to the left of the object
  662. marker_right (:class:`cozmo.objects.CustomObjectMarkers`): the marker
  663. affixed to the right of the object
  664. depth_mm (float): depth of the object (in millimeters) (X axis)
  665. width_mm (float): width of the object (in millimeters) (Y axis)
  666. height_mm (float): height of the object (in millimeters) (Z axis)
  667. (the height of the object)
  668. marker_width_mm (float): width of the printed marker (in millimeters).
  669. maker_height_mm (float): height of the printed marker (in millimeters).
  670. is_unique (bool): If True, the engine will assume there is only 1 of this object
  671. (and therefore only 1 of each of any of these markers) in the world.
  672. Returns:
  673. A :class:`cozmo.object.CustomObject` instance with the specified dimensions.
  674. This is None if the definition failed internally.
  675. Note: No instances of this object are added to the world until they have been seen.
  676. Raises:
  677. TypeError if the custom_object_type is of the wrong type.
  678. ValueError if the 6 markers aren't unique.
  679. '''
  680. if not isinstance(custom_object_type, objects._CustomObjectType):
  681. raise TypeError("Unsupported object_type, requires CustomObjectType")
  682. # verify all 6 markers are unique
  683. markers = {marker_front, marker_back, marker_top, marker_bottom, marker_left, marker_right}
  684. if len(markers) != 6:
  685. raise ValueError("all markers must be unique for a custom box")
  686. custom_object_archetype = self.custom_object_factory(self.conn, self, custom_object_type,
  687. depth_mm, width_mm, height_mm,
  688. marker_width_mm, marker_height_mm,
  689. is_unique, dispatch_parent=self)
  690. msg = _clad_to_engine_iface.DefineCustomBox(customType=custom_object_type.id,
  691. markerFront=marker_front.id,
  692. markerBack=marker_back.id,
  693. markerTop=marker_top.id,
  694. markerBottom=marker_bottom.id,
  695. markerLeft=marker_left.id,
  696. markerRight=marker_right.id,
  697. xSize_mm=depth_mm,
  698. ySize_mm=width_mm,
  699. zSize_mm=height_mm,
  700. markerWidth_mm=marker_width_mm,
  701. markerHeight_mm=marker_height_mm,
  702. isUnique=is_unique)
  703. self.conn.send_msg(msg)
  704. return await self._wait_for_defined_custom_object(custom_object_archetype)
  705. async def define_custom_cube(self, custom_object_type,
  706. marker,
  707. size_mm,
  708. marker_width_mm, marker_height_mm,
  709. is_unique=True):
  710. """Defines a cube of custom size and binds it to a specific custom object type.
  711. The engine will now detect the markers associated with this object and send an
  712. object_observed message when they are seen. The markers must be placed in the center
  713. of their respective sides.
  714. Args:
  715. custom_object_type (:class:`cozmo.objects.CustomObjectTypes`): the
  716. object type you are binding this custom object to.
  717. marker:(:class:`cozmo.objects.CustomObjectMarkers`): the marker
  718. affixed to every side of the cube.
  719. size_mm: size of each side of the cube (in millimeters).
  720. marker_width_mm (float): width of the printed marker (in millimeters).
  721. maker_height_mm (float): height of the printed marker (in millimeters).
  722. is_unique (bool): If True, the engine will assume there is only 1 of this object
  723. (and therefore only 1 of each of any of these markers) in the world.
  724. Returns:
  725. A :class:`cozmo.object.CustomObject` instance with the specified dimensions.
  726. This is None if the definition failed internally.
  727. Note: No instances of this object are added to the world until they have been seen.
  728. Raises:
  729. TypeError if the custom_object_type is of the wrong type.
  730. """
  731. if not isinstance(custom_object_type, objects._CustomObjectType):
  732. raise TypeError("Unsupported object_type, requires CustomObjectType")
  733. custom_object_archetype = self.custom_object_factory(self.conn, self, custom_object_type,
  734. size_mm, size_mm, size_mm,
  735. marker_width_mm, marker_height_mm,
  736. is_unique, dispatch_parent=self)
  737. msg = _clad_to_engine_iface.DefineCustomCube(customType=custom_object_type.id,
  738. marker=marker.id,
  739. size_mm=size_mm,
  740. markerWidth_mm=marker_width_mm,
  741. markerHeight_mm=marker_height_mm,
  742. isUnique=is_unique)
  743. self.conn.send_msg(msg)
  744. return await self._wait_for_defined_custom_object(custom_object_archetype)
  745. async def define_custom_wall(self, custom_object_type,
  746. marker,
  747. width_mm, height_mm,
  748. marker_width_mm, marker_height_mm,
  749. is_unique=True):
  750. """Defines a wall of custom width and height, with a fixed depth of 10mm, and binds it to a specific custom object type.
  751. The engine will now detect the markers associated with this object and send an
  752. object_observed message when they are seen. The markers must be placed in the center
  753. of their respective sides.
  754. Args:
  755. custom_object_type (:class:`cozmo.objects.CustomObjectTypes`): the
  756. object type you are binding this custom object to.
  757. marker:(:class:`cozmo.objects.CustomObjectMarkers`): the marker
  758. affixed to the front and back of the wall
  759. width_mm (float): width of the object (in millimeters). (Y axis).
  760. height_mm (float): height of the object (in millimeters). (Z axis).
  761. width_mm: width of the wall (along Y axis) (in millimeters).
  762. height_mm: height of the wall (along Z axis) (in millimeters).
  763. marker_width_mm (float): width of the printed marker (in millimeters).
  764. maker_height_mm (float): height of the printed marker (in millimeters).
  765. is_unique (bool): If True, the engine will assume there is only 1 of this object
  766. (and therefore only 1 of each of any of these markers) in the world.
  767. Returns:
  768. A :class:`cozmo.object.CustomObject` instance with the specified dimensions.
  769. This is None if the definition failed internally.
  770. Note: No instances of this object are added to the world until they have been seen.
  771. Raises:
  772. TypeError if the custom_object_type is of the wrong type.
  773. """
  774. if not isinstance(custom_object_type, objects._CustomObjectType):
  775. raise TypeError("Unsupported object_type, requires CustomObjectType")
  776. # TODO: share this hardcoded constant from engine
  777. WALL_THICKNESS_MM = 10.0
  778. custom_object_archetype = self.custom_object_factory(self.conn, self, custom_object_type,
  779. WALL_THICKNESS_MM, width_mm, height_mm,
  780. marker_width_mm, marker_height_mm,
  781. is_unique, dispatch_parent=self)
  782. msg = _clad_to_engine_iface.DefineCustomWall(customType=custom_object_type.id,
  783. marker=marker.id,
  784. width_mm=width_mm,
  785. height_mm=height_mm,
  786. markerWidth_mm=marker_width_mm,
  787. markerHeight_mm=marker_height_mm,
  788. isUnique=is_unique)
  789. self.conn.send_msg(msg)
  790. return await self._wait_for_defined_custom_object(custom_object_archetype)
  791. async def create_custom_fixed_object(self, pose, x_size_mm, y_size_mm, z_size_mm,
  792. relative_to_robot=False, use_robot_origin=True):
  793. '''Defines a cuboid of custom size and places it in the world. It cannot be observed.
  794. Args:
  795. pose (:class:`cozmo.util.Pose`): The pose of the object we are creating.
  796. x_size_mm (float): size of the object (in millimeters) in the x axis.
  797. y_size_mm (float): size of the object (in millimeters) in the y axis.
  798. z_size_mm (float): size of the object (in millimeters) in the z axis.
  799. relative_to_robot (bool): whether or not the pose given assumes the robot's pose as its origin.
  800. use_robot_origin (bool): whether or not to override the origin_id in the given pose to be
  801. the origin_id of Cozmo.
  802. Returns:
  803. A :class:`cozmo.objects.FixedCustomObject` instance with the specified dimensions and pose.
  804. '''
  805. # Override the origin of the pose to be the same as the robot's. This will make sure they are in
  806. # the same space in the engine every time.
  807. if use_robot_origin:
  808. pose.origin_id = self.robot.pose.origin_id
  809. # In this case define the given pose to be with respect to the robot's pose as its origin.
  810. if relative_to_robot:
  811. pose = self.robot.pose.define_pose_relative_this(pose)
  812. msg = _clad_to_engine_iface.CreateFixedCustomObject(pose=pose.encode_pose(),
  813. xSize_mm=x_size_mm, ySize_mm=y_size_mm, zSize_mm=z_size_mm)
  814. self.conn.send_msg(msg)
  815. #pylint: disable=no-member
  816. response = await self.wait_for(_clad._MsgCreatedFixedCustomObject)
  817. fixed_custom_object = objects.FixedCustomObject(pose, x_size_mm, y_size_mm, z_size_mm, response.msg.objectID)
  818. self._objects[fixed_custom_object.object_id] = fixed_custom_object
  819. return fixed_custom_object
  820. def enable_block_tap_filter(self, enable=True):
  821. '''Enable or disable the block tap filter in the engine.
  822. The block (AKA LightCube) tap filter removes low intensity taps, and
  823. filters out taps that come in rapidly together and instead just sends
  824. the strongest one
  825. Args:
  826. enable (bool): specifies whether the filter should be enabled or disabled
  827. '''
  828. msg = _clad_to_engine_iface.EnableBlockTapFilter(enable=enable)
  829. self.conn.send_msg(msg)
  830. def disconnect_from_cubes(self):
  831. """Disconnect from all cubes (to save battery life etc.).
  832. Call :meth:`connect_to_cubes` to re-connect to the cubes later.
  833. """
  834. logger.info("Disconnecting from cubes.")
  835. for cube in self.connected_light_cubes:
  836. logger.info("Disconnecting from %s" % cube)
  837. msg = _clad_to_engine_iface.BlockPoolResetMessage(enable=False,
  838. maintainPersistentPool=True)
  839. self.conn.send_msg(msg)
  840. async def connect_to_cubes(self):
  841. """Connect to all cubes.
  842. Request that Cozmo connects to all cubes - this is required if you
  843. previously called :meth:`disconnect_from_cubes` or
  844. :meth:`auto_disconnect_from_cubes_at_end` with enable=False. Connecting
  845. to a cube can take up to about 5 seconds, and this method will wait until
  846. either all 3 cubes are connected, or it has timed out waiting for this.
  847. Returns:
  848. bool: True if all 3 cubes are now connected.
  849. """
  850. connected_cubes = list(self.connected_light_cubes)
  851. num_connected_cubes = len(connected_cubes)
  852. num_unconnected_cubes = 3 - num_connected_cubes
  853. if num_unconnected_cubes < 1:
  854. logger.info("connect_to_cubes skipped - already connected to %s cubes", num_connected_cubes)
  855. return True
  856. logger.info("Connecting to cubes (already connected to %s, waiting for %s)", num_connected_cubes, num_unconnected_cubes)
  857. for cube in connected_cubes:
  858. logger.info("Already connected to %s" % cube)
  859. msg = _clad_to_engine_iface.BlockPoolResetMessage(enable=True,
  860. maintainPersistentPool=True)
  861. self.conn.send_msg(msg)
  862. success = True
  863. try:
  864. for _ in range(num_unconnected_cubes):
  865. #pylint: disable=no-member
  866. msg = await self.wait_for(_clad._MsgObjectConnectionState, timeout=10)
  867. except asyncio.TimeoutError as e:
  868. logger.warning("Failed to connect to all cubes in time!")
  869. success = False
  870. if success:
  871. logger.info("Connected to all cubes!")
  872. self.conn._request_connected_objects()
  873. try:
  874. #pylint: disable=no-member
  875. msg = await self.wait_for(_clad._MsgConnectedObjectStates, timeout=5)
  876. except asyncio.TimeoutError as e:
  877. logger.warning("Failed to receive connected cube states.")
  878. success = False
  879. return success
  880. def auto_disconnect_from_cubes_at_end(self, enable=True):
  881. """Tell the SDK to auto disconnect from cubes at the end of every SDK program.
  882. This can be used to save cube battery life if you spend a lot of time in
  883. SDK mode but aren't running programs as much (as you're busy writing
  884. them). Call :meth:`connect_to_cubes` to re-connect to the cubes later.
  885. Args:
  886. enable (bool): True if cubes should disconnect after every SDK program exits.
  887. """
  888. msg = _clad_to_engine_iface.SetShouldAutoDisconnectFromCubesAtEnd(doAutoDisconnect=enable)
  889. self.conn.send_msg(msg)
  890. def request_nav_memory_map(self, frequency_s):
  891. """Request navigation memory map data from Cozmo.
  892. The memory map can be accessed via :attr:`~cozmo.world.World.nav_memory_map`,
  893. it will be None until :meth:`request_nav_memory_map` has been called and
  894. a map has been received. The memory map provides a quad-tree map of
  895. where Cozmo thinks there are objects, and where Cozmo thinks it is safe
  896. to drive.
  897. Args:
  898. frequency_s (float): number of seconds between each update being sent.
  899. Negative values, e.g. -1.0, will disable any updates being sent.
  900. """
  901. msg = _clad_to_engine_iface.SetMemoryMapBroadcastFrequency_sec(frequency_s)
  902. self.conn.send_msg(msg)
  903. class CameraImage:
  904. '''A single image from Cozmo's camera.
  905. This wraps a raw image and provides an :meth:`annotate_image` method
  906. that can resize and add dynamic annotations to the image, such as
  907. marking up the location of objects, faces and pets.
  908. '''
  909. def __init__(self, raw_image, image_annotator, image_number=0):
  910. #: :class:`PIL.Image.Image`: the raw unprocessed image from the camera
  911. self.raw_image = raw_image
  912. #: :class:`cozmo.annotate.ImageAnnotator`: the image annotation object
  913. self.image_annotator = image_annotator
  914. #: int: An image number that increments on every new image received
  915. self.image_number = image_number
  916. #: float: The time the image was received and processed by the SDK
  917. self.image_recv_time = time.time()
  918. def annotate_image(self, scale=None, fit_size=None, resample_mode=annotate.RESAMPLE_MODE_NEAREST):
  919. '''Adds any enabled annotations to the image.
  920. Optionally resizes the image prior to annotations being applied. The
  921. aspect ratio of the resulting image always matches that of the raw image.
  922. Args:
  923. scale (float): If set then the base image will be scaled by the
  924. supplied multiplier. Cannot be combined with fit_size
  925. fit_size (tuple of int): If set, then scale the image to fit inside
  926. the supplied (width, height) dimensions. The original aspect
  927. ratio will be preserved. Cannot be combined with scale.
  928. resample_mode (int): The resampling mode to use when scaling the
  929. image. Should be either :attr:`~cozmo.annotate.RESAMPLE_MODE_NEAREST`
  930. (fast) or :attr:`~cozmo.annotate.RESAMPLE_MODE_BILINEAR` (slower,
  931. but smoother).
  932. Returns:
  933. :class:`PIL.Image.Image`
  934. '''
  935. return self.image_annotator.annotate_image(self.raw_image,
  936. scale=scale,
  937. fit_size=fit_size,
  938. resample_mode=resample_mode)