Zahlen erkennen mit Python

In diesem Artikel möchte ich zeigen, wie man Zahlen mit Python erkennen kann. Das Touchpad vom Laptop benutze ich, um Zahlen zu zeichnen. Dafür verwende ich keras und Tensorflow und einen eigenen Datensatz zum Trainieren und Validieren.

Datensatz erstellen

Für den Datensatz verwende ich folgenden Python Code. Die Ordnerstruktur muss vorher angelegt sein:

import cv2 as cv
import pyautogui
import numpy as np
import keyboard
import string
import random
from time import sleep


def find_minmax(pic):
    # find the min,max of x,y
    colored = np.where(pic != [0,0,0])
    freespace = 10
    min_y = np.amin(colored[0]) - freespace
    max_y = np.amax(colored[0]) + freespace
    min_x = np.amin(colored[1]) - freespace
    max_x = np.amax(colored[1]) + freespace
    
    return min_y,max_y,min_x,max_x

def main():
    screenWidth, screenHeight = pyautogui.size()
    prevmouseX = prevmouseY = 0
    dim=(28,28)

    for n in range(0,10,1):
        for d in range(0,1,1):
            print(f'Zahl: {n} \nAnzahl: {d}')
            # create np array with a size of max windows resolution
            y = np.zeros([screenHeight,screenWidth,3],dtype=np.uint8)
            # make array image black
            pic = np.full_like(y,[0,0,0])
            while not keyboard.is_pressed('esc'):
                # get mouse position
                mouseX, mouseY = pyautogui.position()
                #print(mouseX,mouseY)
                # make the drawing lines bigger
                for i in range(-5,5):
                    pic[mouseY ,mouseX + i] = [255,255,255]
                    pic[mouseY + i, mouseX] = [255,255,255]
                      
            sleep(0.1)
            # find min and max of each axis(x,y)
            min_y,max_y,min_x,max_x = find_minmax(pic)
            picresized = pic[min_y:max_y,min_x:max_x]
            picresized = cv.resize(picresized,dim)
            randomstring = ''.join(random.choices(string.ascii_lowercase,k=8))
            #print(f"create file {n}/{d}{randomstring}.png")
            #cv.imwrite(f'./{n}/{d}{randomstring}.png',picresized)
            print(f"create file Test/{d}{randomstring}.png")
            cv.imwrite(f'./Test/{d}{randomstring}.png',picresized)
        


if __name__ == '__main__':
    main()

Für jede einzelne Ziffer gibt es mindestens 50 Datensätze, insgesamt habe ich 554 Bilder erstellt. Dabei könnte man die Prozedur in 3 Teile gliedern. Die erste Prozedur besteht darin die Zahl zu zeichnen. Dabei entsteht ein Bild von der Größe des Displays, bei mir 1920×1080. Dieses Bild wird dann reduziert auf den Zahlenbereich, bevor es dann auf eine Größe von 28×28 reduziert wird. Damit hätten wir einen vollständigen Datensatz, mit dem wir unser Model trainieren können

Das Model erstellen und trainieren

Für die Erstellung des Models verwende ich Tensorflow und Keras.Sequential. Dabei bin ich nach dem Tutorial von tensorflow.org vorgegangen.

import numpy as np
import matplotlib.pyplot as plt
import PIL
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential

import pathlib

def visualize_training_results(history,epochs):
    acc = history.history['accuracy']
    val_acc = history.history['val_accuracy']

    loss = history.history['loss']
    val_loss = history.history['val_loss']

    epochs_range = range(epochs)

    plt.figure(figsize=(8, 8))
    plt.subplot(1, 2, 1)
    plt.plot(epochs_range, acc, label='Training Accuracy')
    plt.plot(epochs_range, val_acc, label='Validation Accuracy')
    plt.legend(loc='lower right')
    plt.title('Training and Validation Accuracy')

    plt.subplot(1, 2, 2)
    plt.plot(epochs_range, loss, label='Training Loss')
    plt.plot(epochs_range, val_loss, label='Validation Loss')
    plt.legend(loc='upper right')
    plt.title('Training and Validation Loss')
    plt.show()

data_dir = pathlib.Path('C:/Users/Willkommen/Desktop/handdigits/Modelcreation')

image_count = len(list(data_dir.glob('*/*.png')))
print(image_count)

batch_size = 32
img_height = 28
img_width = 28

# create dataset
train_ds = tf.keras.utils.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset='training',
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size
)

val_ds = tf.keras.utils.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset='validation',
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size
)

class_names = train_ds.class_names
print(class_names)

# Configure dataset for performance
AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().shuffle(20).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

# Standardize the data
normalization_layer = layers.Rescaling(1./255)

normalized_ds = train_ds.map(lambda x, y: (normalization_layer(x), y))
image_batch, labels_batch = next(iter(normalized_ds))
first_image =  image_batch[0]
print(np.min(first_image), np.max(first_image))

num_classes = len(class_names)

# Create the model
model = Sequential([
    layers.Rescaling(1./255, input_shape=(img_height, img_width, 3)),
    layers.Conv2D(16,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Conv2D(32,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Conv2D(64,3, padding='same', activation='relu'),
    layers.MaxPooling2D(),
    layers.Flatten(),
    layers.Dense(128, activation='relu'),
    layers.Dense(num_classes)
])
# compile model
model.compile(
    optimizer='adam',
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=['accuracy']
)

model.summary()

## Train the model

epochs = 10
history = model.fit(
    train_ds,
    validation_data = val_ds,
    epochs=epochs
)

## Visualize Training results
visualize_training_results(history, epochs)

#Predict on new data
data_dir = pathlib.Path('C:/Users/Willkommen/Desktop/handdigits/Test/0nzzlfidh.png')
img = tf.keras.utils.load_img(
    data_dir, target_size=(img_height, img_width)
)

img_array = tf.keras.utils.img_to_array(img),
img_array = tf.expand_dims(img_array[0], 0) # Create a batch
predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])

