← Kevin Yoder
Digital Dentistry

SmileVision

A chairside tool that segments a patient's teeth, measures the smile, and paints a whitened "ideal" result with Stable Diffusion inpainting β€” meant for discussion, not diagnosis.

Running on LAN Β· :8231 Built Mar 2026 Mar 26 build featured

βš™ How it works πŸ–Ό Screenshots

01 Overview

SmileVision is a self-hosted web app for a cosmetic-consult conversation at the chair: a clinician uploads or camera-captures a patient's smile photo, the system automatically segments the visible teeth, runs a small clinical-esthetics analysis, inpaints an "ideal" smile with Stable Diffusion on a separate GPU, whitens toward a target shade, composites the result back onto the original photo, and shows a zoomed before/after slider. It carries a plain disclaimer β€” an AI-generated visualization for discussion only, not a treatment plan or a guarantee of outcomes.

measure

Reads the smile

Segments the teeth, then measures lip line, dental midline, buccal corridors, and smile arc β€” from the segmentation label map alone.

generate

Redraws the teeth

Zoom-crops the mouth and inpaints new teeth with Stable Diffusion on a remote GPU, then pastes the result back pixel-for-pixel.

finish

Whitens & checks

Adaptive whitening toward a Vita white, an artifact scrubber, and a quality gate that silently regenerates until the checks pass.

02 Why I built it

I work in dentistry, and a cosmetic consult is often a hand-wave: "your teeth could look like this." The tools I found still required manually placing or picking template teeth, which is slow with a patient in the chair. I wanted the visualization to come from the patient's own photo in roughly one click β€” segment the mouth automatically, generate into it, and composite back β€” so the result is grounded in their smile rather than a stock template. It sits alongside my clinical-documentation tools as the patient-facing, "let's talk about it" piece, which is why it leads with a disclaimer and stays deliberately modest about what it claims.

03 What I built & how it works

CPU vision on the server; the GPU diffusion step runs on a GPU PC.

home server Β· CPU vision container FastAPI api.py Β· background thread Segformer face-parse Β· SAM2 teeth mask clinical analysis Β· golden-ratio overlay composite Β· LAB whiten Β· quality gate GPU PC Β· ComfyUI :8188 Β· GTX 1080 Ti Β· SDXL VAEEncodeForInpaint β†’ KSampler 20–30 steps β†’ VAEDecode β†’ SaveImage crop png React 18 UI Β· no build step before/after slider Β· SAM2 debug Β· clinical cards Β· history

Fig. 1 β€” all CPU vision runs in a Docker container on the server; the diffusion step is delegated over the LAN to ComfyUI on a GPU PC's GTX 1080 Ti.

  1. Gate & prep β€” fix EXIF orientation, keep the ICC profile, reject too-dark photos with an actionable message, and downscale to a working copy of about 1500px.
  2. Parse & mask β€” Segformer face-parsing (19 classes; the inner-mouth class is the key one), then SAM2 for the teeth mask, with a MediaPipe FaceMesh fallback on close-up shots.
  3. Analyze β€” measure lip line, dental-vs-facial midline, buccal corridors, and smile arc from the label map, and draw an annotated overlay with the classifications.
  4. Inpaint β€” zoom-crop the mouth region, send it to ComfyUI on the GPU PC, and paste the generated teeth back onto the original at full resolution.
  5. Whiten & gate β€” adaptive LAB whitening toward a Vita A1/B1 target and an artifact scrub, then accept the variation or silently regenerate it, up to three times, until the checks pass.
  6. Present β€” a before/after slider auto-framed on the detected mouth, plus a mask-debug tab and a "Recent Patients" history grid.

04 πŸ›  Skills & tech used

Languages
Python 3.11JavaScript / Node 20React 18 (no build)Docker / Compose YAML
ML / AI
Segformer face-parsingSAM2 promptable segSD inpainting via ComfyUIprompt engineeringMediaPipe FaceMeshvision-LLM age estimate (LiteLLM)
Imaging / CV
OpenCV morphologyLAB perceptual whiteningPoisson / alpha compositingSSIM Β· MSE validationICC / EXIF handling
Infra / Ops
CPU/GPU workload splitlayer-cache Docker buildson-demand wake launcher (dockerode)Caddy + Tailscalehealthchecks
Frontend
build-less React (esm.sh)getUserMedia camerabefore/after drag sliderpolling progress UI
Techniques
quality-gate retry loopsclinical-esthetics thresholdsroot-cause debuggingspec β†’ plan β†’ implement

