// Converts the specified image into an AllRGB version by looping // randomly through the source pixels and choosing the 'nearest' // remaining color to map it to. The number of color bits per channel // used dictates the size of the output image ([image]_allRGBv2.png). // Uses the specified color space for coordinate locations. static void allRGBify(string path, string maskPath, int bitsPerChannel, ColorSpace cs) { if ((bitsPerChannel & 1) != 0) { Console.Out.WriteLine("bitsPerChannel must be divisible by 2"); return; } else if (bitsPerChannel > 8) { Console.Out.WriteLine("Only up to 8 bits per channel are supported"); return; } DateTime startTime = DateTime.Now; // imageSize = 2^(3*bitsPerChannel/2) int imageSize = 1 << (3 * bitsPerChannel >> 1); int numPixels = imageSize * imageSize; Bitmap bitmap = new Bitmap(Image.FromFile(path), imageSize, imageSize); BitmapData bitmapData = bitmap.LockBits( new Rectangle(0, 0, imageSize, imageSize), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); // Load the raw pixel data, BGRBGRBGR..... Note that it may contain // padding at the end of rows; need to be very careful when indexing! int pixelBytes = bitmapData.Stride * bitmapData.Height; Debug.Assert(pixelBytes >= numPixels * 3); byte[] pixels = new byte[pixelBytes]; System.Runtime.InteropServices.Marshal.Copy( bitmapData.Scan0, pixels, 0, pixelBytes); // If available, load the pixel mask as well // True is high priority, false is low. bool[,] maskFlags = new bool[imageSize, imageSize]; if (maskPath != null && maskPath != "") { Bitmap mask = new Bitmap(Image.FromFile(maskPath), imageSize, imageSize); for (int y = 0; y < imageSize; ++y) { for (int x = 0; x < imageSize; ++x) { if (mask.GetPixel(x, y).R == 0) { maskFlags[y, x] = true; } } } } // Set up error diffusion, if it is turned on. At the same time, // calculate the average color of the image for the initial error // term. float[, ,] pixelError = null; // To be added to the corresponding // pixel's color to get the target // color for lookup. // Note: Order is y,x,{R,G,B}. bool[,] donePixels = null; if (USE_ERROR_DIFFUSION) { donePixels = new bool[imageSize, imageSize]; pixelError = new float[imageSize, imageSize, 3]; initializeErrorDiffusion(pixelError, pixels, bitmapData.Stride, imageSize); } // A random ordering of the pixels to process. Pair<int, int>[] coords = getRandomPixelOrdering(maskFlags, imageSize, imageSize); // A KDTree of all the candidate colors the pixels can be mapped to. // We periodically need to rebuild this tree to keep it from getting // unbalanced. KDTree kd = buildKDTreeOfColors(bitsPerChannel, cs); int pixelsTillRebuild = coords.Length * 1 / 4; pixelsTillRebuild = Math.Min(pixelsTillRebuild, 1 << 19); // Actually look up the colours, writing back to the pixel array as // we go. for (int i = 0; i < coords.Length; ++i) { if ((i & ((1 << 7) - 1)) == 0) { Console.Out.WriteLine("Done " + i + " pixels"); // If 'r' is caught, rebuild spatial index immediately. if (Console.KeyAvailable) { ConsoleKeyInfo key = Console.ReadKey(true); switch (key.KeyChar) { case 'r': pixelsTillRebuild = 0; break; default: break; } } } if (pixelsTillRebuild == 0) { rebuildKDTree(ref kd); pixelsTillRebuild = Math.Max((coords.Length - i) * 1 / 4, 1000); pixelsTillRebuild = Math.Min(pixelsTillRebuild, 1 << 19); } pixelsTillRebuild--; Pair<int, int> coord = coords[i]; int x = coord.First; int y = coord.Second; int pixelIndex = y * bitmapData.Stride + 3 * x; byte oldR; byte oldG; byte oldb; if (USE_ERROR_DIFFUSION) { oldR = capToByte(pixels[pixelIndex + 2] + pixelError[y, x, 0]); oldG = capToByte(pixels[pixelIndex + 1] + pixelError[y, x, 1]); oldb = capToByte(pixels[pixelIndex + 0] + pixelError[y, x, 2]); } else { oldR = pixels[pixelIndex + 2]; oldG = pixels[pixelIndex + 1]; oldb = pixels[pixelIndex + 0]; } ColorLocation oldColor = new ColorLocation(oldR, oldG, oldb, cs); ColorLocation newColor = (ColorLocation)kd.nearest(oldColor.Location); kd.delete(newColor.Location); if (USE_ERROR_DIFFUSION) { pixelError[y, x, 0] += pixels[pixelIndex + 2] - newColor.R; pixelError[y, x, 1] += pixels[pixelIndex + 1] - newColor.G; pixelError[y, x, 2] += pixels[pixelIndex + 0] - newColor.B; markPixelDoneAndSpreadError(donePixels, pixelError, imageSize, x, y); } pixels[pixelIndex + 2] = newColor.R; pixels[pixelIndex + 1] = newColor.G; pixels[pixelIndex + 0] = newColor.B; } // Finally, load the pixels back into the bitmap and save out. System.Runtime.InteropServices.Marshal.Copy(pixels, 0, bitmapData.Scan0, pixelBytes); bitmap.UnlockBits(bitmapData); string newFileName = Path.GetDirectoryName(path) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(path) + "_allRGBv2.png"; bitmap.Save(newFileName, ImageFormat.Png); TimeSpan totalTime = DateTime.Now - startTime; Console.Out.WriteLine("Generating " + Path.GetFileName(newFileName) + " took " + totalTime.ToString()); }
// Build a KDTree of all the possible colors, indexed by location in the // chosen color space. When fewer than 8 bits per pixel are used, the // low order bits are skipped within each channel. static KDTree buildKDTreeOfColors(int bitsPerChannel, ColorSpace cs) { int colorsPerChannel = 1 << bitsPerChannel; int shift = 8 - bitsPerChannel; ColorLocation[] colors = new ColorLocation[colorsPerChannel * colorsPerChannel * colorsPerChannel]; int colorIndex = 0; for (int r = 0; r < colorsPerChannel; ++r) { for (int g = 0; g < colorsPerChannel; ++g) { for (int b = 0; b < colorsPerChannel; ++b) { ColorLocation c = new ColorLocation((byte)(r << shift), (byte)(g << shift), (byte)(b << shift), cs); colors[colorIndex] = c; colorIndex++; } } } colors.Shuffle(); KDTree kd = new KDTree(); for (int i = 0; i < colors.Length; ++i) { ColorLocation c = colors[i]; kd.insert(c.Location, c); } colors = null; return kd; }