TensorFlow Image Classification with Keras: Flower Recognition, Data Augmentation, and OpenCV Prediction
This tutorial demonstrates end-to-end image classification with TensorFlow and Keras, focusing on a real dataset of flowers.
You’ll prepare data with ImageDataGenerator, train two CNN variants (a deeper 224×224 model and a lightweight 64×64 baseline), visualize accuracy and loss with Matplotlib, and finally run interactive predictions on new images using OpenCV.
The workflow mirrors a practical production approach: clean directory-based loading, on-the-fly data augmentation, clear training curves for diagnostics, and saved models for reproducible inference.
By the end, you’ll have a complete pipeline you can adapt to any folder-organized dataset and extend with transfer learning or deployment.
The link for the video tutorial is here : https://youtu.be/AamKeCTRSKM&list=UULFTiWJJhaH6BviSWKLJUM9sg
You can find the full code here : https://ko-fi.com/s/0e7d3ab454
Code for Image Classification ( Tensorflow Image Classification ) :
Code for the dataset : https://www.kaggle.com/alxmamaev/flowers-recognition
Installation :
# Requirements : Nvidia GPU card & and Cuda tool kit install # I am using this card : https://amzn.to/3mTa7HX # Working Anaconda enviroment conda create -n Flowers python=3.7 conda activate Flowers pip install tensorflow pip install tensorflow-gpu pip install numpy pip install matplotlib pip install opencv-python pip install sklearn pip install pandas pip install imutils # dataset https://www.kaggle.com/alxmamaev/flowers-recognition
You can find the full code here : https://ko-fi.com/s/0e7d3ab454
Part 1 : Data Preparation and CNN Model (224×224)
Short Introduction
We begin with a 224×224 CNN that uses data augmentation to improve generalization. The model stacks several Conv2D blocks with dropout, compiles with RMSprop and categorical_crossentropy, trains for 20 epochs, plots training/validation curves, and saves a reusable .h5
model.
Data loading is handled by Keras’ flow_from_directory
, which expects a folder structure where each subfolder is a class. This format is simple, reproducible, and ideal for quick experiments. We rescale pixel values and apply common augmentation (shear, zoom, flips) for stronger generalization—core to image classification with Keras.
The CNN backbone uses progressively deeper convolutional layers, pooling to reduce spatial dimensions, and Dropout for regularization. This is a classic keras cnn architecture suitable for medium-sized datasets. The final Dense layers map learned features to the five flower classes with a softmax
output.
We compile with rmsprop
and categorical_crossentropy
, a standard pairing for multi-class classification when labels are one-hot encoded. Metrics include accuracy, and we collect training history to visualize trends with Matplotlib—critical for diagnosing overfitting, underfitting, or learning-rate issues.
Finally, we save the model to disk. Saving enables you to separate training from inference and is essential when you later perform tensorflow predict image class steps in a different script (as in Part 3). This end-to-end cycle represents scalable tensorflow image classification you can reuse or upgrade with transfer learning.
### Import NumPy for numerical operations import numpy as np ### Import TensorFlow/Keras high-level API import tensorflow as tf ### Import ImageDataGenerator for directory-based loading and augmentation from keras.preprocessing.image import ImageDataGenerator ### Import Matplotlib for plotting training curves import matplotlib.pyplot as plt ### Define dataset directories for training/validation/testing TRAIN_DIR = "C:/Python-cannot-upload-to-GitHub/flowers/Train" TEST_DIR = "C:/Python-cannot-upload-to-GitHub/flowers/Test" VAL_DIR = "C:/Python-cannot-upload-to-GitHub/flowers/Validate" ### Set up training data generator with rescaling and augmentation train_datagen = ImageDataGenerator( rescale = 1. / 255, shear_range = 0.2, zoom_range=0.2 , horizontal_flip=True) ### Create augmented training batches from directory at 224×224 train_set = train_datagen.flow_from_directory(TRAIN_DIR, target_size=(224,224), batch_size=32 , class_mode='categorical') ### Validation generator: only rescale (no augmentation to keep validation clean) val_datagen = ImageDataGenerator(rescale = 1. / 255) ### Create validation batches from directory at 224×224 val_set = val_datagen.flow_from_directory(VAL_DIR, target_size=(224,224), batch_size=32 , class_mode='categorical') ### Build a sequential CNN model model = tf.keras.models.Sequential() ### First convolutional block with 32 filters and ReLU activation model.add(tf.keras.layers.Conv2D(filters=32 , kernel_size = (5,5) , padding='Same', activation='relu', input_shape=[224,224,3]) ) ### Downsample with max pooling model.add(tf.keras.layers.MaxPool2D(pool_size=(2,2), strides=(2,2) )) ### Second convolutional block with 64 filters model.add(tf.keras.layers.Conv2D(filters=64 , kernel_size = (5,5) , padding='Same', activation='relu' )) ### Pooling to reduce spatial dimensions model.add(tf.keras.layers.MaxPool2D(pool_size=(2,2), strides=(2,2) )) ### Dropout to mitigate overfitting model.add(tf.keras.layers.Dropout(0.5)) ### Third convolutional block with 96 filters model.add(tf.keras.layers.Conv2D(filters=96 , kernel_size = (5,5) , padding='Same', activation='relu' )) ### Pooling again model.add(tf.keras.layers.MaxPool2D(pool_size=(2,2), strides=(2,2) )) ### Dropout for regularization model.add(tf.keras.layers.Dropout(0.5)) ### Fourth convolutional block (96 filters) model.add(tf.keras.layers.Conv2D(filters=96 , kernel_size = (5,5) , padding='Same', activation='relu' )) ### Pooling to shrink feature maps model.add(tf.keras.layers.MaxPool2D(pool_size=(2,2), strides=(2,2) )) ### Dropout to reduce co-adaptation model.add(tf.keras.layers.Dropout(0.5)) ### Flatten 3D feature maps to 1D vector before Dense layers model.add(tf.keras.layers.Flatten()) ### Dense layer to learn non-linear combinations of features model.add(tf.keras.layers.Dense (units=512 , activation='relu')) ### Output layer with 5 classes and softmax probabilities model.add(tf.keras.layers.Dense(units=5 , activation='softmax')) ### Print model summary for architecture inspection print( model.summary()) ### Compile model with RMSprop optimizer and categorical crossentropy loss model.compile(optimizer='rmsprop' , loss='categorical_crossentropy' , metrics=['accuracy'] ) ### Train the model for 20 epochs with validation history = model.fit (x=train_set, validation_data=val_set, batch_size=32 , epochs=20) ### Extract accuracy and validation accuracy for plotting acc = history.history['accuracy'] val_acc = history.history['val_accuracy'] ### Extract training and validation loss for plotting loss = history.history['loss'] val_loss = history.history['val_loss'] ### Inspect arrays (optional debug) print(acc) print(val_acc) ### Define an epochs range helper for plotting epochs_range = range(20) # creating a sequence of number from 0 to 20 ### Create a new figure for plots plt.figure(figsize=(8,8)) ### Subplot 1: training vs validation accuracy 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') ### Subplot 2: training vs validation loss 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') ### Render plots to screen plt.show() ### Save trained model to disk for later inference model.save('C:/Python-cannot-upload-to-GitHub/flowers/flowers.h5') ### (Optional) Stop execution and re-run later #lets stop and run again
You can find the full code here : https://ko-fi.com/s/0e7d3ab454
Part 2- Lightweight CNN Baseline (64×64)
Short Introduction
This version targets speed, low memory, and rapid iteration.
It reduces input size to 64×64, uses a smaller CNN (2 conv blocks, 3×3 kernels) and a lighter Dense head (128 units), while keeping the same data augmentation and training setup for fair comparison with Code 1.
The goal is to establish a quick baseline, benchmark training curves, and validate the pipeline on hardware-constrained machines before scaling up.
Main changes (vs Code 1):
- Input resolution: 64×64 instead of 224×224.
- Architecture: 2 conv blocks (64→64, 3×3) instead of 4 deeper 5×5 blocks.
- Classifier head: Dense(128) instead of Dense(512).
- Training length: 30 epochs (compared to 20) to offset lower capacity.
- Same augmentation/optimizer/loss for apples-to-apples evaluation.
Smaller input sizes accelerate experimentation. By using 64×64 images and a compact network, you can iterate hyperparameters more rapidly, test augmentation effects, and establish a baseline accuracy. This baseline helps quantify the benefit of larger inputs or deeper models.
Despite its size, this CNN leverages the same ImageDataGenerator pattern. Maintaining identical augmentation and preprocessing enables fair comparisons. For many practical tasks, such a compact model can be sufficient—especially when latency or resource constraints matter.
We again use rmsprop
and categorical_crossentropy
to maintain consistency across experiments. With consistent loss/optimizer, performance differences highlight architecture or resolution effects rather than optimization differences.
We train for 30 epochs, then visualize training/validation curves. If validation accuracy plateaus early or diverges from training accuracy, consider early stopping, stronger regularization, or more augmentation. This section lets you train cnn from scratch quickly and compare outcomes.
### Import standard dependencies import numpy as np import tensorflow as tf from keras.preprocessing.image import ImageDataGenerator import matplotlib.pyplot as plt ### Define dataset directories (same structure as before) TRAIN_DIR = "C:/Python-cannot-upload-to-GitHub/flowers/Train" TEST_DIR = "C:/Python-cannot-upload-to-GitHub/flowers/Test" VAL_DIR = "C:/Python-cannot-upload-to-GitHub/flowers/Validate" ### Training data generator with augmentation and rescaling train_datagen = ImageDataGenerator( rescale = 1. / 255, shear_range = 0.2, zoom_range=0.2 , horizontal_flip=True) ### Create 64×64 training batches train_set = train_datagen.flow_from_directory(TRAIN_DIR, target_size=(64,64), batch_size=32 , class_mode='categorical') ### Validation generator: rescale only val_datagen = ImageDataGenerator(rescale = 1. / 255) ### Create 64×64 validation batches val_set = val_datagen.flow_from_directory(VAL_DIR, target_size=(64,64), batch_size=32 , class_mode='categorical') ### Define a compact sequential CNN model = tf.keras.models.Sequential() ### First conv block with 64 filters at 3×3 kernel model.add(tf.keras.layers.Conv2D(filters=64 , kernel_size = (3,3) , padding='Same', activation='relu', input_shape=[64,64,3]) ) ### Pool to reduce spatial size model.add(tf.keras.layers.MaxPool2D(pool_size=(2,2), strides=(2,2) )) ### Second conv block with 64 filters model.add(tf.keras.layers.Conv2D(filters=64 , kernel_size = (3,3) , padding='Same', activation='relu' )) ### Pooling again model.add(tf.keras.layers.MaxPool2D(pool_size=(2,2), strides=(2,2) )) ### Dropout to prevent overfitting model.add(tf.keras.layers.Dropout(0.5)) ### Flatten feature maps model.add(tf.keras.layers.Flatten()) ### Dense layer with 128 units model.add(tf.keras.layers.Dense (units=128 , activation='relu')) ### Output layer for 5 classes with softmax model.add(tf.keras.layers.Dense(units=5 , activation='softmax')) ### Review architecture print( model.summary()) ### Compile with RMSprop and categorical crossentropy for multi-class model.compile(optimizer='rmsprop' , loss='categorical_crossentropy' , metrics=['accuracy'] ) ### Train for 30 epochs on 64×64 inputs history = model.fit (x=train_set, validation_data=val_set, batch_size=32 , epochs=30) ### Capture history for plots acc = history.history['accuracy'] val_acc = history.history['val_accuracy'] loss = history.history['loss'] val_loss = history.history['val_loss'] ### Optional: inspect arrays print(acc) print(val_acc) ### Build epochs range helper for plotting epochs_range = range(30) # creating a sequence of number from 0 to 20 ### Create figure for two subplots plt.figure(figsize=(8,8)) ### Accuracy subplot 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') ### Loss subplot 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') ### Display plots plt.show() ### Save baseline model variant model.save('C:/Python-cannot-upload-to-GitHub/flowers/flowersOption2.h5')
You can find the full code here : https://ko-fi.com/s/0e7d3ab454
Part 3 – Test the model
Test images :
Short Introduction
With the trained 224×224 model saved to disk, we’ll predict new images interactively. This script lets you display images from a folder, choose one by pressing Enter, run model.predict
, and overlay the predicted class name onto the image using OpenCV’s putText
.
First, we define the label order and load the trained Keras model with tf.keras.models.load_model
. This aligns the inference pipeline with training. Ensuring the list of categories is sorted and consistent with the training generator is critical for correct label mapping.
We use OpenCV to preview candidate images, looping until you press Enter to select one. This simple UI removes guesswork and mimics a lightweight annotation or demo setup that’s common in quick validation workflows.
For inference, we preprocess the image exactly like training: resize to 224×224, convert to array, and expand dimensions to create a batch. The softmax
output is parsed via np.argmax
to get the predicted class index, then mapped to a human-readable label.
Finally, we overlay the label on the original image via cv2.putText
and save a result for documentation. This closes the loop from train cnn from scratch → tensorflow predict image class → visual verification with OpenCV—a practical end-to-end pattern.
### Import numerical and ML deps import numpy as np import tensorflow as tf from keras.preprocessing.image import ImageDataGenerator from keras.preprocessing import image ### Import OpenCV for image display and drawing text import cv2 ### OS utilities for paths import os ### Glob to list files by pattern import glob ### Define interactive workflow: show images, pick one, predict with trained model ### List of target categories (sorted to match training label ordering) flower_categories = ['daisy', 'dandelion' , 'rose', 'sunflower' , 'tulip'] ### Load the saved 224×224 model from Part 1 model = tf.keras.models.load_model('C:/Python-cannot-upload-to-GitHub/flowers/flowers.h5') ### Directory of candidate images for inference img_dir = "C:/GitHub/TensorFlowProjects/Flowers Recognition/FromGoogle" ### Build a glob pattern to fetch all files inside folder data_path = os.path.join(img_dir,'*') ### Retrieve file paths files = glob.glob(data_path) ### Counter (optional) num = 0 ### Loop over files, display each image, wait for a key for f1 in files: num = num + 1 ### Read image with OpenCV (BGR) img = cv2.imread(f1) ### Show the image window cv2.imshow('img', img) ### Wait for a keypress; 0 = indefinitely key = cv2.waitKey(0) ### If Enter is pressed (ASCII 13), select this image and break if key == 13: break ### Print chosen image path print(f1) ### Load the selected image and resize to 224×224 (match training) test_image = image.load_img(f1, target_size=(224,224)) ### Convert PIL image to NumPy array test_image = image.img_to_array(test_image) ### Add batch dimension: (H,W,C) → (1,H,W,C) test_image = np.expand_dims(test_image, axis=0) ### Run model inference to get softmax probabilities result = model.predict(test_image) ### Inspect raw probabilities (optional) print(result[0]) ### Argmax to find predicted class index indPositionMax = np.argmax(result[0]) ### Display predicted index (optional) print('The position is : ',indPositionMax ) ### Map index to class label flower_predict = flower_categories[indPositionMax] ### Build text for overlay text = "Prediction : " + flower_predict ### Reload original image for annotation imgFinalResult = cv2.imread(f1) ### Choose a readable font font = cv2.FONT_HERSHEY_COMPLEX ### Draw the prediction text onto the image cv2.putText(imgFinalResult, text , (0,100), font, 2, (255,0,0), 3) ### Show annotated image cv2.imshow('img', imgFinalResult ) ### Wait for a keypress before closing cv2.waitKey(0) ### Save annotated result to disk for documentation cv2.imwrite('C:/Python-cannot-upload-to-GitHub/flowers/result.jpg',imgFinalResult)
You can find the full code here : https://ko-fi.com/s/0e7d3ab454
This interactive script ties together tensorflow predict image class with a user-friendly OpenCV display. It’s a fast way to validate real-world inputs, share demos, and confirm your keras cnn handles unseen images correctly
Connect :
☕ Buy me a coffee — https://ko-fi.com/eranfeit
🖥️ Email : feitgemel@gmail.com
🤝 Fiverr : https://www.fiverr.com/s/mB3Pbb
Enjoy,
Eran