84 Commits
Author SHA1 Message Date
facok 20455e339e Fix sharpness calibration vector count 2026-06-28 01:21:38 +08:00
facok b554f9722a Update pyproject.toml 2026-04-25 20:25:23 +08:00
facok daa4127efc Add Comfy Registry publishing 2026-04-25 20:08:33 +08:00
facok 44812b79a6 Clean up anchor and calibration code after review
- Remove dead detect_anomalies() (superseded by adaptive variant)
- Remove unused r_ref param from infer_color_from_neighbors()
- Remove write-only c_ema state from anchor hook
- Use math.exp() instead of torch.tensor()+torch.exp() in bilateral loop
- Use in-place .add_() for accumulation in bilateral filter
- Pre-normalize padded tensor once in compute_local_relationships()
- Fix video VAE fallback producing duplicate vectors in calibration
2026-03-23 16:33:06 +08:00
facok 561f6ed041 Fix calibration crash with video VAEs that return 5D latents
vae.encode() on some VAEs returns [B, C, T, H, W] even for still
images. Squeeze temporal dim before patchify to avoid shape mismatch.
2026-03-23 16:24:48 +08:00
facok e9b7d7481d Add missing core modules required by LCSColorAnchor
core/bilateral.py and core/relationships.py were referenced by
nodes/anchor.py but never committed, causing ModuleNotFoundError
on fresh clones.
2026-03-23 16:11:27 +08:00
facok 25e7d953d5 Register sys.modules alias so other plugins can import ComfyUI_LCS
ComfyUI loads custom_nodes by directory name (ComfyUI-LCS) but Python
cannot import module names with hyphens. Register ComfyUI_LCS as a
sys.modules alias so SubspaceLab and other plugins can use
`from ComfyUI_LCS.core.xxx import ...` regardless of load order.
2026-03-23 03:01:19 +08:00
facok 8656e48f9f Add LCSColorAnchor to V3 and V2 node registrations
The anchor node was implemented but missing from __init__.py registrations.
Also removes probe node references (exposure, spatial variance, chroma AC)
which have been moved to ComfyUI-SubspaceLab.
2026-03-23 02:07:19 +08:00
facok 85e437c794 Add .gitignore: exclude .claude/ and __pycache__/ 2026-03-22 21:55:23 +08:00
facok e7a836db4d Fix color/brightness side effects in sharpness intervention
Two bugs caused sharpness edits to shift color even when calibrated
with lcs_data:

1. Calibration order was wrong: DC removal before LCS color removal
   operated in the wrong space (LCS basis has large DC components),
   producing incorrect color projection and incomplete removal.
   Fix: remove color first (in raw space), then DC.

2. Intervention DC removal broke orthogonality: subtracting
   mean(pc1_dir) introduced a color component because the all-ones
   vector projects onto LCS basis. Fix: re-orthogonalize against
   LCS basis after DC removal.

Also: store lcs_basis in SharpnessData/safetensors so intervention
can re-orthogonalize; bump cache name to _grating to force
re-calibration with the corrected pipeline.
2026-03-22 20:57:08 +08:00
facok 818ec85429 Replace Gaussian blur calibration with sinusoidal grating stimuli
Grating stimuli achieve higher linearity (R²=0.94 vs 0.88) because each
contains a single spatial frequency — a purer probe of the sharpness axis.
Both methods discover the same 1D subspace (|cos|=0.986, 9.7° apart).

- Replace _gaussian_kernel_1d, _apply_gaussian_blur with _generate_grating_batch
- Generate stimuli lazily per batch to avoid 1.3GB all-at-once allocation
- Accept deprecated blur_levels parameter with warning
- Update cache filename to sharpness_{fp}_freq{suffix}.safetensors
- Update node descriptions to reflect new calibration method
2026-03-22 20:17:27 +08:00
facok 530832cf90 Add changelog to both READMEs 2026-03-22 16:05:10 +08:00
facok 2261e37b47 Fix Color Anchor self_anchor for img2img (no observe phase)
When img2img denoise is low, all sigmas have low amplification so
self_anchor gets zero observe steps. This caused three issues:

1. Auto-intensity fallback hardcoded drift_signal=0.2, mapping to
   maximum intensity 0.6 — excessive for img2img. Now measures actual
   drift from the seeded baseline, or uses conservative 0.05 default.

2. EMA seed step returned early, wasting the first correction step
   and leaving correction_index unincremented. Now falls through to
   anomaly detection which naturally produces zero correction when
   comparing EMA against itself.

3. The wasted seed step caused an envelope off-by-one (envelope[0]
   = sin(0) = 0 on the real first correction). Resolved by fix 2.
