C# 进行图像处理的几种方法(Bitmap,BitmapData,IntPtr)

在C#中,进行图像处理时主要会使用到 System.Drawing 命名空间中的几个关键类,其中Bitmap、BitmapData和IntPtr是进行高效像素操作的重要工具。以下是如何利用这些类进行图像处理的方法概述:

  1. Bitmap 类:

    • System.Drawing.Bitmap 是一个封装了位图数据的类,它允许你加载、保存、显示和操作图像文件或内存中的位图资源。
      • 使用 new Bitmap(width, height, pixelFormat) 创建一个新的空白位图。
      • 通过 Bitmap.FromFile(path) 或 Bitmap.FromStream(stream) 加载图片文件。
      • 提供 GetPixel(x, y) 和 SetPixel(x, y, color) 方法用于获取和设置单个像素的颜色,但请注意,这种方法对于大规模图像处理效率较低。
  2. BitmapData 类:

    • BitmapData 代表了一个锁定的Bitmap对象的像素数据缓冲区,可以直接对像素进行读写操作以提高性能。
      • 使用 Bitmap.LockBits(Rectangle area, ImageLockMode mode, PixelFormat format, out BitmapData data) 方法可以锁定Bitmap的部分或全部区域,从而获得指向该区域像素数据的指针。
      • data.Scan0 属性是一个IntPtr类型,指向第一个像素的数据地址。
      • 解锁位图数据需要调用 Bitmap.UnlockBits(BitmapData) 来释放资源并确保图形设备正确更新。
  3. IntPtr 类型与 Marshal 类:

    • IntPtr 是一个表示非托管指针(即内存地址)的数据类型。
    • 在处理BitmapData时,通常会将Scan0属性所指向的内存块直接映射到托管代码中,以便于进行快速的像素操作。
      • 使用 System.Runtime.InteropServices.Marshal.Copy(IntPtr source, byte[] destination, int startIndex, int length) 将未托管的内存块内容复制到托管数组中,这样可以在C#中更方便地进行像素遍历和修改。
      • 修改完成后,可以通过类似方法将修改后的数组内容复制回BitmapData所指向的内存区域。

综合以上,一个高效的图像处理流程可能是这样的:

  • 创建或加载Bitmap对象。
  • 锁定Bitmap的像素数据,得到BitmapData。
  • 使用Marshal.Copy将BitmapData的像素数据复制到本地数组中。
  • 对数组进行所需的图像处理操作(如:调整亮度、对比度、滤镜等)。
  • 再次使用Marshal.Copy将处理过的数组数据复制回BitmapData的内存中。
  • 解锁BitmapData,使得GDI+能够自动更新对应的Bitmap图像。

这种方式避免了频繁调用GetPixel和SetPixel函数带来的性能瓶颈,实现了更快的图像处理速度。

 

Bitmap类

命名空间:System.Drawing

封装 GDI+ 位图,此位图由图形图像及其属性的像素数据组成。

Bitmap 是用于处理由像素数据定义的图像的对象。 

利用C#类进行图像处理,最方便的是使用Bitmap类,使用该类的GetPixel()与SetPixel()来访问图像的每个像素点。下面是MSDN中的示例代码:

public void GetPixel_Example(PaintEventArgs e)   
{   // Create a Bitmap object from an image file.   Bitmap myBitmap = new Bitmap("Grapes.jpg");   // Get the color of a pixel within myBitmap.   Color pixelColor = myBitmap.GetPixel(50, 50);   // Fill a rectangle with pixelColor.   SolidBrush pixelBrush = new SolidBrush(pixelColor);   e.Graphics.FillRectangle(pixelBrush, 0, 0, 100, 100);   
}  

可见,Bitmap类使用一种优雅的方式来操作图像,但是带来的性能的降低却是不可忽略的。比如对一个800*600的彩色图像灰度化,其耗费的时间都要以秒为单位来计算。在实际项目中进行图像处理,这种速度是决对不可忍受的。

 

BitmapData类

命名空间:System.Drawing.Imaging

