From f35bf0fe0d652887f89c655453716cc3ef925405 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sun, 8 Mar 2026 00:11:02 +0100 Subject: [PATCH 1/5] Astro stretch --- .../BitmapOperations/AstroStretchOperation.cs | 54 +++++++++++ PhotoLocator/BitmapOperations/FloatBitmap.cs | 3 +- PhotoLocator/JpegTransformCommands.cs | 2 +- PhotoLocator/LocalContrastView.xaml | 29 +++++- PhotoLocator/LocalContrastViewModel.cs | 97 ++++++++++++++----- PhotoLocator/MainWindow.xaml | 6 ++ .../AstroStretchOperationTest.cs | 38 ++++++++ 7 files changed, 199 insertions(+), 30 deletions(-) create mode 100644 PhotoLocator/BitmapOperations/AstroStretchOperation.cs create mode 100644 PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs diff --git a/PhotoLocator/BitmapOperations/AstroStretchOperation.cs b/PhotoLocator/BitmapOperations/AstroStretchOperation.cs new file mode 100644 index 0000000..9869fdf --- /dev/null +++ b/PhotoLocator/BitmapOperations/AstroStretchOperation.cs @@ -0,0 +1,54 @@ +using System; + +namespace PhotoLocator.BitmapOperations +{ + class AstroStretchOperation : OperationBase + { + public double Stretch { get; set; } = 10; + + public double BackgroundSmooth { get; set; } = 8; + + public override void Apply() + { + if (SrcBitmap is not null && DstBitmap != SrcBitmap) + DstBitmap.Assign(SrcBitmap); + if (Stretch > 0) + { + var s = (float)Math.Exp(-Stretch); + DstBitmap.ProcessElementWise(p => Math.Max(0, (s - 1) * p / ((2 * s - 1) * p - s))); + } + if (BackgroundSmooth > 0) + { + var background = ConvertToGrayscaleOperation.ConvertToGrayscale(DstBitmap); + IIRSmoothOperation.Apply(background, (float)Math.Exp(BackgroundSmooth)); + DstBitmap.ProcessElementWise(background, (p, b) => Math.Max(p - b, 0)); + } + } + + public static double OptimizeStretch(FloatBitmap srcBitmap) + { + const double TargetMean = 0.1; + const int SampleHeight = 100; + + var grayImage = ConvertToGrayscaleOperation.ConvertToGrayscale(srcBitmap); + srcBitmap = new FloatBitmap(srcBitmap.Width * SampleHeight / srcBitmap.Height, SampleHeight, 1); + BilinearResizeOperation.ApplyToPlaneParallel(grayImage, srcBitmap); + + double bestStretch = 1; + double bestMean = 0; + var op = new AstroStretchOperation { SrcBitmap = srcBitmap, DstBitmap = new(), BackgroundSmooth = 0 }; + for (double s = 1; s <= 20; s += 0.2) + { + op.Stretch = s; + op.Apply(); + var mean = op.DstBitmap.Mean(); + if (Math.Abs(mean - TargetMean) < Math.Abs(bestMean - TargetMean)) + { + bestStretch = s; + bestMean = mean; + } + } + return bestStretch; + } + } +} diff --git a/PhotoLocator/BitmapOperations/FloatBitmap.cs b/PhotoLocator/BitmapOperations/FloatBitmap.cs index 5194dc9..2c8594c 100644 --- a/PhotoLocator/BitmapOperations/FloatBitmap.cs +++ b/PhotoLocator/BitmapOperations/FloatBitmap.cs @@ -498,7 +498,8 @@ public void ProcessElementWise(FloatBitmap other, Func oper fixed (float* otherElements = &other.Elements[y, 0]) { int xx = 0; - for (var x = 0; x < Width; x++) + var width = Width; + for (var x = 0; x < width; x++) { var otherElement = otherElements[x]; elements[xx] = operation(elements[xx++], otherElement); diff --git a/PhotoLocator/JpegTransformCommands.cs b/PhotoLocator/JpegTransformCommands.cs index 7df6504..0ea522a 100644 --- a/PhotoLocator/JpegTransformCommands.cs +++ b/PhotoLocator/JpegTransformCommands.cs @@ -103,7 +103,7 @@ await Task.Run(() => using (var cursor = new MouseCursorOverride()) { (var image, metadata) = await Task.Run(() => LoadImageWithMetadataAsync(selectedItem)); - localContrastViewModel = new LocalContrastViewModel() { SourceBitmap = image }; + localContrastViewModel = new LocalContrastViewModel() { SourceBitmap = image, IsAstroModeEnabled = o as string == "Astro" }; } var window = new LocalContrastView(); window.Owner = Application.Current.MainWindow; diff --git a/PhotoLocator/LocalContrastView.xaml b/PhotoLocator/LocalContrastView.xaml index 4c958ef..20c3965 100644 --- a/PhotoLocator/LocalContrastView.xaml +++ b/PhotoLocator/LocalContrastView.xaml @@ -9,12 +9,13 @@ d:DataContext="{d:DesignInstance Type=local:LocalContrastViewModel, IsDesignTimeCreatable=True}" Background="Black" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" - Title="Local contrast, brightness and colors" Height="960" Width="800" WindowState="Maximized"> + Title="Local contrast, brightness and colors" Height="1000" Width="800" WindowState="Maximized"> + @@ -44,7 +45,7 @@ - + @@ -55,7 +56,29 @@ - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index fbd26f3..512d108 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -20,7 +20,7 @@ class LocalContrastViewModel : INotifyPropertyChanged, IImageZoomPreviewViewMode static readonly List _adjustmentClipboard = []; static readonly List _lastUsedValues = []; readonly DispatcherTimer _updateTimer; - readonly LaplacianFilterOperation _laplacianFilterOperation = new() { SrcBitmap = new() }; + readonly LaplacianFilterOperation _laplacianFilterOperation = new(); readonly IncreaseLocalContrastOperation _localContrastOperation = new() { DstBitmap = new() }; readonly ColorToneAdjustOperation _colorToneOperation = new(); Task _previewTask = Task.CompletedTask; @@ -64,12 +64,13 @@ public BitmapSource? SourceBitmap if (value is not null) { Mouse.OverrideCursor = Cursors.AppStarting; - _laplacianFilterOperation.SrcBitmap.Assign(value, FloatBitmap.DefaultMonitorGamma); + _sourceFloatBitmap.Assign(value, FloatBitmap.DefaultMonitorGamma); _updateTimer.Start(); } field = value; } } + readonly FloatBitmap _sourceFloatBitmap = new(); public BitmapSource? PreviewPictureSource { @@ -92,6 +93,41 @@ public int PreviewZoom public ICommand ZoomInCommand => new RelayCommand(o => PreviewZoom = Math.Min(PreviewZoom + 1, 4)); public ICommand ZoomOutCommand => new RelayCommand(o => PreviewZoom = Math.Max(PreviewZoom - 1, 0)); + public bool IsAstroModeEnabled + { + get; + set + { + if (value && SetProperty(ref field, value) && SourceBitmap is not null) + AstroStretch = AstroStretchOperation.OptimizeStretch(_sourceFloatBitmap); + } + } + + public const double DefaultAstroStretch = 10; + public double AstroStretch + { + get; + set + { + if (SetProperty(ref field, value)) + StartUpdateTimer(true, true); + } + } = DefaultAstroStretch; + public ICommand ResetAstroStretchCommand => new RelayCommand(o => AstroStretch = DefaultAstroStretch); + + public const double DefaultBackgroundRemovalSmooth = 8; + public double BackgroundRemovalSmooth + { + get; + set + { + if (SetProperty(ref field, value)) + StartUpdateTimer(true, true); + } + } = DefaultBackgroundRemovalSmooth; + public ICommand ResetBackgroundRemovalSmoothCommand => new RelayCommand(o => BackgroundRemovalSmooth = DefaultBackgroundRemovalSmooth); + + public const double DefaultHighlightStrength = 10; public double HighlightStrength { get; @@ -101,11 +137,9 @@ public double HighlightStrength StartUpdateTimer(false, true); } } = DefaultHighlightStrength; - - public const double DefaultHighlightStrength = 10; - public ICommand ResetHighlightCommand => new RelayCommand(o => HighlightStrength = DefaultHighlightStrength); + public const double DefaultShadowStrength = 10; public double ShadowStrength { get; @@ -115,11 +149,9 @@ public double ShadowStrength StartUpdateTimer(false, true); } } = DefaultShadowStrength; - - public const double DefaultShadowStrength = 10; - public ICommand ResetShadowCommand => new RelayCommand(o => ShadowStrength = DefaultShadowStrength); + public const double DefaultMaxStretch = 50; public double MaxStretch { get; @@ -129,11 +161,9 @@ public double MaxStretch StartUpdateTimer(false, true); } } = DefaultMaxStretch; - - public const double DefaultMaxStretch = 50; - public ICommand ResetMaxStretchCommand => new RelayCommand(o => MaxStretch = DefaultMaxStretch); + public const double DefaultOutlierReductionStrength = 10; public double OutlierReductionStrength { get; @@ -143,11 +173,9 @@ public double OutlierReductionStrength StartUpdateTimer(false, true); } } = DefaultOutlierReductionStrength; - - public const double DefaultOutlierReductionStrength = 10; - public ICommand ResetOutlierReductionCommand => new RelayCommand(o => OutlierReductionStrength = DefaultOutlierReductionStrength); + public const double DefaultContrast = 1; public double Contrast { get; @@ -157,11 +185,9 @@ public double Contrast StartUpdateTimer(false, true); } } = DefaultContrast; - - public const double DefaultContrast = 1; - public ICommand ResetContrastCommand => new RelayCommand(o => Contrast = DefaultContrast); + public const double DefaultToneMapping = 1; public double ToneMapping { get; @@ -175,11 +201,9 @@ public double ToneMapping } } } = DefaultToneMapping; - - public const double DefaultToneMapping = 1; - public ICommand ResetToneMappingCommand => new RelayCommand(o => ToneMapping = DefaultToneMapping); + public const double DefaultDetailHandling = 1; public double DetailHandling { get; @@ -189,9 +213,6 @@ public double DetailHandling StartUpdateTimer(true, true); } } = DefaultDetailHandling; - - public const double DefaultDetailHandling = 1; - public ICommand ResetDetailHandlingCommand => new RelayCommand(o => DetailHandling = DefaultDetailHandling); public ColorToneAdjustOperation.ToneAdjustment[] ToneAdjustments => _colorToneOperation.ToneAdjustments; @@ -340,6 +361,8 @@ public double ToneRotation public ICommand ResetCommand => new RelayCommand(o => { + AstroStretch = DefaultAstroStretch; + BackgroundRemovalSmooth = DefaultBackgroundRemovalSmooth; HighlightStrength = DefaultHighlightStrength; ShadowStrength = DefaultShadowStrength; MaxStretch = DefaultMaxStretch; @@ -368,6 +391,8 @@ public void SaveLastUsedValues() private void StoreAdjustmentValues(List valueStore) { valueStore.Clear(); + valueStore.Add(AstroStretch); + valueStore.Add(BackgroundRemovalSmooth); valueStore.Add(HighlightStrength); valueStore.Add(ShadowStrength); valueStore.Add(MaxStretch); @@ -388,6 +413,8 @@ private void StoreAdjustmentValues(List valueStore) private void RestoreAdjustmentValues(List valueStore) { int a = 0; + AstroStretch = valueStore[a++]; + BackgroundRemovalSmooth = valueStore[a++]; HighlightStrength = valueStore[a++]; ShadowStrength = valueStore[a++]; MaxStretch = valueStore[a++]; @@ -419,6 +446,24 @@ private void StartUpdateTimer(bool laplacianPyramidParamsChanged, bool localCont _updateTimer.Start(); } + void ApplyAstroStretchOperation() + { + if (IsAstroModeEnabled && (AstroStretch > 0 || BackgroundRemovalSmooth > 0)) + { + var astroStretch = new AstroStretchOperation() + { + SrcBitmap = _sourceFloatBitmap, + DstBitmap = new(), + Stretch = AstroStretch, + BackgroundSmooth = BackgroundRemovalSmooth, + }; + astroStretch.Apply(); + _laplacianFilterOperation.SrcBitmap = astroStretch.DstBitmap; + } + else + _laplacianFilterOperation.SrcBitmap = _sourceFloatBitmap; + } + private void ApplyLaplacianFilterOperation() { if (DetailHandling != 1 || ToneMapping != 1) @@ -472,6 +517,7 @@ public async Task UpdatePreviewAsync() _localContrastOperation.SourceChanged(); _laplacianPyramidParamsChanged = false; } + ApplyAstroStretchOperation(); ApplyLaplacianFilterOperation(); if (SourceBitmap is null || _updateTimer.IsEnabled) return; @@ -495,7 +541,7 @@ public void ShowSourceHistogram() { if (SourceBitmap is null) return; - (_, var histogramTask) = _laplacianFilterOperation.SrcBitmap.ToBitmapSourceWithHistogram(SourceBitmap.DpiX, SourceBitmap.DpiY, FloatBitmap.DefaultMonitorGamma); + (_, var histogramTask) = _sourceFloatBitmap.ToBitmapSourceWithHistogram(SourceBitmap.DpiX, SourceBitmap.DpiY, FloatBitmap.DefaultMonitorGamma); histogramTask.ContinueWith(task => SetHistogram(task.Result), TaskScheduler.Current); }); } @@ -517,7 +563,8 @@ public BitmapSource ApplyOperations(BitmapSource source) { if (IsNoOperation) return source; - _laplacianFilterOperation.SrcBitmap.Assign(source, FloatBitmap.DefaultMonitorGamma); + _sourceFloatBitmap.Assign(source, FloatBitmap.DefaultMonitorGamma); + ApplyAstroStretchOperation(); _laplacianFilterOperation.SourceChanged(); ApplyLaplacianFilterOperation(); _localContrastOperation.SourceChanged(); diff --git a/PhotoLocator/MainWindow.xaml b/PhotoLocator/MainWindow.xaml index 774a96a..18f4e28 100644 --- a/PhotoLocator/MainWindow.xaml +++ b/PhotoLocator/MainWindow.xaml @@ -35,6 +35,7 @@ + @@ -218,6 +219,11 @@ + + + + + diff --git a/PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs b/PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs new file mode 100644 index 0000000..d35be0f --- /dev/null +++ b/PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs @@ -0,0 +1,38 @@ +using PhotoLocator.PictureFileFormats; +using System.Diagnostics; +using System.Windows.Media.Imaging; + +namespace PhotoLocator.BitmapOperations +{ + [TestClass] + public class AstroStretchOperationTest + { + [TestMethod] + public void Apply_AstroStretchOperation() + { + var source = BitmapDecoder.Create(File.OpenRead(@"TestData\2022-06-17_19.03.02.jpg"), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad).Frames[0]; + + var sw = Stopwatch.StartNew(); + var sourceFloat = new FloatBitmap(source, FloatBitmap.DefaultMonitorGamma); + Console.WriteLine(sw.ElapsedMilliseconds); + + sw.Restart(); + var op = new AstroStretchOperation() + { + SrcBitmap = sourceFloat, + DstBitmap = new FloatBitmap(), + }; + op.Apply(); + Console.WriteLine(sw.ElapsedMilliseconds); + + sw.Restart(); + var result = op.DstBitmap.ToBitmapSource(source.DpiX, source.DpiY, FloatBitmap.DefaultMonitorGamma); + Console.WriteLine(sw.ElapsedMilliseconds); + + Assert.AreEqual(0.0026545193517195226, op.DstBitmap.Mean(), 0.000001); +#if DEBUG + GeneralFileFormatHandler.SaveToFile(result, "astroStretch.png"); +#endif + } + } +} From dda1cc3e7e6ae37a90ea1f47ae0a68a023a2283e Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 2 May 2026 21:08:06 +0200 Subject: [PATCH 2/5] sun altitude, fix reset laplacian --- PhotoLocator/Helpers/CelestialCalculator.cs | 4 +- PhotoLocator/JpegTransformCommands.cs | 4 +- PhotoLocator/LocalContrastViewModel.cs | 79 +++++++++---------- PhotoLocator/MainViewModel.cs | 5 +- PhotoLocator/Metadata/ExifHandler.cs | 4 +- PhotoLocator/Metadata/ExifTool.cs | 6 +- .../GeneralFileFormatHandler.cs | 10 +++ PhotoLocator/PictureItemViewModel.cs | 8 +- 8 files changed, 64 insertions(+), 56 deletions(-) diff --git a/PhotoLocator/Helpers/CelestialCalculator.cs b/PhotoLocator/Helpers/CelestialCalculator.cs index 3b9cb58..1efc944 100644 --- a/PhotoLocator/Helpers/CelestialCalculator.cs +++ b/PhotoLocator/Helpers/CelestialCalculator.cs @@ -33,10 +33,10 @@ public static (DateTime? Sunrise, double? RiseAzimuth, DateTime? Sunset, double? return (sunrise, sunriseAzimuth, sunset, sunsetAzimuth); } - public static double? GetSunPosition(Location location, DateTime time) + public static (double Azimuth, double Altitude) GetSunPosition(Location location, DateTime time) { var pos = SunCalc.GetSunPosition(time, location.Latitude, location.Longitude); - return pos.Altitude < 0 ? null : SunCalcNetAzimuthRadiansToDegrees(pos.Azimuth); + return (SunCalcNetAzimuthRadiansToDegrees(pos.Azimuth), pos.Altitude * 180 / Math.PI); } /// diff --git a/PhotoLocator/JpegTransformCommands.cs b/PhotoLocator/JpegTransformCommands.cs index 0ea522a..ee1c5a1 100644 --- a/PhotoLocator/JpegTransformCommands.cs +++ b/PhotoLocator/JpegTransformCommands.cs @@ -103,7 +103,7 @@ await Task.Run(() => using (var cursor = new MouseCursorOverride()) { (var image, metadata) = await Task.Run(() => LoadImageWithMetadataAsync(selectedItem)); - localContrastViewModel = new LocalContrastViewModel() { SourceBitmap = image, IsAstroModeEnabled = o as string == "Astro" }; + localContrastViewModel = new LocalContrastViewModel() { IsAstroModeEnabled = o as string == "Astro", SourceBitmap = image }; } var window = new LocalContrastView(); window.Owner = Application.Current.MainWindow; @@ -131,7 +131,7 @@ await Task.Run(() => private static async Task<(BitmapSource, BitmapMetadata?)> LoadImageWithMetadataAsync(PictureItemViewModel item) { BitmapMetadata? metadata = null; - var image = await item.LoadPreviewAsync(default, int.MaxValue, preservePixelFormat: true); + var image = await item.LoadPreviewAsync(default, preservePixelFormat: true); try { using var file = File.OpenRead(item.FullPath); diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 512d108..0a5bd15 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -17,6 +17,8 @@ namespace PhotoLocator { class LocalContrastViewModel : INotifyPropertyChanged, IImageZoomPreviewViewModel { + enum FirstParamChanged { Astro, LaplacianPyramid, LocalContrast, ColorTone, None } + static readonly List _adjustmentClipboard = []; static readonly List _lastUsedValues = []; readonly DispatcherTimer _updateTimer; @@ -24,7 +26,7 @@ class LocalContrastViewModel : INotifyPropertyChanged, IImageZoomPreviewViewMode readonly IncreaseLocalContrastOperation _localContrastOperation = new() { DstBitmap = new() }; readonly ColorToneAdjustOperation _colorToneOperation = new(); Task _previewTask = Task.CompletedTask; - bool _laplacianPyramidParamsChanged, _localContrastParamsChanged; + FirstParamChanged _firstParamChanged = FirstParamChanged.None; public LocalContrastViewModel() { @@ -40,7 +42,7 @@ public LocalContrastViewModel() void NotifyPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); } bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) @@ -65,6 +67,8 @@ public BitmapSource? SourceBitmap { Mouse.OverrideCursor = Cursors.AppStarting; _sourceFloatBitmap.Assign(value, FloatBitmap.DefaultMonitorGamma); + if (IsAstroModeEnabled) + ResetCommand.Execute(null); _updateTimer.Start(); } field = value; @@ -96,24 +100,19 @@ public int PreviewZoom public bool IsAstroModeEnabled { get; - set - { - if (value && SetProperty(ref field, value) && SourceBitmap is not null) - AstroStretch = AstroStretchOperation.OptimizeStretch(_sourceFloatBitmap); - } + set => SetProperty(ref field, value); } - public const double DefaultAstroStretch = 10; public double AstroStretch { get; set { if (SetProperty(ref field, value)) - StartUpdateTimer(true, true); + StartUpdateTimer(FirstParamChanged.Astro); } - } = DefaultAstroStretch; - public ICommand ResetAstroStretchCommand => new RelayCommand(o => AstroStretch = DefaultAstroStretch); + } + public ICommand ResetAstroStretchCommand => new RelayCommand(o => AstroStretch = IsAstroModeEnabled ? AstroStretchOperation.OptimizeStretch(_sourceFloatBitmap) : 0); public const double DefaultBackgroundRemovalSmooth = 8; public double BackgroundRemovalSmooth @@ -122,7 +121,7 @@ public double BackgroundRemovalSmooth set { if (SetProperty(ref field, value)) - StartUpdateTimer(true, true); + StartUpdateTimer(FirstParamChanged.Astro); } } = DefaultBackgroundRemovalSmooth; public ICommand ResetBackgroundRemovalSmoothCommand => new RelayCommand(o => BackgroundRemovalSmooth = DefaultBackgroundRemovalSmooth); @@ -134,7 +133,7 @@ public double HighlightStrength set { if (SetProperty(ref field, RealMath.Clamp(value, 0, 100))) - StartUpdateTimer(false, true); + StartUpdateTimer(FirstParamChanged.LocalContrast); } } = DefaultHighlightStrength; public ICommand ResetHighlightCommand => new RelayCommand(o => HighlightStrength = DefaultHighlightStrength); @@ -146,7 +145,7 @@ public double ShadowStrength set { if (SetProperty(ref field, RealMath.Clamp(value, 0, 100))) - StartUpdateTimer(false, true); + StartUpdateTimer(FirstParamChanged.LocalContrast); } } = DefaultShadowStrength; public ICommand ResetShadowCommand => new RelayCommand(o => ShadowStrength = DefaultShadowStrength); @@ -158,10 +157,10 @@ public double MaxStretch set { if (SetProperty(ref field, RealMath.Clamp(value, 0, 100))) - StartUpdateTimer(false, true); + StartUpdateTimer(FirstParamChanged.LocalContrast); } } = DefaultMaxStretch; - public ICommand ResetMaxStretchCommand => new RelayCommand(o => MaxStretch = DefaultMaxStretch); + public ICommand ResetMaxStretchCommand => new RelayCommand(o => MaxStretch = ToneMapping > 1 || IsAstroModeEnabled ? 100 : DefaultMaxStretch); public const double DefaultOutlierReductionStrength = 10; public double OutlierReductionStrength @@ -170,7 +169,7 @@ public double OutlierReductionStrength set { if (SetProperty(ref field, value)) - StartUpdateTimer(false, true); + StartUpdateTimer(FirstParamChanged.LocalContrast); } } = DefaultOutlierReductionStrength; public ICommand ResetOutlierReductionCommand => new RelayCommand(o => OutlierReductionStrength = DefaultOutlierReductionStrength); @@ -182,7 +181,7 @@ public double Contrast set { if (SetProperty(ref field, value)) - StartUpdateTimer(false, true); + StartUpdateTimer(FirstParamChanged.LocalContrast); } } = DefaultContrast; public ICommand ResetContrastCommand => new RelayCommand(o => Contrast = DefaultContrast); @@ -197,7 +196,7 @@ public double ToneMapping { if (value > 1) MaxStretch = 100; - StartUpdateTimer(true, true); + StartUpdateTimer(FirstParamChanged.LaplacianPyramid); } } } = DefaultToneMapping; @@ -210,7 +209,7 @@ public double DetailHandling set { if (SetProperty(ref field, value)) - StartUpdateTimer(true, true); + StartUpdateTimer(FirstParamChanged.LaplacianPyramid); } } = DefaultDetailHandling; public ICommand ResetDetailHandlingCommand => new RelayCommand(o => DetailHandling = DefaultDetailHandling); @@ -279,7 +278,7 @@ public float HueAdjust NotifyPropertyChanged(); } else if (SetProperty(ref _colorToneOperation.ToneAdjustments[ActiveToneIndex].AdjustHue, value)) - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); } } @@ -297,7 +296,7 @@ public float SaturationAdjust NotifyPropertyChanged(); } else if (SetProperty(ref _colorToneOperation.ToneAdjustments[ActiveToneIndex].AdjustSaturation, value)) - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); } } @@ -315,7 +314,7 @@ public float IntensityAdjust NotifyPropertyChanged(); } else if (SetProperty(ref _colorToneOperation.ToneAdjustments[ActiveToneIndex].AdjustIntensity, value)) - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); } } @@ -333,7 +332,7 @@ public float HueUniformity NotifyPropertyChanged(); } else if (SetProperty(ref _colorToneOperation.ToneAdjustments[ActiveToneIndex].HueUniformity, value)) - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); } } @@ -347,7 +346,7 @@ public double ToneRotation if (SetProperty(ref field, RealMath.Clamp(value, -0.5, 0.5))) { UpdateColorTones(); - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); } } } @@ -361,20 +360,20 @@ public double ToneRotation public ICommand ResetCommand => new RelayCommand(o => { - AstroStretch = DefaultAstroStretch; + ResetAstroStretchCommand.Execute(null); BackgroundRemovalSmooth = DefaultBackgroundRemovalSmooth; HighlightStrength = DefaultHighlightStrength; ShadowStrength = DefaultShadowStrength; - MaxStretch = DefaultMaxStretch; OutlierReductionStrength = DefaultOutlierReductionStrength; Contrast = DefaultContrast; ToneMapping = DefaultToneMapping; DetailHandling = DefaultDetailHandling; + ResetMaxStretchCommand.Execute(null); _colorToneOperation.ResetToneAdjustments(); ToneRotation = 0; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); ActiveToneIndex = ColorToneAdjustOperation.NumberOfTones; - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); }); public ICommand CopyAdjustmentsCommand => new RelayCommand(o => StoreAdjustmentValues(_adjustmentClipboard)); @@ -432,16 +431,16 @@ private void RestoreAdjustmentValues(List valueStore) } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); ActiveToneIndex = 0; - StartUpdateTimer(false, false); + StartUpdateTimer(FirstParamChanged.ColorTone); if (a != valueStore.Count) throw new InvalidOperationException("Unexpected number of adjustments"); } - private void StartUpdateTimer(bool laplacianPyramidParamsChanged, bool localContrastParamsChanged) + private void StartUpdateTimer(FirstParamChanged firstParamChanged) { Mouse.OverrideCursor = Cursors.AppStarting; - _laplacianPyramidParamsChanged |= laplacianPyramidParamsChanged; - _localContrastParamsChanged |= localContrastParamsChanged; + if (firstParamChanged < _firstParamChanged) + _firstParamChanged = firstParamChanged; _updateTimer.Stop(); _updateTimer.Start(); } @@ -458,6 +457,7 @@ void ApplyAstroStretchOperation() BackgroundSmooth = BackgroundRemovalSmooth, }; astroStretch.Apply(); + _laplacianFilterOperation.SourceChanged(); _laplacianFilterOperation.SrcBitmap = astroStretch.DstBitmap; } else @@ -507,16 +507,13 @@ public async Task UpdatePreviewAsync() } await (_previewTask = Task.Run(async () => { - if (_laplacianPyramidParamsChanged || _localContrastParamsChanged) - { - _localContrastParamsChanged = false; - _colorToneOperation.SourceChanged(); - } - if (_laplacianPyramidParamsChanged) - { + if (_firstParamChanged < FirstParamChanged.LaplacianPyramid) + _laplacianFilterOperation.SourceChanged(); + if (_firstParamChanged < FirstParamChanged.LocalContrast) _localContrastOperation.SourceChanged(); - _laplacianPyramidParamsChanged = false; - } + if (_firstParamChanged < FirstParamChanged.ColorTone) + _colorToneOperation.SourceChanged(); + _firstParamChanged = FirstParamChanged.None; ApplyAstroStretchOperation(); ApplyLaplacianFilterOperation(); if (SourceBitmap is null || _updateTimer.IsEnabled) diff --git a/PhotoLocator/MainViewModel.cs b/PhotoLocator/MainViewModel.cs index e456dd4..1674416 100644 --- a/PhotoLocator/MainViewModel.cs +++ b/PhotoLocator/MainViewModel.cs @@ -668,8 +668,7 @@ void AddLineSeg(double azimuth, Color color, string text) if (sun.Sunset.HasValue) AddLineSeg(sun.SetAzimuth!.Value, Colors.Yellow, $"Sunset: {sun.Sunset.Value.ToLocalTime()}\nAzimuth: {sun.SetAzimuth:F0}°"); var sunPos = CelestialCalculator.GetSunPosition(MapCenter, now); - if (sunPos.HasValue) - AddLineSeg(sunPos.Value, Colors.Orange, $"Sun now:\nAzimuth: {sunPos:F1}°"); + AddLineSeg(sunPos.Azimuth, sunPos.Altitude > 0 ? Colors.Orange : Colors.OrangeRed, $"Sun now:\nAzimuth: {sunPos.Azimuth:F0}°, altitude {sunPos.Altitude:F0}°"); var moon = CelestialCalculator.GetMoonRiseSet(MapCenter, SunAndMoonDate); if (moon.Moonrise.HasValue) @@ -678,7 +677,7 @@ void AddLineSeg(double azimuth, Color color, string text) AddLineSeg(moon.SetAzimuth!.Value, Colors.LightGray, $"Moonset: {moon.Moonset.Value.ToLocalTime()}\n{moon.SetIllumination * 100:F0}%, azimuth: {moon.SetAzimuth:F0}°"); var moonPos = CelestialCalculator.GetMoonPosition(MapCenter, now); if (moonPos.Azimuth.HasValue) - AddLineSeg(moonPos.Azimuth.Value, Colors.Gray, $"Moon now: {moonPos.Illumination * 100:F0}%\nAzimuth: {moonPos.Azimuth:F1}°"); + AddLineSeg(moonPos.Azimuth.Value, Colors.Gray, $"Moon now: {moonPos.Illumination * 100:F0}%\nAzimuth: {moonPos.Azimuth:F0}°"); } public static ICommand AboutCommand => new RelayCommand(o => diff --git a/PhotoLocator/Metadata/ExifHandler.cs b/PhotoLocator/Metadata/ExifHandler.cs index ce42ffa..0ffad3a 100644 --- a/PhotoLocator/Metadata/ExifHandler.cs +++ b/PhotoLocator/Metadata/ExifHandler.cs @@ -656,6 +656,8 @@ internal static string FormatTimestampForDisplay(DateTimeOffset timestamp) _ => Rotation.Rotate0 }; var timeStamp = DecodeTimeStamp(metadata, file); + if (GeneralFileFormatHandler.IsRawFile(fileName)) + return (GetGeotag(metadata), timeStamp, GetMetadataString(metadata, 0, 0, timeStamp), orientation); return (GetGeotag(metadata), timeStamp, GetMetadataString(metadata, frame.PixelWidth, frame.PixelHeight, timeStamp), orientation); } catch (NotSupportedException) @@ -664,6 +666,6 @@ internal static string FormatTimestampForDisplay(DateTimeOffset timestamp) throw; return ExifTool.DecodeMetadata(fileName, exifToolPath); } - } + } } } \ No newline at end of file diff --git a/PhotoLocator/Metadata/ExifTool.cs b/PhotoLocator/Metadata/ExifTool.cs index c5cf980..4852bd0 100644 --- a/PhotoLocator/Metadata/ExifTool.cs +++ b/PhotoLocator/Metadata/ExifTool.cs @@ -144,7 +144,8 @@ public static (Location? Location, DateTimeOffset? TimeStamp, string Metadata, R !metadata.TryGetValue("SubSecDateTimeOriginal", out timeStampStr) && !metadata.TryGetValue("CreationDate", out timeStampStr) && !metadata.TryGetValue("CreateDate", out timeStampStr) && - !metadata.TryGetValue("DateTimeOriginal", out timeStampStr)) + !metadata.TryGetValue("DateTimeOriginal", out timeStampStr) && + !metadata.TryGetValue("ModifyDate", out timeStampStr)) return null; var fractionStart = timeStampStr.IndexOf('.', StringComparison.Ordinal); if (fractionStart > 0) @@ -166,7 +167,8 @@ public static (Location? Location, DateTimeOffset? TimeStamp, string Metadata, R return timestampOffset; } var timeZone = metadata.TryGetValue("FileType", out var fileType) && fileType == "JPEG" ? DateTimeStyles.AssumeLocal : DateTimeStyles.AssumeUniversal; - if (DateTime.TryParseExact(timeStampStr, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces | timeZone, out var timestamp)) + if (DateTime.TryParseExact(timeStampStr, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces | timeZone, out var timestamp) || + DateTime.TryParse(timeStampStr, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces | timeZone, out timestamp)) return timestamp; return null; } diff --git a/PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs b/PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs index fd4f29f..d561bd2 100644 --- a/PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs +++ b/PhotoLocator/PictureFileFormats/GeneralFileFormatHandler.cs @@ -14,6 +14,16 @@ static class GeneralFileFormatHandler static string? _jpegliPath; static bool _jpegliChecked; + public static bool IsRawFile(string fileName) + { + return Path.GetExtension(fileName).ToLowerInvariant() is ".cr2" or ".cr3" or ".dng" or ".arw" or ".nef"; + } + + public static bool IsVideoFile(string fileName) + { + return Path.GetExtension(fileName).ToLowerInvariant() is ".mp4" or ".mov" or ".avi"; + } + public static BitmapSource LoadFromStream(Stream source, Rotation rotation, int maxPixelWidth, bool preservePixelFormat, CancellationToken ct) { var bitmap = new BitmapImage(); diff --git a/PhotoLocator/PictureItemViewModel.cs b/PhotoLocator/PictureItemViewModel.cs index c02e1cc..861160a 100644 --- a/PhotoLocator/PictureItemViewModel.cs +++ b/PhotoLocator/PictureItemViewModel.cs @@ -105,9 +105,7 @@ public bool IsVideo { if (IsDirectory) return false; - var ext = Path.GetExtension(FullPath).ToLowerInvariant(); - var isVideo = ext is ".mp4" or ".mov" or ".avi"; - return isVideo; + return GeneralFileFormatHandler.IsVideoFile(FullPath); } } @@ -254,7 +252,7 @@ private async Task LoadMetadataAsync(CancellationToken ct) } } - public async Task LoadPreviewAsync(CancellationToken ct, int maxWidth = int.MaxValue, bool preservePixelFormat = false, string? skipTo = null) + public async Task LoadPreviewAsync(CancellationToken ct, bool preservePixelFormat = false, string? skipTo = null) { try { @@ -262,7 +260,7 @@ private async Task LoadMetadataAsync(CancellationToken ct) await LoadMetadataAsync(ct); Log.Write("Loading preview of " + Name); var sw = Stopwatch.StartNew(); - var preview = LoadPreviewInternal(maxWidth, preservePixelFormat, skipTo, ct); + var preview = LoadPreviewInternal(int.MaxValue, preservePixelFormat, skipTo, ct); Log.Write($"Loaded preview of {Name} in {sw.ElapsedMilliseconds} ms"); return preview; } From 5b0e89a0edb78212a49a8fcb7c62328f66002537 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 2 May 2026 22:54:51 +0200 Subject: [PATCH 3/5] Black point --- .../BitmapOperations/AstroStretchOperation.cs | 15 ++++++++++++++- PhotoLocator/LocalContrastView.xaml | 10 ++++++++++ PhotoLocator/LocalContrastViewModel.cs | 17 ++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/PhotoLocator/BitmapOperations/AstroStretchOperation.cs b/PhotoLocator/BitmapOperations/AstroStretchOperation.cs index 9869fdf..6b7a4dd 100644 --- a/PhotoLocator/BitmapOperations/AstroStretchOperation.cs +++ b/PhotoLocator/BitmapOperations/AstroStretchOperation.cs @@ -8,6 +8,8 @@ class AstroStretchOperation : OperationBase public double BackgroundSmooth { get; set; } = 8; + public double BlackPoint { get; set; } + public override void Apply() { if (SrcBitmap is not null && DstBitmap != SrcBitmap) @@ -21,7 +23,18 @@ public override void Apply() { var background = ConvertToGrayscaleOperation.ConvertToGrayscale(DstBitmap); IIRSmoothOperation.Apply(background, (float)Math.Exp(BackgroundSmooth)); - DstBitmap.ProcessElementWise(background, (p, b) => Math.Max(p - b, 0)); + if (BlackPoint > 0) + { + var bp = (float)BlackPoint; + DstBitmap.ProcessElementWise(background, (p, b) => Math.Max(p - b - bp, 0)); + } + else + DstBitmap.ProcessElementWise(background, (p, b) => Math.Max(p - b, 0)); + } + else if (BlackPoint > 0) + { + var bp = (float)BlackPoint; + DstBitmap.ProcessElementWise(p => Math.Max(p - bp, 0)); } } diff --git a/PhotoLocator/LocalContrastView.xaml b/PhotoLocator/LocalContrastView.xaml index 20c3965..aacf872 100644 --- a/PhotoLocator/LocalContrastView.xaml +++ b/PhotoLocator/LocalContrastView.xaml @@ -77,6 +77,16 @@ + + + + + + + + + + diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 0a5bd15..5ea4a3d 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -126,6 +126,17 @@ public double BackgroundRemovalSmooth } = DefaultBackgroundRemovalSmooth; public ICommand ResetBackgroundRemovalSmoothCommand => new RelayCommand(o => BackgroundRemovalSmooth = DefaultBackgroundRemovalSmooth); + public double BlackPoint + { + get; + set + { + if (SetProperty(ref field, value)) + StartUpdateTimer(FirstParamChanged.Astro); + } + } + public ICommand ResetBlackPointCommand => new RelayCommand(o => BlackPoint = 0); + public const double DefaultHighlightStrength = 10; public double HighlightStrength { @@ -362,6 +373,7 @@ public double ToneRotation { ResetAstroStretchCommand.Execute(null); BackgroundRemovalSmooth = DefaultBackgroundRemovalSmooth; + BlackPoint = 0; HighlightStrength = DefaultHighlightStrength; ShadowStrength = DefaultShadowStrength; OutlierReductionStrength = DefaultOutlierReductionStrength; @@ -392,6 +404,7 @@ private void StoreAdjustmentValues(List valueStore) valueStore.Clear(); valueStore.Add(AstroStretch); valueStore.Add(BackgroundRemovalSmooth); + valueStore.Add(BlackPoint); valueStore.Add(HighlightStrength); valueStore.Add(ShadowStrength); valueStore.Add(MaxStretch); @@ -414,6 +427,7 @@ private void RestoreAdjustmentValues(List valueStore) int a = 0; AstroStretch = valueStore[a++]; BackgroundRemovalSmooth = valueStore[a++]; + BlackPoint = valueStore[a++]; HighlightStrength = valueStore[a++]; ShadowStrength = valueStore[a++]; MaxStretch = valueStore[a++]; @@ -447,7 +461,7 @@ private void StartUpdateTimer(FirstParamChanged firstParamChanged) void ApplyAstroStretchOperation() { - if (IsAstroModeEnabled && (AstroStretch > 0 || BackgroundRemovalSmooth > 0)) + if (IsAstroModeEnabled && (AstroStretch > 0 || BackgroundRemovalSmooth > 0 || BlackPoint > 0)) { var astroStretch = new AstroStretchOperation() { @@ -455,6 +469,7 @@ void ApplyAstroStretchOperation() DstBitmap = new(), Stretch = AstroStretch, BackgroundSmooth = BackgroundRemovalSmooth, + BlackPoint = BlackPoint, }; astroStretch.Apply(); _laplacianFilterOperation.SourceChanged(); From 5ee1a457db9fb5f79450da339861ee8e041775df Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 9 May 2026 23:14:56 +0200 Subject: [PATCH 4/5] Hide Local adjustments label --- PhotoLocator/LocalContrastView.xaml | 3 ++- PhotoLocator/LocalContrastViewModel.cs | 2 ++ PhotoLocator/MapDisplay/MapView.xaml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/PhotoLocator/LocalContrastView.xaml b/PhotoLocator/LocalContrastView.xaml index aacf872..6a01e77 100644 --- a/PhotoLocator/LocalContrastView.xaml +++ b/PhotoLocator/LocalContrastView.xaml @@ -55,7 +55,8 @@ - + diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 5ea4a3d..18e928e 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -103,6 +103,8 @@ public bool IsAstroModeEnabled set => SetProperty(ref field, value); } + public bool IsLocalAdjustmentsLabelVisible => !IsAstroModeEnabled; + public double AstroStretch { get; diff --git a/PhotoLocator/MapDisplay/MapView.xaml b/PhotoLocator/MapDisplay/MapView.xaml index 3202b8a..83fab60 100644 --- a/PhotoLocator/MapDisplay/MapView.xaml +++ b/PhotoLocator/MapDisplay/MapView.xaml @@ -212,7 +212,7 @@ + SelectedDate="{Binding SunAndMoonDate}" /> Date: Sun, 10 May 2026 15:56:04 +0200 Subject: [PATCH 5/5] cr --- PhotoLocator/BitmapOperations/AstroStretchOperation.cs | 2 +- PhotoLocator/JpegTransformCommands.cs | 4 +++- PhotoLocator/LocalContrastViewModel.cs | 3 ++- PhotoLocator/MainWindow.xaml | 4 ++-- .../BitmapOperations/AstroStretchOperationTest.cs | 2 +- PhotoLocatorTest/PhotoLocatorTest.csproj | 6 +++--- .../PhotoLocator.PhotoshopImageLoader.csproj | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/PhotoLocator/BitmapOperations/AstroStretchOperation.cs b/PhotoLocator/BitmapOperations/AstroStretchOperation.cs index 6b7a4dd..4aa1007 100644 --- a/PhotoLocator/BitmapOperations/AstroStretchOperation.cs +++ b/PhotoLocator/BitmapOperations/AstroStretchOperation.cs @@ -44,7 +44,7 @@ public static double OptimizeStretch(FloatBitmap srcBitmap) const int SampleHeight = 100; var grayImage = ConvertToGrayscaleOperation.ConvertToGrayscale(srcBitmap); - srcBitmap = new FloatBitmap(srcBitmap.Width * SampleHeight / srcBitmap.Height, SampleHeight, 1); + srcBitmap = new FloatBitmap(Math.Max(1, srcBitmap.Width * SampleHeight / srcBitmap.Height), SampleHeight, 1); BilinearResizeOperation.ApplyToPlaneParallel(grayImage, srcBitmap); double bestStretch = 1; diff --git a/PhotoLocator/JpegTransformCommands.cs b/PhotoLocator/JpegTransformCommands.cs index ee1c5a1..ab494d7 100644 --- a/PhotoLocator/JpegTransformCommands.cs +++ b/PhotoLocator/JpegTransformCommands.cs @@ -14,6 +14,8 @@ namespace PhotoLocator { public sealed class JpegTransformCommands { + public const string AstroCommandParameter = "Astro"; + private readonly IMainViewModel _mainViewModel; private bool HasFileSelected(object? o) => _mainViewModel.SelectedItem is not null && _mainViewModel.SelectedItem.IsFile; @@ -103,7 +105,7 @@ await Task.Run(() => using (var cursor = new MouseCursorOverride()) { (var image, metadata) = await Task.Run(() => LoadImageWithMetadataAsync(selectedItem)); - localContrastViewModel = new LocalContrastViewModel() { IsAstroModeEnabled = o as string == "Astro", SourceBitmap = image }; + localContrastViewModel = new LocalContrastViewModel() { IsAstroModeEnabled = o as string == AstroCommandParameter, SourceBitmap = image }; } var window = new LocalContrastView(); window.Owner = Application.Current.MainWindow; diff --git a/PhotoLocator/LocalContrastViewModel.cs b/PhotoLocator/LocalContrastViewModel.cs index 18e928e..6a6adcc 100644 --- a/PhotoLocator/LocalContrastViewModel.cs +++ b/PhotoLocator/LocalContrastViewModel.cs @@ -570,7 +570,8 @@ private void SetHistogram(int[] histogram) HistogramPoints = histogramPoints; } - public bool IsNoOperation => DetailHandling == 1 && ToneMapping == 1 && HighlightStrength == 0 && ShadowStrength == 0 && Contrast == DefaultContrast + public bool IsNoOperation => (!IsAstroModeEnabled || AstroStretch == 0 && BackgroundRemovalSmooth == 0 && BlackPoint == 0) + && DetailHandling == 1 && ToneMapping == 1 && HighlightStrength == 0 && ShadowStrength == 0 && Contrast == DefaultContrast && !_colorToneOperation.AreToneAdjustmentsChanged; public BitmapSource ApplyOperations(BitmapSource source) diff --git a/PhotoLocator/MainWindow.xaml b/PhotoLocator/MainWindow.xaml index 18f4e28..c855d04 100644 --- a/PhotoLocator/MainWindow.xaml +++ b/PhotoLocator/MainWindow.xaml @@ -35,7 +35,7 @@ - + @@ -219,7 +219,7 @@ - + diff --git a/PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs b/PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs index d35be0f..0ed83bb 100644 --- a/PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs +++ b/PhotoLocatorTest/BitmapOperations/AstroStretchOperationTest.cs @@ -29,7 +29,7 @@ public void Apply_AstroStretchOperation() var result = op.DstBitmap.ToBitmapSource(source.DpiX, source.DpiY, FloatBitmap.DefaultMonitorGamma); Console.WriteLine(sw.ElapsedMilliseconds); - Assert.AreEqual(0.0026545193517195226, op.DstBitmap.Mean(), 0.000001); + Assert.AreEqual(0.0026545193517195226, op.DstBitmap.Mean(), 1e-5); #if DEBUG GeneralFileFormatHandler.SaveToFile(result, "astroStretch.png"); #endif diff --git a/PhotoLocatorTest/PhotoLocatorTest.csproj b/PhotoLocatorTest/PhotoLocatorTest.csproj index 20a9232..bd7e241 100644 --- a/PhotoLocatorTest/PhotoLocatorTest.csproj +++ b/PhotoLocatorTest/PhotoLocatorTest.csproj @@ -34,10 +34,10 @@ - + - - + + diff --git a/PhotoshopImageLoader/PhotoLocator.PhotoshopImageLoader.csproj b/PhotoshopImageLoader/PhotoLocator.PhotoshopImageLoader.csproj index eea74c8..e735a65 100644 --- a/PhotoshopImageLoader/PhotoLocator.PhotoshopImageLoader.csproj +++ b/PhotoshopImageLoader/PhotoLocator.PhotoshopImageLoader.csproj @@ -47,7 +47,7 @@ - +