导 读
本文主要介绍如何使用Yolo-V5 + DeepSORT实现多目标检测与跟踪。(公众号:OpenCV与AI深度学习)
背景介绍 目标跟踪是一种利用检测到对象的空间和时间特征在整个视频帧中跟踪检测到对象的方法。本文中,我们将与YOLOv5一起实现一种最流行的跟踪算法DeepSORT,并使用MOTA和其他指标在MOT17数据集上进行测试。
该模块负责使用一些对象检测器(如 YOLOv4、CenterNet 等)检测和定位画面中的对象。
该模块负责使用其过去的信息预测对象的未来运动 【1】目标跟踪的必要性 您可能会疑惑,为什么我们需要对象跟踪?为什么我们不能只使用物体检测?需要目标跟踪的原因很多,例如:
SORT 算法做目标跟踪非常成功,可以击败许多最先进算法 。目标检测器为我们提供检测,卡尔曼滤波器为我们提供跟踪,匈牙利算法执行数据关联。那么,为什么我们还需要 DeepSORT?
【2】深度排序
SORT 在跟踪精度和准确度方面表现非常出色。但是 SORT 返回具有大量 ID 开关的轨道,并且在遮挡的情况下失败。这是因为使用了关联矩阵。 DeepSORT 使用结合了运动和外观描述符的更好的关联度量。DeepSORT 可以定义为跟踪算法,它不仅基于对象的速度和运动,而且还基于对象的外观来跟踪对象。
出于上述目的,在实施跟踪之前离线训练一个具有良好区分性的特征嵌入。该网络在大规模人员重新识别数据集上进行训练,使其适用于跟踪上下文。在 DeepSORT余弦度量学习方法中训练深度关联度量模型。根据 DeepSORT 的论文,“余弦距离考虑了外观信息,当运动的判别力较低时,这对于在长期遮挡后恢复身份特别有用。” 这意味着余弦距离是一种度量,可帮助模型在长期遮挡和运动估计失败的情况下恢复身份。使用这些简单的东西可以使跟踪器更加强大和准确。
【3】DeepSORT实现
DeepSORT 可以用于各种现实生活中的应用程序,其中之一就是体育运动。在本节中,我们将在足球和 100m短跑等运动中实现 DeepSORT。与 DeepSORT一起,YOLOv5 将用作检测器来检测所需的对象。该代码是在 Tesla T4 GPU 上的 Google Colab 上实现的。
YOLOv5实现:
首先,我们将克隆 YOLOv5 官方存储库以访问功能和预训练的权重。
!mkdir ./yolov5_deepsort
!git clone https://github.com/ultralytics/yolov5.git
安装requirements.
%cd ./yolov5
!pip install -r requirements.txt
现在我们已经准备好 YOLOv5,让我们将DeepSORT与它集成。
集成DeepSORT
同样,我们将克隆 DeepSORT 的官方实现以访问其代码和功能。
!git clone https://github.com/nwojke/deep_sort.git
最后,一切就绪!但是 DeepSORT 将如何与检测器集成呢?YOLOv5detect.py file负责推理。我们将使用该detect.py文件并将使用它的 DeepSORT 功能添加到新文件中detect_track.py.
%%writefile detect_track.py
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license.
"""
Run inference on images, videos, directories, streams, etc.
Usage - sources:
$ 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 torch
import torch.backends.cudnn as cudnn
FILE = Path(__file__).resolve()
ROOT = FILE.parents[0] # YOLOv5 root directory.
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # Add ROOT to PATH.
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # Relative.
# DeepSORT -> Importing DeepSORT.
from deep_sort.application_util import preprocessing
from deep_sort.deep_sort import nn_matching
from deep_sort.deep_sort.detection import Detection
from deep_sort.deep_sort.tracker import Tracker
from deep_sort.tools import generate_detections as gdet
from models.common import DetectMultiBackend
from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadStreams
from utils.general import (LOGGER, check_file, check_img_size, check_imshow, check_requirements, colorstr, cv2,
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
@torch.no_grad()
def run(
weights=ROOT / 'yolov5s.pt', # model.pt path(s)
source=ROOT / 'data/images', # file/dir/URL/glob, 0 for webcam
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.
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.
save_conf=False, # Save confidences in --save-txt labels.
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.
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.
dnn=False, # Use OpenCV DNN for ONNX inference.
):
source = str(source)
save_img = not nosave and not source.endswith('.txt') # Save inference images.
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://'))
webcam = source.isnumeric() or source.endswith('.txt') or (is_url and not is_file)
if is_url and is_file:
source = check_file(source) # Download.
# DeepSORT -> Initializing tracker.
max_cosine_distance = 0.4
nn_budget = None
model_filename = './model_data/mars-small128.pb'
encoder = gdet.create_box_encoder(model_filename, batch_size=1)
metric = nn_matching.NearestNeighborDistanceMetric("cosine", max_cosine_distance, nn_budget)
tracker = Tracker(metric)
# Directories.
if not os.path.isdir('./runs/'):
os.mkdir('./runs/')
save_dir = os.path.join(os.getcwd(), "runs")
print(save_dir)
'''save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir'''
# Load model.
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) # Check image size.
# Dataloader.
if 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)
bs = len(dataset) # batch_size.
else:
dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt)
bs = 1 # batch_size.
vid_path, vid_writer = [None] * bs, [None] * bs
# Run inference.
model.warmup(imgsz=(1 if pt else bs, 3, *imgsz)) # Warmup.
dt, seen = [0.0, 0.0, 0.0], 0
frame_idx=0
for path, im, im0s, vid_cap, s in dataset:
t1 = time_sync()
im = torch.from_numpy(im).to(device)
im = im.half() if model.fp16 else im.float() # uint8 to fp16/32. im /= 255 # 0 - 255 to 0.0 - 1.0
if len(im.shape) == 3:
im = im[None] # Expand for batch dim.
t2 = time_sync()
dt[0] += t2 - t1
# Inference.
visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else False
pred = model(im, augment=augment, visualize=visualize)
t3 = time_sync()
dt[1] += t3 - t2
# NMS.
pred = non_max_suppression(pred, conf_thres, iou_thres, classes, agnostic_nms, max_det=max_det)
dt[2] += time_sync() - t3
# Second-stage classifier (optional).
# pred = utils.general.apply_classifier(pred, classifier_model, im, im0s)
frame_idx=frame_idx+1
# Process predictions.
for i, det in enumerate(pred): # Per image.
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) # To Path.
print("stem", p.stem)
print("dir", save_dir)
save_path = os.path.join(save_dir, p.name) # im.jpg
txt_path = os.path.join(save_dir , p.stem) # im.txt
s += '%gx%g ' % im.shape[2:] # Print string.
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # Normalization gain whwh.
imc = im0.copy() if save_crop else im0 # For save_crop.
annotator = Annotator(im0, line_width=line_thickness, example=str(names))
if len(det):
# Rescale boxes from img_size to im0 size.
det[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()
# Print results.
for c in det[:, -1].unique():
n = (det[:, -1] == c).sum() # Detections per class.
s += f"{n} {names[int(c)]}{'s' * (n > 1)}, " # Add to string.
# DeepSORT -> Extracting Bounding boxes and its confidence scores.
bboxes = []
scores = []
for *boxes, conf, cls in det:
bbox_left = min([boxes[0].item(), boxes[2].item()])
bbox_top = min([boxes[1].item(), boxes[3].item()])
bbox_w = abs(boxes[0].item() - boxes[2].item())
bbox_h = abs(boxes[1].item() - boxes[3].item())
box = [bbox_left, bbox_top, bbox_w, bbox_h]
bboxes.append(box)
scores.append(conf.item())
# DeepSORT -> Getting appearance features of the object.
features = encoder(im0, bboxes)
# DeepSORT -> Storing all the required info in a list.
detections = [Detection(bbox, score, feature) for bbox, score, feature in zip(bboxes, scores, features)]
# DeepSORT -> Predicting Tracks.
tracker.predict()
tracker.update(detections)
#track_time = time.time() - prev_time
# DeepSORT -> Plotting the tracks.
for track in tracker.tracks:
if not track.is_confirmed() or track.time_since_update > 1:
continue
# DeepSORT -> Changing track bbox to top left, bottom right coordinates.
bbox = list(track.to_tlbr())
# DeepSORT -> Writing Track bounding box and ID on the frame using OpenCV.
txt = 'id:' + str(track.track_id)
(label_width,label_height), baseline = cv2.getTextSize(txt , cv2.FONT_HERSHEY_SIMPLEX,1,1)
top_left = tuple(map(int,[int(bbox[0]),int(bbox[1])-(label_height+baseline)]))
top_right = tuple(map(int,[int(bbox[0])+label_width,int(bbox[1])]))
org = tuple(map(int,[int(bbox[0]),int(bbox[1])-baseline]))
cv2.rectangle(im0, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), (255,0,0), 1)
cv2.rectangle(im0, top_left, top_right, (255,0,0), -1)
cv2.putText(im0, txt, org, cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,255), 1)
# DeepSORT -> Saving Track predictions into a text file.
save_format = '{frame},{id},{x1},{y1},{w},{h},{x},{y},{z}\n'
print("txt: ", txt_path, '.txt')
with open(txt_path + '.txt', 'a') as f:
line = save_format.format(frame=frame_idx, id=track.track_id, x1=int(bbox[0]), y1=int(bbox[1]), w=int(bbox[2]- bbox[0]), h=int(bbox[3]-bbox[1]), x = -1, y = -1, z = -1)
f.write(line)
# Stream results.
im0 = annotator.result()
# Save results (image with detections and tracks).
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)
# Print time (inference-only).
LOGGER.info(f'{s}Done. ({t3 - t2:.3f}s)')
# Print results.
t = tuple(x / 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)
if update:
strip_optimizer(weights) # Update model (to fix SourceChangeWarning).
def parse_opt():
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('--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-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()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # Expand.
print_args(vars(opt))
return opt
def main(opt):
check_requirements(exclude=('tensorboard', 'thop'))
run(**vars(opt))
if __name__ == "__main__":
opt = parse_opt()
main(opt)
只需在检测器代码中添加几行代码,我们就集成了可供使用的 DeepSORT。根据 YOLOv5 的官方实现,将结果保存到一个名为 runs 的新文件夹中,跟踪器结果和输出视频也将保存在同一文件夹中。让我们运行这个脚本,看看它是如何执行的。
【4】推理
如前所述,此追踪器将在运动场景中进行测试。detect_track 脚本接受许多参数,但其中一些参数需要传递。
首先,让我们在下面的视频中进行测试。
!python detect_track.py --weights yolov5m.pt --img 640 --source ./football-video.mp4 --classes 0 32 --line-thickness 1
速度很快,每张图像的推理时间为 20.6 毫秒,大致相当于 50 FPS。甚至输出看起来也不错。输出如下所示。
检测器的工作做得非常好。但是在球的情况下以及球员的情况下,遮挡都没有得到很好的处理。此外,还有许多 ID 开关。视频中的球员被很好地跟踪,但由于运动模糊,球甚至没有被检测到,也没有被正确跟踪。也许 DeepSORT 在密度较低的视频上会表现更好。
让我们在另一个短跑比赛的体育视频上对其进行测试。
!python detect_track.py --weights yolov5m.pt --img 640 --source ./sprint.mp4 --save-txt --classes 0 --line-thickness 1
速度仍然同样快,大约 52 FPS。输出还是很准确的,ID开关非常少,但是在遮挡时无法保持。输出可以在下面查看。
因此,我们可以从这些结果中得出结论,DeepSORT 不能很好地处理遮挡并可能导致 ID 切换。
在试验和测试新事物时,评估总是扮演着重要的角色。同样,对于 DeepSORT,我们将根据一些标准指标来判断其性能。众所周知,DeepSORT 是一种多目标跟踪算法,因此要判断其性能,我们需要特殊的指标和基准数据集。我们将使用 CLEARMOT 指标来判断我们的 DeepSORT 在 MOT17 数据集上的性能。让我们在下一节中更深入地研究这些内容。
【1】MOT挑战基准
MOT Challenge benchmark是一个框架,它提供了大量具有挑战性的真实世界序列、准确注释和许多指标的数据集。MOT Challenge 由各种数据集组成,例如人、对象、2D、3D 等等。更具体地说,每年都会发布数据集的几种变体,例如 MOT15、MOT17 和 MOT20,用于衡量多个对象跟踪器的性能。
对于我们的评估,我们将使用 MOT17 数据集的一个子集。
数据集格式:
所有数据集变体都具有相同的格式,为我们提供
这些指标有助于评估跟踪器的整体优势并判断其总体性能。其他措施如下:
对于人员跟踪,我们将基于 MOTA 评估我们的性能,它告诉我们检测、未命中和 ID 切换的性能。跟踪器的准确度,MOTA(多目标跟踪准确度)计算公式为:
其中 FN 是误报的数量,FP 是误报的数量,IDS 是时间 t 的身份转换数量,GT 是基本事实。MOTA 也可以是负面的。
为了评估性能,我们将通过克隆来引用以下存储库。 !git clone https://github.com/shenh10/mot_evaluation.git %cd ./mot_evaluation 视频分辨率为 960×540,注释分辨率为 1920×1080。因此,需要根据视频分辨率修改注释。您可以从此处直接下载调整大小的地面实况注释。 我们将使用evaluate_tracking.py下面的文件mot_evaluation来评估结果。该脚本采用三个参数,如下所示:
!python evaluate_tracking.py --seqmap './MOT17/videos' --track './yolov5/runs/' --gt './label_960x540' 这给出了所有三个视频的准确性,并给出了所有视频的平均结果。这是结果。
本文分享自 OpenCV与AI深度学习 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!