Add pluggable postprocessing filter system with collapsible UI

Replace hardcoded gamma/saturation/brightness fields with a flexible
filter pipeline architecture. Templates now contain an ordered list of
filter instances, each with its own options schema. Filters operate on
full images before border extraction.

- Add filter framework: base class, registry, image pool, filter instance
- Implement 6 built-in filters: brightness, saturation, gamma, downscaler, pixelate, auto crop
- Move smoothing from PP templates to device stream settings (temporal, not spatial)
- Add GET /api/v1/filters endpoint for available filter types
- Dynamic filter UI in template modal with add/remove/reorder/collapse
- Replace camera icon with display icon for screen capture streams
- Legacy migration: existing templates auto-convert flat fields to filter list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 11:57:19 +03:00
parent e8cbc73161
commit ebd6cc7d7d
16 changed files with 1115 additions and 192 deletions

View File

@@ -1,20 +1,19 @@
"""Postprocessing template data model."""
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from typing import List, Optional
from wled_controller.core.filters.filter_instance import FilterInstance
@dataclass
class PostprocessingTemplate:
"""Postprocessing settings template for color correction and smoothing."""
"""Postprocessing settings template containing an ordered list of filters."""
id: str
name: str
gamma: float
saturation: float
brightness: float
smoothing: float
filters: List[FilterInstance]
created_at: datetime
updated_at: datetime
description: Optional[str] = None
@@ -24,10 +23,7 @@ class PostprocessingTemplate:
return {
"id": self.id,
"name": self.name,
"gamma": self.gamma,
"saturation": self.saturation,
"brightness": self.brightness,
"smoothing": self.smoothing,
"filters": [f.to_dict() for f in self.filters],
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"description": self.description,
@@ -35,14 +31,30 @@ class PostprocessingTemplate:
@classmethod
def from_dict(cls, data: dict) -> "PostprocessingTemplate":
"""Create template from dictionary."""
"""Create template from dictionary.
Supports migration from legacy flat-field format (gamma/saturation/brightness)
to the new filters list format.
"""
if "filters" in data:
filters = [FilterInstance.from_dict(f) for f in data["filters"]]
else:
# Legacy migration: construct filters from flat fields
filters = []
brightness = data.get("brightness", 1.0)
if brightness != 1.0:
filters.append(FilterInstance("brightness", {"value": brightness}))
saturation = data.get("saturation", 1.0)
if saturation != 1.0:
filters.append(FilterInstance("saturation", {"value": saturation}))
gamma = data.get("gamma", 2.2)
if gamma != 2.2:
filters.append(FilterInstance("gamma", {"value": gamma}))
return cls(
id=data["id"],
name=data["name"],
gamma=data.get("gamma", 2.2),
saturation=data.get("saturation", 1.0),
brightness=data.get("brightness", 1.0),
smoothing=data.get("smoothing", 0.3),
filters=filters,
created_at=datetime.fromisoformat(data["created_at"])
if isinstance(data.get("created_at"), str)
else data.get("created_at", datetime.utcnow()),