├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── crackdect ├── __init__.py ├── crack_detection.py ├── image_functions.py ├── imagestack.py ├── io.py ├── stack_operations.py └── visualization.py ├── docs ├── requirements.txt └── source │ ├── _templates │ ├── class.rst │ ├── function.rst │ └── module_template.rst │ ├── conf.py │ ├── crack_detection.rst │ ├── images │ ├── crack_counting.png │ ├── dist_corr_example.gif │ ├── example_images.zip │ ├── input_image.png │ ├── input_image_borderless.png │ ├── overview_gif.gif │ ├── pattern.png │ ├── plot_rho.png │ ├── real_example_cracks.png │ ├── roi.png │ ├── shift_corrected.gif │ ├── shift_example.gif │ ├── skeleton.png │ ├── specimen_shift_dist_corr.PNG │ └── tiwli.png │ ├── imagestack_intro.rst │ ├── index.rst │ ├── preprocessing.rst │ ├── quickstart.rst │ └── shift_correction.rst ├── example_images ├── 01.bmp ├── 02.bmp ├── 03.bmp ├── 04.bmp ├── 05.bmp └── 06.bmp └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx docs 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs docs 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | /docs/source/generated/ 131 | /docs/GIF/ 132 | /docs/build/ 133 | /test_images/ 134 | /sandbox/ 135 | /debug_utils.py 136 | /gif_maker.ipynb 137 | /sandbox.py 138 | /example_images/ -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Build docs in the docs/ directory with Sphinx 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | 11 | # Optionally build your docs in additional formats such as PDF and ePub 12 | formats: all 13 | 14 | # Optionally set the version of Python and requirements required to build your docs 15 | python: 16 | version: 3.8 17 | install: 18 | - requirements: docs/requirements.txt 19 | - method: pip 20 | path: . 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 mattdrvo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CrackDect 2 | 3 | Expandable crack detection for composite materials. 4 | 5 | 6 | 7 | ![alt text](docs/source/images/overview_gif.gif) 8 | 9 | This package provides crack detection algorithms for tunneling off axis cracks in 10 | glass fiber reinforced materials. 11 | 12 | Full paper: [CrackDect: Detecting crack densities in images of fiber-reinforced polymers](https://www.sciencedirect.com/science/article/pii/S2352711021001205) 13 | Full documentation: [https://crackdect.readthedocs.io/en/latest/](https://crackdect.readthedocs.io/en/latest) 14 | 15 | If you use this package in publications, please cite the paper. 16 | 17 | In this package, crack detection algorithms based on the works of Glud et al. [1] and Bender et al. [2] are implemented. 18 | This implementation is aimed to provide a modular "batteries included" package for 19 | this crack detection algorithms as well as a framework to preprocess image series to suite the 20 | prerequisites of the different crack detection algorithms. 21 | 22 | ## Quick start 23 | 24 | To install CrackDect, check at first the [prerequisites](#Prerequisites) of your python installation. 25 | Upon meeting all the criteria, the package can be installed with pip, or you can clone or download the repo. 26 | If the installed python version or certain necessary packages are not compatible we recommend the use 27 | of virtual environments by virtualenv or Conda. 28 | 29 | Installation: 30 | 31 | ```pip install crackdect``` 32 | 33 | ## Prerequisites 34 | 35 | This package is written and tested in Python 3.8. The following packages must be installed. 36 | 37 | * [scikit-image](https://scikit-image.org/>) 0.18.1 38 | * [numpy](https://numpy.org/) 1.18.5 39 | * [scipy](https://www.scipy.org/) 1.6.0 40 | * [matplotlib](https://matplotlib.org/) 3.3.4 41 | * [sqlalchemy](https://www.sqlalchemy.org/) 1.3.23 42 | * [numba](https://numba.pydata.org/) 0.52.0 43 | * [psutil](https://psutil.readthedocs.io/en/latest/) 5.8.0 44 | 45 | ## Motivation 46 | Most algorithms and methods for scientific research are implemented as in-house code and not accessible for other 47 | researchers. Code rarely gets published and implementation details are often not included in papers presenting the 48 | results of these algorithms. Our motivation is to provide transparent and modular code with high level functions 49 | for crack detection in composite materials and the framework to efficiently apply it to experimental evaluations. 50 | 51 | ## Contributing 52 | 53 | Clone the repository and add changes to it. Test the changes and make a pull request. 54 | 55 | ## Authors 56 | * Matthias Drvoderic 57 | 58 | ## License 59 | 60 | This project is licensed under the MIT License. 61 | 62 | ## References 63 | 64 | [1] [J.A. Glud, J.M. Dulieu-Barton, O.T. Thomsen, L.C.T. Overgaard 65 | Automated counting of off-axis tunnelling cracks using digital image processing 66 | Compos. Sci. Technol., 125 (2016), pp. 80-89](https://www.sciencedirect.com/science/article/abs/pii/S0266353816300197?via%3Dihub) 67 | 68 | [2] [Bender JJ, Bak BLV, Jensen SM, Lindgaard E. 69 | Effect of variable amplitude block loading on intralaminar crack initiation and propagation in multidirectional GFRP laminate 70 | Composites Part B: Engineering. 2021 Jul](https://www.researchgate.net/publication/350967596_Effect_of_variable_amplitude_block_loading_on_intralaminar_crack_initiation_and_propagation_in_multidirectional_GFRP_laminate) 71 | -------------------------------------------------------------------------------- /crackdect/__init__.py: -------------------------------------------------------------------------------- 1 | from .crack_detection import detect_cracks, detect_cracks_bender, detect_cracks_glud 2 | from .imagestack import ImageStack, ImageStackSQL 3 | from .io import * 4 | from .stack_operations import * 5 | 6 | __version__ = '0.2' -------------------------------------------------------------------------------- /crackdect/crack_detection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Crack detection algorithms 3 | 4 | These module contains the different functions for the crack detection. This includes functions for different 5 | sub-algorithms which are used in the final crack detection as well as different methods for the crack detection. 6 | The different crack detection methods are available as functions with an image stack and additional arguments 7 | as input. 8 | 9 | """ 10 | import numpy as np 11 | from numpy.lib.utils import deprecate_with_doc 12 | from scipy.signal import convolve 13 | from numba import jit, int32 14 | # from skimage.morphology._skeletonize_cy import _fast_skeletonize 15 | from skimage.morphology._skeletonize import skeletonize_3d 16 | from skimage.morphology import closing 17 | from skimage.transform import rotate 18 | from skimage.filters import gabor_kernel, threshold_otsu, threshold_yen, gaussian, unsharp_mask 19 | from skimage.util import img_as_float 20 | from skimage.filters._gabor import _sigma_prefactor 21 | 22 | 23 | _THRESHOLDS = {'yen': threshold_yen, 24 | 'otsu': threshold_otsu} 25 | 26 | 27 | def rotation_matrix_z(phi): 28 | """ 29 | Rotation matrix around the z-axis. 30 | 31 | Computes the rotation matrix for the angle phi(radiant) around the z-axis 32 | 33 | Parameters 34 | ---------- 35 | phi: float 36 | rotation angle (radiant) 37 | 38 | Returns 39 | ------- 40 | R: array 41 | 3x3 rotation matrix 42 | """ 43 | s, c = np.sin(phi), np.cos(phi) 44 | return np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) 45 | 46 | 47 | def _sigma_gabor(lam, bandwidth=1): 48 | """ 49 | Compute the standard deviation for the gabor filter in dependence of the wavelength and the 50 | bandwidth. 51 | 52 | A bandwidth of 1 has shown to lead to good results. The wavelength should be the 53 | average width of a crack in pixel. Measure the width of one major crack in the image 54 | if the cracks are approximately the same width, this is a good approximation. If the cracks 55 | differ vastly in width lean more to the thinner cracks to get a reasonable approximation. 56 | 57 | Parameters 58 | ---------- 59 | lam: float 60 | Wavelength of the gabor filter. This should be approximately the with in pixel of the structures to detect. 61 | bandwidth: float, optional 62 | The bandwidth of the gabor filter. 63 | 64 | Returns 65 | ------- 66 | simga: float 67 | Standard deviation of the gabor kernel. 68 | """ 69 | return _sigma_prefactor(bandwidth) * lam 70 | 71 | 72 | @jit(nopython=True, cache=True) 73 | def _find_crack_end(sk_image, start_row, start_col): 74 | """ 75 | Finde the end of one crack. 76 | 77 | This algorithm finds the end of one crack. The crack must be aligned approximately vertical. The input image 78 | is scanned and if a crack is found, the algorithm follows it down until the end and overwrites all pixels which 79 | belong to the crack. This is done that the same crack is not found again. 80 | 81 | Parameters 82 | ---------- 83 | sk_image: np.ndarray 84 | Bool-Image where False is background and True is the 1 pixel wide representation of the crack. 85 | start_row: int 86 | Row from where the crack searching begins. 87 | start_col: int 88 | Column from where the crack searching begins. 89 | 90 | Returns 91 | ------- 92 | crack_end_x: int 93 | X-coordinate of the crack end 94 | crack_end_y: int 95 | Y-coordinate of the crack end. 96 | """ 97 | row_num, col_num = int32(sk_image.shape) 98 | 99 | active_row = start_row 100 | active_col = start_col 101 | 102 | def check_columns(row, col_numbers): 103 | for i in col_numbers: 104 | if sk_image[row][i]: 105 | return True, i 106 | return False, i 107 | 108 | rn = row_num - 1 109 | cn = col_num - 1 110 | 111 | while active_row < rn: 112 | sk_image[active_row][active_col] = False 113 | if active_col == 0: 114 | check_cols = [0, 1] 115 | elif active_col == cn: 116 | check_cols = [active_col, active_col - 1] 117 | else: 118 | check_cols = [active_col, active_col - 1, active_col + 1] 119 | 120 | b, new_col = check_columns(active_row + 1, check_cols) 121 | if b: 122 | active_col = new_col 123 | else: 124 | return active_row, active_col 125 | active_row += 1 126 | return active_row, active_col 127 | 128 | 129 | @jit(nopython=True, cache=True) 130 | def find_cracks(skel_im, min_size): 131 | """ 132 | Find the cracks in a skeletonized image. 133 | 134 | This function finds the start and end of the cracks in a skeletonized image. All cracks must be aligned 135 | approximately vertical. 136 | 137 | Parameters 138 | ---------- 139 | skel_im: np.ndarray 140 | Bool-Image where False is background and True is the 1 pixel wide representation of the crack. 141 | min_size: int 142 | Minimal minimal crack length in pixels that is detected. 143 | 144 | Returns 145 | ------- 146 | cracks: np.ndarray 147 | Array with the coordinates of the crack with the following structure: 148 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 149 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 150 | """ 151 | image = skel_im.copy() 152 | row_num, col_num = image.shape 153 | 154 | rows = np.arange(0, row_num) 155 | cols = np.arange(0, col_num) 156 | 157 | cracks = [] 158 | 159 | for row in rows: 160 | for col in cols: 161 | if image[row][col]: 162 | # indicating a crack start 163 | crack_start = np.array((row, col), dtype=np.int32) 164 | 165 | # search row wise for the crack end 166 | crack_end = np.array(_find_crack_end(image, row, col), dtype=np.int32) 167 | 168 | # apply a min_size criterion 169 | x, y = np.subtract(crack_start, crack_end) 170 | if np.hypot(x, y) >= min_size: 171 | # add to cracks 172 | cracks.append((crack_start, crack_end)) 173 | 174 | return cracks 175 | 176 | 177 | def cracks_skeletonize(pattern, theta, min_size=5): 178 | """ 179 | Get the cracks and the skeletonized image from a pattern. 180 | 181 | Parameters 182 | ---------- 183 | pattern: array-like 184 | True/False array representing the white/black image 185 | theta: float 186 | The orientation angle of the cracks in degrees!! 187 | min_size: int 188 | The minimal length of pixels for which will be considered a crack 189 | 190 | Returns 191 | ------- 192 | cracks: np.ndarray 193 | Array with the coordinates of the crack with the following structure: 194 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 195 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 196 | skeletonized: np.ndarray 197 | skeletonized image 198 | """ 199 | # skeletonize for crack finding 200 | 201 | # sk = _fast_skeletonize(rotate(pattern, theta, resize=True)) #quicker but results are worse 202 | 203 | sk = skeletonize_3d(rotate(pattern, theta, resize=True)).astype(bool) 204 | 205 | # backrotate skeletonized image (sk must be of dtype bool) 206 | t = rotate(sk, -theta, resize=True) 207 | y0, x0 = pattern.shape 208 | y1, x1 = t.shape 209 | t = t[int((y1 - y0) / 2): int((y1 + y0) / 2), int((x1 - x0) / 2): int((x1 + x0) / 2)] 210 | 211 | # backrotate crack coords 212 | y1, x1 = sk.shape 213 | crack_coords = np.array(find_cracks(sk, min_size)).reshape(-1, 2) - np.array( 214 | (y1 / 2, x1 / 2)) 215 | R = rotation_matrix_z(np.radians(-theta))[0:2, 0:2] 216 | return (R.dot(crack_coords.T).T + np.array((y0 / 2, x0 / 2))).reshape(-1, 2, 2), t 217 | 218 | 219 | def crack_density(cracks, area): 220 | """ 221 | Compute the crack density from an array of crack coordinates. 222 | 223 | The crack density is the combined length of all cracks in a given area. 224 | Therefore, its unit is m^-1. 225 | 226 | Parameters 227 | ---------- 228 | cracks: array-like 229 | Array with the coordinates of the crack with the following structure: 230 | ([[x0, y0],[x1,y1]], [[...]]) where x0 and y0 are the starting coordinates and x1, y1 231 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 232 | area: float 233 | The area to which the density is refered to. 234 | 235 | Returns 236 | ------- 237 | crack density: float 238 | """ 239 | v = cracks[:, 1, :] - cracks[:, 0, :] 240 | return np.sum(np.hypot(*v.T)) / area 241 | 242 | 243 | def anisotropic_gauss_kernel(sig_x, sig_y, theta=0, truncate=3): 244 | """ 245 | Gaussian kernel with different standard deviations in x and y direction. 246 | 247 | Parameters 248 | ---------- 249 | sig_x: int 250 | Standard deviation in x-direction. 251 | A value of e.g. 5 means that the Gaussian kernel will reach a standard deviation of 1 after 5 pixel. 252 | sig_y: int 253 | Standard deviation in y-direction. 254 | theta: float 255 | Angle in degrees 256 | truncate: float 257 | Truncate the filter at this many standard deviations. 258 | Default is 4.0. 259 | Returns 260 | ------- 261 | kernel: ndarray 262 | The Gaussian kernel as a 2D array. 263 | """ 264 | r_x = int(truncate * sig_x + 0.5) 265 | r_y = int(truncate * sig_y + 0.5) 266 | xx = np.arange(-r_x, r_x + 1) 267 | yy = np.arange(-r_y, r_y + 1) 268 | sig_x2 = sig_x * sig_x 269 | sig_y2 = sig_y * sig_y 270 | 271 | phi_x = np.exp(-0.5 / sig_x2 * xx ** 2) 272 | phi_y = np.exp(-0.5 / sig_y2 * yy ** 2) 273 | kernel = np.outer(phi_x, phi_y) 274 | if theta != 0: 275 | kernel = rotate(kernel, theta, resize=True, order=3) 276 | return kernel / np.sum(kernel) 277 | 278 | 279 | class CrackDetectionTWLI: 280 | r""" 281 | The basic method from Glud et al. for crack detection without preprocessing. 282 | 283 | This is the basis for a crack detection with this method. Each object from this class 284 | can be used to detect cracks from images. The workflow of objects from this class is quite easy. 285 | 286 | #. Object instantiation. Create an object from with the input parameter for the crack detection. 287 | #. Call the method :meth:`~.detect_cracks` with an image as input. 288 | This method will call all sub-functions of the crack detection. 289 | 290 | #. apply the gabor filter 291 | #. apply otsu´s threshold to split the image into foreground and background. 292 | #. skeletonize the foreground 293 | #. find the cracks in the skeletonized image. 294 | 295 | Shift detection, normalization, and other preprocessing procedures are not performed! It is assumed that 296 | all the necessary preprocessing is already done for the input image. For preprocessing please use 297 | the :mod:`~.stack_operations` or other means. 298 | 299 | Parameters 300 | ---------- 301 | theta: float 302 | Angle of the cracks in respect to a horizontal line in degrees 303 | frequency: float, optional 304 | Frequency of the gabor filter. Default: 0.1 305 | bandwidth: float, optional 306 | The bandwidth of the gabor filter, Default: 1 307 | sigma_x: float, optional 308 | Standard deviation of the gabor kernel in x-direction. This applies to the kernel before rotation. The 309 | kernel is then rotated *theta* degrees. 310 | sigma_y: float, optional 311 | Standard deviation of the gabor kernel in y-direction. This applies to the kernel before rotation. The 312 | kernel is then rotated *theta* degrees. 313 | n_stds: int, optional 314 | The size of the gabor kernel in standard deviations. A smaller kernel is faster but also less accurate. 315 | Default: 3 316 | min_size: int, optional 317 | The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 1 318 | threshold: str 319 | Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'. 320 | Generally, yen is not as sensitive as otsu. For blurry images with lots of noise yen is nearly always 321 | better than otsu. 322 | sensitivity: float, optional 323 | Adds or subtracts x percent of the input image range to the threshold. E.g. sensitivity=-10 will lower 324 | the threshold to determine foreground by 10 percent of the input image range. For crack detection with 325 | bad image quality or lots of artefacts it can be helpful to lower the sensitivity to avoid too much false 326 | detections. 327 | """ 328 | 329 | def __init__(self, theta=0, frequency=0.1, bandwidth=1, sigma_x=None, sigma_y=None, n_stds=3, 330 | min_size=5, threshold='yen', sensitivity=0): 331 | self.min_size = min_size 332 | self.sensitivity = sensitivity 333 | self._theta = np.radians(theta) 334 | self.theta_deg = theta 335 | self.threshold = threshold 336 | # Gabor kernel 337 | self.gk = gabor_kernel(frequency, self._theta, bandwidth, sigma_x, sigma_y, n_stds) 338 | self._gk_real = np.real(self.gk) 339 | h, w = self.gk.shape 340 | self.h = int(h / 2) 341 | self.w = int(w / 2) 342 | 343 | def detect_cracks(self, image, out_intermediate_images=False): 344 | """ 345 | Compute all steps of the crack detection 346 | 347 | Parameters 348 | ---------- 349 | image: np.ndarray 350 | out_intermediate_images: bool, optional 351 | If True the result of the gabor filter, the foreground pattern as a result of the otsu´s threshold 352 | and the skeletonized image are also included in the output. 353 | As this are three full sized images the default is False. 354 | 355 | Returns 356 | ------- 357 | crack_density: float 358 | cracks: np.ndarray 359 | Array with the coordinates of the crack with the following structure: 360 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 361 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 362 | threshold_density: float 363 | A measure how much of the area of the input image is detected as foreground. If the gabor filter can not 364 | distinguish between cracks with very little space in between the crack detection will break down and 365 | lead to false results. If this value is high but the crack density is low, this is an indicator that 366 | the crack detection does not work with the given input parameters and the input image. 367 | gabor: np.ndarray, optional 368 | The result of the Gabor filter. 369 | pattern: np.ndarray, optional 370 | A bool image the crack detection detects as cracked area. 371 | skel_image: np.ndarray, optional 372 | The skeletonized pattern as bool image. 373 | """ 374 | # gabor = convolve(image, self._gk_real, mode='same', method='fft') 375 | gabor = self._gabor_image(image) 376 | # apply otsu threshold 377 | pattern = self.foreground_pattern(gabor, self.threshold, self.sensitivity) 378 | # compute threshold density 379 | y, x = pattern.shape 380 | threshold_area = np.sum(pattern) 381 | threshold_density = threshold_area / (x * y) 382 | # find cracks 383 | cracks, skel_img = cracks_skeletonize(pattern, self.theta_deg, self.min_size) 384 | cd = crack_density(cracks, x * y) 385 | if out_intermediate_images: 386 | return cd, cracks, threshold_density, gabor, pattern, skel_img 387 | else: 388 | return cd, cracks, threshold_density 389 | 390 | def _gabor_image(self, image): 391 | """ 392 | Apply the gabor filter to an image. 393 | 394 | Parameters 395 | ---------- 396 | image: np.ndarray 397 | 398 | Returns 399 | ------- 400 | out: Result of the gabor filter for the image. 401 | """ 402 | temp = np.pad(image, ((self.h, self.h), (self.w, self.w)), mode='edge') 403 | return convolve(temp, self._gk_real, mode='same', method='fft')[self.h:-self.h, self.w:-self.w] 404 | 405 | @staticmethod 406 | def foreground_pattern(image, method='yen', sensitivity=0): 407 | """ 408 | Apply the threshold to an image do determine foreground and background of the image. 409 | 410 | The result is a bool array with where True is foreground and False background of the image. 411 | The image can be split with image[pattern] into foreground and image[~pattern] into background. 412 | 413 | Parameters 414 | ---------- 415 | image: array-like 416 | method: str 417 | Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'. 418 | sensitivity: float, optional 419 | Adds or subtracts x percent of the input image range to the threshold. E.g. sensitivity=-10 will lower 420 | the threshold to determine foreground by 10 percent of the input image range. 421 | Returns 422 | ------- 423 | pattern: numpy.ndarray 424 | Bool image with True as foreground. 425 | """ 426 | threshold = _THRESHOLDS[method](image) 427 | 428 | if sensitivity: 429 | i_min, i_max = image.min(), image.max() 430 | threshold += (i_max - i_min) * sensitivity / 100 431 | 432 | # check if yen falls on the wrong side of the histogram (swaps foreground and background) 433 | if method == 'yen' and threshold > 0: 434 | histogram, bin_edges = np.histogram(image, bins=256) 435 | temp = bin_edges[np.argmax(histogram)] 436 | if not threshold < temp: 437 | threshold = temp - np.abs(temp - threshold) 438 | pattern = np.full(image.shape, False) 439 | pattern[image <= threshold] = True 440 | return pattern 441 | 442 | def __call__(self, image, **kwargs): 443 | return self.detect_cracks(image, **kwargs) 444 | 445 | 446 | class CrackDetectionBender: 447 | r""" 448 | Base class for the crack detection `method by J.J. Bender 449 | `_. 450 | 451 | This crack detection algorithm only works on an image stack with consecutive images of one specimen. 452 | The first image is used as the background image. 453 | No cracks are detected in the first image. The images must be aligned for this algorithm to 454 | work correctly. Cracks can only be detected in grayscale images with the same shape. 455 | 456 | Following filters are applied to the images: 457 | 1: Apply image history. Images must become darker with time. This subsequently reduces noise in the imagestack 458 | 2: Image division with reference image (first image of the stack) to remove constant objects. 459 | 3: The image is divided by a blurred version of itself to remove the background. 460 | 4: A directional Gaussian filter is applied to diminish cracks in other directions. 461 | 5: Images are sharpened with an `unsharp_mask `_. 462 | 6: A threshold is applied to remove falsely identified cracks or artefacts with a weak signal. 463 | 7: Morphological closing of the image with a crack-like footprint. 464 | 8: Binarization of the image 465 | 9: The n-1st binaritzed image is added. 466 | 10: Finde the cracks with the skeletonizing and scanning method. 467 | 468 | Parameters 469 | ---------- 470 | theta: float 471 | Angle of the cracks in respect to a horizontal line in degrees 472 | crack_width: int 473 | The approximate width of an average crack in pixel. This determines the width of the detected features. 474 | threshold: float, optional 475 | Threshold of what is perceived as a crack after all filters. E.g. 0.96 means that all gray values over 0.96 476 | in the filtered image are cracks. This value should be close to 1. A lower value will detect cracks with a 477 | weak signal but more artefacts as well. Default: 5 478 | min_size: int, optional 479 | The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5 480 | 481 | Returns 482 | ------- 483 | rho_c: float 484 | Crack density [1/px] 485 | cracks: np.ndarray 486 | Array with the coordinates of the crack with the following structure: 487 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 488 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 489 | rho_th: float 490 | A measure how much of the area of the input image is detected as foreground. If the gabor filter can not 491 | distinguish between cracks with very little space in between the crack detection will break down and 492 | lead to false results. If this value is high but the crack density is low, this is an indicator that 493 | the crack detection does not work with the given input parameters and the input image. 494 | """ 495 | 496 | def __init__(self, theta=0, crack_width=10, threshold=0.96, min_size=None): 497 | self.crack_width = int(crack_width) 498 | self.theta = theta % 360 499 | if threshold >= 1: 500 | raise ValueError('The threshold must be lower than 1!') 501 | self.threshold = threshold 502 | 503 | self.gk = anisotropic_gauss_kernel(crack_width, crack_width / 2, -theta, truncate=3) 504 | self.closing_footprint = self.make_footprint(self.crack_width, self.theta) 505 | if min_size is None: 506 | self.min_size = crack_width * 4 507 | else: 508 | self.min_size = min_size 509 | 510 | def detect_cracks(self, images): 511 | if len(images) <= 1: 512 | raise ValueError('This crack detection algorithm needs more than one image to detect cracks. The first' 513 | 'image in the input stack must be the reference and no cracks will be detected in this' 514 | 'image.') 515 | img_0 = img_as_float(images[0], force_copy=True) 516 | hist_img = img_0.copy() 517 | pattern_n1 = np.zeros(img_0.shape, dtype=bool) 518 | 519 | cd, cracks, threshold = [0], [np.array([])], [0] 520 | for ind in range(1, len(images)): 521 | # step 1: applying image history on the n-th image. (0, 1) = (black, white) 522 | img_n = img_as_float(images[ind], force_copy=True) 523 | if img_n.shape != hist_img.shape: 524 | raise ValueError(f'The shape of image {ind} and {ind - 1} is {img_n.shape} and {hist_img.shape}.' 525 | f'This is not allowed since all images must have the same shape for this algorithm to' 526 | f'work.') 527 | mask = img_n > hist_img 528 | img_n[mask] = hist_img[mask] 529 | hist_img = img_n.copy() 530 | 531 | # step 2: change detection with division. No need for cutoff since values can only range from >0 to 1. 532 | # With history, nth image is always lower than n-1st. -> white = no change, black = change 533 | img_n = np.divide(img_n, img_0, out=np.ones_like(img_n), where=img_0 != 0) 534 | 535 | # step 3: division with blurred image 536 | img_n = img_n / gaussian(img_n, sigma=self.crack_width) 537 | 538 | # step 4: Directional Gaussian filter 539 | img_n = self.anisotropic_gauss_filter(img_n, self.gk) 540 | 541 | # step 5: sharpening image -> will rescale to 0-1 542 | img_n = unsharp_mask(img_n, radius=self.crack_width, amount=2, preserve_range=False) 543 | 544 | # step 6: apply threshold to % of the current range of the image 545 | img_n[img_n > self.threshold] = 1 546 | 547 | # step 7: morphological closing with line element 548 | img_n = closing(img_n, self.closing_footprint) 549 | 550 | # step 8: binarization with threshold of 99% -> only 0 and 1 in image 551 | img_n[img_n < 0.99] = 0 552 | 553 | # step 9: computing threshold density and crack density 554 | pattern = ~img_n.astype(bool) 555 | pattern = np.logical_or(pattern, pattern_n1) 556 | pattern_n1 = pattern 557 | y, x = pattern.shape 558 | threshold.append(np.sum(pattern) / (x * y)) 559 | c, skel_img = cracks_skeletonize(pattern, self.theta, self.min_size) 560 | cd.append(crack_density(c, x * y)) 561 | cracks.append(c) 562 | return cd, cracks, threshold 563 | 564 | # TODO find faster convolution method or separate gauss kernel. 565 | # Convolution form scipy.signal is not exactly the same as from scipy.ndimage but takes much longer. 566 | # Convolution from scipy.signal is used. The only difference occurs at the edges of the image but it is ~50x 567 | # faster form testing with 1900x1800 images and a kernel of 77x77. The mean squared error between tests was ~10e-7 568 | # It seems that the padding in scipy.signal.convolve is different since only the edges are affected. 569 | # With the additional padding in this function the difference is even smaller. 570 | @staticmethod 571 | def anisotropic_gauss_filter(image, kernel): 572 | h, w = kernel.shape 573 | h = int(h / 2) 574 | w = int(w / 2) 575 | temp = np.pad(image, ((h, h), (w, w)), mode='reflect') 576 | return convolve(temp, kernel, mode='same', method='fft')[h:-h, w:-w] 577 | 578 | @staticmethod 579 | def make_footprint(width, theta): 580 | p_w = max(int(width / 4), 1) 581 | closing_footprint = rotate(np.pad(np.ones((width * 4, p_w), dtype=bool), (p_w, p_w)), -theta, resize=True) 582 | ind = np.argwhere(closing_footprint == 1) 583 | c_min, c_max = np.min(ind, axis=0), np.max(ind, axis=0) + 1 584 | return closing_footprint[c_min[0]: c_max[0], c_min[1]: c_max[1]] 585 | 586 | 587 | def detect_cracks(images, theta=0, crack_width=10, ar=2, bandwidth=1, n_stds=3, 588 | min_size=5, threshold='yen', sensitivity=0): 589 | """ 590 | Crack detection based on a simpler version of the algorithm by J.A. Glud. 591 | All images are treated separately. 592 | 593 | Parameters 594 | ---------- 595 | images: ImageStack, list 596 | Image stack or list of grayscale images on which the crack detection will be performed. This algorithm treats 597 | each image separately as no image influences the results of the other images. 598 | theta: float 599 | Angle of the cracks in respect to a horizontal line in degrees 600 | crack_width: int 601 | The approximate width of an average crack in pixel. This determines the width of the detected features. 602 | ar: float 603 | The aspect ratio of the gabor kernel. Since cracks are a lot longer than wide a longer gabor kernel will 604 | automatically detect cracks easier and artifacts are filtered out better. A too large aspect ratio will 605 | result in an big kernel which slows down the computation. Default: 2 606 | bandwidth: float, optional 607 | The bandwidth of the gabor filter, Default: 1 608 | n_stds: int, optional 609 | The size of the gabor kernel in standard deviations. A smaller kernel is faster but also less accurate. 610 | Default: 3 611 | min_size: int, optional 612 | The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5 613 | threshold: str 614 | Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'. 615 | Generally, yen is not as sensitive as otsu. For blurry images with lots of noise yen is nearly always 616 | better than otsu. 617 | sensitivity: float, optional 618 | Adds or subtracts x percent of the input image range to the Otsu-threshold. E.g. sensitivity=-10 will lower 619 | the threshold to determine foreground by 10 percent of the input image range. For crack detection with 620 | bad image quality or lots of artefacts it can be helpful to lower the sensitivity to avoid too much false 621 | detections. 622 | 623 | Returns 624 | ------- 625 | rho_c: float 626 | Crack density [1/px] 627 | cracks: np.ndarray 628 | Array with the coordinates of the crack with the following structure: 629 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 630 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 631 | rho_th: float 632 | A measure how much of the area of the input image is detected as foreground. If the gabor filter can not 633 | distinguish between cracks with very little space in between the crack detection will break down and 634 | lead to false results. If this value is high but the crack density is low, this is an indicator that 635 | the crack detection does not work with the given input parameters and the input image. 636 | """ 637 | 638 | frequency = 1 / crack_width 639 | sig = _sigma_gabor(crack_width, bandwidth) 640 | 641 | temp = CrackDetectionTWLI(theta, frequency, bandwidth, sig, sig * ar, n_stds, min_size, threshold, sensitivity) 642 | rho_c, cracks, rho_th = [], [], [] 643 | for ind, img in enumerate(images): 644 | x, y, z = temp.detect_cracks(img) 645 | rho_c.append(x) 646 | cracks.append(y) 647 | rho_th.append(z) 648 | return rho_c, cracks, rho_th 649 | 650 | 651 | def detect_cracks_glud(images, theta=0, crack_width=10, ar=2, bandwidth=1, n_stds=3, 652 | min_size=5, threshold='yen', sensitivity=0): 653 | """ 654 | Crack detection using a slightly modified version of the `algorithm from J.A. Glud. 655 | `_ 656 | 657 | This crack detection algorithm only works on an image stack with consecutive images of one specimen. 658 | In contrast to the original algorithm from J.A. Glud, no change detection is applied in this implementation since 659 | it can be easily applied as a preprocessing step if needed (see :mod:`~.stack_operations`) 660 | 661 | Parameters 662 | ---------- 663 | images: ImageStack, list 664 | Image stack or list with consecutive grayscale images (np.ndarray) of the same shape and aligned. 665 | theta: float 666 | Angle of the cracks in respect to a horizontal line in degrees 667 | crack_width: int 668 | The approximate width of an average crack in pixel. This determines the width of the detected features. 669 | ar: float 670 | The aspect ratio of the gabor kernel. Since cracks are a lot longer than wide a longer gabor kernel will 671 | automatically detect cracks easier and artifacts are filtered out better. A too large aspect ratio will 672 | result in an big kernel which slows down the computation. Default: 2 673 | bandwidth: float, optional 674 | The bandwidth of the gabor filter, Default: 1 675 | n_stds: int, optional 676 | The size of the gabor kernel in standard deviations. A smaller kernel is faster but also less accurate. 677 | Default: 3 678 | min_size: int, optional 679 | The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5 680 | threshold: str 681 | Method of determining the threshold between foreground and background. Choose between 'otsu' or 'yen'. 682 | Generally, yen is not as sensitive as otsu. For blurry images with lots of noise yen is nearly always 683 | better than otsu. 684 | sensitivity: float, optional 685 | Adds or subtracts x percent of the input image range to the threshold. E.g. sensitivity=-10 will lower 686 | the threshold to determine foreground by 10 percent of the input image range. For crack detection with 687 | bad image quality or lots of artefacts it can be helpful to lower the sensitivity to avoid too much false 688 | detections. 689 | 690 | Returns 691 | ------- 692 | rho_c: float 693 | Crack density [1/px] 694 | cracks: np.ndarray 695 | Array with the coordinates of the crack with the following structure: 696 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 697 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 698 | rho_th: float 699 | A measure how much of the area of the input image is detected as foreground. If the gabor filter can not 700 | distinguish between cracks with very little space in between the crack detection will break down and 701 | lead to false results. If this value is high but the crack density is low, this is an indicator that 702 | the crack detection does not work with the given input parameters and the input image. 703 | """ 704 | 705 | frequency = 1 / crack_width 706 | sig = _sigma_gabor(crack_width, bandwidth) 707 | 708 | temp = CrackDetectionTWLI(theta, frequency, bandwidth, sig, sig * ar, n_stds, min_size, sensitivity) 709 | rho_c, cracks, rho_th = [], [], [] 710 | 711 | # pattern of the n-1st image 712 | pattern_nminus1 = np.full(images[0].shape, False) 713 | for ind, img in enumerate(images): 714 | gabor = temp._gabor_image(img) 715 | pattern = temp.foreground_pattern(gabor, threshold, sensitivity) 716 | pattern = pattern | pattern_nminus1 717 | pattern_nminus1 = pattern 718 | y, x = pattern.shape 719 | threshold_area = np.sum(pattern) 720 | rho_th.append(threshold_area / (x * y)) 721 | # find cracks 722 | c, skel_img = cracks_skeletonize(pattern, temp.theta_deg, temp.min_size) 723 | rho_c.append(crack_density(c, x * y)) 724 | cracks.append(c) 725 | return rho_c, cracks, rho_th 726 | 727 | 728 | @deprecate_with_doc(msg='This function is deprecated in version 0.2 and will be removed in the next version! Use ' 729 | '"detect_cracks_glud" instead!') 730 | def detect_cracks_overloaded(images, theta=0, crack_width=10, ar=2, bandwidth=1, n_stds=3, 731 | min_size=5, threshold='yen', sensitivity=0): 732 | return detect_cracks_glud(images, theta, crack_width, ar, bandwidth, n_stds, min_size, threshold, sensitivity) 733 | 734 | 735 | def detect_cracks_bender(images, theta=0, crack_width=10, threshold=0.96, min_size=None): 736 | r""" 737 | Crack detection `algorithm by J.J. Bender. 738 | `_ 739 | 740 | This crack detection algorithm only works on an image stack with consecutive images of one specimen. 741 | The first image is used as the background image. 742 | No cracks are detected in the first image. The images must be aligned for this algorithm to 743 | work correctly. Cracks can only be detected in grayscale images with the same shape. 744 | 745 | Parameters 746 | ---------- 747 | images: ImageStack, list 748 | Image stack or list with consecutive grayscale images (np.ndarray) of the same shape and aligned. 749 | theta: float 750 | Angle of the cracks in respect to a horizontal line in degrees 751 | crack_width: int 752 | The approximate width of an average crack in pixel. This determines the width of the detected features. 753 | threshold: float, optional 754 | Threshold of what is perceived as a crack after all filters. E.g. 0.96 means that all gray values over 0.96 755 | in the filtered image are cracks. This value should be close to 1. A lower value will detect cracks with a 756 | weak signal but more artefacts as well. Default: 5 757 | min_size: int, optional 758 | The minimal number of pixels a crack can be. Cracks under this size will not get counted. Default: 5 759 | 760 | Returns 761 | ------- 762 | rho_c: float 763 | Crack density [1/px] 764 | cracks: np.ndarray 765 | Array with the coordinates of the crack with the following structure: 766 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 767 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 768 | rho_th: float 769 | A measure how much of the area of the input image is detected as foreground. If the gabor filter can not 770 | distinguish between cracks with very little space in between the crack detection will break down and 771 | lead to false results. If this value is high but the crack density is low, this is an indicator that 772 | the crack detection does not work with the given input parameters and the input image. 773 | """ 774 | cd = CrackDetectionBender(theta, crack_width, threshold, min_size) 775 | return cd.detect_cracks(images) 776 | -------------------------------------------------------------------------------- /crackdect/image_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Preprocessing functions for single images 3 | 4 | In this module image processing functions for single images are grouped as well as helper functions. 5 | """ 6 | import numpy as np 7 | from skimage.exposure import rescale_intensity 8 | from skimage.util.dtype import dtype_range 9 | 10 | 11 | def scale_to(x, x_min=0, x_max=1): 12 | """ 13 | Linear scaling to new range of values. 14 | 15 | This function scales the input values from their current range to the given range. 16 | 17 | Parameters 18 | ---------- 19 | x: array-like 20 | x_min: float 21 | Lower boarder of the new range 22 | x_max: float 23 | Upper boarder of the new range 24 | 25 | Returns 26 | ------- 27 | out: 28 | Scaled values 29 | 30 | Examples 31 | -------- 32 | >>> y = np.array((-1,2,5,7)) 33 | >>> scale_to(y, x_min=0, x_max=1) 34 | array([0. , 0.375, 0.75 , 1. ]) 35 | """ 36 | if x.min() == x_min and x.max() == x_max: 37 | return x 38 | else: 39 | temp = (x - np.min(x)) / (np.max(x) - np.min(x)) 40 | return temp * (x_max - x_min) + x_min 41 | 42 | 43 | def _fast_astype(img, dtype): 44 | if np.issubdtype(img.dtype.type, dtype): 45 | return img 46 | else: 47 | return img.astype(dtype) 48 | 49 | 50 | def detect_changes_division(img1, img2, output_range=None): 51 | """ 52 | Detect the changes between two images by division. 53 | Areas with a lot of change will appear darker and areas with little change bright. 54 | 55 | The change from img1 to img2 is computed with. 56 | 57 | :math:`I_d = \\frac{ img2 + 1}{img1 + 1}` 58 | 59 | The range of the output image is scaled to the range of the datatype of the input image. 60 | 61 | Parameters 62 | ---------- 63 | img1: np.ndarray 64 | img2: np.ndarray 65 | output_range: tuple, optional 66 | Range of the output image. This range must be within the possible range of the dtype of the input images. 67 | E.g. (0,1) for float images. 68 | 69 | Returns 70 | ------- 71 | out: np.ndarray 72 | Input dtpye is only converved when an output_range is given. Else the result will have 73 | dtype float with a maximal possible range of 0.5-2 74 | """ 75 | inp_dtype = img1.dtype.type 76 | img1 = _fast_astype(img1, np.floating) 77 | img2 = _fast_astype(img2, np.floating) 78 | temp = (scale_to(img2) + 1) / (scale_to(img1) + 1) 79 | if output_range is not None: 80 | return _fast_astype(rescale_intensity(temp, out_range=output_range), inp_dtype) 81 | else: 82 | return temp 83 | 84 | 85 | def detect_changes_subtraction(img1, img2, output_range=None): 86 | """ 87 | Simple change detection by image subtracting. 88 | Areas with a lot of change will appear darker and areas with little change bright. 89 | 90 | :math:`I_d = I_2 - I_1` 91 | 92 | Parameters 93 | ---------- 94 | img1: np.ndarray 95 | img2: np.ndarray 96 | output_range: tuple, optional 97 | Range of the output image. This range must be within the possible range of the dtype of the input images 98 | E.g. (0,1) for float images. 99 | 100 | Returns 101 | ------- 102 | out: np.ndarray 103 | Image as input dtype 104 | """ 105 | inp_dtype = img1.dtype.type 106 | img1 = _fast_astype(img1, np.floating) 107 | img2 = _fast_astype(img2, np.floating) 108 | if output_range is not None: 109 | return _fast_astype(rescale_intensity(img2 - img1, out_range=output_range[::-1]), inp_dtype) 110 | else: 111 | return _fast_astype(img2-img1, inp_dtype) 112 | -------------------------------------------------------------------------------- /crackdect/imagestack.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides the core functionality for handling a stack of images at once. 3 | 4 | Image stacks are objects that hold multiple images and act in many cases like python lists. They can 5 | be indexed and images in the stack can be changed. All images in one image stack must have the same dtype. If 6 | an image with another dtype is added or an image in the stack is replaced with an other image with different dtype, 7 | the incoming image is automatically converted to match the dtype of the image stack. 8 | 9 | It is strongly recommended that *np.float32* is used when performing a crack detection. The crack detecion is 10 | tested and developed for images of dtypes *float*, *np.float64*, *np.float32* or *np.float16*. 11 | 12 | Currently, there are two image stack objects that can be used. All image stack have the same structure. 13 | Accessing images, replacing images in the stack and adding new images works the same for all image stacks. 14 | 15 | .. currentmodule:: crackdect.imagestack 16 | 17 | * :class:`ImageStack`: A simple wrapper around a list. This container holds all images in the system memory (RAM). 18 | 19 | * :class:`ImageStackSQL`: Manages RAM usage of the image stack. Images are held in memory as long as the 20 | total available memory does not exceed a certain percentage of available memory or the image stack 21 | exceeds a set number of MB. If any more images are added, all current loaded images get stored in a database and only 22 | references to the images are kept in memory. The images are only loaded when directly accessed. This allows working and 23 | changing images of a stack even if the stack is too big to fit into the memory. The loaded images will be kept in 24 | memory until the stack exceeds the RAM limits again. This reduces the number loading and storing operations and 25 | therefore saves time since this can be quite time consuming for a lot of images. 26 | 27 | The image stack is quite easy to use. 28 | """ 29 | import io 30 | from sqlalchemy import Column, Integer, create_engine, TypeDecorator, LargeBinary 31 | from sqlalchemy.ext.declarative import declarative_base 32 | from sqlalchemy.orm import deferred, sessionmaker 33 | import numpy as np 34 | import psutil 35 | from skimage.util.dtype import _convert 36 | from skimage.io import imread 37 | 38 | 39 | class NumpyType(TypeDecorator): 40 | """ 41 | Numpy Type for sql databases when using sqlalchemy. 42 | 43 | This handles the IO with a sql database and sqlalchemy. 44 | 45 | Inside the database, an numpy array is stored as LargeBinary. sqlalchemy handles 46 | loading and storing of entries for columns marked with this custom type. All arrays are 47 | converted to numpy arrays when loading and converted to binary when storing in the database automatically. 48 | """ 49 | 50 | impl = LargeBinary 51 | 52 | def __init__(self): 53 | super(NumpyType, self).__init__() 54 | 55 | def bind_processor(self, dialect): 56 | impl_processor = self.impl.bind_processor(dialect) 57 | if impl_processor: 58 | 59 | def process(value): 60 | if value is not None: 61 | out = io.BytesIO() 62 | np.save(out, value) 63 | out.seek(0) 64 | return impl_processor(out.read()) 65 | 66 | else: 67 | 68 | def process(value): 69 | if value is not None: 70 | out = io.BytesIO() 71 | np.save(out, value) 72 | out.seek(0) 73 | return out.read() 74 | 75 | return process 76 | 77 | def result_processor(self, dialect, coltype): 78 | impl_processor = self.impl.result_processor(dialect, coltype) 79 | if impl_processor: 80 | 81 | def process(value): 82 | value = impl_processor(value) 83 | if value is None: 84 | return None 85 | value = io.BytesIO(value) 86 | value.seek(0) 87 | return np.load(value) 88 | 89 | else: 90 | 91 | def process(value): 92 | if value is None: 93 | return None 94 | value = io.BytesIO(value) 95 | value.seek(0) 96 | return np.load(value) 97 | 98 | return process 99 | 100 | 101 | def _add_docstring(func): 102 | def inner(function): 103 | if function.__doc__ is None: 104 | function.__doc__ = func.__doc__ 105 | else: 106 | function.__doc__ = func.__doc__ + function.__doc__ 107 | return function 108 | 109 | return inner 110 | 111 | 112 | def _add_to_docstring(docstring): 113 | 114 | def docstring_decorator(func): 115 | if func.__doc__ is None: 116 | func.__doc__ = docstring 117 | else: 118 | func.__doc__ = func.__doc__ + docstring 119 | return func 120 | 121 | return docstring_decorator 122 | 123 | 124 | def _fast_convert(img, dtype): 125 | """ 126 | Check if the image is already the right dtype. 127 | 128 | This will ignore value limits if the image is already the right dtype 129 | 130 | Parameters 131 | ---------- 132 | img: array-like 133 | dtype: 134 | dtype the image should be converted to 135 | 136 | Returns 137 | ------- 138 | image: np.ndarray 139 | """ 140 | if img.dtype.type is dtype: 141 | return img 142 | else: 143 | return _convert(img, dtype) 144 | 145 | 146 | class ImageStack: 147 | """ 148 | This object holds multiple images. All images are converted to the same datatype. This ensures that all 149 | images have the same characteristics for further processing. 150 | 151 | All images are represented as numpy arrays. The same convention for representing images is used as in 152 | skimage. 153 | 154 | If an image with mismatching dtype is added it is automatically converted to match the dtype. 155 | Read more about conversion details at skimage.util.dtype. 156 | 157 | This object behaves a lot like a list. Individual images or groups of images can be retrieved with slicing. 158 | Setitem and delitem behaviour is like with normal python lists but mages can only be added with add_image. 159 | 160 | Parameters 161 | ---------- 162 | dtype: optional, default=np.float32 163 | The dtype all images will be converted to. E.g. np.float32, bool, etc. 164 | 165 | Examples 166 | -------- 167 | >>> # make an ImageStack object where all images are represented as unsigned integer arrays [0-255] 168 | >>> stack = ImageStack(dtype=np.uint8) 169 | >>> # Add an image to it. 170 | >>> img = (np.random.rand(200,200) * np.arange(200))/200 # floating point images must be in range [-1,1] 171 | >>> stack.add_image(img) 172 | This ImageStack can be indexed. 173 | >>> stack[0] # getting the image with index 0 from the stack 174 | Changing an image in the stack. The input will also be converted to the dtype of the stack. 175 | >>> stack[0] = (np.random.rand(200,200) * np.arange(200))/200[::-1] # setting an image in the stack 176 | Or deleting an image form the stack 177 | >>> del stack[0] 178 | """ 179 | def __init__(self, dtype=np.float32): 180 | self._dtype = dtype 181 | self._images = [] 182 | 183 | def add_image(self, img): 184 | """ 185 | Add an image to the stack. The image must be a numpy array 186 | 187 | The input array will be converted to the dtype of the ImageStack 188 | 189 | Parameters 190 | ---------- 191 | img: np.ndarray 192 | """ 193 | self._images.append(_fast_convert(img, dtype=self._dtype)) 194 | 195 | def remove_image(self, i=-1): 196 | """ 197 | Remove an image from the stack. 198 | 199 | Parameters 200 | ---------- 201 | i: int 202 | Index of the image to be removed 203 | """ 204 | self._images.pop(i) 205 | 206 | def __len__(self): return self._images.__len__() 207 | def __repr__(self): return 'ImageStack: {}images, {}'.format(len(self), np.dtype(self._dtype).name) 208 | 209 | def __getitem__(self, i): 210 | if hasattr(i, '__index__'): 211 | i = i.__index__() 212 | 213 | if type(i) is int: 214 | return self._images[i] 215 | elif type(i) is slice: 216 | temp_stack = ImageStack(self._dtype) 217 | temp_stack._images = self._images[i] 218 | return temp_stack 219 | else: 220 | raise TypeError('slicing must be with an int or slice object') 221 | 222 | def __delitem__(self, i): del self._images[i] 223 | 224 | def __setitem__(self, i, item): 225 | if type(i) is int: 226 | return self._images.__setitem__(i, _fast_convert(item, dtype=self._dtype)) 227 | elif type(i) is slice: 228 | if len(item) == len(self._images[i]): 229 | item = [_fast_convert(j, self._dtype) for j in item] 230 | self._images[i] = item 231 | else: 232 | raise ValueError('{} images provided to override {} images!'.format(len(item), len(self._images[i]))) 233 | 234 | # def __add__(self, other): 235 | # if isinstance(other, self.__class__) and self._dtype is other._dtype: 236 | # self._images = self._images.__add__(other._images) 237 | # return self 238 | # else: 239 | # raise TypeError('Only two image stacks with the same image format can be combined!') 240 | 241 | @classmethod 242 | def from_paths(cls, paths, dtype=None, **kwargs): 243 | """ 244 | Make an ImageStack object directly form paths of images. The images will be loaded, converted to the 245 | dtype of the ImageStack and added. 246 | 247 | Parameters 248 | ---------- 249 | paths: list 250 | paths of the images to be added 251 | dtype: optional 252 | The dtype all images will be converted to. E.g. np.float32, bool, etc. 253 | If this is not set, the dtype of the first image loaded will determine the dtype of the stack. 254 | kwargs: 255 | kwargs are forwarded to 256 | `skimage.io.imread `_ 257 | For grayscale images simply add **as_gray = True**. For the kwargs for colored images use 258 | `parameters for reading `_. 259 | Keep in mind that some images might have alpha channels and some not even if they have the same format. 260 | 261 | Returns 262 | ------- 263 | out: ImageStack 264 | An ImageStack with all images from paths as arrays. 265 | 266 | Examples 267 | -------- 268 | >>> paths = ['list of image paths'] 269 | >>> stack = ImageStack.from_paths(paths, as_gray=True) 270 | """ 271 | temp = imread(paths[0], **kwargs) 272 | if dtype is None: 273 | c = cls(temp.dtype.type) 274 | else: 275 | c = cls(dtype) 276 | 277 | c.add_image(temp) 278 | for p in paths[1:]: 279 | c.add_image(imread(p, **kwargs)) 280 | return c 281 | 282 | def change_dtype(self, dtype): 283 | """ 284 | Change the dtype of all images in the stack. All images will be converted to the new dtype. 285 | 286 | Parameters 287 | ---------- 288 | dtype 289 | """ 290 | if self._dtype == dtype: 291 | return 292 | 293 | for i in range(len(self._images)): 294 | self._images[i] = _convert(self._images[i], dtype) 295 | 296 | self._dtype = dtype 297 | 298 | def copy(self): 299 | """ 300 | Copy the current image stack. 301 | 302 | The copy is shallow until images are changed in the new stack. 303 | 304 | Returns 305 | ------- 306 | out: ImageStack 307 | """ 308 | temp = ImageStack(self._dtype) 309 | for i in self._images: 310 | temp.add_image(i) 311 | return temp 312 | 313 | def execute_function(self, func, *args, **kwargs): 314 | """ 315 | Perform an operation on all the images in the stack. 316 | 317 | The operation can be any function which takes one images and other arguments as input and returns 318 | only one image. 319 | 320 | This operation changes the images in the stack. If the current state should be kept copy the stack first. 321 | 322 | Parameters 323 | ---------- 324 | func: function 325 | A function which takes ONE image as first input and returns ONE image. 326 | args: 327 | args are forwarded to the func. 328 | kwargs: 329 | kwargs are forwarded to the func. 330 | 331 | Examples 332 | -------- 333 | >>> def fun(img, to_add): 334 | >>> return img + to_add 335 | 336 | >>> stack.execute_function(fun, to_add=4) 337 | This will apply the function *fun* to all images in the stack. 338 | """ 339 | for ind, img in enumerate(self._images): 340 | self._images[ind] = _fast_convert(func(img, *args, **kwargs), self._dtype) 341 | 342 | def execute_rolling_function(self, func, keep_first=False, *args, **kwargs): 343 | """ 344 | Perform an rolling operation on all the images in the stack. 345 | 346 | The operation can be any function which takes two images and other arguments as input and returns 347 | only one image. 348 | 349 | :math:`I_{new} = func(I_{n-1}, I_n)` 350 | 351 | This operation changes the images in the stack. If the current state should be kept copy the stack first. 352 | 353 | Since the 0-th image in the stack will remain unchanged because the rolling operation starts at the 1-st image, 354 | the 0-th image is removed if *keep_first* is set to *False* (default). 355 | 356 | Parameters 357 | ---------- 358 | func: function 359 | A function which takes TWO images and other arguments as input and returns ONE image. The function must 360 | have the following input structure: `fun(img1, img2, args, kwargs)`. *img1* will be the n-1st image in 361 | the calls. 362 | keep_first: bool 363 | If True, keeps the first image in the stack. Delete it otherwise. 364 | args: 365 | args are forwarded to the func. 366 | kwargs: 367 | kwargs are forwarded to the func. 368 | 369 | Examples 370 | -------- 371 | >>> def fun(img1, img2): 372 | >>> mask = img1 > img1.max()/2 373 | >>> return img2[mask] 374 | 375 | >>> stack.execute_rolling_function(fun, keep_first=False) 376 | This will apply the function *fun* to all images in the stack. 377 | 378 | *img1* is always the n-1st image in the rolling operation. 379 | """ 380 | img_minus1 = self._images[0] 381 | for ind, img in enumerate(self._images[1:]): 382 | self._images[ind + 1] = _fast_convert(func(img_minus1, img, *args, **kwargs), self._dtype) 383 | img_minus1 = img 384 | if not keep_first: 385 | del self._images[0] 386 | 387 | 388 | class ImageStackSQL: 389 | """ 390 | This class works the same as ImageStack. 391 | 392 | ImageStackSQL objects will track the amount of memory the images occupy. When the memory limit if 393 | surpassed, all data will be stored in an sqlite database and the RAM will be cleared. Only a lazy loaded object 394 | is left in the image stack. Only when directly accessing the images in the stack they will be loaded into RAM 395 | again. sqlalchemy is used to connect to the database in which all data is stored. 396 | 397 | This makes this container suitable for long term storage and transfer of a lot of images. 398 | The images can be loaded into an ImageStackSQL object in a new python session. 399 | 400 | Parameters 401 | ---------- 402 | database: str, optional 403 | Path of the database. If it does not exist, it will be created. If none is entered, the name is id(object) 404 | stack_name: str, optional 405 | The name of the table the images will be saved. If none is entered it will be id(object) 406 | dtype: optional, default=np.float32 407 | The dtype all images will be converted to. E.g. np.float32, bool, etc. 408 | max_size_mb: float, optional 409 | The maximal size in mb the image stack is allowed to be. If a new image is added after surpassing this 410 | size all images will be saved in the database and the occupied RAM is cleared. All images are still accessible 411 | but will be loaded only when directly accessed. 412 | cache_limit: float, optional, default=90 413 | The limit of the RAM usage in percent of the available system RAM. When the RAM usage of the system 414 | surpasses this limit, all images will be saved in the database and RAM is freed again even it 415 | max_size_mb is not reached. This makes sure that the system never runs out of RAM. 416 | Values over 100 will effectively deactivate this behaviour. If the total size of the image stack is 417 | too small to free enough RAM to reach the cache limit newly added images will be saved immediately in 418 | the database. This also lead to constant reads from the database as no images will be kept im RAM. Therefore 419 | it is recommended to set this well over the current RAM usage of the system when instantiating an object. 420 | """ 421 | def __init__(self, database='', stack_name='', dtype=np.float32, max_size_mb=None, cache_limit=80): 422 | self._dtype = dtype 423 | 424 | # stack name is the name of the table in the sql database. The table must have a name. 425 | self.stack_name = stack_name if stack_name != '' else 'table'+str(id(self)) 426 | # database name must end with .db 427 | self.database = database if database != '' and database.endswith('.db') else 'db'+str(id(self)) + '.db' 428 | 429 | # sqlalchemy connection 430 | self.engine = create_engine('sqlite:///{}'.format(self.database), echo=False) 431 | self.session = sessionmaker(bind=self.engine)() 432 | self.base = declarative_base() 433 | self.table = type(stack_name, (self.base,), {'__tablename__': self.stack_name, 434 | 'id': Column('id', Integer, primary_key=True), 435 | 'image': deferred(Column('image', NumpyType))}) 436 | self.base.metadata.create_all(self.engine) 437 | 438 | # list for easy access to the images. 439 | self._images = [] 440 | 441 | # ram limits 442 | self._max_nbytes = max_size_mb * 1e6 if max_size_mb is not None else np.inf 443 | self._cache_limit = cache_limit 444 | 445 | # nbytes and counter for caching logic 446 | self._nbytes = 0 447 | self.__counter = 0 448 | 449 | @classmethod 450 | def load_from_database(cls, database='', stack_name=''): 451 | """ 452 | Load an image stack from a database. 453 | 454 | A table of a database which was made with an ImageStackSQL object can be loaded and an ImageStackSQL 455 | object with all the images is made. The dtype of the images in the new object is the same as the images in the 456 | table. All images, which will be added to the object will be converted to match the dtype. 457 | 458 | Parameters 459 | ---------- 460 | database: str 461 | Path of the database. 462 | stack_name: str 463 | Name of the table 464 | 465 | 466 | Returns 467 | ------- 468 | out: ImageStackSQL 469 | The image stack object with connection to the database. 470 | """ 471 | c = cls(database, stack_name, dtype=bool) 472 | dtype = c.session.query(c.table).first().image.dtype 473 | c._dtype = dtype 474 | c._images = c.session.query(c.table).all() 475 | return c 476 | 477 | @classmethod 478 | def from_paths(cls, paths, database='', stack_name='', dtype=None, max_size_mb=None, cache_limit=80, **kwargs): 479 | """ 480 | Make an ImageStackSQL object directly form paths of images. The images will be loaded, converted to the 481 | dtype of the ImageStack and added. 482 | 483 | Parameters 484 | ---------- 485 | paths: list 486 | paths of the images to be added 487 | database: str, optional 488 | Path of the database. If it does not exist, it will be created. If none is entered, the name is id(object) 489 | stack_name: str, optional 490 | The name of the table the images will be saved. If none is entered it will be id(object) 491 | dtype: optional 492 | The dtype all images will be converted to. E.g. np.float32, bool, etc. 493 | If this is not set, the dtype of the first image loaded will determine the dtype of the stack. 494 | max_size_mb: float, optional 495 | :class:`ImageStackSQL` for more details. 496 | cache_limit: float, optional, default=90 497 | :class`ImageStackSQL` for more details. 498 | kwargs: 499 | kwargs are forwarded to 500 | `skimage.io.imread `_ 501 | For grayscale images simply add **as_gray = True**. For the kwargs for colored images use 502 | `parameters for reading `_. 503 | Keep in mind that some images might have alpha channels and some not even if they have the same format. 504 | 505 | Returns 506 | ------- 507 | out: ImageStackSQL 508 | A new ImageStackSQL object with connection to the database. 509 | """ 510 | stack_name = stack_name if stack_name != '' else 'table'+str(id(cls)) 511 | database = database if database != '' and database.endswith('.db') else 'db'+str(id(cls)) + '.db' 512 | 513 | temp = imread(paths[0], **kwargs) 514 | if dtype is None: 515 | c = cls(database, stack_name, temp.dtype.type, max_size_mb, cache_limit) 516 | else: 517 | c = cls(database, stack_name, dtype, max_size_mb, cache_limit) 518 | 519 | c.add_image(temp) 520 | for p in paths[1:]: 521 | c.add_image(imread(p, **kwargs)) 522 | return c 523 | 524 | @property 525 | def nbytes(self): 526 | """ 527 | Sum of bytes for all currently fully loaded images. 528 | 529 | This tracks the used RAM from the images. The overhead of the used RAM from sqlalchemy is not included and 530 | will not be tracked. 531 | """ 532 | return self._nbytes 533 | 534 | @nbytes.setter 535 | def nbytes(self, x): 536 | self._nbytes = x 537 | if x > self._max_nbytes: 538 | self.save_state() 539 | elif self.__counter > 50: 540 | if psutil.virtual_memory().percent > self._cache_limit: 541 | self.save_state() 542 | self.__counter = 0 543 | self.__counter += 1 544 | 545 | @staticmethod 546 | def __is_loaded(sql_obj): 547 | return False if 'image' not in sql_obj.__dict__ else True 548 | 549 | def __get_image(self, sql_obj): 550 | if 'image' not in sql_obj.__dict__: 551 | out = sql_obj.__getattribute__('image') 552 | self.nbytes += out.nbytes 553 | return out 554 | else: 555 | return sql_obj.__getattribute__('image') 556 | 557 | def __set_image(self, img, sql_obj): 558 | temp = _fast_convert(img, self._dtype) 559 | if not self.__is_loaded(sql_obj): 560 | self.nbytes += temp.nbytes 561 | else: 562 | self.nbytes += temp.nbytes - sql_obj.image.nbytes 563 | sql_obj.image = temp 564 | 565 | def __clean_remove(self, sql_obj): 566 | if sql_obj._sa_instance_state.pending: 567 | self.session.expunge(sql_obj) 568 | else: 569 | self.session.delete(sql_obj) 570 | 571 | def __getitem__(self, i): 572 | if hasattr(i, '__index__'): 573 | i = i.__index__() 574 | 575 | if type(i) is int: 576 | return self.__get_image(self._images[i]) 577 | elif type(i) is slice: 578 | temp_objects = self._images[i] 579 | temp_stack = ImageStack(self._dtype) 580 | temp_stack._images = [self.__get_image(j) for j in temp_objects] 581 | return temp_stack 582 | else: 583 | raise TypeError('slicing must be with an int or slice object') 584 | 585 | def __setitem__(self, i, value): 586 | if hasattr(i, '__index__'): 587 | i = i.__index__() 588 | 589 | if type(i) is int: 590 | self.__set_image(value, self._images[i]) 591 | elif type(i) is slice: 592 | temp = self._images[i] 593 | if len(value) == len(temp): 594 | for image, sql_obj in zip(value, temp): 595 | self.__set_image(image, sql_obj) 596 | else: 597 | raise ValueError('{} images provided to override {} images!'.format(len(value), len(temp))) 598 | 599 | def __delitem__(self, i): 600 | if hasattr(i, '__index__'): 601 | i = i.__index__() 602 | 603 | if type(i) is int: 604 | self.remove_image(i) 605 | elif type(i) is slice: 606 | temp = self._images[i] 607 | for j in temp: 608 | if self.__is_loaded(j): 609 | self._nbytes -= j.image.nbytes 610 | self.__clean_remove(j) 611 | del self._images[i] 612 | 613 | def __repr__(self): 614 | s = 'ImageStack: {}images, {}, {:.2f}/{:.2f} MB RAM used, caching at {} percent of system memory' 615 | return s.format(len(self), np.dtype(self._dtype).name, self._nbytes / 1e6, self._max_nbytes / 1e6, self._cache_limit) 616 | 617 | def __len__(self): return self._images.__len__() 618 | 619 | def relaod(self): 620 | """ 621 | Reload the images form the table. All not saved changes will be lost. 622 | """ 623 | self.session.expire_all() 624 | self._images = self.session.query(self.table).all() 625 | self._nbytes = 0 626 | 627 | def save_state(self): 628 | """ 629 | Saves the current state of the image stack to the database. 630 | 631 | This commits all changes and adds all new images to the table. All currently loaded images are expired. This 632 | means, that all RAM used by the images is freed. 633 | 634 | Call this method before closing the python session if the changes made to the image stack should be 635 | saved permanently. 636 | """ 637 | self.session.commit() 638 | self.session.expire_all() 639 | self._nbytes = 0 640 | 641 | @_add_docstring(ImageStack.add_image) 642 | def add_image(self, img): 643 | temp = self.table(image=_fast_convert(img, dtype=self._dtype)) 644 | self._images.append(temp) 645 | self.session.add(temp) 646 | self.nbytes += temp.image.nbytes 647 | 648 | @_add_docstring(ImageStack.remove_image) 649 | def remove_image(self, i=-1): 650 | temp = self._images[i] 651 | if self.__is_loaded(temp): 652 | self._nbytes -= temp.image.nbytes 653 | self.__clean_remove(temp) 654 | self._images.pop(i) 655 | 656 | @_add_docstring(ImageStack.change_dtype) 657 | def change_dtype(self, dtype): 658 | if dtype == self._dtype: 659 | return 660 | 661 | for i in self._images: 662 | bytes_old = i.image.nbytes 663 | i.image = _convert(i.image, dtype) 664 | self.nbytes += i.image.nbytes - bytes_old 665 | 666 | self._dtype = dtype 667 | 668 | def copy(self, stack_name=''): 669 | """ 670 | Copy the current image stack. 671 | 672 | A new table in the database is created where all images are stored. 673 | 674 | Parameters 675 | ---------- 676 | stack_name: str 677 | The name of the stack. This is also the name of the new table in the database 678 | 679 | Returns 680 | ------- 681 | out: ImageStack 682 | """ 683 | 684 | temp = ImageStackSQL(self.database, stack_name, self._dtype, self._max_nbytes/1e6, self._cache_limit) 685 | for i in self._images: 686 | temp.add_image(i.image) 687 | return temp 688 | 689 | @_add_docstring(ImageStack.execute_function) 690 | def execute_function(self, func, *args, **kwargs): 691 | for i in self._images: 692 | bytes_old = i.image.nbytes 693 | i.image = _fast_convert(func(i.image, *args, **kwargs), self.dtype) 694 | self.nbytes += i.image.nbytes - bytes_old 695 | 696 | @_add_docstring(ImageStack.execute_rolling_function) 697 | def execute_rolling_function(self, func, keep_first=False, *args, **kwargs): 698 | img_minus1 = self._images[0].image 699 | for i in self._images[1:]: 700 | temp = i.image.copy() 701 | bytes_old = i.image.nbytes 702 | i.image = _fast_convert(func(img_minus1, i.image, *args, **kwargs), self._dtype) 703 | img_minus1 = temp 704 | self.nbytes += i.image.nbytes - bytes_old 705 | if not keep_first: 706 | self.remove_image(0) 707 | 708 | -------------------------------------------------------------------------------- /crackdect/io.py: -------------------------------------------------------------------------------- 1 | """ 2 | IO module 3 | 4 | Convenience functions for handling image paths, sorting paths and loading images. 5 | 6 | The default dtype for the images is np.float32. This saves memory compared to np.float64 without 7 | significant losses in accuracy since these images are normally represented in a range from 0 -> 1 or -1 -> 1. 8 | """ 9 | 10 | import os 11 | import numpy as np 12 | import re 13 | from .imagestack import ImageStack 14 | from .crack_detection import CrackDetectionTWLI 15 | from skimage.io import imsave 16 | import matplotlib.pyplot as plt 17 | 18 | 19 | def image_paths(img_dir, image_types=('jpg', 'png', 'bmp')): 20 | """ 21 | Selects all images given in the image_types list from a directory 22 | 23 | Parameters 24 | ---------- 25 | img_dir: str 26 | path to the directory which includes the images 27 | image_types: list 28 | a list of strings of the image types to select 29 | 30 | Returns 31 | ------- 32 | image_paths: list 33 | """ 34 | files = os.listdir(img_dir) 35 | paths = np.array([ 36 | os.path.abspath(os.path.join(img_dir, name)) 37 | for name in files 38 | if name.lower().split('.')[-1] in image_types]) 39 | return paths 40 | 41 | 42 | def general_path_sorter(path_list, pattern): 43 | """ 44 | General sorting of paths in ascending order with regex pattern. 45 | 46 | The regex pattern must contain a "group1" which matches any number. The paths are then sorted 47 | with this number in ascending order. 48 | 49 | Parameters 50 | ---------- 51 | path_list: list 52 | List of filenames or paths 53 | pattern: str 54 | regex pattern that matches any number. The group must be marked as "group1". 55 | E.g. "(?P[0-9]+)cycles" would match the number 1234 in "1234cycles". 56 | 57 | Returns 58 | ------- 59 | paths: array 60 | Sorted paths in ascending order 61 | numbers: array 62 | The numbers from the match corresponding to the paths. 63 | """ 64 | if '?P' not in pattern: 65 | raise ValueError('The regex pattern must have a group called "group1".' 66 | 'E.g. image(?P[0-9]+) will match 123 from image123 in the filename.') 67 | 68 | x = np.ones(len(path_list), dtype=bool) 69 | var = [] 70 | for ind, name in enumerate(path_list): 71 | try: 72 | var.append(int(re.search(pattern, os.path.split(name)[1]).group('group1'))) 73 | except AttributeError: 74 | x[ind] = False 75 | 76 | index_array = np.argsort(var) 77 | return np.array(path_list)[x][index_array], np.array(var)[index_array] 78 | 79 | 80 | def sort_paths(path_list, sorting_key='cycles'): 81 | """ 82 | Sorts the given paths according to a sorting keyword. 83 | 84 | The paths must have the following structure: 85 | /xx/xx.../[NUMBER][SORTING_KEY].* 86 | E.g. test_3cycles_force1.png. 87 | This will extract the number 3 from the filename and sort other similar filenames in ascending order. 88 | 89 | Parameters 90 | ---------- 91 | path_list: list 92 | list of strings of the image names 93 | sorting_key: str 94 | A sorting keyword. The number for sorting must be before the keyword. E.g. "cycles" will 95 | sort all paths with [NUMBER]cycles.* e.g. 123cycles, 234cycles,.. etc 96 | 97 | Returns 98 | ------- 99 | paths: array 100 | Sorted paths in ascending order 101 | numbers: array 102 | The numbers from the match corresponding to the paths. 103 | 104 | Examples 105 | -------- 106 | >>> paths = ['A_1cycles.jpg', 'A_50cycles.jpg', 'A_2cycles.jpg', 'A_test.jpg'] 107 | >>> sort_paths(paths, 'cycles') 108 | (array(['A_1cycles.jpg', 'A_2cycles.jpg', 'A_50cycles.jpg']), array([ 1, 2, 50])) 109 | """ 110 | pattern = '((?[0-9]+){}'.format(sorting_key) 111 | return general_path_sorter(path_list, pattern) 112 | 113 | 114 | def load_images(paths, dtype=None, **kwargs): 115 | """ 116 | Loads all images from the paths into memory and stores them in an ImageStack. 117 | 118 | Parameters 119 | ---------- 120 | paths: list 121 | Paths of the images 122 | dtype: dtype, optional 123 | The dtype all images will be converted to. 124 | kwargs: 125 | All kwargs are forwarded to :func:`crackdect.imagestack.ImageStack.from_paths` 126 | 127 | Returns 128 | ------- 129 | out: ImageStack 130 | """ 131 | return ImageStack.from_paths(paths, dtype, **kwargs) 132 | 133 | 134 | def save_images(images, image_format='jpg', names="", directory=None): 135 | """ 136 | Save all images. 137 | 138 | This saves all images in a stack to a given directory in the given file format. 139 | 140 | Parameters 141 | ---------- 142 | images: iterable 143 | An iterable like ImageStack with images represented as np.ndarray 144 | image_format: str, optional 145 | The format in which to save the images. Default is jpg. 146 | names: list, optional 147 | A list of names corresponding to the images. If none is entered the images will be saved as 1.jpg, 2.jpg, ... 148 | If these names have extensions for the image format, this extension is ignored. All images are saved with 149 | image_format 150 | directory: str, optional 151 | Path to the directory where the images should be saved. If the path does not exist it is created. 152 | Default is the current working directory 153 | """ 154 | if names == "": 155 | names = np.arange(len(images)) 156 | if directory is None: 157 | directory = os.getcwd() 158 | elif not os.path.exists(directory): 159 | os.mkdir(directory) 160 | for name, image in zip(names, images): 161 | imsave(os.path.join(directory, os.path.splitext(str(name))[0] + f'.{image_format}'), image, 162 | check_contrast=False) 163 | 164 | 165 | def plot_cracks(image, cracks, linewidth=1, color='red', comparison=False, **kwargs): 166 | """ 167 | Plots cracks in the foreground with an image in the background. 168 | 169 | Parameters 170 | ---------- 171 | image: np.ndarray 172 | Background image 173 | cracks: np.ndarray 174 | Array with the coordinates of the crack with the following structure: 175 | ([[x0, y0],[x1,y1], [...]]) where x0 and y0 are the starting coordinates and x1, y1 176 | the end of one crack. Each crack is represented by a 2x2 array stacked into a bigger array (x,2,2). 177 | kwargs: 178 | Forwarded to `plt.figure() `_ 179 | 180 | Returns 181 | ------- 182 | fig: Figure 183 | ax: Axes 184 | """ 185 | fig = plt.figure(**kwargs) 186 | ax = fig.add_subplot(111) 187 | if comparison: 188 | image = np.hstack((image, image)) 189 | ax.imshow(image, cmap='gray') 190 | for (y0, x0), (y1, x1) in cracks: 191 | ax.plot((x0, x1), (y0, y1), color=color, linewidth=linewidth) 192 | ax.set_ylim(image.shape[0], 0) 193 | ax.set_xlim(0, image.shape[1]) 194 | return fig, ax 195 | 196 | 197 | def plot_gabor(kernel, cmap='plasma', **kwargs): 198 | """ 199 | Plot the real part of the Gabor kernel 200 | 201 | Parameters 202 | ---------- 203 | kernel: CrackDetectionTWLI, np.ndarray 204 | cmap: str 205 | Identifier for a cmap from matplotlib 206 | kwargs: 207 | Forwarded to plt.figure() 208 | 209 | Returns 210 | ------- 211 | fig: Figure 212 | ax: Axes 213 | """ 214 | if isinstance(kernel, CrackDetectionTWLI): 215 | gk = kernel._gk_real 216 | if isinstance(kernel, np.ndarray): 217 | gk = np.real(kernel) 218 | 219 | fig = plt.figure(**kwargs) 220 | ax = fig.add_subplot(111) 221 | 222 | ax.imshow(gk, cmap=cmap) 223 | return fig, ax 224 | -------------------------------------------------------------------------------- /crackdect/stack_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Routines for preprocessing image stacks. 3 | 4 | All functions in this module are designed to take an image stack and additional arguments as input. 5 | 6 | The main functionality consists of different methods for shift correction and change detection for consecutive images. 7 | """ 8 | import numpy as np 9 | import warnings 10 | from scipy.fft import fftn 11 | from skimage.registration import phase_cross_correlation 12 | from skimage.transform import AffineTransform, ProjectiveTransform, warp 13 | from .image_functions import detect_changes_division, detect_changes_subtraction 14 | 15 | 16 | def _stack_operation(stack, function, *args, **kwargs): 17 | """ 18 | Perform an operation for all images of an image stack. 19 | 20 | This is just a wrapper for a function which can perform a procedure for one image to 21 | perform it for all images of a stack instead. 22 | 23 | Parameters 24 | ---------- 25 | stack: ImageStack 26 | The image stack the function should be performed on 27 | function: function 28 | A function which takes ONE image as input and returns ONE image 29 | args: 30 | args are forwarded to the function. 31 | kwargs: 32 | kwargs are forwarded to the function. 33 | """ 34 | for ind, img in enumerate(stack): 35 | stack[ind] = function(img, *args, **kwargs) 36 | return stack 37 | 38 | 39 | def _rolling_stack_operation(stack, function, keep_first=False, *args, **kwargs): 40 | """ 41 | Perform an rolling operation for all images of an image stack. 42 | 43 | :math:`I_{new} = func(I_{n-1}, I_n)` 44 | 45 | This is just a wrapper for a function which can perform a procedure for two subsequent images 46 | for a whole stack. 47 | 48 | Parameters 49 | ---------- 50 | stack: ImageStack 51 | The image stack the function should be performed on 52 | function: function 53 | A function which takes TWO subsequent images as input and returns ONE image 54 | keep_first: bool 55 | If True, keep the first image of the stack. 56 | The function will not be performed on the first image alone! 57 | args: 58 | args are forwarded to the function. 59 | kwargs: 60 | kwargs are forwarded to the function. 61 | """ 62 | img_minus1 = stack[0] 63 | for ind, img in enumerate(stack[1:]): 64 | stack[ind+1] = function(img_minus1, img, *args, **kwargs) 65 | img_minus1 = img 66 | if not keep_first: 67 | del stack[0] 68 | return stack 69 | 70 | 71 | def region_of_interest(images, x0=0, x1=None, y0=0, y1=None): 72 | """ 73 | Crop all images in a stack to the desired shape. 74 | 75 | This function changes the images in the stack. 76 | If the input images should be preserved copy the input to a separate object before! 77 | 78 | The coordinate system is the following: x0->x1 = width, y0->y1 = height from the top left corner of the image 79 | 80 | Parameters 81 | ---------- 82 | images: list, ImageStack 83 | x0: int 84 | x1: int 85 | y0: int 86 | y1: int 87 | 88 | Returns 89 | ------- 90 | out: list, ImageStack 91 | ImageStack or list with the cropped images 92 | """ 93 | for ind, img in enumerate(images): 94 | images[ind] = img[y0:y1, x0:x1] 95 | return images 96 | 97 | 98 | def cut_images_to_same_shape(images): 99 | """ 100 | Cuts all images in a stack to the same shape. 101 | 102 | The images are cut to the shape of the smallest image in the stack. The top left corner is 0,0 and the 103 | 104 | Parameters 105 | ---------- 106 | images: list, ImageStack 107 | 108 | Returns 109 | ------- 110 | out: list, ImageStack 111 | list or ImageStack with all images in the same shape. 112 | """ 113 | shapes = np.array([img.shape[:2] for img in images]) 114 | height, width = shapes.min(axis=0) 115 | if not (np.all(shapes[:, 0] == height) and np.all(shapes[:, 1] == width)): 116 | images = region_of_interest(images, 0, width, 0, height) 117 | return images 118 | 119 | 120 | def image_shift(images): 121 | """ 122 | Compute the shift of all images in a stack. 123 | 124 | The shift of the n+1st image relative to the n-th is computed. The commutative sum of these shifts 125 | is the shift relative to the 0th image in the stack. 126 | 127 | All input images must have the same width and height! 128 | Parameters 129 | ---------- 130 | images: ImageStack, list 131 | 132 | Returns 133 | ------- 134 | out: list 135 | [(0,0), (y1, x1), ...(yn, xn)] The shift in x and y direction relative to the first image in the stack. 136 | """ 137 | n_minus_1 = fftn(images[0], workers=-1) 138 | shift = [np.zeros(len(images[0].shape))] 139 | 140 | for img in images[1:]: 141 | fft_n = fftn(img, workers=-1) 142 | shift.append(phase_cross_correlation(n_minus_1, fft_n, space='fourier', upsample_factor=5)[0]) 143 | n_minus_1 = fft_n 144 | 145 | return np.cumsum(np.array(shift), axis=0)[:, :2] 146 | 147 | 148 | def biggest_common_sector(images): 149 | """ 150 | Biggest common sector of the image stack 151 | 152 | This function computes the relative translation between the images with the first image in the stack as 153 | the reference image. Then the biggest common sector is cropped from the images. The cropping window 154 | moves with the relative translation of the images so that the translation is corrected. 155 | 156 | Warping of the images which could be a result of strain is not accounted for. If the warp cant be neglected 157 | do not use this method!! 158 | 159 | Parameters 160 | ---------- 161 | images: list or ImageStack 162 | Images represented as np.ndarray. All images must have the same dimensionality! If the width and height of 163 | the images is not the same, they are cut to the shape of the smallest image. 164 | 165 | Returns 166 | ------- 167 | out: list, ImageStack 168 | list or ImageStack with the corrected images. 169 | """ 170 | # if all images are the same shape, no need to crop them 171 | cut_images_to_same_shape(images) 172 | height, width = images[0].shape[:2] 173 | 174 | # compute shift relative to the 0th image 175 | total_shift = (np.round(image_shift(images)) * -1).astype(int) 176 | 177 | # minimal and maximal boarders to cut after shift 178 | h_min, w_min = np.abs(np.min(total_shift, axis=0)).astype(int) 179 | h_max, w_max = np.abs(np.max(total_shift, axis=0)).astype(int) 180 | 181 | # cutting out the image 182 | for ind, (n, (t_h, t_w)) in enumerate(zip(images, total_shift)): 183 | images[ind] = n[t_h + h_min:height + t_h - h_max, t_w + w_min: width + t_w - w_max] 184 | 185 | return images 186 | 187 | 188 | def shift_correction(images): 189 | """ 190 | Shift correction of all images in a stack. This function is more precise than :func:`biggest_common_sector` 191 | but more time consuming. The memory footprint is the same. 192 | 193 | This function computes the relative translation between the images with the first image in the stack as 194 | the reference image. The images are translated into the coordinate system of the 0th image form the stack. 195 | 196 | Warping of the images which could be a result of strain is not accounted for. If the warp cant be neglected 197 | do not use this function! 198 | 199 | Parameters 200 | ---------- 201 | images: list, ImageStack 202 | Images represented as np.ndarray. All images must have the same dimensionality! If the width and height of 203 | the images is not the same, they are cut to the shape of the smallest image. 204 | 205 | Returns 206 | ------- 207 | out: list, ImageStack 208 | list or ImageStack with the corrected images. 209 | """ 210 | 211 | cut_images_to_same_shape(images) 212 | height, width = images[0].shape[:2] 213 | 214 | # compute shift relative to the 0th image 215 | total_shift = np.round(image_shift(images)) * -1 216 | 217 | h_min, w_min = np.abs(np.min(total_shift, axis=0).astype(int)) 218 | h_max, w_max = np.abs(np.max(total_shift, axis=0).astype(int)) 219 | 220 | for ind, (img, t) in enumerate(zip(images, total_shift)): 221 | if not (t[0] == 0 and t[1] == 0): 222 | shift = AffineTransform(translation=t[::-1]) 223 | temp = warp(img, shift, mode='constant', cval=0.5, 224 | preserve_range=True)[h_min: height - h_max, w_min: width - w_max].astype(img.dtype.type) 225 | else: 226 | temp = img[h_min: height - h_max, w_min: width - w_max] 227 | images[ind] = temp 228 | return images 229 | 230 | 231 | def shift_distortion_correction(images, 232 | reg_regions=((0, 0, 0.1, 0.1), (0.9, 0, 1, 0.1), (0, 0.9, 0.1, 1), (0.9, 0.9, 1, 1)), 233 | absolute=False): 234 | """ 235 | Shift and distortion (=strain) correction for all images in a stack. 236 | 237 | This function computes the relative translation and the warp between the images with the first image in the stack as 238 | the reference image. The images are translated into the coordinate system of the first image. 239 | Black areas in the resulting images are the result of the transformation into the reference coordinate system. 240 | 241 | Four rectangular areas must be chosen from which the global shift and distortion relative to each other is computed. 242 | 243 | Notes 244 | ----- 245 | For this algorithm to work properly it is advantageous to have markers on reach corner of the images (like crosses). 246 | Make sure that the rectangular areas cover the markers in each image. If no distinct features that 247 | can be tracked are given, this function can result in wrong shift and distortion corrections. In this case, 248 | make sure to check the results before further image processing steps. 249 | 250 | Parameters 251 | ---------- 252 | images: list, ImageStack 253 | Images represented as np.ndarray. All images must have the same dimensionality! If the width and height of 254 | the images is not the same, they are cut to the shape of the smallest image. 255 | reg_regions: tuple 256 | A tuple of four tuples containing the upper left and lower right corners of the rectangles for the areas where 257 | the phase-cross-correlation is computed. E.g. ((x1, y1, x2, y2), (...), (...), (...)) where x1, y1 etc. can be 258 | relative dimensions of the image or the absolute coordinates in pixel. Default is relative. 259 | For relative coordinates enter values from 0-1. If values above 1 are given, absolute coordinate values 260 | are assumed. 261 | absolute: bool 262 | True if absolute and False if relative values are given in reg_regions 263 | 264 | Returns 265 | ------- 266 | out: list, ImageStack 267 | list or ImageStack with the corrected images. 268 | """ 269 | # check if reg_regions is valide 270 | reg = np.array(reg_regions) 271 | if reg.shape != (4, 4): 272 | raise ValueError('Regions for distortion registration must be given as ((x1, y1, x2, y2), (..), (..), (..))') 273 | # split into x and y coordinates 274 | x = reg[:, [0, 2]] 275 | y = reg[:, [1, 3]] 276 | # check shape of the images in the stack. If the shape is different, cut to smallest image 277 | cut_images_to_same_shape(images) 278 | 279 | # if reg_regions are given as relative lengths, compute the absolute coordinates 280 | height, width = images[0].shape[:2] 281 | if not absolute: 282 | # if any entry in reg_regions > 1 but flag = False => it is assumed that the User forgot to set flag to True 283 | if np.any(reg > 1): 284 | msg = 'Values in reg_regions > 1. Therefore it is assumed that the regions are given in ' \ 285 | 'absolute coordinates and not relative to the image dimensions' 286 | warnings.warn(msg) 287 | else: 288 | x = (x * width).astype(int) 289 | y = (y * height).astype(int) 290 | 291 | # clean up absolute coordinates 292 | x[x > width] = width 293 | x[x < 0] = 0 294 | y[y > height] = height 295 | y[y < 0] = 0 296 | 297 | # source coordinates (middle points of the regions in the first image) 298 | src = np.array((np.mean(x, axis=1, dtype=int), np.mean(y, axis=1, dtype=int))).T 299 | 300 | # fft for all regions of the first image 301 | img = images[0] 302 | p1_nminus1 = fftn(img[y[0, 0]:y[0, 1], x[0, 0]:x[0, 1]], workers=-1) 303 | p2_nminus1 = fftn(img[y[1, 0]:y[1, 1], x[1, 0]:x[1, 1]], workers=-1) 304 | p3_nminus1 = fftn(img[y[2, 0]:y[2, 1], x[2, 0]:x[2, 1]], workers=-1) 305 | p4_nminus1 = fftn(img[y[3, 0]:y[3, 1], x[3, 0]:x[3, 1]], workers=-1) 306 | 307 | dx = [] 308 | for i in range(1, len(images)): 309 | img = images[i] 310 | # fft for all regions of the nth image (n>=1) 311 | p1_n = fftn(img[y[0, 0]:y[0, 1], x[0, 0]:x[0, 1]], workers=-1) 312 | p2_n = fftn(img[y[1, 0]:y[1, 1], x[1, 0]:x[1, 1]], workers=-1) 313 | p3_n = fftn(img[y[2, 0]:y[2, 1], x[2, 0]:x[2, 1]], workers=-1) 314 | p4_n = fftn(img[y[3, 0]:y[3, 1], x[3, 0]:x[3, 1]], workers=-1) 315 | 316 | # compute the relative shift of the regions from the n-1st to the nth image 317 | dx1 = phase_cross_correlation(p1_nminus1, p1_n, space='fourier', upsample_factor=5)[0] 318 | dx2 = phase_cross_correlation(p2_nminus1, p2_n, space='fourier', upsample_factor=5)[0] 319 | dx3 = phase_cross_correlation(p3_nminus1, p3_n, space='fourier', upsample_factor=5)[0] 320 | dx4 = phase_cross_correlation(p4_nminus1, p4_n, space='fourier', upsample_factor=5)[0] 321 | 322 | # n-1st regions 323 | p1_nminus1, p2_nminus1, p3_nminus1, p4_nminus1 = p1_n, p2_n, p3_n, p4_n 324 | 325 | dx.append(np.array((dx1[:2], dx2[:2], dx3[:2], dx4[:2]))) 326 | 327 | # total shift of all regions 328 | dx_total = np.cumsum(np.array(dx), axis=0)[:, :, [1, 0]] 329 | p = ProjectiveTransform() 330 | for i, dx in zip(range(1, len(images)), dx_total): 331 | # backtransformation of the nth image (n>=1) to the coordinate system of the 1st image 332 | p.estimate(src + dx, src) 333 | images[i] = warp(images[i], p, mode='constant', cval=0, preserve_range=True) 334 | 335 | return images 336 | 337 | 338 | def change_detection_division(images, output_range=None): 339 | """ 340 | Change detection for all images in an image stack. 341 | 342 | Change detection with image rationing is applied to an image stack. 343 | The new images are the result of the change between the n-th and the n-1st image. 344 | 345 | The first image will be deleted from the stack. 346 | 347 | Parameters 348 | ---------- 349 | images: ImageStack, list 350 | output_range: tuple, optional 351 | The resulting images will be rescaled to the given range. E.g. (0,1). 352 | 353 | Returns 354 | ------- 355 | out: ImageStack, list 356 | """ 357 | return _rolling_stack_operation(images, detect_changes_division, output_range=output_range) 358 | 359 | 360 | def change_detection_subtraction(images, output_range=None): 361 | """ 362 | Change detection for all images in an image stack. 363 | 364 | Change detection with image differencing is applied to an image stack. 365 | The new images are the result of the change between the n-th and the n-1st image. 366 | 367 | The first image will be deleted from the stack. 368 | 369 | Parameters 370 | ---------- 371 | images: ImageStack, list 372 | output_range: tuple, optional 373 | The resulting images will be rescaled to the given range. E.g. (0,1). 374 | 375 | Returns 376 | ------- 377 | out: ImageStack, list 378 | """ 379 | return _rolling_stack_operation(images, detect_changes_subtraction, output_range=output_range) 380 | -------------------------------------------------------------------------------- /crackdect/visualization.py: -------------------------------------------------------------------------------- 1 | """ 2 | Viewer for an image stack. 3 | 4 | This viewer will run in an separate thread. Therefore it does not function in all versions of jupyter. 5 | 6 | Call the viewer with 7 | 8 | >>> from crackdect.visualization import show_images 9 | >>> show_images(stack) 10 | 11 | and navigate the viewer with the arrow keys <- and -> or the mouse wheel. 12 | 13 | Additional kwargs for the plot can be set like this 14 | 15 | >>> show_images(stack, plt_args=dict(cmap='gray')) 16 | """ 17 | import sys 18 | from PyQt5.QtGui import QWheelEvent, QKeyEvent 19 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar 20 | from matplotlib.figure import Figure 21 | from skimage.viewer.qt import QtWidgets, QtCore 22 | 23 | # import matplotlib 24 | # matplotlib.use('Qt5Agg') 25 | 26 | 27 | class MplCanvas(FigureCanvasQTAgg): 28 | 29 | def __init__(self, parent=None, width=5, height=4, dpi=100): 30 | fig = Figure(figsize=(width, height), dpi=dpi) 31 | self.axes = fig.add_subplot(111) 32 | super(MplCanvas, self).__init__(fig) 33 | 34 | 35 | class ImageStackViewer(QtWidgets.QWidget): 36 | def __init__(self, image_stack, plt_args=None, *args, **kwargs): 37 | super(ImageStackViewer, self).__init__(*args, **kwargs) 38 | # main layout 39 | self.layout = QtWidgets.QVBoxLayout() 40 | # image stack 41 | self._stack = image_stack 42 | self._plt_args = plt_args if plt_args is not None else {} 43 | 44 | # matplotlib canvas and toolbar 45 | self.canvas = MplCanvas(self) 46 | toolbar = NavigationToolbar(self.canvas, self) 47 | 48 | self.layout.addWidget(toolbar) 49 | self.layout.addWidget(self.canvas) 50 | 51 | # toggle axis on and off 52 | 53 | self.setLayout(self.layout) 54 | 55 | self._plot_ref = None 56 | self.__update_plot(0) 57 | 58 | self.ind = 0 59 | 60 | # self.show() 61 | 62 | def __update_plot(self, ind): 63 | if self._plot_ref is None: 64 | self._plot_ref = self.canvas.axes.imshow(self._stack[ind], **self._plt_args) 65 | else: 66 | self._plot_ref.set_data(self._stack[ind]) 67 | self.canvas.draw() 68 | 69 | def __add_to_index(self, number): 70 | if self.ind + number > len(self._stack) - 1: 71 | self.ind = 0 72 | elif self.ind + number < 0: 73 | self.ind = len(self._stack) - 1 74 | else: 75 | self.ind = self.ind + number 76 | self.__update_plot(self.ind) 77 | 78 | def wheelEvent(self, event: QWheelEvent) -> None: 79 | delta = event.angleDelta().y() 80 | x = (delta and delta // abs(delta)) 81 | self.__add_to_index(x) 82 | 83 | def keyPressEvent(self, event: QKeyEvent) -> None: 84 | if event.key() == QtCore.Qt.Key_Left: 85 | self.__add_to_index(-1) 86 | elif event.key() == QtCore.Qt.Key_Right: 87 | self.__add_to_index(1) 88 | 89 | def set_axis_visible(self): 90 | self.canvas.axes.get_xaxis().set_visible(False) 91 | self.canvas.axes.get_yaxis().set_visible(False) 92 | 93 | 94 | def show_images(image_stack, plt_args=None, **kwargs): 95 | """ 96 | Create a viewer for multiple images. 97 | 98 | Parameters 99 | ---------- 100 | image_stack: ImageStack or list 101 | plt_args: dict 102 | Forwarded to 103 | `imshow `_. 104 | E.g. dict(cmap='gray') for plotting in grayscale 105 | kwargs: 106 | forwarded to QtWidgets.QWidget 107 | """ 108 | if not QtWidgets.QApplication.instance(): 109 | app = QtWidgets.QApplication(sys.argv) 110 | else: 111 | app = QtWidgets.QApplication.instance() 112 | main = ImageStackViewer(image_stack, plt_args=plt_args, **kwargs) 113 | main.show() 114 | return main 115 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc 2 | sphinx_rtd_theme 3 | psutil -------------------------------------------------------------------------------- /docs/source/_templates/class.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline}} 2 | 3 | .. autoclass:: {{ fullname }} 4 | :members: 5 | -------------------------------------------------------------------------------- /docs/source/_templates/function.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline}} 2 | 3 | .. autofunction:: {{ fullname }} -------------------------------------------------------------------------------- /docs/source/_templates/module_template.rst: -------------------------------------------------------------------------------- 1 | {{ name | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block classes %} 6 | {% if classes %} 7 | .. rubric:: Classes 8 | 9 | .. autosummary:: 10 | :toctree: {{ objname }} 11 | :template: class.rst 12 | {% for item in classes %} 13 | {{ item }} 14 | {%- endfor %} 15 | {% endif %} 16 | {% endblock %} 17 | 18 | {% block functions %} 19 | {% if functions %} 20 | .. rubric:: Functions 21 | 22 | .. autosummary:: 23 | :toctree: {{ objname }} 24 | :template: function.rst 25 | {% for item in functions %} 26 | {{ item }} 27 | {% endfor %} 28 | {% endif %} 29 | {% endblock %} 30 | 31 | 32 | {% block exceptions %} 33 | {% if exceptions %} 34 | .. rubric:: Exceptions 35 | 36 | .. autosummary:: 37 | {% for item in exceptions %} 38 | {{ item }} 39 | {%- endfor %} 40 | {% endif %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -- Project import ----------------------------------------------------------- 2 | from crackdect import * 3 | # -- General configuration --------------------------------------------------- 4 | 5 | needs_sphinx = '3.5' 6 | 7 | extensions = ['sphinx.ext.autodoc', # autodocumentation module 8 | # 'sphinx.ext.imgmath', # mathematical expressions can be rendered as png images 9 | 'numpydoc', # docs in numpy-style 10 | 'sphinx.ext.napoleon', # numpy docstrings 11 | 'sphinx.ext.intersphinx', 12 | 'sphinx.ext.coverage', 13 | 'sphinx.ext.autosummary', # make autosummarys 14 | 'sphinx.ext.viewcode', 15 | 'sphinx.ext.autosectionlabel', 16 | # 'sphinx.ext.readthedocks' 17 | ] 18 | 19 | 20 | templates_path = ['_templates'] 21 | 22 | # List of patterns, relative to source directory, that match files and 23 | # directories to ignore when looking for source files. 24 | # This pattern also affects html_static_path and html_extra_path. 25 | exclude_patterns = [] 26 | 27 | add_module_names = False 28 | 29 | source_suffix = '.rst' 30 | master_doc = 'index' 31 | pygments_style = 'sphinx' 32 | html_show_sourcelink = True 33 | 34 | # -- Render options --------- 35 | 36 | # imgmath_image_format = 'svg' # This would render mathematical expressions as svg images 37 | imgmath_latex_preamble = r'\usepackage{xcolor}' # mathematical expressions can be colored 38 | 39 | # -- Options for HTML output --------------------------------------------------- 40 | 41 | html_theme = 'sphinx_rtd_theme' 42 | 43 | # -- autosummary settings ------------------------------------------------------- 44 | autosummary_generate = True 45 | autosummary_imported_members = False 46 | 47 | # numpydoc options 48 | # napoleon_google_docstring = False 49 | # napoleon_numpy_docstring = True 50 | # napoleon_include_init_with_doc = True 51 | # napoleon_include_private_with_doc = False 52 | # napoleon_include_special_with_doc = False 53 | # napoleon_use_admonition_for_examples = False 54 | # napoleon_use_admonition_for_notes = False 55 | # napoleon_use_admonition_for_references = False 56 | # napoleon_use_ivar = False 57 | # napoleon_use_param = True 58 | # napoleon_use_rtype = True 59 | 60 | # numpydoc_show_class_members = True 61 | # numpydoc_class_members_toctree = True 62 | numpydoc_show_inherited_class_members = False 63 | 64 | # -- Project information ----------------------------------------------------- 65 | 66 | project = 'CrackDect' 67 | copyright = '2022, Matthias Drvoderic' 68 | author = 'Matthias Drvoderic' 69 | version = '0.2' 70 | release = '0.2' -------------------------------------------------------------------------------- /docs/source/crack_detection.rst: -------------------------------------------------------------------------------- 1 | Crack Detection 2 | =============== 3 | 4 | :mod:`~.imagestack` and :mod:`~.stack_operations` are not necessary restrained for the usage of 5 | crack detection. :mod:`~.imagestack` provides the framework to conveniently process images 6 | stacks. The module :mod:`~.crack_detection` provides the algorithms for crack detection. 7 | In *CrackDect* the basic crack detection algorithms are implemented without image preprocessing since this step 8 | depends on the image quality, imaging techinque etc. 9 | Currently, three algorithms are implemented for the detection of **multiple straight cracks in a given direction** 10 | in **grayscale** images. 11 | 12 | .. py:currentmodule:: crack_detection 13 | 14 | 1. :func:`~.detect_cracks` 15 | 2. :func:`~.detect_cracks_glud` 16 | 3. :func:`~.detect_cracks_bender` 17 | 18 | Theory 19 | ------ 20 | 21 | This package features algorithms for the detection of multiple straight cracks in a given direction. 22 | The algorithms were developed for computing the crack density semi-transparent composites where 23 | transilluminated white light imaging (TWLI) can be used as imaging technique. 24 | 25 | .. figure:: images/tiwli.png 26 | :width: 300 27 | 28 | This technique results in a bright image of the specimen with dark crack like this. 29 | 30 | .. figure:: images/input_image_borderless.png 31 | :width: 300 32 | 33 | *CrackDect* works only on images similar to the one above, where cracks appear dark on a bright background. 34 | The cracks must be straight, since the currently implemented algorithms work with masks and filters 35 | to extract cracks in a specific direction. Therefore, it is possible to only detect one kind of cracks 36 | and ignore cracks in other directions. 37 | 38 | Glud´s algorithm 39 | ################ 40 | 41 | The following functions work with the basis of this algorithm: 42 | 43 | 1. :func:`~.detect_cracks` 44 | 2. :func:`~.detect_cracks_glud` 45 | 46 | While :func:`~.detect_cracks` is a pure implementation of the 47 | filter and processing steps, :func:`~.detect_cracks_glud` incorporates cracks detected in the n-1st image to 48 | the nth image. Therefore all images must be related and without shift (see :ref:`shift_correction_label`). 49 | :func:`~.detect_cracks_glud` is basically the full crack detection (without change detection) described by 50 | `Glud et al. `_ whereas 51 | :func:`~.detect_cracks` is only the "crack counting algorithm". :func:`~.detect_cracks` is more versatile and 52 | detects cracks for each image in the given stack without influence of other images. If the position of 53 | a crack changes in the image stack, use :func:`~.detect_cracks`. 54 | 55 | This method from `Glud et al. `_ 56 | is designed to detect off axis tunneling cracks in composite materials. 57 | It works on applying the following filters on the images: 58 | 59 | 1. **Gabor Filter:** The Gabor filter is applied which detects lines in a set direction. Cracks are only detected in 60 | the given direction. This allows to separate crack densities from different layers of the laminate. 61 | 2. **Threshold:** A threshold is applied on the result of the Gabor filter. This separates foreground 62 | and background in an image. The default is 63 | `Yen´s threshold `_. 64 | In the case of the crack detection it separates cracked and intact area. The 65 | Result of the threshold for the image is shown in the next image. 66 | .. figure:: images/pattern.png 67 | :width: 200 68 | 3. **Skeletonizing:** Since off axis tunneling cracks are aligned with the fibers they are straight. The white bands 69 | from the threshold are thinned to a width of one pixel. The algorithm which determines the start and end of 70 | each crack relies on only one pixel wide lines. The result of this skeletonizing for a part af the 71 | threshold image from above is shown in the next image. This 72 | The lines in this image are not continuous. The skeletonizing is done in a rotated coordinate system. This 73 | image is rotated back which creates this effect. 74 | .. figure:: images/skeleton.png 75 | :width: 200 76 | 4. **Crack Counting:** The cracks are counted in the skeletonized image. The skeletonized image is rotated into 77 | a coordinate system where all cracks are vertical (y-direction). Then a loop 78 | scans each pixel in each line of pixels in the image. If a crack is found, it follows it down the ydirection 79 | until the end of the crack. The coordinates of the beginning and end are saved. After 80 | one crack has been detected, it is removed from the image to avoid double detection when the 81 | loop runs over the next line of pixels. The following image shows this process. 82 | .. figure:: images/crack_counting.png 83 | :width: 300 84 | 5. **Crack Density:** The crack density is computed from the detected cracks with 85 | 86 | :math:`\rho_c = \frac{\sum_{i=1}^{n} L_i}{AREA}` 87 | 88 | with :math:`L_i` as the length of the i-th crack and :math:`AREA` as the area of the image. 89 | 90 | 6. **Threshold Density:** The threshold density is the area which is detected as cracked divided by the total image area. 91 | It simply is the ratio of white pixels to the total number of pixels in the threshold image. For series of related 92 | images from the same specimen where the cracks grow and new cracks initiate this measure can be taken as an sanity 93 | check. If the cracks grow too close to each other the white bands in the threshold image merge. Then the 94 | crack density fails to detect two individual cracks since the skeletonizing will result in only one line for two 95 | merged bands. The crack density starts to decrease even tho the threshold density still rises. This is a sign that the 96 | crack detection reached its limit and the cracks in the images are too close to each other. 97 | 98 | The crack density, crack coordinates (start- and endpoints) and the threshold density are the main results of the crack 99 | detection. 100 | 101 | Disadvantages 102 | ************* 103 | 104 | The usage of a computed threshold for the separation between cracks and no cracks is "greedy". This means, 105 | cracks will be detected in the image. In images without cracks, artefacts will appear. This 106 | problem is dampened with the use of 107 | `Yen´s threshold `_. 108 | `Otsu´s threshold `_, as 109 | used in the original paper is even more "greedy" and will always detect cracks even if there are none. 110 | 111 | 112 | .. note:: 113 | The results of this algorithm are sensitive to the input 114 | parameters especially to the parameters which control the gabor filter. Therefore it is a good practice to 115 | try the input parameters on a few images from the preprocessed stack before running the crack detection for the whole 116 | stack. The crack detection is resource intensive and can take a long time if lots of images are processed at 117 | once. 118 | 119 | Bender´s algorithm 120 | ################## 121 | 122 | is implemented as :func:`~.detect_cracks_bender` 123 | 124 | This method introduced by 125 | `Bender JJ `_ 126 | was also developed to detect off-axis cracks in fiber-reinforced polymers. 127 | It can only be used for a series of shift-corrected images and needs a background image as reference 128 | (the first image in the stack). 129 | Therefore, it is not as versatile as :func:`~.detect_cracks` but it has shown good results for image series up to 130 | high crack densities. It is also not "greedy" because a hard threshold is used. 131 | 132 | The following filters and image processing steps are used to extract the cracks: 133 | 134 | 1. **Image History**: Starting form the second image in the stack, as the first is used as the background image, only 135 | darker pixels are taken form the image. This builds on the fact that cracks only result in dark pixels on the image 136 | and therefore, brighter pixels are only random noise. 137 | 2. **Image division** with background image (first image of the stack) to remove constant objects. 138 | 3. The image is divided by a blurred version of itself to remove the background. 139 | 4. A **directional Gaussian filter** is applied to diminish cracks in other than the given directions. 140 | 5. Images are **sharpened** with an `unsharp_mask `_. 141 | 6. A **threshold** is applied to remove falsely identified cracks or artefacts with a weak signal. 142 | 7. **Morphological closing** of the image with a crack-like footprint to smooth the cracks and patch small discontinuities. 143 | 8. **Binarization** of the image. This results in an image with only cracks and background. 144 | 9. **Adding prior cracks**: The n-1st binarized image is added to add cracks already detected in prior images to the 145 | current image since cracks can only grow. 146 | 10. **Crack counting** with the skeletonizing and scanning method similar to the 4th point of `Glud´s algorithm`_. 147 | 148 | .. note:: 149 | This algorithm relies on the fact that cracks only grow. Also, cracks must not move between the images. 150 | Therefore, :ref:`shift_correction_label` is important for this algorithm. If this prerequisites are nor met, 151 | do not use this crack detection method. -------------------------------------------------------------------------------- /docs/source/images/crack_counting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/crack_counting.png -------------------------------------------------------------------------------- /docs/source/images/dist_corr_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/dist_corr_example.gif -------------------------------------------------------------------------------- /docs/source/images/example_images.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/example_images.zip -------------------------------------------------------------------------------- /docs/source/images/input_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/input_image.png -------------------------------------------------------------------------------- /docs/source/images/input_image_borderless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/input_image_borderless.png -------------------------------------------------------------------------------- /docs/source/images/overview_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/overview_gif.gif -------------------------------------------------------------------------------- /docs/source/images/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/pattern.png -------------------------------------------------------------------------------- /docs/source/images/plot_rho.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/plot_rho.png -------------------------------------------------------------------------------- /docs/source/images/real_example_cracks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/real_example_cracks.png -------------------------------------------------------------------------------- /docs/source/images/roi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/roi.png -------------------------------------------------------------------------------- /docs/source/images/shift_corrected.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/shift_corrected.gif -------------------------------------------------------------------------------- /docs/source/images/shift_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/shift_example.gif -------------------------------------------------------------------------------- /docs/source/images/skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/skeleton.png -------------------------------------------------------------------------------- /docs/source/images/specimen_shift_dist_corr.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/specimen_shift_dist_corr.PNG -------------------------------------------------------------------------------- /docs/source/images/tiwli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/docs/source/images/tiwli.png -------------------------------------------------------------------------------- /docs/source/imagestack_intro.rst: -------------------------------------------------------------------------------- 1 | The Image Stack 2 | =============== 3 | 4 | Since this package is build for processing multiple images the efficient handling of image collections is important. The 5 | whole functionality of the package as also available for working with single images but the API of most top level 6 | functions is built for image stacks. 7 | 8 | The image stack is the core of this package. It works as a container for collections of images. It can hold images of 9 | any size and color/grayscale images can be mixed. The only restriction is that all images are from the same data type 10 | e.g *np.float32*, *np.unit8*, etc. The data type of incoming images is checked automatically and if the types do not 11 | match the incoming image is converted. 12 | 13 | .. py:currentmodule:: crackdect.imagestack 14 | 15 | All image stack classes have the same io structure. Images can be added, removed and altered. Single images are 16 | accessed with indices and groups of images via slicing. Currently these image stacks are available: 17 | 18 | * :class:`ImageStack`: The most basic image stack. It just manages the dtype checks and conversion of incoming 19 | images. All images are held in memory (RAM) at all times. When working with just a few images this is the best choice 20 | since it adds nearly zero overhead. 21 | 22 | 23 | * :class:`ImageStackSQL`: When working with a large number of images available RAM can become a problem. 24 | This container manages the used RAM of the stack. When exceeding set limits it automatically saves the current state 25 | of the images to an SQL database. It is built with `sqlalchemy `_ for maximum flexibility. 26 | It can als be used to save the current state of an image stack for later usage or transferring the data to other 27 | locations. Image stacks can be constructed directly from the created databases. The creation of the database is 28 | automated and does not need user input. One database can hold multiple stacks so more than one image stack can 29 | interact with one database at once. `Sqlalchemy `_ handles all transactions 30 | with the database. 31 | 32 | This is a quick introduction on how the image stack works. The full API documentation is :ref:`here `. 33 | 34 | Now a few examples are given on how to use an image stack. Image stacks can be constructed from directly or 35 | via convenience methods to automatically load all images from a list of paths. 36 | 37 | Basic functionality 38 | ------------------- 39 | 40 | This is an example of the basic functionality all image stacks must have to work with the preprocessing funcitons and 41 | the crack detection. 42 | 43 | Directly construct an image stack. The dtype of the images in the stack should be set. The default is *np.float32* since 44 | all functions and the crack detection are optimised for handling float images. 45 | 46 | .. code-block:: python 47 | 48 | import crackdect as cd 49 | stack = cd.ImageStack(dtype=np.float32) 50 | 51 | Adding images to the stack directly. Numpy arrays and `pillow `_ 52 | image objects can be added. PIL images will be converted to numpy arrays. 53 | 54 | .. code-block:: python 55 | 56 | stack.add_image(img) 57 | 58 | To access images from the stack use indices or slices if multiple images should be accessed. The return when slicing 59 | will be a new image stack. 60 | 61 | .. code-block:: python 62 | 63 | stack[0] # => first image = numpy array 64 | stack[1:4] # => image stack of the images with index 1-4(not included). 65 | stack[-1] # => last image of the stack. 66 | 67 | Overriding images in a stack works also like for objects in normal lists. 68 | 69 | .. code-block:: python 70 | 71 | stack[1] = np.random.rand(200,200) * np.linspace(0,1,200) 72 | 73 | This overrides the 2nd image in the stack. If the dtype does not fit the image is converted. 74 | Multiple images can be overridden at once 75 | 76 | .. code-block:: python 77 | 78 | stack[1:5] = ['list of 4 images'] 79 | 80 | But unlike lists 4 images must be given to replace 4 images in the stack. There is no thing as sub-stacks. Removing 81 | images also works like for lists. 82 | 83 | .. code-block:: python 84 | 85 | del stack[4] # removes the 5th image of the stack 86 | del stack[-3:] # removes the last 3 images. 87 | stack.remove_image(4) # the same as del stack[4] but no slicing possible 88 | stack.remove_image() # removes per default the last image 89 | 90 | 91 | Advanced Features 92 | ----------------- 93 | 94 | :class:`ImageStackSQL` has more functionality to it. It can be created like the normal :class:`ImageStack` 95 | but it is it is recommended to set the name of the database and the name of the table the images 96 | will be stored in. With this it is easy to identify saved results. If no 97 | names are set, the object id is taken. The database is created in the current 98 | working directory. 99 | 100 | .. code-block:: python 101 | 102 | stack = cd.ImageStackSQL() # completely default creation 103 | stack = cd.ImageStackSQL(database='test', stack_name='test_stack1') 104 | 105 | Multiple stacks can be connected with one database 106 | 107 | .. code-block:: python 108 | 109 | stack2 = cd.ImageStackSQL(database='test', stack_name='test_stack2') 110 | stack3 = cd.ImageStackSQL(database='test', stack_name='test_stack3') 111 | 112 | Saving and loading is done automatically but only when needed. So it is possible that 113 | the stack was altered but the current state is not saved jet. To save the current state call 114 | 115 | .. code-block:: python 116 | 117 | stack.save_state() 118 | 119 | This will save all changes and free the RAM the images used. When images are accessed after this, they 120 | are loaded form the databased again. 121 | 122 | All stacks can be copied. 123 | 124 | .. code-block:: python 125 | 126 | new_stack = stack.copy() # works for all stacks 127 | 128 | Stacks with sql connection should be named 129 | 130 | .. code-block:: python 131 | 132 | new_sql_stack = sql_stack.copy(stack_name='test_stack4') 133 | 134 | Copying a normal stack will not use more ram until the images in the new stack are overridden. 135 | Copying a stack with sql-connection will create a new table in the database and copy all 136 | images to the new table. For big image stacks, this is a costly operation since all images 137 | will be loaded at some point, copied to the other table and saved there. It the image stack exceeds 138 | its set RAM limits multiple rounds of loading parts of the stack and saving them in 139 | the new table may be required. 140 | 141 | Convenience Creation 142 | -------------------- 143 | 144 | To avoid manually loading all images and putting them into an image stack 145 | there are several options to automatically create an image stack. Images are loaded with 146 | `skimage.io.imread `_ 147 | so a huge flexibility is provided to control the loading process which can be controlled with kwargs. 148 | 149 | .. code-block:: python 150 | 151 | # create from a list of image paths 152 | stack = cd.ImageStack.from_paths(['list of paths']) 153 | # create image stack with database connection. Database and stack_name are optional 154 | stack = cd.ImageStackSQL.from_paths(['list of paths'], 'database', 'stack_name') 155 | # create from previously saved database. 156 | stack = cd.ImageStackSQL.load_from_database('database', 'stack_name') 157 | 158 | The simplest form of creating a basic :class:`ImageStack` is 159 | 160 | .. code-block:: python 161 | 162 | stack = cd.load_images(['list of paths']) 163 | 164 | For more information and more control over the behaviour of the full documentation for :ref:`imagestacks `. 165 | 166 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | CrackDect: Expandable crack detection for composite materials. 2 | ============================================================== 3 | 4 | .. figure:: images/overview_gif.gif 5 | 6 | This package provides crack detection algorithms for tunneling off axis cracks in 7 | glass fiber reinforced materials. 8 | 9 | | Full paper: ``_ 10 | | GitHub Repo: ``_ 11 | 12 | If you use this package in publications, please cite the paper. 13 | 14 | In this package, crack detection algorithms based on the works of Glud et al. [1]_ and Bender et al. [2]_ are implemented. 15 | This implementation is aimed to provide a modular "batteries included" package for 16 | this crack detection algorithms as well as a framework to preprocess image series to suite the 17 | prerequisites of the different crack detection algorithms. 18 | 19 | Quick start 20 | ----------- 21 | 22 | To install CrackDect, check at first the :ref:`Prerequisites` of your python installation. 23 | Upon meeting all the criteria, the package can be installed with pip, or you can clone or download the repo. 24 | If the installed python version or certain necessary packages are not compatible we recommend the use 25 | of virtual environments by virtualenv or Conda. See 26 | `the conda guide `_ 27 | for infos about creating and managing Conda environments. 28 | 29 | **Installation:** 30 | 31 | Open a command line and check if python is available 32 | 33 | .. code-block:: 34 | 35 | $ python --version 36 | 37 | This displays the version of the global python environment. If this does not return 38 | the python version, something is not working and you need to fix your global python 39 | environment. 40 | 41 | If all the prerequisites are met CrackDect can be installed in the global environment via pip 42 | 43 | .. code-block:: 44 | 45 | $ pip install crackdect 46 | 47 | :ref:`Quick Start` shows an illustrative example of the crack detection. 48 | 49 | :ref:`Crack Detection` provides a quick theoretical introduction into the crack detection algorithm. 50 | 51 | 52 | Prerequisites 53 | ------------- 54 | 55 | It is recommended to use virtual environments (`anaconda `_). 56 | This package is written and tested in Python 3.8 and relies on here listed packages. 57 | 58 | | `scikit-image `_ 0.18.1 59 | | `numpy `_ 1.18.5 60 | | `scipy `_ 1.6.0 61 | | `matplotlib `_ 3.3.4 62 | | `sqlalchemy `_ 1.3.23 63 | | `numba `_ 0.52.0 64 | | `psutil `_ 5.8.0 65 | 66 | And if the visualization module is used `PyQt5 `_ is also needed. 67 | 68 | Motivation 69 | ---------- 70 | Most algorithms and methods for scientific research are implemented as in-house code and not accessible for other 71 | researchers. Code rarely gets published and implementation details are often not included in papers presenting the 72 | results of these algorithms. Our motivation is to provide transparent and modular code with high level functions 73 | for crack detection in composite materials and the framework to efficiently apply it to experimental evaluations. 74 | 75 | .. toctree:: 76 | :caption: Examples 77 | :hidden: 78 | 79 | quickstart 80 | imagestack_intro 81 | preprocessing 82 | shift_correction 83 | crack_detection 84 | 85 | 86 | Contributing 87 | ------------ 88 | 89 | Clone the repository and add changes to it. Test the changes and make a pull request. 90 | 91 | Modules 92 | ------- 93 | 94 | .. currentmodule:: crackdect 95 | 96 | .. autosummary:: 97 | :toctree: generated 98 | :caption: User Documentation 99 | :template: module_template.rst 100 | 101 | imagestack 102 | image_functions 103 | stack_operations 104 | io 105 | crack_detection 106 | visualization 107 | 108 | 109 | Authors 110 | ------- 111 | - Matthias Drvoderic 112 | 113 | License 114 | ------- 115 | 116 | This project is licensed under the MIT License 117 | 118 | Indices and tables 119 | ------------------ 120 | 121 | * :ref:`genindex` 122 | 123 | 124 | .. [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 `_ 125 | 126 | .. [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 `_ 127 | -------------------------------------------------------------------------------- /docs/source/preprocessing.rst: -------------------------------------------------------------------------------- 1 | Preprocessing 2 | ============= 3 | 4 | .. currentmodule:: crackdect 5 | 6 | The preprocessing for the images is a modular process. Since each user might capture the images in a slightly different 7 | way it´s impossible to just set up one preprocessing routine and expect it to work for all circumstances. Therefore, 8 | the preprocessing is modular. The preprocessing routines included in this package are defined in 9 | :mod:`~.stack_operations`. But these are just some predefined functions for the most important preprocessing steps. 10 | Here an example of how to use custom image processing functions with the image stack (:mod:`~.imagestack`) is shown. 11 | 12 | Apply functions 13 | --------------- 14 | 15 | An arbitrary function that takes one image and other arguments can be applied to the whole image stack. The function 16 | must return an image and nothing else. Applying such an function to the whole image stack will alter all the images 17 | in the stack since the images from the stack are taken as input and are replaced with the output of the function. 18 | E.g histogram equalisation for the whole image stack can be done in one line of code. 19 | 20 | .. code-block:: 21 | 22 | import crackdect as cd 23 | from skimage import exposure 24 | 25 | stack.execute_function(exposure.equalize_adapthist, clip_limit=0.03) 26 | 27 | This performs *equalize_adapthist* on all images in the stack with a clip limit of 0.03. *clip_limit* is a 28 | keyword argument of *equalize_adapthist*. 29 | 30 | With this functionality custom functions can be defined easily without worrying about the image stack. A cascade 31 | of different preprocessing functions can be performed on one image stack. This enables a really modular approach 32 | and the most flexibility. 33 | 34 | .. code-block:: 35 | 36 | def contrast_stretching(img): 37 | p5, p95 = np.percentile(img, (5, 95)) 38 | return exposure.rescale_intensity(img, in_range=(p5, p95)) 39 | 40 | stack.execute_function(custom_stretching) 41 | 42 | Rolling Operations 43 | ------------------ 44 | Another way to preprocess the images in stacks is to perform an rolling operation on them. A function for rolling 45 | operations takes two images as input and returns just one image. Change detection with image differencing is an example. 46 | 47 | :math:`I_d = I_2 - I_1` 48 | 49 | Applying this function to the image stack would look like this: 50 | 51 | .. code-block:: 52 | 53 | def simple_differencing(img1, img2): 54 | return img2-img1 55 | 56 | stack.execute_rolling_function(simple_differencing, keep_first=False) 57 | 58 | This will evaluate *simple_differencing* for all images starting from the second image in the stack. The n-th image 59 | in the stack is computed with this schema. 60 | 61 | :math:`I_{new}^n = f(I^{n-1}, I^n)` 62 | 63 | Since this schema can only start at the second image, the argument *keep_first* defines is the first image is deleted 64 | after the rolling operation or not. The first image will not be changed since the function is not applied on 65 | it. 66 | 67 | Predefined Preprocessing Functions 68 | ---------------------------------- 69 | 70 | .. currentmodule:: crackdect 71 | 72 | The most important preprocessing for the crack detection is the change detection and 73 | shift correction. This package comes with functions for these routines. There are variations for both routines and 74 | other useful functions like cutting to the region of interest in :mod:`~.stack_operations`. 75 | 76 | All the functions in :mod:`~.stack_operations` take an image stack and return the stack with the results of the 77 | routine. The images in the stack get changed. If the state of the image stack prior to applying a routine should 78 | be kept, copy the stack before. 79 | 80 | For more information see the documentation from :mod:`~.stack_operations`. 81 | 82 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | .. py:currentmodule:: imagestack 5 | 6 | Lets start with initialising an :class:`~.ImageStack`. Its a container object for a stack of images. The crack detection 7 | works with this container objects to process the whole image stack at once. 8 | It is also possible to work just with a list of images if no extra functionality from :class:`~.ImageStack` is needed. 9 | 10 | Create an stack and add images to it. The images in this example can be downloaded 11 | :download:`here `. Unpack the folder and set the working directory in the parent folder of 12 | *example_images*. 13 | 14 | .. code-block:: python 15 | 16 | import numpy as np 17 | import crackdect as cd 18 | # read image paths from folder 19 | paths = cd.image_paths('example_images') 20 | 21 | # We want the dtype of the images to be np.float32 and only grayscale images. 22 | stack = cd.ImageStack.from_paths(paths, dtype=np.float32, as_gray=True) 23 | 24 | The following image shows the last image from the stack. A lot of small and some bigger cracks are visible. 25 | Also, the region of interest is only the middle part of the specimen without the edges of the specimen and the painted 26 | black bar. 27 | 28 | .. figure:: images/input_image.png 29 | :width: 400 30 | 31 | Before cutting to the desired shape a shift correction is necessary to align all images in a global 32 | coordinate system. The following image shows the last image of the stack after the shift correction and the 33 | cut to the region of interest. 34 | 35 | .. code-block:: python 36 | 37 | # shift correction to align all images in one coordinate system 38 | cd.shift_correction(stack) 39 | 40 | # The region of interest is form pixel 200-1400 in x and 20-900 in y 41 | cd.region_of_interest(stack, 200, 1400, 20, 900) 42 | 43 | 44 | .. figure:: images/roi.png 45 | :width: 400 46 | 47 | .. py:currentmodule:: crack_detection 48 | Currently, three functions using different algorithms for crack detection are available in the package. 49 | For this tutorial, :func:`~.detect_cracks`, the simplest crack detection function with the least 50 | prerequisites in image preprocessing is used. 51 | For more information go to :ref:`Crack Detection`. 52 | 53 | Only cracks in a set direction are detected with :func:`~.detect_cracks`. For the algorithm to 54 | work properly, the following arguments must be set. 55 | 56 | 1. **theta:** The angle between the cracks and a vertical line. 57 | 2. **crack_width:** Approximate width of the major detected cracks in pixels. This value is taken as the wavelength 58 | of the Gabor kernel. 59 | 3. **ar:** The aspect ratio of the kernel. Since cracks are usually long and thin an aspect ratio bigger than 1 60 | should be chosen. A good compromise between speed and accuracy is 2. Too big aspect ratios can lead to false detection. 61 | 4. **min_size:** The minimum length of detected cracks in pixels. Since small artifacts or noise can lead 62 | to false detection, this parameter provides an reliable filter. 63 | 64 | .. code-block:: python 65 | 66 | # crack detection 67 | rho, cracks, thd = cd.detect_cracks(stack, theta=60, crack_width=10, ar=2, bandwidth=1, min_size=10) 68 | 69 | The results can be plotted and inspected. 70 | 71 | .. code-block:: python 72 | 73 | # plot the crack density 74 | import matplotlib.pyplot as plt 75 | plt.plot(np.arange(len(stack)), rho) 76 | 77 | 78 | .. figure:: images/plot_rho.png 79 | :width: 400 80 | 81 | The crack density is growing with each image. To look if all cracks are detected lets look at the last image in the 82 | stack. 83 | 84 | .. code-block:: python 85 | 86 | # plot the background image and the associated cracks 87 | cd.plot_cracks(stack[-1], cracks[-1]) 88 | 89 | 90 | .. figure:: images/real_example_cracks.png 91 | :width: 400 92 | 93 | 94 | Nearly all cracks get detected. Some cracks are too close to each other and the crack detection can not distinguish 95 | them. Cracks in other directions are not detected. This image has low contrast so it is hard to detect all 96 | the cracks since some are quite faint compared to the background. There is also quite a lot of blur at some cracks. 97 | This are the main problems with the crack detection. This image would benefit form an histogram equalization to 98 | boost the contrast. 99 | 100 | The full script: 101 | 102 | .. code-block:: python 103 | 104 | import numpy as np 105 | import crackdect as cd 106 | # read image paths from folder 107 | paths = cd.image_paths('example_images') 108 | 109 | # We want the dtype of the images to be np.float32 and only grayscale images. 110 | stack = cd.ImageStack.from_paths(paths, dtype=np.float32, as_gray=True) 111 | # shift correction to align all images in one coordinate system 112 | cd.shift_correction(stack) 113 | # The region of interest is form pixel 200-1400 in x and 20-900 in y 114 | cd.region_of_interest(stack, 200, 1400, 20, 900) 115 | # crack detection 116 | rho, cracks, thd = cd.detect_cracks(stack, theta=60, crack_width=10, ar=2, bandwidth=1, min_size=10) 117 | # plot the background image and the associated cracks 118 | cd.plot_cracks(stack[-1], cracks[-1]) -------------------------------------------------------------------------------- /docs/source/shift_correction.rst: -------------------------------------------------------------------------------- 1 | .. _shift_correction_label: 2 | 3 | Shift Correction 4 | ================ 5 | 6 | In image series taken during tensile mechanical tests, it often appears that the specimen is moving 7 | relative to the background like the specimen in this gif. 8 | 9 | .. figure:: images/shift_example.gif 10 | :width: 400 11 | 12 | Since some crack detection algorithms work only with image series with no shift of the specimen all 13 | must be aligned in a global coordinate system for them. Otherwise, these algorithms will compute wrong results. 14 | Currently, the following functions need an aligned image stack: 15 | 16 | .. py:currentmodule:: crack_detection 17 | 18 | 1. :func:`~.detect_cracks_glud` 19 | 2. :func:`~.detect_cracks_bender` 20 | 21 | .. py:currentmodule:: imagestack 22 | 23 | .. warning:: 24 | All shift correction algorithms **only** take images with the **same** dimensionality as input. If 25 | the dimensionality of just one image differs it will result in an error. When using an :class:`~.ImageStack` 26 | make sure to ether only add images with the same dimensionality or when using :meth:`~.ImageStack.from_paths` 27 | to construct the stack, add kwargs to ensure all images are loaded the same way ( 28 | `parameters for reading `_ 29 | )! 30 | 31 | Global Shift Correction 32 | ----------------------- 33 | 34 | There are two methods to correct the global shift of an image and one to correct the shift as well as 35 | distortion. The later can be used to correct images where the specimen show significant strain. 36 | 37 | .. py:currentmodule:: stack_operations 38 | 39 | #. :func:`~.biggest_common_sector` 40 | #. :func:`~.shift_correction` 41 | 42 | Both correct the shift of the image with global 43 | `phase-cross-correlation `_ 44 | and cut the image to the biggest common sector that no black borders appear. 45 | :func:`~.biggest_common_sector` is more efficient but less accurate. 46 | 47 | The following gif shows the corrected image series from above. 48 | 49 | .. figure:: images/shift_corrected.gif 50 | :width: 400 51 | 52 | With this corrected image series, crack detection methods that incorporate the history of the image are 53 | applicable. 54 | 55 | Shift-Distortion Correction 56 | --------------------------- 57 | 58 | If the distortion due to strain of the specimen is significant, :func:`~.shift_distortion_correction` 59 | can be used. This function tracks for subareas of the images to compute global shift, rotation and 60 | relative movement between this subareas. The only prerequisite is that for distinct features are visible 61 | throughout the whole image stack. 62 | 63 | .. figure:: images/dist_corr_example.gif 64 | :width: 800 65 | 66 | A best practice example would be to mark specimen at four positions 67 | to enable reliable shift-distortion correction. 68 | 69 | .. figure:: images/specimen_shift_dist_corr.PNG 70 | :height: 250 71 | 72 | The red crosses are used to correct the shift and distortion of the image while the 73 | blue dotted rectangle would mark the usable region of interest for the crack detection. 74 | 75 | .. note:: 76 | If shift and distortion are high it can be necessary to apply the correction twice in a row. 77 | 78 | Always check if the shift correction worked properly since its reliability depends on the 79 | quality of the images! -------------------------------------------------------------------------------- /example_images/01.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/example_images/01.bmp -------------------------------------------------------------------------------- /example_images/02.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/example_images/02.bmp -------------------------------------------------------------------------------- /example_images/03.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/example_images/03.bmp -------------------------------------------------------------------------------- /example_images/04.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/example_images/04.bmp -------------------------------------------------------------------------------- /example_images/05.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/example_images/05.bmp -------------------------------------------------------------------------------- /example_images/06.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattdrvo/CrackDect/03845d08481e633f291d5616bd1d01b8d8a4c6c0/example_images/06.bmp -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.md', 'r') as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name='crackdect', # Required 8 | version='0.2', # Required 9 | description='crack detection for composite materials', # Optional 10 | long_description=long_description, # Optional 11 | long_description_content_type='text/markdown', # Optional 12 | url='https://github.com/mattdrvo/CrackDect', # Optional 13 | author='Matthias Drvoderic', # Optional 14 | author_email='matthias.drvoderic@unileoben.ac.at', # Optional 15 | classifiers=[ # Optional 16 | 'Development Status :: 4 - Beta', 17 | 'Intended Audience :: Science/Research', 18 | 'Topic :: Scientific/Engineering :: Image Processing', 19 | 'Programming Language :: Python :: 3.8', 20 | 'Operating System :: OS Independent', 21 | 'License :: OSI Approved :: MIT License' 22 | ], 23 | keywords='crackdetection composites imageprocessing imagestack', # Optional 24 | # package_dir={'': 'crackdect'}, # Optional 25 | # packages=find_packages(where=''), # Required 26 | packages=['crackdect'], 27 | python_requires='>=3.8', 28 | install_requires=['numpy', 29 | 'scipy', 30 | 'scikit-image', 31 | 'sqlalchemy', 32 | 'numba', 33 | 'matplotlib', 34 | 'psutil', 35 | ], 36 | package_data={}, 37 | ) 38 | --------------------------------------------------------------------------------