一、概述

opentrafficmonitoringplus是一个车辆检测的工作,可以根据视频或图像输入识别出车辆。

该项目是基于 Detectron2 的实现,在此基础上补充了无人机视角的航拍图像进行的车辆位置检测、追踪与估计。

项目来源:GitHub - fkthi/OpenTrafficMonitoringPlus

以下是项目来源提供的运行环境参考(非本教程使用环境):

Detectron2, Python 3.6 or 3.7 and packages listed in
Tested on:requirements.txt

  • Ubuntu 18.04, Python 3.6.9, torch 1.4.0, torchvision 0.5.0, CUDA 10.2, Nvidia 440.x drivers
  • Intel Xeon E-2176G, 32 GB RAM, GeForce RTX 2080 Ti, M.2 SDD

二、快速开始

运行环境

类别 详细信息
CPU 6 vCPU Intel(R) Xeon(R) Gold 6130 CPU @ 2.10GHz
CPU 核心数 16核心
GPU V100
GPU 显存 32 GB
CUDA 版本 10.2
操作系统 Linux 5.19.0-50-generic x86_64
Python 版本 3.7
PyTorch 版本 1.8.0

源码下载

wget https://mirrors.aheadai.cn/scripts/OpenTrafficMonitoringPlus.zip
unzip OpenTrafficMonitoringPlus.zip
cd OpenTrafficMonitoringPlus-master

conda环境创建

conda create --name opentraffic python==3.7
conda activate opentraffic

pytorch与cuda下载

conda install pytorch==1.8.0 torchvision==0.9.0 torchaudio==0.8.0 cudatoolkit=10.2 -c pytorch

detectron2安装

这里不能使用conda安装,因为这个库未被conda收录

可根据自己不同的cuda版本参考文档:Installation — detectron2 0.6 documentation

python -m pip install detectron2 -f \
  https://dl.fbaipublicfiles.com/detectron2/wheels/cu102/torch1.8/index.html

环境配置

由于github自带的环境本身就存在冲突,并且有一些包名也已经更改。

因此打开requirements.txt文件,将里面的内容修改如下:

opencv-python==3.4.8.29
opencv-contrib-python==3.4.8.29
tqdm
shapely
scikit-learn<=0.23.2

运行命令:

pip install -r requirements.txt

下载初始权重

wget https://mirrors.aheadai.cn/scripts/model_final_FHD_50kIt_bs2_noAug_790img.pth
mv model_final_FHD_50kIt_bs2_noAug_790img.pth ./src/maskrcnn
cd src

尝试推理

用文件夹中已存在的视频文件进行推理测试:

python main.py --videos=./videos/:./out/

运行过程中可以看到输出结果:

**********************************该帧所包含掩码个数: 1 ****************************************
       Predicting Masks       :  72%|██████████████████████████████████████████████████████████████████████████████▏                              | 201/280 [00:24<00:09,  8.38it/s]**********************************该帧所包含掩码个数: 1 ****************************************
       Predicting Masks       :  72%|██████████████████████████████████████████████████████████████████████████████▋                              | 202/280 [00:24<00:09,  8.40it/s]**********************************该帧所包含掩码个数: 1 ****************************************
       Predicting Masks       :  72%|███████████████████████████████████████████████████████████████████████████████                              | 203/280 [00:24<00:09,  8.39it/s]       Predicting Masks       :  72%|███████████████████████████████████████████████████████████████████████████████                              | 203/280 [00:24<00:09,  8.21it/s]

三、微调教程

下载微调训练数据:

wget https://mirrors.aheadai.cn/data/opentraffic_dataset_train.zip
mkdir dataset_train
unzip opentraffic_dataset_train.zip -d dataset_train/

如果只是试一下预训练的效果,可以跳过 “使用自己的数据集” 这一步,这里提供的数据集就是经过下面的各个步骤所得到的

使用自己的数据集:

如果有自己的数据集要进行微调也可以尝试,对应的cocojson标签的生成过程:

