C# 中的图像处理基础






3.85/5 (18投票s)
本文演示了使用 C# 进行基本图像处理算法

介绍
C# 和 Java 等语言非常易于使用,它们为应用程序层开发提供了许多功能。这意味着它们可以轻松访问外围设备,同时使 GUI(图形用户界面)开发更加容易。然而,人们常说它们不适合信号处理应用,因为开销大且优化可能性有限。在这些应用中,与 C/C++ 代码相比,代码通常运行得更慢。我们知道这是自然结果。但是,有些应用对速度要求不高。例如,教育软件、算法测试软件或与现有应用程序集成。出于这些原因,在 C# 中进行信号处理(或在本例中进行图像处理)可能是一个不错的选择。在本文中,我将不介绍复杂的图像处理算法,而是介绍如何使用简单的示例(如阈值处理、灰度转换和连通分量分析)在 C# 中高效地实现这些算法。这有点像一个入门,适合喜欢信号处理同时又想用 C# 编写测试代码以简化工作的人。
背景
阈值处理
阈值处理是将图像二值化,使得大于阈值的值为 255(字节中的最大像素值),而强度较小的像素设置为 0(黑色)。这是一项非常重要的操作,通常用于准备图像以进行矢量化、进一步分割或作为创建图纸的引导层。它可以与栅格数据图像一起使用,以区分可能用于后续分析的值范围或用作选择蒙版。有些人称之为前景和背景分离。这是 Lena 图像阈值处理的结果。

灰度转换
灰度转换是将图像的所有像素设置为不同颜色通道值加权平均值。转换通过映射完成
GRAY= (byte)(.299 * R + .587 * G + .114 * B);
连通分量分析
连通分量标记在计算机视觉中用于检测二值数字图像中的非连通区域,尽管彩色图像和更高维度的数据也可以处理。(维基百科定义)。当集成到图像识别系统或人机交互界面中时,连通分量标记可以处理各种信息。
连通分量标记通过逐像素(从上到下,从左到右)扫描图像来工作,以识别连通像素区域,即具有相同强度值集合 V
的相邻像素区域。(对于二值图像 V={1}
;然而,在灰度图像中 V
将取一系列值,例如:V={51, 52, 53, ..., 77, 78, 79, 80}
。)
指针算术
图像像素在内存中表示为一维数组。我们通过指针访问它。阅读其余代码时,应牢记指针实际上只是地址。尽管图像是二维结构,但为了方便处理和提高速度,通常将其表示为一维结构。在这种情况下,图像可以索引为
[y*ImageWidth*ChannelCount+x*ChannelCount]
由于内存的线性结构,指针用于线性地使用给定公式为图像编制索引。C/C++/C# 甚至汇编器等语言允许我们对指针执行算术运算。例如
int x[5];
int* p =&x[0]; // Get address of x
p=p+2; // This line increments the pointer by 2 memory addresses and hence
//we could access x[2]
使用代码
现在我们将逐步展示如何在 C# 中编写一个简单的图像处理例程:此代码非常简单,但对于编写图像处理例程来说并不理想。GetPixel
和 SetPixel
函数有几个缺点。首先,它们是函数。我们有访问和修改像素值的函数开销。但是,在许多情况下,我们希望通过指针操作而不是函数调用来修改图像。下一个示例利用了 C# 中的“unsafe”块。在 unsafe 块中,我们可以访问 C# 中的指针。但不要将其与你从 C/C++ 中熟悉的指针概念混淆。这些指针不直接指向非托管代码。它们指向的是中间语言。你可以在许多文章中阅读更多相关内容。它有自己的指令(当然是软指令)和自己的语言。结论是 unsafe 块中的指针不像本机指针那样快。以下是如何实现它
你在这里看到了三个重要问题
Lockbits
和UnlockBits
函数byte*
访问p[0]
操作
请注意,我们有一个从 0
到 Width*ChannelCount*Height
的循环,这是存储在内存中的总字节数。然后我们将指针增加 ptr+=3
,以便我们可以处理每个彩色像素。使用 GDI 时,应注意两个重要事实
- 像素的顺序是 BGRBGRBGRBGRBGRBGR… 而不是 RBGRBGRBGRBG… 这是微软推出的一些奇怪的事情之一。
- 图像是翻转的。在使用本文所述技术时,这并不重要,但当需要更接近本机时,这很重要。
大多数用 C# 进行图像处理的人都使用我上面描述的编码约定。不幸的是,这种风格有一些未优化的操作。例如,我们有一个从 0
到 imageSize
计数的 y
值,但在循环中未使用它。此外,指针每次都增加 3
,而 y
也增加。为了解决这个问题,我们可以继续使用进一步的指针算术。这是一个例子
private void Convert2GrayScaleFast(Bitmap bmp)
{
BitmapData bmData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
unsafe
{
byte* p = (byte*)(void*)bmData.Scan0.ToPointer();
int stopAddress = (int)p + bmData.Stride * bmData.Height;
while ((int)p != stopAddress)
{
p[0] = (byte)(.299 * p[2] + .587 * p[1] + .114 * p[0]);
p[1] = p[0];
p[2] = p[0];
p += 3;
}
}
bmp.UnlockBits(bmData);
}
在此示例中,我们将图像结构的开始和结束地址计算出来,认为它是一个一维线性数组。我们将指针从开始地址递增到结束地址,并获取中间值。如果有人测量这三种算法(慢速、已知和新)的计算时间,他们会发现它们之间存在巨大差异。如果您下载了代码,您将在消息框中看到测量时间。
连通分量标记算法
该算法是通用递归连通分量标记算法的堆实现。你可以将其用于许多应用程序并获得良好的结果。我不太确定这是最好的实现,但结果似乎令人满意。该实现非常类似于 Trajan 的强连通分量算法。然而,它已被泛化为图像。伪代码如下
Input: Graph G = (V, E), Start node v0
index = 0 // DFS node number counter
S = empty // An empty stack of nodes
tarjan(v0) // Start a DFS at the start node
procedure tarjan(v)
v.index = index // Set the depth index for v
v.lowlink = index
index = index + 1
S.push(v) // Push v on the stack
forall (v, v') in E do // Consider successors of v
if (v'.index is undefined) // Was successor v' visited?
tarjan(v') // Recurse
v.lowlink = min(v.lowlink, v'.lowlink)
elseif (v' in S) // Is v' on the stack?
v.lowlink = min(v.lowlink, v'.lowlink)
if (v.lowlink == v.index) // Is v the root of an SCC?
print "SCC:"
repeat
v' = S.pop
print v'
until (v' == v)
结论
我希望本文能帮助任何人加快他们的图像处理算法。我不知道这是否会发生,但如果微软可以在 C# 中集成内联汇编器(不是中间语言汇编器,而是本机汇编器),我们将能够以几乎与 C++ 代码一样快的速度编写算法,如果不是更快的话。
历史
- 2008 年 11 月 16 日:首次发布