File size: 3,441 Bytes
68bc1c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
101
102
103
104
105
106
107
108
109
110
111
112
from __future__ import annotations

import pickle
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import numpy as np

from app.ml.archive_model import clamp

FEATURE_ORDER: tuple[str, ...] = (
    "base_anemia_risk",
    "uncertainty",
    "predicted_hemoglobin",
    "predicted_hemoglobin_missing",
    "brightness_score",
    "contrast_score",
    "blur_score",
    "framing_score",
    "lighting_score",
    "glare_risk",
    "shadow_risk",
    "lighting_balanced",
    "lighting_overexposed",
    "lighting_glare_heavy",
    "lighting_shadow_heavy",
    "lighting_flat_contrast",
    "lighting_dim",
    "base_likely",
)


@dataclass
class RuntimeScreeningRefiner:
    version: str = "runtime-screening-refiner-v1"
    method: str = "logistic-regression"
    threshold: float = 0.53
    feature_order: tuple[str, ...] = FEATURE_ORDER
    model: Any = None
    report: dict[str, object] = field(default_factory=dict)

    def _feature_vector(
        self,
        *,
        base_anemia_risk: float,
        uncertainty: float,
        predicted_hemoglobin: float | None,
        quality,
        base_likely: bool,
    ) -> list[float]:
        hb_missing = predicted_hemoglobin is None
        hb_value = 13.5 if predicted_hemoglobin is None else float(predicted_hemoglobin)
        lighting = str(getattr(quality, "lighting_condition", "balanced"))
        return [
            float(base_anemia_risk),
            float(uncertainty),
            hb_value,
            float(hb_missing),
            float(getattr(quality, "brightness_score", 0.0)),
            float(getattr(quality, "contrast_score", 0.0)),
            float(getattr(quality, "blur_score", 0.0)),
            float(getattr(quality, "framing_score", 0.0)),
            float(getattr(quality, "lighting_score", 0.0)),
            float(getattr(quality, "glare_risk", 0.0)),
            float(getattr(quality, "shadow_risk", 0.0)),
            float(lighting == "balanced"),
            float(lighting == "overexposed"),
            float(lighting == "glare_heavy"),
            float(lighting == "shadow_heavy"),
            float(lighting == "flat_contrast"),
            float(lighting == "dim"),
            float(base_likely),
        ]

    def refine(
        self,
        *,
        base_anemia_risk: float,
        uncertainty: float,
        predicted_hemoglobin: float | None,
        quality,
        base_likely: bool,
    ) -> float:
        if self.model is None:
            return clamp(float(base_anemia_risk), 0.0, 1.0)
        vector = np.asarray(
            [
                self._feature_vector(
                    base_anemia_risk=base_anemia_risk,
                    uncertainty=uncertainty,
                    predicted_hemoglobin=predicted_hemoglobin,
                    quality=quality,
                    base_likely=base_likely,
                )
            ],
            dtype=np.float32,
        )
        probability = float(self.model.predict_proba(vector)[0, 1])
        return clamp(probability, 0.0, 1.0)

    def save(self, path: str | Path) -> None:
        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open("wb") as handle:
            pickle.dump(self, handle)

    @classmethod
    def load(cls, path: str | Path) -> "RuntimeScreeningRefiner":
        with Path(path).open("rb") as handle:
            return pickle.load(handle)