
1. 为什么要自己处理R_JPEG——项目背景与整体思路做无人机热红外数据处理这一行早晚会撞上R_JPEG这个格式。大疆的禅思X系列比如XT2、H20T以及部分带热红外模块的机型拍出来的照片默认后缀就是.jpg看着和普通照片差不多但它内部藏着的根本不是普通图像数据——这是一张带辐射信息的JPEG也就是Radiometric JPEG行业内叫R_JPEG。如果你只是双击打开看一眼它就是一张灰度图或者伪彩色图顶多能目视判断哪里热哪里冷但想提取某个像素的绝对温度、想生成科研或者工程上可用的温度TIF光靠看图是绝对不行的。我最早接手这个需求是给一个光伏电站做组件热斑巡检。飞机飞完一个方阵几百张R_JPEG拿回来甲方张嘴就要“带温度坐标的TIF并且能拼成一整张电站热力图”。那会儿我也以为直接用DJI Terra或者Pix4D导一下就行实际上手才发现无人机自带软件能出jpg马赛克但精度不够通用GIS软件很多只认普通光学影像根本不认R_JPEG里的温度元数据。最后只能自己写流程从TSDKDJI Thermal SDK解算原始温度开始一步步转到16位TIF再做配准拼接。整套流程跑通之后不光光伏组件连变电站设备测温、建筑外墙空鼓检测、动物监测这些项目都能复用了。这篇文章就把这套流程掰开揉碎讲清楚R_JPEG是什么、温度TIF的数字本质是什么、怎么基于TSDK做批量转换、多张影像怎么拼成一张带地理坐标的完整热力图。文章里涉及到的代码思路和参数含义都是我实际踩过坑之后整理出来的适合做无人机应用开发、遥感数据处理、测绘地信相关工作的朋友参考。哪怕你完全不懂热红外原理只要照着这套逻辑走也能把R_JPEG变成可分析的TIF。1.1 大疆热红外数据到底长什么样先说硬件侧。大疆的热红外传感器最常见的输出有几种RAW一般只有通过SDK才能直接拉流、R_JPEG带辐射信息的静态照片、以及普通JPEG纯视觉图输出给用户预览用。这里最容易混淆的是后面两个普通JPEG和R_JPEG从文件后缀上看都是.jpg但R_JPEG文件的体积通常比同等尺寸的普通JPEG大一些因为它除了包含一帧可视化的图像数据之外还塞进了完整的温度辐射矩阵、相机型号参数、传感器响应曲线、发射率设置、环境温度、反射温度等一系列和辐射定标相关的元数据。很多人下载照片之后习惯用系统自带图片查看器打开看到一张灰白色图片就觉得“这不就是灰度图嘛我自己用OpenCV转一下存成TIF不就完了吗”。这个误区特别要命。你看到的那张灰度图像素亮度只是“伪温度”的可视化映射并不是真实的辐射值。如果你直接把灰度值拿去当温度用误差可能高达十几度甚至几十度。真正可用的温度数据是以二进制形式藏在R_JPEG内部的需要专门的方法才能解算出来。1.2 R_JPEG和普通JPEG的根本区别一句话总结普通JPEG只有“能看到什么”的信息R_JPEG多了一层“每个像素对应多少温度”的信息。这套“温度信息”不是简单地在EXIF里写了中心点温度、最高温最低温那几个数字而是完整的二维辐射矩阵。矩阵里的数值通常被称为DN值Digital Number代表传感器输出的原始量化值。这个DN值和物理温度之间存在一个近似线性的映射关系而映射系数由相机本身的辐射定标结果决定还受镜头透过率、发射率参数、环境温度和距离等因素影响。大疆TSDK的作用就是帮我们从R_JPEG里把这段原始辐射矩阵读出来再结合元数据里的标定参数换算成我们熟悉的摄氏度或者开尔文温度。所以TSDK不是用来“打开图片”的它的核心价值是“解码辐射信息”。换句话说只要你拿到R_JPEG且没有在拍摄时把发射率等参数设置错理论上你随时可以通过TSDK重新计算温度数据这和拿温度计现场测一次没有本质区别。1.3 整体处理流程的五个阶段从R_JPEG到拼接后的温度TIF我把整个流程拆成五个阶段每一步都有明确输入和输出阶段一原始数据整理。把飞机存储卡里的R_JPEG按架次、航带、时间归好类剔除起飞降落阶段拍摄的废片。阶段二辐射解算。用TSDK读取每张R_JPEG的原始辐射矩阵按温度换算公式生成16位整数TIF像素值对应开尔文温度乘以缩放系数。阶段三坐标写入。读取R_JPEG自带的GPS/IMU信息或者配合POS数据、控制点在生成的TIF里写入地理参考信息让每个像素都有实际坐标。阶段四多图配准与拼接。根据飞行航带的重叠度、POS位置信息把多张TIF拼合成一张完整的测区温度影像。阶段五结果质检与输出。检查整体温度分布是否合理、拼接缝是否明显、坐标是否准确最后按需求输出成整幅或分幅TIF。这套流程的主干全在这了。后面我按这个主干逐步展开每一步都有可以直接抄作业的细节。2. 温度TIF的底层逻辑从DN值到开尔文温度的数学关系在动手写代码之前必须先搞懂温度TIF的数值本质。否则你会陷入“明明代码跑通了但怎么算出来的温度不对”的困境。2.1 R_JPEG文件里到底藏了什么R_JPEG本质上是标准的JPEG容器但里面附加了XMP/EXIF扩展信息。其中和温度高度相关的是DJI自定义的XMP字段比如DJIR_Radiometry。这一大段XML结构里包含了大量关键参数相机型号、传感器序列号、定标时间、镜头参数、发射率、反射温度、大气温度、相对湿度、距离、原始图像宽高、数据位深等。这些字段不仅是记录它们直接就参与温度解算一个都不能省。TSDK内部会解析这些XMP字段并把原始辐射矩阵暴露出来。以禅思XT2为例DC档辐射JPEG的输出位深通常是16位所以单帧数据量就比8位灰度图大一倍。原始辐射矩阵的存储方式还有两种一种是浮点型辐射值另一种是无符号短整型DN值。TSDK返回的类型不同后续换算公式也不同。2.2 温度换算公式与参数确定温度换算的核心概念是“辐射定标”。传感器读到的DN值和物体真实温度之间的关系一般可以写成温度(K) DN值 × 缩放系数 偏移量以大疆常见机型的TSDK回调结果为例很多型号的缩放系数是0.04也就是每个DN值代表0.04K。如果把温度存成整数TIF为了保留小数精度业界常用的做法是先把温度转换成开尔文再乘以100结果作为16位整数保存。这样存储的温度精度可以达到0.01K而数值范围完全落在16位无符号整数0-65535的安全区间内。举个例子如果某像素的原始DN值是8250缩放系数取0.04那么该像素的开尔文温度是8250×0.04330K换算成摄氏度就是330-273.15≈56.85℃。如果我们要把它存成“温度TIF”存储值就是33000读取时除以100得到330K再减去273.15就是56.85℃。这套编码规则在国际上很多热红外数据处理软件里都是通用惯例DJI Thermal SDK生成的16位TIF也遵循类似的逻辑。需要特别提醒不同型号、不同版本的SDK缩放系数不一定都是0.04甚至同一个相机在不同温度量程下的系数也可能不同。所以我强烈建议你在批量处理前先拿一帧R_JPEG用官方命令行工具或SDK自带demo解算一次把输出的温度值和相机屏幕上显示的温度做个对照确认系数没问题再跑批量。2.3 为什么要输出成16位TIF这一步是很多人想不通的明明8位灰度也能存温度数据为什么非要16位TIF说白了就两个字精度和通用性。如果直接把摄氏度值四舍五入存成8位整数0.5℃级别的温差在存储层面就消失了那还做什么热分析而16位TIF不仅能存下完整的温度精度还能让后续的影像处理软件QGIS、ArcGIS、Global Mapper、ENVI等直接读取、采样、对比。更重要的是16位TIF配合地理参考之后可以被当作标准遥感栅格数据交给算法模型训练、做时序变化检测。8位色深在这种场景下是完全没法用的。另外TIF格式对GeoTIFF的支持也很成熟可以把坐标参考信息直接嵌入到文件里。这一点对拼接环节至关重要。3. 基于TSDK的批量转换从R_JPEG到温度TIF理论说清楚之后直接上实操。这一步是所有后续环节的基础也是我调试次数最多的地方。3.1 环境准备与SDK选型TSDK官方提供C和Java两种SDK形态同时官方也发布了命令行工具比如dji_thermal_tool对不熟悉C的Python用户特别友好。我的建议是如果你只是做数据后处理优先用官方命令行工具或者封装好的库别一上来就看C源码。因为你真正需要的是“解算辐射矩阵”这个核心能力而不是自己再去实现一遍SDK的底层协议。我个人常用的组合是Windows环境 DJI Thermal SDK 1.5及以上版本 Python 3.8 GDAL处理TIF地理参考 NumPy数组运算。其中GDAL的安装建议用conda或者预编译的wheel包否则Windows下编译会让人怀疑人生。还需要注意TSDK运行时会校验文件大小和结构有的老版本对超大分辨率比如H20T的640×512以后变体支持不完整遇到读不出来的情况优先检查SDK版本是否匹配相机固件。3.2 单张转换的核心代码先实现最基础的单张R_JPEG转温度TIF。以命令行工具为例标准解算命令大致长这样dji_thermal_tool -a input.RJPEG -o output_thermal.raw -t 2 -d 0其中-t 2表示输出16位温度原始数据-d 0表示不输出调色板伪彩色图。生成的output_thermal.raw是二进制裸数据没有头文件宽高需要从XMP信息里读取。用Python读进NumPy数组并转成温度TIF核心代码如下import numpy as np from osgeo import gdal, osr width, height 640, 512 # 实际值请从XMP中读取 with open(output_thermal.raw, rb) as f: data np.fromfile(f, dtypeu2, countwidth * height) data data.reshape((height, width)) # 如果SDK输出的是开尔文温度×100直接用 temp_kelvin_x100 data.astype(np.float32) # 如果SDK输出的是DN值需要乘缩放系数再乘100 # temp_kelvin_x100 data * 0.04 * 100 # 创建16位TIF driver gdal.GetDriverByName(GTiff) out_tif driver.Create(output_temperature.tif, width, height, 1, gdal.GDT_UInt16) out_tif.GetRasterBand(1).WriteArray(temp_kelvin_x100.astype(np.uint16)) out_tif.GetRasterBand(1).SetDescription(Kelvin x100) out_tif None这段代码里有个关键点温度TIF和原始灰度TIF看起来很像但数值含义完全不同。你保存的是“开尔文温度×100”而不是“DN值”。这也是文件命名时要注明“temperature”的原因否则三个月后连自己都容易混淆。3.3 批量处理与目录组织拿到单张成功的经验之后批量处理就很简单了但有几个坑一定要提前避掉。第一图片路径不能有中文或特殊空格。有的TSDK版本在解析含中文路径时会直接卡死或者解算出来的全图都是同一个温度排查起来非常隐蔽。第二原始R_JPEG和输出TIF务必分目录保存。我的习惯是建立一个工作目录结构如下thermal_project/ ├── raw/ # 原始R_JPEG ├── tif_temperature/ # 温度TIF ├── tif_georef/ # 带地理参考的温度TIF └── mosaic/ # 拼接结果批量处理的时候可以写一个简单的循环调用命令行工具处理单张再用Python统一读取raw、写TIF。实测下来用TSDK命令行处理一张640×512的R_JPEG大约耗时0.1秒批量几百张几分钟就能跑完瓶颈基本不在解算而在磁盘IO。写地理参考的代码并不复杂主要是把影像的GPS位置和旋转角整理成坐标变换信息。这里列出核心思路from osgeo import gdal, osr # 通过EXIF/XMP获取无人机位置和姿态信息 # 简化示例直接手工指定仿射变换参数 lon_origin, lat_origin 121.123456, 31.654321 # 影像左上角坐标 pixel_size 0.00001 # 约1米级分辨率按实际情况调整 srs osr.SpatialReference() srs.ImportFromEPSG(4326) ds gdal.Open(output_temperature.tif, gdal.GA_Update) ds.SetGeoTransform([lon_origin, pixel_size, 0, lat_origin, 0, -pixel_size]) ds.SetProjection(srs.ExportToWkt()) ds None这一步做完之后前缀tif_temperature/的裸温度图就升级成了带坐标的tif_georef/文件可以进GIS软件直接叠加了。需要注意的是精确的仿射变换参数应该根据每个相机的内参、安装角度、POS数据结合光束平差计算我这里给的是简化示例适用于初步预览和拼前检查如果要做高精度成果还是建议用专业航测软件或自己实现畸变校正。4. 多张热红外影像的拼接从单张到整个测区单张温度TIF做好之后最核心也最容易翻车的就是多张影像拼接。热红外影像不像可见光那样纹理丰富它的特征点少、对比度低纯靠特征匹配很容易拼出鬼影和错位。所以热红外拼接的正路不是“找特征点”而是“用空间位置”。4.1 拼接前的几何准备先做一个关键判断你的飞行数据是正规航测数据有完整的POS、航线重叠率大于60%还是随手飞的手持/单张数据如果是正规航测数据后续拼接建议直接用支持热红外数据的专业摄影测量软件或者用热红外专用正射流程。关键是要把每张R_JPEG的辐射信息正确带入而不是只做可见光的马尾辫。很多人在软件里导入R_JPEG后生成的是RGB伪彩色图什么温度信息都没留下这一步要格外注意。如果是手持或用无人机手动拍摄的零散照片没有严格的POS信息那么只能靠图像配准。我的做法是先用GPS粗略确定每张图的大致位置再用OpenCV的特征匹配对重叠区做精细化对齐。热红外图的特征点确实少但边缘、地物轮廓等结构信息还是能用的。常用特征检测器里ORB在低纹理图上表现优于SIFT速度快、匹配稳定性也够用实际项目中我把ORB作为默认首选。4.2 基于空间参考的拼接流程当每张TIF都有地理参考时拼接可以采用“地图投影聚合”的思路这个思路和在线地图切片拼接或者无人机视频拼全景的思路是相通的把每张影像按照自己的地理范围“放置”到输出画布上重叠区按规则融合。使用GDAL直接实现这个拼接逻辑核心命令如下gdalbuildvrt mosaic.vrt tif_georef/*.tif gdal_translate --config GDAL_NUM_THREADS ALL_CPUS \ -co COMPRESSLZW -co TILEDYES \ -ot UInt16 -r cubicspline mosaic.vrt mosaic_final.tif第一行先把所有带坐标的温度TIF挂到一个虚拟目录文件VRT下这一步不产生实际重采样速度非常快。第二行才是真正执行重采样和输出的步骤。这里的 -ot UInt16 保证输出依然是16位-r cubicspline 用三次样条插值让温度过渡更平滑。如果你非要走OpenCV手工拼接路线比如没有POS数据思路是先做两两配准得到单应矩阵再将所有的单应矩阵串联最后投影到公共画布。但我要明确说在没有POS的情况下拼大场景热红外图成功率不稳定。所以能飞航线的尽量飞航线能拿SDK端实时里程计的就别省这一步。4.3 无缝融合与色调均衡热红外拼接最突出问题就是接缝。由于传感器性能、拍摄角度、环境温度变化的影响相邻两张影像在重叠区的温度值会出现肉眼可辨的跳变如果不处理拼出来的图就像打满补丁的彩色毯子绝对没法交付。接缝处理通常分两步几何上做羽化feathering辐射上做增益归一化。羽化的效果是让重叠区从一张图到另一张图渐变而不是硬切辐射归一化是统计重叠区两张图的均值差然后把差异按渐变权重补偿到其中一张图上。代码层面可以直接用GDAL的-blend选项或者专业镶嵌软件的“直方图匹配”功能。还有一个特别容易忽略的事件热红外数据在航带边缘会有严重的畸变和混合像元导致边缘温度明显偏低。如果你带着这类边缘数据去融合整片的均温会被拉低。我的做法是在拼接前先裁剪每张图靠边缘的5%-10%像素只保留可靠度高的中心区域参与拼接。5. 常见问题与排查技巧实录这部分全部来自我实际项目里的排查记录建议收藏备用。5.1 转换后温度明显偏低或偏高排查顺序先查发射率设置再查SDK缩放系数最后查是否把原始灰度当成了温度。发射率设置错误是最常见的尤其是拍摄金属表面或者水面时默认的0.95发射率完全不对金属抛光面发射率只有0.1左右实际温度可能被低估一半以上。取温度时要用TSDK读参数不要看相机屏幕上的“伪温度”。5.2 EXIF信息缺失或R_JPEG无法识别多半是文件被传输软件“压过”或者被改了文件名。大疆R_JPEG的文件头对完整性很敏感有些网盘、微信传文件后会把私有字段剥离掉导致TSDK读不出辐射信息。排查方法是打开文件二进制头看一下有没有JPEG标准头之外的数据段。另外SDK read接口在文件名后缀改成大写.JPG或小写.jpg时行为也可能不同统一小写.jpg最稳。5.3 拼接后出现接缝、重影怎么办先看几何是否正确如果重影方向始终一致说明POS方位角偏差、相机安装角没校正如果重影只在特定区域可能是单张畸变校正没做。处理顺序先手工选择控制点做一次全局平差再做逐图畸变校正最后再拼。这一步不能上来就套自动融合要一步一步来。5.4 导出TIF分辨率怎么选这个问题的答案完全取决于应用场景。如果只是看全局温度分布像素分辨率设为原始GSD的2倍也够用如果要统计小目标比如光伏组件的热斑必须保持原始GSD甚至超采样。但注意超采样不会增加真实信息只是插值平滑反而会掩盖细小的温度异常。我的经验是16位温度TIF的物理意义比分辨率重要得多宁可分辨率低一点、像素值保真也不要为了美观做过度插值。这和Global Mapper导出TIF时选分辨率是同一个套路先明确目标再反推输出分辨率。遇到上面没法解决的问题尤其输出TIF以后再各种编辑软件里打开全黑或全白多半是拉伸显示问题。16位TIF在普通看图软件里会因为像素范围没自动拉伸而显示成全黑这不代表数据坏了用GIS软件设置“拉伸到当前数据集统计值”即可。下面整理了一份排查速查表平时可以直接对着查症状可能原因处理方式温度普遍偏低发射率设置过低按实际材质修正发射率重新解算温度普遍偏高反射温度或环境温度参数异常核对XMP中反射温度用遮罩法测真实值全图一个温度TSDK未解析出辐射矩阵更新SDK版本检查文件名/路径TIF显示全黑16位显示范围问题在GIS中设置拉伸统计值拼接后明显色差两张图辐射基准不一致先做直方图匹配和均值对齐重影明显POS角度或畸变参数异常手动控制点平差逐图校正边缘温度偏低边缘混合像元严重拼接前裁剪边缘像素6. 从温度TIF到温度分析还能往后做点什么拿到拼接好的温度TIF之后很多人以为工作就结束了。其实这时才刚进入有意思的阶段。16位温度TIF可以直接做统计分析比如统计测区最高温、最低温、平均温、温度标准差也可以做阈值分割把超过设定温度的区域自动提取出来。TIF文件做温度Temperature分析的关键就是准确理解“存储值除以100再减去273.15才是摄氏度”这一点否则后面所有统计都是自欺欺人。举个例子在光伏热斑检测里我会以正常组件平均温度为基准设定温差阈值比如高出15℃来提取热斑区域再用连通域分析输出每个热斑的位置、面积和最高温度。这套流程的输入端就是拼接后的整幅TIF。用GDAL打开转成NumPy数组一行np.where(temp_c threshold)就完成了。如果你有同区域的点云数据比如无人机激光雷达还可以把点云的温度属性投影成TIF栅格原理类似点云转高程DEM只是把高度字段换成温度字段这样就能做三维温度分布分析了。最后分享一个我的个人习惯每次跑完批处理先用官方工具解算一张已知温度图像做校验再把拼接结果和现场温度计实测点作对比两边误差如果超过2℃一定回去查发射率或解算系数绝不轻易交付成果。这套习惯帮我挡掉了很多返工也是这个流程里最值得保留的一环。