根据Python,OpenCV中的优先级对轮廓进行排序

问题描述

我正在尝试根据left-to-righttop-to-bottom的到来对轮廓进行排序,就像您编写任何内容一样。从topleft开始,然后依次选择哪个。

这是我到目前为止所取得的成就和成就:

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,0),2)
    cv2.putText(image,label,(x - 2,y - 2),cv2.FONT_HERShey_SIMPLEX,0.5,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)

输入图像:

Input image

输出图像1:

Output Image 1(WOrking)

输入图片2:

Image Input 2

图像2的输出

Output Image 2

输入图片3:

Input Image 3

输出图片3:

Output Image 3

正如您所看到的,1,2,3,4不是我期望的每幅图像,如图像编号3所示。

我如何调整它以使其正常工作甚至编写自定义函数

注意:我在问题中提供了同一输入图像的多张图像。内容是相同的,但是它们的文本有所不同,因此tolerance factor不适用于每个文本。手动调整它不是一个好主意。

解决方法

我宁愿使用质心或至少使用边界框中心,而不是使用轮廓的左上角。

Traceback (most recent call last):
  File "C:\Users\chengwu\Desktop\Free travel\Free_travel_code.py",line 10,in <module>
    df=pd.read_excel(path) #read the file you choose earlier on
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\util\_decorators.py",line 208,in wrapper
    return func(*args,**kwargs)
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\io\excel\_base.py",line 310,in read_excel
    io = ExcelFile(io,engine=engine)
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\io\excel\_base.py",line 819,in __init__
    self._reader = self._engines[engine](self._io)
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\io\excel\_xlrd.py",line 21,in __init__
    super().__init__(filepath_or_buffer)
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\io\excel\_base.py",line 359,in __init__
    self.book = self.load_workbook(filepath_or_buffer)
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\io\excel\_xlrd.py",line 36,in load_workbook
    return open_workbook(filepath_or_buffer)
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\xlrd\__init__.py",line 148,in open_workbook
    bk = book.open_workbook_xls(
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\xlrd\book.py",line 92,in open_workbook_xls
    biff_version = bk.getbof(XL_WORKBOOK_GLOBALS)
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\xlrd\book.py",line 1278,in getbof
    bof_error('Expected BOF record; found %r' % self.mem[savpos:savpos+8])
  File "C:\Users\chengwu\AppData\Local\Programs\Python\Python38\lib\site-packages\xlrd\book.py",line 1272,in bof_error
    raise XLRDError('Unsupported format,or corrupt file: ' + msg)
xlrd.biffh.XLRDError: Unsupported format,or corrupt file: Expected BOF record; found b'import p'

但是很难找到在所有情况下都有效的公差值。

,

我什至会说使用色相矩,它倾向于更好地估计多边形的中心点 比矩形的“法线”坐标中心点大,因此该函数可能是:

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

也许您应该考虑摆脱这种宽容因素 通常使用类似的聚类算法 kmeans将您的中心聚类到行和列。 OpenCv有一个kmeans实现,您可以找到here

我不确定您的目标是什么,但是另一个想法可能是将每一行划分为一个感兴趣的区域(ROI) 进行进一步处理,之后您可以轻松计算字母 通过每个轮廓的X值和行号

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,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,(255,0),1)

for y in lowers:
    cv2.line(rotated,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

我发现了一条旧代码为here

的帖子 ,

在Python / OpenCV中,这是一种方法,首先处理行,然后处理字符。

  • 阅读输入内容
  • 转换为灰度
  • 阈值和求反
  • 使用较长的水平核并在靠近行的位置应用形态学
  • 获取行的轮廓及其边界框
  • 保存行框并按Y排序
  • 在每个排序的行框上循环,然后从阈值图像中提取行
  • 获取行中每个字符的轮廓并保存字符的边框。
  • 对X上给定行的轮廓进行排序
  • 在输入上绘制边框,并在图像上以文本形式显示索引号
  • 增加索引
  • 保存结果

输入:

enter image description here

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,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),255),1)
    rowboxes.append((xr,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.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,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),1)
        cv2.putText(boxes_img,str(index),cv2.FONT_HERSHEY_COMPLEX_SMALL,0.75,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)

阈值图像:

enter image description here

行的形态图像:

enter image description here

行轮廓图像:

enter image description here

人物轮廓图像:

enter image description here

,

这是我对这个问题的看法。我将给您大致的要点,然后在C++中介绍我的实现。主要思想是我要从从左到右,从从上到下处理图像。找到后,我将处理每个斑点(或轮廓),但是,我需要一些中间步骤来实现成功的(有序的)分割。

使用

进行垂直排序

第一步正在尝试按行对斑点进行排序 –这意味着每一行都有一组(无序)水平斑点。没关系。第一步是计算某种 vertical 排序,如果我们从上到下处理每一行,我们将实现这一目标。

按行对Blob(垂直)进行排序后,我可以查看其质心(或质心)并对其进行水平排序。这个想法是,我将处理每行行,并且for每行对Blob 质心进行排序。让我们来看一个我想在这里实现的示例。

这是您的输入图片:

这就是我所说的行面具

最后一张图片包含白色区域,每个区域代表一个“行”。每个有一个数字(例如Row1Row2等),每个row拥有一组斑点(在这种情况下为字符) 。通过处理每个row自上而下,您已经在垂直轴上对blob进行了排序。

如果我从上到下对每一行进行编号,则会得到以下图像:

行掩码是一种创建“斑点行”的方法,并且可以从形态上计算。查看叠加的2张图片,以更好地了解处理顺序:

我们在这里要做的是,首先是垂直排序(蓝色箭头),然后我们将处理水平(红色箭头)排序。您可以看到,通过处理每一行,我们可以(可能)克服排序问题!

