From cdb3165fad60bfff0b6afb4b4f4a4feff9441c48 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Wed, 10 Jun 2026 07:51:34 +0200 Subject: [PATCH] API mit .npz --- data/calibration/cam0/calibration.npz | Bin 0 -> 646 bytes data/calibration/cam1/calibration.npz | Bin 0 -> 646 bytes data/calibration/cam2/calibration.npz | Bin 0 -> 646 bytes server.js | 18 ++++- setup/README.md | 79 +++++++++++++++++++++ setup/calibrate.py | 77 +++++++++++++++++++++ setup/checkerboard_11x8_25mm.pdf | 95 ++++++++++++++++++++++++++ src/snapshotService.js | 24 ++++++- 8 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 data/calibration/cam0/calibration.npz create mode 100644 data/calibration/cam1/calibration.npz create mode 100644 data/calibration/cam2/calibration.npz create mode 100644 setup/README.md create mode 100644 setup/calibrate.py create mode 100644 setup/checkerboard_11x8_25mm.pdf diff --git a/data/calibration/cam0/calibration.npz b/data/calibration/cam0/calibration.npz new file mode 100644 index 0000000000000000000000000000000000000000..f705b712ebc67df9b1d3550b2fea4cd4a8387f50 GIT binary patch literal 646 zcmWIWW@gc4fB;2?_KnJw|DiyTL4+YWF*mg+F+Ml3q$smOFR!4IkwJjr1XMYUp6nOu z8xYCJP{vTLo|0OeT%>NLpl*|9p{}E#o|a!!Qk0k%pI?-c3KDlq%qdO72-f)RIm>fquq z^$_}leE>>OsNCP30SgL#U{IuF7MH{)=clHn6~h8z1=Iu>O;JD?>L{2}7!dPZ%QDW$ zPqa6YRIItS@VR}5hUodfON8tt!ncSOu1T^t*_qREcPrC@0B=SnT?W)x2Sp|b3qt(_ uq8b=MBt{xQ*A5CPP!NDHKS&1@G%#`l4L}RG0B=?{kUSF*)&S`*U^@WyO_vM+ literal 0 HcmV?d00001 diff --git a/data/calibration/cam1/calibration.npz b/data/calibration/cam1/calibration.npz new file mode 100644 index 0000000000000000000000000000000000000000..2887aa7da9587e4e91276cd02a9c318fe669b857 GIT binary patch literal 646 zcmWIWW@gc4fB;1X{NLpl*|9p{}E#o|a!!Qk0k%pI?-c3KDlq%qdO7-Mz6gU&fj3TV{$?7G>7l5eVMvub#QT* zdIsx{NwKU_rqT42qP@;*$8}{M59xVpu?|fSLfKDGCTf9R*Vg1ES|l$c13; z8TQuxNo%$0p4)GpbaJ;yXt90ax-a>v50mV7rOgkYH<9r`fHxzPE(2<;gCY}z1)+Wd uQ4NeB5+e3@upI!$!6(sJKm{Xhz6fe$5EJy|N zHH>u>j5T!>Y8A)^T*@(Rr%bO;b$~jHp?`sZp^ii^oZk^EKTqhszDY%C!h(Vy7!)a)#U=5{`Kf7X#jt=_0W|?eQxp(}Itr!~280enQb)p? z1@_BsUlMy%cWnQ?thkvT%RKi>e#u{0m*c*F=e`XgJ6sO~J<7 camerasJson, diff --git a/setup/README.md b/setup/README.md new file mode 100644 index 0000000..71be141 --- /dev/null +++ b/setup/README.md @@ -0,0 +1,79 @@ +# Kamera-Kalibrierung einrichten + +Jede Kamera braucht eine Kalibrierungsdatei (`calibration.npz`), die +Kameramatrix und Verzerrungskoeffizienten enthält. +Die Datei wird beim Serverstart einmalig in den RAM geladen und über +`GET /api/cameras/{id}/calibration` ausgeliefert. + +--- + +## Voraussetzungen + +``` +pip install opencv-python numpy requests +``` + +--- + +## Schritt 1 – Schachbrettmuster drucken + +Datei: [`checkerboard_11x8_25mm.pdf`](checkerboard_11x8_25mm.pdf) + +- **A4 Querformat, 100 % drucken** (keine Skalierung, kein „An Seite anpassen") +- Muster: 11 × 8 Felder → **10 × 7 innere Ecken**, 25 mm pro Feld +- Auf festes Papier oder Karton drucken; flach und verwindungssteif halten + +--- + +## Schritt 2 – Fotos aufnehmen + +Server läuft auf `thinkcentre.local:8444`. Skript schießt für alle Kameras +gleichzeitig Hires-Aufnahmen und legt sie in `test/files/{camId}/` ab: + +```bash +python test/grabSnapShot.py +``` + +Das Schachbrettmuster dabei aus **verschiedenen Winkeln und Positionen** halten +(mind. 15–20 Aufnahmen pro Kamera). +Bilder landen unter `test/files/cam0/`, `test/files/cam1/`, `test/files/cam2/`. + +--- + +## Schritt 3 – Kalibrierung berechnen + +Für jede Kamera einmal ausführen: + +```bash +python setup/calibrate.py test/files/cam0 data/calibration/cam0/calibration.npz +python setup/calibrate.py test/files/cam1 data/calibration/cam1/calibration.npz +python setup/calibrate.py test/files/cam2 data/calibration/cam2/calibration.npz +``` + +Ein guter RMS-Wert liegt unter **0.5 px**. Wenn der Wert zu hoch ist: +- Bilder mit schlechter Beleuchtung oder Bewegungsunschärfe löschen +- Schachbrettmuster flacher halten +- Mehr Aufnahmen aus verschiedenen Winkeln machen + +--- + +## Schritt 4 – Dateien committen + +Die `.npz`-Dateien sind Deployment-Konfiguration und gehören ins Repository: + +```bash +git add data/calibration/ +git commit -m "Kalibrierungsdaten für cam0–cam2" +``` + +--- + +## Ergebnis prüfen + +```bash +curl http://thinkcentre.local:8444/api/cameras +# → jede Kamera mit "calibrationUrl": "/api/cameras/cam0/calibration" + +curl -o cam0.npz http://thinkcentre.local:8444/api/cameras/cam0/calibration +python -c "import numpy as np; d=np.load('cam0.npz'); print(d['camera_matrix'])" +``` diff --git a/setup/calibrate.py b/setup/calibrate.py new file mode 100644 index 0000000..88e47ad --- /dev/null +++ b/setup/calibrate.py @@ -0,0 +1,77 @@ +""" +Berechnet eine Kamera-Kalibrierung aus Schachbrett-Fotos und speichert das +Ergebnis als .npz. + +Aufruf: + python calibrate.py + +Beispiel: + python setup/calibrate.py test/files/cam0 data/calibration/cam0/calibration.npz + +Erwartet *.jpg-Dateien im Bildordner, die ein 10x7-Eck-Schachbrettmuster +(= 11x8 Felder, 25 mm/Feld) zeigen. +""" + +import sys +import glob +import pathlib +import numpy as np +import cv2 + +CHECKERBOARD = (10, 7) # innere Ecken +SQUARE_MM = 25.0 / 1000 # 25 mm in Metern + +def calibrate(image_dir: str, out_path: str) -> None: + images = sorted(glob.glob(str(pathlib.Path(image_dir) / "*.jpg"))) + if not images: + sys.exit(f"Keine .jpg-Dateien in {image_dir!r}") + print(f"Gefundene Bilder: {len(images)}") + + objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) + objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) + objp *= SQUARE_MM + + objpoints, imgpoints = [], [] + img_size = None + + for fname in images: + img = cv2.imread(fname) + if img is None: + print(f" WARNUNG: konnte nicht lesen: {fname}") + continue + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + if img_size is None: + img_size = gray.shape[::-1] + + ret, corners = cv2.findChessboardCorners( + gray, CHECKERBOARD, + cv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE | cv2.CALIB_CB_FAST_CHECK, + ) + if ret: + corners2 = cv2.cornerSubPix( + gray, corners, (11, 11), (-1, -1), + (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6), + ) + objpoints.append(objp) + imgpoints.append(corners2) + print(f" OK {pathlib.Path(fname).name}") + else: + print(f" -- {pathlib.Path(fname).name} (keine Ecken gefunden)") + + print(f"\nGültige Bilder: {len(objpoints)} / {len(images)}") + if not objpoints: + sys.exit("Kalibrierung fehlgeschlagen: keine Ecken in keinem Bild gefunden.") + + rms, K, D, _rvecs, _tvecs = cv2.calibrateCamera(objpoints, imgpoints, img_size, None, None) + print(f"RMS-Fehler: {rms:.4f} px") + print(f"Kameramatrix:\n{K}") + print(f"Verzerrungskoeff: {D}") + + pathlib.Path(out_path).parent.mkdir(parents=True, exist_ok=True) + np.savez(out_path, camera_matrix=K, dist_coeffs=D) + print(f"\nGespeichert: {out_path}") + +if __name__ == "__main__": + if len(sys.argv) != 3: + sys.exit(__doc__) + calibrate(sys.argv[1], sys.argv[2]) diff --git a/setup/checkerboard_11x8_25mm.pdf b/setup/checkerboard_11x8_25mm.pdf new file mode 100644 index 0000000..da4defd --- /dev/null +++ b/setup/checkerboard_11x8_25mm.pdf @@ -0,0 +1,95 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +5 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> +endobj +4 0 obj +<< /Length 1465 >> +stream +q +0 g +31.18 14.18 70.87 70.87 re +172.91 14.18 70.87 70.87 re +314.65 14.18 70.87 70.87 re +456.38 14.18 70.87 70.87 re +598.11 14.18 70.87 70.87 re +739.84 14.18 70.87 70.87 re +102.05 85.04 70.87 70.87 re +243.78 85.04 70.87 70.87 re +385.51 85.04 70.87 70.87 re +527.24 85.04 70.87 70.87 re +668.98 85.04 70.87 70.87 re +31.18 155.91 70.87 70.87 re +172.91 155.91 70.87 70.87 re +314.65 155.91 70.87 70.87 re +456.38 155.91 70.87 70.87 re +598.11 155.91 70.87 70.87 re +739.84 155.91 70.87 70.87 re +102.05 226.77 70.87 70.87 re +243.78 226.77 70.87 70.87 re +385.51 226.77 70.87 70.87 re +527.24 226.77 70.87 70.87 re +668.98 226.77 70.87 70.87 re +31.18 297.64 70.87 70.87 re +172.91 297.64 70.87 70.87 re +314.65 297.64 70.87 70.87 re +456.38 297.64 70.87 70.87 re +598.11 297.64 70.87 70.87 re +739.84 297.64 70.87 70.87 re +102.05 368.51 70.87 70.87 re +243.78 368.51 70.87 70.87 re +385.51 368.51 70.87 70.87 re +527.24 368.51 70.87 70.87 re +668.98 368.51 70.87 70.87 re +31.18 439.37 70.87 70.87 re +172.91 439.37 70.87 70.87 re +314.65 439.37 70.87 70.87 re +456.38 439.37 70.87 70.87 re +598.11 439.37 70.87 70.87 re +739.84 439.37 70.87 70.87 re +102.05 510.24 70.87 70.87 re +243.78 510.24 70.87 70.87 re +385.51 510.24 70.87 70.87 re +527.24 510.24 70.87 70.87 re +668.98 510.24 70.87 70.87 re +f +Q +q +0.5 w +0 G +31.18 14.18 779.53 566.93 re +S +Q +BT +/F1 7.5 Tf +31.18 3.18 Td +(Kalibrierungsmuster | 11x8 Felder | 10x7 innere Ecken | 25 mm/Feld | A4 Querformat, 100% drucken ohne Skalierung) Tj +ET +endstream +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R + /MediaBox [0 0 841.89 595.28] + /Contents 4 0 R + /Resources << /ProcSet [/PDF /Text] /Font << /F1 5 0 R >> >> +>> +endobj +xref +0 6 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000001707 00000 n +0000000191 00000 n +0000000121 00000 n +trailer +<< /Size 6 /Root 1 0 R >> +startxref +1870 +%%EOF diff --git a/src/snapshotService.js b/src/snapshotService.js index 53af369..eba9e51 100644 --- a/src/snapshotService.js +++ b/src/snapshotService.js @@ -190,14 +190,32 @@ function createStreamRouter(switches) { return router; } -// GET /api/cameras → vollständige Kamera-Metadaten (ohne device-Pfad) -function createCamerasRouter(cameras) { +// GET /api/cameras → Kamera-Metadaten (ohne device-Pfad) +// GET /api/cameras/:id/calibration → .npz aus RAM (geladen beim Start) +function createCamerasRouter(cameras, calibrations = {}) { const router = express.Router(); + router.get('/', (_req, res) => { res.json({ - cameras: cameras.map(({ device: _d, ...rest }) => rest), + cameras: cameras.map(({ device: _d, ...rest }) => ({ + ...rest, + ...(calibrations[rest.id] ? { calibrationUrl: `/api/cameras/${rest.id}/calibration` } : {}), + })), }); }); + + router.get('/:id/calibration', (req, res) => { + const buf = calibrations[req.params.id]; + if (!buf) return res.status(404).json({ error: `Keine Kalibrierdaten für: ${req.params.id}` }); + res.set({ + 'Content-Type': 'application/octet-stream', + 'Content-Length': buf.length, + 'Content-Disposition': `attachment; filename="${req.params.id}_calibration.npz"`, + 'Cache-Control': 'public, max-age=86400', + }); + res.end(buf); + }); + return router; }