CrackDect: Expandable crack detection for composite materials.

_images/overview_gif.gif

This package provides crack detection algorithms for tunneling off axis cracks in glass fiber reinforced materials.

If you use this package in publications, please cite the paper.

In this package, crack detection algorithms based on the works of Glud et al. 1 and Bender et al. 2 are implemented. This implementation is aimed to provide a modular “batteries included” package for this crack detection algorithms as well as a framework to preprocess image series to suite the prerequisites of the different crack detection algorithms.

Quick start

To install CrackDect, check at first the Prerequisites of your python installation. Upon meeting all the criteria, the package can be installed with pip, or you can clone or download the repo. If the installed python version or certain necessary packages are not compatible we recommend the use of virtual environments by virtualenv or Conda. See the conda guide for infos about creating and managing Conda environments.

Installation:

Open a command line and check if python is available

$ python --version

This displays the version of the global python environment. If this does not return the python version, something is not working and you need to fix your global python environment.

If all the prerequisites are met CrackDect can be installed in the global environment via pip

$ pip install crackdect

Quick Start shows an illustrative example of the crack detection.

Crack Detection provides a quick theoretical introduction into the crack detection algorithm.

Prerequisites

It is recommended to use virtual environments (anaconda). This package is written and tested in Python 3.8 and relies on here listed packages.

numpy 1.18.5
scipy 1.6.0
sqlalchemy 1.3.23
numba 0.52.0
psutil 5.8.0

And if the visualization module is used PyQt5 is also needed.

Motivation

Most algorithms and methods for scientific research are implemented as in-house code and not accessible for other researchers. Code rarely gets published and implementation details are often not included in papers presenting the results of these algorithms. Our motivation is to provide transparent and modular code with high level functions for crack detection in composite materials and the framework to efficiently apply it to experimental evaluations.

Quick Start

Lets start with initialising an ImageStack. Its a container object for a stack of images. The crack detection works with this container objects to process the whole image stack at once. It is also possible to work just with a list of images if no extra functionality from ImageStack is needed.

Create an stack and add images to it. The images in this example can be downloaded here. Unpack the folder and set the working directory in the parent folder of example_images.

import numpy as np
import crackdect as cd
# read image paths from folder
paths = cd.image_paths('example_images')

# We want the dtype of the images to be np.float32 and only grayscale images.
stack = cd.ImageStack.from_paths(paths,  dtype=np.float32, as_gray=True)

The following image shows the last image from the stack. A lot of small and some bigger cracks are visible. Also, the region of interest is only the middle part of the specimen without the edges of the specimen and the painted black bar.

_images/input_image.png

Before cutting to the desired shape a shift correction is necessary to align all images in a global coordinate system. The following image shows the last image of the stack after the shift correction and the cut to the region of interest.

# shift correction to align all images in one coordinate system
cd.shift_correction(stack)

# The region of interest is form pixel 200-1400 in x and 20-900 in y
cd.region_of_interest(stack, 200, 1400, 20, 900)
_images/roi.png

Currently, three functions using different algorithms for crack detection are available in the package. For this tutorial, detect_cracks(), the simplest crack detection function with the least prerequisites in image preprocessing is used. For more information go to Crack Detection.

Only cracks in a set direction are detected with detect_cracks(). For the algorithm to work properly, the following arguments must be set.

  1. theta: The angle between the cracks and a vertical line.

  2. crack_width: Approximate width of the major detected cracks in pixels. This value is taken as the wavelength of the Gabor kernel.

  3. ar: The aspect ratio of the kernel. Since cracks are usually long and thin an aspect ratio bigger than 1 should be chosen. A good compromise between speed and accuracy is 2. Too big aspect ratios can lead to false detection.

  4. min_size: The minimum length of detected cracks in pixels. Since small artifacts or noise can lead to false detection, this parameter provides an reliable filter.

# crack detection
rho, cracks, thd = cd.detect_cracks(stack, theta=60, crack_width=10, ar=2, bandwidth=1, min_size=10)

The results can be plotted and inspected.

# plot the crack density
import matplotlib.pyplot as plt
plt.plot(np.arange(len(stack)), rho)
_images/plot_rho.png

The crack density is growing with each image. To look if all cracks are detected lets look at the last image in the stack.

