画像内のテキスト領域に基づいて画像をセグメント化し、適切と思われる領域を抽出するアプリケーションを作成しました。私がやろうとしているのは、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;
}
最初の画像をきれいにして、2番目の画像に似せるには、さらに何ができますか?
編集:これは、cleanImage
関数を実行する前の元の画像です。
私の答えは、次の仮定に基づいています。あなたの場合、それらのどれもが成立しない可能性があります。
これは数字を抽出するための私の手順です:
[〜#〜] edit [〜#〜]
私の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
Tesseractを呼び出す前に、できる限り鮮明に画像を準備するために、前処理の部分でさらに作業する必要があると思います。
それを行うための私のアイデアは次のとおりです:
1-画像から輪郭を抽出し、画像内の輪郭を見つけます( this をチェック)および this
2-各輪郭には幅、高さ、および面積があるため、幅、高さ、およびその面積に応じて輪郭をフィルタリングできます( this および this をチェックします)。ここで輪郭解析コードの一部を使用して輪郭をフィルタリングします。さらに、テンプレート輪郭マッチングを使用して、「文字または数字」輪郭に類似しない輪郭を削除できます。
3-輪郭をフィルタリングした後、この画像の文字と数字の位置を確認できます。そのため、 here のようなテキスト検出方法を使用する必要がある場合があります。
4-非テキスト領域と画像から良くない輪郭を削除する場合に必要なすべて
5-これで、binirizationメソッドを作成できます。または、tesseractを使用して、イメージに対してbinirizationを実行し、イメージでOCRを呼び出します。
確かにこれらはこれを行うための最良の手順です。それらのいくつかを使用することができ、それで十分かもしれません。
その他のアイデア:
さまざまな方法でこれを行うことができますが、最良のアイデアは、テンプレートマッチングなどのさまざまな方法、またはHOGのような機能ベースを使用して、数字と文字の位置を検出する方法を見つけることです。
最初に画像を二値化してバイナリ画像を取得し、次に水平および垂直に線構造の開口部を適用する必要があります。これはその後、エッジを検出し、画像でセグメンテーションを実行し、OCR 。
画像内のすべての輪郭を検出した後、Hough transformation
は、このような one のようなあらゆる種類の線と定義された曲線を検出します。この方法で、線のある文字を検出できるため、画像をセグメント化し、その後OCRを実行できます。
はるかに簡単な方法:
2-等高線を分離するためのいくつかの形態操作:
3-画像の色を反転します(これは手順2の前の場合があります)
4-画像内のすべての輪郭を見つける
5-幅が高さよりも大きいすべての輪郭を削除し、非常に小さい輪郭、非常に大きい輪郭、および長方形ではない輪郭を削除します
注:手順4および5の代わりに、テキスト検出方法(またはHOGまたはエッジ検出)を使用できます。
6-画像内の残りのすべての輪郭を含む大きな長方形を見つける
7- tesseractの入力を強化するために、いくつかの追加の前処理を行ってから、今すぐOCRを呼び出すことができます。 (画像を切り取り、OCRへの入力として作成することをお勧めします[黄色の四角形を切り取り、入力として画像全体を黄色の四角形だけにしないで、結果も向上させます])
フォントサイズはそれほど大きくも小さくもできません。およそ10〜12 ptの範囲(つまり、文字の高さがおよそ20から80未満)でなければなりません。画像をダウンサンプリングして、tesseractで試すことができます。また、tesseractでトレーニングされていないフォントはほとんどありません。トレーニングされたフォントでない場合、問題が発生する可能性があります。
箱から出して少し考えてみましょう。
オリジナルの画像から、かなり厳密にフォーマットされたドキュメントであり、道路税のバッジのようなものであることがわかりますよね?
上記の仮定が正しい場合、あまり一般的ではない解決策を実装できます。除去しようとしているノイズは、特定のドキュメントテンプレートの機能によるもので、画像の特定の既知の領域で発生します。実際、テキストも同様です。
その場合、対処する方法の1つは、そのような「ノイズ」があることがわかっている領域の境界を定義し、それらを単に白抜きにすることです。
次に、既に実行している残りの手順に従います:最も細かい部分(つまり、バッジの安全透かしまたはホログラムのように見える背景パターン)を削除するノイズリダクションを実行します。結果は、Tesseractが問題なく処理するのに十分明確でなければなりません。
とにかく考えてください。一般的な解決策ではないので、実際の要件に依存することを認識しています。