โ How it works ๐ Results ๐ผ Screenshots โป Reproduce the pipeline
01 Overview
Dental insurers want a written justification attached to a claim โ a short paragraph explaining why a crown, a root canal, or a bridge is medically necessary. Writing them is repetitive work: the same near-identical, copy-paste language, one claim after another.
The pipeline came together by connecting two projects I'd built independently. The first is an auto-narrative app that drafts that justification. The second is a dental computer-vision model trained on radiographs to do two things: identify and number the teeth โ the foundation everything else builds on โ and detect the pathology and restorations that public datasets can teach. Connecting the two makes an almost fully automatic tool: the vision model reads an attached radiograph, and the narrative app writes the justification from what it found. The piece that closes the gap to full accuracy is the treating dentist confirming the findings before anything is written; adding payer-specific claim details is the most recent component โ in place, but not yet fully tested.
The computer vision it runs on โ FDI tooth-numbering, per-tooth SAM masks, and the pathology stack (caries, PARL, and common restorations) โ is its own project: Dental Radiograph Computer Vision.
02 Why I built it
The first version of the narrative app took a shortcut that turned out to matter: it sent the radiograph straight to a vision LLM that both read the image and wrote the narrative in one pass. That single pass occasionally invented specifics โ millimetre measurements that were never taken, teeth mis-identified, diagnoses assumed โ and it produced patient-preference language that insurer-side AI screeners are known to discount as evidence of necessity.
Splitting perception from prose was the fix. In the shipped path the language model never sees the radiograph. Deterministic computer vision produces the findings; the clinician โ who already knows the diagnosis โ confirms them; and the LLM's only job is to document, not diagnose. That reframing is the whole point of the project, and it mirrors how a dentist actually works.
03 What I built & how it works
A deterministic vision pass, a clinician's confirmation, and a hard rule: the LLM composes only from validated findings, never from the image.
Fig. 1 โ a deterministic vision pass produces structured findings; the narrative app runs the clinician review, rules, safety gate, text-only composition, and export. The language model only ever sees the findings, never the radiograph.
- Detect โ the FDI detector numbers every tooth, SAM outlines each one, and the pathology stage flags caries, periapical lesions, and restorations. Every finding carries a confidence score.
- Review โ a human-in-the-loop modal lets the clinician anchor teeth, confirm or override rules-suggested procedures, and add prior-restoration metadata. Clinician knowledge overrides CV uncertainty.
- Ground โ the validated findings, not the image, are handed to a text-only LLM. Confidence tiers decide whether each finding is stated as fact, hedged as "evidence ofโฆ", or omitted.
- Compose & check โ the model writes per-procedure prose; deterministic regex validators strip patient-preference language, map lay terms to clinical ones, and check for payer-required necessity keywords.
- Document โ an audit-trail PDF records the CV findings, the clinician's inputs, and the final narrative, so the basis for every claim is traceable.
04 ๐ Skills & tech used
05 Notable challenges & decisions
Most of the interesting engineering was in the honesty: separating what the model may claim from what a human actually confirmed.
The LLM never reads the radiograph
Rather than fight hallucination with prompt wording, I removed the opportunity for it. In the shipped path the model receives a compact findings dictionary and a confidence tier per item; it composes prose and nothing else. Interpretation lives entirely in the deterministic CV stack and the clinician's review โ the LLM cannot invent a measurement it was never given.
A matrix that suppresses harmful disclosures
Patient context is double-edged: "smoker" strengthens a deep-cleaning claim but weakens a bridge claim. So every disclosure is gated through a condition-by-procedure effect matrix. Helpful pairings become narrated facts; harmful ones are silently withheld from the LLM and surfaced to the clinician as an on-screen warning. Toggling a condition says "I have this information"; the matrix decides whether it helps.
06 Results
Sources: routed evaluation on the held-out test set. The FDI F1 is a count-based micro-F1 measured on images the detector never trained on โ full CV benchmarking detail lives on the Dental Radiograph Computer Vision page.
07 Screenshots
Both captures are the runnable, de-identified clone working end-to-end on a public sample radiograph โ no patient data.
08 Honest status
The shipped Phase-2 pipeline was built and runs. It was designed for real insurance claims in my own practice setting, but I have no usage logs to prove clinical submissions, so I make no claim of production caseload. The v2 web app is currently dormant โ it was paused at a checkpoint in mid-2026. The payer-specific claim-detail layer is the newest piece and the least exercised: it's built, but not yet validated against real claim adjudications.
What reproduces the whole flow today is a runnable, PHI-safe, de-identified clone that collapses the system into two co-located containers. It runs end-to-end on a single bundled CC BY 4.0 radiograph โ live CV detection, rules-engine suggestions, the 12-payer checklist, the approval-likelihood meter, overlay rendering, and the audit-trail PDF โ with the LLM narrative prose opt-in behind a local model. The clone carries no patient images, no real secrets, and no protected health information; that is the artifact behind the screenshots above.
AI Radiograph โ Insurance-Narrative Pipeline โ Build Recipe
Take a bare machine to a running copy of an AI dental radiograph โ insurance narrative pipeline. Upload a radiograph and a deterministic computer-vision pass detects + FDI-numbers the teeth (YOLOv8m), segments them (SAM 2.1), and runs a pathology stack (modality classifier, apex regressor, lesion / restoration / caries detectors). Those structured findings drive a tooth chart, image overlays, a rules-engine procedure suggester, a payer-specific documentation checklist, an approval-likelihood meter, and an audit-trail PDF โ and, optionally, an LLM writes the final insurance narrative from the findings (it never sees the radiograph on the grounded path).
The system runs as two co-located containers on one compose network โ a CV service and a narrative service. CPU-only โ no GPU required.
Status: โ Verified 2026-08-01 โ built from this clean clone with
docker compose up --build. Both images build (cv โ 6.2 GB incl. ~1.1 GB of bundled weights + torch/SAM2; narrative โ 250 MB), both come up healthy, and the full pipeline ran end-to-end, key-free on a bundled CC BY 4.0 bitewing: the real detector found 10 FDI-numbered teeth with SAM masks and caries pathology (e.g. tooth #30 @ 0.33), the rules engine suggested โ Core Buildup, the approval meter scored the claim at 58 % with a breakdown, the overlays rendered on the radiograph (seedemo.png), and the WeasyPrint PDF export produced a valid PDF. The LLM-off gate returns a friendly notice, and with a local Ollama endpoint the full CV-grounded narrative generated live (_meta.path: "findings") โ real radiograph โ real CV โ real prose, no API key. All three tiers are covered โ the prebuilt tarball (docker save radiograph-cv radiograph-narrative, ~6069 MB, verified 2026-08-01), a from-source build, and bare-metal. The clean clone lives inclone/.PHI-safety โ read this. This is a dental-imaging system, so be explicit: no patient data ships, by construction. The narrative service is stateless โ radiographs flow through RAM and are discarded; there is no database, no uploads dir, no bundled patient images. The author's CV research repo holds ~16 GB / ~100k radiograph-type images; that entire image surface is excluded โ the clone ships only source code, the bundled model weights (learned tensors, no recoverable patient images), and exactly two public, CC BY 4.0 research radiographs (
samples/, seeATTRIBUTIONS.md) โ never a real patient image, never a bulk dataset. All secrets are placeholders.
What it is
Two services on one compose network:
cv(cv/, FastAPI, port 8237, internal) โ wraps a curated subset of the CV code + the production weights.POST /detectruns FDI detection โ SAM masks โ pathology and returns structured findings JSON. CPU-only (torch==2.11+cpu); a GPU is auto-detected if present.narrative(narrative/, FastAPI, port 8248, published) โ the product: proxies images tocv, turns findings into a text prompt for an LLM, and adds the rules engine, 12 payer rulesets, a patient-context effect matrix, HITL review, overlays, the approval meter, and two WeasyPrint PDFs. No ML in-process.
Prerequisites
New machine? See ../SETUP.md for the base tools.
| Tier | You need |
|---|---|
| 1 โ prebuilt container (recommended) | Docker Desktop / Engine. Load the on-request radiograph-images.tar bundle and run โ nothing to build, no other deps. |
| 2 โ build from source | Docker Desktop / Engine. docker compose up --build compiles both images locally โ torch, SAM 2, all deps and the weights are baked into the cv image (a few minutes). |
| 3 โ bare-metal | A Linux/macOS host with Python 3.11, and the two services run as separate processes (the CV side needs torch/ultralytics/onnxruntime/opencv + sam2 from git; the narrative side needs WeasyPrint's system libs). |
Tier 1 โ Run the prebuilt container(s) (recommended)
Availability: the prebuilt image bundle is available on request โ it is not published or linked anywhere. Ask Kevin for it, or build everything from source via Tier 2/3 below.
Both images ship in one tarball. Produce it once on a machine that has built them, then load-and-run anywhere with no build step:
# --- produce the tarball once (on a machine that has already built the images) ---
docker save radiograph-cv radiograph-narrative -o radiograph-images.tar # ~6069 MB
# --- consume it anywhere (no --build; the images are already loaded) ---
docker load -i radiograph-images.tar
cd clone
docker compose up -d # โ http://localhost:8248
curl -s localhost:8248/health # โ {"status":"ok"}
The tarball is ~6069 MB (5.9 GB) โ most of it is the cv image, which bundles
~1.1 GB of production weights plus CPU torch/SAM 2. That is the honest price of a
self-contained, key-free CV pipeline: no download step, no API key. The opt-in
LiteLLM gateway (--profile llm) uses a separate public image
(ghcr.io/berriai/litellm) that is pulled on demand and is not in this tarball.
Tier 2 โ Build the image from source
cd clone
docker compose up -d --build # โ http://localhost:8248
Open http://localhost:8248, drag in samples/bitewing_demo.jpg, and click
๐ Detect & Review. The real detector labels the teeth, SAM masks + FDI
numbers + pathology badges render on the radiograph, and the rules engine
suggests procedures โ all key-free.
First detection is slow. The
cvcontainer cold-loads the model stack on the first/detect(~1โ2 min on CPU incl. the 856 MB SAM base); subsequent calls are ~8โ13 s. A GPU is a pure speed-up, not a requirement. The build also takes a few minutes (torch + SAM 2 from source + a one-time timm cache warm).
Tier 3 โ Bare-metal (two processes, Linux/macOS)
cd clone
# CV service (needs torch/ultralytics/onnxruntime/opencv + sam2 from git; weights in cv/app/roboflow/)
cd cv && python -m venv .venv && . .venv/bin/activate
pip install torch==2.11.0 torchvision==0.26.0 --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt && pip install "git+https://github.com/facebookresearch/sam2.git"
python -c "import timm; timm.create_model('efficientnet_b0', pretrained=True, in_chans=4)" # warm cache
ROBOFLOW_DIR=$PWD/app/roboflow python -m uvicorn inference_service:app --app-dir app --port 8237 &
# Narrative service (separate shell)
cd narrative && python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt # + WeasyPrint OS libs (see narrative/Dockerfile)
INFERENCE_URL=http://localhost:8237 python -m uvicorn main:app --app-dir src --port 8248
Optional: the AI narrative (LLM)
The prose step talks to any OpenAI-compatible endpoint and is off by
default โ the /analyze route returns a friendly notice until you enable it.
Everything else (CV findings, overlays, rules, PDFs) works without it. See
.env.example; the turnkey path is the bundled LiteLLM proxy:
# put a provider key in .env, then:
docker compose --profile llm up -d --build
Or point at any OpenAI-compatible base URL โ e.g. a local Ollama (ollama serve
+ ollama pull llama3.2): set LITELLM_URL=http://host.docker.internal:11434,
LITELLM_KEY=ollama, LLM_CHAT_MODELS=llama3.2 and it runs key-free (this is how
the live narrative above was verified).
Verify
curl -s localhost:8248/health # โ {"status":"ok"}
curl -s localhost:8248/findings/health # โ inference ok
# Deterministic PDF export works without any LLM:
curl -s -X POST localhost:8248/export/pdf -H 'Content-Type: application/json' \
-d '[{"procedure_type":"core_buildup","tooth":"30","narrative":"โฆ","cdt_codes":["D2950"]}]' \
-o out.pdf # โ a valid PDF
Then open the UI, upload samples/bitewing_demo.jpg, and click Detect & Review.
Provenance โ models, training data & hardware
Model weights (~1.1 GB, bundled). The author's custom-trained detectors โ a YOLOv8m FDI detector (F1 0.9608 on a held-out 48-image / 295-tooth benchmark), a SAM 2.1-L LoRA fine-tune (val Dice 0.9657, over Meta's SAM 2.1 Hiera-L base), and the pathology stack (modality ONNX + apex regressor on EfficientNet-B0, plus YOLOv11 lesion / restoration / caries detectors). These are learned tensors with no recoverable patient images โ PHI-safe to distribute.
Training data & licenses. The models were trained on an assembled corpus of 15+ public dental-radiograph datasets plus the author's own office radiographs (~150 hand-annotated for FDI). The principal public sources:
| Dataset | Role in training | License |
|---|---|---|
| DenPAR โ Rasnayaka S., Leuke Bandara D., Jayasundara A., et al., "DenPAR: Annotated Intra-Oral Periapical Radiographs Dataset for Machine Learning," Sci Data 12, 1615 (2025); Zenodo DOI 10.5281/zenodo.16645076; paper DOI 10.1038/s41597-025-05906-9 | per-tooth PA masks + FDI pairing; SAM-FT ground truth | CC BY 4.0 |
| DENTEX (panoramic challenge set) | OPG pathology (PARL) + the separate OPG-FDI detector | CC BY-NC-SA (non-commercial) |
| PRAD-5K โ Zhou Z. et al., "PRAD," MICCAI 2025, pp. 475โ484 (Springer); Nankai University | restoration (RCF/DC/IM/DF) + PARL (AP) | research/personal scope; ยฉ Nankai University |
| sam-ybj0w/dental-7vwaw (Roboflow Universe) | bitewing caries recall booster | CC BY 4.0 |
| Additional: Mendeley bitewing/caries sets, NLM Open-i, Roboflow Universe ICCMS caries sets, Bitewing_allside | caries / FDI / restoration supplements | mixed โ CC BY 4.0 / CC BY-NC / Public Domain (per source) |
The corpus carries mixed licenses, several explicitly non-commercial (DENTEX
CC-BY-NC-SA, PRAD research-scope, some Mendeley sets CC BY-NC). The project is
personal-use, non-commercial โ which is why the entire image corpus is
excluded from this clone; only the learned weights and two CC BY 4.0 sample
radiographs ship (see samples/ATTRIBUTIONS.md). Evaluation used strictly
held-out benchmarks (the 48-image FDI test set; a public Mendeley 100-bitewing
caries set, CC BY-NC 3.0), kept disjoint from the training data by perceptual-hash
checks.
Training hardware & automation. The detectors' weights were produced on rented cloud GPUs โ Vast.ai and RunPod โ under a rent โ train โ export โ download โ tear-down discipline (pods destroyed after each run's weights are pulled; costs accounted per run). The FDI detector was pushed through a closed-loop retrain campaign (five cycles): each cycle added a data source, retrained on an H100/H200, and either adopted or rejected the result against a held-out floor. The adopted champion (Cycle 2, an H200 run, ~$1.94) reached F1 0.9608; the whole campaign cost โ $9.13 of ~$17 in credit. The honest outcome: a regression of F1 against training-instance count came out essentially flat, so later single-source cycles stopped helping โ a real ~0.96 data ceiling on the author's own sensor distribution, documented rather than papered over.
No training in this clone. The clone ships the finished weights and runs CPU-only โ there is no dataset, no GPU, and no retrain step here. Reproducing the training would require the public datasets above (per their individual licenses) and a GPU; this recipe reproduces the running pipeline.
What was stripped from the personal version (de-identification + minimalism)
- The hardcoded LiteLLM master key (present in 5
places incl. source + Dockerfile) โ removed everywhere; AI made opt-in
(
--profile llm, placeholder keys), the app now speaks to any OpenAI-compatible endpoint via env, and the model-group names are configurable. - The entire image/dataset surface (~16 GB / ~100k radiographs: datasets, annotation queues, outputs, test sets, even the training-run mosaics next to the weights) โ never copied. The clone is code + weights + two public samples.
.envsecrets (Roboflow / HuggingFace / Label-Studio tokens) โ never copied; a placeholder.env.exampleships instead.- Personal paths in the CV code comments (
C:\Users\โฆ) โ genericized. The CV code is a curated 28-file subset of the research monorepo (detector + SAM + pathology package only), not the full repo. - A CDN dependency โ the UI's
html2canvaswas loaded from jsDelivr โ vendored locally so the app is self-contained. - Kept as-is (the author's call): the 12 payer rulesets with their real carrier names โ they encode public provider-reference documentation patterns and carry no PHI.
Known limitations (stated honestly)
Personal-LAN tool: no authentication, permissive CORS (*), and both services
are stateless (nothing persisted). CPU inference is slow โ the first /detect
cold-loads the models (~1โ2 min) and warm calls are ~8โ13 s for the full
FDI+SAM+pathology stack (a GPU cuts this sharply). The cv image is large (~6 GB)
because it bundles torch + SAM 2 + the weights. Put a reverse proxy with auth in
front before exposing it. The modality classifier can mis-call BW vs PA on some
inputs โ that is the real model's output, surfaced honestly.