20 lines
6.1 KiB
HTML
20 lines
6.1 KiB
HTML
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>颞颧比与颧颞比测试</title>
|
||
<style>body{font:15px system-ui;background:#f4f6fa;color:#203047;margin:0}main{max-width:1200px;margin:auto;padding:28px}h1{font-size:25px}section,article{background:white;padding:20px;border-radius:12px;margin:16px 0}button,input{font:inherit;padding:9px}button{cursor:pointer} .grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(260px,1fr);gap:20px}canvas{max-width:100%;height:auto;touch-action:none}pre{overflow:auto;max-height:300px;font-size:12px}.muted{color:#66758a;line-height:1.7}.value{font-size:28px;font-weight:700}#status{white-space:pre-wrap} @media(max-width:750px){.grid{display:block}}</style>
|
||
<main><p><a href="/cases/">查看批量案例与筛选结果</a></p><h1>颞颧比与颧颞比测试</h1><p class="muted">Ft:HL right right → HL left left(两端取平均高度,水平测量) · Zy:234 → 454<br>先按接口52统一参数扶正裁切,均按处理后图片二维直线距离计算。Ft ÷ Zy 小于 0.8 时,同时判定为颧弓外扩和太阳穴凹陷。</p>
|
||
<section><input aria-label="选择人像" type="file" id="file" accept="image/jpeg,image/png"><button id="run">开始检测</button> <button id="reset" disabled>恢复自动点位</button><p id="status" role="status">请选择单人正面照。</p></section>
|
||
<div class="grid"><section><canvas id="canvas"></canvas><p class="muted">粉色:Ft 蓝色:Zy。检测后可拖动端点校正,结果实时更新。</p></section><div><article><h2>颧弓外扩</h2><p>颞颧比 Ft ÷ Zy < 0.8</p><div class="value" id="r1">—</div><p id="s1">等待检测</p></article><article><h2>太阳穴凹陷</h2><p>判定条件 Ft ÷ Zy < 0.8(下方 Zy ÷ Ft 为参考值)</p><div class="value" id="r2">—</div><p id="s2">等待检测</p></article><p id="lengths"></p></div></div><section><h2>测量数据</h2><pre id="json">暂无数据</pre></section></main>
|
||
<script>
|
||
const $=id=>document.getElementById(id),keys=['hair_line_right_right','hair_line_left_left','face_edge_234','face_edge_454'],labels=['HL right right','HL left left','234','454'];
|
||
let img=null,points={},original={},drag=null,manual=false,busy=false;
|
||
function dist(a,b){return a&&b&&[a.x,a.y,b.x,b.y].every(Number.isFinite)?Math.hypot(a.x-b.x,a.y-b.y):null}
|
||
function update(){const left=points[keys[0]],right=points[keys[1]];if(left&&right){const y=(left.y+right.y)/2;left.y=right.y=y}const ft=dist(points[keys[0]],points[keys[1]]),zy=dist(points[keys[2]],points[keys[3]]),valid=ft>0&&zy>0;const a=valid?ft/zy:null,b=valid?zy/ft:null;
|
||
const matched = valid ? a < 0.8 : null;
|
||
$('r1').textContent=valid?a.toFixed(4):'—';$('r2').textContent=valid?b.toFixed(4):'—';$('s1').textContent=valid?(matched?'命中':'未命中'):'无法计算:点位缺失或距离为零';$('s2').textContent=valid?(matched?'命中':'未命中'):'无法计算:点位缺失或距离为零';$('lengths').textContent=`Ft:${ft?.toFixed(2)??'缺失'} px Zy:${zy?.toFixed(2)??'缺失'} px`;
|
||
$('json').textContent=JSON.stringify({source:manual?'manual':'automatic',points,ft_px:ft,zy_px:zy,temporal_zygomatic_ratio:a,zygomatic_temporal_ratio:b,zygoma_expansion:matched,temple_hollowing:matched},null,2);draw()}
|
||
function draw(){const c=$('canvas'),ctx=c.getContext('2d');ctx.clearRect(0,0,c.width,c.height);if(!img)return;ctx.drawImage(img,0,0,c.width,c.height);const scale=c.width/img.naturalWidth;for(let i=0;i<4;i++){const p=points[keys[i]],q=points[keys[i^1]],color=i<2?'#ed397a':'#1685ff';if(!p)continue;ctx.strokeStyle=color;ctx.fillStyle=color;ctx.lineWidth=3;if(i%2===0&&q){ctx.beginPath();ctx.moveTo(p.x*scale,p.y*scale);ctx.lineTo(q.x*scale,q.y*scale);ctx.stroke()}ctx.beginPath();ctx.arc(p.x*scale,p.y*scale,7,0,Math.PI*2);ctx.fill();ctx.font='bold 14px sans-serif';const t=labels[i],x=Math.max(2,Math.min(c.width-ctx.measureText(t).width-4,p.x*scale+10)),y=Math.max(17,p.y*scale-12);ctx.strokeStyle='#172033';ctx.lineWidth=4;ctx.strokeText(t,x,y);ctx.fillStyle='white';ctx.fillText(t,x,y)}}
|
||
function position(e){const r=$('canvas').getBoundingClientRect();return{x:(e.clientX-r.left)/r.width*img.naturalWidth,y:(e.clientY-r.top)/r.height*img.naturalHeight}}
|
||
$('canvas').onpointerdown=e=>{if(!img||busy)return;const p=position(e),radius=18*img.naturalWidth/$('canvas').getBoundingClientRect().width;drag=keys.find(k=>points[k]&&dist(points[k],p)<radius);if(drag)$('canvas').setPointerCapture(e.pointerId)};
|
||
$('canvas').onpointermove=e=>{if(!drag)return;const p=position(e);points[drag]={x:Math.max(0,Math.min(img.naturalWidth-1,p.x)),y:Math.max(0,Math.min(img.naturalHeight-1,p.y))};manual=true;update()};$('canvas').onpointerup=$('canvas').onpointercancel=()=>drag=null;
|
||
$('reset').onclick=()=>{points=structuredClone(original);manual=false;update()};
|
||
$('run').onclick=async()=>{const file=$('file').files[0];if(!file)return;busy=true;$('run').disabled=true;$('reset').disabled=true;points={};original={};manual=false;img=null;update();$('status').textContent='正在检测人脸与发际线…';let url;try{url=URL.createObjectURL(file);const next=new Image();next.src=url;await next.decode();img=next;$('canvas').width=Math.min(1000,img.naturalWidth);$('canvas').height=Math.round($('canvas').width*img.naturalHeight/img.naturalWidth);draw();const form=new FormData();form.append('image_file',file);const response=await fetch('/detect',{method:'POST',body:form});const data=await response.json();if(!response.ok)throw Error(data.detail||'检测失败');const processed=new Image();processed.src=data.input_image_base64;await processed.decode();img=processed;$('canvas').width=Math.min(1000,img.naturalWidth);$('canvas').height=Math.round($('canvas').width*img.naturalHeight/img.naturalWidth);points=Object.fromEntries(keys.map(k=>[k,data.points[k]??null]));original=structuredClone(points);update();$('reset').disabled=false;$('status').textContent='检测完成。可拖动点位验证公式;发际线点受刘海和头发遮挡影响。'}catch(e){$('status').textContent='检测失败:'+e.message}finally{if(url)URL.revokeObjectURL(url);busy=false;$('run').disabled=false}};
|
||
</script></html> |