基于OpenGL ES实现的Android人体热力图可视化库

基于OpenGL ES实现的Android人体热力图可视化库

demo: github.com/GggggitHub/...

前言

本文将详细介绍我们基于OpenGL ES开发的Android人体热力图可视化库,该库已开源并发布到 github,可供开发者在各类健康监测、医疗诊断和运动分析应用中使用。

技术栈

  • Python 3.0
  • OpenCV
  • Android SDK
  • OpenGL ES 2.0
  • Gradle 8.0+
  • Java 8+

主要的核心流程

  1. 抠图获取人体轮廓坐标
  2. 在人体轮廓切割成各个部分
  3. 绘制人体轮廓
  4. 设置每部分的颜色变化
  5. 调整人体轮廓的位置,自适应
  6. 重叠位置下面放置骨骼的原始图片

抠图获取人体轮廓坐标

最终实现的效果是:

错误示范:

1. 轮廓提取技术

轮廓提取是整个项目的基础,我们采用了OpenCV库中的轮廓检测算法,结合图像二值化和边缘检测技术,实现了精确的人体轮廓提取。

python 复制代码
def extract_body_contour(img_path, output_dir=None):
    # 1. 读取图像
    rgb_image = cv2.imread(img_path)
    rgb_image = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2RGB)
    
    # 2. 图像预处理
    gray = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    
    # 3. 边缘检测和二值化
    edges = cv2.Canny(blurred, 50, 150)
    _, binary = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY_INV)
    
    # 4. 轮廓检测
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # 5. 选择最大轮廓并优化点分布
    # ... 代码实现 ...
    
    return contour_points

技术难点与解决方案

  • 背景干扰: UI 提供一张纯色的图便于 opencv 识别,否则需要多次调整,opencv 切割方法。

2. 身体部位精确分割

最终效果是: 更多剩余图片详情见: github.com/GggggitHub/...

错误示范:

本项目最大的亮点在于实现了高精度的人体部位分割。我们没有采用传统的基于比例的简单划分方法,而是基于人体解剖学特征和关键点位置,精确定义了各个部位的边界。

python 复制代码
def split_body_parts(contour_points):
    # 定义各部位的索引范围
    body_parts = {
        "头部": [*range(0, 9), *range(111, 120)],
        "颈部": [*range(8, 11), *range(109, 112)],
        "左肩膀": [*range(11, 17), 38],
        "左臂": [*range(16, 21), *range(33, 39)],
        "左手": [*range(20, 34)],
        "上身": [10, 11, *range(38, 45), 61, 62, 63, *range(79, 84), 108, 109],
        "左腿": [*range(44, 50), *range(54, 62)],
        "左脚": [*range(49, 55)],
        "右腿": [*range(63, 71), *range(74, 80)],
        "右脚": [*range(70, 75)],
        "右肩膀": [*range(103, 109), 83],
        "右臂": [*range(83, 89), *range(99, 104)],
        "右手": [*range(88, 100)]
    }
    
    # 创建各部位的点集
    parts_points = {}
    for part_name, indices in body_parts.items():
        parts_points[part_name] = [contour_points[i] for i in indices]
    
    return parts_points

技术难点与解决方案

  • 背景干扰: 重点是别瞎折腾,open CV 识别每个部分不太准,手画下来自己切割。手画下来自己切割。手画下来自己切割。

下面就是绘制的核心

技术选型:

  • 原生 Canvas
  • ✅OpenGL渲染
  • U3d 游戏引擎
  1. 人体轮廓渲染
  2. 温度数据热力图映射
  3. 透明度调节
  4. 图像居中处理

架构设计

该库采用模块化设计,主要包含以下几个核心组件:

arduino 复制代码
com.aj.bodyheartmaplib 
├── MainActivity.java        // 使用类
├── HeatMapView.java         // 热力图视图
├── HeatMapRenderer.java     // 热力图渲染器
├── BodyModel.java           // 人体模型
└── OpenGlxyz.java           // 3D坐标系

技术实现详解

1. 人体轮廓数据处理

人体轮廓数据存储在JSON文件中,包含一系列坐标点:

json 复制代码
[[490, 187], [449, 202], [421, 229], ..., [551, 199]]

这些点按顺序连接形成人体轮廓。我们通过BodyModel类加载和处理这些数据:

java 复制代码
public class BodyModel {
    private final Context context;
    private float[] vertices;
    private float[] boundaries; // 存储模型边界:[minX, maxX, minY, maxY]
    
    public BodyModel(Context context) {
        this.context = context;
        loadBodyContour();
    }
    
    private void loadBodyContour() {
        try {
            // 从assets目录加载JSON文件
            InputStream is = context.getAssets().open("body_red_2_contour_copy.json");
            // 解析JSON数据并转换为顶点坐标
            // ...
        } catch (IOException e) {
            Log.e(TAG, "加载人体轮廓失败", e);
        }
    }
}

所有数据放到 buffer 里面

// 创建顶点缓冲区
ByteBuffer bb = ByteBuffer.allocateDirect(vertices.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

// 创建纹理坐标缓冲区
ByteBuffer tb = ByteBuffer.allocateDirect(texCoords.length * 4);
tb.order(ByteOrder.nativeOrder());
texCoordBuffer = tb.asFloatBuffer();
texCoordBuffer.put(texCoords);
texCoordBuffer.position(0);

2. 温度数据处理与归一化

java 复制代码
public void updateTemperatureData(float[] temperatures) {
    if (temperatures == null || temperatures.length == 0) {
        return;
    }
    
    // 找出温度范围
    float minTemp = Float.MAX_VALUE;
    float maxTemp = Float.MIN_VALUE;
    for (float temp : temperatures) {
        if (temp < minTemp) minTemp = temp;
        if (temp > maxTemp) maxTemp = temp;
    }
    
    // 温度范围过小时进行调整,确保有足够的颜色区分度
    float range = maxTemp - minTemp;
    if (range < 0.5f) {
        float mid = (maxTemp + minTemp) / 2;
        minTemp = mid - 0.25f;
        maxTemp = mid + 0.25f;
    }
    
    // 归一化温度数据到0-1范围
    float[] normalizedTemps = new float[temperatures.length];
    for (int i = 0; i < temperatures.length; i++) {
        normalizedTemps[i] = (temperatures[i] - minTemp) / (maxTemp - minTemp);
        // 限制在0-1范围内
        normalizedTemps[i] = Math.max(0, Math.min(1, normalizedTemps[i]));
    }
    
    // 更新渲染器中的温度数据
    if (renderer != null) {
        renderer.setTemperatureData(normalizedTemps, minTemp, maxTemp);
        requestRender();
    }
}

在着色器中应用温度数据

java 复制代码
// 顶点着色器中传递温度数据
private static final String VERTEX_SHADER =
        "uniform mat4 uMVPMatrix;" +
        "attribute vec4 aPosition;" +
        "attribute float aTemperature;" +  // 温度属性
        "varying float vTemperature;" +    // 传递给片段着色器
        "void main() {" +
        "  gl_Position = uMVPMatrix * aPosition;" +
        "  vTemperature = aTemperature;" +
        "}";

// 片段着色器中使用温度数据查找颜色
private static final String FRAGMENT_SHADER =
        "precision mediump float;" +
        "varying float vTemperature;" +     // 从顶点着色器接收
        "uniform sampler2D uColorMap;" +    // 颜色映射纹理
        "uniform float uAlpha;" +
        "void main() {" +
        "  vec4 color = texture2D(uColorMap, vec2(vTemperature, 0.5));" +
        "  gl_FragColor = vec4(color.rgb, color.a * uAlpha);" +
        "}";

3. 透明度控制机制。重点:(全局透明度+局部透明度)

透明度控制是热力图可视化的重要功能,它允许用户调整热力图的叠加效果,以便更好地观察底层图像。

实现原理

在OpenGL ES中,透明度控制主要通过片段着色器(Fragment Shader)中的alpha通道实现:

java 复制代码
// 片段着色器中的透明度处理
private static final String FRAGMENT_SHADER =
        "precision mediump float;" +
        "varying vec4 vColor;" +
        "uniform float uAlpha;" +  // 全局透明度参数
        "void main() {" +
        "  gl_FragColor = vec4(vColor.rgb, vColor.a * uAlpha);" +  // 应用透明度
        "}";

透明度更新机制

java 复制代码
// HeatMapView类中的透明度更新方法
public void updateGlAlpha(float alpha) {
    if (alpha < 0.0f) alpha = 0.0f;
    if (alpha > 1.0f) alpha = 1.0f;
    
    if (renderer != null) {
        renderer.setAlpha(alpha);
        requestRender();  // 请求重新渲染
    }
}

// 渲染器中的透明度设置
public void setAlpha(float alpha) {
    this.alpha = alpha;
}

// 在onDrawFrame方法中应用透明度
@Override
public void onDrawFrame(GL10 gl) {
    // ...其他渲染代码...
    
    // 设置透明度uniform变量
    GLES20.glUniform1f(alphaHandle, alpha);
    
    // ...继续渲染...
}

透明度渲染优化

为了正确渲染透明效果,我们需要进行以下设置:

java 复制代码
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 设置背景色为完全透明
    GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    
}

// 在GLSurfaceView初始化时设置透明支持
setEGLConfigChooser(8, 8, 8, 8, 16, 0);  // RGBA各8位,深度16位
getHolder().setFormat(PixelFormat.TRANSLUCENT);  // 设置透明背景
setZOrderOnTop(true);  // 确保视图在顶层

// 在渲染器中启用混合
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // ...其他初始化代码...
    
    // 启用混合
    GLES20.glEnable(GLES20.GL_BLEND);
    GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
}

4. 视图填充与居中技术

为了使人体模型正确地填充整个视图并居中显示,我们实现了一套自适应的投影矩阵计算方法。

  1. 切割扫描出来的坐标 找到 边界点。
  2. 去除边界点,把图像移到最左边。
  3. 把图像填充满 Y 周,修改坐标
  4. 把图像移动到中央 --主要是移动视窗,与图像大小的关系。

模型边界计算

java 复制代码
private void calculateModelBoundaries() {
    if (vertices == null || vertices.length < 6) {
        return;
    }
    
    // 初始化边界值
    float minX = Float.MAX_VALUE;
    float maxX = Float.MIN_VALUE;
    float minY = Float.MAX_VALUE;
    float maxY = Float.MIN_VALUE;
    
    // 遍历所有顶点找出边界
    for (int i = 0; i < vertices.length; i += 3) {
        float x = vertices[i];
        float y = vertices[i + 1];
        
        if (x < minX) minX = x;
        if (x > maxX) maxX = x;
        if (y < minY) minY = y;
        if (y > maxY) maxY = y;
    }
    
    // 存储模型边界
    boundaries = new float[] {minX, maxX, minY, maxY};
    
    // 计算模型中心点
    modelCenterX = (minX + maxX) / 2;
    modelCenterY = (minY + maxY) / 2;
}

自适应投影矩阵

java 复制代码
private void updateProjectionMatrix(int width, int height) {
    // 获取视图宽高比
    float viewRatio = (float) width / height;
    
    // 获取模型尺寸
    float modelWidth = boundaries[1] - boundaries[0];
    float modelHeight = boundaries[3] - boundaries[2];
    float modelRatio = modelWidth / modelHeight;
    
    // 计算缩放因子,使模型填满视图(保持比例)
    float scale;
    if (modelRatio > viewRatio) {
        // 模型比视图更宽,以宽度为基准
        scale = 2.0f / modelWidth;
    } else {
        // 模型比视图更高,以高度为基准
        scale = 2.0f / modelHeight;
    }
    
    // 应用用户设置的缩放因子
    scale *= scaleFactor;
    
    // 计算偏移量,使模型居中
    float offsetX = -modelCenterX;
    float offsetY = -modelCenterY;
    
    // 应用用户设置的偏移
    offsetX += userOffsetX;
    offsetY += userOffsetY;
    
    // 设置模型矩阵(缩放和平移)
    Matrix.setIdentityM(modelMatrix, 0);
    Matrix.translateM(modelMatrix, 0, offsetX, offsetY, 0);
    Matrix.scaleM(modelMatrix, 0, scale, scale, 1.0f);
    
    // 计算最终的MVP矩阵
    Matrix.multiplyMM(mvpMatrix, 0, viewMatrix, 0, modelMatrix, 0);
    Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, mvpMatrix, 0);
}

性能优化

1. 顶点缓冲区优化

为了提高渲染性能,我们使用了顶点缓冲区:

java 复制代码
// 初始化顶点缓冲区
ByteBuffer bb = ByteBuffer.allocateDirect(vertices.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(vertices);
vertexBuffer.position(0);

2. 渲染模式优化

根据不同场景需求,我们提供了两种渲染模式:

java 复制代码
// 连续渲染模式,适合动态数据
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);

// 按需渲染模式,适合静态数据,节省电量
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

3. EGL上下文保留

为了避免频繁重建OpenGL上下文,我们启用了EGL上下文保留:

java 复制代码
// 设置保留EGL上下文
setPreserveEGLContextOnPause(true);

如何使用,查看 github 依赖 release aar 包。

实际应用场景

该库可应用于多种场景:

  1. 医疗诊断:可视化患者体表温度分布,辅助医生诊断炎症、血液循环问题等
  2. 运动科学:分析运动员在不同运动状态下的肌肉热量分布
  3. 健康监测:个人健康应用中监测体温异常
  4. 人体工程学研究:评估不同环境条件下人体的热舒适度

未来展望

我们计划在未来版本中添加以下功能:

  1. Kotlin协程支持
  2. 自定义热力图颜色主题
  3. 动态温度变化动画
  4. 多人体模型支持
  5. 机器学习模型集成,用于温度异常检测

总结

本文详细介绍了基于OpenGL ES的Android人体热力图可视化库的设计与实现。通过该库,开发者可以轻松地在自己的应用中集成人体热力图功能,为用户提供直观的体温分布可视化。该库采用模块化设计,提供了简洁的API,同时保持了良好的性能和可扩展性。

希望这个库能为医疗健康、运动科学等领域的Android应用开发提供帮助。欢迎大家使用、反馈和贡献代码!


相关推荐
喝拿铁写前端5 小时前
前端与 AI 结合的 10 个可能路径图谱
前端·人工智能
codingandsleeping5 小时前
浏览器的缓存机制
前端·后端
一笑的小酒馆6 小时前
Android在ksp中简单使用Room
android
灵感__idea6 小时前
JavaScript高级程序设计(第5版):扎实的基本功是唯一捷径
前端·javascript·程序员
摇滚侠6 小时前
Vue3 其它API toRow和markRow
前端·javascript
難釋懷6 小时前
JavaScript基础-history 对象
开发语言·前端·javascript
beibeibeiooo7 小时前
【CSS3】04-标准流 + 浮动 + flex布局
前端·html·css3
拉不动的猪7 小时前
刷刷题47(react常规面试题2)
前端·javascript·面试
浪遏7 小时前
场景题:大文件上传 ?| 过总字节一面😱
前端·javascript·面试
meimeiqian7 小时前
flutter android端抓包工具
android·flutter