0

I am working on a Python program for displaying photos on the Raspberry Pi (Model B Revision 2.0 with 512MB RAM). It uses Tk for displaying the images. The program is mostly finished, but I ran into an issue where the program is terminated by the kernel because of low memory. This seems to happen randomly.

I do not understand why this happens. I have noticed that during image switching, the CPU spikes up significantly (up to 90%). I therefore thought that it might be an issue with the CPU not keeping up between two images and then falling behind and running out of memory. To test this I increased the timeout between showing images to 1 minute, but that did not help.

My question is, whether I am doing something wrong/inefficiently in the code (see below)? If not: I am considering switching to PyQt, because it seems to accelerate graphics with OpenGL (from what I read). Is this true and/or do you think that this might help with the issue I am facing?

This is my current Python code:

# From: https://stackoverflow.com/questions/19838972/how-to-update-an-image-on-a-canvas
import os
from pathlib import Path
from tkinter import *
from PIL import Image, ExifTags, ImageTk
import ipdb

class MainWindow():
    def __init__(self, main):
        self.my_images = []
        self._imageDirectory = str(Path.home().joinpath("./Pictures/rpictureframe"))
        self.main = main
        w, h = main.winfo_screenwidth(), root.winfo_screenheight()
        self.w, self.h = w, h
        main.attributes("-fullscreen", True) # REF: https://stackoverflow.com/questions/45136287/python-tkinter-toggle-quit-fullscreen-image-with-double-mouse-click
        main.focus_set()

        self.canvas = Canvas(main, width=w, height=h)
        self.canvas.configure(background="black", highlightthickness=0)
        self.canvas.pack()
        self.firstCall = True

        # set first image on canvas
        self.image_on_canvas = self.canvas.create_image(w/2, h/2, image = self.getNextImage()) ### replacing getNextImage instead of getNextImageV1 here fails

    @property
    def imageDirectory(self):
        return self._imageDirectory

    @imageDirectory.setter
    def setImageDirectory(self,imageDirectory):
        self._imageDirectory = imageDirectory

    def getNextImage(self):
        if self.my_images == []:
            self.my_images = os.listdir(self.imageDirectory)
        currentImagePath = self.imageDirectory + "/" + self.my_images.pop()
        self.currentImage = self.readImage(currentImagePath, self.w, self.h)
        return self.currentImage

    def readImage(self,imagePath,w,h):
        pilImage = Image.open(imagePath)
        pilImage = self.rotateImage(pilImage)
        pilImage = self.resizeImage(pilImage,w,h)
        return ImageTk.PhotoImage(pilImage)

    def rotateImage(self,image):
        # REF: https://stackoverflow.com/a/26928142/653770
        try:
            for orientation in ExifTags.TAGS.keys():
                if ExifTags.TAGS[orientation]=='Orientation':
                    break
            exif=dict(image._getexif().items())
            if exif[orientation] == 3:
                image=image.rotate(180, expand=True)
            elif exif[orientation] == 6:
                image=image.rotate(270, expand=True)
            elif exif[orientation] == 8:
                image=image.rotate(90, expand=True)
        except (AttributeError, KeyError, IndexError):
            # cases: image don't have getexif
            pass
        return image

    def resizeImage(self,pilImage,w,h):
        imgWidth, imgHeight = pilImage.size
        if imgWidth > w or imgHeight > h:
            ratio = min(w/imgWidth, h/imgHeight)
            imgWidth = int(imgWidth*ratio)
            imgHeight = int(imgHeight*ratio)
            pilImage = pilImage.resize((imgWidth,imgHeight), Image.ANTIALIAS)
        return pilImage

    def update_image(self):
        # REF: https://stackoverflow.com/questions/7573031/when-i-use-update-with-tkinter-my-label-writes-another-line-instead-of-rewriti/7582458#
        self.canvas.itemconfig(self.image_on_canvas, image = self.getNextImage()) ### replacing getNextImage instead of getNextImageV1 here fails
        self.main.after(5000, self.update_image)


root = Tk()
app = MainWindow(root)
app.update_image()
root.mainloop()

UPDATE:

Below you will find the current code that still produces the out-of-memory issue.

You can find the dmesg out-of-memory error here: https://pastebin.com/feTFLSxq

Furthermore this is the periodic (every second) output from top: https://pastebin.com/PX99VqX0 I have plotted the columns 6 and 7 (memory usage) of the top output:

Plot top memory usage.

As you can see, there does not appear to be a continues increase in memory usage as I would expect from a memory leak.

This is my current code:

# From: https://stackoverflow.com/questions/19838972/how-to-update-an-image-on-a-canvas
import glob
from pathlib import Path
from tkinter import *

from PIL import Image, ExifTags, ImageTk


