C#数字图像处理(一)

news2025/6/2 13:06:17

文章目录

  • 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 Pixel方法和 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枚举提供了对数据的访问方式。

表2.1所示是它的成员;

在这里插入图片描述
  Pixelformat枚举表示像素的格式,

表2.2所示是它的主要成员

在这里插入图片描述
  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);
  1. 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)
        {
            //创建OpenFileDialog
            OpenFileDialog 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;

            //调用SaveFileDialog
            SaveFileDialog 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 GetpixelBitmapSetpixel方法。为了将位图的颜色设置为灰度或其他颜色,就需要使用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=ret
                        curBitmpap.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=B
                        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);
                //解锁位图像素
                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=B
                            ptr[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)
        {
            //创建OpenFileDialog
            OpenFileDialog 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;

            //调用SaveFileDialog
            SaveFileDialog 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=ret
                        curBitmpap.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=B
                        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);
                //解锁位图像素
                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=B
                            ptr[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#三种性能分析计时器介绍

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2394383.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力

麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授&#xff0c;介绍了一种新方法&#xff0c;可以让机器人在扫描的家庭环境模拟中接受训练&#xff0c;为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点&#xff0c;研…

从零实现本地语音识别(FunASR)

FunASR 是达摩院开源的综合性语音处理工具包&#xff0c;提供语音识别&#xff08;ASR&#xff09;、语音活动检测&#xff08;VAD&#xff09;、标点恢复&#xff08;PUNC&#xff09;等全流程功能&#xff0c;支持多种主流模型&#xff08;如 Paraformer、Whisper、SenseVoic…

已解决:.NetCore控制台程序(WebAPI)假死,程序挂起接口不通

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

Excel如何分开查看工作表方便数据撰写

首先我这里有2class和3class两个工作表 接下来我们点击视图 按照顺序分别点击新建窗口和全部重排 ### 然后就是这样 接下来就OK了

微软技术赋能:解锁开发、交互与数据潜力,共探未来创新路

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

VR看房系统,新生代看房新体验

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

【Linux笔记】Shell-脚本(下)|(常用命令详细版)

在&#xff08;上&#xff09;篇&#xff0c;我们详细的讲解了Shell脚本的基础知识和些许命令与实验&#xff0c;这次的的&#xff08;下&#xff09;篇&#xff0c;我们会详细讲解Shell脚本的常用命令 关于脚本的基础知识请各位移步到&#xff08;上&#xff09;篇啦~ Shell…

钉钉热点实时推送助理-思路篇

以下是针对热点实时推送助理的功能描述&#xff0c;结合机器学习技术栈与用户场景的通俗化解释&#xff1a; 快速体验的话直接用钉钉扫描下方二维码体验 1. 核心功能 &#xff08;1&#xff09;热点抓取引擎 类比&#xff1a;像蜘蛛爬取全网信息&#xff08;网络爬虫信息抽取…

实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.11 R语言解题

本文是实验设计与分析&#xff08;第6版&#xff0c;Montgomery著&#xff0c;傅珏生译) 第5章析因设计引导5.7节思考题5.11 R语言解题。主要涉及方差分析&#xff0c;正态假设检验&#xff0c;残差分析&#xff0c;交互作用图。 dataframe<-data.frame( densityc(570,565,…

《软件工程》实战— 在线教育平台开发

一、项目概述 1.1 项目背景与目标 随着教育数字化转型加速&#xff0c;传统教育模式逐渐向线上迁移&#xff0c;教育机构急需一个支持多终端访问、实时互动及高并发场景稳定运行的在线教育平台。本项目旨在构建学生、教师、管理员三位一体的协作教学环境&#xff0c;实现 50-2…

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归一化&#xff1a;调整一个批次的分布&#xff0c;常用与图像数据特征图&#xff1a;只有卷积操作输出的才叫特征图调度器&#xff1a;直接修改基础学习率 卷积操作常见流程如下&#xff1a; 1. 输入 → 卷积层 →…

C++深入类与对象

在上一篇中提到了构造函数&#xff0c;那么这篇再来提一下构造函数&#xff0c;编译器自动生成的默认构造函数对于内置类型不做处理&#xff0c;自定义类型会调用它自己的构造函数。对于自己写的构造函数&#xff0c;之前是在函数体中初始化&#xff0c;当然不止这一种初始化&a…

阿里云服务器邮件发送失败(dail tcp xxxx:25: i/o timeout)因为阿里云默认禁用 25 端口

最近在测试发送邮件的功能&#xff0c;发现了一个奇怪的问题&#xff0c;同样的 docker 镜像&#xff0c;在本地跑起来是可以正常发送邮件的&#xff0c;但是在阿里云的服务器上跑&#xff0c;就会报错 i/o timeout。 排查了一圈发现&#xff0c;原来是阿里云的操作&#xff0…

力扣HOT100之动态规划:322. 零钱兑换

这道题和上一道题279.完全平方数的套路是完全一样的&#xff0c;但是这道题不需要我们自己生成物品列表&#xff0c;函数的输入中已经给出了&#xff0c;但是这道题有一个坑&#xff0c;就是我们在初始化dp数组的时候&#xff0c;所有的位置不应该赋值为INT_MAX&#xff0c;因为…

电商售后服务系统与其他系统集成:实现售后流程自动化

在竞争激烈的电商市场中&#xff0c;优质的售后服务对于提升用户满意度和忠诚度至关重要。然而&#xff0c;售后服务流程通常涉及多个环节和系统&#xff0c;如何高效地管理这些流程&#xff0c;减少人工干预&#xff0c;提升服务效率&#xff0c;是电商企业亟待解决的问题。电…

kafka学习笔记(三、消费者Consumer使用教程——消费性能多线程提升思考)

1.简介 KafkaConsumer是非线程安全的&#xff0c;它定义了一个acquire()方法来检测当前是否只有一个线程在操作&#xff0c;如不是则会抛出ConcurrentModifcationException异常。 acquire()可以看做是一个轻量级锁&#xff0c;它仅通过线程操作计数标记的方式来检测线程是否发…

[JVM] JVM内存调优

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

秒出PPT正式改名秒出AI,开启AI赋能新体验!

在现代办公环境中&#xff0c;借助智能工具提升工作效率已经成为趋势。秒出AI作为一款集AI PPT制作、动画、巨幕、视频、设计以及智能简历功能于一体的综合办公平台&#xff0c;为用户提供一站式智能内容生成解决方案&#xff0c;极大地简化了内容创作流程。 1. AI驱动的一键P…

VM改MAC电脑密码(截图)

进入恢复模式重置密码 重启mac并同时按下CommandR&#xff0c;进入恢复模式。进入「菜单栏-实用程序-终端」&#xff0c;输入命令「resetpassword」回车运行&#xff0c;调出密码重置工具。选择包含密码的启动磁盘卷宗、需重设密码的用户账户&#xff1b;输入并确认新的用户密…