从图像中去除背景噪音,使 OCR 的文本更清晰

Posted

技术标签:

【中文标题】从图像中去除背景噪音,使 OCR 的文本更清晰【英文标题】:Remove background noise from image to make text more clear for OCR 【发布时间】:2016-02-26 03:46:40 【问题描述】:

我编写了一个应用程序,它根据图像中的文本区域对图像进行分割,并在我认为合适的时候提取这些区域。我正在尝试做的是清理图像,以便 OCR(Tesseract)给出准确的结果。我以下面的图片为例:

通过 tesseract 运行它会得到一个广泛不准确的结果。但是清理图像(使用 Photoshop)得到图像如下:

给出了我期望的结果。第一个图像已经通过以下方法运行以将其清理到该点:

 public Mat cleanImage (Mat srcImage) 
    Core.normalize(srcImage, srcImage, 0, 255, Core.NORM_MINMAX);
    Imgproc.threshold(srcImage, srcImage, 0, 255, Imgproc.THRESH_OTSU);
    Imgproc.erode(srcImage, srcImage, new Mat());
    Imgproc.dilate(srcImage, srcImage, new Mat(), new Point(0, 0), 9);
    return srcImage;

我还能做些什么来清理第一个图像,使其与第二个图像相似?

编辑:这是通过cleanImage 函数运行之前的原始图像。

【问题讨论】:

您好,会尽快处理。干杯。 @Miki 我在处理之前添加了原始图像。 如果您知道文本始终大致位于图像的中心,您可以删除连接的暗像素段,其中段中没有像素超出边缘一定距离。如果您知道文本的大小始终相同,则可以删除其中像素少于某个阈值数量的深色文本的连接段。如果您以某种方式对齐图像并且数字都是相同的高度,您可以尝试计算顶线和底线并抛出异常值。如果总是有 4 位数字,您可以使用它来删除某些规则中大于 4 的段。 您可以过滤图像边界附近的噪声段(连接组件)(即连接到图像边界):在您的示例中,所需的文本未连接到边界。 runitme 真的很重要吗? 【参考方案1】:

这张图片对你有帮助吗?

生成该图像的算法很容易实现。我敢肯定,如果你调整它的一些参数,你可以为这种图像获得非常好的结果。

我用 tesseract 测试了所有图像:

原始图像:未检测到任何内容 已处理的图像 #1:未检测到任何内容 处理后的图像 #2:12-14(完全匹配) 我处理的图像:y'1'2-14/j

【讨论】:

你在移除边缘的连接组件后尝试了 tesseract 吗?由于在您处理的图像中,边缘的连接组件根本没有连接到文本,删除它们可能会产生更好的结果。 你是对的!如果把那些连接的结构去掉,肯定会得到更好的结果。在发布该图像时,我不知道这一事实。我认为 tesseract 足够强大,可以独立完成这项工作,并且只需消除数字之间的噪音和其他伪影就足够了。我将对该算法进行扩展,使其保持简单但摆脱边界结构。干杯! 另外,您可以将您的算法添加到答案中吗? Tesseract 可能很棘手。尝试运行tesseract -psm 7 yourimage.png digits,这将强制 tesseract 仅识别数字。您能否在上面发布您减少图像的方法? 是的,我将发布代码。我只是理论上有它,稍后会实施和发布。除此之外,如果我们能用随机分布在图像中的更大结构(不仅连接到边界)来解决您的问题,那将会很有趣。【参考方案2】:

我的回答基于以下假设。在你的情况下,它们可能都不成立。

您可以为分割区域中的边界框高度设置阈值。然后您应该能够过滤掉其他组件。 您知道数字的平均笔画宽度。使用此信息可以最大限度地减少数字连接到其他区域的机会。为此,您可以使用距离变换和形态学运算。

这是我提取数字的过程:

对图像应用 Otsu 阈值 进行距离变换

使用描边宽度 (= 8) 约束对距离变换图像进行阈值处理

应用形态学操作断开连接

过滤边界框高度并猜测数字在哪里

笔画宽度 = 8 笔画宽度 = 10

编辑

使用找到的数字轮廓的凸包准备掩码

使用掩码将数字区域复制到干净的图像

笔画宽度 = 8

笔画宽度 = 10

我的 Tesseract 知识有点生疏。我记得你可以获得角色的信心水平。如果您仍然碰巧将嘈杂区域检测为字符边界框,则可以使用此信息过滤掉噪声。

C++ 代码

