feat: 接口5 新增 face_measure(复用接口1测量数值)+ 接口1 七眼 eye1~eye7
接口5(/api/v1/hairline/generate)在发际线结果基础上新增 data.face_measure, 复用接口1的四庭七眼测量数值(四庭/七眼 eye1~eye7/landmarks/姿态), 不含标注图;独立流程容错,测量失败时为 null 不影响发际线主结果。 重构:提取 _run_face_measure_data() 共用函数,接口1/6 改调它再补标注图, 行为不变(43 测试全过,错误码 1001/1003/1008 回归正常)。 接口1/5 七眼新增 eye1~eye7 从左到右 7 段宽度(cm),eye1/eye7 耳朵不可见 时为 null(保留键)。 测试页 test_interface5.html:移除点击切换卡片网格改为所有发型平铺, 新增四庭七眼测量卡片。前端接入页 integration.html / 接口文档同步更新。
This commit is contained in:
+119
-41
@@ -45,15 +45,11 @@
|
||||
.col-main { flex: 1.5; min-width: 0; }
|
||||
.col-side { flex: 1; min-width: 0; }
|
||||
|
||||
/* 方案网格 */
|
||||
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 14px; margin-top: 12px; }
|
||||
.result-card { background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06); overflow: hidden; cursor: pointer; transition: .15s; border: 3px solid transparent; }
|
||||
.result-card:hover { box-shadow: 0 2px 12px rgba(0,0,0,.12); }
|
||||
.result-card.selected { border-color: #2563eb; }
|
||||
.result-card .img-wrap { background: #222; min-height: 150px; max-height: 200px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.result-card .img-wrap img { max-width: 100%; max-height: 200px; object-fit: contain; }
|
||||
.result-card .info { padding: 10px 12px; font-size: 12px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.result-card .info .badge { background: #2563eb; color: #fff; padding: 1px 8px; border-radius: 10px; font-size: 11px; }
|
||||
/* 每个发型区块 */
|
||||
.hair-block { padding: 16px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.hair-block:first-child { padding-top: 4px; }
|
||||
.hair-block:last-child { border-bottom: none; padding-bottom: 4px; }
|
||||
.hair-block-title { font-size: 15px; font-weight: 700; color: #111827; margin-bottom: 10px; }
|
||||
|
||||
/* 三档 + 生发图 横向排列 */
|
||||
.level-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||||
@@ -68,6 +64,20 @@
|
||||
.coord-box .label { font-size: 11px; color: #9ca3af; margin-bottom: 4px; }
|
||||
.coord-box .value { font-weight: 700; font-size: 18px; color: #111827; }
|
||||
|
||||
/* 四庭七眼测量(face_measure) */
|
||||
.fm-section { margin-bottom: 18px; }
|
||||
.fm-section:last-child { margin-bottom: 0; }
|
||||
.fm-title { font-size: 13px; font-weight: 700; color: #374151; margin-bottom: 8px; }
|
||||
.fm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
|
||||
.fm-cell { background: #f9fafb; border-radius: 6px; padding: 8px 10px; border: 1px solid #f0f0f0; }
|
||||
.fm-cell .k { font-size: 11px; color: #9ca3af; margin-bottom: 2px; }
|
||||
.fm-cell .v { font-size: 15px; font-weight: 700; color: #111827; }
|
||||
.fm-cell .v .sub { font-size: 11px; font-weight: 500; color: #6b7280; margin-left: 4px; }
|
||||
.fm-cell.muted .v { color: #9ca3af; font-weight: 600; }
|
||||
.fm-tag { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 10px; margin-left: 6px; font-weight: 600; }
|
||||
.fm-tag.ok { background: #d1fae5; color: #065f46; }
|
||||
.fm-tag.warn { background: #fef3c7; color: #92400e; }
|
||||
|
||||
.json-panel { max-height: 550px; overflow: auto; }
|
||||
.json-content { padding: 14px 16px; font-family: "SF Mono", "Fira Code", monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
|
||||
|
||||
@@ -108,22 +118,22 @@
|
||||
|
||||
<div class="results-layout hidden" id="resultsArea">
|
||||
<div class="col-main">
|
||||
<!-- 预览 -->
|
||||
<!-- 四庭七眼测量(face_measure,复用接口1数值) -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span>📐 三档发际线叠图 + 生发图</span></div>
|
||||
<div class="card-body" style="text-align:center">
|
||||
<div id="previewArea"><span style="color:#9ca3af">← 点击下方发型卡片</span></div>
|
||||
<div class="coord-box">
|
||||
<div class="label">📍 最佳发际线中心点(best_hairline_center_point,首个选中发型 middle 档)— 原图像素坐标</div>
|
||||
<div class="card-header"><span>📐 四庭七眼测量(face_measure)</span><span style="font-weight:400;font-size:12px;color:#9ca3af">复用接口1测量数值</span></div>
|
||||
<div class="card-body"><div id="faceMeasureArea"><span style="color:#9ca3af">—</span></div></div>
|
||||
</div>
|
||||
<!-- 所有发型平铺 -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span>🎯 所有发型结果(三档 + 生发图)</span><span style="font-weight:400;font-size:12px;color:#9ca3af" id="resultCount">共 0 个发型</span></div>
|
||||
<div class="card-body">
|
||||
<div class="coord-box" style="margin-top:0;margin-bottom:16px">
|
||||
<div class="label">📍 最佳发际线中心点(best_hairline_center_point,首个发型 middle 档)— 原图像素坐标</div>
|
||||
<div class="value" id="centerPoint">—</div>
|
||||
</div>
|
||||
<div id="resultsGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 发型列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header"><span>🎯 选中发型</span><span style="font-weight:400;font-size:12px;color:#9ca3af">点击切换预览</span></div>
|
||||
<div class="card-body"><div class="results-grid" id="resultsGrid"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-side">
|
||||
<div class="card">
|
||||
@@ -198,9 +208,10 @@ async function submitTest() {
|
||||
_images = json.data.hairline_images || [];
|
||||
_center = json.data.best_hairline_center_point;
|
||||
$('centerPoint').textContent = _center ? '(' + _center.x + ', ' + _center.y + ')' : '—';
|
||||
$('resultCount').textContent = '共 ' + _images.length + ' 个发型';
|
||||
setStatus('✅ ' + _images.length + ' 个发型 × 三档 (' + _elapsed + 's)', 'success');
|
||||
renderGrid();
|
||||
if (_images.length) selectCard(0);
|
||||
renderFaceMeasure(json.data.face_measure);
|
||||
} else {
|
||||
setStatus('❌ (' + _elapsed + 's) code=' + json.code + ' ' + json.message, 'error');
|
||||
}
|
||||
@@ -211,38 +222,105 @@ async function submitTest() {
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
if (!_images.length) { $('resultsGrid').innerHTML = '<span style="color:#9ca3af">无数据</span>'; return; }
|
||||
let h = '';
|
||||
_images.forEach((img, i) => {
|
||||
h += '<div class="result-card'+(i===0?' selected':'')+'" onclick="selectCard('+i+',this)">'+
|
||||
'<div class="img-wrap"><img src="'+img.image_middle_url+'" alt="#'+img.order+'" loading="lazy"></div>'+
|
||||
'<div class="info"><span class="badge">#'+(img.order||'—')+' '+(img.hairline_type||'')+'</span>'+
|
||||
(img.grown_image_url?'<span style="font-size:11px;color:#7c3aed">🌱 生发</span>':'')+'</div></div>';
|
||||
});
|
||||
$('resultsGrid').innerHTML = h;
|
||||
}
|
||||
|
||||
function selectCard(idx, el) {
|
||||
document.querySelectorAll('.result-card').forEach(c => c.classList.remove('selected'));
|
||||
if (el) el.classList.add('selected');
|
||||
const it = _images[idx];
|
||||
const cell = (label, url) => url
|
||||
? '<div class="level-cell"><div class="level-label">'+label+'</div><img src="'+url+'" alt="'+label+'"></div>'
|
||||
: '<div class="level-cell"><div class="level-label">'+label+'</div><div class="level-none">无</div></div>';
|
||||
$('previewArea').innerHTML =
|
||||
'<div class="level-row">'+
|
||||
cell('middle', it.image_middle_url)+
|
||||
cell('high', it.image_high_url)+
|
||||
cell('low', it.image_low_url)+
|
||||
cell('🌱 生发图', it.grown_image_url)+
|
||||
_images.forEach(function(it) {
|
||||
h += '<div class="hair-block">' +
|
||||
'<div class="hair-block-title">#' + (it.order||'—') + ' ' + (it.hairline_type||'') +
|
||||
(it.grown_image_url ? ' <span style="font-size:11px;color:#7c3aed">含生发图</span>' : '') + '</div>' +
|
||||
'<div class="level-row">' +
|
||||
cell('middle', it.image_middle_url) +
|
||||
cell('high', it.image_high_url) +
|
||||
cell('low', it.image_low_url) +
|
||||
cell('生发图', it.grown_image_url) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
$('resultsGrid').innerHTML = h;
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
_images=[]; _origUrl=''; _center=null;
|
||||
$('resultsArea').classList.add('hidden'); $('statusBar').className='status hidden';
|
||||
$('imageFile').value=''; $('jsonContent').textContent=''; $('resultsGrid').innerHTML='';
|
||||
$('previewArea').innerHTML='<span style="color:#9ca3af">← 点击下方发型卡片</span>';
|
||||
$('centerPoint').textContent='—';
|
||||
$('resultCount').textContent = '共 0 个发型';
|
||||
$('faceMeasureArea').innerHTML = '<span style="color:#9ca3af">—</span>';
|
||||
}
|
||||
|
||||
// 四庭七眼测量渲染(face_measure,接口1数值)
|
||||
const _PCT = r => (r == null ? '' : '(' + (r * 100).toFixed(1) + '%)');
|
||||
const _FMT = v => (v == null ? '—' : Number(v).toFixed(2));
|
||||
function renderFaceMeasure(fm) {
|
||||
const box = $('faceMeasureArea');
|
||||
if (!fm) {
|
||||
box.innerHTML = '<div style="padding:24px;text-align:center;color:#9ca3af;font-size:14px">⚠️ face_measure = null(测量失败或未启用)</div>';
|
||||
return;
|
||||
}
|
||||
// 发际线来源 + 头部姿态
|
||||
const src = fm.hairline_source;
|
||||
const srcTag = src === 'segmentation'
|
||||
? '<span class="fm-tag ok">segmentation 真实分割</span>'
|
||||
: (src === 'estimated' ? '<span class="fm-tag warn">estimated 比例估算</span>' : '');
|
||||
let poseHtml = '—';
|
||||
if (fm.head_pose) {
|
||||
const p = fm.head_pose;
|
||||
poseHtml = 'yaw ' + _FMT(p.yaw) + '° pitch ' + _FMT(p.pitch) + '° roll ' + _FMT(p.roll) + '°';
|
||||
}
|
||||
let h = '<div class="coord-box" style="margin-top:0;margin-bottom:16px;display:flex;gap:32px;flex-wrap:wrap;align-items:center">'
|
||||
+ '<div><div class="label">脸总高</div><div class="value">' + _FMT(fm.face_total_height_cm) + ' cm</div></div>'
|
||||
+ '<div><div class="label">发际线来源</div><div class="value" style="font-size:15px">' + (src || '—') + srcTag + '</div></div>'
|
||||
+ '<div><div class="label">头部姿态</div><div class="value" style="font-size:14px">' + poseHtml + '</div></div>'
|
||||
+ '</div>';
|
||||
|
||||
// 四庭
|
||||
const fc = fm.four_courts || {};
|
||||
const fr = (fc.ratios || {});
|
||||
const courts = [
|
||||
['顶庭', fc.top_court_cm, fr.top_court],
|
||||
['上庭', fc.upper_court_cm, fr.upper_court],
|
||||
['中庭', fc.middle_court_cm, fr.middle_court],
|
||||
['下庭', fc.lower_court_cm, fr.lower_court],
|
||||
];
|
||||
h += '<div class="fm-section"><div class="fm-title">📏 四庭(纵向,自上而下)</div><div class="fm-grid">';
|
||||
courts.forEach(c => {
|
||||
h += '<div class="fm-cell"><div class="k">' + c[0] + '</div><div class="v">' + _FMT(c[1]) + ' cm<span class="sub">' + _PCT(c[2]).replace(/[()]/g, '') + '</span></div></div>';
|
||||
});
|
||||
h += '</div></div>';
|
||||
|
||||
// 七眼
|
||||
const se = fm.seven_eyes || {};
|
||||
const ser = (se.ratios || {});
|
||||
h += '<div class="fm-section"><div class="fm-title">👁 七眼(横向汇总)</div><div class="fm-grid">';
|
||||
[['单眼宽', se.eye_width_cm, ser.eye_width], ['脸宽', se.face_width_cm, null], ['两眼间距', se.inter_eye_distance_cm, ser.inter_eye_distance]].forEach(e => {
|
||||
h += '<div class="fm-cell"><div class="k">' + e[0] + '</div><div class="v">' + _FMT(e[1]) + ' cm' + (e[2] != null ? '<span class="sub">' + (e[2] * 100).toFixed(1) + '%</span>' : '') + '</div></div>';
|
||||
});
|
||||
h += '</div></div>';
|
||||
|
||||
// 七眼 7 段(从左到右)
|
||||
const segLabels = ['eye1 左耳外段', 'eye2 左脸颊段', 'eye3 左眼', 'eye4 两眼间距', 'eye5 右眼', 'eye6 右脸颊段', 'eye7 右耳外段'];
|
||||
h += '<div class="fm-section"><div class="fm-title">↔️ 七眼 7 段宽度(从左到右,cm)</div><div class="fm-grid" style="grid-template-columns:repeat(7,1fr)">';
|
||||
segLabels.forEach((lab, i) => {
|
||||
const v = se['eye' + (i + 1)];
|
||||
const muted = v == null ? ' muted' : '';
|
||||
h += '<div class="fm-cell' + muted + '"><div class="k">' + lab + '</div><div class="v">' + (v == null ? '—' : _FMT(v)) + '</div></div>';
|
||||
});
|
||||
h += '</div></div>';
|
||||
|
||||
// 关键点坐标
|
||||
const lm = fm.landmarks || {};
|
||||
const lmLabels = { hair_top: '头顶', hairline: '发际线', brow_center: '眉心', nose_bottom: '鼻翼下缘', chin_tip: '下巴尖' };
|
||||
h += '<div class="fm-section"><div class="fm-title">📍 关键分界点(原图像素坐标)</div><div class="fm-grid">';
|
||||
Object.keys(lmLabels).forEach(k => {
|
||||
const p = lm[k];
|
||||
h += '<div class="fm-cell"><div class="k">' + lmLabels[k] + '</div><div class="v" style="font-size:14px">' + (p ? '(' + p.x + ', ' + p.y + ')' : '—') + '</div></div>';
|
||||
});
|
||||
h += '</div></div>';
|
||||
|
||||
box.innerHTML = h;
|
||||
}
|
||||
|
||||
function copyJson() {
|
||||
|
||||
Reference in New Issue
Block a user