使用ctypes数组时PIL的Image.frombuffer预期的数据长度

4 投票
2 回答
8449 浏览
提问于 2025-04-17 00:04

我正在使用Python、PIL和ctypes来处理图像。在我拼凑代码的时候,我用PIL的fromstring函数把ctypes中的像素缓冲区转成了PIL对象。我只是简单地遍历数组,构建了一个Python字符串。

这能工作

tx = foo.tx
tx.restype = POINTER(c_ubyte)
result = tx(...args...)

#TODO there must also be a better way to do this
pystr = ""
for i in xrange(w*h*4):
   pystr += result[i]
i = Image.fromstring("RGBA", (w, h), pystr)
i.save("out.png")

虽然看起来不太好,但确实能用。我在代码里加了个TODO,然后继续往下写。等到初步的代码搭建完成后,性能分析显示这个代码块有明显的性能问题。这个结果也不奇怪。

这不能工作

类似于这个问题:使用PIL.Image和ctypes进行像素操作,我想用frombuffer()更高效地把像素数据放进PIL对象里:

tx = foo.tx
tx.restype = POINTER(c_ubyte)
result = tx(...args...)
i = Image.frombuffer('RGBA', (w, h), result, 'raw', 'RGBA', 0, 1)
i.save("out.png")

虽然fromstring能用,但使用frombuffer时却出现了以下异常:

Traceback (most recent call last):
  File "test.py", line 53, in <module>
    i = Image.frombuffer('RGBA', (w, h), res, 'raw', 'RGBA', 0, 1)
  File "/Library/Python/2.7/site-packages/PIL/Image.py", line 1853, in frombuffer
    core.map_buffer(data, size, decoder_name, None, 0, args)
ValueError: buffer is not large enough

环境

这个缓冲区是在C语言中用malloc分配的,具体如下:

unsigned char *pixels_color = (unsigned char*)malloc((WIDTH*HEIGHT*4)*sizeof(unsigned char*));
  • 每个像素有4个字节,分别对应RGBA四个通道。
  • 操作系统是Mac OS X 10.7,Python版本是2.7.1,PIL版本是1.1.7

编辑

根据eryksun的评论和下面的回答,我把缓冲区的分配从C库的malloc转到了Python里,并把指针传给C:

tx = foo.tx
tx.restype = POINTER(c_ubyte)

pix_clr = (c_ubyte*(w*h*4))()

tx(...args..., pix_clr)
i = Image.frombuffer('RGBA', (w, h), pix_clr, 'raw', 'RGBA', 0, 1)
i.save("out.png")

这样做后,效果如预期。这仍然不能解释为什么PIL对C分配的缓冲区不满意,但这对于内存管理来说是更好的结构。感谢ErykSun和HYRY的帮助!

2 个回答

3

试试下面的代码:

import Image
from ctypes import c_ubyte, cast, POINTER

buf = (c_ubyte * 400)()
pbuf = cast(buf, POINTER(c_ubyte))
pbuf2 = cast(pbuf, POINTER(c_ubyte*400))

img1 = Image.frombuffer("RGBA", (10,10), buf, "raw", "RGBA", 0, 1)
img2 = Image.frombuffer("RGBA", (10,10), pbuf2.contents, "raw", "RGBA", 0, 1)

buf 是一个无符号字节数组,pbuf 是指向无符号字节的指针,pbuf2 是指向无符号字节数组(大小为400)的指针。img1 是直接从 buf 创建的,img2 是从 pbuf2.contents 创建的。

你的程序是从一个指向无符号字节的指针创建图像的,你必须把它转换成指向数组的指针,并使用 contents 属性来获取缓冲区。所以使用下面的代码来转换指针为数组:

tmp = cast(result, POINTER(c_ubyte*4*w*h)).contents
Image.frombuffer('RGBA', (w, h), tmp, 'raw', 'RGBA', 0, 1)
3

当你设置 tx.restype = POINTER(c_ubyte) 时,得到的 ctypes 指针对象是一个 4 或 8 字节的缓冲区,用来存放图像缓冲区的 地址。你需要从这个地址创建一个 ctypes 数组。

如果你提前知道数组的大小,可以设置 SIZE = WIDTH * HEIGHT * 4; tx.restype = POINTER(c_ubyte * SIZE)。如果不知道大小,就设置 tx.restype = POINTER(c_ubyte),然后转换为动态大小,也就是 size = w * h * 4; pbuf = cast(result, POINTER(c_ubyte * size))。无论哪种方式,你都需要解引用这个指针才能访问数组。可以使用 buf = pbuf[0]buf = pbuf.contents。然后你就可以把 buf 传给 Image.frombuffer

不过,通常使用 ctypes 来分配内存会更简单。这种方式提供了引用计数的内存管理,不需要手动释放内存。下面是一个简单的示例。

假设大小是动态的,调用者需要获取图像的尺寸,以便分配数组,所以我添加了一个结构体,里面包含图像的宽度、高度和深度,这些信息会由 C 函数 get_image_info 填充。函数 get_image 接收一个指向图像数组的指针,用来复制数据。

import ctypes

lib = ctypes.CDLL('imgtest.dll')

class ImageInfo(ctypes.Structure):
    _fields_ = (
        ('width', ctypes.c_int),
        ('height', ctypes.c_int),
        ('depth', ctypes.c_int),
    )

pImageInfo = ctypes.POINTER(ImageInfo)
pImage = ctypes.POINTER(ctypes.c_ubyte)

lib.get_image_info.argtypes = [pImageInfo]
lib.get_image_info.restype = ctypes.c_int

lib.get_image.argtypes = [pImage]
lib.get_image.restype = ctypes.c_int

imgnfo = ImageInfo()
lib.get_image_info(ctypes.byref(imgnfo))
w, h, d = imgnfo.width, imgnfo.height, imgnfo.depth

imgdata = (w * h * d * ctypes.c_ubyte)()
lib.get_image(imgdata)

from PIL import Image
img = Image.frombuffer('RGBA', (w, h), imgdata, 'raw', 'RGBA', 0, 1)

C 语言声明:

typedef struct _ImageInfo {
    int width;
    int height;
    int depth;
} ImageInfo, *pImageInfo;

typedef unsigned char *pImage;

int get_image_info(pImageInfo imgnfo);
int get_image(pImage img);

撰写回答