Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ChestAI — AI Chest X-Ray Diagnostic Platform

CI status tests p50 latency mean AUC

Live Demo · API Docs · Model Weights · Changelog

ThoraxNet is a production-grade, full-stack AI diagnostic platform that detects 14 thoracic pathologies from chest X-rays. It combines a fine-tuned BioMedCLIP vision-language foundation model with Monte Carlo Dropout uncertainty quantification, ViT-GradCAM explainability, and automated radiology report generation via Groq's LLaMA-3.3-70b.

For research use only. Not FDA cleared. Not a substitute for clinical radiologist interpretation.


Benchmark Results

Evaluated on the NIH ChestX-ray14 official validation split (224×224 resolution, per-class threshold tuning).

Pathology AUC Threshold
Cardiomegaly 0.888 0.74
Hernia 0.872 0.62
Edema 0.851 0.75
Effusion 0.834 0.66
Emphysema 0.823 0.61
Pneumothorax 0.793 0.66
Fibrosis 0.782 0.60
Mass 0.776 0.64
Nodule 0.754 0.58
Atelectasis 0.745 0.63
Consolidation 0.736 0.67
Pleural Thickening 0.728 0.61
Infiltration 0.704 0.58
Pneumonia 0.695 0.67
Mean 0.8215

Compared to the original NIH paper (Wang et al., 2017) mean AUC of 0.745, ThoraxNet achieves a +7.65% absolute improvement by leveraging BioMedCLIP's medical vision-language pretraining on 15 million biomedical image-text pairs.


Key Features

  • Multi-label classification — detects 14 pathologies simultaneously with per-class calibrated thresholds
  • Uncertainty quantification — Monte Carlo Dropout with 20 stochastic forward passes; flags high-uncertainty predictions for radiologist review
  • Explainability — ViT-GradCAM heatmaps overlaid on the original X-ray per detected pathology
  • Automated radiology reports — structured FINDINGS / IMPRESSION / RECOMMENDATION sections generated by Groq LLaMA-3.3-70b-versatile
  • Fairness analysis — per-demographic subgroup AUC evaluation across age and sex
  • Production API — async FastAPI with singleton model loading, request validation, GradCAM session store
  • PWA frontend — installable Next.js app with Google OAuth, scan history, analytics dashboard

Architecture

┌─────────────────────────────────────────────────────────┐
│                    Next.js Frontend (Vercel)             │
│  Login → Home Dashboard → Scan → Stats → Profile        │
└────────────────────────┬────────────────────────────────┘
                         │ HTTPS (multipart/form-data)
┌────────────────────────▼────────────────────────────────┐
│              FastAPI Backend (HuggingFace Spaces)        │
│                                                          │
│  POST /api/v1/predict                                    │
│  GET  /api/v1/gradcam/{session_id}/{class}               │
│  GET  /health                                            │
└────────────────────────┬────────────────────────────────┘
                         │
          ┌──────────────▼──────────────────┐
          │      InferencePipeline          │
          │                                 │
          │  1. Preprocess (224×224)        │
          │  2. BioMedCLIP ViT-B/16        │
          │  3. MC Dropout (20 passes)      │
          │  4. Per-class thresholds        │
          │  5. ViT-GradCAM                 │
          │  6. Groq report generation      │
          └─────────────────────────────────┘
                         │
          ┌──────────────▼──────────────────┐
          │    HuggingFace Hub              │
          │    Sowaiba01/chestai-model      │
          │    chestai_best.pt              │
          └─────────────────────────────────┘

Model Details

Component Detail
Backbone BioMedCLIP ViT-B/16 (Microsoft) — pretrained on 15M biomedical image-text pairs
Classification head LayerNorm → Dropout(0.3) → Linear(512→512) → GELU → Dropout(0.3) → Linear(512→14)
Uncertainty Monte Carlo Dropout — 20 stochastic passes at inference; entropy + per-class std
Explainability ViT-GradCAM — gradient-weighted attention map from final transformer block
Training data NIH ChestX-ray14 — 112,120 frontal-view X-rays, 30,805 patients
Input RGB 224×224, normalized to ImageNet mean/std
Loss Weighted binary cross-entropy with class imbalance correction
Optimizer AdamW, lr=1e-4, weight decay=1e-2
Report generation Groq API — llama-3.3-70b-versatile, T=0.2, max_tokens=512

