一、LLamaFactory部署(wsl/linux)

1.配置环境

git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git 
cd LLaMA-Factory
conda create -n llama_factory python=3.12 -y
conda activate llama_factory
pip install -e ".[torch,metrics]"

这个命令会自动安装一系列关键库:

  • transformers, datasets, peft, accelerate
  • trl(用于强化学习)
  • sentencepiece, safetensors, bitsandbytes(支持量化训练)

安装完成后,验证是否成功:

llamafactory-cli version

返回版本号说明成功

请添加图片描述

⭐⭐⭐ 文件说明 ⭐⭐⭐

请添加图片描述

2.设置模型路径

因为微调大模型时会自动下载基础模型到默认路径 \home\username\.cache\huggingface\hub

,这样模型并不好找,需要手动指定模型下载位置(直接修改 WSL 的 .bashrc 文件,这样每次打开终端都会自动生效):

(1)首先新建好目标文件夹

请添加图片描述

(2)打开 wsl 配置文件

nano ~/.bashrc

(3)在文件末尾添加以下内容

# 1. 统一模型存储路径到 E 盘(防止撑爆 WSL 虚拟磁盘)
export MODELSCOPE_CACHE='/home/xx/LlamaFactory/models'
export HF_HOME='/home/xx/LlamaFactory/models'
# 2. 启用 Hugging Face 国内镜像加速(保留全生态 + 国内全速下载)
export HF_ENDPOINT=https://hf-mirror.com

(4)保存并刷新配置文件

保存并退出配置文件后

source ~/.bashrc

(5)检查配置文件是否生效

echo $HF_HOME

3.启动 WebUI:开启可视化训练

启动服务只需一条命令:

llamafactory-cli webui

首次运行时会自动下载 Gradio 并启动本地服务器,默认地址是:

👉 http://127.0.0.1

打开浏览器访问该链接,你会看到一个功能完整的控制台:

  • 左侧导航栏清晰划分出【模型加载】、【训练配置】、【数据集管理】等模块;
  • 支持中英文切换,对中文用户非常友好;
  • 实时展示 loss 曲线、GPU 利用率、训练日志等关键信息。

请添加图片描述

4.QWen2.5VL 目标检测微调

(1)数据集标注

采用十张 512*512 影像,并利用 labelme 绘制矩形框标注,得到标注文件

请添加图片描述

json 内容格式如下:

{
  "version": "5.5.0",
  "flags": {},
  "shapes": [
    {
      "label": "building",
      "points": [
        [
          3.560975609756099,
          67.78861788617886
        ],
        [
          49.4959349593496,
          133.64227642276424
        ]
      ],
      "group_id": null,
      "description": "",
      "shape_type": "rectangle",
      "flags": {},
      "mask": null
    },
    {
      "label": "building",
      "points": [
        [
          0.7154471544715761,
          145.02439024390245
        ],
        [
          43.398373983739816,
          225.51219512195124
        ]
      ],
      "group_id": null,
      "description": "",
      "shape_type": "rectangle",
      "flags": {},
      "mask": null
    },

❗❗ 其中的坐标点代表(左上角点和右下角点坐标)[[xmin, ymin], [xmax, ymax]],且为绝对坐标 ❗❗

默认原点从左上角开始,纵轴朝下,横轴朝右,

例如:

“points”: [

[3.560975609756099, 67.78861788617886],

[49.4959349593496, 133.64227642276424]

]

含义是这个矩形框在当前这张图像中的像素位置:

x_min ≈ 3.56 像素

y_min ≈ 67.79 像素

x_max ≈ 49.50 像素

y_max ≈ 133.64 像素

我的数据结构:

  • ├─ LLAMA-FACTORY /
    • ├─ data /
      • ├─ building_gt_bbox /
        • ├─ gt / (这里存的是原始的影像和 labelme 的 json 标注)
        • ├─ qwen_det_building /(用于存储缩放后的影像和转化的 jsonl 文件)
    • ├─ notebook /
      • ├─ 格式转化代码.ipynb
(2)训练数据格式转换

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

模型输入尺寸必须能被 28 整除
1.【图像与边界框的协同处理

原始图片: 512*512 像素

经过 smart_resize 处理后:504*504 像素

2.模型输入要求

  • Qwen2.5-VL 使用 Vision Transformer 架构
  • 需要将图片分割成 28×28 的 patch
  • 输入尺寸必须是 28 的倍数
_# 约束1: 能被factor整除。调整后的尺寸必须是28的倍数_
_# 这是因为Vision Transformer使用28×28的patch_
_# new_height % 28 == 0、new_width % 28 == 0_
**round**(512/28)×28 = 18×28 = 504(宽度)
**round**(512/28)×28 = 18×28 = 504(高度)

3.计算效率考虑

  • 固定尺寸便于批处理
  • 避免内存溢出(通过 max_pixels 限制)
  • 避免太小影响效果(通过 min_pixels 保证)
# 约束2: 像素总数在指定范围内
min_pixels = 56×56 = 3,136 像素
max_pixels = 14×14×4×1280 = 1,003,520 像素
# 14×14是patch网格,4是某种设计参数,1280是模型维度
检查像素数:504 * 504= 254016 像素,在 31361003520之间 
最终尺寸:504*504 像素

4.保持空间关系

  • 相对位置不变:如果原始框在图片中心,缩放后仍在中心
  • 相对大小不变:框与图片的比例关系保持不变
  • 纵横比不变:不会发生形变
  • 注意:由于需要同时满足"能被 28 整除"和"保持宽高比"这两个要求,在实际操作中存在内在矛盾。当原始图片尺寸不是 28 的倍数时,完美保持宽高比在数学上是不可能的。因此,smart_resize 函数采取了一种近似保持的策略:
  1. 分别调整:将高度和宽度分别四舍五入到最接近的 28 的倍数
  2. 轻微形变:接受由此产生的轻微纵横比变化(通常在 1-3% 范围内)
  3. 权衡选择:相较于填充法(增加无效像素)或裁剪法(丢失图像信息),这种轻微形变是更好的权衡
  • 这种处理在保持图像主要内容不变的同时,确保了模型输入要求

5.关键注意事项

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

尽可能的保持问题可控性,我将图片 resize 到固定的、模型可接受的尺寸,同时将标签同等映射处理。具体实现如下:格式转化代码.ipynb

import json, glob, os, math, cv2
from pathlib import Path
from typing import List, Tuple, Dict, Optional


class Qwen2_5VLProcessor:
    """
    这个类的作用:
    把 LabelMe 标注的建筑物检测数据,转换成 Qwen2.5-VL / LLaMA-Factory 可以训练的 JSONL 格式。

    你的原始数据结构类似:
        building_gt_bbox/gt/
        ├── 0.jpg
        ├── 0.json
        ├── 1.jpg
        ├── 1.json

    每个 json 是 LabelMe 标注文件,里面的 rectangle 框表示建筑物。

    最终输出:
        1. resize 后的图片
        2. train_part.jsonl

    train_part.jsonl 每一行是一张图片的训练样本,例如:
        {
          "id": "0",
          "images": ["qwen_det_building/resized_imgs/0.jpg"],
          "messages": [
            {
              "role": "user",
              "content": "<image>请检测..."
            },
            {
              "role": "assistant",
              "content": "[{\"bbox_2d\":[...],\"label\":\"building\"}]"
            }
          ]
        }
    """

    def __init__(
        self,
        factor: int = 28,
        min_pixels: int = 56 * 56,
        max_pixels: int = 14 * 14 * 4 * 1280,
        long_edge_range: Tuple[int, int] = (504, 504),
        target_label: str = "building",
    ):
        """
        初始化参数。

        factor:
            Qwen2.5-VL 图像尺寸通常要求高和宽是 28 的倍数。
            所以这里 factor 默认是 28。

        min_pixels:
            resize 后图片允许的最小像素数。
            太小的图会被放大。

        max_pixels:
            resize 后图片允许的最大像素数。
            太大的图会被缩小。

        long_edge_range:
            控制图片长边缩放到哪个范围。
            你的原图是 512x512,这里设置成 (504, 504),
            表示强制把长边缩放到 504。
            因为 504 = 28 * 18,正好是 28 的倍数。

        target_label:
            只保留这个类别的框。
            你的 LabelMe json 里建筑物 label 是 "building",
            所以这里默认 target_label="building"。
        """

        # Qwen2.5-VL 尺寸对齐因子,一般是 28
        self.factor = factor

        # 图片最小像素数限制
        self.min_pixels = min_pixels

        # 图片最大像素数限制
        self.max_pixels = max_pixels

        # 长边目标范围,例如 (504, 504)
        self.long_edge_range = long_edge_range

        # 只保留 LabelMe 中 label 等于 target_label 的目标
        self.target_label = target_label

        # 训练时给模型看的问题,也就是 user 的 content
        # 后续模型会学习:看到这句话 + 图像,就输出 assistant 中的 bbox JSON
        self.user_prompt = (
            "<image>请检测输入遥感影像中的所有建筑物,并仅以 JSON 数组格式返回结果。"
            "每个建筑物使用 bbox_2d 表示,格式为 [xmin, ymin, xmax, ymax]。"
            "如果没有建筑物,返回空数组 []。只输出合法 JSON,不要输出解释文字。"
        )

    # ============================================================
    # private 部分:类内部使用的辅助函数
    # ============================================================

    def _smart_resize(self, h: int, w: int) -> Tuple[int, int]:
        """
        根据 Qwen2.5-VL 的尺寸要求,对图片尺寸进行调整。

        输入:
            h: 原图高度
            w: 原图宽度

        输出:
            new_h: resize 后高度
            new_w: resize 后宽度

        处理逻辑:
            1. 先把图片长边缩放到 long_edge_range 指定范围
            2. 再把高宽调整为 factor=28 的倍数
            3. 如果图片面积太大,则继续缩小
            4. 如果图片面积太小,则继续放大

        对你的 512x512 图片来说:
            long_edge_range=(504,504)
            最终大概率会得到 504x504
        """

        # 取出目标长边范围
        # 比如 long_edge_range=(504,504),lo=504,hi=504
        lo, hi = self.long_edge_range

        # 原图长边,例如 512x512 的 long=512
        long = max(h, w)

        # 如果原图长边小于目标范围下限,就放大
        if long < lo:
            scale = lo / long

        # 如果原图长边大于目标范围上限,就缩小
        elif long > hi:
            scale = hi / long

        # 如果原图长边已经在目标范围内,就不缩放
        else:
            scale = 1.0

        # 按比例缩放高宽
        # 注意这里先变成 int,得到初步缩放后的尺寸
        h, w = int(h * scale), int(w * scale)

        # 防止图片太小,小于 factor=28 无法处理
        if h < self.factor or w < self.factor:
            raise ValueError(
                f"height:{h} or width:{w} must be larger than factor:{self.factor}"
            )

        # 防止长宽比极端异常,比如 10000x20 这种图
        if max(h, w) / min(h, w) > 200:
            raise ValueError("absolute aspect ratio must be smaller than 200")

        # 把高度调整成最接近的 28 的倍数
        # 例如 504 / 28 = 18,所以还是 504
        h_bar = round(h / self.factor) * self.factor

        # 把宽度调整成最接近的 28 的倍数
        w_bar = round(w / self.factor) * self.factor

        # 如果调整后的图片总像素数超过 max_pixels,就继续缩小
        if h_bar * w_bar > self.max_pixels:
            # beta 是缩小比例
            beta = math.sqrt((h * w) / self.max_pixels)

            # 缩小后仍然保证是 28 的倍数
            h_bar = math.floor(h / beta / self.factor) * self.factor
            w_bar = math.floor(w / beta / self.factor) * self.factor

        # 如果调整后的图片总像素数小于 min_pixels,就继续放大
        elif h_bar * w_bar < self.min_pixels:
            # beta 是放大比例
            beta = math.sqrt(self.min_pixels / (h * w))

            # 放大后仍然保证是 28 的倍数
            h_bar = math.ceil(h * beta / self.factor) * self.factor
            w_bar = math.ceil(w * beta / self.factor) * self.factor

        # 返回最终 resize 后的高和宽
        return h_bar, w_bar

    @staticmethod
    def _shape2xyxy(points: List[List[float]]) -> List[int]:
        """
        把 LabelMe rectangle 的两个点转换成 bbox_2d 格式。

        LabelMe 中 rectangle 的 points 通常长这样:
            [
              [x1, y1],
              [x2, y2]
            ]

        但是这两个点不一定严格是左上角和右下角,
        所以这里用 min / max 统一转成:
            [xmin, ymin, xmax, ymax]

        这正是 Qwen2.5-VL 常用的 bbox_2d 格式。
        """

        # zip(*points) 可以把 [[x1,y1],[x2,y2]] 拆成 xs=(x1,x2), ys=(y1,y2)
        xs, ys = zip(*points)

        # xmin 是两个 x 中较小的
        xmin = round(min(xs))

        # ymin 是两个 y 中较小的
        ymin = round(min(ys))

        # xmax 是两个 x 中较大的
        xmax = round(max(xs))

        # ymax 是两个 y 中较大的
        ymax = round(max(ys))

        return [xmin, ymin, xmax, ymax]

    def _map_bbox(
        self,
        bbox: List[int],
        oh: int,
        ow: int,
        nh: int,
        nw: int,
    ) -> List[int]:
        """
        当图片 resize 后,把原图上的 bbox 坐标同步缩放到新图上。

        输入:
            bbox: 原图上的框 [x1, y1, x2, y2]
            oh: original height,原图高度
            ow: original width,原图宽度
            nh: new height,新图高度
            nw: new width,新图宽度

        输出:
            resize 后图片上的 bbox [x1, y1, x2, y2]

        举例:
            原图 512x512
            新图 504x504
            原 bbox = [100,100,200,200]

            scale_x = 504 / 512
            scale_y = 504 / 512

            新 bbox ≈ [98,98,197,197]
        """

        # 拆开 bbox 坐标
        x1, y1, x2, y2 = bbox

        # 计算 x 方向缩放比例
        scale_x = nw / ow

        # 计算 y 方向缩放比例
        scale_y = nh / oh

        # 缩放 x1,并限制在 [0, nw-1] 范围内
        x1 = max(0, min(round(x1 * scale_x), nw - 1))

        # 缩放 y1,并限制在 [0, nh-1] 范围内
        y1 = max(0, min(round(y1 * scale_y), nh - 1))

        # 缩放 x2,并限制在 [0, nw-1] 范围内
        x2 = max(0, min(round(x2 * scale_x), nw - 1))

        # 缩放 y2,并限制在 [0, nh-1] 范围内
        y2 = max(0, min(round(y2 * scale_y), nh - 1))

        return [x1, y1, x2, y2]

    def _find_image(self, json_path: Path) -> Optional[Path]:
        """
        根据 json 文件自动寻找同名图片。

        你的数据是:
            0.json 对应 0.jpg
            1.json 对应 1.jpg

        这个函数会在 json 所在目录下依次尝试:
            0.jpg
            0.jpeg
            0.png
            0.tif
            0.tiff

        找到就返回图片路径,找不到就返回 None。
        """

        # 支持的图片后缀
        img_exts = [".jpg", ".jpeg", ".png", ".tif", ".tiff"]

        # 逐个尝试同名不同后缀的图片
        for ext in img_exts:
            # json_path.with_suffix(".jpg") 会把 0.json 变成 0.jpg
            img_path = json_path.with_suffix(ext)

            # 如果图片存在,就返回
            if img_path.exists():
                return img_path

        # 所有后缀都没找到,返回 None
        return None

    # ============================================================
    # public 部分:真正被外部调用的处理函数
    # ============================================================

    def process_one(
        self,
        json_path: str,
        save_img_dir: Optional[str] = None,
        image_rel_prefix: Optional[str] = None,
    ) -> Optional[Dict]:
        """
        处理一张图片对应的一个 LabelMe json。

        输入:
            json_path:
                LabelMe json 文件路径,例如 building_gt_bbox/gt/0.json

            save_img_dir:
                resize 后图片保存目录,例如 building_gt_bbox/qwen_det_building/resized_imgs

            image_rel_prefix:
                写入 JSONL 的图片路径前缀。
                例如 image_rel_prefix="qwen_det_building/resized_imgs"
                那么 JSONL 中会写:
                "images": ["qwen_det_building/resized_imgs/0.jpg"]

        输出:
            一个训练样本 dict。
            如果没有找到图片,或者没有 building 框,返回 None。
        """

        # 转成 Path 对象,方便后面处理路径
        json_path = Path(json_path)

        # 根据 0.json 自动找 0.jpg / 0.png 等
        img_path = self._find_image(json_path)

        # 如果找不到图片,则跳过
        if img_path is None:
            print(f"[WARN] skip {json_path}: image not found")
            return None

        # 读取 LabelMe json 标注
        ann = json.loads(json_path.read_text(encoding="utf-8"))

        # 用 OpenCV 读取图片
        img = cv2.imread(str(img_path))

        # 如果图片读取失败,直接报错
        if img is None:
            raise ValueError(f"cannot read image: {img_path}")

        # 从真实图片中获取高和宽
        # OpenCV 读出来的 shape 是 [高度, 宽度, 通道数]
        real_h, real_w = img.shape[:2]

        # 优先使用 json 中记录的 imageHeight 和 imageWidth
        # 如果 json 里没有,就使用真实图片尺寸
        orig_h = ann.get("imageHeight", real_h)
        orig_w = ann.get("imageWidth", real_w)

        # 计算 smart_resize 后的新尺寸
        # 例如 512x512 -> 504x504
        new_h, new_w = self._smart_resize(orig_h, orig_w)

        # 用于保存当前图片中的所有建筑物目标
        # 最终会变成:
        # [
        #   {"bbox_2d":[...],"label":"building"},
        #   {"bbox_2d":[...],"label":"building"}
        # ]
        objects = []

        # 遍历 LabelMe 中所有标注 shape
        for shape in ann.get("shapes", []):

            # 只处理 rectangle 标注
            # 如果是 polygon / circle / line,则跳过
            if shape.get("shape_type") != "rectangle":
                continue

            # 只保留 label 等于 target_label 的目标
            # 你的 target_label 是 building
            if shape.get("label") != self.target_label:
                continue

            # 获取 rectangle 的两个点
            points = shape.get("points", [])

            # rectangle 至少应该有两个点
            if len(points) < 2:
                continue

            # 把 LabelMe 的两个点转成 [xmin, ymin, xmax, ymax]
            bbox = self._shape2xyxy(points)

            # 因为图片要从原尺寸 resize 到 new_h,new_w,
            # 所以 bbox 也要同步缩放到新图坐标
            bbox = self._map_bbox(
                bbox=bbox,
                oh=orig_h,
                ow=orig_w,
                nh=new_h,
                nw=new_w,
            )

            # 加入当前图片的 objects 列表
            objects.append({
                "bbox_2d": bbox,
                "label": "building"
            })

        # 这里严格仿照甲下损坏项目逻辑:
        # 如果当前图片没有任何目标框,就不写入训练集。
        #
        # 如果你以后想加入“无建筑物负样本”,
        # 可以把下面两行注释掉,
        # 这样 objects=[] 也会写入 JSONL。
        if not objects:
            return None

        # 对原图进行 resize
        # 注意 cv2.resize 的尺寸参数顺序是 (宽, 高)
        img_res = cv2.resize(
            img,
            (new_w, new_h),
            interpolation=cv2.INTER_LINEAR
        )

        # 如果指定了保存目录,就把 resize 后图片保存进去
        if save_img_dir:
            # 转成 Path 对象
            save_img_dir = Path(save_img_dir)

            # 如果目录不存在,就自动创建
            save_img_dir.mkdir(parents=True, exist_ok=True)

            # resize 后图片保存路径
            # 比如 save_img_dir/0.jpg
            save_path = save_img_dir / img_path.name

            # 保存 resize 后图片
            cv2.imwrite(str(save_path), img_res)

        else:
            # 如果没有指定保存目录,就使用原图路径
            # 但是注意:bbox 已经是 resize 后坐标,
            # 所以正式训练时不建议 save_img_dir=None
            save_path = img_path

        # 构造写入 JSONL 的图片路径
        if image_rel_prefix:
            # 如果设置了相对路径前缀,就写成:
            # qwen_det_building/resized_imgs/0.jpg
            image_path_for_jsonl = f"{image_rel_prefix}/{save_path.name}".replace("\\", "/")
        else:
            # 否则就写真实保存路径
            image_path_for_jsonl = str(save_path).replace("\\", "/")

        # 构造 LLaMA-Factory / Qwen2.5-VL 训练样本
        sample = {
            # 样本 id,使用图片文件名,不带后缀
            # 例如 0.jpg -> id = "0"
            "id": img_path.stem,

            # 图片路径列表
            # Qwen2.5-VL 多模态格式中 images 一般是列表
            "images": [image_path_for_jsonl],

            # 对话数据
            # user 是输入提示词
            # assistant 是模型应该学习输出的 bbox JSON 字符串
            "messages": [
                {
                    "role": "user",
                    "content": self.user_prompt
                },
                {
                    "role": "assistant",

                    # 注意:
                    # assistant 的 content 必须是字符串,
                    # 所以这里用 json.dumps 把 objects 转成 JSON 字符串。
                    #
                    # separators=(",", ":") 的作用是压缩空格,
                    # 让输出更紧凑:
                    # [{"bbox_2d":[1,2,3,4],"label":"building"}]
                    "content": json.dumps(
                        objects,
                        ensure_ascii=False,
                        separators=(",", ":")
                    )
                },
            ],
        }

        return sample

    def process_batch(
        self,
        json_dir: str,
        out_jsonl: str,
        save_img_dir: Optional[str] = None,
        image_rel_prefix: Optional[str] = None,
    ):
        """
        批量处理一个目录下的所有 LabelMe json。

        输入:
            json_dir:
                原始数据目录,例如:
                building_gt_bbox/gt

            out_jsonl:
                输出 JSONL 文件路径,例如:
                building_gt_bbox/qwen_det_building/train_part.jsonl

            save_img_dir:
                resize 后图片保存目录,例如:
                building_gt_bbox/qwen_det_building/resized_imgs

            image_rel_prefix:
                写入 JSONL 的图片相对路径前缀,例如:
                qwen_det_building/resized_imgs

        输出:
            一个 jsonl 文件。
            每一行是一张图的训练样本。
        """

        # 找到 json_dir 目录下所有 .json 文件
        # 例如 building_gt_bbox/gt/*.json
        json_files = sorted(glob.glob(os.path.join(json_dir, "*.json")))

        # 如果一个 json 都没找到,说明路径写错或目录为空
        if not json_files:
            raise ValueError(f"no json found in {json_dir}")

        # 确保输出 jsonl 的父目录存在
        Path(out_jsonl).parent.mkdir(parents=True, exist_ok=True)

        # 如果指定了 resize 后图片目录,也提前创建
        if save_img_dir:
            Path(save_img_dir).mkdir(parents=True, exist_ok=True)

        # 记录成功写入了多少条样本
        written = 0

        # 打开输出 jsonl 文件
        with open(out_jsonl, "w", encoding="utf-8") as fw:

            # 遍历每个 LabelMe json
            for jf in json_files:

                # 处理单个 json
                sample = self.process_one(
                    json_path=jf,
                    save_img_dir=save_img_dir,
                    image_rel_prefix=image_rel_prefix,
                )

                # 如果成功得到样本,就写入 jsonl
                if sample:
                    # 每一行是一个完整 JSON
                    fw.write(json.dumps(sample, ensure_ascii=False) + "\n")

                    # 计数加 1
                    written += 1

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


# ============================================================
# 使用示例
# ============================================================

if __name__ == "__main__":
    # 创建处理器
    proc = Qwen2_5VLProcessor(
        # 只保留 LabelMe 中 label == "building" 的框
        target_label="building",

        # 你的原图是 512x512。
        # 设置为 504 是为了让输出图像尺寸是 28 的倍数:
        # 504 = 28 * 18
        long_edge_range=(504, 504),
    )

    # 批量转换
    proc.process_batch(
        # 原始数据目录:
        # 里面同时有 0.jpg、0.json、1.jpg、1.json
        json_dir="../data/building_gt_bbox/gt",

        # 输出 JSONL 文件
        out_jsonl="../data/building_gt_bbox/qwen_det_building/train_part.jsonl",

        # resize 后图片保存目录
        save_img_dir="../data/building_gt_bbox/qwen_det_building/resized_imgs",

        # 写入 JSONL 中 images 字段的图片路径前缀
        #
        # 生成后,每条样本里会是:
        # "images": ["qwen_det_building/resized_imgs/0.jpg"]
        #
        # 如果你后续把 qwen_det_building 整个文件夹放到:
        # LLaMA-Factory/data/
        # 那这个路径就是相对于 data/ 的路径。
        image_rel_prefix="building_gt_bbox/qwen_det_building/resized_imgs",
    )

直接运行:✅ 完成!共转换 10/10 条样本 -> …/data/building_gt_bbox/qwen_det_building/train_part.jsonl

查看结果:

请添加图片描述

请添加图片描述

504*504尺寸影像

请添加图片描述

json文件内容

✅ 转换成功

接下来查看转化的是否正确,需要可视化检查

import json
import cv2
from pathlib import Path
import matplotlib.pyplot as plt

jsonl_path = Path("../data/building_gt_bbox/qwen_det_building/train_part.jsonl")
img_prefix = Path("../data")   # jsonl 中图片路径前面加这个
max_show = 10                  # 显示前 10 张;想全部显示就设为 0

with open(jsonl_path, "r", encoding="utf-8") as f:
    lines = f.read().strip().splitlines()

if max_show > 0:
    lines = lines[:max_show]

for i, line in enumerate(lines, 1):
    sample = json.loads(line)

    img_path = img_prefix / sample["images"][0]
    objects = json.loads(sample["messages"][1]["content"])

    img = cv2.imread(str(img_path))
    if img is None:
        print(f"读取失败:{img_path}")
        continue

    for obj in objects:
        x1, y1, x2, y2 = map(int, obj["bbox_2d"])
        label = obj.get("label", "building")

        cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
        cv2.putText(
            img,
            label,
            (x1, max(0, y1 - 5)),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.5,
            (0, 0, 255),
            1
        )

    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    plt.figure(figsize=(6, 6))
    plt.imshow(img_rgb)
    plt.title(f"{i}: {img_path.name} | buildings: {len(objects)}")
    plt.axis("off")
    plt.show()

在这里插入图片描述

发现是正确的,可以进行下一步

(3)模型训练测试

1)注册数据集

首先需要将上述转化得到的 jsonl 文件移动到 dataset_info.josn 同级目录

在这里插入图片描述

并注册到 dataset_info.josn 中

"rs_building": {
    "file_name": "train_rs_building.jsonl",
    "formatting": "sharegpt",
    "columns": {
      "messages": "messages",
      "images": "images"
    },
    "tags": {
      "role_tag": "role",
      "content_tag": "content",
      "user_tag": "user",
      "assistant_tag": "assistant"
    }
  },

2)训练
首先利用llamafactory-cli webui 进行web页面
然后在训练参数中选择QWen2.5VL_3B_instruct模型(或下载到本地,保存到之前的model文件夹中)
在这里插入图片描述

其他参数:

  • 模型:qwen2.5vl
  • 数据集:rs_building
  • epoch:由于数据量太少,这里设置 40
  • 梯度累计:1

训练日志

请添加图片描述
请添加图片描述
请添加图片描述

3)测试

需要用 resize 之后的数据去测试
在这里插入图片描述
question:

请检测输入遥感影像中的所有建筑物,并仅以 JSON 数组格式返回结果

answer:

[{“bbox_2d”:[231,121,299,209],“label”:“building”},{“bbox_2d”:[228,212,328,287],“label”:“building”},{“bbox_2d”:[324,192,463,278],“label”:“building”},{“bbox_2d”:[349,283,498,344],“label”:“building”},{“bbox_2d”:[9,303,100,367],“label”:“building”},{“bbox_2d”:[7,370,90,485],“label”:“building”},{“bbox_2d”:[116,316,196,407],“label”:“building”},{“bbox_2d”:[207,330,298,404],“label”:“building”},{“bbox_2d”:[98,403,179,491],“label”:“building”},{“bbox_2d”:[189,419,292,501],“label”:“building”},{“bbox_2d”:[320,348,424,417],“label”:“building”},{“bbox_2d”:[304,434,365,492],“label”:“building”},{“bbox_2d”:[2,184,33,243],“label”:“building”},{“bbox_2d”:[442,357,502,431],“label”:“building”}]

4)与原始模型对比验证

进入 chat 模式,分别加载原始模型和微调后 lora 模型,分开测试,并将测试结果可视化对比:

由于训练数据只有十张,且为自己标注,标签精度不高,所以微调的模型效果也一般,但是也有了质的提升。
在这里插入图片描述

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