确定图像倾斜的有效方法

时间:2009-09-17 21:42:25

标签: c# image-processing

我正在尝试编写一个程序来以编程方式确定任意图像中的倾斜或旋转角度。

图片具有以下属性:

  • 由浅色背景上的深色文字组成
  • 偶尔包含仅以90度角相交的水平或垂直线。
  • 在-45到45度之间倾斜。
  • 请参阅this image作为参考(它已倾斜2.8度)。

到目前为止,我已经提出了这个策略:从左到右画一条路线,总是选择最近的白色像素。据推测,从左到右的路线更倾向于沿着图像倾斜的文本行之间的路径。

这是我的代码:

private bool IsWhite(Color c) { return c.GetBrightness() >= 0.5 || c == Color.Transparent; }

private bool IsBlack(Color c) { return !IsWhite(c); }

private double ToDegrees(decimal slope) { return (180.0 / Math.PI) * Math.Atan(Convert.ToDouble(slope)); }

private void GetSkew(Bitmap image, out double minSkew, out double maxSkew)
{
    decimal minSlope = 0.0M;
    decimal maxSlope = 0.0M;
    for (int start_y = 0; start_y < image.Height; start_y++)
    {
        int end_y = start_y;
        for (int x = 1; x < image.Width; x++)
        {
            int above_y = Math.Max(end_y - 1, 0);
            int below_y = Math.Min(end_y + 1, image.Height - 1);

            Color center = image.GetPixel(x, end_y);
            Color above = image.GetPixel(x, above_y);
            Color below = image.GetPixel(x, below_y);

            if (IsWhite(center)) { /* no change to end_y */ }
            else if (IsWhite(above) && IsBlack(below)) { end_y = above_y; }
            else if (IsBlack(above) && IsWhite(below)) { end_y = below_y; }
        }

        decimal slope = (Convert.ToDecimal(start_y) - Convert.ToDecimal(end_y)) / Convert.ToDecimal(image.Width);
        minSlope = Math.Min(minSlope, slope);
        maxSlope = Math.Max(maxSlope, slope);
    }

    minSkew = ToDegrees(minSlope);
    maxSkew = ToDegrees(maxSlope);
}

这在某些图像上运行良好,在其他图像上效果不佳,而且速度慢。

是否有更有效,更可靠的方法来确定图像的倾斜度?

9 个答案:

答案 0 :(得分:6)

我对我的代码进行了一些修改,它确实运行得更快,但不是很准确。

我做了以下改进:

<强>代码

private double ToDegrees(double slope) { return (180.0 / Math.PI) * Math.Atan(slope); }

private double GetSkew(Bitmap image)
{
    BrightnessWrapper wrapper = new BrightnessWrapper(image);

    LinkedList<double> slopes = new LinkedList<double>();

    for (int y = 0; y < wrapper.Height; y++)
    {
        int endY = y;

        long sumOfX = 0;
        long sumOfY = y;
        long sumOfXY = 0;
        long sumOfXX = 0;
        int itemsInSet = 1;
        for (int x = 1; x < wrapper.Width; x++)
        {
            int aboveY = endY - 1;
            int belowY = endY + 1;

            if (aboveY < 0 || belowY >= wrapper.Height)
            {
                break;
            }

            int center = wrapper.GetBrightness(x, endY);
            int above = wrapper.GetBrightness(x, aboveY);
            int below = wrapper.GetBrightness(x, belowY);

            if (center >= above && center >= below) { /* no change to endY */ }
            else if (above >= center && above >= below) { endY = aboveY; }
            else if (below >= center && below >= above) { endY = belowY; }

            itemsInSet++;
            sumOfX += x;
            sumOfY += endY;
            sumOfXX += (x * x);
            sumOfXY += (x * endY);
        }

        // least squares slope = (NΣ(XY) - (ΣX)(ΣY)) / (NΣ(X^2) - (ΣX)^2), where N = elements in set
        if (itemsInSet > image.Width / 2) // path covers at least half of the image
        {
            decimal sumOfX_d = Convert.ToDecimal(sumOfX);
            decimal sumOfY_d = Convert.ToDecimal(sumOfY);
            decimal sumOfXY_d = Convert.ToDecimal(sumOfXY);
            decimal sumOfXX_d = Convert.ToDecimal(sumOfXX);
            decimal itemsInSet_d = Convert.ToDecimal(itemsInSet);
            decimal slope =
                ((itemsInSet_d * sumOfXY) - (sumOfX_d * sumOfY_d))
                /
                ((itemsInSet_d * sumOfXX_d) - (sumOfX_d * sumOfX_d));

            slopes.AddLast(Convert.ToDouble(slope));
        }
    }

    double mean = slopes.Average();
    double sumOfSquares = slopes.Sum(d => Math.Pow(d - mean, 2));
    double stddev = Math.Sqrt(sumOfSquares / (slopes.Count - 1));

    // select items within 1 standard deviation of the mean
    var testSample = slopes.Where(x => Math.Abs(x - mean) <= stddev);

    return ToDegrees(testSample.Average());
}

