Case Study · Computer Vision · IoT · Systems Design
Sortify — Smart Waste Management
An ML-powered waste monitoring system that uses computer vision to classify waste types and IoT sensors to track fill levels — enabling smarter, data-driven collection in urban environments.
The Problem
Kampala and other rapidly growing African cities face a critical waste management crisis:
- Overflowing bins create health and environmental hazards
- Inefficient routes — collection trucks follow fixed schedules regardless of fill levels
- Mixed waste — recyclables contaminate organic waste, reducing recovery rates
- No real-time visibility — city authorities have no live data on bin status
Sortify’s mission: Give waste management operators real-time intelligence so they can collect smarter, route efficiently, and improve sorting outcomes.
System Architecture
The system is composed of four integrated layers:
[Camera + Sensor Module]
↓
[Edge Inference — PyTorch ONNX]
↓
[FastAPI Backend — REST + WebSocket]
↓
[Next.js Dashboard — Real-time map]
Components
| Layer | Technology | Role |
|---|---|---|
| Edge device | Raspberry Pi 4 | Runs lightweight CV model |
| CV model | PyTorch (MobileNetV3) | Waste type classification |
| Fill sensor | Ultrasonic HC-SR04 | Measures bin fill level |
| Backend | FastAPI + PostgreSQL | Data ingestion + API |
| Frontend | Next.js + Mapbox | Live operator dashboard |
The ML Model
Dataset
A custom dataset was assembled from: - TrashNet (public, ~2500 images, 6 classes) - Additional scraped images for African waste context (~800 images) - On-device photos from pilot bins (~300 images)
Classes: plastic · glass · metal · paper · organic · e-waste
Model — Transfer Learning with MobileNetV3
import torch
import torch.nn as nn
from torchvision import models, transforms
# Load pretrained MobileNetV3-Small (efficient for edge deployment)
model = models.mobilenet_v3_small(pretrained=True)
# Replace classifier head for 6-class output
model.classifier = nn.Sequential(
nn.Linear(576, 256),
nn.Hardswish(),
nn.Dropout(0.3),
nn.Linear(256, 6)
)
# Training transforms
train_transforms = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(brightness=0.3, contrast=0.3),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])Training
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
criterion = nn.CrossEntropyLoss(weight=class_weights.to(device))
optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = CosineAnnealingLR(optimizer, T_max=30)
for epoch in range(30):
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
scheduler.step()Export to ONNX for Edge Deployment
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, "sortify_model.onnx",
input_names=["image"],
output_names=["logits"],
opset_version=13)Backend API
from fastapi import FastAPI, UploadFile, File
from sqlalchemy.orm import Session
import onnxruntime as ort
import numpy as np
app = FastAPI()
session = ort.InferenceSession("sortify_model.onnx")
CLASSES = ["plastic", "glass", "metal", "paper", "organic", "e-waste"]
@app.post("/classify")
async def classify_waste(file: UploadFile = File(...)):
"""Classify waste type from bin camera image."""
img = preprocess_image(await file.read())
logits = session.run(None, {"image": img})[0]
probs = softmax(logits[0])
return {
"class": CLASSES[np.argmax(probs)],
"confidence": float(np.max(probs)),
"probabilities": dict(zip(CLASSES, probs.tolist()))
}
@app.post("/bin/{bin_id}/status")
async def update_bin_status(bin_id: str, fill_percent: float, db: Session):
"""Update bin fill level from IoT sensor reading."""
bin_record = db.query(Bin).filter(Bin.id == bin_id).first()
bin_record.fill_level = fill_percent
bin_record.updated_at = datetime.utcnow()
db.commit()
if fill_percent >= 85:
await trigger_collection_alert(bin_id)
return {"status": "updated", "alert_triggered": fill_percent >= 85}Results
<div class="result-number">94%</div>
<div class="result-label">Classification accuracy</div>
<div class="result-number">1.8s</div>
<div class="result-label">Inference on Pi 4</div>
<div class="result-number">-40%</div>
<div class="result-label">Unnecessary collections</div>
<div class="result-number">Real-time</div>
<div class="result-label">Dashboard latency</div>
Hardest class: E-waste (88% accuracy) — visually diverse and underrepresented in training data.
Insights & Challenges
Lighting variability was the biggest real-world challenge. Outdoor bins in direct sunlight vs. shade produced very different image distributions. Augmenting with ColorJitter and contrast normalization reduced this gap significantly.
Edge constraints forced a rethink: the original ResNet-50 model was 98MB and ran at 12 seconds on Pi 4 — unusable. Switching to MobileNetV3-Small (3.8MB, ONNX) brought inference to 1.8 seconds with only a 3% accuracy drop.
Sensor fusion — combining visual classification with fill-level data let the API prioritise collections: a bin full of organic waste was escalated higher than one full of paper.