尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

大疆热红外影像温度转换:R-JPEG解析与GeoTIFF生成实战

大疆热红外影像温度转换:R-JPEG解析与GeoTIFF生成实战 简介热红外遥感中获取真实的温度信息是后续分析与应用的前提。无人机搭载的热红外相机如大疆禅思XT2、H20T虽然输出JPG图像但底层实际是复合的R-JPEG格式其中嵌入了16位原始热辐射数据与XMP元数据。若不解析这些数据仅凭可见的JPG无法进行精准的温度反演。本文从辐射定标原理出发解析大疆私有R-JPEG文件结构说明如何从XMP中提取增益、偏移等标定参数进而将DN值换算为开尔文或摄氏温度同时结合地理坐标转换生成带投影信息的GeoTIFF影像使其能够无缝接入Pix4D等航测软件进行温度正射图的拼合。这一流程适用于毕业设计、科研验证及工程应用帮助开发者掌握从原始影像到温度产品的完整技术链路。 做毕业设计选了“大疆热红外影像温度转换”这个题第一眼看上去感觉就是把JPG换成tif结果动手才发现大疆的热红外照片压根不是普通JPG而是一个藏了不少私货的复合文件。表面是JPEG里面埋了16位的原始热辐射数据还有一堆XML元数据。你要是不把这些数据抠出来做温度换算Pix4D拿到的就是一张“好看的图”而不是“有温度的图”。我做的这个项目正好就是把这个问题完整走通解析大疆热红外影像的私有格式提取每张图的温度标定参数把像素值换算成实际温度再写成一幅带地理坐标的GeoTIFF影像最后扔进Pix4D里正常合成温度正射图。整个过程包括R-JPEG格式解析、XMP参数提取、温度公式换算、UTM坐标计算、批量转换以及配套的源码和文档说明。这篇文章就把整个实现思路和关键代码拆开讲不管是做毕业设计还是自己接项目搞热红外数据处理都可以照着复现。1. 项目整体设计与思路拆解1.1 需求侧大疆热红外照片为什么不能直接用先聊一个很实际的问题大疆无人机比如禅思XT2、H20T拍出来的热红外照片在电脑上双击打开确实是一张正常的JPG图肉眼看是可见光画面或者伪彩色热图。但如果你拿这个JPG去Pix4D做拼接或者做温度分析结果基本是废的。原因在于热红外成像的底层数据流。热红外相机感光元件输出的是16位数字信号DN值Digital Number这个值和物体的实际温度存在一个线性关系。大疆把这种16位原始数据塞进了JPEG文件的附加段里同时把标定参数增益、偏移、发射率、环境参数写进了XMP元数据。普通看图软件读取JPEG主图然后丢弃附加数据所以你只能看到一张“渲染过的图”拿不到真实温度。Pix4D做温度正射图时需要的是每一张影像都带温度值最好还是单波段的浮点型tiff这样它才能把温度数据当作“辐射信息”去参与合成。直接拿大疆的R-JPEG给Pix4D很多版本会直接提示不支持或者拼接出来的图不仅没温度连位置都是乱的。这个毕业设计的核心需求总结下来就三句话一是把大疆照片里的16位原始热数据无损提取出来二是结合XMP里的标定参数把DN值换算成真实温度值三是输出Pix4D能直接吃的带地理坐标的GeoTIFF。1.2 方案选型为什么自己写解析而不是用SDK做这个题目之前我也研究过两条现成的路。第一条是用DJI Thermal SDK。大疆官方提供了一套热红外SDK可以读R-JPEG也能输出温度矩阵。但问题是这个SDK依赖特定运行库跨平台编译有点折磨人而且对于毕业设计来说SDK把底层解析全封装了你最后连论文里“核心算法实现”这一章都没东西可写。更直接的痛点是SDK的版本和机型绑定换一台相机可能就要换库。第二条路是用常用的遥感工具直接转换比如Flir Tools、Thermovision等。这些工具对FLIR相机的支持很好但大疆的R-JPEG格式和FLIR并不完全一致很多工具要么读不出原始数据要么读出来坐标信息是错的。试了一圈还是得回到“自己解析”这条路上。所以这个项目最终选了Python GDAL 自研解析器这个方案。核心逻辑自己写数据读写用成熟库这样既能讲清楚原理又能保证代码是“自己的”后续想扩展也方便。实际做下来R-JPEG的解析工作量并不大真正容易出坑的是温度公式的细节和地理坐标的写入这两块后面我会重点展开。2. 大疆热红外影像的核心原理2.1 R-JPEG复合文件长什么样大疆热红外照片在底层是一个复合文件格式俗称R-JPEG本质上就是把多种数据打包在一个文件里。我这边的理解是它在普通JPEG的完整流之后追加了一段原始热辐射TIFF数据再追加一段XMP元数据。文件结构大致如下文件起始部分普通JPEG图像流从SOI标记FF D8开始到EOI标记FF D9结束。这部分就是你在系统相册里能看到的那张图。JPEG流之后嵌入式TIFF段以“II*\0”或“MM\0*”开头里面保存的是16位单波段的原始热数据矩阵宽高和热成像分辨率一致比如640×512。最后XMP元数据段以“x:xmpmeta”或“?xpacket”开头里面是XML格式的标签记录了GPS经纬度、高度、发射率、温度标定参数等。这里有个很实用的定位思路先通过JPEG的SOI/EOI定位主图结束位置然后在主图之后搜索TIFF头标记再往后搜索XMP标记。只要知道这三个段的位置整个文件就被拆清楚了。由于JPEG图流里也可能内嵌EXIF里面也有TIFF结构所以定位TIFF段时一定要从JPEG的EOI之后开始找不要从头扫否则很容易定位到EXIF里那张缩略图导致热数据提取失败。这是我一开始踩过最典型的坑。2.2 温度到底怎么算gain、offset和开尔文大疆热红外影像温度转换的核心公式在公开资料和实际导出的XMP参数里都能对上温度 原始DN值 × (RadiometricGain / 10000) RadiometricOffset其中RadiometricGain和RadiometricOffset就在XMP元数据里分别表示增益和偏移量。为什么大疆要除以10000再乘因为gain在内部存储时为了保留小数点精度直接乘了10000读出来是整数或浮点数形式算法里要还原回去。这个公式最坑的地方在于不同机型的RadiometricOffset单位可能不一样。有的机型公式算出来直接是摄氏度比如算完得到26.5那说明就是摄氏度有的机型算出来是开尔文量级比如值在290~310之间需要减掉273.15才是摄氏度。具体怎么判断我的做法是算完温度后统计全图中位数如果中位数在150以上基本可以确定是开尔文如果中位数在-20到60之间那就是摄氏度。还有不少网上资料会把公式写成“温度 raw × 0.04 - 273.15”这个公式对FLIR的某些相机是成立的因为FLIR的原始值刻度不同。但大疆不是通用公式必须从XMP中读取每张图自己的gain和offset不能拿一个固定系数套所有影像。这也是毕业设计里容易被答辩老师追问的点你只要说清楚“不同影像的温度标定参数不同必须逐张读取并应用”这个思路就是对的。2.3 XMP元数据里藏着什么XMP是Adobe定义的扩展元数据标准大疆把很多飞行和成像参数都写在这里面。我实际解析时最关心的几个标签包括GPSLatitude、GPSLongitude影像中心点的经纬度是后续写GeoTIFF坐标的基础。GPSAltitude、RelativeAltitude绝对海拔和相对起飞点高度用来估算地面分辨率GSD。RadiometricGain、RadiometricOffset温度换算必须的增益和偏移。Emissivity发射率默认一般0.95左右用于辐射精度校正。ReflectedTemperature反射环境温度会对高温目标有影响。拿到XMP字符串后由于它是XML格式我一开始想直接用XML库解析但发现大疆XMP里的命名空间前缀很杂比如drone-dji、djigo等不同固件版本可能还有变化。稳妥的做法是用正则表达式做标签匹配比如查找RadiometricGain0.0271这样带引号的属性值找不到再尝试标签内文本匹配。这个方法不依赖命名空间兼容性更好。3. 从零到一Python 源码解析与实现3.1 环境准备与依赖安装整个项目的运行环境我用的是Python 3.8 GDAL NumPy tifffile Pillow。其中Pillow只是用来辅助看图验证不是必须真正负责热数据读取的是tifffile负责地理坐标写入的是GDAL。环境安装建议直接用conda特别是GDAL用pip装容易出现动态库缺失的问题。用conda一条命令conda create -n thermal python3.8 conda activate thermal conda install gdal numpy pillow tifffile如果你没装conda也可以用pip这里给一个相对容易成功的安装组合pip install numpy pillow tifffile pip install gdal3.6.2不同版本的GDAL对GeoTIFF写入接口基本一致但如果你用的版本太新部分API可能调整建议锁定3.6.x系列实测最稳。GDAL安装完后导入时可能会提示找不到osgeo模块这时候多半是系统的动态库路径没配置好。Windows下可以在环境变量PATH里加上GDAL的bin目录Linux下可以用conda install gdal解决大部分问题实在不行再检查LD_LIBRARY_PATH。3.2 关键代码片段逐段分析先把文件结构解析部分的代码放出来这一段是整个项目的地基。import io import re import numpy as np from tifffile import TiffFile def parse_thermal_file(filepath): with open(filepath, rb) as f: data f.read() # 1. 定位JPEG主图流 jpeg_start data.find(b\xff\xd8) jpeg_end data.find(b\xff\xd9, jpeg_start) if jpeg_start -1 or jpeg_end -1: raise ValueError(JPEG stream not found) jpeg_end 2 # 把EOI标记包含进去 # 2. 在JPEG流之后定位TIFF段 tiff_start -1 for hdr in (bII*\x00, bMM\x00*): idx data.find(hdr, jpeg_end) if idx ! -1: tiff_start idx break if tiff_start -1: raise ValueError(Embedded TIFF not found) # 3. 定位XMP段TIFF段的结束位置可以看成是XMP起始位置 xmp_start data.find(bx:xmpmeta, tiff_start) if xmp_start -1: xmp_start data.find(b?xpacket, tiff_start) tiff_bytes data[tiff_start:xmp_start if xmp_start ! -1 else len(data)] xmp_str data[xmp_start:].decode(utf-8, errorsignore) if xmp_start ! -1 else return tiff_bytes, xmp_str用TiffFile读取这段TIFF字节流就能直接得到16位原始热数据矩阵def read_raw_temperature(tiff_bytes): with TiffFile(io.BytesIO(tiff_bytes)) as tif: raw tif.asarray() if raw.ndim 3: raw raw[:, :, 0] return raw.astype(np.float32)接着是从XMP字符串里提取参数我比较推荐用正则而不是XML解析def get_xmp_value(xmp_str, field_name): # 优先匹配属性形式fieldvalue m re.search(rf{re.escape(field_name)}\s*\s*([^]*), xmp_str) if m: return m.group(1).strip() # 再尝试标签内文本形式fieldvalue/field m re.search(rf[^]*{re.escape(field_name)}[^]*([^]*)/, xmp_str) if m: return m.group(1).strip() return None def get_radiance_params(xmp_str): gain float(get_xmp_value(xmp_str, RadiometricGain) or 0) offset float(get_xmp_value(xmp_str, RadiometricOffset) or 0) lon float(get_xmp_value(xmp_str, GPSLongitude) or get_xmp_value(xmp_str, GPSLongitude) or 0) lat float(get_xmp_value(xmp_str, GPSLatitude) or 0) alt float(get_xmp_value(xmp_str, RelativeAltitude) or get_xmp_value(xmp_str, GPSAltitude) or 100) return gain, offset, lon, lat, altGPS经纬度在XMP里有时候会有一些变体字段名所以我做了多层兼容找不到第一个名就找下一个。实际项目中还遇到过经纬度写作“31.2304N”这种带方向的字符串需要额外做个判断把N/S/E/W去掉并赋予符号。3.3 温度换算与GeoTIFF写入细节温度换算函数很直接但要对“单位”做智能化判断def raw_to_temperature(raw, gain, offset): temp raw * (gain / 10000.0) offset median_val np.median(temp) # 如果中位数明显大于常温开尔文范围判定为开尔文转摄氏度 if median_val 150: temp temp - 273.15 return temp接下来是写GeoTIFF。这里有个很容易被忽略的点经纬度坐标不能直接当成投影坐标写进GeoTIFF不然Pix4D会算错位置。大疆GPS给的是WGS84经纬度而Pix4D做拼接时一般工作在投影坐标系比如UTM。所以正确做法是先把经纬度转成UTM坐标再用UTM东坐标和北坐标作为GeoTIFF的原点。UTM分带计算和坐标转换的代码from osgeo import osr, gdal def lonlat_to_utm(lon, lat): zone int((lon 180) / 6) 1 epsg 32600 zone if lat 0 else 32700 zone src osr.SpatialReference() src.ImportFromEPSG(4326) dst osr.SpatialReference() dst.ImportFromEPSG(epsg) transform osr.CoordinateTransformation(src, dst) easting, northing, _ transform.TransformPoint(lon, lat) return epsg, easting, northing像素大小GSD的估算可以根据飞行高度和相机传感器参数来。大疆H20T热成像的传感器宽度大约10.88mm图像宽度640像素焦距大概13.6mm那么GSD计算公式为def estimate_gsd(altitude_m, sensor_width_mm10.88, focal_mm13.6, image_width640): # GSD 飞行高度 * 传感器宽度 / (焦距 * 图像宽度) return altitude_m * (sensor_width_mm / 1000.0) / (focal_mm / 1000.0 * image_width)最终写入GeoTIFF的函数def write_geotiff(out_path, temp_c, easting, northing, epsg, gsd0.1): rows, cols temp_c.shape driver gdal.GetDriverByName(GTiff) ds driver.Create(out_path, cols, rows, 1, gdal.GDT_Float32, options[COMPRESSLZW]) # 设置六参数仿射变换 ds.SetGeoTransform([easting, gsd, 0, northing, 0, -gsd]) srs osr.SpatialReference() srs.ImportFromEPSG(epsg) ds.SetProjection(srs.ExportToWkt()) band ds.GetRasterBand(1) band.WriteArray(temp_c) band.SetNoDataValue(-9999.0) band.SetDescription(Temperature_Celsius) ds.FlushCache() ds None单张转换的主流程就是把上面几个函数串起来def convert_single(input_path, output_path): tiff_bytes, xmp_str parse_thermal_file(input_path) raw read_raw_temperature(tiff_bytes) gain, offset, lon, lat, alt get_radiance_params(xmp_str) temp_c raw_to_temperature(raw, gain, offset) epsg, easting, northing lonlat_to_utm(lon, lat) gsd estimate_gsd(alt) write_geotiff(output_path, temp_c, easting, northing, epsg, gsd) print(fConverted {input_path} - {output_path})输出时用LZW压缩是因为热红外温度矩阵数值比较连续压缩率不错同时Pix4D读取也不受影响。NoData值我设为-9999这个值在正常温度范围内不可能出现后续统计分析时可以直接过滤。3.4 批量转换脚本与提速一个航测任务动辄几百上千张热红外照片单张循环转换会很慢。但瓶颈不在计算而在文件读取和GeoTIFF写入。这块用到Python的多进程会很有效因为每个文件之间完全独立不存在共享状态。from concurrent.futures import ProcessPoolExecutor from pathlib import Path def batch_convert(input_dir, output_dir): input_dir Path(input_dir) output_dir Path(output_dir) output_dir.mkdir(parentsTrue, exist_okTrue) files list(input_dir.glob(*.jpg)) list(input_dir.glob(*.JPG)) tasks [(str(f), str(output_dir / (f.stem _temp.tif))) for f in files] with ProcessPoolExecutor(max_workers4) as pool: for _ in pool.map(lambda p: convert_single(*p), tasks): pass if __name__ __main__: batch_convert(raw_images, output_tiffs)这里有个细节进程池的max_workers不是越大越好因为GDAL在并发写入时偶尔会有锁冲突实测4到6个进程比较合适再多容易报“generic error”。如果是Windows系统需要把入口代码放到if __name__ __main__:下面否则多进程会递归创建子进程得很长时间才能发现这个坑。4. Pix4D 合成实操与精度验证4.1 Pix4D 中导入热红外 tiff 的完整设置转换完的GeoTIFF可以直接放进Pix4D mapper不需要再做任何格式转换。但有个前置条件所有tiff必须是同一个坐标系和相同的GSD否则Pix4D坐标解算时容易出现分层漂移。我的操作流程是新建项目把转换后的所有tiff文件一次性全部选中拖进去。在“图像属性”里检查相片模型识别情况。如果Pix4D没有自动识别手动选择“Generic Thermal Camera”或者“自定义相机”因为我们的tiff已经是温度数值不需要再让Pix4D做辐射解算。处理模板选择“热红外”相关模板如果版本里没有现成的热红外模板选“3D地图”也可以但在“辐射校正”相关选项里要选择“不校正”或“已含辐射信息”避免温度被二次处理。直接跑初始化处理和点云生成坐标基准方面Pix4D会自动识别GeoTIFF自带的投影和原点。批量转换时最关键的是每张tiff的坐标原点要和照片实际拍摄位置对应。如果某张图GPS坐标提取失败写出来的tiff原点就是(0,0)Pix4D拼接时这张图会跑到奇怪的地方去。所以批量转换前最好先对全部文件的XMP做一次预扫描标记出缺失GPS的影像及时剔除或补录。4.2 温度精度怎么验证转换后的温度值准不准不能光靠肉眼“感觉”我建议这样做准备一个手持点温枪和一个常温黑体目标没有黑体可以用汽车引擎盖、水泥地面等温度均匀的物体替代。无人机起飞前用点温枪测量目标表面温度记录当前环境温度。飞完拿到转换后的tiff后在QGIS里打开用“Identify Features”工具读取同一目标的像元值计算差值。正常误差范围在±2°C以内可以接受如果误差大优先从发射率找原因。大疆XMP默认发射率一般在0.95左右但实际物体发射率可能不同比如水泥地约0.92、植被约0.98、金属阳光直射下可能只有0.65。发射率设置不对温度偏移会非常明显。另外还要检查公式算出来的温度是否出现“整体偏高低温区失真”的情况。如果是多半是反射温度参数ReflectedTemperature设置偏大或偏小这个参数在近距测量时影响不大但在高空大视场场景下会影响视觉一致性。4.3 高频问题排查清单下面这个表是我做这个项目过程中实际遇到的问题整理成速查形式照着排查能省不少时间现象可能原因解决办法输出的tiff全黑温度矩阵是float32但GIS软件默认按0-255显示在GIS里拉伸显示范围或检查像元值是否合理温度值都是300多公式输出是开尔文没有减273.15确认median阈值判断逻辑是否正确温度值明显偏高/偏低XMP里的gain/offset读取错误打印XMP中的RadiometricGain和Offset和DJI Thermal SDK导出值对比Pix4D中影像位置错乱GPS坐标读取失败或UTM带号错误检查lon/lat是否成功提取再检查EPSG是否落在正确的UTM分带Pix4D提示“不支持的格式”GeoTIFF里包含压缩选项导致兼容性差改用COMPRESSNONE重新输出批量转换到一半崩溃个别文件无TIFF段或XMP缺失对单文件解析做异常捕获并输出文件名便于快速定位问题文件两张影像温度不一致每张图的gain/offset不同或飞行中发射率变化确认逐张读取XMP参数而不是全局统一一个gain/offset还有一个容易被忽视的问题GPS经纬度在XMP里可能是“小数度”格式也可能是“时分秒”格式。如果解析函数不做兼容会导致定位偏差几公里。我的建议是在解析时统一检测是否含“°”或“N/S/E/W”字符若有则按度分秒转换代码里加一层预处理逻辑会比直接float()稳妥得多。5. 写在最后一些老实话这个项目做下来的最大体会是技术难点不在写代码本身而在“把格式搞清楚”。大疆热红外R-JPEG格式没有官方公开的完整文档网上的信息又杂又乱很多代码只针对某一款机型有效。我最后能做稳定靠的是拿不同批次拍摄的照片反复测试把定位TIFF段、XMP解析、单位判断这些逻辑一层层加固。如果你也是做毕业设计建议一开始就把“兼容性”放在第一位不要只测一张照片就认为万事大吉。另外一个小建议是在论文或项目文档里一定要把“为什么不能直接用DJI Thermal SDK”和“为什么必须逐张读取标定参数”讲透。这两个问题是我在答辩前被反复追问的。自己写解析器虽然工作量更大但真正理解了参数怎么流动后面不管换什么机型都能快速适配。最后再分享一个技巧转换完的tiff可以用QGIS或者Python的rasterio直接打开加载一个热力图渲染方案比如蓝到红的渐变色带快速检查温度分布是否合理。这比在Pix4D里导来导去高效得多也能在批量处理时帮你第一时间发现异常数据。本文还有配套的精品资源点击获取
返回列表