接口6 增加字段
This commit is contained in:
@@ -34,8 +34,12 @@
|
||||
.status.success { background: #d1fae5; color: #065f46; display: block; }
|
||||
.steps { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
|
||||
.step { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
||||
.step .cap { font-size: 13px; font-weight: 600; padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #f0f0f0; }
|
||||
.step .cap { font-size: 13px; font-weight: 600; padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #f0f0f0; display:flex; align-items:center; gap:8px; }
|
||||
.step .cap .badge { flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; min-width:24px; height:24px; padding:0 6px; border-radius:6px; background:#2563eb; color:#fff; font-size:13px; font-weight:700; }
|
||||
.step .cap .ttext { flex:1; }
|
||||
.step .cap small { color: #999; font-weight: 400; display:block; margin-top:2px; }
|
||||
.step .desc { font-size: 12px; line-height: 1.7; color: #4b5563; padding: 10px 12px; background: #f9fafb; border-bottom: 1px solid #f0f0f0; }
|
||||
.step .desc b { color:#1f2937; }
|
||||
.step img { width: 100%; display: block; background: #eee; cursor: zoom-in; }
|
||||
.step .noimg { padding: 30px; text-align: center; color: #ccc; font-size: 13px; }
|
||||
.big img { max-height: 520px; object-fit: contain; }
|
||||
@@ -147,8 +151,8 @@
|
||||
<div class="card">
|
||||
<h2 style="margin-top:0">🎯 最终结果</h2>
|
||||
<div class="steps" style="grid-template-columns: repeat(auto-fill, minmax(320px, 1fr))">
|
||||
<div class="step big"><div class="cap">输入原图</div><img id="finalInput"></div>
|
||||
<div class="step big"><div class="cap">最终结果</div><img id="finalOut"></div>
|
||||
<div class="step big"><div class="cap"><span class="badge" style="background:#9ca3af">输入</span><span class="ttext">输入原图</span></div><img id="finalInput"></div>
|
||||
<div class="step big"><div class="cap"><span class="badge">⑪</span><span class="ttext">最终结果<small>接口11 最终输出 = ⑩接缝融合结果</small></span></div><img id="finalOut"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -192,19 +196,55 @@ function flog(msg, level) {
|
||||
function clearLog() { FE_LOGS.length = 0; document.getElementById('logPanel').innerHTML = ''; }
|
||||
|
||||
const STEPS = [
|
||||
{ key: 'baseline_overlay', title: '①-a 发际线分割线', sub: '黄线=baseline' },
|
||||
{ key: 'upper_overlay', title: '①-b 上半区', sub: '青=baseline以上' },
|
||||
{ key: 'hair_seg_overlay', title: '①-c 头发分割', sub: '绿=头发像素' },
|
||||
{ key: 'top_fill_overlay', title: '①-d 填充到基线', sub: '蓝=top_fill(仅eroded/closed)' },
|
||||
{ key: 'closed_overlay', title: '①-e 闭合区域', sub: '紫=closed(仅eroded/closed)' },
|
||||
{ key: 'hairline_overlay', title: '①-f 头发内轮廓线', sub: '绿=头发内轮廓(额头弧+两侧到下颌),黄=baseline折线(仅pushed)' },
|
||||
{ key: 'pushed_overlay', title: '①-g 外推发际线', sub: '青=外推线(进头发push_cm),红=遮罩,绿=内轮廓(仅pushed)' },
|
||||
{ key: 'mask_overlay', title: '① 最终遮罩(叠加)', sub: '红=最终遮罩区' },
|
||||
{ key: 'mask', title: '① 纯遮罩', sub: '白=贴回区' },
|
||||
{ key: 'swap_raw', title: '② 生成全帧', sub: '换发型结果' },
|
||||
{ key: 'hard_paste', title: '③ 严格贴回', sub: '遮罩内=生成,外=原图' },
|
||||
{ key: 'alpha', title: '④ 融合权重', sub: '白=用生成图' },
|
||||
{ key: 'final', title: '④ 接缝融合(最终)', sub: '最终输出' },
|
||||
{ no: '①', key: 'baseline_overlay', title: '发际线分割线 baseline', sub: '黄线=baseline(眉峰水平连线)',
|
||||
desc: '用 468 点人脸关键点定位两侧眉峰,连成一条水平线作为「上半区」的底界。'
|
||||
+ '这条线把画面分成上下两半:以上是额头+头发(要处理),以下是五官(保持不动)。'
|
||||
+ '它是后续所有遮罩、外推、贴回的坐标基准线。图中的黄线就是 baseline,红点为 151 号中心点(径向外推的圆心)。' },
|
||||
{ no: '②', key: 'upper_overlay', title: '上半区 upper', sub: '青色=baseline 以上区域',
|
||||
desc: '把 baseline 以上到画面顶部的整片区域标为「上半区」。后续的头发分割、发际线外推、'
|
||||
+ '最终遮罩都只会在这个上半区内计算,确保下半张脸(眉、眼、鼻、嘴)永远不被改动。'
|
||||
+ '青色覆盖的就是上半区范围。' },
|
||||
{ no: '③', key: 'hair_seg_overlay', title: '头发分割', sub: '绿色=头发像素',
|
||||
desc: '用语义分割模型(默认 SegFormer,可选 BiSeNet)逐像素判断哪些是「头发」。'
|
||||
+ '绿色覆盖的就是被识别为头发的像素。这一步的目的是找到现有头发的边界,'
|
||||
+ '为下一步「发际线内轮廓」提供输入——发际线就长在头发区域的内边缘上。' },
|
||||
{ no: '④', key: 'hairline_overlay', title: '头发内轮廓线', sub: '绿=内轮廓(额头弧+两侧),黄=baseline',
|
||||
desc: '从头发分割结果里提取出「头发的内轮廓」:即头发与皮肤交界的那条线。'
|
||||
+ '它包含额头弧线(发际线本体)和两侧向下的鬓角轮廓。'
|
||||
+ '提取方式 hairline_edge=column 时按逐列(竖向)找头发最低点连成线;=contour 时用形态学轮廓。'
|
||||
+ '绿色折线就是提取出的内轮廓,黄色折线是 ① 的 baseline。这条内轮廓是外推发际线的起点。' },
|
||||
{ no: '⑤', key: 'pushed_overlay', title: '外推发际线 + 遮罩', sub: '青=外推线,红=遮罩,绿=内轮廓,红点=圆心',
|
||||
desc: '把 ④ 的发际线内轮廓「往头发方向(向头顶)推进 hairline_push_cm 厘米」得到一条新的外推线(青色)。'
|
||||
+ '外推方式:以眉心(151 点)为圆心做径向外推,推过的这段就是「要新长出头发的区域」。'
|
||||
+ '然后用【外推线(上界)到 baseline(下界)】之间的闭合区域作为最终遮罩(红色半透明)。'
|
||||
+ 'push_cm 越大,新发际线越靠上、生发区越大(默认 0.8cm)。' },
|
||||
{ no: '⑥', key: 'mask_overlay', title: '最终遮罩(叠加图)', sub: '红色=要重绘/贴回的区域',
|
||||
desc: '把 ⑤ 算出的遮罩叠回原图看效果。红色区域 = 需要被新生成的头发覆盖的位置(遮罩内),'
|
||||
+ '红色以外 = 保持原样不动(遮罩外)。这张图用来直观确认遮罩范围是否合理——'
|
||||
+ '理想情况是红色正好覆盖额头该生发的区域,不越界到眉毛或脸颊。' },
|
||||
{ no: '⑦', key: 'mask', title: '纯遮罩', sub: '白色=贴回区,黑色=保留区',
|
||||
desc: '同一张遮罩的纯黑白版本(无原图背景)。白色=贴回区,黑色=保留区。'
|
||||
+ '这张纯遮罩会作为 ext_mask 传给换发型服务,让 webui 精确地只在这个区域内重绘画头发。'
|
||||
+ '它的好处是不受背景图干扰,便于检查遮罩形状是否干净(无噪点、无破洞)。' },
|
||||
{ no: '⑧', key: 'swap_raw', title: '换发型生成', sub: 'change_hair 换该发际线类型后的整帧图',
|
||||
desc: '调用 change_hair 换发型服务(POST :8801/api/swapHair/v1),传入原图 + 发际线类型 ID(hairline_id)。'
|
||||
+ '服务用对应发型的 LoRA 模型(webui img2img,denoising_strength 控制生发强度)生成一张'
|
||||
+ '「同一个人、换成该发际线类型发型」的完整图。注意:这张图是整帧都变了,'
|
||||
+ '下一步会严格按遮罩只取额头那块,其余丢掉,保证五官不动。inpainting_fill/mask_blur 等参数控制这里的重绘方式。' },
|
||||
{ no: '⑨', key: 'hard_paste', title: '严格贴回(无融合)', sub: '遮罩内=生成图,遮罩外=原图',
|
||||
desc: '把 ⑧ 的生成图按 ⑥/⑦ 的遮罩「硬贴」回原图:遮罩内用生成图,遮罩外完全保留原图。'
|
||||
+ '这是没有做任何接缝处理的版本,因此遮罩边缘通常能看到明显的接缝/色差。'
|
||||
+ '它的作用是让你对比看出「融合前后的差别」——边缘接缝要靠下一步的融合来消除。' },
|
||||
{ no: '⑩', key: 'alpha', title: '融合权重 alpha', sub: '白=用生成图,黑=保留原图,灰=过渡',
|
||||
desc: '决定每个像素最终取多少比例的生成图。纯白(=1)完全用生成图,纯黑(=0)完全保留原图,'
|
||||
+ '灰色是两者按比例过渡。blend_method=multiband/two_stage 时是多层金字塔权重(过渡带较宽、自然);'
|
||||
+ '=feather/alpha_gradient 时是单层羽化(硬边缘软过渡)。这张图用来理解融合是怎么"渐变"地把新头发融进去的。' },
|
||||
{ no: '⑪', key: 'final', title: '接缝融合(最终输出)', sub: '接口11 最终结果',
|
||||
desc: '按 ⑩ 的权重,把生成图和原图加权融合,得到无接缝的最终图。这就是接口11 的最终返回结果。'
|
||||
+ 'blend_method 选择融合算法:multiband=多频段金字塔(分频段融合,大色差场景用 two_stage 先泊松调色再多频段);'
|
||||
+ 'seamless=泊松无缝克隆(梯度域自动调色);feather/alpha_gradient=简单羽化。'
|
||||
+ 'color_match=true 时融合前还做一次 Reinhard 颜色迁移消除整体色差(seamless/two_stage 自带调色故跳过)。'
|
||||
+ '本接口不含重绘,需要重绘(美颜/补发丝)见接口12。' },
|
||||
];
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
@@ -212,13 +252,16 @@ function setStatus(text, type) { const b = $('statusBar'); b.textContent = text;
|
||||
function pick(obj, name) { if (!obj) return null; return obj[name + '_url'] || obj[name + '_base64'] || null; }
|
||||
function zoom(src) { $('lightboxImg').src = src; $('lightbox').style.display = 'flex'; }
|
||||
|
||||
function stepCard(title, sub, src) {
|
||||
function stepCard(st, src) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'step';
|
||||
const hasImg = src && src.length > 50;
|
||||
const img = hasImg ? '<img src="' + src + '" onclick="zoom(this.src)">'
|
||||
: '<div class="noimg">无图(后端返回空)</div>';
|
||||
div.innerHTML = '<div class="cap">' + title + '<small>' + (sub||'') + (hasImg ? ' ('+src.length+'字符)' : '') + '</small></div>' + img;
|
||||
const badge = st.no ? '<span class="badge">' + st.no + '</span>' : '';
|
||||
const ttext = '<span class="ttext">' + (st.title||'') + '<small>' + (st.sub||'') + (hasImg ? ' ('+src.length+'字符)' : '') + '</small></span>';
|
||||
const desc = st.desc ? '<div class="desc">' + st.desc + '</div>' : '';
|
||||
div.innerHTML = '<div class="cap">' + badge + ttext + '</div>' + desc + img;
|
||||
return div;
|
||||
}
|
||||
|
||||
@@ -238,7 +281,7 @@ function renderResult(d) {
|
||||
const src = pick(s, st.key);
|
||||
const hasImg = src && src.length > 50;
|
||||
flog(' 渲染 ' + st.key + ': ' + (hasImg ? '有图(' + src.length + '字符)' : '无图'), hasImg ? 'info' : 'warn');
|
||||
grid.appendChild(stepCard(st.title, st.sub, src));
|
||||
grid.appendChild(stepCard(st, src));
|
||||
});
|
||||
flog('renderResult 完成', 'info');
|
||||
}
|
||||
@@ -364,6 +407,10 @@ async function downloadBackendLog() {
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤序号 → 对应英文 key 的映射,供按序号定位步骤
|
||||
const STEP_BY_NO = {};
|
||||
STEPS.forEach(s => { if (s.no) STEP_BY_NO[s.no] = s.key; });
|
||||
|
||||
// 参数联动(遮罩/融合已固定,无下拉联动)
|
||||
$('imageFile').addEventListener('change', function(){ if(this.files.length) flog('选择图片: ' + this.files[0].name, 'info'); });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user