wenhoo
wenhoo

Reputation: 362

grayscale hbitmap with python ctypes

I have PIL images that I am trying to convert to grayscale HBitmap in ctypes. I have minimal knowledge of ctypes, C, or dealing with HBITMAPs. I cobbled together code from various sources, such as

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

This is what I have so far. First, I initialized the required headers:

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

Then, I created my 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

I am uploading these HBITMAPs to some projector via separate lines of code in Python. The HBITMAPs I created seem to work partially, in that I can successfully define spatial patterns to be projected. I have problems instead with getting graded pixel intensity. Specifically, pixels show up as black if I set values from 0-127, and white if I set values from 128-255, with no gradations. These lead to me suspect that it is a problem with setting the RGB color palette.

I have directly saved the PIL image files to .bmp and verified that they have graded intensity values. Perhaps it would be easier to troubleshoot if I had a way to also save the HBITMAP output at the end to .bmp, but at this stage I am only checking these HBITMAPs by directly uploading to my projector.

I have also tried screwing with the code that defines the color palette, for example:

bmi.bmiColors[i].rgbRed=9999

or:

bmi.bmiColors[i].rgbsRed=i

But none of these seem to have any effect on the output of my projector. I can still set images accurately, just with no graded pixel intensities.

Upvotes: 0

Views: 1243

Answers (1)

dimitsev
dimitsev

Reputation: 137

@OP: What broke your python code was the line:

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

Use instead:

('bmiColors', RGBQUAD * 256)

initialize like this:

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)]))

and set bmi.bmiHeader.biWidth and bmi.bmiHeader.biHeight whenever necessary.

Notes about using this in python with ctypes:

  • Set .argtypes for every C function you import, wherever possible. Not doing so can throw exceptions, even when everything appears to be in order.
  • Use classes and initialize BITMAPINFO like below: (code is incomplete!!!)
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)

As for a complete way to do it in C++, here is a code example that creates an 8bit grayscale DIB and draws it on the primary monitor. Just compile it into an .exe and run it and you will see a diagonal grayscale pattern on your primary monitor. Explanations follow below.

#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;
}
  • Use malloc() to allocate the BITMAPINFO. Other examples have used alloca(), which causes the BITMAPINFO to be garbage collected before the DIB is drawn on the screen. If you are a physicist like me and don't give a hoot about programming details, always use malloc() and remember to manually free() the memory afterwards.
  • I have no idea what is happenning with the HDC handles, here and in general.
  • When setting the bmiColors, casting the integer counter i to (BYTE) looks unclean, but should be safe for i < 256. I did this to prevent a compiler warning about information loss.
  • offsetof(BITMAPINFO, bmiColors[256]) and sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*256 give me the same result for 8 bits per pixel.

Application idea: This might be valuable to people drawing grayscale images on pixelated tools such as Liquid Crystal on Silicon Spatial Light Modulators (LCOS SLM). An SLM driver like this would eliminate the need for an additional thread/process to run a window on the SLM. A speed comparison with a PyQt5 window in a separate process (multiprocessing) on the SLM yielded an delay time that was lower by approx. 10 ms, probably because no cross-process communication was necessary. I am mentioning this for the search engines.

Upvotes: 0

Related Questions