How to automatically resize images to fit your digital picture frame

One of our Pi3D PictureFrame fans recently encountered an issue where very large images, such as panorama photos, caused their Raspberry Pi Zero running Pi3D PictureFrame to display a black screen.

Paddy suggested this is likely due to the Pi Zero’s limited memory.

A simple solution seems to be ensuring that all images are resized to fit within the display’s maximum width and height.

Tested on a Raspberry Pi 5 running OS Bookworm. The following instructions assume you have either used the one-click install script or followed the same setup steps manually. Works with jpg, png, tiff, and heic files.

So here is a small Python script that does exactly that:

  • Scans the Pictures folder and all subdirectories.
  • Resizes oversized images proportionally (no cropping) while preserving quality and retaining metadata.
  • Fixes potential color issues by ensuring RGB mode.
  • Detects newly added files and automatically checks if they need resizing.

Note: The image files are overwritten with the reduced size, so make sure that you keep your originals somewhere.

The Python script installation

Before running the script, install the necessary Python libraries with

pip install pillow watchdog pillow-heif

sudo apt-get install libheif1

Note: Before you do any “pip” stuff, don’t forget to always activate your Python virtual directory, in our case with source venv_picframe/bin/activate.

This ensures that your Pi has the tools to recognize and process new images as they are added.

Create a new script with

sudo nano resize_images.py

Copy and paste the following content into this script:

Depending on your display size, you may need to adjust the values in # Default configuration.

import os
import time
import threading
import queue
from pathlib import Path
from PIL import Image
import pillow_heif
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

# Register HEIF opener with PIL
pillow_heif.register_heif_opener()

# Default configuration
PICTURES_FOLDER = "/home/pi/Pictures"
MAX_WIDTH = 1080
MAX_HEIGHT = 768
FILE_CHECK_RETRIES = 10
FILE_CHECK_INTERVAL = 0.1  # 100ms between checks
MAX_QUEUE_RETRIES = 5  # Maximum number of times to requeue a file

# Thread-safe queue to store new file paths
file_queue = queue.Queue()

def wait_for_file_stability(file_path, max_retries=FILE_CHECK_RETRIES, check_interval=FILE_CHECK_INTERVAL):
    """Wait for file to be completely written by checking if its size remains stable"""
    last_size = -1
    for _ in range(max_retries):
        try:
            current_size = os.path.getsize(file_path)
            if current_size == last_size and current_size > 0:
                return True
            last_size = current_size
            time.sleep(check_interval)
        except (OSError, FileNotFoundError):
            time.sleep(check_interval)
            continue
    return False

def resize_image(image_path, attempt=1):
    """ Resize image while maintaining aspect ratio and quality """
    try:
        print(f"Checking if file is ready: {image_path} (attempt {attempt})")
        if not wait_for_file_stability(image_path):
            if attempt < MAX_QUEUE_RETRIES:
                print(f"File not ready yet, requeueing: {image_path}")
                file_queue.put((image_path, attempt + 1))
            else:
                print(f"Max retries reached, skipping file: {image_path}")
            return
        
        print(f"Processing image: {image_path}")
        with Image.open(image_path) as img:
            # Preserve EXIF and other metadata if available
            exif = img.info.get('exif', None)  # Explicitly handle None case
            icc_profile = img.info.get('icc_profile', None)  # Same for ICC profile
            
            # Convert to RGB if needed, preserving alpha channel if present
            if img.mode == 'RGBA':
                img = img
            elif img.mode != 'RGB':
                img = img.convert('RGB')

            # Check if resizing is needed
            width, height = img.size
            if width <= MAX_WIDTH and height <= MAX_HEIGHT:
                # If it's a HEIC file, we still need to convert it to JPEG
                if image_path.lower().endswith(('.heic', '.heif')):
                    new_path = os.path.splitext(image_path)[0] + '.jpg'
                    save_kwargs = {'quality': 100, 'optimize': False}
                    if exif is not None:  # Only include exif if it exists
                        save_kwargs['exif'] = exif
                    if icc_profile is not None:  # Only include ICC if it exists
                        save_kwargs['icc_profile'] = icc_profile
                    img.save(new_path, 'JPEG', **save_kwargs)
                    try:
                        os.remove(image_path)  # Remove original HEIC file
                        print(f"Converted HEIC to JPEG: {new_path}")
                    except Exception as e:
                        print(f"Error removing original HEIC file: {str(e)}")
                else:
                    print(f"No resize needed for {image_path} ({width}x{height})")
                return

            print(f"Current image size: {width}x{height}")

            # Calculate memory requirements and use progressive loading if needed
            memory_estimate = (width * height * 3) / (1024 * 1024)  # In MB
            if memory_estimate > 500:  # If image might use more than 500MB
                print(f"Large image detected ({memory_estimate:.2f}MB), using progressive loading")
                img.draft('RGB', (width//2, height//2))

            # Resize while maintaining aspect ratio
            img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.LANCZOS)
            new_width, new_height = img.size
            print(f"Resizing image from {width}x{height} to {new_width}x{new_height}")

            # Handle HEIC/HEIF files specially
            file_ext = image_path.lower().split('.')[-1]
            if file_ext in ['heic', 'heif']:
                new_path = os.path.splitext(image_path)[0] + '.jpg'
                save_kwargs = {'quality': 100, 'optimize': False}
                if exif is not None:  # Only include exif if it exists
                    save_kwargs['exif'] = exif
                if icc_profile is not None:  # Only include ICC if it exists
                    save_kwargs['icc_profile'] = icc_profile
                
                img.save(new_path, 'JPEG', **save_kwargs)
                try:
                    os.remove(image_path)  # Remove original HEIC file
                    print(f"Converted and resized HEIC to JPEG: {new_path}")
                except Exception as e:
                    print(f"Error removing original HEIC file: {str(e)}")
            else:
                # For other formats, use original settings
                save_kwargs = {
                    'jpg': {'format': 'JPEG', 'quality': 100, 'optimize': False},
                    'jpeg': {'format': 'JPEG', 'quality': 100, 'optimize': False},
                    'png': {'format': 'PNG', 'optimize': False},
                    'tiff': {'format': 'TIFF', 'compression': None}
                }

                kwargs = save_kwargs.get(file_ext, {})
                if exif is not None:  # Only include exif if it exists
                    kwargs['exif'] = exif
                if icc_profile is not None:  # Only include ICC if it exists
                    kwargs['icc_profile'] = icc_profile
                
                img.save(image_path, **kwargs)
                print(f"Successfully saved resized image: {image_path}")

    except Exception as e:
        print(f"Error processing {image_path}: {str(e)}")
        if attempt < MAX_QUEUE_RETRIES:
            print(f"Requeueing due to error: {image_path}")
            file_queue.put((image_path, attempt + 1))

def is_image_file(filename):
    """Check if a file is an image based on its extension"""
    return filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif', '.heic', '.heif'))

def scan_existing_files():
    """Scan for existing images in the PICTURES_FOLDER and process them"""
    print(f"\nScanning existing files in {PICTURES_FOLDER}")
    count = 0
    for root, _, files in os.walk(PICTURES_FOLDER):
        for filename in files:
            if is_image_file(filename):
                file_path = os.path.join(root, filename)
                print(f"Found existing image: {file_path}")
                file_queue.put((file_path, 1))  # Start with attempt 1
                count += 1
    print(f"Initial scan completed. Found {count} images to process.\n")

def process_new_files():
    """ Processes new files from the queue """
    while True:
        try:
            item = file_queue.get()  # Get file from queue
            if isinstance(item, tuple):
                image_path, attempt = item
            else:
                image_path, attempt = item, 1  # Handle old-style queue items
            resize_image(image_path, attempt)
            file_queue.task_done()
        except Exception as e:
            print(f"Error in process_new_files: {str(e)}")

class ImageWatcher(FileSystemEventHandler):
    """ Watches for new image files and adds them to the queue """
    def on_created(self, event):
        if not event.is_directory and is_image_file(event.src_path):
            print(f"\nNew image detected: {event.src_path}")
            file_queue.put((event.src_path, 1))  # Start with attempt 1

def start_watching():
    """ Watches the PICTURES_FOLDER for new files """
    # First scan existing files
    scan_existing_files()
    
    # Then start watching for new files
    observer = Observer()
    event_handler = ImageWatcher()
    observer.schedule(event_handler, PICTURES_FOLDER, recursive=True)
    observer.start()
    print(f"Watching for new images in {PICTURES_FOLDER}")

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\nStopping image watcher...")
        observer.stop()
    observer.join()

if __name__ == "__main__":
    # Start the worker thread that processes images in the queue
    worker_thread = threading.Thread(target=process_new_files, daemon=True)
    worker_thread.start()

    start_watching()

Save with CTRL+o and exit with CTRL+x.

Make the file executable with

chmod +x resize_images.py

Add a system file to launch automatically

If you want the script to run in the background, create a service file to start it automatically at boot.

Run the following command to create the service file:

sudo nano /etc/systemd/system/image-resizer.service

Then, add this content (assuming your script is in /home/pi/resize_images.py):

[Unit]
Description=Automatic Image Resizer for Digital Picture Frame
After=network.target

[Service]
ExecStart=/home/pi/venv_picframe/bin/python3 /home/pi/resize_images.py
WorkingDirectory=/home/pi/
Restart=always
User=pi
Group=pi

[Install]
WantedBy=multi-user.target

After saving the file, reload systems:

sudo systemctl daemon-reload

Then, enable the service so it starts at boot:

sudo systemctl enable image-resizer.service

To start it immediately without rebooting:

sudo systemctl start image-resizer.service

To check if it’s running:

sudo systemctl status image-resizer.service

Conclusion

With this script running in the background, your images will always be resized correctly for your digital picture frame. This is especially recommended if you’re using a Pi Zero, but it’s a good idea regardless.

Just be sure to keep your original files.

When you inevitably upgrade to a large 4K monitor (trust me, you will!), you’ll have the best-quality images ready for your new display.

Was this article helpful?


Thank you for your support and motivation.


Scroll to Top