Bird-friendly glass guidance calls for exterior markings with no gap wider than 2 inches (50.8 mm). Dot treatments go on by hand, and one missing dot on a 40 mm grid opens the gap to about 61 mm. I built a phone check that measures those gaps and entered it in the OpenCV AI Competition 2026.
How it works
- A printed ChArUco sheet gives scale. CharucoDetector, findHomography (RANSAC) and warpPerspective flatten the photo to millimetres.
- Top-hat and black-hat filters plus findContours find the dots, keeping only blobs with roundness 0.80 or more and the right size.
- Glare and blur are marked as unreadable, so they never count as covered or as a gap.
- distanceTransform (DIST_C) gives the largest empty square, checked against 50.8 mm.
- An ordered rule table decides PASS, REPAIR (with a target) or RETAKE (with the reason). The next photo, a retake or a recheck after a repair, joins the same scan.
It runs on AWS Lambda arm64 behind a plain HTML phone page. Photos are processed in memory and never stored.
It still misses dots. In 4 of the 6 readable real photos, a top-row dot was missed against a bright sky reflection. So far it’s tested on one window and one dot product. I’d welcome ideas on separating dots from bright sky reflections.

Code and technical report: GitHub - Endokelp/Bird_safe_OpenCV · GitHub
Devpost: https://devpost.com/software/bird-safe-gap-check
first, I would recommend not posting AI slop. the AI tried to dress this up as a finished achievement.
Honestly, calling this AI slop stung a little. I printed the ChArUco sheet, hand made the dots, placed them on an actual window, and took the photos myself. I tested it myself and reviewed the code myself. That’s also why I know the tool isn’t finished. In 4 of 6 readable real photos it misses the top row of dots, and it can’t tell which side of the glass the dots are on. I did use AI to help write the code and refine the text, and I don’t believe that makes it slop. If I had written all of the code by hand, I would have been the limiting factor. Using AI let me put more effort into testing. If you have suggestions for the missed top-row dots, please share.
The README’s merged dot/reflection example (roundness 0.41) suggests checking where the candidate first connects to the reflection before changing the 0.80 cutoff. For the missed top-row crop and a correctly detected dot, save the rectified grayscale image, top-hat and black-hat responses, threshold masks, and contours before filtering. Overlay each contour and log its area, perimeter, and roundness. OpenCV defines black-hat as closing minus the input, so kernel size and the local background can change the response. Sweep the kernel size and threshold on those same crops to check whether the dot separates before the shape filter, and count false candidates in reflection-only areas. If it stays merged or ambiguous, keep that region unreadable/retake rather than treating a relaxed roundness cutoff as a confirmed dot.
Thanks, this is very helpful. I do agree that the 0.41 blob is due to a merge. I wouldn’t lower the 0.80 cutoff, since that would let reflections in: actual dots had a roundness of 0.88 or higher, while reflection blobs had 0.74 or less. I also think it would be useful to save the masks and contours from the missed crop and a found dot, and then try varying kernel size and threshold. Until the dot can be separated cleanly, the region stays marked as unreadable and asks for a retake.