hirschme
hirschme

Reputation: 894

How to display an image from a numpy array in tkinter?

The following short code is meant to create an array with numpy, convert it into an image object with PIL and then insert into a canvas on a tkinter window.

from tkinter import *
from PIL import Image

root = Tk()
array = np.ones((40,40))*150
img = Image.fromarray(array)
canvas = Canvas(root,width=300,height=300)
canvas.pack()
canvas.create_image(20,20,anchor=NW,image=img)

root.mainloop()

This throws the error:

TclError: image "<PIL.Image.Image image mode=F size=40x40 at 0x7F42D3BC3290>" doesn't exist

Upvotes: 16

Views: 24998

Answers (2)

Adrian W
Adrian W

Reputation: 5026

tkinter supports only a few image formats directly, but one of them, PPM can be created easily from numpy data. So, here is a solution which converts an array directly to a tk.PhotoImage - no need to take the detour (and overhead!) of ImageTk:

import tkinter as tk

import numpy as np


def _photo_image(image: np.ndarray):
    height, width = image.shape
    data = f'P5 {width} {height} 255 '.encode() + image.astype(np.uint8).tobytes()
    return tk.PhotoImage(width=width, height=height, data=data, format='PPM')


root = tk.Tk()

array = np.ones((40, 40)) * 150
img = _photo_image(array)

canvas = tk.Canvas(root, width=300, height=300)
canvas.pack()
canvas.create_image(20, 20, anchor="nw", image=img)

root.mainloop()

The magic is in the function _photo_image which creates a portable pixmap header and appends the picture data, which must be an array of bytes.

Notes:

  1. the above creates a portable graymap (PGM). With a slight modification, this also works with color images. These have one more dimension. So, use

    height, width = image.shape[:2]
    

    to extract the geometry and P6 for the magic value to pass in the header.

    For example, to convert an openCV image (which is usually encoded as BGR), use:

    import cv2
    
    def _photo_image(image: np.ndarray):
        height, width = image.shape[:2]
        ppm_header = f'P6 {width} {height} 255 '.encode()
        data = ppm_header + cv2.cvtColor(image, cv2.COLOR_BGR2RGB).tobytes()
        return tk.PhotoImage(width=width, height=height, data=data, format='PPM')
    
  2. the above link to the English Wikipedia page on netpbm does not fully explain the header format (you can find it in the examples, though). The German Wikipedia page on portable anymap has more details on the header: Magic Value, space, width, space, height, space, max-pixel-value, space

Upvotes: 4

Mike - SMT
Mike - SMT

Reputation: 15226

You need to use PhotoImage from ImageTk.

Do this instead:

import tkinter as tk
import numpy as np
from PIL import Image, ImageTk

root = tk.Tk()

array = np.ones((40,40))*150
img =  ImageTk.PhotoImage(image=Image.fromarray(array))

canvas = tk.Canvas(root,width=300,height=300)
canvas.pack()
canvas.create_image(20,20, anchor="nw", image=img)

root.mainloop()

Upvotes: 16

Related Questions