...

How to Build a CNN for Chess Piece Image Classification

Building a CNN Model

Introduction

This end-to-end tutorial shows how to build a complete image classification pipeline in Python using TensorFlow Keras, focusing specifically on image classification with keras.
You will prepare the dataset folders, split images into train and validation sets, build and train a convolutional neural network (CNN) with augmentation and callbacks, and finally run single-image predictions with OpenCV visualization.
The example uses a chess piece dataset with six classes and demonstrates best practices like normalization, data augmentation, EarlyStopping, and saving the best model for reproducible results.

The link for the video tutorial is here : https://youtu.be/Y2MkEuZ3AEw&list=UULFTiWJJhaH6BviSWKLJUM9sg

Link for the full code : https://ko-fi.com/s/9584dd3afd

You can find more tutorials in my blog : https://eranfeit.net/blog/


Part 1 – Preparing dataset folders for training and validation

Below we create a clean directory structure for train and validation subsets, one subfolder per class.
This organization lets ImageDataGenerator.flow_from_directory read labeled images automatically.

This setup step creates a clean, mirrored directory structure for training and validation, which is crucial for image classification with Keras and smooth integration with flow_from_directory.
By defining train/ and validation/ roots and adding subfolders for Bishop, King, Knight, Pawn, Queen, and Rook, you enable automatic label discovery without manual CSVs.
That structure is a best practice in any computer vision tutorial Python workflow because it standardizes data ingestion across projects and makes experiments reproducible.

Organizing data like this directly supports cnn image classification python pipelines by making batch loading, shuffling, and augmentation predictable.
When the TensorFlow ImageDataGenerator later scans these folders, it infers class indices from directory names (sorted order), ensuring a reliable mapping between folders and labels.
Keeping class names consistent between train and validation reduces the chance of label mismatches and silently broken experiments.

Operationally, this step also prevents dataset leakage and simplifies deployments.
Because the model will be trained and validated from folders with identical sub-structures, you can swap datasets, run ablation studies, and compare build CNN with Keras architectures without changing data-loading code.
If you run on Windows paths (as shown), it’s wise to wrap os.mkdir calls with existence checks to avoid exceptions when re-running cells during iterative development.

### Import the standard library module for filesystem utilities. # prepare the data folders  import os  ### List the existing class folders to confirm the dataset layout. # show the list of folders dataDirList = os.listdir("C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess") print(dataDirList)  ### Define a base directory to hold train and validation splits. # lets create our working images folder # this will only run once baseDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset"  ### Create the top-level training directory. #train folders trainData = os.path.join(baseDir,'train') os.mkdir(trainData)  ### Create the top-level validation directory. validationData = os.path.join(baseDir,'validation') os.mkdir(validationData)  ### Create one subfolder per class under train for labeled data. trainBishopData = os.path.join(trainData,'Bishop') os.mkdir(trainBishopData) trainKingData = os.path.join(trainData,'King') os.mkdir(trainKingData) trainKnightData = os.path.join(trainData,'Knight') os.mkdir(trainKnightData) trainPawnData = os.path.join(trainData,'Pawn') os.mkdir(trainPawnData) trainQueenData = os.path.join(trainData,'Queen') os.mkdir(trainQueenData) trainRookData = os.path.join(trainData,'Rook') os.mkdir(trainRookData)  ### Create matching subfolders per class under validation. #validation folders valBishopData = os.path.join(validationData,'Bishop') os.mkdir(valBishopData) valKingData = os.path.join(validationData,'King') os.mkdir(valKingData) valKnightData = os.path.join(validationData,'Knight') os.mkdir(valKnightData) valPawnData = os.path.join(validationData,'Pawn') os.mkdir(valPawnData) valQueenData = os.path.join(validationData,'Queen') os.mkdir(valQueenData) valRookData = os.path.join(validationData,'Rook') os.mkdir(valRookData) 

Link for the full code : https://ko-fi.com/s/9584dd3afd

You now have a mirrored folder structure for train and validation with subfolders for Bishop, King, Knight, Pawn, Queen, and Rook.
This enables automatic class labeling when loading images with Keras.

