A computer vision project for classifying white blood cells, detecting unusual images, and explaining model predictions.
I started this project as a simple image classification task, but later added transfer learning, anomaly detection with a VAE, Grad-CAM, and a small segmentation experiment with U-Net.
The project currently works with four blood cell classes:
- Eosinophil
- Lymphocyte
- Monocyte
- Neutrophil
This is an educational project, not a medical diagnostic tool.
The main steps of the project were:
- explored the dataset and checked class balance;
- trained a simple CNN as a baseline;
- improved the CNN with augmentation and regularization;
- tested ResNet-18 and EfficientNet-B0 with transfer learning;
- trained a VAE for anomaly detection;
- combined the classifier and VAE into one hybrid pipeline;
- added Grad-CAM to see what image regions affected the prediction;
- made a small U-Net segmentation experiment using automatically generated masks;
- added a Streamlit interface and deployment files.
The dataset contains microscope images of four white blood cell types.
| Class | Training images |
|---|---|
| Eosinophil | 2,497 |
| Lymphocyte | 2,483 |
| Monocyte | 2,478 |
| Neutrophil | 2,499 |
Dataset split used in the notebook:
| Split | Images |
|---|---|
| Full training source | 9,957 |
| Training subset | 7,965 |
| Validation subset | 1,992 |
| Test set | 2,487 |
The dataset is almost perfectly balanced, so I did not use class weights or oversampling.
The original image size is around 240 x 320, but I resized images for training:
128 x 128for classification;64 x 64for the VAE.
The dataset itself is not included in this repository.
For the classification models I used:
- resize;
- tensor conversion;
- normalization;
- random horizontal flip;
- random vertical flip;
- random rotation;
- other basic augmentation.
The main reason for augmentation was to reduce overfitting. The first CNN was learning the training set quite well, but its test result was much worse.
The first model was a simple CNN with convolution, ReLU, max pooling, and fully connected layers.
It gave me a starting point, but it overfitted and confused visually similar classes, especially Eosinophil and Neutrophil.
After that I added:
- more convolutional layers;
- batch normalization;
- dropout;
- stronger augmentation.
This improved test accuracy from about 68% to 87%.
I tested two options:
- frozen pretrained backbone;
- full fine-tuning.
The frozen version did not work very well, while fine-tuning gave a much better result.
EfficientNet-B0 was also tested with a frozen backbone and with full fine-tuning.
The fine-tuned version gave the best validation result, so I used it as the final classifier.
I trained a convolutional Variational Autoencoder on the known blood cell images.
The idea is simple: if the VAE cannot reconstruct an input image well, the image may be unusual or outside the training distribution.
The anomaly threshold was calculated as:
threshold = mean reconstruction error + 3 * standard deviation
Values from the notebook:
| Statistic | Value |
|---|---|
| Mean reconstruction error | 0.0137 |
| Standard deviation | 0.0023 |
| Threshold | 0.0207 |
The final logic is:
Input image
|
v
VAE reconstruction
|
|-- error > threshold --> anomaly
|
|-- error <= threshold
|
v
EfficientNet-B0
|
v
class + confidence
So the model does not always force an image into one of the four classes. First, it checks whether the image looks similar to the data used during training.
I used Grad-CAM to visualize the image regions that influenced the final prediction.
The notebook shows:
- original image;
- heatmap;
- heatmap overlay;
- predicted class;
- confidence score.
This part was useful for checking whether the classifier was looking at the cell area and not only at the background.
I also made a small U-Net segmentation experiment.
There were no manually labeled masks, so I generated pseudo-masks with:
- grayscale conversion;
- Otsu thresholding;
- morphological opening;
- morphological closing.
The U-Net was trained with BCE and Dice loss.
The result was very high, but this should be interpreted carefully because the model was learning to reproduce automatically generated masks, not expert annotations.
| Model | Split | Accuracy | Macro F1 |
|---|---|---|---|
| Baseline CNN | Test | 0.680 | 0.690 |
| Improved CNN | Test | 0.870 | 0.870 |
| ResNet-18, frozen | Validation | 0.550 | 0.550 |
| ResNet-18, fine-tuned | Validation | 0.978 | 0.980 |
| EfficientNet-B0, frozen | Validation | 0.754 | 0.750 |
| EfficientNet-B0, fine-tuned | Validation | 0.998 | 1.000 |
U-Net experiment:
| Metric | Value |
|---|---|
| Training samples | 800 |
| Validation samples | 200 |
| Best validation Dice | 0.9990 |
The EfficientNet result is very high, so I would not treat it as final proof of real-world performance. It still needs a strict external test set and a duplicate check.
cellvision-ai/
├── Dockerfile
├── best_model_efficientnet.pth
├── hybrid_model.py
├── hybrid_model_config.json
├── main.py
├── project.ipynb
├── railway.toml
├── requirements.txt
├── streamlit_app.py
├── vae_anomaly.pth
└── README.md
Main files:
project.ipynb— EDA, training, evaluation, Grad-CAM, and segmentation experimentsbest_model_efficientnet.pth— trained classifiervae_anomaly.pth— trained VAEhybrid_model.py— inference logichybrid_model_config.json— class names and anomaly thresholdstreamlit_app.py— Streamlit interfaceDockerfile— Docker configurationrailway.toml— Railway deployment configuration
Clone the repository:
git clone https://github.com/weakyweaky7/cellvision-ai.git
cd cellvision-aiInstall dependencies:
pip install -r requirements.txtRun the Streamlit app:
streamlit run streamlit_app.pyOr open the notebook:
jupyter notebook project.ipynbBuild the image:
docker build -t cellvision-ai .Run it:
docker run --rm -p 8501:8501 cellvision-aiThen open:
http://localhost:8501
- Python
- PyTorch
- Torchvision
- OpenCV
- NumPy
- pandas
- Matplotlib
- Seaborn
- scikit-learn
- Streamlit
- Docker
- Railway
- EfficientNet was evaluated on the validation split, not on an external dataset.
- The VAE threshold is experimental and was not tested on a labeled anomaly dataset.
- U-Net was trained on pseudo-masks, not on manually annotated masks.
- Model confidence is not calibrated probability.
- Results may change on images from another microscope or laboratory.
- The dataset should be checked for duplicate and near-duplicate images.
- test the classifier on external data;
- check train and validation images for duplicates;
- calibrate probabilities;
- evaluate anomaly detection on real out-of-distribution examples;
- use manually annotated masks for segmentation;
- split training and inference code into separate modules;
- add tests and experiment tracking.
This repository was made for learning and portfolio purposes. It should not be used for diagnosis or any real medical decision.