Tech Stack

Backend

  • Python 3.11, FastAPI, Uvicorn
  • PyTorch 2.x, open_clip (BioMedCLIP), transformers
  • HuggingFace Hub (model weights), HuggingFace Spaces (deployment)
  • Groq Python SDK

Frontend

  • Next.js 14, TypeScript, Tailwind CSS
  • NextAuth.js (Google OAuth)
  • Framer Motion, Lucide React
  • Vercel (deployment)

ML / Data

  • NIH ChestX-ray14 dataset
  • Kaggle (training environment)
  • Monte Carlo Dropout, ViT-GradCAM

Project Structure

ThoraxNet/
├── api/                        # FastAPI backend
│   ├── main.py                 # App entrypoint, CORS, lifespan
│   ├── inference.py            # InferencePipeline singleton
│   ├── schemas.py              # Pydantic v2 request/response models
│   └── routes/
│       └── predict.py          # /api/v1/predict endpoint
├── models/
│   ├── backbone.py             # BioMedCLIP ViT-B/16 wrapper
│   ├── classifier.py           # ChestAIClassifier
│   └── uncertainty.py          # Monte Carlo Dropout inference
├── data/
│   ├── dataset.py              # NIH ChestX-ray14 dataset + CLASSES
│   └── transforms.py           # Train/val image transforms
├── training/
│   ├── trainer.py              # Training loop
│   ├── losses.py               # Weighted BCE
│   └── metrics.py              # Per-class AUC, threshold tuning
├── explainability/
│   └── gradcam.py              # ViT-GradCAM implementation
├── fairness/
│   └── evaluator.py            # Subgroup fairness metrics
├── report_generation/
│   └── generator.py            # Groq LLaMA radiology report
├── frontend/                   # Next.js PWA
│   └── src/
│       ├── pages/
│       │   ├── index.tsx       # Home / Dashboard
│       │   ├── login.tsx       # Google OAuth login
│       │   ├── scan.tsx        # X-ray upload + results
│       │   ├── stats.tsx       # Model analytics
│       │   ├── profile.tsx     # User settings
│       │   └── api/auth/       # NextAuth.js API routes
│       ├── components/
│       │   ├── Layout.tsx      # Shared header + bottom nav
│       │   ├── XRayUploader.tsx
│       │   ├── FindingsPanel.tsx
│       │   ├── ReportViewer.tsx
│       │   └── UncertaintyChart.tsx
│       └── lib/
│           └── api.ts          # Typed API client + scan history
├── Dockerfile                  # HuggingFace Spaces Docker config
├── requirements.txt
└── README.md

API Reference

Base URL: https://Sowaiba01-ThoraxNet.hf.space

POST /api/v1/predict

Analyze a chest X-ray image.

Request (multipart/form-data)

Field Type Required Description
file File PNG or JPEG, max 10 MB
patient_age float Patient age in years
patient_gender string "M" or "F"
generate_report bool Default true. Set false to skip the LLM narrative and return in ~85 ms.
generate_gradcam bool Default true. Set false to skip heatmap generation.

Response

{
  "findings": [
    {
      "name": "Effusion",
      "probability": 0.724,
      "uncertainty": 0.043,
      "present": true,
      "high_uncertainty": false
    }
  ],
  "report": "FINDINGS:\n...\nIMPRESSION:\n...\nRECOMMENDATION:\n...",
  "entropy": 0.312,
  "inference_time_ms": 3145.8,
  "stage_timings_ms": {
    "preprocess": 15.7,
    "mc_dropout": 2770.5,
    "gradcam": 5.4,
    "report": 42.4
  },
  "model_version": "1.1.0",
  "gradcam_available": true,
  "gradcam_classes": ["Effusion"],
  "gradcam_session_id": "9f2c1e40-5b3a-4d81-b7e6-2a4c8d1f0e33"
}

report is null when generate_report=false. stage_timings_ms gives the per-stage latency breakdown in milliseconds — use it to attribute latency regressions to a specific stage in production.

GET /api/v1/gradcam/{session_id}/{class_name}

Returns the GradCAM heatmap overlay as a PNG for a specific pathology. Use the gradcam_session_id returned by /predict; sessions are held in memory and evicted after 100 newer scans.

GET /health

Returns model load status and device info.


Local Development

Backend

git clone https://github.com/Sowaiba-01/ThoraxNet.git
cd chestai

pip install -r requirements.txt

# Set environment variables
export MODEL_HUB_REPO=Sowaiba01/chestai-model
export GROQ_API_KEY=your_groq_api_key

uvicorn api.main:app --host 0.0.0.0 --port 7860 --reload

API docs available at http://localhost:7860/docs

Frontend

cd frontend
npm install

# Create .env.local from template
cp .env.local.example .env.local
# Fill in your values

npm run dev

Deployment

Backend — HuggingFace Spaces (Docker)

The API is deployed as a Docker container on HuggingFace Spaces. Model weights are downloaded at startup from Sowaiba01/chestai-model via huggingface_hub.

Required Space secrets:

  • GROQ_API_KEY
  • MODEL_HUB_REPO (optional, defaults to Sowaiba01/chestai-model)

Frontend — Vercel

cd frontend
vercel --prod

Required environment variables in Vercel dashboard:

  • NEXTAUTH_SECRET
  • NEXTAUTH_URL
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • NEXT_PUBLIC_API_URL

Results & Discussion

ThoraxNet demonstrates that medical vision-language foundation models (BioMedCLIP) significantly outperform task-specific CNN architectures on chest X-ray multi-label classification when fine-tuned with appropriate regularization. Key observations:

  • Cardiomegaly achieves the highest AUC (0.888) due to its distinct visual morphology
  • Pneumonia is the hardest class (AUC 0.695), consistent with literature — its appearance overlaps heavily with Consolidation and Infiltration
  • Monte Carlo Dropout provides well-calibrated uncertainty estimates; predictions with std > 0.15 correlate strongly with ambiguous or borderline cases
  • Per-class threshold tuning on the validation set improves macro F1 by ~4% over using a uniform 0.5 threshold

Performance

Single-scan latency, measured client-side against the deployed Space with scripts/benchmark.py. Reproduce with:

python scripts/benchmark.py --image tests/fixtures/sample_cxr.png \
    --requests 200 --concurrency 1,4,16

Environment. HuggingFace Spaces free tier, CPU only (device: cpu, 2 vCPU). Latency is wall-clock from the client and therefore includes network round-trip to the Space. All figures below are from real runs; nothing here is estimated.

Before vs after (v1.0.0 → v1.1.0)

Identical hardware, identical image, identical protocol: 30 requests, 3 warmup requests discarded, 0 failures. The only variable is the code.

Metric v1.0.0 v1.1.0 Improvement
p50 latency 6,387 ms 3,327 ms 1.9× faster
p95 latency 7,525 ms 3,862 ms 1.9× faster
p99 latency 8,026 ms 4,768 ms 1.7× faster
Throughput 0.15 req/s 0.30 req/s 2.0×

Every percentile improved by ~2×, with no change to model weights or accuracy — this is purely a change in how the model is executed.

Where the win came from

Three changes to the request path, no change to the model:

Change Effect
Batched MC Dropout 20 sequential batch-1 forward passes → one batched forward pass. The dominant win.
Async report Groq call moved off the critical path onto a worker thread.
GradCAM cache Heatmaps were recomputed every call; now LRU-cached per (image, class).

v1.1.0 exposes a per-stage breakdown on every response (stage_timings_ms), measured in production — MC Dropout is now 2,868 ms of ~3,000 ms server-side, preprocess 16 ms, GradCAM 6 ms (cached), report 92 ms (async). (v1.0.0 had no per-stage instrumentation, so only its end-to-end total is comparable.)

The single biggest win: the 20 Monte Carlo Dropout passes were running sequentially at batch size 1. Tiling the input into one batched forward pass — while preserving the independent per-sample dropout masks the estimator requires — roughly halved end-to-end latency.

The gradcam figure is a cache-hit number: the benchmark reuses one image, so every request after the first hits the cache. Cold heatmaps cost more.


Engineering Notes

What broke and how I found it

GradCAM retrieval had never worked. api/routes/predict.py read pipeline.gradcam._last_overlays to populate its session store. That attribute did not exist — InferencePipeline.predict() built the overlays into a local variable and let them go out of scope. Every GET /api/v1/gradcam/{session_id}/{class} returned 404. Nothing logged an error, and the frontend rendered an empty panel rather than a failure state, so it shipped and stayed broken. Found while reading the request path end-to-end for the latency work, not from a bug report — which is the uncomfortable part. Fixed by having ViTGradCAM.generate_overlays() record overlays on the instance, and pinned with a regression test.

MC Dropout was 65% of request time and nobody had measured it. The implementation looked reasonable — a list comprehension over 20 forward passes. But at batch size 1 a T4 is almost entirely idle during each pass; the cost was kernel launch overhead and Python dispatch, repeated 20 times. Tiling the input along the batch dimension gives the same 20 independent dropout samples in one pass. The correctness argument matters here: dropout masks are sampled per batch element, so the tiled copies are independent samples, not correlated ones. test_batched_matches_sequential_in_distribution verifies this empirically at n=400 rather than taking it on faith.

std returned NaN at n_samples=1. torch.std applies Bessel's correction by default, so a single sample divides by zero. Only surfaced when writing the chunking tests — no production request uses T=1, but the failure mode would have been a NaN silently propagating into the uncertainty field and rendering as a blank badge in the UI.

Session-store eviction leaked memory. The eviction check was if len(store) > MAX rather than while, so a burst of concurrent requests could only ever evict one entry per request while adding one — the store grew monotonically under load.

A test was passing on a vacuous truth. The stub backbone used to avoid downloading BioMedCLIP in CI returned a hard-coded torch.zeros(2, 512), ignoring its input entirely. All-zero features stay zero through LayerNorm and zero-initialised Linear layers, so every MC Dropout sample was identical and assert std.mean() > 0 was asserting nothing about dropout. The stub also ignored the input batch size, so the shape assertions in the same file could never have held. Both tests were red before this work started — the batching rewrite surfaced them rather than caused them, because reshaping into (T, B, C) forces the batch dimension to actually be correct. A test that cannot fail is worse than no test: it reports safety it isn't providing.

An optional dependency was a hard import. report_generation/generator.py imported groq at module scope, which made the whole inference pipeline unimportable without it — even though the code already carried a template fallback for precisely the case where Groq isn't available. The fallback existed; the import made it unreachable.

What I tried that didn't work

torch.jit.script on the full model. The BioMedCLIP backbone contains constructs TorchScript's compiler rejects (dynamic attribute access in open_clip's attention implementation). Switched to torch.jit.trace, which is valid here only because inference has no data-dependent control flow — a worse tool in general, the right one for this specific graph.

Static (calibrated) INT8 quantization. Quantizing activations as well as weights meant quantizing the attention softmax path, and ViT accuracy is notably sensitive there. Early runs showed AUC regressions above 0.01 on the low-prevalence classes, which is not a trade I'm willing to make for a medical model. Fell back to dynamic quantization, which touches only Linear weights. The measured delta is published in the quantization table rather than asserted to be negligible.

Caching MC Dropout results by image hash. Tempting, and wrong: the whole point of MC Dropout is that repeated evaluation draws fresh samples. Caching the result would return a stale point estimate and quietly destroy the uncertainty semantics. Only GradCAM — which is deterministic given (image, class) — is safe to cache.

Raising max_chunk above 32. No measurable throughput gain past 32 samples per pass on a T4, and it pushed peak memory high enough to OOM when two requests overlapped. The lock serialises GPU work, but the allocator still has to hold both.

Known gaps

  • INT8 weights are exported but not yet serving traffic; the accuracy table has to be published before that promotion.
  • The GradCAM session store is process-local in-memory. It does not survive a restart and will not work behind more than one replica — Redis is the correct fix and is not done.
  • The benchmark measures client-side latency against a single Space instance. It is not a load test of a horizontally scaled deployment.

Citation

If you use ChestAI in your research, please cite:

@software{thoraxnet2026,
  author    = {Arshad, Sowaiba},
  title     = {ThoraxNet: AI Chest X-Ray Diagnostic Platform with Uncertainty Quantification},
  year      = {2026},
  url       = {https://github.com/Sowaiba-01/ThoraxNet},
}

Acknowledgements


License

MIT License — see LICENSE for details.


Built by Sowaiba Arshad

About

Production-grade medical AI framework using BioMedCLIP (ViT-B/16) for multi-label chest X-ray pathology detection. Features Monte Carlo Dropout for uncertainty quantification, per-class threshold calibration, ViT-GradCAM explainability, and LLaMA-3.3 reporting. FastAPI + Next.js.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages