susundberg
susundberg

Reputation: 727

python3 memoryview object to C function char* parameter

The frame is following: PySide2 QImage gives me access to the internal pixel data with the bits() - call. The call returns memoryview - object that i would like to pass C-library function that does processing on the image pixel data (say it does super-fancy-blur for an example).

Problem is that i have not found a way to get memoryview underlaying data as ctypes call parameter.

That is: how can i get from memoryview object ctypes.c_void_p (or similar) -- without making copy of the buffer. The memoryview tobytes() returns bytes, but its copy from the memory, and so modifying that will not modify the original image (pixel data).

Now the minimal example python silly.py side is:

import ctypes
from PySide2.QtGui import QImage

img = QImage(500,100, QImage.Format.Format_RGB32 )
data = img.bits()
print(data)

dll = ctypes.CDLL( "./silly.o" )
dll_set_image = getattr( dll, "set_image" )
dll_set_image.argtypes = [ ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32 ]
# Next line gives: ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type
dll_set_image( data, img.width(), img.height() ) 

C-"dll": silly.c

#include <stdint.h>
#include <stdio.h>

void set_image( void* data, uint32_t xsize, uint32_t ysize )
{
   printf("Data set %lx %d %d", (uint64_t)data, xsize, ysize );
}

Compiling and testing:

% gcc -shared -o silly.o silly.c
% python3 silly.py              
<memory at 0x7f2f9806a700>
Traceback (most recent call last):
  File "silly.py", line 13, in <module>
    dll_set_image( data, img.width(), img.height() ) 
ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type

Upvotes: 2

Views: 1111

Answers (3)

susundberg
susundberg

Reputation: 727

For completeness sake ppl finding this page from google, yet another alternative that works is based on making python module with C/C++ as described on docs.

It requires some boilerplate, but the actual access to buffer is easy:

PyObject* set_image(PyObject *self, PyObject *args) 
{
    Py_buffer buffer;
    long xsize;
    long ysize;
    int ok = PyArg_ParseTuple(args, "y*ll", &buffer, &xsize, &ysize); 

    if ( ok != 1 )
        return NULL;

    printf("Data set: %d %d - %lx\n", (int)xsize, (int)ysize, (unsigned long)buffer.buf );

    Py_RETURN_NONE;
}

Upvotes: 1

Mark Tolonen
Mark Tolonen

Reputation: 177481

A memoryview supports the Python buffer protocol, so create a ctypes array of the appropriate size using its from_buffer method, which won't make a copy but reference the same memory.

Modifying your example slightly to see the buffer change:

silly.c:

#include <stdint.h>
#include <stdio.h>

#ifdef _WIN32
#   define API __declspec(dllexport)
#else
#   define API
#endif

API void set_image(unsigned char* data, uint32_t xsize, uint32_t ysize)
{
    printf("%p %u %u\n",data,xsize,ysize);
    *data = 123;
}

test.py:

import ctypes
from PySide2.QtGui import QImage

img = QImage(500,100, QImage.Format.Format_RGB32 )
data = img.bits()
print('original:',data[0])

dll = ctypes.CDLL('./silly')
dll_set_image = dll.set_image
dll_set_image.argtypes = ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32

buffer = (ctypes.c_ubyte * len(data)).from_buffer(data)
dll_set_image(buffer, img.width(), img.height())
print('final:',data[0])

Output:

original: 80
00000213CCF3C080 500 100
final: 123

Upvotes: 4

eyllanesc
eyllanesc

Reputation: 243907

memoryview is not a void * but a structure (Py_buffer) that within its members is the buffer, so the conversion is not direct.

In these cases I prefer to use Cython(python -m pip install cython) that allows creating wrappers to libraries in a simple way. Considering the above I have created a project:

├── main.py
├── psilly.c
├── psilly.pyx
├── setup.py
├── silly.c
├── silly.h
└── silly.pxd

silly.h

extern void set_image( unsigned char* data, uint32_t xsize, uint32_t ysize, uint32_t channels);

silly.c

#include <stdint.h>

#include "silly.h"

void set_image( unsigned char* data, uint32_t xsize, uint32_t ysize, uint32_t channels)
{
    // fill white color
    for (uint32_t i = 0; i < (xsize * ysize * channels); i++)
    {
        data[i] = 255;
    }
}

silly.pxd

from libc.stdint cimport uint32_t

cdef extern from "silly.h":  
    void set_image(unsigned char* data, uint32_t xsize, uint32_t ysize, uint32_t channels)

psilly.pyx

cimport silly

cpdef set_image(unsigned char[:] data, int width, int height, int channels):
    silly.set_image(&data[0], width, height, channels)

setup.py

from distutils.core import Extension, setup
from Cython.Build import cythonize

ext = Extension(
    name="psilly",
    sources=["psilly.pyx", "silly.c"],
)
setup(ext_modules=cythonize(ext))

main.py

import psilly

from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
from PySide2.QtGui import QImage, QPixmap

app = QApplication()

img = QImage(500, 100, QImage.Format.Format_RGB32)
data = img.bits()

label1 = QLabel()
label1.setPixmap(QPixmap.fromImage(img.copy()))

psilly.set_image(data, img.width(), img.height(), 4)

label2 = QLabel()
label2.setPixmap(QPixmap.fromImage(img))

widget = QWidget()
lay = QHBoxLayout(widget)
lay.addWidget(label1)
lay.addWidget(label2)
widget.show()

app.exec_()

Compiling and testing:

python setup.py build_ext --inplace
python main.py

Output:

enter image description here

Note: When the project is compiled, it is generated as an intermediate element to psilly.c where you will see the conversion between memoryview and unsigned char * using the Py_buffer structure.

Upvotes: 0

Related Questions