Android - 热力图

前言:简书菜鸟一枚,主要用于记录。如有侵权,望告知,我会下架文章。

话不多,先上图:


heatMap_both.png

最近公司在做一款能彩集压力数据的坐垫。怎么样能将这个功能高大上的体现出牛逼样儿。这时同事们就想到了热力图了。如图示,不明觉厉,确实高大上。

一、找资源

实际工作的开发过程中,因为时间限定研发成本等原因,每遇到新玩意,首先都是找“轮子”。我运气不错(天选打工人一枚)一个早上就找到2个,并且在接入时发现第一个就很不错。大佬轮子链接https://github.com/ChristianFF/HeatMapForAndroid。效果也贴一个:

image.png

二、接入

由于直接效果略有出入,感觉要对应自己细节调整所以我是直接下载源码接入,最终关键代码没变。配置略有调整,关键类Gradient、HeatMap、WeightedPoint;关键代码如下:

// 转换数据
private List<WeightedPoint> generateHeatMapData(Object[] obs) {
        List<WeightedPoint> data = new ArrayList<>();
        for (int i = 0; i < obs.length; i++) {
            data.add(new WeightedPoint(obs[i].getOx() + mHeatMapPointRadius,
                    obs[i].getOy() + mHeatMapPointRadius, obs[i].getPressure()));
        }
        return data;
}

// 生成热力图
List<WeightedPoint> data = generateHeatMapData(mMotorCircles);
HeatMap heatMap = new HeatMap.Builder().weightedData(data).radius(mHeatMapPointRadius)
                .width(view.getWidth() + mHeatMapPointRadius).height(view.getHeight() + mHeatMapPointRadius).build();
Bitmap heatMap = heatMap.generateMap();

/**
* 颜色配置(色阶渐变)
* 色彩区间,参数1色阶。参数2色阶占比(按0~1分段),
*/
private static final Gradient DEFAULT_MY_GRADIENT = new Gradient(
            new int[]{0xFF7C89DF, Color.GREEN, Color.YELLOW, Color.RED},
            new float[]{0.2f, 0.4f, 0.7f, 1f});

三、源码分析

我感觉这个东西很有意思,加上预防需求变化有调整。就具体看了看源码,以下是我的个人收获。记录一番。

1、设计思路:与二维码相似,通过描绘像素点形成位图Bitmap;不同的是这是给图片的不同像素点上色不同颜色值,以一个坐标点为圆心,圆形向外渐变颜色,如:
image.png

散点、相邻点:
image.png

色彩与像素点的映射关系:(参考一个点的图对比)

image.png

2、代码实现

设计:

image.png

关键计算代码:

1.生成位图方法(注释是我个人的理解,仅供参考)

    public Bitmap generateMap() {
//        double[][] intensity = new double[mWidth][mHeight];
        // 强度二维数组(坐标强度)
        double[][] intensity = new double[mWidth + mRadius * 2][mHeight + mRadius * 2];
        for (WeightedPoint w : mData) {
            // 数据集点,热力中心点,然后周边扩散
            int bucketX = w.x;
            int bucketY = w.y;
            if (bucketX < mWidth && bucketX >= 0 && bucketY < mHeight && bucketY >= 0) {
                intensity[bucketX][bucketY] += w.intensity;
            }
        }
        // convolved 卷积;intensity 强度;Kernel 内核
        double[][] convolved = convolve(intensity, mKernel);
        return colorize(convolved, mColorMap, mMaxIntensity);
    }

2.点与点之间交叉重合的处理(强度相容)-重点

