Reputation: 1055
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_()
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
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
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