After improving performance I was able to run a HD render which took 2 days, I haven't given up on a 4k yet but it could take weeks.
HD Render
classusing Program
{System;
private static int MaxX =using 256;System.Collections.Generic;
private static int MaxY =using 128;System.Diagnostics;
private static intusing NumOfSteps;System.Drawing;
private static int ColorStep => 255 / (NumOfSteps -using 1);System.Drawing.Imaging;
private static List<Point> directions = new List<Point> { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0,using -1)System.IO;
using };System.Linq;
namespace SandBox
{
static void Main(string[]class args)Program
{
//Getprivate thestatic numberreadonly ofList<Point> permutationsdirections for= eachnew colorList<Point>
value base on the required number of pixels.{
NumOfSteps = (int)Math.Ceiling(Math.Pow( new Point(ulong1, 0)MaxX,
* new Point(ulong0, 1)MaxY,
new Point(-1., 0),
/ 3. new Point(0)), -1)
};
//Thestatic numvoid ofMain(string[] stepsargs)
calculation is going to give us more pixels{
than there are colors, so lets determine the number of extra colorsif (args.Length != 2)
decimal numToSkip = Convert.ToDecimal {
HelpFile(Math);
return;
}
try
{
var config = new ColorGeneratorConfig
{
XLength = int.PowParse(NumOfStepsargs[0]),
3) - MaxY * MaxX YLength = int.Parse(args[1])
};
//This factor will be used to as evenly asConsole.WriteLine("Starting possibleimage spreadgeneration outwith:");
the colors to be skipped so there are no large gaps in the spectrum Console.WriteLine($"\tDimensions:\t\t{config.XLength} X {config.YLength}");
decimal iterationFactor = numToSkip / Convert Console.ToDecimalWriteLine(Math$"\tSteps Per Channel:\t{config.Pow(NumOfSteps,}");
3 Console.WriteLine($"\tStep Size:\t\t{config.ColorStep}");
Console.WriteLine($"\tSteps to Skip:\t\t{config.StepsToSkip}\n");
//Every iteration of our color generation loop will addvar therunner iterationfactor= tonew thisTaskRunner();
accumlator which is used to know when to skip var colors = runner.Run(() => GenerateColorList(config), "color selection");
decimal iterationAccumulator var pixels = 0;runner.Run(() => BuildPixelArray(colors, config), "pixel array generation");
runner.Run(() => OutputBitmap(pixels, config), "bitmap creation");
}
catch (Exception ex)
{
HelpFile("There was an issue in execution");
}
Console.ReadLine();
}
var colorsprivate =static newvoid List<Color>HelpFile(string errorMessage = "");
for{
(var r const string Header = 0;"Generates ran <image NumOfSteps;with r++)
every pixel having a unique color";
for Console.WriteLine(varerrorMessage g== =string.Empty 0;? gHeader <: NumOfSteps;$"An g++error has occured: {errorMessage}\n Ensure the Arguments you have provided are valid");
Console.WriteLine();
for Console.WriteLine(var$"{AppDomain.CurrentDomain.FriendlyName} bX =Y");
0; b < NumOfSteps; b++ Console.WriteLine();
Console.WriteLine("X\t\tThe Length of the X dimension eg: {256");
Console.WriteLine("Y\t\tThe Length of the Y dimension eg: 128");
iterationAccumulator += iterationFactor; }
public static List<Color> GenerateColorList(ColorGeneratorConfig config)
{
//IfEvery iteration of our accumulatorcolor hasgeneration reachedloop 1,will thenadd subtractthe oneiterationfactor andto this accumlator which is used to know when to skip
this iteration decimal iterationAccumulator = 0;
var colors = new List<Color>();
if for (iterationAccumulatorvar >r 1= 0; r < config.NumOfSteps; r++)
for (var g = 0; g < config.NumOfSteps; g++)
for (var b = 0; b < config.NumOfSteps; b++)
{
iterationAccumulator += config.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*config.ColorStep, g*config.ColorStep,b*config.ColorStep));
}
return colors;
}
public static Color?[,] BuildPixelArray(List<Color> colors, ColorGeneratorConfig config)
{
colors //Get a random color to start with.Add
var random = new Random(Guid.NewGuid().GetHashCode());
var nextColor = colors[random.Next(colors.Count)];
var pixels = new Color?[config.FromArgbXLength, config.YLength];
var currPixel = new Point(r0, *0);
ColorStep var i = 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)
{
i++;
//Set the current pixel and remove the color from the list.
pixels[currPixel.X, gcurrPixel.Y] *= ColorStepnextColor;
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, bdirections.First());
* ColorStep //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 >= config.XLength || nextPixel.Y >= config.YLength || 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());
}
//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]; //This code sets the pixel to pick a color for and also gets the next color
var currPixel = new Point(0, 0); //We do this at the end of the loop so that we can also support haveing the first pixel set outside of the loop
currPixel = nextPixel;
//Since we've only generated exactly enough if (colors.Count to== fill0) ourcontinue;
image we can remove them from the list as we add them to our image andvar stopneighbours when= noneGetNeighbours(currPixel, arepixels, left.config);
while ( nextColor = colors.CountAsParallel().Aggregate((item1, >item2) 0=> GetAvgColorDiff(item1, neighbours) <
GetAvgColorDiff(item2, neighbours)
? item1
: item2);
}
return pixels;
}
public static void OutputBitmap(Color?[,] pixels, ColorGeneratorConfig config)
{
//SetNow thethat currentwe pixelhave andgenerated removeour image in the color from thearray list.
we need to copy it into a bitmap and save it to pixels[currPixelfile.X,
currPixel.Y] = nextColor;
var image = colors.RemoveAtnew Bitmap(colorsconfig.IndexOf(nextColor)XLength, config.YLength);
//Our image generation works in an inward spiral generation GetNext point will retrieve thefor next(var pixelx given= the0; currentx top< directionconfig.
var nextPixel = GetNextPoint(currPixel,XLength; directions.First()x++);
//If this next pixel were to be out of bounds (for first circle of spiral) or hit a(var previouslyy generated= pixel0; (fory all< otherconfig.YLength; circlesy++)
//Then we need to cycle the direction and get aimage.SetPixel(x, newy, nextpixels[x, pixely].Value);
ifusing (nextPixel.Xvar >=file MaxX= ||new nextPixelFileStream($@".Y >= MaxY || nextPixel\{config.XLength}X < 0 || nextPixel{config.Y < 0 || pixels[nextPixelYLength}.Xpng", nextPixelFileMode.Y] != nullCreate))
{
var d = directions.First();
directions.RemoveAt(0);
directionsimage.Add(d);
nextPixel = GetNextPointSave(currPixelfile, directionsImageFormat.First()Png);
}
}
//This code sets the pixel to pick a color for andstatic alsoPoint getsGetNextPoint(Point thecurrent, nextPoint colordirection)
//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 need toreturn enumeratenew allPoint(current.X the+ remainingdirection.X, colorscurrent.Y every+ timedirection.Y);
currPixel = nextPixel;}
static List<Color> GetNeighbours(Point current, ifColor?[,] (colors.Countgrid, !ColorGeneratorConfig config)
{
var list = 0new List<Color>();
foreach (var direction in directions)
{
nextColorvar xCoord = colorscurrent.Aggregate((item1,X item2)+ =>direction.X;
GetAvgColorDiff(item1, GetNeighbours var yCoord = current.Y + direction.Y;
if (currPixel,xCoord pixels))< 0 || xCoord >= config.XLength|| yCoord < GetAvgColorDiff(item2,0 GetNeighbours(currPixel,|| pixels)yCoord >= config.YLength)
? item1 : item2 {
continue;
}
var cell = grid[xCoord, yCoord];
if (cell.HasValue) list.Add(cell.Value);
}
return list;
}
//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 =static newdouble BitmapGetAvgColorDiff(MaxXColor source, MaxYIList<Color> colors);
for (int x = 0; x < MaxX; x++){
forreturn colors.Average(int y = 0; ycolor <=> MaxY;GetColorDiff(source, y++color));
image.SetPixel(x, y, pixels[x, y].Value);}
using (var file =static newint FileStreamGetColorDiff(@".\image.png"Color color1, FileMode.Create)Color color2)
{
imagevar redDiff = Math.SaveAbs(file,color1.R ImageFormat- color2.PngR);
var greenDiff = Math.Abs(color1.G - color2.G);
var blueDiff = Math.Abs(color1.B - color2.B);
return redDiff + greenDiff + blueDiff;
}
Console.ReadLine();
}
static Point GetNextPoint(Point current,public Pointclass direction)ColorGeneratorConfig
{
returnpublic newint Point(currentXLength { get; set; }
public int YLength { get; set; }
//Get the number of permutations for each color value base on the required number of pixels.X
+ direction public int NumOfSteps
=> (int)Math.XCeiling(Math.Pow((ulong)XLength * (ulong)YLength, current1.Y0 +/ directionColorDimensions));
//Calculate the increment for each step
public int ColorStep
=> 255 / (NumOfSteps - 1);
//Because NumOfSteps will either give the exact number of colors or more (never less) we will sometimes to to skip some
//this calculation tells how many we need to skip
public decimal StepsToSkip
=> Convert.YToDecimal(Math.Pow(NumOfSteps, ColorDimensions) - XLength * YLength);
//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
public decimal IterationFactor => StepsToSkip / Convert.ToDecimal(Math.Pow(NumOfSteps, ColorDimensions));
private double ColorDimensions => 3.0;
}
static List<Color> GetNeighbours(Point current,public Color?[,]class grid)TaskRunner
{
var list =private newStopwatch List<Color>();_sw;
foreachpublic TaskRunner(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_sw = grid[xCoord, yCoord];
if (cell.HasValue)new list.AddStopwatch(cell.Value);
}
return list;
}
static double GetAvgColorDiff public void Run(ColorAction sourcetask, IList<Color>string colorstaskName)
{
return colors Console.AverageWriteLine(color$"Starting =>{taskName}...");
GetColorDiff _sw.Start(source,);
color task();
_sw.Stop();
Console.WriteLine($"Finished {taskName}. Elapsed(ms): {_sw.ElapsedMilliseconds}");
Console.WriteLine();
_sw.Reset();
}
static int GetColorDiff public T Run<T>(ColorFunc<T> color1task, Colorstring color2taskName)
{
var redDiff = Math Console.AbsWriteLine(color1$"Starting {taskName}.R..");
- color2 _sw.RStart();
var greenDiffresult = Mathtask();
_sw.AbsStop(color1);
Console.GWriteLine($"Finished -{taskName}. color2Elapsed(ms): {_sw.GElapsedMilliseconds}");
var blueDiff = Math Console.AbsWriteLine(color1.B);
- color2 _sw.BReset();
return redDiffresult;
+ greenDiff + blueDiff; }
}
}