asterioskryos commited on
Commit
0150ef7
·
verified ·
1 Parent(s): a258da9

React face analysis app

Browse files
Files changed (9) hide show
  1. assets/index-B3zpsVgk.css +1 -0
  2. assets/index-Bv-INYt6.js +0 -0
  3. favicon.svg +1 -0
  4. icons.svg +24 -0
  5. index.html +13 -32
  6. js/mediapipe.js +0 -24
  7. js/scoring.js +0 -109
  8. js/ui.js +0 -139
  9. style.css +0 -7
assets/index-B3zpsVgk.css ADDED
@@ -0,0 +1 @@
 
 
1
+ *{box-sizing:border-box;margin:0;padding:0}body{color:#e0e0e0;background:#0f0f13;min-height:100vh;font-family:system-ui,-apple-system,sans-serif}#root{max-width:900px;margin:0 auto;padding:24px}
assets/index-Bv-INYt6.js ADDED
The diff for this file is too large to render. See raw diff
 
favicon.svg ADDED
icons.svg ADDED
index.html CHANGED
@@ -1,34 +1,15 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Face Analysis</title>
7
- <link rel="stylesheet" href="style.css">
8
- <script src="https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/wasm/vision_bundle.js" crossorigin="anonymous"></script>
9
- <script type="module" src="js/ui.js"></script>
10
- </head>
11
- <body>
12
- <div id="app">
13
- <h1>Face Analysis</h1>
14
-
15
- <div id="uploadArea">
16
- <div id="uploadPrompt">
17
- <p>Drop photo here or click to upload</p>
18
- </div>
19
- <img id="preview" style="display:none">
20
- <input type="file" id="fileInput" accept="image/*">
21
- </div>
22
-
23
- <button id="analyzeBtn" disabled>Analyze</button>
24
- <div id="status">Loading...</div>
25
-
26
- <canvas id="resultCanvas" style="display:none"></canvas>
27
-
28
- <div id="results" style="display:none">
29
- <h2>Overall: <span id="overallScore"></span></h2>
30
- <div id="scoreGrid"></div>
31
- </div>
32
- </div>
33
- </body>
34
  </html>
 
1
+ <!doctype html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Face Analysis</title>
8
+ <script src="https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/wasm/vision_bundle.js" crossorigin="anonymous"></script>
9
+ <script type="module" crossorigin src="/assets/index-Bv-INYt6.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-B3zpsVgk.css">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  </html>
js/mediapipe.js DELETED
@@ -1,24 +0,0 @@
1
- let faceLandmarker = null;
2
-
3
- export async function initModel() {
4
- const { FilesetResolver, FaceLandmarker } = window.vision;
5
- const resolver = await FilesetResolver.forVisionTasks(
6
- 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/wasm/'
7
- );
8
- faceLandmarker = await FaceLandmarker.createFromOptions(resolver, {
9
- baseOptions: {
10
- modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',
11
- delegate: 'GPU',
12
- },
13
- runningMode: 'IMAGE',
14
- numFaces: 1,
15
- });
16
- return true;
17
- }
18
-
19
- export function detectLandmarks(image) {
20
- if (!faceLandmarker) return null;
21
- const result = faceLandmarker.detect(image);
22
- if (!result.faceLandmarks || result.faceLandmarks.length === 0) return null;
23
- return result.faceLandmarks[0];
24
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
js/scoring.js DELETED
@@ -1,109 +0,0 @@
1
- const PAIRS = [
2
- [33,263],[133,362],[157,387],[158,386],[159,385],[160,384],[161,398],[173,466],[246,380],
3
- [46,276],[53,283],[52,282],[65,295],[66,296],[70,300],[105,334],[107,336],
4
- [234,454],[93,351],[132,361],[58,288],[172,397],[136,377],[150,379],[149,378],[176,401],[148,384],
5
- [61,291],[39,269],[37,267],[84,314],[181,321],[91,308],[146,375],[174,399],[89,359],[169,420],
6
- ];
7
- const LEFT_EYE=[33,133,157,158,159,160,161,173,246];
8
- const RIGHT_EYE=[362,263,387,386,385,384,398,466,380];
9
- const LEFT_BROW=[46,53,52,65,66,70,105,107];
10
- const RIGHT_BROW=[276,283,282,295,296,300,334,336];
11
- const LIPS=[0,37,39,40,61,84,91,146,178,181,185,267,269,270,291,308,314,317,321,324,325,375,402,405];
12
- const FOREHEAD=10, CHIN=152, L_CHEEK=234, R_CHEEK=454;
13
- const NOSE_TIP=1, NOSE_BRIDGE=6, UPPER_LIP=13, LOWER_LIP=14;
14
- const L_EYE=159, R_EYE=386;
15
-
16
- function d(a,b){return Math.hypot(a.x-b.x,a.y-b.y)}
17
-
18
- export function computeAll(pts){
19
- if(!pts||pts.length<468)return null;
20
- const sym=computeSymmetry(pts);
21
- const props=computeProps(pts);
22
- const eyeS=featureSym(pts,LEFT_EYE,RIGHT_EYE);
23
- const browS=featureSym(pts,LEFT_BROW,RIGHT_BROW);
24
- const lipS=lipScore(pts);
25
- const[chinShape,chinS]=chinScore(pts);
26
- const overall=sym*0.35+props*0.35+eyeS*0.1+browS*0.05+lipS*0.1+chinS*0.05;
27
- return{
28
- overall:Math.round(overall*10)/10,
29
- symmetry:Math.round(sym*10)/10,
30
- proportions:Math.round(props*10)/10,
31
- eyeSymmetry:Math.round(eyeS*10)/10,
32
- browSymmetry:Math.round(browS*10)/10,
33
- lipFullness:Math.round(lipS*10)/10,
34
- chinShape,chinScore:Math.round(chinS*10)/10,
35
- };
36
- }
37
-
38
- function computeSymmetry(pts){
39
- let errs=[];
40
- for(const[l,r]of PAIRS){
41
- if(l<pts.length&&r<pts.length){
42
- const mid=(pts[l].x+pts[r].x)/2;
43
- errs.push(Math.abs(mid-0.5));
44
- }
45
- }
46
- if(!errs.length)return 50;
47
- const avg=errs.reduce((a,b)=>a+b,0)/errs.length;
48
- return Math.max(0,Math.min(100,100-avg*500));
49
- }
50
-
51
- function featureSym(pts,left,right){
52
- let ld=left.map(i=>pts[i]).filter(Boolean);
53
- let rd=right.map(i=>pts[i]).filter(Boolean);
54
- const len=Math.min(ld.length,rd.length);
55
- if(!len)return 100;
56
- let tot=0;
57
- for(let i=0;i<len;i++)tot+=d(ld[i],rd[i]);
58
- return Math.max(0,Math.min(100,100-(tot/len)*1000));
59
- }
60
-
61
- function lipScore(pts){
62
- const lp=LIPS.map(i=>pts[i]).filter(Boolean);
63
- if(lp.length<4)return 50;
64
- const xs=lp.map(p=>p.x),ys=lp.map(p=>p.y);
65
- const w=Math.max(...xs)-Math.min(...xs);
66
- const h=Math.max(...ys)-Math.min(...ys);
67
- if(!w)return 50;
68
- const ratio=h/w,ideal=0.35;
69
- return Math.max(0,Math.min(100,100-Math.abs(ratio-ideal)/ideal*150));
70
- }
71
-
72
- function chinScore(pts){
73
- if(pts.length<=CHIN)return['Unknown',50];
74
- const chin=pts[CHIN],jl=pts[136],jr=pts[377];
75
- const jw=d(jl,jr);
76
- const cj=Math.abs(chin.y-(jl.y+jr.y)/2);
77
- const ratio=jw?cj/jw:0;
78
- let shape=ratio<0.25?'Round':ratio<0.35?'Square':'Pointed';
79
- return[shape,Math.max(0,Math.min(100,100-Math.abs(ratio-0.3)*300))];
80
- }
81
-
82
- function computeProps(pts){
83
- const r={};
84
- if(pts.length>Math.max(FOREHEAD,CHIN,L_CHEEK,R_CHEEK)){
85
- const fw=pts[R_CHEEK].x-pts[L_CHEEK].x;
86
- const fh=pts[CHIN].y-pts[FOREHEAD].y;
87
- r.faceRatio=fh?fw/fh:0;
88
- }
89
- if(pts.length>Math.max(NOSE_BRIDGE,NOSE_TIP,L_EYE,R_EYE,UPPER_LIP,LOWER_LIP)){
90
- const ed=d(pts[L_EYE],pts[R_EYE]);
91
- const nl=d(pts[NOSE_BRIDGE],pts[NOSE_TIP]);
92
- const mh=d(pts[UPPER_LIP],pts[LOWER_LIP]);
93
- r.eyeToNose=nl?ed/nl:0;
94
- r.noseToMouth=mh?nl/mh:0;
95
- const fw=d(pts[L_CHEEK],pts[R_CHEEK]);
96
- r.eyeToFace=fw?ed/fw:0;
97
- }
98
- return ratioScore(r);
99
- }
100
-
101
- function ratioScore(r){
102
- const vals=[
103
- r.faceRatio?Math.max(0,100-Math.abs(r.faceRatio-0.72)/0.72*150):null,
104
- r.eyeToNose?Math.max(0,100-Math.abs(r.eyeToNose-1.6)/1.6*100):null,
105
- r.noseToMouth?Math.max(0,100-Math.abs(r.noseToMouth-2)/2*100):null,
106
- r.eyeToFace?Math.max(0,100-Math.abs(r.eyeToFace-0.46)/0.46*150):null,
107
- ].filter(v=>v!==null);
108
- return vals.length?vals.reduce((a,b)=>a+b,0)/vals.length:50;
109
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
js/ui.js DELETED
@@ -1,139 +0,0 @@
1
- import { initModel, detectLandmarks } from './mediapipe.js';
2
- import { computeAll } from './scoring.js';
3
-
4
- let currentImage = null;
5
-
6
- export function initUI() {
7
- const upload = document.getElementById('uploadArea');
8
- const input = document.getElementById('fileInput');
9
- const btn = document.getElementById('analyzeBtn');
10
- const status = document.getElementById('status');
11
-
12
- status.textContent = 'Loading AI model...';
13
-
14
- upload.onclick = () => input.click();
15
- upload.ondragover = e => { e.preventDefault(); upload.style.borderColor = '#7c5cfc'; };
16
- upload.ondragleave = () => { upload.style.borderColor = '#444'; };
17
- upload.ondrop = e => {
18
- e.preventDefault();
19
- if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
20
- };
21
- input.onchange = () => { if (input.files[0]) handleFile(input.files[0]); };
22
- btn.onclick = analyze;
23
-
24
- initModel().then(() => {
25
- status.textContent = 'Ready. Upload a photo.';
26
- btn.disabled = false;
27
- }).catch(e => {
28
- status.textContent = 'Failed to load model: ' + e.message;
29
- });
30
- }
31
-
32
- function handleFile(file) {
33
- if (!file || !file.type.startsWith('image/')) return;
34
- const reader = new FileReader();
35
- reader.onload = e => {
36
- const img = new Image();
37
- img.onload = () => {
38
- currentImage = img;
39
- document.getElementById('preview').src = e.target.result;
40
- document.getElementById('preview').style.display = 'block';
41
- document.getElementById('uploadPrompt').style.display = 'none';
42
- document.getElementById('analyzeBtn').disabled = false;
43
- document.getElementById('results').style.display = 'none';
44
- };
45
- img.src = e.target.result;
46
- };
47
- reader.readAsDataURL(file);
48
- }
49
-
50
- async function analyze() {
51
- if (!currentImage) return;
52
- const status = document.getElementById('status');
53
- const btn = document.getElementById('analyzeBtn');
54
- status.textContent = 'Analyzing...';
55
- btn.disabled = true;
56
-
57
- try {
58
- const landmarks = detectLandmarks(currentImage);
59
- if (!landmarks) {
60
- status.textContent = 'No face detected. Try a front-facing photo.';
61
- btn.disabled = false;
62
- return;
63
- }
64
-
65
- const scores = computeAll(landmarks);
66
- if (!scores) {
67
- status.textContent = 'Could not compute scores.';
68
- btn.disabled = false;
69
- return;
70
- }
71
-
72
- drawLandmarks(currentImage, landmarks);
73
- displayScores(scores);
74
- status.textContent = `Done — ${landmarks.length} landmarks detected.`;
75
- } catch (e) {
76
- status.textContent = 'Error: ' + e.message;
77
- }
78
- btn.disabled = false;
79
- }
80
-
81
- function drawLandmarks(img, landmarks) {
82
- const canvas = document.getElementById('resultCanvas');
83
- canvas.width = img.width;
84
- canvas.height = img.height;
85
- canvas.style.display = 'block';
86
- const ctx = canvas.getContext('2d');
87
- ctx.drawImage(img, 0, 0);
88
-
89
- const conns = [
90
- [33,133],[133,157],[157,158],[158,159],[159,160],[160,161],[161,173],[173,246],[246,33],
91
- [362,263],[263,387],[387,386],[386,385],[385,384],[384,398],[398,466],[466,380],[380,362],
92
- [46,53],[53,52],[52,65],[65,66],[66,70],[70,105],[105,107],[107,46],
93
- [276,283],[283,282],[282,295],[295,296],[296,300],[300,334],[334,336],[336,276],
94
- [61,146],[146,91],[91,181],[181,84],[84,17],[17,314],[314,405],[405,321],[321,375],[375,291],[291,409],[409,270],[270,269],[269,0],
95
- [10,338],[338,297],[297,332],[332,284],[284,251],[251,389],[389,356],[356,454],[454,323],[323,361],[361,288],[288,397],[397,365],[365,379],[379,378],[378,400],[400,377],[377,152],[152,148],[148,176],[176,149],[149,150],[150,136],[136,172],[172,58],[58,132],[132,93],[93,234],[234,127],[127,162],[162,21],[21,54],[54,103],[103,67],[67,109],[109,10],
96
- ];
97
-
98
- ctx.strokeStyle = '#00ff88';
99
- ctx.lineWidth = 2;
100
- for (const [i, j] of conns) {
101
- if (i < landmarks.length && j < landmarks.length) {
102
- ctx.beginPath();
103
- ctx.moveTo(landmarks[i].x * img.width, landmarks[i].y * img.height);
104
- ctx.lineTo(landmarks[j].x * img.width, landmarks[j].y * img.height);
105
- ctx.stroke();
106
- }
107
- }
108
-
109
- ctx.fillStyle = '#ff4444';
110
- for (const lm of landmarks) {
111
- ctx.beginPath();
112
- ctx.arc(lm.x * img.width, lm.y * img.height, 2, 0, 2 * Math.PI);
113
- ctx.fill();
114
- }
115
- }
116
-
117
- function displayScores(s) {
118
- document.getElementById('results').style.display = 'block';
119
- document.getElementById('overallScore').textContent = s.overall;
120
-
121
- const items = [
122
- ['Facial Symmetry', s.symmetry],
123
- ['Facial Proportions', s.proportions],
124
- ['Eye Symmetry', s.eyeSymmetry],
125
- ['Eyebrow Symmetry', s.browSymmetry],
126
- ['Lip Fullness', s.lipFullness],
127
- [`Chin (${s.chinShape})`, s.chinScore],
128
- ];
129
-
130
- const grid = document.getElementById('scoreGrid');
131
- grid.innerHTML = '';
132
- for (const [label, val] of items) {
133
- const div = document.createElement('div');
134
- div.className = 'score';
135
- const color = val >= 70 ? '#4ade80' : val >= 50 ? '#facc15' : '#f87171';
136
- div.innerHTML = `<span>${label}</span><span style="color:${color};font-weight:700;font-size:1.2em">${val}</span>`;
137
- grid.appendChild(div);
138
- }
139
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
style.css DELETED
@@ -1,7 +0,0 @@
1
- body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #111; color: #eee; }
2
- #uploadArea { border: 2px dashed #444; padding: 30px; text-align: center; cursor: pointer; margin: 16px 0; }
3
- #uploadArea:hover { border-color: #7c5cfc; }
4
- img, canvas { max-width: 100%; }
5
- button { padding: 10px 24px; font-size: 16px; cursor: pointer; }
6
- .score { display: flex; justify-content: space-between; padding: 8px 12px; background: #222; margin: 4px 0; border-radius: 6px; }
7
- #status { margin-top: 8px; color: #888; }