Files
EPEEAIKit/docs/art-agent/VIEW-TRANSFORM-MESH-PIPELINE-PLAN.md
2026-04-20 21:52:35 +08:00

23 KiB
Raw Blame History

Mesh-Pipeline 视角变换 · 跨会话实施计划

状态SPEC / 尚未开工 最后更新2026-04-20 15:30 负责人Agent + 用户 @shine 适用对象:任意新会话的 Agent本文档自包含无需额外上下文即可开工


0 · 上下文Why

0.1 起因

当前项目 art-agent 已经集成了 Zero123++jd7h/zero123plusplus)做单图→多视角变换,但存在固有缺陷:

  1. 只输出 6 个固定视角azimuth/elevation 写死在 config.py,用户无法指定任意相机位姿
  2. 大角度一致性差Zero123++ 主要训练于小物体,对建筑类规整几何 + 大角度偏转常出现立面错位
  3. 无几何中间产物,纯图对图黑盒映射,无法二次利用

0.2 目标

新增一条"Mesh Pipeline"视角变换路径,与现有 zero123plus 并列保留,让用户可以:

  • 输入 1 张 2D 图(典型场景:带描边卡通风的建筑,如教堂/房屋/塔楼)
  • 输出 N 张保留原画风几何一致的新视角 2D 图
  • 可指定任意 azimuth/elevation(至少阶段性支持 Trellis 360° 中的任意角度)

非目标:输出 3D mesh 给用户下载mesh 只是中间产物);替代 Zero123++(它作为"轻量路径"继续保留)。

0.3 相关历史上下文

记录 说明
CL-20260416-1000 Zero123++ 首次集成
CL-20260420-1212 Zero123++ 切图布局修复cols=2/rows=3
CL-20260420-1501 后端 watchfiles 依赖修复uvicorn --reload 生效
PF-20260420-1212 Grid 切图布局坑("3×2" 措辞歧义)
PF-20260420-1501 Windows uvicorn --reload 无 watchfiles 退化坑

1 · 核心决策

1.1 三段式流水线

[输入: 1 张 2D 图]
      ↓
【阶段 1】单图 3D 重建 → 生成 textured .glb + 预渲染 360° 视频
      ↓
【阶段 2】从视频抽帧 / 自定义相机渲染 → 多视角"几何参考图"
      ↓
【阶段 3】SDXL + ControlNet + IP-Adapter 二次重绘 → 还原原画风
      ↓
[输出: N 张保留风格、几何一致的 2D 新视角图]

1.2 模型选型

阶段 主选 备选 / 高级选项 理由
阶段 1 Trellis (firtoz/trellis) Hunyuan3D-2 (tencent/hunyuan3d-2) Trellis $0.041/30s 且自带 color_videoHunyuan 贵 3×慢 4×但 mesh 更扎实
阶段 2 ffmpeg 从 Trellis color_video 抽帧 pyrender 加载 .glb 任意角度渲染 Trellis 已送 360° 环绕视频,首版完全省事
阶段 3 chigozienri/ip_adapter-sdxl-controlnet-depth 自建 SDXL + 多 ControlNet 组合 一个 Replicate 模型打包了 SDXL+ControlNet-Depth+IP-Adapter

1.3 为什么不用 Hunyuan3D-2 作默认

  • 贵 3 倍($0.12 vs $0.041
  • 慢 4 倍127s vs 30s
  • 不返回预渲染视频 → 必须额外实现阶段 2pyrender / blender 渲染器)
  • 唯一优势mesh 几何质量更高

→ 留作"高质量模式"供用户显式选择,一期不默认启用。

1.4 为什么不用 ControlNet-Depth 必须先估深度

chigozienri/ip_adapter-sdxl-controlnet-depthcontrolnet_input 要求已经是 depth map。两条路

  • 路 A:从 Trellis normal_video 抽帧,把 normal 当"结构约束"直接喂(近似方案,不完美但可用)
  • 路 B:从 color_video 抽帧后,额外调用 chenxwh/depth-anything-v2 生成 depth map

一期走路 A 简化;若效果不足再升级路 B多一次 API 调用 + $0.003/张)。


2 · 技术数据清单Schema

