"""External wound classification using a wound-specific Hugging Face classifier. """ from __future__ import annotations from PIL import Image import logging logger = logging.getLogger(__name__) # Lazy-loaded model singletons _processor = None _model = None def _get_model(): """Load the ViT model and processor (lazy singleton).""" global _processor, _model if _model is None: logger.info("Loading wound classification model...") try: from transformers import AutoImageProcessor, AutoModelForImageClassification from app.config import get_settings model_name = get_settings().wound_model_name _processor = AutoImageProcessor.from_pretrained(model_name) _model = AutoModelForImageClassification.from_pretrained(model_name) _model.eval() logger.info(f"Wound model loaded successfully: {model_name}") except Exception as e: logger.error(f"Failed to load wound model: {e}") raise return _processor, _model # dermaintel-wound-classifier classes → clean label + approximate ICD-10 code. # "normal_skin" is the model's negative class; the rest are wound types. _WOUND_LABELS: dict[str, tuple[str, str]] = { "pressure_ulcer": ("Pressure Ulcer", "L89.90"), "venous_ulcer": ("Venous Ulcer", "I83.009"), "arterial_ulcer": ("Arterial Ulcer", "I70.25"), "diabetic_ulcer": ("Diabetic Foot Ulcer", "E11.621"), "surgical_wound": ("Surgical Wound", "T81.89XA"), "traumatic_wound": ("Traumatic Wound", "T14.8"), "normal_skin": ("Normal Skin", ""), } # Secondary wound types below the top prediction are only worth showing if they # carry some real signal — a 7-way softmax baseline is ~14%. _SECONDARY_MIN_CONFIDENCE = 15.0 def predict_wound(image: Image.Image, confidence_threshold: float = 0.10) -> list[dict]: """Run wound classification inference. The underlying model is a 7-class softmax (6 wound types + normal_skin), so we report the top prediction directly rather than thresholding every class — a confident wound call on 7 classes can sit well below 40%. Args: image: PIL Image (RGB). confidence_threshold: retained for API compatibility; not used to gate the top prediction (softmax always has a most-likely class). Returns: List of findings with classification labels and confidence. """ import torch processor, model = _get_model() inputs = processor(images=image, return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) probs = torch.nn.functional.softmax(outputs.logits, dim=-1)[0] id2label = model.config.id2label ranked = sorted( ((float(probs[i]) * 100, id2label[int(i)]) for i in range(len(probs))), reverse=True, ) top_conf, top_label = ranked[0] # If the model is most confident the skin is normal, say so honestly — # using the model's real confidence, not a fabricated number. if top_label == "normal_skin": return [{ "name": "No significant wound detected", "confidence": round(top_conf, 1), "severity": "clear", "model": "WoundClassifier", "region": "External", "icd_code": "", "color": "success", }] # Otherwise report the detected wound type(s): always the top prediction, # plus any other wound classes carrying meaningful signal. findings: list[dict] = [] for conf, label in ranked: if label == "normal_skin": continue if findings and conf < _SECONDARY_MIN_CONFIDENCE: break name, icd = _WOUND_LABELS.get(label, (label.replace("_", " ").title(), "T14.8")) if conf >= 70: severity, color = "high", "destructive" elif conf >= 40: severity, color = "moderate", "warning" else: severity, color = "low", "info" findings.append({ "name": name, "confidence": round(conf, 1), "severity": severity, "model": "WoundClassifier", "region": "External", "icd_code": icd, "color": color, }) if len(findings) >= 5: break return findings