图像处理系列教程一: GDI版图像数据的获取

谈起VB中的图像,很多人觉得VB对图像的支持太弱了,实际上,我觉得,比起VCVB对图像的编程要方便很多。神奇的AutoRedraw,多格式的图像文件支持,AutoSize让你少去不少麻烦等等,而这些在VC中都是需要不少额外的代码的,并且VB内部用底层函数对这些功能的封装,使得其执行效率亦是相当高的。那么,今天,我要给大家用一个简单的反色程序说明如何用VB实现对各种色深的图片的处理,让你对VB信心十足。
一、打开图像。

这个很简单,直接使用LoadPicture(其实就是对OleLoadPicture这个函数的封装),其支持BMP,JPG,GIF,ICO,WMF,EMF格式。这里说明一下,对于由柯达控件生成的BMP,部分GIFJPEG2000,以及32位的ICO这个函数似乎会产生一些未知的错误。JPG格式加载后,无论原始的JPG是否是8位的,在内存中VB是以24位的DIB格式图像保存的,而GIF则以8位索引色放置于内存。至于BMP格式,则可以按照原始的文件位数加载。我们可以用下面的函数来认证。

Private Function GetBitmapColorDepth(Pic As StdPicture) As Long
Dim Bmp As Bitmap
GetGDIObject Pic.Handle,Len(Bmp),Bmp
GetBitmapColorDepth = Bmp.bmBitsPixel
End Function

二、保存图像。

这个也很简单,SavePicture函数,注意,对于BMPICOWMFEMF格式,Savepicture函数能以原始格式保存,而对于加载的GIFJPG格式,只能将图像保存为BMP格式,无论你给他的路径参数的后缀是什么(看到有些VB的书上居然说将后缀改为JPG就能保存为JPG格式,真是傻)。并且该函数能保留原始的位深,这对我们来说是个好消息。那么这个函数的实现在我看来也很简单,用VB内部的语言来表达就是:
Put #FileNum,BmpInfoHeader 'BMP
文件头

Put #FileNum,mBmpInfo '
位图信息头

If mBmpInfo.biBitCount <= 8 Then Put #FileNum,ColorTable '
调色板

Put #FileNum,DibBytes '
位图数据


3
、图像数据的获得

这个地方就是大家常常说VB慢的罪魁祸首,因为VB的自带了一个四不像的PointPset函数,而这两个函数可以得到和设置图像的颜色,因此,常常会作为初学者的最爱工具,而最终的结果就是让VB落得一个骂名:龟速。
总结一下,在VB中可以用来得到图像数据的常用API函数有:Point|Pset; GetPixel|SetPixel; GetBitmapBits|SetBitmapBits; GetDIBits|SetDIBits ; SafeArray模拟指针等等。抛弃前两组不说,因为他们是一丘之貉。第三组函数因为是DDB函数,是设备相关的,个人认为不是很好,因为我体验过他莫名其妙的失败。最后一组因为其复杂性,不作为向大家推荐的函数。因此,我们重点谈谈GetDIBits

Private Declare Function GetDIBits Lib "gdi32" Alias "GetDIBits" (ByVal aHDC As Long,ByVal hBitmap As Long,ByVal nStartScan As Long,ByVal nNumScans As Long,lpBits As Any,lpBI As BITMAPINFO,ByVal wUsage As Long) As Long

谈起GetDIBits,我们重点讲讲参数lpBits,这个参数表示存储图像数据的缓冲区首地址,编程时只需要将一个数组的第一个元素赋值给他就可以了, 而如何确定这个数组的大小是值得商榷的。我们知道,对于不同的位深每个像素所占用的字节数是不同的,既然VB保留了被加载的图像的位深,那么我们在对图像进行后续处理的时候就应该按照这个位深来给图像数据缓冲区分配内存。不过,也许大家在实际的应用中并没有这样做,而是统一把BITMAPINFO.bmiHeader.biBitCount设置为32或者为24,那么这里其实GetDIBits 帮我们帮图像的原始格式的数据转换为我们所需要的数据了。


好,下面给出一个简单的处理反色的函数。


Option Explicit


Private Type BITMAPFILEHEADER
bfType As Integer
bfSize As Long
bfReserved1 As Integer
bfReserved2 As Integer
bfOffBits As Long
End Type

