mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
fix(icc): apply the user gamma and contrast on profiled outputs
This commit is contained in:
@@ -544,6 +544,13 @@ func SampleCurve(curve Curve, t float64) float64 {
|
||||
// the ramp, so treating them as one would wash the display out instead of
|
||||
// calibrating it. A profile without a vcgt table therefore has no ramp.
|
||||
func GenerateGammaRamp(size uint32, profile *Profile) (GammaRamp, error) {
|
||||
return GenerateGammaRampAt(size, profile, func(t float64) float64 { return t })
|
||||
}
|
||||
|
||||
// GenerateGammaRampAt samples the vcgt table at input(t) for each ramp
|
||||
// position t in [0, 1], so a caller can reshape the ramp input (contrast)
|
||||
// before the table is applied.
|
||||
func GenerateGammaRampAt(size uint32, profile *Profile, input func(float64) float64) (GammaRamp, error) {
|
||||
if size < 2 {
|
||||
return GammaRamp{}, errors.New("icc: ramp size must be at least 2")
|
||||
}
|
||||
@@ -559,7 +566,7 @@ func GenerateGammaRamp(size uint32, profile *Profile) (GammaRamp, error) {
|
||||
|
||||
vcgt := profile.VCGT
|
||||
for i := uint32(0); i < size; i++ {
|
||||
t := float64(i) / float64(size-1)
|
||||
t := input(float64(i) / float64(size-1))
|
||||
ramp.Red[i] = resampleVCGT(vcgt.Red, t)
|
||||
ramp.Green[i] = resampleVCGT(vcgt.Green, t)
|
||||
ramp.Blue[i] = resampleVCGT(vcgt.Blue, t)
|
||||
|
||||
@@ -169,38 +169,51 @@ func GenerateIdentityRamp(size uint32) GammaRamp {
|
||||
return ramp
|
||||
}
|
||||
|
||||
// ProfileRampWithTemp builds the gamma ramp for an output that has an ICC
|
||||
// profile. The profile describes the display at its reference white point
|
||||
// (baseTemp), so a temperature target is composed as the ratio between the
|
||||
// target ramp and the base ramp. targetTemp <= 0 means "no target", which
|
||||
// leaves the profile as measured.
|
||||
// ProfileRampWithTemp builds the ramp for an output with an ICC profile: the
|
||||
// vcgt table is sampled at the contrast-adjusted input, the user gamma is
|
||||
// applied to the result, and a temperature target (targetTemp > 0) is composed
|
||||
// as the white point ratio against baseTemp, the profile's reference white.
|
||||
func ProfileRampWithTemp(size uint32, profile *icc.Profile, baseTemp, targetTemp int, gamma, contrast float64) (GammaRamp, error) {
|
||||
iccRamp, err := icc.GenerateGammaRamp(size, profile)
|
||||
iccRamp, err := icc.GenerateGammaRampAt(size, profile, func(t float64) float64 { return applyContrast(t, contrast) })
|
||||
if err != nil {
|
||||
return GammaRamp{}, err
|
||||
}
|
||||
ramp := GammaRamp{Red: iccRamp.Red, Green: iccRamp.Green, Blue: iccRamp.Blue}
|
||||
if targetTemp <= 0 || targetTemp == baseTemp {
|
||||
return ramp, nil
|
||||
|
||||
tint := whitepointRatio(baseTemp, targetTemp)
|
||||
ramp := GammaRamp{
|
||||
Red: make([]uint16, size),
|
||||
Green: make([]uint16, size),
|
||||
Blue: make([]uint16, size),
|
||||
}
|
||||
|
||||
base := GenerateGammaRamp(size, baseTemp, gamma, contrast)
|
||||
target := GenerateGammaRamp(size, targetTemp, gamma, contrast)
|
||||
|
||||
for i := range ramp.Red {
|
||||
ramp.Red[i] = scaleRampValue(ramp.Red[i], base.Red[i], target.Red[i])
|
||||
ramp.Green[i] = scaleRampValue(ramp.Green[i], base.Green[i], target.Green[i])
|
||||
ramp.Blue[i] = scaleRampValue(ramp.Blue[i], base.Blue[i], target.Blue[i])
|
||||
for i := range size {
|
||||
ramp.Red[i] = adjustRampValue(iccRamp.Red[i], tint.r, gamma)
|
||||
ramp.Green[i] = adjustRampValue(iccRamp.Green[i], tint.g, gamma)
|
||||
ramp.Blue[i] = adjustRampValue(iccRamp.Blue[i], tint.b, gamma)
|
||||
}
|
||||
|
||||
return ramp, nil
|
||||
}
|
||||
|
||||
// scaleRampValue rescales a profile ramp value by the ratio of the target and
|
||||
// base temperature ramps at the same index.
|
||||
func scaleRampValue(value, base, target uint16) uint16 {
|
||||
if base == 0 {
|
||||
return value
|
||||
func whitepointRatio(baseTemp, targetTemp int) rgb {
|
||||
if targetTemp <= 0 || targetTemp == baseTemp {
|
||||
return rgb{r: 1, g: 1, b: 1}
|
||||
}
|
||||
base := calcWhitepoint(baseTemp)
|
||||
target := calcWhitepoint(targetTemp)
|
||||
return rgb{
|
||||
r: channelRatio(target.r, base.r),
|
||||
g: channelRatio(target.g, base.g),
|
||||
b: channelRatio(target.b, base.b),
|
||||
}
|
||||
return uint16(clamp01(float64(value)*float64(target)/float64(base)/65535.0) * 65535.0)
|
||||
}
|
||||
|
||||
func channelRatio(target, base float64) float64 {
|
||||
if base <= 0 {
|
||||
return 1
|
||||
}
|
||||
return target / base
|
||||
}
|
||||
|
||||
func adjustRampValue(value uint16, tint, gamma float64) uint16 {
|
||||
linear := clamp01(float64(value) / 65535.0 * tint)
|
||||
return uint16(math.Round(clamp01(math.Pow(linear, 1.0/gamma)) * 65535.0))
|
||||
}
|
||||
|
||||
@@ -240,3 +240,85 @@ func TestProfileRampWithTemp(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func identityVCGTProfile(entries int) *icc.Profile {
|
||||
table := func() []uint16 {
|
||||
out := make([]uint16, entries)
|
||||
for i := range out {
|
||||
out[i] = uint16(float64(i) / float64(entries-1) * 65535.0)
|
||||
}
|
||||
return out
|
||||
}
|
||||
return &icc.Profile{
|
||||
HasVCGT: true,
|
||||
VCGT: &icc.VCGT{Channels: 3, Entries: entries, Red: table(), Green: table(), Blue: table()},
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileRampWithTempAppliesGammaAndContrast(t *testing.T) {
|
||||
const size = uint32(256)
|
||||
const tolerance = 2
|
||||
profile := identityVCGTProfile(256)
|
||||
|
||||
closeTo := func(t *testing.T, name string, got, want []uint16) {
|
||||
t.Helper()
|
||||
for i := range want {
|
||||
diff := int(got[i]) - int(want[i])
|
||||
if diff < -tolerance || diff > tolerance {
|
||||
t.Fatalf("%s[%d] = %d, want %d (±%d)", name, i, got[i], want[i], tolerance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
target int
|
||||
gamma float64
|
||||
contrast float64
|
||||
}{
|
||||
{"no target, gamma only", noTempTarget, 1.8, 1.0},
|
||||
{"no target, contrast only", noTempTarget, 1.0, 1.4},
|
||||
{"reference target, both", neutralTemp, 0.8, 0.7},
|
||||
{"warm target, both", 4000, 1.5, 1.3},
|
||||
{"cool target, both", 8000, 0.9, 0.6},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ramp, err := ProfileRampWithTemp(size, profile, neutralTemp, tc.target, tc.gamma, tc.contrast)
|
||||
if err != nil {
|
||||
t.Fatalf("ProfileRampWithTemp: %v", err)
|
||||
}
|
||||
plainTemp := tc.target
|
||||
if plainTemp <= 0 {
|
||||
plainTemp = neutralTemp
|
||||
}
|
||||
want := GenerateGammaRamp(size, plainTemp, tc.gamma, tc.contrast)
|
||||
closeTo(t, "red", ramp.Red, want.Red)
|
||||
closeTo(t, "green", ramp.Green, want.Green)
|
||||
closeTo(t, "blue", ramp.Blue, want.Blue)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("gamma changes the profile ramp without a target", func(t *testing.T) {
|
||||
flat, _ := ProfileRampWithTemp(size, profile, neutralTemp, noTempTarget, 1.0, 1.0)
|
||||
raised, _ := ProfileRampWithTemp(size, profile, neutralTemp, noTempTarget, 2.0, 1.0)
|
||||
mid := int(size) / 2
|
||||
if raised.Red[mid] <= flat.Red[mid] {
|
||||
t.Fatalf("gamma 2.0 should lift mid gray: got %d, flat %d", raised.Red[mid], flat.Red[mid])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contrast pivots the profile ramp at mid gray", func(t *testing.T) {
|
||||
const oddSize = uint32(257)
|
||||
flat, _ := ProfileRampWithTemp(oddSize, profile, neutralTemp, noTempTarget, 1.0, 1.0)
|
||||
high, _ := ProfileRampWithTemp(oddSize, profile, neutralTemp, noTempTarget, 1.0, 2.0)
|
||||
mid := int(oddSize) / 2
|
||||
quarter := int(oddSize) / 4
|
||||
if diff := int(high.Green[mid]) - int(flat.Green[mid]); diff < -tolerance || diff > tolerance {
|
||||
t.Fatalf("mid gray should hold under contrast: got %d, flat %d", high.Green[mid], flat.Green[mid])
|
||||
}
|
||||
if high.Green[quarter] >= flat.Green[quarter] {
|
||||
t.Fatalf("quarter gray should drop under contrast 2.0: got %d, flat %d", high.Green[quarter], flat.Green[quarter])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user