ai-chatbot/图片物体智能标注系统-实施文档.md
Your Name 53ddb7e585
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Initial commit - clean repository
2025-12-17 20:15:18 +08:00

35 KiB
Raw Blame History

图片物体智能标注系统 - 实施文档

📋 项目概述

功能描述

用户在对话框中上传图片后,可以通过点击图片预览中的"定位标注"按钮,点击图片中的任意物体,系统自动识别该物体的名称(包括颜色、材质等详细信息),并将识别结果以特殊格式自动填入对话框。

使用场景示例

  1. 用户上传两张图片
  2. 点击第一张图中的"绿色手提包" → 输入框自动添加:📍图1: 绿色手提包
  3. 点击第二张图中的"红色背包" → 输入框继续添加:📍图2: 红色背包
  4. 用户手动输入"替换" → 最终输入框内容:将📍图1: 绿色手提包 替换成 📍图2: 红色背包

🏗️ 技术架构

方案选择:Florence-2

选择理由:

  • ✅ 零配置:不需要预定义物体类别
  • ✅ 速度快:推理时间 0.5-1秒
  • ✅ 识别准确:自动识别颜色、材质、细节
  • ✅ 轻量级:模型仅 230MB
  • ✅ 多任务支持:物体检测、区域描述、OCR 等

系统架构图

┌─────────────────────────────────────────────────────────┐
│                        前端 (React)                       │
├─────────────────────────────────────────────────────────┤
│  1. MultimodalInput (上传图片)                           │
│  2. ImagePreview (图片预览 + 标注按钮)                   │
│  3. ObjectAnnotation (点击检测 + 标注UI)                 │
└─────────────────┬───────────────────────────────────────┘
                  │ HTTP POST
                  ▼
┌─────────────────────────────────────────────────────────┐
│                  后端 API (FastAPI)                       │
├─────────────────────────────────────────────────────────┤
│  POST /api/detect-object                                 │
│  - 接收图片 URL 和点击坐标                               │
│  - 裁剪点击区域                                           │
│  - 调用 Florence-2 模型                                  │
│  - 返回物体描述                                           │
└─────────────────┬───────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────────┐
│               Florence-2 模型服务                         │
├─────────────────────────────────────────────────────────┤
│  - 加载预训练模型 (microsoft/Florence-2-base)            │
│  - 图像预处理                                             │
│  - 推理生成物体描述                                       │
│  - 中英文翻译(可选)                                     │
└─────────────────────────────────────────────────────────┘

🔧 实施步骤

阶段一:后端 API 开发(第1-2天)

1.1 环境准备

服务器要求:

  • CPU: 8核或以上(如果使用 GPU,CPU 要求可降低)
  • 内存: 16GB 或以上
  • GPU: NVIDIA GPU(推荐,支持 CUDA 11.8+)
    • 显存: 4GB 或以上(推荐 8GB)
    • 驱动: 最新版 NVIDIA 驱动
  • 存储: 10GB 可用空间
  • Python: 3.9+

安装依赖:

步骤 1:检查 GPU 是否可用

# 检查 NVIDIA 驱动
nvidia-smi

# 查看 CUDA 版本
nvcc --version

步骤 2:安装 Python 依赖

# 创建 Python 虚拟环境
cd /var/www/ai-chatbot-backend
python3 -m venv venv
source venv/bin/activate

# 安装核心依赖
pip install fastapi uvicorn python-multipart

# 🚀 GPU 加速版本(推荐,如果有 NVIDIA GPU)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

# 或者使用国内镜像(更快)
pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple

# CPU 版本(如果没有 GPU)
# pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu

# 其他依赖
pip install transformers pillow requests
pip install openai  # 用于调用翻译 API(可选)

步骤 3:验证 GPU 可用性

python -c "import torch; print(f'CUDA 可用: {torch.cuda.is_available()}'); print(f'GPU 数量: {torch.cuda.device_count()}'); print(f'GPU 名称: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"N/A\"}')"

