commit 3ba22ac3b204f9c5caf635a6d3949e9733166381 Author: Samuele Lorefice Date: Wed Mar 26 05:55:39 2025 +0100 BaseLine diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.SDFMapCreator/.idea/.gitignore b/.idea/.idea.SDFMapCreator/.idea/.gitignore new file mode 100644 index 0000000..0b1f720 --- /dev/null +++ b/.idea/.idea.SDFMapCreator/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.SDFMapCreator.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.SDFMapCreator/.idea/indexLayout.xml b/.idea/.idea.SDFMapCreator/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.SDFMapCreator/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SDFMapCreator/.idea/vcs.xml b/.idea/.idea.SDFMapCreator/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.SDFMapCreator/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SDFMapCreator.sln b/SDFMapCreator.sln new file mode 100644 index 0000000..6512174 --- /dev/null +++ b/SDFMapCreator.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDFMapCreator", "SDFMapCreator\SDFMapCreator.csproj", "{915A479D-55CC-4B48-B7C0-75E0B8978698}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {915A479D-55CC-4B48-B7C0-75E0B8978698}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {915A479D-55CC-4B48-B7C0-75E0B8978698}.Debug|Any CPU.Build.0 = Debug|Any CPU + {915A479D-55CC-4B48-B7C0-75E0B8978698}.Release|Any CPU.ActiveCfg = Release|Any CPU + {915A479D-55CC-4B48-B7C0-75E0B8978698}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/SDFMapCreator/1.png b/SDFMapCreator/1.png new file mode 100644 index 0000000..b28b6f2 Binary files /dev/null and b/SDFMapCreator/1.png differ diff --git a/SDFMapCreator/2.png b/SDFMapCreator/2.png new file mode 100644 index 0000000..b138515 Binary files /dev/null and b/SDFMapCreator/2.png differ diff --git a/SDFMapCreator/Program.cs b/SDFMapCreator/Program.cs new file mode 100644 index 0000000..36816f2 --- /dev/null +++ b/SDFMapCreator/Program.cs @@ -0,0 +1,254 @@ +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; +using ImageMagick; + +public struct float2(float x, float y) { + public float x = x, y = y; +} + +public struct float3(float r, float g, float b) { + public float r = r, g = g, b = b; + public float3(float value) : this(value, value, value) {} + public float3(float r, float g) : this(r, g, 0f) {} +} + +public struct float4(float r, float g, float b, float a) { + public float r = r, g = g, b = b, a = a; + public float4(float value) : this(value, value, value, value) {} + public float4(float r, float g, float b) : this(r, g, b, 1f) {} +} + +public record ImageData(MagickImage Image, float3[,] Pixels, List Edges); +public record MaskData(float3[,] Mask, ImageData A, ImageData B, List Edges); + +public record SDFData(float3[,] SDF); + +public static class ArrayExt { + public static float3[,] To2DFloat3(this float[] array, uint width, uint height) { + float3[,] result = new float3[width, height]; + for(int i = 0; i < width*height; i++) { + uint x = (uint)(i % width); + uint y = (uint)(i / width); + result[y, x] = new (array[i*3], array[i*3+1], array[i*3+2]); + } + return result; + } + + public static float[] ToFloatArray(this float3[,] array) { + float[] result = new float[array.GetLength(0) * array.GetLength(1) * 3]; + for(int x = 0; x < array.GetLength(0); x++) { + for(int y = 0; y < array.GetLength(1); y++) { + result[x*array.GetLength(1)*3 + y*3] = array[x, y].r; + result[x*array.GetLength(1)*3 + y*3+1] = array[x, y].g; + result[x*array.GetLength(1)*3 + y*3+2] = array[x, y].b; + } + } + return result; + } +} + +public class Program { + private const float MAX = 65535f; + private const float MIN = 0f; + private const bool outputMasks = true; + static List Images = new(); + static List Masks = new(); + static List SDFs = new(); + + static void LoadImage(string imgPath) { + var image = new MagickImage(imgPath); + float3[,] pixels; + if(image.Channels.Count() ==4) + //skip ever 4th value if we have an alpha channel + pixels = image.GetPixels().ToArray()! + .Where((_, i) => (i + 1) % 4 != 0).ToArray() + .To2DFloat3(image.Width, image.Height); + else + pixels = image.GetPixels().ToArray()! + .To2DFloat3(image.Width, image.Height); + Images.Add(new (image, pixels, new())); + Console.WriteLine($"Loaded image: {imgPath}"); + ImageData(image, image.GetPixels()); + } + + public static void Main(string[] args) { + Console.WriteLine("Reading images..."); + //foreach image in arguments load the image + LoadImage("01.png"); + LoadImage("02.png"); + LoadImage("03.png"); + LoadImage("04.png"); + LoadImage("05.png"); + LoadImage("06.png"); + LoadImage("07.png"); + LoadImage("08.png"); + + //LoadImage("1.png"); + //LoadImage("2.png"); + //check if all the images in Images are the same resolution + if (Images.Select(img => (img.Image.Width, img.Image.Height)).Distinct().Count() > 1) { + Console.WriteLine("Error: Not all images have the same resolution."); + Environment.Exit(1); + } + + Console.WriteLine("Creating masks..."); + //for each image pair, create a mask + for (int i = 0; i < Images.Count - 1; i++) { + var mask = GetABMask(Images[i].Pixels, Images[i + 1].Pixels, Images[i].Image.Width, Images[i].Image.Height); + Masks.Add(new(mask, Images[i], Images[i + 1], new())); + } + + Console.WriteLine("Edge detecting masks..."); + //EdgeDetect all masks + foreach (var t in Masks) { EdgeDetect(t); } + + if(outputMasks) { + Console.WriteLine("Writing masks..."); + for (int i = 0; i < Masks.Count; i++) { + var mask = new MagickImage(MagickColors.Black, (uint)Masks[i].Mask.GetLength(0), (uint)Masks[i].Mask.GetLength(1)); + mask.GetPixels().SetPixels(Masks[i].Mask.ToFloatArray()); + mask.Write($"mask{i}.png", MagickFormat.Png24); + } + } + + Console.WriteLine("Creating SDFs..."); + for (var i = 0; i < Masks.Count; i++) { + var mask = Masks[i]; + var sdf = new MagickImage(MagickColors.Black, (uint)mask.Mask.GetLength(0), (uint)mask.Mask.GetLength(1)); + SDFs.Add(SDF(mask)); + sdf.GetPixels().SetPixels(SDFs[i].SDF.ToFloatArray()); + sdf.Write($"sdf{i}.png", MagickFormat.Png48); + } + Console.WriteLine("Done!"); + } + + private static void EdgeDetect(MaskData maskData) { + uint width = maskData.A.Image.Width; + uint height = maskData.A.Image.Height; + int iterCount = 0; + var sw = new Stopwatch(); + sw.Start(); + Console.WriteLine("Running edge detection..."); + Parallel.For(0, width * height, (i) => { + int x = (int)(i % width); + int y = (int)(i / width); + + if (!EdgeKernel(maskData.Mask, x, y, width, height)) return; + var color = maskData.Mask[x, y]; + color.g = MAX; + maskData.Mask[x, y] = color; + lock(maskData.Edges) maskData.Edges.Add(new(x, y)); + iterCount++; + if (iterCount % (width * height / 100) == 0) { + Console.WriteLine($"Progress: {iterCount/(width*height):P}% | {iterCount/(sw.Elapsed.TotalSeconds):N0} pixels/s"); + } + }); + sw.Stop(); + Console.WriteLine($"Edge pixels: {maskData.Edges.Count} | {maskData.Edges.Count/sw.ElapsedMilliseconds} pixels/s\n Time: {sw.Elapsed.TotalSeconds:F4}s"); + } + + static SDFData SDF(MaskData mask) { + var width = (uint)mask.Mask.GetLength(0); + var height = (uint)mask.Mask.GetLength(1); + var temp = new float3[width, height]; + float AbsMax = MIN; + int iterCount = 0; + var sw = new Stopwatch(); + sw.Start(); + Parallel.For(0, width * height, (i) => { + //convert 1D index to 2D index + var x = (int)(i % width); + var y = (int)(i / width); + float2 p = new(x, y); + //skip all pixels we don't care about + if(mask.Mask[x, y].r == 0) { + temp[x, y] = new(MIN); + return; + } + + float minDist = MAX; //initialize the minimum distance to the maximum possible value + + //loop through all the pixels in the mask + foreach (var edge in mask.Edges) { + float dist = EuclideanDistance(p, edge); + if (dist < minDist) minDist = dist; + } + + temp[x, y] = new(float.Abs(minDist)); + if (minDist > AbsMax) AbsMax = minDist; + iterCount++; + if (iterCount % (width * height / 100) == 0) { + Console.WriteLine($"Progress: {iterCount/(width*height):P}% | {iterCount/(sw.Elapsed.TotalSeconds):N0} pixels/s"); + } + }); + + Console.WriteLine($"SDF Generation Time: {sw.Elapsed.TotalSeconds:N4}s ({iterCount/sw.Elapsed.TotalSeconds:N0} pixels/s)"); + sw.Restart(); + Parallel.For(0, width * height, (i) => { + //convert 1D index to 2D index + var x = (int)(i % width); + var y = (int)(i / width); + temp[x, y] = new(Remap(temp[x, y].r, 0, AbsMax, MIN, MAX)); + }); + Console.WriteLine($"SDF Normalization Time: {sw.Elapsed.TotalSeconds:N4}s ({iterCount/sw.Elapsed.TotalSeconds:N0} pixels/s)"); + Console.WriteLine("AbsMax: " + AbsMax); + return new(temp); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float EuclideanDistance(float2 a, float2 b) => + MathF.Sqrt(MathF.Pow(a.x - b.x, 2) + MathF.Pow(a.y - b.y, 2)); + + + private static void ImageData(MagickImage image1, IPixelCollection pixels1) { + Console.WriteLine( + $""" + Image file: {image1.Format.ToString()} + Resolution: {image1.Width}x{image1.Height} + Total Pixels: {pixels1.Count()} |{pixels1.Channels} channels, {image1.Depth} bits per channel + + """); + } + + static float3[,] GetABMask(float3[,] A, float3[,] B, uint resX, uint resY) { + var temp = new float3[resX, resY]; + Parallel.For(0, resX*resY, (i) => { + uint x = (uint)(i % resX); + uint y = (uint)(i / resX); + var pixelA = A[x, y]; + var pixelB = B[x, y]; + float lumaA = (pixelA.r+pixelA.g+pixelA.b)/3; + float lumaB = (pixelB.r+pixelB.g+pixelB.b)/3; + float resultPixel = lumaB > lumaA ? MAX : MIN; + temp[x, y] = new(resultPixel, 0, 0); + }); + return temp; + } + + static bool EdgeKernel(float3[,] mask, int x, int y, uint width, uint height) { + //if we are already empty, return false + if (mask[x, y].r == 0) return false; + //if we are on the edge of the image, return false + if (x == 0 || y == 0 || x == width - 1 || y == height - 1) return false; + //check the 3x3 kernel + for (int xi = x - 1; xi <= x + 1; xi++) { + for (int yi = y - 1; yi <= y + 1; yi++) { + if (xi < 0 || xi >= width || yi < 0 || yi >= height) + continue; //skip out of bounds pixels + if (mask[xi, yi].r == 0) + return true; //if we find a black pixel, return true + } + } + //if we didn't find any black pixels, return false + return false; + } + + static T Lerp(T a, T b, float t) + where T : INumber, IMultiplyOperators, IAdditionOperators + => a * (1 - t) + b * t; + + static T Remap(T value, T min, T max, T newMin, T newMax) + where T : INumber, ISubtractionOperators, IMultiplyOperators, IAdditionOperators + => (value - min) / (max - min) * (newMax - newMin) + newMin; +} \ No newline at end of file diff --git a/SDFMapCreator/SDFMapCreator.csproj b/SDFMapCreator/SDFMapCreator.csproj new file mode 100644 index 0000000..2307201 --- /dev/null +++ b/SDFMapCreator/SDFMapCreator.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + Always + + + Always + + + PreserveNewest + + + + diff --git a/SDFMapCreator/TestPattern.png b/SDFMapCreator/TestPattern.png new file mode 100644 index 0000000..599a808 Binary files /dev/null and b/SDFMapCreator/TestPattern.png differ diff --git a/global.json b/global.json new file mode 100644 index 0000000..2ddda36 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file