其实不用多言,需要这个的人自然知道这是什么。基于一系列点生成的热度图,放张图感受一下:
ma...大概就是这种样子。
实现方式实际上是在每个点上叠加高斯矩阵。高斯矩阵就是在二维平面上的高斯(正态)分布。
高斯分布的计算公式如下:
在二维上
简单的理解就是像下图一样,离(u1,u2)越近的点的值越大,越远的值越小。
将这样的小矩阵,根据点的位置和权重进行叠加。然后根据叠加后的数值转换成对应的颜色就好。
项目源码在github上有上传,有需要的可以直接去下载→https://github.com/RainkLH/HeatMapSharp
以下代码C#语言,通过对Bitmap类操作实现
要用到一下命名空间:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
public class HeatMapImage
{
///
/// width of img
///
private int w;
///
/// height of img
///
private int h;
///
/// gaussian kernel size
///
private int gSize;
///
/// gaussian kernel sigma
///
private double gSigma;
///
/// radius
///
private int r;
///
/// Two dimensional matrix corresponding to data list
///
private double[,] heatVals;
///
/// Color map matrix
///
private byte[] ColorArgbValues;
///
/// gaussian kernel
///
private double[,] kernel;
///
/// color numbers
///
private const int NUMCOLORS = 1000;
///
/// width of img
///
public int W { get => w; set => w = value; }
///
/// height of img
///
public int H { get => h; set => h = value; }
///
/// gaussian kernel
///
public double[,] Kernel { get => kernel; }
///
/// Two dimensional matrix corresponding to data list
///
public double[,] HeatVals { get => heatVals; }
///
/// construction
///
/// image width
/// image height
/// gaussian kernel size
/// gaussian kernel sigma
public HeatMapImage(int width, int height, int gSize, double gSigma)
{
this.w = width;
this.h = height;
CreateColorMap();
//对高斯核尺寸进行判断
if (gSize < 3 || gSize > 400)
{
throw new Exception("Kernel size is invalid");
}
this.gSize = gSize % 2 == 0 ? gSize + 1 : gSize;
//高斯的sigma值,计算半径r
this.r = this.gSize / 2;
this.gSigma = gSigma;
//计算高斯核
kernel = new double[this.gSize, this.gSize];
this.gaussiankernel();
//初始化高斯累加图
heatVals = new double[h, w];
}
///
/// 创建一个ColorMap,用于根据数值查询颜色
///
private void CreateColorMap()
{
ColorBlend colorBlend = new ColorBlend(8);
colorBlend.Colors = new Color[8]
{
Color.FromArgb(0, 255, 255, 255),
Color.FromArgb(10, 128, 0, 128),
Color.FromArgb(30, 128, 0, 255),
Color.FromArgb(70, 0, 0, 255),
Color.FromArgb(110, 0, 255, 0),
Color.FromArgb(130, 255, 255, 0),
Color.FromArgb(145, 255, 128, 0),
Color.FromArgb(155, 255, 0, 0)
};
colorBlend.Positions = new float[8] { 0.0f, 0.1f, 0.25f, 0.4f, 0.65f, 0.75f, 0.9f, 1.0f };
Color startColor = colorBlend.Colors[0];
Color endColor = colorBlend.Colors[colorBlend.Colors.Length - 1];
using (Bitmap colorMapBitmap = new Bitmap(NUMCOLORS, 1, PixelFormat.Format32bppArgb))
{
Rectangle colorRect = new Rectangle(0, 0, colorMapBitmap.Width, colorMapBitmap.Height);
using (Graphics bitmapGraphics = Graphics.FromImage(colorMapBitmap))
{
using (LinearGradientBrush brush = new LinearGradientBrush(colorRect, startColor, endColor, LinearGradientMode.Horizontal))
{
brush.InterpolationColors = colorBlend;
bitmapGraphics.FillRectangle(brush, colorRect);
}
}
BitmapData colorMapData = colorMapBitmap.LockBits(colorRect, ImageLockMode.ReadOnly, colorMapBitmap.PixelFormat);
IntPtr colorPtr = colorMapData.Scan0;
int colorBytes = Math.Abs(colorMapData.Stride) * colorMapBitmap.Height;
ColorArgbValues = new byte[colorBytes];
System.Runtime.InteropServices.Marshal.Copy(colorPtr, ColorArgbValues, 0, colorBytes);
colorMapBitmap.UnlockBits(colorMapData);
}
}
///
/// 高斯核
///
private void gaussiankernel()
{
for (int y = -r, i = 0; i < gSize; y++, i++)
{
for (int x = -r, j = 0; j < gSize; x++, j++)
{
kernel[i, j] = Math.Exp(((x * x) + (y * y)) / (-2 * gSigma * gSigma)) / (2 * Math.PI * gSigma * gSigma);
}
}
}
///
/// 高斯核乘上权重
///
///
///
private double[,] MultiplyKernel(double weight)
{
double[,] wKernel = (double[,])kernel.Clone();
for (int i = 0; i < gSize; i++)
{
for (int j = 0; j < gSize; j++)
{
wKernel[i, j] *= weight;
}
}
return wKernel;
}
///
/// 对所有权重进行归一化,压缩到颜色数量以内的值
///
private void RescaleArray()
{
float max = 0;
foreach (float value in heatVals)
{
if (value > max)
{
max = value;
}
}
for (int i = 0; i < heatVals.GetLength(0); i++)
{
for (int j = 0; j < heatVals.GetLength(1); j++)
{
heatVals[i, j] *= (NUMCOLORS - 1) / max;
if (heatVals[i, j] > NUMCOLORS - 1)
{
heatVals[i, j] = NUMCOLORS - 1;
}
}
}
}
///
/// 一次性设置一组数据
///
///
public void SetDatas(List datas)
{
foreach (DataType data in datas)
{
int i, j, tx, ty, ir, jr;
int radius = gSize >> 1;
int x = data.X;
int y = data.Y;
double[,] kernelMultiplied = MultiplyKernel(data.Weight);
for (i = 0; i < gSize; i++)
{
ir = i - radius;
ty = y + ir;
if (ty < 0)
{
continue;
}
if (ty >= h)
{
break;
}
for (j = 0; j < gSize; j++)
{
jr = j - radius;
tx = x + jr;
// skip column
if (tx < 0)
{
continue;
}
if (tx < w)
{
heatVals[ty, tx] += kernelMultiplied[i, j];
}
}
}
}
}
///
/// 逐个数据一个一个的存入
///
///
public void SetAData(DataType data)
{
int i, j, tx, ty, ir, jr;
int radius = gSize >> 1;
int x = data.X;
int y = data.Y;
double[,] kernelMultiplied = MultiplyKernel(data.Weight);
for (i = 0; i < gSize; i++)
{
ir = i - radius;
ty = y + ir;
if (ty < 0)
{
continue;
}
if (ty >= h)
{
break;
}
for (j = 0; j < gSize; j++)
{
jr = j - radius;
tx = x + jr;
if (tx < 0)
{
continue;
}
if (tx < w)
{
heatVals[ty, tx] += kernelMultiplied[i, j];
}
}
}
}
///
/// 基于存储的数据进行计算heatMap
///
///
public Bitmap GetHeatMap()
{
RescaleArray();
Bitmap heatMap = new Bitmap(W, H, PixelFormat.Format32bppArgb);
Rectangle rect = new Rectangle(0, 0, heatMap.Width, heatMap.Height);
BitmapData heatMapData = heatMap.LockBits(rect, ImageLockMode.WriteOnly, heatMap.PixelFormat);
IntPtr ptrw = heatMapData.Scan0;
int wbytes = Math.Abs(heatMapData.Stride) * heatMap.Height;
byte[] argbValuesW = new byte[wbytes];
System.Runtime.InteropServices.Marshal.Copy(ptrw, argbValuesW, 0, wbytes);
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
int colorIndex = double.IsNaN(heatVals[i, j]) ? 0 : (int)heatVals[i, j];
int index = (i * heatMap.Width + j) * 4;
argbValuesW[index] = ColorArgbValues[4 * colorIndex];
argbValuesW[index + 1] = ColorArgbValues[4 * colorIndex + 1];
argbValuesW[index + 2] = ColorArgbValues[4 * colorIndex + 2];
argbValuesW[index + 3] = ColorArgbValues[4 * colorIndex + 3];
}
}
System.Runtime.InteropServices.Marshal.Copy(argbValuesW, 0, ptrw, wbytes);
heatMap.UnlockBits(heatMapData);
return heatMap;
}
///
/// 输出查看一下颜色图谱。
///
///
public Bitmap CheckColorMap()
{
Bitmap checkColor = new Bitmap(w, h, PixelFormat.Format32bppArgb);
int step = (NUMCOLORS - w) / h + 1;
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
if ((i * step) + j >= NUMCOLORS)
{
return checkColor;
}
Color color = Color.FromArgb(ColorArgbValues[i * step * 4],
ColorArgbValues[i * step * 4 + 1],
ColorArgbValues[i * step * 4 + 2],
ColorArgbValues[i * step * 4 + 3]);
checkColor.SetPixel(j, i, color);
}
}
return checkColor;
}
}
类中涉及的数据类型 DataType:
public class DataType
{
public int X { get; set; }
public int Y { get; set; }
public double Weight { get; set; }
}
使用Numpy在C#中的wrapper: NumSharp生成一下随机数据
using System;
using System.Collections.Generic;
using NumSharp;
using HeatMap;
//省略了namespace,如果需要,请理性复制
class MockDatasGen
{
private DataType tPt;
private int W;
private int H;
public MockDatasGen(int w, int h)
{
W = w;
H = h;
tPt = new DataType() { X = -1, Y = -1, Weight = -1};
}
public List CreateMockDatas(int nums)
{
List datas = new List();
for (int i = 0; i < nums; i++)
{
if (tPt.X > 0 && tPt.Y > 0 && tPt.Weight > 0)
{
double c = np.random.rand();
int x = 0, y = 0;
if (c < 0.85)
{
int l = np.random.randint(1, 50);
double d = 2 * np.random.rand() * np.pi;
x = (int)(l * Math.Cos(d));
y = (int)(l * Math.Sin(d));
x = tPt.X + x < 0 ? 0 - x : x;
y = tPt.Y + y < 0 ? 0 - y : y;
x = tPt.X + x >= W ? 0 - x : x;
y = tPt.Y + y >= H ? 0 - y : y;
tPt.X = tPt.X + x;
tPt.Y = tPt.Y + y;
tPt.Weight = np.random.rand() * 10;
DataType data = new DataType() { X = tPt.X, Y = tPt.Y, Weight = tPt.Weight };
datas.Add(data);
}
else
{
tPt.X = np.random.randint(1, W);
tPt.Y = np.random.randint(1, H);
tPt.Weight = np.random.rand() * 10;
DataType data = new DataType() { X = tPt.X, Y = tPt.Y, Weight = tPt.Weight };
datas.Add(data);
}
}
else
{
tPt.X = np.random.randint(1, W);
tPt.Y = np.random.randint(1, H);
tPt.Weight = np.random.rand() * 10;
DataType data = new DataType() { X = tPt.X, Y = tPt.Y, Weight = tPt.Weight };
datas.Add(data);
}
}
return datas;
}
}
const int WIDTH = 800;
const int HEIGHT = 600;
static void Main(string[] args)
{
MockDatasGen datasGen;
Console.WriteLine("Create some mock datas");
datasGen = new MockDatasGen(WIDTH, HEIGHT);
List datas = datasGen.CreateMockDatas(100);
Console.WriteLine("Set datas");
HeatMapImage heatMapImage = new HeatMapImage(WIDTH, HEIGHT, 200, 50);
heatMapImage.SetDatas(datas);
Console.WriteLine("Calculate and generate heatmap");
Bitmap img = heatMapImage.GetHeatMap();
img.Save("..\\..\\..\\..\\Images\\heatmap1.png");
}
最终效果就如文章开始时放的那张图了。