C# 数字图像处理笔记

记录了如下内容

  • MDI多文档窗口创建
  • 打开图像
  • 保存图像
  • 图像的去色处理
  • 图像的二值化
  • 图像的提取边缘
  • 图像的中值滤波
  • 图像的直方图
  • 图像的直方图图像拉伸
  • 图像的直方图均衡化
  • 图像的线性点运算
  • 图像的目标提取标记

1 MDI多文档窗口创建

  • 创建一个普通的C# Windows窗体应用程序,将IsMdiContainer设置为true,这样一个多文档应用程序就创建完成了。
  • 只需将子窗口的MdiParent设置为主窗体就可以为主窗口添加一个子窗口。
  • 子窗口的显示只能用form.Show()方法,如果用的是form.ShowDialog()则==不是==MDI子窗口了;

2 打开图像

  • 打开文件需要用到OpenFileDialog控件,然后子主程序里面判断是否点击了确定按钮,当用户点击了确定按钮的时候就相应打开文件操作;
  • 当用户选择了一个文件并确定后,openFileDialogFileName属性就存储了需要打开文件的完整文件名;
  • 得到了需要打开图像的完整文件名后就需要用Bitmap对象存储图像文件;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try
{
if (openFileDialog_main.ShowDialog() == DialogResult.OK)
{
form_MDI form = new form_MDI();
form.MdiParent = this;
form.Text = openFileDialog_main.FileName;
Bitmap image = new Bitmap(openFileDialog_main.FileName);
form.image = image;
form.Show();
}
}
catch
{
MessageBox.Show("打开文件错误!", "无法正常打开文件!");
}
  • 这里有一个特别需要注意的地方就是openFileDialogFilter属性设置方法;
1
2
this.openFileDialog_main.Filter =  "所有图片文件|*.jpg;*.bmp;*.png;*.pcx;*.gif;";
this.openFileDialog_main.Title = "选择图片";
  • 在窗体上面显示图像,就需要用到Graphics对象;
  • 在整个窗体绘制完成后用强制刷新窗口,显示最新的图像;
1
2
3
g = Graphics.FromHwnd(this.Handle);
g.InterpolationMode = InterpolationMode.High;
g.DrawImage(image, new Rectangle(new Point(0, 0), imageSize));
1
2
3
4
5
6
7
8
private void form_MDI_SizeChanged(object sender, EventArgs e)
{
g = Graphics.FromHwnd(this.Handle);
g.InterpolationMode = InterpolationMode.High;
g.SmoothingMode = SmoothingMode.HighQuality;
this.Invalidate(new Rectangle(0,0,1,1));//重绘窗口

}

3 保存图像

  • 和打开图像一样,保存图像同样也会用到一个类似的saveFileDialog控件;
  • 当用户点击了保存窗口的确定按钮后就开始了保存动作;
  • 同样需要注意的是这个控件的Filter属性;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
try
{
if (this.ActiveMdiChild != null)
{
form_MDI form = (form_MDI)this.ActiveMdiChild;
saveFileDialog1.FileName = "MyPicture"; // 缺损文件名
saveFileDialog1.DefaultExt = ".bmp"; // Default file extension
saveFileDialog1.Filter = "bmp 文件 (.bmp)|*.bmp|jpg 文件(.jpg)|*.jpg|png 文件(.png)|*.png"; // Filter files by extension
string filename = string.Empty;
if (saveFileDialog1.ShowDialog() == DialogResult.OK)
{
filename = saveFileDialog1.FileName;
string fileXtn = "bmp";//设置默认扩展名
ImageFormat Formt = ImageFormat.Bmp;//设置保存图片的格式
fileXtn = filename.Remove(0, filename.Length - 3);//提取图片的扩展名
switch (fileXtn)
{
case "bmp": Formt = ImageFormat.Bmp;
break;
case "jpg": Formt = ImageFormat.Jpeg;
break;
case "png": Formt = ImageFormat.Png;
break;
default: return;
}
form.image.Save(filename, Formt);//保存图片
}
MessageBox.Show("保存成功!", "保存文件", MessageBoxButtons.OK, MessageBoxIcon.Asterisk);//保存成功提示框
}
}
}
catch
{
}

4 图像的去色处理

  • 图像的去色,就是将普通的彩色图像修改为改为黑白照片;
  • 普通的RGB图像换算为灰度图像有一个公式(Red * 19595 + Green * 38469 + Blue * 7472) >> 16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void GreyImage(Bitmap image)
{
BitmapData data = image.LockBits(new Rectangle(new Point(), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
for (int y = 0; y < image.Height * data.Stride; y += data.Stride)
{
for (int x = 0; x < image.Width*3; x += 3)
{
int index = y + x;
byte Blue = datas[index];
byte Green = datas[index + 1];
byte Red = datas[index + 2];
datas[index] = datas[index + 1] = datas[index + 2] = (byte)((Red * 19595 + Green * 38469 + Blue * 7472) >> 16);
}
}
Marshal.Copy(datas, 0,data.Scan0, datas.Length);
image.UnlockBits(data);
}

5 图像的提取边缘

  • 图像的边缘提取有很多的方法,这里利用的是差分法,就是利用拉普拉斯算子对图像进行差分计算,得到了灰度边缘,然后进行二值化,使图像形成比较清晰的轮廓;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void ExtactEage(Bitmap image)
{
BitmapData data = image.LockBits(new Rectangle(new Point(), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas=new byte[data.Stride*image.Height];
byte[] eages=new byte[data.Stride*image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
for (int y = data.Stride; y < image.Height * data.Stride - data.Stride; y += data.Stride)
{
for (int x = 3; x < image.Width * 3 - 3; x += 3)
{
int index = y + x;
byte grey = datas[index];
int value = 0;
for (int yy = -data.Stride; yy <= data.Stride; yy += data.Stride)
{
for (int xx = -3; xx <= 3; xx += 3)
{
if (xx == 0 && yy == 0)
continue;
index = x + y + xx + yy;
value += Math.Abs(grey - datas[index]);
}
eages[index] = eages[index + 1] = eages[index + 2] = (byte)(value >> 3);
}
}
}
Marshal.Copy(eages, 0, data.Scan0, datas.Length);
image.UnlockBits(data);
}

6 图像的二值化

  • 图像二值化就是将图片转换成只有黑白两种颜色的图像;
  • 二值化转换的过程中需要得到进行二值化最小、最大的两个颜色值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// 将图像二值化
/// </summary>
/// <param name="image">需要处理的图像</param>
/// <param name="indexColor">处理的颜色索引值,Blue:0,Green:1,Red:2</param>
/// <param name="thresholMin">阈值下限</param>
/// <param name="thresholMax">阈值上限</param>
public static void BinaryImage(Bitmap image, int indexColor, int thresholMin, int thresholMax)
{
BitmapData data = image.LockBits(new Rectangle(new Point(), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
for (int y = 0; y < data.Stride * image.Height;y+=data.Stride )
{
for (int x = 0; x < image.Width*3; x += 3)
{
int index = y + x;
if (datas[index+indexColor] >= thresholMin && datas[index+indexColor] <= thresholMax)
{
datas[index] = datas[index + 1] = datas[index + 2] = 255;
}
else
datas[index] = datas[index + 1] = datas[index + 2] = 0;
}
}
Marshal.Copy(datas, 0, data.Scan0, datas.Length);
image.UnlockBits(data);
}

7 图像的中值滤波

  • 图像的滤波处理可以使图像更加的平滑,这里利用的是中值滤波的方法进行图像滤波处理;
  • 中值滤波算法的思想是重新计算图像所有的像素值,去像素的3X3邻域9个像素点的颜色值,从大到小进行排列,去排序后中间一个像素的颜色值作为滤波后该颜色的像素值;
  • 这里排序利用的是List<>[].sort()方法;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/// <summary>
/// 对图像进行中值滤波
/// </summary>
/// <param name="image">需要处理的图像</param>
public static void medianFilter(Bitmap image)
{

BitmapData data = image.LockBits(new Rectangle(new Point(), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
for (int y = 1; y < image.Height-1; y++)
{
for (int x = 1; x < image.Width-1; x++)
{
List<byte>[] cacData = new List<byte>[3];//此时的数字3是表示[3个byte]为一个list
for (int k = 0; k <= 2; k++)
cacData[k]=new List<byte>();
for (int yy = -1; yy <= 1; yy++)
{
for (int xx = -1; xx <= 1; xx++)
{
int index = (y + yy) * data.Stride + (x + xx) * 3;
for (int k = 0; k <= 2; k++)
{
cacData[k].Add(datas[index + k]);
}
}
}
for (int k = 0; k <= 2; k++)
cacData[k].Sort();
int indexMedian = y * data.Stride + x * 3;
for (int k = 0; k <= 2; k++)
{
datas[indexMedian + k] = cacData[k][4];
cacData[k].Clear();
}
cacData = null;
}
}
Marshal.Copy(datas, 0, data.Scan0, datas.Length);
image.UnlockBits(data);
}

8 图像的直方图

  • 图像直方图显示的是一个灰度图像的所有像素值得分不规律;
  • 横坐标是0-255,纵坐标则是格银色像素点的的相对数量比;
  • 直方图可以显示一个图像的像素分布水平,可以为其他的更高级的图像处理提供参考依据;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/// <summary>
/// 生成灰度直方图
/// </summary>
/// <param name="image">需要统计的图像</param>
/// <returns>imageHistogram</returns>
public static Bitmap getHistogram(Bitmap image, int indexColor)
{
Bitmap imageHistogram = new Bitmap(256, 256);
Graphics gHistogram = Graphics.FromImage(imageHistogram);
int maxHistogram = 0;
int[] histogram = new int[256];
BitmapData data = image.LockBits(new Rectangle(new Point(0, 0), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
for (int y = 0; y < image.Height * data.Stride; y += data.Stride)
{
for (int x = 0; x < image.Width * 3; x += 3)
{
int index = y + x;
histogram[datas[index + indexColor]]++;
}
}
image.UnlockBits(data);

for (int value = 0; value < 256; value++)
{
if (histogram[value] > histogram[maxHistogram])
{
maxHistogram = (byte)value;
}
}

gHistogram.Clear(Color.Gray);
for (int value = 0; value < 256; value++)
{
int length = Byte.MaxValue * histogram[value] / histogram[maxHistogram];
gHistogram.DrawLine(new Pen(Color.Black, 1f), value, 256, value, 256 - length);
}
Font font = new Font("宋体", 9f);
for (int value = 32; value < 256; value += 32)
{
Pen pen = new Pen(Color.Red, 1f);
gHistogram.DrawLine(pen, 0, value, 255, value);
gHistogram.DrawLine(pen, value, 0, value, 255);
SizeF sizeValue = gHistogram.MeasureString(value.ToString(), font);
gHistogram.DrawString(value.ToString(), font, Brushes.SkyBlue, value - sizeValue.Width / 2, 240);
}
return imageHistogram;
}

9 图像的直方图图像拉伸

  • 基于直方图的图像拉伸可以改善图像整体颜色的对比度和亮度;
  • 直方图的图像拉伸思想是将图像的最大像素值和最小像素值拉伸至255和0,中间各像素点也会同比例的拉伸;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// 灰度拉伸,
/// </summary>
/// <param name="image">需要做拉伸处理的图像</param>
public static void GreyStretch(Bitmap image)
{
int maxVlue, minValue;
BitmapData data = image.LockBits(new Rectangle(new Point(), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
maxVlue = minValue = datas[0];
try
{
//寻找最大、最小值
for (int y = 0; y < data.Stride * image.Height; y += data.Stride)
{
for (int x = 0; x < image.Width * 3; x += 3)
{
int index = y + x;
if (datas[index] > maxVlue)
maxVlue = datas[index];
if (datas[index] < minValue)
minValue = datas[index];
}
}
//图像拉伸
for (int y = 0; y < data.Stride * image.Height; y += data.Stride)
{
for (int x = 0; x < image.Width * 3; x += 3)
{
int index = y + x;
if (maxVlue > minValue)
{
//maxVlue = (int)(maxVlue -(maxVlue-minValue)/4);
//minValue = (int)(minValue + (maxVlue - minValue) / 4);
datas[index] = (byte)(255 * (datas[index] - minValue) / (maxVlue - minValue));//主要计算公式
datas[index + 1] = datas[index + 2] = datas[index];
}
}
}
Marshal.Copy(datas, 0, data.Scan0, datas.Length);
image.UnlockBits(data);
}
catch
{
}
}

10 图像的直方图均衡化

  • 直方图均衡化又叫直方图修平,通常该方法用来增加图像的局部对比度,尤其是图像有用像素的对比度十分接近时,通过这种方法亮度可以很好的在直方图上面分布;
  • 直方图均衡化的思想是将图像所有的像素点通过直方图映射到0-1(也就是0-255,最亮和最暗)区间里;
  • 此种方法的到的图像直方图是不连续的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/// <summary>
/// 图像直方图均衡化
/// </summary>
/// <param name="image">需要进行直方图均衡化的图像</param>
public static void equalization(Bitmap image)
{
int[] histogram = new int[256];
int[] quaHistogram = new int[256];
BitmapData data = image.LockBits(new Rectangle(new Point(0, 0), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
//得到图像的直方图数据
for (int y = 0; y < image.Height * data.Stride; y += data.Stride)
{
for (int x = 0; x < image.Width * 3; x += 3)
{
int index = y + x;
histogram[datas[index]]++;
}
}
//进行直方图均衡化处理
int temp = 0;
for (int i = 0; i <= 255; i++)
{
temp += histogram[i];
quaHistogram[i] = (int)(255 * temp / image.Height / image.Width + 0.5);//将灰度直方图均衡化的数据映射到0-255之内
}
for (int y = 0; y < image.Height * data.Stride; y += data.Stride)
{
for (int x = 0; x < image.Width * 3; x += 3)
{
int index = y + x;
int htemp = datas[index];
datas[index] = (byte)quaHistogram[htemp];
datas[index + 1] = datas[index + 2] = datas[index];
}
}

Marshal.Copy(datas, 0, data.Scan0, datas.Length);
image.UnlockBits(data);

}

11 图像的线性点运算

  • 图像的线性点运算,可以利用线性的方法改变图像的整体亮度和对比度;
  • 当斜率设置为-1,偏移量设置为255时,进过图像的线性运算后得到的就是图像的负片;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// 线性点运算,
/// Out=Old*scaling+offset
/// 当scaling=-1, offset=255时得到的就是负片
/// </summary>
/// <param name="image">进行点运算的图像</param>
/// <param name="scaling">斜率</param>
/// <param name="offset">偏移量</param>
public static void linear(Bitmap image, double scaling, int offset)
{
BitmapData data = image.LockBits(new Rectangle(new Point(), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
for (int y = 0; y < data.Stride * image.Height; y += data.Stride)
{
for (int x = 0; x < image.Width * 3; x += 3)
{
int index = y + x;
int temp = (int)(datas[y + x] * scaling + offset + 0.5);//进行主要运算
if (temp > 255)
{
datas[y + x] = datas[y + x + 1] = datas[y + x + 2] = 255;

}
else if (temp < 0)
{
datas[y + x] = datas[y + x + 1] = datas[y + x + 2] = 0;

}
else
{
datas[y + x] = datas[y + x + 1] = datas[y + x + 2] = (byte)temp;
}
}
}
Marshal.Copy(datas, 0, data.Scan0, datas.Length);
image.UnlockBits(data);
}

12 图像的目标提取标记

  • ==此方法对于色块比较大的二值图像会出现堆栈溢出的现象!==
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// <summary>
/// 图像连通区域的标记,对于连通区域太大的
/// </summary>
/// <param name="image">需要处理的图像</param>
public static void connectRegion(Bitmap image)
{
BitmapData data = image.LockBits(new Rectangle(new Point(), image.Size), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
byte[] datas = new byte[data.Stride * image.Height];
Marshal.Copy(data.Scan0, datas, 0, datas.Length);
int sign = 11;
for (int y = 0; y < data.Stride * image.Height; y += data.Stride)
{
for (int x = 0; x < image.Width * 3; x += 3)
{
int index = y + x;
if (datas[index] == 0)
{
signRegion(datas, x, y, sign, data.Stride, image.Width, image.Height);
sign = (sign + 33) % 255;
}
}
}
Marshal.Copy(datas, 0, data.Scan0, datas.Length);
image.UnlockBits(data);

}
  • 增加了一个进行上下左右搜索的递归函数,此处易出现堆栈溢出;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/// <summary>
/// 连通区域的递归计算
/// </summary>
/// <param name="datas">图像数据</param>
/// <param name="x">像素点的竖直坐标</param>
/// <param name="y">像素点的横向坐标</param>
/// <param name="sign">标记的颜色</param>
/// <param name="stride">行宽</param>
/// <param name="width">图像的宽度</param>
/// <param name="heigh">图像的高度</param>
static void signRegion(byte[] datas, int x, int y, int sign, int stride, int width, int heigh)
{

try
{
datas[y + x] = datas[y + x + 1] = datas[y + x + 2] = (byte)sign;//标记区域
if (x > 0 && datas[y + x - 3] == 0)
{
signRegion(datas, x - 3, y, sign, stride, width, heigh);//搜索左边像素
}
if (x < width * 3 - 3 && datas[y + x + 3] == 0)
{
signRegion(datas, x + 3, y, sign, stride, width, heigh);//搜索右边像素
}
if (y > 0 && datas[y - stride + x] == 0)
{
signRegion(datas, x, y - stride, sign, stride, width, heigh);//搜索上边像素
}
if (y < stride * heigh && datas[y + stride + x] == 0)
{
signRegion(datas, x, y + stride, sign, stride, width, heigh);//搜索下边像素
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "出现错误", MessageBoxButtons.OK, MessageBoxIcon.Error);

}
}

–END–

-------------本文结束感谢您的阅读-------------