ARTICLE DETAIL

资讯详情

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

大疆热红外R-JPEG温度反演与Pix4D正射拼接全流程

大疆热红外R-JPEG温度反演与Pix4D正射拼接全流程 简介无人机热红外遥感中R-JPEG格式并非普通照片而是辐射定标数据的载体。理解温度反演原理掌握从DN值到摄氏温度的换算是解锁热红外数据工程价值的基础。将单张热红外影像转换为带地理坐标的GeoTIFF再通过Pix4D进行正射拼接能够生成可量测的温度正射图广泛应用于植保、电力巡检与建筑检测。本文结合工程实践系统讲解温度定标参数解析、GeoTIFF坐标写入及Pix4D处理参数设置为毕业设计与实际项目提供可落地的技术路径。 每年毕业季总有一批人被大疆热红外数据折腾得怀疑人生飞完一架次拷回几百张JPG打开一看全是灰度图或者伪彩图想问“这棵树到底多少度”都得去DJI的软件里一个个点。如果你正在做植保、电力巡检、建筑检测方向的毕业设计或者想让热红外数据真正变成能算、能拼、能出图的成果这篇内容就是为你准备的。我会完整讲清楚如何把大疆热红外影像转换成保存实际温度值的TIFF再用Pix4D把几百张单张影像拼成一张温度正射图并给出可运行的源码思路和毕业设计需要的文档组织方式。1. 大疆热红外照片温度信息到底藏在哪里1.1 R-JPEG不是一张普通JPG很多人第一次拿到大疆热红外相机的照片都会怀疑自己是不是导错了文件明明拍的是热红外拷出来却是一张JPG看起来除了灰度差异什么都读不出来。大疆这种携带温度信息的照片格式叫R-JPEGRadiometric JPEG外观看是普通JPEG内部却另有文章。R-JPEG的本质是“常规JPEG图像 辐射定标元数据”的复合文件。JPEG部分保存的是用于显示和无人机飞手日常看的伪彩图或灰度图元数据部分则藏在EXIF和XMP里。像素的灰度值本身并不等于温度值它更像是“温度经过某种映射后的编码”。要拿到真正的温度必须找到映射关系配合相机厂家写入的定标参数做一次反演计算。很多网上教程直接教你读灰度值然后除以一个系数那是针对特定机型的临时办法换一台机器就失效。正确做法是从R-JPEG内部提取出与辐射定标有关的字段把每个像素的DN值数字量化值还原成物体表面的辐射温度。这部分工作是大疆软件和Pix4D这类商业软件在后台帮你做的毕业设计里自己实现一遍就是核心工作量所在。1.2 从XMP里挖出辐射定标参数大疆热红外影像的元数据一般写在JPEG的APP段里通过EXIF Tool可以无痛导出。建议先在电脑上装一个exiftool然后对你的R-JPEG批量提取元数据看看exiftool -XMP -j DJI_0001.jpg输出是一段JSON里面会出现一批和辐射定标直接相关的关键字段字段名含义典型值范围R1辐射定标常数110000~30000R2辐射定标常数21000~2000B普朗克反演常数1000~2000F普朗克反演常数F0.5~2O辐射偏移量-1000~1000Emissivity发射率默认0.95~0.980.95ReflectedTemp反射环境温度20~35AtmoTemp大气温度15~40GPSLatitude / GPSLongitude影像中心点坐标WGS84GPSAltitude飞行高度相对起飞点RelativeAltitude相对高度0~120这些参数不是摆设。R1、R2、B、F、O共同决定了一条从“DN值”到“开尔文温度”的曲线这条曲线就是相机出厂时在工厂标定出来的辐射响应曲线。相机温度变化、环境湿度变化都会轻微影响准确度但用出厂标定参数做相对测温已经足够支撑大部分无人机遥感应用。这里有一个容易踩的坑大疆不同机型、不同固件版本XMP字段的前缀和名称可能略有差异。比如有的写成FLIR:R1有的写成XMP:R1。批量解析时不要用固定字段名硬编码先把第一个文件的所有XMP键打印出来确认字段名再写逻辑。1.3 那条“Planck反演公式”FLIR系热红外传感器大疆XT、XT2、H20T等内部的传感器多为此系列的辐射定标普遍基于普朗克定律的简化形式。从DN值到温度的反演公式为T B / ln( R1 / ( R2 * (DN O) ) F ) - 273.15其中T是摄氏温度DN是像素的原始数字量化值ln是自然对数。这个公式的本质是辐射亮度与温度之间不是线性关系而是近似指数关系。用普朗克公式反演就是让DN值先映射到辐射亮度再由辐射亮度反推温度。写代码时直接套公式即可import numpy as np def dn_to_celsius(dn, R1, R2, B, F, O): dn_float dn.astype(np.float32) with np.errstate(divideignore, invalidignore): rad R1 / (R2 * (dn_float O)) temp_kelvin B / np.log(rad F) temp_celsius temp_kelvin - 273.15 return temp_celsius很多毕设文章在这一步就直接结束了但实际上问题远没有这么简单。DN值从哪里来如果是把JPEG解压后直接拿0~255的灰度值代入误差可能会很大。因为R-JPEG里的JPEG图像层本身是经过显示映射的不一定等于传感器原始14bit/16bit DN值。严谨的处理方式是从R-JPEG中解析出传感器原始DN值再代入反演公式。不同机型的数据封装方式不同有的照片直接提供线性映射后的16bit温度值有的需要先从灰度值反推。我在实际项目中采用过两种方案如果R-JPEG的XMP里存在RawValueRangeMin/RawValueRangeMax这类字段可以用线性映射把8bit灰度近似还原到16bit原始DN范围再代入公式。如果目标是绝对测温精度推荐用大疆官方Thermal SDK读取导出每像素的原始温度矩阵再把温度矩阵写为GeoTIFF。毕业设计如果主打“自己实现”选方案1更有展示价值但必须在论文里诚实地分析精度局限如果主打“工程应用”选方案2更稳定源码里可以留一个SDK封装接口。2. 核心实现把DN值换成摄氏度的完整路子2.1 从照片到参数代码怎么读取R-JPEG的XMP实现温度转换第一步是批量读取XMP参数。写Python代码时推荐用subprocess调用exiftool而不是自己用二进制解析JPEG APP段因为XMP本身是XML格式手工解析容易漏字段。下面是核心读取函数import json import subprocess def read_radiometric_params(jpg_path): cmd [ exiftool, -json, -XMP, -n, # 输出数值而不是字符串 jpg_path ] result subprocess.run(cmd, capture_outputTrue, textTrue, checkTrue) data json.loads(result.stdout)[0] def get(*keys, defaultNone): for key in keys: val data.get(key) if val is not None and val ! : try: return float(val) except ValueError: return default return default params { R1: get(R1, XMP:R1, default14973.0), R2: get(R2, XMP:R2, default1489.0), B: get(B, XMP:B, default1428.0), F: get(F, XMP:F, default1.0), O: get(O, XMP:O, default-1120.0), Emissivity: get(Emissivity, XMP:Emissivity, default0.95), ReflectedTemp: get(ReflectedTemp, XMP:ReflectedTemp, default28.0), } return params代码里保留默认值是为了防止某一张照片的XMP缺字段时程序直接崩溃。但默认值只是兜底绝不能当作万能参数批量处理时每一张照片都必须单独读取参数。大疆热红外相机的定标参数在出厂时写死在设备里一般同一台相机短期内数值变化不大但不同架次、不同环境温度下反射环境温度和大气温度会有变化逐张读取是最稳妥的做法。2.2 温度换算核心算法与误差控制拿到参数后就能把整张影像的像素矩阵做一次向量化计算。这里有几个细节决定你的结果可不可信。第一像素矩阵读取时要用灰度模式。R-JPEG虽然是彩色JPEG但热红外通道本质是单波段灰度彩色信息是软件后处理的伪彩。用OpenCV读取时明确指定灰度模式import cv2 img_bgr cv2.imread(jpg_path, cv2.IMREAD_GRAYSCALE) if img_bgr is None: raise ValueError(f无法读取影像: {jpg_path})第二公式里的DN值是否要预处理。如果JPEG灰度层是8bit的而传感器原始数据是14bit就需要做线性拉伸还原RAW_MIN 0 RAW_MAX 16383 # 14bit DN_8BIT_MAX 255 def map_dn_8bit_to_raw(dn_8bit): return dn_8bit.astype(np.float32) / DN_8BIT_MAX * (RAW_MAX - RAW_MIN) RAW_MIN这个线性映射只是近似在某些机型上可能不成立。务必要用实际拍摄数据验证。我做过的一个项目中把水杯和黑体同时放进画面用测温枪和水银温度计对照发现直接拿8bit灰度代入公式的结果普遍偏低3~5℃经过线性还原到14bit再代入公式后误差缩小到±2℃以内。这就是“近似处理”和“严谨处理”的差别。第三温度矩阵的输出类型。温度可能是负值-10℃也可能是小数25.6℃所以输出数组类型必须是float32或float64。用uint16会丢掉小数精度用uint8会直接把负温度截断导致成品完全不能用。def jpg_to_temperature_matrix(jpg_path): params read_radiometric_params(jpg_path) dn cv2.imread(jpg_path, cv2.IMREAD_GRAYSCALE) if dn is None: raise ValueError(f无法读取灰度影像: {jpg_path}) dn_raw map_dn_8bit_to_raw(dn) temp dn_to_celsius(dn_raw, params[R1], params[R2], params[B], params[F], params[O]) return temp.astype(np.float32)2.3 批量处理单张验证通过后怎么上量单张转换成功只是第一步。一个架次飞下来通常有几百张影像必须写批量处理脚本。批量处理时最需要注意的是异常照片比如起飞前误触快门拍的镜头盖照片、降落时拍到近景树木的照片。这些照片温度矩阵可能包含严重异常值直接进入拼接会影响空三解算。我在批量处理脚本里做了三件事输出信息汇总表、记录缺参数照片、剔除温度范围明显异常的照片。import os import pandas as pd from tqdm import tqdm def batch_convert(input_dir, output_dir): os.makedirs(output_dir, exist_okTrue) records [] jpg_files [f for f in os.listdir(input_dir) if f.lower().endswith(.jpg)] for fname in tqdm(sorted(jpg_files)): src_path os.path.join(input_dir, fname) try: temp_mat jpg_to_temperature_matrix(src_path) temp_min float(temp_mat.min()) temp_max float(temp_mat.max()) # 温度范围合理性检查-40~100℃以外说明数据异常 if temp_min -40 or temp_max 100: records.append({file: fname, status: temperature_range_abnormal}) continue out_path os.path.join(output_dir, fname.replace(.jpg, _temp.tif)) save_geotiff(temp_mat, src_path, out_path) records.append({file: fname, status: ok, temp_min: round(temp_min, 2), temp_max: round(temp_max, 2)}) except Exception as e: records.append({file: fname, status: ferror: {e}}) df pd.DataFrame(records) df.to_csv(os.path.join(output_dir, conversion_report.csv), indexFalse) return df这一步生成的conversion_report.csv很有价值既能用来排查异常数据也是毕业设计里“数据处理”章节的原始素材。3. GeoTIFF输出与坐标写入Pix4D能拼接的关键3.1 为什么要带坐标Pix4D怎么用这些坐标生成的TIFF如果没有地理坐标Pix4D也能导入并拼接因为Pix4D本来就有特征匹配算法可以通过影像重叠区域算出相对位置。但直接用无坐标的TIFF做拼接有几个严重问题空中三角测量时容易尺度漂移不同架次、不同飞行路线之间的影像难以统一到同一坐标系最终成果没有地理位置信息在ArcGIS/QGIS里没办法做精确的栅格分析。所以输出TIFF时一定要写入地理参考信息。Pix4D在导入影像时会读取图像元数据里的GPS坐标作为初始点云和相机位置的初始化值。这个初始值不要求非常精确误差在几米甚至几十米都没关系空三解算会通过特征匹配优化。真正要保证的是每个影像都有坐标、坐标大体准确、坐标系统一。大疆热红外照片的EXIF里自带GPSLatitude、GPSLongitude、GPSAltitude、GPSDateTime、GimbalYawDegree等字段。读取EXIF的方式和读取XMP类似也是用exiftool批量抽取。关键是要把这些GPS坐标写入到GeoTIFF的仿射变换矩阵中。3.2 利用EXIF里的GPS信息生成仿射变换GeoTIFF的地理参考由两部分组成CRS坐标系和Transform仿射变换矩阵。对单张无人机影像来说如果没有做畸变校正和正射纠正它只是中心点坐标已知的一幅透视投影影像。写入GeoTIFF时可以用中心点坐标 地面分辨率来构造Transform。from pyproj import Transformer import rasterio from rasterio.transform import from_origin def extract_gps(src_path): cmd [exiftool, -json, -n, -GPSLatitude, -GPSLongitude, -GPSAltitude, -RelativeAltitude, src_path] result subprocess.run(cmd, capture_outputTrue, textTrue, checkTrue) data json.loads(result.stdout)[0] lat float(data.get(GPSLatitude, 0)) lon float(data.get(GPSLongitude, 0)) alt float(data.get(GPSAltitude, 0)) return lat, lon, alt拿到经纬度后如果直接把经纬度作为CRS写入用经纬单位构造Transform会非常别扭因为单像素尺寸的经纬度跨度极小容易产生浮点精度问题。推荐做法是先把经纬度投影到当地UTM投影带再按米为单位构造Transform。H20T这类相机拍摄的影像地面分辨率由飞行相对高度和传感器焦距决定。一般大疆的R-JPEG里会给出FocalLength和SensorWidth之类信息也可以用简化公式估算GSD 相对高度 * 传感器宽度像素个数 / (焦距 * 图像宽度像素个数)示例参数焦距13mm传感器宽度8.8mm相对高度50m图像宽度640pxGSD约为0.14m/px。实际毕设里飞30m~80m高度GSD一般在3~10cm之间具体以实际飞行记录为准。3.3 投影转换从WGS84到UTM的细节坐标转换用pyproj简洁且不容易出错。流程是先根据经纬度算出UTM带号再构造投影变换器。import math from pyproj import CRS, Transformer def lonlat_to_utm(lat, lon): zone int((lon 180) / 6) 1 crs_wgs84 CRS.from_epsg(4326) crs_utm CRS.from_epsg(32600 zone) # 北半球 if lat 0: crs_utm CRS.from_epsg(32700 zone) # 南半球 transformer Transformer.from_crs(crs_wgs84, crs_utm, always_xyTrue) x, y transformer.transform(lon, lat) return x, y, crs_utm这里有一个容易忽略的点国内毕业生如果测区在境内UTM带号通常是适合的但如果是面向全球应用最好在设置界面里允许用户手动指定投影带。Pix4D最终拼接默认使用WGS84 UTM坐标所以转换到UTM后进入Pix4D会很顺畅。实际写入GeoTIFF时如果单张影像没有经过正射纠正直接用中心点坐标构造的Transform会存在系统偏差。这个偏差对Pix4D来说不是问题因为Pix4D会根据特征点重新计算影像外方位元素。但如果想先把转换后的TIFF直接叠加到底图上看位置你会发现影像与底图有偏移这是正常的别怀疑代码写错了。3.4 输出GeoTIFF的完整代码示例下面给出一个完整的单张转换函数把温度矩阵、GPS坐标、仿射变换都写进GeoTIFF。import numpy as np import rasterio from rasterio.transform import from_origin def save_geotiff(temp_mat, src_path, out_path): lat, lon, alt extract_gps(src_path) x, y, crs_utm lonlat_to_utm(lat, lon) # 估算GSD精确值可以从影像EXIF读取 relative_alt 50.0 # 从影像获取更准确 focal_mm 13.0 sensor_width_mm 8.8 img_width_px temp_mat.shape[1] gsd relative_alt * sensor_width_mm / (focal_mm * img_width_px) rows, cols temp_mat.shape # 影像中心点坐标换算为左上角坐标 top_left_x x - cols / 2 * gsd top_left_y y rows / 2 * gsd transform from_origin(top_left_x, top_left_y, gsd, gsd) profile { driver: GTiff, height: rows, width: cols, count: 1, dtype: float32, crs: crs_utm, transform: transform, } with rasterio.open(out_path, w, **profile) as dst: dst.write(temp_mat.astype(np.float32), 1) dst.update_tags(nsTEMP, TEMPERATURE_UNITCelsius)这段代码基本可以直接用于批量转换。Pix4D导入TIFF时会通过CRS与Transform自动获取每张影像的大致地理位置省去了单独导入POS数据的步骤。4. 在Pix4D中合成的实操步骤与参数建议4.1 新建工程与导入热红外TIFF的姿势打开Pix4Dmapper新建工程时工程类型会有多个选项。如果是纯热红外温度正射推荐选择“Ag Multispectral”或“Thermal Mapping”不同版本可能叫法不同核心是让Pix4D按热红外传感器的工作流去处理而不是默认的RGB相机。导入影像时直接选择批量转换生成的GeoTIFF文件夹即可。Pix4D会读取TIFF自带的地理参考并显示在地图区域。如果发现影像之间位置关系混乱检查一下GSD估算是否正确有没有出现坐标转换的南北半球问题。这里有一个容易被忽略的坑如果TIFF保存的是float32单波段Pix4D的缩略图预览可能显示成一片黑或一片白因为显示范围默认按0~255拉伸。这个现象不会影响实际拼接但会让你误以为数据坏了。解决方法是先在QGIS里打开一张TIFF查看温度值范围是否正常确认数据没问题再继续。4.2 关键参数相机模型、处理模板、坐标系Pix4D对热红外影像的相机模型识别不如RGB相机方便。导入后进入“Image Properties”面板如果Pix4D没有自动识别传感器需要手动设置焦距传感器尺寸等参数。大多数情况下从大疆R-JPEG的XMP里可以读到FocalLength和SensorWidth填进去即可。处理步骤建议“Initial Processing”里开启“Calibration”“Matching”选择“Aerial Grid”或“Free Flight”均可热红外影像纹理弱建议把“Target Number of Keypoints”调大。“Point Cloud and Mesh”一般不需要在温度正射任务中生成密集点云直接跳过可以省很多时间。“DSM and Orthomosaic”要勾选“Orthomosaic”分辨率设置成与原始GSD接近或略低太高的分辨率会放大拼接缝。坐标系选择上如果已经用UTM投影写了GeoTIFFPix4D应该能自动识别不需要额外设置。输出坐标系保持与输入一致即可。模板处理流程如下1. 新建工程 - 选择热红外模板 2. 导入GeoTIFF目录 3. 检查自动读取的相机参数和GSD 4. 选择处理模式全流程或只做正射 5. 运行初始化处理 6. 检查质量报告重点是“匹配影像数”和“重投影误差” 7. 运行DSM与正射影像生成 8. 导出正射TIFF得到温度正射图4.3 结果成果温度正射影像的检查方法拼接完成后Pix4D会输出一个正射TIFF。这个TIFF仍然是单波段但每个像素的值就是该位置的摄氏温度。验证成果是否合理有几件事要做第一打开TIFF的统计信息检查温度范围是否在合理区间。正常的土地、植被温度在-10℃到50℃区间如果出现大面积-273.15℃绝对零度以下说明公式代入时出了问题多半是DN值没有正确还原。第二用RGB航拍图或Google底图做对照看异常温度区域是否对应建筑、水体、道路等明显目标。水体和植被的温度通常比柏油路低这是判断结果是否合理的直观指标。第三单独挑一个点在源JPG上用exiftool或DJI软件读取该位置温度与正射影像上同位置的温度值对比。两者误差在1~3℃以内说明全链路工作正常。5. 毕业设计视角精度验证、文档组织与答辩亮点5.1 精度验证方案黑体与水杯实测对比毕业设计如果没有精度验证论文答辩很容易被评委一句话问住“你这个转换结果准不准凭什么说它准”所以一定要设计一个可重复的精度验证实验。我推荐一个低成本方案在测区里放置若干目标物作为温度参考。最理想是黑体辐射源但学生实验室不一定有钱买。替代方案是用几个保温杯装上不同温度的温水用热电偶或水银温度计实测表面温度然后无人机悬停拍摄热红外影像再把你程序转换得到的温度与实测温度对比。实验记录表可以这样设计目标物实测温度(℃)转换温度(℃)绝对误差(℃)保温杯142.543.10.6保温杯235.233.81.4水面24.125.00.9多次实验后统计平均误差、均方根误差然后在论文中给出结论。按照工程经验如果误差控制在±3℃以内即可满足多数热红外遥感应用的精度需求。5.2 源代码与文档说明的组织方式毕业设计源码和文档的整理看起来是工作量最少但最影响答辩印象的一环。最忌讳的就是给一个main.py然后什么注释都不写。我建议按以下结构组织project/ ├── docs/ │ ├── 01_需求分析与技术选型.md │ ├── 02_系统设计.md │ ├── 03_算法原理.md │ ├── 04_测试报告.md │ └── 05_使用说明.md ├── src/ │ ├── convert.py # 单张R-JPEG转温度GeoTIFF │ ├── batch_convert.py # 批量转换入口 │ ├── geotiff_writer.py # GeoTIFF写入封装 │ └── utils.py # 通用工具函数 ├── examples/ │ ├── sample_data/ # 几张测试影像注意脱敏 │ └── output/ # 示例输出 ├── requirements.txt └── README.md代码里每一个核心函数都要写清输入、输出、异常处理逻辑关键公式加注释说明来源。比如Planck反演公式旁边务必标注参考的文档或依据不要只写一个没有出处的公式。5.3 “加分项”还能往哪个方向扩展这个项目如果只做完“转TIFFPix4D拼接”只能算中规中矩的工程型毕设。想拿高分可以再加一个“基于温度正射影像的分析应用”比如植被水分胁迫分析利用冠层温度与空气温度差计算作物水分亏缺指数画一张缺水分布图。建筑物外墙空鼓检测温度梯度异常区域往往对应外墙空鼓或保温层缺陷用温度正射图做边缘检测提取疑似区域。热岛效应评价把温度正射影像与土地利用分类叠加统计不同地类的平均温度。这些扩展方向都不需要太多额外编程只要能把温度正射影像读进Python做统计分析即可。但放在毕业论文里这就是一个完整的“应用示范”比单纯的数据转换更有说服力。5.4 我踩过的几个坑最后说几个我在整个项目里踩过的坑每一个都耽误过不少时间。第一批量处理时一定要逐张读取XMP参数不能图省事用第一张的参数套全部。有一次我就是为了省时间结果第二天发现其中一次飞行的相机参数和前一天完全不同输出的温度整体偏了5℃。第二exiftool执行速度很慢处理几百张照片时每张都新起一个进程会非常耗时。如果数据量上千张建议一次性导出所有XMP为JSON然后在Python里直接解析能省一大半时间。第三Pix4D处理热红外影像时如果影像的纹理太弱会出现匹配失败的情况。比如大面积水面、平坦雪地这种场景建议飞行时保留足够重叠率旁向重叠70%以上并尽量让航线包含一些岸线、树丛等明显地物。第四输出的GeoTIFF如果后期要在QGIS里做显示建议同时生成一个拉伸后的伪彩版本不然默认拉伸会让温度正射图看起来对比度很低。Pix4D生成的成果一般自带显示渲染但你自己程序输出的单张TIFF需要额外处理。这个项目做到最后我真切体会是热红外遥感看起来高深核心难点其实就三个——格式解析、坐标参考、拼接参数。把这三点啃下来剩下的就是写代码和跑实验。刚开始接触R-JPEG的时候我也被那一堆Planck常数搞得一头雾水但当你亲眼看着几百张灰度照片变成一张能直接量温度的测绘图那种感觉还是很值的。毕业设计能把这条链路完整走通无论最后答辩还是后续找工作都算得上一个拿得出手的作品。本文还有配套的精品资源点击获取
返回列表