【问题标题】:Efficient ways to determine tilt of an image确定图像倾斜的有效方法
【发布时间】:2010-11-29 07:33:17
【问题描述】:

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

图像具有以下属性:

  • 由浅色背景上的深色文本组成
  • 偶尔包含仅以 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);
}

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

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

【问题讨论】:

  • 我喜欢代码如何将snake_case、camelCase 和PascalCase 混合在一个简短的代码块中。显然我写的 F# 太多了。
  • 你为什么使用decimal?它不会为计算斜率增加太多精度,并且无论如何在将其传递给atan 方法时,您必须将其转换回double。
  • @Cecil:将一堆双精度数相加和相除会导致精度问题。预先处理小数,然后在最后转换为双精度似乎可以轻松完成。
  • Google 和 Google Scholar 为“文档倾斜角度”提供了无数点击(感谢您提供的关键字,底座)。你看过那里的算法创意吗?
  • 这里很棒。谢谢。与其只选择下一个白色像素,不如选择离其区域相邻黑色像素最远的下一个白色像素呢? font_height 大小的区域应该工作得很好。与内核的卷积可以很快地做到这一点。

标签: c# image-processing


【解决方案1】:

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

我做了以下改进:

代码

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的斜率,导致代码低估了图像的实际倾斜度。

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

【讨论】:

  • 如果您想知道,出于精确的原因,我以一种奇怪的方式混合了小数和双精度数。使用双精度计算线性回归时,我不断得到 NaN,但使用小数可以正常工作。
【解决方案2】:

GetPixel 很慢。您可以使用here 列出的方法将速度提高一个数量级。

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 如果你真的走这条路,我会选择大约 10 - 20 个随机样本点,然后丢弃统计异常(文本行之间的样本)。然后剩下的样本应该画一条相当直线,你可以用它们来计算斜率。
    • 基于有限的实验,通过选择随机样本点与对所有点进行抽样,我没有得到更好的结果。图像中的空白区域(例如分隔段落的空间)以接近零的斜率进行采样。由于随机采样将选择频率与整个图像中“平坦”路径总数成比例的“平坦”路径,因此我没有得到更好的近似值,只有一个不确定的近似值。但是,我确实发现在平均值的标准差内对所有路径进行平均可以得到更好的整体平均值。
    【解决方案4】:

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

    但从性能的角度来看,您可以进行许多优化,这些优化可能会加起来。

    也就是说,我会从你的内部循环中更改这个 sn-p:

    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 替代方案,以进一步提高速度。

    【讨论】:

    • 因为 IsBlack == !IsWhite,IsBlack 的返回值可以类似地被缓存并用于两个 if 语句。
    【解决方案5】:

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

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

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

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

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

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

    【讨论】:

      【解决方案6】:

      测量每条线的角度似乎有点过头了,尤其是考虑到 GetPixel 的性能。

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

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

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

      【讨论】:

        【解决方案7】:

        您的时间限制是什么?

        霍夫变换是确定图像倾斜角度的一种非常有效的机制。它的时间成本可能很高,但如果你要使用高斯模糊,你已经在消耗大量的 CPU 时间了。还有其他加速 Hough 变换的方法,包括创造性的图像采样。

        【讨论】:

          【解决方案8】:

          您的最新输出让我有些困惑。 当您在源图像上叠加蓝线时,您是否将其偏移了一点?看起来蓝线在文本中心上方大约 5 个像素处。

          不确定该偏移量,但您肯定会遇到派生线以错误的角度“漂移”的问题。它似乎对产生水平线有太强的偏见。

          我想知道是否将遮罩窗口从 3 像素(中心,上方 1,下方 1)增加到 5 可能会改善这一点(上方 2,下方 2)。如果您按照richardtallent 的建议将图像重新采样得更小,您也会得到这种效果。

          【讨论】:

            【解决方案9】:

            非常酷的寻路应用程序。 我想知道这种其他方法是否会帮助或伤害您的特定数据集。

            假设一个黑白图像:

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

            我想如果事实上你必须考虑一个真正的 -45 -> +45 度的倾斜,这将不会很好。如果实际数字更小(?+/- 10 度),这可能是一个很好的策略。获得初步结果后,您可以考虑以较小的度数增量重新运行以微调答案。因此,我可能会尝试使用接受 float degree_tick 作为参数的函数来编写此代码,这样我就可以使用相同的代码同时运行粗通道和细通道(或粗度或细度范围)。

            这可能在计算上很昂贵。要进行优化,您可以考虑只选择图像的一部分进行 project-test-rotate-repeat 。

            【讨论】:

              猜你喜欢
              • 2019-12-04
              • 1970-01-01
              • 1970-01-01
              • 2020-04-12
              • 2013-07-19
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多