Part 2 – Splitting images into train and validation sets

Next, we randomly split each class into train and validation according to a configurable ratio and copy the files into the new directories.

This script implements a repeatable python train validation split images routine with a configurable splitSize (e.g., 0.85 for training).
It filters out zero-byte files, shuffles filenames to avoid ordering bias, and copies images into the directory structure created earlier.
Randomized splitting is vital for trustworthy cnn image classification python results because it ensures that train and validation sets are independent samples from the same distribution.

The split_data function is class-agnostic and gets called for each chess piece.
That design keeps the logic DRY and makes it easy to adjust the split ratio or insert additional checks (e.g., allowed extensions) across all classes at once.
In the context of an end-to-end computer vision tutorial Python pipeline, this modularity makes it simple to port the code to other datasets with similar folder hierarchies.

For larger or imbalanced datasets, consider stratified sampling (maintaining class proportions) or seed-controlled shuffles for reproducibility.
You can also augment this step to create a third test/ set for final evaluation.
Together with the previous step, this forms a robust pre-processing layer that downstream Keras tools—like TensorFlow ImageDataGenerator—can consume without custom loaders.

### Import an unused symbol (from fileinput); harmless but not required. from fileinput import filename ### Access filesystem utilities like listing directories and sizes. import os ### Use randomness for shuffling file lists. import random ### Copy files from source to destination folders. import shutil  ### Choose the fraction of images assigned to the training set. splitSize = .85  ### Inspect class folders to verify the dataset location. # show the list of folders dataDirList = os.listdir("C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess") print(dataDirList)  ### Define a reusable function to split a single class folder into train and validation sets. # lest vuild a function that will split the data between train and validation def split_data(SOURCE , TRAINING , VALIDATION , SPLIT_SIZE):      ### Collect eligible filenames (non-empty files).     files = []      ### Iterate over source directory entries.     for filename in os.listdir(SOURCE):         file = SOURCE + filename         print(file)         ### Include only files with size > 0 to avoid corrupt entries.         if os.path.getsize(file) > 0 :             files.append(filename)         else:             print(filename + " - would ignore this file")      ### Show total number of valid files detected.     print(len(files))      ### Compute split counts for train and validation.     trainLength = int( len(files) * SPLIT_SIZE)     validLength = int (len(files) - trainLength)     ### Shuffle files to ensure a random split.     shuffledSet = random.sample(files , len(files))      ### Slice the shuffled list into train and validation subsets.     trainSet = shuffledSet[0:trainLength]     validSet = shuffledSet[trainLength:]      ### Copy the training files into the TRAINING directory.     # copy the train images :     for filename in trainSet:         thisfile = SOURCE + filename         destination = TRAINING + filename         shutil.copyfile(thisfile, destination)      ### Copy the validation files into the VALIDATION directory.     # copy the validation images :     for filename in validSet:         thisfile = SOURCE + filename         destination = VALIDATION + filename         shutil.copyfile(thisfile, destination)  ### Define absolute paths for each class (note the trailing slash). BishopSourceDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess/Bishop/" #dont forget the last "/" BishopTrainDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train/Bishop/" #dont forget the last "/" BishopValDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/validation/Bishop/" #dont forget the last "/"  KingSourceDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess/King/" #dont forget the last "/" KingTrainDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train/King/" #dont forget the last "/" KingValDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/validation/King/" #dont forget the last "/"  KnightSourceDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess/Knight/" #dont forget the last "/" KnightTrainDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train/Knight/" #dont forget the last "/" KnightValDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/validation/Knight/" #dont forget the last "/"  PawnSourceDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess/Pawn/" #dont forget the last "/" PawnTrainDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train/Pawn/" #dont forget the last "/" PawnValDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/validation/Pawn/" #dont forget the last "/"  QueenSourceDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess/Queen/" #dont forget the last "/" QueenTrainDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train/Queen/" #dont forget the last "/" QueenValDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/validation/Queen/" #dont forget the last "/"  RookSourceDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Chess/Rook/" #dont forget the last "/" RookTrainDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train/Rook/" #dont forget the last "/" RookValDir = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/validation/Rook/" #dont forget the last "/"  ### Perform the split for each class using the same ratio. split_data(BishopSourceDir,BishopTrainDir,BishopValDir,splitSize) split_data(KingSourceDir,KingTrainDir,KingValDir,splitSize) split_data(KnightSourceDir,KnightTrainDir,KnightValDir,splitSize) split_data(PawnSourceDir,PawnTrainDir,PawnValDir,splitSize) split_data(QueenSourceDir,QueenTrainDir,QueenValDir,splitSize) split_data(RookSourceDir,RookTrainDir,RookValDir,splitSize) 

