NTU UAV Research RoboticsUAVCalibrationROSComputer VisionAPF

Experiment Notes: Setup, Calibration, Tuning, and Observations

Lab setup, UWB anchor placement, camera calibration, depth model evaluation, APF parameter tuning, PyBullet simulation experiments, live flight protocol, and observed failure modes for the NTU UAV obstacle avoidance system.

Environment: Indoor lab, Nanyang Technological University
Hardware: DJI Tello · Nooploop LinkTrack · Ubuntu 20.04 workstation


Table of Contents

  1. Physical Environment
  2. UWB Anchor Placement
  3. Camera Calibration
  4. Depth Model Evaluation
  5. APF Parameter Tuning
  6. PyBullet Simulation Experiments
  7. Live Flight Protocol
  8. Observed Failure Modes
  9. Configuration Reference
  10. Checklist: Pre-Flight

1. Physical Environment

1.1 Flight Space

The system was designed and tested in a single indoor lab space. The configured flight envelope reflects the usable portion of this space:

AxisMinMaxSpan
xx (forward)0.50-0.50 m7.007.00 m7.507.50 m
yy (lateral)0.50-0.50 m4.504.50 m5.005.00 m
zz (vertical, negative = up)3.75-3.75 m0.50-0.50 m3.253.25 m

Total flight volume: approximately 7.50×5.00×3.251217.50 \times 5.00 \times 3.25 \approx 121 m³.

The 0.50 m inset from physical walls on each axis provides a safety buffer for UWB measurement noise and control lag. In practice, the drone operates primarily in the z[2.5,1.5]z \in [-2.5, -1.5] m band (approximately 1.5–2.5 m altitude).

1.2 Reference Positions

These positions appear in task/main.py and task/sim.py and correspond to specific physical locations in the lab:

LabelxxyyzzNotes
Default start5.952.30−0.85Commented out in main.py
Sim start6.1682.187−2.148Loaded from a real flight log
Default target−0.301.75−2.00The primary navigation goal
Sim target−0.202.15−4.00Alternative target in simulation

The start-to-target distance at the nominal positions is approximately:

d=(6.168(0.30))2+(2.1871.75)2+(2.148(2.00))26.49 md = \sqrt{(6.168 - (-0.30))^2 + (2.187 - 1.75)^2 + (-2.148 - (-2.00))^2} \approx 6.49 \text{ m}

This is a traverse of approximately 6.5 m across the long axis of the room, passing through whatever obstacles are present in the space.

1.3 Environment Characteristics

The lab contains:


2. UWB Anchor Placement

2.1 Requirements

Nooploop LinkTrack requires at least three anchors for 3D localisation. Four anchors were used in the experimental setup to improve geometric dilution of precision (GDOP), particularly in the vertical axis.

Anchors should be:

Anchor coordinates are surveyed relative to the UWB system origin and entered into the LinkTrack configuration tool. The origin determines the zero-point of the coordinate frame used in task/main.py.

2.3 Initialisation Procedure

  1. Power on all anchors and allow them to stabilise for 60 s.
  2. Power on the tag (on the drone).
  3. Verify position output with rostopic echo /nlink_linktrack_nodeframe1.
  4. Confirm that reported x,y,zx, y, z match the drone’s known physical position (within ~0.1 m).
  5. If values are incorrect, check anchor configuration in the Nooploop software.

2.4 Observed Noise Characteristics

In the experimental setup, raw UWB position noise was approximately:

No filtering is applied to the raw position in the current implementation. The 100-sample running average used for depth anchoring (§6.4 of the technical report) reduces effective noise for that specific purpose to approximately ±1–2 cm, at the cost of a positional lag of approximately 1 second at 100 Hz.


3. Camera Calibration

3.1 Setup

The calibration was performed using task/camera_calibrate.py, which calls cv2.calibrateCamera on detected chessboard corners.

3.2 Calibration Quality

The calibrate/ directory contains 19 images. A good calibration for a fisheye-adjacent lens like the Tello’s should include:

Reprojection error below 1.0 pixel is generally acceptable; the Tello lens typically achieves 0.5–1.5 px with sufficient image diversity.

3.3 Calibration Data

The resulting intrinsics are stored in calibration_data.npz:

import numpy as np
data = np.load("calibration_data.npz")
K = data["camera_matrix"]    # shape (3, 3)
d = data["dist_coeffs"]      # shape (1, 5) or (5,)

This file is committed to the repository and is the sole camera calibration artefact used at runtime.

3.4 Recalibration Procedure

