- One sensor, a whole quarter
ECE 140's hardware track kept returning to the same part: an AMG8833 thermal camera (64 pixels of temperature at about 10 fps) wired to an ESP32-S3. Week after week the assignments changed what happened to those 64 numbers: print them, publish them, collect them, learn from them. This page replays that arc on real frames from the class dataset.
- Serial, then MQTT, then WebSockets
The frames first crawled up a USB cable as CSV lines (and the tutorial's delay() showed up as gaps in the stream). Then MQTT: the ESP32 publishes to a broker topic and a Python viewer subscribes, plus a request/response challenge where the laptop asks for one frame on a request topic and the ESP32 answers on a response topic. Finally WebSockets, so a browser labeling UI could stream frames both ways while you tapped p or e to label them.
- 22,054 frames from the whole class
Everyone's labeled frames landed in one dataset: 22,054 frames, 50.6% empty / 49.4% present, nearly balanced. The dataset-explorer analysis found a max-pixel-temperature boundary around 26–28 °C between the classes, and used exactly that to hunt likely mislabeled frames ('present' with max under 26 °C). Its histogram and mislabeled-frame figures are reproduced here.
- 76 features, one BFS
A max-temperature threshold works until the room warms up. The feature pipeline normalizes each frame against its own median and std, then adds intensity stats and eight spatial features, the biggest being the largest connected blob of pixels more than 3 °C above the median, found by breadth-first search: a person is one warm contiguous region, noise isn't. Watch the flood fill find it, cell by cell.
- Train, quantize, deploy: 6,672 bytes
A dense 76→32→16→1 network with L2 0.005, validated with GroupKFold by student so the model is never graded on a person it trained on. Then INT8 quantization: the 61,863-byte Keras model becomes a 6,672-byte TFLite flatbuffer, flashed as a C array. The panel runs float32 and INT8 side by side on the live frame (activations, scales, zero-points) and the ESP32's runInference loop (quantize → invoke → dequantize) classifies the stream.
- Side quest: the WiFi net-map
One week the ESP32 scanned WiFi instead: scan results POSTed to a FastAPI endpoint, rendered as a force-directed graph of SSIDs by channel and signal strength. The scans here are synthetic (real SSIDs and BSSIDs would map the neighbors) but the pipeline is the real one.
- Rebuilt for this page (2026-08-31)
The TypeScript ports (features, BFS, the INT8 kernels) were written with AI coding tools and tested against the real pipeline: fixtures generated by running David's features.py and the deployed model.tflite through the TFLite interpreter at build time. Privacy: contributor IDs are salted-hash prefixes, only ~500 of the 17,610 frames ship, and an 8×8 thermal frame identifies no one. One correction to the plan: the spec said 65 features, but the deployed assignment pipeline is 76, and 76 is what runs here.