简介从 FLIR 热像仪输出的 JPG 中除了可见光原图原始热传感器数据也会嵌入在元数据中使用 Python 小工具可一次性提取可见光图像、热矩阵并转换为物理温度值同时结合边缘检测算法增强热图像。工具适合具备基础 Python 知识的开发者以及从事无人机热成像巡检、设备红外检测的工程人员可用于快速解析 Flir One、Xenmuse XTR、AX8 等机型生成的 JPG 文件若使用其他热像仪只需针对不同的嵌入式原始图像格式微调代码。压缩包共 7 个文件包括 2 个 py 核心脚本、3 张不同相机的示例 JPG、LICENSE 与 README 说明整体大小仅 891KB结构一目了然。示例图为 Flir One、Zenmuse XTR、AX8 实拍输出方便直接对照验证效果README 介绍环境依赖与调用方式底层模块封装了提取与转换流程读者可在此基础上扩展成批量处理或温度分析脚本。已有 2419 人学习下载适合用于学习热像仪元数据解析、红外图像增强及二次开发。1. 从FLIR热像仪图像里读出温度本质上是在读EXIF里的辐射数据很多工程师第一次拿到FLIR热像仪拍出的JPG用普通看图软件打开看到的只是一张伪彩色照片。但FLIR的 radiometric JPEG 里真正值钱的信息并不在像素颜色里而是藏在每个像素对应的原始辐射强度值以及写在EXIF里的一组温度校准参数。我们要做的就是用Python去解析这些数据还原出每个像素点的温度数值进而做批量分析、导出CSV、甚至生成你自己的温度图。这个需求常见于电气设备巡检、建筑热桥检测、实验室热分布记录。如果你手头只有FLIR的SDK或桌面软件往往只能一张张手动导出难以进入自动化流程。这篇文章会给出一条不依赖官方SDK的路径只用常见的Python库就能把温度数据读出来。做完之后你能理解FLIR的EXIF结构、会用exiftool验证原始数据也能写出一套批量读取脚本。2. 读懂FLIR图像的EXIF与温度换算原理2.1 FLIR图像文件里的温度数据藏在哪FLIR的radiometric JPEG从外观上看是普通JPG但文件里嵌入了多个APP1段和APP2段。普通相机只写一个EXIFFLIR会在EXIF里额外写入一组FLIR专用标签这些标签的字节内容不是纯文本而是一段自定义二进制结构。常见的标签名有标签名作用RawThermalImage原始热像数据通常是PNG格式的16位灰度图也可能直接是一段裸数据Emissivity表面发射率默认0.95ReflectedApparentTemperature反射环境温度单位KAtmosphericTemperature大气温度单位KIRWindowTemperature红外窗口温度单位KPlanckR1、PlanckR2、PlanckB、PlanckF、PlanckO普朗克公式校准系数AtmosphericTransAlpha1、AtmosphericTransAlpha2大气透射率系数这些参数中PlanckR1到PlanckO是计算温度的核心通常由FLIR出厂时标定。不同型号的热像仪这些系数不同。同一个传感器在出厂后基本固定但要注意AtmosphericTemperature和ReflectedApparentTemperature会随环境变化记录在图像里。RawThermalImage的二进制数据在EXIF里显示为一段压缩的PNG。这段PNG的每个像素值就是原始辐射计数范围通常是16位。你需要把它解出来再结合普朗克系数转换成开尔文温度。2.2 温度换算公式与参数FLIR官方文档里温度换算公式是tempK B / log( R1 / (R2 * (S O)) F )其中S是原始辐射计数值tempK是开尔文温度R1、R2、B、F、O就是上面的普朗克系数。如果从热像仪直接拿到的是16位PNG那么S就是PNG的像素值。如果拿到的是浮点raw那可能已经是直接温度值需要看Exif里的RawThermalImageType字段。公式的推导来自普朗克辐射定律的简化反演模型。FLIR在出厂时通过多点黑体标定拟合出这5个系数。你不需要理解热力学细节但要知道环境温度、发射率、大气透射率最终都会影响绝对温度精度。公式算出来的是开尔文转成摄氏度要减273.15。2.2.1 为什么不能直接读RGB伪彩色图像伪彩色图是FLIR内部根据温宽做映射后生成的每个像素的颜色对应一个温度但映射关系依赖当时设定的色带、温宽以及场景的动态范围。同样的颜色在不同温宽下代表完全不同的温度。所以从RGB反推温度是不可靠的必须回到原始辐射值。另外RawThermalImage有可能不是PNG而是直接以TIFF或未压缩数组形式存储。FLIR的exiftool插件能识别这些变体但你自己用Python解析时需要对前几个字节做判断。常见情况PNG头是0x89 0x50 0x4E 0x47。3. 用Python实现FLIR温度读取与原始照片提取3.1 环境准备python安装与vscode python环境配置如果你还没有Python环境先从官网下载并安装Python 3.10或更高版本。安装时注意勾选“Add Python to PATH”。然后在VSCode中安装Python扩展按CtrlShiftP打开命令面板选择“Python: Select Interpreter”指向你刚装好的Python。本次演示只需要两个核心库PIL和numpy。另外为了解析EXIF我建议先装exiftool作为命令行工具不依赖它也能写纯Python方案但exiftool用来验证中间结果非常省事。Windows下用choco install exiftoolmacOS下用brew install exiftoolLinux下用apt install libimage-exiftool-perl。在Python侧安装依赖pip install pillow numpy这里pillow负责打开图片和读取EXIFnumpy负责处理16位像素数组。如果你不想装exiftool后面会给出纯Python解析方案。3.2 读取EXIF温度参数先拿exiftool验证拿到一张FLIR JPG后先用exiftool把它内部所有标签都列出来确认你看到的字段与我上面表格是否一致。exiftool -FLIR:All 001.jpg运行后会输出大量FLIR专用标签。重点看这几个Raw Thermal Image : (Binary data 42984 bytes, use -b option to extract) Planck R1 : 21436.41015625 Planck R2 : 0.012894779764131953 Planck B : 1471.4691162109375 Planck F : 1.0 Planck O : -7448.405517578125 Emissivity : 0.95 Reflected Apparent Temperature : 293.0其中Raw Thermal Image显示的是二进制长度说明EXIF里确实有一段独立数据。继续用exiftool把这段数据导出为文件exiftool -b -RawThermalImage 001.jpg raw_thermal.png如果导出成功你可以直接用支持16位PNG的看图软件打开看到一张灰蒙蒙的图片那就是原始辐射强度图。接下来我们要用Python读取它然后套公式。3.3 核心代码解析Raw Thermal Image3.3.1 方案A调用exiftool子进程这是最稳的方案因为exiftool已经处理好各种FLIR变体。你在Python里调用它拿到PNG字节流再用PIL打开。import subprocess from PIL import Image import numpy as np image_path 001.jpg # 用exiftool把RawThermalImage导出到stdout raw_proc subprocess.run( [exiftool, -b, -RawThermalImage, image_path], capture_outputTrue ) raw_bytes raw_proc.stdout # 用exiftool读取普朗克系数 def get_flir_tag(tag: str) - float: proc subprocess.run( [exiftool, f-{tag}, image_path], capture_outputTrue, textTrue ) # 输出形如 Planck R1 : 21436.41 return float(proc.stdout.split(:, 1)[1].strip()) R1 get_flir_tag(PlanckR1) R2 get_flir_tag(PlanckR2) B get_flir_tag(PlanckB) F get_flir_tag(PlanckF) O get_flir_tag(PlanckO) # 打开原始辐射图 raw_img Image.open(io.BytesIO(raw_bytes)) raw_array np.array(raw_img).astype(np.float32) # 温度换算 temp_k B / np.log(R1 / (R2 * (raw_array O)) F) temp_c temp_k - 273.15 print(f图像尺寸: {temp_c.shape}) print(f最高温度: {temp_c.max():.2f} °C) print(f最低温度: {temp_c.min():.2f} °C)这段代码的逻辑是先用exiftool把EXIF中的二进制数据提取为字节流再用PIL的Image.open直接读取这段字节流。因为RawThermalImage本身是PNG格式所以PIL可以识别。之后把16位灰度图转成numpy数组套用普朗克反演公式。注意raw_array O的顺序不能错否则公式不成立。3.3.2 方案B纯PILstruct解析如果不想依赖exiftool可以用PIL的_getexif()方法拿到EXIF字典但PIL默认不解析FLIR的二进制结构。你需要先从EXIF里拿到RawThermalImage标签的原始值再用struct解出其中的PNG部分。PIL读出的EXIF中FLIR标签通常用IFDTag或ExifByteArray表示。但不同版本PIL对未知标签的处理方式不同。更可靠的做法是直接解析JPEG的APP1段import struct import numpy as np from PIL import Image import io def extract_raw_thermal(image_path): with open(image_path, rb) as f: data f.read() segments [] idx 2 while idx len(data): if data[idx] ! 0xFF: idx 1 continue marker data[idx1] if marker in (0xD8, 0xD9): idx 2 continue if marker 0xDA: # 图像数据开始 break length struct.unpack(H, data[idx2:idx4])[0] segments.append((marker, data[idx4:idx2length])) idx 2 length # 在APP1段里找Exif其中包含FLIR数据 for marker, payload in segments: if marker ! 0xE1 or not payload.startswith(bExif\x00\x00): continue if bRawThermalImage in payload: # 简化定位在payload中搜索PNG头 png_start payload.find(b\x89PNG) if png_start -1: continue png_bytes payload[png_start:] return png_bytes return None png_bytes extract_raw_thermal(001.jpg) raw_img Image.open(io.BytesIO(png_bytes)) raw_array np.array(raw_img)这个方案不需要exiftool但解析嵌在EXIF里的二进制结构时需要确保JPEG段遍历正确。代码里做了基本防护遇到SOS0xDA即终止避免把影像数据误FD入。提示纯Python解析方案对FLIR固件格式变化敏感。如果你拿到的图像不是标准radiometric JPEG建议先跑exiftool确认标签名再决定用哪套解析。4. 批量读取FLIR图像并导出温度图与CSV4.1 批量处理脚本实际项目中不会只处理一张图。常见做法是遍历目录下所有FLIR JPG对每张图提取温度数组和关键参数汇总成表格。import subprocess import numpy as np import pandas as pd from PIL import Image import io, os, glob def read_flir_temperature(image_path): # 复用前面的子进程方案 raw_proc subprocess.run( [exiftool, -b, -RawThermalImage, image_path], capture_outputTrue ) raw_bytes raw_proc.stdout if not raw_bytes.startswith(b\x89PNG): return None def get_tag(tag: str) - float: proc subprocess.run( [exiftool, f-{tag}, image_path], capture_outputTrue, textTrue ) return float(proc.stdout.split(:, 1)[1].strip()) R1 get_tag(PlanckR1) R2 get_tag(PlanckR2) B get_tag(PlanckB) F get_tag(PlanckF) O get_tag(PlanckO) raw_img Image.open(io.BytesIO(raw_bytes)) raw_array np.array(raw_img).astype(np.float32) temp_k B / np.log(R1 / (R2 * (raw_array O)) F) temp_c temp_k - 273.15 return temp_c files sorted(glob.glob(thermal_images/*.jpg)) records [] for fp in files: temp read_flir_temperature(fp) if temp is None: continue records.append({ file: os.path.basename(fp), mean_temp_c: temp.mean(), max_temp_c: temp.max(), min_temp_c: temp.min(), std_temp_c: temp.std(), }) df pd.DataFrame(records) df.to_csv(thermal_summary.csv, indexFalse)这段代码把每张图的平均温度、最高温度、最低温度和标准差记录下来。你可以增加环境温度、相对湿度等列用exiftool一并读出。这里pandas不是必须的但输出CSV时很方便。4.2 生成伪彩色温度图从温度数组生成伪彩色图可以用matplotlib的imshow配合颜色映射也可以直接用PIL的调色板。下面给出一个不依赖matplotlib的轻量方案。def save_temp_image(temp_c, output_path, colormapinferno): import matplotlib.pyplot as plt plt.figure(figsize(10, 8)) im plt.imshow(temp_c, cmapcolormap) plt.colorbar(im, labelTemperature (°C)) plt.axis(off) plt.tight_layout() plt.savefig(output_path, dpi150) plt.close()这里inferno是常见的温度显示色带。如果你不想装matplotlib也可以用PIL的Image.putpalette手动构造一条温度到颜色的映射但效果不如inferno细腻。4.3 输出CSV与参数说明批量处理时你需要同时保留原始图像路径和对应温度矩阵。CSV文件适合存标量统计量但完整温度数组最好存成.npy文件或者按原图坐标叠加成一张等尺寸的浮点TIFF。# 保存浮点温度数组为npy np.save(temp_array_001.npy, temp_c)提示.npy可以直接用np.load读取比重新解析FLIR JPEG快一个数量级。如果你需要频繁做算法验证建议先批量解析一次把结果缓存下来。5. 排错与验证不同FLIR型号、温度偏大、原始照片保存5.1 常见错误与解决办法读取FLIR图像时最容易碰到三个问题。第一个是RawThermalImage标签为空或读取失败。这通常表示你拍的不是radiometric JPEG而是普通JPEG或者FLIR在非辐射模式下导出的图像。解决办法是重新在热像仪上确认“保存原始数据”选项已打开部分型号需要设置成JPEG FPC或Radiometric JPEG模式。第二个是温度公式溢出。raw_array里的像素值存在0值这样R1 / (R2 * (0 O))可能产生除零。实际原始辐射图中的坏像素、饱和像素会漏进计算。通常的做法是在计算前先过滤valid (raw_array 0) (np.isfinite(temp_c)) temp_c[~valid] np.nan第三个是温度整体偏高或偏低。先看Emissivity是否符合被测物体表面。金属表面反射率高发射率可能只有0.3但EXIF里默认写的0.95这会造成很大误差。FLIR有一些型号会记录Emissivity为0.95但如果你在拍摄时手动调整了发射率参数EXIF里就是调整后的值。5.2 用已知黑体验证温度准确性验证你的读取脚本是否正确最好用面黑体或恒温槽。如果没有也可以用手头的恒温物体做粗略验证。sample_temp temp_c[100:200, 100:200] # 选中一块区域 print(f区域平均: {np.nanmean(sample_temp):.2f} °C)一个可行的验证流把热像仪对准一杯冰水混合物拍摄后脚本读出的温度应该在0°C到2°C之间。因为冰水混合物的发射率约0.97环境反射影响小适合当定标测试物。如果偏差超过1°C检查环境温度参数是否更新。热像仪在拍摄时记录了当时的AtmosphericTemperature但如果你很久没做内置传感器校准内部温度计可能漂移。5.3 原始照片与radiometric JPEG的区别标题里提到的“原始照片”在FLIR语境里其实有两个概念。一是RawThermalImage也就是EXIF里那段辐射数据。这段数据保存的是传感器响应的原始值经过出厂标定是温度计算的基础。二是在FLIR相机的SD卡里可能同时存在.jpg和.bin或.fts文件。.fts是FLIR专有的原始数据格式包含温控和标定状态但解析方式完全不同于JPG里的EXIF。如果你手上只有.fts文件读取路径就完全变了但正规数据分析场景中radiometric JPEG是标准交换格式。在自动化检测流程里优先让相机设为“同时保存JPEG和原始FTS”是必要的但如果你要直接写Python脚本直接用radiometric JPEG最省事。5.4 一个快速验证脚本的完整形态下面这个脚本可以当作独立工具使用输入一张FLIR JPG输出温度范围、最小值、最大值和伪彩色图。import subprocess, io, sys import numpy as np from PIL import Image import matplotlib.pyplot as plt def parse_flir(image_path): raw subprocess.run([exiftool, -b, -RawThermalImage, image_path], capture_outputTrue).stdout if not raw.startswith(b\x89PNG): raise RuntimeError(No radiometric data found) img np.array(Image.open(io.BytesIO(raw))).astype(np.float32) def tag(name): out subprocess.run([exiftool, f-{name}, image_path], capture_outputTrue, textTrue).stdout return float(out.split(:, 1)[1].strip()) r1, r2, b, f, o tag(PlanckR1), tag(PlanckR2), tag(PlanckB), tag(PlanckF), tag(PlanckO) kelvin b / np.log(r1 / (r2 * (img o)) f) return kelvin - 273.15 if __name__ __main__: temp parse_flir(sys.argv[1]) print(fShape{temp.shape} Min{np.nanmin(temp):.2f} Max{np.nanmax(temp):.2f}) plt.imshow(temp, cmapinferno); plt.colorbar(); plt.show()保存为flir_temp.py然后运行python flir_temp.py 001.jpg这条命令会直接打印温度范围并弹出伪彩色图。把这段代码块放在你的工具库里以后遇到FLIR图像就能快速分析。最后再强调一次验证数据正确性的唯一标准是你用已知温度物体看过偏差而不是单纯对比软件显示的数字。本文还有配套的精品资源点击获取