diff --git a/app.py b/app.py index 6102c83..4654848 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,554 @@ -from fastapi import FastAPI +"""旷视五接口 - 第一版(Mock 实现) -app = FastAPI(title="hair") +当前为 Mock 服务,所有接口返回固定示例值,不做真实算法计算。 +图片字段统一返回 https://hair.xiangsilian.com/static/sample.jpg。 +""" +import json +from typing import Any, List, Optional + +from fastapi import FastAPI, File, Form, UploadFile +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field + +# --------------------------------------------------------------------------- +# App & 全局常量 +# --------------------------------------------------------------------------- + +BASE_URL = "https://hair.xiangsilian.com" +SAMPLE_IMAGE_URL = f"{BASE_URL}/static/sample.jpg" + +app = FastAPI( + title="旷视五接口", + version="0.1.0", + description=""" +## 概述 + +本服务提供五个人像分析接口,当前为 **Mock 第一版**: +- 接口已全部上线,传任意合法参数均可正常响应 +- 所有字段返回固定示例值,图片字段统一指向示例图片 +- 真实算法逻辑后续接入,字段结构不变 + +## 图片传参说明 + +每个接口的图片参数均支持三种方式,**严格互斥,必须且只能选其一**: + +| 方式 | 字段名 | 说明 | +|------|--------|------| +| 文件上传 | `image_file` | `multipart/form-data`,单文件 ≤ 1 MB | +| URL | `image_url` | 图片的完整 HTTP/HTTPS 地址 | +| base64 | `image_base64` | 需携带前缀,如 `data:image/jpeg;base64,xxxx` | + +> 传 0 个或同时传多个,均返回错误码 `1007`。 + +## 图片要求 + +- 格式:**JPG / PNG** +- 分辨率:最低 1080×1920,最大 4000×5000 +- 人脸数量:仅支持**单人**,多人返回错误码 `1005` +- 文件大小:≤ 1 MB(文件上传方式) + +## 统一响应结构 + +```json +{ + "code": 0, + "message": "success", + "request_id": "唯一请求标识", + "data": {} +} +``` + +`code = 0` 表示成功,非 0 表示失败,`message` 为具体原因。 + +## 错误码一览 + +| code | 说明 | +|------|------| +| 1001 | 无法识别人像 | +| 1002 | 人像分辨率过低 | +| 1003 | 非正面照 / 角度过大 | +| 1004 | 性别标签无法判定 | +| 1005 | 检测到多张人脸(仅支持单人)| +| 1006 | 文件超出 1 MB 限制 | +| 1007 | 图片参数错误(未传或同时传多个)| +| 1008 | 图片格式不支持(仅 JPG / PNG)| +""", +) + +app.mount("/static", StaticFiles(directory="static"), name="static") -@app.get("/") -async def hello(): - return {"message": "Hello, World!"} +# --------------------------------------------------------------------------- +# 通用响应模型 +# --------------------------------------------------------------------------- + +def ok(data: Any) -> dict: + return { + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": data, + } -@app.get("/health") +def err(code: int, message: str) -> dict: + return { + "code": code, + "message": message, + "request_id": "mock-request-id", + "data": None, + } + + +# --------------------------------------------------------------------------- +# 通用图片请求 Body(JSON 方式,用于 url / base64) +# --------------------------------------------------------------------------- + +_image_fields_desc = ( + "图片传参方式严格互斥,必须且只能选其一:\n" + "- **image_file**(multipart/form-data 上传,≤ 1 MB)\n" + "- **image_url**(完整 HTTP/HTTPS 地址)\n" + "- **image_base64**(需携带前缀,如 `data:image/jpeg;base64,xxxx`)\n\n" + "同时传多个或一个都不传,均返回错误码 `1007`。" +) + + +class ImageJsonBody(BaseModel): + image_url: Optional[str] = Field( + default=None, + description="图片 URL(与 image_base64 二选一,不可同时传)", + examples=["https://hair.xiangsilian.com/static/sample.jpg"], + ) + image_base64: Optional[str] = Field( + default=None, + description="图片 base64,需带前缀,如 `data:image/jpeg;base64,xxxx`", + examples=["data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."], + ) + + model_config = { + "json_schema_extra": { + "examples": [ + { + "summary": "使用 URL 传图", + "value": {"image_url": "https://hair.xiangsilian.com/static/sample.jpg"}, + }, + { + "summary": "使用 base64 传图", + "value": {"image_base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."}, + }, + ] + } + } + + +# --------------------------------------------------------------------------- +# 接口 1:四庭七眼测量标注 +# --------------------------------------------------------------------------- + +@app.post( + "/api/v1/face/measure", + summary="接口1 四庭七眼测量标注", + tags=["人脸分析"], + description=f""" +输入用户正面照,返回: +- 标注好四庭七眼数据的 **PNG 图片**(仅标注图层,不含人物) +- 四庭(顶庭/上庭/中庭/下庭)各段**厘米数值及占比** +- 七眼(眼宽/脸宽/两眼间距)**厘米数值及占比** +- 五个关键分界点的**原图像素坐标**(头顶/发际线/眉心/鼻翼下缘/下巴尖) + +{_image_fields_desc} + +图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`),或 JSON Body 传 `image_url` / `image_base64`。 + +--- + +**坐标说明**:所有坐标以原图像素为基准,原点为图片左上角,x 向右,y 向下。 + +**标注图片 UI 规范**(真实版本生效): +- 字体/线条颜色:`#FFFFFF 100%` +- 四庭数值在图片**左侧**呈现,七眼间距**上下穿插**展示 +- 字体:PingFangSC-Regular 10pt,线宽 1pt +- 横线/竖线渐变消失,虚线两侧带箭头 +""", + responses={ + 200: { + "description": "成功", + "content": { + "application/json": { + "example": { + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "annotated_image_url": SAMPLE_IMAGE_URL, + "face_total_height_cm": 13.76, + "four_courts": { + "top_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, + }, + }, + "seven_eyes": { + "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}, + }, + "landmarks": { + "hair_top": {"x": 540, "y": 120}, + "hairline": {"x": 540, "y": 430}, + "brow_center": {"x": 540, "y": 740}, + "nose_bottom": {"x": 540, "y": 1050}, + "chin_tip": {"x": 540, "y": 1360}, + }, + }, + } + } + }, + }, + 400: { + "description": "参数错误 / 图片识别失败", + "content": { + "application/json": { + "examples": { + "图片参数错误": {"value": {"code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", "request_id": "x", "data": None}}, + "无法识别人像": {"value": {"code": 1001, "message": "无法识别人像", "request_id": "x", "data": None}}, + "多张人脸": {"value": {"code": 1005, "message": "检测到多张人脸,仅支持单人照片", "request_id": "x", "data": None}}, + } + } + }, + }, + }, +) +async def face_measure( + image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + image_url: Optional[str] = Form(default=None, description="图片 URL"), + image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), +): + data = { + "annotated_image_url": SAMPLE_IMAGE_URL, + "face_total_height_cm": 13.76, + "four_courts": { + "top_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, + }, + }, + "seven_eyes": { + "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}, + }, + "landmarks": { + "hair_top": {"x": 540, "y": 120}, + "hairline": {"x": 540, "y": 430}, + "brow_center": {"x": 540, "y": 740}, + "nose_bottom": {"x": 540, "y": 1050}, + "chin_tip": {"x": 540, "y": 1360}, + }, + } + return ok(data) + + +# --------------------------------------------------------------------------- +# 接口 2:C 端生发 +# --------------------------------------------------------------------------- + +@app.post( + "/api/v1/hair/grow", + summary="接口2 C端生发", + tags=["生发"], + description=f""" +输入用户正面照,返回多个生发方案,每个方案包含: +- 生发后效果图 URL +- 对应的发际线形(如花瓣形、波浪形) +- 合适度排序(order=1 最优) + +{_image_fields_desc} + +图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。 + +--- + +**beauty_enabled**:是否对生发后图片开启美颜,默认 `false`。 +""", + responses={ + 200: { + "description": "成功", + "content": { + "application/json": { + "example": { + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "results": [ + {"image_url": SAMPLE_IMAGE_URL, "hairline_type": "花瓣形", "order": 1}, + {"image_url": SAMPLE_IMAGE_URL, "hairline_type": "波浪形", "order": 2}, + ] + }, + } + } + }, + }, + 400: { + "description": "参数错误 / 图片识别失败", + "content": { + "application/json": { + "examples": { + "图片参数错误": {"value": {"code": 1007, "message": "图片参数错误:必须且只能传 image_file / image_url / image_base64 其中一个", "request_id": "x", "data": None}}, + "非正面照": {"value": {"code": 1003, "message": "角度问题,请上传正面照", "request_id": "x", "data": None}}, + } + } + }, + }, + }, +) +async def hair_grow( + image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + image_url: Optional[str] = Form(default=None, description="图片 URL"), + image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), + beauty_enabled: bool = Form(default=False, description="是否开启美颜效果,默认 false"), +): + data = { + "results": [ + {"image_url": SAMPLE_IMAGE_URL, "hairline_type": "花瓣形", "order": 1}, + {"image_url": SAMPLE_IMAGE_URL, "hairline_type": "波浪形", "order": 2}, + ] + } + return ok(data) + + +# --------------------------------------------------------------------------- +# 接口 3:B 端生发 +# --------------------------------------------------------------------------- + +@app.post( + "/api/v1/hair/grow-b", + summary="接口3 B端生发(医生/操作端)", + tags=["生发"], + description=""" +医生/操作端在用户原图上**手动划线标注**目标发际线后,上传划线图与原图,返回: +- 最合适的发际线图片 URL +- 生发后效果图 URL +- 发际线形 + +**划线图片**:字段名前缀为 `marked_image_`,同样支持文件/URL/base64 三选一。 + +**原始用户照片**:字段名前缀为 `original_image_`,同样支持文件/URL/base64 三选一,**必填**。 + +> 两组图片的传参方式可以不同,例如划线图用 URL,原图用 base64,但各自组内严格互斥。 +""", + responses={ + 200: { + "description": "成功", + "content": { + "application/json": { + "example": { + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "best_hairline_image_url": SAMPLE_IMAGE_URL, + "hair_growth_image_url": SAMPLE_IMAGE_URL, + "hairline_type": "花瓣形", + }, + } + } + }, + }, + 400: { + "description": "参数错误", + "content": { + "application/json": { + "example": {"code": 1007, "message": "图片参数错误", "request_id": "x", "data": None} + } + }, + }, + }, +) +async def hair_grow_b( + marked_image_file: Optional[UploadFile] = File(default=None, description="划线图片文件(JPG/PNG,≤ 1 MB)"), + marked_image_url: Optional[str] = Form(default=None, description="划线图片 URL"), + marked_image_base64: Optional[str] = Form(default=None, description="划线图片 base64"), + original_image_file: Optional[UploadFile] = File(default=None, description="原始用户照片文件(JPG/PNG,≤ 1 MB)"), + original_image_url: Optional[str] = Form(default=None, description="原始用户照片 URL"), + original_image_base64: Optional[str] = Form(default=None, description="原始用户照片 base64"), +): + data = { + "best_hairline_image_url": SAMPLE_IMAGE_URL, + "hair_growth_image_url": SAMPLE_IMAGE_URL, + "hairline_type": "花瓣形", + } + return ok(data) + + +# --------------------------------------------------------------------------- +# 接口 4:用户特征 +# --------------------------------------------------------------------------- + +@app.post( + "/api/v1/face/features", + summary="接口4 用户特征分析", + tags=["人脸分析"], + description=f""" +输入用户照片,返回 N 个用户面部特征字段。 + +{_image_fields_desc} + +图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。 + +--- + +**返回格式**:`data.features` 为一个 **JSON 字符串**(不是对象),需要在客户端 `JSON.parse()` 后使用。 + +当前优先返回字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| face_shape | string | 脸形,如"鹅蛋脸" | +| eyebrow_shape | string | 眉形,如"柳叶眉" | +| facial_age | int | 面部年龄 | +| dynamic_static_type | string | 动静类型,如"静态" | +| gender | string | 性别,如"女" | +| gene_style | object | 面部特征对应的基因风格 | + +> 字段后续可随时增删,不固定,客户端按需取用即可。 +""", + responses={ + 200: { + "description": "成功", + "content": { + "application/json": { + "example": { + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "features": '{"face_shape": "鹅蛋脸", "eyebrow_shape": "柳叶眉", "facial_age": 26, "dynamic_static_type": "静态", "gender": "女", "gene_style": {"label": "面部特征标签", "style": "基因风格示例"}}', + }, + } + } + }, + }, + 400: { + "description": "参数错误 / 图片识别失败", + "content": { + "application/json": { + "example": {"code": 1001, "message": "无法识别人像", "request_id": "x", "data": None} + } + }, + }, + }, +) +async def face_features( + image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + image_url: Optional[str] = Form(default=None, description="图片 URL"), + image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), +): + features = json.dumps( + { + "face_shape": "鹅蛋脸", + "eyebrow_shape": "柳叶眉", + "facial_age": 26, + "dynamic_static_type": "静态", + "gender": "女", + "gene_style": {"label": "面部特征标签", "style": "基因风格示例"}, + }, + ensure_ascii=False, + ) + return ok({"features": features}) + + +# --------------------------------------------------------------------------- +# 接口 5:发际线 PNG 生成 +# --------------------------------------------------------------------------- + +@app.post( + "/api/v1/hairline/generate", + summary="接口5 发际线PNG生成", + tags=["人脸分析"], + description=f""" +输入用户照片,返回 N 张用户发际线的 PNG 图片,并标注最合适发际线的面部中间点坐标。 + +{_image_fields_desc} + +图片同时支持 `multipart/form-data` 文件上传(字段名 `image_file`)。 + +--- + +**返回说明**: + +- `hairline_images`:发际线 PNG 列表,数量 N 不固定,已按合适度**从高到低排序**(`order=1` 最合适) +- `best_hairline_center_point`:最合适发际线的面部中间点坐标,以**原图像素**为基准(左上角为原点,x 向右,y 向下) +""", + responses={ + 200: { + "description": "成功", + "content": { + "application/json": { + "example": { + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "hairline_images": [ + {"image_url": SAMPLE_IMAGE_URL, "order": 1}, + {"image_url": SAMPLE_IMAGE_URL, "order": 2}, + ], + "best_hairline_center_point": {"x": 540, "y": 430}, + }, + } + } + }, + }, + 400: { + "description": "参数错误 / 图片识别失败", + "content": { + "application/json": { + "example": {"code": 1002, "message": "人像分辨率过低", "request_id": "x", "data": None} + } + }, + }, + }, +) +async def hairline_generate( + image_file: Optional[UploadFile] = File(default=None, description="上传图片文件(JPG/PNG,≤ 1 MB)"), + image_url: Optional[str] = Form(default=None, description="图片 URL"), + image_base64: Optional[str] = Form(default=None, description="图片 base64(需带 data:image/...;base64, 前缀)"), +): + data = { + "hairline_images": [ + {"image_url": SAMPLE_IMAGE_URL, "order": 1}, + {"image_url": SAMPLE_IMAGE_URL, "order": 2}, + ], + "best_hairline_center_point": {"x": 540, "y": 430}, + } + return ok(data) + + +# --------------------------------------------------------------------------- +# 健康检查 +# --------------------------------------------------------------------------- + +@app.get("/health", include_in_schema=False) async def health(): return {"status": "ok"} + + +@app.get("/", include_in_schema=False) +async def index(): + return {"service": "旷视五接口", "version": "0.1.0", "docs": f"{BASE_URL}/docs"} diff --git a/docs/接口文档.md b/docs/接口文档.md new file mode 100644 index 0000000..fbcd9d9 --- /dev/null +++ b/docs/接口文档.md @@ -0,0 +1,370 @@ +# 旷视五个接口 — 接口文档(输入 / 输出定义) + +> 本文档依据《旷视具体需求.docx》整理,描述各接口的**输入**与**输出**。 +> 文中标记为 **【待确认】** 的内容为需求文档中尚未明确、需与对方对齐的点。 + +> **当前为第一版 Mock 服务**:所有接口已上线,但**不做真实算法计算**,统一返回下文「响应示例」中的默认值;所有图片字段均返回示例图 `https://hair.xiangsilian.com/static/sample.jpg`。真实逻辑后续接入。 + +--- + +## 服务地址与接口路径 + +- **Base URL**:`https://hair.xiangsilian.com` +- **在线接口文档(Swagger)**:`https://hair.xiangsilian.com/docs` + +| 接口 | 方法 | 路径 | +|------|------|------| +| 1 四庭七眼测量 | POST | `/api/v1/face/measure` | +| 2 C 端生发 | POST | `/api/v1/hair/grow` | +| 3 B 端生发 | POST | `/api/v1/hair/grow-b` | +| 4 用户特征 | POST | `/api/v1/face/features` | +| 5 发际线 PNG 生成 | POST | `/api/v1/hairline/generate` | + +--- + +## 通用约定 + +以下约定适用于全部 5 个接口(最终以联调为准)。 + +### 请求 + +| 项目 | 约定 | +|------|------| +| 协议 | HTTPS | +| 请求方式 | `POST` | +| 图片传参方式 | 三选一,三种都支持:① `multipart/form-data` 上传文件、② 图片 URL、③ base64 字符串 | +| 字符编码 | UTF-8 | +| 坐标基准 | 所有返回的坐标均以**原图像素**为基准(原点为图片左上角,x 向右、y 向下) | + +图片传参字段(三者**严格互斥**,必须且只能传其一;传了 0 个或 ≥2 个均报错): + +| 参数 | 类型 | 说明 | +|------|------|------| +| image_file | file | 上传文件(`multipart/form-data`) | +| image_url | string | 图片 URL | +| image_base64 | string | 图片 base64 字符串,**需带前缀**,如 `data:image/png;base64,xxxx` | + +### 图片输入要求(接口 1/2/3/4/5 通用) + +| 项目 | 要求 | +|------|------| +| 图片类型 | 用户正面照(人像) | +| 人脸数量 | 默认仅支持**单人**;若检测到 2 或 3 张人脸(即多人),报错并提示客户端 | +| 单文件大小 | ≤ 1 MB | +| 最低分辨率 | 720 × 1024 | +| 最大分辨率 | 2160 × 3840 | +| 格式 | 目前仅支持 JPG / PNG | + +### 统一响应结构 + +```json +{ + "code": 0, + "message": "success", + "request_id": "唯一请求标识", + "data": { } +} +``` + +| 字段 | 类型 | 说明 | +|------|------|------| +| code | int | 0 表示成功,非 0 表示失败 | +| message | string | 结果描述 / 错误原因 | +| request_id | string | 请求追踪 ID | +| data | object | 业务数据,结构见各接口 | + +### 通用错误码(识别失败时返回,见需求文档) + +| code | message | 说明 | +|------|---------|------| +| 1001 | 无法识别人像 | 图片中未检测到人脸 | +| 1002 | 人像分辨率过低 | 低于最低分辨率要求 | +| 1003 | 角度问题,非正面照 | 非正面 / 角度过大 | +| 1004 | 性别标签判断异常 | 男女标签无法判定 **【待确认】** 是否作为错误 | +| 1005 | 检测到多张人脸 | 默认仅支持单人,检测到 2 人或以上时返回 | +| 1006 | 文件超出大小限制 | 单文件超过 1 MB | +| 1007 | 图片参数错误 | file / url / base64 未传,或同时传了多个(三者严格互斥) | +| 1008 | 图片格式不支持 | 非 JPG / PNG | + +--- + +## 接口 1:四庭七眼测量标注接口 + +**说明**:输入用户照片,输出一张标注好四庭七眼数据的 PNG 图片(**不包含人物本体,仅标注图层**),同时返回四庭、七眼的比例与厘米数值。 + +**请求**:`POST /api/v1/face/measure` + +### 输入 + +图片参数见「通用约定 → 图片传参字段」(`image_file` / `image_url` / `image_base64` 三选一)。本接口无其他专属参数。 + +### 输出(data) + +| 字段 | 类型 | 说明 | +|------|------|------| +| annotated_image_url | string | 标注好数据的 PNG 图片(仅标注图层,不含人物) | +| face_total_height_cm | number | 全脸总高度(cm) | +| four_courts | object | 四庭数据,见下表 | +| seven_eyes | object | 七眼数据,见下表 | +| landmarks | object | 关键分界点坐标(头顶 / 发际线 / 眉心 / 鼻翼下缘 / 下巴尖),原图像素坐标 | + +`four_courts`(四庭,自上而下): + +| 字段 | 类型 | 说明 | +|------|------|------| +| top_court_cm | number | 顶庭长度(cm),示例:约占全脸高度 22% | +| upper_court_cm | number | 上庭长度(cm),约占 25% | +| middle_court_cm | number | 中庭长度(cm),约占 28% | +| lower_court_cm | number | 下庭长度(cm),约占 25% | +| ratios | object | 各庭占全脸高度的百分比 | + +`seven_eyes`(七眼,横向): + +| 字段 | 类型 | 说明 | +|------|------|------| +| eye_width_cm | number | 单眼宽度(cm) | +| face_width_cm | number | 脸宽(cm) | +| inter_eye_distance_cm | number | 两眼间距(cm) | +| ratios | object | 七眼各段占脸宽的比例 | + +### 标注图片(UI)规范 + +| 项目 | 要求 | +|------|------| +| 字体及线颜色 | `#FFFFFF` 100% | +| 数值排布 | 四庭数值统一在图片**左侧**呈现;七眼间距**上下穿插**展示 | +| 字体 | PingFangSC-Regular,字号 10pt | +| 线宽 | 横线、竖线、虚线均为 1pt | +| 线样式 | 横线、竖线渐变消失;虚线两侧呈现箭头 | + +> **【待确认】** 标注图片需提供设计稿后才能最终确定样式。 + +### 响应示例(当前 Mock 返回值) + +```json +{ + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "annotated_image_url": "https://hair.xiangsilian.com/static/sample.jpg", + "face_total_height_cm": 13.76, + "four_courts": { + "top_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 } + }, + "seven_eyes": { + "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 } + }, + "landmarks": { + "hair_top": { "x": 540, "y": 120 }, + "hairline": { "x": 540, "y": 430 }, + "brow_center": { "x": 540, "y": 740 }, + "nose_bottom": { "x": 540, "y": 1050 }, + "chin_tip": { "x": 540, "y": 1360 } + } + } +} +``` + +--- + +## 接口 2:C 端生发接口 + +**说明**:输入用户正面照,输出生发后的图片,以及推荐的发际线(可能多张),并按合适度排序。 + +**请求**:`POST /api/v1/hair/grow` + +### 输入 + +图片参数见「通用约定 → 图片传参字段」。专属参数: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| beauty_enabled | bool | 否 | 生发图是否带美颜效果,默认 false。提供该开关 | + +### 输出(data) + +`results`:发际线方案数组(可能多张),每个元素: + +| 字段 | 类型 | 说明 | +|------|------|------| +| image_url | string | 生发后图片 URL | +| hairline_type | string | 图片对应的发际线形(如:花瓣形、波浪形) | +| order | int | 排序序号(1 = 最优,2 次之 …) | + +### 响应示例(当前 Mock 返回值) + +```json +{ + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "results": [ + { "image_url": "https://hair.xiangsilian.com/static/sample.jpg", "hairline_type": "花瓣形", "order": 1 }, + { "image_url": "https://hair.xiangsilian.com/static/sample.jpg", "hairline_type": "波浪形", "order": 2 } + ] + } +} +``` + +> 识别失败时返回通用错误码(1001 / 1002 / 1003 等)。 + +--- + +## 接口 3:B 端生发接口 + +**说明**:输入医生/操作端的「划线图片」(在原图上标注了目标发际线),输出最合适的发际线图片 + 生发后图片。 + +**请求**:`POST /api/v1/hair/grow-b` + +### 输入 + +划线图片同样支持「文件 / URL / base64」三种方式(字段:`marked_image_file` / `marked_image_url` / `marked_image_base64`)。 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| marked_image_* | file / string | 是 | 已划线(标注发际线)的图片,三选一 | +| original_image_* | file / string | 是 | 原始用户照片,需同时上传 | + +### 输出(data) + +| 字段 | 类型 | 说明 | +|------|------|------| +| best_hairline_image_url | string | 最合适的发际线图片 | +| hair_growth_image_url | string | 生发后图片 | +| hairline_type | string | 发际线形,需返回 | + +### 响应示例(当前 Mock 返回值) + +```json +{ + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "best_hairline_image_url": "https://hair.xiangsilian.com/static/sample.jpg", + "hair_growth_image_url": "https://hair.xiangsilian.com/static/sample.jpg", + "hairline_type": "花瓣形" + } +} +``` + +--- + +## 接口 4:用户特征接口 + +**说明**:输入用户照片,输出 N 个用户特征字段。 + +**请求**:`POST /api/v1/face/features` + +### 输入 + +图片参数见「通用约定 → 图片传参字段」。本接口无其他专属参数。 + +### 输出(data) + +`data` 直接返回一个 **JSON 字符串**(`features`),其内部字段不固定、可随时调整,由业务方约定。当前优先返回的字段如下(仅作示例,最终以实际返回为准): + +| 字段 | 类型 | 说明 | +|------|------|------| +| face_shape | string | 脸形 | +| eyebrow_shape | string | 眉形 | +| facial_age | int | 面部年龄 | +| dynamic_static_type | string | 动静类型 | +| gender | string | 性别 | +| gene_style | object | 面部特征对应面部标签的「基因风格」 | + +### 响应示例(当前 Mock 返回值) + +```json +{ + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "features": "{\"face_shape\": \"鹅蛋脸\", \"eyebrow_shape\": \"柳叶眉\", \"facial_age\": 26, \"dynamic_static_type\": \"静态\", \"gender\": \"女\", \"gene_style\": {\"label\": \"面部特征标签\", \"style\": \"基因风格示例\"}}" + } +} +``` + +> `features` 为字符串形式的 JSON,字段后续可随时增删,不固定。 + +--- + +## 接口 5:发际线 PNG 生成接口 + +**说明**:输入用户照片,返回 N 张用户发际线的 PNG 图片,并返回「最合适发际线」的面部中间点坐标。 + +**请求**:`POST /api/v1/hairline/generate` + +### 输入 + +图片参数见「通用约定 → 图片传参字段」。本接口无其他专属参数。 + +### 输出(data) + +| 字段 | 类型 | 说明 | +|------|------|------| +| hairline_images | object[] | N 张用户发际线 PNG,**数量 N 不固定**,已按合适度排序,元素见下表 | +| best_hairline_center_point | object | 最合适发际线的「面部中间点」坐标,原图像素:`{ "x": number, "y": number }` | + +`hairline_images` 元素: + +| 字段 | 类型 | 说明 | +|------|------|------| +| image_url | string | 发际线 PNG 图片 URL | +| order | int | 排序序号(1 = 最合适,依次递增) | + +### 响应示例(当前 Mock 返回值) + +```json +{ + "code": 0, + "message": "success", + "request_id": "mock-request-id", + "data": { + "hairline_images": [ + { "image_url": "https://hair.xiangsilian.com/static/sample.jpg", "order": 1 }, + { "image_url": "https://hair.xiangsilian.com/static/sample.jpg", "order": 2 } + ], + "best_hairline_center_point": { "x": 540, "y": 430 } + } +} +``` + +--- + +## 汇总:输入输出一览 + +| 接口 | 输入 | 主要输出 | +|------|------|----------| +| 1 四庭七眼测量 | 用户照片 | 标注 PNG(无人物)+ 四庭/七眼厘米数值与坐标 | +| 2 C 端生发 | 用户照片 | 生发后图片 + 多张发际线(带类型与排序) | +| 3 B 端生发 | 划线图片 | 最合适发际线图片 + 生发后图片 | +| 4 用户特征 | 用户照片 | N 个用户特征字段(脸形/眉形/年龄/动静/性别/基因风格…) | +| 5 发际线 PNG | 用户照片 | N 张发际线 PNG + 最合适发际线面部中间点坐标 | + +--- + +## 已确认结论 + +- 图片传参:**文件 / URL / base64 三种都支持,严格互斥**(必须且只能传一个,base64 需带 `data:image/...;base64,` 前缀)。 +- 默认单人;检测到 2~3 人报错提示客户端;单文件 **≤ 1 MB**;格式仅 **JPG / PNG**。 +- 所有坐标基准:**原图像素**。 +- 接口 2:提供「美颜」开关 `beauty_enabled`。 +- 接口 3:需同时上传原图,需返回发际线类型。 +- 接口 4:`data` 返回**一个 JSON 字符串**,字段不固定,可随时调整。 +- 接口 5:发际线 PNG 数量 **N 不固定**,需按合适度**排序**。 +- 英文字段命名由本文档约定(见各接口表格)。 + +## 待与需求方确认的问题清单 + +1. 接口 1 标注图片设计稿(见下方说明)。 diff --git a/docs/旷视具体需求.md b/docs/旷视具体需求.md new file mode 100644 index 0000000..509cc73 --- /dev/null +++ b/docs/旷视具体需求.md @@ -0,0 +1,156 @@ +# 旷视具体需求 + +## 五个接口 + +--- + +## 1. 四庭七眼测量接口 + +**输入**:照片 + +**输出**:标注好数据的 PNG 图片(**不要人物**),以及四庭七眼坐标或图片,要显示厘米数(需要设计稿)。 + +### 接口返回数据要求 + +#### 四庭比例数值 + +- `<顶庭:XXcm>`,约占 `<全脸高度的 22%>` +- `<上庭:长约XXcm>`,约占 `<全脸高度的 25%>` +- `<中庭:长约XXcm>`,约占 `<全脸高度的 28%>` +- `<下庭:长约XXcm>`,约占 `<全脸高度的 25%>` +- `<全脸长约xxcm>` + +#### 七眼比例数据 + +- `<眼宽度约xxcm>` +- `<脸宽xxcm>` +- `<两眼间距约xxcm>` + +### UI 图片要求 + +| 项目 | 要求 | +|------|------| +| 字体及线颜色色值 | `#FFFFFF` 100% | +| 数值排布 | 四庭数值统一在图片**左侧**呈现;七眼间距**上下穿插**展示 | +| 字体 | PingFangSC-Regular,字号 10pt | +| 线宽 | 横线、竖线、虚线均为 1pt | +| 线样式 | 横线、竖线渐变消失;虚线两侧呈现箭头 | + +### 参考图:分析图片规范 · 版本 2 + +![分析图片规范版本2](旷视具体需求_media/image4.jpeg) + +图中标注说明: + +| 标注 | 含义 | +|------|------| +| 头顶 | 头顶位置 | +| 发际线 | 发际线位置 | +| 眉心 | 眉心位置 | +| 鼻翼下缘 | 鼻翼下缘位置 | +| 下巴尖 | 下巴尖位置 | +| 顶庭 / 上庭 / 中庭 / 下庭 | 四庭分段(示例各约 3.44cm) | + +### 设计稿参考(魔镜 2) + +![魔镜2设计稿](旷视具体需求_media/image6.png) + +> 虚线:两侧箭头呈现。 + +--- + +## 2. C 端生发接口 + +**输入**:图片(用户拍摄的正面照) + +**输出**:生发后图片 + 发际线图片(可能多张) + +### 图片要求 + +- 上传是否有大小要求?图片最大、最小的要求是多少? +- **最低分辨率**:1080 × 1920 +- **最大分辨率**:4000 × 5000 + +### 识别与错误 + +返回图片是否可以识别?如果图片识别失败,会返回具体错误原因吗? + +- 【无法识别人像】【人像分辨率过低】 +- 【角度问题,非正面照】 +- 【男女】标签判断 + +### 其他 + +- 生发图是否可以带美颜效果? + +### 需要返回的图片内容 + +示例 1(排序 1): + +- `<生成后图片url>` +- `<图片对应的发际线形>`【花瓣形】 +- `<排序>`【1】 + +示例 2(排序 2): + +- `<生成后图片url>` +- `<图片对应的发际线形>`【波浪形】 +- `<排序>`【2】 + +--- + +## 3. B 端生发接口 + +**输入**:划线图片(医生/操作端在原图上标注目标发际线) + +**输出**:最合适发际线图片 + 生发后图片 + +--- + +## 4. 用户特征接口 + +**输入**:图片 + +**输出**:N 个用户特征字段 + +### 优先返回 + +| 字段 | 说明 | +|------|------| +| `<脸形>` | 脸形 | +| `<眉形>` | 眉形 | +| `<面部年龄>` | 面部年龄 | +| `<动静类型>` | 动静类型 | +| `<性别>` | 性别 | +| `<面部特征>` 对应面部标签的 `<基因风格>` | 基因风格 | + +**剩余标签 & 数据**:后续返回即可。 + +![用户特征参考](旷视具体需求_media/image9.png) + +--- + +## 5. 发际线 PNG 生成接口 + +**输入**:用户照片 + +**输出**: + +- N 张用户发际线的 PNG 图片 +- 最合适发际线面部中间点的坐标 + +--- + +## 文档附图 + +以下为原 Word 文档中的其他嵌入图片(水印/版式参考): + +| 图片 | 说明 | +|------|------| +| ![image2](旷视具体需求_media/image2.png) | 文档附图 | +| ![image3](旷视具体需求_media/image3.png) | 文档附图 | +| ![image5](旷视具体需求_media/image5.png) | 文档附图 | + +--- + +> 本文档由 `旷视具体需求.docx` 自动转换生成。图片保存在 `旷视具体需求_media/` 目录。 diff --git a/docs/旷视具体需求_media/image1.png b/docs/旷视具体需求_media/image1.png new file mode 100644 index 0000000..f90fb8d Binary files /dev/null and b/docs/旷视具体需求_media/image1.png differ diff --git a/docs/旷视具体需求_media/image2.png b/docs/旷视具体需求_media/image2.png new file mode 100644 index 0000000..3d4160b Binary files /dev/null and b/docs/旷视具体需求_media/image2.png differ diff --git a/docs/旷视具体需求_media/image3.png b/docs/旷视具体需求_media/image3.png new file mode 100644 index 0000000..10a60dd Binary files /dev/null and b/docs/旷视具体需求_media/image3.png differ diff --git a/docs/旷视具体需求_media/image4.jpeg b/docs/旷视具体需求_media/image4.jpeg new file mode 100644 index 0000000..94b68d4 Binary files /dev/null and b/docs/旷视具体需求_media/image4.jpeg differ diff --git a/docs/旷视具体需求_media/image5.png b/docs/旷视具体需求_media/image5.png new file mode 100644 index 0000000..9b93447 Binary files /dev/null and b/docs/旷视具体需求_media/image5.png differ diff --git a/docs/旷视具体需求_media/image6.png b/docs/旷视具体需求_media/image6.png new file mode 100644 index 0000000..c45dd13 Binary files /dev/null and b/docs/旷视具体需求_media/image6.png differ diff --git a/docs/旷视具体需求_media/image7.png b/docs/旷视具体需求_media/image7.png new file mode 100644 index 0000000..48a0b16 Binary files /dev/null and b/docs/旷视具体需求_media/image7.png differ diff --git a/docs/旷视具体需求_media/image8.png b/docs/旷视具体需求_media/image8.png new file mode 100644 index 0000000..6a2bd3b Binary files /dev/null and b/docs/旷视具体需求_media/image8.png differ diff --git a/docs/旷视具体需求_media/image9.png b/docs/旷视具体需求_media/image9.png new file mode 100644 index 0000000..af77345 Binary files /dev/null and b/docs/旷视具体需求_media/image9.png differ diff --git a/requirements.txt b/requirements.txt index 336a1d9..fdd52f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ fastapi==0.115.12 uvicorn[standard]==0.34.2 +python-multipart==0.0.31 diff --git a/static/sample.jpg b/static/sample.jpg new file mode 100644 index 0000000..94b68d4 Binary files /dev/null and b/static/sample.jpg differ