基于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. 轮廓提取技术
轮廓提取是整个项目的基础,我们采用了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 游戏引擎
- 人体轮廓渲染
- 温度数据热力图映射
- 透明度调节
- 图像居中处理
架构设计
该库采用模块化设计,主要包含以下几个核心组件:
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. 视图填充与居中技术
为了使人体模型正确地填充整个视图并居中显示,我们实现了一套自适应的投影矩阵计算方法。
- 切割扫描出来的坐标 找到 边界点。
- 去除边界点,把图像移到最左边。
- 把图像填充满 Y 周,修改坐标
- 把图像移动到中央 --主要是移动视窗,与图像大小的关系。
模型边界计算
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 包。
实际应用场景
该库可应用于多种场景:
- 医疗诊断:可视化患者体表温度分布,辅助医生诊断炎症、血液循环问题等
- 运动科学:分析运动员在不同运动状态下的肌肉热量分布
- 健康监测:个人健康应用中监测体温异常
- 人体工程学研究:评估不同环境条件下人体的热舒适度
未来展望
我们计划在未来版本中添加以下功能:
- Kotlin协程支持
- 自定义热力图颜色主题
- 动态温度变化动画
- 多人体模型支持
- 机器学习模型集成,用于温度异常检测
总结
本文详细介绍了基于OpenGL ES的Android人体热力图可视化库的设计与实现。通过该库,开发者可以轻松地在自己的应用中集成人体热力图功能,为用户提供直观的体温分布可视化。该库采用模块化设计,提供了简洁的API,同时保持了良好的性能和可扩展性。
希望这个库能为医疗健康、运动科学等领域的Android应用开发提供帮助。欢迎大家使用、反馈和贡献代码!