Tuesday, November 14, 2017

Thickening with Falloff

Yesterday I was looking for a way to generate decreasing intensity extending outward from initial shapes.  It is something that  may be useful for some other techniques going forward, though is of some limited use in and of itself, and the effect itself is somewhat interesting.  It might look nicer with some smoothing, though.

Here's a more : Given a raster representation of some initial geometry, this program calculates distance of pixels from the initial geometry, then visually represents the distance as a gradient between white (initial geometry) and black (at and beyond threshold).

The program has two required command line arguments (input and output files) and an optional argument specifying the threshold in pixels.  Here's an example of the arguments I was using:

Thicken.exe -in C:\test\input.png -out C:\test\output.png -threshold 48

In the input image, black is treated as no geometry, any other color counts as geometry.


Input was a 512 x 512 PNG file
Output image with a 48 pixel falloff threshold

I'm releasing the C# source code under the MIT license, with no warranty.  Use at own risk.  Since I used the Vector2 struct you'll need to add the System.Numerics.Vectors package (which can be done with Install-Package System.Numerics.Vectors -Version 4.4.0 from the Package Manager Console in Visual Studio) or substitute your own implementation of Vector2.

using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Numerics; using System.Text; using System.Threading.Tasks; namespace Thickening { class Program { private static void ReadImageAsMap(string fileName, out int[] map, out int width, out int height) { // Read input image into map array. Any non-black pixel is a starting point. Bitmap inputImage = (Bitmap)Bitmap.FromFile(fileName); width = inputImage.Width; height = inputImage.Height; map = new int[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int i = y * width + x; Color c = inputImage.GetPixel(x, y); if (c.R > 0 || c.G > 0 || c.B > 0) { map[i] = 0; } else { map[i] = -1; } } } } private static void Thicken(int[] map, int width, int height) { // Find starting points (zero). List<Vector2> current = new List<Vector2>(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int i = y * width + x; if (map[i] == 0) current.Add(new Vector2(x, y)); } } // Iterative set distance around starting points or points // where distance has already been calculated. while (current.Count > 0) { List<Vector2> next = new List<Vector2>(); foreach (var curr in current) { int cx = (int)curr.X; int cy = (int)curr.Y; int val = map[cy * width + cx]; // Look at neighbors. for (int y = cy - 1; y <= cy + 1; y++) { if (y < 0 || y >= height) continue; int ny = y; for (int x = cx - 1; x <= cx + 1; x++) { int nx = x; if (nx < 0) nx = width - 1; else if (nx >= width) nx = 0; int j = ny * width + nx; int nval = map[j]; if (nval < 0) { Vector2 neighbor = new Vector2(nx, ny); if (!current.Contains(neighbor) && !next.Contains(neighbor)) { map[j] = val + 1; next.Add(neighbor); } } } } } current = next; } } private static float Clamp(float min, float val, float max) { if (val < min) return min; if (val > max) return max; return val; } private static void WriteMapAsImage(string fileName, int[] map, int width, int height, float threshold) { Bitmap outputImage = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int i = y * width + x; int val = map[i]; Color c = Color.Black; if (val >= 0) { // Calculate percentage closeness to initial geometry. // Intensity is 100% at initial geometry, // 0 % at or beyond threshold. float closePercent = (threshold - val) / threshold; closePercent = Clamp(0, closePercent, 1.0f); int val2 = (int)(closePercent * 255.0); c = Color.FromArgb(val2, val2, val2); } outputImage.SetPixel(x, y, c); } } outputImage.Save(fileName); } private static string ParseForArgumentValue(string[] args, string argumentName, bool caseInsensitive = true, string defaultValue = null) { if (args == null || args.Length == 0) return defaultValue; string prevArg = ""; foreach (string arg in args) { bool isMatch = false; if (caseInsensitive) isMatch = (prevArg.ToLower() == argumentName.ToLower()); else isMatch = (prevArg == argumentName); if (isMatch) return arg; prevArg = arg; } return defaultValue; } static void Main(string[] args) { if (args == null || args.Length == 0) { string msg = "Thicken.exe -in input.png -out output.png [-threshold 32]"; Console.Error.WriteLine(msg); Environment.Exit(1); } string input = ParseForArgumentValue(args, "-in", true, null); string output = ParseForArgumentValue(args, "-out", true, null); string thresholdString = ParseForArgumentValue(args, "-threshold", true, "32"); int threshold = 32; bool thresholdOk = Int32.TryParse(thresholdString, out threshold); if (!thresholdOk) { string msg = "Invalid value for -threshold argument. " + "Omit -threshold argument or supply a valid positive integer " + "less than two billion."; Console.Error.WriteLine(msg); Environment.Exit(2); } try { int width, height; int[] map; ReadImageAsMap(input, out map, out width, out height); Thicken(map, width, height); WriteMapAsImage(output, map, width, height, threshold); } catch (Exception ex) { Console.Error.WriteLine("Unexpected error: {0}", ex.ToString()); Environment.Exit(3); } } } }




No comments:

Post a Comment