Mat im = imread("aRh8C.png", 0);
// apply Otsu threshold
Mat bw;
threshold(im, bw, 0, 255, CV_THRESH_BINARY_INV | CV_THRESH_OTSU);
// take the distance transform
Mat dist;
distanceTransform(bw, dist, CV_DIST_L2, CV_DIST_MASK_PRECISE);
Mat dibw;
// threshold the distance transformed image
double SWTHRESH = 8;    // stroke width threshold
threshold(dist, dibw, SWTHRESH/2, 255, CV_THRESH_BINARY);
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
// perform opening, in case digits are still connected
Mat morph;
morphologyEx(dibw, morph, CV_MOP_OPEN, kernel);
dibw.convertTo(dibw, CV_8U);
// find contours and filter
Mat cont;
morph.convertTo(cont, CV_8U);

Mat binary;
cvtColor(dibw, binary, CV_GRAY2BGR);

const double HTHRESH = im.rows * .5;    // height threshold
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
vector<Point> digits; // points corresponding to digit contours

findContours(cont, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));
for(int idx = 0; idx >= 0; idx = hierarchy[idx][0])

    Rect rect = boundingRect(contours[idx]);
    if (rect.height > HTHRESH)
    
        // append the points of this contour to digit points
        digits.insert(digits.end(), contours[idx].begin(), contours[idx].end());

        rectangle(binary, 
            Point(rect.x, rect.y), Point(rect.x + rect.width - 1, rect.y + rect.height - 1),
            Scalar(0, 0, 255), 1);
    


// take the convexhull of the digit contours
vector<Point> digitsHull;
convexHull(digits, digitsHull);
// prepare a mask
vector<vector<Point>> digitsRegion;
digitsRegion.push_back(digitsHull);
Mat digitsMask = Mat::zeros(im.rows, im.cols, CV_8U);
drawContours(digitsMask, digitsRegion, 0, Scalar(255, 255, 255), -1);
// expand the mask to include any information we lost in earlier morphological opening
morphologyEx(digitsMask, digitsMask, CV_MOP_DILATE, kernel);
// copy the region to get a cleaned image
Mat cleaned = Mat::zeros(im.rows, im.cols, CV_8U);
dibw.copyTo(cleaned, digitsMask);

编辑

Java 代码

Mat im = Highgui.imread("aRh8C.png", 0);
// apply Otsu threshold
Mat bw = new Mat(im.size(), CvType.CV_8U);
Imgproc.threshold(im, bw, 0, 255, Imgproc.THRESH_BINARY_INV | Imgproc.THRESH_OTSU);
// take the distance transform
Mat dist = new Mat(im.size(), CvType.CV_32F);
Imgproc.distanceTransform(bw, dist, Imgproc.CV_DIST_L2, Imgproc.CV_DIST_MASK_PRECISE);
// threshold the distance transform
Mat dibw32f = new Mat(im.size(), CvType.CV_32F);
final double SWTHRESH = 8.0;    // stroke width threshold
Imgproc.threshold(dist, dibw32f, SWTHRESH/2.0, 255, Imgproc.THRESH_BINARY);
Mat dibw8u = new Mat(im.size(), CvType.CV_8U);
dibw32f.convertTo(dibw8u, CvType.CV_8U);

Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
// open to remove connections to stray elements
Mat cont = new Mat(im.size(), CvType.CV_8U);
Imgproc.morphologyEx(dibw8u, cont, Imgproc.MORPH_OPEN, kernel);
// find contours and filter based on bounding-box height
final double HTHRESH = im.rows() * 0.5; // bounding-box height threshold
List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
List<Point> digits = new ArrayList<Point>();    // contours of the possible digits
Imgproc.findContours(cont, contours, new Mat(), Imgproc.RETR_CCOMP, Imgproc.CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); i++)

    if (Imgproc.boundingRect(contours.get(i)).height > HTHRESH)
    
        // this contour passed the bounding-box height threshold. add it to digits
        digits.addAll(contours.get(i).toList());
       

// find the convexhull of the digit contours
MatOfInt digitsHullIdx = new MatOfInt();
MatOfPoint hullPoints = new MatOfPoint();
hullPoints.fromList(digits);
Imgproc.convexHull(hullPoints, digitsHullIdx);
// convert hull index to hull points
List<Point> digitsHullPointsList = new ArrayList<Point>();
List<Point> points = hullPoints.toList();
for (Integer i: digitsHullIdx.toList())

    digitsHullPointsList.add(points.get(i));

MatOfPoint digitsHullPoints = new MatOfPoint();
digitsHullPoints.fromList(digitsHullPointsList);
// create the mask for digits
List<MatOfPoint> digitRegions = new ArrayList<MatOfPoint>();
digitRegions.add(digitsHullPoints);
Mat digitsMask = Mat.zeros(im.size(), CvType.CV_8U);
Imgproc.drawContours(digitsMask, digitRegions, 0, new Scalar(255, 255, 255), -1);
// dilate the mask to capture any info we lost in earlier opening
Imgproc.morphologyEx(digitsMask, digitsMask, Imgproc.MORPH_DILATE, kernel);
// cleaned image ready for OCR
Mat cleaned = Mat.zeros(im.size(), CvType.CV_8U);
dibw8u.copyTo(cleaned, digitsMask);
// feed cleaned to Tesseract

