TensorFlow Image Classification Tutorial: Flower Recognition with Keras

TensorFlow Image Classification

Last Updated on 22/04/2026 by Eran Feit

TensorFlow Image Classification with Keras: Flower Recognition, Data Augmentation, and OpenCV Prediction

In this comprehensive TensorFlow Image Classification Tutorial, we will explore how to build and deploy a robust deep learning model to recognize various flower species using Python. Image classification is a fundamental pillar of Computer Vision, and by leveraging the Keras API within TensorFlow, we can develop powerful Convolutional Neural Networks (CNN) with high accuracy. This guide walks you through the entire pipeline: from advanced data augmentation with ImageDataGenerator to real-time interactive predictions using OpenCV. Whether you are building a model from scratch or optimizing for speed, this tutorial provides the production-ready code you need.


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

You can find the code and instructions in this link : https://eranfeit.lemonsqueezy.com/buy/ffecaa8b-78f8-49d2-8fd0-5924bf293446 or here : https://ko-fi.com/s/0e7d3ab454

Master Computer Vision

Follow my latest tutorials and AI insights on my Personal Blog.

Bootcamp
Beginner

Complete CV Bootcamp

Foundation using PyTorch & TensorFlow.

Get Started →
PyTorch
Interactive

Deep Learning with PyTorch

Hands-on practice in an interactive environment.

Start Learning →
GPT OpenCV
Advanced

Modern CV: GPT & OpenCV4

Vision GPT and production-ready models.

Go Advanced →

Want the exact dataset so your results match mine?

If you want to reproduce the same training flow and compare your results to mine, I can share the dataset structure and what I used in this tutorial. Send me an email and mention the name of the tutorial / dataset , so I know what you’re requesting.

🖥️ Email: feitgemel@gmail.com


Code for Image Classification ( Tensorflow Image Classification )

Installation :

Setting up a proper environment is the first step toward successful model training. For this project, we utilize an NVIDIA GPU combined with the CUDA toolkit to handle the heavy computations required by Deep Learning. By using an Anaconda environment, we ensure that dependencies like TensorFlow, OpenCV, and Scikit-learn work in harmony, preventing the version conflicts often found in complex Python projects.

TensorFlow Image Classification Tutorial
TensorFlow Image Classification Tutorial

# 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

You can find the full code here : https://ko-fi.com/s/0e7d3ab454

Part 1 : Data Preparation and CNN Model (224×224)

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)

Optimizing for Speed: The 64×64 Lightweight CNN Baseline

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 :

Real-Time Inference: Predicting Flower Species with OpenCV

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 scratchtensorflow predict image classvisual 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

🌐 https://eranfeit.net

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

Enjoy,

Eran

Eran Feit