1、打开网站https://www.robots.ox.ac.uk/~vgg/software/via,打标签后生成VIAjson标签via_export_coco.json

2、通过以下脚本将via_export_coco.json转化为coco格式的json标签文件coco_format_output.json:

import json
import numpy as np
import pycocotools.mask as mask

# 读取VIA格式的JSON文件
via_file = 'via_export_coco.json'  # 替换为你的VIA文件路径
with open(via_file, 'r') as f:
    via_data = json.load(f)

# 创建一个空的COCO格式数据
coco_format = {
    "images": [],
    "annotations": [],
    "categories": []
}

# 假设只有一个类别 "car",并且类别的ID为1
categories = [{"id": 1, "name": "car"}]
coco_format["categories"] = categories

# 初始化image_id和annotation_id
image_id = 1
annotation_id = 1

# 遍历所有图像和其对应的标注信息
for fid, file_info in via_data["file"].items():
    file_name = file_info["fname"]

    # 假设图片的尺寸(你可以根据实际情况调整这些值)
    height = 3840  # 假设图片高度
    width = 2160   # 假设图片宽度

    # 创建COCO格式的图像信息
    image_metadata = {
        "id": image_id,
        "file_name": file_name,
        "height": height,
        "width": width,
    }

    # 添加图像信息到COCO格式
    coco_format["images"].append(image_metadata)

    # 提取标注信息
    for annotation_key, annotation_value in via_data["metadata"].items():
        if annotation_value["vid"] == str(image_id):  # 匹配图像ID
            # 获取标注框信息(x, y, width, height)
            x, y, box_width, box_height = annotation_value["xy"][1:5]

            # 计算四个角的坐标,形成矩形多边形
            segmentation = [
                x, y,  # 左上角
                x + box_width, y,  # 右上角
                x + box_width, y + box_height,  # 右下角
                x, y + box_height  # 左下角
            ]

            # 创建COCO格式的标注信息
            coco_format["annotations"].append({
                "id": annotation_id,
                "image_id": image_id,
                "category_id": 1,  # 只有一个类别“car”
                "segmentation": segmentation,  # 直接用多边形坐标列表
                "area": box_width * box_height,  # 标注区域的面积
                "bbox": [x, y, box_width, box_height],  # 计算框的坐标和尺寸
                "iscrowd": 0
            })
            annotation_id += 1

    # 增加图像ID
    image_id += 1

# 将COCO格式数据保存为JSON文件
with open('coco_format_output.json', 'w') as f:
    json.dump(coco_format, f)

print("转换完成,COCO格式数据已保存为 'coco_format_output.json'")
dataset_train/via2coco_json
python via2coco.py
cd ../..

3、调用vgg_to_coco生成detectron2 coco格式标签文件transformed_annotations.json:

python vgg_to_coco.py 

4、将生成的文件transformed_annotations.json移动到数据集图片同一目录下

mv transformed_annotations.json ./dataset_train/drone_cars/

最后得到大致的数据集文件结构如下:

├── dataset_train
│   └── drone_cars
│       ├── frame_0000.jpg
│       ├── frame_0001.jpg
│       ├── frame_0002.jpg
│       ├── frame_0003.jpg
│       ├── frame_0004.jpg
│       ├── frame_0005.jpg
│       ├── frame_0006.jpg
...
│   │   ├── frame_0098.jpg
│   │   └── transformed_annotations.json
│   └── via2coco_json
│       ├── coco_format_output.json
│       ├── via2coco.py
│       └── via_export_coco.json

微调指令:

python train.py --dataset_train=./dataset_train/drone_cars/ --weights=./maskrcnn/model_final_FHD_50kIt_bs2_noAug_790img.pth 

命令行参数如下:

--weights
默认值:detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl
用途:指定预训练模型的权重路径,可以使用 Detectron2 提供的预训练模型,也可以加载自己训练的模型权重。

--dataset_train
默认值:./custom_data/train/
用途:指定训练数据集的路径,该路径下需要包含图像文件和 COCO 格式的标注文件(如 transformed_annotations.json)。

