mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-20 02:04: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
|
// 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.
|
// calibrating it. A profile without a vcgt table therefore has no ramp.
|
||||||
func GenerateGammaRamp(size uint32, profile *Profile) (GammaRamp, error) {
|
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 {
|
if size < 2 {
|
||||||
return GammaRamp{}, errors.New("icc: ramp size must be at least 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
|
vcgt := profile.VCGT
|
||||||
for i := uint32(0); i < size; i++ {
|
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.Red[i] = resampleVCGT(vcgt.Red, t)
|
||||||
ramp.Green[i] = resampleVCGT(vcgt.Green, t)
|
ramp.Green[i] = resampleVCGT(vcgt.Green, t)
|
||||||
ramp.Blue[i] = resampleVCGT(vcgt.Blue, t)
|
ramp.Blue[i] = resampleVCGT(vcgt.Blue, t)
|
||||||
|
|||||||
@@ -169,38 +169,51 @@ func GenerateIdentityRamp(size uint32) GammaRamp {
|
|||||||
return ramp
|
return ramp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfileRampWithTemp builds the gamma ramp for an output that has an ICC
|
// ProfileRampWithTemp builds the ramp for an output with an ICC profile: the
|
||||||
// profile. The profile describes the display at its reference white point
|
// vcgt table is sampled at the contrast-adjusted input, the user gamma is
|
||||||
// (baseTemp), so a temperature target is composed as the ratio between the
|
// applied to the result, and a temperature target (targetTemp > 0) is composed
|
||||||
// target ramp and the base ramp. targetTemp <= 0 means "no target", which
|
// as the white point ratio against baseTemp, the profile's reference white.
|
||||||
// leaves the profile as measured.
|
|
||||||
func ProfileRampWithTemp(size uint32, profile *icc.Profile, baseTemp, targetTemp int, gamma, contrast float64) (GammaRamp, error) {
|
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 {
|
if err != nil {
|
||||||
return GammaRamp{}, err
|
return GammaRamp{}, err
|
||||||
}
|
}
|
||||||
ramp := GammaRamp{Red: iccRamp.Red, Green: iccRamp.Green, Blue: iccRamp.Blue}
|
|
||||||
if targetTemp <= 0 || targetTemp == baseTemp {
|
tint := whitepointRatio(baseTemp, targetTemp)
|
||||||
return ramp, nil
|
ramp := GammaRamp{
|
||||||
|
Red: make([]uint16, size),
|
||||||
|
Green: make([]uint16, size),
|
||||||
|
Blue: make([]uint16, size),
|
||||||
}
|
}
|
||||||
|
for i := range size {
|
||||||
base := GenerateGammaRamp(size, baseTemp, gamma, contrast)
|
ramp.Red[i] = adjustRampValue(iccRamp.Red[i], tint.r, gamma)
|
||||||
target := GenerateGammaRamp(size, targetTemp, gamma, contrast)
|
ramp.Green[i] = adjustRampValue(iccRamp.Green[i], tint.g, gamma)
|
||||||
|
ramp.Blue[i] = adjustRampValue(iccRamp.Blue[i], tint.b, gamma)
|
||||||
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])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ramp, nil
|
return ramp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// scaleRampValue rescales a profile ramp value by the ratio of the target and
|
func whitepointRatio(baseTemp, targetTemp int) rgb {
|
||||||
// base temperature ramps at the same index.
|
if targetTemp <= 0 || targetTemp == baseTemp {
|
||||||
func scaleRampValue(value, base, target uint16) uint16 {
|
return rgb{r: 1, g: 1, b: 1}
|
||||||
if base == 0 {
|
}
|
||||||
return value
|
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