diff --git a/.gitignore b/.gitignore index c6b9373..8db26c5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ dist/ .env *.log logs/ -public/snapshots/ \ No newline at end of file +public/snapshots/ + +# Aruco detection results +test/data/screenShots/*.json \ No newline at end of file diff --git a/programs/01_read_ArUco_jpg_to_json.py b/programs/01_read_ArUco_jpg_to_json.py new file mode 100644 index 0000000..c18a145 --- /dev/null +++ b/programs/01_read_ArUco_jpg_to_json.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import hashlib +import time +from typing import Dict, Any + +import cv2 +import numpy as np + + +# ------------------------------------------------------------ +# Utilities +# ------------------------------------------------------------ + +def load_intrinsics_npz(npz_path: str): + data = np.load(npz_path) + + for k in ('camera_matrix', 'mtx', 'K'): + if k in data: + K = data[k].astype(np.float32) + break + else: + raise KeyError('Camera matrix not found in npz') + + for k in ('dist_coeffs', 'dist', 'D'): + if k in data: + D = data[k].astype(np.float32).reshape(-1, 1) + break + else: + D = np.zeros((5,1), dtype=np.float32) + + return K, D + + +# ------------------------------------------------------------ + +def get_aruco_detector(dict_name: str): + mapping = { + 'DICT_4X4_250': cv2.aruco.DICT_4X4_250, + 'DICT_5X5_100': cv2.aruco.DICT_5X5_100, + 'DICT_6X6_250': cv2.aruco.DICT_6X6_250, + 'DICT_ARUCO_ORIGINAL': cv2.aruco.DICT_ARUCO_ORIGINAL, + } + + dict_id = mapping.get(dict_name, cv2.aruco.DICT_4X4_250) + + dictionary = cv2.aruco.getPredefinedDictionary(dict_id) + + try: + params = cv2.aruco.DetectorParameters() + except Exception: + params = cv2.aruco.DetectorParameters_create() + + try: + detector = cv2.aruco.ArucoDetector(dictionary, params) + return detector, None + except Exception: + return None, (dictionary, params) + + +# ------------------------------------------------------------ + +def detect_markers(image, detector_tuple): + detector, fallback = detector_tuple + + if detector is not None: + corners, ids, rejected = detector.detectMarkers(image) + else: + dictionary, params = fallback + corners, ids, rejected = cv2.aruco.detectMarkers( + image, + dictionary, + parameters=params + ) + + return corners, ids, rejected + + +# ------------------------------------------------------------ + +def compute_sharpness(gray_roi): + if gray_roi.size == 0: + return 0.0 + + return float(cv2.Laplacian(gray_roi, cv2.CV_64F).var()) + + +# ------------------------------------------------------------ + +def compute_local_stats(gray_roi): + if gray_roi.size == 0: + return 0.0, 0.0 + + mean = float(np.mean(gray_roi)) + std = float(np.std(gray_roi)) + + return mean, std + + +# ------------------------------------------------------------ + +def hash_file(path): + sha = hashlib.sha256() + + with open(path, 'rb') as f: + while True: + chunk = f.read(1024 * 1024) + if not chunk: + break + sha.update(chunk) + + return sha.hexdigest() + + +# ------------------------------------------------------------ + +def main(): + + parser = argparse.ArgumentParser() + + parser.add_argument('-i', '--image', required=True) + parser.add_argument('-npz', '--intrinsics', required=True) + parser.add_argument('-cameraId', '--cameraId', required=True) + + parser.add_argument( + '--dict', + default='DICT_4X4_250' + ) + + parser.add_argument( + '-o', + '--output', + default=None + ) + + args = parser.parse_args() + + image = cv2.imread(args.image) + + if image is None: + raise RuntimeError(f'Cannot read image: {args.image}') + + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + + h, w = gray.shape[:2] + + K, D = load_intrinsics_npz(args.intrinsics) + + detector_tuple = get_aruco_detector(args.dict) + + corners_list, ids, rejected = detect_markers(gray, detector_tuple) + + observations = [] + + if ids is not None: + + ids = ids.flatten().tolist() + + for i, marker_id in enumerate(ids): + + corners = corners_list[i].reshape((4,2)).astype(np.float32) + + center = corners.mean(axis=0) + + area_px = float(cv2.contourArea(corners)) + + perimeter_px = float(cv2.arcLength(corners, True)) + + x_min = float(np.min(corners[:,0])) + x_max = float(np.max(corners[:,0])) + y_min = float(np.min(corners[:,1])) + y_max = float(np.max(corners[:,1])) + + bbox_x = int(max(0, np.floor(x_min))) + bbox_y = int(max(0, np.floor(y_min))) + bbox_w = int(min(w - bbox_x, np.ceil(x_max - x_min))) + bbox_h = int(min(h - bbox_y, np.ceil(y_max - y_min))) + + roi = gray[ + bbox_y:bbox_y+bbox_h, + bbox_x:bbox_x+bbox_w + ] + + sharpness = compute_sharpness(roi) + + mean_gray, std_gray = compute_local_stats(roi) + + edge_lengths = [] + + for k in range(4): + p1 = corners[k] + p2 = corners[(k+1)%4] + edge_lengths.append(float(np.linalg.norm(p1 - p2))) + + edge_ratio = ( + max(edge_lengths) / max(1e-6, min(edge_lengths)) + ) + + obs = { + 'marker_id': int(marker_id), + + 'corners_px': corners.tolist(), + + 'center_px': center.tolist(), + + 'bbox_px': { + 'x': x_min, + 'y': y_min, + 'w': x_max - x_min, + 'h': y_max - y_min + }, + + 'quality': { + 'area_px': area_px, + 'perimeter_px': perimeter_px, + 'sharpness_laplacian': sharpness, + 'mean_gray': mean_gray, + 'std_gray': std_gray, + 'edge_ratio': edge_ratio, + 'edge_lengths_px': edge_lengths + } + } + + observations.append(obs) + + output = { + 'schema_version': '1.0', + + 'created_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), + + 'camera': { + 'camera_id': args.cameraId, + 'intrinsics_file': os.path.abspath(args.intrinsics), + 'camera_matrix': K.tolist(), + 'distortion_coefficients': D.reshape(-1).tolist() + }, + + 'image': { + 'image_file': os.path.abspath(args.image), + 'image_sha256': hash_file(args.image), + 'width_px': int(w), + 'height_px': int(h) + }, + + 'aruco': { + 'dictionary': args.dict, + 'num_detected_markers': len(observations), + 'num_rejected_candidates': int(len(rejected)) + }, + + 'observations': observations + } + + if args.output is None: + base = os.path.splitext(args.image)[0] + out_json = f'{base}_aruco_detection.json' + else: + out_json = args.output + + with open(out_json, 'w', encoding='utf-8') as f: + json.dump(output, f, indent=2) + + print(f'Saved: {out_json}') + + +# ------------------------------------------------------------ + +if __name__ == '__main__': + main() diff --git a/test/01_read_ArUco.test.js b/test/01_read_ArUco.test.js new file mode 100644 index 0000000..1a9dcbb --- /dev/null +++ b/test/01_read_ArUco.test.js @@ -0,0 +1,57 @@ +// test/aruco.test.js +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +describe('Aruco Detection Script', () => { + const scriptPath = 'programs/01_read_Aruco_jpg_to_json.py'; + const calibrationFile = 'data/settings/callibration_cam0.npz'; + const screenshotsDir = 'test/data/screenShots'; + + test('should exist and be executable', () => { + expect(fs.existsSync(scriptPath)).toBe(true); + }); + + test('should have calibration file', () => { + expect(fs.existsSync(calibrationFile)).toBe(true); + }); + + test('should have screenshot directory', () => { + expect(fs.existsSync(screenshotsDir)).toBe(true); + }); + + test('should process screenshots successfully', () => { + const screenshots = fs.readdirSync(screenshotsDir) + .filter(file => file.endsWith('.jpg') || file.endsWith('.png')); + + expect(screenshots.length).toBeGreaterThan(0); + + screenshots.forEach(screenshot => { + const screenshotPath = path.join(screenshotsDir, screenshot); + const jsonPath = screenshotPath.replace(/\.(jpg|png)$/i, '_aruco_detection.json'); + + // Lösche alte JSON-Datei, falls vorhanden + if (fs.existsSync(jsonPath)) { + fs.unlinkSync(jsonPath); + } + + // Führe das Python-Skript aus + const cmd = `python ${scriptPath} -i ${screenshotPath} -npz ${calibrationFile} -cameraId 0`; + + try { + execSync(cmd, { stdio: 'inherit' }); + + // Überprüfe, ob JSON-Datei erstellt wurde + expect(fs.existsSync(jsonPath)).toBe(true); + + // Lese und prüfe JSON-Inhalt + const jsonData = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); + expect(jsonData).toBeDefined(); + expect(typeof jsonData).toBe('object'); + + } catch (error) { + fail(`Failed to process ${screenshot}: ${error.message}`); + } + }); + }); +}); \ No newline at end of file