--dataset_eval
默认值:./custom_data/eval/
用途:指定验证数据集的路径,用于训练过程中评估模型性能。该路径同样需要包含图像文件和标注文件。

--config
默认值:./maskrcnn/mask_rcnn_R_50_FPN_1x.yaml
用途:指定模型的配置文件路径,包含网络结构和训练参数等详细信息。

--workers
默认值:0
用途:设置 PyTorch 数据加载器的工作线程数量,0 表示只使用主进程加载数据。可以根据硬件性能调整此值。

--batch_size
默认值:2
用途:每批次训练的数据样本数量。建议根据显存大小调整。

--lr
默认值:0.001
用途:模型的初始学习率,需根据任务和 batch size 调整。

--max_iter
默认值:30000
用途:最大训练迭代次数,控制训练总时长。

--eval_interval
默认值:10000
用途:训练过程中每隔多少步在验证集上运行评估。

--gamma
默认值:0.3
用途:学习率调整时的衰减系数,在指定步数后按该比例降低学习率。

--steps
默认值:(20000, 25000)
用途:指定学习率调整的步数,例如在 20000 和 25000 步时按 --gamma 值降低学习率。

--save_interval
默认值:5000
用途:训练时每隔指定步数保存模型权重,便于中断后恢复或选择最佳模型。

--warmup_iters
默认值:2000
用途:学习率预热的步数,在初始阶段逐步增加学习率,防止训练不稳定。

--freeze_at
默认值:2
用途:冻结网络的前几层(如 backbone 的前几个阶段),用于加速训练或减少过拟合。

--output_dir
默认值:./model_output/
用途:训练过程中模型权重和日志文件的保存路径。

--num_classes
默认值:1
用途:设置数据集中的目标类别数量(不包括背景类)。例如,如果只有一个类别(如"车"),则设为 1。

--resume
默认值:False
用途:是否从上一次训练的中断点恢复。如果设置为 True,会从 --output_dir 中加载最新的模型权重。

输出结果:

运行成功得到以下输出:

[12/10 17:00:04 d2.utils.events]:  eta: 14:04:41  iter: 179  total_loss: 1.409  loss_cls: 0.2494  loss_box_reg: 0.6586  loss_mask: 0.3173  loss_rpn_cls: 0.02784  loss_rpn_loc: 0.1113  time: 1.7044  data_time: 1.2705  lr: 9.0411e-05  max_mem: 4870M
[12/10 17:00:38 d2.utils.events]:  eta: 14:03:57  iter: 199  total_loss: 1.364  loss_cls: 0.2543  loss_box_reg: 0.6459  loss_mask: 0.316  loss_rpn_cls: 0.0218  loss_rpn_loc: 0.1052  time: 1.7038  data_time: 1.2667  lr: 0.0001004  max_mem: 4870M
[12/10 17:01:12 d2.utils.events]:  eta: 14:03:23  iter: 219  total_loss: 1.383  loss_cls: 0.2582  loss_box_reg: 0.6651  loss_mask: 0.3172  loss_rpn_cls: 0.01913  loss_rpn_loc: 0.1162  time: 1.7036  data_time: 1.2680  lr: 0.00011039  max_mem: 4870M
[12/10 17:01:46 d2.utils.events]:  eta: 14:02:49  iter: 239  total_loss: 1.307  loss_cls: 0.244  loss_box_reg: 0.6247  loss_mask: 0.303  loss_rpn_cls: 0.01778  loss_rpn_loc: 0.1063  time: 1.7031  data_time: 1.2652  lr: 0.00012038  max_mem: 4870M
[12/10 17:02:20 d2.utils.events]:  eta: 14:02:12  iter: 259  total_loss: 1.324  loss_cls: 0.2399  loss_box_reg: 0.618  loss_mask: 0.3044  loss_rpn_cls: 0.01611  loss_rpn_loc: 0.1092  time: 1.7032  data_time: 1.2705  lr: 0.00013037  max_mem: 4870M

