带有python ctypes的灰度hbitmap

Posted

技术标签:

【中文标题】带有python ctypes的灰度hbitmap【英文标题】:grayscale hbitmap with python ctypes 【发布时间】:2017-02-01 20:29:34 【问题描述】:

我有 PIL 图像,我正在尝试将其转换为 ctypes 中的灰度 HBitmap。我对 ctypes、C 或处理 HBITMAP 知之甚少。我拼凑了来自各种来源的代码,例如

    Drawing on 8bpp grayscale bitmap (unmanaged C++) http://d.hatena.ne.jp/chrono-meter/20090905/p3

这是我目前所拥有的。首先,我初始化了所需的标头:

import ctypes
from ctypes import wintypes

class BITMAPINFOHEADER(ctypes.Structure):
    _fields_ = [
        ('biSize', wintypes.DWORD),
        ('biWidth', wintypes.LONG),
        ('biHeight', wintypes.LONG),
        ('biPlanes', wintypes.WORD),
        ('biBitCount', wintypes.WORD),
        ('biCompression', wintypes.DWORD),
        ('biSizeImage', wintypes.DWORD),
        ('biXPelsPerMeter', wintypes.LONG),
        ('biYPelsPerMeter', wintypes.LONG),
        ('biClrUsed', wintypes.DWORD),
        ('biClrImportant', wintypes.DWORD),
        ]

class RGBQUAD(ctypes.Structure):
    _fields_ = [
        ('rgbRed', ctypes.c_byte),
        ('rgbGreen', ctypes.c_byte),
        ('rgbBlue', ctypes.c_byte),
        ('rgbReserved', ctypes.c_byte),
    ]

class BITMAPINFO(ctypes.Structure):
    _fields_ = [
        ('bmiHeader', BITMAPINFOHEADER),
        ('bmiColors', ctypes.POINTER(RGBQUAD))
    ]

w,h=image.size

bmi = BITMAPINFO()
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = w
bmi.bmiHeader.biHeight = h
bmi.bmiHeader.biPlanes = 1
bmi.bmiHeader.biBitCount = 8
bmi.bmiHeader.biCompression = 0 
bmi.bmiHeader.biSizeImage = 0 

elems=(RGBQUAD*256)()
bmi.bmiColors=ctypes.cast(elems,ctypes.POINTER(RGBQUAD))


for i in range(256):
    bmi.bmiColors[i].rgbRed=i
    bmi.bmiColors[i].rgbGreen=i
    bmi.bmiColors[i].rgbBlue=i
    bmi.bmiColors[i].rgbReserved=0

然后,我创建了我的 hbitmap:

ctypes.windll.LoadLibrary('C:\Windows\System32\gdi32.dll')
gdi=ctypes.WinDLL('C:\Windows\System32\gdi32.dll')

hDC = gdi.CreateCompatibleDC(0)

try:
    dataptr = ctypes.c_void_p()
    result = gdi.CreateDIBSection(hDC, ctypes.byref(bmi), 0,
                                  ctypes.byref(dataptr), None, 0)

    hOldBitmap = gdi.SelectObject(hDC, result)
    try:
        buf = imagebytes
        wintypes.memmove(dataptr, buf, len(buf))
    finally:
        gdi.SelectObject(hDC, hOldBitmap)

finally:
    gdi.DeleteDC(hDC)

hbitmap = result

我正在通过 Python 中的单独代码行将这些 HBITMAP 上传到某些投影仪。我创建的 HBITMAP 似乎部分起作用,因为我可以成功定义要投影的空间模式。我有问题,而不是获得分级像素强度。具体来说,如果我将值设置为 0-127,像素将显示为黑色,如果我将值设置为 128-255,则像素显示为白色,没有渐变。这些让我怀疑这是设置 RGB 调色板的问题。

我已将 PIL 图像文件直接保存为 .bmp 并验证它们具有分级强度值。如果我有办法将最后的 HBITMAP 输出保存到 .bmp,也许会更容易排除故障,但在这个阶段,我只是通过直接上传到我的投影仪来检查这些 HBITMAP。

我也尝试过修改定义调色板的代码,例如:

bmi.bmiColors[i].rgbRed=9999

或:

bmi.bmiColors[i].rgbsRed=i

但这些似乎都对我的投影仪的输出没有任何影响。我仍然可以准确地设置图像,只是没有分级像素强度。

【问题讨论】:

HBITMAP 是一种结构数据类型,它不是一种文件格式。你能举一些例子或链接吗? 抱歉,我只想说 HBITMAP 句柄,我通过 python 将它传递给我的投影仪系统。 “我无法分配调色板值” - 具体如何?它在运行时失败了吗?它成功了,但没有产生预期的结果吗?代码存在一些问题:您没有将 biCompression 成员设置为 BI_RGB(这是必需的)。您将 biSizeImage 设置为非零值,但它是错误的值,因为它没有考虑到扫描线与DWORD 边界的对齐。然后,BITMAPINFO 的大小是可变的(请参阅Why do some structures end with an array of size 1? 以获得解释)。 @IInspectable 我现在已将 biCompressionbiSizeImage 都设置为 0。但是,我对 C++ 的了解太少,不知道如何继续:BITMAPINFO。我知道 BITMAPINFO 由于 bmiColors 的大小可变(正确吗?)。这是否意味着我必须为 BITMAPINFO 正确分配内存?我不知道如何在 ctypes 中执行此操作,我找不到与 alloca() 或 offsetof() 对应的函数,正如我在您在 ***.com/questions/3142349/… 上的回答中看到的那样 【参考方案1】:

@OP:破坏你的 python 代码的是这行:

('bmiColors', ctypes.POINTER(RGBQUAD))

改用:

('bmiColors', RGBQUAD * 256)

像这样初始化:

bmi = BITMAPINFO(BITMAPINFOHEADER(sizeof(BITMAPINFOHEADER), 0, 0, 1, 8, 0, 0, 0, 0, 0, 0),
                 (RGBQUAD * 256)(*[RGBQUAD(i,i,i,0) for i in range(256)]))

并在必要时设置 bmi.bmiHeader.biWidth 和 bmi.bmiHeader.biHeight。

关于在 python 中使用 ctypes 的注意事项:

尽可能为您导入的每个 C 函数设置 .argtypes。不这样做可能会引发异常,即使一切看起来都井井有条。 使用类并初始化 BITMAPINFO,如下所示:(代码不完整!!!)
import ctypes
from ctypes import c_ubyte, c_int, c_uint, c_void_p, POINTER, byref, sizeof
from ctypes.wintypes import WORD, DWORD, LONG, HDC

class RGBQUAD(ctypes.Structure):
    _fields_ = [
        ('rgbRed', c_ubyte),
        ('rgbGreen', c_ubyte),
        ('rgbBlue', c_ubyte),
        ('rgbReserved', c_ubyte)
    ]
class BITMAPINFOHEADER(ctypes.Structure):
    _fields_ = [
        ('biSize', DWORD),
        ('biWidth', LONG),
        ('biHeight', LONG),
        ('biPlanes', WORD), # 1
        ('biBitCount', WORD), # 8
        ('biCompression', DWORD), # BI_RGB = 0 for uncompressed format
        ('biSizeImage', DWORD), # 0
        ('biXPelsPerMeter', LONG), # 0
        ('biYPelsPerMeter', LONG), # 0
        ('biClrUsed', DWORD), # 0
        ('biClrImportant', DWORD) # 0
    ]
class BITMAPINFO(ctypes.Structure):
    _fields_ = [
        ('bmiHeader', BITMAPINFOHEADER),
        ('bmiColors', RGBQUAD * 256)
    ]

SetDIBitsToDevice = ctypes.windll.Gdi32.SetDIBitsToDevice
SetDIBitsToDevice.restype = BOOL # 0 if failed
SetDIBitsToDevice.argtypes = [HDC, c_int, c_int, DWORD, DWORD, c_int, c_int, c_uint, c_uint, c_void_p, POINTER(BITMAPINFO), c_uint]

bmi = BITMAPINFO(BITMAPINFOHEADER(sizeof(BITMAPINFOHEADER), 0, 0, 1, 8, 0, 0, 0, 0, 0, 0),
                 (RGBQUAD * 256)(*[RGBQUAD(i,i,i,0) for i in range(256)]))

SLM_HDC = CreateDC(None, monitor.info.szDevice, None, None)
data = np.array(...).astype(np.uint8)
data_p = data.ctypes.data_as(c_void_p)
SetDIBitsToDevice(SLM_HDC,
                  0, 0,
                  monitor.width(), monitor.height(),
                  0, 0,
                  0, monitor.height(),
                  data_p, byref(bmi), 0)

至于在 C++ 中执行此操作的完整方法,这里有一个代码示例,它创建一个 8 位灰度 DIB 并将其绘制在主监视器上。只需将其编译为 .exe 并运行它,您就会在主显示器上看到对角线灰度图案。说明如下。

#include <cstdlib>
#include <iostream>
#include <malloc.h>
#include <windows.h>

// tell linker where to resolve external dependencies
#pragma comment(lib, "User32.lib")
#pragma comment(lib, "Gdi32.lib")

BITMAPINFO* CreateGreyscaleBITMAPINFO_P(int width, int height) 
    BITMAPINFO* pbmi = (BITMAPINFO*) std::malloc(offsetof(BITMAPINFO, bmiColors[256]));
    pbmi->bmiHeader.biSize = sizeof(pbmi->bmiHeader);
    pbmi->bmiHeader.biWidth = width;
    pbmi->bmiHeader.biHeight = height;
    pbmi->bmiHeader.biPlanes = 1;
    pbmi->bmiHeader.biBitCount = 8;
    pbmi->bmiHeader.biCompression = BI_RGB;
    pbmi->bmiHeader.biSizeImage = 0;
    pbmi->bmiHeader.biXPelsPerMeter = 0;
    pbmi->bmiHeader.biYPelsPerMeter = 0;
    pbmi->bmiHeader.biClrUsed = 0;
    pbmi->bmiHeader.biClrImportant = 0;
    for(int i=0; i<256; i++) 
        pbmi->bmiColors[i].rgbRed = (BYTE)i;
        pbmi->bmiColors[i].rgbGreen = (BYTE)i;
        pbmi->bmiColors[i].rgbBlue = (BYTE)i;
        pbmi->bmiColors[i].rgbReserved = (BYTE)0;
    
    return pbmi;


int main(int argc, char** argv) 
    // to identify screen resolution correctly
    SetProcessDPIAware();

    // get HWND of full primary monitor to retrieve screen resolution and get HDC for drawing
    HWND desktop_HWND = GetDesktopWindow();
    LPRECT desktop_RECT = new RECT();
    if(GetWindowRect(desktop_HWND, desktop_RECT) == 0)  return 0; 
    int width = std::abs(desktop_RECT -> right - desktop_RECT -> left);
    int height = std::abs(desktop_RECT -> bottom - desktop_RECT -> top);
    HDC desktop_DC = GetDC(desktop_HWND);

    // define array with linearly increasing pixel value along the diagonal x=y
    // pixels have 8bit grayscale values from 0 (black) to 255 (white)
    BYTE* array = (BYTE*) std::malloc(sizeof(BYTE) * width * height);
    for(int i=0; i<height; i++) 
        for(int j=0; j<width; j++) 
            array[i*width + j] = ((j + i) % 256);
        
    

    // initialize a BITMAPINFO instance and draw on desktop with SetDIBitsToDevice()
    const BITMAPINFO* bmip = CreateGreyscaleBITMAPINFO_P(width, height);
    int result = SetDIBitsToDevice(
        desktop_DC,
        0, 0, width, height,
        0, 0, 0, height,
        array, bmip, DIB_RGB_COLORS
        );

    // print out for debugging
    std::cout << "primary monitor resolution: " << width << " (width) x " << height << " (height)" << std::endl;
    std::cout << "naive BITMAPINFO length (BYTES): " << sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*256
    << " vs. with windows macro offsetof(): " << offsetof(BITMAPINFO, bmiColors[256]) << std::endl;
    std::cout << "bmiHeader.biSize: " << bmip->bmiHeader.biSize << std::endl;
    std::cout << "number of lines drawn on monitor: " << result << std::endl;

使用 malloc() 分配 BITMAPINFO。其他示例使用了 alloca(),这会导致在屏幕上绘制 DIB 之前对 BITMAPINFO 进行垃圾收集。如果您像我一样是物理学家,并且不关心编程细节,请始终使用 malloc() 并记住之后手动 free() 内存。 我不知道 HDC 句柄在这里和一般情况下发生了什么。 设置 bmiColors 时,将整数计数器 i 转换为 (BYTE) 看起来不干净,但对于 i offsetof(BITMAPINFO, bmiColors[256]) 和 sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*256 对于每像素 8 位给出相同的结果。

应用理念:这可能对人们在像素化工具上绘制灰度图像很有价值,例如硅上液晶Spatial Light Modulators (LCOS SLM)。像这样的 SLM 驱动程序将消除在 SLM 上运行窗口的额外线程/进程的需要。与 SLM 上单独进程(多处理)中的 PyQt5 窗口的速度比较产生的延迟时间降低了大约 100 倍。 10 毫秒,可能是因为不需要跨进程通信。我是为搜索引擎提到的。

【讨论】:

以上是关于带有python ctypes的灰度hbitmap的主要内容,如果未能解决你的问题,请参考以下文章

使用 ctypes 和 windll 的带有图标的 Python MessageBox

Python ctypes:带有 LPCSTR [out] 参数的原型

如何通过 Bitmap::GetHBITMAP 将位图转换为带有 alpha 的 HBITMAP?

Python ctypes:如何释放内存?获取无效指针错误

Python ctypes:如何释放内存?获取无效指针错误

如何像 python ctypes 那样指定 JNR 指针