05 Notable challenges & decisions

Most of the real work was making a generative result behave like a clinical one.

Clinical CV

Measuring a smile from the label map alone

The clinical module derives lip line, dental-vs-facial midline, buccal corridors, and smile arc geometrically from the segmentation β€” no extra model. Small choices mattered: the midline compares a nose-bbox midpoint to the upper-mouth bbox midpoint because a bounding-box midpoint is distribution-invariant, and the thresholds are framed against published esthetics ranges rather than invented.

Reliability

A quality gate so the clinician never sees the failures

Diffusion output is inconsistent, so every variation is checked on several signals β€” overall brightness, a dark-pixel floor, a texture-variance test that catches flat "denture-slab" output, that pixels outside the mask are untouched (SSIM), and near-zero change inside. A failing variation is silently regenerated up to three times, with a last-resort fallback, so only passing results surface.

Diffusion

The inpaint silently did nothing

Early runs came back visually unchanged. The cause was encoding the crop with a plain VAE encode instead of the inpainting-specific 9-channel latent β€” the model was technically running but had nothing to work against. Switching to the inpaint encode, and forcing a higher guidance for the inpainting checkpoint, made the step actually change teeth.

Diffusion

Teeth were too few pixels to look real

Inpainting the whole frame left the teeth at roughly 23Γ—10 latent tokens β€” not enough for coherent detail. The fix was to zoom-crop a padded window around the mouth, inpaint that at model resolution with a minimum crop height, then paste back β€” spending the pixel budget where it shows.

Prompt engineering

The negative prompts caused the artifact they fought

Prompting against "interdental shadows" pushed the model toward flat, white, featureless slabs β€” the opposite of natural teeth. Removing those negatives, nudging toward real tooth separation, and adding a variance check to detect slabs did more than any single prompt term. I also declined a fabricated esthetics metric the research draft suggested, rather than encode a made-up number.

Where I stayed honest about scope. The proportion-guide picker (Levin golden, RED 70/80, Snow) renders an esthetic overlay but does not yet steer generation β€” there is no ControlNet in the workflow, so it informs analysis, not the output. And rather than declare a winner between the pre- and post-whitening-overhaul builds, I kept versions running side by side to compare, with an on-demand launcher sleeping the idle ones.

06 Results

~0.877
typical SAM2 teeth-mask IoU
<1 L*
run-to-run whitening variance, down from Β±8 L* before adaptive normalization
~57s
one SDXL generation (512Β², 30 steps, GTX 1080 Ti)
15β†’2s
per CPU vision step after working-resolution downscale
≀3
silent quality-gate retries per variation
2,369
lines across 9 modules in the first LLM-generated drop

Sources: project git history and commit messages, task-summary notes, and the on-disk source. Figures are from development runs, not a controlled benchmark.

07 Screenshots

