...

How to build a Brain Tumor Classification using Deep learning

brain tumor classification

Brain Tumor Classification with CNN and Keras

This tutorial builds a complete deep learning pipeline for brain tumor classification from brain MRI images using Python and Keras.
You will organize the dataset into train, validation, and test splits to ensure reliable medical image classification results.
A compact CNN architecture will be trained with image augmentation, binary cross-entropy, and the Adam optimizer for stable convergence.
To improve generalization, the training loop applies EarlyStopping and ModelCheckpoint so only the best model is saved.
You will visualize accuracy and loss curves to track performance and prevent overfitting.


After training, the model will be evaluated on a held-out test set to report unbiased accuracy.
Finally, you will run a single-image prediction, convert the sigmoid score into a clear label, and overlay the decision on the MRI image with OpenCV.
The goal is an end-to-end, reproducible workflow for brain tumor detection that you can adapt to new datasets and clinical imaging tasks.
This guide emphasizes clean directory structure, robust preprocessing, and practical model monitoring for production-ready results.
By the end, you will have a saved Keras CNN capable of classifying MRI scans as tumor or healthy and a repeatable pipeline you can refine further.

Here is a link to the video: https://youtu.be/-147KGbGI3g&list=UULFTiWJJhaH6BviSWKLJUM9sg

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

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


Here is the code for Brain Tumor Classification :

Link for the dataset : https://www.dropbox.com/s/jztol5j7hvm2w96/brain_tumor%20data%20set.zip

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 Brain python=3.7 conda activate Brain  pip install tensorflow pip install tensorflow-gpu  pip install numpy pip install pillow pip install SciPy pip install matplotlib pip install imutils pip install pandas pip install opencv-python   DataSet :  =========  https://www.dropbox.com/s/jztol5j7hvm2w96/brain_tumor%20data%20set.zip

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

Part 1 — Preparing the Brain Tumor Dataset

Introduction

This part organizes the raw MRI dataset into train, validate, and test splits.
A reproducible folder structure improves model reliability and evaluation.
Class balance and random sampling help prevent data leakage and bias.

A clean, reliable directory structure is the backbone of any high-quality brain tumor classification project. In this part, you read the class subfolders inside the seed dataset and count their images to understand class distribution early. Knowing how many “healthy” versus “tumor” images you have helps you anticipate imbalance, choose the right loss/metrics later, and decide whether you need sampling tweaks or augmentation strategies. The script then lays out a reproducible split of 70% training, 15% validation, and 15% test, which is a common, sensible default for medical image classification when data volume is moderate.

The split happens per class, so each category gets its proportional share in train/validation/test. This per-class approach reduces the chance of skewed evaluation and better reflects real performance in deployment. The use of np.random.choice produces a randomized selection for each split, which mitigates selection bias; just keep in mind that if you need absolute reproducibility, you should set a random seed (e.g., np.random.seed(42)) before sampling. Randomization here is critical because MRIs often come in series or adjacent slices; without randomness, you might unintentionally cluster similar slices in a single split, inflating or deflating metrics.

After choosing the files, the script copies images into the target split folders and removes them from the seed directory. This “move-like” behavior ensures you don’t accidentally load the same image twice via path aliases and helps prevent data leakage between splits, a particularly important point in brain MRI classification where patient-level separation is essential. If you’d rather keep the original seed images intact, you can comment out the os.remove calls and simply rely on strict path separation for training and evaluation.

Finally, the resulting folder layout is tailor-made for Keras’ flow_from_directory, which expects a root directory per split with subfolders per class. That compatibility streamlines the next stages—data augmentation, batch loading, and label mapping—while keeping your pipeline simple and maintainable. With this structure in place, you’ll get faster iteration, fewer surprises during training, and a robust foundation for CNN brain tumor detection experiments and benchmarking.

