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.
Related Articles
- Celebrate birthdays and anniversaries with auto-themed photos on your digital frame
- How to only show portrait photos on your Raspberry Pi photo frame
- How to automatically remove duplicate images from your Pictures folder (2024 Edition)
- Date filtering doesn’t work? Check your photos for valid Exif date information