My family and I recently moved into a new house in the center of our little college town. We love it, but our new location also puts us next to a busy residential street. All too often passersby would tear through well above the posted 25 mph limit. It brought out my latent grumpy side.
Being a data-oriented guy, I wanted to have some hard numbers for how many folks were speeding and I wasn’t going to spend hundreds of dollars on a radar system. So instead I threw together a web camera, some simple video processing, and anomaly detection to make a system for tracking vehicle speeds. The diagram below shows the process from a high level, but I’ll also dive into a few of the details.
I’ve previously toyed with combining videos and BigML’s anomaly detection (an extended variety of isolation forests) as a way to do motion detection. By tiling a video and building an anomaly detector for each tile, I made a motion detector that could disregard common movement. For example, in this video the per-tile detectors don’t trigger for the oscillating fan, but they do for the nefarious panda interloper (on loan from my daughter).
To do this I extracted features for each tile, such as the average red, green, and blue pixel values (shout-out to OpenIMAJ for making this easy). This gave me tile-specific datasets where each row represents a video frame, like so:
|Red Mean||Green Mean||Blue Mean||Gray Mean||Gray StdDev|
Once collecting data in a row/column form, it’s easy to create an anomaly detector with the BigML language bindings. I do my hobby projects in Clojure, so the BigML Clojure bindings let me transform the data above into an anomaly detection function with only this snippet of code:
|(:require (bigml.api [core :as api]|
|[source :as source]|
|[dataset :as dataset]|
|[anomaly-detector :as anomaly-detector]|
|[anomaly-score :as anomaly-score])|
|(clojure.data [csv :as csv])))|
|(defn data->detector [data]|
|(-> (source/create data)|
|(anomaly-detector/create :forest_size 64)|
|;; A tiny example...|
|(-> (slurp "https://goo.gl/79KsJ4")|
|(println (my-detector [0.5 0.45 0.4 0.44 0.01]))|
The tiny example above loads the data from the previous gist, builds an anomaly detector as a Clojure function, and uses that function to score a new point. Scores from BigML anomaly detectors are always between 0 and 1. The stranger a point, the larger the score. Generally scores greater than 0.6 are the interesting ones. The green highlighted tiles in the panda example represent scores above 0.65.
So I took this tiling+detectors approach and applied it to video of cars passing my house. My intuition was that while tracking cars can be tricky, learning the regular background should be easy. Then all I’d need is to track the clumps of anomalies which represented the cars.
Instead of tiling the entire video, I only tiled two rows. Each row captured a vehicle lane. I tracked the clumps of anomalies and timed how long it took them to sweep across the video. Those times let me estimate vehicle speeds.
By tracking clumps of anomalies the system is more robust to occasional misfires by individual tile detectors. Also, as expected, the detectors helped ignore common motion like the small tree swaying in the foreground.
An approach like this is far from perfect. It can be confused by things like lane changes, bicycles, or tall trucks (which can register as two vehicles or occlude other cars).
Nonetheless, I was pleasantly surprised how well it did given the simplicity. With occasional retraining of the detectors, it also handled shadows and shifting lighting conditions. In some cases it tracked vehicles even when I had a hard time finding them. There is, believe it or not, a car in this image:
So I had a passable vehicle-counter/speed-detector using a webcam. To culminate the project, I collected vehicle speeds over a typical Saturday afternoon. The results surprised me.
I expected speeders to be much more common than they actually were. In fact, the significant speeders (which I deemed as 30+ mph) made up only about 3% of the total. So I’ve done my best to lose the grumpiness. Without the data, I’d just be one more victim of confirmation bias.
For the Clojure-friendly and curious, feel free to check out the project on GitHub.