Display numpy array cv2 image in wxpython correctly - python

I am trying to convert a numpy array (cv2 image) to a wxpython Bitmap and display it properly. I have looked into various solutions on SO and elsewhere, but without success. You can see two of my attempts in the code below.
import wx
import cv2
import numpy as np
def create_wx_bitmap(cv2_image):
# type: (np.ndarray) -> wx.Bitmap
# My Attempt based on https://stackoverflow.com/questions/32995679/converting-wx-bitmap-to-numpy-using-bitmapbufferformat-rgba-python/32995940#32995940
height, width = cv2_image.shape[:2]
array = cv2_image # the OpenCV image
image = wx.Image(width, height)
image.SetData(array.tobytes())
wxBitmap = image.ConvertToBitmap()
return wxBitmap
# My other attempt:
# height, width = cv2_image.shape[:2]
# cv2_image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
# return wx.Bitmap.FromBuffer(width, height, cv2_image_rgb)
class MyFrame(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title)
cv2_image = cv2.imread("test1.png", cv2.IMREAD_ANYDEPTH | cv2.IMREAD_COLOR) # type: np.ndarray
print(cv2_image.dtype)
bitmap = create_wx_bitmap(cv2_image) # type: wx.Bitmap
wx.StaticBitmap(self, -1, bitmap, (0, 0), self.GetClientSize())
self.SetSize(bitmap.GetSize())
if __name__ == "__main__":
app = wx.App(False)
frame = MyFrame(None, "wxPython with OpenCV")
frame.Show()
app.MainLoop()
The code above seems to work for images under 16-bit (24 bit depth). However, an image that has a bit depth of 64 results in banding like the below screenshot. (which was a render from Blender 3D exported with 16 bit depth setting):
I have also tried converting the array datatype, but it didn't seem to make any difference.
Edit (The Final Solution):
The solution to my problem was to convert the array to np.uint8 after normalizing the data as mentioned in this SO answer. Thanks to #PetrBlahos for mentioning that the data needs to be 8bit RGB in his answer.
def create_wx_bitmap(cv2_image):
# type: (np.ndarray) -> wx.Bitmap
height, width = cv2_image.shape[:2]
info = np.iinfo(cv2_image.dtype) # Get the information of the incoming image type
data = cv2_image.astype(np.float64) / info.max # normalize the data to 0 - 1
data = 255 * data # Now scale by 255
cv2_image = data.astype(np.uint8)
cv2_image_rgb = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2RGB)
return wx.Bitmap.FromBuffer(width, height, cv2_image_rgb)

I use
dc.DrawBitmap(wx.Bitmap.FromBuffer(iw, ih, cv_image), 0, 0)
but cv_image must be a numpy byte array of rgb values. So, whatever you do, you must convert your data to 8bit RGB (or possibly RGBA, the use FromBufferRGBA).
I do not quite see how your data is structured. 64bits means you have 4 channels (RGBA) each one is 16b integer?
I think you might use cv2.convertScaleAbs, or perhaps convertTo.

Related

[Possible Pyqtgraph bug]PyQt5/Pyqtgraph Get Numpy Array for What is Currently on the Viewbox

