# 「物品拾取器」功能开发方案 v2.0 > 📅 更新日期:2024-02 > 🔍 基于行业调研和技术分析的优化方案 --- ## 一、需求分析 ### 1.1 用户原始需求 - 用户选中画布图片后,点击"获取物品"按钮 - 鼠标变成红色小手图标 - 点击图片中的物品,小手图标贴在点击位置 - 后台调用 QWEN-VL 模型识别物品名称和位置 - 生成包含物品信息的标签到对话框 ### 1.2 深度理解:用户的真实目标 **核心痛点:** 用户想要精准地告诉 AI "我要处理图片中的这个东西" **当前问题:** 1. 用户只能用文字描述物品(如"左边的椅子"),AI 可能理解错误 2. 复杂场景中,文字描述不够精准(如"第二排右边第三个人") 3. 用户不知道 AI 能否正确识别他想要的物品 **用户期望:** - 像"指哪打哪"一样精准指定目标物品 - AI 能准确理解并反馈识别结果 - 后续编辑操作(替换、删除、修改)能精准作用于该物品 --- ## 二、行业调研与技术选型 ### 2.1 竞品功能分析 | 产品 | 功能名称 | 实现方式 | 优缺点 | |------|----------|----------|--------| | **Midjourney** | Vary Region | 矩形/套索工具手动选区,修改 prompt 重新生成 | ✅ 灵活 ❌ 需手动勾选,学习成本高 | | **Adobe Firefly** | Generative Fill | 点击+自动识别边界,AI 填充 | ✅ 智能边界识别 ❌ 依赖 Adobe 生态 | | **Canva Magic Edit** | Magic Eraser | 画笔涂抹选区 | ✅ 直观 ❌ 精度依赖用户手绘 | | **Remove.bg** | 前景分离 | 自动识别主体 | ✅ 一键操作 ❌ 只能选主体 | ### 2.2 核心技术调研 #### SAM (Segment Anything Model) - Meta - **能力**:点击即可分割任意物品,生成精准蒙版 - **优势**: - 支持点击 (Point)、框选 (Box)、文本 (Text) 三种选择方式 - 可在浏览器端运行(WebAssembly + ONNX) - 实时响应,无需等待 API - **API 服务**: - Segmind API:$0.008/次,~3.8s 响应 - Fal.ai SAM2:支持 WebSocket 实时连接 #### Qwen-VL 视觉语言模型 - **能力**:识别物品名称、位置、属性 - **输出格式**: - `bbox_2d`:边界框坐标 - `point_2d`:点坐标 - 物品描述文本 - **局限**:复杂场景准确率约 45%(需要结合分割模型提升精度) #### Google MediaPipe Interactive Segmenter - **能力**:点击生成分割蒙版 - **平台**:Android、Web、Python - **优势**:轻量级,适合移动端 ### 2.3 技术方案对比 | 方案 | 描述 | 延迟 | 精度 | 实现成本 | |------|------|------|------|----------| | **A. 纯 VLM** | 点击坐标 → Qwen-VL → 名称+位置 | ~2s | ⭐⭐ | 低 | | **B. SAM + VLM** | 点击 → SAM 分割 → VLM 识别 | ~4s | ⭐⭐⭐⭐ | 中 | | **C. 浏览器 SAM** | 预计算 Embedding → 点击即时分割 | <100ms | ⭐⭐⭐⭐⭐ | 高 | | **D. SAM API + VLM** | SAM Cloud API → VLM | ~5s | ⭐⭐⭐⭐ | 中 | ### 2.4 推荐方案 **MVP 阶段**:方案 A(纯 VLM) - 快速上线验证需求 - 1-2 天开发周期 **正式版本**:方案 B/D(SAM + VLM) - 提供物品轮廓可视化 - 蒙版数据可用于后续精准编辑 **高级版本**:方案 C(浏览器端 SAM) - 极致体验,鼠标悬停即可预览轮廓 - 适合高频用户场景 --- ## 三、功能设计 ### 3.1 功能定位 **「物品拾取器」** - 可视化的物品选择工具,让用户通过点击精准指定图片中的目标物品。 ### 3.2 核心价值 | 价值点 | 说明 | |--------|------| | **精准指定** | 点击即选中,无需文字描述 | | **即时反馈** | AI 识别后显示物品名称和轮廓,用户可确认 | | **上下文传递** | 物品信息自动带入对话,后续指令更精准 | | **降低沟通成本** | 减少"不是这个,是那个"的来回沟通 | ### 3.3 交互流程(MVP 版本) ``` ┌─────────────────────────────────────────────────────────────┐ │ 1. 用户选中图片 → 工具栏显示「🎯」拾取按钮 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 2. 点击「🎯」→ 进入拾取模式 │ │ - 鼠标变成 ⊕ 准星图标 │ │ - 图片边框变成橙色虚线 │ │ - 顶部提示:「点击图片中的物品」 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 3. 用户点击图片某位置 │ │ - 点击位置显示脉冲动画圆点 ◉ │ │ - 提示:「正在识别...」 │ │ - 调用 API 识别物品 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 4. 识别完成 → 显示结果浮层 │ │ │ │ ┌────────────────────────────────┐ │ │ │ 🎯 识别结果 │ │ │ │ │ │ │ │ ┌──────┐ 黑色皮质沙发 │ │ │ │ │ 缩略 │ 位置:左侧中央 │ │ │ │ └──────┘ │ │ │ │ │ │ │ │ [重新选择] [添加到对话 ✓] │ │ │ └────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 5. 用户确认 → 生成标签到对话框 │ │ │ │ 对话框: │ │ ┌─────────────────────────────────────────┐ │ │ │ 🎯 [缩略图] 黑色皮质沙发 · 左侧中央 │ │ │ └─────────────────────────────────────────┘ │ │ │ │ 用户输入:「把这个换成白色」 │ └─────────────────────────────────────────────────────────────┘ ``` ### 3.4 交互流程(增强版本 - 带 SAM 分割) ``` ┌─────────────────────────────────────────────────────────────┐ │ 3. 用户点击图片某位置 │ │ - Step 1: SAM 分割 → 获取物品蒙版 │ │ - Step 2: VLM 识别 → 获取物品名称 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 4. 识别完成 → 图片上显示物品轮廓 │ │ │ │ ┌────────────────────────────────────────┐ │ │ │ │ │ │ │ ╭───────────╮ ← 橙色轮廓线 │ │ │ │ │ 沙发 │ │ │ │ │ ╰───────────╯ │ │ │ │ │ │ │ │ ┌────────────────────────┐ │ │ │ │ │ 🎯 黑色皮质沙发 │ │ │ │ │ │ 左侧中央 │ │ │ │ │ │ [重新选] [添加到对话] │ │ │ │ │ └────────────────────────┘ │ │ │ └────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### 3.5 数据结构 ```typescript interface PickedObject { // 原图信息 imageUrl: string; imageWidth: number; imageHeight: number; // 点击位置(相对坐标 0-1) clickX: number; clickY: number; // AI 识别结果(严格格式) objectName: string; // ≤8字,格式:[颜色/材质]+[物品],如"黑色沙发" objectPosition: string; // 固定4字,如"左上区域"、"中央区域" confidence: number; // 置信度 0-1 // 物品边界框(必需) boundingBox: { x: number; // 相对坐标 0-1,精确到小数点后2位 y: number; width: number; height: number; }; // 分割蒙版(增强版,可选) mask?: { dataUrl: string; // Base64 PNG 蒙版图 polygon?: Array<{ x: number; y: number }>; // 轮廓点 }; } // 位置词库常量 const POSITION_OPTIONS = [ '左上区域', '中央顶部', '右上区域', '左侧中部', '中央区域', '右侧中部', '左下区域', '中央底部', '右下区域' ] as const; ``` --- ## 四、技术实现 ### 4.1 图片压缩优化 为加速 AI 分析返回速度,前端在发送图片前需进行压缩: ```typescript // 图片压缩配置 const PICKER_IMAGE_CONFIG = { maxSize: 600, // 最大宽度或高度 600px quality: 0.85, // JPEG 质量 format: 'jpeg' // 输出格式 }; ``` **压缩策略:** - 宽度 > 600px 或 高度 > 600px 时,等比缩放至最大边 600px - 例如:1200x800 → 600x400,800x1200 → 400x600 - 保持宽高比不变,确保点击坐标映射准确 **压缩函数:** ```typescript async function compressImageForPicker(imageUrl: string): Promise<{ compressedUrl: string; // Base64 或临时 URL scale: number; // 缩放比例,用于坐标映射 originalWidth: number; originalHeight: number; }> { const img = new Image(); img.crossOrigin = 'anonymous'; return new Promise((resolve, reject) => { img.onload = () => { const { naturalWidth, naturalHeight } = img; let scale = 1; let targetWidth = naturalWidth; let targetHeight = naturalHeight; // 计算缩放比例 if (naturalWidth > 600 || naturalHeight > 600) { scale = Math.min(600 / naturalWidth, 600 / naturalHeight); targetWidth = Math.round(naturalWidth * scale); targetHeight = Math.round(naturalHeight * scale); } // Canvas 压缩 const canvas = document.createElement('canvas'); canvas.width = targetWidth; canvas.height = targetHeight; const ctx = canvas.getContext('2d')!; ctx.drawImage(img, 0, 0, targetWidth, targetHeight); const compressedUrl = canvas.toDataURL('image/jpeg', 0.85); resolve({ compressedUrl, scale, originalWidth: naturalWidth, originalHeight: naturalHeight }); }; img.onerror = reject; img.src = imageUrl; }); } ``` ### 4.2 API 设计 #### 端点 ``` POST /api/pick-object ``` #### 请求 ```json { "imageData": "data:image/jpeg;base64,...", // 压缩后的图片(Base64) "clickX": 0.35, "clickY": 0.42, "originalImageUrl": "https://s3.xxx/image.jpg", // 原图 URL(用于生成标签) "options": { "includeMask": false, "includePolygon": false } } ``` #### 响应 ```json { "success": true, "object": { "name": "黑色皮质沙发", "position": "图片左侧中央偏下", "confidence": 0.92, "boundingBox": { "x": 0.1, "y": 0.3, "width": 0.4, "height": 0.35 }, "mask": null, "polygon": null } } ``` ### 4.3 后端实现(MVP 版本) #### Prompt 设计原则 - **字数限制**:名称 ≤8字,位置 ≤4字 - **格式统一**:固定词库,避免自由发挥 - **简洁清晰**:无冗余修饰词 #### 标准词库 **物品名称格式**:`[颜色/材质] + [物品]`(≤8字) ``` ✅ 正确:黑色沙发、白色花瓶、木质书架、红色跑车 ❌ 错误:一个非常漂亮的黑色皮质沙发、看起来像是白色的陶瓷花瓶 ``` **位置描述词库**(固定4个字): | 横向 | 纵向 | 组合示例 | |------|------|----------| | 左侧 | 顶部 | 左上区域 | | 中央 | 中部 | 中央区域 | | 右侧 | 底部 | 右下区域 | ``` ✅ 正确:左上区域、中央区域、右下区域、左侧中部 ❌ 错误:图片的左边偏上一点的位置、大概在中间靠右 ``` #### 实现代码 ```typescript // lib/ai/pick-object.ts export async function pickObject( imageData: string, clickX: number, clickY: number, originalImageUrl: string ): Promise { const prompt = ` 识别图片中坐标 (${(clickX * 100).toFixed(0)}%, ${(clickY * 100).toFixed(0)}%) 位置的物品。 【输出要求 - 严格遵守】 返回纯 JSON,无其他文字: { "name": "物品名称", "position": "位置描述", "confidence": 0.95, "boundingBox": { "x": 0.1, "y": 0.2, "width": 0.3, "height": 0.4 } } 【name 格式】≤8字,格式:[颜色/材质]+[物品] 正确:黑色沙发、白色花瓶、木质书架、玻璃茶几 错误:一个黑色的皮质沙发(太长) 【position 格式】固定4字,从以下选择: 横向:左侧/中央/右侧 纵向:顶部/中部/底部 组合:左上区域、左侧中部、左下区域、中央顶部、中央区域、中央底部、右上区域、右侧中部、右下区域 【boundingBox】相对坐标 0-1,精确到小数点后2位 `; const result = await callQwenVL(imageData, prompt); return JSON.parse(result); } ``` **性能预估:** | 图片尺寸 | 压缩后 | API 响应时间 | |----------|--------|--------------| | 2000x1500 | 600x450 | ~1-2s | | 1200x800 | 600x400 | ~1-2s | | 800x600 | 600x450 | ~1s | | 600x400 | 不压缩 | <1s | ### 4.4 前端组件 ``` components/ ├── object-picker/ │ ├── ObjectPickerMode.tsx # 拾取模式状态管理 │ ├── ObjectPickerOverlay.tsx # 图片遮罩层 │ ├── ObjectPickerCursor.tsx # 自定义光标 │ ├── ObjectPickerResult.tsx # 识别结果弹窗 │ ├── ObjectPickerOutline.tsx # 物品轮廓显示(增强版) │ └── PickedObjectTag.tsx # 对话框中的物品标签 ``` ### 4.5 Canvas Store 扩展 ```typescript interface CanvasStore { // ... 现有状态 // 物品拾取模式 isPickerMode: boolean; pickerTargetNodeId: string | null; setPickerMode: (enabled: boolean, nodeId?: string) => void; // 拾取结果 pickedObject: PickedObject | null; setPickedObject: (obj: PickedObject | null) => void; // 待添加到对话的拾取物品 pendingPickedObjects: PickedObject[]; addPendingPickedObject: (obj: PickedObject) => void; clearPendingPickedObjects: () => void; } ``` --- ## 五、对话框集成 ### 5.1 物品标签样式 ``` ┌────────────────────────────────┐ │ 🎯 [缩略图] 黑色沙发 · 左上区域 │ └────────────────────────────────┘ ``` **标签格式**:`🎯 [缩略图] {名称} · {位置}` - **图标** 🎯:区分普通图片和拾取物品 - **缩略图**:裁剪自边界框区域 - **名称**:≤8字,如"黑色沙发" - **位置**:固定4字,如"左上区域" **示例**: | 完整标签 | 字数 | |----------|------| | 🎯 黑色沙发 · 左上区域 | 12字 | | 🎯 白色花瓶 · 中央区域 | 12字 | | 🎯 木质书架 · 右下区域 | 12字 | ### 5.2 发送消息时的数据 ```typescript { type: "file", mediaType: "image/jpeg", url: "https://...", // 🆕 物品拾取信息 pickedObject: { name: "黑色皮质沙发", position: "左侧中央", clickX: 0.35, clickY: 0.42, boundingBox: { x: 0.1, y: 0.3, width: 0.4, height: 0.35 } } } ``` ### 5.3 后端 Prompt 增强 当消息包含 `pickedObject` 时,自动增强 prompt: ``` 用户想要处理图片中的「黑色皮质沙发」。 - 物品位置:左侧中央 - 边界框:(10%, 30%) - (50%, 65%) - 用户点击坐标:(35%, 42%) 用户指令:把这个沙发换成白色 请精准地只修改该物品区域,保持其他部分不变。 ``` --- ## 六、扩展功能(未来迭代) ### 6.1 多物品选择 - 支持连续点击选择多个物品 - 对话框显示多个物品标签 - "把 A 和 B 都删掉" ### 6.2 负向选择 - 标记"不要这个" - 用于排除干扰物品 ### 6.3 智能建议 - 进入拾取模式时,自动高亮主要物品 - 显示可点击的热区提示 ### 6.4 历史记录 - 记住用户最近拾取的物品 - 快速重新选择 ### 6.5 快捷键 - 按住 Alt + 点击:直接拾取(跳过确认) - Esc:退出拾取模式 --- ## 七、开发计划 ### Phase 1:MVP(1-2 天) - [ ] 工具栏「🎯」拾取按钮 - [ ] 拾取模式 UI(光标、提示) - [ ] `/api/pick-object` API - [ ] 识别结果弹窗 - [ ] 物品标签生成到对话框 ### Phase 2:体验优化(2-3 天) - [ ] 点击脉冲动画 - [ ] 边界框可视化显示 - [ ] 标签样式优化 - [ ] 错误处理与重试 - [ ] 支持 ESC 退出 ### Phase 3:功能增强(3-5 天) - [ ] SAM 集成,显示物品轮廓 - [ ] 多物品选择 - [ ] 拾取历史记录 - [ ] 快捷键支持 --- ## 八、总结 ### 功能本质 **让用户从"描述物品"变成"指向物品"**,大幅降低人机沟通成本,提升图片编辑精准度。 ### 核心创新 1. **点击即选中** - 最直观的物品选择方式 2. **AI 即时反馈** - 显示物品名称和轮廓,用户可确认 3. **上下文自动传递** - 物品信息(含坐标)无缝带入对话 ### 竞争优势 - 市面上少有的"精准物品选择 + AI 编辑"组合 - 比 Midjourney 更简单(无需手动勾选) - 比文字描述更准确(减少误解) ### 预期效果 - 用户满意度提升:减少"AI 理解错误"的挫败感 - 编辑效率提升:一次点击 > 多次文字描述 - 差异化竞争力:独特的交互体验