"""
Crack detection algorithms
These module contains the different functions for the crack detection. This includes functions for different
sub-algorithms which are used in the final crack detection as well as different methods for the crack detection.
The different crack detection methods are available as functions with an image stack and additional arguments
as input.
"""
import numpy as np
from numpy.lib.utils import deprecate_with_doc
from scipy.signal import convolve
from numba import jit, int32
# from skimage.morphology._skeletonize_cy import _fast_skeletonize
from skimage.morphology._skeletonize import skeletonize_3d
from skimage.morphology import closing
from skimage.transform import rotate
from skimage.filters import gabor_kernel, threshold_otsu, threshold_yen, gaussian, unsharp_mask
from skimage.util import img_as_float
from skimage.filters._gabor import _sigma_prefactor
_THRESHOLDS = {'yen': threshold_yen,
'otsu': threshold_otsu}
[docs]def rotation_matrix_z(phi):
"""
Rotation matrix around the z-axis.
Computes the rotation matrix for the angle phi(radiant) around the z-axis
Parameters
----------
phi: float
rotation angle (radiant)
Returns
-------
R: array
3x3 rotation matrix
"""
s, c = np.sin(phi), np.cos(phi)
return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
def _sigma_gabor(lam, bandwidth=1):
"""
Compute the standard deviation for the gabor filter in dependence of the wavelength and the
bandwidth.
A bandwidth of 1 has shown to lead to good results. The wavelength should be the
average width of a crack in pixel. Measure the width of one major crack in the image
if the cracks are approximately the same width, this is a good approximation. If the cracks
differ vastly in width lean more to the thinner cracks to get a reasonable approximation.
Parameters
----------
lam: float
Wavelength of the gabor filter. This should be approximately the with in pixel of the structures to detect.
bandwidth: float, optional
The bandwidth of the gabor filter.
Returns
-------
simga: float
Standard deviation of the gabor kernel.
"""
return _sigma_prefactor(bandwidth) * lam
@jit(nopython=True, cache=True)
def _find_crack_end(sk_image, start_row, start_col):
"""
Finde the end of one crack.
This algorithm finds the end of one crack. The crack must be aligned approximately vertical. The input image
is scanned and if a crack is found, the algorithm follows it down until the end and overwrites all pixels which
belong to the crack. This is done that the same crack is not found again.
Parameters
----------
sk_image: np.ndarray
Bool-Image where False is background and True is the 1 pixel wide representation of the crack.
start_row: int
Row from where the crack searching begins.
start_col: int
Column from where the crack searching begins.
Returns
-------
crack_end_x: int
X-coordinate of the crack end
crack_end_y: int
Y-coordinate of the crack end.
"""
row_num, col_num = int32(sk_image.shape)
active_row = start_row
active_col = start_col
def check_columns(row, col_numbers):
for i in col_numbers:
if sk_image[row][i]:
return True, i
return False, i
rn = row_num - 1
cn = col_num - 1
while active_row < rn:
sk_image[active_row][active_col] = False
if active_col == 0:
check_cols = [0, 1]
elif active_col == cn:
check_cols = [active_col, active_col - 1]
else:
check_cols = [active_col, active_col - 1, active_col + 1]
b, new_col = check_columns(active_row + 1, check_cols)
if b:
active_col = new_col
else:
return active_row, active_col
active_row += 1
return active_row, active_col
[docs]@jit(nopython=True, cache=True)
def find_cracks(skel_im, min_size):
"""
Find the cracks in a skeletonized image.
This function finds the start and end of the cracks in a skeletonized image. All cracks must be aligned
approximately vertical.
Parameters
----------
skel_im: np.ndarray
Bool-Image where False is background and True is the 1 pixel wide representation of the crack.
min_size: int
Minimal minimal crack length in pixels that is detected.
Returns
-------
cracks: np.ndarray
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
"""
image = skel_im.copy()
row_num, col_num = image.shape
rows = np.arange(0, row_num)
cols = np.arange(0, col_num)
cracks = []
for row in rows:
for col in cols:
if image[row][col]:
# indicating a crack start
crack_start = np.array((row, col), dtype=np.int32)
# search row wise for the crack end
crack_end = np.array(_find_crack_end(image, row, col), dtype=np.int32)
# apply a min_size criterion
x, y = np.subtract(crack_start, crack_end)
if np.hypot(x, y) >= min_size:
# add to cracks
cracks.append((crack_start, crack_end))
return cracks
[docs]def cracks_skeletonize(pattern, theta, min_size=5):
"""
Get the cracks and the skeletonized image from a pattern.
Parameters
----------
pattern: array-like
True/False array representing the white/black image
theta: float
The orientation angle of the cracks in degrees!!
min_size: int
The minimal length of pixels for which will be considered a crack
Returns
-------
cracks: np.ndarray
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
skeletonized: np.ndarray
skeletonized image
"""
# skeletonize for crack finding
# sk = _fast_skeletonize(rotate(pattern, theta, resize=True)) #quicker but results are worse
sk = skeletonize_3d(rotate(pattern, theta, resize=True)).astype(bool)
# backrotate skeletonized image (sk must be of dtype bool)
t = rotate(sk, -theta, resize=True)
y0, x0 = pattern.shape
y1, x1 = t.shape
t = t[int((y1 - y0) / 2): int((y1 + y0) / 2), int((x1 - x0) / 2): int((x1 + x0) / 2)]
# backrotate crack coords
y1, x1 = sk.shape
crack_coords = np.array(find_cracks(sk, min_size)).reshape(-1, 2) - np.array(
(y1 / 2, x1 / 2))
R = rotation_matrix_z(np.radians(-theta))[0:2, 0:2]
return (R.dot(crack_coords.T).T + np.array((y0 / 2, x0 / 2))).reshape(-1, 2, 2), t
[docs]def crack_density(cracks, area):
"""
Compute the crack density from an array of crack coordinates.
The crack density is the combined length of all cracks in a given area.
Therefore, its unit is m^-1.
Parameters
----------
cracks: array-like
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1]], [[...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
area: float
The area to which the density is refered to.
Returns
-------
crack density: float
"""
v = cracks[:, 1, :] - cracks[:, 0, :]
return np.sum(np.hypot(*v.T)) / area
[docs]def anisotropic_gauss_kernel(sig_x, sig_y, theta=0, truncate=3):
"""
Gaussian kernel with different standard deviations in x and y direction.
Parameters
----------
sig_x: int
Standard deviation in x-direction.
A value of e.g. 5 means that the Gaussian kernel will reach a standard deviation of 1 after 5 pixel.
sig_y: int
Standard deviation in y-direction.
theta: float
Angle in degrees
truncate: float
Truncate the filter at this many standard deviations.
Default is 4.0.
Returns
-------
kernel: ndarray
The Gaussian kernel as a 2D array.
"""
r_x = int(truncate * sig_x + 0.5)
r_y = int(truncate * sig_y + 0.5)
xx = np.arange(-r_x, r_x + 1)
yy = np.arange(-r_y, r_y + 1)
sig_x2 = sig_x * sig_x
sig_y2 = sig_y * sig_y
phi_x = np.exp(-0.5 / sig_x2 * xx ** 2)
phi_y = np.exp(-0.5 / sig_y2 * yy ** 2)
kernel = np.outer(phi_x, phi_y)
if theta != 0:
kernel = rotate(kernel, theta, resize=True, order=3)
return kernel / np.sum(kernel)
[docs]class CrackDetectionTWLI:
r"""
The basic method from Glud et al. for crack detection without preprocessing.
This is the basis for a crack detection with this method. Each object from this class
can be used to detect cracks from images. The workflow of objects from this class is quite easy.
#. Object instantiation. Create an object from with the input parameter for the crack detection.
#. Call the method :meth:`~.detect_cracks` with an image as input.
This method will call all sub-functions of the crack detection.
#. apply the gabor filter
#. apply otsu´s threshold to split the image into foreground and background.
#. skeletonize the foreground
#. find the cracks in the skeletonized image.
Shift detection, normalization, and other preprocessing procedures are not performed! It is assumed that
all the necessary preprocessing is already done for the input image. For preprocessing please use
the :mod:`~.stack_operations` or other means.
Parameters
----------
theta: float
Angle of the cracks in respect to a horizontal line in degrees
frequency: float, optional
Frequency of the gabor filter. Default: 0.1
bandwidth: float, optional
The bandwidth of the gabor filter, Default: 1
sigma_x: float, optional
Standard deviation of the gabor kernel in x-direction. This applies to the kernel before rotation. The
kernel is then rotated *theta* degrees.
sigma_y: float, optional
Standard deviation of the gabor kernel in y-direction. This applies to the kernel before rotation. The
kernel is then rotated *theta* degrees.
n_stds: int, optional
The size of the gabor kernel in standard deviations. A smaller kernel is faster but also less accurate.
Default: 3
min_size: int, optional
The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 1
threshold: str
Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'.
Generally, yen is not as sensitive as otsu. For blurry images with lots of noise yen is nearly always
better than otsu.
sensitivity: float, optional
Adds or subtracts x percent of the input image range to the threshold. E.g. sensitivity=-10 will lower
the threshold to determine foreground by 10 percent of the input image range. For crack detection with
bad image quality or lots of artefacts it can be helpful to lower the sensitivity to avoid too much false
detections.
"""
def __init__(self, theta=0, frequency=0.1, bandwidth=1, sigma_x=None, sigma_y=None, n_stds=3,
min_size=5, threshold='yen', sensitivity=0):
self.min_size = min_size
self.sensitivity = sensitivity
self._theta = np.radians(theta)
self.theta_deg = theta
self.threshold = threshold
# Gabor kernel
self.gk = gabor_kernel(frequency, self._theta, bandwidth, sigma_x, sigma_y, n_stds)
self._gk_real = np.real(self.gk)
h, w = self.gk.shape
self.h = int(h / 2)
self.w = int(w / 2)
[docs] def detect_cracks(self, image, out_intermediate_images=False):
"""
Compute all steps of the crack detection
Parameters
----------
image: np.ndarray
out_intermediate_images: bool, optional
If True the result of the gabor filter, the foreground pattern as a result of the otsu´s threshold
and the skeletonized image are also included in the output.
As this are three full sized images the default is False.
Returns
-------
crack_density: float
cracks: np.ndarray
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
threshold_density: float
A measure how much of the area of the input image is detected as foreground. If the gabor filter can not
distinguish between cracks with very little space in between the crack detection will break down and
lead to false results. If this value is high but the crack density is low, this is an indicator that
the crack detection does not work with the given input parameters and the input image.
gabor: np.ndarray, optional
The result of the Gabor filter.
pattern: np.ndarray, optional
A bool image the crack detection detects as cracked area.
skel_image: np.ndarray, optional
The skeletonized pattern as bool image.
"""
# gabor = convolve(image, self._gk_real, mode='same', method='fft')
gabor = self._gabor_image(image)
# apply otsu threshold
pattern = self.foreground_pattern(gabor, self.threshold, self.sensitivity)
# compute threshold density
y, x = pattern.shape
threshold_area = np.sum(pattern)
threshold_density = threshold_area / (x * y)
# find cracks
cracks, skel_img = cracks_skeletonize(pattern, self.theta_deg, self.min_size)
cd = crack_density(cracks, x * y)
if out_intermediate_images:
return cd, cracks, threshold_density, gabor, pattern, skel_img
else:
return cd, cracks, threshold_density
def _gabor_image(self, image):
"""
Apply the gabor filter to an image.
Parameters
----------
image: np.ndarray
Returns
-------
out: Result of the gabor filter for the image.
"""
temp = np.pad(image, ((self.h, self.h), (self.w, self.w)), mode='edge')
return convolve(temp, self._gk_real, mode='same', method='fft')[self.h:-self.h, self.w:-self.w]
[docs] @staticmethod
def foreground_pattern(image, method='yen', sensitivity=0):
"""
Apply the threshold to an image do determine foreground and background of the image.
The result is a bool array with where True is foreground and False background of the image.
The image can be split with image[pattern] into foreground and image[~pattern] into background.
Parameters
----------
image: array-like
method: str
Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'.
sensitivity: float, optional
Adds or subtracts x percent of the input image range to the threshold. E.g. sensitivity=-10 will lower
the threshold to determine foreground by 10 percent of the input image range.
Returns
-------
pattern: numpy.ndarray
Bool image with True as foreground.
"""
threshold = _THRESHOLDS[method](image)
if sensitivity:
i_min, i_max = image.min(), image.max()
threshold += (i_max - i_min) * sensitivity / 100
# check if yen falls on the wrong side of the histogram (swaps foreground and background)
if method == 'yen' and threshold > 0:
histogram, bin_edges = np.histogram(image, bins=256)
temp = bin_edges[np.argmax(histogram)]
if not threshold < temp:
threshold = temp - np.abs(temp - threshold)
pattern = np.full(image.shape, False)
pattern[image <= threshold] = True
return pattern
def __call__(self, image, **kwargs):
return self.detect_cracks(image, **kwargs)
[docs]class CrackDetectionBender:
r"""
Base class for the crack detection `method by J.J. Bender
<https://www.researchgate.net/publication/350967596_Effect_of_variable_amplitude_block_loading_on_intralaminar_crack_initiation_and_propagation_in_multidirectional_GFRP_laminate>`_.
This crack detection algorithm only works on an image stack with consecutive images of one specimen.
The first image is used as the background image.
No cracks are detected in the first image. The images must be aligned for this algorithm to
work correctly. Cracks can only be detected in grayscale images with the same shape.
Following filters are applied to the images:
1: Apply image history. Images must become darker with time. This subsequently reduces noise in the imagestack
2: Image division with reference image (first image of the stack) to remove constant objects.
3: The image is divided by a blurred version of itself to remove the background.
4: A directional Gaussian filter is applied to diminish cracks in other directions.
5: Images are sharpened with an `unsharp_mask <https://scikit-image.org/docs/stable/auto_examples/filters/plot_unsharp_mask.html>`_.
6: A threshold is applied to remove falsely identified cracks or artefacts with a weak signal.
7: Morphological closing of the image with a crack-like footprint.
8: Binarization of the image
9: The n-1st binaritzed image is added.
10: Finde the cracks with the skeletonizing and scanning method.
Parameters
----------
theta: float
Angle of the cracks in respect to a horizontal line in degrees
crack_width: int
The approximate width of an average crack in pixel. This determines the width of the detected features.
threshold: float, optional
Threshold of what is perceived as a crack after all filters. E.g. 0.96 means that all gray values over 0.96
in the filtered image are cracks. This value should be close to 1. A lower value will detect cracks with a
weak signal but more artefacts as well. Default: 5
min_size: int, optional
The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5
Returns
-------
rho_c: float
Crack density [1/px]
cracks: np.ndarray
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
rho_th: float
A measure how much of the area of the input image is detected as foreground. If the gabor filter can not
distinguish between cracks with very little space in between the crack detection will break down and
lead to false results. If this value is high but the crack density is low, this is an indicator that
the crack detection does not work with the given input parameters and the input image.
"""
def __init__(self, theta=0, crack_width=10, threshold=0.96, min_size=None):
self.crack_width = int(crack_width)
self.theta = theta % 360
if threshold >= 1:
raise ValueError('The threshold must be lower than 1!')
self.threshold = threshold
self.gk = anisotropic_gauss_kernel(crack_width, crack_width / 2, -theta, truncate=3)
self.closing_footprint = self.make_footprint(self.crack_width, self.theta)
if min_size is None:
self.min_size = crack_width * 4
else:
self.min_size = min_size
def detect_cracks(self, images):
if len(images) <= 1:
raise ValueError('This crack detection algorithm needs more than one image to detect cracks. The first'
'image in the input stack must be the reference and no cracks will be detected in this'
'image.')
img_0 = img_as_float(images[0], force_copy=True)
hist_img = img_0.copy()
pattern_n1 = np.zeros(img_0.shape, dtype=bool)
cd, cracks, threshold = [0], [np.array([])], [0]
for ind in range(1, len(images)):
# step 1: applying image history on the n-th image. (0, 1) = (black, white)
img_n = img_as_float(images[ind], force_copy=True)
if img_n.shape != hist_img.shape:
raise ValueError(f'The shape of image {ind} and {ind - 1} is {img_n.shape} and {hist_img.shape}.'
f'This is not allowed since all images must have the same shape for this algorithm to'
f'work.')
mask = img_n > hist_img
img_n[mask] = hist_img[mask]
hist_img = img_n.copy()
# step 2: change detection with division. No need for cutoff since values can only range from >0 to 1.
# With history, nth image is always lower than n-1st. -> white = no change, black = change
img_n = np.divide(img_n, img_0, out=np.ones_like(img_n), where=img_0 != 0)
# step 3: division with blurred image
img_n = img_n / gaussian(img_n, sigma=self.crack_width)
# step 4: Directional Gaussian filter
img_n = self.anisotropic_gauss_filter(img_n, self.gk)
# step 5: sharpening image -> will rescale to 0-1
img_n = unsharp_mask(img_n, radius=self.crack_width, amount=2, preserve_range=False)
# step 6: apply threshold to % of the current range of the image
img_n[img_n > self.threshold] = 1
# step 7: morphological closing with line element
img_n = closing(img_n, self.closing_footprint)
# step 8: binarization with threshold of 99% -> only 0 and 1 in image
img_n[img_n < 0.99] = 0
# step 9: computing threshold density and crack density
pattern = ~img_n.astype(bool)
pattern = np.logical_or(pattern, pattern_n1)
pattern_n1 = pattern
y, x = pattern.shape
threshold.append(np.sum(pattern) / (x * y))
c, skel_img = cracks_skeletonize(pattern, self.theta, self.min_size)
cd.append(crack_density(c, x * y))
cracks.append(c)
return cd, cracks, threshold
# TODO find faster convolution method or separate gauss kernel.
# Convolution form scipy.signal is not exactly the same as from scipy.ndimage but takes much longer.
# Convolution from scipy.signal is used. The only difference occurs at the edges of the image but it is ~50x
# faster form testing with 1900x1800 images and a kernel of 77x77. The mean squared error between tests was ~10e-7
# It seems that the padding in scipy.signal.convolve is different since only the edges are affected.
# With the additional padding in this function the difference is even smaller.
@staticmethod
def anisotropic_gauss_filter(image, kernel):
h, w = kernel.shape
h = int(h / 2)
w = int(w / 2)
temp = np.pad(image, ((h, h), (w, w)), mode='reflect')
return convolve(temp, kernel, mode='same', method='fft')[h:-h, w:-w]
@staticmethod
def make_footprint(width, theta):
p_w = max(int(width / 4), 1)
closing_footprint = rotate(np.pad(np.ones((width * 4, p_w), dtype=bool), (p_w, p_w)), -theta, resize=True)
ind = np.argwhere(closing_footprint == 1)
c_min, c_max = np.min(ind, axis=0), np.max(ind, axis=0) + 1
return closing_footprint[c_min[0]: c_max[0], c_min[1]: c_max[1]]
[docs]def detect_cracks(images, theta=0, crack_width=10, ar=2, bandwidth=1, n_stds=3,
min_size=5, threshold='yen', sensitivity=0):
"""
Crack detection based on a simpler version of the algorithm by J.A. Glud.
All images are treated separately.
Parameters
----------
images: ImageStack, list
Image stack or list of grayscale images on which the crack detection will be performed. This algorithm treats
each image separately as no image influences the results of the other images.
theta: float
Angle of the cracks in respect to a horizontal line in degrees
crack_width: int
The approximate width of an average crack in pixel. This determines the width of the detected features.
ar: float
The aspect ratio of the gabor kernel. Since cracks are a lot longer than wide a longer gabor kernel will
automatically detect cracks easier and artifacts are filtered out better. A too large aspect ratio will
result in an big kernel which slows down the computation. Default: 2
bandwidth: float, optional
The bandwidth of the gabor filter, Default: 1
n_stds: int, optional
The size of the gabor kernel in standard deviations. A smaller kernel is faster but also less accurate.
Default: 3
min_size: int, optional
The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5
threshold: str
Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'.
Generally, yen is not as sensitive as otsu. For blurry images with lots of noise yen is nearly always
better than otsu.
sensitivity: float, optional
Adds or subtracts x percent of the input image range to the Otsu-threshold. E.g. sensitivity=-10 will lower
the threshold to determine foreground by 10 percent of the input image range. For crack detection with
bad image quality or lots of artefacts it can be helpful to lower the sensitivity to avoid too much false
detections.
Returns
-------
rho_c: float
Crack density [1/px]
cracks: np.ndarray
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
rho_th: float
A measure how much of the area of the input image is detected as foreground. If the gabor filter can not
distinguish between cracks with very little space in between the crack detection will break down and
lead to false results. If this value is high but the crack density is low, this is an indicator that
the crack detection does not work with the given input parameters and the input image.
"""
frequency = 1 / crack_width
sig = _sigma_gabor(crack_width, bandwidth)
temp = CrackDetectionTWLI(theta, frequency, bandwidth, sig, sig * ar, n_stds, min_size, threshold, sensitivity)
rho_c, cracks, rho_th = [], [], []
for ind, img in enumerate(images):
x, y, z = temp.detect_cracks(img)
rho_c.append(x)
cracks.append(y)
rho_th.append(z)
return rho_c, cracks, rho_th
[docs]def detect_cracks_glud(images, theta=0, crack_width=10, ar=2, bandwidth=1, n_stds=3,
min_size=5, threshold='yen', sensitivity=0):
"""
Crack detection using a slightly modified version of the `algorithm from J.A. Glud.
<https://www.researchgate.net/publication/292077678_Automated_counting_of_off-axis_tunnelling_cracks_using_digital_image_processing>`_
This crack detection algorithm only works on an image stack with consecutive images of one specimen.
In contrast to the original algorithm from J.A. Glud, no change detection is applied in this implementation since
it can be easily applied as a preprocessing step if needed (see :mod:`~.stack_operations`)
Parameters
----------
images: ImageStack, list
Image stack or list with consecutive grayscale images (np.ndarray) of the same shape and aligned.
theta: float
Angle of the cracks in respect to a horizontal line in degrees
crack_width: int
The approximate width of an average crack in pixel. This determines the width of the detected features.
ar: float
The aspect ratio of the gabor kernel. Since cracks are a lot longer than wide a longer gabor kernel will
automatically detect cracks easier and artifacts are filtered out better. A too large aspect ratio will
result in an big kernel which slows down the computation. Default: 2
bandwidth: float, optional
The bandwidth of the gabor filter, Default: 1
n_stds: int, optional
The size of the gabor kernel in standard deviations. A smaller kernel is faster but also less accurate.
Default: 3
min_size: int, optional
The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5
threshold: str
Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'.
Generally, yen is not as sensitive as otsu. For blurry images with lots of noise yen is nearly always
better than otsu.
sensitivity: float, optional
Adds or subtracts x percent of the input image range to the threshold. E.g. sensitivity=-10 will lower
the threshold to determine foreground by 10 percent of the input image range. For crack detection with
bad image quality or lots of artefacts it can be helpful to lower the sensitivity to avoid too much false
detections.
Returns
-------
rho_c: float
Crack density [1/px]
cracks: np.ndarray
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
rho_th: float
A measure how much of the area of the input image is detected as foreground. If the gabor filter can not
distinguish between cracks with very little space in between the crack detection will break down and
lead to false results. If this value is high but the crack density is low, this is an indicator that
the crack detection does not work with the given input parameters and the input image.
"""
frequency = 1 / crack_width
sig = _sigma_gabor(crack_width, bandwidth)
temp = CrackDetectionTWLI(theta, frequency, bandwidth, sig, sig * ar, n_stds, min_size, sensitivity)
rho_c, cracks, rho_th = [], [], []
# pattern of the n-1st image
pattern_nminus1 = np.full(images[0].shape, False)
for ind, img in enumerate(images):
gabor = temp._gabor_image(img)
pattern = temp.foreground_pattern(gabor, threshold, sensitivity)
pattern = pattern | pattern_nminus1
pattern_nminus1 = pattern
y, x = pattern.shape
threshold_area = np.sum(pattern)
rho_th.append(threshold_area / (x * y))
# find cracks
c, skel_img = cracks_skeletonize(pattern, temp.theta_deg, temp.min_size)
rho_c.append(crack_density(c, x * y))
cracks.append(c)
return rho_c, cracks, rho_th
@deprecate_with_doc(msg='This function is deprecated in version 0.2 and will be removed in the next version! Use '
'"detect_cracks_glud" instead!')
def detect_cracks_overloaded(images, theta=0, crack_width=10, ar=2, bandwidth=1, n_stds=3,
min_size=5, threshold='yen', sensitivity=0):
return detect_cracks_glud(images, theta, crack_width, ar, bandwidth, n_stds, min_size, threshold, sensitivity)
[docs]def detect_cracks_bender(images, theta=0, crack_width=10, threshold=0.96, min_size=None):
r"""
Crack detection `algorithm by J.J. Bender.
<https://www.researchgate.net/publication/350967596_Effect_of_variable_amplitude_block_loading_on_intralaminar_crack_initiation_and_propagation_in_multidirectional_GFRP_laminate>`_
This crack detection algorithm only works on an image stack with consecutive images of one specimen.
The first image is used as the background image.
No cracks are detected in the first image. The images must be aligned for this algorithm to
work correctly. Cracks can only be detected in grayscale images with the same shape.
Parameters
----------
images: ImageStack, list
Image stack or list with consecutive grayscale images (np.ndarray) of the same shape and aligned.
theta: float
Angle of the cracks in respect to a horizontal line in degrees
crack_width: int
The approximate width of an average crack in pixel. This determines the width of the detected features.
threshold: float, optional
Threshold of what is perceived as a crack after all filters. E.g. 0.96 means that all gray values over 0.96
in the filtered image are cracks. This value should be close to 1. A lower value will detect cracks with a
weak signal but more artefacts as well. Default: 5
min_size: int, optional
The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5
Returns
-------
rho_c: float
Crack density [1/px]
cracks: np.ndarray
Array with the coordinates of the crack with the following structure:
([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1
the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2).
rho_th: float
A measure how much of the area of the input image is detected as foreground. If the gabor filter can not
distinguish between cracks with very little space in between the crack detection will break down and
lead to false results. If this value is high but the crack density is low, this is an indicator that
the crack detection does not work with the given input parameters and the input image.
"""
cd = CrackDetectionBender(theta, crack_width, threshold, min_size)
return cd.detect_cracks(images)