If the camera is recalibrated (e.g. after firmware update or lens damage):

  1. Fly the drone and capture calibration frames via task/test_video.py:

    tello.active_img_task = partial(tello.run_depth_model, manual=True)
    tello.run_calibration()

    Manual captures are saved to calibrate/frame-<idx>.jpg.

  2. Run calibration:

    cd task && python camera_calibrate.py
  3. Verify the output camera matrix looks reasonable for a ~82° FOV:

    • fx,fyf_x, f_y should be approximately 900–1100 px (for 960×720 resolution)
    • cx480c_x \approx 480, cy360c_y \approx 360
    • Distortion coefficients k1k_1 should be negative (barrel distortion)

4. Depth Model Evaluation

4.1 Model Selection

Three models were evaluated:

ModelTypeSizeNotes
ZoeDepth NKMetric depth (NYU+KITTI)~1.5 GBSelected. Best indoor metric accuracy
MiDaS LargeRelative depth~400 MBNo metric scale; good relative structure
MiDaS SmallRelative depth~80 MBFaster; acceptable relative depth

ZoeDepth NK was selected because it produces metric (absolute) depth estimates directly, which are required for 3D obstacle reconstruction in physical units (metres). Relative depth models would require a separate scale estimation step.

4.2 Qualitative Observations on the Tello Feed

ZoeDepth performance on the Tello camera feed showed:

Well-estimated regions:

Poorly estimated regions:

4.3 Depth Scale Consistency

ZoeDepth is calibrated for the NYU and KITTI depth distributions. The Tello’s lens geometry and sensor size differ from those datasets. In practice, the absolute depth values have approximately the right scale for indoor objects at 0.5–3 m, but may underestimate depth at longer ranges (>4 m) where the Tello footage approaches the training distribution boundary.

4.4 Inference Timing

Approximate per-frame inference times (single forward pass, batch size 1, 960×720 input):

HardwareApproximate Time
CPU (Intel Core i7, 8-core)3–8 s
GPU (NVIDIA RTX 3060)0.3–0.8 s
GPU (NVIDIA RTX 4090)0.1–0.2 s

These are rough estimates; actual times vary with CPU/GPU load and memory bandwidth.

The 250-frame inference interval at 24 fps equals approximately 10.4 s. This is comfortably above even the slowest CPU inference time, ensuring no depth cycle overlap. With a GPU, the interval could be reduced to 25–50 frames (1–2 s) without overlap risk.


5. APF Parameter Tuning

5.1 Methodology

APF parameters were tuned using the PyBullet simulation before live flight. The simulator uses the same apf_with_bounds function as the live controller, so behaviour in simulation transfers directly.

5.2 Parameter Sensitivity

Attractive gain kak_a:

Too low (ka<5k_a < 5): Drone barely moves toward the goal; small repulsive forces dominate and the drone drifts.
Too high (ka>50k_a > 50): Drone rushes toward the goal with insufficient braking near obstacles; overshoots.
Selected: ka=30k_a = 30, reduced to ka=max(10,30d)k_a' = \max(10, 30d) within 1 m of the goal.

Repulsive gain krk_r:

Too low (kr<3k_r < 3): Obstacles insufficiently repelled; drone passes through or collides.
Too high (kr>30k_r > 30): Drone is pushed aggressively away from any nearby object; unstable oscillation when navigating near walls.
Selected: kr=10k_r = 10.

Influence radius ρ0\rho_0:

Too small (ρ0<0.2\rho_0 < 0.2 m): Repulsion activates too late; drone is already close to the obstacle before being deflected.
Too large (ρ0>1.0\rho_0 > 1.0 m): Large portions of the flight volume become repulsively active; navigation is sluggish.
Selected: ρ0=0.5\rho_0 = 0.5 m.

Boundary influence distance ρb\rho_b:

Same sensitivity as ρ0\rho_0. In the experimental space, 0.5 m gives approximately 4–5 cell-widths of active boundary near walls, which is sufficient to prevent boundary violations at vmax=30v_{\max} = 30.

Selected: ρb=0.5\rho_b = 0.5 m.

5.3 Velocity Clamping

The maximum velocity command max_val = 30 (of 100 scale) corresponds to approximately 0.5–1.0 m/s actual drone speed based on the Tello specification. This was chosen conservatively for a prototype system; higher values would reduce flight time but risk insufficient braking near obstacles.

5.4 Control Loop Rate

The time.sleep(0.2) at the end of follow_path() limits the effective control rate to 5 Hz (plus APF computation time, which is negligible). The UWB callback may fire at 100 Hz, but the control task only runs every 200 ms. This prevents command flooding (tellopy rejects commands too close together) and gives the drone time to respond to each command before the next is sent.


