【大模型】使用llamafactory 训练 qwen2.5-VL 的目标检测任务

前言

截止到目前,目标检测的功能还是yolo模型落地性更强。但大模型也已经全面开花,所以也尝试下使用大模型来完成目标检测的训练,看看其效果如何,看看它在目标检测上有怎样的优势。

本次选用qwen2.5-VL,一开始使用github上阅读性强的工程训练,总觉得差些意思。
于是决定自己手搓个大模型训练推理工程,emm...,预测效果也是差强人意。
兜兜转转还是使用个高star的工程,先跑通得到想要的效果。后面再阅读核心代码掌握大模型训练过程吧

一 llama factory的部署

【大模型】LLaMA-Factory的环境配置、微调模型与测试 的第二章节,已经介绍了工程安装步骤,整个过程比较流畅。(其余章节介绍了在文本上的大模型微调的过程)

二 llama factory的配置文件介绍

github上给出的训练命令:

bash 复制代码
llamafactory-cli train examples/train_lora/qwen2_5vl_lora_sft.yaml

以此为切入点,来看看训练自己模型时需要关注的内容,这里先陈列出

  • examples/qwen2_5vl_lora_sft.yaml
  • data/mllm_demo.jsonl

2.1 配置文件 qwen2_5vl_lora_sft.yaml

该文件中配置了大模型训练过程中所有的参数,包括:

  • model 模型的选择与配置、
  • method 训练方式、
  • dataset 数据集、
  • output 训练时输出信息、
  • train 训练参数。

现阶段要跑通工程需要关注的有:model、dataset


2.2 model 模型的设置

【examples/qwen2_5vl_lora_sft.yaml】 中的 model_name_or_path

  • 设置为自己微调的大模型名称,工程会自动从huggingface中拉取模型。但这种方式不建议,网络限制和不稳定等原因,会让下载过程很不稳定。
  • 设置为已经下载到本地大模型的绝对路径。该方式需要提前将大模型下载下来

2.3 数据集的关联

【examples/qwen2_5vl_lora_sft.yaml】 中的 dataset: mllm_demo

  • 对应文件【data/dataset_info.json】的 mllm_demo,内容如下:

    json 复制代码
    ...
    {
      "mllm_demo": {
        "file_name": "mllm_demo.json",   # 数据文件
        "formatting": "sharegpt",        # 数据格式
        "columns": {
          "messages": "messages",
          "images": "images"
        },
        "tags": {
          "role_tag": "role",
        "content_tag": "content",
          "user_tag": "user",
          "assistant_tag": "assistant"
        }
      },
    }
  • 数据文件 【data/mllm_demo.json】
    最小单元内容

    json 复制代码
      {
        "messages": [
          {
            "content": "<image>Who is he?",
            "role": "user"
          },
          {
            "content": "He's Thomas Muller from Bayern Munich.",
            "role": "assistant"
          },
          {
            "content": "Why is he on the ground?",
            "role": "user"
          },
          {
            "content": "Because he's sliding on his knees to celebrate.",
            "role": "assistant"
          }
        ],
        "images": [
          "mllm_demo_data/2.jpg"
        ]
      },


这两个文件中,变量的命名是有对应性的。对应效果如图所示:图片上半部分为dataset_info.json 的内容,下半部分为 mllm_demo.json的内容。相同颜色的框内的变量命名一定是要一致的。

三 使用llamafacotry训练目标检测


3.1 qwen2.5-VL-3B 的下载

使用modelscope 下载qwen2.5-VL-3B模型

  1. 首先确定虚拟环境中安装了modelscope
  2. modelscope官网 搜索qwen2.5-VL,最终找到界面如下。

本人尝试用的下载方式为图中圈出的两种

  • 方式一:

    bash 复制代码
    modelscope download --model Qwen/Qwen2.5-VL-3B-Instruct --local_dir [本地下载路径]
  • 方式二:

    python 复制代码
    from modelscope.hub.snapshot_download import snapshot_download
    
    # 定义模型ID和本地目标路径
    model_id = 'qwen/Qwen2.5-VL-3B-Instruct'
    local_dir = '~/llm_models/Qwen2.5-VL'
    
    # 下载模型
    model_dir = snapshot_download(model_id, cache_dir=local_dir, revision='master')
    print(f"模型已下载至: {model_dir}")

