Reproduce Python 2 PyQt4 QImage constructor behavior in Python 3
Asked Answered
S

1

1

I have written a small GUI using PyQt4 that displays an image and gets point coordinates that the user clicks on. I need to display a 2D numpy array as a grayscale, so I am creating a QImage from the array, then from that creating a QPixmap. In Python 2 it works fine.

When I moved to Python 3, however, it can't decide on a constructor for QImage - it gives me the following error:

TypeError: arguments did not match any overloaded call:
  QImage(): too many arguments
  QImage(QSize, QImage.Format): argument 1 has unexpected type 'numpy.ndarray'
  QImage(int, int, QImage.Format): argument 1 has unexpected type 'numpy.ndarray'
  QImage(str, int, int, QImage.Format): argument 1 has unexpected type 'numpy.ndarray'
  QImage(sip.voidptr, int, int, QImage.Format): argument 1 has unexpected type 'numpy.ndarray'
  QImage(str, int, int, int, QImage.Format): argument 1 has unexpected type 'numpy.ndarray'
  QImage(sip.voidptr, int, int, int, QImage.Format): argument 1 has unexpected type 'numpy.ndarray'
  QImage(list-of-str): argument 1 has unexpected type 'numpy.ndarray'
  QImage(str, str format=None): argument 1 has unexpected type 'numpy.ndarray'
  QImage(QImage): argument 1 has unexpected type 'numpy.ndarray'
  QImage(object): too many arguments

As far as I can tell, the QImage constructor I was calling previously was one of these:

  • QImage(str, int, int, QImage.Format)
  • QImage(sip.voidptr, int, int, QImage.Format)

I'm assuming that a numpy array fits one of the protocols necessary for one of these. I'm thinking it might have to do with an array versus a view, but all the variations I've tried either produce the above error or just make the GUI exit without doing anything. How can I reproduce the Python 2 behavior in Python 3?

The following is a small example, in which the same exact code works fine under Python 2 but not Python 3:

from __future__ import (print_function, division)

from PyQt4 import QtGui, QtCore
import numpy as np

class MouseView(QtGui.QGraphicsView):

    mouseclick = QtCore.pyqtSignal(tuple)

    def __init__(self, scene, parent=None):
        super(MouseView, self).__init__(scene, parent=parent)

    def mousePressEvent(self, event):
        self.mouseclick.emit((event.x(),
                              self.scene().sceneRect().height() - event.y()))


class SimplePicker(QtGui.QDialog):

    def __init__(self, data, parent=None):
        super(SimplePicker, self).__init__(parent)

        mind = data.min()
        maxd = data.max()
        bdata = ((data - mind) / (maxd - mind) * 255.).astype(np.uint8)

        wdims = data.shape
        wid = wdims[0]
        hgt = wdims[1]

        # This is the line of interest - it works fine under Python 2, but not Python 3
        img = QtGui.QImage(bdata.T, wid, hgt,
                           QtGui.QImage.Format_Indexed8)

        self.scene = QtGui.QGraphicsScene(0, 0, wid, hgt)
        self.px = self.scene.addPixmap(QtGui.QPixmap.fromImage(img))

        view = MouseView(self.scene)
        view.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        view.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        view.setSizePolicy(QtGui.QSizePolicy.Fixed,
                           QtGui.QSizePolicy.Fixed)
        view.setMinimumSize(wid, hgt)
        view.mouseclick.connect(self.click_point)

        quitb = QtGui.QPushButton("Done")
        quitb.clicked.connect(self.quit)

        lay = QtGui.QVBoxLayout()
        lay.addWidget(view)
        lay.addWidget(quitb)

        self.setLayout(lay)

        self.points = []

    def click_point(self, xy):
        self.points.append(xy)

    def quit(self):
        self.accept()


def test_picker():

    x, y = np.mgrid[0:100, 0:100]
    img = x * y

    app = QtGui.QApplication.instance()
    if app is None:
        app = QtGui.QApplication(['python'])

    picker = SimplePicker(img)
    picker.show()
    app.exec_()

    print(picker.points)

if __name__ == "__main__":
    test_picker()

I am using an Anaconda installation on Windows 7 64-bit, Qt 4.8.7, PyQt 4.10.4, numpy 1.9.2.

