Search code examples
python-3.xtkintertkinter-canvas

Increase speed of background made of tiles tkinter


I am trying to make a tiling background and have 50 000 small images for the background. I tried doing it by creating a lot of images and I read here that having many canvas items can be very slow. The images are only 16 by 16 and I wonder how to make it faster (it takes around a quarter second to scroll to the side currently).

Should I use one large bitmap image that I edit somehow or is there a better way to do this. I need to be able to change the tiles but only one or two at a time.

I tried only loading the ones on screen (~10 000) but it was still very slow and I had to recreate them when I went back using up even more ids.

I expect to be able to scroll the canvas without it being incredibly slow.


Solution

  • I thought @BryanOakley 's advice was quite good, and I made some example code that does just that, and I think it works quite well, I was able to get about 1.05 ms update times, and that's with the .after call with 1 ms.

    import tkinter as tk
    import time
    from PIL import Image, ImageTk
    import numpy as np
    
    NUM_IMAGES = 30*300  # approx 10,000
    IMAGE_FNAMES = ['a.png', 'b.png', 'c.png', 'd.png']
    IMAGE_SIZE = 16
    CANVAS_SIZE = 480
    
    def _gt(s=0.0):
        return time.perf_counter() - s
    
    class JonathanGassendWindow():
        def __init__(self, master):
            self._master = master
            self._canvas = tk.Canvas(self._master, width=CANVAS_SIZE, height=CANVAS_SIZE, bg='#FF00FF')
            self._canvas.grid(row=0, column=0, sticky=tk.N + tk.E + tk.W + tk.S)
            self._images = []
            # https://stackoverflow.com/questions/12760389/
            self._bg_image = Image.new('RGB', ((NUM_IMAGES // (CANVAS_SIZE // IMAGE_SIZE)) * IMAGE_SIZE * 2, CANVAS_SIZE))
            print(self._bg_image)
    
            # Load image files (I only made 4, but the concept remains)
            for i in range(NUM_IMAGES):
                with Image.open(IMAGE_FNAMES[i % len(IMAGE_FNAMES)]) as img:
                    img.load()
                self._images.append(img)
            # Build the bg image
            rng = np.random.default_rng(seed=42)
            for i in range(self._bg_image.height // IMAGE_SIZE):
                for j in range(self._bg_image.width // IMAGE_SIZE):
                    self._bg_image.paste(
                        self._images[rng.integers(0, len(self._images), size=(1,))[0]],
                        (j * IMAGE_SIZE, i * IMAGE_SIZE)
                    )
            self._bg_photoimage = ImageTk.PhotoImage(self._bg_image)
            self._bg_handle = self._canvas.create_image(0, 0, image=self._bg_photoimage, anchor=tk.N + tk.W)
            self._prev_loop = _gt()
            self._master.after(5000, self._updateloop)
    
        def _updateloop(self):
            self._canvas.move(self._bg_handle, -1, 0)
            s = _gt()
            dur_total = s - self._prev_loop
            self._prev_loop = s
            print(f'\rUpdate loop dur: {dur_total * 1000:6.3f} ms', end='')
            self._master.after(1, self._updateloop)
    
    
    def _main():
        root = tk.Tk()
        jgw = JonathanGassendWindow(root)
        root.mainloop()
    
    
    if __name__ == '__main__':
        _main()
    

    That should be enough to get you started, let me know if you have any questions.