指定位图图像的属性。BitmapData 类由 Bitmap 类的 LockBits 和 UnlockBits 方法使用。不可继承。

    好在我们还有BitmapData类,通过BitmapData BitmapData LockBits ( )可将 Bitmap 锁定到系统内存中。该类的公共属性有:

  • Width           获取或设置 Bitmap 对象的像素宽度。这也可以看作是一个扫描行中的像素数。
  • Height          获取或设置 Bitmap 对象的像素高度。有时也称作扫描行数。
  • PixelFormat  
    获取或设置返回此 BitmapData 对象的 Bitmap 对象中像素信息的格式。
  • Scan0            获取或设置位图中第一个像素数据的地址。它也可以看成是位图中的第一个扫描行。
  • Stride            获取或设置 Bitmap 对象的跨距宽度(也称为扫描宽度)。

    下面的MSDN中的示例代码演示了如何使用 PixelFormat、Height、Width 和 Scan0 属性;LockBits 和 UnlockBits 方法;以及 ImageLockMode 枚举。

private void LockUnlockBitsExample(PaintEventArgs e)   
{  // Create a new bitmap.   Bitmap bmp = new Bitmap("c:\\fakePhoto.jpg");  // Lock the bitmap‘s bits.    Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);   System.Drawing.Imaging.BitmapData bmpData =   bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,   bmp.PixelFormat);   // Get the address of the first line.   IntPtr ptr = bmpData.Scan0;  // Declare an array to hold the bytes of the bitmap.   int bytes  = bmpData.Stride * bmp.Height;   byte[] rgbValues = new byte[bytes];  // Copy the RGB values into the array.   System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);  // Set every red value to 255.    for (int counter = 0; counter < rgbValues.Length; counter+=3)   rgbValues[counter] = 255;   // Copy the RGB values back to the bitmap   System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);  // Unlock the bits.   bmp.UnlockBits(bmpData);  // Draw the modified image.   e.Graphics.DrawImage(bmp, 0, 150);  
}  或Bitmap bitmap = new Bitmap("image.jpg");
BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
// 对 BitmapData 进行图像处理操作
bitmap.UnlockBits(bitmapData);

上面的代码演示了如何用数组的方式来访问一幅图像,而不在使用低效的GetPixel()和SetPixel()。

IntPtr

使用 IntPtr 可以直接操作图像的内存数据。

通过获取图像的句柄(IntPtr),可以使用指针操作来访问和修改图像的像素值。

这种方法需要更高级的编程技能和对内存管理的了解。

Bitmap bitmap = new Bitmap("image.jpg");
IntPtr ptr = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
// 使用 IntPtr 进行图像处理操作
bitmap.UnlockBits(ptr);

 

unsafe代码

    而在实际中上面的做法仍然不能满足我们的要求,图像处理是一种运算量比较大的操作,不同于我们写的一般的应用程序。我们需要的是一种性能可以同C++程序相媲美的图像处理程序。C++是怎么提高效率的呢,答曰:指针。幸运的是.Net也允许我们使用指针,只能在非安全代码块中使用指针。何谓非安全代码?

    为了保持类型安全,默认情况下,C# 不支持指针运算。不过,通过使用 unsafe 关键字,可以定义可使用指针的不安全上下文。在公共语言运行库 (CLR) 中,不安全代码是指无法验证的代码。C# 中的不安全代码不一定是危险的,只是其安全性无法由 CLR 进行验证的代码。因此,CLR 只对在完全受信任的程序集中的不安全代码执行操作。如果使用不安全代码,由您负责确保您的代码不会引起安全风险或指针错误。不安全代码具有下列属性:

  • 方法、类型和可被定义为不安全的代码块。
  • 在某些情况下,通过移除数组界限检查,不安全代码可提高应用程序的性能。
  • 当调用需要指针的本机函数时,需要使用不安全代码。
  • 使用不安全代码将引起安全风险和稳定性风险。
  • 在 C# 中,为了编译不安全代码,必须用 /unsafe 编译应用程序。

    正如《C#语言规范》中所说无论从开发人员还是从用户角度来看,不安全代码事实上都是一种“安全”功能。不安全代码必须用修饰符 unsafe 明确地标记,这样开发人员就不会误用不安全功能,而执行引擎将确保不会在不受信任的环境中执行不安全代码。

    以下代码演示如何借助BitmapData类采用指针的方式来遍历一幅图像,这里的unsafe代码块中的代码就是非安全代码。

