\$\begingroup\$

C#

Wow, really cool things in this challenge. I took a stab at this at in C# and generated a 4096x4096 image in about 3 minutes (i7 CPU) using every single color via Random Walk logic.

Ok, so for the code. After being frustrated with hours of research and trying to generate every single HSL color using for loops in code, I settled for creating a flat file to read HSL colors from. What I did was create every single RGB color into a List, then I ordered by Hue, Luminosity, then Saturation. Then I saved the List to a text file. ColorData is just a small class I wrote that accepts an RGB color and also stores the HSL equivalent. This code is a HUGE RAM eater. Used about 4GB RAM lol.

public class RGB { public double R = 0; public double G = 0; public double B = 0; public override string ToString() { return "RGB:{" + (int)R + "," + (int)G + "," + (int)B + "}"; } } public class HSL { public double H = 0; public double S = 0; public double L = 0; public override string ToString() { return "HSL:{" + H + "," + S + "," + L + "}"; } } public class ColorData { public RGB rgb; public HSL hsl; public ColorData(RGB _rgb) { rgb = _rgb; var _hsl = ColorHelper._color_rgb2hsl(new double[]{rgb.R,rgb.G,rgb.B}); hsl = new HSL() { H = _hsl[0], S = _hsl[1], L = _hsl[2] }; } public ColorData(double[] _rgb) { rgb = new RGB() { R = _rgb[0], G = _rgb[1], B = _rgb[2] }; var _hsl = ColorHelper._color_rgb2hsl(_rgb); hsl = new HSL() { H = _hsl[0], S = _hsl[1], L = _hsl[2] }; } public override string ToString() { return rgb.ToString() + "|" + hsl.ToString(); } public int Compare(ColorData cd) { if (this.hsl.H > cd.hsl.H) { return 1; } if (this.hsl.H < cd.hsl.H) { return -1; } if (this.hsl.S > cd.hsl.S) { return 1; } if (this.hsl.S < cd.hsl.S) { return -1; } if (this.hsl.L > cd.hsl.L) { return 1; } if (this.hsl.L < cd.hsl.L) { return -1; } return 0; } } public static class ColorHelper { public static void MakeColorFile(string savePath) { List<ColorData> Colors = new List<ColorData>(); System.IO.File.Delete(savePath); for (int r = 0; r < 256; r++) { for (int g = 0; g < 256; g++) { for (int b = 0; b < 256; b++) { double[] rgb = new double[] { r, g, b }; ColorData cd = new ColorData(rgb); Colors.Add(cd); } } } Colors = Colors.OrderBy(x => x.hsl.H).ThenBy(x => x.hsl.L).ThenBy(x => x.hsl.S).ToList(); string cS = ""; using (System.IO.StreamWriter fs = new System.IO.StreamWriter(savePath)) { foreach (var cd in Colors) { cS = cd.ToString(); fs.WriteLine(cS); } } } public static IEnumerable<Color> NextColorHThenSThenL() { HashSet<string> used = new HashSet<string>(); double rMax = 720; double gMax = 700; double bMax = 700; for (double r = 0; r <= rMax; r++) { for (double g = 0; g <= gMax; g++) { for (double b = 0; b <= bMax; b++) { double h = (r / (double)rMax); double s = (g / (double)gMax); double l = (b / (double)bMax); var c = _color_hsl2rgb(new double[] { h, s, l }); Color col = Color.FromArgb((int)c[0], (int)c[1], (int)c[2]); string key = col.R + "-" + col.G + "-" + col.B; if (!used.Contains(key)) { used.Add(key); yield return col; } else { continue; } } } } } public static Color HSL2RGB(double h, double s, double l){ double[] rgb= _color_hsl2rgb(new double[] { h, s, l }); return Color.FromArgb((int)rgb[0], (int)rgb[1], (int)rgb[2]); } public static double[] _color_rgb2hsl(double[] rgb) { double r = rgb[0]; double g = rgb[1]; double b = rgb[2]; double min = Math.Min(r, Math.Min(g, b)); double max = Math.Max(r, Math.Max(g, b)); double delta = max - min; double l = (min + max) / 2.0; double s = 0; if (l > 0 && l < 1) { s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l)); } double h = 0; if (delta > 0) { if (max == r && max != g) h += (g - b) / delta; if (max == g && max != b) h += (2 + (b - r) / delta); if (max == b && max != r) h += (4 + (r - g) / delta); h /= 6; } return new double[] { h, s, l }; } public static double[] _color_hsl2rgb(double[] hsl) { double h = hsl[0]; double s = hsl[1]; double l = hsl[2]; double m2 = (l <= 0.5) ? l * (s + 1) : l + s - l * s; double m1 = l * 2 - m2; return new double[]{255*_color_hue2rgb(m1, m2, h + 0.33333), 255*_color_hue2rgb(m1, m2, h), 255*_color_hue2rgb(m1, m2, h - 0.33333)}; } public static double _color_hue2rgb(double m1, double m2, double h) { h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h); if (h * (double)6 < 1) return m1 + (m2 - m1) * h * (double)6; if (h * (double)2 < 1) return m2; if (h * (double)3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * (double)6; return m1; } }

