使用GetDIBits函数并通过X、Y循环像素。

10
我正在捕获屏幕的一部分,并扫描像素以寻找特定的颜色范围。
我查看了MSDN的捕获图像示例并知道如何使用函数。
我可以将位数获取到数组中,但我不确定如何以可以像处理图像一样循环遍历它的方式来做。一个伪例子(我确定是错误的):
for ( x = 1; x <= Image.Width; x += 3 )
{
    for ( y = 1; y <= Image.Height; y += 3 )
    {
        red = lpPixels[x];
        green = lpPixels[x + 1];
        blue = lpPixels[x + 2];
    }
}

基本上,我想要做的就是,如果红色、蓝色和绿色是某种颜色,我就会知道图像中它所在的坐标(x,y)。
我只是不知道如何以这样的方式使用GetDIBits,并且如何适当地设置数组以能够实现这一点。
5个回答

16

除了已经给出的好答案外,下面是一个例子,演示如何获取一个简单的数组结构进行遍历。(您可以使用例如Goz' code进行迭代。)

MSDN上的 GetDIBits 参考文献

您必须选择 DIB_RGB_COLORS 作为 uUsage 的标志,并设置BITMAPINFO 结构和它所包含的BITMAPINFOHEADER 结构。当您将 biClrUsedbiClrImportant 设置为零时,就没有颜色表,因此您可以将从 GetDIBits 得到的位图的像素读取为 RGB 值的序列。使用 32 作为位数(biBitCount)会根据 MSDN 设置数据结构:

位图最多有 2^32 种颜色。如果 BITMAPINFOHEADERbiCompression 成员是 BI_RGB,则 BITMAPINFObmiColors 成员为 NULL。位图数组中的每个 DWORD 分别表示像素的蓝、绿和红的相对强度值。每个 DWORD 中的高字节未使用。

由于 MS 的 LONG 恰好为 32 位长(与 DWORD 的大小相同),因此您不必关注填充(如备注部分所述)。

代码:

HDC hdcSource = NULL; // the source device context
HBITMAP hSource = NULL; // the bitmap selected into the device context

BITMAPINFO MyBMInfo = {0};
MyBMInfo.bmiHeader.biSize = sizeof(MyBMInfo.bmiHeader);

// Get the BITMAPINFO structure from the bitmap
if(0 == GetDIBits(hdcSource, hSource, 0, 0, NULL, &MyBMInfo, DIB_RGB_COLORS))
{
    // error handling
}

// create the pixel buffer
BYTE* lpPixels = new BYTE[MyBMInfo.bmiHeader.biSizeImage];

// We'll change the received BITMAPINFOHEADER to request the data in a
// 32 bit RGB format (and not upside-down) so that we can iterate over
// the pixels easily. 

// requesting a 32 bit image means that no stride/padding will be necessary,
// although it always contains an (possibly unused) alpha channel
MyBMInfo.bmiHeader.biBitCount = 32;
MyBMInfo.bmiHeader.biCompression = BI_RGB;  // no compression -> easier to use
// correct the bottom-up ordering of lines (abs is in cstdblib and stdlib.h)
MyBMInfo.bmiHeader.biHeight = abs(MyBMInfo.bmiHeader.biHeight);

// Call GetDIBits a second time, this time to (format and) store the actual
// bitmap data (the "pixels") in the buffer lpPixels
if(0 == GetDIBits(hdcSource, hSource, 0, MyBMInfo.bmiHeader.biHeight,
                  lpPixels, &MyBMInfo, DIB_RGB_COLORS))
{
    // error handling
}
// clean up: deselect bitmap from device context, close handles, delete buffer

在代码中缺少一个语句,用于初始化为MyBMInfo。.bmiHeader.biSize = sizeof(MyBMInfo); - xMRi
@xMRi 你说得对,感谢你发现了这个错误。我认为现在已经修复了,我将初始化移动到第一次调用 GetDIBits 之前;据我所知,在第一次调用后重新初始化是不必要的。 - dyp
1
嗯,我对此不太确定。我在我的代码中尝试了这个,如果我对GetDIBits进行第二次调用,我会得到一个错误,指出本地结构在堆栈上发生了缓冲区溢出。最后我又回到了手动初始化该结构的解决方案。在某些情况下,您可能不想使用压缩。重用信息将重用压缩... 我不确定这是否属实。但我没有时间进一步检查。 - xMRi
非常好的答案,非常有帮助。但是我不得不删除对32位的请求,否则第二个GetDIBits调用会失败。不知道为什么 :-) - anhoppe
我曾经遇到过同样的问题。在第一次调用后,biCompression被设置为3,因此将其设置为0是必要的,以避免在结构末尾写入额外的3个DWORD。 - Karlis Olte
很抱歉,@user64985,我已经没有Windows开发环境来维护这个答案了。根据我记得的和Jim Mischel的回答,24位可以(甚至)意味着填充。根据MSDN的说法,biHeight可能是负数,这取决于原始DIB的内存布局。 - dyp

10
GetDIBits 返回一个一维数组。对于一个宽为M像素,高为N像素,使用24位颜色的位图,前(M*3)个字节将是第一行像素。然后可能会跟随一些填充字节,这取决于BITMAPINFOHEADER。通常会有填充以使宽度成为4字节的倍数。因此,如果您的位图宽度为33像素,则每行实际上将有(36 * 3)个字节。
这种“像素加填充”的跨度称为“步幅”。对于RGB位图,您可以使用以下公式计算步幅:stride = (biWidth * (biBitCount / 8) + 3) & ~3,其中biWidthbiBitCount来自BITMAPINFOHEADER。
我不确定您想如何遍历该数组。如果您想从左上角到右下角逐像素移动(假设这是自上而下的位图):
for (row = 0; row < Image.Height; ++row)
{
    int rowBase = row*stride;
    for (col = 0; col < Image.Width; ++col)
    {
        red = lpPixels[rowBase + col];
        // etc.
    }
}

哎呀,这太糟糕了:<code>stride = (biWidth * (biBitCount / 8) + 3) & ~3</code> - 我花了一段时间才理解(向上舍入到下一个LONG = 4 [字节])。但当你需要注意填充时,这是一个简单的方法。 - dyp
1
终于有一个考虑到步幅的例子了...而不是要求先转换为32位 :) - rogerdpack
“实际上每行将有(36*3)字节”是不正确的,因为它没有将像素数舍入到4的倍数,只是将字节数舍入了。应用正确的公式,你会得到每行100个字节而不是108个字节。 - Ben Voigt

3
在您发布的链接中,您创建了一个32位位图,因此我假设您正在读取32位位图(这种假设可能是错误的)。
因此,将您的循环更改为以下内容应该可以工作:
char* pCurrPixel = (char*)lpPixels;
for ( y = 0; y < Image.Height; y++ )
{
    for ( x = 0; x < Image.Width; x++ )
    {
        red = pCurrPixel[0];
        green = pCurrPixel[1];
        blue = pCurrPixel[2];

        pCurrPixel += 4;
    }
}

需要注意的事项:

1.在C/C++中,数组是以0为基础的。
2.您每次水平和垂直移动3个像素。这意味着您没有访问到每个像素。
3.位图通常是这样组织的:有“高度”个“宽度”像素跨度。因此,您应该遍历每个跨度中的每个像素,然后移动到下一个跨度。
4.如已指出,请确保正确读取像素。在16位模式下,这更加复杂。


2

来自 MSDN 的一些惊喜:

该表由 RGBQUAD 数据结构的数组组成。(BITMAPCOREINFO 格式的表使用 RGBTRIPLE 数据结构构建。)红色、绿色和蓝色字节按照与 Windows 约定相反的顺序排列(红色与蓝色交换位置)。

因此,在 GetDIBits() 后,内存中的颜色以 BGR 顺序排列。


2
这并不容易。您的算法将取决于图像的颜色深度。如果它是256或更少,您将没有像素颜色,而是颜色调色板中的索引。16位像素可以是RGB555或RGB565,24位图像将是RGB888,32位图像将是RGBA或ARGB。您需要使用BITMAPINFOHEADER来找出这些信息。
一旦您找到了这些信息,像素数据将只是一个大小为width * height *(BitsPerPixel / 8)的数组。

或者您可以尝试使用固定/已知颜色深度将其复制/传输到位图中。+1。 - user180326
我认为OP将位图选择到设备上下文中,因此对位图拥有完全控制权。使用24位且无压缩可轻松将其读取为RGB888。 - dyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接