Cainvas

Surface crack detection

Credit: AITS Cainvas Community

Photo by Tevis Godfrey on Dribbble

Concrete surface cracks are major defects in civil structures. Identifying them is an important part of the building inspection process where the rigidity and tensile strength of the building are evaluated.

Automating this process involves using a mobile bot with a camera input that scans the surfaces of the building for cracks and logs the locations for the same.

In order to make predictions on a larger surface, the camera is to be moved over the surface, covering it in small sections. The trained model is then applied to the image of this smaller section.

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix
from keras import layers, optimizers, models, preprocessing, losses, callbacks, Input, Model, applications
import os
import random
from PIL import Image
import keras
import tensorflow as tf

The dataset

On Kaggle by Arun Pandian R

Özgenel, Çağlar Fırat (2019), “Concrete Crack Images for Classification”, Mendeley Data, v2 Reference

The dataset has 20000 images each of two categories - with and without cracks on concrete surfaces without any augmentation techniques applied.

In [2]:
!wget https://cainvas-static.s3.amazonaws.com/media/user_data/AyishaR0/surface.zip
    
!unzip -qo -d surface surface.zip

!rm surface.zip
--2021-04-11 10:56:43--  https://cainvas-static.s3.amazonaws.com/media/user_data/AyishaR0/surface.zip
Resolving cainvas-static.s3.amazonaws.com (cainvas-static.s3.amazonaws.com)... 52.219.64.52
Connecting to cainvas-static.s3.amazonaws.com (cainvas-static.s3.amazonaws.com)|52.219.64.52|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 244476725 (233M) [application/x-zip-compressed]
Saving to: ‘surface.zip’

surface.zip         100%[===================>] 233.15M  73.5MB/s    in 3.2s    

2021-04-11 10:56:46 (73.5 MB/s) - ‘surface.zip’ saved [244476725/244476725]

The dataset has two folders - negative and positive, each with their respective images.

Using 80-20 (train-test) split on the images.

In [3]:
# Loading the dataset

path = 'surface'
batch_size = 256

train_df = preprocessing.image_dataset_from_directory(path, label_mode = 'binary', validation_split = 0.2, seed = 113, subset='training', batch_size = batch_size)

test_df = preprocessing.image_dataset_from_directory(path, label_mode = 'binary', validation_split = 0.2, seed = 113, subset='validation', batch_size = batch_size)
Found 40000 files belonging to 2 classes.
Using 32000 files for training.
Found 40000 files belonging to 2 classes.
Using 8000 files for validation.
In [4]:
# Looking into the class labels

class_names = train_df.class_names

print("Train class names: ", train_df.class_names)
print("Test class names: ", test_df.class_names)
Train class names:  ['Negative', 'Positive']
Test class names:  ['Negative', 'Positive']

Visualization

In [5]:
num_samples = 4    # the number of samples to be displayed in each class

for x in class_names:
    plt.figure(figsize=(20, 20))

    filenames = os.listdir(path + '/' + x)

    for i in range(num_samples):
        ax = plt.subplot(1, num_samples, i + 1)
        img = Image.open(path +'/' + x + '/' + filenames[i])
        plt.imshow(img)
        plt.title(x)
        plt.axis("off")

Preprocessing

Normalization

In [6]:
# Normalizing the pixel values for faster convergence

normalization_layer = layers.experimental.preprocessing.Rescaling(1./255)

train_df = train_df.map(lambda x, y: (normalization_layer(x), y))
test_df = test_df.map(lambda x, y: (normalization_layer(x), y))

The model

In [7]:
model = models.Sequential([
    layers.Conv2D(8, 2, activation='relu', input_shape=(256,256,3)),
    layers.Conv2D(16, 2, activation='relu'),
    layers.MaxPool2D(pool_size=(2, 2)),
    
    layers.Conv2D(16, 2, activation='relu'),
    layers.Conv2D(32, 2, activation='relu'),
    layers.MaxPool2D(pool_size=(2, 2)),
    
    layers.Flatten(),
    layers.Dense(64, activation='relu'),
    layers.Dense(32, activation='relu'),
    layers.Dense(1, activation='sigmoid')
])

cb = [callbacks.EarlyStopping(monitor = 'val_loss', patience = 5, restore_best_weights = True)]
In [8]:
model.compile(loss=losses.BinaryCrossentropy(), optimizer=optimizers.Adam(0.0001), metrics=['accuracy'])

history = model.fit(train_df, validation_data =  test_df, epochs=32, callbacks = cb)
Epoch 1/32
125/125 [==============================] - 92s 736ms/step - loss: 0.5602 - accuracy: 0.7218 - val_loss: 0.3801 - val_accuracy: 0.8819
Epoch 2/32
125/125 [==============================] - 91s 728ms/step - loss: 0.2703 - accuracy: 0.9101 - val_loss: 0.1935 - val_accuracy: 0.9526
Epoch 3/32
125/125 [==============================] - 91s 725ms/step - loss: 0.1459 - accuracy: 0.9536 - val_loss: 0.1247 - val_accuracy: 0.9548
Epoch 4/32
125/125 [==============================] - 91s 728ms/step - loss: 0.1046 - accuracy: 0.9664 - val_loss: 0.0920 - val_accuracy: 0.9728
Epoch 5/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0870 - accuracy: 0.9709 - val_loss: 0.0756 - val_accuracy: 0.9714
Epoch 6/32
125/125 [==============================] - 91s 730ms/step - loss: 0.0695 - accuracy: 0.9760 - val_loss: 0.0705 - val_accuracy: 0.9730
Epoch 7/32
125/125 [==============================] - 91s 730ms/step - loss: 0.0556 - accuracy: 0.9806 - val_loss: 0.0446 - val_accuracy: 0.9881
Epoch 8/32
125/125 [==============================] - 91s 730ms/step - loss: 0.0364 - accuracy: 0.9883 - val_loss: 0.0308 - val_accuracy: 0.9901
Epoch 9/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0302 - accuracy: 0.9905 - val_loss: 0.0282 - val_accuracy: 0.9905
Epoch 10/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0261 - accuracy: 0.9914 - val_loss: 0.0257 - val_accuracy: 0.9914
Epoch 11/32
125/125 [==============================] - 91s 730ms/step - loss: 0.0240 - accuracy: 0.9928 - val_loss: 0.0235 - val_accuracy: 0.9923
Epoch 12/32
125/125 [==============================] - 91s 730ms/step - loss: 0.0209 - accuracy: 0.9940 - val_loss: 0.0212 - val_accuracy: 0.9939
Epoch 13/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0201 - accuracy: 0.9938 - val_loss: 0.0246 - val_accuracy: 0.9911
Epoch 14/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0175 - accuracy: 0.9950 - val_loss: 0.0180 - val_accuracy: 0.9948
Epoch 15/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0159 - accuracy: 0.9958 - val_loss: 0.0194 - val_accuracy: 0.9935
Epoch 16/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0157 - accuracy: 0.9956 - val_loss: 0.0228 - val_accuracy: 0.9925
Epoch 17/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0157 - accuracy: 0.9955 - val_loss: 0.0217 - val_accuracy: 0.9940
Epoch 18/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0139 - accuracy: 0.9961 - val_loss: 0.0178 - val_accuracy: 0.9940
Epoch 19/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0131 - accuracy: 0.9961 - val_loss: 0.0198 - val_accuracy: 0.9944
Epoch 20/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0129 - accuracy: 0.9962 - val_loss: 0.0212 - val_accuracy: 0.9934
Epoch 21/32
125/125 [==============================] - 91s 730ms/step - loss: 0.0130 - accuracy: 0.9961 - val_loss: 0.0168 - val_accuracy: 0.9946
Epoch 22/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0104 - accuracy: 0.9971 - val_loss: 0.0281 - val_accuracy: 0.9908
Epoch 23/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0101 - accuracy: 0.9975 - val_loss: 0.0159 - val_accuracy: 0.9949
Epoch 24/32
125/125 [==============================] - 91s 729ms/step - loss: 0.0099 - accuracy: 0.9975 - val_loss: 0.0159 - val_accuracy: 0.9946
Epoch 25/32
125/125 [==============================] - 91s 727ms/step - loss: 0.0101 - accuracy: 0.9973 - val_loss: 0.0181 - val_accuracy: 0.9937
Epoch 26/32
125/125 [==============================] - 91s 727ms/step - loss: 0.0099 - accuracy: 0.9974 - val_loss: 0.0198 - val_accuracy: 0.9937
Epoch 27/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0086 - accuracy: 0.9977 - val_loss: 0.0156 - val_accuracy: 0.9948
Epoch 28/32
125/125 [==============================] - 91s 728ms/step - loss: 0.0077 - accuracy: 0.9980 - val_loss: 0.0149 - val_accuracy: 0.9952
Epoch 29/32
125/125 [==============================] - 91s 727ms/step - loss: 0.0087 - accuracy: 0.9974 - val_loss: 0.0196 - val_accuracy: 0.9946
Epoch 30/32
125/125 [==============================] - 91s 727ms/step - loss: 0.0083 - accuracy: 0.9975 - val_loss: 0.0169 - val_accuracy: 0.9948
Epoch 31/32
125/125 [==============================] - 91s 727ms/step - loss: 0.0071 - accuracy: 0.9979 - val_loss: 0.0145 - val_accuracy: 0.9952
Epoch 32/32
125/125 [==============================] - 91s 727ms/step - loss: 0.0070 - accuracy: 0.9985 - val_loss: 0.0167 - val_accuracy: 0.9949
In [9]:
model.evaluate(test_df)
32/32 [==============================] - 12s 363ms/step - loss: 0.0167 - accuracy: 0.9949
Out[9]:
[0.016650965437293053, 0.9948750138282776]

Plotting the metrics

In [10]:
def plot(history, variable1, variable2):
    plt.plot(range(len(history[variable1])), history[variable1])
    plt.plot(range(len(history[variable2])), history[variable2])
    plt.legend([variable1, variable2])
    plt.title(variable1)
In [11]:
plot(history.history, "accuracy", 'val_accuracy')
In [12]:
plot(history.history, "loss", 'val_loss')

Prediction

In [13]:
# pick random test data sample from one batch
x = random.randint(0, 32 - 1) # default batch size is 32

for i in test_df.as_numpy_iterator():
    img, label = i    
    plt.axis('off')   # remove axes
    plt.imshow(img[x])    # shape from (32, 256, 256, 3) --> (256, 256, 3)
    output = model.predict(np.expand_dims(img[x],0))[0][0]    # getting output; input shape (256, 256, 3) --> (1, 256, 256, 3)
    pred = (output > 0.5).astype('int')
    print("Predicted: ", class_names[pred], '(', output, '-->', pred, ')')    # Picking the label from class_names base don the model output
    print("True: ", class_names[label[x][0].astype('int')])
    break
Predicted:  Positive ( 0.9999989 --> 1 )
True:  Positive

deepC

In [14]:
model.save('surface.h5')

!deepCC surface.h5
[INFO]
Reading [keras model] 'surface.h5'
[SUCCESS]
Saved 'surface_deepC/surface.onnx'
[INFO]
Reading [onnx model] 'surface_deepC/surface.onnx'
[INFO]
Model info:
  ir_vesion : 5
  doc       : 
[WARNING]
[ONNX]: terminal (input/output) conv2d_input's shape is less than 1. Changing it to 1.
[WARNING]
[ONNX]: terminal (input/output) dense_2's shape is less than 1. Changing it to 1.
WARN (GRAPH): found operator node with the same name (dense_2) as io node.
[INFO]
Running DNNC graph sanity check ...
[SUCCESS]
Passed sanity check.
[INFO]
Writing C++ file 'surface_deepC/surface.cpp'
[INFO]
deepSea model files are ready in 'surface_deepC/' 
[RUNNING COMMAND]
g++ -std=c++11 -O3 -fno-rtti -fno-exceptions -I. -I/opt/tljh/user/lib/python3.7/site-packages/deepC-0.13-py3.7-linux-x86_64.egg/deepC/include -isystem /opt/tljh/user/lib/python3.7/site-packages/deepC-0.13-py3.7-linux-x86_64.egg/deepC/packages/eigen-eigen-323c052e1731 "surface_deepC/surface.cpp" -D_AITS_MAIN -o "surface_deepC/surface.exe"
[RUNNING COMMAND]
size "surface_deepC/surface.exe"
   text	   data	    bss	    dec	    hex	filename
31678919	   3368	    760	31683047	1e371e7	surface_deepC/surface.exe
[SUCCESS]
Saved model as executable "surface_deepC/surface.exe"