Files
hair/scripts/batch_face_ratios.py
T

65 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Run 20 local source-image cases against the ratio test server and build a report."""
import base64
import numpy as np
import csv, hashlib, html, json, math
from pathlib import Path
import cv2
import requests
ROOT=Path(__file__).resolve().parents[1]
OUT=ROOT/'static/face_ratio_cases'
OUT.mkdir(exist_ok=True)
files=sorted((ROOT/'image/girl_img').glob('*.jpg'),key=lambda p:int(p.stem.replace('girl','')))+[ROOT/'tests/fixtures/frontal.jpg',ROOT/'tests/fixtures/hard_longhair.jpg',ROOT/'image/hair_test.jpg']
files += sorted((ROOT/'tests/output/hairline_height_compare/inputs').glob('case_*.png'), key=lambda p:int(p.stem.split('_')[-1]))
keys=['hair_line_right_right','hair_line_left_left','face_edge_234','face_edge_454']
rows=[];seen=set()
for path in files:
image=cv2.imread(str(path))
if image is None:continue
digest=hashlib.sha256(image.tobytes()).hexdigest()
if digest in seen:continue
seen.add(digest)
idx=len(rows)+1
row={'case':idx,'source':str(path.relative_to(ROOT)),'ft_px':None,'zy_px':None,'ft_zy':None,'zy_ft':None,'zygoma_expansion':None,'temple_hollowing':None,'error':None}
try:
with path.open('rb') as f:r=requests.post('http://127.0.0.1:8190/detect',files={'image_file':(path.name,f,'image/jpeg')},timeout=180)
data=r.json();(OUT/f'{idx:02d}_response.json').write_text(json.dumps(data,ensure_ascii=False,indent=2))
if not r.ok:raise ValueError(data.get('detail',str(r.status_code)))
image=cv2.imdecode(np.frombuffer(base64.b64decode(data['input_image_base64'].split(',')[1]),np.uint8),cv2.IMREAD_COLOR)
row['framing']=data['framing']
points={k:data['points'].get(k) for k in keys};row['points']=points
for i in (0,2):
a,b=[points[keys[j]] for j in (i,i+1)]
if a and b:
length=math.hypot(a['x']-b['x'],a['y']-b['y']);row['ft_px' if i==0 else 'zy_px']=length
color=(140,50,240) if i==0 else (255,150,20)
cv2.line(image,(round(a['x']),round(a['y'])),(round(b['x']),round(b['y'])),color,3)
for j,p in ((i,a),(i+1,b)):
xy=(round(p['x']),round(p['y']));cv2.circle(image,xy,5,color,-1)
label=['HL RR','HL LL','234','454'][j]
cv2.putText(image,label,(max(0,xy[0]-30),max(18,xy[1]-12)),cv2.FONT_HERSHEY_SIMPLEX,.6,(0,0,0),4)
cv2.putText(image,label,(max(0,xy[0]-30),max(18,xy[1]-12)),cv2.FONT_HERSHEY_SIMPLEX,.6,(255,255,255),1)
if not row['ft_px'] or not row['zy_px']:raise ValueError('测量点缺失或距离为零')
row['ft_zy']=row['ft_px']/row['zy_px'];row['zy_ft']=row['zy_px']/row['ft_px']
matched = row['ft_zy'] < 0.8
row['zygoma_expansion'] = row['temple_hollowing'] = matched
except Exception as e:row['error']=str(e)
cv2.imwrite(str(OUT/f'{idx:02d}.jpg'),image)
rows.append(row);print(json.dumps(row,ensure_ascii=False),flush=True)
(OUT/'results.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2))
with (OUT/'results.csv').open('w',encoding='utf-8-sig',newline='') as f:
writer=csv.DictWriter(f,fieldnames=[k for k in rows[0] if k!='points'],extrasaction='ignore');writer.writeheader();writer.writerows(rows)
def fmt(v):return '—' if v is None else f'{v:.4f}'
def status(v):return '无法判断' if v is None else ('命中' if v else '未命中')
valid=sum(r['error'] is None for r in rows);hit=sum(r['zygoma_expansion'] is True for r in rows)
head=f'''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{len(rows)}例颞颧比测试结果</title><style>body{{font:15px system-ui;background:#f3f5f9;color:#203047;margin:0;padding:26px}}main{{max-width:1400px;margin:auto}}.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:18px}}article{{background:white;padding:16px;border-radius:12px}}img{{width:100%;height:340px;object-fit:contain;background:#eef1f5}}td{{padding:7px 2px}}p{{line-height:1.7}}.error{{color:#ad3426}}</style><main><h1>{len(rows)}例颞颧比/颧颞比测试结果</h1><p>所有样本先使用接口52共用函数和配置扶正裁切,再测量。项目本地原始样本,按文件编号选择并剔除完全相同的图像;非人工标注验证集。共{len(rows)}例,有效{valid}例,无法计算{len(rows)-valid}例。颧弓外扩命中{hit}例,太阳穴凹陷命中{sum(r['temple_hollowing'] is True for r in rows)}例。</p><p>Ft:HL RR—HL LL(粉色,两端平均高度水平线);Zy:234—454(蓝色)。Ft ÷ Zy 小于 0.8 时,同时判定为颧弓外扩和太阳穴凹陷。缺失点不计入未命中。点击图片查看大图。</p><p><a href="results.csv">下载CSV</a> · <a href="results.json">完整JSON</a> · <a href="http://localhost:8190/">上传新照片测试</a></p><section style="position:sticky;top:0;background:#f3f5f9;padding:14px 0;z-index:2"><label>按结果筛选 <select id="filter" style="padding:9px;font:inherit"><option value="all">全部</option><option value="zygoma">颧弓外扩命中</option><option value="temple">太阳穴凹陷命中</option><option value="neither">两项均未命中</option><option value="error">无法计算</option></select></label> <label>搜索案例 <input id="search" placeholder="编号或文件名" style="padding:9px;font:inherit"></label> <span id="count"></span></section><div class="grid">'''
cards=[]
for r in rows:
i=r['case'];cards.append(f'''<article data-zygoma="{str(r['zygoma_expansion'] is True).lower()}" data-temple="{str(r['temple_hollowing'] is True).lower()}" data-error="{str(r['error'] is not None).lower()}" data-search="{i:02d} {html.escape(r['source'],quote=True)}"><h2>案例 {i:02d}</h2><p>{html.escape(r['source'])}</p><a href="{i:02d}.jpg" target="_blank"><img loading="lazy" src="{i:02d}.jpg" alt="案例{i}测量标注"></a><p>Ft {fmt(r['ft_px'])} px · Zy {fmt(r['zy_px'])} px</p><table><tr><td>颞颧比 Ft/Zy</td><td>{fmt(r['ft_zy'])}</td><td><0.8</td></tr><tr><td>颧弓外扩</td><td>{status(r['zygoma_expansion'])}</td></tr><tr><td>颧颞比 Zy/Ft</td><td>{fmt(r['zy_ft'])}</td><td>参考值</td></tr><tr><td>太阳穴凹陷</td><td>{status(r['temple_hollowing'])}</td></tr></table><p class="error">{html.escape(r['error'] or '')}</p><a href="{i:02d}_response.json">检测原始数据</a></article>''')
(OUT/'index.html').write_text(head+''.join(cards)+'''</div></main><script>
const filter=document.getElementById('filter'), search=document.getElementById('search'), cards=[...document.querySelectorAll('article')];
function apply(){let n=0;for(const c of cards){const d=c.dataset,f=filter.value;const match=f==='all'||f==='zygoma'&&d.zygoma==='true'||f==='temple'&&d.temple==='true'||f==='error'&&d.error==='true'||f==='neither'&&d.error==='false'&&d.zygoma==='false'&&d.temple==='false';const show=match&&d.search.toLowerCase().includes(search.value.trim().toLowerCase());c.hidden=!show;if(show)n++}document.getElementById('count').textContent=`显示 ${n} / ${cards.length} 例`}
filter.onchange=apply;search.oninput=apply;apply();
</script>''')
print(f'DONE total={len(rows)} valid={valid} hit={hit}',flush=True)