PAIRS / app.py
andrewzamp's picture
Update app.py
a9bf735 verified
Raw
History Blame Contribute Delete
16.9 kB
import os
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
import numpy as np
import pandas as pd
import gradio as gr
import tensorflow as tf
from PIL import Image
CUTICLE_IMG_PATH = "assets/cuticle_demo.jpg"
MEDULLA_IMG_PATH = "assets/medulla_demo.jpg"
try:
DEFAULT_IMAGE_CUTICLE = Image.open(CUTICLE_IMG_PATH)
except Exception:
DEFAULT_IMAGE_CUTICLE = None
try:
DEFAULT_IMAGE_MEDULLA = Image.open(MEDULLA_IMG_PATH)
except Exception:
DEFAULT_IMAGE_MEDULLA = None
CUTICLE_MODEL_PATH = 'models/rodent_cuticle_classifier_sps.h5'
MEDULLA_MODEL_PATH = 'models/rodent_medulla_classifier_sps.h5'
TAXO_MAP_CSV_PATH = 'taxonomy/taxo_map.csv'
IMG_SIZE = (224, 224)
UNKNOWN_LABEL = "Sconosciuto"
CONFIDENCE_THRESHOLD = 0.5
# Runtime caches
CUTICLE_MODEL = None
MEDULLA_MODEL = None
TAXO_MAP = None
CUTICLE_SPECIES_NAMES = None
MEDULLA_SPECIES_NAMES = None
# Force dark mode
FORCE_DARK_JS = """
() => {
const url = new URL(window.location.href);
if (url.searchParams.get("__theme") !== "dark") {
url.searchParams.set("__theme", "dark");
window.location.replace(url.toString());
}
}
"""
# ── Custom CSS ───────────────────────────────────────────────────────────────
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Lora:ital,wght@0,400;0,600;1,400&display=swap');
:root {
--bg: #121611; --bg2: #1a2018; --surface: #232a21; --border: #3b4735;
--accent: #9fc47b; --accent2: #d4b57a; --text: #e7ebdf; --muted: #aab3a1;
--danger: #d37f7f; --radius: 8px;
--mono: 'IBM Plex Mono', monospace; --serif: 'Lora', Georgia, serif;
}
body, .gradio-container {
background: var(--bg) !important; color: var(--text) !important;
font-family: var(--mono) !important;
}
.app-header { border-bottom: 1px solid var(--border); padding: 28px 0 22px; margin-bottom: 8px; }
.app-header h1 { font-family: var(--serif) !important; font-size: 1.9rem !important; font-weight: 600 !important; color: var(--accent) !important; letter-spacing: 0.01em !important; margin: 0 0 6px !important; }
.app-header p { font-family: var(--mono) !important; font-size: 0.9rem !important; color: var(--muted) !important; margin: 0 !important; line-height: 1.65 !important; }
.app-header .tag { display: inline-block; background: var(--surface); border: 1px solid var(--border); border-radius: 3px; padding: 1px 7px; font-size: 0.7rem; color: var(--accent2); margin-right: 6px; letter-spacing: 0.05em; text-transform: uppercase; }
.gradio-container .block, .gradio-container fieldset, .gradio-container .form { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; }
label span, .gradio-container label { font-family: var(--mono) !important; font-size: 0.78rem !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; color: var(--muted) !important; }
.gradio-container input[type="radio"] + span { color: var(--text) !important; font-size: 0.9rem !important; text-transform: none !important; letter-spacing: 0 !important; }
.gradio-container input[type="radio"]:checked + span { color: var(--accent) !important; }
.gradio-container input[type="range"] { accent-color: var(--accent) !important; }
.gradio-container .number-component input { background: var(--bg2) !important; border-color: var(--border) !important; color: var(--text) !important; }
.gradio-container button.primary, .gradio-container button[variant="primary"] { background: var(--accent) !important; border: none !important; border-radius: var(--radius) !important; color: #0d1009 !important; font-family: var(--mono) !important; font-size: 0.8rem !important; font-weight: 500 !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; padding: 10px 24px !important; transition: background 0.18s ease, transform 0.1s ease, box-shadow 0.18s ease !important; box-shadow: 0 0 0 0 transparent !important; }
.gradio-container button.primary:hover, .gradio-container button[variant="primary"]:hover { background: #a0c480 !important; transform: translateY(-1px) !important; box-shadow: 0 4px 18px rgba(138,171,110,0.25) !important; }
.gradio-container button.primary:active { transform: translateY(0) !important; }
.gradio-container button:not(.primary):not([variant="primary"]) { background: var(--bg2) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; color: var(--text) !important; font-family: var(--mono) !important; font-size: 0.75rem !important; transition: border-color 0.15s ease, color 0.15s ease !important; }
.gradio-container button:not(.primary):not([variant="primary"]):hover { border-color: var(--accent) !important; color: var(--accent) !important; }
.gradio-container .image-container, .gradio-container .upload-container { background: var(--bg2) !important; border: 1.5px dashed var(--border) !important; border-radius: var(--radius) !important; transition: border-color 0.2s ease !important; }
.gradio-container .image-container:hover, .gradio-container .upload-container:hover { border-color: var(--accent) !important; }
.gradio-container table { font-family: var(--mono) !important; font-size: 0.79rem !important; border-collapse: collapse !important; width: 100% !important; }
.gradio-container table thead th { background: var(--bg) !important; color: #c4ccbb !important; font-size: 0.74rem !important; letter-spacing: 0.1em !important; text-transform: uppercase !important; border-bottom: 1px solid var(--border) !important; padding: 8px 14px !important; }
.gradio-container table tbody tr:first-child td { color: var(--accent) !important; font-weight: 500 !important; }
.gradio-container table tbody tr { transition: background 0.12s ease !important; }
.gradio-container table tbody tr:hover { background: rgba(138,171,110,0.06) !important; }
.gradio-container table tbody td { border-bottom: 1px solid var(--border) !important; padding: 7px 14px !important; color: var(--text) !important; }
.prediction-box {
background: var(--bg2) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
padding: 12px 14px !important;
margin-top: 8px !important;
}
.prediction-box .section-label {
color: var(--accent2) !important;
margin-bottom: 6px !important;
}
.prediction-box p,
.prediction-box b,
.prediction-box span {
color: var(--text) !important;
}
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
.section-label { font-size: 0.74rem !important; letter-spacing: 0.12em !important; text-transform: uppercase !important; color: #b8c1ad !important; border-left: 2px solid var(--accent) !important; padding-left: 8px !important; margin-bottom: 4px !important; }
.taxon-badge { display: inline-block; border-radius: 3px; padding: 1px 8px; font-size: 0.68rem; letter-spacing: 0.07em; text-transform: uppercase; font-family: var(--mono); margin-left: 8px; }
.taxon-badge.species { background: rgba(138,171,110,0.15); border: 1px solid var(--accent); color: var(--accent); }
.taxon-badge.genus { background: rgba(201,169,110,0.15); border: 1px solid var(--accent2); color: var(--accent2); }
.taxon-badge.family { background: rgba(201,169,110,0.15); border: 1px solid var(--accent2); color: var(--accent2); }
"""
HEADER_HTML = """
<div class="app-header">
<h1>🐾 Classificatore peli di roditori</h1>
<p>
β—Œ Carica l'immagine di un pelo al microscopio: il modello identificherΓ  la specie dall'aspetto del pelo.<br>
β—Œ Seleziona <b style="color:#8aab6e">cuticola</b> o <b style="color:#c9a96e">medulla</b> per attivare il modello corrispondente.<br>
</p>
</div>
"""
# ── Model helpers ────────────────────────────────────────────────────────────
def load_taxo_map(taxo_map_csv_path):
"""Load taxonomy map CSV."""
if not os.path.isfile(taxo_map_csv_path):
raise FileNotFoundError(f"File tassonomico non trovato: {taxo_map_csv_path}")
return pd.read_csv(taxo_map_csv_path)
def preprocess_image(image):
"""Preprocess image for model input."""
image = image.convert("RGB").resize(IMG_SIZE, Image.Resampling.LANCZOS)
arr = np.asarray(image, dtype=np.float32)
return tf.keras.applications.resnet.preprocess_input(arr)
def predict_on_image(image, model):
"""Run model prediction on full image. Returns probabilities from model."""
processed = preprocess_image(image)
batch = np.expand_dims(processed, axis=0)
probs = model.predict(batch, verbose=0)
return probs[0]
def aggregate_to_higher_taxon(taxo_map, taxon_col, lower_taxon_col, confidences_dict):
"""
Aggregate confidences to a higher taxonomic level.
For each unique value in taxon_col, sum confidences of all lower_taxon values that map to it.
"""
aggregated = {}
for idx, row in taxo_map.iterrows():
higher = str(row[taxon_col]).strip()
lower = str(row[lower_taxon_col]).strip()
if lower in confidences_dict:
if higher not in aggregated:
aggregated[higher] = 0.0
aggregated[higher] += confidences_dict[lower]
return aggregated
def resolve_prediction_with_fallback(
species_probs, taxo_map, threshold, species_class_names
):
"""
Resolve prediction: if species confidence < threshold, aggregate to genus.
If genus confidence < threshold, aggregate to family.
If family confidence < threshold, return Unknown.
Returns: (prediction_text, confidence, level)
"""
# Species level
species_dict = {species_class_names[i]: species_probs[i] for i in range(len(species_class_names))}
top_species = max(species_dict.items(), key=lambda x: x[1])
if top_species[1] >= threshold:
return f"{top_species[0]}", top_species[1], "Specie"
# Aggregate to genus
genus_dict = aggregate_to_higher_taxon(taxo_map, "genus", "species", species_dict)
if genus_dict:
top_genus = max(genus_dict.items(), key=lambda x: x[1])
if top_genus[1] >= threshold:
return f"{top_genus[0]}", top_genus[1], "Genere"
# Aggregate to family
family_dict = aggregate_to_higher_taxon(taxo_map, "family", "species", species_dict)
if family_dict:
top_family = max(family_dict.items(), key=lambda x: x[1])
if top_family[1] >= threshold:
return f"{top_family[0]}", top_family[1], "Famiglia"
# Unknown
return UNKNOWN_LABEL, 0.0, "Sconosciuto"
def get_species_class_names(model, taxo_map):
"""Extract species names from taxo_map in order matching model output."""
num_classes = model.output_shape[-1]
species_names = sorted({
n for n in taxo_map["species"].dropna().astype(str).str.strip() if n
})
if len(species_names) != num_classes:
raise ValueError(
f"Mismatch: model output {num_classes} classes, but {len(species_names)} species in taxo_map"
)
return species_names
def ensure_runtime_assets_loaded():
"""Ensure models and taxonomy map are loaded."""
global CUTICLE_MODEL, MEDULLA_MODEL, TAXO_MAP, CUTICLE_SPECIES_NAMES, MEDULLA_SPECIES_NAMES
if CUTICLE_MODEL is None:
CUTICLE_MODEL = tf.keras.models.load_model(CUTICLE_MODEL_PATH)
if MEDULLA_MODEL is None:
MEDULLA_MODEL = tf.keras.models.load_model(MEDULLA_MODEL_PATH)
if TAXO_MAP is None:
TAXO_MAP = load_taxo_map(TAXO_MAP_CSV_PATH)
if CUTICLE_SPECIES_NAMES is None:
CUTICLE_SPECIES_NAMES = get_species_class_names(CUTICLE_MODEL, TAXO_MAP)
if MEDULLA_SPECIES_NAMES is None:
MEDULLA_SPECIES_NAMES = get_species_class_names(MEDULLA_MODEL, TAXO_MAP)
def top_prediction_html(prediction_text, confidence, level):
"""Generate HTML for top prediction."""
return (
'<div class="prediction-box">'
'<p class="section-label">Miglior predizione</p>'
f'<p style="margin:0;color:var(--text) !important;">'
f'<b style="color:var(--accent) !important;">{level}: {prediction_text}</b> '
f'<span style="color:var(--accent2) !important;">({confidence*100:.1f}%)</span>'
f'</p>'
'</div>'
)
def switch_model_type(model_type):
"""Switch image based on model type selection."""
if model_type == "Cuticola":
return gr.update(value=DEFAULT_IMAGE_CUTICLE)
else:
return gr.update(value=DEFAULT_IMAGE_MEDULLA)
# ── Prediction ───────────────────────────────────────────────────────────────
def run_prediction(image, model_type, confidence_threshold):
"""Run prediction on image with selected model."""
if image is None:
raise gr.Error("Carica prima un'immagine.")
ensure_runtime_assets_loaded()
# Select model and species names
if model_type == "Cuticola":
model = CUTICLE_MODEL
species_names = CUTICLE_SPECIES_NAMES
else:
model = MEDULLA_MODEL
species_names = MEDULLA_SPECIES_NAMES
# Predict (model output is already probabilities)
species_probs = predict_on_image(image, model)
# Resolve prediction with fallback
pred_text, pred_conf, pred_level = resolve_prediction_with_fallback(
species_probs, TAXO_MAP, float(confidence_threshold), species_names
)
# Top prediction HTML
top_html = top_prediction_html(pred_text, pred_conf, pred_level)
# Ranked table
ranked_species = sorted(
[(species_names[i], species_probs[i]) for i in range(len(species_names))],
key=lambda x: x[1],
reverse=True,
)
table_data = {
"Specie": [s[0] for s in ranked_species],
"ProbabilitΓ  (%)": [f"{s[1]*100:.1f}%" for s in ranked_species],
}
table_df = pd.DataFrame(table_data)
section_html = '<p class="section-label">ProbabilitΓ  classificazione</p>'
return (
gr.update(visible=True),
gr.update(value=section_html),
gr.update(value=top_html),
table_df,
)
# ── Load models at startup ───────────────────────────────────────────────────
ensure_runtime_assets_loaded()
# ── UI ───────────────────────────────────────────────────────────────────────
with gr.Blocks(
title="Classificatore impronte di roditori",
css=CUSTOM_CSS,
js=FORCE_DARK_JS,
) as demo:
gr.HTML(HEADER_HTML)
with gr.Row():
image_input = gr.Image(
type="pil",
label="Immagine di input",
height=420,
value=DEFAULT_IMAGE_CUTICLE,
)
with gr.Column():
model_type_input = gr.Radio(
["Cuticola", "Medulla"],
value="Cuticola",
label="Tipo di immagine",
)
confidence_threshold_input = gr.Slider(
minimum=0.0,
maximum=1.0,
value=CONFIDENCE_THRESHOLD,
step=0.01,
label="Soglia di confidenza",
)
run_button = gr.Button("β–Ά Esegui predizione", variant="primary")
with gr.Column(visible=False) as probs_section:
probs_label = gr.HTML('<p class="section-label">ProbabilitΓ  classificazione</p>')
top_output = gr.HTML()
with gr.Accordion("Tutte le probabilitΓ ", open=False):
probs_output = gr.Dataframe(interactive=False, show_label=False)
# ── Event wiring ─────────────────────────────────────────────────────────
model_type_input.change(
fn=switch_model_type,
inputs=model_type_input,
outputs=image_input,
)
run_button.click(
fn=run_prediction,
inputs=[
image_input,
model_type_input,
confidence_threshold_input,
],
outputs=[probs_section, probs_label, top_output, probs_output],
)
if __name__ == "__main__":
demo.launch()