Draft: density-space Vibrance/anti-vibrance + spatially-modulated Dye Mute (extends #667) - #675
Conversation
… Dye Mute Squashed from 6 exploratory commits for a clean rebase base. Still experimental/pre-PR — built against the pre-marcinz606#664/marcinz606#667 codebase and needs reconciling against both before this is presentable. Contains: - ExposureConfig.density_vibrance (signed, 0=off): per-pixel, inside _apply_print_curve_kernel, right after the dye_mix block. Positive boosts muted pixels (classic Vibrance), negative compresses vivid pixels (real anti-vibrance, no Lab equivalent). Mask *target* flips with sign, not just the formula's sign. - Per-pixel-modulated Dye Mute (variant b): spread = max(e0,e1,e2) - min(e0,e1,e2) drives a sigmoid mask so damping concentrates on already-separated pixels, leaving near-neutral pixels untouched regardless of frame-wide grade. - CPU + GPU parity for both, verified within this project's established tolerance (mean<0.01, max<0.04). - Divergence-based Dye Mute reference signal prototype: built, tested, found redundant with density_saturation itself, removed. Not yet reconciled with marcinz606#667 (maintainer's own Dye Mute-in-density-space follow-up, which shipped a global slope_g-based damping term this per-pixel work needs to either replace or layer with). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-0.5 was a fairly aggressive anti-vibrance push for a quick-compare demo. Lab has no real equivalent to match against for the compress direction anyway (its sub-1.0 branch desaturates the muted population, not vivid pixels), so 0.33 stays as a fixed reference point rather than a matched value. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same class of gap marcinz606#667 already fixed for Print Saturation: the processor already forces density_vibrance/density_damping_spatial inert in B&W (get_luminance collapses to one channel before either ever runs), but the sliders and the Vibrance A/B row stayed visible and interactive anyway. Lab's own Vibrance slider already hides in B&W (sidebar/lab.py), so the A/B row comparing Density vs Lab Vibrance -- both color-only -- follows the same convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Light pass appropriate to a draft/exploratory PR, not a full test suite: - tests/test_density_vibrance.py: identity/no-op, boost changes output, compress changes output, boost/compress aren't a simple sign flip (different target populations), B&W inertness -- for both density_vibrance and density_damping_spatial. Also encodes the actual proof that density_damping_spatial=False is bit-exact with manually pre-multiplying density_saturation by grade_saturation_damping (marcinz606#667's own uniform path, untouched by default). - tests/test_gpu_curve_parity.py: CPU/GPU parity for density_vibrance (both directions) and density_damping_spatial -- neither was covered by any existing test; test_cpu_gpu_match_dye_mute only covers marcinz606#667's uniform (non-spatial) damping. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
I played around and I like the effect, i think it should replace the dye mute. But for consistency/simplification I would maybe name it "Dye Separation" instead of "density vibrance". I can make a changes on top of your branch |
|
Also I think i will drop vibrance from lab panel completely after that |
|
Stoked to hear that you will include it :) Yeah, after having played around with the density vibrance and density saturation in tandem the last few days I also really started liking it. feels like a quite powerful but subtle tool for getting the saturation you want. And looks natural. |
|
No objections on calling it Dye separation:) |
|
So go ahead and make the chances you want. The only thing that could be explored more imo is if some other value than 0.4 gives a better result. But that can be done over a time period as well. Don't need to land that right away. |
|
created new branch/pr on top of your commits here #676, i will close this one :) |
|
I'm playing with spread scale and i think smaller values work better, with current 0.4 behaviour is pretty close to print saturation |
|
something in 0.1-0.2 range seems way more selective |
|
I was thinking something a bit smaller might be better too:)
ons. 29. juli 2026 kl. 20:14 skrev marcinz606 ***@***.***>:
… *marcinz606* left a comment (marcinz606/NegPy#675)
<#675 (comment)>
something in 0.1-0.2 range seems way more selective
—
Reply to this email directly, view it on GitHub
<#675?email_source=notifications&email_token=B4YPVFMYIZPWKKWMG2E3QWL5HI5J3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGE3TSMZRGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5121793142>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/B4YPVFKRXS2V4VO6XPLTETL5HI5J3AVCNFSNUABGKJSXA33TNF2G64TZHMYTCMRTG43DMNRQHE5US43TOVSTWNJQGA4TINBTHEYTRILWAI>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/B4YPVFMWOT2YFFXYU4VHL6L5HI5J3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGE3TSMZRGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/B4YPVFKF6C4JJPAWCMYFKYL5HI5J3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGE3TSMZRGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
0.4 seemed a bit heavy handed
ons. 29. juli 2026 kl. 20:17 skrev Mats Fagerberg ***@***.***>:
… I was thinking something a bit smaller might be better too:)
ons. 29. juli 2026 kl. 20:14 skrev marcinz606 ***@***.***>:
> *marcinz606* left a comment (marcinz606/NegPy#675)
> <#675 (comment)>
>
> something in 0.1-0.2 range seems way more selective
>
> —
> Reply to this email directly, view it on GitHub
> <#675?email_source=notifications&email_token=B4YPVFMYIZPWKKWMG2E3QWL5HI5J3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGE3TSMZRGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5121793142>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/B4YPVFKRXS2V4VO6XPLTETL5HI5J3AVCNFSNUABGKJSXA33TNF2G64TZHMYTCMRTG43DMNRQHE5US43TOVSTWNJQGA4TINBTHEYTRILWAI>
> .
> Triage notifications, keep track of coding agent tasks and review pull
> requests on the go with GitHub Mobile for iOS
> <https://github.com/notifications/mobile/ios/B4YPVFMWOT2YFFXYU4VHL6L5HI5J3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGE3TSMZRGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
> and Android
> <https://github.com/notifications/mobile/android/B4YPVFKF6C4JJPAWCMYFKYL5HI5J3A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGE3TSMZRGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
> Download it today!
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>
|
|
i synced with main now. Dye separation seems make almost no visible change between 0 and 1? Going the other direction works well though. 0 to -1 |
|
like the histogram changes but i barely see any visual change |
|
i reduced the k 0.4 -> 0.12, might need some more tweaking |
|
now the positive effect more heavily depends on negative |
|
but I agree it's weird that negative effect is more pronounced, i havent changed anything other than the 0.4 const |
|
I'm heading out for a bit so will look more later. But yeah, on the test negative going in the positive direction made almost no difference |
|
might be possible to use different k number on the two directions as well, if that works better idk? |
|
Yes, that's what I'm considering, different k depending on direction |
|
After playing a bit more I'm thinking that when effect is made stronger again it's almost indistinguishable from print saturation, formulas are almost the same after all. I'm considering consolidating it into one slider to make things simpler |
|
Yeah, it’s quite subtle. Or that was my original idea. Something that would
let you push or pull saturated colors in a way that would blend in and look
natural. But if you decide too only keep one tool I think the print
saturation is the purer tool.
ons. 29. juli 2026 kl. 21:54 skrev marcinz606 ***@***.***>:
… *marcinz606* left a comment (marcinz606/NegPy#675)
<#675 (comment)>
After playing a bit more I'm thinking that when effect is made stronger
again it's almost indistinguishable from print saturation, formulas are
almost the same after all. I'm considering consolidating it into one slider
to make things simpler
—
Reply to this email directly, view it on GitHub
<#675?email_source=notifications&email_token=B4YPVFINCKIHDK5WCXUWPC35HJI7PA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGI4DANJXGM22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5122805735>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/B4YPVFNTXNM22UL6EKIY5BL5HJI7PAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMRTG43DMNRQHE5US43TOVSTWNJQGA4TINBTHEYTRILWAI>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/B4YPVFKCS7TCWD5H2J5WVYL5HJI7PA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGI4DANJXGM22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
and Android
<https://github.com/notifications/mobile/android/B4YPVFIYZANEVS7I7DSAKTD5HJI7PA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGI4DANJXGM22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
like if you want to push the saturation on an image with print sat, but
then there is one strong color like red that goes too far. Then you can get
that red back in line with the rest by compensating with the anti-vibrance.
That is how I was using it under testing the last few days.
ons. 29. juli 2026 kl. 22:12 skrev Mats Fagerberg ***@***.***>:
… Yeah, it’s quite subtle. Or that was my original idea. Something that
would let you push or pull saturated colors in a way that would blend in
and look natural. But if you decide too only keep one tool I think the
print saturation is the purer tool.
ons. 29. juli 2026 kl. 21:54 skrev marcinz606 ***@***.***>:
> *marcinz606* left a comment (marcinz606/NegPy#675)
> <#675 (comment)>
>
> After playing a bit more I'm thinking that when effect is made stronger
> again it's almost indistinguishable from print saturation, formulas are
> almost the same after all. I'm considering consolidating it into one slider
> to make things simpler
>
> —
> Reply to this email directly, view it on GitHub
> <#675?email_source=notifications&email_token=B4YPVFINCKIHDK5WCXUWPC35HJI7PA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGI4DANJXGM22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5122805735>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/B4YPVFNTXNM22UL6EKIY5BL5HJI7PAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMRTG43DMNRQHE5US43TOVSTWNJQGA4TINBTHEYTRILWAI>
> .
> Triage notifications, keep track of coding agent tasks and review pull
> requests on the go with GitHub Mobile for iOS
> <https://github.com/notifications/mobile/ios/B4YPVFKCS7TCWD5H2J5WVYL5HJI7PA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGI4DANJXGM22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG>
> and Android
> <https://github.com/notifications/mobile/android/B4YPVFIYZANEVS7I7DSAKTD5HJI7PA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMJSGI4DANJXGM22M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>.
> Download it today!
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>
|
|
I've made a hybrid now, one 'Dye Saturation' slider but you can dial in the "k" using separate slider. Gives a lot of flexibility |
|
Yes, I just noticed:) was looking over the docs now. Will be interesting to
try out.
|
|
Seems to work well and is flexible as you say. It reacts a bit differently on different frames, so I have to recalibrate my brain:) |
|
I'm convinced. Gives you very fine saturation control over the whole image. |
|
Since Chroma is still in the lab panel I made some changes to it so it behaves a bit better. |
|
Test with some skin tones while boosting chroma for best a/b comparison |
Draft: density-space Vibrance/anti-vibrance + spatially-modulated Dye Mute (extends #667)
What
Two related, additive controls, both off by default:
ExposureConfig.density_vibrance).ExposureConfig.density_damping_spatial), using the same masking mechanism as anti-vibrance.This is a draft — I'm looking for your read on whether the direction is worth continuing, not asking you to merge as-is. I already had some A/B testing built in that I decided to keep as it might make your testing easier. Happy to close and keep iterating if it doesn't look right, or rip out some of the A/B features.
Why
#667moved Dye Mute into density space as a single frame-wide scalar, grade-coupled viaslope_g. That fixed the domain problem (density vs. post-decode Lab), but the correction is still uniform — it can't distinguish a near-neutral pixel from an already-separated one.Anti-vibrance needed the same distinction (compress already-vivid pixels, leave muted ones alone), so one per-pixel masking mechanism ended up answering both:
Same math, two applications — not three separate ideas.
What each control does
Dye Mute — unchanged:
#667exactly as shipped, one flat(slope_min/slope_g)^strengthdamp applied uniformly, grade-coupled. This PR doesn't touch its default behavior.Spatial Damping (new toggle, off by default) — Dye Mute run through anti-vibrance's math instead of the uniform formula. Same strength value, same field, redistributed per-pixel via
d_k = 1 + (damp_global - 1) * mask— the same shape as the anti-vibrance formula below, just fed by Dye Mute's strength.#667(verified, see Testing).Doesn't touch the Vibrance slider or Print Saturation — a second application of the masking trick, not a shared control.
Density Vibrance (new slider,
-1.0to1.0,0= off):The mask targets a genuinely different population depending on sign — not just a negated output.
A/B row (
Off / Density +0.5 / Lab 1.50 / Density -0.3 / Lab 0.33) — left over from my own testing against Lab Vibrance, not built specifically for review, but there if useful. The boost pair is a fair comparison (both target muted pixels, matched by chroma effect). The compress pair is magnitude-matched only — Lab's 0.33 desaturates muted pixels further, Density's -0.3 compresses vivid pixels, so they'll look different even at "equal" strength.Shared constant to flag: all three per-pixel behaviors — Vibrance boost, anti-vibrance, Spatial Damping — share one tunable,
vibrance_spread_scale = 0.4(exposure/models.py), controlling how much per-pixel separation counts as "vivid enough" to affect. It's an unvalidated starting guess. Tuning it for one effect will retune all three at once — that's the main risk of the unified-mask approach, and worth deciding now whether that coupling is acceptable or whether it needs decoupling before this ships.Honest note on the boost direction
Density Vibrance's boost direction converged with Lab Vibrance at realistic settings in my testing, only diverging at extremes — I can't confidently say one is better. Including it anyway since it costs nothing and the A/B row makes it checkable, but the real case for this PR is the compress direction, which Lab has no equivalent of at all.
Replace vs. layer
Spatial Damping replaces the uniform
grade_saturation_dampingpre-multiply when it's on, rather than layering on top. I lean toward replace for three reasons: your own#667PR already diagnosed the failure mode of uncoordinated stacking (Lab Saturation vs. old Dye Mute fighting each other); and two of my own earlier prototypes (a global chroma-damping signal, then a per-channel-divergence version — both dropped, seeDROPPED_KEYS) turned out mathematically redundant withdensity_saturationonce measured. Flagging this as a decision point rather than deciding it unilaterally — layer is a reasonable position if you'd rather.Risk / compatibility
Both fields default to off/identity (
density_vibrance=0.0,density_damping_spatial=False) — can't affect any existing saved edit. With Spatial Damping off, Dye Mute is unchanged from#667, verified bit-exact (test_off_by_default_matches_uniform_damping_bit_exact).Implemented on CPU and GPU, mirrored in the WGSL shader — no CPU-only fallback.
Testing
tests/test_density_vibrance.py— identity/no-op, boost and compress each change output and aren't a simple sign flip of each other, B&W inertness, bit-exact proof Spatial Damping off matches#667's uniform path.tests/test_gpu_curve_parity.py— new CPU/GPU parity for Density Vibrance (both directions) and Spatial Damping — neither covered by the existingtest_cpu_gpu_match_dye_mute, which only exercises the uniform path.