带有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 我现在已将 biCompression 和 biSizeImage 都设置为 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] 参数的原型