使用质心

进行水平排序

现在让我们看看如何对blob horizontally进行排序。如果我们创建一个更简单的图像,其中 Row Mask 中的width等于输入图像,而height等于rows的数量,我们可以只需覆盖每个斑点质心的每个水平坐标(x坐标)即可。看看这个例子:

这是行表。每行表示在行掩码中找到的行数,并且也从上至下读取。表格的width与输入图像的width相同,并且在空间上与水平轴相对应。每个 square 是输入图像中的一个像素,仅使用水平坐标即可映射到“行表”(因为简化行非常简单)。行表中每个像素的实际值是label,标记输入图像上的每个斑点。请注意,标签未订购!

例如,此表显示了第1行(您已经知道第1行是什么-这是行掩码上的第一个白色区域)在(1,4)位置有一个斑点编号3。在位置(1,6)中,有斑点号2,依此类推。关于此表,最酷的是(我认为)您可以遍历该表,并且for的每个值与0不同,水平排序变得非常琐碎。现在,这是从左到右排序的行表:

用质心映射Blob信息

我们将使用斑点质心map两种表示形式(行掩码/行表)之间的信息。假设您已经拥有两个“辅助”图像,并且一次处理了输入图像上的每个斑点(或轮廓)。例如,您以此为起点:

好的,这里有一个斑点。我们如何将其映射到行掩码行表?使用其质心。如果我们计算质心(图中显示为绿点),则可以构造一个dictionary的质心和标签。例如,对于此blob,centroid位于(271,193)。好的,让我们分配label = 1。现在我们有了这本字典:

现在,我们在行掩码上使用相同的row找到了放置该斑点的centroid。像这样:

rowNumber = rowMask.at( 271,193 )

此操作应返回rownNumber = 3。真好!我们知道我们的Blob放在哪一行,因此,现在垂直进行了排序。现在,让我们将其水平坐标存储在行表中:

rowTable.at( 271,193 ) = 1

现在,rowTable保留(在其行和列中)已处理Blob的标签。行表应如下所示:

该表格的宽度很多,因为其水平尺寸必须与您输入的图像相同。在此图像中,label 1被放置在Column 271,Row 3.中。如果这是图像上唯一的斑点,则斑点已被排序。但是,如果在Column 2Row 1中添加另一个Blob,会发生什么情况?这就是为什么您需要在处理完所有Blob之后再次遍历此表-正确纠正其标记。

C ++的实现

好的,希望算法应该有点清楚(如果不是,请问我的人)。我将尝试使用OpenCVC++中实现这些想法。首先,我需要您输入的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,cv::THRESH_OTSU );

//Invert image:
binImage = 255 - binImage;

这是生成的二进制映像,没什么花哨的,只是我们开始工作所需要的:

第一步是获取Row Mask。这可以使用形态来实现。只需应用{strong>非常好大水平dilation + erosion的{​​{1}}。这个想法是,您想将这些斑点变成矩形,然后将它们水平“融合”在一起:

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,cv::MORPH_ERODE,1 );

这很酷,现在我们有了Row Mask,我们必须给它们编号行,好吗?有很多方法可以做到这一点,但现在我对更简单的方法感兴趣:遍历此图像并获取每个像素。 Row Mask像素为白色,请使用If操作将图像的该部分标记为唯一的Blob(在这种情况下为行)。可以按照以下步骤进行操作:

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上计数的行数相同的高度:

Row Mask

在这里,为了方便起见,我只是反转了最终图像。因为我想实际查看表格中如何填充(非常低的像素)像素,并确保一切正常进行。

现在是有趣的部分。我们准备了两个图像(或数据容器)。我们需要独立处理每个斑点。这个想法是,您必须从二进制图像中提取每个斑点/轮廓/字符并计算其//create rows image: cv::Mat rowTable = cv::Mat::zeros( cv::Size(binImage.cols,rowCount),CV_8UC1 ); //Just for convenience: rowTable = 255 - rowTable; 并分配一个新的centroid。同样,有很多方法可以做到这一点。在这里,我使用以下方法:

我将遍历label。我将从此二进制输入中获取binary mask。我将计算其current biggest blob并将其数据存储在所需的每个容器中,然后将centroid从掩码中删除。我将重复该过程,直到不再留下斑点为止。这是我执行此操作的方式,尤其是因为我已经为此编写了函数。这是方法:

delete

首先,两个//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,对其进行处理并删除它,直到没有剩余内容为止:

现在,带有上面摘录的一些笔记。我有一些辅助函数:biggestBlob//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的{​​{1}}结构。这些是这些功能执行的操作。

代码段的“内容”是这样的:一旦有了一个隔离的blob ,就计算其OpenCV(我实际上是通过Rect计算了centroid )。生成一个新的center of mass。将此central momentslabel存储在label字典的centroid中。另外,计算dictionary并将其存储在另一个blobMapbounding box中:

dictionary

现在,使用boundingBoxMap数据,//Label blob: blobLabel++; blobMap.emplace( blobLabel,blobCentroid ); boundingBoxMap.emplace( blobLabel,cropBox ); 该Blob的相应centroid。获取行后,将此数字存储到行表中:

fetch

非常好。至此,您已经准备好行表。让我们遍历它,最后,最后订购那些该死的斑点:

row

没有什么幻想,只是一个常规的嵌套//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; 循环,遍历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,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,cv::LINE_8,false ); blobCounter++; //Increment the blob/label } } } 上的每个像素。如果像素不同于白色,请使用for来同时获取row tablelabel,然后将centroid更改为递增的数字。为了显示结果,我只需要在原始图像上绘制边界框和新标签。

查看此动画中的顺序处理:

非常酷,这是一个奖励动画,行表中填充了水平坐标: