为 OCR 拆分多列图像
Splitting multicolumn image for OCR
我正在尝试像这样从多个页面中裁剪两列以便以后进行 OCR,查看沿垂直线拆分页面
到目前为止,我找到了 header,以便将其裁剪掉:
image = cv2.imread('014-page1.jpg')
im_h, im_w, im_d = image.shape
base_image = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (7,7), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Create rectangular structuring element and dilate
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50,10))
dilate = cv2.dilate(thresh, kernel, iterations=1)
# Find contours and draw rectangle
cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=lambda x: cv2.boundingRect(x)[1])
for c in cnts:
x,y,w,h = cv2.boundingRect(c)
if h < 20 and w > 250:
cv2.rectangle(image, (x, y), (x + w, y + h), (36,255,12), 2)
如何垂直拆分页面,并从列中按顺序抓取文本?或者,有没有更好的方法来解决这个问题?
为了将两列分开,您必须找到中间的分界线。
您可以在x-axis
中使用Sobel derivative filter
来找到黑色竖线。关注 this tutorial 以获取有关 Sobel 过滤器运算符的更多详细信息。
sobel_vertical = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3) # (1,0) for x direction derivatives
通过对sobel
结果进行阈值化来提取行位置:
ret, sobel_thresh = cv.threshold(sobel_vertical,127,255,cv.THRESH_BINARY)
然后扫描中心列以查找具有高浓度白色值的列。
一种方法是进行 column-wise
求和,然后找到具有最大值的列。但是还有其他方法可以做到。
sum_cols = np.add.reduce(sobel_thresh, axis = 1)
max_col = np.argmax(sum_cols)
如果没有黑色分割线可以跳过sobel
。只是 resize
积极地搜索中心的白色像素高度集中的列。
这是我对这个问题的看法。它涉及选择图像的 中间部分 ,假设垂直线贯穿所有图像(或至少穿过页面的中间)。我处理这个 ROI 然后 reduce
它到一行。然后,我得到裁剪的开始和结束水平坐标。使用此信息,然后生成 最终裁剪图像。
我试图使算法通用。如果原始图像中有两列以上,它可以拆分所有列。让我们看看代码:
# Imports:
import numpy as np
import cv2
Image path
path = "D://opencvImages//"
fileName = "pmALU.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# To grayscale:
grayImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Otsu Threshold:
_, binaryImage = cv2.threshold(grayImage, 0, 255, cv2.THRESH_OTSU)
# Get image dimensions:
(imageHeight, imageWidth) = binaryImage.shape[:2]
# Set middle ROI dimensions:
middleVertical = 0.5 * imageHeight
roiWidth = imageWidth
roiHeight = int(0.1 * imageHeight)
middleRoiVertical = 0.5 * roiHeight
roiY = int(0.5 * imageHeight - middleRoiVertical)
代码的第一部分获得 ROI。我将它设置为在图像中间裁剪。让我们想象一下将用于处理的 ROI:
下一步是裁剪:
# Slice the ROI:
middleRoi = binaryImage[roiY:roiY + roiHeight, 0:imageWidth]
showImage("middleRoi", middleRoi)
writeImage(path+"middleRoi", middleRoi)
这会产生以下作物:
好的。这个想法是将这个图像减少到一行。如果我得到所有列的最大值并将它们存储在一行中,我应该得到垂直线穿过的大白色部分。
现在,这里有一个问题。如果我直接缩小这个图像,结果会是这样(下面是缩小行的图像):
图像有点小,但您可以看到该行在两侧产生两个黑色列,然后是两个白色斑点。那是因为图像已经被扫描过,另外文本似乎是合理的,并且在两侧产生了一些边距。我只需要 中央的白色斑点,其他所有东西都是黑色的。
我可以分两步解决这个问题:在缩小图像之前在图像周围画一个白色矩形 - 这将处理黑色列。在此之后,我可以 Flood-filling
在缩小图像的两侧再次使用黑色:
# White rectangle around ROI:
rectangleThickness = int(0.01 * imageHeight)
cv2.rectangle(middleRoi, (0, 0), (roiWidth, roiHeight), 255, rectangleThickness)
# Image reduction to a row:
reducedImage = cv2.reduce(middleRoi, 0, cv2.REDUCE_MIN)
# Flood fill at the extreme corners:
fillPositions = [0, imageWidth - 1]
for i in range(len(fillPositions)):
# Get flood-fill coordinate:
x = fillPositions[i]
currentCorner = (x, 0)
fillColor = 0
cv2.floodFill(reducedImage, None, currentCorner, fillColor)
现在,缩小后的图像是这样的:
不错。但是还有一个问题。中央黑线在行的中心产生了一个“间隙”。真的不是问题,因为我可以用 opening
:
来填补这个空白
# Apply Opening:
kernel = np.ones((3, 3), np.uint8)
reducedImage = cv2.morphologyEx(reducedImage, cv2.MORPH_CLOSE, kernel, iterations=2)
这是结果。不再有中心间隙:
酷。让我们从 0
:
开始获取从黑色到白色以及从黑色到白色的过渡发生的垂直位置(索引)
# Get horizontal transitions:
whiteSpaces = np.where(np.diff(reducedImage, prepend=np.nan))[1]
我现在知道在哪里裁剪了。让我们看看:
# Crop the image:
colWidth = len(whiteSpaces)
spaceMargin = 0
for x in range(0, colWidth, 2):
# Get horizontal cropping coordinates:
if x != colWidth - 1:
x2 = whiteSpaces[x + 1]
spaceMargin = (whiteSpaces[x + 2] - whiteSpaces[x + 1]) // 2
else:
x2 = imageWidth
# Set horizontal cropping coordinates:
x1 = whiteSpaces[x] - spaceMargin
x2 = x2 + spaceMargin
# Clamp and Crop original input:
x1 = clamp(x1, 0, imageWidth)
x2 = clamp(x2, 0, imageWidth)
currentCrop = inputImage[0:imageHeight, x1:x2]
cv2.imshow("currentCrop", currentCrop)
cv2.waitKey(0)
你会注意到我计算了 margin
。这是为了裁剪列的边距。我还使用 clamp
函数来确保水平裁剪点始终在图像尺寸范围内。这是该函数的定义:
# Clamps an integer to a valid range:
def clamp(val, minval, maxval):
if val < minval: return minval
if val > maxval: return maxval
return val
这些是结果 (为 post 调整了大小,在新选项卡中打开它们以查看完整图像):
让我们看看这如何扩展到两列以上。这是对原始输入的修改,手动添加了更多列,只是为了查看结果:
这是生成的四张图片:
我正在尝试像这样从多个页面中裁剪两列以便以后进行 OCR,查看沿垂直线拆分页面
到目前为止,我找到了 header,以便将其裁剪掉:
image = cv2.imread('014-page1.jpg')
im_h, im_w, im_d = image.shape
base_image = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (7,7), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Create rectangular structuring element and dilate
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50,10))
dilate = cv2.dilate(thresh, kernel, iterations=1)
# Find contours and draw rectangle
cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=lambda x: cv2.boundingRect(x)[1])
for c in cnts:
x,y,w,h = cv2.boundingRect(c)
if h < 20 and w > 250:
cv2.rectangle(image, (x, y), (x + w, y + h), (36,255,12), 2)
如何垂直拆分页面,并从列中按顺序抓取文本?或者,有没有更好的方法来解决这个问题?
为了将两列分开,您必须找到中间的分界线。
您可以在x-axis
中使用Sobel derivative filter
来找到黑色竖线。关注 this tutorial 以获取有关 Sobel 过滤器运算符的更多详细信息。
sobel_vertical = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3) # (1,0) for x direction derivatives
通过对sobel
结果进行阈值化来提取行位置:
ret, sobel_thresh = cv.threshold(sobel_vertical,127,255,cv.THRESH_BINARY)
然后扫描中心列以查找具有高浓度白色值的列。
一种方法是进行 column-wise
求和,然后找到具有最大值的列。但是还有其他方法可以做到。
sum_cols = np.add.reduce(sobel_thresh, axis = 1)
max_col = np.argmax(sum_cols)
如果没有黑色分割线可以跳过sobel
。只是 resize
积极地搜索中心的白色像素高度集中的列。
这是我对这个问题的看法。它涉及选择图像的 中间部分 ,假设垂直线贯穿所有图像(或至少穿过页面的中间)。我处理这个 ROI 然后 reduce
它到一行。然后,我得到裁剪的开始和结束水平坐标。使用此信息,然后生成 最终裁剪图像。
我试图使算法通用。如果原始图像中有两列以上,它可以拆分所有列。让我们看看代码:
# Imports:
import numpy as np
import cv2
Image path
path = "D://opencvImages//"
fileName = "pmALU.jpg"
# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)
# To grayscale:
grayImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
# Otsu Threshold:
_, binaryImage = cv2.threshold(grayImage, 0, 255, cv2.THRESH_OTSU)
# Get image dimensions:
(imageHeight, imageWidth) = binaryImage.shape[:2]
# Set middle ROI dimensions:
middleVertical = 0.5 * imageHeight
roiWidth = imageWidth
roiHeight = int(0.1 * imageHeight)
middleRoiVertical = 0.5 * roiHeight
roiY = int(0.5 * imageHeight - middleRoiVertical)
代码的第一部分获得 ROI。我将它设置为在图像中间裁剪。让我们想象一下将用于处理的 ROI:
下一步是裁剪:
# Slice the ROI:
middleRoi = binaryImage[roiY:roiY + roiHeight, 0:imageWidth]
showImage("middleRoi", middleRoi)
writeImage(path+"middleRoi", middleRoi)
这会产生以下作物:
好的。这个想法是将这个图像减少到一行。如果我得到所有列的最大值并将它们存储在一行中,我应该得到垂直线穿过的大白色部分。
现在,这里有一个问题。如果我直接缩小这个图像,结果会是这样(下面是缩小行的图像):
图像有点小,但您可以看到该行在两侧产生两个黑色列,然后是两个白色斑点。那是因为图像已经被扫描过,另外文本似乎是合理的,并且在两侧产生了一些边距。我只需要 中央的白色斑点,其他所有东西都是黑色的。
我可以分两步解决这个问题:在缩小图像之前在图像周围画一个白色矩形 - 这将处理黑色列。在此之后,我可以 Flood-filling
在缩小图像的两侧再次使用黑色:
# White rectangle around ROI:
rectangleThickness = int(0.01 * imageHeight)
cv2.rectangle(middleRoi, (0, 0), (roiWidth, roiHeight), 255, rectangleThickness)
# Image reduction to a row:
reducedImage = cv2.reduce(middleRoi, 0, cv2.REDUCE_MIN)
# Flood fill at the extreme corners:
fillPositions = [0, imageWidth - 1]
for i in range(len(fillPositions)):
# Get flood-fill coordinate:
x = fillPositions[i]
currentCorner = (x, 0)
fillColor = 0
cv2.floodFill(reducedImage, None, currentCorner, fillColor)
现在,缩小后的图像是这样的:
不错。但是还有一个问题。中央黑线在行的中心产生了一个“间隙”。真的不是问题,因为我可以用 opening
:
# Apply Opening:
kernel = np.ones((3, 3), np.uint8)
reducedImage = cv2.morphologyEx(reducedImage, cv2.MORPH_CLOSE, kernel, iterations=2)
这是结果。不再有中心间隙:
酷。让我们从 0
:
# Get horizontal transitions:
whiteSpaces = np.where(np.diff(reducedImage, prepend=np.nan))[1]
我现在知道在哪里裁剪了。让我们看看:
# Crop the image:
colWidth = len(whiteSpaces)
spaceMargin = 0
for x in range(0, colWidth, 2):
# Get horizontal cropping coordinates:
if x != colWidth - 1:
x2 = whiteSpaces[x + 1]
spaceMargin = (whiteSpaces[x + 2] - whiteSpaces[x + 1]) // 2
else:
x2 = imageWidth
# Set horizontal cropping coordinates:
x1 = whiteSpaces[x] - spaceMargin
x2 = x2 + spaceMargin
# Clamp and Crop original input:
x1 = clamp(x1, 0, imageWidth)
x2 = clamp(x2, 0, imageWidth)
currentCrop = inputImage[0:imageHeight, x1:x2]
cv2.imshow("currentCrop", currentCrop)
cv2.waitKey(0)
你会注意到我计算了 margin
。这是为了裁剪列的边距。我还使用 clamp
函数来确保水平裁剪点始终在图像尺寸范围内。这是该函数的定义:
# Clamps an integer to a valid range:
def clamp(val, minval, maxval):
if val < minval: return minval
if val > maxval: return maxval
return val
这些是结果 (为 post 调整了大小,在新选项卡中打开它们以查看完整图像):
让我们看看这如何扩展到两列以上。这是对原始输入的修改,手动添加了更多列,只是为了查看结果:
这是生成的四张图片: