9

私は.Netで画像をデスキューするための信頼できる方法を探し求めてきましたが、あまり運がありません。

その瞬間、私はAforgeを使用しています。これは、WPFで作業しているときに苦痛です。したがって、作業している画像は、BitmapオブジェクトではなくBitmapImageオブジェクトです。つまり、BitmapImageオブジェクトから始めて、これをメモリストリームに保存し、新しいBitmapオブジェクトを作成する必要があります。メモリストリームから、デスキュープロセスを実行し、デスキューされたイメージを新しいメモリストリームに保存してから、そのメモリストリームから新しいBitmapImageオブジェクトを作成します。それだけでなく、デスキューイングも素晴らしいものではありません。

スキャナーにスキャンされた一枚の紙のOMRデータを読み取ろうとしているので、毎回同じ座標にある特定のOMRボックスに依存する必要があります。そのため、デスキューは信頼できるものである必要があります。

そのため、私はその瞬間にAforgeを使用していますが、.Netで画像デスキュー用の他のフリー/オープンソースライブラリを見つけることができません。見つけたものはすべて、適切に高価であるか、C /C++です。

私の質問は、.Netでの画像デスキューを支援する他のフリー/オープンソースライブラリが存在するかどうかです。もしそうなら、彼らは何と呼ばれていますか、そうでなければ、私はこの問題にどのように取り組むべきですか?

編集:たとえば、次のページがあるとします。

初期画像

注:これは説明のみを目的としていますが、実際の画像では、ページの各隅に黒い長方形があります。これが役立つ場合があります。

これを印刷してスキャナーにスキャンバックすると、次のようになります。

スキャンした画像

ボックスが毎回同じ場所にあるように、この画像をデスキューする必要があります。現実の世界では、箱がたくさんあり、それらは小さくて互いに接近しているので、精度が重要です。

これに対する私の現在の方法は、大規模で効果のないお尻の痛みです。

using AForge.Imaging;
using AForge.Imaging.Filters;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Media.Imaging;

public static BitmapImage DeskewBitmap(BitmapImage skewedBitmap)
{
    //Using a memory stream to minimise disk IO
    var memoryStream = BitmapImageToMemoryStream(skewedBitmap);

    var bitmap = MemoryStreamToBitmap(memoryStream);
    var skewAngle = CalculateSkewAngle(bitmap);

    //Aforge needs a Bppp indexed image for the deskewing process
    var bitmapConvertedToBbppIndexed = ConvertBitmapToBbppIndexed(bitmap);

    var rotatedImage = DeskewBitmap(skewAngle, bitmapConvertedToBbppIndexed);

    //I need to convert the image back to a non indexed format to put it back into a BitmapImage object
    var imageConvertedToNonIndexed = ConvertImageToNonIndexed(rotatedImage);

    var imageAsMemoryStream = BitmapToMemoryStream(imageConvertedToNonIndexed);
    var memoryStreamAsBitmapImage = MemoryStreamToBitmapImage(imageAsMemoryStream);

    return memoryStreamAsBitmapImage;
}

private static Bitmap ConvertImageToNonIndexed(Bitmap rotatedImage)
{
    var imageConvertedToNonIndexed = rotatedImage.Clone(
        new Rectangle(0, 0, rotatedImage.Width, rotatedImage.Height), PixelFormat.Format32bppArgb);
    return imageConvertedToNonIndexed;
}

private static Bitmap DeskewBitmap(double skewAngle, Bitmap bitmapConvertedToBbppIndexed)
{
    var rotationFilter = new RotateBilinear(-skewAngle) { FillColor = Color.White };

    var rotatedImage = rotationFilter.Apply(bitmapConvertedToBbppIndexed);
    return rotatedImage;
}

private static double CalculateSkewAngle(Bitmap bitmapConvertedToBbppIndexed)
{
    var documentSkewChecker = new DocumentSkewChecker();

    double skewAngle = documentSkewChecker.GetSkewAngle(bitmapConvertedToBbppIndexed);

    return skewAngle;
}

private static Bitmap ConvertBitmapToBbppIndexed(Bitmap bitmap)
{
    var bitmapConvertedToBbppIndexed = bitmap.Clone(
        new Rectangle(0, 0, bitmap.Width, bitmap.Height), PixelFormat.Format8bppIndexed);
    return bitmapConvertedToBbppIndexed;
}

