使用SWIG将Python PIL图像进行编组

8

我有一个库,它接受一个非常简单的C图像结构:

// Represents a one-channel 8-bit image
typedef struct simple_image_t {
    uint32 rows;
    uint32 cols;
    uint8 *imgdata;
} simple_image;

我没有创建这个库,也没有创建这个结构,所以我不能改变它。我的责任是使用SWIG为Python封装这个库。Python包装器需要能够接收PIL图像并将其转换为这个结构。这是我目前的做法(使用SWIG %inline%):

// Allows python to easily create and initialize this structure
simple_image* py_make_simple_image(uint32 width, uint32 height)
{
    simple_image* img = new simple_image();
    img->rows = height;
    img->cols = width;
    img->imgdata = new uint8[height * width];

    return img;
}

// Allows python to set a particular pixel value
void py_set_simple_image(simple_image* img, uint32 pos, uint8 val)
{
    img->imgdata[pos] = val;
}

现在来看一下Python包装器的情况:

# Make sure it's an 8-bit image
if pil_image.mode != "L":
    pil_image = pil_image.convert("L")

# Create the simple image structure
(width, height) = pil_image.size
img = swig_wrapper.py_make_simple_image(width, height)

try:
    # Copy the image data into the simple image structure
    pos = 0
    for pixel in pil_image.getdata():
        swig_wrapper.py_set_simple_image(img, pos, pixel)
        pos += 1

    # Call some library method that accepts a simple_image*
    return swig_wrapper.some_image_method(img)

finally:
    # Clean up the simple image structure
    swig_wrapper.py_destroy_simple_image(img)

惊人的是,这个方法确实可行,但是你可能已经猜到了,当处理相对较大的图像时,速度非常缓慢。我知道使用SWIG正确的做法是使用typemap,但这意味着要深入PIL的C API,而我目前没有时间去做。

在速度方面,我的选择是什么?有更快的方式来将PIL图像中的像素数据转换为这个简单的图像结构吗?有人已经做过这个事情,只是我的谷歌搜索技能太差了吗?还是我注定要学习PIL的内部工作呢?

谢谢。


PIL的Image类包含一些序列化方法 - getdatatostring。其中一个肯定可以被强制转换为您所需的操作。 - zdav
@zdav:我已经在使用getdata了,问题是使用这种方法设置我的数据太慢了。请看代码。 - Chris Eberle
3个回答

5

PIL的Image.tostring()返回一个精确的字符串数据,可用于imgdata。我使用的类型映射非常简单,但并不完美,下面会说明。以下是在Windows上为我工作的示例代码:

sample.h

typedef unsigned int uint32;
typedef unsigned char uint8;

typedef struct simple_image_t {
    uint32 rows;
    uint32 cols;
    uint8 *imgdata;
} simple_image;

#ifdef SAMPLE_EXPORT
#   define SAMPLE_API __declspec(dllexport)
#else
#   define SAMPLE_API __declspec(dllimport)
#endif

SAMPLE_API void some_func(const simple_image* si);

sample.c

#include <stdio.h>

#define SAMPLE_EXPORT
#include "sample.h"

void some_func(const simple_image* si)
{
    uint32 i,j;

    printf(
        "rows = %d\n"
        "cols = %d\n",
        si->rows,si->cols);

    /* Dump a simple map of the image data */
    for(i = 0; i < si->rows; i++)
    {
        for(j = 0; j < si->cols; j++)
        {
            if(si->imgdata[i * si->rows + j] < 0x80)
                printf(" ");
            else
                printf("*");
        }
        printf("\n");
    }
}

sample.i

%module sample

%begin %{
#pragma warning(disable:4100 4127 4706)
%}

%{
#include "sample.h"
%}

%include <windows.i>

%typemap(in) uint8* (char* buffer, Py_ssize_t length) {
    PyString_AsStringAndSize($input,&buffer,&length);
    $1 = (uint8*)buffer;
}

%include "sample.h"

makefile

all: _sample.pyd

sample.dll: sample.c sample.h
    cl /nologo /W4 /LD /MD sample.c

sample_wrap.c: sample.i
    @echo sample.i
    swig -python sample.i

_sample.pyd: sample_wrap.c sample.dll
    cl /nologo /W4 /LD /MD /Fe_sample.pyd sample_wrap.c /Ic:\Python27\include -link /LIBPATH:c:\Python27\libs python27.lib sample.lib

example.py

from PIL import Image
import sample

im = Image.open('sample.gif')
im = im.convert('L')
si = sample.simple_image()
si.rows,si.cols = im.size
s = im.tostring() # Must keep a reference 
si.imgdata = s
sample.some_func(si)

通过这个简单的示例,我还没有确定类型映射应该如何正确地增加字符串对象的引用计数。请注意,如果使用以下代码,则上述代码可能会崩溃:

si.imgdata = im.tostring()

当前类型映射的PyString_AsStringAndSize返回 PyString 对象缓冲区的直接指针,但不会增加对象的引用计数。在 some_func 执行之前它可能被垃圾回收(对我来说就是这样,导致 Python 崩溃)。将值赋给s可以保留字符串的引用并避免问题。类型映射应该复制缓冲区,但您可能正在寻找速度,因此这个 hack 可能是您想要的。


哇,这看起来像是一个很棒的答案。我现在手头忙不过来,所以无法验证它是否有效,但这看起来就是正确的答案。给我几天时间,我会将其标记为答案。 - Chris Eberle
我现在真想亲你一口,但我会冷静下来,授予你300分。这大大降低了延迟。我怀疑你关于引用计数的看法是正确的,但我可以自己解决。非常感谢,这避免了双重复制,速度现在相当可接受。 - Chris Eberle

1
也许你可以使用 array 模块将图像转换为字符数组,然后通过 swig 将数据复制到 C 数组中。
import array
imagar = array.array('B', pil_image.getdata())
(mem, length) = imagar.buffer_info()
swig_wrapper.py_copy(img, mem, length)

成为类似于py_copy的东西:

void py_copy(simple_image* img, uint32 mem, uint32 length) {
   memcpy((void*)img->imgdata ,(void*)mem, length );
}

那将时间从大约2.5秒(平均值)缩短到了约1.8秒。我对这个努力表示赞赏,但仍然存在很多开销(至少有两个立即显而易见的副本)。 - Chris Eberle
不要使用getdata(),它很慢,你可以将图像转换为字符串。我知道最快的方法是直接从PIL对象进行转换,但这需要动手操作。分析信息可以帮助识别瓶颈。 - Samuel

0

使用ctypes如何?这允许您直接访问c结构,因此无需创建struct的Python等效项,您还应该能够执行memcpy(这比逐像素复制更快)。


我该如何完成memcpy?我相当确定pil_image.getdata()返回一个可迭代对象,这在memcpy中是没有用的... - Chris Eberle
1
对于memcpy的观点很好 - 这取决于PIL图像的像素缓冲区是否全部连续存储在内存中。但是如果是这样,我进行了Google搜索,并找到了类似的问题(和被接受的答案)。https://dev59.com/6k7Sa4cB1Zd3GeqP0xoq - michael pan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接