Commit Graph
46 Commits
Author SHA1 Message Date
facok 8c91c1957e Update README with sharpness control documentation 2026-03-20 17:09:27 +08:00
facok 0fc53fce8b Remove image_size parameter from LCSSharpnessCalibrate 2026-03-20 17:07:49 +08:00
facok 1d024be9da Revert to 512px default, remove diagnostic logging
256 vs 512 difference (97.5% vs 97.3%) is within random variation —
different image_size consumes different random numbers from the same
seed, producing different images rather than a controlled comparison.
Keep 512 consistent with LCS color calibration. Remove per-blur-level
mean logging that was used during development.
2026-03-20 17:06:17 +08:00
facok ce4007b916 Default to 256px calibration images, add image_size parameter
256px gives the best PC1 (97.5%) and fastest calibration.
Larger images average more patches, reducing per-sample variance
but not improving PCA purity. Added image_size input to
LCSSharpnessCalibrate for experimentation.
2026-03-20 17:02:21 +08:00
facok 71233801a2 Use uniform blur levels (0,0.5,1,2,4,8) for sharpness calibration
σ=16 nearly destroys all spatial content (≈solid color), adding little
information beyond σ=8 while distorting PCA with an extreme outlier.
Adding σ=0.5 better captures subtle sharpness differences.

Result: PC1 96.7% → 97.3%, correlation 0.748 → 0.893.
2026-03-20 16:51:57 +08:00
facok 9717de8456 Remove DC component from pc1_dir to eliminate residual brightness shift 2026-03-20 16:42:32 +08:00
facok f1661ed4bf Use grayscale noise for sharpness calibration
RGB noise has independent per-channel variation that blur affects
differently, introducing inter-channel variance captured as PC2.
Grayscale noise (same value across RGB) ensures blur affects all
channels identically, raising PC1 from 95.8% to 96.7%.

Also added per-vector zero-mean to remove VAE brightness drift
and diagnostic logging for per-blur-level latent means.
2026-03-20 16:37:34 +08:00
facok a120651fba Fix sharpness strength scaling: drop pc1_std multiplier
pc1_std=12.78 represents the spread across blur levels 0-16 during
calibration, producing edits ~34x larger than the patch mean. Since PCA
basis vectors are already unit norm, strength directly controls the
L2 magnitude of the edit. Widen UI range to -5..5 to compensate.
2026-03-20 16:14:25 +08:00
facok e9662139c7 Simplify sharpness intervention: patches + delta * pc1_dir
Root cause of color shift and mosaic artifacts:
1. delta = strength * pc1_std was too large (pc1_std=12.78)
2. Removing residual when using LCS destroyed 62/64 dimensions → mosaic
3. Complex projection/residual/color-removal logic was unnecessary

The correct approach is simple: patches + delta * pc1_dir.
Adding along one direction preserves all other dimensions by construction.
LCS color removal only matters at calibration time (makes pc1_dir orthogonal
to color). At intervention time, just add along the orthogonal direction.

Removed lcs_data input from LCSSharpnessIntervene (only needed at calibration).
Removed all debug logging.
2026-03-20 15:34:40 +08:00
facok 6fe4add02c Add lcs_data to LCSSharpnessIntervene for color-preserved sharpness
When calibration used LCS color removal, the intervention must also
remove the color component before projecting onto the sharpness basis,
then add it back after reconstruction. This ensures sharpness edits
don't affect color.

Workflow:
1. LCSSharpnessCalibrate(vae, lcs_data) → sharpness_data
2. LCSSharpnessIntervene(model, sharpness_data, strength, lcs_data=lcs_data)

Both nodes now have optional lcs_data input. When connected, color is
preserved during sharpness intervention.
2026-03-20 14:16:22 +08:00
facok cc0291352e Add lcs_data input to remove color component during calibration
When LCS color data is connected, the sharpness PC1 will be orthogonal
to the color subspace, preventing color shifts during intervention.

