Files
appRobotRender/pipeline/3_readTwoImages.py
2026-05-30 11:30:59 +02:00

921 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
readCamPositionTwo.py
Two-camera ArUco detection with joint optimization of both camera extrinsics
against known machine-frame reference markers, plus triangulation of unknown
marker positions. Outputs camera pose and marker poses in machine coordinates,
with CSV and JSON similar to the single-camera script.
Dependencies: numpy, opencv-python (cv2)
Optional but NOT required: SciPy (we implement a simple LevenbergMarquardt).
Usage example:
python3 readTwoImages.py -i snapshot_video0_1764531874081.jpg -i snapshot_video1_1764531874081.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json
python3 readTwoImages.py -i snapshot_video0_1764524369655.jpg -i snapshot_video1_1764524369655.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json
python3 readTwoImages.py -i snapshot_video0_1765009029764.jpg -i snapshot_video1_1765009029764.jpg -npz callibration_cam0.npz -npz callibration_cam1.npz -settings settings.json
NEW:
python3 readTwoImages.py -i img0.jpg -i img1.jpg -npz cam0.npz -npz cam1.npz -settings settings.json -outDir results/
Settings JSON is expected to contain:
{
"coordinateSystem": {
"KnownMarkers": {
"50": [0.0, 0.0, 0.0],
"71": [0.140, 0.0, 0.0],
"101": [0.0, -0.080, 0.0]
}
}
}
"""
import argparse
import os
import sys
import json
import time
from typing import Dict, Tuple, List
import numpy as np
import cv2
# ---------------- Configuration defaults ----------------
AXIS_LENGTH_M = 0.05
# ---------------- Utilities ----------------
def load_intrinsics_npz(npz_path: str) -> Tuple[np.ndarray, np.ndarray]:
print("NPZ reading of file:", npz_path)
if os.path.exists(npz_path):
data = np.load(npz_path)
for k in ('camera_matrix', 'mtx', 'K'):
if k in data:
camera_matrix = 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:
dist = data[k].astype(np.float32).reshape(-1,1)
break
else:
dist = np.zeros((5,1), dtype=np.float32)
print("NPZ loaded:", npz_path)
return camera_matrix, dist
# Fallback default intrinsics
camera_matrix = np.array([[1400, 0, 640],
[0, 1400, 360],
[0, 0, 1]], dtype=np.float32)
dist_coeffs = np.zeros((5,1), dtype=np.float32)
print("[WARN] Using default approximate intrinsics.")
return camera_matrix, dist_coeffs
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,
}
if dict_name not in mapping:
dict_id = cv2.aruco.DICT_4X4_250
else:
dict_id = mapping[dict_name]
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: np.ndarray, 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 corners_to_image_points(corners: np.ndarray) -> np.ndarray:
return corners.reshape(4,2).astype(np.float32)
def marker_center_from_corners(corners: np.ndarray) -> np.ndarray:
pts = corners.reshape(4,2)
return pts.mean(axis=0).astype(np.float32)
def rvec_to_R(rvec: np.ndarray) -> np.ndarray:
R, _ = cv2.Rodrigues(rvec)
return R
def rigid_transform_no_scale(A: np.ndarray, B: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""Find R,t s.t. B ≈ R A + t. A,B: Nx3."""
assert A.shape == B.shape and A.shape[1] == 3, "A and B must be Nx3"
N = A.shape[0]
if N < 2:
raise ValueError("Need at least 2 points; 3+ recommended.")
centroid_A = A.mean(axis=0)
centroid_B = B.mean(axis=0)
AA = A - centroid_A
BB = B - centroid_B
H = AA.T @ BB
U, S, Vt = np.linalg.svd(H)
R = Vt.T @ U.T
if np.linalg.det(R) < 0:
Vt[-1, :] *= -1
R = Vt.T @ U.T
t = centroid_B - R @ centroid_A
return R.astype(np.float32), t.astype(np.float32)
def undistort_to_normalized(points_px: np.ndarray, K: np.ndarray, D: np.ndarray) -> np.ndarray:
"""points_px: Nx2 pixel. Returns Nx2 normalized coords (x,y) where projection is x=Xp/Z, y=Yp/Z.
cv2.undistortPoints with P=None yields normalized coordinates.
"""
pts = points_px.reshape(-1,1,2).astype(np.float32)
und = cv2.undistortPoints(pts, K, D, P=None) # returns Nx1x2
return und.reshape(-1,2)
# ---------------- Joint optimization (LM, numerical Jacobian) ----------------
def pack_params(omega1, t1, omega2, t2) -> np.ndarray:
return np.hstack([omega1, t1, omega2, t2]).astype(np.float64)
def unpack_params(theta: np.ndarray):
omega1 = theta[0:3]
t1 = theta[3:6]
omega2 = theta[6:9]
t2 = theta[9:12]
return omega1, t1, omega2, t2
def residuals_centers_normalized(theta: np.ndarray,
X_mach: np.ndarray,
obs1_norm: np.ndarray,
obs2_norm: np.ndarray) -> np.ndarray:
"""
Compute residuals in normalized coordinates (no intrinsics, no distortion).
For camera j: X_cam = R_j X_mach + t_j; proj: (x/z, y/z).
Returns stacked residuals [r1; r2] shape (4N,), where N = number of references.
"""
omega1, t1, omega2, t2 = unpack_params(theta)
R1 = cv2.Rodrigues(omega1)[0]
R2 = cv2.Rodrigues(omega2)[0]
# Camera 1 projections
X_cam1 = (R1 @ X_mach.T + t1.reshape(3,1)).T # Nx3
uv1 = X_cam1[:, :2] / X_cam1[:, 2:3]
r1 = (obs1_norm - uv1).reshape(-1)
# Camera 2 projections
X_cam2 = (R2 @ X_mach.T + t2.reshape(3,1)).T
uv2 = X_cam2[:, :2] / X_cam2[:, 2:3]
r2 = (obs2_norm - uv2).reshape(-1)
return np.hstack([r1, r2])
def numerical_jacobian(f, theta: np.ndarray, eps: float, *args) -> np.ndarray:
"""Finite-difference Jacobian: forward differences."""
r0 = f(theta, *args)
m = r0.size
n = theta.size
J = np.zeros((m, n), dtype=np.float64)
for k in range(n):
th = theta.copy()
th[k] += eps
rk = f(th, *args)
J[:, k] = (rk - r0) / eps
return J, r0
def lm_solve(theta0: np.ndarray,
X_mach: np.ndarray,
obs1_norm: np.ndarray,
obs2_norm: np.ndarray,
max_iter: int = 50,
eps_jac: float = 1e-6,
lambda_init: float = 1e-3) -> Tuple[np.ndarray, Dict]:
"""Simple LevenbergMarquardt on center normalized residuals."""
lam = lambda_init
theta = theta0.copy()
history = {"iters": [], "rms": []}
for it in range(max_iter):
J, r = numerical_jacobian(residuals_centers_normalized, theta, eps_jac,
X_mach, obs1_norm, obs2_norm)
rms = float(np.sqrt(np.mean(r*r))) if r.size else 0.0
history["iters"].append(it)
history["rms"].append(rms)
# Normal equations with damping
JTJ = J.T @ J
g = J.T @ r
H = JTJ + lam * np.eye(JTJ.shape[0])
try:
delta = -np.linalg.solve(H, g)
except np.linalg.LinAlgError:
# Fallback to least squares
delta, *_ = np.linalg.lstsq(H, -g, rcond=None)
theta_trial = theta + delta
r_trial = residuals_centers_normalized(theta_trial, X_mach, obs1_norm, obs2_norm)
rms_trial = float(np.sqrt(np.mean(r_trial*r_trial)))
if rms_trial < rms: # improve
theta = theta_trial
lam *= 0.5
else:
lam *= 2.0
# Convergence criteria
if np.linalg.norm(delta) < 1e-9 or abs(rms - rms_trial) < 1e-9:
break
return theta, history
# ---------------- Triangulation ----------------
def build_projection_matrix(K: np.ndarray, R: np.ndarray, t: np.ndarray) -> np.ndarray:
return K @ np.hstack([R, t.reshape(3,1)])
def triangulate_center(P1: np.ndarray, P2: np.ndarray, u1: np.ndarray, u2: np.ndarray) -> np.ndarray:
# u1,u2: (2,) pixel coordinates
x1 = u1.reshape(2,1)
x2 = u2.reshape(2,1)
X_h = cv2.triangulatePoints(P1, P2, x1, x2)
X = (X_h[:3] / X_h[3]).reshape(3)
return X.astype(np.float32)
# ---------------- Main ----------------
def main():
print("Started")
parser = argparse.ArgumentParser(description="Two-camera ArUco joint optimization and triangulation")
parser.add_argument('-i', '--images', action='append', required=True,
help="Two image paths: first camera then second camera")
parser.add_argument('-npz', '--npz', action='append', required=True,
help="Two NPZ intrinsics paths: cam1 then cam2")
parser.add_argument('-markerSizeMM', '--markerSizeMM', type=float, default=25.0,
help="Marker side length in millimeters")
parser.add_argument('--dict', default='DICT_4X4_250',
help="ArUco dictionary name")
parser.add_argument('-settings', type=str, default=None,
help="Json settings file containing machine KnownMarkers")
# ============================================================
# NEW OPTIONAL PARAMETER (100% backward compatible)
# ============================================================
parser.add_argument('-outDir', '--outDir',
type=str,
default=None,
help="Optional output directory")
args = parser.parse_args()
if len(args.images) != 2 or len(args.npz) != 2:
print("[ERROR] Provide exactly two images and two intrinsics NPZ files.")
sys.exit(1)
img1 = cv2.imread(args.images[0])
img2 = cv2.imread(args.images[1])
draw1 = img1.copy()
draw2 = img2.copy()
h, w = draw1.shape[:2]
drawPNG1 = np.zeros((h, w, 3), dtype=np.uint8)
drawPNG2 = np.zeros((h, w, 3), dtype=np.uint8)
if img1 is None or img2 is None:
print("[ERROR] Cannot read one of the images.")
sys.exit(1)
K1, D1 = load_intrinsics_npz(args.npz[0])
K2, D2 = load_intrinsics_npz(args.npz[1])
# Marker 3D local geometry (square corners)
half = (args.markerSizeMM / 1000.0) / 2.0
obj_points = np.array([
[-half, half, 0.0],
[ half, half, 0.0],
[ half, -half, 0.0],
[-half, -half, 0.0],
], dtype=np.float32)
# Read settings for machine known markers
known_markers: Dict[int, np.ndarray] = {}
if args.settings is not None and os.path.exists(args.settings):
with open(args.settings, 'r') as f:
settings = json.load(f)
for marker_id, coords in settings['coordinateSystem']['KnownMarkers'].items():
known_markers[int(marker_id)] = np.array(coords, dtype=np.float32)
print("[INFO] Loaded KnownMarkers from settings.")
else:
known_markers = {
50: np.array([0.0, 0.0, 0.0], dtype=np.float32),
71: np.array([0.140, 0.0, 0.0], dtype=np.float32),
101: np.array([0.0, -0.080, 0.0], dtype=np.float32),
}
print("[WARN] Using default KnownMarkers; provide -settings for your machine.")
# Detect markers in both images
detector_tuple = get_aruco_detector(args.dict)
corners1, ids1, _ = detect_markers(img1, detector_tuple)
corners2, ids2, _ = detect_markers(img2, detector_tuple)
if ids1 is None or ids2 is None:
print("[ERROR] No markers detected in one or both images.")
sys.exit(1)
ids1 = ids1.flatten().tolist()
ids2 = ids2.flatten().tolist()
# Neu: merken, welche Kamera welchen Marker gesehen hat
seen_by = {}
for mid in ids1:
seen_by[mid] = seen_by.get(mid, 0) | 1
for mid in ids2:
seen_by[mid] = seen_by.get(mid, 0) | 2
# Build dicts
def build_marker_dict(img, corners_list, ids, K, D, draw=False):
centers = {}
corners_dict = {}
poses = {}
for i, mid in enumerate(ids):
C = corners_list[i]
corners_dict[int(mid)] = C
centers[int(mid)] = marker_center_from_corners(C)
# Pose from single marker
img_pts = corners_to_image_points(C)
success, rvec, tvec = cv2.solvePnP(
obj_points,
img_pts,
K,
D,
flags=cv2.SOLVEPNP_IPPE_SQUARE
)
if not success:
success, rvec, tvec = cv2.solvePnP(
obj_points,
img_pts,
K,
D
)
if success:
poses[int(mid)] = (
rvec.reshape(3,1),
tvec.reshape(3,1)
)
if draw:
cv2.drawFrameAxes(draw1, K, D, rvec, tvec, 0.02)
cv2.drawFrameAxes(drawPNG1, K, D, rvec, tvec, 0.02)
return centers, corners_dict, poses
centers1, corners_dict1, poses1 = build_marker_dict(
img1, corners1, ids1, K1, D1, draw=True
)
centers2, corners_dict2, poses2 = build_marker_dict(
img2, corners2, ids2, K2, D2
)
# Common reference markers
common_refs = [
mid for mid in known_markers.keys()
if (mid in centers1 and mid in centers2)
]
if len(common_refs) < 3:
print(f"[ERROR] Need ≥3 common reference markers in both cameras; found {len(common_refs)}: {common_refs}")
sys.exit(1)
# Initial extrinsics
def init_extrinsics_from_rigid(poses_cam, refs):
A = []
B = []
for mid in refs:
if mid in poses_cam:
_, tvec = poses_cam[mid]
A.append(tvec.flatten())
B.append(known_markers[mid])
if len(A) < 2:
raise RuntimeError("Insufficient reference poses for rigid transform init.")
A = np.stack(A, axis=0)
B = np.stack(B, axis=0)
R_cm, t_cm = rigid_transform_no_scale(A, B)
R_mc = R_cm.T
t_mc = - R_mc @ t_cm
return R_mc.astype(np.float32), t_mc.astype(np.float32)
R1_init, t1_init = init_extrinsics_from_rigid(poses1, common_refs)
R2_init, t2_init = init_extrinsics_from_rigid(poses2, common_refs)
# Observations
X_mach_refs = np.stack([
known_markers[mid] for mid in common_refs
], axis=0).astype(np.float32)
obs1_px = np.stack([
centers1[mid] for mid in common_refs
], axis=0).astype(np.float32)
obs2_px = np.stack([
centers2[mid] for mid in common_refs
], axis=0).astype(np.float32)
obs1_norm = undistort_to_normalized(obs1_px, K1, D1)
obs2_norm = undistort_to_normalized(obs2_px, K2, D2)
omega1_init = cv2.Rodrigues(R1_init)[0].reshape(3)
omega2_init = cv2.Rodrigues(R2_init)[0].reshape(3)
theta0 = pack_params(
omega1_init,
t1_init.reshape(3),
omega2_init,
t2_init.reshape(3)
)
theta_opt, hist = lm_solve(
theta0,
X_mach_refs,
obs1_norm,
obs2_norm,
max_iter=60,
eps_jac=1e-6,
lambda_init=1e-3
)
omega1, t1, omega2, t2 = unpack_params(theta_opt)
R1_opt = cv2.Rodrigues(omega1)[0]
R2_opt = cv2.Rodrigues(omega2)[0]
# Camera pose in machine coordinates
R1_cm = R1_opt.T
t1_cm = - R1_cm @ t1
R2_cm = R2_opt.T
t2_cm = - R2_cm @ t2
# Projection matrices
P1 = build_projection_matrix(K1, R1_opt, t1)
P2 = build_projection_matrix(K2, R2_opt, t2)
# Collect markers
all_ids = set(ids1) | set(ids2)
rows = [("id", "x_mm", "y_mm", "z_mm", "roll_deg", "pitch_deg", "yaw_deg", "seen_by")]
marker_list = []
def R_to_euler_zyx(R: np.ndarray):
yaw = float(np.degrees(np.arctan2(R[1,0], R[0,0])))
sp = np.sqrt(R[2,1]**2 + R[2,2]**2)
pitch = float(np.degrees(np.arctan2(-R[2,0], sp)))
roll = float(np.degrees(np.arctan2(R[2,1], R[2,2])))
return roll, pitch, yaw
cam1_roll, cam1_pitch, cam1_yaw = R_to_euler_zyx(R1_cm)
cam2_roll, cam2_pitch, cam2_yaw = R_to_euler_zyx(R2_cm)
c1_mm = (t1_cm * 1000.0).tolist()
rows.append((
"camera 0",
f"{c1_mm[0]:.2f}",
f"{c1_mm[1]:.2f}",
f"{c1_mm[2]:.2f}",
f"{cam1_roll:.3f}",
f"{cam1_pitch:.3f}",
f"{cam1_yaw:.3f}"
))
c2_mm = (t2_cm * 1000.0).tolist()
rows.append((
"camera 1",
f"{c2_mm[0]:.2f}",
f"{c2_mm[1]:.2f}",
f"{c2_mm[2]:.2f}",
f"{cam2_roll:.3f}",
f"{cam2_pitch:.3f}",
f"{cam2_yaw:.3f}"
))
# Triangulate and output markers
def orientation_in_machine(mid: int):
if mid in poses1:
Rm_cam = rvec_to_R(poses1[mid][0])
Rm_machine = R1_cm @ Rm_cam
elif mid in poses2:
Rm_cam = rvec_to_R(poses2[mid][0])
Rm_machine = R2_cm @ Rm_cam
else:
Rm_machine = np.eye(3, dtype=np.float32)
return R_to_euler_zyx(Rm_machine)
for mid in sorted(all_ids):
if mid in centers1 and mid in centers2:
X_mach = triangulate_center(
P1,
P2,
centers1[mid],
centers2[mid]
)
elif mid in poses1:
X_mach = (
R1_cm @ poses1[mid][1].flatten() + t1_cm
)
elif mid in poses2:
X_mach = (
R2_cm @ poses2[mid][1].flatten() + t2_cm
)
else:
continue
roll, pitch, yaw = orientation_in_machine(mid)
x_mm, y_mm, z_mm = (X_mach * 1000.0).tolist()
rows.append((
mid,
f"{x_mm:.2f}",
f"{y_mm:.2f}",
f"{z_mm:.2f}",
f"{roll:.3f}",
f"{pitch:.3f}",
f"{yaw:.3f}",
seen_by.get(mid, 0)
))
marker_list.append({
"id": int(mid),
"position_mm": [float(x_mm), float(y_mm), float(z_mm)],
"orientation_deg": {
"roll": float(roll),
"pitch": float(pitch),
"yaw": float(yaw)
}
})
# ============================================================
# OUTPUT DIRECTORY HANDLING
# ============================================================
base1 = os.path.splitext(os.path.basename(args.images[0]))[0]
base2 = os.path.splitext(os.path.basename(args.images[1]))[0]
if args.outDir is not None:
out_dir = args.outDir
os.makedirs(out_dir, exist_ok=True)
else:
out_dir = os.path.dirname(args.images[0])
if out_dir == "":
out_dir = "."
# Save CSV & JSON
out_csv = os.path.join(out_dir, f"{base1}_two_cam.csv")
out_json = os.path.join(out_dir, f"{base1}_two_cam.json")
try:
import csv
with open(out_csv, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerows(rows)
print(f"[INFO] Saved CSV poses to '{out_csv}'.")
except Exception as e:
print(f"[WARN] Could not save CSV: {e}")
json_data = {
"metadata": {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"reference_markers": common_refs,
"dict": args.dict,
"marker_size_mm": args.markerSizeMM,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [float(x) for x in (t1_cm * 1000.0)],
"orientation_deg": {
"roll": cam1_roll,
"pitch": cam1_pitch,
"yaw": cam1_yaw
},
},
{
"id": "camera2",
"position_mm": [float(x) for x in (t2_cm * 1000.0)],
"orientation_deg": {
"roll": cam2_roll,
"pitch": cam2_pitch,
"yaw": cam2_yaw
},
}
],
"markers": marker_list
}
try:
with open(out_json, 'w', encoding='utf-8') as f:
json.dump(json_data, f, indent=4)
print(f"[INFO] Saved JSON poses to '{out_json}'.")
except Exception as e:
print(f"[WARN] Could not save JSON: {e}")
# Annotate images with machine axes using camera1 extrinsics
try:
R_machine_to_cam1 = R1_opt
t_machine_to_cam1 = t1
machine_axes = np.float32([
[0.0, 0.0, 0.0],
[0.200, 0.0, 0.0],
[0.0, -0.100, 0.0],
[0.0, 0.0, 0.100],
])
rvec_global, _ = cv2.Rodrigues(R_machine_to_cam1)
imgpts, _ = cv2.projectPoints(
machine_axes,
rvec_global,
t_machine_to_cam1,
K1,
D1
)
origin = tuple(np.round(imgpts[0].ravel()).astype(int))
x_end = tuple(np.round(imgpts[1].ravel()).astype(int))
y_end = tuple(np.round(imgpts[2].ravel()).astype(int))
z_end = tuple(np.round(imgpts[3].ravel()).astype(int))
cv2.aruco.drawDetectedMarkers(draw1, corners1)
cv2.aruco.drawDetectedMarkers(drawPNG1, corners1)
try:
for i, mid in enumerate(ids1):
try:
pts = corners1[i].reshape((4, 2))
center = tuple(np.round(pts.mean(axis=0)).astype(int))
except Exception:
continue
text = str(int(mid))
pos = (
int(center[0]) + 15,
int(center[1]) - 15
)
cv2.putText(
draw1,
text,
pos,
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(255,0,0),
3,
lineType=cv2.LINE_AA
)
cv2.putText(
drawPNG1,
text,
pos,
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(255,0,0,255),
3,
lineType=cv2.LINE_AA
)
except Exception:
pass
cv2.line(draw1, origin, x_end, (0,0,255), 3)
cv2.line(draw1, origin, y_end, (0,255,0), 3)
cv2.line(draw1, origin, z_end, (255,0,0), 3)
cv2.line(drawPNG1, origin, x_end, (0,0,255,255), 3)
cv2.line(drawPNG1, origin, y_end, (0,255,0,255), 3)
cv2.line(drawPNG1, origin, z_end, (255,0,0,255), 3)
cv2.putText(draw1, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
cv2.putText(draw1, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
cv2.putText(draw1, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2)
cv2.putText(drawPNG1, "X (200 mm)", x_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255,255), 2)
cv2.putText(drawPNG1, "Y (-100 mm)", y_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0,255), 2)
cv2.putText(drawPNG1, "+Z (100 mm)", z_end, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0,255), 2)
out_img1 = os.path.join(out_dir, f"{base1}_two_cam_annotated.jpg")
cv2.imwrite(out_img1, draw1)
print(f"[INFO] Annotated image saved as '{out_img1}'.")
gray = cv2.cvtColor(drawPNG1, cv2.COLOR_BGR2GRAY)
_, alpha = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
drawPNG_1 = cv2.merge([
drawPNG1[:, :, 0],
drawPNG1[:, :, 1],
drawPNG1[:, :, 2],
alpha
])
out_png1 = os.path.join(out_dir, f"{base1}_two_cam_overlay.png")
cv2.imwrite(out_png1, drawPNG_1)
except Exception as e:
print(f"[WARN] Failed to draw machine axes: {e}")
# Camera 2 overlay
try:
machine_axes2 = np.float32([
[0.0, 0.0, 0.0],
[0.200, 0.0, 0.0],
[0.0, -0.100, 0.0],
[0.0, 0.0, 0.100],
])
rvec_global2, _ = cv2.Rodrigues(R2_opt)
imgpts2, _ = cv2.projectPoints(
machine_axes2,
rvec_global2,
t2,
K2,
D2
)
origin2 = tuple(np.round(imgpts2[0].ravel()).astype(int))
x_end2 = tuple(np.round(imgpts2[1].ravel()).astype(int))
y_end2 = tuple(np.round(imgpts2[2].ravel()).astype(int))
z_end2 = tuple(np.round(imgpts2[3].ravel()).astype(int))
cv2.aruco.drawDetectedMarkers(draw2, corners2)
cv2.aruco.drawDetectedMarkers(drawPNG2, corners2)
try:
for i, mid in enumerate(ids2):
try:
pts = corners2[i].reshape((4, 2))
center = tuple(np.round(pts.mean(axis=0)).astype(int))
except Exception:
continue
text = str(int(mid))
pos = (
int(center[0]) + 13,
int(center[1]) + 3
)
cv2.putText(
draw2,
text,
pos,
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(255,0,0),
3,
lineType=cv2.LINE_AA
)
cv2.putText(
drawPNG2,
text,
pos,
cv2.FONT_HERSHEY_SIMPLEX,
1.0,
(255,0,0,255),
3,
lineType=cv2.LINE_AA
)
except Exception:
pass
cv2.line(draw2, origin2, x_end2, (0,0,255), 3)
cv2.line(draw2, origin2, y_end2, (0,255,0), 3)
cv2.line(draw2, origin2, z_end2, (255,0,0), 3)
cv2.line(drawPNG2, origin2, x_end2, (0,0,255,255), 3)
cv2.line(drawPNG2, origin2, y_end2, (0,255,0,255), 3)
cv2.line(drawPNG2, origin2, z_end2, (255,0,0,255), 3)
cv2.putText(draw2, "X (200 mm)", x_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2)
cv2.putText(draw2, "Y (-100 mm)", y_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0), 2)
cv2.putText(draw2, "+Z (100 mm)", z_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2)
cv2.putText(drawPNG2, "X (200 mm)", x_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255,255), 2)
cv2.putText(drawPNG2, "Y (-100 mm)", y_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,0,255), 2)
cv2.putText(drawPNG2, "+Z (100 mm)", z_end2, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0,255), 2)
out_img2 = os.path.join(out_dir, f"{base2}_two_cam_annotated.jpg")
cv2.imwrite(out_img2, draw2)
print(f"[INFO] Annotated image saved as '{out_img2}'.")
gray2 = cv2.cvtColor(drawPNG2, cv2.COLOR_BGR2GRAY)
_, alpha2 = cv2.threshold(gray2, 0, 255, cv2.THRESH_BINARY)
drawPNG_2 = cv2.merge([
drawPNG2[:, :, 0],
drawPNG2[:, :, 1],
drawPNG2[:, :, 2],
alpha2
])
out_png2 = os.path.join(out_dir, f"{base2}_two_cam_overlay.png")
cv2.imwrite(out_png2, drawPNG_2)
print(f"[INFO] Overlay PNG saved as '{out_png2}'.")
except Exception as e:
print(f"[WARN] Failed to draw machine axes for camera2: {e}")
if __name__ == '__main__':
main()