зеркало из
https://github.com/ssciwr/AMMICO.git
synced 2025-10-29 05:04:14 +02:00
* fix typos * add buttons for google colab everywhere * update readme, separate out FAQ * add privacy disclosure statement * do not install using uv * update docs notebook * explicit install of libopenblas * explicit install of libopenblas * explicit install of libopenblas * try to get scipy installed using uv * use ubuntu 24.04 * go back to pip * try with scipy only * try with a few others * use hatchling * wording changes, install all requirements * fix offending spacy version * run all tests * include faq in documentation, fix link
602 строки
23 KiB
Python
602 строки
23 KiB
Python
import ammico.faces as faces
|
|
import ammico.text as text
|
|
import ammico.colors as colors
|
|
from ammico.utils import is_interactive
|
|
import ammico.summary as summary
|
|
import pandas as pd
|
|
from dash import html, Input, Output, dcc, State, Dash
|
|
from PIL import Image
|
|
import dash_bootstrap_components as dbc
|
|
|
|
|
|
COLOR_SCHEMES = [
|
|
"CIE 1976",
|
|
"CIE 1994",
|
|
"CIE 2000",
|
|
"CMC",
|
|
"ITP",
|
|
"CAM02-LCD",
|
|
"CAM02-SCD",
|
|
"CAM02-UCS",
|
|
"CAM16-LCD",
|
|
"CAM16-SCD",
|
|
"CAM16-UCS",
|
|
"DIN99",
|
|
]
|
|
SUMMARY_ANALYSIS_TYPE = ["summary_and_questions", "summary", "questions"]
|
|
SUMMARY_MODEL = ["base", "large"]
|
|
|
|
|
|
class AnalysisExplorer:
|
|
def __init__(self, mydict: dict) -> None:
|
|
"""Initialize the AnalysisExplorer class to create an interactive
|
|
visualization of the analysis results.
|
|
|
|
Args:
|
|
mydict (dict): A nested dictionary containing image data for all images.
|
|
|
|
"""
|
|
self.app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
|
|
self.mydict = mydict
|
|
self.theme = {
|
|
"scheme": "monokai",
|
|
"author": "wimer hazenberg (http://www.monokai.nl)",
|
|
"base00": "#272822",
|
|
"base01": "#383830",
|
|
"base02": "#49483e",
|
|
"base03": "#75715e",
|
|
"base04": "#a59f85",
|
|
"base05": "#f8f8f2",
|
|
"base06": "#f5f4f1",
|
|
"base07": "#f9f8f5",
|
|
"base08": "#f92672",
|
|
"base09": "#fd971f",
|
|
"base0A": "#f4bf75",
|
|
"base0B": "#a6e22e",
|
|
"base0C": "#a1efe4",
|
|
"base0D": "#66d9ef",
|
|
"base0E": "#ae81ff",
|
|
"base0F": "#cc6633",
|
|
}
|
|
|
|
# Setup the layout
|
|
app_layout = html.Div(
|
|
[
|
|
# Top row, only file explorer
|
|
dbc.Row(
|
|
[dbc.Col(self._top_file_explorer(mydict))],
|
|
id="Div_top",
|
|
style={
|
|
"width": "30%",
|
|
},
|
|
),
|
|
# second row, middle picture and right output
|
|
dbc.Row(
|
|
[
|
|
# first column: picture
|
|
dbc.Col(self._middle_picture_frame()),
|
|
dbc.Col(self._right_output_json()),
|
|
]
|
|
),
|
|
],
|
|
# style={"width": "95%", "display": "inline-block"},
|
|
)
|
|
self.app.layout = app_layout
|
|
|
|
# Add callbacks to the app
|
|
self.app.callback(
|
|
Output("img_middle_picture_id", "src"),
|
|
Input("left_select_id", "value"),
|
|
prevent_initial_call=True,
|
|
)(self.update_picture)
|
|
|
|
self.app.callback(
|
|
Output("right_json_viewer", "children"),
|
|
Input("button_run", "n_clicks"),
|
|
State("left_select_id", "options"),
|
|
State("left_select_id", "value"),
|
|
State("Dropdown_select_Detector", "value"),
|
|
State("setting_Text_analyse_text", "value"),
|
|
State("setting_Text_model_names", "value"),
|
|
State("setting_Text_revision_numbers", "value"),
|
|
State("setting_privacy_env_var", "value"),
|
|
State("setting_Emotion_emotion_threshold", "value"),
|
|
State("setting_Emotion_race_threshold", "value"),
|
|
State("setting_Emotion_gender_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"),
|
|
State("setting_Summary_list_of_questions", "value"),
|
|
prevent_initial_call=True,
|
|
)(self._right_output_analysis)
|
|
|
|
self.app.callback(
|
|
Output("settings_TextDetector", "style"),
|
|
Output("settings_EmotionDetector", "style"),
|
|
Output("settings_ColorDetector", "style"),
|
|
Output("settings_Summary_Detector", "style"),
|
|
Input("Dropdown_select_Detector", "value"),
|
|
)(self._update_detector_setting)
|
|
|
|
# I split the different sections into subfunctions for better clarity
|
|
def _top_file_explorer(self, mydict: dict) -> html.Div:
|
|
"""Initialize the file explorer dropdown for selecting the file to be analyzed.
|
|
|
|
Args:
|
|
mydict (dict): A dictionary containing image data.
|
|
|
|
Returns:
|
|
html.Div: The layout for the file explorer dropdown.
|
|
"""
|
|
left_layout = html.Div(
|
|
[
|
|
dcc.Dropdown(
|
|
options={value["filename"]: key for key, value in mydict.items()},
|
|
id="left_select_id",
|
|
)
|
|
]
|
|
)
|
|
return left_layout
|
|
|
|
def _middle_picture_frame(self) -> html.Div:
|
|
"""Initialize the picture frame to display the image.
|
|
|
|
Returns:
|
|
html.Div: The layout for the picture frame.
|
|
"""
|
|
middle_layout = html.Div(
|
|
[
|
|
html.Img(
|
|
id="img_middle_picture_id",
|
|
style={
|
|
"width": "80%",
|
|
},
|
|
)
|
|
]
|
|
)
|
|
return middle_layout
|
|
|
|
def _create_setting_layout(self):
|
|
settings_layout = html.Div(
|
|
[
|
|
# text summary start
|
|
html.Div(
|
|
id="settings_TextDetector",
|
|
style={"display": "none"},
|
|
children=[
|
|
dbc.Row(
|
|
dcc.Checklist(
|
|
["Analyse text"],
|
|
["Analyse text"],
|
|
id="setting_Text_analyse_text",
|
|
style={"margin-bottom": "10px"},
|
|
),
|
|
),
|
|
# row 1
|
|
dbc.Row(
|
|
dbc.Col(
|
|
[
|
|
html.P(
|
|
"Privacy disclosure acceptance environment variable"
|
|
),
|
|
dcc.Input(
|
|
type="text",
|
|
value="PRIVACY_AMMICO",
|
|
id="setting_privacy_env_var",
|
|
style={"width": "100%"},
|
|
),
|
|
],
|
|
align="start",
|
|
),
|
|
),
|
|
# text row 2
|
|
dbc.Row(
|
|
[
|
|
dbc.Col(
|
|
[
|
|
html.P(
|
|
"Select models for text_summary, text_sentiment, text_NER or leave blank for default:",
|
|
# style={"width": "45%"},
|
|
),
|
|
]
|
|
), #
|
|
dbc.Col(
|
|
[
|
|
html.P(
|
|
"Select model revision number for text_summary, text_sentiment, text_NER or leave blank for default:"
|
|
),
|
|
]
|
|
),
|
|
]
|
|
), # row 2
|
|
# input row 3
|
|
dbc.Row(
|
|
[
|
|
dbc.Col(
|
|
dcc.Input(
|
|
type="text",
|
|
id="setting_Text_model_names",
|
|
style={"width": "100%"},
|
|
),
|
|
),
|
|
dbc.Col(
|
|
dcc.Input(
|
|
type="text",
|
|
id="setting_Text_revision_numbers",
|
|
style={"width": "100%"},
|
|
),
|
|
),
|
|
]
|
|
), # row 3
|
|
],
|
|
), # text summary end
|
|
# start emotion detector
|
|
html.Div(
|
|
id="settings_EmotionDetector",
|
|
style={"display": "none"},
|
|
children=[
|
|
dbc.Row(
|
|
[
|
|
dbc.Col(
|
|
[
|
|
html.P("Emotion threshold"),
|
|
dcc.Input(
|
|
value=50,
|
|
type="number",
|
|
max=100,
|
|
min=0,
|
|
id="setting_Emotion_emotion_threshold",
|
|
style={"width": "100%"},
|
|
),
|
|
],
|
|
align="start",
|
|
),
|
|
dbc.Col(
|
|
[
|
|
html.P("Race threshold"),
|
|
dcc.Input(
|
|
type="number",
|
|
value=50,
|
|
max=100,
|
|
min=0,
|
|
id="setting_Emotion_race_threshold",
|
|
style={"width": "100%"},
|
|
),
|
|
],
|
|
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(
|
|
"Disclosure acceptance environment variable"
|
|
),
|
|
dcc.Input(
|
|
type="text",
|
|
value="DISCLOSURE_AMMICO",
|
|
id="setting_Emotion_env_var",
|
|
style={"width": "100%"},
|
|
),
|
|
],
|
|
align="start",
|
|
),
|
|
],
|
|
style={"width": "100%"},
|
|
),
|
|
],
|
|
), # end emotion detector
|
|
html.Div(
|
|
id="settings_ColorDetector",
|
|
style={"display": "none"},
|
|
children=[
|
|
html.Div(
|
|
[
|
|
dcc.Dropdown(
|
|
options=COLOR_SCHEMES,
|
|
value="CIE 1976",
|
|
id="setting_Color_delta_e_method",
|
|
)
|
|
],
|
|
style={
|
|
"width": "49%",
|
|
"display": "inline-block",
|
|
"margin-top": "10px",
|
|
},
|
|
)
|
|
],
|
|
),
|
|
html.Div(
|
|
id="settings_Summary_Detector",
|
|
style={"display": "none"},
|
|
children=[
|
|
dbc.Col(
|
|
[
|
|
dbc.Row([html.P("Analysis type:")]),
|
|
dbc.Row([html.P("Model type:")]),
|
|
dbc.Row([html.P("Analysis question:")]),
|
|
],
|
|
),
|
|
dbc.Col(
|
|
[
|
|
dbc.Row(
|
|
dcc.Dropdown(
|
|
options=SUMMARY_ANALYSIS_TYPE,
|
|
value="summary_and_questions",
|
|
id="setting_Summary_analysis_type",
|
|
)
|
|
),
|
|
dbc.Row(
|
|
dcc.Dropdown(
|
|
options=SUMMARY_MODEL,
|
|
value="base",
|
|
id="setting_Summary_model",
|
|
)
|
|
),
|
|
dbc.Row(
|
|
dcc.Input(
|
|
type="text",
|
|
id="setting_Summary_list_of_questions",
|
|
style={
|
|
"height": "auto",
|
|
"margin-left": "11px",
|
|
},
|
|
),
|
|
),
|
|
]
|
|
),
|
|
],
|
|
),
|
|
],
|
|
style={"width": "100%", "display": "inline-block"},
|
|
)
|
|
return settings_layout
|
|
|
|
def _right_output_json(self) -> html.Div:
|
|
"""Initialize the DetectorDropdown, argument Div and JSON viewer for displaying the analysis output.
|
|
|
|
Returns:
|
|
html.Div: The layout for the JSON viewer.
|
|
"""
|
|
right_layout = html.Div(
|
|
[
|
|
dbc.Col(
|
|
[
|
|
dbc.Row(
|
|
dcc.Dropdown(
|
|
options=[
|
|
"TextDetector",
|
|
"EmotionDetector",
|
|
"SummaryDetector",
|
|
"ColorDetector",
|
|
],
|
|
value="TextDetector",
|
|
id="Dropdown_select_Detector",
|
|
style={"width": "60%"},
|
|
),
|
|
justify="start",
|
|
),
|
|
dbc.Row(
|
|
children=[self._create_setting_layout()],
|
|
id="div_detector_args",
|
|
justify="start",
|
|
),
|
|
dbc.Row(
|
|
html.Button(
|
|
"Run Detector",
|
|
id="button_run",
|
|
style={
|
|
"margin-top": "15px",
|
|
"margin-bottom": "15px",
|
|
"margin-left": "11px",
|
|
"width": "30%",
|
|
},
|
|
),
|
|
justify="start",
|
|
),
|
|
dbc.Row(
|
|
dcc.Loading(
|
|
id="loading-2",
|
|
children=[
|
|
# This is where the json is shown.
|
|
html.Div(id="right_json_viewer"),
|
|
],
|
|
type="circle",
|
|
),
|
|
justify="start",
|
|
),
|
|
],
|
|
align="start",
|
|
)
|
|
]
|
|
)
|
|
return right_layout
|
|
|
|
def run_server(self, port: int = 8050) -> None:
|
|
"""Run the Dash server to start the analysis explorer.
|
|
|
|
|
|
Args:
|
|
port (int, optional): The port number to run the server on (default: 8050).
|
|
"""
|
|
|
|
self.app.run_server(debug=True, port=port)
|
|
|
|
# Dash callbacks
|
|
def update_picture(self, img_path: str):
|
|
"""Callback function to update the displayed image.
|
|
|
|
Args:
|
|
img_path (str): The path of the selected image.
|
|
|
|
Returns:
|
|
Union[PIL.PngImagePlugin, None]: The image object to be displayed
|
|
or None if the image path is
|
|
|
|
"""
|
|
if img_path is not None:
|
|
image = Image.open(img_path)
|
|
return image
|
|
else:
|
|
return None
|
|
|
|
def _update_detector_setting(self, setting_input):
|
|
# return settings_TextDetector -> style, settings_EmotionDetector -> style
|
|
display_none = {"display": "none"}
|
|
display_flex = {
|
|
"display": "flex",
|
|
"flexWrap": "wrap",
|
|
"width": 400,
|
|
"margin-top": "20px",
|
|
}
|
|
|
|
if setting_input == "TextDetector":
|
|
return display_flex, display_none, display_none, display_none
|
|
|
|
if setting_input == "EmotionDetector":
|
|
return display_none, display_flex, display_none, display_none
|
|
|
|
if setting_input == "ColorDetector":
|
|
return display_none, display_none, display_flex, display_none
|
|
|
|
if setting_input == "SummaryDetector":
|
|
return display_none, display_none, display_none, display_flex
|
|
|
|
else:
|
|
return display_none, display_none, display_none, display_none
|
|
|
|
def _right_output_analysis(
|
|
self,
|
|
n_clicks,
|
|
all_img_options: dict,
|
|
current_img_value: str,
|
|
detector_value: str,
|
|
settings_text_analyse_text: list,
|
|
settings_text_model_names: str,
|
|
settings_text_revision_numbers: str,
|
|
setting_privacy_env_var: str,
|
|
setting_emotion_emotion_threshold: int,
|
|
setting_emotion_race_threshold: int,
|
|
setting_emotion_gender_threshold: int,
|
|
setting_emotion_env_var: str,
|
|
setting_color_delta_e_method: str,
|
|
setting_summary_analysis_type: str,
|
|
setting_summary_model: str,
|
|
setting_summary_list_of_questions: str,
|
|
) -> dict:
|
|
"""Callback function to perform analysis on the selected image and return the output.
|
|
|
|
Args:
|
|
all_options (dict): The available options in the file explorer dropdown.
|
|
current_value (str): The current selected value in the file explorer dropdown.
|
|
|
|
Returns:
|
|
dict: The analysis output for the selected image.
|
|
"""
|
|
identify_dict = {
|
|
"EmotionDetector": faces.EmotionDetector,
|
|
"TextDetector": text.TextDetector,
|
|
"SummaryDetector": summary.SummaryDetector,
|
|
"ColorDetector": colors.ColorDetector,
|
|
}
|
|
|
|
# Get image ID from dropdown value, which is the filepath
|
|
if current_img_value is None:
|
|
return {}
|
|
image_id = all_img_options[current_img_value]
|
|
# copy image so prvious runs don't leave their default values in the dict
|
|
image_copy = self.mydict[image_id].copy()
|
|
|
|
# detector value is the string name of the chosen detector
|
|
identify_function = identify_dict[detector_value]
|
|
|
|
if detector_value == "TextDetector":
|
|
analyse_text = (
|
|
True if settings_text_analyse_text == ["Analyse text"] else False
|
|
)
|
|
detector_class = identify_function(
|
|
image_copy,
|
|
analyse_text=analyse_text,
|
|
model_names=(
|
|
[settings_text_model_names]
|
|
if (settings_text_model_names is not None)
|
|
else None
|
|
),
|
|
revision_numbers=(
|
|
[settings_text_revision_numbers]
|
|
if (settings_text_revision_numbers is not None)
|
|
else None
|
|
),
|
|
accept_privacy=(
|
|
setting_privacy_env_var
|
|
if setting_privacy_env_var
|
|
else "PRIVACY_AMMICO"
|
|
),
|
|
)
|
|
elif detector_value == "EmotionDetector":
|
|
detector_class = identify_function(
|
|
image_copy,
|
|
emotion_threshold=setting_emotion_emotion_threshold,
|
|
race_threshold=setting_emotion_race_threshold,
|
|
gender_threshold=setting_emotion_gender_threshold,
|
|
accept_disclosure=(
|
|
setting_emotion_env_var
|
|
if setting_emotion_env_var
|
|
else "DISCLOSURE_AMMICO"
|
|
),
|
|
)
|
|
elif detector_value == "ColorDetector":
|
|
detector_class = identify_function(
|
|
image_copy,
|
|
delta_e_method=setting_color_delta_e_method,
|
|
)
|
|
elif detector_value == "SummaryDetector":
|
|
detector_class = identify_function(
|
|
image_copy,
|
|
analysis_type=setting_summary_analysis_type,
|
|
model_type=setting_summary_model,
|
|
list_of_questions=(
|
|
[setting_summary_list_of_questions]
|
|
if (setting_summary_list_of_questions is not None)
|
|
else None
|
|
),
|
|
)
|
|
else:
|
|
detector_class = identify_function(image_copy)
|
|
analysis_dict = detector_class.analyse_image()
|
|
|
|
# Initialize an empty dictionary
|
|
new_analysis_dict = {}
|
|
|
|
# Iterate over the items in the original dictionary
|
|
for k, v in analysis_dict.items():
|
|
# Check if the value is a list
|
|
if isinstance(v, list):
|
|
# If it is, convert each item in the list to a string and join them with a comma
|
|
new_value = ", ".join([str(f) for f in v])
|
|
else:
|
|
# If it's not a list, keep the value as it is
|
|
new_value = v
|
|
|
|
# Add the new key-value pair to the new dictionary
|
|
new_analysis_dict[k] = new_value
|
|
|
|
df = pd.DataFrame([new_analysis_dict]).set_index("filename").T
|
|
df.index.rename("filename", inplace=True)
|
|
return dbc.Table.from_dataframe(
|
|
df, striped=True, bordered=True, hover=True, index=True
|
|
)
|