简介:鞋垫式柔性压力传感器阵列采集的离散压力点,非常适合详细演示异形热力图的绘制方法;资源代码基于Python编程语言,完整完成数据筛选、插值、归一化以及传感器不规则布局到二维坐标的映射,最终输出足部压力分布色图,可用于生物力学分析、运动医学和穿戴设备开发等场景。压缩包仅68KB,共3个文件,其中Python脚本为源码,负责数据处理与可视化,测试图和输出图分别用于验证坐标映射和查看最终热力效果,方便学习者对照实践。目前已有229人学习浏览,这份轻量级示例结构清晰、依赖少,既能帮助初学者理解从离散点到连续热力分布的计算思路,也可替换传感器坐标后迁移至其他柔性接触压力分布场景,是相关研究与产品开发中值得参考的代码模板。
1. 为什么普通热力图搞不定“异形”需求
1.1 这个需求到底难在哪
我跟你讲,第一次接到这个需求的时候,我也有点低估了。表面上看,不就是画个热力图吗,matplotlib里一个imshow就能搞定。但实际情况完全不是这么回事。鞋垫上的柔性压力传感器阵列,它不是一个规规矩矩的矩形网格,而是按照脚掌的实际形状来排布的。脚趾区域、前掌区域、足弓区域、后跟区域,传感器密度不一样,甚至有些地方是空缺的。你从采集端拿到的数据,本质上是几十个乃至上百个离散点,每个点包含三个信息:物理坐标 (x, y) 和压力值。
普通热力图工具要求你喂进去的是一张完整的二维矩阵,比如 32x32 的像素阵列。可传感器数据哪里是矩阵?它就是不规则的“点云”。你要是硬把点云掰成矩阵,要么空缺位置填零,导致热力图出现大片假性的“零压区”,要么就得做插值。更麻烦的是,鞋垫本身是异形的,你要是直接画一张矩形热力图,边缘会延伸到鞋垫外面去,看起来特别业余,也没法跟实物对应起来。所以这个项目的本质,其实是两个技术问题的叠加:第一,把稀疏离散点插值成连续的压力分布场;第二,用鞋垫的真实轮廓把这张连续场裁剪成异形区域。
1.2 整体方案选型与思路
当时我手头的环境是 Python 3.9,数据处理用 NumPy + SciPy,可视化成图用 Matplotlib。工具的选型思路很简单:SciPy 的griddata是目前做散点插值最成熟的方案,支持最近邻、线性、三次立方等多种插值方式;Matplotlib 的Path类可以直接做多边形裁剪。这两个库配合起来,完全不需要额外引入像 OpenCV 或者 Plotly 这种偏重量级的依赖,轻巧、可控、出了问题也好排查。
完整的处理链路是这样的:先做数据清洗和坐标归一化,再通过griddata把离散点插值到一块规则网格上,紧接着用鞋垫轮廓多边形生成掩膜,把它跟插值后的网格做一次蒙版运算,最后用contourf或imshow配合自定义色带渲染出异形热力图。这一套流程拆开看每一步都不难,难就难在中间几个“坑位”你事先不知道,等到画出来的图歪七扭八才开始排查,那效率就太低了。下面我把每个环节需要关注的细节逐一展开。
2. 数据准备:从离散点到网格的第一步
2.1 传感器布局与坐标映射
在做任何插值之前,先得搞清楚你的传感器到底长在哪儿。我这边用的鞋垫是 8x12 的阵列,一共 96 个采样点,传感器间距大约 1 厘米左右,脚掌边缘区域的点会刻意往内侧收一收,避免传感器悬空在鞋垫边缘。这时候如果你只看数据不看实物,很容易把坐标方向搞反。
我建议你这样做:拿到鞋垫之后,先画一张坐标图,把传感器的物理位置一个一个标出来。坐标原点我习惯放在后跟中心点,x 轴指向脚掌左侧,y 轴指向前脚掌方向,单位用厘米。这一步很土,但特别管用。因为后面的插值网格、轮廓多边形、甚至实拍图的叠加,全都依赖这个坐标基准。
映射的时候有个小细节,传感器的行列号跟实际物理坐标往往不是线性对应的,尤其是靠近脚趾的弧形区域,第 1 行传感器可能并不是严格等间距排列。所以千万不要用“我觉得它大概在这个位置”来填坐标,一定要用手册里的结构图,或者用游标卡尺实际量,把每个点的坐标落到表格里。坐标错 2 毫米,插值出来的云图在边界上就会出现肉眼可见的扭曲和凹陷。
import numpy as np # 传感器物理坐标(单位:cm),以鞋垫后跟中心为原点 # 这里只展示前几个点,实际应是完整的 96 点坐标表 sensor_coords = np.array([ [3.0, 20.5], [5.2, 20.7], [7.4, 20.6], [9.6, 20.4], # 脚趾区 [2.8, 18.9], [5.1, 19.1], [7.6, 19.0], [9.8, 18.7], # ... 其他行数据 ]) # 同帧的压力数据,单位可先不做换算,用原始 ADC 值也行 pressure_vals = np.array([128, 256, 512, 341, 156, 234, 689, 879, ...])2.2 压力数据的采集与预处理
坐标搞对了,接下来是对压力值本身做预处理。柔性压阻传感器的原始数据其实挺脏的,特点有两个:一是零漂,就是传感器在不受力的时候输出不一定是 0,可能有个几十的基线;二是高频抖动,特别是人在行走落地的那一瞬间,ADC 采样值上下跳得厉害。
我处理的方式是这样:静态标定阶段,采集 5 秒空载数据,对每个通道取平均值作为该通道的基线值,之后所有原始值先减掉这个基线。然后再上滑窗均值滤波,窗口宽度取 5 个采样点。别小看这两步,减基线能让你热力图的冷区颜色真正反映出“无压力”而不是“压力 20”,滤波能防止热力图出现那种一帧一个样的雪花噪点。
另外,做科研或产品演示的时候,压力单位最好统一。柔性压阻传感器出来的原始值是 ADC 码值,可以用砝码压出几个已知力值,做一次线性回归,把码值换算成 kPa。这一步可以事后做,但在项目一开始就要把接口留好,不然热力图画完才发现要换单位,又得重新跑一遍全流程。
def preprocess(raw_data, baseline, window=5): # raw_data shape: (n_frames, n_channels) data = raw_data - baseline kernel = np.ones(window) / window filtered = np.apply_along_axis( lambda m: np.convolve(m, kernel, mode="same"), axis=0, arr=data ) return np.clip(filtered, 0, None)3. 插值算法与网格化:把稀疏点变成连续面
3.1 为什么必须插值,以及有哪些选择
离散点是没法直接画成云图的,你总不能让热力图上一格一格的色块悬在半空。所以必须插值。插值说白了就是:根据已知点的压力值,推测未知位置的压力值。这个“推测”的逻辑不同,出来的效果差异会非常大。
在 SciPy 的griddata里,常见三种方法。最近邻(nearest)是把离目标网格最近的那个传感器的值直接赋过去,结果像马赛克,几乎不可用。线性插值(linear)是用周围三个点拟合成一个平面,计算快、稳定性好,但出来的图有棱有角,视觉上不够顺滑。三次立方插值(cubic)是用周围更多点去拟合一个光滑曲面,视觉效果最好,但有副作用:它可能算出负数或者超出传感器最大量程的值,这在物理上是不成立的。
我的建议是:做预览和调试用线性插值,速度飞快;做最终成果图和数据报告用 cubic,然后对结果做个截断处理,把负值归零,超过上限的压回上限。如果你拿到一款传感器数据的空间分布比较均匀,比如点间距大致一致,cubic 基本不会出幺蛾子;但要是某个区域传感器特别稀疏,比如足弓区域可能只有一两个点,cubic 就容易在空洞区域出现明显的“过冲”,看起来像有个压力尖峰,遇到这种情况就得考虑把整个区域的数据单独处理或者降低插值阶数。
| 插值方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| nearest | 计算最快,严格保留原始值 | 块状感强,不连续 | 快速预览、点间距极大 |
| linear | 速度快,数值稳定 | 视觉有棱角,不够平滑 | 调试阶段、数据稀疏 |
| cubic | 光滑连续,视觉效果好 | 可能出现物理不可能值 | 最终出图、演示汇报 |
3.2 基于 SciPy 的插值实现
插值的实现本身不复杂,核心就几行代码。先把目标网格建出来:在鞋垫所在的空间范围内,生成一个足够密集的规则网格。网格密度要怎么选?假如鞋垫长 25 厘米,宽 10 厘米,传感器间距是 1 厘米,那么网格间距取 0.05 厘米就已经非常精细,再密就是纯粹增加计算负担。按这个密度,一个网格就是 500x200,总计十万个点,griddata跑 cubic 也只需要一两秒。
这里要提醒一个容易翻车的细节:griddata要求输入的网格点是“坐标对”的形式,也就是你要用np.meshgrid生成 X 方向和 Y 方向的坐标矩阵,然后ravel()拉平。标准的输入是这样:points是传感器坐标数组,shape 为 (N, 2),values是压力值数组,shape 为 (N,),xi是你要插值的目标坐标数组,shape 为 (M, 2)。
from scipy.interpolate import griddata # 定义网格范围,比传感器最外侧坐标稍微外扩一点 grid_x, grid_y = np.mgrid[ x_min:x_max:500j, y_min:y_max:200j ] # 插值,返回的 grid_z 就是二维矩阵 grid_z = griddata( sensor_coords, pressure_vals, (grid_x, grid_y), method="cubic" ) # 修正 cubic 插值可能出现的物理不可能值 grid_z = np.clip(grid_z, 0, np.max(pressure_vals))插值完成后,你手上就有一块规则的二维矩阵grid_z,它覆盖了整个鞋垫外接矩形范围。这一步做完,大多数教程就停在这里了,直接拿imshow把矩阵画出来。但画出来的图必然是矩形的,边缘会渗出鞋垫范围,这也是“异形热力图”和“矩形热力图”拉开差距的关键节点。
4. 异形边界裁剪与最终可视化
4.1 用多边形掩膜实现鞋垫轮廓
要裁剪出鞋垫轮廓,就得先有鞋垫轮廓。获取轮廓的方式不外乎两种:如果鞋垫是你们自己设计的,结构图纸里可以直接导出轮廓点;如果是买来的现成鞋垫,最简单的方式是把它平放在扫描仪或者坐标纸上,手动描点采样轮廓,每隔一厘米取一个点就行,精度完全足够。
拿着轮廓点集,用matplotlib.path.Path生成一个闭合多边形。这个路径对象可以做两件事:第一,判断某个坐标点是否在多边形内部;第二,批量判断一组点。后面这件事才是我们真正需要的。
实现思路是这样的:把插值网格里的每一个坐标点取出来,逐一判断它在不在鞋垫轮廓内部。如果在内部,保留;如果在外部,标记为无效。这里我强烈建议用Path.contains_points方法,它支持一次性传入整个坐标数组,返回一个同样形状的布尔数组,效率比逐点循环高得多。然后把插值结果grid_z转成一个 masked array,外部区域的数值设为掩膜,视觉上就会变成透明或者背景色。
from matplotlib.path import Path import numpy.ma as ma # outline_points: 鞋垫轮廓坐标,首尾相连的闭合多边形 outline = Path(outline_points) # 把网格坐标拉平后做包含判断 points_flat = np.column_stack([grid_x.ravel(), grid_y.ravel()]) mask = outline.contains_points(points_flat) mask = mask.reshape(grid_x.shape) # 外部区域掩膜 grid_masked = ma.masked_where(~mask, grid_z)这一步做完,你就已经拥有一个“鞋垫形状”的二维压力场了。凡是鞋垫外面的网格点都是掩膜状态,画图的时候不会显示。
4.2 完整的绘制代码与效果调优
接下来是最后一步,把掩膜后的二维矩阵渲染出来。这里有两种常用渲染方式:一种是用imshow,另一种是contourf。imshow适合展示像素化的连续压力分布,色彩过渡细腻;contourf适合展示等压线层次,带一圈一圈的云图效果,科研论文里比较多见。
我个人更推荐先用imshow把连续场画出来,在上面叠加一层半透明的contour线,既有视觉细腻度,又能看出压强梯度方向。色带的选择上,jet这种彩虹色带虽然经典但容易误导人,红绿色盲用户看了会非常痛苦。我用的是coolwarm,蓝色表低压、红色表高压,中间色过渡自然,也符合人对“冷低压、热高压”的直觉。
import matplotlib.pyplot as plt from matplotlib.colors import Normalize plt.figure(figsize=(6, 10)) norm = Normalize(vmin=0, vmax=800) # 按实际数据范围调整 # 绘制插值热力图,注意要关闭插值平滑,保留真实分辨率 img = plt.imshow( grid_masked, extent=[x_min, x_max, y_min, y_max], origin="lower", cmap="coolwarm", norm=norm, aspect="equal", interpolation="bilinear" ) # 叠加轮廓线 plt.contour(grid_x, grid_y, grid_masked, levels=8, colors="gray", linewidths=0.5, alpha=0.5) # 把传感器位置标出来,方便排查坐标映射错误 plt.scatter(sensor_coords[:, 0], sensor_coords[:, 1], c="k", s=8) plt.colorbar(img, label="Pressure (kPa)") plt.xlabel("x (cm)") plt.ylabel("y (cm)") plt.title("Foot Pressure Distribution") plt.tight_layout() plt.show()绘制过程中有三个小细节值得注意。
坐标轴向问题。imshow默认的原点在左上角,图像会上下颠倒。如果你拿到的传感器坐标是后跟在下、脚趾在上,那就必须设置origin="lower",让图像和物理位置一致。否则画出来的热力图跟穿着鞋垫时的方向正好相反,踩后跟的时候热力图是在“脚尖”亮,排查半小时才发现是这一步的问题。
aspect="equal"一定要加。不加的话,Matplotlib 会自动拉伸纵横比,脚掌的宽度会被拉长,整个图看上去像被挤过,比例完全失真。设成equal之后,一厘米就是一厘米,跟真实鞋垫比例严格一致。
还有掩膜区域的边界锯齿问题。因为鞋垫轮廓是用的折线段多边形,网格分辨率不够高时,边界看起来会有锯齿。要解决这个问题,可以在生成鞋子轮廓时多采点样,轮廓点越多,掩膜边界越顺滑;或者把网格密度调高,比如从 500x200 提到 1000x400,锯齿肉眼就基本看不出来了。
4.3 左右脚镜像与多帧动画
说完单帧热力图,还有一个实际项目中绕不开的场景:你有左右两只鞋垫,或者你要连续处理几十帧数据来展示一次完整的行走过程。
先说说左右脚。左脚和右脚的传感器布局是镜像关系,如果代码里坐标表的构建方式是一样的,比如都是从“后跟中心为原点、x 轴指向脚外侧”这种方式推导的,那么左脚画出来的热力图跟右脚画出来的在视觉上就是互为镜像。这个在单张图上问题不大,但要并排对比左右脚受力时,建议都统一转换成“从脚背视角看”的坐标,或者明确标注“足迹方向”。
多帧数据做动画就更有意思了。把每一帧的压力值都过一遍同样的插值和掩膜流程,然后生成图片列表,用matplotlib.animation.FuncAnimation串起来。这里要给个性能提示:96 个点插值到 500x200 的网格,单帧耗时大约 100~300 毫秒,30 帧的动画就是大概 3~9 秒。如果非常卡,先把网格密度降到 300x120,或者改用线性插值,动画跑顺了再回到高精度出静态图。
5. 我在实际项目中踩过的坑
5.1 坐标错位导致的热力图漂移
第一个大坑是坐标方向搞反。我最初做第一版的时候,x 轴和 y 轴写反了,热力图整体转了 90 度,怎么调都觉得不对,最后把传感器点一个一个画出来比对才发现问题。建议你在写代码第二步,也就是映射坐标完成之后,就先画一张散点图,把传感器点位置跟鞋垫轮廓叠在一起看看。这一步只花一分钟,能省掉后面一整天的排查时间。
5.2 cubic 插值产生的“鬼影”高压区
第二个问题是情绪化比较强的:cubic 插值会在数据稀疏区域产生一些本来不存在的尖峰。我遇到过一次,足弓部位只有一个传感器点,周围全是边界,cubic 插值在这个区域跑出一个数值比实际踩着后跟还高的“假高压区”。排查了半天,最后把数据单独筛选出来看才发现是插值算法在“硬凹造型”。遇到稀疏区域,要么直接用线性插值,要么就把这个区域在网格中标记为无效,别让算法自作主张。
5.3 数据同步和数据缺失
第三个很容易忽略的问题是数据同步和缺失值。有些柔性传感器阵列在采集时,个别通道可能因为接触不良,静置状态下是一条直线,压力变化时完全没有响应。这类通道如果直接参与插值,会在它的坐标点周围画出一块恒定低压区,形成“死区假象”。对策是逐通道检查数据的标准差,如果某个通道在压力变化期间的标准差几乎是 0,基本可以断定是坏道,果断剔除或者置为 NaN,不要让坏数据污染整张热力图。
5.4 关于色彩映射的一个忠告
最后说一句色彩的事。热力图这东西,颜色就是信息。很多人喜欢用彩虹色jet,觉得鲜艳专业,但你知道吗,jet在低压区域是深蓝色、高压区域是红色,中间会经过青、绿、黄,这些都是视觉上容易被误读成“等距多级分类”的颜色,本质上它的亮度不是单调的。我用coolwarm或者viridis之后,很多合作方的反馈是“看着舒服多了,也更可信了”。如果你们公司有自己的品牌色,完全可以用LinearSegmentedColormap自定义色带,把品牌色融入热力图,展示效果会更有辨识度。
回过头来看,异形热力图这个需求看起来只是“画个图”,真正做起来其实是一套完整的数据处理流程:从物理坐标到逻辑坐标,从离散采样到连续场重建,再到几何裁剪和视觉呈现。每个环节都有独立的技术细节,都不算难,但环环相扣,任何一环掉链子都会让成品图“一眼假”。希望这篇实操记录能帮你绕开我当年踩过的那些坑。
本文还有配套的精品资源,点击获取