Skip to content
Chif3n
5 min read

Edge AI on Limbe's Coast: Deploying Quantized YOLO Models on $35 Hardware for Marine Plastic Tracking

Deploying heavy PyTorch models on battery-constrained Raspberry Pi 4 units in tropical humidity fails within hours. How CoastClear quantizes models into INT8 ONNX runtimes for real-time beach debris classification.

When we won 3rd place with CoastClear at the national MTN YaMo innovation challenge, the demonstration ran on a high-spec MacBook with thermal fans blasting and a stable 50 Mbps fiber link.

When we took the same system down to the black sand beaches of Limbe (Down Beach and Batoke), that prototype disintegrated in 45 minutes:

  • Thermal Throttling: 32°C ambient coastal heat with 85% humidity made unoptimized PyTorch FP32 models overheat the Raspberry Pi 4 SoC past 82°C, cutting CPU clocks from 1.5 GHz down to 600 MHz.
  • Battery Drain: Running an unquantized model drew 14.8 Watts, draining our 10,000 mAh field battery in under 2.5 hours.
  • Zero Real-Time Bandwidth: Sending 1080p video streams back to a cloud GPU server in Douala failed because coastal 3G bandwidth hovered at 140 kbps with 600ms latency.

If edge environmental monitoring is going to work on African coastlines, the neural network must run entirely on-device at the edge, consuming under 4 Watts, without sending a single raw video frame to the cloud.


The Optimization: INT8 Post-Training Quantization

To hit our target of 12 frames per second (FPS) on a CPU without a dedicated GPU, we converted our custom-trained marine waste detection model (trained on 2,400 annotated coastal debris photos: PET bottles, fishing nets, plastic sachets, polyurethane foam) into an INT8 ONNX runtime:

  1. Weight & Activation Quantization: Dropped 32-bit floating-point weights (float32) to 8-bit signed integers (int8). Model weight size dropped from 28.4 MB to 7.1 MB.
  2. ONNX Runtime Engine: Bypassed heavy Python dependencies (torch, torchvision, cuda) and executed inference through native onnxruntime bindings compiled with ARM NEON SIMD instructions.
  3. Telemetry-Only Sync: Instead of streaming video, the edge node stores lightweight JSON telemetry: timestamp, GPS lat/long, detected class labels, confidence scores, and bounding-box coordinates. One full 8-hour beach scan compresses into a 42 KB sync packet.

Edge Inspection Codebase

Browse the multi-file architecture below. Switch between the edge detector loop, local telemetry circular buffer, device power config, and backend telemetry sync handler. All codes support folder expansion, scrolling, copy, and download:

coastclear-edgeedge/detector.py
"""
edge/detector.py
CoastClear edge inference engine running quantized ONNX models.
"""
 
import time
import json
import numpy as np
import onnxruntime as ort
from edge.telemetry.buffer import TelemetryBuffer
 
class CoastClearDetector:
CLASSES = ["pet_bottle", "sachet_water", "fishing_gear", "plastic_bag", "rubber_tire"]
 
def __init__(self, model_path: str = "models/marine_waste_int8.onnx", conf_threshold: float = 0.45):
# Configure ONNX Runtime session with ARM NEON CPU optimizations
opts = ort.SessionOptions()
opts.intra_op_num_threads = 4
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
 
self.session = ort.InferenceSession(model_path, opts, providers=["CPUExecutionProvider"])
self.input_name = self.session.get_inputs()[0].name
self.conf_threshold = conf_threshold
self.telemetry = TelemetryBuffer(max_records=5000)
 
def preprocess(self, raw_frame_rgb: np.ndarray) -> np.ndarray:
"""
Resizes 640x640, normalizes to 0-1, and formats to NCHW channel-first tensor.
"""
# Simulated fast array resize and normalization
tensor = raw_frame_rgb.astype(np.float32) / 255.0
tensor = np.transpose(tensor, (2, 0, 1))
tensor = np.expand_dims(tensor, axis=0)
return tensor
 
def detect_debris(self, frame: np.ndarray, gps_coords: tuple) -> list:
t0 = time.perf_counter()
inp = self.preprocess(frame)
 
# Execute edge inference
outputs = self.session.run(None, {self.input_name: inp})
inference_ms = (time.perf_counter() - t0) * 1000.0
 
detections = []
raw_boxes = outputs[0][0] # shape (N, 6): [x1, y1, x2, y2, score, class_id]
 
for box in raw_boxes:
score = float(box[4])
if score >= self.conf_threshold:
cls_id = int(box[5])
cls_name = self.CLASSES[cls_id] if cls_id < len(self.CLASSES) else "unknown"
item = {
"class": cls_name,
"confidence": round(score, 3),
"box": [round(float(coord), 1) for coord in box[:4]],
}
detections.append(item)
 
if detections:
self.telemetry.record(
timestamp=int(time.time()),
latitude=gps_coords[0],
longitude=gps_coords[1],
debris_count=len(detections),
detections=detections,
inference_ms=round(inference_ms, 1)
)
 
return detections
 

Results from Limbe Coast Trials

  • Inference Latency: Dropped from 284 ms down to 68 ms per frame on a standard ARM Cortex-A72 CPU core.
  • Power Envelope: Averaged 3.6 Watts, allowing an all-day survey on a compact solar-recharged power bank.
  • Thermal Performance: Operating temperatures stabilized at 54°C with a passive aluminum heatsink in 31°C direct sun.
  • Data Footprint: 3,800 detected plastic debris events across 4.2 km of coastline took only 126 KB to sync when the field truck returned to Limbe Town 4G coverage.
All writing

Comments

Comments coming soon. Set up Giscus on the repo to enable them.