windows设备相关位图与设备无关位图

    windows支持两种位图格式,DDB(device-dependent bitmap),DIB(device-independent bitmap)。设备相关位图用于windows显示系统中,其图像格式与显卡格式兼容,因此显示速度很快。设备不相关位图定义了位图的文件格式,用于位图传输,由于其数据格式可能与显卡格式不一致,直接使用设备不相关位图显示图像时需要进行转换,因此显示速度较慢。

    历史上显卡支持16色或者256色,分别使用4位或者8位表示一个像素颜色。在16色系统中,仅支持黑白灰,红绿蓝,青品红黄等基本颜色。在256色系统中,windows保留了20中基本颜色,剩余236中颜色通过颜色查找表定义。因此,基于以上色彩体系创建的位图需要设备相关的颜色查找表来解释真实颜色。

    另外两种不需要查找表的颜色包括16位色与24位色。使用16位(2字节)表示红绿蓝,每个颜色分量使用5位(或者绿色使用6位),被称为 high color。使用24位(3字节)表示红绿蓝,每个颜色分量使用一个字节,被称为 true color。在没有查找表情况下,位图红绿蓝分量排列顺序可能不一致,因此DDB显示速度要优于DIB。

   现代显示器一般都是32位真彩色,可以通过 ::GetDeviceCaps(hdc, BITSPIXEL) 获得每个像素上的位数,在本机测试值为32,通过 ::GetDeviceCaps(hdc, PLANES) 获得位面数,一般情况下该值为1。

   windows提供了BITMAP结构体来描述DDB,定义如下: 

    

/* Bitmap Header Definition */
typedef struct tagBITMAP
  {
    LONG        bmType;
    LONG        bmWidth;
    LONG        bmHeight;
    LONG        bmWidthBytes;
    WORD        bmPlanes;
    WORD        bmBitsPixel;
    LPVOID      bmBits;
  } BITMAP, *PBITMAP, NEAR *NPBITMAP, FAR *LPBITMAP;

    其中,bmWidth, bmHeight表示DDB的尺寸,bmPlanes通常为1,bmBitsPixel表示每个像素需要多少位来表示。

    有两个参数需要特别注意:

     bmBits并不是指向DDB的一个指针,应用程序无法直接操作显存,多数情况下该指针为空,但可以通过特定函数获得DDB数据区的一个拷贝;

     bmWidthBytes一般不需要赋值, windows会根据规则计算出正确的值,其规则是位图每行字节为2的倍数,一般计算公式为 bmWidthBytes = 2 *((bmWidth * bmBitsPixel + 15) / 16)。

 

    windows提供了两个基本函数进行DDB绘制,BitBlt 直接将DDB一个区域拷贝到另一个区域中,StretchBlt 引入了缩放模式。

    以下函数块仅在左上角绘制一条直线段,然后利用 BitBlt 将其拷贝到其他区域,关键代码如下:

    

::MoveToEx(hdc, 0, 0, NULL);
::LineTo(hdc, 10, 10);
for (int y = 50; y < cyClient; y += cySource)
    for (int x = 50; x < cxClient; x += cxSource)
    {
        BitBlt(hdc, x, y, cxSource, cySource, 
                hdc, 0, 0, SRCCOPY);
    }

 

    得到如下显示效果:

    

    使用 StretchBlt 替代 BitBlt ,结果如下:

    

     代码如下:

::MoveToEx(hdc, 0, 0, NULL);
::LineTo(hdc, 10, 10);
for (int y = 50; y < cyClient; y += 30)
    for (int x = 50; x < cxClient; x += 30)
    {
        ::StretchBlt(hdc, x, y, 20, 20,
            hdc, 0, 0, 10, 10, SRCCOPY);
    }

     StretchBlt 将10*10区域拷贝到20*20区域,当目标区域与原始区域尺寸不一致时,必然存在插值操作,使用函数 SetStretchBltMode 设置插值模式,windows 定义了如下模式:

     .BLACKONWHITE:位与操作,保留插值区域中最暗的值(缩小时应用该值,放大时直接映射到原始区域中)

     .WHITEONBLACK:位或操作,保留插值区域中最亮的值(缩小时应用该值,放大时直接映射到原始区域中)

     .COLORONCOLOR:抽取冗余行列,即得到缩小图像(放大时同样直接映射到原始区域中)

     .HALFTONE:使用均值作为目标结果(缩小时先平均再采样,放大时使用插值)

      以上插值模式中,COLORONCOLOR速度最快,HALFTONE速度最慢,BLACKONWHITE与WHITEONBLACK应用在一些特殊场景中,一般使用COLORONCOLOR。

    注意参数 SRCCOPY 定义了拷贝操作运算,该运算针对每个像素做同样的操作,这是一个三元运算,其操作数包括源像素,目标像素以及模式像素(画刷),该三元运算包括256中组合,下面仅给出部分重要组合:

     .SRCCOPY: dest = src

     .SRCPAINT: dest = source OR dest

     .SRCAND: dest = source AND dest

     .SRCINVERT: dest = source XOR dest

     .SRCERASE: dest = source AND (NOT dest )

     .NOTSRCCOPY: dest = (NOT source)

    有了以上拷贝模式,很自然会想到是否可以使用某些运算组合实现椭圆图像绘制,这里所谓椭圆图像是指对矩形图像绘制椭圆区域,典型应用如界面上圆角按钮。

 

// load image
hBitmapImag = (HBITMAP)LoadImage(hInstance, 
                        TEXT("img.bmp"),
            IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | 
                        LR_CREATEDIBSECTION);

// obtain image size
GetObject(hBitmapImag, sizeof(BITMAP), &bitmap);
cxBitmap = bitmap.bmWidth;
cyBitmap = bitmap.bmHeight;

// Select the original image into a memory DC
hdcMemImag = CreateCompatibleDC(NULL);
SelectObject(hdcMemImag, hBitmapImag);

// Create the monochrome mask bitmap and memory DC
hBitmapMask = CreateBitmap(cxBitmap, cyBitmap, 1, 1, NULL);
hdcMemMask = CreateCompatibleDC(NULL);
SelectObject(hdcMemMask, hBitmapMask);

// Color the mask bitmap black with a white ellipse
SelectObject(hdcMemMask, GetStockObject(BLACK_BRUSH));
Rectangle(hdcMemMask, 0, 0, cxBitmap, cyBitmap);
SelectObject(hdcMemMask, GetStockObject(WHITE_BRUSH));
Ellipse(hdcMemMask, 0, 0, cxBitmap, cyBitmap);

// Mask the original image
BitBlt(hdcMemImag, 0, 0, cxBitmap, cyBitmap,
            hdcMemMask, 0, 0, SRCAND);

// Center image
x = (cxClient - cxBitmap) / 2;
y = (cyClient - cyBitmap) / 2;

// Do the bitblts
SelectObject(hdc, GetStockObject(GRAY_BRUSH));
Rectangle(hdc, 0, 0, cxClient, cyClient);
BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemMask, 0, 0, 0x220326);
BitBlt(hdc, x, y, cxBitmap, cyBitmap, hdcMemImag, 0, 0, SRCPAINT);

DeleteDC(hdcMemImag);
DeleteDC(hdcMemMask);

 

   

 

    以上代码实现了绘制图像椭圆区域,下面详细解读实现过程:

    1)首先加载DDB图像,并获取图像尺寸

    2)创建内存兼容DC,并将DDB图像加载到DC

    3)创建与原始图像尺寸一致的模板图像,模板图像为二值图像,并加载到内存DC中

    4)对模板图像椭圆区域内设置为1,椭圆区域外设置为0

    5)使用 SRCAND 操作将模板图像绘制到原图像中,此时原图像保留图像区域内像素值,椭圆区域外被设置为0

    6)为了使图像居中显示,计算绘制起点x,y

    7)将整个区域填充为中灰色

    8)使用 0x220326 操作得到一个椭圆中心为黑色,椭圆外围保持不变的DC,该操作没有命名,具体执行逻辑为 Dest = Dest AND (Not Source)

    9)使用 SRCPAINT 操作得到最终结果,该操作执行 Dest = Source OR Dest

    windows 同样提供了一个更加简便的函数来实现以上功能,使用 PlgBlt 函数可以实现任意自定义区域绘制,同时该函数还可以做图像变换功能。

     BOOL PlgBlt( HDC hdcDest, CONST POINT * lpPoint, HDC hdcSrc,  int xSrc, int ySrc, int width,

                           int height,  HBITMAP hbmMask,  int xMask,  int yMask);

    参数 hbmMask 为一个二值图像,作为原始图像的绘制模板

    参数 hdcSrc 为原始图像,同时定义了绘制区间

    参数 hdcDest 为目标DC,图像将安装指定规则绘制到目标DC上

    参数 lpPoint 包含了三个点,在目标区域上构成了一个平行四边形,三个点分别对应左上,右上,左下,第四个点(右下)可以根据平行四边形规则计算得出,

    将原始图像矩形映射到目标平行四边形上即构成了一个变换,该变换包括平移,旋转,缩放,放射变换。

    下面给出 PlgBlt 函数的使用效果:

      

      左边图像仅包括平移与缩放,右边图像为任意仿射变换,主要代码如下:

 

POINT pt[3];
pt[0].x = 50;
pt[0].y = 50;
pt[1].x = 300;
pt[1].y = 50;
pt[2].x = 50;
pt[2].y = 300;
PlgBlt(hdc, pt, hdcMemImag, 0, 0, cxBitmap, cyBitmap,
hBitmapMask, 0, 0);

pt[0].x = 350;
pt[0].y = 50;
pt[1].x = 600;
pt[1].y = 0;
pt[2].x = 500;
pt[2].y = 400;
PlgBlt(hdc, pt, hdcMemImag, 0, 0, cxBitmap, cyBitmap, hBitmapMask, 0, 0);

    除了对DDB图像进行透明与仿射变换操作外,是否可以可以对DDB图像做融合操作呢?msimg32.dll 提供了更懂的图像操作函数。

    TransparentBlt 函数可以对指定像素做透明处理,函数定义如下:

    BOOL TransparentBlt(_In_ HDC hdcDest, _In_ int xoriginDest, _In_ int yoriginDest, _In_ int wDest, _In_ int hDest, 

                                     _In_ HDC hdcSrc, _In_ int xoriginSrc, _In_ int yoriginSrc, _In_ int wSrc, _In_ int hSrc, _In_ UINT crTransparent);

   该函数对指定像素透明处理,但指定像素不能为RGB(0,0,0),不论DC缩放模式设置为什么值,该函数永远执行 COLORONCOLOR 模式。

    AlphaBlend 将图像与背景融合,函数定义如下:

    BOOL AlphaBlend (_In_ HDC hdcDest, _In_ int xoriginDest, _In_ int yoriginDest, _In_ int wDest, _In_ int hDest, 

                                     _In_ HDC hdcSrc, _In_ int xoriginSrc, _In_ int yoriginSrc, _In_ int wSrc, _In_ int hSrc, _In_ BLENDFUNCTION ftn);

    该函数关键参数为 BLENDFUNCTION 结构体,当 AlphaFormat 取值为 AC_SRC_ALPHA 时,原图像必须具有 alpha 通道。

    当 SourceConstantAlpha 取值小于255时,整个图像应用SourceConstantAlpha;当 SourceConstantAlpha 取值为255时,应用每个像素的alpha值。

    图像融合计算公式为 dest = source * alpha / 255 + dest * (1 - alpah / 255)。

    一个注意点:source * alpha / 255 在调用显示函数前可以提前计算出来,为了提升显示速度,AlphaBlend 函数应用了公式 dest = source  + dest * (1 - alpah / 255),

    即 source 不再是原始图像,而是 source = source * alpha / 255。

    下面给出一个 AlphaBlend 函数应用示例,我创建了一个32位图像,其上半部分图像的alpha值被设置为128,下半部分图像的alpha值设置为255,

    同时上半部分图像的每个像素进行了预计算(source = source * alpha / 255),以下为部分显示代码及效果:

    

hdcMemImag = CreateCompatibleDC(hdc);
SelectObject(hdcMemImag, hBitmapImag);
SelectObject(hdc, GetStockObject(GRAY_BRUSH));
Rectangle(hdc, 0, 0, cxClient, cyClient);
BLENDFUNCTION bn;
bn.AlphaFormat = AC_SRC_ALPHA; 
bn.BlendFlags = 0; 
bn.BlendOp = AC_SRC_OVER; 
bn.SourceConstantAlpha = 255;  // 应用每个像素的alpha值
AlphaBlend(hdc, 0, 0, cxBitmap, cyBitmap,
        hdcMemImag, 0, 0, cxBitmap, cyBitmap, bn);
DeleteDC(hdcMemImag); 

   

 

 

    以上是DDB的一些主要内容,windows同样提供了DIB,其目的是为了图像文件传输与存储,因为DDB高度依赖设备。

   windows DIB 结构集成自 OS/2,包括以下几个部分:

    1)文件头,定义为 BITMAPFILEHEADER,包括文件识别及尺寸等基本信息;

    2)信息头,OS/2定义为BITMAPCOREHEADER, windows 对其进行扩展,定义为 BITMAPINFOHEADER,在windows中两者均可使用;

    3)查找表,高彩色或者真彩色不需要查找表;

    4)图像数据区;

    这里不详细解释所有结构体成员,仅对一些自认为很重要的地方进行说明。

    1)BITMAPCOREHEADER 与 BITMAPINFOHEADER 的成员变量 bcPlanes 与 biPlanes 永远为1;

    2)BITMAPCOREHEADER 的成员变量 bcBitCount可取值为 1,2,4,8,24,而windows扩展下 BITMAPINFOHEADER的成员变量biBitCount 可取值1,2,4,8,16,24,32;

    3)BITMAPCOREINFO 使用查找表 RGBTRIPLE,而 BITMAPINFO 使用查找表 RGBQUAD;

    4)由于 OS/2 风格定义也被 windows 支持,当拿到一个信息头结构体后,可以通过 bcSize/biSize 字段来判断到底是哪种风格位图,因为两个结构体尺寸分别为 12,40字节;

    5)由于结构体在内存中的对齐方式由编译器确定,不同编译器可能得到不同大小的结构体,当将图像结构写入文件时,会导致无法正确读取结构体(对齐方式未知),因此需要使用 packed 模式存储结构体变量;

    6)图像数据区原点位于左下角(满足笛卡尔坐标系),而显示系统原点一般位于左上角,这会导致显示图像时上下翻转;

    7)位图数据行一般是4字节整数倍,而DDB要求为两字节整数倍,所以当DIB转换为DDB时自然满足对齐要求;

    8)biCompression 可取值 BI_RGB,BI_RLE8, BI_RLE4,BI_BITFIELDS,具体解释如下:

          .1位位图时,biCompression 取值只能为 BI_RGB;

          .4位位图时,biCompression  取值可以为 BI_RGB 或者 BI_RLE4, BI_RLE4为行程码压缩数据,该压缩算法直观简单,因此编解码速度会很快,但压缩率有限;

          .8位位图时,biCompression  取值可以为 BI_RGB 或者 BI_RLE8, BI_RLE8为行程码压缩数据;

          .24位位图时,biCompression 取值只能为 BI_RGB;

          .16位或者32位位图时,biCompression 取值可以为 BI_RGB 或者 BI_BITFIELDS

          .只要 biCompression 取值为 BI_RGB,不管16位,24位或者32位位图,其排列顺序均为blue->green->red->alpha(可能没有),只是16位位图时每个颜色占5位,24位与32位位图时每个颜色占8位;

          .当 biCompression 取值为 BI_BITFIELDS 时,在 BITMAPCOREHEADER 后紧跟3个DWORD数据作为颜色位标记符,程序根据这3个DWORD数据提取色彩;

    与DDB类似,windows同样提供了两个函数用于显示DIB, SetDIBitsToDevice直接将DIB显示在DC上, StretchDIBits 提供了缩放显示,定义如下:

    int SetDIBitsToDevice(_In_ HDC hdc, _In_ int xDest, _In_ int yDest, _In_ DWORD w, _In_ DWORD h, _In_ int xSrc,

                                      _In_ int ySrc, _In_ UINT StartScan, _In_ UINT cLines, _In_ CONST VOID * lpvBits, _In_ CONST BITMAPINFO * lpbmi, _In_ UINT ColorUse);

    int StretchDIBits(_In_ HDC hdc, _In_ int xDest, _In_ int yDest, _In_ int DestWidth, _In_ int DestHeight, _In_ int xSrc, _In_ int ySrc, _In_ int SrcWidth, _In_ int SrcHeight,

                             _In_opt_ CONST VOID * lpBits, _In_ CONST BITMAPINFO * lpbmi, _In_ UINT iUsage, _In_ DWORD rop);

    是否可以对使用 StretchDIBits  函数完成图像仿射变换及椭圆显示呢?可以采用如下方法实现:

    1)直接对DIB图像数据应用仿射变换;

    2)使用类似位逻辑操作(如SRCPAINT等 )实现指定区域透明显示功能;

    要实现DIB图像融合,目前没有看到windows提供更多的方法,需要自己对数据直接处理。

    在实际应用中,DDB与DIB一般会交互使用,以下流程给出了DIB与DDB之间的转换方法:

    1)从文件中加载一张DIB图像;

    2)使用 CreateCompatibleBitmap 创建与DIB尺寸一致且与设备兼容的位图(DDB);

    3)使用 CreateCompatibleDC 创建与设备兼容的内存DC,并将兼容位图选人兼容内存DC;

    4)使用 SetDIBitsToDevice 将DIB绘制到兼容内存DC上;

    5)使用 BitBlt 将内存DC显示到物理DC上,实现了DIB到DDB转换并显示;

    6)使用 GetDIBits 并将第五个参数(数据指针)设置为 NULL,可获得内存DC数据相关参数;

    7)根据 6)获得参数分配所需内存,再次调用 GetDIBits 可获得数据,返回值表示成功拷贝图像行数;

    8)将获得图像保存到文件,对比与原始图像一致,实现了DDB到DIB转换并保存。

    以下给出部分代码:

// 打开图像
...

// DIB转换到DDB
HBITMAP hBitmap = ::CreateCompatibleBitmap(hdc, w, h);
HDC hMemDC = ::CreateCompatibleDC(hdc);
::SelectObject(hMemDC, hBitmap);
::SetDIBitsToDevice(hMemDC, 0, 0, w, h, 0, 0, 0, h,
                imgs[0].data, (LPBITMAPINFO)lpBmpInfoHead, 
                DIB_RGB_COLORS);

// 显示DDB
CRect rcClient;
this->GetClientRect(&rcClient);
BitBlt(hdc, rcClient.left, rcClient.top, rcClient.Width(), 
              rcClient.Height(), hMemDC, 0, 0, SRCCOPY);

// DIB信息头,在第一次调用GetDIBits时被填充
BITMAPINFO bmi = { 0 };
int sz = sizeof(BITMAPINFO);
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

// 获取DIB尺寸等参数
::GetDIBits(hMemDC, hBitmap, 0, h, NULL, &bmi, 
         DIB_RGB_COLORS);

// 分配DIB数据所需内存
unsigned char *bitmapBits = new unsigned 
           char[bmi.bmiHeader.biSizeImage];
memset(bitmapBits, 0, bmi.bmiHeader.biSizeImage);

// 从DDB中获取DIB数据
int lines = ::GetDIBits(hMemDC, hBitmap, 0, h,
            bitmapBits, &bmi,  DIB_RGB_COLORS);

// 拷贝数据并保存文件
...

// 删除内存
delete []bitmapBits ;

 

    参考资料:Programming Windows by Charles Petzold

                      Wingdi.h header - Win32 apps | Microsoft Docs

posted @ 2022-01-25 16:14  罗飞居  阅读(1150)  评论(0编辑  收藏  举报