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.

faces.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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. '''Face recognition and enrollment.
  15. Cozmo is capable of recognizing human faces, tracking their position and rotation
  16. ("pose") and assigning names to them via an enrollment process.
  17. The :class:`cozmo.world.World` object keeps track of faces the robot currently
  18. knows about, along with those that are currently visible to the camera.
  19. Each face is assigned a :class:`Face` object, which generates a number of
  20. observable events whenever the face is observed, has its ID updated, is
  21. renamed, etc.
  22. Note that these face-specific events are also passed up to the
  23. :class:`cozmo.world.World` object, so events for all known faces can be
  24. observed by adding handlers there.
  25. '''
  26. # __all__ should order by constants, event classes, other classes, functions.
  27. __all__ = ['FACE_VISIBILITY_TIMEOUT',
  28. 'FACIAL_EXPRESSION_UNKNOWN', 'FACIAL_EXPRESSION_NEUTRAL', 'FACIAL_EXPRESSION_HAPPY',
  29. 'FACIAL_EXPRESSION_SURPRISED', 'FACIAL_EXPRESSION_ANGRY', 'FACIAL_EXPRESSION_SAD',
  30. 'EvtErasedEnrolledFace', 'EvtFaceAppeared', 'EvtFaceDisappeared',
  31. 'EvtFaceIdChanged', 'EvtFaceObserved', 'EvtFaceRenamed',
  32. 'Face',
  33. 'erase_all_enrolled_faces', 'erase_enrolled_face_by_id',
  34. 'update_enrolled_face_by_id']
  35. from . import logger
  36. from . import behavior
  37. from . import event
  38. from . import objects
  39. from . import util
  40. from ._clad import _clad_to_engine_iface
  41. from ._clad import _clad_to_game_anki
  42. #: Length of time in seconds to go without receiving an observed event before
  43. #: assuming that Cozmo can no longer see a face.
  44. FACE_VISIBILITY_TIMEOUT = objects.OBJECT_VISIBILITY_TIMEOUT
  45. # Facial expressions that Cozmo can distinguish
  46. #: Facial expression not recognized.
  47. #: Call :func:`cozmo.robot.Robot.enable_facial_expression_estimation` to enable recognition.
  48. FACIAL_EXPRESSION_UNKNOWN = "unknown"
  49. #: Facial expression neutral
  50. FACIAL_EXPRESSION_NEUTRAL = "neutral"
  51. #: Facial expression happy
  52. FACIAL_EXPRESSION_HAPPY = "happy"
  53. #: Facial expression surprised
  54. FACIAL_EXPRESSION_SURPRISED = "surprised"
  55. #: Facial expression angry
  56. FACIAL_EXPRESSION_ANGRY = "angry"
  57. #: Facial expression sad
  58. FACIAL_EXPRESSION_SAD = "sad"
  59. class EvtErasedEnrolledFace(event.Event):
  60. '''Triggered when a face enrollment is removed (via erase_enrolled_face_by_id)'''
  61. face = 'The Face instance that the enrollment is being erased for'
  62. old_name = 'The name previously used for this face'
  63. class EvtFaceIdChanged(event.Event):
  64. '''Triggered whenever a face has its ID updated in engine.
  65. Generally occurs when:
  66. 1) A tracked but unrecognized face (negative ID) is recognized and receives a positive ID or
  67. 2) Face records get merged (on realization that 2 faces are actually the same)
  68. '''
  69. face = 'The Face instance that is being given a new id'
  70. old_id = 'The ID previously used for this face'
  71. new_id = 'The new ID that will be used for this face'
  72. class EvtFaceObserved(event.Event):
  73. '''Triggered whenever a face is visually identified by the robot.
  74. A stream of these events are produced while a face is visible to the robot.
  75. Each event has an updated image_box field.
  76. See EvtFaceAppeared if you only want to know when a face first
  77. becomes visible.
  78. '''
  79. face = 'The Face instance that was observed'
  80. updated = 'A set of field names that have changed'
  81. image_box = 'A comzo.util.ImageBox defining where the face is within Cozmo\'s camera view'
  82. name = 'The name associated with the face that was observed'
  83. pose = 'The cozmo.util.Pose defining the position and rotation of the face.'
  84. class EvtFaceAppeared(event.Event):
  85. '''Triggered whenever a face is first visually identified by a robot.
  86. This differs from EvtFaceObserved in that it's only triggered when
  87. a face initially becomes visible. If it disappears for more than
  88. FACE_VISIBILITY_TIMEOUT seconds and then is seen again, a
  89. EvtFaceDisappeared will be dispatched, followed by another
  90. EvtFaceAppeared event.
  91. For continuous tracking information about a visible face, see
  92. EvtFaceObserved.
  93. '''
  94. face = 'The Face instance that was observed'
  95. updated = 'A set of field names that have changed'
  96. image_box = 'A comzo.util.ImageBox defining where the face is within Cozmo\'s camera view'
  97. name = 'The name associated with the face that was observed'
  98. pose = 'The cozmo.util.Pose defining the position and rotation of the face.'
  99. class EvtFaceDisappeared(event.Event):
  100. '''Triggered whenever a face that was previously being observed is no longer visible.'''
  101. face = 'The Face instance that is no longer being observed'
  102. class EvtFaceRenamed(event.Event):
  103. '''Triggered whenever a face is renamed (via RobotRenamedEnrolledFace)'''
  104. face = 'The Face instance that is being given a new name'
  105. old_name = 'The name previously used for this face'
  106. new_name = 'The new name that will be used for this face'
  107. def erase_all_enrolled_faces(conn):
  108. '''Erase the enrollment (name) records for all faces.
  109. Args:
  110. conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over
  111. '''
  112. msg = _clad_to_engine_iface.EraseAllEnrolledFaces()
  113. conn.send_msg(msg)
  114. def erase_enrolled_face_by_id(conn, face_id):
  115. '''Erase the enrollment (name) record for the face with this ID.
  116. Args:
  117. conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over
  118. face_id (int): The ID of the face to erase.
  119. '''
  120. msg = _clad_to_engine_iface.EraseEnrolledFaceByID(face_id)
  121. conn.send_msg(msg)
  122. def update_enrolled_face_by_id(conn, face_id, old_name, new_name):
  123. '''Update the name enrolled for a given face.
  124. Args:
  125. conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over.
  126. face_id (int): The ID of the face to rename.
  127. old_name (string): The old name of the face (must be correct, otherwise message is ignored).
  128. new_name (string): The new name for the face.
  129. '''
  130. msg = _clad_to_engine_iface.UpdateEnrolledFaceByID(face_id, old_name, new_name)
  131. conn.send_msg(msg)
  132. def _clad_facial_expression_to_facial_expression(clad_expression_type):
  133. if clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Unknown:
  134. return FACIAL_EXPRESSION_UNKNOWN
  135. elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Neutral:
  136. return FACIAL_EXPRESSION_NEUTRAL
  137. elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Happiness:
  138. return FACIAL_EXPRESSION_HAPPY
  139. elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Surprise:
  140. return FACIAL_EXPRESSION_SURPRISED
  141. elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Anger:
  142. return FACIAL_EXPRESSION_ANGRY
  143. elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Sadness:
  144. return FACIAL_EXPRESSION_SAD
  145. else:
  146. raise ValueError("Unexpected facial expression type %s" % clad_expression_type)
  147. class Face(objects.ObservableElement):
  148. '''A single face that Cozmo has detected.
  149. May represent a face that has previously been enrolled, in which case
  150. :attr:`name` will hold the name that it was enrolled with.
  151. Each Face instance has a :attr:`face_id` integer - This may change if
  152. Cozmo later gets an improved view and makes a different prediction about
  153. which face it is looking at.
  154. See parent class :class:`~cozmo.objects.ObservableElement` for additional properties
  155. and methods.
  156. '''
  157. #: Length of time in seconds to go without receiving an observed event before
  158. #: assuming that Cozmo can no longer see a face.
  159. visibility_timeout = FACE_VISIBILITY_TIMEOUT
  160. def __init__(self, conn, world, robot, face_id=None, **kw):
  161. super().__init__(conn, world, robot, **kw)
  162. self._face_id = face_id
  163. self._updated_face_id = None
  164. self._name = ''
  165. self._expression = None
  166. self._expression_score = None
  167. self._left_eye = None
  168. self._right_eye = None
  169. self._nose = None
  170. self._mouth = None
  171. def _repr_values(self):
  172. return 'face_id=%s,%s name=%s' % (self.face_id, self.updated_face_id,
  173. self.name)
  174. #### Private Methods ####
  175. def _dispatch_observed_event(self, changed_fields, image_box):
  176. self.dispatch_event(EvtFaceObserved, face=self, name=self._name,
  177. updated=changed_fields, image_box=image_box, pose=self._pose)
  178. def _dispatch_appeared_event(self, changed_fields, image_box):
  179. self.dispatch_event(EvtFaceAppeared, face=self,
  180. updated=changed_fields, image_box=image_box, pose=self._pose)
  181. def _dispatch_disappeared_event(self):
  182. self.dispatch_event(EvtFaceDisappeared, face=self)
  183. #### Properties ####
  184. @property
  185. def face_id(self):
  186. '''int: The internal ID assigned to the face.
  187. This value can only be assigned once as it is static in the engine.
  188. '''
  189. return self._face_id
  190. @face_id.setter
  191. def face_id(self, value):
  192. if self._face_id is not None:
  193. raise ValueError("Cannot change face ID once set (from %s to %s)" % (self._face_id, value))
  194. logger.debug("Updated face_id for %s from %s to %s", self.__class__, self._face_id, value)
  195. self._face_id = value
  196. @property
  197. def has_updated_face_id(self):
  198. '''bool: True if this face been updated / superseded by a face with a new ID'''
  199. return self._updated_face_id is not None
  200. @property
  201. def updated_face_id(self):
  202. '''int: The ID for the face that superseded this one (if any, otherwise :meth:`face_id`)'''
  203. if self.has_updated_face_id:
  204. return self._updated_face_id
  205. else:
  206. return self.face_id
  207. @property
  208. def name(self):
  209. '''string: The name Cozmo has associated with the face in his memory.
  210. This string will be empty if the face is not recognized or enrolled.
  211. '''
  212. return self._name
  213. @property
  214. def expression(self):
  215. '''string: The facial expression Cozmo has recognized on the face.
  216. Will be :attr:`FACIAL_EXPRESSION_UNKNOWN` by default if you haven't called
  217. :meth:`cozmo.robot.Robot.enable_facial_expression_estimation` to enable
  218. the facial expression estimation. Otherwise it will be equal to one of:
  219. :attr:`FACIAL_EXPRESSION_NEUTRAL`, :attr:`FACIAL_EXPRESSION_HAPPY`,
  220. :attr:`FACIAL_EXPRESSION_SURPRISED`, :attr:`FACIAL_EXPRESSION_ANGRY`,
  221. or :attr:`FACIAL_EXPRESSION_SAD`.
  222. '''
  223. return self._expression
  224. @property
  225. def expression_score(self):
  226. '''int: The score/confidence that :attr:`expression` was correct.
  227. Will be 0 if expression is :attr:`FACIAL_EXPRESSION_UNKNOWN` (e.g. if
  228. :meth:`cozmo.robot.Robot.enable_facial_expression_estimation` wasn't
  229. called yet). The maximum possible score is 100.
  230. '''
  231. return self._expression_score
  232. @property
  233. def known_expression(self):
  234. '''string: The known facial expression Cozmo has recognized on the face.
  235. Like :meth:`expression` but returns an empty string for the unknown expression.
  236. '''
  237. expression = self.expression
  238. if expression == FACIAL_EXPRESSION_UNKNOWN:
  239. return ""
  240. return expression
  241. @property
  242. def left_eye(self):
  243. '''sequence of tuples of float (x,y): points representing the outline of the left eye'''
  244. return self._left_eye
  245. @property
  246. def right_eye(self):
  247. '''sequence of tuples of float (x,y): points representing the outline of the right eye'''
  248. return self._right_eye
  249. @property
  250. def nose(self):
  251. '''sequence of tuples of float (x,y): points representing the outline of the nose'''
  252. return self._nose
  253. @property
  254. def mouth(self):
  255. '''sequence of tuples of float (x,y): points representing the outline of the mouth'''
  256. return self._mouth
  257. #### Private Event Handlers ####
  258. def _recv_msg_robot_observed_face(self, evt, *, msg):
  259. changed_fields = {'pose', 'left_eye', 'right_eye', 'nose', 'mouth'}
  260. self._pose = util.Pose._create_from_clad(msg.pose)
  261. self._name = msg.name
  262. expression = _clad_facial_expression_to_facial_expression(msg.expression)
  263. expression_score = 0
  264. if expression != FACIAL_EXPRESSION_UNKNOWN:
  265. expression_score = msg.expressionValues[msg.expression]
  266. if expression_score == 0:
  267. # The expression should have been marked unknown - this is a
  268. # bug in the engine because even a zero score overwrites the
  269. # default negative score for Unknown.
  270. expression = FACIAL_EXPRESSION_UNKNOWN
  271. if expression != self._expression:
  272. self._expression = expression
  273. changed_fields.add('expression')
  274. if expression_score != self._expression_score:
  275. self._expression_score = expression_score
  276. changed_fields.add('expression_score')
  277. self._left_eye = msg.leftEye
  278. self._right_eye = msg.rightEye
  279. self._nose = msg.nose
  280. self._mouth = msg.mouth
  281. image_box = util.ImageBox._create_from_clad_rect(msg.img_rect)
  282. self._on_observed(image_box, msg.timestamp, changed_fields)
  283. def _recv_msg_robot_changed_observed_face_id(self, evt, *, msg):
  284. self._updated_face_id = msg.newID
  285. self.dispatch_event(EvtFaceIdChanged, face=self, old_id=msg.oldID, new_id = msg.newID)
  286. def _recv_msg_robot_renamed_enrolled_face(self, evt, *, msg):
  287. old_name = self._name
  288. self._name = msg.name
  289. self.dispatch_event(EvtFaceRenamed, face=self, old_name=old_name, new_name=msg.name)
  290. def _recv_msg_robot_erased_enrolled_face(self, evt, *, msg):
  291. old_name = self._name
  292. self._name = ''
  293. self.dispatch_event(EvtErasedEnrolledFace, face=self, old_name=old_name)
  294. #### Public Event Handlers ####
  295. #### Event Wrappers ####
  296. #### Commands ####
  297. def _is_valid_name(self, name):
  298. if not (name and name.isalpha()):
  299. return False
  300. try:
  301. name.encode('ascii')
  302. except UnicodeEncodeError:
  303. return False
  304. return True
  305. def name_face(self, name):
  306. '''Assign a name to this face. Cozmo will remember this name between SDK runs.
  307. Args:
  308. name (string): The name that will be assigned to this face. Must
  309. be a non-empty ASCII string of alphabetic characters only.
  310. Returns:
  311. An instance of :class:`cozmo.behavior.Behavior` object
  312. Raises:
  313. :class:`ValueError` if name is invalid.
  314. '''
  315. if not self._is_valid_name(name):
  316. raise ValueError("new_name '%s' is an invalid face name. "
  317. "Must be non-empty and contain only alphabetic ASCII characters." % name)
  318. logger.info("Enrolling face=%s with name='%s'", self, name)
  319. # Note: saveID must be 0 if face_id doesn't already have a name
  320. msg = _clad_to_engine_iface.SetFaceToEnroll(name=name,
  321. observedID=self.face_id,
  322. saveID=0,
  323. saveToRobot=True,
  324. sayName=False,
  325. useMusic=False)
  326. self.conn.send_msg(msg)
  327. enroll_behavior = self._robot.start_behavior(behavior.BehaviorTypes._EnrollFace)
  328. return enroll_behavior
  329. def rename_face(self, new_name):
  330. '''Change the name assigned to the face. Cozmo will remember this name between SDK runs.
  331. Args:
  332. new_name (string): The new name that will be assigned to this face. Must
  333. be a non-empty ASCII string of alphabetic characters only.
  334. Raises:
  335. :class:`ValueError` if new_name is invalid.
  336. '''
  337. if not self._is_valid_name(new_name):
  338. raise ValueError("new_name '%s' is an invalid face name. "
  339. "Must be non-empty and contain only alphabetic ASCII characters." % new_name)
  340. update_enrolled_face_by_id(self.conn, self.face_id, self.name, new_name)
  341. def erase_enrolled_face(self):
  342. '''Remove the name associated with this face.
  343. Cozmo will no longer remember the name associated with this face between SDK runs.
  344. '''
  345. erase_enrolled_face_by_id(self.conn, self.face_id)