介绍
热图(Heat Map)现在已经成为一种算不上一种时髦的应用,基本上涉及到地理信息的应用都会包含热图。热图可以以一种非常直观的形式来呈现密度信息,带来非常棒的用户体验。下面是idvsolutions出售的地图套件的两个热图实例:
http://vfdemo.idvsolutions.com/collisions/
http://vfdemo.idvsolutions.com/piracy/
虽然说,热图本质上不过是三维数据的二维呈现,但显然上面两个显示效果要比下图那样的好的多。
生成
如何生成前面那种热图呢?可以看出,存在一个预设的调色板(如下图),而热图是由许多个椭圆(圆)叠加而成,叠得越多,颜色就朝调色板的一端靠近。
注意观察一些单点,我们发现即使是单个椭圆也是有渐变的,颜色大约是从调色板的中部开始,这样在椭圆叠加后就产生了渐变的边缘。然而,我们并不能用这些颜色直接画椭圆,因为这些颜色叠加后,并不会向调色板上设计好的一端渐变,从而得不到我们想要的效果。
Dylan Vester在他的文章中提出了一种可行的方法。由于灰色在叠加时,颜色会逐渐变浓最后变成黑色,于是可以先绘制灰色椭圆(圆),然后将整个画布看做位图,将图上256级灰度映射到一个256色的调色板上的颜色,以生成热图,效果如下:
不过,仍有一些缺憾,比如生成的热图没有渐变的透明度,因而无法直接覆盖在地图一类的背景上。在默认的实现中,浓度为0的区域用灰色覆盖,即使将此区域的着色在色彩化后手动修改为透明,也存在边缘太硬的问题。同样,原灰度图的绘制也有透明度不可调的问题。
因此,我修改了原灰度图绘制方式,重写了色彩化的方法。做出了以下几点改进:
- 改原来的灰度叠加模式为透明度叠加模式,用Alpha通道值来映射调色板;
- 支持32位ARGB调色板,也就是说,生成的热图可以包含渐变的透明度;
- 绘制椭圆时直接绘制Ellipse,而不是绘制一个360边形,可以更好地支持各种尺寸的热点(不知为何原作者说.NET 2.0不支持渐变的radial gradient就采用了变通方式,其实可以用PathGradientBrush来实现);
- 直接支持从图片加载调色板,并可调节热点绘制的参数,可手动指定热点(其实就是做成了一个调色板效果预览工具,用于设计Heat Map调色板)
当前版本最终效果:
换了一个调色板的效果:
实现细节
(如果您对此节没有兴趣,可以跳到最后去下载源码和可执行程序。)
灰度图的笔刷混色
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;
色彩化 (Colorize)
载入调色板后,首先创建一个相同大小的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产品开发而衍生的工具。)
http://www.cnblogs.com/Gildor/archive/2010/05/13/1734649.html