6. PyBullet Simulation Experiments

6.1 Purpose

The PyBullet simulation serves three purposes:

  1. Parameter tuning: APF gains can be explored rapidly without hardware risk.
  2. Controller verification: A new path or obstacle configuration can be tested before flight.
  3. Debugging: Post-flight, TelloDroneSim.load_config() can replay a real flight log to visualise the drone’s path and obstacle positions.

6.2 Running the Simulation

cd task
python sim.py

The PyBullet GUI opens. The duck model represents the drone; the soccer ball represents the target; spheres represent obstacles.

Key simulation state (in sim.py, modify before running):

t.attract_coeff = 20          # APF gains
t.repel_coeff = 10
t.generate_obstacles(5, 0.75) # 5 random spheres, 0.75 m radius
t.start_pos = Vector3D(6.168, 2.187, -2.148)
t.target_pos = Vector3D(-0.30, 1.75, -2.00)
t.path = [t.start_pos, t.target_pos]
t.active_task = t.run_apf

6.3 Replaying a Real Flight Log

from tellodrone_sim import TelloDroneSim

t = TelloDroneSim()
t.load_config("logs/log-<run-name>")  # loads positions and obstacle list
t.active_task = t.run_log            # replay recorded positions
t.start_sim()
t.run_sim()

This visualises the drone’s actual trajectory alongside the obstacle map that was generated during the flight.

6.4 A* vs APF Comparison

To compare A* waypoints with direct APF:

# A* mode
t.a_star_waypoints(grid_resolution=0.5)
t.active_task = t.run_apf

# Direct APF mode
t.path = [t.start_pos, t.target_pos]
t.active_task = t.run_apf

In simulation, A* + APF typically produces smoother paths around complex obstacle configurations. Direct APF is sufficient when obstacles are sparse and well-separated from the direct path.

6.5 Simulation Fidelity

The PyBullet simulation does not model:

It is used solely for APF control logic validation. Real-flight behaviour will differ, particularly in tight obstacle corridors where the timing of depth updates matters.


7. Live Flight Protocol

7.1 Pre-Flight Setup

Follow the checklist in §10. Key steps:

  1. Ensure battery > 50% (ideally > 80% for a full run).
  2. Start UWB system and verify position in rostopic echo.
  3. Confirm target position is set correctly in task/main.py:
    tello.set_target_pos(Vector3D(-0.30, 1.75, -2.00))
  4. Confirm flight bounds match the physical space.
  5. Clear the flight path of people and loose objects.

7.2 Launch Sequence

Terminal 1: UWB:

bash cmd/uwb.sh

Wait for [nlink_parser] topics to appear in rostopic list.

Terminal 2: Verify UWB:

rostopic echo /nlink_linktrack_nodeframe1

Confirm x,y,zx, y, z values are near the drone’s known physical position. Ctrl+C when confirmed.

Terminal 3: Main:

cd task && python main.py

The drone will:

  1. Load ZoeDepth model (~20–60 s depending on hardware)
  2. Connect to drone
  3. Start video
  4. Take off
  5. Wait for first depth cycle (frame 250, ~10 s after takeoff)
  6. Begin APF navigation

7.3 During Flight

7.4 Post-Flight

Log files are in logs/log-<timestamp>/:

Images are in img/<original|depth|annotated>/<timestamp>/. Raw video is in vid/raw/vid-<timestamp>.avi.

7.5 Trajectory Analysis

Quick position trace plot from log:

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

data = np.loadtxt("logs/log-<run>/log-pos.log", usecols=(2, 3, 4))
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot(data[:, 0], data[:, 1], data[:, 2], lw=0.8)
ax.scatter(*data[0], c="green", s=50, label="start")
ax.scatter(*data[-1], c="red", s=50, label="end")
ax.set_xlabel("X (m)"); ax.set_ylabel("Y (m)"); ax.set_zlabel("Z (m)")
plt.legend(); plt.show()

8. Observed Failure Modes

The following failure modes were observed or anticipated during development. Each entry includes the symptom, probable cause, and recommended action.


Symptom: Drone does not move after takeoff and depth model run.
Cause: depth_model_run = False: depth thread has not completed yet (likely still running ZoeDepth inference on CPU).
Action: Wait. The follow_path() function returns immediately if depth_model_run is False. Check terminal for “Depth Model Running” message. If it never appears, check that cur_frame_idx is counting up (video stream is working).


