14.4 Artifical Neural Network to Detect Cracks

14.4 Artifical Neural Network to Detect Cracks#

In this example we will apply our single-layer ANN model to detect cracks in images. Here we are flattening the images so will lose spatial information, might consider a convolution kernel to preserve spatial relationships.

The data are sourced from:

Adrien Müller, Nikos Karathanasopoulos, Christian C. Roth, Dirk Mohr, Machine Learning Classifiers for Surface Crack Detection in Fracture Experiments, International Journal of Mechanical Sciences, Volume 209, (2021), 106698, ISSN 0020-7403

Workflow:

  1. Destroy existing data (need explained below)

  2. Obtain the original dataset (.zip)

  3. Use python libraries to extract the images; they are stored in MatLab.mat file structures.

  4. Store imgaes locally into subdirectories for processing

  5. Then apply our ANN (homebrew or PyTorch)

%reset -f

Note

In this example, we delete the processed data primarily due to GitHub file size limitations and to demonstrate a clean, reproducible workflow.

In real-world research or applications, you typically do not delete large datasets once downloaded and processed. Instead, you would:

  • Archive them to a reliable location

  • Use versioning and metadata to track provenance

  • Avoid redundant downloads to conserve bandwidth and storage

This cleanup is used here for pedagogical clarity and control, especially in constrained teaching environments.

Data Cleanup Block#

Warning

The code block below will permanently delete all downloaded and generated data, including:

  • Extracted .mat files and working directories

  • Converted .png image files

  • CSV files containing image labels

Only run this cell if:

You want to start fresh

You have already backed up your work (if needed)

You're confident all required artifacts can be re-downloaded and regenerated

This is a reasonable practice to demonstrate reproducibility and manage disk space — but treat it with care!

import shutil
import os

# Define directories to delete
dirs_to_remove = [
    "extracted_dataset",  # Contains mmc1 and .mat files
    "images",             # Contains all converted PNGs
    "labels"              # CSV files mapping filenames to labels
]

for target_dir in dirs_to_remove:
    if os.path.exists(target_dir):
        print(f"Deleting: {target_dir}")
        shutil.rmtree(target_dir)
    else:
        print(f"Directory not found (already deleted?): {target_dir}")

print("Cleanup complete.")
Directory not found (already deleted?): extracted_dataset
Directory not found (already deleted?): images
Directory not found (already deleted?): labels
Cleanup complete.
import os
import zipfile
import requests
from scipy.io import loadmat
import numpy as np
import matplotlib.pyplot as plt

url = "https://ars.els-cdn.com/content/image/1-s2.0-S002074032100429X-mmc1.zip"

# Step 1: Download the ZIP archive
def download_file(url, output_path):
    if not os.path.exists(output_path):
        print(f"Downloading {url}...")
        response = requests.get(url)
        response.raise_for_status()
        with open(output_path, "wb") as f:
            f.write(response.content)
        print(f"Saved to {output_path}")
    else:
        print(f"File already exists: {output_path}")
# Workflow paths
url = "https://ars.els-cdn.com/content/image/1-s2.0-S002074032100429X-mmc1.zip"
zip_file = "dataset.zip"
extract_dir = "extracted_dataset"
png_output_dir = "images"
# Execute workflow
download_file(url, zip_file)
Downloading https://ars.els-cdn.com/content/image/1-s2.0-S002074032100429X-mmc1.zip...
Saved to dataset.zip
# Step 2: Extract ZIP archive
def extract_zip(zip_path, extract_to):
    print(f"Extracting {zip_path}...")
    with zipfile.ZipFile(zip_path, 'r') as zip_ref:
        zip_ref.extractall(extract_to)
    print(f"Extracted to {extract_to}")
