基于C#实现图像颜色重映射:用22种颜色绘制图像的最优方案咨询
C#实现将图像颜色映射到指定22种颜色的最优方案
Hey there, I’ve tackled a similar requirement before—converting any image to a fixed set of 22 colors boils down to color quantization, and for this specific scenario (fixed target palette), a nearest neighbor match is the most efficient and straightforward approach. Let me walk you through how to implement this in C# step by step.
1. 先定义你的22种目标颜色集合
首先你需要把指定的22种颜色存成一个Color数组,替换成你实际需要的颜色即可:
// 示例:替换为你自己的22种目标颜色 private static readonly Color[] TargetPalette = new[] { Color.Red, Color.Green, Color.Blue, Color.Yellow, Color.Cyan, Color.Magenta, Color.Orange, Color.Purple, Color.Brown, // ... 补充剩下的13种颜色 };
2. 核心逻辑:找到与像素最接近的目标色
我们需要计算原始像素颜色和每个目标色的“距离”,选择距离最小的那个作为替换色。这里有两种常用的距离计算方式:
2.1 RGB空间欧氏距离(快速但感知偏差)
这是最简单的计算方式,直接比较RGB三个通道的差值平方和(不用开根号,不影响大小比较,还能省计算):
private static double CalculateRgbDistance(Color original, Color target) { int rDiff = original.R - target.R; int gDiff = original.G - target.G; int bDiff = original.B - target.B; // 返回差值平方和,避免开根号的开销 return rDiff * rDiff + gDiff * gDiff + bDiff * bDiff; }
2.2 Lab空间距离(贴合人眼感知)
RGB空间的距离和人眼实际感知的颜色差异并不完全一致,如果需要更准确的匹配,可以先把RGB转成CIE Lab颜色空间(基于人眼视觉模型),再计算距离:
// 近似实现RGB转CIE Lab(如需高精度可参考标准转换公式) private static (double L, double a, double b) RgbToLab(Color color) { double r = color.R / 255.0; double g = color.G / 255.0; double b = color.B / 255.0; // 伽马校正 r = r > 0.04045 ? Math.Pow((r + 0.055) / 1.055, 2.4) : r / 12.92; g = g > 0.04045 ? Math.Pow((g + 0.055) / 1.055, 2.4) : g / 12.92; b = b > 0.04045 ? Math.Pow((b + 0.055) / 1.055, 2.4) : b / 12.92; // 转换为XYZ颜色空间(参考白点D65) double x = r * 0.4124 + g * 0.3576 + b * 0.1805; double y = r * 0.2126 + g * 0.7152 + b * 0.0722; double z = r * 0.0193 + g * 0.1192 + b * 0.9505; x /= 0.95047; y /= 1.0; z /= 1.08883; // 转换为Lab空间 x = x > 0.008856 ? Math.Pow(x, 1.0/3) : (7.787 * x) + 16.0/116; y = y > 0.008856 ? Math.Pow(y, 1.0/3) : (7.787 * y) + 16.0/116; z = z > 0.008856 ? Math.Pow(z, 1.0/3) : (7.787 * z) + 16.0/116; double L = (116 * y) - 16; double a = 500 * (x - y); double b = 200 * (y - z); return (L, a, b); } // 计算Lab空间的欧氏距离 private static double CalculateLabDistance(Color original, Color target) { var labOriginal = RgbToLab(original); var labTarget = RgbToLab(target); double lDiff = labOriginal.L - labTarget.L; double aDiff = labOriginal.a - labTarget.a; double bDiff = labOriginal.b - labTarget.b; return lDiff * lDiff + aDiff * aDiff + bDiff * bDiff; }
3. 基础版本实现(遍历像素替换)
用GetPixel和SetPixel实现最基础的转换,适合小图像:
public static Bitmap ConvertImageToTargetPalette(Bitmap originalImage) { Bitmap resultImage = new Bitmap(originalImage.Width, originalImage.Height); for (int y = 0; y < originalImage.Height; y++) { for (int x = 0; x < originalImage.Width; x++) { Color originalColor = originalImage.GetPixel(x, y); Color closestColor = FindClosestColor(originalColor); resultImage.SetPixel(x, y, closestColor); } } return resultImage; } // 找到最接近的目标色 private static Color FindClosestColor(Color originalColor) { Color closest = TargetPalette[0]; double minDistance = CalculateRgbDistance(originalColor, closest); foreach (Color targetColor in TargetPalette) { double currentDistance = CalculateRgbDistance(originalColor, targetColor); if (currentDistance < minDistance) { minDistance = currentDistance; closest = targetColor; } } // 如果要使用Lab距离,把上面的CalculateRgbDistance换成CalculateLabDistance即可 return closest; }
4. 性能优化(用LockBits操作内存)
GetPixel和SetPixel的性能很差,处理大图像时会很慢。推荐使用LockBits直接操作图像内存,速度能提升几十倍:
public static Bitmap ConvertImageToTargetPaletteFast(Bitmap originalImage) { Bitmap resultImage = new Bitmap(originalImage.Width, originalImage.Height); // 锁定原始图像和结果图像的内存区域 BitmapData originalData = originalImage.LockBits( new Rectangle(0, 0, originalImage.Width, originalImage.Height), ImageLockMode.ReadOnly, originalImage.PixelFormat); BitmapData resultData = resultImage.LockBits( new Rectangle(0, 0, resultImage.Width, resultImage.Height), ImageLockMode.WriteOnly, resultImage.PixelFormat); int bytesPerPixel = Image.GetPixelFormatSize(originalData.PixelFormat) / 8; int stride = originalData.Stride; IntPtr originalPtr = originalData.Scan0; IntPtr resultPtr = resultData.Scan0; // 分配缓冲区存储图像数据 byte[] originalBuffer = new byte[stride * originalImage.Height]; byte[] resultBuffer = new byte[stride * resultImage.Height]; // 把原始图像数据复制到缓冲区 Marshal.Copy(originalPtr, originalBuffer, 0, originalBuffer.Length); // 遍历每个像素 for (int y = 0; y < originalImage.Height; y++) { for (int x = 0; x < originalImage.Width; x++) { int bufferIndex = y * stride + x * bytesPerPixel; // 注意:内存中像素的通道顺序通常是BGRA(而非ARGB) byte blue = originalBuffer[bufferIndex]; byte green = originalBuffer[bufferIndex + 1]; byte red = originalBuffer[bufferIndex + 2]; // 如果图像带Alpha通道,可以读取bufferIndex+3的字节 Color originalColor = Color.FromArgb(red, green, blue); Color closestColor = FindClosestColor(originalColor); // 写入结果缓冲区 resultBuffer[bufferIndex] = closestColor.B; resultBuffer[bufferIndex + 1] = closestColor.G; resultBuffer[bufferIndex + 2] = closestColor.R; // 处理Alpha通道:resultBuffer[bufferIndex + 3] = closestColor.A; } } // 把结果缓冲区的数据写回图像 Marshal.Copy(resultBuffer, 0, resultPtr, resultBuffer.Length); // 解锁内存 originalImage.UnlockBits(originalData); resultImage.UnlockBits(resultData); return resultImage; }
5. 最终注意事项
- 如果你的目标色数量只有22种,遍历所有目标色找最近邻的开销完全可以忽略,不需要用KD树这类空间索引优化;
- 若追求视觉效果优先,优先选择Lab空间的距离计算;若追求速度优先,用RGB距离即可;
- 处理带Alpha通道的图像时,记得在代码中保留Alpha通道的处理逻辑(上面的注释已经标出)。
内容的提问来源于stack exchange,提问作者mtnair




