radiologist-inference 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- radiologist_inference-0.1.0/LICENSE +21 -0
- radiologist_inference-0.1.0/PKG-INFO +326 -0
- radiologist_inference-0.1.0/README.md +287 -0
- radiologist_inference-0.1.0/pyproject.toml +76 -0
- radiologist_inference-0.1.0/pyproject.toml.orig +69 -0
- radiologist_inference-0.1.0/src/radiologist/inference/__init__.py +61 -0
- radiologist_inference-0.1.0/src/radiologist/inference/app.py +222 -0
- radiologist_inference-0.1.0/src/radiologist/inference/base_predictor.py +361 -0
- radiologist_inference-0.1.0/src/radiologist/inference/cam.py +122 -0
- radiologist_inference-0.1.0/src/radiologist/inference/classifier.py +101 -0
- radiologist_inference-0.1.0/src/radiologist/inference/cli.py +485 -0
- radiologist_inference-0.1.0/src/radiologist/inference/explainer.py +84 -0
- radiologist_inference-0.1.0/src/radiologist/inference/mc_dropout.py +103 -0
- radiologist_inference-0.1.0/src/radiologist/inference/metrics.py +412 -0
- radiologist_inference-0.1.0/src/radiologist/inference/models.py +64 -0
- radiologist_inference-0.1.0/src/radiologist/inference/optional.py +48 -0
- radiologist_inference-0.1.0/src/radiologist/inference/verbs.py +172 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 @CedrickArmel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: radiologist-inference
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ONNX inference and serving for the radiologist pipeline
|
|
5
|
+
Keywords: chest-xray,medical-imaging,machine-learning,pytorch
|
|
6
|
+
Author: Cédrick-Armel YEBOUET
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Dist: numpy>=2.2.0
|
|
14
|
+
Requires-Dist: pillow>=12.2.0
|
|
15
|
+
Requires-Dist: onnxruntime>=1.18.0
|
|
16
|
+
Requires-Dist: radiologist-registry>=0.1.0
|
|
17
|
+
Requires-Dist: wandb>=0.27.0 ; extra == 'all'
|
|
18
|
+
Requires-Dist: fastapi>=0.100.0 ; extra == 'all'
|
|
19
|
+
Requires-Dist: uvicorn>=0.23.0 ; extra == 'all'
|
|
20
|
+
Requires-Dist: python-multipart>=0.0.9 ; extra == 'all'
|
|
21
|
+
Requires-Dist: typer>=0.9.0 ; extra == 'all'
|
|
22
|
+
Requires-Dist: prometheus-client>=0.20.0 ; extra == 'all'
|
|
23
|
+
Requires-Dist: typer>=0.9.0 ; extra == 'cli'
|
|
24
|
+
Requires-Dist: wandb>=0.27.0 ; extra == 'registry'
|
|
25
|
+
Requires-Dist: fastapi>=0.100.0 ; extra == 'serve'
|
|
26
|
+
Requires-Dist: uvicorn>=0.23.0 ; extra == 'serve'
|
|
27
|
+
Requires-Dist: python-multipart>=0.0.9 ; extra == 'serve'
|
|
28
|
+
Requires-Dist: prometheus-client>=0.20.0 ; extra == 'serve'
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Project-URL: Homepage, https://github.com/CedrickArmel/radiologist
|
|
31
|
+
Project-URL: Repository, https://github.com/CedrickArmel/radiologist
|
|
32
|
+
Project-URL: Documentation, https://cedrickarmel.github.io/radiologist/
|
|
33
|
+
Project-URL: Issues, https://github.com/CedrickArmel/radiologist/issues
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Provides-Extra: cli
|
|
36
|
+
Provides-Extra: registry
|
|
37
|
+
Provides-Extra: serve
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# radiologist-inference
|
|
41
|
+
|
|
42
|
+
[](https://github.com/CedrickArmel/radiologist/actions/workflows/ci.yml)
|
|
43
|
+
[](https://codecov.io/gh/CedrickArmel/radiologist)
|
|
44
|
+
[](https://pypi.org/project/radiologist-inference/)
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
ONNX inference and serving for the radiologist pipeline. Pulls trained models from the W&B Model Registry, runs deterministic classification, Score-CAM saliency, and MC-Dropout uncertainty estimation via ONNX Runtime, and optionally exposes a FastAPI HTTP server and a Typer CLI.
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
### Hard dependencies (always installed)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install radiologist-inference
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Installs: `numpy`, `Pillow`, `onnxruntime`.
|
|
58
|
+
|
|
59
|
+
### Optional extras
|
|
60
|
+
|
|
61
|
+
| Extra | Installs | Enables |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `registry` | `wandb` | `BasePredictor.from_registry` |
|
|
64
|
+
| `serve` | `fastapi`, `uvicorn`, `python-multipart`, `prometheus-client` | `create_app`, HTTP server, `GET /metrics` |
|
|
65
|
+
| `cli` | `typer` | `radiologist` CLI entry point |
|
|
66
|
+
| `all` | all of the above | everything |
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install "radiologist-inference[all]"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
### Library
|
|
75
|
+
|
|
76
|
+
The predictor hierarchy is capability-based: `Classifier` adds deterministic
|
|
77
|
+
prediction, `Explainer` (a `Classifier`) adds Score-CAM explanation, and
|
|
78
|
+
`MCDropoutPredictor` adds MC-Dropout uncertainty estimation. Pick the class
|
|
79
|
+
matching the capabilities you need.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from radiologist.inference import Classifier, Explainer, MCDropoutPredictor
|
|
83
|
+
|
|
84
|
+
# Deterministic prediction only
|
|
85
|
+
classifier = Classifier.from_path(model_path="model.onnx")
|
|
86
|
+
result = classifier.predict("chest_xray.png")
|
|
87
|
+
print(result.predicted_class) # "NORMAL"
|
|
88
|
+
print(result.probabilities) # {"NORMAL": 0.93, "PNEUMONIA": 0.07}
|
|
89
|
+
|
|
90
|
+
# Score-CAM explanation (Explainer inherits predict from Classifier)
|
|
91
|
+
explainer = Explainer.from_path(model_path="model.onnx")
|
|
92
|
+
explanation = explainer.explain("chest_xray.png")
|
|
93
|
+
print(explanation.saliency_map.shape) # (H, W)
|
|
94
|
+
|
|
95
|
+
# MC-Dropout uncertainty (loaded from a stochastic MC-Dropout ONNX model)
|
|
96
|
+
mcd_predictor = MCDropoutPredictor.from_path(model_path="model_mcd.onnx")
|
|
97
|
+
uncertainty = mcd_predictor.predict_with_uncertainty("chest_xray.png", n_passes=30)
|
|
98
|
+
print(uncertainty.predictive_entropy)
|
|
99
|
+
print(uncertainty.std_per_class)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Download a model from the W&B Model Registry via `from_registry` (requires
|
|
103
|
+
the `registry` extra):
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
classifier = Classifier.from_registry(
|
|
107
|
+
artifact_path="entity/project/model-name:v1",
|
|
108
|
+
local_dir="./models",
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`from_path`/`from_registry` accept optional `mean`, `std`, and `input_shape`
|
|
113
|
+
kwargs. This is a **behavior-relevant** addition: existing callers that omit
|
|
114
|
+
them keep getting exactly today's `/255.0`-only preprocessing (fully
|
|
115
|
+
backward compatible). Passing both `mean` and `std` applies
|
|
116
|
+
`(arr / 255 - mean) / std` instead, letting inference match the model's
|
|
117
|
+
actual training-time normalization without a custom fork — e.g. this
|
|
118
|
+
project's `radiologist-core` training pipeline uses `Normalize(mean=[128],
|
|
119
|
+
std=[65])` after `[0, 1]`-scaling, so `mean=128.0, std=65.0` reproduces
|
|
120
|
+
train/serve-consistent preprocessing:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
classifier = Classifier.from_path(
|
|
124
|
+
model_path="model.onnx", mean=128.0, std=65.0,
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`mean` and `std` must be provided together — passing only one raises
|
|
129
|
+
`ValueError` eagerly at load time (`from_path`/`from_registry`/
|
|
130
|
+
`from_selector`), before any inference request and, for the registry-backed
|
|
131
|
+
loaders, before the ONNX artifact is pulled. `input_shape` (`[N, C, H, W]`)
|
|
132
|
+
is a fallback used only when the
|
|
133
|
+
ONNX file's embedded metadata has no `input_shape` key; if metadata has no
|
|
134
|
+
`input_shape` and none is passed, loading raises `ValueError`.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
classifier = Classifier.from_path(
|
|
138
|
+
model_path="model_without_shape_metadata.onnx", input_shape=[1, 3, 224, 224],
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### HTTP server (requires `serve` extra)
|
|
143
|
+
|
|
144
|
+
`create_app` dispatches routes based on `isinstance` checks against the
|
|
145
|
+
injected predictor: any `Classifier` gets `/predict`, an `Explainer` also
|
|
146
|
+
gets `/explain`, and an `MCDropoutPredictor` gets `/uncertainty`. Passing no
|
|
147
|
+
predictor wires every route, each guarded by a 503 until one is injected.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from radiologist.inference import Explainer, create_app
|
|
151
|
+
|
|
152
|
+
predictor = Explainer.from_path("model.onnx")
|
|
153
|
+
app = create_app(predictor=predictor)
|
|
154
|
+
# Pass app to uvicorn or any ASGI server
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
uvicorn mymodule:app --host 0.0.0.0 --port 8000
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Routes:
|
|
162
|
+
|
|
163
|
+
| Method | Path | Description |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `GET` | `/healthz` | Liveness check |
|
|
166
|
+
| `GET` | `/readyz` | Readiness check; 503 until a predictor is loaded |
|
|
167
|
+
| `POST` | `/predict` | Classify a chest X-ray image (multipart upload) |
|
|
168
|
+
| `POST` | `/explain` | Return Score-CAM saliency map |
|
|
169
|
+
| `POST` | `/uncertainty` | MC-Dropout uncertainty estimation |
|
|
170
|
+
| `GET` | `/metrics` | Prometheus exposition of the metric catalogue below |
|
|
171
|
+
|
|
172
|
+
### Metrics (`GET /metrics`, requires `serve` extra)
|
|
173
|
+
|
|
174
|
+
Instrumentation is always-on whenever `prometheus-client` is importable — no
|
|
175
|
+
CLI flag, no configuration. `GET /metrics` returns a
|
|
176
|
+
`text/plain; version=0.0.4; charset=utf-8` Prometheus exposition payload of
|
|
177
|
+
this application's own `CollectorRegistry`. When `prometheus-client` is not
|
|
178
|
+
installed, `GET /metrics` is not wired at all and the route 404s cleanly;
|
|
179
|
+
every `Metrics` recording method silently no-ops instead of raising, so
|
|
180
|
+
request handling is unaffected either way.
|
|
181
|
+
|
|
182
|
+
The registry is **per-application and per-process**: each call to
|
|
183
|
+
`create_app` builds a fresh `CollectorRegistry`, so only these ten families
|
|
184
|
+
are exposed — there are no `process_*` / `python_gc_*` collectors. The
|
|
185
|
+
deployment model is assumed **single-worker**;
|
|
186
|
+
`prometheus_client.multiprocess` (multi-process/multi-worker aggregation) is
|
|
187
|
+
not supported. Scrape traffic against `/metrics` itself is excluded from all
|
|
188
|
+
counters, histograms and gauges below — it is not counted, timed, or tracked
|
|
189
|
+
in flight.
|
|
190
|
+
|
|
191
|
+
| Metric | Type | Labels | Description |
|
|
192
|
+
|---|---|---|---|
|
|
193
|
+
| `inference_requests_total` | Counter | `route`, `status` | Total inference API requests |
|
|
194
|
+
| `inference_request_duration_seconds` | Histogram | `route` | Wall-clock request duration, in seconds |
|
|
195
|
+
| `inference_requests_in_progress` | Gauge | `route` | Requests currently being served |
|
|
196
|
+
| `inference_errors_total` | Counter | `route`, `error_type` | Request-level errors |
|
|
197
|
+
| `inference_input_image_size_bytes` | Histogram | — | Uploaded input image size, in bytes |
|
|
198
|
+
| `inference_input_image_width_pixels` | Histogram | — | Pre-resize input image width, in pixels |
|
|
199
|
+
| `inference_input_image_height_pixels` | Histogram | — | Pre-resize input image height, in pixels |
|
|
200
|
+
| `inference_predicted_class_total` | Counter | `class` | Predictions per predicted class |
|
|
201
|
+
| `inference_confidence` | Histogram | — | Maximum predicted class probability |
|
|
202
|
+
| `inference_predictive_entropy` | Histogram | — | Predictive entropy of the mean MC-Dropout prediction |
|
|
203
|
+
| `inference_uncertainty_std_max` | Histogram | — | Maximum per-class std across MC-Dropout passes |
|
|
204
|
+
|
|
205
|
+
`route` and `error_type` are closed, bounded-cardinality label sets — never
|
|
206
|
+
caller-controlled text:
|
|
207
|
+
|
|
208
|
+
- `route` is one of `/predict`, `/explain`, `/uncertainty`, `/healthz`,
|
|
209
|
+
`/readyz`, `/metrics`, or the fallback value `unmatched` for any other
|
|
210
|
+
path.
|
|
211
|
+
- `error_type` is one of `invalid_image`, `empty_file`, `no_model_loaded`,
|
|
212
|
+
`validation_error`.
|
|
213
|
+
|
|
214
|
+
Two scope reconciliations, recorded here so they are not re-litigated:
|
|
215
|
+
|
|
216
|
+
- `/explain` does not observe `inference_confidence` — `Explanation` carries
|
|
217
|
+
no probability vector to derive a confidence value from.
|
|
218
|
+
- `/uncertainty` does not increment `inference_predicted_class_total` —
|
|
219
|
+
`UncertaintyResult` has no `predicted_class` field.
|
|
220
|
+
|
|
221
|
+
### CLI (requires `cli` extra)
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Classify a chest X-ray
|
|
225
|
+
radiologist predict chest_xray.png --path model.onnx
|
|
226
|
+
|
|
227
|
+
# Score-CAM explanation
|
|
228
|
+
radiologist explain chest_xray.png --path model.onnx --out saliency.npy
|
|
229
|
+
|
|
230
|
+
# MC-Dropout uncertainty
|
|
231
|
+
radiologist uncertainty chest_xray.png --path model_mcd.onnx
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`predict`, `explain`, and `uncertainty` all accept optional `--mean`,
|
|
235
|
+
`--std`, and `--input-shape` flags, threaded straight to `from_path`.
|
|
236
|
+
`--input-shape` takes a comma-separated `N,C,H,W`, e.g. `1,3,224,224`.
|
|
237
|
+
Omitting all three keeps today's default `/255.0`-only preprocessing:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
radiologist predict chest_xray.png --path model.onnx \
|
|
241
|
+
--mean 128 --std 65 --input-shape 1,3,224,224
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# Serve — picks the verb to serve via --predict/--explain/--uncertainty
|
|
246
|
+
# (default: explain, preserving today's behavior)
|
|
247
|
+
radiologist serve --path model.onnx
|
|
248
|
+
radiologist serve --uncertainty --path model_mcd.onnx
|
|
249
|
+
radiologist serve --predict --run-id abc123
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Public API reference
|
|
253
|
+
|
|
254
|
+
### `BasePredictor`
|
|
255
|
+
|
|
256
|
+
Common loading surface shared by every predictor class.
|
|
257
|
+
|
|
258
|
+
| Method | Signature | Description |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `from_path` | `(model_path: str, mean: Optional[float] = None, std: Optional[float] = None, input_shape: Optional[List[int]] = None) -> BasePredictor` | Load from a local ONNX file |
|
|
261
|
+
| `from_registry` | `(artifact_path: str, local_dir: str, registry=None, mean: Optional[float] = None, std: Optional[float] = None, input_shape: Optional[List[int]] = None) -> BasePredictor` | Download from W&B Registry and load; requires `registry` extra |
|
|
262
|
+
|
|
263
|
+
### `Classifier(BasePredictor)`
|
|
264
|
+
|
|
265
|
+
| Method | Signature | Description |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| `predict` | `(image, deployment_prior=None) -> Prediction` | Deterministic inference; `image` accepts a file path, NumPy HWC uint8 array, or PIL Image |
|
|
268
|
+
|
|
269
|
+
### `Explainer(Classifier)`
|
|
270
|
+
|
|
271
|
+
| Method | Signature | Description |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `explain` | `(image) -> Explanation` | Score-CAM saliency map for the given image; `predict` is inherited from `Classifier` |
|
|
274
|
+
|
|
275
|
+
### `MCDropoutPredictor(BasePredictor)`
|
|
276
|
+
|
|
277
|
+
| Method | Signature | Description |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| `predict_with_uncertainty` | `(image, n_passes: int = 30) -> UncertaintyResult` | MC-Dropout stochastic inference |
|
|
280
|
+
|
|
281
|
+
### `score_cam`
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
score_cam(feature_maps: np.ndarray, logits: np.ndarray) -> np.ndarray
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Compute a Score-CAM saliency map from feature maps `(C, H, W)` and logits `(num_classes,)`. Returns a `(H, W)` array with values in `[0, 1]`.
|
|
288
|
+
|
|
289
|
+
### `mc_dropout_predict`
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
mc_dropout_predict(
|
|
293
|
+
session: ort.InferenceSession,
|
|
294
|
+
image: np.ndarray,
|
|
295
|
+
n_passes: int = 30,
|
|
296
|
+
) -> UncertaintyResult
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Run `n_passes` stochastic forward passes through an MC-Dropout ONNX model and aggregate uncertainty statistics.
|
|
300
|
+
|
|
301
|
+
### `create_app`
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
create_app(predictor: Optional[BasePredictor] = None) -> FastAPI
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Create and return the FastAPI application, wiring routes to the injected
|
|
308
|
+
predictor's capabilities (see [HTTP server](#http-server-requires-serve-extra)
|
|
309
|
+
above). Requires the `serve` extra.
|
|
310
|
+
|
|
311
|
+
### Result dataclasses
|
|
312
|
+
|
|
313
|
+
| Class | Fields |
|
|
314
|
+
|---|---|
|
|
315
|
+
| `Prediction` | `probabilities: Dict[str, float]`, `predicted_class: str` |
|
|
316
|
+
| `Explanation` | `saliency_map: np.ndarray`, `predicted_class: str` |
|
|
317
|
+
| `UncertaintyResult` | `mean_probabilities: Dict[str, float]`, `std_per_class: Dict[str, float]`, `predictive_entropy: float`, `n_passes: int` |
|
|
318
|
+
| `ModelMetadata` | `classes: List[str]`, `input_shape: List[int]`, `cam_target_layer: str`, `output_names: List[str]` |
|
|
319
|
+
|
|
320
|
+
## Development setup
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
pyenv activate radiologist
|
|
324
|
+
uv sync --active --extra all --all-groups
|
|
325
|
+
uv run --active pytest radiologist-inference/radiologist_inference_tests -q
|
|
326
|
+
```
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# radiologist-inference
|
|
2
|
+
|
|
3
|
+
[](https://github.com/CedrickArmel/radiologist/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/CedrickArmel/radiologist)
|
|
5
|
+
[](https://pypi.org/project/radiologist-inference/)
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
ONNX inference and serving for the radiologist pipeline. Pulls trained models from the W&B Model Registry, runs deterministic classification, Score-CAM saliency, and MC-Dropout uncertainty estimation via ONNX Runtime, and optionally exposes a FastAPI HTTP server and a Typer CLI.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
### Hard dependencies (always installed)
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install radiologist-inference
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Installs: `numpy`, `Pillow`, `onnxruntime`.
|
|
19
|
+
|
|
20
|
+
### Optional extras
|
|
21
|
+
|
|
22
|
+
| Extra | Installs | Enables |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `registry` | `wandb` | `BasePredictor.from_registry` |
|
|
25
|
+
| `serve` | `fastapi`, `uvicorn`, `python-multipart`, `prometheus-client` | `create_app`, HTTP server, `GET /metrics` |
|
|
26
|
+
| `cli` | `typer` | `radiologist` CLI entry point |
|
|
27
|
+
| `all` | all of the above | everything |
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "radiologist-inference[all]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
### Library
|
|
36
|
+
|
|
37
|
+
The predictor hierarchy is capability-based: `Classifier` adds deterministic
|
|
38
|
+
prediction, `Explainer` (a `Classifier`) adds Score-CAM explanation, and
|
|
39
|
+
`MCDropoutPredictor` adds MC-Dropout uncertainty estimation. Pick the class
|
|
40
|
+
matching the capabilities you need.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from radiologist.inference import Classifier, Explainer, MCDropoutPredictor
|
|
44
|
+
|
|
45
|
+
# Deterministic prediction only
|
|
46
|
+
classifier = Classifier.from_path(model_path="model.onnx")
|
|
47
|
+
result = classifier.predict("chest_xray.png")
|
|
48
|
+
print(result.predicted_class) # "NORMAL"
|
|
49
|
+
print(result.probabilities) # {"NORMAL": 0.93, "PNEUMONIA": 0.07}
|
|
50
|
+
|
|
51
|
+
# Score-CAM explanation (Explainer inherits predict from Classifier)
|
|
52
|
+
explainer = Explainer.from_path(model_path="model.onnx")
|
|
53
|
+
explanation = explainer.explain("chest_xray.png")
|
|
54
|
+
print(explanation.saliency_map.shape) # (H, W)
|
|
55
|
+
|
|
56
|
+
# MC-Dropout uncertainty (loaded from a stochastic MC-Dropout ONNX model)
|
|
57
|
+
mcd_predictor = MCDropoutPredictor.from_path(model_path="model_mcd.onnx")
|
|
58
|
+
uncertainty = mcd_predictor.predict_with_uncertainty("chest_xray.png", n_passes=30)
|
|
59
|
+
print(uncertainty.predictive_entropy)
|
|
60
|
+
print(uncertainty.std_per_class)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Download a model from the W&B Model Registry via `from_registry` (requires
|
|
64
|
+
the `registry` extra):
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
classifier = Classifier.from_registry(
|
|
68
|
+
artifact_path="entity/project/model-name:v1",
|
|
69
|
+
local_dir="./models",
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`from_path`/`from_registry` accept optional `mean`, `std`, and `input_shape`
|
|
74
|
+
kwargs. This is a **behavior-relevant** addition: existing callers that omit
|
|
75
|
+
them keep getting exactly today's `/255.0`-only preprocessing (fully
|
|
76
|
+
backward compatible). Passing both `mean` and `std` applies
|
|
77
|
+
`(arr / 255 - mean) / std` instead, letting inference match the model's
|
|
78
|
+
actual training-time normalization without a custom fork — e.g. this
|
|
79
|
+
project's `radiologist-core` training pipeline uses `Normalize(mean=[128],
|
|
80
|
+
std=[65])` after `[0, 1]`-scaling, so `mean=128.0, std=65.0` reproduces
|
|
81
|
+
train/serve-consistent preprocessing:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
classifier = Classifier.from_path(
|
|
85
|
+
model_path="model.onnx", mean=128.0, std=65.0,
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`mean` and `std` must be provided together — passing only one raises
|
|
90
|
+
`ValueError` eagerly at load time (`from_path`/`from_registry`/
|
|
91
|
+
`from_selector`), before any inference request and, for the registry-backed
|
|
92
|
+
loaders, before the ONNX artifact is pulled. `input_shape` (`[N, C, H, W]`)
|
|
93
|
+
is a fallback used only when the
|
|
94
|
+
ONNX file's embedded metadata has no `input_shape` key; if metadata has no
|
|
95
|
+
`input_shape` and none is passed, loading raises `ValueError`.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
classifier = Classifier.from_path(
|
|
99
|
+
model_path="model_without_shape_metadata.onnx", input_shape=[1, 3, 224, 224],
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### HTTP server (requires `serve` extra)
|
|
104
|
+
|
|
105
|
+
`create_app` dispatches routes based on `isinstance` checks against the
|
|
106
|
+
injected predictor: any `Classifier` gets `/predict`, an `Explainer` also
|
|
107
|
+
gets `/explain`, and an `MCDropoutPredictor` gets `/uncertainty`. Passing no
|
|
108
|
+
predictor wires every route, each guarded by a 503 until one is injected.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from radiologist.inference import Explainer, create_app
|
|
112
|
+
|
|
113
|
+
predictor = Explainer.from_path("model.onnx")
|
|
114
|
+
app = create_app(predictor=predictor)
|
|
115
|
+
# Pass app to uvicorn or any ASGI server
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uvicorn mymodule:app --host 0.0.0.0 --port 8000
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Routes:
|
|
123
|
+
|
|
124
|
+
| Method | Path | Description |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `GET` | `/healthz` | Liveness check |
|
|
127
|
+
| `GET` | `/readyz` | Readiness check; 503 until a predictor is loaded |
|
|
128
|
+
| `POST` | `/predict` | Classify a chest X-ray image (multipart upload) |
|
|
129
|
+
| `POST` | `/explain` | Return Score-CAM saliency map |
|
|
130
|
+
| `POST` | `/uncertainty` | MC-Dropout uncertainty estimation |
|
|
131
|
+
| `GET` | `/metrics` | Prometheus exposition of the metric catalogue below |
|
|
132
|
+
|
|
133
|
+
### Metrics (`GET /metrics`, requires `serve` extra)
|
|
134
|
+
|
|
135
|
+
Instrumentation is always-on whenever `prometheus-client` is importable — no
|
|
136
|
+
CLI flag, no configuration. `GET /metrics` returns a
|
|
137
|
+
`text/plain; version=0.0.4; charset=utf-8` Prometheus exposition payload of
|
|
138
|
+
this application's own `CollectorRegistry`. When `prometheus-client` is not
|
|
139
|
+
installed, `GET /metrics` is not wired at all and the route 404s cleanly;
|
|
140
|
+
every `Metrics` recording method silently no-ops instead of raising, so
|
|
141
|
+
request handling is unaffected either way.
|
|
142
|
+
|
|
143
|
+
The registry is **per-application and per-process**: each call to
|
|
144
|
+
`create_app` builds a fresh `CollectorRegistry`, so only these ten families
|
|
145
|
+
are exposed — there are no `process_*` / `python_gc_*` collectors. The
|
|
146
|
+
deployment model is assumed **single-worker**;
|
|
147
|
+
`prometheus_client.multiprocess` (multi-process/multi-worker aggregation) is
|
|
148
|
+
not supported. Scrape traffic against `/metrics` itself is excluded from all
|
|
149
|
+
counters, histograms and gauges below — it is not counted, timed, or tracked
|
|
150
|
+
in flight.
|
|
151
|
+
|
|
152
|
+
| Metric | Type | Labels | Description |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `inference_requests_total` | Counter | `route`, `status` | Total inference API requests |
|
|
155
|
+
| `inference_request_duration_seconds` | Histogram | `route` | Wall-clock request duration, in seconds |
|
|
156
|
+
| `inference_requests_in_progress` | Gauge | `route` | Requests currently being served |
|
|
157
|
+
| `inference_errors_total` | Counter | `route`, `error_type` | Request-level errors |
|
|
158
|
+
| `inference_input_image_size_bytes` | Histogram | — | Uploaded input image size, in bytes |
|
|
159
|
+
| `inference_input_image_width_pixels` | Histogram | — | Pre-resize input image width, in pixels |
|
|
160
|
+
| `inference_input_image_height_pixels` | Histogram | — | Pre-resize input image height, in pixels |
|
|
161
|
+
| `inference_predicted_class_total` | Counter | `class` | Predictions per predicted class |
|
|
162
|
+
| `inference_confidence` | Histogram | — | Maximum predicted class probability |
|
|
163
|
+
| `inference_predictive_entropy` | Histogram | — | Predictive entropy of the mean MC-Dropout prediction |
|
|
164
|
+
| `inference_uncertainty_std_max` | Histogram | — | Maximum per-class std across MC-Dropout passes |
|
|
165
|
+
|
|
166
|
+
`route` and `error_type` are closed, bounded-cardinality label sets — never
|
|
167
|
+
caller-controlled text:
|
|
168
|
+
|
|
169
|
+
- `route` is one of `/predict`, `/explain`, `/uncertainty`, `/healthz`,
|
|
170
|
+
`/readyz`, `/metrics`, or the fallback value `unmatched` for any other
|
|
171
|
+
path.
|
|
172
|
+
- `error_type` is one of `invalid_image`, `empty_file`, `no_model_loaded`,
|
|
173
|
+
`validation_error`.
|
|
174
|
+
|
|
175
|
+
Two scope reconciliations, recorded here so they are not re-litigated:
|
|
176
|
+
|
|
177
|
+
- `/explain` does not observe `inference_confidence` — `Explanation` carries
|
|
178
|
+
no probability vector to derive a confidence value from.
|
|
179
|
+
- `/uncertainty` does not increment `inference_predicted_class_total` —
|
|
180
|
+
`UncertaintyResult` has no `predicted_class` field.
|
|
181
|
+
|
|
182
|
+
### CLI (requires `cli` extra)
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Classify a chest X-ray
|
|
186
|
+
radiologist predict chest_xray.png --path model.onnx
|
|
187
|
+
|
|
188
|
+
# Score-CAM explanation
|
|
189
|
+
radiologist explain chest_xray.png --path model.onnx --out saliency.npy
|
|
190
|
+
|
|
191
|
+
# MC-Dropout uncertainty
|
|
192
|
+
radiologist uncertainty chest_xray.png --path model_mcd.onnx
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`predict`, `explain`, and `uncertainty` all accept optional `--mean`,
|
|
196
|
+
`--std`, and `--input-shape` flags, threaded straight to `from_path`.
|
|
197
|
+
`--input-shape` takes a comma-separated `N,C,H,W`, e.g. `1,3,224,224`.
|
|
198
|
+
Omitting all three keeps today's default `/255.0`-only preprocessing:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
radiologist predict chest_xray.png --path model.onnx \
|
|
202
|
+
--mean 128 --std 65 --input-shape 1,3,224,224
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Serve — picks the verb to serve via --predict/--explain/--uncertainty
|
|
207
|
+
# (default: explain, preserving today's behavior)
|
|
208
|
+
radiologist serve --path model.onnx
|
|
209
|
+
radiologist serve --uncertainty --path model_mcd.onnx
|
|
210
|
+
radiologist serve --predict --run-id abc123
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Public API reference
|
|
214
|
+
|
|
215
|
+
### `BasePredictor`
|
|
216
|
+
|
|
217
|
+
Common loading surface shared by every predictor class.
|
|
218
|
+
|
|
219
|
+
| Method | Signature | Description |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `from_path` | `(model_path: str, mean: Optional[float] = None, std: Optional[float] = None, input_shape: Optional[List[int]] = None) -> BasePredictor` | Load from a local ONNX file |
|
|
222
|
+
| `from_registry` | `(artifact_path: str, local_dir: str, registry=None, mean: Optional[float] = None, std: Optional[float] = None, input_shape: Optional[List[int]] = None) -> BasePredictor` | Download from W&B Registry and load; requires `registry` extra |
|
|
223
|
+
|
|
224
|
+
### `Classifier(BasePredictor)`
|
|
225
|
+
|
|
226
|
+
| Method | Signature | Description |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `predict` | `(image, deployment_prior=None) -> Prediction` | Deterministic inference; `image` accepts a file path, NumPy HWC uint8 array, or PIL Image |
|
|
229
|
+
|
|
230
|
+
### `Explainer(Classifier)`
|
|
231
|
+
|
|
232
|
+
| Method | Signature | Description |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `explain` | `(image) -> Explanation` | Score-CAM saliency map for the given image; `predict` is inherited from `Classifier` |
|
|
235
|
+
|
|
236
|
+
### `MCDropoutPredictor(BasePredictor)`
|
|
237
|
+
|
|
238
|
+
| Method | Signature | Description |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| `predict_with_uncertainty` | `(image, n_passes: int = 30) -> UncertaintyResult` | MC-Dropout stochastic inference |
|
|
241
|
+
|
|
242
|
+
### `score_cam`
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
score_cam(feature_maps: np.ndarray, logits: np.ndarray) -> np.ndarray
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Compute a Score-CAM saliency map from feature maps `(C, H, W)` and logits `(num_classes,)`. Returns a `(H, W)` array with values in `[0, 1]`.
|
|
249
|
+
|
|
250
|
+
### `mc_dropout_predict`
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
mc_dropout_predict(
|
|
254
|
+
session: ort.InferenceSession,
|
|
255
|
+
image: np.ndarray,
|
|
256
|
+
n_passes: int = 30,
|
|
257
|
+
) -> UncertaintyResult
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Run `n_passes` stochastic forward passes through an MC-Dropout ONNX model and aggregate uncertainty statistics.
|
|
261
|
+
|
|
262
|
+
### `create_app`
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
create_app(predictor: Optional[BasePredictor] = None) -> FastAPI
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Create and return the FastAPI application, wiring routes to the injected
|
|
269
|
+
predictor's capabilities (see [HTTP server](#http-server-requires-serve-extra)
|
|
270
|
+
above). Requires the `serve` extra.
|
|
271
|
+
|
|
272
|
+
### Result dataclasses
|
|
273
|
+
|
|
274
|
+
| Class | Fields |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `Prediction` | `probabilities: Dict[str, float]`, `predicted_class: str` |
|
|
277
|
+
| `Explanation` | `saliency_map: np.ndarray`, `predicted_class: str` |
|
|
278
|
+
| `UncertaintyResult` | `mean_probabilities: Dict[str, float]`, `std_per_class: Dict[str, float]`, `predictive_entropy: float`, `n_passes: int` |
|
|
279
|
+
| `ModelMetadata` | `classes: List[str]`, `input_shape: List[int]`, `cam_target_layer: str`, `output_names: List[str]` |
|
|
280
|
+
|
|
281
|
+
## Development setup
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
pyenv activate radiologist
|
|
285
|
+
uv sync --active --extra all --all-groups
|
|
286
|
+
uv run --active pytest radiologist-inference/radiologist_inference_tests -q
|
|
287
|
+
```
|