# plot the background image and the associated cracks
cd.plot_cracks(stack[-1], cracks[-1])
_images/real_example_cracks.png

Nearly all cracks get detected. Some cracks are too close to each other and the crack detection can not distinguish them. Cracks in other directions are not detected. This image has low contrast so it is hard to detect all the cracks since some are quite faint compared to the background. There is also quite a lot of blur at some cracks. This are the main problems with the crack detection. This image would benefit form an histogram equalization to boost the contrast.

The full script:

import numpy as np
import crackdect as cd
# read image paths from folder
paths = cd.image_paths('example_images')

# We want the dtype of the images to be np.float32 and only grayscale images.
stack = cd.ImageStack.from_paths(paths,  dtype=np.float32, as_gray=True)
# shift correction to align all images in one coordinate system
cd.shift_correction(stack)
# The region of interest is form pixel 200-1400 in x and 20-900 in y
cd.region_of_interest(stack, 200, 1400, 20, 900)
# crack detection
rho, cracks, thd = cd.detect_cracks(stack, theta=60, crack_width=10, ar=2, bandwidth=1, min_size=10)
# plot the background image and the associated cracks
cd.plot_cracks(stack[-1], cracks[-1])

The Image Stack

Since this package is build for processing multiple images the efficient handling of image collections is important. The whole functionality of the package as also available for working with single images but the API of most top level functions is built for image stacks.

The image stack is the core of this package. It works as a container for collections of images. It can hold images of any size and color/grayscale images can be mixed. The only restriction is that all images are from the same data type e.g np.float32, np.unit8, etc. The data type of incoming images is checked automatically and if the types do not match the incoming image is converted.

All image stack classes have the same io structure. Images can be added, removed and altered. Single images are accessed with indices and groups of images via slicing. Currently these image stacks are available:

  • ImageStack: The most basic image stack. It just manages the dtype checks and conversion of incoming images. All images are held in memory (RAM) at all times. When working with just a few images this is the best choice since it adds nearly zero overhead.

  • ImageStackSQL: When working with a large number of images available RAM can become a problem. This container manages the used RAM of the stack. When exceeding set limits it automatically saves the current state of the images to an SQL database. It is built with sqlalchemy for maximum flexibility. It can als be used to save the current state of an image stack for later usage or transferring the data to other locations. Image stacks can be constructed directly from the created databases. The creation of the database is automated and does not need user input. One database can hold multiple stacks so more than one image stack can interact with one database at once. Sqlalchemy handles all transactions with the database.

This is a quick introduction on how the image stack works. The full API documentation is here.

Now a few examples are given on how to use an image stack. Image stacks can be constructed from directly or via convenience methods to automatically load all images from a list of paths.

Basic functionality

This is an example of the basic functionality all image stacks must have to work with the preprocessing funcitons and the crack detection.

Directly construct an image stack. The dtype of the images in the stack should be set. The default is np.float32 since all functions and the crack detection are optimised for handling float images.

import crackdect as cd
stack = cd.ImageStack(dtype=np.float32)

Adding images to the stack directly. Numpy arrays and pillow image objects can be added. PIL images will be converted to numpy arrays.

stack.add_image(img)

To access images from the stack use indices or slices if multiple images should be accessed. The return when slicing will be a new image stack.

stack[0]  # => first image = numpy array
stack[1:4]  # => image stack of the images with index 1-4(not included).
stack[-1]  # => last image of the stack.

Overriding images in a stack works also like for objects in normal lists.

stack[1] = np.random.rand(200,200) * np.linspace(0,1,200)

This overrides the 2nd image in the stack. If the dtype does not fit the image is converted. Multiple images can be overridden at once

stack[1:5] = ['list of 4 images']

But unlike lists 4 images must be given to replace 4 images in the stack. There is no thing as sub-stacks. Removing images also works like for lists.

del stack[4]  # removes the 5th image of the stack
del stack[-3:]  # removes the last 3 images.
stack.remove_image(4)  # the same as del stack[4] but no slicing possible
stack.remove_image()  # removes per default the last image

Advanced Features

ImageStackSQL has more functionality to it. It can be created like the normal ImageStack but it is it is recommended to set the name of the database and the name of the table the images will be stored in. With this it is easy to identify saved results. If no names are set, the object id is taken. The database is created in the current working directory.

stack = cd.ImageStackSQL()  # completely default creation
stack = cd.ImageStackSQL(database='test', stack_name='test_stack1')

