save code
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
"""接口2:C端生发"""
|
"""接口2:C端生发"""
|
||||||
|
|||||||
Reference in New Issue
Block a user