18 KiB
无限画布「框选抠图」功能开发方案
1. 功能目标
在无限画布中,用户选中一张图片后,工具栏新增一个按钮:框选抠图。
用户点击后,可以直接在当前选中的图片内部拖拽一个矩形框,框出其中某个元素。系统随后需要:
- 根据框选区域,从原图中裁出该区域;
- 对裁切后的局部图调用现有抠图能力;
- 将抠图结果作为一张新的图片节点回写到当前画布;
- 原图不被覆盖,结果图与原图建立来源关联。
2. 现状评估
2.1 当前已有能力
项目里已经有几块能力可直接复用:
-
components/canvas/nodes/ImageNode.tsx- 已有选中图片后的浮动工具栏;
- 已有图片处理动作分发逻辑:
抠图 / 去水印 / 高清增强 / 扩图 / 视角生成 / 重绘 / 备注 / 尺寸调整; - 已有“处理中新建节点 -> 处理成功更新节点”的结果回写模式。
-
services/canvas/index.ts- 已封装
removeBackground(imageUrl),当前直接调/api/rmbg。
- 已封装
-
app/api/rmbg/route.ts- 已支持两种入参:
imageUrl / image_urlimage_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
它负责做三件事:
- 拉原图并按框选区域裁切;
- 调用现有抠图能力;
- 返回抠图结果并记录来源关系。
前端 ImageNode 新增 框选抠图 按钮后,调用该接口即可。
4.2 前端交互方案
交互入口
在选中图片后的浮动工具栏中,在 抠图 附近增加一个新按钮:
- 文案:
框选抠图
建议位置:
- 放在
抠图右侧; - 属于“局部编辑类”动作,和整图
抠图相邻最符合用户心智。
推荐交互流程
- 用户选中一张图片;
- 点击
框选抠图; - 图片节点进入“框选模式”;
- 鼠标在图片内部拖拽出矩形框;
- 松手后显示:
- 框选框;
确认抠图;取消;
- 点击确认后进入处理态;
- 右侧新增一张“处理中”节点;
- 成功后替换为抠图结果;
- 自动退出框选模式。
交互细节建议
- 支持
Esc退出框选模式; - 支持重新拖拽覆盖上一次框选;
- 框选时,画布拖拽、节点拖拽、缩放手柄全部临时禁用;
- 框选区域外增加半透明遮罩,强调当前选择区域;
- 框选框最小尺寸限制,例如
24 x 24像素对应的归一化尺寸; - 加一个
padding默认策略,避免用户框得太紧导致元素边缘被切掉。
4.3 前端状态设计
不建议一开始把这个功能做成全局 store 模式。
MVP 建议
先在 components/canvas/nodes/ImageNode.tsx 本地状态实现:
boxCutoutMode: booleanboxCutoutDraftRect: { x1: number; y1: number; x2: number; y2: number } | nullboxCutoutRect: { x: number; y: number; width: number; height: number } | nullboxCutoutLoading: boolean
原因:
- 该交互只作用于当前被选中的单张图片;
- 不需要全局共享;
- 本地状态更简单,落地更快;
- 避免和现有
isPickerMode语义混淆。
为什么不建议直接复用 isPickerMode
当前 isPickerMode 的语义是“点击识物”,核心是单点坐标。
框选抠图 的语义是“矩形区域编辑”,交互模式完全不同。
如果强行复用,会带来:
- store 语义混乱;
- 后续维护困难;
- 同一节点内两种交互模式耦合。
结论
MVP 用 ImageNode 本地状态独立实现更合理。
4.4 坐标计算方式
这是这个功能里最关键的技术点之一。
当前图片节点使用的是 object-contain 展示,因此用户拖拽的矩形不能直接按节点容器宽高换算,必须按图片真实显示区域换算。
正确计算流程
假设:
- 容器尺寸:
containerWidth / containerHeight - 原图尺寸:
imageWidth / imageHeight
则:
- 计算展示缩放比:
displayScale = min(containerWidth / imageWidth, containerHeight / imageHeight)
- 计算图片真实显示尺寸:
renderedWidth = imageWidth * displayScalerenderedHeight = imageHeight * displayScale
- 计算图片在容器中的偏移:
offsetX = (containerWidth - renderedWidth) / 2offsetY = (containerHeight - renderedHeight) / 2
- 鼠标坐标减去偏移,再除以展示尺寸,得到归一化坐标:
normalizedX = (pointerX - offsetX) / renderedWidthnormalizedY = (pointerY - offsetY) / renderedHeight
- 最后 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) 读取:
widthheight
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.tslib/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. 数据落库建议
第一版不是必须新增表结构。
建议直接复用现有图片记录链路
GeneratedImageMediaAssetMediaAssetRelation
推荐 relation 语义
如果现有 relationType 可自由写字符串,建议新增:
cropped_fromremoved_background_from
如果暂时不想扩 relationType,也可以先用现有:
edited_from
并在 metadata.mode = "box-cutout" 中区分。
9. 边界情况与容错
9.1 用户框得过小
处理建议:
- 前端最小框选尺寸限制;
- 后端再次做最小尺寸校验;
- 小于阈值时直接报错:
框选区域过小,请重新框选
9.2 用户框选超出图片显示区
处理建议:
- 前端坐标直接 clamp;
- 只允许在真实图片显示区域内绘制。
9.3 用户框了整张图的大部分区域
处理建议:
- 如果裁切区域面积超过整图
85%,可直接退化为普通抠图; - 避免“几乎整图裁切”带来的多余处理。
9.4 原图拉取失败
错误提示:
原图获取失败,请稍后重试
9.5 裁切后抠图失败
错误提示:
框选抠图失败,请重新框选或稍后重试
10. 验收标准
满足以下条件即可认为 MVP 可交付:
- 选中图片后能看到
框选抠图按钮; - 点击后可在图片内部拖拽矩形框;
- 框选区域外有遮罩;
- 确认后右侧生成一个“处理中”图片节点;
- 处理完成后节点更新为透明背景抠图结果;
- 结果图与原图保持来源关系;
- 原图不被覆盖;
- 失败时能正确提示且不污染已有节点;
- 框选模式可取消、可重选、可按
Esc退出。
11. 测试清单
11.1 交互测试
- 单张图片正常框选;
- 连续多次重新框选;
- 按
Esc退出; - 点击空白取消;
- 画布缩放后再框选;
- 图片被缩放节点尺寸后再框选。
11.2 数据正确性测试
- 框选左上角物体;
- 框选右下角物体;
- 目标贴边;
- 目标很小;
- 目标透明或半透明;
- 目标带阴影;
- 原图是长图 / 高图 / 方图。
11.3 稳定性测试
- CDN 图片;
- OSS 图片;
- 大图;
- 慢网;
- 抠图 provider 失败;
- 后端存图失败。
12. 开发优先级建议
Phase 1:MVP(推荐先做)
- 工具栏新增
框选抠图 - 节点内矩形框选
- 服务端裁切 + 复用 RMBG
- 结果回写画布
Phase 2:体验优化
- 自动 padding 可配置
- 框选确认前局部预览
- 热键支持
- 框选后回显裁切预览
Phase 3:精度优化
- 局部分割细化
- 自动贴边修正
- 更智能的小物体保护
13. 最终建议
如果以“最快可上线且效果稳定”为目标,建议采用下面这条方案:
最推荐方案
- 前端在
ImageNode上新增框选抠图按钮; - 用户在节点内拖拽矩形框;
- 前端只上传归一化框选坐标;
- 后端新增
POST /api/canvas/box-cutout; - 后端基于原图裁切局部图,并自动加
8%padding; - 后端复用现有
/api/rmbg核心能力做抠图; - 结果图按现有模式回写到画布右侧。
为什么这是当前最优解
- 改动范围可控;
- 不会破坏现有
/api/rmbg主链路; - 用户体验足够直观;
- 效果比整图抠图更符合“抠局部元素”的预期;
- 后续还能平滑升级到更智能的局部分割方案。
14. 预计改造工作量
如果只做 MVP:
- 前端交互:0.5 ~ 1 天
- 后端接口:0.5 ~ 1 天
- 联调与修正:0.5 天
合计
- 约
1.5 ~ 2.5 天
如果加上“局部分割细化”和“框选预览”:
- 再增加
1 ~ 2 天
15. 建议结论
这项功能值得做,而且与你当前无限画布的使用方式高度匹配。
从工程角度看,最好的落地方式不是前端裁图,而是服务端基于原图裁切后复用现有抠图链路。
第一版不建议上复杂语义分割,先把:
框选裁切抠图回写画布
这条主链路做稳,是性价比最高的方案。