Autonomous Intruder-Detecting Drone

n7729697/drone-project  ·  KTH DD2419 — Project Course in Robotics and Autonomous Systems  ·  Group 6, four students  ·  January – May 2022

A semester-long robotics project course at KTH. Each group is handed a Crazyflie 2.0 — a 27-gram nano-quadcopter with a monocular camera and an optical-flow deck — and a room, and has to make it fly itself. Our brief: patrol a known indoor world, work out where you are from the traffic signs on the walls, and report any sign that is not on the map as an intruder.

The Crazyflie flying the patrol, with traffic signs on the brick wall The final system. The drone holds altitude on the flow deck, follows a planned patrol, and reads the signs on the wall as it passes.

I worked on localization, path planning and the brain — the parts that decide where the drone thinks it is and where it goes next. Perception was built by two teammates and is summarised further down. This page is weighted accordingly.

The problem, and why it is harder than it looks

The Crazyflie has no GPS, no lidar, and no external motion-capture rig. It knows two things: what its optical-flow deck says it has drifted since take-off, and what its single forward camera can see. Optical flow integrates, so its error integrates too — fly for a minute and the drone’s idea of its own position has walked away from reality.

The fix is to bolt the odometry to something absolute. The world contains ArUco markers and fifteen classes of traffic sign (stop, roundabout, airport, no_bicycle, residential, no_heavy_truck, narrows_from_left, …), and the map says where each one should be. Every time the drone recognises a sign it knows where it is and gets a free check on whether that sign belongs there at all. Localization and intruder detection turn out to be the same measurement read two different ways.

System overview

Three subsystems and a brain, talking over ROS topics:

Architecture diagram: localization, perception, planning subsystems and the brain The node graph. Red is localization, green perception, purple planning, yellow the brain, blue the Crazyflie hardware layer. Arrows are ROS topics.

Localization

loc5_filt is the node that answers “where am I on the map?”. It subscribes to two topics — /aruco/markers from the ArUco detector and /perception/sign_pose from the perception stack — and publishes the map → odom transform that anchors the drone’s drifting odometry frame to the world.

The design decision worth explaining is that ArUco is used exactly once. At start-up the drone needs an unambiguous fix, and ArUco markers give one: each carries its own ID, so there is no data association problem. Once that initial map → odom transform exists, the ArUco detections are dropped and the node localises from traffic signs alone — which is the interesting case, because signs are what the real world actually has on its walls.

ArUco marker detection in the camera feed with the TF tree in RViz Bootstrapping. Left: a marker detected in the camera stream. Right: the resulting TF tree, with the detected marker pose sitting next to where the map says that marker should be — the gap between the two is the odometry error being corrected.

For each detection the node takes the camera_link → sign transform, looks up that sign’s known pose on the map, and multiplies the two to recover map → odom. Three things keep this usable:

Path planning

Planning happens in two nodes. mapping.py converts the world description into an occupancy grid — walls and the outer air boundary become occupied cells, everything else is free — with each cell connected to its eight neighbours. a_star.py then searches that grid.

Plain A* off the shelf does not fly well on a map like this. The grid resolution, the cost function and the heuristic all had to be retuned together: too coarse a grid and the path clips a wall, too fine and the search crawls; a heuristic tuned for a square grid behaves badly on an 8-connected one where diagonal moves are cheaper than they look.

A-star search on the occupancy grid: explored nodes in cyan, resulting path in red A* on the grid map. Black cells are walls and boundary, cyan crosses are expanded nodes, the red line is the returned path. The search stays tight around the corridor rather than flooding the room — that is the retuned heuristic doing its job.

The planner returns the path as 2-D poses at turning points only, not as every grid cell. That keeps the drone from stuttering along a staircase of tiny waypoints, and makes the path cheap to publish and inspect.

Execution goes through navgoal3. The obvious implementation is to publish poses straight to /cf1/cmd_position, and it is the wrong one: cmd_position is expressed in the drifting odometry frame, so the drift the localization node just corrected gets reintroduced with every command. navgoal3 instead takes goals in the map frame from /move_base_simple/goal and converts them at the moment of sending, so each command is issued against the freshest available estimate.

The brain

brain3 sequences everything:

  1. Hover in place until an ArUco marker is visible and localization initialises.
  2. Hold for four seconds — long enough for the averaged transform to settle before trusting it.
  3. Pick observation points on the map, chosen so that the patrol sees every sign it needs to.
  4. Plan the shortest path between successive observation points.
  5. Fly it, detecting signs along the way, and publish the path as a message so the rest of us could see in RViz what the drone thought it was doing.

That last point sounds like a detail and was not. Publishing the planned path as a visualisable topic was the single most useful debugging decision in the project — it made the difference between “the drone flew somewhere strange” and “the planner returned this, and the drone followed it”.

Perception

Built by Mattias Hansson and Marcus Rickenlund; summarised here for completeness.

Sign detection runs a pre-trained MobileNetV2 backbone with a 1×1 convolution producing a 20×15×5 tensor — per grid cell, a confidence that a bounding-box centre falls in it, plus the box’s relative centre, width and height — and a further 15 channels for the sign classes. Input is downsized to 200×200 for speed, and training used blur and colour-jitter augmentation on images captured from the drone’s own camera.

Pose comes from classical vision rather than the network: SIFT keypoints from the detected crop are cross-matched by k-nearest-neighbours against reference images of each sign, then PnP RANSAC recovers the 6-D pose, published on /perception/sign_pose.

SIFT feature matches between the reference airport sign and the camera crop SIFT correspondences between the reference airport sign (left) and the crop from the drone camera (right). These matches are what PnP turns into a 6-D pose.

intruder.py closes the loop: it compares each detected sign pose against the signs the map says should exist, and anything left over is an intruder.

The system running

The full system: RViz with the map and detected signs, terminal logs, and the drone flying Everything at once. RViz shows the world model with the mapped signs labelled and the drone’s pose updating; the terminal logs detections and intruder decisions; the inset is the drone flying the patrol in real time.

RViz alongside the live camera feed with a detected sign bounding box A closer look at the same thing mid-flight: live camera with a sign bounding box on the left, and the map, marker poses and full TF chain — map, cf1/odom, cf1/base_link, cf1/camera_link — on the right.

What was hard

My role

Early on the split was pairwise — Pascal and I on localization, Mattias and Marcus on perception — and it converged as the parts had to be integrated.

Team and repository

Group 6: Xuezhi Niu, Pascal Jaufmann, Mattias Hansson, Marcus Rickenlund.

Code lives at github.com/n7729697/drone-project, my fork of the group’s shared repository. It is a ROS workspace: localization/, pathplanning/, perception/, brain/ and world/, plus the world descriptions we flew against. It is student code written to a deadline — the README is a list of rosrun commands, not documentation — but it is the real thing, and it flew.