2026-03-22 04:56:25 +08:00
facok 9e85d747d7 Document LCS Color Anchor node in both READMEs
Add feature entry, quick start guide, node table row, detailed
parameter/mode descriptions, and "How It Works" section for the
Color Anchor node. Also update the file structure listing with
new core and node files. Both EN and ZH versions in plain language.
2026-03-22 04:36:50 +08:00
facok cf2410bb76 Eliminate duplicate computation in auto-intensity measurement
Hoist bilateral filter and reference resize before the auto-intensity
block so the result is computed once and reused for both drift
measurement and correction. Replace drift_samples list with running
sum/count to avoid unbounded accumulation.
2026-03-22 03:35:40 +08:00
facok bab475edc0 Add auto mode with drift-driven intensity to LCS Color Anchor
Default mode is now "auto", which infers the concrete mode from
connected inputs (reference+vae → reference, mask → smooth,
nothing → self_anchor) and derives intensity from runtime drift
signals instead of requiring manual tuning.
2026-03-22 03:29:27 +08:00
facok ef74e1d5c9 Update tested models: add LTX2.3 2026-03-21 23:05:33 +08:00
facok 858f0747c4 Extract shared downsample_mask helper to core/sampling.py
Deduplicate mask downsampling logic that was copied in 3 places
(intervene.py color hook, intervene.py tone hook, sharpen.py).
2026-03-21 22:56:59 +08:00
facok 0d9b685c2f Clean up: remove unused pack_info fields and legacy constants 2026-03-21 22:17:42 +08:00
facok cc5a8cbd6f Pad odd spatial dims in patchify for LTXV compatibility
LTXV can produce latents with odd H or W (e.g. 11x20) which can't be
divided into 2x2 patches. Patchify now pads odd dims to even using
replicate padding before rearranging, and unpatchify crops back to the
original size. This keeps patch_size=2 consistent between calibration
and inference for all models.
2026-03-21 21:39:46 +08:00
facok ec9941b5fc Use model's latent_format for space conversion instead of hardcoded FLUX constants
Replaces hardcoded SCALE_FACTOR/SHIFT_FACTOR with model.latent_format.process_in/out
so LCS works with any model (FLUX, LTXV, SD, etc). Adds LTXAV packed tensor
unpack/repack support for audio+video models. All hooks (color, tone, sharpness,
observer) now use the model-aware conversion from args["model"].
2026-03-21 21:30:22 +08:00
facok 4948395ac4 Skip intervention gracefully for incompatible latent formats (LTXAV)
LTXAV uses a flattened 1D latent layout (e.g. [1, 1, 466048]) where
spatial H/W < 2, making 2x2 patchification impossible. patchify() now
returns None for such formats, and all hooks skip intervention cleanly.
2026-03-21 20:40:53 +08:00
facok 5154bae137 Handle 3D tensors (LTXAV) in patchify/unpatchify 2026-03-21 20:34:40 +08:00
facok fefe0ad007 Add Wan (qwen-image) to tested models 2026-03-21 16:27:54 +08:00
facok 0d82606fb0 Support 5D video VAE latents (Wan) in patchify/unpatchify
Video VAEs output [B, C, T, H, W]. Patchify now merges T into batch,
processes all frames, and unpatchify restores the 5D shape.
Updated all callers to pass extra_shape through.
2026-03-21 16:19:03 +08:00
facok a336d4ca95 Fall back to per-image VAE encoding for video VAEs (Wan)
Video VAEs like Wan don't support batch encoding — input B images
produces only 1 latent. Detect this by comparing output batch size
to input, and fall back to encoding images one by one.
Fixes both color calibration and sharpness calibration.
2026-03-21 16:10:13 +08:00
facok 53b2c4cadb Fix blur_labels count to match actual VAE output size 2026-03-21 15:41:14 +08:00
facok bab5d1b1ec Fix calibration batch indexing for VAEs that don't preserve batch size 2026-03-21 15:36:39 +08:00
facok 8c1d987b4f Fix patchify for video VAEs: slice first frame when ndim > 4 2026-03-21 15:33:32 +08:00
facok 6dd89e55e7 Handle 5D video VAE output (Wan) by squeezing temporal dimension 2026-03-21 15:21:51 +08:00
facok cf7c10695c Update README_zh.md 2026-03-21 15:10:57 +08:00
facok 11c9aa875e Update README.md 2026-03-21 15:10:26 +08:00
facok b3b08761c3 Auto-detect channel count in unpatchify for FLUX2 (128ch) support 2026-03-21 14:57:52 +08:00
facok d6ee472d66 Add tip for distilled models: start intervention from step 0 2026-03-20 19:01:20 +08:00
facok 516f9a1093 Update README.md 2026-03-20 18:13:59 +08:00
facok 8b1a92ce46 Update README_zh.md 2026-03-20 18:13:39 +08:00
facok c9e7d608a8 Simplify sharpness: precompute edit vector, remove dead pc1_std, reduce memory
- Precompute edit_vec = (strength * sign) * dc_removed_pc1_dir once in
  closure instead of recomputing pc1_dir mean removal every denoising step
- Remove SharpnessData.pc1_std (stored but never used since dropping
  pc1_std multiplier)
- Blur single-channel gray noise, expand to 3-channel per-batch instead
  of allocating full 201 MB 3-channel copy upfront
- Remove unused LCSData import
2026-03-20 17:25:49 +08:00
facok fad2f87fcc Update Chinese README with sharpness control documentation 2026-03-20 17:11:43 +08:00
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