01 read Aruco
start of the new tool chain
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -3,4 +3,7 @@ dist/
|
|||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
public/snapshots/
|
public/snapshots/
|
||||||
|
|
||||||
|
# Aruco detection results
|
||||||
|
test/data/screenShots/*.json
|
||||||
272
programs/01_read_ArUco_jpg_to_json.py
Normal file
272
programs/01_read_ArUco_jpg_to_json.py
Normal file
@@ -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()
|
||||||
57
test/01_read_ArUco.test.js
Normal file
57
test/01_read_ArUco.test.js
Normal file
@@ -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}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user