The interface shot below is from the verified runnable clone (Steps to Build tab) β€” it processes only a synthetic, computer-drawn face, never a patient. Real patient jobs are never shown (consent/provenance can't be confirmed).

The SmileVision interface: New Smile Visualization with Take Photo / Upload Image buttons, photo tips, and a Recent Patients strip showing one completed job whose thumbnail is an obviously synthetic cartoon face.
The clone's interface, key-free on CPU: upload, run the pipeline, review. The one "recent patient" is the bundled synthetic face β€” the clone ships zero real patient imagery.
[ mask debug ]
Segformer class map β†’ 5-step SAM2 gallery
The Mask tab: the segmentation class map and a step-by-step SAM2 gallery β€” crop, prompt points, candidates, winning overlay.
[ clinical analysis ]
lip line Β· midline Β· buccal corridors Β· smile arc
The clinical overlay and color-coded metric cards, each with its classification and a research-framed threshold note.

08 Honest status

SmileVision was built and iterated heavily over about a week and a half in March 2026, then development stopped. Three versions were kept running as a deliberate comparison; the March 26 build is the one I kept always-on and treat as the working version, still healthy on the LAN at :8231. One caveat to be clear about: this mar26 build predates a later whitening overhaul, so the ambient-luma gate, dark-spot/artifact scrubber, and adaptive LAB whitening described above actually live in a separate, currently-parked build β€” a visitor on :8231 sees the base analysis-and-inpaint pipeline, not those whitening stages. The two other containers were later removed as collateral during an unrelated server cleanup, so the app is not the polished, finished product a screenshot might imply.

It is LAN-only with no authentication, a home-lab convenience, not production security. A full generation also depends on the GPU PC's ComfyUI being awake, which it often isn't; without it the earlier stages (upload, mask, clinical analysis, age estimate) still run. Camera capture needs a secure context, so upload is the reliable path over plain HTTP. And to be clear about the domain: it is a discussion aid, explicitly not a diagnosis, treatment plan, or promise of any outcome. Update (2026-08-02): a runnable, PHI-safe clone (Steps to Build tab) now exists — it ships zero patient imagery (the real photos live only in server-side data volumes and were never in the source tree), runs the full CPU vision pipeline key-free with a mock generation backend, and was verified end-to-end on a synthetic, computer-drawn face. It's the mar26 lineage, so it demonstrates the segment→analyze→composite pipeline, not the parked whitening overhaul.

SmileVision AI β€” Build Recipe

Take a bare machine to a running copy of a dental "ideal smile" / whitening visualization web app. Upload a smile photo and a CPU-only computer-vision pipeline segments the visible teeth (Segformer face-parsing β†’ SAM 2 promptable masking), runs a clinical-esthetics analysis (lip line, dental midline, buccal corridors, smile arc), builds an inpainting mask, generates an "ideal smile" into that region (mock by default), composites the result back pixel-exactly, validates that pixels outside the mask are untouched, and presents a zoomed before/after slider for chairside patient discussion.

Key-free and GPU-free by default. Out of the box the app runs the whole vision pipeline on CPU and uses a built-in mock inpainting backend (SV_METHOD=dummy) β€” a whitened composite blended into the detected mouth mask. No GPU, no model download for generation, no API keys. Real Stable-Diffusion generation is opt-in against your own ComfyUI, and the LLM age-estimator is opt-in against your own endpoint (both off by default).

This is a partial clone of a larger personal project β€” the single canonical web app + CPU pipeline, collapsed from a three-version / multi-machine deployment down to one self-contained build (see What was stripped).

Status: βœ… Verified 2026-08-02 β€” built from this clone on a Linux Docker host (docker compose up -d --build). The service came up healthy on :8231 key-free and GPU-free (SV_METHOD=dummy default); a real end-to-end run on the bundled synthetic face (upload β†’ Segformer/SAM segmentation β†’ clinical analysis β†’ mock composite β†’ validation) completed on CPU in ~75 s and produced a before/after result. The UI rendered with only a benign favicon 404, and the "Recent Patients" thumbnail is the unmistakably-synthetic sample face. All three tiers covered; the prebuilt tarball (docker save smile-vision-clone) size is recorded in ../PREBUILT_IMAGES.md.

Sensitive data: none β€” PHI-safe by construction. All ~638 patient photos on the live server live only in Docker data volumes and were never in the git source tree; this clone ships zero patient imagery β€” the only faces are synthetic, PIL-drawn samples (see clone/samples/ATTRIBUTIONS.md). No data//jobs/ dir ships; real generation is opt-in against your own ComfyUI.

PHI-safety β€” read this. SmileVision's real inputs are patient smile photographs. None of them ship here β€” none, by construction. The original app's ~638 patient and test photos live only in Docker data volumes on the author's server; the git-tracked source tree this clone was built from contains zero images (verified with find over the copied tree β€” the only images anywhere in the clone are three synthetic, PIL-drawn faces under clone/samples/, see below). The clone ships only source code, a self-contained Dockerfile, and that synthetic sample face. No data/, no jobs/, no meta.json, no patient filenames, no bundled dataset. All secrets are placeholders; the author's LiteLLM key and .env were never copied.


What it is

One container: a FastAPI service (src/api.py, port 8231) that serves a build-less React 18 single-page UI (src/static/index.html, no build step) and runs the pipeline (src/pipeline.py) in a background thread. The vision stack is ~14 Python modules β€” face parser, SAM 2 masker, clinical analysis, mask post-processing, golden-ratio guide, compositor (alpha / Poisson / lab-matched blend), validation, and a multi-backend teeth generator whose default backend is the GPU-free mock.

Piece Role
api.py FastAPI: upload / generate / status / image / jobs / health; serves the UI
pipeline.py Orchestrator: parse β†’ mask β†’ clinical analysis β†’ mask post-proc β†’ generate β†’ composite β†’ validate
face_parser.py Segformer jonathandinu/face-parsing (19 classes) + MediaPipe close-up fallback
maskers/sam2_masker.py facebook/sam2.1-hiera-small, bbox+point prompts, multi-candidate best-IoU
clinical_analysis.py Lip line / midline / buccal corridors / smile arc β†’ annotated overlay + prompt modifiers
mask_processor.py Denoise, fill, asymmetric dilation, morph-close, Gaussian feather
golden_ratio.py 4 proportion systems (Levin golden / RED70 / RED80 / Snow) guide overlay
teeth_generator.py Inpainting backends: dummy (default) / comfyui / replicate / local
compositor.py Alpha / Poisson / LAB-matched blend β€” pastes the generated region back so pixels outside the mask are untouched
validation.py SSIM/MSE checks that pixels outside the mask are untouched
age_estimator.py Opt-in vision-LLM age β†’ age-appropriate prompt prefixes

Prerequisites

New machine? See ../SETUP.md for the base tools. Then, per tier:

Tier You need
1 β€” prebuilt container (recommended) Docker Desktop / Engine. Load the on-request image tarball and run β€” nothing to build.
2 β€” build from source Docker Desktop / Engine. docker compose up --build compiles the image (torch + transformers + the ~500 MB of CPU vision models are baked in β€” a few minutes).
3 β€” bare-metal A host with Python 3.11 and the OpenCV system libs; run the one service directly with uvicorn.

Tier 1 β€” Run the prebuilt container (recommended)

Availability: the prebuilt image is available on request β€” it is not published or linked anywhere. Ask Kevin for it, or build from source (Tier 2).

# --- produce the tarball once (on a machine that has already built the image) ---
docker save smile-vision-clone -o smile-vision-clone-image.tar

# --- consume it anywhere (no --build; the image is already loaded) ---
docker load -i smile-vision-clone-image.tar
cd clone
docker compose up -d                  # β†’ http://localhost:8231
curl -s localhost:8231/health         # β†’ {"status":"ok","service":"smile-vision"}

The image is honestly large β€” it bundles CPU torch + transformers plus ~500 MB of pre-downloaded vision models (Segformer ~323 MB, SAM 2-small ~185 MB). That is the price of a self-contained, key-free CV pipeline with no runtime model download for the vision stages.

Tier 2 β€” Build from source

cd clone
docker compose up -d --build          # β†’ http://localhost:8231

Open http://localhost:8231, upload a smile photo (or drag in samples/synthetic_smile.png), and click Generate. With the default SV_METHOD=dummy the pipeline runs end-to-end on CPU β€” face parse β†’ SAM 2 mask β†’ clinical analysis β†’ mock whitened composite β†’ before/after slider β€” with no GPU and no keys.

First request is slow. The container cold-loads (and, if it could not pre-download at build time, fetches) the vision models on the first generation. After warm-up, the CPU vision stages are quick (the original measured a face parse and a SAM 2 mask at ~2 s each on a downscaled working image). A GPU is a pure speed-up for the optional Stable-Diffusion path, never a requirement for the default flow.

Tier 3 β€” Bare-metal (one process)

cd clone
python -m venv .venv && . .venv/bin/activate     # Windows: .venv\Scripts\activate
# CPU torch keeps the install lean:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
# (needs OpenCV's system libs β€” libgl1, libglib2.0-0 β€” on Linux; see Dockerfile)
SV_METHOD=dummy SV_DEVICE=cpu PYTHONPATH=src \
  python -m uvicorn api:app --app-dir src --host 0.0.0.0 --port 8231
# β†’ http://localhost:8231 ; jobs are written to clone/data/jobs/ (gitignored)

There is also a GPU-free, key-free, photo-free smoke test that draws a synthetic face and runs the whole pipeline mechanically (with a synthetic-mask fallback so it needs no models at all):

PYTHONPATH=src python src/quick_test.py

Optional: real Stable-Diffusion generation (your own ComfyUI + GPU)

The dummy backend is a placeholder composite, not a real smile simulation. To generate an actual inpainted smile, point the app at your own ComfyUI server β€” nothing is bundled, and no model is downloaded by this clone:

# in clone/.env  (copy from .env.example)
SV_METHOD=comfyui
COMFYUI_URL=http://your-comfyui-host:8188
COMFYUI_CHECKPOINT=sd-v1-5-inpainting.ckpt

The teeth generator sends a workflow (CheckpointLoader β†’ CLIPTextEncode(Β±prompt) β†’ VAEEncodeForInpaint β†’ KSampler β†’ VAEDecode β†’ SaveImage), polls, and cancels on timeout. This is a GPU workload you host yourself; the original ran it on a GTX 1080 Ti at roughly ~57 s per 512Γ—512 / 30-step generation.

Optional: LLM age estimation (your own endpoint)

Age estimation auto-suggests an age bracket (which tunes the generation prompt) from the portrait. It is off by default and speaks to any OpenAI-compatible endpoint:

# in clone/.env
LITELLM_URL=http://your-litellm:4000       # or a local Ollama, key-free
LITELLM_API_KEY=your-key-or-ollama
LITELLM_VISION_MODEL=gpt-4o-mini           # a vision-capable model your endpoint serves

With LITELLM_URL unset, age_estimator.py short-circuits and the clinician just picks the bracket manually β€” the pipeline is unaffected.

The sample face β€” provenance

The before/after showcase uses only a 100% synthetic, PIL-drawn face β€” a cartoon of ovals, arcs, and lines, not a photograph of anyone. It is generated by the exact code that ships in src/quick_test.py (create_test_image) and src/teeth_generator.py (_generate_dummy), so its provenance is verifiable by construction. See clone/samples/ATTRIBUTIONS.md. The three files (synthetic_smile.png, synthetic_smile_after_dummy.png, and a before_after_dummy.png strip) are public-domain / CC0. No patient photo, and no third-party photograph, appears anywhere in this clone.

Verify

curl -s localhost:8231/health                 # β†’ {"status":"ok","service":"smile-vision"}
curl -s localhost:8231/api/jobs               # β†’ [] on a fresh boot (empty β€” no bundled jobs)

Then open the UI, upload samples/synthetic_smile.png, and click Generate β€” the mask, clinical overlay, and a mock whitened before/after render, key-free.

Provenance β€” models, data & measured facts

Models (downloaded from Hugging Face at build/first-run, not authored here). Segformer jonathandinu/face-parsing (~323 MB) for 19-class face parsing, and Meta's SAM 2.1 Hiera-small (~185 MB) for promptable tooth-region masking. These are public pretrained weights; no model was trained for this project, and no training data is involved in running it.

Measured facts (from the author's development notes, stated honestly).

Applies to what this clone ships (the mar26 lineage): - SAM 2 mask quality: ~0.877 IoU typical on the author's inputs. - Optional SD generation timing (opt-in ComfyUI path): see below.

From a later "Current" build's whitening/quality overhaul β€” described here as development history; this mar26-based clone does not include these stages (its compositor is blend-only and there is no per-variation quality gate): - Whitening stability: switching from fixed offsets to adaptive per-pixel LAB normalization to a target L* cut run-to-run variance from Β±8 L* to <1 L* (fixed offsets gave Vita A2 one run, A3 the next under SD variance). - Whitening targets: upper arch L*=88 (β‰ˆ Vita A1/B1), lower-arch pass L*=87, ceiling 93, with a 12 L* minimum-perceptible-change floor. - Quality gate per generated variation: brightness>145, P25>100, std>18, SSIM outside the mask >0.99 β€” with up to 3 silent regenerations per slot. - CPU working-resolution speedups: face parse and SAM 2 each ~15 s β†’ 2 s, SSIM ~65 s β†’ 5 s, after downscaling large photos to a ≀1500 px working copy. - Optional SD generation: ~57 s per 512Γ—512 / 30-step run on a GTX 1080 Ti (the author's GPU host β€” not part of this clone).

The original was ~6 k lines across the "Current" build and ~5.6 k across the "mar26" snapshot this clone is based on; it grew from a Phase-1 drop of 9 files / ~2,369 lines over 25 commits (2026-03-27 β†’ 03-31).

What was stripped from the personal version (de-identification + minimalism)

  • The three-version / multi-machine deployment. The original ran three side-by-side builds (a "Current" overhaul, and Mar-26 / Mar-27 snapshots) behind a Node/dockerode wake-proxy launcher, with Stable-Diffusion delegated over the LAN to a ComfyUI on a separate GPU PC. Collapsed to one self-contained container that runs the CPU pipeline + mock backend locally; real SD is opt-in against your ComfyUI.
  • The broken base image. mar26's Dockerfile was FROM smile-vision-new:latest (a personal layered image that has since been deleted, so it could not rebuild). Replaced with a full self-contained build (python:3.11-slim + requirements.txt
  • a cached model-download layer).
  • A hardcoded LAN IP β€” the compose COMFYUI_URL pointed at a private LAN address (the author's GPU host) β†’ an unset placeholder (.env.example); the default flow never dials it.
  • A hardcoded internal LiteLLM alias β€” age_estimator.py had http://litellm:4000/... baked in β†’ made env-driven and opt-in (LITELLM_URL, unset = disabled), with the personal model alias (free-vision) replaced by a configurable LITELLM_VISION_MODEL.
  • The LiteLLM key. The mar26 compose referenced ${SMILE_VISION_LITELLM_KEY} from a server-side .env holding a real key β€” that .env was never copied; a placeholder .env.example ships instead.
  • A personal hardware reference β€” a specific workstation model named in requirements.txt and a quick_test.py hint β†’ genericized to "your own GPU / your own ComfyUI".
  • The legacy JSX UI (SmileVisionApp.jsx, unused, hardcoded a dev port) β†’ dropped; only the production index.html ships.
  • A server's local hostname in a design doc β†’ localhost.
  • The entire patient/test image surface (~638 photos across four Docker data volumes, with meta.json clinical geometry and phone-camera filenames) β€” never in the source tree, never copied. Fresh empty volumes; the app boots with an empty job list.
  • The SAM 2 pre-download mismatch β€” the original image pre-fetched sam2.1-hiera-base-plus while the code loads sam2.1-hiera-small; the clone pre-fetches hiera-small so the baked layer is actually used.

Known limitations (stated honestly)

  • The default backend is a mock, not a smile simulator. SV_METHOD=dummy composites a flat lightened fill into the mask β€” it demonstrates the pipeline (segmentation, clinical analysis, mask building, compositing, validation), not a realistic generated smile. A real result needs the opt-in ComfyUI + GPU path.
  • This clone is the mar26 lineage β€” blend-only compositor, no whitening or quality-gate stages. The adaptive-LAB whitening, 3-pass artifact scrubber, and per-variation quality-gate-with-regeneration described in Provenance were a later "Current" build overhaul and are not in this shipped code (the compositor here does alpha / Poisson / LAB-matched blending only). Kept honest rather than back-porting an unshipped feature.
  • The proportion-system picker has no effect on generated output. The golden-ratio guide is drawn and saved as an intermediate, but it is not fed to the generator (there is no ControlNet node in the workflow) β€” it is a visual guide, not a generation constraint. Kept as-is, flagged here for honesty.
  • Personal-LAN tool: no authentication, permissive CORS (*), one shared workspace. Put a reverse proxy with auth in front before exposing it.
  • Camera capture needs a secure context. getUserMedia requires HTTPS or localhost; over plain http://<host>:8231 browsers hide it β€” use file upload.
  • The UI loads React and fonts from CDNs (esm.sh, Google Fonts), so the demo machine needs internet for the page to render.
  • The image is large (CPU torch + transformers + ~500 MB of vision models) β€” the honest cost of a self-contained, key-free CV pipeline.
  • No training here. The clone runs finished public pretrained models on CPU; there is no dataset, no GPU step, and no retrain in this recipe.