pythonnumpytkintertkinter-canvas

Image is mangled when displaying numpy array image in tkinter


I'm trying to display an image, procedurally generated by my code, into a canvas object. I don't want to use the canvas functions to generate these images because there's no good way (the screenshot and ghostcript methods are no good) to get pixel data out of the canvas.

I looked around for ways to generate the image pixels and settled on scikit-image, and then you can load the array as data in an image object in tkinter.

On the left is the pixel data displayed with matplotlib, on the right is the tkinter canvas with the same data displayed as a PhotoImage.

It's clear something is wrong.

enter image description here

This is my code:

from skimage.draw import polygon
from PIL import Image, ImageTk

import matplotlib.pyplot as plt

import tkinter as tk
import numpy as np

def generate():
    
    # clear canvas
    canvas.delete("all")

    # make clean array
    img = np.ones((width, height, 3), dtype=np.double) * 0.75

    # randomly generate positions
    x_generated = np.random.uniform(0, width, 5)
    y_generated = np.random.uniform(0, height, 5)
    
    for x, y in zip(x_generated, y_generated):
        base_height = 70
        base_width = 70

        # generate the corners
        x0 = x - base_width/2
        x1 = x + base_width/2
        y0 = y - base_height/2
        y1 = y + base_height/2

        # create rectangle
        poly = np.array((
            (x0, y0),
            (x1, y0),
            (x1, y1),
            (x0, y1),
        ))

        rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape)
        img[rr, cc, :] = 0.5

        # # canvas method. I don't want to use this!
        # canvas.create_rectangle(x0, y0, x1, y1, fill="red", outline="")
    
    # display generated image in tkinter canvas
    scaled_to_8bit = img * 255
    imgarray = ImageTk.PhotoImage(image=Image.fromarray(scaled_to_8bit, 'RGB'))

    # create the canvas image object from the numpy array
    canvas.create_image(20, 20, anchor="nw", image=imgarray)
    
    # display pixel data for debugging
    plt.imshow(img)
    plt.show()



if __name__ == "__main__":
    width = 1000
    height = 1000

    # Create a root window
    root = tk.Tk()
    root.configure(background="green")

    # Create a canvas widget
    canvas = tk.Canvas(root, width=width, height=height)
    canvas.pack()

    # Create a button widget
    button = tk.Button(root, text="Generate", command=generate)
    button.pack()

    # start main tk loop
    root.mainloop()

Solution

  • The datatype of the blank image array was a double, but the image object doesn't support that format. I converted it to an 8 bit datatype just before displaying the image and this fixed the issue.

    Note: there's still an issue where the image disappears when the matplotlib window is closed, but its a separate issue

    from skimage.draw import polygon
    from PIL import Image, ImageTk
    
    import matplotlib.pyplot as plt
    
    import tkinter as tk
    import numpy as np
    
    def generate():
        
        # clear canvas
        canvas.delete("all")
    
        # make clean array
        img = np.ones((width, height, 3), dtype=np.float32) * 0.75
    
        # randomly generate positions
        x_generated = np.random.uniform(0, width, 5)
        y_generated = np.random.uniform(0, height, 5)
        
        for x, y in zip(x_generated, y_generated):
            base_height = 70
            base_width = 70
    
            # generate the corners
            x0 = x - base_width/2
            x1 = x + base_width/2
            y0 = y - base_height/2
            y1 = y + base_height/2
    
            # create rectangle
            poly = np.array((
                (x0, y0),
                (x1, y0),
                (x1, y1),
                (x0, y1),
            ))
    
            rr, cc = polygon(poly[:, 0], poly[:, 1], img.shape)
            img[rr, cc, :] = 0.5
    
            # # canvas method. I don't want to use this!
            # canvas.create_rectangle(x0, y0, x1, y1, fill="red", outline="")
        
        # display generated image in tkinter canvas
        scaled_to_8bit = img * 255
        newarr = scaled_to_8bit.astype(np.uint8)
        imgarray = ImageTk.PhotoImage(image=Image.fromarray(newarr, 'RGB'))
    
        # create the canvas image object from the numpy array
        canvas.create_image(20, 20, anchor="nw", image=imgarray)
        
        # display pixel data for debugging
        plt.imshow(img)
        plt.show()
    
    
    
    if __name__ == "__main__":
        width = 1000
        height = 1000
    
        # Create a root window
        root = tk.Tk()
        root.configure(background="green")
    
        # Create a canvas widget
        canvas = tk.Canvas(root, width=width, height=height)
        canvas.pack()
    
        # Create a button widget
        button = tk.Button(root, text="Generate", command=generate)
        button.pack()
    
        # start main tk loop
        root.mainloop()