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 e980b92..a22e2f2 100644 --- a/PhotoLocator/BitmapOperations/FloatBitmap.cs +++ b/PhotoLocator/BitmapOperations/FloatBitmap.cs @@ -386,6 +386,21 @@ public float Min() return (min, max); } + public double Mean() + { + double sum = 0; + unsafe + { + fixed (float* elements = Elements) + { + var size = Size; + for (var i = 0; i < size; i++) + sum += elements[i]; + } + } + return sum / Size; + } + /// /// Apply operation to all elements /// diff --git a/PhotoLocator/BitmapOperations/StarfieldRenderer.cs b/PhotoLocator/BitmapOperations/StarfieldRenderer.cs new file mode 100644 index 0000000..d4ac51b --- /dev/null +++ b/PhotoLocator/BitmapOperations/StarfieldRenderer.cs @@ -0,0 +1,161 @@ +using PhotoLocator.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace PhotoLocator.BitmapOperations +{ + /// + /// Render a simple starfield (flying through stars) producing 8-bit grayscale frames. + /// + public class StarfieldRenderer + { + readonly int _width; + readonly int _height; + readonly int _centerX; + readonly int _centerY; + readonly Star[] _stars; + readonly Random _rnd; + readonly float _focal; // projection focal length + readonly float _growthFactor; + readonly float _speed; + + struct Star + { + public float X; // world X (centered) + public float Y; // world Y (centered) + public float Z; // depth in range (0..1], smaller = closer + } + + /// + /// Create a starfield renderer. + /// + /// Frame width in pixels. + /// Frame height in pixels. + /// Number of stars to render. + /// Per-frame approach speed. Typical small values like 0.01 - 0.1. + /// How much stars grow when they get close. + /// Optional random seed for reproducible results. + public StarfieldRenderer(int width, int height, int starCount, float speed = 0.02f, float growthFactor = 1.5f, int? seed = null) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(starCount); + + _width = width; + _height = height; + _centerX = width / 2; + _centerY = height / 2; + _stars = new Star[starCount]; + _rnd = seed.HasValue ? new Random(seed.Value) : new Random(); + _focal = Math.Max(width, height) * 0.5f; // reasonable focal length + _growthFactor = Math.Max(0.1f, growthFactor); + _speed = Math.Max(0f, speed); + + InitializeStars(); + } + + void InitializeStars() + { + for (int i = 0; i < _stars.Length; i++) + _stars[i] = CreateStar(); + } + + Star CreateStar() + { + // World coordinates centered around 0. X range roughly [-width/2, width/2] +#pragma warning disable CA5394 // Do not use insecure randomness + var x = (float)(_rnd.NextDouble() * _width - _centerX); + var y = (float)(_rnd.NextDouble() * _height - _centerY); + // Z in (0.05 .. 1], avoid zero + var z = (float)(_rnd.NextDouble() * 0.95 + 0.05); +#pragma warning restore CA5394 // Do not use insecure randomness + return new Star { X = x, Y = y, Z = z }; + } + + /// + /// Generate a sequence of frames. Each yielded BitmapSource is frozen and is an 8-bit grayscale image. + /// + /// Number of frames to generate. + public IEnumerable GenerateFrames(int frameCount) + { + for (int f = 0; f < frameCount; f++) + { + // create fresh pixel buffer for this frame + var pixels = new byte[_width * _height * 3]; + + // update stars and draw + for (int i = 0; i < _stars.Length; i++) + { + var s = _stars[i]; + + // Move star closer by decreasing Z + s.Z -= _speed; + if (s.Z <= 0.02f) + { + // respawn at far distance + s = CreateStar(); + s.Z = 1.0f; + } + + // projection + var invZ = 1f / s.Z; + var sx = IntMath.Round(_centerX + s.X * invZ * (_focal / _centerX)); + var sy = IntMath.Round(_centerY + s.Y * invZ * (_focal / _centerY)); + + // compute intensity and radius + // closer stars (smaller Z) are brighter and bigger + var brightness = (1f - s.Z) * 255f; + var b = Math.Clamp((int)brightness, 0, 255); + var radius = Math.Max(0.0f, (1f - s.Z) * _growthFactor * 2.0f); + Debug.Assert(radius >= 0f); + var r = (int)radius; + + // draw filled circle + if (sx >= -r && sx < _width + r && sy >= -r && sy < _height + r) + { + for (int yy = -r; yy <= r; yy++) + { + int py = sy + yy; + if (py < 0 || py >= _height) continue; + int dx = (int)Math.Floor(Math.Sqrt(radius * radius - yy * yy)); + int x0 = Math.Max(0, sx - dx); + int x1 = Math.Min(_width - 1, sx + dx); + int baseIndex = py * _width * 3; + for (int px = x0; px <= x1; px++) + { + int idx = baseIndex + px * 3; + // additive blending clamped to 255 + var val = (byte)Math.Min(pixels[idx] + b, 255); + pixels[idx] = val; + pixels[idx + 1] = val; + pixels[idx + 2] = val; + } + } + } + + _stars[i] = s; + } + + var bmp = BitmapSource.Create(_width, _height, 96, 96, PixelFormats.Rgb24, null, pixels, _width * 3); + bmp.Freeze(); + yield return bmp; + } + } + + internal static BitmapSource AddFrames(BitmapSource frame1, BitmapSource frame2) + { + var buf1 = new byte[frame1.PixelWidth * frame1.PixelHeight * 3]; + var buf2 = new byte[frame2.PixelWidth * frame2.PixelHeight * 3]; + frame1.CopyPixels(buf1, frame1.PixelWidth * 3, 0); + frame2.CopyPixels(buf2, frame1.PixelWidth * 3, 0); + Parallel.For(0, buf1.Length, i => buf1[i] = (byte)Math.Min(buf1[i] + buf2[i], 255)); + var result = BitmapSource.Create(frame1.PixelWidth, frame1.PixelHeight, 96, 96, frame1.Format, null, buf1, frame1.PixelWidth * 3); + result.Freeze(); + return result; + } + } +} 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 59484a3..86999bc 100644 --- a/PhotoLocator/MainWindow.xaml +++ b/PhotoLocator/MainWindow.xaml @@ -35,6 +35,7 @@ + @@ -218,6 +219,11 @@ + + + + + diff --git a/PhotoLocator/PhotoLocator.csproj b/PhotoLocator/PhotoLocator.csproj index d04075e..cd348f7 100644 --- a/PhotoLocator/PhotoLocator.csproj +++ b/PhotoLocator/PhotoLocator.csproj @@ -32,6 +32,7 @@ + diff --git a/PhotoLocator/PictureFileFormats/FitsFileFormatHandler.cs b/PhotoLocator/PictureFileFormats/FitsFileFormatHandler.cs new file mode 100644 index 0000000..7feb663 --- /dev/null +++ b/PhotoLocator/PictureFileFormats/FitsFileFormatHandler.cs @@ -0,0 +1,74 @@ +using FITSReader; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace PhotoLocator.PictureFileFormats +{ + class FitsFileFormatHandler + { + /// + /// Takes extension in lower case including . + /// + public static bool CanLoad(string extension) + { + return extension == ".fits"; + } + + public static BitmapSource LoadFromFile(string fileName, out string metadataString, CancellationToken ct) + { + var fitsFile = FITSFile.ReadFile(fileName); + var header = fitsFile.Headers.Find(header => header.DataType == FITSDataType.Int16) ?? throw new FileFormatException(); + + var metadata = new Dictionary(); + foreach(var kvp in header.RawHeaders) + { + var parts = kvp.Split('=', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (parts.Length > 1) + metadata[parts[0]] = parts[1]; + } + var offset = int.Parse(metadata["BZERO"], CultureInfo.InvariantCulture); + + var width = header.Width; + var height = header.Height; + var rawPixels = header.RawData.AsSpan(header.DataStartIndex, header.DataEndIndex - header.DataStartIndex); + + var srcPixels16 = MemoryMarshal.Cast(rawPixels).ToArray(); + var dstPixels = new ushort[srcPixels16.Length]; + + Parallel.For(0, height, y => + { + var iSrc = y * width; + var iDst = (height - 1 - y) * width; + for (int x = 0; x < width; x++) + { + var p1 = srcPixels16[iSrc]; + var p2 = (short)(p1 >> 8 | p1 << 8) + offset; // swap bytes and apply offset + var p3 = (ushort)int.Clamp(p2 * 50, 0, 65535); + dstPixels[iDst] = p3; + + iSrc += 1; + iDst += 1; + } + ct.ThrowIfCancellationRequested(); + }); + + var result = BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray16, null, dstPixels, width * 2); + result.Freeze(); + + metadataString = + $"{metadata["OBJECT"].Split("'", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)[0]}, " + + $"{metadata["FILTER"].Split("'", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)[0]}, " + + $"{width}x{height}, " + + $"{metadata["DATE-OBS"].Split("'", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)[0]}"; + + return result; + } + } +} diff --git a/PhotoLocator/PictureItemViewModel.cs b/PhotoLocator/PictureItemViewModel.cs index c02e1cc..fa416ea 100644 --- a/PhotoLocator/PictureItemViewModel.cs +++ b/PhotoLocator/PictureItemViewModel.cs @@ -313,6 +313,8 @@ private BitmapSource LoadPreviewInternal(int maxPixelWidth, bool preservePixelFo return CR3FileFormatHandler.LoadFromStream(fileStream, Orientation, maxPixelWidth, preservePixelFormat, ct); if (PhotoshopFileFormatHandler.CanLoad(ext)) return PhotoshopFileFormatHandler.LoadFromStream(fileStream, Orientation, maxPixelWidth, preservePixelFormat, ct); + if (FitsFileFormatHandler.CanLoad(ext)) + return FitsFileFormatHandler.LoadFromFile(FullPath, out _metadataString, ct); } catch (OperationCanceledException) { diff --git a/PhotoLocator/VideoTransformCommands.cs b/PhotoLocator/VideoTransformCommands.cs index 50d6fc4..b1c23f0 100644 --- a/PhotoLocator/VideoTransformCommands.cs +++ b/PhotoLocator/VideoTransformCommands.cs @@ -1127,6 +1127,8 @@ async Task RunLocalFrameProcessingAsync(string outFileName, CancellationToken ct _ => null, }; + IEnumerator? starfield = null; + using var frameEnumerator = new QueueEnumerable(); var readTask = _videoTransforms.RunFFmpegWithStreamOutputImagesAsync($"{InputArguments} {ProcessArguments}", source => @@ -1143,6 +1145,11 @@ async Task RunLocalFrameProcessingAsync(string outFileName, CancellationToken ct } source = runningAverage.GetResult16(); } + + starfield ??= new StarfieldRenderer(source.PixelWidth, source.PixelHeight, 10000, speed: 0.001f, growthFactor: 2f, seed: 42).GenerateFrames(10000).GetEnumerator(); + starfield.MoveNext(); + source = StarfieldRenderer.AddFrames(source, starfield.Current); + frameEnumerator.AddItem(_localContrastSetup!.ApplyOperations(source)); }, ProcessStdError, ct); await Task.WhenAny(frameEnumerator.GotFirst, Task.Delay(TimeSpan.FromSeconds(10), ct)).ConfigureAwait(false); diff --git a/PhotoLocatorTest/BitmapOperations/StarfieldRendererTest.cs b/PhotoLocatorTest/BitmapOperations/StarfieldRendererTest.cs new file mode 100644 index 0000000..2300586 --- /dev/null +++ b/PhotoLocatorTest/BitmapOperations/StarfieldRendererTest.cs @@ -0,0 +1,32 @@ +using PhotoLocator.Helpers; +using PhotoLocator.Settings; +using System.Diagnostics; + +namespace PhotoLocator.BitmapOperations +{ + [TestClass] + public class StarfieldRendererTest + { + public TestContext TestContext { get; set; } + + [TestMethod] + public async Task GenerateFrames_ShouldGenerateVideo() + { + const string TargetPath = @"Starfield.mp4"; + const double FrameRate = 30; + const double Duration = 10; + + if (!File.Exists(VideoProcessingTest.FFmpegPath)) + Assert.Inconclusive("FFmpegPath not found"); + + var renderers = new StarfieldRenderer(3840, 2160, 10000, speed: 0.001f, growthFactor: 2f, seed: 42); + + var settings = new ObservableSettings() { FFmpegPath = VideoProcessingTest.FFmpegPath }; + var videoTransforms = new VideoProcessing(settings); + + var writerArgs = $"-pix_fmt yuv420p -y {TargetPath}"; + await videoTransforms.RunFFmpegWithStreamInputImagesAsync(FrameRate, writerArgs, renderers.GenerateFrames((int)(FrameRate * Duration)), + stdError => Debug.WriteLine(stdError), TestContext.CancellationToken); + } + } +}