I trained a CNN to tell parasitized blood cells from uninfected ones, got 95.28% test accuracy, deployed it on Streamlit, and felt pretty good about it.
Then I uploaded a photo of a flower to my own app. It told me the flower was Parasitized.
That one result changed what the project was about. This post covers the model, the fix, and the part that still isn't solved.
Live app: malariacellchecker.streamlit.app
Code: MwangiJoseph-25/Malaria_Cell_Checker
This is a learning project, not a diagnostic tool. Please don't use it for medical decisions.
The classifier
The data is the malaria dataset from TensorFlow Datasets: 27,558 images of single red blood cells, labelled parasitized or uninfected.
import tensorflow_datasets as tfds dataset, info = tfds.load( "malaria", split="train", with_info=True, as_supervised=True ) print("Number of images:", info.splits["train"].num_examples) print("Classes:", info.features["label"].names) I resized everything to 128 × 128, scaled pixels to 0–1, and split the data into train, validation and test sets. The model is a plain convolutional network.
The final CNN used two convolutional layers, with 32 and 64 filters respectively, each followed by max pooling. I resized the images to 128 × 128 pixels and normalized pixel values to the range 0–1. I did not implement data augmentation in this version. The model used Adam optimization, binary cross-entropy loss, a 128-unit dense layer, and dropout of 0.3. I trained it for up to 15 epochs with early stopping based on validation loss, and achieved 95.28% accuracy on the held-out test set.For a first model that felt solid, and I moved on to the app.
The flower problem
A binary classifier only ever answers "A or B". It has no way to say "neither". Give it a picture of a cat and it will pick whichever class the pixels lean toward, and it will often sound confident about it.
My test accuracy was measuring one thing: how well the model separates two kinds of blood cell when it's handed a blood cell. It said nothing about what happens when someone uploads their lunch.
For a tool that touches anything health-related, even as a demo, that's a bad failure mode.The moment of realization came when I uploaded a photo of a Red Bull racing car and the model confidently classified it as Parasitized. That unexpected result made me realize that the model would classify almost any image as either parasitized or uninfected, even when it wasn't a blood-cell image at all.
Adding a gatekeeper model
My fix was a second model whose only job is to answer "does this look like a blood-cell image at all?" before the malaria classifier gets to see it.
For training data I built a balanced set of 10,000 images:
| Source | Images |
|---|---|
| Malaria dataset (2,500 parasitized + 2,500 uninfected) | 5,000 |
| Natural Images dataset (airplanes, cars, cats, dogs, flowers, fruit, motorbikes, people) | 5,000 |
I didn't train a network from scratch. I used a pretrained MobileNetV2 as a frozen feature extractor and trained a small classifier on top of its features. For a two-way "cell or not cell" problem this is cheap and quick to train.
The result: 100% on validation and 100% on the test set.
I don't trust that number, and you shouldn't either. Both classes came from the same sources the model trained on, and the "not a cell" class is eight categories of everyday photos that look nothing like a microscope slide. The model learned that cells look like cells and cars look like cars. That's an easy task, and it doesn't prove the detector works on microscopy images from anywhere else.
How the app works
The Streamlit flow is:
- The user uploads a file.
- The app checks it's a readable image within the size limit.
- The gatekeeper decides whether it looks like a blood-cell image.
- If not, the user gets a message asking for a suitable microscopy image.
- If it passes, the malaria classifier returns Parasitized or Uninfected with its scores.
It also shows an image preview, a progress indicator, and a disclaimer. I spent a surprising amount of time on text contrast and layout, because a demo people can't read is a demo nobody tries.

Deployment was Streamlit Community Cloud connected to a GitHub repo.
Where it still breaks
Flowers and cars get rejected, and the dataset's blood-cell images get through. Good.
But when I tested real malaria microscopy images from other sources, the gatekeeper rejected some of them. Different staining, magnification or camera, and it decided they weren't cells.
So I've swapped one failure for another. Before, it accepted things it shouldn't. Now it sometimes refuses things it should accept. The cause is the same in both cases: my training data is too narrow. The "cell" class is one dataset's idea of a cell, and the "not cell" class is too easy.
What I'd tell myself at the start
- Check what your test set can't tell you. 95% on in-distribution data says nothing about out-of-distribution inputs.
- Validate inputs before the model sees them. I now treat this as part of the product, not an add-on.
- Be suspicious of 100%. When I see it, I ask what the test set is failing to challenge.
- Hard negatives matter more than more easy ones. Another 5,000 cat photos won't help. Microscope images of other things will.
Next steps
- Add microscopy images from more sources, with different stains, magnifications and image quality.
- Add harder negatives: other microscope slides, diagrams, screenshots.
- Measure false rejections and false acceptances on a fully independent test set.
If you know of public microscopy datasets that would suit this, I'd really like to hear about them in the comments. The same goes if you've dealt with out-of-distribution inputs in a small classifier and found a better approach than a separate gatekeeper model.

