You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于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. 基础版本实现(遍历像素替换)

GetPixelSetPixel实现最基础的转换,适合小图像:

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操作内存)

GetPixelSetPixel的性能很差,处理大图像时会很慢。推荐使用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

火山引擎 最新活动