3.2 训练数据的格式转换

已有的目标检测框保存的格式为labelme的json格式。现在需要将该种标签,转换成与mllm_demo一致的数据格式。
一般的数据处理,仅在生成训练时需要的jsonl 时,使用官方提供的smart_resize 获取图片resize后应该有的尺寸,使用该尺寸处理检测框,无需resize图片。图片的resize操作,放在工程的图像预处理中。
但这里为了尽可能减少出错的可能性,同时处理了图片和标签,将训练的图片和标签一开始就对齐,保存到新的路径下。

图像与边界框的协同处理

复制代码
原始图片: 640×428 像素
经过smart_resize处理后: 644×420 像素
  1. 模型输入要求

    • Qwen2.5-VL使用Vision Transformer架构
    • 需要将图片分割成28×28的patch
    • 输入尺寸必须是28的倍数
    python 复制代码
    # 约束1: 能被factor整除。调整后的尺寸必须是28的倍数
    # 这是因为Vision Transformer使用28×28的patch
    # new_height % 28 == 0、new_width % 28 == 0
    round(640/28)×28 = 23×28 = 644(宽度)
    round(428/28)×28 = 15×28 = 420(高度)
  2. 计算效率考虑

    • 固定尺寸便于批处理
    • 避免内存溢出(通过max_pixels限制)
    • 避免太小影响效果(通过min_pixels保证)
    python 复制代码
    # 约束2: 像素总数在指定范围内
    min_pixels = 56×56 = 3,136 像素
    max_pixels = 14×14×4×1280 = 1,003,520 像素
    # 14×14是patch网格,4是某种设计参数,1280是模型维度
    检查像素数:644×420 = 270,480 像素,在 3136 和 1003520之间 
    最终尺寸:644×420 像素
  3. 保持空间关系

    • 相对位置不变:如果原始框在图片中心,缩放后仍在中心

    • 相对大小不变:框与图片的比例关系保持不变

    • 纵横比不变:不会发生形变


    • 注意 :由于需要同时满足"能被28整除"和"保持宽高比"这两个要求,在实际操作中存在内在矛盾。当原始图片尺寸不是28的倍数时,完美保持宽高比在数学上是不可能的。因此,smart_resize函数采取了一种近似保持 的策略:

      1. 分别调整 :将高度和宽度分别四舍五入到最接近的28的倍数
      2. 轻微形变 :接受由此产生的轻微纵横比变化(通常在1-3%范围内)
      3. 权衡选择:相较于填充法(增加无效像素)或裁剪法(丢失图像信息),这种轻微形变是更好的权衡
    • 这种处理在保持图像主要内容不变的同时,确保了模型输入要求

  4. 关键注意事项

    • a 坐标系统:始终是绝对像素坐标,不是相对坐标(百分比)
    • b 舍入处理 :使用round()四舍五入,可能引入1像素误差
    • c 边界保护:防止坐标超出图像边界
    • d 保持有效性 :确保转换后仍然是有效的矩形(x2≥x1, y2≥y1)

为了工程的可观整洁,在根目录创建文件夹【LL_own】。
创建数据处理脚本【LL_own/data_deal.py】,在实验阶段,尽可能的保持问题可控性,我将图片resize到固定的、模型可接受的尺寸,同时将标签同等映射处理。具体实现如下:

python 复制代码
import json, glob, os, math, cv2
from pathlib import Path
from typing import List, Tuple, Dict, Optional

class Qwen2_5VLProcessor:
   """
   封装 Qwen2.5-VL 训练数据前置处理:
   1. 按官方规则 smart-resize 图片
   2. 同步映射 bbox
   3. 保存 resize 后图片
   4. 生成官方格式的 jsonl 样本
   """

   def __init__(
       self,
       factor: int = 28,
       min_pixels: int = 56 * 56,
       max_pixels: int = 14 * 14 * 4 * 1280,
       long_edge_range: Tuple[int, int] = (642, 644),
       target_label: str = "甲下损坏",
   ):
       self.factor = factor
       self.min_pixels = min_pixels
       self.max_pixels = max_pixels
       self.long_edge_range = long_edge_range
       self.target_label = target_label

   # ------------- private -------------
   def _smart_resize(self, h: int, w: int) -> Tuple[int, int]:
       lo, hi = self.long_edge_range
       long = max(h, w)
       if long < lo:
           scale = lo / long
       elif long > hi:
           scale = hi / long
       else:
           scale = 1.0
       h, w = int(h * scale), int(w * scale)

       if h < self.factor or w < self.factor:
           raise ValueError(f"height:{h} or width:{w} must be larger than factor:{self.factor}")
       if max(h, w) / min(h, w) > 200:
           raise ValueError("absolute aspect ratio must be smaller than 200")

       h_bar = round(h / self.factor) * self.factor
       w_bar = round(w / self.factor) * self.factor
       if h_bar * w_bar > self.max_pixels:
           beta = math.sqrt((h * w) / self.max_pixels)
           h_bar = math.floor(h / beta / self.factor) * self.factor
           w_bar = math.floor(w / beta / self.factor) * self.factor
       elif h_bar * w_bar < self.min_pixels:
           beta = math.sqrt(self.min_pixels / (h * w))
           h_bar = math.ceil(h * beta / self.factor) * self.factor
           w_bar = math.ceil(w * beta / self.factor) * self.factor
       return h_bar, w_bar

   @staticmethod
   def _shape2xyxy(points: List[List[float]]) -> List[int]:
       xs, ys = zip(*points)
       return [round(min(xs)), round(min(ys)), round(max(xs)), round(max(ys))]

   def _map_bbox(self, bbox: List[int], oh: int, ow: int, nh: int, nw: int) -> List[int]:
       x1, y1, x2, y2 = bbox
       scale_x, scale_y = nw / ow, nh / oh
       x1 = max(0, min(round(x1 * scale_x), nw - 1))
       y1 = max(0, min(round(y1 * scale_y), nh - 1))
       x2 = max(0, min(round(x2 * scale_x), nw - 1))
       y2 = max(0, min(round(y2 * scale_y), nh - 1))
       return [x1, y1, x2, y2]


   # ------------- public -------------
   def process_one(self, json_path: str, img_path: str, save_img_dir: Optional[str] = None) -> Optional[Dict]:
       """
       单张图+labelme json -> 返回官方格式 dict(含新图片保存路径)
       """
       json_path, img_path = Path(json_path), Path(img_path)
       ann = json.loads(json_path.read_text(encoding="utf-8"))
       orig_h, orig_w = ann["imageHeight"], ann["imageWidth"]

       # 1. smart resize 尺寸
       new_h, new_w = self._smart_resize(orig_h, orig_w)

       # 2. 处理 bbox 与 label
       boxes, labels = [], []
       for shape in ann["shapes"]:
           if shape["shape_type"] != "rectangle":
               continue
           if self.target_label not in shape["label"]:
               continue
           bbox = self._shape2xyxy(shape["points"])
           bbox = self._map_bbox(bbox, orig_h, orig_w, new_h, new_w)
           boxes.append(bbox)
           labels.append(shape["label"])

       if not boxes:
           return None

       # 3. 读取并 resize 图片
       img = cv2.imread(str(img_path))
       if img is None:
           raise ValueError(f"cannot read image: {img_path}")
       img_res = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)

       # 4. 保存图片        
       if save_img_dir:
           save_path = Path(save_img_dir) / img_path.name
           cv2.imwrite(str(save_path), img_res)
       else:
           save_path = img_path  # 不保存时仍返回原路径
           
       return {
           "id": img_path.stem,
           "images": [str(save_path)],
           "messages": [
               {"role": "user", "content": "<image>\n找出图中所有目标,给出类别和检测框。"},
               {"role": "assistant", "content": json.dumps({"boxes": boxes, "labels": labels}, ensure_ascii=False)},
           ],
       }

   def process_batch(self, json_dir: str, out_jsonl: str, save_img_dir: str = None, img_dir_replace: Tuple[str, str] = ("Annotations", "JPEGImages")):
       """
       批量把目录下所有 labelme json 转成 jsonl
       json_dir:  labelme json 目录
       out_jsonl: 输出 jsonl 路径
       img_dir_replace: 把 json 路径中的字符串替换成图片目录,用于自动找图
       """
       json_files = sorted(glob.glob(os.path.join(json_dir, "*.json")))
       if not json_files:
           raise ValueError(f"no json found in {json_dir}")

       Path(out_jsonl).parent.mkdir(parents=True, exist_ok=True)
       if save_img_dir:
           Path(save_img_dir).mkdir(parents=True, exist_ok=True)
       written = 0
       with open(out_jsonl, "w", encoding="utf-8") as fw:
           for jf in json_files:
               # 自动找图:把 Annotations → JPEGImages,并匹配任意后缀
               img_dir = jf.replace(img_dir_replace[0], img_dir_replace[1])
               img_files = glob.glob(img_dir.replace(".json", ".*"))

               if not img_files:
                   print(f"[WARN] skip {jf}: image not found")
                   continue
               sample = self.process_one(jf, img_files[0], save_img_dir)
               if sample:
                   fw.write(json.dumps(sample, ensure_ascii=False) + "\n")
                   written += 1

       print(f"✅ 完成!共转换 {written}/{len(json_files)} 条样本 -> {out_jsonl}")