【讨论】:

需要考虑的几件事:不仅仅是数字;还需要检测负号;检测到的元素需要合并到一张图片中作为 tesseract 的输入源。 @MarkusAtCvlabDotDe 我已经更新了我的答案,需要修改以获得干净的图像。 稍后我也会介绍我的解决方案。它基于 blob 和更少的代码。 +1 谢谢。我的 C++ 不是那么好。我已经在 J​​ava 中实现了这个解决方案,仅使用 convexHull 创建掩码不会提供与上面显示的结果相同的结果。我在这里发布了代码:pastebin.com/KfYFu1vk @XueQing 它应该很容易转换为 python,因为 opencv 调用在 c++ 和 java 中是相似的。目前没有添加python代码的计划。【参考方案3】:

开箱即用的一点点思考:

我可以从您的原始图片中看出,这是一个相当严格的预格式化文件,看起来像路税徽章或类似的东西,对吧?

如果上述假设是正确的,那么您可以实施一个不太通用的解决方案:您试图消除的噪音是由于特定文档模板的特征,它发生在图像的特定和已知区域。事实上,文字也是如此。

在这种情况下,其中一种方法是定义您知道存在此类“噪音”的区域的边界,然后将它们涂白。

然后,按照您已在执行的其余步骤进行操作:进行降噪以去除最精细的细节(即看起来像徽章中的安全水印或全息图的背景图案)。结果应该足够清晰,Tesseract 可以毫无问题地处理。

只是一个想法。我承认这不是通用解决方案,因此这取决于您的实际要求。

【讨论】:

【参考方案4】:

我认为在调用 tesseract 之前,您需要在预处理部分做更多工作,以使图像尽可能清晰。

我的想法如下:

1-从图像中提取轮廓并在图像中找到轮廓(检查this)和this

2- 每个轮廓都有宽度、高度和面积,所以你可以根据宽度、高度和面积过滤轮廓(检查this和this),另外你可以使用部分轮廓分析此处的代码过滤轮廓,您可以使用模板轮廓匹配删除与“字母或数字”轮廓不相似的轮廓。

3- 过滤轮廓后,您可能会检查此图像中的字母和数字在哪里,因此您可能需要使用一些文本检测方法,例如here

4- 如果要删除非文本区域以及图像中不好的轮廓,您现在需要的所有内容

5- 现在您可以创建您的二值化方法,或者您可以使用 tesseract 对图像进行二值化,然后在图像上调用 OCR。

当然,这些是执行此操作的最佳步骤,您可以使用其中的一些,这对您来说可能就足够了。

其他想法:

您可以使用不同的方法来做到这一点,最好的办法是找到一种方法来检测数字和字符的位置,使用不同的方法,如模板匹配,或基于特征的 HOG。

您可能首先对您的图像进行二值化并获得二值图像,然后您需要对水平和垂直应用具有线结构的开口,这将有助于您检测边缘并在图像上进行分割然后是 OCR。

在检测出图像中的所有轮廓后,您还可以使用Hough transformation 来检测像one 这样的任何类型的线条和定义的曲线,这样您就可以检测出有线条的字符,这样您就可以分割图像,然后进行 OCR。

更简单的方法:

1- 进行二值化

2- 一些形态学操作来分离轮廓:

3- 反转图像中的颜色(这可能在第 2 步之前)

4- 查找图像中的所有轮廓

5-删除所有宽度大于其高度的轮廓,删除非常小的轮廓,非常大的轮廓和非矩形轮廓

注意:您可以使用文本检测方法(或使用 HOG 或边缘检测)代替第 4 步和第 5 步

6- 找到包含图像中所有剩余轮廓的大矩形

7- 您可以做一些额外的预处理来增强 tesseract 的输入,然后您现在可以调用 OCR。 (我建议您裁剪图像并将其作为 OCR 的输入[我的意思是裁剪黄色矩形,不要将整个图像仅作为黄色矩形的输入,这也会增强结果])

【讨论】:

【参考方案5】:

字体大小不宜过大或过小,大约应该在 10-12 pt 的范围内(即字符高度大约在 20 以上且小于 80)。您可以对图像进行下采样并尝试使用 tesseract。而且很少有字体没有在 tesseract 中训练过,如果不是在训练过的字体中,可能会出现问题。

【讨论】:

以上是关于从图像中去除背景噪音,使 OCR 的文本更清晰的主要内容,如果未能解决你的问题,请参考以下文章

去除历史文档中的噪音和污点以进行 OCR 识别

去除 OCR 图像处理中的背景颜色

ImageMagick消除背景噪音

ImageMagick消除背景噪音并将其留白

OCR 的背景图像清理

为 OCR 准备复杂图像