
1. 热红外数据工程化处理的整体设计思路1.1 为什么需要工程化处理大疆行业无人机挂载热红外相机如Zenmuse H20T、XT2、H30T系列执行巡检任务后拿到手的原始素材是一堆后缀为.jpg的文件。但如果你直接把这些文件拖进Pix4D或者大疆智图会发现出来的正射影像只有颜色信息没有温度数值。原因很简单这些文件不是普通JPEG而是RJPGRadiometric JPEG也就是辐射测量JPEG。它在普通JPEG的像素数据之外额外嵌入了一段元数据记录了每个像素对应的原始辐射值、发射率、环境温度、距离、湿度等参数。问题在于Pix4D这类摄影测量软件在空三和正射拼接阶段会把RJPG当作普通图片处理元数据在重采样、匀色、拼接过程中被丢弃。最终输出的正射影像虽然看起来有热红外色调但每个像素的DN值已经和真实温度脱钩了。这就是为什么必须做工程化处理——把温度信息从单张RJPG中提取出来再通过摄影测量解算出的几何关系映射到正射影像的每个像素上。我最早接触这个需求是在做光伏电站组件热斑巡检的时候。客户要的不是一张花花绿绿的热图而是一张能直接读出每块组件温度值的正射影像方便他们导入GIS系统做缺陷定位和发电量损失评估。那时候市面上没有现成的傻瓜工具只能自己搭工作流。踩了几轮坑之后总结出一套相对稳定的方案大疆TSDK负责温度提取Pix4D负责几何解算Python负责数据融合。1.2 技术路线选型的考量市面上做热红外正射的路线不止一条我简单对比一下常见的几种方案温度提取几何解算优点缺点大疆智图TSDKTSDK大疆智图一体化操作简单智图对热红外支持有限大场景吃力Pix4DTSDKTSDKPix4D几何精度高支持大场景需要自己写融合代码FLIR ToolsAgisoftFLIR SDKMetashape温度提取成熟FLIR SDK授权费用高纯Python方案自己解析OpenCV免费几何解算精度差不适合工程我最终选择TSDKPix4DPython这条路线核心原因有三个。第一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用的是UTMTSDK输出的是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_irpimport os import subprocess from pathlib import Path raw_dir Path(raw) temp_dir Path(temperature) temp_dir.mkdir(exist_okTrue) 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, checkTrue) 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实现温度映射温度映射的代码稍微长一点我拆成几个函数来写。先读正射影像和DSMimport 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[i1].split()) r1 list(map(float, lines[i2].split())) r2 list(map(float, lines[i3].split())) r3 list(map(float, lines[i4].split())) f float(lines[i5]) 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, dtypenp.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, driverGTiff, heightresult.shape[0], widthresult.shape[1], count1, dtypefloat32, crsortho_crs, transformortho_transform, nodatanp.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输出的是UTMTSDK输出的是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版本兼容和坐标系转换上。如果你刚开始接触建议先用一小批数据跑通全流程再逐步扩大规模。热红外数据工程化处理没有太多玄学关键是把每个环节的输入输出对齐剩下的就是耐心调试。