Reading the ground from the air.
Week 2 of UAVs@Berkeley Software Ground School 2026, solved end to end: HSV color segmentation with object centers, SUAS target shapes, and a from scratch pipeline that stitches drone video into one map. In Python and C++17.
- stack
- Python 3.10+, OpenCV, NumPy, C++17
- vision
- HSV masks, CIELAB k-means, IoU shape templates
- mapping
- ORB/SIFT tracking, loop closure, bundle adjustment, multi band blending
- accuracy
- 0.36 px pose RMSE on the rough flight, down from 56.72 px
Pick one option. We did both.
The Week 2 skill booster covers computer vision for aerial imagery. Each option below lists the assignment as given and what the repo ships on top of it.
-
Option 1
Color Me Impressed
Split an image into one image per color with OpenCV HSV masking. Bonus: print the center of each colored object.
- Exact HSV partition: twelve bands, every pixel lands in exactly one color.
- Speckle removal and 8-connected components, so each object gets its own center.
- Palette discovery with k-means in CIELAB, k chosen by silhouette score.
- SUAS shape classification by rotation searched IoU template matching: 100% on 13 shapes across sizes and rotations.
-
Option 2
I'll be Needin' Stitches
Split a drone flight video into frames and stitch them into one image spanning the flight path, like the club did in competition.
- Flight simulator: a procedural voxel world and lawnmower survey with exact ground truth poses.
- Grid bucketed ORB/SIFT, symmetric ratio matching, keyframe tracking with relocalization.
- Loop closure and Levenberg Marquardt similarity bundle adjustment with complex number Jacobians.
- Brown and Lowe gain compensation, Laplacian pyramid multi band blending, and a benchmark against
cv2.Stitcher.
-
Extra challenge
Do it in C++
Solve the challenges with the OpenCV C++ API.
- C++17 ports of both options, built with CMake against OpenCV 4 or 5.
- CI compiles both binaries and runs parity tests against the Python implementation.
Color Me Impressed, running in this tab.
The named mode of colors.py, ported to JavaScript. The HSV conversion reproduces OpenCV's 8-bit fixed point math: across all 16,777,216 RGB colors it agrees with cv2.cvtColor exactly on hue and value and within one step on saturation. Twelve inclusive bands split the image, a 3x3 elliptical opening removes speckle, and 8-connected components give every object a center: the mean of its pixel coordinates.
Pick a test image or bring your own. Uploads are decoded and processed locally and never leave the browser.
Objects and centers
Largest first per color, like format_report.
Color layers
Twelve bands, no gaps, no overlaps
OpenCV stores hue as 0 to 179 (half degrees) and saturation and value as 0 to 255. Hue wraps, so red needs two ranges. Brown is dark orange and dark yellow, carved out by value at or below 150. Anything with saturation of 50 or less is black, gray or white by brightness alone.
Colors covering under 1% of the image are dropped unless they contain a real object, and blobs smaller than 0.08% of the image count as noise. That is how a thin white letter survives while JPEG fringes do not.
Not ported: shape classification. The Python and C++ versions also label each object (octagon, star, cross) by rotation searched IoU template matching. Where the browser table shows a shape, it comes from the precomputed Python report for that test image.
I'll be Needin' Stitches.
The club's competition footage is not public, so the repo makes its own: a seeded Minecraft style world and a lawnmower survey flight rendered to video, with the true camera pose of every frame. Stitching quality is then measured in pixels instead of eyeballed.
Pipeline
-
Sample
Keep every Nth frame, resize, and undistort with the radial model.
cv2.VideoCapture, frame % every -
Track
Grid bucketed features after CLAHE, symmetric ratio test, RANSAC similarity against the current keyframe. Lost frames are relocalized.
ORB or SIFT, 4 DOF RANSAC + LM -
Close loops
Predict which earlier passes overlap each keyframe, then verify the candidates with feature matching.
footprint overlap >= 25% -
Bundle adjust
Solve every similarity at once. Residuals live in frame pixels, so frames cannot shrink to cheat.
z → αz + β, LM + Huber IRLS -
Gain compensation
One exposure gain per frame from mean intensities of every overlapping pair.
Brown and Lowe, IJCV 2007 -
Blend
Low frequencies mix over wide seams, high frequencies over narrow ones.
Laplacian pyramid, written from scratch
Loop closures add edges between non consecutive keyframes. They are what bundle adjustment uses to pull accumulated drift back out.
Drag to remove the drift.
Chaining pairwise alignments compounds small errors: after five passes the sequential mosaic is off by 56.72 px RMSE (worst keyframe 159.65 px). Loop closure, bundle adjustment and undistortion bring it to 0.36 px.
Sequential chaining
Full pipeline
The flight path, recovered from pixels alone.
Every white outline is a keyframe footprint and the orange track joins their centers. No GPS, no IMU: the lawnmower pattern falls out of the image alignment.
Against cv2.Stitcher.
OpenCV's built in stitcher in SCANS mode was handed 67 frames of the calm flight and took 103.9 s to return the mosaic labeled cv2.Stitcher. This pipeline stitched the same flight in 20.1 s with 0.30 px pose RMSE and a ZNCC of 0.979 against the true world.
cv2.Stitcher67 frames, 103.9 s
This pipeline20.1 s, 0.30 px RMSE
Benchmark
Two simulated flights, stitched with progressively more of the pipeline switched on and scored against the simulator's true poses and world. Generated by python -m week02 benchmark docs/week02.
| Variant | Keyframes | Loop closures | Pose RMSE (px) | Max error (px) | ZNCC | Time (s) |
|---|---|---|---|---|---|---|
| Calm flight3 passes, light noise, no lens distortion | ||||||
| sequential chaining | 50 | 0 | 1.43 | 2.51 | 0.941 | 12.8 |
| + loop closure and bundle adjustment | 50 | 152 | 0.30 | 0.70 | 0.976 | 18.5 |
| + gain compensation and multi band blending | 50 | 152 | 0.30 | 0.70 | 0.979 | 20.1 |
cv2.Stitcher SCANS (67 frames) | finished, no poses to score | 103.9 | ||||
| Rough flight5 passes, barrel distortion k1=-0.06, motion blur, heavy noise, +/-18% exposure, +/-5% altitude | ||||||
| sequential chaining | 75 | 0 | 56.72 | 159.65 | 0.486 | 20.8 |
| + loop closure and bundle adjustment | 75 | 199 | 7.21 | 17.56 | 0.839 | 27.3 |
| + gain compensation and multi band blending | 75 | 199 | 7.21 | 17.56 | 0.867 | 25.5 |
| + lens undistortion (full pipeline) | 78 | 248 | 0.37 | 0.91 | 0.941 | 36.1 |
| full pipeline with SIFT | 78 | 254 | 0.36 | 0.63 | 0.947 | 74.7 |
- Pose RMSE and max error
- Error on a 5x5 grid of points in every keyframe, after a least squares similarity alignment to the true poses.
- ZNCC
- Zero normalized cross correlation between the mosaic and the true world, so blending and exposure count too.
- Time
- Wall clock seconds summed over the pipeline stages, single machine, same settings for every row.
The extra challenge, in C++17.
Both options are ported to OpenCV's C++ API, with the same bands, thresholds and pipeline stages as the Python modules.
color_me_impressed: HSV bands, object centers, IoU shape templates, JSON report.needin_stitches: keyframe tracking, loop closure, complex number bundle adjustment, gain compensation, multi band blending, orcv::Stitcherfor comparison.- One
CMakeLists.txt, builds against OpenCV 4 (apt) or OpenCV 5 (Homebrew).
$ cmake -S week02/cpp -B build/cpp -DCMAKE_BUILD_TYPE=Release
$ cmake --build build/cpp -j
$ ./build/cpp/color_me_impressed docs/week02/colors/targets.jpg --out out/targets
$ ./build/cpp/needin_stitches out/flight/flight.mp4 --k1 -0.06 --out out/stitched.jpg
$ pip install -e .
$ python -m week02 colors docs/week02/colors/targets.jpg --out out/targets
$ python -m week02 simulate out/flight --hard
$ python -m week02 stitch out/flight/flight.mp4 --k1 -0.06 --path-overlay out/path.jpg
$ python -m week02 benchmark docs/week02