public static void Run(VideoConfiguration configuration)
        {
            var videoFile           = configuration.VideoFile;
            var outputBlueprintFile = configuration.OutputBlueprint;
            var outputJsonFile      = configuration.OutputJson;
            var frameWidth          = configuration.FrameWidth ?? 32;
            var frameHeight         = configuration.FrameHeight ?? 32;
            var colorMode           = configuration.ColorMode ?? ColorMode.Monochrome;
            var ditheringMode       = configuration.DitheringMode ?? DitheringMode.None;
            var romHeight           = configuration.RomHeight ?? 2;

            var maxFrames = romHeight * 32;

            var videoBuffer = new MemoryStream();

            using (var videoStream = File.OpenRead(videoFile))
            {
                videoStream.CopyTo(videoBuffer);
            }

            var pixelSize = colorMode switch
            {
                ColorMode.Monochrome => 1,
                ColorMode.RedGreenBlue => 2,
                ColorMode.RedGreenBlueWhite => 2,
                _ => throw new Exception($"Unexpected color mode: {colorMode}")
            };

            var palette = colorMode switch
            {
                ColorMode.Monochrome => GeneratePalette(new HdrColor[]
                {
                    HdrColor.FromRgb(1, 1, 1)
                }),
                ColorMode.RedGreenBlue => GeneratePalette(new HdrColor[]
                {
                    HdrColor.FromRgb(1, 0, 0),
                    HdrColor.FromRgb(0, 1, 0),
                    HdrColor.FromRgb(0, 0, 1)
                }),
                ColorMode.RedGreenBlueWhite => GeneratePalette(new HdrColor[]
                {
                    HdrColor.FromRgb(0.8, 0, 0),
                    HdrColor.FromRgb(0, 0.8, 0),
                    HdrColor.FromRgb(0, 0, 0.8),
                    HdrColor.FromRgb(0.8, 0.8, 0.8)
                }),
                _ => throw new Exception($"Unexpected color mode: {colorMode}")
            };

            // Information on dithering: https://cmitja.files.wordpress.com/2015/01/hellandtanner_imagedithering11algorithms.pdf
            var ditheringWeights = ditheringMode switch
            {
                DitheringMode.None => Array.Empty <DitheringWeight>(),
                DitheringMode.Sierra => new DitheringWeight[]
                {
                    new DitheringWeight(5 / 32d, 1, 0),
                    new DitheringWeight(3 / 32d, 2, 0),
                    new DitheringWeight(2 / 32d, -2, 1),
                    new DitheringWeight(4 / 32d, -1, 1),
                    new DitheringWeight(5 / 32d, 0, 1),
                    new DitheringWeight(4 / 32d, 1, 1),
                    new DitheringWeight(2 / 32d, 2, 1),
                    new DitheringWeight(2 / 32d, -1, 2),
                    new DitheringWeight(3 / 32d, 0, 2),
                    new DitheringWeight(2 / 32d, 1, 2),
                },
                DitheringMode.SierraLite => new DitheringWeight[]
                {
                    new DitheringWeight(2 / 4d, 1, 0),
                    new DitheringWeight(1 / 4d, -1, 1),
                    new DitheringWeight(1 / 4d, 0, 1)
                },
                DitheringMode.Temporal => new DitheringWeight[]
                {
                    new DitheringWeight(2 / 6d, 1, 0, 0),
                    new DitheringWeight(1 / 6d, -1, 1, 0),
                    new DitheringWeight(1 / 6d, 0, 1, 0),
                    new DitheringWeight(2 / 6d, 0, 0, 1)
                },
                _ => throw new Exception($"Unexpected dithering mode: {ditheringMode}")
            };

            var rawFrameWidth  = frameWidth / pixelSize;
            var rawFrameHeight = frameHeight / pixelSize;

            var video    = new Video(videoBuffer, rawFrameWidth, rawFrameHeight);
            var rawFrame = new int[rawFrameWidth * rawFrameHeight];
            var frames   = new List <bool[, ]>();

            var ditheringFrames = ditheringWeights.Max(ditheringWeight => ditheringWeight.Frame) + 1;
            var colorErrors     = new HdrColor[ditheringFrames][, ];

            for (var index = 0; index < ditheringFrames; index++)
            {
                colorErrors[index] = new HdrColor[frameHeight, frameWidth];
            }

            while (video.AdvanceFrame(rawFrame) && frames.Count < maxFrames)
            {
                var frame = new bool[frameHeight, frameWidth];
                frames.Add(frame);

                var totalBrightness = 0d;

                for (var rawY = 0; rawY < rawFrameHeight; rawY++)
                {
                    for (var rawX = 0; rawX < rawFrameWidth; rawX++)
                    {
                        var color = System.Drawing.Color.FromArgb(rawFrame[rawX + rawY * rawFrameWidth]);
                        totalBrightness += color.GetBrightness();
                    }
                }

                var averageBrightness = totalBrightness / (rawFrameWidth * rawFrameHeight);

                const double gammaMultiplier = 2;
                var          gamma           = gammaMultiplier * (averageBrightness is > 0.1 and < 0.9 ? Math.Log(0.5, averageBrightness) : 1);

                for (var rawY = 0; rawY < rawFrameHeight; rawY++)
                {
                    for (var rawX = 0; rawX < rawFrameWidth; rawX++)
                    {
                        var x                   = rawX * pixelSize;
                        var y                   = rawY * pixelSize;
                        var rawColor            = HdrColor.FromArgb(rawFrame[rawX + rawY * rawFrameWidth]);
                        var gammaCorrectedColor = rawColor.Pow(gamma);
                        var ditheredColor       = gammaCorrectedColor + colorErrors[0][rawY, rawX];
                        var closestPaletteEntry = GetClosestPaletteEntry(palette, ditheredColor);
                        var newColorError       = ditheredColor - closestPaletteEntry.Color;
                        var outputColor         = closestPaletteEntry.OutputColor;

                        for (var subPixelY = 0; subPixelY < pixelSize; subPixelY++)
                        {
                            for (var subPixelX = 0; subPixelX < pixelSize; subPixelX++)
                            {
                                var subPixelIndex = subPixelX + subPixelY * pixelSize;

                                if (subPixelIndex < outputColor.Length)
                                {
                                    frame[y + subPixelY, x + subPixelX] = outputColor[subPixelIndex];
                                }
                            }
                        }

                        for (var index = 0; index < ditheringWeights.Length; index++)
                        {
                            var ditheringWeight = ditheringWeights[index];
                            var errorX          = rawX + ditheringWeight.X;
                            var errorY          = rawY + ditheringWeight.Y;

                            if (errorX >= 0 && errorX < frameWidth && errorY < frameHeight)
                            {
                                colorErrors[ditheringWeight.Frame][errorY, errorX] += newColorError * ditheringWeight.Weight;
                            }
                        }
                    }
                }

                for (var index = 0; index < ditheringFrames - 1; index++)
                {
                    colorErrors[index] = colorErrors[index + 1];
                }

                colorErrors[ditheringFrames - 1] = new HdrColor[frameHeight, frameWidth];

                Console.Write('.');
            }

            Console.WriteLine($"Frames: {frames.Count}");

            var blueprint = VideoRomGenerator.Generate(new VideoMemoryConfiguration
            {
                SnapToGrid  = configuration.SnapToGrid,
                X           = configuration.X,
                Y           = configuration.Y,
                Width       = frameWidth,
                Height      = romHeight,
                BaseAddress = configuration.BaseAddress
            }, frames);

            BlueprintUtil.PopulateIndices(blueprint);

            var blueprintWrapper = new BlueprintWrapper {
                Blueprint = blueprint
            };

            BlueprintUtil.WriteOutBlueprint(outputBlueprintFile, blueprintWrapper);
            BlueprintUtil.WriteOutJson(outputJsonFile, blueprintWrapper);
        }
        public static void Run(VideoConfiguration configuration)
        {
            var videoFile           = configuration.VideoFile;
            var outputBlueprintFile = configuration.OutputBlueprint;
            var outputJsonFile      = configuration.OutputJson;
            var frameWidth          = configuration.FrameWidth ?? 32;
            var frameHeight         = configuration.FrameHeight ?? 32;
            var colorMode           = configuration.ColorMode ?? ColorMode.Monochrome;
            var ditherSize          = configuration.DitherSize ?? 1;
            var useEdgeDetection    = configuration.UseEdgeDetection ?? false;
            var romHeight           = configuration.RomHeight ?? 2;

            var maxFrames = romHeight * 32;

            var videoBuffer = new MemoryStream();

            using (var videoStream = File.OpenRead(videoFile))
            {
                videoStream.CopyTo(videoBuffer);
            }

            var basePixelSize = colorMode switch
            {
                ColorMode.Monochrome => 1,
                ColorMode.RedGreenBlue => 2,
                ColorMode.RedGreenBlueWhite => 2,
                _ => throw new Exception($"Unexpected color mode: {colorMode}")
            };
            var pixelSize = basePixelSize * ditherSize;

            var rawFrameWidth  = frameWidth / pixelSize;
            var rawFrameHeight = frameHeight / pixelSize;

            var video    = new Video(videoBuffer, rawFrameWidth, rawFrameHeight);
            var rawFrame = new int[rawFrameWidth * rawFrameHeight];
            var frames   = new List <bool[, ]>();

            while (video.AdvanceFrame(rawFrame) && frames.Count < maxFrames)
            {
                var frame = new bool[frameHeight, frameWidth];
                frames.Add(frame);

                var brightnessCutoff = 0d;

                if (ditherSize == 1)
                {
                    var totalBrightness = 0d;

                    for (var rawY = 0; rawY < rawFrameHeight; rawY++)
                    {
                        for (var rawX = 0; rawX < rawFrameWidth; rawX++)
                        {
                            var color = System.Drawing.Color.FromArgb(rawFrame[rawX + rawY * rawFrameWidth]);
                            totalBrightness += color.GetBrightness();
                        }
                    }

                    brightnessCutoff = totalBrightness / (rawFrameWidth * rawFrameHeight);
                }

                for (var rawY = 0; rawY < rawFrameHeight; rawY++)
                {
                    for (var rawX = 0; rawX < rawFrameWidth; rawX++)
                    {
                        var x      = rawX * pixelSize;
                        var y      = rawY * pixelSize;
                        var color  = SysColor.FromArgb(rawFrame[rawX + rawY * rawFrameWidth]);
                        var levels = ditherSize * ditherSize + 1;

                        switch (colorMode)
                        {
                        case ColorMode.Monochrome:
                        {
                            var brightness = color.GetBrightness();

                            for (var ditherY = 0; ditherY < ditherSize; ditherY++)
                            {
                                for (var ditherX = 0; ditherX < ditherSize; ditherX++)
                                {
                                    var currentBrightnessCutoff = ditherSize == 1 ? brightnessCutoff : (double)(ditherX + ditherY * ditherSize + 1) / levels;
                                    frame[y + ditherY, x + ditherX] = brightness > currentBrightnessCutoff;
                                }
                            }
                        }

                        break;

                        case ColorMode.RedGreenBlue:
                            for (var ditherY = 0; ditherY < ditherSize; ditherY++)
                            {
                                for (var ditherX = 0; ditherX < ditherSize; ditherX++)
                                {
                                    var currentBrightnessCutoff = ditherSize == 1 ? brightnessCutoff : (double)(ditherX + ditherY * ditherSize + 1) / levels;
                                    var cutoff = (byte)(currentBrightnessCutoff * 255);
                                    var red    = color.R >= cutoff;
                                    var green  = color.G >= cutoff;
                                    var blue   = color.B >= cutoff;
                                    var baseX  = x + ditherX * 2;
                                    var baseY  = y + ditherY * 2;

                                    frame[baseY, baseX]     = red;
                                    frame[baseY, baseX + 1] = green;
                                    frame[baseY + 1, baseX] = blue;
                                }
                            }

                            break;

                        case ColorMode.RedGreenBlueWhite:
                            for (var ditherY = 0; ditherY < ditherSize; ditherY++)
                            {
                                for (var ditherX = 0; ditherX < ditherSize; ditherX++)
                                {
                                    var currentBrightnessCutoff = ditherSize == 1 ? brightnessCutoff : (double)(ditherX + ditherY * ditherSize + 1) / levels;
                                    var cutoff = (byte)(currentBrightnessCutoff * 255);
                                    var red    = color.R >= cutoff;
                                    var green  = color.G >= cutoff;
                                    var blue   = color.B >= cutoff;

                                    bool white = useEdgeDetection
                                            ? DetectEdge(color, rawFrame, rawX, rawY, rawFrameWidth, -1, 0) ||
                                                 DetectEdge(color, rawFrame, rawX, rawY, rawFrameWidth, 0, -1) ||
                                                 DetectEdge(color, rawFrame, rawX, rawY, rawFrameWidth, -1, -1)
                                            : red && green && blue && color.GetBrightness() >= (currentBrightnessCutoff + 1) / 2;

                                    var baseX = x + ditherX * 2;
                                    var baseY = y + ditherY * 2;

                                    frame[baseY, baseX]         = red;
                                    frame[baseY, baseX + 1]     = green;
                                    frame[baseY + 1, baseX]     = blue;
                                    frame[baseY + 1, baseX + 1] = white;
                                }
                            }

                            break;
                        }
                    }
                }
            }

            Console.WriteLine($"Frames: {frames.Count}");

            var blueprint = VideoRomGenerator.Generate(new VideoMemoryConfiguration
            {
                SnapToGrid  = configuration.SnapToGrid,
                X           = configuration.X,
                Y           = configuration.Y,
                Width       = frameWidth,
                Height      = romHeight,
                BaseAddress = configuration.BaseAddress
            }, frames);

            BlueprintUtil.PopulateIndices(blueprint);

            var blueprintWrapper = new BlueprintWrapper {
                Blueprint = blueprint
            };

            BlueprintUtil.WriteOutBlueprint(outputBlueprintFile, blueprintWrapper);
            BlueprintUtil.WriteOutJson(outputJsonFile, blueprintWrapper);
        }