基于双线性插值与AMG8833的M5StickC Plus红外热成像系统实现

发布时间:2026/7/28 4:13:29

基于双线性插值与AMG8833的M5StickC Plus红外热成像系统实现 1. 项目缘起从“热成像”到“热成像摄像头”的执念几年前我第一次接触到热成像技术那种能“看见”温度的能力让我着迷。但市面上成品的热成像仪要么价格昂贵要么功能单一很难满足我这种喜欢动手折腾的人。后来我发现了AMG8833这款红外热成像传感器它只有指甲盖大小价格亲民能输出8x864个像素点的温度矩阵。这让我萌生了一个想法能不能用它配合一个便携的开发板做一个属于自己的、能实时显示热力图的“红外摄像头”这个想法听起来简单但实际操作起来第一个拦路虎就是分辨率。8x8的像素点直接显示在屏幕上就是一个模糊的马赛克方块根本谈不上“成像”。这就像你拿一个只有64个格子的棋盘去描绘一幅风景画细节全无。为了解决这个问题我决定引入图像处理中一个经典的技术双线性插值。它的核心思想就是利用已知的、稀疏的像素点通过数学计算“猜”出它们之间那些未知点的值从而将图像“放大”到我们想要的尺寸让画面看起来更平滑、更连续。我的硬件选择是M5StickC Plus。它体积小巧自带一块1.14英寸的彩色IPS屏幕和锂电池还有Wi-Fi和蓝牙非常适合做这种便携式的显示终端。把AMG8833通过I2C接口连接到M5StickC Plus上一个硬件上的“红外摄像头”原型就搭好了。剩下的就是如何用代码让这64个温度数据点变成屏幕上那幅生动、直观的热力图。这篇文章就是记录我如何一步步实现这个想法并解决其中关键问题的完整过程。2. 核心硬件选型为什么是M5StickC Plus和AMG8833在开始写代码之前硬件平台的确定是第一步。这个选择直接决定了项目的可行性、便携性和最终效果。我对比了几种方案最终锁定了M5StickC Plus AMG8833这个组合原因如下。2.1 M5StickC Plus为便携显示而生的“瑞士军刀”市面上常见的开发板如Arduino Uno或NodeMCU虽然也能驱动AMG8833但它们缺少一个关键部件集成的显示屏。你需要额外连接一个OLED或TFT屏这会让整个项目变得臃肿供电和接线也复杂很多。M5StickC Plus几乎是为这类项目量身定做的集成度高它自带一块1.14英寸、135x240分辨率的彩色IPS屏幕。这意味着我们不需要为显示部分额外操心拿到手就能用。供电方便板载一颗120mAh的锂电池可以通过Type-C接口充电。这让设备可以完全脱离电脑和电源线运行实现真正的“手持式”体验。性能足够其核心是一颗ESP32-PICO-D4芯片双核240MHz主频性能远超普通的Arduino。处理64个点的数据并进行双线性插值计算再实时渲染到屏幕上对它来说游刃有余。接口丰富它提供了一个GROVE接口本质上是I2CGPIO正好可以用来连接AMG8833模块接线极其简单只需要四根线VCC, GND, SDA, SCL。编程友好支持Arduino IDE和UIFlow图形化编程社区资源丰富遇到问题容易找到解决方案。注意M5StickC Plus的GROVE接口是3.3V电平而AMG8833模块的工作电压也是3.3V两者可以直接连接无需电平转换这又省去一个麻烦。2.2 AMG8833低成本入门热成像的“眼睛”AMG8833是松下出品的一款红外热成像传感器网格阵列。它的核心参数决定了我们这个项目的起点和上限分辨率8x8像素共64个独立的热电堆传感器。这是它最大的限制也是我们为什么要做插值的原因。测温范围默认-20°C到80°C通过配置可以扩展到0°C到80°C。对于室内环境、电子设备发热检测、人体感应等场景完全够用。精度±2.5°C。对于定性观察热分布哪里热、哪里冷来说足够了但不要指望用它做高精度的温度计量。刷新率最高10Hz。这意味着每秒可以更新10帧数据对于实时观察缓慢变化的温度场如人手移动、电器发热过程来说基本流畅。通信接口I2C。这使得它与微控制器的连接变得非常简单只需要两根数据线。选择AMG8833就是选择在有限的成本下切入热成像领域。它提供的不是一个“图像”而是一个8x8的“温度矩阵”。我们的所有软件工作都是围绕如何将这个矩阵转化为人眼可友好识别的图像。3. 从温度到颜色热力图的渲染逻辑拿到AMG8833的64个温度数据后我们不能直接把数字扔到屏幕上。我们需要一套规则将温度这个标量值映射成屏幕上的颜色。这个过程就是伪彩色渲染。3.1 温度标定与范围设定AMG8833读出的原始数据是传感器ADC值需要根据数据手册中的公式转换为摄氏度。通常库函数会帮我们完成这一步。更关键的是我们需要确定一个温度显示范围。因为实际场景的温度可能从室温25°C到一杯热水60°C再到一个发热的芯片80°C跨度很大。如果固定使用传感器全量程如0-80°C那么当观察一个温差很小的场景时比如人手可能就31-34°C整个画面可能都是一种颜色缺乏对比度。因此一个实用的做法是提供自动或手动缩放功能自动缩放每一帧都计算当前64个点中的最小温度T_min和最大温度T_max然后将这个区间[T_min, T_max]映射到整个颜色谱上。这样能最大化每一帧的视觉对比度清晰显示温差。手动固定范围用户可以设定一个关心的温度区间比如[20°C, 40°C]用于观察人体超出范围的颜色饱和最冷或最热色。这便于在不同时间、不同场景下进行对比。在我的实现中我优先采用了自动缩放因为它最“智能”能适应各种未知场景。同时我在代码里预留了手动设置范围的接口。3.2 颜色映射方案的选择确定了温度值归一化到0-1之间0代表T_min1代表T_max后就要给它分配颜色。常见的方案有彩虹色系从蓝冷到绿温到红热。这是最直观、最常用的方案符合大众认知。铁红色系从黑到暗红到亮黄再到白。这种方案在显示高温物体如熔炉、电路板过热点时对比度更强。灰度色系从黑到白。虽然信息量相同但人眼对灰度的分辨能力远不如对色彩的分辨能力。我选择了彩虹色系作为默认方案因为它视觉效果最好能一眼看出温度梯度。在M5StickC Plus的屏幕上我们需要将最终的颜色转换为RGB565格式16位色。这意味着我们需要一个函数输入一个0到1的值输出一个RGB565颜色。我实现了一个简单的getColor()函数其逻辑如下// 简化示例将0-1的值映射到彩虹色 uint16_t getColor(float value) { value constrain(value, 0.0, 1.0); // 限制在0-1 uint8_t r, g, b; if (value 0.25) { // 蓝 - 青 r 0; g (uint8_t)(value * 4 * 255); b 255; } else if (value 0.5) { // 青 - 绿 r 0; g 255; b (uint8_t)((1.0 - (value - 0.25) * 4) * 255); } else if (value 0.75) { // 绿 - 黄 r (uint8_t)((value - 0.5) * 4 * 255); g 255; b 0; } else { // 黄 - 红 r 255; g (uint8_t)((1.0 - (value - 0.75) * 4) * 255); b 0; } // 将RGB888转换为RGB565 (R:5位, G:6位, B:5位) return ((r 0xF8) 8) | ((g 0xFC) 3) | (b 3); }这样对于插值后得到的每一个“像素”的温度值我们都能计算出它对应的颜色从而准备填充屏幕。4. 灵魂所在双线性插值算法详解与实现这是本项目的核心算法。AMG8833给我们的原始数据是一个8行8列的矩阵。我们的屏幕分辨率是135x240。如果我们简单地将一个温度点拉伸成一个大色块显示出来就是8x8个大方块毫无细节可言。双线性插值的目的就是根据这64个已知点计算出屏幕上成百上千个点的“推测”温度值使图像变得平滑。4.1 双线性插值原理说人话版想象一下你有一张4个钉子拉起来的渔网2x2网格每个钉子的高度温度已知。现在你想知道渔网中间任意一点的高度怎么办双线性插值就是解决这个问题的办法。具体到我们的8x8网格扩展到135x240屏幕映射关系首先建立屏幕坐标(x_screen, y_screen)与原始8x8温度矩阵坐标(x_grid, y_grid)的映射。因为屏幕宽135像素我们要显示8个网格所以每个网格在水平方向约占135/7 ≈ 19.3像素注意是7个间隔因为8个点有7个间隔。同理垂直方向240/7 ≈ 34.3像素。这样屏幕上的每个像素点都能找到它在原始温度矩阵中对应的“虚拟”浮点数坐标。找到最近的四个已知点对于屏幕上的一个目标点P它必然落在原始8x8网格的某一个单元格内。我们找到包围这个单元格的四个角点左上Q11右上Q21左下Q12右下Q22。这四个点的温度是已知的。两次线性插值第一次插值X方向在顶部边缘Q11和Q21之间根据P点的水平位置比例插值算出R1点的温度。在底部边缘Q12和Q22之间同样插值算出R2点的温度。第二次插值Y方向在R1和R2之间根据P点的垂直位置比例最终插值算出P点的温度。这个过程相当于先在水平方向“猜”两次再在垂直方向“猜”一次综合了四个角点信息结果比最近邻插值直接取最近角点的值要平滑得多。4.2 在ESP32上的高效实现在资源有限的微控制器上实现这个算法需要考虑效率。我们不能对屏幕上的每一个像素135*24032400个都进行完整的双线性插值计算那会非常慢。我们需要优化。我的策略是先插值后渲染。我定义了一个中间缓冲区大小可以是16x16,32x32或64x64。这里我选择了32x32作为一个平衡点。它比8x8细腻计算量又比直接插值到135x240小很多。我只对这个32x32的缓冲区进行双线性插值计算。计算量从32400次降低到1024次大大减少。然后在屏幕上绘制时将这个32x32的缓冲图像通过简单的“最近邻”放大算法绘制到135x240的屏幕上。因为32x32已经比较平滑再次放大带来的锯齿感在观看热力图时可以接受。核心插值函数代码如下// 将8x8的原始温度数组src双线性插值到32x32的数组dst中 void bilinearInterpolate(float src[8][8], float dst[32][32]) { for (int y 0; y 32; y) { float gy ((float)y / 31.0) * 7.0; // 映射到[0, 7]的网格坐标 int gyi (int)gy; // 整数部分即左上角点的行索引 float gyf gy - gyi; // 小数部分即垂直方向的比例因子 for (int x 0; x 32; x) { float gx ((float)x / 31.0) * 7.0; // 映射到[0, 7]的网格坐标 int gxi (int)gx; // 整数部分即左上角点的列索引 float gxf gx - gxi; // 小数部分即水平方向的比例因子 // 获取四个角点的温度值注意数组边界检查 float q11 src[gyi][gxi]; float q21 src[gyi][gxi1]; float q12 src[gyi1][gxi]; float q22 src[gyi1][gxi1]; // 双线性插值公式 float r1 q11 * (1 - gxf) q21 * gxf; // 顶部边缘插值 float r2 q12 * (1 - gxf) q22 * gxf; // 底部边缘插值 dst[y][x] r1 * (1 - gyf) r2 * gyf; // 垂直方向最终插值 } } }这个函数运行一次就将8x8的数据“平滑”到了32x32。实测在ESP32上一次计算仅需几毫秒完全不影响10Hz的刷新率。5. 软件架构与关键代码剖析有了硬件、渲染逻辑和核心算法我们需要用代码将它们有机结合起来。整个项目的软件流程是一个清晰的循环。5.1 主程序流程与状态管理程序的主体是一个loop()循环每一帧执行以下步骤数据采集通过I2C从AMG8833读取64个像素的温度值存入一个8x8的二维数组rawTempGrid[8][8]。这里要处理I2C通信可能出现的错误比如设备未连接。数据滤波可选但推荐原始温度数据可能存在噪声。我加入了一个简单的移动平均滤波filteredTempGrid α * rawTempGrid (1-α) * lastFilteredTempGrid。参数α取0.3到0.7之间能在平滑度和响应速度间取得平衡。插值计算调用bilinearInterpolate(filteredTempGrid, interpolatedGrid)函数生成32x32的插值后温度矩阵。温度范围计算遍历interpolatedGrid或filteredTempGrid找出当前帧的T_min和T_max用于颜色映射的归一化。屏幕绘制清屏。遍历屏幕上每一个像素或分块遍历以提高速度根据其坐标映射到interpolatedGrid中的对应位置取得温度值归一化调用getColor()获取颜色最后用M5.Lcd.drawPixel()或更快的fillRect()进行块绘制。在屏幕边缘绘制一个颜色条Color Bar标注T_min和T_max的实际温度值方便读数。UI交互检测M5StickC Plus机身上的按键电源键、HOME键用于切换显示模式如自动/手动温标、暂停/继续刷新等。5.2 性能优化技巧在ESP32上实时渲染图形需要关注性能否则会影响刷新率导致卡顿。减少drawPixel调用drawPixel是相对较慢的函数。对于热力图这种大面积色块更好的方法是使用fillRect。我可以将32x32的插值网格每个点对应屏幕上的一个4x7或4x8的矩形块因为135/32≈4.2,240/327.5然后用fillRect填充整个矩形这样只需要1024次绘图调用而不是32400次速度提升一个数量级。使用局部刷新如果温度变化不剧烈可以考虑只重绘温度变化超过一定阈值的区域。但这增加了逻辑复杂度对于10Hz的刷新率和ESP32的性能来说全屏重绘通常可以接受。浮点数运算ESP32有硬件浮点单元但大量浮点运算仍有开销。在插值函数中对于gx,gy,gxf,gyf这些系数可以考虑使用定点数运算来进一步加速但会牺牲一些代码可读性。在我的测试中当前的浮点实现已能满足10Hz帧率。6. 实际测试、现象分析与调优代码写完后烧录到M5StickC Plus接上AMG8833真正的乐趣和挑战才刚刚开始。测试能让你发现设计时考虑不到的问题。6.1 典型测试场景与现象对准手掌这是最直观的测试。屏幕上会出现一个黄色的亮块温度较高轮廓大致是手的形状但边缘比较模糊。手指之间的缝隙由于温差小且传感器分辨率低可能无法清晰显示。移动手掌热力图会平滑地跟随变化延迟感很小。对准电脑主板或手机可以清晰看到芯片如CPU、电源管理IC是高温热点周围电路区域温度较低。这对于快速定位发热元件很有帮助。观察一杯热水杯子边缘温度高中心由于水面反射环境温度可能显示温度反而低一些这展示了热成像对表面辐射率的依赖性。面对窗户或墙壁由于室内外温差窗户玻璃会显示为一个明显的冷区蓝色而墙体保温不好的地方也可能出现温度差异。6.2 遇到的主要问题与解决方案图像“马赛克”感依然存在即使做了32x32插值放大到全屏后有时仍能看到明显的块状结构。原因双线性插值只在局部4个点间平滑当相邻网格温度差异很大时插值区域交界处仍会有不连续感。解决尝试了双三次插值。它考虑周围16个点平滑效果更好。但在ESP32上计算量显著增加32x32网格需要计算约16倍于双线性的采样帧率下降到约2-3Hz。对于实时显示我最终放弃了选择了性能更优的双线性。如果不需要高帧率双三次插值的图像质量确实更胜一筹。温度读数不稳定噪声静止对准一个均匀温度的表面热力图上仍有细小的颜色波动。原因传感器本身的噪声、电路噪声以及环境微气流的影响。解决软件滤波如前所述引入移动平均滤波效果立竿见影画面变得稳定。但滤波会引入延迟参数α需要根据应用场景调整。多次采样平均在数据采集阶段连续读取多次如4次然后取平均值再交给后续处理。这能进一步抑制随机噪声。硬件考虑确保AMG8833供电稳定I2C线上加上拉电阻M5StickC Plus内部已带传感器远离风扇等气流扰动源。颜色条刻度跳动因为采用自动温标T_min和T_max每一帧都可能微变导致颜色条旁边的数字频繁跳动观看不适。解决对T_min和T_max也进行缓变处理。例如新一帧的显示温标范围 0.8 * 旧范围 0.2 * 新计算出的范围。这样颜色条只会平滑地缩放而不会剧烈跳动除非温度发生剧烈变化。7. 项目总结与扩展思考经过硬件连接、算法实现、代码编写和反复调试这个基于M5StickC Plus和AMG8833的双线性插值红外摄像头终于能稳定工作了。拿在手里它能实时地将周围的热世界转化为屏幕上流动的色彩这种将不可见变为可见的成就感是购买成品设备无法比拟的。回顾整个项目双线性插值算法是提升视觉表现力的关键。它用有限的数学计算显著改善了低分辨率传感器的显示效果这个思路可以应用到任何需要“数据升维”或“图像平滑”的嵌入式显示项目中。而M5StickC Plus作为一款高度集成的硬件平台极大地降低了开发门槛让我们可以专注于核心算法的实现和应用逻辑的构建。这个项目本身还有很大的扩展空间更高阶的插值算法如前所述可以尝试双三次插值甚至Lanczos插值牺牲帧率换取更精美的图像。温度报警功能可以设置一个温度阈值当检测到某个区域温度超过阈值时让设备发出声音用M5StickC Plus的蜂鸣器或屏幕闪烁报警。数据记录与回传利用ESP32的Wi-Fi功能将温度数据实时上传到服务器或手机APP进行长期监测和数据分析。机械结构可以3D打印一个外壳将传感器和屏幕固定甚至加入一个简单的云台方便对准目标。最后从实操经验来看有两点特别值得分享一是滤波参数需要根据实际场景耐心调整没有一劳永逸的值二是在资源受限的嵌入式设备上永远要在效果和性能之间做权衡。例如我最终选择32x32的插值网格和双线性算法就是在看了实际效果并测试了帧率后做出的折中选择。有时候一个“足够好”的解决方案比一个“理论上最优”但难以实现的方案更能让项目成功落地。

相关新闻