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.
@@ -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
+ [![ci](https://github.com/CedrickArmel/radiologist/actions/workflows/ci.yml/badge.svg)](https://github.com/CedrickArmel/radiologist/actions/workflows/ci.yml)
43
+ [![codecov](https://codecov.io/gh/CedrickArmel/radiologist/branch/main/graph/badge.svg)](https://codecov.io/gh/CedrickArmel/radiologist)
44
+ [![PyPI](https://img.shields.io/pypi/v/radiologist-inference)](https://pypi.org/project/radiologist-inference/)
45
+ ![tested on](https://img.shields.io/badge/tested%20on-ubuntu--latest%20%7C%20python%203.10-blue)
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
+ [![ci](https://github.com/CedrickArmel/radiologist/actions/workflows/ci.yml/badge.svg)](https://github.com/CedrickArmel/radiologist/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/CedrickArmel/radiologist/branch/main/graph/badge.svg)](https://codecov.io/gh/CedrickArmel/radiologist)
5
+ [![PyPI](https://img.shields.io/pypi/v/radiologist-inference)](https://pypi.org/project/radiologist-inference/)
6
+ ![tested on](https://img.shields.io/badge/tested%20on-ubuntu--latest%20%7C%20python%203.10-blue)
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
+ ```