ai-chatbot/docs/框选抠图功能开发方案-2026-03-07.md
Your Name 055652cc91
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add box cutout flow for canvas images
2026-03-07 09:45:52 +08:00

18 KiB
Raw Blame History

无限画布「框选抠图」功能开发方案

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

推荐传给后端的数据格式

{
  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 请求参数设计

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 返回值设计

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:把归一化坐标转成像素坐标

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

{
  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

建议新增:

async boxCutout(params: {
  imageUrl: string;
  cropRect: { x: number; y: number; width: number; height: number };
  paddingRatio?: number;
}): Promise<ApiResponse<{ url: string }>>

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

建议补充编辑动作类型:

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. 建议结论

这项功能值得做,而且与你当前无限画布的使用方式高度匹配。
从工程角度看,最好的落地方式不是前端裁图,而是服务端基于原图裁切后复用现有抠图链路。

第一版不建议上复杂语义分割,先把:

  • 框选
  • 裁切
  • 抠图
  • 回写画布

这条主链路做稳,是性价比最高的方案。