1241 lines
35 KiB
Markdown
1241 lines
35 KiB
Markdown
# 图片物体智能标注系统 - 实施文档
|
||
|
||
## 📋 项目概述
|
||
|
||
### 功能描述
|
||
用户在对话框中上传图片后,可以通过点击图片预览中的"定位标注"按钮,点击图片中的任意物体,系统自动识别该物体的名称(包括颜色、材质等详细信息),并将识别结果以特殊格式自动填入对话框。
|
||
|
||
### 使用场景示例
|
||
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 是否可用**
|
||
```bash
|
||
# 检查 NVIDIA 驱动
|
||
nvidia-smi
|
||
|
||
# 查看 CUDA 版本
|
||
nvcc --version
|
||
```
|
||
|
||
**步骤 2:安装 Python 依赖**
|
||
```bash
|
||
# 创建 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 可用性**
|
||
```bash
|
||
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 模型
|
||
|
||
**方式一:自动下载(推荐)**
|
||
```python
|
||
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
|
||
)
|
||
```
|
||
|
||
**方式二:手动下载(网络受限时)**
|
||
```bash
|
||
# 使用魔搭社区镜像(国内访问更快)
|
||
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**
|
||
|
||
**请求参数**:
|
||
```typescript
|
||
{
|
||
"imageUrl": string, // 图片 URL(S3 地址)
|
||
"clickX": number, // 点击 X 坐标(像素)
|
||
"clickY": number, // 点击 Y 坐标(像素)
|
||
"imageIndex": number, // 图片序号(从 1 开始)
|
||
"imageWidth": number, // 图片原始宽度
|
||
"imageHeight": number // 图片原始高度
|
||
}
|
||
```
|
||
|
||
**响应格式**:
|
||
```typescript
|
||
{
|
||
"success": boolean,
|
||
"data": {
|
||
"description": string, // 物体描述(中文):"绿色皮革手提包"
|
||
"descriptionEn": string, // 物体描述(英文):"green leather handbag"
|
||
"confidence": number, // 置信度(0-1)
|
||
"imageIndex": number, // 图片序号
|
||
"formattedText": string // 格式化文本:"📍图1: 绿色皮革手提包"
|
||
},
|
||
"error": string | null
|
||
}
|
||
```
|
||
|
||
**错误响应**:
|
||
```typescript
|
||
{
|
||
"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: 绿色手提包`)
|
||
- 支持点击删除
|
||
- 特殊样式(蓝色背景、圆角、图标)
|
||
|
||
**样式示例**:
|
||
```tsx
|
||
<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 本地开发测试
|
||
|
||
**启动后端**:
|
||
```bash
|
||
cd /var/www/ai-chatbot-backend
|
||
source venv/bin/activate
|
||
python app/main.py
|
||
# 访问 http://localhost:8001/docs 查看 API 文档
|
||
```
|
||
|
||
**启动前端**:
|
||
```bash
|
||
cd /var/www/ai-chatbot
|
||
npm run dev
|
||
# 访问 http://localhost:3000
|
||
```
|
||
|
||
**测试流程**:
|
||
1. 上传测试图片(包含明显物体)
|
||
2. 点击"定位标注"按钮
|
||
3. 点击图片中的物体
|
||
4. 验证识别结果是否准确
|
||
5. 验证文本是否正确插入输入框
|
||
|
||
#### 3.2 性能测试
|
||
|
||
**测试指标**:
|
||
- 单次识别耗时:< 1秒
|
||
- 并发请求数:10 QPS
|
||
- 内存占用:< 2GB
|
||
|
||
**测试命令**:
|
||
```bash
|
||
# 使用 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 管理进程**:
|
||
```bash
|
||
# 安装 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 反向代理**:
|
||
```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 前端部署
|
||
|
||
**构建生产版本**:
|
||
```bash
|
||
cd /var/www/ai-chatbot
|
||
npm run build
|
||
pm2 restart ai-chatbot
|
||
```
|
||
|
||
#### 4.3 监控与日志
|
||
|
||
**设置日志轮转**:
|
||
```bash
|
||
# 查看 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 主入口
|
||
|
||
```python
|
||
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 服务
|
||
|
||
```python
|
||
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 依赖
|
||
|
||
```txt
|
||
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` - 修改
|
||
|
||
```typescript
|
||
"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` 组件中添加:
|
||
|
||
```typescript
|
||
// 在组件顶部添加状态
|
||
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` - 修改
|
||
|
||
```typescript
|
||
// 添加新的 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` - 一键部署脚本
|
||
|
||
```bash
|
||
#!/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 状态
|
||
|
||
```bash
|
||
# 检查 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
|
||
|
||
```bash
|
||
# 进入虚拟环境
|
||
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 可用性
|
||
|
||
```bash
|
||
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 监控
|
||
|
||
```bash
|
||
# 实时监控 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
|
||
|
||
**解决方案**:
|
||
```bash
|
||
# 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)
|
||
|
||
**解决方案**:
|
||
```python
|
||
# 方法1: 减小批处理大小(在代码中已经是单张推理,无需调整)
|
||
|
||
# 方法2: 清空 GPU 缓存
|
||
import torch
|
||
torch.cuda.empty_cache()
|
||
|
||
# 方法3: 使用半精度推理(FP16)
|
||
model = model.half() # 转换为 FP16
|
||
```
|
||
|
||
#### 问题 3:推理速度慢(GPU 利用率低)
|
||
|
||
**原因**:首次推理会有模型编译开销
|
||
|
||
**解决方案**:
|
||
```python
|
||
# 在启动时预热模型
|
||
@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 官方论文](https://arxiv.org/abs/2311.06242)
|
||
2. [Hugging Face - Florence-2 文档](https://huggingface.co/microsoft/Florence-2-base)
|
||
3. [FastAPI 官方文档](https://fastapi.tiangolo.com/)
|
||
4. [Next.js 官方文档](https://nextjs.org/docs)
|
||
|
||
---
|
||
|
||
## 📞 技术支持
|
||
|
||
如有问题,请联系开发团队。
|
||
|
||
**文档版本**: v1.0
|
||
**更新日期**: 2025-12-15
|
||
**作者**: AI Chatbot 开发团队
|
||
|