Link for the full code : https://ko-fi.com/s/9584dd3afd

Part 3 – Building and training a CNN with TensorFlow Keras

We define input size and batch size, set up ImageDataGenerator with normalization and augmentation, construct a multi-block CNN, compile with Adam, train with validation, save the best model, and plot accuracy and loss.

This code defines the data pipeline and the model architecture for image classification with Keras, leveraging ImageDataGenerator to normalize and augment images.
Augmentations—rotation, zoom, horizontal flip, and shear—expand the effective dataset, helping the model generalize better and mitigating overfitting.
This is the heart of image augmentation in Keras, and it’s a major performance lever for small to medium datasets like chess pieces.

The Sequential CNN stacks multiple Conv2D + MaxPooling2D blocks to learn increasingly abstract features, followed by fully connected layers and a softmax head for multi-class output.
Using categorical_crossentropy and Adam aligns with common best practices in cnn image classification python projects, while the batch size and input shape are tuned for stability and throughput.
Counting classes with glob('train/*') keeps the code flexible for reuse across different datasets without manual label mapping.

Training stability comes from Keras ModelCheckpoint EarlyStopping patterns: monitoring validation metrics, saving the best model, and halting when progress stalls.
The plots of accuracy and loss visualize learning dynamics so you can diagnose under/overfitting and decide whether to increase capacity, tweak augmentation, or adjust the learning rate.
All of this integrates seamlessly with the TensorFlow ImageDataGenerator flow, creating a repeatable, production-ready training loop.

