【Datawhale AI夏令营第五期】 CV方向 Task02学习笔记 精读Baseline 建模方案解读与进阶
教程:
链接:
https://linklearner.com/activity/16/16/68
传送门
之前我看原画课的时候,造型的部分就跟我们说,让我们日常观察事物的时候把它想象出有个外边框。在画室学画的期间,第一步打型也是先在纸上草拟最上下左右的几个关键点。
可以理解为分类器只需要输出一个最正确的答案,但是检测器需要同时输出所有合理的答案?
物体检测算法主要分为两类:One-Stage(一阶段)和Two-Stage(两阶段)模型。
一步到位:全盘扫描,简单粗暴速度快。
两步战略:先抓可疑区域,再精细排查,准确精度高。
具体选择?还是要看模型的应用场景。
Yolo适合要求速度的:
认同,高频亮眼的是YOLO5,YOLO8。
一图一个txt文件,一行标注一个物体,包括类别索引和边界框。
YOLO习惯看的数据是中心坐标+长宽比例。所以需要对原本坐标在左上角,宽高是长度的数据集做处理。
在Baseline中就有类别列表和需要识别的违规行为数量。
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: ../dataset/ # dataset root dir
train: images/train/ # train images (relative to 'path') 128 images
val: images/val/ # train images (relative to 'path') 128 images
# Classes
nc: 2 # number of classes
names: ["0", '1'] # class names
我记得训练模型好像还有个best.pt的说法,是训练完成后模型保存的最佳权重,一般就用这个best.pt来执行后续的工作。
results和weight应该比较重要,跟混淆矩阵相关的这几个F1,RP啥的图表根据需要看,我记得这几个指标好像别的地方见过有函数可以直接打印输出。
from sklearn.metrics import confusion_matrix, classification_report, roc_curve, auc
# 假设 y_true 是真实标签,y_pred 是模型预测的标签
y_true = [0, 1, 0, 1]
y_pred = [0, 1, 1, 0]
# 计算混淆矩阵
conf_matrix = confusion_matrix(y_true, y_pred)
# 打印分类报告,其中包含了精确度、召回率和F1分数
print(classification_report(y_true, y_pred))
# 计算ROC曲线和AUC
fpr, tpr, thresholds = roc_curve(y_true, y_pred)
roc_auc = auc(fpr, tpr)
# 打印AUC值
print("AUC:", roc_auc)
此外,还可以使用 matplotlib 或 seaborn 等可视化库来绘制混淆矩阵和ROC曲线:
import matplotlib.pyplot as plt
import seaborn as sns
# 绘制混淆矩阵热力图
sns.heatmap(conf_matrix, annot=True, fmt='d')
plt.show()
# 绘制ROC曲线
plt.plot(fpr, tpr, label='ROC curve (area = %0.2f)' % roc_auc)
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc="lower right")
plt.show()
下面这个点是我第一次看,以后多加留心。
选择YOLO的原因应该是这个最后的应用场景类似于监控摄像头,不要求很精确,但是必须实时检测,快准狠,反应迅速。
如果毕设答辩选了YOLO视频图像识别啥的,下面教程的这段话应该可以借鉴思路,一下子多了好几行。
顺便,这放一张搞笑梗图:
Baseline 进阶思路:
1.加数据量。2.换模型。
原始为
!wget http://mirror.coggle.club/yolo/yolov8n-v8.2.0.pt -O yolov8n.pt
下面任意一个
!wget http://mirror.coggle.club/yolo/yolov8s-v8.2.0.pt -O yolov8s.pt
!wget http://mirror.coggle.club/yolo/yolov8m-v8.2.0.pt -O yolov8m.pt
!wget http://mirror.coggle.club/yolo/yolov8l-v8.2.0.pt -O yolov8l.pt
!wget http://mirror.coggle.club/yolo/yolov8x-v8.2.0.pt -O yolov8x.pt
我这就试试多放几个数据喂模型,这是我第一次跑Baseline的结果,泯然众人。
我给他安排15个视频依葫芦画瓢:
下载了zip文件以后,解压json这步厚德云很成功,上一期的阿里云实例就不行,代码一点没动过,一样是git命令拉下来的,我不知道为什么。
结果文件夹还没出,先看看训练的result——????这啥啊??
看这个图倒是感觉检测得还挺准?话说他怎么知道是不是违章停车呢?会不会把正常停车也标出来,本质上是个车就要逮呢??
一看得分,我靠,效果拔群啊??!
不知道是触发了什么神奇开关,有机会的话我再多试试。
应该看不到哪去但是也搬来的教程参考资料:
https://docs.ultralytics.com/datasets/detect/#ultralytics-yolo-format
https://docs.ultralytics.com/guides/yolo-performance-metrics/
https://docs.ultralytics.com/models/yolov8/#performance-metrics
精读Baseline:
把Task01的小试牛刀体验代码跑了,我对这其中的细节很好奇,因为我之前自己跑过别人分享的Yolo5项目,当时感觉是特别大的一个文件夹,里面塞满了需要用到的各类东西,但是这个代码只需要短短几行就可以了。
之前我只听过F1-score,这个跟混淆矩阵有关系,但这个MOTA指标是啥?
下载数据集,并且还指定了下载的名字。
我还是第一次注意到read_json这个函数:
读取视频的一帧测试一下:
我感觉Kimi说的有道理,这个逻辑读了第一帧会break,啥也没读到也是break,那该如何区分是哪种情况呢?起码该有个输出??
因为如果不看是否ret确实读到了视频第一帧的话,下一个frame.shape的代码可能就会遇到报错。
所以为什么返回的帧数还可能是浮点数呢?
先简单拉个框看看效果:
既然这段代码实现的功能是在图片上拉个能指定(颜色,边框粗细,大小,四角点)的框,那么市面上那些图像标注工具,是不是核心逻辑就跟这个差不多呢?
关于市面上免费的图像标注工具,我之前的帖子里面记了几个:
https://blog.csdn.net/bailichen800/article/details/140308080
传送门
表示目录路径的方法:
感觉这个会比较通用,码住。
import os
# 获取当前目录的绝对路径,并确保路径以分隔符结尾
dir_path = os.path.join(os.path.abspath('./'), '')
print(dir_path)
dir_path = os.path.abspath('.')
# 连接目录路径和文件名
file_path = os.path.join(dir_path, 'example.txt')
这个具体情况再慢慢print出路径调整吧,先暂时把大概函数记住。
打开yaml文件并写入:
这段代码本质上是遵循了读写文本文件的基本流程,所以操作大同小异。
# 需要按照你的修改path
with open('yolo-dataset/yolo.yaml', 'w', encoding='utf-8') as up:
up.write(f'''
path: {dir_path}/yolo-dataset/
train: train/
val: val/
names:
0: 非机动车违停
1: 机动车违停
2: 垃圾桶满溢
3: 违法经营
''')
用glob模块获取指定名称的标注文件和视频文件,放入列表,并且排序,确保对应关系。
下面这段代码看着比较复杂:
for anno_path, video_path in zip(train_annos[:5], train_videos[:5]): #使用 zip 函数将 train_annos 和 train_videos 列表的前5个元素配对。这意味着代码将同时处理每个标注文件和对应的视频文件
print(video_path)
anno_df = pd.read_json(anno_path) #使用 pandas 的 read_json 函数读取标注文件(.json 格式),并将结果存储在 anno_df DataFrame 中
cap = cv2.VideoCapture(video_path) #使用 cv2.VideoCapture 打开视频文件
frame_idx = 0
while True:
ret, frame = cap.read() #使用 cap.read() 在一个循环中逐帧读取视频。如果读取失败(ret 为 False),则跳出循环
if not ret:
break
img_height, img_width = frame.shape[:2] #从当前帧中获取高度和宽度
frame_anno = anno_df[anno_df['frame_id'] == frame_idx] #使用 anno_df 中的 frame_id 筛选出当前帧的标注信息
cv2.imwrite('./yolo-dataset/train/' + anno_path.split('/')[-1][:-5] + '_' + str(frame_idx) + '.jpg', frame) #将当前帧保存为JPEG图像,文件名基于标注文件名和帧索引
if len(frame_anno) != 0: #如果存在当前帧的标注信息,使用 with open 打开一个文本文件进行写入。文本文件名基于标注文件名和帧索引
with open('./yolo-dataset/train/' + anno_path.split('/')[-1][:-5] + '_' + str(frame_idx) + '.txt', 'w') as up:
for category, bbox in zip(frame_anno['category'].values, frame_anno['bbox'].values):
category_idx = category_labels.index(category)
#对于每个标注,将原始的边界框坐标(bbox)转换为 YOLO 格式的相对坐标和尺寸。YOLO 格式使用中心点坐标和宽度、高度来表示边界框。
x_min, y_min, x_max, y_max = bbox
x_center = (x_min + x_max) / 2 / img_width
y_center = (y_min + y_max) / 2 / img_height
width = (x_max - x_min) / img_width
height = (y_max - y_min) / img_height
if x_center > 1:#如果 x_center 大于1(这通常不应该发生,因为坐标应该在0到1之间),打印出边界框坐标。
print(bbox)
up.write(f'{category_idx} {x_center} {y_center} {width} {height}\n') #将转换后的标注信息写入文本文件。每个标注一行,包含类别索引和转换后的坐标、宽度、高度。
frame_idx += 1
但我感觉有点抽象,他这一顿操作到底在干啥??
原来是在适应Yolo的需求,尤其是这个矩形框的表示形式。
下载文件并保存到指定目录:注意这个-p和-o的参数。
!mkdir -p ~/.config/Ultralytics/
!wget http://mirror.coggle.club/yolo/Arial.ttf -O ~/.config/Ultralytics/Arial.ttf
使用 Ultralytics 的 YOLO(You Only Look Once)目标检测模型进行训练的:
import os
os.environ["CUDA_VISIBLE_DEVICES"] = "0" #指定了 CUDA 应该使用的第一个 GPU 设备。在这个例子中,它被设置为 "0",意味着使用默认的 GPU 设备
import warnings
warnings.filterwarnings('ignore') #忽略所有的警告信息,以避免在训练过程中输出不必要的警告
from ultralytics import YOLO #从 Ultralytics 库导入 YOLO 类
model = YOLO("yolov8n.pt") #创建了一个 YOLO 模型实例,使用预训练的权重文件 yolov8n.pt
results = model.train(data="yolo-dataset/yolo.yaml", epochs=2, imgsz=1080, batch=16) #启动了模型的训练过程
注意指定默认GPU设备、忽略所有的警告信息的方法,以后其他代码可以复用:
使用 Ultralytics 的 YOLO 模型对视频文件进行目标检测,并将检测结果保存为 JSON 格式的文件:
from ultralytics import YOLO
model = YOLO("runs/detect/train/weights/best.pt")
import glob
for path in glob.glob('测试集/*.mp4'):
submit_json = []
results = model(path, conf=0.05, imgsz=1080, verbose=False)
for idx, result in enumerate(results):
boxes = result.boxes # Boxes object for bounding box outputs
masks = result.masks # Masks object for segmentation masks outputs
keypoints = result.keypoints # Keypoints object for pose outputs
probs = result.probs # Probs object for classification outputs
obb = result.obb # Oriented boxes object for OBB outputs
if len(boxes.cls) == 0:
continue
xywh = boxes.xyxy.data.cpu().numpy().round()
cls = boxes.cls.data.cpu().numpy().round()
conf = boxes.conf.data.cpu().numpy()
for i, (ci, xy, confi) in enumerate(zip(cls, xywh, conf)):
submit_json.append(
{
'frame_id': idx,
'event_id': i+1,
'category': category_labels[int(ci)],
'bbox': list([int(x) for x in xy]),
"confidence": float(confi)
}
)
with open('./result/' + path.split('/')[-1][:-4] + '.json', 'w', encoding='utf-8') as up:
json.dump(submit_json, up, indent=4, ensure_ascii=False)
首先看第一个问题,这个“最小置信度阈值”是什么呢?
也就是说,给模型对自己的答案定一个信心标准,达到这个标准的才计数考核模型的预测水平。如果模型都是瞎猜给的答案,直接当无效结果考虑。
我感觉就像有些问卷调查里面的“弱智问题”的作用,故意问“1+1=?”,“C选项是什么颜色”这种很简单的,就是要看这份问卷有没有被认真作答。如果这些选项都选错得话说明是瞎填的,可以直接作废了。
这几个提取的信息里边谁是啥得看一眼,以后应该用得多:
for idx, result in enumerate(results): #从结果中提取边界框、分割掩码、关键点、概率和旋转边界框不同类型的检测输出。,,,,
boxes = result.boxes #boxes 通常包含边界框信息
masks = result.masks # masks 包含实例分割的掩码
keypoints = result.keypoints # keypoints 包含关键点检测结果
probs = result.probs # probs 包含每个检测到的对象的类别概率
obb = result.obb #obb 包含旋转边界框(Oriented Bounding Box)信息
from ultralytics import YOLO
model = YOLO("runs/detect/train/weights/best.pt") #创建一个 YOLO 模型实例,并加载预训练的权重文件 best.pt
import glob
for path in glob.glob('测试集/*.mp4'): #用于文件名模式匹配,获取符合特定规则的文件列表 获取测试集目录下所有 .mp4 视频文件的路径,并逐个进行处理
submit_json = [] #存储所有检测结果
results = model(path, conf=0.05, imgsz=1080, verbose=False) #使用模型对每个视频文件进行检测,设置最小置信度阈值 conf=0.05,图像尺寸 imgsz=1080,以及 verbose=False 表示不打印详细输出
for idx, result in enumerate(results): #从结果中提取边界框、分割掩码、关键点、概率和旋转边界框不同类型的检测输出。,,,,
boxes = result.boxes #boxes 通常包含边界框信息
masks = result.masks # masks 包含实例分割的掩码
keypoints = result.keypoints # keypoints 包含关键点检测结果
probs = result.probs # probs 包含每个检测到的对象的类别概率
obb = result.obb #obb 包含旋转边界框(Oriented Bounding Box)信息
if len(boxes.cls) == 0: #如果当前帧没有检测到任何对象(len(boxes.cls) == 0),则跳过当前帧
continue
xywh = boxes.xyxy.data.cpu().numpy().round() #从 boxes.xyxy 提取的边界框坐标,表示为 (x, y, w, h) 格式,其中 x, y 是边界框左上角的坐标,w, h 是边界框的宽度和高度
cls = boxes.cls.data.cpu().numpy().round() #cls 是从 boxes.cls 提取的类别索引
conf = boxes.conf.data.cpu().numpy() #conf 是从 boxes.conf 提取的置信度。
for i, (ci, xy, confi) in enumerate(zip(cls, xywh, conf)):
submit_json.append( #为每个检测到的对象构建一个字典,包含帧 ID、事件 ID、类别、边界框坐标和置信度。
{
'frame_id': idx,
'event_id': i+1,
'category': category_labels[int(ci)],
'bbox': list([int(x) for x in xy]),
"confidence": float(confi)
}
)
with open('./result/' + path.split('/')[-1][:-4] + '.json', 'w', encoding='utf-8') as up:
json.dump(submit_json, up, indent=4, ensure_ascii=False) #将 submit_json 列表写入到结果目录下,文件名为原始视频文件名加上 .json 后缀。
ci等于0,1,2,3时,在之前这个定义这个列表里面找对应的违规行为类别,把汉字代替数字存进JSON文件里。
xywh = boxes.xyxy.data.cpu().numpy().round() #从 boxes.xyxy 提取的边界框坐标,表示为 (x, y, w, h) 格式,其中 x, y 是边界框左上角的坐标,w, h 是边界框的宽度和高度
cls = boxes.cls.data.cpu().numpy().round() #cls 是从 boxes.cls 提取的类别索引
conf = boxes.conf.data.cpu().numpy() #conf 是从 boxes.conf 提取的置信度
输出result:
!\rm result/.ipynb_checkpoints/ -rf
!\rm result.zip
!zip -r result.zip result/