拿到一幅遥感图像的时候,我们会主动关注什么要素?比如下面这张图,我们一眼就能看出,这块区域分布着公路、植被、楼房、河流等等地物。
我们为什么能够不假思索地分辨出这些不同的地物?其实这和我们后天通过学习和观察所积累起来的“经验”有关系。从某种意义上说,我们的双眼“读取了图像”,大脑再利用我们识别不同地物的“经验”,为这幅图像迅速地做了一遍 “监督分类”。
这里就可以看出来,“监督分类”需要预先经过“学习”的过程。我们必须亲眼见过不同的地物,学习和总结出它们各自所具有的“特征”,当我们下一次再看见的时候,才能根据“特征”对它们的属性做出判断。那么有没有一种方法,能跳过学习的步骤,直接对图像分类?这就是下面要介绍的“非监督分类”方法。
对于计算机而言,图像就是一个个的二维矩阵,包含着每个像素的亮度值。要让计算机对图像进行分类,肯定是要对像素的亮度值做文章的。那么计算机怎么判断不同的类别呢?依据什么规则来为每个像素划分类别呢?属于相同类别的像元,在光谱特征上是相似的,同理,属于不同类别的像元,在光谱特征上是有差别的。如果我们需要把一幅图像分为m类,那么我们完全可以先假设能够代表这m类的“特征值”,然后设法求出待分类的像素的值与每个类的“特征值”的“差异”。按照这段话开头的规律,差异越小,这个像元属于这个类的概率就越大,最终我们就取差异最小的所对应的类别,作为这个像元最终的分类结果。
那么现在大方向基本是定下来了,但是还是有一些细节需要考虑。比如代表不同类别的“特征值”到底怎么来确定?“差异”又如何衡量?最初我们肯定是很难知道不同类别的“特征值”到底是多少,但是又不能随便假设,不然分类结果大概率就会变得很糟糕。因此我们可以采用一种“迭代”的思想,先作假设,然后再设法逐次逼近。比如我们先假定m个类的特征值为U1、U2、...、Um, 然后利用它们先为所有像元分一次类,这样的话每一个类里边都有若干像元了,然后再计算每一个类新的特征值,然后利用新的特征值又为所有像元分一次类,.... ,如此反复,直到满足一个条件,就认为找到了每一个类最接近现实的特征值。这个条件是什么呢?如果一个分类任务完成的效果好、精度高,那么属于同一个类别的像素很相似、属于不同类别的像素差异很大。当这种"相似"和"差异"的程度趋于稳定时,迭代就收敛了,能够尽可能地接近真实情况。
那么现在来想想“差异”怎么衡量。其实我们可以利用统计方法,比如计算均值来代表不同类别的特征,然后求每一个像元的值到这些特征的欧式距离,来衡量像元属于某个类的概率。
引言中我们逐步讨论了非监督分类的基本方法,那么下面介绍一种具体的方法FCM(模糊C均值聚类方法),具体的原理如下:
聚类之详解FCM算法原理及应用_on2way的博客-CSDN博客_fcm聚类https://blog.csdn.net/on2way/article/details/47087201
算法的流程简单来说就是:
(1)初始化隶属度矩阵U。要注意约束条件,即一个像元属于所有类别的概率之和应当为1。
(2)利用初始化的隶属度计算聚类中心C。
(3)利用计算得到的聚类中心,更新隶属度矩阵U。
(4)反复执行(2)、(3)步,直到满足迭代次数,退出循环。或者计算目标函数J,如果相邻两次迭代计算得到的目标函数J之差小于设定值,则认为收敛,退出循环(后者更科学)。
我使用C#实现了这个算法,下面具体展示一下代码供大家参考。
使用随机数生成初始的隶属度矩阵,并且考虑到约束条件,把生成的随机隶属度归一化到【0,1】区间。
pixels ---- 像素总数
class_num -----类别数
public double[,] initializtion_U(int pixels, int class_num)
{
double[,] U = new double[pixels, class_num];
double[] sum = new double[pixels];
//生成随机数并求出每一行的和
Random ran = new Random();
for (int i = 0; i < pixels; i++)
for (int j = 0; j < class_num; j++)
{
U[i, j] = ran.Next(0, 100);
sum[i] += U[i,j];
}
//把概率归一化到【0,1】区间
for (int i = 0; i < pixels; i++)
for (int j = 0; j < class_num; j++)
{
U[i, j] = U[i, j] / sum[i];
}
return U;
}
由于在计算隶属度矩阵时,需要计算两个向量之间的欧式距离,所以单独用一个函数来实现这个功能。
X ---- 图像的光谱向量 ,定义为一个大小为pixels * bands(像素总数*波段数)的二维矩阵。
C ----聚类中心向量,同样是一个二维矩阵,大小为class_num*bands(分类的类别数*波段数)。
dis ---- 向量之间的距离
public double dist(int[,] X, double[,] C, int bands, int j, int i)
{
double dis = 0.0;
double temp = 0.0;
for (int band = 0; band < bands; band++)
{
temp += Math.Pow((X[j, band] - C[i, band]), 2);
}
dis = Math.Sqrt(temp);
return dis;
}
< input >
dt --- 将图像信息以GDAL库中的Dataset数据类型来存储
m --- 模糊因子,取值要大于1
class_num ---分类的类别数
loop --- 最大迭代次数,作为退出循环的条件
filepath --- 分类后的灰度图像保存路径
< output >
分类后的灰度图像
// 非监督分类模糊聚类算法(FCM)
public void FCM(Dataset dt, double m, int class_num, int loop,string filepath)
{
basicFunc bc = new basicFunc();
List data = bc.getvalue(dt);
int pixels = data[0].Length;
int bands = data.Count;
// 构建光谱特征向量 [像元,波段号]
int[,] X = new int[pixels, bands];
//获取向量
for (int i = 0; i < pixels; i++)
{
for (int j = 0; j < bands; j++)
{
X[i, j] = data[j][i];
}
}
//初始化隶属度矩阵
double[,] U = initializtion_U(pixels, class_num);
//聚类中心矩阵
double[,] C = new double[class_num, bands];
//迭代
for (int k = 0; k <= loop; k++)
{
//计算聚类中心 C(i,b) [class,bands]
for (int band = 0; band < bands; band++)
{
for (int i = 0; i < class_num; i++)
{
double u_sum = 0.0;
double xu_sum = 0.0;
for (int j = 0; j < pixels; j++)
{
u_sum += Math.Pow(U[j, i], m);
}
for (int j = 0; j < pixels; j++)
{
xu_sum += Math.Pow(U[j, i], m) * X[j, band];
}
C[i, band] = xu_sum / u_sum;
}
}
//更新隶属度矩阵U
for (int i = 0; i < class_num; i++)
{
for (int j = 0; j < pixels; j++)
{
double trans = 0.0;
for (int kk = 0; kk < class_num; kk++)
{
trans += Math.Pow((dist(X, C, bands, j, i) / dist(X, C, bands, j, kk)), (2 / (m - 1)));
}
U[j, i] = 1 / trans;
}
}
}
//获取每个像元所属的类别
int[] classes = new int[pixels];
for (int j = 0; j < pixels; j++)
{
int classflag = 0;
double max = U[j, 0];
for (int c = 1; c < class_num; c++)
{
if (U[j, c] > max)
{
max = U[j, c];
classflag = c;
}
}
classes[j] = classflag;
}
//给分类后图像赋色
int[] value = new int[pixels];//赋值后的每个像素值的集合
if (class_num == 2)//如果分为2类,则像素值分别赋值为0、255
{
for (int j = 0; j < pixels; j++)
{
if (classes[j] == 0)
value[j] = 0;
if (classes[j] == 1)
value[j] = 255;
}
}
//如果分类次数大于2类,则第0类为0,第(class_num-1)类为255,剩余类的像素值通过等分0-255这个范围得到
int[] temp1 = new int[class_num];
temp1[0] = 0;
temp1[class_num - 1] = 255;
if (class_num > 2)
{
for (int i = 1; i < class_num - 1; i++)
{
temp1[i] = temp1[i - 1] + 256 / (class_num - 1);
}
for (int j = 0; j < pixels; j++)
{
value[j] = temp1[classes[j]];
}
}
//保存
saveFiles sf = new saveFiles();
List dns = new List {value};
sf.SaveFromDataset(dt, filepath, dns, false);
}
注:还需要准备读取图像、保存图像的函数。
设置分类数为5,模糊指数为2,迭代次数为20次。
将这张分类后的灰度图像伪彩色显示,能更明显地看出来分类的效果。
目前来看,代码也存在以下一些问题:
(1)运行效率有限。如果迭代次数设置过多或者是图像尺寸太大,需要等待比较久才能得出分类结果。或许可以尝试一下“分块”处理的思想,来提高计算速度。
(2)分类精度有限。从分类结果可以看出,存在诸多错分、漏分现象。说明单纯依靠光谱特征来分类,不足以很好地区分不同类别的地物。或许可以通过增加特征向量的维度,比如说添加DEM数据、纹理结构数据等,来增大区分度。
(3)本文的算法在迭代次数达到设定值后就退出循环并输出分类结果。后来一想这种方法其实不太科学,因为这样做不能保证迭代是收敛的。应当计算相邻两次迭代得到的目标函数J的差值,如果这个差值小于设定的某个值(比如0.01),那么就认为收敛了。
由于个人水平所限,文章内容难免有原理或者表达上的错误,恳请大家批评指正,一起交流进步。最后特别感谢hhr和zqh两位partner的帮助。
查看完整算法请移步: GitHub - RefineM/DIP-GeoStudio: An image processing program based on the. NET framework, using GDAL.An image processing program based on the. NET framework, using GDAL. - GitHub - RefineM/DIP-GeoStudio: An image processing program based on the. NET framework, using GDAL.https://github.com/RefineM/DIP-GeoStudio.git
(1) AID Data Set 下载地址:https://pan.baidu.com/s/1mifOBv6#list/path=%2F
(2) NWPU-VHR 下载地址:https://pan.baidu.com/s/1hqwzXeG#list/path=%2F