1. 热红外数据工程化处理的整体设计思路
1.1 为什么需要工程化处理
大疆行业无人机挂载热红外相机(如Zenmuse H20T、XT2、H30T系列)执行巡检任务后,拿到手的原始素材是一堆后缀为.jpg的文件。但如果你直接把这些文件拖进Pix4D或者大疆智图,会发现出来的正射影像只有颜色信息,没有温度数值。原因很简单:这些文件不是普通JPEG,而是RJPG(Radiometric JPEG),也就是辐射测量JPEG。它在普通JPEG的像素数据之外,额外嵌入了一段元数据,记录了每个像素对应的原始辐射值、发射率、环境温度、距离、湿度等参数。
问题在于,Pix4D这类摄影测量软件在空三和正射拼接阶段,会把RJPG当作普通图片处理,元数据在重采样、匀色、拼接过程中被丢弃。最终输出的正射影像虽然看起来有热红外色调,但每个像素的DN值已经和真实温度脱钩了。这就是为什么必须做工程化处理——把温度信息从单张RJPG中提取出来,再通过摄影测量解算出的几何关系,映射到正射影像的每个像素上。
我最早接触这个需求是在做光伏电站组件热斑巡检的时候。客户要的不是一张花花绿绿的热图,而是一张能直接读出每块组件温度值的正射影像,方便他们导入GIS系统做缺陷定位和发电量损失评估。那时候市面上没有现成的傻瓜工具,只能自己搭工作流。踩了几轮坑之后,总结出一套相对稳定的方案:大疆TSDK负责温度提取,Pix4D负责几何解算,Python负责数据融合。
1.2 技术路线选型的考量
市面上做热红外正射的路线不止一条,我简单对比一下常见的几种:
| 方案 | 温度提取 | 几何解算 | 优点 | 缺点 |
|---|---|---|---|---|
| 大疆智图+TSDK | TSDK | 大疆智图 | 一体化,操作简单 | 智图对热红外支持有限,大场景吃力 |
| Pix4D+TSDK | TSDK | Pix4D | 几何精度高,支持大场景 | 需要自己写融合代码 |
| FLIR Tools+Agisoft | FLIR SDK | Metashape | 温度提取成熟 | FLIR SDK授权费用高 |
| 纯Python方案 | 自己解析 | OpenCV | 免费 | 几何解算精度差,不适合工程 |
我最终选择TSDK+Pix4D+Python这条路线,核心原因有三个。第一,TSDK是大疆官方提供的热红外数据处理库,能直接读取RJPG里的温度矩阵,精度有保障,而且随大疆行业机免费提供。第二,Pix4D在测绘级摄影测量领域积累深厚,空三解算和正射纠正的精度经过大量工程验证,尤其是大区域、多架次的数据处理能力比大疆智图强不少。第三,Python做数据融合灵活度最高,无论是像素级映射还是批量处理,都能按项目需求定制。
提示:TSDK目前主要支持Windows和Linux平台,Python绑定需要自己编译或者用官方提供的C++接口封装。如果你不想折腾编译,可以用大疆提供的命令行工具先导出温度矩阵为TIFF,再用Python读取。
1.3 完整工作流的阶段划分
整个工作流我把它拆成四个阶段,每个阶段有明确的输入输出,方便排查问题:
阶段一:数据准备与质检。检查RJPG文件完整性,确认每张图片都包含温度元数据,剔除损坏或元数据缺失的文件。这个阶段用TSDK的dir命令或者Python脚本批量扫描即可。
阶段二:温度矩阵提取。用TSDK把每张RJPG解算成两个东西:一张温度矩阵(通常是16位或32位浮点TIFF,单位摄氏度)和一张对应的RGB预览图。温度矩阵的每个像素值就是真实温度,RGB图用于后续和Pix4D的空三结果做配准。
阶段三:Pix4D空三与正射解算。把RGB预览图导入Pix4D,走标准摄影测量流程:空三、点云、DSM、正射影像。关键是要导出相机参数文件和正射影像的几何变换关系,这是后续温度映射的桥梁。
阶段四:温度映射与正射融合。用Python读取Pix4D导出的相机参数和正射影像,结合TSDK提取的温度矩阵,通过共线方程反算每个正射像素对应的原始温度值,最终输出一张带温度信息的GeoTIFF。
这四个阶段里,阶段二和阶段四是最容易出问题的。阶段二常见问题是TSDK版本和相机固件不匹配,导致温度矩阵全零或者数值异常。阶段四常见问题是坐标系不统一,Pix4D用的是UTM,TSDK输出的是WGS84经纬度,映射的时候如果没做投影转换,温度会整体偏移。
2. 核心细节解析与实操要点
2.1 RJPG文件结构拆解
要理解整个流程,得先搞清楚RJPG到底存了什么。用十六进制编辑器打开一个RJPG文件,你会发现它和普通JPEG几乎一样,只是在APP1段或者自定义段里多了一段二进制数据。这段数据就是大疆私有的热红外元数据,结构大致如下:
- 文件头:标识版本、相机型号、图像宽高
- 温度矩阵:按行优先存储的浮点数组,每个值对应一个像素的温度
- 参数块:发射率、环境温度、相对湿度、拍摄距离、镜头透过率等
- 校准数据:黑体校准系数、坏点表
TSDK的作用就是解析这段私有数据,把温度矩阵和参数块提取出来。我实测过,同一台H20T在不同固件版本下,元数据段的偏移量可能不一样,所以不要自己写解析器,直接用TSDK最稳妥。
注意:有些第三方工具声称能解析RJPG,但遇到大疆新固件(比如H30T的某些版本)就会读出错值。TSDK虽然用起来麻烦点,但兼容性是最好的。
2.2 TSDK环境搭建与Python绑定
TSDK官方只提供C++库,Python要用的话得自己封装。我试过两种方式:一种是用ctypes直接调用动态库,另一种是用pybind11写一层薄封装。前者上手快但容易内存泄漏,后者编译麻烦但稳定。如果你只是做批量处理,建议用官方提供的命令行工具dji_irp,它可以直接把RJPG转成TIFF,省去编译的麻烦。
命令行工具的用法大概是这样:
dji_irp -s input.RJPG -o output.tiff -t temperature -p params.txt其中-t temperature表示输出温度矩阵,-p指定参数文件(可以从TSDK的示例里抄一份)。输出的TIFF是32位浮点,单位摄氏度。如果你需要同时输出RGB预览图,加一个-t rgb再跑一次就行。
Python这边,我习惯用rasterio读TIFF,用numpy做矩阵运算,用pyproj做坐标转换。这几个库安装都不复杂:
pip install rasterio numpy pyproj opencv-python提示:
rasterio在Windows上安装有时候会报GDAL相关的错,建议用conda install -c conda-forge rasterio,省心很多。
2.3 Pix4D工程配置的关键参数
Pix4D这边的配置直接决定了几何精度,有几个参数必须手动调:
相机模型:热红外相机的内参和可见光不一样,Pix4D自带的相机库可能没有你的型号。这时候需要手动输入焦距、像元尺寸、主点坐标。这些参数可以从TSDK的参数块里读出来,或者查大疆官方的相机参数文档。
坐标系:建议统一用WGS84 UTM。Pix4D默认可能是WGS84经纬度,但经纬度做像素映射的时候计算量大且容易出精度问题。在项目设置里把输出坐标系改成UTM,带号根据测区经度算。
正射影像分辨率:热红外相机分辨率通常不高(比如H20T是640×512),如果按可见光的分辨率出图,会过度插值导致温度值失真。我一般按原始地面采样距离(GSD)的1到1.5倍来设,比如飞行高度100米时GSD约5厘米,正射就设5到7.5厘米。
输出格式:一定要勾选GeoTIFF,并且保留地理标签。如果只出普通TIFF,后面还得手动配坐标,很麻烦。
2.4 温度映射的数学原理
温度映射的核心是共线方程。简单说,对于正射影像上的每一个像素,我要找到它在原始RJPG上的对应位置,然后从温度矩阵里取值。这个过程分两步:
第一步,正射像素坐标→地面坐标。正射影像本身带有地理变换信息,用rasterio的transform就能把像素坐标转成UTM坐标。
第二步,地面坐标→原始图像坐标。这一步需要知道拍摄时相机的位姿(位置和姿态)。Pix4D在空三完成后会输出一个cameras.txt或者.pix4d工程文件,里面记录了每张照片的曝光点坐标和旋转矩阵。用共线方程反算:
x = -f * (R11*(X-Xs) + R12*(Y-Ys) + R13*(Z-Zs)) / (R31*(X-Xs) + R32*(Y-Ys) + R33*(Z-Zs)) y = -f * (R21*(X-Xs) + R22*(Y-Ys) + R23*(Z-Zs)) / (R31*(X-Xs) + R32*(Y-Ys) + R33*(Z-Zs))其中(X,Y,Z)是地面点坐标,(Xs,Ys,Zs)是相机曝光点坐标,R是旋转矩阵,f是焦距。算出来的(x,y)是像平面坐标,再根据主点和像元尺寸转成像素行列号。
这里有个坑:地面点的高程Z。正射影像每个像素对应的高程来自DSM,所以必须把Pix4D输出的DSM也一起用上。如果只用平均高程,在地形起伏大的区域温度会偏移好几个像素。
注意:共线方程反算出来的像素坐标可能是小数,需要用双线性插值从温度矩阵里取值。直接取整会导致温度出现阶梯状伪影。
3. 实操过程与核心环节实现
3.1 数据准备与批量提取温度矩阵
假设你有一批RJPG文件放在raw/目录下,第一步是批量转成温度TIFF。我写了一个Python脚本调用dji_irp:
import os import subprocess from pathlib import Path raw_dir = Path("raw") temp_dir = Path("temperature") temp_dir.mkdir(exist_ok=True) for rjpg in raw_dir.glob("*.RJPG"): out_tiff = temp_dir / (rjpg.stem + "_temp.tiff") cmd = [ "dji_irp", "-s", str(rjpg), "-o", str(out_tiff), "-t", "temperature" ] subprocess.run(cmd, check=True) print(f"已处理: {rjpg.name}")跑完之后,temperature/目录下就是每张图对应的温度矩阵。用rasterio打开看一眼:
import rasterio import numpy as np with rasterio.open("temperature/DJI_0001_temp.tiff") as src: temp = src.read(1) print(f"温度范围: {temp.min():.2f} ~ {temp.max():.2f} 摄氏度") print(f"矩阵尺寸: {temp.shape}")正常情况温度范围应该在-20到150摄氏度之间(取决于场景)。如果全是0或者全是65535,说明TSDK没读到元数据,检查一下相机固件版本和TSDK版本是否匹配。
3.2 Pix4D空三与正射导出
Pix4D的操作界面比较直观,但有几个地方容易忽略:
导入照片:把RGB预览图(不是RJPG)导入Pix4D。如果你只有RJPG,可以用TSDK再跑一遍-t rgb导出预览图。注意预览图要和温度矩阵一一对应,文件名最好保持一致。
初始化处理:选择“标准”模板,关键点密度设“高”,因为热红外图像纹理弱,关键点少了空三容易失败。如果测区有大量重复纹理(比如光伏板阵列),建议开启“校准”模式,让Pix4D同时优化内参。
点云和DSM:点云密度设“高”,DSM分辨率设和正射一样。这一步比较耗时,但DSM精度直接影响温度映射的准确性。
正射影像:分辨率按前面说的设,勾选“GeoTIFF”和“合并瓦片”。导出后你会得到一个.tif文件和一个.tfw世界文件(或者地理标签直接嵌在TIFF里)。
导出相机参数:Pix4D的工程目录下有个1_initial/params/文件夹,里面有个cameras.txt,记录了每张照片的曝光点坐标和旋转矩阵。格式大概是:
# Camera 0 DJI_0001.JPG Xs Ys Zs R11 R12 R13 R21 R22 R23 R31 R32 R33 f ...这个文件是后续温度映射的关键输入。
3.3 Python实现温度映射
温度映射的代码稍微长一点,我拆成几个函数来写。先读正射影像和DSM:
import rasterio import numpy as np from pyproj import Transformer ortho_path = "pix4d/output/ortho.tif" dsm_path = "pix4d/output/dsm.tif" with rasterio.open(ortho_path) as ortho_src: ortho = ortho_src.read() ortho_transform = ortho_src.transform ortho_crs = ortho_src.crs height, width = ortho.shape[1], ortho.shape[2] with rasterio.open(dsm_path) as dsm_src: dsm = dsm_src.read(1) dsm_transform = dsm_src.transform然后读相机参数:
def read_cameras(camera_file): cameras = [] with open(camera_file, "r") as f: lines = [l.strip() for l in f if l.strip() and not l.startswith("#")] i = 0 while i < len(lines): name = lines[i] xs, ys, zs = map(float, lines[i+1].split()) r1 = list(map(float, lines[i+2].split())) r2 = list(map(float, lines[i+3].split())) r3 = list(map(float, lines[i+4].split())) f = float(lines[i+5]) cameras.append({ "name": name, "position": np.array([xs, ys, zs]), "rotation": np.array([r1, r2, r3]), "focal": f }) i += 6 return cameras核心映射函数:
def map_temperature(ortho_transform, dsm, cameras, temp_matrices, camera_params): result = np.full(dsm.shape, np.nan, dtype=np.float32) for row in range(dsm.shape[0]): for col in range(dsm.shape[1]): # 正射像素 -> UTM坐标 x_utm, y_utm = ortho_transform * (col + 0.5, row + 0.5) z = dsm[row, col] if np.isnan(z): continue # 找最近的相机 best_cam = None best_dist = float("inf") for cam in cameras: dist = np.linalg.norm(cam["position"] - np.array([x_utm, y_utm, z])) if dist < best_dist: best_dist = dist best_cam = cam if best_cam is None: continue # 共线方程反算 X, Y, Z = x_utm, y_utm, z Xs, Ys, Zs = best_cam["position"] R = best_cam["rotation"] f = best_cam["focal"] dx, dy, dz = X - Xs, Y - Ys, Z - Zs denom = R[2,0]*dx + R[2,1]*dy + R[2,2]*dz if abs(denom) < 1e-6: continue x_img = -f * (R[0,0]*dx + R[0,1]*dy + R[0,2]*dz) / denom y_img = -f * (R[1,0]*dx + R[1,1]*dy + R[1,2]*dz) / denom # 像平面坐标 -> 像素坐标 px = x_img / camera_params["pixel_size"] + camera_params["cx"] py = y_img / camera_params["pixel_size"] + camera_params["cy"] # 双线性插值取温度 temp = bilinear_interp(temp_matrices[best_cam["name"]], px, py) result[row, col] = temp return result这段代码是逐像素循环的,速度比较慢。实际工程中我会用numpy向量化或者numba加速,但逻辑是一样的。
3.4 输出带温度信息的GeoTIFF
最后把结果写成GeoTIFF,保留地理参考:
with rasterio.open( "output/temperature_ortho.tif", "w", driver="GTiff", height=result.shape[0], width=result.shape[1], count=1, dtype="float32", crs=ortho_crs, transform=ortho_transform, nodata=np.nan ) as dst: dst.write(result, 1)这样输出的TIFF可以直接在QGIS或者ArcGIS里打开,每个像素值就是摄氏度。你可以用QGIS的“识别要素”工具点一下,就能看到具体温度。
4. 常见问题与排查技巧实录
4.1 温度矩阵全零或异常值
这是最常见的问题,通常有三个原因。第一,TSDK版本太老,不支持你的相机型号。去大疆官网下载最新版TSDK,或者用大疆智图自带的dji_irp。第二,RJPG文件损坏,可能是拷贝过程中断。用certutil -hashfile算一下MD5,和原始文件对比。第三,相机固件和TSDK不匹配,比如H30T需要TSDK 3.0以上版本。
排查步骤:先用TSDK的命令行工具单独处理一张图,看输出是否正常。如果单张正常,批量异常,那就是脚本问题;如果单张也异常,那就是环境问题。
4.2 Pix4D空三失败或精度差
热红外图像纹理弱,空三失败很常见。我的经验是:第一,确保RGB预览图和温度矩阵一一对应,不要混入其他图片。第二,在Pix4D里手动添加连接点,尤其是在纹理贫乏区域(比如水面、沥青路面)。第三,如果测区有RTK数据,一定要导入POS文件,能大幅提升空三稳定性。第四,实在不行就降低关键点密度,用“快速”模式先跑一遍,成功后再用“高”模式精化。
4.3 温度映射偏移
映射偏移通常表现为温度图像和正射影像对不齐,比如光伏板的热斑位置偏了几厘米。原因可能是:第一,DSM和正射影像的分辨率不一致,导致高程采样错位。解决方法是把DSM重采样到和正射一样的分辨率。第二,相机参数文件里的旋转矩阵顺序搞错了,Pix4D输出的是行优先还是列优先,要仔细看文档。第三,坐标系没统一,Pix4D输出的是UTM,TSDK输出的是WGS84,映射前必须用pyproj转换。
4.4 批量处理速度优化
逐像素循环在Python里非常慢,一张640×512的图可能要跑几分钟。我试过几种加速方案:用numpy的meshgrid生成所有像素坐标,一次性算完;用numba的@jit装饰器编译核心循环;或者用multiprocessing多进程并行。实测下来,numba加速比最高,能到50倍以上。如果不想装numba,用numpy向量化也能提速10倍左右。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 温度全零 | TSDK版本不匹配 | 单张测试 | 升级TSDK或固件 |
| 温度范围异常 | 参数文件错误 | 检查发射率、环境温度 | 用TSDK默认参数 |
| 空三失败 | 纹理弱、连接点少 | 查看空三报告 | 手动加连接点、导入POS |
| 映射偏移 | 坐标系不统一 | 对比UTM和WGS84 | 统一用UTM |
| 映射错位 | DSM分辨率不一致 | 检查DSM和正射GSD | 重采样DSM |
| 处理速度慢 | 逐像素循环 | 计时 | 用numba或numpy向量化 |
提示:如果项目时间紧,可以先跑一个小区域验证流程,确认无误后再批量处理。我吃过亏,有一次直接跑了几百张图,结果发现坐标系搞错了,全部重来。
4.6 实操心得与避坑技巧
第一个心得:TSDK的参数文件不要自己编。TSDK安装包里有个sample文件夹,里面有各种相机型号的参数模板,直接抄。自己编的话,发射率、环境温度、距离这些参数稍微错一点,温度就能差好几度。
第二个心得:Pix4D的相机参数文件要备份。空三完成后,1_initial/params/里的文件可能会被后续步骤覆盖。我习惯在空三结束后立刻把整个params文件夹复制一份,后面映射的时候用备份的。
第三个心得:温度映射前先做小区域验证。选一个有明显温度差异的区域(比如光伏板的热斑),手动算几个像素的温度,和TSDK单张提取的结果对比。如果误差在0.5度以内,说明流程没问题;如果误差大,检查共线方程的符号和旋转矩阵。
第四个心得:输出GeoTIFF的时候加个颜色表。虽然温度值是浮点,但QGIS里可以设伪彩色,看起来更直观。我一般用“铁红”或者“彩虹”色带,热斑一目了然。
第五个心得:批量处理的时候加日志。每处理完一张图就写一行日志,记录文件名、温度范围、处理时间。万一中间出错,能快速定位是哪张图的问题。
这个工作流我用了两年多,从最初的手忙脚乱到现在基本能稳定输出,中间踩的坑大多集中在TSDK版本兼容和坐标系转换上。如果你刚开始接触,建议先用一小批数据跑通全流程,再逐步扩大规模。热红外数据工程化处理没有太多玄学,关键是把每个环节的输入输出对齐,剩下的就是耐心调试。