Changes:
- calibrate_sharpness() now accepts optional lcs_data parameter
- LCSSharpnessCalibrate node has optional lcs_data input
- Cache files use "_lcs" suffix when LCS is used (different subspace)
2026-03-20 14:00:57 +08:00
facok 28d97c327e Restore full projection+residual to prevent color shift
The simplified patches + delta * pc1_dir was incorrect because PC1
may correlate with brightness/color directions. Now properly:
1. Project to sharpness subspace: projection = (patches - mean) @ basis
2. Compute residual: residual = patches - (projection @ basis.T + mean)
3. Shift PC1 only
4. Reconstruct with residual: patches_new = new_proj @ basis.T + mean + residual

This ensures sharpness edits don't affect color/brightness.
2026-03-20 13:50:17 +08:00
facok c6b746ff32 Fix conv2d groups parameter and add .contiguous() for expanded kernels 2026-03-20 13:37:50 +08:00
facok 531fb5fd1a Fix sharpness calibration: use same base images for all blur levels
- Generate base images once upfront, then apply all blur levels to the
  SAME images (was generating different random images per blur level)
- Add separable convolution for kernel_size > 15 (O(2k) vs O(k²))
- Add kernel caching to avoid recomputation
- Add per-blur-level progress logging

This matches SubspaceLab's methodology and ensures PCA receives
properly paired stimuli (same image × multiple blur levels).
2026-03-20 13:34:45 +08:00
facok 03fb376b8c Fix sharpness calibration and simplify intervention hook
- Use random noise images instead of solid-color (blur had no effect
  on spatially uniform images, making calibration a no-op)
- Generate images per-batch to avoid 200 MB upfront allocation
- Simplify hook algebraically: patches + delta * pc1_dir eliminates
  projection/reconstruction/residual intermediates (~24 MB per step)
- Move SCALE_FACTOR, SHIFT_FACTOR, find_step_index to core/sampling.py
- Rename sd → shd to avoid confusion with state_dict convention
- Remove redundant [:,:,:,:3] slice and unused imports
2026-03-20 13:12:46 +08:00
facok cf9edf0239 Add sharpness intervention via PCA subspace in FLUX VAE patch space
New calibration (core/sharpness.py) generates blur stimuli, VAE-encodes,
and extracts PC1 as the sharpness direction. New nodes (nodes/sharpen.py)
provide LCSSharpnessCalibrate (auto-cached per-VAE) and
LCSSharpnessIntervene (post-CFG hook with strength, step window, mask).
Positive strength = sharper, negative = blurrier.
2026-03-20 13:01:23 +08:00
facok 28fa9450e0 Add progress output to calibration
- Print calibration parameters at start
- Show encoding progress with batch count
- Print PCA results (variance explained per component)
- Show anchor color encoding status
- Display final basis shape and anchor coordinates
2026-03-20 03:02:04 +08:00
facok 3fec154341 Refactor: extract shared _hue_lerp and _wrap_hue_diff helpers
- Add _wrap_hue_diff() and _hue_lerp() to core/color_space.py for
  reuse across the codebase
- Remove duplicate _hue_lerp from nodes/intervene.py, import from core
- Update diagnostics.py to use shared _hue_lerp instead of inline logic
- Move input_var computation outside strength loop in test_type_ii_uniformity
- Move get_alpha_beta_t50() outside sigma loop in test_early_timestep_amplification
2026-03-19 18:14:13 +08:00
facok b8834564e2 Refactor: extract _bicone_factor helper and clean up diagnostics
- Extract repeated bicone factor formula (1 - |2L - 1|) into _bicone_factor()
  helper function, used in 4 places across color_space.py
- Optimize test_type_ii_uniformity: move decode outside strength loop
- Add named constants for test parameters in diagnostics.py
- Remove unused imports and variables
2026-03-19 15:47:20 +08:00
facok 651599b003 Fix bicone geometry: normalize chroma radii to equatorial
The chromatic anchors (R,G,B,C,M,Y) have different lightness levels
(L=0.38-0.63), so their observed chroma radii are already scaled by
the bicone factor (1 - |2L - 1|). The old code double-counted this
factor, causing round-trip errors of 10-12 units and color distortion.

Fix: In _hue_to_chroma_vector, normalize anchor radii to equatorial
(radius at L=0.5) before interpolation. During encode/decode, apply
bicone factor at target lightness. This ensures proper round-trip:
- encode(lcs) → hsl → encode(hsl) ≈ lcs with error < 1e-5
- Saturation now correctly returns 1.0 for saturated anchor colors

