本章我们学习一下Hilditch算法的基本原理,从网上找资料的时候,竟然发现两个有很大差别的算法描述,而且都叫Hilditch算法。不知道那一个才是正宗的,两个算法实现的效果接近,第一种算法更好一些。

第一种算法描述参考paper和代码:

Linear Skeletons from Square Cupboards

Speedup Method for Real-Time Thinning Algorithm

http://cis.k.hosei.ac.jp/~wakahara/Hilditch.c

第二种算法描述参考资料:

http://cgm.cs.mcgill.ca/~godfried/teaching/projects97/azar/skeleton.html#algorithm

 

下面我们分别看一下这两种算法描述:

一、第一种算法描述

假设当前被处理的像素为p0,我们使用下图所示的8邻域表示方式。

     我们处理的为二值图像,背景为黑色,值为0,要细化的前景物体像素值为255。

     对于Hilditch算法来说,它并不是一个完全的并行算法,而是串行并行相结合。当前像素是否是能够删除的骨架点,不仅是由它周围的8邻域决定,而且和前面像素的判定结果有关。一个像素判定为可以删除,我们并不直接删除它,而是在目地图像中设置像素值为GRAY=128,这个信息可能会影响之后其它像素的判定。

      当图像一次扫描迭代完成后,我们把所有置为GRAY的像素设置为0,从而删除它。

 

算法的描述如下。

迭代扫描当前图像

    对于当前像素点,扫描它的8邻域,如果邻域的像素值为255,则b[i]=1(i=0…8),像素值为128(GRAY,表示该像素点在前面的循环中被标记为删除),b[i]=-1,如果像素值为0,则b[i]=0。

下面会根据b[i]的值进行6个条件判断,如果条件满足,则会标记该像素值为GRAY(128)。

1. b[0]=1,即当前像素必须为前景点。

2. 1-abs(b1) + 1 – abs(b3) + 1 – abs(b5) + 1 – abs(b7) >= 1,该条件表示当前像素为边界点,即东西南北四个点至少有一个b[i]=0。

3. abs(b1)+…+abs(b8)>=2, 该条件表示不能删除端点,即p0点周围只有一个点为1或-1的情况。

4.  统计b1到b8等于1的数量,该数量值必须大于1,该条件表示不能删除端点。、

5.  连通性检测,使用下面的公式:首先根据当前像素周围3*3域的值,记录d[9]数组,如果b[i]等于0,则d[i]=0, 否则d[i]=1,最后计算 d1-d1*d2*d3+d3-d3*d4*d5+d5-d5*d6*d7+d7-d7*d8*d1是否为1,为1则满足连通性,可以删除。

6.最后一个条件保证当轮廓是2个像素宽时,只删除一边。统计sum的值,当值为8时候,可以删除。

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;
    }

     当这6个条件都满足时候,标记当前像素值为GRAY(128),然后在判断别的像素。当所有像素都扫描一遍后,完成一次迭代。

此时我们会把刚才标记为GARY的像素,都设置为0,真正的删除它,如果上一次循环已经没有标记删除的像素,则退出迭代,否则进行下一次迭代。

算法代码:

int gThin::func_nc8(int *b)
//端点的连通性检测
{
int n_odd[4] = { 1, 3, 5, 7 }; //四邻域
int i, j, sum, d[10]; for (i = 0; i <= 9; i++) {
j = i;
if (i == 9) j = 1;
if (abs(*(b + j)) == 1)
{
d[i] = 1;
}
else
{
d[i] = 0;
}
}
sum = 0;
for (i = 0; i < 4; i++)
{
j = n_odd[i];
sum = sum + d[j] - d[j] * d[j + 1] * d[j + 2];
}
return (sum);
} void gThin::cvHilditchThin(cv::Mat& src, cv::Mat& dst)
{
if(src.type()!=CV_8UC1)
{
printf("只能处理二值或灰度图像\n");
return;
}
//非原地操作时候,copy src到dst
if(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++)
{ //前面标记为删除的像素,我们置其相应邻域值为-1
for (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); }

二、第二种算法描述

       第二种算法描述和Zhang的并行算法很相似,特别是前2个条件一模一样,不同的是3,4两个条件,还有就是该描述算法并没有像zhang算法那样,把一次迭代分成2个阶段。

此时我们使用的8邻域标记为:

下面看下它的算法描述:

复制目地图像到临时图像,对临时图像进行一次扫描,对于不为0的点,如果满足以下四个条件,则在目地图像中删除该点(就是设置该像素为0)

a. 2<= p2+p3+p4+p5+p6+p7+p8+p9<=6

    大于等于2会保证p1点不是端点或孤立点,因为删除端点和孤立点是不合理的,小于等于6保证p1点是一个边界点,而不是一个内部点。等于0时候,周围没有等于1的像素,所以p1为孤立点,等于1的时候,周围只有1个灰度等于1的像素,所以是端点(注:端点是周围有且只能有1个值为1的像素)。

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

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

 

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

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

算法代码:

void gThin::cvHilditchThin1(cv::Mat& src, cv::Mat& dst)
{
//http://cgm.cs.mcgill.ca/~godfried/teaching/projects97/azar/skeleton.html#algorithm
//算法有问题,得不到想要的效果
if(src.type()!=CV_8UC1)
{
printf("只能处理二值或灰度图像\n");
return;
}
//非原地操作时候,copy src到dst
if(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,AP4
int 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; //满足删除条件,设置当前像素为0
ifEnd = true;
//printf("\n"); //PrintMat(dst);
}
}
}
}
}
//printf("\n");
//PrintMat(dst);
//PrintMat(dst);
//已经没有可以细化的像素了,则退出迭代
if(!ifEnd) break;
}
}

第一种Hilditch算法的结果:

第二种Hilditch算法的结果:

程序代码:工程FirstOpenCV11

OpenCV学习(15) 细化算法(3)的更多相关文章

  1. OpenCV学习(16) 细化算法(4)

    本章我们学习Rosenfeld细化算法,参考资料:http://yunpan.cn/QGRjHbkLBzCrn 在开始学习算法之前,我们先看下连通分量,以及4连通性,8连通性的概念: http://w ...

  2. OpenCV学习(18) 细化算法(6)

    本章我们在学习一下基于索引表的细化算法. 假设要处理的图像为二值图,前景值为1,背景值为0. 索引表细化算法使用下面的8邻域表示法: 一个像素的8邻域,我们可以用8位二进制表示,比如下面的8邻域,表示 ...

  3. OpenCV学习(17) 细化算法(5)

    本章我们看下Pavlidis细化算法,参考资料http://www.imageprocessingplace.com/downloads_V3/root_downloads/tutorials/con ...

  4. OpenCV学习(14) 细化算法(2)

          前面一篇教程中,我们实现了Zhang的快速并行细化算法,从算法原理上,我们可以知道,算法是基于像素8邻域的形状来决定是否删除当前像素.还有很多与此算法相似的细化算法,只是判断的条件不一样. ...

  5. OpenCV学习(13) 细化算法(1)

    程序编码参考经典的细化或者骨架算法文章: T. Y. Zhang and C. Y. Suen, "A fast parallel algorithm for thinning digita ...

  6. OpenCV学习(19) 细化算法(7)

    最后再来看一种通过形态学腐蚀和开操作得到骨架的方法.http://felix.abecassis.me/2011/09/opencv-morphological-skeleton/ 代码非常简单: v ...

  7. c++opencv中线条细化算法

    要达到的效果就是将线条尽量细化成单像素,按照论文上的Hilditch算法试了一下,发现效果不好,于是自己尝试着写了一下细化的算法,基本原理就是从上下左右四个方向向内收缩. 1.先是根据图片中的原则确定 ...

  8. OpenCV学习(21) Grabcut算法详解

    grab cut算法是graph cut算法的改进.在理解grab cut算之前,应该学习一下graph cut算法的概念及实现方式. 我搜集了一些graph cut资料:http://yunpan. ...

  9. OpenCV学习(9) 分水岭算法(3)

    本教程我学习一下opencv中分水岭算法的具体实现方式. 原始图像和Mark图像,它们的大小都是32*32,分水岭算法的结果是得到两个连通域的轮廓图. 原始图像:(原始图像必须是3通道图像) Mark ...

随机推荐

  1. PyQt5点击按钮产生新窗体

    import sys from PyQt5.QtWidgets import QApplication,QWidget from form1 import Ui_Form1 from form2 im ...

  2. Underscore.js-精巧而强大实用功能库

    前言 从其他语言转向Javascript时,通常都会遇到一些困惑性问题.比如,Java中的HashMap在Javascript中如何实现?Javascript面向对象式编程如何实现继承?如何实现通用的 ...

  3. Java String、StringBuilder和StringBuffer

    转载: Java String.StringBuilder和StringBuffer 概览 在Android/Java开发中,用来处理字符串常用的类有3种: String.StringBuilder. ...

  4. 湖南大学ACM程序设计新生杯大赛(同步赛)L - Liao Han

    题目描述 Small koala special love LiaoHan (of course is very handsome boys), one day she saw N (N<1e1 ...

  5. BNUOJ 52509 Borrow Classroom

    最近公共祖先. 如果$A$到$1$的时间小于$B$到$C$再到$1$的时间,那么一定可以拦截. 如果上述时间相等,需要在到达$1$之前,两者相遇才可以拦截. #include<bits/stdc ...

  6. 力扣:丑数II和数组中前K大的元素

    数组中的第K个元素 在未排序的数组中找到第 k 个最大的元素.请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素. 示例 1: 输入: [3,2,1,5,6,4] 和 k ...

  7. 【LeetCode】32. Longest Valid Parentheses

    Given a string containing just the characters '(' and ')', find the length of the longest valid (wel ...

  8. javascript中Date对象的应用

    前面的话 简易日历作为javascript中Date对象的常见应用,用途较广泛.本文将详细说明简易日历的实现思路 效果演示 HTML说明 使用type=number的两个input分别作为年和月的输入 ...

  9. Angular Material Starter App

      介绍 Material Design反映了Google基于Android 5.0 Lollipop操作系统的原生应用UI开发理念,而AngularJS还发起了一个Angular Material ...

  10. [CSAcademy]A-Game

    题目大意: 给你一个只含字符'A'和'B'的串,A和B两人轮流对其中的子串染色,要求被染色的子串中不包含已经被染色的子串. 最后,如果一方染的'A'少,那么这一方胜: 如果双方染的'A'和'B'一样多 ...