『大模型部署』NVIDIA Orin + bnb量化 + Qwen3-VL | 4bit、8bit量化
本文介绍了如何使用BitsAndBytes库,在NVIDIA Orin上,对Qwen3-VL多模态模型进行量化部署。
BitsAndBytes是一个轻量级PyTorch库,支持8/4位量化技术,能将模型显存占用降低90%以上。
文章详细讲解了从环境搭建到模型量化、再到实际推理的全流程,包括:
- 在NVIDIA Orin上安装PyTorch环境和量化依赖库
- 使用LLaMA-Factory进行LoRA微调
- 合并基础模型和LoRA适配器权重
- 实施8位和4位量化(包含NF4/FP4量化类型选择)
- 提供多个应用案例:图像描述生成、指定任意物体检测等
量化后的模型在保持较高精度前提下,显存占用大幅降低(8位量化4.6G,4位量化2.7G),使得大模型能在边缘设备上高效运行。
文中还展示了量化模型在NVIDIA Orin上的实际推理效果和资源占用情况。
万字长文,需耐心观看~
目录
基础内容
1、推理环境搭建
2、Qwen3-VL+ NVIDIA Orin 推理——8bit量化版本
2.1对Qwen3-VL进行Lora 监督微调
2.2、合并权重
2.3、使用bnb进行8bit量化
2.4、8bit量化后的模型推理(实践案例1)
2.5、8bit量化后的模型推理(实践案例2)
3、Qwen3-VL+ NVIDIA Orin 推理——4bit量化版本
3.1、对Qwen3-VL进行Lora 监督微调
3.2、合并权重
3.3、使用bnb进行4bit量化
3.4、量化后的模型推理(实践案例)
基础内容
bnb量化,全称是bitsandbytes,为什么要用它?
bitsandbytes是一个轻量级 PyTorch 库,专为深度学习模型提供k-bit 量化技术,通过将模型权重和优化器状态从 32/16 位压缩至 8/4 位,显著减少内存占用 (最高 90%),同时保持几乎相同的模型性能。
核心特点:
- 降低硬件门槛:使普通显卡也能运行训练千亿参数大模型
- 无缝集成 PyTorch:仅需几行代码修改,不改变原有训练流程
- 支持多种量化策略:8-bit 优化器、LLM.int8 () 推理量化、4-bit QLoRA 微调
- 多硬件兼容:NVIDIA GPU、AMD ROCm、Intel XPU、Apple MPS 等
关键依赖库:
torch 2.6.0
torchaudio 2.6.0
torchtyping 0.1.5
torchvision 0.21.0
transformers 4.57.1
bitsandbytes 0.49.0.dev0基础依赖库:
scikit-image 0.24.0
scikit-learn 1.6.1
numpy 1.23.4
opencv-python 4.11.0.86
pillow 11.1.0
transforms3d 0.3.1
1、推理环境搭建
1)在NVIDIA Orin 中安装torch 2.6.0,参考我这篇博客:
NVIDIA Jetson 环境安装指导 PyTorch | Conda | cudnn
2)安装相关依赖库
编辑一个 requirements.txt,内容如下:
transformers==4.57.1
scikit-image==0.24.0
scikit-learn==1.6.1
numpy==1.23.4
opencv-python==4.11.0.86
pillow==11.1.0
transforms3d==0.3.1
执行命令进行安装:
pip install -r requirements.txt
3)安装bitsandbytes
由于是在NVIDIA Orin中,我们使用源码编译方式,进行安装。
git clone https://github.com/bitsandbytes-foundation/bitsandbytes.git && cd bitsandbytes/
cmake -DCOMPUTE_BACKEND=cuda -S .
make
pip install -e .
执行cmake -DCOMPUTE_BACKEND=cuda -S .时,打印信息:
-- The CXX compiler identification is GNU 11.4.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring bitsandbytes (Backend: cuda)
-- The CUDA compiler identification is NVIDIA 12.6.68
-- Detecting CUDA compiler ABI info
-- Detecting CUDA compiler ABI info - done
-- Check for working CUDA compiler: /usr/local/cuda/bin/nvcc - skipped
-- Detecting CUDA compile features
-- Detecting CUDA compile features - done
-- Found CUDAToolkit: /usr/local/cuda/include (found version "12.6.68")
-- Looking for C++ include pthread.h
-- Looking for C++ include pthread.h - found
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
-- Found Threads: TRUE
-- CUDA Version: 126 (12.6.68)
-- CUDA Compiler: /usr/local/cuda/bin/nvcc
-- CMake < 3.23.0; determining CUDA architectures supported...
-- CUDA Capabilities Available: 50;52;53;60;61;62;70;72;75;80;86;87;89;90
-- CUDA Capabilities Selected: 50;52;53;60;61;62;70;72;75;80;86;87;89;90
-- CUDA Targets: 50-real;52-real;53-real;60-real;61-real;62-real;70-real;72-real;75-real;80-real;86-real;87-real;89-real;90
-- CUDA NVCC Flags: --use_fast_math
-- Configuring done
-- Generating done
-- Build files have been written to: /home/byd/2025/bitsandbytes
执行make,进行编译时,打印信息:
[ 14%] Building CXX object CMakeFiles/bitsandbytes.dir/csrc/common.cpp.o[ 28%] Building CXX object CMakeFiles/bitsandbytes.dir/csrc/cpu_ops.cpp.o
[ 42%] Building CXX object CMakeFiles/bitsandbytes.dir/csrc/pythonInterface.cpp.o
[ 57%] Building CUDA object CMakeFiles/bitsandbytes.dir/csrc/ops.cu.o
[ 71%] Building CUDA object CMakeFiles/bitsandbytes.dir/csrc/kernels.cu.o
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
ptxas warning : Value of threads per SM for entry _Z9kQuantizePfS_Phi is out of range. .minnctapersm will be ignored
[ 85%] Linking CUDA device code CMakeFiles/bitsandbytes.dir/cmake_device_link.o
[100%] Linking CXX shared library bitsandbytes/libbitsandbytes_cuda126.so
[100%] Built target bitsandbytes
最后执行pip install -e .进行安装,打印信息:
.....
Building wheels for collected packages: bitsandbytesBuilding editable for bitsandbytes (pyproject.toml) ... doneCreated wheel for bitsandbytes: filename=bitsandbytes-0.49.0.dev0-0.editable-cp310-cp310-linux_aarch64.whl size=6975 sha256=322943120b81314fd0891b079dd84d225c803db267e2f256de2eb7b3f48f5acaStored in directory: /tmp/pip-ephem-wheel-cache-b93_rfxb/wheels/dc/8b/31/ae7b8434a1a8f35b69dab355573ddf62b93fb625e58acb4640
Successfully built bitsandbytes
Installing collected packages: bitsandbytes
Successfully installed bitsandbytes-0.49.0.dev0
编写一个代码,测试使用能使用CUDA加速:
import torch
import bitsandbytes as bnb
import sysprint("=" * 60)
print("bitsandbytes CUDA 详细检测")
print("=" * 60)# 1. 检查系统环境
print("1. 系统环境检查:")
print(f" Python 版本: {sys.version}")
print(f" PyTorch 版本: {torch.__version__}")
print(f" bitsandbytes 版本: {bnb.__version__}")# 2. 检查 PyTorch 的 CUDA 支持
print("\n2. PyTorch CUDA 支持:")
print(f" torch.cuda.is_available(): {torch.cuda.is_available()}")
if torch.cuda.is_available():print(f" CUDA 版本: {torch.version.cuda}")print(f" 当前设备: {torch.cuda.current_device()}")print(f" 设备名称: {torch.cuda.get_device_name()}")print(f" GPU 数量: {torch.cuda.device_count()}")
else:print(" ❌ PyTorch 无法访问 CUDA")# 3. 检查 bitsandbytes 的具体模块
print("\n3. bitsandbytes 模块检查:")
modules_to_check = ['optim', 'functional', 'nn']
for module in modules_to_check:has_module = hasattr(bnb, module)status = "✓" if has_module else "✗"print(f" {status} bnb.{module}: {has_module}")# 4. 尝试导入具体功能
print("\n4. 功能导入测试:")
try:from bitsandbytes import optim, functional, nnprint(" ✓ 核心模块导入成功")# 检查优化器if hasattr(optim, 'Adam8bit'):print(" ✓ Adam8bit 优化器可用")if hasattr(optim, 'Adam32bit'):print(" ✓ Adam32bit 优化器可用")except ImportError as e:print(f" ❌ 模块导入失败: {e}")# 5. 实际功能测试
print("\n5. 实际功能测试:")
if torch.cuda.is_available():try:# 创建测试模型和数据model = torch.nn.Linear(10, 5)input_data = torch.randn(2, 10)# 测试 GPU 转移model = model.cuda()input_data = input_data.cuda()print(" ✓ 模型和数据成功转移到 GPU")# 测试 8-bit 优化器optimizer = bnb.optim.Adam8bit(model.parameters(), lr=0.001)print(" ✓ 8-bit 优化器创建成功")# 前向传播output = model(input_data)print(" ✓ GPU 上前向传播成功")# 反向传播loss = output.sum()loss.backward()print(" ✓ 反向传播成功")# 优化器步骤optimizer.step()print(" ✓ 优化器步骤执行成功")print("\n🎉 所有 CUDA 功能测试通过!bitsandbytes 可以使用 CUDA 加速")except Exception as e:print(f" ❌ CUDA 功能测试失败: {e}")print(f" 错误类型: {type(e).__name__}")
else:print(" ⚠ 跳过 CUDA 功能测试(PyTorch CUDA 不可用)")print("=" * 60)
运行信息:
============================================================
bitsandbytes CUDA 详细检测
============================================================
1. 系统环境检查:
Python 版本: 3.10.16 | packaged by conda-forge | (main, Apr 3 2025, 14:16:18) [GCC 13.3.0]
PyTorch 版本: 2.6.0
bitsandbytes 版本: 0.49.0.dev02. PyTorch CUDA 支持:
torch.cuda.is_available(): True
CUDA 版本: 12.6
当前设备: 0
设备名称: Orin
GPU 数量: 13. bitsandbytes 模块检查:
✓ bnb.optim: True
✓ bnb.functional: True
✓ bnb.nn: True4. 功能导入测试:
✓ 核心模块导入成功
✓ Adam8bit 优化器可用
✓ Adam32bit 优化器可用5. 实际功能测试:
✓ 模型和数据成功转移到 GPU
✓ 8-bit 优化器创建成功
✓ GPU 上前向传播成功
✓ 反向传播成功
✓ 优化器步骤执行成功🎉 所有 CUDA 功能测试通过!bitsandbytes 可以使用 CUDA 加速
============================================================
2、Qwen3-VL+ NVIDIA Orin 推理——8bit量化版本
- 首先使用LLaMA Factory,对Qwen3-VL-4b进行Lora监督微调,得到微调后的lora权重;
- 然后合并 基础Qwen3-VL-4b权重 + lora权重 = 完整微调权重
- 最后对“完整微调权重”进行8bit量化,进行模型推理,提供实践案例。
2.1对Qwen3-VL进行Lora 监督微调
下面是LLaMA Factory 的微调训练配置,用于Qwen3-VL-4B-Instruct 多模态模型 的有监督微调:
| 项 | 配置值 | 作用 & 分析 |
|---|---|---|
| 模型 | Qwen3-VL-4B-Instruct(HuggingFace 源) | 选择了 4B 参量的多模态(图文)模型,适合轻量化多模态任务微调 |
| 微调方法 | LoRA | 低参数量微调(只训练适配器权重),显存占用低、训练速度快 |
| 量化策略 | 8bit(QLoRA)+ bnb 量化 | 进一步压缩显存(8bit 加载模型),结合 LoRA 可在普通消费级显卡(如 16G 显存)上训练 |
| 训练阶段 | Supervised Fine-Tuning (SFT,有监督微调) | 基于标注数据集训练模型生成符合要求的输出 |
微调界面:

