From 3ba22ac3b204f9c5caf635a6d3949e9733166381 Mon Sep 17 00:00:00 2001 From: Samuele Lorefice Date: Wed, 26 Mar 2025 05:55:39 +0100 Subject: [PATCH] BaseLine --- .gitignore | 5 + .idea/.idea.SDFMapCreator/.idea/.gitignore | 13 + .../.idea.SDFMapCreator/.idea/indexLayout.xml | 8 + .idea/.idea.SDFMapCreator/.idea/vcs.xml | 6 + SDFMapCreator.sln | 16 ++ SDFMapCreator/1.png | Bin 0 -> 2786 bytes SDFMapCreator/2.png | Bin 0 -> 3206 bytes SDFMapCreator/Program.cs | 254 ++++++++++++++++++ SDFMapCreator/SDFMapCreator.csproj | 26 ++ SDFMapCreator/TestPattern.png | Bin 0 -> 1382 bytes global.json | 7 + 11 files changed, 335 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.idea.SDFMapCreator/.idea/.gitignore create mode 100644 .idea/.idea.SDFMapCreator/.idea/indexLayout.xml create mode 100644 .idea/.idea.SDFMapCreator/.idea/vcs.xml create mode 100644 SDFMapCreator.sln create mode 100644 SDFMapCreator/1.png create mode 100644 SDFMapCreator/2.png create mode 100644 SDFMapCreator/Program.cs create mode 100644 SDFMapCreator/SDFMapCreator.csproj create mode 100644 SDFMapCreator/TestPattern.png create mode 100644 global.json 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 0000000000000000000000000000000000000000..b28b6f2267b5150532f394ad43e392e6f2c62e25 GIT binary patch literal 2786 zcmeHHJCD>>6uyf#h%8XiAgU|VfWU9>!&ht2GTtyF;bpaq!lvWS_4SOkV_(O<%ginm zC`uDWN|6%LQKSlGA|(YyenLb6;s?;=Q6^24<9S%SXaO-UXs~AFWBWU=?|k=sv$=8o z^uoCXgwW|$)89hq7?{V_J4Z=jC z(NJ`_HcT==BeZ&Tm{GRFCGPNUoO;S%zdluP9D2&_#lQ%%2Jgkq{hZ(0-)OV_9p;A0 z)hl>)NFYGMCB?&}pB7~3DWkXq?DDm);1NXbc*a1((xI9b=cf^TUa&mQWGqr#Mx-6FD?J5D9ruuARoJwc@C1zMLz?NPW|TdR zfFY}J*>2t+B@da-`#j;PEC6d(*sLd{D0<=zp{vh_5&*YBP~|wXmL#cC6mtD8v@s#% zNOaNO%ecP93$dFszJ3?VJYRN`kw(s`6nR^S{&Z5C(~_~Zq}lkw?Klm^ptyLzf%{Z) zPl2f|s)nsvi*0j-*e-FbONL1dV+;*M7)N_YK^>FW<{>C(CZv+S6C5%Ui9DguaGcOC z*R!;%;EG1l5PgwDVyMm@RS^WFl@^kwjJJGGfu6NE4#`RsnUUpk)nOf14K2r3JC?Jg zGH20Vc2>A;MSSY-3$|NMcIqF#r~l17hNDIMGsh_pTqz0Cj0+gn-sD8w;$KdzevC&d zAe5Er_7ql*m50jYbv!czbKC z;LJ|pun0=CT0!toEis|ib_dv;#hk_G zaM3kqSl1oT)e>-R4b;s3Q9_`mx*GVvYvBDQ@bdCMUoZUkiyuEmaJAMq{3qA$eDyzp C<1nNE literal 0 HcmV?d00001 diff --git a/SDFMapCreator/2.png b/SDFMapCreator/2.png new file mode 100644 index 0000000000000000000000000000000000000000..b138515f6be0d0b8cd9fb83554023c663b919ef5 GIT binary patch literal 3206 zcmeHIO^@3|7@jSnfG7_z6nnN@~H*ZaI$__=PL}Hj^f%@Aw->OQ&1NDv_>S5jyqoj9Qh});T19p1I zJg(k)Te&&)0U#9>Ri^1UEB$Gp&TxHL*N-(-nT1q`fx2B2DhJ`7(vgKwyq1BPt`o)a zTGU|NAn{uY(TSxQ27IvL`MT>{ZDnz(Kv(eC-|xP=AcMO=9aUBCYuaQoX-&+QEQXrl zd7h>djSvhGxID=!I>lLeV~NleB`cD=N@S+gh%}N%RiJ{@c@1gaz-Hy5PtY-KN^{L< z>2*!BK+c-D{HPeu$T`!*SfnDWN{BU@*nA`_S&rl*pqtkh2%xuN*l?`WlBNwsS?wG{ z8Hjjk=oL?-YVovKUA?UQTLnDOn-ymZ`jcC&{>+ly96% z5M5e{K!vH#S zPEtA)TAmG6rD>zzkz-i^F{m?VD#Fn3XJth*Ci>k#g_>JQ!hK;AJC2x(BQv6yx|W40 zH9Z_t*E38zB8)`K{H|n2^J zI!m+bbKDQR>c9)jbkGwp1>B1Jj{t@;s}s2vux9`Tw#(})NY;`#P1~~jP7Ld z|F|taTCB#2GV_E_S?%>eW%ZB+R~L_y75#WB=0*NwBmnd=!=JO4GOi}H5ZgoO-4o5F zJxY8@j~_2x#5(aT4!cU^7}$0!9lNoiW5+afn>&P991Jvy*Y)1Q9*(bz)h 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 0000000000000000000000000000000000000000..599a80824233b52126516920b21fd671a9791784 GIT binary patch literal 1382 zcmbVM%TCl#7%pleU`R~d7#E}|iIGUJbD=$9OBoqlu0>+Jp)6eG|gF_jdvCGiYq{ZFdzB7An|dJ5=d~F`xxusnUxv z*`-|Eq0Jz&rC*=FNMhjG(pJgU+_*|xLG2)+n+IzRa7;%S6)*anVw+PCVW47eqrdimI-|E9(~0 zOjIn3qelX9iRYsY=gCMItZb>pd5l!G+wCgdg2Ixfs#}(&YKCeUG9cu%A9381qjYY} z;LwyLLCgadi5Vl_VI6Kupz6?uFwSYCbmS-SnA*d!sw-M%(*Wp^92a+z_CUBtRNAH? zjd%)JJ;%l^##!28f1%Eg&jkPFdiqaRasgQl#L5W2Dt zYR+ZW#HgB3%vsW4tUZp@`dDSrC@Tf=!B!A?teeiCxm0PUkdgJ@>vpMFgOTtld5sll*HVPsLI+b z0l}bMVW5TUqI#5a91&V~Yzb^ug1|!s>KVRem1Lt>G-cB-QrXmLS@!XwX6lwlscDVx zJB)O)$d2!O|9Sse5v1vqConOa1UWA^LZC1bh!!Mx-^|sP+raIaA=2m-o(M#shSaKzp05JIjs97v$vl>yO_b4iCRqZfoY;#OtG)urPC7*!wtp^P97J QoOQzb%9``(@yl0#01-&3XaE2J literal 0 HcmV?d00001 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