Bitmap类、BitmapData类和Graphics类是C#图像处理中最常用到的类。如果要使用C#进行图像处理,应该了解他们。
1.Bitmap类:
Bitmap对象封装了GDI+中的一个位图,此位图由图像图像及其属性的像素数据组成。因此,Bitmap是用于处理由像素数据定义的图像的对象的。 该类的主要方法和属性如下:
- GetPixel 方法和SetPixel方法: 获取和设置一个图像的指定像素的颜色。
- PixelFormat: 返回图像的像素格式。
- Palette属性: 获取或设置图像所用的颜色调色板。
- Height属性和Width属性:返回图像的高度和宽度。
- LockBits方法和UnlockBits方法:分别锁定和解锁系统内存中的位图像素。在基于像素点的图像处理方法中使用LockBits方法和UnlockBits方法是一个很好的方式,这两种方法可以使我们通过制定像素的范围来控制位图的任意一部分,从而消除了通过循环对位图的像素逐个处理的需要。每次调用LockBits之后,都应该在调用一次UnlockBits。
Bitmap类使用LockBits和UnLockBits方法来将位图的数据矩阵保存在内存中、直接对它进行操作,最后用修改后的数据代替位图中的原始数据。LockBits返回以各BitmapData的类用已描述数据在已锁定的矩阵中的位置和分布。
2. BitmapData类
BitmapData对象指定了位图的属性,如下:
- Scan0:数据矩阵在内存中的地址。获取或设置位图中第一个 像素 数据的地址。
- Stride:数据矩阵中的行宽,以byte为单位。可能会扩展几个Byte,后面会介绍。
- PixelFormat:像素格式,这对矩阵中字节的定位很重要。
- Width:位图的宽度。
- Height:位图的高度。
具体关系见下图:
数组的宽度并不一定等于图像像素数组的宽度,还有一部分未用区域。只是为了提高效率,系统确定每行的字节数必须是4的倍数。例如一副24位、宽为17个像素的图像,它需要每行占用的空间为51(317)个字节,但是51不是4的倍数,因此还需要补充1个字节,从而使每行的字节数扩展为52(413)。即Stride=52;
在处理任意宽度的图像的时候,在进行下一行扫描的时候,需要将扩展的字节考虑进去。
3. Graphics类
Graphics对象是GDI+的关键,许多对象都是由Graphics 类表示的,该类定义了绘制和填充图形对象的方法和属性。一个应用程序,只要需要进行绘制或者着色,他就必须使用Graphics对象。
案例1:三种方式处理图像:
- OpenFileDialog 打开文件对话框:
OpenFileDialog openFileDialog = new OpenFileDialog();
//打开的文件选择对话框上的标题
openFileDialog.Title = "请选择文件";
//设置文件类型
openFileDialog.Filter = "文本文件(*.txt)|*.txt|所有文件(*.*)|*.*";
//设置默认文件类型显示顺序
openFileDialog.FilterIndex = 1;
//保存对话框是否记忆上次打开的目录
openFileDialog.RestoreDirectory = true;
//设置是否允许多选
openFileDialog.Multiselect = false;
//按下确定选择的按钮
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
//获得文件路径
string localFilePath = openFileDialog.FileName.ToString();
//获取文件路径,不带文件名
//FilePath = localFilePath.Substring(0, localFilePath.LastIndexOf("\\"));
//获取文件名,带后缀名,不带路径
string fileNameWithSuffix = localFilePath.Substring(localFilePath.LastIndexOf("\\") + 1);
//去除文件后缀名
string fileNameWithoutSuffix = fileNameWithSuffix.Substring(0, fileNameWithSuffix.LastIndexOf("."));
//在文件名前加上时间
string fileNameWithTime = DateTime.Now.ToString("yyyy-MM-dd ") + fileNameExt;
//在文件名里加字符
string newFileName = localFilePath.Insert(1, "dameng");
}
3. 分别用像素提取法、内存法、指针法对图像进行处理:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 三种图像处理方法
{
public partial class Form1 : Form
{
//存储文件的名称
private string currentFileName;
//图像
private Bitmap curBitmap;
//计时器
private HiperfTimer myTime = new HiperfTimer();
public Form1()
{
InitializeComponent();
}
/// <summary>
/// 打开图像
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void openImage_Click(object sender, EventArgs e)
{
//创建OpenFileDialog,选择文件的时候需要使用该类。
//打开文件对话框.
OpenFileDialog opnDlg = new OpenFileDialog();
//为对啊框设置一个标题
opnDlg.Title = "打开图像文件";
//设置文件类型 Filter属性,设置文件类型。
//openFileDialog.Filter = "文本文件(*.txt)|*.txt|所有文件(*.*)|*.*";
//启动帮助按钮。
opnDlg.ShowHelp = true;
//保存对话框是否记忆上次打开的目录。
opnDlg.RestoreDirectory = true;
//按下确定选择的按钮
if(opnDlg.ShowDialog()==DialogResult.OK)
{
//获得文件路径
currentFileName = opnDlg.FileName.ToString();
//使用Image.FromFile创建图像对象
try
{
//Image.FromFile: 从指定的文件创建 Image。
curBitmap = (Bitmap)Image.FromFile(currentFileName);
}
catch(Exception exp)
{
//抛出异常
MessageBox.Show(exp.Message);
}
pictureBox1.Image = curBitmap;
myTime.Stop();
}
}
/// <summary>
/// 关闭窗口
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void close_Click(object sender, EventArgs e)
{
this.Close();
}
/// <summary>
/// 保存图像
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void saveImage_Click(object sender, EventArgs e)
{
//如果没有创建图像,则直接退出
if(curBitmap==null)
{
return;
}
//调用SaveFileDiaLog
SaveFileDialog saveDlg = new SaveFileDialog();
//设置对话框标题
saveDlg.Title = "保存图片";
//改写已经存在文件时提示用户
saveDlg.OverwritePrompt = true;
//为图像选择一个筛选器
//saveDlg.Filter = "BMP文件(*.bmp)|*.bmp|" + "JPEG文件(*.jpg)|*.jpg|" + "PNG文件(*.png)|*.png|";
//启用帮助按钮
saveDlg.ShowHelp = true;
//如果选择了格式,则保存图像
if(saveDlg.ShowDialog()==DialogResult.OK)
{
//获取用户选择的文件名
string filename = saveDlg.FileName;
//获取用户选择文件的扩展名
string strFilExtn = filename.Remove(0, filename.Length - 3);
//保存文件
switch (strFilExtn)
{
case "bmp":
//bmp格式
curBitmap.Save(filename,System.Drawing.Imaging.ImageFormat.Bmp); break;
case "jpg":
//jpg格式
curBitmap.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg); break;
case "png":
//png格式
curBitmap.Save(filename, System.Drawing.Imaging.ImageFormat.Png); break;
default:
break;
}
}
}
/// <summary>
/// 像素提取法,该方法使用的是GDI+中的Bitmap.GetPixel和Bitmap.SetPixel方法
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void GetPix_Click(object sender, EventArgs e)
{
//首先判断图像是否为空
if(curBitmap !=null)
{
myTime.Start();
Color curColor;
int ret;
//二维图像数组循环
//此处curBitmap.Width是位图的宽度
for(int i=0;i<curBitmap.Width;i++)
{
for(int j=0;j<curBitmap.Height;j++)
{
//获取该点像素RGB颜色值
//GetPixel 返回结果:
// System.Drawing.Color 结构,它表示指定像素的颜色。
curColor = curBitmap.GetPixel(i, j);
//计算灰度值
ret = (int)(curColor.R * 0.299 + curColor.G * 0.587 + curColor.B * 0.114);
//设置该点的像素的灰度值为,R=G=B=ret
curBitmap.SetPixel(i, j, Color.FromArgb(ret, ret, ret));
}
}
}
pictureBox2.Image = curBitmap;
myTime.Stop();
time.Text = myTime.Duration.ToString("####.##") + "毫秒";
}
/// <summary>
/// 内存法,该方法直接将图像数据复制到内存中,提高程序的运行速度
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void memory_Click(object sender, EventArgs e)
{
if(curBitmap!=null)
{
myTime.Start();
//位图矩形;
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height); //定义矩形的大小就是整个位图的大小
//以可读写的方式锁定全部的位图像素
// BitmapData 包含被锁定区域的长、宽、格式、指针信息。
BitmapData bmpData = curBitmap.LockBits(rect, ImageLockMode.ReadWrite, curBitmap.PixelFormat);
//得到首地址
IntPtr ptr = bmpData.Scan0;
//定义被锁定的数组的大小,由位图区域和未用空间组成
int bytes = bmpData.Stride*bmpData.Height;
//定义位图数组
byte[] rgbValues = new byte[bytes];
//将被锁定的位图像素值赋值到该数组内
// Marshal.Copy
// 用来在托管对象和非托管对象之间进行内容复制。
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
//灰度化
double colortemp = 0;
for(int i=0;i<bmpData.Height;i++)
{
for(int j=0;j<bmpData.Width*3;j+=3)
{
colortemp = rgbValues[i * bmpData.Stride + j + 2] * 0.299 + rgbValues[i * bmpData.Stride + j + 1] * 0.587 + rgbValues[i * bmpData.Stride + j] * 0.114;
rgbValues[i * bmpData.Stride + j] = rgbValues[i * bmpData.Stride + j + 1] = rgbValues[i * bmpData.Stride + j + 2] = (byte)colortemp;
}
}
//将数组赋值回位图
System.Runtime.InteropServices.Marshal.Copy(rgbValues,0,ptr,bytes);
//解锁位图像素
curBitmap.UnlockBits(bmpData);
//显示图像
pictureBox2.Image = curBitmap;
myTime.Stop();
time.Text = myTime.Duration.ToString("####.##") + "毫秒";
}
}
/// <summary>
/// 指针法;指针法和内存法相似,也是通过LockBits方法来获得位图的首地址,但是该方法更简洁,直接应用指针对位图进行操作。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Point_Click(object sender, EventArgs e)
{
if(curBitmap!=null)
{
myTime.Start();
//位图矩形
Rectangle rect = new Rectangle(0, 0, curBitmap.Width, curBitmap.Height);
//以可读写的方式锁定全部的位图像素
//返回值是BitmapData
BitmapData bmpData = curBitmap.LockBits(rect, ImageLockMode.ReadWrite, curBitmap.PixelFormat);
byte temp = 0;
//启动不安全模式
unsafe
{
//得到首地址 bmpData.Scan0: 数据矩阵在内存中的地址。
byte* ptr = (byte*)(bmpData.Scan0);
//二维图像循环
for (int i = 0; i < bmpData.Height; i++)
{
for (int j = 0; j < bmpData.Width; j++)
{
//计算灰度值
temp = (byte)(ptr[2] * 0.299 + ptr[1] * 0.587 + ptr[0] * 0.114); //此处操作相当于数组,ptr相当于数字组名。
//R=G=B
ptr[0] = ptr[1] = ptr[2] = temp;
ptr += 3;
}
}
//让指针指向下一行数组的首个字节
ptr += bmpData.Stride - bmpData.Width * 3; //将未用空间空过。
}
//解锁位图像素
curBitmap.UnlockBits(bmpData);
pictureBox2.Image = curBitmap;
myTime.Stop();
time.Text = myTime.Duration.ToString("####.##") + "毫秒";
}
}
}
}
案例2: 灰度直方图:
窗口一:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 直方图
{
public partial class Form1 : Form
{
//位图。
Bitmap curBirmap;
//灰度数据
byte[] grayData;
//8位灰度图像
Bitmap grayImage;
public Form1()
{
InitializeComponent();
}
private void button2_Click(object sender, EventArgs e)
{
this.Close();
}
/// <summary>
/// 打开图像
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
//创建文本对话框,openFileFialog对象
OpenFileDialog openFile = new OpenFileDialog();
//设置对话框标题
openFile.Title="打开文本对话框";
//启用帮助按钮
openFile.ShowHelp = true;
openFile.RestoreDirectory = true;
//选定文件
if(openFile.ShowDialog()==DialogResult.OK)
{
//读取当前选定的文件名
string currentFileName = openFile.FileName;
//使用Image.FromFile创建图像对象
try
{
curBirmap = (Bitmap)Image.FromFile(currentFileName);
//显示图像。
pictureBox1.Image = curBirmap;
}
catch
{
MessageBox.Show("异常");
}
}
}
private void button3_Click(object sender, EventArgs e)
{
if(curBirmap!=null)
{
//创建矩形
Rectangle rect = new Rectangle(0, 0, curBirmap.Width, curBirmap.Height);
//用可读写的方式锁定位图像素
BitmapData bmpData = curBirmap.LockBits(rect, ImageLockMode.ReadWrite, curBirmap.PixelFormat);
//规定灰度数组的大小
grayData = new byte[bmpData.Width * bmpData.Height]; //不含未用空间
//获得首地址
IntPtr ptr = bmpData.Scan0;
//定义字节数组,用来存储位图像素数据
int bytes = bmpData.Height * bmpData.Stride;
byte[] rgbValue = new byte[bytes];
//复制被锁定的位图数据到数组中去
// 将数据从非托管内存指针复制到托管8位无符号整数数组中去。
// 参数1: IntPtr, 从中进行复制的内存指针
// 参数2: Byte[], 要赋值到的数组
// 参数3: startIndex, 开始的索引。目标数组从0开始的索引。
// 参数4: length, 要赋值的数组元素的数目。
Marshal.Copy(ptr, rgbValue, 0, bytes);
//灰度化
double colorTemp = 0;
for (int i = 0; i < bmpData.Height; i++)
{
for (int j = 0; j < bmpData.Width * 3; j += 3)
{
colorTemp = rgbValue[i * bmpData.Stride + j + 2] * 0.299 + rgbValue[i * bmpData.Stride + j + 1] * 0.587 + rgbValue[i * bmpData.Stride + j] * 0.114;
rgbValue[i * bmpData.Stride + j] = rgbValue[i * bmpData.Stride + j + 1] = rgbValue[i * bmpData.Stride + j + 2] = (byte)colorTemp;
grayData[i * bmpData.Width + (int)(Math.Floor(j / 3.0))] = (byte)colorTemp;
}
}
//用指定大小来初始化图像,并指定像素格式为八位。
grayImage = BuiltGrayBitmap(grayData, bmpData.Width, bmpData.Height);
//把数组赋值回位图
Marshal.Copy(rgbValue, 0, ptr, bytes);
//解锁位图
curBirmap.UnlockBits(bmpData);
pictureBox2.Image = grayImage;
//定义并实例化新窗体,并把图像数据传递给他。(更改其构造函数);
histForm histoGram = new histForm(grayImage);
//打开该窗体
histoGram.ShowDialog();
}
}
/// <summary>
/// 用灰度数组新建一个8位的灰度图像
/// </summary>
/// <param name="灰度数组 grayData"></param>
/// <param name="图像宽度 width"></param>
/// <param name="图像高度 height"></param>
/// <returns></returns>
private static Bitmap BuiltGrayBitmap(byte[] grayData, int width,int height)
{
//新建一个8位的灰度位图,并锁定内存操作区域
Bitmap grayImage_8 = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
//锁定图像区域
BitmapData grayImafeData = grayImage_8.LockBits(new Rectangle(0,0,width,height),ImageLockMode.WriteOnly,PixelFormat.Format8bppIndexed);
//获取图像的参数
int offset = grayImafeData.Stride - grayImafeData.Width; //计算每行未用空间的字节数
IntPtr ptr = grayImafeData.Scan0; //获得首地址
int scanBytes = grayImafeData.Height * grayImafeData.Stride; //图像字节数。
//为图像数据分配内存(将图像数据拷贝出来进行处理)
byte[] bytes=new byte[scanBytes];
//为图像数据数组赋值
int posSrc = 0, posScan = 0;
for(int i=0;i<height;i++)
{
for(int j=0;j<width;j++)
{
bytes[posScan++] = grayData[posSrc++];
}
posScan += offset;
}
//内存解锁
Marshal.Copy(bytes, 0, ptr, scanBytes);
grayImage_8.UnlockBits(grayImafeData); //解锁内存区域
//修改生成位图的索引表,从伪彩色改为灰度
ColorPalette palette;
using(Bitmap bmp=new Bitmap (1,1,PixelFormat.Format8bppIndexed))
{
palette = bmp.Palette;
}
for (int i=0;i<256;i++)
{
palette.Entries[i] = Color.FromArgb(i, i, i);
}
//修改生成位图的索引表
grayImage_8.Palette = palette;
return grayImage_8;
}
}
}
窗口2:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 直方图
{
public partial class histForm : Form
{
//传过来的图像数据
public Bitmap bmpHist;
//灰度级
private int[] countPixel;
//记录最大的灰度级个数
private int maxPixel;
public histForm(Bitmap bmp)
{
InitializeComponent();
bmpHist = bmp;
//灰度级
countPixel = new int[256];
}
/// <summary>
/// 为该窗体添加Paint事件,用来绘制直方图
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void histForm_Paint(object sender, PaintEventArgs e)
{
//获取Graphics对象
Graphics g = e.Graphics; //创建Graphics对象
//创建一个宽度为1的黑色钢笔
Pen curPen = new Pen(Brushes.Black, 1);
//绘制坐标轴
g.DrawLine(curPen, 50, 240, 320, 240);
g.DrawLine(curPen, 50, 240, 50, 30);
//绘制并标识坐标刻度
g.DrawLine(curPen, 100, 240, 100, 243);
g.DrawLine(curPen, 150, 240, 150, 243);
g.DrawLine(curPen, 200, 240, 200, 243);
g.DrawLine(curPen, 250, 240, 250, 243);
g.DrawLine(curPen, 300, 240, 300, 243);
g.DrawString("0", new Font("New Timer", 8), Brushes.Black, new PointF(46, 242));
g.DrawString("50", new Font("New Timer", 8), Brushes.Black, new PointF(92, 242));
g.DrawString("100", new Font("New Timer", 8), Brushes.Black, new PointF(139, 242));
g.DrawString("150", new Font("New Timer", 8), Brushes.Black, new PointF(189, 242));
g.DrawString("200", new Font("New Timer", 8), Brushes.Black, new PointF(239, 242));
g.DrawString("250", new Font("New Timer", 8), Brushes.Black, new PointF(289, 242));
g.DrawLine(curPen, 48, 40, 50, 40);
g.DrawString("0", new Font("New Timer", 8), Brushes.Black, new PointF(34, 232));
g.DrawString(maxPixel.ToString(), new Font("New Timer", 8), Brushes.Black, new PointF(18, 34));
//绘制直方图
double temp = 0;
for(int i=0;i<256;i++)
{
//纵坐标长度
temp = 200.0 * countPixel[i] / maxPixel;
g.DrawLine(curPen, 50 + i, 240, 50 + i, 240 - (int)temp);
}
//释放对象
curPen.Dispose();
}
/// <summary>
/// Load事件中计算各个灰度级所具有的像素个数。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void histForm_Load(object sender, EventArgs e)
{
//计算灰度级
//锁定8为灰度图像
Rectangle rect = new Rectangle(0, 0, bmpHist.Width, bmpHist.Height);
//锁定位图的像素数据,以可读写的方式
BitmapData bmpData = bmpHist.LockBits(rect, ImageLockMode.ReadWrite, bmpHist.PixelFormat);
//首地址
IntPtr ptr = bmpData.Scan0;
//创建byte数组,用来存储要被处理的数据
int bytes = bmpData.Stride * bmpData.Height;
byte[] grayValues = new byte[bytes];
//将数据复制到数组中
Marshal.Copy(ptr,grayValues,0,bytes);
byte temp = 0;
maxPixel = 0;
Array.Clear(countPixel, 0, 255);
//计算各灰度级的像素个数
//此处要考虑未用空间的影响,切记切记。
//for(int i=0;i<bytes;i++)
//{
// //灰度级
// temp = grayValues[i];
// //计数
// countPixel[(int)temp]++;
// if(countPixel[temp]>maxPixel)
// {
// //找到灰度频率最大的像素值
// maxPixel = countPixel[(int)temp];
// }
//}
for (int i = 0; i <bmpData.Height;i++ )
{
for(int j=0;j<bmpData.Width;j++)
{
temp = grayValues[i * bmpData.Stride + j];
countPixel[temp]++;
if(maxPixel<countPixel[temp])
{
//找到灰度频率最大的像素值
maxPixel = countPixel[temp];
}
}
}
//将数组中的数据复制回去
Marshal.Copy(grayValues, 0, ptr, bytes);
bmpHist.UnlockBits(bmpData);
}
}
}