Multiple stacks can be connected with one database

stack2 = cd.ImageStackSQL(database='test', stack_name='test_stack2')
stack3 = cd.ImageStackSQL(database='test', stack_name='test_stack3')

Saving and loading is done automatically but only when needed. So it is possible that the stack was altered but the current state is not saved jet. To save the current state call

stack.save_state()

This will save all changes and free the RAM the images used. When images are accessed after this, they are loaded form the databased again.

All stacks can be copied.

new_stack = stack.copy()  # works for all stacks

Stacks with sql connection should be named

new_sql_stack = sql_stack.copy(stack_name='test_stack4')

Copying a normal stack will not use more ram until the images in the new stack are overridden. Copying a stack with sql-connection will create a new table in the database and copy all images to the new table. For big image stacks, this is a costly operation since all images will be loaded at some point, copied to the other table and saved there. It the image stack exceeds its set RAM limits multiple rounds of loading parts of the stack and saving them in the new table may be required.

Convenience Creation

To avoid manually loading all images and putting them into an image stack there are several options to automatically create an image stack. Images are loaded with skimage.io.imread so a huge flexibility is provided to control the loading process which can be controlled with kwargs.

# create from a list of image paths
stack = cd.ImageStack.from_paths(['list of paths'])
# create image stack with database connection. Database and stack_name are optional
stack = cd.ImageStackSQL.from_paths(['list of paths'], 'database', 'stack_name')
# create from previously saved database.
stack = cd.ImageStackSQL.load_from_database('database', 'stack_name')

The simplest form of creating a basic ImageStack is

stack = cd.load_images(['list of paths'])

For more information and more control over the behaviour of the full documentation for imagestacks.

Preprocessing

The preprocessing for the images is a modular process. Since each user might capture the images in a slightly different way it´s impossible to just set up one preprocessing routine and expect it to work for all circumstances. Therefore, the preprocessing is modular. The preprocessing routines included in this package are defined in stack_operations. But these are just some predefined functions for the most important preprocessing steps. Here an example of how to use custom image processing functions with the image stack (imagestack) is shown.

Apply functions

An arbitrary function that takes one image and other arguments can be applied to the whole image stack. The function must return an image and nothing else. Applying such an function to the whole image stack will alter all the images in the stack since the images from the stack are taken as input and are replaced with the output of the function. E.g histogram equalisation for the whole image stack can be done in one line of code.

import crackdect as cd
from skimage import exposure

stack.execute_function(exposure.equalize_adapthist, clip_limit=0.03)

This performs equalize_adapthist on all images in the stack with a clip limit of 0.03. clip_limit is a keyword argument of equalize_adapthist.

With this functionality custom functions can be defined easily without worrying about the image stack. A cascade of different preprocessing functions can be performed on one image stack. This enables a really modular approach and the most flexibility.

def contrast_stretching(img):
    p5, p95 = np.percentile(img, (5, 95))
    return exposure.rescale_intensity(img, in_range=(p5, p95))

stack.execute_function(custom_stretching)

Rolling Operations

Another way to preprocess the images in stacks is to perform an rolling operation on them. A function for rolling operations takes two images as input and returns just one image. Change detection with image differencing is an example.

\(I_d = I_2 - I_1\)

Applying this function to the image stack would look like this:

def simple_differencing(img1, img2):
    return img2-img1

stack.execute_rolling_function(simple_differencing, keep_first=False)

This will evaluate simple_differencing for all images starting from the second image in the stack. The n-th image in the stack is computed with this schema.

\(I_{new}^n = f(I^{n-1}, I^n)\)

Since this schema can only start at the second image, the argument keep_first defines is the first image is deleted after the rolling operation or not. The first image will not be changed since the function is not applied on it.

Predefined Preprocessing Functions

The most important preprocessing for the crack detection is the change detection and shift correction. This package comes with functions for these routines. There are variations for both routines and other useful functions like cutting to the region of interest in stack_operations.

All the functions in stack_operations take an image stack and return the stack with the results of the routine. The images in the stack get changed. If the state of the image stack prior to applying a routine should be kept, copy the stack before.

For more information see the documentation from stack_operations.

Shift Correction

In image series taken during tensile mechanical tests, it often appears that the specimen is moving relative to the background like the specimen in this gif.

