Color/contrast/dithering sliders + before/after render preview

Advanced configuration gains three sliders (PIL ImageEnhance factors
for color/contrast, 0-2, 1=unchanged; a 0-1 dithering strength) applied
to every photo this frame renders. Confirmed the parameter conventions
against a similar project (jwchen119/EPF: ImageEnhance.Color/Contrast,
1.0 baseline) before implementing; dithering strength isn't natively
exposed by PIL's quantize(), so it's implemented by blending the source
toward its own flat/undithered quantization before running Floyd-
Steinberg on the blend -- at 0 there's no quantization error left to
diffuse (exactly the flat result), at 1 it's the original unmodified
behavior, with a smooth continuum between rather than dithering being
an on/off toggle.

image_pipeline.py split into composition (_compose), enhancement
(_enhance), quantization (_quantize), and transpose+pack stages so
render_frame (device bytes) and the new render_preview_png (a normal
viewable PNG, upright logical orientation) share the same pipeline
instead of duplicating it. Named-face overlay label math (face_labels.py)
was already routed through the shared _placement_transform, so it
needed no changes for the new params.

Also added the requested before/after comparison: the Configuration
tab's new Preview card shows the current photo's untouched Immich
preview next to that same photo run through the frame's actual saved
rendering pipeline (two new GET endpoints, /preview/original and
/preview/rendered) -- immediate visual feedback for tuning the palette
and these new sliders. "Refresh preview" re-fetches after saving.

Schema migration v6 adds color_boost/contrast_boost/dither_strength,
defaulting to 1.0/1.0/1.0 -- reproduces the exact previous rendering
until a frame's Configuration tab changes one.

Verified against the live-shaped test database: the migration, sliders
persisting and clamping out-of-range input, both preview endpoints
(real JPEG passthrough / real PNG at correct logical size+orientation),
confirmed dither_strength=0 actually changes the rendered bytes vs.
default, and the standing legacy-device curl suite.
This commit is contained in:
2026-07-22 08:46:22 -04:00
parent 83c59af1dd
commit e48ac50ea1
9 changed files with 303 additions and 57 deletions
+7
View File
@@ -131,6 +131,13 @@ class Frame(Base):
# DEFAULT_PALETTE_RGB for this frame's actual panel. NULL = use the
# default -- most frames never touch this.
palette_rgb: Mapped[list | None] = mapped_column(JSON, nullable=True, default=None)
# Advanced configuration: PIL ImageEnhance factors, 1.0 = unchanged
# (see image_pipeline.render_frame).
color_boost: Mapped[float] = mapped_column(Float, default=1.0)
contrast_boost: Mapped[float] = mapped_column(Float, default=1.0)
# 0.0-1.0, see image_pipeline._quantize -- 1.0 matches this project's
# original always-on full-strength Floyd-Steinberg dithering.
dither_strength: Mapped[float] = mapped_column(Float, default=1.0)
# -- state --
current_asset_id: Mapped[str] = mapped_column(String, default="")