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.

<span class="case-meta-label">Domain</span>
<span class="case-meta-value">Urban Tech / Sustainability</span>
<span class="case-meta-label">Stack</span>
<span class="case-meta-value">PyTorch · FastAPI · Next.js · PostgreSQL</span>
<span class="case-meta-label">Type</span>
<span class="case-meta-value">Concept + Prototype</span>
<span class="case-meta-label">Status</span>
<span class="case-meta-value">🔬 Prototype</span>

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.


Future Improvements


View on GitHub ↗ ← All Projects