2 Camera Detection
Checke wo sich im Bild die Kamera befindet
This commit is contained in:
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug ArUco Detection",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/programs/1_detect_aruco_observations.py",
|
||||
"console": "integratedTerminal",
|
||||
"args": [
|
||||
"-i", "${workspaceFolder}/test/data/screenshots/snapshot_video1_1778819665744.jpg",
|
||||
"-npz", "${workspaceFolder}/data/settings/callibration_cam1.npz",
|
||||
"-robot", "${workspaceFolder}/test/data/robot/robot.json",
|
||||
"-cameraId", "cam1",
|
||||
"-outDir", "${workspaceFolder}/test/data/screenshots/1778819665744_detection_Step1_testResults"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,216 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def load_detection_json(path):
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def find_detection_files(directory, timestamp):
|
||||
|
||||
pattern = os.path.join(
|
||||
directory,
|
||||
f'*_{timestamp}_aruco_detection.json'
|
||||
)
|
||||
|
||||
files = sorted(glob.glob(pattern))
|
||||
|
||||
return files
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def build_scene(detection_files, timestamp):
|
||||
|
||||
scene = {
|
||||
|
||||
"schema_version": "1.0",
|
||||
|
||||
"scene_timestamp": str(timestamp),
|
||||
|
||||
"created_utc": time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime()
|
||||
),
|
||||
|
||||
"cameras": {},
|
||||
|
||||
"markers": {}
|
||||
}
|
||||
|
||||
marker_map = defaultdict(list)
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Read all camera detection jsons
|
||||
# --------------------------------------------------------
|
||||
|
||||
for json_file in detection_files:
|
||||
|
||||
data = load_detection_json(json_file)
|
||||
|
||||
cam = data["camera"]
|
||||
img = data["image"]
|
||||
|
||||
basename = os.path.basename(json_file)
|
||||
# snapshot_video1_1778819665744_aruco_detection.json
|
||||
parts = basename.split('_')
|
||||
video_part = parts[1] # "video1"
|
||||
camera_id = video_part.replace('video', '')
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Camera information
|
||||
# ----------------------------------------------------
|
||||
|
||||
scene["cameras"][camera_id] = {
|
||||
|
||||
"camera_id": camera_id,
|
||||
|
||||
"intrinsics_file": cam["intrinsics_file"],
|
||||
|
||||
"camera_matrix": cam["camera_matrix"],
|
||||
|
||||
"distortion_coefficients":
|
||||
cam["distortion_coefficients"],
|
||||
|
||||
"image": {
|
||||
|
||||
"image_file": img["image_file"],
|
||||
|
||||
"image_sha256": img["image_sha256"],
|
||||
|
||||
"width_px": img["width_px"],
|
||||
|
||||
"height_px": img["height_px"]
|
||||
}
|
||||
}
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Marker observations
|
||||
# ----------------------------------------------------
|
||||
|
||||
for obs in data["observations"]:
|
||||
|
||||
marker_id = str(obs["marker_id"])
|
||||
|
||||
observation = {
|
||||
|
||||
"camera_id": camera_id,
|
||||
|
||||
"center_px": obs["center_px"],
|
||||
|
||||
"corners_px": obs["corners_px"],
|
||||
|
||||
"bbox_px": obs["bbox_px"],
|
||||
|
||||
"quality": obs["quality"]
|
||||
}
|
||||
|
||||
marker_map[marker_id].append(observation)
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Convert marker map
|
||||
# --------------------------------------------------------
|
||||
|
||||
for marker_id, observations in marker_map.items():
|
||||
|
||||
scene["markers"][marker_id] = {
|
||||
|
||||
"marker_id": int(marker_id),
|
||||
|
||||
"num_observations": len(observations),
|
||||
|
||||
"observations": observations
|
||||
}
|
||||
|
||||
return scene
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
'-timestamp',
|
||||
required=True,
|
||||
help='Scene timestamp'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-dir',
|
||||
default='.',
|
||||
help='Directory containing detection json files'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-o',
|
||||
'--output',
|
||||
default=None
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
detection_files = find_detection_files(
|
||||
args.dir,
|
||||
args.timestamp
|
||||
)
|
||||
|
||||
if len(detection_files) == 0:
|
||||
|
||||
raise RuntimeError(
|
||||
f'No detection json files found for timestamp '
|
||||
f'{args.timestamp}'
|
||||
)
|
||||
|
||||
print('Found detection files:')
|
||||
|
||||
for f in detection_files:
|
||||
print(' ', f)
|
||||
|
||||
scene = build_scene(
|
||||
detection_files,
|
||||
args.timestamp
|
||||
)
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Output filename
|
||||
# --------------------------------------------------------
|
||||
|
||||
if args.output is None:
|
||||
|
||||
out_json = os.path.join(
|
||||
args.dir,
|
||||
f'scene_{args.timestamp}.json'
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
out_json = args.output
|
||||
|
||||
with open(out_json, 'w', encoding='utf-8') as f:
|
||||
|
||||
json.dump(scene, f, indent=2)
|
||||
|
||||
print()
|
||||
print(f'Saved scene: {out_json}')
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,587 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
import time
|
||||
import uuid
|
||||
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 load_robot_vision_config(robot_json_path: str):
|
||||
|
||||
with open(robot_json_path, 'r', encoding='utf-8') as f:
|
||||
robot = json.load(f)
|
||||
|
||||
vision_config = robot.get('vision_config', {})
|
||||
|
||||
marker_type = vision_config.get('MarkerType', 'DICT_4X4_250')
|
||||
marker_size = float(vision_config.get('MarkerSize', 0.025))
|
||||
|
||||
return {
|
||||
'MarkerType': marker_type,
|
||||
'MarkerSize': marker_size
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
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 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 polygon_mask(shape, polygon):
|
||||
|
||||
mask = np.zeros(shape, dtype=np.uint8)
|
||||
|
||||
cv2.fillConvexPoly(
|
||||
mask,
|
||||
polygon.astype(np.int32),
|
||||
255
|
||||
)
|
||||
|
||||
return mask
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def shrink_polygon(points, scale=0.80):
|
||||
|
||||
center = np.mean(points, axis=0)
|
||||
|
||||
shrunk = center + (points - center) * scale
|
||||
|
||||
return shrunk.astype(np.float32)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def compute_sharpness(gray_image, polygon):
|
||||
|
||||
shrunk = shrink_polygon(polygon, scale=0.80)
|
||||
|
||||
mask = polygon_mask(gray_image.shape, shrunk)
|
||||
|
||||
pixels = gray_image[mask == 255]
|
||||
|
||||
if pixels.size == 0:
|
||||
return 0.0
|
||||
|
||||
temp = np.zeros_like(gray_image)
|
||||
temp[mask == 255] = gray_image[mask == 255]
|
||||
|
||||
lap = cv2.Laplacian(temp, cv2.CV_64F)
|
||||
|
||||
values = lap[mask == 255]
|
||||
|
||||
if values.size == 0:
|
||||
return 0.0
|
||||
|
||||
return float(values.var())
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def compute_contrast(gray_image, polygon):
|
||||
|
||||
shrunk = shrink_polygon(polygon, scale=0.80)
|
||||
|
||||
mask = polygon_mask(gray_image.shape, shrunk)
|
||||
|
||||
pixels = gray_image[mask == 255]
|
||||
|
||||
if pixels.size == 0:
|
||||
|
||||
return {
|
||||
'p05': 0.0,
|
||||
'p95': 0.0,
|
||||
'dynamic_range': 0.0,
|
||||
'mean_gray': 0.0,
|
||||
'std_gray': 0.0
|
||||
}
|
||||
|
||||
p05 = float(np.percentile(pixels, 5))
|
||||
p95 = float(np.percentile(pixels, 95))
|
||||
|
||||
return {
|
||||
'p05': p05,
|
||||
'p95': p95,
|
||||
'dynamic_range': float(p95 - p05),
|
||||
'mean_gray': float(np.mean(pixels)),
|
||||
'std_gray': float(np.std(pixels))
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def compute_edge_ratio(corners):
|
||||
|
||||
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))
|
||||
)
|
||||
|
||||
return edge_ratio, edge_lengths
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def compute_geometry_metrics(center, corners, width, height):
|
||||
|
||||
image_center = np.array(
|
||||
[width / 2.0, height / 2.0],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
dist_center = np.linalg.norm(center - image_center)
|
||||
|
||||
max_dist = np.linalg.norm(image_center)
|
||||
|
||||
distance_center_norm = float(
|
||||
dist_center / max(1e-6, max_dist)
|
||||
)
|
||||
|
||||
min_x = np.min(corners[:, 0])
|
||||
max_x = np.max(corners[:, 0])
|
||||
|
||||
min_y = np.min(corners[:, 1])
|
||||
max_y = np.max(corners[:, 1])
|
||||
|
||||
border_distance_px = float(min(
|
||||
min_x,
|
||||
min_y,
|
||||
width - max_x,
|
||||
height - max_y
|
||||
))
|
||||
|
||||
return {
|
||||
'distance_to_center_norm': distance_center_norm,
|
||||
'distance_to_border_px': border_distance_px
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def compute_confidence(
|
||||
area_px,
|
||||
sharpness,
|
||||
edge_ratio,
|
||||
dynamic_range,
|
||||
border_distance_px
|
||||
):
|
||||
|
||||
score = 1.0
|
||||
|
||||
# area
|
||||
score *= min(1.0, area_px / 1500.0)
|
||||
|
||||
# sharpness
|
||||
score *= min(1.0, sharpness / 120.0)
|
||||
|
||||
# edge distortion
|
||||
score *= 1.0 / max(1.0, edge_ratio)
|
||||
|
||||
# contrast
|
||||
score *= min(1.0, dynamic_range / 80.0)
|
||||
|
||||
# border distance
|
||||
score *= min(1.0, max(0.0, border_distance_px) / 50.0)
|
||||
|
||||
score = max(0.0, min(1.0, score))
|
||||
|
||||
return float(score)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
'-i',
|
||||
'--image',
|
||||
required=True
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-npz',
|
||||
'--intrinsics',
|
||||
required=True
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-robot',
|
||||
'--robot',
|
||||
required=True
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-cameraId',
|
||||
'--cameraId',
|
||||
required=True,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-outDir',
|
||||
'--outDir',
|
||||
required=True
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
os.makedirs(args.outDir, exist_ok=True)
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Load robot vision config
|
||||
# --------------------------------------------------------
|
||||
|
||||
vision_config = load_robot_vision_config(args.robot)
|
||||
|
||||
marker_type = vision_config['MarkerType']
|
||||
marker_size = vision_config['MarkerSize']
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Load image
|
||||
# --------------------------------------------------------
|
||||
|
||||
image = cv2.imread(args.image)
|
||||
|
||||
if image is None:
|
||||
raise RuntimeError(f'Cannot read image: {args.image}')
|
||||
|
||||
gray = cv2.cvtColor(
|
||||
image,
|
||||
cv2.COLOR_BGR2GRAY
|
||||
)
|
||||
|
||||
height, width = gray.shape[:2]
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Intrinsics
|
||||
# --------------------------------------------------------
|
||||
|
||||
K, D = load_intrinsics_npz(args.intrinsics)
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Detection
|
||||
# --------------------------------------------------------
|
||||
|
||||
detector_tuple = get_aruco_detector(marker_type)
|
||||
|
||||
corners_list, ids, rejected = detect_markers(
|
||||
gray,
|
||||
detector_tuple
|
||||
)
|
||||
|
||||
detections = []
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Valid detections
|
||||
# --------------------------------------------------------
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
edge_ratio, edge_lengths = compute_edge_ratio(corners)
|
||||
|
||||
sharpness = compute_sharpness(
|
||||
gray,
|
||||
corners
|
||||
)
|
||||
|
||||
contrast = compute_contrast(
|
||||
gray,
|
||||
corners
|
||||
)
|
||||
|
||||
geometry = compute_geometry_metrics(
|
||||
center,
|
||||
corners,
|
||||
width,
|
||||
height
|
||||
)
|
||||
|
||||
confidence = compute_confidence(
|
||||
area_px=area_px,
|
||||
sharpness=sharpness,
|
||||
edge_ratio=edge_ratio,
|
||||
dynamic_range=contrast['dynamic_range'],
|
||||
border_distance_px=geometry['distance_to_border_px']
|
||||
)
|
||||
|
||||
detection = {
|
||||
|
||||
'observation_id': str(uuid.uuid4()),
|
||||
|
||||
'type': 'aruco',
|
||||
|
||||
'marker_id': int(marker_id),
|
||||
|
||||
'marker_size_m': marker_size,
|
||||
|
||||
'image_points_px': corners.tolist(),
|
||||
|
||||
'center_px': center.tolist(),
|
||||
|
||||
'quality': {
|
||||
|
||||
'area_px': area_px,
|
||||
|
||||
'perimeter_px': perimeter_px,
|
||||
|
||||
'sharpness': {
|
||||
'laplacian_var': sharpness
|
||||
},
|
||||
|
||||
'contrast': contrast,
|
||||
|
||||
'geometry': geometry,
|
||||
|
||||
'edge_ratio': edge_ratio,
|
||||
|
||||
'edge_lengths_px': edge_lengths
|
||||
},
|
||||
|
||||
'confidence': confidence
|
||||
}
|
||||
|
||||
detections.append(detection)
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Rejected candidates
|
||||
# --------------------------------------------------------
|
||||
|
||||
rejected_candidates = []
|
||||
|
||||
if rejected is not None:
|
||||
|
||||
for candidate in rejected:
|
||||
|
||||
pts = candidate.reshape((-1, 2)).astype(np.float32)
|
||||
|
||||
center = pts.mean(axis=0)
|
||||
|
||||
area_px = float(
|
||||
cv2.contourArea(pts)
|
||||
)
|
||||
|
||||
rejected_candidates.append({
|
||||
|
||||
'image_points_px': pts.tolist(),
|
||||
|
||||
'center_px': center.tolist(),
|
||||
|
||||
'area_px': area_px
|
||||
})
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Final output
|
||||
# --------------------------------------------------------
|
||||
|
||||
output = {
|
||||
|
||||
'schema_version': '1.0',
|
||||
|
||||
'created_utc': time.strftime(
|
||||
'%Y-%m-%dT%H:%M:%SZ',
|
||||
time.gmtime()
|
||||
),
|
||||
|
||||
'vision_config': {
|
||||
'MarkerType': marker_type,
|
||||
'MarkerSize': marker_size
|
||||
},
|
||||
|
||||
'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(width),
|
||||
|
||||
'height_px': int(height)
|
||||
},
|
||||
|
||||
'aruco': {
|
||||
|
||||
'dictionary': marker_type,
|
||||
|
||||
'num_detected_markers': len(detections),
|
||||
|
||||
'num_rejected_candidates': len(rejected_candidates)
|
||||
},
|
||||
|
||||
'detections': detections,
|
||||
|
||||
'rejected_candidates': rejected_candidates
|
||||
}
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Output path
|
||||
# --------------------------------------------------------
|
||||
|
||||
input_filename = os.path.basename(args.image)
|
||||
|
||||
input_base = os.path.splitext(input_filename)[0]
|
||||
|
||||
out_json = os.path.join(
|
||||
args.outDir,
|
||||
f'{input_base}_aruco_detection.json'
|
||||
)
|
||||
|
||||
# --------------------------------------------------------
|
||||
# Save JSON
|
||||
# --------------------------------------------------------
|
||||
|
||||
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()
|
||||
383
programs/2_estimate_camera_pose_from_aruco_json.py
Normal file
383
programs/2_estimate_camera_pose_from_aruco_json.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
estimate_camera_pose_from_aruco_json.py
|
||||
|
||||
Berechnet die Kameraposition im Maschinen-/Board-Koordinatensystem
|
||||
aus:
|
||||
1. einer ArUco-Detections-JSON
|
||||
2. robots.json mit bekannten Marker-Positionen
|
||||
|
||||
Das Script verwendet ausschließlich bekannte Marker
|
||||
und bestimmt daraus die Kamera-Extrinsics mittels solvePnP.
|
||||
|
||||
Ergebnis:
|
||||
- Kamera-Position im Weltkoordinatensystem
|
||||
- Kamera-Orientierung (Roll/Pitch/Yaw)
|
||||
- optionale Reprojektion zur Qualitätskontrolle
|
||||
|
||||
Benötigt:
|
||||
pip install opencv-python numpy
|
||||
|
||||
Beispiel:
|
||||
python 2_estimate_camera_pose_from_aruco_json.py \
|
||||
--detections detection.json \
|
||||
--robots robots.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Hilfsfunktionen
|
||||
# ============================================================
|
||||
|
||||
def rvec_tvec_to_camera_pose(rvec, tvec):
|
||||
"""
|
||||
OpenCV liefert:
|
||||
X_cam = R * X_world + t
|
||||
|
||||
Gesucht:
|
||||
Kamera-Pose im Weltkoordinatensystem
|
||||
|
||||
=> R_wc = R^T
|
||||
=> C = -R^T * t
|
||||
"""
|
||||
|
||||
R_cw, _ = cv2.Rodrigues(rvec)
|
||||
|
||||
R_wc = R_cw.T
|
||||
cam_pos = -R_wc @ tvec
|
||||
|
||||
return R_wc, cam_pos.reshape(3)
|
||||
|
||||
|
||||
def rotation_matrix_to_euler_zyx(R):
|
||||
"""
|
||||
Euler ZYX:
|
||||
yaw(Z), pitch(Y), roll(X)
|
||||
"""
|
||||
|
||||
yaw = math.degrees(math.atan2(R[1, 0], R[0, 0]))
|
||||
|
||||
sp = math.sqrt(R[2, 1] ** 2 + R[2, 2] ** 2)
|
||||
|
||||
pitch = math.degrees(math.atan2(-R[2, 0], sp))
|
||||
|
||||
roll = math.degrees(math.atan2(R[2, 1], R[2, 2]))
|
||||
|
||||
return roll, pitch, yaw
|
||||
|
||||
|
||||
def build_marker_lookup(robot_data):
|
||||
"""
|
||||
Liest nur Marker mit ABSOLUTER Position.
|
||||
|
||||
Unterstützt:
|
||||
"position" -> absolute Weltposition [m]
|
||||
"relPos" -> wird aktuell ignoriert
|
||||
"""
|
||||
|
||||
marker_lookup = {}
|
||||
|
||||
for marker in robot_data.get("Marker", []):
|
||||
|
||||
marker_id = int(marker.get("id", -1))
|
||||
|
||||
# negative IDs ignorieren
|
||||
if marker_id < 0:
|
||||
continue
|
||||
|
||||
# Nur absolute Weltpositionen verwenden
|
||||
if "position" not in marker:
|
||||
continue
|
||||
|
||||
pos = marker["position"]
|
||||
|
||||
if pos is None:
|
||||
continue
|
||||
|
||||
if len(pos) != 3:
|
||||
continue
|
||||
|
||||
marker_lookup[marker_id] = np.array(
|
||||
pos,
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
return marker_lookup
|
||||
|
||||
def build_marker_object_points(marker_center_world, marker_size_m):
|
||||
"""
|
||||
Baut die 3D-Eckpunkte eines Markers auf.
|
||||
|
||||
Wichtig:
|
||||
Die Corner-Reihenfolge MUSS zur OpenCV-ArUco-Reihenfolge passen.
|
||||
|
||||
Reihenfolge:
|
||||
top-left
|
||||
top-right
|
||||
bottom-right
|
||||
bottom-left
|
||||
"""
|
||||
|
||||
half = marker_size_m / 2.0
|
||||
|
||||
local = np.array([
|
||||
[-half, half, 0.0],
|
||||
[ half, half, 0.0],
|
||||
[ half, -half, 0.0],
|
||||
[-half, -half, 0.0],
|
||||
], dtype=np.float32)
|
||||
|
||||
return local + marker_center_world.reshape(1, 3)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"--detections",
|
||||
required=True,
|
||||
help="ArUco detection JSON"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--robots",
|
||||
required=True,
|
||||
help="robots.json"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--min-confidence",
|
||||
type=float,
|
||||
default=0.5,
|
||||
help="Minimale Marker-Confidence"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--max-reprojection-error",
|
||||
type=float,
|
||||
default=3.0,
|
||||
help="Maximal erlaubter Reprojektionsfehler in Pixel"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# ============================================================
|
||||
# JSON laden
|
||||
# ============================================================
|
||||
|
||||
with open(args.detections, "r", encoding="utf-8") as f:
|
||||
detection_data = json.load(f)
|
||||
|
||||
with open(args.robots, "r", encoding="utf-8") as f:
|
||||
robot_data = json.load(f)
|
||||
|
||||
# ============================================================
|
||||
# Kamera-Parameter
|
||||
# ============================================================
|
||||
|
||||
K = np.array(
|
||||
detection_data["camera"]["camera_matrix"],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
D = np.array(
|
||||
detection_data["camera"]["distortion_coefficients"],
|
||||
dtype=np.float32
|
||||
).reshape(-1, 1)
|
||||
|
||||
# ============================================================
|
||||
# Bekannte Marker
|
||||
# ============================================================
|
||||
|
||||
known_markers = build_marker_lookup(robot_data)
|
||||
|
||||
# ============================================================
|
||||
# 2D/3D Punktlisten aufbauen
|
||||
# ============================================================
|
||||
|
||||
object_points = []
|
||||
image_points = []
|
||||
|
||||
used_markers = []
|
||||
|
||||
detections = detection_data["detections"]
|
||||
|
||||
for det in detections:
|
||||
|
||||
marker_id = int(det["marker_id"])
|
||||
|
||||
confidence = float(det.get("confidence", 1.0))
|
||||
|
||||
if confidence < args.min_confidence:
|
||||
continue
|
||||
|
||||
if marker_id not in known_markers:
|
||||
continue
|
||||
|
||||
marker_center_world = known_markers[marker_id]
|
||||
|
||||
marker_size = float(det["marker_size_m"])
|
||||
|
||||
obj_pts = build_marker_object_points(
|
||||
marker_center_world,
|
||||
marker_size
|
||||
)
|
||||
|
||||
img_pts = np.array(
|
||||
det["image_points_px"],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
object_points.append(obj_pts)
|
||||
image_points.append(img_pts)
|
||||
|
||||
used_markers.append(marker_id)
|
||||
|
||||
if len(object_points) == 0:
|
||||
raise RuntimeError(
|
||||
"Keine bekannten Marker gefunden."
|
||||
)
|
||||
|
||||
object_points = np.concatenate(object_points, axis=0)
|
||||
image_points = np.concatenate(image_points, axis=0)
|
||||
|
||||
print()
|
||||
print("==================================================")
|
||||
print("Bekannte Marker verwendet:")
|
||||
print(sorted(set(used_markers)))
|
||||
print("==================================================")
|
||||
print()
|
||||
|
||||
# ============================================================
|
||||
# solvePnP
|
||||
# ============================================================
|
||||
|
||||
success, rvec, tvec = cv2.solvePnP(
|
||||
object_points,
|
||||
image_points,
|
||||
K,
|
||||
D,
|
||||
flags=cv2.SOLVEPNP_ITERATIVE
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise RuntimeError("solvePnP fehlgeschlagen")
|
||||
|
||||
# ============================================================
|
||||
# Kamera-Pose berechnen
|
||||
# ============================================================
|
||||
|
||||
R_wc, cam_pos = rvec_tvec_to_camera_pose(
|
||||
rvec,
|
||||
tvec
|
||||
)
|
||||
|
||||
roll, pitch, yaw = rotation_matrix_to_euler_zyx(R_wc)
|
||||
|
||||
# ============================================================
|
||||
# Reprojektionsfehler
|
||||
# ============================================================
|
||||
|
||||
projected, _ = cv2.projectPoints(
|
||||
object_points,
|
||||
rvec,
|
||||
tvec,
|
||||
K,
|
||||
D
|
||||
)
|
||||
|
||||
projected = projected.reshape(-1, 2)
|
||||
|
||||
reproj_error = np.linalg.norm(
|
||||
projected - image_points,
|
||||
axis=1
|
||||
)
|
||||
|
||||
rms = np.sqrt(np.mean(reproj_error ** 2))
|
||||
max_err = np.max(reproj_error)
|
||||
|
||||
# ============================================================
|
||||
# Ausgabe
|
||||
# ============================================================
|
||||
|
||||
print("==================================================")
|
||||
print("KAMERA-POSE")
|
||||
print("==================================================")
|
||||
print()
|
||||
|
||||
print("Position [m]")
|
||||
print(f" X = {cam_pos[0]: .6f}")
|
||||
print(f" Y = {cam_pos[1]: .6f}")
|
||||
print(f" Z = {cam_pos[2]: .6f}")
|
||||
|
||||
print()
|
||||
|
||||
print("Orientation [deg]")
|
||||
print(f" Roll = {roll: .3f}")
|
||||
print(f" Pitch = {pitch: .3f}")
|
||||
print(f" Yaw = {yaw: .3f}")
|
||||
|
||||
print()
|
||||
|
||||
print("Reprojection Error")
|
||||
print(f" RMS = {rms:.3f} px")
|
||||
print(f" MAX = {max_err:.3f} px")
|
||||
|
||||
print()
|
||||
|
||||
if max_err > args.max_reprojection_error:
|
||||
print("[WARNUNG] Reprojektionsfehler relativ hoch")
|
||||
else:
|
||||
print("[OK] Pose stabil")
|
||||
|
||||
print()
|
||||
|
||||
# ============================================================
|
||||
# JSON speichern
|
||||
# ============================================================
|
||||
|
||||
out = {
|
||||
"camera_pose": {
|
||||
"position_m": {
|
||||
"x": float(cam_pos[0]),
|
||||
"y": float(cam_pos[1]),
|
||||
"z": float(cam_pos[2]),
|
||||
},
|
||||
"orientation_deg": {
|
||||
"roll": float(roll),
|
||||
"pitch": float(pitch),
|
||||
"yaw": float(yaw),
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"reprojection_rms_px": float(rms),
|
||||
"reprojection_max_px": float(max_err),
|
||||
"num_markers_used": len(set(used_markers)),
|
||||
"markers_used": sorted(set(used_markers))
|
||||
}
|
||||
}
|
||||
|
||||
out_file = Path(args.detections).with_suffix(".camera_pose.json")
|
||||
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
|
||||
print(f"Pose gespeichert in:")
|
||||
print(out_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
programs/__pycache__/1_detect_aruco_observations.cpython-311.pyc
Normal file
BIN
programs/__pycache__/1_detect_aruco_observations.cpython-311.pyc
Normal file
Binary file not shown.
@@ -1,45 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name":"Test 0",
|
||||
"robot":"test/data/robot/robot.json",
|
||||
"image":[
|
||||
{
|
||||
"timestamp":11778819665744,
|
||||
"file":"test/data/screenShots/snapshot_video0_11778819665744.jpg",
|
||||
"camera":"c310",
|
||||
"callibration":"data/settings/callibration_cam0.npz"
|
||||
},
|
||||
{
|
||||
"timestamp":11778819665744,
|
||||
"file":"test/data/screenShots/snapshot_video1_11778819665744.jpg",
|
||||
"camera":"c310",
|
||||
"callibration":"data/settings/callibration_cam0.npz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"Test 1: 45° unterarm",
|
||||
"robot":"test/data/robot/robot.json",
|
||||
"timestamp":1779690911822,
|
||||
"image":[
|
||||
{
|
||||
"timestamp":1779690911822,
|
||||
"file":"test/data/screenShots/snapshot_video0_1779690911822.jpg",
|
||||
"camera":"c310",
|
||||
"callibration":"data/settings/callibration_cam0.npz"
|
||||
},
|
||||
{
|
||||
"timestamp":1779690911822,
|
||||
"file":"test/data/screenShots/snapshot_video0_1779690911822.jpg",
|
||||
"camera":"c310",
|
||||
"callibration":"data/settings/callibration_cam0.npz"
|
||||
},
|
||||
{
|
||||
"timestamp":1779690911822,
|
||||
"file":"test/data/screenShots/1779690911822_DSCF1382.JPG",
|
||||
"camera":"XT1",
|
||||
"callibration":"data/callibration/XT1-16mm28-1mFokus_f8/callibration_XT1-16mm_1m_f8.npz"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,64 +0,0 @@
|
||||
// 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 sourceScreenshotsDir = path.join(__dirname, 'data', 'screenShots');
|
||||
const screenshotFiles = [
|
||||
'snapshot_video0_1778819665744.jpg',
|
||||
'snapshot_video1_1778819665744.jpg',
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
screenshotFiles.forEach((file) => {
|
||||
const src = path.join(sourceScreenshotsDir, file);
|
||||
if (!fs.existsSync(src)) {
|
||||
throw new Error(`Missing test fixture screenshot: ${src}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Keep generated detection JSON files in the screenshot directory.
|
||||
});
|
||||
|
||||
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(sourceScreenshotsDir)).toBe(true);
|
||||
});
|
||||
|
||||
test('should process screenshots successfully', () => {
|
||||
const screenshots = fs.readdirSync(sourceScreenshotsDir)
|
||||
.filter(file => file.endsWith('.jpg') || file.endsWith('.png'))
|
||||
.filter(file => screenshotFiles.includes(file));
|
||||
|
||||
expect(screenshots.length).toBeGreaterThan(0);
|
||||
|
||||
screenshots.forEach(screenshot => {
|
||||
const screenshotPath = path.join(sourceScreenshotsDir, screenshot);
|
||||
const jsonPath = screenshotPath.replace(/\.(jpg|png)$/i, '_aruco_detection.json');
|
||||
const cmd = `python ${scriptPath} -i "${screenshotPath}" -npz "${calibrationFile}" -cameraId 0`;
|
||||
|
||||
try {
|
||||
execSync(cmd, { stdio: 'inherit' });
|
||||
expect(fs.existsSync(jsonPath)).toBe(true);
|
||||
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
// test/02_build_scene_json.test.js
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('Build Scene JSON Script', () => {
|
||||
const scriptPath = 'programs/02_build_scene_json.py';
|
||||
const timestamp = 1778819665744;
|
||||
const screenshotDir = path.join(__dirname, 'data', 'screenShots');
|
||||
const detectionFiles = [
|
||||
'snapshot_video0_1778819665744_aruco_detection.json',
|
||||
'snapshot_video1_1778819665744_aruco_detection.json',
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
detectionFiles.forEach((file) => {
|
||||
const src = path.join(screenshotDir, file);
|
||||
if (!fs.existsSync(src)) {
|
||||
throw new Error(`Missing test fixture detection file: ${src}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Keep the generated scene JSON in the screenshot directory for real-world behavior.
|
||||
});
|
||||
|
||||
test('should exist and be executable', () => {
|
||||
expect(fs.existsSync(scriptPath)).toBe(true);
|
||||
});
|
||||
|
||||
test('should build scene JSON with timestamp parameter', () => {
|
||||
const cmd = `python ${scriptPath} -timestamp ${timestamp} -dir "${screenshotDir}"`;
|
||||
|
||||
try {
|
||||
execSync(cmd, { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to build scene JSON: ${error.message}`);
|
||||
}
|
||||
|
||||
const expectedJsonFile = path.join(screenshotDir, `scene_${timestamp}.json`);
|
||||
expect(fs.existsSync(expectedJsonFile)).toBe(true);
|
||||
|
||||
const jsonData = JSON.parse(fs.readFileSync(expectedJsonFile, 'utf8'));
|
||||
expect(jsonData).toBeDefined();
|
||||
expect(typeof jsonData).toBe('object');
|
||||
expect(jsonData.cameras).toBeDefined();
|
||||
expect(Object.keys(jsonData.cameras).sort()).toEqual(['0', '1']);
|
||||
});
|
||||
|
||||
test('should handle timestamp parameter correctly', () => {
|
||||
// Überprüfe, ob der Timestamp korrekt verarbeitet wird
|
||||
expect(timestamp).toBe(1778819665744);
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
// test/03a_cameraPose.test.js
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('Camera Pose Script', () => {
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
const scriptPath = path.resolve(projectRoot, 'programs/03a_cameraPose.py');
|
||||
const timestamp = 1778819665744;
|
||||
const screenshotDir = path.join(__dirname, 'data', 'screenShots');
|
||||
const detectionFiles = [
|
||||
'snapshot_video0_1778819665744_aruco_detection.json',
|
||||
'snapshot_video1_1778819665744_aruco_detection.json',
|
||||
];
|
||||
const robotFile = path.resolve(projectRoot, 'test/data/robot/robot.json');
|
||||
const sceneFile = path.join(screenshotDir, `scene_${timestamp}.json`);
|
||||
const outputFile = path.join(screenshotDir, `scene_${timestamp}_cameras.json`);
|
||||
|
||||
beforeEach(() => {
|
||||
detectionFiles.forEach((file) => {
|
||||
const src = path.join(screenshotDir, file);
|
||||
if (!fs.existsSync(src)) {
|
||||
throw new Error(`Missing test fixture detection file: ${src}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (!fs.existsSync(sceneFile)) {
|
||||
throw new Error(`Missing scene file generated by step 2: ${sceneFile}`);
|
||||
}
|
||||
|
||||
if (fs.existsSync(outputFile)) {
|
||||
fs.unlinkSync(outputFile);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Keep the generated scene and camera JSON in the screenshot directory.
|
||||
});
|
||||
|
||||
test('should exist and be executable', () => {
|
||||
expect(fs.existsSync(scriptPath)).toBe(true);
|
||||
});
|
||||
|
||||
test('should have scene file', () => {
|
||||
expect(fs.existsSync(sceneFile)).toBe(true);
|
||||
});
|
||||
|
||||
test('should have robot file', () => {
|
||||
expect(fs.existsSync(robotFile)).toBe(true);
|
||||
});
|
||||
|
||||
test('should compute camera poses with timestamp parameter', () => {
|
||||
// Führe das Python-Skript mit den korrekten Parametern aus
|
||||
const cmd = `python "${scriptPath}" -scene "${sceneFile}" -robot "${robotFile}" -out "${outputFile}"`;
|
||||
|
||||
try {
|
||||
execSync(cmd, { stdio: 'inherit', cwd: projectRoot });
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to compute camera poses: ${error.message}`);
|
||||
}
|
||||
// Überprüfe, ob die erwartete Ausgabedatei erstellt wurde
|
||||
expect(fs.existsSync(outputFile)).toBe(true);
|
||||
|
||||
// Prüfe den Inhalt der JSON-Datei
|
||||
const jsonData = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
|
||||
expect(jsonData).toBeDefined();
|
||||
expect(typeof jsonData).toBe('object');
|
||||
expect(jsonData).toHaveProperty('camera_poses');
|
||||
});
|
||||
|
||||
test('should handle timestamp parameter correctly', () => {
|
||||
expect(timestamp).toBe(1778819665744);
|
||||
});
|
||||
});
|
||||
99
test/0_path_OK.test.js
Normal file
99
test/0_path_OK.test.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
||||
const PROJECT_PATH = process.cwd()
|
||||
const TEST_PATH = path.join(__dirname, '.');
|
||||
const SCRIPT_FILE = path.join(PROJECT_PATH, 'programs', '1_detect_aruco_observations.py');
|
||||
const SOURCE_DIR = path.join(__dirname,'data', 'screenShots');
|
||||
const TARGET_DIR = path.join(__dirname,'data', 'screenshots', '1778819665744_detection_testResults');
|
||||
const PYTHON_CMD = process.platform === 'win32' ? 'python' : 'python3';
|
||||
const TEST_SETUP_FILE = path.join(TEST_PATH, 'data', '0_testSetup.json');
|
||||
const ROBOT_PATH = path.join(__dirname, 'data', 'robot', 'robot.json');
|
||||
const SCRIPT_FILE_2 = path.join(PROJECT_PATH, 'programs', '2_estimate_camera_pose_from_aruco_json.py');
|
||||
|
||||
|
||||
const cam = {
|
||||
id : 'cam1',
|
||||
image: 'snapshot_video1_1779690911822.jpg',
|
||||
intrinsics: path.join(PROJECT_PATH, 'data', 'settings','callibration_cam0.npz')
|
||||
};
|
||||
|
||||
|
||||
// Passe diese Werte an deine echten Daten an
|
||||
const pathsToCheck = {
|
||||
SOURCE_DIR,
|
||||
SCRIPT_FILE,
|
||||
ROBOT_PATH,
|
||||
SCRIPT_FILE_2,
|
||||
NPZ_PATH: cam.intrinsics,
|
||||
IMAGE_PATH: path.join(SOURCE_DIR, cam.image),
|
||||
PYTHON_SCRIPT: path.join(PROJECT_PATH, 'programs', '1_detect_aruco_observations.py'),
|
||||
TEST_SETUP_FILE,
|
||||
};
|
||||
|
||||
|
||||
|
||||
describe('Check if Paths exist', () => {
|
||||
|
||||
test('Each relevant Python File etc.', () => {
|
||||
const missing = [];
|
||||
|
||||
for (const [name, filePath] of Object.entries(pathsToCheck)) {
|
||||
if (!filePath || !fs.existsSync(filePath)) {
|
||||
missing.push(`${name} -> ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
'Folgende Pfade fehlen oder sind ungültig:\n' +
|
||||
missing.join('\n')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Test if files in Setup exist', () => {
|
||||
|
||||
|
||||
console.log('TEST_PATH:', TEST_PATH);
|
||||
console.log('PROJECT_PATH:', PROJECT_PATH);
|
||||
console.log('SOURCE_DIR:', SOURCE_DIR);
|
||||
|
||||
if (!fs.existsSync(TEST_SETUP_FILE)) {
|
||||
throw new Error(`Test-Setup Datei fehlt: ${TEST_SETUP_FILE}`);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(TEST_SETUP_FILE, 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
|
||||
const missing = [];
|
||||
|
||||
data.forEach((testCase, i) => {
|
||||
|
||||
const r = testCase.robot.map(i => i.replace("TEST_PATH", TEST_PATH).replace("PROJECT_PATH", PROJECT_PATH));
|
||||
const pathR = path.join(r.map(p => p.toString()).join(path.sep));
|
||||
if (!pathR || !fs.existsSync(pathR)) {
|
||||
missing.push(`${name} -> ${filePath}`);
|
||||
}
|
||||
|
||||
testCase.image.forEach((img, j) => {
|
||||
const imgPath = path.join(img.file.map(i => i.replace("TEST_PATH", TEST_PATH).replace("PROJECT_PATH", PROJECT_PATH)).join(path.sep));
|
||||
if (!imgPath || !fs.existsSync(imgPath)) {
|
||||
missing.push(`TestCase ${i} Image ${j} -> ${imgPath}`);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
console.log('Missing files in test setup:', missing);
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
'Folgende Dateien aus dem Test-Setup fehlen oder sind ungültig:\n' +
|
||||
missing.join('\n')
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const PROJECT_PATH = process.cwd()
|
||||
const TEST_PATH = path.join(__dirname, '.');
|
||||
const SOURCE_DIR = path.join(__dirname,'data', 'screenShots');
|
||||
const TARGET_DIR = path.join(__dirname,'data', 'screenshots', '1778819665744_detection_singleTest_testResults');
|
||||
const PYTHON_CMD = process.platform === 'win32' ? 'python' : 'python3';
|
||||
const TEST_SETUP_FILE = path.join(TEST_PATH, 'data', '0_testSetup.json');
|
||||
const SCRIPT_FILE = path.join(PROJECT_PATH, 'programs', '1_detect_aruco_observations.py');
|
||||
const ROBOT_PATH = path.join(__dirname, 'data', 'robot', 'robot.json');
|
||||
|
||||
|
||||
const cam = {
|
||||
id : 'cam1',
|
||||
image: 'snapshot_video1_1779690911822.jpg',
|
||||
intrinsics: path.join(PROJECT_PATH, 'data', 'settings','callibration_cam0.npz')
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
describe('Check if Python runs', () => {
|
||||
|
||||
test('First Run of Python', () => {
|
||||
|
||||
|
||||
console.log('Intrinsics : ', cam.intrinsics);
|
||||
execFileSync(PYTHON_CMD, [
|
||||
SCRIPT_FILE,
|
||||
'-i', path.join(SOURCE_DIR, cam.image),
|
||||
'-npz', cam.intrinsics,
|
||||
'-robot', ROBOT_PATH,
|
||||
'-cameraId', cam.id
|
||||
|
||||
,
|
||||
'-outDir', TARGET_DIR
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
cwd: SOURCE_DIR // <- wichtig
|
||||
});
|
||||
|
||||
|
||||
const resultFile = path.join(TARGET_DIR, 'snapshot_video1_1779690911822_aruco_detection.json');
|
||||
|
||||
if (!fs.existsSync(resultFile)) {
|
||||
throw new Error(`Erwartete Datei fehlt: ${resultFile}`);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
// ✅ Cleanup läuft IMMER nach jedem Test
|
||||
afterEach(() => {
|
||||
if (!fs.existsSync(TARGET_DIR)) return;
|
||||
|
||||
fs.readdirSync(TARGET_DIR).forEach(file => {
|
||||
fs.rmSync(path.join(TARGET_DIR, file), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
78
test/2_estimate_camera_pose_from_aruco_json.test.js
Normal file
78
test/2_estimate_camera_pose_from_aruco_json.test.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const PROJECT_PATH = process.cwd()
|
||||
const TEST_PATH = path.join(__dirname, '.');
|
||||
const SOURCE_DIR = path.join(__dirname,'data', 'screenShots');
|
||||
const TARGET_DIR = path.join(__dirname,'data', 'screenshots', '1778819665744_detection_singleTest_testResults');
|
||||
const PYTHON_CMD = process.platform === 'win32' ? 'python' : 'python3';
|
||||
const TEST_SETUP_FILE = path.join(TEST_PATH, 'data', '0_testSetup.json');
|
||||
const SCRIPT_FILE = path.join(PROJECT_PATH, 'programs', '1_detect_aruco_observations.py');
|
||||
const ROBOT_PATH = path.join(__dirname, 'data', 'robot', 'robot.json');
|
||||
const SCRIPT_FILE_2 = path.join(PROJECT_PATH, 'programs', '2_estimate_camera_pose_from_aruco_json.py');
|
||||
|
||||
|
||||
const cam = {
|
||||
id : 'cam1',
|
||||
image: 'snapshot_video1_1779690911822.jpg',
|
||||
intrinsics: path.join(PROJECT_PATH, 'data', 'settings','callibration_cam0.npz')
|
||||
};
|
||||
|
||||
|
||||
|
||||
describe('Check if Python 2 runs', () => {
|
||||
|
||||
test('First Run of Python second script', () => {
|
||||
|
||||
|
||||
console.log('Intrinsics : ', cam.intrinsics);
|
||||
execFileSync(PYTHON_CMD, [
|
||||
SCRIPT_FILE,
|
||||
'-i', path.join(SOURCE_DIR, cam.image),
|
||||
'-npz', cam.intrinsics,
|
||||
'-robot', ROBOT_PATH,
|
||||
'-cameraId', cam.id
|
||||
|
||||
,
|
||||
'-outDir', TARGET_DIR
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
cwd: SOURCE_DIR // <- wichtig
|
||||
});
|
||||
|
||||
|
||||
const resultFile = path.join(TARGET_DIR, 'snapshot_video1_1779690911822_aruco_detection.json');
|
||||
|
||||
if (!fs.existsSync(resultFile)) {
|
||||
throw new Error(`Erwartete Datei fehlt: ${resultFile}`);
|
||||
}
|
||||
|
||||
|
||||
console.log('Intrinsics : ', cam.intrinsics);
|
||||
execFileSync(PYTHON_CMD, [
|
||||
SCRIPT_FILE_2,
|
||||
'--detections' , resultFile,
|
||||
'--robots', ROBOT_PATH
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
cwd: SOURCE_DIR // <- wichtig
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
// ✅ Cleanup läuft IMMER nach jedem Test
|
||||
afterEach(() => {
|
||||
if (!fs.existsSync(TARGET_DIR)) return;
|
||||
|
||||
fs.readdirSync(TARGET_DIR).forEach(file => {
|
||||
fs.rmSync(path.join(TARGET_DIR, file), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
45
test/data/0_testSetup.json
Normal file
45
test/data/0_testSetup.json
Normal file
@@ -0,0 +1,45 @@
|
||||
[
|
||||
{
|
||||
"name":"Test 0",
|
||||
"robot":["TEST_PATH","data","robot","robot.json"],
|
||||
"image":[
|
||||
{
|
||||
"timestamp":11778819665744,
|
||||
"file":["TEST_PATH","data","screenShots","snapshot_video0_1778819665744.jpg"],
|
||||
"camera":"c310",
|
||||
"callibration":["PROJECT_PATH", "data", "settings","callibration_cam0.npz"]
|
||||
},
|
||||
{
|
||||
"timestamp":11778819665744,
|
||||
"file":["TEST_PATH","data","screenShots","snapshot_video1_1778819665744.jpg"],
|
||||
"camera":"c310",
|
||||
"callibration":["PROJECT_PATH", "data", "settings","callibration_cam0.npz"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"Test 1: 45° unterarm",
|
||||
"robot":["TEST_PATH","data","robot","robot.json"],
|
||||
"timestamp":1779690911822,
|
||||
"image":[
|
||||
{
|
||||
"timestamp":1779690911822,
|
||||
"file":["TEST_PATH","data","screenShots","snapshot_video0_1779690911822.jpg"],
|
||||
"camera":"c310",
|
||||
|
||||
"callibration":["PROJECT_PATH", "data", "settings","callibration_cam0.npz"]
|
||||
} ,
|
||||
{
|
||||
"timestamp":1779690911822,
|
||||
"file":["TEST_PATH","data","screenShots","snapshot_video1_1779690911822.jpg"],
|
||||
"camera":"c310",
|
||||
"callibration":["PROJECT_PATH", "data", "settings","callibration_cam0.npz"] },
|
||||
{
|
||||
"timestamp":1779690911822,
|
||||
"file":["TEST_PATH","data","screenShots","1779690911822_DSCF1382.JPG"],
|
||||
"camera":"XT1",
|
||||
"callibration":["PROJECT_PATH", "data", "XT1-16mm28-1mFokus_f8","callibration_XT1-16mm_1m_f8.npz"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"vision_config":{
|
||||
"MarkerType":"DICT_4X4_250",
|
||||
"MarkerSize":0.025
|
||||
},
|
||||
"recognized":{"x":null, "y":null, "z": null, "a":null, "b":null, "c":null, "e": null},
|
||||
"Elements":["Board","Base","Arm1","Joint1","Arm2","Finger1","Finger2"],
|
||||
"ElementLength":{"Arm1":250, "Arm2":250, "Finger1":100, "Finger2":100},
|
||||
@@ -8,7 +12,6 @@
|
||||
"jointC":{"name":"EllbowLift","type":"revolute","axis":[1,0,0],"parent":"Arm1","child":"Joint1", "origin":[null, null, null]},
|
||||
"jointD":{"name":"EllbowTwist","type":"revolute","axis":[0,1,0],"parent":"Joint1","child":"Arm2", "origin":[null, null, null]}
|
||||
},
|
||||
"MarkerType":"DICT_4X4_250",
|
||||
"Marker":[
|
||||
{"id":205,"on":"Board","position":[0.80, -0.090, 0.0]},
|
||||
{"id":207,"on":"Board","position":[0.80, 0.0, 0.0]},
|
||||
|
||||
Reference in New Issue
Block a user