Python & OpenCV:如何裁剪半成型的边界框

Python & OpenCV: How to crop half-formed bounding boxes

我有一个为无网格创建网格线的脚本table:

脚本之前:

脚本之后:

是否有使用 OpenCV 裁剪“脚本后”图像使其仅包含四边边界框的简单方法?示例输出:

编辑:

我目前正在研究一种解决方案,可以找到 first/last 全黑像素线走向 vertically/horizontally。它会工作,但想知道是否有更优雅的东西。

这是 Python/OpenCV 中的一种方法,即从除最大轮廓之外的所有轮廓中获取最小和最大 x 和 y。

输入:

import cv2
import numpy as np

# read image
img = cv2.imread('test_table.png')
hh, ww = img.shape[:2]

# convert to gray
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# threshold
thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)[1]

# crop 1 pixel and add 1 pixel white border to ensure outer white regions not considered small contours
thresh = thresh[1:hh-1, 1:ww-1]
thresh = cv2.copyMakeBorder(thresh, 1,1,1,1, borderType=cv2.BORDER_CONSTANT, value=(255,255,255))

# get contours
contours = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)

# get min and max x and y from all bounding boxes larger than half the image size
area_thresh = hh * ww / 2
xmin = ww
ymin = hh
xmax = 0
ymax = 0

for cntr in contours:
    area = cv2.contourArea(cntr)
    if area < area_thresh:
        x,y,w,h = cv2.boundingRect(cntr)
        xmin = x if (x < xmin) else xmin
        ymin = y if (y < ymin) else ymin
        xmax = x+w-1 if (x+w-1 > xmax ) else xmax
        ymax = y+h-1 if (y+h-1 > ymax) else ymax


# draw bounding box     
bbox = img.copy()
cv2.rectangle(bbox, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2)

# crop img at bounding box, but add 2 all around to keep the black lines
result = img[ymin-3:ymax+3, xmin-3:xmax+3]

# save results
cv2.imwrite('test_table_bbox.png',bbox)
cv2.imwrite('test_table_trimmed.png',result)

# show results
cv2.imshow("thresh", thresh)
cv2.imshow("bbox", bbox)
cv2.imshow("result", result)
cv2.waitKey(0)
cv2.destroyAllWindows()

输入的所有边界框的边界框:

修剪过的图片:

注意:我知道已经有一个可接受的答案,但我想提供一个更简单的版本。

基本上,首先找到图像中每个形状的轮廓(每个单元格),其面积大于选定的数字,将滤除任何噪音。

遍历轮廓,找到最小和最大的 x 和 y 坐标。有了这4个点,我们就可以把图片中4个坐标内的像素保存到一个单独的数组中,用白色填充原图,然后把table画回图片上。

代码:

import cv2

img = cv2.imread("table.png")
h, w, _ = img.shape

x1, y1 = w, h
x2, y2 = 0, 0

contours, _ = cv2.findContours(cv2.Canny(img, 0, 0), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for cnt in contours:
    if cv2.contourArea(cnt) > 1000:
        x1 = min(cnt[..., 0].min(), x1)
        y1 = min(cnt[..., 1].min(), y1)
        x2 = max(cnt[..., 0].max(), x2)
        y2 = max(cnt[..., 1].max(), y2)

pad = 2
x1 -= pad
y1 -= pad
x2 += pad * 2
y2 += pad * 2

table = img[y1:y2, x1:x2].copy()

img.fill(255)
img[y1:y2, x1:x2] = table
cv2.imshow("lined_table.png", img)
cv2.waitKey(0)

输出:

解释:

  1. 导入opencv模块,读入图像。获取图像的尺寸并为 table 的第一个角和 table 的最后一个角定义临时坐标:
import cv2

img = cv2.imread("table.png")
h, w, _ = img.shape

x1, y1 = w, h
x2, y2 = 0, 0
  1. 获取图像的轮廓,并循环遍历每个轮廓,过滤掉面积小于1000的轮廓:
contours, _ = cv2.findContours(cv2.Canny(img, 0, 0), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for cnt in contours:
    if cv2.contourArea(cnt) > 999:
  1. 更新第一个角点和最后一个角点的坐标值 table:
        x1 = min(cnt[..., 0].min(), x1)
        y1 = min(cnt[..., 1].min(), y1)
        x2 = max(cnt[..., 0].max(), x2)
        y2 = max(cnt[..., 1].max(), y2)
  1. 根据线条的宽度在每个坐标周围应用填充:
pad = 2
x1 -= pad
y1 -= pad
x2 += pad * 2
y2 += pad * 2
  1. 根据找到的x和y坐标将部分图片复制到一个变量中,清空图片,重绘table到图片上。最后,显示图像:
table = img[y_1:y_2, x_1:x_2].copy()

img.fill(255)
img[y_1:y_2, x_1:x_2] = table
cv2.imwrite("lined_table.png", img)
cv2.waitKey(0)