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.

Ajean
  • 5,528
  • 14
  • 46
  • 69
  • You need an object that supports the [buffer protocol](https://docs.python.org/3.5/c-api/buffer.html#index-0). 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. – ekhumoro Oct 30 '15 at 23:38
  • 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? – Ajean Oct 31 '15 at 00:19
  • 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? – ekhumoro Oct 31 '15 at 01:04
  • @ekhumoro 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) – Ajean Nov 05 '15 at 16:08
  • 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. – Ajean Nov 05 '15 at 16:15
  • 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. – ekhumoro Nov 05 '15 at 17:35
  • 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. – Ajean Nov 05 '15 at 17:44
  • I'm putting together an answer that documents this with one solution: use `.copy()` ... – Ajean Nov 05 '15 at 17:45

1 Answers1

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.

Ajean
  • 5,528
  • 14
  • 46
  • 69
  • 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. – ekhumoro Nov 05 '15 at 19:16
  • 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. – Ajean Nov 05 '15 at 21:20