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

298 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import torch
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
from PIL import Image, ImageDraw
import requests
from io import BytesIO
import asyncio
import time
import os
class QwenVLService:
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")
print(f"📊 GPU 数量: {torch.cuda.device_count()}")
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("🚀 正在加载 Qwen2-VL-2B 模型...")
# 直接使用 HuggingFace(通过 HF_ENDPOINT 环境变量使用镜像)
model_path = "Qwen/Qwen2-VL-2B-Instruct"
print(f"📦 从 HuggingFace 下载模型: {model_path}")
print(f"🌐 使用镜像: {os.environ.get('HF_ENDPOINT', 'https://huggingface.co')}")
print(f"💾 缓存目录: {os.environ.get('HF_HOME', './models')}")
# 加载模型(会自动从 HF_ENDPOINT 下载)
print(f"🔄 正在下载并加载模型到 {self.device}...")
self.model = Qwen2VLForConditionalGeneration.from_pretrained(
model_path,
torch_dtype=torch.float16 if self.device == "cuda" else torch.float32,
device_map="auto" if self.device == "cuda" else None,
trust_remote_code=True
)
if self.device == "cpu":
self.model = self.model.to(self.device)
# 加载处理器
print("🔄 正在加载处理器...")
self.processor = AutoProcessor.from_pretrained(
model_path,
trust_remote_code=True
)
self.is_loaded = True
print("✅ Qwen2-VL-2B 模型加载完成!")
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:
"""使用Qwen2-VL检测点击位置的物体"""
start_time = time.time()
# 1. 下载图片
print(f"📥 下载图片: {image_url}")
response = requests.get(image_url, timeout=10)
image = Image.open(BytesIO(response.content)).convert("RGB")
actual_width, actual_height = image.size
print(f"📊 原始图片: {actual_width}x{actual_height}")
# 缩放大图以加快推理速度(保持宽高比)
max_size = 768 # 降低到768以加快推理(从1024)
if max(actual_width, actual_height) > max_size:
scale = max_size / max(actual_width, actual_height)
new_width = int(actual_width * scale)
new_height = int(actual_height * scale)
# 同步缩放坐标
click_x = int(click_x * scale)
click_y = int(click_y * scale)
image = image.resize((new_width, new_height), Image.LANCZOS)
print(f"⚡ 已缩放至: {new_width}x{new_height} (加速推理)")
print(f"📍 标注坐标: ({click_x}, {click_y})")
# 2. 在图片上绘制标注点
marked_image = image.copy()
draw = ImageDraw.Draw(marked_image)
# 绘制超级明显的红色标记(放大3倍)
marker_size = 50 # 从30增加到50
line_width = 10 # 从6增加到10
# 绘制3层嵌套圆圈,更明显
for i in range(3):
size = marker_size - i * 10
draw.ellipse(
[click_x - size, click_y - size,
click_x + size, click_y + size],
outline=(255, 0, 0),
width=line_width
)
# 绘制十字线
draw.line(
[click_x - marker_size - 20, click_y, click_x + marker_size + 20, click_y],
fill=(255, 0, 0),
width=line_width
)
draw.line(
[click_x, click_y - marker_size - 20, click_x, click_y + marker_size + 20],
fill=(255, 0, 0),
width=line_width
)
# 在圆心绘制实心点
center_size = 8
draw.ellipse(
[click_x - center_size, click_y - center_size,
click_x + center_size, click_y + center_size],
fill=(255, 0, 0)
)
print(f"✏️ 已在图片上绘制标注点")
# 3. 构建对话式问答prompt
messages = [
{
"role": "user",
"content": [
{
"type": "image",
"image": marked_image,
},
{
"type": "text",
"text": "看图中红色圆圈的正中心位置,那里有一个物体。只用2-4个词描述这个被圆圈中心点标记的物体(格式:颜色+材质/物品,如:白色沙发、红色毛衣)。只说这一个物体,不要其他。"
}
],
}
]
# 4. 准备输入
text = self.processor.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
image_inputs, video_inputs = process_vision_info(messages)
inputs = self.processor(
text=[text],
images=image_inputs,
videos=video_inputs,
padding=True,
return_tensors="pt",
)
# 移动到设备
inputs = inputs.to(self.device)
# 5. 生成回答
print("🔍 正在识别物体...")
inference_start = time.time()
loop = asyncio.get_event_loop()
description = await loop.run_in_executor(
None,
self._generate_sync,
inputs
)
inference_time = time.time() - inference_start
print(f"⚡ 推理耗时: {inference_time:.2f}秒")
# 6. 清理结果
description = self._clean_description(description)
# 7. 格式化返回结果
formatted_text = f"图{image_index}: {description}"
total_time = time.time() - start_time
print(f"✅ 识别完成: {description} (总耗时: {total_time:.2f}秒)")
return {
"description": description,
"descriptionEn": "", # Qwen2-VL直接输出中文
"confidence": 0.95,
"imageIndex": image_index,
"formattedText": formatted_text
}
def _generate_sync(self, inputs):
"""同步生成(在线程池中执行)"""
with torch.no_grad():
generated_ids = self.model.generate(
**inputs,
max_new_tokens=20, # 进一步减少生成长度加速(30→20)
do_sample=False,
temperature=0.0,
use_cache=True # 启用KV缓存加速
)
# 只解码生成的部分
generated_ids_trimmed = [
out_ids[len(in_ids):]
for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
]
output_text = self.processor.batch_decode(
generated_ids_trimmed,
skip_special_tokens=True,
clean_up_tokenization_spaces=False
)[0]
return output_text
def _clean_description(self, text: str) -> str:
"""清理描述文本,移除不需要的前缀和格式"""
import re
text = text.strip()
# 移除常见的前缀(Qwen2-VL可能生成的)
prefixes_to_remove = [
r"红色圆圈标记中心位置的物体是\s*",
r"红色圆圈标记中心位置的\s*",
r"标记中心位置的物体是\s*",
r"红色圆圈标记的物体是\s*",
r"红色圆圈标记的是\s*",
r"红色圆圈标记[::]\s*",
r"红色标记的物体是\s*",
r"红色标记[::]\s*",
r"标记的物体是\s*",
r"标记位置的物体是\s*",
r"标记位置[::]\s*",
r"标记的位置[::]\s*",
r"标记处[::]\s*",
r"标记中心位置的\s*",
r"中心位置的物体是\s*",
r"中心位置的\s*",
r"物体是\s*",
r"图片中[,,、]?\s*",
r"这是\s*",
r"这个是\s*",
r"标记的是\s*",
]
for prefix in prefixes_to_remove:
text = re.sub(f"^{prefix}", "", text, flags=re.IGNORECASE)
# 移除开头的标点符号
text = re.sub(r"^[::,,、。\s]+", "", text)
# 如果包含多个物品(用顿号、逗号分隔),只取第一个
if '、' in text:
text = text.split('、')[0]
elif ',' in text:
text = text.split(',')[0]
elif ',' in text:
text = text.split(',')[0]
# 限制长度(最多20个字,避免截断)
if len(text) > 20:
text = text[:20]
return text.strip()
# 全局单例
_qwen_service = None
def get_qwen_service() -> QwenVLService:
"""获取全局Qwen服务实例"""
global _qwen_service
if _qwen_service is None:
_qwen_service = QwenVLService()
return _qwen_service