9

我一直在寻找一种可靠的方法来在 .Net 中对图像进行歪斜校正,但运气不佳。

目前我正在使用 Aforge。这是一个痛苦,因为我正在使用 WPF,所以我正在使用的图像是 BitmapImage 对象,而不是 Bitmap 对象,这意味着我需要从 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

给定示例输入,很明显您不是在图像校正之后。这种操作不会纠正您的失真,而是需要执行透视变换。这可以在下图中清楚地看到。四个白色矩形代表四个黑框的边缘,黄线是连接黑框的结果。黄色四边形不是倾斜的红色四边形(您想要实现的那个)。

在此处输入图像描述

所以,如果你真的能得到上图,问题就简单多了。如果您没有四个角框,则需要其他四个参考点,因此它们确实对您有很大帮助。得到上图后,你就知道了四个黄色的角,然后你只需将它们映射到四个红色的角。这是您需要做的透视变换,根据您的库,可能有一个现成的函数(至少有一个,请查看对您问题的评论)。

上图有多种获取方式,我就简单介绍一个比较简单的。首先,对灰度图像进行二值化。为此,我选择了一个简单的全局阈值 100(您的图像在 [0, 255] 范围内),它保留了图像中的框和其他细节(如图像周围的粗线)。大于或等于 100 的强度设置为 255,低于 100 的强度设置为 0。但是,由于这是打印的图像,因此框的显示程度很可能会有所不同。所以你可能需要一个更好的方法,像形态梯度这样简单的方法可能会更好。第二步是消除不相关的细节。为此,请使用 7x7 正方形(大约是输入图像的宽度和高度之间最小值的 1%)执行形态闭合。要获得框的边框,current_image - erosion(current_image)使用一个基本的 3x3 正方形。现在你有一张上面有四个白色轮廓的图像(这是假设除了盒子之外的所有东西都被消除了,我相信这是对其他输入的简化)。要获得这些白色轮廓的像素,您可以进行连接组件标记。通过这 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)]

第一个点列表描述了黄色矩形的四个角,第二个与红色矩形相关。要进行透视变换,可以使用 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

这给出了您所追求的对齐方式(与以前一样找到蓝线以更好地显示结果):

在此处输入图像描述

您可能会注意到左边的垂直蓝线不是完全笔直的,实际上最左边的两个框在 x 轴上未对齐 1 个像素。这可以通过在透视变换期间使用的不同插值来校正。

于 2013-01-07T01:44:07.447 回答
1

John Leptonica 库旨在非常快速和稳定。
这是有关如何从 c# http://www.leptonica.com/vs2008doc/csharp-and-leptonlib.html调用它的链接。我不确定这是否是答案,所以我只是添加了评论。

它有一个 LeptonicaCLR.Utils.DeskewBinaryImage() 来实际去歪斜黑白图像。

我不确定您尝试处理的实际表格会有多好。

于 2013-01-06T20:11:34.177 回答
1

约翰,我也认为模板匹配也可能有助于解决这个问题(如果 Leptonica 库不够好)。

Aforge.net 内置了模板匹配: http ://www.aforgenet.com/framework/docs/html/17494328-ef0c-dc83-1bc3-907b7b75039f.htm

在我对此的有限了解中,您将拥有裁剪/注册标记的源图像,并使用扫描图像中的模板匹配找到它。然后,您可以裁剪图像以获取仅包含注册标记内的部分的子图像。对于您在上面提供的图像,我认为您可以假设一个相当小的初始倾斜,并且仅在图像的裁剪区域上执行模板匹配以减少总时间。

这里有一些讨论: 如何在图像中定位对齐标记

于 2013-01-06T21:59:28.937 回答
1

对于那些最终从谷歌来到这里的人来说,使用Magick.NET库可以轻松完成纠偏。

安装其中一个 nuget 包,例如Magick.NET-Q16-AnyCPU

用法:

private byte[] DeskewImage(byte[] imageFileBytes)
{
    var img = new MagickImage(imageFileBytes);
    // ImageMagick docs say 40% should work for most images
    img.Deskew(new Percentage(40d));
    return img.ToByteArray();
}
于 2020-02-18T08:21:51.273 回答