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); }