AI Plant Disease Detector

Train a ResNet-18 model to classify tomato diseases and deploy it as a web app.

Introduction

30 Second Summary

Tomato farmers lose billions of dollars each year to plant diseases that could be caught early with the right diagnosis. What if you could point a camera at a leaf and instantly know what is wrong?

In this project, you will use PyTorch to train a deep learning model that detects 10 types of tomato leaf diseases from photos. You will then build and deploy a web app where anyone can upload a leaf image and get a diagnosis with a confidence score.

What You'll Build

You send your evaluators a link, they upload a photo of a tomato leaf, and the app instantly names the disease with a confidence score.

By the end of this project, you'll have:

  • A Streamlit web app where you upload any tomato leaf photo and get one of 10 disease classifications in under a second, powered by a model you trained using transfer learning on real agricultural data.
  • A confidence percentage alongside every prediction, so you can explain to evaluators how the model assesses its own certainty.
  • A live URL on Streamlit Community Cloud that your evaluators can visit and test without installing anything.
  • Secret Mission: Add a treatment recommendation engine that displays specific remedies and management advice for each detected disease.

Are there any prerequisites?

You should have Python fundamentals and a basic understanding of machine learning concepts. You will also need a Google account, a GitHub account, VS Code, and Python 3.14 installed on your computer.

Before We Start

Before we dive into training models and writing code, let's lock in what you're about to build and why it matters for your evaluation.

Set Up Google Colab and Prepare the Dataset

Training a deep learning model on thousands of images takes serious computing power. Without a GPU, the training process could take hours instead of minutes.

In this step, you will set up Google Colab to access a free GPU and download the PlantVillage dataset. This dataset contains thousands of real, labeled images of tomato leaves that your model will learn from.

In this step, get ready to:
  • Open Google Colab and connect to a T4 GPU runtime.
  • Download and prepare the PlantVillage tomato disease dataset.
  • Create data transforms and DataLoaders for training.
Open Google Colab and connect to a GPU

Google Colab is a free, browser-based notebook environment from Google. It gives you access to a GPU without installing anything on your machine.

  • Go to colab.research.google.com and sign in with your Google account.
  • Click File in the top menu bar.
  • Click New notebook.
  • Click the notebook title at the top (it defaults to Untitled0.ipynb) and rename it to train_model.ipynb.

Now you need to switch the runtime to use a GPU. By default, Colab runs on a CPU.

  • Click Runtime in the top menu bar.
  • Click Change runtime type.
  • In the Hardware accelerator dropdown, select T4 GPU.
  • Click Save.
  • Click the Connect button in the top-right corner of the notebook. You should see it change to Connected with a green checkmark and a RAM/Disk usage indicator.

What is a GPU runtime?

A GPU (graphics processing unit) can process thousands of operations in parallel. This makes it perfect for training deep learning models, where the same math is applied to thousands of images simultaneously.

Colab's free tier gives you access to an NVIDIA T4 GPU with 16 GB of VRAM. Training on a T4 takes minutes instead of hours on a CPU.

Don't see T4 GPU in the dropdown?

GPU availability on Colab's free tier depends on current demand. If T4 is not listed, try again in a few minutes. You can also try selecting just GPU and Colab will assign whatever GPU is available.

Help me connect to a GPU in Google Colab.

Import libraries and download the dataset

With the GPU connected, you can now set up PyTorch and download the dataset. Colab comes with PyTorch pre-installed, so you just need to import it.

  • In the first code cell of your notebook, import the libraries and install gdown by pasting this code:
import os
import copy
import time
import zipfile
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
import numpy as np

# Install gdown for downloading from Mendeley
!pip install gdown
import gdown
  • Run the cell by pressing Shift+Enter.

What does this code do?

The first block imports the core libraries you need. torch is the PyTorch deep learning framework. torchvision provides pre-trained models, image transforms, and dataset utilities.

The !pip install gdown command installs a tool for downloading large files from URLs. You will use it to download the PlantVillage dataset.

You should see pip output ending with a success message. The cell finishes with no errors.

Seeing a ModuleNotFoundError?

If torch or torchvision fail to import, make sure you are running the notebook on Colab's hosted runtime, not a local runtime. These libraries come pre-installed on Colab.

Help me fix a ModuleNotFoundError in Google Colab.

Now you need to download the PlantVillage dataset. This is a collection of over 54,000 plant leaf images across 38 classes. You will filter it down to just the 10 tomato classes.

  • Click the + Code button to add a new cell below. Paste this code to download, extract, and filter the dataset:
# Download PlantVillage dataset from Mendeley Data
url = "https://data.mendeley.com/public-files/datasets/tywbtsjrjv/files/d5652a28-c1d8-4b76-97f3-72fb80f94efc/file_downloaded"
output = "plantvillage.zip"
gdown.download(url, output, quiet=False)

# Extract the zip file
with zipfile.ZipFile(output, 'r') as zip_ref:
    zip_ref.extractall('data')

# Filter to tomato classes only
import shutil
source_dir = 'data/Plant_leave_diseases_dataset_without_augmentation'
tomato_dir = 'data/tomato'
os.makedirs(tomato_dir, exist_ok=True)

for folder in os.listdir(source_dir):
    if folder.startswith('Tomato'):
        shutil.copytree(os.path.join(source_dir, folder), os.path.join(tomato_dir, folder))
  • Run the cell by pressing Shift+Enter. The download takes 2-3 minutes depending on network speed.

What does this code do?

The gdown.download call fetches the full PlantVillage dataset (about 828 MB) from Mendeley Data. The dataset arrives as a zip file, which zipfile.ZipFile extracts into a data/ folder.

The full dataset contains 38 classes across 14 different crops. The for loop filters out everything except the folders that start with Tomato, giving you exactly 10 tomato disease classes to work with.

You should see a download progress bar followed by the cell completing without errors.

Download failing or timing out?

If the download stalls, the Mendeley server may be under heavy load. Try running the cell again. If the error persists, check that your Colab runtime is still connected (look for the green checkmark in the top-right).

Help me fix a dataset download issue in Google Colab.

Define transforms and create DataLoaders

Before the model can learn from these images, each image needs to be resized, converted to a tensor, and normalized. These preprocessing steps are called transforms.

  • Add a new code cell and paste this code to define the training and validation transforms:
# Define training and validation transforms
data_transforms = {
    'train': transforms.Compose([
        transforms.Resize(256),
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}
  • Run the cell by pressing Shift+Enter. The cell completes with no output.

What do these transforms do?

Training transforms include data augmentation. RandomResizedCrop randomly crops each image to 224x224 pixels, and RandomHorizontalFlip randomly mirrors images horizontally. These create slightly different versions of each image on every training pass, which helps the model generalize better.

Validation transforms use a fixed CenterCrop instead of random crops. You want consistent, repeatable results when measuring accuracy.

Both use the same Normalize values [0.485, 0.456, 0.406] and [0.229, 0.224, 0.225]. These are the ImageNet mean and standard deviation values, which match the data the pre-trained model was originally trained on.

Now load the images, split them into training and validation sets, and create the DataLoaders that feed batches of images to the model.

  • Add a new code cell and paste this code to load the dataset, split it, and print a summary:
# Load dataset and create 80/20 train/val split
full_dataset = datasets.ImageFolder(tomato_dir)
train_size = int(0.8 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])

# Apply transforms to each split
train_dataset.dataset.transform = data_transforms['train']
val_dataset.dataset.transform = data_transforms['val']

# Create DataLoaders for batched training
dataloaders = {
    'train': DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2),
    'val': DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2),
}
dataset_sizes = {'train': train_size, 'val': val_size}
class_names = full_dataset.classes

# Print dataset summary
print(f"Training images: {train_size}")
print(f"Validation images: {val_size}")
print(f"Classes: {len(class_names)}")
for i, name in enumerate(class_names):
    print(f"  {i}: {name}")

# Set computation device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

Before you run this cell, what do you think the output will show? How many training images do you expect from an 80/20 split?

  • Run the cell by pressing Shift+Enter.

You should see output showing approximately 16,000 training images, approximately 4,000 validation images, 10 classes, and a list of tomato disease names. The last line should read Using device: cuda, confirming your GPU is active.

What does this code do?

The ImageFolder loader reads the data/tomato/ directory and treats each subfolder as a class. It automatically assigns numeric labels to each disease name.

The random_split function splits the dataset into 80% training and 20% validation. DataLoader wraps each split into batches of 32 images, which is how the model will consume the data during training.

The class_names list stores the 10 disease labels. The device variable tells PyTorch to run computations on the GPU (cuda) if available.

Seeing "Using device: cpu" instead of cuda?

This means your notebook is not connected to a GPU. Go back to Runtime > Change runtime type and make sure T4 GPU is selected. After saving, click Connect again and re-run all your cells from the top.

Help me connect my Colab notebook to a GPU.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook (Colab auto-saves to Google Drive, but you can also press Ctrl+S or Cmd+S).

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm you have these four cells in order:

  • Cell 1: All import statements plus !pip install gdown and import gdown.
  • Cell 2: The gdown.download call, zipfile extraction, and the for loop that filters to tomato classes.
  • Cell 3: The data_transforms dictionary with train and val transform pipelines.
  • Cell 4: ImageFolder, random_split, DataLoader setup, print summary, and device assignment.

The last cell's output should show approximately 16,000 training images, 10 classes, and Using device: cuda.

Your dataset is loaded, split, and ready for training. Next up, you will visualize sample images from each disease class to build your intuition for what the model will learn to distinguish.

Explore the Data and See Your First Leaf Images

In the previous step, you downloaded the PlantVillage dataset, filtered it down to 10 tomato disease classes, and loaded everything into PyTorch DataLoaders. Your notebook has roughly 16,000 training images and 4,000 validation images ready to go.

Before training a model on this data, you need to see what it actually looks like. Visualizing samples from each disease class builds your intuition for what the model will learn to distinguish.

In this step, get ready to:
  • Visualize a sample image from each of the 10 disease classes in a grid.
  • Print the class distribution to check for any major imbalances.
Visualize a sample image from each disease class

Each of the 10 disease classes lives in its own subdirectory inside data/tomato/. Plotting one image from each class in a grid gives you a quick visual reference for what healthy and diseased leaves look like.

  • Add a new code cell at the bottom of your Colab notebook and paste the following visualization code:
# Create a 2x5 grid to display one image per class
fig, axes = plt.subplots(2, 5, figsize=(20, 8))
for i, class_name in enumerate(class_names):
    # Build the path to this class's image folder
    class_dir = os.path.join(tomato_dir, class_name)
    img_name = os.listdir(class_dir)[0]
    img_path = os.path.join(class_dir, img_name)
    from PIL import Image
    img = Image.open(img_path)
    # Place the image in the correct grid position
    ax = axes[i // 5][i % 5]
    ax.imshow(img)
    ax.set_title(class_name.replace('Tomato___', ''), fontsize=10)
    ax.axis('off')
plt.tight_layout()
plt.show()

What does this code do?

  • plt.subplots(2, 5) creates a 2-row, 5-column grid of image slots. That is 10 slots total, one for each tomato disease class.
  • The loop iterates through class_names, grabs the first image file from each class's folder, and opens it with PIL.
  • axes[i // 5][i % 5] uses integer division to place class 0-4 in the top row and class 5-9 in the bottom row.
  • set_title strips the Tomato___ prefix so the labels are easier to read.

Before you run this cell, what do you expect to see? Think about how many images will appear and what they might look like.

  • Run the cell by pressing the play button or hitting Shift+Enter.

You should see a 2x5 grid of 10 leaf images. Each image is labeled with a disease name like Early_blight or healthy. Notice how some diseases show dark spots, others show yellowing, and the healthy leaf is uniformly green.

Not seeing the image grid?

  • Make sure you ran all the cells from Step 1 first. The variables class_names and tomato_dir must already exist in memory.
  • If you see a NameError, your Colab runtime may have disconnected. Reconnect and re-run the Step 1 cells from the top of the notebook.
  • help me debug why my leaf visualization isn't showing in Colab
Check the class distribution

A dataset with heavily imbalanced classes can skew your model toward the majority class. Printing the number of images in each class tells you whether the data is roughly balanced before you start training.

  • Add a new code cell below the visualization and count the images per class by running this code:
# Count images in each class directory
for class_name in class_names:
    class_dir = os.path.join(tomato_dir, class_name)
    count = len(os.listdir(class_dir))
    print(f"{class_name}: {count} images")

What does this tell you?

Each line shows how many images exist for that disease. If one class had 5,000 images and another had only 50, the model would learn to always guess the bigger class.

The PlantVillage tomato classes are roughly balanced, with most classes having between 300 and 2,000 images. This means your model gets a fair chance to learn each disease without heavy class-weighting tricks.

  • Run the cell. You should see 10 lines of output, each showing a class name and its image count.
  • Scan the counts to confirm no class is drastically smaller than the others.

Extra for Experts: What is the PlantVillage dataset?

PlantVillage is an open agricultural dataset with over 54,000 images across 38 disease classes and 14 crop species. You are using only the tomato subset (10 classes), which keeps training fast and the model file small.

The images were captured under controlled lab conditions. This means a model trained on PlantVillage may not perform as well on photos taken in a real field with different lighting and backgrounds. For your academic project, this is perfectly fine as a demonstration of the full ML pipeline.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook before moving on.

ⓧ I'd like to double check the full code

Your notebook is too long to display in one block, so confirm these sections are present in order by scrolling through your cells:

  • Cell 1 (from Step 1): Imports, dataset download, extraction, and filtering to the data/tomato/ directory.
  • Cell 2 (from Step 1): Transforms, DataLoaders, class_names, and device setup with print statements.
  • Cell 3 (this step): The visualization cell with plt.subplots(2, 5) that displays one leaf image per class in a 2x5 grid.
  • Cell 4 (this step): The class distribution cell that prints the image count for each of the 10 classes.

You now know what your data looks like and that the classes are reasonably balanced. Next up, you will train a deep learning model to automatically tell these diseases apart.

Train the Model with Transfer Learning

You have your dataset loaded and you know what the images look like. Now it is time to build the brain of your app.

Training a convolutional neural network from scratch on this dataset would take hours and likely underperform. Transfer learning lets you leverage a model that was already trained on millions of images and adapt it to your tomato disease task in minutes.

In this step, get ready to:
  • Load a pretrained ResNet-18 model and adapt it for 10 tomato disease classes.
  • Define a training loop that tracks accuracy each epoch and saves the best model.
  • Run the training, save the model file, and download it to your computer.
Load a pretrained ResNet-18 and adapt it for tomato diseases

ResNet-18 is a neural network that was trained on over a million images from the ImageNet dataset. It already understands edges, textures, and shapes. You just need to swap out its final layer so it classifies your 10 tomato disease categories instead of the original 1,000 ImageNet categories.

  • Create a new code cell in your Colab notebook and paste the following code to load and modify ResNet-18:
# Load pretrained ResNet-18 and modify for 10 classes
model = models.resnet18(weights='IMAGENET1K_V1')

for param in model.parameters():
    param.requires_grad = False

num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, len(class_names))
model = model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.fc.parameters(), lr=0.001, momentum=0.9)
scheduler = lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)

What does this code do?

  • models.resnet18(weights='IMAGENET1K_V1') downloads ResNet-18 with its pretrained ImageNet weights. These weights already encode powerful visual features.
  • param.requires_grad = False freezes every layer in the network so the pretrained knowledge is preserved during training.
  • model.fc = nn.Linear(num_ftrs, len(class_names)) replaces the final fully connected layer (originally 1,000 ImageNet categories) with a new layer that outputs 10 tomato disease classes. This new layer is NOT frozen, so it will learn during training.
  • The optimizer only receives model.fc.parameters() because that is the only layer that needs to learn. The scheduler reduces the learning rate after 3 epochs to fine-tune more carefully.
  • Run the cell by pressing Shift+Enter.

You should see a download progress bar as Colab fetches the pretrained ResNet-18 weights (about 45 MB). The cell completes in a few seconds.

Seeing a download error?

  • Make sure your Colab runtime is still connected. Check the top right corner for a green checkmark next to the RAM/Disk indicators.
  • If the runtime disconnected, click Reconnect and re-run all previous cells before running this one.

Help me troubleshoot my ResNet-18 download error in Google Colab.

What is transfer learning?

Imagine you already know how to play guitar, and now you want to learn bass. You do not start from zero because your finger coordination and music theory carry over. You just need to adapt to the new instrument.

Transfer learning works the same way. ResNet-18 already learned to detect edges, textures, and patterns from millions of images. You freeze those learned features and only train a new final layer to recognize tomato diseases.

Define the training loop

The training loop is the engine that teaches your model. Each pass through the dataset is called an epoch. The loop runs the model on training images, measures how wrong its predictions are, adjusts the final layer, then checks accuracy on validation images the model has never seen.

  • Create a new code cell and paste the first part of the training function:
# Training function
def train_model(model, criterion, optimizer, scheduler, num_epochs=5):
    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    for epoch in range(num_epochs):
        print(f'Epoch {epoch+1}/{num_epochs}')
        print('-' * 30)

        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()
            else:
                model.eval()

            running_loss = 0.0
            running_corrects = 0

            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                optimizer.zero_grad()

                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

What does this code do?

  • best_model_wts stores a snapshot of the model at its highest accuracy. If later epochs get worse, you keep the best version.
  • Each epoch has two phases. In train, the model learns and updates weights. In val, it predicts on unseen images without learning, so you can measure real accuracy.
  • torch.set_grad_enabled(phase == 'train') turns gradient tracking on only during training. During validation, gradients are off because the model should not learn from validation data.
  • Still in the same cell, paste this code directly below to complete the training function:
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)

            if phase == 'train':
                scheduler.step()

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]

            print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}')

            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    print(f'Best validation accuracy: {best_acc:.4f}')
    model.load_state_dict(best_model_wts)
    return model

What does the rest of the function do?

  • loss.backward() calculates how each weight contributed to the error. optimizer.step() nudges those weights in the right direction.
  • After every epoch, the function prints the loss and accuracy for both training and validation. This lets you watch the model improve in real time.
  • The if phase == 'val' and epoch_acc > best_acc block saves the model weights whenever validation accuracy hits a new high. At the end, the function loads those best weights back into the model.
  • Run the cell by pressing Shift+Enter. The cell defines the function and completes without any output.

Seeing a syntax or indentation error?

  • Make sure the second code block is pasted directly below the first in the same cell. The indentation must continue from the first block without any extra blank lines or shifts.
  • Python is very strict about indentation. Each level uses exactly four spaces. If you see IndentationError, check that the first line of the second block (if phase == 'train':) is indented to the same level as the loss = criterion(outputs, labels) line above it.

Help me fix an indentation error in my training function in Google Colab.

Run the training and download the model

Everything is set up. The model is loaded, the layers are frozen, and the training function is defined. Time to train.

Before you run the next cell, think about this: you froze every layer except the very last one. Do you think training just one layer is enough to get above 90% accuracy on 10 disease classes?

  • Create a new code cell and paste the following to run training, save the model, and download it to your computer:
model = train_model(model, criterion, optimizer, scheduler, num_epochs=5)

# Save the model
torch.save({
    'model_state_dict': model.state_dict(),
    'class_names': class_names,
}, 'plant_disease_model.pth')

print("Model saved as plant_disease_model.pth")

# Download the model file
from google.colab import files
files.download('plant_disease_model.pth')

What does this code do?

  • train_model() runs 5 epochs of training. Each epoch prints the loss and accuracy for both the training and validation sets.
  • torch.save() saves both the trained model weights and the class names list into a single .pth file. Bundling class names with the weights means you do not need to hardcode them separately when you load the model later.
  • files.download() triggers your browser to download the saved model file to your local machine. You will need this file when building the web app.
  • Run the cell by pressing Shift+Enter. Training takes about 2-3 minutes on a T4 GPU.

Watch the epoch-by-epoch output scroll in. The answer to the earlier question: yes, training just one layer is enough. Validation accuracy climbs above 90% because the frozen ResNet-18 backbone already extracts strong visual features from ImageNet. You only had to teach it what those features mean for tomato diseases.

Why does accuracy plateau?

You may notice accuracy jumps quickly in the first 1-2 epochs, then levels off. That is because only one layer is learning. The frozen backbone cannot adapt its feature detectors to tomato-specific patterns like lesion shapes or vein discoloration.

For this project, above 90% is excellent. On harder problems where pretrained features are a weaker fit, you would unfreeze earlier layers and fine-tune them too.

The last line of output reads Best validation accuracy: followed by a number above 0.9000. After that, you see Model saved as plant_disease_model.pth and your browser downloads the file automatically.

Model file did not download?

  • Some browsers block automatic downloads from Colab. Check your browser's download bar or popup blocker. You may need to allow downloads from colab.research.google.com.
  • Alternatively, click the folder icon in the left sidebar of Colab, find plant_disease_model.pth, right-click it, and select Download.
  • If accuracy is below 90%, make sure your runtime has a GPU connected. Go to Runtime then Change runtime type and confirm T4 GPU is selected.

Help me download my trained model from Google Colab.

  • Confirm that plant_disease_model.pth is in your Downloads folder (or wherever your browser saves files). The file is approximately 45 MB.

✔️ Awesome, I've got everything!

Make sure all three cells in this step ran without errors and that plant_disease_model.pth is downloaded to your local machine. You will need that file in the next step.

ⓧ I'd like to double check the full code

Your notebook now has many cells from Steps 1, 2, and 3. Scroll through and confirm these three cells from this step appear in order after your visualization code:

  • Cell 1: Starts with model = models.resnet18(weights='IMAGENET1K_V1') and ends with the scheduler definition. This loads the model, freezes layers, replaces the final layer, and sets up the optimizer.
  • Cell 2: The complete train_model function definition. Starts with def train_model(...) and ends with return model. Both code blocks you pasted should be combined into this single cell.
  • Cell 3: Starts with model = train_model(...) and ends with files.download('plant_disease_model.pth'). This runs training, saves the model, and downloads it.

Your model is trained and sitting on your local machine. Next up, you will wrap it in a web app so anyone can upload a leaf photo and get a disease prediction without touching any code.

Build the Streamlit Web App Locally

You have a trained model that classifies tomato diseases with over 90% accuracy. The problem is that it lives in a Google Colab notebook that your evaluators cannot use.

In this step, you will wrap the model in a Streamlit web app that runs on your local machine. By the end, anyone sitting at your laptop can upload a leaf photo and get an instant disease prediction.

In this step, get ready to:
  • Create a local project folder with a requirements file and your trained model.
  • Write a Streamlit app that loads the model, accepts image uploads, and runs predictions.
  • Test the app in your browser at localhost:8501 with a real tomato leaf image.
Create the project folder and install dependencies

