File size: 3,502 Bytes
5c22e5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
"""
inference.py — ONNX Runtime wrapper for the Random Forest model.

Expected model: final_model_Random_Forest.onnx
  Input  : float_input  [None, 120]  float32  (raw features — no scaling needed)
  Outputs: label        [None]       int64
           probabilities [None, 3]   float32
"""

from pathlib import Path
from threading import Lock

import numpy as np
import onnxruntime as rt

MODEL_PATH = Path(__file__).parent / "model" / "final_model_Random_Forest.onnx"

CLASS_NAMES = {
    0: "Common / Benign Nevi",
    1: "Atypical / Other Benign",
    2: "Melanoma (Suspected)",
}

CLASS_RISK = {
    0: "healthy",
    1: "watch",
    2: "danger",
}

CLASS_WHAT = {
    0: "A common benign mole. Melanocytic nevi are very common — most adults have 10–40. Almost always completely harmless.",
    1: "This category includes atypical or other benign lesions such as seborrhoeic keratosis, actinic keratosis, dermatofibroma, or vascular lesions. While many are harmless, some may need treatment.",
    2: "Melanoma is the most serious type of skin cancer. It develops from pigment-producing cells. Early detection is critical — when caught early, treatment is highly effective.",
}

CLASS_ACTION = {
    0: "No action needed. Monitor for changes in shape, colour, size, or bleeding.",
    1: "Recommended: book a consultation with a dermatologist for professional evaluation.",
    2: "Please see a dermatologist or doctor as soon as possible. Do not delay.",
}

_session = None
_session_lock = Lock()

def load_model() -> rt.InferenceSession:
    """Load ONNX model (cached after first call)."""
    global _session
    if _session is None:
        with _session_lock:
            if _session is None:
                if not MODEL_PATH.exists():
                    raise FileNotFoundError(
                        f"ONNX model not found at {MODEL_PATH}. "
                        "Please copy final_model_Random_Forest.onnx into backend/models/"
                    )
                opts = rt.SessionOptions()
                opts.intra_op_num_threads = 4
                _session = rt.InferenceSession(str(MODEL_PATH), sess_options=opts)
    return _session


def predict(features: np.ndarray) -> dict:
    """
    Run ONNX inference on a (120,) or (1, 120) feature vector.

    Returns:
        {
            "label":         int,          # 0, 1, or 2
            "class_name":    str,
            "risk":          str,          # healthy / watch / danger
            "probabilities": [p0, p1, p2], # float list, sums to 1
            "confidence":    float,        # max probability
            "what":          str,          # plain-language explanation
            "action":        str,          # recommended next step
        }
    """
    sess = load_model()
    if features.ndim == 1:
        features = features.reshape(1, -1)
    features = features.astype(np.float32)

    label_arr, prob_arr = sess.run(
        ["label", "probabilities"],
        {"float_input": features}
    )
    label = int(label_arr[0])
    probs = prob_arr[0].tolist()
    if label not in CLASS_NAMES:
        raise ValueError(f"Model returned unexpected label {label}; probabilities={probs}")

    return {
        "label":         label,
        "class_name":    CLASS_NAMES[label],
        "risk":          CLASS_RISK[label],
        "probabilities": probs,
        "confidence":    float(max(probs)),
        "what":          CLASS_WHAT[label],
        "action":        CLASS_ACTION[label],
    }