机器学习周报第39周

一、文献阅读

论文标题:Object Detection in Videos by High Quality Object Linking

1.1 摘要

与静态图像中的目标检测相比,视频中的目标检测由于图像质量下降而更具挑战性。许多以前的方法都通过链接视频中的相同对象以形成管状结构,并在管状结构中聚合分类得分,从而利用时间上下文信息。这些方法首先使用静态图像检测器来检测每帧中的对象,然后根据不同帧中对象框之间的空间重叠情况或预测相邻帧之间的对象移动情况,来链接这些检测到的对象。

在本文中,我们专注于获得高质量的对象链接结果以实现更好的分类。与以前通过检查相邻帧之间的框来链接对象的方法不同,我们建议在同一帧中链接。为了实现这一目标,我们在以下方面扩展了现有方法:(1)长方体提议网络,提取限制对象运动的时空候选长方体; (2)短tubelet检测网络,检测短视频片段中的短tubelet; (3)短管连接算法,其将时间上重叠的短管连接成长管。在 ImageNet VID 数据集上的实验表明,我们的方法优于静态图像检测器和之前的技术水平。特别是,对于快速移动的物体,我们的方法比静态图像检测器的结果提高了 8.8%。

1.2 论文背景

由于物体运动,同一物体在相邻帧中的位置和外观会发生变化,这可能导致相邻帧中同一物体的边界框之间的空间重叠不足,或者预测的对象移动不够准确。这会影响对象链接的质量,特别是对于快速移动的对象。相比之下,在同一帧内,如果两个边界框有足够的空间重叠,那么它们显然对应于同一物体。基于这些事实,我们提出在同一帧内链接对象,而不是在相邻帧之间,以实现高质量的对象链接。

许多以前的方法都通过链接视频中的相同对象以形成管状结构,并在管状结构中聚合分类得分,从而利用时间上下文信息。这些方法首先使用静态图像检测器来检测每帧中的对象,然后根据不同帧中对象框之间的空间重叠情况或预测相邻帧之间的对象移动情况,来链接这些检测到的对象。

1.3 论文背景

首先将长视频分割成一些时间上重叠的短视频片段。对于每个短视频片段,我们通过将静态图像的区域提议网络 扩展为短视频片段的立方体提议网络,提取出一组立方体提议,即时空候选立方体,它们能够界定对象的运动。位于同一立方体中的跨帧对象被视为同一对象。立方体提议的主要好处是能够在同一帧中实现对象链接,并且它本身就能带来较小的检测性能提升。

这里的静态图像区域提议网络(Region Proposal Network,RPN)是一种用于生成目标候选区域的深度神经网络结构,它通常与卷积神经网络(CNN)结合使用,以在静态图像中识别可能包含目标对象的区域。RPN的核心思想是通过在CNN特征图上的滑动窗口来生成一系列候选区域(也称为锚点或提议)。这些滑动窗口在特征图的不同位置和尺度上进行遍历,每个位置都生成多个不同尺度和长宽比的候选区域。每个候选区域都通过RPN输出一个分数,表示该区域包含目标对象的可能性,以及该区域的精确边界框坐标。RPN通常与Fast R-CNN或Faster R-CNN等目标检测算法结合使用。在这些算法中,RPN首先生成候选区域,然后这些区域被送入后续的卷积层进行分类和边界框回归,以得到最终的目标检测结果。RPN的设计使得目标检测算法能够高效地生成高质量的候选区域,从而提高了检测速度和精度。它已经成为了现代目标检测算法中不可或缺的一部分,并在许多计算机视觉任务中取得了显著的效果。

对于每个立方体提议,我们调整Fast R-CNN来检测短管状物体。更具体地说,我们分别计算每帧中精确的边界框位置和分类得分,形成一个短管状物体,代表短视频片段中链接的对象边界框。我们通过聚合跨帧的边界框分类得分来计算管状物体的分类得分。此外,为了去除空间上冗余的短管状物体,我们将标准的非极大值抑制(NMS)扩展为管状物体重叠测量,这可以防止在逐帧NMS中可能发生的管状物体断裂。通过短管状物体考虑短距离时间上下文有助于检测。

最后,我们将具有足够重叠度的跨时间重叠的短视频片段中的短管状物体进行链接。如果两个盒子来自于两个相邻短管状物体的时间重叠帧(即同一帧),并且具有足够的空间重叠度,则这两个对应的短管状物体会被链接并合并。这里的“盒子”指的是在某一帧中检测到的对象的边界框。如果两个来自相邻短管状物体的盒子(即两个边界框)在时间上重叠的帧(即同一帧)中有足够的空间重叠,那么它们可能代表的是同一个对象。我们利用对象链接来提高分类质量,通过聚合链接管状物体的分类得分来提升正检测的分类得分。
在这里插入图片描述

1.4 视频处理特征传播

  • 特征传播无对象链接
    特征传播是一种技术,用于增强当前帧的特征信息。这通常是通过聚合从相邻帧中传播过来的特征来实现的。这样的方法有助于提升视频处理的准确性,因为它考虑了时间上的连续性,使得每一帧的特征信息不再孤立。无对象链接没有执行对象链接。对象链接是指将不同帧中检测到的同一对象进行关联,形成一个连贯的轨迹。这种链接有助于更准确地识别和跟踪视频中的对象。因此,尽管这些特征传播方法可以增强当前帧的特征信息,但它们并没有利用对象链接来进一步提升性能。
  • 带对象链接的特征传播
    管状物体提议网络首先通过在第一帧中生成静态对象提议,然后预测后续帧中这些对象的相对运动,来计算管状物体。通过使用CNN-LSTM网络,将管状物体中盒子的特征传播到每个盒子进行分类。除了无对象链接的特征传播外,还链接了相邻帧中的对象以进行特征传播。更具体地说,预测当前帧中每个提议在相邻帧中的相对运动,并将相邻帧中盒子的特征通过平均池化传播到当前帧中对应的盒子。与这些方法不同,我们在同一帧中链接对象,并在帧之间传播盒子得分而不是特征。此外,我们直接为视频片段生成时空立方体提议,而不是像中那样生成每帧提议。
  • 带对象链接的得分传播。
    第一种方式追踪当前帧中检测到的盒子到其相邻帧,以增强它们的原始检测结果,从而提高对象召回率。同时,得分也被传播以改善分类准确性。这种链接是基于盒子内的平均光流向量。第二种方式使用跟踪算法将对象链接成长管状物体,然后采用分类器来聚合管状物体中的检测得分。Seq-NMS方法通过检查相邻帧中盒子之间的空间重叠来链接对象,而不考虑运动信息,然后聚合链接对象的得分作为最终得分。在中的方法同时预测两个帧中的对象位置以及从前一帧到当前帧的对象运动。然后,他们使用这些运动信息将检测到的对象链接成管状物体。同一管状物体中的对象检测得分通过以某种方式聚合该管状物体中的得分来重新加权。

1.5 论文方法

阶段一:为短视频片段生成立方体提议

在这个阶段,系统针对每一个短视频片段(这些片段是原始视频在时间上的分段,且相互之间存在重叠部分),生成一组立方体(或称为容器)。这些立方体旨在跨越多帧绑定相同的对象,即它们表示了对象在连续帧中的可能位置和范围。这些立方体提议是后续处理的基础,帮助系统确定哪些区域可能包含需要关注的对象。

阶段二:短视频片段的短管状物体检测

在有了立方体提议之后,系统进入第二个阶段。对于每一个立方体提议,系统尝试回归(即调整边界框的位置和大小以更准确地匹配对象)并分类一个短管状物体。这个短管状物体实际上是一个边界框的序列,每个边界框都对应视频中的一帧,并定位了那一帧中的对象。为了避免冗余和重叠,系统还会使用非极大值抑制技术来去除那些在空间上过于接近的短管状物体。这些短管状物体代表了短视频片段中对象跨帧的连续存在。

阶段三:整个视频的短管状物体链接

在完成了前两个阶段之后,系统进入第三个阶段,即链接整个视频中的短管状物体。由于视频被分为了时间上重叠的短视频片段,每个片段都生成了自己的短管状物体。在这一阶段,系统需要将这些时间上重叠的短管状物体连接起来,以形成跨整个视频的对象轨迹。这样做可以确保对象在整个视频中的连续性和一致性。同时,通过链接短管状物体,系统还可以进一步优化和细化对象的分类得分,提高对象检测的准确性。

综上,这三个阶段构成了一个完整的视频对象检测与链接流程。前两个阶段为对象检测提供了基础,而第三个阶段则确保了对象在整个视频中的连续跟踪和准确分类。这种方法的优点在于它能够处理视频中的时间连续性和对象运动,从而提高了对象检测的准确性和可靠性。
在这里插入图片描述

1.6 结论

1.数据集和评估指标:
使用了ImageNet VID数据集,该数据集包含30个类别的对象,数据集分为训练集、验证集和测试集。
评估指标采用了平均精度(AP)和所有类别的平均精度(mAP)。

2.消融研究:
对方法中的不同组件进行了详细的消融实验,以研究每个组件的有效性。
静态图像检测器基线是使用与论文中描述的相同设置的Faster R-CNN网络,但将所有帧视为静态图像,不考虑时间信息。

3.Cuboid Proposal Recall:
评估了由Cuboid Proposal Network (CPN)生成的提议的召回率。
通过在不同的IoU阈值下计算提议的召回率,发现保留少量提议(如50个)已经可以获得合理的性能。

4.Short Tubelet Detection:
研究了短视频片段中的短tubelets是否对应于同一对象。
使用更严格的评估标准,即使用短tubelets的链接结果是否准确。

5.短视频片段长度的影响:
讨论了短视频片段长度对性能的影响,发现片段长度为2时获得最大的性能提升。

6.NMS与T-NMS:
研究了标准非最大值抑制(NMS)和tubelet NMS(T-NMS)对对象检测的影响,发现T-NMS的性能优于NMS。

结果:
本方法在VID数据集上取得了74.5%的mAP,当使用VID和DET数据集的混合进行训练时,性能提升到80.6%。

二、centernet

datasets
dataset_factory.py

dataset_factory = {
  'coco': COCO,
  'pascal': PascalVOC,
  'kitti': KITTI,
  'coco_hp': COCOHP
}

_sample_factory = {
  'exdet': EXDetDataset,
  'ctdet': CTDetDataset,
  'ddd': DddDataset,
  'multi_pose': MultiPoseDataset
}

def get_dataset(dataset, task):
  class Dataset(dataset_factory[dataset], _sample_factory[task]):
    pass
  return Dataset

dataset_factory : 定义了数据集字典,根据配置选择相应的数据集,后面以COCO数据集为例;

_sample_factory :任务字典,目标检测、肢体识别等,配置文件默认为目标检测,即取值为CTDetDataset

get_dataset: 相当于对数据集和任务类做了一个封装

这里的class Dataset(dataset_factory[dataset], _sample_factory[task])是一个python的多继承,即Dataset这个类继承了COCO和CTDetDataset,所以在main.py中可以看到

train_loader = torch.utils.data.DataLoader(
      Dataset(opt, 'train'), 
      batch_size=opt.batch_size, 
      shuffle=True,
      num_workers=opt.num_workers,
      pin_memory=True,
      drop_last=True
  )

这样的写法,其中“Dataset(opt, ‘train’)”其实就是使用了COCO类的构造函数,可以去看src/lib/datasets/dataset/coco.py的构造函数,就是需要opt和split两个参数,split用于区分’tarin’和’val’,顺带说一句,训练时每个epcho中需要的数据迭代器由src/lib/datasets/sample文件夹下的类实现。

dataset/coco.py
这个文件比较简单,主要是一些参数的定义,比如总共多少个类、类别名称、默认的图片大小、数据集的均值和方差等。这里唯一要注意的地方是,如果换成自己的数据集,除了num_classes要改之外,均值和方差也需要根据自己的数据集计算,而不是直接使用默认值!

sample/ctdet.py
核心类CTDetDataset,主要实现了训练时需要的数据迭代器。核心函数:getitem()

在函数内部出现了http://self.opt.xxx和http://self.coco.xxx这种类型的调用,但是仔细看CTDetDataset类,却没有__init__函数,更找不到这两个变量的定义。别忘了,在dataset_factory.py中Dataset类时继承了COCO和CTDetDataset两个类的,而且train_loader的定义中用的是Dataset类,所以在实际使用中,即epcho中这个__getitem__()是由Dataset调用的,所以这里的self.opt和self.coco在coco.py中定义!
2. 函数内部可以分成三个部分加载数据、数据增强、生成gt

加载数据

img_id = self.images[index]
        file_name = self.coco.loadImgs(ids=[img_id])[0]['file_name']
        img_path = os.path.join(self.img_dir, file_name)
        ann_ids = self.coco.getAnnIds(imgIds=[img_id])
        anns = self.coco.loadAnns(ids=ann_ids)
        num_objs = min(len(anns), self.max_objs)

        img = cv2.imread(img_path)

数据增强

height, width = img.shape[0], img.shape[1]
c = np.array([img.shape[1] / 2., img.shape[0] / 2.], dtype=np.float32)
if self.opt.keep_res:
    input_h = (height | self.opt.pad) + 1
    input_w = (width | self.opt.pad) + 1
    s = np.array([input_w, input_h], dtype=np.float32)
else:
    s = max(img.shape[0], img.shape[1]) * 1.0
    input_h, input_w = self.opt.input_h, self.opt.input_w

flipped = False
if self.split == 'train':
    if not self.opt.not_rand_crop:
        s = s * np.random.choice(np.arange(0.6, 1.4, 0.1))
        w_border = self._get_border(128, img.shape[1])
        h_border = self._get_border(128, img.shape[0])
        c[0] = np.random.randint(low=w_border, high=img.shape[1] - w_border)
        c[1] = np.random.randint(low=h_border, high=img.shape[0] - h_border)
    else:
        sf = self.opt.scale
        cf = self.opt.shift
        c[0] += s * np.clip(np.random.randn() * cf, -2 * cf, 2 * cf)
        c[1] += s * np.clip(np.random.randn() * cf, -2 * cf, 2 * cf)
        s = s * np.clip(np.random.randn() * sf + 1, 1 - sf, 1 + sf)

    if np.random.random() < self.opt.flip:
        flipped = True
        img = img[:, ::-1, :]
        c[0] = width - c[0] - 1

trans_input = get_affine_transform( c, s, 0, [input_w, input_h])
inp = cv2.warpAffine(img, trans_input,
                     (input_w, input_h),
                     flags=cv2.INTER_LINEAR)
inp = (inp.astype(np.float32) / 255.)
if self.split == 'train' and not self.opt.no_color_aug:
    color_aug(self._data_rng, inp, self._eig_val, self._eig_vec)
inp = (inp - self.mean) / self.std
inp = inp.transpose(2, 0, 1)

解释一下,作者这里用了很多的缩写,其实如果写成全拼会更好理解。

inp:input,就是网络的输入图像了,也是做过数据增加的图像

c:center,图像的中心坐标

s:scale,随机缩放比例

如果对imgaug包熟悉的话,这部分可以用imgaug提供的功能做替换,当然后续关于bbox的变换也要做相应改变。

生成gt

output_h = input_h // self.opt.down_ratio
output_w = input_w // self.opt.down_ratio
num_classes = self.num_classes
trans_output = get_affine_transform(c, s, 0, [output_w, output_h])

hm = np.zeros((num_classes, output_h, output_w), dtype=np.float32)
wh = np.zeros((self.max_objs, 2), dtype=np.float32)
dense_wh = np.zeros((2, output_h, output_w), dtype=np.float32)
reg = np.zeros((self.max_objs, 2), dtype=np.float32)
ind = np.zeros((self.max_objs), dtype=np.int64)
reg_mask = np.zeros((self.max_objs), dtype=np.uint8)
cat_spec_wh = np.zeros((self.max_objs, num_classes * 2), dtype=np.float32)
cat_spec_mask = np.zeros((self.max_objs, num_classes * 2), dtype=np.uint8)

draw_gaussian = draw_msra_gaussian if self.opt.mse_loss else \
    draw_umich_gaussian

gt_det = []
for k in range(num_objs):
    ann = anns[k]
    bbox = self._coco_box_to_bbox(ann['bbox'])
    cls_id = int(self.cat_ids[ann['category_id']])
    if flipped:
        bbox[[0, 2]] = width - bbox[[2, 0]] - 1
    bbox[:2] = affine_transform(bbox[:2], trans_output)
    bbox[2:] = affine_transform(bbox[2:], trans_output)
    bbox[[0, 2]] = np.clip(bbox[[0, 2]], 0, output_w - 1)
    bbox[[1, 3]] = np.clip(bbox[[1, 3]], 0, output_h - 1)
    h, w = bbox[3] - bbox[1], bbox[2] - bbox[0]
    if h > 0 and w > 0:
        radius = gaussian_radius((math.ceil(h), math.ceil(w)))
        radius = max(0, int(radius))
        radius = self.opt.hm_gauss if self.opt.mse_loss else radius
        ct = np.array(
            [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2], dtype=np.float32)
        ct_int = ct.astype(np.int32)
        draw_gaussian(hm[cls_id], ct_int, radius)
        wh[k] = 1. * w, 1. * h
        ind[k] = ct_int[1] * output_w + ct_int[0]
        reg[k] = ct - ct_int
        reg_mask[k] = 1
        cat_spec_wh[k, cls_id * 2: cls_id * 2 + 2] = wh[k]
        cat_spec_mask[k, cls_id * 2: cls_id * 2 + 2] = 1
        if self.opt.dense_wh:
            draw_dense_reg(dense_wh, hm.max(axis=0), ct_int, wh[k], radius)
        gt_det.append([ct[0] - w / 2, ct[1] - h / 2,
                       ct[0] + w / 2, ct[1] + h / 2, 1, cls_id])

ret = {'input': inp, 'hm': hm, 'reg_mask': reg_mask, 'ind': ind, 'wh': wh}

hm:理论中需要热力图,形状如下图
在这里插入图片描述
然后在后续的for循环中,对图片上的每个obj分别生成对应的热图:

for k in range(num_objs):
	....
	if h > 0 and w > 0:
		draw_gaussian(hm[cls_id], ct_int, radius)

wh:网络最后需要回归出的目标的宽和高,形状:[128, 2]。赋值的时候需要注意是在标注好的bbox做完相应的数据增加相关操作后进行的计算。

# 先对bbox做对应的数据增强变换
bbox[:2] = affine_transform(bbox[:2], trans_output)
bbox[2:] = affine_transform(bbox[2:], trans_output)
bbox[[0, 2]] = np.clip(bbox[[0, 2]], 0, output_w - 1)
bbox[[1, 3]] = np.clip(bbox[[1, 3]], 0, output_h - 1)
# 做完后再计算对应的h,w
h, w = bbox[3] - bbox[1], bbox[2] - bbox[0]
if h > 0 and w > 0:
    radius = gaussian_radius((math.ceil(h), math.ceil(w)))
    radius = max(0, int(radius))
    radius = self.opt.hm_gauss if self.opt.mse_loss else radius
    ct = np.array([(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2], dtype=np.float32)
    ct_int = ct.astype(np.int32)
    draw_gaussian(hm[cls_id], ct_int, radius)
    wh[k] = 1. * w, 1. * h

reg:网络需要回归出的中心点偏移量,即在理论部分看到的由于降采样和取整后造成的偏移量。

if h > 0 and w > 0:
     ....
     ct = np.array([(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2], dtype=np.float32)
     ct_int = ct.astype(np.int32)
     ......
     reg[k] = ct - ct_int

ind:一个索引数组,根据它的赋值能看出实际用处

ind[k] = ct_int[1] * output_w + ct_int[0]

这里只打开这个文件,在__init__函数中有wh和reg的损失计算函数定义,在计算wh的损失时有个NormRegL1Loss()类,跟进去可以看到forward()函数中有个_transpose_and_gather_feat()函数,这个函数中用到了这里生成的inds

def _gather_feat(feat, ind, mask=None):
    dim = feat.size(2)
    ind = ind.unsqueeze(2).expand(ind.size(0), ind.size(1), dim)
    feat = feat.gather(1, ind)
    if mask is not None:
        mask = mask.unsqueeze(2).expand_as(feat)
        feat = feat[mask]
        feat = feat.view(-1, dim)
    return feat


def _transpose_and_gather_feat(feat, ind):
    feat = feat.permute(0, 2, 3, 1).contiguous()
    feat = feat.view(feat.size(0), -1, feat.size(3))
    feat = _gather_feat(feat, ind)
    return feat

在_transpose_and_gather_feat()函数第一行将tensor的形状变成了(batch_size, w, h, c),然后reshape成(batch_size, wh, c)然后传给_gather_feat()函数。在这一步将二维的feature map变成了wh的行向量,然后再在_gather_feat()函数中使用tensor.gather()函数在dim=1的维度上进行聚合。即在w*h这个维度上选取ind指定的数据。如果这里理解有困难,建议看一下pytorch中gather函数的理解。

trains
train_factory.py
同样,只是简单的提供一个可以根据配置文件选择的字典.
base_trainer.py
这个就是之前提到过的那个基类了。BaseTrainer()核心基类,主要功能如下:

实现了每个训练器都需要用到了run_epoch(),在这个函数内部实现了网络的迭代训练

定义了各个子类需要实现的接口,特别是self._get_losses()这个函数,这个是核心函数,而且可以看到具体的loss
计算是由ModelWithLoss()这个类定义并完成的。但是,在基类中定位到_get_losses()时,发现仅仅返回了一个NotImplementedError,如果学过C++这里就是一个虚函数,让子类必须自己来实现功能

ModelWithLoss类的forward函数中的loss计算也仅仅是个定义,具体计算过程在子类中实现

trains/ctdet.py
目标检测网络训练的具体实现。文件中最核心的是损失函数的计算类CtdetLoss(),另一个类CtdetTrainer()继承自BaseTrainer,主要是基类中几个函数模板的功能实现。

detectors

  1. 老套路,detector_factory.py提供各种检测器的集合,根据配置文件来选择具体运行的时候是哪一个。

  2. base_detector.py各个检测器的基类,主要提供了一个公共函数的实现,比如pre_process()图像输入网络前的预处理函数,对图像做一系列变换比如缩放,减均值除方差操作,最后输出符合网络输入的数据。

  3. 在detectors/ctdet.py中实现具体的检测过程。CtdetDetector()继承自BaseDetector,在process中做预测预测输出结果。到这里应该基本上不存在代码逻辑上的理解困难了。

  • 26
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值