With that out of the way. I wrote a class to get the next color from the generated file. It lets you set the hue start and hue end. In reality, that could and should probably be generalized to whichever dimension the file was sorted by first. Also I realize that for a performance boost here, I could have just put the RGB values into the file and kept each line at a fixed length. That way I could have easily specified the byte offset instead of looping through every line until I reached the line I wanted to start at. But it wasn't that much of a performance hit for me. But here's that class

public class HSLGenerator { double hEnd = 1; double hStart = 0; double colCount = 256 * 256 * 256; public static Color ReadRGBColorFromLine(string line) { string sp1 = line.Split(new string[] { "RGB:{" }, StringSplitOptions.None)[1]; string sp2 = sp1.Split('}')[0]; string[] sp3 = sp2.Split(','); return Color.FromArgb(Convert.ToInt32(sp3[0]), Convert.ToInt32(sp3[1]), Convert.ToInt32(sp3[2])); } public IEnumerable<Color> GetNextFromFile(string colorFile) { int currentLine = -1; int startLine = Convert.ToInt32(hStart * colCount); int endLine = Convert.ToInt32(hEnd * colCount); string line = ""; using(System.IO.StreamReader sr = new System.IO.StreamReader(colorFile)) { while (!sr.EndOfStream) { line = sr.ReadLine(); currentLine++; if (currentLine < startLine) //begin at correct offset { continue; } yield return ReadRGBColorFromLine(line); if (currentLine > endLine) { break; } } } HashSet<string> used = new HashSet<string>(); public void SetHueLimits(double hueStart, double hueEnd) { hEnd = hueEnd; hStart = hueStart; } }

So now that we have the color file, and we have a way to read the file, we can now actually make the image. I used a class I found to boost performance of setting pixels in a bitmap, called LockBitmap. LockBitmap source

I created a small Vector2 class to store coordinate locations

public class Vector2 { public int X = 0; public int Y = 0; public Vector2(int x, int y) { X = x; Y = y; } public Vector2 Center() { return new Vector2(X / 2, Y / 2); } public override string ToString() { return X.ToString() + "-" + Y.ToString(); } }

And I also created a class called SearchArea, which was helpful for finding neighboring pixels. You specify the pixel you want to find neighbors for, the bounds to search within, and the size of the "neighbor square" to search. So if the size is 3, that means you're searching a 3x3 square, with the specified pixel right in the center.

public class SearchArea { public int Size = 0; public Vector2 Center; public Rectangle Bounds; public SearchArea(int size, Vector2 center, Rectangle bounds) { Center = center; Size = size; Bounds = bounds; } public bool IsCoordinateInBounds(int x, int y) { if (!IsXValueInBounds(x)) { return false; } if (!IsYValueInBounds(y)) { return false; } return true; } public bool IsXValueInBounds(int x) { if (x < Bounds.Left || x >= Bounds.Right) { return false; } return true; } public bool IsYValueInBounds(int y) { if (y < Bounds.Top || y >= Bounds.Bottom) { return false; } return true; } }

Here's the class that actually chooses the next neighbor. Basically there's 2 search modes. A) The full square, B) just the perimeter of the square. This was an optimization that I made to prevent searching the full square again after realizing the square was full. The DepthMap was a further optimization to prevent searching the same squares over and over again. However, I didn't fully optimize this. Every call to GetNeighbors will always do the full square search first. I know I could optimize this to only do the perimeter search after completing the initial full square. I just didn't get around to that optimization yet, and even without it the code is pretty fast. The commented out "lock" lines are because I was using Parallel.ForEach at one point, but realized I had to write more code than I wanted for that lol.

public class RandomWalkGenerator { HashSet<string> Visited = new HashSet<string>(); Dictionary<string, int> DepthMap = new Dictionary<string, int>(); Rectangle Bounds; Random rnd = new Random(); public int DefaultSearchSize = 3; public RandomWalkGenerator(Rectangle bounds) { Bounds = bounds; } private SearchArea GetSearchArea(Vector2 center, int size) { return new SearchArea(size, center, Bounds); } private List<Vector2> GetNeighborsFullSearch(SearchArea srchArea, Vector2 coord) { int radius = (int)Math.Floor((double)((double)srchArea.Size / (double)2)); List<Vector2> pixels = new List<Vector2>(); for (int rX = -radius; rX <= radius; rX++) { for (int rY = -radius; rY <= radius; rY++) { if (rX == 0 && rY == 0) { continue; } //not a new coordinate int x = rX + coord.X; int y = rY + coord.Y; if (!srchArea.IsCoordinateInBounds(x, y)) { continue; } var key = x + "-" + y; // lock (Visited) { if (!Visited.Contains(key)) { pixels.Add(new Vector2(x, y)); } } } } if (pixels.Count == 0) { int depth = 0; string vecKey = coord.ToString(); if (!DepthMap.ContainsKey(vecKey)) { DepthMap.Add(vecKey, depth); } else { depth = DepthMap[vecKey]; } var size = DefaultSearchSize + 2 * depth; var sA = GetSearchArea(coord, size); pixels = GetNeighborsPerimeterSearch(sA, coord, depth); } return pixels; } private Rectangle GetBoundsForPerimeterSearch(SearchArea srchArea, Vector2 coord) { int radius = (int)Math.Floor((decimal)(srchArea.Size / 2)); Rectangle r = new Rectangle(-radius + coord.X, -radius + coord.Y, srchArea.Size, srchArea.Size); return r; } private List<Vector2> GetNeighborsPerimeterSearch(SearchArea srchArea, Vector2 coord, int depth = 0) { string vecKey = coord.ToString(); if (!DepthMap.ContainsKey(vecKey)) { DepthMap.Add(vecKey, depth); } else { DepthMap[vecKey] = depth; } Rectangle bounds = GetBoundsForPerimeterSearch(srchArea, coord); List<Vector2> pixels = new List<Vector2>(); int depthMax = 1500; if (depth > depthMax) { return pixels; } int yTop = bounds.Top; int yBot = bounds.Bottom; //left to right scan for (int x = bounds.Left; x < bounds.Right; x++) { if (srchArea.IsCoordinateInBounds(x, yTop)) { var key = x + "-" + yTop; // lock (Visited) { if (!Visited.Contains(key)) { pixels.Add(new Vector2(x, yTop)); } } } if (srchArea.IsCoordinateInBounds(x, yBot)) { var key = x + "-" + yBot; // lock (Visited) { if (!Visited.Contains(key)) { pixels.Add(new Vector2(x, yBot)); } } } } int xLeft = bounds.Left; int xRight = bounds.Right; int yMin = bounds.Top + 1; int yMax = bounds.Bottom - 1; //top to bottom scan for (int y = yMin; y < yMax; y++) { if (srchArea.IsCoordinateInBounds(xLeft, y)) { var key = xLeft + "-" + y; // lock (Visited) { if (!Visited.Contains(key)) { pixels.Add(new Vector2(xLeft, y)); } } } if (srchArea.IsCoordinateInBounds(xRight, y)) { var key = xRight + "-" + y; // lock (Visited) { if (!Visited.Contains(key)) { pixels.Add(new Vector2(xRight, y)); } } } } if (pixels.Count == 0) { var size = srchArea.Size + 2; var sA = GetSearchArea(coord, size); pixels = GetNeighborsPerimeterSearch(sA, coord, depth + 1); } return pixels; } private List<Vector2> GetNeighbors(SearchArea srchArea, Vector2 coord) { return GetNeighborsFullSearch(srchArea, coord); } public Vector2 ChooseNextNeighbor(Vector2 coord) { SearchArea sA = GetSearchArea(coord, DefaultSearchSize); List<Vector2> neighbors = GetNeighbors(sA, coord); if (neighbors.Count == 0) { return null; } int idx = rnd.Next(0, neighbors.Count); Vector2 elm = neighbors.ElementAt(idx); string key = elm.ToString(); // lock (Visited) { Visited.Add(key); } return elm; } }

Ok great, so now here's the class that creates the image

public class RandomWalk { Rectangle Bounds; Vector2 StartPath = new Vector2(0, 0); LockBitmap LockMap; RandomWalkGenerator rwg; public int RandomWalkSegments = 1; string colorFile = ""; public RandomWalk(int size, string _colorFile) { colorFile = _colorFile; Bounds = new Rectangle(0, 0, size, size); rwg = new RandomWalkGenerator(Bounds); } private void Reset() { rwg = new RandomWalkGenerator(Bounds); } public void CreateImage(string savePath) { Reset(); Bitmap bmp = new Bitmap(Bounds.Width, Bounds.Height); LockMap = new LockBitmap(bmp); LockMap.LockBits(); if (RandomWalkSegments == 1) { RandomWalkSingle(); } else { RandomWalkMulti(RandomWalkSegments); } LockMap.UnlockBits(); bmp.Save(savePath); } public void SetStartPath(int X, int Y) { StartPath.X = X; StartPath.Y = Y; } private void RandomWalkMulti(int buckets) { int Buckets = buckets; int PathsPerSide = (Buckets + 4) / 4; List<Vector2> Positions = new List<Vector2>(); var w = Bounds.Width; var h = Bounds.Height; var wInc = w / Math.Max((PathsPerSide - 1),1); var hInc = h / Math.Max((PathsPerSide - 1),1); //top for (int i = 0; i < PathsPerSide; i++) { var x = Math.Min(Bounds.Left + wInc * i, Bounds.Right - 1); Positions.Add(new Vector2(x, Bounds.Top)); } //bottom for (int i = 0; i < PathsPerSide; i++) { var x = Math.Max(Bounds.Right -1 - wInc * i, 0); Positions.Add(new Vector2(x, Bounds.Bottom - 1)); } //right and left for (int i = 1; i < PathsPerSide - 1; i++) { var y = Math.Min(Bounds.Top + hInc * i, Bounds.Bottom - 1); Positions.Add(new Vector2(Bounds.Left, y)); Positions.Add(new Vector2(Bounds.Right - 1, y)); } Positions = Positions.OrderBy(x => Math.Atan2(x.X, x.Y)).ToList(); double cnt = 0; List<IEnumerator<bool>> _execs = new List<IEnumerator<bool>>(); foreach (Vector2 startPath in Positions) { double pct = cnt / (Positions.Count); double pctNext = (cnt + 1) / (Positions.Count); var enumer = RandomWalkHueSegment(pct, pctNext, startPath).GetEnumerator(); _execs.Add(enumer); cnt++; } bool hadChange = true; while (hadChange) { hadChange = false; foreach (var e in _execs) { if (e.MoveNext()) { hadChange = true; } } } } private IEnumerable<bool> RandomWalkHueSegment(double hueStart, double hueEnd, Vector2 startPath) { var colors = new HSLGenerator(); colors.SetHueLimits(hueStart, hueEnd); var colorFileEnum = colors.GetNextFromFile(colorFile).GetEnumerator(); Vector2 coord = new Vector2(startPath.X, startPath.Y); LockMap.SetPixel(coord.X, coord.Y, ColorHelper.HSL2RGB(0, 0, 0)); while (true) { if (!colorFileEnum.MoveNext()) { break; } var rgb = colorFileEnum.Current; coord = ChooseNextNeighbor(coord); if (coord == null) { break; } LockMap.SetPixel(coord.X, coord.Y, rgb); yield return true; } } private void RandomWalkSingle() { Vector2 coord = new Vector2(StartPath.X, StartPath.Y); LockMap.SetPixel(coord.X, coord.Y, ColorHelper.HSL2RGB(0, 0, 0)); int cnt = 1; var colors = new HSLGenerator(); var colorFileEnum = colors.GetNextFromFile(colorFile).GetEnumerator(); while (true) { if (!colorFileEnum.MoveNext()) { return; } var rgb = colorFileEnum.Current; var newCoord = ChooseNextNeighbor(coord); coord = newCoord; if (newCoord == null) { return; } LockMap.SetPixel(newCoord.X, newCoord.Y, rgb); cnt++; } } private Vector2 ChooseNextNeighbor(Vector2 coord) { return rwg.ChooseNextNeighbor(coord); } }

And here's an example implementation:

class Program { static void Main(string[] args) { { // ColorHelper.MakeColorFile(); // return; } string colorFile = "colors.txt"; var size = new Vector2(1000,1000); var ctr = size.Center(); RandomWalk r = new RandomWalk(size.X,colorFile); r.RandomWalkSegments = 8; r.SetStartPath(ctr.X, ctr.Y); r.CreateImage("test.bmp"); } }

If RandomWalkSegments = 1, then it basically just starts walking wherever you tell it to, and begins at the first first color in the file.

It's not the cleanest code I'll admit, but it runs pretty fast!

EDIT:

So I've been learning about OpenGL and Shaders. I generated a 4096x4096 using every color blazing fast on the GPU with 2 simple shader scripts. The output is boring, but figured somebody might find this interesting and come up with some cool ideas:

Vertex Shader

attribute vec3 a_position; varying vec2 vTexCoord; void main() { vTexCoord = (a_position.xy + 1) / 2; gl_Position = vec4(a_position, 1); }

Frag Shader

void main(void){ int num = int(gl_FragCoord.x*4096.0 + gl_FragCoord.y); int h = num % 256; int s = (num/256) % 256; int l = ((num/256)/256) % 256; vec4 hsl = vec4(h/255.0,s/255.0,l/255.0,1.0); gl_FragColor = hsl_to_rgb(hsl); // you need to implement a conversion method }

Edit (10/15/16) : Just wanted to show a proof of concept of a genetic algorithm. I am STILL running this code 24 hours later on a 100x100 set of random colors, but so far the output is beautiful!

Edit (10/26/16): Ive been running the genetic algorithm code for 12 days now..and its still optimizing the output. Its basically converged to some local minimum but it apparently is finding more improvement still:

Edit: 8/12/17 - I wrote a new random walk algorithm - basically you specify a number of "walkers", but instead of walking randomly - they will randomly choose another walker and either avoid them (choose the next available pixel furthest away) - or walk towards them (choose the next available pixel closest to them). An example grayscale output is here (I will be doing a full 4096x4096 color render after I wire up the coloring!) :