From b4890be7c9838173d4d2a2a8605acd9ad77df558 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sun, 23 Aug 2026 13:31:40 +0200 Subject: [PATCH 01/10] Additional saturation points in tone adjustment --- .../ColorToneAdjustOperation.cs | 78 ++++++++++++------- .../Controls/ColorToneControl.xaml.cs | 10 +-- PhotoLocator/LocalContrastViewModel.cs | 3 +- 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index 683b327..522c613 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -6,14 +6,20 @@ namespace PhotoLocator.BitmapOperations { class ColorToneAdjustOperation : OperationBase { - public const int NumberOfTones = 8; + public const int NumberOfHues = 8; + + public const int NumberOfTones = NumberOfHues * 2; + + public const float ToneLowSaturation = 0.4f; + public const float ToneHighSaturation = 0.8f; FloatBitmap? _srcHSI; bool _updateSrcHsi; - public struct ToneAdjustment(float toneHue) + public struct ToneAdjustment(float toneHue, float toneSaturation) { public readonly float ToneHue = toneHue; + public readonly float ToneSaturation = toneSaturation; public float AdjustHue = 0; public float AdjustSaturation = 1; public float AdjustIntensity = 1; @@ -40,8 +46,11 @@ public ColorToneAdjustOperation() public void ResetToneAdjustments() { - for (var i = 0; i < NumberOfTones; i++) - ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfTones); + for (var i = 0; i < NumberOfHues; i++) + { + ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfHues, ToneLowSaturation); + ToneAdjustments[i + NumberOfHues] = new ToneAdjustment((float)i / NumberOfHues, ToneHighSaturation); + } } public bool AreToneAdjustmentsChanged @@ -186,49 +195,66 @@ public override void Apply() int xx = 0; for (var x = 0; x < width; x++) { - var tone = (src[xx] - Rotation) * NumberOfTones; - if (tone < 0) - tone += NumberOfTones; - if (tone >= NumberOfTones) - tone -= NumberOfTones; - var toneIndex = (int)tone; - var nextToneIndex = toneIndex + 1; - if (nextToneIndex == NumberOfTones) - nextToneIndex = 0; - var nextToneWeight = RealMath.SmoothStep(tone - toneIndex); - var toneWeight = 1 - nextToneWeight; + var hueTone = (src[xx] - Rotation) * NumberOfHues; + if (hueTone < 0) + hueTone += NumberOfHues; + if (hueTone >= NumberOfHues) + hueTone -= NumberOfHues; + var hueIndex = (int)hueTone; + var nextHueIndex = hueIndex + 1; + if (nextHueIndex == NumberOfHues) + nextHueIndex = 0; + var nextHueWeight = RealMath.SmoothStep(hueTone - hueIndex); + var hueWeight = 1 - nextHueWeight; + + var saturation = src[xx + 1]; + float saturationWeight, nextSaturationWeight; + if (saturation <= ToneLowSaturation) + { + saturationWeight = 1; nextSaturationWeight = 0; + } + else if (saturation >= ToneHighSaturation) + { + saturationWeight = 0; nextSaturationWeight = 1; + } + else + { + var saturationTone = (saturation - ToneLowSaturation) / (ToneHighSaturation - ToneLowSaturation); + nextSaturationWeight = RealMath.SmoothStep(saturationTone); + saturationWeight = 1 - nextSaturationWeight; + } var hue = src[xx]; - if (toneAdjustments[toneIndex].HueUniformity > 0 || toneAdjustments[nextToneIndex].HueUniformity > 0) + if (toneAdjustments[hueIndex].HueUniformity > 0 || toneAdjustments[nextHueIndex].HueUniformity > 0) { - var toneHue = toneAdjustments[toneIndex].ToneHue + Rotation; + var toneHue = toneAdjustments[hueIndex].ToneHue + Rotation; if (toneHue < hue - 0.5f) toneHue++; else if (toneHue > hue + 0.5f) toneHue--; - var nextToneHue = toneAdjustments[nextToneIndex].ToneHue + Rotation; + var nextToneHue = toneAdjustments[nextHueIndex].ToneHue + Rotation; if (nextToneHue < hue - 0.5f) nextToneHue++; else if (nextToneHue > hue + 0.5f) nextToneHue--; - var toneHueWeight = toneAdjustments[toneIndex].HueUniformity * toneWeight; - var nextToneHueWeight = toneAdjustments[nextToneIndex].HueUniformity * nextToneWeight; + var toneHueWeight = toneAdjustments[hueIndex].HueUniformity * hueWeight; + var nextToneHueWeight = toneAdjustments[nextHueIndex].HueUniformity * nextHueWeight; hue = hue * (1 - toneHueWeight - nextToneHueWeight) + toneHue * toneHueWeight + nextToneHue * nextToneHueWeight; } var h = hue + - toneAdjustments[toneIndex].AdjustHue * toneWeight + - toneAdjustments[nextToneIndex].AdjustHue * nextToneWeight; + (toneAdjustments[hueIndex].AdjustHue * saturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustHue * nextSaturationWeight) * hueWeight + + (toneAdjustments[nextHueIndex].AdjustHue * saturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustHue * nextSaturationWeight) * nextHueWeight; var s = src[xx + 1] * - (toneAdjustments[toneIndex].AdjustSaturation * toneWeight + - toneAdjustments[nextToneIndex].AdjustSaturation * nextToneWeight); + ((toneAdjustments[hueIndex].AdjustSaturation * saturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustSaturation * nextSaturationWeight) * hueWeight + + (toneAdjustments[nextHueIndex].AdjustSaturation * saturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustSaturation * nextSaturationWeight) * nextHueWeight); if (s > 1) s = 1; var i = src[xx + 2] * - (toneAdjustments[toneIndex].AdjustIntensity * toneWeight + - toneAdjustments[nextToneIndex].AdjustIntensity * nextToneWeight); + ((toneAdjustments[hueIndex].AdjustIntensity * saturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustIntensity * nextSaturationWeight) * hueWeight + + (toneAdjustments[nextHueIndex].AdjustIntensity * saturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustIntensity * nextSaturationWeight) * nextHueWeight); ColorTransformHSI2RGB(h, s, i, out dst[xx], out dst[xx + 1], out dst[xx + 2]); xx += 3; } diff --git a/PhotoLocator/Controls/ColorToneControl.xaml.cs b/PhotoLocator/Controls/ColorToneControl.xaml.cs index c1c0bd5..5502446 100644 --- a/PhotoLocator/Controls/ColorToneControl.xaml.cs +++ b/PhotoLocator/Controls/ColorToneControl.xaml.cs @@ -130,8 +130,8 @@ void UpdateToneControlImage() var rotation = _viewModel.ToneRotation; for (int i = 0; i < ToneAdjustments.Length; i++) { - var p1 = HS2XY(ToneAdjustments[i].ToneHue + rotation, 0.5); - var p2 = HS2XY(ToneAdjustments[i].ToneHue + rotation + ToneAdjustments[i].AdjustHue, 0.5 * ToneAdjustments[i].AdjustSaturation); + var p1 = HS2XY(ToneAdjustments[i].ToneHue + rotation, ToneAdjustments[i].ToneSaturation); + var p2 = HS2XY(ToneAdjustments[i].ToneHue + rotation + ToneAdjustments[i].AdjustHue, ToneAdjustments[i].ToneSaturation * ToneAdjustments[i].AdjustSaturation); var group = new GeometryGroup(); group.Children.Add(new EllipseGeometry(p1, 2, 2)); @@ -187,7 +187,7 @@ private void HandleMouseMove(object sender, MouseEventArgs e) else if (h < -0.5f) h += 1; _viewModel.HueAdjust = h; - _viewModel.SaturationAdjust = s * 2f; + _viewModel.SaturationAdjust = s / ToneAdjustments[ActiveToneIndex].ToneSaturation; UpdateToneControlImage(); } } @@ -198,8 +198,8 @@ private void HandleMouseMove(object sender, MouseEventArgs e) _highlightTone = -1; for (int i = 0; i < ToneAdjustments.Length; i++) { - var p1 = HS2XY(ToneAdjustments[i].ToneHue + rotation, 0.5); - var p2 = HS2XY(ToneAdjustments[i].ToneHue + rotation + ToneAdjustments[i].AdjustHue, 0.5 * ToneAdjustments[i].AdjustSaturation); + var p1 = HS2XY(ToneAdjustments[i].ToneHue + rotation, ToneAdjustments[i].ToneSaturation); + var p2 = HS2XY(ToneAdjustments[i].ToneHue + rotation + ToneAdjustments[i].AdjustHue, ToneAdjustments[i].ToneSaturation * ToneAdjustments[i].AdjustSaturation); var distance = SqrDistance(p1, pt); if (distance < bestDistance) { diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 57236d1..0f759cc 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -265,7 +265,8 @@ private void UpdateColorTones() for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones; i++) { _colorTones[i] ??= new Rectangle() { Width = 35, Height = 12, RadiusX = 4, RadiusY = 4, Stroke = Brushes.Black }; - ColorToneAdjustOperation.ColorTransformHSI2RGB(_colorToneOperation.ToneAdjustments[i].ToneHue + (float)ToneRotation, 0.8f, 0.5f, + ColorToneAdjustOperation.ColorTransformHSI2RGB( + _colorToneOperation.ToneAdjustments[i].ToneHue + (float)ToneRotation, _colorToneOperation.ToneAdjustments[i].ToneSaturation, 0.5f, out var r, out var g, out var b); _colorTones[i].Fill = new SolidColorBrush(Color.FromRgb((byte)(r * 255), (byte)(g * 255), (byte)(b * 255))); } From 9edbb96c4371eb612f9d9f4c3804bae3120ab42a Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 19 Sep 2026 20:18:01 +0200 Subject: [PATCH 02/10] Smaller adjustment points --- PhotoLocator/Controls/ColorToneControl.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PhotoLocator/Controls/ColorToneControl.xaml.cs b/PhotoLocator/Controls/ColorToneControl.xaml.cs index 5502446..11ec866 100644 --- a/PhotoLocator/Controls/ColorToneControl.xaml.cs +++ b/PhotoLocator/Controls/ColorToneControl.xaml.cs @@ -135,7 +135,7 @@ void UpdateToneControlImage() var group = new GeometryGroup(); group.Children.Add(new EllipseGeometry(p1, 2, 2)); - group.Children.Add(new EllipseGeometry(p2, 2, 2)); + group.Children.Add(new EllipseGeometry(p2, 1.5, 1.5)); group.Children.Add(new LineGeometry(p1, p2)); double thickness; @@ -152,7 +152,7 @@ void UpdateToneControlImage() ColorToneAdjustOperation.ColorTransformRGB2HSI(color[0], color[1], color[2], out float h, out float s, out _); var xy = HS2XY(h, s); var group = new GeometryGroup(); - group.Children.Add(new EllipseGeometry(xy, 2, 2)); + group.Children.Add(new EllipseGeometry(xy, 1.5, 1.5)); var drawing = new GeometryDrawing(Brushes.Transparent, new Pen(Brushes.White, InactivePenThickness), group); drawings.Children.Add(drawing); } From 02f03c71461c7799c09ad1d2f0447628ed798dbc Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 19 Sep 2026 20:18:14 +0200 Subject: [PATCH 03/10] Fix hue shift --- .../ColorToneAdjustOperation.cs | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index 522c613..e2b0a76 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -11,7 +11,7 @@ class ColorToneAdjustOperation : OperationBase public const int NumberOfTones = NumberOfHues * 2; public const float ToneLowSaturation = 0.4f; - public const float ToneHighSaturation = 0.8f; + public const float ToneHighSaturation = 0.9f; FloatBitmap? _srcHSI; bool _updateSrcHsi; @@ -78,6 +78,8 @@ public static void ColorTransformHSI2RGB(float h, float s, float i, out float r, h -= 1; else if (h < 0) h += 1; + if (s > 1) + s = 1; double rr, rg, rb, rh; if (h <= 1f / 3) // 0°= ToneHighSaturation) { - saturationWeight = 0; nextSaturationWeight = 1; + lowSaturationWeight = 0; highSaturationWeight = 1; } else { var saturationTone = (saturation - ToneLowSaturation) / (ToneHighSaturation - ToneLowSaturation); - nextSaturationWeight = RealMath.SmoothStep(saturationTone); - saturationWeight = 1 - nextSaturationWeight; + highSaturationWeight = RealMath.SmoothStep(saturationTone); + lowSaturationWeight = 1 - highSaturationWeight; } var hue = src[xx]; @@ -244,17 +246,23 @@ public override void Apply() toneHue * toneHueWeight + nextToneHue * nextToneHueWeight; } + + var haLow = toneAdjustments[hueIndex].AdjustHue; + var haHigh = FixHue(toneAdjustments[hueIndex + NumberOfHues].AdjustHue, haLow); + var haLowNext = FixHue(toneAdjustments[nextHueIndex].AdjustHue, haLow); + var haHighNext = FixHue(toneAdjustments[nextHueIndex + NumberOfHues].AdjustHue, haLowNext); var h = hue + - (toneAdjustments[hueIndex].AdjustHue * saturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustHue * nextSaturationWeight) * hueWeight + - (toneAdjustments[nextHueIndex].AdjustHue * saturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustHue * nextSaturationWeight) * nextHueWeight; + (haLow * lowSaturationWeight + haHigh * highSaturationWeight) * hueWeight + + (haLowNext * lowSaturationWeight + haHighNext * highSaturationWeight) * nextHueWeight; + var s = src[xx + 1] * - ((toneAdjustments[hueIndex].AdjustSaturation * saturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustSaturation * nextSaturationWeight) * hueWeight + - (toneAdjustments[nextHueIndex].AdjustSaturation * saturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustSaturation * nextSaturationWeight) * nextHueWeight); - if (s > 1) - s = 1; + ((toneAdjustments[hueIndex].AdjustSaturation * lowSaturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustSaturation * highSaturationWeight) * hueWeight + + (toneAdjustments[nextHueIndex].AdjustSaturation * lowSaturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustSaturation * highSaturationWeight) * nextHueWeight); + var i = src[xx + 2] * - ((toneAdjustments[hueIndex].AdjustIntensity * saturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustIntensity * nextSaturationWeight) * hueWeight + - (toneAdjustments[nextHueIndex].AdjustIntensity * saturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustIntensity * nextSaturationWeight) * nextHueWeight); + ((toneAdjustments[hueIndex].AdjustIntensity * lowSaturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustIntensity * highSaturationWeight) * hueWeight + + (toneAdjustments[nextHueIndex].AdjustIntensity * lowSaturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustIntensity * highSaturationWeight) * nextHueWeight); + ColorTransformHSI2RGB(h, s, i, out dst[xx], out dst[xx + 1], out dst[xx + 2]); xx += 3; } @@ -262,5 +270,14 @@ public override void Apply() } }); } + + private static float FixHue(float hueAdjust, float hueAdjustBaseBase) + { + if (hueAdjust < hueAdjustBaseBase - 0.5f) + hueAdjust += 1; + else if (hueAdjust > hueAdjustBaseBase + 0.5f) + hueAdjust -= 1; + return hueAdjust; + } } } From dd51093a16dc797890640fb83f448b6dbe972093 Mon Sep 17 00:00:00 2001 From: meesoft Date: Sat, 19 Sep 2026 23:00:54 +0200 Subject: [PATCH 04/10] Dual tone option --- .../ColorToneAdjustOperation.cs | 135 ++++++++++++++++-- PhotoLocator/LocalContrastViewModel.cs | 71 +++++---- 2 files changed, 168 insertions(+), 38 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index e2b0a76..3d11a36 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -8,8 +8,7 @@ class ColorToneAdjustOperation : OperationBase { public const int NumberOfHues = 8; - public const int NumberOfTones = NumberOfHues * 2; - + public const float SingleToneSaturation = 0.5f; public const float ToneLowSaturation = 0.4f; public const float ToneHighSaturation = 0.9f; @@ -18,22 +17,24 @@ class ColorToneAdjustOperation : OperationBase public struct ToneAdjustment(float toneHue, float toneSaturation) { - public readonly float ToneHue = toneHue; - public readonly float ToneSaturation = toneSaturation; + public float ToneHue { get; } = toneHue; + public float ToneSaturation { get; internal set; } = toneSaturation; public float AdjustHue = 0; public float AdjustSaturation = 1; public float AdjustIntensity = 1; public float HueUniformity = 0; } + + public int NumberOfTones => ToneAdjustments.Length; - public ToneAdjustment[] ToneAdjustments { get; } = new ToneAdjustment[NumberOfTones]; + public ToneAdjustment[] ToneAdjustments { get; private set; } = new ToneAdjustment[NumberOfHues]; public float Rotation { get; set { - if (value <= -1 || value >= 1) + if (value is <= -1 or >= 1) throw new ArgumentOutOfRangeException(nameof(value)); field = value; } @@ -43,13 +44,49 @@ public ColorToneAdjustOperation() { ResetToneAdjustments(); } - + public void ResetToneAdjustments() { - for (var i = 0; i < NumberOfHues; i++) + if (DualToneMode) + for (var i = 0; i < NumberOfHues; i++) + { + ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfHues, ToneHighSaturation); + ToneAdjustments[i + NumberOfHues] = new ToneAdjustment((float)i / NumberOfHues, ToneLowSaturation); + } + else + for (var i = 0; i < NumberOfHues; i++) + ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfHues, SingleToneSaturation); + + } + + public bool DualToneMode + { + get => ToneAdjustments.Length > NumberOfHues; + set { - ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfHues, ToneLowSaturation); - ToneAdjustments[i + NumberOfHues] = new ToneAdjustment((float)i / NumberOfHues, ToneHighSaturation); + if (value != DualToneMode) + { + if (value) + { + var newToneAdjustments = new ToneAdjustment[NumberOfHues * 2]; + Array.Copy(ToneAdjustments, newToneAdjustments, NumberOfHues); + Array.Copy(ToneAdjustments, 0, newToneAdjustments, NumberOfHues, NumberOfHues); + for (var i = 0; i < NumberOfHues; i++) + { + newToneAdjustments[i].ToneSaturation = ToneLowSaturation; + newToneAdjustments[i + NumberOfHues].ToneSaturation = ToneHighSaturation; + } + ToneAdjustments = newToneAdjustments; + } + else + { + var newToneAdjustments = new ToneAdjustment[NumberOfHues]; + Array.Copy(ToneAdjustments, newToneAdjustments, NumberOfHues); + for (var i = 0; i < NumberOfHues; i++) + newToneAdjustments[i].ToneSaturation = SingleToneSaturation; + ToneAdjustments = newToneAdjustments; + } + } } } @@ -185,7 +222,83 @@ public override void Apply() ColorTransformRGB2HSI(SrcBitmap, _srcHSI); } DstBitmap.New(_srcHSI.Width, _srcHSI.Height, 3); - Parallel.For(0, _srcHSI.Height, y => + if (DualToneMode) + ApplyDualToneAdjustments(); + else + ApplyToneAdjustments(); + } + + void ApplyToneAdjustments() + { + Parallel.For(0, _srcHSI!.Height, y => + { + var toneAdjustments = ToneAdjustments; + var width = _srcHSI.Width; + unsafe + { + fixed (float* src = &_srcHSI.Elements[y, 0]) + fixed (float* dst = &DstBitmap.Elements[y, 0]) + { + int xx = 0; + for (var x = 0; x < width; x++) + { + var tone = (src[xx] - Rotation) * NumberOfHues; + if (tone < 0) + tone += NumberOfHues; + if (tone >= NumberOfHues) + tone -= NumberOfHues; + var toneIndex = (int)tone; + var nextToneIndex = toneIndex + 1; + if (nextToneIndex == NumberOfHues) + nextToneIndex = 0; + var nextToneWeight = RealMath.SmoothStep(tone - toneIndex); + var toneWeight = 1 - nextToneWeight; + + var hue = src[xx]; + if (toneAdjustments[toneIndex].HueUniformity > 0 || toneAdjustments[nextToneIndex].HueUniformity > 0) + { + var toneHue = toneAdjustments[toneIndex].ToneHue + Rotation; + if (toneHue < hue - 0.5f) + toneHue++; + else if (toneHue > hue + 0.5f) + toneHue--; + var nextToneHue = toneAdjustments[nextToneIndex].ToneHue + Rotation; + if (nextToneHue < hue - 0.5f) + nextToneHue++; + else if (nextToneHue > hue + 0.5f) + nextToneHue--; + + var toneHueWeight = toneAdjustments[toneIndex].HueUniformity * toneWeight; + var nextToneHueWeight = toneAdjustments[nextToneIndex].HueUniformity * nextToneWeight; + hue = hue * (1 - toneHueWeight - nextToneHueWeight) + + toneHue * toneHueWeight + + nextToneHue * nextToneHueWeight; + } + var ha = toneAdjustments[toneIndex].AdjustHue; + var haNext = FixHue(toneAdjustments[nextToneIndex].AdjustHue, ha); + var h = hue + + ha * toneWeight + + haNext * nextToneWeight; + + var s = src[xx + 1] * + (toneAdjustments[toneIndex].AdjustSaturation * toneWeight + + toneAdjustments[nextToneIndex].AdjustSaturation * nextToneWeight); + + var i = src[xx + 2] * + (toneAdjustments[toneIndex].AdjustIntensity * toneWeight + + toneAdjustments[nextToneIndex].AdjustIntensity * nextToneWeight); + + ColorTransformHSI2RGB(h, s, i, out dst[xx], out dst[xx + 1], out dst[xx + 2]); + xx += 3; + } + } + } + }); + } + + void ApplyDualToneAdjustments() + { + Parallel.For(0, _srcHSI!.Height, y => { var toneAdjustments = ToneAdjustments; var width = _srcHSI.Width; diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 0f759cc..18ccc6f 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Numerics; using System.Runtime.CompilerServices; @@ -252,17 +253,17 @@ public IEnumerable ColorTones get { UpdateColorTones(); - for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones; i++) - { + for (int i = 0; i < _colorTones.Length; i++) yield return new ComboBoxItem { Content = _colorTones[i] }; - } yield return new ComboBoxItem { Content = "All" }; } } + [MemberNotNull(nameof(_colorTones))] private void UpdateColorTones() { - for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones; i++) + _colorTones = new Rectangle[_colorToneOperation.NumberOfTones]; + for (int i = 0; i < _colorToneOperation.NumberOfTones; i++) { _colorTones[i] ??= new Rectangle() { Width = 35, Height = 12, RadiusX = 4, RadiusY = 4, Stroke = Brushes.Black }; ColorToneAdjustOperation.ColorTransformHSI2RGB( @@ -271,7 +272,19 @@ private void UpdateColorTones() _colorTones[i].Fill = new SolidColorBrush(Color.FromRgb((byte)(r * 255), (byte)(g * 255), (byte)(b * 255))); } } - readonly Rectangle[] _colorTones = new Rectangle[ColorToneAdjustOperation.NumberOfTones]; + Rectangle[]? _colorTones; + + public bool IsDualToneModeEnabled + { + get => _colorToneOperation.DualToneMode; + set + { + if (value == IsDualToneModeEnabled) + return; + _colorToneOperation.DualToneMode = value; + NotifyPropertyChanged(nameof(ColorTones)); + } + } public int ActiveToneIndex { @@ -280,14 +293,14 @@ public int ActiveToneIndex { if (SetProperty(ref field, value) && value >= 0) { - if (value == ColorToneAdjustOperation.NumberOfTones) + if (value == _colorToneOperation.NumberOfTones) { - for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones - 1; i++) + for (int i = 0; i < _colorToneOperation.NumberOfTones - 1; i++) { - _colorToneOperation.ToneAdjustments[i].AdjustHue = _colorToneOperation.ToneAdjustments[ColorToneAdjustOperation.NumberOfTones - 1].AdjustHue; - _colorToneOperation.ToneAdjustments[i].AdjustSaturation = _colorToneOperation.ToneAdjustments[ColorToneAdjustOperation.NumberOfTones - 1].AdjustSaturation; - _colorToneOperation.ToneAdjustments[i].AdjustIntensity = _colorToneOperation.ToneAdjustments[ColorToneAdjustOperation.NumberOfTones - 1].AdjustIntensity; - _colorToneOperation.ToneAdjustments[i].HueUniformity = _colorToneOperation.ToneAdjustments[ColorToneAdjustOperation.NumberOfTones - 1].HueUniformity; + _colorToneOperation.ToneAdjustments[i].AdjustHue = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].AdjustHue; + _colorToneOperation.ToneAdjustments[i].AdjustSaturation = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].AdjustSaturation; + _colorToneOperation.ToneAdjustments[i].AdjustIntensity = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].AdjustIntensity; + _colorToneOperation.ToneAdjustments[i].HueUniformity = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].HueUniformity; } } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HueAdjust))); @@ -296,16 +309,16 @@ public int ActiveToneIndex PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HueUniformity))); } } - } = ColorToneAdjustOperation.NumberOfTones; + } = ColorToneAdjustOperation.NumberOfHues; public float HueAdjust { - get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, ColorToneAdjustOperation.NumberOfTones - 1)].AdjustHue; + get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, _colorToneOperation.NumberOfTones - 1)].AdjustHue; set { - if (ActiveToneIndex == ColorToneAdjustOperation.NumberOfTones) + if (ActiveToneIndex == _colorToneOperation.NumberOfTones) { - for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones; i++) + for (int i = 0; i < _colorToneOperation.NumberOfTones; i++) _colorToneOperation.ToneAdjustments[i].AdjustHue = value; NotifyPropertyChanged(); } @@ -318,12 +331,12 @@ public float HueAdjust public float SaturationAdjust { - get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, ColorToneAdjustOperation.NumberOfTones - 1)].AdjustSaturation; + get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, _colorToneOperation.NumberOfTones - 1)].AdjustSaturation; set { - if (ActiveToneIndex == ColorToneAdjustOperation.NumberOfTones) + if (ActiveToneIndex == _colorToneOperation.NumberOfTones) { - for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones; i++) + for (int i = 0; i < _colorToneOperation.NumberOfTones; i++) _colorToneOperation.ToneAdjustments[i].AdjustSaturation = value; NotifyPropertyChanged(); } @@ -342,12 +355,12 @@ public Vector3? ColorUnderCursor public float IntensityAdjust { - get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, ColorToneAdjustOperation.NumberOfTones - 1)].AdjustIntensity; + get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, _colorToneOperation.NumberOfTones - 1)].AdjustIntensity; set { - if (ActiveToneIndex == ColorToneAdjustOperation.NumberOfTones) + if (ActiveToneIndex == _colorToneOperation.NumberOfTones) { - for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones; i++) + for (int i = 0; i < _colorToneOperation.NumberOfTones; i++) _colorToneOperation.ToneAdjustments[i].AdjustIntensity = value; NotifyPropertyChanged(); } @@ -360,12 +373,12 @@ public float IntensityAdjust public float HueUniformity { - get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, ColorToneAdjustOperation.NumberOfTones - 1)].HueUniformity; + get => _colorToneOperation.ToneAdjustments[Math.Min(ActiveToneIndex, _colorToneOperation.NumberOfTones - 1)].HueUniformity; set { - if (ActiveToneIndex == ColorToneAdjustOperation.NumberOfTones) + if (ActiveToneIndex == _colorToneOperation.NumberOfTones) { - for (int i = 0; i < ColorToneAdjustOperation.NumberOfTones; i++) + for (int i = 0; i < _colorToneOperation.NumberOfTones; i++) _colorToneOperation.ToneAdjustments[i].HueUniformity = value; NotifyPropertyChanged(); } @@ -411,7 +424,7 @@ public double ToneRotation _colorToneOperation.ResetToneAdjustments(); ToneRotation = 0; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); - ActiveToneIndex = ColorToneAdjustOperation.NumberOfTones; + ActiveToneIndex = _colorToneOperation.NumberOfTones; StartUpdateTimer(FirstParamChanged.ColorTone); }); @@ -507,9 +520,12 @@ private void RestoreAdjustmentValues(AdjustmentValues av) DetailHandling = av.DetailHandling; MaxStretch = av.MaxStretch; ToneRotation = av.ToneRotation; - if (_colorToneOperation.ToneAdjustments.Length != av.ToneAdjustments?.Length) + + if (av.ToneAdjustments is null || av.ToneAdjustments.Length != ColorToneAdjustOperation.NumberOfHues && av.ToneAdjustments.Length != ColorToneAdjustOperation.NumberOfHues * 2) _colorToneOperation.ResetToneAdjustments(); else + { + _colorToneOperation.DualToneMode = av.ToneAdjustments.Length == ColorToneAdjustOperation.NumberOfHues * 2; for (int i = 0; i < av.ToneAdjustments.Length; i++) { var src = av.ToneAdjustments[i]; @@ -518,8 +534,9 @@ private void RestoreAdjustmentValues(AdjustmentValues av) _colorToneOperation.ToneAdjustments[i].AdjustIntensity = src?.AdjustIntensity ?? 1; _colorToneOperation.ToneAdjustments[i].HueUniformity = src?.HueUniformity ?? 0; } + } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); - ActiveToneIndex = _colorToneOperation.AreToneAdjustmentsChanged ? 0 : ColorToneAdjustOperation.NumberOfTones; + ActiveToneIndex = _colorToneOperation.AreToneAdjustmentsChanged ? 0 : _colorToneOperation .NumberOfTones; StartUpdateTimer(FirstParamChanged.ColorTone); } From 389786617dc527940cd0d08f0936c174e6674f52 Mon Sep 17 00:00:00 2001 From: meesoft Date: Sun, 20 Sep 2026 09:34:09 +0200 Subject: [PATCH 05/10] Attach menu item --- PhotoLocator/LocalContrastView.xaml | 2 ++ PhotoLocator/LocalContrastViewModel.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/PhotoLocator/LocalContrastView.xaml b/PhotoLocator/LocalContrastView.xaml index 0b238ff..cd990f0 100644 --- a/PhotoLocator/LocalContrastView.xaml +++ b/PhotoLocator/LocalContrastView.xaml @@ -46,6 +46,8 @@ + + diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 18ccc6f..7eba18c 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -282,7 +282,9 @@ public bool IsDualToneModeEnabled if (value == IsDualToneModeEnabled) return; _colorToneOperation.DualToneMode = value; + NotifyPropertyChanged(nameof(IsDualToneModeEnabled)); NotifyPropertyChanged(nameof(ColorTones)); + ActiveToneIndex = 0; } } From b2ea2b0e243ee9787d36d7596c3122e100b4aa48 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sun, 20 Sep 2026 09:52:43 +0200 Subject: [PATCH 06/10] Fix reset --- PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs | 5 ++--- PhotoLocator/LocalContrastViewModel.cs | 3 ++- PhotoLocator/Settings/RegistrySettings.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index 3d11a36..d560a2c 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -50,13 +50,12 @@ public void ResetToneAdjustments() if (DualToneMode) for (var i = 0; i < NumberOfHues; i++) { - ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfHues, ToneHighSaturation); - ToneAdjustments[i + NumberOfHues] = new ToneAdjustment((float)i / NumberOfHues, ToneLowSaturation); + ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfHues, ToneLowSaturation); + ToneAdjustments[i + NumberOfHues] = new ToneAdjustment((float)i / NumberOfHues, ToneHighSaturation); } else for (var i = 0; i < NumberOfHues; i++) ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfHues, SingleToneSaturation); - } public bool DualToneMode diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 7eba18c..853b160 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -281,10 +281,11 @@ public bool IsDualToneModeEnabled { if (value == IsDualToneModeEnabled) return; + var isAllSelected = ActiveToneIndex == _colorToneOperation.NumberOfTones; _colorToneOperation.DualToneMode = value; NotifyPropertyChanged(nameof(IsDualToneModeEnabled)); NotifyPropertyChanged(nameof(ColorTones)); - ActiveToneIndex = 0; + ActiveToneIndex = isAllSelected ? _colorToneOperation.NumberOfTones : 0; } } diff --git a/PhotoLocator/Settings/RegistrySettings.cs b/PhotoLocator/Settings/RegistrySettings.cs index 5094e9d..be5e1d7 100644 --- a/PhotoLocator/Settings/RegistrySettings.cs +++ b/PhotoLocator/Settings/RegistrySettings.cs @@ -60,7 +60,7 @@ public bool IncludeSidecarFiles public string SavedFilePostfix { - get => Key.GetValue(nameof(SavedFilePostfix)) as string ?? "[geo]"; + get => Key.GetValue(nameof(SavedFilePostfix)) as string ?? "[PL]"; set => Key.SetValue(nameof(SavedFilePostfix), value); } From 14f8722c8fdd84d8445f7b6267d007b4fa93b5fa Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sun, 20 Sep 2026 15:07:13 +0200 Subject: [PATCH 07/10] Cleanup --- .../BitmapOperations/ColorToneAdjustOperation.cs | 10 +++++----- PhotoLocator/LocalContrastViewModel.cs | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index d560a2c..c06a74b 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -8,9 +8,9 @@ class ColorToneAdjustOperation : OperationBase { public const int NumberOfHues = 8; - public const float SingleToneSaturation = 0.5f; - public const float ToneLowSaturation = 0.4f; - public const float ToneHighSaturation = 0.9f; + const float SingleToneSaturation = 0.5f; + const float ToneLowSaturation = 0.4f; + const float ToneHighSaturation = 0.9f; FloatBitmap? _srcHSI; bool _updateSrcHsi; @@ -224,10 +224,10 @@ public override void Apply() if (DualToneMode) ApplyDualToneAdjustments(); else - ApplyToneAdjustments(); + ApplySingleToneAdjustments(); } - void ApplyToneAdjustments() + void ApplySingleToneAdjustments() { Parallel.For(0, _srcHSI!.Height, y => { diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 853b160..74113e6 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -263,7 +263,7 @@ public IEnumerable ColorTones private void UpdateColorTones() { _colorTones = new Rectangle[_colorToneOperation.NumberOfTones]; - for (int i = 0; i < _colorToneOperation.NumberOfTones; i++) + for (int i = 0; i < _colorTones.Length; i++) { _colorTones[i] ??= new Rectangle() { Width = 35, Height = 12, RadiusX = 4, RadiusY = 4, Stroke = Brushes.Black }; ColorToneAdjustOperation.ColorTransformHSI2RGB( @@ -298,12 +298,13 @@ public int ActiveToneIndex { if (value == _colorToneOperation.NumberOfTones) { - for (int i = 0; i < _colorToneOperation.NumberOfTones - 1; i++) + var numberOfTones_1 = _colorToneOperation.NumberOfTones - 1; + for (int i = 0; i < numberOfTones_1; i++) { - _colorToneOperation.ToneAdjustments[i].AdjustHue = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].AdjustHue; - _colorToneOperation.ToneAdjustments[i].AdjustSaturation = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].AdjustSaturation; - _colorToneOperation.ToneAdjustments[i].AdjustIntensity = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].AdjustIntensity; - _colorToneOperation.ToneAdjustments[i].HueUniformity = _colorToneOperation.ToneAdjustments[_colorToneOperation.NumberOfTones - 1].HueUniformity; + _colorToneOperation.ToneAdjustments[i].AdjustHue = _colorToneOperation.ToneAdjustments[numberOfTones_1].AdjustHue; + _colorToneOperation.ToneAdjustments[i].AdjustSaturation = _colorToneOperation.ToneAdjustments[numberOfTones_1].AdjustSaturation; + _colorToneOperation.ToneAdjustments[i].AdjustIntensity = _colorToneOperation.ToneAdjustments[numberOfTones_1].AdjustIntensity; + _colorToneOperation.ToneAdjustments[i].HueUniformity = _colorToneOperation.ToneAdjustments[numberOfTones_1].HueUniformity; } } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HueAdjust))); From 31a1ef5efba19bb079d95f999e38a3c894a0b196 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sun, 20 Sep 2026 22:52:38 +0200 Subject: [PATCH 08/10] Cartesian hue-map interpolation --- .../ColorToneAdjustOperation.cs | 46 ++++++++++++++----- .../ColorToneAdjustOperationTest.cs | 23 ++++++++++ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index c06a74b..e0478a3 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -275,13 +275,13 @@ void ApplySingleToneAdjustments() } var ha = toneAdjustments[toneIndex].AdjustHue; var haNext = FixHue(toneAdjustments[nextToneIndex].AdjustHue, ha); - var h = hue + - ha * toneWeight + - haNext * nextToneWeight; + double hueVectorX = 0; + double hueVectorY = 0; + AddHueVector(hue + ha, toneAdjustments[toneIndex].AdjustSaturation, toneWeight, ref hueVectorX, ref hueVectorY); + AddHueVector(hue + haNext, toneAdjustments[nextToneIndex].AdjustSaturation, nextToneWeight, ref hueVectorX, ref hueVectorY); + GetHueAndSaturation(hueVectorX, hueVectorY, hue, out var h, out var saturationAdjust); - var s = src[xx + 1] * - (toneAdjustments[toneIndex].AdjustSaturation * toneWeight + - toneAdjustments[nextToneIndex].AdjustSaturation * nextToneWeight); + var s = src[xx + 1] * saturationAdjust; var i = src[xx + 2] * (toneAdjustments[toneIndex].AdjustIntensity * toneWeight + @@ -363,13 +363,15 @@ void ApplyDualToneAdjustments() var haHigh = FixHue(toneAdjustments[hueIndex + NumberOfHues].AdjustHue, haLow); var haLowNext = FixHue(toneAdjustments[nextHueIndex].AdjustHue, haLow); var haHighNext = FixHue(toneAdjustments[nextHueIndex + NumberOfHues].AdjustHue, haLowNext); - var h = hue + - (haLow * lowSaturationWeight + haHigh * highSaturationWeight) * hueWeight + - (haLowNext * lowSaturationWeight + haHighNext * highSaturationWeight) * nextHueWeight; + double hueVectorX = 0; + double hueVectorY = 0; + AddHueVector(hue + haLow, toneAdjustments[hueIndex].AdjustSaturation, hueWeight * lowSaturationWeight, ref hueVectorX, ref hueVectorY); + AddHueVector(hue + haHigh, toneAdjustments[hueIndex + NumberOfHues].AdjustSaturation, hueWeight * highSaturationWeight, ref hueVectorX, ref hueVectorY); + AddHueVector(hue + haLowNext, toneAdjustments[nextHueIndex].AdjustSaturation, nextHueWeight * lowSaturationWeight, ref hueVectorX, ref hueVectorY); + AddHueVector(hue + haHighNext, toneAdjustments[nextHueIndex + NumberOfHues].AdjustSaturation, nextHueWeight * highSaturationWeight, ref hueVectorX, ref hueVectorY); + GetHueAndSaturation(hueVectorX, hueVectorY, hue, out var h, out var saturationAdjust); - var s = src[xx + 1] * - ((toneAdjustments[hueIndex].AdjustSaturation * lowSaturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustSaturation * highSaturationWeight) * hueWeight + - (toneAdjustments[nextHueIndex].AdjustSaturation * lowSaturationWeight + toneAdjustments[nextHueIndex + NumberOfHues].AdjustSaturation * highSaturationWeight) * nextHueWeight); + var s = src[xx + 1] * saturationAdjust; var i = src[xx + 2] * ((toneAdjustments[hueIndex].AdjustIntensity * lowSaturationWeight + toneAdjustments[hueIndex + NumberOfHues].AdjustIntensity * highSaturationWeight) * hueWeight + @@ -391,5 +393,25 @@ private static float FixHue(float hueAdjust, float hueAdjustBaseBase) hueAdjust -= 1; return hueAdjust; } + + private static void AddHueVector(float hue, float saturation, float weight, ref double x, ref double y) + { + var angle = hue * (Math.PI * 2); + x += weight * saturation * Math.Cos(angle); + y += weight * saturation * Math.Sin(angle); + } + + private static void GetHueAndSaturation(double x, double y, float fallbackHue, out float hue, out float saturation) + { + saturation = (float)Math.Sqrt(x * x + y * y); + if (saturation > 0) + { + hue = (float)(Math.Atan2(y, x) / (Math.PI * 2)); + if (hue < 0) + hue++; + } + else + hue = fallbackHue; + } } } diff --git a/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs b/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs index b256610..3e9c7f6 100644 --- a/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs +++ b/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs @@ -33,5 +33,28 @@ public void Apply_ColorToneAdjustOperation() GeneralFileFormatHandler.SaveToFile(result, "ColorToneAdjust.png"); #endif } + + [TestMethod] + public void ApplySingleToneAdjustments_OppositeHueInterpolationReducesSaturation() + { + var source = new FloatBitmap(1, 1, 3); + ColorToneAdjustOperation.ColorTransformHSI2RGB(1f / 16, 1, 0.4f, + out source.Elements[0, 0], out source.Elements[0, 1], out source.Elements[0, 2]); + + var operation = new ColorToneAdjustOperation + { + SrcBitmap = source, + DstBitmap = new FloatBitmap(), + }; + operation.ToneAdjustments[0].AdjustHue = 0; + operation.ToneAdjustments[1].AdjustHue = 0.5f; + + operation.Apply(); + + ColorToneAdjustOperation.ColorTransformRGB2HSI( + operation.DstBitmap.Elements[0, 0], operation.DstBitmap.Elements[0, 1], operation.DstBitmap.Elements[0, 2], + out _, out var saturation, out _); + Assert.IsTrue(saturation < 0.001f, $"Saturation was {saturation}"); + } } } From 3211041d5c59c47645b1150ac9924938b5da16a0 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Mon, 21 Sep 2026 22:53:19 +0200 Subject: [PATCH 09/10] Fix setting uniformity --- .../BitmapOperations/ColorToneAdjustOperation.cs | 6 +++--- PhotoLocator/LocalContrastViewModel.cs | 9 +++++++++ .../BitmapOperations/ColorToneAdjustOperationTest.cs | 6 +++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index e0478a3..f65584a 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -396,9 +396,9 @@ private static float FixHue(float hueAdjust, float hueAdjustBaseBase) private static void AddHueVector(float hue, float saturation, float weight, ref double x, ref double y) { - var angle = hue * (Math.PI * 2); - x += weight * saturation * Math.Cos(angle); - y += weight * saturation * Math.Sin(angle); + var (sin, cos) = Math.SinCos(hue * (Math.PI * 2)); + x += weight * saturation * cos; + y += weight * saturation * sin; } private static void GetHueAndSaturation(double x, double y, float fallbackHue, out float hue, out float saturation) diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 74113e6..256d22e 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -387,7 +387,16 @@ public float HueUniformity NotifyPropertyChanged(); } else if (SetProperty(ref _colorToneOperation.ToneAdjustments[ActiveToneIndex].HueUniformity, value)) + { + if (IsDualToneModeEnabled) + { + if (ActiveToneIndex < ColorToneAdjustOperation.NumberOfHues) + _colorToneOperation.ToneAdjustments[ActiveToneIndex + ColorToneAdjustOperation.NumberOfHues].HueUniformity = value; + else + _colorToneOperation.ToneAdjustments[ActiveToneIndex - ColorToneAdjustOperation.NumberOfHues].HueUniformity = value; + } StartUpdateTimer(FirstParamChanged.ColorTone); + } } } diff --git a/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs b/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs index 3e9c7f6..5313c7d 100644 --- a/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs +++ b/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs @@ -35,7 +35,7 @@ public void Apply_ColorToneAdjustOperation() } [TestMethod] - public void ApplySingleToneAdjustments_OppositeHueInterpolationReducesSaturation() + public void ApplySingleToneAdjustments_OppositeHueInterpolationShouldReducesSaturation() { var source = new FloatBitmap(1, 1, 3); ColorToneAdjustOperation.ColorTransformHSI2RGB(1f / 16, 1, 0.4f, @@ -53,8 +53,8 @@ public void ApplySingleToneAdjustments_OppositeHueInterpolationReducesSaturation ColorToneAdjustOperation.ColorTransformRGB2HSI( operation.DstBitmap.Elements[0, 0], operation.DstBitmap.Elements[0, 1], operation.DstBitmap.Elements[0, 2], - out _, out var saturation, out _); - Assert.IsTrue(saturation < 0.001f, $"Saturation was {saturation}"); + out var hue, out var saturation, out var intensity); + Assert.IsLessThan(0.001f, saturation, $"Saturation was {saturation}"); } } } From 7902779bd7c45ed3a2a4713f4f3a0f3408208cfb Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 26 Sep 2026 08:28:43 +0200 Subject: [PATCH 10/10] Guard against mode change during processing --- .../BitmapOperations/ColorToneAdjustOperation.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index f65584a..308e47f 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -221,17 +221,18 @@ public override void Apply() ColorTransformRGB2HSI(SrcBitmap, _srcHSI); } DstBitmap.New(_srcHSI.Width, _srcHSI.Height, 3); - if (DualToneMode) - ApplyDualToneAdjustments(); + + var toneAdjustments = ToneAdjustments; // Guard against mode change during processing + if (toneAdjustments.Length > NumberOfHues) + ApplyDualToneAdjustments(toneAdjustments); else - ApplySingleToneAdjustments(); + ApplySingleToneAdjustments(toneAdjustments); } - void ApplySingleToneAdjustments() + void ApplySingleToneAdjustments(ToneAdjustment[] toneAdjustments) { Parallel.For(0, _srcHSI!.Height, y => { - var toneAdjustments = ToneAdjustments; var width = _srcHSI.Width; unsafe { @@ -295,11 +296,10 @@ void ApplySingleToneAdjustments() }); } - void ApplyDualToneAdjustments() + void ApplyDualToneAdjustments(ToneAdjustment[] toneAdjustments) { Parallel.For(0, _srcHSI!.Height, y => { - var toneAdjustments = ToneAdjustments; var width = _srcHSI.Width; unsafe {