private static BitmapImage ResizeBitmap(BitmapImage originalBitmap, int desiredWidth, int desiredHeight)
{
    var ms = BitmapImageToMemoryStream(originalBitmap);
    ms.Position = 0;

    var result = new BitmapImage();
    result.BeginInit();
    result.DecodePixelHeight = desiredHeight;
    result.DecodePixelWidth = desiredWidth;

    result.StreamSource = ms;
    result.CacheOption = BitmapCacheOption.OnLoad;

    result.EndInit();
    result.Freeze();

    return result;
}

private static MemoryStream BitmapImageToMemoryStream(BitmapImage image)
{
    var ms = new MemoryStream();

    var encoder = new JpegBitmapEncoder();
    encoder.Frames.Add(BitmapFrame.Create(image));

    encoder.Save(ms);

    return ms;
}

private static BitmapImage MemoryStreamToBitmapImage(MemoryStream ms)
{
    ms.Position = 0;
    var bitmap = new BitmapImage();

    bitmap.BeginInit();

    bitmap.StreamSource = ms;
    bitmap.CacheOption = BitmapCacheOption.OnLoad;

    bitmap.EndInit();
    bitmap.Freeze();

    return bitmap;
}

private static Bitmap MemoryStreamToBitmap(MemoryStream ms)
{
    return new Bitmap(ms);
}

private static MemoryStream BitmapToMemoryStream(Bitmap image)
{
    var memoryStream = new MemoryStream();
    image.Save(memoryStream, ImageFormat.Bmp);

    return memoryStream;
}

振り返ってみると、さらにいくつかの質問があります。

  1. AForgeを正しく使用していますか?
  2. AForgeはこのタスクに使用するのに最適なライブラリですか?
  3. より正確な結果を得るために、これに対する私の現在のアプローチをどのように改善できますか?
4

4 に答える 4

7

サンプル入力を考えると、画像の傾き補正を行っていないことは明らかです。この種の操作では歪みは修正されません。代わりに、透視変換を実行する必要があります。これは、次の図で明確に確認できます。4 つの白い長方形は 4 つのブラック ボックスのエッジを表し、黄色の線はブラック ボックスを接続した結果です。黄色の四角形は、歪んだ赤い四角形 (達成したいもの) ではありません。

ここに画像の説明を入力

したがって、実際に上の図を取得できれば、問題ははるかに簡単になります。4 つのコーナー ボックスがない場合は、他の 4 つの基準点が必要になるため、これらは非常に役立ちます。上の画像を取得した後、4 つの黄色い角がわかったので、それらを 4 つの赤い角にマッピングするだけです。これは、実行する必要がある透視変換です。ライブラリによると、そのための準備ができている関数がある可能性があります (少なくとも 1 つあり、質問へのコメントを確認してください)。

上の画像にたどり着くには複数の方法があるので、比較的簡単な方法を簡単に説明します。まず、グレースケール イメージを 2 値化します。そのために、単純なグローバルしきい値 100 (画像は [0, 255] の範囲内) を選択しました。これにより、画像内のボックスやその他の詳細 (画像の周りの強い線など) が維持されます。100 以上の強度は 255 に設定され、100 未満の強度は 0 に設定されます。ただし、これは印刷されたイメージであるため、ボックスの表示の暗さはさまざまである可​​能性が非常に高くなります。したがって、ここではより良い方法が必要になる可能性があります。形態学的勾配のような単純なものがよりうまく機能する可能性があります。2 番目のステップは、無関係な詳細を削除することです。これを行うには、7x7 の正方形 (入力画像の幅と高さの間の最小値の約 1%) でモルフォロジー クロージングを実行します。ボックスの境界を取得するには、current_image - erosion(current_image)基本的な 3x3 の正方形を使用します。これで、上記の 4 つの白い輪郭を持つ画像ができました (これは、ボックス以外のすべてが削除されたと仮定しており、他の入力を単純化したものだと思います)。これらの白い輪郭のピクセルを取得するには、連結成分のラベル付けを行うことができます。これらの 4 つのコンポーネントで、右上、左上、右下、左下を決定します。これで、黄色の長方形の角を取得するために必要な点を簡単に見つけることができます。これらの操作はすべて AForge ですぐに利用できるため、次のコードを C# に変換するだけです。

import sys
import numpy
from PIL import Image, ImageOps, ImageDraw
from scipy.ndimage import morphology, label

# Read input image and convert to grayscale (if it is not yet).
orig = Image.open(sys.argv[1])
img = ImageOps.grayscale(orig)

