从图像中去除背景噪声以使OCR更清晰地识别文本

29

我编写了一个应用程序,根据图像中的文本区域对其进行分割,并按照我的要求提取这些区域。我正在尝试清理图像,以便OCR(Tesseract)能够给出准确的结果。我有以下图像作为示例:

enter image description here

通过tesseract处理得到的结果非常不准确。但是通过Photoshop清理图片后,得到了如下图片:

enter image description here

给出了我期望的准确结果。第一张图片已经通过以下方法进行清理,以达到该点:
 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”函数处理之前的原始图片。

enter image description here


嗨,会尽快完成。干杯。 - Zy0n
@Miki 在处理之前,我已经添加了原始图像。 - Zy0n
2
如果您知道文本始终大致位于图像中心,则可以删除连接的黑色像素段,其中段中没有一个像素在一定距离之外。 如果您知道文本始终具有相同的大小,则可以删除连接的黑色文本段,这些段中的像素数量少于一定阈值。 如果您以某种方式对齐了图像并且数字高度都相同,则可以尝试计算顶部行和底部行,并丢弃异常值。 如果始终有4个数字,则可以使用该规则删除大于4的段。 - Pace
您可以过滤靠近图像边缘(即连接到图像边缘)的噪声片段(连通组件):在您的示例所需文本未连接到边框。 - avtomaton
运行时实际上很重要吗? - MarkusAtCvlabDotDe
5个回答

31

我的答案基于以下假设。在您的情况下,可能没有一个符合条件。

  • 您可以对分割区域中的边界框高度进行阈值限制。然后,您应该能够过滤掉其他组件。
  • 您知道数字的平均笔画宽度。使用此信息最小化数字连接到其他区域的机会。您可以使用距离变换和形态学操作来实现。

这是我提取数字的步骤:

  • 对图像应用Otsu阈值 otsu
  • 获取距离变换 dist
  • 使用笔画宽度(=8)约束对距离变换图像进行阈值处理 sw2

  • 应用形态学操作以断开连接 ws2op

  • 过滤边界框高度并猜测数字所在位置

笔画宽度=8 bb 笔画宽度=10 bb2

编辑

  • 使用找到的数字轮廓的凸包准备一个掩模 mask

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

笔画宽度=8 cl1

笔画宽度=10 cl2

我的 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
谢谢。我的C++不是很好。我只在Java中实现了这个解决方案,但使用convexHull创建掩码并不能像您上面显示的那样提供相同的结果。我已经在这里发布了代码:http://pastebin.com/KfYFu1vk - Zy0n
我想在iOS上实现它,如果有人能帮忙,将不胜感激。 - Ahmed Sahib
@dhanushka,这是一个不错的解决方案。我不熟悉Java或C++,你能把相同的代码用Python写一遍吗? - Gary Chen
1
@XueQing,将其转换为Python应该很容易,因为OpenCV调用在C++和Java中非常相似。目前没有计划添加Python代码。 - dhanushka
显示剩余7条评论

7
I think you need to work more on the pre-processing part to prepare the image to be clear as much as you can before calling the tesseract.
What's my ideas to do that are the following:
1- Extract contours from the image and find contours in the image (check this) and this 2- Each contour has width, height and area, so you may filter the contours according to the width, height and its area (check this and this), plus you may use some part of the contour analysis code here to filter the contours. Furthermore, you may delete the contours that are not similar to a "letter or number" contour using a template contour matching.

3-筛选轮廓后,您可以查看图像中的字母和数字位置,因此您可能需要使用一些文本检测方法,例如这里

4-现在您所需要做的就是从图像中删除非文本区域和不好的轮廓。

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

当然,这些是完成此任务的最佳步骤,您可以使用其中的一些步骤,这可能已经足够。

其他想法:

  • 您可以使用不同的方法来完成此操作,最好的方法是找到一种方法来检测数字和字符的位置,例如模板匹配或基于特征的方法如HOG。

  • 您可以先对图像进行二值化并获得二进制图像,然后需要使用线性结构进行开运算以水平和垂直方向分割图像,在此之后可以检测出边缘并对图像进行分割,然后进行OCR。

  • 在检测出图像中所有轮廓之后,您还可以使用霍夫变换来检测任何类型的线条和定义的曲线,如one,通过这种方式可以检测到被连线的字符,然后可以对图像进行分割并进行OCR。

更简单的方法:

1- 进行二值化 enter image description here

2- 对轮廓进行一些形态学操作以进行分割:

enter image description here

3- 反转图像颜色(可能在步骤2之前完成)

enter image description here

4- 在图像中找到所有轮廓

enter image description here

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

enter image description here

注意:您可以使用文本检测方法(或使用HOG或边缘检测)代替步骤4和5。
6- 查找包含图像中所有剩余轮廓的大矩形。

enter image description here

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


1
这张图片能帮到你吗?

enter image description here

这张图片的算法很容易实现。我相信,如果你调整一些参数,你可以得到这种图片的非常好的结果。

我用tesseract测试了所有的图片:

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

你尝试过在去除边缘的连通组件后使用tesseract吗?由于在您处理的图像中,边缘处的连通组件与文本根本没有连接,因此去除这些可能会得到更好的结果。 - HelloWorld123456789
你说得对!如果移除那些连接的结构,肯定会获得更好的结果。在发布那张图片时,我并不知道这个事实。我以为tesseract足够强大,可以自己完成这项工作,只需删除数字之间的噪声和其他伪影即可。我将开发一个扩展算法,保持简单但摆脱边框结构。干杯! - MarkusAtCvlabDotDe
此外,你能否将你的算法添加到答案中? - HelloWorld123456789
1
Tesseract 很棘手。尝试运行 tesseract -psm 7 yourimage.png digits,这将强制 tesseract 仅识别数字。请问您能否发布您减小上述图像的方法? - Zy0n
当然,我会发布代码。我目前只有理论上的想法,稍后会实现并发布它。此外,如果我们能够解决图像中随机分布的更大结构(不仅连接到边界)的问题,那将是很有趣的。 - MarkusAtCvlabDotDe

0

稍微有点跳出固定思维的想法:

我从你原始图片中看到,它是一个相当严格预格式化的文档,看起来像是路税徽章或类似的东西,对吧?

如果以上假设是正确的,那么您可以实现一种不太通用的解决方案:您试图摆脱的噪声是由特定的文档模板特征引起的,它出现在您的图片特定且已知的区域。实际上,文字也是如此。

在这种情况下,一种方法就是定义您知道存在这种“噪音”的区域的边界,然后将它们变成白色。

然后,按照您已经采取的步骤继续进行:进行去噪,以消除最细微的细节(即徽章中看起来像安全水印或全息图案的背景图案)。结果应该足够清晰,Tesseract可以轻松处理。

只是一个想法。并非通用解决方案,我认识到了这一点,因此取决于您的实际要求。


0

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接