Reputation: 362
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
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
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:
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;
}
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