十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BIT-Vehicle数据集详解:从.mat标注到YOLO/VOC/COCO格式转换

BIT-Vehicle数据集详解:从.mat标注到YOLO/VOC/COCO格式转换 做目标检测的兄弟应该都有同感跑通一个模型不难真正让人头大的是数据。特别是做车型识别这类细分任务能公开下载、标注规整的数据集本来就不多。我最早用的是CompCar但那东西标注格式比较乱下载还老是断点。后来换到BIT-Vehicle才算是把训练流程理顺了。这篇就把BIT-Vehicle数据集的介绍、下载方式以及从原始.mat标注转到YOLO/VOC/COCO三种训练格式的完整方案过一遍重点是你拿到手之后怎么少走弯路。这篇东西适合谁看一是刚入门目标检测、想做车型识别或者车辆检测的二是手头有.mat格式标注数据、不知道怎么转成主流框架格式的三是想用BIT-Vehicle跑YOLOv5/YOLOv8但卡在数据准备这一步的。我尽量把操作细节都写出来包括一些脚本和踩坑记录你照着做基本能跑通。1. BIT-Vehicle数据集是什么一个容易被低估的车型识别数据集1.1 数据集背景与基本信息BIT-Vehicle全称BIT Vehicle Dataset是北京理工大学智能信息技术实验室公开的车辆检测数据集。它发布于2015年前后整个数据集包含9280张真实道路场景下的车辆图片分辨率统一为1600像素乘1200像素覆盖白天、夜晚、阴天等多种光照条件采集地点是北京多条城市道路。对比很多车辆数据集动辄几万张图片的规模9280张看起来不算大但这个数据集的优势在于类别划分清楚、遮挡少、每张图片的车辆主体突出用来做车型分类或者单类目标检测的训练起点是完全够用的。数据集一共定义了6个车型类别类别名称中文含义典型特征Bus公共汽车车体长、高度高、车身方正Microbus面包车短头、整体像方盒子Minibus小客车/中巴介于面包车和客车之间Sedan轿车三厢或两厢占比最多SUV越野车/城市SUV车身较高、离地间隙大Truck卡车/货车车厢和驾驶室分离明显从样本分布来看Sedan占了绝对多数这也是BIT-Vehicle的一个特点类别不均衡。这会导致一个问题如果直接拿去训练模型对轿车会拟合得非常好但对Microbus、Truck这类样本量比较少的类别就识别得一般这个后面我会专门讲怎么处理。1.2 数据集的天然特点与使用价值说它容易被低估是因为很多初学者嫌它图片少、背景单一转身就去用那些几十万张的数据集。但实际上BIT-Vehicle有个非常友好的点它的标注信息是以Matlab的.mat文件集中保存的而且绝大多数图片中只有一辆主要车辆目标尺寸大、位置居中不像COCO那种一张图十几个小目标训练难度成倍上升。对刚接触目标检测的人来说拿这个数据集跑通一套YOLO训练流程比直接用COCO要友好太多。另外它还包含了不少夜间场景。夜间图片的光线暗、灯光明暗差异大这其实是一个很有价值的难点。如果你的应用场景除了白天还有夜间监控BIT-Vehicle的这些夜间样本就是天然的增广数据不用你再费劲去采集。我自己的项目里就有一半场景是夜间所以这份数据集对我来说比很多新数据集都实用。从标注格式来看BIT-Vehicle原始给的是.mat文件里面包含了每张图片的车辆包围框坐标和类别标签。问题是这个格式既不是VOC XML也不是YOLO txt更不是COCO JSON主流的训练框架根本吃不了。所以拿到数据集之后第一件事就是做格式转换这也是这篇文章最核心的部分。2. 下载这件事比想象中要绕一点2.1 官方渠道的申请流程BIT-Vehicle的官方页面在北理工智能信息技术实验室的网站下早期是直接提供下载链接后来改成了邮件申请制。如果你从官网下载流程大致是这样打开BIT-Vehicle的官方介绍页面找到申请说明。给页面上的联系人邮箱发一封申请邮件说明你的姓名、单位、用途、联系方式。邮件内容建议写清楚是用于学术研究并承诺不会将数据集再分发。等待对方回复下载链接和密码这个周期不确定快的话一两天慢的话可能一个星期。这里我贴一个我当年用的邮件模板你可以直接改改信息发过去To: BIT-Vehicle联系邮箱 Subject: Request for BIT-Vehicle Dataset Dear Sir/Madam, I am a researcher/engineer working on vehicle type recognition and objective detection. I would like to request the BIT-Vehicle Dataset for academic research purposes. I have read and agreed to the dataset license terms, and I will not redistribute the dataset to any third party. My details: Name: [你的姓名] Affiliation: [你的单位/学校] Email: [联系方式] Purpose: [具体用途比如训练YOLO实现车型识别] Looking forward to your reply. Best regards, [你的姓名]有两点提醒第一国内有些单位邮箱对国外或者跨域邮件有拦截发出之后记得留意垃圾箱第二如果官网页面本身打开都比较慢别硬等直接用下面的镜像渠道更省事。2.2 更省事的镜像渠道因为BIT-Vehicle发布年数比较久了现在有很多镜像渠道可以下载。我在Kaggle上见过BITVehicle的镜像直接搜索BITVehicle就能找到下载下来是一个压缩包解压后是image文件夹加VehicleInfo.mat和官方结构一致。国内的一些AI数据集平台上也有人传过你搜一下也能找到。用镜像下载有一点要注意一定先验证文件完整性再去跑训练。最直观的验证方式是数一下图片数量应该是9280张jpgVehicelInfo.mat的尺寸大约在220MB左右。如果图片数量对不上或者.mat文件很小那很可能是被截断或者压缩有损后面解析标注的时候就会发现坐标对不上图片白折腾一场。2.3 下载后的目录结构下载解压之后的标准目录结构如下BITVehicle/ ├── image/ │ ├── 00001.jpg │ ├── 00002.jpg │ ├── 00003.jpg │ └── ... └── VehicleInfo.matimage目录下就是全部图片文件名从00001.jpg开始递增。VehicleInfo.mat是Matlab格式的结构体文件里面保存了每一张图片的标注信息包括车辆包围框和类别。你需要用Python或者Matlab去解析这个文件。我强烈建议你拿到手之后先写几行代码把mat结构打印出来看一眼因为不同渠道下载的.mat细节字段名可能不一样不要想当然认为结构一定相同。3. 为什么要转格式YOLO/VOC/COCO三种格式的差异3.1 三种格式到底存的是什么在动手写转换脚本之前先把三种格式的本质搞清楚不然脚本逻辑容易写错。先说VOC格式VOCVisual Object Classes是Pascal VOC挑战赛的定义它用XML文件描述每一张图片每个XML里包含图片文件名、尺寸以及每一个目标的类别和包围框包围框是像素坐标系下的xmin、ymin、xmax、ymax四个整数也就是左上角和右下角的坐标。YOLO格式是Darknet和Ultralytics系列框架使用的纯文本格式。每张图片对应一个同名txt文件txt文件里每一行代表一个目标格式是class_id x_center y_center width height注意这四项全部是归一化到0到1之间的小数而且x_center和y_center是目标中心点的归一化坐标width和height是目标宽高的归一化值。归一化就是除以图片的宽或高别把宽度除以了高度这是新手最容易犯的错。COCO格式则是MS COCO数据集使用的JSON聚合格式。它不是一张图一个文件而是把所有图片和所有标注信息放在一个大的JSON文件里包含info、licenses、images、annotations、categories五个主要字段。其中annotations里的每一条包含image_id、category_id、bbox这是COCO专用的[x, y, width, height]像素坐标以及area、iscrowd等字段。用一个表格对照起来看会更清楚对比项VOCYOLOCOCO文件形态每图一个XML每图一个txt全数据一个大JSON坐标类型像素整数归一化小数像素整数坐标含义xmin,ymin,xmax,ymaxx_center,y_center,w,hx,y,w,h典型工具LabelImgLabelImg/YOLO框架Labelme/MMDetection阅读难度中等简单略复杂3.2 为什么必须转换而不是直接喂给YOLO你可能想既然BIT-Vehicle原始标注是.mat那能不能写个自定义Dataset类在训练的时候直接读.mat省去转换这一步理论上可以实际上非常不推荐。原因有三条第一YOLO系列框架的官方训练流程默认扫描同类标注文件VOC就是xmlUltralytics系列就是txt你改了标注读取方式就等于魔改框架以后每次换版本都要重改维护成本极高。第二标注工具生态几乎只认VOC/COCO/YOLO这三种格式转过去之后你方便用OpenCV可视化检查标注也方便在LabelImg里二次修正。第三和其他数据集混合训练的时候统一格式才能直接合并目录这个在工程上特别重要。所以老老实实先把数据转好这是成本最低、最可控的路线。4. 手写一套稳健的转换脚本从BIT-Vehicle到VOC/YOLO/COCO4.1 先解析.mat文件看清标注结构首先用Python读取VehicleInfo.mat。老版本Matlab保存的是v5格式scipy.io.loadmat可以直接读如果你下载到的.mat是Matlab 7.3以上版本scipy会报错这时候改用h5py读取。我建议代码里做一个兼容判断import scipy.io import numpy as np try: mat scipy.io.loadmat(VehicleInfo.mat) VehicleInfo mat[VehicleInfo] print(使用scipy.io.loadmat读取成功) except NotImplementedError: import h5py with h5py.File(VehicleInfo.mat, r) as f: VehicleInfo f[VehicleInfo] print(使用h5py读取成功) print(VehicleInfo.dtype)读取之后VehicleInfo是一个结构体数组数组长度等于图片数量。你需要遍历每个元素把里面的图片路径、车辆包围框、类别标签提取出来。以我手里的版本为例它的结构大致是VehicleInfo[0] 对应第一张图片 字段0图片文件名 字段1该图片中的车辆信息可能是嵌套数组 字段2附加标签不过不同渠道下载的版本字段顺序确实存在差异所以我不建议你直接照抄索引而是先打印几行看看for i in range(3): print(--- 样本, i, ---) print(VehicleInfo[i])看到实际输出之后再根据字段位置写提取逻辑。这一步多花五分钟能帮你后面省下大半天排查时间。4.2 把标注提取成统一的内存结构为了转换到三种格式都不费劲最好先把所有标注读到一个统一的Python字典里结构如下dataset [ { image: 00001.jpg, width: 1600, height: 1200, objects: [ {name: Sedan, bbox: [x1, y1, x2, y2]}, {name: Bus, bbox: [x1, y1, x2, y2]}, ] }, ... ]我建议这一步不要省略。很多人在转换脚本里直接从.mat一路写到txt中途没有任何中间结构一旦格式要求变了整个脚本重写。抽出一个统一结构之后VOC、YOLO、COCO的生成逻辑都变成了从统一结构到目标格式的纯映射非常清晰。读取mat并构建统一结构的伪代码如下import scipy.io import os mat scipy.io.loadmat(VehicleInfo.mat) VehicleInfo mat[VehicleInfo] class_names [Bus, Microbus, Minibus, Sedan, SUV, Truck] dataset [] for item in VehicleInfo: # 请根据实际打印结果替换下面这一行的字段索引 filename item[0][0] info item[1] # 这是一个包含多辆车信息的数组 objects [] for obj in info: # obj中可能包含bbox和label bbox obj[0] # [x1, y1, x2, y2] 或 [x, y, w, h]以实际打印为准 label obj[1][0] objects.append({name: label, bbox: bbox}) dataset.append({ image: filename, width: 1600, height: 1200, objects: objects })注意如果mat中是[x, y, w, h]而不是[x1, y1, x2, y2]转换公式要调整。这也是先说统一结构的好处你只需要在这一层的提取逻辑里把坐标转成[x1, y1, x2, y2]后面三种格式的生成代码完全不用管这件事。4.3 划分训练集和验证集数据转换前先划分训练集和验证集。我建议按8:2的比例随机划分同时固定随机种子保证可复现import random random.seed(42) indices list(range(len(dataset))) random.shuffle(indices) train_indices indices[:int(len(indices) * 0.8)] val_indices indices[int(len(indices) * 0.8):] print(f训练集图片数: {len(train_indices)}) print(f验证集图片数: {len(val_indices)})这么做的原因是BIT-Vehicle的图片相邻编号有时候来自同一场景如果只按前80%后20%切分可能造成验证集和训练集图片过于相似最终验证指标虚高。随机打乱后能有效缓解这个问题。4.4 生成VOC格式XML文件VOC格式的XML文件结构是固定的可以直接用Python标准库xml.etree.ElementTree来写不需要额外依赖import xml.etree.ElementTree as ET def write_voc_xml(record, output_dir): annotation ET.Element(annotation) folder ET.SubElement(annotation, folder) folder.text BITVehicle filename ET.SubElement(annotation, filename) filename.text record[image] source ET.SubElement(annotation, source) database ET.SubElement(source, database) database.text BITVehicle size ET.SubElement(annotation, size) width ET.SubElement(size, width) width.text str(record[width]) height ET.SubElement(size, height) height.text str(record[height]) depth ET.SubElement(size, depth) depth.text 3 segmented ET.SubElement(annotation, segmented) segmented.text 0 for obj in record[objects]: object_elem ET.SubElement(annotation, object) name ET.SubElement(object_elem, name) name.text obj[name] pose ET.SubElement(object_elem, pose) pose.text Unspecified truncated ET.SubElement(object_elem, truncated) truncated.text 0 difficult ET.SubElement(object_elem, difficult) difficult.text 0 bndbox ET.SubElement(object_elem, bndbox) xmin ET.SubElement(bndbox, xmin) xmin.text str(int(obj[bbox][0])) ymin ET.SubElement(bndbox, ymin) ymin.text str(int(obj[bbox][1])) xmax ET.SubElement(bndbox, xmax) xmax.text str(int(obj[bbox][2])) ymax ET.SubElement(bndbox, ymax) ymax.text str(int(obj[bbox][3])) tree ET.ElementTree(annotation) xml_path os.path.join(output_dir, record[image].replace(.jpg, .xml)) tree.write(xml_path, encodingutf-8)这里生成VOC XML的目录结构一般是voc_format/ ├── Annotations/ │ ├── 00001.xml │ └── ... ├── JPEGImages/ │ ├── 00001.jpg │ └── ... └── ImageSets/ └── Main/ ├── train.txt └── val.txttrain.txt和val.txt里面存放的是不带扩展名的图片文件名每行一个。4.5 生成YOLO格式txt文件有了统一结构之后YOLO格式的转换非常简单核心就是归一化公式。我单独写一个函数def convert_bbox_to_yolo(bbox, img_w, img_h): x1, y1, x2, y2 bbox # 防止坐标越界或宽高为负 x1 max(0, min(x1, img_w - 1)) y1 max(0, min(y1, img_h - 1)) x2 max(0, min(x2, img_w - 1)) y2 max(0, min(y2, img_h - 1)) x_center (x1 x2) / 2.0 y_center (y1 y2) / 2.0 width x2 - x1 height y2 - y1 x_center / img_w width / img_w y_center / img_h height / img_h return x_center, y_center, width, height注意两个容易写错的地方第一x_center和width除以的是图片宽度y_center和height除以的是图片高度不能图省事统一除以一个数值。第二如果原始bbox是浮点数最好在x2 - x1这一步之前就限制范围否则后面归一化可能算出大于1的值甚至负数训练时会直接报错。生成YOLO标注txt的完整逻辑def write_yolo_txt(record, class_to_id, output_dir): lines [] for obj in record[objects]: class_id class_to_id[obj[name]] cx, cy, w, h convert_bbox_to_yolo(obj[bbox], record[width], record[height]) lines.append(f{class_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) txt_path os.path.join(output_dir, record[image].replace(.jpg, .txt)) with open(txt_path, w) as f: f.write(\n.join(lines))class_to_id这个映射要在一个固定的配置文件里定义好并且保证和最终训练时的data.yaml一致。我的建议是类别顺序按照下面这个固定列表class_names [Bus, Microbus, Minibus, Sedan, SUV, Truck] class_to_id {name: i for i, name in enumerate(class_names)}千万注意类别索引从0开始不是从1开始。很多人第一次转数据会踩这个坑训练的时候看起来没报错但其实类别全错位了。4.6 生成COCO格式JSON文件COCO格式相对复杂一点因为它是一个全局JSON里面要组装好images、annotations、categories三个列表的数据。images列表里每个图片有一个唯一的image_idannotations列表里每个标注有一个唯一的annotation_idcategories列表里记录类别id和名字。伪代码如下def build_coco(records, class_to_id): images [] annotations [] categories [{id: v, name: k, supercategory: vehicle} for k, v in class_to_id.items()] annotation_id 1 for img_id, rec in enumerate(records, start1): images.append({ id: img_id, file_name: rec[image], width: rec[width], height: rec[height] }) for obj in rec[objects]: x1, y1, x2, y2 obj[bbox] width x2 - x1 height y2 - y1 annotations.append({ id: annotation_id, image_id: img_id, category_id: class_to_id[obj[name]], bbox: [x1, y1, width, height], area: width * height, iscrowd: 0 }) annotation_id 1 return { info: { description: BIT-Vehicle converted to COCO format, version: 1.0 }, licenses: [{id: 1, name: Unknown, url: }], images: images, annotations: annotations, categories: categories }COCO的bbox格式是[x, y, width, height]而不是[x1, y1, x2, y2]转换的代码要放在最后。annotations的id必须从1开始并且全局递增不能每个图片重新从1开始否则用MMDetection或者Detectron2训练时会因为id冲突出问题。最后输出import json with open(train_coco.json, w) as f: json.dump(build_coco(train_records, class_to_id), f) with open(val_coco.json, w) as f: json.dump(build_coco(val_records, class_to_id), f)4.7 一键执行的入口函数整个转换流程可以封装成一个脚本传入BITVehicle根目录即可。我在实际使用中是把训练集和验证集分别输出到以下目录方便不同框架直接读取train/ ├── images/ │ ├── 00001.jpg │ └── ... ├── labels/ │ ├── 00001.txt │ └── ... ├── voc_xmls/ │ ├── 00001.xml │ └── ... └── train_coco.json val/ ├── images/ │ ├── 00010.jpg │ └── ... ├── labels/ │ ├── 00010.txt │ └── ... ├── voc_xmls/ │ ├── 00010.xml │ └── ... └── val_coco.json这样YOLO训练直接指定train/images和train/labelsPascal VOC训练指定voc_xmls和imagesCOCO训练指定train_coco.json一套数据三份标注互不干扰。5. 转换完之后别急着训练先做这一步验证5.1 可视化抽查标注脚本跑完后我建议你做的第一件事不是启动训练而是把标注画到图片上肉眼确认一下。这个习惯是我吃过几次亏之后养成的因为格式转换的bug往往藏在坐标和类别对应关系里光看数字很难发现。可视化代码非常简单import cv2 def visualize_bboxes(image_path, boxes, labels, class_names): img cv2.imread(image_path) for box, label in zip(boxes, labels): x1, y1, x2, y2 [int(v) for v in box] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[label], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(check, img) cv2.waitKey(0)重点抽查三类图片夜间低照度的、多辆车的、卡车和面包车的。我曾经在某个版本的.mat中发现Microbus和Minibus的标注顺序被调换了如果不做可视化这个错误会直接导致模型的混淆矩阵异常单看指标很难定位。5.2 常见坑排查清单根据我和身边同事的使用经验把BIT-Vehicle转换训练中最常遇到的现象和原因整理成一个表现象可能原因解决方法YOLO训练时提示label中的坐标超出0到1范围归一化时除错尺寸或原始坐标就超出图片范围在convert函数中先用max/min裁剪坐标验证集的mAP很低但loss正常训练集和验证集切分没打乱场景分布不一致设置固定seed随机划分类别预测结果全部错位类别索引从1开始了检查class_to_id是否从0开始读取VehicleInfo.mat直接报错Matlab 7.3以上的HDF5格式改用h5py读取COCO训练时id冲突或加载失败annotation_id重复检查全局递增逻辑图片数量与标注数量对不上镜像下载文件不完整重新下载并数图片数量除了这些还有一个比较隐蔽的问题VOC的XML格式要检查一下文件名和图片是否严格对应。BIT-Vehicle的图片文件名是比较规整的00001.jpg但如果你是从其他渠道拿到的可能带前后缀最好预先处理成纯数字命名避免后面训练和推理时路径解析出错。5.3 我踩过的一个典型坐标坑我自己第一次转这个数据集的时候就在坐标上栽过跟头。当时解析mat以为里面的坐标是[x, y, width, height]写完转YOLO的脚本跑出来的框怎么画怎么偏要么宽高变成原来的两倍要么位置整体往下移。排查了半天最后打印原始mat数据才发现它存的其实是[x1, y1, x2, y2]我用[x, y, w, h]的公式去套自然全错了。所以这里特意提醒大家拿到.mat文件之后先打印前三个样本的原始数据对照图片实际位置手算一遍再写转换逻辑这个时间花得绝对值得。6. 转好的数据怎么喂给YOLO训练从data.yaml到参数调节6.1 编写data.yaml配置文件使用Ultralytics系列YOLOv5/YOLOv8/YOLOv11训练时需要一个data.yaml文件来指定训练路径、验证路径、类别数和类别名。如果是用我上面生成的YOLO格式目录data.yaml可以这样写train: /path/to/train/images val: /path/to/val/images nc: 6 names: [Bus, Microbus, Minibus, Sedan, SUV, Truck]有一点要注意Ultralytics框架会自动查找与图片同名的txt文件存放位置和images目录同级或在其父目录下的labels目录中。如果你的目录结构和默认的不一致可以在data.yaml末尾加上两句path: /path/to/bitvehicle train: train/images val: val/images只要保证images目录下的00001.jpg对应labels目录下的00001.txt并且txt的每一行坐标是归一化后的格式YOLO就能直接训练。不少人在这一步遇到no labels found的报错八成就是目录结构不对或者图片和txt没有同名。6.2 训练参数与针对类别不均衡的调整BIT-Vehicle存在明显的类别不均衡再加上6个类别中Sedan占了过半直接开训的结果就是模型对Sedan的AP很高但对Microbus、Truck的AP掉得厉害。我的建议是在标准参数基础上做以下调整初始模型建议用yolov8s或者yolov5s不要一上来就试x版本。BIT-Vehicle只有9280张图大模型在小数据集上容易过拟合s版本参数量适中先跑通流程再决定要不要换大模型。训练epoch可以先设150到200。BIT-Vehicle图片尺寸是1600乘1200如果你的显存不够Ultralytics默认会自动letterbox缩放到640或1280这个不用太担心。数据增强参数里重点调hsv_h、hsv_s、hsv_v。因为BIT-Vehicle里有不少夜间图片适当提高饱和度变化和明度变化能让模型对光照条件更鲁棒。我常用的设定是hsv_h0.02hsv_s0.7hsv_v0.5左右再配套mosaic1.0。如果发现Microbus和Truck的AP特别低可以尝试给少数类提高损失权重。Ultralytics没有直接暴露类别权重参数但你可以把少数类的图片数量做一下人工复制或者采用过采样策略把train/images里的Microbus和Truck图片复制两三份对应的txt也复制这样简单粗暴但有效。6.3 结果解读与下一步扩展训练完之后要看的指标不只是mAP50或mAP50-95每一类的AP才重要。用Ultralytics自带的results.csv就能看到每类的AP曲线。我实际跑下来的经验是Sedan的AP通常会到95以上Bus和SUV也不错Minibus、Microbus、Truck按顺序往下掉。如果你的最终目标不是做论文而是做工程完全可以让模型只保留几个大类把容易混淆的Microbus和Minibus合并成一个类别这样工程上的准确率会好看很多。从BIT-Vehicle还可以做一些扩展一是用它作为预训练数据在自己业务场景的小样本上进行微调二是把它和其他车辆数据集混合比如KTITI或者UA-DETRAC增加场景多样性。前提就是先把标注统一成同一种格式。这也是前面说一定要先转VOC/YOLO/COCO的原因否则每个数据集自带一套格式融合的时候光写转换代码就写到怀疑人生。我自己的经验是数据集转换这件事看起来枯燥但只要把中间结构设计好后续不管换框架还是加数据都很顺手。特别是对BIT-Vehicle这种.mat标注的老牌数据集一次转换完成后可以反复使用后面的注意力就能全部集中在模型结构和业务指标上了。
返回列表