Compare commits
6
Commits
b4714cedf3
...
ubuntu3090
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe0e74ece8 | ||
|
|
52913c94fc | ||
|
|
13526cb5b8 | ||
|
|
f509fe99b4 | ||
|
|
a1d458eb20 | ||
|
|
92e628b0d5 |
@@ -401,9 +401,24 @@ def _run_face_measure_data(image, variant="v1"):
|
||||
logger.warning("头发/耳朵分割失败,回退方案A:%s", seg_e)
|
||||
|
||||
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
|
||||
discarded = result.hairline_discarded
|
||||
data = result.to_response()
|
||||
if variant == "v6":
|
||||
vd = result.vertical
|
||||
if variant == "v6":
|
||||
if discarded:
|
||||
# 发际线弃用:接口6 的上庭也依赖发际线,一并置 null;只保留中/下庭。
|
||||
base_px = vd["middle_court_px"] + vd["lower_court_px"]
|
||||
data["four_courts"]["upper_court_cm"] = None
|
||||
data["four_courts"]["ratios"] = {
|
||||
"upper_court": None,
|
||||
"middle_court": round(vd["middle_court_px"] / base_px, 3),
|
||||
"lower_court": round(vd["lower_court_px"] / base_px, 3),
|
||||
}
|
||||
data["four_courts"].pop("top_court_cm", None)
|
||||
data["face_total_height_cm"] = round(
|
||||
result.middle_cm + result.lower_cm, 2)
|
||||
data["landmarks"]["hairline"] = None
|
||||
else:
|
||||
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
|
||||
# 接口6 是三庭:去掉顶庭相关字段(top_court_cm / ratios.top_court / landmarks.hair_top)
|
||||
data["four_courts"]["ratios"] = {
|
||||
@@ -430,11 +445,14 @@ def _run_face_measure_data(image, variant="v1"):
|
||||
data["seven_eyes"][f"eye{i + 2}"] = (
|
||||
None if (a is None or b is None) else round((b - a) / pc, 2))
|
||||
if variant != "v6":
|
||||
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线
|
||||
# 接口1 额外算 eye1/eye7(左/右耳外段),需耳朵分割端线。
|
||||
# 竖向范围:发际线弃用时用眉心做上界(hair_top 不可靠),否则用头顶。
|
||||
from face_analysis.annotation import _ear_edges_from_mask
|
||||
top_y = (vd["brow_center"][1] if discarded
|
||||
else vd["hair_top"][1])
|
||||
head_l, head_r = _ear_edges_from_mask(
|
||||
ear_mask, hair_mask,
|
||||
result.vertical["hair_top"][1], result.vertical["chin_tip"][1],
|
||||
top_y, vd["chin_tip"][1],
|
||||
lcx, rcx, (lcx + rcx) / 2)
|
||||
data["seven_eyes"]["eye1"] = (
|
||||
None if (head_l is None) else round((lcx - head_l) / pc, 2))
|
||||
@@ -999,6 +1017,8 @@ async def face_features(
|
||||
`female`:1=ellipse,2=flower,3=heart,4=straight,5=wave;`male`:1=ellipse,2=inverse_arc,3=m,4=straight。
|
||||
- 可选 `use_mask` / `prompt`:同接口2 的生发控制参数。
|
||||
注:生发黑模板固定取 `hairline_texture_black/`(middle 档),即三档叠图分别用各自贴图、但生发目标固定 middle。
|
||||
- 可选 `generate_grow_image`(默认 `true`):是否生成生发效果图(ComfyUI 生发,全流程最耗时)。
|
||||
`false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,大幅降低耗时。
|
||||
|
||||
**返回说明**:
|
||||
|
||||
@@ -1077,6 +1097,7 @@ async def hairline_generate(
|
||||
hair_style: Optional[str] = Form(default=None, description="发型序号逗号分隔(必填,如 1,2,3)。female:1-5 male:1-4"),
|
||||
use_mask: bool = Form(default=True, description="生发是否启用 inpaint 遮罩(同接口2,测试对比用)"),
|
||||
prompt: str = Form(default="填充遮罩区域的头发,皮肤加一点磨皮,再加一点美颜", description="ComfyUI 提示词(同接口2),会替换工作流节点60的文本"),
|
||||
generate_grow_image: bool = Form(default=True, description="是否生成生发效果图(ComfyUI 生发,最耗时)。默认 true 出图;false 时跳过生发,各发型 grown_image 恒为 null,仅返回三档发际线叠图与中心点"),
|
||||
):
|
||||
if gender not in ("male", "female"):
|
||||
return err(1004, "gender 必填且只能为 male / female")
|
||||
@@ -1100,7 +1121,8 @@ async def hairline_generate(
|
||||
from hairline.service import generate_hairline_pngs
|
||||
|
||||
res = await run_in_threadpool(
|
||||
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt)
|
||||
generate_hairline_pngs, image, gender, hair_styles, use_mask, prompt,
|
||||
generate_grow_image=generate_grow_image)
|
||||
if res is None:
|
||||
return err(1001, "无法识别人像")
|
||||
|
||||
|
||||
+9
-57
@@ -20,7 +20,6 @@
|
||||
| 3 B 端生发 | POST | `/api/v1/hair/grow-b` |
|
||||
| 4 用户特征 | POST | `/api/v1/face/features` |
|
||||
| 5 发际线 PNG 生成 | POST | `/api/v1/hairline/generate` |
|
||||
| 7 C 端生发 v2 | POST | `/api/v1/hair/grow-v2` |
|
||||
|
||||
---
|
||||
|
||||
@@ -87,6 +86,7 @@
|
||||
| 1006 | 文件超出大小限制 | 单文件超过 1 MB |
|
||||
| 1007 | 图片参数错误 | file / url / base64 未传,或同时传了多个(三者严格互斥) |
|
||||
| 1008 | 图片格式不支持 | 非 JPG / PNG |
|
||||
| 1009 | 未授权 | 缺少或错误的 `X-Internal-Token`(`/api/*` 路径鉴权) |
|
||||
|
||||
---
|
||||
|
||||
@@ -109,6 +109,8 @@
|
||||
| four_courts | object | 四庭数据,见下表 |
|
||||
| seven_eyes | object | 七眼数据,见下表 |
|
||||
| landmarks | object | 关键分界点坐标(头顶 / 发际线 / 眉心 / 鼻翼下缘 / 下巴尖),原图像素坐标 |
|
||||
| left_position | object | MediaPipe 21 号关键点坐标(左脸定位点),原图像素:`{ "x": int, "y": int }` |
|
||||
| right_position | object | MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:`{ "x": int, "y": int }` |
|
||||
|
||||
`four_courts`(四庭,自上而下):
|
||||
|
||||
@@ -211,6 +213,8 @@
|
||||
| four_courts | object | 三庭数据(上/中/下庭,各含 cm 与 ratio;**无顶庭**) |
|
||||
| seven_eyes | object | 七眼数据(眼宽/脸宽/两眼间距 cm + 占比 ratios + **eye2~eye6** 共 5 段宽度) |
|
||||
| landmarks | object | 四个关键点像素坐标(发际线/眉心/鼻翼下缘/下巴尖) |
|
||||
| left_position | object | MediaPipe 21 号关键点坐标(左脸定位点),原图像素:`{ "x": int, "y": int }` |
|
||||
| right_position | object | MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:`{ "x": int, "y": int }` |
|
||||
|
||||
> 接口6 是**三庭五眼**:`four_courts`/`landmarks` 不含顶庭与头顶点(无 `top_court_cm`/`hair_top`);`seven_eyes` 只含 **eye2~eye6**(左脸颊/左眼/两眼间距/右眼/右脸颊,5 段),**无 eye1/eye7**(耳外段需头发轮廓端线,仅接口1 有)。
|
||||
|
||||
@@ -405,6 +409,7 @@
|
||||
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`),决定返回哪些发际线类型。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight。缺失/越界/非法返回 `1007` |
|
||||
| use_mask | bool | 否 | 生发是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线),供测试对比 |
|
||||
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
||||
| generate_grow_image | bool | 否 | 是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 `true`。传 `false` 时跳过生发,各发型 `grown_image_*` 恒为 `null`,仅返回三档发际线叠图与中心点,可大幅降低耗时 |
|
||||
|
||||
> ⚠️ 三档叠图分别用 `hairline_texture` / `hairline_texture_high` / `hairline_texture_low` 三套同名贴图;**生发黑模板固定取自 `hairline_texture_black/`(middle 档)**,即生发目标固定压到 middle 档,每个发型仅 1 张生发图。
|
||||
|
||||
@@ -426,7 +431,7 @@
|
||||
| image_middle_url | string | middle 档发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加原图显示) |
|
||||
| image_high_url | string | high 档发际线曲线**透明 PNG** URL(同上,high 档曲线) |
|
||||
| image_low_url | string | low 档发际线曲线**透明 PNG** URL(同上,low 档曲线) |
|
||||
| grown_image_url | string \| null | **生发后图片** URL(ComfyUI「植发」效果图,完整人像照片,生发失败时为 `null`) |
|
||||
| grown_image_url | string \| null | **生发后图片** URL(ComfyUI「植发」效果图,完整人像照片,生发失败或 `generate_grow_image=false` 时为 `null`) |
|
||||
| order | int | 发型序号(= 传入的 hair_style 值) |
|
||||
|
||||
> worker 侧返回 `image_middle_base64` / `image_high_base64` / `image_low_base64` / `grown_image_base64`,网关落盘后改写为上表对应的 `*_url`。
|
||||
@@ -443,6 +448,8 @@
|
||||
| landmarks | object | 5 个纵向关键点像素坐标(hair_top/hairline/brow_center/nose_bottom/chin_tip),结构同接口1 |
|
||||
| hairline_source | string | 发际线来源:`segmentation`(真实分割)/ `estimated`(比例估算) |
|
||||
| head_pose | object | 头部姿态角度(yaw/pitch/roll,单位:度) |
|
||||
| left_position | object | MediaPipe 21 号关键点坐标(左脸定位点),原图像素:`{ "x": int, "y": int }` |
|
||||
| right_position | object | MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:`{ "x": int, "y": int }` |
|
||||
|
||||
> `eye1`~`eye7` 为从左到右共 7 段宽度,eye1=左耳外段、eye7=右耳外段,某侧耳朵不可见时对应段为 `null`。详见接口1说明。
|
||||
|
||||
@@ -508,60 +515,6 @@
|
||||
|
||||
---
|
||||
|
||||
## 接口 7:C 端生发 v2 接口
|
||||
|
||||
**说明**:功能与[接口 2](#接口-2c-端生发接口)完全一致,仅 ComfyUI 工作流不同——使用 `add_hair2.json` 替代 `add_hair.json`。
|
||||
|
||||
**请求**:`POST /api/v1/hair/grow-v2`
|
||||
|
||||
### 输入
|
||||
|
||||
与接口 2 完全相同。图片参数见「通用约定 → 图片传参字段」。专属参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| gender | string | **是** | 性别:`male` / `female`。决定使用的发际线贴图集合 |
|
||||
| hair_style | string | **是** | 发型序号,**逗号分隔多选**(如 `1,2,3`)。female:1=ellipse, 2=flower, 3=heart, 4=straight, 5=wave;male:1=ellipse, 2=inverse_arc, 3=m, 4=straight。越界/非法返回 `1007` |
|
||||
| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false(当前阶段不生效) |
|
||||
| use_mask | bool | 否 | 是否启用 inpaint 遮罩,默认 `true`。`false` 时用干净原图生成(空遮罩、不烧模板黑线) |
|
||||
| prompt | string | 否 | ComfyUI 提示词,默认「补充遮罩区域的头发,加一点美颜」,会替换工作流节点 60 的文本 |
|
||||
|
||||
### 输出(data)
|
||||
|
||||
与接口 2 完全相同。`results`:发际线方案数组,**数量 = 所选发型数**。每个元素:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| image_url | string | 发际线曲线**透明 PNG** URL(仅曲线,透明底,**不含人物**,需叠加原图显示) |
|
||||
| grown_image_url | string | **生发后图片** URL(ComfyUI/Flux「植发 3 个月」效果图,完整人像照片) |
|
||||
| hairline_type | string | 发际线类型 key |
|
||||
| order | int | 排序序号 |
|
||||
|
||||
> ⚠️ 与接口 2 的区别:本接口使用 `add_hair2.json` 工作流(Flux-2 Klein 9b),输入/遮罩节点同为 26,
|
||||
> SaveImage 输出节点为 75。
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"request_id": "mock-request-id",
|
||||
"data": {
|
||||
"results": [
|
||||
{
|
||||
"image_url": "https://hair.xiangsilian.com/static/sample.jpg",
|
||||
"grown_image_url": "https://hair.xiangsilian.com/static/sample.jpg",
|
||||
"hairline_type": "ellipse",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 汇总:输入输出一览
|
||||
|
||||
| 接口 | 输入 | 主要输出 |
|
||||
@@ -572,7 +525,6 @@
|
||||
| 3 B 端生发 | 划线图片 | 最合适发际线图片 + 生发后图片 |
|
||||
| 4 用户特征 | 用户照片 | 6 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格) |
|
||||
| 5 发际线 PNG | 用户照片 + gender + hair_style(多选) | 每个选中发型 middle/high/low 三档发际线叠图 + 生发图 + 最合适发际线面部中间点坐标 |
|
||||
| 7 C 端生发 v2 | 用户照片 + gender + hair_style | 同接口2,使用 add_hair2.json 工作流 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
||||
|
||||
# --- 自适应尺寸:字号/线宽/虚线/箭头按短边缩放 ---
|
||||
s = min(w, h)
|
||||
font_size = max(8, round(s * 0.017)) # 字体更小
|
||||
font_size = max(9, round(s * 0.020)) # 字号上调一档
|
||||
line_w = max(1, round(s * 0.0022))
|
||||
dash_len = max(4, round(s * 0.008))
|
||||
gap_len = max(2, round(dash_len * 0.7)) # 虚线更稠密(间隙<划线)
|
||||
@@ -212,7 +212,11 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
||||
|
||||
buf = np.zeros((h, w, 4), dtype=np.uint8)
|
||||
|
||||
if variant == "v6":
|
||||
# 发际线弃用(hairline_discarded):保留头顶横线,去掉发际线横线,
|
||||
# 也不标顶/上庭(缺发际线作边界,算不出)。横线 = 头顶/眉心/鼻翼下缘/下巴尖。
|
||||
if getattr(measure_result, "hairline_discarded", False):
|
||||
order = ["hair_top", "brow_center", "nose_bottom", "chin_tip"]
|
||||
elif variant == "v6":
|
||||
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||
else:
|
||||
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
|
||||
@@ -239,7 +243,7 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
||||
face_cx = (fx0 + fx1) / 2
|
||||
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
|
||||
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
|
||||
# v6 竖线纵向范围 = 发际线→下巴尖(不超出);v1 = 头顶→下巴尖并两端超出一点
|
||||
# 竖线纵向范围:v6 = 发际线→下巴尖(不超出);v1(含发际线弃用)= 头顶→下巴尖并两端超出一点
|
||||
v_top = fy0 if variant == "v6" else fy0 - over
|
||||
v_bot = fy1 if variant == "v6" else fy1 + over
|
||||
|
||||
@@ -265,19 +269,28 @@ def create_annotated_image(image_bgr, measure_result, ear_mask=None, hair_mask=N
|
||||
draw.text((x, ys[i]), text, fill=LINE_COLOR, font=font, anchor="lm")
|
||||
|
||||
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
|
||||
if variant == "v6":
|
||||
# court_start:庭段在 order 里的起始索引。发际线弃用时 order 首位是头顶(无下界发际线,
|
||||
# 顶/上庭不标),中庭从眉心开始 → 跳过 order[0]。
|
||||
if getattr(measure_result, "hairline_discarded", False):
|
||||
court_cm = [measure_result.middle_cm, measure_result.lower_cm]
|
||||
court_name = ["中庭", "下庭"]
|
||||
n_court = 2
|
||||
court_start = 1
|
||||
elif variant == "v6":
|
||||
court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
|
||||
court_name = ["上庭", "中庭", "下庭"]
|
||||
n_court = 3
|
||||
court_start = 0
|
||||
else:
|
||||
court_cm = [measure_result.top_cm, measure_result.upper_cm,
|
||||
measure_result.middle_cm, measure_result.lower_cm]
|
||||
court_name = ["顶庭", "上庭", "中庭", "下庭"]
|
||||
n_court = 4
|
||||
court_start = 0
|
||||
arrow_x = max(arrow_size + 1, fx0 - pad) # 竖箭头所在 x(脸左侧,贴近最左竖线)
|
||||
court_total = sum(court_cm) or 1.0 # 各庭占比分母 = 四庭(v6 三庭)之和
|
||||
for i in range(n_court):
|
||||
y_a, y_b = ys[i], ys[i + 1]
|
||||
y_a, y_b = ys[court_start + i], ys[court_start + i + 1]
|
||||
# 竖向虚线双箭头,覆盖该庭高度(略收一点避免压到横线)
|
||||
inset = min(arrow_size, (y_b - y_a) * 0.12)
|
||||
draw_dashed_line_with_arrows(
|
||||
|
||||
@@ -18,6 +18,8 @@ RIGHT_EYE_INNER = 362 # 右眼内角
|
||||
RIGHT_EYE_OUTER = 263 # 右眼外角
|
||||
LEFT_CHEEK = 234 # 左脸颧弓(脸宽左端)
|
||||
RIGHT_CHEEK = 454 # 右脸颧弓(脸宽右端)
|
||||
LEFT_POSITION = 21 # 左脸前侧定位点(脸颊/耳前区域,与 251 镜像)
|
||||
RIGHT_POSITION = 251 # 右脸前侧定位点(与 21 镜像)
|
||||
|
||||
# --- 鼻尖(solvePnP 用,可选) ---
|
||||
NOSE_TIP = 1 # 鼻尖(也有用 4 的版本)
|
||||
|
||||
+87
-21
@@ -12,9 +12,9 @@ from face_analysis.calibration import (
|
||||
estimate_scale_factor, normalized_to_pixel, pixel_distance, _lm_list,
|
||||
)
|
||||
from face_analysis.face_mesh_landmarks import (
|
||||
GLABELLA_9, GLABELLA_151, NOSE_BOTTOM, CHIN_TIP,
|
||||
GLABELLA_9, NOSE_BOTTOM, CHIN_TIP,
|
||||
LEFT_EYE_OUTER, LEFT_EYE_INNER, RIGHT_EYE_INNER, RIGHT_EYE_OUTER,
|
||||
LEFT_CHEEK, RIGHT_CHEEK,
|
||||
LEFT_CHEEK, RIGHT_CHEEK, LEFT_POSITION, RIGHT_POSITION,
|
||||
)
|
||||
from face_analysis.hair_segmenter import locate_hairline_by_segmentation
|
||||
|
||||
@@ -24,10 +24,8 @@ _TOP_RATIO = 0.22 / 0.28 # 顶庭 ÷ 中庭(≈ 0.786)
|
||||
|
||||
|
||||
def _brow_center(lm, w, h):
|
||||
"""眉心 = 索引 9 / 151 中点。"""
|
||||
g9 = normalized_to_pixel(lm[GLABELLA_9], w, h)
|
||||
g151 = normalized_to_pixel(lm[GLABELLA_151], w, h)
|
||||
return (g9[0] + g151[0]) / 2, (g9[1] + g151[1]) / 2
|
||||
"""眉心 = 索引 9(眉间上点)。"""
|
||||
return normalized_to_pixel(lm[GLABELLA_9], w, h)
|
||||
|
||||
|
||||
def estimate_vertical_landmarks(landmarks, image_width, image_height):
|
||||
@@ -144,21 +142,46 @@ def measure_seven_eyes(landmarks, image_width, image_height):
|
||||
}
|
||||
|
||||
|
||||
def pt_or_none(vertical, name):
|
||||
"""vertical dict 的点 → {"x","y"},值为 None 时返回 None。"""
|
||||
v = vertical.get(name)
|
||||
if v is None:
|
||||
return None
|
||||
return {"x": int(round(v[0])), "y": int(round(v[1]))}
|
||||
|
||||
|
||||
class MeasureResult:
|
||||
"""测量结果,提供 to_response() 输出与接口文档同构的 data 字段。"""
|
||||
|
||||
def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose):
|
||||
# 发际线弃用阈值:发际线离头顶(顶庭)< 此值时判定分割不可靠,弃用发际线。
|
||||
# hairline 与 hair_top 几乎重合(如稀疏头发中轴漏检只剩一小撮),说明发际线
|
||||
# 定位无意义 → 顶/上庭置 null、标注图不画头顶/发际线。
|
||||
HAIRLINE_DISCARD_TOP_CM = 0.7
|
||||
|
||||
def __init__(self, vertical, eyes, px_per_cm, hairline_source, head_pose,
|
||||
landmarks=None, image_width=None, image_height=None):
|
||||
self.vertical = vertical
|
||||
self.eyes = eyes
|
||||
self.px_per_cm = px_per_cm
|
||||
self.hairline_source = hairline_source
|
||||
self.head_pose = head_pose # (yaw, pitch, roll) 或 None
|
||||
# 原始 mediapipe 点集 + 图像尺寸,供 to_response 输出 21/251 号定位点
|
||||
self.landmarks = landmarks
|
||||
self.w = image_width
|
||||
self.h = image_height
|
||||
|
||||
# 各庭厘米
|
||||
self.top_cm = vertical["top_court_px"] / px_per_cm
|
||||
self.upper_cm = vertical["upper_court_px"] / px_per_cm
|
||||
self.middle_cm = vertical["middle_court_px"] / px_per_cm
|
||||
self.lower_cm = vertical["lower_court_px"] / px_per_cm
|
||||
# 发际线弃用判定:顶庭(头顶→发际线)过小视为发际线贴近头顶、不可靠。
|
||||
# 弃用时 hairline_source 改为 "discarded",face_total 只算中庭+下庭。
|
||||
self.hairline_discarded = self.top_cm < self.HAIRLINE_DISCARD_TOP_CM
|
||||
if self.hairline_discarded:
|
||||
self.hairline_source = "discarded"
|
||||
self.face_total_cm = self.middle_cm + self.lower_cm
|
||||
else:
|
||||
self.face_total_cm = self.top_cm + self.upper_cm + self.middle_cm + self.lower_cm
|
||||
|
||||
# 七眼厘米
|
||||
@@ -167,14 +190,45 @@ class MeasureResult:
|
||||
self.inter_eye_cm = eyes["inter_eye_distance_px"] / px_per_cm
|
||||
|
||||
def to_response(self):
|
||||
# 发际线弃用:顶/上庭相关字段置 null(保留键),ratio 分母只算中下庭;
|
||||
# landmarks.hair_top/hairline 置 null。否则按四庭正常输出。
|
||||
if self.hairline_discarded:
|
||||
base_px = (self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
||||
data = {
|
||||
"face_total_height_cm": round(self.face_total_cm, 2),
|
||||
"four_courts": {
|
||||
"top_court_cm": None,
|
||||
"upper_court_cm": None,
|
||||
"middle_court_cm": round(self.middle_cm, 2),
|
||||
"lower_court_cm": round(self.lower_cm, 2),
|
||||
"ratios": {
|
||||
"top_court": None,
|
||||
"upper_court": None,
|
||||
"middle_court": round(self.vertical["middle_court_px"] / base_px, 3),
|
||||
"lower_court": round(self.vertical["lower_court_px"] / base_px, 3),
|
||||
},
|
||||
},
|
||||
"seven_eyes": {
|
||||
"eye_width_cm": round(self.eye_width_cm, 2),
|
||||
"face_width_cm": round(self.face_width_cm, 2),
|
||||
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
||||
"ratios": {
|
||||
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
||||
},
|
||||
},
|
||||
"landmarks": {
|
||||
"hair_top": None,
|
||||
"hairline": None,
|
||||
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
||||
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
||||
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
||||
},
|
||||
"hairline_source": self.hairline_source,
|
||||
}
|
||||
else:
|
||||
total_px = (self.vertical["top_court_px"] + self.vertical["upper_court_px"]
|
||||
+ self.vertical["middle_court_px"] + self.vertical["lower_court_px"])
|
||||
fw_px = self.eyes["face_width_px"]
|
||||
|
||||
def pt(name):
|
||||
x, y = self.vertical[name]
|
||||
return {"x": int(round(x)), "y": int(round(y))}
|
||||
|
||||
data = {
|
||||
"face_total_height_cm": round(self.face_total_cm, 2),
|
||||
"four_courts": {
|
||||
@@ -194,19 +248,30 @@ class MeasureResult:
|
||||
"face_width_cm": round(self.face_width_cm, 2),
|
||||
"inter_eye_distance_cm": round(self.inter_eye_cm, 2),
|
||||
"ratios": {
|
||||
"eye_width": round(self.eyes["eye_width_px"] / fw_px, 3),
|
||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / fw_px, 3),
|
||||
"eye_width": round(self.eyes["eye_width_px"] / self.eyes["face_width_px"], 3),
|
||||
"inter_eye_distance": round(self.eyes["inter_eye_distance_px"] / self.eyes["face_width_px"], 3),
|
||||
},
|
||||
},
|
||||
"landmarks": {
|
||||
"hair_top": pt("hair_top"),
|
||||
"hairline": pt("hairline"),
|
||||
"brow_center": pt("brow_center"),
|
||||
"nose_bottom": pt("nose_bottom"),
|
||||
"chin_tip": pt("chin_tip"),
|
||||
"hair_top": pt_or_none(self.vertical, "hair_top"),
|
||||
"hairline": pt_or_none(self.vertical, "hairline"),
|
||||
"brow_center": pt_or_none(self.vertical, "brow_center"),
|
||||
"nose_bottom": pt_or_none(self.vertical, "nose_bottom"),
|
||||
"chin_tip": pt_or_none(self.vertical, "chin_tip"),
|
||||
},
|
||||
"hairline_source": self.hairline_source,
|
||||
}
|
||||
# left/right_position:mediapipe 21/251 号定位点(原图像素,与 landmarks 同坐标系)。
|
||||
# landmarks 缺省(如测试直构 MeasureResult)时不输出,保持向后兼容。
|
||||
if self.landmarks is not None and self.w and self.h:
|
||||
lm = _lm_list(self.landmarks)
|
||||
|
||||
def _pt_lm(idx):
|
||||
px, py = normalized_to_pixel(lm[idx], self.w, self.h)
|
||||
return {"x": int(round(px)), "y": int(round(py))}
|
||||
|
||||
data["left_position"] = _pt_lm(LEFT_POSITION)
|
||||
data["right_position"] = _pt_lm(RIGHT_POSITION)
|
||||
if self.head_pose is not None:
|
||||
yaw, pitch, roll = self.head_pose
|
||||
data["head_pose"] = {
|
||||
@@ -220,7 +285,8 @@ def measure_face(landmarks, hair_mask, image_width, image_height, head_pose=None
|
||||
vertical, source = decide_vertical(landmarks, image_width, image_height, hair_mask)
|
||||
eyes = measure_seven_eyes(landmarks, image_width, image_height)
|
||||
px_per_cm = estimate_scale_factor(landmarks, image_width, image_height)
|
||||
return MeasureResult(vertical, eyes, px_per_cm, source, head_pose)
|
||||
return MeasureResult(vertical, eyes, px_per_cm, source, head_pose,
|
||||
landmarks, image_width, image_height)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+13
-5
@@ -396,13 +396,16 @@ def _grow_from_texture(image_bgr: np.ndarray, ctx: dict, white_path: str | None,
|
||||
|
||||
def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||
hair_styles: list[int], use_mask: bool = True,
|
||||
prompt: str | None = None):
|
||||
prompt: str | None = None,
|
||||
generate_grow_image: bool = True):
|
||||
"""接口5:对选中发型返回 middle/high/low 三档发际线透明叠图 + 生发图(同接口2)。
|
||||
|
||||
入参同接口2:先选 gender,再多选 hair_styles(必填,1-indexed 按贴图排序)。
|
||||
每个选中发型返回三档叠图(middle/high/low,RGBA 透明层只含发际线曲线)与一张生发图;
|
||||
三档贴图同名,生发黑模板固定取自 hairline_texture_black/(middle),故生发目标固定 middle 档。
|
||||
use_mask/prompt:同接口2 的生发参数。
|
||||
generate_grow_image(默认 True):是否生成生发图(ComfyUI,最耗时)。False 时跳过生发,
|
||||
各发型 grown_png 恒为 None,可大幅降低耗时(仅留三档发际线叠图与中心点)。
|
||||
Returns: {"images":[{hairline_type,order,overlays:{middle,high,low}((H,W,4) RGBA 透明层),grown_png}],
|
||||
"best_centers":{"middle":(x,y),"high":(x,y),"low":(x,y)}};无人脸 None。
|
||||
best_centers 取首个选中发型三档各自的发际线中点。
|
||||
@@ -424,8 +427,9 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||
tex_by_level = {lv: get_texture_map(lv)[gender] for lv in _TEXTURE_DIRS}
|
||||
|
||||
# use_mask=False:干净原图+空遮罩与贴图无关,只跑一次 ComfyUI,选中项复用
|
||||
# generate_grow_image=False:完全跳过生发(最耗时),grown_png 恒为 None
|
||||
shared_grown = None
|
||||
if not use_mask:
|
||||
if generate_grow_image and not use_mask:
|
||||
shared_grown = _grow_from_texture(image_bgr, ctx, None, use_mask=False, prompt=prompt)
|
||||
|
||||
def _center_of(overlay):
|
||||
@@ -444,9 +448,13 @@ def generate_hairline_pngs(image_bgr: np.ndarray, gender: str,
|
||||
for lv in _TEXTURE_DIRS:
|
||||
white = load_texture_rgba(tex_by_level[lv][s - 1][1])
|
||||
overlays[lv] = build_overlay_layer(h, w, ctx["points"], ext_faces, uv, white)
|
||||
# 生发:固定 middle 黑模板
|
||||
grown_png = shared_grown if not use_mask else \
|
||||
_grow_from_texture(image_bgr, ctx, mid_path, use_mask=True, prompt=prompt)
|
||||
# 生发:固定 middle 黑模板(generate_grow_image=False 时跳过,恒 None)
|
||||
if not generate_grow_image:
|
||||
grown_png = None
|
||||
elif not use_mask:
|
||||
grown_png = shared_grown
|
||||
else:
|
||||
grown_png = _grow_from_texture(image_bgr, ctx, mid_path, use_mask=True, prompt=prompt)
|
||||
images.append({"hairline_type": key, "order": s,
|
||||
"overlays": overlays, "grown_png": grown_png})
|
||||
# best_centers:首个选中发型三档(middle/high/low)发际线中点
|
||||
|
||||
+16
-40
@@ -57,7 +57,6 @@
|
||||
<a href="#if4">接口4</a>
|
||||
<a href="#if5">接口5</a>
|
||||
<a href="#if6">接口6</a>
|
||||
<a href="#if7">接口7</a>
|
||||
<a href="#errors">错误码</a>
|
||||
<a href="#test">在线测试</a>
|
||||
</div>
|
||||
@@ -109,6 +108,8 @@
|
||||
<tr><td><code>landmarks</code></td><td>object</td><td>5 个关键点像素坐标:hair_top/hairline/brow_center/nose_bottom/chin_tip</td></tr>
|
||||
<tr><td><code>hairline_source</code></td><td>string</td><td>发际线来源:<code>"segmentation"</code>(真实分割,可信度高)/ <code>"estimated"</code>(比例估算,可信度低)</td></tr>
|
||||
<tr><td><code>head_pose</code></td><td>object</td><td>头部姿态角度:<code>{ yaw, pitch, roll }</code>(度),接近 0 表示正面照</td></tr>
|
||||
<tr><td><code>left_position</code></td><td>object</td><td>MediaPipe 21 号关键点坐标(左脸定位点),原图像素:<code>{ x: number, y: number }</code></td></tr>
|
||||
<tr><td><code>right_position</code></td><td>object</td><td>MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:<code>{ x: number, y: number }</code></td></tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:12px;font-size:12px;color:#64748b">💡 前端把标注图叠加到原图上即可呈现测量效果(标注图白色线条 #FFFFFF,透明底)。</p>
|
||||
@@ -189,17 +190,17 @@ const { code, data } = await res.json();
|
||||
<div class="card" id="if4">
|
||||
<h2>4. 用户特征分析 <span class="badge post">POST</span> <code>/api/v1/face/features</code></h2>
|
||||
<div class="card-body">
|
||||
<p class="desc">上传照片 → 火山方舟豆包视觉模型分析 → 返回几十项面部特征(脸型/眉形/肤色/四季色彩…)。</p>
|
||||
<p class="desc">上传照片 → 火山方舟豆包视觉模型分析 → 返回固定 6 项面部特征(脸型/眉形/面部年龄/动静类型/性别/基因风格)。</p>
|
||||
|
||||
<p><strong>入参</strong>:image_file / image_url / image_base64 三选一。无其他参数。</p>
|
||||
|
||||
<p style="margin-top:12px"><strong>data 字段</strong></p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td><code>features</code></td><td>string</td><td><strong>JSON 字符串</strong>(不是对象!客户端需 <code>JSON.parse()</code>)</td></tr>
|
||||
<tr><td><code>features</code></td><td>string</td><td><strong>JSON 字符串</strong>(不是对象!客户端需 <code>JSON.parse()</code>)。解析后得到<strong>固定 6 个英文字段</strong></td></tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:8px"><strong>features 英文优先字段</strong>(其余中文字段同时返回,共~42个):</p>
|
||||
<p style="margin-top:8px"><strong>features 字段</strong>(固定返回 6 个):</p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>说明</th><th>字段</th><th>说明</th></tr>
|
||||
<tr><td>face_shape</td><td>脸型</td><td>eyebrow_shape</td><td>眉形</td></tr>
|
||||
@@ -214,7 +215,7 @@ const { code, data } = await res.json();
|
||||
const { code, data } = await res.json();
|
||||
const features = JSON.parse(data.features); // ← 注意:data.features 是字符串!
|
||||
console.log(features.face_shape); // "鹅蛋脸"
|
||||
console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保留)</pre>
|
||||
console.log(features.gene_style); // "自然型"</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -230,12 +231,13 @@ console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保
|
||||
<tr><td>image_file / image_url / image_base64</td><td>—</td><td>三选一</td><td>用户正面照</td></tr>
|
||||
<tr><td>gender</td><td>string</td><td>✅ 必填</td><td><code>"male"</code> / <code>"female"</code></td></tr>
|
||||
<tr><td>hair_style</td><td>string</td><td>✅ 必填</td><td>发型序号,逗号分隔多选(如 <code>1,2,3</code>)。缺失/越界返回 1007</td></tr>
|
||||
<tr><td>generate_grow_image</td><td>bool</td><td>否</td><td>是否生成生发效果图(ComfyUI 生发,全流程最耗时),默认 <code>true</code>。传 <code>false</code> 时跳过生发,各发型 <code>grown_image_url</code> 恒为 <code>null</code>,仅返回三档发际线叠图与中心点,大幅降低耗时</td></tr>
|
||||
</table>
|
||||
|
||||
<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> 三档<strong>透明 PNG 叠图</strong>(仅曲线,需叠加原图)、<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> 生发图(完整人像,失败或 <code>generate_grow_image=false</code> 时为 null)、<code>order</code></td></tr>
|
||||
<tr><td><code>best_hairline_center_point</code></td><td>object \| null</td><td>首个选中发型 <strong>middle 档</strong>发际线中心点像素坐标 <code>{ x: number, y: number }</code></td></tr>
|
||||
<tr><td><code>high_hairline_center_point</code></td><td>object \| null</td><td>同上,<strong>high 档</strong>发际线中点(发际线偏高,y 更小)</td></tr>
|
||||
<tr><td><code>low_hairline_center_point</code></td><td>object \| null</td><td>同上,<strong>low 档</strong>发际线中点(发际线偏低,y 更大)</td></tr>
|
||||
@@ -251,6 +253,8 @@ console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保
|
||||
<tr><td><code>landmarks</code></td><td>object</td><td>5 个关键点像素坐标:hair_top/hairline/brow_center/nose_bottom/chin_tip</td></tr>
|
||||
<tr><td><code>hairline_source</code></td><td>string</td><td>发际线来源:<code>"segmentation"</code>(真实分割)/ <code>"estimated"</code>(比例估算)</td></tr>
|
||||
<tr><td><code>head_pose</code></td><td>object</td><td>头部姿态角度:<code>{ yaw, pitch, roll }</code>(度)</td></tr>
|
||||
<tr><td><code>left_position</code></td><td>object</td><td>MediaPipe 21 号关键点坐标(左脸定位点),原图像素:<code>{ x: number, y: number }</code></td></tr>
|
||||
<tr><td><code>right_position</code></td><td>object</td><td>MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:<code>{ x: number, y: number }</code></td></tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:12px;font-size:12px;color:#64748b">💡 前端无需额外请求接口1 即可拿到四庭七眼测量数值;<code>face_measure</code> 为 <code>null</code> 时(角度过大/无人脸等)仅隐藏测量区块,发际线结果照常展示。</p>
|
||||
@@ -282,41 +286,12 @@ console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保
|
||||
<tr><td><code>four_courts</code></td><td>object</td><td>三庭:upper/middle/lower,各含 _cm 和 ratios(<strong>无 top_court</strong>)</td></tr>
|
||||
<tr><td><code>seven_eyes</code></td><td>object</td><td>七眼:<code>eye_width_cm</code>/<code>face_width_cm</code>/<code>inter_eye_distance_cm</code> + <code>ratios</code> + <strong><code>eye2</code>~<code>eye6</code></strong>(左脸颊/左眼/两眼间距/右眼/右脸颊,5 段宽度 cm;<strong>无 eye1/eye7</strong>)</td></tr>
|
||||
<tr><td><code>landmarks</code></td><td>object</td><td>4 个关键点:hairline/brow_center/nose_bottom/chin_tip(<strong>无 hair_top</strong>)</td></tr>
|
||||
<tr><td><code>left_position</code></td><td>object</td><td>MediaPipe 21 号关键点坐标(左脸定位点),原图像素:<code>{ x: number, y: number }</code></td></tr>
|
||||
<tr><td><code>right_position</code></td><td>object</td><td>MediaPipe 251 号关键点坐标(右脸定位点,与 21 号镜像),原图像素:<code>{ x: number, y: number }</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======== 接口7 ======== -->
|
||||
<div class="card" id="if7">
|
||||
<h2>7. C端生发 v2 <span class="badge post">POST</span> <code>/api/v1/hair/grow-v2</code> <span class="badge warn">v2</span></h2>
|
||||
<div class="card-body">
|
||||
<p class="desc">功能与<a href="#if2">接口2</a>完全一致,仅 ComfyUI 工作流不同——使用 <code>add_hair2.json</code> 替代 <code>add_hair.json</code>(Flux-2 Klein 9b)。</p>
|
||||
|
||||
<p><strong>入参</strong></p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>
|
||||
<tr><td>image_file / image_url / image_base64</td><td>—</td><td>三选一</td><td>用户正面照</td></tr>
|
||||
<tr><td>gender</td><td>string</td><td>✅ 必填</td><td><code>"male"</code> / <code>"female"</code></td></tr>
|
||||
<tr><td>hair_style</td><td>int</td><td>✅ 必填</td><td>发型序号。female: 1~5,male: 1~4</td></tr>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:12px"><strong>data.results[] 元素</strong>(同接口2)</p>
|
||||
<table>
|
||||
<tr><th>字段</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td><code>image_url</code></td><td>string</td><td>发际线曲线<strong>透明 PNG</strong>(仅曲线,需叠加原图显示,同接口2)</td></tr>
|
||||
<tr><td><code>grown_image_url</code></td><td>string</td><td>生发后效果图(完整人像)⚠ 可空</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>排序</td></tr>
|
||||
</table>
|
||||
|
||||
<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>
|
||||
⚠ 工作流: add_hair2.json(Flux-2 Klein 9b),输入节点 26,输出节点 75。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ======== 错误码 ======== -->
|
||||
<div class="card" id="errors">
|
||||
<h2>⚠ 错误码</h2>
|
||||
@@ -326,12 +301,14 @@ console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保
|
||||
<tr><td>1001</td><td>无法识别人像</td><td>未检测到人脸</td></tr>
|
||||
|
||||
<tr><td>1003</td><td>角度问题,非正面照</td><td>非正面 / 角度过大</td></tr>
|
||||
<tr><td>1004</td><td>gender 必填且只能为 male / female</td><td>接口2/5 的 <code>gender</code> 缺失或非法</td></tr>
|
||||
<tr><td>1005</td><td>检测到多张人脸</td><td>仅支持单人</td></tr>
|
||||
|
||||
<tr><td>1007</td><td>图片参数错误 / 后端不可用</td><td>参数传错 / 服务繁忙请稍后重试</td></tr>
|
||||
<tr><td>1008</td><td>图片格式不支持</td><td>非 JPG/PNG / base64 解码失败</td></tr>
|
||||
<tr><td>1009</td><td>未授权</td><td>缺少或错误的 <code>X-Internal-Token</code>(<code>/api/*</code> 路径鉴权)</td></tr>
|
||||
</table>
|
||||
<p style="font-size:12px;color:#94a3b8;margin-top:8px">1004 已废弃(接口2 不再自动判性别,改由客户端传 gender 参数)。</p>
|
||||
<p style="font-size:12px;color:#94a3b8;margin-top:8px">注:1004 仍在使用(接口2/5 的 gender 校验);接口7(grow-v2)已弃用,请改用接口2。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -344,10 +321,9 @@ console.log(features['四季色彩季型']); // "冷夏型"(中文字段也保
|
||||
<tr><td>1. 四庭七眼</td><td><a href="/static/test_interface1.html" class="link">/static/test_interface1.html</a></td><td>上传照片 → 原图+标注叠加,底图/标注开关,指标卡片</td></tr>
|
||||
<tr><td>2. C端生发</td><td><a href="/static/test_interface2.html" class="link">/static/test_interface2.html</a></td><td>上传+性别 → 方案一覧(原图/叠加/生发),双图对比</td></tr>
|
||||
<tr><td>3. B端生发</td><td><a href="/static/test_interface3.html" class="link">/static/test_interface3.html</a></td><td>划线图上传 → 生发效果图</td></tr>
|
||||
<tr><td>4. 用户特征</td><td><a href="/static/test_interface4.html" class="link">/static/test_interface4.html</a></td><td>上传照片 → 42项面部特征表格 + 原始JSON</td></tr>
|
||||
<tr><td>4. 用户特征</td><td><a href="/static/test_interface4.html" class="link">/static/test_interface4.html</a></td><td>上传照片 → 6项面部特征 + 原始JSON</td></tr>
|
||||
<tr><td>5. 发际线PNG</td><td><a href="/static/test_interface5.html" class="link">/static/test_interface5.html</a></td><td>上传+性别 → 发际线方案+中心点坐标</td></tr>
|
||||
<tr><td>6. 四庭七眼 v2</td><td><a href="/static/test_interface6.html" class="link">/static/test_interface6.html</a></td><td>同接口1,去顶庭 · 竖线发际线→下巴 · 无头部端线</td></tr>
|
||||
<tr><td>7. C端生发 v2</td><td><a href="/static/test_interface7.html" class="link">/static/test_interface7.html</a></td><td>同接口2,使用 add_hair2.json 工作流(Flux-2 Klein 9b)</td></tr>
|
||||
</table>
|
||||
<p style="font-size:12px;color:#94a3b8;margin-top:12px">
|
||||
完整 API 文档:<a href="/docs" class="link">/docs</a>(Swagger UI)
|
||||
|
||||
@@ -113,6 +113,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>生发效果图</label>
|
||||
<label class="checkbox-inline" style="font-weight:normal;display:flex;align-items:center;gap:6px">
|
||||
<input type="checkbox" id="genGrowImg" checked> generate_grow_image(默认开;关闭后跳过最耗时的生发,仅返回三档叠图与中心点)
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="submitBtn" onclick="submitTest()">🚀 提交</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="clearResults()">清除</button>
|
||||
</div>
|
||||
@@ -206,7 +212,7 @@ async function submitTest() {
|
||||
$('submitBtn').disabled = true; $('submitBtn').textContent = '⏳ ...';
|
||||
setStatus('请求中...', 'info'); $('resultsArea').classList.add('hidden');
|
||||
|
||||
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hair_style', checked.join(','));
|
||||
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hair_style', checked.join(',')); fd.append('generate_grow_image', $('genGrowImg').checked ? 'true' : 'false');
|
||||
const _reqStart = performance.now();
|
||||
try {
|
||||
const r = await fetch(API_BASE + '/api/v1/hairline/generate', { method:'POST', headers: { 'X-Internal-Token': 'dev-shared-secret-2026' }, body:fd });
|
||||
|
||||
Reference in New Issue
Block a user