You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

树莓派Zero Python电子相框脚本OOM被终止问题求助

树莓派Zero Python电子相框OOM内存泄漏问题

我在树莓派Zero上运行自制的Python电子相框脚本,脚本功能正常,但频繁因内存不足(OOM)被内核终止,内核日志如下:

Dec 14 15:55:25 raspberrypi kernel: [ 2987.175293] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),global_oom,task_memcg=/,task=python,pid=1759,uid=1000
Dec 14 15:55:25 raspberrypi kernel: [ 2987.175357] Out of memory: Killed process 1759 (python) total-vm:273328kB, anon-rss:250680kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:276kB oom_score_adj:0

我几乎没接触过Python,对内存管理不太懂,脚本代码如下:

from PIL import Image
import os
import random
import subprocess
import time
import signal

def scan_images(image_dir):
    print("scanning images")
    image_files = []
    for root, dirs, files in os.walk(image_dir):
        for file in files:
            if file.lower().endswith(('.png', '.jpg', '.jpeg')):
                image_files.append(os.path.join(root, file))
    print("finished scanning")
    return image_files

def create_slideshow(image_dir):
    # Register a signal handler for interrupt (Ctrl+C)
    signal.signal(signal.SIGINT, signal_handler)

    try:
        os.environ['DISPLAY'] = ':0.0'
        image_files = scan_images(image_dir)
        vertical_images = [img for img in image_files if Image.open(os.path.join(image_dir, img)).height > Image.open(os.path.join(image_dir, img)).width]
        horizontal_images = [img for img in image_files if Image.open(os.path.join(image_dir, img)).width >= Image.open(os.path.join(image_dir, img)).height]
        random.shuffle(vertical_images)
        print("found {} vertical images".format(len(vertical_images)))
        random.shuffle(horizontal_images)
        print("found {} horizontal images".format(len(horizontal_images)))

        feh_current_process = None
        feh_previous_process = None

        while True:
            # Display horizontal images on their own
            for horizontal_image in horizontal_images:
            # Launch new feh process
                print("executing command")
                feh_previous_process = feh_current_process
                feh_current_process = subprocess.Popen(['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', os.path.join(image_dir, horizontal_image)])

                # Pause for a few seconds (adjust as needed)
                print("sleeping 5 sec")
                time.sleep(5)

                # terminate old feh
                print("terminate old feh")
                if feh_previous_process != None:
                    feh_previous_process.terminate()
                
                # Pause for a few seconds (adjust as needed)
                print("sleeping 5 sec")
                time.sleep(5)
            # Display paired vertical images with an additional vertical image on the right side
            for i in range(0, len(vertical_images)):
                # Select vertical image
                current_vertical_image = vertical_images[i]

                # Select one additional random vertical image for the right side
                additional_vertical_image = random.choice(vertical_images)

                # Create composite image with two vertical images side by side
                composite_image_path = os.path.join(image_dir, 'composite_image.jpg')
                create_composite_image(image_dir, current_vertical_image, additional_vertical_image, composite_image_path)

                # Display the composite image in the background
                display_command = ['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', composite_image_path]
                print("executing command {}".format(display_command))
                feh_previous_process = feh_current_process
                feh_current_process = subprocess.Popen(display_command)

                # Pause for a few seconds (adjust as needed)
                print("sleeping 5 sec")
                time.sleep(5)

                # terminate old feh
                print("terminate old feh")
                if feh_previous_process != None:
                    feh_previous_process.terminate()
                
                # Pause for a few seconds (adjust as needed)
                print("sleeping 5 sec")
                time.sleep(5)
    except KeyboardInterrupt:
        print("Slideshow interrupted. Cleaning up...")
        cleanup()

def signal_handler(sig, frame):
    # Handle interrupt signal (Ctrl+C)
    raise KeyboardInterrupt

def cleanup():
    # Additional cleanup steps if needed
    sys.exit(0)
    
def create_composite_image(image_dir, image1, image2, output_path):
    img1 = Image.open(os.path.join(image_dir, image1))
    img2 = Image.open(os.path.join(image_dir, image2))

    # Resize both vertical images to the height of the taller one
    max_height = max(img1.height, img2.height)
    img1 = img1.resize((int(img1.width * max_height / img1.height), max_height))
    img2 = img2.resize((int(img2.width * max_height / img2.height), max_height))

    # Create a new image with double width for vertical images
    composite_img = Image.new('RGB', (img1.width + img2.width, max_height))
    composite_img.paste(img1, (0, 0))
    composite_img.paste(img2, (img1.width, 0))

    # Save the composite image
    composite_img.save(output_path)
    img1.close()
    img2.close()
    composite_img.close()

if __name__ == "__main__":
    import sys

    if len(sys.argv) != 2:
        print("Usage: python slideshow.py /path/to/image/directory")
        sys.exit(1)

    image_directory = sys.argv[1]

    if not os.path.isdir(image_directory):
        print(f"Error: {image_directory} is not a valid directory.")
        sys.exit(1)

    create_slideshow(image_directory)

我已经尝试关闭图片对象、终止旧feh进程,但还是有内存泄漏,希望找出遗漏的清理点。


问题分析与修复方案

你的内存泄漏主要来自三个核心问题,下面逐个说明并给出修改后的代码:

1. 分类图片时重复打开且未关闭图片对象

在分类垂直/水平图片的列表推导式里,你每张图片打开了两次,而且没有关闭这些临时打开的Image对象,这些对象会一直占用内存直到Python垃圾回收,但树莓派Zero内存紧张,等不及回收就会OOM。

修改方式:遍历图片时只打开一次,获取尺寸后立刻关闭。

# 替换原有的vertical_images和horizontal_images生成代码
vertical_images = []
horizontal_images = []
for img_path in image_files:
    with Image.open(img_path) as img:
        width, height = img.size
    if height > width:
        vertical_images.append(img_path)
    else:
        horizontal_images.append(img_path)

用with语句会自动关闭图片,避免内存残留。

2. feh进程只terminate但未等待回收

调用terminate()只是给进程发终止信号,但进程可能没完全退出,残留的进程资源会占用内存。需要用wait()等待进程彻底结束,释放资源。

修改feh终止的代码:

if feh_previous_process is not None:
    feh_previous_process.terminate()
    feh_previous_process.wait()  # 等待进程彻底退出
    feh_previous_process = None  # 清空引用,帮助GC回收

3. 复合图片生成时的中间对象未及时回收

虽然你关闭了图片,但resize生成的新Image对象没有显式处理。用with语句包裹所有Image操作,确保自动释放。

修改后的create_composite_image函数:

def create_composite_image(image_dir, image1, image2, output_path):
    with Image.open(os.path.join(image_dir, image1)) as img1, \
         Image.open(os.path.join(image_dir, image2)) as img2:
        max_height = max(img1.height, img2.height)
        # resize后直接使用,不用赋值给原变量,避免额外引用
        img1_resized = img1.resize((int(img1.width * max_height / img1.height), max_height))
        img2_resized = img2.resize((int(img2.width * max_height / img2.height), max_height))
        
        with Image.new('RGB', (img1_resized.width + img2_resized.width, max_height)) as composite_img:
            composite_img.paste(img1_resized, (0, 0))
            composite_img.paste(img2_resized, (img1_resized.width, 0))
            composite_img.save(output_path)
    # 所有对象都会在with块结束后自动关闭

4. 额外优化:主动触发垃圾回收

树莓派Zero内存有限,可在脚本开头开启垃圾回收并调整阈值,在循环间隙手动触发回收,加快内存释放:

import gc
gc.enable()
gc.set_threshold(500, 10, 10)  # 让GC更频繁运行

完整修改后的脚本

from PIL import Image
import os
import random
import subprocess
import time
import signal
import sys
import gc

# 开启并优化垃圾回收
gc.enable()
gc.set_threshold(500, 10, 10)

def scan_images(image_dir):
    print("scanning images")
    image_files = []
    for root, dirs, files in os.walk(image_dir):
        for file in files:
            if file.lower().endswith(('.png', '.jpg', '.jpeg')):
                image_files.append(os.path.join(root, file))
    print("finished scanning")
    return image_files

def create_slideshow(image_dir):
    signal.signal(signal.SIGINT, signal_handler)

    try:
        os.environ['DISPLAY'] = ':0.0'
        image_files = scan_images(image_dir)
        
        # 修复图片分类时的内存泄漏
        vertical_images = []
        horizontal_images = []
        for img_path in image_files:
            with Image.open(img_path) as img:
                width, height = img.size
            if height > width:
                vertical_images.append(img_path)
            else:
                horizontal_images.append(img_path)
        
        random.shuffle(vertical_images)
        print("found {} vertical images".format(len(vertical_images)))
        random.shuffle(horizontal_images)
        print("found {} horizontal images".format(len(horizontal_images)))

        feh_current_process = None
        feh_previous_process = None

        while True:
            # Display horizontal images on their own
            for horizontal_image in horizontal_images:
                print("executing command")
                feh_previous_process = feh_current_process
                feh_current_process = subprocess.Popen(['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', horizontal_image])

                print("sleeping 5 sec")
                time.sleep(5)

                # 修复feh进程回收
                if feh_previous_process is not None:
                    feh_previous_process.terminate()
                    feh_previous_process.wait()
                    feh_previous_process = None
                
                print("sleeping 5 sec")
                time.sleep(5)
            
            # Display paired vertical images
            for i in range(0, len(vertical_images)):
                current_vertical_image = vertical_images[i]
                additional_vertical_image = random.choice(vertical_images)

                composite_image_path = os.path.join(image_dir, 'composite_image.jpg')
                create_composite_image(image_dir, current_vertical_image, additional_vertical_image, composite_image_path)

                display_command = ['feh', '--fullscreen', '--auto-zoom', '--hide-pointer', composite_image_path]
                print("executing command {}".format(display_command))
                feh_previous_process = feh_current_process
                feh_current_process = subprocess.Popen(display_command)

                print("sleeping 5 sec")
                time.sleep(5)

                # 修复feh进程回收
                if feh_previous_process is not None:
                    feh_previous_process.terminate()
                    feh_previous_process.wait()
                    feh_previous_process = None
                
                print("sleeping 5 sec")
                time.sleep(5)
            # 每次大循环后手动触发一次垃圾回收
            gc.collect()
    except KeyboardInterrupt:
        print("Slideshow interrupted. Cleaning up...")
        cleanup()

def signal_handler(sig, frame):
    raise KeyboardInterrupt

def cleanup():
    # 清理残留的feh进程
    global feh_current_process, feh_previous_process
    if feh_current_process is not None:
        feh_current_process.terminate()
        feh_current_process.wait()
    if feh_previous_process is not None:
        feh_previous_process.terminate()
        feh_previous_process.wait()
    sys.exit(0)
    
def create_composite_image(image_dir, image1, image2, output_path):
    with Image.open(os.path.join(image_dir, image1)) as img1, \
         Image.open(os.path.join(image_dir, image2)) as img2:
        max_height = max(img1.height, img2.height)
        img1_resized = img1.resize((int(img1.width * max_height / img1.height), max_height))
        img2_resized = img2.resize((int(img2.width * max_height / img2.height), max_height))
        
        with Image.new('RGB', (img1_resized.width + img2_resized.width, max_height)) as composite_img:
            composite_img.paste(img1_resized, (0, 0))
            composite_img.paste(img2_resized, (img1_resized.width, 0))
            composite_img.save(output_path)
    # 手动清理临时变量
    del img1_resized, img2_resized
    gc.collect()

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python slideshow.py /path/to/image/directory")
        sys.exit(1)

    image_directory = sys.argv[1]

    if not os.path.isdir(image_directory):
        print(f"Error: {image_directory} is not a valid directory.")
        sys.exit(1)

    create_slideshow(image_directory)

内容的提问来源于stack exchange,提问作者Toan Pham

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 01:19:51