In Maharashtra and Punjab, a few million farmers now carry an app called Plantix. Point a phone camera at a cotton leaf with brown streaks, and within seconds the app returns a diagnosis — early blight, bacterial spot, or a nutrient deficiency — along with a treatment plan. No agronomist visited the field. No lab ran a pathogen culture. A convolutional neural network looked at roughly 150,000 pixel values and decided which of dozens of disease classes best explains that pattern. This chapter builds that system from the pixel up: how a machine "sees" a leaf, how it learns which pixel patterns mean disease, and — just as importantly — the specific way this exact kind of model fails when the training data doesn't match the real world.
An image is a tensor, not a picture
Before any learning happens, a photograph has to become numbers. A colour photo of a tomato leaf, say 28×28 pixels after resizing (real photos are larger; we use 28×28 here so every arithmetic step below stays checkable by hand), is stored as a rank-3 tensor of shape (height, width, channels) = (28, 28, 3). Each of the three channels — red, green, blue — is a 28×28 grid of intensities, typically scaled to the range [0, 1]. So the leaf is really 28 × 28 × 3 = 2,352 numbers. A healthy leaf and a diseased leaf differ from each other only in the specific values and spatial arrangement of those 2,352 numbers: diseased tissue tends to shift the red channel up and green channel down in localized patches, and it disrupts the smooth colour gradients healthy leaf tissue normally has.
A naive classifier could flatten all 2,352 numbers into one long vector and feed it to a dense (fully connected) layer. This is a real option covered in your deep-learning foundations, and it is a bad one for images, for a reason worth quantifying. A dense layer connecting a 2,352-value input to a modest 128-neuron hidden layer needs 2,352 × 128 + 128 (bias) = 301,184 parameters — and every one of those weights is learned independently, so a lesion pattern the network saw in the top-left corner during training gives it zero head start on recognising the identical lesion pattern if it appears in the bottom-right corner of a new photo. The network has to relearn the same visual feature at every possible location. Convolution exists to fix exactly this waste.
The convolution operation, traced by hand
A convolutional layer does not connect every input pixel to every output neuron. Instead it slides a small weight matrix — a kernel (also called a filter) — across the image, computing one dot product per position. The same kernel is reused at every position, which is what "parameter sharing" means: a filter that has learned to detect a leaf-tissue boundary works identically whether that boundary sits at the top of the photo or the bottom.
Take a tiny 5×5 patch of a leaf-image channel, values already normalised to [0,1], where the left three columns are healthy tissue (0.2) and the right two columns are a necrotic brown lesion (0.9):
I =
0.2 0.2 0.2 0.9 0.9
0.2 0.2 0.2 0.9 0.9
0.2 0.2 0.2 0.9 0.9
0.2 0.2 0.2 0.9 0.9
0.2 0.2 0.2 0.9 0.9
Apply a 3×3 vertical-edge kernel, a classic edge detector, with stride 1 and no padding:
K =
-1 0 1
-1 0 1
-1 0 1
Output size follows the standard formula O = (W − F) / S + 1, where W = 5 (input width), F = 3 (kernel width), S = 1 (stride): O = (5 − 3)/1 + 1 = 3. So the output is a 3×3 feature map. Compute the top-left cell (kernel covering rows 0–2, columns 0–2, all inside the uniform healthy region):
each row: (-1)(0.2) + (0)(0.2) + (1)(0.2) = 0
sum over 3 rows = 0
output(0,0) = 0
Now slide the kernel one column right, to cover columns 1–3 (values 0.2, 0.2, 0.9 in every row — the kernel now straddles the tissue/lesion boundary):
each row: (-1)(0.2) + (0)(0.2) + (1)(0.9) = 0.7
sum over 3 rows = 2.1
output(0,1) = 2.1
Sliding one more column (covering columns 2–4: 0.2, 0.9, 0.9) gives the same arithmetic pattern, (-1)(0.2)+(0)(0.9)+(1)(0.9)=0.7 per row, summing to 2.1 again. Because every row of the input is identical, all three output rows are identical too. The full 3×3 output feature map is:
0 2.1 2.1
0 2.1 2.1
0 2.1 2.1
This is the entire point of a convolutional filter, made concrete: wherever the kernel sits entirely inside uniform tissue, the response is 0 — nothing interesting there. The instant the kernel straddles a sharp transition, the response spikes. A trained disease-detection network learns dozens of filters like this one, not by a human hand-designing the -1/0/1 pattern, but by gradient descent discovering whichever small weight patterns reduce the classification loss — some end up as edge detectors, others as colour-contrast detectors, others as texture detectors for the granular look of fungal spot lesions.
Parameter counting: why convolution scales
A real input has 3 channels (RGB), not 1, so a filter is really kernel_h × kernel_w × in_channels numbers plus one bias, and a convolutional layer typically applies many filters in parallel to learn many different features at once. The parameter count for a conv layer is:
params = (kernel_h × kernel_w × in_channels + 1) × num_filters
For a 3×3 kernel over a 3-channel RGB input with 32 filters: (3×3×3 + 1) × 32 = (27+1) × 32 = 28 × 32 = 896 parameters — regardless of whether the input image is 28×28 or 2800×2800. Compare that to the 301,184 parameters the dense layer needed for a single 28×28 colour (RGB) image above. The saving isn't a minor optimisation; it is what makes training on realistic photo resolutions possible at all, and it is why every serious image classifier — plant disease detection included — is built from convolutional, not dense, layers near the input.
Stacking layers: ReLU, pooling, and receptive field
After each convolution, a ReLU activation (f(x) = max(0, x)) is applied elementwise — it introduces non-linearity and, applied to our example output above, leaves it unchanged since all values are already non-negative. Next comes a pooling layer, almost always max pooling, which downsamples by keeping only the strongest response in each small window. Max pooling with a 2×2 window and stride 2 over four feature-map values {2.1, 0, 0, 2.1} keeps max(2.1, 0, 0, 2.1) = 2.1 and discards the rest. Two things happen simultaneously: the feature map shrinks (halving both dimensions, quartering the compute needed downstream), and the representation becomes slightly translation-invariant — a lesion edge detected one pixel to the left still triggers the same pooled output.
Stacking Conv→ReLU→Pool blocks repeatedly grows the network's receptive field — the region of the original image that influences one value deep in the network. A single 3×3 conv sees a 3×3 patch. After a 2×2 pool and a second 3×3 conv, that same output location has effectively "seen" an 8×8 region of the original image (RF = 3 after conv1; pool doubles the jump and RF becomes 3+(2-1)×1=4; conv2 adds (3-1)×2=4 more, giving RF=8). By the fourth or fifth block, a single number can depend on a large fraction of the whole leaf — enough to distinguish "a scattered scatter of small brown flecks" (early blight) from "one large expanding necrotic ring with concentric bands" (late blight), a distinction that genuinely requires seeing shape and extent, not just a local 3×3 patch.
From feature maps to a diagnosis: softmax and cross-entropy loss
After the last pooling layer, the feature maps are flattened into one long vector and passed through one or more dense layers, ending in an output layer with one neuron per disease class. Suppose the model is choosing between three classes — Healthy, Early Blight, Late Blight — and the final dense layer produces raw scores (logits) z = [1.2, 3.5, 0.3]. Softmax converts these into probabilities that sum to 1:
softmax(z_i) = e^(z_i) / Σ_j e^(z_j)
e^1.2 = 3.3201
e^3.5 = 33.1155
e^0.3 = 1.3499
sum = 37.7854
p(Healthy) = 3.3201 / 37.7854 = 0.0879
p(Early Blight) = 33.1155 / 37.7854 = 0.8764
p(Late Blight) = 1.3499 / 37.7854 = 0.0357
Verify with code — every line below is arithmetic you can check against the hand calculation above:
import numpy as np
logits = np.array([1.2, 3.5, 0.3]) # Healthy, Early Blight, Late Blight
probs = np.exp(logits) / np.exp(logits).sum()
print(np.round(probs, 4))
# [0.0879 0.8764 0.0357]
true_label = 1 # ground truth: Early Blight
loss = -np.log(probs[true_label])
print(round(loss, 4))
# 0.1319
The loss function used to train the network is cross-entropy: L = −ln(p_true). Here the model assigned 87.64% probability to the correct class, so the loss is small (0.1319). If instead the model had been confidently wrong — say it assigned only 2% probability to the true class — the loss would be −ln(0.02) = 3.91, nearly 30 times larger. This asymmetry is exactly what drives learning: gradient descent adjusts every convolutional filter and dense weight in the direction that shrinks this loss, batch after batch, until confident-and-correct becomes the network's default behaviour on the training distribution.
Building it: transfer learning instead of training from scratch
The architecture above, trained from randomly initialised weights, needs a very large labelled dataset to work well — plant disease research typically uses the PlantVillage dataset, roughly 54,000 leaf images spanning 14 crop species and 38 healthy/diseased classes, published by Mohanty, Hughes, and Salathé in 2016. But most real deployments — including on-device apps that must run offline in low-connectivity rural areas — use transfer learning instead: take a network like MobileNetV2, already trained on 1.4 million general-purpose photographs (ImageNet), freeze its early convolutional layers (which have already learned generic edge, texture, and colour-gradient detectors — useful for any photograph, leaves included), and retrain only a new classification head on the plant-disease data:
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras import layers, models
base = MobileNetV2(input_shape=(224, 224, 3),
include_top=False,
weights="imagenet")
base.trainable = False # freeze pretrained convolutional layers
model = models.Sequential([
base,
layers.GlobalAveragePooling2D(),
layers.Dense(128, activation="relu"),
layers.Dropout(0.3),
layers.Dense(38, activation="softmax") # 38 PlantVillage classes
])
model.compile(optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["accuracy"])
This works because the early layers of any well-trained CNN learn genuinely general-purpose features — edges, blobs, colour boundaries — the same kind of filter our hand-traced example computed. Only the last few layers need to specialise in "what a fungal lesion versus a bacterial spot versus a nutrient-deficiency chlorosis looks like," and that specialisation needs far fewer labelled examples to learn than the entire visual hierarchy would.
Why accuracy alone is the wrong scoreboard
Suppose a trained detector is evaluated on 200 field-collected leaf images: 150 healthy, 50 genuinely diseased (a realistic imbalance — most of a field is healthy at any given time). The model's confusion matrix on this test set:
Predicted Healthy Predicted Diseased
Actual Healthy 144 (TN) 6 (FP)
Actual Diseased 8 (FN) 42 (TP)
Accuracy = (144+42)/200 = 186/200 = 93% — which sounds excellent, until you compare it to the trivial baseline of predicting "healthy" for every single image: that baseline scores 150/200 = 75% accuracy while catching zero disease cases. The metric that actually matters for a farmer is recall on the diseased class — of the 50 truly diseased plants, how many did the model catch?
Precision = TP/(TP+FP) = 42/48 = 0.875 (87.5%)
Recall = TP/(TP+FN) = 42/50 = 0.84 (84%)
F1 = 2·P·R/(P+R) = 2(0.875)(0.84)/(0.875+0.84) = 0.8571 (85.71%)
A recall of 84% means 16% of diseased plants are missed entirely — eight infected plants per fifty, left untreated, potentially spreading the infection through the field before the next inspection. In agricultural screening, a false negative (missed disease) is typically far costlier than a false positive (a healthy plant flagged for a second look), so practitioners often deliberately lower the classification threshold to trade some precision for higher recall — accepting more false alarms in exchange for catching more real infections.
The misconception: the network is not looking at the lesion the way a pathologist does
A natural assumption is that a CNN trained to 99% accuracy on plant disease images has learned to recognise lesion shape and pattern the way a plant pathologist would — spotting the characteristic concentric rings of early blight or the water-soaked margins of bacterial spot. This is only partly true, and the gap matters enormously in practice. The PlantVillage dataset that most published results (including the original 99%+ accuracy figures) were trained and tested on consists of leaves photographed against plain, uniform lab backgrounds — a single leaf, often detached, on a grey or white sheet. When researchers tested PlantVillage-trained models on photographs taken in actual fields — leaves still on the plant, with soil, other leaves, shadows, and varying light in the background — accuracy fell dramatically, down to roughly 30% in the reporting researchers' own follow-up analysis.
The reason is that a CNN optimises purely to reduce the loss on its training distribution, with no built-in preference for "the visually meaningful reason" over "any statistically reliable shortcut." If every lab photo of a diseased leaf happens to sit on a slightly different shade of background sheet than every healthy-leaf photo — an artifact of how the dataset was collected, not a property of disease — the network is entirely capable of partly learning "background shade" as a predictive feature, alongside or even instead of lesion morphology, because doing so also reduces the training loss. It looks identical to genuine disease detection right up until you show it a photo whose background doesn't match its training assumptions. The fix is not a smarter architecture; it is training data that varies backgrounds, lighting, and plant orientation deliberately (data augmentation, and field-collected training images), so the only reliably predictive signal left for the model to key on is the lesion itself.
Active recall
Attempt these before reading the answers.
- A 32×32×3 RGB image passes through a conv layer with 16 filters of size 5×5, stride 1, no padding. What is the output feature map's shape?
- How many trainable parameters does that conv layer have?
- Using the confusion matrix in this chapter, compute the false negative rate (the fraction of truly diseased plants the model misses).
- Why does a model trained only on PlantVillage's plain-background lab photos perform far worse on photos farmers actually take in their fields?
- Why is recall usually prioritised over precision when screening crops for disease, and what is the cost of pushing recall higher?
- A softmax layer receives logits [0.5, 0.5, 0.5] for three classes. What are the resulting probabilities, and what does this output tell you about the model's confidence?
Answers
- Output width/height = (32 − 5)/1 + 1 = 28. With 16 filters, the output shape is 28×28×16.
- params = (5×5×3 + 1) × 16 = (75+1) × 16 = 76 × 16 = 1,216 parameters.
- False negative rate = FN/(TP+FN) = 8/50 = 16% — the same figure as (1 − recall), since recall was 84%.
- The lab photos share a uniform background that happens to correlate with the disease label purely by how the dataset was collected, not because background is biologically meaningful. The CNN, optimising only for training loss, can partly learn to use that background cue instead of (or alongside) actual lesion morphology. Field photos have different, cluttered, varying backgrounds that break this shortcut, so accuracy collapses even though the model never learned to reliably read lesions in the first place.
- A missed diseased plant (false negative) can spread infection and cause crop loss before the next check, while a false positive only costs a farmer a few minutes double-checking a healthy plant — the two error types have very different real costs. Lowering the classification threshold needed to predict "diseased" catches more true cases (raises recall) but also raises the false positive rate (lowers precision), so more healthy plants get flagged unnecessarily.
- Equal logits produce equal exponentials, so softmax gives
e^0.5/(3·e^0.5) = 1/3for each class: probabilities [0.3333, 0.3333, 0.3333]. This is the maximum-entropy distribution over three classes — the model is giving no class any preference at all, equivalent to random guessing, meaning at this point it has learned nothing useful for distinguishing the three diseases.
Think About It
Think about this: How would you explain image classification: building a plant disease detector to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind image classification: building a plant disease detector, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.