2.1 Trellis — firtoz/trellis

  • 版本 IDe8f6c45206993f297372f5436b90350817bd9b4a0d52d2a76df50c1c8afa2b3c
  • 成本$0.041/次(~24 次/$1
  • 耗时~30 秒
  • 硬件Nvidia A100 80GB
  • 输入关键字段
字段 类型 默认 说明
images array[uri] 必填 输入图(支持多图,一期只传 1 张)
seed int 0 种子
randomize_seed bool true 一期设为 false 便于复现
texture_size int (512-2048) 1024 GLB 纹理分辨率
mesh_simplify float (0.9-0.98) 0.95 网格简化,建议保持默认
generate_color bool true 必开,用于得到 color_video
generate_normal bool true 一期开启,便于阶段 3 做结构约束
generate_model bool true 必开,否则不输出 .glb
save_gaussian_ply bool false 不需要
ss_sampling_steps int (1-50) 12 默认即可
slat_sampling_steps int (1-50) 12 默认即可
ss_guidance_strength float (0-10) 7.5 默认即可
slat_guidance_strength float (0-10) 3 默认即可
  • 输出字段⚠️ 实测修订 2026-04-20 16:15版本 e8f6c452...
字段 类型 实测行为
model_file uri .glb 文件(generate_model=true 时)
color_video uri 恒为 null — 该部署版不单独输出
normal_video uri 恒为 null — 该部署版不单独输出
combined_video uri 唯一可用的视频 — color+normal 左右并排 mp4
gaussian_ply uri 高斯点云(本项目不用)
no_background_images array[uri] 去背景的预处理图

实测 schema 踩坑(已记为 PF-20260420-1600

  1. 输入字段必须用 images(数组)—— 传 image(单数字符串)会 422 input: images is required,即使 Replicate README example 里写的是 image
  2. 尽管 generate_color=Truegenerate_normal=True 都开,color_videonormal_video 字段也恒为 null,只有 combined_video 可用。
  3. 因此 Phase 2 抽帧的数据源是 combined_video,抽帧后需要切左半 = color 帧、切右半 = normal 帧

2.2 Hunyuan3D-2 — tencent/hunyuan3d-2(备选)

  • 版本 IDb1b9449a1277e10402781c5d41eb30c0a0683504fb23fab591ca9dfc2aabe1cb
  • 成本$0.12/次
  • 耗时~127 秒
  • 硬件Nvidia L40S
  • 输入image, steps (20-50, default 50), guidance_scale (1-20, default 5.5), seed, octree_resolution (default 256), remove_background (default true)
  • 输出mesh (uri, .glb) —— 无视频无多视角预览,必须配套阶段 2 自定义渲染

2.3 ControlNet + IP-Adapter — chigozienri/ip_adapter-sdxl-controlnet-depth

  • 版本 ID0436c8702ef52616be5c30948551b3af6a86c821cca9b01f11ac297624fff14c
  • 成本$0.070/次
  • 耗时~72 秒
  • 硬件Nvidia L40S
  • 输入关键字段
字段 类型 默认 说明
image uri 必填 IP-Adapter 参考图(此处传用户原图保持风格)
controlnet_input uri 必填 Depth / Normal / Lineart 参考图(此处传阶段 2 抽出来的 normal 帧
scale float (0-1) 0.6 IP-Adapter 权重,建议调到 0.7-0.8 强化风格
controlnet_conditioning_scale float (0-1) 0.6 结构约束权重,建议 0.7-0.9
prompt str "" 留空或用 "same building, same art style"
negative_prompt str "" "blurry, distorted, different style"
num_outputs int (1-4) 1 一期 1
num_inference_steps int (1-500) 30 默认即可
seed int 随机 调参阶段固定

2.4 依赖清单

一期必装(首版走 Trellis + ffmpeg 抽帧,最小化依赖):

# backend requirements.txt
ffmpeg-python==0.2.0   # 或者直接在后端宿主机装 ffmpeg 可执行文件

二期可选(若要上 Hunyuan3D-2 + 自定义相机):

trimesh==4.5.0
pyrender==0.1.45
PyOpenGL==3.1.7
# Windows 额外需要 OSMesa 或改用 Blender headless二选一

系统依赖

  • ffmpeg 可执行文件Windowschoco install ffmpeg 或手动放 PATH
  • 若二期启用Windows 上 OSMesa 编译困难,推荐用 Blender 4.x headless 替代

3 · 数据流与接口设计

3.1 新增后端服务模块

art-agent/backend/app/services/
├── view_transform.py           # [改造] 管道调度器
├── mesh_generator.py           # [新增] 阶段 1调用 Trellis / Hunyuan3D-2
├── video_frame_extractor.py    # [新增] 阶段 2轻量ffmpeg 抽帧
├── mesh_renderer.py            # [新增, 二期] 阶段 2自由相机pyrender 加载 .glb 渲染任意角度
└── style_restorer.py           # [新增] 阶段 3ControlNet + IP-Adapter 重绘

3.2 管道调度伪代码

# services/view_transform.py
async def transform_view_mesh_pipeline(
    image_path: str,
    model_id: str = "trellis",        # or "hunyuan3d"
    azimuths: list[int] = [0, 60, 120, 180, 240, 300],
    elevations: list[int] = [0, 0, 0, 0, 0, 0],
    preserve_style: bool = True,      # 是否走阶段 3
) -> dict:
    # === 阶段 1 ===
    mesh_result = await mesh_generator.generate(image_path, model_id)
    # 返回 {glb_url, color_video_url?, normal_video_url?}

    # === 阶段 2 ===
    if model_id == "trellis":
        # 从视频抽帧Trellis 的 360° 视频约 5s对应 az 0→360°
        frames = await video_frame_extractor.extract_by_azimuth(
            color_video_url=mesh_result["color_video_url"],
            normal_video_url=mesh_result["normal_video_url"],
            azimuths=azimuths,
            total_rotation_deg=360,
        )
    else:  # hunyuan3d
        # 二期:自定义相机渲染
        frames = await mesh_renderer.render_views(
            glb_path=mesh_result["glb_path"],
            azimuths=azimuths,
            elevations=elevations,
        )
    # frames = [{az, el, color_png, normal_png}, ...]

    # === 阶段 3 ===
    if preserve_style:
        final_images = await asyncio.gather(*[
            style_restorer.restore(
                original_image=image_path,
                structure_image=f["normal_png"],
                azimuth=f["az"],
                elevation=f["el"],
            )
            for f in frames
        ])
    else:
        final_images = [f["color_png"] for f in frames]

    return {
        "success": True,
        "images": [
            {"url": img, "azimuth": az, "elevation": el}
            for img, (az, el) in zip(final_images, zip(azimuths, elevations))
        ],
        "mesh_url": mesh_result.get("glb_url"),
        "model_name": f"Mesh Pipeline ({model_id})",
    }

3.3 config.py 扩展

VIEW_TRANSFORM_MODELS = {
    "zero123plus": {...},  # 保留不动
    "trellis": {
        "id": "trellis",
        "name": "Trellis (Mesh Pipeline)",
        "provider": "replicate",
        "model_id": "firtoz/trellis:e8f6c45206993f297372f5436b90350817bd9b4a0d52d2a76df50c1c8afa2b3c",
        "description": "3D 重建 + 任意视角 + 风格还原,建筑友好(~30s + 风格化 ~72s/张)",
        "pipeline": "mesh",       # 新增字段区分管道类型
        "default_params": {
            "texture_size": 1024,
            "mesh_simplify": 0.95,
            "generate_color": True,
            "generate_normal": True,
            "generate_model": True,
        },
    },
    "hunyuan3d": {
        "id": "hunyuan3d",
        "name": "Hunyuan3D-2 (高质量)",
        "provider": "replicate",
        "model_id": "tencent/hunyuan3d-2:b1b9449a1277e10402781c5d41eb30c0a0683504fb23fab591ca9dfc2aabe1cb",
        "description": "几何质量最佳(~127s需自定义渲染二期启用",
        "pipeline": "mesh",
        "enabled": False,  # 一期先禁用
    },
}

IMAGE_MODELS["ip-adapter-controlnet-depth"] = {
    "id": "ip-adapter-controlnet-depth",
    "name": "IP-Adapter + ControlNet Depth (内部)",
    "provider": "replicate",
    "model_id": "chigozienri/ip_adapter-sdxl-controlnet-depth:0436c8702ef52616be5c30948551b3af6a86c821cca9b01f11ac297624fff14c",
    "internal": True,  # 不在前端展示,仅用于阶段 3
    ...
}

3.4 工具层 tools.py

transform_view 工具参数扩展(向后兼容):

{
    "name": "transform_view",
    "parameters": {
        "image_path": {...},
        "model_id": {
            "enum": ["zero123plus", "trellis", "hunyuan3d"],
            "default": "zero123plus",
        },
        # 以下为 mesh pipeline 专属参数
        "azimuths": {"type": "array", "default": [0, 60, 120, 180, 240, 300]},
        "elevations": {"type": "array", "default": [0, 0, 0, 0, 0, 0]},
        "preserve_style": {"type": "boolean", "default": True},
    },
}

3.5 前端改造

  • 模型下拉增加 "Trellis (Mesh Pipeline)" 和 "Hunyuan3D-2 (高质量)" 选项
  • 选中 mesh pipeline 时,额外显示:
    • "保留原画风格" 开关(默认开)
    • 角度输入:简化为"快速预设6/8/12 视角)" + "自定义 azimuth 数组"(高级)
  • 结果区域:除了 N 张图,还展示 color_video 可预览

4 · 施工任务清单(跨会话可独立执行)

Phase 1Trellis 接入(预计 1.5h

验收标准:用 curl / Replicate SDK 能跑通一次 Trellis拿到 .glb + color_video URL下载到本地。

  • P1-1 services/mesh_generator.py 新建,封装 generate_with_trellis(image_path) 函数(调用 Replicate + 下载结果到 GENERATED_DIR
  • P1-2 config.py 新增 VIEW_TRANSFORM_MODELS["trellis"] 条目
  • P1-3 services/view_transform.py 根据 pipeline == "mesh" 分流到 mesh_generator
  • P1-4 tools.pytransform_view 增加 model_id 参数默认值兼容
  • P1-5 本地手动测试:scripts/test_trellis.py 可独立运行验证 Trellis → .glb + color_video + normal_video 本地落地(等用户执行

Phase 2ffmpeg 抽帧(预计 1h

验收标准:从 color_video.mp4 抽出指定 azimuth 对应的帧,保存为 PNG帧数正确、角度映射正确。

  • P2-1 宿主机 ffmpeg 未装,改用 imageio-ffmpeg==0.5.1(静态二进制随包,免系统依赖),已写入 requirements.txt
  • P2-2 services/video_frame_extractor.py 新建,实现 extract_by_azimuth(combined_video_path, azimuths, elevations=None, total_rotation_deg=360)
    • 策略改为"一次抽全帧到 temp dir + 按帧索引挑" —— 比 -ss 逐帧抽更稳(不受 keyframe 对齐影响),且 5s 视频成本可忽略
    • 抽出后用 PIL 按中线切:左半 → color_png、右半 → normal_png实测 1024×512 → 512×512 方形)
    • ffmpeg + PIL 是阻塞的,用 run_in_executor 丢到线程池避免堵住 async 事件循环
    • 临时目录抽完即清理,只保留切好的最终帧
  • P2-3 view_transform._transform_view_mesh 串接 Stage 1 → Stage 2默认 azimuths=[0,60,120,180,240,300]Stage 2 失败不会把整体置为失败Stage 1 的 .glb / video 仍有交付价值,error 字段单独带回 Stage 2 错误)
  • P2-4 本地测试脚本 scripts/test_frame_extract.py等用户执行验收)
    • Agent 已自测120 帧视频 → az=0/60/120/180/240/300 映射到 frame_idx=0/20/40/60/80/100完全均匀 ✓
    • 12 个 color/normal PNG 全部落盘,尺寸 512×512 方形 ✓

Phase 3风格还原重绘预计 1.5h

验收标准6 张视角图通过 IP-Adapter+ControlNet 重绘后,风格与原图一致、几何不变形。

  • P3-1 config.py 注册 IMAGE_MODELS["ip-adapter-controlnet-depth"],标 internal=True,同时改 get_image_models_list() 过滤 internal不污染前端下拉
  • P3-2 services/style_restorer.py 新建,实现 restore(original_image, structure_image, ...) + restore_batch()(带并发 Semaphore 防 Replicate rate limit
  • P3-3 view_transform.py 增加阶段 3preserve_style=True 时对 Stage 2 的每一帧做重绘;单帧失败自动回退为 Stage 2 color 帧,不阻塞整体
  • P3-4 等待 Checkpoint 3:调参轮次需要用户执行 scripts/test_style_restore.py <原图>,分别尝试 scale=0.6/0.7/0.8 × controlnet_conditioning_scale=0.6/0.7/0.9,选出最佳组合写入默认参数

Phase 4前端 + 端到端联调(预计 1h

  • P4-1 types.ts 扩展 ViewTransformModel 类型
  • P4-2 前端模型下拉新增两个选项
  • P4-3 工作台选中 mesh 管道时显示"保留原画风"开关
  • P4-4 结果面板支持展示视频color_video+ N 张风格化后的视角图
  • P4-5 端到端联调:前端上传一张卡通建筑图 → 选 Trellis → 确认 N 张结果正确显示

Phase 5Hunyuan3D-2 + 自由相机(可选,二期,预计 3-4h

仅在 Phase 1-4 完成且用户明确要求自由 elevation/远近相机时启动。

  • P5-1 venv 安装 trimesh + pyrender + PyOpenGLWindows 上验证 headless 可用性
  • P5-2 若 pyrender 在 Windows 不可行 → 改用 Blender 4.x headlessblender.exe -b --python render.py
  • P5-3 services/mesh_renderer.py 新建,实现 render_views(glb_path, azimuths, elevations, distance=2.0, fov=45)
  • P5-4 启用 config.pyhunyuan3d 条目(enabled=True
  • P5-5 前端"高级模式"解锁 elevation 滑块

Phase 6收尾

  • P6-1 changelog-full.md / -recent.md / -headlines.md 三层同步记录
  • P6-2 发现的坑 → pitfalls.md
  • P6-3 更新 art-agent/README.md 里视角变换段落

5 · 风险与兜底

风险 概率 影响 兜底
Trellis 对 2D 卡通描边图重建几何偏糊 阶段 3 结构约束失效 先用 SDXL+InstantStyle 把原图"半写实化"一版给 Trellis 吃,最后阶段 3 用原 2D 图做 IP-Adapter 还原风格
从 color_video 抽帧只有水平环绕elevation=0不能仰视/俯视 无法满足"仰视大教堂"等需求 二期启用 Phase 5 的 pyrender 自由相机,或改用 Hunyuan3D-2 + 自定义渲染
IP-Adapter+ControlNet 6 次调用并发打满 Replicate rate limit 部分帧失败 改成串行 + 指数退避 + 失败单帧重试
总链路太慢(>3 分钟)用户失去耐心 产品体验差 前端用 SSE 流式展示中间产物:阶段 1 完成显示视频 → 阶段 3 每张完成立即插入
ffmpeg 未装 Windows Phase 2 不能跑 开工前检查 ffmpeg -version,未装先 choco install ffmpeg 或用 Python 库 imageio-ffmpeg 绑定
Trellis 某次输出 video 是 4s 某次 6s固定 total_rotation_deg=360 可能不准 抽帧角度偏差 ffprobe 动态读取总帧数;如有疑问就固定 24 帧均匀抽样全部保留

6 · 成本与性能预估

一次完整请求6 视角,保留风格)

阶段 耗时 成本
Trellis 重建 ~30s $0.041
ffmpeg 抽 6 帧 ~2s本地 0
ControlNet+IP-Adapter × 6并发 ~72s $0.42
合计 ~105s ~$0.46

参考:当前 Zero123++ 一次 ~$0.02~15s。新管道贵约 23×、慢约 7×但产出质量对建筑是跨代提升。


7 · 跨会话恢复指引

新会话的 Agent 在开工前务必

  1. 读本文件 §0-§3,建立完整技术背景
  2. art-agent/backend/app/services/view_transform.py 当前实现Zero123++ 的做法作参照)
  3. art-agent/backend/app/config.pyVIEW_TRANSFORM_MODELS / IMAGE_MODELS 当前结构
  4. art-agent/backend/app/agent/tools.pytransform_view 工具定义
  5. §4 的 Phase 顺序推进Phase 之间互相独立,完成一个交付一个
  6. 每个 Phase 完成后在本文件对应 checkbox 打勾(改 [ ][x]),并在本节底部追加简短进度 note

不要一次 Phase 1-6 全做完再交付——每个 Phase 完成后停下来让用户测试/审阅。

进度日志

  • 2026-04-20 · 会话"Zero123++ 切图诊断 + 方案调研" → 完成 §2 全部 schema 查证,写入本 SPEC。尚未开工 Phase 1。
  • 2026-04-20 · 会话"按计划实施 Mesh Pipeline" → 完成 Phase 1P1-1 ~ P1-5新增 mesh_generator.pyconfig.py 注册 trellis/hunyuan3d后者 enabled=Falseview_transform.pypipeline 字段分流、tools.pymodel_id/azimuths/preserve_style 参数,额外提供 scripts/test_trellis.py 供用户手动验收。
  • 2026-04-20 16:15 · Checkpoint 1 通过 → Agent 运行 scripts/test_trellis.py 实测发现:
    • Trellis 调用链路完全打通Replicate token / 代理 / httpx 下载 / GENERATED_DIR 落盘均正常
    • .glb (1.89MB) 下载正常
    • ⚠️ 意外发现:该部署版 color_video / normal_video 字段恒为 null,只有 combined_video 可用color+normal 左右并排 mp4 ~450KB
    • 修订 §2.1 输出字段表为实测版本P2-2 任务改为"从 combined_video 抽帧 + 切左右半",已记 PF-20260420-1600 踩坑
    • Phase 1 verdictfunctionally green,可以开 Phase 2
  • 2026-04-20 · 会话"继续执行 Phase 2" → 完成 Phase 2 全部 itemP2-1 ~ P2-4
    • 放弃系统 ffmpeg改用 imageio-ffmpeg 自带二进制22.6MB wheelWin amd64 下已测 v4.2.2 ok
    • 新增 services/video_frame_extractor.py — 一次全帧抽取 + 按索引挑 + PIL 切左右半的极简实现
    • view_transform.py 的 mesh 管道调度器现已走到 Stage 2返回的 images[]{url, azimuth, elevation, normal_url, frame_idx}
    • 新增 scripts/test_frame_extract.py 供用户复用已有 combined_video 做 Checkpoint 2 验收(无需再花钱跑 Trellis
    • Agent 自测120 帧 → 6 视角 frame_idx 均匀分布 (0/20/40/60/80/100),所有切图 512×512 方形,逻辑正确
    • 等待 Checkpoint 2 用户验收:跑 python scripts/test_frame_extract.py 并目视检查 generated/view_*_color.png 6 张图的角度顺序
  • 2026-04-20 · 会话"继续执行 Phase 3风格还原" → 完成 P3-1 ~ P3-3
    • config.py 注册内部模型 ip-adapter-controlnet-depthinternal=True),并让 get_image_models_list() 自动过滤,不会污染前端下拉;新增 helper get_style_restore_model_id()
    • 新增 services/style_restorer.pyrestore() 单帧调用 + restore_batch() 带 Semaphore 并发 3 的批量接口;统一处理 FileOutput/list/str 的 Replicate 返回
    • view_transform._transform_view_mesh 接入 Stage 3preserve_style=True 对每一帧做 IP-Adapter+ControlNet 重绘;单帧失败会自动回退为 Stage 2 color 帧,不阻塞整体;images[i] 新增 restyled 布尔和 color_url(保留原 color 便于对比)
    • 最终返回的 _stage 字段从 stage2 升级为 stage3 (N/M restyled),便于前端 / 日志快速判断链路完整度
    • 新增 scripts/test_style_restore.py 供 Checkpoint 3 用户验收:支持 --single 冒烟、--prefix 指定 normal 帧组、--scale/--cn-scale 覆盖参数,复用 Phase 2 已有帧无需再跑 Trellis
    • 等待 Checkpoint 3 用户验收:跑 python scripts/test_style_restore.py <原图> --single 做冒烟,通过后再全量 6 帧并调参P3-4

8 · 开放问题(留给未来决策)

  • 前端角度输入 UX要不要做一个"3D 预览转盘"让用户拖动选角度Phase 4+ 再说)
  • 是否把 mesh .glb 作为资产保存到用户的 gallery涉及存储与用户系统对接
  • Hunyuan3D-2 路线的 Windows 渲染器pyrender vs Blender 的选型需要一次实测才能决定
  • 是否对同一原图的重复请求加缓存Trellis 结果相对稳定,可按 image_hash 缓存 .glb