接口11/12:新增发际线外推遮罩(pushed)模式 + multiband融合修复 + 调试日志 + 对比报告脚本
发际线生发遮罩算法(mask_type=pushed): - _extract_hairline:提取头发/皮肤交界线(逐列头发下沿),用 baseline 水平 y 线截断(无竖线) - _pushed_mask:以眉心(151点)为圆心逐点径向外推 push_cm,与 baseline 组闭合区域 - 径向归并锯齿用插值填补,避免遮罩碎裂 - pushed 模式过程可视化(①-f 交界线 / ①-g 外推+遮罩),eroded/closed 不展示无关步骤 multiband 金字塔融合修复(hairline_grow.py): - mb_levels 按层数膨胀外缘 keep 区,让过渡带随层数变宽(旧硬二值钳回导致 mb_levels 形同虚设) 接口12 grow_v2(固定参数精简版): - 固定 multiband/mb_levels=5/erode_cm=0.6,仅返回 final_base64 - 支持 mask_type=pushed + hairline_push_cm/hairline_edge 调试支持: - 调试页 test_interface11_debug.html(前后端日志面板 + 下载日志按钮) - hairline_grow.log 全链路日志(按 rid 关联),/api/v1/debug/hairline_log 下载接口 - 遮罩计算过程可视化(baseline/upper/头发分割/交界线/外推/最终遮罩) 文档与脚本: - docs/发际线生发遮罩算法_pushed模式.md 算法说明 - scripts/batch_grow_v2.py 批量调用、gen_report_hairline_v2.py 对比报告生成
This commit is contained in:
+100
-14
@@ -85,7 +85,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>发际线类型 ID <span class="desc">= change_hair hair_id(仅swaphair)</span></label>
|
||||
<select id="hairlineId">
|
||||
<option value="chang_zhixian">chang_zhixian(直线)</option>
|
||||
@@ -96,7 +96,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>swap_mode <span class="desc">换发型取图模式</span></label>
|
||||
<select id="swapMode">
|
||||
<option value="ext_mask">ext_mask(改造换发型,用接口9遮罩重绘)</option>
|
||||
@@ -117,6 +117,7 @@
|
||||
<select id="maskType">
|
||||
<option value="eroded">eroded(外缘内缩,默认)</option>
|
||||
<option value="closed">closed(闭合区域,未内缩)</option>
|
||||
<option value="pushed">pushed(发际线外推)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -128,16 +129,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="mask:pushed">
|
||||
<label>hairline_push_cm <span class="desc">发际线外推距离(仅pushed)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="hairlinePushCm" min="0" max="3" step="0.1" value="1.0">
|
||||
<input type="range" id="hairlinePushRange" min="0" max="3" step="0.1" value="1.0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="mask:pushed">
|
||||
<label>hairline_edge <span class="desc">发际线提取方式(仅pushed)</span></label>
|
||||
<select id="hairlineEdge">
|
||||
<option value="column">column(逐列最低点,稳定)</option>
|
||||
<option value="contour">contour(形态学轮廓,精确)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>blend_method <span class="desc">接缝融合算法</span></label>
|
||||
<select id="blendMethod">
|
||||
<option value="feather">feather(高斯羽化,默认)</option>
|
||||
<option value="alpha_gradient">alpha_gradient(距离变换内渐变)</option>
|
||||
<option value="seamless">seamless(泊松无缝克隆)</option>
|
||||
<option value="multiband">multiband(多频段金字塔融合)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<div class="pf" data-show="blend:feather,alpha_gradient,multiband">
|
||||
<label>color_match <span class="desc">颜色校正前置</span></label>
|
||||
<div class="chk"><input type="checkbox" id="colorMatch"><span style="font-size:13px">融合前对生成图做 Reinhard 颜色校正(消除整体色差,seamless 自动跳过)</span></div>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="blend:feather,alpha_gradient">
|
||||
<label>feather_px <span class="desc">羽化/渐变过渡宽度(px)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="featherPx" min="1" max="80" step="1" value="15">
|
||||
@@ -145,6 +168,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf" data-show="blend:multiband">
|
||||
<label>mb_levels <span class="desc">多频段金字塔层数(仅multiband)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="mbLevels" min="2" max="6" step="1" value="5">
|
||||
<input type="range" id="mbLevelsRange" min="2" max="6" step="1" value="5">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<label>edge_erode_px <span class="desc">贴图前遮罩内缩(px)</span></label>
|
||||
<div class="row">
|
||||
@@ -153,7 +184,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>denoising_strength <span class="desc">换发型重绘强度(仅swaphair)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="denoise" min="0.2" max="1.0" step="0.05" value="0.6">
|
||||
@@ -161,7 +192,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<div class="pf" data-show="gen:hairgrow">
|
||||
<label>hairgrow_strength <span class="desc">区域生发强度(仅hairgrow)</span></label>
|
||||
<div class="row">
|
||||
<input type="number" id="hgStrength" min="0.2" max="1.0" step="0.05" value="0.75">
|
||||
@@ -169,7 +200,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pf">
|
||||
<div class="pf" data-show="gen:swaphair">
|
||||
<label>is_hr <span class="desc">高清模式</span></label>
|
||||
<div class="chk"><input type="checkbox" id="isHr"><span style="font-size:13px">1152×1536(否则 576×768)</span></div>
|
||||
</div>
|
||||
@@ -219,12 +250,22 @@ const ENDPOINT = '/api/v1/hairline/grow';
|
||||
|
||||
// 分步展示的图(按算法文档 4 步;key 前缀、标题、副标题)
|
||||
const STEPS = [
|
||||
{ key: 'mask_overlay', title: '① 接口9 最终遮罩(叠加)', sub: '红=遮罩区(含额头,外缘内缩)' },
|
||||
{ key: 'mask', title: '① 纯遮罩', sub: '白=生成/贴回区(传给换发型作遮罩 & 贴回)' },
|
||||
{ key: 'swap_raw', title: '② 生成全帧', sub: 'change_hair 生成,已对齐原图' },
|
||||
{ key: 'hard_paste', title: '③ 严格按遮罩贴回', sub: '遮罩内=生成,遮罩外=原图,无融合' },
|
||||
{ key: 'alpha', title: '④ 融合权重 alpha', sub: '羽化/渐变,白=用生成图' },
|
||||
{ key: 'final', title: '④ 接缝融合(最终)', sub: '遮罩边缘自然过渡' },
|
||||
// 遮罩计算全过程(接口9 子步骤,蓝→红)
|
||||
{ key: 'baseline_overlay', title: '①-a 发际线分割线', sub: '黄线=baseline(151中心点标红、其余标绿)' },
|
||||
{ key: 'upper_overlay', title: '①-b 分割线上半区', sub: '青=baseline 以上区域(裁剪范围)' },
|
||||
{ key: 'hair_seg_overlay', title: '①-c 头发分割', sub: '绿=分割模型原始头发像素(segformer/bisenet)' },
|
||||
{ key: 'top_fill_overlay', title: '①-d 填充到基线', sub: '蓝=头发向下填充含额头(延伸到图底,未裁剪)' },
|
||||
{ key: 'closed_overlay', title: '①-e 闭合区域', sub: '紫=top_fill ∩ 上半区(头发+额头,底=基线)' },
|
||||
// pushed 模式专有(非 pushed 模式无图)
|
||||
{ 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: 'change_hair 生成,已对齐原图' },
|
||||
{ key: 'hard_paste', title: '③ 严格按遮罩贴回', sub: '遮罩内=生成,遮罩外=原图,无融合' },
|
||||
{ key: 'alpha', title: '④ 融合权重 alpha', sub: '羽化/渐变,白=用生成图' },
|
||||
{ key: 'final', title: '④ 接缝融合(最终)', sub: '遮罩边缘自然过渡' },
|
||||
];
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
@@ -252,6 +293,10 @@ function renderMetrics(d) {
|
||||
{ label: 'seg_model', value: d.seg_model },
|
||||
{ label: 'mask_type', value: d.mask_type },
|
||||
{ label: 'blend', value: d.blend_method },
|
||||
{ label: 'color_match', value: d.color_match },
|
||||
{ label: 'mb_levels', value: d.mb_levels },
|
||||
{ label: 'hairline_push', value: d.hairline_push_cm + 'cm' },
|
||||
{ label: 'hairline_edge', value: d.hairline_edge },
|
||||
{ label: 'denoise', value: d.denoising_strength },
|
||||
{ label: 'px_per_cm', value: d.px_per_cm },
|
||||
{ label: '内缩', value: d.erode_cm + 'cm/' + d.erode_px + 'px' },
|
||||
@@ -272,7 +317,14 @@ function renderResult(d) {
|
||||
$('finalOut').onclick = function(){ zoom(this.src); };
|
||||
renderMetrics(d);
|
||||
const grid = $('stepsGrid'); grid.innerHTML = '';
|
||||
STEPS.forEach(st => grid.appendChild(stepCard(st.title, st.sub, pick(s, st.key))));
|
||||
// 按 mask_type 过滤步骤:pushed 只显示 baseline/upper/头发分割/发际线/外推;
|
||||
// eroded/closed 只显示 baseline/upper/头发分割/填充/闭合,不显示发际线/外推。
|
||||
const mt = d.mask_type;
|
||||
const showKeys = new Set(['baseline_overlay','upper_overlay','hair_seg_overlay','mask_overlay','mask','swap_raw','hard_paste','alpha','final']);
|
||||
if (mt === 'pushed') { showKeys.add('hairline_overlay'); showKeys.add('pushed_overlay'); }
|
||||
else { showKeys.add('top_fill_overlay'); showKeys.add('closed_overlay'); }
|
||||
STEPS.filter(st => showKeys.has(st.key)).forEach(st =>
|
||||
grid.appendChild(stepCard(st.title, st.sub, pick(s, st.key))));
|
||||
}
|
||||
|
||||
async function submitTest() {
|
||||
@@ -293,11 +345,19 @@ async function submitTest() {
|
||||
form.append('seg_model', $('segModel').value);
|
||||
form.append('mask_type', $('maskType').value);
|
||||
form.append('erode_cm', $('erodeCm').value || '1.2');
|
||||
setStatus('正在请求 mask_type=' + $('maskType').value
|
||||
+ ' hairline_push_cm=' + ($('hairlinePushCm').value||'1.0')
|
||||
+ ' hairline_edge=' + $('hairlineEdge').value
|
||||
+ '(换发型走 GPU,约 10~15s)...', 'info');
|
||||
form.append('swap_mode', $('swapMode').value);
|
||||
form.append('blend_method', $('blendMethod').value);
|
||||
form.append('feather_px', $('featherPx').value || '15');
|
||||
form.append('edge_erode_px', $('edgeErodePx').value || '3');
|
||||
form.append('denoising_strength', $('denoise').value || '0.6');
|
||||
form.append('color_match', $('colorMatch').checked ? 'true' : 'false');
|
||||
form.append('mb_levels', $('mbLevels').value || '5');
|
||||
form.append('hairline_push_cm', $('hairlinePushCm').value || '1.0');
|
||||
form.append('hairline_edge', $('hairlineEdge').value);
|
||||
|
||||
try {
|
||||
const headers = {};
|
||||
@@ -337,7 +397,7 @@ function copyJson() {
|
||||
}
|
||||
|
||||
// ---- 参数持久化 + 联动 ----
|
||||
const FIELDS = ['token','genBackend','hairlineId','swapMode','segModel','maskType','erodeCm','blendMethod','featherPx','edgeErodePx','denoise','hgStrength'];
|
||||
const FIELDS = ['token','genBackend','hairlineId','swapMode','segModel','maskType','erodeCm','blendMethod','featherPx','edgeErodePx','denoise','hgStrength','mbLevels','hairlinePushCm','hairlineEdge'];
|
||||
function saveField(id) { try { localStorage.setItem('if11_' + id, $(id).type === 'checkbox' ? ($(id).checked?'1':'0') : $(id).value); } catch(e){} }
|
||||
function linkNumRange(numId, rngId) {
|
||||
const num = $(numId), rng = $(rngId);
|
||||
@@ -359,17 +419,43 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
try { $('isHr').checked = localStorage.getItem('if11_isHr') === '1'; } catch(e){}
|
||||
$('isHr').addEventListener('change', () => saveField('isHr'));
|
||||
try { $('colorMatch').checked = localStorage.getItem('if11_colorMatch') === '1'; } catch(e){}
|
||||
$('colorMatch').addEventListener('change', () => saveField('colorMatch'));
|
||||
linkNumRange('erodeCm','erodeRange');
|
||||
linkNumRange('featherPx','featherRange');
|
||||
linkNumRange('edgeErodePx','edgeErodeRange');
|
||||
linkNumRange('denoise','denoiseRange');
|
||||
linkNumRange('hgStrength','hgStrengthRange');
|
||||
linkNumRange('mbLevels','mbLevelsRange');
|
||||
linkNumRange('hairlinePushCm','hairlinePushRange');
|
||||
// 初始化滑块值
|
||||
$('erodeRange').value = Math.min(3, parseFloat($('erodeCm').value)||1.2);
|
||||
$('featherRange').value = Math.min(60, parseFloat($('featherPx').value)||15);
|
||||
$('edgeErodeRange').value = Math.min(30, parseFloat($('edgeErodePx').value)||3);
|
||||
$('denoiseRange').value = Math.min(1.0, parseFloat($('denoise').value)||0.6);
|
||||
$('hgStrengthRange').value = Math.min(1.0, parseFloat($('hgStrength').value)||0.75);
|
||||
$('mbLevelsRange').value = Math.min(6, Math.max(2, parseFloat($('mbLevels').value)||5));
|
||||
$('hairlinePushRange').value = Math.min(3, parseFloat($('hairlinePushCm').value)||1.0);
|
||||
|
||||
// 参数联动:按 gen_backend / blend_method 显隐无用参数(data-show 声明)
|
||||
function applyVisibility() {
|
||||
const gen = $('genBackend').value, bm = $('blendMethod').value, mt = $('maskType').value;
|
||||
document.querySelectorAll('.pf[data-show]').forEach(el => {
|
||||
const conds = el.dataset.show.split(',').map(s => s.trim());
|
||||
let vis = true;
|
||||
conds.forEach(c => {
|
||||
const [k, v] = c.split(':');
|
||||
if (k === 'gen' && v !== gen) vis = false;
|
||||
if (k === 'blend' && v !== bm) vis = false;
|
||||
if (k === 'mask' && v !== mt) vis = false;
|
||||
});
|
||||
el.style.display = vis ? '' : 'none';
|
||||
});
|
||||
}
|
||||
$('genBackend').addEventListener('change', applyVisibility);
|
||||
$('blendMethod').addEventListener('change', applyVisibility);
|
||||
$('maskType').addEventListener('change', applyVisibility);
|
||||
applyVisibility();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user