Private Type Bitmap
bmType As Long
bmWidth As Long
bmHeight As Long
bmWidthBytes As Long
bmPlanes As Integer
bmBitsPixel As Integer
bmBits As Long
End Type

Private Type BITMAPINFOHEADER '40 bytes
biSize As Long 'BITMAPINFOHEADER
结构的大小

biWidth As Long
biHeight As Long
biPlanes As Integer '
设备的为平面数,现在都是
1
biBitCount As Integer '
图像的颜色位图

biCompression As Long '
压缩方式

biSizeImage As Long '
实际的位图数据所占字节

biXPelsPerMeter As Long '
目标设备的水平分辨率

biYPelsPerMeter As Long '
目标设备的垂直分辨率

biClrUsed As Long '
使用的颜色数

biClrImportant As Long '
重要的颜色数。如果该项为0,表示所有颜色都是重要的

End Type

Private Type RGBQUAD '
只有bibitcount124时才有调色板

Blue As Byte '
蓝色分量

Green As Byte '
绿色分量

Red As Byte '
红色分量

Reserved As Byte '
保留值

End Type

Private Type BITMAPINFO
bmiHeader As BITMAPINFOHEADER
bmiColors As RGBQUAD
End Type

Private Const BI_RGB = 0&
Private Const DIB_RGB_COLORS = 0&
Private Declare Function GetDIBits Lib "gdi32" (ByVal aHDC As Long,ByVal wUsage As Long) As Long
Private Declare Function GetGDIObject Lib "gdi32.dll" Alias "GetObjectA" (ByVal hObject As Long,ByVal nCount As Long,ByRef lpObject As Any) As Long
Private Declare Function SetDIBits Lib "gdi32" (ByVal hDC As Long,ByVal wUsage As Long) As Long
Private Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long,ByVal hDC As Long) As Long


Private Function Invert(Pic As StdPicture) As Boolean '
Dim i As Long,hDC As Long
Dim Bmp As Bitmap,BmpInfo As BITMAPINFO
GetGDIObject Pic.Handle,Bmp
With BmpInfo.bmiHeader
.biSize = Len(BmpInfo.bmiHeader)
.biWidth = Bmp.bmWidth
.biHeight = Bmp.bmHeight
.biPlanes = 1
.biBitCount = Bmp.bmBitsPixel '
按图像实际的位深设置
,
.biCompression = BI_RGB
End With
hDC = GetDC(0)
ReDim PicData(Bmp.bmWidthBytes * Bmp.bmHeight - 1) As Byte '
按图像数据实际的大小分配缓冲区

GetDIBits hDC,Pic.Handle,Bmp.bmHeight,PicData(0),BmpInfo,DIB_RGB_COLORS
For i = 0 To UBound(PicData)
PicData(i) = 255 - PicData(i)
Next
SetDIBits hDC,DIB_RGB_COLORS
ReleaseDC 0,hDC
End Function

调用方式类似于如下: Call Invert(Picture1.picture)然后刷新一下
: Picture1.Refresh.

以上假设你加载的是一副24位的图像(不管原始是什么格式).

在以上函数中,语句.biBitCount = Bmp.bmBitsPixel 表示按照实际的位深来读取数据,而 ReDim PicData(Bmp.bmWidthBytes * Bmp.bmHeight - 1) As Byte 则表示按图像数据实际的大小分配缓冲区 ,这时的GetDIBits 我们可以理解为一个copymemory的过程。如果你把GetDIBits 那句改为 CopyMemory PicData(0),ByVal Bmp.bmBits,Bmp.bmWidthBytes * Bmp.bmHeight,得到的效果是一样的(感觉和模拟指针有点联系),其中
Bmp.bmBits
实际上就是图像在内存的首地址,类似的SetDIBits 也可以用CopyMemory 来代替。

值得说明的一点是,对于这个PicData数组的类型,不同的人可以有不同的爱好,如果.biBitCount 设置为8位及其以下位深的图像,我们没有理由将其声明为Byte之外的任何类型,若biBitCount 设置为24 32,可能有很多人喜欢或习惯将PicData声明为RGBQUAD结构类型,以方便理解每个分量的意义,这也无可非议,但是要注意在设置为24位的时候要删除RGBQUADReserved分量声明,否则数组中的数据是不对的。

另外,还有一点,对图像数据很多人喜欢用二维数据或者三维数组来记录,这样做的目的无非是数组的意义明确,我们不推荐你这样做,原因是二维数组的寻址是有系统自动完成的,在实际的操作中,有着大量的重复寻址的操作,系统确无法在这个过程中实施优化,如果是一维数组,这个操作就完全由我们掌握。

若你决定用二维或三维的数据来保存图像的数据,你很可能会在编码的过程中得到不正确的结果,这是因为你对VB的数组在内存中的保存顺序不了解。对于VB中的二维数组,比如Data(3,5),在内存中,其摆放的顺序是Data(0,0),Data(1,Data(2,Data(3,Data(0,1),1)……,不是我们常见的矩阵从左到右,然后在从上到下,而图像的数据是按照从上到下一个扫描行一个扫描行依次保存的。因此,你所用的二维数组的第一维必须是一个扫描行的大小。

简单的总结下可用的数组声明方式。

PicDataBmp.bmWidthBytes * Bmp.bmHeight - 1) As Byte ‘通用型

PicDataBmp.bmWidthBytes-1,Bmp.bmHeight - 1) As Byte ‘通用型

PicDataBmp.bmWidth-1,Bmp.bmHeight - 1) As RGBQUAD ‘只对biBitCount=32有效

PicData3,Bmp.bmWidth-1,Bmp.bmHeight - 1) As Byte ‘只对biBitCount=32有效

PicDataBmp.bmWidth-1,Bmp.bmHeight - 1) As long‘只对biBitCount=32有效

注意:请按照上面数组的位数签单设置GetDIBitslpBits参数。


以上的反色代码假设你加载的是一副24位或32位的图像,但是如果你加载一副8位或8位以下的BMP图像,然后执行该代码看看(提示,一定要保存下先哦),怎么样,VB悄无声息的消失了,这一次,你问10个人有9个人可能不知道问题出在那里,似乎每个函数都没有问题,如果你单步调试,发现执行到GetDIBits 这里VB挂掉,因此,问题就出在这个函数上。
我们知道,8位及8位以下的图像都有调色板,那么调色板的信息如何得到呢,我们注意到

Private Type BITMAPINFO
bmiHeader As BITMAPINFOHEADER
bmiColors As RGBQUAD
End Type
这个结构中除了位图信息头之外还有个RGBQUAD 元素,GetDIBits 函数在执行时会自动将图像的信息填充到这个结构体中,而对于8位位图,一般有256RGBQUAD 元素的调色板,而我们的声明中只给了他一个元素的空间,因此,会造成访问非法内存之类的事情发生,导致IDE崩溃。

那么,解决问题的方式就是修改BITMAPINFO 结构的声明方式,现修改如下:

Private Type BITMAPINFO
bmiHeader As BITMAPINFOHEADER
bmiColors(255) As RGBQUAD
End Type
然后执行类似的代码,很多情况下,你看到的不正确的效果,至于为什么,详细的分析可见
http://topic.csdn.net/u/20070505/16/3bb480ae-3eeb-4f5d-80db-19ecc61202e6.html
实际上,在PS中能对索引色进行调增的选项很少,只有调整菜单中的若干项,而那若干项其实都是读调色板进行处理的,而没有改变实际的图像数据,因此,对于8位色以下的图像,反色的过程应该类似如下:

For i = 0 To 255
BmpInfo.bmiColors(i).Red = 255 - BmpInfo.bmiColors(i).Red
BmpInfo.bmiColors(i).Green = 255 - BmpInfo.bmiColors(i).Green
BmpInfo.bmiColors(i).Blue = 255 - BmpInfo.bmiColors(i).Blue
Next
对于4位色以及1为色,调色板中最多只会有162个元素,对于8位色,也会存在调色板中只有在【17255】个元素的情况,但是由于对索引色图像的处理时一般不改动数据,而只改变调色板,为了方便,这里的 bmiColors(255) As RGBQUAD 直接定义为255个比较方便。


按照严格的定义,索引色中实际使用的调色板数应该由biClrUsed As Long给出,但是,我观察过很多图像,这个值默认都是0,即使8位色没有使用256个或4位色没有使用哪个16个调色板,因此,如果你必须得到实际用的调色板数,可以根据

bmiColors
的颜色数字来判断。提示:PS中会把白色和黑色两种索引色放在调色板的最前面(如果有的话)。


好了,也许你认为完美了,其实不然,我们再回到24色及32位色的问题上,

For i = 0 To UBound(PicData)
PicData(i) = 255 - PicData(i)
Next
从严格的以上讲,这个代码所执行的过程已经大于了反色算法的需求了,这里因为是简单反色算法,所以我们看不出什么异常。而实际过程是,我们很有可能处理了一些我们不需要处理的数据。还记得扫描行的概念吗,扫描行的字节数必须是4的倍数(这是指DIB,对于DDB2个倍数,一定要分清哦),不够的部分用0补齐。由我们上述的代码可以看到,对于这些用0补齐的部分反色后就变为255了,因此,最终的反色算法应该如下:



Public Function Invert(Pic As StdPicture) As Boolean '
Dim i As Long,j As Long
Dim hDC As Long,Speed As Long
Dim Pixel As Long
Dim Bmp As Bitmap,Bmp
With BmpInfo.bmiHeader
.biSize = Len(BmpInfo.bmiHeader)
.biWidth = Bmp.bmWidth
.biHeight = Bmp.bmHeight
.biPlanes = 1
.biBitCount = Bmp.bmBitsPixel
.biCompression = BI_RGB
End With
hDC = GetDC(0)
ReDim PicData(Bmp.bmWidthBytes * Bmp.bmHeight - 1) As Byte
GetDIBits hDC,DIB_RGB_COLORS
If Bmp.bmBitsPixel <= 8 Then
For i = 0 To 255
BmpInfo.bmiColors(i).Red = 255 - BmpInfo.bmiColors(i).Red
BmpInfo.bmiColors(i).Green = 255 - BmpInfo.bmiColors(i).Green
BmpInfo.bmiColors(i).Blue = 255 - BmpInfo.bmiColors(i).Blue
Next
Else
Pixel = Bmp.bmBitsPixel / 8
For j = 0 To Bmp.bmHeight - 1
Speed = j * Bmp.bmWidthBytes
For i = 0 To Bmp.bmWidth - 1
PicData(Speed) = 255 - PicData(Speed) 'Blue
PicData(Speed + 1) = 255 - PicData(Speed + 1) 'Green
PicData(Speed + 2) = 255 - PicData(Speed + 2) 'Red
Speed = Speed + Pixel '
这里这样写是标准的过程,而没有考虑优化

Next
Next
End If
SetDIBits hDC,hDC
End Function
讲了一大堆,其实过程很简单,但是要注意到这些细节,还是有些学问的,这里拿反色只是举个例子,大家可以举一反三。

顺便谈一下,上述过程和模拟指针有多大区别啊,上面不是谈到如果按照实际的位数调用哪个GetDIBits 函数,就可以看成一个copymemory的过程吗,用模拟指针实际上就是不要这个copymemory的过程了,而直接访问图像在

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Format[$] ( expr [ , fmt ] ) format 返回变体型 format$ 强制返回为文本 -------------------------------- 数字类型的格式化 --------------------------------     固定格式参数:     General Number 普通数字,如可以用来去掉千位分隔号     format$("100,1
VB6或者ASP 格式化时间为 MM/dd/yyyy 格式,竟然没有好的办法, Format 或者FormatDateTime 竟然结果和系统设置的区域语言的日期和时间格式相关。意思是尽管你用诸如 Format(Now, "MM/dd/yyyy"),如果系统的设置格式区域语言的日期和时间格式分隔符是"-",那他还会显示为 MM-dd-yyyy     只有拼凑: <%response.write
在项目中添加如下代码:新建窗口来显示异常信息。 Namespace My ‘全局错误处理,新的解决方案直接添加本ApplicationEvents.vb 到工程即可 ‘添加后还需要一个From用来显示错误。如果到这步还不会则需要先打好基础啦 ‘======================================================== ‘以下事件
转了这一篇文章,原来一直想用C#做k3的插件开发,vb没有C#用的爽呀,这篇文章写与2011年,看来我以前没有认真去找这个方法呀。 https://blog.csdn.net/chzjxgd/article/details/6176325 金蝶K3 BOS的插件官方是用VB6编写的,如果  能用.Net下的语言工具开发BOS插件是一件很愉快的事情,其中缘由不言而喻,而本文则是个人首创,实现在了用V
Sub 分列() ‘以空格为分隔符,连续空格只算1个。对所选中的单元格进行处理 Dim m As Range, tmpStr As String, s As String Dim x As Integer, y As Integer, subStr As String If MsgBox("确定要分列处理吗?请确定分列的数据会覆盖它后面的单元格!", _
  窗体代码 1 Private Sub Text1_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single) 2 Dim path As String, hash As String 3 For Each fil
  Imports MySql.Data.MySqlClient Public Class Form1 ‘ GLOBAL DECLARATIONS Dim conString As String = "Server=localhost;Database=net2;Uid=root;Pwd=123456;" Dim con As New MySqlConnection
‘導入命名空間 Imports ADODB Imports Microsoft.Office.Interop   Private Sub A1() Dim Sql As String Dim Cnn As New ADODB.Connection Dim Rs As New ADODB.Recordset Dim S As String   S = "Provider=OraOLEDB.Oracl
Imports System.IO Imports System.Threading Imports System.Diagnostics Public Class Form1 Dim A(254) As String    Function ping(ByVal IP As Integer) As String Dim IPAddress As String IPAddress = "10.0.
VB运行EXE程序,并等待其运行结束 参考:https://blog.csdn.net/useway/article/details/5494084 Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long Pr
今天碰到一个问题,登陆的时候,如果不需要验证手机号为空,则不去验证手机号 因为登陆的时候所有的验证信息都存放在一个数组里 Dim CheckUserInfo() As String ={UserBirthday, SecEmail, UserMob, UserSex, RealNameFirst, RealName, CheckCardID, CheckCardType, Contactemail
在VB6.0中,数据访问接口有三种: 1、ActiveX数据对象(ADO) 2、远程数据对象(RDO) 3、数据访问对象(DAO) 1.使用ADO(ActiveX Data Objec,ActiveX数据对象)连接SQL Server 1)使用ADO控件连接 使用ADO控件的ConnectionString属性就可以连接SQL Server,该属性包含一个由分号分隔的argument=value语
注:大家如果没有VB6.0的安装文件,可自行百度一下下载,一般文件大小在200M左右的均为完整版的软件,可以使用。   特别提示:安装此软件的时候最好退出360杀毒软件(包括360安全卫士,电脑管家等,如果电脑上有这些软件的话),因为现如今的360杀毒软件直接会对VB6.0软件误报,这样的话就可能会在安装过程中被误报阻止而导致安装失败,或者是安装后缺乏很多必须的组件(其它的杀毒软件或安全卫士之类的
Private Sub Form_Load() Call conndb End Sub Private Function conndb() Dim cn As New ADODB.Connection Dim rs As New ADODB.Recordset Dim strCn, sql As String Dim db_host As String Dim db_user As String
  PPSM06S70:  Add  moddate  EDITSPRINTJOB:  MAX(TO_CHAR(ETRN.MODDATE, ‘yyyy/mm/dd/HH24:MI AM‘)) ACTUAL_SHIPDATE   4.Test Scenario (1) :Query SQL Test DN:8016578337 SELECT CTRN.TKCTID TRUCK_ID,        
  沒有出現CrystalReportViewer時,須安裝CRforVS_13_0. 新增1個數據集,新增1個數據表,添加二列,列名要和資料庫名一樣. 修改目標Framework 修改app.config, <startup >改成<startup useLegacyV2RuntimeActivationPolicy ="true">  CrystalReport1.rpt增加數據庫專家 在表單
Imports System.Threading Imports System Public Class Form1 Dim th1, th2 As Thread Public Sub Method1() Dim i As Integer For i = 1 To 100 If Me.Label1.BackColor =
Friend Const PROCESS_ALL_ACCESS = &H1F0FFF = 2035711 Friend Const PROCESS_VM_READ = &H10 Friend Const PROCESS_VM_WRITE = &H20 Friend Const PAGE_READONLY = &H2 Friend Const PAGE_READWRITE = &H4 Friend
以下代码随手写的 并没有大量测试 效率也有待提升 如果需要C#的请自行转换 Function SplitBytes(Data As Byte(), Delimiter As Byte()) As List(Of Byte()) Dim i = 0 Dim List As New List(Of Byte()) Dim bytes As New
Imports System.Data.SqlClient Public Class Form1 REM Public conn1 As SqlConnection = New SqlConnection("server=.; Integrated Security=False;Initial Catalog= mydatabase1; User ID= sa;password")