extract_zip(zip_file, extract_dir)
Extracting dataset.zip...
Extracted to extracted_dataset
def convert_mat_to_pngs(mat_dir, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    for filename in os.listdir(mat_dir):
        if filename.endswith(".mat"):
            full_path = os.path.join(mat_dir, filename)
            print(f"Processing {filename}...")
            mat_data = loadmat(full_path)

            # Try to guess the structure
            for key in mat_data:
                if not key.startswith("__"):
                    images = mat_data[key]

                    # Check dimensions
                    if images.ndim == 3:
                        for i in range(images.shape[2]):
                            img = images[:, :, i]
                            img = np.squeeze(img)
                            output_filename = f"{os.path.splitext(filename)[0]}_{i:03d}.png"
                            plt.imsave(os.path.join(output_dir, output_filename), img, cmap='gray')
                    elif images.ndim == 4:
                        for i in range(images.shape[3]):
                            img = images[:, :, :, i]
                            output_filename = f"{os.path.splitext(filename)[0]}_{i:03d}.png"
                            plt.imsave(os.path.join(output_dir, output_filename), img)
                    else:
                        print(f"Skipping unexpected shape: {images.shape}")
                    break  # process only first candidate
    print("Done.")
convert_mat_to_pngs(os.path.join(extract_dir, "mmc1"), png_output_dir)
Processing NT_NN.mat...
Skipping unexpected shape: (1, 1)
Processing UT_NN.mat...
Skipping unexpected shape: (1, 1)
Processing NT_TestSet_Features.mat...
Skipping unexpected shape: (8, 1407)
Processing ASB_NN.mat...
Skipping unexpected shape: (1, 1)
Processing ASB_TestSet_Features.mat...
Skipping unexpected shape: (21, 3741)
Processing ASB_TestSet_Imgs.mat...
Processing NT_TestSet_Imgs.mat...
Processing UT_TestSet_Imgs.mat...
Processing UT_TestSet_Features.mat...
Skipping unexpected shape: (6, 881)
Done.

This block moves files into subdirectories

import os
import shutil

# Base source directory
source_dir = "./images"

# --- ASB ---
asb_target_dir = os.path.join(source_dir, "ASB")
os.makedirs(asb_target_dir, exist_ok=True)

for filename in os.listdir(source_dir):
    if filename.startswith("ASB_Test"):
        src_path = os.path.join(source_dir, filename)
        dst_path = os.path.join(asb_target_dir, filename)
        if os.path.exists(src_path):  # only move if it still exists
            shutil.move(src_path, dst_path)

# --- NT ---
nt_target_dir = os.path.join(source_dir, "NT")
os.makedirs(nt_target_dir, exist_ok=True)

for filename in os.listdir(source_dir):
    if filename.startswith("NT_Test"):
        src_path = os.path.join(source_dir, filename)
        dst_path = os.path.join(nt_target_dir, filename)
        if os.path.exists(src_path):
            shutil.move(src_path, dst_path)

# --- UT ---
ut_target_dir = os.path.join(source_dir, "UT")
os.makedirs(ut_target_dir, exist_ok=True)

for filename in os.listdir(source_dir):
    if filename.startswith("UT_Test"):
        src_path = os.path.join(source_dir, filename)
        dst_path = os.path.join(ut_target_dir, filename)
        if os.path.exists(src_path):
            shutil.move(src_path, dst_path)
import os
import pandas as pd
from scipy.io import loadmat

# --- ASB ---
prefix = "ASB"
mat_path = f"extracted_dataset/mmc1/{prefix}_TestSet_Features.mat"
output_csv = f"labels/{prefix}_labels.csv"

mat_data = loadmat(mat_path)
labels = mat_data['YTest'].squeeze()

filenames = [f"{prefix}_Image_{i:03d}.png" for i in range(len(labels))]
df = pd.DataFrame({'filename': filenames, 'label': labels.astype(int)})

os.makedirs("labels", exist_ok=True)
df.to_csv(output_csv, index=False)
print(f"Saved {len(df)} labels to {output_csv}")

# --- NT ---
prefix = "NT"
mat_path = f"extracted_dataset/mmc1/{prefix}_TestSet_Features.mat"
output_csv = f"labels/{prefix}_labels.csv"

mat_data = loadmat(mat_path)
labels = mat_data['YTest'].squeeze()

filenames = [f"{prefix}_Image_{i:03d}.png" for i in range(len(labels))]
df = pd.DataFrame({'filename': filenames, 'label': labels.astype(int)})

os.makedirs("labels", exist_ok=True)
df.to_csv(output_csv, index=False)
print(f"Saved {len(df)} labels to {output_csv}")

# --- UT ---
prefix = "UT"
mat_path = f"extracted_dataset/mmc1/{prefix}_TestSet_Features.mat"
output_csv = f"labels/{prefix}_labels.csv"

mat_data = loadmat(mat_path)
labels = mat_data['YTest'].squeeze()

filenames = [f"{prefix}_Image_{i:03d}.png" for i in range(len(labels))]
df = pd.DataFrame({'filename': filenames, 'label': labels.astype(int)})

os.makedirs("labels", exist_ok=True)
df.to_csv(output_csv, index=False)
print(f"Saved {len(df)} labels to {output_csv}")
Saved 3741 labels to labels/ASB_labels.csv
Saved 1407 labels to labels/NT_labels.csv
Saved 881 labels to labels/UT_labels.csv
shutil.move("./labels/ASB_labels.csv", "./images/ASB/")
shutil.move("./labels/NT_labels.csv", "./images/NT/")
shutil.move("./labels/UT_labels.csv", "./images/UT/");

The homebrew ANN#

import imageio.v3 as imageio  # newer imageio uses imageio.v3
import matplotlib.pyplot as plt
import numpy as np

# Read as float grayscale image
img_array = imageio.imread(
    "/home/sensei/ce-5319-webroot/MLBE4CE/chapters/14-neuralnetworks/images/UT/UT_TestSet_Imgs_000.png",
    mode='F'
)

# Invert colors and flatten
img_data = 255.0 - img_array.reshape(128 * 128)

# Plot
plt.imshow(np.asarray(img_data).reshape((128, 128)), cmap='Greys')
plt.show()
plt.close('all')
../../_images/af9ab391d48855efd7c36d778133580604339e4f58a0662a10a0e1b81bdd5cde.png
import numpy              # useful numerical routines
import scipy.special      # special functions library
import scipy.misc         # image processing code
#import imageio           # deprecated as typical
import imageio.v2 as imageio
import matplotlib.pyplot  # import plotting routines

The file pathnames are unique to my computer and are shown here so the notebook renders and typesets correctly.

# now we have to flatten each image, put into a csv file, and add the truth table
# myann expects
# truth, image ....
#howmanyimages = 881
import csv
howmanyimages = 24 # a small subset for demonstration
testimage = numpy.array([i for i in range(howmanyimages)])
split = 0.2 # fraction to hold out for testing
numwritten = 0
# training file
outputfile1 = "ut-881-train.csv" #local to this directory
outfile1 = open(outputfile1,'w')  # open the file in the write mode
writer1 = csv.writer(outfile1) # create the csv writer
# testing file
outputfile2 = "ut-881-test.csv" #local to this directory
outfile2 = open(outputfile2,'w')  # open the file in the write mode
writer2 = csv.writer(outfile2) # create the csv writer
# process truth table (absolute pathname)
groundtruth = open("/home/sensei/ce-5319-webroot/MLBE4CE/chapters/14-neuralnetworks/images/UT/UT_labels.csv","r") #open the file in the reader mode
reader = csv.reader(groundtruth)
truthtable=[] # empty list to store class
for row in reader:
        truthtable.append(row[1])

for irow in range(len(truthtable)-1):
    truthtable[irow]=truthtable[irow+1] # shift all entries by 1
    
#print(truthtable[0:4])
#import imageio.v3 as imageio  # use imageio.v3 for mode='F'
#import numpy as np
#import csv

# Assume truthtable, split, howmanyimages, writer1, writer2, outfile1, outfile2 already defined
numwritten = 0
np.random.seed(11)

for i in range(howmanyimages):
    # build zero-padded image filename: 000, 001, ...
    image_name = f"/home/sensei/ce-5319-webroot/MLBE4CE/chapters/14-neuralnetworks/images/UT/UT_TestSet_Imgs_{i:03}.png"
    
    # read and flatten image
    img_array = imageio.imread(image_name, mode='F')
    img_data = 255.0 - img_array.flatten()  # ensure it's 1D, matches reshape(16384)

    # add label to the front
    newimage = np.insert(img_data, 0, float(truthtable[i]))

    # randomly assign to training or test set
    if np.random.uniform() <= split:
        writer2.writerow(newimage)
    else:
        writer1.writerow(newimage)
    
    numwritten += 1

outfile1.close()
outfile2.close()
print("Images segregated and processed:", numwritten)
Images segregated and processed: 24
class neuralNetwork:  # Class Definitions 

    # initialize the neural network
    def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
        # set number of nodes in input, hidden, and output layer
        self.inodes = inputnodes
        self.hnodes = hiddennodes
        self.onodes = outputnodes

        # learning rate
        self.lr = learningrate
        
        # initalize weight matrices
        #
        # link weight matrices, wih (input to hidden) and
        #                       who (hidden to output)
        # weights inside the arrays are w_i_j where link is from node i
        # to node j in next layer
        #
        # w11 w21
        # w12 w22 etc.
        
        self.wih = (numpy.random.rand(self.hnodes, self.inodes) - 0.5)
        self.who = (numpy.random.rand(self.onodes, self.hnodes) - 0.5)

        # activation function
        self.activation_function = lambda x:scipy.special.expit(x)
        pass

    # train the neural network
    def train(self, inputs_list, targets_list):
        # convert input list into 2D array
        inputs = numpy.array(inputs_list, ndmin=2).T

        # convert target list into 2D array
        targets = numpy.array(targets_list, ndmin=2).T
        
        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)

        # calculate signals from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs)

        # calculate signals into output layer
        final_inputs = numpy.dot(self.who, hidden_outputs)

        # calculate signals from output layer
        final_outputs = self.activation_function(final_inputs)

        # calculate output errors (target - model)
        output_errors = targets - final_outputs

        # calculate hidden layer errors (split by weigths recombined in hidden layer)
        hidden_errors = numpy.dot(self.who.T, output_errors)

        # update the weights for the links from hidden to output layer
        self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))

        # update the weights for the links from input to hidden layer
        self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))                                       
        
        pass

    # query the neural network
    def query(self, inputs_list):
        # convert input list into 2D array
        inputs = numpy.array(inputs_list, ndmin=2).T

        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)

        # calculate signals from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs)

        # calculate signals into output layer
        final_inputs = numpy.dot(self.who, hidden_outputs)

        # calculate signals from output layer
        final_outputs = self.activation_function(final_inputs)

        return final_outputs

        pass
