智慧衣橱虚拟试穿实践:从占位 UI到 Sophnet 双图图生图的落地思考
一、写在前面:
做智慧衣橱时,产品页面上最早写的是AI 试穿 / 图生图 · 接入中。很长一段时间里,我把试穿和小衣对话助手混在一起想——以为在聊天框里发一张衣服图,大模型说几句搭配建议,就算AI 试衣了。
后来明白:虚拟试穿本质上是图像问题,不是文本问题。
- 对话大模型:输入文字 → 输出文字(或附带描述)
- 虚拟试穿:输入 人物图 + 服装图 → 输出 一张新的合成图
这在技术分类上属于 图生图(Image-to-Image) 或 条件图像编辑:以人物照为底,用服装图和 Prompt 作为条件,让扩散模型生成穿上这件衣服的效果图。
专用 VTON(Virtual Try-On)扩散模型(如 IDM-VTON)效果最好,但需要 GPU 或海外 API。我们团队当时 只有 Sophnet 平台的 API Key,没有阿里云 DashScope 原厂 Key,也没有 Replicate 账号。
于是做了一个务实选择:用 Sophnet 上的 `Qwen-Image-Edit-2509` 做双图 + Prompt的近似试穿——不是严格意义上的 VTON,但在工程上能跑通、能演示、能写进论文,而且和现有 Key 兼容。
二、近似试穿
2.1 三种路线,我们选了中间那条
| 路线 | 做法 | 难度 | 效果 |
|---|---|---|---|
A. 专用 VTON | IDM-VTON 等双图模型 | 高(GPU/海外 API) | 最好 |
B. 图像编辑 / 图生图 | 双图 +「穿上这件衣服」Prompt | 中 | 够用,不稳定 |
C. 纯文生图 | 只描述衣服,不喂服装图 | 低 | 衣服对不上 |
我们走的是 B:在 Sophnet 文档里,`Qwen-Image-Edit-2509` 支持 1~3 张参考图 + 编辑指令,任务类型标注为 I2I(图生图),这和让人物穿上某件衣服的需求是对齐的。
2.2 和专用 VTON差在哪里(心里要有数)
图生图近似试穿 不保证:
- 服装纹理 1:1 还原(Logo、条纹可能糊)
- 人脸 100% 不变(偶发换脸感)
- 复杂姿态下的遮挡关系
但它 能保证:
- 端到端链路:App 选图 → 后端 → 云 API → 结果 URL → 展示
- 和衣橱数据打通:服装图可直接来自已录入单品
-基于图生图条件编辑的近似虚拟试穿,后续可升级为专用 VTON
三、整体架构:Key 绝不进 App
个人理解里,移动端 AI 接入的第一原则是:API Key 只放服务端。
```
Android VirtualTryOnScreen
│ Multipart: person_image + garment_image
│ Header: Authorization: Bearer <登录 JWT>
▼
FastAPI /api/v1/ai/tryon
│ 鉴权 → 存图 → tryon_service.run_tryon()
▼
Sophnet Qwen-Image-Edit-2509
│ 异步任务:创建 → 轮询 → 取 results[0].url
▼
backend/uploads/tryon/ → App 用 Coil 加载 result_url
```
令试穿单独一个页面:
1. 图生图慢(常 30~120 秒),不适合在聊天气泡里干等
2. 两张图 + 轮询,交互更像工具而不是对话
3. 以后换 VTON 模型,只改 `tryon_service.py`,App 协议不变
四、后端实现
4.1 接口设计:简单、可演示、可扩展
FastAPI 路由(节选):
```python
@app.post("/api/v1/ai/tryon", response_model=TryOnResponse)
async def ai_tryon(
user_id: Annotated[int, Depends(current_user_id)],
person_image: UploadFile = File(...),
garment_image: UploadFile = File(...),
) -> TryOnResponse:
# 校验 image/ → 落临时文件 → run_tryon()
...
return TryOnResponse(payload)
```
响应里我刻意加了 `mode` 字段:
- `provider`:Sophnet 图生图成功
- `demo`:未配置 Key,或调用失败时回显人物图
这是个人很看重的一点:没有 Key 的日子也要能答辩演示 UI。Demo 不是造假,而是工程上的 降级策略(Graceful Degradation)。
4.2 核心:双图顺序 + Prompt 设计
Sophnet 文档写得很清楚:`input.images` 支持 1~3 张图,多图时按数组顺序,且 输出宽高比以最后一张为准。
因此我们的顺序是:
```python
"images": [garment_data_uri, person_data_uri]
# 图1 = 服装,图2 = 人物 → 输出比例跟人物走
```
Prompt 是反复试出来的(可放 `.env` 覆盖):
```python
DEFAULT_TRYON_PROMPT = (
"参考图1中的服装单品,让图2中的人物自然穿上该服装。"
"保持图2的人脸、发型、体态、背景不变,服装贴合身体,写实摄影风格,高清。"
)
DEFAULT_NEGATIVE_PROMPT = (
"模糊,低质量,变形的脸,多余的手指,文字水印,卡通,插画风格,"
"服装错位,头部替换,背景突变"
)
```
思考: Prompt 写图1 / 图2是和数组顺序绑定的;若以后改成人物在前、服装在后,Prompt 必须一起改,否则模型会懵。
4.3 异步任务:创建 + 轮询
Sophnet 图片接口是 异步 的:POST 返回 `taskId`,再 GET 查状态直到 `SUCCEEDED`。
创建任务(节选):
```python
payload = {
"model": "Qwen-Image-Edit-2509",
"input": {
"prompt": prompt,
"negative_prompt": negative,
"images": [garment_data_uri, person_data_uri],
},
"parameters": {
"size": "12801280",
"watermark": False,
"prompt_extend": True,
"save_to_jpeg": True,
},
}
r = client.post(SOPHNET_IMAGE_TASK_URL, headers={...}, json=payload)
task_id = output["taskId"]
```
轮询逻辑(个人实现要点):
```python
while time.time() < deadline:
r = client.get(f".../task/{task_id}", headers={...})
status = output.get("taskStatus", "").upper()
if status == "SUCCEEDED":
return output["results"][0]["url"]
if status in ("FAILED", "CANCELED"):
raise RuntimeError(...)
time.sleep(2) # 默认 2 秒一轮,最多 180 秒
```
响应体有时包在 `output` 里,有时在 `result` 里,我写了 `_unwrap_output()` 做兼容,避免联调时明明成功却解析失败。
图生图失败时不向 App 抛 500,而是 `print` 日志 + 降级 demo。用户至少能看到人物回显和错误提示,而不是整页崩溃。
4.4 环境变量(`.env`)
```env
TRYON_PROVIDER=sophnet
SOPHNET_API_KEY=你的Key
TRYON_MODEL=Qwen-Image-Edit-2509
TRYON_API_BASE_URL=https://www.sophnet.com/api/open-apis/projects/easyllms/imagegenerator/task
TRYON_POLL_MAX_SEC=180
```
Sophnet 文档:[视觉模型](https://www.sophnet.com/docs/component/vision_model.html)
五、Android 端实现与思考
5.1 页面:VirtualTryOnScreen
交互我设计成三步,尽量像工具不像聊天:
1. 选人物照(相册)
2. 选服装图(相册 或 衣橱横滑选单品)
3. 点生成试穿效果→ 展示 `result_url`
实现的衣橱联动:服装图不必重新拍,直接用 DataStore 里已有 `imageUri`,形成我的衣橱 → 试穿预览闭环。
5.2 上传:Retrofit Multipart
```kotlin
@Multipart
@POST("api/v1/ai/tryon")
suspend fun tryOn(
@Part personImage: MultipartBody.Part,
@Part garmentImage: MultipartBody.Part,
): TryOnResponseDto
```
`AiRepository` 里从 `content://` 读字节,和上传头像同一套路:
```kotlin
suspend fun virtualTryOn(personUri: Uri, garmentUri: Uri): TryOnResponseDto {
val personPart = uriToPart(personUri, "person_image", "person.jpg")
val garmentPart = uriToPart(garmentUri, "garment_image", "garment.jpg")
return api.tryOn(personPart, garmentPart)
}
```
5.3 超时:图生图 ≠ 登录接口
普通接口 readTimeout 20s;图生图单独 `aiApi()` 拉到 120s。这是实际联调时得到的经验——异步轮询在后端做,但整次 HTTP 仍可能接近一分钟。
5.4 登录与游客
试穿接口复用 JWT。游客点击生成会引导登录——既是安全(防刷 API),也是产品逻辑(试穿算深度功能)。
六、联调步骤
1. `cd backend && pip install httpx && uvicorn main:app --host 0.0.0.0 --port 8080`
2. `.env` 填 `SOPHNET_API_KEY`
3. Android `local.properties`:`WARDROBE_API_BASE_URL=http://10.0.2.2:8080/`(模拟器)
4. App 登录 → 搭配 → AI 实验室 → AI 试穿 / 虚拟上身
5. 人物照 + 服装图 → 生成
6. 成功:`mode=provider`;未配 Key:`mode=demo` 回显人物图
七、效果与局限:
效果:
- 认清试穿 = 图生图,没有硬塞进 LLM 聊天
- 服务端代理 Key,JWT 鉴权
- 演示降级 + 真实模式用 `mode` 区分
- Sophnet 双图 I2I 与现有 Key 兼容,零 GPU
局限:
- 效果随 Prompt、照片质量波动大
- 专用 VTON 在衣服还原度上仍明显更好
- 大图 Base64 上传,尚未做压缩/去背景预处理
- 轮询在后端同步阻塞,高并发时要改 task 队列
若继续迭代,我会按这个顺序:
1. 上传前缩放到短边 768、服装图去背景
2. 后端改异步:POST 返回 task_id,App 轮询
3. 有余力再换 Replicate IDM-VTON 或万相试衣专用接口
八、总结
本项目的虚拟试穿模块,是在智慧衣橱场景下对图生图(Image-to-Image)技术的一次工程实践:客户端采集人物照与服装单品图,经 FastAPI 服务端调用 Sophnet 平台的 Qwen-Image-Edit-2509 图像编辑模型,通过双图输入与自然语言编辑指令生成近似上身效果。该方案属于 条件图像编辑式近似试穿,与专用 VTON 扩散模型相比实现成本更低,便于在课程项目中完成端到端验证;API 密钥部署于服务端,移动端仅传输业务图片与 JWT,兼顾安全与可扩展性。