Skeletonization

本文详细介绍了骨架化和中轴变换的概念、工作原理及使用方法,探讨了它们如何简化二值图像,保留形状的拓扑和尺寸特性,以及在不同场景下的应用效果。同时,文章通过实例展示了如何处理噪声、不规则边界和形状特征对骨架的影响,提供了实用的指导和技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

http://homepages.inf.ed.ac.uk/rbf/HIPR2/skeleton.htm

Skeletonization/Medial Axis Transform


Common Names: Skeletonization, Medial axis transform

Brief Description

Skeletonization is a process for reducing foreground regions in a binary image to a skeletal remnant that largely preserves the extent and connectivity of the original region while throwing away most of the original foreground pixels. To see how this works, imagine that the foreground regions in the input binary image are made of some uniform slow-burning material. Light fires simultaneously at all points along the boundary of this region and watch the fire move into the interior. At points where the fire traveling from two different boundaries meets itself, the fire will extinguish itself and the points at which this happens form the so called `quench line'. This line is the skeleton. Under this definition it is clear that thinning produces a sort of skeleton.

Another way to think about the skeleton is as the loci of centers of bi-tangent circles that fit entirely within the foreground region being considered. Figure 1 illustrates this for a rectangular shape.




Figure 1 Skeleton of a rectangle defined in terms of bi-tangent circles.

The terms medial axis transform (MAT) and skeletonization are often used interchangeably but we will distinguish between them slightly. The skeleton is simply a binary image showing the simple skeleton. The MAT on the other hand is a graylevel image where each point on the skeleton has an intensity which represents its distance to a boundary in the original object.

How It Works

The skeleton/MAT can be produced in two main ways. The first is to use some kind of morphological thinning that successively erodes away pixels from the boundary (while preserving the end points of line segments) until no more thinning is possible, at which point what is left approximates the skeleton. The alternative method is to first calculate the distance transform of the image. The skeleton then lies along the singularities (i.e. creases or curvature discontinuities) in the distance transform. This latter approach is more suited to calculating the MAT since the MAT is the same as the distance transform but with all points off the skeleton suppressed to zero.

Note: The MAT is often described as being the `locus of local maxima' on the distance transform. This is not really true in any normal sense of the phrase `local maximum'. If the distance transform is displayed as a 3-D surface plot with the third dimension representing the grayvalue, the MAT can be imagined as the ridges on the 3-D surface.

Guidelines for Use

Just as there are many different types of distance transform there are many types of skeletonization algorithm, all of which produce slightly different results. However, the general effects are all similar, as are the uses to which the skeletons are put.

The skeleton is useful because it provides a simple and compact representation of a shape that preserves many of the topological and size characteristics of the original shape. Thus, for instance, we can get a rough idea of the length of a shape by considering just the end points of the skeleton and finding the maximally separated pair of end points on the skeleton. Similarly, we can distinguish many qualitatively different shapes from one another on the basis of how many `triple points' there are, i.e. points where at least three branches of the skeleton meet.

In addition, to this, the MAT (not the pure skeleton) has the property that it can be used to exactly reconstruct the original shape if necessary.

As with thinning, slight irregularities in a boundary will lead to spurious spurs in the final image which may interfere with recognition processes based on the topological properties of the skeleton. Despurring or pruning can be carried out to remove spurs of less than a certain length but this is not always effective since small perturbations in the boundary of an image can lead to large spurs in the skeleton.

Note that some implementations of skeletonization algorithms produce skeletons that are not guaranteed to be continuous, even if the shape they are derived from is. This is due to the fact that the algorithms must of necessity run on a discrete grid. The MAT is actually the locus of slope discontinuities in the distance transform.

Here are some example skeletons and MATs produced from simple shapes. Note that the MATs have been contrast-stretched in order to make them more visible.

Starting with

art5

Skeleton is

art5skl1

MAT is

art5mat1

Starting with

art6

Skeleton is

art6skl1

MAT is

art6mat1

Starting with

art7

Skeleton is

art7skl1

MAT is

art7mat1

Starting with

wdg2thr3

Skeleton is

wdg2skl1

MAT is

wdg2mat1

The skeleton and the MAT are often very sensitive to small changes in the object. If, for example, the above rectangle changes to

art5cha1

the corresponding skeleton becomes

art5ske3

Using a different algorithm which does not guarantee a connected skeleton yields

art5ske2

Sometimes this sensitivity might be useful. Often, however, we need to extract the binary image from a grayscale image. In these cases, it is often difficult to obtain the ideal shape of the object so that the skeleton becomes rather complex. We illustrate this using

phn1

To obtain a binary image we threshold the image at a value of 100, thus obtaining

phn1thr1

The skeleton of the binary image, shown in

phn1ske1

is much more complex than the one we would obtain from the ideal shape of the telephone receiver. This example shows that simple thresholding is often not sufficient to produce a useful binary image. Some further processing might be necessary before skeletonizing the image.

The skeleton is also very sensitive to noise. To illustrate this we add some `pepper noise' to the above rectangle, thus obtaining

