如何将边界框与 groupRectangle 合并?

Posted

技术标签:

【中文标题】如何将边界框与 groupRectangle 合并?【英文标题】:How to merge bounding boxes with groupRectangle? 【发布时间】:2019-11-26 14:56:24 【问题描述】:

我有一个带有如下边界框的图像:

我想合并重叠的边界框。

我试过了:cv::groupRectangles(detected, 1, 0.8)

我的期望是每个集群都有一个盒子。

但我得到了这个:

如您所见,问题是,中间的飞镖靶和右边的飞镖都没有盒子

我该如何解决这个问题?我宁愿使用 OpenCV api 而不是编写我自己的合并算法。

我看到它消除了由一个框限定的区域。我希望它不要那样做。

我曾尝试随机调整参数,但结果更糟。我希望得到一些正确方向的指导。

【问题讨论】:

【参考方案1】:

如何定义重叠的矩形?

我们需要一种方法来定义两个矩形何时重叠。我们可以使用&交集运算符来查找两个矩形的交集,并检查它是否为空:

bool overlap(const cv::Rect& lhs, const cv::Rect& rhs) 
    return (lhs & rhs).area() > 0;

如果我们想忽略小的交叉路口,我们可以在交叉路口区域上使用一个阈值:

bool overlap(const cv::Rect& lhs, const cv::Rect& rhs, int th) 
    return (lhs & rhs).area() > th;

但现在阈值取决于矩形的尺寸。我们可以使用 [0, 1] 范围内的 "Intersection over Union" 指标 (IoU),并在该区间内应用阈值。

bool overlap(const cv::Rect& lhs, const cv::Rect& rhs, double th) 
    double i = static_cast<double>((lhs & rhs).area());
    double u = static_cast<double>((lhs | rhs).area());
    double iou = i / u;
    return iou > th;

这通常效果很好,但如果两个矩形的大小非常不同,则可能会显示意想不到的结果。另一种方法是检查第一个矩形是否与第二个矩形的大部分区域相交,反之亦然:

bool overlap(const cv::Rect& lhs, const cv::Rect& rhs, double th) 
    double i = static_cast<double>((lhs & rhs).area());
    double ratio_intersection_over_lhs_area = i / static_cast<double>(lhs.area());
    double ratio_intersection_over_rhs_area = i / static_cast<double>(rhs.area());
    return (ratio_intersection_over_lhs_area > th) || (ratio_intersection_over_rhs_area > th);        
    

好的,现在我们有几种方法可以定义两个矩形何时重叠。选择一个。

如何找到重叠的矩形?

我们可以用cv::partition 对矩形进行聚类,并使用一个谓词将重叠的矩形放在同一个聚类中。这会将两个不直接相互重叠但由一个或多个重叠矩形链接的矩形放入同一个簇中。该函数的输出是一个簇向量,其中每个簇包含一个矩形向量:

std::vector<std::vector<cv::Rect>> cluster_rects(const std::vector<cv::Rect>& rects, const double th)

    std::vector<int> labels;
    int n_labels = cv::partition(rects, labels, [th](const cv::Rect& lhs, const cv::Rect& rhs) 
        double i = static_cast<double>((lhs & rhs).area());
        double ratio_intersection_over_lhs_area = i / static_cast<double>(lhs.area());
        double ratio_intersection_over_rhs_area = i / static_cast<double>(rhs.area());
        return (ratio_intersection_over_lhs_area > th) || (ratio_intersection_over_rhs_area > th);
    );

    std::vector<std::vector<cv::Rect>> clusters(n_labels);
    for (size_t i = 0; i < rects.size(); ++i) 
        clusters[labels[i]].push_back(rects[i]);
    

    return clusters;

例如,从这张图片中的矩形:

我们获得了这些集群(阈值为0.2)。请注意:

    在左上角的簇中,三个矩形不相互重叠 右上角的矩形在它自己的簇上,因为它与其他矩形的相交不够。

如何找到代表簇的矩形?

嗯,这真的取决于应用程序。可以是所有矩形的并集:

cv::Rect union_of_rects(const std::vector<cv::Rect>& cluster)

    cv::Rect one;
    if (!cluster.empty())
    
        one = cluster[0];
        for (const auto& r : cluster)  one |= r; 
    
    return one;

也可以是最大内接矩形(代码如下):

或者别的什么。例如,如果您有一个与每个矩形相关联的 score(例如,这是一个有信心的检测),您可以按分数对每个集群进行排序,并且只取第一个。这是一个非最大值抑制 (NMA) 的示例,您只保留每个集群的最高得分矩形(此答案中未显示)。

选择一个。

在我用于创建这些图像的工作代码下方。请玩它:)

#include <opencv2/opencv.hpp>

std::vector<cv::Rect> create_some_rects()

    std::vector<cv::Rect> rects
    
    20, 20, 20, 40,
    30, 40, 40, 40,
    50, 46, 30, 40,
    100, 120, 30, 40,
    110, 130, 36, 20,
    104, 124, 50, 30,
    200, 80, 40, 50,
    220, 90, 50, 30,
    240, 84, 30, 70,
    260, 60, 20, 30,
    ;
    return rects;


void draw_rects(cv::Mat3b& img, const std::vector<cv::Rect>& rects)

    for (const auto& r : rects) 
        cv::Scalar random_color(rand() & 255, rand() & 255, rand() & 255);
        cv::rectangle(img, r, random_color);
    


void draw_rects(cv::Mat3b& img, const std::vector<cv::Rect>& rects, const cv::Scalar& color)

    for (const auto& r : rects) 
        cv::rectangle(img, r, color);
    


void draw_clusters(cv::Mat3b& img, const std::vector<std::vector<cv::Rect>>& clusters)

    for (const auto& cluster : clusters) 
        cv::Scalar random_color(rand() & 255, rand() & 255, rand() & 255);
        draw_rects(img, cluster, random_color);
    


std::vector<std::vector<cv::Rect>> cluster_rects(const std::vector<cv::Rect>& rects, const double th)

    std::vector<int> labels;
    int n_labels = cv::partition(rects, labels, [th](const cv::Rect& lhs, const cv::Rect& rhs) 
        double i = static_cast<double>((lhs & rhs).area());
        double ratio_intersection_over_lhs_area = i / static_cast<double>(lhs.area());
        double ratio_intersection_over_rhs_area = i / static_cast<double>(rhs.area());
        return (ratio_intersection_over_lhs_area > th) || (ratio_intersection_over_rhs_area > th);
    );

    std::vector<std::vector<cv::Rect>> clusters(n_labels);
    for (size_t i = 0; i < rects.size(); ++i) 
        clusters[labels[i]].push_back(rects[i]);
    

    return clusters;


cv::Rect union_of_rects(const std::vector<cv::Rect>& cluster)

    cv::Rect one;
    if (!cluster.empty())
    
        one = cluster[0];
        for (const auto& r : cluster)  one |= r; 
    
    return one;



// https://***.com/a/30418912/5008845
// https://***.com/a/34905215/5008845
cv::Rect findMaxRect(const cv::Mat1b& src)

    cv::Mat1f W(src.rows, src.cols, float(0));
    cv::Mat1f H(src.rows, src.cols, float(0));

    cv::Rect maxRect(0, 0, 0, 0);
    float maxArea = 0.f;

    for (int r = 0; r < src.rows; ++r)
    
        for (int c = 0; c < src.cols; ++c)
        
            if (src(r, c) == 0)
            
                H(r, c) = 1.f + ((r > 0) ? H(r - 1, c) : 0);
                W(r, c) = 1.f + ((c > 0) ? W(r, c - 1) : 0);
            

            float minw = W(r, c);
            for (int h = 0; h < H(r, c); ++h)
            
                minw = std::min(minw, W(r - h, c));
                float area = (h + 1) * minw;
                if (area > maxArea)
                
                    maxArea = area;
                    maxRect = cv::Rect(cv::Point(c - minw + 1, r - h), cv::Point(c + 1, r + 1));
                
            
        
    
    return maxRect;


cv::Rect largest_inscribed_of_rects(const std::vector<cv::Rect>& cluster)

    cv::Rect roi = union_of_rects(cluster);

    cv::Mat1b mask(roi.height, roi.width, uchar(255));
    for (const auto& r : cluster) 
        cv::rectangle(mask, r - roi.tl(), cv::Scalar(0), cv::FILLED);
    

    cv::Rect largest_rect = findMaxRect(mask);
    largest_rect += roi.tl();

    return largest_rect;




std::vector<cv::Rect> find_one_for_cluster(const std::vector<std::vector<cv::Rect>>& clusters)

    std::vector<cv::Rect> one_for_cluster;
    for (const auto& cluster : clusters) 
        //cv::Rect one = union_of_rects(cluster);
        cv::Rect one = largest_inscribed_of_rects(cluster);
        one_for_cluster.push_back(one);
    
    return one_for_cluster;



int main()

    cv::Mat3b img(200, 300, cv::Vec3b(0, 0, 0));

    std::vector<cv::Rect> rects = create_some_rects();

    cv::Mat3b initial_rects_img = img.clone();
    draw_rects(initial_rects_img, rects, cv::Scalar(127, 127, 127));

    std::vector<std::vector<cv::Rect>> clusters = cluster_rects(rects, 0.2);

    cv::Mat3b clustered_rects_img = initial_rects_img.clone();
    draw_clusters(clustered_rects_img, clusters);

    std::vector<cv::Rect> single_rects = find_one_for_cluster(clusters);

    cv::Mat3b single_rects_img = initial_rects_img.clone();
    draw_rects(single_rects_img, single_rects);

    return 0;

【讨论】:

【参考方案2】:

很遗憾,您无法微调 groupRectangles()。您的示例的第二个参数应该是 0。使用 1,所有奇异矩形必须在某处合并。

如果您想要更好地对小矩形进行聚类,您可以首先生成小矩形并使用保守的阈值参数。虽然不是最佳解决方案。


如果您想根据重叠条件进行聚类,我建议您为此编写自己的简单算法。 groupRectangles() 根本不这样做。它找到大小和位置相似的矩形;它不会累积形成簇的矩形。

您可以用矩形填充掩码cv::Mat1b mask(image.size(), uchar(0));,然后使用cv::connectedComponents() 查找合并区域。请注意,填充是微不足道的,循环遍历所有矩形并调用mask(rect).setTo(255);。如果重叠并不总是可靠的,您可以在连接组件步骤之前使用cv::dilate() 在掩码中增长矩形。

您可以测试所有矩形是否有重叠并相应地关联它们。对于大量的矩形,我建议使用 disjoint-set/union-find 数据结构来提高效率。

【讨论】:

谢谢!我将首先尝试挑出所有“单个”矩形,然后将 opencv 的 groupRectangle 应用于剩余的矩形。您能否详细说明如何使用 union-find 来完成这项任务? :) 我在编辑答案时提供了一个更易于实现的替代方案。

以上是关于如何将边界框与 groupRectangle 合并?的主要内容,如果未能解决你的问题,请参考以下文章

用于 Opencv 的 groupRectangles 的 Mex

将数据框与 SpatialPolygonsDataFrame 合并

将具有两个日期列的一个数据框与另一个具有两个日期列的数据框合并

将数据框与系列合并

将大型 Dask 数据框与小型 Pandas 数据框合并

Pandas 将数据框与共享列合并,左右填充