feat: 接口2/5 发际线叠图改为透明 PNG(仅曲线),生发图不变
接口2(/api/v1/hair/grow)的 image_url 和接口5(/api/v1/hairline/generate) 的 image_middle/high/low_url 从「原图+白线合成 JPG」改为「透明底 PNG(仅含 发际线曲线)」,前端需叠加原图显示。grown_image_url 生发图保持不变(ComfyUI 完整人像照片)。 实现: - app.py 新增 _rgba_png_b64() 编码 RGBA 透明层为 PNG base64(保留 alpha) - hairline/service.py 接口2/5 改用 build_overlay_layer(返回 RGBA 透明层) 替代 render_hairline_overlay(合成到原图) - 网关零改动:rewrite_base64_to_url 已按 \x89PNG 魔数嗅探落盘为 .png 测试页:test_interface2/5.html 改为「原图打底 + 透明PNG 绝对定位叠加」显示 (复用 .img-stack 结构,固定叠加无开关)。 文档:接口文档.md / integration.html 更新接口2/5 图片字段说明。 测试:43 passed,接口2 image_base64 改断言为 PNG 魔数,接口5 三档叠图同。
This commit is contained in:
@@ -146,12 +146,14 @@ const { code, data } = await res.json();
|
||||
<p style="margin-top:12px"><strong>data.results[] 元素</strong></p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td><code>image_url</code></td><td>string</td><td>发际线叠加预览图(曲线叠在原图上)</td></tr>
|
||||
<tr><td><code>grown_image_url</code></td><td>string</td><td>生发后效果图(ComfyUI/Flux「植发3个月」)⚠ 可空</td></tr>
|
||||
<tr><td><code>image_url</code></td><td>string</td><td>发际线曲线<strong>透明 PNG</strong>(仅白色曲线,透明底,需叠加原图显示)</td></tr>
|
||||
<tr><td><code>grown_image_url</code></td><td>string</td><td>生发后效果图(ComfyUI/Flux「植发3个月」完整人像)⚠ 可空</td></tr>
|
||||
<tr><td><code>hairline_type</code></td><td>string</td><td>发际线类型 key</td></tr>
|
||||
<tr><td><code>order</code></td><td>int</td><td>排序(1=最佳,当前按贴图顺序)</td></tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:12px;font-size:12px;color:#64748b">💡 <code>image_url</code> 是透明底 PNG,前端需用绝对定位叠加到原图上显示(参考 <a href="/static/test_interface2.html">测试页</a> 的 <code>.img-stack</code> 叠加结构)。</p>
|
||||
|
||||
<p style="margin-top:8px;font-size:12px;color:#64748b">
|
||||
Female 5 种:ellipse/flower/heart/straight/wave |
|
||||
Male 4 种:ellipse/m/straight/inverse_arc<br>
|
||||
@@ -233,7 +235,7 @@ console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保
|
||||
<p style="margin-top:12px"><strong>data 字段</strong></p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td><code>hairline_images[]</code></td><td>object[]</td><td>选中发型列表,每项含 <code>hairline_type</code>、<code>image_middle_url</code>/<code>image_high_url</code>/<code>image_low_url</code> 三档叠图、<code>grown_image_url</code> 生发图(失败为 null)、<code>order</code></td></tr>
|
||||
<tr><td><code>hairline_images[]</code></td><td>object[]</td><td>选中发型列表,每项含 <code>hairline_type</code>、<code>image_middle_url</code>/<code>image_high_url</code>/<code>image_low_url</code> 三档<strong>透明 PNG 叠图</strong>(仅曲线,需叠加原图)、<code>grown_image_url</code> 生发图(完整人像,失败为 null)、<code>order</code></td></tr>
|
||||
<tr><td><code>best_hairline_center_point</code></td><td>object</td><td>首个选中发型 middle 档发际线中心点像素坐标 <code>{ x: number, y: number }</code></td></tr>
|
||||
<tr><td><code>face_measure</code></td><td>object \| null</td><td>复用<a href="#if1">接口1</a>的<strong>四庭七眼测量数值</strong>(不含标注图)。独立流程,测量失败时为 <code>null</code>,不影响发际线主结果。结构见下表</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
.scheme-imgs .img-slot .thumb { background: #222; border-radius: 6px; overflow: hidden; line-height: 0; }
|
||||
.scheme-imgs .img-slot .thumb img { width: 100%; max-height: 220px; object-fit: contain; display: block; }
|
||||
.scheme-imgs .img-slot .na { color: #d1d5db; font-size: 12px; padding: 30px 0; background: #f9fafb; border-radius: 6px; }
|
||||
/* 原图 + 透明 PNG 叠加显示 */
|
||||
.img-stack { position: relative; display: inline-block; line-height: 0; }
|
||||
.img-stack .layer-base { display: block; width: 100%; max-height: 220px; object-fit: contain; }
|
||||
.img-stack .layer-anno { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; }
|
||||
|
||||
.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; }
|
||||
@@ -181,10 +185,13 @@ function renderSchemes(results) {
|
||||
'<div class="label">📷 原图</div>'+
|
||||
'<div class="thumb"><img src="'+_origUrl+'" alt="原图"></div></div>';
|
||||
|
||||
// 发际线叠加
|
||||
// 发际线叠加(原图 + 透明 PNG 叠加显示)
|
||||
const overlaySlot = '<div class="img-slot">'+
|
||||
'<div class="label"><span class="dot preview"></span>发际线叠加</div>'+
|
||||
'<div class="thumb"><img src="'+r.image_url+'" alt="叠加"></div></div>';
|
||||
'<div class="label"><span class="dot preview"></span>原图+发际线叠加</div>'+
|
||||
'<div class="thumb"><div class="img-stack">'+
|
||||
'<img class="layer-base" src="'+_origUrl+'" alt="原图">'+
|
||||
'<img class="layer-anno" src="'+r.image_url+'" alt="叠加">'+
|
||||
'</div></div></div>';
|
||||
|
||||
// 生发效果
|
||||
let grownSlot;
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
.level-cell .level-label { font-size: 12px; font-weight: 600; color: #fff; background: rgba(0,0,0,.55); padding: 4px 8px; }
|
||||
.level-cell img { width: 100%; max-height: 380px; object-fit: contain; display: block; }
|
||||
.level-cell .level-none { color: #9ca3af; font-size: 13px; padding: 28px 0; text-align: center; }
|
||||
/* 原图 + 透明 PNG 叠加(三档叠图用) */
|
||||
.level-cell .img-stack { position: relative; line-height: 0; }
|
||||
.level-cell .img-stack .layer-base { width: 100%; max-height: 380px; object-fit: contain; display: block; }
|
||||
.level-cell .img-stack .layer-anno { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; }
|
||||
@media (max-width: 700px) { .level-row { grid-template-columns: repeat(2, 1fr); } }
|
||||
|
||||
/* 坐标高亮 */
|
||||
@@ -224,7 +228,14 @@ async function submitTest() {
|
||||
function renderGrid() {
|
||||
if (!_images.length) { $('resultsGrid').innerHTML = '<span style="color:#9ca3af">无数据</span>'; return; }
|
||||
let h = '';
|
||||
const cell = (label, url) => url
|
||||
// 叠图档:原图打底 + 透明 PNG 叠加(无原图或无叠图 → 显示占位)
|
||||
const overlayCell = (label, url) => (url && _origUrl)
|
||||
? '<div class="level-cell"><div class="level-label">'+label+'</div><div class="img-stack">'+
|
||||
'<img class="layer-base" src="'+_origUrl+'" alt="原图">'+
|
||||
'<img class="layer-anno" src="'+url+'" alt="'+label+'"></div></div>'
|
||||
: '<div class="level-cell"><div class="level-label">'+label+'</div><div class="level-none">无</div></div>';
|
||||
// 生发图:独立 img(已是 ComfyUI 完整人像照片)
|
||||
const grownCell = (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>';
|
||||
_images.forEach(function(it) {
|
||||
@@ -232,10 +243,10 @@ function renderGrid() {
|
||||
'<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) +
|
||||
overlayCell('middle', it.image_middle_url) +
|
||||
overlayCell('high', it.image_high_url) +
|
||||
overlayCell('low', it.image_low_url) +
|
||||
grownCell('生发图', it.grown_image_url) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user