文章目录

  • DeepSort基本流程
  • DeepSort特征提取网络
  • Market-1501数据集
  • 目录结构
  • 命名规则
  • 数据集划分
  • 网络模型训练过程
  • 参数设置
  • 数据集加载
  • 特征提取网络定义
  • 预训练模型加载
  • 损失函数与优化器定义
  • mian函数调用
  • 训练过程
  • 验证过程
  • 平均指标与结果


DeepSort基本流程

DeepSort(Deep Learning-based SORT)是一种用于目标跟踪的算法,它结合了深度学习和SORT(Simple Online and Realtime Tracking)算法的优点。DeepSort主要用于视频监控、自动驾驶等领域的多目标跟踪。以下是DeepSort的基本流程:

1. 检测目标
首先,使用目标检测算法(如YOLO、Faster R-CNN等)在每一帧中检测出目标物体,并获取其边界框(bounding box)及相应的置信度分数。

2. 特征提取
对于每个检测到的目标,DeepSort使用一个深度学习模型(通常是一个卷积神经网络)来提取目标的特征向量。这些特征向量用于区分不同的目标。

3. 数据关联
DeepSort使用卡尔曼滤波器来预测目标的位置,并结合匈牙利算法(Hungarian Algorithm)进行数据关联。数据关联的步骤包括:

预测:使用卡尔曼滤波器对每个目标的状态进行预测。
计算距离:根据目标的边界框和特征向量计算检测框与跟踪框之间的距离,通常使用IoU(Intersection over Union)和特征距离(如余弦相似度)来进行综合评估。
匹配:通过匈牙利算法将检测到的目标与当前跟踪的目标进行匹配。

4. 更新跟踪器
对于匹配成功的目标,更新其状态(位置、速度等)和特征向量。对于未匹配的目标,可以选择将其标记为“丢失”或进行其他处理(如保留一段时间)。

5. 处理新目标
对于未被跟踪的检测到的目标,DeepSort会将其添加为新的跟踪目标,并初始化其状态。

6. 轨迹管理
DeepSort维护每个目标的轨迹,包括目标的生命周期和丢失状态,确保在目标消失后仍能保持一定的跟踪时间,以防止短暂的遮挡或消失导致的错误丢失。

7. 输出结果
最后,DeepSort将跟踪到的目标及其ID、边界框、特征向量等信息输出,供后续应用使用。

总结
DeepSort的优势在于它通过深度学习提取的特征向量提高了目标跟踪的准确性,尤其是在复杂场景和目标相似度较高的情况下。此外,结合卡尔曼滤波和匈牙利算法,使得它在实时性和准确性上都表现良好。

相较于Sort算法、ByteTrack追踪算法,DeepSort算法引入了深度学习的思想,即DeepSort中具有一个特征提取网络(ReID网络),这个网络可以提取目标的外观特征信息,这也是DeepSort最大的创新点,当然这也就意味着DeepSort追踪模型是需要训练的,其速度相较于先前的算法慢了许多。

事实上,在ultralytics框架中,其使用的目标追踪算法分别是ByteTrack以及BotSort算法,这里的DeepSort算法并非是该框架中集成的。但DeepSort目标追踪算法太具有代表性,因此我们很有必要去学习一下该算法。

DeepSort特征提取网络

其环境与ultralytics一致,当然还需要下载一些依赖,,这里博主就不做过多赘述了。

关于DeepSort,其组成便是Deep+Sort,其中Deep则是用于提取检测框中的目标特征的特征提取网络(这里可以是任意网络,如ResNet、DenseNet等),而Sort部分基本与Sort算法一致,其核心依旧是卡尔曼滤波与匈牙利匹配。

那么,既然DeepSort算法中具有特征提取网络,那么这个方法就不能直接拿来就用了,我们需要训练。

Market-1501数据集

要进行网络模型的训练,自然要使用数据集,这里使用的数据集为Market-1501:
Market-1501 数据集在清华大学校园中采集,夏天拍摄,在 2015 年构建并公开。它包括由6个摄像头(其中5个高清摄像头和1个低清摄像头)拍摄到的 1501 个行人、32668 个检测到的行人矩形框。每个行人至少由2个摄像头捕获到,并且在一个摄像头中可能具有多张图像。训练集有 751 人,包含 12,936 张图像,平均每个人有 17.2 张训练数据;测试集有 750 人,包含 19,732 张图像,平均每个人有 26.3 张测试数据。

