带有python ctypes的灰度hbitmap

时间:2016-09-23 21:38:30

标签: python winapi bitmap ctypes gdi

我有PIL图像,我试图在ctypes中转换为灰度HBitmap。我对ctypes,C或处理HBITMAP的知识很少。我拼凑了各种来源的代码,例如

  1. Drawing on 8bpp grayscale bitmap (unmanaged C++)
  2. http://d.hatena.ne.jp/chrono-meter/20090905/p3
  3. 这是我到目前为止所拥有的。首先,我初始化了所需的标题:

    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
    

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

1 个答案:

答案 0 :(得分:0)

@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。不这样做可能会引发异常,即使一切看起来都井井有条。
  • 使用类并初始化 BITMPINFO,如下所示:(代码不完整!!!)
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() 分配 BITMPINFO。其他示例使用了 alloca(),这会导致在将 DIB 绘制到屏幕上之前对 BITMPINFO 进行垃圾收集。如果您是像我一样的物理学家并且不关心编程细节,请始终使用 malloc() 并记住之后手动 free() 内存。
  • 我不知道这里和一般情况下 HDC 句柄发生了什么。
  • 设置 bmiColors 时,将整数计数器 i 强制转换为 (BYTE) 看起来不干净,但对于 i < 256 应该是安全的。我这样做是为了防止编译器发出有关信息丢失的警告。
  • offsetof(BITMAPINFFO, bmiColors[256]) 和 sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*256 给我相同的结果,每像素 8 位。

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