print(
    "This number most likely belongs to {} with a {:.2f} percent confidence."
    .format(class_names[np.argmax(score)], 100 * np.max(score))
)

# save model
model.save('C:/Users/Willkommen/Desktop/handdigits/ownmodel.h5')

Wir laden die Daten und splitten die Datensätze im Verhältnis 80% zu 20% in Trainings- und Validierungsdaten. Der Klassenname bezieht sich auf den Ordnername, also die Zahlen von 0 – 9. Die Datensätze ziehen wir uns von der Festplatte in den Memory Cache. Dies hat den Vorteil das etwaige I/O auf der Platte nicht zu einem Flaschenhals führen, wenn wir das Model trainieren.
Da der RGB Channel Werte zwischen 0 – 255 hat, normalisieren wir diese noch, sodass ein Wert zwischen 0 und 1 besteht.
Nach der Normalisierung erstellen wir unser Keras Sequential Model mit drei convolution Blöcken. Jeder hat zusätzlich ein max pooling layer.
Zum Kompilieren benutzen wir den Adamoptimizer und die SparseCategoricalCrossentropy loss Funktion.
In insgesamt 10 Epochen trainieren wir unser Model. Zur Visualisierung des Trainingsergebnisses mittels matplotlib gibt es die Methode visualize_training_results.
Um unser gerade neu erstelltes Model zu testen, benutzen wir das Model auf neu erstellte Testdaten.

Nachfolgend könnt ihr die Validierung vom Training sehen. Das Model scheint dabei gut zu performen.

Modelvalidierung

Mit Touchpad zeichnen

Das Model für die Zahlenidentifizierung haben wir nun und wir könnten die Zahlen mit Python erkennen. Fehlt lediglich das Zeichnen mit dem Touchpad. Dafür verwende ich die Bibliothek “keyboard”. Diese ermittelt die aktuelle Mausposition und liefert X,Y Koordinaten. Jene X,Y Koordinaten ändern die Werte im Numpy Array und haben statt dem Wert [0,0,0] den Wert [255,255,255]. Wie auch beim Trainieren des Models, entspricht das erste Bild der Gesamtgröße des Bildschirms. Das zweite ist nur der Zahlenbereich und das dritte und finale Bild, der Zahlenbereich auf die Größe 28,28 reduziert.
Nachdem wir nun das 28×28 Bild-Array haben, rufen wir die Methode predict_num(img) auf. In der Methode wird das Bild noch einmal auf die korrekte Größe gebracht, ehe es dann durch das Model bewertet wird.

import cv2 as cv
import keyboard
import pyautogui
import numpy as np
import tensorflow as tf
from keras.models import load_model

model = load_model('ownmodel.h5')

def predict_num(img):
    # reshape for model compatibility
    img = img.reshape(1,28,28,3)
    #predicting the class
    img = tf.expand_dims(img[0], 0) # Create a batch
    predictions = model.predict(img)
    score = tf.nn.softmax(predictions[0])

    print(
    "You draw number {} with a {:.2f} percent confidence."
    .format(np.argmax(score), 100 * np.max(score))
    )


def find_minmax(pic):
    # find the min,max of x,y
    colored = np.where(pic != [0,0,0])
    freespace = 10
    min_y = np.amin(colored[0]) - freespace
    max_y = np.amax(colored[0]) + freespace
    min_x = np.amin(colored[1]) - freespace
    max_x = np.amax(colored[1]) + freespace
    
    return min_y,max_y,min_x,max_x
 
def main():
    screenWidth, screenHeight = pyautogui.size()
    prevmouseX = prevmouseY = 0
    firstrun = True
    dim=(28,28)

    # create np array with a size of max windows resolution
    y = np.zeros([screenHeight,screenWidth,3],dtype=np.uint8)
    # make array image black
    pic = np.full_like(y,[0,0,0])
    
    while not keyboard.is_pressed('esc'):
        if firstrun:
            print("Draw your number now and confirm with ESC")
            firstrun=False

        # get mouse position
        mouseX, mouseY = pyautogui.position()
        # make the drawing lines bigger
        for i in range(-5,5):
            pic[mouseY ,mouseX + i] = [255,255,255]
            pic[mouseY + i, mouseX] = [255,255,255]

    cv.imwrite('./original.png',pic)

    # find number and reduce picturesize to a minimum
    min_y,max_y,min_x,max_x = find_minmax(pic)
    picresized = pic[min_y:max_y,min_x:max_x]
    cv.imwrite('./originalcut.png',picresized)

    # resize image to (28,28)
    picresized = cv.resize(picresized,dim)
    cv.imwrite('./resized.png',picresized)

    #predict
    predict_num(picresized)

if __name__ == '__main__':
    main()

Testbeispiel

Hier findet ihr ein Beispiel, bei dem ich die 3 über das Touchpad gezeichnet habe. Folgend findet ihr das original Bild, nur den Zahlenbereich und den Zahlenbereich auf 28×28 herunterskaliert. Das Model hat dabei folgendes ausgegeben:

Draw your number now and confirm with ESC
1/1 [==============================] - 0s 417ms/step
You draw number 3 with a 99.95 percent confidence.

Und hier jetzt einmal die Bilder

Den ganzen vollständigen Code findet ihr bei mir auf github: https://github.com/stevieWoW/neuronalnetworkmousepadnumber

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert