Files
hair/static/test_interface11.html
T
xsl 41bb164a52 接口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 对比报告生成
2026-07-11 22:23:57 +08:00

463 lines
23 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>接口11 — 发际线生发 测试页</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f5f5; color: #333; }
.container { max-width: 1240px; margin: 0 auto; padding: 24px; }
h1 { font-size: 22px; margin-bottom: 8px; }
.subtitle { color: #888; font-size: 13px; margin-bottom: 20px; line-height: 1.6; }
h2 { font-size: 16px; margin: 24px 0 12px; }
.card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 1px 4px rgba(0,0,0,.06); margin-bottom: 20px; }
.upload-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
input[type=file] { flex: 1; min-width: 200px; padding: 8px; border: 2px dashed #ddd; border-radius: 8px; cursor: pointer; }
.btn { padding: 10px 24px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; }
.btn-primary { background: #2563eb; color: #fff; }
.btn-primary:disabled { background: #93c5fd; cursor: not-allowed; }
.btn-outline { background: #fff; border: 1px solid #d1d5db; color: #374151; }
.hint { font-size: 12px; color: #9ca3af; margin-top: 8px; line-height: 1.6; }
.params { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 14px; margin-top: 16px; }
.pf { display: flex; flex-direction: column; gap: 4px; }
.pf label { font-size: 13px; font-weight: 600; }
.pf .desc { font-size: 11px; color: #9ca3af; font-weight: 400; }
.pf select, .pf input[type=number], .pf input[type=text] { padding: 8px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
.pf .row { display: flex; gap: 8px; align-items: center; }
.pf .row input[type=number] { width: 80px; }
.pf .row input[type=range] { flex: 1; }
.chk { display: flex; align-items: center; gap: 8px; }
.status { padding: 10px 16px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; display: none; }
.status.info { background: #dbeafe; color: #1e40af; display: block; }
.status.error { background: #fee2e2; color: #991b1b; display: block; }
.status.success { background: #d1fae5; color: #065f46; display: block; }
.metrics { display: flex; gap: 16px; flex-wrap: wrap; }
.metric { background: #f9fafb; border: 1px solid #eee; border-radius: 8px; padding: 10px 16px; min-width: 110px; }
.metric .label { font-size: 12px; color: #888; }
.metric .value { font-size: 17px; font-weight: 700; color: #111; }
.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 small { color: #999; font-weight: 400; display:block; margin-top:2px; }
.step img { width: 100%; display: block; background: #eee; cursor: zoom-in; }
.big img { max-height: 520px; object-fit: contain; }
.json-panel { max-height: 320px; overflow: auto; background: #1e1e1e; color: #d4d4d4; padding: 14px; border-radius: 8px; font: 12px/1.5 Consolas, Monaco, monospace; white-space: pre-wrap; word-break: break-all; }
.hidden { display: none; }
.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.85); display: none; align-items: center; justify-content: center; z-index: 50; cursor: zoom-out; }
.lightbox img { max-width: 95%; max-height: 95%; }
</style>
</head>
<body>
<div class="container">
<h1>接口11 — 发际线生发</h1>
<div class="subtitle">
接口9 头发遮罩 → change_hair 换发型(发际线类型 LoRA)→ 严格按遮罩贴回原图 → 接缝羽化融合。<br>
输入:发际线较高 / 头发稀少的正脸图 + 发际线类型 ID。输出:同一个人同一发型、压低发际线后的图。每一步可视化,所有可调参数都在下面。
</div>
<div class="card">
<div class="upload-row">
<input type="file" id="imageFile" accept="image/*">
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交测试</button>
<button class="btn btn-outline" onclick="clearResults()">清空</button>
</div>
<div class="upload-row" style="margin-top:10px">
<label style="font-size:13px; font-weight:600; white-space:nowrap">X-Internal-Token</label>
<input type="text" id="token" placeholder="直连 worker 需填;走网关可留空" value="dev-shared-secret-2026"
style="flex:1; min-width:200px; padding:8px; border:1px solid #ddd; border-radius:8px">
</div>
<div class="params">
<div class="pf">
<label>gen_backend <span class="desc">生成后端</span></label>
<select id="genBackend">
<option value="swaphair">swaphair(换发型LoRA,应用发际线类型)</option>
<option value="hairgrow">hairgrow(区域生发inpaint,压低发际线)</option>
</select>
</div>
<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>
<option value="chang_tuoyuan">chang_tuoyuan(椭圆)</option>
<option value="chang_bolang">chang_bolang(波浪)</option>
<option value="chang_xinxing">chang_xinxing(心形)</option>
<option value="chang_huaban">chang_huaban(花瓣)</option>
</select>
</div>
<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>
<option value="as_is">as_is(不改换发型,贴回再裁)</option>
</select>
</div>
<div class="pf">
<label>seg_model <span class="desc">头发分割模型</span></label>
<select id="segModel">
<option value="segformer">segformer(默认)</option>
<option value="bisenet">bisenet</option>
</select>
</div>
<div class="pf">
<label>mask_type <span class="desc">遮罩类型</span></label>
<select id="maskType">
<option value="eroded">eroded(外缘内缩,默认)</option>
<option value="closed">closed(闭合区域,未内缩)</option>
<option value="pushed">pushed(发际线外推)</option>
</select>
</div>
<div class="pf">
<label>erode_cm <span class="desc">外缘内缩距离(cm)</span></label>
<div class="row">
<input type="number" id="erodeCm" min="0" max="5" step="0.1" value="1.2">
<input type="range" id="erodeRange" min="0" max="3" step="0.1" value="1.2">
</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" 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">
<input type="range" id="featherRange" min="1" max="60" step="1" value="15">
</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">
<input type="number" id="edgeErodePx" min="0" max="40" step="1" value="3">
<input type="range" id="edgeErodeRange" min="0" max="30" step="1" value="3">
</div>
</div>
<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">
<input type="range" id="denoiseRange" min="0.2" max="1.0" step="0.05" value="0.6">
</div>
</div>
<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">
<input type="range" id="hgStrengthRange" min="0.2" max="1.0" step="0.05" value="0.75">
</div>
</div>
<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>
</div>
<div class="hint">
发际线类型 ID 必须是 change_hair 已训练的 hair_id(下拉为已有的 5 个)。参数改动后点「提交测试」重新计算,设置自动记住。<br>
换发型走 GPU + SD img2img,单次约 10~15sis_hr 更慢。
</div>
</div>
<div class="status hidden" id="statusBar"></div>
<div id="resultsArea" class="hidden">
<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">最终结果 <small>按遮罩贴回 + 接缝融合</small></div><img id="finalOut"></div>
</div>
</div>
<div class="card">
<h2 style="margin-top:0">📏 参数与指标</h2>
<div class="metrics" id="metricsBar"></div>
</div>
<div class="card">
<h2 style="margin-top:0">🪜 分步可视化</h2>
<div class="steps" id="stepsGrid"></div>
</div>
<div class="card">
<h2 style="margin-top:0; display:flex; justify-content:space-between; align-items:center">
<span>原始 JSON</span>
<button class="btn btn-outline" style="padding:6px 14px; font-size:13px" onclick="copyJson()">复制</button>
</h2>
<div class="json-panel" id="jsonContent"></div>
</div>
</div>
</div>
<div class="lightbox" id="lightbox" onclick="this.style.display='none'"><img id="lightboxImg" alt=""></div>
<script>
const API_BASE = window.location.origin;
const ENDPOINT = '/api/v1/hairline/grow';
// 分步展示的图(按算法文档 4 步;key 前缀、标题、副标题)
const STEPS = [
// 遮罩计算全过程(接口9 子步骤,蓝→红)
{ key: 'baseline_overlay', title: '①-a 发际线分割线', sub: '黄线=baseline151中心点标红、其余标绿)' },
{ 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); }
function setStatus(text, type) { const b = $('statusBar'); b.textContent = text; b.className = 'status ' + type; }
// 网关把 *_base64 改写为 *_url;直连 worker 保留 *_base64。两者都兼容。
function pick(obj, name) { if (!obj) return null; return obj[name + '_url'] || obj[name + '_base64'] || null; }
function stepCard(title, sub, src) {
const div = document.createElement('div');
div.className = 'step';
const img = src ? '<img src="' + src + '" alt="' + title + '" onclick="zoom(this.src)">'
: '<div style="padding:30px;text-align:center;color:#bbb;font-size:13px">无图</div>';
div.innerHTML = '<div class="cap">' + title + '<small>' + (sub || '') + '</small></div>' + img;
return div;
}
function zoom(src) { $('lightboxImg').src = src; $('lightbox').style.display = 'flex'; }
function renderMetrics(d) {
const sz = d.image_size || {}, t = d.timings_ms || {};
const items = [
{ label: 'gen_backend', value: d.gen_backend },
{ label: 'hairline_id', value: d.hairline_id },
{ label: 'swap_mode', value: d.swap_mode },
{ 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' },
{ label: '遮罩像素', value: d.mask_pixels },
{ label: '尺寸', value: (sz.width||'?') + '×' + (sz.height||'?') },
{ label: '耗时(遮罩/换发/融合)', value: (t.mask||0) + '/' + (t.swap||0) + '/' + (t.blend||0) + 'ms' },
];
$('metricsBar').innerHTML = items.map(m =>
'<div class="metric"><div class="label">' + m.label + '</div><div class="value">' + (m.value ?? '—') + '</div></div>'
).join('');
}
function renderResult(d) {
const s = d.steps || {};
$('finalInput').src = pick(s, 'input') || '';
$('finalInput').onclick = function(){ zoom(this.src); };
$('finalOut').src = pick(s, 'final') || '';
$('finalOut').onclick = function(){ zoom(this.src); };
renderMetrics(d);
const grid = $('stepsGrid'); grid.innerHTML = '';
// 按 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() {
const file = $('imageFile').files[0];
if (!file) { setStatus('请先选择一张图片', 'error'); return; }
const t0 = performance.now();
const btn = $('submitBtn');
btn.disabled = true; btn.textContent = '⏳ 请求中...';
setStatus('正在请求(换发型走 GPU,约 10~15s...', 'info');
$('resultsArea').classList.add('hidden');
const form = new FormData();
form.append('image_file', file);
form.append('hairline_id', $('hairlineId').value.trim());
form.append('gen_backend', $('genBackend').value);
form.append('hairgrow_strength', $('hgStrength').value || '0.75');
form.append('is_hr', $('isHr').checked ? 'true' : 'false');
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 = {};
const tok = $('token').value.trim();
if (tok) headers['X-Internal-Token'] = tok;
const resp = await fetch(API_BASE + ENDPOINT, { method: 'POST', headers, body: form });
const json = await resp.json();
const dt = ((performance.now() - t0) / 1000).toFixed(2);
$('jsonContent').textContent = JSON.stringify(json, (k, v) =>
(typeof v === 'string' && v.length > 120) ? v.slice(0, 60) + '…(' + v.length + ')' : v, 2);
$('resultsArea').classList.remove('hidden');
if (json.code === 0) {
setStatus('✅ 成功 (' + dt + 's)', 'success');
renderResult(json.data);
} else {
setStatus('❌ 业务错误 (' + dt + 's) — code ' + json.code + '' + json.message, 'error');
}
} catch (err) {
setStatus('❌ 网络错误: ' + err.message, 'error');
$('jsonContent').textContent = 'Error: ' + err.message;
$('resultsArea').classList.remove('hidden');
} finally {
btn.disabled = false; btn.textContent = '🚀 提交测试';
}
}
function clearResults() {
$('resultsArea').classList.add('hidden');
$('statusBar').className = 'status hidden';
$('imageFile').value = '';
$('jsonContent').textContent = '';
}
function copyJson() {
navigator.clipboard.writeText($('jsonContent').textContent).then(() => {
const b = event.target, o = b.textContent; b.textContent = '✅ 已复制'; setTimeout(() => b.textContent = o, 1500);
});
}
// ---- 参数持久化 + 联动 ----
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);
function sync(from) {
let v = parseFloat(from.value); if (isNaN(v)) v = 0;
num.value = v; rng.value = Math.min(parseFloat(rng.max), Math.max(parseFloat(rng.min), v));
saveField(numId);
}
num.addEventListener('input', () => sync(num));
rng.addEventListener('input', () => sync(rng));
}
document.addEventListener('DOMContentLoaded', () => {
const dz = $('imageFile');
dz.addEventListener('change', () => { if (dz.files.length) setStatus('已选择: ' + dz.files[0].name, 'info'); });
FIELDS.forEach(id => {
try { const s = localStorage.getItem('if11_' + id); if (s !== null) $(id).value = s; } catch(e){}
$(id).addEventListener('change', () => saveField(id));
});
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>
</html>