From bc61fcd778c8c73557881337d946fbc6f02b12ae Mon Sep 17 00:00:00 2001 From: Rainer Kartmann <rainer.kartmann@kit.edu> Date: Fri, 25 Mar 2022 09:40:31 +0100 Subject: [PATCH] Integrate armarx_face_recognition to FaceRecognition segment --- python/CMakeLists.txt | 3 +- .../armarx_face_recognition/actions.py | 8 +- .../app/standalone_face_recognition.py | 67 ------- .../armarx_face_recognition/datatypes.py | 48 +++++ .../armarx_face_recognition/db_model.py | 34 ---- .../armarx_face_recognition/face_detection.py | 185 ++++++++++-------- .../armarx_face_recognition/helper.py | 1 - 7 files changed, 155 insertions(+), 191 deletions(-) delete mode 100755 python/armarx_face_recognition/armarx_face_recognition/app/standalone_face_recognition.py create mode 100644 python/armarx_face_recognition/armarx_face_recognition/datatypes.py delete mode 100644 python/armarx_face_recognition/armarx_face_recognition/db_model.py diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 10be299a6..e7c16117d 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -1,2 +1 @@ - -add_subdirectory(face_recognition) \ No newline at end of file +add_subdirectory(armarx_face_recognition) diff --git a/python/armarx_face_recognition/armarx_face_recognition/actions.py b/python/armarx_face_recognition/armarx_face_recognition/actions.py index 04ebfb01f..7fd8e3d82 100644 --- a/python/armarx_face_recognition/armarx_face_recognition/actions.py +++ b/python/armarx_face_recognition/armarx_face_recognition/actions.py @@ -12,6 +12,9 @@ robot = A6() logger = logging.getLogger(__name__) +# ToDo: Update to new data types / move data types to armarx-dev. + + def report_to_memory(p: Person): import numpy as np from armarx_memory.client import MemoryNameSystem @@ -50,7 +53,7 @@ def on_detected_faces(detected_faces, time): report_to_memory(f.person) check_birthday(f.person) greet_persons(f.person) - #look_at_person(f.person) + # look_at_person(f.person) f.person.update(last_seen=now) @@ -60,7 +63,7 @@ def look_at_person(p: Person): def check_birthday(p: Person): - if not p.birthday: + if not p.birthday: return now = datetime.datetime.now() birthday_in_current_year = p.birthday(year=now.date().year) @@ -107,4 +110,3 @@ def label_face() -> str: name = sys.stdin.readline().strip() return name return None - diff --git a/python/armarx_face_recognition/armarx_face_recognition/app/standalone_face_recognition.py b/python/armarx_face_recognition/armarx_face_recognition/app/standalone_face_recognition.py deleted file mode 100755 index a84be44a5..000000000 --- a/python/armarx_face_recognition/armarx_face_recognition/app/standalone_face_recognition.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 - -import os -import glob -import numpy as np - -import logging - -from armarx.parser import ArmarXArgumentParser as ArgumentParser - -from armarx_face_recognition.face_detection import FaceDetection -from armarx_face_recognition.db_model import Person -from armarx_face_recognition.db_model import Face -from armarx_face_recognition.db_model import db - -import armarx_face_recognition - - -logger = logging.getLogger(__name__) - - -def main(): - parser = ArgumentParser('Example Image Provider') - parser.add_argument('-i', '--input-provider', default='OpenNIPointCloudProvider') - parser.add_argument('--create-tables', action='store_true', help='Initializes the database.') - parser.add_argument('--drop-tables', action='store_true', help='Drop tables if they already exist.') - parser.add_argument('--images', default='/tmp/*.jpg', help='Known faces.') - args = parser.parse_args() - - np.set_printoptions(suppress=True,precision=1) - - logger.debug('Setting up database') - db.connect() - - if args.create_tables: - - if args.drop_tables: - db.drop_tables([Person, Face]) - db.create_tables([Person, Face]) - - Person.create(given_name='Unknown') - - image_files = [f for f in glob.glob(args.images)] - - if not image_files: - logger.error('No images found. Aborting') - logger.debug('Found %d images ', len(image_files)) - - for f in image_files: - name = os.path.splitext(os.path.basename(f))[0] - logger.info('Loading person %s', name) - person = Person.create(given_name=name) - logger.debug('Creating face encoding for person %s', name) - image = armarx_face_recognition.load_image_file(f) - encoding = armarx_face_recognition.face_encodings(image)[0] - Face.create(person=person, face_encoding=memoryview(encoding)) - - - logger.debug('Starting example image processor') - - image_processor = FaceDetection(args.input_provider) - image_processor.register() - image_processor.update_calibration() - - -if __name__ == '__main__': - main() diff --git a/python/armarx_face_recognition/armarx_face_recognition/datatypes.py b/python/armarx_face_recognition/armarx_face_recognition/datatypes.py new file mode 100644 index 000000000..d315536b8 --- /dev/null +++ b/python/armarx_face_recognition/armarx_face_recognition/datatypes.py @@ -0,0 +1,48 @@ +import dataclasses as dc +import numpy as np +import typing as ty + +from armarx_memory import client as mem + + +@dc.dataclass +class Person: + + face_encodings: ty.List[np.ndarray] = dc.field(default_factory=list) + + profile_id: ty.Optional[mem.MemoryID] = None + profile: ty.Dict = dc.field(default_factory=dict) + + def name(self) -> str: + try: + return self.profile["spokenNames"][0] + except KeyError: + if self.profile_id is not None: + return self.profile_id.entity_name + else: + return "<unknown>" + + +@dc.dataclass +class FaceRecognition: + + position_3d: np.ndarray + position_2d: np.ndarray + extents_2d: np.ndarray + + profile_id: mem.MemoryID + + last_seen_usec: int = -1 + last_greeted_usec: int = -1 + + def to_aron_data(self): + return { + "position3D": self.position_3d.astype(np.float32).reshape(3, 1), + "position2D": self.position_2d.astype(np.int32), + "extents2D": self.extents_2d.astype(np.int32), + "profileID": self.profile_id.to_aron() if self.profile_id else mem.MemoryID().to_aron(), + } + + def to_aron(self): + from armarx_memory.aron.conversion import to_aron + return to_aron(self.to_aron_data()) diff --git a/python/armarx_face_recognition/armarx_face_recognition/db_model.py b/python/armarx_face_recognition/armarx_face_recognition/db_model.py deleted file mode 100644 index 1e19f2066..000000000 --- a/python/armarx_face_recognition/armarx_face_recognition/db_model.py +++ /dev/null @@ -1,34 +0,0 @@ -import datetime -from peewee import * - -db = SqliteDatabase('/tmp/people.db') - - -class BaseModel(Model): - class Meta: - database = db - - -class Person(BaseModel): - given_name = CharField() - family_name = CharField(null=True) - birthday = DateField(null=True) - roles = CharField(default='user') - - last_seen = DateTimeField(default=datetime.datetime(1,1,1)) - last_greeted = DateTimeField(default=datetime.datetime(1,1,1)) - birthday_year_congratulated = IntegerField(default=0) - created_date = DateTimeField(default=datetime.datetime.now) - - x = FloatField(default=0) - y = FloatField(default=0) - z = FloatField(default=0) - - def __str__(self): - return f'{self.given_name} {self.family_name}' - - -class Face(BaseModel): - - person = ForeignKeyField(Person, backref='faces') - face_encoding = BlobField(null=True) diff --git a/python/armarx_face_recognition/armarx_face_recognition/face_detection.py b/python/armarx_face_recognition/armarx_face_recognition/face_detection.py index cb7559c28..0d746f8be 100755 --- a/python/armarx_face_recognition/armarx_face_recognition/face_detection.py +++ b/python/armarx_face_recognition/armarx_face_recognition/face_detection.py @@ -3,57 +3,23 @@ import os import logging import math -import dataclasses as dc import typing as ty -import numpy as np import cv2 +import numpy as np import face_recognition -from armarx import slice_loader +from armarx import cmake_helper from armarx import arviz as viz from armarx_memory import client as mem -slice_loader.load_armarx_slice("RobotAPI", "core/FramedPoseBase.ice") - -from armarx import FramedPositionBase from armarx.pose_helper import convert_position_to_global +from armarx import FramedPositionBase -from visionx import StereoCalibrationInterfacePrx from visionx.image_processor import ImageProcessor +from visionx import StereoCalibrationInterfacePrx -# from .actions import on_detected_faces - - -@dc.dataclass -class Person: - - face_encoding: ty.Optional[np.ndarray] - - profile_id: ty.Optional[mem.MemoryID] - profile: ty.Dict = dc.field(default_factory=dict) - - def name(self) -> str: - try: - return self.profile["spokenNames"][0] - except KeyError: - if self.profile_id is not None: - return self.profile_id.entity_name - else: - return "<unknown>" - - -@dc.dataclass -class FaceRecognition: - - position_3d: np.ndarray - position_2d: np.ndarray - extents_2d: np.ndarray - - profile_id: mem.MemoryID - - last_seen_usec: int = -1 - last_greeted_usec: int = -1 +from armarx_face_recognition.datatypes import Person, FaceRecognition class FaceDetection(ImageProcessor): @@ -63,15 +29,14 @@ class FaceDetection(ImageProcessor): provider_name: str, persons: ty.List[Person], num_result_images=None, + enable_result_image=True, has_depth=False, + face_recognition_segment_id=mem.MemoryID("Human", "FaceRecognition"), ): super().__init__(provider_name, num_result_images) self.persons = persons - self.unknown_person = Person(face_encoding=None, profile_id=None, profile={}) - - self.logger = logging.getLogger(__name__) - self.arviz = viz.Client(__name__) + self.unknown_person = Person() self.has_depth = has_depth self.calibration = { @@ -80,15 +45,24 @@ class FaceDetection(ImageProcessor): "horizontal_fov": 0.8, "vertical_fov": 0.9 } + self.enable_result_image = enable_result_image + self.agent_name = "Armar6" self.camera_frame_name = "AzureKinectCamera" + self.logger = logging.getLogger(__name__) + self.arviz = viz.Client(__name__) + + self.mns = mem.MemoryNameSystem.wait_for_mns() + self.face_recognition_segment_id = face_recognition_segment_id + self.face_reco_writer = self.mns.wait_for_writer(face_recognition_segment_id) + @classmethod def query_human_profiles( cls, mns: mem.MemoryNameSystem, - logger = None + logger=None ): human_reader = mns.wait_for_reader(mem.MemoryID("Human")) result = human_reader.query_core_segment("Profile") @@ -97,29 +71,29 @@ class FaceDetection(ImageProcessor): if logger: logger.info(f"Gathering face images of {id} ...") - face_image_paths = profile["faceImagePaths"] - - for face_image_path in face_image_paths: - from armarx import cmake_helper - + face_encodings = [] + for face_image_path in profile["faceImagePaths"]: package: str = face_image_path["package"] - path: str = face_image_path["path"] + rel_path: str = face_image_path["path"] [data_path] = cmake_helper.get_data_path(package) - abs_path = os.path.join(data_path, package, path) + abs_path = os.path.join(data_path, package, rel_path) if os.path.isfile(abs_path): name = profile["spokenNames"][0] or id.entity_name if logger is not None: - logger.info("Loading person '%s'", name) - logger.debug("Creating face encoding for person %s", name) + logger.info("Loading person '%s'.", name) + logger.debug("Creating face encoding for person '%s'.", name) image = face_recognition.load_image_file(abs_path) encoding = face_recognition.face_encodings(image)[0] - return Person(face_encoding=encoding, profile_id=id, profile=profile) + face_encodings.append(encoding) - return None + if face_encodings: + return Person(face_encodings=face_encodings, profile_id=id, profile=profile) + else: + return None - persons: ty.List[Person] = list(filter(bool, human_reader.for_each_instance_data(gather_persons, result))) + persons: ty.List[Person] = human_reader.for_each_instance_data(gather_persons, result, discard_none=True) return persons def update_calibration(self): @@ -139,35 +113,53 @@ class FaceDetection(ImageProcessor): self.calibration.update(calibration) + def process_images(self, images, info): + image_rgb = images[0] - def visu_pose(self, person_name, x, y, z): - with self.arviz.begin_stage(commit_on_exit=True) as stage: - layer = stage.layer("Location") - layer.add(viz.Sphere(person_name, position=(x, y, z), radius=100, color=(255, 0, 255))) + face_locations = face_recognition.face_locations(image_rgb) + face_encodings = face_recognition.face_encodings(image_rgb, face_locations) + # self.logger.info("Found %s faces.", len(face_locations)) + detected_persons = self.detect_persons(face_encodings) + self.logger.info("Found %s persons.", len(detected_persons)) - def process_images(self, images, info): - image = images[0] + face_recognitions = self.make_face_recognitions(images, face_locations, detected_persons) - face_locations = face_recognition.face_locations(image) - face_encodings = face_recognition.face_encodings(image, face_locations) + self.commit_recognitions(face_recognitions, info.timeProvided) - self.logger.info("Found %s faces.", len(face_locations)) + if self.enable_result_image: + self.draw_result_image(image_rgb, face_locations, detected_persons) + self.result_image_provider.update_image(images, info.timeProvided) + return images, info - known_persons = self.persons - known_face_encodings = [p.face_encoding for p in known_persons] + def detect_persons(self, face_encodings) -> ty.List[Person]: + face_encoding_index: ty.Dict[int, Person] = {} + known_face_encodings: ty.List[np.ndarray] = [] + for person in self.persons: + for encoding in person.face_encodings: + face_encoding_index[len(known_face_encodings)] = person + known_face_encodings.append(encoding) detected_persons = [] for face_encoding in face_encodings: - matches = face_recognition.compare_faces(known_face_encodings, face_encoding) - face_distances = face_recognition.face_distance(known_face_encodings, face_encoding) + matches = face_recognition.compare_faces(face_encoding, known_face_encodings) + face_distances = face_recognition.face_distance(face_encoding, known_face_encodings) best_match_index = np.argmin(face_distances) + if matches[best_match_index] and face_distances[best_match_index] < 0.5: - person = known_persons[best_match_index] + person = face_encoding_index[best_match_index] else: person = self.unknown_person detected_persons.append(person) + return detected_persons + + def make_face_recognitions( + self, + images, + face_locations, + detected_persons: ty.List[Person], + ) -> ty.List[FaceRecognition]: calibration = self.calibration scale_x = math.tan(calibration["horizontal_fov"] / 2.0) * 2.0 scale_y = math.tan(calibration["vertical_fov"] / 2.0) * 2.0 @@ -198,21 +190,46 @@ class FaceDetection(ImageProcessor): ) face_recognitions.append(recognition) - self.visu_pose(person.name(), *position_3d) - - self.logger.info("Found %s faces.", detected_persons) - for (top, right, bottom, left), person in zip(face_locations, detected_persons): - cv2.rectangle(images[0], (left, top), (right, bottom), (0, 0, 255), 2) - cv2.rectangle(images[0], (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED) - - cv2.putText(images[0], person.name(), (left + 6, bottom - 6), cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), 1) + self.visu_pose(person.name(), position_3d) - self.result_image_provider.update_image(images, info.timeProvided) + return face_recognitions - # on_detected_faces(detected_faces, info.timeProvided) - self.commit_recognitions(face_recognitions) + def commit_recognitions( + self, + recognitions: ty.List[FaceRecognition], + time_usec: int + ): + commit = mem.Commit() + for reco in recognitions: + update = commit.add() + entity_name = reco.profile_id.entity_name if reco.profile_id else "<unknown>" + update.entity_id = (self.face_recognition_segment_id + .with_provider_segment_name(__name__) + .with_entity_name(entity_name)) + update.time_created_usec = time_usec + update.instances_data = [reco.to_aron()] + + self.face_reco_writer.commit(commit) + + def visu_pose( + self, + person_name, + position: np.ndarray, + ): + with self.arviz.begin_stage(commit_on_exit=True) as stage: + layer = stage.layer("Location") + layer.add(viz.Sphere(person_name, position=position, radius=100, color=(255, 0, 255))) - return images, info + @classmethod + def draw_result_image( + cls, + image_rgb, + face_locations, + detected_persons, + ): + for (top, right, bottom, left), person in zip(face_locations, detected_persons): + cv2.rectangle(image_rgb, (left, top), (right, bottom), (0, 0, 255), 2) + cv2.rectangle(image_rgb, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED) - def commit_recognitions(self, recognitions: ty.List[FaceRecognition]): - pass + cv2.putText(image_rgb, person.name(), (left + 6, bottom - 6), + cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), 1) diff --git a/python/armarx_face_recognition/armarx_face_recognition/helper.py b/python/armarx_face_recognition/armarx_face_recognition/helper.py index a08d1be1e..d2c55e163 100644 --- a/python/armarx_face_recognition/armarx_face_recognition/helper.py +++ b/python/armarx_face_recognition/armarx_face_recognition/helper.py @@ -5,7 +5,6 @@ from armarx import RobotStateComponentInterfacePrx from armarx import FramedPositionBase from armarx import FramedPoseBase from armarx import FramedOrientationBase -from armarx.robots import A6 def pose2mat(pose: FramedPoseBase) -> np.ndarray: -- GitLab