我一直在寻找一种可靠的方法来对.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;
}

回想起来,还有两个问题:
  • 我可以正确使用AForge吗?
  • AForge是用于此任务的最佳库吗?
  • 如何改进当前的方法以获得更准确的结果?
  • 最佳答案

    在给出样本输入的情况下,很明显,您不需要进行图像去歪斜。这种操作不会纠正您的失真,而是需要执行透视变换。在下图中可以清楚地看到这一点。四个白色矩形代表四个黑盒的边缘,黄线是连接黑盒的结果。黄色的四边形不是偏斜的红色(您要获得的那个)。

    因此,如果您实际上可以得到上面的数字,则问题会变得简单得多。如果没有四个角框,则需要其他四个引用点,因此它们确实对您有很大帮助。得到上面的图像后,您知道四个黄色角,然后将它们映射到四个红色角。这是您需要执行的透视变换,并且根据您的库可能有一个准备好的函数(至少有一个,请检查对您问题的评论)。

    有多种获取上图的方法,因此我将简单描述一个相对简单的方法。首先,对您的灰度图像进行二值化处理。为此,我选择了一个简单的全局阈值100(您的图像在[0,255]范围内),该阈值将框和其他细节保留在图像中(例如图像周围的粗线)。高于或等于100的强度设置为255,低于100的强度设置为0。但是,由于这是打印图像,因此框出现的暗度很可能会发生变化。因此,您可能需要一个更好的方法,像形态梯度这样简单的方法可能会更好地工作。第二步是消除无关的细节。为此,请使用7x7的正方形(大约是输入图像的宽度和高度之间的最小值的1%)执行形态学闭合。要获得盒子的边界,请使用current_image - erosion(current_image)中使用基本3x3正方形的形态学腐 eclipse 方法。现在,您有了一个具有上述四个白色轮廓的图像(这是假定除盒子外的所有东西都被消除了,我认为是其他输入的简化)。要获取这些白色轮廓的像素,可以进行连接的组件标签。使用这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个像素。这可以通过在透视变换期间使用的不同插值来校正。

    关于.net - 使用.Net校正图像,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/14156498/

    10-12 12:37
    查看更多