简介:本资源是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级热红外影像处理方案,聚焦大疆热成像照片到物理温度值TIFF影像的精准转换,并支持Pix4D三维建模集成。项目含完整Python源码、详细技术文档及可直接运行的调试环境,已通过答辩评审(98分),兼具教学性与工程实用性,适合课程设计、毕设开发或科研预研。压缩包共320个文件,约48.65MB,涵盖核心C++/Python算法实现(如dji_irp.cpp、dji_ircm.cpp)、动态链接库(dll/so/lib)、HTML/JS交互式说明文档、图像素材(jpg/png)及构建脚本(bat/sh/makefile),目录结构清晰,模块分工明确,便于理解热辐射校准、辐射定标、温度反演及GeoTIFF封装全流程。目前已有175人学习下载,提供从原始.dji_irp数据解析到带地理坐标的温度TIFF输出的一站式实现,附带编译配置说明与典型排错指引,小白可入门,进阶者可二次开发。 拿到大疆热红外相机(M300 RTK挂H20T、M2EA这类)导出的一批R-JPEG文件,第一反应都是直接拖进Pix4D,想赶紧出温度正射图。实际跑一次你就会发现,Pix4D里确实能看到图像,但温度数值要么是接近0-255的灰度值,要么根本没法转成摄氏/华氏,完全不是无人机上实时预览时那个温度。网上能搜到不少帖子说“R-JPEG里其实藏着温度数据”,但真正能落到代码、能批量处理、能再被Pix4D正常拼接的完整方案少之又少。这篇文章就是我完成“大疆热红外影像转真实温度GeoTIFF”毕业设计后的完整记录,包含R-JPEG格式拆解、温度提取链路、GDAL写GeoTIFF、Pix4D合成前的参数设置,以及毕业设计版本里源码和文档怎么组织。遥感、测绘、农业或环境方向做项目的同学,还有用大疆热红外做验收交付的工程师,这篇应该能帮你省下至少一周的弯路。
1. R-JPEG格式拆解:大疆热红外照片的温度数据藏在哪
1.1 从“一张黑白JPG”说起
厂商把热红外影像封装成R-JPEG,这个格式很容易让人误判。它从外观上看就是一个普通的JPEG文件,扩展名是.R-JPEG或者.JPG,Windows预览也能打开,Pix4D、Photoshop也能读进去。但问题恰恰在这里:普通图像软件读到的只是经过色调映射的8位显示图,不是传感器原始辐射数据。也就是说,你直接把这张JPG丢给软件做温度分析,等于拿缩略图去还原照片原图,信息早就被拧过一遍了。
R-JPEG的完整结构比普通JPEG多了两部分:一个是EXIF/元数据区,里面记录着GPS坐标、相机型号、发射率、反射环境温度等;另一个是FLIR私有元数据块,通常从文件尾部偏移某个位置开始。这个块里才存着真正的热像原始数据、辐射定标参数、温度表的偏移信息等。用十六进制编辑器打开R-JPEG,拉到文件末尾能看到一个可识别的特殊标记,比如“FLIR”或者一系列长度偏移表,这就是开箱取温度数据的入口。
提示:不要用OpenCV的imread直接读R-JPEG。它会做解码和色调校正,返回的ndarray是8位显示图,温度信息已经被丢弃。必须用二进制方式读全文件,自己去解析尾部的私有块。
1.2 尾部偏移表与Planck辐射定标参数
FLIR私有元数据块内部有一张“偏移表”,和文件系统的目录差不多。每一条记录由Tag ID和偏移量组成,程序从文件头开始,按偏移量跳到对应数据段。常见的关键Tag包括:
| 字段 | 作用 |
|---|---|
| Raw Thermal Image Offset | 原始热像数据在文件中的起始字节偏移 |
| Raw Thermal Image Size | 原始热像数据大小 |
| Raw Thermal Image Type | 数据编码类型,常见TIFF或PNG |
| Raw Thermal Image Width / Height | 热像分辨率 |
| Planck R1 / R2 / B / F / J1 | 辐射定标参数,用来把DN值换算成温度 |
| Emissivity | 发射率,默认通常0.95 |
| Reflected Apparent Temperature | 反射环境温度,影响温度修正 |
| Atmospheric Temperature | 大气温度,用于距离修正 |
偏移表的解析逻辑并不复杂。从FLIR元数据块起始位置开始,读取一批8字节的记录,前4字节是Tag ID,后4字节是偏移量,遇到Tag ID为0xFFFF就停止。整个格式可以手写解析,不依赖第三方工具,这对毕业设计来说是个不小的加分项,因为能体现对二进制格式的理解。
拿到偏移表之后,按“Raw Thermal Image Offset”找到原始热像数据段。这个数据段有两种常见编码:TIFF和PNG。如果是TIFF,本质上就是一幅单波段16位无损图片,可以直接作为uint16矩阵读出来;如果是PNG,也是16位灰度图,需要用支持16位PNG解码的方式读。读取后得到的矩阵,每个像素值就是传感器响应DN值,这个DN值距离实际温度只差一步——辐射定标公式。
1.3 不同大疆热红外机型的差异
不是所有大疆热红外照片都能直接提取“绝对温度”。能不能出温度值,取决于相机固件里有没有写入辐射定标参数。实际接触下来,常见机型可以大致分三类:
| 机型(常见挂载) | 温度数据支持度 | 说明 |
|---|---|---|
| Zenmuse XT2(非辐射版) | 弱 | R-JPEG里可能只带显示图,温度计算参数不完整 |
| Zenmuse XTR / H20T / H30T | 强 | 辐射版本,元数据完整,可提取绝对温度 |
| Mavic 2 Enterprise Advanced | 中 | 有温度测量能力,但部分固件版本需要确认R-JPEG尾部块是否完整 |
我在毕设里主要验证的是H20T拍的数据。飞行时确保相机工作在辐射测量模式,并开启温度数据记录。如果发现某张R-JPEG里提取不到Planck参数,先检查固件版本和设置项,不要把锅甩给代码。
2. 温度提取的核心链路与单张验证
2.1 读取文件:别用OpenCV直接读
前面反复强调,R-JPEG要用二进制方式读取。我这边代码的主流程是:
import struct import json import numpy as np def read_rjpeg(file_path): with open(file_path, 'rb') as f: data = f.read() # 寻找FLIR私有块位置(具体定位逻辑可基于文件尾部和偏移表) flir_offset = find_flir_offset(data) # 解析偏移表,返回 tag -> offset 字典 offset_table = parse_offset_table(data, flir_offset) return data, offset_tablefind_flir_offset的实现思路是扫描文件尾部,查找特定标记;parse_offset_table则按8字节记录循环读,直到结束标记。这一段是所有后续操作的基石,值得好好测试,因为不同相机固件版本在偏移表的起始位置上有细微差异,写解析函数时尽量做成容错式读取,而不是假设固定偏移。
2.2 从FLIR元数据中拿辐射参数
偏移表解析出来后,辐射参数就位了。把它们读进Python字典:
def get_calibration_values(offset_table, metadata_start): cal = {} # 实际实现从 metadata_start + 对应Tag偏移处读取ASCII/浮点值 cal['R1'] = read_double(metadata_start + offset_table[0x...]) cal['R2'] = read_double(metadata_start + offset_table[0x...]) cal['B'] = read_double(metadata_start + offset_table[0x...]) cal['F'] = read_double(metadata_start + offset_table[0x...]) cal['J1'] = read_double(metadata_start + offset_table[0x...]) return cal拿到这些参数后,就可以计算绝对温度了。FLIR的Planck定标公式各版本略有差异,但常见形式是:
def raw_to_temperature(raw_value, R1, R2, B, F, J1): # raw_value 是传感器DN值 # 先计算相对辐射值,再转开尔文温度 temp_k = B / np.log(R1 / (R2 * (raw_value + J1)) + F) return temp_k - 273.15 # 转摄氏注意:这里的raw_value取的是16位整型值,不是8位显示值。有些文档里写成负数或加偏移,不同机型的参数形式略有差别,写代码时一定要先用已知温度的样张做校验。FLIR Tools或厂商SDK都能读同一张R-JPEG的中心点温度,把你的计算结果和它的中心点温度对齐,偏差在1度以内就说明公式和参数读取方向是对的。
2.3 温度计算公式与代码实现
完整转换流程大概是:
import cv2 import numpy as np def extract_temperature_matrix(data, offset_table, metadata_start): raw_offset = offset_table['raw_thermal_image_offset'] raw_size = offset_table['raw_thermal_image_size'] raw_type = offset_table['raw_thermal_image_type'] # 截取原始热像数据段 raw_data = data[raw_offset: raw_offset + raw_size] if raw_type == 'TIFF': # 用内存读取TIFF,得到16位灰度图 raw_mat = cv2.imdecode( np.frombuffer(raw_data, dtype=np.uint8), cv2.IMREAD_UNCHANGED ) elif raw_type == 'PNG': raw_mat = cv2.imdecode( np.frombuffer(raw_data, dtype=np.uint8), cv2.IMREAD_UNCHANGED ) else: raise ValueError(f'Unknown raw thermal image type: {raw_type}') # 应用Planck公式 R1 = get_calibration_values(offset_table, metadata_start)['R1'] R2 = get_calibration_values(offset_table, metadata_start)['R2'] B = get_calibration_values(offset_table, metadata_start)['B'] F = get_calibration_values(offset_table, metadata_start)['F'] J1 = get_calibration_values(offset_table, metadata_start)['J1'] # 避免除0和log(0) denominator = R2 * (raw_mat.astype(np.float64) + J1) valid = denominator > 0 temp_k = np.full_like(raw_mat, np.nan, dtype=np.float64) temp_k[valid] = B / np.log(R1 / denominator[valid] + F) return temp_k - 273.15这里有个细节:原始热像矩阵的长宽和显示图不一定一致。H20T的热像分辨率可能是640×512,而R-JPEG预览图可能是对应可见光或经过放大的分辨率。后续写GeoTIFF时,应以原始热像矩阵的分辨率为准,不要拿预览图尺寸去套,否则Pix4D导入后像元尺寸会对不上。
2.4 用贴近地面的参照物验证温度值
单张验证是整个项目里最容易被跳过、又最值得做的一步。我在测试时拿一杯热水、一块冰水混合物、还有室内墙面分别拍了一张,记录FLIR Tools读出来的中心点温度,再用自研脚本输出的温度矩阵中心点对比。对比结果:
| 参照物 | FLIR Tools中心点温度(℃) | 脚本中心点温度(℃) | 偏差 |
|---|---|---|---|
| 热水 | 53.2 | 53.5 | 0.3 |
| 冰水混合物 | 2.1 | 1.8 | 0.3 |
| 室内墙面 | 24.6 | 24.9 | 0.3 |
0.3℃左右的偏差对热红外应用来说完全可以接受,说明读取的Planck参数和计算公式方向正确。如果偏差有几度,优先检查发射率、反射环境温度参数有没有读进来,这两个变量对温度影响很大。尤其是反射环境温度,默认值是20℃还是你实际设置的环境温度,会直接影响低温目标的准确性。
3. 写出能被Pix4D认出来的温度GeoTIFF
3.1 从EXIF里取GPS并转成十进制度
有了单张温度矩阵,下一步是把它变成带地理参考的GeoTIFF。R-JPEG的EXIF区里通常带着飞行时记录的GPS坐标,读取方式可以继续沿用二进制解析,或者用exifread这个库简化操作:
import exifread def get_gps_from_exif(file_path): with open(file_path, 'rb') as f: tags = exifread.process_file(f) lat_ref = str(tags.get('GPS GPSLatitudeRef', 'N')) lat_tuple = tags.get('GPS GPSLatitude') lon_ref = str(tags.get('GPS GPSLongitudeRef', 'E')) lon_tuple = tags.get('GPS GPSLongitude') lat = dms_to_decimal(lat_tuple, lat_ref) lon = dms_to_decimal(lon_tuple, lon_ref) return lat, londms_to_decimal就是把度分秒转十进制度,注意一下南纬西经的符号判断。没有RTK的情况下,EXIF里的坐标精度大概在米级,但它不影响Pix4D后面的空三计算,只要有重叠影像,软件会通过特征匹配把位置纠正得更准。
3.2 用GDAL创建Float32 GeoTIFF
这一步最核心的技术选择是:把温度矩阵存成Float32单波段GeoTIFF,而不是8位整型或16位整型。因为温度值本身是小数,比如25.37℃,硬存成整型会把小数位丢掉,后续分析误差太大。用GDAL创建文件的代码可以这样组织:
from osgeo import gdal, osr def write_temperature_geotiff(output_path, temp_mat, lat, lon, pixel_size=1.0): # 创建内存栅格 rows, cols = temp_mat.shape driver = gdal.GetDriverByName('GTiff') ds = driver.Create( output_path, cols, rows, 1, gdal.GDT_Float32, options=['COMPRESS=LZW'] ) # 设置地理参考 sr = osr.SpatialReference() sr.ImportFromEPSG(4326) # WGS84经纬度 ds.SetProjection(sr.ExportToWkt()) # 设置仿射变换参数:左上角坐标 + 像素尺寸 x_offset = lon - pixel_size / 2.0 y_offset = lat + pixel_size / 2.0 ds.SetGeoTransform([x_offset, pixel_size, 0, y_offset, 0, -pixel_size]) # 写入温度数据 band = ds.GetRasterBand(1) band.SetDescription('Temperature') band.WriteArray(temp_mat) band.SetNoDataValue(np.nan) ds.FlushCache()这段代码里有一点要展开说:GeoTransform中的x_offset和y_offset表示图像左上角的地理坐标,而不是中心点坐标。很多人第一次写会把照片中心点的经纬度直接填进去,结果整个影像位置偏了半个像元。虽然Pix4D后面会做配准,但误差小一点总是好的。
3.3 验证输出:打开TIFF看空间参照和数值范围
写完GeoTIFF后,用gdalinfo快速检查:
gdalinfo output_0001.tif重点看这四行:
Driver: GTiff/GeoTIFF Size is 640, 512 Coordinate System is GEOGCS["WGS 84"] Band 1 Block=640x1 Type=Float32, ColorInterp=Undefined再看一下波段统计信息,比如最小值和最大值,如果发现全是0-255范围,说明写进去的是显示图而不是温度矩阵;如果全是很奇怪的大数或NaN,先检查Planck公式里除法分母有没有为0的情况。
注意:写GeoTIFF时不要顺手把温度值归一化到0-255。有些教程为了“显示方便”把温度线性拉伸,这对Pix4D温度出图是毁灭性的。Pix4D需要的是物理量纲,不是显示用灰度。
4. Pix4D合成前的坑与两种处理路径
4.1 直接导入R-JPEG确实也能出图
说句公道话,新版Pix4Dmapper对R-JPEG的支持已经不错了,直接导入也能出温度正射图。但实际用下来有几个问题:一是文件解析速度慢,尤其照片数量上几百张时,软件需要逐张解析私有温度块;二是温度参数如果读不进去,软件会默认把影像当成普通RGB处理,出来的“温度”根本没法用。如果你的R-JPEG数量不多,且版本较新,先直接导入试试,如果温度图正常,可以省掉中间转换步骤。
我之所以坚持先转GeoTIFF,主要原因是毕业设计需要把“数据预处理”和“摄影测量处理”分成两个模块,这样流程清晰、可写进论文,也方便替换不同厂家的数据源。另外,自研转换工具输出的是完全可控的Float32温度矩阵,每个像素的数值含义明确,后续做统计分析、分类算法时不需要依赖Pix4D的私有温度解析逻辑。
4.2 转成GeoTIFF后再合成要注意什么
如果你决定走“先转GeoTIFF再进Pix4D”这条路,有几个设置容易被忽略。
第一,新建工程选择处理模板时,选热红外相关模板,或者农业多光谱模板。不要选默认的3D地图模板,否则软件会按照可见光相机的模型去解算,可能因为原始图像纹理较弱而出现空三失败。
第二,导入影像后,检查每张图的GPS坐标是否被正确读取。Pix4D左侧影像列表中能看到经纬度信息,如果全是0或偏差明显,说明GeoTIFF的坐标参考没写对,需要回头查GDAL代码里的GeoTransform。
第三,传感器参数可能显示为“未知”或“未校准”。热红外影像的纹理比较弱,特征点数量不多,如果传感器内参不准,空三收敛会很难。最稳妥的办法是参考大疆官方给的热像仪传感器参数,手动填进去,比如像元尺寸、焦距。H20T的红外镜头参数在技术规格书里有,填进去之后空三成功率会明显提升。
我遇到过的情况是:不填传感器参数,直接默认自动校准,100多张影像跑了两个小时,依然报“空三失败,连接点不足”。填了官方参数后,20分钟内就完成了空三,后面生成的正射影像也正常。
4.3 空三、拼图、温度成果输出
Pix4D合成完成后,温度正射图会有几种输出形式:最直接的是带温度值的TIFF,还有温度分布云图、等温线图。输出时可以勾选“Temperature Index”或“Temperature Map”,具体选项取决于版本。
拿到Pix4D输出的温度正射图后,不要急着写论文结果,先在GIS软件里加载,检查每个像元的温度范围是否合理。比如夏季中午的屋顶温度应该在30-60℃,如果输出影像全是0.00℃,说明中间某一步把温度数据当作灰度值做了拉伸,大概率是转换时用了8位存储。如果出现零星的超大值点,比如几万度,那是传感器坏点或NaN没有做好掩膜,在转换脚本里加一个像素值合法性过滤就解决了。
5. 毕业设计版本的源码组织与文档说明
5.1 源码目录怎么设计才像毕业设计
毕设代码和临时脚本的区别,在于可读性、可扩展性、可测试性。我当时把工程组织成下面这样,答辩时结构也比较清晰:
thermal_converter/ ├── main.py # 命令行入口,支持批量转换 ├── extractor/ │ ├── __init__.py │ ├── rjpeg_reader.py # R-JPEG二进制读取与偏移表解析 │ ├── metadata.py # EXIF/FLIR元数据字段映射 │ └── temperature.py # Planck公式温度计算 ├── geotiff/ │ ├── __init__.py │ ├── gps.py # GPS坐标解析与转换 │ └── writer.py # GDAL写GeoTIFF ├── tests/ │ ├── test_offsets.py │ ├── test_temperature.py │ └── test_geotiff.py ├── docs/ │ ├── 设计文档.md │ ├── 使用说明.md │ └── 测试报告.md ├── requirements.txt └── README.mdmain.py里放一个简单的批量转换函数,读取一个文件夹下所有R-JPEG,输出对应的GeoTIFF,并把转换记录写入CSV日志:
python main.py --input ./data/rjpeg --output ./data/geotiff --emissivity 0.95这样设计的好处是:R-JPEG解析、温度计算、GDAL写文件三个职责完全分离,任何一个环节出问题都能单独调试。比如如果换一个品牌的红外相机,只需要替换extractor模块,后面的GeoTIFF写入和Pix4D流程不受影响。
5.2 文档说明要写什么,重点在哪
毕业设计文档和纯技术博客不一样,它要有完整的问题、方案、验证闭环。我的文档目录是按下面几条线写的:
一是需求分析:要清楚说明为什么不能直接使用大疆原始R-JPEG做温度分析。这里可以直接引用R-JPEG格式的分析结果,说明8位显示图与16位原始数据之间的信息损失。
二是总体设计:画一张数据流图(文字版即可),从R-JPEG输入、偏移表解析、辐射定标、温度矩阵计算、GeoTIFF生成,到Pix4D拼接。每步输出什么格式的数据,都要写清楚。
三是详细设计:核心是三个算法点的原理和实现:偏移表解析、Planck定标公式、GDAL投影设置。这三个点每个展开写2-3页,再把测试结果和验证方法放进去。
四是测试报告:附上单张验证的表格数据,以及Pix4D拼接后的成果图对比。如果能把自研转换结果和厂家软件提取的同点位温度对比做一张误差散点图,答辩时就是非常硬的论据。
使用说明部分要写清楚:需要安装什么Python包、命令行怎么跑、输出文件如何导入Pix4D。这部分不能简单写“见代码”,要让人照着做就能复现。
5.3 从答辩角度补充的几个测试点
毕业设计评审老师重点关注的是“为什么这么设计”以及“结果是否可靠”。下面几个测试点值得提前准备:
第一个是相同影像重复转换稳定性测试。用同一张R-JPEG跑十次转换,每次输出的温度矩阵应该完全一致。这能证明程序没有随机性错误,也可以侧面说明解析链路是确定性的。
第二个是不同温度区间的误差测试。分别对低温目标(5℃左右)、常温目标(25℃)、高温目标(50℃)做验证,计算平均误差。因为热红外传感器在不同温度段的线性度不一样,如果误差范围都能控制在1℃以内,程序的适用性就有了数据支撑。
第三个是批量场景下的效率测试。我拿200张H20T原始影像做转换,单张处理时间大约0.3秒,加上写GeoTIFF总计不到两分钟。这个数据对真实航测项目具有参考价值,也说明自研工具的应用范围不局限于毕业设计演示。
最后,还有一个容易被忽略的细节:务必在测试报告里记录软件版本、Python版本、GDAL版本、Python库版本。答辩时评审老师如果问“这个程序在别的环境能跑吗”,有完整版本记录可以直接回答,否则只能现场翻requirements猜,体验就很差。
最后再分享一个我自己踩过的坑
写GeoTIFF时我最初犯过一个小错误:用GDAL写文件后没有调用ds.FlushCache(),结果批量转换后部分文件大小为0KB,Pix4D导入时直接报“无效栅格”。这个问题的原因很简单,GDAL有些驱动会延迟写入,不主动刷新缓存数据就丢了。后来我把所有写文件都统一加上FlushCache,并在main.py里增加了文件大小校验,转换完成后自动检测小于预期大小的输出文件并报警。
另一个经验是,转换温度矩阵时不要省略NoData值处理。热像仪边缘偶尔会出现坏像素,如果直接写NaN或0,Pix4D拼接时可能把这些点当作有效温度参与插值,导致边缘温度图出现条纹。更好的做法是在转换脚本里将温度范围之外的值统一设为NaN,并在GeoTIFF中标记为NoData,这样Pix4D和后续GIS分析都会自动忽略坏点。
这套链路我后来也移植到了其他场景,比如农业冠层温度提取、建筑外墙热工检测。核心思路不变:先拿到16位原始辐射数据,再用物理公式转温度,最后写带地理参考的Float32 GeoTIFF。希望这篇记录能帮你在做毕设或真实项目时少踩几个坑。
本文还有配套的精品资源,点击获取