目录结构

该数据集的目录结构如下:

Market-1501
  ├── bounding_box_test
       ├── 0000_c1s1_000151_01.jpg
       ├── 0000_c1s1_000376_03.jpg
       ├── 0000_c1s1_001051_02.jpg
  ├── bounding_box_train
       ├── 0002_c1s1_000451_03.jpg
       ├── 0002_c1s1_000551_01.jpg
       ├── 0002_c1s1_000801_01.jpg
  ├── gt_bbox
       ├── 0001_c1s1_001051_00.jpg
       ├── 0001_c1s1_009376_00.jpg
       ├── 0001_c2s1_001976_00.jpg
  ├── gt_query
       ├── 0001_c1s1_001051_00_good.mat
       ├── 0001_c1s1_001051_00_junk.mat
  ├── query
       ├── 0001_c1s1_001051_00.jpg
       ├── 0001_c2s1_000301_00.jpg
       ├── 0001_c3s1_000551_00.jpg
  └── readme.txt
  1. bounding_box_test——用于测试集的 750 人,包含 19,732 张图像,前缀为 0000 表示在提取这 750人的过程中DPM(DPM(Deformable Part Model,一种传统的目标检测方法)检测错的图(可能与query是同一个人),-1 表示检测出来其他人的图(不在这 750 人中)
  2. bounding_box_train——用于训练集的 751 人,包含 12,936 张图像
  3. query——为 750 人在每个摄像头中随机选择一张图像作为query,因此一个人的query最多有 6 个,共有 3,368 张图像
  4. gt_query——matlab格式,用于判断一个query的哪些图片是好的匹配(同一个人不同摄像头的图像)和不好的匹配(同一个人同一个摄像头的图像或非同一个人的图像)
  5. gt_bbox——手工标注的bounding box,用于判断DPM检测的bounding box是不是一个好的box
命名规则

0001_c1s1_000151_01.jpg 为例

  1. 0001 表示每个人的标签编号,从0001到1501(训练集751人,测试集750人);
  2. c1 表示第一个摄像头(camera1),共有6个摄像头;
  3. s1 表示第一个录像片段(sequece1),每个摄像机都有数个录像段;
  4. 000151 表示 c1s1 的第000151帧图片,视频帧率25fps;
  5. 01 表示 c1s1_001051 这一帧上的第1个检测框,由于采用DPM检测器,对于每一帧上的行人可能会框出好几个bbox。00
    表示手工标注框

数据集划分

事实上,我们并不需要全部的文件,比如在这里我们只需要train文件即可,即我们将train文件拆分一下,从每个目标中拿出一张构成验证集,其余的作训练集,拆分代码如下:

import os
from shutil import copyfile

# You only need to change this line to your dataset download path
download_path = './Market-1501-v15.09.15'

if not os.path.isdir(download_path):
    print('please change the download_path')

save_path = download_path + '/pytorch'
if not os.path.isdir(save_path):
    os.mkdir(save_path)

save_path='./deep_sort_pytorch/deep_sort/deep/Market-1501'
train_path = download_path + '/bounding_box_train'
train_save_path =save_path+ '/train'
val_save_path = save_path+  '/test'

if not os.path.isdir(save_path):
    os.mkdir(save_path)

if not os.path.isdir(train_save_path):
    os.mkdir(train_save_path)
    os.mkdir(val_save_path)

from tqdm import tqdm
for root, dirs, files in os.walk(train_path, topdown=True):
    for name in tqdm(files):
        if not name[-3:]=='jpg':
            continue
        ID  = name.split('_')
        src_path = train_path + '/' + name
        dst_path = train_save_path + '/' + ID[0]
        if not os.path.isdir(dst_path):
            os.mkdir(dst_path)
            dst_path = val_save_path + '/' + ID[0]  #first image is used as val image
            os.mkdir(dst_path)
        copyfile(src_path, dst_path + '/' + name)

划分的数据集如下,其已经都抠图:

ultralytics实现DeepSort目标追踪算法之特征提取网络_特征提取

网络模型训练过程

当我们将数据集拆分完成后,我们便可以进行模型的训练了,我们也来详细看看其是如何进行的:

参数设置

首先是参数设定,这里可以指定数据集、GPU、lr以及预训练模型

arser = argparse.ArgumentParser(description="Train on market1501")
parser.add_argument("--data-dir", default='Market-1501', type=str)
parser.add_argument("--no-cuda", action="store_true")#执行时输入参数--no-cuda时 action有效,是 --no-cuda=false
parser.add_argument("--gpu-id", default=0, type=int)
parser.add_argument("--lr", default=0.1, type=float)
parser.add_argument("--interval", '-i', default=20, type=int)
parser.add_argument('--resume', '-r', action='store_true') #执行时输入参数--resume时 action有效,是 resume=true
args = parser.parse_args()

测试能否使用 CUDA 以及 cuDNN

# device
device = "cuda:{}".format(
    args.gpu_id) if torch.cuda.is_available() and not args.no_cuda else "cpu"
# 基准测试模式是cuDNN的一个特性,它会自动选择对于给定任务的最优算法。
# 当cudnn.benchmark = True时,cuDNN会进行基准测试来找出最优的算法。 这通常会使训练或推理速度变慢,但可以提高准确性。
if torch.cuda.is_available() and not args.no_cuda:
    cudnn.benchmark = True

数据集加载

数据集加载,即加载训练集与验证集,其中首先是进行数据集变换,使用的是torchvision.transforms方法,并主要负责将图像转换为图像剪切、图像旋转(数据增强)以及归一化将数据格式转换为tensor格式。随后便是数据集加载了,调用的是torch.utils.data.DataLoader方法

root = args.data_dir
train_dir = os.path.join(root, "train")
test_dir = os.path.join(root, "test")
transform_train = torchvision.transforms.Compose([
    torchvision.transforms.RandomCrop((128, 64), padding=4),#随机位置裁剪指定尺寸 裁剪结果的尺寸是 (128, 64)
    torchvision.transforms.RandomHorizontalFlip(), #随机水平(左右)翻转
    # [0,255]的数据变张量, 例如原来是128*64*3 变为3*128*64 还要归一化到[0,1]
    torchvision.transforms.ToTensor(),
    # rgb三个通道的数据用公式(img-mean)/std将数据归一化到[-1,1]
    # 其中,均值和方差分别如下 mean=[0.485, 0.456, 0.406] and std=[0.229, 0.224, 0.225],三通道
    torchvision.transforms.Normalize(
        [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
transform_test = torchvision.transforms.Compose([
    torchvision.transforms.Resize((128, 64)),
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize(
        [0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
trainloader = torch.utils.data.DataLoader(
    torchvision.datasets.ImageFolder(train_dir, transform=transform_train),
    batch_size=64, shuffle=True
)
testloader = torch.utils.data.DataLoader(
    torchvision.datasets.ImageFolder(test_dir, transform=transform_test),
    batch_size=64, shuffle=True
)

trainloaderdatasets如下:

ultralytics实现DeepSort目标追踪算法之特征提取网络_算法_02

testloaderdatasets如下,前面我们说过,test数据集中每个目标只选一个,所以751个目标,则选出了751张图,对应751个类别

ultralytics实现DeepSort目标追踪算法之特征提取网络_算法_03

特征提取网络定义

随后便是DeepSort中特征提取网络的调用:

net = Net(num_classes=num_classes)

我们看下这个网络具体是如何实现的,其实这里我们并不需要了解其具体结构,只需要知道其输入输出即可,根据 x = torch.randn(4,3,128,64)可知,其传入的数据即为图像,即(batch-size,通道数,宽,高)的格式,而根据最后连接的分类头nn.Linear(256, num_classes),可知,其最终输出的结果必与类别数有关。事实上最终的结果为(batch-size,751)即该图像中目标的类别。

import cv2
import torch
import torch.nn as nn
import torch.nn.functional as F

class BasicBlock(nn.Module):
    def __init__(self, c_in, c_out,is_downsample=False):
        super(BasicBlock,self).__init__()
        self.is_downsample = is_downsample
        if is_downsample:
            self.conv1 = nn.Conv2d(c_in, c_out, 3, stride=2, padding=1, bias=False)
        else:
            self.conv1 = nn.Conv2d(c_in, c_out, 3, stride=1, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(c_out)
        self.relu = nn.ReLU(True)
        self.conv2 = nn.Conv2d(c_out,c_out,3,stride=1,padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(c_out)
        if is_downsample:
            self.downsample = nn.Sequential(
                nn.Conv2d(c_in, c_out, 1, stride=2, bias=False),
                nn.BatchNorm2d(c_out)
            )
        elif c_in != c_out:
            self.downsample = nn.Sequential(
                nn.Conv2d(c_in, c_out, 1, stride=1, bias=False),
                nn.BatchNorm2d(c_out)
            )
            self.is_downsample = True

    def forward(self,x):
        y = self.conv1(x)
        y = self.bn1(y)
        y = self.relu(y)
        y = self.conv2(y)
        y = self.bn2(y)
        if self.is_downsample:
            x = self.downsample(x)
        return F.relu(x.add(y),True)

def make_layers(c_in,c_out,repeat_times, is_downsample=False):
    blocks = []
    for i in range(repeat_times):
        if i ==0:
            blocks += [BasicBlock(c_in,c_out, is_downsample=is_downsample),]
        else:
            blocks += [BasicBlock(c_out,c_out),]
    return nn.Sequential(*blocks)

class Net(nn.Module):
    def __init__(self, num_classes=751, reid=False):
        super(Net,self).__init__()
        # 3 128 64
        self.conv = nn.Sequential(
            nn.Conv2d(3,64,3,stride=1,padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            # nn.Conv2d(32,32,3,stride=1,padding=1),
            # nn.BatchNorm2d(32),
            # nn.ReLU(inplace=True),
            nn.MaxPool2d(3,2,padding=1),
        )
        # 64 64 32
        self.layer1 = make_layers(64,64,2,False)
        # 64 64 32
        self.layer2 = make_layers(64,128,2,True)
        # 128 32 16
        self.layer3 = make_layers(128,256,2,True)
        # 256 16 8
        self.layer4 = make_layers(256,512,2,True)
        # 512 8 4
        self.avgpool = nn.AvgPool2d((8,4),1)
        # 512 1 1
        self.reid = reid

        self.classifier = nn.Sequential(
            nn.Linear(512, 256),
            nn.BatchNorm1d(256),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(256, num_classes),
        )
    
    def forward(self, x):
        x = self.conv(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.avgpool(x)
        x = x.view(x.size(0),-1)
        # B x 128
        if self.reid:
            x = x.div(x.norm(p=2,dim=1,keepdim=True)) # x.norm=(x1^p+...+xn^p)^(1/p)
            return x
        # classifier
        x = self.classifier(x)
        return x


if __name__ == '__main__':
    net = Net()
    x = torch.randn(4,3,128,64)
    y = net(x)

事实上,这个特征提取网络为ResNet

ultralytics实现DeepSort目标追踪算法之特征提取网络_2d_04

预训练模型加载

加载预训练模型,当然在这里我们没有使用预训练模型

if args.resume:
    assert os.path.isfile(
        "./checkpoint/ckpt.t7"), "Error: no checkpoint file found!"
    print('Loading from checkpoint/ckpt.t7')
    checkpoint = torch.load("./checkpoint/ckpt.t7")
    # import ipdb; ipdb.set_trace()
    net_dict = checkpoint['net_dict']
    net.load_state_dict(net_dict)
    best_acc = checkpoint['acc']
    start_epoch = checkpoint['epoch']

损失函数与优化器定义

定义损失函数与优化器,CrossEntropyLoss是分类损失函数,优化器选择SGD,即随机梯度下降法

criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(
    net.parameters(), args.lr, momentum=0.9, weight_decay=5e-4)
best_acc = 0.

mian函数调用

主函数即为main方法,可以看到其epoch为40,其主要过程为训练、验证、画图、更新lr

def main():
    for epoch in range(start_epoch, start_epoch+40):
        train_loss, train_err = train(epoch)
        test_loss, test_err = test(epoch)
        draw_curve(epoch, train_loss, train_err, test_loss, test_err)
        if (epoch+1) % 20 == 0:
            lr_decay()

训练过程

训练过程定义如下:

ef train(epoch):
    print("\nEpoch : %d" % (epoch+1))
    net.train() # 进入训练模式
    training_loss = 0. # 每20个批次的损失
    train_loss = 0. # 本epoch的损失之和
    correct = 0 # 本epoch识别正确的数量
    total = 0   # 本epoch要识别的图像总数
    interval = args.interval  #interval=20 每20个批次显示一下结果
    start = time.time()  #每20个批次计算一下耗时
    for idx, (inputs, labels) in enumerate(trainloader):
        # forward
        inputs, labels = inputs.to(device), labels.to(device)
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        # backward
        optimizer.zero_grad()#清空梯度,即每个批次清空一次
        loss.backward()#反向传播
        optimizer.step()#执行参数更新
        # accumurating
        training_loss += loss.item() #每20个批次计算一次,这是根据后面的
        train_loss += loss.item()#这个epcho的损失
        correct += outputs.max(dim=1)[1].eq(labels).sum().item()#计算分类正确的数量
        total += labels.size(0)
        # print
        if (idx+1) % interval == 0: #每20个批次打印一下本epoch的结果,并设置损失为0,当然总损失是一直保留的
            end = time.time()
            print("[progress:{:.1f}%]time:{:.2f}s Loss:{:.5f} Correct:{}/{} Acc:{:.3f}%".format(
                100.*(idx+1)/len(trainloader), end-start, training_loss /
                interval, correct, total, 100.*correct/total
            ))
            training_loss = 0.
            start = time.time()
    return train_loss/len(trainloader), 1. - correct/total#最终返回平均损失以及分类正确的比例

其核心部分便是通过遍历数据,获得预测结果,从而与真值计算损失,进而更新网络参数的部分

inputs, labels = inputs.to(device), labels.to(device)

ultralytics实现DeepSort目标追踪算法之特征提取网络_2d_05

得到的结果值如下,output结果维度为(64,751)

outputs = net(inputs)

ultralytics实现DeepSort目标追踪算法之特征提取网络_特征提取_06

将预测结果与真值计算损失:

loss = criterion(outputs, labels)

得到的loss值为:tensor(6.7166, device=‘cuda:0’, grad_fn=),这是整个批次(64张图像)的损失和

随后便是反向传播和更新参数了

loss.backward()
optimizer.step()

验证过程

这里虽说是验证过程,但其实其过程较训练过程极为相似,不同之处在于该过程并不需要进行反向梯度传播与模型参数更新,当然其需要计算验证损失,方便我们作图,事实上,这个过程是方便我们查看模型的训练是否符合预期,使我们能够在训练过程中及时发现过拟合、欠拟合等现象,从而对训练过程做出及时调整。

def test(epoch):
    global best_acc
    net.eval()
    test_loss = 0.
    correct = 0
    total = 0
    start = time.time()
    with torch.no_grad():
        for idx, (inputs, labels) in enumerate(testloader):
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = net(inputs)
            loss = criterion(outputs, labels)

            test_loss += loss.item()
            correct += outputs.max(dim=1)[1].eq(labels).sum().item()
            total += labels.size(0)

        print("Testing ...")
        end = time.time()
        print("[progress:{:.1f}%]time:{:.2f}s Loss:{:.5f} Correct:{}/{} Acc:{:.3f}%".format(
            100.*(idx+1)/len(testloader), end-start, test_loss /
            len(testloader), correct, total, 100.*correct/total
        ))

    # saving checkpoint
    acc = 100.*correct/total
    if acc > best_acc:
        best_acc = acc
        print("Saving parameters to checkpoint/ckpt.t7")
        checkpoint = {
            'net_dict': net.state_dict(),
            'acc': acc,
            'epoch': epoch,
        }
        if not os.path.isdir('checkpoint'):
            os.mkdir('checkpoint')
        torch.save(checkpoint, './checkpoint/ckpt.t7')

    return test_loss/len(testloader), 1. - correct/total

平均指标与结果

评价指标使用的是ACC,其计算公式如下:

ultralytics实现DeepSort目标追踪算法之特征提取网络_2d_07


训练40epoch后,结果如下:

由于评价指标是ACC,即将其看作分类任务来进行,这里num_class=751,即认为这751个目标是751个类别,这样方便计算。

ultralytics实现DeepSort目标追踪算法之特征提取网络_算法_08

ultralytics实现DeepSort目标追踪算法之特征提取网络_2d_09

至此,DeepSort算法 中的特征提取网络的训练、预测与验证过程的梳理边完成了。