fix(algo): HDR Toning gamma follows its documented direction (#710) (#801)

The field documents "1 = neutral; lower = more contrast", but the tone
map used gamma directly as the powf exponent, so 0.5 lightened
midtones and 2.0 darkened them - the slider ran opposite to its own
contract. The crate's other two gamma controls (hdr.rs, proof_sim.rs)
already use the reciprocal exponent.

Apply 1.0 / gamma like they do. Neutral (1.0) is unchanged; every
other value now moves midtones the documented way.

Adds a regression test on a flat 50% grey: gamma 0.5 darkens the
midtone, gamma 2.0 lightens it (reversed on main: 0.71 / 0.25).

Co-authored-by: Brandon Fenty <bfenty@users.noreply.github.com>
This commit is contained in:
bfenty 2026-10-07 16:46:04 -05:00 committed by GitHub
parent 080f2c4c8d
commit 385e8979de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 1 deletions

View File

@ -343,7 +343,9 @@ pub fn hdr_toning(px: &mut [[f32; 4]], w: usize, h: usize, p: &HdrToning) {
let c = [q[0], q[1], q[2]];
let l0 = luma(c).max(0.0) + EPS;
let new_log = mean + (base[i] - mean) * compress + (logl[i] - base[i]) * detail;
let mut l1 = (new_log.exp() * exposure).max(0.0).powf(gamma);
// The contract is "lower = more contrast", i.e. gamma is the divisor of
// the exponent, matching the crate's other gamma controls (hdr.rs, proof_sim.rs).
let mut l1 = (new_log.exp() * exposure).max(0.0).powf(1.0 / gamma);
l1 += p.shadow / 100.0 * (1.0 - l1).powi(3) * 0.5 - p.highlight / 100.0 * l1.powi(3) * 0.5;
l1 = curve_eval(&sorted, l1.clamp(0.0, 1.0));
let ratio = l1 / l0;

View File

@ -0,0 +1,29 @@
//! Regression tests for issue #710: HdrToning gamma is documented as
//! "1 = neutral; lower = more contrast", so a gamma below 1 must darken
//! midtones (like the crate's other gamma controls, which use 1/gamma as the
//! exponent). On main the value was used directly as the exponent, so the
//! slider ran opposite to its contract.
use photocraft_engine::Session;
use serde_json::json;
fn midtone_luma(gamma: f64) -> f64 {
let mut s = Session::new();
s.execute("file.new", json!({"width": 8, "height": 8, "background": "#808080"})).unwrap();
s.execute(
"image.adjustments.hdrToning",
json!({"gamma": gamma, "strength": 0.1, "detail": 0, "saturation": 0, "radius": 30}),
)
.unwrap();
let p: Vec<f32> = serde_json::from_value(s.execute("document.pixel", json!({"x": 4, "y": 4})).unwrap()).unwrap();
f64::from(0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2])
}
#[test]
fn hdr_toning_gamma_lower_darkens_midtones() {
let base = midtone_luma(1.0);
let low = midtone_luma(0.5);
let high = midtone_luma(2.0);
// "lower = more contrast" on a mid grey means the midtone moves down.
assert!(low < base, "gamma 0.5 must darken the midtone: base={base} low={low}");
assert!(high > base, "gamma 2.0 must lighten the midtone: base={base} high={high}");
}