446 lines
17 KiB
Python
446 lines
17 KiB
Python
![]() |
# 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)
|
||
|
|
||
|
|