The Sun Project: Teaching a Camera to Catch the Sunrise
TL;DR — a self-calibrating camera now spends every morning trying to catch the first shadow of a tree at first light. Day one ended in a bug and a miss; the geometry is now right, and the logs will tell the truth from here.
Update, September 12, 2026: The tracker is now paused. Two weeks of operation made the limits of this v1 measurement strategy plain: a handful of spot readings per morning, each carrying a few degrees of noise, cannot resolve a signal that drifts roughly half a degree per day — and readings taken at different points of the sun's morning arc don't compare fairly, even before accounting for how the tree's own shadow changes with the light. Version 1 is tabled with its full logs preserved. A more robust version 2 is planned, with a revamped measurement strategy: measure the sun's whole arc across each morning instead of spot-checking it, fit each day's readings into a single point with an honest quality score, and schedule captures by solar geometry rather than trigger luck. The sweep will come back when the instrument deserves it.
I have a camera pointed at a tree. Not for security, not for weather — to catch the exact moment the tree's shadow first appears across the yard at sunrise. The house sits on the far side of a hill, so real daylight-landing-on-the-tree comes roughly two and a half hours after astronomical sunrise. I wanted a machine to be there for it every single morning, and to recognize it when it happened.
The goal: measure the sweep
Catching first light is the daily mechanic — it's how the project earns its data points. Each verified measurement pins down where the sun rose that morning: a compass heading read from the tree's shadow line. Across a year, those headings trace the sunrise's slide across the eastern sky, from its most northerly heading at the summer solstice to its most southerly at the winter solstice. That span, in degrees, is the sweep — the number this project exists to collect.
One year establishes the method; the payoff comes as sweeps accumulate. Each completed sweep is one year's data point, and the point of collecting them is the comparison: does the sunrise reach further north this summer than last? Does the whole band shift, expand, or shrink? Measured at the same tree with the same instrument logic, year after year, the differences between sweeps become the dataset — the record of how the sunrise's path over this house changes over the years.
What the measurements don't account for
These are measurements of a real tree in a real yard, not an observatory, and they don't fully correct for the things that will drift underneath them. The house sits on the far side of the hill in relation to the sunrise, so every reading is a local first light, not the astronomical one. Ground movement can shift what "fixed" means. The tree grows, changing the shadow it casts from year to year. The camera — and the images it captures — degrade as it ages. And the yard will supply other factors nobody has thought of yet. None of these invalidate the sweep; they just mean the year-to-year record carries the instrument along with the sky. Part of the fun is finding out which is which.
Why "is it dark" doesn't work
The tree is a big fixed dark mass, and it's dark all day — its darker half never stops being darker than the yard around it. So the naive signal ("a dark region is present") fires at 2am and means nothing. The robust signal is the scene becoming illuminated: the relative darkness of the shadow region versus the lit ground sits near zero at night (everything is dark, the camera auto-exposes) and climbs sharply the moment real daylight arrives. That's the trigger — and the catcher self-calibrates, taking its baseline from its own predawn readings instead of assuming anything about the scene.
How it runs
A small daemon (sun_catcher.py) wakes every five minutes. Within a window from 30 minutes before sunrise to three hours after, it grabs a frame from the RTSP stream, computes the relative darkness in the ground region, and compares it against the predawn baseline. First light is declared only when the signal clears the baseline by 20 percentage points for two consecutive cycles, and the sun is actually above the horizon. Every in-window frame is kept for tuning; every cycle is logged to CSV so the thresholds can be tuned against real data instead of guesses.
There's a subtlety the code has to handle: a static dark wall in the far corner of the frame dominates the dark reference and would mask the tree's shadow entirely. The metric detects that vertical wall and excludes it from the measurement region before computing anything.
Day one: a bug, then a miss
The catcher went live on August 30, and day one was honest about what first versions are like. The very first run threw a TypeError before the window ever opened. Then, because a state flag had already been flipped, the daemon spent the afternoon convinced the day was handled. By the time the bug was understood, the detection window had passed and the log recorded the outcome without ceremony: missed: window passed, no first light.
No sunrise was harmed. But it was a good reminder that the first job of an automated watcher is to survive its own software.
The calibration detour
Before the catcher can be trusted, the camera geometry needed to be right. The detector's original orientation estimate had misidentified the tree's fixed dark mass as the shadow and steered the whole approach wrong. Ground truth from a compass-and-eyeballs session (August 30) superseded it: camera bearing from the tree at 257°, view direction 112.5°, and the sunrise shadow lands lower-left in the frame, toward the camera. The fence line, at 135°, became the reference azimuth for measuring absorbed into calib.json permanently.
A companion script (measure_day.py) extracts the shadow line from a frame and computes where the sun actually stood at that moment. Its first test shot, at 9:00am August 30, measured the sun at 102.5° azimuth versus 106.8° from the GPS ephemeris — close enough to be encouraging, and a real anchor for the tuning loop.
Where it stands
- The daemon is alive and idle-waiting for its next window each morning. (Paused as of September 12, 2026 — see the update at the top.)
- Calibration is grounded in real geometry, not detector guesses.
- The tuning knobs (
MARGIN_REL,CONFIRM_N, predawn window) are now testable against a real missed morning.
The next sunrise window is the next report card. If the algorithms are wrong, the logs and saved frames will say exactly where.
September 8 update: the sweep becomes the headline number
The homepage widget now leads with the sweep — the goal described in The goal: measure the sweep above. How the widget's two numbers are counted:
- Days online — mornings with at least one captured in-window cycle (
predawn_baselineorsun_uprows; missed and force diagnostics don't count). As of this update: 6 mornings, August 31 – September 8. - Sweep 2026 — the most-southerly minus most-northerly verified sunrise heading within the current sweep-year. Verified headings come from
measure_day.pyshadow-line sessions: 102.5° on August 30 and 111.6° on August 31, so the sweep stands at 9.1° — small by design, and it will grow until the winter solstice fixes the year's full sweep.
Sweep-years run from one winter turning point to the next: when a measurement shows the sunrise has reached its most southerly heading and is beginning to sweep back north, that measurement closes the current sweep and starts the next one, which takes the label of the year holding most of its measurements. Each winter adds one more year to compare against.