### Import Keras utility to load images from directories and augment them. # Create the CNN model from tensorflow.keras.preprocessing.image import ImageDataGenerator ### Import core CNN layers and dense layers for classification. from tensorflow.keras.layers import Conv2D , MaxPooling2D , Flatten, Dense, Dropout ### Import the Sequential API to stack layers linearly. from tensorflow.keras.models import Sequential ### Import the Adam optimizer class (string alias also works). from tensorflow.keras.optimizers import Adam ### Import callbacks for training control and checkpointing. from tensorflow.keras.callbacks import EarlyStopping , ModelCheckpoint ### Import plotting to visualize training metrics after fit. import matplotlib.pyplot as plt ### Import glob to count class subfolders and infer number of classes. from glob import glob  ### Set target input width in pixels. imgWidth = 256 ### Set target input height in pixels. imgHeight = 256 ### Set images processed per batch. batchSize = 32 ### Set the maximum number of training epochs. numOfEpochs = 100  ### Path to training directory that contains one subfolder per class. TRAINING_DIR = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train"  ### Determine number of classes by counting subdirectories under train. NumOfClasses = len(glob('C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/train/*')) # dont forget the '  /*  ' ### Print the class count (should be 6 for chess pieces). print (NumOfClasses) # 6 classes  ### Configure data augmentation and rescaling to 0-1 for robust training. # data augmentation to increase the train data train_datagen = ImageDataGenerator(rescale = 1/255.0, #normalize between 0 - 1                                     rotation_range = 30 ,                                     zoom_range = 0.4 ,                                     horizontal_flip=True,                                     shear_range=0.4)  ### Create a generator that yields augmented training batches and labels. train_generator = train_datagen.flow_from_directory(TRAINING_DIR,                                                     batch_size = batchSize,                                                     class_mode = 'categorical',                                                     target_size = (imgHeight,imgWidth))  ### Path to validation directory for on-the-fly normalization only. validation_DIR = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/validation" ### Rescale validation images without augmentation to measure true generalization. val_datagen = ImageDataGenerator(rescale = 1/255.0)  ### Create a generator for validation batches and labels. val_generator = val_datagen.flow_from_directory(validation_DIR,                                                 batch_size = batchSize,                                                 class_mode='categorical',                                                 target_size = (imgHeight, imgWidth))  ### Define EarlyStopping to halt if validation loss stops improving. # early stopping callBack = EarlyStopping(monitor='val_loss', patience=5, verbose=1, mode='auto')  ### Choose a path to save the best-performing model file by validation accuracy. # if we will find a better model we will save it here : bestModelFileName = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/chess_best_model.h5"  ### Create a ModelCheckpoint to keep only the best model weights. bestModel = ModelCheckpoint(bestModelFileName, monitor='val_accuracy', verbose=1, save_best_only=True)  ### Define a sequential CNN architecture with multiple Conv+Pool blocks. # the model : model = Sequential([      ### First convolutional block with input shape declaration.     Conv2D(32, (3,3) , activation='relu' , input_shape=(imgHeight, imgWidth, 3) ) ,     ### Downsample feature maps to reduce spatial size.     MaxPooling2D(2,2),          ### Second convolutional block to increase capacity.     Conv2D(64 , (3,3) , activation='relu'),     ### Pooling to control overfitting and computation.     MaxPooling2D(2,2),      ### Third convolutional block.     Conv2D(64 , (3,3) , activation='relu'),     ### Pooling again to reduce spatial dimensions.     MaxPooling2D(2,2),      ### Fourth convolutional block with more filters for richer features.     Conv2D(128 , (3,3) , activation='relu'),     ### Pooling for downsampling.     MaxPooling2D(2,2),      ### Fifth convolutional block for high-level features.     Conv2D(256 , (3,3) , activation='relu'),     ### Pool to summarize features.     MaxPooling2D(2,2),      ### Flatten spatial maps to a vector for dense layers.     Flatten(),      ### Dense layer for learned combinations of features.     Dense(512 , activation='relu'),     ### Additional dense layer to increase representational power.     Dense(512 , activation='relu'),      ### Final softmax layer for multi-class probabilities.     Dense(NumOfClasses , activation='softmax') # softmax -> 0 to 1  ])  ### Print a model summary for verification of layers and params. print (model.summary() )  ### Compile with Adam optimizer and categorical cross-entropy for multi-class labels. # compile the model with Adam optimizer model.compile(optimizer='Adam', loss='categorical_crossentropy', metrics=['accuracy'])  ### Train the model with validation monitoring and checkpointing. history = model.fit(train_generator,                     epochs = numOfEpochs,                     verbose=1,                     validation_data = val_generator,                     callbacks = [bestModel])  ### Extract accuracy and loss from the training history for plotting. # display the result using pyplot acc = history.history['accuracy'] val_acc = history.history['val_accuracy'] loss = history.history['loss'] val_loss = history.history['val_loss']  ### Prepare an array of epoch indices for plotting curves. epochs = range(len(acc)) # for the max value in the diagram  ### Plot training vs validation accuracy. # accuracy chart fig = plt.figure(figsize=(14,7)) plt.plot(epochs, acc , 'r', label="Train accuracy") plt.plot(epochs, val_acc , 'b', label="Validation accuracy") plt.xlabel('Epochs') plt.ylabel('Accuracy') plt.title('Train and validation accuracy') plt.legend(loc='lower right') plt.show()  ### Plot training vs validation loss. #loss chart fig2 = plt.figure(figsize=(14,7)) plt.plot(epochs, loss , 'r', label="Train loss") plt.plot(epochs, val_loss , 'b', label="Validation loss") plt.xlabel('Epochs') plt.ylabel('Loss') plt.title('Train and validation Loss') plt.legend(loc='upper right') plt.show() 

Link for the full code : https://ko-fi.com/s/9584dd3afd

You trained a CNN with augmentation, tracked validation performance, and saved the best model automatically.
The accuracy and loss plots help diagnose underfitting or overfitting.

Part 4 – Loading the best model and running single-image prediction

