Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions PhotoLocator/BitmapOperations/AstroStretchOperation.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
15 changes: 15 additions & 0 deletions PhotoLocator/BitmapOperations/FloatBitmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
/// Apply operation to all elements
/// </summary>
Expand Down
161 changes: 161 additions & 0 deletions PhotoLocator/BitmapOperations/StarfieldRenderer.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Render a simple starfield (flying through stars) producing 8-bit grayscale frames.
/// </summary>
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
}

/// <summary>
/// Create a starfield renderer.
/// </summary>
/// <param name="width">Frame width in pixels.</param>
/// <param name="height">Frame height in pixels.</param>
/// <param name="starCount">Number of stars to render.</param>
/// <param name="speed">Per-frame approach speed. Typical small values like 0.01 - 0.1.</param>
/// <param name="growthFactor">How much stars grow when they get close.</param>
/// <param name="seed">Optional random seed for reproducible results.</param>
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 };
}

/// <summary>
/// Generate a sequence of frames. Each yielded BitmapSource is frozen and is an 8-bit grayscale image.
/// </summary>
/// <param name="frameCount">Number of frames to generate.</param>
public IEnumerable<BitmapSource> 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;
}
}
}
2 changes: 1 addition & 1 deletion PhotoLocator/JpegTransformCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 26 additions & 3 deletions PhotoLocator/LocalContrastView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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">

<Window.Resources>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#FFEEEEEE" />
</Style>
<BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
</Window.Resources>

<Window.InputBindings>
Expand Down Expand Up @@ -44,7 +45,7 @@

<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="210"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>

Expand All @@ -55,7 +56,29 @@

<StackPanel Margin="0,0,4,0">
<TextBlock Text="Local adjustments" FontWeight="Bold" Margin="0,0,0,4"/>


<StackPanel Visibility="{Binding IsAstroModeEnabled, Converter={StaticResource BooleanToVisibility}}">
<DockPanel>
<TextBlock Text="Astro stretch">
<TextBlock.InputBindings>
<MouseBinding Command="{Binding ResetAstroStretchCommand}" MouseAction="LeftClick" />
</TextBlock.InputBindings>
</TextBlock>
<TextBox Text="{Binding AstroStretch,StringFormat=N2,UpdateSourceTrigger=PropertyChanged,Delay=500}" Width="80" HorizontalAlignment="Right" />
</DockPanel>
<Slider Value="{Binding AstroStretch}" Minimum="0" Maximum="20" SmallChange="0.01" LargeChange="0.1" />

<DockPanel>
<TextBlock Text="Background filter">
<TextBlock.InputBindings>
<MouseBinding Command="{Binding ResetBackgroundRemovalSmoothCommand}" MouseAction="LeftClick" />
</TextBlock.InputBindings>
</TextBlock>
<TextBox Text="{Binding BackgroundRemovalSmooth,StringFormat=N2,UpdateSourceTrigger=PropertyChanged,Delay=500}" Width="80" HorizontalAlignment="Right" />
</DockPanel>
<Slider Value="{Binding BackgroundRemovalSmooth}" Minimum="0" Maximum="16" SmallChange="0.05" LargeChange="0.1" />
</StackPanel>

<DockPanel>
<TextBlock Text="Local brightness">
<TextBlock.InputBindings>
Expand Down
Loading
Loading