# -------------------------------- usage --------------------------------
if __name__ == "__main__":
   proc = Qwen2_5VLProcessor(
       target_label="损坏",                # 只保留该标签的框
   )

   # 批量转换
   proc.process_batch(
       json_dir="/opt/ai-platform/lldataset/DATASET/detect/traindataset/*/Annotations",
       out_jsonl="qwen_det2/train_part.jsonl",
       save_img_dir="/home/LL/workfile_2026/12-QwenVL/LLaMA-Factory/LL_own/qwen_det2/resized_imgs",  # resize 后图片保存目录,None 则不保存
   )

生成文件【LL_own/qwen_det2/train.jsonl】【LL_own/qwen_det2/resized_imgs/*】 jsonl 文件可用于模型训练。

生成的数据结束后,进行可视化】代码如下

python 复制代码
"""
可视化 jsonl 样本(Qwen2.5-VL 格式)
用法:
   python visualize_jsonl.py \
       --jsonl qwen_det2/train_part.jsonl \
       --vis_dir qwen_det2/vis_jsonl \
       --max 50
"""
import json, random, cv2, os
from pathlib import Path
import argparse


def draw_boxes(img_path: str, boxes: list, labels: list, out_path: str, thickness=2):
   """把框画到图上并保存"""
   img = cv2.imread(img_path)
   if img is None:
       raise FileNotFoundError(img_path)

   # 固定颜色表
   color_map = {}
   for bbox, label in zip(boxes, labels):
       if label not in color_map:
           color_map[label] = tuple(random.randint(0, 255) for _ in range(3))
       color = color_map[label]
       x1, y1, x2, y2 = map(int, bbox)
       cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness)
       cv2.putText(img, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX,
                   0.7, color, thickness)
   cv2.imwrite(out_path, img)
   return color_map


def main(jsonl: str, vis_dir: str, max_samples: int):
   vis_dir = Path(vis_dir)
   vis_dir.mkdir(parents=True, exist_ok=True)

   lines = Path(jsonl).read_text(encoding='utf-8').strip().splitlines()
   if not lines:
       print('jsonl 为空')
       return

   # 随机抽查
   if 0 < max_samples < len(lines):
       lines = random.sample(lines, max_samples)

   for idx, line in enumerate(lines, 1):
       data = json.loads(line)
       img_file = data['images'][0]          # 第一张图
       assistant = json.loads(data['messages'][1]['content'])
       boxes, labels = assistant['boxes'], assistant['labels']

       out_file = vis_dir / f"{Path(img_file).stem}_vis.jpg"
       draw_boxes(img_file, boxes, labels, str(out_file))
       print(f'[{idx:>3}/{len(lines)}] 已画框 -> {out_file}')
       input()

   print(f'✅ 全部完成,共输出 {len(lines)} 张到 {vis_dir}')


if __name__ == '__main__':
   parser = argparse.ArgumentParser(description='可视化 Qwen2.5-VL 格式的 jsonl')
   parser.add_argument('--jsonl', help='输入 jsonl 路径')
   parser.add_argument('--vis_dir', help='可视化结果保存目录')
   parser.add_argument('--max', type=int, default=0,
                       help='最多画多少张(0=全部)', )
   args = parser.parse_args()
   
   args.jsonl = "qwen_det2/train_part.jsonl"
   args.vis_dir = "vis/"
   main(args.jsonl, args.vis_dir, args.max)
   

3.3 配置文件的修改

如第二章节介绍,
我们需要修改两个文件内的配置。

  • 【examples/train_lora/qwen2_5vl_lora_sft.yaml】
    复制文件:

    bash 复制代码
    cp examples/train_lora/qwen2_5vl_lora_sft.yaml examples/train_lora/qwen2_5vl_own.yaml

    修改文件:

    yaml 复制代码
    ### model
    model_name_or_path: /home/LL/llm_models/Qwen2.5-VL/Qwen/Qwen2___5-VL-3B-Instruct
    image_max_pixels: 262144
    video_max_pixels: 16384
    trust_remote_code: true
    
    ### method
    stage: sft
    do_train: true
    finetuning_type: lora
    lora_rank: 8
    lora_target: all
    
    ### dataset
    dataset:  train_det 
    template: qwen2_vl
    cutoff_len: 2048
    max_samples: 20000   # 注意:该参数会限制训练样本数量
    overwrite_cache: true
    preprocessing_num_workers: 16
    dataloader_num_workers: 4
    
    ### output
    output_dir: ./LL_own/save/train_detect_3B  # 注意:根据自己实际情况修改
    logging_steps: 10
    save_steps: 500
    plot_loss: true
    overwrite_output_dir: true
    save_only_model: false
    report_to: none  # choices: [none, wandb, tensorboard, swanlab, mlflow]
      
    ### train
    per_device_train_batch_size: 8   # 注意:符合自己的显存大小
    gradient_accumulation_steps: 8
    learning_rate: 1.0e-5    # 注意:学习率根据自己实际情况修改
    num_train_epochs: 6.0
    lr_scheduler_type: cosine
    warmup_ratio: 0.1
    bf16: true
    ddp_timeout: 180000000
    resume_from_checkpoint: null
    
    ### eval
    # val_size: 0.1
    # per_device_eval_batch_size: 1
    # eval_strategy: steps
    # eval_steps: 500
  • 【data/dataset_info.json】

    json 复制代码
    ...,
      "train_det":{
        "file_name": "/home/LL/workfile_2026/12-QwenVL/LLaMA-Factory/LL_own/qwen_det2/train.jsonl",
        "formatting": "sharegpt",
        "columns": {
          "messages": "messages",
          "images": "images"
        },
        "tags": {
          "role_tag": "role",
          "content_tag": "content",
          "user_tag": "user",
          "assistant_tag": "assistant"
        }
      }
    }

3.4 开启微调训练

bash 复制代码
llamafactory-cli train examples/train_lora/qwen2_5vl_own.yaml

四 训练好的模型的推理

训练结束后,得到lora模型,为了方便后续的使用,所以实现了脱离llamafactory的推理。

  • 图片的resize
  • 送进大模型推理
  • 预测结果解析
  • 检测框的反向映射
python 复制代码
# qwen25vl_detector.py
import json
import re
import math
from pathlib import Path
from typing import List, Dict, Tuple, Optional

import cv2
import torch
import numpy as np
from PIL import Image
from transformers import Qwen2_5_VLForConditionalGeneration, AutoProcessor
from qwen_vl_utils import process_vision_info
from peft import PeftModel


class Qwen25VLDetector:

    def __init__(
        self,
        base_model: str,
        lora_path: str,
        device_map: str = "auto",
        factor: int = 28,
        min_pixels: int = 56 * 56,
        max_pixels: int = 14 * 14 * 4 * 1280,
        long_edge_range: Tuple[int, int] = (642, 644),
        prompt: str = "找出图中所有目标,给出类别和检测框。",
    ):
        self.base_model = base_model
        self.lora_path = lora_path
        self.device_map = device_map
        self.factor = factor
        self.prompt = prompt

        self.min_pixels = min_pixels
        self.max_pixels = max_pixels
        self.long_edge_range = long_edge_range

        # 编译正则
        self._box_pat = re.compile(r'"boxes":\s*(\[\[.*?\]\])')
        self._lab_pat = re.compile(r'"labels":\s*(\[.*?\])')

        self._load_model()
        self.warmup()  # 可选:提前跑一次,减少首次延迟

    # ---------- 公有接口 ----------
    def detect(self, image: Image.Image) -> List[Dict]:
        """返回原图坐标系下的检测框"""
        return self._run(image, visualize=False)

    def detect_visualize(
        self, image: Image.Image, save_path: Optional[str] = None
    ) -> List[Dict]:
        """带框可视化,默认保存为 vis.jpg"""
        results = self._run(image, visualize=True, save_path=save_path)
        return results

    def warmup(self):
        """空图热身,让 CUDA graph 编译"""
        dummy = Image.new("RGB", (224, 224), (114, 114, 114))
        _ = self.detect(dummy)
        print("[Warmup] Done.")

    # ---------- 内部逻辑 ----------
    def _load_model(self):
        print("[Model] Loading base model...")
        model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
            self.base_model,
            torch_dtype=torch.bfloat16,
            device_map=self.device_map,
            trust_remote_code=True,
        )
        print("[Model] Loading LoRA...")
        model = PeftModel.from_pretrained(model, self.lora_path)
        model = model.merge_and_unload()
        model.eval()

        processor = AutoProcessor.from_pretrained(
            self.base_model, trust_remote_code=True
        )

        self.model = model
        self.processor = processor

    def _smart_resize(self, h: int, w: int) -> Tuple[int, int]:
        lo, hi = self.long_edge_range
        long = max(h, w)
        if long < lo:
            scale = lo / long
        elif long > hi:
            scale = hi / long
        else:
            scale = 1.0
        h, w = int(h * scale), int(w * scale)

        if h < self.factor or w < self.factor:
            raise ValueError(f"height:{h} or width:{w} must be larger than factor:{self.factor}")
        if max(h, w) / min(h, w) > 200:
            raise ValueError("absolute aspect ratio must be smaller than 200")

        h_bar = round(h / self.factor) * self.factor
        w_bar = round(w / self.factor) * self.factor
        if h_bar * w_bar > self.max_pixels:
            beta = math.sqrt((h * w) / self.max_pixels)
            h_bar = math.floor(h / beta / self.factor) * self.factor
            w_bar = math.floor(w / beta / self.factor) * self.factor
        elif h_bar * w_bar < self.min_pixels:
            beta = math.sqrt(self.min_pixels / (h * w))
            h_bar = math.ceil(h * beta / self.factor) * self.factor
            w_bar = math.ceil(w * beta / self.factor) * self.factor
        return h_bar, w_bar

    def _preprocess(self, img: Image.Image) -> Tuple[Image.Image, float, float]:
        w, h = img.size
        new_h, new_w = self._smart_resize(h, w)  # 返回 (h, w)
        scaled = img.resize((new_w, new_h), Image.LANCZOS)
        sx, sy = w / new_w, h / new_h
        return scaled, sx, sy

    def _run(
        self,
        image: Image.Image,
        visualize: bool = False,
        save_path: Optional[str] = None,
    ) -> List[Dict]:
        # 1. 预处理
        scaled, sx, sy = self._preprocess(image)

        # 2. 构造对话
        messages = [
            {
                "role": "user",
                "content": [
                    {"type": "image", "image": scaled},
                    {"type": "text", "text": self.prompt},
                ],
            }
        ]
        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",
        ).to(self.model.device)

        # 3. 推理
        with torch.no_grad():
            generated_ids = self.model.generate(
                **inputs, max_new_tokens=512, do_sample=False
            )
        generated_ids_trimmed = [
            out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
        ]
        result = self.processor.batch_decode(
            generated_ids_trimmed,
            skip_special_tokens=True,
            clean_up_tokenization_spaces=False,
        )[0]

        # 4. 解析 JSON
        boxes, labels = self._parse_result(result)
        # 坐标还原到原图
        boxes = [[x1 * sx, y1 * sy, x2 * sx, y2 * sy] for x1, y1, x2, y2 in boxes]

        # 5. 可视化
        if visualize:
            self._visualize(image, boxes, labels, save_path)

        return [{"label": lab, "box": box} for lab, box in zip(labels, boxes)]

    def _parse_result(self, text: str) -> Tuple[List, List]:
        box_match = self._box_pat.search(text)
        lab_match = self._lab_pat.search(text)
        boxes = json.loads(box_match.group(1)) if box_match else []
        labels = json.loads(lab_match.group(1)) if lab_match else []
        return boxes, labels

    def _visualize(
        self,
        image: Image.Image,
        boxes: List[List[float]],
        labels: List[str],
        save_path: Optional[str] = None,
    ):
        img_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
        for (x1, y1, x2, y2), lab in zip(boxes, labels):
            x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
            cv2.rectangle(img_cv, (x1, y1), (x2, y2), (0, 255, 0), 2)
            cv2.putText(
                img_cv,
                lab,
                (x1, y1 - 5),
                cv2.FONT_HERSHEY_SIMPLEX,
                0.8,
                (0, 255, 0),
                2,
            )
        if save_path is None:
            save_path = "vis.jpg"
        cv2.imwrite(save_path, img_cv)
        print(f"[Vis] Saved -> {save_path}")


# ---------------- 简易 CLI 测试 ----------------
if __name__ == "__main__":
    det = Qwen25VLDetector(
        base_model="/home/LL/llm_models/Qwen2.5-VL/Qwen/Qwen2___5-VL-3B-Instruct",
        lora_path="/home/LL/workfile_2026/12-QwenVL/LLaMA-Factory/LL_own/save/train_detect_3B",
    )

    img_path = "test.jpg"
    results = det.detect_visualize(Image.open(img_path))
    print("检测结果:", results)

可得到正确的检测结果。

五 其他

模型合并、合并后的推理,都比较简单,就不记录了。

相关推荐
Franklin2 小时前
AI Coding 基础实践04:Pycharm 项目移植AI 工具 TRAE的history
人工智能
睡醒了叭2 小时前
图像分割-传统算法-聚类算法
opencv·算法·计算机视觉·聚类
得贤招聘官2 小时前
破局传统招聘:AI面试智能体构建精准高效新生态
大数据·人工智能·面试
胡萝卜3.02 小时前
Linux包管理器:高效安装软件的秘诀
linux·运维·服务器·人工智能·linux包管理·yum教程·apt入门
~央千澈~2 小时前
人工智能AI算法推荐之番茄算法推荐证实其算法推荐规则技术解析·卓伊凡
人工智能·算法·机器学习
执笔论英雄2 小时前
【RL】ROLL中loss 计算compute_approx_kl
人工智能
sealaugh322 小时前
AI(学习笔记第十七课)langchain v1.0(SQL Agent)
人工智能·笔记·学习
zbguolei2 小时前
使用VBA将EXCEL生成PPT
人工智能·opencv·计算机视觉
易百纳2 小时前
易百纳携多模态AI桌面机器人——Kubee Robot亮相2025火山引擎冬季FORCE大会
人工智能·火山引擎