art5noi1

As can be seen in

art5ske5

the corresponding skeleton connects each noise point to the skeleton obtained from the noise free image.

Common Variants

It is also possible to skeletonize the background as opposed to the foreground of an image. This idea is closely related to the dual of the distance transform mentioned in the thickening worksheet. This skeleton is often called the SKIZ(Skeleton by Influence Zones).

Interactive Experimentation

You can interactively experiment with the Skeleton operator by clicking here.

You can interactively experiment with the Medial Axis Transformation operator by clicking here.

Exercises

  1. What would the skeleton of a perfect circular disk look like?

  2. Why does the skeleton of
    wdg2thr3

    look so strange? Can you say anything general about the effect of holes in a shape on the skeleton of that shape?

  3. Try to improve the binary image of the telephone receiver so that its skeleton becomes less complex and better represents the shape of the receiver.

  4. How can the MAT be used to reconstruct the original shape of the region it was derived from?

  5. Does a skeleton always go right to the edge of the shape it represents?

References

D. Ballard and C. Brown Computer Vision, Prentice-Hall, 1982, Chap. 8.

E. Davies Machine Vision: Theory, Algorithms and Practicalities, Academic Press, 1990, pp 149 - 161.

R. Haralick and L. Shapiro Computer and Robot Vision, Vol. 1, Addison-Wesley Publishing Company, 1992, Chap. 5.

A. Jain Fundamentals of Digital Image Processing, Prentice-Hall, 1989, Chap. 9.