Spin answered 30/10, 2015 at 21:21 Comment(8)
You need an object that supports the buffer protocol. For python2, this a buffer object, but for python3 it's a memoryview. Using bdata.data will work in your example for both python2 and python3, but, frustratingly, bdata.T.data does not work with python3. Which is totally baffling, given that the error now is unexpected type 'memoryview'?! It seems that there is some subtle difference between the two memoryviews that PyQt is not able to handle.Leroi
Thanks for the info! I tried bdata.data, and it doesn't give any errors, but it doesn't run the GUI either! An empty list gets printed from test_picker, but no GUI appears. Is there something else wrong with the rest of the example?Spin
The GUI works fine for me with just QtGui.QImage(bdata.data, ..., but it does dump core on exit. That can easily be fixed by giving the scene a parent, though: QGraphicsScene(0, 0, wid, hgt, self). Other than that, I can't think why it doesn't work for you. What platform are you on, and what versions of Qt, PyQt and numpy are you using?Leroi
@Leroi Sorry for the long delay, I ended up out of town at a meeting for a few days. I'm on Windows 7 64 bit, Anaconda installation, with Qt 4.8.7, PyQt 4.10.4, numpy 1.6.2 (added to question)Spin
I also just noticed that if I move the contents of test_picker directly under the if __name__ ==..., then the GUI does show up (using bdata.data), but it also prints the empty list before I exit the GUI (no points are saved). Python 2 still works like I want it to. ARGH.Spin
I think you need to properly debug your example script before you try to isolate the numpy issue. I suggest you start by commenting out all the numpy/image related stuff and just aim to get a simple graphics view that records clicks. Then make sure it runs cleanly on both Python 2 and Python 3 (e.g. no spewing error messages or dumping core on exit). Once you have that, you can start adding back the numpy/image related stuff and try to pin-point where the real issue is.Leroi
Agreed; I've been doing just that, and the empty list/GUI issue is actually due to the QDialog not blocking in Python 3 when run from ipython qtconsole (vanilla ipython is fine --> is a different thing altogether). As for the QImage, pretty much if I give it a Numpy view, it dies (both 2 and 3), and if I give it an array (using .copy()) it's fine (both 2 and 3). Turns out that .T is an exception that somehow just happens to work with Python 2, for some unknown reason.Spin
I'm putting together an answer that documents this with one solution: use .copy() ...Spin
S
3

In the PyQt constructor above, the following behavior is observed from a Numpy array called bdata:

  • bdata works correctly for both Python 2 and Python 3
  • bdata.T works for 2, not for 3 (constructor error)
  • bdata.T.copy() works for both
  • bdata[::-1,:] does not work for either 2 or 3 (the same error)
  • bdata[::-1,:].copy() works for both
  • bdata[::-1,:].base works for both, but loses the result of the reverse operation

As mentioned by @ekhumoro in the comments, you need something which supports the Python buffer protocol. The actual Qt constructor of interest here is this QImage constructor, or the const version of it:

QImage(uchar * data, int width, int height, Format format)

From the PyQt 4.10.4 documentation kept here, what PyQt expects for an unsigned char * is different in Python 2 and 3:

Python 2:

If Qt expects a char *, signed char * or an unsigned char * (or a const version) then PyQt4 will accept a unicode or QString that contains only ASCII characters, a str, a QByteArray, or a Python object that implements the buffer protocol.

Python 3:

If Qt expects a signed char * or an unsigned char * (or a const version) then PyQt4 will accept a bytes.

A Numpy array satisfies both of these, but apparently a Numpy view doesn't satisfy either. It's actually baffling that bdata.T works at all in Python 2, as it purportedly returns a view:

>>> a = np.ones((2,3))
>>> b = a.T
>>> b.base is a
True

The final answer: If you need to do transformations that result in a view, you can avoid errors by doing a copy() of the result to a new array for passing into the constructor. This may not be the best answer, but it will work.

Spin answered 5/11, 2015 at 17:59 Comment(2)
What's baffling to me is that type(bdata) is type(bdata.T) returns true, and yet they are not really the same thing. That is why I got the contradictory error message unexpected type 'memoryview' when trying to use bdata.T.data. It's clearly not the actual type of the object that is unexpected - some other property of an ndarray view vs a copy must undermine pyqt's automatic conversion process.Leroi
Agreed. I have no idea why the internals are so different from pyqt's perspective ... perhaps only a pyqt dev could shed light on this particular quirk.Spin

© 2022 - 2024 — McMap. All rights reserved.