Skip to content

Commit 5d28ca5

Browse files
committed
mesh debug
1 parent 5c9357a commit 5d28ca5

2 files changed

Lines changed: 189 additions & 2 deletions

File tree

index.html

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717
transform: scaleX(-1);
1818
}
1919

20+
#debugCanvas {
21+
position: absolute;
22+
top: 0;
23+
left: 0;
24+
/* Mirror horizontally for a natural camera preview */
25+
transform: scaleX(-1);
26+
pointer-events: none;
27+
}
28+
2029
#camera {
2130
/* Mirror horizontally for a natural camera preview */
2231
transform: scaleX(-1);
@@ -34,6 +43,12 @@
3443
display: block;
3544
}
3645

46+
#debugToggle {
47+
margin: 10px auto;
48+
width: 120px;
49+
display: block;
50+
}
51+
3752
#debug {
3853
width: 640px;
3954
margin: 0 auto;
@@ -48,14 +63,15 @@
4863
</style>
4964
</head>
5065
<body>
51-
<!-- MediaPipe Face Mesh and Camera Utils from CDN -->
5266
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script>
5367
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
5468
<div id="tryon">
5569
<video id="camera" loop></video>
5670
<canvas id="overlay"></canvas>
71+
<canvas id="debugCanvas"></canvas>
5772
</div>
5873
<button id="start">Start</button>
74+
<button id="debugToggle">Toggle Debug</button>
5975
<div id="debug"></div>
6076
<div id="hint">Turn your face towards the camera and move your head to adjust the glasses position.</div>
6177

@@ -64,6 +80,7 @@
6480

6581
const startButton = document.getElementById('start');
6682
startButton.style.display = 'none';
83+
const debugToggleButton = document.getElementById('debugToggle');
6784
const debugDiv = document.getElementById('debug');
6885

6986
const object = {
@@ -76,6 +93,7 @@
7693

7794
let tryOn = null;
7895
let debugInterval = null;
96+
let debugMode = false;
7997

8098
function safeFixed(val, digits = 1) {
8199
return (typeof val === 'number' && isFinite(val)) ? val.toFixed(digits) : 'N/A';
@@ -129,6 +147,18 @@
129147
}
130148
});
131149

150+
debugToggleButton.addEventListener('click', function() {
151+
if (!tryOn) return;
152+
debugMode = !debugMode;
153+
if (debugMode) {
154+
tryOn.enableDebug();
155+
debugToggleButton.textContent = 'Hide Debug';
156+
} else {
157+
tryOn.disableDebug();
158+
debugToggleButton.textContent = 'Show Debug';
159+
}
160+
});
161+
132162
updateDebug();
133163
});
134164
</script>

js/tryon-face.js

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export class TryOnFace {
5252
this.size = { x: 1, y: 1, z: 1 };
5353
this.faceMesh = null;
5454
this.camera = null;
55+
this.debugEnabled = params.debug !== undefined ? params.debug : false;
56+
this.debugCanvas = document.getElementById('debugCanvas');
57+
this.debugCanvas.width = this.width;
58+
this.debugCanvas.height = this.height;
59+
this.debugCtx = this.debugCanvas.getContext('2d');
60+
this.lastLandmarks = null;
5561
this.init3D();
5662
}
5763

@@ -99,11 +105,18 @@ export class TryOnFace {
99105
this.changeStatus('STATUS_SEARCH');
100106
this.size.x = 0;
101107
this.size.y = 0;
108+
this.lastLandmarks = null;
102109
this.render();
110+
if (this.debugEnabled && this.debugCtx) {
111+
// Clear debug canvas when no face detected
112+
this.debugCtx.clearRect(0, 0, this.width, this.height);
113+
}
103114
return;
104115
}
105116
this.changeStatus('STATUS_FOUND');
106117
const landmarks = results.multiFaceLandmarks[0];
118+
this.lastLandmarks = landmarks;
119+
107120
// Use MediaPipe landmark indices for left/right ear, eyes, nose, etc.
108121
// See: https://github.com/tensorflow/tfjs-models/blob/master/face-landmarks-detection/mesh_map.jpg
109122
// Example indices:
@@ -163,8 +176,9 @@ export class TryOnFace {
163176
this.size.z = this.size.x * CONFIG.GLASSES.DEPTH_TO_WIDTH_RATIO;
164177
const absYaw = Math.min(Math.abs(yawAngleRad), maxRotationRad) / maxRotationRad;
165178
const depthDampen = 1 - (absYaw * 0.6);
166-
this.position.z = - (this.size.z / 2) * depthDampen;
179+
this.position.z = - (this.size.z) * depthDampen;
167180
this.render();
181+
this.drawDebugLandmarks();
168182
}
169183