预期输出(如果 GPU 可用):

CUDA 可用: True
GPU 数量: 1
GPU 名称: NVIDIA GeForce RTX 3090

1.2 下载 Florence-2 模型

方式一:自动下载(推荐)

from transformers import AutoProcessor, AutoModelForCausalLM

# 首次运行会自动下载模型(约 230MB)
model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Florence-2-base",
    trust_remote_code=True
)
processor = AutoProcessor.from_pretrained(
    "microsoft/Florence-2-base",
    trust_remote_code=True
)

方式二:手动下载(网络受限时)

# 使用魔搭社区镜像(国内访问更快)
pip install modelscope
modelscope download --model=AI-ModelScope/Florence-2-base --local_dir ./models/florence-2

1.3 创建 API 服务

文件结构:

/var/www/ai-chatbot-backend/
├── app/
│   ├── api/
│   │   └── detect_object.py      # 物体检测 API
│   ├── services/
│   │   └── florence_service.py   # Florence-2 服务封装
│   ├── utils/
│   │   ├── image_utils.py        # 图片处理工具
│   │   └── translation.py        # 中英文翻译
│   └── main.py                   # FastAPI 主入口
├── models/                        # 模型文件目录
├── requirements.txt
└── start.sh                       # 启动脚本

核心代码实现(详见下文"代码实现"章节)

1.4 API 端点设计

POST /api/detect-object

请求参数:

{
  "imageUrl": string,      // 图片 URL(S3 地址)
  "clickX": number,        // 点击 X 坐标(像素)
  "clickY": number,        // 点击 Y 坐标(像素)
  "imageIndex": number,    // 图片序号(从 1 开始)
  "imageWidth": number,    // 图片原始宽度
  "imageHeight": number    // 图片原始高度
}

响应格式:

{
  "success": boolean,
  "data": {
    "description": string,     // 物体描述(中文):"绿色皮革手提包"
    "descriptionEn": string,   // 物体描述(英文):"green leather handbag"
    "confidence": number,      // 置信度(0-1)
    "imageIndex": number,      // 图片序号
    "formattedText": string    // 格式化文本:"📍图1: 绿色皮革手提包"
  },
  "error": string | null
}

错误响应:

{
  "success": false,
  "data": null,
  "error": "无法识别该区域的物体,请点击更明显的物体位置"
}

阶段二:前端组件开发(第3-4天)

2.1 修改 ImagePreview 组件

文件:components/image-preview.tsx

新增功能:

  1. 添加"定位标注"模式切换按钮
  2. 在标注模式下,鼠标悬停显示准星光标
  3. 点击时发送请求到后端 API
  4. 显示加载状态和识别结果
  5. 将结果传递给父组件

UI 设计:

┌──────────────────────────────────────────┐
│  ← 返回    📍 定位标注    ⬇️ 下载         │  ← 顶部工具栏
├──────────────────────────────────────────┤
│                                          │
│                                          │
│          [图片显示区域]                   │
│      (标注模式下显示准星)                │
│                                          │
│                                          │
├──────────────────────────────────────────┤
│  识别中... 🔍                             │  ← 底部状态栏(可选)
└──────────────────────────────────────────┘

2.2 修改 MultimodalInput 组件

文件:components/multimodal-input.tsx

新增功能:

  1. 接收来自 ImagePreview 的标注结果
  2. 将标注文本插入到输入框中
  3. 使用特殊样式显示标注文本(蓝色、带图标)
  4. 支持多次标注,文本自动追加

输入框样式:

┌──────────────────────────────────────────────────────────┐
│  将 [📍图1: 绿色手提包] 替换成 [📍图2: 红色背包]           │
│      ↑ 蓝色、可点击       ↑ 蓝色、可点击                  │
└──────────────────────────────────────────────────────────┘

2.3 创建标注文本组件

新建文件:components/annotation-tag.tsx

功能:

  • 显示标注标签(如 📍图1: 绿色手提包)
  • 支持点击删除
  • 特殊样式(蓝色背景、圆角、图标)