# Convert PIL image to numpy array (minor implementation detail).
im = numpy.array(img)

# Binarize.
im[im < 100] = 0
im[im >= 100] = 255

# Eliminate undesidered details.
im = morphology.grey_closing(im, (7, 7))

# Border of boxes.
im = im - morphology.grey_erosion(im, (3, 3))

# Find the boxes by labeling them as connected components.
lbl, amount = label(im)
box = []
for i in range(1, amount + 1):
    py, px = numpy.nonzero(lbl == i) # Points in this connected component.
    # Corners of the boxes.
    box.append((px.min(), px.max(), py.min(), py.max()))
box = sorted(box)
# Now the first two elements in the box list contains the
# two left-most boxes, and the other two are the right-most
# boxes. It remains to stablish which ones are at top,
# and which at bottom.
top = []
bottom = []
for index in [0, 2]:
    if box[index][2] > box[index+1][2]:
        top.append(box[index + 1])
        bottom.append(box[index])
    else:
        top.append(box[index])
        bottom.append(box[index + 1])

# Pick the top left corner, top right corner,
# bottom right corner, and bottom left corner.
reference_corners = [
        (top[0][0], top[0][2]), (top[1][1], top[1][2]),
        (bottom[1][1], bottom[1][3]), (bottom[0][0], bottom[0][3])]

# Convert the image back to PIL (minor implementation detail).
img = Image.fromarray(im)
# Draw lines connecting the reference_corners for visualization purposes.
visual = img.convert('RGB')
draw = ImageDraw.Draw(visual)
draw.line(reference_corners + [reference_corners[0]], fill='yellow')
visual.save(sys.argv[2])

# Map the current quadrilateral to an axis-aligned rectangle.
min_x = min(x for x, y in reference_corners)
max_x = max(x for x, y in reference_corners)
min_y = min(y for x, y in reference_corners)
max_y = max(y for x, y in reference_corners)

# The red rectangle.
perfect_rect = [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]

# Use these points to do the perspective transform.
print reference_corners
print perfect_rect

上記のコードと入力画像の最終的な出力は次のとおりです。

[(55, 30), (734, 26), (747, 1045), (41, 1036)]
[(41, 26), (747, 26), (747, 1045), (41, 1045)]

ポイントの最初のリストは黄色の四角形の 4 つの角を表しており、2 番目のポイントは赤い四角形に関連しています。透視変換を行うには、AForge と ready 関数を使用できます。簡単にするために、次のように ImageMagick を使用しました。

convert input.png -distort Perspective "55,30,41,26 734,26,747,26 747,1045,747,1045 41,1036,41,1045" result.png

これにより、後の配置が得られます(結果をよりよく示すために、以前と同じように青い線が見つかりました):

ここに画像の説明を入力

左の青い垂直線が完全にまっすぐではないことに気付くかもしれません。実際、左端の 2 つのボックスは x 軸で 1 ピクセルずれています。これは、透視変換中に使用される別の補間によって修正される場合があります。

于 2013-01-07T01:44:07.447 に答える
1

ジョン、私はテンプレートマッチングもこれを解決するのに役立つかもしれないと思っています(Leptonicaライブラリが十分でない場合)。

Aforge.netにはテンプレートマッチングが組み込まれています:http: //www.aforgenet.com/framework/docs/html/17494328-ef0c-dc83-1bc3-907b7b75039f.htm

これに関する私の限られた知識では、トリミング/登録マークのソース画像があり、スキャンされた画像のテンプレートマッチングを使用してそれを見つけることができます。次に、画像をトリミングして、レジストレーションマーク内の部分だけのサブ画像を取得できます。上で提供した画像の場合、初期スキューがかなり小さいと想定し、画像のトリミングされた領域でのみテンプレートマッチングを実行して、合計時間を短縮できると思います。

ここでこれについていくつかの議論があります: 画像内の位置合わせマークを見つける方法

于 2013-01-06T21:59:28.937 に答える
1

John the Leptonica ライブラリは、非常に高速で安定していることを意図しています。これは、c# http://www.leptonica.com/vs2008doc/csharp-and-leptonlib.html
から呼び出す方法に関するリンクです。これが答えかどうかわからないので、コメントとして追加しました。

実際に白黒画像をデスキューするための LeptonicaCLR.Utils.DeskewBinaryImage() があります。

あなたが処理しようとしている実際のフォームでそれがどれほど良いかはわかりません。

于 2013-01-06T20:11:34.177 に答える