print("neuralNetwork Class Loads OK")
neuralNetwork Class Loads OK
# Test case 1 p130 MYONN
# number of input, hidden, and output nodes
input_nodes  = 16384    # 28X28 Pixel Image 
hidden_nodes = 1638    # Should be smaller than input count (or kind of useless)
output_nodes =  2    # Classifications
learning_rate = 0.1   # set learning rate
n = neuralNetwork(input_nodes,hidden_nodes,output_nodes,learning_rate) # create an instance
print("Instance n Created")
Instance n Created
# load a training file
# replace code here with a URL get
## training_data_file = open("mnist_train_100.csv",'r') #connect the file#
training_data_file = open("ut-881-train.csv",'r') #connect the file#
training_data_list = training_data_file.readlines() #read entire contents of file into object: data_list#
training_data_file.close() #disconnect the file#
# print(len(training_data_list))   ## activate for debugging otherwise leave disabled
# train the neural network
howManyTrainingTimes = 0
howManyEpisodes = 1
for times in range(howManyEpisodes):  # added outer loop for repeat training same data set 
    howManyTrainingRecords = 0
    for record in training_data_list:
    # split the values on the commas
        all_values = record.split(',') # split datalist on commas - all records.  Is thing going to work? #
        inputs = (numpy.asarray(all_values[1:], dtype=float) / 255.0 * 0.99) + 0.01
#        inputs = (numpy.asarray(all_values[1:])/255.0 * 0.99) + 0.01
#        inputs = (numpy.asfarray(all_values[1:])/255.0 * 0.99) + 0.01
    # print(inputs)          ## activate for debugging otherwise leave disabled
    # create target output values -- all 0.01 except for the label of 0.99
        targets = numpy.zeros(output_nodes) + 0.01
    # all_values[0] is the target for this record
    #  print(int(numpy.asfarray(all_values[0])))
        #targets[int(numpy.asarray(all_values[0]))] = 0.99
        targets[int(float(all_values[0]))] = 0.99
#        print(targets)
    # targets = numpy.asfarray(all_values[0])
        n.train(inputs, targets)
        howManyTrainingRecords += 1
        pass
    howManyTrainingTimes += 1
    learning_rate *= 0.9
    pass
print ("training records processed   = ",howManyTrainingRecords)
print ("training episodes            = ",howManyTrainingTimes)
# load a production file
test_data_file = open("ut-881-test.csv",'r') #connect the file#
#test_data_file = open("mnist_test.csv",'r') #connect the file#
test_data_list = test_data_file.readlines() #read entire contents of file into object: data_list#
test_data_file.close() #disconnect the file#
training records processed   =  16
training episodes            =  1
# test the neural network
scorecard = [] # empty array for keeping score

# run through the records in test_data_list
howManyTestRecords = 0
for record in test_data_list:
    # split the values on the commas
    all_values = record.split(',') # split datalist on commas - all records #
