为 OCR 准备复杂图像

Posted

技术标签:

【中文标题】为 OCR 准备复杂图像【英文标题】:Prepare complex image for OCR 【发布时间】:2012-03-12 23:48:35 【问题描述】:

我想识别信用卡中的数字。更糟糕的是,源图像不能保证是高质量的。 OCR 是通过神经网络实现的,但这不应该是这里的主题。

当前的问题是图像预处理。由于信用卡可能有背景和其他复杂的图形,因此文本不像扫描文档那样清晰。我用边缘检测(Canny Edge,Sobel)做了实验,但没那么成功。 此外,计算灰度图像和模糊图像之间的差异(如 Remove background color in image processing for OCR 所述)不会导致 OCRable 结果。

我认为大多数方法都失败了,因为特定数字与其背景之间的对比度不够强。可能需要将图像分割成块并为每个块找到最佳的预处理方案?

您对如何将源代码转换为可读的二进制图像有什么建议吗? 边缘检测是可行的方法还是我应该坚持基本的颜色阈值处理?

这是灰度阈值方法的示例(我显然对结果不满意):

原图:

灰度图像:

阈值图像:

感谢您的任何建议, 瓦伦丁

【问题讨论】:

由于对比度太小,我会尝试边缘检测,正如你所说的。 【参考方案1】:

如果可能,请求使用更好的照明来捕捉图像。低角度的灯光会照亮凸起(或凹陷)字符的边缘,从而大大提高图像质量。如果图像要由机器分析,则应优化照明以提高机器可读性。

也就是说,您应该研究的一种算法是笔画宽度变换,它用于从自然图像中提取字符。

Stroke Width Transform (SWT) implementation (Java, C#...)

全局阈值(用于二值化或剪裁边缘强度)可能不会为此应用程序削减它,相反,您应该查看局部阈值。在您的示例图像中,“31”之后的“02”特别弱,因此在该区域中搜索最强的局部边缘比使用单个阈值过滤字符串中的所有边缘要好。

如果您可以识别字符的部分片段,那么您可能会使用一些方向形态学操作来帮助连接片段。例如,如果您有两个几乎水平的片段,如下所示,其中 0 是背景,1 是前景......

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 1 1 1 1 0 0 1 1 1 1 1 1 0 0 0
0 0 0 1 0 0 0 1 0 1 0 0 0 0 1 0 0 0

然后您可以沿水平方向执行形态“关闭”操作,仅连接这些段。内核可能类似于

x x x x x
1 1 1 1 1
x x x x x

有更复杂的方法可以使用 Bezier 拟合甚至 Euler 螺旋(也称为回旋曲线)来执行曲线补全,但是识别要连接的线段的预处理和消除不良连接的后处理可能会变得非常棘手。

【讨论】:

【参考方案2】:

解决问题的方法是将卡片分成不同的部分。以 开头的独特信用卡并不多(MasterCard、Visa、 列表由您决定),因此您可以在下拉列表中指定它是哪张信用卡。这样,您可以消除并指定像素区域:

示例:

仅适用于距底部 20 像素、距底部 30 像素的区域 从左到右 10 像素到底部 30 像素(创建一个 矩形) - 这将涵盖所有万事达卡

当我使用图像处理程序(有趣的项目)时,我调高了图片的对比度,将其转换为灰度,取每个 1 个像素的 RGB 值的平均值,并将其与周围的所有像素进行比较:

示例:

PixAvg[i,j] = (Pix.R + Pix.G + Pix.B)/3
if ((PixAvg[i,j] - PixAvg[i,j+1])>30)
    boolEdge == true;

30 代表您希望自己的形象有多么鲜明。差异越小,公差越低。

在我的项目中,为了查看边缘检测,我制作了一个单独的布尔数组,其中包含来自 boolEdge 的值和一个像素数组。像素阵列仅填充有黑白点。它从布尔数组中获取值,其中 boolEdge = true 是一个白点,而 boolEdge = false 是一个黑点。所以最后,你会得到一个只包含白点和黑点的像素阵列(全图)。

从那里,更容易检测数字的开始位置和结束位置。

【讨论】:

【参考方案3】:

在我的实现中,我尝试使用此处的代码:http://rnd.azoft.com/algorithm-identifying-barely-legible-embossed-text-image/ 结果更好,但还不够…… 我发现很难找到纹理卡的正确参数。

(void)processingByStrokesMethod:(cv::Mat)src dst:(cv::Mat*)dst  
cv::Mat tmp;  
cv::GaussianBlur(src, tmp, cv::Size(3,3), 2.0);                    // gaussian blur  
tmp = cv::abs(src - tmp);                                          // matrix of differences between source image and blur iamge  

//Binarization:  
cv::threshold(tmp, tmp, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);  

//Using method of strokes:  
int Wout = 12;  
int Win = Wout/2;  
int startXY = Win;  
int endY = src.rows - Win;  
int endX = src.cols - Win;  

for (int j = startXY; j < endY; j++)   
    for (int i = startXY; i < endX; i++)   
        //Only edge pixels:  
        if (tmp.at<unsigned char="">(j,i) == 255)  
          
            //Calculating maxP and minP within Win-region:  
            unsigned char minP = src.at<unsigned char="">(j,i);  
            unsigned char maxP = src.at<unsigned char="">(j,i);  
            int offsetInWin = Win/2;  

            for (int m = - offsetInWin; m < offsetInWin; m++)   
                for (int n = - offsetInWin; n < offsetInWin; n++)   
                    if (src.at<unsigned char="">(j+m,i+n) < minP)   
                        minP = src.at<unsigned char="">(j+m,i+n);  
                    else if (src.at<unsigned char="">(j+m,i+n) > maxP)   
                        maxP = src.at<unsigned char="">(j+m,i+n);  
                      
                  
              

            //Voiting:  
            unsigned char meanP = lroundf((minP+maxP)/2.0);  

            for (int l = -Win; l < Win; l++)   
                for (int k = -Win; k < Win; k++)   
                    if (src.at<unsigned char="">(j+l,i+k) >= meanP)   
                        dst->at<unsigned char="">(j+l,i+k)++;  
                      
                  
              
          
      
  

///// Normalization of imageOut:  
unsigned char maxValue = dst->at<unsigned char="">(0,0);  

for (int j = 0; j < dst->rows; j++)               //finding max value of imageOut  
    for (int i = 0; i < dst->cols; i++)   
        if (dst->at<unsigned char="">(j,i) > maxValue)  
            maxValue = dst->at<unsigned char="">(j,i);  
      
  
float knorm = 255.0 / maxValue;  

for (int j = 0; j < dst->rows; j++)              //normalization of imageOut  
    for (int i = 0; i < dst->cols; i++)   
        dst->at<unsigned char="">(j,i) = lroundf(dst->at<unsigned char="">(j,i)*knorm);  
      
  

【讨论】:

好的,你提供了链接,你能不能也为OP提供一些解释。

以上是关于为 OCR 准备复杂图像的主要内容,如果未能解决你的问题,请参考以下文章

Python + OpenCV:OCR 图像分割

iOS Tesseract OCR 图像准备

使用 ImageMagick 和 'textcleaner' 为 OCR 清理图像

用于文档 OCR/可读性的图像处理/增强算法?

OCR技术浅探:特征提取

具有两列文本的 Google Vision 复杂 OCR 执行