简介:该资源为遥感变化检测方向的实用代码包,面向地理信息、计算机视觉及遥感初学者,聚焦PCA主成分分析与K-means聚类两类方法的结合应用。压缩包共14个文件、大小约59KB,其中algorithm.py、k_means.py、util.py与main.py构成完整执行流程,方便直接运行或二次修改;PCAKmeans_burn.png、PCAKmeans_forest.png、PCAKmeans_conifer.png可直观查看不同地物的变化检测结果。资源已在CSDN被254人浏览学习,适合希望快速理解PCA降维、K-means聚类及变化检测实施步骤的人群。通过阅读源码与示例图像,可掌握从数据预处理、特征提取到聚类分析、变化区域识别的整体思路,也能了解初始聚类中心选择对结果的影响,为后续环境监测、植被变化分析等项目打下基础。
1. 变化检测不止目视判读:这套 PCA-Kmeans 脚本为什么值得下
手里两期同一地区的遥感影像,想找植被破坏或火烧迹地,靠人工目视判读两张图来回切,圈完一个区域至少半小时,还容易漏掉藏在阴影里的小块变化。用程序跑一遍变化检测,几分钟出一张变化分布图,再回头复核重点区域,效率完全不是一个量级。这份 PCAKmeans 资源就是干这个的:核心是主成分分析加 K-means 聚类,用 PCA 把多波段影像压成少数几个信息最集中的主成分,再交给 K-means 聚成不同的类,通过比较两时相的聚类结果定位变化区域。压缩包里有算法实现、主流程脚本和三张样例结果图。适合刚开始做遥感变化检测的从业者,也适合想把手写 PCA 和 K-means 流程跑通的算法同学。
2. 方法原理:PCA 把噪声滤掉,K-means 把变化找出来
2.1 PCA 在变化检测里到底做了什么:降维与噪声抑制
PCA 在遥感图像处理里不是新鲜事,人脸识别里的"特征脸"就是 PCA 降维后的基向量,遥感里的主成分图也是类似思路,只是基向量从人脸像素变成了波段组合方向。拿哨兵二号或者 Landsat 的多光谱影像来说,十几个波段之间高度相关——近红外和红边波段对植被的响应是同步变化的,直接把十几张波段图逐像素做差,噪声会淹没真实变化信号。
PCA 做的事情是把这些相关波段重新组合成一组互不相关的通道,第一主成分集中了绝大部分方差,对应地表反射的整体亮度结构;第二、第三主成分往往对应植被、水体、裸土这类地物特征的差异。噪声因为方差小,被自然排挤到后面的主成分里,丢弃后不会影响变化信息的提取。
实现步骤上,常见做法是先做标准化,把每个波段的均值归零、方差归一到单位方差,否则量纲差异会让高值波段主导整个协方差矩阵。然后算协方差矩阵,做特征值分解,特征值大小代表每个主成分保留的信息量,特征向量就是投影方向。做主成分选择时,我一般看累计方差贡献率,阈值取 95% 或 99%:
| 参数 | 常用取值 | 建议 |
|---|---|---|
| 累计方差贡献率 | 0.95 ~ 0.99 | 数据质量好取 0.95,噪声大取 0.99 |
| 主成分个数 | 3 ~ 6 | 超过 6 个通常已经进入噪声带 |
| 是否标准化 | 是 | 多光谱影像必须标准化 |
2.2 K-means 的价值:无监督聚类识别变化区域
K-means 在这个流程里扮演的角色是把变化信息切成可解释的区域。对两时相图像做主成分分析之后,把同一位置的主成分值相减,得到的差值特征就代表了该位置在两个时间点之间的光谱变化。理论上,未变化的区域差值接近零,变化区域差值明显偏离零。但"明显"到什么程度,靠人工设阈值容易翻车,因为不同地物类型的背景噪声水平不一样。
K-means 的聪明之处在于用一种无监督的方式自动划分:先在差值特征空间里随机放 k 个中心点,每个像素归到距离最近的中心,然后重新计算中心位置,迭代直到中心不再明显移动。最终聚出来的簇中,一类通常对应"未变化",一类或几类对应不同类型的"变化"。这个过程完全不需要标注数据,这对遥感应用很友好——大部分历史影像根本没有对应的真值标签。
实际使用中,k 值选 2 还是选 3 以上,取决于你要不要区分变化方向。如果只是找"植被变没了"这种单一变化,k=2 就够;如果想同时分出"植被变成裸土"和"裸土变成植被",k=3 或 4 更合适。但要注意,K-means 聚类出的簇不一定严格对应语义类别,簇的物理意义要靠后续的地物属性分析来判断。
2.3 为什么不直接用差值法或监督分类:三个方案的对比
做变化检测最朴素的做法是波段差值——直接把两时相的对应波段相减,超过阈值判定为变化。这个思路简单,但阈值选择很看经验,而且对于不同波段量纲不一致的问题缺少应对手段。变化向量分析 CVA 是差值法的升级版,把多个波段的变化量合成一个向量,用向量模长判断变化强度,但同样依赖阈值。
监督分类则是另一条路:训练一个分类器识别地物类型,再比较两时相的分类结果。这在有高质量训练样本的时候非常准,但样本标注成本极高,而且不同季节影像的分类器通用性差。PCA-Kmeans 的组合恰好卡在中间——不需要标注,不需要调阈值,PCA 做好了噪声抑制,K-means 自动划分变化簇,计算量也远小于深度学习方法。下表把几个方案的特点摆在了一起:
| 方法 | 是否需要标注 | 核心难点 | 适用场景 |
|---|---|---|---|
| 波段差值法 | 否 | 阈值难以确定 | 单波段、变化明显且均匀 |
| 变化向量分析 CVA | 否 | 多维变化阈值难标定 | 多波段变化强度评估 |
| 监督分类后比较 | 是 | 样本标注成本高、跨期鲁棒性差 | 有长期真值数据的区域 |
| PCA + K-means | 否 | k 值选择与初始中心影响 | 快速普查、无真值区域 |
这套资源采用的就是最后一种方案。main.py 里把两时相影像拼接起来做 PCA,再对主成分差值做 K-means,属于遥感变化检测里验证过的经典流程,尤其适用于火烧迹地、砍伐、城市扩张这类有明确光谱响应的地表变化。
3. 代码拆解:从 util.py 到 main.py 的完整流转
3.1 文件清点:哪几个文件是核心,哪几个可以删
解压之后先别急着跑,把文件角色认清楚。algorithm.py 和 k_means.py 是算法的实现核心,util.py 是辅助函数,main.py 是流程入口。三张结果图 PCAKmeans_burn.png、PCAKmeans_forest.png、PCAKmeans_conifer.png 分别是火烧迹地、森林、针叶林场景下的输出示例,跑代码前先看这几张图能建立直观印象。剩下的 .idea 目录和pycache目录是开发环境生成的临时文件。.idea 里是 PyCharm 的项目配置,pycache里是 Python 编译产生的字节码缓存,与算法逻辑无关,删掉不影响运行。
| 文件 | 角色 | 核心内容 |
|---|---|---|
| util.py | 工具层 | 图像读取、数据归一化等辅助处理 |
| algorithm.py | 算法层 | PCA 主成分分析实现 |
| k_means.py | 算法层 | K-means 聚类实现 |
| main.py | 入口层 | 串联完整变化检测流程 |
| PCAKmeans_burn.png | 示例输出 | 火烧迹地检测结果图 |
| PCAKmeans_forest.png | 示例输出 | 森林区域变化检测结果图 |
| PCAKmeans_conifer.png | 示例输出 | 针叶林区域变化检测结果图 |
3.2 algorithm.py:PCA 主成分计算的实现逻辑
从文件划分推断,algorithm.py 里大概率封装了一个 PCA 类或者函数,输入是二维数组,形状是像素数乘波段数,输出是降维后的主成分得分图。手写 PCA 的实现思路一般如下,核心步骤是标准化、协方差矩阵、特征值分解和投影:
import numpy as np def pca(data, n_components=None, variance_ratio=0.95): # data: (n_samples, n_features),每行一个像素,每列一个波段 # 1. 标准化,避免量纲差异影响特征值分解 mean = np.mean(data, axis=0) std = np.std(data, axis=0) std[std == 0] = 1e-12 data_norm = (data - mean) / std # 2. 协方差矩阵与特征值分解 cov = np.cov(data_norm.T) eig_vals, eig_vecs = np.linalg.eigh(cov) # 3. 特征值升序排列,翻转成降序 idx = np.argsort(eig_vals)[::-1] eig_vals = eig_vals[idx] eig_vecs = eig_vecs[:, idx] # 4. 按累计方差贡献率自动确定主成分个数 if n_components is None: total = np.sum(eig_vals) cum_ratio = np.cumsum(eig_vals) / total n_components = int(np.searchsorted(cum_ratio, variance_ratio) + 1) # 5. 投影到主成分空间 proj = data_norm @ eig_vecs[:, :n_components] return proj, eig_vals[:n_components], eig_vecs[:, :n_components]逻辑说明:第 2 步用np.linalg.eigh而不是eig,因为协方差矩阵是实对称矩阵,eigh专门处理对称矩阵,数值稳定性更好;第 3 步翻转特征值顺序是因为eigh默认按升序返回,而我们想要信息量最大的主成分排在前面;第 4 步的searchsorted找的是累计方差贡献率第一次超过 0.95 的位置。
参数说明:variance_ratio控制 PCA 保留多少信息,影像噪声大时建议调到 0.99,但主成分个数会增多,后续聚类计算量也变大,需要做取舍。n_components传固定值可以跳过自动计算,比如直接传 4,前 4 个主成分在多数遥感场景下已经能覆盖绝大部分地表差异。
3.3 k_means.py:聚类核心循环怎么写
k_means.py 里应该是一个标准的 K-means 实现,输入主成分差值特征,输出每个像素的簇标签。手写版核心代码通常长这样:
import numpy as np def kmeans(X, k, max_iter=100, tol=1e-4, random_state=42): # X: (n_samples, n_features),主成分差值特征 rng = np.random.default_rng(random_state) # 从样本里随机挑 k 个像素作为初始中心,远优于全零初始化 centers = X[rng.choice(X.shape[0], k, replace=False)] for epoch in range(max_iter): # 计算每个样本到各中心的欧氏距离 # X[:, None, :] 扩展维度后与 centers 做广播运算 dist = np.linalg.norm(X[:, None, :] - centers[None, :, :], axis=2) labels = np.argmin(dist, axis=1) # 更新中心:每个簇的均值作为新中心 new_centers = np.array([X[labels == j].mean(axis=0) for j in range(k)]) # 中心点位移小于阈值则判定收敛 shift = np.linalg.norm(new_centers - centers) centers = new_centers if shift < tol: break return labels, centers逻辑说明:距离矩阵的计算是 K-means 最耗时的部分,X[:, None, :] - centers[None, :, :]的广播写法把像素数和中心数两个维度同时展开,一行代码算出所有距离。迭代收敛条件是中心点移动距离小于tol,这是最常用的停止标准。注意如果某个簇的样本数为 0,X[labels == j].mean(axis=0)会产生空簇,实际工程里要加一个空簇重初始化逻辑。
参数说明:k是目标簇数,变化检测中通常取 2 或 3,取 2 表示只分变化与未变化,取 3 及以上表示进一步区分变化类型;tol设太小会多跑几次迭代,设太大容易在中心点还没稳定时提前收尾,一般取 1e-4;random_state固定之后结果可复现,这篇资源里不同随机种子跑出的变化图会不一样,这一点在下一章会展开说。
3.4 main.py:把两张时相串成一条变化检测流水线
main.py 的角色是把 util、algorithm、k_means 串起来。整体流程是读取两时相影像、逐像素拉平成二维数组、拼接后统一做 PCA、计算主成分差值、K-means 聚类、重排成图像并输出。关键在 PCA 环节:
import numpy as np from util import read_image, save_image from algorithm import pca from k_means import kmeans # 读取两时相影像,假设通道顺序一致、尺寸一致 t1 = read_image("scene_2019.tif") # (h, w, bands) t2 = read_image("scene_2021.tif") h, w, num_bands = t1.shape X1 = t1.reshape((-1, num_bands)) X2 = t2.reshape((-1, num_bands)) # 关键:两时相拼接后再做 PCA,确保投影方向完全一致 X_all = np.vstack([X1, X2]) P_all, _, _ = pca(X_all, n_components=4) # 切回两时相的主成分得分,并恢复图像形状 P1 = P_all[:X1.shape[0]].reshape((h, w, -1)) P2 = P_all[X1.shape[0]:].reshape((h, w, -1)) # 逐通道做差,得到主成分差值立方体 diff = P1 - P2 feat = diff.reshape((-1, diff.shape[2])) # 聚成变化与未变化两类 labels, centers = kmeans(feat, k=2, random_state=2024) change_map = labels.reshape((h, w)) save_image("change_map.png", change_map)逻辑说明:这里最值得注意的写法是先把X1和X2垂直堆叠成X_all,再统一做 PCA。两时相影像的波段本身就来自不同时刻的观测,各自做 PCA 得到的主成分方向很可能不一致——第一主成分在前一时期对应亮度,在后一时期可能对应湿度——这样直接做差毫无意义。拼接统一做 PCA 之后,两边被投影到同一个坐标空间,逐通道相减才有物理含义。聚类输入用的是差值特征,而不是单时相的主成分图,这样聚出来的簇直接对应变化模式。
参数说明:n_components=4是经验值,覆盖植被、水体、裸土和整体亮度四个维度;k=2对应变化与未变化二分类。跑通之后可以改成 k=3 到 4,在变化区域内部再做细分。random_state=2024固定了随机初始中心,保证每次运行结果一致,避免复现时出现结果对不上的尴尬。
4. 避坑记录:辐射归一化、K 值与初始中心的五个坑
4.1 两时相各自做 PCA:差值图全是噪点
现象:按照"分别对 t1 和 t2 做 PCA,再对主成分图做差"的思路跑出来的结果,变化图上一片花白,看不出集中连片的变化区域,和资源附带的 PCAKmeans_burn.png 那种清晰的火烧迹地形态完全不一样。
原因:PCA 是一种无监督变换,它找到的主成分方向是数据本身方差最大化的方向。两时相影像的亮度分布、大气条件不同,各自求出来的特征向量正交基可能差别很大,甚至某个主成分的方向符号是反的。这个时候逐通道相减,得到的是两个不同坐标空间之间的投影残差,而不是真实的光谱变化。
解决:把所有像素拼起来统一做 PCA。np.vstack([X1, X2])之后再做主成分分析,保证两时相共用同一组特征向量。这个操作在第三章的 main.py 里已经是标准写法,但如果自行改编代码,务必检查 PCA 的输入是不是拼接后的数据。
4.2 辐射归一化不做:大气条件差异全被当成变化
现象:结果图里大片区域显示"变化",连稳定的裸山、水库周边都被标记为变化区域,虚警率高到没法看,甚至整个图像一半以上像素都处于变化簇里。
原因:遥感影像受大气散射、太阳高度角、传感器定标增益等因素影响,两时相影像即使是同一传感器,辐射值也有系统差异。这种差异在光谱空间里表现为整体偏移,PCA 无法区分"大气引起的辐射偏移"和"地表真实变化",聚类时自然会把偏移当成一种变化模式。
解决:在进入 PCA 之前做辐射归一化。快速做法是选取两时相中稳定地物区域,比如裸地、大型水体、道路,用这些像素做线性回归拟合,把 t2 的辐射值校正到 t1 的量纲上;也可以用直方图匹配,让 t2 的波段直方图逼近 t1。正规场景下推荐 IR-MAD 方法做相对辐射归一化,但线性回归加直方图匹配在很多项目中已经够用。
4.3 K 值拍脑袋决定:分割粒度忽粗忽细
现象:K 取 2 的时候,变化检测结果把大片火烧迹地和周边新修道路混在一起,没法区分不同变化类型;改成 K 取 5,变化区域被打碎成许多小斑块,中间还夹着零散噪声簇。
原因:K 值直接决定聚类粒度。取 2 时所有变化被迫归入一个簇,不管光谱形态差异多大;取太大时噪声本身也会聚成独立的簇,因为噪声在特征空间里确实有聚集倾向。遥感变化区域在光谱特征空间里往往不是均匀分布,靠猜容易翻车。
解决:跑肘部法则。对同一份差值特征,依次取 k=2 到 k=8,记录每个 k 对应的簇内距离总和 SSE,SSE 下降趋势出现明显拐点的位置就是合适的 k 值。另外一个直观办法是画两时相主成分前两维的散点图,直接看差值分布在特征空间里形成了几团。
4.4 初始中心敏感:同一份数据两次跑出不同结果
现象:main.py 里不设random_state,连跑三次变化检测得到三张略有差异的变化图,有些边缘像素在变化和未变化之间反复跳动,面积统计结果也不稳定。
原因:K-means 的初始中心是随机从样本里挑的,不同的初值会把迭代过程导向不同的局部最优解。这个现象在特征维度高、簇之间有重叠时格外明显,变化检测的场景恰恰如此——变化与未变化的边界在光谱空间里是渐变的,而不是清晰间断的。
解决:一是固定随机种子,用random_state或np.random.seed锁定;二是换用 k-means++ 初始化策略,让初始中心尽量分散,减少随机性带来的影响;三是多次聚类取 SSE 最小结果的方案。本文的手写版 kmeans 函数支持random_state参数,直接固定一个值是最省事的做法。
4.5 Python 版本兼容性:旧代码跑到新环境直接报错
现象:import 时就报ModuleNotFoundError,或者运行到 PCA 步骤时报AttributeError: module 'numpy' has no attribute 'float',项目目录下还有algorithm.cpython-36.pyc这样的字节码文件。
原因:从__pycache__里的文件后缀看,这套代码最初是在 Python 3.6 环境下开发的。而 2024 年之后的 NumPy 版本已经移除了np.float、np.int这类旧类型别名,很多早期代码跑在新环境里第一行就翻车。pyc字节码文件也不能跨 Python 版本使用,3.6 编译的字节码在 3.12 下无法加载。
解决:把源码里的np.float改成float或np.float64,np.int改成int或np.int64。直接删除__pycache__目录,让 Python 在当前环境下重新编译,不要依赖旧字节码。如果还报别的兼容性错误,优先检查是否有np.bool、np.object这类旧别名,统一替换即可。养成这个习惯之后,这套代码在 Python 3.8 到 3.12 的环境里都能跑。
5. 进阶用法:从"变了没有"到"怎么变的"的验证与后处理
5.1 后处理:形态学滤波与连通域筛选去掉椒盐噪声
K-means 直接输出的变化图通常是逐像素分类结果,边缘区域免不了出现孤立小斑块和椒盐状噪声。这些零散像素在视觉上干扰判读,在面积统计上也会造成较大误差。常见做法是先做形态学开运算,消除小噪点,再做连通域标记,过滤掉面积低于阈值的碎块。只有面积大到有物理意义的区域才值得保留:
from scipy import ndimage # change_map: 布尔类型,True 表示变化区域 opened = ndimage.binary_opening(change_map, iterations=1) # 标记连通域,统计每个连通域的面积 label_im, num = ndimage.label(opened) sizes = ndimage.sum(opened, label_im, range(1, num + 1)) # 面积小于 50 像素的连通域视为噪声,置为 False clean = opened.copy() for lbl, size in zip(range(1, num + 1), sizes): if size < 50: clean[label_im == lbl] = False # 对比去噪前后的变化区域面积 before = change_map.sum() after = clean.sum() print(f"原始变化像素: {before}, 去噪后: {after}")逻辑说明:binary_opening先腐蚀后膨胀,能把孤立的单点噪声抹掉,同时保持大块区域形状基本不变;ndimage.label给每个独立区域分配一个编号,ndimage.sum统计各区域的像素数量;面积阈值 50 是我在一般分辨率影像上的起点,如果是高分影像,阈值建议按实际像元尺寸换算成最小图斑面积。
参数说明:iterations=1控制形态学运算的强度,开一次就能去掉大部分椒盐噪声,开两次以上会让变化区域边界明显收缩,不建议过度使用。面积阈值 50 需要根据分辨率调整——0.5 米分辨率的无人机影像上,50 像素可能只有 12.5 平方米;Landsat 的 30 米分辨率影像上,50 像素就是 4.5 公顷,量级完全不同。
5.2 验证精度:混淆矩阵与 Kappa 系数是必须交的作业
跑出变化图后还有一个容易偷懒但绕不开的环节:精度验证。没有验证的变化检测结果只能算初步产品,提交给别人之前至少要做一个混淆矩阵和 Kappa 系数。做法是先在图上随机撒点,然后回到两时相原始影像的对应位置目视判断这个点到底变没变,得到一组真值标签,再和聚类结果对比:
from sklearn.metrics import confusion_matrix, cohen_kappa_score import numpy as np # y_true: 目视判读真值,0 未变化 / 1 变化 # y_pred: 聚类结果,0 未变化 / 1 变化 cm = confusion_matrix(y_true, y_pred) kappa = cohen_kappa_score(y_true, y_pred) # 总体精度取混淆矩阵对角线之和占总样本的比例 total_acc = np.trace(cm) / np.sum(cm) print(f"混淆矩阵:\n{cm}") print(f"总体精度: {total_acc:.3f}, Kappa: {kappa:.3f}")逻辑说明:混淆矩阵能暴露系统偏差——比如聚类结果把大量真实变化区域漏检了,还是把大片未变化区域误报成了变化,从矩阵的列和行分布一眼就能看出来。Kappa 系数则校正了随机一致性的影响,通常高于 0.8 认为变化检测结果可用,0.6 到 0.8 之间需要回头检查分类器和特征提取的合理性。
参数说明:采样点数根据研究区大小决定,一般至少 300 个样本,其中变化与未变化区域按面积比例分配;每类最少要有 50 个样本支撑统计意义。目视判读时最好使用多波段合成图,比如近红外、红、绿波段合成,比真彩色合成对植被变化的判读更敏感。
5.3 延伸一点:从两时相到时间序列,以及新的方向
PCAKmeans 这套流程做到位之后,往前的路大致有两条。一是把两时相扩展成多时相时间序列,比如用 Sentinel-2 每个月一期影像做长时间序列分析,PCA 主成分得分随时间的变化曲线比单次差值更能反映植被恢复、城市扩张的连续过程。二是留意一下当前 CV 领域红火的"开放词汇变化检测"思路——用视觉语言模型把"道路扩展""裸土增加"这类自然语言描述直接变成检测目标。这套基于 PCA 和 K-means 的传统基线虽然朴实,但数据分布摸得透,后续换新方法时也清楚要对比的参照物是什么。
说到底,变化检测的本质不是魔法,而是把数据维度压低、把变化信号和噪声分开、再把结果解释成人类可理解的语言。从 PCA 和 K-means 入手,能少走很多弯路。
做那一次火烧迹地实验时,我因为偷懒跳过辐射归一化直接跑 PCA,结果把整片山体阴影都当成变化区域标记了出来,白白浪费了一个晚上复查。从那以后,我每次做变化检测都强制先出一张差值统计图扫一眼数据分布,确认没有明显辐射偏移,再决定要不要上 PCA 和 K-means。这个习惯帮我过滤掉了至少一半的无效实验。希望帮到你。
本文还有配套的精品资源,点击获取