"""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'''
{len(rows)}例颞颧比测试结果{len(rows)}例颞颧比/颧颞比测试结果
所有样本先使用接口52共用函数和配置扶正裁切,再测量。项目本地原始样本,按文件编号选择并剔除完全相同的图像;非人工标注验证集。共{len(rows)}例,有效{valid}例,无法计算{len(rows)-valid}例。颧弓外扩命中{hit}例,太阳穴凹陷命中{sum(r['temple_hollowing'] is True for r in rows)}例。
Ft:HL RR—HL LL(粉色,两端平均高度水平线);Zy:234—454(蓝色)。Ft ÷ Zy 小于 0.8 时,同时判定为颧弓外扩和太阳穴凹陷。缺失点不计入未命中。点击图片查看大图。
下载CSV · 完整JSON · 上传新照片测试
'''
cards=[]
for r in rows:
i=r['case'];cards.append(f'''
案例 {i:02d}
{html.escape(r['source'])}

Ft {fmt(r['ft_px'])} px · Zy {fmt(r['zy_px'])} px
| 颞颧比 Ft/Zy | {fmt(r['ft_zy'])} | <0.8 |
| 颧弓外扩 | {status(r['zygoma_expansion'])} |
| 颧颞比 Zy/Ft | {fmt(r['zy_ft'])} | 参考值 |
| 太阳穴凹陷 | {status(r['temple_hollowing'])} |
{html.escape(r['error'] or '')}
检测原始数据''')
(OUT/'index.html').write_text(head+''.join(cards)+'''
''')
print(f'DONE total={len(rows)} valid={valid} hit={hit}',flush=True)