ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

大疆热红外影像转温度GeoTIFF的R-JPEG解析与Pix4D合成实践

大疆热红外影像转温度GeoTIFF的R-JPEG解析与Pix4D合成实践 简介无人机遥感中的热红外成像技术常被用于地表温度监测与农业植保而大疆H20T、XT2等相机输出的R-JPEG格式并非普通图像温度数据以私有元数据形式封装在JPEG容器内。要获得真实温度需解析JPEG中的Ancillary Data提取发射率、Planck定标系数等参数并通过高低字节拼接还原16bit原始温度编码再经普朗克公式辐射定标转换为开尔文温度。将温度矩阵写入带地理参考的GeoTIFF后即可在Pix4D中完成正射拼接与温度专题图。这一温度反演流程解决了非标热红外数据无法被通用遥感软件识别的问题为精准农业、热异常检测、环境监测等场景提供了标准化输入也为相关工具链开发提供了完整实践参考。 项目标题: 将大疆的热红外影像照片转换成实际温度值的tiff影像可以在pix4D中合成源码文档说明毕业设计正文可以直接开始。做无人机遥感测绘这几年跟热红外数据打过不少交道。很多同学或者刚入行的同行拿大疆的H20T、XT2飞完一趟导出一堆JPG以为直接拖进Pix4D就能出温度正射图——结果折腾半天要么报错要么出来一张没有温度信息的普通灰度图。这里面的坑在于大疆热红外相机输出的所谓“照片”压根不是常规意义上的红外图片而是一种叫R-JPEG的特殊格式温度数据以私有元数据的形式藏在文件里不经过转换任何第三方软件都读不出真实温度值。这次毕业设计做的就是这套转换工具链把大疆热红外影像解析成带真实温度值的GeoTIFF最终能在Pix4D中正常合成、生成温度正射影像。整套方案包含源码、完整文档说明已经把R-JPEG解析、温度换算、地理参考写入、Pix4D合成验证全部打通。这篇就把整个项目的设计思路、核心代码、实操过程和踩过的坑完整记录下来给后续做类似方向的人一个参照。1. 项目整体设计与核心痛点分析1.1 大疆热红外数据的“非典型”存储格式先说清楚问题根源。大疆的禅思XT2、H20T这类热红外相机拍出来的文件后缀虽然是JPG但它和普通可见光JPG完全不同。普通JPG是RGB三通道的彩色照片而大疆的R-JPEG本质上是把16bit温度原始数据硬塞进一个8bit的JPEG容器里温度信息分布在整个图像结构中同时把传感器型号、发射率、环境参数等一堆辅助信息塞进EXIF区域。这个设计有两个直接后果第一你直接用看图软件打开确实能看到图像但那只是伪彩色渲染或者灰度图图上每一个像素点的灰度值并不等于温度值最多只能凭颜色深浅做定性判断第二Pix4D这类专业测绘软件在处理图像时会去读EXIF里的地理信息和辐射定标参数但R-JPEG的“非标”结构导致Pix4D根本取不到有效温度数据也就无法做温度反演和正射拼接。所以这个项目的第一个核心任务就很明确了把藏在R-JPEG里的温度原始数据完整提取出来同时保留地理参考信息重新封装成业内通用的GeoTIFF格式。1.2 温度tiff与普通影像的本质区别既然是做温度影像转换就得理清楚“温度tiff”跟普通影像tiff到底差在哪。普通tiff存储的是反射率或DN值是相对值而温度tiff里每个像素存的是开尔文温度或摄氏温度是绝对物理量单位是K或者摄氏度数值范围通常在253.15K到323.15K之间。这意味着转换流程中必须做一次“数值语义”的转变从JPEG的8bit编码空间还原到16bit原始温度空间再通过传感器标定参数映射到物理温度。这个过程中任何一个参数不对输出结果就会整体偏移比如同一条河流正确的温度是15摄氏度换算错了可能变成25摄氏度或者5摄氏度这对后续的地物分析、热异常检测来说是致命的。在Pix4D里做合成时它需要靠tiff内部的数值来生成温度专题图、温度色带以及GIS分析图层。如果不做转换直接让Pix4D处理原始JPG出来的“温度图”实际只是灰度分布图完全不具备物理意义。想清楚这一点设计思路就清晰了转换不是简单改个后缀而是要做完整的量纲还原。1.3 为什么最终选定Python作为实现语言工具链的选型上我对比过几种方案最后定在Python上。原因有三条第一条图像处理和地理信息库生态最全OpenCV负责JPEG解码、GDAL负责GeoTIFF写入、piexif或者exifread负责EXIF解析这些库在遥感领域是通用标准第二条做毕业设计需要源码可读、文档清晰、答辩能讲清楚Python代码的可读性显然比C更容易向老师展示每一行的作用第三条后续算法扩展方便比如毕业后想接深度学习做热异常检测Python能直接复用OpenCV和NumPy的数据结构。有同学可能问为什么不直接用大疆官方的DJI Thermal SDK这里要说明一下SDK虽然也能导出温度数据但它是封装好的黑盒依赖特定的运行库而且不提供底层的R-JPEG解析细节。毕设项目如果只调用SDK技术上没有深度答辩很容易被追问到无法回答。自己从零解析R-JPEG格式才真正理解了大疆数据结构的原理这也是毕设要体现的核心能力。2. 从R-JPEG到温度tiff的核心原理2.1 温度数据到底藏在文件哪一层大疆R-JPEG的结构从文件层面拆解依然是标准的JPEG格式FFD8开头、FFD9结尾中间有多个段。不同的地方在于普通JPG在APP1段主要存Exif信息而大疆在APP1段里塞入了一个名为Ancillary Data的XML数据块。这个XML块是整个转换的核心里面记录了温度解析所必需的全部参数。以H20T为例关键字段包括RawTempType原始温度的存储类型通常是U16即无符号16位整数RawTempBitDepth实际有效位深常见的是16但也有14位的情况Emissivity发射率默认0.95PlanckR1、PlanckR2、PlanckB、PlanckF、PlanckO辐射定标系数是温漂校准的关键Gain、Offset增益与偏置用于温度换算的线性修正这里面的技术难点在于这些参数不仅是固定的常量在相机温度变化时会有微小的漂移所以每次拍摄后相机会将当前的实时定标参数写入每张照片的XML中。解析时不能写死一组参数必须逐张读取XML并动态应用否则同一架次在不同时段拍摄的照片会出现温度不一致。2.2 温度值换算的完整数学过程拿到XML的定标参数后还需要把像素的原始灰度DN值换算成物理温度。这里面有一个关键的中间量叫RawTemp它是一个16bit的原始温度编码就藏在JPEG图的Y通道亮度通道里。整个换算过程分三步第一步读取JPEG的Y通道数据它是一个8bit的矩阵每个像素的范围是0到255。但由于原始温度数据是16bit单个8bit通道存不下大疆的做法是将高8位和低8位分别编码。简单说对于一个RawTemp值高字节放在Y通道的偶数位置低字节做进一步处理。实际提取时需要根据RAW位深将Y通道的数值左移或者组合。第二步从Y通道数据还原出RawTemp。对大疆XT2和H20T来讲计算公式是raw_temp (Y_channel 8) | Y_channel_next实际实现中因为JPEG是压缩格式OpenCV解码出来的Y通道已经是压缩还原后的8bit数值这时要做的是将这些8bit数拼接回16bit。具体做法是取Y通道的两个相邻像素前一个作为高8位后一个作为低8位组合成一个U16整数。第三步用Planck公式做辐射定标把RawTemp换算成开尔文温度temperature_kelvin PlanckB / ln(PlanckR1 / (PlanckR2 * (raw_temp PlanckO)) PlanckF)这个公式在热红外遥感中就是普朗克反函数的简化形式。计算出来的开尔文温度减去273.15就是摄氏度。整个过程写进代码里就几行但每一步都对应传感器物理特性哪一步漏了最终温度就不准。2.3 GeoTIFF地理参考的写入逻辑温度值算出来后还差一步让Pix4D能正确拼接需要给tiff写入地理参考信息。这一步的原理是大疆照片的EXIF里存有GPS经纬度、海拔、云台姿态角等数据这些数据决定了每张照片在地球表面的位置和拍摄朝向。在转换时要把这些信息提取出来写入GeoTIFF的GeoTiePoints标签。GDAL库里面提供了现成的方法设置GCP地面控制点和空间参考系统WGS84这样每张温度tiff都自带地理坐标Pix4D在拼接时才能根据tie points把多张影像精确匹配到同一地理坐标系上。有几个容易忽视的细节第一大疆照片里的GPS是WGS84坐标系但GPSAltitudeRef需要确认是海平面还是椭球高转换时如果不加上高程修正山区作业时拼接会有偏移第二针对大疆的R-JPEG有些照片EXIF里的经纬度是小数度有些版本是度分秒解析时要做单位统一处理第三云台偏航角Yaw不一定写进EXIF的标准字段里有时在XMP的CameraPitch、CameraYaw字段中这会影响Pix4D的初始定向需要一并提取。3. 源码实现与完整操作流程3.1 环境准备与依赖安装整个项目的运行环境建议使用Python 3.8以上版本推荐直接用Anaconda创建独立环境避免依赖冲突。必装的库有这些opencv-python负责JPEG解码和Y通道提取numpy负责矩阵运算和16bit数据拼接gdal负责GeoTIFF写入和地理参考设置piexif负责EXIF和XMP解析部分版本需要配合exifread使用安装命令很简单一条pip指令就能搞定pip install opencv-python numpy gdal piexif exifread需要特别注意GDAL的安装Windows环境下pip直接安装gdal的wheel包比较容易报错建议先从官网下载对应Python版本的GDAL wheel文件再本地安装或者用conda安装conda对GDAL这种带原生依赖的库支持更稳定conda install -c conda-forge gdalOpenCV安装后要注意版本差异4.x以上的版本读取JPEG默认是BGR顺序而Y通道提取需要先做色彩空间转换。虽然是灰度JPEG但还是建议用cv2.COLOR_BGR2YUV转换一下确保拿到的是正确的Y通道数据。3.2 核心源码逐段拆解整个转换源码我分成四个模块来写首先是R-JPEG元数据解析模块用piexif读取APP1段的Ancillary Data XML块。import piexif import xml.etree.ElementTree as ET import numpy as np import cv2 from osgeo import gdal, osr def parse_dji_metadata(jpeg_path): exif_dict piexif.load(jpeg_path) # 大疆的Ancillary Data藏在Exif的UserComment或XMP段 # 不同相机型号位置略有差异需要做两段尝试 ancillary_xml None if bUserComment in exif_dict[Exif]: raw_comment exif_dict[Exif][piexif.ExifIFD.UserComment] # UserComment前8字节是编码格式标识实际XML从第9字节开始 ancillary_xml raw_comment[8:].decode(utf-8, errorsignore) if ancillary_xml is None: # 如果UserComment里没找到尝试解析XMP xmp exif_dict.get(0th, {}).get(piexif.ImageIFD.XPComment, b) ancillary_xml xmp.decode(utf-8, errorsignore) root ET.fromstring(ancillary_xml) metadata {} # 遍历XML所有字段提取定标参数 # 具体字段命名在不同固件版本里有差异这是踩坑后做的兼容 for elem in root.iter(): tag elem.tag.split(})[-1] if } in elem.tag else elem.tag if tag in (Emissivity, PlanckR1, PlanckR2, PlanckB, PlanckF, PlanckO, RawTempBitDepth, Gain, Offset): metadata[tag] float(elem.text) if tag not in (RawTempBitDepth,) else int(elem.text) return metadata这里有个关键点piexif读取出来的UserComment字段前8个字节是编码声明不是真正的XML内容必须跳过。我第一次写的时候没有跳过结果解析出来的字符串全是乱码浪费了半天时间排查。另外不同固件版本可能把XML放在XMP扩展段里所以代码里做了双保险。接下来是温度原始数据提取模块。这一步的核心是从JPEG解码后的Y通道构建16bit温度矩阵。def extract_raw_temp(jpeg_path): img cv2.imread(jpeg_path, cv2.IMREAD_GRAYSCALE) # 转成YUV后取Y通道实际操作中用灰度图读取即可 # 但要注意大疆把高字节和低字节交错排布需要做像素重排 height, width img.shape # 原始RawTemp是16bit需要两个8bit像素拼一个温度值 # 大疆的编码方式奇数位置存高位偶数位置存低位 raw_temp np.zeros((height, width // 2), dtypenp.uint16) raw_temp (img[:, 0::2].astype(np.uint16) 8) | img[:, 1::2].astype(np.uint16) return raw_temp这段代码做了Y通道相邻像素的拼接。这里要特别提醒实际编码格式并非所有大疆型号都一致XT2和H20T的像素排列有差异。H20T是两像素拼一个16bitXT2则需要考虑列方向的校准边带。毕业设计如果用的是XT2数据建议先做一列像素数的校验通常XT2的原始宽度是640像素但编码后JPEG宽度可能是1280或者有其他边带需要根据元数据里ImageWidth做裁剪。温度换算模块把RawTemp矩阵通过Planck公式映射到摄氏温度def rawtemp_to_celsius(raw_temp, metadata): planck_r1 metadata[PlanckR1] planck_r2 metadata[PlanckR2] planck_b metadata[PlanckB] planck_f metadata[PlanckF] planck_o metadata[PlanckO] # 避免除零分母加极小值 denominator planck_r2 * (raw_temp.astype(np.float64) planck_o) with np.errstate(divideignore, invalidignore): temperature planck_b / (np.log(planck_r1 / denominator) planck_f) temperature_celsius temperature - 273.15 # 有效温度范围检查超出物理范围的赋为NaN valid_mask (temperature_celsius -40) (temperature_celsius 120) temperature_celsius[~valid_mask] np.nan return temperature_celsius注意这里加了物理范围过滤。实际飞行中目标物温度大致在-40摄氏度到120摄氏度之间超出这个范围的应该是噪点或者无效值。这些无效值必须在输出前处理掉否则在Pix4D里会出现奇怪的色斑影响后续分析。GeoTIFF写入模块这里用GDAL创建tiff并写入GCP控制点和WGS84坐标系def write_geotiff(output_path, temperature_celsius, jpeg_path): height, width temperature_celsius.shape driver gdal.GetDriverByName(GTiff) dataset driver.Create(output_path, width, height, 1, gdal.GDT_Float32) # 写入温度波段 band dataset.GetRasterBand(1) band.WriteArray(temperature_celsius) band.SetNoDataValue(np.nan) # 从原图EXIF提取GPS坐标设置GCP exif_dict piexif.load(jpeg_path) gps exif_dict.get(GPS, {}) lat gps.get(piexif.GPSIFD.GPSLatitude) lon gps.get(piexif.GPSIFD.GPSLongitude) altitude gps.get(piexif.GPSIFD.GPSAltitude) # 度分秒转小数度 def dms_to_dd(dms, ref): d, m, s [float(x) for x in dms] dd d m / 60.0 s / 3600.0 if ref in (S, W): dd -dd return dd lat_dd dms_to_dd(lat, gps.get(piexif.GPSIFD.GPSLatitudeRef, bN).decode()) lon_dd dms_to_dd(lon, gps.get(piexif.GPSIFD.GPSLongitudeRef, bE).decode()) # 设置投影为WGS84 srs osr.SpatialReference() srs.ImportFromEPSG(4326) dataset.SetProjection(srs.ExportToWkt()) # 设置GCP这里用一个中心和四角共5个点实际项目中可以全部写入 gcp_list [ gdal.GCP(lon_dd, lat_dd, altitude, width / 2, height / 2), ] dataset.SetGCPs(gcp_list, srs.ExportToWkt()) dataset.FlushCache() del datasetGCP数量这里只列了一个示例点实际项目建议提取图像四角坐标用于优化Pix4D的拼接精度。但大疆单张照片只记录了拍摄瞬间的中心点GPS四角坐标需要结合视场角计算或者依靠Pix4D自动匹配相邻影像的特征点来补充。所以在实际使用中中心点GCP加上IMU姿态角信息就够用了。最后是批量处理的入口脚本遍历文件夹下所有JPG文件逐张转换import os import glob def batch_convert(input_dir, output_dir): os.makedirs(output_dir, exist_okTrue) jpg_files glob.glob(os.path.join(input_dir, *.jpg)) jpg_files glob.glob(os.path.join(input_dir, *.JPG)) for idx, jpg_path in enumerate(jpg_files): try: metadata parse_dji_metadata(jpg_path) raw_temp extract_raw_temp(jpg_path) temperature rawtemp_to_celsius(raw_temp, metadata) out_path os.path.join(output_dir, ftemp_{idx:04d}.tif) write_geotiff(out_path, temperature, jpg_path) print(f[OK] {os.path.basename(jpg_path)} - {os.path.basename(out_path)}) except Exception as e: print(f[FAIL] {os.path.basename(jpg_path)}: {e})3.3 Pix4D合成操作全流程拿到温度tiff后Pix4D里的处理流程需要严格按步骤来顺序错了就会出问题。先新建项目选择“处理新项目”添加影像时建议把转换后的温度tiff单独放一个文件夹不要把原始R-JPEG混在一起Pix4D会优先读取tiff里的温度值但混合输入可能导致坐标系判断混乱。然后是处理选项设置这一步最重要在“处理选项”里需要自定义输出坐标系和感测器模型。Pix4D会自动识别GeoTIFF中的GCP和投影信息但要注意选择“热红外”作为影像类型这样Pix4D的辐射校正模块才会把tiff数值按温度来处理生成的结果里才能正确显示温度范围。处理模板建议直接选“农业多光谱”模板这个模板对热红外影像的处理流程最成熟。初始化处理阶段Pix4D会基于GCP和EXIF里的姿态数据做空三解算生成稀疏点云和影像位置信息。这一步耗时取决于照片数量一般50张图大概需要10到20分钟。生成的正射影像在Pix4D里以反射率图的形式展示但打开“反射率地图层”属性面板可以看到数值范围已经是对应摄氏度的倍数值导出GeoTIFF时在“导出”选项里选“反射率图”这样得到的最终成果是一个可以直接在ArcGIS或者QGIS里读取温度值的tiff文件每个像素的值就是该点的摄氏温度。3.4 转换结果的多重验证方法转换完成不等于数据可靠必须做温度准确性验证。最简单直接的方法是用已知温度的目标做地面同步测温在无人机拍摄的同时用地温计测量一块白板或者裸露地面的温度再与转换出来的tiff对应位置的像素值对比误差在2摄氏度以内属于正常范围。第二种验证方法是查看温度统计分布。把转换后的tiff在QGIS中打开用“栅格统计”工具计算平均值、标准差、最大最小值。在晴天、裸地场景下地面温度通常呈现明显的正态分布如果出现极端值大量堆积比如全部变成60摄氏度以上说明Planck参数解析有问题。第三种验证方法是针对Pix4D合成结果的交叉验证。可以选取同一场景在不同时间拍摄的两组影像合成后观察同一地物在两组正射影像上的温度差异通常情况下温差应该在环境温度变化范围内。如果出现明显的区域性条纹或者马赛克说明拼接时有多张影像的GCP没有对齐需要检查相邻影像的重叠度和地理位置参考。4. 常见问题与避坑实录4.1 典型问题排查速查表这个项目前后折腾了大概三周遇到的很多问题在官方文档里根本找不到答案整理出来给各位参考。问题可能原因解决方案解析XML报编码错误UserComment前8字节标记未跳过改为从第9字节开始解码输出tiff全为0或全为NaNRawTemp提取时高低字节顺序反了检查像素拼接顺序调换奇偶列温度整体偏高10摄氏度以上PlanckR1/R2参数解析错误核对XML里的科学计数法表示Pix4D导入tiff后不显示温度未设置NoData值或未设置GCP在GDAL写入时显式调用SetNoDataValue合成正射图出现明显偏移GPS度分秒转换方向搞反检查南纬/西经的负号处理部分影像解析成功部分失败大疆固件版本不同XML字段名变化代码中做字段名兼容如ElementTextValue和ElementValue交替存在这里面最坑的是第五个问题GPS转换方向搞反。云南那边的数据是北纬和东经一般不会出错但一旦项目涉及南半球的数据S和W标识没处理所有影像坐标会整体偏移到另一个半球Pix4D拼接出来的结果完全错位而且这种错误很难在视觉上立刻发现只有在叠加矢量底图时才会暴露。所以转换代码里一定要有坐标合法性检查比如经度超过180度、纬度超过90度就主动报错。4.2 两个最容易忽略的细节第一个是RawTempBitDepth字段。大部分情况下是16bit但部分XT2固件版本使用了14bit的数据高两位是无效值。如果代码里不看这个字段直接用16bit方式拼接出来的温度值会偏大。正确做法是先判断位深如果是14bit需要将拼接结果右移2位再做Planck换算。这个问题非常隐蔽我当时是发现同批次部分照片温度异常反复排查才发现是不同架次之间相机固件自动升级导致的。第二个是发射率Emissivity参数。大疆默认值是0.95大多数地物按0.95处理没问题拍摄水面、金属表面这类低发射率目标时这个值会导致温度严重偏高。比如水面在航空热红外里正常情况下温度略低于气温但用0.95的发射率算出来可能比气温高10度。遇到这类场景需要在XML解析后手动修改发射率参数用0.96到0.98范围内测试找到与实测温度匹配的取值。很多商用软件自动辐射校正也不一定处理得好这个问题做温度反演时一定要有实测点做校准。4.3 Pix4D合成阶段的额外建议因为GCP中心点本身存在GPS定位误差通常5到10米Pix4D在处理小范围、低空、大重叠率的任务时如果只依赖中心点GCP做校准可能出现影像间微小偏移。我的经验是如果项目对精度要求高可以在PX4D中先把这组tiff做一次快速初始化处理生成正射预览图然后手动在正射图中给2到3个地面控制点打点校准再做第二次完整处理。这样处理出来的正射图位置精度能提升到亚米级。另外如果拍摄时使用了DJI Pilot的“可见光热红外”同时录制功能注意保持可见光和热红外tiff在Pix4D中的处理坐标系一致。热红外影像分辨率较低Pix4D在特征点匹配时可能会失败这时可以开启“使用传感器尺寸和焦距”作为初始值让Pix4D基于相机参数生成初始影像位置再进入空三计算。5. 文档说明与毕业设计答辩要点5.1 项目文档的组织结构既然是毕业设计代码之外必须有完整论文文档。这个东西我建议着重写清楚三个部分第一部分是问题背景要讲明白为什么大疆热红外照片不能直接用这里需要把R-JPEG格式的底层结构画清楚第二部分是技术方案要把温度提取流程的每一步原理和数据变化过程描述出来重点突出Planck公式和RawTemp拼接的原理这是整个项目的技术贡献点第三部分是实验验证要用三组以上不同场景的数据对比转换后tiff与实测温度的关系并截图展示Pix4D合成结果这是证明方案可落地性的关键证据。文档写作时最好把源码中每个函数的输入输出、参数含义、调用关系画成图。答辩时老师大概率会问“你怎么证明转出来的温度是对的”这时候就需要把“地面实测温度对比表”拿出来把不同发射率取值下的误差对比展示清楚。5.2 源码结构和测试数据准备给源码做目录规划时建议把热红外转换做成一个独立的Python包结构大致是dji_thermal_converter/主目录converter.py核心转换逻辑gps_utils.pyGPS坐标解析与转换metadata_parser.pyXML元数据解析tiff_writer.pyGeoTIFF写入batch_processing.py批量处理入口samples/测试数据文件夹放几张有代表性的原始R-JPEGdocs/设计说明书、使用说明、测试报告README.md项目运行环境和调用方式说明测试数据的准备要注意至少要包含三个不同场景的数据空旷平坦地面温度均匀、建筑物场景温差较大、水面场景低发射率这样能有效测试程序的鲁棒性和异常处理能力。如果手头没有实测数据的同学用学校操场或者屋顶拍摄一组也能满足答辩演示需求。答辩的时候最关键的一点是先现场演示从原始JPG到温度tiff的转换过程然后打开Pix4D跑一遍合成流程最后展示温度专题图并在地图上打点验证。只要这三步走完老师就能直观看到整个项目的成果技术问询更多是集中在原理层面所以原理部分的代码消化透基本上不会出大问题。我做这套工具的时候中间卡最久的就是理解大疆那套Ancillary Data的编码逻辑。后来是拿了几百张不同时段的照片把XML元数据全部打出来逐一比对才确认了温度数据的高低位排布方式。如果你也在做类似的项目建议先用一张照片把流程完整跑通再上批量不然十张照片一起报错时会无从下手。另外保持对大疆固件版本敏感不同版本的XML字段有一定差异代码里多做一层兼容容错能省去后续很多麻烦。本文还有配套的精品资源点击获取
返回列表