### Import a symbol from a local module if needed by your environment. from this import d  ### Import NumPy for numerical operations and random choice. import numpy as np  ### Import pandas for potential tabular summaries. import pandas as pd  ### Import Matplotlib for optional plotting. import matplotlib.pyplot as plt  ### Import glob for file pattern matching utilities. import glob  ### Import os for filesystem operations. import os  ### Import shutil for high-level file copy operations. import shutil  ### Import math for floor rounding when computing split sizes. import math  ### Import imutils for optional image utilities if needed later. import imutils   ### Define the root folder that contains class subfolders with images. SEED_DATA_DIR = "C:/Python-cannot-upload-to-GitHub/BrainTumor"  ### Prepare a dictionary to hold the number of images per class. num_of_images = {}  ### Iterate over class subfolders and count images in each. for dir in os.listdir(SEED_DATA_DIR):     num_of_images[dir] = len(os.listdir(os.path.join(SEED_DATA_DIR, dir )))  ### Print a summary of class counts for verification. print (num_of_images)  ### Document the target split: 70% train, 15% validate, 15% test. #lets build 3 folders : 70% train data , 15% validate data , and 15% test  ### Define output folders for train, validate, and test splits. TRAIN_DIR = "C:/Python-cannot-upload-to-GitHub/BrainTumor/train" VALIDATE_DIR = "C:/Python-cannot-upload-to-GitHub/BrainTumor/validate" TEST_DIR = "C:/Python-cannot-upload-to-GitHub/BrainTumor/test"   ### Create the train folder if it does not exist. # create the train folder : if not os.path.exists(TRAIN_DIR):     ### Make the train root directory.     os.mkdir(TRAIN_DIR)      ### For each class subfolder, create a corresponding class directory under train.     for dir in os.listdir(SEED_DATA_DIR):         os.makedirs(TRAIN_DIR + "/" + dir)         ### Log the newly created class path.         print (TRAIN_DIR + "/" + dir)          ### Randomly sample ~70% of images (minus 5 buffer) per class for training.         for img in np.random.choice(a=os.listdir(os.path.join(SEED_DATA_DIR,dir)) , size= (math.floor(70/100* num_of_images[dir] )-5) , replace=False ):             ### Source image path within the seed dataset.             O = os.path.join(SEED_DATA_DIR, dir , img)             ### Print source path for traceability.             print(O)             ### Destination class folder under train.             D = os.path.join(TRAIN_DIR, dir)             ### Print destination path for traceability.             print(D)             ### Copy the image into the train split.             shutil.copy(O,D)             ### Remove the copied image from seed to avoid duplication.             os.remove(O) else:     ### If the train folder already exists, log and skip creation.     print("Train Folder Exists")    ### Create the test folder if it does not exist. # create the test folder if not os.path.exists(TEST_DIR):     ### Make the test root directory.     os.mkdir(TEST_DIR)      ### For each class, create a corresponding class subfolder under test.     for dir in os.listdir(SEED_DATA_DIR):         os.makedirs(TEST_DIR + "/" + dir)         ### Log the created path.         print (TEST_DIR + "/" + dir)          ### Randomly sample ~15% of images (minus 5 buffer) for testing.         for img in np.random.choice(a=os.listdir(os.path.join(SEED_DATA_DIR,dir)) , size= (math.floor(15/100* num_of_images[dir] )-5) , replace=False ):             ### Source image path.             O = os.path.join(SEED_DATA_DIR, dir , img)             ### Print source for visibility.             print(O)             ### Destination test class folder.             D = os.path.join(TEST_DIR, dir)             ### Print destination folder.             print(D)             ### Copy image to test split.             shutil.copy(O,D)             ### Remove original to prevent duplication.             os.remove(O) else:     ### If the test folder exists, log and skip.     print("Test Folder Exists")   ### Create the validation folder if it does not exist. # create the validate folder if not os.path.exists(VALIDATE_DIR):     ### Make the validation root directory.     os.mkdir(VALIDATE_DIR)      ### For each class, create a validation class subfolder.     for dir in os.listdir(SEED_DATA_DIR):         os.makedirs(VALIDATE_DIR + "/" + dir)         ### Log the created path.         print (VALIDATE_DIR + "/" + dir)          ### Randomly sample ~15% of images (minus 5 buffer) for validation.         for img in np.random.choice(a=os.listdir(os.path.join(SEED_DATA_DIR,dir)) , size= (math.floor(15/100* num_of_images[dir] )-5) , replace=False ):             ### Source image path.             O = os.path.join(SEED_DATA_DIR, dir , img)             ### Print source for traceability.             print(O)             ### Destination validation class folder.             D = os.path.join(VALIDATE_DIR, dir)             ### Print destination folder.             print(D)             ### Copy image to validation split.             shutil.copy(O,D)             ### Remove original to avoid leakage between splits.             os.remove(O) else:     ### If the validation folder exists, log and skip.     print("Validate Folder Exists") 

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

