A Landing Pad That Recharges the Drone on Its Own
A camera looks up, spots the drone, and tells it where the pad is.
A small drone flies for minutes, not hours. If you want it to do real work, patrol a campus, inspect a roof, carry sensors around a lab, someone has to keep swapping batteries. The DronePort idea removes that someone: the drone comes back to a pad, lands on it precisely, and starts a charging cycle on its own.
The DronePort_Controller repository from our lab at RBCCPS, IISc tackles the hardest-to-get-right part of that loop first: seeing the drone from the pad and working out exactly where it is. Its README puts the goal in one line, helping the drone land and start the charging cycle so operations last longer. This post walks through how the code does the seeing part, so you can build your own.
The setup
There are two pieces of hardware:
- An OpenMV camera sits in the pad, looking straight up at the sky.
- A single-board computer (SBC) next to it reads what the camera reports over USB serial and turns it into a position in metres.
The drone's own height comes from its flight stack through ROS (MAVROS's /mavros/global_position/local topic). Pixel coordinates from the camera plus height from the drone are enough to say "you are 30 cm east and 10 cm north of the pad."
Step 1: Teach the camera what an empty sky looks like
The camera script (OpenMV/frame_difference.py) uses the oldest trick in computer vision: frame differencing. On startup it switches to grayscale, crops to the central 240 by 240 pixels, turns off auto white balance and auto gain (so the image doesn't drift on its own), and saves one snapshot as the background.
From then on, every new frame is compared against that background, pixel by pixel. Anything that wasn't there before, like a drone, shows up bright in the difference image. Everything else stays dark.
img = sensor.snapshot()
img = img.difference(extra_fb) # |new - background|
blobs = img.find_blobs([(30, 255)], pixels_threshold=5,
area_threshold=5, merge=True, margin=100)
find_blobs groups the bright pixels into blobs, and merge=True with a wide margin glues nearby fragments into one, so a drone's arms and body count as a single object.
Step 2: Decide what the blobs mean
The script keeps the rules simple, and it uses the board's LEDs so you can debug it by eye:
- No blob: nothing above the pad. It prints
{900$900}and lights blue. - More than one blob: something confusing is in view. It prints the error code
{901$901}and lights red. - Exactly one blob: that's the drone. It lights green and prints the blob's centre as
{xxx$yyy}, zero-padded to three digits, for example{132$087}.
Two safety valves keep the background fresh. If a single blob covers more than 75% of the frame (a cloud, a hand, a sudden change in light), the script raises an error, waits two seconds and takes a new background. And if the SBC sends the word reset over USB, the camera replies {902$902}, waits two seconds and re-captures the background too. That second one matters outdoors, where the "empty sky" at noon is not the empty sky at 5 pm.
The {x$y} format is worth copying. A fixed-width string with clear start, split and end characters is trivial to parse on the other side and easy to resync if a byte gets dropped.
Step 3: Find the camera and read it
On the SBC, Controller/DronePortController.py doesn't assume which port the camera is on. It lists every serial port on the machine (it handles Windows COM ports, Linux /dev/tty* and macOS /dev/tty.*), opens each one briefly, and keeps the port that speaks the camera's expected greeting string.
To get a position, it flushes the serial buffer first so it isn't reading stale data, then reads 15 samples, parses each {x$y} string, throws away the "no drone" codes, and averages what's left. Averaging a handful of readings is a cheap way to smooth out the jitter that frame differencing always has.
Step 4: Turn pixels into metres
This is the part most hobby projects skip. A pixel offset means nothing until you know how high the drone is: 50 pixels off-centre at 1 m is a small correction, at 3 m it's a big one.
The controller uses the pinhole camera model. With focal length ( f ), drone height ( Z ), the physical size of one pixel ( p ), and the image centre ( u_0 ):
$$ x = \frac{Z}{f} \cdot p \cdot (u - u_0) $$
and the same for ( y ). The code takes its constants straight from the sensor datasheet: a 2.8 mm focal length, a sensor 3.984 mm wide spread over 656 pixels, and the image centre at pixel 120 of the 240-pixel crop. The pixel size is doubled because the camera runs at QVGA, half the sensor's full VGA resolution.
Plug in the numbers and each pixel works out to roughly 4.3 mm on the ground plane per metre of height. So a drone that appears 120 pixels from the centre, right at the edge of the crop, while hovering at 1 m, is about half a metre off the pad. At 2 m the same pixel offset means about a metre.
Step 5: Try it in simulation first
You don't want to debug vision code with a real drone overhead. The Simulation/drone_port_sensor.py node does the same job inside ROS and Gazebo: it subscribes to a simulated camera topic, converts frames to grayscale, takes the absolute difference with the previous frame, thresholds at 30, dilates with a 3 by 3 kernel to close gaps, finds contours, and computes each contour's centroid from its image moments. It shows the raw image, the difference image and the contours in windows, so you can watch the detection work before any propeller spins.
What's built and what's next
Be clear-eyed about where this code stands. It is a working prototype of the perception half of the problem. The controller already defines the landing parameters it will need, a 0.2 m/s approach speed, a 0.5 m starting hover height, a 3 m ceiling and a landing target at the pad's origin, but the repository doesn't yet close the loop by commanding the drone down, and the charging hardware isn't in it. There are rough edges too: the greeting string the controller waits for ({904$904}) isn't one the current camera script prints, and the averaging list starts with a zero that pulls the estimate slightly towards the centre. Those are the kind of things you fix on the first afternoon on the bench.
The approach is still the right one to learn from. A small upward-facing camera, a background frame, a difference, a blob, and a bit of pinhole geometry get you a position estimate good enough to guide a landing. Add a controller that feeds those x and y offsets to the flight stack, and a pad with charging contacts, and the drone can look after its own battery.
The controller code is adapted from an open-source implementation published at IEEE, and the full source is on GitHub.
Share