Case study // 09
Computer vision for dental X-rays, from detection to a costed care plan.
// CONTEXT
Choosing a dental prosthetic — an implant, a bridge, a denture — is a judgement call that depends on what the radiograph shows, but also on the patient's age, their medical history and what they can afford. Two competent dentists can reach different plans for the same patient, and the process usually runs across several consultation visits before anything is fixed. Commercial dental AI has not helped with this part: Overjet, Denti.AI and Pearl read radiographs and mark pathology, which tells a clinician what is wrong, not what to do about it for this particular patient.
// CONSTRAINTS
// DECISIONS
Conditions are localised and multiple per image. Detection returns position and extent per finding, which is what a prosthetic plan actually needs. A classifier collapses all of that into one label.
A CNN classifier per condition, which would need one pass per condition and would still not say where anything is.
Inference is bursty and CPU-bound, while report generation and patient records are neither. Splitting them lets the model be retrained and redeployed without touching patient data flows.
A single monolithic app, simpler on day one but coupling every model update to the clinical record system.
It keeps the model honest. The vision layer reports what it sees, and the affordability and suitability reasoning stays explicit and auditable rather than baked into learned weights.
Training a single end-to-end model on outcomes, which would be far harder to explain to a clinician and impossible to adjust when a price list changes.
A clinically ideal treatment that exceeds the patient's budget is still information the dentist should see. Ranking it down keeps it on the list with its cost attached; dropping it means the option is silently never discussed.
Hard filters on budget and contraindications, which produce a shorter, cleaner list and hide the reason each missing option is missing.
The report and the accuracy demo originally printed 91.3% accuracy and mAP@0.5 0.87 — figures that were never measured and that the checkpoint does not support. Replacing them with an Ultralytics run over the held-out test split, recorded with the weights digest, is the difference between a demo and a result. The per-class table then shows what the average hides: implants at 0.951 mAP, bone loss at 0.156.
Keeping the headline number, which looks better on a slide and falls apart the first time anyone runs the model.
// OUTCOME
// REFLECTION
The number I would defend hardest is the one that looks worst. Averaged over ten classes the detector reads mAP@0.5 0.581, but bone loss recall is 0.035 and periapical lesion recall is 0.129 — it finds implants and impacted teeth almost every time and misses most of the fine pathology that actually changes a treatment plan. That is a dataset and schedule problem rather than an architecture one, and the honest consequence is that a missed detection can remove an appropriate treatment from the ranking entirely, which is why the annotated radiograph is shown beside the recommendations for the clinician to check. The deeper lesson was about provenance: this codebase shipped invented performance figures for months, and they survived precisely because nothing ever recomputed them. The fix was a script that writes the metrics to a file with the weights digest attached, and a report that says the model has not been evaluated rather than substituting a placeholder.