Callibration File API
This commit is contained in:
145
README.md
145
README.md
@@ -33,6 +33,151 @@ http://<host>:8444/api/cameras Kamera-Metadaten (JSON)
|
|||||||
http://<host>:8444/health Status
|
http://<host>:8444/health Status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## API-Referenz
|
||||||
|
|
||||||
|
Basis-URL: `http://<host>:8444`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /api/cameras`
|
||||||
|
|
||||||
|
Vollständige Kamera-Metadaten. Primärer Einstiegspunkt für andere Container.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cameras": [
|
||||||
|
{
|
||||||
|
"id": "cam0",
|
||||||
|
"name": "Kamera 0",
|
||||||
|
"position": "front",
|
||||||
|
"stream": true,
|
||||||
|
"hires": true,
|
||||||
|
"encode": "copybsf",
|
||||||
|
"mseCodec": null,
|
||||||
|
"note": "usb-046d_0825_3BB3FE20-video-index0",
|
||||||
|
"calibrationUrl": "/api/cameras/cam0/calibration"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`calibrationUrl` fehlt, wenn keine `.npz` unter `data/calibration/{id}/` liegt.
|
||||||
|
`encode`: `"copybsf"` (MJPEG-Copy, Default) | `"mjpeg"` (Re-Encode) | `"h264"` (GPU, MSE).
|
||||||
|
`mseCodec`: nur bei `encode="h264"` gesetzt, z.B. `"avc1.4D001F"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /api/cameras/{id}/calibration`
|
||||||
|
|
||||||
|
Liefert die `.npz`-Kalibrierungsdatei (Kameramatrix + Verzerrungskoeffizienten) als Binary.
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
Content-Disposition: attachment; filename="cam0_calibration.npz"
|
||||||
|
Cache-Control: public, max-age=86400
|
||||||
|
```
|
||||||
|
|
||||||
|
404 wenn keine Kalibrierung vorhanden. Einlesen in Python:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import numpy as np, requests
|
||||||
|
d = np.load(requests.get(".../api/cameras/cam0/calibration", stream=True).raw)
|
||||||
|
K, D = d["camera_matrix"], d["dist_coeffs"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /api/snapshot/{id}`
|
||||||
|
|
||||||
|
Letztes Live-JPEG (Live-Auflösung, z.B. 640×480) aus dem RAM-Puffer.
|
||||||
|
Bei `stream: false`: one-shot — öffnet Gerät kurz, schließt es wieder.
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: image/jpeg
|
||||||
|
X-Camera-Id: cam0
|
||||||
|
X-Frame-Width: 640
|
||||||
|
X-Timestamp: 2026-06-10T07:30:00.000Z
|
||||||
|
Cache-Control: no-store
|
||||||
|
```
|
||||||
|
|
||||||
|
503 wenn kein Frame verfügbar (Kamera nicht erreichbar).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /api/snapshot/{id}/hires`
|
||||||
|
|
||||||
|
HD-JPEG (volle Auflösung, z.B. 1280×960 oder 1920×1080).
|
||||||
|
Pausiert den Live-Stream kurz, nimmt ein Einzelbild auf, startet Live neu.
|
||||||
|
Gleiche Response-Headers wie `/api/snapshot/{id}`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /api/stream/{id}`
|
||||||
|
|
||||||
|
Live-Stream. Format hängt vom `encode`-Modus ab:
|
||||||
|
|
||||||
|
| encode | Content-Type | Player |
|
||||||
|
|---|---|---|
|
||||||
|
| `copybsf` / `mjpeg` | `multipart/x-mixed-replace; boundary=frame` | `<img src="...">` |
|
||||||
|
| `h264` | `video/mp4` (fragmentiertes MP4) | `<video>` via MSE |
|
||||||
|
|
||||||
|
Verbindung läuft bis der Client trennt. Langsame Clients droppen Frames (MJPEG)
|
||||||
|
bzw. werden getrennt (H.264, um Decode-Lücken zu vermeiden).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /api/config`
|
||||||
|
|
||||||
|
Laufzeit-Konfiguration aller Kameras.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"liveSizes": ["320x240", "640x480", "1280x960"],
|
||||||
|
"encodes": ["copybsf", "mjpeg", "h264"],
|
||||||
|
"cameras": [
|
||||||
|
{
|
||||||
|
"id": "cam0",
|
||||||
|
"name": "Kamera 0",
|
||||||
|
"liveSize": "640x480",
|
||||||
|
"stream": true,
|
||||||
|
"encode": "copybsf"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `POST /api/config`
|
||||||
|
|
||||||
|
Ändert Auflösung, Stream-Zustand oder Encoder einer oder mehrerer Kameras.
|
||||||
|
Änderungen werden in `cameras.json` gespeichert und sofort aktiv (Hot-Reload).
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Request Body
|
||||||
|
{ "cameras": [{ "id": "cam0", "liveSize": "320x240", "stream": true, "encode": "copybsf" }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle Felder ausser `id` sind optional. Antwort: wie `GET /api/config`.
|
||||||
|
400 bei ungültiger Auflösung, unbekannter Kamera-ID oder falschem Typ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `GET /health`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"cameras": [
|
||||||
|
{ "id": "cam0", "name": "Kamera 0", "device": "/dev/video0", "state": "running", "hasFrame": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`state`: `"running"` | `"idle"` | `"stopping"` — Zustand des FFmpeg-Prozesses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Deploy (Portainer)
|
## Deploy (Portainer)
|
||||||
|
|
||||||
1. Portainer → Stacks → Web editor → `docker-compose.yaml` einfügen
|
1. Portainer → Stacks → Web editor → `docker-compose.yaml` einfügen
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"hires": true,
|
"hires": true,
|
||||||
"note": "usb-046d_0825_3BB3FE20-video-index0",
|
"note": "usb-046d_0825_3BB3FE20-video-index0",
|
||||||
"hiresSize": "1280x960",
|
"hiresSize": "1280x960",
|
||||||
"liveSize": "320x240"
|
"liveSize": "320x240",
|
||||||
|
"calibrationFile": "data/calibration/cam0/calibration.npz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cam1",
|
"id": "cam1",
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
"hires": true,
|
"hires": true,
|
||||||
"note": "usb-046d_081b_342D4F40-video-index0",
|
"note": "usb-046d_081b_342D4F40-video-index0",
|
||||||
"hiresSize": "1280x960",
|
"hiresSize": "1280x960",
|
||||||
"liveSize": "320x240"
|
"liveSize": "320x240",
|
||||||
|
"calibrationFile": "data/calibration/cam1/calibration.npz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cam2",
|
"id": "cam2",
|
||||||
@@ -31,7 +33,8 @@
|
|||||||
"hires": true,
|
"hires": true,
|
||||||
"note": "usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0",
|
"note": "usb-046d_HD_Pro_Webcam_C920_9C5591DF-video-index0",
|
||||||
"hiresSize": "1920x1080",
|
"hiresSize": "1920x1080",
|
||||||
"liveSize": "320x240"
|
"liveSize": "320x240",
|
||||||
|
"calibrationFile": "data/calibration/cam2/calibration.npz"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ const MSE_CODEC = process.env.H264_MSE_CODEC ?? mseCodecString(H264.profile, pro
|
|||||||
function loadCalibrations(camsConfig) {
|
function loadCalibrations(camsConfig) {
|
||||||
const calib = {};
|
const calib = {};
|
||||||
for (const cam of camsConfig) {
|
for (const cam of camsConfig) {
|
||||||
const p = path.join(__dirname, 'data', 'calibration', cam.id, 'calibration.npz');
|
const p = cam.calibrationFile
|
||||||
|
? path.resolve(__dirname, cam.calibrationFile)
|
||||||
|
: path.join(__dirname, 'data', 'calibration', cam.id, 'calibration.npz');
|
||||||
if (fs.existsSync(p)) {
|
if (fs.existsSync(p)) {
|
||||||
calib[cam.id] = fs.readFileSync(p);
|
calib[cam.id] = fs.readFileSync(p);
|
||||||
console.log(` Kalibrierung: ${cam.id} (${calib[cam.id].length} Bytes)`);
|
console.log(` Kalibrierung: ${cam.id} (${calib[cam.id].length} Bytes)`);
|
||||||
|
|||||||
Reference in New Issue
Block a user