热力图生成算法及其具体实现

简介: 热力图生成算法及其具体实现

热力图生成算法及其具体实现

目录

1. 概述

以前一直觉得热力图非常高大上,现在终于有机会研究并总结这个问题了。其实从图像处理的角度上来说,热力图生成算法并没有什么特别的,要得到非常漂亮的效果,数据以及配色方案的也很重要。这里就用OpenCV简单实现一下,用什么工具不重要,重要的是其中的原理。

2. 详论

2.1. 数据准备

我们没有数据,但是可以通过随机数算法,生成一个热力点的集合:

struct HPoint {
  int x;
  int y;
  int value;
};
int width = 512;   //热力图宽
int height = 512;  //热力图高
int reach = 25;    //影响范围
int valueRange = 100;
vector<HPoint> heatPoints;  //热力点
vector<HRect> heatRects;    //热力范围
void GetHeatPoint() {
  int num = 100;
  heatPoints.resize(num);
  heatRects.resize(num);
  for (int i = 0; i < num; i++) {
    heatPoints[i].x = rand() % width;
    heatPoints[i].y = rand() % height;
    heatPoints[i].value = rand() % valueRange;
    heatRects[i].left = (std::max)(heatPoints[i].x - reach, 0);
    heatRects[i].top = (std::max)(heatPoints[i].y - reach, 0);
    heatRects[i].right = (std::min)(heatPoints[i].x + reach, width - 1);
    heatRects[i].bottom = (std::min)(heatPoints[i].y + reach, height - 1);
  }
}

这段代码的意思是,我们根据给定的热力图宽高的范围,生成热力图范围内一定权值范围的热力点;并且,根据热力点影响范围求出其外包矩形。这里的随机数并没有给时间种子,所以每次运行的结果都是固定的。

2.2. 准备绘制

我们绘制的目的是一个包含透明度的彩色图片,所以需要创建4波段的图片。通过直接操作图片的内存buffer,首先我们将背景设置成黑色;然后遍历热力点,将热力点的范围涂成白色:

Mat img(height, width, CV_8UC4);
int nBand = 4;
uchar *data = img.data;
size_t dataLength = (size_t)width * height * nBand;
memset(data, 0, dataLength);
for (size_t i = 0; i < heatPoints.size(); i++) {
  //遍历热力点范围
  for (int hi = heatRects[i].top; hi <= heatRects[i].bottom; hi++) {
    for (int wi = heatRects[i].left; wi <= heatRects[i].right; wi++) {
      size_t m = (size_t)width * nBand * hi + wi * nBand;
      data[m + 0] = data[m + 1] = data[m + 2] = data[m + 3] = 255;
    }
  }
}
imshow("热力图", img);
waitKey();

2.3. 绘制热力范围

上面绘制的是热力点的外接矩形范围,现在我们绘制热力图真正影响范围。原理其实很简单,就是判断点是否在圆内:

for (size_t i = 0; i < heatPoints.size(); i++) {
    //遍历热力点范围
    for (int hi = heatRects[i].top; hi <= heatRects[i].bottom; hi++) {
      for (int wi = heatRects[i].left; wi <= heatRects[i].right; wi++) {
        //判断是否在热力圈范围
        float length =
            sqrt((float)(wi - heatPoints[i].x) * (wi - heatPoints[i].x) +
                 (hi - heatPoints[i].y) * (hi - heatPoints[i].y));
        if (length <= reach) {
          size_t m = (size_t)width * nBand * hi + wi * nBand;
          data[m + 0] = data[m + 1] = data[m + 2] = data[m + 3] = 255;
        }
      }
    }
  }

2.4. 绘制热力图

接下来就让热力范围根据与热力点的距离渐变:距离越近,就越白,距离越远,就越黑:

for (size_t i = 0; i < heatPoints.size(); i++) {
    //遍历热力点范围
    for (int hi = heatRects[i].top; hi <= heatRects[i].bottom; hi++) {
      for (int wi = heatRects[i].left; wi <= heatRects[i].right; wi++) {
        //判断是否在热力圈范围
        float length =
            sqrt((float)(wi - heatPoints[i].x) * (wi - heatPoints[i].x) +
                 (hi - heatPoints[i].y) * (hi - heatPoints[i].y));
        if (length <= reach) {
          float alpha = ((reach - length) / reach);
          size_t m = (size_t)width * nBand * hi + wi * nBand;
          data[m + 0] = data[m + 1] = data[m + 2] = data[m + 3] = uchar(255 * alpha);
        }
      }
    }
  }

立体感到是不错,但是问题在于我们需要将热力点的影响叠加起来,也就是每次遍历热力点之后,像素值也要叠加起来:

for (size_t i = 0; i < heatPoints.size(); i++) {
    //遍历热力点范围
    for (int hi = heatRects[i].top; hi <= heatRects[i].bottom; hi++) {
      for (int wi = heatRects[i].left; wi <= heatRects[i].right; wi++) {
        //判断是否在热力圈范围
        float length =
            sqrt((float)(wi - heatPoints[i].x) * (wi - heatPoints[i].x) +
                 (hi - heatPoints[i].y) * (hi - heatPoints[i].y));
        if (length <= reach) {
          float alpha = ((reach - length) / reach);
          size_t m = (size_t)width * nBand * hi + wi * nBand;
          float newAlpha = data[m + 3] / 255.0f + alpha;
          newAlpha = std::min(std::max(newAlpha * 255, 0.0f), 255.0f);
          data[m + 0] = data[m + 1] = data[m + 2] = data[m + 3] =
              uchar(newAlpha);
        }
      }
    }
  }

看起来略具意思了,但是有个问题是没有体现每个点的权值的影响,因此我们加上权值的影响,让热力的效果更真实一点:

for (size_t i = 0; i < heatPoints.size(); i++) {
    //权值因子
    float ratio = (float)heatPoints[i].value / valueRange;
    //遍历热力点范围
    for (int hi = heatRects[i].top; hi <= heatRects[i].bottom; hi++) {
      for (int wi = heatRects[i].left; wi <= heatRects[i].right; wi++) {
        //判断是否在热力圈范围
        float length =
            sqrt((float)(wi - heatPoints[i].x) * (wi - heatPoints[i].x) +
                 (hi - heatPoints[i].y) * (hi - heatPoints[i].y));
        if (length <= reach) {
          float alpha = ((reach - length) / reach) * ratio;
          size_t m = (size_t)width * nBand * hi + wi * nBand;
          float newAlpha = data[m + 3] / 255.0f + alpha;
          newAlpha = std::min(std::max(newAlpha * 255, 0.0f), 255.0f);
          data[m + 0] = data[m + 1] = data[m + 2] = data[m + 3] =
              uchar(newAlpha);
        }
      }
    }
  }

2.5. 配色方案

最后就是给这个黑白热力图上色了。配色是非常重要的,需要一点美术功底才行,我们直接采用参考2中的颜色值进行配色。首先创建一个颜色映射表,将之前的黑白色映射到一个BGR渐变色集合:

array<array<uchar, 3>, 256> bGRTable;  //颜色映射表
//生成渐变色
void Gradient(array<uchar, 3> &start, array<uchar, 3> &end,
              vector<array<uchar, 3>> &RGBList) {
  array<float, 3> dBgr;
  for (int i = 0; i < 3; i++) {
    dBgr[i] = (float)(end[i] - start[i]) / (RGBList.size() - 1);
  }
  for (size_t i = 0; i < RGBList.size(); i++) {
    for (int j = 0; j < 3; j++) {
      RGBList[i][j] = (uchar)(start[j] + dBgr[j] * i);
    }
  }
}
void InitAlpha2BGRTable() {
  array<double, 7> boundaryValue = {0.2, 0.3, 0.4, 0.6, 0.8, 0.9, 1.0};
  array<array<uchar, 3>, 7> boundaryBGR;
  boundaryBGR[0] = {255, 0, 0};
  boundaryBGR[1] = {231, 111, 43};
  boundaryBGR[2] = {241, 192, 2};
  boundaryBGR[3] = {148, 222, 44};
  boundaryBGR[4] = {83, 237, 254};
  boundaryBGR[5] = {50, 118, 253};
  boundaryBGR[6] = {28, 64, 255};
  double lastValue = 0;
  array<uchar, 3> lastRGB = {0, 0, 0};
  vector<array<uchar, 3>> RGBList;
  int sumNum = 0;
  for (size_t i = 0; i < boundaryValue.size(); i++) {
    int num = 0;
    if (i == boundaryValue.size() - 1) {
      num = 256 - sumNum;
    } else {
      num = (int)((boundaryValue[i] - lastValue) * 256 + 0.5);
    }
    RGBList.resize(num);
    Gradient(lastRGB, boundaryBGR[i], RGBList);
    for (int i = 0; i < num; i++) {
      bGRTable[i + sumNum] = RGBList[i];
    }
    sumNum = sumNum + num;
    lastValue = boundaryValue[i];
    lastRGB = boundaryBGR[i];
  }
}

通过这个颜色映射表,在填充像素的时候,将计算的Alpha映射成一个BGR值,填充到前三个波段中:

for (size_t i = 0; i < heatPoints.size(); i++) {
    //权值因子
    float ratio = (float)heatPoints[i].value / valueRange;
    //遍历热力点范围
    for (int hi = heatRects[i].top; hi <= heatRects[i].bottom; hi++) {
      for (int wi = heatRects[i].left; wi <= heatRects[i].right; wi++) {
        //判断是否在热力圈范围
        float length =
            sqrt((float)(wi - heatPoints[i].x) * (wi - heatPoints[i].x) +
                 (hi - heatPoints[i].y) * (hi - heatPoints[i].y));
        if (length <= reach) {
          float alpha = ((reach - length) / reach) * ratio;
          //计算Alpha
          size_t m = (size_t)width * nBand * hi + wi * nBand;
          float newAlpha = data[m + 3] / 255.0f + alpha;
          newAlpha = std::min(std::max(newAlpha * 255, 0.0f), 255.0f);
          data[m + 3] = (uchar)(newAlpha);
          //颜色映射
          for (int bi = 0; bi < 3; bi++) {
            data[m + bi] = bGRTable[data[m + 3]][bi];
          }
        }
      }
    }
  }

最终的成果如下:

3. 问题

  1. OpenCV显示的背景是黑色的,这是因为其默认是按照RGB三波段来显示的,其实最后的结果是个包含透明通道的图像,可以将其叠加到任何图层上:
  2. 热力点可以有权值,也可以没有。没有权值可以认为所有点的权值是一样的,可以适当调整热力影响的范围让不同的热力点连接,否则就是一个个独立的圈。
  3. 如果出现红色的区域(热力值高)过多,那么原因可能是热力点太密了。同一个区域内收到的热力影响太多,计算的alpha超过1,映射到图像像素值导致被截断,无法区分热力值高的区域。那么一个合理的改进方案就是将计算的alpha缓存住,在计算所有的alpha的最大最小,将alpha再度映射到0到1之间,进而映射到像素值的0~255之间——就不会高位截断的问题了。如果有机会,再实现一下这个问题的改进。

4. 参考

  1. 你不知道的前端算法之热力图的实现
  2. 数据可视化:浅谈热力图如何在前端实现

具体源代码实现

分类: 图像处理 , OpenCV

标签: OpenCV , 算法 , 图像处理 , 热力图


相关文章
|
机器学习/深度学习
这图怎么画| 相关性热图+柱状图
这图怎么画| 相关性热图+柱状图
131 0
|
2月前
|
数据可视化 数据挖掘 Python
绘制带误差分析的柱状图
【9月更文挑战第1天】在数据分析与科研中,带误差分析的柱状图能直观展示数据分布与不确定性。本文介绍使用Python的Matplotlib库和Excel绘制此类图表的方法,包括安装库、准备数据、绘制图表及添加误差线等步骤,帮助用户根据需求进行调整与定制。
|
5月前
|
数据挖掘
散点图中实现数据的线性回归分析
散点图中实现数据的线性回归分析
42 2
|
5月前
|
自然语言处理 数据可视化 Python
卡方分布和 Zipf 分布模拟及 Seaborn 可视化教程
卡方分布是统计学中的一种连续概率分布,用于假设检验,形状由自由度(df)决定。自由度越大,分布越平缓。NumPy的`random.chisquare()`可生成卡方分布随机数。Seaborn能可视化卡方分布。练习包括模拟不同自由度的卡方分布、进行卡方检验。瑞利分布描述信号处理中幅度分布,参数为尺度(scale)。Zipf分布常用于自然语言等幂律特征数据,参数a控制形状。NumPy的`random.zipf()`生成Zipf分布随机数。
83 0
|
6月前
|
数据可视化 定位技术 网络架构
R语言在地图上绘制月亮图、饼状图数据可视化果蝇基因种群
R语言在地图上绘制月亮图、饼状图数据可视化果蝇基因种群
|
6月前
|
数据可视化 算法 数据挖掘
R语言用温度对城市层次聚类、kmean聚类、主成分分析和Voronoi图可视化
R语言用温度对城市层次聚类、kmean聚类、主成分分析和Voronoi图可视化
|
6月前
R语言中绘制箱形图的替代品:蜂群图和小提琴图
R语言中绘制箱形图的替代品:蜂群图和小提琴图
|
数据挖掘 数据处理
这图怎么画| 还是热图(免疫治疗反应预测)
这图怎么画| 还是热图(免疫治疗反应预测)
80 0
|
数据挖掘
这图怎么画| 批量小提琴图+箱线图+散点+差异分析
这图怎么画| 批量小提琴图+箱线图+散点+差异分析
312 0
|
数据可视化 数据挖掘 Linux
数据可视化丨优雅的绘制带显著性标记的箱线散点图,主要使用ggsignif和ggplot2
数据可视化丨优雅的绘制带显著性标记的箱线散点图,主要使用ggsignif和ggplot2