1、细化简介
图像的细化主要是针对二值图而言。所谓细化,也就是从原来的图中去掉一些点,但仍要保持原来的形状,实际上是保持原图的骨架,将其细化为一个像素宽的线条的处理过程。
图像细化的算法有很多种,具体可以分为迭代法和非迭代法,迭代法根据其运算时是否并行处理像素,又分为串行算法和并行算法。在并行算法中,像素点的删除与图像中像素值的顺序无关,而仅仅取决于上一次迭代的结果,串行算法中,是否删除像素不仅与上一次迭代的结果有关,而且与当前像素值的分布也有关系。目前,比较典型的算法又 ZS 图像细化算法,LW 细化算法和EPTA 算法。ZS 算法是一种基于8邻域的并行细化算法,该算法是目前应用最为广泛的算法,其突出的优点是算法效率高,算法的迭代次数少,而且对于直线等要素经过细化处理后能够保持和原图像一致的形状。LW算法改进了ZS算法会丢失局部信息的问题,但产生了多余分叉的问题。EPTA 是对 ZS 算法和 LW 算法的增强,改善了 ZS 和 LW 算法存在的一些问题,但存在对于部分图像细化不完全的问题。
ZS 算法、LW算法和EPTA算法都是基于目标像素的8邻域,8领域表示如图1所示,目标像素的值为1,背景像素的值为0,在目标像素的8邻域内,根据像素值分布的不同,可以分为端点、孤立点和内部点。
判断一个点是否能去掉是以其8个相邻点(八连通)的情况来作为判据的,八邻域图如图1所示。
图1 目标像素的8邻域范围
具体判据为:
(1)内部点不能删除;
(2)孤立点不能删除;
(3)直线端点不能删除;
图2 特殊点示意图
下面主要介绍两种细化算法:
2、ZS 细化算法
ZS 算法是一种基于8邻域的并行细化算法,通过对目标像素8邻域进行分步的算术逻辑运算,来确定该目标像素是否删除,细化过程如下:
第一次迭代的过程中,如果P1的值满足以下四个条件,则删除P1的值,将P1的值置为0:
对于二值图像中的每一个像素点来说,它可能为孤立点、道路的内部点或者端点。将像素点P1的8邻域值P2-P9相加,若其和大于等于2,说明P1点不是端点或者孤立的点,若其和小于等于6,则保证P1不是内部点。如果像素点P1的8邻域值的和满足上述条件(1),P1一定为边界点。S(P1)表示目标像素P1的8邻域中,顺时针变化一周像素由0变1的次数。在目标点8邻域P2-P9的范围内,像素值由0变1的次数只能为1次。在图3中,P2与P3以及P6和P7出现了0和1直接相连,此时若删除P1则P3与P8不再相连,细化后道路的连通性受到影响。条件(2)保证了图像细化后的连通性。
图3 模式示意图
第二次迭代中,像素点如果满足第一次迭代中的条件(1)和(2)及以下条件,则移除该像素点:
重复以上迭代过程,直到处理完所有像素点,此时,细化完成。
3、查表法
由于输入的图像是一张二值图,经过简单的数据处理将其归一化为像素值只有0和1的图像,然后对其进行卷积操作。
举一个特征点及其八邻域说明具体操作:
目标点八邻域 卷积核
将目标点的八邻域和和卷积核进行点乘,然后将所有值相加即可得表的索引 M;
然后实用上述计算的表的索引值M去找表中对应的值,表中对应得值为0/1,就把目标点的像素值修改为0/1(0为可删除,1为不可删除);
使用上述方法,从上到下,从左到右进行扫描,然后对目标点进行查表、修改目标像素值,最后得到细化结果。表即为得一维向量,其中N受卷积核所有元素之和的影响。
4、实验分析
(1)查表法代码
说明:非常抱歉,下面贴的代码是我从细化到矢量化完整的工程(完整工程见下面链接)里面剪切出来的,之前没有调试就放上去了;今天试了下,发现里面有一些小问题,所以对其进行了修改,下面是修改后的代码;读者可以把代码复制过去后,只需要自己配一下 opencv 即可运行!!!
#include<iostream>
#include <opencv2\opencv.hpp>
using namespace std;
using namespace cv;
//查表法//
Mat lookUpTable(Mat& mat, int lut[])
{
Mat mat_in;
mat.convertTo(mat_in, CV_16UC1); //8 转 16
int MatX = mat_in.rows;
int MatY = mat_in.cols;
int num = 512;
//表的维数和卷积核中的数据有关,小矩阵初始化按行赋值
Mat kern = (Mat_<int>(3, 3) << 1, 8, 64, 2, 16, 128, 4, 32, 256); //卷积核
Mat mat_out = Mat::zeros(MatX, MatY, CV_16UC1);
Mat mat_expend = Mat::zeros(MatX + 2, MatY + 2, CV_16UC1);
Rect Roi(1, 1, MatY, MatX); //(列,行,列,行)
Mat mat_expend_Roi(mat_expend, Roi); //确定扩展矩阵的Roi区域
mat_in.copyTo(mat_expend_Roi); //将传入矩阵赋给Roi区域
Mat Mat_conv;
//实用卷积核和和每一个八邻域进行点乘再相加,其结果为表的索引,对应值为0能去掉,为1则不能去掉
filter2D(mat_expend, Mat_conv, mat_expend.depth(), kern); //卷积
Mat mat_index = Mat_conv(Rect(1, 1, MatY, MatX));
for (int i = 0; i < MatX; i++)
{
for (int j = 0; j < MatY; j++)
{
int matindex = mat_index.at<short>(i, j);
if ((matindex < num) && (matindex > 0))
{
mat_out.at<short>(i, j) = lut[matindex];
}
else if (matindex > num)
{
mat_out.at<short>(i, j) = lut[num - 1];
}
}
}
return mat_out;
}
//道路细化查表法//
Mat img_bone(Mat& mat)
{
// mat 为细化后的图像
Mat mat_in = mat;
//在数字图像处理时,只有单通道、三通道 8bit 和 16bit 无符号(即CV_16U)的 mat 才能被保存为图像
mat.convertTo(mat_in, CV_16UC1);
int lut_1[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
//int lut_1[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1,
// 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
// 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
int lut_2[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1 };
//int lut_2[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0,//34
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1,
// 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
// 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1,
// 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1 };
Mat mat_bool;
threshold(mat_in, mat_bool, 0, 1, THRESH_BINARY); //二值图像归一化
Mat mat_out;
Mat image_iters;
while (true)
{
mat_out = mat_bool;
//查表:水平、垂直
image_iters = lookUpTable(mat_bool, lut_1);
mat_bool = lookUpTable(image_iters, lut_2);
Mat diff = mat_out != mat_bool;
//countNonZero函数返回灰度值不为0的像素数
bool mat_equal = countNonZero(diff) == 0; //判断图像是否全黑
if (mat_equal)
{
break;
}
}
Mat Matout;
mat_bool.convertTo(Matout, CV_8UC1);
return Matout;
}
//主函数
int main()
{
Mat src_img, src_imgBool;
//输入道路二值图,参数 0 是指imread按单通道的方式读入图像,即灰白图像
src_img = imread("..\\testImage\\labels.png", 0);
//去掉噪,例如过滤很小或很大像素值的图像点
threshold(src_img, src_imgBool, 0, 255, THRESH_OTSU);
Mat imgbone = img_bone(src_imgBool);
//保存结果
imwrite(".\\image_save\\roadThin.png", imgbone * 255);
system("pause");
return 0;
}
(2)查表法细化结果
说明:由上图细化结果可以看出,细化结果中多处出现毛刺和小孔洞。
(3)优化后细化结果
(4)效率对比
说明:从上面的实验结果可以看出,优化前二值图细化结果有少许毛刺和孔洞,优化后不仅消除了这些缺点,而且在效率上也大大提高;
优化后的代码见:https://download.csdn.net/download/weixin_47156401/73478989