Cainvas

Elephant Detection App

Credit: AITS Cainvas Community

Photo by Lobster on Dribbble

Downloaded the dataset from GitHub. Credit - Human Wildlife Conflict.

!git clone "https://github.com/arribada/human-wildlife-conflict.git"

Viewing dataset

def view_dataset(path):
    print("Number of samples- ")
    for f in os.listdir(data_dir):
        curr_dir = os.path.join(data_dir, f)
        if os.path.isdir(curr_dir):
            print(f, "-", len(os.listdir(curr_dir)))

data_dir = 'human-wildlife-conflict/Elephant/Object/'
view_dataset(data_dir)
Number of samples- 
human_and_elephant - 807
human - 1502
multiple_obstructing_elephants - 2698
multiple_separate_elephants - 1858
goat - 114
single_elephant - 7615

We will now create a new dataset with classifications of elephant and non-elephant

dirs_containing_elephants = []
dirs_not_containing_elephants = []

for f in os.listdir(data_dir):
    curr_dir = os.path.join(data_dir, f)
    if os.path.isdir(curr_dir):
        if "elephant" in f:
            dirs_containing_elephants.append(curr_dir)
        else:
            dirs_not_containing_elephants.append(curr_dir)

elephant_dataset = "elephant_dataset"
elephant_dir = os.path.join(elephant_dataset, "elephant")
not_elephant_dir = os.path.join(elephant_dataset, "not_elephant")

if os.path.isdir(elephant_dataset):
    shutil.rmtree(elephant_dataset)
os.makedirs(elephant_dir)
os.makedirs(not_elephant_dir)

for dir in dirs_containing_elephants:
    for file in os.listdir(dir):
        shutil.copy2(os.path.join(dir, file), elephant_dir)

for dir in dirs_not_containing_elephants:
    for file in os.listdir(dir):
        shutil.copy2(os.path.join(dir, file), not_elephant_dir)

Let us recheck our dataset

data_dir = 'elephant_dataset/'
view_dataset(data_dir)
Number of samples- 
elephant - 12114
not_elephant - 1616

Downloading the above created dataset

In [1]:
!wget -N "https://cainvas-static.s3.amazonaws.com/media/user_data/cainvas-admin/elephant_dataset.zip" -O elephant_dataset.zip
!unzip -qo elephant_dataset.zip
!rm elephant_dataset.zip
WARNING: timestamping does nothing in combination with -O. See the manual
for details.

--2022-03-20 10:47:45--  https://cainvas-static.s3.amazonaws.com/media/user_data/cainvas-admin/elephant_dataset.zip
Resolving cainvas-static.s3.amazonaws.com (cainvas-static.s3.amazonaws.com)... 52.219.64.108
Connecting to cainvas-static.s3.amazonaws.com (cainvas-static.s3.amazonaws.com)|52.219.64.108|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 78090093 (74M) [application/zip]
Saving to: ‘elephant_dataset.zip’

elephant_dataset.zi 100%[===================>]  74.47M  52.2MB/s    in 1.4s    

2022-03-20 10:47:47 (52.2 MB/s) - ‘elephant_dataset.zip’ saved [78090093/78090093]

Import necessary libraries

In [2]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import layers, callbacks, optimizers
from sklearn.metrics import confusion_matrix, f1_score
from tensorflow import keras 
import os, shutil
import random
from PIL import Image
In [3]:
data_dir = 'elephant_dataset/'

batch_size = 64
# image_size = (32, 32)
image_size = (28, 28)

print("Training set")
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
                data_dir,
                validation_split=0.2,
                subset="training",
                color_mode="grayscale",
                image_size=image_size, 
                seed=113,
                shuffle=True,
                batch_size=batch_size
            )

print("Validation set")
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
                data_dir,
                validation_split=0.2,
                subset="validation",
                color_mode="grayscale",
                image_size=image_size, 
                seed=113,
                shuffle=True,
                batch_size=batch_size
            )
Training set
Found 13730 files belonging to 2 classes.
Using 10984 files for training.
Validation set
Found 13730 files belonging to 2 classes.
Using 2746 files for validation.
In [4]:
class_names = train_ds.class_names
print(class_names)
['elephant', 'not_elephant']
In [5]:
Xtrain = np.empty((0,*image_size,1))
ytrain = np.empty((0,1))

for x in train_ds.enumerate():
    for y in x[1][0]:  
        Xtrain = np.append(Xtrain, np.expand_dims(np.array(y),0), axis = 0)
    #print(Xtrain.shape)
    ytrain = np.append(ytrain, np.array(x[1][1]))
    #print(ytrain.shape)
    
Xtrain.shape, ytrain.shape
Out[5]:
((10984, 28, 28, 1), (10984,))

Augmenting images in the train set to increase dataset size

In [6]:
# # Augmenting images in the train set to increase dataset size

data_augmentation = tf.keras.Sequential(
    [
        layers.experimental.preprocessing.RandomFlip("horizontal"),    # Flip along vertical axes
        layers.experimental.preprocessing.RandomZoom(0.1),    # Randomly zoom images in dataset
    ])


print("Train size (number of samples) before augmentation: ", len(Xtrain))

aug_sample_count = ytrain.tolist().count(float(0.0))//2 - ytrain.tolist().count(float(1.0))
cur_augmented = 0
# Apply only to train set
while(cur_augmented!=aug_sample_count):
    for i in range(len(Xtrain)):
        if ytrain[i] == 1: # not elephant
            aug_image = np.array(data_augmentation(np.expand_dims(Xtrain[0], 0)))
            Xtrain = np.append(Xtrain, aug_image.reshape((1, *image_size, 1)), axis = 0)
            ytrain = np.append(ytrain, [1])
            
            cur_augmented += 1
            if (cur_augmented == aug_sample_count):
                break

    
print("Size (number of samples) of final dataset: ", len(Xtrain))

print(" Dataset shapes: ", Xtrain.shape, ytrain.shape)

#Adding to train_ds
#train_ds = train_ds.concatenate(aug_ds)

#print("Train size (number of batches) after augmentation: ", len(train_ds))
Train size (number of samples) before augmentation:  10984
WARNING:tensorflow:Layer random_flip is casting an input tensor from dtype float64 to the layer's dtype of float32, which is new behavior in TensorFlow 2.  The layer has dtype float32 because its dtype defaults to floatx.

If you intended to run this layer in float32, you can safely ignore this warning. If in doubt, this warning is likely only an issue if you are porting a TensorFlow 1.X model to TensorFlow 2.

To change all layers to have dtype float64 by default, call `tf.keras.backend.set_floatx('float64')`. To change just this layer, pass dtype='float64' to the layer constructor. If you are the author of this layer, you can disable autocasting by passing autocast=False to the base Layer constructor.

Size (number of samples) of final dataset:  14538
 Dataset shapes:  (14538, 28, 28, 1) (14538,)
In [7]:
print("Number of samples - ")
for i in range(len(class_names)):
    print(class_names[i], "-", ytrain.tolist().count(float(i)))
Number of samples - 
elephant - 9692
not_elephant - 4846
In [8]:
Xval = np.empty((0,*image_size,1))
yval = np.empty((0,1))

for x in val_ds.enumerate():
    for y in x[1][0]:  
        Xval = np.append(Xval, np.expand_dims(np.array(y),0), axis = 0)
    #print(Xtrain.shape)
    yval = np.append(yval, np.array(x[1][1]))
    #print(ytrain.shape)
    
Xval.shape, yval.shape
Out[8]:
((2746, 28, 28, 1), (2746,))
In [9]:
print("Number of samples - ")
for i in range(len(class_names)):
    print(class_names[i], "-", yval.tolist().count(float(i)))
Number of samples - 
elephant - 2422
not_elephant - 324

Visualization

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

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

    filenames = os.listdir(data_dir + x)

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

Normalizing the pixel values

Pixel values are now integers between 0 and 255. Changing them to the range [0, 1] for faster convergence.

In [11]:
Xtrain = Xtrain/255
Xval = Xval/255

The Model

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

cb = [callbacks.EarlyStopping(monitor = 'val_loss', patience = 5, restore_best_weights = True)]
---------------------------------------------------------------------------
InvalidArgumentError                      Traceback (most recent call last)
/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/framework/ops.py in _create_c_op(graph, node_def, inputs, control_inputs, op_def)
   1811   try:
-> 1812     c_op = pywrap_tf_session.TF_FinishOperation(op_desc)
   1813   except errors.InvalidArgumentError as e:

