| import io |
| import base64 |
| import numpy as np |
| from pathlib import Path |
| from PIL import Image, ImageDraw, ImageFont |
| from .disease_knowledge import get_disease_info |
|
|
| MODEL_LABEL = "best.onnx (YOLOv12)" |
|
|
| CLASS_NAMES = [ |
| "Darier_s-Disease", "Epidermolysis-Bullosa-Pruriginosa", "Hailey-Hailey-Disease", |
| "Hemangiome", "Impetigo", "Leishmanios", "Lupus-Erythematosus-Chronicus-Discoides", |
| "Melanoma", "Molluscum-Contagiosum", "Porokeratosis", "Psoriasis", "Tinea-Corporis", |
| "Tungiasis", "acne", "basal-cell-carcinoma", "eczema", "lichen", "nevus", "normal skin", |
| ] |
|
|
| DISPLAY_NAMES = { |
| "Darier_s-Disease": "Darier's Disease", |
| "Epidermolysis-Bullosa-Pruriginosa": "Epidermolysis Bullosa Pruriginosa", |
| "Hailey-Hailey-Disease": "Hailey-Hailey Disease", |
| "Hemangiome": "Hemangioma", |
| "Impetigo": "Impetigo", |
| "Leishmanios": "Leishmaniasis", |
| "Lupus-Erythematosus-Chronicus-Discoides": "Discoid Lupus Erythematosus", |
| "Melanoma": "Melanoma", |
| "Molluscum-Contagiosum": "Molluscum Contagiosum", |
| "Porokeratosis": "Porokeratosis", |
| "Psoriasis": "Psoriasis", |
| "Tinea-Corporis": "Tinea Corporis (Ringworm)", |
| "Tungiasis": "Tungiasis", |
| "acne": "Acne", |
| "basal-cell-carcinoma": "Basal Cell Carcinoma", |
| "eczema": "Eczema", |
| "lichen": "Lichen Planus", |
| "nevus": "Nevus (Mole)", |
| "normal skin": "Normal Skin", |
| } |
|
|
| _ort_session = None |
| MODEL_PATH = Path(__file__).parent.parent / "best.onnx" |
|
|
| def get_ort_session(): |
| global _ort_session |
| if _ort_session is None: |
| try: |
| import onnxruntime as ort |
| providers = ["CPUExecutionProvider"] |
| _ort_session = ort.InferenceSession(str(MODEL_PATH), providers=providers) |
| except Exception as e: |
| raise Exception(f"Failed to load CV model: {str(e)}. Ensure best.onnx exists in the backend folder.") |
| return _ort_session |
|
|
| def preprocess_image(image_bytes: bytes, input_size: int = 640) -> np.ndarray: |
| img = Image.open(io.BytesIO(image_bytes)).convert("RGB") |
| w, h = img.size |
| scale = input_size / max(w, h) |
| new_w, new_h = int(w * scale), int(h * scale) |
| img = img.resize((new_w, new_h), Image.BILINEAR) |
|
|
| canvas = Image.new("RGB", (input_size, input_size), (114, 114, 114)) |
| pad_x = (input_size - new_w) // 2 |
| pad_y = (input_size - new_h) // 2 |
| canvas.paste(img, (pad_x, pad_y)) |
|
|
| arr = np.array(canvas, dtype=np.float32) / 255.0 |
| arr = arr.transpose(2, 0, 1) |
| arr = np.expand_dims(arr, 0) |
| return arr |
|
|
| def parse_yolo_output(output: np.ndarray) -> tuple[str, float]: |
| num_classes = len(CLASS_NAMES) |
| out = output[0] |
| |
| if out.ndim == 2 and out.shape[0] == (4 + num_classes): |
| class_scores = out[4:, :] |
| per_anchor_max = class_scores.max(axis=0) |
| best_anchor = int(np.argmax(per_anchor_max)) |
| anchor_scores = class_scores[:, best_anchor] |
| elif out.ndim == 2 and out.shape[1] == (4 + num_classes): |
| class_scores = out[:, 4:] |
| per_anchor_max = class_scores.max(axis=1) |
| best_anchor = int(np.argmax(per_anchor_max)) |
| anchor_scores = class_scores[best_anchor, :] |
| elif out.ndim == 1: |
| anchor_scores = out[:num_classes] |
| elif out.ndim == 2 and out.shape[1] == num_classes: |
| anchor_scores = out[0] |
| else: |
| flat = out.flatten() |
| anchor_scores = flat[:num_classes] if len(flat) >= num_classes else flat |
|
|
| exp_s = np.exp(anchor_scores - anchor_scores.max()) |
| probs = exp_s / exp_s.sum() |
|
|
| class_idx = int(np.argmax(probs)) |
| confidence = float(probs[class_idx]) |
| class_idx = min(class_idx, num_classes - 1) |
|
|
| raw_name = CLASS_NAMES[class_idx] |
| display_name = DISPLAY_NAMES.get(raw_name, raw_name) |
| return display_name, round(confidence, 4) |
|
|
| CRITICAL_DISEASES = {"Melanoma", "Basal Cell Carcinoma"} |
| SEVERE_DISEASES = {"Discoid Lupus Erythematosus", "Darier's Disease", "Epidermolysis Bullosa Pruriginosa", "Hailey-Hailey Disease"} |
|
|
| def determine_severity_str(confidence: float, disease_name: str) -> str: |
| if disease_name in CRITICAL_DISEASES: return "critical" |
| if disease_name in SEVERE_DISEASES: return "severe" if confidence >= 0.5 else "moderate" |
| if disease_name == "Normal Skin": return "mild" |
| if confidence >= 0.85: return "severe" |
| elif confidence >= 0.65: return "moderate" |
| else: return "mild" |
|
|
| _PALETTE = [ |
| (255, 80, 80), (80, 200, 80), (80, 120, 255), (255, 180, 0), |
| (200, 0, 200), (0, 210, 210), (255, 100, 0), (0, 150, 255), |
| (180, 255, 0), (255, 0, 120), (120, 0, 255), (0, 255, 150), |
| (255, 220, 50), (50, 255, 220), (200, 100, 50), (100, 200, 50), |
| (50, 100, 200), (200, 50, 100), (150, 150, 150), |
| ] |
|
|
| def analyze_skin_image_core(image_bytes: bytes): |
| session = get_ort_session() |
| input_name = session.get_inputs()[0].name |
| input_tensor = preprocess_image(image_bytes) |
| outputs = session.run(None, {input_name: input_tensor}) |
| |
| disease_name, confidence = parse_yolo_output(outputs[0]) |
| severity = determine_severity_str(confidence, disease_name) |
| disease_info = get_disease_info(disease_name) |
| |
| recommendations = [] |
| if "lifestyle" in disease_info.get("recommendations", {}): |
| recommendations.extend(disease_info["recommendations"]["lifestyle"]) |
| |
| suggested_medicines = [m["name"] for m in disease_info.get("medicines", [])] |
| precautions = disease_info.get("recommendations", {}).get("precautions", []) |
| |
| return { |
| "condition": disease_name, |
| "confidence": confidence, |
| "severity": severity, |
| "description": disease_info["description"], |
| "recommendations": recommendations, |
| "suggestedMedicines": suggested_medicines, |
| "precautions": precautions, |
| "doctorConsultationUrgent": severity in ["severe", "critical"], |
| "boundingBox": None |
| } |
|
|