Your dataset is split into train, validate, and test with stratified sampling by class.
The structure is ready for Keras flow_from_directory in the next part.

Part 2 — Building and Training a CNN Classifier

Introduction

This part defines a compact CNN for binary classification of brain MRI images.
It uses data augmentation, early stopping, and model checkpointing to improve generalization.
Training and validation curves guide you to the best performing model.

Here you define a compact convolutional neural network (CNN) tuned for binary image classification on brain MRI scans. The architecture increments filter counts (32→64→128→256) across stacked Conv2D layers, allowing the network to learn increasingly abstract features—from edges and textures in early layers to tumor-relevant structures in deeper layers. Interleaved MaxPool2D layers reduce spatial dimensions and help the network focus on salient patterns while controlling memory footprint. A Flatten followed by a small dense head distills these features into a single sigmoid output that encodes the probability of “healthy.”

To promote generalization, you add Dropout in the convolutional trunk and again in the dense head. Dropout randomly deactivates a subset of neurons each step, discouraging the network from relying on brittle co-adaptations and thereby reducing overfitting—especially important in medical image classification where datasets can be modest. The input size of (224, 224, 3) keeps computation reasonable while aligning with common augmentation settings and pre-processing conventions in the broader deep learning ecosystem.

Data augmentation is handled by ImageDataGenerator with zoom, shear, horizontal flips, and pixel rescaling to [0, 1]. Even mild augmentations can substantially improve generalization for brain tumor detection by exposing the model to geometric and photometric variations that resemble real-world acquisition differences across scanners and sites. The validation generator uses only rescaling to provide an honest measure of performance—validation data should not be augmented in ways that could disguise overfitting.

Training stability and model selection rely on EarlyStopping and ModelCheckpoint. Early stopping monitors val_accuracy with min_delta=0.01 and patience=5, halting training if improvements stall. The checkpoint callback saves only the best model (by val_accuracy) to disk, guaranteeing that subsequent evaluation and inference use the strongest weights observed during training. After fitting, you plot accuracy and loss for both training and validation sets; diverging curves can indicate overfitting, while parallel convergence suggests a well-regularized fit. Together, these choices create a practical, production-minded baseline for keras brain tumor classification that you can later extend with Batch Normalization, class weighting, or transfer learning if your dataset and goals demand it.

