Smallest possible pipeline: load YOLO11n + keypoints weights, feed one frame, get PnP gate pose, emit a PID command. For deeper integration see getting-started. For the full race stack see race-pipeline.
from vq1_completion_pilot import (
ApexDetectorChain, CompletionPilot, CameraIntrinsics, Telemetry
)
from pathlib import Path
intr = CameraIntrinsics(width=640, height=360, fx=320, fy=320, cx=320, cy=180,
tilt_deg=20.0, gate_width_m=1.5, gate_height_m=1.5)
detector = ApexDetectorChain(
detector_path=Path("models/apex_detector_best.onnx"),
keypoints_path=Path("models/apex_keypoints_best.onnx"),
)
pilot = CompletionPilot(intr=intr, detector=detector)
# ------- one frame, one command -------
frame_bgr = cv2.imread("test_frame.png")
tele = Telemetry(
attitude_q=(1.0, 0.0, 0.0, 0.0),
body_rates=(0.0, 0.0, 0.0),
body_accel=(0.0, 0.0, 9.81),
t_sim=0.0,
)
cmd = pilot.step(frame_bgr, tele)
print(cmd) # ControlCmd(throttle=0.55, roll=0.0, pitch=0.01, yaw=-0.04)
None until real weights wired.ControlCmd for Throttle/Roll/Pitch/Yaw.Replace StubSimClient in vq1_completion_pilot.py with the real AIGP sim adapter when credentials are released:
class AIGPSimClient:
def connect(self) -> None: ...
def next_frame(self) -> tuple[np.ndarray, Telemetry]: ...
def send(self, cmd: ControlCmd) -> None: ...
def close(self) -> None: ...
The rest of the pipeline is transport-agnostic.
| Symptom | Cause | Fix |
|---|---|---|
| Detector returns None for all frames | Weights not loaded (stub active) | Pass real --detector / --keypoints paths |
| PnP raises | Keypoints not 4×2 float | Check det.corners_px.shape == (4, 2) |
| Pilot output saturated at ±1 | PID gains too hot | See tuning reference |
| Telemetry attitude wrong | Sim payload Euler vs quaternion | Adapter in CompletionPilot, convert to quat |