根据 Python、OpenCV 中的优先级对轮廓进行排序
Sorting contours based on precedence in Python, OpenCV
我正在尝试根据它们的到达对轮廓进行排序,left-to-right
和 top-to-bottom
就像您写任何东西一样。从 top
和 left
然后相应地取其次。
这是我到目前为止所取得的成就:
def get_contour_precedence(contour, cols):
tolerance_factor = 61
origin = cv2.boundingRect(contour)
return ((origin[1] // tolerance_factor) * tolerance_factor) * cols + origin[0]
image = cv2.imread("C:/Users/XXXX/PycharmProjects/OCR/raw_dataset/23.png", 0)
ret, thresh1 = cv2.threshold(image, 130, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, h = cv2.findContours(thresh1.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# perform edge detection, find contours in the edge map, and sort the
# resulting contours from left-to-right
contours.sort(key=lambda x: get_contour_precedence(x, thresh1.shape[1]))
# initialize the list of contour bounding boxes and associated
# characters that we'll be OCR'ing
chars = []
inc = 0
# loop over the contours
for c in contours:
inc += 1
# compute the bounding box of the contour
(x, y, w, h) = cv2.boundingRect(c)
label = str(inc)
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.putText(image, label, (x - 2, y - 2),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
print('x=', x)
print('y=', y)
print('x+w=', x + w)
print('y+h=', y + h)
crop_img = image[y + 2:y + h - 1, x + 2:x + w - 1]
name = os.path.join("bounding boxes", 'Image_%d.png' % (
inc))
cv2.imshow("cropped", crop_img)
print(name)
crop_img = Image.fromarray(crop_img)
crop_img.save(name)
cv2.waitKey(0)
cv2.imshow('mat', image)
cv2.waitKey(0)
输入图像:
输出图像 1:
输入图片2:
图像 2 的输出:
输入图像 3:
输出图像 3:
As you can see the 1,2,3,4 is not what I was expecting it to be each image, as displayed in the Image Number 3.
如何调整它以使其工作甚至编写自定义函数?
注意:我的问题中提供了同一个输入图像的多张图像。内容相同,但文本有所不同,因此 tolerance factor
不适用于每一个。手动调整它不是一个好主意。
我宁愿使用质心或至少边界框中心,而不是轮廓的左上角。
def get_contour_precedence(contour, cols):
tolerance_factor = 4
origin = cv2.boundingRect(contour)
return (((origin[1] + origin[3])/2 // tolerance_factor) * tolerance_factor) * cols + (origin[0] + origin[2]) / 2
但可能很难找到适用于所有情况的公差值。
我什至会说使用色调矩,这往往是对多边形中心点的更好估计
比矩形的“正常”坐标中心点,所以函数可以是:
def get_contour_precedence(contour, cols):
tolerance_factor = 61
M = cv2.moments(contour)
# calculate x,y coordinate of centroid
if M["m00"] != 0:
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
else:
# set values as what you need in the situation
cX, cY = 0, 0
return ((cY // tolerance_factor) * tolerance_factor) * cols + cX
超级数学。解释什么是色调时刻,你能找到 here
也许你应该考虑摆脱这个 tolerance_factor
通过使用一般的聚类算法,如
kmeans 将您的中心聚类到行和列。
OpenCv 有一个 kmeans 实现,你可以找到 here
我不完全知道你的目标是什么,但另一个想法可能是将每条线分成感兴趣区域 (ROI)
为了进一步处理,之后你可以很容易地计算字母
通过每个轮廓的 X-Values 和行号
import cv2
import numpy as np
## (1) read
img = cv2.imread("yFX3M.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
## (2) threshold
th, threshed = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)
## (3) minAreaRect on the nozeros
pts = cv2.findNonZero(threshed)
ret = cv2.minAreaRect(pts)
(cx,cy), (w,h), ang = ret
if w>h:
w,h = h,w
## (4) Find rotated matrix, do rotation
M = cv2.getRotationMatrix2D((cx,cy), ang, 1.0)
rotated = cv2.warpAffine(threshed, M, (img.shape[1], img.shape[0]))
## (5) find and draw the upper and lower boundary of each lines
hist = cv2.reduce(rotated,1, cv2.REDUCE_AVG).reshape(-1)
th = 2
H,W = img.shape[:2]
# (6) using histogramm with threshold
uppers = [y for y in range(H-1) if hist[y]<=th and hist[y+1]>th]
lowers = [y for y in range(H-1) if hist[y]>th and hist[y+1]<=th]
rotated = cv2.cvtColor(rotated, cv2.COLOR_GRAY2BGR)
for y in uppers:
cv2.line(rotated, (0,y), (W, y), (255,0,0), 1)
for y in lowers:
cv2.line(rotated, (0,y), (W, y), (0,255,0), 1)
cv2.imshow('pic', rotated)
# (7) we iterate all rois and count
for i in range(len(uppers)) :
print('line=',i)
roi = rotated[uppers[i]:lowers[i],0:W]
cv2.imshow('line', roi)
cv2.waitKey(0)
# here again calc thres and contours
我找到了一个旧的 post 代码
这是 Python/OpenCV 中的一种方法,首先按行处理,然后按字符处理。
- 读取输入
- 转换为灰度
- 阈值和反转
- 使用长水平内核并应用形态接近形成行
- 获取行及其边界框的轮廓
- 保存行框并按 Y 排序
- 遍历每个排序的行框并从阈值图像中提取行
- 获取行中每个字符的轮廓并保存字符的边界框。
- 对 X 上给定行的轮廓进行排序
- 在输入上绘制边界框,在图像上绘制索引号作为文本
- 增加索引
- 保存结果
输入:
import cv2
import numpy as np
# read input image
img = cv2.imread('vision78.png')
# convert img to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# otsu threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU )[1]
thresh = 255 - thresh
# apply morphology close to form rows
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (51,1))
morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
# find contours and bounding boxes of rows
rows_img = img.copy()
boxes_img = img.copy()
rowboxes = []
rowcontours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
rowcontours = rowcontours[0] if len(rowcontours) == 2 else rowcontours[1]
index = 1
for rowcntr in rowcontours:
xr,yr,wr,hr = cv2.boundingRect(rowcntr)
cv2.rectangle(rows_img, (xr, yr), (xr+wr, yr+hr), (0, 0, 255), 1)
rowboxes.append((xr,yr,wr,hr))
# sort rowboxes on y coordinate
def takeSecond(elem):
return elem[1]
rowboxes.sort(key=takeSecond)
# loop over each row
for rowbox in rowboxes:
# crop the image for a given row
xr = rowbox[0]
yr = rowbox[1]
wr = rowbox[2]
hr = rowbox[3]
row = thresh[yr:yr+hr, xr:xr+wr]
bboxes = []
# find contours of each character in the row
contours = cv2.findContours(row, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
for cntr in contours:
x,y,w,h = cv2.boundingRect(cntr)
bboxes.append((x+xr,y+yr,w,h))
# sort bboxes on x coordinate
def takeFirst(elem):
return elem[0]
bboxes.sort(key=takeFirst)
# draw sorted boxes
for box in bboxes:
xb = box[0]
yb = box[1]
wb = box[2]
hb = box[3]
cv2.rectangle(boxes_img, (xb, yb), (xb+wb, yb+hb), (0, 0, 255), 1)
cv2.putText(boxes_img, str(index), (xb,yb), cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.75, (0,255,0), 1)
index = index + 1
# save result
cv2.imwrite("vision78_thresh.jpg", thresh)
cv2.imwrite("vision78_morph.jpg", morph)
cv2.imwrite("vision78_rows.jpg", rows_img)
cv2.imwrite("vision78_boxes.jpg", boxes_img)
# show images
cv2.imshow("thresh", thresh)
cv2.imshow("morph", morph)
cv2.imshow("rows_img", rows_img)
cv2.imshow("boxes_img", boxes_img)
cv2.waitKey(0)
阈值图像:
行的形态图像:
行轮廓图像:
人物轮廓图像:
这是我对这个问题的看法。我会给你它的一般要点,然后是我在 C++
中的实现。主要思想是我想从从左到右、从上到下处理图像。我将处理 每个 blob (或轮廓),但是,我需要几个中间步骤来实现成功的(有序的)分割。
垂直排序使用 行
第一步试图按行对blob进行排序——这意味着每一行都有一组(无序的)水平 斑点。没关系。第一步是计算某种 垂直 排序,如果我们从上到下处理每一行,我们将实现这一点。
在 blob 按行(垂直)排序后,我可以查看它们的 质心(或质心)并水平排序。我的想法是,我将处理 每行 行,并且 for
每行,我对 blob centroids 进行排序。让我们看一个我正在努力实现的例子。
这是您的输入图像:
这就是我所说的行掩码:
最后一张图像包含 白色区域,每个区域代表一个“行”。每个 行 都有一个数字(例如 Row1
、 Row2
等),每个 row
包含一组斑点(或字符,在这个案例)。通过处理每个 row
、 从底部开始的顶部 ,您已经在垂直轴上对 blob 进行了排序。
如果我从上到下对每一行进行编号,我会得到这张图片:
Row Mask 是一种创建“blob 行”的方法,并且可以形态学 计算此掩码。查看叠加的 2 张图像,让您更好地了解处理顺序:
我们在这里尝试做的是,首先是垂直排序(蓝色箭头),然后我们将处理水平排序(红色箭头)。您可以看到,通过处理每一行我们可以(可能)克服排序问题!
水平排序使用质心
现在让我们看看如何对 blob 进行排序 horizontally
。如果我们创建一个更简单的图像,width
等于输入图像,height
等于 Row Mask 中 rows
的数量,我们可以简单地叠加每个斑点质心的每个水平坐标(x 坐标)。看看这个例子:
这是 行 Table。每行代表在 Row Mask 中找到的行数,也是从上到下读取的。 table的width
与输入图像的width
相同,在空间上对应于水平轴。每个 正方形 都是输入图像中的一个像素,仅使用水平坐标映射到行 Table(因为我们对行的简化非常简单)。 table 行中每个像素的实际值是 label
,标记输入图像上的每个斑点。请注意,标签没有排序!
因此,例如,这个 table 表明,在 行 1 中(您已经知道什么是行 1 – 它是 [= =207=]Row Mask) 在位置 (1,4)
中有 blob 编号 3
。在位置 (1,6)
有 blob 编号 2
,依此类推。这个 table 的酷(我认为)是你可以遍历它,并且 for
每个 0
不同的值,水平排序变得非常微不足道。这是 table 排序的行,现在,从左到右:
用质心映射 blob 信息
我们将使用 blob centroids 来 map
我们两个表示之间的信息(行 Mask/Row Table)。假设您已经拥有两个“辅助”图像并且您一次处理输入图像上的每个斑点(或轮廓)。例如,您以此作为开始:
好的,这里有一个斑点。我们如何将它映射到 Row Mask 和 Row Table?使用它的质心。如果我们计算质心(在图中显示为绿点),我们可以构建一个 dictionary
质心和标签。例如,对于此 blob,centroid
位于 (271,193)
。好的,让我们分配 label = 1
。所以我们现在有了这本字典:
现在,我们在行掩码上使用相同的 centroid
找到放置此 blob 的 row
。像这样:
rowNumber = rowMask.at( 271,193 )
这个操作应该returnrownNumber = 3
。好的!我们知道我们的 blob 位于哪一行,因此,它现在是 vertically 排序的。现在,让我们将其 水平 坐标存储在行 Table:
中
rowTable.at( 271, 193 ) = 1
现在,rowTable
保存(在其行和列中)已处理 blob 的标签。 Row Table 应该是 loo像这样:
table 宽很多,因为它的水平尺寸必须与输入图像相同。在此图像中,label 1
位于 Column 271, Row 3.
如果这是图像上唯一的斑点,则斑点已经排序。但是,如果您在 Column 2
、Row 1
中添加另一个 blob 会怎样?这就是为什么您需要在处理完所有 blob 之后再次遍历此 table 以正确更正它们的标签。
C++ 实现
好吧,希望算法应该有点清楚(如果不明白,就问,伙计)。我将尝试使用 C++
在 OpenCV
中实现这些想法。首先,我需要 binary image
您的输入。使用 Otsu’s thresholding
方法计算很简单:
//Read the input image:
std::string imageName = "C://opencvImages//yFX3M.png";
cv::Mat testImage = cv::imread( imageName );
//Compute grayscale image
cv::Mat grayImage;
cv::cvtColor( testImage, grayImage, cv::COLOR_RGB2GRAY );
//Get binary image via Otsu:
cv::Mat binImage;
cv::threshold( grayImage, binImage, 0, 255, cv::THRESH_OTSU );
//Invert image:
binImage = 255 - binImage;
这是生成的二值图像,没什么特别的,正是我们开始工作所需要的:
第一步是获取Row Mask
。这可以使用形态学来实现。只需应用 dilation + erosion
和 非常 大水平 structuring element
。这个想法是你想把这些斑点变成矩形,把它们水平地“融合”在一起:
//Create a hard copy of the binary mask:
cv::Mat rowMask = binImage.clone();
//horizontal dilation + erosion:
int horizontalSize = 100; // a very big horizontal structuring element
cv::Mat SE = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(horizontalSize,1) );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_DILATE, SE, cv::Point(-1,-1), 2 );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_ERODE, SE, cv::Point(-1,-1), 1 );
结果如下 Row Mask
:
太棒了,现在我们有了 Row Mask
,我们必须给它们编号行,好吗?有很多方法可以做到这一点,但现在我对更简单的方法感兴趣:遍历此图像并获取每个像素。 If
一个像素是白色的,使用 Flood Fill
操作将图像的该部分标记为唯一的斑点(或行,在这种情况下)。这可以按如下方式完成:
//Label the row mask:
int rowCount = 0; //This will count our rows
//Loop thru the mask:
for( int y = 0; y < rowMask.rows; y++ ){
for( int x = 0; x < rowMask.cols; x++ ){
//Get the current pixel:
uchar currentPixel = rowMask.at<uchar>( y, x );
//If the pixel is white, this is an unlabeled blob:
if ( currentPixel == 255 ) {
//Create new label (different from zero):
rowCount++;
//Flood fill on this point:
cv::floodFill( rowMask, cv::Point( x, y ), rowCount, (cv::Rect*)0, cv::Scalar(), 0 );
}
}
}
此过程将标记从 1
到 r
的所有行。这就是我们想要的。如果您查看图像,您会隐约看到这些行,那是因为我们的标签对应于非常低的灰度像素强度值。
好的,现在让我们准备行Table。这个“table”实际上只是另一张图片,请记住:宽度与输入相同,高度与您在 Row Mask
:
上计算的行数相同
//create rows image:
cv::Mat rowTable = cv::Mat::zeros( cv::Size(binImage.cols, rowCount), CV_8UC1 );
//Just for convenience:
rowTable = 255 - rowTable;
在这里,为了方便起见,我只是将最终图像倒置。因为我想真正看到 table 是如何填充(非常低强度)像素的,并确保一切都按预期工作。
现在是有趣的部分。我们准备了两个图像(或数据容器)。我们需要独立处理每个 blob。这个想法是你必须从二进制图像中提取每个 blob/contour/character 并计算它的 centroid
并分配一个新的 label
。同样,有很多方法可以做到这一点。在这里,我使用以下方法:
我将遍历 binary mask
。我将从这个二进制输入中得到 current biggest blob
。我将计算它的 centroid
并将其数据存储在每个需要的容器中,然后,我将从掩码中 delete
那个 blob。我会重复这个过程,直到没有更多的斑点留下。这是我这样做的方式,特别是因为我已经为此编写了函数。这是方法:
//Prepare a couple of dictionaries for data storing:
std::map< int, cv::Point > blobMap; //holds label, gives centroid
std::map< int, cv::Rect > boundingBoxMap; //holds label, gives bounding box
第一,两个dictionaries
。一个接收到一个 blob 标签和 returns 质心。另一个收到相同的标签和 return 边界框。
//Extract each individual blob:
cv::Mat bobFilterInput = binImage.clone();
//The new blob label:
int blobLabel = 0;
//Some control variables:
bool extractBlobs = true; //Controls loop
int currentBlob = 0; //Counter of blobs
while ( extractBlobs ){
//Get the biggest blob:
cv::Mat biggestBlob = findBiggestBlob( bobFilterInput );
//Compute the centroid/center of mass:
cv::Moments momentStructure = cv::moments( biggestBlob, true );
float cx = momentStructure.m10 / momentStructure.m00;
float cy = momentStructure.m01 / momentStructure.m00;
//Centroid point:
cv::Point blobCentroid;
blobCentroid.x = cx;
blobCentroid.y = cy;
//Compute bounding box:
boundingBox boxData;
computeBoundingBox( biggestBlob, boxData );
//Convert boundingBox data into opencv rect data:
cv::Rect cropBox = boundingBox2Rect( boxData );
//Label blob:
blobLabel++;
blobMap.emplace( blobLabel, blobCentroid );
boundingBoxMap.emplace( blobLabel, cropBox );
//Get the row for this centroid
int blobRow = rowMask.at<uchar>( cy, cx );
blobRow--;
//Place centroid on rowed image:
rowTable.at<uchar>( blobRow, cx ) = blobLabel;
//Resume blob flow control:
cv::Mat blobDifference = bobFilterInput - biggestBlob;
//How many pixels are left on the new mask?
int pixelsLeft = cv::countNonZero( blobDifference );
bobFilterInput = blobDifference;
//Done extracting blobs?
if ( pixelsLeft <= 0 ){
extractBlobs = false;
}
//Increment blob counter:
currentBlob++;
}
看看这个处理过程如何遍历每个 blob、处理它并删除它直到什么都不剩下的漂亮动画:
现在,用上面的片段做一些注释。我有一些辅助函数: 和 computeBoundingBox
。这些函数计算二值图像中最大的斑点,并将边界框的自定义结构分别转换为 OpenCV
的 Rect
结构。这些是那些函数执行的操作。
代码段的“内容”是这样的:一旦你有了一个 孤立的 blob,计算它的 centroid
(我实际上计算 center of mass
通过central moments
)。生成一个新的 label
。将此 label
和 centroid
存储在 dictionary
中,在我的例子中,是 blobMap
字典。另外计算 bounding box
并将其存储在另一个 dictionary
, boundingBoxMap
:
//Label blob:
blobLabel++;
blobMap.emplace( blobLabel, blobCentroid );
boundingBoxMap.emplace( blobLabel, cropBox );
现在,使用 centroid
数据,fetch
那个 blob 对应的 row
。获得该行后,将此数字存储到您的行中 table:
//Get the row for this centroid
int blobRow = rowMask.at<uchar>( cy, cx );
blobRow--;
//Place centroid on rowed image:
rowTable.at<uchar>( blobRow, cx ) = blobLabel;
非常好。此时,您已准备好行 Table。让我们遍历它,实际上,最后,订购那些该死的斑点:
int blobCounter = 1; //The ORDERED label, starting at 1
for( int y = 0; y < rowTable.rows; y++ ){
for( int x = 0; x < rowTable.cols; x++ ){
//Get current label:
uchar currentLabel = rowTable.at<uchar>( y, x );
//Is it a valid label?
if ( currentLabel != 255 ){
//Get the bounding box for this label:
cv::Rect currentBoundingBox = boundingBoxMap[ currentLabel ];
cv::rectangle( testImage, currentBoundingBox, cv::Scalar(0,255,0), 2, 8, 0 );
//The blob counter to string:
std::string counterString = std::to_string( blobCounter );
cv::putText( testImage, counterString, cv::Point( currentBoundingBox.x, currentBoundingBox.y-1 ),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255,0,0), 1, cv::LINE_8, false );
blobCounter++; //Increment the blob/label
}
}
}
没什么特别的,只是一个常规的嵌套 for
循环,遍历 row table
上的每个像素。如果像素不同于白色,则使用 label
检索 centroid
和 bounding box
,并将 label
更改为递增数字即可。对于结果显示,我只是在原始图像上绘制边界框和新标签。
查看动画中的有序处理:
非常酷,这是一个额外的动画,行 Table 填充了水平坐标:
我正在尝试根据它们的到达对轮廓进行排序,left-to-right
和 top-to-bottom
就像您写任何东西一样。从 top
和 left
然后相应地取其次。
这是我到目前为止所取得的成就:
def get_contour_precedence(contour, cols):
tolerance_factor = 61
origin = cv2.boundingRect(contour)
return ((origin[1] // tolerance_factor) * tolerance_factor) * cols + origin[0]
image = cv2.imread("C:/Users/XXXX/PycharmProjects/OCR/raw_dataset/23.png", 0)
ret, thresh1 = cv2.threshold(image, 130, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, h = cv2.findContours(thresh1.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# perform edge detection, find contours in the edge map, and sort the
# resulting contours from left-to-right
contours.sort(key=lambda x: get_contour_precedence(x, thresh1.shape[1]))
# initialize the list of contour bounding boxes and associated
# characters that we'll be OCR'ing
chars = []
inc = 0
# loop over the contours
for c in contours:
inc += 1
# compute the bounding box of the contour
(x, y, w, h) = cv2.boundingRect(c)
label = str(inc)
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.putText(image, label, (x - 2, y - 2),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
print('x=', x)
print('y=', y)
print('x+w=', x + w)
print('y+h=', y + h)
crop_img = image[y + 2:y + h - 1, x + 2:x + w - 1]
name = os.path.join("bounding boxes", 'Image_%d.png' % (
inc))
cv2.imshow("cropped", crop_img)
print(name)
crop_img = Image.fromarray(crop_img)
crop_img.save(name)
cv2.waitKey(0)
cv2.imshow('mat', image)
cv2.waitKey(0)
输入图像:
输出图像 1:
输入图片2:
图像 2 的输出:
输入图像 3:
输出图像 3:
As you can see the 1,2,3,4 is not what I was expecting it to be each image, as displayed in the Image Number 3.
如何调整它以使其工作甚至编写自定义函数?
注意:我的问题中提供了同一个输入图像的多张图像。内容相同,但文本有所不同,因此 tolerance factor
不适用于每一个。手动调整它不是一个好主意。
我宁愿使用质心或至少边界框中心,而不是轮廓的左上角。
def get_contour_precedence(contour, cols):
tolerance_factor = 4
origin = cv2.boundingRect(contour)
return (((origin[1] + origin[3])/2 // tolerance_factor) * tolerance_factor) * cols + (origin[0] + origin[2]) / 2
但可能很难找到适用于所有情况的公差值。
我什至会说使用色调矩,这往往是对多边形中心点的更好估计 比矩形的“正常”坐标中心点,所以函数可以是:
def get_contour_precedence(contour, cols):
tolerance_factor = 61
M = cv2.moments(contour)
# calculate x,y coordinate of centroid
if M["m00"] != 0:
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
else:
# set values as what you need in the situation
cX, cY = 0, 0
return ((cY // tolerance_factor) * tolerance_factor) * cols + cX
超级数学。解释什么是色调时刻,你能找到 here
也许你应该考虑摆脱这个 tolerance_factor 通过使用一般的聚类算法,如 kmeans 将您的中心聚类到行和列。 OpenCv 有一个 kmeans 实现,你可以找到 here
我不完全知道你的目标是什么,但另一个想法可能是将每条线分成感兴趣区域 (ROI) 为了进一步处理,之后你可以很容易地计算字母 通过每个轮廓的 X-Values 和行号
import cv2
import numpy as np
## (1) read
img = cv2.imread("yFX3M.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
## (2) threshold
th, threshed = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)
## (3) minAreaRect on the nozeros
pts = cv2.findNonZero(threshed)
ret = cv2.minAreaRect(pts)
(cx,cy), (w,h), ang = ret
if w>h:
w,h = h,w
## (4) Find rotated matrix, do rotation
M = cv2.getRotationMatrix2D((cx,cy), ang, 1.0)
rotated = cv2.warpAffine(threshed, M, (img.shape[1], img.shape[0]))
## (5) find and draw the upper and lower boundary of each lines
hist = cv2.reduce(rotated,1, cv2.REDUCE_AVG).reshape(-1)
th = 2
H,W = img.shape[:2]
# (6) using histogramm with threshold
uppers = [y for y in range(H-1) if hist[y]<=th and hist[y+1]>th]
lowers = [y for y in range(H-1) if hist[y]>th and hist[y+1]<=th]
rotated = cv2.cvtColor(rotated, cv2.COLOR_GRAY2BGR)
for y in uppers:
cv2.line(rotated, (0,y), (W, y), (255,0,0), 1)
for y in lowers:
cv2.line(rotated, (0,y), (W, y), (0,255,0), 1)
cv2.imshow('pic', rotated)
# (7) we iterate all rois and count
for i in range(len(uppers)) :
print('line=',i)
roi = rotated[uppers[i]:lowers[i],0:W]
cv2.imshow('line', roi)
cv2.waitKey(0)
# here again calc thres and contours
我找到了一个旧的 post 代码
这是 Python/OpenCV 中的一种方法,首先按行处理,然后按字符处理。
- 读取输入
- 转换为灰度
- 阈值和反转
- 使用长水平内核并应用形态接近形成行
- 获取行及其边界框的轮廓
- 保存行框并按 Y 排序
- 遍历每个排序的行框并从阈值图像中提取行
- 获取行中每个字符的轮廓并保存字符的边界框。
- 对 X 上给定行的轮廓进行排序
- 在输入上绘制边界框,在图像上绘制索引号作为文本
- 增加索引
- 保存结果
输入:
import cv2
import numpy as np
# read input image
img = cv2.imread('vision78.png')
# convert img to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# otsu threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU )[1]
thresh = 255 - thresh
# apply morphology close to form rows
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (51,1))
morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
# find contours and bounding boxes of rows
rows_img = img.copy()
boxes_img = img.copy()
rowboxes = []
rowcontours = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
rowcontours = rowcontours[0] if len(rowcontours) == 2 else rowcontours[1]
index = 1
for rowcntr in rowcontours:
xr,yr,wr,hr = cv2.boundingRect(rowcntr)
cv2.rectangle(rows_img, (xr, yr), (xr+wr, yr+hr), (0, 0, 255), 1)
rowboxes.append((xr,yr,wr,hr))
# sort rowboxes on y coordinate
def takeSecond(elem):
return elem[1]
rowboxes.sort(key=takeSecond)
# loop over each row
for rowbox in rowboxes:
# crop the image for a given row
xr = rowbox[0]
yr = rowbox[1]
wr = rowbox[2]
hr = rowbox[3]
row = thresh[yr:yr+hr, xr:xr+wr]
bboxes = []
# find contours of each character in the row
contours = cv2.findContours(row, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
for cntr in contours:
x,y,w,h = cv2.boundingRect(cntr)
bboxes.append((x+xr,y+yr,w,h))
# sort bboxes on x coordinate
def takeFirst(elem):
return elem[0]
bboxes.sort(key=takeFirst)
# draw sorted boxes
for box in bboxes:
xb = box[0]
yb = box[1]
wb = box[2]
hb = box[3]
cv2.rectangle(boxes_img, (xb, yb), (xb+wb, yb+hb), (0, 0, 255), 1)
cv2.putText(boxes_img, str(index), (xb,yb), cv2.FONT_HERSHEY_COMPLEX_SMALL, 0.75, (0,255,0), 1)
index = index + 1
# save result
cv2.imwrite("vision78_thresh.jpg", thresh)
cv2.imwrite("vision78_morph.jpg", morph)
cv2.imwrite("vision78_rows.jpg", rows_img)
cv2.imwrite("vision78_boxes.jpg", boxes_img)
# show images
cv2.imshow("thresh", thresh)
cv2.imshow("morph", morph)
cv2.imshow("rows_img", rows_img)
cv2.imshow("boxes_img", boxes_img)
cv2.waitKey(0)
阈值图像:
行的形态图像:
行轮廓图像:
人物轮廓图像:
这是我对这个问题的看法。我会给你它的一般要点,然后是我在 C++
中的实现。主要思想是我想从从左到右、从上到下处理图像。我将处理 每个 blob (或轮廓),但是,我需要几个中间步骤来实现成功的(有序的)分割。
垂直排序使用 行
第一步试图按行对blob进行排序——这意味着每一行都有一组(无序的)水平 斑点。没关系。第一步是计算某种 垂直 排序,如果我们从上到下处理每一行,我们将实现这一点。
在 blob 按行(垂直)排序后,我可以查看它们的 质心(或质心)并水平排序。我的想法是,我将处理 每行 行,并且 for
每行,我对 blob centroids 进行排序。让我们看一个我正在努力实现的例子。
这是您的输入图像:
这就是我所说的行掩码:
最后一张图像包含 白色区域,每个区域代表一个“行”。每个 行 都有一个数字(例如 Row1
、 Row2
等),每个 row
包含一组斑点(或字符,在这个案例)。通过处理每个 row
、 从底部开始的顶部 ,您已经在垂直轴上对 blob 进行了排序。
如果我从上到下对每一行进行编号,我会得到这张图片:
Row Mask 是一种创建“blob 行”的方法,并且可以形态学 计算此掩码。查看叠加的 2 张图像,让您更好地了解处理顺序:
我们在这里尝试做的是,首先是垂直排序(蓝色箭头),然后我们将处理水平排序(红色箭头)。您可以看到,通过处理每一行我们可以(可能)克服排序问题!
水平排序使用质心
现在让我们看看如何对 blob 进行排序 horizontally
。如果我们创建一个更简单的图像,width
等于输入图像,height
等于 Row Mask 中 rows
的数量,我们可以简单地叠加每个斑点质心的每个水平坐标(x 坐标)。看看这个例子:
这是 行 Table。每行代表在 Row Mask 中找到的行数,也是从上到下读取的。 table的width
与输入图像的width
相同,在空间上对应于水平轴。每个 正方形 都是输入图像中的一个像素,仅使用水平坐标映射到行 Table(因为我们对行的简化非常简单)。 table 行中每个像素的实际值是 label
,标记输入图像上的每个斑点。请注意,标签没有排序!
因此,例如,这个 table 表明,在 行 1 中(您已经知道什么是行 1 – 它是 [= =207=]Row Mask) 在位置 (1,4)
中有 blob 编号 3
。在位置 (1,6)
有 blob 编号 2
,依此类推。这个 table 的酷(我认为)是你可以遍历它,并且 for
每个 0
不同的值,水平排序变得非常微不足道。这是 table 排序的行,现在,从左到右:
用质心映射 blob 信息
我们将使用 blob centroids 来 map
我们两个表示之间的信息(行 Mask/Row Table)。假设您已经拥有两个“辅助”图像并且您一次处理输入图像上的每个斑点(或轮廓)。例如,您以此作为开始:
好的,这里有一个斑点。我们如何将它映射到 Row Mask 和 Row Table?使用它的质心。如果我们计算质心(在图中显示为绿点),我们可以构建一个 dictionary
质心和标签。例如,对于此 blob,centroid
位于 (271,193)
。好的,让我们分配 label = 1
。所以我们现在有了这本字典:
现在,我们在行掩码上使用相同的 centroid
找到放置此 blob 的 row
。像这样:
rowNumber = rowMask.at( 271,193 )
这个操作应该returnrownNumber = 3
。好的!我们知道我们的 blob 位于哪一行,因此,它现在是 vertically 排序的。现在,让我们将其 水平 坐标存储在行 Table:
rowTable.at( 271, 193 ) = 1
现在,rowTable
保存(在其行和列中)已处理 blob 的标签。 Row Table 应该是 loo像这样:
table 宽很多,因为它的水平尺寸必须与输入图像相同。在此图像中,label 1
位于 Column 271, Row 3.
如果这是图像上唯一的斑点,则斑点已经排序。但是,如果您在 Column 2
、Row 1
中添加另一个 blob 会怎样?这就是为什么您需要在处理完所有 blob 之后再次遍历此 table 以正确更正它们的标签。
C++ 实现
好吧,希望算法应该有点清楚(如果不明白,就问,伙计)。我将尝试使用 C++
在 OpenCV
中实现这些想法。首先,我需要 binary image
您的输入。使用 Otsu’s thresholding
方法计算很简单:
//Read the input image:
std::string imageName = "C://opencvImages//yFX3M.png";
cv::Mat testImage = cv::imread( imageName );
//Compute grayscale image
cv::Mat grayImage;
cv::cvtColor( testImage, grayImage, cv::COLOR_RGB2GRAY );
//Get binary image via Otsu:
cv::Mat binImage;
cv::threshold( grayImage, binImage, 0, 255, cv::THRESH_OTSU );
//Invert image:
binImage = 255 - binImage;
这是生成的二值图像,没什么特别的,正是我们开始工作所需要的:
第一步是获取Row Mask
。这可以使用形态学来实现。只需应用 dilation + erosion
和 非常 大水平 structuring element
。这个想法是你想把这些斑点变成矩形,把它们水平地“融合”在一起:
//Create a hard copy of the binary mask:
cv::Mat rowMask = binImage.clone();
//horizontal dilation + erosion:
int horizontalSize = 100; // a very big horizontal structuring element
cv::Mat SE = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(horizontalSize,1) );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_DILATE, SE, cv::Point(-1,-1), 2 );
cv::morphologyEx( rowMask, rowMask, cv::MORPH_ERODE, SE, cv::Point(-1,-1), 1 );
结果如下 Row Mask
:
太棒了,现在我们有了 Row Mask
,我们必须给它们编号行,好吗?有很多方法可以做到这一点,但现在我对更简单的方法感兴趣:遍历此图像并获取每个像素。 If
一个像素是白色的,使用 Flood Fill
操作将图像的该部分标记为唯一的斑点(或行,在这种情况下)。这可以按如下方式完成:
//Label the row mask:
int rowCount = 0; //This will count our rows
//Loop thru the mask:
for( int y = 0; y < rowMask.rows; y++ ){
for( int x = 0; x < rowMask.cols; x++ ){
//Get the current pixel:
uchar currentPixel = rowMask.at<uchar>( y, x );
//If the pixel is white, this is an unlabeled blob:
if ( currentPixel == 255 ) {
//Create new label (different from zero):
rowCount++;
//Flood fill on this point:
cv::floodFill( rowMask, cv::Point( x, y ), rowCount, (cv::Rect*)0, cv::Scalar(), 0 );
}
}
}
此过程将标记从 1
到 r
的所有行。这就是我们想要的。如果您查看图像,您会隐约看到这些行,那是因为我们的标签对应于非常低的灰度像素强度值。
好的,现在让我们准备行Table。这个“table”实际上只是另一张图片,请记住:宽度与输入相同,高度与您在 Row Mask
:
//create rows image:
cv::Mat rowTable = cv::Mat::zeros( cv::Size(binImage.cols, rowCount), CV_8UC1 );
//Just for convenience:
rowTable = 255 - rowTable;
在这里,为了方便起见,我只是将最终图像倒置。因为我想真正看到 table 是如何填充(非常低强度)像素的,并确保一切都按预期工作。
现在是有趣的部分。我们准备了两个图像(或数据容器)。我们需要独立处理每个 blob。这个想法是你必须从二进制图像中提取每个 blob/contour/character 并计算它的 centroid
并分配一个新的 label
。同样,有很多方法可以做到这一点。在这里,我使用以下方法:
我将遍历 binary mask
。我将从这个二进制输入中得到 current biggest blob
。我将计算它的 centroid
并将其数据存储在每个需要的容器中,然后,我将从掩码中 delete
那个 blob。我会重复这个过程,直到没有更多的斑点留下。这是我这样做的方式,特别是因为我已经为此编写了函数。这是方法:
//Prepare a couple of dictionaries for data storing:
std::map< int, cv::Point > blobMap; //holds label, gives centroid
std::map< int, cv::Rect > boundingBoxMap; //holds label, gives bounding box
第一,两个dictionaries
。一个接收到一个 blob 标签和 returns 质心。另一个收到相同的标签和 return 边界框。
//Extract each individual blob:
cv::Mat bobFilterInput = binImage.clone();
//The new blob label:
int blobLabel = 0;
//Some control variables:
bool extractBlobs = true; //Controls loop
int currentBlob = 0; //Counter of blobs
while ( extractBlobs ){
//Get the biggest blob:
cv::Mat biggestBlob = findBiggestBlob( bobFilterInput );
//Compute the centroid/center of mass:
cv::Moments momentStructure = cv::moments( biggestBlob, true );
float cx = momentStructure.m10 / momentStructure.m00;
float cy = momentStructure.m01 / momentStructure.m00;
//Centroid point:
cv::Point blobCentroid;
blobCentroid.x = cx;
blobCentroid.y = cy;
//Compute bounding box:
boundingBox boxData;
computeBoundingBox( biggestBlob, boxData );
//Convert boundingBox data into opencv rect data:
cv::Rect cropBox = boundingBox2Rect( boxData );
//Label blob:
blobLabel++;
blobMap.emplace( blobLabel, blobCentroid );
boundingBoxMap.emplace( blobLabel, cropBox );
//Get the row for this centroid
int blobRow = rowMask.at<uchar>( cy, cx );
blobRow--;
//Place centroid on rowed image:
rowTable.at<uchar>( blobRow, cx ) = blobLabel;
//Resume blob flow control:
cv::Mat blobDifference = bobFilterInput - biggestBlob;
//How many pixels are left on the new mask?
int pixelsLeft = cv::countNonZero( blobDifference );
bobFilterInput = blobDifference;
//Done extracting blobs?
if ( pixelsLeft <= 0 ){
extractBlobs = false;
}
//Increment blob counter:
currentBlob++;
}
看看这个处理过程如何遍历每个 blob、处理它并删除它直到什么都不剩下的漂亮动画:
现在,用上面的片段做一些注释。我有一些辅助函数:computeBoundingBox
。这些函数计算二值图像中最大的斑点,并将边界框的自定义结构分别转换为 OpenCV
的 Rect
结构。这些是那些函数执行的操作。
代码段的“内容”是这样的:一旦你有了一个 孤立的 blob,计算它的 centroid
(我实际上计算 center of mass
通过central moments
)。生成一个新的 label
。将此 label
和 centroid
存储在 dictionary
中,在我的例子中,是 blobMap
字典。另外计算 bounding box
并将其存储在另一个 dictionary
, boundingBoxMap
:
//Label blob:
blobLabel++;
blobMap.emplace( blobLabel, blobCentroid );
boundingBoxMap.emplace( blobLabel, cropBox );
现在,使用 centroid
数据,fetch
那个 blob 对应的 row
。获得该行后,将此数字存储到您的行中 table:
//Get the row for this centroid
int blobRow = rowMask.at<uchar>( cy, cx );
blobRow--;
//Place centroid on rowed image:
rowTable.at<uchar>( blobRow, cx ) = blobLabel;
非常好。此时,您已准备好行 Table。让我们遍历它,实际上,最后,订购那些该死的斑点:
int blobCounter = 1; //The ORDERED label, starting at 1
for( int y = 0; y < rowTable.rows; y++ ){
for( int x = 0; x < rowTable.cols; x++ ){
//Get current label:
uchar currentLabel = rowTable.at<uchar>( y, x );
//Is it a valid label?
if ( currentLabel != 255 ){
//Get the bounding box for this label:
cv::Rect currentBoundingBox = boundingBoxMap[ currentLabel ];
cv::rectangle( testImage, currentBoundingBox, cv::Scalar(0,255,0), 2, 8, 0 );
//The blob counter to string:
std::string counterString = std::to_string( blobCounter );
cv::putText( testImage, counterString, cv::Point( currentBoundingBox.x, currentBoundingBox.y-1 ),
cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255,0,0), 1, cv::LINE_8, false );
blobCounter++; //Increment the blob/label
}
}
}
没什么特别的,只是一个常规的嵌套 for
循环,遍历 row table
上的每个像素。如果像素不同于白色,则使用 label
检索 centroid
和 bounding box
,并将 label
更改为递增数字即可。对于结果显示,我只是在原始图像上绘制边界框和新标签。
查看动画中的有序处理:
非常酷,这是一个额外的动画,行 Table 填充了水平坐标: