本文适合有一定 python 基础且对 yolov5 有一定了解的朋友阅读
本文将对当前 https://github.com/ultralytics/yolov5.git 中的 detect.py 文件进行详细的流程描述并且附上全程的代码注释, 从而将 detect.py 脚本运行的逻辑清晰地展示出来
注: 只会提供流程描述和每段代码对应的功能, 不会给予语法的讲解
文末有完整的 detect.py 文件 ( 包含笔者的注释 )
第一部分: 如何使用命令行模板调用 detect.py ( 官方提供 )
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
################################### 如何使用 detect.py 脚本 ( 命令行模板 ) ###################################
"""
Run YOLOv5 detection inference on images, videos, directories, globs, YouTube, webcam, streams, etc.
Usage - sources:
$ python detect.py --weights yolov5s.pt --source 0 # webcam
img.jpg # image
vid.mp4 # video
screen # screenshot
path/ # directory
list.txt # list of images
list.streams # list of streams
'path/*.jpg' # glob
'https://youtu.be/LNwODJXcvt4' # YouTube
'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream
Usage - formats:
$ python detect.py --weights yolov5s.pt # PyTorch
yolov5s.torchscript # TorchScript
yolov5s.onnx # ONNX Runtime or OpenCV DNN with --dnn
yolov5s_openvino_model # OpenVINO
yolov5s.engine # TensorRT
yolov5s.mlmodel # CoreML (macOS-only)
yolov5s_saved_model # TensorFlow SavedModel
yolov5s.pb # TensorFlow GraphDef
yolov5s.tflite # TensorFlow Lite
yolov5s_edgetpu.tflite # TensorFlow Edge TPU
yolov5s_paddle_model # PaddlePaddle
"""
以上是官方提供的参考
第一点 : sources 表示需要进行检测的源文件, 可以是多种形式 webcam 相机, image 图片, video 视频, 甚至YouTube, URL网址均可
第二点 : formats 表示使用的权重文件, 同样可以是多种形式, 一般用的是 PyTorch 的 .pt 文件
这里给一个简单的命令行示例模板 :
python detect.py --source 0 --weights weights/yolov5s.pt
--source 0 表示使用电脑相机获取待处理的图像
--weights weights/yolov5s.pt 表示使用 yolo 项目根目录下的 weights 文件夹中的 yolov5s.pt
( 笔者是自己创建了这个 weights 文件夹, 并将 yolov5.st 放在里面, 大家可以自己安排权重文件放置的位置, 记得使用的时候要把路径写全, 如果程序找不到你的权重文件它会自己从网上下载 )
第二部分: 导入必要的库, 函数和类, 并定义 FILE 和 ROOT 路径
# 导入必要的库
import argparse
import csv
import os
import platform
import sys
from pathlib import Path
import torch
这段代码导入了该程序需要使用的库
# 定义 FILE 和 ROOT 路径
FILE = Path(__file__).resolve() # 找到当前文件 detect.py 的绝对路径 resolve 表示将相对路径转为绝对路径
ROOT = FILE.parents[0] # 找到 YOLOv5 根目录
# 将 YOLOv5 根目录添加到 sys.path( 如果尚未存在 )
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT))
# 获取 YOLOv5 根目录的路径
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))
这段代码的功能 :
1 找到当前文件 detect.py 在你电脑上的绝对路径
2 将 YOLOv5 根目录添加到 sys.path( 如果尚未存在 )
3 获取 YOLOv5 根目录的路径
# 从外部模块导入所需的函数/类
from ultralytics.utils.plotting import Annotator, colors, save_one_box
from models.common import DetectMultiBackend
from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams
from utils.general import (LOGGER, Profile, check_file, check_img_size, check_imshow, check_requirements, colorstr, cv2,
increment_path, non_max_suppression, print_args, scale_boxes, strip_optimizer, xyxy2xywh)
from utils.torch_utils import select_device, smart_inference_mode
这段代码从外部模块导入了所需的函数/类
第三部分: 程序的入口, 以及程序对命令行参数的解析
# 程序的入口
if __name__ == '__main__':
# 它首先使用 parse_opt 函数解析命令行选项
opt = parse_opt()
# 然后,它以解析后的选项作为参数调用主函数 main
main(opt)
这段代码虽然在整个文件的末尾, 却是这个程序的入口 !
程序执行的第一步便是使用 parse_opt 函数解析命令行选项
以下是 parse_opt 函数 :
它会创建一个 ArgumentParser 对象 parser, 将命令行的参数添加到 parser 中
因此这是我们编写命令行时的重要参考, 可以通过它知道如何编写命令行以达到我们的需求
这段代码末尾的 print_args 函数会在终端打印本次检测所使用的参数
# 解析命令行参数
def parse_opt():
# 创建 ArgumentParser 对象 parser
parser = argparse.ArgumentParser()
# 对 ArgumentParser 对象 parser 添加命令行参数及其选项和说明 ( 编写命令行时的参考 )
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path or triton URL')
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob/screen/0(webcam)')
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path')
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
parser.add_argument('--conf-thres', type=float, default=0.25, 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-csv', action='store_true', help='save results in CSV format')
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')
parser.add_argument('--vid-stride', type=int, default=1, help='video frame-rate stride')
# 分析命令行参数
opt = parser.parse_args()
# 如果 imgsz 参数只有一个值,将修改该参数为两个值,以适应 yolov5 代码的要求
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
# 将在终端打印已解析的参数
print_args(vars(opt))
# 返回已解析的参数
return opt
然后程序会将 parse_opt 函数解析出的结果 opt 作为参数传给主函数 main
# 主函数 main
def main(opt):
# 检查是否满足 requirements.txt 文件中指定的要求 不包括 “tensorboard” 和 “hop”
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop'))
# 使用 “opt” 参数中的选项运行 run 功能模块
run(**vars(opt))
main 函数检查完 requirements.txt 文件中指定的要求后再将 opt 作为参数传给函数 run
第四部分: run 函数接收并检测参数信息, 做一些准备工作
@smart_inference_mode()
def run(
weights=ROOT / 'yolov5s.pt', # model path or triton URL 选择本地权重文件或通过 Triton URL 网络调用权重文件
source=ROOT / 'data/images', # file/dir/URL/glob/screen/0(webcam) 选择要处理的图像或视频或目录或 URL 或 glob 表达式或屏幕截图或 0(相机)
data=ROOT / 'data/coco128.yaml', # dataset.yaml path 选择数据集配置文件
imgsz=(640, 640), # inference size (height, width) 设置推理尺寸
conf_thres=0.25, # confidence threshold 设置置信度阈值
iou_thres=0.45, # NMS IOU threshold 设置 NMS 阈值
max_det=1000, # maximum detections per image 设置最大检测数
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu 选择推理设备
view_img=False, # show results 选择是否显示结果
save_txt=False, # save results to *.txt 选择是否保存结果到 txt 文件
save_csv=False, # save results to *.csv 选择是否保存结果到 csv 文件
save_conf=False, # save confidences in --save-txt labels 选择是否保存置信度到 txt 文件
save_crop=False, # save cropped prediction boxes 选择是否保存裁剪后的预测框
nosave=False, # do not save images/videos 选择是否保存图像或视频
classes=None, # filter by class: --class 0, or --class 0 2 3 选择要过滤的类别
agnostic_nms=False, # class-agnostic NMS 选择是否使用类无关的 NMS
augment=False, # augmented inference 选择是否使用数据增强
visualize=False, # visualize features 选择是否可视化特征
update=False, # update all models 选择是否更新所有模型
project=ROOT / 'runs/detect', # save results to project/name 设置保存结果的目录和名称
name='exp', # save results to project/name 设置保存结果的目录和名称
exist_ok=False, # existing project/name ok, do not increment 选择是否覆盖已存在的项目和名称
line_thickness=3, # bounding box thickness (pixels) 设置边界框的粗细
hide_labels=False, # hide labels 选择是否隐藏标签
hide_conf=False, # hide confidences 选择是否隐藏置信度
half=False, # use FP16 half-precision inference 选择是否使用 FP16 半精度推理
dnn=False, # use OpenCV DNN for ONNX inference 选择是否使用 OpenCV DNN 进行 ONNX 推理
vid_stride=1, # video frame-rate stride 设置视频帧率步长
):
以上代码是 run 函数在接收传给它的参数信息并设置一些之后会用到的标识符
这些标识符的含义 ( 中英文 ) 笔者在上面都写好了, 这些标识符其实都有默认的值, 大家有兴趣的话可以进行更改, 运行一下看一看效果
# 将要处理的源的路径转换为字符串类型
source = str(source)
# 根据条件确定是否保存推理图像: 1. 如果 nosave 是 False 2. 如果源文件不是以 “.txt” 结尾
save_img = not nosave and not source.endswith('.txt')
# 检查源文件是否为支持图像或视频格式的文件
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
# 检查源是否是具有支持协议的 URL
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
# 根据数字检查、文件扩展名和 URL 格式确定源是否来自摄像头
webcam = source.isnumeric() or source.endswith('.streams') or (is_url and not is_file)
# 检查源是否用于截屏
screenshot = source.lower().startswith('screen')
# 如果源是 URL 和文件,将检查并可能下载该文件
if is_url and is_file:
source = check_file(source)
接下来程序通过这些参数的值来判断出源的类型, 做一些检查工作, 如果是URL的话还会进行下载
# 定义并创建一个新目录,用于保存具有递增路径的项目 目录名是 “project” 和 “name” 的组合
# 如果目录已经存在,则会递增以避免覆盖
# 如果 save_txt 为 True,会在 save_dir 中创建一个子目录 “labels” 以保存图片对应的文本文件( 储存标注框的信息 )
# 如果 save_txt 为 False,则直接创建不带子目录的 save_dir
# 确保所有父目录都已创建( 如果它们不存在 )
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok)
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)
并且还会定义一个新目录,用于保存具有递增路径的项目 ( 即最终的结果文件 )
第五部分: 加载模型和数据集
"""
- bs: 数据加载的批大小
- device: 加载模型的设备
- weights: 权重文件
- dnn: 即 DNN 可能用到的深度神经网络
- data: 数据集的配置文件
- half: 半精度推理
- imgsz: 输入图像的大小
- webcam: 用于表明输入是否来自网络摄像头的标志
- screenshot: 用于表明输入是否为屏幕截图的标志
- source: 输入源( 例如,视频文件或图像目录 )
- vid_stride: 用于视频处理的帧步长
- dataset: 加载的数据集
- view_img: 用于显示图像的标志
- vid_path: 视频路径存储器
- vid_writer: 视频存储器
"""
这里笔者先给出自己整理的该部分会用到的一些标识符及其含义 ( 可以先不看, 在下面遇到了再回上来看 )
# 选择加载模型的设备 有 GPU 则选择 GPU,没有则选择 CPU
device = select_device(device)
# 根据你的环境配置选择如何加载模型
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
# 读取模型数据
stride, names, pt = model.stride, model.names, model.pt
# 检查输入图像的尺寸是否符合模型要求
imgsz = check_img_size(imgsz, s=stride)
首先程序会选择加载模型的设备 有 GPU 则选择 GPU,没有则选择 CPU
再根据你的环境配置选择如何加载模型 比如: 用 .pt 文件, 则通过 pytorch 方式来加载模型
然后读取模型数据 后面需要用到
再检查输入图像的尺寸是否符合模型要求
# 表明每次输入一张图片
bs = 1
# 如果摄像头为 True,则会通过 LoadStreams 函数来进行数据集初始化
if webcam:
view_img = check_imshow(warn=True)
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride)
bs = len(dataset)
# 如果屏幕截图为 True,则会通过 LoadScreenshots 函数来进行数据集初始化
elif screenshot:
dataset = LoadScreenshots(source, img_size=imgsz, stride=stride, auto=pt)
# 如果网络摄像头和屏幕截图都不是 True,则会通过 LoadImages 函数来进行数据集初始化
else:
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride)
# 将 vid_path 和 vid_writer 初始化为长度为 bs 的列表,作为视频路径存储器和视频存储器
vid_path, vid_writer = [None] * bs, [None] * bs
接着根据之前对源类型的判断, 使用对应的方式来加载数据集
第六部分: 运行模型, 进行推理
# 模型预热
model.warmup(imgsz=(1 if pt or model.triton else bs, 3, *imgsz))
# 创建一些中间变量
seen, windows, dt = 0, [], (Profile(), Profile(), Profile())
首先对模型进行预热 其实就是传给模型一张空白的图片先运行一遍
再创建一些中间变量 seen 用于图像的计数 windows 用于后续图像的展示 dt 用于记录处理时长
# 遍历数据集
for path, im, im0s, vid_cap, s in dataset:
接着遍历数据集, 每一次遍历可能会处理多张图像, 一般一次只处理一张 ( 由上文的 bs 决定 )
# 使用配置文件 dt[0] 处理图像
with dt[0]:
# 将图像转换为模型支持的格式 ( 一般会将原图缩放为模型要求的尺寸 ) 并移动到处理设备( 即 GPU 或 CPU )
im = torch.from_numpy(im).to(model.device)
# 如果模型是 fp16,则将图像转换为半精度,否则转换为浮点
im = im.half() if model.fp16 else im.float()
# 将图像的每一个像素点的值从 0-255 规格化到 0.0-1.0
im /= 255
# 如果图像只有3个维度( 通道数, 高度, 宽度 ),则再增加一个维度
if len(im.shape) == 3:
im = im[None]
先进行图像的预处理
# 使用配置文件 dt[1] 处理图像
with dt[1]:
# 如果 visualize 为 True,则将保存推理过程中的特征图
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
# 进行推理并获取预测结果 pred ( 包含大量的预测框、置信度、类别等信息 ), 如果 augment 为 True,则还会对图像进行数据增强
pred = model(im, augment=augment, visualize=visualize)
# 最终结果格式: pred [图片数量, 预测框数量, 预测框信息(中心点x坐标, 中心点y坐标, 宽度, 高度, 置信度, 80个类别)]
# 举例: pred [1, 10000, 85] 每一个预测框包含 85 个信息,分别是中心点x坐标、中心点y坐标、宽度、高度、置信度、80个类别
再进行预测框检测 ( 程序的核心 )
结果格式: pred [ 图片数量, 预测框数量, 预测框信息 ( x1, y1, x2, y2, 置信度, 类别 ) ]
举例: pred [ 1, 10000, 85 ] 每一个预测框包含 85 个信息,分别是中心点x坐标、中心点y坐标、宽度、高度、置信度、80个类别
# 使用配置文件 dt[2] 处理图像
with dt[2]:
# 进行 NMS 非极大值抑制 过滤掉置信度低于 conf_thres 的预测框
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
# 最终结果格式: pred [图片数量, 预测框数量, 预测框信息(x1, y1, x2, y2、置信度、类别)]
# 举例: pred [1, 5, 6] 每一个预测框包含 6 个信息,分别是左上角x坐标、左上角y坐标、右下角x坐标、右下角y坐标、置信度、类别
然后是 NMS 非极大值抑制 过滤掉置信度低于 conf_thres 的预测框
结果格式: pred [ 图片数量, 预测框数量, 预测框信息 ( x1, y1, x2, y2、置信度、类别 ) ]
举例: pred [ 1, 5, 6 ] 每一个预测框包含 6 个信息,分别是左上角x坐标、左上角y坐标、右下角x坐标、右下角y坐标、置信度、对应的类别
第七部分: 定义一个将数据保存到 CSV 文件的函数
"""
将预测结果写入 CSV 文件
参数:
- image_name: 图像的名称
- prediction: 预测类标签
- confidence: 预测的置信度得分
"""
# 定义 CSV 文件的保存路径
csv_path = save_dir / 'predictions.csv'
# 定义一个保存数据到 CSV 文件的函数 ( 如果 save_csv 为 True, 后面的代码则会调用该函数 )
def write_to_csv(image_name, prediction, confidence):
data = {'Image Name': image_name, 'Prediction': prediction, 'Confidence': confidence}
with open(csv_path, mode='a', newline='') as f:
writer = csv.DictWriter(f, fieldnames=data.keys())
if not csv_path.is_file():
writer.writeheader()
writer.writerow(data)
如果 save_csv 设置为 True, 则后面的代码会调用该函数, 但是程序默认设置为 false, 一般用不到, 就不详细展开了
第八部分: 保存数据, 展示处理好的图像
# 遍历预测后的数据集中的每一张图片
for i, det in enumerate(pred):
因为数据集的一次处理中通常只有一个图片, 所以通常只遍历了一次
第一点: 做一些准备工作
# 记录已处理的图片数量 起到计数作用
seen += 1
# 进行一些变量的赋值
if webcam: # batch_size >= 1
p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: '
else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
# 将 p 转换为 Path 对象
p = Path(p)
# 创建图像文件的保存路径 ( 如果 save_img 为 True )
save_path = str(save_dir / p.name)
# 创建文本文件的保存路径 ( 如果 save_txt 为 True )
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}')
# 将图像的尺寸附加到字符串 ( 用于终端输出图像的尺寸 )
s += '%gx%g ' % im.shape[2:]
# 获得原图的尺寸
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]
# 将检测框裁剪并保存 ( 如果 save_crop 为 True )
imc = im0.copy() if save_crop else im0
# 设置检测框的绘制方式
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
这部分进行了很多重要操作:
设置了文件的保存路径
存储了之后终端需要输出的图像尺寸
获得原图的尺寸
将检测框裁剪并保存 ( 如果 save_crop 为 True )
设置检测框的绘制方式
第二点: 存储预测框信息
# 如果这张图片存在预测框, 则遍历所有预测框并绘制保存每一个预测框的信息
if len(det):
如果这张图片存在预测框, 则进行如下这些操作
# 进行坐标映射 将检测框从目前图像大小重新缩放为原图对应的大小
det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round()
进行坐标映射 将检测框从目前图像大小重新缩放为原图对应的大小
原因: 为了满足模型支持的尺寸格式要求, 先前对原图进行了缩放操作, 导致识别出的预测框相对于原图也有一定的缩放度, 现在为了在原图上画出预测框, 则必须将预测框再缩放回来
# 储存之后要在终端输出的预测框信息 ( 即为后续的终端输出做准备 )
for c in det[:, 5].unique():
# 计算每一个种类的检测框数量 n
n = (det[:, 5] == c).sum()
# 存储每一个种类与其对应的检测框数量到字符串 s 中
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, "
储存之后要在终端输出的预测框信息 ( 即为后续的终端输出做准备 )
# 存储每一个预测框的图像结果
for *xyxy, conf, cls in reversed(det):
遍历一张图片上的所有预测框
# 获取当前预测框对应的种类编号
c = int(cls)
# 获取对象的标签,如果 hide_conf 为 False,则还包括置信度
label = names[c] if hide_conf else f'{names[c]}'
# 将置信度分数转换为浮点值
confidence = float(conf)
# 将置信度分数格式化为带两位小数的字符串
confidence_str = f'{confidence:.2f}'
保存这些预测框的标签和置信度信息, 后续绘图需要用到
# 保存预测结果到 CSV 文件 ( 如果 save_csv 为 True )
if save_csv:
write_to_csv(p.name, label, confidence_str)
# 保存预测结果到 TXT 文件 ( 如果 save_txt 为 True )
if save_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(f'{txt_path}.txt', 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n')
# 绘制检测框到原图上 ( 如果 save_img 或 save_crop 或 view_img 为 True )
if save_img or save_crop or view_img:
# 获取当前预测框对应的种类编号
c = int(cls)
# 获取对象的标签( 如果 hide_labels 为 False ),获取对象的置信度( 如果 hide_conf 为 False )
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}')
# 使用 box_label 函数绘制检测框到原图上
annotator.box_label(xyxy, label, color=colors(c, True))
# 保存预测框对应的截取图像 ( 如果 save_crop 为 True )
if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True)
根据之前设置好的标识符, 判断是否要进行各种文件的保存
重点关注 annotator.box_label(xyxy, label, color=colors(c, True)) 这一行代码, 它使用了 box_label 函数绘制了预测框到原图上
另外注意这里有对多种文件的保存, 但并没有对画好预测框的图像进行保存, 而这会在展示预测框模块里进行实现
第三点: 展示并保存已经绘制好预测框后的图像
# 获取已经绘制好预测框后的图像
im0 = annotator.result()
获取已经绘制好预测框后的图像
# 如果 view_img 为 True,则会创建一个窗口展示这些图像 ( 主要使用 cv2 模块中的函数 )
if view_img:
if platform.system() == 'Linux' and p not in windows:
windows.append(p)
cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux)
cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0])
cv2.imshow(str(p), im0)
cv2.waitKey(1) # 1 millisecond
如果 view_img 为 True,则会创建一个窗口展示这些图像 ( 主要使用 cv2 模块中的函数 )
# 保存已经绘制好预测框后的图像 ( 如果 save_img 为 True )
if save_img:
if dataset.mode == 'image':
cv2.imwrite(save_path, im0)
else: # 'video' or 'stream'
if vid_path[i] != save_path: # new video
vid_path[i] = save_path
if isinstance(vid_writer[i], cv2.VideoWriter):
vid_writer[i].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[1], im0.shape[0]
save_path = str(Path(save_path).with_suffix('.mp4')) # force *.mp4 suffix on results videos
vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
vid_writer[i].write(im0)
保存已经绘制好预测框后的图像 ( 如果 save_img 为 True )
第四点: 在终端打印完成一次数据集检测, 保存, 展示的总耗时
# 在终端打印本轮检测所花费的时间,使用 “dt” 变量转换为毫秒
LOGGER.info(f"{s}{'' if len(det) else '(no detections), '}{dt[1].dt * 1E3:.1f}ms")
第九部分: 终端的总结和模型的更新
# 计算平均检测时间并在终端打印
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t)
# 在终端打印保存结果的路径 ( 如果 save_img 或 save_txt 为 True )
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}")
在完成所有数据集检测之后在终端打印平均检测时间, 保存结果的路径等信息
# 更新模型 ( 如果 update 为 True )
if update:
strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning)
更新模型 ( 如果 update 为 True )
至此, detect.py 文件已经完整地过了一遍, 但笔者其实也只是对主要的关键部分进行了讲解, 一些细节只能大家自己去看源码来进行理解 ( 当然我对 detect.py 文件的注释也是大家重要的参考 )
最后, 希望当大家看到笔者在哪里的讲解不够全面, 或者哪里有什么错误, 还望大家帮忙指出, 笔者将感激不尽, 感谢您的耐心阅读 !
最后再附上完整的 detect.py 文件 ( 包含笔者的注释 )
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
################################### 如何使用 detect.py 脚本 ( 命令行模板 ) ###################################
"""
Run YOLOv5 detection inference on images, videos, directories, globs, YouTube, webcam, streams, etc.
Usage - sources:
$ python detect.py --weights yolov5s.pt --source 0 # webcam
img.jpg # image
vid.mp4 # video
screen # screenshot
path/ # directory
list.txt # list of images
list.streams # list of streams
'path/*.jpg' # glob
'https://youtu.be/LNwODJXcvt4' # YouTube
'rtsp://example.com/media.mp4' # RTSP, RTMP, HTTP stream
Usage - formats:
$ python detect.py --weights yolov5s.pt # PyTorch
yolov5s.torchscript # TorchScript
yolov5s.onnx # ONNX Runtime or OpenCV DNN with --dnn
yolov5s_openvino_model # OpenVINO
yolov5s.engine # TensorRT
yolov5s.mlmodel # CoreML (macOS-only)
yolov5s_saved_model # TensorFlow SavedModel
yolov5s.pb # TensorFlow GraphDef
yolov5s.tflite # TensorFlow Lite
yolov5s_edgetpu.tflite # TensorFlow Edge TPU
yolov5s_paddle_model # PaddlePaddle
"""
# 导入必要的库
import argparse
import csv
import os
import platform
import sys
from pathlib import Path
import torch
# 定义 FILE 和 ROOT 路径
FILE = Path(__file__).resolve() # 找到当前文件 detect.py 的绝对路径 resolve 表示将相对路径转为绝对路径
ROOT = FILE.parents[0] # 找到 YOLOv5 根目录
# 将 YOLOv5 根目录添加到 sys.path( 如果尚未存在 )
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT))
# 获取 YOLOv5 根目录的路径
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))
# 从外部模块导入所需的函数/类
from ultralytics.utils.plotting import Annotator, colors, save_one_box
from models.common import DetectMultiBackend
from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams
from utils.general import (LOGGER, Profile, check_file, check_img_size, check_imshow, check_requirements, colorstr, cv2,
increment_path, non_max_suppression, print_args, scale_boxes, strip_optimizer, xyxy2xywh)
from utils.torch_utils import select_device, smart_inference_mode
################################### detect.py 脚本的功能模块 ( detect.py 脚本的灵魂 ) ###################################
@smart_inference_mode()
def run(
weights=ROOT / 'yolov5s.pt', # model path or triton URL 选择本地权重文件或通过 Triton URL 网络调用权重文件
source=ROOT / 'data/images', # file/dir/URL/glob/screen/0(webcam) 选择要处理的图像或视频或目录或 URL 或 glob 表达式或屏幕截图或 0(相机)
data=ROOT / 'data/coco128.yaml', # dataset.yaml path 选择数据集配置文件
imgsz=(640, 640), # inference size (height, width) 设置推理尺寸
conf_thres=0.25, # confidence threshold 设置置信度阈值
iou_thres=0.45, # NMS IOU threshold 设置 NMS 阈值
max_det=1000, # maximum detections per image 设置最大检测数
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu 选择推理设备
view_img=False, # show results 选择是否显示结果
save_txt=False, # save results to *.txt 选择是否保存结果到 txt 文件
save_csv=False, # save results to *.csv 选择是否保存结果到 csv 文件
save_conf=False, # save confidences in --save-txt labels 选择是否保存置信度到 txt 文件
save_crop=False, # save cropped prediction boxes 选择是否保存裁剪后的预测框
nosave=False, # do not save images/videos 选择是否保存图像或视频
classes=None, # filter by class: --class 0, or --class 0 2 3 选择要过滤的类别
agnostic_nms=False, # class-agnostic NMS 选择是否使用类无关的 NMS
augment=False, # augmented inference 选择是否使用数据增强
visualize=False, # visualize features 选择是否可视化特征
update=False, # update all models 选择是否更新所有模型
project=ROOT / 'runs/detect', # save results to project/name 设置保存结果的目录和名称
name='exp', # save results to project/name 设置保存结果的目录和名称
exist_ok=False, # existing project/name ok, do not increment 选择是否覆盖已存在的项目和名称
line_thickness=3, # bounding box thickness (pixels) 设置边界框的粗细
hide_labels=False, # hide labels 选择是否隐藏标签
hide_conf=False, # hide confidences 选择是否隐藏置信度
half=False, # use FP16 half-precision inference 选择是否使用 FP16 半精度推理
dnn=False, # use OpenCV DNN for ONNX inference 选择是否使用 OpenCV DNN 进行 ONNX 推理
vid_stride=1, # video frame-rate stride 设置视频帧率步长
):
# 将要处理的源的路径转换为字符串类型
source = str(source)
# 根据条件确定是否保存推理图像: 1. 如果 nosave 是 False 2. 如果源文件不是以 “.txt” 结尾
save_img = not nosave and not source.endswith('.txt')
# 检查源文件是否为支持图像或视频格式的文件
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
# 检查源是否是具有支持协议的 URL
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
# 根据数字检查、文件扩展名和 URL 格式确定源是否来自摄像头
webcam = source.isnumeric() or source.endswith('.streams') or (is_url and not is_file)
# 检查源是否用于截屏
screenshot = source.lower().startswith('screen')
# 如果源是 URL 和文件,将检查并可能下载该文件
if is_url and is_file:
source = check_file(source)
# 定义并创建一个新目录,用于保存具有递增路径的项目 目录名是 “project” 和 “name” 的组合
# 如果目录已经存在,则会递增以避免覆盖
# 如果 save_txt 为 True,会在 save_dir 中创建一个子目录 “labels” 以保存图片对应的文本文件( 储存标注框的信息 )
# 如果 save_txt 为 False,则直接创建不带子目录的 save_dir
# 确保所有父目录都已创建( 如果它们不存在 )
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok)
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)
###################################### 加载模型和数据集 ######################################
"""
- bs: 数据加载的批大小
- device: 加载模型的设备
- weights: 权重文件
- dnn: 即 DNN 可能用到的深度神经网络
- data: 数据集的配置文件
- half: 半精度推理
- imgsz: 输入图像的大小
- webcam: 用于表明输入是否来自网络摄像头的标志
- screenshot: 用于表明输入是否为屏幕截图的标志
- source: 输入源( 例如,视频文件或图像目录 )
- vid_stride: 用于视频处理的帧步长
- dataset: 加载的数据集
- view_img: 用于显示图像的标志
- vid_path: 视频路径存储器
- vid_writer: 视频存储器
"""
# 选择加载模型的设备 有 GPU 则选择 GPU,没有则选择 CPU
device = select_device(device)
# 根据你的环境配置选择如何加载模型
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
# 读取模型数据
stride, names, pt = model.stride, model.names, model.pt
# 检查输入图像的尺寸是否符合模型要求
imgsz = check_img_size(imgsz, s=stride)
# 表明每次输入一张图片
bs = 1
# 如果摄像头为 True,则会通过 LoadStreams 函数来进行数据集初始化
if webcam:
view_img = check_imshow(warn=True)
dataset = LoadStreams(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride)
bs = len(dataset)
# 如果屏幕截图为 True,则会通过 LoadScreenshots 函数来进行数据集初始化
elif screenshot:
dataset = LoadScreenshots(source, img_size=imgsz, stride=stride, auto=pt)
# 如果网络摄像头和屏幕截图都不是 True,则会通过 LoadImages 函数来进行数据集初始化
else:
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride)
# 将 vid_path 和 vid_writer 初始化为长度为 bs 的列表,作为视频路径存储器和视频存储器
vid_path, vid_writer = [None] * bs, [None] * bs
########################################### 运行模型 ( 主角终于来了 ) ##########################################
# 模型预热
model.warmup(imgsz=(1 if pt or model.triton else bs, 3, *imgsz))
# 创建一些中间变量
seen, windows, dt = 0, [], (Profile(), Profile(), Profile())
# 遍历数据集
for path, im, im0s, vid_cap, s in dataset:
############################################ 进行推理 ############################################
# 使用配置文件 dt[0] 处理图像
with dt[0]:
# 将图像转换为模型支持的格式 ( 一般会将原图缩放为模型要求的尺寸 ) 并移动到处理设备( 即 GPU 或 CPU )
im = torch.from_numpy(im).to(model.device)
# 如果模型是 fp16,则将图像转换为半精度,否则转换为浮点
im = im.half() if model.fp16 else im.float()
# 将图像的每一个像素点的值从 0-255 规格化到 0.0-1.0
im /= 255
# 如果图像只有3个维度( 通道数, 高度, 宽度 ),则再增加一个维度
if len(im.shape) == 3:
im = im[None]
# 使用配置文件 dt[1] 处理图像
with dt[1]:
# 如果 visualize 为 True,则将保存推理过程中的特征图
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
# 进行推理并获取预测结果 pred ( 包含大量的预测框、置信度、类别等信息 ), 如果 augment 为 True,则还会对图像进行数据增强
pred = model(im, augment=augment, visualize=visualize)
# 最终结果格式: pred [图片数量, 预测框数量, 预测框信息(中心点x坐标, 中心点y坐标, 宽度, 高度, 置信度, 80个类别)]
# 举例: pred [1, 10000, 85] 每一个预测框包含 85 个信息,分别是中心点x坐标、中心点y坐标、宽度、高度、置信度、80个类别
# 使用配置文件 dt[2] 处理图像
with dt[2]:
# 进行 NMS 非极大值抑制 过滤掉置信度低于 conf_thres 的预测框
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
# 最终结果格式: pred [图片数量, 预测框数量, 预测框信息(x1, y1, x2, y2、置信度、类别)]
# 举例: pred [1, 5, 6] 每一个预测框包含 6 个信息,分别是左上角x坐标、左上角y坐标、右下角x坐标、右下角y坐标、置信度、类别
################################# 定义一个保存数据到 CSV 文件的函数 #################################
"""
将预测结果写入 CSV 文件
参数:
- image_name: 图像的名称
- prediction: 预测类标签
- confidence: 预测的置信度得分
"""
# 定义 CSV 文件的保存路径
csv_path = save_dir / 'predictions.csv'
# 定义一个保存数据到 CSV 文件的函数 ( 如果 save_csv 为 True, 后面的代码则会调用该函数 )
def write_to_csv(image_name, prediction, confidence):
data = {'Image Name': image_name, 'Prediction': prediction, 'Confidence': confidence}
with open(csv_path, mode='a', newline='') as f:
writer = csv.DictWriter(f, fieldnames=data.keys())
if not csv_path.is_file():
writer.writeheader()
writer.writerow(data)
################################################# 保存数据 ############################################
# 遍历预测后的数据集中的每一张图片
for i, det in enumerate(pred):
# 记录已处理的图片数量 起到计数作用
seen += 1
# 进行一些变量的赋值
if webcam: # batch_size >= 1
p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: '
else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0)
# 将 p 转换为 Path 对象
p = Path(p)
# 创建图像文件的保存路径 ( 如果 save_img 为 True )
save_path = str(save_dir / p.name)
# 创建文本文件的保存路径 ( 如果 save_txt 为 True )
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}')
# 将图像的尺寸附加到字符串 ( 用于终端输出图像的尺寸 )
s += '%gx%g ' % im.shape[2:]
# 获得原图的尺寸
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]
# 将检测框裁剪并保存 ( 如果 save_crop 为 True )
imc = im0.copy() if save_crop else im0
# 设置检测框的绘制方式
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
########################### 遍历预测框并绘制保存每一个预测框的信息 ##############################
# 如果这张图片存在预测框, 则遍历所有预测框并绘制保存每一个预测框的信息
if len(det):
# 进行坐标映射 将检测框从目前图像大小重新缩放为原图对应的大小
det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round()
# 储存之后要在终端输出的预测框信息 ( 即为后续的终端输出做准备 )
for c in det[:, 5].unique():
# 计算每一个种类的检测框数量 n
n = (det[:, 5] == c).sum()
# 存储每一个种类与其对应的检测框数量到字符串 s 中
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, "
# 存储每一个预测框的图像结果
for *xyxy, conf, cls in reversed(det):
# 获取当前预测框对应的种类编号
c = int(cls)
# 获取对象的标签,如果 hide_conf 为 False,则还包括置信度
label = names[c] if hide_conf else f'{names[c]}'
# 将置信度分数转换为浮点值
confidence = float(conf)
# 将置信度分数格式化为带两位小数的字符串
confidence_str = f'{confidence:.2f}'
# 保存预测结果到 CSV 文件 ( 如果 save_csv 为 True )
if save_csv:
write_to_csv(p.name, label, confidence_str)
# 保存预测结果到 TXT 文件 ( 如果 save_txt 为 True )
if save_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(f'{txt_path}.txt', 'a') as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n')
# 绘制检测框到原图上 ( 如果 save_img 或 save_crop 或 view_img 为 True )
if save_img or save_crop or view_img:
# 获取当前预测框对应的种类编号
c = int(cls)
# 获取对象的标签( 如果 hide_labels 为 False ),获取对象的置信度( 如果 hide_conf 为 False )
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}')
# 使用 box_label 函数绘制检测框到原图上
annotator.box_label(xyxy, label, color=colors(c, True))
# 保存预测框对应的截取图像 ( 如果 save_crop 为 True )
if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True)
############################# 展示并保存已经绘制好预测框后的图像 #############################
# 获取已经绘制好预测框后的图像
im0 = annotator.result()
# 如果 view_img 为 True,则会创建一个窗口展示这些图像 ( 主要使用 cv2 模块中的函数 )
if view_img:
if platform.system() == 'Linux' and p not in windows:
windows.append(p)
cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux)
cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0])
cv2.imshow(str(p), im0)
cv2.waitKey(1) # 1 millisecond
# 保存已经绘制好预测框后的图像 ( 如果 save_img 为 True )
if save_img:
if dataset.mode == 'image':
cv2.imwrite(save_path, im0)
else: # 'video' or 'stream'
if vid_path[i] != save_path: # new video
vid_path[i] = save_path
if isinstance(vid_writer[i], cv2.VideoWriter):
vid_writer[i].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[1], im0.shape[0]
save_path = str(Path(save_path).with_suffix('.mp4')) # force *.mp4 suffix on results videos
vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h))
vid_writer[i].write(im0)
# 在终端打印本轮检测所花费的时间,使用 “dt” 变量转换为毫秒
LOGGER.info(f"{s}{'' if len(det) else '(no detections), '}{dt[1].dt * 1E3:.1f}ms")
# 计算平均检测时间并在终端打印
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t)
# 在终端打印保存结果的路径 ( 如果 save_img 或 save_txt 为 True )
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}")
# 更新模型 ( 如果 update 为 True )
if update:
strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning)
# 解析命令行参数
def parse_opt():
# 创建 ArgumentParser 对象 parser
parser = argparse.ArgumentParser()
# 对 ArgumentParser 对象 parser 添加命令行参数及其选项和说明 ( 编写命令行时的参考 )
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path or triton URL')
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob/screen/0(webcam)')
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path')
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
parser.add_argument('--conf-thres', type=float, default=0.25, 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-csv', action='store_true', help='save results in CSV format')
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')
parser.add_argument('--vid-stride', type=int, default=1, help='video frame-rate stride')
# 分析命令行参数
opt = parser.parse_args()
# 如果 imgsz 参数只有一个值,将修改该参数为两个值,以适应 yolov5 代码的要求
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
# 将在终端打印已解析的参数
print_args(vars(opt))
# 返回已解析的参数
return opt
# 主函数 main
def main(opt):
# 检查是否满足 requirements.txt 文件中指定的要求 不包括 “tensorboard” 和 “hop”
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop'))
# 使用 “opt” 参数中的选项运行 run 功能模块
run(**vars(opt))
# 程序的入口
if __name__ == '__main__':
# 它首先使用 parse_opt 函数解析命令行选项
opt = parse_opt()
# 然后,它以解析后的选项作为参数调用主函数 main
main(opt)