_images/shift_example.gif

Since some crack detection algorithms work only with image series with no shift of the specimen all must be aligned in a global coordinate system for them. Otherwise, these algorithms will compute wrong results. Currently, the following functions need an aligned image stack:

  1. detect_cracks_glud()

  2. detect_cracks_bender()

Warning

All shift correction algorithms only take images with the same dimensionality as input. If the dimensionality of just one image differs it will result in an error. When using an ImageStack make sure to ether only add images with the same dimensionality or when using from_paths() to construct the stack, add kwargs to ensure all images are loaded the same way ( parameters for reading )!

Global Shift Correction

There are two methods to correct the global shift of an image and one to correct the shift as well as distortion. The later can be used to correct images where the specimen show significant strain.

  1. biggest_common_sector()

  2. shift_correction()

Both correct the shift of the image with global phase-cross-correlation and cut the image to the biggest common sector that no black borders appear. biggest_common_sector() is more efficient but less accurate.

The following gif shows the corrected image series from above.

_images/shift_corrected.gif

With this corrected image series, crack detection methods that incorporate the history of the image are applicable.

Shift-Distortion Correction

If the distortion due to strain of the specimen is significant, shift_distortion_correction() can be used. This function tracks for subareas of the images to compute global shift, rotation and relative movement between this subareas. The only prerequisite is that for distinct features are visible throughout the whole image stack.

_images/dist_corr_example.gif

A best practice example would be to mark specimen at four positions to enable reliable shift-distortion correction.

_images/specimen_shift_dist_corr.PNG

The red crosses are used to correct the shift and distortion of the image while the blue dotted rectangle would mark the usable region of interest for the crack detection.

Note

If shift and distortion are high it can be necessary to apply the correction twice in a row.

Always check if the shift correction worked properly since its reliability depends on the quality of the images!

Crack Detection

imagestack and stack_operations are not necessary restrained for the usage of crack detection. imagestack provides the framework to conveniently process images stacks. The module crack_detection provides the algorithms for crack detection. In CrackDect the basic crack detection algorithms are implemented without image preprocessing since this step depends on the image quality, imaging techinque etc. Currently, three algorithms are implemented for the detection of multiple straight cracks in a given direction in grayscale images.

  1. detect_cracks()

  2. detect_cracks_glud()

  3. detect_cracks_bender()

Theory

This package features algorithms for the detection of multiple straight cracks in a given direction. The algorithms were developed for computing the crack density semi-transparent composites where transilluminated white light imaging (TWLI) can be used as imaging technique.

_images/tiwli.png

This technique results in a bright image of the specimen with dark crack like this.

_images/input_image_borderless.png

CrackDect works only on images similar to the one above, where cracks appear dark on a bright background. The cracks must be straight, since the currently implemented algorithms work with masks and filters to extract cracks in a specific direction. Therefore, it is possible to only detect one kind of cracks and ignore cracks in other directions.

Glud´s algorithm

The following functions work with the basis of this algorithm:

  1. detect_cracks()

  2. detect_cracks_glud()

While detect_cracks() is a pure implementation of the filter and processing steps, detect_cracks_glud() incorporates cracks detected in the n-1st image to the nth image. Therefore all images must be related and without shift (see Shift Correction). detect_cracks_glud() is basically the full crack detection (without change detection) described by Glud et al. whereas detect_cracks() is only the “crack counting algorithm”. detect_cracks() is more versatile and detects cracks for each image in the given stack without influence of other images. If the position of a crack changes in the image stack, use detect_cracks().

This method from Glud et al. is designed to detect off axis tunneling cracks in composite materials. It works on applying the following filters on the images:

  1. Gabor Filter: The Gabor filter is applied which detects lines in a set direction. Cracks are only detected in the given direction. This allows to separate crack densities from different layers of the laminate.

  2. Threshold: A threshold is applied on the result of the Gabor filter. This separates foreground and background in an image. The default is Yen´s threshold. In the case of the crack detection it separates cracked and intact area. The Result of the threshold for the image is shown in the next image.

_images/pattern.png
  1. Skeletonizing: Since off axis tunneling cracks are aligned with the fibers they are straight. The white bands from the threshold are thinned to a width of one pixel. The algorithm which determines the start and end of each crack relies on only one pixel wide lines. The result of this skeletonizing for a part af the threshold image from above is shown in the next image. This The lines in this image are not continuous. The skeletonizing is done in a rotated coordinate system. This image is rotated back which creates this effect.