class MainWindow():
    def __init__(self, main):
        self.my_images = []
        self._imageDirectory = str(Path.home().joinpath("Pictures/rpictureframe"))
        self.main = main
        w, h = main.winfo_screenwidth(), root.winfo_screenheight()
        self.w, self.h = w, h
        # main.attributes("-fullscreen", True) # REF: https://stackoverflow.com/questions/45136287/python-tkinter-toggle-quit-fullscreen-image-with-double-mouse-click
        main.focus_set()

        self.canvas = Canvas(main, width=w, height=h)
        self.canvas.configure(background="black", highlightthickness=0)
        self.canvas.pack()

        # set first image on canvas
        self.image_on_canvas = self.canvas.create_image(w / 2, h / 2,
                                                        image=self.getNextImage())  ### replacing getNextImage instead of getNextImageV1 here fails

    @property
    def imageDirectory(self):
        return self._imageDirectory

    @imageDirectory.setter
    def setImageDirectory(self, imageDirectory):
        self._imageDirectory = imageDirectory

    def getNextImage(self):
        if self.my_images == []:
            # self.my_images = os.listdir(self.imageDirectory)
            self.my_images = glob.glob(f"{self.imageDirectory}/*.jpg")
        currentImagePath = self.my_images.pop()
        self.currentImage = self.readImage(currentImagePath, self.w, self.h)
        return self.currentImage

    def readImage(self, imagePath, w, h):
        with Image.open(imagePath) as pilImage:
            pilImage = self.rotateImage(pilImage)
            pilImage = self.resizeImage(pilImage, w, h)
            return ImageTk.PhotoImage(pilImage)

    def rotateImage(self, image):
        # REF: https://stackoverflow.com/a/26928142/653770
        try:
            for orientation in ExifTags.TAGS.keys():
                if ExifTags.TAGS[orientation] == 'Orientation':
                    break
            exif = dict(image._getexif().items())
            if exif[orientation] == 3:
                image = image.rotate(180, expand=True)
            elif exif[orientation] == 6:
                image = image.rotate(270, expand=True)
            elif exif[orientation] == 8:
                image = image.rotate(90, expand=True)
        except (AttributeError, KeyError, IndexError):
            # cases: image don't have getexif
            pass
        return image

    def resizeImage(self, pilImage, w, h):
        imgWidth, imgHeight = pilImage.size
        if imgWidth > w or imgHeight > h:
            ratio = min(w / imgWidth, h / imgHeight)
            imgWidth = int(imgWidth * ratio)
            imgHeight = int(imgHeight * ratio)
            pilImage = pilImage.resize((imgWidth, imgHeight), Image.ANTIALIAS)
        return pilImage

    def update_image(self):
        # REF: https://stackoverflow.com/questions/7573031/when-i-use-update-with-tkinter-my-label-writes-another-line-instead-of-rewriti/7582458#
        self.canvas.itemconfig(self.image_on_canvas,
                               image=self.getNextImage())  ### replacing getNextImage instead of getNextImageV1 here fails
        self.main.after(30000, self.update_image)


root = Tk()
app = MainWindow(root)
app.update_image()
root.mainloop()
packoman
  • 1,230
  • 1
  • 16
  • 36

2 Answers2

2

I believe there is a memory leak when you open the image files with PIL and don't close them.

To avoid it, you must call Image.close(), or better yet consider using the with syntax.

 def readImage(self,imagePath,w,h):
    with Image.open(imagePath) as pilImage:    
        pilImage = self.rotateImage(pilImage)
        pilImage = self.resizeImage(pilImage,w,h)
        return ImageTk.PhotoImage(pilImage)
jpnadas
  • 774
  • 6
  • 17
  • Sorry for not coming back to this or accepting. The reason is that while your answer seems correct, it unfortunately did not solve the issue. Not sure how to proceed ATM for debugging .... – packoman Sep 01 '19 at 18:50
  • Are you sure you have implemented my suggestion correctly? This should fix the memory leak, unless you have another memory leak somewhere in your code. Did you post the entire code here? – jpnadas Sep 01 '19 at 21:25
  • I have update my question with my current code. Also added output of memory usage from top (with plot) and the out-of-memory usage from dmesg. – packoman Sep 03 '19 at 19:58
0

I run the code on my machine and I noticed similiar spikes. After some memory adjustments on a virtual machine I had a system without swap (turned of to get the crash "faster") and approximately 250Mb free memory.

While base memory usage was somewhere around 120Mb, the image change was between 190Mb and 200Mb (using images with a file size of 6,6Mb and 5184x3456 pixel) similiar to your plot. Then I copied a bigger (panorama) image (8,1Mb with 20707x2406 pixel) to the folder - and voila the machine got stuck.

I could see that the memory usage of the process reached 315Mb and the system became unusable (after 1 minute I "pulled the plug" on the VM).

So I think your problem has nothing todo with the actual code, but with the pictures you are trying to load (and the limited amount of RAM/Swap from your system). Maybe skipping the rotate and resize functions might mitigate your problem...

  • For a first try: just add more swap space (see https://wpitchoune.net/tricks/raspberry_pi3_increase_swap_size.html). – Seehcpu Sep 13 '19 at 21:17