样式示例:

<span className="inline-flex items-center gap-1 px-2 py-1 bg-blue-500/10 text-blue-600 rounded-md">
  <span>📍图1: 绿色手提包</span>
  <button onClick={onRemove}>×</button>
</span>

阶段三:集成与测试(第5天)

3.1 本地开发测试

启动后端:

cd /var/www/ai-chatbot-backend
source venv/bin/activate
python app/main.py
# 访问 http://localhost:8001/docs 查看 API 文档

启动前端:

cd /var/www/ai-chatbot
npm run dev
# 访问 http://localhost:3000

测试流程:

  1. 上传测试图片(包含明显物体)
  2. 点击"定位标注"按钮
  3. 点击图片中的物体
  4. 验证识别结果是否准确
  5. 验证文本是否正确插入输入框

3.2 性能测试

测试指标:

  • 单次识别耗时:< 1秒
  • 并发请求数:10 QPS
  • 内存占用:< 2GB

测试命令:

# 使用 Apache Bench 测试
ab -n 100 -c 10 -p test_data.json -T application/json \
   http://localhost:8001/api/detect-object

3.3 边缘情况测试

测试场景:

  1. 点击图片空白区域(无物体)
  2. 点击模糊不清的区域
  3. 上传黑白图片
  4. 上传极小/极大图片
  5. 快速连续点击多次

预期处理:

  • 返回友好的错误提示
  • 不崩溃、不卡死
  • 有重试机制

阶段四:生产部署(第6天)

4.1 后端部署

使用 PM2 管理进程:

# 安装 PM2
npm install -g pm2

# 创建 PM2 配置文件
cat > ecosystem.config.js << EOF
module.exports = {
  apps: [{
    name: 'florence-api',
    script: '/var/www/ai-chatbot-backend/venv/bin/python',
    args: '/var/www/ai-chatbot-backend/app/main.py',
    instances: 1,
    exec_mode: 'fork',
    max_memory_restart: '2G',
    env: {
      PORT: 8001,
      MODEL_PATH: '/var/www/ai-chatbot-backend/models/florence-2'
    }
  }]
}
EOF

# 启动服务
pm2 start ecosystem.config.js
pm2 save

Nginx 反向代理:

# /etc/nginx/sites-available/ai-chatbot

# 新增后端 API 代理
location /api/detect-object {
    proxy_pass http://127.0.0.1:8001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
    
    # 增加超时时间(因为 AI 推理需要时间)
    proxy_connect_timeout 10s;
    proxy_send_timeout 10s;
    proxy_read_timeout 10s;
}

4.2 前端部署

构建生产版本:

cd /var/www/ai-chatbot
npm run build
pm2 restart ai-chatbot

4.3 监控与日志

设置日志轮转:

# 查看 Florence API 日志
pm2 logs florence-api

# 设置日志文件大小限制
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 100M
pm2 set pm2-logrotate:retain 7

监控指标:

  • API 响应时间
  • 错误率
  • 内存/CPU 使用率
  • 请求 QPS

💻 代码实现

1. 后端 API 实现

app/main.py - FastAPI 主入口

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn

from app.services.florence_service import FlorenceService

app = FastAPI(title="Florence-2 物体检测 API")

# CORS 配置
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://ai.yydd.ai", "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 初始化 Florence 服务(全局单例)
florence_service = FlorenceService()

class DetectRequest(BaseModel):
    imageUrl: str
    clickX: int
    clickY: int
    imageIndex: int
    imageWidth: int
    imageHeight: int

class DetectResponse(BaseModel):
    success: bool
    data: dict | None
    error: str | None

@app.on_event("startup")
async def startup_event():
    """启动时加载模型"""
    print("🚀 正在加载 Florence-2 模型...")
    await florence_service.load_model()
    print("✅ 模型加载完成!")

