二值图像骨架线提取
二值图像骨架线提取
- HilditchThin算法
- Rosenfeld算法
- OpenCV_Contrib中的算法
- 示例
- 其他细化算法
- 查表法
- HilditchThin的另一种算法
- 参考
二值图像骨架线提取算法:HilditchThin算法、Rosenfeld算法、OpenCV_Contrib中的算法
HilditchThin算法
1、使用的8邻域标记为:
2、下面看下它的算法描述:
复制目地图像到临时图像,对临时图像进行一次扫描,对于不为0的点,如果满足以下四个条件,则在目地图像中删除该点(就是设置该像素为0)
条件一:2<=p2+p3+p4+p5+p6+p7+p8+p9<=6
大于等于2会保证p1点不是端点或孤立点,因为删除端点和孤立点是不合理的,小于等于6保证p1点是一个边界点,而不是一个内部点。等于0时候,周围没有等于1的像素,所以p1为孤立点,等于1的时候,周围只有1个灰度等于1的像素,所以是端点(注:端点是周围有且只能有1个值为1的像素)。

条件二:p2->p9的排列顺序中,01模式的数量为1
比如下面的图中,有p2p3 => 01, p6p7=>01,所以该像素01模式的数量为2。

之所以要01模式数量为1,是要保证删除当前像素点后的连通性。比如下面的图中,01模式数量大于1,如果删除当前点p1,则连通性不能保证。

条件三:p2.p4.p8 = 0 or A(p2)!=1
A(p2)表示p2周围8邻域的01模式和。这个条件保证2个像素宽的垂直条不完全被腐蚀掉。

条件四:p2.p4.p6 = 0 or A(p4)!=1
A(p4)表示p4周围8邻域的01模式和。这个条件保证2个像素宽的水平条不完全被腐蚀掉。

如果图像中没有可以删除的点时,结束循环。
算法代码如下(输入归一化二值图像):
void HilditchThin(cv::Mat& src, cv::Mat& dst) //传入归一化图像src
{if(src.type()!=CV_8UC1){printf("只能处理二值或灰度图像\n");return;}//非原地操作时候,copy src到dstif(dst.data!=src.data)src.copyTo(dst);int i, j;int width, height;//之所以减2,是方便处理8邻域,防止越界width = src.cols -2;height = src.rows -2;int step = src.step;int p2,p3,p4,p5,p6,p7,p8,p9;uchar* img;bool ifEnd;int A1;cv::Mat tmpimg;while(1){dst.copyTo(tmpimg);ifEnd = false;img = tmpimg.data+step;for(i = 2; i < height; i++){img += step;for(j =2; j<width; j++){uchar* p = img + j;A1 = 0;if( p[0] > 0){if(p[-step]==0&&p[-step+1]>0) //p2,p3 01模式{A1++;}if(p[-step+1]==0&&p[1]>0) //p3,p4 01模式{A1++;}if(p[1]==0&&p[step+1]>0) //p4,p5 01模式{A1++;}if(p[step+1]==0&&p[step]>0) //p5,p6 01模式{A1++;}if(p[step]==0&&p[step-1]>0) //p6,p7 01模式{A1++;}if(p[step-1]==0&&p[-1]>0) //p7,p8 01模式{A1++;}if(p[-1]==0&&p[-step-1]>0) //p8,p9 01模式{A1++;}if(p[-step-1]==0&&p[-step]>0) //p9,p2 01模式{A1++;}p2 = p[-step]>0?1:0;p3 = p[-step+1]>0?1:0;p4 = p[1]>0?1:0;p5 = p[step+1]>0?1:0;p6 = p[step]>0?1:0;p7 = p[step-1]>0?1:0;p8 = p[-1]>0?1:0;p9 = p[-step-1]>0?1:0;//计算AP2,AP4int A2, A4;A2 = 0;//if(p[-step]>0){if(p[-2*step]==0&&p[-2*step+1]>0) A2++;if(p[-2*step+1]==0&&p[-step+1]>0) A2++;if(p[-step+1]==0&&p[1]>0) A2++;if(p[1]==0&&p[0]>0) A2++;if(p[0]==0&&p[-1]>0) A2++;if(p[-1]==0&&p[-step-1]>0) A2++;if(p[-step-1]==0&&p[-2*step-1]>0) A2++;if(p[-2*step-1]==0&&p[-2*step]>0) A2++;}A4 = 0;//if(p[1]>0){if(p[-step+1]==0&&p[-step+2]>0) A4++;if(p[-step+2]==0&&p[2]>0) A4++;if(p[2]==0&&p[step+2]>0) A4++;if(p[step+2]==0&&p[step+1]>0) A4++;if(p[step+1]==0&&p[step]>0) A4++;if(p[step]==0&&p[0]>0) A4++;if(p[0]==0&&p[-step]>0) A4++;if(p[-step]==0&&p[-step+1]>0) A4++;}//printf("p2=%d p3=%d p4=%d p5=%d p6=%d p7=%d p8=%d p9=%d\n", p2, p3, p4, p5, p6,p7, p8, p9);//printf("A1=%d A2=%d A4=%d\n", A1, A2, A4);if((p2+p3+p4+p5+p6+p7+p8+p9)>1 && (p2+p3+p4+p5+p6+p7+p8+p9)<7 && A1==1){if(((p2==0||p4==0||p8==0)||A2!=1)&&((p2==0||p4==0||p6==0)||A4!=1)) {dst.at<uchar>(i,j) = 0; //满足删除条件,设置当前像素为0ifEnd = true;//printf("\n");//PrintMat(dst);}}}}}//printf("\n");//PrintMat(dst);//PrintMat(dst);//已经没有可以细化的像素了,则退出迭代if(!ifEnd) break;}
}
Rosenfeld算法
1、使用下面的八邻域表示法:

2、对于前景点像素p1, 如果p2=0,则p1 称作北部边界点。如果p6=0,p1称作南部边界点,p4=0,p1称作东部边界点,p8=0,p1称作西部边界点。

p1周围8个像素的值都为0,则p1为孤立点,如果周围8个像素有且只有1个像素值为1,则此时p1称作端点。
3、另外还要了解的一个概念就是8 simple。就是我们把p1的值设置为0后,不会改变周围8个像素的8连通性。下面的三个图中,如果p1=0后,则会改变8连通性。

而下面的则不会改边8连通性,此时可以称像素p1是8 simple
4、Rosenfeld细化算法描述如下:
- 扫描所有像素,如果像素是北部边界点,且是8simple,但不是孤立点和端点,删除该像素。
- 扫描所有像素,如果像素是南部边界点,且是8simple,但不是孤立点和端点,删除该像素。
- 扫描所有像素,如果像素是东部边界点,且是8simple,但不是孤立点和端点,删除该像素。
- 扫描所有像素,如果像素是西部边界点,且是8simple,但不是孤立点和端点,删除该像素。
- 执行完上面4个步骤后,就完成了一次迭代,我们重复执行上面的迭代过程,直到图像中再也没有可以删除的点后,退出迭代循环。
5、算法代码如下(输入归一化二值图像):
void Rosenfeld(Mat& src, Mat& dst)//输入的目标像素为1,背景像素为0
{if (src.type() != CV_8UC1){printf("只能处理二值或灰度图像\n");return;}//非原地操作时候,copy src到dstif (dst.data != src.data){src.copyTo(dst);}int i, j, n;int width, height;//之所以减1,是方便处理8邻域,防止越界width = src.cols - 1;height = src.rows - 1;int step = src.step;int p2, p3, p4, p5, p6, p7, p8, p9;uchar* img;bool ifEnd;Mat tmpimg;int dir[4] = { -step, step, 1, -1 };while (1){//分四个子迭代过程,分别对应北,南,东,西四个边界点的情况ifEnd = false;for (n = 0; n < 4; n++){dst.copyTo(tmpimg);img = tmpimg.data;for (i = 1; i < height; i++){img += step;for (j = 1; j < width; j++){uchar* p = img + j;//如果p点是背景点或者且为方向边界点,依次为北南东西,继续循环if (p[0] == 0 || p[dir[n]] > 0) continue;p2 = p[-step] > 0 ? 1 : 0;p3 = p[-step + 1] > 0 ? 1 : 0;p4 = p[1] > 0 ? 1 : 0;p5 = p[step + 1] > 0 ? 1 : 0;p6 = p[step] > 0 ? 1 : 0;p7 = p[step - 1] > 0 ? 1 : 0;p8 = p[-1] > 0 ? 1 : 0;p9 = p[-step - 1] > 0 ? 1 : 0;//8 simple判定int is8simple = 1;if (p2 == 0 && p6 == 0){if ((p9 == 1 || p8 == 1 || p7 == 1) && (p3 == 1 || p4 == 1 || p5 == 1))is8simple = 0;}if (p4 == 0 && p8 == 0){if ((p9 == 1 || p2 == 1 || p3 == 1) && (p5 == 1 || p6 == 1 || p7 == 1))is8simple = 0;}if (p8 == 0 && p2 == 0){if (p9 == 1 && (p3 == 1 || p4 == 1 || p5 == 1 || p6 == 1 || p7 == 1))is8simple = 0;}if (p4 == 0 && p2 == 0){if (p3 == 1 && (p5 == 1 || p6 == 1 || p7 == 1 || p8 == 1 || p9 == 1))is8simple = 0;}if (p8 == 0 && p6 == 0){if (p7 == 1 && (p9 == 1 || p2 == 1 || p3 == 1 || p4 == 1 || p5 == 1))is8simple = 0;}if (p4 == 0 && p6 == 0){if (p5 == 1 && (p7 == 1 || p8 == 1 || p9 == 1 || p2 == 1 || p3 == 1))is8simple = 0;}int adjsum;adjsum = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9;//判断是否是邻接点或孤立点,0,1分别对于那个孤立点和端点if (adjsum != 1 && adjsum != 0 && is8simple == 1){dst.at<uchar>(i, j) = 0; //满足删除条件,设置当前像素为0ifEnd = true;}}}}if (!ifEnd) break;}
}
OpenCV_Contrib中的算法
OpenCV细化算法大致描述如下:
1、复制源图,进行一次全图扫描,对于不为0的点,如果满足以下四个条件
- (1)保证当前点不是孤立点或端点
- (2)保证删除当前像素点后的连通性
- (3)上 * 右 * 下 = 0
- (4)左 * 右 * 下 = 0
- 在第一次子迭代中,只是移去东南的边界点,而不考虑西北的边界点。
2、把目地图像再次复制到临时图像,接着对临时图像进行一次扫描,如果不为0的点它的八邻域满足以下4个条件,则在目地图像中删除该点(就是设置该像素为0)
- (1) 周围8个点,2<= p2+p3+p4+p5+p6+p7+p8+p9<=6
- (2) p2->p9的排列顺序中,01模式的数量(这里假设二值图非零值为1)为1。
- (3) 左 * 右 * 上 = 0
- (4) 左 * 下 * 上 = 0
执行完上面两个步骤后,就完成了一次细化算法,我们可以多次迭代执行上述过程,得到最终的骨架图。
算法源代码(输入二值图,无需归一化,一般的:thinningType=ZHANGSUEN):
// Apply the thinning procedure to a given image
void thinning(InputArray input, OutputArray output, int thinningType) {Mat processed = input.getMat().clone();CV_CheckTypeEQ(processed.type(), CV_8UC1, "");// Enforce the range of the input image to be in between 0 - 255processed /= 255;Mat prev = Mat::zeros(processed.size(), CV_8UC1);Mat diff;do {thinningIteration(processed, 0, thinningType);thinningIteration(processed, 1, thinningType);absdiff(processed, prev, diff);processed.copyTo(prev);} while (countNonZero(diff) > 0);processed *= 255;output.assign(processed);
}// Applies a thinning iteration to a binary image
void thinningIteration(Mat img, int iter, int thinningType) {Mat marker = Mat::zeros(img.size(), CV_8UC1);if (thinningType == THINNING_ZHANGSUEN) {for (int i = 1; i < img.rows - 1; i++){for (int j = 1; j < img.cols - 1; j++){uchar p2 = img.at<uchar>(i - 1, j);uchar p3 = img.at<uchar>(i - 1, j + 1);uchar p4 = img.at<uchar>(i, j + 1);uchar p5 = img.at<uchar>(i + 1, j + 1);uchar p6 = img.at<uchar>(i + 1, j);uchar p7 = img.at<uchar>(i + 1, j - 1);uchar p8 = img.at<uchar>(i, j - 1);uchar p9 = img.at<uchar>(i - 1, j - 1);int A = (p2 == 0 && p3 == 1) + (p3 == 0 && p4 == 1) +(p4 == 0 && p5 == 1) + (p5 == 0 && p6 == 1) +(p6 == 0 && p7 == 1) + (p7 == 0 && p8 == 1) +(p8 == 0 && p9 == 1) + (p9 == 0 && p2 == 1);int B = p2 + p3 + p4 + p5 + p6 + p7 + p8 + p9;int m1 = iter == 0 ? (p2 * p4 * p6) : (p2 * p4 * p8);int m2 = iter == 0 ? (p4 * p6 * p8) : (p2 * p6 * p8);if (A == 1 && (B >= 2 && B <= 6) && m1 == 0 && m2 == 0)marker.at<uchar>(i, j) = 1;}}}if (thinningType == THINNING_GUOHALL) {for (int i = 1; i < img.rows - 1; i++){for (int j = 1; j < img.cols - 1; j++){uchar p2 = img.at<uchar>(i - 1, j);uchar p3 = img.at<uchar>(i - 1, j + 1);uchar p4 = img.at<uchar>(i, j + 1);uchar p5 = img.at<uchar>(i + 1, j + 1);uchar p6 = img.at<uchar>(i + 1, j);uchar p7 = img.at<uchar>(i + 1, j - 1);uchar p8 = img.at<uchar>(i, j - 1);uchar p9 = img.at<uchar>(i - 1, j - 1);int C = ((!p2) & (p3 | p4)) + ((!p4) & (p5 | p6)) +((!p6) & (p7 | p8)) + ((!p8) & (p9 | p2));int N1 = (p9 | p2) + (p3 | p4) + (p5 | p6) + (p7 | p8);int N2 = (p2 | p3) + (p4 | p5) + (p6 | p7) + (p8 | p9);int N = N1 < N2 ? N1 : N2;int m = iter == 0 ? ((p6 | p7 | (!p9)) & p8) : ((p2 | p3 | (!p5)) & p4);if ((C == 1) && ((N >= 2) && ((N <= 3)) & (m == 0)))marker.at<uchar>(i, j) = 1;}}}img &= ~marker;
}
示例
总结:其实几种算法的效果都差不多,但第一种算法在细化时会忽略4行4列图像数据,如果目标在边缘位置,最好使用后两种算法。
原图:

细化图:

直接使用细化算法可能有很多毛刺,你可以先删除突出部分,然后再细化会好很多。
其他细化算法
查表法
//查表法//
Mat lookUpTable(Mat& mat, int lut[])
{Mat mat_in;mat.convertTo(mat_in, CV_16UC1); //8 转 16int 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_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 };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;
}
HilditchThin的另一种算法
void cvHilditchThin(cv::Mat& src, cv::Mat& dst)
{if (src.type() != CV_8UC1){printf("只能处理二值或灰度图像\n");return;}//非原地操作时候,copy src到dstif (dst.data != src.data){src.copyTo(dst);}//8邻域的偏移量int offset[9][2] = { {0,0},{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1},{0,1},{1,1} };//四邻域的偏移量int n_odd[4] = { 1, 3, 5, 7 };int px, py;int b[9]; //3*3格子的灰度信息int condition[6]; //1-6个条件是否满足int counter; //移去像素的数量int i, x, y, copy, sum;uchar* img;int width, height;width = dst.cols;height = dst.rows;img = dst.data;int step = dst.step;do{counter = 0;for (y = 0; y < height; y++){for (x = 0; x < width; x++){//前面标记为删除的像素,我们置其相应邻域值为-1for (i = 0; i < 9; i++){b[i] = 0;px = x + offset[i][0];py = y + offset[i][1];if (px >= 0 && px < width && py >= 0 && py < height){// printf("%d\n", img[py*step+px]);if (img[py*step + px] == WHITE){b[i] = 1;}else if (img[py*step + px] == GRAY){b[i] = -1;}}}for (i = 0; i < 6; i++){condition[i] = 0;}//条件1,是前景点if (b[0] == 1) condition[0] = 1;//条件2,是边界点sum = 0;for (i = 0; i < 4; i++){sum = sum + 1 - abs(b[n_odd[i]]);}if (sum >= 1) condition[1] = 1;//条件3, 端点不能删除sum = 0;for (i = 1; i <= 8; i++){sum = sum + abs(b[i]);}if (sum >= 2) condition[2] = 1;//条件4, 孤立点不能删除sum = 0;for (i = 1; i <= 8; i++){if (b[i] == 1) sum++;}if (sum >= 1) condition[3] = 1;//条件5, 连通性检测if (func_nc8(b) == 1) condition[4] = 1;//条件6,宽度为2的骨架只能删除1边sum = 0;for (i = 1; i <= 8; i++){if (b[i] != -1){sum++;}else{copy = b[i];b[i] = 0;if (func_nc8(b) == 1) sum++;b[i] = copy;}}if (sum == 8) condition[5] = 1;if (condition[0] && condition[1] && condition[2] && condition[3] && condition[4] && condition[5]){img[y*step + x] = GRAY; //可以删除,置位GRAY,GRAY是删除标记,但该信息对后面像素的判断有用counter++;//printf("----------------------------------------------\n");//PrintMat(dst);}}}if (counter != 0){for (y = 0; y < height; y++){for (x = 0; x < width; x++){if (img[y*step + x] == GRAY)img[y*step + x] = BLACK;}}}} while (counter != 0);
}
参考
https://www.cnblogs.com/mikewolf2002/p/3321732.html
相关文章:
二值图像骨架线提取
二值图像骨架线提取HilditchThin算法Rosenfeld算法OpenCV_Contrib中的算法示例其他细化算法查表法HilditchThin的另一种算法参考二值图像骨架线提取算法:HilditchThin算法、Rosenfeld算法、OpenCV_Contrib中的算法 HilditchThin算法 1、使用的8邻域标记为ÿ…...
规划数据指标体系方法(上)——OSM 模型
之前我已经有写过文章讲了数据指标体系的搭建思路,但有同学还是不太清楚要从何入手,今天我就来跟大家讲一讲搭建数据指标体系之前第一步要先做的事情——规划数据指标体系。 规划数据指标体系,在业内有三种比较常见的方法,分别是&…...
做程序界中的死神,继续提升灵力上限
标题解读:标题中的死神,是源自《死神》动漫里面的角色,斩魂刀是死神的武器,始解是斩魂刀的初始解放形态,卐解是斩魂刀的觉醒解放形态,也是死神的大招。意旨做程序界中程序员的佼佼者,一步一步最…...
[数据结构]:11-冒泡排序(顺序表指针实现形式)(C语言实现)
目录 前言 已完成内容 冒泡排序实现 01-开发环境 02-文件布局 03-代码 01-主函数 02-头文件 03-PSeqListFunction.cpp 04-SortCommon.cpp 05-SortFunction.cpp 结语 前言 此专栏包含408考研数据结构全部内容,除其中使用到C引用外,全为C语言代…...
Java实验报告经验总结
每一段是每一次实验报告写的经验总结,一共是一学期的内容 文章目录一二三四五六一 ~~~~~分析:这次做程序中也出了不少问题,究其根本还是没有理解清楚各语句功能和其应用。 ~~~~~比如说:当我们在定义浮点数时,数字的后面…...
ESP32使用TCP HTTP访问API接口JSON解析获取数据
ESP32使用TCP HTTP访问API接口JSON解析获取数据API接口代码解析获取时间代码烧录效果总结API接口 单片机常用的API接口基本都是返回的一串JSON格式的数据,这里以ESP32联网获取时间信息作为获取API数据的示例,以便后续移植使用。 很多功能性的API接…...
spring security 实现自定义认证和登录(4):使用token进行验证
前面我们实现了给客户端下发token,虽然客户端拿到了token,但我们还没处理客户端下一次携带token请求时如何验证,我们想要实现拿得到token之后,只需要验证token,不需要用户再携带用户名和密码了。 1. 禁用 UsernamePass…...
戴眼镜检测和识别2:Pytorch实现戴眼镜检测和识别(含戴眼镜数据集和训练代码)
Pytorch实现戴眼镜检测和识别(含戴眼镜数据集和训练代码) 目录 Pytorch实现戴眼镜检测和识别(含戴眼镜数据集和训练代码) 1.戴眼镜检测和识别方法 2.戴眼镜数据集 3.人脸检测模型 4.戴眼镜分类模型训练 (1)项目安装 (2)准…...
信息收集之Google Hacking
Google HackingGoogleHacking作为常用且方便的信息收集搜索引擎工具,它是利用谷歌搜索强大,可以搜出不想被看到的后台、泄露的信息、未授权访问,甚至还有一些网站配置密码和网站漏洞等。掌握了Google Hacking基本使用方法,或许下一…...
【面试题】如何避免使用过多的 if else?
大厂面试题分享 面试题库前后端面试题库 (面试必备) 推荐:★★★★★地址:前端面试题库一、引言相信大家听说过回调地狱——回调函数层层嵌套,极大降低代码可读性。其实,if-else层层嵌套,如下图…...
oneblog_justauth_三方登录配置【Gitee】
文章目录oneblog添加第三方平台gitee中创建三方应用完善信息oneblog添加第三方平台 1.oneblog管理端,点击左侧菜单 网站管理——>社会化登录配置管理 ,添加一个社会化登录 2.编辑信息如下,选择gitee平台后复制redirectUri,然后去gitee获取clientId和…...
33- PyTorch实现分类和线性回归 (PyTorch系列) (深度学习)
知识要点 pytorch最常见的创建模型的方式, 子类 读取数据: data pd.read_csv(./dataset/credit-a.csv, headerNone) 数据转换为tensor: X torch.from_numpy(X.values).type(torch.FloatTensor) 创建简单模型: from torch import nn model nn.Sequential(nn.Linear(15, 1…...
C++基础——Ubuntu下编写C++环境配置总结(C++基本简介、Ubuntu环境配置、编写简单C++例程)
【系列专栏】:博主结合工作实践输出的,解决实际问题的专栏,朋友们看过来! 《QT开发实战》 《嵌入式通用开发实战》 《从0到1学习嵌入式Linux开发》 《Android开发实战》 《实用硬件方案设计》 长期持续带来更多案例与技术文章分享…...
项目管理中,导致进度失控的五种错误
项目管理中对工期的控制主要是进度控制,在项目进行中中,由于项目时间跨度长,人员繁杂,如果管理不规范,就容易导致项目进度滞后,如何管理好施工进度是管理者需要解决的问题之一。 1、项目计划缺乏执行力 安…...
C# 中的abstract和virtual
重新理解了下关键字abstract,做出以下总结: 1.标记为abstract的类不能实例化,但是依然可以有构造函数,也可以重载构造函数,在子类中调用 2.abstract类中可以有abstract标记的方法和属性,也可以没有,被标记…...
第六十周总结——React数据管理
React数据管理 代码仓库 React批量更新 React中的批量更新就是将多次更新合并处理,最终只渲染一次,来获得更好的性能。 React18版本之前的批量更新 // react 17 react-dom 17 react-scripts 4.0.3 import * as ReactDOM from "react-dom"…...
Springboot之@Async异步指定自定义线程池使用
开发中会碰到一些耗时较长或者不需要立即得到执行结果的逻辑,比如消息推送、商品同步等都可以使用异步方法,这时我们可以用到Async。但是直接使用 Async 会有风险,当我们没有指定线程池时,他会默认使用其Spring自带的 SimpleAsync…...
视频知识点(23)- TS格式详解指南
*《音视频开发》系列-总览*(点我) 一、格式简介 TS视频封装格式,是一种被广泛应用的多媒体文件格式。它的全称是MPEG2-TS,其中TS是“Transport Stream”的缩写。TS(Transport Stream)流是一种传输流,它由固定长度(188 字节)的 TS 包组成,TS 包是对PES包的一种封装方式…...
linux篇【16】:传输层协议<后序>
目录 六.滑动窗口 (1)发送缓冲区结构 (2)滑动窗口介绍 (3)滑动窗口不一定只会向右移动。滑动窗口可以变大也可以变小。 (4)那么如果出现了丢包, 如何进行重传? 这里分两种情况…...
【C语言】动态内存管理
一.为什么存在动态内存分配? 我们已经掌握的内存开辟方式有:int val 20;//在栈空间上开辟四个字节 char arr[10] {0};//在栈空间上开辟10个字节的连续空间 但是上述的开辟空间的方式有两个特点: 1. 空间开辟大小是固定的。 2. 数组在申明的…...
Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...
【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