### Import optional pyplot utility. from matplotlib.pyplot import cla  ### Import NumPy for array operations. import numpy as np  ### Import Matplotlib for plotting training curves. import matplotlib.pyplot as plt  ### Import core Keras layers for CNN building. from keras.layers import Dense, Conv2D, Flatten, MaxPooling2D, Dropout, BatchNormalization, MaxPool2D, GlobalAveragePooling2D  ### Import Sequential model container. from keras.models import Sequential  ### Import Keras image utilities and generators. from keras.preprocessing import image  ### Import Keras root package for losses and optimizers. import keras    ### Define paths to the prepared train and validation folders. TRAIN_DIR = "C:/Python-cannot-upload-to-GitHub/BrainTumor/train" VAL_DIR = "C:/Python-cannot-upload-to-GitHub/BrainTumor/validate"  ### Build a simple sequential CNN. # build the CNN model # ===================  ### Initialize the model. model = Sequential()  ### First convolutional layer with 32 filters and ReLU activation. model.add(Conv2D(filters=32, kernel_size=(3,3), activation='relu', input_shape = (224,224,3)    ))  ### Second convolutional layer to deepen representations. model.add(Conv2D(filters=64, kernel_size=(3,3),  activation='relu' ))  ### Max pooling to downsample spatial dimensions. model.add(MaxPool2D(pool_size=(2,2)))  ### Third convolutional block with 128 filters. model.add(Conv2D(filters=128, kernel_size=(3,3),  activation='relu' ))  ### Pooling to control feature map size. model.add(MaxPool2D(pool_size=(2,2)))  ### Fourth convolutional block with 256 filters. model.add(Conv2D(filters=256, kernel_size=(3,3),  activation='relu' ))  ### Pooling again to reduce dimensionality. model.add(MaxPool2D(pool_size=(2,2)))  ### Dropout to reduce overfitting by randomly deactivating neurons. model.add(Dropout(rate=0.25)))  ### Flatten feature maps for the dense classifier head. model.add(Flatten())  ### Dense hidden layer for nonlinear combination of features. model.add(Dense(units=64, activation='relu'))  ### Additional dropout to regularize the dense layer. model.add(Dropout(rate=0.25))  ### Final output layer for binary classification with sigmoid activation. #final layer: model.add(Dense(units=1, activation='sigmoid'))  ### Compile the model with binary crossentropy and Adam optimizer. model.compile(loss=keras.losses.binary_crossentropy, optimizer='adam', metrics=['accuracy'])  ### Print a summary of the architecture. print(model.summary())   ### Configure the training data generator with augmentation and rescaling. # create the train data augmentation object # ========================================== train_datagen = image.ImageDataGenerator(     zoom_range=0.2 , shear_range=0.2, rescale=1. / 255 , horizontal_flip=True )  ### Validation data generator with rescaling only. val_datagen = image.ImageDataGenerator( rescale= 1. / 255)  ### Create training batches from directory with target image size and labels. train_data = train_datagen.flow_from_directory(directory=TRAIN_DIR , target_size=(224,224) , batch_size=32 , class_mode='binary')  ### Create validation batches similarly. val_data = val_datagen.flow_from_directory(directory=VAL_DIR, target_size=(224,224), batch_size=32, class_mode='binary')    ### Import callbacks for early stopping and best-model checkpointing. # create model check point for the performence of the model  from keras.callbacks import ModelCheckpoint , EarlyStopping   ### Configure early stopping on validation accuracy with patience. # lets stop the training if the accuracy is good es = EarlyStopping(monitor='val_accuracy', min_delta=0.01 , patience=5 , verbose=1 , mode='auto')  ### Save only the best model by validation accuracy. mc = ModelCheckpoint(filepath='C:/Python-cannot-upload-to-GitHub/BrainTumor/MyBestModel.h5', monitor='val_accuracy' ,  verbose=1 , mode='auto' , save_best_only=True)   ### Bundle callbacks for training. call_back = [es, mc]  ### Fit the model on training data with validation and callbacks. hist = model.fit(x=train_data, epochs=30 , verbose=1, validation_data=val_data, callbacks=call_back)  ### Extract the training history dictionary. h = hist.history  ### Inspect available metrics keys. print('Keys : ', h.keys() )  ### Plot accuracy curves to compare train and validation. #lets plot the accuracy and the loss #===================================   ### Accuracy plot. #accuracy  plt.plot(h['accuracy']) plt.plot(h['val_accuracy'], c='red') plt.title('Accuracy vs. Val Accuracy') plt.show()  ### Loss plot. #loss plt.plot(h['loss']) plt.plot(h['val_loss'], c='red') plt.title('loss vs. Val loss') plt.show() 

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

A compact CNN is trained with augmentation, early stopping, and checkpoints.
The best model is saved for robust evaluation and inference.

Part 3 — Evaluating the Model and Making Predictions

Introduction

This part evaluates the saved best model on a held-out test set.
It then loads a specific image and generates a tumor or healthy prediction.
A label is drawn on the image for quick visual verification.

With the best checkpoint saved, you evaluate it on the held-out test set using another ImageDataGenerator configured for rescaling. Testing on truly unseen data is non-negotiable for credible brain MRI classification results, and it reveals how the model is likely to perform in real-world scenarios. The generator prints class_indices, which map folder names to numeric labels—useful when interpreting outputs and building downstream logic or dashboards.

You then run model.evaluate to obtain test accuracy. While accuracy is a helpful summary for a roughly balanced binary task, medical contexts often benefit from more detailed metrics: precision, recall (sensitivity), specificity, ROC-AUC, and confusion matrices. If your “tumor” class is rarer or costlier to miss, consider monitoring recall and AUC alongside accuracy, and possibly adjusting the decision threshold away from the default 0.5 to reflect clinical priorities. That threshold is where the next step—single-image prediction—comes into play.

