axis_ptz_rotate
Native ACAP for automatic rotation compensation on AXIS PTZ cameras
A native ACAP extension for the AXIS Q6078-E (ARTPEC-8, aarch64) that automatically corrects camera mounting errors: the live image is rotated based on the current pan angle, controlled by a user-configurable mapping table with up to 360 control points and linear interpolation.
- —Real-time image rotation via GPU shader (OpenGL ES) based on pan angle
- —User-defined mapping table (max. 360 entries, 1° resolution, linear interpolation, 360° wrap-around)
- —Auto-zoom prevents black corners after rotation — calculated from the maximum correction value in the table
- —Event-driven update exclusively when the PTZ motor is stationary (no blur during movement)
- —Separate H.264 RTSP output stream — the primary camera stream remains unmodified
- —MJPEG preview (720p, 5 fps) for the web interface
- —No external dependency graph — cross-compilation fully within a Docker container
A native ACAP extension for the AXIS Q6078-E (ARTPEC-8, aarch64) that automatically corrects camera mounting errors: the live image is rotated based on the current pan angle, controlled by a user-configurable mapping table with up to 360 control points and linear interpolation.
Background
The AXIS Q6078-E is a motorised PTZ camera for demanding surveillance installations. In practice, the camera can rarely be mounted perfectly plumb — even small angular deviations of the bracket cause a measurable horizon tilt in the image depending on the pan position. Software-based image stabilisation does not solve this problem, as the error is angle-dependent and a single uniform offset is not sufficient.
axis_ptz_rotate closes this gap: a freely editable lookup table maps each pan angle to an individual rotation correction value. The ACAP linearly interpolates between control points, automatically calculates the minimum zoom factor to avoid image cropping, and applies the correction GPU-accelerated to every frame — without touching the primary camera pipeline.
Core Features
- Real-time image rotation via GPU shader (OpenGL ES) based on pan angle
- User-defined mapping table (max. 360 entries, 1° resolution, linear interpolation, 360° wrap-around)
- Auto-zoom prevents black corners after rotation — calculated from the maximum correction value in the table
- Event-driven update exclusively when the PTZ motor is stationary (no blur from interpolation during movement)
- Separate H.264 RTSP output stream — the primary camera stream remains unmodified
- MJPEG preview (720p, 5 fps) for the web interface
- No external dependency graph — cross-compilation fully within a Docker container
Technical Stack
ACAP Core (C99)
- ACAP Native SDK 1.15 (
axisecp/acap-native-sdk:1.15-aarch64-ubuntu22.04), target architecture aarch64 / ARTPEC-8 - VDO API for NV12 frame capture at configurable resolution (1080p / 2K / 4K)
- VAPIX Event Stream for PTZ position detection; automatic fallback to 200ms polling when events are absent
- HTTP CGI handler via the camera's internal lighttpd (
/local/ptzrotate/api/...) - Persistence under
/usr/local/packages/ptzrotate/localdata/(mapping.json, settings.json)
GPU Pipeline (OpenGL ES)
- EGL surfaceless context directly on the ARTPEC-8
- NV12 frames as dual-texture (Y-plane + UV-plane), rotation via fragment shader
- 2D rotation matrix around image centre, scaled with auto-zoom factor
s = max(|cos α| + (h/w)·|sin α|, |cos α| + (w/h)·|sin α|) - Render to FBO, readback as NV12 for the encoder
Output
- H.264 / RTSP — separate stream
rtsp://<cam>/local/ptzrotate/stream.h264 - MJPEG preview —
multipart/x-mixed-replaceon/local/ptzrotate/api/preview.mjpg, shared-file tee withstb_image_write.h(MIT/PD, vendored)
Web Interface (Vanilla JS)
- No build step, no framework — static files under
html/ - Live preview: camera MJPEG (left) vs. rotated plugin stream (right)
- Mapping table editor: add / delete rows, sort, JSON import/export
- Calibration assistant: step-by-step through all control points with real-time preview
- Settings page: resolution, frame rate, activation
Architecture
AXIS Q6078-E (ARTPEC-8)
│
├─ VAPIX Event Stream ──────────────────┐
│ (PTZ stopped event) │
│ Fallback: 200ms Polling ▼
│ PTZ Service
│ (pan_angle, is_moving)
│ │
│ lookup + Interpolation
│ │
├─ VDO Capture (NV12) ─────────▶ GPU Rotator (OpenGL ES)
│ │
│ H.264 Encoder
│ ├── RTSP Stream
│ └── MJPEG 720p Preview
│
└─ Web UI (lighttpd / CGI)
├── Live preview (Input ◀│▶ Output)
├── Mapping table editor
├── Calibration assistant
└── Settings / Import / Export
Development Phases
| Phase | Content | Status |
|---|---|---|
| 0a | SDK ↔ firmware compatibility | ✅ Complete |
| 0b | Toolchain & simulator strategy | ✅ Complete |
| 1 | ACAP skeleton (manifest, main loop, web UI shell) | ✅ Complete |
| 2 | PTZ service (event subscription + polling fallback) | ✅ Complete |
| 3 | Mapping table module (interpolation, persistence, HTTP API) | ✅ Complete |
| 4a | VDO capture (real + synthetic test pattern) | ✅ Complete |
| 4b | GPU rotation shader (OpenGL ES, zoom, FBO readback) | ✅ Complete |
| 4c | H.264 encoder + RTSP server (permissively licensed) | ✅ Complete |
| 5 | MJPEG preview endpoint | ✅ Complete |
| 6 | Calibration assistant (backend) | ✅ Complete |
| 7 | Web interface (preview, table editor, calibration) | ✅ Complete |
| 8 | Manual on-device verification (via VPN by the user) | Open |
| 9 | Packaging & documentation | Open |
Current Status
All simulator-verifiable phases (0a–7) are complete. The web interface is fully functional; the GPU rendering path, the calibration system, and the complete HTTP API are implemented. Outstanding: manual verification on the physical camera (phase 8, requires VPN access) and final packaging (phase 9).
The ACAP is installed as an unsigned EAP via sideload. A possible future release through the Axis App Store remains open due to the chosen dependency policy (exclusively MIT/BSD/Apache-2.0 licensed dependencies).
Proprietary — all rights reserved.