save code

This commit is contained in:
xsl
2026-06-23 23:26:40 +08:00
parent 6e8113b3ff
commit 57d1a876cc
6 changed files with 116 additions and 66 deletions
+44 -23
View File
@@ -320,8 +320,12 @@ class ImageJsonBody(BaseModel):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def _face_measure_impl(image_file, image_url, image_base64): async def _face_measure_impl(image_file, image_url, image_base64, variant="v1"):
"""接口1/6 共用实现:四庭七眼测量 + 标注图生成。返回 (ok_dict, err_dict)。""" """接口1/6 共用实现:四庭七眼测量 + 标注图生成。返回 (ok_dict, err_dict)。
variant="v1"(接口1):完整四庭七眼 + 头部端线。
variant="v6"(接口6):去掉头顶/顶庭、不画人头最左/最右端线、数据去顶庭。
"""
# 1. 三选一取图 # 1. 三选一取图
raw, e = await resolve_image_bytes(image_file, image_url, image_base64) raw, e = await resolve_image_bytes(image_file, image_url, image_base64)
if e is not None: if e is not None:
@@ -368,12 +372,25 @@ async def _face_measure_impl(image_file, image_url, image_base64):
# 8. 测量 + 标注图 # 8. 测量 + 标注图
result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose) result = measure_face(landmarks, hair_mask, w, h, head_pose=head_pose)
annotated = create_annotated_image(image, result, hair_mask=hair_mask) annotated = create_annotated_image(image, result, hair_mask=hair_mask, variant=variant)
buf = BytesIO() buf = BytesIO()
annotated.save(buf, format="PNG") annotated.save(buf, format="PNG")
# 9. 拆分架构:返回 base64,不落盘不拼 URL(落盘改 URL 由网关完成) # 9. 拆分架构:返回 base64,不落盘不拼 URL(落盘改 URL 由网关完成)
data = result.to_response() data = result.to_response()
if variant == "v6":
# 接口6:数据去顶庭(重算三庭比例与总高,移除顶庭/头顶字段)
vd = result.vertical
base_px = vd["upper_court_px"] + vd["middle_court_px"] + vd["lower_court_px"]
data["four_courts"]["ratios"] = {
"upper_court": round(vd["upper_court_px"] / base_px, 3),
"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.upper_cm + result.middle_cm + result.lower_cm, 2)
data["landmarks"].pop("hair_top", None)
data["annotated_image_base64"] = base64.b64encode(buf.getvalue()).decode() data["annotated_image_base64"] = base64.b64encode(buf.getvalue()).decode()
return ok(data), None return ok(data), None
except Exception as ex: # noqa: BLE001 except Exception as ex: # noqa: BLE001
@@ -401,10 +418,11 @@ async def _face_measure_impl(image_file, image_url, image_base64):
**坐标说明**:所有坐标以原图像素为基准,原点为图片左上角,x 向右,y 向下。 **坐标说明**:所有坐标以原图像素为基准,原点为图片左上角,x 向右,y 向下。
**标注图片 UI 规范**(真实版本生效): **标注图片 UI 规范**(真实版本生效):
- 字体/线条颜色:`#FFFFFF 100%` - 字体/线条/箭头颜色:`#FFFFFF 100%`,透明底
- 四庭数值在图片**左侧**呈现,七眼间距**上下穿插**展示 - 字号/线宽/虚线/箭头按图片短边自适应缩放
- 字体:PingFangSC-Regular 10pt,线宽 1pt - 四庭数值(名+数值两行,不带 cm)在图片**左侧**呈现,七眼段宽**上下穿插**展示,底部标「单位cm」
- 横线/竖线渐变消失,虚线两侧带箭头 - 横线/竖线渐变消失并略超出端点;段宽/庭高用虚线 + 实心三角双箭头标示
- 竖线含人头最左/最右端线(取自头发分割轮廓),共 8 线 7 段
""", """,
responses={ responses={
200: { 200: {
@@ -483,11 +501,16 @@ async def face_measure(
description=f""" description=f"""
输入用户正面照,返回: 输入用户正面照,返回:
- 标注好四庭七眼数据的 **PNG 图片**(仅标注图层,不含人物) - 标注好四庭七眼数据的 **PNG 图片**(仅标注图层,不含人物)
- 庭(顶庭/上庭/中庭/下庭)各段**厘米数值及占比** - 庭(上庭/中庭/下庭)各段**厘米数值及占比****不含顶庭**
- 七眼(眼宽/脸宽/两眼间距)**厘米数值及占比** - 七眼(眼宽/脸宽/两眼间距)**厘米数值及占比**
- 个关键分界点的**原图像素坐标**头顶/发际线/眉心/鼻翼下缘/下巴尖) - 个关键分界点的**原图像素坐标**(发际线/眉心/鼻翼下缘/下巴尖)
功能与接口1 完全一致,复刻实现。 基于接口1 的变体,与接口1 的差异:
- **去顶庭**:不画头顶横线、不返回顶庭数据;`face_total_height_cm` 为三庭之和
- **竖线范围**:纵向竖线从发际线画到下巴尖(接口1 为头顶→下巴尖)
- **不画人头最左/最右端线**:仅七眼 6 点共 5 段标尺,不取头发轮廓端线(接口1 为 8 线 7 段)
其余(箭头/虚线/字体/单位cm/七眼数据)与接口1 一致。
{_image_fields_desc} {_image_fields_desc}
@@ -498,10 +521,10 @@ async def face_measure(
**坐标说明**:所有坐标以原图像素为基准,原点为图片左上角,x 向右,y 向下。 **坐标说明**:所有坐标以原图像素为基准,原点为图片左上角,x 向右,y 向下。
**标注图片 UI 规范**(真实版本生效): **标注图片 UI 规范**(真实版本生效):
- 字体/线条颜色:`#FFFFFF 100%` - 字体/线条/箭头颜色:`#FFFFFF 100%`,透明底
- 四庭数值在图片**左侧**呈现,七眼间距**上下穿插**展示 - 字号/线宽/虚线/箭头按图片短边自适应缩放
- 字体:PingFangSC-Regular 10pt,线宽 1pt - 三庭数值(名+数值两行,不带 cm)在图片**左侧**呈现,七眼段宽**上下穿插**展示,底部标「单位cm」
- 横线/竖线渐变消失,虚线两侧带箭头 - 段宽/庭高用虚线 + 实心三角双箭头标示
""", """,
responses={ responses={
200: { 200: {
@@ -514,17 +537,15 @@ async def face_measure(
"request_id": "mock-request-id", "request_id": "mock-request-id",
"data": { "data": {
"annotated_image_url": SAMPLE_IMAGE_URL, "annotated_image_url": SAMPLE_IMAGE_URL,
"face_total_height_cm": 13.76, "face_total_height_cm": 10.32,
"four_courts": { "four_courts": {
"top_court_cm": 3.44,
"upper_court_cm": 3.44, "upper_court_cm": 3.44,
"middle_court_cm": 3.44, "middle_court_cm": 3.44,
"lower_court_cm": 3.44, "lower_court_cm": 3.44,
"ratios": { "ratios": {
"top_court": 0.25, "upper_court": 0.333,
"upper_court": 0.25, "middle_court": 0.333,
"middle_court": 0.25, "lower_court": 0.333,
"lower_court": 0.25,
}, },
}, },
"seven_eyes": { "seven_eyes": {
@@ -534,7 +555,6 @@ async def face_measure(
"ratios": {"eye_width": 0.143, "inter_eye_distance": 0.143}, "ratios": {"eye_width": 0.143, "inter_eye_distance": 0.143},
}, },
"landmarks": { "landmarks": {
"hair_top": {"x": 540, "y": 120},
"hairline": {"x": 540, "y": 430}, "hairline": {"x": 540, "y": 430},
"brow_center": {"x": 540, "y": 740}, "brow_center": {"x": 540, "y": 740},
"nose_bottom": {"x": 540, "y": 1050}, "nose_bottom": {"x": 540, "y": 1050},
@@ -564,8 +584,9 @@ async def face_measure_v2(
image_url: Optional[str] = Form(default=None, description="图片 URL"), image_url: Optional[str] = Form(default=None, description="图片 URL"),
image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"),
): ):
"""接口6:四庭七眼测量标注 v2(复刻接口1)""" """接口6:四庭七眼测量标注 v2(复刻接口1,去顶庭 + 去头部端线"""
ok_data, err_data = await _face_measure_impl(image_file, image_url, image_base64) ok_data, err_data = await _face_measure_impl(
image_file, image_url, image_base64, variant="v6")
return ok_data if ok_data is not None else err_data return ok_data if ok_data is not None else err_data
+4 -3
View File
@@ -56,9 +56,10 @@
失败回退比例推算(方案A`hairline_source` 透出)。标注图 numpy 向量化渐变线 + 思源黑体。返回 `annotated_image_base64` 失败回退比例推算(方案A`hairline_source` 透出)。标注图 numpy 向量化渐变线 + 思源黑体。返回 `annotated_image_base64`
- 门槛可配:`MIN_SHORT_SIDE`/`MIN_LONG_SIDE`(默认600/800)、姿态阈值 `FRONTAL_*_THR`(默认30°)。 - 门槛可配:`MIN_SHORT_SIDE`/`MIN_LONG_SIDE`(默认600/800)、姿态阈值 `FRONTAL_*_THR`(默认30°)。
### 接口6 四庭七眼测量 v2 `/api/v1/face/measure-v2`worker)—— 复刻接口1 ### 接口6 四庭七眼测量 v2 `/api/v1/face/measure-v2`worker)—— 接口1 的去顶庭变体
- **做什么**接口 1 完全一致(正面照 → 四庭七眼 cm 与占比 + 5 个关键点 + 标注 PNG)。 - **做什么**基于接口 1,**去顶庭**:不画头顶横线、不返回顶庭数据(`four_courts` 仅上/中/下庭,`landmarks``hair_top``face_total_height_cm` 为三庭之和)。
- **怎么实现**与接口 1 共用 `_face_measure_impl()`,零额外逻辑。对外路径 `/api/v1/face/measure-v2` - **与接口1 的标注差异**`create_annotated_image(variant="v6")`):①竖线范围改为发际线→下巴尖;②不画人头最左/最右端线(仅七眼 6 点 5 段,接口1 为 8 线 7 段);③左侧只标上/中/下庭。箭头/虚线/字体等与接口1 一致
- **怎么实现**:与接口 1 共用 `_face_measure_impl(variant="v6")`;v6 时标注走变体分支、数据由 app.py 边界删顶庭字段并重算三庭比例。
- **网关改动**:新增路由 `POST /api/v1/face/measure-v2`,转发到 worker 同路径;base64→URL 改写无需改动。 - **网关改动**:新增路由 `POST /api/v1/face/measure-v2`,转发到 worker 同路径;base64→URL 改写无需改动。
### 接口2 C端生发 `/api/v1/hair/grow`worker)—— 预览 + 生发图 ### 接口2 C端生发 `/api/v1/hair/grow`worker)—— 预览 + 生发图
+21 -18
View File
@@ -133,13 +133,12 @@
| 项目 | 要求 | | 项目 | 要求 |
|------|------| |------|------|
| 字体及线颜色 | `#FFFFFF` 100% | | 颜色 | 字体及所有线/箭头 `#FFFFFF` 100%,透明底 |
| 数值排布 | 四庭数值统一在图片**左侧**呈现;七眼间距**上下穿插**展示 | | 尺寸 | 字号/线宽/虚线/箭头按图片**短边自适应缩放**(非固定 pt) |
| 字体 | PingFangSC-Regular,字号 10pt | | 横线 | 5 条分界线(头顶/发际线/眉心/鼻翼下缘/下巴尖),两端渐变消失并略超出最外侧竖线;线名在线**右上方** |
| 线 | 横线、竖线、虚线均为 1pt | | 线 | 人头最左 + 七眼 6 点 + 人头最右(最外两条取自头发分割轮廓),两端渐变消失并略超出头顶/下巴 |
| 线样式 | 横线、竖线渐变消失;虚线两侧呈现箭头 | | 数值排布 | 四庭数值(名 + 数值两行,**不带 cm**)统一在图片**左侧**呈现;七眼段宽**上下穿插**展示;底部统一标「单位cm」 |
| 线样式 | 段宽/庭高用**虚线 + 实心三角双箭头**标示(箭头尖端落在虚线两端) |
> **【待确认】** 标注图片需提供设计稿后才能最终确定样式。
### 响应示例(当前 Mock 返回值) ### 响应示例(当前 Mock 返回值)
@@ -179,7 +178,12 @@
## 接口 6:四庭七眼测量 v2 接口 ## 接口 6:四庭七眼测量 v2 接口
**说明**功能与[接口 1](#接口-1四庭七眼测量标注接口)完全一致,复刻实现。输入用户正面照,返回四庭七眼测量数据和标注 PNG。 **说明**基于[接口 1](#接口-1四庭七眼测量标注接口)的变体。与接口 1 的差异:
- **去顶庭**:不画头顶横线、不返回顶庭数据。`four_courts` 仅含上/中/下庭,`landmarks``hair_top``face_total_height_cm` 为三庭之和(不含顶庭)。
- **竖线范围**:纵向竖线从**发际线**画到**下巴尖**(接口 1 为头顶→下巴尖)。
- **不画人头最左/最右端线**:仅画七眼 6 点(左脸颊/左眼外角/左眼内角/右眼内角/右眼外角/右脸颊)共 5 段标尺,不取头发轮廓的头部端线(接口 1 会多出最左/最右 2 条头部端线、共 7 段)。
- 其余(实心三角箭头、虚线样式、字体、单位cm、七眼数据)与接口 1 一致。
**请求**`POST /api/v1/face/measure-v2` **请求**`POST /api/v1/face/measure-v2`
@@ -189,15 +193,13 @@
### 输出(data ### 输出(data
与接口 1 完全相同。详见[接口 1 输出](#接口-1四庭七眼测量标注接口)。
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| annotated_image_url | string | 标注图层 PNG URL(透明底,仅标注线/文字,不含人物) | | annotated_image_url | string | 标注图层 PNG URL(透明底,仅标注线/文字,不含人物) |
| face_total_height_cm | number | 面部总高度(cm | | face_total_height_cm | number | 面部总高度(cm= 上庭 + 中庭 + 下庭(**不含顶庭**) |
| four_courts | object | 庭数据(顶庭/上庭/中/下庭,各含 cm 与 ratio | | four_courts | object | 庭数据(/中/下庭,各含 cm 与 ratio**无顶庭** |
| seven_eyes | object | 七眼数据(眼宽/脸宽/两眼间距,各含 cm 与 ratio) | | seven_eyes | object | 七眼数据(眼宽/脸宽/两眼间距,各含 cm 与 ratio) |
| landmarks | object | 个关键点像素坐标 | | landmarks | object | 个关键点像素坐标(发际线/眉心/鼻翼下缘/下巴尖) |
### 响应示例 ### 响应示例
@@ -208,18 +210,19 @@
"request_id": "mock-request-id", "request_id": "mock-request-id",
"data": { "data": {
"annotated_image_url": "https://hair.xiangsilian.com/static/sample.jpg", "annotated_image_url": "https://hair.xiangsilian.com/static/sample.jpg",
"face_total_height_cm": 13.76, "face_total_height_cm": 10.32,
"four_courts": { "four_courts": {
"top_court_cm": 3.44, "upper_court_cm": 3.44, "middle_court_cm": 3.44, "lower_court_cm": 3.44, "upper_court_cm": 3.44, "middle_court_cm": 3.44, "lower_court_cm": 3.44,
"ratios": { "top_court": 0.25, "upper_court": 0.25, "middle_court": 0.25, "lower_court": 0.25 } "ratios": { "upper_court": 0.333, "middle_court": 0.333, "lower_court": 0.333 }
}, },
"seven_eyes": { "seven_eyes": {
"eye_width_cm": 3.44, "face_width_cm": 24.08, "inter_eye_distance_cm": 3.44, "eye_width_cm": 3.44, "face_width_cm": 24.08, "inter_eye_distance_cm": 3.44,
"ratios": { "eye_width": 0.143, "inter_eye_distance": 0.143 } "ratios": { "eye_width": 0.143, "inter_eye_distance": 0.143 }
}, },
"landmarks": { "landmarks": {
"hair_top": { "x": 540, "y": 120 }, "hairline": { "x": 540, "y": 430 }, "hairline": { "x": 540, "y": 430 },
"brow_center": { "x": 540, "y": 740 }, "nose_bottom": { "x": 540, "y": 1050 }, "brow_center": { "x": 540, "y": 740 },
"nose_bottom": { "x": 540, "y": 1050 },
"chin_tip": { "x": 540, "y": 1360 } "chin_tip": { "x": 540, "y": 1360 }
} }
} }
+5 -5
View File
@@ -113,25 +113,25 @@ async def hair_grow_v2(request: Request):
--- ---
## 6. 🔲【新增】接口6 四庭七眼测量 v2`/api/v1/face/measure-v2` ## 6. ✅【已完成】接口6 四庭七眼测量 v2`/api/v1/face/measure-v2`
**背景**worker 侧已新增接口 6,功能与接口 1 完全一致(复刻),共用同一实现 **背景**worker 侧接口 6接口 1 的**去顶庭变体**(不再完全一致):不画头顶横线/不返回顶庭数据、竖线范围发际线→下巴尖、不画人头最左/最右端线。worker 侧与接口 1 共用 `_face_measure_impl(variant="v6")`
**网关新增一个路由** **网关新增路由**`gateway/app.py` 已实现,网关机器 pull 后生效)
```python ```python
# gateway/app.py # gateway/app.py
@app.post("/api/v1/face/measure-v2", tags=["人脸分析"]) @app.post("/api/v1/face/measure-v2", tags=["人脸分析"])
async def face_measure_v2(request: Request): async def face_measure_v2(request: Request):
"""接口6:四庭七眼测量 v2复刻接口1""" """接口6:四庭七眼测量 v2去顶庭 + 去头部端线"""
return await _proxy(request, "/api/v1/face/measure-v2") return await _proxy(request, "/api/v1/face/measure-v2")
``` ```
**无需额外改动** **无需额外改动**
- 入参:与接口 1 完全相同(image_file/url/base64 三选一) - 入参:与接口 1 完全相同(image_file/url/base64 三选一)
- 出参:`annotated_image_base64` → 经现有 `rewrite_base64_to_url` 自动改写为 `annotated_image_url` - 出参:`annotated_image_base64` → 经现有 `rewrite_base64_to_url` 自动改写为 `annotated_image_url`
- worker 侧与接口 1 共用 `_face_measure_impl()`,逻辑零差异 - worker 侧 v6 差异(去顶庭字段、变体标注)由 `_face_measure_impl` 内部处理,网关透明转发
--- ---
+36 -17
View File
@@ -141,7 +141,7 @@ def _head_edges_from_mask(hair_mask, y0, y1, left_cheek_x, right_cheek_x, min_ga
hr if (hr - right_cheek_x) >= min_gap else None) hr if (hr - right_cheek_x) >= min_gap else None)
def create_annotated_image(image_bgr, measure_result, hair_mask=None): def create_annotated_image(image_bgr, measure_result, hair_mask=None, variant="v1"):
"""生成标注图层 PNG(透明底 RGBA,尺寸同原图)。返回 PIL.Image。 """生成标注图层 PNG(透明底 RGBA,尺寸同原图)。返回 PIL.Image。
布局(对齐 img1.png 版本5): 布局(对齐 img1.png 版本5):
@@ -152,6 +152,10 @@ def create_annotated_image(image_bgr, measure_result, hair_mask=None):
- 四庭(顶/上/中/下庭)在左侧:名 + 数值两行换行(无 cm),竖向虚线双箭头。 - 四庭(顶/上/中/下庭)在左侧:名 + 数值两行换行(无 cm),竖向虚线双箭头。
- 七眼段宽数值上下交替(上 3 / 下 4,无 cm),横向虚线双箭头。 - 七眼段宽数值上下交替(上 3 / 下 4,无 cm),横向虚线双箭头。
- 底部统一标「单位cm」。 - 底部统一标「单位cm」。
variant="v6"(接口6):去掉头顶横线与顶庭(只画发际线/眉心/鼻翼下缘/下巴尖 4 条
横线 + 上/中/下庭),竖线纵向范围改为发际线→下巴尖,且不画人头最左/最右端线
(仅七眼 6 点 5 段,不取头部端)。
""" """
h, w = image_bgr.shape[:2] h, w = image_bgr.shape[:2]
v = measure_result.vertical v = measure_result.vertical
@@ -169,21 +173,27 @@ def create_annotated_image(image_bgr, measure_result, hair_mask=None):
buf = np.zeros((h, w, 4), dtype=np.uint8) buf = np.zeros((h, w, 4), dtype=np.uint8)
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"] if variant == "v6":
order = ["hairline", "brow_center", "nose_bottom", "chin_tip"]
else:
order = ["hair_top", "hairline", "brow_center", "nose_bottom", "chin_tip"]
ys = [v[name][1] for name in order] ys = [v[name][1] for name in order]
pts = measure_result.eyes["points"] pts = measure_result.eyes["points"]
seven_keys = ["left_cheek", "left_outer", "left_inner", seven_keys = ["left_cheek", "left_outer", "left_inner",
"right_inner", "right_outer", "right_cheek"] "right_inner", "right_outer", "right_cheek"]
base_xs = [pts[k][0] for k in seven_keys] base_xs = [pts[k][0] for k in seven_keys]
# 人头最左/最右:取自头发分割掩膜(方案B),脸纵向范围内统计;无掩膜则省略。 if variant == "v6":
# 与脸颊线间距 < 脸宽×8% 视为太近,只保留脸颊线(不画头部端线)。 xs = sorted(base_xs) # 接口6:仅七眼 6 点,不画人头最左/最右端线
face_w = pts["right_cheek"][0] - pts["left_cheek"][0] else:
min_gap = max(1.0, face_w * 0.08) # 人头最左/最右:取自头发分割掩膜(方案B),脸纵向范围内统计;无掩膜则省略。
head_l, head_r = _head_edges_from_mask( # 与脸颊线间距 < 脸宽×8% 视为太近,只保留脸颊线(不画头部端线)。
hair_mask, ys[0], ys[-1], pts["left_cheek"][0], pts["right_cheek"][0], min_gap) face_w = pts["right_cheek"][0] - pts["left_cheek"][0]
head_xs = [x for x in (head_l, head_r) if x is not None] min_gap = max(1.0, face_w * 0.08)
xs = sorted(base_xs + head_xs) # 自左向右(6 或 7/8 点) head_l, head_r = _head_edges_from_mask(
hair_mask, ys[0], ys[-1], pts["left_cheek"][0], pts["right_cheek"][0], min_gap)
head_xs = [x for x in (head_l, head_r) if x is not None]
xs = sorted(base_xs + head_xs) # 自左向右(6 或 7/8 点)
# 人脸/人头包围盒 # 人脸/人头包围盒
fx0, fx1 = xs[0], xs[-1] fx0, fx1 = xs[0], xs[-1]
@@ -191,14 +201,17 @@ def create_annotated_image(image_bgr, measure_result, hair_mask=None):
face_cx = (fx0 + fx1) / 2 face_cx = (fx0 + fx1) / 2
over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格) over = max(6, round(s * 0.030)) # 线超出包围盒的长度(参考图风格)
face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点 face_half = (fx1 - fx0) / 2 + over # 横线超出最外侧竖线一点
# v6 竖线纵向范围 = 发际线→下巴尖(不超出);v1 = 头顶→下巴尖并两端超出一点
v_top = fy0 if variant == "v6" else fy0 - over
v_bot = fy1 if variant == "v6" else fy1 + over
# --- 1. 横向 5 条分界线(渐变,覆盖头宽并超出一点) --- # --- 1. 横向分界线(渐变,覆盖头宽并超出一点) ---
for cy in ys: for cy in ys:
draw_gradient_horizontal_line(buf, face_cx, cy, half_length=face_half, width=line_w) draw_gradient_horizontal_line(buf, face_cx, cy, half_length=face_half, width=line_w)
# --- 2. 纵向竖线(渐变,超出头顶/下巴一点 --- # --- 2. 纵向竖线(渐变,覆盖 v_top→v_bot ---
for vx in xs: for vx in xs:
draw_gradient_vertical_line(buf, vx, fy0 - over, fy1 + over, width=line_w) draw_gradient_vertical_line(buf, vx, v_top, v_bot, width=line_w)
canvas = Image.fromarray(buf, mode="RGBA") canvas = Image.fromarray(buf, mode="RGBA")
draw = ImageDraw.Draw(canvas) draw = ImageDraw.Draw(canvas)
@@ -214,11 +227,17 @@ def create_annotated_image(image_bgr, measure_result, hair_mask=None):
draw.text((x, max(2, ys[i] - th - name_gap)), text, fill=LINE_COLOR, font=font) draw.text((x, max(2, ys[i] - th - name_gap)), text, fill=LINE_COLOR, font=font)
# --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 --- # --- 3b. 左侧四庭:名 + 数值两行(无 cm)+ 竖向虚线双箭头 ---
court_cm = [measure_result.top_cm, measure_result.upper_cm, if variant == "v6":
measure_result.middle_cm, measure_result.lower_cm] court_cm = [measure_result.upper_cm, measure_result.middle_cm, measure_result.lower_cm]
court_name = ["顶庭", "上庭", "中庭", "下庭"] court_name = ["上庭", "中庭", "下庭"]
n_court = 3
else:
court_cm = [measure_result.top_cm, measure_result.upper_cm,
measure_result.middle_cm, measure_result.lower_cm]
court_name = ["顶庭", "上庭", "中庭", "下庭"]
n_court = 4
arrow_x = max(arrow_size + 1, fx0 - pad) # 竖箭头所在 x(脸左侧,贴近最左竖线) arrow_x = max(arrow_size + 1, fx0 - pad) # 竖箭头所在 x(脸左侧,贴近最左竖线)
for i in range(4): for i in range(n_court):
y_a, y_b = ys[i], ys[i + 1] y_a, y_b = ys[i], ys[i + 1]
# 竖向虚线双箭头,覆盖该庭高度(略收一点避免压到横线) # 竖向虚线双箭头,覆盖该庭高度(略收一点避免压到横线)
inset = min(arrow_size, (y_b - y_a) * 0.12) inset = min(arrow_size, (y_b - y_a) * 0.12)
+6
View File
@@ -240,6 +240,12 @@ async def face_measure(request: Request):
return await _proxy(request, "/api/v1/face/measure") return await _proxy(request, "/api/v1/face/measure")
@app.post("/api/v1/face/measure-v2", tags=["人脸分析"])
async def face_measure_v2(request: Request):
"""接口6:四庭七眼测量标注 v2(去顶庭 + 去头部端线)"""
return await _proxy(request, "/api/v1/face/measure-v2")
@app.post("/api/v1/hair/grow", tags=["生发"]) @app.post("/api/v1/hair/grow", tags=["生发"])
async def hair_grow(request: Request): async def hair_grow(request: Request):
"""接口2C端生发""" """接口2C端生发"""