@app.post("/api/detect-object", response_model=DetectResponse)
async def detect_object(request: DetectRequest):
    """检测图片中点击位置的物体"""
    try:
        result = await florence_service.detect_object_at_point(
            image_url=request.imageUrl,
            click_x=request.clickX,
            click_y=request.clickY,
            image_index=request.imageIndex,
            image_width=request.imageWidth,
            image_height=request.imageHeight
        )
        
        return DetectResponse(
            success=True,
            data=result,
            error=None
        )
    
    except Exception as e:
        print(f"❌ 检测失败: {str(e)}")
        return DetectResponse(
            success=False,
            data=None,
            error=f"物体识别失败: {str(e)}"
        )

@app.get("/health")
async def health_check():
    """健康检查"""
    return {"status": "ok", "model_loaded": florence_service.is_loaded}

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8001)

app/services/florence_service.py - Florence-2 服务

import torch
from transformers import AutoProcessor, AutoModelForCausalLM
from PIL import Image
import requests
from io import BytesIO
import asyncio

class FlorenceService:
    def __init__(self):
        self.model = None
        self.processor = None
        self.is_loaded = False
        
        # 🚀 自动检测并使用 GPU(如果可用)
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        print(f"🎯 使用设备: {self.device}")
        
        if self.device == "cuda":
            print(f"🚀 GPU 加速已启用: {torch.cuda.get_device_name(0)}")
            print(f"💾 GPU 显存: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB")
        
    async def load_model(self):
        """异步加载模型"""
        if self.is_loaded:
            return
            
        loop = asyncio.get_event_loop()
        await loop.run_in_executor(None, self._load_model_sync)
        
    def _load_model_sync(self):
        """同步加载模型(在线程池中执行)"""
        print("📦 下载 Florence-2 模型...")
        self.model = AutoModelForCausalLM.from_pretrained(
            "microsoft/Florence-2-base",
            trust_remote_code=True
        ).to(self.device)
        
        self.processor = AutoProcessor.from_pretrained(
            "microsoft/Florence-2-base",
            trust_remote_code=True
        )
        
        self.is_loaded = True
        print("✅ Florence-2 模型加载完成")
        
    async def detect_object_at_point(
        self, 
        image_url: str, 
        click_x: int, 
        click_y: int,
        image_index: int,
        image_width: int,
        image_height: int
    ) -> dict:
        """检测点击位置的物体"""
        
        # 1. 下载图片
        print(f"📥 下载图片: {image_url}")
        response = requests.get(image_url, timeout=10)
        image = Image.open(BytesIO(response.content)).convert("RGB")
        
        # 2. 计算裁剪区域(以点击点为中心,200x200 像素)
        crop_size = 200
        left = max(0, click_x - crop_size // 2)
        top = max(0, click_y - crop_size // 2)
        right = min(image_width, click_x + crop_size // 2)
        bottom = min(image_height, click_y + crop_size // 2)
        
        print(f"✂️ 裁剪区域: ({left}, {top}, {right}, {bottom})")
        cropped_image = image.crop((left, top, right, bottom))
        
        # 3. 使用 Florence-2 识别物体
        print("🔍 正在识别物体...")
        loop = asyncio.get_event_loop()
        description = await loop.run_in_executor(
            None, 
            self._detect_sync, 
            cropped_image
        )
        
        # 4. 翻译成中文(如果是英文)
        description_cn = await self._translate_to_chinese(description)
        
        # 5. 格式化返回结果
        formatted_text = f"📍图{image_index}: {description_cn}"
        
        return {
            "description": description_cn,
            "descriptionEn": description,
            "confidence": 0.95,  # Florence-2 不返回置信度,固定值
            "imageIndex": image_index,
            "formattedText": formatted_text
        }
        
    def _detect_sync(self, image: Image.Image) -> str:
        """同步推理(在线程池中执行)"""
        # 使用 <CAPTION> 任务生成简短描述
        prompt = "<CAPTION>"
        
        inputs = self.processor(
            text=prompt, 
            images=image, 
            return_tensors="pt"
        ).to(self.device)
        
        # 生成描述
        with torch.no_grad():
            generated_ids = self.model.generate(
                input_ids=inputs["input_ids"],
                pixel_values=inputs["pixel_values"],
                max_new_tokens=50,
                num_beams=3,
                do_sample=False
            )
        
        # 解码结果
        generated_text = self.processor.batch_decode(
            generated_ids, 
            skip_special_tokens=False
        )[0]
        
        # 提取描述(去除提示词)
        description = generated_text.replace(prompt, "").strip()
        
        print(f"✅ 识别结果: {description}")
        return description
        
    async def _translate_to_chinese(self, text: str) -> str:
        """翻译成中文(简单的关键词映射)"""
        # 方案1: 简单的关键词映射(快速、离线)
        keyword_map = {
            "red": "红色",
            "green": "绿色",
            "blue": "蓝色",
            "yellow": "黄色",
            "black": "黑色",
            "white": "白色",
            "brown": "棕色",
            "pink": "粉色",
            "purple": "紫色",
            "orange": "橙色",
            "gray": "灰色",
            "handbag": "手提包",
            "backpack": "背包",
            "bag": "包",
            "shoe": "鞋子",
            "hat": "帽子",
            "glasses": "眼镜",
            "chair": "椅子",
            "table": "桌子",
            "sofa": "沙发",
            "bed": "床",
            "person": "人",
            "car": "汽车",
            "cat": "猫",
            "dog": "狗",
            "leather": "皮革",
            "cloth": "布料",
            "plastic": "塑料",
            "metal": "金属",
            "wooden": "木制",
        }
        
        # 简单替换
        result = text.lower()
        for en, cn in keyword_map.items():
            result = result.replace(en, cn)
        
        # 如果没有翻译成功,返回原文
        if result == text.lower():
            return text
            
        return result
        
        # 方案2: 调用翻译 API(可选,需要额外配置)
        # from app.utils.translation import translate
        # return await translate(text, target_lang="zh")

requirements.txt - Python 依赖

fastapi==0.109.0
uvicorn[standard]==0.27.0
python-multipart==0.0.6
pydantic==2.5.3
torch==2.1.2
torchvision==0.16.2
transformers==4.37.0
pillow==10.2.0
requests==2.31.0

2. 前端组件实现

components/image-preview.tsx - 修改

"use client";

import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { X, Download, Target } from "lucide-react";
import Image from "next/image";
import { useState } from "react";
import { toast } from "sonner";

interface ImagePreviewProps {
  isOpen: boolean;
  imageUrl: string;
  imageIndex: number;
  onClose: () => void;
  onAnnotate?: (annotation: {
    imageIndex: number;
    description: string;
    formattedText: string;
  }) => void;
}

export function ImagePreview({
  isOpen,
  imageUrl,
  imageIndex,
  onClose,
  onAnnotate,
}: ImagePreviewProps) {
  const [isAnnotationMode, setIsAnnotationMode] = useState(false);
  const [isDetecting, setIsDetecting] = useState(false);

  const handleImageClick = async (e: React.MouseEvent<HTMLDivElement>) => {
    if (!isAnnotationMode || isDetecting) return;

    const rect = e.currentTarget.getBoundingClientRect();
    const clickX = Math.round(e.clientX - rect.left);
    const clickY = Math.round(e.clientY - rect.top);

    setIsDetecting(true);
    toast.info("正在识别物体...");

    try {
      const response = await fetch("/api/detect-object", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          imageUrl,
          clickX,
          clickY,
          imageIndex,
          imageWidth: rect.width,
          imageHeight: rect.height,
        }),
      });

      const result = await response.json();

      if (result.success && result.data) {
        toast.success(`识别成功:${result.data.description}`);
        onAnnotate?.({
          imageIndex: result.data.imageIndex,
          description: result.data.description,
          formattedText: result.data.formattedText,
        });
        onClose();
      } else {
        toast.error(result.error || "识别失败,请重试");
      }
    } catch (error) {
      console.error("物体检测失败:", error);
      toast.error("识别失败,请检查网络连接");
    } finally {
      setIsDetecting(false);
    }
  };

  const handleDownload = () => {
    const link = document.createElement("a");
    link.href = imageUrl;
    link.download = `image-${imageIndex}.png`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  };

  return (
    <Dialog open={isOpen} onOpenChange={onClose}>
      <DialogContent className="max-w-4xl max-h-[90vh] p-0">
        {/* 顶部工具栏 */}
        <div className="flex items-center justify-between p-4 border-b">
          <Button
            variant="ghost"
            size="sm"
            onClick={onClose}
            className="gap-2"
          >
            <X className="h-4 w-4" />
            关闭
          </Button>

          <div className="flex items-center gap-2">
            <Button
              variant={isAnnotationMode ? "default" : "outline"}
              size="sm"
              onClick={() => setIsAnnotationMode(!isAnnotationMode)}
              className="gap-2"
              disabled={isDetecting}
            >
              <Target className="h-4 w-4" />
              {isAnnotationMode ? "标注中..." : "定位标注"}
            </Button>

            <Button
              variant="outline"
              size="sm"
              onClick={handleDownload}
              className="gap-2"
            >
              <Download className="h-4 w-4" />
              下载
            </Button>
          </div>
        </div>

        {/* 图片显示区域 */}
        <div
          className={`relative flex items-center justify-center p-4 ${
            isAnnotationMode ? "cursor-crosshair" : ""
          } ${isDetecting ? "opacity-50 pointer-events-none" : ""}`}
          onClick={handleImageClick}
        >
          <Image
            src={imageUrl}
            alt={`图片 ${imageIndex}`}
            width={800}
            height={600}
            className="max-w-full max-h-[calc(90vh-120px)] object-contain"
            unoptimized
          />

          {isAnnotationMode && (
            <div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-blue-500 text-white px-4 py-2 rounded-full text-sm">
              点击图片中的物体进行标注
            </div>
          )}

          {isDetecting && (
            <div className="absolute inset-0 flex items-center justify-center">
              <div className="bg-black/70 text-white px-6 py-3 rounded-lg flex items-center gap-3">
                <div className="animate-spin h-5 w-5 border-2 border-white border-t-transparent rounded-full" />
                <span>正在识别...</span>
              </div>
            </div>
          )}
        </div>
      </DialogContent>
    </Dialog>
  );
}

components/multimodal-input.tsx - 修改(部分)

在现有的 MultimodalInput 组件中添加:

// 在组件顶部添加状态
const [annotations, setAnnotations] = useState<Array<{
  imageIndex: number;
  description: string;
  formattedText: string;
}>>([]);

// 添加标注处理函数
const handleAnnotate = (annotation: {
  imageIndex: number;
  description: string;
  formattedText: string;
}) => {
  setAnnotations((prev) => [...prev, annotation]);
  
  // 自动插入到输入框
  setInput((prev) => {
    if (prev) {
      return `${prev} ${annotation.formattedText}`;
    }
    return annotation.formattedText;
  });
};

// 在 PreviewAttachment 组件调用时传递 onAnnotate
<PreviewAttachment
  key={attachment.url}
  attachment={attachment}
  onRemove={() => handleRemoveAttachment(attachment.url)}
  onAnnotate={handleAnnotate}  // 新增
  imageIndex={index + 1}         // 新增
/>

components/preview-attachment.tsx - 修改

// 添加新的 props
interface PreviewAttachmentProps {
  attachment: Attachment;
  onRemove: () => void;
  onAnnotate?: (annotation: any) => void;  // 新增
  imageIndex?: number;                      // 新增
  // ... 其他 props
}

// 在图片点击处理中打开预览
const handleImageClick = () => {
  setShowPreview(true);
};

// 渲染 ImagePreview
{showPreview && (
  <ImagePreview
    isOpen={showPreview}
    imageUrl={attachment.url}
    imageIndex={imageIndex || 1}
    onClose={() => setShowPreview(false)}
    onAnnotate={onAnnotate}
  />
)}

🚀 部署脚本

deploy.sh - 一键部署脚本

#!/bin/bash

echo "🚀 开始部署 Florence-2 物体检测系统..."

# 1. 部署后端
echo "📦 部署后端 API..."
cd /var/www/ai-chatbot-backend

# 激活虚拟环境
source venv/bin/activate

# 安装/更新依赖
pip install -r requirements.txt

# 下载模型(如果未下载)
python -c "from app.services.florence_service import FlorenceService; s = FlorenceService(); s._load_model_sync()"

# 重启服务
pm2 restart florence-api || pm2 start ecosystem.config.js

echo "✅ 后端部署完成"

# 2. 部署前端
echo "📦 部署前端..."
cd /var/www/ai-chatbot

# 构建
npm run build

# 重启服务
pm2 restart ai-chatbot

echo "✅ 前端部署完成"

# 3. 重启 Nginx
echo "🔄 重启 Nginx..."
sudo nginx -t && sudo systemctl reload nginx

echo ""
echo "🎉 部署完成!"
echo ""
echo "📝 检查服务状态:"
echo "   pm2 status"
echo ""
echo "📊 查看日志:"
echo "   pm2 logs florence-api"
echo "   pm2 logs ai-chatbot"
echo ""
echo "🌐 访问地址:"
echo "   https://ai.yydd.ai"
echo ""

🧪 测试计划

功能测试

测试项 测试步骤 预期结果
基础识别 上传包含明显物体的图片,点击物体 正确识别物体名称和颜色
多图标注 上传多张图片,分别点击标注 每张图片的标注独立,序号正确
颜色识别 点击不同颜色的同类物体 能区分颜色(如红色包、绿色包)
空白区域 点击图片空白处 提示"无法识别物体"
模糊图片 上传模糊图片并点击 尽可能识别,或提示图片质量问题
快速点击 连续快速点击多次 不重复识别,有防抖机制

性能测试

指标 目标值 测试方法
单次推理时间 < 1秒 使用计时器测量
并发处理能力 10 QPS Apache Bench 压测
内存占用 < 2GB top 命令监控
错误率 < 1% 压测统计

🚀 GPU 加速配置(推荐)

如果您的服务器有 NVIDIA GPU,强烈建议启用 GPU 加速,可以将推理速度从 0.5-1 秒降低到 0.1-0.3 秒!

1. 检查 GPU 状态

# 检查 GPU 信息
nvidia-smi

# 输出示例:
# +-----------------------------------------------------------------------------+
# | NVIDIA-SMI 525.125.06   Driver Version: 525.125.06   CUDA Version: 12.0     |
# |-------------------------------+----------------------+----------------------+
# | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
# |   0  NVIDIA RTX 3090     Off  | 00000000:01:00.0 Off |                  N/A |
# +-----------------------------------------------------------------------------+

2. 安装 CUDA 版本的 PyTorch

# 进入虚拟环境
cd /var/www/ai-chatbot-backend
source venv/bin/activate

# 安装 CUDA 版本(根据您的 CUDA 版本选择)
# CUDA 11.8
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

# CUDA 12.1
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121

# 或使用国内镜像(更快)
pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simple

3. 验证 GPU 可用性

python << EOF
import torch
print(f"PyTorch 版本: {torch.__version__}")
print(f"CUDA 可用: {torch.cuda.is_available()}")
print(f"CUDA 版本: {torch.version.cuda}")
print(f"GPU 数量: {torch.cuda.device_count()}")
if torch.cuda.is_available():
    print(f"GPU 名称: {torch.cuda.get_device_name(0)}")
    print(f"GPU 显存: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB")
EOF

4. 性能对比

设备 推理时间 吞吐量 内存占用
CPU(8核) 0.5-1秒 1-2 QPS 1.5GB
GPU(RTX 3090) 0.1-0.2秒 5-10 QPS 2GB(显存)
GPU(T4) 0.2-0.3秒 3-5 QPS 2GB(显存)
GPU(V100) 0.15-0.25秒 4-7 QPS 2GB(显存)

💡 推荐配置:RTX 3060 或以上,显存 ≥ 6GB

5. GPU 监控

# 实时监控 GPU 使用率
watch -n 1 nvidia-smi

# 查看 GPU 进程
nvidia-smi pmon

# 记录 GPU 使用日志
nvidia-smi dmon -s pucvmet -f gpu_usage.log

6. 故障排查

问题 1:torch.cuda.is_available() 返回 False

解决方案:

# 1. 检查 NVIDIA 驱动
nvidia-smi
# 如果报错,需要重新安装驱动

# 2. 检查 CUDA 工具包
nvcc --version
# 如果找不到命令,需要安装 CUDA Toolkit

# 3. 重新安装 PyTorch(确保版本匹配)
pip uninstall torch torchvision
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

问题 2:显存不足(CUDA out of memory)

解决方案:

# 方法1: 减小批处理大小(在代码中已经是单张推理,无需调整)

# 方法2: 清空 GPU 缓存
import torch
torch.cuda.empty_cache()

# 方法3: 使用半精度推理(FP16)
model = model.half()  # 转换为 FP16

问题 3:推理速度慢(GPU 利用率低)

原因:首次推理会有模型编译开销

解决方案:

# 在启动时预热模型
@app.on_event("startup")
async def warmup():
    print("🔥 正在预热模型...")
    dummy_image = Image.new("RGB", (224, 224), color="red")
    await florence_service.detect_object_at_point(
        image_url="dummy",
        click_x=112,
        click_y=112,
        image_index=1,
        image_width=224,
        image_height=224
    )
    print("✅ 模型预热完成")

⚠️ 注意事项

1. 模型准确度限制

Florence-2 是通用视觉模型,可能存在以下情况:

  • 小物体或远景物体识别不准确
  • 罕见物体可能无法识别
  • 颜色描述可能不够精确(如"深蓝"和"浅蓝")

解决方案:

  • 引导用户点击明显、清晰的物体
  • 提供"重新识别"按钮
  • 允许用户手动编辑识别结果

2. 性能优化

模型缓存:

  • 模型加载到内存后常驻,避免重复加载
  • 使用全局单例模式

图片缓存:

  • 对于同一张图片的多次点击,缓存下载的图片
  • 使用 LRU 缓存策略,限制内存占用

并发控制:

  • 使用队列限制并发推理数量
  • 避免 OOM(内存溢出)

3. 用户体验优化

加载状态:

  • 首次使用时提示"模型加载中"
  • 识别过程中显示加载动画
  • 预估等待时间(约 0.5-1秒)

错误处理:

  • 网络错误:提示"网络连接失败,请重试"
  • 识别失败:提示"无法识别该区域,请点击更明显的位置"
  • 服务异常:提示"服务暂时不可用,请稍后再试"

📊 项目进度表

阶段 任务 预计时间 负责人
阶段一 后端 API 开发 2天 后端开发
阶段二 前端组件开发 2天 前端开发
阶段三 集成与测试 1天 全栈
阶段四 生产部署 1天 运维
总计 - 6天 -

🔮 未来扩展

1. 支持更多识别任务

  • OCR 文字识别:识别图片中的文字
  • 区域分割:精确标注物体边界
  • 多物体标注:一次点击识别多个物体

2. 用户反馈机制

  • 允许用户纠正错误的识别结果
  • 收集用户反馈,优化模型

3. 自定义类别

  • 允许用户上传自己的物体类别
  • 支持小样本学习(Few-shot Learning)

📚 参考资料

  1. Florence-2 官方论文
  2. Hugging Face - Florence-2 文档
  3. FastAPI 官方文档
  4. Next.js 官方文档

📞 技术支持

如有问题,请联系开发团队。

文档版本: v1.0
更新日期: 2025-12-15
作者: AI Chatbot 开发团队