170184
loop() {
@@ -300,6 +314,7 @@ export class TryOnFace {
300314
this.textures = await this.loadTextures(textureLoader, renderer, sources);
301315
const materials = this.createMaterials(this.textures);
302316
const { scene, camera, cube } = this.createScene(renderer, materials);
317+
this.renderer = renderer;
303318
this.render = () => {
304319
cube.position.x = this.position.x;
305320
cube.position.y = this.position.y;
@@ -324,4 +339,146 @@ export class TryOnFace {
324339
size: { ...this.size }
325340
};
326341
}
342+
343+
enableDebug() {
344+
this.debugEnabled = true;
345+
}
346+
347+
disableDebug() {
348+
this.debugEnabled = false;
349+
if (this.debugCtx) {
350+
this.debugCtx.clearRect(0, 0, this.width, this.height);
351+
}
352+
}
353+
354+
drawDebugLandmarks() {
355+
if (!this.debugEnabled || !this.lastLandmarks || !this.debugCtx) {
356+
return;
357+
}
358+
359+
const ctx = this.debugCtx;
360+
const landmarks = this.lastLandmarks;
361+
362+
// Clear previous debug drawings
363+
ctx.clearRect(0, 0, this.width, this.height);
364+
365+
// Key landmark indices for MediaPipe Face Mesh
366+
const keyPoints = {
367+
LEFT_EAR: 234,
368+
RIGHT_EAR: 454,
369+
LEFT_EYE: 33,
370+
RIGHT_EYE: 263,
371+
NOSE_TIP: 1,
372+
NOSE_BRIDGE: 168,
373+
LEFT_EYE_OUTER: 133,
374+
RIGHT_EYE_OUTER: 362,
375+
LEFT_EYE_INNER: 33,
376+
RIGHT_EYE_INNER: 263,
377+
CHIN: 152,
378+
FOREHEAD: 10,
379+
LEFT_CHEEK: 234,
380+
RIGHT_CHEEK: 454
381+
};
382+
383+
// Draw all landmarks as small dots (optional, can be overwhelming)
384+
ctx.fillStyle = 'rgba(0, 255, 0, 0.3)';
385+
landmarks.forEach((landmark, idx) => {
386+
const x = landmark.x * this.width;
387+
const y = landmark.y * this.height;
388+
ctx.beginPath();
389+
ctx.arc(x, y, 1, 0, 2 * Math.PI);
390+
ctx.fill();
391+
});
392+
393+
// Draw key landmarks with labels
394+
Object.entries(keyPoints).forEach(([label, idx]) => {
395+
const landmark = landmarks[idx];
396+
if (!landmark) return;
397+
398+
const x = landmark.x * this.width;
399+
const y = landmark.y * this.height;
400+
401+
// Draw a larger circle for key points
402+
ctx.fillStyle = 'rgba(255, 0, 0, 0.8)';
403+
ctx.beginPath();
404+
ctx.arc(x, y, 4, 0, 2 * Math.PI);
405+
ctx.fill();
406+
407+
// Draw label
408+
ctx.fillStyle = 'rgba(255, 255, 0, 0.9)';
409+
ctx.font = '10px monospace';
410+
ctx.fillText(label, x + 6, y + 4);
411+
});
412+
413+
// Draw face bounding box
414+
const leftEar = landmarks[keyPoints.LEFT_EAR];
415+
const rightEar = landmarks[keyPoints.RIGHT_EAR];
416+
const forehead = landmarks[keyPoints.FOREHEAD];
417+
const chin = landmarks[keyPoints.CHIN];
418+
419+
if (leftEar && rightEar && forehead && chin) {
420+
const left = leftEar.x * this.width;
421+
const right = rightEar.x * this.width;
422+
const top = forehead.y * this.height;
423+
const bottom = chin.y * this.height;
424+
425+
ctx.strokeStyle = 'rgba(0, 255, 255, 0.8)';
426+
ctx.lineWidth = 2;
427+
ctx.strokeRect(left, top, right - left, bottom - top);
428+
429+
// Draw face width line
430+
const earY = (leftEar.y + rightEar.y) / 2 * this.height;
431+
ctx.strokeStyle = 'rgba(255, 0, 255, 0.8)';
432+
ctx.lineWidth = 2;
433+
ctx.beginPath();
434+
ctx.moveTo(left, earY);
435+
ctx.lineTo(right, earY);
436+
ctx.stroke();
437+
438+
// Draw face width measurement
439+
const faceWidth = Math.abs(right - left);
440+
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
441+
ctx.font = '12px monospace';
442+
ctx.fillText(`Face Width: ${faceWidth.toFixed(1)}px`, left, earY - 10);
443+
}
444+
445+
// Draw eye line
446+
const leftEye = landmarks[keyPoints.LEFT_EYE];
447+
const rightEye = landmarks[keyPoints.RIGHT_EYE];
448+
if (leftEye && rightEye) {
449+
const lx = leftEye.x * this.width;
450+
const ly = leftEye.y * this.height;
451+
const rx = rightEye.x * this.width;
452+
const ry = rightEye.y * this.height;
453+
454+
ctx.strokeStyle = 'rgba(0, 255, 255, 0.8)';
455+
ctx.lineWidth = 2;
456+
ctx.beginPath();
457+
ctx.moveTo(lx, ly);
458+
ctx.lineTo(rx, ry);
459+
ctx.stroke();
460+
461+
const eyeDistance = Math.sqrt((rx - lx) ** 2 + (ry - ly) ** 2);
462+
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
463+
ctx.font = '12px monospace';
464+
ctx.fillText(`Eye Distance: ${eyeDistance.toFixed(1)}px`, (lx + rx) / 2, (ly + ry) / 2 - 10);
465+
}
466+
467+
// Draw nose bridge to tip line
468+
const noseBridge = landmarks[keyPoints.NOSE_BRIDGE];
469+
const noseTip = landmarks[keyPoints.NOSE_TIP];
470+
if (noseBridge && noseTip) {
471+
const bx = noseBridge.x * this.width;
472+
const by = noseBridge.y * this.height;
473+
const tx = noseTip.x * this.width;
474+
const ty = noseTip.y * this.height;
475+
476+
ctx.strokeStyle = 'rgba(255, 128, 0, 0.8)';
477+
ctx.lineWidth = 2;
478+
ctx.beginPath();
479+
ctx.moveTo(bx, by);
480+
ctx.lineTo(tx, ty);
481+
ctx.stroke();
482+
}
483+
}
327484
}

0 commit comments

Comments
 (0)