123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 |
- # Copyright (c) 2016-2017 Anki, Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License in the file LICENSE.txt or at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
-
- '''Face recognition and enrollment.
-
- Cozmo is capable of recognizing human faces, tracking their position and rotation
- ("pose") and assigning names to them via an enrollment process.
-
- The :class:`cozmo.world.World` object keeps track of faces the robot currently
- knows about, along with those that are currently visible to the camera.
-
- Each face is assigned a :class:`Face` object, which generates a number of
- observable events whenever the face is observed, has its ID updated, is
- renamed, etc.
-
- Note that these face-specific events are also passed up to the
- :class:`cozmo.world.World` object, so events for all known faces can be
- observed by adding handlers there.
- '''
-
- # __all__ should order by constants, event classes, other classes, functions.
- __all__ = ['FACE_VISIBILITY_TIMEOUT',
- 'FACIAL_EXPRESSION_UNKNOWN', 'FACIAL_EXPRESSION_NEUTRAL', 'FACIAL_EXPRESSION_HAPPY',
- 'FACIAL_EXPRESSION_SURPRISED', 'FACIAL_EXPRESSION_ANGRY', 'FACIAL_EXPRESSION_SAD',
- 'EvtErasedEnrolledFace', 'EvtFaceAppeared', 'EvtFaceDisappeared',
- 'EvtFaceIdChanged', 'EvtFaceObserved', 'EvtFaceRenamed',
- 'Face',
- 'erase_all_enrolled_faces', 'erase_enrolled_face_by_id',
- 'update_enrolled_face_by_id']
-
-
-
- from . import logger
-
- from . import behavior
- from . import event
- from . import objects
- from . import util
-
- from ._clad import _clad_to_engine_iface
- from ._clad import _clad_to_game_anki
-
-
- #: Length of time in seconds to go without receiving an observed event before
- #: assuming that Cozmo can no longer see a face.
- FACE_VISIBILITY_TIMEOUT = objects.OBJECT_VISIBILITY_TIMEOUT
-
- # Facial expressions that Cozmo can distinguish
- #: Facial expression not recognized.
- #: Call :func:`cozmo.robot.Robot.enable_facial_expression_estimation` to enable recognition.
- FACIAL_EXPRESSION_UNKNOWN = "unknown"
- #: Facial expression neutral
- FACIAL_EXPRESSION_NEUTRAL = "neutral"
- #: Facial expression happy
- FACIAL_EXPRESSION_HAPPY = "happy"
- #: Facial expression surprised
- FACIAL_EXPRESSION_SURPRISED = "surprised"
- #: Facial expression angry
- FACIAL_EXPRESSION_ANGRY = "angry"
- #: Facial expression sad
- FACIAL_EXPRESSION_SAD = "sad"
-
-
- class EvtErasedEnrolledFace(event.Event):
- '''Triggered when a face enrollment is removed (via erase_enrolled_face_by_id)'''
- face = 'The Face instance that the enrollment is being erased for'
- old_name = 'The name previously used for this face'
-
-
- class EvtFaceIdChanged(event.Event):
- '''Triggered whenever a face has its ID updated in engine.
-
- Generally occurs when:
- 1) A tracked but unrecognized face (negative ID) is recognized and receives a positive ID or
- 2) Face records get merged (on realization that 2 faces are actually the same)
- '''
- face = 'The Face instance that is being given a new id'
- old_id = 'The ID previously used for this face'
- new_id = 'The new ID that will be used for this face'
-
-
- class EvtFaceObserved(event.Event):
- '''Triggered whenever a face is visually identified by the robot.
-
- A stream of these events are produced while a face is visible to the robot.
- Each event has an updated image_box field.
-
- See EvtFaceAppeared if you only want to know when a face first
- becomes visible.
- '''
- face = 'The Face instance that was observed'
- updated = 'A set of field names that have changed'
- image_box = 'A comzo.util.ImageBox defining where the face is within Cozmo\'s camera view'
- name = 'The name associated with the face that was observed'
- pose = 'The cozmo.util.Pose defining the position and rotation of the face.'
-
-
- class EvtFaceAppeared(event.Event):
- '''Triggered whenever a face is first visually identified by a robot.
-
- This differs from EvtFaceObserved in that it's only triggered when
- a face initially becomes visible. If it disappears for more than
- FACE_VISIBILITY_TIMEOUT seconds and then is seen again, a
- EvtFaceDisappeared will be dispatched, followed by another
- EvtFaceAppeared event.
-
- For continuous tracking information about a visible face, see
- EvtFaceObserved.
- '''
- face = 'The Face instance that was observed'
- updated = 'A set of field names that have changed'
- image_box = 'A comzo.util.ImageBox defining where the face is within Cozmo\'s camera view'
- name = 'The name associated with the face that was observed'
- pose = 'The cozmo.util.Pose defining the position and rotation of the face.'
-
-
- class EvtFaceDisappeared(event.Event):
- '''Triggered whenever a face that was previously being observed is no longer visible.'''
- face = 'The Face instance that is no longer being observed'
-
-
- class EvtFaceRenamed(event.Event):
- '''Triggered whenever a face is renamed (via RobotRenamedEnrolledFace)'''
- face = 'The Face instance that is being given a new name'
- old_name = 'The name previously used for this face'
- new_name = 'The new name that will be used for this face'
-
-
- def erase_all_enrolled_faces(conn):
- '''Erase the enrollment (name) records for all faces.
-
- Args:
- conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over
- '''
- msg = _clad_to_engine_iface.EraseAllEnrolledFaces()
- conn.send_msg(msg)
-
-
- def erase_enrolled_face_by_id(conn, face_id):
- '''Erase the enrollment (name) record for the face with this ID.
-
- Args:
- conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over
- face_id (int): The ID of the face to erase.
- '''
- msg = _clad_to_engine_iface.EraseEnrolledFaceByID(face_id)
- conn.send_msg(msg)
-
-
- def update_enrolled_face_by_id(conn, face_id, old_name, new_name):
- '''Update the name enrolled for a given face.
-
- Args:
- conn (:class:`~cozmo.conn.CozmoConnection`): The connection to send the message over.
- face_id (int): The ID of the face to rename.
- old_name (string): The old name of the face (must be correct, otherwise message is ignored).
- new_name (string): The new name for the face.
- '''
- msg = _clad_to_engine_iface.UpdateEnrolledFaceByID(face_id, old_name, new_name)
- conn.send_msg(msg)
-
-
- def _clad_facial_expression_to_facial_expression(clad_expression_type):
- if clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Unknown:
- return FACIAL_EXPRESSION_UNKNOWN
- elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Neutral:
- return FACIAL_EXPRESSION_NEUTRAL
- elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Happiness:
- return FACIAL_EXPRESSION_HAPPY
- elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Surprise:
- return FACIAL_EXPRESSION_SURPRISED
- elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Anger:
- return FACIAL_EXPRESSION_ANGRY
- elif clad_expression_type == _clad_to_game_anki.Vision.FacialExpression.Sadness:
- return FACIAL_EXPRESSION_SAD
- else:
- raise ValueError("Unexpected facial expression type %s" % clad_expression_type)
-
-
- class Face(objects.ObservableElement):
- '''A single face that Cozmo has detected.
-
- May represent a face that has previously been enrolled, in which case
- :attr:`name` will hold the name that it was enrolled with.
-
- Each Face instance has a :attr:`face_id` integer - This may change if
- Cozmo later gets an improved view and makes a different prediction about
- which face it is looking at.
-
- See parent class :class:`~cozmo.objects.ObservableElement` for additional properties
- and methods.
- '''
-
- #: Length of time in seconds to go without receiving an observed event before
- #: assuming that Cozmo can no longer see a face.
- visibility_timeout = FACE_VISIBILITY_TIMEOUT
-
- def __init__(self, conn, world, robot, face_id=None, **kw):
- super().__init__(conn, world, robot, **kw)
- self._face_id = face_id
- self._updated_face_id = None
- self._name = ''
- self._expression = None
- self._expression_score = None
- self._left_eye = None
- self._right_eye = None
- self._nose = None
- self._mouth = None
-
- def _repr_values(self):
- return 'face_id=%s,%s name=%s' % (self.face_id, self.updated_face_id,
- self.name)
-
- #### Private Methods ####
-
- def _dispatch_observed_event(self, changed_fields, image_box):
- self.dispatch_event(EvtFaceObserved, face=self, name=self._name,
- updated=changed_fields, image_box=image_box, pose=self._pose)
-
- def _dispatch_appeared_event(self, changed_fields, image_box):
- self.dispatch_event(EvtFaceAppeared, face=self,
- updated=changed_fields, image_box=image_box, pose=self._pose)
-
- def _dispatch_disappeared_event(self):
- self.dispatch_event(EvtFaceDisappeared, face=self)
-
- #### Properties ####
-
- @property
- def face_id(self):
- '''int: The internal ID assigned to the face.
-
- This value can only be assigned once as it is static in the engine.
- '''
- return self._face_id
-
- @face_id.setter
- def face_id(self, value):
- if self._face_id is not None:
- raise ValueError("Cannot change face ID once set (from %s to %s)" % (self._face_id, value))
- logger.debug("Updated face_id for %s from %s to %s", self.__class__, self._face_id, value)
- self._face_id = value
-
- @property
- def has_updated_face_id(self):
- '''bool: True if this face been updated / superseded by a face with a new ID'''
- return self._updated_face_id is not None
-
- @property
- def updated_face_id(self):
- '''int: The ID for the face that superseded this one (if any, otherwise :meth:`face_id`)'''
- if self.has_updated_face_id:
- return self._updated_face_id
- else:
- return self.face_id
-
- @property
- def name(self):
- '''string: The name Cozmo has associated with the face in his memory.
-
- This string will be empty if the face is not recognized or enrolled.
- '''
- return self._name
-
- @property
- def expression(self):
- '''string: The facial expression Cozmo has recognized on the face.
-
- Will be :attr:`FACIAL_EXPRESSION_UNKNOWN` by default if you haven't called
- :meth:`cozmo.robot.Robot.enable_facial_expression_estimation` to enable
- the facial expression estimation. Otherwise it will be equal to one of:
- :attr:`FACIAL_EXPRESSION_NEUTRAL`, :attr:`FACIAL_EXPRESSION_HAPPY`,
- :attr:`FACIAL_EXPRESSION_SURPRISED`, :attr:`FACIAL_EXPRESSION_ANGRY`,
- or :attr:`FACIAL_EXPRESSION_SAD`.
- '''
- return self._expression
-
- @property
- def expression_score(self):
- '''int: The score/confidence that :attr:`expression` was correct.
-
- Will be 0 if expression is :attr:`FACIAL_EXPRESSION_UNKNOWN` (e.g. if
- :meth:`cozmo.robot.Robot.enable_facial_expression_estimation` wasn't
- called yet). The maximum possible score is 100.
- '''
- return self._expression_score
-
- @property
- def known_expression(self):
- '''string: The known facial expression Cozmo has recognized on the face.
-
- Like :meth:`expression` but returns an empty string for the unknown expression.
- '''
- expression = self.expression
- if expression == FACIAL_EXPRESSION_UNKNOWN:
- return ""
- return expression
-
- @property
- def left_eye(self):
- '''sequence of tuples of float (x,y): points representing the outline of the left eye'''
- return self._left_eye
-
- @property
- def right_eye(self):
- '''sequence of tuples of float (x,y): points representing the outline of the right eye'''
- return self._right_eye
-
- @property
- def nose(self):
- '''sequence of tuples of float (x,y): points representing the outline of the nose'''
- return self._nose
-
- @property
- def mouth(self):
- '''sequence of tuples of float (x,y): points representing the outline of the mouth'''
- return self._mouth
-
- #### Private Event Handlers ####
-
- def _recv_msg_robot_observed_face(self, evt, *, msg):
-
- changed_fields = {'pose', 'left_eye', 'right_eye', 'nose', 'mouth'}
- self._pose = util.Pose._create_from_clad(msg.pose)
- self._name = msg.name
-
- expression = _clad_facial_expression_to_facial_expression(msg.expression)
- expression_score = 0
-
- if expression != FACIAL_EXPRESSION_UNKNOWN:
- expression_score = msg.expressionValues[msg.expression]
- if expression_score == 0:
- # The expression should have been marked unknown - this is a
- # bug in the engine because even a zero score overwrites the
- # default negative score for Unknown.
- expression = FACIAL_EXPRESSION_UNKNOWN
-
- if expression != self._expression:
- self._expression = expression
- changed_fields.add('expression')
-
- if expression_score != self._expression_score:
- self._expression_score = expression_score
- changed_fields.add('expression_score')
-
- self._left_eye = msg.leftEye
- self._right_eye = msg.rightEye
- self._nose = msg.nose
- self._mouth = msg.mouth
-
- image_box = util.ImageBox._create_from_clad_rect(msg.img_rect)
- self._on_observed(image_box, msg.timestamp, changed_fields)
-
- def _recv_msg_robot_changed_observed_face_id(self, evt, *, msg):
- self._updated_face_id = msg.newID
- self.dispatch_event(EvtFaceIdChanged, face=self, old_id=msg.oldID, new_id = msg.newID)
-
- def _recv_msg_robot_renamed_enrolled_face(self, evt, *, msg):
- old_name = self._name
- self._name = msg.name
- self.dispatch_event(EvtFaceRenamed, face=self, old_name=old_name, new_name=msg.name)
-
- def _recv_msg_robot_erased_enrolled_face(self, evt, *, msg):
- old_name = self._name
- self._name = ''
- self.dispatch_event(EvtErasedEnrolledFace, face=self, old_name=old_name)
-
- #### Public Event Handlers ####
-
- #### Event Wrappers ####
-
- #### Commands ####
-
- def _is_valid_name(self, name):
- if not (name and name.isalpha()):
- return False
- try:
- name.encode('ascii')
- except UnicodeEncodeError:
- return False
-
- return True
-
- def name_face(self, name):
- '''Assign a name to this face. Cozmo will remember this name between SDK runs.
-
- Args:
- name (string): The name that will be assigned to this face. Must
- be a non-empty ASCII string of alphabetic characters only.
- Returns:
- An instance of :class:`cozmo.behavior.Behavior` object
- Raises:
- :class:`ValueError` if name is invalid.
- '''
- if not self._is_valid_name(name):
- raise ValueError("new_name '%s' is an invalid face name. "
- "Must be non-empty and contain only alphabetic ASCII characters." % name)
-
- logger.info("Enrolling face=%s with name='%s'", self, name)
-
- # Note: saveID must be 0 if face_id doesn't already have a name
- msg = _clad_to_engine_iface.SetFaceToEnroll(name=name,
- observedID=self.face_id,
- saveID=0,
- saveToRobot=True,
- sayName=False,
- useMusic=False)
- self.conn.send_msg(msg)
-
- enroll_behavior = self._robot.start_behavior(behavior.BehaviorTypes._EnrollFace)
- return enroll_behavior
-
- def rename_face(self, new_name):
- '''Change the name assigned to the face. Cozmo will remember this name between SDK runs.
-
- Args:
- new_name (string): The new name that will be assigned to this face. Must
- be a non-empty ASCII string of alphabetic characters only.
- Raises:
- :class:`ValueError` if new_name is invalid.
- '''
- if not self._is_valid_name(new_name):
- raise ValueError("new_name '%s' is an invalid face name. "
- "Must be non-empty and contain only alphabetic ASCII characters." % new_name)
- update_enrolled_face_by_id(self.conn, self.face_id, self.name, new_name)
-
- def erase_enrolled_face(self):
- '''Remove the name associated with this face.
-
- Cozmo will no longer remember the name associated with this face between SDK runs.
- '''
- erase_enrolled_face_by_id(self.conn, self.face_id)
-
|