裂缝类型有很多种,这里我们仅仅判断线性裂缝与网状裂缝,线性裂缝按照其走势有可分为横向裂缝、纵向裂缝和斜向裂缝。
我觉得大家应当有这样的意识,面对网状裂缝,它的二维参数是否有意义?答案是没有!如果检测到网状裂缝,我想大家的第一反应是比较严重了,需要修补了。如果是一条线性裂缝呢?我是不是还有考虑一下它的受损程度是否达到需要修补的地步。
所以按照我的想法,可以求网状裂缝的面积,评估其受损程度,求线性裂缝的面积、长度和宽度,评估其受损程度。

网状裂缝的直方图投影

横向裂缝的直方图投影

纵向裂缝的直方图投影

斜向裂缝的直方图投影
上面四张图是四种裂缝对应的直方图,结合上面的一些特点,我们可以依照自己的数据集进行类型分类。
接下来,get_minAreaRect_information函数会从二值化掩膜图像中提取最小外接矩形的相关信息,包括中心点坐标、宽高和旋转角度。inference_minAreaRect函数用于计算最小外接矩形框的宽、高和角度信息,并将角度转换为相对于图像水平方向的夹角。
- def inference_minAreaRect(minAreaRect):
- w, h = minAreaRect[1]
- if w > h:
- angle = int(minAreaRect[2])
- else:
- angle = -(90 - int(minAreaRect[2]))
- return w, h, angle
-
- def _get_minAreaRect_information(mask):
- mask = pz.BinaryImg(mask)
- contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
- contour_merge = np.vstack(contours)
- minAreaRect = cv2.minAreaRect(contour_merge)
- return minAreaRect
pz.BinaryImg获取图像二值图,请确保读取时候为BGR的图片。
创建了一个ClassificationCrack类,并且对裂缝的分类参数进行初始化,有分类裂缝的阈值threshold,分类裂缝的高宽比阈值HWration,用于分类裂缝的直方图比例阈值Histration。
- class CrackType():
- """直方图投影法推断裂缝类型"""
- def __init__(self, threshold=3, HWratio=10, Histratio=0.5):
- """
- 初始化分类裂缝的参数
- :param threshold: 阈值,用于分类裂缝的阈值
- :param HWratio: 高宽比,用于分类裂缝的高宽比阈值
- :param Histratio: 直方图比例,用于分类裂缝的直方图比例阈值
- """
- self.threshold = threshold
- self.HWratio = HWratio
- self.Histratio = Histratio
- self.types = {0: 'Horizontal',
- 1: 'Vertical',
- 2: 'Oblique',
- 3: 'Mesh'}
这里我们使用字典self.types,这样就可以通过键值对判断裂缝的类型了。
在ClassificationCrack类下,我们再定义了一个hist_judge的方法,less_than_T统计直方图中大于 0 且小于等于阈值 self.threshold 的像素数量,more_than_T统计直方图中大于阈值 self.threshold 的像素数量。通过more_than_T / (less_than_T + 1e-5)来比较是否超过了直方图比例阈值。
- def hist_judge(self, hist_v):
- less_than_T = np.count_nonzero((hist_v > 0) & (hist_v <= self.threshold))
- more_than_T = np.count_nonzero(hist_v > self.threshold)
- return more_than_T / (less_than_T + 1e-5) > self.Histratio
classify 方法是 ClassificationCrack 类中的另一个成员方法,它接收三个值,minAreaRect 是一个元组,表示最小外接矩形框的信息,包括中心点坐标、宽高和旋转角度;skeleton_pts是一个数组,表示骨骼点的坐标;HW是当前 patch 的高和宽。
- def classify(self, minAreaRect, skeleton_pts, HW):
- H, W = HW
- w, h, angle = inference_minAreaRect(minAreaRect)
- if w / h < self.HWratio or h / w < self.HWratio:
- pts_y, pts_x = skeleton_pts[:, 0], skeleton_pts[:, 1]
- hist_x = np.histogram(pts_x, W)
- hist_y = np.histogram(pts_y, H)
- if self.hist_judge(hist_x[0]) and self.hist_judge(hist_y[0]):
- return 3
-
- return self.angle2cls(angle)
-
- @staticmethod
- def angle2cls(angle):
- angle = abs(angle)
- assert 0 <= angle <= 90, "ERROR: The angle value exceeds the limit and should be between 0 and 90 degrees!"
- if angle < 35:
- return 0
- elif 35 <= angle <= 55:
- return 2
- elif angle > 55:
- return 1
- else:
- return None
利用 inference_minAreaRect 函数从 minAreaRect 中获取旋转矩形框的宽度 w、高度 h 和角度 angle。接下来,通过判断 w / h 和 h / w 是否小于 self.HWratio 来判断旋转矩形框的长宽比是否满足分类条件。
如果长宽比满足条件,则将 skeleton_pts 按照 x 和 y 方向投影到直方图 hist_x 和 hist_y,然后通过 self.hist_judge 方法判断这两个直方图是否满足分类条件。
以上条件均满足,则会认为是网状裂缝,否则就使用angle2cls来进行角度分类。
根据角度的大小将裂缝分为以下三类:
- """
- 裂缝分类如何判断
- 横向、纵向、网状、斜裂缝
- """
- import os
- import matplotlib.pyplot as plt
-
- import numpy as np
- import cv2
- import pyzjr as pz
-
- from skimage.morphology import skeletonize
- from skimage.filters import threshold_otsu
- from skimage.color import rgb2gray
-
- class CrackType():
- """直方图投影法推断裂缝类型"""
- def __init__(self, threshold=3, HWratio=10, Histratio=0.5):
- """
- 初始化分类裂缝的参数
- :param threshold: 阈值,用于分类裂缝的阈值
- :param HWratio: 高宽比,用于分类裂缝的高宽比阈值
- :param Histratio: 直方图比例,用于分类裂缝的直方图比例阈值
- """
- self.threshold = threshold
- self.HWratio = HWratio
- self.Histratio = Histratio
- self.types = {0: 'Horizontal',
- 1: 'Vertical',
- 2: 'Oblique',
- 3: 'Mesh'}
-
- def inference_minAreaRect(self, minAreaRect):
- """
- 旋转矩形框长边与x轴的夹角.
- 旋转角度 angle 是相对于图像水平方向的夹角,范围是 -90 到 +90 度.
- 然而,一般情况下,我们习惯将角度定义为相对于 x 轴正方向的夹角,范围是 -180 到 +180 度.
- """
- w, h = minAreaRect[1]
- if w > h:
- angle = int(minAreaRect[2])
- else:
- angle = -(90 - int(minAreaRect[2]))
- return w, h, angle
-
- def classify(self, minAreaRect, skeleton_pts, HW):
- """
- 针对当前crack instance,对其进行分类;
- 主要利用了骨骼点双向投影直方图、旋转矩形框宽高比/角度;
- :param minAreaRect: 最小外接矩形框,[(cx, cy), (w, h), angle];
- :param skeleton_pts: 骨骼点坐标;
- :param HW: 当前patch的高、宽;
- """
- H, W = HW
- w, h, angle = self.inference_minAreaRect(minAreaRect)
- if w / h < self.HWratio or h / w < self.HWratio:
- pts_y, pts_x = skeleton_pts[:, 0], skeleton_pts[:, 1]
- hist_x = np.histogram(pts_x, W)
- hist_y = np.histogram(pts_y, H)
- if self.hist_judge(hist_x[0]) and self.hist_judge(hist_y[0]):
- return 3
-
- return self.angle2cls(angle)
-
- def hist_judge(self, hist_v):
- less_than_T = np.count_nonzero((hist_v > 0) & (hist_v <= self.threshold))
- more_than_T = np.count_nonzero(hist_v > self.threshold)
- return more_than_T / (less_than_T + 1e-5) > self.Histratio
-
- @staticmethod
- def angle2cls(angle):
- angle = abs(angle)
- assert 0 <= angle <= 90, "ERROR: The angle value exceeds the limit and should be between 0 and 90 degrees!"
- if angle < 35:
- return 0
- elif 35 <= angle <= 55:
- return 2
- elif angle > 55:
- return 1
- else:
- return None
-
- def _get_minAreaRect_information(mask):
- """
- 从二值化掩膜图像中获取最小外接矩形的相关信息
- :param mask:二值化掩膜图像,包含目标区域的白色区域
- :return:最小外接矩形的信息,包括中心点坐标、宽高和旋转角度
- """
- mask = pz.BinaryImg(mask)
- contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
- contour_merge = np.vstack(contours)
- minAreaRect = cv2.minAreaRect(contour_merge)
- return minAreaRect
-
- def SkeletonMap(target):
- """
- 获取骨架图的信息
- :param target: 目标图
- :return: 骨架图与一个数组,其中每一行表示一个非零元素的索引(y,x),包括行索引和列索引
- """
- gray = rgb2gray(target)
- thresh = threshold_otsu(gray)
- binary = gray > thresh
- skimage = skeletonize(binary)
- skepoints = np.argwhere(skimage)
- skimage = skimage.astype(np.uint8)
- return skimage, skepoints
-
- if __name__ == '__main__':
- plt.switch_backend('TkAgg')
- masks_dir = r"D:\PythonProject\RoadCrack\dimension2_data\num" # 这里改为存放上面图片的路径
- results_save_dir = "A_results"
- os.makedirs(results_save_dir, exist_ok=True)
- classifier = CrackType()
- imgfile,_ = pz.getPhotopath(masks_dir, debug=False)
- for path in imgfile:
- mask = cv2.imread(path)
- H, W = mask.shape[:2]
- mask_copy = mask.copy()
- skeimage, skepoints = SkeletonMap(mask_copy)
-
- minAreaRect=_get_minAreaRect_information(mask)
-
- pts_y, pts_x = skepoints[:, 0], skepoints[:, 1]
- hist_x = np.histogram(pts_x, W)
- hist_y = np.histogram(pts_y, H)
-
- result = classifier.classify(minAreaRect, skepoints, HW=(H, W))
- crack_type = classifier.types[result]
- print(crack_type)
-
- T = classifier.threshold
-
-
- plt.figure(figsize=(10, 5))
-
- plt.subplot(121)
- plt.plot(hist_x[1][:-1], [T] * len(hist_x[0]), 'r')
- plt.bar(hist_x[1][:-1], hist_x[0])
- plt.title("Histogram X")
-
- plt.subplot(122)
- plt.plot(hist_y[1][:-1], [T] * len(hist_y[0]), 'r')
- plt.bar(hist_y[1][:-1], hist_y[0])
- plt.title("Histogram Y")
-
- plt.tight_layout() # 自动调整子图布局,防止重叠
- plt.show()
与我们实际图片进行对比,其检测效果均还不错,threshold,HWratio,Histratio这三个初始值均为经验所得,还是要依照自己的数据来设定。这里的SkeletionMap函数将会获得骨架图中的索引点,它并没有进行去消除毛刺的,实际并不影响,因为我们采用的这个方法,些许毛刺影响不了判断。
现在我们只需要写一个推动裂缝类型的函数,可以用于直接去判断我们设定的裂缝类型:
- def infertype(mask):
- """推导裂缝类型"""
- crack = CrackType()
- H, W = mask.shape[:2]
- mask_copy = mask.copy()
- skeimage, skepoints = SkeletonMap(mask_copy)
- minAreaRect = _get_minAreaRect_information(mask)
- result = crack.classify(minAreaRect, skepoints, HW=(H, W))
- crack_type = crack.types[result]
- return result, crack_type