#    correct_label = int(all_values[0]) # correct answer is first element of all_values
#    correct_label = int(numpy.asarray(all_values[0])) # correct answer is first element of all_values
    correct_label = int(float(all_values[0]))  # converts '1.0' → 1.0 → 1
    # scale and shift the inputs
    inputs = (numpy.asarray(all_values[1:], dtype=float)/255.0 * 0.99) + 0.01
    # query the neural network
    outputs = n.query(inputs)
    predict_label = numpy.argmax(outputs)
    print("predict =",predict_label,correct_label,"= correct") # activate for small test sets only!
    if (predict_label == correct_label):
        scorecard.append(1)
    else:
        scorecard.append(0)
        pass
    howManyTestRecords += 1
    pass
print ("production records processed =", howManyTestRecords)
## print scorecard   # activate for small test sets only!
# calculate performance score, fraction of correct answers
scorecard_array = numpy.asarray(scorecard,dtype=int)
print ("performance = ",scorecard_array.sum()/scorecard_array.size)
predict = 1 1 = correct
predict = 1 1 = correct
predict = 1 1 = correct
predict = 1 1 = correct
predict = 1 1 = correct
predict = 1 1 = correct
predict = 1 1 = correct
predict = 1 1 = correct
production records processed = 8
performance =  1.0
# lets try one of my own pictures
# first read and render
#img_array = scipy.misc.imread("cat128.png", flatten = True) Fuckers deprecated this utility!
img_array = imageio.imread("/home/sensei/ce-5319-webroot/MLBE4CE/chapters/14-neuralnetworks/cat128.png", mode='F')
img_data = 255.0 - img_array.reshape(16384)
img_data = ((img_data/255.0)*0.99) + 0.01
matplotlib.pyplot.imshow(numpy.asarray(img_data,dtype=float).reshape((128,128)),cmap = 'Greys') # construct a graphic object #
matplotlib.pyplot.show() # show the graphic object to a window #
matplotlib.pyplot.close('all')

mynumber = n.query(img_data)
mylabel = numpy.argmax(mynumber)

m0=img_data.mean()  # gather some statistics
v0=img_data.var()

if mylabel == 0:
    msg = "No Cracks Detected"
elif mylabel == 1:
    msg = "Cracks Detected"
print ("Cat Image ", msg)
../../_images/2527c609992c550340617bf58bf045a8ab79e64081dcd2f62af242a9b77a962c.png
Cat Image  Cracks Detected
# lets try one of my own pictures
# first read and render
#img_array = scipy.misc.imread("cat128.png", flatten = True) Fuckers deprecated this utility!
img_array = imageio.imread("/home/sensei/ce-5319-webroot/MLBE4CE/chapters/14-neuralnetworks/waterfall128.png", mode='F')
img_data = 255.0 - img_array.reshape(16384)
img_data = ((img_data/255.0)*0.99) + 0.01
matplotlib.pyplot.imshow(numpy.asarray(img_data,dtype=float).reshape((128,128)),cmap = 'Greys') # construct a graphic object #
matplotlib.pyplot.show() # show the graphic object to a window #
matplotlib.pyplot.close('all')

mynumber = n.query(img_data)
mylabel = numpy.argmax(mynumber)

m0=img_data.mean()  # gather some statistics
v0=img_data.var()

if mylabel == 0:
    msg = "No Cracks Detected"
elif mylabel == 1:
    msg = "Cracks Detected"
print ("Waterfall Image ", msg)
../../_images/41b2ed38ac5ca3f8a0ec392a7be5efa12ed3725d0762f733c5159656dde41bb7.png
Waterfall Image  Cracks Detected
# lets try one of my own pictures
# first read and render
#img_array = scipy.misc.imread("cat128.png", flatten = True) Fuckers deprecated this utility!
img_array = imageio.imread("/home/sensei/ce-5319-webroot/MLBE4CE/chapters/14-neuralnetworks/concrete-cracks.png", mode='F')
img_data = 255.0 - img_array.reshape(16384)
img_data = ((img_data/255.0)*0.99) + 0.01
matplotlib.pyplot.imshow(numpy.asarray(img_data,dtype=float).reshape((128,128)),cmap = 'Greys') # construct a graphic object #
matplotlib.pyplot.show() # show the graphic object to a window #
matplotlib.pyplot.close('all')

mynumber = n.query(img_data)
mylabel = numpy.argmax(mynumber)

m0=img_data.mean()  # gather some statistics
v0=img_data.var()

if mylabel == 0:
    msg = "No Cracks Detected"
elif mylabel == 1:
    msg = "Cracks Detected"
print ("Cracked Concrete ", msg)
../../_images/de759197a3fb5b61cb342cc99299c7ca4fdf6efaf8a1013a6205a92fcd9e04cf.png
Cracked Concrete  Cracks Detected