class BrightnessWrapper
{
    byte[] rgbValues;
    int stride;
    public int Height { get; private set; }
    public int Width { get; private set; }

    public BrightnessWrapper(Bitmap bmp)
    {
        Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);

        System.Drawing.Imaging.BitmapData bmpData =
            bmp.LockBits(rect,
                System.Drawing.Imaging.ImageLockMode.ReadOnly,
                bmp.PixelFormat);

        IntPtr ptr = bmpData.Scan0;

        int bytes = bmpData.Stride * bmp.Height;
        this.rgbValues = new byte[bytes];

        System.Runtime.InteropServices.Marshal.Copy(ptr,
                       rgbValues, 0, bytes);

        this.Height = bmp.Height;
        this.Width = bmp.Width;
        this.stride = bmpData.Stride;
    }

    public int GetBrightness(int x, int y)
    {
        int position = (y * this.stride) + (x * 3);
        int b = rgbValues[position];
        int g = rgbValues[position + 1];
        int r = rgbValues[position + 2];
        return (r + r + b + g + g + g) / 6;
    }
}

代码,但不是。大量的空白会导致程序绘制相对平坦的线条,导致斜率接近0,导致代码低估图像的实际倾斜度。

通过选择随机采样点与采样所有点,倾斜精度没有明显差异,因为通过随机采样选择的“平坦”路径的比率与整个“平坦”路径的比率相同图像。

答案 1 :(得分:5)

GetPixel很慢。您可以使用列出的here方法获得一个数量级的加速。

答案 2 :(得分:3)

如果文本左(右)对齐,您可以通过测量图像的左(右)边缘与两个随机位置中的第一个暗像素之间的距离来确定斜率,并从中计算斜率。额外的测量可以减少误差,同时需要额外的时间。

答案 3 :(得分:3)

首先,我必须说我喜欢这个主意。但我以前从来没有这么做过,我不确定是什么建议来提高可靠性。我能想到的第一件事是抛弃统计异常的想法。如果斜率突然急剧变化,那么你知道你已经发现图像的白色部分倾向于边缘倾斜(没有双关语)你的结果。所以你想以某种方式抛出那些东西。

但从性能的角度来看,你可以做出一些可能加起来的优化。

即,我将从内循环中更改此片段:

Color center = image.GetPixel(x, end_y);
Color above = image.GetPixel(x, above_y);
Color below = image.GetPixel(x, below_y);

if (IsWhite(center)) { /* no change to end_y */ }
else if (IsWhite(above) && IsBlack(below)) { end_y = above_y; }
else if (IsBlack(above) && IsWhite(below)) { end_y = below_y; }

对此:

Color center = image.GetPixel(x, end_y);

if (IsWhite(center)) { /* no change to end_y */ }
else
{
    Color above = image.GetPixel(x, above_y);
    Color below = image.GetPixel(x, below_y);
    if (IsWhite(above) && IsBlack(below)) { end_y = above_y; }
    else if (IsBlack(above) && IsWhite(below)) { end_y = below_y; }
}

