当我们自己训练出了一个目标检测的yolov5模型后,肯定会想着把这个模型为我所用,这就涉及到后续的模型部署。但是在模型部署之前,我们需要先调用自己的模型测试一下。其实也就是在一个项目中使用自己的训练好的模型。下面我来详细讲讲。
1.模型下载
git clone https://github.com/ultralytics/yolov5
如图,将模型放在我们的项目下,这里的yolov5是我自己的训练好的模型的文件夹,yolov5origin是我下载的初始的yolov5模型重命名后的文件夹,这里仅作为步骤的演示,实际的应用中只要把自己的模型的文件夹放到项目中就可以了。
2.调用模型
相信都到了调用模型的这一步了,大家的pytorch环境肯定已经配置好了,这里就不赘述了。我们现在来调用自己的模型。代码如下:
import sys
print(sys.path)
sys.path.append(r"yolov5")
import torch
from yolov5.models.experimental import attempt_load
from yolov5.utils.general import non_max_suppression
import cv2
import numpy as np
import matplotlib.pyplot as plt
## yolov5输入的图片是640*640的,所以需要对输入的图片进行预处理以符合输入数据的要求
def preprocess_image(image):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
input_size = 640 # yolov5要求输入网络的图片大小为640
h, w = image.shape[:2]
aspect_ratio = input_size / max(h, w) # 计算缩放比例
resized_h, resized_w = int(h * aspect_ratio), int(w * aspect_ratio) # 调整图片的尺寸
image = cv2.resize(image, (resized_w, resized_h))
pad_h = input_size - resized_h # 填充图片
pad_w = input_size - resized_w
if pad_h > 0 or pad_w > 0:
image = cv2.copyMakeBorder(image, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value=0)
# 将处理后的图片转换为浮点数格式,并保持数值在0到1之间。变换维度,将通道维度放在前面。添加新维度,并且转换为pytorch张量
image = image.astype(np.float32) / 255.0
image = image.transpose(2, 0, 1)
image = np.expand_dims(image, axis=0)
image = torch.from_numpy(image).to(device)
return image
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = attempt_load(r"moxing\best.pt")
model.to(device)
model.eval()
img_path = r"yolov5\data\images\13604849-f6d2-4547-b023-82028f2a2277.png"
img = cv2.imread(img_path)
img_tensor = preprocess_image(img)
conf_threshold = 0.2
labels = {0:"d",1:"u"}
pred = model(img_tensor)[0]
pred = non_max_suppression(pred, conf_thres=conf_threshold,iou_thres = 0.45,agnostic=True) ## 进行非极大值抑制
# 假设你输入模型的图像尺寸是 640x640,但输出的框是相对于这个尺寸的。
# 比如原图尺寸是 (h, w),而输入尺寸是 (640, 640)
# ratio 应该是输入图像的尺寸与原始图像尺寸的比例
# 假设原始图像尺寸为 img.shape (h, w)
print(img.shape)
input_size = 640 # 输入模型的尺寸
# 计算缩放比例
ratio = max(img.shape[0],img[1]) / input_size
pres_cls = None
first_cls = None
# 假设模型返回的 det 是目标检测框,包含 [x1, y1, x2, y2, conf, class] 信息
for i, det in enumerate(pred):
if det is not None and len(det):
# 检测框坐标是相对于输入尺寸的,需要反向转换为原图尺寸
for x1, y1, x2, y2, conf, cls in reversed(det):
# 将检测框的坐标反向缩放回原图尺寸
x1 = int(x1 * ratio)
y1 = int(y1 * ratio)
x2 = int(x2 * ratio)
y2 = int(y2 * ratio)
# 绘制检测框
label = f"{labels[int(cls)]}: {conf:.2f}"
if first_cls is None:
first_cls = cls
cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)
seen_classes.add(cls) # 将该类别标记为已出现
else:
if cls == first_cls:
cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)
else:
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
# 绘制文本
text_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.9, 2)[0]
text_x = x1
text_y = y1 - 10 if y1 - 10 > 10 else y1 + text_size[1]
cv2.putText(img, label, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36, 255, 12), 2)
pres_cls = cls
#img_result = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) # 转回 BGR 格式用于 OpenCV 显示
plt.imshow(img)
plt.axis('off') # 不显示坐标轴
plt.show()
output_path = 'output_image.jpg' # 设置输出文件路径
cv2.imwrite(output_path, img) # 保存图像
3.注意要点
图片放缩
yolov5的输入数据的尺寸要求是640*640,但是为了保证图像不会产生变形(如果变形就影响模型的检测结果,因为模型的训练数据是比例正常的数据目标),不能将原图片直接resize到640 * 640。
而是要按照长边的放缩比例将图片resize,然后短边不够640的部分使用0填充。
色彩空间变换
需要将图像从BGR转换为RGB,因为YOLOv5的默认输入格式是RGB,而OpenCV读取的图像是BGR格式。这样做有助于确保模型正确处理图像数据。
def preprocess_image(image):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) ##进行色彩空间变换
input_size = 640 # yolov5要求输入网络的图片大小为640
h, w = image.shape[:2]
aspect_ratio = input_size / max(h, w) # 计算缩放比例
resized_h, resized_w = int(h * aspect_ratio), int(w * aspect_ratio) # 调整图片的尺寸
image = cv2.resize(image, (resized_w, resized_h))
pad_h = input_size - resized_h # 填充图片
pad_w = input_size - resized_w
if pad_h > 0 or pad_w > 0:
image = cv2.copyMakeBorder(image, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value=0)
# 将处理后的图片转换为浮点数格式,并保持数值在0到1之间。变换维度,将通道维度放在前面。添加新维度,并且转换为pytorch张量
image = image.astype(np.float32) / 255.0
image = image.transpose(2, 0, 1)
image = np.expand_dims(image, axis=0)
image = torch.from_numpy(image).to(device)
return image
没有进行色彩空间变换的结果
下面是进行了色彩空间变换的结果图
可以看出没有进行色彩空间变换的检测效果远不如进行了色彩空间变换的检测效果。主要原因如下:
模型训练时的输入格式是RGB YOLOv5模型在训练时,默认使用的是RGB格式的图像。如果直接输入BGR格式的图像,颜色通道会发生混淆。例如,红色和蓝色通道被交换,这可能会导致模型无法正确识别对象的特征。
颜色特征的重要性 颜色在目标检测任务中可能是关键的特征之一。例如,某些目标的分类或位置可能依赖于特定的颜色分布。如果颜色通道被错误地排列,模型提取的特征就会偏离训练时的分布,影响检测性能。
数据分布的匹配性问题 深度学习模型对输入数据的分布非常敏感。如果测试时的输入数据与训练数据的分布不同,模型的性能会大幅下降。这种“数据分布漂移”问题在BGR和RGB格式之间尤为明显,因为颜色通道的分布特性完全不同。
非极大值抑制的参数
pred = non_max_suppression(pred, conf_thres=conf_threshold,iou_thres = 0.45,agnostic=False)
在 YOLOv5 中,非极大值抑制(NMS)的 agnostic
参数决定了是否忽略类别信息来进行框的筛选。它的作用和意义如下:
agnostic
参数的作用:
agnostic=False
(默认):非极大值抑制会考虑目标框的类别信息。
只有属于同一类别的目标框之间会进行比较(基于置信度和 IoU 阈值),从而保留置信度最高的框。
适用于目标类别明确、类别之间可能相互重叠的情况。
agnostic=True
:非极大值抑制会忽略类别信息。
所有类别的目标框都会参与比较,而不是只在相同类别的目标框中进行。
适用于场景中目标类别不明确、或者多个类别之间可能存在严重重叠的情况。例如,多类别目标之间共享相似的空间区域。
agnostic
参数的使用场景:
agnostic=False
(常见场景):在典型的目标检测任务中,通常希望保留不同类别的目标框,因此选择默认设置即可。
例如,在交通场景下检测汽车、行人和自行车,它们属于不同类别,
agnostic=False
可以避免误将属于不同类别的目标框视为冗余。
agnostic=True
(特定需求):在某些特殊场景中,目标类别可能不明确,或者类别之间有大量重叠,例如:
检测同一个目标的多个状态(比如同一类物体在不同角度的表现)。
数据标注不严格,可能存在多类别框的重叠。
需要统一考虑所有框进行抑制,而不关心类别差异。
在代码中的设置:
在 YOLOv5 的推理脚本中,agnostic
参数一般在调用 nms
函数时设置,例如:
# 默认(类别敏感的 NMS)
pred = non_max_suppression(pred, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False)
# 类别无关的 NMS
pred = non_max_suppression(pred, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=True)
总结:
agnostic=False
是默认值,适用于绝大多数任务,非极大值抑制会区分类别进行处理。agnostic=True
用于忽略类别信息的场景,所有检测框视为同一类别参与抑制。
参数设置为False:
出现了一个目标既被识别成抬头,又被识别成低头的情况。
参数设置为True:
可以保证一个目标的类别的唯一性。
其他问题
后面了解到,yolov5填充的方式是上下左右填充,使图片位于中心: YOLOv5 的填充方式选择 上下左右对称填充 的原因主要出于以下几个关键考虑,这种方式对模型训练和推理过程有显著的实际意义。
1. 保持长宽比
YOLOv5 的填充逻辑旨在 保持原始图像的长宽比,而非强制拉伸到目标尺寸。这种做法:
避免了图像拉伸导致目标物体变形,从而提高模型对目标物体特征的准确识别能力。
特别是在目标检测任务中,物体的形状和比例对模型预测至关重要,保持长宽比有助于模型学习准确的几何特征。
2. 中心对齐输入图像
上下左右对称填充可以使原始图像位于目标尺寸的 正中心。这种对齐方式具有以下优势:
对称性处理:
对称填充可以减少模型对某一方向的偏差,提高特征对齐的一致性。
对于卷积操作,输入图像的中心化有助于特征在不同方向上的均衡学习。
简化模型输入约束:
无论输入图像的尺寸如何,填充后图像始终位于正中心,模型训练和推理过程中不需要额外的偏移或对齐操作。
3. 符合锚框的设计要求
YOLOv5 的目标检测网络通常基于锚框的先验分布进行预测,而这些锚框的大小和位置假定与图像输入是 对称分布 的。如果仅上下或仅左右填充:
原始图像在填充后的相对位置会发生偏移,导致锚框与物体位置的先验不匹配。
中心对齐能最大限度地保证锚框与实际目标的匹配程度,尤其是当锚框基于多尺度设计时。
4. 提供统一的尺寸约束
上下左右均衡填充的图像在调整到目标尺寸后:
始终满足特定输入尺寸(如
640×640
),适配 YOLOv5 网络的输入要求。在推理时,确保特征图的尺寸符合模型的步长(stride)的整数倍约束,避免尺寸不一致引发错误。
5. 避免信息损失和形变
如果仅填充上下或左右(即非对称填充):
原图会偏向某一方向,可能导致部分目标物体处于边界区域,卷积操作在边界区域的感受野较小,容易导致目标特征提取不充分。
中心对齐的上下左右填充,可以最大程度减少这种信息损失,同时保持对目标形状和位置的忠实性。
YOLOv5 填充策略的实现逻辑
在 YOLOv5 的代码中,填充策略通常由 letterbox
函数实现,其逻辑如下:
计算缩放比例:
按照最小缩放比例将输入图像缩放到目标尺寸范围内,保持原始长宽比。
计算填充量:
确定缩放后需要的上下和左右填充量,使最终图像达到目标尺寸。
对称分配填充:
将总填充量平均分配到上下和左右两侧,以实现中心对齐。
添加填充:
使用 OpenCV 或其他工具实现对称填充,填充颜色通常为灰色(默认值
(114, 114, 114)
)。
示例
假设输入图像大小为 300×500
,目标尺寸为 640×640
,YOLOv5 的填充逻辑如下:
计算缩放比例:
ratio = min(640 / 300, 640 / 500) = 1.28
计算缩放后尺寸:
new_unpad = (int(500 * 1.28), int(300 * 1.28)) = (640, 384)
计算填充量:
dw = 640 - 640 = 0 # 宽度无需填充 dh = 640 - 384 = 256 # 高度需要填充
对称分配填充:
top, bottom = 256 // 2, 256 - 256 // 2 = 128, 128 left, right = 0, 0 # 宽度无需填充
填充后的图像尺寸:
图像最终尺寸为
640×640
,原图位于正中心。
总结
YOLOv5 使用上下左右对称填充的主要目的是:
保持原图长宽比,避免目标变形。
保证原图位于中心,减少位置偏差。
匹配锚框设计,提升检测性能。
提供统一尺寸约束,满足网络输入要求。
避免信息损失和形变,提高特征提取效果。
这种填充方式经过实践验证,对目标检测任务表现优秀,是实现高性能和高鲁棒性模型的最佳选择之一。
修改后的代码如下:
import sys
sys.path.append("yolov5")
import torch
from yolov5.models.experimental import attempt_load
from yolov5.utils.general import non_max_suppression
import cv2
import numpy as np
import matplotlib.pyplot as plt
def letterbox(image, new_shape=(640, 640), color=(114, 114, 114)):
shape = image.shape[:2] # [height, width]
ratio = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
new_unpad = (int(shape[1] * ratio), int(shape[0] * ratio))
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]
dw /= 2
dh /= 2
resized_image = cv2.resize(image, new_unpad, interpolation=cv2.INTER_LINEAR)
top, bottom = int(np.floor(dh)), int(np.ceil(dh))
left, right = int(np.floor(dw)), int(np.ceil(dw))
padded_image = cv2.copyMakeBorder(resized_image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
print(padded_image.shape)
cv2.imwrite('padded_image.jpg',padded_image)
return padded_image, ratio, (dw, dh)
def img2tensor(image, device):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
image = image.astype(np.float32) / 255.0
image = image.transpose(2, 0, 1)
image = np.expand_dims(image, axis=0)
return torch.from_numpy(image).to(device)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = attempt_load(r"moxing\best.pt",device=device)
model.eval()
img_path = r"yolov5\data\images\13604849-f6d2-4547-b023-82028f2a2277.png"
img = cv2.imread(img_path)
img_pad, ratio, (dw, dh) = letterbox(img)
img_tensor = img2tensor(img_pad, device)
conf_threshold = 0.25
labels = {0: "d", 1: "u"}
pred = model(img_tensor)[0]
pred = non_max_suppression(pred, conf_thres=conf_threshold, iou_thres=0.45, agnostic=True)
first_cls = None
for det in pred:
if det is not None and len(det):
for x1, y1, x2, y2, conf, cls in det:
x1 = int((x1 - dw) / ratio)
y1 = int((y1 - dh) / ratio)
x2 = int((x2 - dw) / ratio)
y2 = int((y2 - dh) / ratio)
label = f"{labels[int(cls)]}: {conf:.2f}"
if first_cls is None:
cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)
first_cls = cls
else:
if first_cls == cls:
cv2.rectangle(img, (x1, y1), (x2, y2), (255, 0, 0), 2)
else:
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
text_size = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.9, 2)[0]
text_x = x1
text_y = y1 - 10 if y1 - 10 > 10 else y1 + text_size[1]
cv2.putText(img, label, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (36, 255, 12), 2)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img_rgb)
plt.axis('off')
plt.show()
output_path = 'output_image.jpg'
cv2.imwrite(output_path, img)
评论区