Skip to content

Add PTZ calibration app and calibrated move parameters#335

Open
MateoLostanlen wants to merge 29 commits intodevelopfrom
calibrate_reolink_cam
Open

Add PTZ calibration app and calibrated move parameters#335
MateoLostanlen wants to merge 29 commits intodevelopfrom
calibrate_reolink_cam

Conversation

@MateoLostanlen
Copy link
Copy Markdown
Member

@MateoLostanlen MateoLostanlen commented Mar 31, 2026

Summary

  • Add tools/ptz_calibration_app.py — Streamlit app for calibrating PTZ motor speed tables with automated ORB keypoint matching
  • Recalibrate all speed/bias tables for both 823A16 and 823S2 (pan + tilt) using server-side timing
  • Add zoom-aware speed selection: full speed table at zoom 0, speed 1 only when zoomed in
  • Add micro-pulse support: zoom to 41 then move+stop for precise small-angle nudges
  • Serialize concurrent PTZ commands via a per-camera lock + 409 on busy, with /stop preempting in-flight blocking handlers
  • Split the overloaded /control/move into focused endpoints with validated inputs (move_camera stays for compatibility and will
    be removed once platform integration is done)
  • Add tools/livestream_app.py — Streamlit tool: stop patrol + start stream + click-to-aim on a camera

Key changes

routes_control.py

  • Updated PAN_SPEEDS, PAN_BIAS, TILT_SPEEDS, TILT_BIAS for both camera models
  • _pick_speed() takes a zoom param — restricts to speed 1 at zoom > 0
  • Extracted _execute_axis() helper in click_to_move (removes pan/tilt duplication)
  • Micro-pulse zooms to 41 before firing for consistent displacement (~1.6° pan, ~2.1° tilt on 823A16)
  • Skip threshold changed from hardcoded 0.5° to bias(speed1) / 2
  • Added /control/speed_tables endpoint for external tools
  • Server-side duration parameter: move+sleep+stop in a single API call (no VPN latency)
  • _resolve_adapter() maps legacy generic "reolink" → "reolink-823S2" (default calibrated model) with a warning, so old configs
    keep working instead of silently hitting the uncalibrated path
  • Legacy /control/move degrees branch now reads zoom from the camera instead of trusting the client query param

Concurrency (new)

  • Per-camera MOVE_LOCKS in camera/registry.py. Every mutating PTZ endpoint acquires non-blocking → 409 camera busy on conflict.
    /stop and /stop_move stay unlocked so aborts always win.
  • Per-camera STOP_EVENTS + _interruptible_sleep(): /stop wakes any in-flight sleeping handler, which then releases the lock
    within milliseconds. Operators can immediately issue a corrective command instead of waiting for the original sleep to expire.
    Responses include "interrupted": true on short-circuited moves.
  • Blocking endpoints (click_to_move, zoom_camera, move_for_duration, move_by_degrees) hold the lock through their sleep; fast
    endpoints (goto_preset, start_move, legacy /move) hold it briefly to gate against preemption.

Focused PTZ routes (new) — move_camera replacements

The existing /control/move is an overloaded endpoint that multiplexes four modes (preset, duration, degrees, bare direction)
through the same signature. It stays in place so the platform keeps working, and will be removed once platform integration
migrates to the focused routes below.

  • POST /control/goto_preset — {pose_id, speed}
  • POST /control/start_move — {direction, speed}
  • POST /control/stop_move/{camera_ip} — alias of /stop
  • POST /control/move_for_duration — {direction, duration, speed} (blocking, locked)
  • POST /control/move_by_degrees — {direction, degrees, speed?} (blocking, locked)
    • speed is optional. When omitted (recommended), the server auto-picks the best calibrated level via _pick_speed. Invalid
      explicit speeds (e.g. speed=10 on Reolink, or any speed != 1 at zoom > 0) return 400 with a helpful message instead of silently
      downgrading.
    • Reads current zoom from the camera; callers don't pass zoom.

