Package: quadrilateral-fitter
QuadrilateralFitterQuadrilateralFitteris an efficient and easy-to-use library for fitting irregular quadrilaterals from polygons or point clouds.QuadrilateralFitterhelps you find that four corners polygon thatbest approximatesyour noisy data or detection, so you can apply further processing steps like:perspective correctionorpattern matching, without worrying about noise or non-expected vertex.OptimalFitted Quadrilateralis the smallest area quadrilateral that contains all the points inside a given polygon.InstallationYou can installQuadrilateralFitterwith pip:pipinstallquadrilateral-fitterUsageThere is only one line you need to useQuadrilateralFitter:fromquadrilateral_fitterimportQuadrilateralFitter# Fit an input polygon of N sidesfitted_quadrilateral=QuadrilateralFitter(polygon=your_noisy_polygon).fit()If your application can accept fitted quadrilateral to don't strictly include all points within input polygon, you can get the tighter quadrilateral shown asInitial Guesswith:fitted_quadrilateral=QuadrilateralFitter(polygon=your_noisy_polygon).tight_quadrilateralAPI ReferenceQuadrilateralFitter(polygon)Initialize theQuadrilateralFitterinstance..polygon:np.ndarray | tuple | list | shapely.Polygon. List of the polygon coordinates. It must be a list of coordinates, in the formatXY, shape (N, 2).QuadrilateralFitter.fit(simplify_polygons_larger_than = 10):simplify_polygons_larger_than:int | None. List of the polygon coordinates. It must be a list of coordinates, in the formatXY, shape (N, 2). If a number is specified, the method will make a preliminarDouglas-Peuckersimplification of the internally usedConvex Hullif it has more thansimplify_polygons_larger_than vertices. This will speed up the process, but may lead to a sub-optimal quadrilateral approximation. Default: 10.Returns:tuple[tuple[float, float], tuple[float, float], tuple[float, float], tuple[float, float]]: Atuplecontaining the fourXYcoordinates of the fitted cuadrilateral. This quadrilateral will minimize theIoU(Intersection Over Union) with the inputpolygon, while containing all its points inside. If your use case can allow loosing points from the input polygon, you can read theQuadrilateralFitter.tight_polygonproperty to obtain a tighter quadrilateral.Real Case ExampleLet's simulate a real case scenario where we detect a noisy polygon from a form that we know should be a perfect rectangle (only deformed by perspective).importnumpyasnpimportcv2image=cv2.cvtColor(cv2.imread('./resources/input_sample.jpg'),cv2.COLOR_BGR2RGB)# Save the Ground Truth cornerstrue_corners=np.array([[50.,100.],[370.,0.],[421.,550.],[0.,614.],[50.,100.]],dtype=np.float32)# Generate a simulated noisy detectionsides=[np.linspace([x1,y1],[x2,y2],20)+np.random.normal(scale=10,size=(20,2))for(x1,y1),(x2,y2)inzip(true_corners[:-1],true_corners[1:])]noisy_corners=np.concatenate(sides,axis=0)# To simplify, we will clip the corners to be within the imagenoisy_corners[:,0]=np.clip(noisy_corners[:,0],a_min=0.,a_max=image.shape[1])noisy_corners[:,1]=np.clip(noisy_corners[:,1],a_min=0.,a_max=image.shape[0])And now, let's runQuadrilateralFitterto find the quadrilateral that best approximates our noisy detection (without leaving points outside).fromquadrilateral_fitterimportQuadrilateralFitter# Define the fitter (we want to keep it for reading internal variables later)fitter=QuadrilateralFitter(polygon=noisy_corners)# Get the fitted quadrilateral that contains all the points inside the input polygonfitted_quadrilateral=np.array(fitter.fit(),dtype=np.float32)# If you wanna to get a tighter mask, less likely to contain points outside the real quadrilateral,# but that cannot ensure to always contain all the points within the input polygon, you can use:tight_quadrilateral=np.array(fitter.tight_quadrilateral,dtype=np.float32)# To show the plot of the fitting processfitter.plot()Finally, for use cases like this, we could use fitted quadrilaterals to apply a perspective correction to the image, so we can get a visual insight of the results.# Generate the destiny points for the perspective correction by adjusting it to a perfect rectangleh,w=image.shape[:2]forquadrilateralin(fitted_quadrilateral,tight_quadrilateral):# Cast it to a numpy for agile manipulationquadrilateral=np.array(quadrilateral,dtype=np.float32)# Get the bounding box of the fitted quadrilateralmin_x,min_y=np.min(quadrilateral,axis=0)max_x,max_y=np.max(quadrilateral,axis=0)# Define the destiny points for the perspective correctiondestiny_points=np.array(((min_x,min_y),(max_x,min_y),(max_x,max_y),(min_x,max_y)),dtype=np.float32)# Calculate the homography matrix from the quadrilateral to the rectanglehomography_matrix,_=cv2.findHomography(srcPoints=quadrilateral,dstPoints=rect_points)# Warp the image using the homography matrixwarped_image=cv2.warpPerspective(src=image,M=homography_matrix,dsize=(w,h))