基于QGIS的遥感影像云检测与地物恢复系统实现

发布时间:2026/8/31 23:42:05
基于QGIS的遥感影像云检测与地物恢复系统实现 简介本资源是一个基于QGIS平台二次开发的遥感图像云检测与地物信息恢复集成系统面向遥感、GIS及数字图像处理领域的开发者、科研人员与高年级本科生解决遥感影像预处理中云层干扰识别难、遮挡区域地物信息难以重建的核心问题。压缩包共76个文件含21个核心Python源码如main.py、MyMainWin.py、各类算法线程模块、5个Qt Designer设计的.ui界面文件、7个图标与界面资源图png/ico、5个XML配置及国际化qm文件以及遥感数据样例tif、算法参数配置json和详细说明文档docx/md整体大小为51.24MB。已有70人学习下载资源结构清晰分层涵盖QGIS菜单集成qgisMenu.py、多源遥感大气校正GF1/Landsat8专用对话框、波段组合与辐射定标线程、深度学习模型调用接口雏形及UI交互逻辑提供可直接运行的桌面端原型系统具备算法替换、流程可视化与模块化扩展能力是开展遥感智能处理二次开发实践的优质工程参考。 我做了几年的遥感影像处理最头疼的就是碰到带云的影像。光学卫星影像只要天气不好云层一盖下面全是废数据。以前处理云遮挡要么在ENVI里人工勾选、要么拿PS一点一点修遇到大范围测区几十景影像光去云就能耗掉两三天。后来我干脆基于QGIS做了一套二次开发系统把云检测和地物恢复整个流程串起来日常处理效率提升非常明显。今天把这套系统的设计思路和实现细节梳理出来给同样踩过这个坑的同学做个参考。这套系统解决的核心问题很简单给一副带云遥感影像自动把云和云影检测出来然后用合理的方式把云遮挡下的地物信息恢复出来最终产出一幅可用、可入库、可分析的“无云”影像。适用于Landsat、Sentinel-2、高分系列等常见光学遥感数据不管是做土地利用分类、植被监测还是影像镶嵌都有实用价值。适合有一定遥感基础、想在QGIS环境下做定制工具的开发者。1. 项目整体设计与架构选型1.1 为什么选QGIS作为二次开发平台先说结论QGIS做遥感影像二次开发底座是当前开源技术栈里综合性价比最高的选择。对比过几个主流平台ENVI的IDL二次开发确实成熟但license费用不低而且做桌面端工具分发很麻烦ArcGIS的ArcObjects和ArcPy生态完整可Python版本锁定严格发布独立工具也得依赖ArcGIS环境OpenCV单独做图像处理没问题但缺乏GIS空间信息管理能力没有矢量、投影、属性表这些基础支撑。QGIS的优势在于它把GIS和遥感影像处理天然地整合在一起。底层用GDAL做栅格数据读写空间参考、地理变换、金字塔、波段管理全部内置不用自己造轮子。而且QGIS提供Python插件开发接口UI设计用Qt Designer拖拽就能完成开发效率比C二次开发高很多。对于遥感算法验证和工具落地来说Python生态足够支撑numpy做矩阵运算、GDAL做I/O、TensorFlow或PyTorch做深度学习推理全部丝滑衔接。实测下来从零搭建一套插件框架到跑通第一个云检测流程一个周以内可以完成。1.2 系统功能模块划分整套系统按职责拆成了五个模块彼此解耦、通过数据流串联这也是后续能持续加算法、换模型的关键。数据管理模块负责输入影像读取、波段提取、影像裁剪、投影转换核心依赖GDAL/QgsRasterLayer。云检测引擎模块内置多套云检测算法包括阈值法、随机森林、深度学习模型统一接口调用。地物恢复模块对云遮挡区域进行重建支持时间序列替换、插值修复、生成式填充三种策略。质量评估模块计算检测精度指标IoU、PA、F1分数评估恢复后的影像质量。可视化交互模块在QGIS地图画布上叠加检测结果、ROI勾选、参数调优、结果导出。系统界面集成在QGIS的菜单栏和工具面板中点击“云检测”按钮弹出参数对话框选好影像路径、勾选算法组合方式、设置阈值参数一键运行。结果以临时图层叠加到地图上用户可以缩放查看检测边界满意后一键导出。1.3 技术栈选型版本说明开发环境这块经历过几次版本升级踩过不少坑当前稳定组合如下供参考组件版本选择说明QGIS3.28 LTR或以上LTR版本稳定插件API兼容性最好Python3.9或3.10匹配QGIS内置Python避免后续依赖冲突GDAL与QGIS配套版本Windows下建议直接使用OSGeo4W安装深度学习框架PyTorch 2.xCPU/GPU均可语义分割模型训练与推理模型转换ONNX Runtime模型部署时用ONNX格式避免依赖冲突UI开发Qt Designer PyQt5快速设计插件交互界面需要特别说明的是QGIS版本不要随手升到最新。QGIS的Python API在不同版本之间有过调整例如某些接口在3.22到3.28之间改过签名如果插件写好了升级后容易报错。我一直在3.28 LTR上开发稳定性很好算法部分完全不受影响。2. 云检测算法集成与多算法融合机制2.1 云检测算法选型全景云检测听起来简单实际做起来很考验对遥感成像机理的理解。云在可见光波段表现为高反射、低纹理在热红外波段表现为低温高云或中温低云而云影则是暗色、低反射区域。针对这些特性业界算法大致分三代第一代是物理阈值法。典型代表是HOTHaze Optimized Transform变换和Fmask算法。这类方法通过设定多个波段反射率阈值来区分云层比如Landsat 8中蓝色波段反射率大于0.3、近红外波段反射率大于0.2、且NDVI较低很可能就是云。优点是速度极快、无需训练样本缺点是阈值的普适性差不同地表类型雪、沙漠、城市高楼容易误检。第二代是传统机器学习法。提取每个像素的光谱特征、纹理特征灰度共生矩阵等、空间上下文特征喂给随机森林、支持向量机分类器。相比阈值法精度有提升但依然需要人工做特征工程而且分类器训练需要标注样本泛化到不同传感器时特征分布差异较大需要重新训练。第三代是深度学习方法。把云检测看作图像语义分割任务UNet、DeepLabV3、SegFormer都有大量成功应用。深度学习模型能自动学习云的形态、边界、阴影关系在多云复杂场景下精度显著优于前两代方法。我在实际测试中UNet模型在Sentinel-2影像上的IoU能达到0.89左右比经典Fmask高出近10个百分点。2.2 多算法融合策略级联投票单一的算法没有一种能通吃所有场景。阈值法薄云漏检严重深度学习在厚云边缘有时过拟合导致边界畸变两者互补性很强。所以我把融合机制做成“级联置信度投票”的混合框架。第一层级使用快速阈值法做粗检测目标是“宁多勿漏”把疑似云的区域尽量找出来。这一步计算量小一景Landsat影像几秒钟就能出结果。粗检测结果作为mask把影像分为“确认无云区”和“疑似云区”。第二层级只在疑似云区上运行深度学习模型做精细分类。这样做有两个好处一是大幅减少深度模型的推理面积原先全图跑一遍要20秒现在只跑可疑区域一到两秒就完成二是阈值法排除掉的区域本就是大概率无云区深度模型只需集中区分薄云、厚云和云影任务更聚焦、准确率更高。融合逻辑可以简单写成final_mask np.zeros_like(coarse_mask) # 粗检确认无云像素直接标为背景 final_mask[coarse_mask 0] 0 # 粗检为云的候选区用深度模型细判 candidate coarse_mask 1 if candidate.any(): refined deep_model.predict(img[:, :, candidate_roi]) final_mask[candidate] refined # 双引擎投票制阈值法判为云、模型判为无云的像素 # 进一步做连通域形态学处理去掉孤立点2.3 三种融合策略对比融合策略实现思路优势劣势适用场景级联式阈值法粗检深度模型精检速度快精度高阈值法漏检区域直接放弃大批量数据快速处理多数投票式多个模型独立预测结果取多数鲁棒性最强推理耗时成倍增加对精度要求极高的单景影像置信度加权各模型输出概率值加权求和取最大精度与速度折中需要标定各模型权重常态化处理流程我在默认配置里使用的是级联式融合因为处理效率优先。当遇到单景影像质量极差、需要精细刻画云边界时会切换到置信度加权模式。置信度权重需要在少量验证样本上做网格搜索确定即尝试不同的权重组合选择验证集上F1分数最高的那一组。这一步看起来繁琐但换来的是不同算法在不同场景下的优势互补。2.4 精度评估不可跳过云检测做得好不好不能只靠肉眼主观判断。我建立了独立标准评估模块加载人工标注的真值mask或人工检查过的样本集计算四个核心指标像素准确率PA所有像素中预测正确的比例直观但容易受背景主导。IoUIntersection over Union预测云区与真实云区的交集除以并集是语义分割最常用的指标。F1分数精确率与召回率的调和平均兼顾漏检和误检。错分率与漏分率具体分析是“把非云当云”多还是“把云当非云”多。评估模块在每次算法调参后自动运行指标变化一目了然有效避免了“调参调了半天效果反而变差”的问题。3. 地物信息恢复的核心实现3.1 云遮挡区域的三种恢复方案云检测只是第一步用户真正关心的是云下面的地物长什么样。要恢复地物信息有三种主流思路各有适用范围方案一时间序列替换法。利用同一区域不同日期的影像把有云区域用无云时相的对应像素替换。这是最可靠的方法因为替换的像素本身就是真实观测数据不存在“想象”的成分。前提是要有同区域、时相接近的无云影像而且两期影像之间地表覆盖不能发生剧烈变化比如不能拿冬天的像素替换夏天的植被。实现时需要对两景影像做配准、辐射归一化处理否则拼接边界很突兀。方案二空间插值修复法。基于云区周围无云像素的光谱值通过反距离加权IDW、普通克里金或样条插值推算云区像素值。这种方式适用于云斑面积较小的场景比如小于几百个像素的碎云插值效果尚可。一旦云区面积大到上万个像素插值结果就是一片模糊的色彩过渡毫无地物纹理细节不可用。方案三生成式填充法。利用生成对抗网络或扩散模型根据云区周边的上下文信息“脑补”出内部地物纹理。这个方法这两年很火但问题同样突出模型生成的纹理可能是“合理的虚假”——形状、颜色合理但和真实情况对不上。在业务验收时生成式填充容易被认定为非真实观测数据所以我一般只用它做视觉预览不用于定量反演分析。3.2 混合恢复策略的设计单一方案不可靠这套系统内置了“先识别后恢复”的混合策略整个流程分成三条分支云区像素先分类为“可替换”和“不可替换”。具体判断依据是时间序列影像库中是否存在相近时相且重叠面积大于阈值默认80%的无云影像替换前做辐射归一化防止色差。如果不可替换再判断云区连通域面积小于2000像素的用插值修复大于2000像素的转入生成式填充作为兜底。用户也可以在界面上手动调整判断阈值针对不同测区做适配。# 混合恢复策略伪代码 if has_time_series_image(cloud_bbox): result temporal_replace(src_img, ref_img, cloud_mask) elif cloud_region_pixel_count 2000: result interpolation_fill(src_img, cloud_mask) else: result generative_fill(src_img, cloud_mask)3.3 实战效果分析拿一景含有大面积厚云的Sentinel-2影像实测云占比约30%云区面积细分后有相当一部分是连片厚云。时间序列替换法找到了30天前的无云影像替换后整体色调一致性较好但部分农田因为时相不同导致颜色出现轻微差异。对于无法替换的零星云区域插值修复效果不错边界过渡自然。实测整体视觉效果接近无云影像但NDVI反演结果中少部分替换区域与其他区域的植被指数存在偏差这套流程更适合做目视解译底图和制图合成。如果是做定量分析建议优先保证时间序列替换的时相一致性把“替换源影像与目标影像的成像日期差”作为必选参数并在输出报告中显著标注。这也是我在开发过程中最深的体会——地物恢复的终点不只是“看着没云”还得多想想“数据能否用于定量计算”。4. 实操环节QGIS插件开发与算法集成的关键代码4.1 插件开发环境搭建避开最容易翻车的几个坑我第二次搭建QGIS插件环境时踩过一次大坑在Windows上直接pip安装PyQt5结果版本和QGIS内置的Qt库不一致界面加载后中文乱码、控件错位反复排查才发现是Qt插件路径冲突。正确做法是使用OSGeo4W Shell作为Python环境直接利用QGIS官方打包好的Python库和Qt库不额外pip安装Qt相关包。确认当前环境方式# 在QGIS Python控制台中检查版本 from qgis.core import Qgis print(Qgis.QGIS_VERSION_INT) import PyQt5.QtCore print(PyQt5.QT_VERSION_STR)插件骨架工程建议是cloud_remove_plugin/ ├── __init__.py ├── cloud_remove_plugin.py # 插件主入口 ├── cloud_remove_plugin_dialog.py # 对话框逻辑 ├── cloud_remove_plugin_dialog_base.ui # Qt Designer绘制的界面 ├── cloud_detection/ │ ├── __init__.py │ ├── threshold_detect.py # 阈值云检测 │ ├── deep_model.py # 深度模型推理 │ └── fusion.py # 多算法融合 ├── restoration/ │ ├── temporal_replace.py # 时间序列替换 │ ├── interpolation_fill.py # 插值修复 │ └── generative_fill.py # 生成式填充 ├── resources.qrc └── metadata.txt用Qt Designer画界面时推荐把所有参数都做成可手动修改的输入框和下拉框不要把参数硬编码在代码里。这在调参阶段能节省大量反复改代码重新加载插件的时间。4.2 核心算法模块的接入示例第一块读取影像波段数据并转换坐标系。遥感影像在处理前必须统一投影坐标系否则后续像素对齐全是问题。在QGIS插件中最稳妥的方式是用GDAL读取原始数据后再用gdal.Warp重投影。from osgeo import gdal, gdalconst import numpy as np def load_bands_as_array(raster_path, target_epsg4326): src_ds gdal.Open(raster_path, gdalconst.GA_ReadOnly) if src_ds is None: raise ValueError(f无法读取影像: {raster_path}) src_wkt src_ds.GetProjection() # 重投影到目标坐标系 dst_wkt fEPSG:{target_epsg} warp_options gdal.WarpOptions(formatVRT, dstSRSdst_wkt) vrt_ds gdal.Warp(, src_ds, optionswarp_options) band_count vrt_ds.RasterCount bands [] for b in range(1, band_count 1): band vrt_ds.GetRasterBand(b).ReadAsArray() bands.append(band) return np.stack(bands, axis-1), vrt_ds.GetGeoTransform(), vrt_ds.GetProjection()第二块深度学习模型推理。训练好的云检测UNet模型先导出为ONNX格式再用ONNX Runtime加载推理。这样的好处是插件运行环境不用安装PyTorch部署简单很多规避了Python库兼容性问题。import onnxruntime as ort import numpy as np class CloudSegEngine: def __init__(self, onnx_path): self.sess ort.InferenceSession( onnx_path, providers[CUDAExecutionProvider, CPUExecutionProvider] ) self.input_name self.sess.get_inputs()[0].name self.output_name self.sess.get_outputs()[0].name def predict(self, patch): # patch shape: (H, W, C), uint16/float32 input_tensor patch.astype(np.float32) # 归一化到 [0,1] input_tensor (input_tensor - input_tensor.min()) / ( input_tensor.max() - input_tensor.min() 1e-6 ) # 转为 NCHW input_tensor np.transpose(input_tensor, (2, 0, 1))[None, ...] prob self.sess.run([self.output_name], {self.input_name: input_tensor})[0] mask np.argmax(prob[0], axis0) return mask第三块恢复结果写回GeoTIFF。注意要保留原始影像的地理参考信息否则输出的文件无法在GIS中定位。使用gdal.GetDriverByName(GTiff)创建文件时通过SetGeoTransform和SetProjection写入坐标信息。def write_geotiff(out_path, data, geo_transform, projection, dtypegdal.GDT_Float32): driver gdal.GetDriverByName(GTiff) height, width data.shape[:2] band_count 1 if data.ndim 2 else data.shape[2] out_ds driver.Create(out_path, width, height, band_count, dtype) out_ds.SetGeoTransform(geo_transform) out_ds.SetProjection(projection) if band_count 1: out_ds.GetRasterBand(1).WriteArray(data) else: for i in range(band_count): out_ds.GetRasterBand(i 1).WriteArray(data[:, :, i]) out_ds.FlushCache()4.3 界面集成要点自定义算法要出现在QGIS菜单中需要重写addAction和initGui方法。加载处理后的图层并添加到地图画布时一定要给定正确的CRS否则QGIS无法正确叠加显示。def initGui(self): self.action QAction(云检测与地物恢复, self.iface.mainWindow()) self.action.triggered.connect(self.run) self.iface.addToolBarIcon(self.action) self.iface.addPluginToMenu(遥感处理工具, self.action) def run(self): dlg CloudRemoveDialog(self.iface) dlg.show()在界面设计上我习惯给每个关键按钮加上“运行日志输出”区域把每一步算法执行的耗时和中间结果反馈给用户比如“阈值粗检完成耗时2.3秒检测疑似云像元185420个”。这种设计在调试和跟用户交付时都很有用——不是所有使用系统的同事都懂算法但他们都看得懂日志。5. 常见问题与排查技巧实录5.1 问题速查表问题现象可能原因解决方案QGIS启动后插件加载失败metadata.txt格式错误或缺少依赖检查metadata.txt版本号格式读取QGIS日志的插件报错信息影像读取后显示全黑拉伸方式不对或数据是16位无符号整型图层属性设置中调整对比度增强方式为“按最大最小值拉伸”云检测mask偏大/偏小阈值参数不适合当前影像开启参数调试模式可视化每一层阈值结果逐步调整深度学习模型推理内存溢出一次加载整景影像过大切块推理每块256x256重叠16像素按块拼接恢复后影像边界色差明显未做辐射归一化或羽化处理对替换边界做20像素宽度渐变羽化输出GeoTIFF在ArcGIS/QGIS中偏移未正确写入GeoTransform输出前打印原始GeoTransform确认使用同一对象写回5.2 内存管理经验遥感影像动辄上GB如果在插件里一次性把整景影像全部读入numpy数组一台16GB内存的电脑瞬间就会被吃满。我的处理方案是分块block计算设定默认分块大小1024x1024像素每个分块独立执行云检测和恢复算法最后统一拼接写出。这样处理一景2GB的影像内存峰值控制在1GB左右稳定可靠。def process_large_image(ds, block_size1024): width ds.RasterXSize height ds.RasterYSize result np.zeros((height, width), dtypenp.uint8) for y in range(0, height, block_size): for x in range(0, width, block_size): block ds.ReadAsArray(x, y, min(block_size, width - x), min(block_size, height - y)) block_mask cloud_detect_block(block) result[y:yblock_mask.shape[0], x:xblock_mask.shape[1]] block_mask return result5.3 调参与日志没有调试输出一切问题都是玄学整个开发过程中我最后悔的一件事是没有从一开始就建立完善的日志系统。前期联调时一旦结果不对根本不知道是阈值算法的问题、模型的问题、还是数据处理的问题只能一段段地打印变量一点一点排查效率极低。后来我写了一个简单的日志模块输出等级分为DEBUG/INFO/WARNING/ERROR关键步骤全部打上日志读取影像的信息、坐标范围、波段数量、每一层算法处理的时间、云检测结果的统计值、恢复结果的文件路径。运行完一次性收集日志文件哪个环节异常一目了然。import logging logging.basicConfig( filenamecloud_remove.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, ) logger logging.getLogger(CloudRemove) logger.info(开始处理影像: %s, image_path)5.4 关于模型训练的一些心得深度模型部分大多数人其实是卡在训练数据准备上。云检测的公开数据集有Landsat8 Biome、Sentinel-2 Cloud Mask Catalogue等可以拿来预训练但应用到本地高分影像时建议用少量人工标注样本做微调。微调样本不需要很多精选20-30个典型场景块每块512x512就能显著提升模型在目标传感器上的表现。因为云的形态总归是全球一致的但不同传感器的波段响应、分辨率不同微调的作用就是让模型适应目标影像的光谱分布。训练时还有一个容易忽视的坑——样本类别不平衡。无云像素占了绝大部分如果直接训练模型倾向于把所有像素都预测为无云准确率看着很高但实际毫无用处。需要给云类加上更高的类别权重或者使用Focal Loss这类处理类别不平衡的损失函数。我在训练时用的是dice_loss加交叉熵的加权组合实际效果比单用交叉熵稳健得多。6. 这套系统后续还可以怎么扩展开发完这套系统后我在实际项目中不断给它加新功能慢慢沉淀出几个很实用的扩展方向供参考。第一接入更多卫星传感器数据。目前系统针对Landsat和Sentinel-2做了适配但高分系列、资源系列的数据格式和波段设置都有差异。做传感器适配层时抽象出统一的“波段提取接口”每个传感器单独实现一个适配器就能顺滑扩展。后续需要支持国产卫星的话这步拆得越早越省事。第二加入批处理模式。日常生产任务往往是几十景影像一起处理单张点按钮效率太低。目前我已经扩展了“批量处理工作流”读取一个文件夹下所有影像的清单文件文本文件每行一条影像路径顺序执行云检测、恢复、质量评估最终生成一个汇总报告。批量模式跑起来后一晚上能自动处理50景以上第二天直接收结果对生产效率提升非常明显。第三服务化封装。桌面端插件适合单机作业但如果业务方是一个团队需要共享处理能力可以额外加一个“导出处理脚本”功能自动生成独立的Python CLI脚本这样就能在没有QGIS环境的服务器上跑批处理。不过要注意CLI脚本和插件共用核心算法模块但入口不同、不用加载QGIS界面部署起来更轻量。这套系统的路还很长但现在回头来看从最初一个“让影像去云更省事”的小需求逐步发展成包含多算法融合、自动化质量评估、批量处理能力的小工具链这个过程中积累的调试经验和工程化工具方法其实比代码本身更有价值。如果你是做遥感数据处理这块的又有长期跟影像打交道的需求花点时间把QGIS二次开发这条路走通绝对值得。本文还有配套的精品资源点击获取