Symptom: Drone immediately lands with “Out of X bounds” message.
Cause: UWB position reading is incorrect at startup (multipath, anchor misconfiguration, or tag not fully acquired).
Action: Verify UWB anchor power and configuration. Run rostopic echo and confirm position is near the drone’s physical location before launching main.py. The takeoff position is set to cur_pos at startup: if that position is wrong, bounds checks will trigger false positives.


Symptom: Drone circles around the target without landing.
Cause: APF oscillation near the goal. The attraction force overshoots, the drone passes the goal, and is pulled back repeatedly.
Action: Reduce attract_coeff for the close-range regime. The current implementation reduces kak_a to max(10,30d)\max(10, 30d) within 1 m, which may be insufficient. Try ka=max(5,15d)k_a' = \max(5, 15d).


Symptom: Drone stops moving partway to the goal with no error.
Cause: APF local minimum. An obstacle (or the combination of boundary and obstacle repulsion) is exactly cancelling the attractive force.
Action: This is a known APF limitation. In simulation, verify the obstacle positions using load_config. For live flight, Ctrl+C and land. Then either remove the obstacle or adjust the target position to avoid the minimum.


Symptom: ZoeDepth detects phantom obstacles (walls, floor, ceiling).
Cause: Row filter did not fully remove horizontal surfaces; or a large wall is within the 3 dark cluster threshold.
Action: Adjust dark_count in extract_segments (default 3) down to 2. Increase min_area in clean_binary (default 20,000 px²) to require larger detections. Check that the row filter threshold (0.85) is appropriate for the specific flight direction.


Symptom: Video stream stalls (all frames identical).
Cause: Wi-Fi interference or range. The Tello operates on 2.4 GHz; conflicting networks or distance from the drone causes packet loss.
Action: Reduce interference sources. Ensure the control laptop is the only device connected to the Tello’s AP. Move the laptop closer to the drone.


Symptom: av.open() raises an exception at startup.
Cause: The Tello video stream is not yet active, or the H.264 container is malformed.
Action: The startup() loop retries av.open() until successful. If it loops indefinitely, restart the drone and try again. Ensure drone.start_video() is called before opening the container.


Symptom: Pygame window is black (no video).
Cause: Video thread has not yet produced a frame, or cur_frame is still None.
Action: Wait 2–5 seconds after takeoff for the first frame. If it remains black, check that the video thread is running (no exception in the terminal).


9. Configuration Reference

9.1 Parameters Currently Hardcoded in Source

These parameters are not yet read from config files. Their locations for manual adjustment:

ParameterLocationDefaultNotes
Model pathtask/tellodrone/core.py:98"model/zoedepth-nyu-kitti"
Flight bounds xtask/tellodrone/core.py:44(-0.50, 7.00)
Flight bounds ytask/tellodrone/core.py:45(-0.50, 4.50)
Flight bounds ztask/tellodrone/core.py:46(-3.75, -0.50)
Frame inference intervaltask/tellodrone/depth_model.py:35250
K-means clusterstask/tellodrone/map_obstacle.py:315
Dark cluster counttask/tellodrone/map_obstacle.py:803
Row filter ratiotask/tellodrone/map_obstacle.py:470.85
Morph kernel sizetask/tellodrone/map_obstacle.py:6411
Min obstacle areatask/tellodrone/map_obstacle.py:6420000 px²
Max obstacle radiustask/tellodrone/map_obstacle.py:1621.0 m
Obstacle merge thresholdtask/tellodrone/depth_model.py:520.5 m
Camera-to-tag offsettask/tellodrone/map_obstacle.py:121–123(-0.4, -0.4, -0.4) m
APF kak_atask/tellodrone/follow_path.py:4930
APF krk_rtask/tellodrone/follow_path.py:5010
APF ρ0\rho_0task/tellodrone/follow_path.py:510.5 m
APF ρb\rho_btask/tellodrone/follow_path.py:520.5 m
Max velocity commandtask/tellodrone/follow_path.py:6130
Arrival thresholdtask/tellodrone/follow_path.py:340.30 m
Control loop sleeptask/tellodrone/follow_path.py:830.20 s
Target positiontask/main.py:33(-0.30, 1.75, -2.00)Change per run
Catkin workspacecmd/uwb.sh:4/home/horse3903/catkin_wsUpdate for your machine

9.2 Config File Templates

Reference config files with full documentation are provided in configs/:

These are not currently loaded at runtime; they serve as documentation of the parameter space and templates for a future config-driven architecture.


10. Checklist: Pre-Flight

Hardware

Software

← Back to NTU UAV Research