Case study // 09
Dental AI Pro
Computer vision for dental X-rays, from detection to a costed care plan.
- Role
- Researcher & engineer
- Year
- 2025
- Category
- AI
- Status
- Live
- Python
- FastAPI
- Flask
- Ultralytics YOLO
- OpenCV
- MongoDB
- Next.js
// CONTEXT
The problem
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
What made it hard
- Dental X-rays are grayscale, low-contrast, and vary widely by machine, so a model has to generalise across capture conditions.
- Multiple conditions can appear in a single image, so this is multi-object detection rather than classification.
- A recommendation that ignores budget or medical history is clinically useless, however good the detection is.
- Ten classes with very different visual signatures. An implant is a high-contrast metal post; bone loss is a gradient. One averaged accuracy figure hides that difference completely.
- Anything advisory in a clinical setting has to be inspectable. A ranking a dentist cannot interrogate is a ranking they will not use.
// DECISIONS
What I chose, and what I didn't
- 01
Use Ultralytics YOLO for detection rather than a whole-image classifier.
WhyConditions 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.
Instead ofA CNN classifier per condition, which would need one pass per condition and would still not say where anything is.
- 02
Separate the vision service (Python/FastAPI) from the clinical reporting layer (Next.js/MongoDB).
WhyInference 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.
Instead ofA single monolithic app, simpler on day one but coupling every model update to the clinical record system.
- 03
Feed demographics, medical history and budget into the recommendation step, downstream of detection.
WhyIt 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.
Instead ofTraining 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.
- 04
Score additively and bounded, so a conflicting factor demotes a treatment instead of eliminating it.
WhyA 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.
Instead ofHard filters on budget and contraindications, which produce a shorter, cleaner list and hide the reason each missing option is missing.
- 05
Report the measured metrics, including the classes the model is bad at.
WhyThe 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.
Instead ofKeeping the headline number, which looks better on a slide and falls apart the first time anyone runs the model.
// OUTCOME
What came of it
- Published after peer review in the Proceedings of the ESOFT International Conference (EICON) 2026, pp. 36–43, and presented orally as paper FPC10 at ESU Kandy on 30 August 2026. Both reviewers returned major revision; the resubmission answered every comment point by point.
- mAP@0.5 0.581 with precision 0.629 and recall 0.584, measured on the 1,501-image held-out test split of a 16,995-radiograph dataset across ten condition classes.
- A nine-treatment knowledge base returning the top five ranked candidates, each with a written rationale naming the detection that triggered it, an indicative cost range and a success-rate estimate.
- A three-tier deployment — Next.js client, FastAPI inference service, MongoDB, and a separate Flask reporting service that compiles the annotated radiograph, the detected conditions and the ranked plan into a PDF for clinical handover.
// REFLECTION
What I'd do differently
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.
