feat: reduce build size — replace Pillow with cv2, refactor build scripts
All checks were successful
Build Release / create-release (push) Successful in 1s
Build Release / build-docker (push) Successful in 42s
Lint & Test / test (push) Successful in 2m50s
Build Release / build-windows (push) Successful in 3m27s
Build Release / build-linux (push) Successful in 1m59s

- Create utils/image_codec.py with cv2-based image helpers
- Replace PIL usage across all routes, filters, and engines with cv2
- Move Pillow from core deps to [tray] optional in pyproject.toml
- Extract shared build logic into build-common.sh (detect_version, cleanup, etc.)
- Strip unused NumPy/PIL/zeroconf/debug files in build scripts
This commit is contained in:
2026-03-25 14:18:16 +03:00
parent 7da5084337
commit 7939322a7f
18 changed files with 444 additions and 377 deletions

View File

@@ -1,12 +1,9 @@
"""Postprocessing template routes."""
import base64
import io
import time
import httpx
import numpy as np
from PIL import Image
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
from wled_controller.api.auth import AuthRequired
@@ -198,6 +195,13 @@ async def test_pp_template(
raw_stream = chain["raw_stream"]
from wled_controller.utils.image_codec import (
encode_jpeg_data_uri,
load_image_bytes,
load_image_file,
thumbnail as make_thumbnail,
)
if isinstance(raw_stream, StaticImagePictureSource):
# Static image: load directly
from pathlib import Path
@@ -209,12 +213,12 @@ async def test_pp_template(
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
resp = await client.get(source)
resp.raise_for_status()
pil_image = Image.open(io.BytesIO(resp.content)).convert("RGB")
image = load_image_bytes(resp.content)
else:
path = Path(source)
if not path.exists():
raise HTTPException(status_code=400, detail=f"Image file not found: {source}")
pil_image = Image.open(path).convert("RGB")
image = load_image_file(path)
actual_duration = time.perf_counter() - start_time
frame_count = 1
@@ -268,53 +272,37 @@ async def test_pp_template(
if last_frame is None:
raise RuntimeError("No frames captured during test")
if isinstance(last_frame.image, np.ndarray):
pil_image = Image.fromarray(last_frame.image)
else:
if not isinstance(last_frame.image, np.ndarray):
raise ValueError("Unexpected image format from engine")
image = last_frame.image
# Create thumbnail
thumbnail_width = 640
aspect_ratio = pil_image.height / pil_image.width
thumbnail_height = int(thumbnail_width * aspect_ratio)
thumbnail = pil_image.copy()
thumbnail.thumbnail((thumbnail_width, thumbnail_height), Image.Resampling.LANCZOS)
thumb = make_thumbnail(image, 640)
# Apply postprocessing filters (expand filter_template references)
flat_filters = pp_store.resolve_filter_instances(pp_template.filters)
if flat_filters:
pool = ImagePool()
def apply_filters(img):
arr = np.array(img)
def apply_filters(arr):
for fi in flat_filters:
f = FilterRegistry.create_instance(fi.filter_id, fi.options)
result = f.process_image(arr, pool)
if result is not None:
arr = result
return Image.fromarray(arr)
return arr
thumbnail = apply_filters(thumbnail)
pil_image = apply_filters(pil_image)
thumb = apply_filters(thumb)
image = apply_filters(image)
# Encode thumbnail
img_buffer = io.BytesIO()
thumbnail.save(img_buffer, format='JPEG', quality=85)
img_buffer.seek(0)
thumbnail_b64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
thumbnail_data_uri = f"data:image/jpeg;base64,{thumbnail_b64}"
# Encode full-resolution image
full_buffer = io.BytesIO()
pil_image.save(full_buffer, format='JPEG', quality=90)
full_buffer.seek(0)
full_b64 = base64.b64encode(full_buffer.getvalue()).decode('utf-8')
full_data_uri = f"data:image/jpeg;base64,{full_b64}"
# Encode as JPEG
thumbnail_data_uri = encode_jpeg_data_uri(thumb, quality=85)
full_data_uri = encode_jpeg_data_uri(image, quality=90)
actual_fps = frame_count / actual_duration if actual_duration > 0 else 0
avg_capture_time_ms = (total_capture_time / frame_count * 1000) if frame_count > 0 else 0
width, height = pil_image.size
thumb_w, thumb_h = thumbnail.size
height, width = image.shape[:2]
thumb_h, thumb_w = thumb.shape[:2]
return TemplateTestResponse(
full_capture=CaptureImage(