C#数字图像处理(一)
文章目录
- 1.C#图像处理基础
- 1.1 Bitmap类
- 1.2 Bitmapdata类
- 1.3 Graphics类
- 1.4 Image类
- 2.彩色图像灰度化
- 1.提取像素法
- 2.内存法
- 3.指针法
- 三种方法的比较
- 4.灰度图像二值化:
- 3.相关链接
Bitmap类、 Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C#
进行图像处理,就一定要掌握它们。
1.C#图像处理基础
1.1 Bitmap类
Bitmap对象封装了GDI+中的一个位图,此位图由图形图像及其属性的像素数据组成。
因此 Bitmap是用于处理由像素数据定义的图像的对象。
Bitmap类类的主要方法和属性如下:
Get Pixe
l方法和 Setpiⅸel方法:
获取和设置一个图像的指定像素的颜色。
Pixelformat
属性:返回图像的像素格式。
Palette
属性:获取或设置图像所使用的颜色调色板。
Height
属性和 Width属性
:返回图像的高度和宽度
Lockbits方法和 Unlockbits方法:
分别锁定和解锁系统内存中的位图像素。在基于像素点的图像处理方法中使用 Lockbits和 Unlockbits是一个很好的方式,这两种方法可以使我们通过指定像素的范围来控制位图的任意一部分,从而消除了通过循环对位图的像素逐个进行处理的需要。每次调用 Lockbits之后都应该调用一次 Unlockbits。
Lockbits方法的定义如下:
public BitmapData LockBits(rectangle rect,ImageLockMode flag,PixelFormat format);
Lockbits矩形参数Rectangle定义了要在系统内存中锁定的位图的一部分;
Image Lockmode枚举提供了对数据的访问方式。
Pixelformat枚举表示像素的格式,
Unlockbits方法使用一个由Lockbits返回的类型为BitmapData的参数,它定义为:
public void Unlockbits(BitmapData bitmapdata);
1.2 Bitmapdata类
Bitmapdata对象指定了位图的属性,如下所示。
Height
属性:被锁定位图的高度
Width
属性:被锁定位图的宽度
PixelFormat
属性:数据的实际像素格式
Scan0
属性:被锁定数组的首字节地数组的地址。如果整个图像被锁定,则是图像Height的第一个字节地址。
Stride
属性:步幅,也称为扫描宽度
如图所示,数组的宽度并不一定等于图像像素数组的宽度,还有一部分未用区域。这是为了提高效率,系统要确定每行的字节数必须为4的倍数。
例如一幅24位、宽为17个像素的图像,它需要每行占有的空间为51(3×17)个字节,但51不是4的倍数,因此还需要补充1个字节,从而使每行的字节数扩展为52(4x13,即 Stride=52),这样就满足了每行字节数是4的倍数的条件。需要扩展多少个字节不仅是由图像的宽度决定,而且还由图像像素的格式决定。
由于本书所选择的图像大小都为512×512,因此无论是24位彩色图像,还是8位的灰度图像,都满足是4的倍数的条件,无需再扩展。如果处理的是任意宽度的图像,那么在进行行扫描的时候,就需要把扩展字节去除掉。
1.3 Graphics类
raphics对象是GDH+的关键所在。许多对象都是由 Graphics类表示的,该类定义了绘
制和填充图形对象的方法和属性。一个应用程序只要需要进行绘制或着色,它就必须使用
Graphics对象。
1.4 Image类
这个类提供了位图和元文件操作的函数.Image类被声明为abstract,也就是说Image类不能实例化对象,而只能做为一个基类
1.FromFile方法:
它根据输入的文件名产生一个Image对象,它有两种函数形式:
public static Image FromFile(string filename);
public static Image FromFile(string filename, bool useEmbeddedColorManagement);
2.FromHBitmap方法:
它从一个windows句柄处创建一个bitmap对象,它也包括两种函数形式:
public static bitmap fromhbitmap(intptr hbitmap);
public static bitmap fromhbitmap(intptr hbitmap, intptr hpalette);
- FromStream方法:
从一个数据流中创建一个image对象,它包含三种函数形式:
public static image fromstream(stream stream);
public static image fromstream(stream stream, bool useembeddedcolormanagement);
fromstream(stream stream, bool useembeddedcolormanagement, bool validateimagedata);
基础代码框架:
打开、保存、显示图像
//文件名private string curFileName;//图像对象private Bitmap curBitmap;/// <summary>/// 打开图像文件/// </summary>private void open_Click(object sender, EventArgs e){//创建OpenFileDialogOpenFileDialog opnDlg = new OpenFileDialog();//为图像选择一个筛选器opnDlg.Filter = "所有图像文件|*.bmp;*.pcx;*.png;*.jpg;*.gif;" +"*.tif;*.ico;*.dxf;*.cgm;*.cdr;*.wmf;*.eps;*.emf|" +"位图(*.bmp;*.jpg;*.png;...)|*.bmp;*.pcx;*.png;*.jpg;*.gif;*.tif;*.ico|"
+"矢量图(*.wmf;*.eps;*.emf;...)|*.dxf;*.cgm;*.cdr;*.wmf;*.eps;*.emf";//设置对话框标题opnDlg.Title = "打开图像文件";//启用“帮助”按钮opnDlg.ShowHelp = true;//如果结果为“打开”,选定文件if (opnDlg.ShowDialog() == DialogResult.OK){//读取当前选中的文件名curFileName = opnDlg.FileName;//使用Image.FromFile创建图像对象try{curBitmap = (Bitmap)Image.FromFile(curFileName);}catch (Exception exp){MessageBox.Show(exp.Message);}}//对窗体进行重新绘制,这将强制执行paint事件处理程序Invalidate();}//在控件需要重新绘制时发生(窗体事件)private void Form1_Paint(object sender, PaintEventArgs e){//获取Graphics对象Graphics g = e.Graphics;if (curBitmap != null){//使用DrawImage方法绘制图像//180,20:显示在主窗体内,图像左上角的坐标//curBitmap.Width, curBitmap.Height图像的宽度和高度g.DrawImage(curBitmap, 180, 20, curBitmap.Width, curBitmap.Height);}}/// <summary>/// 保存图像文件/// </summary>private void save_Click(object sender, EventArgs e){//如果没有创建图像,则退出if (curBitmap == null)return;//调用SaveFileDialogSaveFileDialog saveDlg = new SaveFileDialog();//设置对话框标题saveDlg.Title = "保存为";//改写已存在文件时提示用户saveDlg.OverwritePrompt = true;//为图像选择一个筛选器saveDlg.Filter = "BMP文件(*.bmp)|*.bmp|" + "Gif文件(*.gif)|*.gif|" + "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":curBitmap.Save(filename, ImageFormat.Bmp);break;case "jpg":curBitmap.Save(filename, ImageFormat.Jpeg);break;case "gif":curBitmap.Save(filename, ImageFormat.Gif);break;case "tif":curBitmap.Save(filename, ImageFormat.Tiff);break;case "png":curBitmap.Save(filename, ImageFormat.Png);break;default:break;}}}
2.彩色图像灰度化
为加快处理速度,在图像处理算法中,往往需要把彩色图像转换为灰度图像,在灰度图像上得到验证的算法,很容易移植到彩色图像上。
24位彩色图像每个像素用3个字节表示,每个字节对应着R、G、B分量的亮度(红、绿、蓝)。当R、G、B分量值不同时,表现为彩色图像,当R、G、B分量值相同时,表现为灰度图像,该值就是我们所求的。
一般来说,转换公式有3种。第一种转换公式为:
Gray(i,j)=[R(i,j)+G(i,j)+B(i,j)]÷3 (2.1)
其中,Gray(i,j)为转换后的灰度图像在(i,j)点处的灰度值。该方法虽然简单,但人眼对颜色的感应是不同的,因此有了第二种转换公式:
Gray(i,j)=0299R(i,j)+0.587×G(i,j)+0.114×B(i,j) (2.2)
观察上式,发现绿色所占的比重最大,所以转换时可以直接使用G值作为转换后的灰度。
Gray(i,j)=G(i,j) (2.3)
在这里,我们应用最常用的公式(2.2),并且变换后的灰度图像仍然用24位图像表示。
1.提取像素法
这种方法简单易懂,但相当耗时,完全不可取。该方法使用的是GD+中的Bitmap Getpixel
和 BitmapSetpixel
方法。为了将位图的颜色设置为灰度或其他颜色,就需要使用Getpixel
来读取当前像素的颜色,再计算灰度值,最后使用Setpixel
来应用新的颜色。双击“提取像素法” 的Button控件,为该控件添加 Click
事件。
代码如下:
/// <summary>/// 提取像素法/// </summary>private void pixel_Click(object sender, EventArgs e){if (curBitmpap != null){Color curColor;int ret;//二维图像数组循环for(int i = 0; i < curBitmpap.Width; i++){for(int j = 0; j < curBitmpap.Height; j++){//获取该像素点的RGB颜色值curColor = curBitmpap.GetPixel(i, j);//利用公式计算灰度值ret = (int)(curColor.R * 0.299 + curColor.G * 0.587 + curColor.B * 0.114);//设置该像素点的灰度值,R=G=B=retcurBitmpap.SetPixel(i, j, Color.FromArgb(ret, ret, ret));}}//对窗体进行重新绘制,这将强制执行Paint事件处理程序Invalidate();}}
2.内存法
该方法就是把图像数据直接复制到内存中,这样就使程序的运行速度大大提高。双击“内存法”按钮控件,为该控件添加Cick事件。
代码如下:
/// <summary>/// 内存法(适用于任意大小的24位彩色图像)/// </summary>private void memory_Click(object sender, EventArgs e){if (curBitmpap != null){//位图矩形Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height);//以可读写的方式锁定全部位图像素System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat);//得到首地址IntPtr ptr = bmpData.Scan0;//定义被锁定的数组大小,由位图数据与未用空间组成的int bytes = bmpData.Stride * bmpData.Height;//定义位图数组byte[] rgbValues = new byte[bytes];//复制被锁定的位图像素值到该数组内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;//R=G=BrgbValues[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);//解锁位图像素curBitmpap.UnlockBits(bmpData);//对窗体进行重新绘制,这将强制执行Paint事件处理程序Invalidate();}}
3.指针法
该方法与内存法相似,开始都是通过 Lockbits方法来获取位图的首地址。但该方法更简洁,直接应用指针对位图进行操作。
为了保持类型安全,在默认情况下,C#是不支持指针运算的,因为使用指针会带来相关的风险。所以C#只允许在特别标记的代码块中使用指针。通过使用 unsafe关键字,可以定义可使用指针的不安全上下文。
双击“指针法”按钮控件,为该控件添加 Click
事件,
代码如下:
/// <summary>/// 指针法/// </summary>private void pointer_Click(object sender, EventArgs e){if (curBitmpap != null){//位图矩形Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height);//以可读写的方式锁定全部位图像素System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat);byte temp = 0;//启用不安全模式unsafe{//得到首地址byte* ptr = (byte*)(bmpData.Scan0);//二维图像循环for (int i = 0; i < bmpData.Height; i++){for (int j = 0; j < bmpData.Width; j++){//利用公式计算灰度值temp = (byte)(0.299 * ptr[2] + 0.587 * ptr[1] + 0.114 * ptr[0]);//R=G=Bptr[0] = ptr[1] = ptr[2] = temp;//指向下一个像素ptr += 3;}//指向下一行数组的首个字节ptr += bmpData.Stride - bmpData.Width * 3;}}//解锁位图像素curBitmpap.UnlockBits(bmpData);//对窗体进行重新绘制,这将强制执行Paint事件处理程序Invalidate();}}
由于启动了不安全模式,为了能够顺利地编译该段代码,必须设置相关选项。在主菜单中选择“项目|gray属性”,在打开的属性页中选择“生成”属性页,最后选中“允许不安全代码”复选框。
三种方法的比较
从3段代码的长度和难易程度来看,提取像素法又短又简单。它直接应用GD+中的Bitmap. Getpixel方法和 Bitmap. Setpixel方法,大大减少了代码的长度,降低了使用者的难度,并且可读性好。
但衡量程序好坏的标准,不是仅仅看它的长度和难易度,而是要看它的效率,尤其是像图像处理这种往往需要处理二维数据的大信息量的应用领域,就更需要考虑效率了,为了比较这3种方法的效率,我们对其进行计时。
首先在主窗体内添加一个 Label控件和 Textbox控件。
添加一个类:HiPerfTimer
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Threading;namespace gray
{internal class HiPerfTimer{//引用win32API中的QueryPerformanceCounter()方法//该方法用来查询任意时刻高精度计数器的实际值[DllImport("Kernel32.dll")] //using System.Runtime.InteropServices;private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);//引用win32API中的QueryPerformanceCounter()方法//该方法用来查询任意时刻高精度计数器的实际值[DllImport("Kernel32.dll")]private static extern bool QueryPerformanceFrequency(out long lpFrequency);private long startTime, stopTime;private long freq;public HiPerfTimer(){startTime = 0;stopTime = 0;if(QueryPerformanceFrequency(out freq) == false){//不支持高性能计时器throw new Win32Exception(); //using System.ComponentModel;}}//开始计时public void Start(){//让等待线程工作Thread.Sleep(0); //using System.Threading;QueryPerformanceCounter(out startTime);}//结束计时public void Stop(){QueryPerformanceCounter(out stopTime);}//返回计时结果(ms)public double Duration{get{return (double)(stopTime - startTime) * 1000 / (double)freq;}}}
}
在Form1类内定义HiPerfTimer类并在构造函数内为其实例化。
private HiPerfTimer myTimer;public Form1(){InitializeComponent();myTimer = new gray.HiPerfTimer();}
分别在“提取像素法”、“内存法”和“指针法”的 Button控件的 Click事件,程序代码内的if判断语句之间的最开始一行添加以下代码:
//启动计时器myTimer.Start();
在上述3个单击事件内的 Invalidate0语句之前添加以下代码:
//关闭计时器myTimer.Stop();//在TextBox内显示计时时间timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒";
最后编译并运行该段程序,可以明显看出,内存法和指针法比提取像素法要快得多。
提取像素法应用GDI+中的方法,易于理解,方法简单,很适合于C#的初学者使用,但它的运行速度最慢,效率最低。
内存法把图像复制到内存中,直接对内存中的数据进行处理,速度明显提高,程序难度也不大。 指针法,直接应用指针来对图像进行处理,所以速度最快。但在C#中是不建议使用指针的,因为使用指针,代码不仅难以编写和调试,而且无法通过CLR的内存类型安全检查,不能发挥C#的特长。
只有对C#和指针有了充分的理解,才能用好该方法。究竟要使用哪种方法,还要看具体情况而定。但3种方法都能有效地对图像进行处理。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;namespace gray
{public partial class Form1 : Form{private HiPerfTimer myTimer;public Form1(){InitializeComponent();myTimer = new gray.HiPerfTimer();}//文件名private string curFileName;//图像对象private System.Drawing.Bitmap curBitmpap;/// <summary>/// 打开图像文件/// </summary>private void open_Click(object sender, EventArgs e){//创建OpenFileDialogOpenFileDialog opnDlg = new OpenFileDialog();//为图像选择一个筛选器opnDlg.Filter = "所有图像文件|*.bmp;*.pcx;*.png;*.jpg;*.gif;" +"*.tif;*.ico;*.dxf;*.cgm;*.cdr;*.wmf;*.eps;*.emf|" +"位图(*.bmp;*.jpg;*.png;...)|*.bmp;*.pcx;*.png;*.jpg;*.gif;*.tif;*.ico|" +"矢量图(*.wmf;*.eps;*.emf;...)|*.dxf;*.cgm;*.cdr;*.wmf;*.eps;*.emf";//设置对话框标题opnDlg.Title = "打开图像文件";//启用“帮助”按钮opnDlg.ShowHelp = true;//如果结果为“打开”,选定文件if(opnDlg.ShowDialog()==DialogResult.OK){//读取当前选中的文件名curFileName = opnDlg.FileName;//使用Image.FromFile创建图像对象try{curBitmpap = (Bitmap)Image.FromFile(curFileName);}catch(Exception exp){MessageBox.Show(exp.Message);}}//对窗体进行重新绘制,这将强制执行paint事件处理程序Invalidate();}private void Form1_Paint(object sender, PaintEventArgs e){//获取Graphics对象Graphics g = e.Graphics;if (curBitmpap != null){//使用DrawImage方法绘制图像//160,20:显示在主窗体内,图像左上角的坐标//curBitmpap.Width, curBitmpap.Height图像的宽度和高度g.DrawImage(curBitmpap, 160, 20, curBitmpap.Width, curBitmpap.Height);}}/// <summary>/// 保存图像文件/// </summary>private void save_Click(object sender, EventArgs e){//如果没有创建图像,则退出if (curBitmpap == null)return;//调用SaveFileDialogSaveFileDialog saveDlg = new SaveFileDialog();//设置对话框标题saveDlg.Title = "保存为";//改写已存在文件时提示用户saveDlg.OverwritePrompt = true;//为图像选择一个筛选器saveDlg.Filter = "BMP文件(*.bmp)|*.bmp|" + "Gif文件(*.gif)|*.gif|" + "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":curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp);break;case "jpg":curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg);break;case "gif":curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Gif);break;case "tif":curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Tiff);break;case "png":curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Png);break;default:break;}}}//关闭窗体 private void close_Click(object sender, EventArgs e){this.Close();}/// <summary>/// 提取像素法/// </summary>private void pixel_Click(object sender, EventArgs e){//启动计时器myTimer.Start();if (curBitmpap != null){Color curColor;int ret;//二维图像数组循环for(int i = 0; i < curBitmpap.Width; i++){for(int j = 0; j < curBitmpap.Height; j++){//获取该像素点的RGB颜色值curColor = curBitmpap.GetPixel(i, j);//利用公式计算灰度值ret = (int)(curColor.R * 0.299 + curColor.G * 0.587 + curColor.B * 0.114);//设置该像素点的灰度值,R=G=B=retcurBitmpap.SetPixel(i, j, Color.FromArgb(ret, ret, ret));}}//关闭计时器myTimer.Stop();//在TextBox内显示计时时间timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒";//对窗体进行重新绘制,这将强制执行Paint事件处理程序Invalidate();}}/// <summary>/// 内存法(适用于任意大小的24位彩色图像)/// </summary>private void memory_Click(object sender, EventArgs e){//启动计时器myTimer.Start();if (curBitmpap != null){//位图矩形Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height);//以可读写的方式锁定全部位图像素System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat);//得到首地址IntPtr ptr = bmpData.Scan0;//定义被锁定的数组大小,由位图数据与未用空间组成的int bytes = bmpData.Stride * bmpData.Height;//定义位图数组byte[] rgbValues = new byte[bytes];//复制被锁定的位图像素值到该数组内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;//R=G=BrgbValues[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);//解锁位图像素curBitmpap.UnlockBits(bmpData);//关闭计时器myTimer.Stop();//在TextBox内显示计时时间timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒";//对窗体进行重新绘制,这将强制执行Paint事件处理程序Invalidate();}}/// <summary>/// 指针法/// </summary>private void pointer_Click(object sender, EventArgs e){//启动计时器myTimer.Start();if (curBitmpap != null){//位图矩形Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height);//以可读写的方式锁定全部位图像素System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat);byte temp = 0;//启用不安全模式unsafe{//得到首地址byte* ptr = (byte*)(bmpData.Scan0);//二维图像循环for (int i = 0; i < bmpData.Height; i++){for (int j = 0; j < bmpData.Width; j++){//利用公式计算灰度值temp = (byte)(0.299 * ptr[2] + 0.587 * ptr[1] + 0.114 * ptr[0]);//R=G=Bptr[0] = ptr[1] = ptr[2] = temp;//指向下一个像素ptr += 3;}//指向下一行数组的首个字节ptr += bmpData.Stride - bmpData.Width * 3;}}//解锁位图像素curBitmpap.UnlockBits(bmpData);//关闭计时器myTimer.Stop();//在TextBox内显示计时时间timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒";//对窗体进行重新绘制,这将强制执行Paint事件处理程序Invalidate();}}/// <summary>/// 内存法(仅适用于512*512的图像)/// </summary>//private void memory_Click(object sender, EventArgs e)//{// if (curBitmpap != null)// {// //位图矩形// Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height);// //以可读写的方式锁定全部位图像素// System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat);// //得到首地址// IntPtr ptr = bmpData.Scan0;// //24位bmp位图字节数// int bytes = curBitmpap.Width * curBitmpap.Height * 3;// //定义位图数组// byte[] rgbValues = new byte[bytes];// //复制被锁定的位图像素值到该数组内// System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);// //灰度化// double colorTemp = 0;// for(int i = 0; i < rgbValues.Length; i += 3)// {// //利用公式计算灰度值// colorTemp = rgbValues[i + 2] * 0.299 + rgbValues[i + 1] * 0.587 + rgbValues[i] * 0.114;// //R=G=B// rgbValues[i]=rgbValues[i+1]=rgbValues[i+2]=(byte)colorTemp;// }// //把数组复制回位图// System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);// //解锁位图像素// curBitmpap.UnlockBits(bmpData);// //对窗体进行重新绘制,这将强制执行Paint事件处理程序// Invalidate();// }//}}
}
图像灰度化
/// <summary>/// 图像灰度化/// </summary>/// <param name="bmp"></param>/// <returns></returns>public static Bitmap ToGray(Bitmap bmp){for (int i = 0; i < bmp.Width; i++){for (int j = 0; j < bmp.Height; j++){//获取该点的像素的RGB的颜色Color color = bmp.GetPixel(i, j);//利用公式计算灰度值int gray = (int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11);Color newColor = Color.FromArgb(gray, gray, gray);bmp.SetPixel(i, j, newColor);}}return bmp;}
4.灰度图像二值化:
在进行了灰度化处理之后,图像中的每个象素只有一个值,那就是象素的灰度值。它的大小决定了象素的亮暗程度。为了更加便利的开展下面的图像处理操作,还需要对已经得到的灰度图像做一个二值化处理。
图像的二值化就是把图像中的象素根据一定的标准分化成两种颜色。在系统中是根据象素的灰度值处理成黑白两种颜色。和灰度化相似的,图像的二值化也有很多成熟的算法。它可以采用自适应阀值法,也可以采用给定阀值法。
#region Otsu阈值法二值化模块 /// <summary> /// Otsu阈值 /// </summary> /// <param name="b">位图流</param> /// <returns></returns> public Bitmap OtsuThreshold(Bitmap b){// 图像灰度化 // b = Gray(b); int width = b.Width;int height = b.Height;byte threshold = 0;int[] hist = new int[256];int AllPixelNumber = 0, PixelNumberSmall = 0, PixelNumberBig = 0;double MaxValue, AllSum = 0, SumSmall = 0, SumBig, ProbabilitySmall, ProbabilityBig, Probability;BitmapData data = b.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);unsafe{byte* p = (byte*)data.Scan0;int offset = data.Stride - width * 4;for (int j = 0; j < height; j++){for (int i = 0; i < width; i++){hist[p[0]]++;p += 4;}p += offset;}b.UnlockBits(data);}//计算灰度为I的像素出现的概率 for (int i = 0; i < 256; i++){AllSum += i * hist[i]; // 质量矩 AllPixelNumber += hist[i]; // 质量 }MaxValue = -1.0;for (int i = 0; i < 256; i++){PixelNumberSmall += hist[i];PixelNumberBig = AllPixelNumber - PixelNumberSmall;if (PixelNumberBig == 0){break;}SumSmall += i * hist[i];SumBig = AllSum - SumSmall;ProbabilitySmall = SumSmall / PixelNumberSmall;ProbabilityBig = SumBig / PixelNumberBig;Probability = PixelNumberSmall * ProbabilitySmall * ProbabilitySmall + PixelNumberBig * ProbabilityBig * ProbabilityBig;if (Probability > MaxValue){MaxValue = Probability;threshold = (byte)i;}}return this.Threshoding(b, threshold);} // end of OtsuThreshold 2 #endregion#region 固定阈值法二值化模块public Bitmap Threshoding(Bitmap b, byte threshold){int width = b.Width;int height = b.Height;BitmapData data = b.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);unsafe{byte* p = (byte*)data.Scan0;int offset = data.Stride - width * 4;byte R, G, B, gray;for (int y = 0; y < height; y++){for (int x = 0; x < width; x++){R = p[2];G = p[1];B = p[0];gray = (byte)((R * 19595 + G * 38469 + B * 7472) >> 16);if (gray >= threshold){p[0] = p[1] = p[2] = 255;}else{p[0] = p[1] = p[2] = 0;}p += 4;}p += offset;}b.UnlockBits(data);return b;}}#endregion
3.相关链接
1.C#三种性能分析计时器介绍
相关文章:

C#数字图像处理(一)
文章目录 1.C#图像处理基础1.1 Bitmap类1.2 Bitmapdata类1.3 Graphics类1.4 Image类 2.彩色图像灰度化1.提取像素法2.内存法3.指针法三种方法的比较4.灰度图像二值化: 3.相关链接 Bitmap类、 Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C# 进行…...

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力
麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授,介绍了一种新方法,可以让机器人在扫描的家庭环境模拟中接受训练,为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点,研…...

从零实现本地语音识别(FunASR)
FunASR 是达摩院开源的综合性语音处理工具包,提供语音识别(ASR)、语音活动检测(VAD)、标点恢复(PUNC)等全流程功能,支持多种主流模型(如 Paraformer、Whisper、SenseVoic…...
Vue 项目中 Sass 与 Less 的对比
文章目录 一、核心特性对比二、Vue 项目集成方案三、性能关键指标四、选型决策矩阵五、Vue 3 最佳实践六、构建优化建议最终建议一、核心特性对比 特性Sass/SCSSLess语法扩展.scss(类CSS语法)类似CSS,更接近原生变量系统$variable@variable嵌套规则支持(含属性嵌套)支持Mixi…...
Python爬虫实战:研究CherryPy库相关技术
1. 引言 1.1 研究背景与意义 随着互联网信息的爆炸式增长,如何高效地获取、组织和利用网络信息成为重要研究方向。网络爬虫作为自动采集网页内容的关键技术,被广泛应用于搜索引擎构建、市场调研、数据挖掘等领域。同时,将采集到的数据以 Web 服务的形式提供,能够为用户提…...

已解决:.NetCore控制台程序(WebAPI)假死,程序挂起接口不通
本问题已得到解决,请看以下小结: 关于《.NetCore控制台程序(WebAPI)假死,程序暂停接口不通》的解决方案 记录备注报错时间2025年报错版本VS2022 WINDOWS10报错复现鼠标点一下控制台,会卡死报错描述——报错截图——报错原因 控制台启用了“快…...

Excel如何分开查看工作表方便数据撰写
首先我这里有2class和3class两个工作表 接下来我们点击视图 按照顺序分别点击新建窗口和全部重排 ### 然后就是这样 接下来就OK了...

微软技术赋能:解锁开发、交互与数据潜力,共探未来创新路
在微软 Build 2025 大会以及创想未来峰会上,微软展示的一系列前沿技术与创新应用,不仅展现了其在科技领域的深厚底蕴与前瞻视野,更为开发者和企业带来了前所未有的机遇与变革动力。 领驭科技作为微软中国南区核心合作伙伴及 HKCSP 1T 首批授…...

VR看房系统,新生代看房新体验
VR看房系统的概念 虚拟现实(VirtualReality,VR)看房系统,是近年来随着科技进步在房地产行业中兴起的一种创新看房方式。看房系统利用先进的计算机技术模拟出一个三维环境,使用户能够身临其境地浏览和体验房源,无需亲自…...

【Linux笔记】Shell-脚本(下)|(常用命令详细版)
在(上)篇,我们详细的讲解了Shell脚本的基础知识和些许命令与实验,这次的的(下)篇,我们会详细讲解Shell脚本的常用命令 关于脚本的基础知识请各位移步到(上)篇啦~ Shell…...

钉钉热点实时推送助理-思路篇
以下是针对热点实时推送助理的功能描述,结合机器学习技术栈与用户场景的通俗化解释: 快速体验的话直接用钉钉扫描下方二维码体验 1. 核心功能 (1)热点抓取引擎 类比:像蜘蛛爬取全网信息(网络爬虫信息抽取…...
RuoYi前后端分离框架实现前后端数据传输加密(一)之后端篇
一、背景 项目采用RuoYi前后端分离框架搭建,版本为3.8.9。为确保数据传输安全性,提高爬虫获取数据的门槛,领导要求系统指定的字段在API通信过程中要实现加密传输,但未对算法类型做具体要求,本人基于目前的新创的大环境考虑,采用了SM4对称加密算法对系统指定字段进行加密…...
第七十篇 从餐厅后厨到电影院选座:生活场景拆解Java并发编程核心
目录 一、并发基础:餐厅后厨的协作艺术1.1 厨师与线程(Thread)1.2 共享资源竞争:唯一的炒锅1.3 线程状态转换:厨师工作流 二、线程同步:电影院选座中的锁机制2.1 同步锁(synchronized࿰…...
深入理解设计模式之代理模式
深入理解设计模式之:代理模式 一、什么是代理模式? 代理模式(Proxy Pattern)是一种结构型设计模式。它为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用,可以在不改变目标…...
8位单通道数据保存为JPG
如何将单通道8位灰度数据(0黑~255白)直接保存为JPG文件? 这里提供两种最实用方案:轻量级STB库(推荐)和OpenCV方案(已有环境适用) STB方案 - 推荐 //https://github.com/nothings/…...
【Java实战】低侵入的线程池值传递
欢迎来到啾啾的博客🐱。 记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。 有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。 目录 引言InheritableThreadLocalAlibaba Transmittab…...

实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.11 R语言解题
本文是实验设计与分析(第6版,Montgomery著,傅珏生译) 第5章析因设计引导5.7节思考题5.11 R语言解题。主要涉及方差分析,正态假设检验,残差分析,交互作用图。 dataframe<-data.frame( densityc(570,565,…...
c++复习_第一天(引用+小众考点)
https://en.cppreference.com/w/cpp/io/manip 参考一下,这一部分比较基础,所以就一遍过 eg1:转16进制 #include<iostream> #include<iomanip> using namespace std;int main() {int n;cout << "请输入一个整数:";cin >> n;cou…...

《软件工程》实战— 在线教育平台开发
一、项目概述 1.1 项目背景与目标 随着教育数字化转型加速,传统教育模式逐渐向线上迁移,教育机构急需一个支持多终端访问、实时互动及高并发场景稳定运行的在线教育平台。本项目旨在构建学生、教师、管理员三位一体的协作教学环境,实现 50-2…...
Unity中的JsonManager
1.具体代码 先贴代码 using LitJson; using System.IO; using UnityEngine;/// <summary> /// 序列化和反序列化Json时 使用的是哪种方案 有两种 JsonUtility 不能直接序列化字典 ligJson可以序列化字典 /// </summary> public enum JsonType {JsonUtilit…...
《AI大模型的开源与性能优化:DeepSeek R1的启示》
以下是一篇基于今日新闻的技术博客文章: 在AI大模型领域,开源与性能优化一直是推动技术进步的关键因素。2025年5月28日,DeepSeek开源了其R1最新0528版本,这一事件不仅引发了行业关注,也为我们提供了深入探讨AI大模型技…...
Java-代码段-http接口调用自身服务中的其他http接口(mock)-并建立socket连接发送和接收报文实例
最新版本更新 https://code.jiangjiesheng.cn/article/367?fromcsdn 推荐 《高并发 & 微服务 & 性能调优实战案例100讲 源码下载》 1. controller入口 ApiOperation("模拟平台端现场机socket交互过程,需要Authorization")PostMapping(path "/testS…...

iOS 使用CocoaPods 添加Alamofire 提示错误的问题
Sandbox: rsync(59817) deny(1) file-write-create /Users/aaa/Library/Developer/Xcode/DerivedData/myApp-bpwnzikesjzmbadkbokxllvexrrl/Build/Products/Debug-iphoneos/myApp.app/Frameworks/Alamofire.framework/Alamofire.bundle把这个改成 no 2 设置配置文件...

Python打卡训练营学习记录Day41
DAY 41 简单CNN 知识回顾 数据增强卷积神经网络定义的写法batch归一化:调整一个批次的分布,常用与图像数据特征图:只有卷积操作输出的才叫特征图调度器:直接修改基础学习率 卷积操作常见流程如下: 1. 输入 → 卷积层 →…...
单链表反序实现
这个算法题有两种实现方式,一种是迭代,就是循环,还有一种是递归实现 迭代实现 迭代实现原理上是在一个循环如for中依次将一个节点的方向改变达到原地反序的实现 迭代法的核心是使用三个指针(prev, curr, next)逐个…...

C++深入类与对象
在上一篇中提到了构造函数,那么这篇再来提一下构造函数,编译器自动生成的默认构造函数对于内置类型不做处理,自定义类型会调用它自己的构造函数。对于自己写的构造函数,之前是在函数体中初始化,当然不止这一种初始化&a…...
机器学习算法04:SVC 算法(向量机分类)
目录 一、算法核心特点 二、使用场景 三、代码示例(以 Python 的 scikit - learn 库为例) 四、与其他分类算法对比 SVC 即 Support Vector Classification,是支持向量机(SVM)在分类任务中的具体实现。在你正在阅读…...
Fragment事务commit与commitNow区别
在 Android 的 Fragment 事务处理中,commit() 和 commitNow() 是两种提交事务的方式,它们的区别主要体现在执行时机、事务顺序和兼容性等方面。以下是它们的核心区别: 1. 执行时机 commit() 将事务异步加入主线程的待执行队列。不会立即执行&…...
LVS-DR高可用-Keepalived
目录 Keepalved双机热备 核心概念 关键组件 工作流程 实例环境 配置keepalived Web服务器配置 Keepalved双机热备 Keepalived双机热备是一种基于VRRP(Virtual Router Redundancy Protocol,虚拟路由冗余协议)实现的高可用性解决方案&am…...

阿里云服务器邮件发送失败(dail tcp xxxx:25: i/o timeout)因为阿里云默认禁用 25 端口
最近在测试发送邮件的功能,发现了一个奇怪的问题,同样的 docker 镜像,在本地跑起来是可以正常发送邮件的,但是在阿里云的服务器上跑,就会报错 i/o timeout。 排查了一圈发现,原来是阿里云的操作࿰…...