← Work

Embedded systems · Summer 2025

Shade control and star calibration for an observatory camera array

The Galileo Project at the Harvard–Smithsonian Center for Astrophysics runs sky-monitoring cameras in weatherproof enclosures (“Daleks”) at several sites, including on the Sphere in Las Vegas. Most of the enclosure hardware and firmware was designed by the project’s engineering team. Two pieces are mine and are described here: a fail-safe in the shade controller, and the star-field calibration that solves where each visible-light camera points.

Role
Embedded systems engineering intern, summer 2025
Hardware
Arduino Mega 2560 controllers, servo shades with limit switches, BME280 / TMP117 / TSL2561 sensors on I²C, PWM fans, Raspberry Pi site hosts
Calibration
Python, NumPy, OpenCV, Astropy, the Yale Bright Star Catalogue
Code
Calibration pipeline on GitHub · enclosure firmware is in the project’s private repository
Looking up at the underside of a white camera enclosure mounted on black steel beams, with braided cable harnesses on its base plate and an open stainless junction box labeled Dalek Servos containing an Arduino board and labeled wiring.
An enclosure installed on the Sphere, seen from below. The open box labeled “Dalek Servos” holds the Arduino controller and the shade and fan wiring.

The system

Each enclosure holds its cameras behind windows, and each window has a servo-driven shade that can close over it. The infrared enclosures have eight; the visible-light “Mini Dalek” has four cameras pointed in four directions at about 50° elevation, with a shade each. An Arduino Mega 2560 inside the enclosure runs the shades and the climate: it gets the time over NTP, computes where the sun is from the time and the site’s location, checks a light sensor, and opens or closes the shades on that schedule. The same controller reads dome and electronics temperatures, runs fans with tachometer feedback and a heater, and confirms every shade position with a limit switch. A Raspberry Pi at each site reaches the Arduinos over USB serial, both for the console and for reflashing firmware.

Enclosure controller block diagramA Raspberry Pi at the site connects over USB serial to an Arduino Mega 2560 inside the enclosure. The Arduino gets time from NTP through an Ethernet shield, reads a light sensor and two temperature sensors over I2C, drives servo shades whose positions are confirmed by limit switches, and controls fans and a heater.Site hostRaspberry PiUSB serialconsole, reflashArduino Mega 2560sun position from timeand site location;light threshold; scheduleEthernet shieldNTP timeI²C sensorsTSL2561 lightBME280 dome temp, humidityTMP117 electronics tempShadesservo per camera window,limit switch per shadedriveconfirmClimatePWM fans with tachometer, heaterController design and firmware: the Galileo Project’s engineering team. The shade fail-safe below is my change.
Figure 1. One enclosure's control path as deployed.

A safe state for a controller that has lost the time

The shade schedule is only as good as the clock. If the site loses its network connection and the Arduino’s time is no longer set by NTP, it has no idea where the sun is. In the existing logic, that case closed the shades only if the light sensor was also over its threshold, so the shades’ state depended on one sensor reading at the moment the clock was lost.

I changed the rule so that losing time sync on its own closes every shade, one at a time, and reads back each shade’s limit switch to confirm it actually closed. A camera loses some observing time while the clock is down, but it can’t be left open to the sun because of a missed sync. The change covers the three sketches that run eight-shade and four-shade enclosures.

if clock not set by NTP:           # sun position unknown
    for each shade:
        close it
        confirm with its limit switch
The rule, simplified. The firmware itself is in the project's private repository. The change is on a branch there and hasn't been merged into the main sketches yet.
A white dome-shaped enclosure on a lab bench with four camera windows, two labeled N1 and W4, and triangular shades on hinges. Beside it are an Arduino Mega with a green screw-terminal shield and a relay, a Raspberry Pi in a black case connected to it by USB, a fan-cooled single-board computer, and a multimeter.
A visible-light Mini Dalek on the bench at the CfA: the Arduino Mega with its terminal shield and relay (front), the Raspberry Pi it talks to over USB (left), and two of the four camera windows with their shades.

Solving each camera’s orientation from the stars

To turn a pixel into a direction in the sky, the project needs each camera’s orientation in world coordinates. The intrinsic calibration (focal length, principal point, lens distortion) was already known for the visible cameras; the rotation of each camera was not. Stars are an ideal target for this: their directions at a given time and place are known very precisely, and they sit at infinity, so only rotation matters.

The pipeline, per camera:

  1. Detect stars. Subtract a per-camera median frame to remove hot pixels and fixed glow, high-pass the image with a wide Gaussian, normalize by a robust noise estimate (1.4826 × the median absolute deviation), and keep local maxima above 6σ. Each star’s position is a flux-weighted centroid over a 7 × 7 window, and anything whose peak holds more than 60 % of the flux is rejected as a hot pixel.
  2. Turn pixels into rays. Undistort with the known intrinsics and convert each detection to a unit vector in the camera frame.
  3. Model the sky. For the frame’s capture time, compute the altitude and azimuth of every star brighter than magnitude 5.5 in the Yale Bright Star Catalogue at the site, including atmospheric refraction, and keep the bright ones in the camera’s rough field of view.
  4. Match by geometry. The angle between two stars doesn’t depend on how the camera is rotated, so matching needs no initial guess. For every pair of detected stars, every catalogue pair with the same separation (within 0.3°) proposes a rotation. Each proposal is scored by how many other stars it lines up within 0.4°, and the best one is refined by re-solving on its inliers.
  5. Solve and pool. The rotation that best aligns matched pairs is Wahba’s problem, solved with the SVD (Kabsch) method. Frames with at least five matches and a median error under 15 px are pooled for a final solve, and the reprojection error is reported as its 50th and 90th percentiles.
calibrate_all.pylines 40
kab = lambda A,Bm:(lambda U,S,Vt: Vt.T@np.diag([1,1,np.sign(np.linalg.det(Vt.T@U.T))])@U.T)(*np.linalg.svd(A.T@Bm))
Kabsch in one line: SVD of the cross-covariance of the two sets of unit vectors, with the sign term preventing a reflection when the determinant comes out negative. It solves both the two-star proposals and the final pooled fit. Source: calibrate_all.py.

Results

Three of the four cameras at one of the Las Vegas sites solved:

CameraAzimuthElevationReprojection error (median)
North48.5°50.8°≈ 9 px
South225.1°49.1°≈ 6 px
West315.1°52.7°≈ 9 px
Eastnot solved: too few stars in its frames

The three solutions agree with each other. Elevations are within 3° of the 50° design, and the azimuths are about 90° apart but rotated roughly 45° from the nominal compass directions, which points at how the whole enclosure was mounted rather than at any single camera. With focal lengths around 2,230–2,380 px, a 6–9 px median error is roughly 0.15–0.25° on the sky near the image center.

A larger camera enclosure on a bench with a side panel removed, showing a dense harness of labeled wires running to an Arduino with a screw-terminal shield, a relay, and a large fan duct. A shade housing numbered 4 sits above a small half-dome sensor window.
Inside a larger enclosure with a side panel removed: the Arduino and relay, with the harness labeled by Arduino pin.