PiRK
PiRK

Reputation: 1055

Issue when drawing a numpy array into a QWidget

I'm trying to code a preview widget that is able to display a 2D numpy array image. This widget has a fixed size (square), but the image can have any shape.

It seems to work for some image shapes, but for other shapes it displays non-sense, and for some other shapes it crashes without any error message.

Do you see an obvious mistake in my code?

from silx.gui import qt
import numpy


GRAY_COLORTABLE = []
for i in range(256):
    GRAY_COLORTABLE.append(qt.qRgb(i, i, i))


class PreviewImageWidget(qt.QWidget):
    """Preview image"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.pixmap = qt.QPixmap()
        self.setFixedSize(350, 350)

    def paintEvent(self, event):
        painter = qt.QPainter(self)
        painter.drawPixmap(self.rect(), self.pixmap)

    def setImage(self, img_array):
        # TODO : adjust colortable to actual dtype (autoscale to min - max ??)
        if img_array is None:
            self.pixmap = qt.QPixmap()
        else:
            if img_array.dtype != numpy.uint8:
                max_value = img_array.max()
                img_array = 256. / max_value * img_array
                img_array = img_array.astype(numpy.uint8)

            # binary images are of dtype uint8
            if img_array.max() == 1:
                img_array = img_array * 255
            image = qt.QImage(img_array,
                              img_array.shape[1], img_array.shape[0],
                              qt.QImage.Format_Indexed8)
            image.setColorTable(GRAY_COLORTABLE)
            self.pixmap = qt.QPixmap.fromImage(image)

        self.update()

if __name__ == '__main__':

    app = qt.QApplication([])
    allPreviewWidgets = []

    for sh in [(610, 500), (450, 700), (550, 600),
               (500, 500), (510, 500), (500, 520)]:
        img_array = numpy.zeros(sh, dtype=numpy.uint8)
        img_array[200:350, 250:300] = 1

        previewWidget = PreviewImageWidget()
        previewWidget.setWindowTitle(str(img_array.shape))
        previewWidget.show()
        previewWidget.setImage(img_array)
        allPreviewWidgets.append(previewWidget)

    app.exec_()

enter image description here The shapes that are almost square don't work. The rectangle ones work fine. In the documentation of QPainter, it says:

Note: The image is scaled to fit the rectangle, if both the image and rectangle size disagree.

An example of shape that makes the program crash: (2000, 500)

Edit: here is another example showing the same problem without a QPainter and without resizing the pixmap. I think this narrows it down to an issue with how QImage is decoding the numpy array.

from silx.gui import qt
import numpy

GRAY_COLORTABLE = []
for i in range(256):
    GRAY_COLORTABLE.append(qt.qRgb(i, i, i))


def array2qpixmap(img_array):
    if img_array.max() == 1:
        img_array = img_array * 255
    image = qt.QImage(img_array.astype(numpy.uint8),
                      img_array.shape[1], img_array.shape[0],
                      qt.QImage.Format_Indexed8)
    image.setColorTable(GRAY_COLORTABLE)
    return qt.QPixmap.fromImage(image)


if __name__ == '__main__':

    app = qt.QApplication([])
    labels = []

    for sh in [(610, 500), (450, 700), (550, 600),
               (500, 500), (510, 500), (200, 520)]:
        img_array = numpy.zeros(sh, dtype=numpy.uint8)
        img_array[200:350, 250:300] = 1

        lab = qt.QLabel()
        lab.setFixedSize(700, 700)
        lab.setWindowTitle(str(sh))
        lab.show()
        lab.setPixmap(array2qpixmap(img_array))
        labels.append(lab)

    app.exec_()

Upvotes: 1

Views: 788

Answers (2)

PiRK
PiRK

Reputation: 1055

In case anyone encounters the same issue with my first example, this is what I had to do to make it work.

import numpy
from silx.gui import qt

GRAY_COLORTABLE = [qt.qRgb(i, i, i) for i in range(256)]


class PreviewImageWidget(qt.QLabel):
    """Image preview widget. Displays the image in
    a 2D numpy array with a grayscale colortable.
    """
    def __init__(self, parent=None):
        super().__init__(parent)

        self.size = qt.QSize(150, 150)
        self.setSize(self.size)
        self.pixmap = qt.QPixmap()

    def setSize(self, size):
        self.size = size
        self.setFixedSize(self.size)

    def setImage(self, img_array):
        if img_array is None:
            # null pixmap
            self.pixmap = qt.QPixmap()
        else:
            img_array = img_array.copy()
            bytesPerLine = img_array.strides[0]
            if img_array.dtype != numpy.uint8:
                max_value = img_array.max()
                img_array = 256. / max_value * img_array
                img_array = img_array.astype(numpy.uint8)

            height, width = img_array.shape
            image = qt.QImage(img_array,
                              width, height,
                              bytesPerLine,
                              qt.QImage.Format_Indexed8)
            image.setColorTable(GRAY_COLORTABLE)

            pixmap = qt.QPixmap.fromImage(image)
            self.pixmap = pixmap.scaled(self.size,
                                        qt.Qt.KeepAspectRatio)
        self.setPixmap(self.pixmap)


if __name__ == '__main__':

    app = qt.QApplication([])
    allPreviewWidgets = []

    for sh in [(610, 500), (450, 700), (550, 600),
               (500, 500), (510, 500), (500, 520)]:
        img_array = numpy.zeros(sh, dtype=numpy.uint8)
        img_array[200:350, 250:300] = 255

        previewWidget = PreviewImageWidget()
        previewWidget.setSize(qt.QSize(300, 300))
        previewWidget.setWindowTitle(str(img_array.shape))
        previewWidget.show()
        previewWidget.setImage(img_array)
        allPreviewWidgets.append(previewWidget)

    app.exec_()

In that case, copying the array to ensure it is contiguous in memory seems to have helped and copying the QImage seems to be unnecessary.

But in my real application, this fails with PyQt5.13 for an unknown reason, even before setting a real image. I'm hoping these are all bugs related to the current version of Qt, and that it will be fixed in next version.

Upvotes: 0

eyllanesc
eyllanesc

Reputation: 243907

I have only been able to reproduce the problem in the second case and I have found that the problem is the memory that since you are using the same object in all the transformations, in some cases the memory is being eliminated, the solution is to copy the data:

from PySide2 import QtCore, QtGui, QtWidgets
import numpy

GRAY_COLORTABLE = []
for i in range(256):
    GRAY_COLORTABLE.append(QtGui.qRgb(i, i, i))


def array2qpixmap(img_array):
    height, width = img_array.shape
    bytesPerLine, _ = img_array.strides
    image = QtGui.QImage(
        img_array.data.tobytes(),
        width,
        height,
        bytesPerLine,
        QtGui.QImage.Format_Indexed8,
    )
    image.setColorTable(GRAY_COLORTABLE)
    return QtGui.QPixmap.fromImage(image.copy())


if __name__ == "__main__":

    app = QtWidgets.QApplication([])
    labels = []

    for sh in [
        (610, 500),
        (450, 700),
        (550, 600),
        (500, 500),
        (510, 500),
        (200, 520),
    ]:
        img_array = numpy.zeros(sh, dtype=numpy.uint8)
        img_array[200:350, 250:300] = 255
        lab = QtWidgets.QLabel()
        lab.resize(700, 700)
        lab.setWindowTitle(str(sh))
        lab.show()
        lab.setPixmap(array2qpixmap(img_array.copy()))
        labels.append(lab)

    app.exec_()

Upvotes: 1

Related Questions