计算边界框重叠的百分比,用于图像检测器评估

Posted

技术标签:

【中文标题】计算边界框重叠的百分比,用于图像检测器评估【英文标题】:Calculating percentage of Bounding box overlap, for image detector evaluation 【发布时间】:2014-10-10 12:46:56 【问题描述】:

在大图像中测试对象检测算法时,我们将检测到的边界框与给定的地面实况矩形坐标进行对比。

根据 Pascal VOC 挑战,有这样的:

如果预测的边界框重叠更多,则认为它是正确的 超过 50% 使用真实边界框,否则边界框 被认为是误报检测。多重检测是 受到处罚。如果系统预测几个重叠的边界框 使用单个真实边界框,只有一个预测是 被认为是正确的,其他被认为是误报。

这意味着我们需要计算重叠百分比。这是否意味着检测到的边界框覆盖了地面实况框 50%?还是说边界框的 50% 被地面实况框吸收了?

我已经搜索过,但我还没有找到一个标准算法——这令人惊讶,因为我认为这在计算机视觉中很常见。 (我是新手)。我错过了吗?有谁知道这类问题的标准算法是什么?

【问题讨论】:

【参考方案1】:
import numpy as np

def box_area(arr):
    # arr: np.array([[x1, y1, x2, y2]])
    width = arr[:, 2] - arr[:, 0]
    height = arr[:, 3] - arr[:, 1]
    return width * height

def _box_inter_union(arr1, arr2):
    # arr1 of [N, 4]
    # arr2 of [N, 4]
    area1 = box_area(arr1)
    area2 = box_area(arr2)

    # Intersection
    top_left = np.maximum(arr1[:, :2], arr2[:, :2]) # [[x, y]]
    bottom_right = np.minimum(arr1[:, 2:], arr2[:, 2:]) # [[x, y]]
    wh = bottom_right - top_left
    # clip: if boxes not overlap then make it zero
    intersection = wh[:, 0].clip(0) * wh[:, 1].clip(0)

    #union 
    union = area1 + area2 - intersection
    return intersection, union

def box_iou(arr1, arr2):
    # arr1[N, 4]
    # arr2[N, 4]
    # N = number of bounding boxes
    assert(arr1[:, 2:] > arr[:, :2]).all()
    assert(arr2[:, 2:] > arr[:, :2]).all()
    inter, union = _box_inter_union(arr1, arr2)
    iou = inter / union
    print(iou)
box1 = np.array([[10, 10, 80, 80]])
box2 = np.array([[20, 20, 100, 100]])
box_iou(box1, box2)

参考:https://pytorch.org/vision/stable/_modules/torchvision/ops/boxes.html#nms

【讨论】:

虽然这段代码可以回答这个问题,但这里有很多要阅读的内容,并且没有关于代码作用的描述(外部链接不算在内!)。您能否添加评论以帮助其他读者?【参考方案2】:

适用于任何类型多边形的简单方法。

(图片未按比例绘制)

from shapely.geometry import Polygon


def calculate_iou(box_1, box_2):
    poly_1 = Polygon(box_1)
    poly_2 = Polygon(box_2)
    iou = poly_1.intersection(poly_2).area / poly_1.union(poly_2).area
    return iou


box_1 = [[511, 41], [577, 41], [577, 76], [511, 76]]
box_2 = [[544, 59], [610, 59], [610, 94], [544, 94]]

print(calculate_iou(box_1, box_2))

结果将是0.138211...,这意味着13.82%



注意:shapely 库中坐标系的原点是左下角,而计算机图形学中的坐标系是左上角。此差异不会影响 IoU 计算,但如果您进行其他类型的计算,此信息可能会有所帮助。

【讨论】:

很高兴使用已经具有功能的库。但我几乎 100% 确定这段代码是错误的:iou = poly_1.intersection(poly_2).area / poly_1.union(poly_2).area。您正在计算两个框相交的面积。并除以两个盒子的并集面积。好吧,去看看“杰卡德指数”(IoU)公式。正确的 Jaccard 指数公式为:iou = intersection_area / (union_area - intersection_area) 其实,Shapely 中的“union”函数已经忽略了交集。所以你的代码是正确的。证明:poly_1.areapoly_2.area 都是 2310poly_1.union(poly_2).area4059poly_1.intersection(poly_2).area561。并证明一切:4059+561 == 2310+2310。两者总和为4620。所以是的,您的代码是正确的并且遵循 Jaccard 公式,因为 Shapely 计算了它的并减交集。不错。 图中红框底部两点坐标标注错误。这些应该交换。 感谢您的回答以及您抽出的时间。【参考方案3】:

对于相交距离,我们不应该加一个+1以便有

intersection_area = (x_right - x_left + 1) * (y_bottom - y_top + 1)   

(AABB 也一样) 喜欢这个pyimage search post

我同意 (x_right - x_left) x (y_bottom - y_top) 在数学中使用点坐标,但由于我们处理的是像素,所以我认为不同。

考虑一维示例:

2分:x1 = 1x2 = 3,距离确实是x2-x1 = 2 2 个像素的索引:i1 = 1i2 = 3,从像素 i1 到 i2 的段包含 3 个像素,即 l = i2 - i1 + 1

编辑:我最近知道这是一种“小方块”方法。 但是,如果您将像素视为点样本(即边界框角将位于像素的中心,显然在 matplotlib 中),那么您不需要 +1。 见this comment和this illustration

【讨论】:

你是对的...1920x1080 屏幕的索引从0(第一个像素)到1919(水平方向的最后一个像素)和从0(第一个像素)到@987654329 @(垂直最后一个像素)。因此,如果我们在“像素坐标空间”中有一个矩形,要计算其面积,我们必须在每个方向上加 1。否则想象我们的 1920x1080 屏幕有一个带有left=0,top=0,right=1919,bottom=1079 的全屏矩形。好吧,我们知道1920x1080 像素是2073600 像素。但是使用错误的area = (x_right - x_left) * (y_bottom - y_top) 数学,我们得到:(1919 - 0) * (1079 - 0) = 1919 * 1079 = 2070601 像素! 我已经做了很多测试来验证,现在已经根据您的正确观察提交了已接受答案的编辑。谢谢!我想知道这些年来有多少代码库复制粘贴了原始的错误数学。 ;-) 修正错误编辑的批准存在很多问题,因此我在此页面上发布了单独的答案。简短的回答是:你是对的。像素范围为inclusive:inclusive,因此如果我们想要像素范围的真实区域,我们必须将+ 1 添加到每个轴。【参考方案4】:

您可以使用torchvision 进行如下计算。 bbox以[x1, y1, x2, y2]的格式准备。

import torch
import torchvision.ops.boxes as bops

box1 = torch.tensor([[511, 41, 577, 76]], dtype=torch.float)
box2 = torch.tensor([[544, 59, 610, 94]], dtype=torch.float)
iou = bops.box_iou(box1, box2)
# tensor([[0.1382]])

【讨论】:

谢谢你,这个答案应该更适合任何不想打扰技术的人【参考方案5】:

如果您使用屏幕(像素)坐标,top-voted answer 会出现数学错误!几周前我提交了an edit,并为所有读者提供了很长的解释,以便他们理解数学。但是那个编辑不被审稿人理解并被删除,所以我再次提交了相同的编辑,但这次更简要地总结了。 (更新:Rejected 2vs1,因为它被视为“重大变化”,呵呵)。

因此,我将在这个单独的答案中完整地解释其数学问题。

所以,是的,一般来说,票数最高的答案是正确的,并且是计算 IoU 的好方法。但是(正如其他人也指出的那样)它的数学对于计算机屏幕是完全不正确的。你不能只做(x2 - x1) * (y2 - y1),因为这不会产生正确的面积计算。屏幕索引从像素0,0 开始,到width-1,height-1 结束。屏幕坐标的范围是inclusive:inclusive(包括两端),所以像素坐标中从010的范围实际上是11像素宽,因为它包括0 1 2 3 4 5 6 7 8 9 10(11项)。因此,要计算屏幕坐标的面积,您必须为每个维度添加 +1,如下所示:(x2 - x1 + 1) * (y2 - y1 + 1)

如果您在范围不包含在内的其他坐标系中工作(例如inclusive:exclusive 系统,其中010 表示“元素0-9 但不是10”),那么这个额外的数学不是必需的。但最有可能的是,您正在处理基于像素的边界框。嗯,屏幕坐标从0,0 开始,然后从那里向上。

1920x1080 屏幕的索引从0(第一个像素)到1919(水平最后一个像素)和从0(第一个像素)到1079(垂直最后一个像素)。

因此,如果我们在“像素坐标空间”中有一个矩形,要计算其面积,我们必须在每个方向上加 1。否则,我们得到面积计算的错误答案。

假设我们的1920x1080 屏幕有一个基于像素坐标的矩形left=0,top=0,right=1919,bottom=1079(覆盖整个屏幕上的所有像素)。

好吧,我们知道1920x1080 像素是2073600 像素,这是 1080p 屏幕的正确区域。

但如果数学错误area = (x_right - x_left) * (y_bottom - y_top),我们会得到:(1919 - 0) * (1079 - 0) = 1919 * 1079 = 2070601 像素!错了!

这就是为什么我们必须在每个计算中添加+1,这给了我们以下校正数学:area = (x_right - x_left + 1) * (y_bottom - y_top + 1),给我们:(1919 - 0 + 1) * (1079 - 0 + 1) = 1920 * 1080 = 2073600 像素!这确实是正确的答案!

最简短的总结是:像素坐标范围是inclusive:inclusive,所以如果我们想要像素坐标范围的真实区域,我们必须在每个轴上添加+ 1

有关为什么需要 +1 的更多详细信息,请参阅 Jindil 的回答:https://***.com/a/51730512/8874388

以及这篇 pyimagesearch 文章: https://www.pyimagesearch.com/2016/11/07/intersection-over-union-iou-for-object-detection/

还有这个 GitHub 评论: https://github.com/AlexeyAB/darknet/issues/3995#issuecomment-535697357

由于固定的数学没有被批准,任何从投票最多的答案复制代码的人都希望看到这个答案,并且能够自己修复它,只需复制下面的错误修复断言和面积计算行,已针对inclusive:inclusive(像素)坐标范围进行了修复:

    assert bb1['x1'] <= bb1['x2']
    assert bb1['y1'] <= bb1['y2']
    assert bb2['x1'] <= bb2['x2']
    assert bb2['y1'] <= bb2['y2']

................................................

    # The intersection of two axis-aligned bounding boxes is always an
    # axis-aligned bounding box.
    # NOTE: We MUST ALWAYS add +1 to calculate area when working in
    # screen coordinates, since 0,0 is the top left pixel, and w-1,h-1
    # is the bottom right pixel. If we DON'T add +1, the result is wrong.
    intersection_area = (x_right - x_left + 1) * (y_bottom - y_top + 1)

    # compute the area of both AABBs
    bb1_area = (bb1['x2'] - bb1['x1'] + 1) * (bb1['y2'] - bb1['y1'] + 1)
    bb2_area = (bb2['x2'] - bb2['x1'] + 1) * (bb2['y2'] - bb2['y1'] + 1)

【讨论】:

【参考方案6】:

对于轴对齐的边界框,它相对简单。 “轴对齐”表示边界框不旋转;或者换句话说,框线平行于轴。下面是计算两个轴对齐边界框的 IoU 的方法。

def get_iou(bb1, bb2):
    """
    Calculate the Intersection over Union (IoU) of two bounding boxes.

    Parameters
    ----------
    bb1 : dict
        Keys: 'x1', 'x2', 'y1', 'y2'
        The (x1, y1) position is at the top left corner,
        the (x2, y2) position is at the bottom right corner
    bb2 : dict
        Keys: 'x1', 'x2', 'y1', 'y2'
        The (x, y) position is at the top left corner,
        the (x2, y2) position is at the bottom right corner

    Returns
    -------
    float
        in [0, 1]
    """
    assert bb1['x1'] < bb1['x2']
    assert bb1['y1'] < bb1['y2']
    assert bb2['x1'] < bb2['x2']
    assert bb2['y1'] < bb2['y2']

    # determine the coordinates of the intersection rectangle
    x_left = max(bb1['x1'], bb2['x1'])
    y_top = max(bb1['y1'], bb2['y1'])
    x_right = min(bb1['x2'], bb2['x2'])
    y_bottom = min(bb1['y2'], bb2['y2'])

    if x_right < x_left or y_bottom < y_top:
        return 0.0

    # The intersection of two axis-aligned bounding boxes is always an
    # axis-aligned bounding box
    intersection_area = (x_right - x_left) * (y_bottom - y_top)

    # compute the area of both AABBs
    bb1_area = (bb1['x2'] - bb1['x1']) * (bb1['y2'] - bb1['y1'])
    bb2_area = (bb2['x2'] - bb2['x1']) * (bb2['y2'] - bb2['y1'])

    # compute the intersection over union by taking the intersection
    # area and dividing it by the sum of prediction + ground-truth
    # areas - the interesection area
    iou = intersection_area / float(bb1_area + bb2_area - intersection_area)
    assert iou >= 0.0
    assert iou <= 1.0
    return iou

说明

图片来自this answer

【讨论】:

这段代码有一个错误——y_top = max(bb1['y1'], bb2['y1']) 应该使用min。同样y_bottom 应该使用max @JamesMeakin:代码是正确的。 y=0 位于顶部,向下增加。 然后复制粘贴将不起作用。到目前为止,我在检测中只有轴对齐的边界框。对于语义分割,有任意复杂的形状。但概念是一样的。 @MartinThoma 这是否适用于另一个矩形内的矩形? 代码中确实存在错误,但不是 James Meaking 建议的错误。如果您使用的是 PIXEL COORDINATES,则该错误出现在面积计算中。计算机屏幕使用从0,0(左上角)开始到w-1, h-1 结束的像素/矩形。坐标是inclusive:inclusive。原始函数中使用的数学运算失败了。我已经提交了一个单独的答案,其中仅包含固定的数学公式,并详细解释了为什么需要修复。感谢 Martin 的原始功能。通过修复,我现在在我的 AI/像素分析代码中使用它! 【参考方案7】:

这种方法怎么样?可以扩展到任意数量的联合形状

surface = np.zeros([1024,1024])
surface[1:1+10, 1:1+10] += 1
surface[100:100+500, 100:100+100] += 1
unionArea = (surface==2).sum()
print(unionArea)

【讨论】:

像这样制作一个固定大小的矩阵并在每个形状的偏移处填充数字似乎有点疯狂。尝试使用 Python 的 Shapely 库。它具有用于计算各种形状的交集和并集的辅助函数。我没有尝试用它做任意(非盒子)形状,但它可能是可能的。 我所说的“疯狂”是指:缓慢且内存膨胀。 Shapely 库使用更智能的数学来处理复杂的交叉点/面积计算,以及当对象彼此根本不靠近时的快捷方式等。是的,我刚刚验证了 Shapely 完美地处理了复杂的形状、多边形、旋转的形状等。跨度> 【参考方案8】:

在下面的 sn-p 中,我沿着第一个框的边缘构造了一个多边形。然后我使用 Matplotlib 将多边形剪辑到第二个框。生成的多边形包含四个顶点,但是我们只对左上角和右下角感兴趣,所以我取坐标的最大值和最小值得到一个边界框,返回给用户。

import numpy as np
from matplotlib import path, transforms

def clip_boxes(box0, box1):
    path_coords = np.array([[box0[0, 0], box0[0, 1]],
                            [box0[1, 0], box0[0, 1]],
                            [box0[1, 0], box0[1, 1]],
                            [box0[0, 0], box0[1, 1]]])

    poly = path.Path(np.vstack((path_coords[:, 0],
                                path_coords[:, 1])).T, closed=True)
    clip_rect = transforms.Bbox(box1)

    poly_clipped = poly.clip_to_bbox(clip_rect).to_polygons()[0]

    return np.array([np.min(poly_clipped, axis=0),
                     np.max(poly_clipped, axis=0)])

box0 = np.array([[0, 0], [1, 1]])
box1 = np.array([[0, 0], [0.5, 0.5]])

print clip_boxes(box0, box1)

【讨论】:

就坐标而言,返回值表示:[[ x1 y1 ] [ x2 y2 ]],对吗? 而且输入框也应该符合相同的坐标表示,对吧? 谢谢 - 我已经用了一段时间了!但现在有时会出错,我不知道为什么:***.com/questions/26712637/…

以上是关于计算边界框重叠的百分比,用于图像检测器评估的主要内容,如果未能解决你的问题,请参考以下文章

为啥物体检测 CNN 的边界框必须与图像边界平行?

给定具有多个边界框的图像,如何仅突出显示完全在另一个边界框内的那些边界框?

深度学习和目标检测系列教程 4-300:目标检测入门之目标变量和损失函数

对象检测/分类任务的性能指标(用于图像)

如何使用 tf.image.draw_bounding_boxes 在原始图像上绘制边界框以显示检测到对象的位置?

如何使用Albumentations 对目标检测任务做增强