In this post, let us consider a situation very similar to PyQt5/Pyqtgraph Get Numpy Array for What is Currently on the Scene. With the same image as in the linked post, the following code runs:
from PyQt5.QtWidgets import*
import pyqtgraph as pg
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from numpy import asarray
from PyQt5.QtGui import QImage
from pyqtgraph.exporters import ImageExporter
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.layout = QVBoxLayout()
self.button = QPushButton("Save")
self.button.clicked.connect(self.get_numpy_array)
self.layout.addWidget(self.button)
image = asarray(Image.open(image_directory))
self.graphics = Graphics(image)
self.layout.addWidget(self.graphics)
self.setLayout(self.layout)
def get_numpy_array(self):
# Export current viewvbox into bytes
exporter = ImageExporter(self.graphics.viewbox)
data = exporter.export(toBytes=True)
# Convert QIMage into RGB image
input_img = data.convertToFormat(QImage.Format_RGB888)
width = input_img.width()
height = input_img.height()
# Get pointer to data
ptr = input_img.bits()
ptr.setsize(input_img.byteCount())
# Create numpy array from data
arr = np.array(ptr).reshape(height, width, 3)
# This part transforms array back to image
img = Image.fromarray(arr, 'RGB')
img.save("./slice.png")
print(arr.shape)
return arr
class Graphics(pg.GraphicsLayoutWidget):
def __init__(self, image):
super().__init__()
layout = self.addLayout()
self.image = image
self.shape = image.shape
self.viewbox = layout.addViewBox(lockAspect=True)
self.left_image_item = pg.ImageItem(image)
self.right_image_item = pg.ImageItem(image)
self.viewbox.addItem(self.left_image_item)
self.viewbox.setLimits(minXRange=0,
minYRange=0,
maxXRange=self.shape[0],
maxYRange=self.shape[1])
self.another_viewbox = layout.addViewBox(lockAspect=True)
self.another_viewbox.addItem(self.right_image_item)
x, h = 300, 50
polyline = pg.PolyLineROI(
[[x, x], [x + h, x], [x + h, x + h], [x, x + h]], pen=pg.mkPen("b", width=5), closed=True, rotatable=False)
self.viewbox.addItem(polyline)
x, h = 100, 50
ellipse = pg.EllipseROI(
pos=[x, x], size=[x + h, x], pen=pg.mkPen("r", width=5), rotatable=False)
self.another_viewbox.addItem(ellipse)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()
As it can be seen, this is precisely the solution proposed by Domarm with only one modification: there is another viewbox. Running will give something like:
My goal remains the same as in the linked post: read what is currently on the scene for the (left) viewbox without caring what is going on the (right) another_viewbox. (Again, I added ROIs just to emphasize we want what is currently on the scene and they have nothing to do with the problem.) Notice that, in this case, the proposed solution no longer works.
Questions:
I thought ImageExporter created in this way should only do everything subject to the first viewbox. The second viewbox should not matter. What went wrong?
How to fix the situation ?
Updates:
By typing conda list in my anaconda prompt, I get to know that I am using python 3.8.13, pyqt 5.15.7, and pyqtgraph 0.12.4. If I use the same image then the error message is always the same no matter how the image or roi was moved:
However, if I add the following command in get_numpy_array:
self.label = QLabel()
pixmap = QPixmap(input_img)
self.label.setPixmap(pixmap)
self.label.show()
Then, despite the error described above, a window will show up and give the correct image. This shows input_img is indeed the QImage I want and the problem lies within the conversion from QImage to numpy.
It turns out that if I just write the following:
def get_numpy_array(self):
# Export current viewvbox into bytes
exporter = ImageExporter(self.graphics.viewbox)
data = exporter.export(toBytes=True)
# Convert QIMage into RGB image
input_img = data.convertToFormat(QImage.Format_RGB888)
input_img.save("./slice.png")
Then the image is saved regardless of the number of viewboxes I have. If I read the saved I will get a numpy array. So the problem is resolved. But I will still accept any answer explaining why the above code was not working or whether this is a bug.
The problem was with Qimage.Format888 (basically RGB array of shape RxGxB). Using different format RGBA888 (RxGxBxA) solves the issue. Reshaping using fourth dimension (A) np.array(ptr).reshape(height, width, 4) and converting image back img = Image.fromarray(arr, 'RGBA') works just fine.
This is only necessary if you need np.array. If you just want to save a picture, you can can save data already using data.save("./slice.png"). No need for input_img = data.convertToFormat(QImage.Format_RGB888), since data is already a QImage.
So the final code for get_numpy_array is:
def get_numpy_array(self):
# Export current viewvbox into bytes
exporter = ImageExporter(self.graphics.viewbox)
data = exporter.export(toBytes=True)
# Convert QIMage into RGB image
input_img = data.convertToFormat(QImage.Format_RGBA8888)
width = input_img.width()
height = input_img.height()
# Get pointer to data
ptr = input_img.bits()
ptr.setsize(input_img.byteCount())
# Create numpy array from data
arr = np.array(ptr).reshape(height, width, 4)
# This part transforms array back to image
img = Image.fromarray(arr, 'RGBA')
img.save("./slice.png")
return arr

What format does the wxpython Image object expect for data when instantiating it using the wx.Image(width, height, data) constructor

