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 = """
◌ Carica l'immagine di un pelo al microscopio: il modello identificherà la specie dall'aspetto del pelo.
◌ Seleziona cuticola o medulla per attivare il modello corrispondente.
Miglior predizione
' f'' f'{level}: {prediction_text} ' f'({confidence*100:.1f}%)' f'
' 'Probabilità classificazione
' 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('Probabilità classificazione
') 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()