ai-chatbot/docs/feature-object-picker.md

21 KiB
Raw Blame History

「物品拾取器」功能开发方案 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 数据结构

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 分析返回速度,前端在发送图片前需进行压缩:

// 图片压缩配置
const PICKER_IMAGE_CONFIG = {
  maxSize: 600,      // 最大宽度或高度 600px
  quality: 0.85,     // JPEG 质量
  format: 'jpeg'     // 输出格式
};

压缩策略:

  • 宽度 > 600px 或 高度 > 600px 时,等比缩放至最大边 600px
  • 例如:1200x800 → 600x400,800x1200 → 400x600
  • 保持宽高比不变,确保点击坐标映射准确

压缩函数:

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

请求

{
  "imageData": "data:image/jpeg;base64,...",  // 压缩后的图片(Base64)
  "clickX": 0.35,
  "clickY": 0.42,
  "originalImageUrl": "https://s3.xxx/image.jpg",  // 原图 URL(用于生成标签)
  "options": {
    "includeMask": false,
    "includePolygon": false
  }
}

响应

{
  "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个字):

横向 纵向 组合示例
左侧 顶部 左上区域
中央 中部 中央区域
右侧 底部 右下区域
✅ 正确:左上区域、中央区域、右下区域、左侧中部
❌ 错误:图片的左边偏上一点的位置、大概在中间靠右

实现代码

// lib/ai/pick-object.ts

export async function pickObject(
  imageData: string,
  clickX: number,
  clickY: number,
  originalImageUrl: string
): Promise<PickedObjectResult> {
  
  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 扩展

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 发送消息时的数据

{
  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 理解错误"的挫败感
  • 编辑效率提升:一次点击 > 多次文字描述
  • 差异化竞争力:独特的交互体验