I am trying to generate an image by specifying each pixel. For this, I have written this little test to see how it works and apparently I am not using the right format for the data.
import numpy as np
import wx
class Test(wx.Frame):
def __init__(self, *args, **kwargs):
super(Test, self).__init__(*args, **kwargs)
self.initialize()
def initialize(self):
self.SetSize(500, 500)
self.SetTitle("Test")
panel = wx.Panel(self)
width = 500
height = 500
image_data = np.random.randint(0, 256, size=(width, height, 3))
print(image)
image = wx.Image(width = width, height = height, data = image_data)
bitmap = image.ConvertToBitmap()
wx.StaticBitmap(panel, bitmap = bitmap, size = (500, 500))
def main():
app = wx.App()
window = Test(None, style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER)
print(type(window))
window.Show()
app.MainLoop()
if __name__ == "__main__":
main()
This code opens a window displaying a striped colorful image with black, red, blue and green pixels. Instead, I would have expected every pixel to be a random colour (not just red, blue and green) and far fewer pixels that are pitch black. The documentation on the wxpython site and on the original wxwidgets site only says that "data" ought to be in "RGB format" which I thought I had supplied with the method I use. What am I doing wrong here?
Edit:
Example output of the code above
As one of the comments has already mentioned, the documentation of wxwidgets in its original C implementation asks for an unsigned char array. In essence, the Image object expects its data in a format where every pixel is given by three bytes, each specifying the value of one of the channels of the RGB image.
As such, either a bytes object or for example a numpy array with datatype ubyte will work for this. Using int will result in the int being reinterpreted as separate bytes, which will result in the striped image shown in the original post.

How to display RGB data as image in tkinter

Opening an image file to display in a frame or button in tkinter is simple, but I have images stored in arrays as raw RGB data (for transmission over SPI to a small LCD display). I wish to show this data as a proper image (exactly as processed for the small lcd display) in a frame or button, with tkinter.
I need some steering in how to approach this. Cheers.
You need to convert your array rgb data into an Image object that tkinter knows.
For this the best approach is to convert your rgb data to a PIL Image.
Depending on how your rgb values are ordered in the array you might need to do some conversions: if rgb data are interleaved (usually the case) or in separate channels/plans, the conversion might fail.
Check for here for converting an array to a pil image.
https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.fromarray
Once the conversion to Pil Image is done you can use it in tkinter.
from PIL import Image, ImageTk
import numpy as np
#convert array to PiL Image
PIL_image = Image.fromarray(np.uint8(your_rgb_array)).convert('RGB')
tk_image = ImageTk.PhotoImage(PIL_image )
label1 = tkinter.Label(image=tk_image )
For displaying the image you should have a look at tk.Canvas:
https://www.tutorialspoint.com/python/tk_canvas.htm
Thank you all for your input. I 've been chewing on it for a bit, and now have working code for the task. It takes the image as has been manipulated by numpy or opencv, transforms it to tk format and then shows the original and reduced bitdepth images side by side, with pixel doubling. Here is the snippet with the relevant code:
def nextimg(self):
f = self.image[self.ptr % self.length]
self.drawImage(f)
f = f[...,::-1]
f = np.repeat(f,2,axis=0)
f = np.repeat(f,2,axis=1)
temp=self.size
self.size=[480,480]
if self.bpp == 12: g = self.imageConvert_tk12(f)
elif self.bpp == 16: g = self.imageConvert_tk16(f)
else: g = self.imageConvert_tk18(f)
self.button_image1 = ImageTk.PhotoImage(Image.fromarray(np.uint8(f)).convert('RGB'))
self.button_image2 = ImageTk.PhotoImage(Image.fromarray(np.uint8(g)).convert('RGB'))
self.button_pic1.configure(image=self.button_image1)
self.button_pic2.configure(image=self.button_image2)
self.ptr+=1
self.size=temp
def imageConvert_tk18(self,image):
x,y = image.shape[0:2]
arr =np.ndarray((x,y,3))
arr[...,:] = (np.uint8(image[...,:] >> 2) * 4)
return arr
def imageConvert_tk16(self,image):
x,y = image.shape[0:2]
arr = np.ndarray((x,y,3))
arr[...,0:3:2] = (np.uint8(image[...,0:3:2] >> 3) * 8)
arr[...,1] = (np.uint8(image[...,1] >> 2) * 4)
return arr
def imageConvert_tk12(self,image):
x,y = image.shape[0:2]
arr =np.ndarray((x,y,3))
arr[...,:] = (np.uint8(image[...,:] >> 4) * 16)
return arr
I also understand a lot more now about lists and arrays. Opencv creates an np.ndarray , which I now understand is a 1-dimensional array (or list in list?) but with a given x-dimensional shape.
Of course I am hoping there might be another solution without having to resort to PIL as intermediary, but if not this will work fine.
Please feel free to suggest improvements to my code!