Also add diagnostics module for blurriness analysis:
- test_round_trip_consistency: verify bicone math
- test_normalization_stability: check timestep amplification
- test_type_ii_uniformity: measure variance loss at different strengths
- analyze_blurriness_causes: comprehensive diagnostic report
2026-03-19 13:21:12 +08:00
facok 24d33812c1 Fix step index matching for bfloat16 sigma values
Exact float comparison (sigmas == sigma_val) fails when the sigma
tensor is bfloat16 (FLUX default) but sample_sigmas is float32,
causing intervention hooks to match wrong steps or not fire at all.
Replace with torch.isclose after casting to float32, with argmin
fallback. Both color and tone hooks now share _find_step_index.
2026-03-17 20:07:25 +08:00
facok 6cd650596d Extract shared _chromatic_plane_basis helper and _ANCHOR_HUES constant
Deduplicate the Gram-Schmidt basis construction that was copy-pasted
in decode_lcs_to_hsl, encode_hsl_to_lcs, and calibration.py. Extract
anchor hue values into a module-level constant to prevent silent
divergence. Also remove unnecessary tensor clone in _angle_to_hue and
use torch.empty instead of torch.zeros where all elements are overwritten.
2026-03-17 13:05:27 +08:00
facok 02585b6bf1 Fix bicone HSL mapping by projecting anchors onto chromatic plane
Replace _hue_to_polygon_point with _hue_to_chroma_vector to fix
Type II and interpolated color intervention modes. The old function
interpolated raw 3D anchor positions (mixing lightness into chroma
directions) and ignored calibrated anchor angles. The new function
projects anchors onto the plane perpendicular to the achromatic axis
to get pure chroma radii, then interpolates radius and angle in the
same segment structure as _angle_to_hue for round-trip consistency.
2026-03-17 12:25:49 +08:00
facok 894fb65ef3 Strengthen Warm and Cool preset effects 2026-03-17 07:39:09 +08:00
facok 6fa541ea5a Update README_zh.md 2026-03-17 07:28:29 +08:00
facok 87b78c2f48 Update README.md 2026-03-17 07:27:42 +08:00
facok c0449e3382 Fix BFloat16 error in VAE fingerprinting by casting to float32 2026-03-17 06:57:34 +08:00
facok 773d2bd230 Clarify LCS vs traditional post-processing comparison headings 2026-03-17 06:56:35 +08:00
facok 49b3b4ae44 Polish READMEs: reorganize sections, fix clone URL, tighten wording 2026-03-17 06:45:01 +08:00
facok 750c6484a4 Update READMEs: citation, official repo link, acknowledgments, unofficial notice 2026-03-17 06:39:59 +08:00
facok 20044a9a16 Add LCS vs post-processing comparison to READMEs 2026-03-17 06:32:42 +08:00
facok 48d5f7efb6 Merge LCSCalibrate into LCSLoadData with per-VAE auto-caching
Replace two calibration nodes with a single smart LCSLoadData that
fingerprints VAE weights (sha256 of first decoder weight) and
auto-manages cached calibration files as data/lcs_{fingerprint}.safetensors.
2026-03-17 06:27:55 +08:00
facok 707e789f02 Add server-side preset fallback for headless/batch execution
Frontend JS syncs preset values into sliders on user interaction,
but headless runners (e.g. Lumi-Batcher) skip the frontend entirely.
When a preset is selected but all sliders are still at defaults,
execute() now applies the preset values server-side as a fallback.
2026-03-17 05:50:59 +08:00
facok 98bd528bb4 Add README in English and Chinese 2026-03-17 05:44:36 +08:00
facok e8f37b36d1 Add Base preset to reset all tone sliders to defaults 2026-03-17 05:25:54 +08:00
facok 7aa68375f9 Fix color temperature having no visible effect
The warm_dir was a unit vector (norm=1) but LCS chroma vectors have
norms ~40-46, so color_temperature=0.5 only shifted chroma by 1.2%.
Scale warm_dir by mean anchor chroma magnitude so the shift is
proportional. Slider range narrowed from ±2.0 to ±1.0 and preset
values adjusted accordingly (Warm=0.15, Cool=-0.15, Cinematic=0.05).
2026-03-17 05:05:21 +08:00
facok 78be03db46 Add frontend preset sync for LCSToneAdjust sliders
Selecting a preset now updates slider values in real-time via a
frontend JS extension, and users can tweak values after selecting.
Sliders are always visible (removed optional=True). Preset values
live only in JS (single source of truth); Python keeps just the
name list for the combo widget. Sync only fires on explicit user
dropdown interaction, preserving saved slider tweaks on workflow load.
2026-03-17 04:47:57 +08:00
facok 51becf21e4 Use epsilon comparison for tone no-op check and precompute warm_dir
Float equality (!=) can fail after JS→JSON→Python round-trips on
slider values. Switch to epsilon-based _is_default_tone() helper.
Also precompute the warm/cool direction vector once at closure-build
time instead of recomputing it on every denoising step.
2026-03-17 03:47:44 +08:00
facok 56699625e0 Rename LCSContrastAdjust to LCSToneAdjust and add tone presets
The node now controls contrast, brightness, saturation, and color
temperature — "Contrast Adjust" was misleading. Adds 9 one-click
tonal presets (Cinematic, HDR, Vivid, Dramatic, Low Key, High Key,
Warm, Cool, Desaturated) plus Custom mode for manual slider control.
2026-03-17 03:43:10 +08:00
facok 01f5bd308c Add color temperature control to LCSContrastAdjust node
Shifts chroma along the warm (Red+Yellow) ↔ cool (Blue+Cyan) axis
via additive offset before saturation scaling, so saturation=0 still
yields grayscale. Warm direction is derived from chromatic projections
of the 6 hue anchors (stripping their lightness component).
2026-03-17 03:30:30 +08:00
facok ef69e2f9e7 Remove incorrect clamp on LCS lightness in contrast hook
l_scalar (lightness projection onto achromatic axis) can naturally
extend beyond [0,1] during denoising. Clamping destroys highlight/shadow
detail — directly contradicting the purpose of contrast adjustment.
Type I color intervention already operates without clamping LCS coords.
The no-op skip in execute() ensures default params have zero effect.
2026-03-17 03:25:03 +08:00
facok 29dc6379f9 Skip contrast hook when all parameters are at defaults
Avoids clamp and float round-trip artifacts that caused visible
changes even with contrast=1.0, brightness=0.0, saturation=1.0.
2026-03-17 03:09:14 +08:00
facok 600252d5d4 Fix non-contiguous tensor error when saving calibration data
PCA basis from torch.linalg.eigh produces non-contiguous tensors,
which safetensors rejects. Add .contiguous() to all tensors at save time.
2026-03-17 02:55:11 +08:00
facok b4e8dbd3d7 Add LCSContrastAdjust node for contrast, brightness, and saturation control
Operates directly in 3D LCS space by decomposing patches into lightness
(projection onto black→white achromatic axis) and chroma (perpendicular
residual). No HSL round-trip needed, making it faster than the color
intervention path.
2026-03-17 02:49:40 +08:00
facok 0973fd0fd8 Add docstrings to all public functions, classes, and methods
Coverage: 29% → 100% (41/41 public items documented).
Keeps the existing concise style with inline shape annotations.
2026-03-17 02:26:21 +08:00
facok 534bc19033 Implement LCS plugin for training-free color control in FLUX
Implements "The Latent Color Subspace" (arXiv:2603.12261v1, ICML 2026)
as a ComfyUI plugin with V3 API + V2 backward compatibility.

6 nodes across 3 categories:
- LCS/calibration: LCSCalibrate (PCA from VAE), LCSLoadData (cache load)
- LCS/intervention: LCSColorIntervene (single color), LCSColorBatch (multi)
- LCS/observe: LCSPreviewColors (latent color map), LCSStepObserver

Core modules: patchify (16ch->64d patches), bicone HSL<->LCS mapping,
alpha_t/beta_t tables (51 entries from Appendix F), PCA calibration,
and timestep normalization with residual preservation.
2026-03-17 01:43:26 +08:00