OpenCV—python 图片细化(骨架提取)一

文章目录

一、前言

Hilditch算法参看资料:http://cis.k.hosei.ac.jp/~wakahara/Hilditch.c
Hilditch第二种算法:http://cgm.cs.mcgill.ca/~godfried/teaching/projects97/azar/skeleton.html#algorithm

图像细化:
图像细化主要是针对二值图而言,所谓骨架,可以理解为图像的中轴,,一个长方形的骨架,是它的长方向上的中轴线,圆的骨架是它的圆心,直线的骨架是它自身,孤立点的骨架也是自身。我们来看看典型的图形的骨架(用粗线表示)

细化的算法有很多种,但比较常用的算法是查表法。细化是从原来的图中去掉一些点,但仍要保持原来的形状。实际上是保持原图的骨架。判断一个点是否能去掉是以8个相邻点(八连通)的情况来作为判据的,具体判据为:
1,内部点不能删除
2,鼓励点不能删除
3,直线端点不能删除
4,如果P是边界点,去掉P后,如果连通分量不增加,则P可删除

二、第一种算法

假设当前被处理的像素为p0,我们使用下图所示的8邻域表示方式。
《OpenCV—python 图片细化(骨架提取)一》
我们处理的为二值图像,背景为黑色=0,要细化的前景物体像素值=255
对于Hilditch算法来说,它并不是一个完全的并行算法,而是串行并行相结合。当前像素是否是能够删除的骨架点,不仅是由它周围的8邻域决定,而且和前面像素的判定结果有关。一个像素判定为可以删除,我们并不直接删除它,而是在目地图像中设置像素值为GRAY=128,这个信息可能会影响之后其它像素的判定。
当图像一次扫描迭代完成后,我们把所有置为GRAY的像素设置为0,从而删除它。

算法的描述如下

  1. 迭代扫描当前图像
    对于当前像素点,扫描它的8邻域:
    如果邻域的像素值为255,则b[i]=1(i=0…8),像素值为128(GRAY,表示该像素点在前面的循环中被标记为删除),b[i]=-1;如果像素值为0,则b[i]=0。
    《OpenCV—python 图片细化(骨架提取)一》

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

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

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

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

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

  7. 连通性检测,使用下面的公式:
    首先根据当前像素周围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则满足连通性,可以删除。
    《OpenCV—python 图片细化(骨架提取)一》

6.最后一个条件保证当轮廓是2个像素宽时,只删除一边。统计sum的值,当值为8时候,可以删除。
当这6个条件都满足时候,标记当前像素值为GRAY(128),然后在判断别的像素。当所有像素都扫描一遍后,完成一次迭代。

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

三、第二种算法

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

此时我们使用的8邻域标记为:
《OpenCV—python 图片细化(骨架提取)一》

下面看下它的算法描述:

  1. 复制目地图像到临时图像,对临时图像进行一次扫描,对于不为0的点,如果满足以下四个条件,则在目地图像中删除该点(就是设置该像素为0)
    2<= p2+p3+p4+p5+p6+p7+p8+p9<=6
值说明:

大于等于2会保证p1点不是端点或孤立点,因为删除端点和孤立点是不合理的,

小于等于6保证p1点是一个边界点,而不是一个内部点。

等于0时候,周围没有等于1的像素,所以p1为孤立点,等于1的时候,周围只有1个灰度等于1的像素,所以是端点(注:端点是周围有且只能有1个值为1的像素)。

《OpenCV—python 图片细化(骨架提取)一》

  1. p2->p9的排列顺序中,01模式的数量为1,比如下面的图中,有p2p3 => 01, p6p7=>01,所以该像素01模式的数量为2。
    《OpenCV—python 图片细化(骨架提取)一》
    之所以要01模式数量为1,是要保证删除当前像素点后的连通性。比如下面的图中,01模式数量大于1,如果删除当前点p1,则连通性不能保证。
    《OpenCV—python 图片细化(骨架提取)一》
  2. p2.p4.p8 = 0 or A(p2)!=1,A(p2)表示p2周围8邻域的01模式和。这个条件保证2个像素宽的垂直条不完全被腐蚀掉。(图下左)
    《OpenCV—python 图片细化(骨架提取)一》
  3. p2.p4.p6 = 0 or A(p4)!=1,A(p4)表示p4周围8邻域的01模式和。这个条件保证2个像素宽的水平条不完全被腐蚀掉。(图上右)
3.1 代码演示

代码参看https://blog.csdn.net/hehedadaq/article/details/80303218
测试原图(684*342)《OpenCV—python 图片细化(骨架提取)一》