//创建图像   
Bitmap image =  new Bitmap( "c:\\images\\image.gif" );   
//获取图像的BitmapData对像   
BitmapData data = image.LockBits( new Rectangle( 0 , 0 , image.Width , image.Height ) , ImageLockMode.ReadWrite  , PixelFormat.Format24bppRgb  );    
//循环处理   
unsafe   
{    byte* ptr = ( byte* )( data.Scan0 );    for( int i = 0 ; i < data.Height ; i ++ )   {   for( int j = 0 ;  j < data.Width ;  j ++ )   {   // write the logic implementation here   ptr += 3;     }   ptr += data.Stride - data.Width * 3;   }   
}  

毫无疑问,采用这种方式是最快的,所以在实际工程中都是采用指针的方式来访问图像像素的。

字节对齐问题 
    上例中ptr += data.Stride - data.Width * 3,表示跨过无用的区域,其原因是图像数据在内存中存储时是按4字节对齐的,具体解释如下:

    假设有一张图片宽度为6,假设是Format24bppRgb格式的(每像素3字节,在以下的讨论中,除非特别说明,否则Bitmap都被认为是24位RGB)。显然,每一行需要6*3=18个字节存储。对于Bitmap就是如此。但对于BitmapData,虽然data.Width还是等于image.Width,但大概是出于显示性能的考虑,每行的实际的字节数将变成大于等于它的那个离它最近的4的整倍数,此时的实际字节数就是Stride。就此例而言,18不是4的整倍数,而比18大的离18最近的4的倍数是20,所以这个data.Stride = 20。显然,当宽度本身就是4的倍数时,data.Stride = image.Width * 3。

    画个图可能更好理解。R、G、B 分别代表3个原色分量字节,BGR就表示一个像素。为了看起来方便我在们每个像素之间插了个空格,实际上是没有的。X表示补足4的倍数而自动插入的字节。为了符合人类的阅读习惯我分行了,其实在计算机内存中应该看成连续的一大段。

|-------Stride-----------| 
|-------Width---------| 
Scan0: 
BGR BGR BGR BGR BGR BGR XX 
BGR BGR BGR BGR BGR BGR XX 
BGR BGR BGR BGR BGR BGR XX 

    首先用data.Scan0找到第0个像素的第0个分量的地址,这个地址指向的是个byte类型,所以当时定义为byte* ptr。行扫描时,在当前指针位置(不妨看成当前像素的第0个颜色分量)连续取出三个值(3个原色分量。注意,0 1 2代表的次序是B G R。在取指针指向的值时,貌似p[n]和p += n再取p[0]是等价的),然后下移3个位置(ptr += 3,看成指到下一个像素的第0个颜色分量)。做过Bitmap.Width次操作后,就到达了Bitmap.Width * 3的位置,应该要跳过图中标记为X的字节了(共有Stride - Width * 3个字节),代码中就是 ptr += dataIn.Stride - dataIn.Width * 3。

    通过阅读本文,相信你已经对使用C#进行图像处理可能用到的几种方法有了一个了解。至于采用哪种方式,取决于你的性能要求。其中第一种方式最优雅;第三种方式最快,但不是安全代码;第二种方式取了个折中,保证是安全代码的同时又提高了效率。熟悉C/C++编程的人可能会比较偏向于第三种方式,我个人也比较喜欢第三种方式。

 

参考 http://blog.sina.com.cn/s/blog_628821950100wh9w.html

通义千问、文心一言、豆包

 

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

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

相关文章

Redis黑马点评业务总结(含mac m1pro | windows11 wsl2 ubuntu环境配置 持续更新中~)

redis黑马点评项目分析业务学习笔记 含项目配置教学mac m1pro windows mac M1pro环境配置windows11 wsl2 ubuntu 环境配置一.短信登录1. 1发送验证码1.2短信登录注册1.3登录校验拦截器补缺Cookie Session Token1.4基于redistoken认证实现短信登陆1.5完善token认证的刷新机制 二…

STM32F103C8T6(HAL库函数 - 内部Flash操作)

简介 STM32F103C8T6 内部Flash 为 64KB&#xff0c;本次将对他多余空间进行读写。 介绍 数据手册下载 STM32F103x8/STM32F103xB 数据手册 包含Flash Memory Page分布 STM32F设备命名 设备容量类型 中容量类型 内部空间介绍 64 KBytes大小Flash Memory 从 0x0800 0000 ~…

【BIOS实战】

文章目录 网络通信编程基本常识Java原生网络编程-BIO 网络通信编程基本常识 什么是Socket&#xff1f; Socket是应用层与TCP/IP协议族通信的中间软件抽象层&#xff0c;它是一组接口&#xff0c;一般由操作系统提供。 在设计模式中&#xff0c;Socket其实就是一个门面模式&…

Leetcod面试经典150题刷题记录 —— 二叉搜索树篇

Leetcod面试经典150题刷题记录-系列Leetcod面试经典150题刷题记录——数组 / 字符串篇Leetcod面试经典150题刷题记录 —— 双指针篇Leetcod面试经典150题刷题记录 —— 矩阵篇Leetcod面试经典150题刷题记录 —— 滑动窗口篇Leetcod面试经典150题刷题记录 —— 哈希表篇Leetcod面…

vue3 封裝一个常用固定按钮组件(添加、上传、下载、删除)

效果图 这个组件只有四个按钮&#xff0c;添加&#xff0c;上传、下载、删除&#xff0c;其中删除按钮的颜色默认是灰色&#xff0c;当表格有数据选中时再变成红色 实现 组件代码 <script lang"ts" setup> import { Icon } from /components/Icon/index im…

【Gin实战教程】快速入门

Gin是一个轻量级的Web框架&#xff0c;使用Go语言开发。它具有高性能、易用性和灵活性的特点&#xff0c;是构建可扩展的Web应用程序的理想选择。 首先&#xff0c;Gin是一个高性能的框架。它基于Go语言的原生HTTP包进行开发&#xff0c;利用了Go语言的并发特性和协程模型&…

spark-sql字段血缘实现

spark-sql字段血缘实现 背景 Apache Spark是一个开源的大数据处理框架&#xff0c;它提供了一种高效、易于使用的方式来处理大规模数据集。在Spark中&#xff0c;数据是通过DataFrame和Dataset的形式进行操作的&#xff0c;这些数据结构包含了一系列的字段&#xff08;也称为…

在ASP.NET MVC中使用JQuery提供的弹出窗口(模态窗口)

在ASP.NET MVC中使用JQuery提供的弹出窗口&#xff08;模态窗口&#xff09; 原理 使用<div>图层灵活显示在浏览器的任何位置。默认情况下指定<div>不可见 引用 样式表 在JQuery的官方网站可以下载对应的css样式表。打开官网的样例页。 找到样式表引用路径 …

flex弹性盒子常用的布局属性详解

想必大家在开发中经常会用到flex布局。而且还会经常用到 justify-content 属性实现分栏等等 接下来给大家分别讲一下 justify-content 的属性值。 以下是我敲的效果图大家可以清晰看出区别 space-between 属性值可以就是说两端对齐 space-evenly 属性值是每个盒子之间的…

Requests库的接口测试实现

Requests库是在接口测试中被广泛运用的库&#xff0c;包括模拟请求的下发&#xff0c;请求相关配置和响应结果的获取&#xff0c;核心主体都是通过request库完成。在接口测试中使用非常频繁。 一、Requests库环境搭建 接口测试的核心从模拟请求开始。在Python中&#xff0c;通…

15个等轴视图设计的电动车汽车无人机等PR剪辑素材视频制作元素

包含15个等轴视图、等距视角电动车、汽车、无人机、沙漏、飞机等PR剪辑素材视频制作元素mogrt动画模板。 特征&#xff1a; 等距设计&#xff1b; 可以更改颜色&#xff1b; 分辨率&#xff1a;全高清&#xff08;19201080&#xff09;&#xff1b; 持续时间&#xff1a;15秒&a…

IDEA+Git——项目分支管理

IDEAGit——项目分支管理 1. 前言2. 基础知识点2.1. 分支区分2.2. Git 代码提交规范2.3. 四个工作区域2.4. 文件的四种状态2.5. 常用命令2.6 注重点 3. IDEA分支管理 1. 前言 在Git中&#xff0c;分支是项目的不同版本&#xff0c;当开始开发一个新项目时&#xff0c;主分支通常…

白话编程-概述

前言: 编程作为一种纯理性的学科,时刻都要明白自己到底在干什么.程序方面的书很多,却难以有那种把复杂问题讲清楚,让人很容易理解的.笔者想建立一种程序方面简单的思维,便于理解和记忆. 目录 一.概述 二.面向过程和面向对象 一.概述 编程到底是在干什么? 1)编程大多数情况…

使用命令行方式搭建uni-app + Vue3 + Typescript + Pinia + Vite + Tailwind CSS + uv-ui开发脚手架

使用命令行方式搭建uni-app Vue3 Typescript Pinia Vite Tailwind CSS uv-ui开发脚手架 项目代码以上传至码云&#xff0c;项目地址&#xff1a;https://gitee.com/breezefaith/uniapp-vue3-ts-scaffold 文章目录 使用命令行方式搭建uni-app Vue3 Typescript Pinia V…

深入理解C/C++指针:从基本操作到复杂表达式

目录 代码展示&#xff1a; 示例1&#xff1a;指向数组结束位置之后的地址 示例2&#xff1a;结构体大小对指针运算的影响 示例3&#xff1a;访问数组元素的不同方式 示例4&#xff1a;逗号表达式在数组初始化中的应用 示例5&#xff1a;计算多维数组元素间的地址差值 示…

【Python】不一样的Ansible(一)

不一样的Ansible——进阶学习 前言正文概念Ansible CorePlugins和Modules 插件插件类型编写自定义插件基本要求插件选项文档标准编写插件 添加一个本地插件注册为内置插件指定插件目录 其他一些技巧更改Strategy 结语 前言 Ansible 是一个极其简单的 IT 自动化引擎&#xff0c…

【Unity】动态申请权限

1、AndroidManifest.xml在<application></application>内添加一行&#xff1a; <meta-data android:name"unityplayer.SkipPermissionsDialog" android:value"true" /> 作用&#xff1a;屏蔽应用启动时弹出申请权限弹窗&#xff08;危…

签到积分--题解--c++--模拟

题目描述 每天签到一个应用程序&#xff0c;至少可以得 11 分&#xff0c;若前一天也签到了&#xff0c;则当天得分为前一天的得分加 11&#xff0c;一天最多只能得 55 分。 给定一个由 Y 及 N 构成的字符序列&#xff0c;该序列表示小爱每天签到的情况&#xff0c;签到记为 …

Windows下Redis5+可视化软件下载、安装和配置教程-2024年1月8日

Windows下Redis5下载、安装和配置教程-2024年1月8日 一、下载二、安装三、配置环境四、配置可视化客户端 一、下载 redis是现在是没有对win系统版进行维护的&#xff0c;这个是大神完成的&#xff0c;目前是到5版本&#xff0c;选择Redis-x64-5.0.14.1.zip点击下载 下载地址&…

pgAdmin和asdf postgres的安装

安装pgAdmin&#xff1a; curl https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo apt-key addsudo sh -c echo "deb https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main" > /etc/apt/sources.list.d/pgadmi…