SkiaSharp中如何实现自定义灰度颜色重映射?
我正在开发一款热力图生成程序,原本基于GDI+编写相关代码,现因多种原因决定迁移至SkiaSharp。当前我有一个灰度图块,其中白色代表最大值,纯黑色为透明。在GDI+/C#中,我可以通过ColorMap和SetRemapTable快速实现自定义颜色重映射。我了解过SkiaSharp的ColorFilter,但它采用矩阵变换,无法满足我的自定义重映射需求。请问SkiaSharp是否有类似GDI+的颜色重映射机制?还是我需要手动遍历每个像素进行重新着色?
原GDI+代码:
ImageAttributes imageAttributes = new ImageAttributes(); ColorMap[] remapTable = new ColorMap[256]; Color[] scale = IncandescentHeatScale(); for (int i = 0; i < 256; i++) { remapTable[i] = new ColorMap() { OldColor = Color.FromArgb(i, i, i), NewColor = scale[i] }; } imageAttributes.SetRemapTable(remapTable); var outTile = new Bitmap(TileSize, TileSize); using (var g = Graphics.FromImage(outTile)) { g.DrawImage(tile, new Rectangle(0, 0, TileSize, TileSize), padding, padding, TileSize, TileSize, GraphicsUnit.Pixel, imageAttributes); }
参考的颜色刻度生成函数:
static Color[] GetHeatScale(float[] points, Color[] colors) { var bm = new Bitmap(256, 1); using (Graphics g = Graphics.FromImage(bm)) { LinearGradientBrush brush = new LinearGradientBrush(new Point(0, 0), new Point(256, 0), colors[0], colors[colors.Length - 1]); var cb = new ColorBlend(); cb.Colors = colors; cb.Positions = points; brush.InterpolationColors = cb; g.FillRectangle(brush, 0, 0, 256, 1); } return Enumerable.Range(0, 256).Select(x => bm.GetPixel(x, 0)).ToArray(); } static Color[] IncandescentHeatScale() { float[] points = new float[] { 0.0f, 0.333f, 0.6666f, 1f }; Color[] colors = new Color[] { Color.Black, Color.DarkRed, Color.Yellow, Color.White }; return GetHeatScale(points, colors); }
回答
好消息是,SkiaSharp确实有和GDI+ ColorMap功能几乎一致的颜色重映射方案——SKColorFilter.CreateTable(),它基于查找表(LUT)实现,完美适配你256级灰度图的自定义着色需求,完全不用手动遍历每个像素。
核心原理
SKColorFilter.CreateTable()允许你为红、绿、蓝、alpha四个通道分别提供一个长度为256的字节数组,输入像素的每个通道值(0-255)会直接被替换成对应数组中的值。对于你的灰度图场景,因为输入的R、G、B通道值完全相同,你可以复用同一个热力颜色刻度表到RGB三个通道,alpha通道则可以保持原样(或者根据你的透明需求调整)。
迁移后的代码实现
第一步:将GDI+颜色刻度转为SkiaSharp格式
你可以直接复用原来的IncandescentHeatScale()函数,只需要把生成的Color数组转换为SkiaSharp的SKColor数组:
// 复用原GDI+的颜色刻度生成逻辑 Color[] gdiHeatScale = IncandescentHeatScale(); // 转换为SKColor数组 SKColor[] skHeatScale = gdiHeatScale.Select(c => new SKColor(c.R, c.G, c.B, c.A)).ToArray();
第二步:构建颜色查找表并创建滤镜
// 为RGB三个通道构建查找表(因为输入是灰度图,三个通道值相同) byte[] redTable = skHeatScale.Select(c => c.Red).ToArray(); byte[] greenTable = skHeatScale.Select(c => c.Green).ToArray(); byte[] blueTable = skHeatScale.Select(c => c.Blue).ToArray(); // alpha通道保持原样(如果原灰度图的黑色需要透明,也可以在这里自定义) byte[] alphaTable = Enumerable.Range(0, 256).Select(i => (byte)i).ToArray(); // 创建颜色重映射滤镜 using var colorFilter = SKColorFilter.CreateTable(redTable, greenTable, blueTable, alphaTable);
第三步:应用滤镜绘制图像
用SKCanvas绘制时,只需要把滤镜赋值给SKPaint,就能实现和GDI+ DrawImage带ImageAttributes完全一致的效果:
// 假设tile是你的输入SKBitmap using var outTile = new SKBitmap(TileSize, TileSize); using var canvas = new SKCanvas(outTile); using var paint = new SKPaint { ColorFilter = colorFilter }; // 对应原GDI+的DrawImage裁剪绘制逻辑 canvas.DrawBitmap( tile, new SKRect(padding, padding, padding + TileSize, padding + TileSize), // 源区域 new SKRect(0, 0, TileSize, TileSize), // 目标区域 paint );
可选:完全基于SkiaSharp生成热力颜色刻度
如果你想彻底摆脱GDI+依赖,也可以用SkiaSharp的SKShader实现线性渐变来生成颜色刻度:
static SKColor[] SkiaIncandescentHeatScale() { float[] points = new float[] { 0.0f, 0.333f, 0.6666f, 1f }; SKColor[] colors = new SKColor[] { SKColors.Black, SKColors.DarkRed, SKColors.Yellow, SKColors.White }; // 创建多色线性渐变着色器 using var shader = SKShader.CreateLinearGradient( new SKPoint(0, 0), new SKPoint(256, 0), colors, points, SKShaderTileMode.Clamp ); // 绘制渐变到临时Bitmap using var tempBmp = new SKBitmap(256, 1); using var canvas = new SKCanvas(tempBmp); using var paint = new SKPaint { Shader = shader }; canvas.DrawRect(new SKRect(0, 0, 256, 1), paint); // 提取每个位置的颜色 SKColor[] scale = new SKColor[256]; for (int i = 0; i < 256; i++) { scale[i] = tempBmp.GetPixel(i, 0); } return scale; }
总结
SKColorFilter.CreateTable()完全替代了GDI+ ColorMap的重映射功能,性能上和原生实现相当,而且不需要手动操作像素,是最适合你的迁移方案。
内容的提问来源于stack exchange,提问作者jklemmack




