热图(Heat Map)现在已经成为一种算不上一种时髦的应用,基本上涉及到地理信息的应用都会包含热图。热图可以以一种非常直观的形式来呈现密度信息,带来非常棒的用户体验。下面是idvsolutions出售的地图套件的两个热图实例:
http://vfdemo.idvsolutions.com/collisions/
http://vfdemo.idvsolutions.com/piracy/
虽然说,热图本质上不过是三维数据的二维呈现,但显然上面两个显示效果要比下图那样的好的多。
如何生成前面那种热图呢?可以看出,存在一个预设的调色板(如下图),而热图是由许多个椭圆(圆)叠加而成,叠得越多,颜色就朝调色板的一端靠近。
注意观察一些单点,我们发现即使是单个椭圆也是有渐变的,颜色大约是从调色板的中部开始,这样在椭圆叠加后就产生了渐变的边缘。然而,我们并不能用这些颜色直接画椭圆,因为这些颜色叠加后,并不会向调色板上设计好的一端渐变,从而得不到我们想要的效果。
Dylan Vester在他的文章中提出了一种可行的方法。由于灰色在叠加时,颜色会逐渐变浓最后变成黑色,于是可以先绘制灰色椭圆(圆),然后将整个画布看做位图,将图上256级灰度映射到一个256色的调色板上的颜色,以生成热图,效果如下:
不过,仍有一些缺憾,比如生成的热图没有渐变的透明度,因而无法直接覆盖在地图一类的背景上。在默认的实现中,浓度为0的区域用灰色覆盖,即使将此区域的着色在色彩化后手动修改为透明,也存在边缘太硬的问题。同样,原灰度图的绘制也有透明度不可调的问题。
因此,我修改了原灰度图绘制方式,重写了色彩化的方法。做出了以下几点改进:
当前版本最终效果:
换了一个调色板的效果:
(如果您对此节没有兴趣,可以跳到最后去下载源码和可执行程序。)
private ColorBlend getColorBlend ()
{
ColorBlend colors = new ColorBlend (3);
// Set brush stops.
colors.Positions = new float[3] { 0, _brushStop, 1 };
// The intensity value adjusts alpha of gradient colors.
colors.Colors = new Color[3]
{
Color.FromArgb(0, Color.White),
// The following colors can be any color - Only the alpha value is used.
Color.FromArgb(_intensity, Color.Black),
Color.FromArgb(_intensity, Color.Black)
};
return colors;
}
其中,_brushStop和_intensity分别是界面上由用户指定的笔刷变化点和单点中心浓度。这就相当于是WPF/SL中的GradientStops,只不过是分别指定位置和颜色。
首先创建原图大小的空白位图,必须为ARGB格式:
// Create new memory bitmap the same size as the picture box.
// Set its format to 32bit argb to support transparency.
Bitmap bmp = new Bitmap (pictureBox1.Width, pictureBox1.Height,
PixelFormat.Format32bppArgb);
在位图上创建一个Graphics Surface:
// Create new graphics surface from the bitmap.
Graphics surface = Graphics.FromImage (bmp);
对每个指定的热点位置heatPoint,首先画出椭圆路径:
// Create the ellipse path.
var ellipsePath = new GraphicsPath ();
ellipsePath.AddEllipse (heatPoint.X - radius, heatPoint.Y - radius,
radius * 2, radius * 2);
然后构造一个PathGradientBrush来给它着色:
// Create the brush.
PathGradientBrush brush = new PathGradientBrush (ellipsePath);
ColorBlend gradientSpecifications = colors;
brush.InterpolationColors = gradientSpecifications;
// Use the brush to fill the ellipse.
surface.FillEllipse (brush, heatPoint.X - radius,
heatPoint.Y - radius, radius * 2, radius * 2);
如此便得到一幅热点的灰度图,而在此处,地图是作为PictureBox的背景载入的。
直接使用Bitmap类读取文件,然后依次取出每个像素的ARGB值即可:
int[] palette = new int[256];
Bitmap paletteImage = (Bitmap)Bitmap.FromFile (txtPaletteFileName.Text);
for (int i = 0; i < palette.Length - 1; i++)
{
palette[i] = paletteImage.GetPixel (i, 0).ToArgb ();
}
注意最后一个颜色必须设为透明以保证没有热点的区域保持原状(当然有需要的话你也可以调整):
// Set the last color to 0x00000000 to make sure areas
// with no heat point remain original.
palette[palette.Length - 1] = 0;
载入调色板后,首先创建一个相同大小的ARGB输出位图:
// Create an empty bitmap for output.
Bitmap output = new Bitmap (originalMask.Width, originalMask.Height,
PixelFormat.Format32bppArgb);
遍历灰度图的每个像素,使用其Alpha通道值(高8位)取反作为索引,从调色板中取出相应颜色来着色输出位图的对应点:
for (int y = 0; y < originalMask.Height; y++)
{
for (int x = 0; x < originalMask.Width; x++)
{
// Calucate the pixel of output image according to the original pixel and palette.
output.SetPixel (x, y, Color.FromArgb (
palette[(byte)~(((uint)(originalMask.GetPixel (x, y).ToArgb ())) >> 24)]));
}
}
最后将图像输出到PictureBox中刷新即可。
------------
本文介绍了如何在.NET 2.0桌面环境下生成热图,其性能尚可,在特定情况下还可以进行一定优化(比如直接跳过透明点的取色与着色),因此同样也可以用于服务器端预先生成热图,然后以压缩图片格式在网页上显示的场景。
对于Silverlight应用,同样可以采用上述方案。然而,用户往往更希望看到对其操作的实时反馈,而不是慢慢地等待图片的载入,而且服务端预生成图片,也不利于一些自定义视图的呈现(很遗憾,即使是开头IDV的那两个例子,也是采用的这种方式)。其实Silverlight完全有能力自行在客户端生成热图,我将在下一篇blog中介绍。
(本文编写的WinForm程序其实是在项目开发过程中为了配合下文Silverlight产品开发而衍生的工具。)
程序下载(.NET 2.0):HeatMapDemos_WinForm_bin.zip (左键点击原图添加热点,右键点击清除。)
源码下载:HeatMapDemos_WinForm_src.zip
文章转载地址:http://www.cnblogs.com/Gildor/archive/2010/05/13/1734649.html