/**
     * 使缠绕,将坐标之间的强度相接
     *
     * @param grid   像素点集(x,y)
     * @param kernel 坐标(渐变系数,圆心~圆边:(1,0],0是透明)
     * @return 具体图片的像素点集(带强度)
     */
    protected double[][] convolve(double[][] grid, double[] kernel) {
        Logc.i("radius = " + mRadius);
        int dimOldW = grid.length;
        int dimOldH = grid[0].length;
        int dimW = dimOldW - 2 * mRadius;
        int dimH = dimOldH - 2 * mRadius;
        int lowerLimit = mRadius;
        int upperLimitW = mRadius + dimW - 1; // 向外伸展,目的:是支持强度(红点)在边界上。
        int upperLimitH = mRadius + dimH - 1;
        // 中间的; 中级的; (两地、两物、两种状态等)之间的
        double[][] intermediate = new double[dimOldW][dimOldH];
        int x, y, x2, xUpperLimit, initial;
        double val;
        for (x = 0; x < dimOldW; x++) {
            for (y = 0; y < dimOldH; y++) {
                val = grid[x][y];
                if (val != 0) {
                    // 宽上限:x轴 + 半径
                    xUpperLimit = ((upperLimitW < x + mRadius) ? upperLimitW : x + mRadius);
                    // 初始值
                    initial = (lowerLimit > x - mRadius) ? lowerLimit : x - mRadius;
                    // 遍历x,赋予强度值.(一维数组,同y轴上的强度)
                    for (x2 = initial; x2 < xUpperLimit; x2++) {
                        double v = kernel[x2 - (x - mRadius)];
                        intermediate[x2][y] += val * v;
//                        Logc.d("有坐标的,x2 = " + x2 + ", y = " + y + ", val = " + val + " , v = " + v + ", (x - mRadius) = " + (x - mRadius) + ", old = " + old + ", new =" + (val * v));
                    }
                }
            }
        }
        // 输出的网格
        double[][] outputGrid = new double[dimW][dimH];
        int y2, yUpperLimit;
        // 坐标范围的像素点强度  整图范围的x轴(radius ~ radiusX + mWidth - 1)
        for (x = lowerLimit; x < upperLimitW + 1; x++) {
            for (y = 0; y < dimOldH; y++) {
                val = intermediate[x][y];
                // val != 0 :有强度的,设置坐标了的
                if (val != 0) {
                    yUpperLimit = ((upperLimitH < y + mRadius) ? upperLimitH : y + mRadius);
                    // 初始y值
                    initial = (lowerLimit > y - mRadius) ? lowerLimit : y - mRadius;
                    // 遍历y,赋予强度值.
                    for (y2 = initial; y2 < yUpperLimit; y2++) {
                        //
                        double v = kernel[y2 - (y - mRadius)];
                        // 不同点之间有交集所以用 +=;
                        outputGrid[x - mRadius][y2 - mRadius] += val * v;
                    }
                }
            }
        }
        return outputGrid;
    }

3.位图上色,如zxing二维码一样,给图片的每个像素点按强度上色

 /**
     * 像素点上色,即画位图
     *
     * @param grid     像素点集(x,y)
     * @param colorMap 设置的颜色变化色彩集(比如4中颜色淡、弱、中、强且按排序的色彩集)
     * @param max      最大强度
     * @return
     */
    private Bitmap colorize(double[][] grid, int[] colorMap, double max) {
        int maxColor = colorMap[colorMap.length - 1];
        double colorMapScaling = (colorMap.length - 1) / max;

        int dimW = mWidth;
        int dimH = mHeight;

        int i, j, index, col;
        double val;
        int[] colors = new int[dimW * dimH];
        for (i = 0; i < dimH; i++) {
            for (j = 0; j < dimW; j++) {
                val = grid[j][i];// 强度,最大值:max
                index = i * dimW + j;
                // val * colorMapScaling,结合之前的 colorMapScaling = (colorMap.length - 1) / max;
                // 算出某强度在颜色集的下标
                col = (int) (val * colorMapScaling);
                if (val != 0) {
                    if (col < colorMap.length) {
                        colors[index] = colorMap[col];
                    } else {
                        colors[index] = maxColor;
                    }
                } else {
                    colors[index] = Color.TRANSPARENT;
                }
            }
        }
        Bitmap tile = Bitmap.createBitmap(dimW, dimH, Bitmap.Config.ARGB_8888);
        tile.setPixels(colors, 0, dimW, 0, 0, dimW, dimH);
        return tile;
    }

4.色彩映射按强度形成数据便于与强度关联然后位图上色(注释是我个人的理解,仅供参考)

/**
     * 生成颜色集(按淡、弱、中、强(colors)排序的)
     * @param opacity 不透明度
     * @return
     */
    public int[] generateColorMap(double opacity) {
        HashMap<Integer, ColorInterval> colorIntervals = generateColorIntervals();
        int[] colorMap = new int[colorMapSize]; // (各色阶依据色阶段比,在色彩集中也对应分段)
        ColorInterval interval = colorIntervals.get(0);
        // 初始颜色
        int start = 0;
        for (int i = 0; i < colorMapSize; i++) {
            // 从色阶分段中获取分段信息(红-黄、黄-绿、绿-蓝、蓝-透明)
            if (colorIntervals.containsKey(i)) {
                // 区间范围(色阶分段范围)
                interval = colorIntervals.get(i);
                // 色阶初始值分段中的初始值,类似(红-黄[100~80]、黄-绿[80~50]、绿-蓝(50~20)、蓝-透明(20~0))
                start = i; // 100\80\50\20
            }
            // ratio : 比率;colorEnd时 = 1。;如i = 90,色阶长度是20,初始值是80,- 》 比率 = 0.5
            float ratio = (i - start) / interval.interval; // interval.interval // 色阶长度(如红黄[100~80] 是20)
            colorMap[i] = interpolateColor(interval.colorStart, interval.colorEnd, ratio);
        }
        // 转成带透明度的颜色值
        if (opacity != 1) {
            for (int i = 0; i < colorMapSize; i++) {
                int c = colorMap[i];
                colorMap[i] = Color.argb((int) (Color.alpha(c) * opacity),
                        Color.red(c), Color.green(c), Color.blue(c));
            }
        }

        return colorMap;
    }

    /**
     * 生成色阶集合
     * @return
     */
    private HashMap<Integer, ColorInterval> generateColorIntervals() {
        // 颜色区间数数组
        HashMap<Integer, ColorInterval> colorIntervals = new HashMap<>();
        // Create first color if not already created
        // The initial color is transparent by default : 初始颜色默认为透明
        // 其实默认色阶比colors多一个透明
        if (startPoints[0] != 0) {
            int initialColor = Color.argb(
                    0, Color.red(colors[0]), Color.green(colors[0]), Color.blue(colors[0]));
            colorIntervals.put(0, new ColorInterval(initialColor, colors[0], colorMapSize * startPoints[0]));
        }
        // Generate color intervals
        // 生成色阶集
        for (int i = 1; i < colors.length; i++) {
            colorIntervals.put(((int) (colorMapSize * startPoints[i - 1])),
                    new ColorInterval(colors[i - 1], colors[i],
                            (colorMapSize * (startPoints[i] - startPoints[i - 1]))));
        }
        // If color for 100% intensity is not given, the color of highest intensity is used. 如果没有给出100%强度的颜色,则使用最高强度的颜色。
        // 即最强强度去掉透明度
        if (startPoints[startPoints.length - 1] != 1) {
            int i = startPoints.length - 1;
            colorIntervals.put(((int) (colorMapSize * startPoints[i])),
                    new ColorInterval(colors[i], colors[i], colorMapSize * (1 - startPoints[i])));
        }
        return colorIntervals;
    }

    /**
     * 插入/篡改 颜色
     * @param colorStart 初始颜色
     * @param colorEnd 结束颜色
     * @param ratio 比率
     * @return
     */
    private int interpolateColor(int colorStart, int colorEnd, float ratio) {
        int alpha = (int) ((Color.alpha(colorEnd) - Color.alpha(colorStart)) * ratio + Color.alpha(colorStart));
        float[] hsvStart = new float[3];
        // https://blog.csdn.net/qq_42271561/article/details/115465061
        // HSV是一种比较直观的颜色模型,所以在许多图像编辑工具中应用比较广泛,这个模型中颜色的参数分别是:色调(H,Hue:0°~360°),饱和度(S,Saturation:0%~100%),明度(V, Value:0%(黑)到100%(白))。
        Color.RGBToHSV(Color.red(colorStart), Color.green(colorStart), Color.blue(colorStart), hsvStart);
        float[] hsvEnd = new float[3];
        Color.RGBToHSV(Color.red(colorEnd), Color.green(colorEnd), Color.blue(colorEnd), hsvEnd);
        // 就近渐变,避免渐变经过大于180的色彩范围
        if (hsvStart[0] - hsvEnd[0] > 180) {
            hsvEnd[0] += 360;
        } else if (hsvEnd[0] - hsvStart[0] > 180) {
            hsvStart[0] += 360;
        }

        float[] hsvResult = new float[3];
        // 转换HSV颜色数据后按比率变换
        for (int i = 0; i < 3; i++) {
            // (结束色彩 - 初始色彩)* 比率
            hsvResult[i] = (hsvEnd[i] - hsvStart[i]) * (ratio) + hsvStart[i];
        }

        return Color.HSVToColor(alpha, hsvResult);
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容