Arbeiten mit GPT

This commit is contained in:
chk
2026-05-29 07:44:06 +02:00
parent bbbf398279
commit 24f35a1201
54 changed files with 106838 additions and 9332 deletions

View File

@@ -16,6 +16,17 @@ import numpy as np
# Utilities # Utilities
# ------------------------------------------------------------ # ------------------------------------------------------------
def resolve_path(path):
path = os.path.expanduser(path)
# Absoluter Pfad → direkt verwenden
if os.path.isabs(path):
return path
# Relativer Pfad → absolut machen (auf Basis aktuellem cwd)
return os.path.abspath(path)
def load_intrinsics_npz(npz_path: str): def load_intrinsics_npz(npz_path: str):
data = np.load(npz_path) data = np.load(npz_path)
@@ -40,6 +51,9 @@ def load_intrinsics_npz(npz_path: str):
def load_robot_vision_config(robot_json_path: str): def load_robot_vision_config(robot_json_path: str):
robot_json_path = resolve_path(robot_json_path)
with open(robot_json_path, 'r', encoding='utf-8') as f: with open(robot_json_path, 'r', encoding='utf-8') as f:
robot = json.load(f) robot = json.load(f)
@@ -339,7 +353,9 @@ def main():
args = parser.parse_args() args = parser.parse_args()
os.makedirs(args.outDir, exist_ok=True) out_dir = resolve_path(args.outDir)
os.makedirs(out_dir, exist_ok=True)
# -------------------------------------------------------- # --------------------------------------------------------
# Load robot vision config # Load robot vision config
@@ -354,7 +370,10 @@ def main():
# Load image # Load image
# -------------------------------------------------------- # --------------------------------------------------------
image = cv2.imread(args.image)
image_path = resolve_path(args.image)
image = cv2.imread(image_path)
if image is None: if image is None:
raise RuntimeError(f'Cannot read image: {args.image}') raise RuntimeError(f'Cannot read image: {args.image}')
@@ -370,7 +389,9 @@ def main():
# Intrinsics # Intrinsics
# -------------------------------------------------------- # --------------------------------------------------------
K, D = load_intrinsics_npz(args.intrinsics)
intrinsics_path = resolve_path(args.intrinsics)
K, D = load_intrinsics_npz(intrinsics_path)
# -------------------------------------------------------- # --------------------------------------------------------
# Detection # Detection
@@ -562,7 +583,7 @@ def main():
input_base = os.path.splitext(input_filename)[0] input_base = os.path.splitext(input_filename)[0]
out_json = os.path.join( out_json = os.path.join(
args.outDir, out_dir,
f'{input_base}_aruco_detection.json' f'{input_base}_aruco_detection.json'
) )

View File

@@ -170,7 +170,7 @@ class ObservationQualityConfig:
def _load_nested_quality_config(src: Dict[str, Any]) -> Dict[str, Any]: def _load_nested_quality_config(src: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(src, dict): if not isinstance(src, dict):
return {} return {}
for key in ("multiview_quality", "multiviewQuality", "quality_config", "multiview"): for key in ("multiview_quality","multiview_calculation", "multiviewQuality", "quality_config", "multiview"):
v = src.get(key) v = src.get(key)
if isinstance(v, dict): if isinstance(v, dict):
return v return v

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
{
"schema_version": "1.0",
"created_utc": "2026-05-28T21:58:12.453984Z",
"source_robot_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\robot.json",
"source_detections": [
"C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_2a_aruco_detection.json",
"C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_2b_aruco_detection.json",
"C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_2c_aruco_detection.json",
"C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_2d_aruco_detection.json"
],
"solver": {
"final_cost": 22931.22178571188,
"status": 0,
"message": "The maximum number of function evaluations is exceeded."
},
"robot_pose": {
"state": {
"x": 6.55912456537666,
"y": 4.460952073670867,
"z": 30.7175200969605,
"a": 11.566185446462232,
"b": 1.999999999999993,
"c": 8.999999999999904,
"e": 0.9999999999999776
},
"uncertainty": {
"x_mm": 3481.5587672468964,
"y_mm": 1721.1687436205234,
"z_mm": 230.0328185419544,
"a_deg": 559.8212894814748,
"b_deg": 10506.72751291286,
"c_deg": 10506.727245747321,
"e_mm": 105067.37089893252
},
"confidence": {
"x": 6.278037366676565e-152,
"y": 1.7807019001431863e-75,
"z": 1.0228256841704706e-10,
"a": 4.8671004844006695e-25,
"b": 0.0,
"c": 0.0,
"e": 0.36787944117144233
}
},
"statistics": {
"observation_count": 40,
"camera_count": 4,
"marker_count": 23,
"observed_marker_count": 21,
"mean_detector_confidence": 0.46546541612214093,
"mean_weighted_confidence": 0.42464863675174713,
"mean_reprojection_error_px": 189.61546736162268,
"quality_means": {
"size": 0.7059842696189881,
"aspect": 0.8417551598015706,
"border": 0.8583333333333334,
"homography": 0.8064378287982054
},
"quality_config": {
"size_ref_px": 50.0,
"border_ref_px": 120.0,
"center_ref_norm": 0.01,
"sharpness_ref": 2500.0,
"homography_ref": 0.18,
"size_factor": 0.3,
"aspect_factor": 0.3,
"border_factor": 0.01,
"center_factor": 0.01,
"sharpness_factor": 0.5,
"homography_factor": 0.2
}
}
}

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,6 +1,6 @@
{ {
"schema_version": "1.0", "schema_version": "1.0",
"created_utc": "2026-05-28T21:28:20Z", "created_utc": "2026-05-28T21:44:42Z",
"vision_config": { "vision_config": {
"MarkerType": "DICT_4X4_250", "MarkerType": "DICT_4X4_250",
"MarkerSize": 0.025 "MarkerSize": 0.025
@@ -46,7 +46,7 @@
}, },
"detections": [ "detections": [
{ {
"observation_id": "492f34d0-5512-4bf1-9f5c-2e70826f2e71", "observation_id": "4d04662e-5080-4098-8fc4-46dce4e705f5",
"type": "aruco", "type": "aruco",
"marker_id": 102, "marker_id": 102,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -100,7 +100,7 @@
"confidence": 0.9494437061622057 "confidence": 0.9494437061622057
}, },
{ {
"observation_id": "f517029a-9c63-4593-a583-51bbc9991930", "observation_id": "449cb42f-7615-4568-b338-d2f044c6ec16",
"type": "aruco", "type": "aruco",
"marker_id": 243, "marker_id": 243,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -154,7 +154,7 @@
"confidence": 0.8899728634608616 "confidence": 0.8899728634608616
}, },
{ {
"observation_id": "7f05752e-d8d5-48c1-a9de-1d9fe78d34fc", "observation_id": "fc29ae3d-bcba-4cca-9d8b-e23fbe32ead8",
"type": "aruco", "type": "aruco",
"marker_id": 210, "marker_id": 210,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -208,7 +208,7 @@
"confidence": 0.5673043275049208 "confidence": 0.5673043275049208
}, },
{ {
"observation_id": "0f6019cf-ae79-4c58-b42f-454ad8a49b2c", "observation_id": "2c78068c-dce5-4783-af57-f452515adda2",
"type": "aruco", "type": "aruco",
"marker_id": 247, "marker_id": 247,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -262,7 +262,7 @@
"confidence": 0.8796777163094818 "confidence": 0.8796777163094818
}, },
{ {
"observation_id": "b6afa073-1316-4e40-b067-4ae58fa88bca", "observation_id": "bab2a129-54ce-4ea2-b1d7-23977a28b351",
"type": "aruco", "type": "aruco",
"marker_id": 246, "marker_id": 246,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -316,7 +316,7 @@
"confidence": 0.8015179238063419 "confidence": 0.8015179238063419
}, },
{ {
"observation_id": "5b95720f-f38f-4b70-b5a7-2a8407410850", "observation_id": "bb69b260-1543-4e95-80af-b6fee95585aa",
"type": "aruco", "type": "aruco",
"marker_id": 101, "marker_id": 101,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -370,7 +370,7 @@
"confidence": 0.3325004465415643 "confidence": 0.3325004465415643
}, },
{ {
"observation_id": "7dc93dc7-2686-48f6-bae4-c32d18d8a7a8", "observation_id": "e5ba9b93-f275-43e5-ab66-382210ce90d8",
"type": "aruco", "type": "aruco",
"marker_id": 215, "marker_id": 215,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -424,7 +424,7 @@
"confidence": 0.7952557920267838 "confidence": 0.7952557920267838
}, },
{ {
"observation_id": "94d6d6ac-b481-4d2d-a193-1451736ecf89", "observation_id": "4ad2b53e-2dbe-450c-92c7-d06b071cd796",
"type": "aruco", "type": "aruco",
"marker_id": 124, "marker_id": 124,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -478,7 +478,7 @@
"confidence": 0.34091855704160245 "confidence": 0.34091855704160245
}, },
{ {
"observation_id": "7da58bb6-8abb-4cb8-b93a-f19e3b30bab5", "observation_id": "7989f10c-2d6a-44b7-b16d-e721db2e585e",
"type": "aruco", "type": "aruco",
"marker_id": 229, "marker_id": 229,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -532,7 +532,7 @@
"confidence": 0.26810902547334975 "confidence": 0.26810902547334975
}, },
{ {
"observation_id": "c1447225-65f9-4e8e-ac4a-9913fbd3fa4a", "observation_id": "e2db913d-c818-4637-af5e-253d617ca04e",
"type": "aruco", "type": "aruco",
"marker_id": 122, "marker_id": 122,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -586,7 +586,7 @@
"confidence": 0.18665602922528673 "confidence": 0.18665602922528673
}, },
{ {
"observation_id": "c6bf9790-b7cd-4781-addf-593b43be010a", "observation_id": "76503575-cf6f-4ee8-9b3a-311ed41780be",
"type": "aruco", "type": "aruco",
"marker_id": 198, "marker_id": 198,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -640,7 +640,7 @@
"confidence": 0.20150745250271476 "confidence": 0.20150745250271476
}, },
{ {
"observation_id": "ea51fd97-3cc1-450e-8f88-341da29d24fd", "observation_id": "5458000b-97dc-419e-bc20-2cace8792227",
"type": "aruco", "type": "aruco",
"marker_id": 211, "marker_id": 211,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -694,7 +694,7 @@
"confidence": 0.6412317666518965 "confidence": 0.6412317666518965
}, },
{ {
"observation_id": "0f91cfb9-9382-456b-9fe7-2cc966a1fc59", "observation_id": "27ef8edf-4bf1-45cc-80d2-4ae6afc967c8",
"type": "aruco", "type": "aruco",
"marker_id": 208, "marker_id": 208,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -748,7 +748,7 @@
"confidence": 0.6280424706698967 "confidence": 0.6280424706698967
}, },
{ {
"observation_id": "bc4bcbaf-41ff-477e-a495-f1682dcf7c10", "observation_id": "26da9259-8af1-4dc7-b0dc-9f0bf85958c9",
"type": "aruco", "type": "aruco",
"marker_id": 217, "marker_id": 217,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -802,7 +802,7 @@
"confidence": 0.35596573288593486 "confidence": 0.35596573288593486
}, },
{ {
"observation_id": "a11c0669-5498-40dd-ab5d-944be3d29f37", "observation_id": "0caa0146-c0f8-4aaf-b394-95b32436dd0d",
"type": "aruco", "type": "aruco",
"marker_id": 206, "marker_id": 206,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -856,7 +856,7 @@
"confidence": 0.33820423087105295 "confidence": 0.33820423087105295
}, },
{ {
"observation_id": "d0910683-77ca-4ce7-9533-6cffa7f371ab", "observation_id": "0b3c110f-307c-48f6-9012-656bbc3cd9bf",
"type": "aruco", "type": "aruco",
"marker_id": 205, "marker_id": 205,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -910,7 +910,7 @@
"confidence": 0.28724459014346065 "confidence": 0.28724459014346065
}, },
{ {
"observation_id": "4b7bdee0-6a52-452c-b260-7eea65ad36c2", "observation_id": "698b3a77-1475-47ff-a9e7-c14d8240d428",
"type": "aruco", "type": "aruco",
"marker_id": 207, "marker_id": 207,
"marker_size_m": 0.025, "marker_size_m": 0.025,

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,6 +1,6 @@
{ {
"schema_version": "1.0", "schema_version": "1.0",
"created_utc": "2026-05-28T21:28:21Z", "created_utc": "2026-05-28T21:44:43Z",
"vision_config": { "vision_config": {
"MarkerType": "DICT_4X4_250", "MarkerType": "DICT_4X4_250",
"MarkerSize": 0.025 "MarkerSize": 0.025
@@ -46,7 +46,7 @@
}, },
"detections": [ "detections": [
{ {
"observation_id": "0c8f6caa-0447-4f7c-99c4-2d30e31d30d6", "observation_id": "0ebc83d9-bb33-40a7-943b-78666c577574",
"type": "aruco", "type": "aruco",
"marker_id": 102, "marker_id": 102,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -100,7 +100,7 @@
"confidence": 0.8913851020933449 "confidence": 0.8913851020933449
}, },
{ {
"observation_id": "2df4a132-33c5-4ebf-b29f-dcc52478335b", "observation_id": "3e2026ef-788c-4f2c-9d0e-3bb4a4098c49",
"type": "aruco", "type": "aruco",
"marker_id": 124, "marker_id": 124,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -154,7 +154,7 @@
"confidence": 0.4804167810936165 "confidence": 0.4804167810936165
}, },
{ {
"observation_id": "ba6b5ed9-6a18-47e7-a843-af281c19c30f", "observation_id": "ebb20469-1b13-413a-8ea1-a35701ba2bd4",
"type": "aruco", "type": "aruco",
"marker_id": 243, "marker_id": 243,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -208,7 +208,7 @@
"confidence": 0.9132502955127223 "confidence": 0.9132502955127223
}, },
{ {
"observation_id": "f27e2575-2b8b-497b-be0a-b993197fd894", "observation_id": "211ff361-5d5c-4822-88b6-c5cec573090d",
"type": "aruco", "type": "aruco",
"marker_id": 122, "marker_id": 122,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -262,7 +262,7 @@
"confidence": 0.9456847621099602 "confidence": 0.9456847621099602
}, },
{ {
"observation_id": "a2356407-9a0c-460e-a2af-812ffddaddbf", "observation_id": "45c8a29e-1471-4fb7-bffc-27bdeb579be1",
"type": "aruco", "type": "aruco",
"marker_id": 247, "marker_id": 247,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -316,7 +316,7 @@
"confidence": 0.7448005120854795 "confidence": 0.7448005120854795
}, },
{ {
"observation_id": "e37d03aa-9079-465a-8278-e568958327fc", "observation_id": "16d87417-4ffe-4ce0-b51f-1e3fe5c9695e",
"type": "aruco", "type": "aruco",
"marker_id": 246, "marker_id": 246,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -370,7 +370,7 @@
"confidence": 0.7581700566520715 "confidence": 0.7581700566520715
}, },
{ {
"observation_id": "64462f28-959c-436f-9e7a-7b7e2c180bb7", "observation_id": "8c33c157-e141-4212-9279-69ad56760f53",
"type": "aruco", "type": "aruco",
"marker_id": 215, "marker_id": 215,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -424,7 +424,7 @@
"confidence": 0.826449608683203 "confidence": 0.826449608683203
}, },
{ {
"observation_id": "23c3a60a-e454-4ac1-9ba7-13505aa8ffb7", "observation_id": "fe48446c-7538-4bd8-8cec-e96c3e5148b9",
"type": "aruco", "type": "aruco",
"marker_id": 210, "marker_id": 210,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -478,7 +478,7 @@
"confidence": 0.7813266383036261 "confidence": 0.7813266383036261
}, },
{ {
"observation_id": "977958d8-7446-4234-ab37-604bb459f146", "observation_id": "31c12fb9-cb8b-4677-89dc-b24351a14990",
"type": "aruco", "type": "aruco",
"marker_id": 229, "marker_id": 229,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -532,7 +532,7 @@
"confidence": 0.18684041285479083 "confidence": 0.18684041285479083
}, },
{ {
"observation_id": "28ae43e2-3bd3-4829-87d0-e9cb9526e304", "observation_id": "a059901a-85cf-42e8-870f-c88813554c08",
"type": "aruco", "type": "aruco",
"marker_id": 198, "marker_id": 198,
"marker_size_m": 0.025, "marker_size_m": 0.025,

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,6 +1,6 @@
{ {
"schema_version": "1.0", "schema_version": "1.0",
"created_utc": "2026-05-28T21:28:21Z", "created_utc": "2026-05-28T21:44:43Z",
"vision_config": { "vision_config": {
"MarkerType": "DICT_4X4_250", "MarkerType": "DICT_4X4_250",
"MarkerSize": 0.025 "MarkerSize": 0.025
@@ -46,7 +46,7 @@
}, },
"detections": [ "detections": [
{ {
"observation_id": "56c7970c-3fdc-437a-82dd-918ef861ac60", "observation_id": "b2febb1e-6f84-4da7-b222-dad6bb306cfc",
"type": "aruco", "type": "aruco",
"marker_id": 102, "marker_id": 102,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -100,7 +100,7 @@
"confidence": 0.7012635429952238 "confidence": 0.7012635429952238
}, },
{ {
"observation_id": "4166196c-2e75-4461-97c1-378b70b0754f", "observation_id": "faabcc45-d069-4b1e-bbf4-a12019902b8a",
"type": "aruco", "type": "aruco",
"marker_id": 122, "marker_id": 122,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -154,7 +154,7 @@
"confidence": 0.5749326438029929 "confidence": 0.5749326438029929
}, },
{ {
"observation_id": "1e515a3c-1477-4fc5-b2e9-4a5c759201dc", "observation_id": "0773bfb1-9c4f-4add-9a74-bca81c38b2e8",
"type": "aruco", "type": "aruco",
"marker_id": 243, "marker_id": 243,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -208,7 +208,7 @@
"confidence": 0.9491420540234864 "confidence": 0.9491420540234864
}, },
{ {
"observation_id": "1549938d-da8e-4883-9239-92b88fc508c9", "observation_id": "abf720ab-c1d5-40c7-ad4f-9f58f8bd6ee1",
"type": "aruco", "type": "aruco",
"marker_id": 246, "marker_id": 246,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -262,7 +262,7 @@
"confidence": 0.4780463267147134 "confidence": 0.4780463267147134
}, },
{ {
"observation_id": "634d2ef2-2e64-4009-8d92-c16368d1b20a", "observation_id": "13752012-4478-43b7-b2f9-eab237d61864",
"type": "aruco", "type": "aruco",
"marker_id": 247, "marker_id": 247,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -316,7 +316,7 @@
"confidence": 0.5106218488640142 "confidence": 0.5106218488640142
}, },
{ {
"observation_id": "e96b7926-73c9-42c3-9c6e-e7bd66a0b4b7", "observation_id": "823b9adb-31fb-414d-aaaa-552432206d5c",
"type": "aruco", "type": "aruco",
"marker_id": 214, "marker_id": 214,
"marker_size_m": 0.025, "marker_size_m": 0.025,
@@ -370,7 +370,7 @@
"confidence": 0.6041252446922665 "confidence": 0.6041252446922665
}, },
{ {
"observation_id": "c57683ff-d5c4-412c-b893-95d36298186f", "observation_id": "a33bdbe4-4e7d-42da-9c9e-6b5eaf4b5d8f",
"type": "aruco", "type": "aruco",
"marker_id": 210, "marker_id": 210,
"marker_size_m": 0.025, "marker_size_m": 0.025,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,462 @@
{
"coordinateSystem": {
"handedness": "right",
"x": "right",
"y": "backward",
"z": "up"
},
"units": {
"length": "mm",
"rotation": "degree"
},
"vision_config": {
"MarkerType": "DICT_4X4_250",
"MarkerSize": 0.025
},
"renderingInfo": {
"width": 1280,
"height": 720,
"cameraPosition": [-200, -900, 200],
"cameraTarget": [210, -180, 180],
"cameraUpVector": [0, 0, 1],
"lightPosition": [-500, -500, 500],
"lightTarget": [0, 0, 0],
"lightUpVector": [0, 0, 1],
"metric": "mm",
"showSkeleton": true,
"showMarkers": true,
"backgroundColor": [0.70, 0.85, 1.0],
"backgroundStrength": 0.20,
"sunEnergy": 0.35,
"areaEnergy": 120,
"exposure": -1.5,
"materials": {
"wood": {
"baseColor": [0.72, 0.52, 0.33],
"roughness": 0.85,
"metallic": 0.0
},
"plaWhite": {
"baseColor": [0.95, 0.95, 0.95],
"roughness": 0.45,
"metallic": 0.0
},
"steel": {
"baseColor": [0.72, 0.72, 0.75],
"roughness": 0.25,
"metallic": 1.0
},
"powderCoatBlue": {
"baseColor": [0.15, 0.25, 0.7],
"roughness": 0.55,
"metallic": 0.0
},
"defaultPlastic": {
"baseColor": [0.95, 0.95, 0.95],
"roughness": 0.4,
"metallic": 0.0
},
"skeletonRed": {
"baseColor": [0.85, 0.20, 0.20],
"roughness": 0.35,
"metallic": 0.0
},
"markerBlack": {
"baseColor": [0.04, 0.04, 0.04],
"roughness": 0.80,
"metallic": 0.0
}
},
"skeletonDefaults": {
"radius": 4,
"color": [0.85, 0.20, 0.20]
},
"markerDefaults": {
"size": 25,
"thickness": 1,
"color": [0.04, 0.04, 0.04]
}
},
"defaultPosition": {
"x": 100,
"y": 30,
"z": -30,
"a": -120,
"b": 22,
"c": 91,
"e": 10
},
"recognized": {
"x": null,
"y": null,
"z": null,
"a": null,
"b": null,
"c": null,
"e": null
},
"multiview_calculation": {
"combine_mode": "mean",
"size_ref_px": 50.0,
"border_ref_px": 120.0,
"center_ref_norm": 0.01,
"sharpness_ref": 2500.0,
"homography_ref": 0.18,
"size_factor": 0.3,
"aspect_factor": 0.3,
"border_factor": 0.01,
"center_factor": 0.01,
"sharpness_factor": 0.5,
"homography_factor": 0.2,
"normal_visibility_factor": 0.01,
"spin_factor": 0.3,
"weight_floor": 0.3
},
"movements": {
"x": null,
"y": null,
"z": null,
"a": null,
"b": null,
"c": null,
"e": null
},
"links": {
"Board": {
"parent": null,
"size": [1000, 200, 25],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"skeleton": {
"from": [0, 0, 16],
"to": [1000, 0, 16],
"radius": 4,
"color": [0.85, 0.20, 0.20]
},
"markers":[
{"id":210,"position":[20, -20, 0.3], "normal":[0,0,1]},
{"id":211,"position":[250, -10, 0.3], "normal":[0,0,1]},
{"id":215,"position":[250, -90, 0.3], "normal":[0,0,1]},
{"id":214,"position":[350, -10, 0.3], "normal":[0,0,1]},
{"id":208,"position":[350, -90, 0.3], "normal":[0,0,1]},
{"id":206,"position":[650, -10, 0.3], "normal":[0,0,1]},
{"id":205,"position":[750, -90, 0.3], "normal":[0,0,1]},
{"id":207,"position":[750, -10, 0.3], "normal":[0,0,1]},
{"id":217,"position":[650, -90, 0.3], "normal":[0,0,1]}
],
"model": [
{
"stlFile": "surfaces/Board.stl",
"originOfModel": [0, 0, 0],
"rotationOfModelDegree": [0, 0, -90],
"material": "wood"
},
{
"stlFile": "surfaces/BoardRail.stl",
"originOfModel": [0, 0, 0],
"rotationOfModelDegree": [0, 0, -90],
"material": "steel"
}
]
},
"Base": {
"parent": "Board",
"size": [150, 200, 150],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Slider",
"type": "linear",
"axis": [1, 0, 0],
"origin": [0, 0, 16],
"rotation": [0, 0, 0],
"variable": "x"
},
"skeleton": {
"from": [0, 108, 45],
"to": [110, 108, 45],
"radius": 4,
"color": [0.20, 0.80, 0.20]
},
"markers": [
],
"model": [
{
"stlFile": "surfaces/Base.stl",
"originOfModel": [-30, 0, -35],
"rotationOfModelDegree": [0, 0, 0],
"material": "plaWhite"
}
]
},
"Arm1": {
"parent": "Base",
"size": [70, 250, 70],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint1",
"type": "revolute",
"axis": [-1, 0, 0],
"origin": [110, 108, 45],
"rotation": [0, 0, 0],
"variable": "y"
},
"skeleton": {
"from": [0, 0, 0],
"to": [0, -250, 0],
"radius": 4,
"color": [0.20, 0.20, 0.90]
},
"markers": [
{
"id": 198,
"name": "aruco_198",
"position": [0, -160, 35],
"normal": [0, 0, 1],
"size": 25,
"spin": 0
},
{
"id": 229,
"name": "aruco_229",
"position": [0, -250, 35],
"normal": [0, 0, 1],
"size": 25,
"spin": 0
},
{
"id": 242,
"name": "aruco_242",
"position": [0, -250, -35],
"normal": [0, 0, -1],
"size": 25,
"spin": 0
},
{
"id": 243,
"name": "aruco_243",
"position": [0, -285, 0],
"normal": [0, -1, 0],
"size": 25,
"spin": 0
}
],
"model": [
{
"stlFile": "surfaces/Holm.stl",
"originOfModel__": [-25,29,-28.5],
"originOfModel": [-29,25,28.5],
"rotationOfModelDegree__": [0, 0, 0],
"rotationOfModelDegree": [180, 0, -90],
"material": "powderCoatBlue"
}
]
},
"Ellbow": {
"parent": "Arm1",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint2",
"type": "revolute",
"axis": [-1, 0, 0],
"origin": [0, -250, 0],
"rotation": [0, 0, 0],
"variable": "z"
},
"skeleton": {
"from": [0, 0, 0],
"to": [90, 0, 0],
"radius": 4,
"color": [0.90, 0.20, 0.20]
},
"model": [
{
"stlFile": "surfaces/Ellebogen.stl",
"originOfModel": [90,0,0],
"rotationOfModelDegree": [0,-90,-90],
"material": "defaultPlastic"
}
],
"markers": [
{
"id": 244,
"name": "aruco_244",
"position": [125, 0, 0],
"normal": [1, 0, 0],
"size": 25,
"spin": 0
},
{
"id": 245,
"name": "aruco_245",
"position": [90, 0, -35],
"normal": [0, 0, -1],
"size": 25,
"spin": 0
},
{
"id": 246,
"name": "aruco_246",
"position": [90, 0, 35],
"normal": [0, 0, 1],
"size": 25
},
{
"id": 247,
"name": "aruco_247",
"position": [52.5, 0, 35],
"normal": [0, 0, 1],
"size": 25
}
]
},
"Arm2": {
"parent": "Ellbow",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint3",
"type": "revolute",
"axis": [0, -1, 0],
"origin": [90, 0, 0],
"rotation": [0, 0, 0],
"variable": "a"
},
"skeleton": {
"from": [0, 0, 0],
"to": [0, -250, 0],
"radius": 4,
"color": [0.95, 0.85, 0.20]
},
"model": [
{
"stlFile": "surfaces/Unterarm.stl",
"originOfModel": [0,-250,0],
"rotationOfModelDegree": [180, 0, -90],
"material": "defaultPlastic"
}
],
"markers":[
{"id":124, "position":[24.75, -112, -24.75],"normal":[1,0,-1]},
{"id":122, "name": "aruco_122", "position":[-35,-112,0], "normal":[-1,0,0]},
{"id":218, "name": "aruco_218", "position":[35,-112,0], "normal":[1,0,0]},
{"id":122, "name": "aruco_122", "position":[0, -182, 30],"normal":[0,0,1]},
{"id":101, "name": "aruco_122", "position":[ 24.75, -182, -24.75],"normal":[ 1,0,-1]},
{"id":102, "name": "aruco_122", "position":[-24.75, -182, -24.75],"normal":[-1,0,-1]},
{"id":124, "name": "aruco_124", "position":[-35,-219,0], "normal":[-1,0,0]},
{"id":219, "name": "aruco_219", "position":[35,-219,0], "normal":[1,0,0]}
]
},
"Hand": {
"parent": "Arm2",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint4",
"type": "revolute",
"axis": [1, 0, 0],
"origin": [0, -250, 0],
"rotation": [0, 0, 0],
"variable": "b"
},
"skeleton": {
"from": [0, 0, 0],
"to": [0, -35, 0],
"radius": 4,
"color": [0.95, 0.55, 0.15]
}
},
"Palm": {
"parent": "Hand",
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Joint3",
"type": "revolute",
"axis": [0, -1, 0],
"origin": [0, 0, 0],
"rotation": [0, 0, 0],
"variable": "c"
},
"skeleton": {
"from": [-50, -35, 0],
"to": [50, -35, 0],
"radius": 7,
"color": [0.95, 0.20, 0.20]
}
},
"FingerA": {
"parent": "Palm",
"size": [80, 60, 20],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Slider",
"type": "linear",
"axis": [1, 0, 0],
"origin": [4, -35,0],
"rotation": [0, 0, 0],
"variable": "e"
},
"skeleton": {
"from": [0, 0,0],
"to": [0, -60, 0],
"radius": 4,
"color": [0.20, 0.80, 0.20]
},
"model": [
{
"stlFile": "surfaces/Finger.stl",
"originOfModel": [24,0,-9.1],
"rotationOfModelDegree": [90, -90,0],
"material": "defaultPlastic"
}
]
},
"FingerB": {
"parent": "Palm",
"size": [80, 60, 20],
"mountPosition": [0, 0, 0],
"mountRotation": [0, 0, 0],
"jointToParent": {
"name": "Slider",
"type": "linear",
"axis": [-1, 0, 0],
"origin": [-4, -35,0],
"rotation": [0, 0, 0],
"variable": "e"
},
"skeleton": {
"from": [0, 0,0],
"to": [0, -60, 0],
"radius": 4,
"color": [0.20, 0.80, 0.20]
},
"model": [
{
"stlFile": "surfaces/Finger.stl",
"originOfModel": [-24,0,9.1],
"rotationOfModelDegree": [90, 90,0],
"material": "defaultPlastic"
}
]
}
}
}

View File

@@ -2,5 +2,6 @@
python3 1_detect_aruco_observations.py --image render_1a.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir . python3 1_detect_aruco_observations.py --image render_1a.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_1b.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir . python3 1_detect_aruco_observations.py --image render_1b.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_1c.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir . python3 1_detect_aruco_observations.py --image render_1c.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_1d.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 2_Multiview.py --robot ../robot.json --detections render_1a_aruco_detection.json render_1b_aruco_detection.json render_1c_aruco_detection.json --outDir . --write-summary --max-iter 120 python3 2_Multiview.py --robot ../robot.json --detections render_1a_aruco_detection.json render_1b_aruco_detection.json render_1c_aruco_detection.json render_1d_aruco_detection.json --outDir . --write-summary --max-iter 120

View File

@@ -0,0 +1,7 @@
python3 1_detect_aruco_observations.py --image render_2a.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_2b.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_2c.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_2d.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 2_Multiview.py --robot ../robot.json --detections render_2a_aruco_detection.json render_2b_aruco_detection.json render_2c_aruco_detection.json render_2d_aruco_detection.json --outDir . --write-summary --max-iter 120

File diff suppressed because it is too large Load Diff

View File

@@ -1,787 +0,0 @@
#!/usr/bin/env python3
"""
============================================================
STEP 2b — Simultane Multiview-Optimierung für Roboterpose
============================================================
Ziel:
Aus mehreren ArUco-Detektionsdateien die gemeinsame
Roboterpose (x,y,z,a,b,c,e) schätzen und jede Kamera-Pose
sowie Marker-Weltpositionen ausgeben.
Eingabe:
--robot ../robot.json
--detections render_1a_aruco_detection.json render_1b_aruco_detection.json ...
--outDir .
Ausgabe:
multiview_pose.json
Hinweis:
Dieses Skript verwendet die Markerpositionen aus robot.json
als kinematische Constraints und optimiert gleichzeitig:
- Roboterzustand (x,y,z,a,b,c,e)
- Kameraextrinsische Parameter pro Bild
"""
import argparse
import datetime
import json
import math
import os
import time
from pathlib import Path
from typing import Any, Dict, List, Tuple
import cv2
import numpy as np
from scipy.optimize import least_squares
STATE_KEYS = ["x", "y", "z", "a", "b", "c", "e"]
# ------------------------------------------------------------------
# Constraint definitions and validation
# ------------------------------------------------------------------
class ConstraintResult:
"""Result of validating/applying a single constraint"""
def __init__(self, name: str, enabled: bool, reason: str = ""):
self.name = name
self.enabled = enabled
self.reason = reason
self.residuals = []
def __str__(self) -> str:
status = "✓ ENABLED" if self.enabled else "✗ DISABLED"
return f"{self.name:40s} {status:12s} {self.reason}"
def validate_constraints(robot: Dict[str, Any], robot_markers: Dict[int, Dict[str, Any]]) -> Dict[str, ConstraintResult]:
"""
Validate which constraints can be applied based on robot geometry.
Returns a dict of constraint_name -> ConstraintResult
"""
results = {}
# --- Constraint 1: Rigid body distances within each link ---
rigid_body_result = ConstraintResult("RigidBodyDistances", False)
try:
rigid_body_count = 0
for link_name in ['Arm1', 'Ellbow', 'Arm2']:
link_markers = [m for m in robot_markers.values() if m['link_name'] == link_name]
if len(link_markers) >= 2:
rigid_body_count += 1
if rigid_body_count >= 2:
rigid_body_result.enabled = True
rigid_body_result.reason = f"Found {rigid_body_count} links with 2+ markers each"
else:
rigid_body_result.reason = "Not enough rigid links with multiple markers"
except Exception as e:
rigid_body_result.reason = f"Error: {str(e)}"
results['RigidBodyDistances'] = rigid_body_result
# --- Constraint 2: Fixed X-distances between links (rotation around X-axis) ---
inter_link_x_result = ConstraintResult("InterLinkXDistances", False)
try:
links_with_markers = set(m['link_name'] for m in robot_markers.values())
x_rotated_links = []
for link_name in ['Arm1', 'Ellbow']:
if link_name in links_with_markers:
link_markers = [m for m in robot_markers.values() if m['link_name'] == link_name]
if len(link_markers) >= 1:
x_rotated_links.append(link_name)
if len(x_rotated_links) >= 2:
inter_link_x_result.enabled = True
inter_link_x_result.reason = f"Found {len(x_rotated_links)} X-rotation links: {', '.join(x_rotated_links)}"
else:
inter_link_x_result.reason = "Not enough X-rotation links"
except Exception as e:
inter_link_x_result.reason = f"Error: {str(e)}"
results['InterLinkXDistances'] = inter_link_x_result
# --- Sanity check (not a hard constraint): Arm2 sin(a) dependency ---
arm2_sina_result = ConstraintResult("Arm2SinADependency", True, "Sanity check only (not enforced)")
try:
arm2_markers = [m for m in robot_markers.values() if m['link_name'] == 'Arm2']
if len(arm2_markers) >= 2:
z_values = set(m['position_m'][2] for m in arm2_markers)
if len(z_values) > 1:
arm2_sina_result.enabled = True
arm2_sina_result.reason = "Multiple Z-values detected; sin(a) dependency confirmed"
else:
arm2_sina_result.enabled = False
arm2_sina_result.reason = "No Z-variation in Arm2 markers (cannot use sin(a) constraint)"
else:
arm2_sina_result.enabled = False
arm2_sina_result.reason = "Not enough Arm2 markers"
except Exception as e:
arm2_sina_result.reason = f"Error: {str(e)}"
results['Arm2SinADependency'] = arm2_sina_result
return results
# ------------------------------------------------------------------
# JSON helpers
# ------------------------------------------------------------------
def load_json(path: str) -> Dict[str, Any]:
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def save_json(data: Dict[str, Any], path: Path) -> None:
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
# ------------------------------------------------------------------
# robot.json helpers
# ------------------------------------------------------------------
def resolve_scalar(value: Any, default: float = 0.0) -> float:
if value is None:
return default
if isinstance(value, (int, float)):
return float(value)
try:
return float(str(value).strip())
except ValueError:
return default
def resolve_vector(value: Any, default_len: int = 3) -> Tuple[float, ...]:
if value is None:
return tuple(0.0 for _ in range(default_len))
if isinstance(value, (int, float, str)):
return (resolve_scalar(value),) + tuple(0.0 for _ in range(default_len - 1))
if isinstance(value, (list, tuple)):
resolved = [resolve_scalar(v) for v in value]
if len(resolved) < default_len:
resolved.extend([0.0] * (default_len - len(resolved)))
return tuple(resolved[:default_len])
return tuple(0.0 for _ in range(default_len))
def parse_metric_scale(robot: Dict[str, Any]) -> float:
rendering_info = robot.get('renderingInfo', {}) or {}
metric = rendering_info.get('metric', 'mm')
return 0.001 if str(metric).strip().lower() == 'mm' else 1.0
def normalize_axis(axis: Any) -> np.ndarray:
vec = np.asarray(axis, dtype=np.float64)
if vec.shape != (3,):
vec = vec.reshape(-1)[:3]
norm = np.linalg.norm(vec)
return vec / max(norm, 1e-9)
def euler_deg_to_matrix(euler_deg: Any) -> np.ndarray:
x_deg, y_deg, z_deg = resolve_vector(euler_deg, 3)
x = math.radians(x_deg)
y = math.radians(y_deg)
z = math.radians(z_deg)
cx = math.cos(x)
sx = math.sin(x)
cy = math.cos(y)
sy = math.sin(y)
cz = math.cos(z)
sz = math.sin(z)
Rx = np.array([
[1.0, 0.0, 0.0],
[0.0, cx, -sx],
[0.0, sx, cx]
], dtype=np.float64)
Ry = np.array([
[cy, 0.0, sy],
[0.0, 1.0, 0.0],
[-sy, 0.0, cy]
], dtype=np.float64)
Rz = np.array([
[cz, -sz, 0.0],
[sz, cz, 0.0],
[0.0, 0.0, 1.0]
], dtype=np.float64)
return Rz @ Ry @ Rx
def transform_from_translation_rotation(translation: Any, rotation_deg: Any) -> np.ndarray:
T = np.eye(4, dtype=np.float64)
pos = np.asarray(resolve_vector(translation, 3), dtype=np.float64)
T[:3, 3] = pos
T[:3, :3] = euler_deg_to_matrix(rotation_deg)
return T
def axis_angle_matrix(axis: Any, angle_deg: float) -> np.ndarray:
axis_vec = normalize_axis(axis)
theta = math.radians(angle_deg)
kx, ky, kz = axis_vec
c = math.cos(theta)
s = math.sin(theta)
v = 1.0 - c
R = np.array([
[kx * kx * v + c, kx * ky * v - kz * s, kx * kz * v + ky * s],
[ky * kx * v + kz * s, ky * ky * v + c, ky * kz * v - kx * s],
[kz * kx * v - ky * s, kz * ky * v + kx * s, kz * kz * v + c]
], dtype=np.float64)
T = np.eye(4, dtype=np.float64)
T[:3, :3] = R
return T
# ------------------------------------------------------------------
# Kinematics and marker extraction
# ------------------------------------------------------------------
def extract_markers(robot: Dict[str, Any], scale: float) -> Dict[int, Dict[str, Any]]:
markers = {}
links = robot.get('links', {}) or {}
marker_defaults = (robot.get('renderingInfo', {}) or {}).get('markerDefaults', {}) or {}
default_size_mm = float(marker_defaults.get('size', 25.0))
for link_name, link_info in links.items():
for marker in link_info.get('markers', []) or []:
marker_id = int(marker.get('id', -1))
if marker_id < 0:
continue
pos = resolve_vector(marker.get('position', [0, 0, 0]), 3)
size_mm = float(marker.get('size', default_size_mm))
markers[marker_id] = {
'marker_id': marker_id,
'link_name': link_name,
'position_m': np.asarray([pos[0] * scale, pos[1] * scale, pos[2] * scale], dtype=np.float64),
'normal': normalize_axis(resolve_vector(marker.get('normal', [0, 0, 1]), 3)),
'spin_deg': float(marker.get('spin', 0.0)),
'size_m': size_mm * scale,
}
return markers
def marker_plane_axes(normal: np.ndarray, spin_deg: float) -> Tuple[np.ndarray, np.ndarray]:
n = normalize_axis(normal)
candidate = np.array((0.0, 0.0, 1.0), dtype=np.float64)
if abs(np.dot(n, candidate)) > 0.99:
candidate = np.array((1.0, 0.0, 0.0), dtype=np.float64)
x_dir = np.cross(candidate, n)
x_dir /= max(np.linalg.norm(x_dir), 1e-9)
y_dir = np.cross(n, x_dir)
if abs(spin_deg) > 1e-6:
theta = math.radians(spin_deg)
cos_t = math.cos(theta)
sin_t = math.sin(theta)
x_rot = x_dir * cos_t + np.cross(n, x_dir) * sin_t + n * np.dot(n, x_dir) * (1.0 - cos_t)
y_rot = y_dir * cos_t + np.cross(n, y_dir) * sin_t + n * np.dot(n, y_dir) * (1.0 - cos_t)
return x_rot, y_rot
return x_dir, y_dir
def marker_object_corners(marker: Dict[str, Any]) -> np.ndarray:
half = marker['size_m'] * 0.5
x_dir, y_dir = marker_plane_axes(marker['normal'], marker['spin_deg'])
corners = np.stack([
-x_dir * half + y_dir * half,
x_dir * half + y_dir * half,
x_dir * half - y_dir * half,
-x_dir * half - y_dir * half
], axis=0)
return marker['position_m'].reshape(1, 3) + corners
def build_link_chain(robot: Dict[str, Any]) -> List[str]:
links = robot.get('links', {}) or {}
ordered: List[str] = []
remaining = set(links.keys())
while remaining:
progress = False
for name in list(remaining):
parent = links[name].get('parent')
if not parent or parent in ordered:
ordered.append(name)
remaining.remove(name)
progress = True
if not progress:
raise RuntimeError('Cycle detected in robot link tree or missing parent link')
return ordered
def compute_link_transforms(robot: Dict[str, Any], state: Dict[str, float], scale: float) -> Dict[str, np.ndarray]:
links = robot.get('links', {}) or {}
ordered_links = build_link_chain(robot)
transforms: Dict[str, np.ndarray] = {}
for link_name in ordered_links:
link_info = links[link_name] or {}
parent_name = link_info.get('parent')
parent_transform = transforms[parent_name] if parent_name else np.eye(4, dtype=np.float64)
mount_translation = np.asarray(resolve_vector(link_info.get('mountPosition', [0, 0, 0]), 3), dtype=np.float64) * scale
mount = transform_from_translation_rotation(
mount_translation,
link_info.get('mountRotation', [0, 0, 0])
)
joint_info = link_info.get('jointToParent', {}) or {}
joint_origin = np.asarray(resolve_vector(joint_info.get('origin', [0, 0, 0]), 3), dtype=np.float64) * scale
joint = transform_from_translation_rotation(
joint_origin,
joint_info.get('rotation', [0, 0, 0])
)
motion = np.eye(4, dtype=np.float64)
joint_type = str(joint_info.get('type', 'fixed')).strip().lower()
control_var = str(joint_info.get('variable', joint_info.get('control', ''))).strip().lower()
axis = resolve_vector(joint_info.get('axis', [1, 0, 0]), 3)
if joint_type == 'linear':
motion[:3, 3] = normalize_axis(axis) * state.get(control_var, 0.0) * scale
elif joint_type == 'revolute':
motion = axis_angle_matrix(axis, state.get(control_var, 0.0))
transforms[link_name] = parent_transform @ mount @ joint @ motion
return transforms
def compute_marker_world_position(marker: Dict[str, Any], link_transforms: Dict[str, np.ndarray]) -> np.ndarray:
link_transform = link_transforms[marker['link_name']]
local = np.ones(4, dtype=np.float64)
local[:3] = marker['position_m']
world = link_transform @ local
return world[:3]
# ------------------------------------------------------------------
# Camera / observation helpers
# ------------------------------------------------------------------
def load_intrinsics(detection_json: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
cam = detection_json['camera']
K = np.asarray(cam['camera_matrix'], dtype=np.float64)
D = np.asarray(cam.get('distortion_coefficients', [0, 0, 0, 0, 0]), dtype=np.float64).reshape(-1, 1)
return K, D
def collect_views_and_observations(
detection_files: List[str],
robot_markers: Dict[int, Dict[str, Any]]
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
views: List[Dict[str, Any]] = []
observations: List[Dict[str, Any]] = []
for idx, det_path in enumerate(detection_files):
detection_json = load_json(det_path)
K, D = load_intrinsics(detection_json)
views.append({
'index': idx,
'source_file': os.path.abspath(det_path),
'camera_id': detection_json.get('camera', {}).get('camera_id', f'cam{idx+1}'),
'image_file': detection_json.get('image', {}).get('image_file'),
'K': K,
'D': D
})
for det in detection_json.get('detections', []) or []:
marker_id = int(det.get('marker_id', -1))
if marker_id < 0 or marker_id not in robot_markers:
continue
image_points = det.get('image_points_px')
if isinstance(image_points, list) and len(image_points) == 4:
image_points = np.asarray(image_points, dtype=np.float64)
else:
center = resolve_vector(det.get('center_px', [0, 0]), 2)
image_points = np.asarray([center], dtype=np.float64)
confidence = float(det.get('confidence', 1.0))
marker = robot_markers[marker_id]
observations.append({
'view_index': idx,
'marker_id': marker_id,
'marker_link_corners': marker_object_corners(marker),
'image_points_px': image_points,
'confidence': max(0.01, min(1.0, confidence))
})
if len(views) == 0:
raise RuntimeError('No valid detection views found')
if len(observations) == 0:
raise RuntimeError('No marker observations matched robot.json markers')
return views, observations
def compute_marker_world_corners(marker: Dict[str, Any], link_transforms: Dict[str, np.ndarray]) -> np.ndarray:
link_transform = link_transforms[marker['link_name']]
local = marker_object_corners(marker)
homogeneous = np.concatenate([local, np.ones((local.shape[0], 1), dtype=np.float64)], axis=1)
world = (link_transform @ homogeneous.T).T
return world[:, :3]
def initial_camera_guess(
view: Dict[str, Any],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
default_state: Dict[str, float],
scale: float,
robot: Dict[str, Any]
) -> Tuple[np.ndarray, np.ndarray]:
object_points = []
image_points = []
link_transforms = compute_link_transforms(robot, default_state, scale)
for obs in observations:
if obs['view_index'] != view['index']:
continue
marker = robot_markers[obs['marker_id']]
object_points.append(compute_marker_world_corners(marker, link_transforms))
image_points.append(obs['image_points_px'])
if len(object_points) == 0:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
object_points = np.vstack(object_points)
image_points = np.vstack(image_points)
if object_points.shape[0] < 4:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
success, rvec, tvec = cv2.solvePnP(
object_points,
image_points,
view['K'],
view['D'],
flags=cv2.SOLVEPNP_ITERATIVE
)
if not success:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
return rvec, tvec
def project_points(
points_3d: np.ndarray,
rvec: np.ndarray,
tvec: np.ndarray,
K: np.ndarray,
D: np.ndarray
) -> np.ndarray:
projected, _ = cv2.projectPoints(points_3d, rvec, tvec, K, D)
return projected.reshape(-1, 2)
# ------------------------------------------------------------------
# Optimization
# ------------------------------------------------------------------
def pack_parameters(robot_state: Dict[str, float], camera_params: List[Tuple[np.ndarray, np.ndarray]]) -> np.ndarray:
state_vec = np.asarray([robot_state[k] for k in STATE_KEYS], dtype=np.float64)
cams = []
for rvec, tvec in camera_params:
cams.append(rvec.reshape(3))
cams.append(tvec.reshape(3))
return np.concatenate([state_vec] + cams)
def unpack_parameters(params: np.ndarray, n_views: int) -> Tuple[Dict[str, float], List[Tuple[np.ndarray, np.ndarray]]]:
robot_state = {STATE_KEYS[i]: float(params[i]) for i in range(len(STATE_KEYS))}
camera_params = []
offset = len(STATE_KEYS)
for _ in range(n_views):
rvec = params[offset:offset + 3].reshape(3, 1)
tvec = params[offset + 3:offset + 6].reshape(3, 1)
camera_params.append((rvec, tvec))
offset += 6
return robot_state, camera_params
def residuals_for_parameters(
params: np.ndarray,
views: List[Dict[str, Any]],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
robot: Dict[str, Any],
scale: float,
default_state: Dict[str, float]
) -> np.ndarray:
robot_state, camera_params = unpack_parameters(params, len(views))
link_transforms = compute_link_transforms(robot, robot_state, scale)
residuals = []
for obs in observations:
marker = robot_markers[obs['marker_id']]
world_corners = compute_marker_world_corners(marker, link_transforms)
rvec, tvec = camera_params[obs['view_index']]
proj = project_points(world_corners, rvec, tvec, views[obs['view_index']]['K'], views[obs['view_index']]['D'])
diffs = proj - obs['image_points_px']
weight = math.sqrt(obs['confidence'])
residuals.extend((diffs * weight).reshape(-1))
for key in STATE_KEYS:
diff = robot_state[key] - default_state.get(key, 0.0)
if key in ('x', 'y', 'z', 'e'):
w = 0.001
else:
w = 0.01
residuals.append(diff * w)
return np.asarray(residuals, dtype=np.float64)
def estimate_uncertainty(result: Any, n_params: int) -> np.ndarray:
if result.jac is None:
return np.full(n_params, float('nan'), dtype=np.float64)
J = result.jac
m, n = J.shape
JTJ = J.T @ J
try:
cov = np.linalg.pinv(JTJ)
except np.linalg.LinAlgError:
cov = np.linalg.pinv(JTJ + np.eye(n) * 1e-9)
residuals = result.fun
dof = max(1, m - n)
sigma2 = float(np.sum(residuals ** 2) / dof)
cov *= sigma2
return np.sqrt(np.diag(cov))
# ------------------------------------------------------------------
# Output assembly
# ------------------------------------------------------------------
def camera_position_world(rvec: np.ndarray, tvec: np.ndarray) -> np.ndarray:
R, _ = cv2.Rodrigues(rvec)
return (-R.T @ tvec).reshape(3)
def build_output(
robot_state: Dict[str, float],
state_uncertainty: np.ndarray,
views: List[Dict[str, Any]],
camera_params: List[Tuple[np.ndarray, np.ndarray]],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
scale: float,
robot: Dict[str, Any],
robot_json_path: str
) -> Dict[str, Any]:
link_transforms = compute_link_transforms(robot, robot_state, scale)
marker_summary: Dict[int, Dict[str, Any]] = {}
for marker_id, marker in robot_markers.items():
marker_summary[marker_id] = {
'marker_id': marker_id,
'link_name': marker['link_name'],
'position_world_m': compute_marker_world_position(marker, link_transforms).tolist(),
'size_m': marker['size_m'],
'observation_count': 0,
'mean_confidence': None,
'mean_reprojection_error_px': None,
'observations': []
}
per_marker_errors: Dict[int, List[float]] = {mid: [] for mid in marker_summary}
per_marker_confidences: Dict[int, List[float]] = {mid: [] for mid in marker_summary}
link_transforms = compute_link_transforms(robot, robot_state, scale)
for obs in observations:
marker_id = obs['marker_id']
marker = robot_markers[marker_id]
object_points_m = compute_marker_world_corners(marker, link_transforms)
rvec, tvec = camera_params[obs['view_index']]
proj = project_points(object_points_m, rvec, tvec, views[obs['view_index']]['K'], views[obs['view_index']]['D'])
diffs = proj - obs['image_points_px']
errors = np.linalg.norm(diffs, axis=1)
repro_error = float(np.mean(errors))
per_marker_errors[marker_id].extend(errors.tolist())
per_marker_confidences[marker_id].append(obs['confidence'])
marker_summary[marker_id]['observation_count'] += 1
marker_summary[marker_id]['observations'].append({
'view_index': obs['view_index'],
'source_file': views[obs['view_index']]['source_file'],
'image_file': views[obs['view_index']]['image_file'],
'confidence': obs['confidence'],
'mean_reprojection_error_px': repro_error,
'corner_reprojection_errors_px': errors.tolist()
})
for marker_id, summary in marker_summary.items():
if summary['observation_count'] > 0:
summary['mean_confidence'] = float(np.mean(per_marker_confidences[marker_id]))
summary['mean_reprojection_error_px'] = float(np.mean(per_marker_errors[marker_id]))
camera_outputs = []
for idx, view in enumerate(views):
rvec, tvec = camera_params[idx]
cam_pos = camera_position_world(rvec, tvec)
observed_count = sum(1 for obs in observations if obs['view_index'] == idx)
camera_outputs.append({
'view_index': idx,
'source_file': view['source_file'],
'camera_id': view['camera_id'],
'camera_position_world_m': cam_pos.tolist(),
'rvec': rvec.reshape(-1).tolist(),
'tvec': tvec.reshape(-1).tolist(),
'intrinsics': {
'camera_matrix': view['K'].tolist(),
'distortion_coefficients': view['D'].reshape(-1).tolist()
},
'observation_count': observed_count
})
robot_pose_output = {
'state': {k: float(robot_state[k]) for k in STATE_KEYS},
'uncertainty': {
'x_mm': float(state_uncertainty[0]),
'y_mm': float(state_uncertainty[1]),
'z_mm': float(state_uncertainty[2]),
'a_deg': float(state_uncertainty[3]),
'b_deg': float(state_uncertainty[4]),
'c_deg': float(state_uncertainty[5]),
'e_mm': float(state_uncertainty[6])
},
'confidence': {
'x': float(math.exp(-state_uncertainty[0] / 10.0)),
'y': float(math.exp(-state_uncertainty[1] / 10.0)),
'z': float(math.exp(-state_uncertainty[2] / 10.0)),
'a': float(math.exp(-state_uncertainty[3] / 10.0)),
'b': float(math.exp(-state_uncertainty[4] / 10.0)),
'c': float(math.exp(-state_uncertainty[5] / 10.0)),
'e': float(math.exp(-state_uncertainty[6] / max(1.0, state_uncertainty[6])))
}
}
return {
'schema_version': '1.0',
'created_utc': datetime.datetime.utcnow().isoformat() + 'Z',
'source_robot_json': os.path.abspath(robot_json_path),
'source_detections': [view['source_file'] for view in views],
'robot_pose': robot_pose_output,
'camera_poses': camera_outputs,
'marker_positions': list(marker_summary.values())
}
# ------------------------------------------------------------------
# Main
# ------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description='Multiview optimization of robot pose and camera extrinsics')
parser.add_argument('--robot', required=True, help='Path to robot.json')
parser.add_argument('--detections', required=True, nargs='+', help='List of detection JSON files')
parser.add_argument('--outDir', required=True, help='Output directory')
parser.add_argument('--write-summary', action='store_true', help='Write summary file')
parser.add_argument('--max-iter', type=int, default=500, help='Maximum optimizer iterations')
args = parser.parse_args()
os.makedirs(args.outDir, exist_ok=True)
robot_json_path = os.path.abspath(args.robot)
robot = load_json(robot_json_path)
scale = parse_metric_scale(robot)
default_state = {
k: float(robot.get('defaultPosition', {}).get(k, 0.0) or 0.0)
for k in STATE_KEYS
}
robot_markers = extract_markers(robot, scale)
views, observations = collect_views_and_observations(args.detections, robot_markers)
camera_guesses = []
for view in views:
rvec, tvec = initial_camera_guess(view, observations, robot_markers, default_state, scale, robot)
camera_guesses.append((rvec, tvec))
x0 = pack_parameters(default_state, camera_guesses)
progress = {
'iter': 0,
'last_cost': None,
'last_print': time.time(),
'prev_x': x0.copy()
}
def progress_callback(xk: np.ndarray) -> None:
progress['iter'] += 1
now = time.time()
if progress['iter'] == 1 or now - progress['last_print'] >= 1.0:
res = residuals_for_parameters(xk, views, observations, robot_markers, robot, scale, default_state)
cost = 0.5 * float(np.dot(res, res))
delta_cost = None
convergence = ''
if progress['last_cost'] is not None:
delta_cost = cost - progress['last_cost']
if abs(delta_cost) < 1e-3:
convergence = ' stable'
elif delta_cost < 0:
convergence = ' improving'
else:
convergence = ' worsening'
step_norm = float(np.linalg.norm(xk - progress['prev_x']))
print(
f'[Multiview] iter={progress["iter"]:4d} cost={cost:.4f}'
+ (f' delta={delta_cost:.4g}' if delta_cost is not None else '')
+ f' step={step_norm:.4g}'
+ convergence
)
progress['last_cost'] = cost
progress['last_print'] = now
progress['prev_x'] = xk.copy()
result = least_squares(
residuals_for_parameters,
x0,
args=(views, observations, robot_markers, robot, scale, default_state),
jac='2-point',
method='trf',
loss='soft_l1',
f_scale=1.0,
max_nfev=args.max_iter,
callback=progress_callback
)
robot_state, camera_params = unpack_parameters(result.x, len(views))
uncertainties = estimate_uncertainty(result, len(result.x))
output = build_output(robot_state, uncertainties[:len(STATE_KEYS)], views, camera_params, observations, robot_markers, scale, robot, robot_json_path)
out_path = Path(args.outDir) / 'multiview_pose.json'
save_json(output, out_path)
print(f'Saved: {out_path}')
if args.write_summary:
summary_path = Path(args.outDir) / 'multiview_pose_summary.json'
summary = {
'final_cost': float(result.cost),
'status': int(result.status),
'message': result.message,
'robot_state': output['robot_pose'],
'camera_count': len(views),
'marker_count': len(robot_markers)
}
save_json(summary, summary_path)
print(f'Saved: {summary_path}')
if __name__ == '__main__':
main()

View File

@@ -1,706 +0,0 @@
#!/usr/bin/env python3
"""
============================================================
STEP 2b — Simultane Multiview-Optimierung für Roboterpose
============================================================
Ziel:
Aus mehreren ArUco-Detektionsdateien die gemeinsame
Roboterpose (x,y,z,a,b,c,e) schätzen und jede Kamera-Pose
sowie Marker-Weltpositionen ausgeben.
Eingabe:
--robot ../robot.json
--detections render_1a_aruco_detection.json render_1b_aruco_detection.json ...
--outDir .
Ausgabe:
multiview_pose.json
Hinweis:
Dieses Skript verwendet die Markerpositionen aus robot.json
als kinematische Constraints und optimiert gleichzeitig:
- Roboterzustand (x,y,z,a,b,c,e)
- Kameraextrinsische Parameter pro Bild
"""
import argparse
import datetime
import json
import math
import os
import time
from pathlib import Path
from typing import Any, Dict, List, Tuple
import cv2
import numpy as np
from scipy.optimize import least_squares
STATE_KEYS = ["x", "y", "z", "a", "b", "c", "e"]
# ------------------------------------------------------------------
# JSON helpers
# ------------------------------------------------------------------
def load_json(path: str) -> Dict[str, Any]:
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def save_json(data: Dict[str, Any], path: Path) -> None:
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
# ------------------------------------------------------------------
# robot.json helpers
# ------------------------------------------------------------------
def resolve_scalar(value: Any, default: float = 0.0) -> float:
if value is None:
return default
if isinstance(value, (int, float)):
return float(value)
try:
return float(str(value).strip())
except ValueError:
return default
def resolve_vector(value: Any, default_len: int = 3) -> Tuple[float, ...]:
if value is None:
return tuple(0.0 for _ in range(default_len))
if isinstance(value, (int, float, str)):
return (resolve_scalar(value),) + tuple(0.0 for _ in range(default_len - 1))
if isinstance(value, (list, tuple)):
resolved = [resolve_scalar(v) for v in value]
if len(resolved) < default_len:
resolved.extend([0.0] * (default_len - len(resolved)))
return tuple(resolved[:default_len])
return tuple(0.0 for _ in range(default_len))
def parse_metric_scale(robot: Dict[str, Any]) -> float:
rendering_info = robot.get('renderingInfo', {}) or {}
metric = rendering_info.get('metric', 'mm')
return 0.001 if str(metric).strip().lower() == 'mm' else 1.0
def normalize_axis(axis: Any) -> np.ndarray:
vec = np.asarray(axis, dtype=np.float64)
if vec.shape != (3,):
vec = vec.reshape(-1)[:3]
norm = np.linalg.norm(vec)
return vec / max(norm, 1e-9)
def euler_deg_to_matrix(euler_deg: Any) -> np.ndarray:
x_deg, y_deg, z_deg = resolve_vector(euler_deg, 3)
x = math.radians(x_deg)
y = math.radians(y_deg)
z = math.radians(z_deg)
cx = math.cos(x)
sx = math.sin(x)
cy = math.cos(y)
sy = math.sin(y)
cz = math.cos(z)
sz = math.sin(z)
Rx = np.array([
[1.0, 0.0, 0.0],
[0.0, cx, -sx],
[0.0, sx, cx]
], dtype=np.float64)
Ry = np.array([
[cy, 0.0, sy],
[0.0, 1.0, 0.0],
[-sy, 0.0, cy]
], dtype=np.float64)
Rz = np.array([
[cz, -sz, 0.0],
[sz, cz, 0.0],
[0.0, 0.0, 1.0]
], dtype=np.float64)
return Rz @ Ry @ Rx
def transform_from_translation_rotation(translation: Any, rotation_deg: Any) -> np.ndarray:
T = np.eye(4, dtype=np.float64)
pos = np.asarray(resolve_vector(translation, 3), dtype=np.float64)
T[:3, 3] = pos
T[:3, :3] = euler_deg_to_matrix(rotation_deg)
return T
def axis_angle_matrix(axis: Any, angle_deg: float) -> np.ndarray:
axis_vec = normalize_axis(axis)
theta = math.radians(angle_deg)
kx, ky, kz = axis_vec
c = math.cos(theta)
s = math.sin(theta)
v = 1.0 - c
R = np.array([
[kx * kx * v + c, kx * ky * v - kz * s, kx * kz * v + ky * s],
[ky * kx * v + kz * s, ky * ky * v + c, ky * kz * v - kx * s],
[kz * kx * v - ky * s, kz * ky * v + kx * s, kz * kz * v + c]
], dtype=np.float64)
T = np.eye(4, dtype=np.float64)
T[:3, :3] = R
return T
# ------------------------------------------------------------------
# Kinematics and marker extraction
# ------------------------------------------------------------------
def extract_markers(robot: Dict[str, Any], scale: float) -> Dict[int, Dict[str, Any]]:
markers = {}
links = robot.get('links', {}) or {}
marker_defaults = (robot.get('renderingInfo', {}) or {}).get('markerDefaults', {}) or {}
default_size_mm = float(marker_defaults.get('size', 25.0))
for link_name, link_info in links.items():
for marker in link_info.get('markers', []) or []:
marker_id = int(marker.get('id', -1))
if marker_id < 0:
continue
pos = resolve_vector(marker.get('position', [0, 0, 0]), 3)
size_mm = float(marker.get('size', default_size_mm))
markers[marker_id] = {
'marker_id': marker_id,
'link_name': link_name,
'position_m': np.asarray([pos[0] * scale, pos[1] * scale, pos[2] * scale], dtype=np.float64),
'normal': normalize_axis(resolve_vector(marker.get('normal', [0, 0, 1]), 3)),
'spin_deg': float(marker.get('spin', 0.0)),
'size_m': size_mm * scale,
}
return markers
def marker_plane_axes(normal: np.ndarray, spin_deg: float) -> Tuple[np.ndarray, np.ndarray]:
n = normalize_axis(normal)
candidate = np.array((0.0, 0.0, 1.0), dtype=np.float64)
if abs(np.dot(n, candidate)) > 0.99:
candidate = np.array((1.0, 0.0, 0.0), dtype=np.float64)
x_dir = np.cross(candidate, n)
x_dir /= max(np.linalg.norm(x_dir), 1e-9)
y_dir = np.cross(n, x_dir)
if abs(spin_deg) > 1e-6:
theta = math.radians(spin_deg)
cos_t = math.cos(theta)
sin_t = math.sin(theta)
x_rot = x_dir * cos_t + np.cross(n, x_dir) * sin_t + n * np.dot(n, x_dir) * (1.0 - cos_t)
y_rot = y_dir * cos_t + np.cross(n, y_dir) * sin_t + n * np.dot(n, y_dir) * (1.0 - cos_t)
return x_rot, y_rot
return x_dir, y_dir
def marker_object_corners(marker: Dict[str, Any]) -> np.ndarray:
half = marker['size_m'] * 0.5
x_dir, y_dir = marker_plane_axes(marker['normal'], marker['spin_deg'])
corners = np.stack([
-x_dir * half + y_dir * half,
x_dir * half + y_dir * half,
x_dir * half - y_dir * half,
-x_dir * half - y_dir * half
], axis=0)
return marker['position_m'].reshape(1, 3) + corners
def build_link_chain(robot: Dict[str, Any]) -> List[str]:
links = robot.get('links', {}) or {}
ordered: List[str] = []
remaining = set(links.keys())
while remaining:
progress = False
for name in list(remaining):
parent = links[name].get('parent')
if not parent or parent in ordered:
ordered.append(name)
remaining.remove(name)
progress = True
if not progress:
raise RuntimeError('Cycle detected in robot link tree or missing parent link')
return ordered
def compute_link_transforms(robot: Dict[str, Any], state: Dict[str, float], scale: float) -> Dict[str, np.ndarray]:
links = robot.get('links', {}) or {}
ordered_links = build_link_chain(robot)
transforms: Dict[str, np.ndarray] = {}
for link_name in ordered_links:
link_info = links[link_name] or {}
parent_name = link_info.get('parent')
parent_transform = transforms[parent_name] if parent_name else np.eye(4, dtype=np.float64)
mount_translation = np.asarray(resolve_vector(link_info.get('mountPosition', [0, 0, 0]), 3), dtype=np.float64) * scale
mount = transform_from_translation_rotation(
mount_translation,
link_info.get('mountRotation', [0, 0, 0])
)
joint_info = link_info.get('jointToParent', {}) or {}
joint_origin = np.asarray(resolve_vector(joint_info.get('origin', [0, 0, 0]), 3), dtype=np.float64) * scale
joint = transform_from_translation_rotation(
joint_origin,
joint_info.get('rotation', [0, 0, 0])
)
motion = np.eye(4, dtype=np.float64)
joint_type = str(joint_info.get('type', 'fixed')).strip().lower()
control_var = str(joint_info.get('variable', joint_info.get('control', ''))).strip().lower()
axis = resolve_vector(joint_info.get('axis', [1, 0, 0]), 3)
if joint_type == 'linear':
motion[:3, 3] = normalize_axis(axis) * state.get(control_var, 0.0) * scale
elif joint_type == 'revolute':
motion = axis_angle_matrix(axis, state.get(control_var, 0.0))
transforms[link_name] = parent_transform @ mount @ joint @ motion
return transforms
def compute_marker_world_position(marker: Dict[str, Any], link_transforms: Dict[str, np.ndarray]) -> np.ndarray:
link_transform = link_transforms[marker['link_name']]
local = np.ones(4, dtype=np.float64)
local[:3] = marker['position_m']
world = link_transform @ local
return world[:3]
# ------------------------------------------------------------------
# Camera / observation helpers
# ------------------------------------------------------------------
def load_intrinsics(detection_json: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
cam = detection_json['camera']
K = np.asarray(cam['camera_matrix'], dtype=np.float64)
D = np.asarray(cam.get('distortion_coefficients', [0, 0, 0, 0, 0]), dtype=np.float64).reshape(-1, 1)
return K, D
def collect_views_and_observations(
detection_files: List[str],
robot_markers: Dict[int, Dict[str, Any]]
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
views: List[Dict[str, Any]] = []
observations: List[Dict[str, Any]] = []
for idx, det_path in enumerate(detection_files):
detection_json = load_json(det_path)
K, D = load_intrinsics(detection_json)
views.append({
'index': idx,
'source_file': os.path.abspath(det_path),
'camera_id': detection_json.get('camera', {}).get('camera_id', f'cam{idx+1}'),
'image_file': detection_json.get('image', {}).get('image_file'),
'K': K,
'D': D
})
for det in detection_json.get('detections', []) or []:
marker_id = int(det.get('marker_id', -1))
if marker_id < 0 or marker_id not in robot_markers:
continue
image_points = det.get('image_points_px')
if isinstance(image_points, list) and len(image_points) == 4:
image_points = np.asarray(image_points, dtype=np.float64)
else:
center = resolve_vector(det.get('center_px', [0, 0]), 2)
image_points = np.asarray([center], dtype=np.float64)
confidence = float(det.get('confidence', 1.0))
marker = robot_markers[marker_id]
observations.append({
'view_index': idx,
'marker_id': marker_id,
'marker_link_corners': marker_object_corners(marker),
'image_points_px': image_points,
'confidence': max(0.01, min(1.0, confidence))
})
if len(views) == 0:
raise RuntimeError('No valid detection views found')
if len(observations) == 0:
raise RuntimeError('No marker observations matched robot.json markers')
return views, observations
def compute_marker_world_corners(marker: Dict[str, Any], link_transforms: Dict[str, np.ndarray]) -> np.ndarray:
link_transform = link_transforms[marker['link_name']]
local = marker_object_corners(marker)
homogeneous = np.concatenate([local, np.ones((local.shape[0], 1), dtype=np.float64)], axis=1)
world = (link_transform @ homogeneous.T).T
return world[:, :3]
def initial_camera_guess(
view: Dict[str, Any],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
default_state: Dict[str, float],
scale: float,
robot: Dict[str, Any]
) -> Tuple[np.ndarray, np.ndarray]:
object_points = []
image_points = []
link_transforms = compute_link_transforms(robot, default_state, scale)
for obs in observations:
if obs['view_index'] != view['index']:
continue
marker = robot_markers[obs['marker_id']]
object_points.append(compute_marker_world_corners(marker, link_transforms))
image_points.append(obs['image_points_px'])
if len(object_points) == 0:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
object_points = np.vstack(object_points)
image_points = np.vstack(image_points)
if object_points.shape[0] < 4:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
success, rvec, tvec = cv2.solvePnP(
object_points,
image_points,
view['K'],
view['D'],
flags=cv2.SOLVEPNP_ITERATIVE
)
if not success:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
return rvec, tvec
def project_points(
points_3d: np.ndarray,
rvec: np.ndarray,
tvec: np.ndarray,
K: np.ndarray,
D: np.ndarray
) -> np.ndarray:
projected, _ = cv2.projectPoints(points_3d, rvec, tvec, K, D)
return projected.reshape(-1, 2)
# ------------------------------------------------------------------
# Optimization
# ------------------------------------------------------------------
def pack_parameters(robot_state: Dict[str, float], camera_params: List[Tuple[np.ndarray, np.ndarray]]) -> np.ndarray:
state_vec = np.asarray([robot_state[k] for k in STATE_KEYS], dtype=np.float64)
cams = []
for rvec, tvec in camera_params:
cams.append(rvec.reshape(3))
cams.append(tvec.reshape(3))
return np.concatenate([state_vec] + cams)
def unpack_parameters(params: np.ndarray, n_views: int) -> Tuple[Dict[str, float], List[Tuple[np.ndarray, np.ndarray]]]:
robot_state = {STATE_KEYS[i]: float(params[i]) for i in range(len(STATE_KEYS))}
camera_params = []
offset = len(STATE_KEYS)
for _ in range(n_views):
rvec = params[offset:offset + 3].reshape(3, 1)
tvec = params[offset + 3:offset + 6].reshape(3, 1)
camera_params.append((rvec, tvec))
offset += 6
return robot_state, camera_params
def residuals_for_parameters(
params: np.ndarray,
views: List[Dict[str, Any]],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
robot: Dict[str, Any],
scale: float,
default_state: Dict[str, float]
) -> np.ndarray:
robot_state, camera_params = unpack_parameters(params, len(views))
link_transforms = compute_link_transforms(robot, robot_state, scale)
residuals = []
for obs in observations:
marker = robot_markers[obs['marker_id']]
world_corners = compute_marker_world_corners(marker, link_transforms)
rvec, tvec = camera_params[obs['view_index']]
proj = project_points(world_corners, rvec, tvec, views[obs['view_index']]['K'], views[obs['view_index']]['D'])
diffs = proj - obs['image_points_px']
weight = math.sqrt(obs['confidence'])
residuals.extend((diffs * weight).reshape(-1))
for key in STATE_KEYS:
diff = robot_state[key] - default_state.get(key, 0.0)
if key in ('x', 'y', 'z', 'e'):
w = 0.001
else:
w = 0.01
residuals.append(diff * w)
return np.asarray(residuals, dtype=np.float64)
def estimate_uncertainty(result: Any, n_params: int) -> np.ndarray:
if result.jac is None:
return np.full(n_params, float('nan'), dtype=np.float64)
J = result.jac
m, n = J.shape
JTJ = J.T @ J
try:
cov = np.linalg.pinv(JTJ)
except np.linalg.LinAlgError:
cov = np.linalg.pinv(JTJ + np.eye(n) * 1e-9)
residuals = result.fun
dof = max(1, m - n)
sigma2 = float(np.sum(residuals ** 2) / dof)
cov *= sigma2
return np.sqrt(np.diag(cov))
# ------------------------------------------------------------------
# Output assembly
# ------------------------------------------------------------------
def camera_position_world(rvec: np.ndarray, tvec: np.ndarray) -> np.ndarray:
R, _ = cv2.Rodrigues(rvec)
return (-R.T @ tvec).reshape(3)
def build_output(
robot_state: Dict[str, float],
state_uncertainty: np.ndarray,
views: List[Dict[str, Any]],
camera_params: List[Tuple[np.ndarray, np.ndarray]],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
scale: float,
robot: Dict[str, Any],
robot_json_path: str
) -> Dict[str, Any]:
link_transforms = compute_link_transforms(robot, robot_state, scale)
marker_summary: Dict[int, Dict[str, Any]] = {}
for marker_id, marker in robot_markers.items():
marker_summary[marker_id] = {
'marker_id': marker_id,
'link_name': marker['link_name'],
'position_world_m': compute_marker_world_position(marker, link_transforms).tolist(),
'size_m': marker['size_m'],
'observation_count': 0,
'mean_confidence': None,
'mean_reprojection_error_px': None,
'observations': []
}
per_marker_errors: Dict[int, List[float]] = {mid: [] for mid in marker_summary}
per_marker_confidences: Dict[int, List[float]] = {mid: [] for mid in marker_summary}
link_transforms = compute_link_transforms(robot, robot_state, scale)
for obs in observations:
marker_id = obs['marker_id']
marker = robot_markers[marker_id]
object_points_m = compute_marker_world_corners(marker, link_transforms)
rvec, tvec = camera_params[obs['view_index']]
proj = project_points(object_points_m, rvec, tvec, views[obs['view_index']]['K'], views[obs['view_index']]['D'])
diffs = proj - obs['image_points_px']
errors = np.linalg.norm(diffs, axis=1)
repro_error = float(np.mean(errors))
per_marker_errors[marker_id].extend(errors.tolist())
per_marker_confidences[marker_id].append(obs['confidence'])
marker_summary[marker_id]['observation_count'] += 1
marker_summary[marker_id]['observations'].append({
'view_index': obs['view_index'],
'source_file': views[obs['view_index']]['source_file'],
'image_file': views[obs['view_index']]['image_file'],
'confidence': obs['confidence'],
'mean_reprojection_error_px': repro_error,
'corner_reprojection_errors_px': errors.tolist()
})
for marker_id, summary in marker_summary.items():
if summary['observation_count'] > 0:
summary['mean_confidence'] = float(np.mean(per_marker_confidences[marker_id]))
summary['mean_reprojection_error_px'] = float(np.mean(per_marker_errors[marker_id]))
camera_outputs = []
for idx, view in enumerate(views):
rvec, tvec = camera_params[idx]
cam_pos = camera_position_world(rvec, tvec)
observed_count = sum(1 for obs in observations if obs['view_index'] == idx)
camera_outputs.append({
'view_index': idx,
'source_file': view['source_file'],
'camera_id': view['camera_id'],
'camera_position_world_m': cam_pos.tolist(),
'rvec': rvec.reshape(-1).tolist(),
'tvec': tvec.reshape(-1).tolist(),
'intrinsics': {
'camera_matrix': view['K'].tolist(),
'distortion_coefficients': view['D'].reshape(-1).tolist()
},
'observation_count': observed_count
})
robot_pose_output = {
'state': {k: float(robot_state[k]) for k in STATE_KEYS},
'uncertainty': {
'x_mm': float(state_uncertainty[0]),
'y_mm': float(state_uncertainty[1]),
'z_mm': float(state_uncertainty[2]),
'a_deg': float(state_uncertainty[3]),
'b_deg': float(state_uncertainty[4]),
'c_deg': float(state_uncertainty[5]),
'e_mm': float(state_uncertainty[6])
},
'confidence': {
'x': float(math.exp(-state_uncertainty[0] / 10.0)),
'y': float(math.exp(-state_uncertainty[1] / 10.0)),
'z': float(math.exp(-state_uncertainty[2] / 10.0)),
'a': float(math.exp(-state_uncertainty[3] / 10.0)),
'b': float(math.exp(-state_uncertainty[4] / 10.0)),
'c': float(math.exp(-state_uncertainty[5] / 10.0)),
'e': float(math.exp(-state_uncertainty[6] / max(1.0, state_uncertainty[6])))
}
}
return {
'schema_version': '1.0',
'created_utc': datetime.datetime.utcnow().isoformat() + 'Z',
'source_robot_json': os.path.abspath(robot_json_path),
'source_detections': [view['source_file'] for view in views],
'robot_pose': robot_pose_output,
'camera_poses': camera_outputs,
'marker_positions': list(marker_summary.values())
}
# ------------------------------------------------------------------
# Main
# ------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description='Multiview optimization of robot pose and camera extrinsics')
parser.add_argument('--robot', required=True, help='Path to robot.json')
parser.add_argument('--detections', required=True, nargs='+', help='List of detection JSON files')
parser.add_argument('--outDir', required=True, help='Output directory')
parser.add_argument('--write-summary', action='store_true', help='Write summary file')
parser.add_argument('--max-iter', type=int, default=500, help='Maximum optimizer iterations')
args = parser.parse_args()
os.makedirs(args.outDir, exist_ok=True)
robot_json_path = os.path.abspath(args.robot)
robot = load_json(robot_json_path)
scale = parse_metric_scale(robot)
default_state = {
k: float(robot.get('defaultPosition', {}).get(k, 0.0) or 0.0)
for k in STATE_KEYS
}
robot_markers = extract_markers(robot, scale)
views, observations = collect_views_and_observations(args.detections, robot_markers)
camera_guesses = []
for view in views:
rvec, tvec = initial_camera_guess(view, observations, robot_markers, default_state, scale, robot)
camera_guesses.append((rvec, tvec))
x0 = pack_parameters(default_state, camera_guesses)
progress = {
'iter': 0,
'last_cost': None,
'last_print': time.time(),
'prev_x': x0.copy()
}
def progress_callback(xk: np.ndarray) -> None:
progress['iter'] += 1
now = time.time()
if progress['iter'] == 1 or now - progress['last_print'] >= 1.0:
res = residuals_for_parameters(xk, views, observations, robot_markers, robot, scale, default_state)
cost = 0.5 * float(np.dot(res, res))
delta_cost = None
convergence = ''
if progress['last_cost'] is not None:
delta_cost = cost - progress['last_cost']
if abs(delta_cost) < 1e-3:
convergence = ' stable'
elif delta_cost < 0:
convergence = ' improving'
else:
convergence = ' worsening'
step_norm = float(np.linalg.norm(xk - progress['prev_x']))
print(
f'[Multiview] iter={progress["iter"]:4d} cost={cost:.4f}'
+ (f' delta={delta_cost:.4g}' if delta_cost is not None else '')
+ f' step={step_norm:.4g}'
+ convergence
)
progress['last_cost'] = cost
progress['last_print'] = now
progress['prev_x'] = xk.copy()
result = least_squares(
residuals_for_parameters,
x0,
args=(views, observations, robot_markers, robot, scale, default_state),
jac='2-point',
method='trf',
loss='soft_l1',
f_scale=1.0,
max_nfev=args.max_iter,
callback=progress_callback
)
robot_state, camera_params = unpack_parameters(result.x, len(views))
uncertainties = estimate_uncertainty(result, len(result.x))
output = build_output(robot_state, uncertainties[:len(STATE_KEYS)], views, camera_params, observations, robot_markers, scale, robot, robot_json_path)
out_path = Path(args.outDir) / 'multiview_pose.json'
save_json(output, out_path)
print(f'Saved: {out_path}')
if args.write_summary:
summary_path = Path(args.outDir) / 'multiview_pose_summary.json'
summary = {
'final_cost': float(result.cost),
'status': int(result.status),
'message': result.message,
'robot_state': output['robot_pose'],
'camera_count': len(views),
'marker_count': len(robot_markers)
}
save_json(summary, summary_path)
print(f'Saved: {summary_path}')
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,971 +0,0 @@
#!/usr/bin/env python3
"""
============================================================
STEP 2b — Simultane Multiview-Optimierung für Roboterpose
============================================================
Ziel:
Aus mehreren ArUco-Detektionsdateien die gemeinsame
Roboterpose (x,y,z,a,b,c,e) schätzen und jede Kamera-Pose
sowie Marker-Weltpositionen ausgeben.
Eingabe:
--robot ../robot.json
--detections render_1a_aruco_detection.json render_1b_aruco_detection.json ...
--outDir .
Ausgabe:
multiview_pose.json
Hinweis:
Dieses Skript verwendet die Markerpositionen aus robot.json
als kinematische Constraints und optimiert gleichzeitig:
- Roboterzustand (x,y,z,a,b,c,e)
- Kameraextrinsische Parameter pro Bild
"""
import argparse
import datetime
import json
import math
import os
import time
from pathlib import Path
from typing import Any, Dict, List, Tuple
import cv2
import numpy as np
from scipy.optimize import least_squares
STATE_KEYS = ["x", "y", "z", "a", "b", "c", "e"]
# ------------------------------------------------------------------
# Constraint definitions and validation
# ------------------------------------------------------------------
class ConstraintResult:
"""Result of validating/applying a single constraint"""
def __init__(self, name: str, enabled: bool, reason: str = ""):
self.name = name
self.enabled = enabled
self.reason = reason
self.residuals = []
def __str__(self) -> str:
status = "✓ ENABLED" if self.enabled else "✗ DISABLED"
return f"{self.name:40s} {status:12s} {self.reason}"
def validate_constraints(robot: Dict[str, Any], robot_markers: Dict[int, Dict[str, Any]]) -> Dict[str, ConstraintResult]:
"""
Validate which constraints can be applied based on robot geometry.
Returns a dict of constraint_name -> ConstraintResult
"""
results = {}
# --- Constraint 1: Rigid body distances within each link ---
rigid_body_result = ConstraintResult("RigidBodyDistances", False)
try:
rigid_body_count = 0
for link_name in ['Arm1', 'Ellbow', 'Arm2']:
link_markers = [m for m in robot_markers.values() if m['link_name'] == link_name]
if len(link_markers) >= 2:
rigid_body_count += 1
if rigid_body_count >= 2:
rigid_body_result.enabled = True
rigid_body_result.reason = f"Found {rigid_body_count} links with 2+ markers each"
else:
rigid_body_result.reason = "Not enough rigid links with multiple markers"
except Exception as e:
rigid_body_result.reason = f"Error: {str(e)}"
results['RigidBodyDistances'] = rigid_body_result
# --- Constraint 2: Fixed X-distances between links (rotation around X-axis) ---
inter_link_x_result = ConstraintResult("InterLinkXDistances", False)
try:
links_with_markers = set(m['link_name'] for m in robot_markers.values())
x_rotated_links = []
for link_name in ['Arm1', 'Ellbow']:
if link_name in links_with_markers:
link_markers = [m for m in robot_markers.values() if m['link_name'] == link_name]
if len(link_markers) >= 1:
x_rotated_links.append(link_name)
if len(x_rotated_links) >= 2:
inter_link_x_result.enabled = True
inter_link_x_result.reason = f"Found {len(x_rotated_links)} X-rotation links: {', '.join(x_rotated_links)}"
else:
inter_link_x_result.reason = "Not enough X-rotation links"
except Exception as e:
inter_link_x_result.reason = f"Error: {str(e)}"
results['InterLinkXDistances'] = inter_link_x_result
# --- Sanity check (not a hard constraint): Arm2 sin(a) dependency ---
arm2_sina_result = ConstraintResult("Arm2SinADependency", True, "Sanity check only (not enforced)")
try:
arm2_markers = [m for m in robot_markers.values() if m['link_name'] == 'Arm2']
if len(arm2_markers) >= 2:
z_values = set(m['position_m'][2] for m in arm2_markers)
if len(z_values) > 1:
arm2_sina_result.enabled = True
arm2_sina_result.reason = "Multiple Z-values detected; sin(a) dependency confirmed"
else:
arm2_sina_result.enabled = False
arm2_sina_result.reason = "No Z-variation in Arm2 markers (cannot use sin(a) constraint)"
else:
arm2_sina_result.enabled = False
arm2_sina_result.reason = "Not enough Arm2 markers"
except Exception as e:
arm2_sina_result.reason = f"Error: {str(e)}"
results['Arm2SinADependency'] = arm2_sina_result
return results
# ------------------------------------------------------------------
# JSON helpers
# ------------------------------------------------------------------
def load_json(path: str) -> Dict[str, Any]:
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def save_json(data: Dict[str, Any], path: Path) -> None:
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
# ------------------------------------------------------------------
# robot.json helpers
# ------------------------------------------------------------------
def resolve_scalar(value: Any, default: float = 0.0) -> float:
if value is None:
return default
if isinstance(value, (int, float)):
return float(value)
try:
return float(str(value).strip())
except ValueError:
return default
def resolve_vector(value: Any, default_len: int = 3) -> Tuple[float, ...]:
if value is None:
return tuple(0.0 for _ in range(default_len))
if isinstance(value, (int, float, str)):
return (resolve_scalar(value),) + tuple(0.0 for _ in range(default_len - 1))
if isinstance(value, (list, tuple)):
resolved = [resolve_scalar(v) for v in value]
if len(resolved) < default_len:
resolved.extend([0.0] * (default_len - len(resolved)))
return tuple(resolved[:default_len])
return tuple(0.0 for _ in range(default_len))
def parse_metric_scale(robot: Dict[str, Any]) -> float:
rendering_info = robot.get('renderingInfo', {}) or {}
metric = rendering_info.get('metric', 'mm')
return 0.001 if str(metric).strip().lower() == 'mm' else 1.0
def normalize_axis(axis: Any) -> np.ndarray:
vec = np.asarray(axis, dtype=np.float64)
if vec.shape != (3,):
vec = vec.reshape(-1)[:3]
norm = np.linalg.norm(vec)
return vec / max(norm, 1e-9)
def euler_deg_to_matrix(euler_deg: Any) -> np.ndarray:
x_deg, y_deg, z_deg = resolve_vector(euler_deg, 3)
x = math.radians(x_deg)
y = math.radians(y_deg)
z = math.radians(z_deg)
cx = math.cos(x)
sx = math.sin(x)
cy = math.cos(y)
sy = math.sin(y)
cz = math.cos(z)
sz = math.sin(z)
Rx = np.array([
[1.0, 0.0, 0.0],
[0.0, cx, -sx],
[0.0, sx, cx]
], dtype=np.float64)
Ry = np.array([
[cy, 0.0, sy],
[0.0, 1.0, 0.0],
[-sy, 0.0, cy]
], dtype=np.float64)
Rz = np.array([
[cz, -sz, 0.0],
[sz, cz, 0.0],
[0.0, 0.0, 1.0]
], dtype=np.float64)
return Rz @ Ry @ Rx
def transform_from_translation_rotation(translation: Any, rotation_deg: Any) -> np.ndarray:
T = np.eye(4, dtype=np.float64)
pos = np.asarray(resolve_vector(translation, 3), dtype=np.float64)
T[:3, 3] = pos
T[:3, :3] = euler_deg_to_matrix(rotation_deg)
return T
def axis_angle_matrix(axis: Any, angle_deg: float) -> np.ndarray:
axis_vec = normalize_axis(axis)
theta = math.radians(angle_deg)
kx, ky, kz = axis_vec
c = math.cos(theta)
s = math.sin(theta)
v = 1.0 - c
R = np.array([
[kx * kx * v + c, kx * ky * v - kz * s, kx * kz * v + ky * s],
[ky * kx * v + kz * s, ky * ky * v + c, ky * kz * v - kx * s],
[kz * kx * v - ky * s, kz * ky * v + kx * s, kz * kz * v + c]
], dtype=np.float64)
T = np.eye(4, dtype=np.float64)
T[:3, :3] = R
return T
# ------------------------------------------------------------------
# Kinematics and marker extraction
# ------------------------------------------------------------------
def extract_markers(robot: Dict[str, Any], scale: float) -> Dict[int, Dict[str, Any]]:
markers = {}
links = robot.get('links', {}) or {}
marker_defaults = (robot.get('renderingInfo', {}) or {}).get('markerDefaults', {}) or {}
default_size_mm = float(marker_defaults.get('size', 25.0))
for link_name, link_info in links.items():
for marker in link_info.get('markers', []) or []:
marker_id = int(marker.get('id', -1))
if marker_id < 0:
continue
pos = resolve_vector(marker.get('position', [0, 0, 0]), 3)
size_mm = float(marker.get('size', default_size_mm))
markers[marker_id] = {
'marker_id': marker_id,
'link_name': link_name,
'position_m': np.asarray([pos[0] * scale, pos[1] * scale, pos[2] * scale], dtype=np.float64),
'normal': normalize_axis(resolve_vector(marker.get('normal', [0, 0, 1]), 3)),
'spin_deg': float(marker.get('spin', 0.0)),
'size_m': size_mm * scale,
}
return markers
def marker_plane_axes(normal: np.ndarray, spin_deg: float) -> Tuple[np.ndarray, np.ndarray]:
n = normalize_axis(normal)
candidate = np.array((0.0, 0.0, 1.0), dtype=np.float64)
if abs(np.dot(n, candidate)) > 0.99:
candidate = np.array((1.0, 0.0, 0.0), dtype=np.float64)
x_dir = np.cross(candidate, n)
x_dir /= max(np.linalg.norm(x_dir), 1e-9)
y_dir = np.cross(n, x_dir)
if abs(spin_deg) > 1e-6:
theta = math.radians(spin_deg)
cos_t = math.cos(theta)
sin_t = math.sin(theta)
x_rot = x_dir * cos_t + np.cross(n, x_dir) * sin_t + n * np.dot(n, x_dir) * (1.0 - cos_t)
y_rot = y_dir * cos_t + np.cross(n, y_dir) * sin_t + n * np.dot(n, y_dir) * (1.0 - cos_t)
return x_rot, y_rot
return x_dir, y_dir
def marker_object_corners(marker: Dict[str, Any]) -> np.ndarray:
half = marker['size_m'] * 0.5
x_dir, y_dir = marker_plane_axes(marker['normal'], marker['spin_deg'])
corners = np.stack([
-x_dir * half + y_dir * half,
x_dir * half + y_dir * half,
x_dir * half - y_dir * half,
-x_dir * half - y_dir * half
], axis=0)
return marker['position_m'].reshape(1, 3) + corners
def build_link_chain(robot: Dict[str, Any]) -> List[str]:
links = robot.get('links', {}) or {}
ordered: List[str] = []
remaining = set(links.keys())
while remaining:
progress = False
for name in list(remaining):
parent = links[name].get('parent')
if not parent or parent in ordered:
ordered.append(name)
remaining.remove(name)
progress = True
if not progress:
raise RuntimeError('Cycle detected in robot link tree or missing parent link')
return ordered
def compute_link_transforms(robot: Dict[str, Any], state: Dict[str, float], scale: float) -> Dict[str, np.ndarray]:
links = robot.get('links', {}) or {}
ordered_links = build_link_chain(robot)
transforms: Dict[str, np.ndarray] = {}
for link_name in ordered_links:
link_info = links[link_name] or {}
parent_name = link_info.get('parent')
parent_transform = transforms[parent_name] if parent_name else np.eye(4, dtype=np.float64)
mount_translation = np.asarray(resolve_vector(link_info.get('mountPosition', [0, 0, 0]), 3), dtype=np.float64) * scale
mount = transform_from_translation_rotation(
mount_translation,
link_info.get('mountRotation', [0, 0, 0])
)
joint_info = link_info.get('jointToParent', {}) or {}
joint_origin = np.asarray(resolve_vector(joint_info.get('origin', [0, 0, 0]), 3), dtype=np.float64) * scale
joint = transform_from_translation_rotation(
joint_origin,
joint_info.get('rotation', [0, 0, 0])
)
motion = np.eye(4, dtype=np.float64)
joint_type = str(joint_info.get('type', 'fixed')).strip().lower()
control_var = str(joint_info.get('variable', joint_info.get('control', ''))).strip().lower()
axis = resolve_vector(joint_info.get('axis', [1, 0, 0]), 3)
if joint_type == 'linear':
motion[:3, 3] = normalize_axis(axis) * state.get(control_var, 0.0) * scale
elif joint_type == 'revolute':
motion = axis_angle_matrix(axis, state.get(control_var, 0.0))
transforms[link_name] = parent_transform @ mount @ joint @ motion
return transforms
def compute_marker_world_position(marker: Dict[str, Any], link_transforms: Dict[str, np.ndarray]) -> np.ndarray:
link_transform = link_transforms[marker['link_name']]
local = np.ones(4, dtype=np.float64)
local[:3] = marker['position_m']
world = link_transform @ local
return world[:3]
# ------------------------------------------------------------------
# Camera / observation helpers
# ------------------------------------------------------------------
def load_intrinsics(detection_json: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
cam = detection_json['camera']
K = np.asarray(cam['camera_matrix'], dtype=np.float64)
D = np.asarray(cam.get('distortion_coefficients', [0, 0, 0, 0, 0]), dtype=np.float64).reshape(-1, 1)
return K, D
def collect_views_and_observations(
detection_files: List[str],
robot_markers: Dict[int, Dict[str, Any]]
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
views: List[Dict[str, Any]] = []
observations: List[Dict[str, Any]] = []
for idx, det_path in enumerate(detection_files):
detection_json = load_json(det_path)
K, D = load_intrinsics(detection_json)
views.append({
'index': idx,
'source_file': os.path.abspath(det_path),
'camera_id': detection_json.get('camera', {}).get('camera_id', f'cam{idx+1}'),
'image_file': detection_json.get('image', {}).get('image_file'),
'K': K,
'D': D
})
for det in detection_json.get('detections', []) or []:
marker_id = int(det.get('marker_id', -1))
if marker_id < 0 or marker_id not in robot_markers:
continue
image_points = det.get('image_points_px')
if isinstance(image_points, list) and len(image_points) == 4:
image_points = np.asarray(image_points, dtype=np.float64)
else:
center = resolve_vector(det.get('center_px', [0, 0]), 2)
image_points = np.asarray([center], dtype=np.float64)
confidence = float(det.get('confidence', 1.0))
marker = robot_markers[marker_id]
observations.append({
'view_index': idx,
'marker_id': marker_id,
'marker_link_corners': marker_object_corners(marker),
'image_points_px': image_points,
'confidence': max(0.01, min(1.0, confidence))
})
if len(views) == 0:
raise RuntimeError('No valid detection views found')
if len(observations) == 0:
raise RuntimeError('No marker observations matched robot.json markers')
return views, observations
def compute_marker_world_corners(marker: Dict[str, Any], link_transforms: Dict[str, np.ndarray]) -> np.ndarray:
link_transform = link_transforms[marker['link_name']]
local = marker_object_corners(marker)
homogeneous = np.concatenate([local, np.ones((local.shape[0], 1), dtype=np.float64)], axis=1)
world = (link_transform @ homogeneous.T).T
return world[:, :3]
def initial_camera_guess(
view: Dict[str, Any],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
default_state: Dict[str, float],
scale: float,
robot: Dict[str, Any]
) -> Tuple[np.ndarray, np.ndarray]:
object_points = []
image_points = []
link_transforms = compute_link_transforms(robot, default_state, scale)
for obs in observations:
if obs['view_index'] != view['index']:
continue
marker = robot_markers[obs['marker_id']]
object_points.append(compute_marker_world_corners(marker, link_transforms))
image_points.append(obs['image_points_px'])
if len(object_points) == 0:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
object_points = np.vstack(object_points)
image_points = np.vstack(image_points)
if object_points.shape[0] < 4:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
success, rvec, tvec = cv2.solvePnP(
object_points,
image_points,
view['K'],
view['D'],
flags=cv2.SOLVEPNP_ITERATIVE
)
if not success:
return np.zeros((3, 1), dtype=np.float64), np.array([[0.0], [0.0], [1.0]], dtype=np.float64)
return rvec, tvec
def project_points(
points_3d: np.ndarray,
rvec: np.ndarray,
tvec: np.ndarray,
K: np.ndarray,
D: np.ndarray
) -> np.ndarray:
projected, _ = cv2.projectPoints(points_3d, rvec, tvec, K, D)
return projected.reshape(-1, 2)
def compute_soft_constraint_residuals(
robot_state: Dict[str, float],
robot_markers: Dict[int, Dict[str, Any]],
link_transforms: Dict[str, np.ndarray],
robot: Dict[str, Any],
enabled_constraints: Dict[str, ConstraintResult]
) -> List[float]:
"""
Compute residuals from soft constraints (kinematic consistency, rigid body distances).
Returns a list of constraint residuals to append to the total residual vector.
"""
residuals = []
weight_scale = 0.1 # Weight for soft constraints relative to reprojection errors
# Constraint 1: Rigid body distances within each link
if enabled_constraints['RigidBodyDistances'].enabled:
for link_name in ['Arm1', 'Ellbow', 'Arm2']:
link_markers = [m for m in robot_markers.values() if m['link_name'] == link_name]
if len(link_markers) < 2:
continue
# Compute all pairwise distances in world coords
for i in range(len(link_markers)):
for j in range(i + 1, len(link_markers)):
m_i = link_markers[i]
m_j = link_markers[j]
pos_i = compute_marker_world_position(m_i, link_transforms)
pos_j = compute_marker_world_position(m_j, link_transforms)
dist_world = np.linalg.norm(pos_i - pos_j)
# Reference distance in local coords
dist_local = np.linalg.norm(m_i['position_m'] - m_j['position_m'])
# Residual: difference should be zero (rigid body)
error = dist_world - dist_local
residuals.append(error * weight_scale * 0.1) # Very soft weight
# Constraint 2: Fixed X-distances between links (Arm1 <-> Ellbow)
if enabled_constraints['InterLinkXDistances'].enabled:
arm1_markers = [m for m in robot_markers.values() if m['link_name'] == 'Arm1']
ellbow_markers = [m for m in robot_markers.values() if m['link_name'] == 'Ellbow']
if len(arm1_markers) >= 1 and len(ellbow_markers) >= 1:
# Get first marker from each link
m_arm1 = arm1_markers[0]
m_ellbow = ellbow_markers[0]
pos_arm1 = compute_marker_world_position(m_arm1, link_transforms)
pos_ellbow = compute_marker_world_position(m_ellbow, link_transforms)
# X-distance in world should match reference (relative position)
# Since both rotate around X-axis at different points, we check consistency
x_diff_world = pos_ellbow[0] - pos_arm1[0]
x_diff_ref = m_ellbow['position_m'][0] - m_arm1['position_m'][0]
error = x_diff_world - x_diff_ref
residuals.append(error * weight_scale)
return residuals
def compute_marker_world_position(marker: Dict[str, Any], link_transforms: Dict[str, np.ndarray]) -> np.ndarray:
"""Compute the world position of a marker given current link transforms."""
link_transform = link_transforms[marker['link_name']]
local_pos = np.concatenate([marker['position_m'], [1.0]])
world_pos = (link_transform @ local_pos)[:3]
return world_pos
# ------------------------------------------------------------------
# Optimization
# ------------------------------------------------------------------
def pack_parameters(robot_state: Dict[str, float], camera_params: List[Tuple[np.ndarray, np.ndarray]]) -> np.ndarray:
state_vec = np.asarray([robot_state[k] for k in STATE_KEYS], dtype=np.float64)
cams = []
for rvec, tvec in camera_params:
cams.append(rvec.reshape(3))
cams.append(tvec.reshape(3))
return np.concatenate([state_vec] + cams)
def unpack_parameters(params: np.ndarray, n_views: int) -> Tuple[Dict[str, float], List[Tuple[np.ndarray, np.ndarray]]]:
robot_state = {STATE_KEYS[i]: float(params[i]) for i in range(len(STATE_KEYS))}
camera_params = []
offset = len(STATE_KEYS)
for _ in range(n_views):
rvec = params[offset:offset + 3].reshape(3, 1)
tvec = params[offset + 3:offset + 6].reshape(3, 1)
camera_params.append((rvec, tvec))
offset += 6
return robot_state, camera_params
def residuals_for_parameters(
params: np.ndarray,
views: List[Dict[str, Any]],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
robot: Dict[str, Any],
scale: float,
default_state: Dict[str, float],
enabled_constraints: Dict[str, ConstraintResult]
) -> np.ndarray:
robot_state, camera_params = unpack_parameters(params, len(views))
link_transforms = compute_link_transforms(robot, robot_state, scale)
residuals = []
# Reprojection residuals (primary observation)
for obs in observations:
marker = robot_markers[obs['marker_id']]
world_corners = compute_marker_world_corners(marker, link_transforms)
rvec, tvec = camera_params[obs['view_index']]
proj = project_points(world_corners, rvec, tvec, views[obs['view_index']]['K'], views[obs['view_index']]['D'])
diffs = proj - obs['image_points_px']
weight = math.sqrt(obs['confidence'])
residuals.extend((diffs * weight).reshape(-1))
# Weak priors on robot state
for key in STATE_KEYS:
diff = robot_state[key] - default_state.get(key, 0.0)
if key in ('x', 'y', 'z', 'e'):
w = 0.001
else:
w = 0.01
residuals.append(diff * w)
# Soft constraints (kinematic consistency, rigid body constraints)
soft_constraint_residuals = compute_soft_constraint_residuals(
robot_state, robot_markers, link_transforms, robot, enabled_constraints
)
residuals.extend(soft_constraint_residuals)
return np.asarray(residuals, dtype=np.float64)
def estimate_uncertainty(result: Any, n_params: int) -> np.ndarray:
if result.jac is None:
return np.full(n_params, float('nan'), dtype=np.float64)
J = result.jac
m, n = J.shape
JTJ = J.T @ J
try:
cov = np.linalg.pinv(JTJ)
except np.linalg.LinAlgError:
cov = np.linalg.pinv(JTJ + np.eye(n) * 1e-9)
residuals = result.fun
dof = max(1, m - n)
sigma2 = float(np.sum(residuals ** 2) / dof)
cov *= sigma2
return np.sqrt(np.diag(cov))
def print_constraint_sanity_check(
robot_state: Dict[str, float],
robot_markers: Dict[int, Dict[str, Any]],
link_transforms: Dict[str, np.ndarray],
robot: Dict[str, Any],
enabled_constraints: Dict[str, ConstraintResult]
) -> None:
"""
Print sanity checks for all constraints to verify the optimization result.
"""
print("\n" + "=" * 70)
print("CONSTRAINT SANITY CHECKS (after optimization)")
print("=" * 70)
# Check 1: Rigid body distances
if enabled_constraints['RigidBodyDistances'].enabled:
print("\n1. RIGID BODY DISTANCES")
for link_name in ['Arm1', 'Ellbow', 'Arm2']:
link_markers = [m for m in robot_markers.values() if m['link_name'] == link_name]
if len(link_markers) < 2:
continue
max_error = 0.0
for i in range(len(link_markers)):
for j in range(i + 1, len(link_markers)):
m_i = link_markers[i]
m_j = link_markers[j]
pos_i = compute_marker_world_position(m_i, link_transforms)
pos_j = compute_marker_world_position(m_j, link_transforms)
dist_world = np.linalg.norm(pos_i - pos_j)
dist_local = np.linalg.norm(m_i['position_m'] - m_j['position_m'])
error = abs(dist_world - dist_local)
max_error = max(max_error, error)
status = "" if max_error < 1.0 else "" if max_error < 5.0 else ""
print(f" {link_name:10s}: max_error = {max_error:.3f} mm {status}")
# Check 2: Inter-link X distances
if enabled_constraints['InterLinkXDistances'].enabled:
print("\n2. INTER-LINK X-DISTANCES")
arm1_markers = [m for m in robot_markers.values() if m['link_name'] == 'Arm1']
ellbow_markers = [m for m in robot_markers.values() if m['link_name'] == 'Ellbow']
if len(arm1_markers) >= 1 and len(ellbow_markers) >= 1:
m_arm1 = arm1_markers[0]
m_ellbow = ellbow_markers[0]
pos_arm1 = compute_marker_world_position(m_arm1, link_transforms)
pos_ellbow = compute_marker_world_position(m_ellbow, link_transforms)
x_diff_world = pos_ellbow[0] - pos_arm1[0]
x_diff_ref = m_ellbow['position_m'][0] - m_arm1['position_m'][0]
error = abs(x_diff_world - x_diff_ref)
status = "" if error < 1.0 else "" if error < 5.0 else ""
print(f" Arm1 <-> Ellbow: error = {error:.3f} mm {status}")
# Check 3: Arm2 sin(a) dependency
if enabled_constraints['Arm2SinADependency'].enabled:
print("\n3. ARM2 sin(a) DEPENDENCY (sanity check)")
arm2_markers = [m for m in robot_markers.values() if m['link_name'] == 'Arm2']
if len(arm2_markers) >= 2:
# Check that markers with different Z values have different X spreads
a_rad = math.radians(robot_state['a'])
sin_a = math.sin(a_rad)
cos_a = math.cos(a_rad)
z_variations = {}
for m in arm2_markers:
z_local = m['position_m'][2]
x_local = m['position_m'][0]
pos_world = compute_marker_world_position(m, link_transforms)
x_world = pos_world[0]
# Expected: x_world = 90 + x_local * cos(a) - z_local * sin(a)
x_expected = 90 * (robot.get('renderingInfo', {}).get('metric', 'mm') == 'mm' and 0.09 or 0.09) + x_local * cos_a - z_local * sin_a
x_error = abs(x_world - x_expected)
if z_local not in z_variations:
z_variations[z_local] = []
z_variations[z_local].append(x_error)
max_error = max(max(errors) for errors in z_variations.values()) if z_variations else 0.0
status = "" if max_error < 5.0 else "" if max_error < 10.0 else ""
print(f" X-consistency with sin(a): max_error = {max_error:.3f} mm {status}")
print(f" (Note: this is a consistency check, not a hard constraint)")
print("=" * 70)
def camera_position_world(rvec: np.ndarray, tvec: np.ndarray) -> np.ndarray:
R, _ = cv2.Rodrigues(rvec)
return (-R.T @ tvec).reshape(3)
def build_output(
robot_state: Dict[str, float],
state_uncertainty: np.ndarray,
views: List[Dict[str, Any]],
camera_params: List[Tuple[np.ndarray, np.ndarray]],
observations: List[Dict[str, Any]],
robot_markers: Dict[int, Dict[str, Any]],
scale: float,
robot: Dict[str, Any],
robot_json_path: str
) -> Dict[str, Any]:
link_transforms = compute_link_transforms(robot, robot_state, scale)
marker_summary: Dict[int, Dict[str, Any]] = {}
for marker_id, marker in robot_markers.items():
marker_summary[marker_id] = {
'marker_id': marker_id,
'link_name': marker['link_name'],
'position_world_m': compute_marker_world_position(marker, link_transforms).tolist(),
'size_m': marker['size_m'],
'observation_count': 0,
'mean_confidence': None,
'mean_reprojection_error_px': None,
'observations': []
}
per_marker_errors: Dict[int, List[float]] = {mid: [] for mid in marker_summary}
per_marker_confidences: Dict[int, List[float]] = {mid: [] for mid in marker_summary}
link_transforms = compute_link_transforms(robot, robot_state, scale)
for obs in observations:
marker_id = obs['marker_id']
marker = robot_markers[marker_id]
object_points_m = compute_marker_world_corners(marker, link_transforms)
rvec, tvec = camera_params[obs['view_index']]
proj = project_points(object_points_m, rvec, tvec, views[obs['view_index']]['K'], views[obs['view_index']]['D'])
diffs = proj - obs['image_points_px']
errors = np.linalg.norm(diffs, axis=1)
repro_error = float(np.mean(errors))
per_marker_errors[marker_id].extend(errors.tolist())
per_marker_confidences[marker_id].append(obs['confidence'])
marker_summary[marker_id]['observation_count'] += 1
marker_summary[marker_id]['observations'].append({
'view_index': obs['view_index'],
'source_file': views[obs['view_index']]['source_file'],
'image_file': views[obs['view_index']]['image_file'],
'confidence': obs['confidence'],
'mean_reprojection_error_px': repro_error,
'corner_reprojection_errors_px': errors.tolist()
})
for marker_id, summary in marker_summary.items():
if summary['observation_count'] > 0:
summary['mean_confidence'] = float(np.mean(per_marker_confidences[marker_id]))
summary['mean_reprojection_error_px'] = float(np.mean(per_marker_errors[marker_id]))
camera_outputs = []
for idx, view in enumerate(views):
rvec, tvec = camera_params[idx]
cam_pos = camera_position_world(rvec, tvec)
observed_count = sum(1 for obs in observations if obs['view_index'] == idx)
camera_outputs.append({
'view_index': idx,
'source_file': view['source_file'],
'camera_id': view['camera_id'],
'camera_position_world_m': cam_pos.tolist(),
'rvec': rvec.reshape(-1).tolist(),
'tvec': tvec.reshape(-1).tolist(),
'intrinsics': {
'camera_matrix': view['K'].tolist(),
'distortion_coefficients': view['D'].reshape(-1).tolist()
},
'observation_count': observed_count
})
robot_pose_output = {
'state': {k: float(robot_state[k]) for k in STATE_KEYS},
'uncertainty': {
'x_mm': float(state_uncertainty[0]),
'y_mm': float(state_uncertainty[1]),
'z_mm': float(state_uncertainty[2]),
'a_deg': float(state_uncertainty[3]),
'b_deg': float(state_uncertainty[4]),
'c_deg': float(state_uncertainty[5]),
'e_mm': float(state_uncertainty[6])
},
'confidence': {
'x': float(math.exp(-state_uncertainty[0] / 10.0)),
'y': float(math.exp(-state_uncertainty[1] / 10.0)),
'z': float(math.exp(-state_uncertainty[2] / 10.0)),
'a': float(math.exp(-state_uncertainty[3] / 10.0)),
'b': float(math.exp(-state_uncertainty[4] / 10.0)),
'c': float(math.exp(-state_uncertainty[5] / 10.0)),
'e': float(math.exp(-state_uncertainty[6] / max(1.0, state_uncertainty[6])))
}
}
return {
'schema_version': '1.0',
'created_utc': datetime.datetime.utcnow().isoformat() + 'Z',
'source_robot_json': os.path.abspath(robot_json_path),
'source_detections': [view['source_file'] for view in views],
'robot_pose': robot_pose_output,
'camera_poses': camera_outputs,
'marker_positions': list(marker_summary.values())
}
# ------------------------------------------------------------------
# Main
# ------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description='Multiview optimization of robot pose and camera extrinsics')
parser.add_argument('--robot', required=True, help='Path to robot.json')
parser.add_argument('--detections', required=True, nargs='+', help='List of detection JSON files')
parser.add_argument('--outDir', required=True, help='Output directory')
parser.add_argument('--write-summary', action='store_true', help='Write summary file')
parser.add_argument('--max-iter', type=int, default=500, help='Maximum optimizer iterations')
args = parser.parse_args()
os.makedirs(args.outDir, exist_ok=True)
robot_json_path = os.path.abspath(args.robot)
robot = load_json(robot_json_path)
scale = parse_metric_scale(robot)
default_state = {
k: float(robot.get('defaultPosition', {}).get(k, 0.0) or 0.0)
for k in STATE_KEYS
}
robot_markers = extract_markers(robot, scale)
# Validate constraints
print("\n" + "=" * 70)
print("CONSTRAINT VALIDATION")
print("=" * 70)
enabled_constraints = validate_constraints(robot, robot_markers)
for constraint_name, result in enabled_constraints.items():
print(result)
print("=" * 70)
views, observations = collect_views_and_observations(args.detections, robot_markers)
camera_guesses = []
for view in views:
rvec, tvec = initial_camera_guess(view, observations, robot_markers, default_state, scale, robot)
camera_guesses.append((rvec, tvec))
x0 = pack_parameters(default_state, camera_guesses)
progress = {
'iter': 0,
'last_cost': None,
'last_print': time.time(),
'prev_x': x0.copy()
}
def progress_callback(xk: np.ndarray) -> None:
progress['iter'] += 1
now = time.time()
if progress['iter'] == 1 or now - progress['last_print'] >= 1.0:
res = residuals_for_parameters(xk, views, observations, robot_markers, robot, scale, default_state, enabled_constraints)
cost = 0.5 * float(np.dot(res, res))
delta_cost = None
convergence = ''
if progress['last_cost'] is not None:
delta_cost = cost - progress['last_cost']
if abs(delta_cost) < 1e-3:
convergence = ' stable'
elif delta_cost < 0:
convergence = ' improving'
else:
convergence = ' worsening'
step_norm = float(np.linalg.norm(xk - progress['prev_x']))
print(
f'[Multiview] iter={progress["iter"]:4d} cost={cost:.4f}'
+ (f' delta={delta_cost:.4g}' if delta_cost is not None else '')
+ f' step={step_norm:.4g}'
+ convergence
)
progress['last_cost'] = cost
progress['last_print'] = now
progress['prev_x'] = xk.copy()
result = least_squares(
residuals_for_parameters,
x0,
args=(views, observations, robot_markers, robot, scale, default_state, enabled_constraints),
jac='2-point',
method='trf',
loss='soft_l1',
f_scale=1.0,
max_nfev=args.max_iter,
callback=progress_callback
)
robot_state, camera_params = unpack_parameters(result.x, len(views))
uncertainties = estimate_uncertainty(result, len(result.x))
# Print constraint sanity checks
link_transforms = compute_link_transforms(robot, robot_state, scale)
print_constraint_sanity_check(robot_state, robot_markers, link_transforms, robot, enabled_constraints)
output = build_output(robot_state, uncertainties[:len(STATE_KEYS)], views, camera_params, observations, robot_markers, scale, robot, robot_json_path)
out_path = Path(args.outDir) / 'multiview_pose.json'
save_json(output, out_path)
print(f'Saved: {out_path}')
if args.write_summary:
summary_path = Path(args.outDir) / 'multiview_pose_summary.json'
summary = {
'final_cost': float(result.cost),
'status': int(result.status),
'message': result.message,
'robot_state': output['robot_pose'],
'camera_count': len(views),
'marker_count': len(robot_markers)
}
save_json(summary, summary_path)
print(f'Saved: {summary_path}')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,827 @@
#!/usr/bin/env python3
"""
2_estimate_camera_from_observations.py
Estimate a single camera pose from ArUco observations stored in
*_aruco_detection.json, using marker world positions from robot.json.
This follows the same mathematical idea as readTwoImages.py:
1) use detected marker observations,
2) get an initial pose from a rigid transform,
3) refine with Levenberg-Marquardt on normalized reprojection residuals.
Difference to readTwoImages.py:
- No image processing here.
- Input is the observation JSON created by 1_detect_aruco_observations.py.
- Output is xxx_camera_pose.json.
- Unknown marker reconstruction is intentionally omitted.
Assumptions:
- robot.json contains a marker list for the board/world frame.
- At minimum, marker positions are present for the reference markers.
- The detection JSON contains camera intrinsics and marker corners.
Typical usage:
python3 2_estimate_camera_from_observations.py \
-i frame_0001_aruco_detection.json \
-robot robot.json \
-outDir results/
Output:
frame_0001_camera_pose.json
Notes on uncertainty:
- The script computes an approximate 6x6 covariance for the pose parameters
[rvec_x, rvec_y, rvec_z, t_x, t_y, t_z].
- It also propagates that covariance to camera center uncertainty in world
coordinates and to approximate roll/pitch/yaw uncertainty.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from typing import Any, Dict, List, Optional, Tuple
import cv2
import numpy as np
# ---------------------------------------------------------------------
# Path / JSON helpers
# ---------------------------------------------------------------------
def resolve_path(path: str) -> str:
path = os.path.expanduser(path)
if os.path.isabs(path):
return path
return os.path.abspath(path)
def load_json(path: str) -> Dict[str, Any]:
with open(resolve_path(path), "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path: str, data: Dict[str, Any]) -> None:
with open(resolve_path(path), "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
# ---------------------------------------------------------------------
# Intrinsics
# ---------------------------------------------------------------------
def load_intrinsics_from_detection(detection: Dict[str, Any]) -> Tuple[np.ndarray, np.ndarray]:
"""
Primary source: the embedded camera intrinsics in the detection JSON.
"""
camera = detection.get("camera", {})
K = camera.get("camera_matrix", None)
D = camera.get("distortion_coefficients", None)
if K is None:
raise KeyError("camera_matrix missing in detection JSON.")
if D is None:
D = [0, 0, 0, 0, 0]
K = np.array(K, dtype=np.float32).reshape(3, 3)
D = np.array(D, dtype=np.float32).reshape(-1, 1)
return K, D
# ---------------------------------------------------------------------
# Robot JSON parsing
# ---------------------------------------------------------------------
def _rotation_matrix_from_any(rotation: Any) -> np.ndarray:
"""
Best-effort parser for marker rotation.
Supported inputs:
- 3x3 matrix as nested list
- flat 9 list
- dict with keys:
* rotation_matrix / matrix
* rvec / rodriques / rodrigues
* euler_deg / rpy_deg / roll_pitch_yaw_deg
* euler_rad / rpy_rad / roll_pitch_yaw_rad
* quaternion / quat (best-effort, expects [x,y,z,w] unless specified)
- None => identity
The pose estimator below only needs marker positions, but we keep
this parser for completeness and future extension.
"""
if rotation is None:
return np.eye(3, dtype=np.float32)
# Direct matrix
if isinstance(rotation, (list, tuple, np.ndarray)):
arr = np.array(rotation, dtype=np.float32)
if arr.shape == (3, 3):
return arr
if arr.size == 9:
return arr.reshape(3, 3).astype(np.float32)
if arr.size == 3:
# Treat as Rodrigues vector
R, _ = cv2.Rodrigues(arr.reshape(3, 1))
return R.astype(np.float32)
return np.eye(3, dtype=np.float32)
if isinstance(rotation, dict):
for key in ("rotation_matrix", "matrix"):
if key in rotation:
return _rotation_matrix_from_any(rotation[key])
for key in ("rvec", "rodrigues", "rodriques"):
if key in rotation:
v = np.array(rotation[key], dtype=np.float32).reshape(3, 1)
R, _ = cv2.Rodrigues(v)
return R.astype(np.float32)
def euler_to_R(roll: float, pitch: float, yaw: float, degrees: bool = True) -> np.ndarray:
if degrees:
roll = np.deg2rad(roll)
pitch = np.deg2rad(pitch)
yaw = np.deg2rad(yaw)
cr, sr = np.cos(roll), np.sin(roll)
cp, sp = np.cos(pitch), np.sin(pitch)
cy, sy = np.cos(yaw), np.sin(yaw)
Rx = np.array([[1, 0, 0],
[0, cr, -sr],
[0, sr, cr]], dtype=np.float32)
Ry = np.array([[cp, 0, sp],
[0, 1, 0],
[-sp, 0, cp]], dtype=np.float32)
Rz = np.array([[cy, -sy, 0],
[sy, cy, 0],
[0, 0, 1]], dtype=np.float32)
# ZYX convention
return (Rz @ Ry @ Rx).astype(np.float32)
for key in ("euler_deg", "rpy_deg", "roll_pitch_yaw_deg"):
if key in rotation:
vals = np.array(rotation[key], dtype=np.float32).reshape(-1)
if vals.size == 3:
return euler_to_R(float(vals[0]), float(vals[1]), float(vals[2]), degrees=True)
for key in ("euler_rad", "rpy_rad", "roll_pitch_yaw_rad"):
if key in rotation:
vals = np.array(rotation[key], dtype=np.float32).reshape(-1)
if vals.size == 3:
return euler_to_R(float(vals[0]), float(vals[1]), float(vals[2]), degrees=False)
for key in ("quaternion", "quat"):
if key in rotation:
q = np.array(rotation[key], dtype=np.float32).reshape(-1)
if q.size == 4:
# Best-effort: try [x,y,z,w]
x, y, z, w = [float(v) for v in q]
R = np.array([
[1 - 2*y*y - 2*z*z, 2*x*y - 2*z*w, 2*x*z + 2*y*w],
[2*x*y + 2*z*w, 1 - 2*x*x - 2*z*z, 2*y*z - 2*x*w],
[2*x*z - 2*y*w, 2*y*z + 2*x*w, 1 - 2*x*x - 2*y*y]
], dtype=np.float32)
return R
return np.eye(3, dtype=np.float32)
def get_marker_rotation(marker: Dict[str, Any]) -> np.ndarray:
"""
Flexible rotation extraction. Falls back to identity if absent.
"""
for key in ("rotation", "rotation_matrix", "matrix", "pose_rotation", "orientation"):
if key in marker:
return _rotation_matrix_from_any(marker[key])
# Also allow flat pose-style fields
if "rvec" in marker or "rodrigues" in marker:
return _rotation_matrix_from_any({"rvec": marker.get("rvec", marker.get("rodrigues"))})
if "euler_deg" in marker:
return _rotation_matrix_from_any({"euler_deg": marker["euler_deg"]})
if "rpy_deg" in marker:
return _rotation_matrix_from_any({"rpy_deg": marker["rpy_deg"]})
if "quaternion" in marker:
return _rotation_matrix_from_any({"quaternion": marker["quaternion"]})
return np.eye(3, dtype=np.float32)
def load_marker_lookup(robot_json_path: str) -> Dict[int, Dict[str, Any]]:
"""
Supports the new format:
robot_data["links"]["Board"]["markers"]
Fallback:
robot_data["Marker"]
"""
robot_json_path = resolve_path(robot_json_path)
with open(robot_json_path, "r", encoding="utf-8") as f:
robot_data = json.load(f)
marker_lookup: Dict[int, Dict[str, Any]] = {}
links = robot_data.get("links", {})
board = links.get("Board")
markers = None
if board and "markers" in board:
markers = board["markers"]
if not markers:
markers = robot_data.get("Marker", [])
for marker in markers:
marker_id = int(marker.get("id", -1))
if marker_id < 0:
continue
if "position" not in marker:
continue
pos = marker.get("position")
if pos is None:
continue
if len(pos) != 3:
continue
rotation = get_marker_rotation(marker)
marker_lookup[marker_id] = {
"position": np.array(pos, dtype=np.float32),
"rotation": rotation,
"on": marker.get("on", "unknown"),
}
return marker_lookup
def load_robot_marker_size(robot_json_path: str) -> Optional[float]:
"""
Best-effort marker size reader from robot.json.
Returns meters if found, otherwise None.
"""
robot_json_path = resolve_path(robot_json_path)
with open(robot_json_path, "r", encoding="utf-8") as f:
robot_data = json.load(f)
vision_config = robot_data.get("vision_config", {})
size = vision_config.get("MarkerSize", None)
if size is None:
return None
try:
return float(size)
except Exception:
return None
# ---------------------------------------------------------------------
# Geometry / pose helpers
# ---------------------------------------------------------------------
def marker_local_corners(marker_size_m: float) -> np.ndarray:
half = marker_size_m / 2.0
# Same corner order as the readTwoImages.py example
return np.array([
[-half, half, 0.0],
[ half, half, 0.0],
[ half, -half, 0.0],
[-half, -half, 0.0],
], dtype=np.float32)
def rigid_transform_no_scale(A: np.ndarray, B: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""
Find R, t such that 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:
pts = points_px.reshape(-1, 1, 2).astype(np.float32)
und = cv2.undistortPoints(pts, K, D, P=None)
return und.reshape(-1, 2).astype(np.float32)
def rvec_to_R(rvec: np.ndarray) -> np.ndarray:
R, _ = cv2.Rodrigues(rvec.reshape(3, 1))
return R.astype(np.float32)
def R_to_euler_zyx(R: np.ndarray) -> Tuple[float, float, float]:
"""
Return roll, pitch, yaw in degrees using ZYX convention.
"""
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
def theta_to_camera_pose(theta: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
theta = [omega_x, omega_y, omega_z, t_x, t_y, t_z]
Returns:
R_wc, t_wc, camera_center_world
"""
omega = theta[0:3]
t_wc = theta[3:6].reshape(3, 1).astype(np.float32)
R_wc, _ = cv2.Rodrigues(omega.reshape(3, 1))
R_wc = R_wc.astype(np.float32)
R_cw = R_wc.T
camera_center_world = (-R_cw @ t_wc).reshape(3)
return R_wc, t_wc.reshape(3), camera_center_world
def build_projection_matrix(K: np.ndarray, R: np.ndarray, t: np.ndarray) -> np.ndarray:
return K @ np.hstack([R, t.reshape(3, 1)])
# ---------------------------------------------------------------------
# LM on normalized residuals (same style as readTwoImages.py)
# ---------------------------------------------------------------------
def pack_params(omega: np.ndarray, t: np.ndarray) -> np.ndarray:
return np.hstack([omega.reshape(3), t.reshape(3)]).astype(np.float64)
def unpack_params(theta: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
omega = theta[0:3]
t = theta[3:6]
return omega, t
def residuals_centers_normalized(theta: np.ndarray,
X_world: np.ndarray,
obs_norm: np.ndarray) -> np.ndarray:
"""
Residuals in normalized coordinates:
obs_norm - project(R X_world + t)
"""
omega, t = unpack_params(theta)
R_wc = cv2.Rodrigues(omega.reshape(3, 1))[0].astype(np.float64)
X_cam = (R_wc @ X_world.T + t.reshape(3, 1)).T
uv = X_cam[:, :2] / X_cam[:, 2:3]
r = (obs_norm - uv).reshape(-1)
return r
def numerical_jacobian(f, theta: np.ndarray, eps: float, *args) -> Tuple[np.ndarray, np.ndarray]:
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_world: np.ndarray,
obs_norm: np.ndarray,
max_iter: int = 60,
eps_jac: float = 1e-6,
lambda_init: float = 1e-3) -> Tuple[np.ndarray, Dict[str, List[float]]]:
lam = lambda_init
theta = theta0.copy().astype(np.float64)
history = {"iters": [], "rms": [], "lambda": []}
for it in range(max_iter):
J, r = numerical_jacobian(residuals_centers_normalized, theta, eps_jac, X_world, obs_norm)
rms = float(np.sqrt(np.mean(r * r))) if r.size else 0.0
history["iters"].append(it)
history["rms"].append(rms)
history["lambda"].append(lam)
JTJ = J.T @ J
g = J.T @ r
H = JTJ + lam * np.eye(JTJ.shape[0], dtype=np.float64)
try:
delta = -np.linalg.solve(H, g)
except np.linalg.LinAlgError:
delta, *_ = np.linalg.lstsq(H, -g, rcond=None)
theta_trial = theta + delta
r_trial = residuals_centers_normalized(theta_trial, X_world, obs_norm)
rms_trial = float(np.sqrt(np.mean(r_trial * r_trial))) if r_trial.size else rms
if rms_trial < rms:
theta = theta_trial
lam *= 0.5
else:
lam *= 2.0
if np.linalg.norm(delta) < 1e-10:
break
if abs(rms - rms_trial) < 1e-12:
break
return theta, history
def pose_covariance(theta: np.ndarray,
X_world: np.ndarray,
obs_norm: np.ndarray,
eps_jac: float = 1e-6) -> Tuple[np.ndarray, float, np.ndarray]:
"""
Returns:
cov_theta_6x6, sigma2, residual_vector
"""
J, r = numerical_jacobian(residuals_centers_normalized, theta, eps_jac, X_world, obs_norm)
m = r.size
n = theta.size
dof = max(1, m - n)
sigma2 = float((r @ r) / dof)
JTJ = J.T @ J
cov = sigma2 * np.linalg.pinv(JTJ)
return cov.astype(np.float64), sigma2, r
def propagate_covariance(theta: np.ndarray,
cov_theta: np.ndarray) -> Dict[str, Any]:
"""
Propagate pose covariance to camera center and Euler angle uncertainties.
"""
def camera_center_fn(th: np.ndarray) -> np.ndarray:
_, _, c = theta_to_camera_pose(th)
return c.astype(np.float64)
def euler_fn(th: np.ndarray) -> np.ndarray:
R_wc, _, _ = theta_to_camera_pose(th)
return np.array(R_to_euler_zyx(R_wc), dtype=np.float64) # deg
Jc, _ = numerical_jacobian(lambda th, *_: camera_center_fn(th), theta, 1e-6)
cov_center = Jc @ cov_theta @ Jc.T
Je, _ = numerical_jacobian(lambda th, *_: euler_fn(th), theta, 1e-6)
cov_euler = Je @ cov_theta @ Je.T
center_std_m = np.sqrt(np.maximum(0.0, np.diag(cov_center)))
euler_std_deg = np.sqrt(np.maximum(0.0, np.diag(cov_euler)))
# Parameter std directly from covariance
param_std = np.sqrt(np.maximum(0.0, np.diag(cov_theta)))
rvec_std_deg = np.degrees(param_std[0:3])
tvec_std_m = param_std[3:6]
return {
"pose_covariance_6x6": cov_theta.tolist(),
"parameter_std": {
"rvec_std_deg": [float(x) for x in rvec_std_deg],
"tvec_std_m": [float(x) for x in tvec_std_m],
},
"camera_center_std_m": [float(x) for x in center_std_m],
"camera_center_std_mm": [float(x * 1000.0) for x in center_std_m],
"orientation_std_deg": {
"roll": float(euler_std_deg[0]),
"pitch": float(euler_std_deg[1]),
"yaw": float(euler_std_deg[2]),
},
}
# ---------------------------------------------------------------------
# Marker processing
# ---------------------------------------------------------------------
def build_object_corners_from_world_position(position_m: np.ndarray,
marker_size_m: float) -> np.ndarray:
"""
Marker corners in world coordinates, assuming the marker frame is aligned
with the world frame and only translated to 'position_m'.
This is the direct analogue of readTwoImages.py using marker center positions.
"""
h = marker_size_m / 2.0
local = np.array([
[-h, h, 0.0],
[ h, h, 0.0],
[ h, -h, 0.0],
[-h, -h, 0.0],
], dtype=np.float32)
return local + position_m.reshape(1, 3)
def solve_single_marker_pose(corners_px: np.ndarray,
K: np.ndarray,
D: np.ndarray,
marker_size_m: float) -> Optional[Tuple[np.ndarray, np.ndarray]]:
obj = marker_local_corners(marker_size_m)
success, rvec, tvec = cv2.solvePnP(
obj,
corners_px.astype(np.float32),
K,
D,
flags=cv2.SOLVEPNP_IPPE_SQUARE
)
if not success:
success, rvec, tvec = cv2.solvePnP(
obj,
corners_px.astype(np.float32),
K,
D,
flags=cv2.SOLVEPNP_ITERATIVE
)
if not success:
return None
return rvec.reshape(3), tvec.reshape(3)
# ---------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(description="Estimate camera pose from ArUco observation JSON")
parser.add_argument("-i", "--input", required=True, help="*_aruco_detection.json")
parser.add_argument("-robot", "--robot", required=True, help="robot.json with board markers")
parser.add_argument("-outDir", "--outDir", default=None, help="Optional output directory")
parser.add_argument("--minConfidence", type=float, default=0.0,
help="Skip detections below this confidence")
parser.add_argument("--minCommonMarkers", type=int, default=3,
help="Minimum number of world markers required")
parser.add_argument("--maxRmsPx", type=float, default=None,
help="Optional soft warning threshold for final reprojection RMS in pixels")
parser.add_argument("--epsJac", type=float, default=1e-6, help="Finite-difference epsilon")
args = parser.parse_args()
detection_path = resolve_path(args.input)
robot_path = resolve_path(args.robot)
detection = load_json(detection_path)
marker_lookup = load_marker_lookup(robot_path)
K, D = load_intrinsics_from_detection(detection)
robot_marker_size = load_robot_marker_size(robot_path)
det_marker_size = detection.get("vision_config", {}).get("MarkerSize", None)
if det_marker_size is not None:
marker_size_m = float(det_marker_size)
elif robot_marker_size is not None:
marker_size_m = float(robot_marker_size)
else:
marker_size_m = 0.025
detections = detection.get("detections", [])
if not isinstance(detections, list):
raise TypeError("detection['detections'] must be a list")
used_ids: List[int] = []
used_world_positions: List[np.ndarray] = []
used_obs_centers_px: List[np.ndarray] = []
used_obs_centers_norm: List[np.ndarray] = []
used_marker_cam_centers: List[np.ndarray] = []
used_marker_meta: List[Dict[str, Any]] = []
sanity_notes: List[str] = []
for det in detections:
if det.get("type", "aruco") != "aruco":
continue
marker_id = int(det.get("marker_id", -1))
if marker_id < 0:
continue
if marker_id not in marker_lookup:
continue
confidence = float(det.get("confidence", 1.0))
if confidence < args.minConfidence:
continue
corners = det.get("image_points_px", None)
if corners is None:
continue
corners_px = np.array(corners, dtype=np.float32).reshape(4, 2)
center_from_corners = corners_px.mean(axis=0)
center_px = np.array(det.get("center_px", center_from_corners), dtype=np.float32).reshape(2)
center_delta = float(np.linalg.norm(center_from_corners - center_px))
if center_delta > 0.75:
sanity_notes.append(
f"marker {marker_id}: center_px differs from corner-mean by {center_delta:.2f}px"
)
pnp = solve_single_marker_pose(corners_px, K, D, marker_size_m)
if pnp is None:
continue
rvec_m, tvec_m = pnp
world_pos = marker_lookup[marker_id]["position"].astype(np.float32)
used_ids.append(marker_id)
used_world_positions.append(world_pos)
used_obs_centers_px.append(center_from_corners.astype(np.float32))
used_obs_centers_norm.append(undistort_to_normalized(center_from_corners.reshape(1, 2), K, D)[0])
used_marker_cam_centers.append(tvec_m.astype(np.float32))
used_marker_meta.append({
"marker_id": marker_id,
"confidence": confidence,
"center_px": [float(center_from_corners[0]), float(center_from_corners[1])],
"marker_size_m": marker_size_m,
})
# Unique / deduplicate by marker_id while preserving order
dedup: Dict[int, int] = {}
uniq_ids: List[int] = []
uniq_world_positions: List[np.ndarray] = []
uniq_obs_px: List[np.ndarray] = []
uniq_obs_norm: List[np.ndarray] = []
uniq_cam_centers: List[np.ndarray] = []
uniq_meta: List[Dict[str, Any]] = []
for idx, mid in enumerate(used_ids):
if mid in dedup:
continue
dedup[mid] = idx
uniq_ids.append(mid)
uniq_world_positions.append(used_world_positions[idx])
uniq_obs_px.append(used_obs_centers_px[idx])
uniq_obs_norm.append(used_obs_centers_norm[idx])
uniq_cam_centers.append(used_marker_cam_centers[idx])
uniq_meta.append(used_marker_meta[idx])
if len(uniq_ids) < args.minCommonMarkers:
raise RuntimeError(
f"Need at least {args.minCommonMarkers} common markers; found {len(uniq_ids)}: {uniq_ids}"
)
X_world = np.stack(uniq_world_positions, axis=0).astype(np.float64)
obs_px = np.stack(uniq_obs_px, axis=0).astype(np.float64)
obs_norm = np.stack(uniq_obs_norm, axis=0).astype(np.float64)
marker_cam_centers = np.stack(uniq_cam_centers, axis=0).astype(np.float64)
# Initial pose from rigid transform of per-marker camera-frame centers to world positions
# B ≈ R A + t -> world = R * camera + t
R_cw_init, t_cw_init = rigid_transform_no_scale(marker_cam_centers, X_world)
R_wc_init = R_cw_init.T
t_wc_init = (-R_wc_init @ t_cw_init).reshape(3)
omega_init = cv2.Rodrigues(R_wc_init)[0].reshape(3)
theta0 = pack_params(omega_init, t_wc_init)
theta_opt, hist = lm_solve(
theta0=theta0,
X_world=X_world,
obs_norm=obs_norm,
max_iter=60,
eps_jac=args.epsJac,
lambda_init=1e-3,
)
R_wc, t_wc, camera_center_world = theta_to_camera_pose(theta_opt)
cov_theta, sigma2, residual_vec = pose_covariance(
theta_opt, X_world, obs_norm, eps_jac=args.epsJac
)
propagated = propagate_covariance(theta_opt, cov_theta)
# Exact pixel-space reprojection statistics
proj_pts, _ = cv2.projectPoints(
X_world.reshape(-1, 1, 3).astype(np.float32),
theta_opt[0:3].reshape(3, 1).astype(np.float32),
theta_opt[3:6].reshape(3, 1).astype(np.float32),
K,
D,
)
proj_pts = proj_pts.reshape(-1, 2)
reproj_err_px = np.linalg.norm(proj_pts - obs_px, axis=1)
rms_px = float(np.sqrt(np.mean(reproj_err_px ** 2))) if reproj_err_px.size else 0.0
median_px = float(np.median(reproj_err_px)) if reproj_err_px.size else 0.0
max_px = float(np.max(reproj_err_px)) if reproj_err_px.size else 0.0
if args.maxRmsPx is not None and rms_px > args.maxRmsPx:
print(f"[WARN] Final reprojection RMS is {rms_px:.3f}px (threshold {args.maxRmsPx:.3f}px).")
# Convert outputs
roll, pitch, yaw = R_to_euler_zyx(R_wc)
position_mm = (camera_center_world * 1000.0).astype(float).tolist()
# Reproject each used marker center for QA
per_marker_results = []
proj_pts_exact, _ = cv2.projectPoints(
X_world.reshape(-1, 1, 3).astype(np.float32),
theta_opt[0:3].reshape(3, 1).astype(np.float32),
theta_opt[3:6].reshape(3, 1).astype(np.float32),
K,
D,
)
proj_pts_exact = proj_pts_exact.reshape(-1, 2)
for idx, mid in enumerate(uniq_ids):
x = proj_pts_exact[idx]
err = float(np.linalg.norm(x - obs_px[idx]))
per_marker_results.append({
"marker_id": int(mid),
"observed_center_px": [float(obs_px[idx, 0]), float(obs_px[idx, 1])],
"projected_center_px": [float(x[0]), float(x[1])],
"reprojection_error_px": err,
"confidence": float(uniq_meta[idx]["confidence"]),
})
# Output directory
in_base = os.path.splitext(os.path.basename(detection_path))[0]
out_name = in_base.replace("_aruco_detection", "_camera_pose") + ".json"
if args.outDir is not None:
out_dir = resolve_path(args.outDir)
else:
out_dir = os.path.dirname(detection_path) or "."
os.makedirs(out_dir, exist_ok=True)
out_json = os.path.join(out_dir, out_name)
output = {
"schema_version": "1.0",
"created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"source": {
"detection_json": detection_path,
"robot_json": robot_path,
},
"camera": {
"camera_id": detection.get("camera", {}).get("camera_id", "unknown"),
"camera_matrix": K.tolist(),
"distortion_coefficients": D.reshape(-1).tolist(),
},
"estimation": {
"method": "single_camera_marker_center_lm",
"description": "Rigid init from per-marker pose estimates, followed by LM on normalized marker-center reprojection residuals.",
"marker_size_m": float(marker_size_m),
"num_used_markers": int(len(uniq_ids)),
"used_marker_ids": [int(x) for x in uniq_ids],
"history": hist,
"residual_rms_px": float(rms_px),
"residual_median_px": float(median_px),
"residual_max_px": float(max_px),
"sigma2_normalized": float(sigma2),
},
"camera_pose": {
"world_to_camera": {
"rotation_matrix": R_wc.tolist(),
"translation_m": [float(x) for x in t_wc.tolist()],
"rvec_rad": [float(x) for x in theta_opt[0:3].tolist()],
},
"camera_in_world": {
"position_m": [float(x) for x in camera_center_world.tolist()],
"position_mm": [float(x) for x in position_mm],
"orientation_deg": {
"roll": float(roll),
"pitch": float(pitch),
"yaw": float(yaw),
},
},
"uncertainty": propagated,
},
"observations": {
"markers": per_marker_results,
},
"qa": {
"sanity_notes": sanity_notes,
},
}
save_json(out_json, output)
print(f"[INFO] Saved camera pose JSON: {out_json}")
if __name__ == "__main__":
try:
main()
except Exception as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
sys.exit(1)

View File

@@ -0,0 +1,685 @@
#!/usr/bin/env python3
"""
2_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
NEU:
- Marker-Orientierungen unterstützt
- Default: Board-Marker zeigen nach +Z
- Qualitätsbewertung erweitert
- Speichert ALLE erkannten Marker
- Speichert auch fehlgeschlagene Lösungen
- Bewertet Kamerageometrie
- Bewertet Markerabdeckung
- Bewertet Sichtwinkel
- Bewertet Markeranzahl
- Speichert vollständige Rohdaten
Benötigt:
pip install opencv-python numpy
"""
import argparse
import json
import math
from pathlib import Path
import cv2
import numpy as np
# ============================================================
# Hilfsfunktionen
# ============================================================
def normalize(v):
n = np.linalg.norm(v)
if n < 1e-9:
return v
return v / n
def rotation_matrix_from_axes(x_axis, y_axis, z_axis):
R = np.column_stack([
normalize(x_axis),
normalize(y_axis),
normalize(z_axis)
])
return R.astype(np.float32)
def rvec_tvec_to_camera_pose(rvec, tvec):
"""
OpenCV:
X_cam = R * X_world + t
Kamera im Weltkoordinatensystem:
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):
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
# ============================================================
# Marker-Orientierung
# ============================================================
def get_marker_rotation(marker):
# Explizite Rotation vorhanden?
if "rotation_matrix" in marker:
return np.array(
marker["rotation_matrix"],
dtype=np.float32
)
# Default:
# Marker zeigt nach +Z
#
# x = rechts
# y = oben
# z = aus Board heraus (+Z)
x_axis = np.array([1, 0, 0], dtype=np.float32)
y_axis = np.array([0, 1, 0], dtype=np.float32)
z_axis = np.array([0, 0, 1], dtype=np.float32)
return rotation_matrix_from_axes(
x_axis,
y_axis,
z_axis
)
# ============================================================
# Marker Lookup
# ============================================================
def build_marker_lookup(robot_data):
marker_lookup = {}
markers = []
# Neues Format: links -> Board -> markers
links = robot_data.get("links", {})
board = links.get("Board")
if board and "markers" in board:
markers = board["markers"]
# Fallback für altes Format
if not markers:
markers = robot_data.get("Marker", [])
for marker in markers:
marker_id = int(marker.get("id", -1))
if marker_id < 0:
continue
# Nur absolute Marker verwenden
if "position" not in marker:
continue
pos = marker["position"]
if pos is None:
continue
if len(pos) != 3:
continue
rotation = get_marker_rotation(marker)
marker_lookup[marker_id] = {
"position": np.array(
pos,
dtype=np.float32
),
"rotation": rotation,
"on": marker.get("on", "unknown")
}
return marker_lookup
# ============================================================
# Marker-Eckpunkte
# ============================================================
def build_marker_object_points(
marker_center_world,
marker_rotation,
marker_size_m):
half = marker_size_m / 2.0
# OpenCV Corner Reihenfolge
local = np.array([
[-half, half, 0.0],
[ half, half, 0.0],
[ half, -half, 0.0],
[-half, -half, 0.0],
], dtype=np.float32)
rotated = (marker_rotation @ local.T).T
return rotated + marker_center_world.reshape(1, 3)
# ============================================================
# Qualitätsmetriken
# ============================================================
def compute_marker_spread(points_3d):
if len(points_3d) < 2:
return 0.0
mins = np.min(points_3d, axis=0)
maxs = np.max(points_3d, axis=0)
diag = np.linalg.norm(maxs - mins)
return float(diag)
def compute_viewing_angles(
camera_position,
marker_lookup,
used_markers):
results = []
for marker_id in used_markers:
marker = marker_lookup[marker_id]
pos = marker["position"]
R = marker["rotation"]
normal = R[:, 2]
to_camera = normalize(camera_position - pos)
dot = np.clip(
np.dot(normal, to_camera),
-1.0,
1.0
)
angle = math.degrees(math.acos(dot))
results.append(angle)
if len(results) == 0:
return {
"mean": None,
"max": None
}
return {
"mean": float(np.mean(results)),
"max": float(np.max(results))
}
def compute_pose_quality(
rms,
max_err,
num_markers,
marker_spread,
viewing_angle_mean):
score = 100.0
# Reprojection Error
score -= rms * 8.0
# Max Error
score -= max_err * 2.0
# Wenige Marker
if num_markers < 2:
score -= 50
elif num_markers < 4:
score -= 25
elif num_markers < 6:
score -= 10
# Schlechte räumliche Verteilung
if marker_spread < 0.10:
score -= 30
elif marker_spread < 0.25:
score -= 15
# Schlechter Blickwinkel
if viewing_angle_mean is not None:
if viewing_angle_mean > 70:
score -= 25
elif viewing_angle_mean > 50:
score -= 10
score = max(0.0, min(100.0, score))
return float(score)
# ============================================================
# Main
# ============================================================
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--detections",
default=None
)
parser.add_argument(
"--robots",
required=True
)
parser.add_argument(
"--min-confidence",
type=float,
default=0.5
)
parser.add_argument(
"--max-reprojection-error",
type=float,
default=3.0
)
parser.add_argument(
"--outDir",
default=None
)
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
# ============================================================
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)
# ============================================================
# Marker laden
# ============================================================
known_markers = build_marker_lookup(robot_data)
# ============================================================
# Punktlisten
# ============================================================
object_points = []
image_points = []
used_markers = []
rejected_markers = []
detections = detection_data["detections"]
for det in detections:
marker_id = int(det["marker_id"])
confidence = float(
det.get("confidence", 1.0)
)
reason = None
if confidence < args.min_confidence:
reason = "low_confidence"
elif marker_id not in known_markers:
reason = "unknown_marker"
if reason is not None:
rejected_markers.append({
"marker_id": marker_id,
"reason": reason,
"confidence": confidence
})
continue
marker_info = known_markers[marker_id]
marker_center_world = marker_info["position"]
marker_rotation = marker_info["rotation"]
marker_size = float(
det["marker_size_m"]
)
obj_pts = build_marker_object_points(
marker_center_world,
marker_rotation,
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)
# ============================================================
# Ausgabe vorbereiten
# ============================================================
out = {
"success": False,
"camera_pose": None,
"quality": {},
"used_markers": [],
"rejected_markers": rejected_markers,
"all_detected_markers": [
int(d["marker_id"])
for d in detections
]
}
# ============================================================
# Zu wenige Marker
# ============================================================
if len(object_points) == 0:
out["quality"] = {
"error": "no_known_markers",
"num_detected_markers": len(detections),
"num_used_markers": 0
}
if args.outDir:
out_dir = Path(args.outDir)
out_dir.mkdir(parents=True, exist_ok=True)
out_file = out_dir / (
Path(args.detections).stem + ".camera_pose.json"
)
else:
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("Keine bekannten Marker gefunden.")
print(out_file)
return
# ============================================================
# solvePnP
# ============================================================
object_points = np.concatenate(
object_points,
axis=0
)
image_points = np.concatenate(
image_points,
axis=0
)
success, rvec, tvec = cv2.solvePnP(
object_points,
image_points,
K,
D,
flags=cv2.SOLVEPNP_ITERATIVE
)
if not success:
out["quality"] = {
"error": "solvepnp_failed",
"num_used_markers": len(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("solvePnP fehlgeschlagen")
return
# ============================================================
# Kamera Pose
# ============================================================
R_wc, cam_pos = rvec_tvec_to_camera_pose(
rvec,
tvec
)
roll, pitch, yaw = rotation_matrix_to_euler_zyx(
R_wc
)
# ============================================================
# Reprojektion
# ============================================================
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 = float(
np.sqrt(np.mean(reproj_error ** 2))
)
max_err = float(
np.max(reproj_error)
)
# ============================================================
# Qualität
# ============================================================
marker_positions = np.array([
known_markers[mid]["position"]
for mid in set(used_markers)
])
marker_spread = compute_marker_spread(
marker_positions
)
viewing = compute_viewing_angles(
cam_pos,
known_markers,
set(used_markers)
)
quality_score = compute_pose_quality(
rms,
max_err,
len(set(used_markers)),
marker_spread,
viewing["mean"]
)
# ============================================================
# Ausgabe
# ============================================================
print()
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")
print(f"RMS = {rms:.3f}px")
print(f"MAX = {max_err:.3f}px")
print()
print("Quality")
print(f"Score = {quality_score:.1f}/100")
print(f"Marker Spread = {marker_spread:.3f}m")
if viewing["mean"] is not None:
print(
f"Mean Viewing Angle = "
f"{viewing['mean']:.1f}deg"
)
# ============================================================
# JSON speichern
# ============================================================
out["success"] = True
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),
},
"rotation_matrix_world_from_camera": (
R_wc.tolist()
)
}
out["quality"] = {
"pose_quality_score": quality_score,
"reprojection_rms_px": rms,
"reprojection_max_px": max_err,
"num_detected_markers": len(detections),
"num_used_markers": len(set(used_markers)),
"marker_spread_m": marker_spread,
"mean_viewing_angle_deg": viewing["mean"],
"max_viewing_angle_deg": viewing["max"],
"pose_stable":
max_err <= args.max_reprojection_error
}
out["used_markers"] = sorted(
list(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()
print("Gespeichert:")
print(out_file)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +0,0 @@
{
"schema_version": "1.0",
"created_utc": "2026-05-28T21:28:35.973154Z",
"source_robot_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\robot.json",
"source_detections": [
"C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1a_aruco_detection.json",
"C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1b_aruco_detection.json",
"C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1c_aruco_detection.json"
],
"solver": {
"final_cost": 26976.87543820547,
"status": 0,
"message": "The maximum number of function evaluations is exceeded."
},
"robot_pose": {
"state": {
"x": 100.21525656040845,
"y": 29.397626553503173,
"z": -36.04825291833897,
"a": -120.21337951359496,
"b": 22.00000000000008,
"c": 90.99999999999989,
"e": 10.000000000000258
},
"uncertainty": {
"x_mm": 17194.235819424397,
"y_mm": 6014.1408132596725,
"z_mm": 3621.298501957444,
"a_deg": 8796.009134059024,
"b_deg": 12562.017200522667,
"c_deg": 12562.017199380893,
"e_mm": 125620.17196327279
},
"confidence": {
"x": 0.0,
"y": 6.444409678452354e-262,
"z": 5.358019964976179e-158,
"a": 0.0,
"b": 0.0,
"c": 0.0,
"e": 0.36787944117144233
}
},
"statistics": {
"observation_count": 34,
"camera_count": 3,
"marker_count": 23,
"observed_marker_count": 18,
"mean_detector_confidence": 0.5871913728875101,
"mean_weighted_confidence": 0.5774319023193206,
"mean_reprojection_error_px": 236.06726174073253,
"quality_means": {
"size": 0.8143773112577551,
"aspect": 0.834768084426124,
"border": 0.9237745098039215,
"homography": 0.7934741744632792
},
"quality_config": {
"size_ref_px": 50.0,
"border_ref_px": 120.0,
"center_ref_norm": 0.01,
"sharpness_ref": 2500.0,
"homography_ref": 0.18,
"size_factor": 0.01,
"aspect_factor": 0.01,
"border_factor": 0.01,
"center_factor": 0.01,
"sharpness_factor": 0.01,
"homography_factor": 0.01
}
}
}

921
pipeline/readTwoImages.py Normal file
View File

@@ -0,0 +1,921 @@
#!/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()

BIN
pipeline/render_2d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
pipeline/render_3a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,329 @@
{
"schema_version": "1.0",
"created_utc": "2026-05-29T05:34:16Z",
"source": {
"detection_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_3a_aruco_detection.json",
"robot_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\robot.json"
},
"camera": {
"camera_id": "cam1",
"camera_matrix": [
[
1777.77783203125,
0.0,
640.0
],
[
0.0,
1500.0,
360.0
],
[
0.0,
0.0,
1.0
]
],
"distortion_coefficients": [
0.0,
0.0,
0.0,
0.0,
0.0
]
},
"estimation": {
"method": "single_camera_marker_center_lm",
"description": "Rigid init from per-marker pose estimates, followed by LM on normalized marker-center reprojection residuals.",
"marker_size_m": 0.025,
"num_used_markers": 4,
"used_marker_ids": [
210,
205,
206,
207
],
"history": {
"iters": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27
],
"rms": [
1.0114134663676055,
0.6639182528097215,
0.5739469492087472,
0.5286835626522526,
0.4694051705223743,
0.3969964464975006,
0.3151761230050886,
0.22825811420039874,
0.14334708153431971,
0.07618149778337073,
0.04802982967284346,
0.04035287824536157,
0.033959902522562355,
0.028057435706365975,
0.022767087572099415,
0.018281990481919433,
0.01422702734760535,
0.01058775888038278,
0.007566043482152367,
0.005123936904466261,
0.003261157779092087,
0.0018882632118665536,
0.0007684323923658777,
0.0001398894364081004,
4.2590187754836205e-05,
4.161871278906256e-05,
4.161660887281736e-05,
4.16166073463803e-05
],
"lambda": [
0.001,
0.0005,
0.00025,
0.000125,
6.25e-05,
3.125e-05,
1.5625e-05,
7.8125e-06,
3.90625e-06,
1.953125e-06,
9.765625e-07,
4.8828125e-07,
2.44140625e-07,
1.220703125e-07,
6.103515625e-08,
3.0517578125e-08,
1.52587890625e-08,
7.62939453125e-09,
3.814697265625e-09,
1.9073486328125e-09,
9.5367431640625e-10,
4.76837158203125e-10,
2.384185791015625e-10,
1.1920928955078125e-10,
5.960464477539063e-11,
2.980232238769531e-11,
1.4901161193847657e-11,
7.450580596923828e-12
]
},
"residual_rms_px": 0.10430687360172344,
"residual_median_px": 0.07433889289506992,
"residual_max_px": 0.1640622076531485,
"sigma2_normalized": 6.92776802797649e-09
},
"camera_pose": {
"world_to_camera": {
"rotation_matrix": [
[
0.7620593309402466,
-0.647443413734436,
0.009086735546588898
],
[
-0.27515384554862976,
-0.33650344610214233,
-0.9005863666534424
],
[
0.5861364603042603,
0.6837999820709229,
-0.4345821440219879
]
],
"translation_m": [
-273.4744567871094,
192.1861572265625,
922.5557250976562
],
"rvec_rad": [
1.9264447184295097,
-0.7016308556001063,
0.45266440822553355
]
},
"camera_in_world": {
"position_m": [
-279.4590148925781,
-743.2315063476562,
576.491455078125
],
"position_mm": [
-279459.0,
-743231.5,
576491.4375
],
"orientation_deg": {
"roll": 122.43758392333984,
"pitch": -35.88331624307256,
"yaw": -19.852935791015625
}
},
"uncertainty": {
"pose_covariance_6x6": [
[
3.9943919468012445e-06,
1.1154473298546209e-06,
-1.118897886488766e-06,
-0.0005765194014531353,
0.00041358722245405364,
0.0022187093112645048
],
[
1.1154473298546145e-06,
8.886589103748773e-07,
-2.3371494827736547e-06,
-0.0005830464121191728,
0.00045167407920918234,
0.0022451714145222145
],
[
-1.1188978864887337e-06,
-2.337149482773653e-06,
7.81346071908241e-06,
0.0017024049446432569,
-0.0013917849716835104,
-0.006611198495461098
],
[
-0.0005765194014531287,
-0.0005830464121191726,
0.0017024049446432584,
0.4034039987420133,
-0.3201198249762993,
-1.5516686677679883
],
[
0.00041358722245404654,
0.0004516740792091824,
-0.001391784971683514,
-0.3201198249762996,
0.264775180218034,
1.2494317575832103
],
[
0.002218709311264482,
0.002245171414522214,
-0.006611198495461105,
-1.5516686677679885,
1.2494317575832097,
6.052706569330424
]
],
"parameter_std": {
"rvec_std_deg": [
0.11451120140577908,
0.05401199046483257,
0.16015641874171344
],
"tvec_std_m": [
0.6351409282529455,
0.51456309644011,
2.460224902184844
]
},
"camera_center_std_m": [
2.1995968929483474,
1.1548044194575717,
1.510385159357917
],
"camera_center_std_mm": [
2199.5968929483474,
1154.8044194575718,
1510.385159357917
],
"orientation_std_deg": {
"roll": 0.08832300327411201,
"pitch": 0.13890967443140134,
"yaw": 0.020881373905939325
}
}
},
"observations": {
"markers": [
{
"marker_id": 210,
"observed_center_px": [
166.25,
674.75
],
"projected_center_px": [
166.26881408691406,
674.7455444335938
],
"reprojection_error_px": 0.019334475384928378,
"confidence": 0.23718801109435655
},
{
"marker_id": 205,
"observed_center_px": [
1127.0,
378.25
],
"projected_center_px": [
1127.124267578125,
378.265380859375
],
"reprojection_error_px": 0.12521582091799144,
"confidence": 0.19676029488014599
},
{
"marker_id": 206,
"observed_center_px": [
953.25,
379.0
],
"projected_center_px": [
953.086181640625,
379.0089416503906
],
"reprojection_error_px": 0.1640622076531485,
"confidence": 0.23511362818048806
},
{
"marker_id": 207,
"observed_center_px": [
1039.5,
347.75
],
"projected_center_px": [
1039.5140380859375,
347.731201171875
],
"reprojection_error_px": 0.023461964872148418,
"confidence": 0.18076863774153512
}
]
},
"qa": {
"sanity_notes": []
}
}

BIN
pipeline/render_3b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,400 @@
{
"schema_version": "1.0",
"created_utc": "2026-05-29T05:34:16Z",
"source": {
"detection_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_3b_aruco_detection.json",
"robot_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\robot.json"
},
"camera": {
"camera_id": "cam1",
"camera_matrix": [
[
1777.77783203125,
0.0,
640.0
],
[
0.0,
1500.0,
360.0
],
[
0.0,
0.0,
1.0
]
],
"distortion_coefficients": [
0.0,
0.0,
0.0,
0.0,
0.0
]
},
"estimation": {
"method": "single_camera_marker_center_lm",
"description": "Rigid init from per-marker pose estimates, followed by LM on normalized marker-center reprojection residuals.",
"marker_size_m": 0.025,
"num_used_markers": 5,
"used_marker_ids": [
208,
215,
214,
211,
210
],
"history": {
"iters": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46
],
"rms": [
14.818157274424525,
6.6429684472286725,
3.508293175019171,
1.9569659577739633,
1.0760750577807272,
0.6558111727688115,
0.5974384941473672,
0.45776430515731414,
0.3406747506535104,
0.21284192970803506,
0.21284192970803506,
0.21284192970803506,
0.21284192970803506,
0.21284192970803506,
0.13834869751593928,
0.13834869751593928,
0.12196764428961278,
0.12196764428961278,
0.12196764428961278,
0.11109006687857191,
0.11109006687857191,
0.11109006687857191,
0.10513254832847944,
0.10513254832847944,
0.10009463133300968,
0.0972011239152783,
0.08965747726813587,
0.07896966206278197,
0.06848004274985553,
0.058568387282928645,
0.04925640249856142,
0.040730620366500596,
0.033135104063634416,
0.02650247419978119,
0.020790086792429628,
0.015941755983382246,
0.012075496055238399,
0.010697420477957333,
0.010697420477957333,
0.010697420477957333,
0.010249775295046573,
0.010249775295046573,
0.010217299105490024,
0.003080951472807064,
0.0029219478792723522,
0.002921843589954249,
0.0029218434666077736
],
"lambda": [
0.001,
0.0005,
0.00025,
0.000125,
6.25e-05,
3.125e-05,
1.5625e-05,
7.8125e-06,
3.90625e-06,
1.953125e-06,
3.90625e-06,
7.8125e-06,
1.5625e-05,
3.125e-05,
1.5625e-05,
3.125e-05,
1.5625e-05,
3.125e-05,
6.25e-05,
3.125e-05,
6.25e-05,
0.000125,
6.25e-05,
0.000125,
6.25e-05,
3.125e-05,
1.5625e-05,
7.8125e-06,
3.90625e-06,
1.953125e-06,
9.765625e-07,
4.8828125e-07,
2.44140625e-07,
1.220703125e-07,
6.103515625e-08,
3.0517578125e-08,
1.52587890625e-08,
7.62939453125e-09,
1.52587890625e-08,
3.0517578125e-08,
1.52587890625e-08,
3.0517578125e-08,
1.52587890625e-08,
7.62939453125e-09,
3.814697265625e-09,
1.9073486328125e-09,
9.5367431640625e-10
]
},
"residual_rms_px": 7.325988977382018,
"residual_median_px": 5.2610158050715405,
"residual_max_px": 11.95199194038243,
"sigma2_normalized": 2.1342923107320546e-05
},
"camera_pose": {
"world_to_camera": {
"rotation_matrix": [
[
-0.9725239872932434,
0.033903129398822784,
-0.23032091557979584
],
[
-0.10051494091749191,
0.8312227725982666,
0.5467773079872131
],
[
0.20998544991016388,
0.5549046993255615,
-0.8049763441085815
]
],
"translation_m": [
180.4925537109375,
-45.28935623168945,
-859.5316162109375
],
"rvec_rad": [
-0.05955419353489003,
3.226373471557783,
0.9849571463938018
]
},
"camera_in_world": {
"position_m": [
351.47021484375,
508.4844055175781,
-625.5681762695312
],
"position_mm": [
351470.21875,
508484.40625,
-625568.1875
],
"orientation_deg": {
"roll": 145.41983032226562,
"pitch": -12.121499395069357,
"yaw": -174.09915161132812
}
},
"uncertainty": {
"pose_covariance_6x6": [
[
0.002350044145762458,
0.0018448663795067805,
0.002791262415386132,
0.044346229182918716,
-0.07916151257588579,
-0.14927480851129382
],
[
0.0018448663795067872,
0.0036429204877860334,
-0.001238434463788633,
0.006197854651266595,
0.030206644893667412,
-0.35477216802776246
],
[
0.002791262415386112,
-0.00123843446378866,
0.015531235537186557,
0.08030698335197134,
-0.04481294954598946,
0.2747388184172584
],
[
0.04434622918291879,
0.006197854651266368,
0.08030698335197246,
5.115947766978903,
-1.7296342108112102,
11.501531197034321
],
[
-0.07916151257588577,
0.030206644893667495,
-0.044812949545989626,
-1.7296342108112113,
17.275134170996235,
15.481414615323398
],
[
-0.14927480851129452,
-0.3547721680277628,
0.2747388184172568,
11.5015311970343,
15.481414615323407,
179.23527748073005
]
],
"parameter_std": {
"rvec_std_deg": [
2.777542050899846,
3.458179074295007,
7.14045087126885
],
"tvec_std_m": [
2.261846097102741,
4.156336628690731,
13.387878005148167
]
},
"camera_center_std_m": [
52.59102600708334,
50.66008686135971,
43.3784726294464
],
"camera_center_std_mm": [
52591.02600708335,
50660.08686135971,
43378.4726294464
],
"orientation_std_deg": {
"roll": 4.057851126238033,
"pitch": 3.771174837779904,
"yaw": 1.7875669447691551
}
}
},
"observations": {
"markers": [
{
"marker_id": 208,
"observed_center_px": [
995.5,
638.0
],
"projected_center_px": [
986.5569458007812,
638.244384765625
],
"reprojection_error_px": 8.946392698950438,
"confidence": 0.6072727689079809
},
{
"marker_id": 215,
"observed_center_px": [
764.5,
612.5
],
"projected_center_px": [
776.3766479492188,
613.8399047851562
],
"reprojection_error_px": 11.95199194038243,
"confidence": 0.6168610191628993
},
{
"marker_id": 214,
"observed_center_px": [
996.0,
527.75
],
"projected_center_px": [
999.8966064453125,
527.873291015625
],
"reprojection_error_px": 3.898556459022334,
"confidence": 0.5524643209674667
},
{
"marker_id": 211,
"observed_center_px": [
783.0,
506.25
],
"projected_center_px": [
777.8927612304688,
504.9873046875
],
"reprojection_error_px": 5.2610158050715405,
"confidence": 0.5730650050844824
},
{
"marker_id": 210,
"observed_center_px": [
312.75,
470.75
],
"projected_center_px": [
311.1917419433594,
470.3533630371094
],
"reprojection_error_px": 1.6079455996446261,
"confidence": 0.5452330001368535
}
]
},
"qa": {
"sanity_notes": []
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
pipeline/render_3c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,390 @@
{
"schema_version": "1.0",
"created_utc": "2026-05-29T05:34:17Z",
"source": {
"detection_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_3c_aruco_detection.json",
"robot_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\robot.json"
},
"camera": {
"camera_id": "cam1",
"camera_matrix": [
[
1777.77783203125,
0.0,
640.0
],
[
0.0,
1500.0,
360.0
],
[
0.0,
0.0,
1.0
]
],
"distortion_coefficients": [
0.0,
0.0,
0.0,
0.0,
0.0
]
},
"estimation": {
"method": "single_camera_marker_center_lm",
"description": "Rigid init from per-marker pose estimates, followed by LM on normalized marker-center reprojection residuals.",
"marker_size_m": 0.025,
"num_used_markers": 3,
"used_marker_ids": [
214,
215,
211
],
"history": {
"iters": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52
],
"rms": [
8.276652092351446,
3.2695419276393194,
1.6997554577172982,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.9031353472765118,
0.8761012206869034,
0.8761012206869034,
0.8761012206869034,
0.6657758310157231,
0.4982919577482978,
0.463899810578019,
0.4620191377323524,
0.46061835425038966,
0.45820325758845926,
0.4535628754664195,
0.44440988606652543,
0.42668189732885436,
0.39303163296273724,
0.33250807541862465,
0.2873217198737302,
0.2310754990881048,
0.1683327418729557,
0.12140389664238704,
0.091031135899403,
0.06974953807273628,
0.05302622844733477,
0.039719619692176966,
0.037288115046028705,
0.019479927098974645,
0.009348648211600379,
0.005670126166140046,
0.004435173697432046,
0.003309583557122678,
0.0021075079585650964,
0.0010750386520058455,
0.00038672742677653426,
8.049312829495705e-05,
9.15644812387087e-06,
5.813634591654259e-07,
1.9407836813260934e-08,
3.2977852013459893e-10,
2.8258286470569123e-12,
1.2165664095469026e-14
],
"lambda": [
0.001,
0.0005,
0.00025,
0.000125,
0.00025,
0.0005,
0.001,
0.002,
0.004,
0.008,
0.016,
0.032,
0.064,
0.128,
0.256,
0.128,
0.256,
0.512,
0.256,
0.128,
0.064,
0.032,
0.016,
0.008,
0.004,
0.002,
0.001,
0.0005,
0.00025,
0.000125,
6.25e-05,
3.125e-05,
1.5625e-05,
7.8125e-06,
3.90625e-06,
1.953125e-06,
9.765625e-07,
4.8828125e-07,
2.44140625e-07,
1.220703125e-07,
6.103515625e-08,
3.0517578125e-08,
1.52587890625e-08,
7.62939453125e-09,
3.814697265625e-09,
1.9073486328125e-09,
9.5367431640625e-10,
4.76837158203125e-10,
2.384185791015625e-10,
1.1920928955078125e-10,
5.960464477539063e-11,
2.980232238769531e-11,
1.4901161193847657e-11
]
},
"residual_rms_px": 0.0,
"residual_median_px": 0.0,
"residual_max_px": 0.0,
"sigma2_normalized": 9.648909021380052e-32
},
"camera_pose": {
"world_to_camera": {
"rotation_matrix": [
[
0.7478234767913818,
0.5858157873153687,
-0.3123779296875
],
[
0.6608203053474426,
-0.6115613579750061,
0.43509677052497864
],
[
0.06384828686714172,
-0.5318012237548828,
-0.8444588780403137
]
],
"translation_m": [
-38.69017028808594,
-76.18257904052734,
733.07861328125
],
"rvec_rad": [
3.428568125507585,
1.3340777852363335,
-0.2659620236785496
]
},
"camera_in_world": {
"position_m": [
32.47060012817383,
365.9270935058594,
640.1156005859375
],
"position_mm": [
32470.599609375,
365927.09375,
640115.625
],
"orientation_deg": {
"roll": -147.79916381835938,
"pitch": -3.660727574305826,
"yaw": 41.46568298339844
}
},
"uncertainty": {
"pose_covariance_6x6": [
[
8.629612631969329e-29,
6.419343182143809e-30,
1.7908295924670243e-29,
-7.073499368795134e-27,
5.6187259819083295e-27,
-2.0666865847138868e-26
],
[
6.419343182143446e-30,
3.051760893870683e-29,
4.68511451162592e-29,
1.5261835153152037e-27,
-5.226262008292569e-28,
-1.103919177657209e-26
],
[
1.7908295924670055e-29,
4.685114511625924e-29,
2.0846170216876133e-28,
6.842689966147899e-29,
3.484907360650631e-27,
-3.678530361558646e-26
],
[
-7.073499368795157e-27,
1.5261835153151694e-27,
6.842689966146321e-29,
8.882557859237887e-25,
-4.5534571307576935e-25,
1.9082917888723267e-24
],
[
5.618725981908361e-27,
-5.226262008292345e-28,
3.484907360650649e-27,
-4.553457130757711e-25,
6.148749838486226e-25,
-9.837708583024273e-25
],
[
-2.0666865847138762e-26,
-1.1039191776572181e-26,
-3.678530361558645e-26,
1.9082917888723113e-24,
-9.837708583024117e-25,
1.3630824927229218e-23
]
],
"parameter_std": {
"rvec_std_deg": [
5.322532023987312e-13,
3.1651761469334046e-13,
8.272480883107961e-13
],
"tvec_std_m": [
9.424732282265575e-13,
7.841396456299239e-13,
3.691994708450869e-12
]
},
"camera_center_std_m": [
6.236013873929964e-12,
5.578343371266709e-12,
2.895045264319711e-12
],
"camera_center_std_mm": [
6.2360138739299636e-09,
5.578343371266708e-09,
2.8950452643197107e-09
],
"orientation_std_deg": {
"roll": 4.550535817991357e-13,
"pitch": 4.763282335043431e-13,
"yaw": 1.775861640110364e-13
}
}
},
"observations": {
"markers": [
{
"marker_id": 214,
"observed_center_px": [
1147.5,
678.25
],
"projected_center_px": [
1147.5,
678.25
],
"reprojection_error_px": 0.0,
"confidence": 0.1700115058329765
},
{
"marker_id": 215,
"observed_center_px": [
853.0,
631.5
],
"projected_center_px": [
853.0,
631.5
],
"reprojection_error_px": 0.0,
"confidence": 0.9372326698512834
},
{
"marker_id": 211,
"observed_center_px": [
975.5,
549.5
],
"projected_center_px": [
975.5,
549.5
],
"reprojection_error_px": 0.0,
"confidence": 0.8902520865668376
}
]
},
"qa": {
"sanity_notes": []
}
}

View File

@@ -0,0 +1,17 @@
id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg,seen_by
camera 0,462.85,-447.38,679.21,-139.679,-3.166,49.062
camera 1,278.74,-619.38,596.01,-128.837,-0.336,9.419
122,202.24,-295.10,250.81,-28.414,-15.485,1.697,3
198,126.58,-65.51,155.07,-3.661,-5.631,-2.911,3
208,351.02,-86.53,-1.04,-1.448,-0.084,0.377,2
210,-2.56,36.17,-52.21,1.418,0.510,0.839,2
211,250.00,-10.00,3.00,68.426,25.579,18.293,3
214,350.00,-10.00,3.00,-1.702,-0.340,0.760,3
215,250.00,-90.00,3.00,2.880,-1.221,-1.765,3
218,236.59,-245.24,189.57,-58.103,50.616,-52.876,1
219,227.26,-329.71,232.26,-59.187,48.504,-50.906,1
229,124.73,-146.21,161.04,-3.612,-3.364,-1.997,3
243,122.58,-179.86,116.23,67.866,-8.370,-6.945,2
244,246.50,-158.07,135.63,-68.544,55.107,-66.578,1
246,179.04,-106.04,108.88,103.363,42.379,32.981,1
247,143.22,-106.60,110.01,-28.304,-5.147,-2.319,1
1 id,x_mm,y_mm,z_mm,roll_deg,pitch_deg,yaw_deg,seen_by
2 camera 0,462.85,-447.38,679.21,-139.679,-3.166,49.062
3 camera 1,278.74,-619.38,596.01,-128.837,-0.336,9.419
4 122,202.24,-295.10,250.81,-28.414,-15.485,1.697,3
5 198,126.58,-65.51,155.07,-3.661,-5.631,-2.911,3
6 208,351.02,-86.53,-1.04,-1.448,-0.084,0.377,2
7 210,-2.56,36.17,-52.21,1.418,0.510,0.839,2
8 211,250.00,-10.00,3.00,68.426,25.579,18.293,3
9 214,350.00,-10.00,3.00,-1.702,-0.340,0.760,3
10 215,250.00,-90.00,3.00,2.880,-1.221,-1.765,3
11 218,236.59,-245.24,189.57,-58.103,50.616,-52.876,1
12 219,227.26,-329.71,232.26,-59.187,48.504,-50.906,1
13 229,124.73,-146.21,161.04,-3.612,-3.364,-1.997,3
14 243,122.58,-179.86,116.23,67.866,-8.370,-6.945,2
15 244,246.50,-158.07,135.63,-68.544,55.107,-66.578,1
16 246,179.04,-106.04,108.88,103.363,42.379,32.981,1
17 247,143.22,-106.60,110.01,-28.304,-5.147,-2.319,1

View File

@@ -0,0 +1,225 @@
{
"metadata": {
"timestamp": "2026-05-29 07:01:38",
"reference_markers": [
211,
215,
214
],
"dict": "DICT_4X4_250",
"marker_size_mm": 25.0,
"description": "Two-camera joint optimization with triangulation"
},
"cameras": [
{
"id": "camera1",
"position_mm": [
462.85337477011745,
-447.3761981117614,
679.2119813369037
],
"orientation_deg": {
"roll": -139.67859744771047,
"pitch": -3.165957561501241,
"yaw": 49.06230550849313
}
},
{
"id": "camera2",
"position_mm": [
278.74292453701025,
-619.3816023355497,
596.013403520274
],
"orientation_deg": {
"roll": -128.8365839999356,
"pitch": -0.33589695340564535,
"yaw": 9.419081380244078
}
}
],
"markers": [
{
"id": 122,
"position_mm": [
202.24261474609375,
-295.09710693359375,
250.81494140625
],
"orientation_deg": {
"roll": -28.41441452710221,
"pitch": -15.485467478547452,
"yaw": 1.6970935429672367
}
},
{
"id": 198,
"position_mm": [
126.584228515625,
-65.51023864746094,
155.06854248046875
],
"orientation_deg": {
"roll": -3.660633208127074,
"pitch": -5.630630889795624,
"yaw": -2.9106432706678698
}
},
{
"id": 208,
"position_mm": [
351.0157443651476,
-86.53427022011029,
-1.0397490394067344
],
"orientation_deg": {
"roll": -1.4484575811662157,
"pitch": -0.08375253351626927,
"yaw": 0.37677851429577486
}
},
{
"id": 210,
"position_mm": [
-2.558166926849015,
36.16561975926857,
-52.21100279675572
],
"orientation_deg": {
"roll": 1.4182725874800994,
"pitch": 0.5101505675200076,
"yaw": 0.8388746917381826
}
},
{
"id": 211,
"position_mm": [
250.00003051757812,
-10.000012397766113,
3.0000078678131104
],
"orientation_deg": {
"roll": 68.42584362262588,
"pitch": 25.578734156462517,
"yaw": 18.29336709620276
}
},
{
"id": 214,
"position_mm": [
350.0,
-9.999968528747559,
2.999967336654663
],
"orientation_deg": {
"roll": -1.7021011527964998,
"pitch": -0.34049805661256133,
"yaw": 0.7603837134528649
}
},
{
"id": 215,
"position_mm": [
250.0,
-90.00000762939453,
3.000002145767212
],
"orientation_deg": {
"roll": 2.879885512023364,
"pitch": -1.2207582699857618,
"yaw": -1.7652611699290457
}
},
{
"id": 218,
"position_mm": [
236.59325735949972,
-245.23626778778672,
189.57266262534728
],
"orientation_deg": {
"roll": -58.10323420416955,
"pitch": 50.61599751527859,
"yaw": -52.875746653538656
}
},
{
"id": 219,
"position_mm": [
227.2649738848997,
-329.7144822031666,
232.2572485343396
],
"orientation_deg": {
"roll": -59.18660506982057,
"pitch": 48.503782448589995,
"yaw": -50.906238173493115
}
},
{
"id": 229,
"position_mm": [
124.72924041748047,
-146.21078491210938,
161.03932189941406
],
"orientation_deg": {
"roll": -3.6119417177489486,
"pitch": -3.3643207255933993,
"yaw": -1.9966876167092664
}
},
{
"id": 243,
"position_mm": [
122.57701769337837,
-179.85790574360627,
116.23497462026722
],
"orientation_deg": {
"roll": 67.86582375271519,
"pitch": -8.37046200567875,
"yaw": -6.945220224171688
}
},
{
"id": 244,
"position_mm": [
246.4997454888863,
-158.06502830092427,
135.62936814972826
],
"orientation_deg": {
"roll": -68.54419573537868,
"pitch": 55.10689840067641,
"yaw": -66.57844357425445
}
},
{
"id": 246,
"position_mm": [
179.03805942690053,
-106.03594119519106,
108.88294827910639
],
"orientation_deg": {
"roll": 103.36293213539558,
"pitch": 42.37920301941833,
"yaw": 32.98079690795832
}
},
{
"id": 247,
"position_mm": [
143.22372509352726,
-106.60064380982665,
110.0127892418814
],
"orientation_deg": {
"roll": -28.30352249482726,
"pitch": -5.146503410532437,
"yaw": -2.318781544830814
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,10 @@
python3 1_detect_aruco_observations.py --image render_3a.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_3b.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 1_detect_aruco_observations.py --image render_3c.png -npz render.npz -robot ../robot.json -cameraId cam1 -outDir .
python3 2_estimate_camera_from_observations.py --robot ../robot.json --i render_3a_aruco_detection.json --outDir "C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/appRobotRendering/pipeline/"
python3 2_estimate_camera_from_observations.py --robot ../robot.json --i render_3b_aruco_detection.json --outDir "C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/appRobotRendering/pipeline/"
python3 2_estimate_camera_from_observations.py --robot ../robot.json --i render_3c_aruco_detection.json --outDir "C:/Users/kech/SynologyDrive/2026-AppServer-AppRobot/appRobotRendering/pipeline/"

View File

@@ -0,0 +1 @@
python3 readTwoImages.py -i render_3c.png -i render_3b.png -npz render.npz -npz render.npz -settings settingsReadTwoMarkers.json

View File

@@ -0,0 +1,16 @@
{ "coordinateSystem":{
"MarkersUsed":"DICT_4X4_250",
"KnownMarkers":
{
"210": [0.020, -0.020, 0.003],
"211": [0.250, -0.010, 0.003],
"215": [0.250, -0.090, 0.003],
"214": [0.350, -0.010, 0.003],
"208": [0.350, -0.090, 0.003],
"206": [0.650, -0.010, 0.003],
"205": [0.750, -0.090, 0.003],
"207": [0.750, -0.010, 0.003],
"217": [0.650, -0.090, 0.003]
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -16,7 +16,13 @@
"renderingInfo": { "renderingInfo": {
"width": 1280, "width": 1280,
"height": 720, "height": 720,
"cameraPosition": [370, -600, 500], "cameraPosition__1": [-10, -800, 500],
"cameraPosition__2": [-500, 300, 1200],
"cameraPosition__3": [-200, -900, 200],
"cameraPosition__4": [1200, 200, 300],
"cameraPosition_a":[-300, -800,500],
"cameraPosition":[300, -700,500],
"cameraPosition_c":[600, -500,600],
"cameraTarget": [210, -180, 180], "cameraTarget": [210, -180, 180],
"cameraUpVector": [0, 0, 1], "cameraUpVector": [0, 0, 1],
"lightPosition": [-500, -500, 500], "lightPosition": [-500, -500, 500],
@@ -78,10 +84,19 @@
} }
}, },
"defaultPosition": { "defaultPosition": {
"x": 100, "x": 10,
"y": 30, "y": 4,
"z": -30, "z": 20,
"a": -120, "a": 10,
"b": 2,
"c": 9,
"e": 1
},
"defaultPosition____": {
"x": 140,
"y": 46,
"z": -70,
"a": 120,
"b": 22, "b": 22,
"c": 91, "c": 91,
"e": 10 "e": 10
@@ -95,22 +110,22 @@
"c": null, "c": null,
"e": null "e": null
}, },
"multiview_quality": { "multiview_calculation": {
"combine_mode": "mean", "combine_mode": "mean",
"size_ref_px": 50.0, "size_ref_px": 50.0,
"border_ref_px": 120.0, "border_ref_px": 120.0,
"center_ref_norm": 0.01, "center_ref_norm": 0.01,
"sharpness_ref": 2500.0, "sharpness_ref": 2500.0,
"homography_ref": 0.18, "homography_ref": 0.18,
"size_factor": 0.01, "size_factor": 0.3,
"aspect_factor": 0.01, "aspect_factor": 0.3,
"border_factor": 0.01, "border_factor": 0.01,
"center_factor": 0.01, "center_factor": 0.01,
"sharpness_factor": 0.01, "sharpness_factor": 0.5,
"homography_factor": 0.01, "homography_factor": 0.2,
"normal_visibility_factor": 0.01, "normal_visibility_factor": 0.01,
"spin_factor": 0.01, "spin_factor": 0.3,
"weight_floor": 0.01 "weight_floor": 0.3
}, },
"movements": { "movements": {
"x": null, "x": null,

File diff suppressed because it is too large Load Diff