基于航空影像深度学习的目标检测

1.数据集

对象检测部分,我们使用了由劳伦斯利弗莫尔国家实验室提供的Cars Overhead With Context (COWC)数据集(https://gdo152.llnl.gov/cowc/)。它具有在六个不同位置拍摄的航拍图像:

  • 加拿大多伦多
  • 塞尔温,新西兰
  • 波茨坦和Vaihingen,德国
  • 哥伦布(俄亥俄州)和美国犹他州

*我们最终没有使用哥伦布和Vaihingen的数据,因为图像是灰度的。

该数据集提供了大图像(高达4平方公里),分辨率高(每像素15厘米),并且每辆车都具有中心定位功能。我们假设汽车的平均尺寸为3米。我们创建了围绕每个汽车中心的箱子,以实现预测图像中箱子(即汽车)位置的最终目标。

基于航空影像深度学习的目标检测

图1:来自COWC数据集的示例图像

2.架构

为了检测这些大型航拍图像中的汽车,我们使用了RetinaNet架构(由Facebook FAIR于2017年发布,赢得了2017年ICCV最佳学生论文)。

对象检测体系结构分为两类:single-stage和two-stage。

two-stage体系结构首先将潜在对象分为两类:前景或背景。然后,将所有前景的潜在对象分类到更细的类别中:猫,狗,汽车等。这种two-stage方法非常缓慢,但会产生最佳准确性。最着名的two-stage架构是Faster-RCNN。

single-stage体系结构没有这个潜在前景对象的预选步骤。他们通常不太准确,但他们也更快。RetinaNet的single-stage体系结构是个例外:它具有single-stage速度和two-stage性能!

在下面的图2中,您可以看到各种对象检测体系结构的比较。

基于航空影像深度学习的目标检测

图2:对象检测算法的性能

RetinaNet由几个组件组成。我们将尝试描述数据如何通过每一步转换

基于航空影像深度学习的目标检测

图3:RetinaNet架构

2.1、卷积网络

首先是ResNet -50。就像每个卷积神经网络(CNN)一样,它将图像作为输入并通过多个卷积核进行处理。每个内核的输出都是一个特征映射 - 第一个特征映射捕获高级特征(例如线条或颜色)。我们越深入网络,特征映射就越小,因为有池化层。虽然它们较小,但它们也代表更多的细粒度信息(如眼睛,狗耳朵等)。输入图像有三个通道(红色,蓝色,绿色),但每个后续feature maps都有数十个通道!它们中的每一个代表它捕获的不同类型的特征。

一个普通的分类器使用ResNet的最后一个特征图(shape (7, 7, 2048)),在每个通道上应用一个平均池(resulting in(1, 1, 2048)),并将其馈送到一个具有softmax的全连接层。

2.2、特征金字塔网络(Feature Pyramid Network

ResNet之后,RetinaNet添加了一个特征金字塔网络(FPN),而不是添加分类器。通过从ResNet的不同层选取feature maps,它提供了丰富的多尺度特征。

基于航空影像深度学习的目标检测

图4:backbone 和FPN之间的横向连接

然而,ResNet的第一张特征图可能太粗糙,无法提取任何有用的信息。正如您在图4中看到的那样,更小更精确的特征图与更大的特征图组合在一起。我们首先对较小的那个进行上取样,然后将它与大一些的进行相加。存在几种上采样方法; 这里,上采样是用最近邻方法完成的。

FPN的每个级别都以不同的比例编码不同类型的信息。因此,他们每个人都应该参与对象检测任务。FPN将ResNet 的第三个(512 channels),第四个(1024 channels)和第五个(2048 channels)块的输出作为输入。第三个是第四个的一半,第四个是第五个的一半。

我们应用点卷积(与1x1核的卷积)将每个层的通道数统一为256。然后我们对较小的能级进行向上采样,使之与较大能级的尺寸相匹配。

2.3. Anchors

在每个FPN级别,几个anchors 都围绕FPN的特征图移动。anchor 是一个具有不同大小和比例的矩形,如下所示:

基于航空影像深度学习的目标检测

图5:不同尺寸和比例的anchors的示例

这些anchors 是潜在物体的基础位置。存在五种尺寸和三种比率,因此有15种独特的anchors 。这些anchors 也根据FPN层面的尺寸进行缩放。这些独特的anchors 在特征图中的所有可能位置上都是重复的。它导致了总共有K个anchors 。

2.4、回归和分类

每个FPN’s level被馈送到两个完全卷积网络(FCN),它们是仅由卷积和池化而成的神经网络。为了充分利用每个FPN的等级拥有不同类型信息的事实,这两个FCN在各个层次之间共享!卷积层独立于输入大小; 只有它们的内核大小很重要 因此,虽然每个FPN的特征图具有不同的大小,但它们可以全部馈送到相同的FCN。

第一个FCN是回归分支。它预测K x 4 (每个anchor 的x1,y1,x2,y2)值。这些值是略微修改了的原始anchor ,以便更好地适应潜在对象。所有可能的对象现在都有这种类型的坐标:

(x1 + dx1, y1 + dy1, x2 + dx2, y2 + dy2)

这里x?和y?是anchor的固定坐标,dx?,dy?由回归分支所产生的变化量。

我们现在拥有所有对象的最终坐标 - 也就是所有可能的对象。他们还没有被归类为背景或汽车,卡车等。

第二个FCN是分类分支。这是一个多标签问题,分类器用sigmoid 预测潜在对象K x N(N 是类的数量)。

2.5、删除重复项

此时我们有K x 4坐标和K x N类得分。我们现在有一个问题:对同一个对象检测同一个对象的几个盒子是很常见的!

基于航空影像深度学习的目标检测

图6:单个汽车检测到几个boxes

因此,对于每个类(即使它不是最高得分类),我们都会应用非最大限制。Tensorflow提供了一个功能:

tf.image.non_max_suppression(boxes, scores, max_output_size, iou_threshold)

这种方法的主要要点是它将删除重叠的盒子(如图6),只保留一个。它也使用scores保留最可能的盒子。

上面的Tensorflow方法的输入参数的一般评论:max_output_size对应于我们最后想要的最大盒子数量 - 比方说300。iou_threshold是一个介于0和1之间的浮点数,描述了接受的最大重叠比例。

基于航空影像深度学习的目标检测

图7:应用非最大抑制后的图6

2.6、保持最可能的类

现在删除同一地点的同一类的复制框。对于剩下的每个箱子,我们只保留得分最高的类(汽车,卡车等)。如果没有一个分数的分数高于固定阈值(我们使用0.4),则认为它是背景的一部分。

2.7. The Focal Loss

所有这些听起来都很复杂,但这并不是什么新鲜事 - 仅仅具有良好的准确性是不够的。RetinaNet的真正改善是它的损失:Focal Loss。没有潜在对象预选的single-stage体系结构会被背景对象的高频率所淹没。Focal Loss是根据低分量来处理的,通常是背景。

基于航空影像深度学习的目标检测

图8:我们定义 Pt, the confidence to be right

在图8中,我们定义了Pt

基于航空影像深度学习的目标检测

图9:The Focal Loss

在图9中,我们将交叉熵损失-log(Pt)组成一个因子(1 — Pt)^y 。这里y是一个在0和5之间振荡的调制因子。良好分类的例子具有很高的Pt ,因此也是一个低因素。因此,良好分类的例子的损失非常低,迫使模型学习更难的例子。您可以在图10中看到损失有多大影响。

基于航空影像深度学习的目标检测

图10The focal loss under various modulating factors

3.实现

我们使Keras 实现 RetinaNet。我们还编写了一个新的generator,将Pandas的数据框替换为CSV文件。

class DfGenerator(CSVGenerator):

"""Custom generator intented to work with in-memory Pandas' dataframe."""

def __init__(self, df, class_mapping, cols, base_dir='', **kwargs):

"""Initialization method.

Arguments:

df: Pandas DataFrame containing paths, labels, and bounding boxes.

class_mapping: Dict mapping label_str to id_int.

cols: Dict Mapping 'col_{filename/label/x1/y1/x2/y2} to corresponding df col.

"""

self.base_dir = base_dir

self.cols = cols

self.classes = class_mapping

self.labels = {v: k for k, v in self.classes.items()}

self.image_data = self._read_data(df)

self.image_names = list(self.image_data.keys())

Generator.__init__(self, **kwargs)

def _read_classes(self, df):

return {row[0]: row[1] for _, row in df.iterrows()}

def __len__(self):

return len(self.image_names)

def _read_data(self, df):

data = {}

for _, row in df.iterrows():

img_file, class_name = row[self.cols['col_filename']], row[self.cols['col_label']]

x1, y1 = row[self.cols['col_x1']], row[self.cols['col_y1']]

x2, y2 = row[self.cols['col_x2']], row[self.cols['col_y2']]

if img_file not in data:

data[img_file] = []

# Image without annotations

if not isinstance(class_name, str) and np.isnan(class_name):

continue

data[img_file].append({

'x1': int(x1), 'x2': int(x2),

'y1': int(y1), 'y2': int(y2),

'class': class_name

})

return data

您可以看到,没有注释的图像保存在培训阶段。

我们在COCO上使用预先训练的RetinaNet ,然后对COWC数据集进行微调。这个新任务只有两个FCN被重新训练,而ResNet主干和FPN被冻结

您可以在下面的Python代码块中看到如何加载RetinaNet并编译它。请注意,skip_mismatch=True加载重量时添加非常重要!权重是在COCO上创建的,具有80个类别,但在我们的例子中,我们只有1个类别,因此Anchors的数量并不相同。

def load_retinanet(weights, n_classes, freeze=True):

modifier = freeze_model if freeze else None

model = resnet50_retinanet(num_classes=num_classes, modifier=modifier)

model.load_weights(weights, by_name=True, skip_mismatch=True)

return model

def compile(model):

model.compile(

loss={

'regression' : keras_retinanet.losses.smooth_l1(),

'classification': keras_retinanet.losses.focal()

},

optimizer=optimizers.adam(lr=configs['lr'], clipnorm=0.001)

)

def train(model, train_gen, val_gen, callbacks, n_epochs=20):

"""train_gen and val_gen are instances of DfGenerator."""

model.fit_generator(

train_gen,

steps_per_epoch=len(train_gen),

validation_data=val_gen,

validation_steps=len(val_gen),

callbacks=callbacks,

epochs=n_epochs,

verbose=2

)

我们仍然需要处理一些东西,来自COWC数据集的图像达4平方公里,或者13k像素宽和高。那些大图像达300MB。向RetinaNet提供如此大的图像是不切实际的。因此,我们将图像分割成1000x1000像素(或150x150米)的小块。

然而,错过汽车将是愚蠢的,因为它们被切割成两块。为了避免这个问题,我们制作了一个1000x1000像素的滑动窗口,它的移动速度是800像素。这样,两个相邻补丁之间有一个200像素宽的重叠。

这导致了另一个问题:我们可能会检测到两次汽车。要删除重复项,我们在将小块绑定在一起时应用了非最大抑制。事实上,这意味着我们有两次非最大抑制:在RetinaNet之后并且将小块捆绑在一起。对于第二个非最大抑制,我们使用了PyImageSearch的Numpy版本:

# import the necessary packages

import numpy as np

# Malisiewicz et al.

def non_max_suppression_fast(boxes, overlapThresh):

# if there are no boxes, return an empty list

if len(boxes) == 0:

return []

# if the bounding boxes integers, convert them to floats --

# this is important since we'll be doing a bunch of divisions

if boxes.dtype.kind == "i":

boxes = boxes.astype("float")

# initialize the list of picked indexes

pick = []

# grab the coordinates of the bounding boxes

x1 = boxes[:,0]

y1 = boxes[:,1]

x2 = boxes[:,2]

y2 = boxes[:,3]

# compute the area of the bounding boxes and sort the bounding

# boxes by the bottom-right y-coordinate of the bounding box

area = (x2 - x1 + 1) * (y2 - y1 + 1)

idxs = np.argsort(y2)

# keep looping while some indexes still remain in the indexes

# list

while len(idxs) > 0:

# grab the last index in the indexes list and add the

# index value to the list of picked indexes

last = len(idxs) - 1

i = idxs[last]

pick.append(i)

# find the largest (x, y) coordinates for the start of

# the bounding box and the smallest (x, y) coordinates

# for the end of the bounding box

xx1 = np.maximum(x1[i], x1[idxs[:last]])

yy1 = np.maximum(y1[i], y1[idxs[:last]])

xx2 = np.minimum(x2[i], x2[idxs[:last]])

yy2 = np.minimum(y2[i], y2[idxs[:last]])

# compute the width and height of the bounding box

w = np.maximum(0, xx2 - xx1 + 1)

h = np.maximum(0, yy2 - yy1 + 1)

# compute the ratio of overlap

overlap = (w * h) / area[idxs[:last]]

# delete all indexes from the index list that have

idxs = np.delete(idxs, np.concatenate(([last],

np.where(overlap > overlapThresh)[0])))

# return only the bounding boxes that were picked using the

# integer data type

return boxes[pick].astype("int")

在处理航拍图像时,我们可以使用大量的数据增强。首先,我们可以翻转水平轴和垂直轴。我们也可以以任何角度旋转图像。如果图像的比例不统一(无人机对地距离可能不恒定),则随机缩小和放大图像也很有用。

4.结果

您可以在下面的图11和图12中看到我们的RetinaNet在盐湖城这个图像上的表现。

基于航空影像深度学习的目标检测

图11:在盐湖城4平方公里的地区内检测到13,000辆汽车

基于航空影像深度学习的目标检测

图12:图11的放大

5、评估模型

精度是够不够?我们需要知道我们得到多少假阳性和假阴性。如果我们发现到处都是车,我们会有很多假阳性,但是如果我们错过了大部分的车,那就是很多假阴性。

召回衡量前者,而精度衡量后者。最后,f1-score是这两个指标的组合。

def compute_metrics(true_pos, false_pos, false_neg):

"""Compute the precision, recall, and f1 score."""

precision = true_pos / (true_pos + false_pos)

recall = true_pos / (true_pos + false_neg)

if precision == 0 or recall == 0:

return precision, recall, f1

f1 = 2 / (1 / precision + 1 / recall)

return precision, recall, f1

然而,我们并不期望RetinaNet检测汽车精确的像素。因此,我们正在计算被检测车辆和地面真实车辆的Jaccard指数,如果它超过了一个选择的阈值,我们认为汽车被正确地检测到了

def jaccard(box_a, box_b):

"""box_X is a tuple of the shape (x1, y1, x2, y2)."""

side1 = max(0, min(a[2], b[2]) - max(a[0], b[0]))

side2 = max(0, min(a[3], b[3]) - max(a[1], b[1]))

inter = side1 * side2

area_a = (a[2] - a[0]) * (a[3] - a[1])

area_b = (b[2] - b[0]) * (b[3] - b[1])

union = area_a + area_b - inter

return inter / union

def is_valid(box_pred, box_true, threshold=0.3):

return jaccard(box_red, box_true) >= threshold

基于航空影像深度学习的目标检测

图13:真阳性(绿色),假阳性(黄色)和假阴性(红色)

请注意,在四个误报中,其中两个是垃圾桶,一个是重复的,一个实际上是......一辆车!事实上,就像在每个数据集中一样,地面真实注释中可能存在一些错误。

在图12中,f1分数是0.91。通常在更多的城市环境中,f1分数大约为0.95。

相关推荐