_images/skeleton.png
  1. Crack Counting: The cracks are counted in the skeletonized image. The skeletonized image is rotated into a coordinate system where all cracks are vertical (y-direction). Then a loop scans each pixel in each line of pixels in the image. If a crack is found, it follows it down the ydirection until the end of the crack. The coordinates of the beginning and end are saved. After one crack has been detected, it is removed from the image to avoid double detection when the loop runs over the next line of pixels. The following image shows this process.

_images/crack_counting.png
  1. Crack Density: The crack density is computed from the detected cracks with

    \(\rho_c = \frac{\sum_{i=1}^{n} L_i}{AREA}\)

    with \(L_i\) as the length of the i-th crack and \(AREA\) as the area of the image.

  2. Threshold Density: The threshold density is the area which is detected as cracked divided by the total image area. It simply is the ratio of white pixels to the total number of pixels in the threshold image. For series of related images from the same specimen where the cracks grow and new cracks initiate this measure can be taken as an sanity check. If the cracks grow too close to each other the white bands in the threshold image merge. Then the crack density fails to detect two individual cracks since the skeletonizing will result in only one line for two merged bands. The crack density starts to decrease even tho the threshold density still rises. This is a sign that the crack detection reached its limit and the cracks in the images are too close to each other.

The crack density, crack coordinates (start- and endpoints) and the threshold density are the main results of the crack detection.

Disadvantages

The usage of a computed threshold for the separation between cracks and no cracks is “greedy”. This means, cracks will be detected in the image. In images without cracks, artefacts will appear. This problem is dampened with the use of Yen´s threshold. Otsu´s threshold, as used in the original paper is even more “greedy” and will always detect cracks even if there are none.

Note

The results of this algorithm are sensitive to the input parameters especially to the parameters which control the gabor filter. Therefore it is a good practice to try the input parameters on a few images from the preprocessed stack before running the crack detection for the whole stack. The crack detection is resource intensive and can take a long time if lots of images are processed at once.

Bender´s algorithm

is implemented as detect_cracks_bender()

This method introduced by Bender JJ was also developed to detect off-axis cracks in fiber-reinforced polymers. It can only be used for a series of shift-corrected images and needs a background image as reference (the first image in the stack). Therefore, it is not as versatile as detect_cracks() but it has shown good results for image series up to high crack densities. It is also not “greedy” because a hard threshold is used.

The following filters and image processing steps are used to extract the cracks:

  1. Image History: Starting form the second image in the stack, as the first is used as the background image, only darker pixels are taken form the image. This builds on the fact that cracks only result in dark pixels on the image and therefore, brighter pixels are only random noise.

  2. Image division with background 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 than the given directions.

  5. Images are sharpened with an unsharp_mask.

  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 to smooth the cracks and patch small discontinuities.

  8. Binarization of the image. This results in an image with only cracks and background.

  9. Adding prior cracks: The n-1st binarized image is added to add cracks already detected in prior images to the current image since cracks can only grow.

  10. Crack counting with the skeletonizing and scanning method similar to the 4th point of Glud´s algorithm.

Note

This algorithm relies on the fact that cracks only grow. Also, cracks must not move between the images. Therefore, Shift Correction is important for this algorithm. If this prerequisites are nor met, do not use this crack detection method.

Contributing

Clone the repository and add changes to it. Test the changes and make a pull request.

Modules

imagestack

This module provides the core functionality for handling a stack of images at once.

image_functions

Preprocessing functions for single images

stack_operations

Routines for preprocessing image stacks.

io

IO module

crack_detection

Crack detection algorithms

Authors

  • Matthias Drvoderic

License

This project is licensed under the MIT License

Indices and tables

1

J.A. Glud, J.M. Dulieu-Barton, O.T. Thomsen, L.C.T. Overgaard Automated counting of off-axis tunnelling cracks using digital image processing Compos. Sci. Technol., 125 (2016), pp. 80-89

2

Bender JJ, Bak BLV, Jensen SM, Lindgaard E. Effect of variable amplitude block loading on intralaminar crack initiation and propagation in multidirectional GFRP laminate Composites Part B: Engineering. 2021 Jul