Finally, we load the saved best model, preprocess a test image, predict its class, and visualize the label on the image with OpenCV.

Inference loads the best-performing weights and applies the exact same preprocessing used during training—resizing and scaling to [0, 1].
That function, prepareImage, enforces input shape and normalization, which is essential for consistent predict image with Keras behavior.
By keeping imgWidth and imgHeight consistent with training, the model receives data in the format it expects.

The predicted class is obtained by np.argmax over model probabilities, and then mapped to a human-readable label through the classes list.
Ensuring classes matches the alphabetical order of the training directories is key: it guarantees that numeric indices align with semantic labels.
This practice is foundational in any computer vision tutorial Python pipeline where label indices are derived from sorted folder names.

Finally, the OpenCV overlay makes results interpretable by putting the predicted label directly on the image, a handy touch for demos and debugging.
This turns raw probabilities into an end-to-end image classification with Keras experience—from dataset to on-screen prediction.
With the trained model persisted via checkpointing, you can batch-infer new images, integrate into apps, or wrap the pipeline in an API for real-world chess piece recognition.

### Import Keras utility to load a previously saved model. from keras.models import load_model  ### Maintain the same target image dimensions used in training. imgWidth = 256 imgHeight = 256  ### Define class names in the exact order used during training (sorted directories). # the names of the classes should be sorted  classes = ["Bishop","King","Knight", "Pawn","Queen","Rook"]  ### Import data tools for arrays and image loading. import pandas as pd import numpy as np from keras.preprocessing.image import load_img , img_to_array ### Import OpenCV for image drawing and display. import cv2  ### Load the best-performing model from checkpoint. #lets load the model  model = load_model("C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/chess_best_model.h5")  ### Optionally print the model summary to verify layers. #print(model.summary() )  ### Define a helper function to load and normalize an image for inference. # lets build a function for preparing an image for model def prepareImage(pathToImage) :     ### Load and resize the image to the model's input size.     image = load_img(pathToImage , target_size=(imgHeight, imgWidth))     ### Convert the PIL image to a NumPy array.     imgResult = img_to_array(image)     ### Add a batch axis to match model input shape (1, H, W, 3).     imgResult = np.expand_dims(imgResult , axis=0 )     ### Normalize pixel values to 0-1 as during training.     imgResult = imgResult / 255.     ### Return the preprocessed tensor.     return imgResult  ### Choose a test image path for prediction (update to your file). #testImagePath = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Test/knight.jpeg" testImagePath = "C:/Python-cannot-upload-to-GitHub/Chessman-image-dataset/Test/rook.jpg"  ### Preprocess the image using the helper function. # run the function imageForModel = prepareImage(testImagePath)  ### Print the input batch shape for sanity checking. print(imageForModel.shape)  ### Run model inference to get class probabilities. #predict the image resultArray = model.predict(imageForModel , batch_size=32 , verbose=1) ### Convert probabilities to the winning class index. answer = np.argmax(resultArray , axis=1 )  ### Print the numeric predicted class. print(answer[0])  ### Map the index to a human-readable label. text = classes[answer[0]] print ('Predicted : '+ text)  ### Read the original image with OpenCV for annotation. #lets show the image with the predicted text  img = cv2.imread (testImagePath) ### Choose a font for overlay text. font = cv2.FONT_HERSHEY_COMPLEX  ### Draw the predicted label on the image. cv2.putText(img , text , (0,100) , font , 2 , (209,19,77 ) , 3 ) ### Display the annotated image in a window. cv2.imshow('img', img) ### Wait for a key press to close the window. cv2.waitKey(0) 

Link for the full code : https://ko-fi.com/s/9584dd3afd

You loaded the saved Keras model, prepared a test image consistently with training, predicted the class, and displayed the label overlay.
This completes the pipeline from data preparation to inference.


Connect :

☕ Buy me a coffee — https://ko-fi.com/eranfeit

🖥️ Email : feitgemel@gmail.com

🌐 https://eranfeit.net

🤝 Fiverr : https://www.fiverr.com/s/mB3Pbb

Enjoy,

Eran

error: Content is protected !!
Eran Feit