Source code for profile_photo.utils.aws.rekognition_models

from __future__ import annotations

__all__ = [
    'Coordinates',
    'DetectLabelsResp',
    'DetectFacesResp',
    'RecognizeCelebritiesResp',
    'CompareFacesResp',
    'Face',
    'Label',
    'FaceDetail',
    'BoundingBox',
    'Landmark'
]

from dataclasses import dataclass
from functools import cached_property
from typing import List, Literal, Optional

from dataclass_wizard import JSONWizard

from ..dict_helper import DictWithLowerStore


ImageOrientation = Literal['ROTATE_0', 'ROTATE_90', 'ROTATE_180', 'ROTATE_270']


[docs]@dataclass class Coordinates: """ Coordinates dataclass. Can be visualized using this diagram: x1,y1 ----------------- | | | | | | | | | | ------------------ x2,y2 """ x1: int y1: int x2: int y2: int
[docs] @classmethod def from_box(cls, im, box: BoundingBox, offset=0, y_offset=0): im_height, im_width = im.shape[:2] # draw a colored bounding box x1_orig = im_width * box.left y1_orig = im_height * box.top width = im_width * box.width height = im_height * box.height # calculate `offset` as relative to width and height (x and y) dimensions offset_x = im_width * offset offset_y = im_height * y_offset # Assume in the top-left corner of the image we have (x=0, y=0), and at the # bottom-right, we have (x=height, y=width). # # Assumption here is the offset will magnify the bounding box, so it will # *increase* in size. The result coordinates after applying the offset is # indicated by (x1T, y1T) and (x2T,y2T) in the diagram below. # # (0, 0) # ... # \ # \ # x1T,y1T ------------ # | | # | x1,y1 ------ | # | | | | # | | | | # | | | | # | --------x2,y2 | # | | # ---------------x2T,y2T # \ # \ # ... # (x_max, y_max) # -or- # (height, width) # # Hence, from above, note that we want to reduce the value of both (x1, y1) # and increase both (x2, y2). That will get us desired coordinates with offset # applied as expected. x1, y1 = round(x1_orig - offset_x), round(y1_orig - offset_y) x2, y2 = round(x1_orig + (width + offset_x)), round((y1_orig + offset_y) + height) return cls(max(x1, 0), max(y1, 0), min(x2, im_width), min(y2, im_height))
[docs]@dataclass class RecognizeCelebritiesResp(JSONWizard): """ Response from the Rekognition RecognizeCelebrities API """ celebrity_faces: List['CelebrityFace'] unrecognized_faces: List['Face'] orientation_correction: ImageOrientation = 'ROTATE_0'
@dataclass class CelebrityFace: """ CelebrityFace dataclass """ urls: List[str] name: str id: str face: 'Face' match_confidence: float
[docs]@dataclass class Face: """ Face dataclass """ bounding_box: 'BoundingBox' confidence: float landmarks: List['Landmark'] pose: 'Pose' quality: 'Quality'
[docs]@dataclass class DetectLabelsResp(JSONWizard): """ Response from Rekognition DetectLabels API """ labels: List['Label'] # Technically provided in response, but we don't need this. # label_model_version: Union[float, str] @cached_property def label_name_to_value(self) -> DictWithLowerStore[str, 'Label']: """ Return a case-insensitive mapping of label name to label. """ return DictWithLowerStore( {label.name: label for label in self.labels})
[docs] def get_person_box(self, face: FaceDetail | None) -> BoundingBox | None: """ Get the bounding box for the 'Person' label. If there are multiple instances of a 'Person', then the argument for `face` helps pinpoint a matching person. """ labels = self.label_name_to_value person = labels.get('person') if people := person.instances: # if there's only one person in the image if len(people) == 1: first = people[0] return first.bounding_box # if there's multiple people in the image, find the # person instance which wraps the input face box if face is None: return None face_box = face.bounding_box face_left = face_box.left face_right = face_left + face_box.width for person in people: person_box = person.bounding_box person_left = person_box.left person_right = person_left + person_box.width if person_left <= face_left and person_right >= face_right: return person_box return None
@cached_property def person_boxes(self) -> list[BoundingBox] | None: labels = self.label_name_to_value person = labels.get('person') if instances := person.instances: return [i.bounding_box for i in instances] return None
[docs]@dataclass class Label: """ Label dataclass """ name: str confidence: float instances: List['Instance'] parents: List['Parent']
@dataclass class Instance: """ Instance dataclass """ bounding_box: 'BoundingBox' confidence: float @dataclass class Parent: """ Parent dataclass """ name: str
[docs]@dataclass class CompareFacesResp(JSONWizard): """ Response from the Rekognition CompareFaces API """ source_image_face: 'SourceImageFace' face_matches: List['FaceMatch'] unmatched_faces: List['UnmatchedFace']
@dataclass class SourceImageFace: """ SourceImageFace dataclass """ bounding_box: 'BoundingBox' confidence: float @dataclass class FaceMatch: """ FaceMatch dataclass """ similarity: float face: 'Face' @dataclass class UnmatchedFace: """ UnmatchedFace dataclass """ bounding_box: 'BoundingBox' confidence: float landmarks: List['Landmark'] pose: 'Pose' quality: 'Quality'
[docs]@dataclass class DetectFacesResp(JSONWizard): """ Response from the Rekognition DetectFaces API """ face_details: List['FaceDetail']
[docs] def get_face(self) -> Optional['FaceDetail']: """ Return the primary face that is detected, if available. """ try: return self.face_details[0] except IndexError: return None
[docs]@dataclass class FaceDetail: """ FaceDetail dataclass """ bounding_box: 'BoundingBox' age_range: 'AgeRange' smile: 'Smile' eyeglasses: 'Eyeglasses' sunglasses: 'Sunglasses' gender: 'Gender' beard: 'Beard' mustache: 'Mustache' eyes_open: 'EyesOpen' mouth_open: 'MouthOpen' emotions: List['Emotion'] landmarks: List['Landmark'] pose: 'Pose' quality: 'Quality' confidence: float @cached_property def emotion_to_confidence(self) -> DictWithLowerStore[str, float]: """ Return a case-insensitive mapping of emotion name to its confidence. """ return DictWithLowerStore( {e.type: e.confidence for e in self.emotions})
[docs]@dataclass class BoundingBox: """ BoundingBox dataclass """ width: float height: float left: float top: float
@dataclass class AgeRange: """ AgeRange dataclass """ low: int high: int @dataclass class Smile: """ Smile dataclass """ value: bool confidence: float @dataclass class Eyeglasses: """ Eyeglasses dataclass """ value: bool confidence: float @dataclass class Sunglasses: """ Sunglasses dataclass """ value: bool confidence: float @dataclass class Gender: """ Gender dataclass """ value: str confidence: float @dataclass class Beard: """ Beard dataclass """ value: bool confidence: float @dataclass class Mustache: """ Mustache dataclass """ value: bool confidence: float @dataclass class EyesOpen: """ EyesOpen dataclass """ value: bool confidence: float @dataclass class MouthOpen: """ MouthOpen dataclass """ value: bool confidence: float @dataclass class Emotion: """ Emotion dataclass """ type: str confidence: float
[docs]@dataclass class Landmark: """ Landmark dataclass """ type: str x: float y: float
@dataclass class Pose: """ Pose dataclass """ roll: float yaw: float pitch: float @dataclass class Quality: """ Quality dataclass """ brightness: float sharpness: float