表示和描述
- 0. 前言
- 1. 表示
- 1.1 边界追踪
- 1.2 链码
- 1.3 使用最小周长多边形的多边形近似
 
- 2. 边界描绘子
- 2.1 一些简单的描绘子
- 2.2 形状数
- 2.3 傅里叶描绘子
- 2.4 统计矩
 
- 3. 区域描绘子
- 3.1 一些简单的描绘子
- 3.2 拓扑描绘子
- 3.3 纹理
 
- Opencv补充:
 
0. 前言
本章只学习了前三节……
VS安装Image watch插件请查看官网OpenCV: Image Watch: viewing in-memory images in the Visual Studio debugger
第三版教材中图片下载地址: book images downloads
vs2019配置opencv可以查看:VS2019 & Opencv4.5.4配置教程
前情回顾:
 《数字图像处理》第三章 灰度变换和空间滤波 学习笔记附部分例子代码
 《数字图像处理》第四章 频率域滤波 学习笔记附部分例子代码
 数字图像处理第五章 图像复原和重建(内容较简单,就没有详细记录笔记)
 《数字图像处理》第六章 彩色图像处理 学习笔记附部分例子代码
 《数字图像处理》第七章 小波域多分辨率处理 学习笔记附部分例子代码
 数字图像处理第八章 图像压缩 非重点
 《数字图像处理》第九章 形态学图像处理 学习笔记附部分例子代码
 《数字图像处理》第十章 图像分割 学习笔记附部分例子代码
1. 表示
1.1 边界追踪
处理的是二值图像,其目标和背景点分别标为1和0,Moore边界追踪算法的步骤如下:
-  找到图像左上角为1的点b0为边界起始点。b0左边的点为c0,从c0开始按顺时针方向考察b0的8邻域,找到的第一个值1的点为b1,令扫描到b1前的点为c1。 
-  赋值b=b1,c=c1。 
-  从c开始顺时针方向行进,找到第一个值1的点nk,其之前的点均为背景点。 
-  赋值b=nk,c=nk-1。 
-  重复step3和step4,直到b=b0且下一个边界点为b1。 
1.2 链码
链码用于表示由顺次连接的具有指定长度和方向的直线段组成的边界
使用了opencv的findContours()和drawContours(),具体形参的表示可以看补充
void ch11_test01(string path) {
    Mat image = imread(path, IMREAD_GRAYSCALE);
    if (image.empty()) {
        cout << "Unable to load the image\n";
        return;
    }
    // 创建一个纯黑画布
    Mat black(image.size(), CV_8U, Scalar(0));
    Mat filtered;
    blur(image, filtered, Size(9, 9));
    Mat result = Otsu(filtered);//该函数的实现请看第十章笔记的3.2节
    // 寻找轮廓
    vector<vector<Point>> contours;
    findContours(result, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
    vector<Point> contour = contours[0];
    vector<int> freemaCode;
    Point currentPoint = contour[0];
    Point nextPoint;
    for (size_t i = 1; i < contour.size(); i++) {
        nextPoint = contour[i];
        // 计算方向
        int dx = nextPoint.x - currentPoint.x;
        int dy = nextPoint.y - currentPoint.y;
        // 转换为佛雷曼链码
        int code = -1;
        if (dx == 0 && dy == -1)      code = 0;
        else if (dx == 1 && dy == -1) code = 1;
        else if (dx == 1 && dy == 0)  code = 2;
        else if (dx == 1 && dy == 1)  code = 3;
        else if (dx == 0 && dy == 1)  code = 4;
        else if (dx == -1 && dy == 1) code = 5;
        else if (dx == -1 && dy == 0) code = 6;
        else if (dx == -1 && dy == -1)code = 7;
        // 添加到链码序列
        freemaCode.push_back(code);
        // 更新当前点
        currentPoint = nextPoint;
    }
    // 输出佛雷曼链码
    cout << "Freeman Chain Code: ";
    for (int code : freemaCode) {
        cout << code << " ";
    }
    cout << endl;
    // 在黑布上画出轮廓
    drawContours(black, contours, -1, Scalar(128), 2);
    displayImg(image, "原图01");
    displayImg(black, "轮廓");
    waitKey(0);
}

1.3 使用最小周长多边形的多边形近似
数字边界可以用多边形以任意精度来近似。对于一条闭合边界,当多边形的边数等于边界上的点数时,这种近似会变得很精确,此时,每对相邻的点定义了多边形的一条边。多边形近似的目的是使用尽可能少的线段数来获取给定边界的基本形状。
2. 边界描绘子
2.1 一些简单的描绘子
 
边界的长度是其最简单的描绘子之一。边界B的直径定义为
D i a m ( B ) = m a x [ D ( p i , p j ) ] Diam(B)=max[D(p_i, p_j)] Diam(B)=max[D(pi,pj)]
D是距离度量,pi和pj是边界上的两点。该直线段称为边界的长轴,边界的短边定义为与长轴垂直的直线,长轴与短轴之比称为边界的偏心率,偏心率同样是一个描述子。有时使用相邻边界线段的斜率差作为两条线段交点处曲率的描述子。
2.2 形状数
链码边界的一次差分取决于起始点。形状数的阶n定义为该链码的数字个数,形状编码不知道怎么来的,差分可看下图(方向按逆时针排序):
2.3 傅里叶描绘子
边界由坐标序列s(k)=[x(k), y(k)]表示,每个坐标对可以当做一个复数来处理
s ( k ) = x ( k ) + j y ( k ) s(k) = x(k) + jy(k) s(k)=x(k)+jy(k)
对sk进行离散傅里叶变换,使用前P个傅里叶系数进行逆变换得到
s ^ ( k ) = 1 K ∑ u = 0 P − 1 a ( u ) e j 2 π u k / P \hat s(k)=\frac{1}{K}\sum_{u=0}^{P-1}a(u)e^{j2\pi uk/P} s^(k)=K1u=0∑P−1a(u)ej2πuk/P
由第四章得知,高频分量说明精细细节,而低频分量决定全局形状,因此,P越小,边界丢失的细节就越多。傅里叶描绘子对起始点不敏感,对平移、旋转和尺度变化不敏感。
这里教材的例子,代码复现一直有问题555555,所以就放弃放代码了
2.4 统计矩
对于一段边界,连接两个端点连接起来,然后旋转至水平得到g(r)

看成一个幅度直方图,纵坐标p(v)是v出现的概率估计,所以关于其均值的v的第n阶矩阵为
μ n ( v ) = ∑ i = 0 A − 1 ( v i − m ) n p ( v i ) \mu _n(v)=\sum_{i=0}^{A-1}(v_i-m)^{n}p(v_i) μn(v)=i=0∑A−1(vi−m)np(vi)
式中, m = ∑ i = 0 A − 1 v i p ( v i ) m= \sum_{i=0}^{A-1}v_i p(v_i) m=∑i=0A−1vip(vi)
m为v的均值或平均值, μ 2 \mu_2 μ2为v的方差
3. 区域描绘子
3.1 一些简单的描绘子
一个区域的面积定义为该区域中像素的数量,区域的周长是其边界的长度。致密性描绘子圆度率有下式表示:
R c = 4 π A P 2 R_c =\frac{4\pi A}{P^2} Rc=P24πA
A为讨论区域的面积,P是其周长
3.2 拓扑描绘子
拓扑学是研究未受任何变形影响的图形的性质,前提是该图形为被撕裂或粘连。区域描述有两点
-  区域内的孔洞数量H 
-  连通分量的数量C 
可以定义欧拉数E=C-H.
当描述有直线线段表示的区域非常简单,V表示顶点数,Q表示边数,F表示面数,那么欧拉公式如下:
V − Q + F = C − H V-Q+F=C-H V−Q+F=C−H
opencv可以使用connectedComponents()和connectedComponentsWithStats()两个函数,后者会得到stats这一5列矩阵,该矩阵的每一行对应一个连通区域的标签,包含有连通区域左上角的坐标x, y,以及外接矩形的宽高和面积。
void ch11_test03(string path) {
	Mat image = imread(path, IMREAD_GRAYSCALE);
	if (image.empty()) {
		cout << "Unable to load the image\n";
		return;
	}
	Mat binaryImage;
	threshold(image, binaryImage, 78, 255, THRESH_BINARY); //阈值选择78
	binaryImage = ~binaryImage;
	//Mat labeledImg;
	//int numLabels = connectedComponents(binaryImg, labeledImg, 8);
	//numLabels--;
	Mat labeledImage;
	Mat stats, centroids;
	int numLabels = connectedComponentsWithStats(binaryImage, labeledImage, stats, centroids, 8);
	// 减去背景区域
	numLabels--;
	cout << "连通个数:" << numLabels << endl;
	//displayImg(labeledImage, "标记结果");
	displayImg(image, "原图");
	displayImg(binaryImage, "阈值处理结果");
	waitKey(0);
}


3.3 纹理
统计方法: 一个区域的灰度级直方图的统计矩,如同3.1。二阶矩在纹理描述中特别重要,度量:
R ( z ) = 1 − 1 1 − σ 2 ( z ) R(z)=1-\frac{1}{1-\sigma ^2(z)} R(z)=1−1−σ2(z)1
可以提现图像的平滑程度。
而三阶矩:
μ 3 ( v ) = ∑ i = 0 A − 1 ( v i − m ) 3 p ( v i ) \mu _3(v)=\sum_{i=0}^{A-1}(v_i-m)^{3}p(v_i) μ3(v)=i=0∑A−1(vi−m)3p(vi)
是直方图偏斜度的度量。
“一致性”度量:
U ( z ) = ∑ L − 1 i = 0 p 2 ( z i ) U(z)=\sum_{L-1}^{i=0}p^2(z_i) U(z)=L−1∑i=0p2(zi)
平均熵度量:
e ( z ) = ∑ i = 0 L − 1 p ( z i ) l o g 2 p ( z i ) e(z)=\sum_{i=0}^{L-1}p(z_i)log_2p(z_i) e(z)=i=0∑L−1p(zi)log2p(zi)
令 Q 是定义两个像素彼此相对位置的一个算子,并考虑一幅具有L个可能灰度级的图像f。 令 G 为一个矩阵,其元素gij是灰度为zi和zj的像素对出现在f中由Q所指定的位置处的次数。按这种方法形成的矩阵称为灰度级共生矩阵。

图像中可能的灰度级决定了G的大小。
ch11_test04("..\\Images_CH11\\Fig1130(a)(uniform_noise).tif", "a"); //图a显示时需要归一化到0-255
ch11_test04("..\\Images_CH11\\Fig1130(b)(sinusoidal).tif", "b"); //显示时归一化到0-1
ch11_test04("..\\Images_CH11\\Fig1130(c)(cktboard_section).tif", "c");//显示时归一化到0-255
//计算共生矩阵,像素对(水平方向一格)
Mat calGLCM(Mat input, int level) {
	Mat result = Mat::zeros(Size(level, level), CV_32F);
	//遍历水平像素对
	for (int x = 0; x + 1 < input.cols; x++) {
		for (int y = 0; y < input.rows; y++) {
			int scale1 = input.at<uchar>(y, x);		//at.(row, col)
			int scale2 = input.at<uchar>(y, x + 1);
			result.at<float>(scale1, scale2)++;
		}
	}
	return result;
}
void ch11_test04(string path, string info) {
	Mat image = imread(path, IMREAD_GRAYSCALE);
	if (image.empty()) {
		cout << "Unable to load the image\n";
		return;
	}
	Mat GLCM = calGLCM(image, 256);
	//normalize(GLCM, GLCM, 0, 1, NORM_MINMAX);
	displayImg(image, "原图04" + info);
	displayImg(GLCM, "共生矩阵" + info);
	waitKey(0);
}


Opencv补充:
-  void findContours(InputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method)-  image: 输入的二值化图像,通常是通过阈值处理得到的,要求是8位单通道图像。
-  contours: 输出参数,包含检测到的轮廓的容器,通常是vector<vector<Point>>类型。
-  hierarchy: 输出参数,轮廓的层次结构,通常是vector<Vec4i>类型。可以为可选参数,如果不需要层次结构,可以设置为noArray()。
-  mode: 轮廓检索模式,可以是RETR_EXTERNAL(仅检测外部轮廓)、RETR_LIST(检测所有轮廓,不建立层次关系)、RETR_CCOMP(检测所有轮廓,建立两层层次关系)、RETR_TREE(检测所有轮廓,建立层次树结构)。
-  method: 轮廓逼近方法,可以是CHAIN_APPROX_NONE(保存所有的轮廓点)、CHAIN_APPROX_SIMPLE(压缩水平、垂直、对角方向的元素,只保留其端点)
 
-  
-  void drawContours(InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar& color, int thickness = 1, int lineType = LINE_8);-  image: 要绘制轮廓的图像。
-  contours: 保存轮廓点的容器,通常是vector<vector<Point>>类型
-  contourIdx: 要绘制的轮廓的索引。如果是负数,表示绘制所有轮廓。
-  color: 绘制轮廓的颜色,通常使用Scalar类表示,例如Scalar(0, 255, 0)表示绿色。
-  thickness: 绘制轮廓线的粗细,如果是负数表示填充轮廓内部。默认值是1。
-  lineType: 绘制轮廓的线型,可以是LINE_8、LINE_4或LINE_AA等
 );`
-  image: 要绘制轮廓的图像。
-  contours: 保存轮廓点的容器,通常是vector<vector<Point>>类型
-  contourIdx: 要绘制的轮廓的索引。如果是负数,表示绘制所有轮廓。
-  color: 绘制轮廓的颜色,通常使用Scalar类表示,例如Scalar(0, 255, 0)表示绿色。
-  thickness: 绘制轮廓线的粗细,如果是负数表示填充轮廓内部。默认值是1。
-  lineType: 绘制轮廓的线型,可以是LINE_8、LINE_4或LINE_AA等
 
-  



