For a single image, you load the file, resize it to (224, 224), convert to a NumPy array, and normalize it just like the training pipeline. The predicted sigmoid score reflects the model’s confidence in the “healthy” class in this setup. Rounding the score converts it to a hard label, which is fine for a quick check; however, in practice you should think about score calibration, threshold tuning on validation data, or even abstaining on low-confidence images to reduce risk in sensitive applications like brain tumor detection.

Finally, you use OpenCV to overlay the predicted label (“Has a brain tumor” or “Brain healthy”) directly on the MRI and display/save the result. This lightweight visualization makes qualitative spot-checks fast and intuitive for content creation, demos, or QA. For deeper audits, consider saving both the raw score and the overlay, logging predictions in a CSV with filenames and scores, and adding optional Grad-CAM visualizations to highlight the regions most responsible for each decision. These practices help you catch data issues early and build trust in model behavior before any real-world usage.

### Comment summarizing the next steps for evaluation and prediction. # now , we will check our model within a new test data # than , we will run a prediction on an image  ### Import normalize symbol if needed by locale utilities. from locale import normalize  ### Import NumPy for array handling. import numpy as np  ### Import model loader for reading the saved best model. from keras.models import load_model  ### Import Keras image utilities for preprocessing. from keras.preprocessing import image  ### Import Keras for base symbols if needed. import keras  ### Import OpenCV for image I/O and drawing text on predictions. import cv2  ### Path to the held-out test directory. TEST_DIR = "C:/Python-cannot-upload-to-GitHub/BrainTumor/test"  ### Define a test data generator with rescaling. test_datagen = image.ImageDataGenerator( rescale= 1. / 255)  ### Create test batches from directory for evaluation. test_data = test_datagen.flow_from_directory(directory=TEST_DIR , target_size=(224,224) , batch_size=32 , class_mode='binary')  ### Display the class-to-index mapping for clarity. # lets print the classes : print("test_data.class_indices: ", test_data.class_indices)  ### Load the best saved model from training. #load the saved model : model = load_model('C:/Python-cannot-upload-to-GitHub/BrainTumor/MyBestModel.h5')  ### Optionally inspect the model structure. #print(model.summary() )  ### Evaluate accuracy on the test generator and take the accuracy component. acc = model.evaluate(x=test_data)[1]  ### Print test accuracy for record. print(acc)    ### Pick a single image path from the test set for demonstration. # load an image from the test folder  imagePath = "C:/Python-cannot-upload-to-GitHub/BrainTumor/test/Healthey/Not Cancer  (1523).jpg" #imagePath = "C:/Python-cannot-upload-to-GitHub/BrainTumor/test/Brain Tumor/Cancer (17).jpg"  ### Load the image and resize to the model target. img = image.load_img(imagePath,target_size=(224,224))  ### Convert the PIL image to a NumPy array. i = image.img_to_array(img) # convert to array  ### Normalize pixel values to match training rescale. i = i / 255 # -> normalize to our model  ### Confirm the shape of a single image array. print(i.shape)  ### Add a batch dimension for prediction. input_arr = np.array([i]) # add another dimention   ### Confirm the batch shape expected by the model. print(input_arr.shape)   ### Run the forward pass to get a sigmoid probability. # run the prediction predictions = model.predict(input_arr)[0][0]  ### Print the raw score for inspection. print(predictions)   ### Convert sigmoid score to a binary label for readability. # since it is binary if the result is close to 0 it is Tumor , and if it close to 1 it is healthy result = round(predictions)  ### Map the numeric label to a human-readable string. if result == 0 :     text = 'Has a brain tumor' else :     text = "Brain healthy"   ### Print the final decision text. print(text)  ### Read the original image with OpenCV for annotation. imgResult = cv2.imread(imagePath)  ### Choose a font for overlay. font = cv2.FONT_HERSHEY_COMPLEX  ### Draw the prediction on the image. cv2.putText(imgResult, text, (0,20), font, 0.8 , (255,0,0),2 )  ### Show the annotated image in a window. cv2.imshow('img', imgResult)  ### Wait for a key press to close the window. cv2.waitKey(0)  ### Save the annotated prediction image to disk. cv2.imwrite("C:/Python-cannot-upload-to-GitHub/BrainTumor/predictImage.jpg",imgResult) 

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

Test accuracy is reported from the generator.
A single image prediction is produced and visualized for quick validation.


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