Spaces:
Sleeping
Sleeping
| 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() | |