四、吞吐量指标输出教程

运行指令:

conda install subprocess
conda install re

找到python3.7目录下第三方库site-packages中的detectron2中的train_loop.py脚本,将其TrainerBase类进行修改,可以得到相应的指标输出,具体修改内容如下:

1、构造函数的修改:

    def __init__(self) -> None:
        self._hooks: List[HookBase] = []
        self.iter: int = 0
        self.start_iter: int = 0
        self.max_iter: int
        self.storage: EventStorage
        self.step_times: List[float] = []  # 记录每步的时间
        self.throughputs: List[float] = []  # 记录吞吐量
        self.epoch_times: List[float] = []  # 记录每个epoch的时间
        self.gpu_memory_usages: List[float] = []  # 记录显存使用率
        self.powers: List[float] = []  # 记录每步的功耗
        self.device_id = torch.cuda.current_device() # 当前GPU编号
        _log_api_usage("trainer." + self.__class__.__name__)

2、train函数的修改:

 def train(self, start_iter: int, max_iter: int):
        """
        Args:
            start_iter, max_iter (int): See docs above
        """
        logger = logging.getLogger(__name__)
        logger.info("Starting training from iteration {}".format(start_iter))

        self.iter = self.start_iter = start_iter
        self.max_iter = max_iter

        with EventStorage(start_iter) as self.storage:
            try:
                self.before_train()
                epoch_start_time = time.time()
                for self.iter in range(start_iter, max_iter):
                    step_start_time = time.time()

                    # 查询显存和功率
                    gpu_memory = self.get_gpu_memory()  # 调用独立函数获取显存
                    power = self.get_gpu_power()  # 调用独立函数获取功率

                    self.gpu_memory_usages.append(gpu_memory)
                    self.powers.append(power)

                    self.before_step()
                    self.run_step()
                    self.after_step()

                    step_time = time.time() - step_start_time
                    self.step_times.append(step_time)
                    # 计算吞吐量
                    throughput = self.calculate_throughput(step_time)
                    self.throughputs.append(throughput)

                    # 打印每步的指标
                    self.print_metrics(step_time, throughput, gpu_memory, power)
                # 计算并输出每个epoch的时间
                epoch_time = time.time() - epoch_start_time
                self.epoch_times.append(epoch_time)

                # 最后输出每个指标的平均值
                self.print_average_metrics()
                # self.iter == max_iter can be used by `after_train` to
                # tell whether the training successfully finished or failed
                # due to exceptions.
                self.iter += 1
            except Exception:
                logger.exception("Exception during training:")
                raise
            finally:
                self.after_train()

