""" Compress images using Pillow. Reads from data/filtered_images, writes compressed output to data/compressed_images. """ import os import sys from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed from PIL import Image INPUT_DIR = Path(__file__).parent / "data" / "filtered_images" OUTPUT_DIR = Path(__file__).parent / "data" / "compressed_images" JPEG_QUALITY = 60 MAX_DIMENSION = 800 CONCURRENCY = 20 def compress_one(file: Path, total: int, counters: dict) -> None: out_path = OUTPUT_DIR / file.name if out_path.exists(): counters["skipped"] += 1 return try: img = Image.open(file).convert("RGB") img.thumbnail((MAX_DIMENSION, MAX_DIMENSION), Image.LANCZOS) img.save(out_path, "JPEG", quality=JPEG_QUALITY, optimize=True) counters["processed"] += 1 if counters["processed"] % 100 == 0: print(f" Compressed {counters['processed']}/{total} (skipped: {counters['skipped']})...") except Exception as e: print(f" Failed: {file.name} — {e}", file=sys.stderr) def main(): if not INPUT_DIR.exists(): print(f"INPUT_DIR not found: {INPUT_DIR}", file=sys.stderr) sys.exit(1) OUTPUT_DIR.mkdir(parents=True, exist_ok=True) files = [f for f in INPUT_DIR.iterdir() if f.suffix.lower() in (".jpg", ".jpeg")] print(f"Found {len(files)} images. Running with concurrency={CONCURRENCY}...") counters = {"processed": 0, "skipped": 0} with ThreadPoolExecutor(max_workers=CONCURRENCY) as executor: futures = {executor.submit(compress_one, f, len(files), counters): f for f in files} for future in as_completed(futures): future.result() print(f"\nDone. Compressed: {counters['processed']}, Skipped (existing): {counters['skipped']}") if __name__ == "__main__": main()