# 无限画布「框选抠图」功能开发方案 ## 1. 功能目标 在无限画布中,用户选中一张图片后,工具栏新增一个按钮:`框选抠图`。 用户点击后,可以直接在当前选中的图片内部拖拽一个矩形框,框出其中某个元素。系统随后需要: 1. 根据框选区域,从原图中裁出该区域; 2. 对裁切后的局部图调用现有抠图能力; 3. 将抠图结果作为一张新的图片节点回写到当前画布; 4. 原图不被覆盖,结果图与原图建立来源关联。 --- ## 2. 现状评估 ### 2.1 当前已有能力 项目里已经有几块能力可直接复用: - `components/canvas/nodes/ImageNode.tsx` - 已有选中图片后的浮动工具栏; - 已有图片处理动作分发逻辑:`抠图 / 去水印 / 高清增强 / 扩图 / 视角生成 / 重绘 / 备注 / 尺寸调整`; - 已有“处理中新建节点 -> 处理成功更新节点”的结果回写模式。 - `services/canvas/index.ts` - 已封装 `removeBackground(imageUrl)`,当前直接调 `/api/rmbg`。 - `app/api/rmbg/route.ts` - 已支持两种入参: - `imageUrl / image_url` - `image_base64` - 当前服务端收到 `imageUrl` 后,会在后端下载原图,再进入抠图流程。 - `components/canvas/ObjectPickerOverlay.tsx` 与 `ImageNode.tsx` - 已有图片内点击坐标换算经验; - 已有“在图片内部交互但不影响整个画布”的模式基础。 ### 2.2 当前最适合复用的链路 从现有实现看,最合适的方式不是前端把用户框中的图裁出来再转成 base64 上传,而是: - 前端只负责: - 进入框选模式; - 记录用户框选区域; - 把框选区域以**归一化矩形坐标**发给后端。 - 后端负责: - 拉取原图; - 按框选区域精确裁切; - 调用现有抠图流程; - 保存结果并返回 URL。 这是当前架构下最稳妥的方案。 --- ## 3. 为什么不建议前端直接裁切再上传 不建议走“前端裁切 -> base64 -> `/api/rmbg`”的主路径,原因有四个: ### 3.1 会重复下载和重复编码 浏览器先下载一遍图片,裁完再转 base64 上传;服务端再解码处理。链路更长,耗时更高。 ### 3.2 会引入跨域和画布污染问题 图片如果来自 OSS、CDN、第三方 URL,浏览器 canvas 很容易遇到 tainted canvas 风险。 ### 3.3 会损失原图精度 前端通常拿到的是展示图、代理图或压缩图,不一定是原始大图。 框选抠图最好基于原图像素做裁切,不应基于展示层尺寸。 ### 3.4 会增加前端内存压力 大图在浏览器中裁切、转 base64、传输,容易造成内存抖动和卡顿,尤其在无限画布场景里更明显。 ### 结论 **最佳实践是:前端只发框选坐标,后端基于原图裁切后再调用现有抠图能力。** --- ## 4. 推荐实现方案 ## 4.1 方案概述 推荐新增一条专门的服务端编排接口,例如: - `POST /api/canvas/box-cutout` 它负责做三件事: 1. 拉原图并按框选区域裁切; 2. 调用现有抠图能力; 3. 返回抠图结果并记录来源关系。 前端 `ImageNode` 新增 `框选抠图` 按钮后,调用该接口即可。 --- ## 4.2 前端交互方案 ### 交互入口 在选中图片后的浮动工具栏中,在 `抠图` 附近增加一个新按钮: - 文案:`框选抠图` 建议位置: - 放在 `抠图` 右侧; - 属于“局部编辑类”动作,和整图 `抠图` 相邻最符合用户心智。 ### 推荐交互流程 1. 用户选中一张图片; 2. 点击 `框选抠图`; 3. 图片节点进入“框选模式”; 4. 鼠标在图片内部拖拽出矩形框; 5. 松手后显示: - 框选框; - `确认抠图`; - `取消`; 6. 点击确认后进入处理态; 7. 右侧新增一张“处理中”节点; 8. 成功后替换为抠图结果; 9. 自动退出框选模式。 ### 交互细节建议 - 支持 `Esc` 退出框选模式; - 支持重新拖拽覆盖上一次框选; - 框选时,画布拖拽、节点拖拽、缩放手柄全部临时禁用; - 框选区域外增加半透明遮罩,强调当前选择区域; - 框选框最小尺寸限制,例如 `24 x 24` 像素对应的归一化尺寸; - 加一个 `padding` 默认策略,避免用户框得太紧导致元素边缘被切掉。 --- ## 4.3 前端状态设计 不建议一开始把这个功能做成全局 store 模式。 ### MVP 建议 先在 `components/canvas/nodes/ImageNode.tsx` 本地状态实现: - `boxCutoutMode: boolean` - `boxCutoutDraftRect: { x1: number; y1: number; x2: number; y2: number } | null` - `boxCutoutRect: { x: number; y: number; width: number; height: number } | null` - `boxCutoutLoading: boolean` 原因: - 该交互只作用于当前被选中的单张图片; - 不需要全局共享; - 本地状态更简单,落地更快; - 避免和现有 `isPickerMode` 语义混淆。 ### 为什么不建议直接复用 `isPickerMode` 当前 `isPickerMode` 的语义是“点击识物”,核心是单点坐标。 `框选抠图` 的语义是“矩形区域编辑”,交互模式完全不同。 如果强行复用,会带来: - store 语义混乱; - 后续维护困难; - 同一节点内两种交互模式耦合。 ### 结论 **MVP 用 `ImageNode` 本地状态独立实现更合理。** --- ## 4.4 坐标计算方式 这是这个功能里最关键的技术点之一。 当前图片节点使用的是 `object-contain` 展示,因此用户拖拽的矩形不能直接按节点容器宽高换算,必须按**图片真实显示区域**换算。 ### 正确计算流程 假设: - 容器尺寸:`containerWidth / containerHeight` - 原图尺寸:`imageWidth / imageHeight` 则: 1. 计算展示缩放比: - `displayScale = min(containerWidth / imageWidth, containerHeight / imageHeight)` 2. 计算图片真实显示尺寸: - `renderedWidth = imageWidth * displayScale` - `renderedHeight = imageHeight * displayScale` 3. 计算图片在容器中的偏移: - `offsetX = (containerWidth - renderedWidth) / 2` - `offsetY = (containerHeight - renderedHeight) / 2` 4. 鼠标坐标减去偏移,再除以展示尺寸,得到归一化坐标: - `normalizedX = (pointerX - offsetX) / renderedWidth` - `normalizedY = (pointerY - offsetY) / renderedHeight` 5. 最后 clamp 到 `0 ~ 1` ### 推荐传给后端的数据格式 ```ts { x: 0.12, y: 0.23, width: 0.31, height: 0.28 } ``` 全部使用相对坐标,后端再根据原图真实像素做裁切。 这样: - 与前端展示尺寸无关; - 与当前画布缩放倍率无关; - 与节点尺寸变化无关; - 精度最稳定。 --- ## 5. 后端实现方案 ## 5.1 推荐新增独立接口 ### 推荐路由 - `POST /api/canvas/box-cutout` ### 不建议直接把逻辑塞进 `/api/rmbg` 虽然也可以给 `/api/rmbg` 增加 `cropRect` 参数,但不建议第一版就这么做。 原因: - `/api/rmbg` 现在职责单一:整图抠图; - `框选抠图` 实际是“裁切 + 抠图”的编排能力; - 单独接口更清晰,也更利于后续扩展成“框选扩图 / 框选去水印 / 框选重绘”。 ### 更好的工程化方式 从 `app/api/rmbg/route.ts` 中抽出共享逻辑到类似: - `lib/ai/rmbg/service.ts` 供这两个入口共同复用: - `/api/rmbg` - `/api/canvas/box-cutout` 这样可以避免复制 provider、存图、相册落库、媒体资产登记等逻辑。 --- ## 5.2 请求参数设计 ```ts type BoxCutoutRequest = { imageUrl: string; cropRect: { x: number; // 0~1 y: number; // 0~1 width: number; // 0~1 height: number; // 0~1 }; paddingRatio?: number; // 默认 0.08 prompt?: string; // 默认 "框选抠图" chatId?: string | null; messageId?: string | null; skipGallerySave?: boolean; sourceNodeId?: string; }; ``` --- ## 5.3 返回值设计 ```ts type BoxCutoutResponse = { success: boolean; original_url?: string; thumbnail_url?: string; image_base64?: string; processing_time?: number; cropRectApplied?: { x: number; y: number; width: number; height: number; }; paddedCropRectApplied?: { x: number; y: number; width: number; height: number; }; error?: string; }; ``` --- ## 5.4 服务端处理流程 ### Step 1:拉取原图 后端根据 `imageUrl` 获取原图 buffer。 ### Step 2:解析原图尺寸 使用 `sharp(metadata)` 读取: - `width` - `height` ### Step 3:把归一化坐标转成像素坐标 ```ts left = round(x * width) top = round(y * height) cropWidth = round(widthRatio * width) cropHeight = round(heightRatio * height) ``` 同时做边界保护: - 不小于 0; - 不超过原图边界; - 宽高至少为最小值。 ### Step 4:自动留白 padding 建议默认: - `paddingRatio = 0.08` 即在用户框选基础上,向四周自动扩 8%。 原因: - 用户经常会框得偏紧; - 留白能避免抠图把边缘、阴影、轮廓切掉; - 对小物体尤其重要。 ### Step 5:裁切局部图 使用 `sharp.extract({ left, top, width, height })` 得到局部 buffer。 ### Step 6:调用现有抠图能力 将裁切后的局部图作为新的输入,走现有 RMBG provider 链路。 这里有两种实现方式: #### 方式 A:快速落地版 新接口内部把裁切后的局部图转 base64,再直接调用当前 `/api/rmbg` 的内部处理逻辑。 优点: - 开发快; - 复用现有稳定能力。 缺点: - 若直接通过 HTTP 再请求一次 `/api/rmbg`,会多一层内部调用。 #### 方式 B:更推荐版 把 `/api/rmbg` 的核心逻辑抽到公共 service,再供新接口复用。 优点: - 单一职责更清晰; - 没有重复链路; - 后续扩展更容易。 ### Step 7:保存结果并回写来源关系 结果图建议继续复用现有: - 相册保存; - `GeneratedImage` 记录; - `MediaAsset` 登记; - 与源图建立 relation。 ### 推荐增加的 metadata ```ts { sourceNodeId, sourceImageUrl, cropRectNormalized, cropRectPixels, paddingRatio, mode: "box-cutout" } ``` --- ## 6. 更好的实现建议 ## 6.1 建议一:服务端裁切,而不是前端裁切 这是本功能最重要的建议,优先级最高。 ### 原因 - 精度最好; - 稳定性最高; - 避免浏览器跨域和大图内存问题; - 能保证基于原图处理而不是展示图。 --- ## 6.2 建议二:矩形框选后自动扩边 不要按用户框的矩形直接硬裁。 ### 推荐默认策略 - 四周自动扩边 `6% ~ 10%` - 默认先取 `8%` ### 原因 - 用户框得太紧是高频场景; - 不扩边时,耳朵、把手、阴影、叶片、轮廓容易被切断; - 扩边对抠图效果改善非常明显。 --- ## 6.3 建议三:第一版先做“矩形框选 + 局部抠图”,不要直接上智能分割 从产品角度看,最容易想到的是“既然框选了,为什么不直接智能分割到边缘”。 但从工程节奏看,不建议第一版就上太复杂的语义分割链路。 ### 第一版推荐 - 矩形框选 - 服务端裁切 - 调现有 RMBG ### 第二版可升级 在裁切后、抠图前,增加一个“局部前景细化”步骤,例如: - 基于轻量分割模型自动修正前景边界; - 或在裁切区域内部做一次前景检测; - 或对抠图结果再做一次自动裁边。 ### 原因 第一版要先解决的是: - 用户是否能稳定框中目标; - 结果是否能稳定回写到画布; - 整个交互是否顺手。 先把主链路做通,再优化精度最合理。 --- ## 6.4 建议四:不要复用全屏 Overlay,优先做节点内局部模式 现有 `ObjectPickerOverlay` 已经退化为不启用状态,当前交互主线在 `ImageNode` 内部。 ### 推荐 框选抠图也保持同样原则: - 直接在被选中的图片节点内绘制框选层; - 不做全屏遮罩; - 不打断用户对无限画布的空间感知。 ### 好处 - 更符合当前产品形态; - 交互更轻; - 状态更简单; - 不会和画布右侧对话区、底部工具栏形成过强冲突。 --- ## 6.5 建议五:结果图默认与原图并排生成 直接复用当前 `ImageNode` 图片处理动作的结果生成模式: - 在原图右侧新建一个处理中节点; - 完成后更新为结果图; - `originalSrc` 指回原图。 ### 这样做的好处 - 用户认知成本低; - 和现有抠图、扩图、高清增强一致; - 易于后续启用“对比显示”。 --- ## 7. 推荐改动点 ## 7.1 前端 ### `components/canvas/nodes/ImageNode.tsx` 需要新增: - `框选抠图` 按钮; - 框选模式本地状态; - 鼠标拖拽生成矩形框; - 确认 / 取消 UI; - 调用新接口; - 新建处理中节点并更新结果。 ### `services/canvas/index.ts` 建议新增: ```ts async boxCutout(params: { imageUrl: string; cropRect: { x: number; y: number; width: number; height: number }; paddingRatio?: number; }): Promise> ``` --- ## 7.2 后端 ### 新增 - `app/api/canvas/box-cutout/route.ts` ### 建议抽取 - `lib/ai/rmbg/service.ts` - `lib/image/crop.ts` 用途: - 统一做图片下载、尺寸解析、裁切、边界 clamp; - 统一复用 RMBG 核心调用逻辑。 --- ## 7.3 类型 ### `types/canvas/index.ts` 建议补充编辑动作类型: ```ts type ImageEditAction = | "rmbg" | "enhance" | "extend" | "resize" | "angle-edit" | "scene-fusion" | "annotate" | "text-edit" | "box-cutout"; ``` 如果第一版只是局部功能,也可以先不改类型,把 metadata 先记下来。 --- ## 8. 数据落库建议 第一版不是必须新增表结构。 ### 建议直接复用现有图片记录链路 - `GeneratedImage` - `MediaAsset` - `MediaAssetRelation` ### 推荐 relation 语义 如果现有 relationType 可自由写字符串,建议新增: - `cropped_from` - `removed_background_from` 如果暂时不想扩 relationType,也可以先用现有: - `edited_from` 并在 `metadata.mode = "box-cutout"` 中区分。 --- ## 9. 边界情况与容错 ### 9.1 用户框得过小 处理建议: - 前端最小框选尺寸限制; - 后端再次做最小尺寸校验; - 小于阈值时直接报错: - `框选区域过小,请重新框选` ### 9.2 用户框选超出图片显示区 处理建议: - 前端坐标直接 clamp; - 只允许在真实图片显示区域内绘制。 ### 9.3 用户框了整张图的大部分区域 处理建议: - 如果裁切区域面积超过整图 `85%`,可直接退化为普通抠图; - 避免“几乎整图裁切”带来的多余处理。 ### 9.4 原图拉取失败 错误提示: - `原图获取失败,请稍后重试` ### 9.5 裁切后抠图失败 错误提示: - `框选抠图失败,请重新框选或稍后重试` --- ## 10. 验收标准 满足以下条件即可认为 MVP 可交付: 1. 选中图片后能看到 `框选抠图` 按钮; 2. 点击后可在图片内部拖拽矩形框; 3. 框选区域外有遮罩; 4. 确认后右侧生成一个“处理中”图片节点; 5. 处理完成后节点更新为透明背景抠图结果; 6. 结果图与原图保持来源关系; 7. 原图不被覆盖; 8. 失败时能正确提示且不污染已有节点; 9. 框选模式可取消、可重选、可按 `Esc` 退出。 --- ## 11. 测试清单 ### 11.1 交互测试 - 单张图片正常框选; - 连续多次重新框选; - 按 `Esc` 退出; - 点击空白取消; - 画布缩放后再框选; - 图片被缩放节点尺寸后再框选。 ### 11.2 数据正确性测试 - 框选左上角物体; - 框选右下角物体; - 目标贴边; - 目标很小; - 目标透明或半透明; - 目标带阴影; - 原图是长图 / 高图 / 方图。 ### 11.3 稳定性测试 - CDN 图片; - OSS 图片; - 大图; - 慢网; - 抠图 provider 失败; - 后端存图失败。 --- ## 12. 开发优先级建议 ## Phase 1:MVP(推荐先做) - 工具栏新增 `框选抠图` - 节点内矩形框选 - 服务端裁切 + 复用 RMBG - 结果回写画布 ## Phase 2:体验优化 - 自动 padding 可配置 - 框选确认前局部预览 - 热键支持 - 框选后回显裁切预览 ## Phase 3:精度优化 - 局部分割细化 - 自动贴边修正 - 更智能的小物体保护 --- ## 13. 最终建议 如果以“最快可上线且效果稳定”为目标,建议采用下面这条方案: ### 最推荐方案 1. 前端在 `ImageNode` 上新增 `框选抠图` 按钮; 2. 用户在节点内拖拽矩形框; 3. 前端只上传**归一化框选坐标**; 4. 后端新增 `POST /api/canvas/box-cutout`; 5. 后端基于原图裁切局部图,并自动加 `8%` padding; 6. 后端复用现有 `/api/rmbg` 核心能力做抠图; 7. 结果图按现有模式回写到画布右侧。 ### 为什么这是当前最优解 - 改动范围可控; - 不会破坏现有 `/api/rmbg` 主链路; - 用户体验足够直观; - 效果比整图抠图更符合“抠局部元素”的预期; - 后续还能平滑升级到更智能的局部分割方案。 --- ## 14. 预计改造工作量 如果只做 MVP: - 前端交互:0.5 ~ 1 天 - 后端接口:0.5 ~ 1 天 - 联调与修正:0.5 天 ### 合计 - 约 `1.5 ~ 2.5 天` 如果加上“局部分割细化”和“框选预览”: - 再增加 `1 ~ 2 天` --- ## 15. 建议结论 这项功能值得做,而且与你当前无限画布的使用方式高度匹配。 从工程角度看,**最好的落地方式不是前端裁图,而是服务端基于原图裁切后复用现有抠图链路**。 第一版不建议上复杂语义分割,先把: - `框选` - `裁切` - `抠图` - `回写画布` 这条主链路做稳,是性价比最高的方案。