pyro_camera_api_client (library)

  • Mirrored the new focused routes: goto_preset, start_move, stop_move, move_for_duration, move_by_degrees
  • move_for_duration auto-widens the HTTP timeout to duration + 5s; move_by_degrees uses 30s (same as click_to_move)
  • Legacy move_camera kept and flagged deprecated in its docstring

tools/ptz_calibration_app.py

  • Automated calibration via ORB keypoint matching (2000 features, Lowe's ratio test)
  • Zoom sweep mode to measure speed vs zoom across multiple levels
  • Manual annotation fallback for micro-pulse at high zoom
  • Click-to-move accuracy test tab
  • Fetches speed tables from the API to avoid duplication
  • Incremental JSON save during long sweeps
  • Internal HTTP client now wraps PyroCameraAPIClient from the shared library (single source of HTTP logic)

tools/livestream_app.py (new)

  • One-click stop patrol + start stream, stop stream, apply zoom
  • Embedded iframe of the public HLS livestream for low-latency preview
  • Clickable snapshot pane for click_to_move, auto-refreshed after each move
  • Graceful UX on 409 "camera busy" responses

Docs

  • tools/ptz_zoom_speed_calibration_report.md — Reolink speed-cap research data
  • tools/README.md — step-by-step calibration guide; livestream app and FOV calibration sections

Context

Previous speed tables were calibrated through VPN with separate move/stop API calls, adding 50–200 ms uncontrolled latency per
measurement. Server-side timing eliminates this. A zoom sweep revealed Reolink cameras cap motor speed at zoom > 0 (all speeds
→ ~1.5 °/s), so higher speed levels are only useful at zoom 0. The concurrency and focused-route work came out of a code
review: unlocked PTZ endpoints could interrupt mid-move click_to_move, /stop could only halt the motor without freeing the
lock, and the overloaded /control/move made it too easy to pick the wrong mode or an out-of-range speed — all fixed here.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.44%. Comparing base (02ad4be) to head (c8170ad).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop     #335   +/-   ##
========================================
  Coverage    73.44%   73.44%           
========================================
  Files            7        7           
  Lines          610      610           
========================================
  Hits           448      448           
  Misses         162      162           
Flag Coverage Δ
unittests 73.44% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

When the target angle is below the calibrated bias at speed 1,
fire a 50ms impulse instead of skipping the move. Applies to both
/click_to_move and /move endpoints. Add micro-pulse calibration
section to the calibration app (zoom 41 default for precision).
Add duration parameter to /move endpoint so move+sleep+stop
happens on the Pi. Update calibration app to use single API
calls for all movements.
- update 823A16 and 823S2 pan/tilt speed and bias tables (zoom 0)
- add zoom-aware speed selection: speed 1 only when zoom > 0
- micro-pulse zooms to 41 for consistent small displacement
- add auto keypoint matching (ORB) for hands-free calibration
- add zoom sweep mode to measure speed vs zoom relationship
- add /control/speed_tables endpoint, calibration app fetches from API
- add calibration report and tools README
- fetch speed tables from /control/speed_tables endpoint (single source of truth)
- click-to-move: zoom-aware speed selection, micro-pulse support
- skip threshold based on bias/2 instead of hardcoded 0.5 deg
- zoom settle time scales with zoom delta
- update reference tables to match routes_control.py
- move route: add zoom param, force speed=1 when zoom>0 with warning
- click_to_move route: remove zoom manipulation in micro-impulse, add warning when zoom>0
- client: add click_to_move, get_speed_tables methods; add duration/zoom params to move_camera; log warnings when zoom>0
- click_to_move accepts only camera_ip + normalized click_x/click_y in [0,1]
- server reads current zoom via get_focus_level and resolves FOV from table
- response includes zoom, h_fov, v_fov for easier debugging
- add tools/pyproject.toml for uv-managed calibration app deps
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant