图片随机轮播工具
用ai写了一个简单的图片轮播的工具
功能
1.随机播放特定文件夹中的图片
2.左右方向键随机切换图片
特点
1.支持播放webp动图
2.切换图片时不会卡顿
使用说明
1.安装依赖pip install pygame pillow
2.将IMAGE_DIR 改成你的文件夹路径
注意
短时间多次切换图片会卡顿
import os
import random
import threading
import time
from collections import OrderedDict
from concurrent.futures import ThreadPoolExecutor
import pygame
from PIL import Image, ImageSequence
# ------------------------- 配置区 -------------------------
IMAGE_DIR = r"这里输入文件夹路径Enter the folder path here."
SUPPORTED_EXT = {".webp", ".png", ".jpg", ".jpeg", ".bmp", ".gif"}
PREFETCH_POOL_SIZE = 2 # 后台随时保持解码好的候选图数量
DECODE_THREAD_WORKERS = 4 # 解码线程池大小
IMAGE_CACHE_SIZE = 8 # 最近访问图片的 LRU 缓存容量
BG_COLOR = (0, 0, 0)
TARGET_FPS = 60
# -----------------------------------------------------------
class Frame:
"""单帧数据:原始 Surface(未做显示格式转换) + 时长(秒)"""
__slots__ = ("raw_surf", "converted_surf", "duration")
def __init__(self, raw_surf, duration):
self.raw_surf = raw_surf
self.converted_surf = None
self.duration = duration
def get_surface(self):
# 懒转换:第一次被主线程用到时才 convert_alpha,
# 避免在后台解码线程里调用 SDL 相关函数
if self.converted_surf is None:
self.converted_surf = self.raw_surf.convert_alpha()
return self.converted_surf
class AnimatedImage:
"""
表示一张图片(可能是动图)。后台线程逐帧解码并追加到 self.frames,
first_frame_ready 事件在第 0 帧解码完成后立即置位,主线程据此即可开始播放,
不需要等整张动图解码完。
"""
def __init__(self, path):
self.path = path
self.frames = [] # List[Frame]
self.is_static = False
self.done_decoding = threading.Event()
self.first_frame_ready = threading.Event()
self.error = None
def decode(self):
try:
im = Image.open(self.path)
n_frames = getattr(im, "n_frames", 1)
self.is_static = n_frames <= 1
for i, frame in enumerate(ImageSequence.Iterator(im)):
rgba = frame.convert("RGBA")
data = rgba.tobytes()
# frombuffer 是纯内存操作,线程安全,不涉及 SDL 显示子系统
surf = pygame.image.frombuffer(data, rgba.size, "RGBA").copy()
duration = frame.info.get("duration", 100) / 1000.0
if duration <= 0:
duration = 0.1
self.frames.append(Frame(surf, duration))
if i == 0:
self.first_frame_ready.set()
except Exception as e: # 单张图片损坏不应影响整个程序
self.error = e
self.first_frame_ready.set()
finally:
self.done_decoding.set()
class ImageLibrary:
"""管理图片列表、后台解码线程池、LRU 缓存与随机预取候选池"""
def __init__(self, root_dir):
self.paths = self._scan(root_dir)
if not self.paths:
raise RuntimeError(f"目录中没有找到支持的图片: {root_dir}")
self.executor = ThreadPoolExecutor(max_workers=DECODE_THREAD_WORKERS)
self.cache = OrderedDict() # path -> AnimatedImage (LRU)
self.prefetch_pool = {} # path -> AnimatedImage (预取中/已就绪)
self._lock = threading.Lock()
@staticmethod
def _scan(root_dir):
paths = []
for dirpath, _, filenames in os.walk(root_dir):
for fn in filenames:
ext = os.path.splitext(fn)[1].lower()
if ext in SUPPORTED_EXT:
paths.append(os.path.join(dirpath, fn))
return paths
def _start_decode(self, path):
img = AnimatedImage(path)
self.executor.submit(img.decode)
return img
def _touch_cache(self, path, img):
with self._lock:
if path in self.cache:
self.cache.move_to_end(path)
else:
self.cache[path] = img
while len(self.cache) > IMAGE_CACHE_SIZE:
self.cache.popitem(last=False)
def get_or_start(self, path):
with self._lock:
if path in self.cache:
img = self.cache[path]
self.cache.move_to_end(path)
return img
if path in self.prefetch_pool:
img = self.prefetch_pool.pop(path)
self._touch_cache(path, img)
return img
img = self._start_decode(path)
self._touch_cache(path, img)
return img
def random_path(self, exclude=None):
if len(self.paths) == 1:
return self.paths[0]
while True:
p = random.choice(self.paths)
if p != exclude:
return p
def ensure_prefetch(self, exclude_paths, count=PREFETCH_POOL_SIZE):
"""把预取候选池补充到 count 个"""
with self._lock:
need = count - len(self.prefetch_pool)
existing = set(self.prefetch_pool) | set(self.cache) | set(exclude_paths)
for _ in range(max(0, need)):
candidate = self.random_path()
tries = 0
while candidate in existing and tries < 10:
candidate = self.random_path()
tries += 1
img = self._start_decode(candidate)
with self._lock:
self.prefetch_pool[candidate] = img
existing.add(candidate)
def pop_prefetched(self, exclude_paths, wait_timeout=0.5):
"""从预取池取一张随机候选;优先取已经就绪的,极端连续按键时短暂兜底等待"""
with self._lock:
items = [
(p, img) for p, img in self.prefetch_pool.items()
if p not in exclude_paths
]
if not items:
return None
ready = [(p, img) for p, img in items if img.first_frame_ready.is_set()]
if ready:
p, img = random.choice(ready)
else:
p, img = items[0]
img.first_frame_ready.wait(timeout=wait_timeout)
with self._lock:
self.prefetch_pool.pop(p, None)
self._touch_cache(p, img)
return p, img
class Viewer:
def __init__(self, library):
pygame.init()
pygame.display.set_caption("随机图片播放器")
info = pygame.display.Info()
self.window_size = (info.current_w, info.current_h)
self.screen = pygame.display.set_mode(self.window_size, pygame.RESIZABLE)
self.library = library
self.current_path = self.library.random_path()
self.current_img = self.library.get_or_start(self.current_path)
# 初始第一张图需要短暂等待首帧解码完成(通常几毫秒到几十毫秒)
self.current_img.first_frame_ready.wait(timeout=3.0)
self.frame_index = 0
self.frame_timer = 0.0
self._scaled_cache = None
self._scaled_cache_key = None
self.clock = pygame.time.Clock()
self.running = True
# 提前把预取池填满,为第一次按键切换做准备
self.library.ensure_prefetch(exclude_paths={self.current_path})
def switch_to_random(self):
result = self.library.pop_prefetched(exclude_paths={self.current_path})
if result is None:
path = self.library.random_path(exclude=self.current_path)
img = self.library.get_or_start(path)
else:
path, img = result
self.current_path = path
self.current_img = img
self.frame_index = 0
self.frame_timer = 0.0
self._scaled_cache = None
self._scaled_cache_key = None
# 补齐预取池,为下一次切换做准备
self.library.ensure_prefetch(exclude_paths={self.current_path})
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
elif event.type == pygame.VIDEORESIZE:
new_size = (event.w, event.h)
if new_size != self.window_size:
self.window_size = new_size
self.screen = pygame.display.set_mode(self.window_size, pygame.RESIZABLE)
self._scaled_cache = None
elif event.type == pygame.KEYDOWN:
if event.key in (pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN):
self.switch_to_random()
elif event.key == pygame.K_ESCAPE:
self.running = False
def _current_frame(self):
img = self.current_img
if img.error and not img.frames:
# 损坏文件:自动跳过,换下一张,避免卡死在黑屏
self.switch_to_random()
img = self.current_img
if not img.frames:
return None
if self.frame_index >= len(img.frames):
if img.done_decoding.is_set():
self.frame_index = 0
self.frame_timer = 0.0
else:
self.frame_index = len(img.frames) - 1
return img.frames[self.frame_index]
def update_animation(self, dt):
img = self.current_img
if img.is_static or not img.frames:
return
self.frame_timer += dt
while True:
idx = min(self.frame_index, len(img.frames) - 1)
duration = img.frames[idx].duration
if self.frame_timer < duration:
break
self.frame_timer -= duration
nxt = self.frame_index + 1
if nxt >= len(img.frames):
if img.done_decoding.is_set():
nxt = 0
else:
break # 解码还没追上播放进度,先停在当前帧
self.frame_index = nxt
def _get_scaled(self, frame):
surf = frame.get_surface()
key = (id(surf), self.window_size)
if self._scaled_cache_key == key:
return self._scaled_cache
w, h = surf.get_size()
win_w, win_h = self.window_size
if w == 0 or h == 0 or win_w <= 0 or win_h <= 0:
return surf
scale = min(win_w / w, win_h / h)
new_size = (max(1, int(w * scale)), max(1, int(h * scale)))
scaled = surf if new_size == (w, h) else pygame.transform.smoothscale(surf, new_size)
self._scaled_cache = scaled
self._scaled_cache_key = key
return scaled
def draw(self):
self.screen.fill(BG_COLOR)
frame = self._current_frame()
if frame is not None:
scaled = self._get_scaled(frame)
win_w, win_h = self.window_size
x = (win_w - scaled.get_width()) // 2
y = (win_h - scaled.get_height()) // 2
self.screen.blit(scaled, (x, y))
pygame.display.flip()
def run(self):
while self.running:
dt = self.clock.tick(TARGET_FPS) / 1000.0
self.handle_events()
self.update_animation(dt)
self.draw()
pygame.quit()
def main():
library = ImageLibrary(IMAGE_DIR)
viewer = Viewer(library)
viewer.run()
if __name__ == "__main__":
main()图片随机轮播工具
https:/adrrr.com/a/1
评论