Source code for phasefieldx.PostProcessing.measure_crack_length_2d

"""
Measure crack length in 2D phase-field simulations
==================================================

This script provides a post-processing workflow for measuring crack length in 2D phase-field fracture
simulations, following the procedure presented in :footcite:t:`Castillon2025_arxiv`. It is designed to
work with simulation outputs in VTU format (e.g., from dolfinx or FEniCSx), where the phase-field
variable ('phi') indicates the presence of a crack.

Workflow overview
-----------------
1. VTU to PNG conversion: For each .vtu file in the simulation output folder, the mesh is visualized,
    and the crack region (where phi > 0.95) is highlighted in red. The resulting image is saved as a PNG
    for further processing.

2. Crack skeletonization and length measurement: Each PNG image is processed to extract the red crack
    region, skeletonize it (reducing it to a 1-pixel-wide centerline), and count the number of pixels
    in the skeleton. The pixel count is converted to a physical length using the known specimen size and
    image resolution.

3. Saving results: The measured crack lengths for all time steps are saved to a text file
    (`crack_lengths.txt`) with a column header 'a' for easy loading into pandas or other analysis tools.

4. Visualization: Optionally, a GIF is generated showing the crack evolution, with the measured crack
    length overlaid on each frame.

Requirements
------------
- pyvista
- matplotlib
- scikit-image
- numpy
- imageio

Usage example
-------------
Set your simulation folder and specimen size, then call:

     folder_simulation = "path/to/your/simulation/folder"
     measure_crack(folder_simulation, physical_horizontal=1.0, physical_vertical=1.0)

This will generate PNG images, measure crack lengths, and create a GIF in the specified folder.

Functions
---------
- get_black_bbox(image): Finds the bounding box of the specimen in an image.
- generate_crack_images(input_folder, output_folder): Converts VTU files to PNG images with cracks highlighted.
- measure_crack_lengths(output_folder, dx, dy): Measures crack lengths from PNG images and saves results.
- generate_crack_gif(output_folder, crack_lengths, gif_name): Creates a GIF showing crack evolution.
- measure_crack(folder_simulation, physical_horizontal, physical_vertical): Main workflow function.
"""

import os
import numpy as np
try:
    import matplotlib.pyplot as plt
    import matplotlib.image as mpimg
except Exception:
    plt = None
    mpimg = None

# Optional imports: only required if user calls functions that need them.
try:
    import pyvista as pv
except Exception:
    pv = None

try:
    import imageio.v2 as imageio
except Exception:
    imageio = None

try:
    from skimage.morphology import skeletonize
    from skimage.color import rgb2hsv
except Exception:
    skeletonize = None
    rgb2hsv = None


[docs] def get_black_bbox(image): """ Find the bounding box of the black (background/specimen) region in an image. Parameters ---------- image : np.ndarray Input image (grayscale or RGB). Returns ------- min_row, max_row, min_col, max_col : int Indices defining the bounding box of the black region. """ # Convert to grayscale if needed if image.ndim == 3: gray = np.mean(image[..., :3], axis=2) else: gray = image # Threshold for black black_mask = gray < 0.2 rows = np.any(black_mask, axis=1) cols = np.any(black_mask, axis=0) min_row, max_row = np.where(rows)[0][[0, -1]] min_col, max_col = np.where(cols)[0][[0, -1]] return min_row, max_row, min_col, max_col
[docs] def generate_crack_images(input_folder, output_folder): """ Generate PNG images from .vtu files, highlighting the crack region in red. For each .vtu file in the input folder, this function: - Loads the mesh and phase-field data. - Thresholds the 'phi' field to extract the crack region. - Plots the mesh in black and the crack in red. - Saves the result as a PNG image in the output folder. Parameters ---------- input_folder : str Path to the folder containing .vtu files. output_folder : str Path to the folder where PNG images will be saved. """ os.makedirs(output_folder, exist_ok=True) pv.global_theme.allow_empty_mesh = True # List all .vtu files (skip the first if needed) pvtu_files = sorted([f for f in os.listdir(input_folder) if f.endswith('.vtu')])[1:] for fname in pvtu_files: file_path = os.path.join(input_folder, fname) file_vtu = pv.read(file_path) crack = file_vtu.threshold(value=0.95, scalars='phi', invert=False) plotter = pv.Plotter(off_screen=True) plotter.add_mesh(file_vtu, color='black', opacity=1.0) plotter.add_mesh(crack, color='red', opacity=1.0) plotter.camera_position = 'xy' img_path = os.path.join(output_folder, fname.replace('.vtu', '.png')) plotter.show(screenshot=img_path, window_size=(1024, 1024)) plotter.close() print(f"Saved images to {output_folder}")
[docs] def measure_crack_lengths(output_folder, dx, dy): """ Measure crack lengths from PNG images and save results to a text file. For each PNG image in the output folder: - Converts the image to HSV and thresholds for red pixels (crack). - Skeletonizes the crack region to obtain a 1-pixel wide centerline. - Counts the number of skeleton pixels and converts to physical length. Parameters ---------- output_folder : str Path to the folder containing PNG images. dx : float Physical size of a pixel in the horizontal direction. dy : float Physical size of a pixel in the vertical direction (not used, but available). Returns ------- crack_lengths : np.ndarray Array of measured crack lengths for each image. """ # Ensure optional dependencies are available if skeletonize is None or rgb2hsv is None: raise ImportError( "measure_crack_lengths requires scikit-image. " "Install it with `pip install scikit-image` or avoid calling this function." ) image_files = sorted([f for f in os.listdir(output_folder) if f.endswith('.png')]) crack_lengths = [] for fname in image_files: img_path = os.path.join(output_folder, fname) img = mpimg.imread(img_path) pixel_length = dx # Use dx for pixel-to-physical conversion # Convert to HSV and threshold for red pixels (crack) hsv = rgb2hsv(img[..., :3]) red_mask = ((hsv[..., 0] < 0.05) | (hsv[..., 0] > 0.95)) & (hsv[..., 1] > 0.5) & (hsv[..., 2] > 0.2) # Skeletonize to get the crack centerline skeleton = skeletonize(red_mask) crack_lengths.append(skeleton.sum()) # Convert pixel count to physical length crack_lengths = np.array(crack_lengths) * pixel_length # Save crack lengths to a text file with column name 'a' np.savetxt(os.path.join(output_folder, "crack_lengths.txt"), crack_lengths, header="a", comments='') print("Crack lengths:", crack_lengths) return crack_lengths
[docs] def generate_crack_gif(output_folder, crack_lengths, gif_name="crack_evolution.gif"): """ Generate a GIF showing crack evolution, overlaying the measured crack length on each frame. Parameters ---------- output_folder : str Path to the folder containing PNG images. crack_lengths : array-like Crack lengths to overlay on each frame. gif_name : str, optional Name of the output GIF file (default: "crack_evolution.gif"). """ if imageio is None: raise ImportError( "generate_crack_gif requires imageio. " "Install it with `pip install imageio` or avoid calling this function." ) image_files = sorted([f for f in os.listdir(output_folder) if f.endswith('.png')]) images = [] for fname, crack_length in zip(image_files, crack_lengths): img_path = os.path.join(output_folder, fname) img = imageio.imread(img_path) # Overlay crack length using matplotlib fig, ax = plt.subplots(figsize=(8, 8)) ax.imshow(img) ax.axis('off') ax.text(0.05, 0.95, f"Crack length: {crack_length:.3f}", color='yellow', fontsize=18, fontweight='bold', ha='left', va='top', transform=ax.transAxes, bbox=dict(facecolor='black', alpha=0.5, boxstyle='round,pad=0.5')) fig.canvas.draw() # Get RGBA buffer and convert to numpy array frame = np.asarray(fig.canvas.buffer_rgba()) # Convert RGBA to RGB if needed if frame.shape[2] == 4: frame = frame[..., :3] images.append(frame) plt.close(fig) gif_path = os.path.join(output_folder, gif_name) imageio.mimsave(gif_path, images, duration=0.7) print(f"GIF saved to {gif_path}")
[docs] def measure_crack(folder_simulation, physical_horizontal, physical_vertical): """ Main workflow to measure crack lengths and generate images and GIF. This function: - Generates PNG images from VTU files, highlighting cracks. - Detects the specimen bounding box and computes pixel size. - Measures crack lengths from images and saves results. - Generates a GIF showing crack evolution. Parameters ---------- folder_simulation : str Path to the simulation folder containing 'paraview-solutions_vtu'. physical_horizontal : float Physical width of the specimen (used to compute pixel size). physical_vertical : float Physical height of the specimen (used to compute pixel size). """ input_folder = os.path.join(folder_simulation, "paraview-solutions_vtu") output_folder = os.path.join(folder_simulation, "paraview_images") generate_crack_images(input_folder, output_folder) # Use the first image to determine pixel size image_files = sorted([f for f in os.listdir(output_folder) if f.endswith('.png')]) img_path = os.path.join(output_folder, image_files[0]) img = mpimg.imread(img_path) min_row, max_row, min_col, max_col = get_black_bbox(img) horizontal_pixels = max_col - min_col + 1 vertical_pixels = max_row - min_row + 1 # Compute pixel size in physical units dx = physical_horizontal / horizontal_pixels dy = physical_vertical / vertical_pixels print(f"Detected specimen bounding box: rows {min_row}-{max_row}, cols {min_col}-{max_col}") print(f"Horizontal pixels: {horizontal_pixels}, Vertical pixels: {vertical_pixels}") print(f"Pixel size: dx={dx:.6f}, dy={dy:.6f}") crack_lengths = measure_crack_lengths(output_folder, dx, dy) generate_crack_gif(output_folder, crack_lengths)