Files
juzi/h5/assets/mock/build-fixture.py
T
Ubuntu ba3b346fce feat: 报告页 V2.2 检测可视化 + 评分引擎审计 + 真值回归/e2e 闭环
- server.js: face_maps 改为白名单 5 图层完整 dataURL 透传(REPORT_MAPS,按魔数判 mime),marks 坐标随 result 原样透传;实测 return_marks 不支持毛孔/黑头等扩展标记
- skin-score.js: 按需求文档 V2.2 逐字审计修正 9 处(黑眼圈 dark_circle_severity、oil_desc/spot_flag §7.2 拼接、skintone 中文映射、轮廓预警档前缀、痘痘 heavy_note、50+精华全文、综合文案数值格式),契约引用 V2.0→V2.2
- app.js/index.html/css: 报告页 6 分类检测可视化(轮廓=正脸实拍、皱纹=正脸压暗+皱纹增强线、光滑=正脸+色斑/痣/痤疮标记、肤色=色沉图、敏感=红区图、痘痘=正脸+闭口/痤疮标记),数据驱动 VIZ_CAT 配置,优雅降级
- h5/test/: Excel 两组三视图真值回归 84/84(含案例2 AY 列扁平化数据重建 fixture)
- h5/assets/mock/: 真实案例1 报告页 fixture(坐标与照片同比例缩放),?mock=1&screen=report 直渲
- e2e: Chrome 无头闭环 55/55(mock 全流程/直渲/真实 API 降级链路),截图存档 preview/e2e-20260926
- 自测: skin-score 83/83,fixture 冒烟 35/35
2026-09-26 22:35:02 +08:00

105 lines
4.5 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.
#!/usr/bin/env python3
# ─── 报告页可视化调试 fixture 生成器(仅本目录工具,需 Pillow) ───
# 数据来源:reference/facepp-skinanalyze/examples/ref_3view_raw.json(案例1 完整原始返回)
# + reference/facepp-skinanalyze/examples/images/C3_180.jpg(案例1 正脸 4000×3000)
# 产物:report-fixture.js → window.MOCK_FIXTURE = { result, face_rectangle, side, photos:[正脸dataURL,null,null] }
# 生产不变量:照片与 face_maps 统一缩到宽 1280,marks 坐标同比例缩放(坐标空间 = 照片空间)
# 用法:python3 build-fixture.py
import base64
import io
import json
import os
from PIL import Image
ROOT = os.path.dirname(os.path.abspath(__file__))
REF = os.path.join(ROOT, '..', '..', '..', 'reference', 'facepp-skinanalyze', 'examples')
TARGET_W = 1280
# 与服务端 REPORT_MAPS 白名单一致(h5/server.js)
MAP_KEYS = ['red_area', 'brown_area', 'texture_enhanced_lines', 'texture_enhanced_pores', 'texture_enhanced_blackheads']
def to_data_url(img, fmt):
buf = io.BytesIO()
if fmt == 'JPEG':
img.convert('RGB').save(buf, 'JPEG', quality=85)
mime = 'image/jpeg'
else:
img.save(buf, 'PNG', optimize=True)
mime = 'image/png'
return 'data:%s;base64,%s' % (mime, base64.b64encode(buf.getvalue()).decode('ascii'))
def scale_coords(node, s):
"""递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。"""
if isinstance(node, dict):
ks = node.keys()
if {'left', 'top', 'width', 'height'} <= ks and all(isinstance(node[k], (int, float)) for k in ('left', 'top', 'width', 'height')):
node['left'] = round(node['left'] * s)
node['top'] = round(node['top'] * s)
node['width'] = max(1, round(node['width'] * s))
node['height'] = max(1, round(node['height'] * s))
return
if {'x', 'y'} <= ks and isinstance(node['x'], (int, float)) and isinstance(node['y'], (int, float)):
node['x'] = round(node['x'] * s)
node['y'] = round(node['y'] * s)
return
for v in node.values():
scale_coords(v, s)
elif isinstance(node, list):
for v in node:
scale_coords(v, s)
def main():
raw = json.load(open(os.path.join(REF, 'ref_3view_raw.json'), encoding='utf-8'))
result = raw['result']
src_w, src_h = Image.open(os.path.join(REF, 'images', 'C3_180.jpg')).size
s = TARGET_W / src_w # 4000 → 1280:0.32
target_h = round(src_h * s)
# 正脸照片
photo = Image.open(os.path.join(REF, 'images', 'C3_180.jpg')).convert('RGB')
photo = photo.resize((TARGET_W, target_h), Image.LANCZOS)
photo_url = to_data_url(photo, 'JPEG')
# face_maps:白名单图层,同比例缩放;格式保持原样(jpeg 图层出 jpeg,透明叠加层出 png,与服务端魔数判定一致)
maps = {}
for k in MAP_KEYS:
b64 = result.get('face_maps', {}).get(k)
if not b64:
continue
im = Image.open(io.BytesIO(base64.b64decode(b64)))
fmt = im.format # resize 后 format 丢失,先取
im = im.resize((TARGET_W, target_h), Image.LANCZOS)
maps[k] = to_data_url(im, 'JPEG' if fmt == 'JPEG' else 'PNG')
# marks 坐标随照片同比例缩放(face_maps 已替换为缩放后 dataURL,不参与坐标缩放)
result['face_maps'] = maps
scale_coords(result, s)
face_rect = raw.get('face_rectangle')
if face_rect:
scale_coords(face_rect, s)
fixture = {
'result': result,
'face_rectangle': face_rect,
'side': {'left': raw.get('left_side_result'), 'right': raw.get('right_side_result')},
'photos': [photo_url, None, None] # 与 CAM_STEPS 同序:[0]正脸;侧脸图未纳入 fixture
}
out = os.path.join(ROOT, 'report-fixture.js')
with open(out, 'w', encoding='utf-8') as f:
f.write('/* 报告页可视化 mock fixture — 由 build-fixture.py 生成,勿手改\n')
f.write(' 来源:ref_3view_raw.json(案例1)+ C3_180.jpg;照片与 face_maps 统一宽 %d,marks 坐标同比例缩放(×%.2f)\n' % (TARGET_W, s))
f.write(' 仅在 ?mock=1 时由 index.html 按需注入,生产零额外请求 */\n')
f.write('window.MOCK_FIXTURE = ')
json.dump(fixture, f, ensure_ascii=False, separators=(',', ':'))
f.write(';\n')
print('[build-fixture] %s %.1f KB(照片 %d×%d,图层 %d 张)' % (
out, os.path.getsize(out) / 1024, TARGET_W, target_h, len(maps)))
if __name__ == '__main__':
main()