#C#
So I started working on this just as a fun exercise and ended up with an output that at least to me looks pretty neat. The key difference in my solution to (at least) most others is that I'm generating exactly the number of colors needed to start with and evenly spacing the generation out from pure white to pure black. I'm also setting colors working in an inward spiral and choosing the next color based on the average of the color diff between all neighbors that have been set.
Here is a small sample output that I've produced so far, I'm working on a 4k render but I expect it to take upwards of a day to finish.
Second run at 360 x 240 produced a much smoother image
class Program
{
private static int MaxX = 256;
private static int MaxY = 128;
private static int NumOfSteps;
private static int ColorStep => 255 / (NumOfSteps - 1);
private static List<Point> directions = new List<Point> { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1) };
static void Main(string[] args)
{
//Get the number of permutations for each color value base on the required number of pixels.
NumOfSteps = (int)Math.Ceiling(Math.Pow((ulong)MaxX * (ulong)MaxY, (1.0 / 3.0)));
//The num of steps calculation is going to give us more pixels than there are colors, so lets determine the number of extra colors
decimal numToSkip = Convert.ToDecimal(Math.Pow(NumOfSteps, 3) - MaxY * MaxX);
//This factor will be used to as evenly as possible spread out the colors to be skipped so there are no large gaps in the spectrum
decimal iterationFactor = numToSkip / Convert.ToDecimal(Math.Pow(NumOfSteps, 3));
//Every iteration of our color generation loop will add the iterationfactor to this accumlator which is used to know when to skip
decimal iterationAccumulator = 0;
var colors = new List<Color>();
for (var r = 0; r < NumOfSteps; r++)
for (var g = 0; g < NumOfSteps; g++)
for (var b = 0; b < NumOfSteps; b++)
{
iterationAccumulator += iterationFactor;
//If our accumulator has reached 1, then subtract one and skip this iteration
if (iterationAccumulator > 1)
{
iterationAccumulator -= 1;
continue;
}
colors.Add(Color.FromArgb(r * ColorStep, g * ColorStep, b * ColorStep));
}
//Get a random color to start with.
var random = new Random(Guid.NewGuid().GetHashCode());
var nextColor = colors[random.Next(colors.Count)];
var pixels = new Color?[MaxX, MaxY];
var currPixel = new Point(0, 0);
//Since we've only generated exactly enough colors to fill our image we can remove them from the list as we add them to our image and stop when none are left.
while (colors.Count > 0)
{
//Set the current pixel and remove the color from the list.
pixels[currPixel.X, currPixel.Y] = nextColor;
colors.RemoveAt(colors.IndexOf(nextColor));
//Our image generation works in an inward spiral generation GetNext point will retrieve the next pixel given the current top direction.
var nextPixel = GetNextPoint(currPixel, directions.First());
//If this next pixel were to be out of bounds (for first circle of spiral) or hit a previously generated pixel (for all other circles)
//Then we need to cycle the direction and get a new next pixel
if (nextPixel.X >= MaxX || nextPixel.Y >= MaxY || nextPixel.X < 0 || nextPixel.Y < 0 || pixels[nextPixel.X, nextPixel.Y] != null)
{
var d = directions.First();
directions.RemoveAt(0);
directions.Add(d);
nextPixel = GetNextPoint(currPixel, directions.First());
}
//This code sets the pixel to pick a color for and also gets the next color
//We do this at the end of the loop so that we can also support haveing the first pixel set outside of the loop
//This is the major bottleneck of the algorithm since we are resorting the list of colors every time.
currPixel = nextPixel;
if (colors.Count != 0)
{
nextColor = colors.Aggregate((item1, item2) => GetAvgColorDiff(item1, GetNeighbours(currPixel, pixels)) < GetAvgColorDiff(item2, GetNeighbours(currPixel, pixels)) ? item1 : item2);
}
}
//Now that we have generated our image in the color array we need to copy it into a bitmap and save it to file.
var image = new Bitmap(MaxX, MaxY);
for (int x = 0; x < MaxX; x++)
for (int y = 0; y < MaxY; y++)
image.SetPixel(x, y, pixels[x, y].Value);
using (var file = new FileStream(@".\image.png", FileMode.Create))
{
image.Save(file, ImageFormat.Png);
}
Console.ReadLine();
}
static Point GetNextPoint(Point current, Point direction)
{
return new Point(current.X + direction.X, current.Y + direction.Y);
}
static List<Color> GetNeighbours(Point current, Color?[,] grid)
{
var list = new List<Color>();
foreach (var direction in directions)
{
var xCoord = current.X + direction.X;
var yCoord = current.Y + direction.Y;
if (xCoord < 0 || xCoord >= MaxX || yCoord < 0 || yCoord >= MaxY)
{
continue;
}
var cell = grid[xCoord, yCoord];
if (cell.HasValue) list.Add(cell.Value);
}
return list;
}
static double GetAvgColorDiff(Color source, IList<Color> colors)
{
return colors.Average(color => GetColorDiff(source, color));
}
static int GetColorDiff(Color color1, Color color2)
{
var redDiff = Math.Abs(color1.R - color2.R);
var greenDiff = Math.Abs(color1.G - color2.G);
var blueDiff = Math.Abs(color1.B - color2.B);
return redDiff + greenDiff + blueDiff;
}
}