Calculating Crack Dimensions with Streamlit and LandingLens

Published on August 25, 2024

Crack Dimensions

In this blog post, I will walk you through the process of using a pre-trained segmentation model from LandingAI to analyze crack images. The goal is to generate a segmentation mask and then calculate both the length and the largest perpendicular width of the detected crack. This technique is particularly useful in structural analysis, where accurate measurement of crack dimensions is crucial for assessing damage.

Overview of the Workflow

The process involves three key steps:

  1. Using LandingAI's LandingLens to create a segmentation model that can identify cracks in images.
  2. Applying the model to a crack image to obtain a segmentation mask.
  3. Using mathematical algorithms to calculate the crack's length and the largest perpendicular width from the segmentation mask.

Step 1: Building the Segmentation Model

LandingAI provides a platform called LandingLens, which allows you to train machine learning models on custom datasets. For this project, I trained a segmentation model on labeled crack images. The model learns to identify and segment cracks from the rest of the image. After training, the model is deployed to predict segmentation masks for new images.

Step 2: Getting the Segmentation Mask

Once the model is trained, we use it to predict the segmentation mask of a given crack image. The mask is a binary image where pixels belonging to the crack are marked in one color (e.g., white), and all other pixels are marked in another color (e.g., black). Below is the Python code that uses the LandingAI predictor to get the segmentation mask:


    predictor = Predictor("your-model-id", api_key="your-api-key")

    uploaded_file = st.file_uploader("Upload crack image")
    if uploaded_file is not None:
        image = Image.open(uploaded_file).convert("RGB")
        seg_pred = predictor.predict(image)
        color_dict = {"crack": "red"}
        image_with_preds = overlay_predictions(seg_pred, np.asarray(image), 
                                               {"color_map": color_dict})
        st.image(image_with_preds, caption="Segmentation Predictions")

        flattened_bitmap = decode_bitmap_rle(seg_pred[0].encoded_mask,
                                             seg_pred[0].encoding_map)
        seg_mask_channel = np.array(flattened_bitmap, dtype=np.uint8)
                             .reshape(image.size[::-1])
        seg_mask_channel *= 255
    

This code snippet uploads an image, uses the predictor to generate a segmentation mask, and then displays the mask on the original image.

Step 3: Calculating Crack Length

With the segmentation mask in hand, we can now calculate the crack's length. The crack's length is determined by tracing the longest continuous path within the crack segment using the following method:


    def extend_line_to_binary(start_point, end_point, binary_image, org_image):
        # Calculate slope and intercept of the line
        if end_point[0] - start_point[0] != 0:
            slope = (end_point[1] - start_point[1]) / (end_point[0] - start_point[0])
        else:
            slope = float("inf")

        if slope == 0:
            slope = 0.001

        # Extend the line until it reaches the first point in the binary image
        rows, cols = binary_image.shape[:2]

        # Extend line in the negative direction
        extended_start_point = start_point
        for col in range(int(start_point[1]), -1, -1):
            row = int(start_point[0] + (col - start_point[1]) / slope)
            if row >= rows - 2:
                break
            if row >= 0 and row < rows and binary_image[col, row] != 0:
                extended_start_point = (row, col)
                break

        # Extend line in the positive direction
        extended_end_point = end_point
        for col in range(int(start_point[1]), cols - 1):
            row = int(start_point[0] + (col - start_point[1]) / slope)
            if row >= 0 and row < rows and binary_image[col, row] != 0:
                extended_end_point = (row, col)
                break

        distance = math.dist(extended_start_point, extended_end_point)
        return distance
    

This code calculates the length of the crack in pixels by extending a line along the crack's centerline and measuring the distance between the endpoints.

Step 4: Calculating Crack Width

Next, we calculate the crack's largest perpendicular width. This involves finding the medial axis of the crack, which represents the centerline, and then determining the maximum distance from the medial axis to the crack's boundary. Here's how this is done:


    def width(contours, seg_mask_channel, final_img, new_arr):
        delta = 3
        new_im = seg_mask_channel

        test_arr = np.array(np.zeros_like(seg_mask_channel), dtype=np.uint8)
        cv2.drawContours(test_arr, [max(contours, key=cv2.contourArea)], -1, 
                         255, thickness=-1)
        
        medial, distance = medial_axis(test_arr, return_distance=True)

        # Get the maximum width from the medial axis
        max_idx = np.argmax(distance)
        max_pos = np.unravel_index(max_idx, distance.shape)
        coords = np.array([max_pos[1] - 1, max_pos[0]])

        # Intersect orthogonal with crack and get contour
        orth = np.array([-vector[1], vector[0]])
        orth_img = np.zeros(final_img.shape, dtype=np.uint8)
        cv2.line(orth_img, tuple(coords + orth), tuple(coords - orth),
                 color=255, thickness=1)
        gap_img = cv2.bitwise_and(orth_img, new_im)

        gap_contours, _ = cv2.findContours(np.asarray(gap_img), cv2.RETR_EXTERNAL, 
                                           cv2.CHAIN_APPROX_SIMPLE)
        width_pixels = max(distance)
        return width_pixels
    

This code calculates the largest width by finding the maximum distance from the medial axis (centerline) of the crack to its boundary. The orthogonal line drawn at this point intersects the crack, and the width is determined by the distance between these intersection points.

Final Step: Converting Pixels to Physical Measurements

The final output of this analysis is in pixels. To convert these pixel measurements to real-world units (e.g., inches), we use a calibration step where we define the conversion factor from pixels to inches based on a known reference in the image.

Once the length and width in pixels are calculated, they are divided by this conversion factor to get the crack's dimensions in inches:


    st.write(
        "Predicted crack length in inches: "
        + str(length_pixels / 2 / st.session_state.inch_to_pixels)
    )
    st.write(
        "Predicted crack width in inches: "
        + str(width_pixels / 2 / st.session_state.inch_to_pixels)
    )
    

Conclusion

Using LandingAI to segment crack images and applying mathematical algorithms to measure crack dimensions can significantly improve the accuracy and efficiency of structural assessments. This approach automates the detection and measurement process, reducing the potential for human error and providing precise measurements that are critical for maintenance and safety evaluations.

Back to Blog
← Previous Post Next Post →