Skip to content

Commit

Permalink
add disclaimer checks and tests (#202)
Browse files Browse the repository at this point in the history
* add disclaimer checks and tests

* add changes to display

* webcolors library update changes

* disclosure instead of disclaimer

* change interface to disclosure

* update demo notebook

* improve explorer display of disclosure env variable, update docs notebook
  • Loading branch information
iulusoy authored Jun 12, 2024
1 parent 4ac760e commit 894ad09
Show file tree
Hide file tree
Showing 9 changed files with 345 additions and 35 deletions.
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, TextAnalyzer, PostprocessText
Expand All @@ -27,4 +27,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

0 comments on commit 894ad09

Please sign in to comment.