public void AssignBestMatchColors() { Image <Bgr, byte> result = ColorQuantization.AssignBestMatchColors(image, colorTable); Assert.AreEqual(new Bgr(0, 0, 0), result[0, 0]); Assert.AreEqual(new Bgr(0, 0, 0), result[0, 1]); Assert.AreEqual(new Bgr(100, 100, 100), result[1, 0]); Assert.AreEqual(new Bgr(0, 0, 0), result[1, 1]); }
public void BestMatch() { Assert.AreEqual(new Bgr(0, 0, 0), ColorQuantization.BestMatch(new Bgr(0, 0, 0), colorTable)); Assert.AreEqual(new Bgr(0, 0, 0), ColorQuantization.BestMatch(new Bgr(0, 100, 0), colorTable)); Assert.AreEqual(new Bgr(0, 0, 0), ColorQuantization.BestMatch(new Bgr(101, 0, 50), colorTable)); Assert.AreEqual(new Bgr(0, 0, 0), ColorQuantization.BestMatch(new Bgr(50, 50, 50), colorTable)); Assert.AreEqual(new Bgr(100, 100, 100), ColorQuantization.BestMatch(new Bgr(100, 100, 100), colorTable)); Assert.AreEqual(new Bgr(100, 100, 100), ColorQuantization.BestMatch(new Bgr(100, 1, 50), colorTable)); }
static Texture CreateDecalTexture(string name, Image <Rgba32>[] images, TextureSettings textureSettings) { // Create any missing mipmaps (this does not affect the palette, so it can be done up-front): for (int i = 1; i < images.Length; i++) { if (images[i] == null) { images[i] = images[0].Clone(context => context.Resize(images[0].Width >> i, images[0].Height >> i)); } } // The last palette color determines the color of the decal. All other colors are irrelevant - palette indexes are treated as alpha values instead. var decalColor = textureSettings.DecalColor ?? ColorQuantization.GetAverageColor(ColorQuantization.GetColorHistogram(images, color => color.A == 0)); var palette = Enumerable.Range(0, 255) .Select(i => new Rgba32((byte)i, (byte)i, (byte)i)) .Append(decalColor) .ToArray(); var textureData = images .Select(CreateDecalTextureData) .ToArray(); return(Texture.CreateMipmapTexture( name: name, width: images[0].Width, height: images[0].Height, imageData: textureData[0], palette: palette, mipmap1Data: textureData[1], mipmap2Data: textureData[2], mipmap3Data: textureData[3])); byte[] CreateDecalTextureData(Image <Rgba32> image) { var mode = textureSettings.DecalTransparencySource ?? DecalTransparencySource.AlphaChannel; var getPaletteIndex = (mode == DecalTransparencySource.AlphaChannel) ? (Func <Rgba32, byte>)(color => color.A) : (Func <Rgba32, byte>)(color => (byte)((color.R + color.G + color.B) / 3)); var data = new byte[image.Width * image.Height]; for (int y = 0; y < image.Height; y++) { var rowSpan = image.GetPixelRowSpan(y); for (int x = 0; x < image.Width; x++) { var color = rowSpan[x]; data[y * image.Width + x] = getPaletteIndex(color); } } return(data); } }
// TODO: Disable dithering for animated textures again? It doesn't actually remove all flickering, because different frames can still have different palettes...! static byte[] CreateTextureData( Image <Rgba32> image, Rgba32[] palette, IDictionary <Rgba32, int> colorIndexMappingCache, TextureSettings textureSettings, Func <Rgba32, bool> isTransparent, bool disableDithering) { var getColorIndex = ColorQuantization.CreateColorIndexLookup(palette, colorIndexMappingCache, isTransparent); var ditheringAlgorithm = textureSettings.DitheringAlgorithm ?? (disableDithering ? DitheringAlgorithm.None : DitheringAlgorithm.FloydSteinberg); switch (ditheringAlgorithm) { default: case DitheringAlgorithm.None: return(ApplyPaletteWithoutDithering()); case DitheringAlgorithm.FloydSteinberg: return(Dithering.FloydSteinberg(image, palette, getColorIndex, textureSettings.DitherScale ?? 0.75f, isTransparent)); } byte[] ApplyPaletteWithoutDithering() { var textureData = new byte[image.Width * image.Height]; for (int y = 0; y < image.Height; y++) { var rowSpan = image.GetPixelRowSpan(y); for (int x = 0; x < image.Width; x++) { var color = rowSpan[x]; textureData[y * image.Width + x] = (byte)getColorIndex(color); } } return(textureData); } }
private bool QuantizeImage(Image <Bgr, byte> i) { quantizedImage = ColorQuantization.AssignBestMatchColors(i, testTable); return(true); }
static Texture CreateTextureFromImage(string path, string textureName, TextureSettings textureSettings, bool isDecalsWad) { // Load the main texture image, and any available mipmap images: using (var images = new DisposableList <Image <Rgba32> >(GetMipmapFilePaths(path).Prepend(path) .Select(imagePath => File.Exists(imagePath) ? ImageReading.ReadImage(imagePath) : null))) { // Verify image sizes: if (images[0].Width % 16 != 0 || images[0].Height % 16 != 0) { throw new InvalidDataException($"Texture '{path}' width or height is not a multiple of 16."); } for (int i = 1; i < images.Count; i++) { if (images[i] != null && (images[i].Width != images[0].Width >> i || images[i].Height != images[0].Height >> i)) { throw new InvalidDataException($"Mipmap {i} for texture '{path}' width or height does not match texture size."); } } if (isDecalsWad) { return(CreateDecalTexture(textureName, images.ToArray(), textureSettings)); } var filename = Path.GetFileName(path); var isTransparentTexture = filename.StartsWith("{"); var isAnimatedTexture = AnimatedTextureNameRegex.IsMatch(filename); var isWaterTexture = filename.StartsWith("!"); // Create a suitable palette, taking special texture types into account: var transparencyThreshold = isTransparentTexture ? Clamp(textureSettings.TransparencyThreshold ?? 128, 0, 255) : 0; Func <Rgba32, bool> isTransparentPredicate = null; if (textureSettings.TransparencyColor != null) { var transparencyColor = textureSettings.TransparencyColor.Value; isTransparentPredicate = color => color.A < transparencyThreshold || (color.R == transparencyColor.R && color.G == transparencyColor.G && color.B == transparencyColor.B); } else { isTransparentPredicate = color => color.A < transparencyThreshold; } var colorHistogram = ColorQuantization.GetColorHistogram(images.Where(image => image != null), isTransparentPredicate); var maxColors = 256 - (isTransparentTexture ? 1 : 0) - (isWaterTexture ? 2 : 0); var colorClusters = ColorQuantization.GetColorClusters(colorHistogram, maxColors); // Always make sure we've got a 256-color palette (some tools can't handle smaller palettes): if (colorClusters.Length < maxColors) { colorClusters = colorClusters .Concat(Enumerable .Range(0, maxColors - colorClusters.Length) .Select(i => (new Rgba32(), new[] { new Rgba32() }))) .ToArray(); } // Make palette adjustments for special textures: if (isWaterTexture) { var fogColor = textureSettings.WaterFogColor ?? ColorQuantization.GetAverageColor(colorHistogram); var fogIntensity = new Rgba32((byte)Clamp(textureSettings.WaterFogColor?.A ?? (int)((1f - GetBrightness(fogColor)) * 255), 0, 255), 0, 0); colorClusters = colorClusters.Take(3) .Append((fogColor, new[] { fogColor })) // Slot 3: water fog color .Append((fogIntensity, new[] { fogIntensity })) // Slot 4: fog intensity (stored in red channel) .Concat(colorClusters.Skip(3)) .ToArray(); } if (isTransparentTexture) { var colorKey = new Rgba32(0, 0, 255); colorClusters = colorClusters .Append((colorKey, new[] { colorKey })) // Slot 255: used for transparent pixels .ToArray(); } // Create the actual palette, and a color index lookup cache: var palette = colorClusters .Select(cluster => cluster.Item1) .ToArray(); var colorIndexMappingCache = new Dictionary <Rgba32, int>(); for (int i = 0; i < colorClusters.Length; i++) { (_, var colors) = colorClusters[i]; foreach (var color in colors) { colorIndexMappingCache[color] = i; } } // Create any missing mipmaps: for (int i = 1; i < images.Count; i++) { if (images[i] == null) { images[i] = images[0].Clone(context => context.Resize(images[0].Width >> i, images[0].Height >> i)); } } // Create texture data: var textureData = images .Select(image => CreateTextureData(image, palette, colorIndexMappingCache, textureSettings, isTransparentPredicate, disableDithering: isAnimatedTexture)) .ToArray(); return(Texture.CreateMipmapTexture( name: textureName, width: images[0].Width, height: images[0].Height, imageData: textureData[0], palette: palette, mipmap1Data: textureData[1], mipmap2Data: textureData[2], mipmap3Data: textureData[3])); } }