web-dev-qa-db-ja.com

画像から背景ノイズを除去して、OCRのテキストをより明確にします

画像内のテキスト領域に基づいて画像をセグメント化し、適切と思われる領域を抽出するアプリケーションを作成しました。私がやろうとしているのは、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;
}

最初の画像をきれいにして、2番目の画像に似せるには、さらに何ができますか?

編集:これは、cleanImage関数を実行する前の元の画像です。

enter image description here

25
Zy0n

私の答えは、次の仮定に基づいています。あなたの場合、それらのどれもが成立しない可能性があります。

  • セグメント化された領域の境界ボックスの高さにしきい値を課すことは可能です。その後、他のコンポーネントを除外できるはずです。
  • 数字の平均ストローク幅を知っています。この情報を使用して、数字が他の地域に接続される可能性を最小限に抑えます。これには、距離変換とモルフォロジー演算を使用できます。

これは数字を抽出するための私の手順です:

  • 画像にOtsuしきい値を適用 otsu
  • 距離変換を行う dist
  • ストローク幅(= 8)制約を使用して距離変換画像のしきい値を設定 sw2

  • モルフォロジー演算を適用して切断 ws2op

  • 境界ボックスの高さをフィルタリングし、数字の位置を推測します

ストローク幅= 8 bb ストローク幅= 10 bb2

[〜#〜] edit [〜#〜]

  • 見つかった指の輪郭の凸包を使用してマスクを準備します 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);

[〜#〜] edit [〜#〜]

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
26
dhanushka

Tesseractを呼び出す前に、できる限り鮮明に画像を準備するために、前処理の部分でさらに作業する必要があると思います。

それを行うための私のアイデアは次のとおりです:

1-画像から輪郭を抽出し、画像内の輪郭を見つけます( this をチェック)および this

2-各輪郭には幅、高さ、および面積があるため、幅、高さ、およびその面積に応じて輪郭をフィルタリングできます( this および this をチェックします)。ここで輪郭解析コードの一部を使用して輪郭をフィルタリングします。さらに、テンプレート輪郭マッチングを使用して、「文字または数字」輪郭に類似しない輪郭を削除できます。

3-輪郭をフィルタリングした後、この画像の文字と数字の位置を確認できます。そのため、 here のようなテキスト検出方法を使用する必要がある場合があります。

4-非テキスト領域と画像から良くない輪郭を削除する場合に必要なすべて

5-これで、binirizationメソッドを作成できます。または、tesseractを使用して、イメージに対してbinirizationを実行し、イメージでOCRを呼び出します。

確かにこれらはこれを行うための最良の手順です。それらのいくつかを使用することができ、それで十分かもしれません。

その他のアイデア:

  • さまざまな方法でこれを行うことができますが、最良のアイデアは、テンプレートマッチングなどのさまざまな方法、またはHOGのような機能ベースを使用して、数字と文字の位置を検出する方法を見つけることです。

  • 最初に画像を二値化してバイナリ画像を取得し、次に水平および垂直に線構造の開口部を適用する必要があります。これはその後、エッジを検出し、画像でセグメンテーションを実行し、OCR 。

  • 画像内のすべての輪郭を検出した後、Hough transformationは、このような 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

注:手順4および5の代わりに、テキスト検出方法(またはHOGまたはエッジ検出)を使用できます。

6-画像内の残りのすべての輪郭を含む大きな長方形を見つける

enter image description here

7- tesseractの入力を強化するために、いくつかの追加の前処理を行ってから、今すぐOCRを呼び出すことができます。 (画像を切り取り、OCRへの入力として作成することをお勧めします[黄色の四角形を切り取り、入力として画像全体を黄色の四角形だけにしないで、結果も向上させます])

5
Hazem Abdullah

その画像は役に立ちますか?

enter image description here

その画像を生成するアルゴリズムは簡単に実装できます。いくつかのパラメーターを微調整すると、そのような画像に対して非常に良い結果が得られると確信しています。

すべての画像をtesseractでテストしました。

  • 元の画像:何も検出されない
  • 処理された画像#1:何も検出されない
  • 処理済み画像#2:12〜14(完全一致)
  • 処理した画像:y’1'2-14/j
1

フォントサイズはそれほど大きくも小さくもできません。およそ10〜12 ptの範囲(つまり、文字の高さがおよそ20から80未満)でなければなりません。画像をダウンサンプリングして、tesseractで試すことができます。また、tesseractでトレーニングされていないフォントはほとんどありません。トレーニングされたフォントでない場合、問題が発生する可能性があります。

0
Gowthaman

箱から出して少し考えてみましょう。

オリジナルの画像から、かなり厳密にフォーマットされたドキュメントであり、道路税のバッジのようなものであることがわかりますよね?

上記の仮定が正しい場合、あまり一般的ではない解決策を実装できます。除去しようとしているノイズは、特定のドキュメントテンプレートの機能によるもので、画像の特定の既知の領域で発生します。実際、テキストも同様です。

その場合、対処する方法の1つは、そのような「ノイズ」があることがわかっている領域の境界を定義し、それらを単に白抜きにすることです。

次に、既に実行している残りの手順に従います:最も細かい部分(つまり、バッジの安全透かしまたはホログラムのように見える背景パターン)を削除するノイズリダクションを実行します。結果は、Tesseractが問題なく処理するのに十分明確でなければなりません。

とにかく考えてください。一般的な解決策ではないので、実際の要件に依存することを認識しています。

0
Yannis Douros