Version 1.1.0 has three profiles: default, correlation and legacy-adaptive.
Coming from 1.0.0, see the 1.1.0 release notes: pure is
gone and NanoTrack moved behind ml=True. Coming from 0.2.x, see also the
1.0.0 release notes.
Prerequisites
- Python 3.10 or later
- OpenCV for Python:
pip install opencv-python sky_tracker.pyd(Windows) orsky_tracker.so(Linux) built from the C++ Python binding- Your evaluation licence key
# PowerShell
$env:SKY_TRACKER_LICENSE_KEY = "<your-token>"rem Windows CMD
set SKY_TRACKER_LICENSE_KEY=<your-token># Linux / macOS
export SKY_TRACKER_LICENSE_KEY=<your-token>The Tracker constructor reads this variable at import time. A missing or invalid key raises RuntimeError immediately.
The quickest check — runs 30 frames of the sample video and reports confidence:
python cpp_tracker\tools\smoke_python_sdk.py `
--module-dir cpp_tracker\build-python\Release `
--video sample.mp4 `
--bbox 469,409,26,38 `
--frames 30Expected output:
python_sdk_smoke passed frames=30 active=29 max_confidence=0.891
If the module directory is the default (cpp_tracker/build-python/Release) and video_20.mp4 exists in the repo root, all flags are optional:
python cpp_tracker\tools\smoke_python_sdk.pyimport os
import sys
import cv2
# ── Load the module ──────────────────────────────────────────────────────────
MODULE_DIR = r"cpp_tracker\build-python\Release"
if os.name == "nt":
os.add_dll_directory(MODULE_DIR) # Windows: load OpenCV DLLs next to the .pyd
sys.path.insert(0, MODULE_DIR)
import sky_tracker # noqa: E402 (must come after path setup)
# ── Open video ───────────────────────────────────────────────────────────────
cap = cv2.VideoCapture("sample.mp4")
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
dt = 1.0 / fps
# ── Lock on to the target ────────────────────────────────────────────────────
ok, first_frame = cap.read()
assert ok, "could not read first frame"
tracker = sky_tracker.Tracker("default") # profiles: default, correlation, legacy-adaptive
tracker.lock(first_frame, bbox=(469, 409, 26, 38)) # (x, y, width, height)
# ── Process frames ───────────────────────────────────────────────────────────
frame = first_frame
for i in range(120):
if i > 0:
ok, frame = cap.read()
if not ok:
break
result = tracker.update(frame, dt)
print(
f"frame={result.frame:4d} state={result.state:12s} "
f"cx={result.cx:6.1f} cy={result.cy:6.1f} "
f"conf={result.confidence:.3f}"
)
# Optional: get annotated frame with cyan HUD overlay
# annotated = result.annotate()
# cv2.imshow("sky_tracker", annotated)
# cv2.waitKey(1)
cap.release()Use sky_tracker.MultiTracker when the caller provides several selected
targets. The SDK assigns stable target_id values from the initial bbox order.
multi = sky_tracker.MultiTracker("default")
multi.lock(first_frame, [
(469, 409, 26, 38),
(520, 390, 24, 34),
])
for i in range(120):
ok, frame = cap.read()
if not ok:
break
for result in multi.update(frame, dt):
print(
f"target={result.target_id} state={result.state} "
f"bbox={result.bbox()} suppressed={result.suppressed}"
)Target proposals are assigned one-to-one before tracker state is committed. A
clear duplicate loser coasts with suppressed=True, state="lost", and
suppressed_by_id pointing to the owner. Indistinguishable claims fail closed
with reason="coast-ambiguous-owner" and suppressed_by_id=0, avoiding an
arbitrary target-order tie-break.
During close interactions, translation continues while appearance learning and
adaptive bbox geometry updates are frozen. Outside an interaction,
legacy-adaptive foreground geometry and default candidate geometry are
aspect/scale constrained, clipped to the assigned ownership cell, and kept
separate from the identity search bbox.
Command-line example:
python cpp_tracker\examples\track_multi_video.py `
--video video_20.mp4 `
--bbox 469,409,26,38 `
--bbox 520,390,24,34 `
--csv cpp_tracker\build-python\video_20_multi_sdk.csvsky_tracker.MotionTracker needs no bbox. It finds whatever moves in each
frame and follows each object with its own stable id: drones and birds against
the sky, or meteors. Use it with a static or slowly moving camera.
tracker = sky_tracker.MotionTracker(mode="bright-diff", moving_only=True)
while True:
ok, frame = cap.read()
if not ok:
break
for track in tracker.update(frame, dt=1 / fps):
if track.state == "confirmed":
print(track.id, track.center(), f"{track.speed:.0f} px/s")Modes:
"background"(the default): background subtraction. It tolerates slow changes in light; the first frames are spent learning the background."diff": frame differencing. Faster; static camera only."bright-diff": what got brighter since the last frame. It boxes a meteor's head rather than its trail. Static camera only. Free: it needs no licence key, so meteor and sky-event tools can use it without one.
moving_only=True reports an object once it has moved 3 px, and drops it after
1 frame without a detection. That removes twinkling stars and sensor noise.
region=(x, y, w, h) looks only at part of the frame, for example the sky
above the horizon; results stay in frame coordinates. Call reset() after a
scene cut.
| Profile | Use case |
|---|---|
"default" |
Recommended speed/quality balance; uses the former Pi4-target policy |
"correlation" |
An improved correlation-filter tracker, no neural network; single target, Tracker only |
"legacy-adaptive" |
Template tracker with correlation assist (strict checks, camera-motion compensation) and scheduled foreground box fitting |
"correlation" needs no OpenCV dnn and no model files; its confidence is
1 while tracking and 0 once it has lost the target. ml=True (with
"correlation" only) adds NanoTrack, a small neural tracker, and arbitrates
between the two, on every frame by default (exactly 1.0.0's correlation).
ml_every=N runs NanoTrack on every Nth frame while the two agree, and on
every frame while they disagree or the target is lost: about half the cost at
N = 3, but on fast footage the box can jump between checks. With
ml=True the constructor raises when the module was built without NanoTrack
or cannot find its models; sky_tracker.has_nanotrack tells you which builds
have it (the Linux and macOS wheels do). The default aliases pi4-target, raspi4-target and
pi-target are still accepted. Accuracy and speed per profile are in the
benchmarks.
You can also pass a TrackerConfig object:
cfg = sky_tracker.TrackerConfig()
cfg.profile = "correlation"
cfg.ml = True
cfg.ml_every = 1
tracker = sky_tracker.Tracker(cfg)Seeds the tracker on the first frame. bbox is (x, y, width, height) in pixels.
Process the next frame. Pass dt in seconds for deterministic video processing. If omitted, the tracker measures wall-clock time automatically.
Native selected-target multi-tracker. Use lock(frame, bboxes) with
bboxes=[(x, y, width, height), ...], then call update(frame, dt) once per
frame. It returns a list of TrackResult objects.
sky_tracker.MotionTracker(*, mode="background", min_area=3, max_area=5000, diff_threshold=25, hit_threshold=3, coast_frames=10, association_distance=60, moving_only=False, box_padding=0, region=None)
Finds and follows everything that moves (section 5). min_area and
max_area bound a detection's area in pixels, and diff_threshold (1-254) is
the per-pixel change for diff and bright-diff. An object becomes
confirmed after hit_threshold frames, and ends after coast_frames frames
without a detection. association_distance is how far, in pixels, an object
may be from its predicted position and still keep its id. box_padding grows
every box by that many pixels on each side.
update(frame, dt=-1.0) takes a grey or BGR frame, the same size every time,
and returns a list of MotionTrack in id order, tentative ones included.
reset() forgets the background and every object; new ids continue above the
old ones. Invalid options raise ValueError.
MotionTrack fields: id, state (tentative, confirmed or lost), cx,
cy, bbox_x, bbox_y, bbox_w, bbox_h, vx and vy (pixels/second),
speed (pixels/second), age (frames since found), hits (frames detected),
misses (consecutive frames without a detection) and prediction_only.
Methods bbox() and center() return tuples.
| Field | Type | Description |
|---|---|---|
frame |
int | Frame counter since lock() |
lost |
bool | True when the tracker has lost the target |
state |
str | "confirmed", "tentative", or "lost" |
cx, cy |
float | Target centre, pixels |
bbox_x, bbox_y, bbox_w, bbox_h |
float | Bounding box |
confidence |
float | 0–1 template match confidence |
speed |
float | Speed in pixels/second |
hits, misses |
int | Track quality counters |
reason |
str | Internal tracking decision label |
target_id |
int | Stable selected-target id, same as id |
anchor_appearance |
float | Appearance similarity to the immutable initialization anchor |
suppressed |
bool | True when this target coasts due to ambiguous ownership or another target owning the observation |
suppressed_by_id |
int | Owning target id, or 0 for an ambiguous claim / no owner |
interacting |
bool | True while this target is in a guarded close interaction |
model_learning_suppressed |
bool | True when appearance and correlation learning are frozen |
geometry_update_suppressed |
bool | True when bbox width/height adaptation is frozen |
geometry_ownership_constrained |
bool | True when a geometry measurement was clipped to the target's ownership cell |
geometry_confidence |
float | Owned geometry measurement confidence, or 0 when no measurement was available |
prediction_only |
bool | True when bbox/center are a tentative velocity coast, not an accepted observation |
bbox() |
tuple | (x, y, w, h) shorthand |
center() |
tuple | (cx, cy) shorthand |
annotate() |
ndarray | Copy of the frame with cyan HUD drawn |
Prediction-only results never update appearance or geometry. Treat them as a short-gap display/prediction signal, not as a confirmed image observation.
import csv
with open("results.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["frame", "state", "cx", "cy", "confidence"])
for i in range(120):
ok, frame = cap.read()
if not ok:
break
r = tracker.update(frame, dt)
writer.writerow([r.frame, r.state, r.cx, r.cy, r.confidence])For USB/V4L2 cameras, pass an integer camera id to OpenCV and keep the tracker API unchanged:
import cv2
import sky_tracker
cap = cv2.VideoCapture(0)
ok, frame = cap.read()
assert ok, "could not read camera frame"
bbox = cv2.selectROI("select target", frame, fromCenter=False)
cv2.destroyAllWindows()
tracker = sky_tracker.Tracker("default")
tracker.lock(frame, bbox)
while True:
ok, frame = cap.read()
if not ok:
break
result = tracker.update(frame)
cv2.imshow("sky_tracker", result.annotate())
if cv2.waitKey(1) == ord("q"):
break
cap.release()
cv2.destroyAllWindows()On Raspberry Pi OS 64-bit, install the Linux ARM64 wheel and use the same frame-by-frame API. If the official Pi camera is not available as /dev/video0, use Picamera2 to capture BGR frames and pass those frames to tracker.update(...).
Share your CSV and any annotated frames with us. The reason field in each row is especially useful for diagnosing tracking failures.
- The Python module is a
.pyd/.sobinary built against a specific OpenCV version. If you see import errors, ensureopencv-pythonmatches the version used to build the module. - On Windows, call
os.add_dll_directory(MODULE_DIR)beforeimport sky_trackerso the loader finds OpenCV DLLs next to the module. annotate()returns a full-resolution copy of the frame — avoid calling it on every frame in throughput-critical paths.- The licence key is checked when
Tracker()is constructed. If the key expires mid-session the current tracker instance keeps running; the nextTracker()construction will fail. - A 7-day trial key (
SKT2.…) is also confirmed with the licence server over HTTPS whenTracker()is first constructed in a process, so a trial needs an internet connection and 1.1.1 or later. Bought and student keys never contact the network.