效果相同,但应大幅减少对GetPixel的调用次数。

还要考虑在疯狂开始之前将未更改的值放入变量中。像image.Height和image.Width这样的东西每次调用它们都会有轻微的开销。因此,在循环开始之前将这些值存储在您自己的变量中。在处理嵌套循环时,我总是告诉自己的事情是优化内部循环中的所有内容而牺牲其他所有内容。

另外......正如Vinko Vrsalovic建议的那样,你可以看看他的GetPixel替代方案,以提高速度。

答案 4 :(得分:2)

乍一看,您的代码看起来过于幼稚。 这就解释了为什么它并不总是有效。

我喜欢Steve Wortham建议的方法, 但如果你有背景图片,它可能会遇到问题。

通常有助于图像的另一种方法是首先模糊它们。 如果您足够模糊示例图像,则每行文本都将结束 模糊的线条。然后,您应用某种算法 基本上做回归分析。有很多方法可以做 网上有很多例子。

边缘检测可能有用,或者它可能会导致更多值得的问题。

顺便说一句,如果你足够搜索代码,可以非常有效地实现高斯模糊。否则,我确信有很多库可用。 最近没有做太多,所以手头没有任何链接。 但搜索图像处理库可以获得良好的效果。

我假设你正在享受解决这个问题的乐趣,所以在这里的实际实现并不多见。

答案 5 :(得分:1)

测量每条线的角度似乎有些过分,特别是考虑到GetPixel的性能。

我想知道你是否会通过在左上角或右上角(取决于倾斜方向)寻找白色三角形并测量斜边的角度来获得更好的表现。所有文本都应该在页面上遵循相同的角度,并且页面的左上角不会被上面的内容的下降或空格欺骗。

要考虑的另一个提示:而不是模糊,在大大降低的分辨率下工作。这将为您提供所需的更流畅的数据和更少的GetPixel调用。

例如,我在.NET中为传真的TIFF文件做了一次空白页面检测例程,它只是将整个页面重新采样为一个像素,并测试该值为白色的阈值。

答案 6 :(得分:1)

你的时间限制是什么?

霍夫变换是一种非常有效的机制,用于确定图像的倾斜角度。它可能会花费很多时间,但如果你要使用高斯模糊,你已经烧掉了一堆CPU时间。还有其他方法可以加速霍夫变换,包括创意图像采样。

答案 7 :(得分:0)

您的最新输出让我感到困惑。 当您在源图像上叠加蓝线时,您是否稍微偏移了它?看起来蓝线距文本中心约5个像素。

不确定那个偏移量,但你确实遇到一个问题,派生线“漂移”在错误的角度。它似乎对产生水平线有太强烈的偏见。

我想知道将掩模窗口从3个像素(中心,上面一个,下面一个)增加到5可能会改善这个(上面两个,下面两个)。如果你遵循richardtallent的建议并将图像重新采样得更小,你也会得到这种效果。

答案 8 :(得分:0)

非常酷的路径查找应用程序。 我想知道这种其他方法是否会对您的特定数据集产生帮助或影响。

假设黑白图像:

  • 将所有黑色像素投影到右侧(EAST)。这应该给出一个大小为IMAGE_HEIGHT的一维数组的结果。调用阵列CANVAS。
  • 在投影所有像素EAST时,请以数字方式跟踪投影到CANVAS每个区域的像素数。
  • 将图像旋转任意度数并重新投影。
  • 选择为CANVAS中的值提供最高峰和最低谷的结果。

我想这不会很好,如果事实上你必须考虑到真正的-45 - &gt; +45度倾斜。如果实际数字较小(?+/- 10度),这可能是一个非常好的策略。一旦获得初始结果,您可以考虑使用较小的度数增量重新运行以微调答案。因此,我可能尝试用一个函数来写这个函数,该函数接受一个浮点数_tick作为参数,所以我可以使用相同的代码运行粗调和细调(或粗糙或细度的光谱)。

这可能在计算上很昂贵。要进行优化,您可以考虑仅选择图像的一部分进行投影 - 测试 - 旋转 - 重复。