# -*- coding: utf-8 -*-
import cv2


# 细化函数,输入需要细化的图片(经过二值化处理的图片)和映射矩阵array
# 这个函数将根据算法,运算出中心点的对应值
def Thin(image, array):
    h, w = image.shape
    iThin = image

    for i in range(h):
        for j in range(w):
            if image[i, j] == 0:
                a = [1] * 9
                for k in range(3):
                    for l in range(3):
                        # 如果3*3矩阵的点不在边界且这些值为零,也就是黑色的点
                        if -1 < (i - 1 + k) < h and -1 < (j - 1 + l) < w and iThin[i - 1 + k, j - 1 + l] == 0:
                            a[k * 3 + l] = 0
                sum = a[0] * 1 + a[1] * 2 + a[2] * 4 + a[3] * 8 + a[5] * 16 + a[6] * 32 + a[7] * 64 + a[8] * 128
                iThin[i, j] = array[sum] * 255
    return iThin


# 映射表
array = [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1,\
         0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 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, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1,\
         0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0,\
         1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0]




if __name__ == '__main__':
    src = cv2.imread(r'C:\Users\xxx\Desktop\001.png', 0)
    Gauss_img = cv2.GaussianBlur(src, (3,3), 0)
    cv2.imshow('image', Gauss_img)
    cv2.waitKey(0)

    # 自适应二值化函数,需要修改的是55那个位置的数字,越小越精细,细节越好,噪点更多,最大不超过图片大小
    adap_binary = cv2.adaptiveThreshold(Gauss_img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 55, 2)
    iThin = Thin(adap_binary.copy(), array)

    cv2.imshow('adaptive', adap_binary)
    cv2.imshow('adaptive_iThin', iThin)
    cv2.waitKey(0)

    # 获取简单二值化的细化图,并显示
    ret, binary = cv2.threshold(Gauss_img, 130, 255, cv2.THRESH_BINARY)
    iThin_2 = Thin(binary.copy(), array)

    cv2.imshow('binary', binary)
    cv2.imshow('binary_iThin', iThin_2)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

《OpenCV—python 图片细化(骨架提取)一》
显然细化算法有问题。

3.1 算法优化及代码演示

在每行水平扫描的过程中,先判断每一点的左右邻居,如果都是黑点,则该点不做处理。另外,如果某个黑店被删除了,则跳过它的右邻居,处理下一点。对矩形这样做完一遍,水平方向会减少两像素。
然后我们再改垂直方向扫描,方法一样。
这样做一次水平扫描和垂直扫描,原图会“瘦”一圈

多次重复上面的步骤,知道图形不在变化为止算法优化后如下:

import cv2


def VThin(image, array):
    h,w= image.shape[:2]
    NEXT = 1
    for i in range(h):
        for j in range(w):
            if NEXT == 0:
                NEXT = 1
            else:
                M = image[i, j-1] + image[i,j] + image[i, j+1] if 0<j<w-1 else 1
                if image[i, j] == 0 and M != 0:
                    a = [0] * 9
                    for k in range(3):
                        for l in range(3):
                            if-1<(i-1+k)<h and -1<(j-1+l)<w and image[i-1+k, j-1+l] == 255:
                                a[k*3 + l] = 1
                    sum = a[0]*1 + a[1]*2 + a[2]*4 + a[3]*8 + a[5]*16 + a[6]*32 + a[7]*64 + a[8]*128
                    image[i,j] = array[sum]*255
                    if array[sum] == 1:
                        NEXT = 0
    return image


def HThin(image, array):
    h, w = image.shape[:2]
    NEXT = 1
    for j in range(w):
        for i in range(h):
            if NEXT == 0:
                NEXT = 1
            else:
                M = image[i-1, j] + image[i, j] + image[i+1, j] if 0<i<h-1 else 1
                if image[i, j] == 0 and M != 0:
                    a = [0] * 9
                    for k in range(3):
                        for l in range(3):
                            if -1<(i-1+k)<h and -1<(j-1+l)<w and image[i-1+k, j-1+l] == 255:
                                a[k*3 + l] = 1
                    sum = a[0]*1 + a[1]*2 + a[2]*4 + a[3]*8 + a[5]*16 + a[6]*32 + a[7]*64 + a[8]*128
                    image[i, j] = array[sum] * 255
                    if array[sum] == 1:
                        NEXT = 0
    return image


def Xihua(binary, array, num=10):
    iXihua = binary.copy()
    for i in range(num):
        VThin(iXihua, array)
        HThin(iXihua, array)
    return iXihua


array = [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1,\
         0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 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, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1,\
         0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0,\
         1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0]


if __name__ == '__main__':
    image = cv2.imread(r'C:\Users\xxx\Desktop\002.png', 0)
    ret, binary = cv2.threshold(image, 70, 255, cv2.THRESH_BINARY)
    cv2.imshow('image', image)
    cv2.imshow('binary', binary)
    cv2.waitKey(0)
    
    t1 = time.time()
    iThin = Xihua(binary, array)
    t2 = time.time()
    print('cost time:',t2-t1)
    cv2.imshow('iThin', iThin)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
''' cost time: 21.486935138702393 '''

显然,比之前效果好些了。使用 测试原图(684*342) 耗时有点久(处理器i5-8300H)
《OpenCV—python 图片细化(骨架提取)一》
测试图片2

《OpenCV—python 图片细化(骨架提取)一》
代码运算速度优化,减少了运行时间,使用测试原图(684*342)(上面的图片)

import cv2,time
import numpy as np



def Three_element_add(array):
    array0 = array[:]
    array1 = np.append(array[1:],np.array([0]))
    array2 = np.append(array[2:],np.array([0, 0]))
    arr_sum = array0 + array1 + array2
    return arr_sum[:-2]


def VThin(image, array):
    NEXT = 1
    height, width = image.shape[:2]
    for i in range(1,height):
        M_all = Three_element_add(image[i])
        for j in range(1,width):
            if NEXT == 0:
                NEXT = 1
            else:
                M = M_all[j-1] if j<width-1 else 1
                if image[i, j] == 0 and M != 0:
                    a = np.zeros(9)
                    if height-1 > i and width-1 > j:
                        kernel = image[i - 1:i + 2, j - 1:j + 2]
                        a = np.where(kernel == 255, 1, 0)
                        a = a.reshape(1, -1)[0]
                    NUM = np.array([1,2,4,8,0,16,32,64,128])
                    sumArr = np.sum(a*NUM)
                    image[i, j] = array[sumArr] * 255
                    if array[sumArr] == 1:
                        NEXT = 0
    return image


def HThin(image, array):
    height, width = image.shape[:2]
    NEXT = 1
    for j in range(1,width):
        M_all = Three_element_add(image[:,j])
        for i in range(1,height):
            if NEXT == 0:
                NEXT = 1
            else:
                M = M_all[i-1] if i < height - 1 else 1
                if image[i, j] == 0 and M != 0:
                    a = np.zeros(9)
                    if height - 1 > i and width - 1 > j:
                        kernel = image[i - 1:i + 2, j - 1:j + 2]
                        a = np.where(kernel == 255, 1, 0)
                        a = a.reshape(1, -1)[0]
                    NUM = np.array([1, 2, 4, 8, 0, 16, 32, 64, 128])
                    sumArr = np.sum(a * NUM)
                    image[i, j] = array[sumArr] * 255
                    if array[sumArr] == 1:
                        NEXT = 0
    return image


def Xihua(binary, array, num=10):
    binary_image = binary.copy()
    image = cv2.copyMakeBorder(binary_image, 1, 0, 1, 0, cv2.BORDER_CONSTANT, value=0)
    for i in range(num):
        VThin(image, array)
        HThin(image, array)
    return image


array = [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1,\
         0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 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, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1,\
         0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,\
         1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0,\
         1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0]


if __name__ == '__main__':
    image = cv2.imread(r'C:\Users\xxx\Desktop\001.png', 0)
    ret, binary = cv2.threshold(image, 70, 255, cv2.THRESH_BINARY)
    cv2.imshow('image', image)
    cv2.imshow('binary', binary)
    cv2.waitKey(0)

    t1 = time.time()

    iThin = Xihua(binary, array)
    t2 = time.time()
    print('cost time:',t2-t1)
    cv2.imshow('iThin', iThin)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
''' cost time: 7.425575494766235 '''

参考与鸣谢
https://www.cnblogs.com/xianglan/archive/2011/01/01/1923779.html
https://blog.csdn.net/FunnyWhiteCat/article/details/80670332
https://www.cnblogs.com/DOMLX/p/8672489.html
https://www.cnblogs.com/My-code-z/p/5712524.html
https://www.cnblogs.com/mikewolf2002/p/3327183.html
https://www.researchgate.net/signup.SignUp.html?ev=su_requestFulltext
https://blog.csdn.net/Lg_cc/article/details/50179469
优化算法:https://wenku.baidu.com/view/a74c003567ec102de2bd8915.html

    原文作者:SongpingWang
    原文地址: https://blog.csdn.net/wsp_1138886114/article/details/101050825
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