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

解决方案 »

  1.   

    1.读取图像。这个很简单,直接使用LoadPicture(其实就是对OleLoadPicture这个函数的封装),其支持BMP,JPG,GIF,ICO,WMF,EMF格式。这里说明一下,对于由柯达控件生成的BMP,部分GIF和JPEG2000,以及32位的ICO这个函数似乎会产生一些未知的错误。JPG格式加载后,在内存中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 Function2、保存图像。这个也很简单,SavePicture函数,注意,Savepicture函数只能将图像保存为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的自带了一个point和Pset函数,而这两个函数可以得到和设置图像的颜色,因此,常常会作为初学者的最爱工具,而最终的结果就是让VB落得一个骂名:龟速。
        总结一下,在VB中可以用来得到图像数据的常用函数有: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 ExplicitPrivate Type BITMAPFILEHEADER
        bfType      As Integer
        bfSize      As Long
        bfReserved1 As Integer
        bfReserved2 As Integer
        bfOffBits  As Long
    End TypePrivate Type Bitmap
      bmType As Long
      bmWidth As Long
      bmHeight As Long
      bmWidthBytes As Long
      bmPlanes As Integer
      bmBitsPixel As Integer
      bmBits As Long
    End TypePrivate 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                '只有bibitcount为1,2,4时才有调色板
        Blue As Byte                     '蓝色分量
        Green As Byte                    '绿色分量
        Red As Byte                      '红色分量
        Reserved As Byte                 '保留值
    End TypePrivate Type BITMAPINFO
        bmiHeader As BITMAPINFOHEADER
        bmiColors As RGBQUAD
    End TypePrivate Const BI_RGB = 0&
    Private Const DIB_RGB_COLORS = 0&
    Private Declare Function GetDIBits Lib "gdi32" (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
    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 hBitmap As Long, ByVal nStartScan As Long, ByVal nNumScans As Long, lpBits As Any, lpBI As BITMAPINFO, 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, Len(Bmp), 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, 0, Bmp.bmHeight, PicData(0), BmpInfo, DIB_RGB_COLORS
        For i = 0 To UBound(PicData)
            PicData(i) = 255 - PicData(i)
        Next
        SetDIBits hDC, Pic.Handle, 0, Bmp.bmHeight, PicData(0), BmpInfo, DIB_RGB_COLORS
        ReleaseDC 0, hDC
    End Function调用方式类似于如下: Call Invert(Picture1.picture)然后刷新一下: Picture1.Refresh.以上假设你加载的是一副24位的图像(不管原始是什么格式).
      

  2.   

    楼主的方法应该是针对256色以上的图像吧,对于有调色板的图像,一般常用的方法应该是用SetPaletteEntries修改逻辑调色板项为其互补色吧,这方法比逐个修改像素快。
    另外,如果仅是图像取补而不必再对数据做其它处理,我觉得使用带rop为notsrccopy的bitblt似乎更简单点。
      

  3.   

        在以上函数中,语句.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 来代替。    以上假设你加载的是一副24位的图像,但是如果你加载一副8位或8位以下的BMP图像,然后执行该代码看看(提示,一定要保存下先哦),怎么样,VB悄无声息的消失了,这一次,你问10个人有9个人可能不知道问题出在那里,似乎每个函数都没有问题,如果你单步调试,发现执行到GetDIBits 这里VB挂掉,因此,问题就出在这个函数上。
        我们知道,8位及8位以下的图像都有调色板,那么调色板的信息如何得到呢,我们注意到
    Private Type BITMAPINFO 
        bmiHeader As BITMAPINFOHEADER 
        bmiColors As RGBQUAD 
    End Type 
         这个结构中除了位图信息头之外还有个RGBQUAD 元素,GetDIBits 函数在执行时会自动将图像的信息填充到这个结构体中,而对于8位位图,一般有256个RGBQUAD 元素的调色板,而我们的声明中只给了他一个元素的空间,因此,会造成访问非法内存之类的事情发生,导致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为色,调色板中最多只会有16和2个元素,对于8位色,也会存在调色板中只有在【17,255】个元素的情况,但是由于对索引色图像的处理时一般不改动数据,而只改变调色板,为了方便,这里的 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,对于DDB是2个倍数,一定要分清哦),不够的部分用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, BmpInfo  As BITMAPINFO
        GetGDIObject Pic.Handle, Len(Bmp), 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, 0, Bmp.bmHeight, PicData(0), BmpInfo, 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, Pic.Handle, 0, Bmp.bmHeight, PicData(0), BmpInfo, DIB_RGB_COLORS
        ReleaseDC 0, hDC
    End Function
    讲了一大堆,其实过程很简单,但是要注意到这些细节,还是有些学问的,这里拿反色只是举个例子,大家可以举一反三。
      

  4.   

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

  5.   

    由于看过楼主的那个很象PS的那个程序,因此很佩服楼主在图像处理方面的功夫。但我还是要纠正楼主一个认识上的误区,虽然这件事情与本贴讨论的问题没有太大的关系。楼主在第一楼发的贴子中写道:“2、保存图像。这个也很简单,SavePicture函数,注意,Savepicture函数只能将图像保存为BMP格式,无论你给他的路径参数的后缀是什么”,其实这种说法并不准确。试试下面的代码就知道了:1. 载入一个图标文件
    SavePicture LoadPicture("0.ico"), "e:\0.ico"
    其结果是保存了一个图标文件,当然其色深可能会与原图发生变化,由于VB的早期原因吧,其保存后的图标被修改为16色了,但毕竟不是BMP。
    2. 载入一个图元文件
    SavePicture LoadPicture("0.wmf"), "e:\0.wmf"
    其结果是生成了一个图元文件,同样地,不是一个BMP文件。有一点可以肯定,SavePicture保存的文件格式并不受给出的存储文件扩展名的影响。
    希望楼主能够再接再厉,发更好的贴子让大家学习。
      

  6.   

    其实就速度上来说,ddb会比dib快一点点的。有些程序中,如果是已知色深的图片,还是用ddb更有效率一点。
    况且楼主已经验证了,ddb排列是2的整数倍就可以了。难道还有其他问题?
      

  7.   

    对于反色 除了255-RGB之外 也可以RGB xor 255的方法
      

  8.   

    下面这个帖子里,我回复的“生命游戏”代码用了与楼主文章相似的方法。它将“生命游戏”的地图转换成位图数据并显示在一个PictureBox里,可以作为参考。http://topic.csdn.net/u/20081125/13/1E0CB51F-D21B-47EC-A6DA-CF5855A868BF.html另外,QuickBASIC下对调色板用得比较多。
    Palette Using Colors()
    配合PhotoShop以及直接Bload显示缓冲区的方法,可以做出当时来讲相当惊人的效果:在486的DOS环境下可以实现320×200 256色、30帧以上动画。VB起码比QB要强多了。
      

  9.   

    慢慢学习PS: 
    我的目标是 ----> ^_^
      

  10.   

    GDI+了一段时间,感觉现在自己算法好缺~~~
      

  11.   

    VB现在还有人用呐,不过确实用起来比别的简单多了
    =================================
    北京思源计算机培训中心(http://www.ciitc.com)
      

  12.   

    强帖要顶~~ps:怎么最近 csdn 论坛多了这么多打广告的。。
    建议给每贴楼主删回帖的权力!
      

  13.   

    我也用过这个API函数
    不过我现在处理图像用的都是GetBitmapBits