核心超参数配置(仅供参考,需要根据自己的模型调整的)
| 项 | 配置值 | 作用 & 分析 |
|---|---|---|
| 学习率 | 3e-5 | LoRA 微调的常用学习率(比全量微调高 1-2 个数量级) |
| 训练轮数 | 3.0 | 训练轮次较少,适合快速收敛或小数据集(避免过拟合) |
| 批处理 / 梯度累积 | 批大小 2 + 梯度累积 2 → 等效批大小 4 | 小批量 + 梯度累积平衡显存占用与训练稳定性 |
| 截断长度 | 2048 | 匹配 Qwen3-VL 的上下文长度上限,适配长文本 / 多模态输入 |
| 学习率调节器 | cosine(余弦退火) | 训练后期平滑降低学习率,帮助模型稳定收敛 |
| 计算类型 | bf16 | 半精度计算(需显卡支持 Ampere 及以上架构),进一步节省显存并加速训练 |
微调完整后,得到lora权重,默认保存在LLaMA Factory目录下的./saves/Qwen3-VL-4B-Instruct/lora/xxxxxx
2.2、合并权重
这里的思路:基础Qwen3-VL-4b权重 + lora权重 = 完整微调权重
我们只需确定:基础权重的路径model_name_or_path、lora微调权重路径adapter_name_or_path
然后执行下面命令,进行合并:
llamafactory-cli export \--model_name_or_path "/home/user/.cache/huggingface/hub/models--Qwen--Qwen3-VL-4B-Instruct/snapshots/ebb281ec70b05090aa6165b016eac8ec08e71b17/" \--adapter_name_or_path ./saves/Qwen3-VL-4B-Instruct/lora/train_2025-11-13-13-37/ \--export_dir ./qwen3-vl-4b-merged-20251113-fp16-lora\--template qwen3_vl_nothink
等待合并完成~
合并后的权重大小:
8.3G model_path/qwen3-vl-4b-merged
2.3、使用bnb进行8bit量化
通过BitsAndBytes工具对 Qwen3-VL 模型进行 8 位量化,以降低模型显存占用,
同时保存量化后的模型、分词器(tokenizer)和处理器(processor),便于后续部署使用。
- 使用
transformers库中的模型加载类(Qwen3VLForConditionalGeneration、AutoModel)、量化配置(BitsAndBytesConfig) - 依赖
torch进行张量计算和设备管理。
模型量化的实力代码,如下所示:
from transformers import Qwen3VLForConditionalGeneration, AutoModel, BitsAndBytesConfig, AutoTokenizer, AutoProcessor
import torchinput_merged_model = "./model_path/qwen3-vl-4b-merged-20251113-8bit-lora"
output_merged_model = "./model_path/qwen3-vl-4b-bnb-quantized-8bit-merged-20251113-int8"'''
BitsAndBytes 仅支持:位量化: quant_type="fp4" 或 "nf4"(配合 load_in_4bit=True)位量化:必须用 quant_type="int8"(实际是 8 位整数,不是浮点)。fp8 是 NVIDIA 的 8 位浮点格式,不在 Hugging Face BitsAndBytes 支持范围内。
'''def quantize_model():# 配置8位bnb量化quantization_config = BitsAndBytesConfig(load_in_8bit=True, # 8位量化(必须)bnb_8bit_compute_dtype=torch.float16, # 计算用float16bnb_8bit_use_double_quant=True, # 双量化(压缩高效,可选)bnb_8bit_quant_type="int8" # ✅ 修正:必须为 "int8"(8位整数量化))try:# 尝试使用特定模型类print("尝试使用 Qwen3VLForConditionalGeneration 加载模型...")model = Qwen3VLForConditionalGeneration.from_pretrained(input_merged_model,quantization_config=quantization_config,device_map="auto",trust_remote_code=True)except Exception as e:print(f"使用特定类失败: {e}")print("尝试使用 AutoModel 加载模型...")# 回退到 AutoModelmodel = AutoModel.from_pretrained(input_merged_model,quantization_config=quantization_config,device_map="auto",trust_remote_code=True)# 加载并保存tokenizer和processortokenizer = AutoTokenizer.from_pretrained(input_merged_model, trust_remote_code=True)processor = AutoProcessor.from_pretrained(input_merged_model, trust_remote_code=True)# 保存量化后的模型和相关组件model.save_pretrained(output_merged_model)tokenizer.save_pretrained(output_merged_model)processor.save_pretrained(output_merged_model)print("模型8位量化并保存完成!")if __name__ == "__main__":quantize_model()
明确 8 位量化的关键参数:
load_in_8bit=True:启用 8 位量化bnb_8bit_quant_type="int8":指定量化类型为 8 位整数(符合 BitsAndBytes 对 8 位量化的要求)bnb_8bit_compute_dtype=torch.float16:量化过程中使用 float16 进行计算,平衡精度与性能bnb_8bit_use_double_quant=True:启用双量化(可选优化,进一步压缩模型大小,减少内存占用)
量化后的权重大小:
4.6G model_path/qwen3-vl-4b-bnb-quantized-8bit-merged-20251113-int8
2.4、8bit量化后的模型推理(实践案例1)
这个实践案例,基于 Qwen3-VL 量化模型的单张图像描述
- 模型加载:使用
AutoModelForImageTextToText加载多模态模型,适配 Qwen3-VL 的图文生成能力。 - 核心参数:
device_map="auto"自动分配模型到 CPU/GPU,trust_remote_code=True允许加载模型自定义代码。 - 处理器配套:
AutoProcessor用于统一处理文本提示和图像输入,确保格式符合模型要求。
特点:
- 输入处理:用
apply_chat_template格式化对话(用户 + 图像 + 文本提示),processor将文本和图像转为模型可识别的张量,并移至对应设备。 - 生成配置:
max_new_tokens=512限制描述长度,do_sample=False+num_beams=1采用确定性生成,保证结果稳定。 - 结果提取:裁剪模型输出中冗余的输入部分,解码后去除特殊 token,得到纯净描述文本。
推理代码:
import os
from PIL import Image
import torch
from transformers import AutoProcessor, AutoModelForImageTextToText# 模型路径配置
QUANTIZED_MODEL_PATH = "./model_path/qwen3-vl-4b-bnb-quantized-8bit-merged-20251113-int8"
# 输入图片路径
input_image = "dataset/test_pic/350012.jpg"# 初始化模型和处理器
print(f"正在加载模型:{QUANTIZED_MODEL_PATH}...")
model = AutoModelForImageTextToText.from_pretrained(QUANTIZED_MODEL_PATH,device_map="auto",trust_remote_code=True
)
processor = AutoProcessor.from_pretrained(QUANTIZED_MODEL_PATH)
print("模型和处理器加载成功!")def describe_image(image_path):"""生成图像的详细描述"""try:# 加载图像image = Image.open(image_path).convert("RGB")# 构建提示词(专注于图像整体描述)messages = [{"role": "user","content": [{"type": "image"},{"type": "text", "text": "请详细描述这张图片的内容,包括场景、物体、颜色、布局等信息。"}]}]# 处理输入text = processor.apply_chat_template(messages,tokenize=False,add_generation_prompt=True)inputs = processor(text=text,images=image,return_tensors="pt",padding=True).to(model.device)# 生成描述with torch.no_grad():generated_ids = model.generate(**inputs,max_new_tokens=512,do_sample=False,num_beams=1)# 提取并返回生成的描述generated_ids_trimmed = generated_ids[:, inputs.input_ids.shape[1]:]description = processor.batch_decode(generated_ids_trimmed,skip_special_tokens=True,clean_up_tokenization_spaces=False)[0]return descriptionexcept Exception as e:print(f"图像描述失败: {str(e)}")return ""def process_single_image(image_path):"""处理单张图像并打印描述"""if not os.path.exists(image_path):print(f"图像文件不存在: {image_path}")returnprint(f"\n--- 处理图像: {os.path.basename(image_path)} ---")description = describe_image(image_path)if description:print("\n图像描述:")print(description)print(f"\n--- 处理完成 ---")if __name__ == "__main__":import argparseparser = argparse.ArgumentParser(description='图像描述工具')parser.add_argument('--image', type=str, default=input_image, help='需要描述的图像路径')args = parser.parse_args()process_single_image(args.image)
输入图片:

运行信息:
正在加载模型:./model_path/qwen3-vl-4b-bnb-quantized-8bit-merged-20251113-int8...
模型和处理器加载成功!--- 处理图像: 350012.jpg ---
The following generation flags are not valid and may be ignored: ['temperature', 'top_p', 'top_k']. Set `TRANSFORMERS_VERBOSITY=info` for more details.图像描述:
这张图片展示了一个布置温馨、光线柔和的现代客厅。整个空间以中性色调为主,搭配暖色装饰,营造出舒适宜人的氛围。**场景与布局:**
客厅布局清晰,以一张米色布艺沙发为中心,沙发靠墙放置,旁边是深色木质边桌。前方是一个深棕色木质咖啡桌,桌下铺着一块色彩丰富的几何图案地毯,地毯以蓝色、红色和米色为主,为整个空间增添活力。左侧是壁炉区域,右侧是边桌和台灯,墙上挂有艺术画作,整体布局平衡且富有层次感。**主要物体与细节:**1. **沙发:**- 一张米色布艺沙发,靠背和坐垫厚实,显得柔软舒适。- 沙发上摆放着多个靠垫,颜色丰富:深紫色、橙色、红色和橄榄绿,形成视觉焦点。- 沙发右侧有一个深色木质边桌,上面放着一盏白色台灯和一个酒杯。2. **咖啡桌:**- 深棕色木质咖啡桌,带有开放式下层置物架。- 桌面上摆放着一个白色陶瓷花瓶,瓶身呈优雅的鹅颈状,旁边是一个透明玻璃花瓶,内有干花枝。- 一个小型绿色盆栽植物放在花瓶旁,增添自然气息。- 一个透明玻璃杯和一个酒杯也放在桌上。3. **壁炉与镜子:**- 左侧是一个深色砖砌壁炉,壁炉上方悬挂着一个大尺寸的木框镜子,镜中反射出楼梯的一部分,暗示房屋结构。- 壁炉上方的架子上摆放着几个玻璃烛台和装饰品。4. **墙面装饰:**- 墙上挂着一幅大型抽象画,画中描绘了城市景观,色调以暖橙、赭石和米白为主,与沙发靠垫颜色相呼应。- 墙角处有一扇白色窗框的小窗,透入自然光。5. **灯光与家具:**- 墙角的台灯和壁炉旁的台灯都采用白色灯罩,与整体色调协调。- 墙边有一个白色书架,上面摆放着书籍和装饰品。**颜色与氛围:**
整个空间以米--- 处理完成 ---
2.5、8bit量化后的模型推理(实践案例2)
这个模型推理示例,是 Qwen3-VL 量化模型的特定类别物体检测与可视化,
核心用于批量处理图像、提取目标物体多维度信息并生成可视化结果。
核心功能:
- 支持指定任意类别的物体(table、cup、bottle 等),输出类别、边界框、颜色、形状、外观 5 类信息。
- 支持批量读取图像目录,自动按自然排序处理文件。
- 生成带边界框和类别的可视化图像,同时打印详细物体描述。
- 包含异常处理和格式容错,确保流程稳定性。
推理代码:
import os
import json
import glob
import re
import torch
import numpy as np
from PIL import Image
import cv2
import time
from transformers import AutoProcessor, AutoModelForImageTextToTextQUANTIZED_MODEL_PATH = "./model_path/qwen3-vl-4b-bnb-quantized-8bit-merged-20251113-int8" # lora微调后的8-bit量化模型
input_dir = "./dataset_label/01_waitingroom/"
output_dir = "./Output_DATA/01_waitingroom"# 指定检测的物体类别,可以指定任意物体名称或在物体描述
NODE_SPACE = ['table', 'cup', 'bottle', 'chair', 'robot', 'garbage can', 'shelf', 'tissue box', 'potted plant']# 全局初始化模型和处理器
print(f"正在加载8-bit量化后的Qwen3-VL-4B模型:{QUANTIZED_MODEL_PATH}...")
model = AutoModelForImageTextToText.from_pretrained(QUANTIZED_MODEL_PATH,device_map="auto",trust_remote_code=True
)
processor = AutoProcessor.from_pretrained(QUANTIZED_MODEL_PATH)
print("修复后的量化Qwen3-VL-4B模型和处理器加载成功!")def call_vlm(image_path, prompt):"""安全的VLM调用函数"""try:# >>>>> 修改点1:直接使用 PIL 加载图像 <<<<<# 假设模型能处理任意大小的有效图像,不再强制调整尺寸image = Image.open(image_path).convert("RGB") # 确保是 RGB 模式messages = [{"role": "user","content": [{"type": "image"},{"type": "text", "text": prompt}]}]text = processor.apply_chat_template(messages,tokenize=False,add_generation_prompt=True)inputs = processor(text=text,images=image,return_tensors="pt",padding=True).to(model.device)# 生成参数with torch.no_grad():generated_ids = model.generate(**inputs,max_new_tokens=512,do_sample=False,num_beams=1,early_stopping=False)generated_ids_trimmed = generated_ids[:, inputs.input_ids.shape[1]:]output_text = processor.batch_decode(generated_ids_trimmed,skip_special_tokens=True,clean_up_tokenization_spaces=False)[0]return output_textexcept Exception as e:print(f"VLM调用失败: {str(e)}")return ""def detect_and_describe_objects(image_path):"""检测图像中的物体,获取其描述并反归一化bbox"""# 构建提示词prompt = f"""请严格检测图像中属于以下类别的所有物体:{NODE_SPACE}。对于每个检测到的物体,请提供:1. 类别 (category)2. 紧贴边缘的边界框 (bbox),格式为 [x1,y1,x2,y2](整数像素坐标)3. 颜色 (color)4. 形状 (shape)5. 外观描述 (appearance)输出要求:- 仅识别列表中的类别。- bbox必须精确且紧密贴合物体边缘。- 描述信息需与bbox内的区域一致。- 严格以 JSON 数组形式返回,不要包含任何额外的文本或代码块标记。输出示例(格式参考):[{{"category": "cup","bbox": [97, 203, 176, 282],"color": "白色","shape": "圆柱形","appearance": "带把手陶瓷杯"}},{{"category": "table","bbox": [10, 318, 639, 474],"color": "原木色","shape": "长方形","appearance": "四腿木质桌面"}}]"""try:# >>>>> 修改点2:直接使用 PIL 获取图像尺寸 <<<<<# 加载图像以获取尺寸用于坐标反归一化with Image.open(image_path) as img_pil:w_img, h_img = img_pil.convert("RGB").size # 确保获取到尺寸print("开始调用VLM进行物体检测...")time_start = time.time()response = call_vlm(image_path, prompt)time_end = time.time()print(f'VLM推理时间:{time_end - time_start:.2f}s')if not response:print("VLM返回空响应")return []# 解析JSON响应try:objects_data = json.loads(response.strip())except json.JSONDecodeError:# 尝试清理常见的格式问题cleaned_response = response.strip()if cleaned_response.startswith('```json'):cleaned_response = cleaned_response[7:]if cleaned_response.startswith('```'):cleaned_response = cleaned_response[3:]if cleaned_response.endswith('```'):cleaned_response = cleaned_response[:-3]cleaned_response = cleaned_response.strip()try:objects_data = json.loads(cleaned_response)except json.JSONDecodeError as e:print(f"JSON解析失败: {e}")print(f"原始响应内容: {response}")return []if not isinstance(objects_data, list):print(f"响应不是列表格式: {objects_data}")return []# 处理并验证每个检测到的物体valid_objects = []for i, obj in enumerate(objects_data):# 验证基本结构if not (isinstance(obj, dict) and obj.get('category') in NODE_SPACE and len(obj.get('bbox', [])) == 4):print(f"警告:跳过无效的检测结果 #{i+1}: {obj}")continue# 提取并转换bbox坐标try:coords = list(map(float, obj['bbox']))x1_norm, y1_norm, x2_norm, y2_norm = coordsexcept (ValueError, TypeError) as e:print(f"警告:跳过坐标格式错误的物体 #{i+1}: {e}")continue# 反归一化 (假设模型输出的是0-1000范围的坐标)x1 = int(round(x1_norm / 1000 * w_img))y1 = int(round(y1_norm / 1000 * h_img))x2 = int(round(x2_norm / 1000 * w_img))y2 = int(round(y2_norm / 1000 * h_img))# 确保坐标在图像范围内x1 = max(0, min(x1, w_img - 1))y1 = max(0, min(y1, h_img - 1))x2 = max(x1 + 1, min(x2, w_img - 1))y2 = max(y1 + 1, min(y2, h_img - 1))# 更新对象字典obj['bbox'] = [x1, y1, x2, y2]valid_objects.append(obj)print(f"共检测并验证了 {len(valid_objects)} 个物体。")return valid_objectsexcept Exception as e:print(f"物体检测过程失败: {str(e)}")return []def visualize_detections(image_path, detected_objects, output_path):"""可视化检测结果:绘制bbox和标签"""try:# 读取图像image_bgr = cv2.imread(image_path)if image_bgr is None:print(f"无法读取图像: {image_path}")returnh_img, w_img = image_bgr.shape[:2] # 获取OpenCV图像的尺寸# 绘制每个检测到的物体for i, obj in enumerate(detected_objects):bbox = obj.get('bbox', [0, 0, 0, 0])category = obj.get('category', 'Unknown')# 确保坐标在有效范围内 (基于OpenCV图像尺寸)x1, y1, x2, y2 = bboxx1 = max(0, min(x1, w_img - 1))y1 = max(0, min(y1, h_img - 1))x2 = max(x1 + 1, min(x2, w_img - 1))y2 = max(y1 + 1, min(y2, h_img - 1))# 绘制边界框cv2.rectangle(image_bgr, (x1, y1), (x2, y2), (0, 255, 0), 2)# 准备标签文本label_parts = [category]label = " ".join(label_parts)# 计算标签位置(在框上方)(text_width, text_height), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)y_label = y1 - 10 if y1 - 10 > 10 else y1 + 10 + text_height# 绘制标签背景矩形cv2.rectangle(image_bgr, (x1, y_label - text_height - baseline), (x1 + text_width, y_label + baseline), (0, 255, 0), -1)# 绘制标签文字cv2.putText(image_bgr, label, (x1, y_label),cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)# 保存可视化图像cv2.imwrite(output_path, image_bgr)print(f"检测结果可视化已保存至: {output_path}")except Exception as e:print(f"可视化检测结果失败: {str(e)}")def print_object_descriptions(detected_objects):"""打印每个检测到的物体的详细描述信息"""if not detected_objects:print("未检测到任何物体。")returnprint("\n--- 检测到的物体描述 ---")for i, obj in enumerate(detected_objects):print(f"物体 #{i+1}:")print(f" 类别 (Category): {obj.get('category', 'N/A')}")print(f" 边界框 (Bbox): {obj.get('bbox', 'N/A')}")print(f" 颜色 (Color): {obj.get('color', 'N/A')}")print(f" 形状 (Shape): {obj.get('shape', 'N/A')}")print(f" 外观 (Appearance): {obj.get('appearance', 'N/A')}")print("-" * 20)print("--- 描述结束 ---\n")def natural_sort_key(filename):"""自然排序含数字的文件名"""return [int(t) if t.isdigit() else t.lower()for t in re.split(r'(\d+)', os.path.basename(filename))]def process_images_simple(input_dir, output_dir):"""批量处理图像目录,执行简化任务"""try:os.makedirs(output_dir, exist_ok=True)viz_output_dir = os.path.join(output_dir, "visualizations_simple")os.makedirs(viz_output_dir, exist_ok=True)image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.gif']image_files = []for ext in image_extensions:image_files.extend(glob.glob(os.path.join(input_dir, ext)))image_files = sorted(image_files, key=natural_sort_key)if not image_files:print(f"在目录 {input_dir} 中未找到图像文件")returnfor image_path in image_files:image_name = os.path.basename(image_path)print(f"\n---------- 处理图像: {image_name} ----------")# 1. 检测物体并获取描述detected_objects = detect_and_describe_objects(image_path)# 2. 打印物体描述print_object_descriptions(detected_objects)# 3. 可视化检测框viz_filename = os.path.splitext(image_name)[0] + "_simple_viz.jpg"viz_path = os.path.join(viz_output_dir, viz_filename)visualize_detections(image_path, detected_objects, viz_path)print(f"---------- 完成处理: {image_name} ----------\n")print("\n所有图像处理完成")except Exception as e:print(f"处理图像目录失败: {str(e)}")if __name__ == "__main__":import argparseparser = argparse.ArgumentParser(description='简化版Qwen3-VL物体检测与可视化工具')parser.add_argument('--input_dir', type=str, default=input_dir, help='包含图像的输入目录')parser.add_argument('--output_dir', type=str, default=output_dir, help='输出结果的目录')args = parser.parse_args()process_images_simple(args.input_dir, args.output_dir)
初始化模型→2. 批量读取图像→3. 单图处理(检测物体→解析结果→打印描述→绘制可视化)→4. 输出所有结果。
核心逻辑是利用多模态模型实现特定类别物体的检测与描述,并通过可视化和文本输出直观呈现结果
运行效果:

打印信息:
---------- 处理图像: be158920.jpg ----------
开始调用VLM进行物体检测...
VLM推理时间:17.25s
共检测并验证了 2 个物体。--- 检测到的物体描述 ---
物体 #1:
类别 (Category): table
边界框 (Bbox): [162, 304, 315, 401]
颜色 (Color): 原木色
形状 (Shape): 长方形
外观 (Appearance): 木质桌面,表面有纹理
--------------------
物体 #2:
类别 (Category): potted plant
边界框 (Bbox): [402, 148, 495, 256]
颜色 (Color): 绿色
形状 (Shape): 灌木状
外观 (Appearance): 高大的盆栽植物,叶片茂密
--------------------
--- 描述结束 ---检测结果可视化已保存至: ./Output_DATA/01_waitingroom/visualizations_simple/be158920_simple_viz.jpg
---------- 完成处理: be158920.jpg ----------
其他运行示例:

3、Qwen3-VL+ NVIDIA Orin 推理——4bit量化版本
思路其实和上面的8bit量化基本一致的,只是代码参数有差异;
- 首先使用LLaMA Factory,对Qwen3-VL-4b进行Lora监督微调,得到微调后的lora权重;
- 然后合并 基础Qwen3-VL-4b权重 + lora权重 = 完整微调权重
- 最后对“完整微调权重”进行4bit量化,进行模型推理,计算模型准确率和召回率。
3.1、对Qwen3-VL进行Lora 监督微调
下面是LLaMA Factory 的微调训练配置,用于Qwen3-VL-4B-Instruct 多模态模型 的有监督微调:
| 项 | 配置值 | 作用 & 分析 |
|---|---|---|
| 模型 | Qwen3-VL-4B-Instruct(HuggingFace 源) | 选择了 4B 参量的多模态(图文)模型,适合轻量化多模态任务微调 |
| 微调方法 | LoRA | 低参数量微调(只训练适配器权重),显存占用低、训练速度快 |
| 量化策略 | 4bit(QLoRA)+ bnb 量化 | 进一步压缩显存(4bit 加载模型),结合 LoRA 可在普通消费级显卡(如 16G 显存)上训练 |
| 训练阶段 | Supervised Fine-Tuning (SFT,有监督微调) | 基于标注数据集训练模型生成符合要求的输出 |
微调界面:

核心超参数配置(仅供参考,需要根据自己的模型调整的)
| 项 | 配置值 | 作用 & 分析 |
|---|---|---|
| 学习率 | 3e-5 | LoRA 微调的常用学习率(比全量微调高 1-2 个数量级) |
| 训练轮数 | 3.0 | 训练轮次较少,适合快速收敛或小数据集(避免过拟合) |
| 批处理 / 梯度累积 | 批大小 2 + 梯度累积 2 → 等效批大小 4 | 小批量 + 梯度累积平衡显存占用与训练稳定性 |
| 截断长度 | 2048 | 匹配 Qwen3-VL 的上下文长度上限,适配长文本 / 多模态输入 |
| 学习率调节器 | cosine(余弦退火) | 训练后期平滑降低学习率,帮助模型稳定收敛 |
| 计算类型 | bf16 | 半精度计算(需显卡支持 Ampere 及以上架构),进一步节省显存并加速训练 |
微调完整后,得到lora权重,默认保存在LLaMA Factory目录下的./saves/Qwen3-VL-4B-Instruct/lora/xxxxxx
3.2、合并权重
这里的思路:基础Qwen3-VL-4b权重 + lora权重 = 完整微调权重
我们只需确定:基础权重的路径model_name_or_path、lora微调权重路径adapter_name_or_path
然后执行下面命令,进行合并:
llamafactory-cli export \--model_name_or_path "/home/user/.cache/huggingface/hub/models--Qwen--Qwen3-VL-4B-Instruct/snapshots/ebb281ec70b05090aa6165b016eac8ec08e71b17/" \--adapter_name_or_path ./saves/Qwen3-VL-4B-Instruct/lora/train_2025-11-11-08-59-06/ \--export_dir ./model_path/qwen3-vl-4b-merged-t \--template qwen3_vl_nothink
等待合并完成,合并后的权重大小:
8.3G model_path/qwen3-vl-4b-merged
3.3、使用bnb进行4bit量化
通过BitsAndBytes工具对 Qwen3-VL 模型进行 4 位量化,以降低模型显存占用,
同时保存量化后的模型、分词器(tokenizer)和处理器(processor),便于后续部署使用。
- 使用
transformers库中的模型加载类(Qwen3VLForConditionalGeneration、AutoModel)、量化配置(BitsAndBytesConfig) - 依赖
torch进行张量计算和设备管理。
模型量化的实力代码,如下所示:
from transformers import Qwen3VLForConditionalGeneration, AutoModel, BitsAndBytesConfig, AutoTokenizer, AutoProcessor
import torchdef quantize_model():# 配置4位bnb量化quantization_config = BitsAndBytesConfig(load_in_4bit=True,bnb_4bit_compute_dtype=torch.float16,bnb_4bit_quant_type="nf4",bnb_4bit_use_double_quant=True,)try:# 尝试使用特定模型类print("尝试使用 Qwen3VLForConditionalGeneration 加载模型...")model = Qwen3VLForConditionalGeneration.from_pretrained("./model_path/qwen3-vl-4b-merged",quantization_config=quantization_config,device_map="auto",trust_remote_code=True)except Exception as e:print(f"使用特定类失败: {e}")print("尝试使用 AutoModel 加载模型...")# 回退到 AutoModelmodel = AutoModel.from_pretrained("./model_path/qwen3-vl-4b-merged",quantization_config=quantization_config,device_map="auto",trust_remote_code=True)# 加载tokenizer和processortokenizer = AutoTokenizer.from_pretrained("./model_path/qwen3-vl-4b-merged",trust_remote_code=True)processor = AutoProcessor.from_pretrained("./model_path/qwen3-vl-4b-merged",trust_remote_code=True)# 保存量化后的模型和相关组件model.save_pretrained("./model_path/qwen3-vl-4b-bnb-quantized-merged")tokenizer.save_pretrained("./model_path/qwen3-vl-4b-bnb-quantized-merged")processor.save_pretrained("./model_path/qwen3-vl-4b-bnb-quantized-merged")print("模型量化并保存完成!")if __name__ == "__main__":quantize_model()
通过BitsAndBytesConfig定义量化参数,核心配置包括:
load_in_4bit=True:启用 4 位量化(相比 16 位 / 32 位大幅减少显存占用)。bnb_4bit_compute_dtype=torch.float16:计算时使用 float16 精度,平衡效率与性能。bnb_4bit_quant_type="nf4":采用 “Normalized Float 4” 量化类型,专为自然语言模型优化,精度优于普通 4 位量化。bnb_4bit_use_double_quant=True:启用双重量化(对量化参数再量化),进一步减少内存开销。
量化后的权重大小:
2.7G model_path/qwen3-vl-4b-bnb-quantized-merged
3.4、量化后的模型推理(实践案例)
这个模型推理示例,是 Qwen3-VL 量化模型的特定类别物体检测与可视化,
核心用于批量处理图像、提取目标物体多维度信息并生成可视化结果。
核心功能:
- 支持指定任意类别的物体(table、cup、bottle 等),输出类别、边界框、颜色、形状、外观 5 类信息。
- 支持批量读取图像目录,自动按自然排序处理文件。
- 生成带边界框和类别的可视化图像,同时打印详细物体描述。
- 包含异常处理和格式容错,确保流程稳定性。
推理代码:
import os
import json
import glob
import re
import torch
import numpy as np
from PIL import Image
import cv2
import time
from transformers import AutoProcessor, AutoModelForImageTextToText# --- 基础路径 ---
QUANTIZED_MODEL_PATH = "./model_path/qwen3-vl-4b-bnb-quantized-merged" # 融合lora微调 4-bit量化
input_dir = "./dataset_label/01_office/"
output_dir = "./Output_DATA/01_office"
# 定义要检测的物体类别
NODE_SPACE = ['table', 'cup', 'bottle', 'chair', 'robot', 'garbage can', 'shelf', 'tissue box', 'potted plant']# 全局初始化模型和处理器
print(f"正在加载修复后的量化Qwen3-VL-4B模型:{QUANTIZED_MODEL_PATH}...")
try:# 使用修复后的量化模型(不指定dtype)model = AutoModelForImageTextToText.from_pretrained(QUANTIZED_MODEL_PATH,device_map="auto",trust_remote_code=True)processor = AutoProcessor.from_pretrained(QUANTIZED_MODEL_PATH)print("修复后的量化Qwen3-VL-4B模型和处理器加载成功!")except Exception as e:print(f"模型加载失败:{str(e)}")exit(1)def call_vlm(image_path, prompt):"""安全的VLM调用函数"""try:# >>>>> 修改点1:直接使用 PIL 加载图像 <<<<<# 假设模型能处理任意大小的有效图像,不再强制调整尺寸image = Image.open(image_path).convert("RGB") # 确保是 RGB 模式messages = [{"role": "user","content": [{"type": "image"},{"type": "text", "text": prompt}]}]text = processor.apply_chat_template(messages,tokenize=False,add_generation_prompt=True)inputs = processor(text=text,images=image,return_tensors="pt",padding=True).to(model.device)# 生成参数with torch.no_grad():generated_ids = model.generate(**inputs,max_new_tokens=512,do_sample=False,num_beams=1,early_stopping=False)generated_ids_trimmed = generated_ids[:, inputs.input_ids.shape[1]:]output_text = processor.batch_decode(generated_ids_trimmed,skip_special_tokens=True,clean_up_tokenization_spaces=False)[0]return output_textexcept Exception as e:print(f"VLM调用失败: {str(e)}")return ""def detect_and_describe_objects(image_path):"""检测图像中的物体,获取其描述并反归一化bbox"""# 构建提示词prompt = f"""请严格检测图像中属于以下类别的所有物体:{NODE_SPACE}。对于每个检测到的物体,请提供:1. 类别 (category)2. 紧贴边缘的边界框 (bbox),格式为 [x1,y1,x2,y2](整数像素坐标)3. 颜色 (color)4. 形状 (shape)5. 外观描述 (appearance)输出要求:- 仅识别列表中的类别。- bbox必须精确且紧密贴合物体边缘。- 描述信息需与bbox内的区域一致。- 严格以 JSON 数组形式返回,不要包含任何额外的文本或代码块标记。输出示例(格式参考):[{{"category": "cup","bbox": [97, 203, 176, 282],"color": "白色","shape": "圆柱形","appearance": "带把手陶瓷杯"}},{{"category": "table","bbox": [10, 318, 639, 474],"color": "原木色","shape": "长方形","appearance": "四腿木质桌面"}}]"""try:# >>>>> 修改点2:直接使用 PIL 获取图像尺寸 <<<<<# 加载图像以获取尺寸用于坐标反归一化with Image.open(image_path) as img_pil:w_img, h_img = img_pil.convert("RGB").size # 确保获取到尺寸print("开始调用VLM进行物体检测...")time_start = time.time()response = call_vlm(image_path, prompt)time_end = time.time()print(f'VLM推理时间:{time_end - time_start:.2f}s')if not response:print("VLM返回空响应")return []# 解析JSON响应try:objects_data = json.loads(response.strip())except json.JSONDecodeError:# 尝试清理常见的格式问题cleaned_response = response.strip()if cleaned_response.startswith('```json'):cleaned_response = cleaned_response[7:]if cleaned_response.startswith('```'):cleaned_response = cleaned_response[3:]if cleaned_response.endswith('```'):cleaned_response = cleaned_response[:-3]cleaned_response = cleaned_response.strip()try:objects_data = json.loads(cleaned_response)except json.JSONDecodeError as e:print(f"JSON解析失败: {e}")print(f"原始响应内容: {response}")return []if not isinstance(objects_data, list):print(f"响应不是列表格式: {objects_data}")return []# 处理并验证每个检测到的物体valid_objects = []for i, obj in enumerate(objects_data):# 验证基本结构if not (isinstance(obj, dict) and obj.get('category') in NODE_SPACE and len(obj.get('bbox', [])) == 4):print(f"警告:跳过无效的检测结果 #{i+1}: {obj}")continue# 提取并转换bbox坐标try:coords = list(map(float, obj['bbox']))x1_norm, y1_norm, x2_norm, y2_norm = coordsexcept (ValueError, TypeError) as e:print(f"警告:跳过坐标格式错误的物体 #{i+1}: {e}")continue# 反归一化 (假设模型输出的是0-1000范围的坐标)x1 = int(round(x1_norm / 1000 * w_img))y1 = int(round(y1_norm / 1000 * h_img))x2 = int(round(x2_norm / 1000 * w_img))y2 = int(round(y2_norm / 1000 * h_img))# 确保坐标在图像范围内x1 = max(0, min(x1, w_img - 1))y1 = max(0, min(y1, h_img - 1))x2 = max(x1 + 1, min(x2, w_img - 1))y2 = max(y1 + 1, min(y2, h_img - 1))# 更新对象字典obj['bbox'] = [x1, y1, x2, y2]valid_objects.append(obj)print(f"共检测并验证了 {len(valid_objects)} 个物体。")return valid_objectsexcept Exception as e:print(f"物体检测过程失败: {str(e)}")return []def visualize_detections(image_path, detected_objects, output_path):"""可视化检测结果:绘制bbox和标签"""try:# 读取图像image_bgr = cv2.imread(image_path)if image_bgr is None:print(f"无法读取图像: {image_path}")returnh_img, w_img = image_bgr.shape[:2] # 获取OpenCV图像的尺寸# 绘制每个检测到的物体for i, obj in enumerate(detected_objects):bbox = obj.get('bbox', [0, 0, 0, 0])category = obj.get('category', 'Unknown')# 确保坐标在有效范围内 (基于OpenCV图像尺寸)x1, y1, x2, y2 = bboxx1 = max(0, min(x1, w_img - 1))y1 = max(0, min(y1, h_img - 1))x2 = max(x1 + 1, min(x2, w_img - 1))y2 = max(y1 + 1, min(y2, h_img - 1))# 绘制边界框cv2.rectangle(image_bgr, (x1, y1), (x2, y2), (0, 255, 0), 2)# 准备标签文本label_parts = [category]# 可以选择性地添加颜色、形状等信息到标签# color = obj.get('color', '')# shape = obj.get('shape', '')# if color: label_parts.append(color)# if shape: label_parts.append(shape)label = " ".join(label_parts)# 计算标签位置(在框上方)(text_width, text_height), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)y_label = y1 - 10 if y1 - 10 > 10 else y1 + 10 + text_height# 绘制标签背景矩形cv2.rectangle(image_bgr, (x1, y_label - text_height - baseline), (x1 + text_width, y_label + baseline), (0, 255, 0), -1)# 绘制标签文字cv2.putText(image_bgr, label, (x1, y_label),cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)# 保存可视化图像cv2.imwrite(output_path, image_bgr)print(f"检测结果可视化已保存至: {output_path}")except Exception as e:print(f"可视化检测结果失败: {str(e)}")def print_object_descriptions(detected_objects):"""打印每个检测到的物体的详细描述信息"""if not detected_objects:print("未检测到任何物体。")returnprint("\n--- 检测到的物体描述 ---")for i, obj in enumerate(detected_objects):print(f"物体 #{i+1}:")print(f" 类别 (Category): {obj.get('category', 'N/A')}")print(f" 边界框 (Bbox): {obj.get('bbox', 'N/A')}")print(f" 颜色 (Color): {obj.get('color', 'N/A')}")print(f" 形状 (Shape): {obj.get('shape', 'N/A')}")print(f" 外观 (Appearance): {obj.get('appearance', 'N/A')}")print("-" * 20)print("--- 描述结束 ---\n")def natural_sort_key(filename):"""自然排序含数字的文件名"""return [int(t) if t.isdigit() else t.lower()for t in re.split(r'(\d+)', os.path.basename(filename))]def process_images_simple(input_dir, output_dir):"""批量处理图像目录,执行简化任务"""try:os.makedirs(output_dir, exist_ok=True)viz_output_dir = os.path.join(output_dir, "visualizations_simple")os.makedirs(viz_output_dir, exist_ok=True)image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.gif']image_files = []for ext in image_extensions:image_files.extend(glob.glob(os.path.join(input_dir, ext)))image_files = sorted(image_files, key=natural_sort_key)if not image_files:print(f"在目录 {input_dir} 中未找到图像文件")returnfor image_path in image_files:image_name = os.path.basename(image_path)print(f"\n---------- 处理图像: {image_name} ----------")# 1. 检测物体并获取描述detected_objects = detect_and_describe_objects(image_path)# 2. 打印物体描述print_object_descriptions(detected_objects)# 3. 可视化检测框viz_filename = os.path.splitext(image_name)[0] + "_simple_viz.jpg"viz_path = os.path.join(viz_output_dir, viz_filename)visualize_detections(image_path, detected_objects, viz_path)print(f"---------- 完成处理: {image_name} ----------\n")print("\n所有图像处理完成")except Exception as e:print(f"处理图像目录失败: {str(e)}")if __name__ == "__main__":import argparseparser = argparse.ArgumentParser(description='简化版Qwen3-VL物体检测与可视化工具')parser.add_argument('--input_dir', type=str, default=input_dir, help='包含图像的输入目录')parser.add_argument('--output_dir', type=str, default=output_dir, help='输出结果的目录')args = parser.parse_args()process_images_simple(args.input_dir, args.output_dir)
代码流程:
- 初始化:加载量化模型和处理器,自动适配设备(CPU/GPU)。
- 图像读取:批量获取指定目录下的多种格式图像,按自然排序处理。
- VLM 调用:通过提示词引导模型检测目标物体,返回 JSON 格式结果。
- 结果处理:解析 JSON 响应(含格式容错),将归一化坐标反转为像素坐标并校验范围。
- 可视化与输出:用 OpenCV 绘制边界框和标签,保存图像并打印详细物体信息。
运行效果:

打印信息:
---------- 处理图像: 011002.jpg ----------
开始调用VLM进行物体检测...
VLM推理时间:26.43s
共检测并验证了 7 个物体。--- 检测到的物体描述 ---
物体 #1:
类别 (Category): table
边界框 (Bbox): [0, 48, 639, 479]
颜色 (Color): 原木色
形状 (Shape): 圆形
外观 (Appearance): 木质圆桌,表面有食物和餐具
--------------------
物体 #2:
类别 (Category): cup
边界框 (Bbox): [396, 74, 458, 157]
颜色 (Color): 透明
形状 (Shape): 圆柱形
外观 (Appearance): 玻璃杯,装有橙色液体
--------------------
物体 #3:
类别 (Category): cup
边界框 (Bbox): [190, 281, 272, 385]
颜色 (Color): 透明
形状 (Shape): 圆柱形
外观 (Appearance): 玻璃杯,装有清水
--------------------
物体 #4:
类别 (Category): bottle
边界框 (Bbox): [339, 4, 403, 205]
颜色 (Color): 绿色
形状 (Shape): 圆柱形
外观 (Appearance): 玻璃瓶,瓶身有蓝色标签和文字
--------------------
物体 #5:
类别 (Category): bottle
边界框 (Bbox): [224, 0, 304, 143]
颜色 (Color): 黄色
形状 (Shape): 圆柱形
外观 (Appearance): 玻璃瓶,瓶身有绿色标签和文字
--------------------
物体 #6:
类别 (Category): bottle
边界框 (Bbox): [240, 104, 298, 297]
颜色 (Color): 银色
形状 (Shape): 圆柱形
外观 (Appearance): 金属瓶,瓶身有黑色盖子
--------------------
物体 #7:
类别 (Category): chair
边界框 (Bbox): [554, 0, 639, 186]
颜色 (Color): 白色
形状 (Shape): 四脚椅
外观 (Appearance): 白色塑料椅,部分可见
--------------------
--- 描述结束 ---检测结果可视化已保存至: ./Output_DATA/COCO-trian-2017-Graph/visualizations_simple/011002_simple_viz.jpg
---------- 完成处理: 011002.jpg ----------
其他运行示例:

NVIDIA Orin的资源占用情况:

分享完成~
