ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

足底压力异形热力图绘制指南:从离散插值到边界裁剪

足底压力异形热力图绘制指南:从离散插值到边界裁剪 简介面向生物力学、运动医学及穿戴设备开发者的实用示例包演示如何将柔性压力传感器阵列采集的足底离散压力点转换为直观的异形热力图。整套以鞋垫为应用场景完整覆盖数据筛选、插值、归一化及坐标映射等关键环节为非规则传感器布局下的数据可视化提供了可直接借鉴的解决路径。压缩包共三个文件包含可运行的Python绘图脚本、用于过程校验的测试图片以及最终高分辨率输出图整体体积仅六十八KB。脚本基于常用的科学计算与绘图库实现适合有一定Python基础、希望提升不规则数据可视化能力的开发者学习。下载后可掌握非规则网格热力图的绘制思路获得可修改的示例代码了解从离散点阵到连续分布的完整流程并借助前后对比图验证插值策略与归一化效果。该思路还能迁移至其他柔性传感阵列或曲面压力分布场景具有较强参考价值。目前已有二百二十九人学习适合作为触觉传感、足底压力分析等方向的入门参考。1. 异形热力图为什么难画鞋垫上几十个离散点要还原一只脚的受力做足底压力监测的人都有个共同烦恼柔性压力传感器阵列铺在鞋垫里采集回来的数据是稀疏离散点——常见的是“8 行 × 8 列”或“16 行 × 16 列”的传感器节点每帧只有几十个数值。可临床、康复评估乃至鞋垫选型时大家想看的不是数字表格而是一张像“踩脚印”一样连续、带颜色渐变、边界贴合鞋垫轮廓的足部压力热力图。难点在于两个一是数据稀疏相邻传感器间距往往有 10 毫米以上点与点之间没有真实测量值必须靠插值构造过渡二是边界不规则鞋垫不是矩形后跟圆、前掌宽、足弓处内凹如果直接把插值结果框成矩形输出图会“越界”把鞋垫外本不存在的压力也画出来。这篇文章就从这两个问题展开完整走一遍“传感器坐标标定 → 离散点插值 → 异形裁剪 → 动态刷新”的链路适合正在做足底压力可视化、柔性传感阵列数据展示的开发者参考也适用于任何非矩形区域内离散点绘制热力图的场景。2. 数据准备与坐标标定从传感器行列号到鞋垫物理坐标的映射2.1 传感器阵列布局与坐标原点选择拿到鞋垫压力数据的第一步是先搞清楚传感器的物理位置。柔性压力传感器阵列通常是等间距排布例如“8 行 × 8 列”的压阻式阵列行距和列距都是 10 毫米左右。但鞋垫是异形的传感器不可能铺满整个鞋垫所以阵列往往只覆盖承重区域——前掌、后跟覆盖密度较高足弓处可能没有传感器。我这里有一个比较省事的原点约定以鞋垫后跟中心线为 y 轴以鞋垫最宽处的中线为 x 轴原点取两者交点。这样左右脚的数据做镜像处理时更直观。传感器阵列的原始数据通常按行列编号存储例如data[row][col]其中row0对应阵列的最前缘或最后缘具体要看采集板定义。标定前的第一件事就是把这套行列号映射到物理坐标。我一般会在采集板上电后做一次“空载标定”让鞋垫平放不踩踏记录每个传感器的零点偏移。这是因为压阻式传感器有温度漂移和零点漂移空载值可能不是 0。后续所有帧数据都减去这个零点偏移才能得到真实的压力增量。2.2 物理坐标到画布坐标的仿射变换数据进入可视化管线之前需要统一坐标系。常见做法是先定义传感器物理坐标(x_mm, y_mm)再通过仿射变换映射到画布像素坐标(u, v)。画布坐标系和图像坐标系一致原点在左上角u 轴向右v 轴向下而物理坐标系一般是原点在中心或左下角y 轴向上。这两个坐标系的 y 方向相反如果不做翻转出来的热力图会上下颠倒——这是一个非常容易踩的坑。下面这段代码把传感器物理坐标换算成画布坐标并自动处理 y 轴翻转import numpy as np # 传感器行列号 - 物理坐标单位mm # pitch_x, pitch_y: 阵列列距和行距 # origin_x, origin_y: 物理坐标系原点在阵列中的位置以行列号表示 def row_col_to_phys(row, col, pitch_x10.0, pitch_y10.0, origin_x3.5, origin_y3.5): x_mm (col - origin_x) * pitch_x y_mm (row - origin_y) * pitch_y return x_mm, y_mm # 物理坐标 - 画布像素坐标 # scale: 像素/毫米例如 5 表示每毫米 5 像素 # canvas_h: 画布高度像素offset_x, offset_y: 画布上鞋垫区域左上角偏移 def phys_to_canvas(x_mm, y_mm, scale5.0, canvas_h720, offset_x40, offset_y40): u x_mm * scale offset_x v -y_mm * scale offset_y canvas_h / 2 # y 轴翻转 return int(u), int(v) # 示例第 3 行第 4 列传感器 row, col 3, 4 x_mm, y_mm row_col_to_phys(row, col) u, v phys_to_canvas(x_mm, y_mm) print(frow{row}, col{col} - 物理坐标({x_mm:.1f}, {y_mm:.1f})mm f- 画布坐标({u}, {v}))逻辑说明row_col_to_phys把行列号转换为物理坐标origin_x和origin_y表示物理原点对应到阵列中的位置。例如 8×8 阵列的物理原点若设在中心则origin_x3.5、origin_y3.5。phys_to_canvas里-y_mm是关键——物理坐标系 y 向上为正画布坐标系 y 向下为正翻转不处理好足跟和脚尖会调个。参数说明scale决定了热力图的输出分辨率一般取 3~5像素/毫米鞋垫长度 250mm 时5 像素/毫米会生成 1250 像素宽的图做展示足够做实时预览时建议降到 2减少插值和绘制开销。offset_x、offset_y用来在画布上给鞋垫周围留白。如果你用的采集板 SDK 已经给出了物理坐标序列可以省略row_col_to_phys直接做画布映射。2.3 离散点数据清洗坏点、零漂与接触噪声柔性传感器阵列最常见的脏数据有三类坏点某个传感器始终无响应或满偏、零漂空载时输出不为 0 且缓慢变化、接触噪声踩踏瞬间的冲击尖峰。如果不清洗插值出来的热力图上会出现一个异常亮点或暗点看起来像脚底踩了颗石子。我一般用两步处理第一离线标定时记录坏点列表直接把坏点从插值数据源里剔除第二在线数据流上做滑动窗口滤波抑制接触噪声。import numpy as np # 单帧数据清洗 # 假设 frame 是 (n_rows, n_cols) 的二维数组 # bad_points: 已知坏点集合元素为 (row, col) def clean_frame(frame, bad_points): # 剔除坏点 frame frame.copy() for (r, c) in bad_points: frame[r, c] np.nan # 滑动窗口均值滤波3x3 窗口边缘自动扩展 from scipy.ndimage import uniform_filter # 先用 NaN 填充坏点再做 3x3 平滑坏点位置用邻域均值回填 valid np.isfinite(frame) smoothed uniform_filter(np.where(valid, frame, 0.0), size3, modenearest) counts uniform_filter(valid.astype(float), size3, modenearest) frame np.where(valid, frame, smoothed / np.maximum(counts, 1e-6)) return frame # 示例第 2 行第 5 列传感器为坏点 frame_raw np.random.rand(8, 8) * 30 bad_points {(2, 5)} frame_clean clean_frame(frame_raw, bad_points)逻辑说明clean_frame先把坏点位置填NaN然后利用uniform_filter做邻域平滑坏点值用周围有效点的均值回填。uniform_filter的size3指的是 3×3 邻域第二个counts数组记录每个邻域内有多少个有效点避免分母为零。参数说明3×3 窗口适合 8×8 的阵列如果你的阵列是 16×16 或更密可以考虑 5×5但注意窗口越大真实的压力峰值被削得越狠。如果后续要做精确的峰值压力分析滑动窗口会低估峰值这种情况下改用中值滤波scipy.ndimage.median_filter更稳妥。这些清洗操作要在插值之前做因为插值算法对输入点的异常值非常敏感一个坏点就可能把周围一片区域都带偏。3. 离散点插值从几十个压力值到连续热力场的关键一步3.1 为什么不能直接画散点图有人图省事直接把传感器点位的压力值画成散点图每个点用颜色表达数值大小看起来也算“热力图”。但这有两个问题第一离散点之间的过渡区域没有视觉效果你不知道前掌中间那块压力究竟是怎么分布的第二散点图是“点状”视觉人眼很难把它感知为连续的足印。插值就是要在已知点之间构造出连续的标量场。对于足底压力这种物理量合理假设是压力在空间上连续变化且相邻区域数值不会突变。常用插值方案有三类线性插值基于 Delaunay 三角剖分、径向基函数RBF插值、以及保有理样条PCHIP变体。下面重点讲前两种它们在足底压力可视化场景里够用且可控。3.2 Delaunay 三角剖分与线性插值最快的方案Delaunay 三角剖分把离散点连接成三角形网格且保证每个三角形的最小内角尽量大——这样插值结果不容易出现细长三角形带来的伪影。在线性插值方案中每个三角形内部的压力值由三个顶点线性组合得到计算量小适合实时刷新。import numpy as np from scipy.spatial import Delaunay from scipy.interpolate import LinearNDInterpolator # 已知点集pts 形状为 (n_points, 2)values 形状为 (n_points,) # pts 中每个点是 (x_mm, y_mm) 物理坐标 n_row, n_col frame_clean.shape pts, values [], [] for r in range(n_row): for c in range(n_col): if np.isfinite(frame_clean[r, c]): x_mm, y_mm row_col_to_phys(r, c) pts.append([x_mm, y_mm]) values.append(frame_clean[r, c]) pts np.array(pts) values np.array(values) # 构建插值器 tri Delaunay(pts) interp_linear LinearNDInterpolator(tri, values) # 生成网格点在鞋垫包围盒内生成 1mm 分辨率网格 gx np.arange(pts[:, 0].min() - 10, pts[:, 0].max() 10, 1.0) gy np.arange(pts[:, 1].min() - 10, pts[:, 1].max() 10, 1.0) gxx, gyy np.meshgrid(gx, gy) grid_vals interp_linear(gxx, gyy)逻辑说明Delaunay(pts)构建三角形网格LinearNDInterpolator接受三角剖分对象和顶点值后续查询任意网格点坐标就能得到插值结果。返回的grid_vals中落在所有三角形外部即凸包外的查询点是NaN。这一步很重要——鞋垫的足弓区域可能没有传感器那些位置的插值结果天然是NaN在后面裁剪时会派上用场。参数说明网格分辨率1.0表示每平方毫米一个网格点对应 5 像素/毫米的画布分辨率足够平滑如果机器性能强且想要更高清可以改成0.5但插值耗时和内存占用会上升约 4 倍。注意gy的生成方向和 numpy 数组的行序相反——np.arange从小到大生成坐标而meshgrid生成的gxx第一行对应最小的gy如果你最终用imshow显示需要np.flipud翻转一次否则图会上下颠倒。3.3 RBF 插值想要更平滑的压力过渡但注意过拟合线性插值的缺点是过渡带不光滑在三角形边界处会有视觉可见的“折痕”尤其是传感器间距大的区域。径向基函数插值用一组基函数的线性组合逼近原始数据能生成 C∞ 光滑曲面足底压力分布看起来更自然——整体过渡柔和更接近真实软组织受压时的渐变。from scipy.interpolate import RBFInterpolator # RBF 插值器 # neighbors: 每个查询点参考邻近点个数kernel: 高斯核 avg_dist np.mean(np.linalg.norm(pts[:, None, :] - pts[None, :, :], axis2)) interp_rbf RBFInterpolator( pts, values, kernelgaussian, epsilon1.0 / avg_dist, neighbors12 ) grid_vals_rbf interp_rbf(gxx, gyy)逻辑说明RBFInterpolator是 scipy 较新的接口取代了旧版Rbf。neighbors12表示每个查询点只使用最近的 12 个已知点参与计算这样局部细节保留得更好也避免全局面拟合导致的计算量爆炸。epsilon是高斯核的带宽参数1.0 / avg_dist让带宽和传感器间距匹配——间距越大带宽越小插值作用半径相应放大。参数说明epsilon是 RBF 插值里最玄学的参数。设置过大时每个已知点只影响极小的邻域插值结果会出现“牛眼”伪影设置过小时曲面过于平滑压力峰值被严重抹平。一个可靠的起点是epsilon 1.5 / avg_dist然后看可视化结果微调。另外RBF 插值会外推——查询点即使在所有已知点的凸包外也会得到非零的预测值这点和线性插值不同。后处理必须用掩码把这些区域置为NaN否则鞋垫外会出现一大片伪压力。3.4 线性插值与 RBF 插值的取舍对比维度Delaunay 线性插值RBF 高斯核插值计算速度快适合实时逐帧刷新慢点集多时明显可用neighbors缓解插值结果连续但不光滑三角形边界有折痕光滑连续视觉更接近自然受压分布凸包外行为返回NaN天然截断会外推需要后处理掩码峰值保留程度较好取决于epsilon调不好会削峰实现复杂度低中实际使用时我会分两种场景实时预览比如踩踏力反馈用线性插值16×16 阵列在一台普通电脑上能做到 30fps 以上离线分析和报告出图用 RBF 插值追求的是视觉平滑度和临床可读性。两边都不冲突插值器可以共存切换时重建一次网格即可。4. 异形边界裁剪与热力图渲染两种落地路线完整实现4.1 鞋垫边界数据从哪来异形热力图的核心难点在“异形”二字。要裁剪出鞋垫形状首先需要鞋垫的边界轮廓。常见获取途径有三一是直接用鞋垫的 CAD 设计文件导出轮廓多边形二是用扫描仪或手机拍照后轮廓提取三是最笨的办法——在鞋垫实物上沿边缘均匀采集几十个点的坐标手工生成多边形。我通常用第二种一张鞋垫照片背景纯色用 OpenCV 提取轮廓然后手工删掉杂点、boxfit 对齐坐标系。下面给出一个简化版本用多边形顶点数组直接定义边界因为在实际工程里你可能已经通过上面的方法得到了一组边界坐标。import numpy as np from matplotlib.path import Path # 鞋垫边界多边形顶点物理坐标单位 mm # 这里是一个简化的左脚鞋垫轮廓实际应从 CAD/扫描获得 sole_boundary np.array([ [0, 0], [12, 22], [20, 45], [24, 75], [25, 110], [22, 145], [16, 175], [8, 205], [0, 225], # 脚尖外侧 [-6, 205], [-12, 175], [-16, 140], [-18, 105], # 足弓内侧 [-15, 70], [-10, 35], [0, 0] ]) # 用 matplotlib Path 构建掩码 def make_sole_mask(gxx, gyy, boundary): points np.column_stack([gxx.ravel(), gyy.ravel()]) path Path(boundary) mask path.contains_points(points).reshape(gxx.shape) return mask mask make_sole_mask(gxx, gyy, sole_boundary)逻辑说明Path.contains_points逐一判断网格点是否落在鞋垫多边形内返回布尔掩码。这个掩码就是后续所有裁剪操作的基础——不仅插值结果要用它过滤画图时也要把它作为透明度通道叠加在热力图上。参数说明边界顶点的顺序要沿逆时针方向否则contains_points的判断结果可能全部反相。如果边界点太多导致计算慢可以先对多边形做shapely.geometry.Polygon(buffer(0))简化再导出顶点。还有一个细节鞋垫边界点坐标系要和传感器物理坐标系一致否则裁剪位置会整体偏移图上的压力区域和鞋垫轮廓对不上。4.2 matplotlib 路线contourf 叠加裁剪与自定义色标有了插值网格和边界掩码绘制异形热力图的完整流程就清晰了。下面这段代码完成线性插值 → 边界掩码 →contourf填充 → 边界描边 → 参数调整。import matplotlib.pyplot as plt import matplotlib.path as mpath from matplotlib.colors import Normalize # 将掩码外的网格值置为 NaN grid_masked np.where(mask, grid_vals, np.nan) fig, ax plt.subplots(figsize(8, 10)) # 压力色标低-中-高对应蓝-黄-红 cmap plt.cm.get_cmap(jet) levels np.linspace(np.nanmin(grid_vals), np.nanmax(grid_vals), 40) # 绘制等值面 cf ax.contourf(gxx, gyy, grid_masked, levelslevels, cmapcmap) # 画鞋垫边界线 boundary_x sole_boundary[:, 0] boundary_y sole_boundary[:, 1] ax.plot(boundary_x, boundary_y, k, linewidth1.2) # 传感器位置叠加 phys_all np.array([row_col_to_phys(r, c) for r in range(n_row) for c in range(n_col)]) ax.scatter(phys_all[:, 0], phys_all[:, 1], cwhite, s8, edgecolorsblack, linewidths0.3, zorder5) ax.set_aspect(equal) ax.invert_yaxis() # 让 y 轴朝上模拟俯视足底 plt.colorbar(cf, axax, labelPressure (arb. unit)) plt.show()逻辑说明contourf的核心动作是把插值网格以等值面方式填充。np.where(mask, grid_vals, np.nan)把鞋垫外的区域设置为NaN这样contourf在鞋垫外不会绘制任何色块——这就是异形热力图的“裁剪”动作。随后ax.plot画出边界轮廓线让图形更清晰。传感器位置用白色散点叠加方便对比原始数据点位置和插值结果。参数说明levels40是等值面分度数数值越大颜色过渡越细腻但相邻色带之间的差值会变小人眼反而难分辨一般 30~50 合适。cmap选择jet是最直观的压力配色但要注意jet的亮度不均匀蓝色部分在灰度打印时几乎是黑色如果图要进论文或报告建议换viridis或turbo。ax.invert_yaxis()在这里的作用物理坐标系 y 向上为正matplotlib 的 y 轴默认向上不invert的话图上方向你预期的方向不一样后跟会显示在上方。4.3 Web 端路线ECharts 自定义系列 Canvas 遮罩实现异形裁剪如果系统是 B/S 架构要在网页里显示足底压力热力图常见方案是 ECharts。但这里有一个坑ECharts 内置的热力图组件只支持矩形网格它不接受异形边界。我曾经试过直接用热力图 series 画矩形网格再叠一个遮罩层盖住鞋垫外区域结果遮罩和热力图坐标对不齐效果很乱。正确做法是改用 EChartscustom系列在renderItem回调里自己画小矩形再用一个覆盖物裁掉边界外的色块。实现思路如下——先用 Python 端算好插值网格维度约 200×60和掩码将数据导出为 JSON前端在渲染时只画掩码内的小方格。// renderItem 回调的简化实现 function renderPressureHeatmap(params, api) { // 从数据项拿到网格坐标和压力值 const x api.coord([api.value(0), api.value(1)])[0]; const y api.coord([api.value(0), api.value(1)])[1]; const val api.value(2); // 归一化颜色 const norm (val - minVal) / (maxVal - minVal); const color rgba(255, ${Math.round(200 * norm)}, 0, 0.85); // 每个网格点画一个小矩形宽度为网格分辨率对应的像素 return { type: rect, shape: { x: x - cellW / 2, y: y - cellH / 2, width: cellW, height: cellH }, style: { fill: color } }; }逻辑说明renderItem是 EChartscustom系列的核心它返回一个图形元素描述对象。每次调用接收到一个数据项数据项前三项是(x, y, value)其中x, y是网格中心点的笛卡尔坐标value是插值后的压力值。api.coord把数据坐标转换成像素坐标然后把像素坐标作为小矩形的中心点进行绘制。矩形大小就是网格分辨率对应的像数尺寸不会有缝隙。参数说明网格分辨率在 Python 端生成时用 2mm/格对应 5 像素/毫米的 scale每格就是 10 像素。矩形大小取一格像素刚好无缝拼接。前端渲染性能的瓶颈在于矩形数量——200×60 的网格有 1.2 万个格子浏览器绘制成 canvas 后在普通电脑上能稳定 30fps如果再加密建议用离屏 canvas 离屏渲染一次再用zrender的 image 元素贴上去。异形裁剪策略是Python 端只导出掩码内的网格点这样前端根本不会绘制鞋垫外的色块这个方案比“遮罩覆盖”稳定得多。4.4 伪彩映射统一量程避免“同鞋不同色”热力图的配色量程是个容易忽略的细节。如果你加载两帧数据一帧踩踏较轻、一帧较重直接按照各自帧的最大最小值做颜色归一化那么同样的压力值在两张图上颜色完全不同——看起来像是脚的某个部位压力突变实际只是量程变了。正确做法是确定全局量程然后让所有帧共用同一个归一化区间。量程的选取可以根据标定时的最大允许压力来定如果没有标定就用一段静置采集的最大值加 20% 余量。# 统一量程所有帧都使用相同的 vmin/vmax GLOBAL_VMIN 0.0 GLOBAL_VMAX 120.0 # 绘制帧时 def plot_frame(ax, grid_values, mask, vminGLOBAL_VMIN, vmaxGLOBAL_VMAX): grid_masked np.where(mask, grid_values, np.nan) cmap plt.cm.get_cmap(jet).copy() # 低于 vmin 或高于 vmax 的值也映射到色标而非丢弃 norm Normalize(vminvmin, vmaxvmax) cf ax.contourf(gxx, gyy, grid_masked, levels40, cmapcmap, normnorm) return cf逻辑说明Normalize强制全局量程这样所有帧的同一颜色代表同一压力值。contourf在默认情况下会自动取当前帧的最小最大值这就是“同鞋不同色”的根源这里显式传入norm后不会再发生色差。参数说明GLOBAL_VMAX的标定策略——压阻式传感器的输出范围和驱动电压有关通常以满量程的 80% 为上限因为传感器在上限附近的响应曲线非线性明显。如果你用的是已经校准为 kPa 的数据直接把量程设为临床常用区间如 0~400kPa即可。5. 绘制中的 5 个高频翻车点坐标、掩码、色标与插值边界排查记录5.1 热力图整体上下颠倒坐标系方向没翻转现象画出来的足部轮廓是后跟在上下颠倒或者左右脚镜像对不上。原因物理坐标系 y 轴朝上而图像坐标系 y 轴向下phys_to_canvas做映射时漏掉了-y_mm导致鞋垫被垂直翻转。解决在phys_to_canvas中把y_mm取负数配合canvas_h / 2做偏移。这里需要提醒的是左右脚镜像问题只靠 y 翻转解决不了还需要把 x 方向也取反——这是最常见的“左右脚穿错鞋”问题因为左脚和右脚的轮廓是镜像对称的边界坐标必须分别定义不能直接复用。5.2 裁剪边缘出现锯齿状空隙掩码精度不够现象鞋垫轮廓边缘出现一排白色锯齿像是图像被“啃”了一口。原因掩码判断是离散的——网格分辨率 1mm 时边界附近的网格点要么在里要么在外不会存在半透明过渡当边界斜率大时锯齿就非常明显。解决最简单的是把掩码做一次高斯平滑把布尔掩码转成 0~1 浮点 alpha再乘到颜色数组上。from scipy.ndimage import gaussian_filter # 布尔掩码 - 软掩码平滑过渡带 soft_mask gaussian_filter(mask.astype(float), sigma1.0) grid_masked np.where(soft_mask 0.1, grid_vals, np.nan) # 绘制时用 soft_mask 作为 alpha 通道可配合 imshow 使用逻辑说明gaussian_filter把严格的 0/1 掩码变成 0~1 渐变边缘外 1~2mm 的范围形成半透明过渡带视觉上锯齿消失。.astype(float)是必须的——布尔数组不能直接参与高斯滤波。参数说明sigma1.0对应 1mm 左右的过渡带宽度。sigma 太大会导致鞋垫外的压力“渗”进来鞋垫边缘的颜色看起来模糊太小则锯齿还在。网格分辨率 1mm 时 sigma 取 0.8~1.5 均可接受。注意这只影响视觉边缘不影响插值精度。5.3 热力图足弓区域出现“假压力岛”外推插值的锅现象鞋垫足弓内侧没有传感器但热力图在该区域却显示出一片中等强度的压力色块。原因RBF 插值支持外推即使查询点远离所有已知点也会算出非零值。鞋垫足弓区域的网格点不在传感器构成的多边形凸包内线性插值会返回NaN而 RBF 不会。解决构建一个只属于所有传感器点的凸包掩码把凸包外的插值结果全部置为NaN。from scipy.spatial import ConvexHull from matplotlib.path import Path hull ConvexHull(pts) hull_pts pts[hull.vertices] hull_path Path(hull_pts) # 判断网格点是否在传感器凸包内 hull_mask hull_path.contains_points( np.column_stack([gxx.ravel(), gyy.ravel()]) ).reshape(gxx.shape) # 最终有效区域 鞋垫边界 传感器凸包 final_mask mask hull_mask grid_final np.where(final_mask, grid_vals_rbf, np.nan)逻辑说明ConvexHull计算出所有传感器点构成的最小凸多边形contains_points判断网格点是否在该凸多边形内。最终绘图时把鞋垫边界和传感器凸包两个条件做交集足弓区域因为没有传感器不会显示假压力。注意hull.vertices的顺序是逆时针的可以直接交给Path使用。参数说明如果足弓区域你其实想要一个平缓的压力过渡而不是直接显示为空白可以把final_mask改为“鞋垫边界内 传感器凸包外”的区域填充一个固定底色比如深灰模拟无信号状态。这样既不会出现假压力也不会让人觉得数据缺失。5.4 动态刷新时颜色整体抖动每帧独立归一化的后果现象同样的踩踏力度前一帧热力图整体是黄色后一帧变成了红色压力没有变颜色大变样。原因每一帧绘制时没有传入vmin/vmaxcontourf自动取当前帧的最大值做归一化。解决策略在前面 4.4 节已经说过这里再补充一个工程细节动态刷新时不要每次重建contourf对象而应该复用同一个句柄只更新其set_array数据否则绘图开销大且可能产生残留色块。# 动态刷新时复用 contourf 句柄 cf ax.contourf(gxx, gyy, grid_masked_pre, levelslevels, cmapcmap, normnorm) def update(new_grid): new_masked np.where(final_mask, new_grid, np.nan) # 更新数据 for coll in cf.collections: coll.remove() ax.contourf(gxx, gyy, new_masked, levelslevels, cmapcmap, normnorm)逻辑说明cf.collections是contourf生成的所有 Polygon 对象先remove再重建是 matplotlib 里更新等值面图的常见方式。注意new_grid也要经过相同的掩码处理否则新帧的等值面会在鞋垫外多画出来。参数说明如果你用的是imshow而不是contourf更新更简单img.set_data(new_masked)然后对matplotlib.animation.FuncAnimation的效率会好很多。所以我通常建议骨架采用imshow 软掩码的方式而不是contourf性能差距在实时刷新时非常明显。5.5 传感器坐标和热力图轮廓总差几个毫米未考虑鞋垫变形现象把热力图叠在鞋垫实物图上传感器点的位置和图上压力中心有明显偏差。原因柔性传感器阵列在鞋垫弯折后发生了位移——传感器是刺绣在柔性基材上的鞋垫在穿着时前掌区弯折传感器之间的实际距离变短而标定时是按平面距离算的。解决在标定时就考虑鞋垫的二维轮廓用轮廓上的关键点脚尖、脚跟、前掌外侧边缘和传感器阵列的相对位置比例来重新插值计算传感器坐标而不是直接按固定的行列 pitch 计算。这在实时采集系统里极难彻底解决一个折中的做法是在标定时记录鞋垫未弯折的平面坐标同时记录鞋垫受压时的实际形状拍照提取边界用这两个边界做仿射变换的配准把传感器坐标重新映射到变形后的鞋垫坐标系。如果项目对精度要求不高接受 2~3mm 的偏差直接忽略这一步也可以——但至少要知道偏差存在做动静态对比分析时不要对这 2mm 的差异过度解读。6. 从静态热力图到动态监测压力中心轨迹与逐帧时序播放的进阶玩法只画一张静态热力图对康复分析和运动监测来说远远不够。步态分析里最常用的两个进阶指标是压力中心轨迹Center of Pressure, CoP和分时相热力图序列。前者能反映平衡能力后者能直观呈现步态周期内足底压力的转移过程。压力中心的计算很简单——它是压力分布的加权质心x_cop sum(p_i * x_i) / sum(p_i) y_cop sum(p_i * y_i) / sum(p_i)其中p_i是第 i 个传感器的压力值(x_i, y_i)是它的物理坐标。放在热力图上通常叠加一条随时间变化的曲线并用颜色深浅或箭头标记时间顺序。实现时把每帧计算出的 CoP 坐标连成线叠加在第 4 章的坐标轴上即可。注意 CoP 计算要用手清洗后的帧数据而且坏点必须剔除否则一个坏点就能把质心拉偏。动态时序播放的常见实现是matplotlib.animation.FuncAnimation。一个实践中值得注意的性能技巧FuncAnimation更新时不要重绘整个画布用blitTrue并只更新热力图和边界线的句柄如果帧率还是不够就降低网格分辨率到 2mm/格、把contourf换成imshow。我这边的实测经验是16×16 阵列、1mm 网格、imshowFuncAnimation的松鞋模式在一台 i5 处理器上能跑到 25fps 左右足够预览。踩过的最后一个坑是时间对齐采集板的数据帧率可能不稳定画动态图时不要用“帧索引”当时间轴要用采集板硬件时间戳。否则压力中心和热力图序列的速度会忽快忽慢看起来像人在跳着走。我的习惯是数据进管线前先做时间戳重采样统一到 50Hz 的固定时间网格再做滑动窗口平滑。做到这里异形热力图就从一个“画图脚本”升级成了可以支撑步态分析、平衡评估的完整可视化模块。希望这些排错和参数选择的经验能帮你在自己的鞋垫数据上少走一遍我走过的弯路。本文还有配套的精品资源点击获取
返回列表