diff --git a/CharClassification/generate_images.py b/CharClassification/generate_images.py new file mode 100644 index 000000000..80dc9b105 --- /dev/null +++ b/CharClassification/generate_images.py @@ -0,0 +1,138 @@ +import os +import random +import numpy as np +import uuid + +PATH_TO_LIGHT_BACKGROUNDS = 'light_backgrounds/' +PATH_TO_DARK_BACKGROUNDS = 'dark_backgrounds/' +PATH_TO_FONT_FILES = 'fonts/' +OUTPUT_DIR = 'output/' +NUM_IMAGES_PER_CLASS = 10 + +# Get all files from directory +def get_files_from_dir(dirname): + list_files = (os.listdir(dirname)) + list_files = [dirname + x for x in list_files] + return list_files + + +# Random perspective distortion created by randomly moving the for corners of the image. +def get_distort_arg(): + amount = 5 + hundred_minus_amount = 100 - amount + return '\'0,0 ' + str(np.random.randint(0,amount)) + ',' + str(np.random.randint(0,amount)) + ' 100,0 ' + str(np.random.randint(hundred_minus_amount,100)) + ',' + str(np.random.randint(0,amount)) + ' 0,100 ' + str(np.random.randint(0,amount)) + ',' + str(np.random.randint(hundred_minus_amount,100)) + ' 100,100 ' + str(np.random.randint(hundred_minus_amount,100)) + ',' + str(np.random.randint(hundred_minus_amount,100)) + '\'' + +# Randomly extracts 32x32 regions of an image and saves it to outdir +def create_random_crops(image_filename, num_crops, out_dir): + dim = os.popen('convert ' + image_filename + ' -ping -format "%w %h" info:').read() + dim = dim.split() + im_width = int(dim[0]) + im_height = int(dim[1]) + + for i in range(0, num_crops): + # Randomly select first co-ordinate of square for cropping image + x = random.randint(0,im_width - 32) + y = random.randint(0,im_height - 32) + outfile = uuid.uuid4().hex + '.jpg' + command = "magick convert "+ image_filename + " -crop 32x32"+"+"+str(x)+"+"+str(y)+" " + os.path.join(out_dir, outfile) + os.system(str(command)) + +# Generate crops for all files in file_list and store them in dirname +def generate_crops(file_list, dirname): + if not os.path.isdir(dirname): + os.mkdir(dirname) + for f in file_list: + create_random_crops(f, 10, dirname) + + +# List of characters +char_list = [] +for i in range(65, 65+26): + char_list.append(chr(i)) + +# List of digits +for j in range(48,48+10): + char_list.append(chr(j)) + +# List of light font colors +color_light = ['white','lime','gray','yellow','silver','aqua'] + +# List of light dark colors +color_dark = ['black','green','maroon','blue','purple','red'] + + +# List of light backgrounds +light_backgrounds = get_files_from_dir(PATH_TO_LIGHT_BACKGROUNDS) + +# List of dark backgrounds +dark_backgrounds = get_files_from_dir(PATH_TO_DARK_BACKGROUNDS) + +# List of font files +list_files_fontt = get_files_from_dir(PATH_TO_FONT_FILES) + + + +light_backgrounds_crops_dir = 'light_backgrounds_crops/' +dark_backgrounds_crops_dir = 'dark_backgrounds_crops/' + +generate_crops(light_backgrounds, light_backgrounds_crops_dir) +generate_crops(dark_backgrounds, dark_backgrounds_crops_dir) + +# List of all files in the crops directory +light_backgrounds = get_files_from_dir(light_backgrounds_crops_dir) +dark_backgrounds = get_files_from_dir(dark_backgrounds_crops_dir) + +# List of all backgrounds +all_backgrounds = [dark_backgrounds, light_backgrounds] + + +# Sample Command----- magick convert image.jpg -fill Black -font Courier-Oblique -weight 50 -pointsize 12 -gravity center -blur 0x8 -evaluate Gaussian-noise 1.2 -annotate 0+0 "Some text" output_image + +for i in range(0,len(char_list)): + char = char_list[i] + char_output_dir = OUTPUT_DIR + str(char) + "/" + + if not os.path.exists(char_output_dir): + os.makedirs(char_output_dir) + + print("Generating data " + char_output_dir) + + + # Generate synthetic images + for j in range(0,NUM_IMAGES_PER_CLASS): + + # Choose a light or dark background + path = random.choice(all_backgrounds) + + # Choose a file + list_filernd = random.choice(path) + + # Choose a font + list_rfo = random.choice(list_files_fontt) + + # Get random distortion + distort_arg = get_distort_arg() + + # Get random blur amount + blur = random.randint(0,3) + + # Get random noise amount + noise = random.randint(0,5) + + # Add random shifts from the center + x = str(random.randint(-3,3)) + y = str(random.randint(-3,3)) + + # Choose light color for dark backgrounds and vice-versa + if path == all_backgrounds[0] : + color = random.choice(color_light) + else: + color = random.choice(color_dark) + + command = "magick convert " + str(list_filernd) + " -fill "+str(color)+" -font "+ \ + str(list_rfo) + " -weight 200 -pointsize 24 -distort Perspective "+str(distort_arg)+" "+"-gravity center" + " -blur 0x" + str(blur) \ ++ " -evaluate Gaussian-noise " + str(noise) + " " + " -annotate +" + x + "+" + y + " " + str(char_list[i]) + " " + char_output_dir + "output_file"+str(i)+str(j)+".jpg" + + # Uncomment line below to see what command is executed. + # print(command) + os.system(str(command)) diff --git a/CharClassification/net.py b/CharClassification/net.py new file mode 100644 index 000000000..2a44396b9 --- /dev/null +++ b/CharClassification/net.py @@ -0,0 +1,51 @@ +# import required modules +from keras.models import Sequential +from keras.layers import Convolution2D, MaxPooling2D +from keras.layers import Activation, Dropout, Flatten, Dense +import matplotlib.pyplot as plt + +class Net: + @staticmethod + def build(width, height, depth, weightsPath=None): + ''' + modified lenet structure + input: input_shape (width, height, channels) + returns: trained/loaded model + ''' + # initialize the model + model = Sequential() + + # first layer CONV => RELU => POOL + model.add(Convolution2D(32, (3, 3), input_shape = (width, height, depth))) + model.add(Activation('relu')) + model.add(MaxPooling2D(pool_size = (2, 2))) + + # second layer CONV => RELU => POOL + model.add(Convolution2D(32, (3, 3))) + model.add(Activation('relu')) + model.add(MaxPooling2D(pool_size = (2, 2))) + + # third layer of CONV => RELU => POOL + model.add(Convolution2D(64, (3, 3))) + model.add(Activation('relu')) + model.add(MaxPooling2D(pool_size = (2, 2))) + + # set of FC => RELU layers + model.add(Flatten()) + + # number of neurons in FC layer = 128 + model.add(Dense(128)) + model.add(Activation('relu')) + model.add(Dropout(0.5)) + + # as number of classes is 36 + model.add(Dense(36)) + model.add(Activation('softmax')) + + # if weightsPath is specified load the weights + if weightsPath is not None: + print('weights loaded') + model.load_weights(weightsPath) + # return model + + return model diff --git a/CharClassification/train_model.py b/CharClassification/train_model.py new file mode 100644 index 000000000..302d198a7 --- /dev/null +++ b/CharClassification/train_model.py @@ -0,0 +1,97 @@ +# import required modules +from keras.preprocessing.image import ImageDataGenerator +from keras import optimizers +import matplotlib.pyplot as plt + +# import created model +from net import Net + +# Dimensions of our images +img_width, img_height = 32, 32 + +# 3 channel image +no_of_channels = 3 + +# train data Directory +train_data_dir = 'train/' +# test data Directory +validation_data_dir = 'test/' + +epochs = 80 +batch_size = 32 + +#initialize model +model = Net.build(width = img_width, height = img_height, depth = no_of_channels) +print('building done') +# Compile model +rms = optimizers.RMSprop(lr=0.001, rho=0.9, epsilon=None, decay=0.0) +print('optimizing done') + +model.compile(loss='categorical_crossentropy', + optimizer=rms, + metrics=['accuracy']) + +print('compiling') + +# this is the augmentation configuration used for training +# horizontal_flip = False, as we need to retain Characters +train_datagen = ImageDataGenerator( + featurewise_center=True, + featurewise_std_normalization=True, + rescale=1. / 255, + shear_range=0.1, + zoom_range=0.1, + rotation_range=5, + width_shift_range=0.05, + height_shift_range=0.05, + horizontal_flip=False) + +# this is the augmentation configuration used for testing, only rescaling +test_datagen = ImageDataGenerator(featurewise_center=True, featurewise_std_normalization=True, rescale=1. / 255) + +train_generator = train_datagen.flow_from_directory( + train_data_dir, + target_size=(img_width, img_height), + batch_size=batch_size, + class_mode='categorical') + +validation_generator = test_datagen.flow_from_directory( + validation_data_dir, + target_size=(img_width, img_height), + batch_size=batch_size, + class_mode='categorical') + +# fit the model +history = model.fit_generator( + train_generator, + steps_per_epoch=train_generator.samples / batch_size, + epochs=epochs, + validation_data=validation_generator, + validation_steps=validation_generator.samples / batch_size) + +# evaluate on validation dataset +model.evaluate_generator(validation_generator) +# save weights in a file +model.save_weights('trained_weights.h5') + +print(history.history) + +# Loss Curves +plt.figure(figsize=[8,6]) +plt.plot(history.history['loss'],'r',linewidth=3.0) +plt.plot(history.history['val_loss'],'b',linewidth=3.0) +plt.legend(['Training loss', 'Validation Loss'],fontsize=18) +plt.xlabel('Epochs ',fontsize=16) +plt.ylabel('Loss',fontsize=16) +plt.title('Loss Curves',fontsize=16) + +# Accuracy Curves +plt.figure(figsize=[8,6]) +plt.plot(history.history['acc'],'r',linewidth=3.0) +plt.plot(history.history['val_acc'],'b',linewidth=3.0) + +plt.legend(['Training Accuracy', 'Validation Accuracy'],fontsize=18) +plt.xlabel('Epochs ',fontsize=16) +plt.ylabel('Accuracy',fontsize=16) +plt.title('Accuracy Curves',fontsize=16) +plt.show() \ No newline at end of file