yolov5总结逐行读一遍代码detect.py
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Run inference on images, videos, directories, streams, etc.
Usage:
$ python path/to/detect.py --weights yolov5s.pt --source 0# webcam
img.jpg# image
vid.mp4# video
path/# directory
path/*.jpg# glob
'https://youtu.be/Zgi9g1ksQHc'# YouTube
'rtsp://example.com/media.mp4'# RTSP, RTMP, HTTP stream
"""
import argparse
import os
import sys
from pathlib import Path
import cv2# opencv包
import torch
import torch.backends.cudnn as cudnn
# 第一部分导入已经安装在环境里的库
FILE = Path(__file__).resolve()# __file__获取当前文件的绝对路径E:\YOLOTOW\detect.py
ROOT = FILE.parents# YOLOv5 root directory 获取ROOT的次一级目录比如 E:\YOLOTOW父目录
if str(ROOT) not in sys.path: # 模块查询的路径列表(确保导包的文件路径存在路径中)
sys.path.append(str(ROOT))# add ROOT to PATH
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))# 绝对路径转化为相对路径
# 第二部分就是处理文件的路径问题
from models.common import DetectMultiBackend
from utils.datasets import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams
from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr,
increment_path, non_max_suppression, print_args, scale_coords, strip_optimizer, xyxy2xywh)
from utils.plots import Annotator, colors, save_one_box
from utils.torch_utils import select_device, time_sync
# 第三部分导入文件里的类
# 导入py包
@torch.no_grad()
# 核心的run函数的部分
# 分成6个部分
# run(函数传入的值,设置了实参,如果没有传入则默认使用实参的内容)
def run(weights=ROOT / 'yolov5s.pt',# 指定不同的权重文件
source=ROOT / 'data/images',# 指定读取那个文件夹文件或者视频亦或者直播流地址
imgsz=640,# 模型在检测图片前会把图片resize成640的size,然后再喂进网络里,并不是说会把们最终得到的结果resize成640大小
conf_thres=0.25,# 置信度的阈值
iou_thres=0.45,# 调节IoU的阈值
max_det=1000,# 最大检测数量,默认是最多检测1000个目标
device='',# GPU数量,如果不指定的话,他会自动检测,这个参数是给GPU多的土豪准备的
view_img=False,# 检测的时候是否实时的把检测结果显示出来
save_txt=False,# 是否把检测结果保存成一个.txt的格式
save_conf=False,# 是否以.txt的格式保存目标的置信度
save_crop=False,# 是否把模型检测的物体裁剪下来---就是实时保存检测到的目标
nosave=False,# 不保存预测的结果,但是还会生成exp文件夹,只不过是一个空的exp
classes=None,# 可以给变量指定多个赋值,也就是说我们可以把“0”赋值给“classes”,也可以把“0”“2”“4”“6”都赋值给“classes暂时理解为指定是被模型的类型
agnostic_nms=False,# 增强版的nms,算是一种trick吧
augment=False,# 这个参数也是一种增强的方式
visualize=False,# 是否把特征图可视化出来,如果开启了这和参数可以看到exp文件夹下又多了一些文件,这里.npy格式的文件就是保存的模型文件,可以使用numpy读写,.png就是图片文件啦
update=False,# 指定这个参数,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息
project=ROOT / 'runs/detect',# 预测结果保存的路径
name='exp',# 预测结果保存的文件夹名字
exist_ok=False,# 每次预测模型的结果是否保存在原来的文件夹,如果指定了这个参数的话,那么本次预测的结果还是保存在上一次保存的文件夹里;如果不指定就是每次预测结果保存一个新的文件夹下
line_thickness=3,# 这个参数就是调节预测框线条粗细的,因为有的时候目标重叠太多会产生遮挡
hide_labels=False,# 参数就是隐藏标签的
hide_conf=False,# 隐藏标签的置信度用的
half=False,# 是否使用 FP16 半精度推理,简单介绍一下低精度技术
dnn=False,# 这个参数的意思就是是否使用 OpenCV DNN 进行 ONNX 推理 DNN即Deep Neural Networks
):
# 输入的路径变为字符串
source = str(source)# 第一部分对source就是命令行传入的部分进行了一些判断--sourcedata/images/fishman.jpg
# 处于预测路径 str() 强制把路径转化为字符串类型
# 是否保存图片和txt文件
save_img = not nosave and not source.endswith('.txt')# nosave=False因为默认值是false所以not一般表示取反以及判断文件后缀是否为txt然后取反true表示预测结果需要保存下来
# 判断文件是否是视频流
# Path()提取文件名 例如:Path("./data/test_images/bus.jpg") Path.name->bus.jpg Path.parent->./data/test_images Path.suffix->.jpg
is_file = Path(source).suffix in (IMG_FORMATS + VID_FORMATS)# 判断文件路径是否为视频以及图片格式 提取文件后缀名是否符合要求的文件,例如:是否格式是jpg, png, asf, avi等
# .lower()转化成小写 .upper()转化成大写 .title()首字符转化成大写,其余为小写, .startswith('http://')返回True or Flase
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))# 判断文件路径是否为直播格式网络流地址
# .isnumeric()是否是由数字组成,返回True or False
webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file) # 判断是否为启动电脑摄像头0数字为 0 或者文件为txt或者为直播流格式等等说
# source.isnumeric()数字为0,则表示为打开电脑的第一个摄像头 source.endswith('.txt') 表示传入的路径是否为txt结尾的(is_url and not is_file)表示是否为直播流开头或者已图片或者视频开头的not is_file 表示不是文件
if is_url and is_file:
# 判断 is_url 是否为ture和 is_file 是否为true 也就是是否为直播流和视频文件
# 返回文件
source = check_file(source)# 如果是true的,他就会根据直播流的地址去下载视频----专门处理直播流参数的
# 第二部分新建了一个保存结果的文件夹
# 预测路径是否存在,不存在新建,按照实验文件以此递增新建
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok)# project表示project=ROOT / 'runs/detect', 这个文件部分 name对于的是 name='exp',这个参数
# increment_path表示增量路径检测文件夹下的exp数字到几了,然后再加一一个文件夹也就表示save_dir文件下的这个路径
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)# 判断save_txt是否为ture 如果是则增量路径创建文件夹用来去存储一个结果
# 第三部分加载模型的权重
# 获取设备 CPU/CUDA
device = select_device(device)# 根据加载环境不同去选择加载的设备
# 检测编译框架PYTORCH/TENSORFLOW/TENSORRT
model = DetectMultiBackend(weights, device=device, dnn=dnn)# 选择模型的后端框架DetectMultiBackend weights表示ROOT / 'yolov5s.pt' device表示cpu或者gpudnn表示 dnn=False,
stride, names, pt, jit, onnx = model.stride, model.names, model.pt, model.jit, model.onnx# 加载完成之后,开始检测出模型的步长 模型的昵称 等参数
# 确保输入图片的尺寸imgsz能整除stride=32 如果不能则调整为能被整除并返回
imgsz = check_img_size(imgsz, s=stride)# imgsz表示输入的模型的图片的大小stride表示模型的步长 然后检测是否为模型的倍数 如果是那么还是640*640的尺寸必须是输入尺寸的32倍一定是倍数关系,如果没有则会默认给你计算一个倍数
# Half
# 如果不是CPU,使用半进度(图片半精度/模型半精度)
half &= pt and device.type != 'cpu'# half precision only supported by PyTorch on CUDA
if pt:
model.model.half() if half else model.model.float()
# 第四部分专门定义了 Dataloader模块,用来加载待预测的图片
# 使用视频流或者页面
if webcam:
# 判断webcam是否是数字也就是是否是调用到电脑摄像头
view_img = check_imshow()
cudnn.benchmark = True# set True to speed up constant image size inference
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt and not jit)
bs = len(dataset)# batch_size
else:
# 如果不是则会跑到这里来,加载图片文件
# 直接从source文件下读取图片
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt and not jit)
# 加载图片文件是先是初始化LoadImages(专门加载图片的模块)对象
bs = 1# batch_size表示每次输入一张图片
# 保存的路径
vid_path, vid_writer = * bs, * bs# 设置列表长度 默认为1
# 第五部分去执行一个模型的推理过程--把图片输入模型产生一个预测结果,并把预测框给画出来
if pt and device.type != 'cpu':
model(torch.zeros(1, 3, *imgsz).to(device).type_as(next(model.model.parameters())))# warmup 热身的意思 初始化先给一张空白的图片给gpu跑一下预热,机器启动
dt, seen = , 0# 在识别前预设的用于存储一些结果信息 dt变量用来储存时间
for path, im, im0s, vid_cap, s in dataset:
# 热身完成之后,就把图片依次传入模型,进行识别
# dataset使用数据加载模型,进行一张一张的遍历识别
# path 图片路径
# im 预处理过的图片文件(RGB,长,宽)
# im0s 表示原图
# vid_cap 返回空
# s 代表图片打印的信息
t1 = time_sync()# 代表时间与电脑系统时间同步
# 转化到GPU上
im = torch.from_numpy(im).to(device)# im代表处理过的图片 也就是torch.Size() 颜色通道 高 宽 转化格式之后再放到cpu/gpu上进行检测
# 是否使用半精度
im = im.half() if half else im.float()# uint8 to fp16/32 判断你的模型有没有使用半精度
im /= 255# 0 - 255 to 0.0 - 1.0 把输入的图片数据除于255的大小,做了一个归一化的操作
if len(im.shape) == 3:# 判断输入图片的字符串尺寸是否为3 (这个部分),
# 增加一个维度
im = im# expand for batch dim然后扩增尺寸 torch.Size()
t2 = time_sync()# 代表时间与电脑系统时间同步
dt += t2 - t1# 记录一下第1部分的耗时对应dt变量用来储存时间
# Inference
# 第二部分就是对第一部分整理好的数据图片进行预测
# 可是化文件路径
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
"""
pred.shape=(1, num_boxes, 5+num_class)
h,w为传入网络图片的长和宽,注意dataset在检测时使用了矩形推理,所以这里h不一定等于w
num_boxes = h/32 * w/32 + h/16 * w/16 + h/8 * w/8
pred[..., 0:4]为预测框坐标=预测框坐标为xywh(中心点+宽长)格式
pred[..., 4]为objectness置信度
pred[..., 5:-1]为分类结果
"""
#visualize=False,# 是否把特征图可视化出来,如果开启了这和参数可以看到exp文件夹下又多了一些文件,这里.npy格式判断这个
# 保存特征图片
pred = model(im, augment=augment, visualize=visualize)# augment表示是否需要做一个数据增强有点提升准确率代价就是速度被降低
# pred变量的意思及是被检测出来的所有的数据的框--还需要再过滤一遍
t3 = time_sync()# 代表时间与电脑系统时间同步
dt += t3 - t2# 记录一下第2部分的耗时对应dt变量用来储存时间
# 首先对每一张图片做了预处理
# 再做一个NMS也就是做一个非极大值的过滤
# NMS 已经获取了所有的图片的信息
"""
pred: 网络的输出结果
conf_thres:置信度阈值
ou_thres:iou阈值
classes: 是否只保留特定的类别
agnostic_nms: 进行nms是否也去除不同类别之间的框
max-det: 保留的最大检测框数量
---NMS, 预测框格式: xywh(中心点+长宽)-->xyxy(左上角右下角)
pred是一个列表list, 长度为batch_size
每一个torch.tensor的shape为(num_boxes, 6), 内容为box + conf + cls
"""
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
#根据图片的conf_thresiou_thres置信度进行过滤置信度太低的就算了--根据置信度过滤
#max_det表示一张图片中能检测多少张图片默认值为1000个--超过一千个就会自动过滤剩下的目标
# 预测+NMS的时间
dt += time_sync() - t3# 记录一下第3部分的耗时对应dt变量用来储存时间
# Second-stage classifier (optional)
# pred = utils.general.apply_classifier(pred, classifier_model, im, im0s)
# Process predictions
# 对所检测的框进行进一步处理
# 最后把所有的检测框划到原图中--以及保存结果的操作
for i, det in enumerate(pred):# per image遍历每一张图片det代表那些检测框的信息
seen += 1# 也就是之前定义好的变量 计数的功能
# mask for certain region
if webcam:# batch_size >= 1 判断是否是调用计算机的摄像头数
p, im0, frame = path, im0s.copy(), dataset.count
s += f'{i}: '
else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
#p 表示循环图片的路径
# frame 表示dataset有没有 frame的属性如果没有则表示 0
p = Path(p)# to Path
save_path = str(save_dir / p.name)# im.jpg表示图片的保存路径
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}')# im.txt 保存txt文件的路径
s += '%gx%g ' % im.shape# print string 拼接表示%gx%g图片的尺寸
gn = torch.tensor(im0.shape)[]# normalization gain whwh获得原图的宽和高的大小
imc = im0.copy() if save_crop else im0# for save_crop 判断是否需要把检测到的目标模型给裁剪下来
annotator = Annotator(im0, line_width=line_thickness, example=str(names))# 定义一个绘图的工具,传入原图与画线的工具,检测框的粗细names写上标签的标签名
if len(det):
# Rescale boxes from img_size to im0 size 因为尺寸问题预测出来的值不能直接画到原图中做一个坐标映射的功能
det[:, :4] = scale_coords(im.shape, det[:, :4], im0.shape).round()
# Print results统计所有框的类别再添加到s的变量上 并打印出来
for c in det[:, -1].unique():
n = (det[:, -1] == c).sum()# detections per class
s += f"{n} {names}{'s' * (n > 1)}, "# add to string
# Write results是否保存预测结果
for *xyxy, conf, cls in reversed(det):
if save_txt:# Write to file如果需要保存成txt文档
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()# normalized xywh
line = (cls, *xywh, conf) if save_conf else (cls, *xywh)# label format
with open(txt_path + '.txt', 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n')
if save_img or save_crop or view_img:# Add bbox to image
c = int(cls)# integer class 如果需要把图片保存则执行这里的内容 默认都是不执行的需要自己调
# c 变量获取cls变量的类别
label = None if hide_labels else (names if hide_conf else f'{names} {conf:.2f}')
# hide_labels 隐藏标签做一个判断,如果是则执行再判断 是否隐藏标签的置信度
annotator.box_label(xyxy, label, color=colors(c, True))
if save_crop:
# 判断是否保存截图下来的目标框
save_one_box(xyxy, imc, file=save_dir / 'crops' / names / f'{p.stem}.jpg', BGR=True)
# Print time (inference-only) 每次遍历之后的打印的结果
LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)')
# Stream results
im0 = annotator.result()# 返回画好的图片
if view_img:
# 判断是否需要显示出来
cv2.imshow(str(p), im0)
cv2.waitKey(1)# 1 millisecond
# Save results (image with detections)
if save_img:
if dataset.mode == 'image':
cv2.imwrite(save_path, im0)
#如果需要保存下来,则使用opencv保存图片的函数
else:# 'video' or 'stream'
if vid_path != save_path:# new video
vid_path = save_path
if isinstance(vid_writer, cv2.VideoWriter):
vid_writer.release()# release previous video writer
if vid_cap:# video
fps = vid_cap.get(cv2.CAP_PROP_FPS)
w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
else:# stream
fps, w, h = 30, im0.shape, im0.shape
save_path += '.mp4'
vid_writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
vid_writer.write(im0)
# Print results 第六部分负责最终打印出来的结果输出信息
t = tuple(x / seen * 1E3 for x in dt)# speeds per image统计打印结果
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms 结果可以写中文 per image at shape {(1, 3, *imgsz)}' % t)
if save_txt or save_img:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels这里也是 saved to {save_dir / 'labels'}" if save_txt else ''
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
# 如果保存txt结果或者图片结果,则会而外打印出一些信息说明一些图片被保存在那些的文件夹中
if update:
strip_optimizer(weights)# update model (to fix SourceChangeWarning)
# 解析命令行传来的参数
# 这个部分代码可以分成三个部分
def parse_opt():
"""
weights: 训练的权重路径,可以使用自己训练的权重,也可以使用官网提供的权重
默认官网的权重yolov5s.pt(yolov5n.pt/yolov5s.pt/yolov5m.pt/yolov5l.pt/yolov5x.pt/区别在于网络的宽度和深度以此增加)
source: 测试数据,可以是图片/视频路径,也可以是'0'(电脑自带摄像头),也可以是rtsp等视频流, 默认data/images
data: 配置数据文件路径, 包括image/label/classes等信息, 训练自己的文件, 需要作相应更改, 可以不用管
如果设置了只显示个别类别即使用了--classes = 0 或二者1, 2, 3等, 则需要设置该文件,数字和类别相对应才能只检测某一个类
imgsz: 网络输入图片大小, 默认的大小是640
conf-thres: 置信度阈值, 默认为0.25
iou-thres:做nms的iou阈值, 默认为0.45
max-det: 保留的最大检测框数量, 每张图片中检测目标的个数最多为1000类
device: 设置设备CPU/CUDA, 可以不用设置
view-img: 是否展示预测之后的图片/视频, 默认False, --view-img 电脑界面出现图片或者视频检测结果
save-txt: 是否将预测的框坐标以txt文件形式保存, 默认False, 使用--save-txt 在路径runs/detect/exp*/labels/*.txt下生成每张图片预测的txt文件
save-conf: 是否将置信度conf也保存到txt中, 默认False
save-crop: 是否保存裁剪预测框图片, 默认为False, 使用--save-crop 在runs/detect/exp*/crop/剪切类别文件夹/ 路径下会保存每个接下来的目标
nosave: 不保存图片、视频, 要保存图片,不设置--nosave 在runs/detect/exp*/会出现预测的结果
classes: 设置只保留某一部分类别, 形如0或者0 2 3, 使用--classes = n, 则在路径runs/detect/exp*/下保存的图片为n所对应的类别, 此时需要设置data
agnostic-nms: 进行NMS去除不同类别之间的框, 默认False
augment: TTA测试时增强/多尺度预测
visualize: 是否可视化网络层输出特征
update: 如果为True,则对所有模型进行strip_optimizer操作,去除pt文件中的优化器等信息,默认为False
project:保存测试日志的文件夹路径
name:保存测试日志文件夹的名字, 所以最终是保存在project/name中
exist_ok: 是否重新创建日志文件, False时重新创建文件
line-thickness: 画框的线条粗细
hide-labels: 可视化时隐藏预测类别
hide-conf: 可视化时隐藏置信度
half: 是否使用F16精度推理, 半进度提高检测速度
dnn: 用OpenCV DNN预测
"""
parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path(s)')
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob, 0 for webcam')
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=, help='inference size h,w')
parser.add_argument('--conf-thres', type=float, default=0.5, help='confidence threshold')
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold')
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--view-img', action='store_true', help='show results')
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes')
parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
parser.add_argument('--augment', action='store_true', help='augmented inference')
parser.add_argument('--visualize', action='store_true', help='visualize features')
parser.add_argument('--update', action='store_true', help='update all models')
parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')
parser.add_argument('--name', default='exp', help='save results to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
opt = parser.parse_args()
# 以上部分定义一些命令行可以传来的参数 default表示一个默认值
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1# 对imgsz图片尺寸做了一个额外的判断扩充维度, 如果是一位就扩充一位
# 判断 imgaesz的长度是否为1,则需要把这个长度文字乘2 这样就保证长宽都是都是规定的值的正方形
# 如果没有定义,则使用默认的,判断imgaesz的值判断是否为长和宽相同,用于保证至少是和正方形
print_args(FILE.stem, opt)
# 输出所有参数
# 打印出,处理好的参数信息并返回
# opt变量用于暂时存储这些参数信息-并返回
return opt
def main(opt):
# 检查环境/打印参数,主要是requrement.txt的包是否安装,用彩色显示设置的参数
check_requirements(exclude=('tensorboard', 'thop'))# 用于检测requiremants.txt内的依赖包是否完成了安装
run(**vars(opt))# 执行run函数并把值传入,图片加载,图片保存,以及图片识别都在run函数内去执行
# 命令使用
# python detect.py --weights runs/train/exp_yolov5s/weights/best.pt --sourcedata/images/fishman.jpg # webcam
# 完成导包步奏之后接下来
# 就会执行__name__的内的2个方法
if __name__ == "__main__":
opt = parse_opt()
# 负责解析命令行传来的参数 python detect.py --weights runs/train/exp_yolov5s/weights/best.pt --sourcedata/images/fishman.jpg
main(opt)
# 传值给main函数 执行定义号的main函数
好详细,太厉害了!! 给大佬递茶:'(weeqw 感谢大佬 感谢大佬 👍👍👍👍👍 大佬好强 这注释写的无敌。{:1_921:}{:1_893:} 很详细呀,论坛里大佬真是厉害。 给大佬敬茶!!牛皮哦
页:
[1]
2