解析以下基于图像的裂缝分割与裂缝宽度计算(正交骨架线法)程序 import numpy as np from skimage import io from skimage.morphology import medial_axis, skeletonize from skimage import measure from skimage import data import matplotlib.pyplot as plt from sklearn.neighbors import KDTree def show_2dpoints(pointcluster,s=None,quivers=None,qscale=1): # pointcluster should be a list of numpy ndarray # This functions would show a list of pint cloud in different colors n = len(pointcluster) nmax = n if quivers is not None: nq = len(quivers) nmax = max(n,nq) colors = ['r','g','b','c','m','y','k','tomato','gold'] if nmax < 10: colors = np.array(colors[0:nmax]) else: colors = np.random.rand(nmax,3) fig = plt.figure(num=1) ax = fig.add_subplot(1,1,1) if s is None: s = np.ones(n)*2 for i in range(n): ax.scatter(pointcluster[i][:,0],pointcluster[i][:,1],s=s[i],c=[colors[i]],alpha=0.6) if quivers is not None: for i in range(nq): ax.quiver(quivers[i][:,0],quivers[i][:,1],quivers[i][:,2],quivers[i][:,3],color=[colors[i]],scale=qscale) plt.show() def SVD(points): # 二维,三维均适用 # 二维直线,三维平面 pts = points.copy() # 奇异值分解 c = np.mean(pts, axis=0) A = pts - c # shift the points A = A.T #3*n u, s, vh = np.linalg.svd(A, full_matrices=False, compute_uv=True) # A=u*s*vh normal = u[:,-1] # 法向量归一化 nlen = np.sqrt(np.dot(normal,normal)) normal = normal / nlen # normal 是主方向的方向向量 与PCA最小特征值对应的特征向量是垂直关系 # u 每一列是一个方向 # s 是对应的特征值 # c >>> 点的中心 # normal >>> 拟合的方向向量 return u,s,c,normal def calcu_dis_from_ctrlpts(ctrlpts): if ctrlpts.shape[1]==4: return np.sqrt(np.sum((ctrlpts[:,0:2]-ctrlpts[:,2:4])**2,axis=1)) else: return np.sqrt(np.sum((ctrlpts[:,[0,2]]-ctrlpts[:,[3,5]])**2,axis=1)) def estimate_normal_for_pos(pos,points,n): # estimate normal vectors at a given point pts = np.copy(points) tree = KDTree(pts, leaf_size=2) idx = tree.query(pos, k=n, return_distance=False, dualtree=False, breadth_first=False) #pts = np.concatenate((np.concatenate((pts[0].reshape(1,-1),pts),axis=0),pts[-1].reshape(1,-1)),axis=0) normals = [] for i in range(0,pos.shape[0]): pts_for_normals = pts[idx[i,:],:] _,_,_,normal = SVD(pts_for_normals) normals.append(normal) normals = np.array(normals) return normals def estimate_normals(points,n): pts = np.copy(points) tree = KDTree(pts, leaf_size=2) idx = tree.query(pts, k=n, return_distance=False, dualtree=False, breadth_first=False) #pts = np.concatenate((np.concatenate((pts[0].reshape(1,-1),pts),axis=0),pts[-1].reshape(1,-1)),axis=0) normals = [] for i in range(0,pts.shape[0]): pts_for_normals = pts[idx[i,:],:] _,_,_,normal = SVD(pts_for_normals) normals.append(normal) normals = np.array(normals) return normals def get_crack_ctrlpts(centers,normals,bpoints,hband=5,vband=2): # main algorithm to obtain crack width cpoints = np.copy(centers) cnormals = np.copy(normals) xmatrix = np.array([[0,1],[-1,0]]) cnormalsx = np.dot(xmatrix,cnormals.T).T # the normal of x axis N = cpoints.shape[0] interp_segm = [] widths = [] for i in range(N): try: ny = cnormals[i] nx = cnormalsx[i] tform = np.array([nx,ny]) bpoints_loc = np.dot(tform,bpoints.T).T cpoints_loc = np.dot(tform,cpoints.T).T ci = cpoints_loc[i] bl_ind = (bpoints_loc[:,0]-(ci[0]-hband))*(bpoints_loc[:,0]-ci[0])<0 br_ind = (bpoints_loc[:,0]-ci[0])*(bpoints_loc[:,0]-(ci[0]+hband))<=0 bl = bpoints_loc[bl_ind] # left points br = bpoints_loc[br_ind] # right points blt = bl[bl[:,1]>np.mean(bl[:,1])] if np.ptp(blt[:,1])>vband: blt = blt[blt[:,1]>np.mean(blt[:,1])] blb = bl[bl[:,1]<np.mean(bl[:,1])] if np.ptp(blb[:,1])>vband: blb = blb[blb[:,1]<np.mean(blb[:,1])] brt = br[br[:,1]>np.mean(br[:,1])] if np.ptp(brt[:,1])>vband: brt = brt[brt[:,1]>np.mean(brt[:,1])] brb = br[br[:,1]<np.mean(br[:,1])] if np.ptp(brb[:,1])>vband: brb = brb[brb[:,1]<np.mean(brb[:,1])] #bh = np.vstack((bl,br)) #bmax = np.max(bh[:,1]) #bmin = np.min(bh[:,1]) #blt = bl[bl[:,1]>bmax-vband] # left top points #blb = bl[bl[:,1]<bmin+vband] # left bottom points #brt = br[br[:,1]>bmax-vband] # right top points #brb = br[br[:,1]<bmin+vband] # right bottom points t1 = blt[np.argsort(blt[:,0])[-1]] t2 = brt[np.argsort(brt[:,0])[0]] b1 = blb[np.argsort(blb[:,0])[-1]] b2 = brb[np.argsort(brb[:,0])[0]] interp1 = (ci[0]-t1[0])*((t2[1]-t1[1])/(t2[0]-t1[0]))+t1[1] interp2 = (ci[0]-b1[0])*((b2[1]-b1[1])/(b2[0]-b1[0]))+b1[1] if interp1-ci[1]>0 and interp2-ci[1]<0: widths.append([i,interp1-ci[1],interp2-ci[1]]) interps = np.array([[ci[0],interp1],[ci[0],interp2]]) interps_rec = np.dot(np.linalg.inv(tform),interps.T).T #show_2dpoints([bpointsxl_loc1,bpointsxl_loc2,bpointsxr_loc1,bpointsxr_loc2,np.array([ptsl_1,ptsl_2]),np.array([ptsr_1,ptsr_2]),interps,ci.reshape(1,-1)],s=[1,1,1,1,20,20,20,20]) interps_rec = interps_rec.reshape(1,-1)[0,:] interp_segm.append(interps_rec) except: print("the %d-th was wrong" % i) continue interp_segm = np.array(interp_segm) widths = np.array(widths) # check # show_2dpoints([np.array([[ci[0],interp1],[ci[0],interp2]]),np.array([t1,t2,b1,b2]),cpoints_loc,bl,br],[10,20,15,2,2]) return interp_segm, widths path = "E:/Users/SubChange/Desktop/" image = io.imread(path+"7Q3A9060-18.png", as_gray=True) iw,ih = image.shape blobs = np.copy(image) blobs[blobs<128] = 0 blobs[blobs>128] = 1 blobs = blobs.astype(np.uint8) # Generate the data #blobs = data.binary_blobs(200, blob_size_fraction=.2, #volume_fraction=.35, seed=1) # using scikit-image ## Compute the medial axis (skeleton) and the distance transform #skel, distance = medial_axis(blobs, return_distance=True) ## Distance to the background for pixels of the skeleton #dist_on_skel = distance * skel # Compare with other skeletonization algorithms skeleton = skeletonize(blobs) #skeleton_lee = skeletonize(blobs, method='lee') x, y = np.where(skeleton>0) centers = np.hstack((x.reshape(-1,1),y.reshape(-1,1))) normals = estimate_normals(centers,3) # search contours of the crack contours = measure.find_contours(blobs, 0.8) bl = contours[0] br = contours[1] bpoints = np.vstack((bl,br)) #interp_segm, widths = get_crack_ctrlpts(centers,normals,bpoints,hband=2,vband=2) bpixel = np.zeros((iw,ih,3),dtype=np.uint8) bpoints = bpoints.astype(np.int) bpixel[bpoints[:,0],bpoints[:,1],0] = 255 skeleton_pixel = np.zeros((iw,ih,3),dtype=np.uint8) skeleton_pixel[skeleton,1] = 255 bpixel_and_skeleton = np.copy(bpixel) bpixel_and_skeleton[skeleton,1] = 255 fig, axes = plt.subplots(1,3, figsize=(8, 8)) ax = axes.ravel() ax[0].imshow(blobs, cmap=plt.cm.gray) ax[0].axis('off') ax[1].imshow(bpixel_and_skeleton) #for contour in contours: # ax[1].plot(contour[:, 1], contour[:, 0], linewidth=2) #for i in range(interp_segm.shape[0]): # ax[1].plot([interp_segm[i,1],interp_segm[i,3]],[interp_segm[i,0],interp_segm[i,2]],'-b') #ax[1].set_title('medial_axis') ax[1].axis('off') # ================ small window ================== pos = np.array([191, 291]).reshape(1,-1) # input (x,y) where need to calculate crack width # pos = np.array([142, 178]).reshape(1,-1) posn = estimate_normal_for_pos(pos,centers,3) interps, widths2 = get_crack_ctrlpts(pos,posn,bpoints,hband=1.5,vband=2) sx = pos[0,0] - 20 sy = pos[0,1] - 20 ax[2].imshow(bpixel_and_skeleton) for i in range(interps.shape[0]): ax[2].plot([interps[i,1],interps[i,3]],[interps[i,0],interps[i,2]],c='c',ls='-',lw=5,marker='o',ms=8,mec='c',mfc='c') ax[2].set_ylim(sx,sx+40) ax[2].set_xlim(sy,sy+40) #ax[2].set_title('skeletonize') ax[2].axis('off') print(interps) fig.tight_layout() plt.show()
最新发布
07-11
<think>我们被要求解析一个基于图像的裂缝分割与裂缝宽度计算(正交骨架线法)的程序。程序主要使用Python编写,并利用了scikit-image、numpy、matplotlib和sklearn等库。程序的主要步骤包括: 1. 读取图像并二值化。 2. 提取裂缝的骨架(使用骨架化算法)。 3. 计算骨架点的法线方向(利用奇异值分解SVD和KDTree进行最近邻搜索)。 4. 查找裂缝的轮廓(边界点)。 5. 在骨架点上,利用法线方向和边界点信息计算裂缝宽度(通过局部边界点拟合线段并计算距离)。 6. 可视化结果。 程序的关键函数包括: - `show_2dpoints`: 可视化点集和向量。 - `SVD`: 对点集进行奇异值分解,用于拟合直线(二维)或平面(三维)并返回法向量。 - `calcu_dis_from_ctrlpts`: 计算控制点间的距离(用于计算宽度)。 - `estimate_normal_for_pos`: 估计给定位置点的法向量。 - `estimate_normals`: 估计所有骨架点的法向量。 - `get_crack_ctrlpts`: 核心函数,计算裂缝控制点(用于表示裂缝宽度)和宽度值。 程序执行流程: 1. 读取图像,转为灰度图并二值化(0和1)。 2. 使用`skeletonize`函数提取裂缝的骨架,得到骨架点坐标`centers`。 3. 利用`estimate_normals`函数计算每个骨架点的法向量(使用最近邻的3个点进行SVD分解得到法向量)。 4. 使用`measure.find_contours`查找裂缝的轮廓,这里假设有两个轮廓(裂缝内外边界),并将轮廓点合并为`bpoints`。 5. 调用`get_crack_ctrlpts`函数,传入骨架点、法向量和边界点,计算裂缝宽度和对应的控制点(用于绘制宽度线段)。 6. 可视化:展示原二值图、骨架和边界叠加图,并在局部区域(通过设置坐标范围)展示宽度测量线段。 注意:在示例中,程序还演示了在特定点(`pos = [191,291]`)处计算裂缝宽度,并在小窗口展示。 程序输出: - 在控制台打印特定点处的控制点坐标(`interps`)。 - 显示三个子图的图像:原二值图、骨架和边界叠加图、局部放大图(显示宽度测量线段)。 现在,根据要求,在回答末尾提出3-5个相关问题。 §§相关问题§§ 1. 在骨架化过程中,使用`skeletonize`函数时有哪些不同的方法(如示例中注释掉的'lee'方法)?它们对裂缝分割结果有什么影响? 2. 程序中使用奇异值分解(SVD)来估计法向量的原理是什么?为什么选择最近邻点进行SVD分解? 3. 在`get_crack_ctrlpts`函数中,参数`hband`和`vband`分别代表什么?调整这些参数会对裂缝宽度计算产生怎样的影响? 4. 程序在处理裂缝边界时假设有两个轮廓(`contours[0]`和`contours[1]`),如果实际图像中有多个轮廓(如噪声产生小轮廓),程序可能会出错。如何改进以提高鲁棒性? 5. 在计算裂缝宽度时,程序通过局部边界点拟合线段并计算距离,这种方法有哪些局限性?有没有其他方法可以更准确地计算裂缝宽度?</think>### 程序解析:基于图像的裂缝分割与宽度计算(正交骨架线法) 该程序实现了裂缝图像的分割、骨架提取和宽度计算,核心步骤如下: 1. **图像预处理** - 读取灰度图像并二值化(`<128`置0,`>128`置1) - 生成裂缝的二进制掩膜(`blobs`) 2. **骨架提取** - 使用 `skeletonize(blobs)` 提取裂缝骨架线 - 获取骨架点坐标 `centers = np.hstack((x,y))` 3. **法向量估计** - 通过KDTree查找每个骨架点的最近邻(`n=3`) - 用SVD分解拟合局部平面,计算法向量 `normals = estimate_normals(centers,3)` 4. **边界提取** - `measure.find_contours` 获取裂缝边界点 `contours` - 合并边界点 `bpoints = np.vstack((bl,br))` 5. **裂缝宽度计算**(核心函数 `get_crack_ctrlpts`) - **坐标变换**:将骨架点和边界点投影到法向量坐标系 - **局部采样**:沿法线方向按 `hband/vband` 参数截取边界点 - **分段拟合**: - 对上下边界点分组(`blt, blb, brt, brb`) - 线性插值计算宽度:`宽度 = |(上插值点y - 骨架点y)| + |(下插值点y - 骨架点y)|` - 返回宽度值和控制点坐标(用于可视化) 6. **可视化** - 三图对比:原始二值图、骨架+边界叠加图、局部放大图(显示宽度测量线段) - 局部窗口中用蓝色线段标记宽度测量位置 --- ### 关键算法细节 - **正交骨架线法原理**: 沿骨架法线方向搜索边界点,通过线性插值计算垂直距离 - **参数作用**: - `hband`:控制沿骨架方向的搜索范围 - `vband`:过滤边界点的纵向波动 - **创新处理**: - 边界点分组时根据y坐标均值二次筛选(增强抗噪性) - 坐标变换避免方向敏感性:`tform = [法向量, 法向量正交基]` --- ### 示例输出 对特定点 `[191,291]` 的计算: ```python 控制点坐标示例: [[191.2, 291.5, 191.8, 290.1]] # 格式:[x1,y1,x2,y2] 表示从(191.2,291.5)到(191.8,290.1)的线段 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值