Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add disclaimer checks and tests #202

Merged
merged 7 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ammico/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import importlib_metadata as metadata # type: ignore
from ammico.cropposts import crop_media_posts, crop_posts_from_refs
from ammico.display import AnalysisExplorer
from ammico.faces import EmotionDetector
from ammico.faces import EmotionDetector, ethical_disclosure
from ammico.multimodal_search import MultimodalSearch
from ammico.summary import SummaryDetector
from ammico.text import TextDetector, PostprocessText
Expand All @@ -26,4 +26,5 @@
"PostprocessText",
"find_files",
"get_dataframe",
"ethical_disclosure",
]
2 changes: 1 addition & 1 deletion ammico/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def rgb2name(
output_color = output_color.lower().replace("grey", "gray")
except ValueError:
delta_e_lst = []
filtered_colors = webcolors.CSS3_NAMES_TO_HEX
filtered_colors = webcolors._definitions._CSS3_NAMES_TO_HEX

for _, img_hex in filtered_colors.items():
cur_clr = webcolors.hex_to_rgb(img_hex)
Expand Down
64 changes: 63 additions & 1 deletion ammico/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ def __init__(self, mydict: dict) -> None:
State("setting_Text_revision_numbers", "value"),
State("setting_Emotion_emotion_threshold", "value"),
State("setting_Emotion_race_threshold", "value"),
State("setting_Emotion_gender_threshold", "value"),
State("setting_Emotion_age_threshold", "value"),
State("setting_Emotion_env_var", "value"),
State("setting_Color_delta_e_method", "value"),
State("setting_Summary_analysis_type", "value"),
State("setting_Summary_model", "value"),
Expand Down Expand Up @@ -200,6 +203,13 @@ def _create_setting_layout(self):
style={"width": "100%"},
),
),
dbc.Col(
[
html.P(
"Select name of the environment variable to accept or reject the disclosure*:"
),
]
),
dbc.Col(
dcc.Input(
type="text",
Expand Down Expand Up @@ -246,6 +256,48 @@ def _create_setting_layout(self):
],
align="start",
),
dbc.Col(
[
html.P("Gender threshold"),
dcc.Input(
type="number",
value=50,
max=100,
min=0,
id="setting_Emotion_gender_threshold",
style={"width": "100%"},
),
],
align="start",
),
dbc.Col(
[
html.P("Age threshold"),
dcc.Input(
type="number",
value=50,
max=100,
min=0,
id="setting_Emotion_age_threshold",
style={"width": "100%"},
),
],
align="start",
),
dbc.Col(
[
html.P(
"Disclosure acceptance environment variable"
),
dcc.Input(
type="text",
value="DISCLOSURE_AMMICO",
id="setting_Emotion_env_var",
style={"width": "100%"},
),
],
align="start",
),
],
style={"width": "100%"},
),
Expand Down Expand Up @@ -441,6 +493,9 @@ def _right_output_analysis(
settings_text_revision_numbers: str,
setting_emotion_emotion_threshold: int,
setting_emotion_race_threshold: int,
setting_emotion_gender_threshold: int,
setting_emotion_age_threshold: int,
setting_emotion_env_var: str,
setting_color_delta_e_method: str,
setting_summary_analysis_type: str,
setting_summary_model: str,
Expand Down Expand Up @@ -493,8 +548,15 @@ def _right_output_analysis(
elif detector_value == "EmotionDetector":
detector_class = identify_function(
image_copy,
race_threshold=setting_emotion_race_threshold,
emotion_threshold=setting_emotion_emotion_threshold,
race_threshold=setting_emotion_race_threshold,
gender_threshold=setting_emotion_gender_threshold,
age_threshold=setting_emotion_age_threshold,
accept_disclosure=(
setting_emotion_env_var
if setting_emotion_env_var
else "DISCLOSURE_AMMICO"
),
)
elif detector_value == "ColorDetector":
detector_class = identify_function(
Expand Down
127 changes: 117 additions & 10 deletions ammico/faces.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,78 @@ def _processor(fname, action, pooch):
)


def ethical_disclosure(accept_disclosure: str = "DISCLOSURE_AMMICO"):
"""
Asks the user to accept the ethical disclosure.

Args:
accept_disclosure (str): The name of the disclosure variable (default: "DISCLOSURE_AMMICO").
"""
if not os.environ.get(accept_disclosure):
accepted = _ask_for_disclosure_acceptance(accept_disclosure)
elif os.environ.get(accept_disclosure) == "False":
accepted = False
elif os.environ.get(accept_disclosure) == "True":
accepted = True
else:
print(
"Could not determine disclosure - skipping \
race/ethnicity, gender and age detection."
)
accepted = False
return accepted


def _ask_for_disclosure_acceptance(accept_disclosure: str = "DISCLOSURE_AMMICO"):
"""
Asks the user to accept the disclosure.
"""
print("This analysis uses the DeepFace and RetinaFace libraries.")
print(
"""
DeepFace and RetinaFace provide wrappers to trained models in face recognition and
emotion detection. Age, gender and race / ethnicity models were trained
on the backbone of VGG-Face with transfer learning.
ETHICAL DISCLOSURE STATEMENT:
The Emotion Detector uses RetinaFace to probabilistically assess the gender, age and
race of the detected faces. Such assessments may not reflect how the individuals
identified by the tool view themselves. Additionally, the classification is carried
out in simplistic categories and contains only the most basic classes, for example
“male” and “female” for gender. By continuing to use the tool, you certify that you
understand the ethical implications such assessments have for the interpretation of
the results.
"""
)
answer = input("Do you accept the disclosure? (yes/no): ")
answer = answer.lower().strip()
if answer == "yes":
print("You have accepted the disclosure.")
print(
"""Age, gender, race/ethnicity detection will be performed based on the provided
confidence thresholds."""
)
os.environ[accept_disclosure] = "True"
accepted = True
elif answer == "no":
print("You have not accepted the disclosure.")
print("No age, gender, race/ethnicity detection will be performed.")
os.environ[accept_disclosure] = "False"
accepted = False
else:
print("Please answer with yes or no.")
accepted = _ask_for_disclosure_acceptance()
return accepted


class EmotionDetector(AnalysisMethod):
def __init__(
self,
subdict: dict,
emotion_threshold: float = 50.0,
race_threshold: float = 50.0,
gender_threshold: float = 50.0,
age_threshold: float = 50.0,
accept_disclosure: str = "DISCLOSURE_AMMICO",
) -> None:
"""
Initializes the EmotionDetector object.
Expand All @@ -94,6 +160,10 @@ def __init__(
subdict (dict): The dictionary to store the analysis results.
emotion_threshold (float): The threshold for detecting emotions (default: 50.0).
race_threshold (float): The threshold for detecting race (default: 50.0).
gender_threshold (float): The threshold for detecting gender (default: 50.0).
age_threshold (float): The threshold for detecting age (default: 50.0).
accept_disclosure (str): The name of the disclosure variable, that is
set upon accepting the disclosure (default: "DISCLOSURE_AMMICO").
"""
super().__init__(subdict)
self.subdict.update(self.set_keys())
Expand All @@ -102,8 +172,14 @@ def __init__(
raise ValueError("Emotion threshold must be between 0 and 100.")
if race_threshold < 0 or race_threshold > 100:
raise ValueError("Race threshold must be between 0 and 100.")
if gender_threshold < 0 or gender_threshold > 100:
raise ValueError("Gender threshold must be between 0 and 100.")
if age_threshold < 0 or age_threshold > 100:
raise ValueError("Age threshold must be between 0 and 100.")
self.emotion_threshold = emotion_threshold
self.race_threshold = race_threshold
self.gender_threshold = gender_threshold
self.age_threshold = age_threshold
self.emotion_categories = {
"angry": "Negative",
"disgust": "Negative",
Expand All @@ -113,6 +189,7 @@ def __init__(
"surprise": "Neutral",
"neutral": "Neutral",
}
self.accepted = ethical_disclosure(accept_disclosure)

def set_keys(self) -> dict:
"""
Expand Down Expand Up @@ -143,6 +220,44 @@ def analyse_image(self) -> dict:
"""
return self.facial_expression_analysis()

def _define_actions(self, fresult: dict) -> list:
# Adapt the features we are looking for depending on whether a mask is worn.
# White masks screw race detection, emotion detection is useless.
# also, depending on the disclosure, we might not want to run the analysis
# for gender, age, ethnicity/race
conditional_actions = {
"all": ["age", "gender", "race", "emotion"],
"all_with_mask": ["age", "gender"],
"restricted_access": ["emotion"],
"restricted_access_with_mask": [],
}
if fresult["wears_mask"] and self.accepted:
actions = conditional_actions["all_with_mask"]
elif fresult["wears_mask"] and not self.accepted:
actions = conditional_actions["restricted_access_with_mask"]
elif not fresult["wears_mask"] and self.accepted:
actions = conditional_actions["all"]
elif not fresult["wears_mask"] and not self.accepted:
actions = conditional_actions["restricted_access"]
else:
raise ValueError(
"Invalid mask detection {} and disclosure \
acceptance {} result.".format(
fresult["wears_mask"], self.accepted
)
)
return actions

def _ensure_deepface_models(self, actions: list):
# Ensure that all data has been fetched by pooch
deepface_face_expression_model.get()
if "race" in actions:
deepface_race_model.get()
if "age" in actions:
deepface_age_model.get()
if "gender" in actions:
deepface_gender_model.get()

def analyze_single_face(self, face: np.ndarray) -> dict:
"""
Analyzes the features of a single face.
Expand All @@ -156,16 +271,8 @@ def analyze_single_face(self, face: np.ndarray) -> dict:
fresult = {}
# Determine whether the face wears a mask
fresult["wears_mask"] = self.wears_mask(face)
# Adapt the features we are looking for depending on whether a mask is worn.
# White masks screw race detection, emotion detection is useless.
actions = ["age", "gender"]
if not fresult["wears_mask"]:
actions = actions + ["race", "emotion"]
# Ensure that all data has been fetched by pooch
deepface_age_model.get()
deepface_face_expression_model.get()
deepface_gender_model.get()
deepface_race_model.get()
actions = self._define_actions(fresult)
self._ensure_deepface_models(actions)
# Run the full DeepFace analysis
fresult.update(
DeepFace.analyze(
Expand Down
Loading
Loading