"""Module contains face sample models"""
from enum import Enum
from operator import add
from typing import List, Optional
import attr
from attr import dataclass
from lunavl.sdk.descriptors.descriptors import FaceDescriptor
from lunavl.sdk.detectors.facedetector import FaceDetection, Landmarks5
from lunavl.sdk.estimators.face_estimators.background import FaceDetectionBackground
from lunavl.sdk.estimators.face_estimators.basic_attributes import BasicAttributes
from lunavl.sdk.estimators.face_estimators.emotions import Emotion, Emotions
from lunavl.sdk.estimators.face_estimators.eyebrow_expressions import EyebrowExpressions
from lunavl.sdk.estimators.face_estimators.eyes import EyesEstimation, GazeDirection
from lunavl.sdk.estimators.face_estimators.facewarper import FaceWarpedImage
from lunavl.sdk.estimators.face_estimators.fisheye import Fisheye
from lunavl.sdk.estimators.face_estimators.glasses import Glasses
from lunavl.sdk.estimators.face_estimators.head_pose import HeadPose
from lunavl.sdk.estimators.face_estimators.headwear import Headwear
from lunavl.sdk.estimators.face_estimators.image_type import ImageColorType
from lunavl.sdk.estimators.face_estimators.livenessv1 import LivenessV1
from lunavl.sdk.estimators.face_estimators.mask import Mask, MaskState
from lunavl.sdk.estimators.face_estimators.mouth_state import MouthStates
from lunavl.sdk.estimators.face_estimators.natural_light import FaceNaturalLight
from lunavl.sdk.estimators.face_estimators.red_eye import RedEyes
from lunavl.sdk.estimators.face_estimators.warp_quality import Quality
from .filtration.base import FiltrationResults
[docs]class ScoreOperator(Enum):
"""Score operator enum."""
# sum values
concat = add
# get higher value
higher = max
[docs]@dataclass(slots=True)
class FaceSample:
"""Face attributes containers"""
# face detetction (None if origin is face warp)
detection: Optional[FaceDetection] = None
# eyes estimation
eyes: Optional[EyesEstimation] = None
# gaze direction estimation
gaze: Optional[GazeDirection] = None
# emotions estimation
emotions: Optional[Emotions] = None
# mouth state estimation
mouthState: Optional[MouthStates] = None
# basic attributes estimation
basicAttributes: Optional[BasicAttributes] = None
# face warp quality estimation
warpQuality: Optional[Quality] = None
# mask estimation
mask: Optional[Mask] = None
# glasses estimation
glasses: Optional[Glasses] = None
# head pose estimation
headPose: Optional[HeadPose] = None
# transformed landmarks5
transformedLandmarks5: Optional[Landmarks5] = None
# livenessV1 estimation
livenessV1: Optional[LivenessV1] = None
# face warp estimation
warp: Optional[FaceWarpedImage] = None
# face descriptor estimation
descriptor: Optional[FaceDescriptor] = None
# headwear estimation
headwear: Optional[Headwear] = None
# fisheye estimation
fisheye: Optional[Fisheye] = None
# red-eyes estimation
redEyes: Optional[RedEyes] = None
# eyebrow expression estimation
eyebrowExpression: Optional[EyebrowExpressions] = None
# face natural light estimation
naturalLight: Optional[FaceNaturalLight] = None
# detection background estimation
detectionBackground: Optional[FaceDetectionBackground] = None
# image color type estimation
imageColorType: Optional[ImageColorType] = None
# filtration results
filters: FiltrationResults = attr.field(factory=FiltrationResults)
# dynamic range
dynamicRange: Optional[float] = None
[docs]@dataclass(slots=True)
class AggregateAttributesCounter:
"""Container class for counting aggregate attributes."""
# attribute score
score: float
# top attribute index
index: int
# count of attribute
count: int = 1
# score operator
operator: ScoreOperator = ScoreOperator.concat
# highest score for predominant attribute
_highestScore: float = attr.field(init=False, repr=False)
def __attrs_post_init__(self):
"""post init attributes"""
self._highestScore = self.score
[docs] def update(self, score: float, index: int) -> None:
"""
Update attribute score and set top index.
Args:
score: next attribute score
index: attribute index
"""
self.count += 1
if score > self._highestScore:
self._highestScore = score
self.index = index
self.score = self.operator.value(self.score, score)
# helper map: enum to attribute name Mask
_MASK_TO_ATTR_NAME_MAP = {
MaskState.Occluded: "occluded",
MaskState.Missing: "missing",
MaskState.MedicalMask: "medicalMask",
}
# helper map: enum to jsom name
_MASK_TO_DICT_NAME_MAP = {
MaskState.Occluded: "occluded",
MaskState.Missing: "missing",
MaskState.MedicalMask: "medical_mask",
}
[docs]class AggregatedMask:
"""
Aggregated mask
Attributes:
predominateMask (MaskState): predominant aggregated mask
medicalMask (float): aggregated mask score
occluded (float): aggregated occluded score
missing (float): aggregated missing score
"""
__slots__ = ("medicalMask", "missing", "occluded", "predominateMask", "faceOcclusion")
def __init__(self, masks: list[Mask]):
predominantToCountAndScore = {}
# get most frequent mask. If there are several mask we will choose with higher sum score
for index, mask in enumerate(masks):
predominant = mask.predominateMask
score = mask.__getattribute__(_MASK_TO_ATTR_NAME_MAP[predominant])
if predominant in predominantToCountAndScore:
predominantToCountAndScore[predominant].update(score=score, index=index)
else:
predominantToCountAndScore[predominant] = AggregateAttributesCounter(
score=score, index=index, operator=ScoreOperator.concat
)
topAttributeIndex = sorted(
sorted(predominantToCountAndScore.values(), key=lambda attrib: attrib.score, reverse=True),
key=lambda attrib: attrib.count,
reverse=True,
)[0].index
predominantMask = masks[topAttributeIndex]
self.predominateMask = predominantMask.predominateMask
self.medicalMask = predominantMask.medicalMask
self.occluded = predominantMask.occluded
self.missing = predominantMask.missing
# get most frequent occlusion. If there are several mask we will choose with higher sum score
for index, mask in enumerate(masks):
predominant = mask.faceOcclusion.predominantOcclusion
score = mask.faceOcclusion.__getattribute__(predominant.name.lower())
if predominant in predominantToCountAndScore:
predominantToCountAndScore[predominant].update(score=score, index=index)
else:
predominantToCountAndScore[predominant] = AggregateAttributesCounter(
score=score, index=index, operator=ScoreOperator.concat
)
topAttributeIndex = sorted(
sorted(predominantToCountAndScore.values(), key=lambda attrib: attrib.score, reverse=True),
key=lambda attrib: attrib.count,
reverse=True,
)[0].index
predominantOcclusion = masks[topAttributeIndex].faceOcclusion
self.faceOcclusion = predominantOcclusion
def __repr__(self) -> str:
"""
Representation.
Returns:
str(self.asDict())
"""
return str(self.asDict())
[docs] def asDict(self) -> dict:
"""Convert aggregated mask to dict"""
return {
"predominant_mask": _MASK_TO_DICT_NAME_MAP[self.predominateMask],
"estimations": {"medical_mask": self.medicalMask, "missing": self.missing, "occluded": self.occluded},
"face_occlusion": self.faceOcclusion.asDict(),
}
# helper map: enum to an Emontions attribute name
_EMOTION_TO_NAME_MAP = {
Emotion.Anger: "anger",
Emotion.Disgust: "disgust",
Emotion.Fear: "fear",
Emotion.Happiness: "happiness",
Emotion.Neutral: "neutral",
Emotion.Sadness: "sadness",
Emotion.Surprise: "surprise",
}
[docs]class AggregatedEmotions:
"""
Aggregated emotions
Attributes:
predominateEmotion (Emotion): predominant aggregated emotion
anger (float): aggregated anger score
disgust (float): aggregated disgust score
fear (float): aggregated fear score
happiness (float): aggregated happiness score
neutral (float): aggregated neutral score
sadness (float): aggregated sadness score
surprise (float): aggregated surprise score
"""
__slots__ = ("anger", "disgust", "fear", "happiness", "neutral", "sadness", "surprise", "predominateEmotion")
def __init__(self, emotions: list[Emotions]):
predominantToCountAndScore = {}
for index, emotion in enumerate(emotions):
predominant = emotion.predominateEmotion
score = emotion.__getattribute__(_EMOTION_TO_NAME_MAP[predominant])
if predominant in predominantToCountAndScore:
predominantToCountAndScore[predominant].update(score=score, index=index)
else:
predominantToCountAndScore[predominant] = AggregateAttributesCounter(
score=score, index=index, operator=ScoreOperator.higher
)
topAttributeIndex = sorted(
sorted(predominantToCountAndScore.values(), key=lambda attrib: attrib.score, reverse=True),
key=lambda attrib: attrib.count,
reverse=True,
)[0].index
predominateEmotion = emotions[topAttributeIndex]
self.predominateEmotion = predominateEmotion.predominateEmotion
self.anger = predominateEmotion.anger
self.disgust = predominateEmotion.disgust
self.fear = predominateEmotion.fear
self.sadness = predominateEmotion.sadness
self.surprise = predominateEmotion.surprise
self.neutral = predominateEmotion.neutral
self.happiness = predominateEmotion.happiness
[docs] def asDict(self) -> dict:
"""Convert aggregated emotions to dict"""
return {
"predominant_emotion": _EMOTION_TO_NAME_MAP[self.predominateEmotion],
"estimations": {
"anger": self.anger,
"disgust": self.disgust,
"fear": self.fear,
"sadness": self.sadness,
"surprise": self.surprise,
"neutral": self.neutral,
"happiness": self.happiness,
},
}
def __repr__(self) -> str:
"""
Representation.
Returns:
str(self.asDict())
"""
return str(self.asDict())
[docs]@dataclass(slots=True)
class AggregatedFaceSample:
"""Aggregated face sample contaiter"""
# origin samples
samples: List[FaceSample] = attr.field(factory=list)
# aggregated liveness
liveness: Optional[LivenessV1] = None
# aggregated descriptor
descriptor: Optional[FaceDescriptor] = None
# aggregated basic attributes
basicAttributes: Optional[BasicAttributes] = None
# applied filters
filters: FiltrationResults = attr.field(factory=FiltrationResults)
# aggregated mask
mask: Optional[AggregatedMask] = None
# aggregated emotions
emotions: Optional[AggregatedEmotions] = None