确定图像倾斜的有效方法

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

图像具有以下属性:

  • 包括在明亮的背景上的黑暗文本
  • 偶尔包含仅以90度角相交的水平或垂直线。
  • 偏斜在-45到45度之间。
  • 将此图像视为参考(其倾斜度为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); } 

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

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

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

我做了以下改进:

  • 使用Vinko的建议 ,我避免使用GetPixel直接使用字节,现在代码以我需要的速度运行。

  • 我的原始代码只使用了“IsBlack”和“IsWhite”,但这还不够精细。 原始代码在图像中跟踪以下路径:

    http://img43.imageshack.us/img43/1545/tilted3degtextoriginalw.gif

    请注意,许多路径都通过文本。 通过将我的中心,上方和下方路径与实际亮度值进行比较并选择最亮的像素。 基本上我将位图视为高度图,从左到右的路径遵循图像的轮廓,从而产生更好的路径:

    http://img10.imageshack.us/img10/5807/tilted3degtextbrightnes.gif

    正如Toaomalkster所建议的那样 ,高斯模糊平滑了高度图,我得到了更好的结果:

    http://img197.imageshack.us/img197/742/tilted3degtextblurredwi.gif

    由于这只是原型代码,我使用GIMP模糊了图像,我没有编写自己的模糊function。

    选择的路径非常适合贪婪的算法。

  • 正如Toaomalkster建议的那样 ,选择最小/最大斜率是天真的。 简单的线性回归可以更好地逼近路径的斜率。 另外,一旦我离开图像的边缘,我应该缩短路径,否则路径将拥抱图像的顶部并给出不正确的斜率。

 private double ToDegrees(double slope) { return (180.0 / Math.PI) * Math.Atan(slope); } private double GetSkew(Bitmap image) { BrightnessWrapper wrapper = new BrightnessWrapper(image); LinkedList slopes = new LinkedList(); 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,导致代码低估图像的实际倾斜度。

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

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

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

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

但从性能角度来看,您可以进行一些优化,这些优化可能会增加。

也就是说,我将从内循环中更改此片段:

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

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

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

另一种经常帮助图像的方法是首先模糊它们。 如果您足够模糊示例图像,则每行文本最终将变为模糊的平滑线。 然后,您应用某种算法来基本上做回归分析。 有很多方法可以做到这一点,网上有很多例子。

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

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

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

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

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

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

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

你对时间的限制是什么?

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

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

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

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

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

假设黑白图像:

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

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

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