diff --git a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs index 683b327..308e47f 100644 --- a/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs +++ b/PhotoLocator/BitmapOperations/ColorToneAdjustOperation.cs @@ -6,28 +6,35 @@ namespace PhotoLocator.BitmapOperations { class ColorToneAdjustOperation : OperationBase { - public const int NumberOfTones = 8; + public const int NumberOfHues = 8; + + const float SingleToneSaturation = 0.5f; + const float ToneLowSaturation = 0.4f; + const float ToneHighSaturation = 0.9f; FloatBitmap? _srcHSI; bool _updateSrcHsi; - public struct ToneAdjustment(float toneHue) + public struct ToneAdjustment(float toneHue, float toneSaturation) { - public readonly float ToneHue = toneHue; + 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; } @@ -37,11 +44,49 @@ public ColorToneAdjustOperation() { ResetToneAdjustments(); } - + public void ResetToneAdjustments() { - for (var i = 0; i < NumberOfTones; i++) - ToneAdjustments[i] = new ToneAdjustment((float)i / NumberOfTones); + if (DualToneMode) + for (var i = 0; i < NumberOfHues; i++) + { + 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 + { + get => ToneAdjustments.Length > NumberOfHues; + set + { + 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; + } + } + } } public bool AreToneAdjustmentsChanged @@ -69,6 +114,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° + + var toneAdjustments = ToneAdjustments; // Guard against mode change during processing + if (toneAdjustments.Length > NumberOfHues) + ApplyDualToneAdjustments(toneAdjustments); + else + ApplySingleToneAdjustments(toneAdjustments); + } + + void ApplySingleToneAdjustments(ToneAdjustment[] toneAdjustments) + { + Parallel.For(0, _srcHSI!.Height, y => { - var toneAdjustments = ToneAdjustments; var width = _srcHSI.Width; unsafe { @@ -186,14 +242,14 @@ public override void Apply() int xx = 0; for (var x = 0; x < width; x++) { - var tone = (src[xx] - Rotation) * NumberOfTones; + var tone = (src[xx] - Rotation) * NumberOfHues; if (tone < 0) - tone += NumberOfTones; - if (tone >= NumberOfTones) - tone -= NumberOfTones; + tone += NumberOfHues; + if (tone >= NumberOfHues) + tone -= NumberOfHues; var toneIndex = (int)tone; var nextToneIndex = toneIndex + 1; - if (nextToneIndex == NumberOfTones) + if (nextToneIndex == NumberOfHues) nextToneIndex = 0; var nextToneWeight = RealMath.SmoothStep(tone - toneIndex); var toneWeight = 1 - nextToneWeight; @@ -218,17 +274,109 @@ public override void Apply() toneHue * toneHueWeight + nextToneHue * nextToneHueWeight; } - var h = hue + - toneAdjustments[toneIndex].AdjustHue * toneWeight + - toneAdjustments[nextToneIndex].AdjustHue * nextToneWeight; - var s = src[xx + 1] * - (toneAdjustments[toneIndex].AdjustSaturation * toneWeight + - toneAdjustments[nextToneIndex].AdjustSaturation * nextToneWeight); - if (s > 1) - s = 1; + var ha = toneAdjustments[toneIndex].AdjustHue; + var haNext = FixHue(toneAdjustments[nextToneIndex].AdjustHue, ha); + 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] * saturationAdjust; + 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(ToneAdjustment[] toneAdjustments) + { + Parallel.For(0, _srcHSI!.Height, y => + { + 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 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 lowSaturationWeight, highSaturationWeight; + if (saturation <= ToneLowSaturation) + { + lowSaturationWeight = 1; highSaturationWeight = 0; + } + else if (saturation >= ToneHighSaturation) + { + lowSaturationWeight = 0; highSaturationWeight = 1; + } + else + { + var saturationTone = (saturation - ToneLowSaturation) / (ToneHighSaturation - ToneLowSaturation); + highSaturationWeight = RealMath.SmoothStep(saturationTone); + lowSaturationWeight = 1 - highSaturationWeight; + } + + var hue = src[xx]; + if (toneAdjustments[hueIndex].HueUniformity > 0 || toneAdjustments[nextHueIndex].HueUniformity > 0) + { + var toneHue = toneAdjustments[hueIndex].ToneHue + Rotation; + if (toneHue < hue - 0.5f) + toneHue++; + else if (toneHue > hue + 0.5f) + toneHue--; + var nextToneHue = toneAdjustments[nextHueIndex].ToneHue + Rotation; + if (nextToneHue < hue - 0.5f) + nextToneHue++; + else if (nextToneHue > hue + 0.5f) + nextToneHue--; + + var toneHueWeight = toneAdjustments[hueIndex].HueUniformity * hueWeight; + var nextToneHueWeight = toneAdjustments[nextHueIndex].HueUniformity * nextHueWeight; + hue = hue * (1 - toneHueWeight - nextToneHueWeight) + + 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); + 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] * saturationAdjust; + + var i = src[xx + 2] * + ((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; } @@ -236,5 +384,34 @@ 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; + } + + private static void AddHueVector(float hue, float saturation, float weight, ref double x, ref double y) + { + 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) + { + 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/PhotoLocator/Controls/ColorToneControl.xaml.cs b/PhotoLocator/Controls/ColorToneControl.xaml.cs index c1c0bd5..11ec866 100644 --- a/PhotoLocator/Controls/ColorToneControl.xaml.cs +++ b/PhotoLocator/Controls/ColorToneControl.xaml.cs @@ -130,12 +130,12 @@ 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)); - 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); } @@ -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/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 57236d1..256d22e 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,25 +253,41 @@ 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 < _colorTones.Length; 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))); } } - readonly Rectangle[] _colorTones = new Rectangle[ColorToneAdjustOperation.NumberOfTones]; + Rectangle[]? _colorTones; + + public bool IsDualToneModeEnabled + { + get => _colorToneOperation.DualToneMode; + set + { + if (value == IsDualToneModeEnabled) + return; + var isAllSelected = ActiveToneIndex == _colorToneOperation.NumberOfTones; + _colorToneOperation.DualToneMode = value; + NotifyPropertyChanged(nameof(IsDualToneModeEnabled)); + NotifyPropertyChanged(nameof(ColorTones)); + ActiveToneIndex = isAllSelected ? _colorToneOperation.NumberOfTones : 0; + } + } public int ActiveToneIndex { @@ -279,14 +296,15 @@ 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++) + var numberOfTones_1 = _colorToneOperation.NumberOfTones - 1; + for (int i = 0; i < 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[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))); @@ -295,16 +313,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(); } @@ -317,12 +335,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(); } @@ -341,12 +359,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(); } @@ -359,17 +377,26 @@ 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(); } 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); + } } } @@ -410,7 +437,7 @@ public double ToneRotation _colorToneOperation.ResetToneAdjustments(); ToneRotation = 0; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); - ActiveToneIndex = ColorToneAdjustOperation.NumberOfTones; + ActiveToneIndex = _colorToneOperation.NumberOfTones; StartUpdateTimer(FirstParamChanged.ColorTone); }); @@ -506,9 +533,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]; @@ -517,8 +547,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); } 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); } diff --git a/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs b/PhotoLocatorTest/BitmapOperations/ColorToneAdjustOperationTest.cs index b256610..5313c7d 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_OppositeHueInterpolationShouldReducesSaturation() + { + 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 var hue, out var saturation, out var intensity); + Assert.IsLessThan(0.001f, saturation, $"Saturation was {saturation}"); + } } }