AR Smart Mirror
Real-time interactive AR virtual try-on system designed for retail fashion.
The Problem
In-store clothing fitting is slow, creates long queues in fitting rooms, and increases garment wear. Retailers need an instant, hygienic, and engaging digital mirror that lets shoppers visualize outfits without undressing.
My Role
Built solo with AI-assisted development — computer vision pipeline design, MediaPipe pose detection integration, Canvas AR overlay engine, FastAPI backend services, Docker multi-container topology, and Cloudflare Zero Trust ingress.
Highlights
- Sub-30ms real-time body tracking and landmark extraction using MediaPipe Pose
- Dynamic Canvas AR garment warping and affine transformation for realistic clothing drape
- FastAPI backend with PostgreSQL catalog and Redis session pub/sub synchronization
- Mobile controller pairing allowing customers to browse catalogs and trigger try-ons from their smartphones
- Production-grade Docker Compose architecture with Nginx reverse proxy and Cloudflare Tunnel
Stack
Constraints
- Hard real-time latency target (<33ms per frame / 30 FPS) to avoid motion sickness and visual lag.
- Zero customer biometric persistence — video feeds must process in memory without saving raw footage to disk.
- Compatibility with standard consumer hardware (e.g. RTX 3060) without requiring specialized depth cameras.
System Architecture
Key Trade-offs
The decisions worth defending — what I chose, what I turned down, and why.
Pose tracking execution model
Chose
Client-side MediaPipe Pose on Canvas API with GPU acceleration
Rejected
Server-side video streaming and frame-by-frame inference
Server inference introduces network jitter, high bandwidth overhead, and privacy concerns streaming raw camera feeds over the network.
Garment deformation approach
Chose
Real-time 2D affine mesh deformation keyed to skeletal anchor landmarks
Rejected
Full 3D physics-based volumetric cloth simulation
3D cloth simulation cannot sustain 30+ FPS on edge kiosk hardware; 2D mesh triangulation provides fluid response with low computational cost.
Remote store interconnect
Chose
Cloudflare Tunnel over outbound HTTPS
Rejected
Direct port forwarding and dynamic DNS on store routers
Retail store Wi-Fi and ISP firewalls prevent inbound port forwarding; Cloudflare Tunnels punch through NAT securely without public IPs.
What I'd Do Differently
An honest retrospective — the stuff I'd change with more time, more users, or a second pass.
- 1Implement WebGPU shaders for cloth lighting and shadow blending to improve realism under dynamic store lighting.
- 2Add an offline fallback queue for store analytics so telemetry buffers locally during internet interruptions.
- 3Optimize landmark smoothing filters (OneEuro filter) to eliminate micro-jitter when shoppers stand at edge distances.
Computer Vision & Pose Tracking Shell
Benchmark the real-time AR try-on pipeline (status, camera init, pose detect, mesh align -shirt, latency bench).
Technical Deep-Dive
Architecture, specifications, and implementation details.
19 — Try-On Pipeline Architecture
#Overview
The AR Mirror try-on system processes three independent inputs in parallel and fuses them into a single photorealistic try-on result. The three streams are:
| Stream | Input | Purpose |
|---|---|---|
| A | Reference body model | Build a 3D cloth template |
| B | Target cloth image | Provide the garment texture |
| C | Target human (live camera) | Provide the actual body to dress |
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Reference Body │ │ Target Cloth │ │ Target Human │
│ Model (3D mesh) │ │ (product photo) │ │ (live camera) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ │ ▼
┌──────────────────┐ │ ┌──────────────────────┐
│ Reference Mask │◄─────────────┘ │ Semantic Segmentation│
│ Generation │ │ (body-part labelling)│
└────────┬─────────┘ └──────────┬───────────┘
│ Ref. Mask │ Human
▼ │ Segmentation
┌──────────────────┐ │
│ 2D Cloth │◄── Target Cloth ──────────────────────►│
│ Matching │ │
└────────┬─────────┘ ┌──────────▼───────────┐
│ Cloth (2D matched) │ 2D Pose Estimation │
▼ │ (landmark detection) │
┌──────────────────┐ └──────────┬───────────┘
│ 3D Cloth │ │ 2D joints
│ Reconstruction │ ┌──────────▼───────────┐
└────────┬─────────┘ │ 3D Pose & Shape │
│ 3D Cloth Model │ Estimation │
│ └──────────┬───────────┘
│ │ 3D pose & shape
│ ┌───────────────────┤
▼ ▼ ▼
┌──────────────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│ 3D Shape Transfer & │ │ Target Segment. │ │ Target Skin Parts │
│ Re-posing │ │ Generation │ │ Generation │
│ (cloth warped to body) │ └────────┬─────────┘ └──────────┬───────────┘
└────────────┬─────────────┘ │ Target seg. │ Skin parts
│ 3D warped cloth │ │
└─────────────────────────┴────────────────────────┘
│
┌─────────▼─────────┐
│ Try-On Fusion │
│ (compositing) │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ Try-On Result │
└───────────────────┘
#Stage-by-Stage Breakdown
##Stream A — 3D Cloth Model
Stage A1: Reference Mask Generation
- Input: Reference body model (parametric 3D mesh, e.g. SMPL)
- Output: Ref. Mask — a silhouette image encoding body-part regions
- Purpose: Provides a canonical shape template so the cloth matching stage knows where shoulders, torso, and hips are in normalised body space.
Stage A2: 2D Cloth Matching
- Input: Ref. Mask + Target Cloth photo (product image)
- Output: Cloth (2D matched) — the garment warped onto the reference silhouette
- Purpose: Aligns the flat garment photo with the reference body outline so that seams, collar, and hem land on the correct body-part boundaries.
Stage A3: 3D Cloth Reconstruction
- Input: 2D matched cloth + reference body geometry
- Output: 3D Cloth Model — a textured mesh of the garment draped on the reference body
- Purpose: Lifts the 2D aligned garment into 3D so it can be re-posed to match the target person's actual body pose and shape.
##Stream B — Target Human Analysis
Stage B1: 2D Pose Estimation
- Input: Target human frame (live camera or static image)
- Output: 2D joints — a set of 2D keypoints (skeleton landmarks)
- Current implementation: MediaPipe Pose, 33-point model, 30 fps
- Key landmarks used: shoulders [11,12], hips [23,24], nose [0]
Stage B2: 3D Pose & Shape Estimation
- Input: 2D joints + human image context
- Output: 3D pose & shape — a full SMPL-family body mesh fitted to the person
- Purpose: Recovers the person's body volume (not just 2D silhouette), enabling the cloth to wrap correctly around shoulders, chest depth, and waist.
- Current implementation: Approximated by bilinear quad-mesh deformation anchored to 4 body-derived control points (collar-left, collar-right, hem-left, hem-right).
##Stream C — Semantic Understanding
Stage C1: Semantic Segmentation
- Input: Target human frame
- Output: Human Segmentation — per-pixel body-part labels (head, torso, left-arm, right-arm, legs, background, etc.)
- Current implementation: MediaPipe segmentation mask (binary person/background)
Stage C2: Target Segmentation Generation
- Input: Human Segmentation + 3D pose & shape
- Output: Target Segmentation — refined pixel mask aligned to the predicted 3D body
- Purpose: Produces precise garment region boundaries (where cloth should start/end) accounting for body pose, so the fusion stage doesn't bleed cloth onto the face or hands.
Stage C3: Target Skin Parts Generation
- Input: Target Segmentation
- Output: Target Skin Parts — isolated face, neck, hand regions (RGBA patches)
- Purpose: These skin patches are composited on top of the warped cloth in the fusion stage so that hands and face always appear in front of the garment.
##Fusion — Try-On Fusion
Stage F1: 3D Shape Transfer & Re-posing
- Input: 3D Cloth Model (Stream A) + 3D pose & shape (Stream B)
- Output: 3D Warped Cloth — the reference cloth re-posed to the target person's body
- Purpose: Physically warps the cloth mesh to match the target's skeletal pose and body shape, producing correct fold deformations, sleeve length, and torso width.
Stage F2: Try-On Fusion
- Input: 3D Warped Cloth + Target Segmentation + Target Skin Parts
- Output: Try-On Result — final composite frame
- Steps:
- Render 3D warped cloth into the camera view using the estimated camera parameters
- Mask the rendered cloth to the torso segmentation region
- Composite target skin parts (face, hands) on top of the cloth layer
- Blend with the original background
#Current vs. Target Implementation
| Pipeline Stage | Target (full pipeline) | Current Implementation |
|---|---|---|
| Pose estimation | 2D → 3D SMPL fitting | MediaPipe 33-pt 2D landmarks |
| Cloth matching | Reference mask + 2D warping | rembg + 800×860px canvas normalisation |
| 3D reconstruction | Parametric mesh (SMPL+D) | None — 2D quad only |
| Body segmentation | Semantic (14 body parts) | MediaPipe binary person mask (enableSegmentation:true) — used per-frame |
| Cloth warping | 3D mesh re-posing | 2D canvas overlay clipped to body silhouette via mask |
| Skin compositing | Per-region RGBA patch | Landmark capsules (face, neck, forearms) intersected with segmentation mask |
| Fusion | Neural renderer | Layered canvases: video → garment (mask-clipped) → skin (mask-clipped) |
#Garment Image Requirements
For the current pipeline to produce good results:
| Property | Requirement |
|---|---|
| Format | PNG with transparent background |
| Resolution | 800 × 860 px (enforced by upload pipeline) |
| Aspect ratio | ~0.93 : 1 (portrait, shoulder-to-hem) |
| Background | Fully transparent (alpha = 0) |
| Subject | Flat-lay, ghost mannequin, or product shot |
| Shoulder position | Top 15–25 % of image height |
Images uploaded via /admin.html are automatically processed:
- Background removed by rembg (U2Net model)
- Transparent margins trimmed (2 % padding kept)
- Resized and centred on an 800 × 860 transparent canvas
#Key Constants (three_engine.js v61)
| Constant | Value | Meaning |
|---|---|---|
SEGMENTS | 16 | Grid subdivisions → 289 vertices |
COLLAR_FRAC | 0.65 | Collar lifted 65 % of nose-to-shoulder above shoulders |
COLLAR_EXTRA_W | 0.25 | Collar extends 25 % of shoulder span beyond each shoulder |
HEM_EXTRA_W | 0.40 | Hem extends 40 % of hip span beyond each hip |
HEM_EXTRA_H | 0.30 | Hem drops 30 % of body length below hip landmarks |
LERP_POS | 0.18 | Collar corner tracking smoothing factor |
LERP_SIZE | 0.10 | Hem corner tracking smoothing factor |
minDetectionConfidence | 0.5 | MediaPipe pose detection threshold |
minTrackingConfidence | 0.5 | MediaPipe pose tracking threshold |
#Comparison with nawodyaishan/ar-fashion-tryon
Reference repo: https://github.com/nawodyaishan/ar-fashion-tryon
(audited at commit on main, 2026-05-15)
This section documents why the current garment looks the way it does and how the reference repo approaches the same problem, based on direct code inspection — not just the README. The repo is a final-year project with five separate services in one monorepo; AR Mirror is a single product. The comparison below maps service to service.
##Why the garment looks "wrong" today
Every garment in this project is a flat 2D PNG warped onto a 16×16 quad mesh between four MediaPipe-derived anchors (collar-left, collar-right, hem-left, hem-right). Direct consequences:
| Visible artefact | Root cause in the data model |
|---|---|
| Sleeves missing / arms clip through cloth | Garment region is a torso rectangle, no sleeve geometry |
| No realistic folds, drape, or shading | Bilinear interpolation cannot model cloth physics |
| Flat chest, no shoulder roundness | No depth — quad lives at z = 0 |
| Hands appear behind garment when crossing it | No per-pixel skin-part compositing (Stage C3 not implemented) |
| Side / 3⁄4 turns shear the texture | Single front-only PNG, no view-dependent texture |
| Slim-fit vs oversized look identical | Garment width is derived from body span, not cloth shape |
The 800 × 860 px upload canvas (see 09_Garment_Processing_Pipeline.md) bakes the "front-facing 2D PNG" assumption into the DB. Changing the visible structure of the garment therefore requires changing the asset structure first.
##Repository layout of ar-fashion-tryon (what's actually in it)
| Folder | Role | Stack |
|---|---|---|
web-frontend/ | Next.js 15 client (try-on UI, gallery, settings) | Next.js, TS, Tailwind, shadcn/ui, react-rnd |
web-backend/ | NestJS scaffold | TS, Nest — mostly placeholder controllers |
ml-backend/ | Pose & garment ML API | FastAPI, MediaPipe, YOLO, abstract pipeline framework |
image-extraction-backend/ | Upload + classify + bg-removal | FastAPI, TensorFlow/Keras, rembg, Cloudinary |
catvton-gradio/ | Photo-realistic VTON | Gradio + GPU + Stable Diffusion + CatVTON + DensePose (Detectron2) |
vton-api-notebook/ | Jupyter notebook server for VTON | One-off |
The densepose/ and detectron2/ folders inside catvton-gradio/ are vendored
in full (~150 source files) — this is not a slim dependency, it's the whole
Facebook AI library copy-pasted in.
##Architecture comparison
| Aspect | AR Mirror (this project) | nawodyaishan/ar-fashion-tryon |
|---|---|---|
| Form factor | Retail mirror + phone controller | Personal browser try-on (single user, single device) |
| Deployment | On-premises (Docker Compose, 6 services, no cloud) | Cloud — Railway for FastAPI, HuggingFace Space for Gradio, Cloudinary for assets |
| Frontend | Vanilla JS + HTML5 Canvas + Three.js | Next.js 15 + TypeScript + Tailwind + shadcn/ui |
| Backend(s) | One FastAPI service | Four services (Nest, two FastAPIs, one Gradio) |
| Database | PostgreSQL + Redis | None — Cloudinary is the store |
| Session model | Mirror ↔ phone over WebSocket, paired by QR | None — single browser session, local zustand store |
| Real-time engine | MediaPipe Pose (33 pts) on the mirror, in-browser, WebGL quad mesh | MediaPipe Pose on backend, static_image_mode=True, model_complexity=2, one snapshot at a time |
| Offline try-on | Not present | CatVTON diffusion (50 steps, 768×1024) on GPU |
| Body understanding | 5 landmarks (nose, shoulders, hips) | 33 landmarks + DensePose (per-pixel body parts) + SCHP (clothing segmentation) |
| Garment classification | Manual type field at admin upload | TensorFlow/Keras CNN (clothing_model_final.h5) → upper / lower / full + confidence |
| Latency target | < 100 ms render, 30 fps | "Capture-then-process" — seconds per frame for CatVTON, no fps target for AR mode |
| Failed earlier approach | n/a | ml-backend/previous codes/3D approch 1.py — PyTorch3D + DepthAnythingV2 attempt to generate a 3D garment mesh from a 2D photo; abandoned |
##Garment structure comparison — the key finding
Both projects represent the garment as a 2D RGBA PNG with a removed background. Neither uses a true 3D mesh, neither rigs the garment to a skeleton, neither stores per-vertex weights or cloth-physics parameters.
| Garment attribute | AR Mirror | ar-fashion-tryon |
|---|---|---|
| Asset format | PNG, 800 × 860 RGBA | PNG RGBA, no fixed canvas; Cloudinary auto-formats |
| Background removal | rembg / U2Net (one pass) | rembg / U2NET (in image-extraction-backend/services/image_processing.py) |
| Classification | Manual at admin upload (DB field) | TF/Keras CNN at upload, returns label + confidence |
| Storage | Local Docker volume /app/garments/ | Cloudinary folders: /originals/, /cutouts/, /tryon_results/, /outfits/ |
| Real-time geometry | 16 × 16 bilinear quad mesh, 4 pose-derived corners, auto-tracking 30 fps | None — react-rnd widget; the garment is a plain <img> inside a draggable / resizable / rotatable box; user manually positions it (web-frontend/components/tryon/GarmentOverlay.tsx) |
| Pose driving the overlay | Yes (every frame) | No — pose only provides a shoulderY value used as a magnetic snap target when the user drags the image near it (15-px threshold) |
| Photo-realistic mode | Not present | CatVTON diffusion re-renders the whole person — the garment is never positioned at all, it's regenerated as part of the output image |
| Multi-view | Front only | Front only |
| Cloth-physics simulation | No | No in AR mode; faked by the diffusion model in CatVTON mode (no actual physics, but the network has learned to produce plausible folds) |
The reference repo's better-looking screenshots come not from a different garment structure, but from a second, offline pipeline. In CatVTON mode the flat PNG plus the person photo plus DensePose + SCHP masks are fed to a Stable-Diffusion-inpainting model, which synthesises a photo-realistic image of the person wearing the garment. That is image generation, not garment rendering — it takes seconds per frame on a GPU and is not real-time.
In its AR (real-time) mode, ar-fashion-tryon is actually less automatic
than AR Mirror: the user must drag the garment onto themselves with a mouse,
because there is no pose-driven mesh deformation; the only "smart" behaviour is
a 15-pixel magnetic snap to the detected shoulder line.
##What this means for "I want a different garment structure"
If by "different structure" you mean the cloth wraps the body better — that is solvable inside the existing 2D PNG asset format. If you mean the asset itself is no longer a flat picture — that requires replacing both the upload pipeline and the renderer.
Four concrete paths, in increasing cost:
-
Stay 2D, add semantic regions to the existing PNG asset. Render sleeves, collar, hem, side seams as separate rules anchored to elbow [13,14], nose [0], knee [25,26] respectively. Asset stays one PNG; only the renderer changes. Fixes: sleeves, hem fall, collar tracking. Does not fix: drape, occlusion, depth. Cost: renderer rewrite only.
-
Add segmentation-based skin compositing. Implement Stage C3 (Target Skin Parts) from section above using MediaPipe Selfie Segmentation (already shipped with MediaPipe Pose, just turn
enable_segmentation=Trueon). Composite hands/face over the garment. Asset format unchanged. Fixes: hands no longer disappear behind cloth. Cost: one extra mask blit per frame. -
Adopt the reference repo's approach for hero shots only — run a CatVTON-style diffusion model server-side for a "save my outfit" button, keeping the real-time quad-mesh for live preview. Requires a GPU on-prem (incompatible with the current on-premises CPU constraint) or accepting a cloud dependency. The reference repo demonstrates that this is the only path to photo-realistic output today.
-
True 3D garment per item — glTF/GLB mesh with a skeleton rig per SKU, driven by MediaPipe pose. The reference repo tried this in
ml-backend/previous codes/3D approch 1.py(PyTorch3D + DepthAnythingV2 to lift a single product photo into a 3D mesh) and abandoned it — the file sits in aprevious codes/quarantine. The realistic version of this requires per-garment 3D authoring, which breaks the "admin uploads a product photo" workflow.
Option 1 is the cheapest visible improvement to garment shape. Option 2 is the cheapest fix for the "hand-behind-cloth" artefact. Option 3 is the only path to photo-realism, at the cost of GPU and latency. Option 4 is structurally the "real" answer but is unproven — the reference repo's own attempt did not ship.
##Implementation log
| Date | Change | Files | Notes |
|---|---|---|---|
| 2026-05-15 | Option 2 shipped — segmentation-mask compositing | frontend/AR-mirror/app.js | enableSegmentation:true was already on but results.segmentationMask was unused. Added maskBuffer + updatePersonMask(). Garment is now painted into garmentBuffer, clipped via destination-in against the mask, then composited onto cvOv. Skin patches in drawSkinParts() are intersected with the same mask at the end of the function so capsule edges follow real arm silhouette. Cost: one extra drawImage of the mask per frame, ~0.3 ms on the target hardware. |
| 2026-05-15 | Realism stack on top of Option 2 — five cheap passes layered into drawGarmentCanvas2D and drawSkinParts | frontend/AR-mirror/app.js | Approximates the cues a diffusion model produces, in real time on CPU. See "Realism stack" below for the per-pass breakdown. |
| 2026-05-15 | Temporal mask EMA — silhouette no longer shimmers | frontend/AR-mirror/app.js | MediaPipe's per-frame segmentation jitters by 1-2 px at the edges. updatePersonMask now blends current + previous smoothed mask using globalCompositeOperation = 'lighter' with globalAlpha = MASK_EMA_CURR (0.6) + 1 - MASK_EMA_CURR (0.4), producing a true weighted average. Adds currMaskBuffer and prevMaskBuffer (the smoothed result feeds back as prev next frame). Settles in ~3 frames (~100 ms at 30 fps), imperceptible lag, kills the cloth-edge shimmer. |
##Realism stack (added 2026-05-15)
Five passes layered on top of the segmentation-mask base. None require new dependencies or a GPU; total added cost is ~1 ms per frame.
| # | Pass | Where | What it fakes | How |
|---|---|---|---|---|
| 1 | Edge feather | drawGarmentCanvas2D step 5 | Soft fabric edge instead of pixel-binary mask cut | ctx.filter = 'blur(2px)' applied only at the moment of the destination-in mask draw |
| 2 | Collar shadow | drawGarmentCanvas2D step 2 | Natural occlusion shadow at neckline | Dark-to-clear vertical gradient over the top 12 % of the garment, composited with source-atop |
| 3 | Body-lean shading | drawGarmentCanvas2D step 3 | Side-lighting that responds to body rotation | Horizontal gradient driven by (ls.z - rs.z). Smoothed (LERP 0.15) in gSmooth.leanZ to avoid flicker. Max opacity 0.32 |
| 4 | Hem fade | drawGarmentCanvas2D step 4 | Cloth bottom blending with body / bottoms instead of a hard line | destination-out alpha gradient over bottom 8 % of garment |
| 5 | Z-aware skin visibility | drawSkinParts armInFront() | Arms behind the body don't get a skin patch drawn over the garment | Skip a forearm capsule when wrist.visibility < 0.4 or ((elbow.z + wrist.z)/2 - shoulder.z) > 0.18 |
Why this combination matters: the reference repo
nawodyaishan/ar-fashion-tryon produces realistic results only via the
offline CatVTON diffusion pipeline (seconds per frame, GPU-bound). The five
passes above approximate the most-visible cues that pipeline learns —
silhouette-aware edges, collar occlusion, side shading, hem blending,
correct front/back depth ordering — without any ML inference beyond the
already-running MediaPipe Pose. They are not photo-realistic, but together
they cross the threshold from "PNG taped to a body" to "cloth that responds
to lighting and pose."
Tuning constants (current values, in drawGarmentCanvas2D)
| Constant | Value | Pass |
|---|---|---|
collarShadowH | dH * 0.12 | Collar shadow band height |
| Collar gradient max alpha | 0.38 | Collar shadow darkness |
| Lean Z threshold | 0.04 | Below this, no body-lean shading |
| Lean intensity cap | 0.32 | Max darkness of body-lean gradient |
| Lean LERP factor | 0.15 | Smoothing on gSmooth.leanZ |
hemFadeH | dH * 0.08 | Hem fade band height |
| Hem fade max alpha | 0.55 | Hem fade strength |
| Mask blur | 2 px | Edge feather radius |
| Z-behind threshold | 0.18 | Hide forearm if (avg(elbow,wrist).z - shoulder.z) > this |
| Wrist visibility floor | 0.4 | Hide forearm if MediaPipe is unsure |