InvalidArgumentError: Negative dimension size caused by subtracting 3 from 1 for '{{node conv2d_3/Conv2D}} = Conv2D[T=DT_FLOAT, data_format="NHWC", dilations=[1, 1, 1, 1], explicit_paddings=[], padding="VALID", strides=[1, 1, 1, 1], use_cudnn_on_gpu=true](max_pooling2d_2/MaxPool, conv2d_3/Conv2D/ReadVariableOp)' with input shapes: [?,1,1,32], [3,3,32,64].

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-12-6bff539a2d3f> in <module>
     13     layers.Flatten(),
     14     layers.Dense(32, activation='relu'),
---> 15     layers.Dense(1, activation='sigmoid')
     16 ])
     17 

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/training/tracking/base.py in _method_wrapper(self, *args, **kwargs)
    455     self._self_setattr_tracking = False  # pylint: disable=protected-access
    456     try:
--> 457       result = method(self, *args, **kwargs)
    458     finally:
    459       self._self_setattr_tracking = previous_value  # pylint: disable=protected-access

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/keras/engine/sequential.py in __init__(self, layers, name)
    140         layers = [layers]
    141       for layer in layers:
--> 142         self.add(layer)
    143 
    144   @property

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/training/tracking/base.py in _method_wrapper(self, *args, **kwargs)
    455     self._self_setattr_tracking = False  # pylint: disable=protected-access
    456     try:
--> 457       result = method(self, *args, **kwargs)
    458     finally:
    459       self._self_setattr_tracking = previous_value  # pylint: disable=protected-access

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/keras/engine/sequential.py in add(self, layer)
    219       # If the model is being built continuously on top of an input layer:
    220       # refresh its output.
--> 221       output_tensor = layer(self.outputs[0])
    222       if len(nest.flatten(output_tensor)) != 1:
    223         raise ValueError(SINGLE_LAYER_OUTPUT_ERROR_MSG)

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/keras/engine/base_layer.py in __call__(self, *args, **kwargs)
    924     if _in_functional_construction_mode(self, inputs, args, kwargs, input_list):
    925       return self._functional_construction_call(inputs, args, kwargs,
--> 926                                                 input_list)
    927 
    928     # Maintains info about the `Layer.call` stack.

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/keras/engine/base_layer.py in _functional_construction_call(self, inputs, args, kwargs, input_list)
   1115           try:
   1116             with ops.enable_auto_cast_variables(self._compute_dtype_object):
-> 1117               outputs = call_fn(cast_inputs, *args, **kwargs)
   1118 
   1119           except errors.OperatorNotAllowedInGraphError as e:

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/keras/layers/convolutional.py in call(self, inputs)
    245       inputs = array_ops.pad(inputs, self._compute_causal_padding(inputs))
    246 
--> 247     outputs = self._convolution_op(inputs, self.kernel)
    248 
    249     if self.use_bias:

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/util/dispatch.py in wrapper(*args, **kwargs)
    199     """Call target, and fall back on dispatchers if there is a TypeError."""
    200     try:
--> 201       return target(*args, **kwargs)
    202     except (TypeError, ValueError):
    203       # Note: convert_to_eager_tensor currently raises a ValueError, not a

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/ops/nn_ops.py in convolution_v2(input, filters, strides, padding, data_format, dilations, name)
   1016       data_format=data_format,
   1017       dilations=dilations,
-> 1018       name=name)
   1019 
   1020 

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/ops/nn_ops.py in convolution_internal(input, filters, strides, padding, data_format, dilations, name, call_from_convolution, num_spatial_dims)
   1146           data_format=data_format,
   1147           dilations=dilations,
-> 1148           name=name)
   1149     else:
   1150       if channel_index == 1:

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/ops/nn_ops.py in _conv2d_expanded_batch(input, filters, strides, padding, data_format, dilations, name)
   2590         data_format=data_format,
   2591         dilations=dilations,
-> 2592         name=name)
   2593   return squeeze_batch_dims(
   2594       input,

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/ops/gen_nn_ops.py in conv2d(input, filter, strides, padding, use_cudnn_on_gpu, explicit_paddings, data_format, dilations, name)
    977                   padding=padding, use_cudnn_on_gpu=use_cudnn_on_gpu,
    978                   explicit_paddings=explicit_paddings,
--> 979                   data_format=data_format, dilations=dilations, name=name)
    980   _result = _outputs[:]
    981   if _execute.must_record_gradient():

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/framework/op_def_library.py in _apply_op_helper(op_type_name, name, **keywords)
    742       op = g._create_op_internal(op_type_name, inputs, dtypes=None,
    743                                  name=scope, input_types=input_types,
--> 744                                  attrs=attr_protos, op_def=op_def)
    745 
    746     # `outputs` is returned as a separate return value so that the output

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/framework/func_graph.py in _create_op_internal(self, op_type, inputs, dtypes, input_types, name, attrs, op_def, compute_device)
    591     return super(FuncGraph, self)._create_op_internal(  # pylint: disable=protected-access
    592         op_type, inputs, dtypes, input_types, name, attrs, op_def,
--> 593         compute_device)
    594 
    595   def capture(self, tensor, name=None, shape=None):

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/framework/ops.py in _create_op_internal(self, op_type, inputs, dtypes, input_types, name, attrs, op_def, compute_device)
   3483           input_types=input_types,
   3484           original_op=self._default_original_op,
-> 3485           op_def=op_def)
   3486       self._create_op_helper(ret, compute_device=compute_device)
   3487     return ret

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/framework/ops.py in __init__(self, node_def, g, inputs, output_types, control_inputs, input_types, original_op, op_def)
   1973         op_def = self._graph._get_op_def(node_def.op)
   1974       self._c_op = _create_c_op(self._graph, node_def, inputs,
-> 1975                                 control_input_ops, op_def)
   1976       name = compat.as_str(node_def.name)
   1977     # pylint: enable=protected-access

/opt/tljh/user/lib/python3.7/site-packages/tensorflow/python/framework/ops.py in _create_c_op(graph, node_def, inputs, control_inputs, op_def)
   1813   except errors.InvalidArgumentError as e:
   1814     # Convert to ValueError for backwards compatibility.
-> 1815     raise ValueError(str(e))
   1816 
   1817   return c_op

ValueError: Negative dimension size caused by subtracting 3 from 1 for '{{node conv2d_3/Conv2D}} = Conv2D[T=DT_FLOAT, data_format="NHWC", dilations=[1, 1, 1, 1], explicit_paddings=[], padding="VALID", strides=[1, 1, 1, 1], use_cudnn_on_gpu=true](max_pooling2d_2/MaxPool, conv2d_3/Conv2D/ReadVariableOp)' with input shapes: [?,1,1,32], [3,3,32,64].
In [ ]:
model.summary()
In [ ]:
model.compile(loss=keras.losses.BinaryCrossentropy(), optimizer=optimizers.Adam(0.0001), metrics=['accuracy'])

history = model.fit(Xtrain, ytrain, validation_data=(Xval, yval), epochs=300, callbacks=cb)
In [ ]:
model.evaluate(Xval, yval)
In [ ]:
ypred = (model.predict(Xval)>0.5).astype('int')
In [ ]:
cm = confusion_matrix(yval, ypred)

cm = cm.astype('int') / cm.sum(axis=1)[:, np.newaxis]

fig = plt.figure(figsize = (4, 4))
ax = fig.add_subplot(111)

for i in range(cm.shape[1]):
    for j in range(cm.shape[0]):
        if cm[i,j] > 0.8:
            clr = "white"
        else:
            clr = "black"
        ax.text(j, i, format(cm[i, j], '.2f'), horizontalalignment="center", color=clr)

_ = ax.imshow(cm, cmap=plt.cm.Blues)
ax.set_xticks(range(len(class_names)))
ax.set_yticks(range(len(class_names)))
ax.set_xticklabels(class_names, rotation = 90)
ax.set_yticklabels(class_names)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()
In [ ]:
f1_score(yval, ypred, average = 'binary')

Plotting the metrics

In [ ]:
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 [ ]:
plot(history.history, "accuracy", 'val_accuracy')
In [ ]:
plot(history.history, "loss", 'val_loss')

Prediction

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

for i in val_ds.as_numpy_iterator():
    img, label = i    
    plt.axis('off')   # remove axes
    plt.imshow(img[x])    # shape from (64, 64, 64, 1) --> (64, 64, 1)
    output = model.predict(np.expand_dims(img[x],0))[0][0]    # getting output; input shape (64, 64, 3) --> (1, 64, 64, 1)
    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]])
    break

deepC

In [ ]:
model.save('elephant_classification.h5')

!deepCC elephant_classification.h5