Your Streamlit app needs its own folder with three files: the app code, a list of dependencies, and the trained model you downloaded from Colab.

  • Return to VS Code and open the integrated terminal by pressing Ctrl+` (backtick key, both macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir plant-disease-detector
cd plant-disease-detector
  • Copy the trained model file from your Downloads folder into plant-disease-detector by running this command:

macOS

cp ~/Downloads/plant_disease_model.pth .

Windows

copy $HOME\Downloads\plant_disease_model.pth .
  • Confirm the model file is in your project folder by running:
ls

You should see plant_disease_model.pth listed in the output.

Don't see the model file?

If the copy command fails, check where your browser saved the file. It may be in a different folder than Downloads. Open Finder (macOS) or File Explorer (Windows) and search for plant_disease_model.pth to locate it, then copy it manually into your plant-disease-detector folder.

Need more help?

Now you need a requirements.txt file that lists every Python package the app needs.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the plant-disease-detector folder on your Desktop, then click Open.
  • Create a new file by pressing Cmd+N (macOS) or Ctrl+N (Windows).
  • Save it immediately as requirements.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Paste the following contents into the file:
torch
torchvision
streamlit
Pillow
--extra-index-url https://download.pytorch.org/whl/cpu

What does --extra-index-url do?

The --extra-index-url line tells pip to look for CPU-only versions of PyTorch. The CPU-only build is much smaller than the CUDA version, and your app only needs to run inference on uploaded images.

  • Save requirements.txt.
  • Install all the dependencies by running this command in your VS Code terminal:
pip install -r requirements.txt

The install takes a minute or two. You should see a stream of download progress bars ending with a success message.

Seeing 'command not found' for pip?

Try running pip3 install -r requirements.txt instead. On some systems, pip3 is the correct command for Python 3.

Still stuck?

Load the model and define the class names

The app needs to know exactly which 10 disease classes the model was trained on. It also needs a function to rebuild the same ResNet-18 architecture and load the saved weights into it.

  • Create a new file in VS Code and save it as app.py.
  • Paste the following code into the file:
import streamlit as st
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Image

# The 10 tomato disease classes matching the training data
CLASS_NAMES = [
    'Tomato___Bacterial_spot',
    'Tomato___Early_blight',
    'Tomato___Late_blight',
    'Tomato___Leaf_Mold',
    'Tomato___Septoria_leaf_spot',
    'Tomato___Spider_mites Two-spotted_spider_mite',
    'Tomato___Target_Spot',
    'Tomato___Tomato_Yellow_Leaf_Curl_Virus',
    'Tomato___Tomato_mosaic_virus',
    'Tomato___healthy',
]

@st.cache_resource  # Cache so the model loads only once
def load_model():
    model = models.resnet18(weights=None)  # Same architecture as training
    num_ftrs = model.fc.in_features
    model.fc = nn.Linear(num_ftrs, len(CLASS_NAMES))
    checkpoint = torch.load('plant_disease_model.pth', map_location=torch.device('cpu'), weights_only=False)
    model.load_state_dict(checkpoint['model_state_dict'])
    model.eval()
    return model

What does this code do?

CLASS_NAMES lists the exact same 10 class labels the model learned during training. The order matters because the model outputs a number (0 through 9), and the app uses this list to translate that number into a disease name.

The @st.cache_resource decorator tells Streamlit to load the model only once and keep it in memory. Without caching, the model would reload from disk every time a user interacts with the app.

Inside load_model(), you rebuild the same ResNet-18 structure (with weights=None so no pretrained weights are loaded), then fill it with your trained weights from the checkpoint file.

  • Save app.py.
Build the prediction function and upload interface

The model expects images preprocessed with the exact same transforms used during validation. If the transforms differ, the predictions will be unreliable.

  • In app.py, add the prediction function and app interface below the load_model() function:
def predict(image, model):
    # Apply the same transforms used during validation
    transform = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])
    img_tensor = transform(image).unsqueeze(0)  # Add batch dimension
    with torch.no_grad():  # No gradient needed for inference
        outputs = model(img_tensor)
        probabilities = torch.nn.functional.softmax(outputs, dim=1)
        confidence, predicted_class = probabilities.max(1)
    return CLASS_NAMES[predicted_class.item()], confidence.item() * 100

# App title and description
st.title("Tomato Leaf Disease Detector")
st.write("Upload a photo of a tomato leaf to identify diseases.")

model = load_model()  # Load the cached model

# File upload widget accepting common image formats
uploaded_file = st.file_uploader("Choose a leaf image", type=["jpg", "jpeg", "png"])

Why do the transforms matter?

The predict() function applies the same Resize, CenterCrop, and Normalize transforms you used during validation in Colab. The model learned to recognize diseases from images processed this way. Feeding it raw, unprocessed images would produce meaningless output.

The torch.no_grad() context manager tells PyTorch to skip gradient tracking, which speeds up inference and uses less memory.

  • Save app.py.
  • Launch the Streamlit app by running this command in your terminal:
streamlit run app.py

Your default browser opens to localhost:8501. You should see the title "Tomato Leaf Disease Detector" and a file upload widget.

What about the terminal?

The streamlit run command takes over your terminal. Leave it running. Streamlit auto-detects file changes, so every time you save app.py, the app will prompt you to rerun in the browser. Click Always rerun when prompted so future saves reload automatically.

Browser didn't open?

Check your terminal output for a URL. Copy and paste it into your browser manually. If the terminal shows an error about a missing module, make sure you installed the dependencies with pip install -r requirements.txt in the same terminal session.

Still not working?

Now add the logic that handles an uploaded image and displays the prediction.

  • In app.py, add the following code below the st.file_uploader line:
if uploaded_file is not None:
    image = Image.open(uploaded_file).convert('RGB')
    st.image(image, caption="Uploaded Leaf Image", use_container_width=True)

    # Run the prediction
    with st.spinner("Analyzing..."):
        disease_name, confidence = predict(image, model)

    # Clean up the class name for display
    display_name = disease_name.replace('Tomato___', '').replace('_', ' ')

    # Show green for healthy, yellow warning for diseased
    if 'healthy' in disease_name.lower():
        st.success(f"Prediction: {display_name} ({confidence:.1f}% confidence)")
    else:
        st.warning(f"Prediction: {display_name} ({confidence:.1f}% confidence)")

What does this code do?

When a user uploads a file, the code opens it as an RGB image and displays it with st.image. The image then passes through the predict() function while a loading spinner shows.

The raw class name like Tomato___Early_blight gets cleaned up by stripping the Tomato___ prefix and replacing underscores with spaces. Healthy leaves get a green success banner. Diseased leaves get a yellow warning.

  • Save app.py.

Before you test, think about this: when you upload a diseased leaf image, will the prediction show the raw class name from the dataset, or something cleaner?

  • Switch to your browser at localhost:8501. The app should have reloaded automatically.
  • Search online for a tomato leaf disease image (try searching "tomato early blight leaf") and download one.
  • Click Browse files in the upload widget and select your downloaded leaf image.

The app displays your uploaded image, then shows the predicted disease name and a confidence percentage. Healthy leaves appear in a green banner. Diseased leaves appear in a yellow warning banner.

Seeing an error after uploading?

If the app crashes when you upload an image, check that plant_disease_model.pth is in the same folder as app.py. Also make sure you uploaded a standard image file (JPG or PNG), not a PDF or screenshot of a webpage.

Need help debugging?

✔️ Awesome, I've got everything!

Great. Double check you have saved both app.py and requirements.txt.

ⓧ I'd like to double check the full code

Your app.py should contain three sections in this exact order. Scroll up and confirm each one is present:

  • Imports, CLASS_NAMES list, and the load_model() function with the @st.cache_resource decorator.
  • The predict() function, app title, model = load_model(), and the st.file_uploader call.
  • The if uploaded_file is not None block with image display, prediction call, display name cleanup, and the st.success/st.warning conditional.

Your requirements.txt should contain:

torch
torchvision
streamlit
Pillow
--extra-index-url https://download.pytorch.org/whl/cpu

Your disease detector works locally. Next up, you will deploy it to Streamlit Community Cloud so your evaluators can visit a live URL and test it themselves.

Deploy to Streamlit Community Cloud

Your Streamlit app works on your local machine. But your evaluators cannot install Python and download a trained model just to test it.

Streamlit Community Cloud hosts your app at a public URL for free, directly from a GitHub repository. In this step, you'll push your project to GitHub and deploy it so anyone with the link can diagnose tomato diseases from their browser.

In this step, get ready to:
  • Push your project files to a GitHub repository.
  • Deploy the app on Streamlit Community Cloud with the correct Python version.
  • Test the live URL and share it with your evaluators.
Push your project to GitHub

Streamlit Community Cloud deploys apps directly from a GitHub repository. You need to push your three project files to a public repo so the platform can access them.

  • In your terminal where the Streamlit app is running, press Ctrl+C to stop the local server.

Before you push, confirm your requirements.txt is set up for cloud deployment.

  • Open requirements.txt in VS Code and verify it contains these five lines:
torch
torchvision
streamlit
Pillow
--extra-index-url https://download.pytorch.org/whl/cpu

Why CPU-only PyTorch?

Streamlit Community Cloud does not have GPUs. Your app only runs inference, which works fine on a CPU.

The full PyTorch package with CUDA support is much larger and would slow down deployment. The --extra-index-url line tells pip to install the lightweight CPU-only version instead.

Now create a new GitHub repository to hold your project.

  • Open your browser and go to github.com.
  • Click the + icon in the top-right corner, then click New repository.
  • Enter plant-disease-detector as the repository name.
  • Keep the repository set to Public.
  • Do not check any initialization options (no README, no .gitignore). You already have local files to push.
  • Click Create repository.

GitHub shows a Quick setup page with the repository URL. You will need this URL in the next command.

  • Copy the HTTPS URL shown on the Quick setup page (it looks like https://github.com/your-username/plant-disease-detector.git).
  • Back in your terminal, initialize a Git repository and make your first commit by running these commands:
git init -b main
git add .
git commit -m "Add plant disease detector app"

What do these commands do?

  • git init -b main creates a new Git repository with main as the default branch name.
  • git add . stages all three files (app.py, requirements.txt, plant_disease_model.pth) for the commit.
  • git commit saves a snapshot of your project to the local repository.
  • Connect your local repository to GitHub and push your files by running these commands:
git remote add origin [[REPO_URL="https://github.com/your-username/plant-disease-detector.git"]]
git push -u origin main

The push may take a minute because the model file is about 45 MB.

Push failed?

  • If you see an authentication error, your terminal may need GitHub credentials. GitHub no longer accepts passwords for git push. Generate a Personal Access Token in your GitHub account under Settings then Developer settings then Personal access tokens, and use it as your password.
  • If you see error: src refspec main does not match any, make sure you committed your files before pushing. Run git status to check.

Help me push my project to GitHub.

  • Refresh your GitHub repository page in your browser.

You should see app.py, requirements.txt, and plant_disease_model.pth listed in the repository.

Deploy on Streamlit Community Cloud

Your code is on GitHub. Now you will connect it to Streamlit Community Cloud so it becomes a live web app with its own URL.

  • Open a new browser tab and go to share.streamlit.io.
  • Sign in with your GitHub account. If this is your first visit, this creates your Streamlit Community Cloud account automatically.
  • In the upper-right corner, click Create app.
  • When asked "Do you already have an app?" click Yup, I have an app..

You now see a form where you configure which repository, branch, and file to deploy.

  • In the Repository field, select or type your plant-disease-detector repository.
  • Set the Branch to main.
  • Set the Main file path to app.py.
  • Click Advanced settings at the bottom of the form.
  • Select 3.12 from the Python version dropdown.
  • Click Save.

Why set Python to 3.12?

Streamlit Community Cloud defaults to Python 3.12, but the default has been known to change. Explicitly selecting 3.12 ensures your PyTorch and torchvision packages install correctly.

Once deployed, you cannot change the Python version without deleting and redeploying the app. Getting it right now saves you from having to redo this later.

  • Click Deploy.

Streamlit Community Cloud starts building your app. You can see the build logs on the right side of the screen. The build typically takes 1 to 3 minutes because it needs to install PyTorch.

Build failed?

  • If the logs show a dependency error, check that your requirements.txt includes the --extra-index-url https://download.pytorch.org/whl/cpu line. Without it, the build tries to install GPU-enabled PyTorch and may run out of memory.
  • If you see a Python version error, make sure you selected 3.12 in Advanced settings before deploying. To fix this after the fact, delete the app from your Streamlit dashboard and redeploy with the correct version.

Help me fix my Streamlit Community Cloud deployment.

Test your live app

Once the build finishes, Streamlit Community Cloud redirects you to your live app. The URL is shown in your browser's address bar and ends with .streamlit.app.

Before you upload an image, predict what you will see. Will the disease classification and confidence score match what you saw when testing on your local machine?

  • Upload a tomato leaf image using the file uploader on your live app.

The disease prediction and confidence percentage appear within a few seconds. The result should match what you saw when you ran the app locally.

  • Copy the URL from your browser's address bar. This is the link you will share with your evaluators.
  • Send the URL to a friend or classmate and ask them to upload a tomato leaf image from their device to confirm the app works for others.

Keep your app awake for demo day

Free Streamlit Community Cloud apps go to sleep after extended inactivity. The app wakes up when someone visits the URL, but it takes 30 to 60 seconds to restart.

Visit your app URL at least once before your evaluation day so it is already awake when your evaluators test it.

Your plant disease detector is live on the internet. Anyone with the link can upload a tomato leaf photo and get an instant diagnosis. Next up, take on the Secret Mission to turn your classifier into a complete decision-support tool with treatment recommendations.

Secret mission

Add Treatment Recommendations

Your app can detect diseases, but what should a farmer actually do about it? In this secret mission, you will add a treatment recommendation engine that displays specific causes, symptoms, remedies, and prevention tips for each detected disease. This transforms your classifier from a demo into a genuine decision-support tool.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on free services, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Google Colab notebook (free, notebooks auto-disconnect after 12 hours of inactivity).
  • Streamlit Community Cloud app (free, stays live as long as it gets traffic).
  • GitHub repository (free).
  • Local project folder containing app.py, requirements.txt, and plant_disease_model.pth.

Keep everything running

No action needed. Choose this if you plan to show your app to evaluators or continue developing it.

  • Your Streamlit Community Cloud app stays live for free at its public URL.
  • Free apps sleep after extended inactivity but wake up when someone visits the URL. This takes about 30-60 seconds.
  • Visit your app URL at least once before evaluation day to make sure it wakes up quickly for your demo.
  • Your Google Colab notebook and GitHub repository remain accessible at no cost.

Pause - I'll come back to this later

Shut down any running local processes to free up memory on your machine. Your cloud resources stay available.

  • If Streamlit is still running in your terminal, press Ctrl+C to stop the local server.
  • Your Streamlit Community Cloud app will automatically sleep after extended inactivity. It wakes up when someone visits the URL.
  • Your Google Colab notebook disconnects automatically after 12 hours of inactivity. Your code is saved in the notebook.
  • Your GitHub repository and local project folder remain untouched.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Delete the Streamlit Community Cloud app by going to share.streamlit.io.
  • Click the overflow menu icon next to your app and click Delete.
  • Enter the required confirmation string and click Delete to confirm.
  • Delete the GitHub repository by opening the repository page on GitHub.
  • Click Settings in the repository tabs.
  • Scroll down to the Danger Zone section and click Delete this repository.
  • Follow the confirmation prompts and type the repository name to confirm deletion.
  • Delete the local project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/plant-disease-detector

Windows

rmdir /s /q %USERPROFILE%\Desktop\plant-disease-detector
  • Your Google Colab notebook does not need deletion. Colab notebooks auto-disconnect after 12 hours and do not incur ongoing charges.

That's a Wrap!

That's a Wrap!

Nice work! You just trained a deep learning model from scratch, wrapped it in a web app, and deployed it to a live URL that anyone can visit.

You've learned how to:

  • Train a ResNet-18 model using transfer learning on the PlantVillage dataset, achieving over 90% validation accuracy across 10 tomato disease classes.
  • Build a Streamlit web app where you upload a tomato leaf photo and get a real-time disease prediction with a confidence score.
  • Deploy the full system to Streamlit Community Cloud with a live, shareable URL your evaluators can visit and test without installing anything.
  • Secret Mission: Added a treatment recommendation engine that turns a simple classifier into a practical decision-support tool.

Ready to quiz yourself?