3、添加新的方法:

    def calculate_throughput(self, step_time: float) -> float:
        """
        计算吞吐量,即每秒处理的样本数(Samples per Second)。
        假设每次训练步骤处理一个batch。
        """
        samples_per_step = 4  # 假设每次训练步骤处理2个样本
        return samples_per_step / step_time

    def print_metrics(self, step_time: float, throughput: float, gpu_memory: float, power: float) -> None:
        """
        打印当前step的训练指标。
        """
        print(f"Step Time: {step_time:.4f} s, Throughput: {throughput:.2f} samples/s, "
              f"GPU Memory: {gpu_memory:.2f} MB, Power: {power:.2f} W")

    def print_average_metrics(self) -> None:
        """
        打印训练结束后的平均指标。
        """
        avg_step_time = mean(self.step_times)
        avg_throughput = mean(self.throughputs)
        avg_epoch_time = mean(self.epoch_times)
        avg_gpu_memory = mean(self.gpu_memory_usages)
        avg_power = mean(self.powers)

        print(f"Average Step Time: {avg_step_time:.4f} s")
        print(f"Average Throughput: {avg_throughput:.2f} samples/s")
        print(f"Average Epoch Time: {avg_epoch_time:.2f} s")
        print(f"Average GPU Memory Usage: {avg_gpu_memory:.2f} MB")
        print(f"Average Power: {avg_power:.2f} W")

        # 将结果输出到log.txt文件
        with open("log.txt", "a") as log_file:
            log_file.write(f"Average Step Time: {avg_step_time:.4f} s\n")
            log_file.write(f"Average Throughput: {avg_throughput:.2f} samples/s\n")
            log_file.write(f"Average Epoch Time: {avg_epoch_time:.2f} s\n")
            log_file.write(f"Average GPU Memory Usage: {avg_gpu_memory:.2f} MB\n")
            log_file.write(f"Average Power: {avg_power:.2f} W\n")
            log_file.write("\n")

       def get_gpu_power(self) -> float:
        """
            使用 nvidia-smi 获取当前 GPU 的功耗(瓦特)。
            """
        import subprocess
        import re

        # 使用 nvidia-smi 查询功率
        result = subprocess.run(
            ["nvidia-smi", "--query-gpu=power.draw", "--format=csv,noheader,nounits"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )

        if result.returncode != 0:
            raise RuntimeError(f"nvidia-smi error: {result.stderr}")

        power_draws = [float(re.search(r"[\d.]+", line).group()) for line in result.stdout.strip().split("\n")]
        return power_draws[self.device_id]  # 当前设备的功率(W)

    def get_gpu_memory(self) -> float:
        """
        使用 nvidia-smi 获取当前 GPU 的显存使用情况(单位:MiB)。
        """
        import subprocess
        import re

        # 使用 nvidia-smi 查询显存
        result = subprocess.run(
            ["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )

        if result.returncode != 0:
            raise RuntimeError(f"nvidia-smi error: {result.stderr}")

        # 解析显存信息
        gpu_memories = [int(re.search(r"\d+", line).group()) for line in result.stdout.strip().split("\n")]
        return gpu_memories[self.device_id]  # 返回当前设备显存使用(MiB)

修改成功后可以在运行时在终端看到以下输出:

(batch_size = 4,修改batch_size时calculate_throughput方法也要修改)

python train.py --dataset_train=./dataset_train/drone_cars/ --weights=./maskrcnn/model_final_FHD_50kIt_bs2_noAug_790img.pth --batch_size 4 --save_interval 1000
[12/12 14:22:13 d2.utils.events]:  eta: 1 day, 1:25:49  iter: 3479  total_loss: 0.4996  loss_cls: 0.08558  loss_box_reg: 0.2394  loss_mask: 0.1362  loss_rpn_cls: 0.001144  loss_rpn_loc: 0.03836  time: 3.4450  data_time: 2.4815  lr: 0.001  max_mem: 8925M
Step Time: 3.7019 s, Throughput: 1.08 samples/s, GPU Memory: 10638.00 MB, Power: 40.77 W
Step Time: 3.6123 s, Throughput: 1.11 samples/s, GPU Memory: 10638.00 MB, Power: 40.58 W
Step Time: 3.7199 s, Throughput: 1.08 samples/s, GPU Memory: 10638.00 MB, Power: 40.66 W
Step Time: 3.6326 s, Throughput: 1.10 samples/s, GPU Memory: 10638.00 MB, Power: 40.58 W
Step Time: 3.6540 s, Throughput: 1.09 samples/s, GPU Memory: 10638.00 MB, Power: 40.57 W
Step Time: 3.6430 s, Throughput: 1.10 samples/s, GPU Memory: 10638.00 MB, Power: 40.57 W
Step Time: 3.7828 s, Throughput: 1.06 samples/s, GPU Memory: 10638.00 MB, Power: 40.67 W
^C[12/12 14:22:37 d2.engine.hooks]: Overall training speed: 1484 iterations in 1:25:13 (3.4459 s / it)
[12/12 14:22:37 d2.engine.hooks]: Total training time: 1:31:39 (0:06:25 on hooks)
wget https://mirrors.aheadai.cn/log/opentraffic_metrics.json #日志输出
本文系作者 @ admin 原创发布在 文档中心 | AheadAI ,未经许可,禁止转载。