|
Download README.md from vectorsense/organscan: direct link, hf CLI and curl.
- Browser
- Download file 18.2 kB
-
https://huggingface.co/vectorsense/organscan/resolve/main/README.md
- Command line
-
hf download hf://vectorsense/organscan/README.md
-
curl -L -o README.md https://huggingface.co/vectorsense/organscan/resolve/main/README.md
18.2 kB
| license: apache-2.0 | |
| tags: | |
| - image-classification | |
| - multi-label-image-classification | |
| - medical-imaging | |
| - medical | |
| - anatomy | |
| - ultrasound | |
| - computed-tomography | |
| - radiology | |
| - dicom | |
| - onnx | |
| - onnxruntime | |
| - quantized | |
| - int8 | |
| - edge | |
| pipeline_tag: image-classification | |
| library_name: onnx | |
| base_model: timm/mobilenetv4_conv_small.e2400_r224_in1k | |
| metrics: | |
| - accuracy | |
| - f1 | |
| # OrganScan β anatomy classification from DICOM frames | |
| Given a frame rendered from DICOM pixel data β ultrasound cine, CT, MR or X-ray β OrganScan returns | |
| the **modality**, the **body region**, and **every organ visible in the frame**. It ships as a | |
| CPU-only **INT8 ONNX** artifact of 2.9 MB that runs fully offline, on a phone-class ARM core. | |
| It is a **hierarchical, multi-label** classifier, not a flat organ classifier. One shared backbone, | |
| three sigmoid heads. | |
| | | | | |
| |---|---| | |
| | **Backbone** | `mobilenetv4_conv_small.e2400_r224_in1k` (ImageNet-init, fine-tuned) | | |
| | **Heads** | modality (5) Β· region (8) Β· organ (30), independent sigmoids | | |
| | **Input** | `pixel_values` float32 `[batch, 3, 224, 224]`, NCHW, ImageNet-normalized | | |
| | **Outputs** | `modality_logits`, `region_logits`, `organ_logits` β **logits, not probabilities** | | |
| | **Artifact** | INT8 ONNX, 2.9 MB, CPU-only, no network at inference | | |
| | **Latency** | ~26.878 ms / frame on CPU via `onnxruntime` | | |
| | **Batch** | dynamic; H/W fixed at 224 so ORT/NNAPI kernels stay optimised | | |
| | **Modalities** | US Β· CT Β· MR Β· XR | | |
| | **Taxonomy** | `1.0.0` β a head width change is a version bump, never a silent edit | | |
| **What it is for:** routing and triage of imaging frames β *which study is this, and what is in | |
| view* β so a downstream pipeline can pick the right model, the right window, or the right reader. | |
| **What it is not:** a diagnostic device. It reports anatomy, never pathology, and holds no | |
| regulatory clearance of any kind. | |
| ## Why multi-label and hierarchical | |
| An RUQ ultrasound view shows liver *and* right kidney *and* diaphragm. A softmax is normalised, so | |
| raising `liver` mathematically forces `kidney` down β it would train the model to suppress something | |
| visibly present. Independent sigmoids let `liver=0.91, kidney_right=0.86` coexist. | |
| The three levels exist because labels arrive at different granularities. A DICOM series with only | |
| `Modality` supervises one level; a TotalSegmentator slice supervises all three. A per-class masked | |
| loss lets every frame contribute to whichever levels β and whichever *classes* β it actually knows | |
| about. A flat organ classifier would have to discard most of the corpus. | |
| ## Results β test split, policy `balanced` | |
| | Level | Classes scored | macro-AP | | |
| |---|---:|---:| | |
| | modality | 4 | **1.000** | | |
| | region | 8 | **0.980** | | |
| | organ | 30 | **0.917** | | |
| ### Per organ | |
| | Organ | AP | Precision | Recall | F1 | Support | | |
| |---|---:|---:|---:|---:|---:| | |
| | `BREAST` | 1.000 | 1.000 | 1.000 | **1.000** | 155 | | |
| | `UTERUS` | 1.000 | 0.978 | 1.000 | **0.989** | 132 | | |
| | `LUNG_RIGHT` | 0.992 | 0.959 | 0.986 | **0.972** | 2,148 | | |
| | `LUNG_LEFT` | 0.992 | 0.953 | 0.987 | **0.969** | 2,154 | | |
| | `AORTA_ABDOMINAL` | 0.989 | 0.894 | 0.989 | **0.939** | 1,784 | | |
| | `BRAIN` | 0.986 | 0.831 | 0.997 | **0.906** | 773 | | |
| | `AORTA_THORACIC` | 0.984 | 0.852 | 0.997 | **0.919** | 766 | | |
| | `LIVER` | 0.983 | 0.921 | 0.956 | **0.938** | 2,806 | | |
| | `COLON` | 0.983 | 0.901 | 0.961 | **0.930** | 2,092 | | |
| | `FEMUR_LEFT` | 0.978 | 0.915 | 0.949 | **0.932** | 926 | | |
| | `FEMUR_RIGHT` | 0.976 | 0.924 | 0.953 | **0.938** | 894 | | |
| | `HEART` | 0.976 | 0.925 | 0.927 | **0.926** | 1,178 | | |
| | `KIDNEY_RIGHT` | 0.970 | 0.804 | 0.964 | **0.877** | 1,369 | | |
| | `KIDNEY_LEFT` | 0.969 | 0.823 | 0.966 | **0.889** | 1,404 | | |
| | `OVARY` | 0.967 | 0.675 | 0.986 | **0.801** | 141 | | |
| | `SMALL_BOWEL` | 0.962 | 0.816 | 0.971 | **0.887** | 1,515 | | |
| | `TRACHEA` | 0.962 | 0.792 | 0.957 | **0.867** | 740 | | |
| | `INFERIOR_VENA_CAVA` | 0.959 | 0.857 | 0.966 | **0.908** | 2,249 | | |
| | `SPLEEN` | 0.956 | 0.864 | 0.909 | **0.886** | 1,631 | | |
| | `PANCREAS` | 0.950 | 0.776 | 0.966 | **0.861** | 1,349 | | |
| | `STOMACH` | 0.932 | 0.807 | 0.910 | **0.855** | 1,173 | | |
| | `BLADDER` | 0.931 | 0.830 | 0.882 | **0.856** | 748 | | |
| | `DUODENUM` | 0.929 | 0.625 | 0.979 | **0.763** | 875 | | |
| | `PORTAL_VEIN` | 0.821 | 0.522 | 0.961 | **0.676** | 309 | | |
| | `THYROID` | 0.811 | 0.552 | 0.896 | **0.683** | 212 | | |
| | `ESOPHAGUS` | 0.801 | 0.555 | 0.963 | **0.704** | 1,103 | | |
| | `PROSTATE` | 0.790 | 0.520 | 0.878 | **0.653** | 74 | | |
| | `GALLBLADDER` | 0.689 | 0.431 | 0.940 | **0.591** | 446 | | |
| | `ADRENAL_LEFT` | 0.643 | 0.320 | 0.919 | **0.475** | 174 | | |
| | `ADRENAL_RIGHT` | 0.633 | 0.358 | 0.905 | **0.513** | 147 | | |
| ### The numbers that matter more than the headline | |
| | Slice | Rows | Organ macro-AP | | |
| |---|---:|---:| | |
| | in_distribution | 15,395 | 0.917 | | |
| **Loop level** (1,413 loops): organ macro-AP **0.945**. This is the number a user experiences for ultrasound. | |
| **Laterality:** the model picks the wrong side on 0.7% of frames where it identifies | |
| the organ correctly. That error class is reported separately on purpose β `kidney_left -> | |
| kidney_right` is a minor error, `thyroid -> bladder` is not, and averaging them hides the expensive | |
| one. | |
| ## Class tiers β what is actually supported | |
| Not every class is equal, and this card says so rather than advertising one number. | |
| | Tier | Count | Classes | | |
| |---|---:|---| | |
| | Tier | Count | Classes | | |
| |---|---:|---| | |
| | **model_trained** | 0 | β | | |
| | **weak_label_only** | 0 | β | | |
| | **experimental** | 0 | β | | |
| | **blocked** | 0 | β | | |
| `GET /metadata` on the service reports the tier for every class, so a caller can tell a validated | |
| class from an experimental one at runtime. | |
| ## Quick start (Python + onnxruntime) | |
| ```bash | |
| pip install onnxruntime pillow numpy huggingface_hub | |
| ``` | |
| ```python | |
| import json, numpy as np, onnxruntime as ort | |
| from PIL import Image | |
| from huggingface_hub import hf_hub_download | |
| REPO = "vectorsense/organscan" | |
| sess = ort.InferenceSession(hf_hub_download(REPO, "model.int8.onnx"), | |
| providers=["CPUExecutionProvider"]) | |
| tax = json.load(open(hf_hub_download(REPO, "anatomy-tiers.json"), encoding="utf-8")) | |
| pre = json.load(open(hf_hub_download(REPO, "preprocessor.json"), encoding="utf-8")) | |
| ORGANS = tax["levels"]["organ"]["index"] | |
| REGIONS = tax["levels"]["region"]["index"] | |
| MODS = tax["levels"]["modality"]["index"] | |
| PARENT = [REGIONS.index(tax["parents"]["organ_to_region"][o]) for o in ORGANS] | |
| mean = np.array(pre["image_mean"], np.float32)[:, None, None] | |
| std = np.array(pre["image_std"], np.float32)[:, None, None] | |
| def pixel_values(path, size=224): | |
| image = Image.open(path).convert("RGB") | |
| scale = size / min(image.size) | |
| image = image.resize((round(image.width * scale), round(image.height * scale)), Image.BILINEAR) | |
| left, top = (image.width - size) // 2, (image.height - size) // 2 | |
| crop = np.asarray(image.crop((left, top, left + size, top + size)), np.float32) / 255.0 | |
| return ((crop.transpose(2, 0, 1) - mean) / std)[None] | |
| def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) | |
| m, r, o = sess.run(["modality_logits", "region_logits", "organ_logits"], | |
| {"pixel_values": pixel_values("frame.png")}) | |
| p_mod, p_reg, p_org = sigmoid(m[0]), sigmoid(r[0]), sigmoid(o[0]) | |
| # Chain down the hierarchy so a child can never outrank its parent. | |
| p_org = p_org * p_reg[PARENT] | |
| # Per-class and per-modality thresholds, not a flat cut. Ultrasound labels one organ per frame | |
| # and masks the rest, so the organ head carries a CT co-occurrence prior into US frames; the | |
| # per-modality gate is what holds it back. A flat 0.5 over-reports badly on ultrasound. | |
| book = json.load(open(hf_hub_download(REPO, "anatomy-thresholds.json"), encoding="utf-8")) | |
| policy = "balanced" | |
| modality = MODS[p_mod.argmax()] | |
| default = book["policies"][policy]["organ"] | |
| per_class = book.get("per_class_overrides", {}).get(policy, {}) | |
| per_modality = book.get("per_modality_overrides", {}).get(policy, {}).get(modality, {}) | |
| def threshold(label): | |
| return per_modality.get(label, per_class.get(label, default)) | |
| print("modality:", modality, round(float(p_mod.max()), 3)) | |
| print("region: ", REGIONS[p_reg.argmax()], round(float(p_reg.max()), 3)) | |
| for i in np.argsort(-p_org)[:5]: | |
| if p_org[i] >= threshold(ORGANS[i]): | |
| print(f" {ORGANS[i]:16s} {p_org[i]:.3f}") | |
| ``` | |
| **Apply the chaining.** Without it the organ head can report `bladder` on a chest X-ray. It costs one | |
| multiply. | |
| ## Run it as a REST API | |
| The companion repo ships a FastAPI service with the policy presets, hierarchy chaining and | |
| loop-level aggregation already wired up. | |
| ```bash | |
| pip install fastapi "uvicorn[standard]" onnxruntime pillow numpy | |
| uvicorn service.app:app --port 5002 # auto-loads model.int8.onnx from ./models | |
| ``` | |
| ```bash | |
| curl -s http://127.0.0.1:5002/classify_anatomy \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"frames":[{"id":"loop-1/f000","image_base64":"iVBORw0KGgo..."}], | |
| "options":{"policy":"balanced","aggregate_by":"loop"}}' | |
| ``` | |
| The same call from Python, which is what most callers actually want β send a PNG or JPEG, get the | |
| parsed JSON back: | |
| ```python | |
| import base64, json, requests | |
| def classify(path, url="http://127.0.0.1:5002/classify_anatomy", policy="balanced"): | |
| payload = { | |
| "frames": [{"id": "loop-1/f000", | |
| "image_base64": base64.b64encode(open(path, "rb").read()).decode()}], | |
| "options": {"policy": policy, "top_k": 5}, | |
| } | |
| response = requests.post(url, json=payload, timeout=30) | |
| response.raise_for_status() # unknown request fields return 422, not a silent default | |
| return response.json() | |
| result = classify("frame.png") | |
| frame = result["results"][0] | |
| print(frame["modality"]["label"], frame["region"]["label"]) | |
| for organ in frame["organs"]: | |
| print(f" {organ['label']:16s} {organ['score']:.3f}") | |
| print("model", result["model_version"], "taxonomy", result["taxonomy_version"]) | |
| ``` | |
| A whole cine loop in one request, which is how ultrasound should be served: | |
| ```python | |
| payload = { | |
| "frames": [{"id": f"loop-1/f{i:03d}", | |
| "image_base64": base64.b64encode(open(p, "rb").read()).decode()} | |
| for i, p in enumerate(frame_paths)], # 8-16 frames at temporal stride | |
| "options": {"policy": "balanced", "aggregate_by": "loop"}, | |
| } | |
| print(requests.post(url, json=payload, timeout=60).json()["aggregate"]) | |
| ``` | |
| Endpoints: `GET /`, `POST /classify_anatomy`, `GET /health/ready`, `GET /metadata`. | |
| Prefer this over the raw-ONNX snippet above unless you have a reason not to: the service already | |
| applies the calibrated per-class and per-modality thresholds, the hierarchy chaining and the | |
| abstention rules. The raw snippet uses a flat 0.5 cut, which on ultrasound over-reports badly. | |
| ## Loop-level aggregation | |
| For ultrasound this is the single most valuable thing in the serving path. Sample 8β16 frames at | |
| temporal stride across the cine, send them as one request with `aggregate_by: "loop"`, and the | |
| service returns a per-loop verdict using the **mean of the top-half frame scores**. Probe-in-air and | |
| transition frames drag a plain mean down; the top-half mean is worth several points for one batched | |
| forward pass. | |
| ## Policy presets and abstention | |
| `strict` / `balanced` / `precision` set per-level thresholds; override per request with | |
| `options.score_threshold`. Published results use `balanced`. | |
| `indeterminate` is a first-class outcome. `region=abdomen, organs=[]` β "abdominal ultrasound, organ | |
| indeterminate" β is a legitimate and actionable answer. A model that confidently says "kidney" on a | |
| probe-in-air frame is worse than useless. | |
| Scores are **not calibrated probabilities**. 0.90 does not mean 90% correct. See | |
| `anatomy-thresholds.json`. | |
| ## Training record | |
| Full provenance lives in this repo under `training/` β best checkpoint, configs, corpus manifest, | |
| source licences, corpus validation report. Trained on 82,354 frames from 7,483 | |
| patients. | |
| ## Limitations | |
| - **Not a diagnostic device.** It identifies what is in the picture. It says nothing about whether | |
| what is in the picture is normal. | |
| - **Ultrasound organ coverage is thin.** Only one licence-clear public abdominal ultrasound organ | |
| dataset exists (563 patients, one site, one vendor family). Public data can carry the modality and | |
| region heads; a production ultrasound organ head needs internal DICOM. | |
| - **Cardiac echo is effectively absent.** EchoNet forbids derivative works and CAMUS publishes no | |
| licence, so the `cardiac` region is trained from CT only. | |
| - **Vendor and site shift is the dominant failure mode**, more than anatomy. Validate on your own | |
| scanners before relying on this. | |
| - **Burned-in annotation.** If you feed uncropped ultrasound frames whose vendor UI prints the organ | |
| name, you are measuring OCR, not anatomy. Crop to `SequenceOfUltrasoundRegions` (0018,6011). | |
| - **No fetal organ labels.** `obstetric` is a region with no organ children by design. | |
| ## Licence | |
| **Model weights and code: `apache-2.0`.** Every contributing source is CC BY 4.0, which requires attribution only, so the weights carry no copyleft or field-of-use restriction. | |
| The weights are a derivative of their training data, so the training-data licences reach through to | |
| this artifact. The table below lists every source that contributed at least one frame, and | |
| `ATTRIBUTION.md` in this repo carries the full notice. | |
| | Source | Creators | Licence | Frames used | | |
| |---|---|---|---:| | |
| | [TotalSegmentator CT dataset v3.0.0](https://zenodo.org/records/22688904) | Jakob Wasserthal et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 61,902 | | |
| | [AMOS22: A Large-Scale Abdominal Multi-Organ Benchmark for Versatile Medical Image Segmentation](https://zenodo.org/records/7262581) | Yuanfeng Ji et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 11,480 | | |
| | [FETAL_PLANES_DB: Common maternal-fetal ultrasound images](https://zenodo.org/records/3904280) | Xavier P. Burgos-Artizzu et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 10,748 | | |
| | [MedMNIST v2 (ChestMNIST, 224px)](https://zenodo.org/records/10519652) | Jiancheng Yang et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 7,924 | | |
| | [MedMNIST v2 (OrganSMNIST, 224px)](https://zenodo.org/records/10519652) | Jiancheng Yang et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 4,000 | | |
| | [MedMNIST v2 (OrganAMNIST, 224px)](https://zenodo.org/records/10519652) | Jiancheng Yang et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 3,914 | | |
| | [MedMNIST v2 (OrganCMNIST, 224px)](https://zenodo.org/records/10519652) | Jiancheng Yang et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 3,902 | | |
| | [Abdominal Ultrasound Image Dataset for Organ Classification and Disease Detection](https://scholarsjunction.msstate.edu/research-data/5/) | Sifat Zina Karim | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 3,380 | | |
| | [MedMNIST v2 (BreastMNIST, 224px)](https://zenodo.org/records/10519652) | Jiancheng Yang et al. | [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) | 779 | | |
| **All source material was modified.** Frames were sliced from volumes or cine loops, windowed, | |
| resized to 224Γ224, capped per patient, and relabelled into the OrganScan taxonomy. Per-source detail | |
| is in `ATTRIBUTION.md` and `training/sources.json`. | |
| ### If you redistribute this model | |
| You must pass on the attribution. Copy `ATTRIBUTION.md` alongside the weights, keep this licence | |
| section intact, and state that you modified the model if you did. The simplest compliant thing is to | |
| link back to this repository. | |
| ### Attribution | |
| ``` | |
| OrganScan is trained on the following datasets, all of which were modified | |
| (sliced to 2D, windowed, resized to 224px, and relabelled): | |
| - TotalSegmentator CT dataset v3.0.0 | |
| Jakob Wasserthal, Hanns-Christian Breit, Manfred T. Meyer, Maurice Pradella, Daniel Hinck, Alexander W. Sauter, Tobias Heye, Daniel T. Boll, Joshy Cyriac, Shan Yang, Michael Bach, Martin Segeroth - CC-BY-4.0 | |
| https://zenodo.org/records/22688904 | |
| - AMOS22: A Large-Scale Abdominal Multi-Organ Benchmark for Versatile Medical Image Segmentation | |
| Yuanfeng Ji, Haotian Bai, Chongjian Ge, Jie Yang, Ye Zhu, Ruimao Zhang, Zhen Li, Lingyan Zhang, Wanling Ma, Xiang Wan, Ping Luo - CC-BY-4.0 | |
| https://zenodo.org/records/7262581 | |
| - FETAL_PLANES_DB: Common maternal-fetal ultrasound images | |
| Xavier P. Burgos-Artizzu, David Coronado-Gutierrez, Brenda Valenzuela-Alcaraz, Elisenda Bonet-Carne, Elisenda Eixarch, Fatima Crispi, Eduard GratacΓ³s - CC-BY-4.0 | |
| https://zenodo.org/records/3904280 | |
| - MedMNIST v2 (ChestMNIST, 224px) | |
| Jiancheng Yang, Rui Shi, Donglai Wei, Zequan Liu, Lin Zhao, Bilian Ke, Hanspeter Pfister, Bingbing Ni - CC-BY-4.0 | |
| https://zenodo.org/records/10519652 | |
| - MedMNIST v2 (OrganSMNIST, 224px) | |
| Jiancheng Yang, Rui Shi, Donglai Wei, Zequan Liu, Lin Zhao, Bilian Ke, Hanspeter Pfister, Bingbing Ni - CC-BY-4.0 | |
| https://zenodo.org/records/10519652 | |
| - MedMNIST v2 (OrganAMNIST, 224px) | |
| Jiancheng Yang, Rui Shi, Donglai Wei, Zequan Liu, Lin Zhao, Bilian Ke, Hanspeter Pfister, Bingbing Ni - CC-BY-4.0 | |
| https://zenodo.org/records/10519652 | |
| - MedMNIST v2 (OrganCMNIST, 224px) | |
| Jiancheng Yang, Rui Shi, Donglai Wei, Zequan Liu, Lin Zhao, Bilian Ke, Hanspeter Pfister, Bingbing Ni - CC-BY-4.0 | |
| https://zenodo.org/records/10519652 | |
| - Abdominal Ultrasound Image Dataset for Organ Classification and Disease Detection | |
| Sifat Zina Karim - CC-BY-4.0 | |
| https://scholarsjunction.msstate.edu/research-data/5/ | |
| - MedMNIST v2 (BreastMNIST, 224px) | |
| Jiancheng Yang, Rui Shi, Donglai Wei, Zequan Liu, Lin Zhao, Bilian Ke, Hanspeter Pfister, Bingbing Ni - CC-BY-4.0 | |
| https://zenodo.org/records/10519652 | |
| ``` | |
| ## Author | |
| Arnab Pal β [LinkedIn](https://www.linkedin.com/in/pal-arnab) · [GitHub](https://github.com/palarnab) | |