Display image without shape pyqt

I am trying to create a pyqt application which contains three windows.
Display video stream from camera as BGR format using QPixmap and opencv.
Display Masked image using QPixmap and opencv.
Display screen grab using PIL library, opencv and QPixmap.
The error that I face while displaying masked & screen grab frame is given below.
screen_grab_height, screen_grab_width, screen_grab_channel = image_grab_frame.shape ValueError: not enough values to unpack (expected 3, got 2)
When I checked the frame.shape I found that it has only two values, i.e. image_height and image_width. It does not have image_channel value.
I am attaching the code for both the functions below,
Masked image
mask = cv2.inRange(hsv, lower_hue, upper_hue)
mask1=cv2.bitwise_not(mask)
hsv_height, hsv_width, hsv_channel = hsv_image.shape
hsv_step = hsv_channel * hsv_width
mask_height, mask_width, mask_channel = mask_frame.shape
mask_step = mask_channel * mask_width
convertToQFormat = QImage(mask_frame.data, mask_frame.shape[1], mask_frame.shape[0], QImage.Format_RGB888)
pic = convertToQFormat.scaled(1280, 720, Qt.KeepAspectRatio)
self.normal_screen.setPixmap(QPixmap.fromImage(pic))
and
screen grab
screen_grab_height, screen_grab_width, screen_grab_channel = image_grab_frame.shape
screen_grab_step = screen_grab_channel * screen_grab_width
#Display image grab#
convertToQFormat = QImage(image_grab_frame.data,image_grab_frame.shape[1], image_grab_frame.shape[0], QImage.Format_RGB888)
image_grab_pic = convertToQFormat.scaled(1280, 720, Qt.KeepAspectRatio)
self.normal_screen.setPixmap(QPixmap.fromImage(image_grab_pic))
You have 2 options :-
loss="binary_crossentropy" is correct as you only have 1 class. Earlier due to it being unclear about the number of classes, I did a mistake there.
change the model and the rest is the same.:-
model.add(Dense(1))
model.add(Activation("sigmoid"))
DEFAULT_IMAGE_SIZE= (256, 256)
In this section
try:
image = cv2.imread(image_dir)
if image is not None:
image = cv2.resize(image, DEFAULT_IMAGE_SIZE)
return img_to_array(image)
else:
pass
If you had "None" images then you have to filter them out at first and you can just a simple pass.
5) I am not sure why you are changing the mode to channels_first but that would make the channels at 0 index and here it should be
model.add(BatchNormalization(axis=0))
In fact, just don't change the K.image_data_format() and then you won't even need to pass the axis in BatchNormalization.

from binary data to wxbitmap

I have a device which stores a grayscale image as a series of 8 bit unsigned integer values. I want to write a python program to read these images from a file and show them using wxBitmap. I have a code that works, but it seems inefficient due to a lot of conversions between formats.
Any suggestions for a faster code are highly appreciated.
My current code:
imagearray=numpy.fromfile(file=self.f, dtype=numpy.uint8, count=npixels).reshape(Height, Width)[::-1]
pilimage = Image.fromarray(imagearray)
rgb= pilimage.convert('RGB')
rgbdata = rgb.tostring()
WxBitmap = wx.EmptyBitmap(Width,Height)
WxBitmap.CopyFromBuffer(rgbdata)
output=WxBitmap
You can get a wxBitmap directly from a numpy array.
This is an example from wxPyWiki:
import wx, numpy
def GetBitmap( self, width=32, height=32, colour = (0,0,0) ):
array = numpy.zeros( (height, width, 3),'uint8')
array[:,:,] = colour
image = wx.EmptyImage(width,height)
image.SetData( array.tostring())
wxBitmap = image.ConvertToBitmap() # OR: wx.BitmapFromImage(image)
return wxBitmap

Categories