生成包含多个 2D 点的矩形的快速算法

Posted

技术标签:

【中文标题】生成包含多个 2D 点的矩形的快速算法【英文标题】:Fast algorithm to generate rectangles that contains a number of 2D points 【发布时间】:2021-12-10 17:06:00 【问题描述】:

我有一个问题正在努力解决。 鉴于以下情况:

包含二维点的数组all_points,每个点表示为一个元组(x, y)。 一个数组musthave_points,包含all_points中的点的索引。 一个整数m,与m < len(all_points)

返回一个矩形列表,其中一个矩形由一个包含其4个顶点((x0, y0), (x1, y1), (x2, y2), (x3, y3))的元组表示,每个矩形必须满足以下条件:

    包含来自all_pointsm 点,这些m 点必须完全位于矩形内,即不在矩形的任何4 个边缘上。 包含来自musthave_points 的所有点。如果musthave_points为空列表,则矩形只需要满足第一个条件即可。

如果没有这样的矩形,则返回一个空列表。如果两个矩形包含相同的点子集并且输出中不应有“相同”的矩形,则它们被认为是“相同的”。

注意:一种简单的暴力破解解决方案是首先生成m 点的所有组合,每个组合都包含来自musthave_points 的所有点。对于每个组合,创建一个覆盖组合中所有点的矩形。然后计算矩形内的点数,如果点数为m,则为有效矩形。 但是该解决方案以阶乘时间复杂度运行。你能想出比这更快的方法吗?

我已经实现了如下所示的蛮力,但是速度非常慢。

import itertools
import numpy as np 
import cv2 
import copy 
import sys 

from shapely.geometry import Point
from shapely.geometry.polygon import Polygon

# Credit: https://github.com/dbworth/minimum-area-bounding-rectangle/blob/master/python/min_bounding_rect.py
def minBoundingRect(hull_points_2d):
    #print "Input convex hull points: "
    #print hull_points_2d

    # Compute edges (x2-x1,y2-y1)
    edges = np.zeros((len(hull_points_2d) - 1, 2)) # empty 2 column array
    for i in range(len(edges)):
        edge_x = hull_points_2d[i+1, 0] - hull_points_2d[i, 0]
        edge_y = hull_points_2d[i+1, 1] - hull_points_2d[i, 1]
        edges[i] = [edge_x,edge_y]

    # Calculate edge angles   atan2(y/x)
    edge_angles = np.zeros((len(edges))) # empty 1 column array
    for i in range(len(edge_angles)):
        edge_angles[i] = np.math.atan2(edges[i,1], edges[i,0])

    # Check for angles in 1st quadrant
    for i in range(len(edge_angles)):
        edge_angles[i] = np.abs(edge_angles[i] % (np.math.pi/2)) # want strictly positive answers

    # Remove duplicate angles
    edge_angles = np.unique(edge_angles)

    # Test each angle to find bounding box with smallest area
    min_bbox = (0, sys.maxsize, 0, 0, 0, 0, 0, 0) # rot_angle, area, width, height, min_x, max_x, min_y, max_y
    for i in range(len(edge_angles) ):
        R = np.array([[np.math.cos(edge_angles[i]), np.math.cos(edge_angles[i]-(np.math.pi/2))], [np.math.cos(edge_angles[i]+(np.math.pi/2)), np.math.cos(edge_angles[i])]])

        # Apply this rotation to convex hull points
        rot_points = np.dot(R, np.transpose(hull_points_2d)) # 2x2 * 2xn

        # Find min/max x,y points
        min_x = np.nanmin(rot_points[0], axis=0)
        max_x = np.nanmax(rot_points[0], axis=0)
        min_y = np.nanmin(rot_points[1], axis=0)
        max_y = np.nanmax(rot_points[1], axis=0)

        # Calculate height/width/area of this bounding rectangle
        width = max_x - min_x
        height = max_y - min_y
        area = width*height

        # Store the smallest rect found first (a simple convex hull might have 2 answers with same area)
        if (area < min_bbox[1]):
            min_bbox = (edge_angles[i], area, width, height, min_x, max_x, min_y, max_y)

    # Re-create rotation matrix for smallest rect
    angle = min_bbox[0]   
    R = np.array([[np.math.cos(angle), np.math.cos(angle-(np.math.pi/2))], [np.math.cos(angle+(np.math.pi/2)), np.math.cos(angle)]])

    # Project convex hull points onto rotated frame
    proj_points = np.dot(R, np.transpose(hull_points_2d)) # 2x2 * 2xn
    #print "Project hull points are \n", proj_points

    # min/max x,y points are against baseline
    min_x = min_bbox[4]
    max_x = min_bbox[5]
    min_y = min_bbox[6]
    max_y = min_bbox[7]
    #print "Min x:", min_x, " Max x: ", max_x, "   Min y:", min_y, " Max y: ", max_y

    # Calculate center point and project onto rotated frame
    center_x = (min_x + max_x)/2
    center_y = (min_y + max_y)/2
    center_point = np.dot([center_x, center_y], R)
    #print "Bounding box center point: \n", center_point

    # Calculate corner points and project onto rotated frame
    corner_points = np.zeros((4,2)) # empty 2 column array
    corner_points[0] = np.dot([max_x, min_y], R)
    corner_points[1] = np.dot([min_x, min_y], R)
    corner_points[2] = np.dot([min_x, max_y], R)
    corner_points[3] = np.dot([max_x, max_y], R)

    return (angle, min_bbox[1], min_bbox[2], min_bbox[3], center_point, corner_points) # rot_angle, area, width, height, center_point, corner_points

class PatchGenerator:
    def __init__(self, all_points, musthave_points, m):
        self.all_points = copy.deepcopy(all_points)
        self.n = len(all_points)
        self.musthave_points = copy.deepcopy(musthave_points)
        self.m = m

    @staticmethod
    def create_rectangle(points):
        rot_angle, area, width, height, center_point, corner_points = minBoundingRect(points)
        return corner_points 

    @staticmethod
    def is_point_inside_rectangle(rect, point):
        pts = Point(*point)
        polygon = Polygon(rect)

        return polygon.contains(pts)

    def check_valid_rectangle(self, rect, the_complement):
        # checking if the rectangle contains any other point from `the_complement`
        for point in the_complement:
            if self.is_point_inside_rectangle(rect, point):
                return False
        return True 

    def generate(self):

        rects = [] 
        # generate all combinations of m points, including points from musthave_points
        the_rest_indices = list(set(range(self.n)).difference(self.musthave_points))
        comb_indices = itertools.combinations(the_rest_indices, self.m - len(self.musthave_points))
        comb_indices = [self.musthave_points + list(inds) for inds in comb_indices]

        # for each combination
        for comb in comb_indices:
            comb_points = np.array(self.all_points)[comb]
            ## create the rectangle that covers all m points
            rect = self.create_rectangle(comb_points)

            ## check if the rectangle is valid 
            the_complement_indices = list(set(range(self.n)).difference(comb))
            the_complement_points = list(np.array(self.all_points)[the_complement_indices])

            if self.check_valid_rectangle(rect, the_complement_points):
                rects.append([comb, rect]) # indices of m points and 4 vertices of the valid rectangle

        return rects 

if __name__ == '__main__':
    all_points = [[47.43, 20.5 ], [47.76, 43.8 ], [47.56, 23.74], [46.61, 23.73], [47.49, 18.94], [46.95, 25.29], [54.31, 23.5], [48.07, 17.77],
                        [48.2 , 34.87], [47.24, 22.07], [47.32, 27.05], [45.56, 17.95], [41.29, 19.33], [45.48, 28.49], [42.94, 15.24], [42.05, 34.3 ],
                        [41.04, 26.3 ], [45.37, 21.17], [45.44, 24.78], [44.54, 43.89], [30.49, 26.79], [40.55, 22.81]]
    musthave_points =  [3, 5, 9]
    m = 17 
    patch_generator = PatchGenerator(all_points, musthave_points, 17)
    patches = patch_generator.generate()

【问题讨论】:

我假设只是随机选择 m 点(或第一个 m 或其他)不会是保证可行的解决方案,因为包围这些点的矩形也可能包含其他点,在这种情况下它是无效的。这是一个正确的假设吗? 是的,如果你随机选取m点,包围这些点的矩形可能包含其他点,这样的矩形是无效的。这就是为什么在我的幼稚解决方案中,在生成封闭矩形后,我必须检查在生成的矩形内的那些 (n - m) 点(其余点)中是否有任何点。 【参考方案1】:

每个这样的矩形都可以缩小到最小尺寸,这样它仍然包含相同的点。因此,您只需要检查这些最小的矩形。设 n 为点的总数。那么左边最多有 n 个可能的坐标,其他边也是如此。对于每对可能的左侧和右侧坐标,您可以对顶部和底部坐标进行线性扫描。最终时间复杂度为 O(n^3)。

【讨论】:

以上是关于生成包含多个 2D 点的矩形的快速算法的主要内容,如果未能解决你的问题,请参考以下文章

在频率域中直接生成滤波器

快速排序之算法

在对角线段中获取点的算法

常用算法-快速排序

寻找附近点的算法?

快速算法在一组范围中快速找到一个数字所属的范围?