diff --git a/pipeline/3_multiview_bundle_adjustment.py b/pipeline/3_multiview_bundle_adjustment_v1.py similarity index 100% rename from pipeline/3_multiview_bundle_adjustment.py rename to pipeline/3_multiview_bundle_adjustment_v1.py diff --git a/pipeline/3_multiview_bundle_adjustment_v3.py b/pipeline/3_multiview_bundle_adjustment_v3.py new file mode 100644 index 0000000..7376326 --- /dev/null +++ b/pipeline/3_multiview_bundle_adjustment_v3.py @@ -0,0 +1,1466 @@ +#!/usr/bin/env python3 +""" +3_multiview_bundle_adjustment_v3.py + +Multi-view ArUco marker position optimization with rule-based geometric constraints. + +Mathematical model +------------------ +We estimate 3D marker positions X_i ∈ R^3 by minimizing a weighted least-squares objective + + E(X) = Σ_i Σ_c w_ic || π_c(X_i) - u_ic ||^2 + + λ_d Σ_j w_j^d || g_j^d(X) ||^2 + + λ_a Σ_k w_k^a || g_k^a(X) ||^2 + +where: +- u_ic are observed normalized image coordinates of marker i in camera c +- π_c(.) is the normalized reprojection model for camera c +- w_ic are observation weights (quality-based) +- g_j^d are rigid-link distance constraints +- g_k^a are joint-axis / chain-axis projection constraints + +Important design choices: +- robot.json is used as a kinematic description, not as a direct source of marker positions. +- constraint families are explicit and switchable. +- marker observation weights are based on available measurement quality cues + (e.g. detection confidence, observed pixel size if present, marker size prior, + and initial range from the camera); orientation information is treated cautiously. +- optimization is single-stage in this version to keep comparisons reproducible. + If a second refinement stage is needed later, it can be added as a separate version. + +Dependencies: + numpy, opencv-python, scipy (optional for optimization) + +Example: + python 3_multiview_bundle_adjustment_v3.py ^ + -det cam1_aruco_detection.json cam2_aruco_detection.json cam3_aruco_detection.json ^ + -pose cam1_camera_pose.json cam2_camera_pose.json cam3_camera_pose.json ^ + -robot robot.json ^ + -lambdaWeight 100.0 +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from dataclasses import dataclass +from itertools import combinations +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) + + +# =================================================================== +# Units +# =================================================================== + +def get_length_scale(robot_data: Dict[str, Any]) -> float: + units = robot_data.get("units", {}) or {} + length_unit = str(units.get("length", "")).strip().lower() + if length_unit in ("mm", "millimeter", "millimeters"): + return 1.0 / 1000.0 + if length_unit in ("cm", "centimeter", "centimeters"): + return 1.0 / 100.0 + return 1.0 + + +# =================================================================== +# Small geometry helpers +# =================================================================== + +def safe_norm(v: np.ndarray, eps: float = 1e-12) -> float: + return float(np.linalg.norm(v) + eps) + + +def normalize_vector(v: np.ndarray, eps: float = 1e-12) -> np.ndarray: + return np.asarray(v, dtype=np.float64) / safe_norm(v, eps) + + +def clamp(v: float, lo: float, hi: float) -> float: + return float(max(lo, min(hi, v))) + + +def principal_axis_id(axis: np.ndarray, threshold: float = 0.95) -> Optional[int]: + """Return 0,1,2 for x,y,z if axis is close enough to a principal axis.""" + a = normalize_vector(np.asarray(axis, dtype=np.float64)) + idx = int(np.argmax(np.abs(a))) + if abs(a[idx]) >= threshold: + return idx + return None + + +def camera_center_from_world_to_cam(R_wc: np.ndarray, t_wc: np.ndarray) -> np.ndarray: + """world_to_camera: X_cam = R_wc * X_world + t_wc; camera center is -R^T t.""" + return -R_wc.T @ t_wc + + +def principal_axis_vector(axis: np.ndarray) -> np.ndarray: + """Convert a near-principal axis to an exact signed principal axis vector.""" + a = normalize_vector(axis) + idx = int(np.argmax(np.abs(a))) + out = np.zeros(3, dtype=np.float64) + out[idx] = 1.0 if a[idx] >= 0 else -1.0 + return normalize_vector(out) + + +# =================================================================== +# Configuration +# =================================================================== + +@dataclass +class ConstraintRuleConfig: + rigid_distance_enabled: bool = True + rigid_distance_mode: str = "mst" # mst | star | full + rigid_distance_weight: float = 1.0 + + joint_axis_enabled: bool = True + joint_axis_max_pairs: int = 2 + joint_axis_weight: float = 0.5 + + chain_axis_enabled: bool = True + chain_axis_max_depth: int = 3 + chain_axis_max_pairs: int = 2 + chain_axis_weight: float = 0.3 + + axis_alignment_threshold: float = 0.95 + + strict_unique_marker_ids: bool = False + show_skipped_constraints: bool = True + + enable_observation_weights: bool = True + weight_floor: float = 0.30 + weight_ceiling: float = 3.00 + ref_distance_m: float = 0.75 + ref_marker_size_px: float = 50.0 + use_detection_confidence: bool = True + use_detection_size_px: bool = True + use_initial_range: bool = True + use_marker_size_prior: bool = True + + +def _bool_or_default(value: Any, default: bool) -> bool: + if value is None: + return default + return bool(value) + + +def _float_or_default(value: Any, default: float) -> float: + if value is None: + return default + return float(value) + + +def _int_or_default(value: Any, default: int) -> int: + if value is None: + return default + return int(value) + + +def load_constraint_rule_config(robot_data: Dict[str, Any], args: argparse.Namespace) -> ConstraintRuleConfig: + rules = robot_data.get("constraint_rules", {}) or {} + + cfg = ConstraintRuleConfig() + rigid = rules.get("rigid_distance", {}) or {} + joint = rules.get("joint_axis_projection", {}) or {} + chain = rules.get("chain_axis_projection", {}) or {} + obs = rules.get("observation_weights", {}) or {} + + cfg.rigid_distance_enabled = _bool_or_default(rigid.get("enabled"), cfg.rigid_distance_enabled) + cfg.rigid_distance_mode = str(rigid.get("mode", cfg.rigid_distance_mode)).strip().lower() + cfg.rigid_distance_weight = _float_or_default(rigid.get("weight"), cfg.rigid_distance_weight) + + cfg.joint_axis_enabled = _bool_or_default(joint.get("enabled"), cfg.joint_axis_enabled) + cfg.joint_axis_max_pairs = _int_or_default(joint.get("max_pairs"), cfg.joint_axis_max_pairs) + cfg.joint_axis_weight = _float_or_default(joint.get("weight"), cfg.joint_axis_weight) + + cfg.chain_axis_enabled = _bool_or_default(chain.get("enabled"), cfg.chain_axis_enabled) + cfg.chain_axis_max_depth = _int_or_default(chain.get("max_depth"), cfg.chain_axis_max_depth) + cfg.chain_axis_max_pairs = _int_or_default(chain.get("max_pairs"), cfg.chain_axis_max_pairs) + cfg.chain_axis_weight = _float_or_default(chain.get("weight"), cfg.chain_axis_weight) + + cfg.axis_alignment_threshold = _float_or_default( + rules.get("axis_alignment_threshold"), cfg.axis_alignment_threshold + ) + + cfg.enable_observation_weights = _bool_or_default(obs.get("enabled"), cfg.enable_observation_weights) + cfg.weight_floor = _float_or_default(obs.get("weight_floor"), cfg.weight_floor) + cfg.weight_ceiling = _float_or_default(obs.get("weight_ceiling"), cfg.weight_ceiling) + cfg.ref_distance_m = _float_or_default(obs.get("ref_distance_m"), cfg.ref_distance_m) + cfg.ref_marker_size_px = _float_or_default(obs.get("ref_marker_size_px"), cfg.ref_marker_size_px) + cfg.use_detection_confidence = _bool_or_default(obs.get("use_detection_confidence"), cfg.use_detection_confidence) + cfg.use_detection_size_px = _bool_or_default(obs.get("use_detection_size_px"), cfg.use_detection_size_px) + cfg.use_initial_range = _bool_or_default(obs.get("use_initial_range"), cfg.use_initial_range) + cfg.use_marker_size_prior = _bool_or_default(obs.get("use_marker_size_prior"), cfg.use_marker_size_prior) + + if getattr(args, "strictUniqueMarkerIds", False): + cfg.strict_unique_marker_ids = True + if getattr(args, "showSkippedConstraints", False): + cfg.show_skipped_constraints = True + if getattr(args, "noShowSkippedConstraints", False): + cfg.show_skipped_constraints = False + + return cfg + + +# =================================================================== +# Observation / constraint definitions +# =================================================================== + +@dataclass +class Observation: + cam_idx: int + norm_coords: np.ndarray + meta: Dict[str, Any] + + +@dataclass +class MarkerDistanceConstraint: + marker_id_a: int + marker_id_b: int + link_name: str + target_distance_m: float + weight: float = 1.0 + enabled: bool = True + source: str = "rigid_distance" + + +@dataclass +class JointAxisConstraint: + marker_id_parent: int + marker_id_child: int + parent_link: str + child_link: str + joint_axis: np.ndarray + target_delta_along_axis_m: float + weight: float = 1.0 + enabled: bool = True + source: str = "joint_axis_projection" + + +Constraint = MarkerDistanceConstraint | JointAxisConstraint + + +# =================================================================== +# Robot parsing +# =================================================================== + +def parse_robot_markers( + robot_data: Dict[str, Any], + length_scale: float, + strict_unique_marker_ids: bool = False +) -> Tuple[Dict[int, str], Dict[str, List[Dict[str, Any]]], List[str], Dict[int, Dict[str, Any]]]: + links = robot_data.get("links", {}) or {} + + marker_to_link: Dict[int, str] = {} + link_markers: Dict[str, List[Dict[str, Any]]] = {} + issues: List[str] = [] + marker_meta: Dict[int, Dict[str, Any]] = {} + + seen_global: Dict[int, str] = {} + + for link_name, link_data in links.items(): + markers = link_data.get("markers", []) or [] + collected: List[Dict[str, Any]] = [] + seen_local: set[int] = set() + + for idx, marker in enumerate(markers): + marker_id = int(marker.get("id", -1)) + pos = marker.get("position", None) + + if marker_id < 0 or pos is None or len(pos) != 3: + issues.append(f"[WARN] link={link_name}: skipped invalid marker entry at index {idx}") + continue + + if marker_id in seen_local: + msg = f"[WARN] duplicate marker id {marker_id} inside link '{link_name}'" + if strict_unique_marker_ids: + raise ValueError(msg) + issues.append(msg + " -> skipped duplicate inside same link") + continue + + if marker_id in seen_global: + msg = ( + f"[WARN] duplicate marker id {marker_id} appears in link '{link_name}' " + f"and already in link '{seen_global[marker_id]}'" + ) + if strict_unique_marker_ids: + raise ValueError(msg) + issues.append(msg + " -> skipped duplicate occurrence") + continue + + seen_local.add(marker_id) + seen_global[marker_id] = link_name + + pos_raw = np.array(pos, dtype=np.float64) + pos_m = pos_raw * float(length_scale) + + item = { + "id": marker_id, + "name": marker.get("name", f"marker_{marker_id}"), + "position_raw": pos_raw, + "position_m": pos_m, + "normal": np.array(marker.get("normal", [0, 0, 1]), dtype=np.float64), + "size": marker.get("size", None), + "spin": marker.get("spin", None), + } + collected.append(item) + marker_to_link[marker_id] = link_name + marker_meta[marker_id] = { + "link_name": link_name, + "name": item["name"], + "position_m": pos_m, + "normal": item["normal"], + "size": item["size"], + "spin": item["spin"], + } + + link_markers[link_name] = collected + + return marker_to_link, link_markers, issues, marker_meta + + +def get_link_parent_map(robot_data: Dict[str, Any]) -> Dict[str, Optional[str]]: + links = robot_data.get("links", {}) or {} + return {link_name: (link_data.get("parent", None)) for link_name, link_data in links.items()} + + +def get_joint_info(robot_data: Dict[str, Any], child_link: str) -> Dict[str, Any]: + links = robot_data.get("links", {}) or {} + return (links.get(child_link, {}) or {}).get("jointToParent", {}) or {} + + +def get_joint_axis(robot_data: Dict[str, Any], child_link: str) -> Optional[np.ndarray]: + joint = get_joint_info(robot_data, child_link) + axis = joint.get("axis", None) + if axis is None: + return None + axis = np.asarray(axis, dtype=np.float64) + if safe_norm(axis) < 1e-12: + return None + return normalize_vector(axis) + + +def get_vision_marker_size_default(robot_data: Dict[str, Any]) -> float: + vision = robot_data.get("vision_config", {}) or {} + ms = vision.get("MarkerSize", None) + if ms is None: + return 0.025 + return float(ms) + + +# =================================================================== +# Constraint compilation helpers +# =================================================================== + +def get_enabled_link_rule( + robot_data: Dict[str, Any], + link_name: str, + rule_name: str, + default_enabled: bool = True +) -> bool: + overrides = robot_data.get("constraint_overrides", {}) or {} + link_override = overrides.get(link_name, {}) or {} + rule_override = link_override.get(rule_name, {}) or {} + if "enabled" in rule_override: + return bool(rule_override["enabled"]) + return default_enabled + + +def select_anchor_marker_ids( + markers: List[Dict[str, Any]], + axis: Optional[np.ndarray] = None, + max_count: int = 2 +) -> List[int]: + if not markers: + return [] + if len(markers) == 1: + return [int(markers[0]["id"])] + + ids = [int(m["id"]) for m in markers] + pos = np.stack([np.asarray(m["position_m"], dtype=np.float64) for m in markers], axis=0) + + selected: List[int] = [] + + if axis is not None and safe_norm(axis) > 1e-12: + a = normalize_vector(axis) + proj = pos @ a + min_idx = int(np.argmin(proj)) + max_idx = int(np.argmax(proj)) + selected = [ids[min_idx], ids[max_idx]] + else: + centroid = np.mean(pos, axis=0) + d = np.linalg.norm(pos - centroid, axis=1) + min_idx = int(np.argmin(d)) + max_idx = int(np.argmax(d)) + selected = [ids[min_idx], ids[max_idx]] + + if len(selected) < max_count: + for mid in ids: + if mid not in selected: + selected.append(mid) + if len(selected) >= max_count: + break + + out: List[int] = [] + seen: set[int] = set() + for mid in selected: + if mid not in seen: + seen.add(mid) + out.append(mid) + if len(out) >= max_count: + break + return out + + +def mst_edges_for_link(markers: List[Dict[str, Any]]) -> List[Tuple[int, int]]: + n = len(markers) + if n < 2: + return [] + + ids = [int(m["id"]) for m in markers] + pos = np.stack([np.asarray(m["position_m"], dtype=np.float64) for m in markers], axis=0) + in_tree = np.zeros(n, dtype=bool) + in_tree[0] = True + edges: List[Tuple[int, int]] = [] + dist = np.linalg.norm(pos[:, None, :] - pos[None, :, :], axis=2) + + for _ in range(n - 1): + best = None + best_d = float("inf") + for i in range(n): + if not in_tree[i]: + continue + for j in range(n): + if in_tree[j]: + continue + d = float(dist[i, j]) + if d < best_d: + best_d = d + best = (i, j) + if best is None: + break + i, j = best + in_tree[j] = True + edges.append((ids[i], ids[j])) + return edges + + +def compile_rigid_distance_constraints( + robot_data: Dict[str, Any], + link_markers: Dict[str, List[Dict[str, Any]]], + cfg: ConstraintRuleConfig +) -> List[MarkerDistanceConstraint]: + constraints: List[MarkerDistanceConstraint] = [] + + for link_name, markers in link_markers.items(): + if not get_enabled_link_rule(robot_data, link_name, "rigid_distance", cfg.rigid_distance_enabled): + continue + if len(markers) < 2: + continue + + mode = cfg.rigid_distance_mode + if mode == "full": + pairs = [(int(a["id"]), int(b["id"])) for a, b in combinations(markers, 2)] + elif mode == "star": + anchor_ids = select_anchor_marker_ids(markers, axis=None, max_count=1) + anchor_id = anchor_ids[0] + pairs = [] + for m in markers: + mid = int(m["id"]) + if mid != anchor_id: + pairs.append((anchor_id, mid)) + elif mode == "mst": + pairs = mst_edges_for_link(markers) + else: + raise ValueError(f"Unknown rigid_distance_mode='{mode}'. Use mst|star|full.") + + pos_map = {int(m["id"]): np.asarray(m["position_m"], dtype=np.float64) for m in markers} + seen_pairs: set[Tuple[int, int]] = set() + + for mid_a, mid_b in pairs: + if mid_a == mid_b: + continue + key = tuple(sorted((int(mid_a), int(mid_b)))) + if key in seen_pairs: + continue + seen_pairs.add(key) + + pos_a = pos_map[mid_a] + pos_b = pos_map[mid_b] + target = float(np.linalg.norm(pos_b - pos_a)) + + constraints.append( + MarkerDistanceConstraint( + marker_id_a=mid_a, + marker_id_b=mid_b, + link_name=link_name, + target_distance_m=target, + weight=cfg.rigid_distance_weight, + enabled=True, + source=f"rigid_distance:{mode}", + ) + ) + + return constraints + + +def compile_direct_joint_axis_constraints( + robot_data: Dict[str, Any], + link_markers: Dict[str, List[Dict[str, Any]]], + cfg: ConstraintRuleConfig +) -> List[JointAxisConstraint]: + constraints: List[JointAxisConstraint] = [] + links = robot_data.get("links", {}) or {} + + for child_link, child_data in links.items(): + parent_link = child_data.get("parent", None) + if not parent_link: + continue + + if not get_enabled_link_rule(robot_data, child_link, "joint_axis_projection", cfg.joint_axis_enabled): + continue + + joint_axis = get_joint_axis(robot_data, child_link) + if joint_axis is None: + continue + + axis_vec = principal_axis_vector(joint_axis) + + parent_markers = link_markers.get(parent_link, []) + child_markers = link_markers.get(child_link, []) + if len(parent_markers) == 0 or len(child_markers) == 0: + continue + + max_pairs = max(1, int(cfg.joint_axis_max_pairs)) + parent_anchor_ids = select_anchor_marker_ids(parent_markers, axis=axis_vec, max_count=max_pairs) + child_anchor_ids = select_anchor_marker_ids(child_markers, axis=axis_vec, max_count=max_pairs) + + parent_pos = {int(m["id"]): np.asarray(m["position_m"], dtype=np.float64) for m in parent_markers} + child_pos = {int(m["id"]): np.asarray(m["position_m"], dtype=np.float64) for m in child_markers} + + seen: set[Tuple[int, int]] = set() + for mid_p in parent_anchor_ids: + for mid_c in child_anchor_ids: + if mid_p == mid_c: + continue + key = (mid_p, mid_c) + if key in seen: + continue + seen.add(key) + + delta = child_pos[mid_c] - parent_pos[mid_p] + target = float(np.dot(delta, axis_vec)) + + constraints.append( + JointAxisConstraint( + marker_id_parent=mid_p, + marker_id_child=mid_c, + parent_link=parent_link, + child_link=child_link, + joint_axis=axis_vec, + target_delta_along_axis_m=target, + weight=cfg.joint_axis_weight, + enabled=True, + source="joint_axis_projection", + ) + ) + + return constraints + + +def ancestors_of(link_name: str, parent_map: Dict[str, Optional[str]], max_depth: int) -> List[str]: + out = [] + cur = parent_map.get(link_name, None) + depth = 0 + while cur is not None and depth < max_depth: + out.append(cur) + cur = parent_map.get(cur, None) + depth += 1 + return out + + +def path_axes_consistent( + robot_data: Dict[str, Any], + ancestor: str, + descendant: str, + parent_map: Dict[str, Optional[str]], + threshold: float +) -> Optional[np.ndarray]: + chain: List[str] = [] + cur = descendant + while cur is not None and cur != ancestor: + chain.append(cur) + cur = parent_map.get(cur, None) + + if cur != ancestor: + return None + + chain.reverse() + axes: List[np.ndarray] = [] + for link in chain: + ax = get_joint_axis(robot_data, link) + if ax is None: + return None + axes.append(ax) + + if not axes: + return None + + axis_ids: List[int] = [] + signs: List[float] = [] + for ax in axes: + aid = principal_axis_id(ax, threshold=threshold) + if aid is None: + return None + axis_ids.append(aid) + signs.append(float(np.sign(ax[aid])) if abs(ax[aid]) > 1e-12 else 1.0) + + if len(set(axis_ids)) != 1: + return None + + axis_id = axis_ids[0] + sign = 1.0 if np.sum(signs) >= 0 else -1.0 + axis_vec = np.zeros(3, dtype=np.float64) + axis_vec[axis_id] = sign + return normalize_vector(axis_vec) + + +def compile_chain_axis_constraints( + robot_data: Dict[str, Any], + link_markers: Dict[str, List[Dict[str, Any]]], + cfg: ConstraintRuleConfig +) -> List[JointAxisConstraint]: + constraints: List[JointAxisConstraint] = [] + parent_map = get_link_parent_map(robot_data) + + links_with_markers = [ln for ln, mk in link_markers.items() if len(mk) > 0] + max_depth = max(1, int(cfg.chain_axis_max_depth)) + max_pairs = max(1, int(cfg.chain_axis_max_pairs)) + + pos_cache: Dict[int, np.ndarray] = {} + for mk in link_markers.values(): + for m in mk: + pos_cache[int(m["id"])] = np.asarray(m["position_m"], dtype=np.float64) + + for descendant in links_with_markers: + if not get_enabled_link_rule(robot_data, descendant, "chain_axis_projection", cfg.chain_axis_enabled): + continue + + for ancestor in ancestors_of(descendant, parent_map, max_depth=max_depth): + if ancestor == descendant: + continue + + axis_vec = path_axes_consistent( + robot_data=robot_data, + ancestor=ancestor, + descendant=descendant, + parent_map=parent_map, + threshold=cfg.axis_alignment_threshold, + ) + if axis_vec is None: + continue + + ancestor_markers = link_markers.get(ancestor, []) + descendant_markers = link_markers.get(descendant, []) + if len(ancestor_markers) == 0 or len(descendant_markers) == 0: + continue + + anc_anchors = select_anchor_marker_ids(ancestor_markers, axis=axis_vec, max_count=max_pairs) + des_anchors = select_anchor_marker_ids(descendant_markers, axis=axis_vec, max_count=max_pairs) + + seen: set[Tuple[int, int]] = set() + for mid_a in anc_anchors: + for mid_b in des_anchors: + if mid_a == mid_b: + continue + key = (mid_a, mid_b) + if key in seen: + continue + seen.add(key) + + target = float(np.dot(pos_cache[mid_b] - pos_cache[mid_a], axis_vec)) + constraints.append( + JointAxisConstraint( + marker_id_parent=mid_a, + marker_id_child=mid_b, + parent_link=ancestor, + child_link=descendant, + joint_axis=axis_vec, + target_delta_along_axis_m=target, + weight=cfg.chain_axis_weight, + enabled=True, + source=f"chain_axis_projection:depth{len(ancestors_of(descendant, parent_map, max_depth))}", + ) + ) + + return constraints + + +def compile_constraints( + robot_data: Dict[str, Any], + length_scale: float, + cfg: ConstraintRuleConfig +) -> Tuple[Dict[int, str], Dict[str, List[Dict[str, Any]]], List[Constraint], List[str], Dict[int, Dict[str, Any]]]: + marker_to_link, link_markers, issues, marker_meta = parse_robot_markers( + robot_data, + length_scale=length_scale, + strict_unique_marker_ids=cfg.strict_unique_marker_ids, + ) + + constraints: List[Constraint] = [] + constraints.extend(compile_rigid_distance_constraints(robot_data, link_markers, cfg)) + constraints.extend(compile_direct_joint_axis_constraints(robot_data, link_markers, cfg)) + constraints.extend(compile_chain_axis_constraints(robot_data, link_markers, cfg)) + + unique_constraints: List[Constraint] = [] + seen_keys: set[Tuple[Any, ...]] = set() + + for c in constraints: + if isinstance(c, MarkerDistanceConstraint): + key = ( + "d", + min(c.marker_id_a, c.marker_id_b), + max(c.marker_id_a, c.marker_id_b), + c.link_name, + round(c.target_distance_m, 9), + ) + else: + key = ( + "j", + c.parent_link, + c.child_link, + c.marker_id_parent, + c.marker_id_child, + tuple(np.round(c.joint_axis, 9).tolist()), + round(c.target_delta_along_axis_m, 9), + ) + if key in seen_keys: + continue + seen_keys.add(key) + unique_constraints.append(c) + + return marker_to_link, link_markers, unique_constraints, issues, marker_meta + + +# =================================================================== +# Observation quality / weighting +# =================================================================== + +def _optional_float(meta: Dict[str, Any], keys: List[str]) -> Optional[float]: + for k in keys: + if k in meta and meta[k] is not None: + try: + return float(meta[k]) + except Exception: + pass + return None + + +def detection_quality_from_metadata(det_obj: Dict[str, Any], cfg: ConstraintRuleConfig) -> float: + q = 1.0 + + if cfg.use_detection_confidence: + conf = _optional_float(det_obj, ["confidence", "score", "quality", "det_confidence"]) + if conf is not None: + q *= clamp(conf, 0.1, 1.0) + + if cfg.use_detection_size_px: + size_px = _optional_float(det_obj, ["size_px", "marker_size_px", "side_px", "side_length_px"]) + if size_px is None and "corners_px" in det_obj and isinstance(det_obj["corners_px"], list): + try: + corners = np.asarray(det_obj["corners_px"], dtype=np.float64).reshape(-1, 2) + if len(corners) >= 4: + edges = [] + for i in range(len(corners)): + p = corners[i] + q2 = corners[(i + 1) % len(corners)] + edges.append(float(np.linalg.norm(q2 - p))) + size_px = float(np.mean(edges)) + except Exception: + size_px = None + if size_px is not None: + q *= clamp(size_px / max(cfg.ref_marker_size_px, 1e-6), 0.25, 3.0) + + sharpness = _optional_float(det_obj, ["sharpness", "corner_sharpness"]) + if sharpness is not None: + q *= clamp(sharpness / 2500.0, 0.5, 1.5) + + normal_alignment = _optional_float(det_obj, ["normal_alignment", "view_cosine", "cos_to_camera"]) + if normal_alignment is not None: + q *= clamp(normal_alignment, 0.3, 1.0) + + return float(q) + + +def marker_size_prior_factor(marker_meta: Dict[str, Any], default_marker_size_m: float) -> float: + size_val = marker_meta.get("size", None) + if size_val is None: + return 1.0 + + try: + size_val = float(size_val) + except Exception: + return 1.0 + + size_m = size_val / 1000.0 if size_val > 1.0 else size_val + ref = max(default_marker_size_m, 1e-6) + return clamp(size_m / ref, 0.7, 1.3) + + +def compute_observation_weights( + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + initial_positions: Dict[int, np.ndarray], + marker_meta: Dict[int, Dict[str, Any]], + cfg: ConstraintRuleConfig, + robot_data: Dict[str, Any] +) -> Dict[Tuple[int, int], float]: + weights: Dict[Tuple[int, int], float] = {} + default_marker_size_m = get_vision_marker_size_default(robot_data) + + for marker_id, obs_list in marker_observations.items(): + X = initial_positions.get(marker_id, None) + size_prior = marker_size_prior_factor(marker_meta.get(marker_id, {}), default_marker_size_m) + + for obs_idx, obs in enumerate(obs_list): + w = 1.0 + q = detection_quality_from_metadata(obs.meta, cfg) + w *= q + + if cfg.use_marker_size_prior: + w *= size_prior + + if cfg.use_initial_range and X is not None: + _, _, R_wc, t_wc = cameras[obs.cam_idx] + C = camera_center_from_world_to_cam(R_wc, t_wc) + dist = float(np.linalg.norm(X - C)) + if np.isfinite(dist): + w *= clamp(cfg.ref_distance_m / max(dist, 1e-6), 0.4, 2.0) + + weights[(marker_id, obs_idx)] = clamp(w, cfg.weight_floor, cfg.weight_ceiling) + + return weights + + +# =================================================================== +# Multi-view loading +# =================================================================== + +def load_observations_and_poses( + detection_files: List[str], + pose_files: List[str] +) -> Tuple[ + Dict[int, List[Observation]], + List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + List[Dict[str, Any]] +]: + if len(detection_files) != len(pose_files): + raise ValueError(f"Mismatch: {len(detection_files)} detections vs {len(pose_files)} poses") + + marker_observations: Dict[int, List[Observation]] = {} + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]] = [] + obs_metadata: List[Dict[str, Any]] = [] + + for cam_idx, (det_file, pose_file) in enumerate(zip(detection_files, pose_files)): + det = load_json(det_file) + pose_data = load_json(pose_file) + + cam_section = det.get("camera", {}) or {} + K = np.array(cam_section.get("camera_matrix", []), dtype=np.float64).reshape(3, 3) + D = np.array(cam_section.get("distortion_coefficients", []), dtype=np.float64).reshape(-1, 1) + + pose_section = pose_data.get("camera_pose", {}) or {} + world_to_cam = pose_section.get("world_to_camera", {}) or {} + R_wc = np.array(world_to_cam.get("rotation_matrix", []), dtype=np.float64).reshape(3, 3) + t_wc = np.array(world_to_cam.get("translation_m", []), dtype=np.float64).reshape(3) + + cameras.append((K, D, R_wc, t_wc)) + + detections = det.get("detections", []) or [] + for det_obj in detections: + marker_id = int(det_obj.get("marker_id", -1)) + if marker_id < 0: + continue + + center_px = np.array(det_obj.get("center_px", []), dtype=np.float64) + if center_px.shape != (2,): + continue + + pts = center_px.reshape(1, 1, 2).astype(np.float32) + und = cv2.undistortPoints(pts, K.astype(np.float32), D.astype(np.float32), P=None) + norm_coords = und.reshape(2).astype(np.float64) + + obs = Observation(cam_idx=cam_idx, norm_coords=norm_coords, meta=dict(det_obj)) + marker_observations.setdefault(marker_id, []).append(obs) + + obs_metadata.append( + { + "detection_file": det_file, + "pose_file": pose_file, + "num_detections": len(detections), + } + ) + + return marker_observations, cameras, obs_metadata + + +# =================================================================== +# Initial triangulation +# =================================================================== + +def triangulate_marker_initial( + marker_id: int, + observations: List[Observation], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]] +) -> Optional[np.ndarray]: + if len(observations) < 2: + return None + + best_pair = None + best_baseline = -1.0 + + for obs_i, obs_j in combinations(observations, 2): + cam_i, cam_j = obs_i.cam_idx, obs_j.cam_idx + _, _, R1, t1 = cameras[cam_i] + _, _, R2, t2 = cameras[cam_j] + c1 = camera_center_from_world_to_cam(R1, t1) + c2 = camera_center_from_world_to_cam(R2, t2) + baseline = float(np.linalg.norm(c2 - c1)) + if baseline > best_baseline: + best_baseline = baseline + best_pair = (obs_i, obs_j) + + if best_pair is None: + return None + + obs_i, obs_j = best_pair + cam_i, cam_j = obs_i.cam_idx, obs_j.cam_idx + norm_coords_i = obs_i.norm_coords + norm_coords_j = obs_j.norm_coords + + K1, D1, R1, t1 = cameras[cam_i] + K2, D2, R2, t2 = cameras[cam_j] + + x1_px = K1[0, 0] * norm_coords_i[0] + K1[0, 2] + y1_px = K1[1, 1] * norm_coords_i[1] + K1[1, 2] + x2_px = K2[0, 0] * norm_coords_j[0] + K2[0, 2] + y2_px = K2[1, 1] * norm_coords_j[1] + K2[1, 2] + + P1 = K1 @ np.hstack([R1, t1.reshape(3, 1)]) + P2 = K2 @ np.hstack([R2, t2.reshape(3, 1)]) + + try: + X_h = cv2.triangulatePoints( + P1, + P2, + np.array([[x1_px], [y1_px]], dtype=np.float64), + np.array([[x2_px], [y2_px]], dtype=np.float64), + ) + X = (X_h[:3] / X_h[3]).reshape(3).astype(np.float64) + if not np.all(np.isfinite(X)): + return None + return X + except Exception: + return None + + +def initial_triangulation( + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]] +) -> Dict[int, np.ndarray]: + triangulated: Dict[int, np.ndarray] = {} + for marker_id, observations in marker_observations.items(): + X = triangulate_marker_initial(marker_id, observations, cameras) + if X is not None: + triangulated[marker_id] = X + return triangulated + + +# =================================================================== +# Weighted residuals / optimization +# =================================================================== + +def bundle_adjustment_residuals( + marker_positions_flat: np.ndarray, + marker_ids: List[int], + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + constraints: List[Constraint], + obs_weights: Dict[Tuple[int, int], float], + lambda_constraint: float = 100.0 +) -> np.ndarray: + marker_dict: Dict[int, np.ndarray] = {} + for i, marker_id in enumerate(marker_ids): + marker_dict[marker_id] = marker_positions_flat[i * 3:(i + 1) * 3] + + residuals: List[float] = [] + + for marker_id, observations in marker_observations.items(): + if marker_id not in marker_dict: + continue + + X_world = marker_dict[marker_id] + for obs_idx, obs in enumerate(observations): + cam_idx, norm_coords_obs = obs.cam_idx, obs.norm_coords + K, D, R_wc, t_wc = cameras[cam_idx] + X_cam = R_wc @ X_world + t_wc + if X_cam[2] > 1e-6: + proj_norm = X_cam[:2] / X_cam[2] + r = proj_norm - norm_coords_obs + w = float(np.sqrt(obs_weights.get((marker_id, obs_idx), 1.0))) + residuals.append(w * float(r[0])) + residuals.append(w * float(r[1])) + + for constraint in constraints: + if isinstance(constraint, MarkerDistanceConstraint): + if constraint.marker_id_a in marker_dict and constraint.marker_id_b in marker_dict: + pos_a = marker_dict[constraint.marker_id_a] + pos_b = marker_dict[constraint.marker_id_b] + actual_dist = float(np.linalg.norm(pos_b - pos_a)) + residuals.append((actual_dist - constraint.target_distance_m) * constraint.weight * lambda_constraint) + + elif isinstance(constraint, JointAxisConstraint): + if constraint.marker_id_parent in marker_dict and constraint.marker_id_child in marker_dict: + pos_parent = marker_dict[constraint.marker_id_parent] + pos_child = marker_dict[constraint.marker_id_child] + delta = pos_child - pos_parent + actual_delta = float(np.dot(delta, constraint.joint_axis)) + residuals.append((actual_delta - constraint.target_delta_along_axis_m) * constraint.weight * lambda_constraint) + + return np.asarray(residuals, dtype=np.float64) + + +def optimize_with_constraints( + initial_positions: Dict[int, np.ndarray], + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + constraints: List[Constraint], + obs_weights: Dict[Tuple[int, int], float], + lambda_constraint: float = 100.0, + max_iterations: int = 50 +) -> Dict[int, np.ndarray]: + try: + from scipy.optimize import least_squares + except ImportError: + print("[WARN] scipy not available, skipping optimization.") + return initial_positions + + marker_ids = sorted(initial_positions.keys()) + if not marker_ids: + return {} + + x0 = np.concatenate([initial_positions[mid] for mid in marker_ids]) + + def residuals_fn(x: np.ndarray) -> np.ndarray: + return bundle_adjustment_residuals( + x, marker_ids, marker_observations, cameras, constraints, obs_weights, lambda_constraint + ) + + print(f"[INFO] Starting optimization with {len(x0)} variables and {len(constraints)} constraints...") + + result = least_squares( + residuals_fn, + x0, + max_nfev=max_iterations * max(1, len(marker_ids)), + verbose=1, + ) + + optimized = {} + for i, marker_id in enumerate(marker_ids): + optimized[marker_id] = result.x[i * 3:(i + 1) * 3] + + print(f"[INFO] Optimization complete. Final cost: {float(np.sum(result.fun ** 2)):.6f}") + return optimized + + +# =================================================================== +# Reporting helpers +# =================================================================== + +def print_constraint_summary(constraints: List[Constraint]) -> None: + num_dist = sum(isinstance(c, MarkerDistanceConstraint) for c in constraints) + num_joint = sum(isinstance(c, JointAxisConstraint) for c in constraints) + print(f"[INFO] Constraint summary: total={len(constraints)} distance={num_dist} joint/chain={num_joint}") + + +def print_constraint_list(constraints: List[Constraint]) -> None: + print("\n[INFO] Constraint list:") + for idx, constraint in enumerate(constraints): + if isinstance(constraint, MarkerDistanceConstraint): + print( + f" [{idx:03d}] DISTANCE | " + f"Link='{constraint.link_name}' | " + f"M{constraint.marker_id_a} <-> M{constraint.marker_id_b} | " + f"Target={constraint.target_distance_m:.6f} m | " + f"Weight={constraint.weight} | " + f"Source={constraint.source}" + ) + else: + axis_str = np.array2string(constraint.joint_axis, precision=3, suppress_small=True) + print( + f" [{idx:03d}] JOINT_AXIS | " + f"{constraint.parent_link}(M{constraint.marker_id_parent}) -> " + f"{constraint.child_link}(M{constraint.marker_id_child}) | " + f"Axis={axis_str} | " + f"TargetDelta={constraint.target_delta_along_axis_m:.6f} m | " + f"Weight={constraint.weight} | " + f"Source={constraint.source}" + ) + + +def print_constraints_with_errors( + title: str, + constraints: List[Constraint], + positions: Dict[int, np.ndarray], + show_skipped: bool = True +) -> None: + print(f"\n[INFO] {title}") + + active = 0 + skipped = 0 + + for idx, constraint in enumerate(constraints): + if isinstance(constraint, MarkerDistanceConstraint): + if constraint.marker_id_a not in positions or constraint.marker_id_b not in positions: + skipped += 1 + if show_skipped: + print( + f" [{idx:03d}] DISTANCE | " + f"M{constraint.marker_id_a} <-> M{constraint.marker_id_b} | SKIPPED (missing marker)" + ) + continue + + pos_a = positions[constraint.marker_id_a] + pos_b = positions[constraint.marker_id_b] + actual = float(np.linalg.norm(pos_b - pos_a)) + error = actual - constraint.target_distance_m + active += 1 + + print( + f" [{idx:03d}] DISTANCE | " + f"Link='{constraint.link_name}' | " + f"M{constraint.marker_id_a} <-> M{constraint.marker_id_b} | " + f"target={constraint.target_distance_m*1000:.2f} mm | " + f"actual={actual*1000:.2f} mm | " + f"error={error*1000:+.2f} mm" + ) + + elif isinstance(constraint, JointAxisConstraint): + if constraint.marker_id_parent not in positions or constraint.marker_id_child not in positions: + skipped += 1 + if show_skipped: + print( + f" [{idx:03d}] JOINT_AXIS | " + f"M{constraint.marker_id_parent} -> M{constraint.marker_id_child} | SKIPPED (missing marker)" + ) + continue + + pos_parent = positions[constraint.marker_id_parent] + pos_child = positions[constraint.marker_id_child] + delta = pos_child - pos_parent + actual = float(np.dot(delta, constraint.joint_axis)) + error = actual - constraint.target_delta_along_axis_m + active += 1 + + axis_str = np.array2string(constraint.joint_axis, precision=2, suppress_small=True) + print( + f" [{idx:03d}] JOINT_AXIS | " + f"{constraint.parent_link}(M{constraint.marker_id_parent}) -> " + f"{constraint.child_link}(M{constraint.marker_id_child}) | " + f"axis={axis_str} | " + f"target={constraint.target_delta_along_axis_m*1000:.2f} mm | " + f"actual={actual*1000:.2f} mm | " + f"error={error*1000:+.2f} mm" + ) + + print(f"[INFO] Active constraints: {active} | Skipped: {skipped}") + + +def print_observation_weight_summary(obs_weights: Dict[Tuple[int, int], float]) -> None: + if not obs_weights: + print("[INFO] Observation weighting: disabled or empty") + return + values = np.array(list(obs_weights.values()), dtype=np.float64) + print( + "[INFO] Observation weights: " + f"min={values.min():.3f} mean={values.mean():.3f} " + f"median={np.median(values):.3f} max={values.max():.3f}" + ) + + +# =================================================================== +# Main +# =================================================================== + +def main() -> None: + parser = argparse.ArgumentParser( + description="Multi-view bundle adjustment with rule-based geometric constraints" + ) + parser.add_argument( + "-det", "--detections", + action="append", + required=True, + help="*_aruco_detection.json files" + ) + parser.add_argument( + "-pose", "--poses", + action="append", + required=True, + help="*_camera_pose.json files" + ) + parser.add_argument( + "-robot", "--robot", + required=True, + help="robot.json" + ) + parser.add_argument( + "-outDir", "--outDir", + default=None, + help="Output directory" + ) + parser.add_argument( + "-lambdaWeight", "--lambdaWeight", + type=float, + default=100.0, + help="Constraint weight multiplier" + ) + parser.add_argument( + "--strictUniqueMarkerIds", + action="store_true", + help="Fail if a marker ID appears more than once in robot.json" + ) + parser.add_argument( + "--showSkippedConstraints", + action="store_true", + help="Print skipped constraints in the report" + ) + parser.add_argument( + "--noShowSkippedConstraints", + action="store_true", + help="Hide skipped constraints in the report" + ) + parser.add_argument( + "--saveConstraintReport", + action="store_true", + help="Save constraint report JSON files" + ) + parser.add_argument( + "--saveObservationWeightReport", + action="store_true", + help="Save observation-weight report JSON file" + ) + + args = parser.parse_args() + + if args.showSkippedConstraints and args.noShowSkippedConstraints: + print("[ERROR] Choose only one of --showSkippedConstraints or --noShowSkippedConstraints") + sys.exit(1) + + if len(args.detections) != len(args.poses): + print(f"[ERROR] Mismatch: {len(args.detections)} detection files vs {len(args.poses)} pose files") + sys.exit(1) + + robot_data = load_json(args.robot) + length_scale = get_length_scale(robot_data) + cfg = load_constraint_rule_config(robot_data, args) + + print("[STEP 1] Compile constraints from robot.json structure...") + marker_to_link, link_markers, constraints, issues, marker_meta = compile_constraints(robot_data, length_scale, cfg) + + for issue in issues: + print(issue) + + print(f"[INFO] Links with markers: {sum(1 for v in link_markers.values() if len(v) > 0)}") + print(f"[INFO] Unique marker IDs: {len(marker_to_link)}") + print_constraint_summary(constraints) + print_constraint_list(constraints) + + print("\n[STEP 2] Load observations and camera poses...") + marker_observations, cameras, obs_metadata = load_observations_and_poses(args.detections, args.poses) + print(f"[INFO] {len(cameras)} cameras, {len(marker_observations)} observed markers") + print(f"[INFO] Detection files loaded: {len(obs_metadata)}") + + print("\n[STEP 3] Initial triangulation...") + initial_pos = initial_triangulation(marker_observations, cameras) + print(f"[INFO] Triangulated {len(initial_pos)} markers") + + out_dir = args.outDir or os.path.dirname(args.detections[0]) or "." + os.makedirs(resolve_path(out_dir), exist_ok=True) + + initial_output_markers = [] + for marker_id, position in sorted(initial_pos.items()): + initial_output_markers.append( + { + "marker_id": int(marker_id), + "position_m": [float(x) for x in position], + "position_mm": [float(x * 1000.0) for x in position], + "link": marker_to_link.get(marker_id, "unknown"), + } + ) + + initial_output = { + "schema_version": "1.2", + "stage": "initial_triangulation", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_cameras": len(cameras), + "num_markers": len(initial_pos), + "num_constraints": len(constraints), + }, + "markers": initial_output_markers, + } + initial_out_file = os.path.join(out_dir, "aruco_positions_initial.json") + save_json(initial_out_file, initial_output) + print(f"[INFO] Initial triangulation saved to {initial_out_file}") + + obs_weights = compute_observation_weights( + marker_observations=marker_observations, + cameras=cameras, + initial_positions=initial_pos, + marker_meta=marker_meta, + cfg=cfg, + robot_data=robot_data, + ) + print_observation_weight_summary(obs_weights) + + print_constraints_with_errors( + "Constraint list BEFORE optimization", + constraints, + initial_pos, + show_skipped=cfg.show_skipped_constraints, + ) + + print("\n[STEP 4] Bundle adjustment with constraints...") + optimized_pos = optimize_with_constraints( + initial_pos, + marker_observations, + cameras, + constraints, + obs_weights, + lambda_constraint=args.lambdaWeight, + ) + + print_constraints_with_errors( + "Constraint list AFTER optimization", + constraints, + optimized_pos, + show_skipped=cfg.show_skipped_constraints, + ) + + output_markers = [] + for marker_id, position in sorted(optimized_pos.items()): + output_markers.append( + { + "marker_id": int(marker_id), + "position_m": [float(x) for x in position], + "position_mm": [float(x * 1000.0) for x in position], + "link": marker_to_link.get(marker_id, "unknown"), + } + ) + + output = { + "schema_version": "1.2", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_cameras": len(cameras), + "num_markers": len(optimized_pos), + "num_constraints": len(constraints), + }, + "markers": output_markers, + } + out_file = os.path.join(out_dir, "aruco_positions_optimized.json") + save_json(out_file, output) + print(f"\n[INFO] Saved to {out_file}") + + if args.saveConstraintReport: + report = { + "schema_version": "1.0", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_constraints": len(constraints), + "num_links_with_markers": sum(1 for v in link_markers.values() if len(v) > 0), + "num_observed_markers": len(marker_observations), + "num_triangulated_markers": len(initial_pos), + "num_optimized_markers": len(optimized_pos), + }, + "constraints": [], + } + for c in constraints: + if isinstance(c, MarkerDistanceConstraint): + report["constraints"].append( + { + "kind": "distance", + "link_name": c.link_name, + "marker_id_a": c.marker_id_a, + "marker_id_b": c.marker_id_b, + "target_distance_m": c.target_distance_m, + "weight": c.weight, + "source": c.source, + } + ) + else: + report["constraints"].append( + { + "kind": "joint_axis", + "parent_link": c.parent_link, + "child_link": c.child_link, + "marker_id_parent": c.marker_id_parent, + "marker_id_child": c.marker_id_child, + "joint_axis": [float(x) for x in c.joint_axis], + "target_delta_along_axis_m": c.target_delta_along_axis_m, + "weight": c.weight, + "source": c.source, + } + ) + report_file = os.path.join(out_dir, "constraint_report.json") + save_json(report_file, report) + print(f"[INFO] Constraint report saved to {report_file}") + + if args.saveObservationWeightReport: + obs_report = { + "schema_version": "1.0", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_weighted_observations": len(obs_weights), + }, + "observation_weights": [ + { + "marker_id": int(mid), + "observation_index": int(obs_idx), + "weight": float(w), + } + for (mid, obs_idx), w in sorted(obs_weights.items()) + ], + } + obs_file = os.path.join(out_dir, "observation_weight_report.json") + save_json(obs_file, obs_report) + print(f"[INFO] Observation-weight report saved to {obs_file}") + + +if __name__ == "__main__": + main() diff --git a/pipeline/3_multiview_bundle_adjustment_v4.py b/pipeline/3_multiview_bundle_adjustment_v4.py new file mode 100644 index 0000000..4d7384f --- /dev/null +++ b/pipeline/3_multiview_bundle_adjustment_v4.py @@ -0,0 +1,1462 @@ +#!/usr/bin/env python3 +""" +3_multiview_bundle_adjustment_v4.py + +Multi-view ArUco marker position optimization with explicit, switchable +degrees-of-freedom constraints. + +Mathematical model +------------------ +We estimate 3D marker positions X_i ∈ R^3 by minimizing + + E(X) = + Σ_{i,c} w_ic || π_c(X_i) - u_ic ||² + + λ_r Σ_j w_j^r || ||X_a - X_b|| - d_j ||² + + λ_rev Σ_k w_k^rev || (X_b - X_a)·a_k - t_k ||² + + λ_pri Σ_m w_m^pri ( ||(X_b - X_a)·u_m - t_u||² + + ||(X_b - X_a)·v_m - t_v||² ) + +where: +- u_ic are observed normalized image coordinates for marker i in camera c +- π_c(.) is the normalized reprojection model +- w_ic are observation weights from detection quality / marker priors / range +- rigid-link constraints preserve internal marker geometry of a link +- revolute joints keep the projection along the joint axis constant +- prismatic joints keep the two orthogonal projection components constant + +Important design choices +------------------------ +- robot.json is used as a kinematic description, not as a direct source of + world-space marker positions. +- constraint families are explicit, switchable, and easy to compare across + versions. +- legacy chain-propagation constraints are retained only as an optional family + and are OFF by default. +- observation weighting remains separate from constraint weighting so both can + be tested independently. + +Dependencies: + numpy, opencv-python, scipy (optional for optimization) + +Example: + python 3_multiview_bundle_adjustment_v4.py ^ + -det cam1_aruco_detection.json cam2_aruco_detection.json cam3_aruco_detection.json ^ + -pose cam1_camera_pose.json cam2_camera_pose.json cam3_camera_pose.json ^ + -robot robot.json ^ + -lambdaWeight 100.0 +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from dataclasses import dataclass +from itertools import combinations +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) + + +# =================================================================== +# Units +# =================================================================== + +def get_length_scale(robot_data: Dict[str, Any]) -> float: + units = robot_data.get("units", {}) or {} + length_unit = str(units.get("length", "")).strip().lower() + if length_unit in ("mm", "millimeter", "millimeters"): + return 1.0 / 1000.0 + if length_unit in ("cm", "centimeter", "centimeters"): + return 1.0 / 100.0 + return 1.0 + + +# =================================================================== +# Small geometry helpers +# =================================================================== + +def safe_norm(v: np.ndarray, eps: float = 1e-12) -> float: + return float(np.linalg.norm(v) + eps) + + +def normalize_vector(v: np.ndarray, eps: float = 1e-12) -> np.ndarray: + return np.asarray(v, dtype=np.float64) / safe_norm(v, eps) + + +def clamp(v: float, lo: float, hi: float) -> float: + return float(max(lo, min(hi, v))) + + +def principal_axis_id(axis: np.ndarray, threshold: float = 0.95) -> Optional[int]: + """Return 0,1,2 for x,y,z if axis is close enough to a principal axis.""" + a = normalize_vector(np.asarray(axis, dtype=np.float64)) + idx = int(np.argmax(np.abs(a))) + if abs(a[idx]) >= threshold: + return idx + return None + + +def camera_center_from_world_to_cam(R_wc: np.ndarray, t_wc: np.ndarray) -> np.ndarray: + """world_to_camera: X_cam = R_wc * X_world + t_wc; camera center is -R^T t.""" + return -R_wc.T @ t_wc + + +def principal_axis_vector(axis: np.ndarray) -> np.ndarray: + """Convert a near-principal axis to an exact signed principal axis vector.""" + a = normalize_vector(axis) + idx = int(np.argmax(np.abs(a))) + out = np.zeros(3, dtype=np.float64) + out[idx] = 1.0 if a[idx] >= 0 else -1.0 + return normalize_vector(out) + + +def orthonormal_basis_from_axis(axis: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """ + Build two unit vectors orthogonal to axis, with a deterministic orientation. + """ + a = normalize_vector(axis) + ref = np.array([1.0, 0.0, 0.0], dtype=np.float64) + if abs(float(np.dot(a, ref))) > 0.90: + ref = np.array([0.0, 1.0, 0.0], dtype=np.float64) + u = np.cross(a, ref) + if np.linalg.norm(u) < 1e-12: + ref = np.array([0.0, 0.0, 1.0], dtype=np.float64) + u = np.cross(a, ref) + u = normalize_vector(u) + v = normalize_vector(np.cross(a, u)) + return u, v + + +# =================================================================== +# Configuration +# =================================================================== + +@dataclass +class ConstraintRuleConfig: + rigid_distance_enabled: bool = True + rigid_distance_mode: str = "mst" # mst | star | full + rigid_distance_weight: float = 1.0 + + # Revolute joints: keep the projection along the axis constant. + revolute_axis_enabled: bool = True + revolute_axis_max_pairs: int = 2 + revolute_axis_weight: float = 0.5 + + # Prismatic joints: keep the two orthogonal projection components constant. + prismatic_orthogonal_enabled: bool = True + prismatic_orthogonal_max_pairs: int = 2 + prismatic_orthogonal_weight: float = 0.35 + + # Legacy / optional chain propagation, disabled by default. + chain_axis_enabled: bool = False + chain_axis_max_depth: int = 3 + chain_axis_max_pairs: int = 2 + chain_axis_weight: float = 0.3 + + axis_alignment_threshold: float = 0.95 + + strict_unique_marker_ids: bool = False + show_skipped_constraints: bool = True + + enable_observation_weights: bool = True + weight_floor: float = 0.30 + weight_ceiling: float = 3.00 + ref_distance_m: float = 0.75 + ref_marker_size_px: float = 50.0 + use_detection_confidence: bool = True + use_detection_size_px: bool = True + use_initial_range: bool = True + use_marker_size_prior: bool = True + + +def _bool_or_default(value: Any, default: bool) -> bool: + if value is None: + return default + return bool(value) + + +def _float_or_default(value: Any, default: float) -> float: + if value is None: + return default + return float(value) + + +def _int_or_default(value: Any, default: int) -> int: + if value is None: + return default + return int(value) + + +def load_constraint_rule_config(robot_data: Dict[str, Any], args: argparse.Namespace) -> ConstraintRuleConfig: + """ + Merge built-in defaults with optional robot.json constraint_rules and CLI flags. + Backward compatibility: + - joint_axis_projection -> revolute_axis + """ + rules = robot_data.get("constraint_rules", {}) or {} + + cfg = ConstraintRuleConfig() + rigid = rules.get("rigid_distance", {}) or {} + revolute = rules.get("joint_revolute_axis", {}) or rules.get("joint_axis_projection", {}) or {} + prismatic = rules.get("joint_prismatic_orthogonal", {}) or {} + chain = rules.get("chain_axis_projection", {}) or {} + obs = rules.get("observation_weights", {}) or {} + + cfg.rigid_distance_enabled = _bool_or_default(rigid.get("enabled"), cfg.rigid_distance_enabled) + cfg.rigid_distance_mode = str(rigid.get("mode", cfg.rigid_distance_mode)).strip().lower() + cfg.rigid_distance_weight = _float_or_default(rigid.get("weight"), cfg.rigid_distance_weight) + + cfg.revolute_axis_enabled = _bool_or_default(revolute.get("enabled"), cfg.revolute_axis_enabled) + cfg.revolute_axis_max_pairs = _int_or_default(revolute.get("max_pairs"), cfg.revolute_axis_max_pairs) + cfg.revolute_axis_weight = _float_or_default(revolute.get("weight"), cfg.revolute_axis_weight) + + cfg.prismatic_orthogonal_enabled = _bool_or_default(prismatic.get("enabled"), cfg.prismatic_orthogonal_enabled) + cfg.prismatic_orthogonal_max_pairs = _int_or_default(prismatic.get("max_pairs"), cfg.prismatic_orthogonal_max_pairs) + cfg.prismatic_orthogonal_weight = _float_or_default(prismatic.get("weight"), cfg.prismatic_orthogonal_weight) + + cfg.chain_axis_enabled = _bool_or_default(chain.get("enabled"), cfg.chain_axis_enabled) + cfg.chain_axis_max_depth = _int_or_default(chain.get("max_depth"), cfg.chain_axis_max_depth) + cfg.chain_axis_max_pairs = _int_or_default(chain.get("max_pairs"), cfg.chain_axis_max_pairs) + cfg.chain_axis_weight = _float_or_default(chain.get("weight"), cfg.chain_axis_weight) + + cfg.axis_alignment_threshold = _float_or_default( + rules.get("axis_alignment_threshold"), cfg.axis_alignment_threshold + ) + + cfg.enable_observation_weights = _bool_or_default(obs.get("enabled"), cfg.enable_observation_weights) + cfg.weight_floor = _float_or_default(obs.get("weight_floor"), cfg.weight_floor) + cfg.weight_ceiling = _float_or_default(obs.get("weight_ceiling"), cfg.weight_ceiling) + cfg.ref_distance_m = _float_or_default(obs.get("ref_distance_m"), cfg.ref_distance_m) + cfg.ref_marker_size_px = _float_or_default(obs.get("ref_marker_size_px"), cfg.ref_marker_size_px) + cfg.use_detection_confidence = _bool_or_default(obs.get("use_detection_confidence"), cfg.use_detection_confidence) + cfg.use_detection_size_px = _bool_or_default(obs.get("use_detection_size_px"), cfg.use_detection_size_px) + cfg.use_initial_range = _bool_or_default(obs.get("use_initial_range"), cfg.use_initial_range) + cfg.use_marker_size_prior = _bool_or_default(obs.get("use_marker_size_prior"), cfg.use_marker_size_prior) + + if getattr(args, "strictUniqueMarkerIds", False): + cfg.strict_unique_marker_ids = True + if getattr(args, "showSkippedConstraints", False): + cfg.show_skipped_constraints = True + if getattr(args, "noShowSkippedConstraints", False): + cfg.show_skipped_constraints = False + + return cfg + + +# =================================================================== +# Observation / constraint definitions +# =================================================================== + +@dataclass +class Observation: + cam_idx: int + norm_coords: np.ndarray + meta: Dict[str, Any] + + +@dataclass +class MarkerDistanceConstraint: + marker_id_a: int + marker_id_b: int + link_name: str + target_distance_m: float + weight: float = 1.0 + enabled: bool = True + source: str = "rigid_distance" + + +@dataclass +class JointAxisConstraint: + marker_id_parent: int + marker_id_child: int + parent_link: str + child_link: str + joint_axis: np.ndarray + target_delta_along_axis_m: float + weight: float = 1.0 + enabled: bool = True + source: str = "joint_axis_projection" + + +Constraint = MarkerDistanceConstraint | JointAxisConstraint + + +# =================================================================== +# Robot parsing +# =================================================================== + +def parse_robot_markers( + robot_data: Dict[str, Any], + length_scale: float, + strict_unique_marker_ids: bool = False +) -> Tuple[Dict[int, str], Dict[str, List[Dict[str, Any]]], List[str], Dict[int, Dict[str, Any]]]: + links = robot_data.get("links", {}) or {} + + marker_to_link: Dict[int, str] = {} + link_markers: Dict[str, List[Dict[str, Any]]] = {} + issues: List[str] = [] + marker_meta: Dict[int, Dict[str, Any]] = {} + + seen_global: Dict[int, str] = {} + + for link_name, link_data in links.items(): + markers = link_data.get("markers", []) or [] + collected: List[Dict[str, Any]] = [] + seen_local: set[int] = set() + + for idx, marker in enumerate(markers): + marker_id = int(marker.get("id", -1)) + pos = marker.get("position", None) + + if marker_id < 0 or pos is None or len(pos) != 3: + issues.append(f"[WARN] link={link_name}: skipped invalid marker entry at index {idx}") + continue + + if marker_id in seen_local: + msg = f"[WARN] duplicate marker id {marker_id} inside link '{link_name}'" + if strict_unique_marker_ids: + raise ValueError(msg) + issues.append(msg + " -> skipped duplicate inside same link") + continue + + if marker_id in seen_global: + msg = ( + f"[WARN] duplicate marker id {marker_id} appears in link '{link_name}' " + f"and already in link '{seen_global[marker_id]}'" + ) + if strict_unique_marker_ids: + raise ValueError(msg) + issues.append(msg + " -> skipped duplicate occurrence") + continue + + seen_local.add(marker_id) + seen_global[marker_id] = link_name + + pos_raw = np.array(pos, dtype=np.float64) + pos_m = pos_raw * float(length_scale) + + item = { + "id": marker_id, + "name": marker.get("name", f"marker_{marker_id}"), + "position_raw": pos_raw, + "position_m": pos_m, + "normal": np.array(marker.get("normal", [0, 0, 1]), dtype=np.float64), + "size": marker.get("size", None), + "spin": marker.get("spin", None), + } + collected.append(item) + marker_to_link[marker_id] = link_name + marker_meta[marker_id] = { + "link_name": link_name, + "name": item["name"], + "position_m": pos_m, + "normal": item["normal"], + "size": item["size"], + "spin": item["spin"], + } + + link_markers[link_name] = collected + + return marker_to_link, link_markers, issues, marker_meta + + +def get_link_parent_map(robot_data: Dict[str, Any]) -> Dict[str, Optional[str]]: + links = robot_data.get("links", {}) or {} + return {link_name: (link_data.get("parent", None)) for link_name, link_data in links.items()} + + +def get_joint_info(robot_data: Dict[str, Any], child_link: str) -> Dict[str, Any]: + links = robot_data.get("links", {}) or {} + return (links.get(child_link, {}) or {}).get("jointToParent", {}) or {} + + +def get_joint_axis(robot_data: Dict[str, Any], child_link: str) -> Optional[np.ndarray]: + joint = get_joint_info(robot_data, child_link) + axis = joint.get("axis", None) + if axis is None: + return None + axis = np.asarray(axis, dtype=np.float64) + if safe_norm(axis) < 1e-12: + return None + return normalize_vector(axis) + + +def get_vision_marker_size_default(robot_data: Dict[str, Any]) -> float: + vision = robot_data.get("vision_config", {}) or {} + ms = vision.get("MarkerSize", None) + if ms is None: + return 0.025 + return float(ms) + + +# =================================================================== +# Constraint compilation helpers +# =================================================================== + +def get_enabled_link_rule( + robot_data: Dict[str, Any], + link_name: str, + rule_name: str, + default_enabled: bool = True +) -> bool: + overrides = robot_data.get("constraint_overrides", {}) or {} + link_override = overrides.get(link_name, {}) or {} + rule_override = link_override.get(rule_name, {}) or {} + if "enabled" in rule_override: + return bool(rule_override["enabled"]) + return default_enabled + + +def select_anchor_marker_ids( + markers: List[Dict[str, Any]], + axis: Optional[np.ndarray] = None, + max_count: int = 2 +) -> List[int]: + if not markers: + return [] + if len(markers) == 1: + return [int(markers[0]["id"])] + + ids = [int(m["id"]) for m in markers] + pos = np.stack([np.asarray(m["position_m"], dtype=np.float64) for m in markers], axis=0) + + selected: List[int] = [] + + if axis is not None and safe_norm(axis) > 1e-12: + a = normalize_vector(axis) + proj = pos @ a + min_idx = int(np.argmin(proj)) + max_idx = int(np.argmax(proj)) + selected = [ids[min_idx], ids[max_idx]] + else: + centroid = np.mean(pos, axis=0) + d = np.linalg.norm(pos - centroid, axis=1) + min_idx = int(np.argmin(d)) + max_idx = int(np.argmax(d)) + selected = [ids[min_idx], ids[max_idx]] + + if len(selected) < max_count: + for mid in ids: + if mid not in selected: + selected.append(mid) + if len(selected) >= max_count: + break + + out: List[int] = [] + seen: set[int] = set() + for mid in selected: + if mid not in seen: + seen.add(mid) + out.append(mid) + if len(out) >= max_count: + break + return out + + +def mst_edges_for_link(markers: List[Dict[str, Any]]) -> List[Tuple[int, int]]: + n = len(markers) + if n < 2: + return [] + + ids = [int(m["id"]) for m in markers] + pos = np.stack([np.asarray(m["position_m"], dtype=np.float64) for m in markers], axis=0) + in_tree = np.zeros(n, dtype=bool) + in_tree[0] = True + edges: List[Tuple[int, int]] = [] + dist = np.linalg.norm(pos[:, None, :] - pos[None, :, :], axis=2) + + for _ in range(n - 1): + best = None + best_d = float("inf") + for i in range(n): + if not in_tree[i]: + continue + for j in range(n): + if in_tree[j]: + continue + d = float(dist[i, j]) + if d < best_d: + best_d = d + best = (i, j) + if best is None: + break + i, j = best + in_tree[j] = True + edges.append((ids[i], ids[j])) + return edges + + +def compile_rigid_distance_constraints( + robot_data: Dict[str, Any], + link_markers: Dict[str, List[Dict[str, Any]]], + cfg: ConstraintRuleConfig +) -> List[MarkerDistanceConstraint]: + constraints: List[MarkerDistanceConstraint] = [] + + for link_name, markers in link_markers.items(): + if not get_enabled_link_rule(robot_data, link_name, "rigid_distance", cfg.rigid_distance_enabled): + continue + if len(markers) < 2: + continue + + mode = cfg.rigid_distance_mode + if mode == "full": + pairs = [(int(a["id"]), int(b["id"])) for a, b in combinations(markers, 2)] + elif mode == "star": + anchor_ids = select_anchor_marker_ids(markers, axis=None, max_count=1) + anchor_id = anchor_ids[0] + pairs = [] + for m in markers: + mid = int(m["id"]) + if mid != anchor_id: + pairs.append((anchor_id, mid)) + elif mode == "mst": + pairs = mst_edges_for_link(markers) + else: + raise ValueError(f"Unknown rigid_distance_mode='{mode}'. Use mst|star|full.") + + pos_map = {int(m["id"]): np.asarray(m["position_m"], dtype=np.float64) for m in markers} + seen_pairs: set[Tuple[int, int]] = set() + + for mid_a, mid_b in pairs: + if mid_a == mid_b: + continue + key = tuple(sorted((int(mid_a), int(mid_b)))) + if key in seen_pairs: + continue + seen_pairs.add(key) + + pos_a = pos_map[mid_a] + pos_b = pos_map[mid_b] + target = float(np.linalg.norm(pos_b - pos_a)) + + constraints.append( + MarkerDistanceConstraint( + marker_id_a=mid_a, + marker_id_b=mid_b, + link_name=link_name, + target_distance_m=target, + weight=cfg.rigid_distance_weight, + enabled=True, + source=f"rigid_distance:{mode}", + ) + ) + + return constraints + + +def compile_joint_dof_constraints( + robot_data: Dict[str, Any], + link_markers: Dict[str, List[Dict[str, Any]]], + cfg: ConstraintRuleConfig +) -> List[JointAxisConstraint]: + """ + Compile local joint constraints from robot.json. + + Revolute joints: one scalar constraint per anchor pair + (projection along the joint axis stays constant) + + Prismatic joints: two scalar constraints per anchor pair + (the orthogonal projections stay constant) + + Both are emitted as JointAxisConstraint objects so the rest of the + optimization pipeline remains unchanged. + """ + constraints: List[JointAxisConstraint] = [] + links = robot_data.get("links", {}) or {} + + for child_link, child_data in links.items(): + parent_link = child_data.get("parent", None) + if not parent_link: + continue + + joint_info = child_data.get("jointToParent", {}) or {} + joint_type = str(joint_info.get("type", "")).strip().lower() + + joint_axis = get_joint_axis(robot_data, child_link) + if joint_axis is None: + continue + + axis_vec = principal_axis_vector(joint_axis) + + parent_markers = link_markers.get(parent_link, []) + child_markers = link_markers.get(child_link, []) + if len(parent_markers) == 0 or len(child_markers) == 0: + continue + + parent_pos = {int(m["id"]): np.asarray(m["position_m"], dtype=np.float64) for m in parent_markers} + child_pos = {int(m["id"]): np.asarray(m["position_m"], dtype=np.float64) for m in child_markers} + + seen: set[Tuple[int, int]] = set() + + if joint_type == "revolute": + if not get_enabled_link_rule( + robot_data, child_link, "joint_revolute_axis", cfg.revolute_axis_enabled + ): + continue + + max_pairs = max(1, int(cfg.revolute_axis_max_pairs)) + parent_anchor_ids = select_anchor_marker_ids(parent_markers, axis=axis_vec, max_count=max_pairs) + child_anchor_ids = select_anchor_marker_ids(child_markers, axis=axis_vec, max_count=max_pairs) + + for mid_p in parent_anchor_ids: + for mid_c in child_anchor_ids: + if mid_p == mid_c: + continue + key = (mid_p, mid_c) + if key in seen: + continue + seen.add(key) + + delta = child_pos[mid_c] - parent_pos[mid_p] + target = float(np.dot(delta, axis_vec)) + + constraints.append( + JointAxisConstraint( + marker_id_parent=mid_p, + marker_id_child=mid_c, + parent_link=parent_link, + child_link=child_link, + joint_axis=axis_vec, + target_delta_along_axis_m=target, + weight=cfg.revolute_axis_weight, + enabled=True, + source="revolute_axis_projection", + ) + ) + + elif joint_type == "linear": + if not get_enabled_link_rule( + robot_data, child_link, "joint_prismatic_orthogonal", cfg.prismatic_orthogonal_enabled + ): + continue + + max_pairs = max(1, int(cfg.prismatic_orthogonal_max_pairs)) + parent_anchor_ids = select_anchor_marker_ids(parent_markers, axis=axis_vec, max_count=max_pairs) + child_anchor_ids = select_anchor_marker_ids(child_markers, axis=axis_vec, max_count=max_pairs) + basis_u, basis_v = orthonormal_basis_from_axis(axis_vec) + + for mid_p in parent_anchor_ids: + for mid_c in child_anchor_ids: + if mid_p == mid_c: + continue + key = (mid_p, mid_c) + if key in seen: + continue + seen.add(key) + + delta = child_pos[mid_c] - parent_pos[mid_p] + + constraints.append( + JointAxisConstraint( + marker_id_parent=mid_p, + marker_id_child=mid_c, + parent_link=parent_link, + child_link=child_link, + joint_axis=basis_u, + target_delta_along_axis_m=float(np.dot(delta, basis_u)), + weight=cfg.prismatic_orthogonal_weight, + enabled=True, + source="prismatic_orthogonal_projection:u", + ) + ) + constraints.append( + JointAxisConstraint( + marker_id_parent=mid_p, + marker_id_child=mid_c, + parent_link=parent_link, + child_link=child_link, + joint_axis=basis_v, + target_delta_along_axis_m=float(np.dot(delta, basis_v)), + weight=cfg.prismatic_orthogonal_weight, + enabled=True, + source="prismatic_orthogonal_projection:v", + ) + ) + + else: + continue + + return constraints + + + + +def compile_constraints( + robot_data: Dict[str, Any], + length_scale: float, + cfg: ConstraintRuleConfig +) -> Tuple[Dict[int, str], Dict[str, List[Dict[str, Any]]], List[Constraint], List[str], Dict[int, Dict[str, Any]]]: + marker_to_link, link_markers, issues, marker_meta = parse_robot_markers( + robot_data, + length_scale=length_scale, + strict_unique_marker_ids=cfg.strict_unique_marker_ids, + ) + + constraints: List[Constraint] = [] + constraints.extend(compile_rigid_distance_constraints(robot_data, link_markers, cfg)) + constraints.extend(compile_joint_dof_constraints(robot_data, link_markers, cfg)) + + # Legacy optional family, OFF by default. + if cfg.chain_axis_enabled: + constraints.extend(compile_chain_axis_constraints(robot_data, link_markers, cfg)) + + unique_constraints: List[Constraint] = [] + seen_keys: set[Tuple[Any, ...]] = set() + + for c in constraints: + if isinstance(c, MarkerDistanceConstraint): + key = ( + "d", + min(c.marker_id_a, c.marker_id_b), + max(c.marker_id_a, c.marker_id_b), + c.link_name, + round(c.target_distance_m, 9), + ) + else: + key = ( + "j", + c.parent_link, + c.child_link, + c.marker_id_parent, + c.marker_id_child, + tuple(np.round(c.joint_axis, 9).tolist()), + round(c.target_delta_along_axis_m, 9), + ) + if key in seen_keys: + continue + seen_keys.add(key) + unique_constraints.append(c) + + return marker_to_link, link_markers, unique_constraints, issues, marker_meta + + +# =================================================================== +# Observation quality / weighting +# =================================================================== + +def _optional_float(meta: Dict[str, Any], keys: List[str]) -> Optional[float]: + for k in keys: + if k in meta and meta[k] is not None: + try: + return float(meta[k]) + except Exception: + pass + return None + + +def detection_quality_from_metadata(det_obj: Dict[str, Any], cfg: ConstraintRuleConfig) -> float: + q = 1.0 + + if cfg.use_detection_confidence: + conf = _optional_float(det_obj, ["confidence", "score", "quality", "det_confidence"]) + if conf is not None: + q *= clamp(conf, 0.1, 1.0) + + if cfg.use_detection_size_px: + size_px = _optional_float(det_obj, ["size_px", "marker_size_px", "side_px", "side_length_px"]) + if size_px is None and "corners_px" in det_obj and isinstance(det_obj["corners_px"], list): + try: + corners = np.asarray(det_obj["corners_px"], dtype=np.float64).reshape(-1, 2) + if len(corners) >= 4: + edges = [] + for i in range(len(corners)): + p = corners[i] + q2 = corners[(i + 1) % len(corners)] + edges.append(float(np.linalg.norm(q2 - p))) + size_px = float(np.mean(edges)) + except Exception: + size_px = None + if size_px is not None: + q *= clamp(size_px / max(cfg.ref_marker_size_px, 1e-6), 0.25, 3.0) + + sharpness = _optional_float(det_obj, ["sharpness", "corner_sharpness"]) + if sharpness is not None: + q *= clamp(sharpness / 2500.0, 0.5, 1.5) + + normal_alignment = _optional_float(det_obj, ["normal_alignment", "view_cosine", "cos_to_camera"]) + if normal_alignment is not None: + q *= clamp(normal_alignment, 0.3, 1.0) + + return float(q) + + +def marker_size_prior_factor(marker_meta: Dict[str, Any], default_marker_size_m: float) -> float: + size_val = marker_meta.get("size", None) + if size_val is None: + return 1.0 + + try: + size_val = float(size_val) + except Exception: + return 1.0 + + size_m = size_val / 1000.0 if size_val > 1.0 else size_val + ref = max(default_marker_size_m, 1e-6) + return clamp(size_m / ref, 0.7, 1.3) + + +def compute_observation_weights( + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + initial_positions: Dict[int, np.ndarray], + marker_meta: Dict[int, Dict[str, Any]], + cfg: ConstraintRuleConfig, + robot_data: Dict[str, Any] +) -> Dict[Tuple[int, int], float]: + weights: Dict[Tuple[int, int], float] = {} + default_marker_size_m = get_vision_marker_size_default(robot_data) + + for marker_id, obs_list in marker_observations.items(): + X = initial_positions.get(marker_id, None) + size_prior = marker_size_prior_factor(marker_meta.get(marker_id, {}), default_marker_size_m) + + for obs_idx, obs in enumerate(obs_list): + w = 1.0 + q = detection_quality_from_metadata(obs.meta, cfg) + w *= q + + if cfg.use_marker_size_prior: + w *= size_prior + + if cfg.use_initial_range and X is not None: + _, _, R_wc, t_wc = cameras[obs.cam_idx] + C = camera_center_from_world_to_cam(R_wc, t_wc) + dist = float(np.linalg.norm(X - C)) + if np.isfinite(dist): + w *= clamp(cfg.ref_distance_m / max(dist, 1e-6), 0.4, 2.0) + + weights[(marker_id, obs_idx)] = clamp(w, cfg.weight_floor, cfg.weight_ceiling) + + return weights + + +# =================================================================== +# Multi-view loading +# =================================================================== + +def load_observations_and_poses( + detection_files: List[str], + pose_files: List[str] +) -> Tuple[ + Dict[int, List[Observation]], + List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + List[Dict[str, Any]] +]: + if len(detection_files) != len(pose_files): + raise ValueError(f"Mismatch: {len(detection_files)} detections vs {len(pose_files)} poses") + + marker_observations: Dict[int, List[Observation]] = {} + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]] = [] + obs_metadata: List[Dict[str, Any]] = [] + + for cam_idx, (det_file, pose_file) in enumerate(zip(detection_files, pose_files)): + det = load_json(det_file) + pose_data = load_json(pose_file) + + cam_section = det.get("camera", {}) or {} + K = np.array(cam_section.get("camera_matrix", []), dtype=np.float64).reshape(3, 3) + D = np.array(cam_section.get("distortion_coefficients", []), dtype=np.float64).reshape(-1, 1) + + pose_section = pose_data.get("camera_pose", {}) or {} + world_to_cam = pose_section.get("world_to_camera", {}) or {} + R_wc = np.array(world_to_cam.get("rotation_matrix", []), dtype=np.float64).reshape(3, 3) + t_wc = np.array(world_to_cam.get("translation_m", []), dtype=np.float64).reshape(3) + + cameras.append((K, D, R_wc, t_wc)) + + detections = det.get("detections", []) or [] + for det_obj in detections: + marker_id = int(det_obj.get("marker_id", -1)) + if marker_id < 0: + continue + + center_px = np.array(det_obj.get("center_px", []), dtype=np.float64) + if center_px.shape != (2,): + continue + + pts = center_px.reshape(1, 1, 2).astype(np.float32) + und = cv2.undistortPoints(pts, K.astype(np.float32), D.astype(np.float32), P=None) + norm_coords = und.reshape(2).astype(np.float64) + + obs = Observation(cam_idx=cam_idx, norm_coords=norm_coords, meta=dict(det_obj)) + marker_observations.setdefault(marker_id, []).append(obs) + + obs_metadata.append( + { + "detection_file": det_file, + "pose_file": pose_file, + "num_detections": len(detections), + } + ) + + return marker_observations, cameras, obs_metadata + + +# =================================================================== +# Initial triangulation +# =================================================================== + +def triangulate_marker_initial( + marker_id: int, + observations: List[Observation], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]] +) -> Optional[np.ndarray]: + if len(observations) < 2: + return None + + best_pair = None + best_baseline = -1.0 + + for obs_i, obs_j in combinations(observations, 2): + cam_i, cam_j = obs_i.cam_idx, obs_j.cam_idx + _, _, R1, t1 = cameras[cam_i] + _, _, R2, t2 = cameras[cam_j] + c1 = camera_center_from_world_to_cam(R1, t1) + c2 = camera_center_from_world_to_cam(R2, t2) + baseline = float(np.linalg.norm(c2 - c1)) + if baseline > best_baseline: + best_baseline = baseline + best_pair = (obs_i, obs_j) + + if best_pair is None: + return None + + obs_i, obs_j = best_pair + cam_i, cam_j = obs_i.cam_idx, obs_j.cam_idx + norm_coords_i = obs_i.norm_coords + norm_coords_j = obs_j.norm_coords + + K1, D1, R1, t1 = cameras[cam_i] + K2, D2, R2, t2 = cameras[cam_j] + + x1_px = K1[0, 0] * norm_coords_i[0] + K1[0, 2] + y1_px = K1[1, 1] * norm_coords_i[1] + K1[1, 2] + x2_px = K2[0, 0] * norm_coords_j[0] + K2[0, 2] + y2_px = K2[1, 1] * norm_coords_j[1] + K2[1, 2] + + P1 = K1 @ np.hstack([R1, t1.reshape(3, 1)]) + P2 = K2 @ np.hstack([R2, t2.reshape(3, 1)]) + + try: + X_h = cv2.triangulatePoints( + P1, + P2, + np.array([[x1_px], [y1_px]], dtype=np.float64), + np.array([[x2_px], [y2_px]], dtype=np.float64), + ) + X = (X_h[:3] / X_h[3]).reshape(3).astype(np.float64) + if not np.all(np.isfinite(X)): + return None + return X + except Exception: + return None + + +def initial_triangulation( + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]] +) -> Dict[int, np.ndarray]: + triangulated: Dict[int, np.ndarray] = {} + for marker_id, observations in marker_observations.items(): + X = triangulate_marker_initial(marker_id, observations, cameras) + if X is not None: + triangulated[marker_id] = X + return triangulated + + +# =================================================================== +# Weighted residuals / optimization +# =================================================================== + +def bundle_adjustment_residuals( + marker_positions_flat: np.ndarray, + marker_ids: List[int], + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + constraints: List[Constraint], + obs_weights: Dict[Tuple[int, int], float], + lambda_constraint: float = 100.0 +) -> np.ndarray: + marker_dict: Dict[int, np.ndarray] = {} + for i, marker_id in enumerate(marker_ids): + marker_dict[marker_id] = marker_positions_flat[i * 3:(i + 1) * 3] + + residuals: List[float] = [] + + for marker_id, observations in marker_observations.items(): + if marker_id not in marker_dict: + continue + + X_world = marker_dict[marker_id] + for obs_idx, obs in enumerate(observations): + cam_idx, norm_coords_obs = obs.cam_idx, obs.norm_coords + K, D, R_wc, t_wc = cameras[cam_idx] + X_cam = R_wc @ X_world + t_wc + if X_cam[2] > 1e-6: + proj_norm = X_cam[:2] / X_cam[2] + r = proj_norm - norm_coords_obs + w = float(np.sqrt(obs_weights.get((marker_id, obs_idx), 1.0))) + residuals.append(w * float(r[0])) + residuals.append(w * float(r[1])) + + for constraint in constraints: + if isinstance(constraint, MarkerDistanceConstraint): + if constraint.marker_id_a in marker_dict and constraint.marker_id_b in marker_dict: + pos_a = marker_dict[constraint.marker_id_a] + pos_b = marker_dict[constraint.marker_id_b] + actual_dist = float(np.linalg.norm(pos_b - pos_a)) + residuals.append((actual_dist - constraint.target_distance_m) * constraint.weight * lambda_constraint) + + elif isinstance(constraint, JointAxisConstraint): + if constraint.marker_id_parent in marker_dict and constraint.marker_id_child in marker_dict: + pos_parent = marker_dict[constraint.marker_id_parent] + pos_child = marker_dict[constraint.marker_id_child] + delta = pos_child - pos_parent + actual_delta = float(np.dot(delta, constraint.joint_axis)) + residuals.append((actual_delta - constraint.target_delta_along_axis_m) * constraint.weight * lambda_constraint) + + return np.asarray(residuals, dtype=np.float64) + + +def optimize_with_constraints( + initial_positions: Dict[int, np.ndarray], + marker_observations: Dict[int, List[Observation]], + cameras: List[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]], + constraints: List[Constraint], + obs_weights: Dict[Tuple[int, int], float], + lambda_constraint: float = 100.0, + max_iterations: int = 50 +) -> Dict[int, np.ndarray]: + try: + from scipy.optimize import least_squares + except ImportError: + print("[WARN] scipy not available, skipping optimization.") + return initial_positions + + marker_ids = sorted(initial_positions.keys()) + if not marker_ids: + return {} + + x0 = np.concatenate([initial_positions[mid] for mid in marker_ids]) + + def residuals_fn(x: np.ndarray) -> np.ndarray: + return bundle_adjustment_residuals( + x, marker_ids, marker_observations, cameras, constraints, obs_weights, lambda_constraint + ) + + print(f"[INFO] Starting optimization with {len(x0)} variables and {len(constraints)} constraints...") + + result = least_squares( + residuals_fn, + x0, + max_nfev=max_iterations * max(1, len(marker_ids)), + verbose=1, + ) + + optimized = {} + for i, marker_id in enumerate(marker_ids): + optimized[marker_id] = result.x[i * 3:(i + 1) * 3] + + print(f"[INFO] Optimization complete. Final cost: {float(np.sum(result.fun ** 2)):.6f}") + return optimized + + +# =================================================================== +# Reporting helpers +# =================================================================== + +def print_constraint_summary(constraints: List[Constraint]) -> None: + num_dist = sum(isinstance(c, MarkerDistanceConstraint) for c in constraints) + num_joint = sum(isinstance(c, JointAxisConstraint) for c in constraints) + print(f"[INFO] Constraint summary: total={len(constraints)} distance={num_dist} joint/chain={num_joint}") + + +def print_constraint_list(constraints: List[Constraint]) -> None: + print("\n[INFO] Constraint list:") + for idx, constraint in enumerate(constraints): + if isinstance(constraint, MarkerDistanceConstraint): + print( + f" [{idx:03d}] DISTANCE | " + f"Link='{constraint.link_name}' | " + f"M{constraint.marker_id_a} <-> M{constraint.marker_id_b} | " + f"Target={constraint.target_distance_m:.6f} m | " + f"Weight={constraint.weight} | " + f"Source={constraint.source}" + ) + else: + axis_str = np.array2string(constraint.joint_axis, precision=3, suppress_small=True) + print( + f" [{idx:03d}] JOINT_AXIS | " + f"{constraint.parent_link}(M{constraint.marker_id_parent}) -> " + f"{constraint.child_link}(M{constraint.marker_id_child}) | " + f"Axis={axis_str} | " + f"TargetDelta={constraint.target_delta_along_axis_m:.6f} m | " + f"Weight={constraint.weight} | " + f"Source={constraint.source}" + ) + + +def print_constraints_with_errors( + title: str, + constraints: List[Constraint], + positions: Dict[int, np.ndarray], + show_skipped: bool = True +) -> None: + print(f"\n[INFO] {title}") + + active = 0 + skipped = 0 + + for idx, constraint in enumerate(constraints): + if isinstance(constraint, MarkerDistanceConstraint): + if constraint.marker_id_a not in positions or constraint.marker_id_b not in positions: + skipped += 1 + if show_skipped: + print( + f" [{idx:03d}] DISTANCE | " + f"M{constraint.marker_id_a} <-> M{constraint.marker_id_b} | SKIPPED (missing marker)" + ) + continue + + pos_a = positions[constraint.marker_id_a] + pos_b = positions[constraint.marker_id_b] + actual = float(np.linalg.norm(pos_b - pos_a)) + error = actual - constraint.target_distance_m + active += 1 + + print( + f" [{idx:03d}] DISTANCE | " + f"Link='{constraint.link_name}' | " + f"M{constraint.marker_id_a} <-> M{constraint.marker_id_b} | " + f"target={constraint.target_distance_m*1000:.2f} mm | " + f"actual={actual*1000:.2f} mm | " + f"error={error*1000:+.2f} mm" + ) + + elif isinstance(constraint, JointAxisConstraint): + if constraint.marker_id_parent not in positions or constraint.marker_id_child not in positions: + skipped += 1 + if show_skipped: + print( + f" [{idx:03d}] JOINT_AXIS | " + f"M{constraint.marker_id_parent} -> M{constraint.marker_id_child} | SKIPPED (missing marker)" + ) + continue + + pos_parent = positions[constraint.marker_id_parent] + pos_child = positions[constraint.marker_id_child] + delta = pos_child - pos_parent + actual = float(np.dot(delta, constraint.joint_axis)) + error = actual - constraint.target_delta_along_axis_m + active += 1 + + axis_str = np.array2string(constraint.joint_axis, precision=2, suppress_small=True) + print( + f" [{idx:03d}] JOINT_AXIS | " + f"{constraint.parent_link}(M{constraint.marker_id_parent}) -> " + f"{constraint.child_link}(M{constraint.marker_id_child}) | " + f"axis={axis_str} | " + f"target={constraint.target_delta_along_axis_m*1000:.2f} mm | " + f"actual={actual*1000:.2f} mm | " + f"error={error*1000:+.2f} mm" + ) + + print(f"[INFO] Active constraints: {active} | Skipped: {skipped}") + + +def print_observation_weight_summary(obs_weights: Dict[Tuple[int, int], float]) -> None: + if not obs_weights: + print("[INFO] Observation weighting: disabled or empty") + return + values = np.array(list(obs_weights.values()), dtype=np.float64) + print( + "[INFO] Observation weights: " + f"min={values.min():.3f} mean={values.mean():.3f} " + f"median={np.median(values):.3f} max={values.max():.3f}" + ) + + +# =================================================================== +# Main +# =================================================================== + +def main() -> None: + parser = argparse.ArgumentParser( + description="Multi-view bundle adjustment with rule-based geometric constraints" + ) + parser.add_argument( + "-det", "--detections", + action="append", + required=True, + help="*_aruco_detection.json files" + ) + parser.add_argument( + "-pose", "--poses", + action="append", + required=True, + help="*_camera_pose.json files" + ) + parser.add_argument( + "-robot", "--robot", + required=True, + help="robot.json" + ) + parser.add_argument( + "-outDir", "--outDir", + default=None, + help="Output directory" + ) + parser.add_argument( + "-lambdaWeight", "--lambdaWeight", + type=float, + default=100.0, + help="Constraint weight multiplier" + ) + parser.add_argument( + "--strictUniqueMarkerIds", + action="store_true", + help="Fail if a marker ID appears more than once in robot.json" + ) + parser.add_argument( + "--showSkippedConstraints", + action="store_true", + help="Print skipped constraints in the report" + ) + parser.add_argument( + "--noShowSkippedConstraints", + action="store_true", + help="Hide skipped constraints in the report" + ) + parser.add_argument( + "--saveConstraintReport", + action="store_true", + help="Save constraint report JSON files" + ) + parser.add_argument( + "--saveObservationWeightReport", + action="store_true", + help="Save observation-weight report JSON file" + ) + + args = parser.parse_args() + + if args.showSkippedConstraints and args.noShowSkippedConstraints: + print("[ERROR] Choose only one of --showSkippedConstraints or --noShowSkippedConstraints") + sys.exit(1) + + if len(args.detections) != len(args.poses): + print(f"[ERROR] Mismatch: {len(args.detections)} detection files vs {len(args.poses)} pose files") + sys.exit(1) + + robot_data = load_json(args.robot) + length_scale = get_length_scale(robot_data) + cfg = load_constraint_rule_config(robot_data, args) + + print("[STEP 1] Compile constraints from robot.json structure...") + print( + "[INFO] Constraint families: " + f"rigid_distance={'on' if cfg.rigid_distance_enabled else 'off'}, " + f"revolute={'on' if cfg.revolute_axis_enabled else 'off'}, " + f"prismatic={'on' if cfg.prismatic_orthogonal_enabled else 'off'}, " + f"chain_legacy={'on' if cfg.chain_axis_enabled else 'off'}, " + f"observation_weights={'on' if cfg.enable_observation_weights else 'off'}" + ) + marker_to_link, link_markers, constraints, issues, marker_meta = compile_constraints(robot_data, length_scale, cfg) + + for issue in issues: + print(issue) + + print(f"[INFO] Links with markers: {sum(1 for v in link_markers.values() if len(v) > 0)}") + print(f"[INFO] Unique marker IDs: {len(marker_to_link)}") + print_constraint_summary(constraints) + print_constraint_list(constraints) + + print("\n[STEP 2] Load observations and camera poses...") + marker_observations, cameras, obs_metadata = load_observations_and_poses(args.detections, args.poses) + print(f"[INFO] {len(cameras)} cameras, {len(marker_observations)} observed markers") + print(f"[INFO] Detection files loaded: {len(obs_metadata)}") + + print("\n[STEP 3] Initial triangulation...") + initial_pos = initial_triangulation(marker_observations, cameras) + print(f"[INFO] Triangulated {len(initial_pos)} markers") + + out_dir = args.outDir or os.path.dirname(args.detections[0]) or "." + os.makedirs(resolve_path(out_dir), exist_ok=True) + + initial_output_markers = [] + for marker_id, position in sorted(initial_pos.items()): + initial_output_markers.append( + { + "marker_id": int(marker_id), + "position_m": [float(x) for x in position], + "position_mm": [float(x * 1000.0) for x in position], + "link": marker_to_link.get(marker_id, "unknown"), + } + ) + + initial_output = { + "schema_version": "1.2", + "stage": "initial_triangulation", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_cameras": len(cameras), + "num_markers": len(initial_pos), + "num_constraints": len(constraints), + }, + "markers": initial_output_markers, + } + initial_out_file = os.path.join(out_dir, "aruco_positions_initial.json") + save_json(initial_out_file, initial_output) + print(f"[INFO] Initial triangulation saved to {initial_out_file}") + + obs_weights = compute_observation_weights( + marker_observations=marker_observations, + cameras=cameras, + initial_positions=initial_pos, + marker_meta=marker_meta, + cfg=cfg, + robot_data=robot_data, + ) + print_observation_weight_summary(obs_weights) + + print_constraints_with_errors( + "Constraint list BEFORE optimization", + constraints, + initial_pos, + show_skipped=cfg.show_skipped_constraints, + ) + + print("\n[STEP 4] Bundle adjustment with constraints...") + optimized_pos = optimize_with_constraints( + initial_pos, + marker_observations, + cameras, + constraints, + obs_weights, + lambda_constraint=args.lambdaWeight, + ) + + print_constraints_with_errors( + "Constraint list AFTER optimization", + constraints, + optimized_pos, + show_skipped=cfg.show_skipped_constraints, + ) + + output_markers = [] + for marker_id, position in sorted(optimized_pos.items()): + output_markers.append( + { + "marker_id": int(marker_id), + "position_m": [float(x) for x in position], + "position_mm": [float(x * 1000.0) for x in position], + "link": marker_to_link.get(marker_id, "unknown"), + } + ) + + output = { + "schema_version": "1.2", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_cameras": len(cameras), + "num_markers": len(optimized_pos), + "num_constraints": len(constraints), + }, + "markers": output_markers, + } + out_file = os.path.join(out_dir, "aruco_positions_optimized.json") + save_json(out_file, output) + print(f"\n[INFO] Saved to {out_file}") + + if args.saveConstraintReport: + report = { + "schema_version": "1.0", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_constraints": len(constraints), + "num_links_with_markers": sum(1 for v in link_markers.values() if len(v) > 0), + "num_observed_markers": len(marker_observations), + "num_triangulated_markers": len(initial_pos), + "num_optimized_markers": len(optimized_pos), + }, + "constraints": [], + } + for c in constraints: + if isinstance(c, MarkerDistanceConstraint): + report["constraints"].append( + { + "kind": "distance", + "link_name": c.link_name, + "marker_id_a": c.marker_id_a, + "marker_id_b": c.marker_id_b, + "target_distance_m": c.target_distance_m, + "weight": c.weight, + "source": c.source, + } + ) + else: + report["constraints"].append( + { + "kind": "joint_axis", + "parent_link": c.parent_link, + "child_link": c.child_link, + "marker_id_parent": c.marker_id_parent, + "marker_id_child": c.marker_id_child, + "joint_axis": [float(x) for x in c.joint_axis], + "target_delta_along_axis_m": c.target_delta_along_axis_m, + "weight": c.weight, + "source": c.source, + } + ) + report_file = os.path.join(out_dir, "constraint_report.json") + save_json(report_file, report) + print(f"[INFO] Constraint report saved to {report_file}") + + if args.saveObservationWeightReport: + obs_report = { + "schema_version": "1.0", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "summary": { + "num_weighted_observations": len(obs_weights), + }, + "observation_weights": [ + { + "marker_id": int(mid), + "observation_index": int(obs_idx), + "weight": float(w), + } + for (mid, obs_idx), w in sorted(obs_weights.items()) + ], + } + obs_file = os.path.join(out_dir, "observation_weight_report.json") + save_json(obs_file, obs_report) + print(f"[INFO] Observation-weight report saved to {obs_file}") + + +if __name__ == "__main__": + main() diff --git a/pipeline/4_robotState_estimation_seq.py b/pipeline/4_robotState_estimation_seq.py new file mode 100644 index 0000000..d72dcf5 --- /dev/null +++ b/pipeline/4_robotState_estimation_seq.py @@ -0,0 +1,1182 @@ +#!/usr/bin/env python3 +""" +4_robotState_estimation_v2.py + +Sequential robot-state estimation from optimized 3D ArUco marker positions. + +Mathematical idea +----------------- +This script estimates a robot state q from already optimized marker positions p_i^obs. + +The robot state consists of: + - a world pose for the root link (translation + rotation) + - one scalar variable for each actuated joint + +Forward kinematics predicts each marker position: + p_i^pred(q) = T_world(link(i); q) * p_i^local + +Instead of solving the whole robot at once, we solve it sequentially by kinematic prefix: + stage 0: root link + stage 1: root + first child layer + stage 2: root + first three connected elements ... + ... +Each stage reuses the previous solution as initialization and only activates the +joint variables that belong to the current kinematic prefix. + +The residual minimized at each stage is: + sum_i w_i ||p_i^pred(q) - p_i^obs||^2 + + weak priors on the active joint values + + weak prior on the root pose + +This is intentionally conservative and debuggable. It is designed so that: + - early links can be resolved first, + - later links only refine an already plausible state, + - one visible marker on an early rigid prefix can already fix a lot of state, + - ambiguous branches can later be resolved by adding more links. + +Optional future extension: + raw detections + camera poses can be added later as visibility / normal cues, + but this script intentionally works directly from aruco_positions_optimized*.json. + +Dependencies +------------ +numpy, scipy (optional but strongly recommended) +""" + +from __future__ import annotations + +import argparse +import json +import math +import os +import sys +import time +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np + + +# ============================================================================= +# JSON / path 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) -> Any: + with open(resolve_path(path), "r", encoding="utf-8") as f: + return json.load(f) + + +def save_json(path: str, data: Any) -> None: + with open(resolve_path(path), "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + +# ============================================================================= +# Small math helpers +# ============================================================================= + +def safe_norm(v: np.ndarray, eps: float = 1e-12) -> float: + return float(np.linalg.norm(v) + eps) + + +def normalize(v: np.ndarray, eps: float = 1e-12) -> np.ndarray: + v = np.asarray(v, dtype=np.float64) + return v / safe_norm(v, eps) + + +def get_length_scale(robot_data: Dict[str, Any]) -> float: + units = robot_data.get("units", {}) or {} + length_unit = str(units.get("length", "")).strip().lower() + if length_unit in ("mm", "millimeter", "millimeters"): + return 1.0 / 1000.0 + if length_unit in ("cm", "centimeter", "centimeters"): + return 1.0 / 100.0 + return 1.0 + + +def rotation_matrix_xyz(rx: float, ry: float, rz: float, degrees: bool = False) -> np.ndarray: + """Rotation order: X then Y then Z.""" + if degrees: + rx, ry, rz = math.radians(rx), math.radians(ry), math.radians(rz) + + cx, sx = math.cos(rx), math.sin(rx) + cy, sy = math.cos(ry), math.sin(ry) + cz, sz = math.cos(rz), math.sin(rz) + + rx_m = np.array([[1.0, 0.0, 0.0], + [0.0, cx, -sx], + [0.0, sx, cx]], dtype=np.float64) + ry_m = np.array([[cy, 0.0, sy], + [0.0, 1.0, 0.0], + [-sy, 0.0, cy]], dtype=np.float64) + rz_m = np.array([[cz, -sz, 0.0], + [sz, cz, 0.0], + [0.0, 0.0, 1.0]], dtype=np.float64) + return rz_m @ ry_m @ rx_m + + +def axis_angle_rotation(axis: np.ndarray, angle_rad: float) -> np.ndarray: + axis = normalize(axis) + x, y, z = axis + c = math.cos(angle_rad) + s = math.sin(angle_rad) + C = 1.0 - c + return np.array([ + [c + x * x * C, x * y * C - z * s, x * z * C + y * s], + [y * x * C + z * s, c + y * y * C, y * z * C - x * s], + [z * x * C - y * s, z * y * C + x * s, c + z * z * C] + ], dtype=np.float64) + + +def make_transform(R: Optional[np.ndarray] = None, t: Optional[np.ndarray] = None) -> np.ndarray: + T = np.eye(4, dtype=np.float64) + if R is not None: + T[:3, :3] = np.asarray(R, dtype=np.float64) + if t is not None: + T[:3, 3] = np.asarray(t, dtype=np.float64).reshape(3) + return T + + +def transform_point(T: np.ndarray, p: np.ndarray) -> np.ndarray: + p4 = np.ones(4, dtype=np.float64) + p4[:3] = np.asarray(p, dtype=np.float64).reshape(3) + return (T @ p4)[:3] + + +def matrix_to_euler_xyz(R: np.ndarray) -> np.ndarray: + """Return XYZ Euler angles in degrees.""" + R = np.asarray(R, dtype=np.float64) + sy = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0]) + singular = sy < 1e-9 + + if not singular: + x = math.atan2(R[2, 1], R[2, 2]) + y = math.atan2(-R[2, 0], sy) + z = math.atan2(R[1, 0], R[0, 0]) + else: + x = math.atan2(-R[1, 2], R[1, 1]) + y = math.atan2(-R[2, 0], sy) + z = 0.0 + return np.degrees([x, y, z]) + + +def wrap_angle_rad(a: float) -> float: + return (a + math.pi) % (2.0 * math.pi) - math.pi + + +def kabsch(P: np.ndarray, Q: np.ndarray, W: Optional[np.ndarray] = None) -> Tuple[np.ndarray, np.ndarray]: + """Find R, t such that R @ P + t ≈ Q.""" + P = np.asarray(P, dtype=np.float64) + Q = np.asarray(Q, dtype=np.float64) + assert P.shape == Q.shape and P.shape[1] == 3 + + if W is None: + W = np.ones(len(P), dtype=np.float64) + W = np.asarray(W, dtype=np.float64).reshape(-1) + W = np.maximum(W, 1e-12) + W = W / np.sum(W) + + p_cent = np.sum(P * W[:, None], axis=0) + q_cent = np.sum(Q * W[:, None], axis=0) + + P0 = P - p_cent + Q0 = Q - q_cent + H = (P0 * W[:, None]).T @ Q0 + + U, S, Vt = np.linalg.svd(H) + R = Vt.T @ U.T + if np.linalg.det(R) < 0: + Vt[-1, :] *= -1.0 + R = Vt.T @ U.T + t = q_cent - R @ p_cent + return R, t + + +# ============================================================================= +# Data structures +# ============================================================================= + +@dataclass +class MarkerInfo: + marker_id: int + link_name: str + local_pos_m: np.ndarray + size: Optional[float] = None + normal: Optional[np.ndarray] = None + name: str = "" + + +@dataclass +class LinkInfo: + name: str + parent: Optional[str] + joint_type: Optional[str] + joint_axis: Optional[np.ndarray] + joint_origin_m: np.ndarray + joint_rotation_deg: np.ndarray + joint_variable: Optional[str] + mount_position_m: np.ndarray + mount_rotation_deg: np.ndarray + markers: List[MarkerInfo] + + +@dataclass +class StageResult: + depth: int + active_links: List[str] + active_joint_vars: List[str] + mean_error_m: Optional[float] + rms_error_m: Optional[float] + worst_error_m: Optional[float] + num_markers_used: int + optimizer_info: Dict[str, Any] + + +@dataclass +class EstimationConfig: + root_pose_prior_weight: float = 0.1 + joint_prior_weight: float = 0.05 + marker_base_weight: float = 1.0 + robust_loss: str = "soft_l1" + max_iterations: int = 120 + include_joint_prior: bool = True + include_root_prior: bool = True + sequential: bool = True + show_stage_reports: bool = True + + +# ============================================================================= +# Robot parsing +# ============================================================================= + +def parse_robot(robot_data: Dict[str, Any]) -> Tuple[Dict[str, LinkInfo], Dict[int, MarkerInfo], List[str]]: + length_scale = get_length_scale(robot_data) + links = robot_data.get("links", {}) or {} + + link_infos: Dict[str, LinkInfo] = {} + marker_by_id: Dict[int, MarkerInfo] = {} + issues: List[str] = [] + + for link_name, link_data in links.items(): + parent = link_data.get("parent", None) + + joint = link_data.get("jointToParent", {}) or {} + joint_type = joint.get("type", None) + + axis = joint.get("axis", None) + if axis is not None: + axis = np.asarray(axis, dtype=np.float64) + if safe_norm(axis) > 1e-12: + axis = normalize(axis) + else: + axis = None + + joint_origin = np.asarray(joint.get("origin", [0, 0, 0]), dtype=np.float64) * float(length_scale) + joint_rotation_deg = np.asarray(joint.get("rotation", [0, 0, 0]), dtype=np.float64) + joint_variable = joint.get("variable", None) + + mount_position = np.asarray(link_data.get("mountPosition", [0, 0, 0]), dtype=np.float64) * float(length_scale) + mount_rotation_deg = np.asarray(link_data.get("mountRotation", [0, 0, 0]), dtype=np.float64) + + markers_data = link_data.get("markers", []) or [] + markers: List[MarkerInfo] = [] + for idx, m in enumerate(markers_data): + marker_id = int(m.get("id", -1)) + pos = m.get("position", None) + + if marker_id < 0 or pos is None or len(pos) != 3: + issues.append(f"[WARN] link='{link_name}': skipped invalid marker entry at index {idx}") + continue + + if marker_id in marker_by_id: + issues.append( + f"[WARN] duplicate marker ID {marker_id} in link '{link_name}' " + f"(already in '{marker_by_id[marker_id].link_name}'); using first occurrence" + ) + continue + + normal = None + if "normal" in m and isinstance(m["normal"], list) and len(m["normal"]) == 3: + normal = np.asarray(m["normal"], dtype=np.float64) + + marker = MarkerInfo( + marker_id=marker_id, + link_name=link_name, + local_pos_m=np.asarray(pos, dtype=np.float64) * float(length_scale), + size=m.get("size", None), + normal=normal, + name=str(m.get("name", f"marker_{marker_id}")), + ) + markers.append(marker) + marker_by_id[marker_id] = marker + + link_infos[link_name] = LinkInfo( + name=link_name, + parent=parent, + joint_type=joint_type, + joint_axis=axis, + joint_origin_m=joint_origin, + joint_rotation_deg=joint_rotation_deg, + joint_variable=joint_variable, + mount_position_m=mount_position, + mount_rotation_deg=mount_rotation_deg, + markers=markers, + ) + + return link_infos, marker_by_id, issues + + +def topological_links(link_infos: Dict[str, LinkInfo]) -> List[str]: + children = {name: [] for name in link_infos} + roots = [] + for name, info in link_infos.items(): + if info.parent is None: + roots.append(name) + else: + children.setdefault(info.parent, []).append(name) + + order: List[str] = [] + queue = list(roots) + seen = set() + + while queue: + cur = queue.pop(0) + if cur in seen: + continue + seen.add(cur) + order.append(cur) + for ch in children.get(cur, []): + queue.append(ch) + + for name in link_infos: + if name not in seen: + order.append(name) + return order + + +def compute_link_depths(link_infos: Dict[str, LinkInfo]) -> Dict[str, int]: + depths: Dict[str, int] = {} + + def depth_of(name: str) -> int: + if name in depths: + return depths[name] + info = link_infos[name] + if info.parent is None: + depths[name] = 0 + else: + depths[name] = depth_of(info.parent) + 1 + return depths[name] + + for name in link_infos: + depth_of(name) + return depths + + +# ============================================================================= +# Marker weights +# ============================================================================= + +def marker_weight(marker_info: MarkerInfo, base_weight: float = 1.0, ref_size: float = 25.0) -> float: + """ + Weight the marker residuals lightly by marker size. + Larger markers tend to be more stable in image space. + """ + w = base_weight + if marker_info.size is not None: + try: + size = float(marker_info.size) + if size > 0: + w *= max(0.35, math.sqrt(size / ref_size)) + except Exception: + pass + return w + + +# ============================================================================= +# Forward kinematics +# ============================================================================= + +def link_static_transform(link: LinkInfo) -> np.ndarray: + Rm = rotation_matrix_xyz(*link.mount_rotation_deg, degrees=True) + return make_transform(Rm, link.mount_position_m) + + +def joint_motion_transform(link: LinkInfo, q_value: float) -> np.ndarray: + joint_type = (link.joint_type or "").strip().lower() + if link.joint_axis is None: + return np.eye(4, dtype=np.float64) + + axis = normalize(link.joint_axis) + if joint_type == "linear": + return make_transform(np.eye(3), axis * float(q_value)) + if joint_type == "revolute": + return make_transform(axis_angle_rotation(axis, float(q_value)), np.zeros(3)) + return np.eye(4, dtype=np.float64) + + +def world_to_link_transform( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + cache: Dict[str, np.ndarray], +) -> np.ndarray: + if link_name in cache: + return cache[link_name] + + link = link_infos[link_name] + if link.parent is None: + T = make_transform(state["root_R"], state["root_t"]) + cache[link_name] = T + return T + + parent_T = world_to_link_transform(link.parent, link_infos, state, cache) + + # Convention: + # parent frame -> joint origin -> joint motion -> static joint rotation -> child mount transform + T = parent_T @ make_transform(np.eye(3), link.joint_origin_m) + + q = state["joint_values"].get(link.joint_variable, state["joint_defaults"].get(link.joint_variable, 0.0)) + T = T @ joint_motion_transform(link, q) + + R_static = rotation_matrix_xyz(*link.joint_rotation_deg, degrees=True) + T = T @ make_transform(R_static, np.zeros(3)) + + T = T @ link_static_transform(link) + + cache[link_name] = T + return T + + +def predict_marker_positions( + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], +) -> Dict[int, np.ndarray]: + pred: Dict[int, np.ndarray] = {} + cache: Dict[str, np.ndarray] = {} + for link_name, link in link_infos.items(): + T = world_to_link_transform(link_name, link_infos, state, cache) + for marker in link.markers: + pred[marker.marker_id] = transform_point(T, marker.local_pos_m) + return pred + + +# ============================================================================= +# Initial parameters +# ============================================================================= + +def infer_joint_defaults(robot_data: Dict[str, Any], link_infos: Dict[str, LinkInfo]) -> Dict[str, float]: + defaults_raw = robot_data.get("defaultPosition", {}) or {} + out: Dict[str, float] = {} + + for link in link_infos.values(): + var = link.joint_variable + if not var: + continue + raw_val = defaults_raw.get(var, 0.0) + joint_type = (link.joint_type or "").strip().lower() + if joint_type == "linear": + out[var] = float(raw_val) * get_length_scale(robot_data) + elif joint_type == "revolute": + out[var] = math.radians(float(raw_val)) + else: + out[var] = float(raw_val) + return out + + +def initial_root_pose( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], +) -> Tuple[np.ndarray, np.ndarray]: + roots = [name for name, info in link_infos.items() if info.parent is None] + if not roots: + return np.eye(3, dtype=np.float64), np.zeros(3, dtype=np.float64) + + root = roots[0] + root_info = link_infos[root] + + P = [] + Q = [] + W = [] + + for marker in root_info.markers: + if marker.marker_id in observed_markers: + obs = np.asarray(observed_markers[marker.marker_id]["position_m"], dtype=np.float64) + P.append(marker.local_pos_m) + Q.append(obs) + W.append(marker_weight(marker, base_weight=1.0)) + + if len(P) >= 3: + return kabsch(np.asarray(P), np.asarray(Q), np.asarray(W)) + + return np.eye(3, dtype=np.float64), np.zeros(3, dtype=np.float64) + + +def build_initial_state( + robot_data: Dict[str, Any], + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], +) -> Dict[str, Any]: + root_R, root_t = initial_root_pose(link_infos, observed_markers) + joint_defaults = infer_joint_defaults(robot_data, link_infos) + + return { + "root_R": root_R, + "root_t": root_t, + "joint_defaults": joint_defaults, + "joint_values": dict(joint_defaults), + } + + +# ============================================================================= +# Stage helpers +# ============================================================================= + +def active_links_for_depth(link_infos: Dict[str, LinkInfo], depths: Dict[str, int], max_depth: int) -> List[str]: + ordered = topological_links(link_infos) + return [name for name in ordered if depths.get(name, 0) <= max_depth] + + +def active_joint_vars_for_links(link_infos: Dict[str, LinkInfo], active_links: List[str]) -> List[str]: + vars_out: List[str] = [] + seen = set() + for name in active_links: + link = link_infos[name] + if not link.joint_variable: + continue + if link.joint_variable in seen: + continue + seen.add(link.joint_variable) + vars_out.append(link.joint_variable) + return vars_out + + +def active_observations_for_links( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + active_links: List[str], +) -> Dict[int, Dict[str, Any]]: + allowed_links = set(active_links) + out: Dict[int, Dict[str, Any]] = {} + for marker_id, obs in observed_markers.items(): + link_name = obs.get("link", None) + if link_name in allowed_links: + out[marker_id] = obs + return out + + +def stage_variables_to_vector( + state: Dict[str, Any], + active_joint_vars: List[str], +) -> np.ndarray: + try: + from scipy.spatial.transform import Rotation + root_rvec = Rotation.from_matrix(state["root_R"]).as_rotvec() + except Exception: + root_rvec = np.zeros(3, dtype=np.float64) + + root_t = np.asarray(state["root_t"], dtype=np.float64).reshape(3) + values = list(root_t) + list(np.asarray(root_rvec, dtype=np.float64)) + for var in active_joint_vars: + values.append(float(state["joint_values"].get(var, state["joint_defaults"].get(var, 0.0)))) + return np.asarray(values, dtype=np.float64) + + +def vector_to_stage_state( + x: np.ndarray, + template_state: Dict[str, Any], + active_joint_vars: List[str], +) -> Dict[str, Any]: + try: + from scipy.spatial.transform import Rotation + except Exception: + Rotation = None + + x = np.asarray(x, dtype=np.float64).reshape(-1) + + out = { + "root_t": x[0:3].copy(), + "root_R": template_state["root_R"].copy(), + "joint_defaults": dict(template_state["joint_defaults"]), + "joint_values": dict(template_state["joint_values"]), + } + + root_rvec = x[3:6].copy() + if Rotation is not None: + out["root_R"] = Rotation.from_rotvec(root_rvec).as_matrix() + else: + angle = safe_norm(root_rvec) + if angle < 1e-12: + out["root_R"] = np.eye(3, dtype=np.float64) + else: + out["root_R"] = axis_angle_rotation(root_rvec / angle, angle) + + idx = 6 + for var in active_joint_vars: + if idx >= len(x): + break + out["joint_values"][var] = float(x[idx]) + idx += 1 + + return out + + +def residuals_stage( + x: np.ndarray, + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + template_state: Dict[str, Any], + active_joint_vars: List[str], + cfg: EstimationConfig, +) -> np.ndarray: + state = vector_to_stage_state(x, template_state, active_joint_vars) + pred = predict_marker_positions(link_infos, state) + + res: List[float] = [] + + # Marker residuals + for marker_id, obs in observed_markers.items(): + if marker_id not in pred: + continue + + link_name = obs.get("link", None) + marker = None + if link_name is not None and link_name in link_infos: + for m in link_infos[link_name].markers: + if m.marker_id == marker_id: + marker = m + break + if marker is None: + continue + + w = marker_weight(marker, base_weight=cfg.marker_base_weight) + sqrt_w = math.sqrt(max(1e-12, w)) + + p_obs = np.asarray(obs["position_m"], dtype=np.float64) + p_pred = pred[marker_id] + d = (p_pred - p_obs) * sqrt_w + res.extend(d.tolist()) + + # Root prior + if cfg.include_root_prior: + root_t_prior = np.asarray(template_state["root_t"], dtype=np.float64) + try: + from scipy.spatial.transform import Rotation + root_rvec_prior = Rotation.from_matrix(template_state["root_R"]).as_rotvec() + except Exception: + root_rvec_prior = np.zeros(3, dtype=np.float64) + + root_t = x[0:3] + root_rvec = x[3:6] + w = math.sqrt(max(1e-12, cfg.root_pose_prior_weight)) + res.extend(((root_t - root_t_prior) * w).tolist()) + res.extend(((root_rvec - root_rvec_prior) * w).tolist()) + + # Joint priors + if cfg.include_joint_prior: + idx = 6 + w = math.sqrt(max(1e-12, cfg.joint_prior_weight)) + for var in active_joint_vars: + if idx >= len(x): + break + q = x[idx] + q0 = template_state["joint_defaults"].get(var, 0.0) + # Revolute angles should not wrap too aggressively during optimization, + # but the prior itself should be short-arc. + if abs(q0) <= math.pi * 2.5: + dq = wrap_angle_rad(float(q - q0)) + else: + dq = float(q - q0) + res.append(dq * w) + idx += 1 + + return np.asarray(res, dtype=np.float64) + + +def optimize_stage( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + template_state: Dict[str, Any], + active_joint_vars: List[str], + cfg: EstimationConfig, +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + try: + from scipy.optimize import least_squares + except ImportError: + print("[WARN] scipy not available; returning template state for this stage.") + return template_state, {"success": False, "message": "scipy unavailable"} + + x0 = stage_variables_to_vector(template_state, active_joint_vars) + + print(f"[INFO] Stage variables: {len(x0)} (root pose + {len(active_joint_vars)} active joints)") + + result = least_squares( + lambda x: residuals_stage(x, link_infos, observed_markers, template_state, active_joint_vars, cfg), + x0, + loss=cfg.robust_loss, + f_scale=1.0, + max_nfev=cfg.max_iterations, + verbose=1, + ) + + final_state = vector_to_stage_state(result.x, template_state, active_joint_vars) + opt_info = { + "cost": float(np.sum(result.fun ** 2)), + "success": bool(result.success), + "status": int(result.status), + "message": str(result.message), + "nfev": int(result.nfev), + "njev": int(getattr(result, "njev", -1)), + } + final_state["_optimizer"] = opt_info + return final_state, opt_info + + +def compute_error_stats( + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], +) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + pred = predict_marker_positions(link_infos, state) + + marker_reports: List[Dict[str, Any]] = [] + errors = [] + + marker_to_link = {} + marker_meta = {} + for link in link_infos.values(): + for m in link.markers: + marker_to_link[m.marker_id] = link.name + marker_meta[m.marker_id] = m + + for marker_id, obs in observed_markers.items(): + if marker_id not in pred: + continue + + p_obs = np.asarray(obs["position_m"], dtype=np.float64) + p_pred = pred[marker_id] + err = p_pred - p_obs + err_norm = float(np.linalg.norm(err)) + errors.append(err_norm) + + m = marker_meta.get(marker_id) + marker_reports.append( + { + "marker_id": int(marker_id), + "link": marker_to_link.get(marker_id, "unknown"), + "observed_position_m": [float(x) for x in p_obs], + "predicted_position_m": [float(x) for x in p_pred], + "error_m": [float(x) for x in err], + "error_norm_m": err_norm, + "error_norm_mm": err_norm * 1000.0, + "marker_size": None if m is None else m.size, + } + ) + + if errors: + arr = np.asarray(errors, dtype=np.float64) + stats = { + "num_markers_used": int(len(errors)), + "mean_error_m": float(np.mean(arr)), + "median_error_m": float(np.median(arr)), + "rms_error_m": float(np.sqrt(np.mean(arr ** 2))), + "worst_error_m": float(np.max(arr)), + "p80_error_m": float(np.quantile(arr, 0.80)), + "p90_error_m": float(np.quantile(arr, 0.90)), + } + else: + stats = { + "num_markers_used": 0, + "mean_error_m": None, + "median_error_m": None, + "rms_error_m": None, + "worst_error_m": None, + "p80_error_m": None, + "p90_error_m": None, + } + + return marker_reports, stats + + +def print_stage_stats(stage_idx: int, depth: int, stage_result: StageResult) -> None: + print( + f"[INFO] Stage {stage_idx} (depth={depth}) | " + f"active_links={len(stage_result.active_links)} | " + f"active_joint_vars={len(stage_result.active_joint_vars)} | " + f"markers={stage_result.num_markers_used}" + ) + if stage_result.mean_error_m is not None: + print( + f"[INFO] Stage {stage_idx} error: " + f"mean={stage_result.mean_error_m*1000.0:.2f} mm | " + f"rms={stage_result.rms_error_m*1000.0:.2f} mm | " + f"worst={stage_result.worst_error_m*1000.0:.2f} mm" + ) + if stage_result.optimizer_info: + print( + f"[INFO] Stage {stage_idx} optimizer: " + f"success={stage_result.optimizer_info.get('success', False)} | " + f"nfev={stage_result.optimizer_info.get('nfev', -1)} | " + f"cost={stage_result.optimizer_info.get('cost', 0.0):.6f}" + ) + + +def sequential_optimize_state( + robot_data: Dict[str, Any], + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + initial_state: Dict[str, Any], + cfg: EstimationConfig, +) -> Tuple[Dict[str, Any], List[StageResult]]: + """ + Sequential prefix optimization: + - solve links up to depth 0, + - then depth 1, + - then depth 2, ... + Each new stage starts from the previous state and only activates the + joint variables that belong to the current prefix. + """ + depths = compute_link_depths(link_infos) + max_depth = max(depths.values()) if depths else 0 + + current_state = { + "root_R": initial_state["root_R"].copy(), + "root_t": initial_state["root_t"].copy(), + "joint_defaults": dict(initial_state["joint_defaults"]), + "joint_values": dict(initial_state["joint_values"]), + } + + stage_results: List[StageResult] = [] + + for depth in range(0, max_depth + 1): + active_links = active_links_for_depth(link_infos, depths, depth) + active_joint_vars = active_joint_vars_for_links(link_infos, active_links) + active_obs = active_observations_for_links(link_infos, observed_markers, active_links) + + # If a stage introduces no observations, we still may want the chain prefix, + # but there is nothing to fit against. In that case just keep the current state. + if len(active_obs) == 0: + stage_state = current_state + stage_res = StageResult( + depth=depth, + active_links=active_links, + active_joint_vars=active_joint_vars, + mean_error_m=None, + rms_error_m=None, + worst_error_m=None, + num_markers_used=0, + optimizer_info={"skipped": True, "reason": "no active observations"}, + ) + stage_results.append(stage_res) + if cfg.show_stage_reports: + print_stage_stats(len(stage_results) - 1, depth, stage_res) + continue + + # Optimize only the active prefix variables. + template_state = current_state + optimized_state, opt_info = optimize_stage( + link_infos=link_infos, + observed_markers=active_obs, + template_state=template_state, + active_joint_vars=active_joint_vars, + cfg=cfg, + ) + + # Merge optimized active values into the running state. + current_state["root_R"] = optimized_state["root_R"].copy() + current_state["root_t"] = optimized_state["root_t"].copy() + current_state["joint_values"].update(optimized_state["joint_values"]) + + # Evaluate the active prefix after the stage. + marker_reports, stats = compute_error_stats(link_infos, current_state, active_obs) + + stage_res = StageResult( + depth=depth, + active_links=active_links, + active_joint_vars=active_joint_vars, + mean_error_m=stats["mean_error_m"], + rms_error_m=stats["rms_error_m"], + worst_error_m=stats["worst_error_m"], + num_markers_used=stats["num_markers_used"], + optimizer_info=opt_info, + ) + stage_results.append(stage_res) + + if cfg.show_stage_reports: + print_stage_stats(len(stage_results) - 1, depth, stage_res) + + if stage_results: + current_state["_optimizer"] = stage_results[-1].optimizer_info + return current_state, stage_results + + +# ============================================================================= +# Output +# ============================================================================= + +def state_to_json( + robot_data: Dict[str, Any], + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + marker_reports: List[Dict[str, Any]], + stats: Dict[str, Any], + input_file: str, + robot_file: str, + stage_results: List[StageResult], +) -> Dict[str, Any]: + movements = {} + for link_name in topological_links(link_infos): + link = link_infos[link_name] + if not link.joint_variable: + continue + + q = state["joint_values"].get(link.joint_variable, state["joint_defaults"].get(link.joint_variable, 0.0)) + joint_type = (link.joint_type or "").strip().lower() + + if joint_type == "revolute": + movements[link.joint_variable] = { + "value_rad": float(q), + "value_deg": float(math.degrees(q)), + "joint_type": joint_type, + "link": link_name, + } + elif joint_type == "linear": + movements[link.joint_variable] = { + "value_m": float(q), + "value_mm": float(q * 1000.0), + "joint_type": joint_type, + "link": link_name, + } + else: + movements[link.joint_variable] = { + "value": float(q), + "joint_type": joint_type, + "link": link_name, + } + + link_pose_entries = [] + cache: Dict[str, np.ndarray] = {} + for link_name in topological_links(link_infos): + T = world_to_link_transform(link_name, link_infos, state, cache) + R = T[:3, :3] + t = T[:3, 3] + link = link_infos[link_name] + num_used = sum(1 for m in link.markers if m.marker_id in observed_markers) + link_pose_entries.append( + { + "link": link_name, + "parent": link.parent, + "position_m": [float(x) for x in t], + "rotation_matrix": [[float(v) for v in row] for row in R], + "euler_xyz_deg": [float(x) for x in matrix_to_euler_xyz(R)], + "num_observed_markers": int(num_used), + "num_markers_total": int(len(link.markers)), + } + ) + + root_link = next((name for name, info in link_infos.items() if info.parent is None), None) + root_T = make_transform(state["root_R"], state["root_t"]) + + out = { + "schema_version": "2.0", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "source": { + "marker_positions_file": input_file, + "robot_file": robot_file, + }, + "summary": { + "num_links": int(len(link_infos)), + "num_observed_markers": int(len(observed_markers)), + "num_link_markers": int(sum(len(li.markers) for li in link_infos.values())), + "root_link": root_link, + "optimizer": state.get("_optimizer", {}), + "fit_stats": stats, + "stages": [ + { + "depth": int(s.depth), + "active_links": s.active_links, + "active_joint_vars": s.active_joint_vars, + "mean_error_m": s.mean_error_m, + "rms_error_m": s.rms_error_m, + "worst_error_m": s.worst_error_m, + "num_markers_used": int(s.num_markers_used), + "optimizer_info": s.optimizer_info, + } + for s in stage_results + ], + }, + "world_pose": { + "root_translation_m": [float(x) for x in root_T[:3, 3]], + "root_rotation_matrix": [[float(v) for v in row] for row in root_T[:3, :3]], + "root_euler_xyz_deg": [float(x) for x in matrix_to_euler_xyz(root_T[:3, :3])], + }, + "movements": movements, + "links": link_pose_entries, + "markers": marker_reports, + } + + return out + + +# ============================================================================= +# Main +# ============================================================================= + +def main() -> None: + parser = argparse.ArgumentParser( + description="Sequential robot state estimation from optimized ArUco marker positions" + ) + parser.add_argument( + "optimized_markers", + help="aruco_positions_optimized*.json" + ) + parser.add_argument( + "-robot", "--robot", + required=True, + help="robot.json" + ) + parser.add_argument( + "-out", "--out", + default=None, + help="Output robot_state.json path" + ) + parser.add_argument( + "--maxIterations", + type=int, + default=120, + help="Maximum least-squares iterations per stage" + ) + parser.add_argument( + "--jointPriorWeight", + type=float, + default=0.05, + help="Prior weight for joint variables" + ) + parser.add_argument( + "--rootPosePriorWeight", + type=float, + default=0.1, + help="Prior weight for root pose" + ) + parser.add_argument( + "--markerBaseWeight", + type=float, + default=1.0, + help="Base weight for marker residuals" + ) + parser.add_argument( + "--noJointPrior", + action="store_true", + help="Disable joint priors" + ) + parser.add_argument( + "--noRootPrior", + action="store_true", + help="Disable root pose prior" + ) + parser.add_argument( + "--noSequential", + action="store_true", + help="Disable sequential prefix optimization and do one global pass" + ) + + args = parser.parse_args() + + print("[STEP 1] Load robot.json and optimized marker positions...") + robot_data = load_json(args.robot) + link_infos, marker_by_id, issues = parse_robot(robot_data) + observed_markers = load_json(args.optimized_markers) + markers = observed_markers.get("markers", []) if isinstance(observed_markers, dict) else [] + + observed_map: Dict[int, Dict[str, Any]] = {} + for m in markers: + marker_id = int(m["marker_id"]) + observed_map[marker_id] = m + + for issue in issues: + print(issue) + + print(f"[INFO] Links: {len(link_infos)}") + print(f"[INFO] Known robot markers: {len(marker_by_id)}") + print(f"[INFO] Observed optimized markers: {len(observed_map)}") + + print("\n[STEP 2] Build initial robot state...") + initial_state = build_initial_state(robot_data, link_infos, observed_map) + print(f"[INFO] Initial root translation (m): {initial_state['root_t']}") + print(f"[INFO] Initial joint defaults: {initial_state['joint_defaults']}") + + cfg = EstimationConfig( + root_pose_prior_weight=float(args.rootPosePriorWeight), + joint_prior_weight=float(args.jointPriorWeight), + marker_base_weight=float(args.markerBaseWeight), + robust_loss="soft_l1", + max_iterations=int(args.maxIterations), + include_joint_prior=not args.noJointPrior, + include_root_prior=not args.noRootPrior, + sequential=not args.noSequential, + show_stage_reports=True, + ) + + print("\n[STEP 3] Estimate robot state...") + if cfg.sequential: + final_state, stage_results = sequential_optimize_state( + robot_data=robot_data, + link_infos=link_infos, + observed_markers=observed_map, + initial_state=initial_state, + cfg=cfg, + ) + else: + # Fallback: single global stage over the entire kinematic tree. + active_links = topological_links(link_infos) + active_joint_vars = active_joint_vars_for_links(link_infos, active_links) + final_state, opt_info = optimize_stage( + link_infos=link_infos, + observed_markers=observed_map, + template_state=initial_state, + active_joint_vars=active_joint_vars, + cfg=cfg, + ) + final_state["_optimizer"] = opt_info + stage_results = [] + + print("\n[STEP 4] Build report and save robot_state.json...") + marker_reports, stats = compute_error_stats(link_infos, final_state, observed_map) + + out_data = state_to_json( + robot_data=robot_data, + link_infos=link_infos, + state=final_state, + observed_markers=observed_map, + marker_reports=marker_reports, + stats=stats, + input_file=args.optimized_markers, + robot_file=args.robot, + stage_results=stage_results, + ) + + out_path = args.out + if out_path is None: + out_dir = os.path.dirname(args.optimized_markers) or "." + out_path = os.path.join(out_dir, "robot_state.json") + + save_json(out_path, out_data) + + print(f"[INFO] Saved to {out_path}") + if stats["mean_error_m"] is not None: + print(f"[INFO] Mean marker error: {stats['mean_error_m']*1000.0:.2f} mm") + print(f"[INFO] RMS marker error : {stats['rms_error_m']*1000.0:.2f} mm") + print(f"[INFO] Worst marker error: {stats['worst_error_m']*1000.0:.2f} mm") + else: + print("[INFO] No marker error statistics available.") + + +if __name__ == "__main__": + main() diff --git a/pipeline/4_robotState_estimation_v5.py b/pipeline/4_robotState_estimation_v5.py new file mode 100644 index 0000000..a5de588 --- /dev/null +++ b/pipeline/4_robotState_estimation_v5.py @@ -0,0 +1,1330 @@ +#!/usr/bin/env python3 +""" +4_robotState_estimation_v5.py + +Sequential robot-state estimation from optimized 3D ArUco marker positions. + +Mathematical idea +----------------- +This script estimates a robot state q directly from already optimized marker +positions p_i^obs (from aruco_positions_optimized*.json). + +The state is built incrementally along the kinematic chain: + 1) estimate the root link pose from observed root-link markers + 2) extend the active prefix link-by-link + 3) for each newly activated joint, estimate its scalar variable by a geometric + rule that matches the robot's degrees of freedom + +For each stage we keep the already estimated prefix fixed and only add the next +link(s) from the chain. This is intentionally *not* a generic global optimizer +by default. It is a deterministic geometric initializer that uses the robot +structure and marker geometry directly. + +The stage logic is controlled by robot.json::state_pose_params: + - numbers_of_Elements_to_consider_start + - numbers_of_Elements_to_consider_final + - solver_in_between_geometrical + - solver_after_geometrical + +Geometric rules used here +------------------------- +Rigid root / early prefix: + - root link pose is estimated from its observed markers with weighted Kabsch + +Linear joint (slider): + - the joint translates its whole descendant subtree along the joint axis + - q is estimated as the weighted mean projection of observed minus predicted + marker positions onto the world-space joint axis + +Revolute joint: + - the joint rotates its descendant subtree around the joint axis + - q is estimated by a coarse-to-fine 1D angular search that minimizes the + weighted marker residual over the active prefix + +Optional solver +--------------- +If enabled in state_pose_params, a least-squares refinement can be run after a +geometric stage or at the end. The geometric estimator remains the default and +is the first-class method in this script. + +Important limitation +-------------------- +This script works from marker positions only. It does not yet use image-space +marker orientation / normal / visibility cues. Those can be added later by +feeding in raw detections and camera poses. + +Dependencies +------------ +numpy, scipy (optional for refinement) +""" + +from __future__ import annotations + +import argparse +import copy +import json +import math +import os +import sys +import time +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +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) -> Any: + with open(resolve_path(path), 'r', encoding='utf-8') as f: + return json.load(f) + + +def save_json(path: str, data: Any) -> None: + with open(resolve_path(path), 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2) + + +# ============================================================================= +# Small math helpers +# ============================================================================= + +def safe_norm(v: np.ndarray, eps: float = 1e-12) -> float: + return float(np.linalg.norm(v) + eps) + + +def normalize(v: np.ndarray, eps: float = 1e-12) -> np.ndarray: + v = np.asarray(v, dtype=np.float64) + return v / safe_norm(v, eps) + + +def wrap_angle_rad(a: float) -> float: + return (a + math.pi) % (2.0 * math.pi) - math.pi + + +def get_length_scale(robot_data: Dict[str, Any]) -> float: + units = robot_data.get('units', {}) or {} + length_unit = str(units.get('length', '')).strip().lower() + if length_unit in ('mm', 'millimeter', 'millimeters'): + return 1.0 / 1000.0 + if length_unit in ('cm', 'centimeter', 'centimeters'): + return 1.0 / 100.0 + return 1.0 + + +def rotation_matrix_xyz(rx: float, ry: float, rz: float, degrees: bool = False) -> np.ndarray: + """Rotation order: X then Y then Z.""" + if degrees: + rx, ry, rz = math.radians(rx), math.radians(ry), math.radians(rz) + + cx, sx = math.cos(rx), math.sin(rx) + cy, sy = math.cos(ry), math.sin(ry) + cz, sz = math.cos(rz), math.sin(rz) + + rx_m = np.array([[1.0, 0.0, 0.0], + [0.0, cx, -sx], + [0.0, sx, cx]], dtype=np.float64) + ry_m = np.array([[cy, 0.0, sy], + [0.0, 1.0, 0.0], + [-sy, 0.0, cy]], dtype=np.float64) + rz_m = np.array([[cz, -sz, 0.0], + [sz, cz, 0.0], + [0.0, 0.0, 1.0]], dtype=np.float64) + return rz_m @ ry_m @ rx_m + + +def axis_angle_rotation(axis: np.ndarray, angle_rad: float) -> np.ndarray: + axis = normalize(axis) + x, y, z = axis + c = math.cos(angle_rad) + s = math.sin(angle_rad) + C = 1.0 - c + return np.array([ + [c + x * x * C, x * y * C - z * s, x * z * C + y * s], + [y * x * C + z * s, c + y * y * C, y * z * C - x * s], + [z * x * C - y * s, z * y * C + x * s, c + z * z * C] + ], dtype=np.float64) + + +def make_transform(R: Optional[np.ndarray] = None, t: Optional[np.ndarray] = None) -> np.ndarray: + T = np.eye(4, dtype=np.float64) + if R is not None: + T[:3, :3] = np.asarray(R, dtype=np.float64) + if t is not None: + T[:3, 3] = np.asarray(t, dtype=np.float64).reshape(3) + return T + + +def transform_point(T: np.ndarray, p: np.ndarray) -> np.ndarray: + p4 = np.ones(4, dtype=np.float64) + p4[:3] = np.asarray(p, dtype=np.float64).reshape(3) + return (T @ p4)[:3] + + +def matrix_to_euler_xyz(R: np.ndarray) -> np.ndarray: + R = np.asarray(R, dtype=np.float64) + sy = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0]) + singular = sy < 1e-9 + + if not singular: + x = math.atan2(R[2, 1], R[2, 2]) + y = math.atan2(-R[2, 0], sy) + z = math.atan2(R[1, 0], R[0, 0]) + else: + x = math.atan2(-R[1, 2], R[1, 1]) + y = math.atan2(-R[2, 0], sy) + z = 0.0 + return np.degrees([x, y, z]) + + +def kabsch(P: np.ndarray, Q: np.ndarray, W: Optional[np.ndarray] = None) -> Tuple[np.ndarray, np.ndarray]: + """Find R, t such that R @ P + t ≈ Q.""" + P = np.asarray(P, dtype=np.float64) + Q = np.asarray(Q, dtype=np.float64) + assert P.shape == Q.shape and P.shape[1] == 3 + + if W is None: + W = np.ones(len(P), dtype=np.float64) + W = np.asarray(W, dtype=np.float64).reshape(-1) + W = np.maximum(W, 1e-12) + W = W / np.sum(W) + + p_cent = np.sum(P * W[:, None], axis=0) + q_cent = np.sum(Q * W[:, None], axis=0) + + P0 = P - p_cent + Q0 = Q - q_cent + H = (P0 * W[:, None]).T @ Q0 + + U, S, Vt = np.linalg.svd(H) + R = Vt.T @ U.T + if np.linalg.det(R) < 0: + Vt[-1, :] *= -1.0 + R = Vt.T @ U.T + t = q_cent - R @ p_cent + return R, t + + +# ============================================================================= +# Data structures +# ============================================================================= + +@dataclass +class MarkerInfo: + marker_id: int + link_name: str + local_pos_m: np.ndarray + size: Optional[float] = None + normal: Optional[np.ndarray] = None + name: str = '' + + +@dataclass +class LinkInfo: + name: str + parent: Optional[str] + joint_type: Optional[str] + joint_axis: Optional[np.ndarray] + joint_origin_m: np.ndarray + joint_rotation_deg: np.ndarray + joint_variable: Optional[str] + mount_position_m: np.ndarray + mount_rotation_deg: np.ndarray + markers: List[MarkerInfo] + + +@dataclass +class StageResult: + stage_idx: int + active_links: List[str] + active_joint_vars: List[str] + num_markers_used: int + mean_error_m: Optional[float] + rms_error_m: Optional[float] + worst_error_m: Optional[float] + method: str + optimizer_info: Dict[str, Any] + + +@dataclass +class StatePoseParams: + numbers_of_elements_to_consider_start: int = 4 + numbers_of_elements_to_consider_final: int = 5 + solver_in_between_geometrical: bool = False + solver_after_geometrical: bool = False + geometric_passes_per_stage: int = 2 + revolute_search_coarse_deg: float = 5.0 + revolute_search_fine_deg: float = 1.0 + root_pose_min_markers: int = 3 + + +@dataclass +class EstimationConfig: + marker_base_weight: float = 1.0 + root_pose_prior_weight: float = 0.0 + joint_prior_weight: float = 0.0 + robust_loss: str = 'soft_l1' + max_iterations: int = 120 + show_stage_reports: bool = True + use_geometric_prefix: bool = True + + +# ============================================================================= +# Robot parsing +# ============================================================================= + +def parse_robot(robot_data: Dict[str, Any]) -> Tuple[Dict[str, LinkInfo], Dict[int, MarkerInfo], List[str]]: + length_scale = get_length_scale(robot_data) + links = robot_data.get('links', {}) or {} + + link_infos: Dict[str, LinkInfo] = {} + marker_by_id: Dict[int, MarkerInfo] = {} + issues: List[str] = [] + + for link_name, link_data in links.items(): + parent = link_data.get('parent', None) + + joint = link_data.get('jointToParent', {}) or {} + joint_type = joint.get('type', None) + + axis = joint.get('axis', None) + if axis is not None: + axis = np.asarray(axis, dtype=np.float64) + if safe_norm(axis) > 1e-12: + axis = normalize(axis) + else: + axis = None + + joint_origin = np.asarray(joint.get('origin', [0, 0, 0]), dtype=np.float64) * float(length_scale) + joint_rotation_deg = np.asarray(joint.get('rotation', [0, 0, 0]), dtype=np.float64) + joint_variable = joint.get('variable', None) + + mount_position = np.asarray(link_data.get('mountPosition', [0, 0, 0]), dtype=np.float64) * float(length_scale) + mount_rotation_deg = np.asarray(link_data.get('mountRotation', [0, 0, 0]), dtype=np.float64) + + markers_data = link_data.get('markers', []) or [] + markers: List[MarkerInfo] = [] + for idx, m in enumerate(markers_data): + marker_id = int(m.get('id', -1)) + pos = m.get('position', None) + + if marker_id < 0 or pos is None or len(pos) != 3: + issues.append(f"[WARN] link='{link_name}': skipped invalid marker entry at index {idx}") + continue + + if marker_id in marker_by_id: + issues.append( + f"[WARN] duplicate marker ID {marker_id} in link '{link_name}' " + f"(already in '{marker_by_id[marker_id].link_name}'); using first occurrence" + ) + continue + + normal = None + if isinstance(m.get('normal', None), list) and len(m['normal']) == 3: + normal = np.asarray(m['normal'], dtype=np.float64) + + marker = MarkerInfo( + marker_id=marker_id, + link_name=link_name, + local_pos_m=np.asarray(pos, dtype=np.float64) * float(length_scale), + size=m.get('size', None), + normal=normal, + name=str(m.get('name', f'marker_{marker_id}')), + ) + markers.append(marker) + marker_by_id[marker_id] = marker + + link_infos[link_name] = LinkInfo( + name=link_name, + parent=parent, + joint_type=joint_type, + joint_axis=axis, + joint_origin_m=joint_origin, + joint_rotation_deg=joint_rotation_deg, + joint_variable=joint_variable, + mount_position_m=mount_position, + mount_rotation_deg=mount_rotation_deg, + markers=markers, + ) + + return link_infos, marker_by_id, issues + + +def topological_links(link_infos: Dict[str, LinkInfo]) -> List[str]: + children = {name: [] for name in link_infos} + roots = [] + for name, info in link_infos.items(): + if info.parent is None: + roots.append(name) + else: + children.setdefault(info.parent, []).append(name) + + order: List[str] = [] + queue = list(roots) + seen = set() + + while queue: + cur = queue.pop(0) + if cur in seen: + continue + seen.add(cur) + order.append(cur) + for ch in children.get(cur, []): + queue.append(ch) + + for name in link_infos: + if name not in seen: + order.append(name) + return order + + +def compute_children_map(link_infos: Dict[str, LinkInfo]) -> Dict[str, List[str]]: + children: Dict[str, List[str]] = {name: [] for name in link_infos} + for name, info in link_infos.items(): + if info.parent is not None: + children.setdefault(info.parent, []).append(name) + return children + + +def compute_link_depths(link_infos: Dict[str, LinkInfo]) -> Dict[str, int]: + depths: Dict[str, int] = {} + + def depth_of(name: str) -> int: + if name in depths: + return depths[name] + info = link_infos[name] + if info.parent is None: + depths[name] = 0 + else: + depths[name] = depth_of(info.parent) + 1 + return depths[name] + + for name in link_infos: + depth_of(name) + return depths + + +# ============================================================================= +# State pose params +# ============================================================================= + +def load_state_pose_params(robot_data: Dict[str, Any]) -> StatePoseParams: + raw = robot_data.get('state_pose_params', {}) or {} + return StatePoseParams( + numbers_of_elements_to_consider_start=int(raw.get('numbers_of_Elements_to_consider_start', 4)), + numbers_of_elements_to_consider_final=int(raw.get('numbers_of_Elements_to_consider_final', 5)), + solver_in_between_geometrical=bool(raw.get('solver_in_between_geometrical', False)), + solver_after_geometrical=bool(raw.get('solver_after_geometrical', False)), + geometric_passes_per_stage=max(1, int(raw.get('geometric_passes_per_stage', 2))), + revolute_search_coarse_deg=float(raw.get('revolute_search_coarse_deg', 5.0)), + revolute_search_fine_deg=float(raw.get('revolute_search_fine_deg', 1.0)), + root_pose_min_markers=max(1, int(raw.get('root_pose_min_markers', 3))), + ) + + +# ============================================================================= +# Observations +# ============================================================================= + +def load_observed_markers(optimized_markers_file: str) -> Dict[int, Dict[str, Any]]: + data = load_json(optimized_markers_file) + if isinstance(data, dict): + markers = data.get('markers', []) or [] + elif isinstance(data, list): + markers = data + else: + markers = [] + + observed: Dict[int, Dict[str, Any]] = {} + for m in markers: + if not isinstance(m, dict): + continue + if 'marker_id' not in m: + continue + marker_id = int(m['marker_id']) + pos = m.get('position_m', None) + if pos is None or len(pos) != 3: + continue + obs = dict(m) + obs['marker_id'] = marker_id + obs['position_m'] = np.asarray(pos, dtype=np.float64) + observed[marker_id] = obs + return observed + + +def marker_weight(marker_info: MarkerInfo, base_weight: float = 1.0, ref_size: float = 25.0) -> float: + w = base_weight + if marker_info.size is not None: + try: + size = float(marker_info.size) + if size > 0: + w *= max(0.35, math.sqrt(size / ref_size)) + except Exception: + pass + return w + + +# ============================================================================= +# Forward kinematics +# ============================================================================= + +def link_static_transform(link: LinkInfo) -> np.ndarray: + Rm = rotation_matrix_xyz(*link.mount_rotation_deg, degrees=True) + return make_transform(Rm, link.mount_position_m) + + +def joint_motion_transform(link: LinkInfo, q_value: float) -> np.ndarray: + joint_type = (link.joint_type or '').strip().lower() + if link.joint_axis is None: + return np.eye(4, dtype=np.float64) + + axis = normalize(link.joint_axis) + if joint_type == 'linear': + return make_transform(np.eye(3), axis * float(q_value)) + if joint_type == 'revolute': + return make_transform(axis_angle_rotation(axis, float(q_value)), np.zeros(3)) + return np.eye(4, dtype=np.float64) + + +def world_to_link_transform( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray], + joint_override: Optional[Dict[str, float]] = None, +) -> np.ndarray: + override_items = tuple(sorted((joint_override or {}).items())) + cache_key = (link_name, override_items) + if cache_key in cache: + return cache[cache_key] + + link = link_infos[link_name] + if link.parent is None: + T = make_transform(state['root_R'], state['root_t']) + cache[cache_key] = T + return T + + parent_T = world_to_link_transform(link.parent, link_infos, state, cache, joint_override=joint_override) + + T = parent_T @ make_transform(np.eye(3), link.joint_origin_m) + + joint_values = state['joint_values'] + q = joint_override.get(link.name, joint_values.get(link.joint_variable, 0.0)) if joint_override else joint_values.get(link.joint_variable, 0.0) + T = T @ joint_motion_transform(link, q) + + R_static = rotation_matrix_xyz(*link.joint_rotation_deg, degrees=True) + T = T @ make_transform(R_static, np.zeros(3)) + + T = T @ link_static_transform(link) + + cache[cache_key] = T + return T + + +def predict_marker_positions( + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + joint_override: Optional[Dict[str, float]] = None, + active_links: Optional[set[str]] = None, +) -> Dict[int, np.ndarray]: + pred: Dict[int, np.ndarray] = {} + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray] = {} + for link_name, link in link_infos.items(): + if active_links is not None and link_name not in active_links: + continue + T = world_to_link_transform(link_name, link_infos, state, cache, joint_override=joint_override) + for marker in link.markers: + pred[marker.marker_id] = transform_point(T, marker.local_pos_m) + return pred + + +def collect_observations_by_link( + observed_markers: Dict[int, Dict[str, Any]], + active_links: Optional[set[str]] = None, +) -> Dict[str, List[int]]: + by_link: Dict[str, List[int]] = {} + for marker_id, obs in observed_markers.items(): + link = obs.get('link', None) + if link is None or link == 'unknown': + continue + if active_links is not None and link not in active_links: + continue + by_link.setdefault(link, []).append(marker_id) + return by_link + + +# ============================================================================= +# Initial geometric root pose +# ============================================================================= + +def initial_root_pose( + root_link: LinkInfo, + observed_markers: Dict[int, Dict[str, Any]], + min_markers: int = 3, +) -> Tuple[np.ndarray, np.ndarray]: + P = [] + Q = [] + W = [] + + for marker in root_link.markers: + if marker.marker_id in observed_markers: + obs = np.asarray(observed_markers[marker.marker_id]['position_m'], dtype=np.float64) + P.append(marker.local_pos_m) + Q.append(obs) + W.append(marker_weight(marker)) + + if len(P) >= min_markers: + return kabsch(np.asarray(P), np.asarray(Q), np.asarray(W)) + + if len(P) >= 1: + # Weak fallback: keep world axes and place root so the first marker matches. + marker = root_link.markers[0] + obs = np.asarray(observed_markers[marker.marker_id]['position_m'], dtype=np.float64) + return np.eye(3, dtype=np.float64), obs - marker.local_pos_m + + return np.eye(3, dtype=np.float64), np.zeros(3, dtype=np.float64) + + +# ============================================================================= +# Subtree helpers +# ============================================================================= + +def subtree_links(link_name: str, children_map: Dict[str, List[str]], active_links: set[str]) -> List[str]: + out: List[str] = [] + queue = [link_name] + while queue: + cur = queue.pop(0) + if cur not in active_links: + continue + out.append(cur) + for ch in children_map.get(cur, []): + if ch in active_links: + queue.append(ch) + return out + + +def marker_ids_in_links(link_infos: Dict[str, LinkInfo], links: List[str]) -> List[int]: + ids: List[int] = [] + for link_name in links: + for m in link_infos[link_name].markers: + ids.append(m.marker_id) + return ids + + +# ============================================================================= +# Geometric joint estimation +# ============================================================================= + +def current_joint_value(state: Dict[str, Any], link: LinkInfo) -> float: + return float(state['joint_values'].get(link.joint_variable, 0.0)) if link.joint_variable else 0.0 + + +def world_joint_axis_for_link( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], +) -> np.ndarray: + link = link_infos[link_name] + if link.parent is None or link.joint_axis is None: + return np.array([1.0, 0.0, 0.0], dtype=np.float64) + + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray] = {} + parent_T = world_to_link_transform(link.parent, link_infos, state, cache) + axis_world = parent_T[:3, :3] @ normalize(link.joint_axis) + return normalize(axis_world) + + +def estimate_linear_joint_value( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], + children_map: Dict[str, List[str]], +) -> Tuple[float, Dict[str, Any]]: + link = link_infos[link_name] + if link.joint_variable is None: + return 0.0, {'reason': 'no_joint_variable'} + + subtree = subtree_links(link_name, children_map, active_links) + obs_ids = [mid for mid in marker_ids_in_links(link_infos, subtree) if mid in observed_markers] + if not obs_ids: + return current_joint_value(state, link), {'reason': 'no_observations'} + + axis_world = world_joint_axis_for_link(link_name, link_infos, state) + + # Evaluate current prediction with this joint forced to zero. + override = {link_name: 0.0} + pred0 = predict_marker_positions(link_infos, state, joint_override=override, active_links=active_links) + + num = 0.0 + den = 0.0 + used = 0 + per_marker = [] + for mid in obs_ids: + if mid not in pred0: + continue + marker = next((m for m in link_infos[observed_markers[mid]['link']].markers if m.marker_id == mid), None) + w = marker_weight(marker) if marker is not None else 1.0 + p_obs = np.asarray(observed_markers[mid]['position_m'], dtype=np.float64) + p0 = pred0[mid] + q_i = float(np.dot(axis_world, p_obs - p0)) + num += w * q_i + den += w + used += 1 + per_marker.append({'marker_id': mid, 'q_i': q_i, 'weight': w}) + + if den <= 1e-12: + return current_joint_value(state, link), {'reason': 'zero_weight'} + + q = num / den + return float(q), { + 'reason': 'weighted_projection', + 'used_markers': used, + 'axis_world': [float(x) for x in axis_world], + 'per_marker': per_marker, + } + + +def marker_residual_error_for_state( + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: Optional[set[str]] = None, +) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + pred = predict_marker_positions(link_infos, state, active_links=active_links) + + marker_reports: List[Dict[str, Any]] = [] + errors = [] + for marker_id, obs in observed_markers.items(): + if marker_id not in pred: + continue + p_obs = np.asarray(obs['position_m'], dtype=np.float64) + p_pred = pred[marker_id] + err = p_pred - p_obs + err_norm = float(np.linalg.norm(err)) + errors.append(err_norm) + marker_reports.append({ + 'marker_id': int(marker_id), + 'link': obs.get('link', 'unknown'), + 'observed_position_m': [float(x) for x in p_obs], + 'predicted_position_m': [float(x) for x in p_pred], + 'error_m': [float(x) for x in err], + 'error_norm_m': err_norm, + 'error_norm_mm': err_norm * 1000.0, + 'marker_size': obs.get('position_mm', None), + }) + + if errors: + arr = np.asarray(errors, dtype=np.float64) + stats = { + 'num_markers_used': int(len(errors)), + 'mean_error_m': float(np.mean(arr)), + 'median_error_m': float(np.median(arr)), + 'rms_error_m': float(np.sqrt(np.mean(arr ** 2))), + 'worst_error_m': float(np.max(arr)), + 'p80_error_m': float(np.quantile(arr, 0.80)), + 'p90_error_m': float(np.quantile(arr, 0.90)), + } + else: + stats = { + 'num_markers_used': 0, + 'mean_error_m': None, + 'median_error_m': None, + 'rms_error_m': None, + 'worst_error_m': None, + 'p80_error_m': None, + 'p90_error_m': None, + } + + return marker_reports, stats + + +def revolute_weighted_error( + link_name: str, + q: float, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], + children_map: Dict[str, List[str]], +) -> float: + link = link_infos[link_name] + if link.joint_variable is None: + return float('inf') + + subtree = subtree_links(link_name, children_map, active_links) + obs_ids = [mid for mid in marker_ids_in_links(link_infos, subtree) if mid in observed_markers] + if not obs_ids: + return float('inf') + + override = {link_name: q} + pred = predict_marker_positions(link_infos, state, joint_override=override, active_links=active_links) + + err = 0.0 + wsum = 0.0 + for mid in obs_ids: + if mid not in pred: + continue + marker = next((m for m in link_infos[observed_markers[mid]['link']].markers if m.marker_id == mid), None) + w = marker_weight(marker) if marker is not None else 1.0 + d = pred[mid] - np.asarray(observed_markers[mid]['position_m'], dtype=np.float64) + err += w * float(np.dot(d, d)) + wsum += w + + if wsum <= 1e-12: + return float('inf') + return err / wsum + + +def estimate_revolute_joint_value( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], + children_map: Dict[str, List[str]], + coarse_step_deg: float = 5.0, + fine_step_deg: float = 1.0, +) -> Tuple[float, Dict[str, Any]]: + link = link_infos[link_name] + if link.joint_variable is None: + return 0.0, {'reason': 'no_joint_variable'} + + subtree = subtree_links(link_name, children_map, active_links) + obs_ids = [mid for mid in marker_ids_in_links(link_infos, subtree) if mid in observed_markers] + if not obs_ids: + return current_joint_value(state, link), {'reason': 'no_observations'} + + q0 = current_joint_value(state, link) + best_q = q0 + best_e = float('inf') + used = len(obs_ids) + + # Coarse-to-fine search around the current value, but still wide enough to catch flips. + centers = [q0, 0.0] + span_list = [math.pi, math.pi / 4.0, math.pi / 16.0] + step_list = [math.radians(coarse_step_deg), math.radians(fine_step_deg), math.radians(max(0.25, fine_step_deg / 2.0))] + + for center in centers: + for span, step in zip(span_list, step_list): + if step <= 0: + continue + qs = np.arange(center - span, center + span + 0.5 * step, step) + for q in qs: + e = revolute_weighted_error(link_name, float(q), link_infos, state, observed_markers, active_links, children_map) + if e < best_e: + best_e = e + best_q = float(q) + center = best_q + + return wrap_angle_rad(best_q), { + 'reason': 'coarse_to_fine_scan', + 'used_markers': used, + 'best_error': best_e, + 'q0': q0, + 'search_span_rad': math.pi, + } + + +# ============================================================================= +# Geometric prefix estimation +# ============================================================================= + +def build_state_template(link_infos: Dict[str, LinkInfo]) -> Dict[str, Any]: + return { + 'root_R': np.eye(3, dtype=np.float64), + 'root_t': np.zeros(3, dtype=np.float64), + 'joint_values': {li.joint_variable: 0.0 for li in link_infos.values() if li.joint_variable}, + } + + +def copy_state(state: Dict[str, Any]) -> Dict[str, Any]: + return { + 'root_R': np.asarray(state['root_R'], dtype=np.float64).copy(), + 'root_t': np.asarray(state['root_t'], dtype=np.float64).copy(), + 'joint_values': dict(state.get('joint_values', {})), + } + + +def active_observations_for_links( + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], +) -> Dict[int, Dict[str, Any]]: + out: Dict[int, Dict[str, Any]] = {} + for marker_id, obs in observed_markers.items(): + link_name = obs.get('link', None) + if link_name in active_links: + out[marker_id] = obs + return out + + +def optimize_geometric_prefix( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + initial_state: Dict[str, Any], + active_links: List[str], + cfg: StatePoseParams, +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + active_set = set(active_links) + children_map = compute_children_map(link_infos) + stage_obs = active_observations_for_links(observed_markers, active_set) + + state = copy_state(initial_state) + stage_info: Dict[str, Any] = { + 'method': 'geometric_prefix', + 'active_links': active_links, + 'active_observations': len(stage_obs), + 'joint_updates': [], + } + + # Root pose is refreshed from the root markers if possible. + root_link_name = next((ln for ln in active_links if link_infos[ln].parent is None), None) + if root_link_name is not None: + root_link = link_infos[root_link_name] + root_R, root_t = initial_root_pose(root_link, stage_obs, min_markers=cfg.root_pose_min_markers) + state['root_R'] = root_R + state['root_t'] = root_t + stage_info['root_link'] = root_link_name + stage_info['root_pose_source'] = 'kabsch_root_markers' if len([m for m in root_link.markers if m.marker_id in stage_obs]) >= cfg.root_pose_min_markers else 'weak_single_marker_fallback' + + # Forward geometric passes. + active_joint_links = [ln for ln in active_links if link_infos[ln].parent is not None] + for pass_idx in range(cfg.geometric_passes_per_stage): + pass_updates = [] + for link_name in active_joint_links: + link = link_infos[link_name] + jt = (link.joint_type or '').strip().lower() + if jt == 'linear': + q_old = current_joint_value(state, link) + q_new, info = estimate_linear_joint_value(link_name, link_infos, state, stage_obs, active_set, children_map) + state['joint_values'][link.joint_variable] = q_new + pass_updates.append({'link': link_name, 'joint_variable': link.joint_variable, 'joint_type': jt, 'old': q_old, 'new': q_new, 'info': info}) + elif jt == 'revolute': + q_old = current_joint_value(state, link) + q_new, info = estimate_revolute_joint_value( + link_name, link_infos, state, stage_obs, active_set, children_map, + coarse_step_deg=cfg.revolute_search_coarse_deg, + fine_step_deg=cfg.revolute_search_fine_deg, + ) + state['joint_values'][link.joint_variable] = q_new + pass_updates.append({'link': link_name, 'joint_variable': link.joint_variable, 'joint_type': jt, 'old': q_old, 'new': q_new, 'info': info}) + stage_info[f'pass_{pass_idx+1}_updates'] = pass_updates + + marker_reports, stats = marker_residual_error_for_state(link_infos, state, stage_obs, active_links=active_set) + stage_info['fit_stats'] = stats + stage_info['num_markers_used'] = stats['num_markers_used'] + return state, stage_info + + +# ============================================================================= +# Optional least-squares refinement +# ============================================================================= + +def stage_variables_to_vector(state: Dict[str, Any], active_joint_vars: List[str]) -> np.ndarray: + try: + from scipy.spatial.transform import Rotation + root_rvec = Rotation.from_matrix(state['root_R']).as_rotvec() + except Exception: + root_rvec = np.zeros(3, dtype=np.float64) + + root_t = np.asarray(state['root_t'], dtype=np.float64).reshape(3) + values = list(root_t) + list(np.asarray(root_rvec, dtype=np.float64)) + for var in active_joint_vars: + values.append(float(state['joint_values'].get(var, 0.0))) + return np.asarray(values, dtype=np.float64) + + +def vector_to_state(x: np.ndarray, template_state: Dict[str, Any], active_joint_vars: List[str]) -> Dict[str, Any]: + try: + from scipy.spatial.transform import Rotation + except Exception: + Rotation = None + + x = np.asarray(x, dtype=np.float64).reshape(-1) + out = { + 'root_t': x[0:3].copy(), + 'root_R': template_state['root_R'].copy(), + 'joint_values': dict(template_state['joint_values']), + } + + root_rvec = x[3:6].copy() + if Rotation is not None: + out['root_R'] = Rotation.from_rotvec(root_rvec).as_matrix() + else: + angle = safe_norm(root_rvec) + out['root_R'] = np.eye(3, dtype=np.float64) if angle < 1e-12 else axis_angle_rotation(root_rvec / angle, angle) + + idx = 6 + for var in active_joint_vars: + if idx >= len(x): + break + out['joint_values'][var] = float(x[idx]) + idx += 1 + return out + + +def residuals_stage( + x: np.ndarray, + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + template_state: Dict[str, Any], + active_joint_vars: List[str], + active_links: set[str], + cfg: EstimationConfig, +) -> np.ndarray: + state = vector_to_state(x, template_state, active_joint_vars) + pred = predict_marker_positions(link_infos, state, active_links=active_links) + res: List[float] = [] + + for marker_id, obs in observed_markers.items(): + if marker_id not in pred: + continue + link_name = obs.get('link', None) + marker = None + if link_name is not None and link_name in link_infos: + marker = next((m for m in link_infos[link_name].markers if m.marker_id == marker_id), None) + w = marker_weight(marker, base_weight=cfg.marker_base_weight) if marker is not None else cfg.marker_base_weight + sqrt_w = math.sqrt(max(1e-12, w)) + p_obs = np.asarray(obs['position_m'], dtype=np.float64) + p_pred = pred[marker_id] + d = (p_pred - p_obs) * sqrt_w + res.extend(d.tolist()) + + if cfg.root_pose_prior_weight > 0: + root_t_prior = np.asarray(template_state['root_t'], dtype=np.float64) + try: + from scipy.spatial.transform import Rotation + root_rvec_prior = Rotation.from_matrix(template_state['root_R']).as_rotvec() + except Exception: + root_rvec_prior = np.zeros(3, dtype=np.float64) + w = math.sqrt(max(1e-12, cfg.root_pose_prior_weight)) + res.extend(((x[0:3] - root_t_prior) * w).tolist()) + res.extend(((x[3:6] - root_rvec_prior) * w).tolist()) + + if cfg.joint_prior_weight > 0: + idx = 6 + w = math.sqrt(max(1e-12, cfg.joint_prior_weight)) + for var in active_joint_vars: + if idx >= len(x): + break + q = x[idx] + q0 = template_state['joint_values'].get(var, 0.0) + dq = wrap_angle_rad(float(q - q0)) if abs(q0) <= math.pi * 2.5 else float(q - q0) + res.append(dq * w) + idx += 1 + + return np.asarray(res, dtype=np.float64) + + +def optional_solver_refine( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + state: Dict[str, Any], + active_links: List[str], + cfg: EstimationConfig, +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + try: + from scipy.optimize import least_squares + except Exception: + return state, {'success': False, 'message': 'scipy unavailable'} + + active_joint_vars = [link_infos[ln].joint_variable for ln in active_links if link_infos[ln].joint_variable is not None] + x0 = stage_variables_to_vector(state, active_joint_vars) + active_set = set(active_links) + + result = least_squares( + lambda x: residuals_stage(x, link_infos, observed_markers, state, active_joint_vars, active_set, cfg), + x0, + loss=cfg.robust_loss, + f_scale=1.0, + max_nfev=cfg.max_iterations, + verbose=1, + ) + refined = vector_to_state(result.x, state, active_joint_vars) + info = { + 'success': bool(result.success), + 'status': int(result.status), + 'message': str(result.message), + 'nfev': int(result.nfev), + 'cost': float(np.sum(result.fun ** 2)), + } + return refined, info + + +# ============================================================================= +# Sequential estimation driver +# ============================================================================= + +def sequential_geometric_estimation( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + params: StatePoseParams, + cfg: EstimationConfig, +) -> Tuple[Dict[str, Any], List[StageResult]]: + ordered = topological_links(link_infos) + if not ordered: + return build_state_template(link_infos), [] + + start_n = max(1, min(int(params.numbers_of_elements_to_consider_start), len(ordered))) + final_n = max(start_n, min(int(params.numbers_of_elements_to_consider_final), len(ordered))) + + state = build_state_template(link_infos) + stage_results: List[StageResult] = [] + + for n in range(start_n, final_n + 1): + active_links = ordered[:n] + stage_state, stage_info = optimize_geometric_prefix(link_infos, observed_markers, state, active_links, params) + state = stage_state + + if params.solver_in_between_geometrical: + state, solver_info = optional_solver_refine(link_infos, observed_markers, state, active_links, cfg) + stage_info['solver_in_between'] = solver_info + + stage_obs = active_observations_for_links(observed_markers, set(active_links)) + marker_reports, stats = marker_residual_error_for_state(link_infos, state, stage_obs, active_links=set(active_links)) + + active_joint_vars = [link_infos[ln].joint_variable for ln in active_links if link_infos[ln].joint_variable is not None] + stage_result = StageResult( + stage_idx=len(stage_results), + active_links=active_links, + active_joint_vars=active_joint_vars, + num_markers_used=stats['num_markers_used'], + mean_error_m=stats['mean_error_m'], + rms_error_m=stats['rms_error_m'], + worst_error_m=stats['worst_error_m'], + method='geometric_prefix' + ('+solver' if params.solver_in_between_geometrical else ''), + optimizer_info=stage_info, + ) + stage_results.append(stage_result) + + if cfg.show_stage_reports: + print( + f"[INFO] Stage {stage_result.stage_idx} | links={len(active_links)} | joints={len(active_joint_vars)} | " + f"markers={stage_result.num_markers_used} | mean={None if stats['mean_error_m'] is None else stats['mean_error_m']*1000.0:.2f} mm" + ) + + if params.solver_after_geometrical: + state, solver_info = optional_solver_refine(link_infos, observed_markers, state, ordered[:final_n], cfg) + if stage_results: + stage_results[-1].optimizer_info['solver_after_geometrical'] = solver_info + + return state, stage_results + + +# ============================================================================= +# Output +# ============================================================================= + +def state_to_json( + robot_data: Dict[str, Any], + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + marker_reports: List[Dict[str, Any]], + stats: Dict[str, Any], + input_file: str, + robot_file: str, + stage_results: List[StageResult], + params: StatePoseParams, +) -> Dict[str, Any]: + movements = {} + for link_name in topological_links(link_infos): + link = link_infos[link_name] + if not link.joint_variable: + continue + + q = float(state['joint_values'].get(link.joint_variable, 0.0)) + jt = (link.joint_type or '').strip().lower() + entry = { + 'joint_type': jt, + 'link': link_name, + 'estimated': True, + } + if jt == 'revolute': + entry['value_rad'] = q + entry['value_deg'] = float(math.degrees(q)) + elif jt == 'linear': + entry['value_m'] = q + entry['value_mm'] = q * 1000.0 + else: + entry['value'] = q + movements[link.joint_variable] = entry + + link_pose_entries = [] + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray] = {} + for link_name in topological_links(link_infos): + T = world_to_link_transform(link_name, link_infos, state, cache) + R = T[:3, :3] + t = T[:3, 3] + link = link_infos[link_name] + num_used = sum(1 for m in link.markers if m.marker_id in observed_markers) + link_pose_entries.append({ + 'link': link_name, + 'parent': link.parent, + 'position_m': [float(x) for x in t], + 'rotation_matrix': [[float(v) for v in row] for row in R], + 'euler_xyz_deg': [float(x) for x in matrix_to_euler_xyz(R)], + 'num_observed_markers': int(num_used), + 'num_markers_total': int(len(link.markers)), + }) + + root_link = next((name for name, info in link_infos.items() if info.parent is None), None) + root_T = make_transform(state['root_R'], state['root_t']) + + out = { + 'schema_version': '3.0', + 'created_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), + 'source': { + 'marker_positions_file': input_file, + 'robot_file': robot_file, + }, + 'state_pose_params': { + 'numbers_of_Elements_to_consider_start': params.numbers_of_elements_to_consider_start, + 'numbers_of_Elements_to_consider_final': params.numbers_of_elements_to_consider_final, + 'solver_in_between_geometrical': params.solver_in_between_geometrical, + 'solver_after_geometrical': params.solver_after_geometrical, + 'geometric_passes_per_stage': params.geometric_passes_per_stage, + 'revolute_search_coarse_deg': params.revolute_search_coarse_deg, + 'revolute_search_fine_deg': params.revolute_search_fine_deg, + 'root_pose_min_markers': params.root_pose_min_markers, + }, + 'summary': { + 'num_links': int(len(link_infos)), + 'num_observed_markers': int(len(observed_markers)), + 'num_link_markers': int(sum(len(li.markers) for li in link_infos.values())), + 'root_link': root_link, + 'fit_stats': stats, + 'optimizer': state.get('_optimizer', {}), + 'stages': [ + { + 'stage_idx': int(s.stage_idx), + 'active_links': s.active_links, + 'active_joint_vars': s.active_joint_vars, + 'num_markers_used': int(s.num_markers_used), + 'mean_error_m': s.mean_error_m, + 'rms_error_m': s.rms_error_m, + 'worst_error_m': s.worst_error_m, + 'method': s.method, + 'optimizer_info': s.optimizer_info, + } + for s in stage_results + ], + }, + 'world_pose': { + 'root_translation_m': [float(x) for x in root_T[:3, 3]], + 'root_rotation_matrix': [[float(v) for v in row] for row in root_T[:3, :3]], + 'root_euler_xyz_deg': [float(x) for x in matrix_to_euler_xyz(root_T[:3, :3])], + }, + 'movements': movements, + 'links': link_pose_entries, + 'markers': marker_reports, + } + return out + + +# ============================================================================= +# Main +# ============================================================================= + +def main() -> None: + parser = argparse.ArgumentParser( + description='Sequential geometric robot-state estimation from optimized ArUco marker positions' + ) + parser.add_argument('optimized_markers', help='aruco_positions_optimized*.json') + parser.add_argument('-robot', '--robot', required=True, help='robot.json') + parser.add_argument('-out', '--out', default=None, help='Output robot_state.json path') + parser.add_argument('--maxIterations', type=int, default=120, help='Maximum least-squares iterations per optional refinement stage') + parser.add_argument('--jointPriorWeight', type=float, default=0.0, help='Prior weight for joint variables during optional solver refinement') + parser.add_argument('--rootPosePriorWeight', type=float, default=0.0, help='Prior weight for root pose during optional solver refinement') + parser.add_argument('--markerBaseWeight', type=float, default=1.0, help='Base weight for marker residuals') + parser.add_argument('--noSequential', action='store_true', help='Disable sequential prefix mode and run one prefix stage only') + parser.add_argument('--forceSolverAfter', action='store_true', help='Force solver after the geometric prefix regardless of robot.json') + parser.add_argument('--forceSolverBetween', action='store_true', help='Force solver between geometric stages regardless of robot.json') + + args = parser.parse_args() + + print('[STEP 1] Load robot.json and optimized marker positions...') + robot_data = load_json(args.robot) + link_infos, marker_by_id, issues = parse_robot(robot_data) + observed_map = load_observed_markers(args.optimized_markers) + params = load_state_pose_params(robot_data) + + if args.noSequential: + params.numbers_of_elements_to_consider_start = len(topological_links(link_infos)) + params.numbers_of_elements_to_consider_final = len(topological_links(link_infos)) + + if args.forceSolverBetween: + params.solver_in_between_geometrical = True + if args.forceSolverAfter: + params.solver_after_geometrical = True + + for issue in issues: + print(issue) + + print(f"[INFO] Links: {len(link_infos)}") + print(f"[INFO] Known robot markers: {len(marker_by_id)}") + print(f"[INFO] Observed optimized markers: {len(observed_map)}") + print(f"[INFO] state_pose_params: start={params.numbers_of_elements_to_consider_start}, final={params.numbers_of_elements_to_consider_final}, between={params.solver_in_between_geometrical}, after={params.solver_after_geometrical}") + + cfg = EstimationConfig( + marker_base_weight=float(args.markerBaseWeight), + root_pose_prior_weight=float(args.rootPosePriorWeight), + joint_prior_weight=float(args.jointPriorWeight), + robust_loss='soft_l1', + max_iterations=int(args.maxIterations), + show_stage_reports=True, + use_geometric_prefix=True, + ) + + print('\n[STEP 2] Geometric sequential pose estimation...') + final_state, stage_results = sequential_geometric_estimation( + link_infos=link_infos, + observed_markers=observed_map, + params=params, + cfg=cfg, + ) + + print('\n[STEP 3] Build report and save robot_state.json...') + marker_reports, stats = marker_residual_error_for_state(link_infos, final_state, observed_map) + + out_data = state_to_json( + robot_data=robot_data, + link_infos=link_infos, + state=final_state, + observed_markers=observed_map, + marker_reports=marker_reports, + stats=stats, + input_file=args.optimized_markers, + robot_file=args.robot, + stage_results=stage_results, + params=params, + ) + + out_path = args.out + if out_path is None: + out_dir = os.path.dirname(args.optimized_markers) or '.' + out_path = os.path.join(out_dir, 'robot_state.json') + + save_json(out_path, out_data) + + print(f"[INFO] Saved to {out_path}") + if stats['mean_error_m'] is not None: + print(f"[INFO] Mean marker error: {stats['mean_error_m'] * 1000.0:.2f} mm") + print(f"[INFO] RMS marker error : {stats['rms_error_m'] * 1000.0:.2f} mm") + print(f"[INFO] Worst marker error: {stats['worst_error_m'] * 1000.0:.2f} mm") + else: + print('[INFO] No marker error statistics available.') + + +if __name__ == '__main__': + main() diff --git a/pipeline/4_robotState_estimation_v6.py b/pipeline/4_robotState_estimation_v6.py new file mode 100644 index 0000000..709eb95 --- /dev/null +++ b/pipeline/4_robotState_estimation_v6.py @@ -0,0 +1,1217 @@ +#!/usr/bin/env python3 +""" +4_robotState_estimation_v6.py + +Deterministic geometric robot-state estimation from optimized 3D ArUco marker positions. + +Mathematical idea +----------------- +This script does NOT run a generic minimizer by default. It estimates the robot +state q with closed-form geometric rules and sequential prefix expansion. + +Input: + aruco_positions_optimized*.json + robot.json + +Core geometric steps +-------------------- +1) Root pose estimation + The root link pose (Board) is estimated from its observed markers using a + weighted Kabsch fit: + R_root, t_root = argmin || R P_i + t - Q_i ||^2 + with weights driven by marker size. + +2) Linear joints / sliders + For a prismatic joint along axis a, the joint variable q is estimated by + weighted averaging of the projection residuals: + q = mean_i w_i * dot(a_world, p_obs_i - p_pred_i(q=0)) / mean_i w_i + This is a closed-form geometry update, not a numeric optimizer. + +3) Revolute joints + For a revolute joint around axis a, the joint angle is estimated by + projecting the marker vectors into the plane orthogonal to a and solving a + 2D weighted rotation alignment: + theta = atan2( sum_i w_i * cross2(u_i, v_i), + sum_i w_i * dot2(u_i, v_i) ) + where u_i are predicted vectors (q=0) and v_i are observed vectors. + + To handle 180° flip ambiguities, the script optionally compares theta and + theta + pi and uses a weak normal-based tie-break. Marker normals from + robot.json are used as a geometric hint, not as hard image evidence. + +4) Sequential prefix expansion + The chain is estimated link-by-link from the root outwards. Each stage only + activates the first N links (controlled by robot.json::state_pose_params), + re-estimates the active prefix, and keeps the previous prefix as the start + of the next stage. + +This is intentionally designed as a fast, deterministic geometric initializer +for later refinement, not as a full bundle-adjustment solver. + +Notes +----- +- The script uses the robot hierarchy, joint axes, joint origins, and marker + local positions from robot.json. +- No least-squares minimizer is used in the default path. +- Solver-related flags may be present in robot.json for compatibility but are + not used here. + +Dependencies +------------ +numpy only +""" + +from __future__ import annotations + +import argparse +import json +import math +import os +import sys +import time +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +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) -> Any: + with open(resolve_path(path), "r", encoding="utf-8") as f: + return json.load(f) + + +def save_json(path: str, data: Any) -> None: + with open(resolve_path(path), "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + +# ============================================================================= +# Small geometry helpers +# ============================================================================= + +def safe_norm(v: np.ndarray, eps: float = 1e-12) -> float: + return float(np.linalg.norm(v) + eps) + + +def normalize(v: np.ndarray, eps: float = 1e-12) -> np.ndarray: + v = np.asarray(v, dtype=np.float64) + return v / safe_norm(v, eps) + + +def wrap_angle_rad(a: float) -> float: + return (a + math.pi) % (2.0 * math.pi) - math.pi + + +def get_length_scale(robot_data: Dict[str, Any]) -> float: + units = robot_data.get("units", {}) or {} + length_unit = str(units.get("length", "")).strip().lower() + if length_unit in ("mm", "millimeter", "millimeters"): + return 1.0 / 1000.0 + if length_unit in ("cm", "centimeter", "centimeters"): + return 1.0 / 100.0 + return 1.0 + + +def rotation_matrix_xyz(rx: float, ry: float, rz: float, degrees: bool = False) -> np.ndarray: + """Rotation order: X then Y then Z.""" + if degrees: + rx, ry, rz = math.radians(rx), math.radians(ry), math.radians(rz) + + cx, sx = math.cos(rx), math.sin(rx) + cy, sy = math.cos(ry), math.sin(ry) + cz, sz = math.cos(rz), math.sin(rz) + + rx_m = np.array([[1.0, 0.0, 0.0], + [0.0, cx, -sx], + [0.0, sx, cx]], dtype=np.float64) + ry_m = np.array([[cy, 0.0, sy], + [0.0, 1.0, 0.0], + [-sy, 0.0, cy]], dtype=np.float64) + rz_m = np.array([[cz, -sz, 0.0], + [sz, cz, 0.0], + [0.0, 0.0, 1.0]], dtype=np.float64) + return rz_m @ ry_m @ rx_m + + +def axis_angle_rotation(axis: np.ndarray, angle_rad: float) -> np.ndarray: + axis = normalize(axis) + x, y, z = axis + c = math.cos(angle_rad) + s = math.sin(angle_rad) + C = 1.0 - c + return np.array([ + [c + x * x * C, x * y * C - z * s, x * z * C + y * s], + [y * x * C + z * s, c + y * y * C, y * z * C - x * s], + [z * x * C - y * s, z * y * C + x * s, c + z * z * C], + ], dtype=np.float64) + + +def make_transform(R: Optional[np.ndarray] = None, t: Optional[np.ndarray] = None) -> np.ndarray: + T = np.eye(4, dtype=np.float64) + if R is not None: + T[:3, :3] = np.asarray(R, dtype=np.float64) + if t is not None: + T[:3, 3] = np.asarray(t, dtype=np.float64).reshape(3) + return T + + +def transform_point(T: np.ndarray, p: np.ndarray) -> np.ndarray: + p4 = np.ones(4, dtype=np.float64) + p4[:3] = np.asarray(p, dtype=np.float64).reshape(3) + return (T @ p4)[:3] + + +def transform_vector(T: np.ndarray, v: np.ndarray) -> np.ndarray: + return T[:3, :3] @ np.asarray(v, dtype=np.float64).reshape(3) + + +def kabsch(P: np.ndarray, Q: np.ndarray, W: Optional[np.ndarray] = None) -> Tuple[np.ndarray, np.ndarray]: + """Weighted Kabsch fit: find R, t so that R @ P + t ≈ Q.""" + P = np.asarray(P, dtype=np.float64) + Q = np.asarray(Q, dtype=np.float64) + assert P.shape == Q.shape and P.shape[1] == 3 + + if W is None: + W = np.ones(len(P), dtype=np.float64) + W = np.asarray(W, dtype=np.float64).reshape(-1) + W = np.maximum(W, 1e-12) + W = W / np.sum(W) + + p_cent = np.sum(P * W[:, None], axis=0) + q_cent = np.sum(Q * W[:, None], axis=0) + + P0 = P - p_cent + Q0 = Q - q_cent + H = (P0 * W[:, None]).T @ Q0 + + U, S, Vt = np.linalg.svd(H) + R = Vt.T @ U.T + if np.linalg.det(R) < 0: + Vt[-1, :] *= -1.0 + R = Vt.T @ U.T + t = q_cent - R @ p_cent + return R, t + + +def principal_axis_id(axis: np.ndarray, threshold: float = 0.95) -> Optional[int]: + a = normalize(np.asarray(axis, dtype=np.float64)) + idx = int(np.argmax(np.abs(a))) + if abs(a[idx]) >= threshold: + return idx + return None + + +def axis_unit_from_id(axis_id: int, sign: float = 1.0) -> np.ndarray: + v = np.zeros(3, dtype=np.float64) + v[axis_id] = 1.0 if sign >= 0 else -1.0 + return v + + +def plane_basis_for_axis(axis: np.ndarray) -> Tuple[np.ndarray, np.ndarray, int]: + """ + Return a 2D basis (e1, e2) spanning the plane orthogonal to axis. + Also return the principal axis index if axis is close to x/y/z. + """ + a = normalize(axis) + axis_id = int(np.argmax(np.abs(a))) + + if axis_id == 0: # x -> yz-plane + e1 = np.array([0.0, 1.0, 0.0], dtype=np.float64) + e2 = np.array([0.0, 0.0, 1.0], dtype=np.float64) + elif axis_id == 1: # y -> zx-plane + e1 = np.array([0.0, 0.0, 1.0], dtype=np.float64) + e2 = np.array([1.0, 0.0, 0.0], dtype=np.float64) + else: # z -> xy-plane + e1 = np.array([1.0, 0.0, 0.0], dtype=np.float64) + e2 = np.array([0.0, 1.0, 0.0], dtype=np.float64) + + return e1, e2, axis_id + + +def project_to_plane(v: np.ndarray, axis: np.ndarray) -> np.ndarray: + axis = normalize(axis) + return np.asarray(v, dtype=np.float64) - np.dot(v, axis) * axis + + +def weighted_mean(values: List[float], weights: List[float]) -> float: + if not values or not weights: + return 0.0 + w = np.asarray(weights, dtype=np.float64) + v = np.asarray(values, dtype=np.float64) + s = float(np.sum(w)) + if s <= 1e-12: + return float(np.mean(v)) + return float(np.sum(v * w) / s) + + +def marker_quality_weight(marker: "MarkerInfo", ref_size: float = 25.0) -> float: + w = 1.0 + if marker.size is not None: + try: + size = float(marker.size) + if size > 0: + w *= max(0.35, math.sqrt(size / ref_size)) + except Exception: + pass + return float(w) + + +def normal_flip_bias(marker: "MarkerInfo", world_up: np.ndarray = np.array([0.0, 0.0, 1.0], dtype=np.float64)) -> float: + """ + Weak bias for top/bottom flip discrimination. + Only markers with a strong ±z local normal contribute. + """ + if marker.local_normal is None: + return 0.0 + n = normalize(marker.local_normal) + if abs(n[2]) < 0.5: + return 0.0 + pref = 1.0 if n[2] >= 0 else -1.0 + # Positive score means "prefer world_up alignment" for top markers, + # negative score means "prefer world_down alignment" for bottom markers. + return pref + + +# ============================================================================= +# Robot structures +# ============================================================================= + +@dataclass +class MarkerInfo: + marker_id: int + name: str + local_pos_m: np.ndarray + local_normal: np.ndarray + size: Optional[float] + spin: Optional[float] + link_name: str + + +@dataclass +class LinkInfo: + name: str + parent: Optional[str] + joint_type: str + joint_axis: Optional[np.ndarray] + joint_variable: Optional[str] + joint_origin_m: np.ndarray + joint_rotation_deg: np.ndarray + mount_position_m: np.ndarray + mount_rotation_deg: np.ndarray + markers: List[MarkerInfo] + + +@dataclass +class StatePoseParams: + numbers_of_elements_to_consider_start: int = 4 + numbers_of_elements_to_consider_final: int = 5 + geometric_passes_per_stage: int = 2 + root_pose_min_markers: int = 2 + use_marker_normals_flip_tiebreak: bool = True + normal_flip_weight: float = 0.05 + + +def load_state_pose_params(robot_data: Dict[str, Any]) -> StatePoseParams: + raw = robot_data.get("state_pose_params", {}) or {} + return StatePoseParams( + numbers_of_elements_to_consider_start=max(1, int(raw.get("numbers_of_Elements_to_consider_start", 4))), + numbers_of_elements_to_consider_final=max(1, int(raw.get("numbers_of_Elements_to_consider_final", 5))), + geometric_passes_per_stage=max(1, int(raw.get("geometric_passes_per_stage", 2))), + root_pose_min_markers=max(1, int(raw.get("root_pose_min_markers", 2))), + use_marker_normals_flip_tiebreak=bool(raw.get("use_marker_normals_flip_tiebreak", True)), + normal_flip_weight=float(raw.get("normal_flip_weight", 0.05)), + ) + + +def parse_robot(robot_data: Dict[str, Any], length_scale: float) -> Tuple[Dict[str, LinkInfo], Dict[int, str], List[str]]: + links = robot_data.get("links", {}) or {} + link_infos: Dict[str, LinkInfo] = {} + marker_by_id: Dict[int, str] = {} + issues: List[str] = [] + + for link_name, link_data in links.items(): + parent = link_data.get("parent", None) + + joint = link_data.get("jointToParent", {}) or {} + joint_type = str(joint.get("type", "")).strip().lower() + axis = joint.get("axis", None) + joint_axis = None + if axis is not None: + axis_arr = np.asarray(axis, dtype=np.float64) + if safe_norm(axis_arr) > 1e-12: + joint_axis = normalize(axis_arr) + + joint_variable = joint.get("variable", None) + joint_origin_m = np.asarray(joint.get("origin", [0, 0, 0]), dtype=np.float64) * float(length_scale) + joint_rotation_deg = np.asarray(joint.get("rotation", [0, 0, 0]), dtype=np.float64) + + mount_position_m = np.asarray(link_data.get("mountPosition", [0, 0, 0]), dtype=np.float64) * float(length_scale) + mount_rotation_deg = np.asarray(link_data.get("mountRotation", [0, 0, 0]), dtype=np.float64) + + markers: List[MarkerInfo] = [] + seen_local: set[int] = set() + for idx, marker in enumerate(link_data.get("markers", []) or []): + marker_id = int(marker.get("id", -1)) + pos = marker.get("position", None) + if marker_id < 0 or pos is None or len(pos) != 3: + issues.append(f"[WARN] link={link_name}: skipped invalid marker entry at index {idx}") + continue + if marker_id in seen_local: + issues.append(f"[WARN] link={link_name}: duplicate marker id {marker_id} inside same link -> skipped") + continue + if marker_id in marker_by_id: + issues.append( + f"[WARN] marker id {marker_id} already assigned to link '{marker_by_id[marker_id]}', " + f"duplicate in link '{link_name}' -> skipped" + ) + continue + + seen_local.add(marker_id) + marker_by_id[marker_id] = link_name + markers.append( + MarkerInfo( + marker_id=marker_id, + name=str(marker.get("name", f"aruco_{marker_id}")), + local_pos_m=np.asarray(pos, dtype=np.float64) * float(length_scale), + local_normal=normalize(np.asarray(marker.get("normal", [0, 0, 1]), dtype=np.float64)), + size=marker.get("size", None), + spin=marker.get("spin", None), + link_name=link_name, + ) + ) + + link_infos[link_name] = LinkInfo( + name=link_name, + parent=parent, + joint_type=joint_type, + joint_axis=joint_axis, + joint_variable=joint_variable, + joint_origin_m=joint_origin_m, + joint_rotation_deg=joint_rotation_deg, + mount_position_m=mount_position_m, + mount_rotation_deg=mount_rotation_deg, + markers=markers, + ) + + return link_infos, marker_by_id, issues + + +def topological_links(link_infos: Dict[str, LinkInfo]) -> List[str]: + roots = [ln for ln, li in link_infos.items() if li.parent is None] + if not roots: + return [] + root = roots[0] + + children: Dict[str, List[str]] = {ln: [] for ln in link_infos} + for ln, li in link_infos.items(): + if li.parent is not None and li.parent in children: + children[li.parent].append(ln) + + order: List[str] = [] + queue = [root] + while queue: + cur = queue.pop(0) + if cur in order: + continue + order.append(cur) + for ch in children.get(cur, []): + queue.append(ch) + + # Append disconnected subtrees if any. + for ln in link_infos: + if ln not in order: + order.append(ln) + + return order + + +def compute_children_map(link_infos: Dict[str, LinkInfo]) -> Dict[str, List[str]]: + children: Dict[str, List[str]] = {ln: [] for ln in link_infos} + for ln, li in link_infos.items(): + if li.parent is not None and li.parent in children: + children[li.parent].append(ln) + return children + + +def subtree_links(link_name: str, children_map: Dict[str, List[str]], active_links: set[str]) -> List[str]: + out: List[str] = [] + queue = [link_name] + while queue: + cur = queue.pop(0) + if cur not in active_links: + continue + out.append(cur) + for ch in children_map.get(cur, []): + if ch in active_links: + queue.append(ch) + return out + + +def marker_ids_in_links(link_infos: Dict[str, LinkInfo], links: List[str]) -> List[int]: + ids: List[int] = [] + for link_name in links: + for m in link_infos[link_name].markers: + ids.append(m.marker_id) + return ids + + +# ============================================================================= +# Observations +# ============================================================================= + +def load_observed_markers(optimized_markers_file: str) -> Dict[int, Dict[str, Any]]: + data = load_json(optimized_markers_file) + if isinstance(data, dict): + markers = data.get("markers", []) or [] + elif isinstance(data, list): + markers = data + else: + markers = [] + + observed: Dict[int, Dict[str, Any]] = {} + for m in markers: + if not isinstance(m, dict) or "marker_id" not in m: + continue + marker_id = int(m["marker_id"]) + pos = m.get("position_m", None) + if pos is None or len(pos) != 3: + continue + obs = dict(m) + obs["marker_id"] = marker_id + obs["position_m"] = np.asarray(pos, dtype=np.float64) + observed[marker_id] = obs + return observed + + +def active_observations_for_links( + observed_markers: Dict[int, Dict[str, Any]], + link_infos: Dict[str, LinkInfo], + active_links: set[str], +) -> Dict[int, Dict[str, Any]]: + out: Dict[int, Dict[str, Any]] = {} + for marker_id, obs in observed_markers.items(): + link = obs.get("link", None) + if link is None or link == "unknown": + continue + if link not in active_links: + continue + if link not in link_infos: + continue + out[marker_id] = obs + return out + + +# ============================================================================= +# Forward kinematics +# ============================================================================= + +def link_static_transform(link: LinkInfo) -> np.ndarray: + Rm = rotation_matrix_xyz(*link.mount_rotation_deg, degrees=True) + return make_transform(Rm, link.mount_position_m) + + +def joint_motion_transform(link: LinkInfo, q_value: float) -> np.ndarray: + joint_type = (link.joint_type or "").strip().lower() + if link.joint_axis is None: + return np.eye(4, dtype=np.float64) + + axis = normalize(link.joint_axis) + if joint_type == "linear": + return make_transform(np.eye(3), axis * float(q_value)) + if joint_type == "revolute": + return make_transform(axis_angle_rotation(axis, float(q_value)), np.zeros(3)) + return np.eye(4, dtype=np.float64) + + +def world_to_link_transform( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray], + joint_override: Optional[Dict[str, float]] = None, +) -> np.ndarray: + override_items = tuple(sorted((joint_override or {}).items())) + cache_key = (link_name, override_items) + if cache_key in cache: + return cache[cache_key] + + link = link_infos[link_name] + if link.parent is None: + T = make_transform(state["root_R"], state["root_t"]) + cache[cache_key] = T + return T + + parent_T = world_to_link_transform(link.parent, link_infos, state, cache, joint_override=joint_override) + + # Parent -> joint origin + T = parent_T @ make_transform(np.eye(3), link.joint_origin_m) + + # Joint motion + joint_values = state["joint_values"] + q = joint_override.get(link.name, joint_values.get(link.joint_variable, 0.0)) if joint_override else joint_values.get(link.joint_variable, 0.0) + T = T @ joint_motion_transform(link, q) + + # Static rotation after the joint + R_static = rotation_matrix_xyz(*link.joint_rotation_deg, degrees=True) + T = T @ make_transform(R_static, np.zeros(3)) + + # Mount transform / link-local offset + T = T @ link_static_transform(link) + + cache[cache_key] = T + return T + + +def predict_marker_positions( + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + joint_override: Optional[Dict[str, float]] = None, + active_links: Optional[set[str]] = None, +) -> Dict[int, np.ndarray]: + pred: Dict[int, np.ndarray] = {} + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray] = {} + for link_name, link in link_infos.items(): + if active_links is not None and link_name not in active_links: + continue + T = world_to_link_transform(link_name, link_infos, state, cache, joint_override=joint_override) + for marker in link.markers: + pred[marker.marker_id] = transform_point(T, marker.local_pos_m) + return pred + + +def world_joint_axis_for_link( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], +) -> np.ndarray: + link = link_infos[link_name] + if link.parent is None or link.joint_axis is None: + return np.array([1.0, 0.0, 0.0], dtype=np.float64) + + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray] = {} + parent_T = world_to_link_transform(link.parent, link_infos, state, cache) + axis_world = transform_vector(parent_T, link.joint_axis) + return normalize(axis_world) + + +def world_joint_origin_for_link( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], +) -> np.ndarray: + link = link_infos[link_name] + if link.parent is None: + return np.asarray(state["root_t"], dtype=np.float64).copy() + + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray] = {} + parent_T = world_to_link_transform(link.parent, link_infos, state, cache) + return transform_point(parent_T, link.joint_origin_m) + + +# ============================================================================= +# Geometric estimation +# ============================================================================= + +def initial_root_pose( + root_link: LinkInfo, + observed_markers: Dict[int, Dict[str, Any]], + min_markers: int = 2, +) -> Tuple[np.ndarray, np.ndarray, Dict[str, Any]]: + P = [] + Q = [] + W = [] + matched = [] + + for marker in root_link.markers: + if marker.marker_id in observed_markers: + obs = np.asarray(observed_markers[marker.marker_id]["position_m"], dtype=np.float64) + P.append(marker.local_pos_m) + Q.append(obs) + W.append(marker_quality_weight(marker)) + matched.append(marker.marker_id) + + if len(P) >= min_markers: + R, t = kabsch(np.asarray(P), np.asarray(Q), np.asarray(W)) + return R, t, {"reason": "kabsch", "used_markers": matched} + + if len(P) >= 1: + marker = root_link.markers[0] + obs = np.asarray(observed_markers[marker.marker_id]["position_m"], dtype=np.float64) + return np.eye(3, dtype=np.float64), obs - marker.local_pos_m, {"reason": "single_marker_fallback", "used_markers": matched} + + return np.eye(3, dtype=np.float64), np.zeros(3, dtype=np.float64), {"reason": "no_root_markers"} + + +def estimate_linear_joint_value( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], + children_map: Dict[str, List[str]], +) -> Tuple[float, Dict[str, Any]]: + link = link_infos[link_name] + if link.joint_variable is None: + return 0.0, {"reason": "no_joint_variable"} + + subtree = subtree_links(link_name, children_map, active_links) + obs_ids = [mid for mid in marker_ids_in_links(link_infos, subtree) if mid in observed_markers] + if not obs_ids: + return float(state["joint_values"].get(link.joint_variable, 0.0)), {"reason": "no_observations"} + + axis_world = world_joint_axis_for_link(link_name, link_infos, state) + + pred0 = predict_marker_positions(link_infos, state, joint_override={link_name: 0.0}, active_links=active_links) + + num = 0.0 + den = 0.0 + per_marker = [] + for mid in obs_ids: + if mid not in pred0: + continue + marker = next((m for m in link_infos[observed_markers[mid]["link"]].markers if m.marker_id == mid), None) + w = marker_quality_weight(marker) if marker is not None else 1.0 + p_obs = np.asarray(observed_markers[mid]["position_m"], dtype=np.float64) + p0 = pred0[mid] + q_i = float(np.dot(axis_world, p_obs - p0)) + num += w * q_i + den += w + per_marker.append({"marker_id": mid, "q_i": q_i, "weight": w}) + + if den <= 1e-12: + return float(state["joint_values"].get(link.joint_variable, 0.0)), {"reason": "zero_weight"} + + q = num / den + return float(q), { + "reason": "weighted_projection", + "used_markers": len(per_marker), + "axis_world": [float(x) for x in axis_world], + "per_marker": per_marker, + } + + +def revolute_score_with_normals( + link_name: str, + q_value: float, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], +) -> float: + """ + Weak tiebreak for q vs q + pi using marker normals. + Lower is better. + """ + link = link_infos[link_name] + if not link.markers: + return 0.0 + + pred = predict_marker_positions(link_infos, state, joint_override={link_name: q_value}, active_links=active_links) + T_link = world_to_link_transform(link_name, link_infos, state, cache={}, joint_override={link_name: q_value}) + R_link = T_link[:3, :3] + up = np.array([0.0, 0.0, 1.0], dtype=np.float64) + + score = 0.0 + for marker in link.markers: + if marker.marker_id not in observed_markers: + continue + if marker.local_normal is None: + continue + n_local = normalize(marker.local_normal) + if abs(n_local[2]) < 0.5: + continue + n_world = normalize(R_link @ n_local) + sign_pref = 1.0 if n_local[2] >= 0 else -1.0 + score += marker_quality_weight(marker) * (1.0 - sign_pref * float(np.dot(n_world, up))) + + return float(score) + + +def estimate_revolute_joint_value( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], + children_map: Dict[str, List[str]], + use_normal_tiebreak: bool = True, + normal_weight: float = 0.05, +) -> Tuple[float, Dict[str, Any]]: + link = link_infos[link_name] + if link.joint_variable is None: + return 0.0, {"reason": "no_joint_variable"} + + subtree = subtree_links(link_name, children_map, active_links) + obs_ids = [mid for mid in marker_ids_in_links(link_infos, subtree) if mid in observed_markers] + if not obs_ids: + return float(state["joint_values"].get(link.joint_variable, 0.0)), {"reason": "no_observations"} + + q0 = float(state["joint_values"].get(link.joint_variable, 0.0)) + + # Prediction with joint set to zero (only upstream geometry active). + pred0 = predict_marker_positions(link_infos, state, joint_override={link_name: 0.0}, active_links=active_links) + origin_world = world_joint_origin_for_link(link_name, link_infos, state) + axis_world = world_joint_axis_for_link(link_name, link_infos, state) + e1, e2, axis_id = plane_basis_for_axis(axis_world) + + S = 0.0 + C = 0.0 + used = 0 + per_marker = [] + + for mid in obs_ids: + if mid not in pred0: + continue + marker = next((m for m in link_infos[observed_markers[mid]["link"]].markers if m.marker_id == mid), None) + w = marker_quality_weight(marker) if marker is not None else 1.0 + + p_obs = np.asarray(observed_markers[mid]["position_m"], dtype=np.float64) + p0 = pred0[mid] + + u = project_to_plane(p0 - origin_world, axis_world) + v = project_to_plane(p_obs - origin_world, axis_world) + + u2 = np.array([np.dot(u, e1), np.dot(u, e2)], dtype=np.float64) + v2 = np.array([np.dot(v, e1), np.dot(v, e2)], dtype=np.float64) + + nu = float(np.linalg.norm(u2)) + nv = float(np.linalg.norm(v2)) + if nu <= 1e-9 or nv <= 1e-9: + continue + + S += w * float(u2[0] * v2[1] - u2[1] * v2[0]) + C += w * float(u2[0] * v2[0] + u2[1] * v2[1]) + used += 1 + per_marker.append({"marker_id": mid, "weight": w}) + + if used == 0: + return q0, {"reason": "no_valid_vectors"} + + theta = math.atan2(S, C) + theta_alt = wrap_angle_rad(theta + math.pi) + + # Evaluate both candidates deterministically, choose the better one. + def score_candidate(qcand: float) -> float: + pred = predict_marker_positions(link_infos, state, joint_override={link_name: qcand}, active_links=active_links) + err = 0.0 + for mid in obs_ids: + if mid not in pred: + continue + marker = next((m for m in link_infos[observed_markers[mid]["link"]].markers if m.marker_id == mid), None) + w = marker_quality_weight(marker) if marker is not None else 1.0 + p_obs = np.asarray(observed_markers[mid]["position_m"], dtype=np.float64) + d = pred[mid] - p_obs + err += w * float(np.dot(d, d)) + if use_normal_tiebreak: + err += normal_weight * revolute_score_with_normals(link_name, qcand, link_infos, state, observed_markers, active_links) + return float(err) + + score_theta = score_candidate(theta) + score_alt = score_candidate(theta_alt) + + best_q = theta if score_theta <= score_alt else theta_alt + best_score = min(score_theta, score_alt) + + return wrap_angle_rad(best_q), { + "reason": "2d_alignment" + ("+normal_tiebreak" if use_normal_tiebreak else ""), + "used_markers": used, + "axis_world": [float(x) for x in axis_world], + "axis_id": axis_id, + "theta_rad": float(theta), + "theta_alt_rad": float(theta_alt), + "score_theta": float(score_theta), + "score_theta_alt": float(score_alt), + "best_score": float(best_score), + "per_marker": per_marker, + } + + +def estimate_joint_geometrically( + link_name: str, + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: set[str], + children_map: Dict[str, List[str]], + params: StatePoseParams, +) -> Tuple[float, Dict[str, Any]]: + link = link_infos[link_name] + jt = (link.joint_type or "").strip().lower() + + if jt == "linear": + return estimate_linear_joint_value(link_name, link_infos, state, observed_markers, active_links, children_map) + if jt == "revolute": + return estimate_revolute_joint_value( + link_name, + link_infos, + state, + observed_markers, + active_links, + children_map, + use_normal_tiebreak=params.use_marker_normals_flip_tiebreak, + normal_weight=params.normal_flip_weight, + ) + + return float(state["joint_values"].get(link.joint_variable, 0.0)) if link.joint_variable else 0.0, {"reason": "unsupported_joint_type"} + + +# ============================================================================= +# Scoring / reporting +# ============================================================================= + +def build_state_template(link_infos: Dict[str, LinkInfo]) -> Dict[str, Any]: + return { + "root_R": np.eye(3, dtype=np.float64), + "root_t": np.zeros(3, dtype=np.float64), + "joint_values": {li.joint_variable: 0.0 for li in link_infos.values() if li.joint_variable}, + } + + +def copy_state(state: Dict[str, Any]) -> Dict[str, Any]: + return { + "root_R": np.asarray(state["root_R"], dtype=np.float64).copy(), + "root_t": np.asarray(state["root_t"], dtype=np.float64).copy(), + "joint_values": dict(state["joint_values"]), + } + + +def marker_error_report( + link_infos: Dict[str, LinkInfo], + state: Dict[str, Any], + observed_markers: Dict[int, Dict[str, Any]], + active_links: Optional[set[str]] = None, +) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + pred = predict_marker_positions(link_infos, state, active_links=active_links) + marker_reports: List[Dict[str, Any]] = [] + errors = [] + + for marker_id, obs in observed_markers.items(): + if marker_id not in pred: + continue + p_obs = np.asarray(obs["position_m"], dtype=np.float64) + p_pred = pred[marker_id] + err = p_pred - p_obs + err_norm = float(np.linalg.norm(err)) + errors.append(err_norm) + marker_reports.append( + { + "marker_id": int(marker_id), + "link": obs.get("link", "unknown"), + "error_m": [float(x) for x in err], + "error_norm_m": err_norm, + "predicted_m": [float(x) for x in p_pred], + "observed_m": [float(x) for x in p_obs], + } + ) + + if errors: + arr = np.asarray(errors, dtype=np.float64) + stats = { + "num_markers_used": int(len(errors)), + "mean_error_m": float(np.mean(arr)), + "rms_error_m": float(math.sqrt(np.mean(arr ** 2))), + "median_error_m": float(np.median(arr)), + "worst_error_m": float(np.max(arr)), + } + else: + stats = { + "num_markers_used": 0, + "mean_error_m": None, + "rms_error_m": None, + "median_error_m": None, + "worst_error_m": None, + } + + return marker_reports, stats + + +def summarize_state(state: Dict[str, Any], link_infos: Dict[str, LinkInfo]) -> Dict[str, Any]: + movements: Dict[str, Any] = {} + for link_name, link in link_infos.items(): + q = state["joint_values"].get(link.joint_variable, None) if link.joint_variable else None + if q is None: + continue + jt = (link.joint_type or "").strip().lower() + if jt == "linear": + movements[link.joint_variable] = { + "value_m": float(q), + "value_mm": float(q * 1000.0), + "joint_type": jt, + "link": link_name, + } + elif jt == "revolute": + movements[link.joint_variable] = { + "value_rad": float(q), + "value_deg": float(math.degrees(q)), + "joint_type": jt, + "link": link_name, + } + else: + movements[link.joint_variable] = { + "value": float(q), + "joint_type": jt, + "link": link_name, + } + + root_R = np.asarray(state["root_R"], dtype=np.float64) + root_t = np.asarray(state["root_t"], dtype=np.float64) + return { + "root_pose": { + "translation_m": [float(x) for x in root_t], + "rotation_matrix": [[float(x) for x in row] for row in root_R], + "euler_xyz_deg": [float(x) for x in np.degrees(np.array([ + math.atan2(root_R[2, 1], root_R[2, 2]), + math.atan2(-root_R[2, 0], math.sqrt(root_R[0, 0] ** 2 + root_R[1, 0] ** 2)), + math.atan2(root_R[1, 0], root_R[0, 0]), + ]))], + }, + "movements": movements, + } + + +# ============================================================================= +# Sequential geometric estimation +# ============================================================================= + +def optimize_geometric_prefix( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + initial_state: Dict[str, Any], + active_links: List[str], + params: StatePoseParams, +) -> Tuple[Dict[str, Any], Dict[str, Any]]: + active_set = set(active_links) + children_map = compute_children_map(link_infos) + stage_obs = active_observations_for_links(observed_markers, link_infos, active_set) + + state = copy_state(initial_state) + stage_info: Dict[str, Any] = { + "method": "deterministic_geometric_prefix", + "active_links": active_links, + "active_observations": len(stage_obs), + "joint_updates": [], + } + + root_link_name = next((ln for ln in active_links if link_infos[ln].parent is None), None) + if root_link_name is not None: + root_link = link_infos[root_link_name] + root_R, root_t, root_info = initial_root_pose(root_link, stage_obs, min_markers=params.root_pose_min_markers) + state["root_R"] = root_R + state["root_t"] = root_t + stage_info["root_link"] = root_link_name + stage_info["root_pose"] = root_info + + active_joint_links = [ln for ln in active_links if link_infos[ln].parent is not None] + + for pass_idx in range(params.geometric_passes_per_stage): + pass_updates = [] + for link_name in active_joint_links: + link = link_infos[link_name] + if not link.joint_variable: + continue + q_old = float(state["joint_values"].get(link.joint_variable, 0.0)) + q_new, info = estimate_joint_geometrically( + link_name, + link_infos, + state, + stage_obs, + active_set, + children_map, + params, + ) + state["joint_values"][link.joint_variable] = q_new + pass_updates.append( + { + "link": link_name, + "joint_variable": link.joint_variable, + "joint_type": link.joint_type, + "old": q_old, + "new": q_new, + "info": info, + } + ) + stage_info["joint_updates"].append({"pass": pass_idx, "updates": pass_updates}) + + marker_reports, stats = marker_error_report(link_infos, state, stage_obs, active_links=active_set) + stage_info["marker_stats"] = stats + stage_info["marker_reports"] = marker_reports[:] + + return state, stage_info + + +def sequential_geometric_estimation( + link_infos: Dict[str, LinkInfo], + observed_markers: Dict[int, Dict[str, Any]], + params: StatePoseParams, +) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: + ordered = topological_links(link_infos) + if not ordered: + return build_state_template(link_infos), [] + + start_n = max(1, min(int(params.numbers_of_elements_to_consider_start), len(ordered))) + final_n = max(start_n, min(int(params.numbers_of_elements_to_consider_final), len(ordered))) + + state = build_state_template(link_infos) + stage_reports: List[Dict[str, Any]] = [] + + for n in range(start_n, final_n + 1): + active_links = ordered[:n] + state, stage_info = optimize_geometric_prefix(link_infos, observed_markers, state, active_links, params) + stage_info["stage_idx"] = len(stage_reports) + stage_info["num_active_links"] = len(active_links) + stage_info["active_links"] = active_links + + stage_obs = active_observations_for_links(observed_markers, link_infos, set(active_links)) + marker_reports, stats = marker_error_report(link_infos, state, stage_obs, active_links=set(active_links)) + stage_info["marker_stats"] = stats + stage_info["marker_reports"] = marker_reports + stage_reports.append(stage_info) + + mean_mm = None if stats["mean_error_m"] is None else stats["mean_error_m"] * 1000.0 + print( + f"[INFO] Stage {stage_info['stage_idx']} | links={len(active_links)} | " + f"markers_used={stats['num_markers_used']} | " + f"mean_error={'n/a' if mean_mm is None else f'{mean_mm:.2f} mm'}" + ) + + return state, stage_reports + + +# ============================================================================= +# Serialization +# ============================================================================= + +def link_world_poses(link_infos: Dict[str, LinkInfo], state: Dict[str, Any]) -> Dict[str, Any]: + poses: Dict[str, Any] = {} + cache: Dict[Tuple[str, Tuple[Tuple[str, float], ...]], np.ndarray] = {} + for link_name in link_infos: + T = world_to_link_transform(link_name, link_infos, state, cache) + R = T[:3, :3] + t = T[:3, 3] + poses[link_name] = { + "translation_m": [float(x) for x in t], + "rotation_matrix": [[float(x) for x in row] for row in R], + } + return poses + + +def state_to_json( + state: Dict[str, Any], + link_infos: Dict[str, LinkInfo], + stage_reports: List[Dict[str, Any]], + observed_markers: Dict[int, Dict[str, Any]], +) -> Dict[str, Any]: + movements = summarize_state(state, link_infos)["movements"] + root_pose = summarize_state(state, link_infos)["root_pose"] + predicted = predict_marker_positions(link_infos, state) + + markers_out = [] + for marker_id, obs in sorted(observed_markers.items()): + entry = { + "marker_id": int(marker_id), + "link": obs.get("link", "unknown"), + "observed_position_m": [float(x) for x in np.asarray(obs["position_m"], dtype=np.float64)], + } + if marker_id in predicted: + entry["predicted_position_m"] = [float(x) for x in predicted[marker_id]] + err = predicted[marker_id] - np.asarray(obs["position_m"], dtype=np.float64) + entry["error_m"] = [float(x) for x in err] + entry["error_norm_m"] = float(np.linalg.norm(err)) + markers_out.append(entry) + + return { + "schema_version": "1.0", + "method": "deterministic_geometric_sequential_prefix", + "created_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "root_pose": root_pose, + "movements": movements, + "link_poses": link_world_poses(link_infos, state), + "stage_reports": stage_reports, + "markers": markers_out, + } + + +# ============================================================================= +# Main +# ============================================================================= + +def main() -> None: + parser = argparse.ArgumentParser( + description="Sequential deterministic geometric robot-state estimation from optimized ArUco marker positions" + ) + parser.add_argument("optimized_markers", help="aruco_positions_optimized*.json") + parser.add_argument("-robot", "--robot", required=True, help="robot.json") + parser.add_argument("-out", "--out", default=None, help="Output robot_state.json path") + parser.add_argument("--prefixStart", type=int, default=None, help="Override state_pose_params start") + parser.add_argument("--prefixFinal", type=int, default=None, help="Override state_pose_params final") + parser.add_argument("--passes", type=int, default=None, help="Override geometric passes per stage") + + args = parser.parse_args() + + print("[STEP 1] Load robot.json and optimized marker positions...") + robot_data = load_json(args.robot) + length_scale = get_length_scale(robot_data) + params = load_state_pose_params(robot_data) + + if args.prefixStart is not None: + params.numbers_of_elements_to_consider_start = max(1, int(args.prefixStart)) + if args.prefixFinal is not None: + params.numbers_of_elements_to_consider_final = max(1, int(args.prefixFinal)) + if args.passes is not None: + params.geometric_passes_per_stage = max(1, int(args.passes)) + + link_infos, marker_by_id, issues = parse_robot(robot_data, length_scale) + observed_map = load_observed_markers(args.optimized_markers) + + for issue in issues: + print(issue) + + print(f"[INFO] Links: {len(link_infos)}") + print(f"[INFO] Known robot markers: {len(marker_by_id)}") + print(f"[INFO] Observed optimized markers: {len(observed_map)}") + print( + f"[INFO] state_pose_params: start={params.numbers_of_elements_to_consider_start}, " + f"final={params.numbers_of_elements_to_consider_final}, " + f"passes={params.geometric_passes_per_stage}, " + f"normal_flip_tiebreak={params.use_marker_normals_flip_tiebreak}" + ) + + print("[STEP 2] Sequential geometric reconstruction...") + state, stage_reports = sequential_geometric_estimation(link_infos, observed_map, params) + + out_data = state_to_json(state, link_infos, stage_reports, observed_map) + + out_dir = os.path.dirname(resolve_path(args.out)) if args.out else os.path.dirname(resolve_path(args.optimized_markers)) + if not out_dir: + out_dir = "." + os.makedirs(out_dir, exist_ok=True) + + out_file = args.out or os.path.join(out_dir, "robot_state.json") + save_json(out_file, out_data) + + print(f"[INFO] Saved robot state to {out_file}") + + # Compact summary of the resulting movements + print("\n[INFO] Estimated movements:") + for key, info in out_data["movements"].items(): + if "value_mm" in info: + print(f" {key}: {info['value_mm']:.3f} mm ({info['link']}, {info['joint_type']})") + elif "value_deg" in info: + print(f" {key}: {info['value_deg']:.3f} deg ({info['link']}, {info['joint_type']})") + else: + print(f" {key}: {info.get('value', 0.0):.6f} ({info.get('link', 'unknown')}, {info.get('joint_type', 'unknown')})") + + +if __name__ == "__main__": + main() diff --git a/pipeline/9_poseEvaluation.py b/pipeline/9_poseEvaluation.py new file mode 100644 index 0000000..d7ab7bf --- /dev/null +++ b/pipeline/9_poseEvaluation.py @@ -0,0 +1,73 @@ +import json +import math +import argparse + +def calculate_distance(point1, point2): + """Berechnet den euklidischen Abstand zwischen zwei 3D-Punkten.""" + return math.sqrt(sum([(a - b) ** 2 for a, b in zip(point1, point2)])) + +def analyze_marker_detection(detected_markers_file, original_markers_file): + """Analysiert die Markererkennung und gibt Erkennungsrate und Genauigkeit aus.""" + + with open(detected_markers_file, 'r') as f: + detected_data = json.load(f) + + with open(original_markers_file, 'r') as f: + original_data = json.load(f) + + detected_marker_ids = {marker['marker_id'] for marker in detected_data['markers']} + original_marker_ids = {marker['id'] for marker in original_data} + + # Erkannte und nicht erkannte Marker + recognized_markers = len(detected_marker_ids.intersection(original_marker_ids)) + missing_markers = len(original_marker_ids.difference(detected_marker_ids)) + + # Distanzen berechnen + distances = [] + for original_marker in original_data: + marker_id = original_marker['id'] + + # Gefundener Marker suchen + detected_marker = next((m for m in detected_data['markers'] if m['marker_id'] == marker_id), None) + + if detected_marker: + distance = calculate_distance(original_marker['position_m'], detected_marker['position_m']) + distances.append(distance) + + if not distances: + print("Keine gemeinsamen Marker gefunden, um die Genauigkeit zu bewerten.") + return + + # Statistiken berechnen + distances.sort() + mean_distance = sum(distances) / len(distances) + + # 90%-Radius + n = len(distances) + index_90 = int(0.9 * n) - 1 + radius_90 = distances[index_90] + + # 80%-Radius + index_80 = int(0.8 * n) - 1 + radius_80 = distances[index_80] + + # Schlechtester Abstand + worst_distance = distances[-1] + + print(f"Erkannte Marker: {recognized_markers}") + print(f"Nicht erkannte Marker: {missing_markers}") + print(f"Gesamtzahl der Original-Marker: {len(original_marker_ids)}") + print(f"Erkennungsrate: {recognized_markers / len(original_marker_ids) * 100:.2f}%") + print(f"Gemittelter 3D-Abstand: {mean_distance:.4f}m") + print(f"90%-Radius: {radius_90:.4f}m") + print(f"80%-Radius: {radius_80:.4f}m") + print(f"Schlechtester Abstand: {worst_distance:.4f}m") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Analysiert die Markererkennung.") + parser.add_argument("detected_file", help="Pfad zur JSON-Datei mit den erkannten Markern.") + parser.add_argument("original_file", help="Pfad zur JSON-Datei mit den originalen Markern.") + args = parser.parse_args() + + analyze_marker_detection(args.detected_file, args.original_file) \ No newline at end of file diff --git a/pipeline/aruco_positions_initial.json b/pipeline/aruco_positions_initial.json index aec87b2..2ad1968 100644 --- a/pipeline/aruco_positions_initial.json +++ b/pipeline/aruco_positions_initial.json @@ -1,11 +1,11 @@ { - "schema_version": "1.1", + "schema_version": "1.2", "stage": "initial_triangulation", - "created_utc": "2026-05-29T11:11:53Z", + "created_utc": "2026-05-29T17:30:29Z", "summary": { "num_cameras": 3, "num_markers": 8, - "num_constraints": 31 + "num_constraints": 25 }, "markers": [ { diff --git a/pipeline/aruco_positions_optimized.json b/pipeline/aruco_positions_optimized.json index db48fd3..d435c4f 100644 --- a/pipeline/aruco_positions_optimized.json +++ b/pipeline/aruco_positions_optimized.json @@ -1,121 +1,121 @@ { - "schema_version": "1.1", - "created_utc": "2026-05-29T11:11:54Z", + "schema_version": "1.2", + "created_utc": "2026-05-29T17:30:30Z", "summary": { "num_cameras": 3, "num_markers": 8, - "num_constraints": 31 + "num_constraints": 25 }, "markers": [ { "marker_id": 122, "position_m": [ - 0.17089422820567965, - -0.25275415672150703, - 0.17641525453920115 + 0.16408414603046484, + -0.2598816877789887, + 0.17784668754227578 ], "position_mm": [ - 170.89422820567964, - -252.75415672150703, - 176.41525453920116 + 164.08414603046484, + -259.8816877789887, + 177.8466875422758 ], "link": "Arm2" }, { "marker_id": 198, "position_m": [ - 0.0782893264155328, - -0.04392881888831051, - 0.12370444588754521 + 0.11704671868929717, + -0.05006352997427808, + 0.13545744846179997 ], "position_mm": [ - 78.2893264155328, - -43.92881888831051, - 123.70444588754522 + 117.04671868929717, + -50.063529974278076, + 135.45744846179997 ], "link": "Arm1" }, { "marker_id": 210, "position_m": [ - 0.09810874978803365, - 0.04840357531258377, - -0.061619957949719154 + 0.019883267288547467, + -0.019568887683375974, + -6.02159742251129e-05 ], "position_mm": [ - 98.10874978803365, - 48.40357531258377, - -61.61995794971915 + 19.883267288547465, + -19.568887683375973, + -0.0602159742251129 ], "link": "Board" }, { "marker_id": 211, "position_m": [ - 0.26424764035228315, - -0.0563864907759364, - 0.05844872874629282 + 0.24989200282636703, + -0.009772675866705374, + 1.0334501290569615e-05 ], "position_mm": [ - 264.24764035228316, - -56.3864907759364, - 58.448728746292815 + 249.89200282636702, + -9.772675866705374, + 0.010334501290569615 ], "link": "Board" }, { "marker_id": 214, "position_m": [ - 0.34990989719464827, - -0.024224263154301, - 0.01810457272024633 + 0.34989199483349664, + -0.009735514620349328, + 1.4271508683918328e-05 ], "position_mm": [ - 349.9098971946483, - -24.224263154301, - 18.10457272024633 + 349.89199483349665, + -9.735514620349328, + 0.014271508683918327 ], "link": "Board" }, { "marker_id": 215, "position_m": [ - 0.24658646632630482, - -0.07931906684450687, - -0.016131019201613698 + 0.24981646807454147, + -0.08977263518943693, + -1.508171313798581e-05 ], "position_mm": [ - 246.58646632630482, - -79.31906684450688, - -16.131019201613697 + 249.81646807454146, + -89.77263518943693, + -0.01508171313798581 ], "link": "Board" }, { "marker_id": 229, "position_m": [ - 0.1138713279648291, - -0.12617149104964914, - 0.1320972625673932 + 0.11773154751353263, + -0.13967941405892637, + 0.1437326114716167 ], "position_mm": [ - 113.8713279648291, - -126.17149104964915, - 132.09726256739322 + 117.73154751353262, + -139.67941405892637, + 143.7326114716167 ], "link": "Arm1" }, { "marker_id": 243, "position_m": [ - 0.1181663829394805, - -0.16401370137352922, - 0.1004795056960036 + 0.11835862963107012, + -0.16963843269272738, + 0.10433506872776273 ], "position_mm": [ - 118.1663829394805, - -164.01370137352922, - 100.4795056960036 + 118.35862963107012, + -169.63843269272738, + 104.33506872776273 ], "link": "Arm1" } diff --git a/pipeline/aruco_positions_optimized_Set1_v1.json b/pipeline/aruco_positions_optimized_Set1_v1.json new file mode 100644 index 0000000..a6e68b6 --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set1_v1.json @@ -0,0 +1,95 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T17:28:02Z", + "summary": { + "num_cameras": 2, + "num_markers": 6, + "num_constraints": 124 + }, + "markers": [ + { + "marker_id": 101, + "position_m": [ + 0.2763032509383358, + -0.2695119989339591, + 0.20699150350169354 + ], + "position_mm": [ + 276.3032509383358, + -269.5119989339591, + 206.99150350169353 + ], + "link": "Arm2" + }, + { + "marker_id": 124, + "position_m": [ + 0.2793537536854657, + -0.19507589060009986, + 0.20575080844610052 + ], + "position_mm": [ + 279.35375368546573, + -195.07589060009985, + 205.75080844610054 + ], + "link": "Arm2" + }, + { + "marker_id": 211, + "position_m": [ + 0.2504668030892929, + -0.009744842404164922, + 0.00036153034259814553 + ], + "position_mm": [ + 250.4668030892929, + -9.744842404164922, + 0.36153034259814554 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.2505650600131585, + -0.08974350678317572, + -7.314160355311047e-05 + ], + "position_mm": [ + 250.56506001315847, + -89.74350678317572, + -0.07314160355311047 + ], + "link": "Board" + }, + { + "marker_id": 217, + "position_m": [ + 0.6505649424117744, + -0.08925113206211865, + 5.944202640911437e-06 + ], + "position_mm": [ + 650.5649424117744, + -89.25113206211866, + 0.005944202640911437 + ], + "link": "Board" + }, + { + "marker_id": 243, + "position_m": [ + 0.22189116732597133, + -0.11814767263539784, + 0.2410348899290192 + ], + "position_mm": [ + 221.89116732597134, + -118.14767263539784, + 241.0348899290192 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/aruco_positions_optimized_Set1_v3.json b/pipeline/aruco_positions_optimized_Set1_v3.json new file mode 100644 index 0000000..3b569d3 --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set1_v3.json @@ -0,0 +1,95 @@ +{ + "schema_version": "1.2", + "created_utc": "2026-05-29T17:27:22Z", + "summary": { + "num_cameras": 2, + "num_markers": 6, + "num_constraints": 25 + }, + "markers": [ + { + "marker_id": 101, + "position_m": [ + 0.27925522736880354, + -0.26810432660177613, + 0.20675746626010996 + ], + "position_mm": [ + 279.25522736880356, + -268.1043266017761, + 206.75746626010996 + ], + "link": "Arm2" + }, + { + "marker_id": 124, + "position_m": [ + 0.27817972721745543, + -0.198693114830424, + 0.20728978938475356 + ], + "position_mm": [ + 278.1797272174554, + -198.693114830424, + 207.28978938475356 + ], + "link": "Arm2" + }, + { + "marker_id": 211, + "position_m": [ + 0.25188564339287434, + -0.009996760415642205, + 8.624190819127337e-06 + ], + "position_mm": [ + 251.88564339287433, + -9.996760415642205, + 0.008624190819127337 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.24918266331600872, + -0.08994956851968179, + 0.0004979860645055631 + ], + "position_mm": [ + 249.18266331600873, + -89.94956851968179, + 0.49798606450556304 + ], + "link": "Board" + }, + { + "marker_id": 217, + "position_m": [ + 0.6508078605096576, + -0.08875098840686577, + -0.00019743032075163398 + ], + "position_mm": [ + 650.8078605096575, + -88.75098840686577, + -0.19743032075163397 + ], + "link": "Board" + }, + { + "marker_id": 243, + "position_m": [ + 0.22189115068314824, + -0.11814766370925495, + 0.24103488459175698 + ], + "position_mm": [ + 221.89115068314825, + -118.14766370925494, + 241.034884591757 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/aruco_positions_optimized_Set1_v4.json b/pipeline/aruco_positions_optimized_Set1_v4.json new file mode 100644 index 0000000..2d7631c --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set1_v4.json @@ -0,0 +1,95 @@ +{ + "schema_version": "1.2", + "created_utc": "2026-05-29T17:25:57Z", + "summary": { + "num_cameras": 2, + "num_markers": 6, + "num_constraints": 25 + }, + "markers": [ + { + "marker_id": 101, + "position_m": [ + 0.27925522736880354, + -0.26810432660177613, + 0.20675746626010996 + ], + "position_mm": [ + 279.25522736880356, + -268.1043266017761, + 206.75746626010996 + ], + "link": "Arm2" + }, + { + "marker_id": 124, + "position_m": [ + 0.27817972721745543, + -0.198693114830424, + 0.20728978938475356 + ], + "position_mm": [ + 278.1797272174554, + -198.693114830424, + 207.28978938475356 + ], + "link": "Arm2" + }, + { + "marker_id": 211, + "position_m": [ + 0.25188564339287434, + -0.009996760415642205, + 8.624190819127337e-06 + ], + "position_mm": [ + 251.88564339287433, + -9.996760415642205, + 0.008624190819127337 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.24918266331600872, + -0.08994956851968179, + 0.0004979860645055631 + ], + "position_mm": [ + 249.18266331600873, + -89.94956851968179, + 0.49798606450556304 + ], + "link": "Board" + }, + { + "marker_id": 217, + "position_m": [ + 0.6508078605096576, + -0.08875098840686577, + -0.00019743032075163398 + ], + "position_mm": [ + 650.8078605096575, + -88.75098840686577, + -0.19743032075163397 + ], + "link": "Board" + }, + { + "marker_id": 243, + "position_m": [ + 0.22189115068314824, + -0.11814766370925495, + 0.24103488459175698 + ], + "position_mm": [ + 221.89115068314825, + -118.14766370925494, + 241.034884591757 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/aruco_positions_optimized_Set3_v1.json b/pipeline/aruco_positions_optimized_Set3_v1.json new file mode 100644 index 0000000..b2a5e19 --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set3_v1.json @@ -0,0 +1,123 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T15:46:21Z", + "summary": { + "num_cameras": 3, + "num_markers": 8, + "num_constraints": 124 + }, + "markers": [ + { + "marker_id": 122, + "position_m": [ + 0.17089422822056546, + -0.2527541571654684, + 0.1764152549940555 + ], + "position_mm": [ + 170.89422822056545, + -252.7541571654684, + 176.4152549940555 + ], + "link": "Arm2" + }, + { + "marker_id": 198, + "position_m": [ + 0.11820089366496654, + -0.047715698517725044, + 0.13484286562398873 + ], + "position_mm": [ + 118.20089366496654, + -47.715698517725045, + 134.84286562398873 + ], + "link": "Arm1" + }, + { + "marker_id": 210, + "position_m": [ + 0.019901746801512552, + -0.01963466568686263, + 9.617252287009137e-06 + ], + "position_mm": [ + 19.90174680151255, + -19.63466568686263, + 0.009617252287009137 + ], + "link": "Board" + }, + { + "marker_id": 211, + "position_m": [ + 0.2499096083027387, + -0.009818013466186716, + 7.989879668767411e-05 + ], + "position_mm": [ + 249.9096083027387, + -9.818013466186716, + 0.0798987966876741 + ], + "link": "Board" + }, + { + "marker_id": 214, + "position_m": [ + 0.34990956338036805, + -0.009897723013002886, + 0.00018350870434418896 + ], + "position_mm": [ + 349.90956338036807, + -9.897723013002885, + 0.18350870434418895 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.2498458511342522, + -0.08981799167832137, + 8.390710569292869e-05 + ], + "position_mm": [ + 249.8458511342522, + -89.81799167832138, + 0.08390710569292868 + ], + "link": "Board" + }, + { + "marker_id": 229, + "position_m": [ + 0.11749065839320026, + -0.1374999325479895, + 0.14103531578996023 + ], + "position_mm": [ + 117.49065839320026, + -137.49993254798952, + 141.03531578996024 + ], + "link": "Arm1" + }, + { + "marker_id": 243, + "position_m": [ + 0.11810035461313918, + -0.17482765345663995, + 0.10853212348806678 + ], + "position_mm": [ + 118.10035461313919, + -174.82765345663995, + 108.53212348806677 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/aruco_positions_optimized_Set3_v2.json b/pipeline/aruco_positions_optimized_Set3_v2.json new file mode 100644 index 0000000..db48fd3 --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set3_v2.json @@ -0,0 +1,123 @@ +{ + "schema_version": "1.1", + "created_utc": "2026-05-29T11:11:54Z", + "summary": { + "num_cameras": 3, + "num_markers": 8, + "num_constraints": 31 + }, + "markers": [ + { + "marker_id": 122, + "position_m": [ + 0.17089422820567965, + -0.25275415672150703, + 0.17641525453920115 + ], + "position_mm": [ + 170.89422820567964, + -252.75415672150703, + 176.41525453920116 + ], + "link": "Arm2" + }, + { + "marker_id": 198, + "position_m": [ + 0.0782893264155328, + -0.04392881888831051, + 0.12370444588754521 + ], + "position_mm": [ + 78.2893264155328, + -43.92881888831051, + 123.70444588754522 + ], + "link": "Arm1" + }, + { + "marker_id": 210, + "position_m": [ + 0.09810874978803365, + 0.04840357531258377, + -0.061619957949719154 + ], + "position_mm": [ + 98.10874978803365, + 48.40357531258377, + -61.61995794971915 + ], + "link": "Board" + }, + { + "marker_id": 211, + "position_m": [ + 0.26424764035228315, + -0.0563864907759364, + 0.05844872874629282 + ], + "position_mm": [ + 264.24764035228316, + -56.3864907759364, + 58.448728746292815 + ], + "link": "Board" + }, + { + "marker_id": 214, + "position_m": [ + 0.34990989719464827, + -0.024224263154301, + 0.01810457272024633 + ], + "position_mm": [ + 349.9098971946483, + -24.224263154301, + 18.10457272024633 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.24658646632630482, + -0.07931906684450687, + -0.016131019201613698 + ], + "position_mm": [ + 246.58646632630482, + -79.31906684450688, + -16.131019201613697 + ], + "link": "Board" + }, + { + "marker_id": 229, + "position_m": [ + 0.1138713279648291, + -0.12617149104964914, + 0.1320972625673932 + ], + "position_mm": [ + 113.8713279648291, + -126.17149104964915, + 132.09726256739322 + ], + "link": "Arm1" + }, + { + "marker_id": 243, + "position_m": [ + 0.1181663829394805, + -0.16401370137352922, + 0.1004795056960036 + ], + "position_mm": [ + 118.1663829394805, + -164.01370137352922, + 100.4795056960036 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/aruco_positions_optimized_Set3_v3.json b/pipeline/aruco_positions_optimized_Set3_v3.json new file mode 100644 index 0000000..c710e17 --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set3_v3.json @@ -0,0 +1,123 @@ +{ + "schema_version": "1.2", + "created_utc": "2026-05-29T16:16:41Z", + "summary": { + "num_cameras": 3, + "num_markers": 8, + "num_constraints": 31 + }, + "markers": [ + { + "marker_id": 122, + "position_m": [ + 0.1640841460387451, + -0.25988168778348236, + 0.1778466875479601 + ], + "position_mm": [ + 164.0841460387451, + -259.88168778348233, + 177.8466875479601 + ], + "link": "Arm2" + }, + { + "marker_id": 198, + "position_m": [ + 0.07976244041440077, + -0.038576018828714795, + 0.11487315663801236 + ], + "position_mm": [ + 79.76244041440077, + -38.576018828714794, + 114.87315663801236 + ], + "link": "Arm1" + }, + { + "marker_id": 210, + "position_m": [ + 0.0996821756406683, + 0.03710847589362415, + -0.05331635027530674 + ], + "position_mm": [ + 99.68217564066829, + 37.108475893624146, + -53.31635027530674 + ], + "link": "Board" + }, + { + "marker_id": 211, + "position_m": [ + 0.2680081552819457, + -0.062117037306744734, + 0.0684224623138917 + ], + "position_mm": [ + 268.00815528194573, + -62.117037306744734, + 68.4224623138917 + ], + "link": "Board" + }, + { + "marker_id": 214, + "position_m": [ + 0.34938478881198143, + -0.027829551996677953, + 0.02149438584856872 + ], + "position_mm": [ + 349.3847888119814, + -27.829551996677953, + 21.49438584856872 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.24863351055087862, + -0.08655725852674244, + -0.0052476759116907076 + ], + "position_mm": [ + 248.6335105508786, + -86.55725852674244, + -5.247675911690708 + ], + "link": "Board" + }, + { + "marker_id": 229, + "position_m": [ + 0.11002053390049146, + -0.12250830094158902, + 0.12671062737439784 + ], + "position_mm": [ + 110.02053390049146, + -122.50830094158901, + 126.71062737439784 + ], + "link": "Arm1" + }, + { + "marker_id": 243, + "position_m": [ + 0.11781900402232785, + -0.16245035968935984, + 0.09853348887316105 + ], + "position_mm": [ + 117.81900402232785, + -162.45035968935983, + 98.53348887316105 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/aruco_positions_optimized_Set3_v3_ohneConstraints.json b/pipeline/aruco_positions_optimized_Set3_v3_ohneConstraints.json new file mode 100644 index 0000000..e24ab5a --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set3_v3_ohneConstraints.json @@ -0,0 +1,123 @@ +{ + "schema_version": "1.2", + "created_utc": "2026-05-29T16:20:54Z", + "summary": { + "num_cameras": 3, + "num_markers": 8, + "num_constraints": 0 + }, + "markers": [ + { + "marker_id": 122, + "position_m": [ + 0.1640841237526153, + -0.25988166817741076, + 0.17784664622850113 + ], + "position_mm": [ + 164.0841237526153, + -259.88166817741075, + 177.84664622850113 + ], + "link": "Arm2" + }, + { + "marker_id": 198, + "position_m": [ + 0.11885196815304096, + -0.054736573352492865, + 0.13960310586430766 + ], + "position_mm": [ + 118.85196815304096, + -54.73657335249286, + 139.60310586430765 + ], + "link": "Arm1" + }, + { + "marker_id": 210, + "position_m": [ + 0.020141963615235826, + -0.019634164877100443, + 7.753085871262744e-06 + ], + "position_mm": [ + 20.141963615235827, + -19.634164877100442, + 0.007753085871262744 + ], + "link": "Board" + }, + { + "marker_id": 211, + "position_m": [ + 0.2496593710697911, + -0.009392918889186603, + -0.0004990225599633067 + ], + "position_mm": [ + 249.65937106979112, + -9.392918889186603, + -0.49902255996330674 + ], + "link": "Board" + }, + { + "marker_id": 214, + "position_m": [ + 0.3498840377079552, + -0.009731954903122878, + 9.81931763459405e-06 + ], + "position_mm": [ + 349.8840377079552, + -9.731954903122878, + 0.009819317634594048 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.24980138880179487, + -0.08972396993800054, + -8.289008073662344e-05 + ], + "position_mm": [ + 249.80138880179487, + -89.72396993800054, + -0.08289008073662345 + ], + "link": "Board" + }, + { + "marker_id": 229, + "position_m": [ + 0.11814536932848009, + -0.137946952742281, + 0.14664027759400072 + ], + "position_mm": [ + 118.14536932848009, + -137.94695274228098, + 146.64027759400074 + ], + "link": "Arm1" + }, + { + "marker_id": 243, + "position_m": [ + 0.11842637897038044, + -0.16941492081348128, + 0.0997105370611122 + ], + "position_mm": [ + 118.42637897038044, + -169.41492081348127, + 99.7105370611122 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/aruco_positions_optimized_Set3_v4.json b/pipeline/aruco_positions_optimized_Set3_v4.json new file mode 100644 index 0000000..c7778d8 --- /dev/null +++ b/pipeline/aruco_positions_optimized_Set3_v4.json @@ -0,0 +1,123 @@ +{ + "schema_version": "1.2", + "created_utc": "2026-05-29T17:29:56Z", + "summary": { + "num_cameras": 3, + "num_markers": 8, + "num_constraints": 25 + }, + "markers": [ + { + "marker_id": 122, + "position_m": [ + 0.16408414603046484, + -0.2598816877789887, + 0.17784668754227578 + ], + "position_mm": [ + 164.08414603046484, + -259.8816877789887, + 177.8466875422758 + ], + "link": "Arm2" + }, + { + "marker_id": 198, + "position_m": [ + 0.11704671868929717, + -0.05006352997427808, + 0.13545744846179997 + ], + "position_mm": [ + 117.04671868929717, + -50.063529974278076, + 135.45744846179997 + ], + "link": "Arm1" + }, + { + "marker_id": 210, + "position_m": [ + 0.019883267288547467, + -0.019568887683375974, + -6.02159742251129e-05 + ], + "position_mm": [ + 19.883267288547465, + -19.568887683375973, + -0.0602159742251129 + ], + "link": "Board" + }, + { + "marker_id": 211, + "position_m": [ + 0.24989200282636703, + -0.009772675866705374, + 1.0334501290569615e-05 + ], + "position_mm": [ + 249.89200282636702, + -9.772675866705374, + 0.010334501290569615 + ], + "link": "Board" + }, + { + "marker_id": 214, + "position_m": [ + 0.34989199483349664, + -0.009735514620349328, + 1.4271508683918328e-05 + ], + "position_mm": [ + 349.89199483349665, + -9.735514620349328, + 0.014271508683918327 + ], + "link": "Board" + }, + { + "marker_id": 215, + "position_m": [ + 0.24981646807454147, + -0.08977263518943693, + -1.508171313798581e-05 + ], + "position_mm": [ + 249.81646807454146, + -89.77263518943693, + -0.01508171313798581 + ], + "link": "Board" + }, + { + "marker_id": 229, + "position_m": [ + 0.11773154751353263, + -0.13967941405892637, + 0.1437326114716167 + ], + "position_mm": [ + 117.73154751353262, + -139.67941405892637, + 143.7326114716167 + ], + "link": "Arm1" + }, + { + "marker_id": 243, + "position_m": [ + 0.11835862963107012, + -0.16963843269272738, + 0.10433506872776273 + ], + "position_mm": [ + 118.35862963107012, + -169.63843269272738, + 104.33506872776273 + ], + "link": "Arm1" + } + ] +} \ No newline at end of file diff --git a/pipeline/arucos_state.json b/pipeline/arucos_state.json new file mode 100644 index 0000000..fa4dfff --- /dev/null +++ b/pipeline/arucos_state.json @@ -0,0 +1,1593 @@ +{ + "schema_version": "1.0", + "method": "deterministic_geometric_sequential_prefix", + "created_utc": "2026-05-29T22:10:49Z", + "root_pose": { + "translation_m": [ + -0.00010859211742092478, + 0.00042385507834860614, + -0.00035770640297937134 + ], + "rotation_matrix": [ + [ + 0.9999997764000005, + 0.0006263290459170604, + -0.00023433282914781965 + ], + [ + -0.0006263716246644091, + 0.9999997873267732, + -0.00018167276957779116 + ], + [ + 0.00023421899237906197, + 0.00018181950839066228, + 0.999999956041564 + ] + ], + "euler_xyz_deg": [ + 0.010417490807070044, + -0.013419759867825114, + -0.03588845383117196 + ] + }, + "movements": { + "x": { + "value_m": -0.000596897216554421, + "value_mm": -0.596897216554421, + "joint_type": "linear", + "link": "Base" + }, + "y": { + "value_rad": -0.058884156231993945, + "value_deg": -3.3738136322822174, + "joint_type": "revolute", + "link": "Arm1" + }, + "z": { + "value_rad": 1.951876013739172, + "value_deg": 111.83425772007364, + "joint_type": "revolute", + "link": "Ellbow" + }, + "a": { + "value_rad": 1.335934792352644, + "value_deg": 76.5434253064925, + "joint_type": "revolute", + "link": "Arm2" + }, + "b": { + "value_rad": 0.0, + "value_deg": 0.0, + "joint_type": "revolute", + "link": "Hand" + }, + "c": { + "value_rad": 0.0, + "value_deg": 0.0, + "joint_type": "revolute", + "link": "Palm" + }, + "e": { + "value_m": 0.0, + "value_mm": 0.0, + "joint_type": "linear", + "link": "FingerB" + } + }, + "link_poses": { + "Board": { + "translation_m": [ + -0.00010859211742092478, + 0.00042385507834860614, + -0.00035770640297937134 + ], + "rotation_matrix": [ + [ + 0.9999997764000005, + 0.0006263290459170604, + -0.00023433282914781965 + ], + [ + -0.0006263716246644091, + 0.9999997873267732, + -0.00018167276957779116 + ], + [ + 0.00023421899237906197, + 0.00018181950839066228, + 0.999999956041564 + ] + ] + }, + "Base": { + "translation_m": [ + -0.0007092385257748967, + 0.00042132219351465196, + 0.015642153089021036 + ], + "rotation_matrix": [ + [ + 0.9999997764000005, + 0.0006263290459170604, + -0.00023433282914781965 + ], + [ + -0.0006263716246644091, + 0.9999997873267732, + -0.00018167276957779116 + ], + [ + 0.00023421899237906197, + 0.00018181950839066228, + 0.999999956041564 + ] + ] + }, + "Arm1": { + "translation_m": [ + 0.10934783543787255, + 0.10834422307146208, + 0.0606875517069593 + ], + "rotation_matrix": [ + [ + 0.999999776399997, + 0.000611452992262134, + -0.0002707862382571833 + ], + [ + -0.0006263716246644069, + 0.9982559251795456, + -0.05903147891175731 + ], + [ + 0.00023421899237906116, + 0.05903163532513469, + 0.9982560849663261 + ] + ] + }, + "Ellbow": { + "translation_m": [ + 0.10919497218980702, + -0.14121975822342434, + 0.04592964287567563 + ], + "rotation_matrix": [ + [ + 0.9999997763972533, + 2.394763040175848e-05, + 0.0006683011743017988 + ], + [ + -0.0006263716246626884, + -0.31647748461030295, + 0.9485998151968996 + ], + [ + 0.00023421899237841852, + -0.9486000216948743, + -0.31647739884582166 + ] + ] + }, + "Arm2": { + "translation_m": [ + 0.19919495206555982, + -0.14127613166964398, + 0.04595072258498969 + ], + "rotation_matrix": [ + [ + 0.23335822552244836, + 2.3947630401721733e-05, + -0.9723908360325794 + ], + [ + 0.9224117372832784, + -0.3164774846098173, + 0.2213562482816927 + ], + [ + -0.3077345048889252, + -0.9486000216934186, + -0.07387471380203312 + ] + ] + }, + "Hand": { + "translation_m": [ + 0.1991889651579594, + -0.06215676051718966, + 0.28310072800834435 + ], + "rotation_matrix": [ + [ + 0.23335822552244836, + 2.3947630401721733e-05, + -0.9723908360325794 + ], + [ + 0.9224117372832784, + -0.3164774846098173, + 0.2213562482816927 + ], + [ + -0.3077345048889252, + -0.9486000216934186, + -0.07387471380203312 + ] + ] + }, + "Palm": { + "translation_m": [ + 0.1991889651579594, + -0.06215676051718966, + 0.28310072800834435 + ], + "rotation_matrix": [ + [ + 0.23335822552244836, + 2.3947630401721733e-05, + -0.9723908360325794 + ], + [ + 0.9224117372832784, + -0.3164774846098173, + 0.2213562482816927 + ], + [ + -0.3077345048889252, + -0.9486000216934186, + -0.07387471380203312 + ] + ] + }, + "FingerA": { + "translation_m": [ + 0.20012155989298513, + -0.047390401606712936, + 0.3150707907480583 + ], + "rotation_matrix": [ + [ + 0.23335822552244836, + 2.3947630401721733e-05, + -0.9723908360325794 + ], + [ + 0.9224117372832784, + -0.3164774846098173, + 0.2213562482816927 + ], + [ + -0.3077345048889252, + -0.9486000216934186, + -0.07387471380203312 + ] + ] + }, + "FingerB": { + "translation_m": [ + 0.19825469408880553, + -0.05476969550497916, + 0.3175326667871697 + ], + "rotation_matrix": [ + [ + 0.23335822552244836, + 2.3947630401721733e-05, + -0.9723908360325794 + ], + [ + 0.9224117372832784, + -0.3164774846098173, + 0.2213562482816927 + ], + [ + -0.3077345048889252, + -0.9486000216934186, + -0.07387471380203312 + ] + ] + } + }, + "stage_reports": [ + { + "method": "deterministic_geometric_prefix", + "active_links": [ + "Board", + "Base", + "Arm1" + ], + "active_observations": 7, + "joint_updates": [ + { + "pass": 0, + "updates": [ + { + "link": "Base", + "joint_variable": "x", + "joint_type": "linear", + "old": 0.0, + "new": 0.007926192045056242, + "info": { + "reason": "weighted_projection", + "used_markers": 3, + "axis_world": [ + 0.9999997763990008, + -0.000626371624663783, + 0.00023421899237882783 + ], + "per_marker": [ + { + "marker_id": 198, + "q_i": 0.0072187189649061964, + "weight": 1.0 + }, + { + "marker_id": 229, + "q_i": 0.007961618683264957, + "weight": 1.0 + }, + { + "marker_id": 243, + "q_i": 0.008598238486997574, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm1", + "joint_variable": "y", + "joint_type": "revolute", + "old": 0.0, + "new": -0.1780529392099517, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 3, + "axis_world": [ + -0.9999997763990008, + 0.000626371624663783, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -0.17805293920995172, + "theta_alt_rad": 2.9635397143798414, + "score_theta": 0.023858209417714757, + "score_theta_alt": 0.8762278856863018, + "best_score": 0.023858209417714757, + "per_marker": [ + { + "marker_id": 198, + "weight": 1.0 + }, + { + "marker_id": 229, + "weight": 1.0 + }, + { + "marker_id": 243, + "weight": 1.0 + } + ] + } + } + ] + }, + { + "pass": 1, + "updates": [ + { + "link": "Base", + "joint_variable": "x", + "joint_type": "linear", + "old": 0.007926192045056242, + "new": 0.007926192045056247, + "info": { + "reason": "weighted_projection", + "used_markers": 3, + "axis_world": [ + 0.9999997763990008, + -0.000626371624663783, + 0.00023421899237882783 + ], + "per_marker": [ + { + "marker_id": 198, + "q_i": 0.007218718964906196, + "weight": 1.0 + }, + { + "marker_id": 229, + "q_i": 0.007961618683264962, + "weight": 1.0 + }, + { + "marker_id": 243, + "q_i": 0.00859823848699758, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm1", + "joint_variable": "y", + "joint_type": "revolute", + "old": -0.1780529392099517, + "new": -0.1780529392099517, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 3, + "axis_world": [ + -0.9999997763990008, + 0.000626371624663783, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -0.17805293920995172, + "theta_alt_rad": 2.9635397143798414, + "score_theta": 0.023858209417714757, + "score_theta_alt": 0.8762278856863018, + "best_score": 0.023858209417714757, + "per_marker": [ + { + "marker_id": 198, + "weight": 1.0 + }, + { + "marker_id": 229, + "weight": 1.0 + }, + { + "marker_id": 243, + "weight": 1.0 + } + ] + } + } + ] + } + ], + "root_link": "Board", + "root_pose": { + "reason": "kabsch", + "used_markers": [ + 210, + 211, + 215, + 214 + ] + }, + "marker_stats": { + "num_markers_used": 7, + "mean_error_m": 0.036612631242172615, + "rms_error_m": 0.05640926160205541, + "median_error_m": 6.087233578986277e-05, + "worst_error_m": 0.09420953868705992 + }, + "marker_reports": [ + { + "marker_id": 198, + "link": "Arm1", + "error_m": [ + 0.0007202617604440409, + -0.005268103086883809, + -0.06868917582050779 + ], + "error_norm_m": 0.06889466279791363, + "predicted_m": [ + 0.11776698044974121, + -0.055331633061161886, + 0.06676827264129218 + ], + "observed_m": [ + 0.11704671868929717, + -0.05006352997427808, + 0.13545744846179997 + ] + }, + { + "marker_id": 210, + "link": "Board", + "error_m": [ + -4.460758735464615e-06, + -1.9834919135045675e-05, + 3.557547737978705e-06 + ], + "error_norm_m": 2.0639247357833144e-05, + "predicted_m": [ + 0.019878806529812002, + -0.01958872260251102, + -5.6658426487134195e-05 + ], + "observed_m": [ + 0.019883267288547467, + -0.019568887683375974, + -6.02159742251129e-05 + ] + }, + { + "marker_id": 211, + "link": "Board", + "error_m": [ + -6.984434095747005e-06, + 3.988566378927173e-05, + -1.1304364446612915e-05 + ], + "error_norm_m": 4.204089855236805e-05, + "predicted_m": [ + 0.24988501839227129, + -0.009732790202916102, + -9.698631560433007e-07 + ], + "observed_m": [ + 0.24989200282636703, + -0.009772675866705374, + 1.0334501290569615e-05 + ] + }, + { + "marker_id": 214, + "link": "Board", + "error_m": [ + -6.998801225233109e-06, + -5.991274503321445e-05, + 8.180527397944547e-06 + ], + "error_norm_m": 6.087233578986277e-05, + "predicted_m": [ + 0.3498849960322714, + -0.009795427365382543, + 2.2452036081862874e-05 + ], + "observed_m": [ + 0.34989199483349664, + -0.009735514620349328, + 1.4271508683918328e-05 + ] + }, + { + "marker_id": 215, + "link": "Board", + "error_m": [ + 1.8443994056455137e-05, + 3.986200037897625e-05, + -4.3371068931049083e-07 + ], + "error_norm_m": 4.392434513945628e-05, + "predicted_m": [ + 0.24983491206859793, + -0.08973277318905795, + -1.55154238272963e-05 + ], + "observed_m": [ + 0.24981646807454147, + -0.08977263518943693, + -1.508171313798581e-05 + ] + }, + { + "marker_id": 229, + "link": "Arm1", + "error_m": [ + -1.6310178929279662e-05, + -0.004226441081051802, + -0.0929206700434215 + ], + "error_norm_m": 0.09301674038339523, + "predicted_m": [ + 0.11771523733460335, + -0.14390585513997817, + 0.05081194142819521 + ], + "observed_m": [ + 0.11773154751353263, + -0.13967941405892637, + 0.1437326114716167 + ] + }, + { + "marker_id": 243, + "link": "Arm1", + "error_m": [ + -0.000651560035294721, + -0.0025077194399967806, + -0.09417390292623168 + ], + "error_norm_m": 0.09420953868705992, + "predicted_m": [ + 0.1177070695957754, + -0.17214615213272416, + 0.010161165801531043 + ], + "observed_m": [ + 0.11835862963107012, + -0.16963843269272738, + 0.10433506872776273 + ] + } + ], + "stage_idx": 0, + "num_active_links": 3 + }, + { + "method": "deterministic_geometric_prefix", + "active_links": [ + "Board", + "Base", + "Arm1", + "Ellbow" + ], + "active_observations": 7, + "joint_updates": [ + { + "pass": 0, + "updates": [ + { + "link": "Base", + "joint_variable": "x", + "joint_type": "linear", + "old": 0.007926192045056247, + "new": 0.007926192045056247, + "info": { + "reason": "weighted_projection", + "used_markers": 3, + "axis_world": [ + 0.9999997763990008, + -0.000626371624663783, + 0.00023421899237882783 + ], + "per_marker": [ + { + "marker_id": 198, + "q_i": 0.007218718964906196, + "weight": 1.0 + }, + { + "marker_id": 229, + "q_i": 0.007961618683264962, + "weight": 1.0 + }, + { + "marker_id": 243, + "q_i": 0.00859823848699758, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm1", + "joint_variable": "y", + "joint_type": "revolute", + "old": -0.1780529392099517, + "new": -0.1780529392099517, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 3, + "axis_world": [ + -0.9999997763990008, + 0.000626371624663783, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -0.17805293920995172, + "theta_alt_rad": 2.9635397143798414, + "score_theta": 0.023858209417714757, + "score_theta_alt": 0.8762278856863018, + "best_score": 0.023858209417714757, + "per_marker": [ + { + "marker_id": 198, + "weight": 1.0 + }, + { + "marker_id": 229, + "weight": 1.0 + }, + { + "marker_id": 243, + "weight": 1.0 + } + ] + } + }, + { + "link": "Ellbow", + "joint_variable": "z", + "joint_type": "revolute", + "old": 0.0, + "new": 0.0, + "info": { + "reason": "no_observations" + } + } + ] + }, + { + "pass": 1, + "updates": [ + { + "link": "Base", + "joint_variable": "x", + "joint_type": "linear", + "old": 0.007926192045056247, + "new": 0.007926192045056247, + "info": { + "reason": "weighted_projection", + "used_markers": 3, + "axis_world": [ + 0.9999997763990008, + -0.000626371624663783, + 0.00023421899237882783 + ], + "per_marker": [ + { + "marker_id": 198, + "q_i": 0.007218718964906196, + "weight": 1.0 + }, + { + "marker_id": 229, + "q_i": 0.007961618683264962, + "weight": 1.0 + }, + { + "marker_id": 243, + "q_i": 0.00859823848699758, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm1", + "joint_variable": "y", + "joint_type": "revolute", + "old": -0.1780529392099517, + "new": -0.1780529392099517, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 3, + "axis_world": [ + -0.9999997763990008, + 0.000626371624663783, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -0.17805293920995172, + "theta_alt_rad": 2.9635397143798414, + "score_theta": 0.023858209417714757, + "score_theta_alt": 0.8762278856863018, + "best_score": 0.023858209417714757, + "per_marker": [ + { + "marker_id": 198, + "weight": 1.0 + }, + { + "marker_id": 229, + "weight": 1.0 + }, + { + "marker_id": 243, + "weight": 1.0 + } + ] + } + }, + { + "link": "Ellbow", + "joint_variable": "z", + "joint_type": "revolute", + "old": 0.0, + "new": 0.0, + "info": { + "reason": "no_observations" + } + } + ] + } + ], + "root_link": "Board", + "root_pose": { + "reason": "kabsch", + "used_markers": [ + 210, + 211, + 215, + 214 + ] + }, + "marker_stats": { + "num_markers_used": 7, + "mean_error_m": 0.036612631242172615, + "rms_error_m": 0.05640926160205541, + "median_error_m": 6.087233578986277e-05, + "worst_error_m": 0.09420953868705992 + }, + "marker_reports": [ + { + "marker_id": 198, + "link": "Arm1", + "error_m": [ + 0.0007202617604440409, + -0.005268103086883809, + -0.06868917582050779 + ], + "error_norm_m": 0.06889466279791363, + "predicted_m": [ + 0.11776698044974121, + -0.055331633061161886, + 0.06676827264129218 + ], + "observed_m": [ + 0.11704671868929717, + -0.05006352997427808, + 0.13545744846179997 + ] + }, + { + "marker_id": 210, + "link": "Board", + "error_m": [ + -4.460758735464615e-06, + -1.9834919135045675e-05, + 3.557547737978705e-06 + ], + "error_norm_m": 2.0639247357833144e-05, + "predicted_m": [ + 0.019878806529812002, + -0.01958872260251102, + -5.6658426487134195e-05 + ], + "observed_m": [ + 0.019883267288547467, + -0.019568887683375974, + -6.02159742251129e-05 + ] + }, + { + "marker_id": 211, + "link": "Board", + "error_m": [ + -6.984434095747005e-06, + 3.988566378927173e-05, + -1.1304364446612915e-05 + ], + "error_norm_m": 4.204089855236805e-05, + "predicted_m": [ + 0.24988501839227129, + -0.009732790202916102, + -9.698631560433007e-07 + ], + "observed_m": [ + 0.24989200282636703, + -0.009772675866705374, + 1.0334501290569615e-05 + ] + }, + { + "marker_id": 214, + "link": "Board", + "error_m": [ + -6.998801225233109e-06, + -5.991274503321445e-05, + 8.180527397944547e-06 + ], + "error_norm_m": 6.087233578986277e-05, + "predicted_m": [ + 0.3498849960322714, + -0.009795427365382543, + 2.2452036081862874e-05 + ], + "observed_m": [ + 0.34989199483349664, + -0.009735514620349328, + 1.4271508683918328e-05 + ] + }, + { + "marker_id": 215, + "link": "Board", + "error_m": [ + 1.8443994056455137e-05, + 3.986200037897625e-05, + -4.3371068931049083e-07 + ], + "error_norm_m": 4.392434513945628e-05, + "predicted_m": [ + 0.24983491206859793, + -0.08973277318905795, + -1.55154238272963e-05 + ], + "observed_m": [ + 0.24981646807454147, + -0.08977263518943693, + -1.508171313798581e-05 + ] + }, + { + "marker_id": 229, + "link": "Arm1", + "error_m": [ + -1.6310178929279662e-05, + -0.004226441081051802, + -0.0929206700434215 + ], + "error_norm_m": 0.09301674038339523, + "predicted_m": [ + 0.11771523733460335, + -0.14390585513997817, + 0.05081194142819521 + ], + "observed_m": [ + 0.11773154751353263, + -0.13967941405892637, + 0.1437326114716167 + ] + }, + { + "marker_id": 243, + "link": "Arm1", + "error_m": [ + -0.000651560035294721, + -0.0025077194399967806, + -0.09417390292623168 + ], + "error_norm_m": 0.09420953868705992, + "predicted_m": [ + 0.1177070695957754, + -0.17214615213272416, + 0.010161165801531043 + ], + "observed_m": [ + 0.11835862963107012, + -0.16963843269272738, + 0.10433506872776273 + ] + } + ], + "stage_idx": 1, + "num_active_links": 4 + }, + { + "method": "deterministic_geometric_prefix", + "active_links": [ + "Board", + "Base", + "Arm1", + "Ellbow", + "Arm2" + ], + "active_observations": 8, + "joint_updates": [ + { + "pass": 0, + "updates": [ + { + "link": "Base", + "joint_variable": "x", + "joint_type": "linear", + "old": 0.007926192045056247, + "new": 0.005794016107234206, + "info": { + "reason": "weighted_projection", + "used_markers": 4, + "axis_world": [ + 0.9999997763990008, + -0.000626371624663783, + 0.00023421899237882783 + ], + "per_marker": [ + { + "marker_id": 198, + "q_i": 0.007218718964906196, + "weight": 1.0 + }, + { + "marker_id": 229, + "q_i": 0.007961618683264962, + "weight": 1.0 + }, + { + "marker_id": 243, + "q_i": 0.00859823848699758, + "weight": 1.0 + }, + { + "marker_id": 122, + "q_i": -0.0006025117062319173, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm1", + "joint_variable": "y", + "joint_type": "revolute", + "old": -0.1780529392099517, + "new": -0.23589185759318765, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 4, + "axis_world": [ + -0.9999997763990008, + 0.000626371624663783, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -0.23589185759318754, + "theta_alt_rad": 2.905700795996605, + "score_theta": 0.07369009246472558, + "score_theta_alt": 1.3871696424026658, + "best_score": 0.07369009246472558, + "per_marker": [ + { + "marker_id": 198, + "weight": 1.0 + }, + { + "marker_id": 229, + "weight": 1.0 + }, + { + "marker_id": 243, + "weight": 1.0 + }, + { + "marker_id": 122, + "weight": 1.0 + } + ] + } + }, + { + "link": "Ellbow", + "joint_variable": "z", + "joint_type": "revolute", + "old": 0.0, + "new": 1.9537800882054759, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 1, + "axis_world": [ + -0.9999997763990008, + 0.000626371624663783, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -1.1878125653843175, + "theta_alt_rad": 1.9537800882054759, + "score_theta": 0.09389488476976013, + "score_theta_alt": 0.024271154524160916, + "best_score": 0.024271154524160916, + "per_marker": [ + { + "marker_id": 122, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm2", + "joint_variable": "a", + "joint_type": "revolute", + "old": 0.0, + "new": 1.297808413286715, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 1, + "axis_world": [ + -0.00014000630341583307, + 0.1463823210875183, + 0.9892280811164156 + ], + "axis_id": 2, + "theta_rad": 1.2978084132867151, + "theta_alt_rad": -1.8437842403030782, + "score_theta": 0.01631117339313782, + "score_theta_alt": 0.038026649549060154, + "best_score": 0.01631117339313782, + "per_marker": [ + { + "marker_id": 122, + "weight": 1.0 + } + ] + } + } + ] + }, + { + "pass": 1, + "updates": [ + { + "link": "Base", + "joint_variable": "x", + "joint_type": "linear", + "old": 0.005794016107234206, + "new": -0.000596897216554421, + "info": { + "reason": "weighted_projection", + "used_markers": 4, + "axis_world": [ + 0.9999997763990008, + -0.000626371624663783, + 0.00023421899237882783 + ], + "per_marker": [ + { + "marker_id": 198, + "q_i": 0.00721871896490619, + "weight": 1.0 + }, + { + "marker_id": 229, + "q_i": 0.007961618683264955, + "weight": 1.0 + }, + { + "marker_id": 243, + "q_i": 0.008598238486997579, + "weight": 1.0 + }, + { + "marker_id": 122, + "q_i": -0.02616616500138641, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm1", + "joint_variable": "y", + "joint_type": "revolute", + "old": -0.23589185759318765, + "new": -0.058884156231993945, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 4, + "axis_world": [ + -0.9999997763990008, + 0.000626371624663783, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -0.058884156231994125, + "theta_alt_rad": 3.0827084973577996, + "score_theta": 0.026338477058174103, + "score_theta_alt": 1.3158364575890724, + "best_score": 0.026338477058174103, + "per_marker": [ + { + "marker_id": 198, + "weight": 1.0 + }, + { + "marker_id": 229, + "weight": 1.0 + }, + { + "marker_id": 243, + "weight": 1.0 + }, + { + "marker_id": 122, + "weight": 1.0 + } + ] + } + }, + { + "link": "Ellbow", + "joint_variable": "z", + "joint_type": "revolute", + "old": 1.9537800882054759, + "new": 1.951876013739172, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 1, + "axis_world": [ + -0.9999997763990008, + 0.0006263716246637829, + -0.00023421899237882783 + ], + "axis_id": 0, + "theta_rad": -1.1897166398506211, + "theta_alt_rad": 1.951876013739172, + "score_theta": 0.07582250229968954, + "score_theta_alt": 0.015787381665384438, + "best_score": 0.015787381665384438, + "per_marker": [ + { + "marker_id": 122, + "weight": 1.0 + } + ] + } + }, + { + "link": "Arm2", + "joint_variable": "a", + "joint_type": "revolute", + "old": 1.297808413286715, + "new": 1.335934792352644, + "info": { + "reason": "2d_alignment+normal_tiebreak", + "used_markers": 1, + "axis_world": [ + -2.394763040175525e-05, + 0.31647748461026026, + 0.9486000216947464 + ], + "axis_id": 2, + "theta_rad": 1.335934792352644, + "theta_alt_rad": -1.8056578612371492, + "score_theta": 0.015774381349414834, + "score_theta_alt": 0.03792035021936615, + "best_score": 0.015774381349414834, + "per_marker": [ + { + "marker_id": 122, + "weight": 1.0 + } + ] + } + } + ] + } + ], + "root_link": "Board", + "root_pose": { + "reason": "kabsch", + "used_markers": [ + 210, + 211, + 215, + 214 + ] + }, + "marker_stats": { + "num_markers_used": 8, + "mean_error_m": 0.03760308986215115, + "rms_error_m": 0.057114446045352985, + "median_error_m": 0.02503283617989611, + "worst_error_m": 0.12559610403756494 + }, + "marker_reports": [ + { + "marker_id": 122, + "link": "Arm2", + "error_m": [ + 0.02694058600720428, + 0.12176662358072948, + -0.01488205485651084 + ], + "error_norm_m": 0.12559610403756494, + "predicted_m": [ + 0.19102473203766912, + -0.1381150641982592, + 0.16296463268576494 + ], + "observed_m": [ + 0.16408414603046484, + -0.2598816877789887, + 0.17784668754227578 + ] + }, + { + "marker_id": 198, + "link": "Arm1", + "error_m": [ + -0.007806193248525564, + -0.0033792967448986536, + -0.0492759954330408 + ], + "error_norm_m": 0.05000480002400236, + "predicted_m": [ + 0.1092405254407716, + -0.05344282671917673, + 0.08618145302875917 + ], + "observed_m": [ + 0.11704671868929717, + -0.05006352997427808, + 0.13545744846179997 + ] + }, + { + "marker_id": 210, + "link": "Board", + "error_m": [ + -4.460758735464615e-06, + -1.9834919135045675e-05, + 3.557547737978705e-06 + ], + "error_norm_m": 2.0639247357833144e-05, + "predicted_m": [ + 0.019878806529812002, + -0.01958872260251102, + -5.6658426487134195e-05 + ], + "observed_m": [ + 0.019883267288547467, + -0.019568887683375974, + -6.02159742251129e-05 + ] + }, + { + "marker_id": 211, + "link": "Board", + "error_m": [ + -6.984434095747005e-06, + 3.988566378927173e-05, + -1.1304364446612915e-05 + ], + "error_norm_m": 4.204089855236805e-05, + "predicted_m": [ + 0.24988501839227129, + -0.009732790202916102, + -9.698631560433007e-07 + ], + "observed_m": [ + 0.24989200282636703, + -0.009772675866705374, + 1.0334501290569615e-05 + ] + }, + { + "marker_id": 214, + "link": "Board", + "error_m": [ + -6.998801225233109e-06, + -5.991274503321445e-05, + 8.180527397944547e-06 + ], + "error_norm_m": 6.087233578986277e-05, + "predicted_m": [ + 0.3498849960322714, + -0.009795427365382543, + 2.2452036081862874e-05 + ], + "observed_m": [ + 0.34989199483349664, + -0.009735514620349328, + 1.4271508683918328e-05 + ] + }, + { + "marker_id": 215, + "link": "Board", + "error_m": [ + 1.8443994056455137e-05, + 3.986200037897625e-05, + -4.3371068931049083e-07 + ], + "error_norm_m": 4.392434513945628e-05, + "predicted_m": [ + 0.24983491206859793, + -0.08973277318905795, + -1.55154238272963e-05 + ], + "observed_m": [ + 0.24981646807454147, + -0.08977263518943693, + -1.508171313798581e-05 + ] + }, + { + "marker_id": 229, + "link": "Arm1", + "error_m": [ + -0.008546052842064611, + -0.003606445926409463, + -0.06286400562211966 + ], + "error_norm_m": 0.06354466676486209, + "predicted_m": [ + 0.10918549467146801, + -0.14328585998533583, + 0.08086860584949704 + ], + "observed_m": [ + 0.11773154751353263, + -0.13967941405892637, + 0.1437326114716167 + ] + }, + { + "marker_id": 243, + "link": "Arm1", + "error_m": [ + -0.009185058295992282, + -0.00652028291198109, + -0.06047153308846681 + ], + "error_norm_m": 0.06151167124394025, + "predicted_m": [ + 0.10917357133507784, + -0.17615871560470847, + 0.04386353563929592 + ], + "observed_m": [ + 0.11835862963107012, + -0.16963843269272738, + 0.10433506872776273 + ] + } + ], + "stage_idx": 2, + "num_active_links": 5 + } + ], + "markers": [ + { + "marker_id": 122, + "link": "Arm2", + "observed_position_m": [ + 0.16408414603046484, + -0.2598816877789887, + 0.17784668754227578 + ], + "predicted_position_m": [ + 0.19102473203766912, + -0.1381150641982592, + 0.16296463268576494 + ], + "error_m": [ + 0.02694058600720428, + 0.12176662358072948, + -0.01488205485651084 + ], + "error_norm_m": 0.12559610403756494 + }, + { + "marker_id": 198, + "link": "Arm1", + "observed_position_m": [ + 0.11704671868929717, + -0.05006352997427808, + 0.13545744846179997 + ], + "predicted_position_m": [ + 0.1092405254407716, + -0.05344282671917673, + 0.08618145302875917 + ], + "error_m": [ + -0.007806193248525564, + -0.0033792967448986536, + -0.0492759954330408 + ], + "error_norm_m": 0.05000480002400236 + }, + { + "marker_id": 210, + "link": "Board", + "observed_position_m": [ + 0.019883267288547467, + -0.019568887683375974, + -6.02159742251129e-05 + ], + "predicted_position_m": [ + 0.019878806529812002, + -0.01958872260251102, + -5.6658426487134195e-05 + ], + "error_m": [ + -4.460758735464615e-06, + -1.9834919135045675e-05, + 3.557547737978705e-06 + ], + "error_norm_m": 2.0639247357833144e-05 + }, + { + "marker_id": 211, + "link": "Board", + "observed_position_m": [ + 0.24989200282636703, + -0.009772675866705374, + 1.0334501290569615e-05 + ], + "predicted_position_m": [ + 0.24988501839227129, + -0.009732790202916102, + -9.698631560433007e-07 + ], + "error_m": [ + -6.984434095747005e-06, + 3.988566378927173e-05, + -1.1304364446612915e-05 + ], + "error_norm_m": 4.204089855236805e-05 + }, + { + "marker_id": 214, + "link": "Board", + "observed_position_m": [ + 0.34989199483349664, + -0.009735514620349328, + 1.4271508683918328e-05 + ], + "predicted_position_m": [ + 0.3498849960322714, + -0.009795427365382543, + 2.2452036081862874e-05 + ], + "error_m": [ + -6.998801225233109e-06, + -5.991274503321445e-05, + 8.180527397944547e-06 + ], + "error_norm_m": 6.087233578986277e-05 + }, + { + "marker_id": 215, + "link": "Board", + "observed_position_m": [ + 0.24981646807454147, + -0.08977263518943693, + -1.508171313798581e-05 + ], + "predicted_position_m": [ + 0.24983491206859793, + -0.08973277318905795, + -1.55154238272963e-05 + ], + "error_m": [ + 1.8443994056455137e-05, + 3.986200037897625e-05, + -4.3371068931049083e-07 + ], + "error_norm_m": 4.392434513945628e-05 + }, + { + "marker_id": 229, + "link": "Arm1", + "observed_position_m": [ + 0.11773154751353263, + -0.13967941405892637, + 0.1437326114716167 + ], + "predicted_position_m": [ + 0.10918549467146801, + -0.14328585998533583, + 0.08086860584949704 + ], + "error_m": [ + -0.008546052842064611, + -0.003606445926409463, + -0.06286400562211966 + ], + "error_norm_m": 0.06354466676486209 + }, + { + "marker_id": 243, + "link": "Arm1", + "observed_position_m": [ + 0.11835862963107012, + -0.16963843269272738, + 0.10433506872776273 + ], + "predicted_position_m": [ + 0.10917357133507784, + -0.17615871560470847, + 0.04386353563929592 + ], + "error_m": [ + -0.009185058295992282, + -0.00652028291198109, + -0.06047153308846681 + ], + "error_norm_m": 0.06151167124394025 + } + ] +} \ No newline at end of file diff --git a/pipeline/arucos_state_set3.json b/pipeline/arucos_state_set3.json new file mode 100644 index 0000000..5147816 --- /dev/null +++ b/pipeline/arucos_state_set3.json @@ -0,0 +1,799 @@ +{ + "schema_version": "2.0", + "created_utc": "2026-05-29T18:13:32Z", + "source": { + "marker_positions_file": "aruco_positions_optimized_Set3_v4.json", + "robot_file": "..\\robot.json" + }, + "summary": { + "num_links": 9, + "num_observed_markers": 8, + "num_link_markers": 25, + "root_link": "Board", + "optimizer": { + "cost": 0.001596169080421209, + "success": true, + "status": 2, + "message": "`ftol` termination condition is satisfied.", + "nfev": 4, + "njev": 4 + }, + "fit_stats": { + "num_markers_used": 8, + "mean_error_m": 0.010174015156011006, + "median_error_m": 0.0081510187450574, + "rms_error_m": 0.01234803815522387, + "worst_error_m": 0.027803375607782888, + "p80_error_m": 0.01066478101691201, + "p90_error_m": 0.016414008933028513 + }, + "stages": [ + { + "depth": 0, + "active_links": [ + "Board" + ], + "active_joint_vars": [], + "mean_error_m": 4.1869206709871756e-05, + "rms_error_m": 4.423857209220498e-05, + "worst_error_m": 6.087233578986151e-05, + "num_markers_used": 4, + "optimizer_info": { + "cost": 7.828205043028872e-09, + "success": true, + "status": 1, + "message": "`gtol` termination condition is satisfied.", + "nfev": 1, + "njev": 1 + } + }, + { + "depth": 1, + "active_links": [ + "Board", + "Base" + ], + "active_joint_vars": [ + "x" + ], + "mean_error_m": 4.1869206709871756e-05, + "rms_error_m": 4.423857209220498e-05, + "worst_error_m": 6.087233578986151e-05, + "num_markers_used": 4, + "optimizer_info": { + "cost": 7.828205043028872e-09, + "success": true, + "status": 1, + "message": "`gtol` termination condition is satisfied.", + "nfev": 1, + "njev": 1 + } + }, + { + "depth": 2, + "active_links": [ + "Board", + "Base", + "Arm1" + ], + "active_joint_vars": [ + "x", + "y" + ], + "mean_error_m": 0.006745485580070273, + "rms_error_m": 0.007483965411863164, + "worst_error_m": 0.013168099127981843, + "num_markers_used": 7, + "optimizer_info": { + "cost": 0.0006290927673917855, + "success": true, + "status": 2, + "message": "`ftol` termination condition is satisfied.", + "nfev": 5, + "njev": 5 + } + }, + { + "depth": 3, + "active_links": [ + "Board", + "Base", + "Arm1", + "Ellbow" + ], + "active_joint_vars": [ + "x", + "y", + "z" + ], + "mean_error_m": 0.0066626047418244985, + "rms_error_m": 0.007500866443446507, + "worst_error_m": 0.012885780730839473, + "num_markers_used": 7, + "optimizer_info": { + "cost": 0.0005983942414244817, + "success": true, + "status": 1, + "message": "`gtol` termination condition is satisfied.", + "nfev": 5, + "njev": 5 + } + }, + { + "depth": 4, + "active_links": [ + "Board", + "Base", + "Arm1", + "Ellbow", + "Arm2" + ], + "active_joint_vars": [ + "x", + "y", + "z", + "a" + ], + "mean_error_m": 0.01000608974879495, + "rms_error_m": 0.012284520280612297, + "worst_error_m": 0.0278453161070784, + "num_markers_used": 8, + "optimizer_info": { + "cost": 0.001600681668676478, + "success": true, + "status": 2, + "message": "`ftol` termination condition is satisfied.", + "nfev": 5, + "njev": 5 + } + }, + { + "depth": 5, + "active_links": [ + "Board", + "Base", + "Arm1", + "Ellbow", + "Arm2", + "Hand" + ], + "active_joint_vars": [ + "x", + "y", + "z", + "a", + "b" + ], + "mean_error_m": 0.010093215903747043, + "rms_error_m": 0.01231106970746885, + "worst_error_m": 0.027801581705374914, + "num_markers_used": 8, + "optimizer_info": { + "cost": 0.0015973675989890592, + "success": true, + "status": 2, + "message": "`ftol` termination condition is satisfied.", + "nfev": 4, + "njev": 4 + } + }, + { + "depth": 6, + "active_links": [ + "Board", + "Base", + "Arm1", + "Ellbow", + "Arm2", + "Hand", + "Palm" + ], + "active_joint_vars": [ + "x", + "y", + "z", + "a", + "b", + "c" + ], + "mean_error_m": 0.01014248256819925, + "rms_error_m": 0.012332226944012195, + "worst_error_m": 0.027800128557559874, + "num_markers_used": 8, + "optimizer_info": { + "cost": 0.0015965216948719777, + "success": true, + "status": 2, + "message": "`ftol` termination condition is satisfied.", + "nfev": 4, + "njev": 4 + } + }, + { + "depth": 7, + "active_links": [ + "Board", + "Base", + "Arm1", + "Ellbow", + "Arm2", + "Hand", + "Palm", + "FingerA", + "FingerB" + ], + "active_joint_vars": [ + "x", + "y", + "z", + "a", + "b", + "c", + "e" + ], + "mean_error_m": 0.010174015156011006, + "rms_error_m": 0.01234803815522387, + "worst_error_m": 0.027803375607782888, + "num_markers_used": 8, + "optimizer_info": { + "cost": 0.001596169080421209, + "success": true, + "status": 2, + "message": "`ftol` termination condition is satisfied.", + "nfev": 4, + "njev": 4 + } + } + ] + }, + "world_pose": { + "root_translation_m": [ + -0.00010499551928665273, + -0.004122751371632455, + 0.007868336770484426 + ], + "root_rotation_matrix": [ + [ + 0.9998881512412199, + 0.00015742221075852172, + 0.01495527417545956 + ], + [ + -0.00034452146365532657, + 0.9999217021212259, + 0.012508834156371969 + ], + [ + -0.014952134040888227, + -0.012512587471746087, + 0.9998099163552967 + ] + ], + "root_euler_xyz_deg": [ + -0.7170173207318126, + 0.8567260997949179, + -0.019741833137479233 + ] + }, + "movements": { + "x": { + "value_m": 0.0036718418153270935, + "value_mm": 3.6718418153270935, + "joint_type": "linear", + "link": "Base" + }, + "y": { + "value_rad": 0.153997576654123, + "value_deg": 8.823411197523626, + "joint_type": "revolute", + "link": "Arm1" + }, + "z": { + "value_rad": 0.3681612038699359, + "value_deg": 21.094083162202796, + "joint_type": "revolute", + "link": "Ellbow" + }, + "a": { + "value_rad": 0.16870892545216015, + "value_deg": 9.666309394596011, + "joint_type": "revolute", + "link": "Arm2" + }, + "b": { + "value_rad": 0.03490658503988659, + "value_deg": 2.0, + "joint_type": "revolute", + "link": "Hand" + }, + "c": { + "value_rad": 0.15707963267948966, + "value_deg": 9.0, + "joint_type": "revolute", + "link": "Palm" + }, + "e": { + "value_m": 0.001, + "value_mm": 1.0, + "joint_type": "linear", + "link": "FingerB" + } + }, + "links": [ + { + "link": "Board", + "parent": null, + "position_m": [ + -0.00010499551928665273, + -0.004122751371632455, + 0.007868336770484426 + ], + "rotation_matrix": [ + [ + 0.9998881512412199, + 0.00015742221075852172, + 0.01495527417545956 + ], + [ + -0.00034452146365532657, + 0.9999217021212259, + 0.012508834156371969 + ], + [ + -0.014952134040888227, + -0.012512587471746087, + 0.9998099163552967 + ] + ], + "euler_xyz_deg": [ + -0.7170173207318126, + 0.8567260997949179, + -0.019741833137479233 + ], + "num_observed_markers": 4, + "num_markers_total": 9 + }, + { + "link": "Base", + "parent": "Board", + "position_m": [ + 0.003805719991894641, + -0.003923875053447029, + 0.02381039356116952 + ], + "rotation_matrix": [ + [ + 0.9998881512412199, + 0.00015742221075852172, + 0.01495527417545956 + ], + [ + -0.00034452146365532657, + 0.9999217021212259, + 0.012508834156371969 + ], + [ + -0.014952134040888227, + -0.012512587471746087, + 0.9998099163552967 + ] + ], + "euler_xyz_deg": [ + -0.7170173207318126, + 0.8567260997949179, + -0.019741833137479233 + ], + "num_observed_markers": 0, + "num_markers_total": 0 + }, + { + "link": "Arm1", + "parent": "Base", + "position_m": [ + 0.11448340556508642, + 0.10459266895168001, + 0.06580574560571159 + ], + "rotation_matrix": [ + [ + 0.9998881512411962, + -0.002138424520931235, + 0.014802437231208825 + ], + [ + -0.00034452146365531843, + 0.9861696920072487, + 0.16573840795434897 + ], + [ + -0.014952134040887874, + -0.16572497007647763, + 0.9860586534181053 + ] + ], + "euler_xyz_deg": [ + -9.540428518246754, + 0.8567260997949179, + -0.019741833137479233 + ], + "num_observed_markers": 3, + "num_markers_total": 4 + }, + { + "link": "Ellbow", + "parent": "Arm1", + "position_m": [ + 0.11501801169531922, + -0.14194975405013216, + 0.107236988124831 + ], + "rotation_matrix": [ + [ + 0.9998881512410622, + -0.007322534197157381, + 0.013040916392148211 + ], + [ + -0.00034452146365527225, + 0.8604378277497007, + 0.5095553217090676 + ], + [ + -0.01495213404088587, + -0.5095028214544074, + 0.8603390660764603 + ] + ], + "euler_xyz_deg": [ + -30.634511680430315, + 0.8567260997949179, + -0.019741833137479233 + ], + "num_observed_markers": 0, + "num_markers_total": 4 + }, + { + "link": "Arm2", + "parent": "Ellbow", + "position_m": [ + 0.20500794530701483, + -0.14198076098186113, + 0.10589129606115127 + ], + "rotation_matrix": [ + [ + 0.9878818086254475, + -0.007322534197157173, + -0.15503519819536069 + ], + [ + 0.08521967406056483, + 0.8604378277496763, + 0.5023786935470759 + ], + [ + 0.12971946399655934, + -0.509502821454393, + 0.8506349014648388 + ] + ], + "euler_xyz_deg": [ + -30.920246253571754, + -7.453381520416379, + 4.930417324847138 + ], + "num_observed_markers": 1, + "num_markers_total": 8 + }, + { + "link": "Hand", + "parent": "Arm2", + "position_m": [ + 0.20683857885630413, + -0.3570902179192802, + 0.23326700142474951 + ], + "rotation_matrix": [ + [ + 0.9878818086254463, + -0.012728723895357681, + -0.15468520218346354 + ], + [ + 0.08521967406056473, + 0.8774464358320986, + 0.4720438108885581 + ], + [ + 0.12971946399655918, + -0.4795057161631937, + 0.867898109703497 + ] + ], + "euler_xyz_deg": [ + -28.92024625357375, + -7.453381520416377, + 4.930417324847139 + ], + "num_observed_markers": 0, + "num_markers_total": 0 + }, + { + "link": "Palm", + "parent": "Hand", + "position_m": [ + 0.20683857885630413, + -0.3570902179192802, + 0.23326700142474951 + ], + "rotation_matrix": [ + [ + 0.9515212474122047, + -0.012728723895357367, + -0.3073195329143514 + ], + [ + 0.15801439949076734, + 0.8774464358320769, + 0.45290087414217106 + ], + [ + 0.2638915786384106, + -0.4795057161631819, + 0.8369202488231169 + ] + ], + "euler_xyz_deg": [ + -29.810153131835676, + -15.301100626605999, + 9.428779059340897 + ], + "num_observed_markers": 0, + "num_markers_total": 0 + }, + { + "link": "FingerA", + "parent": "Palm", + "position_m": [ + 0.2120416904297017, + -0.38701077117594923, + 0.2513691593836527 + ], + "rotation_matrix": [ + [ + 0.9515212474122047, + -0.012728723895357367, + -0.3073195329143514 + ], + [ + 0.15801439949076734, + 0.8774464358320769, + 0.45290087414217106 + ], + [ + 0.2638915786384106, + -0.4795057161631819, + 0.8369202488231169 + ] + ], + "euler_xyz_deg": [ + -29.810153131835676, + -15.301100626605999, + 9.428779059340897 + ], + "num_observed_markers": 0, + "num_markers_total": 0 + }, + { + "link": "FingerB", + "parent": "Palm", + "position_m": [ + 0.20252647795558157, + -0.38859091517085653, + 0.24873024359726909 + ], + "rotation_matrix": [ + [ + 0.9515212474122047, + -0.012728723895357367, + -0.3073195329143514 + ], + [ + 0.15801439949076734, + 0.8774464358320769, + 0.45290087414217106 + ], + [ + 0.2638915786384106, + -0.4795057161631819, + 0.8369202488231169 + ] + ], + "euler_xyz_deg": [ + -29.810153131835676, + -15.301100626605999, + 9.428779059340897 + ], + "num_observed_markers": 0, + "num_markers_total": 0 + } + ], + "markers": [ + { + "marker_id": 122, + "link": "Arm2", + "observed_position_m": [ + 0.16408414603046484, + -0.2598816877789887, + 0.17784668754227578 + ], + "predicted_position_m": [ + 0.17125220583520576, + -0.24133248628194467, + 0.15841543082416373 + ], + "error_m": [ + 0.007168059804740917, + 0.018549201497044004, + -0.01943125671811205 + ], + "error_norm_m": 0.027803375607782888, + "error_norm_mm": 27.80337560778289, + "marker_size": null + }, + { + "marker_id": 198, + "link": "Arm1", + "observed_position_m": [ + 0.11704671868929717, + -0.05006352997427808, + 0.13545744846179997 + ], + "predicted_position_m": [ + 0.11534363879152772, + -0.047393637491077556, + 0.1268337936875817 + ], + "error_m": [ + -0.0017030798977694522, + 0.0026698924832005214, + -0.008623654774218281 + ], + "error_norm_m": 0.009186742005462807, + "error_norm_mm": 9.186742005462806, + "marker_size": 25 + }, + { + "marker_id": 210, + "link": "Board", + "observed_position_m": [ + 0.019883267288547467, + -0.019568887683375974, + -6.02159742251129e-05 + ], + "predicted_position_m": [ + 0.019894105643575213, + -0.024124323193083167, + 0.008119488814008173 + ], + "error_m": [ + 1.083835502774591e-05, + -0.004555435509707193, + 0.008179704788233285 + ], + "error_norm_m": 0.0093626748622222, + "error_norm_mm": 9.3626748622222, + "marker_size": null + }, + { + "marker_id": 211, + "link": "Board", + "observed_position_m": [ + 0.24989200282636703, + -0.009772675866705374, + 1.0334501290569615e-05 + ], + "predicted_position_m": [ + 0.24986995465116338, + -0.014204346108511633, + 0.004555372109886419 + ], + "error_m": [ + -2.2048175203653875e-05, + -0.0044316702418062594, + 0.004545037608595849 + ], + "error_norm_m": 0.006348035453405379, + "error_norm_mm": 6.348035453405379, + "marker_size": null + }, + { + "marker_id": 214, + "link": "Board", + "observed_position_m": [ + 0.34989199483349664, + -0.009735514620349328, + 1.4271508683918328e-05 + ], + "predicted_position_m": [ + 0.3498587697752854, + -0.014238798254877167, + 0.003060158705797595 + ], + "error_m": [ + -3.322505821123922e-05, + -0.0045032836345278385, + 0.0030458871971136767 + ], + "error_norm_m": 0.005436735805153714, + "error_norm_mm": 5.436735805153714, + "marker_size": null + }, + { + "marker_id": 215, + "link": "Board", + "observed_position_m": [ + 0.24981646807454147, + -0.08977263518943693, + -1.508171313798581e-05 + ], + "predicted_position_m": [ + 0.2498573608743027, + -0.09419808227820971, + 0.005556379107626107 + ], + "error_m": [ + 4.089279976121629e-05, + -0.00442544708877278, + 0.005571460820764092 + ], + "error_norm_m": 0.007115295484651995, + "error_norm_mm": 7.115295484651996, + "marker_size": null + }, + { + "marker_id": 229, + "link": "Arm1", + "observed_position_m": [ + 0.11773154751353263, + -0.13967941405892637, + 0.1437326114716167 + ], + "predicted_position_m": [ + 0.11553609699841152, + -0.13614890977172994, + 0.14174904099446467 + ], + "error_m": [ + -0.0021954505151211, + 0.003530504287196423, + -0.00198357047715203 + ], + "error_norm_m": 0.004606410242703852, + "error_norm_mm": 4.606410242703852, + "marker_size": 25 + }, + { + "marker_id": 243, + "link": "Arm1", + "observed_position_m": [ + 0.11835862963107012, + -0.16963843269272738, + 0.10433506872776273 + ], + "predicted_position_m": [ + 0.11509285655355182, + -0.17646569327038591, + 0.11303736207750772 + ], + "error_m": [ + -0.003265773077518297, + -0.006827260577658534, + 0.008702293349744997 + ], + "error_norm_m": 0.011532851786705215, + "error_norm_mm": 11.532851786705216, + "marker_size": 25 + } + ] +} \ No newline at end of file diff --git a/pipeline/render_1a.png b/pipeline/render_1a.png new file mode 100644 index 0000000..4451893 Binary files /dev/null and b/pipeline/render_1a.png differ diff --git a/pipeline/render_1a_aruco_detection.json b/pipeline/render_1a_aruco_detection.json new file mode 100644 index 0000000..416c2b8 --- /dev/null +++ b/pipeline/render_1a_aruco_detection.json @@ -0,0 +1,11244 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T17:30:27Z", + "vision_config": { + "MarkerType": "DICT_4X4_250", + "MarkerSize": 0.025 + }, + "camera": { + "camera_id": "cam1", + "intrinsics_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render.npz", + "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 + ] + }, + "image": { + "image_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1a.png", + "image_sha256": "693cee15555027b57d05defcf84517eafed047372b0effa201fc0c3fbc9f664f", + "width_px": 1280, + "height_px": 720 + }, + "aruco": { + "dictionary": "DICT_4X4_250", + "num_detected_markers": 17, + "num_rejected_candidates": 411 + }, + "detections": [ + { + "observation_id": "17c2cdc3-f7a1-4131-94e8-919957279f76", + "type": "aruco", + "marker_id": 102, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 863.0, + 344.0 + ], + [ + 894.0, + 309.0 + ], + [ + 936.0, + 332.0 + ], + [ + 906.0, + 368.0 + ] + ], + "center_px": [ + 899.75, + 338.25 + ], + "quality": { + "area_px": 2225.5, + "perimeter_px": 190.7457504272461, + "sharpness": { + "laplacian_var": 3935.778766550373 + }, + "contrast": { + "p05": 14.0, + "p95": 185.0, + "dynamic_range": 171.0, + "mean_gray": 101.42780748663101, + "std_gray": 79.4179284235363 + }, + "geometry": { + "distance_to_center_norm": 0.3549750745296478, + "distance_to_border_px": 309.0 + }, + "edge_ratio": 1.0532483321651058, + "edge_lengths_px": [ + 46.75468063354492, + 47.88528060913086, + 46.86149978637695, + 49.24428939819336 + ] + }, + "confidence": 0.9494437061622057 + }, + { + "observation_id": "e10cc5ce-3dff-457f-a3d6-e1c594054c72", + "type": "aruco", + "marker_id": 243, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 524.0, + 300.0 + ], + [ + 558.0, + 275.0 + ], + [ + 577.0, + 317.0 + ], + [ + 544.0, + 342.0 + ] + ], + "center_px": [ + 550.75, + 308.5 + ], + "quality": { + "area_px": 1894.5, + "perimeter_px": 176.21891403198242, + "sharpness": { + "laplacian_var": 2678.6956247003295 + }, + "contrast": { + "p05": 25.0, + "p95": 191.0, + "dynamic_range": 166.0, + "mean_gray": 82.62843676355067, + "std_gray": 73.67721820744883 + }, + "geometry": { + "distance_to_center_norm": 0.14032743871212006, + "distance_to_border_px": 275.0 + }, + "edge_ratio": 1.1236297656439467, + "edge_lengths_px": [ + 42.20189666748047, + 46.097721099853516, + 41.400482177734375, + 46.51881408691406 + ] + }, + "confidence": 0.8899728634608616 + }, + { + "observation_id": "085fc617-561d-4d7f-98fe-60003af21e6f", + "type": "aruco", + "marker_id": 210, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 137.0, + 662.0 + ], + [ + 172.0, + 635.0 + ], + [ + 200.0, + 659.0 + ], + [ + 165.0, + 686.0 + ] + ], + "center_px": [ + 168.5, + 660.5 + ], + "quality": { + "area_px": 1596.0, + "perimeter_px": 162.16449737548828, + "sharpness": { + "laplacian_var": 3797.5312506581813 + }, + "contrast": { + "p05": 17.0, + "p95": 180.0, + "dynamic_range": 163.0, + "mean_gray": 75.3645933014354, + "std_gray": 71.58896556303098 + }, + "geometry": { + "distance_to_center_norm": 0.7614269256591797, + "distance_to_border_px": 34.0 + }, + "edge_ratio": 1.1986511772098227, + "edge_lengths_px": [ + 44.204071044921875, + 36.878177642822266, + 44.204071044921875, + 36.878177642822266 + ] + }, + "confidence": 0.5673043275049208 + }, + { + "observation_id": "54875aeb-74d0-43ac-90b4-97b81f7abd51", + "type": "aruco", + "marker_id": 247, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 543.0, + 206.0 + ], + [ + 574.0, + 184.0 + ], + [ + 610.0, + 203.0 + ], + [ + 578.0, + 226.0 + ] + ], + "center_px": [ + 576.25, + 204.75 + ], + "quality": { + "area_px": 1413.0, + "perimeter_px": 158.43882751464844, + "sharpness": { + "laplacian_var": 4202.582433286217 + }, + "contrast": { + "p05": 10.0, + "p95": 176.0, + "dynamic_range": 166.0, + "mean_gray": 91.01580611169652, + "std_gray": 74.78021722492437 + }, + "geometry": { + "distance_to_center_norm": 0.22855591773986816, + "distance_to_border_px": 184.0 + }, + "edge_ratio": 1.0708467232203849, + "edge_lengths_px": [ + 38.01315689086914, + 40.70626449584961, + 39.408119201660156, + 40.31128692626953 + ] + }, + "confidence": 0.8796777163094818 + }, + { + "observation_id": "717464f7-eb45-4586-acad-42bdef364641", + "type": "aruco", + "marker_id": 246, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 593.0, + 172.0 + ], + [ + 623.0, + 150.0 + ], + [ + 658.0, + 169.0 + ], + [ + 628.0, + 190.0 + ] + ], + "center_px": [ + 625.5, + 170.25 + ], + "quality": { + "area_px": 1307.5, + "perimeter_px": 153.0037727355957, + "sharpness": { + "laplacian_var": 3236.7051337367984 + }, + "contrast": { + "p05": 10.0, + "p95": 174.0, + "dynamic_range": 164.0, + "mean_gray": 59.25871766029246, + "std_gray": 68.86066607932854 + }, + "geometry": { + "distance_to_center_norm": 0.25916191935539246, + "distance_to_border_px": 150.0 + }, + "edge_ratio": 1.0875198679615226, + "edge_lengths_px": [ + 37.202152252197266, + 39.824615478515625, + 36.619667053222656, + 39.357337951660156 + ] + }, + "confidence": 0.8015179238063419 + }, + { + "observation_id": "d29d54a1-46b4-4b77-a6c6-eb91b5ea3004", + "type": "aruco", + "marker_id": 101, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 842.0, + 431.0 + ], + [ + 838.0, + 405.0 + ], + [ + 880.0, + 430.0 + ], + [ + 882.0, + 455.0 + ] + ], + "center_px": [ + 860.5, + 430.25 + ], + "quality": { + "area_px": 972.0, + "perimeter_px": 146.9107780456543, + "sharpness": { + "laplacian_var": 3563.870814189421 + }, + "contrast": { + "p05": 40.0, + "p95": 170.0, + "dynamic_range": 130.0, + "mean_gray": 96.38335809806836, + "std_gray": 56.1831421207441 + }, + "geometry": { + "distance_to_center_norm": 0.31515657901763916, + "distance_to_border_px": 265.0 + }, + "edge_ratio": 1.9488695631540953, + "edge_lengths_px": [ + 26.305892944335938, + 48.87739944458008, + 25.079872131347656, + 46.647613525390625 + ] + }, + "confidence": 0.3325004465415643 + }, + { + "observation_id": "f11de580-c7df-430e-85c8-ad8b9ae5a604", + "type": "aruco", + "marker_id": 215, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 520.0, + 489.0 + ], + [ + 548.0, + 466.0 + ], + [ + 577.0, + 486.0 + ], + [ + 549.0, + 509.0 + ] + ], + "center_px": [ + 548.5, + 487.5 + ], + "quality": { + "area_px": 1227.0, + "perimeter_px": 142.92633819580078, + "sharpness": { + "laplacian_var": 3704.0260869185627 + }, + "contrast": { + "p05": 10.0, + "p95": 176.0, + "dynamic_range": 166.0, + "mean_gray": 74.18817852834741, + "std_gray": 74.50710285964344 + }, + "geometry": { + "distance_to_center_norm": 0.213719442486763, + "distance_to_border_px": 211.0 + }, + "edge_ratio": 1.0285998645985972, + "edge_lengths_px": [ + 36.2353401184082, + 35.22782897949219, + 36.2353401184082, + 35.22782897949219 + ] + }, + "confidence": 0.7952557920267838 + }, + { + "observation_id": "d9dc7e02-daad-4f9c-8397-8d15e5cf3b0e", + "type": "aruco", + "marker_id": 124, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 731.0, + 365.0 + ], + [ + 725.0, + 339.0 + ], + [ + 764.0, + 362.0 + ], + [ + 769.0, + 388.0 + ] + ], + "center_px": [ + 747.25, + 363.5 + ], + "quality": { + "area_px": 874.5, + "perimeter_px": 142.85512161254883, + "sharpness": { + "laplacian_var": 3569.5639450778804 + }, + "contrast": { + "p05": 41.0, + "p95": 169.0, + "dynamic_range": 128.0, + "mean_gray": 96.77759472817134, + "std_gray": 55.93531471356835 + }, + "geometry": { + "distance_to_center_norm": 0.14613474905490875, + "distance_to_border_px": 332.0 + }, + "edge_ratio": 1.7100858488288635, + "edge_lengths_px": [ + 26.68332862854004, + 45.27692413330078, + 26.476404190063477, + 44.41846466064453 + ] + }, + "confidence": 0.34091855704160245 + }, + { + "observation_id": "e63c79e5-6ed0-4d1a-a51f-0081245c110d", + "type": "aruco", + "marker_id": 229, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 450.0, + 259.0 + ], + [ + 482.0, + 236.0 + ], + [ + 511.0, + 235.0 + ], + [ + 477.0, + 258.0 + ] + ], + "center_px": [ + 480.0, + 247.0 + ], + "quality": { + "area_px": 611.0, + "perimeter_px": 136.49262046813965, + "sharpness": { + "laplacian_var": 2271.5009939565166 + }, + "contrast": { + "p05": 20.0, + "p95": 136.0, + "dynamic_range": 116.0, + "mean_gray": 55.44152744630072, + "std_gray": 45.60616184926728 + }, + "geometry": { + "distance_to_center_norm": 0.26675668358802795, + "distance_to_border_px": 235.0 + }, + "edge_ratio": 1.5192824359947652, + "edge_lengths_px": [ + 39.408119201660156, + 29.017236709594727, + 41.04875183105469, + 27.018512725830078 + ] + }, + "confidence": 0.26810902547334975 + }, + { + "observation_id": "b0ff92c5-de76-4b21-81b4-71b596bf31a8", + "type": "aruco", + "marker_id": 122, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 781.0, + 242.0 + ], + [ + 809.0, + 242.0 + ], + [ + 846.0, + 262.0 + ], + [ + 823.0, + 264.0 + ] + ], + "center_px": [ + 814.75, + 252.5 + ], + "quality": { + "area_px": 575.0, + "perimeter_px": 140.55935287475586, + "sharpness": { + "laplacian_var": 2332.9301951620905 + }, + "contrast": { + "p05": 20.0, + "p95": 144.0, + "dynamic_range": 124.0, + "mean_gray": 56.91729323308271, + "std_gray": 47.96616055186142 + }, + "geometry": { + "distance_to_center_norm": 0.27940502762794495, + "distance_to_border_px": 242.0 + }, + "edge_ratio": 2.0536884606639982, + "edge_lengths_px": [ + 28.0, + 42.05948257446289, + 23.0867919921875, + 47.41307830810547 + ] + }, + "confidence": 0.18665602922528673 + }, + { + "observation_id": "eb997c0d-6434-436c-b0c5-714ade9c90e3", + "type": "aruco", + "marker_id": 198, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 355.0, + 259.0 + ], + [ + 388.0, + 238.0 + ], + [ + 412.0, + 237.0 + ], + [ + 379.0, + 259.0 + ] + ], + "center_px": [ + 383.5, + 248.25 + ], + "quality": { + "area_px": 499.5, + "perimeter_px": 126.79710388183594, + "sharpness": { + "laplacian_var": 3972.096222043203 + }, + "contrast": { + "p05": 22.3, + "p95": 135.0, + "dynamic_range": 112.7, + "mean_gray": 68.38616714697406, + "std_gray": 42.61002890981658 + }, + "geometry": { + "distance_to_center_norm": 0.3810231387615204, + "distance_to_border_px": 237.0 + }, + "edge_ratio": 1.652544339497884, + "edge_lengths_px": [ + 39.11521530151367, + 24.020824432373047, + 39.66106414794922, + 24.0 + ] + }, + "confidence": 0.20150745250271476 + }, + { + "observation_id": "c78124cb-4a47-41ac-b148-e6500725e065", + "type": "aruco", + "marker_id": 211, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 427.0, + 425.0 + ], + [ + 456.0, + 405.0 + ], + [ + 482.0, + 423.0 + ], + [ + 455.0, + 444.0 + ] + ], + "center_px": [ + 455.0, + 424.25 + ], + "quality": { + "area_px": 1071.5, + "perimeter_px": 134.89371490478516, + "sharpness": { + "laplacian_var": 3165.7184033540047 + }, + "contrast": { + "p05": 8.399999999999999, + "p95": 165.0, + "dynamic_range": 156.6, + "mean_gray": 68.09879839786382, + "std_gray": 69.14261001184734 + }, + "geometry": { + "distance_to_center_norm": 0.26670128107070923, + "distance_to_border_px": 276.0 + }, + "edge_ratio": 1.1140017860673477, + "edge_lengths_px": [ + 35.22782897949219, + 31.62277603149414, + 34.20526123046875, + 33.83784866333008 + ] + }, + "confidence": 0.6412317666518965 + }, + { + "observation_id": "72eb0591-0a58-427d-8f90-69b113d27f57", + "type": "aruco", + "marker_id": 208, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 630.0, + 399.0 + ], + [ + 655.0, + 379.0 + ], + [ + 684.0, + 396.0 + ], + [ + 660.0, + 417.0 + ] + ], + "center_px": [ + 657.25, + 397.75 + ], + "quality": { + "area_px": 1033.5, + "perimeter_px": 132.50724029541016, + "sharpness": { + "laplacian_var": 2848.9886204705635 + }, + "contrast": { + "p05": 7.0, + "p95": 154.0, + "dynamic_range": 147.0, + "mean_gray": 57.04347826086956, + "std_gray": 62.4690403227182 + }, + "geometry": { + "distance_to_center_norm": 0.05652238056063652, + "distance_to_border_px": 303.0 + }, + "edge_ratio": 1.0970595655180506, + "edge_lengths_px": [ + 32.015621185302734, + 33.61547088623047, + 31.890438079833984, + 34.98571014404297 + ] + }, + "confidence": 0.6280424706698967 + }, + { + "observation_id": "a3c244ab-a02e-4eb1-baa6-ad7dd1713f3e", + "type": "aruco", + "marker_id": 217, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 905.0, + 176.0 + ], + [ + 924.0, + 161.0 + ], + [ + 952.0, + 174.0 + ], + [ + 933.0, + 190.0 + ] + ], + "center_px": [ + 928.5, + 175.25 + ], + "quality": { + "area_px": 690.5, + "perimeter_px": 111.22257423400879, + "sharpness": { + "laplacian_var": 3793.153007417641 + }, + "contrast": { + "p05": 9.0, + "p95": 161.0, + "dynamic_range": 152.0, + "mean_gray": 61.256513026052104, + "std_gray": 62.35783295888325 + }, + "geometry": { + "distance_to_center_norm": 0.4665455222129822, + "distance_to_border_px": 161.0 + }, + "edge_ratio": 1.2931956388084183, + "edge_lengths_px": [ + 24.20743751525879, + 30.870698928833008, + 24.83948516845703, + 31.30495262145996 + ] + }, + "confidence": 0.35596573288593486 + }, + { + "observation_id": "83ee225a-67d3-4f4c-89bb-ff182dd1e223", + "type": "aruco", + "marker_id": 206, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 816.0, + 132.0 + ], + [ + 836.0, + 118.0 + ], + [ + 862.0, + 130.0 + ], + [ + 843.0, + 145.0 + ] + ], + "center_px": [ + 839.25, + 131.25 + ], + "quality": { + "area_px": 628.0, + "perimeter_px": 107.22283935546875, + "sharpness": { + "laplacian_var": 4112.338450277431 + }, + "contrast": { + "p05": 9.0, + "p95": 161.0, + "dynamic_range": 152.0, + "mean_gray": 63.62443438914027, + "std_gray": 61.59026881671632 + }, + "geometry": { + "distance_to_center_norm": 0.41312646865844727, + "distance_to_border_px": 118.0 + }, + "edge_ratio": 1.2379107901411548, + "edge_lengths_px": [ + 24.413110733032227, + 28.635643005371094, + 24.20743751525879, + 29.96664810180664 + ] + }, + "confidence": 0.33820423087105295 + }, + { + "observation_id": "5ae06d28-4ed7-4cc3-8560-44b5c35847d2", + "type": "aruco", + "marker_id": 205, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 982.0, + 114.0 + ], + [ + 999.0, + 100.0 + ], + [ + 1027.0, + 112.0 + ], + [ + 1010.0, + 126.0 + ] + ], + "center_px": [ + 1004.5, + 113.0 + ], + "quality": { + "area_px": 596.0, + "perimeter_px": 104.97161483764648, + "sharpness": { + "laplacian_var": 5067.414824476962 + }, + "contrast": { + "p05": 9.0, + "p95": 161.0, + "dynamic_range": 152.0, + "mean_gray": 71.72076372315036, + "std_gray": 63.212494032456604 + }, + "geometry": { + "distance_to_center_norm": 0.5996246933937073, + "distance_to_border_px": 100.0 + }, + "edge_ratio": 1.383257847031654, + "edge_lengths_px": [ + 22.022714614868164, + 30.463092803955078, + 22.022714614868164, + 30.463092803955078 + ] + }, + "confidence": 0.28724459014346065 + }, + { + "observation_id": "4396d4e5-cc6d-4af8-814a-d2a00ca204d3", + "type": "aruco", + "marker_id": 207, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 895.0, + 73.0 + ], + [ + 912.0, + 60.0 + ], + [ + 938.0, + 71.0 + ], + [ + 921.0, + 85.0 + ] + ], + "center_px": [ + 916.5, + 72.25 + ], + "quality": { + "area_px": 546.5, + "perimeter_px": 100.29047966003418, + "sharpness": { + "laplacian_var": 4016.6321593416924 + }, + "contrast": { + "p05": 10.0, + "p95": 159.0, + "dynamic_range": 149.0, + "mean_gray": 64.26385224274406, + "std_gray": 61.4948700002493 + }, + "geometry": { + "distance_to_center_norm": 0.5434604287147522, + "distance_to_border_px": 60.0 + }, + "edge_ratio": 1.3380557461583085, + "edge_lengths_px": [ + 21.40093421936035, + 28.23118782043457, + 22.022714614868164, + 28.635643005371094 + ] + }, + "confidence": 0.27228561618555186 + } + ], + "rejected_candidates": [ + { + "image_points_px": [ + [ + 1234.0, + 676.0 + ], + [ + 1265.0, + 694.0 + ], + [ + 1249.0, + 716.0 + ], + [ + 1218.0, + 697.0 + ] + ], + "center_px": [ + 1241.5, + 695.75 + ], + "area_px": 962.5 + }, + { + "image_points_px": [ + [ + 481.0, + 676.0 + ], + [ + 506.0, + 694.0 + ], + [ + 482.0, + 715.0 + ], + [ + 457.0, + 697.0 + ] + ], + "center_px": [ + 481.5, + 695.5 + ], + "area_px": 957.0 + }, + { + "image_points_px": [ + [ + 631.0, + 667.0 + ], + [ + 657.0, + 686.0 + ], + [ + 634.0, + 707.0 + ], + [ + 609.0, + 689.0 + ] + ], + "center_px": [ + 632.75, + 687.25 + ], + "area_px": 964.5 + }, + { + "image_points_px": [ + [ + 780.0, + 659.0 + ], + [ + 807.0, + 677.0 + ], + [ + 786.0, + 698.0 + ], + [ + 759.0, + 680.0 + ] + ], + "center_px": [ + 783.0, + 678.5 + ], + "area_px": 945.0 + }, + { + "image_points_px": [ + [ + 730.0, + 662.0 + ], + [ + 757.0, + 680.0 + ], + [ + 736.0, + 701.0 + ], + [ + 709.0, + 683.0 + ] + ], + "center_px": [ + 733.0, + 681.5 + ], + "area_px": 945.0 + }, + { + "image_points_px": [ + [ + 581.0, + 670.0 + ], + [ + 606.0, + 688.0 + ], + [ + 585.0, + 709.0 + ], + [ + 558.0, + 691.0 + ] + ], + "center_px": [ + 582.5, + 689.5 + ], + "area_px": 942.0 + }, + { + "image_points_px": [ + [ + 34.0, + 659.0 + ], + [ + 53.0, + 678.0 + ], + [ + 24.0, + 698.0 + ], + [ + 5.0, + 680.0 + ] + ], + "center_px": [ + 29.0, + 678.75 + ], + "area_px": 926.0 + }, + { + "image_points_px": [ + [ + 680.0, + 665.0 + ], + [ + 707.0, + 683.0 + ], + [ + 685.0, + 704.0 + ], + [ + 659.0, + 685.0 + ] + ], + "center_px": [ + 682.75, + 684.25 + ], + "area_px": 941.0 + }, + { + "image_points_px": [ + [ + 1122.0, + 640.0 + ], + [ + 1152.0, + 658.0 + ], + [ + 1135.0, + 679.0 + ], + [ + 1105.0, + 661.0 + ] + ], + "center_px": [ + 1128.5, + 659.5 + ], + "area_px": 936.0 + }, + { + "image_points_px": [ + [ + 531.0, + 673.0 + ], + [ + 556.0, + 691.0 + ], + [ + 534.0, + 712.0 + ], + [ + 508.0, + 694.0 + ] + ], + "center_px": [ + 532.25, + 692.5 + ], + "area_px": 940.5 + }, + { + "image_points_px": [ + [ + 84.0, + 656.0 + ], + [ + 104.0, + 675.0 + ], + [ + 75.0, + 695.0 + ], + [ + 56.0, + 677.0 + ] + ], + "center_px": [ + 79.75, + 675.75 + ], + "area_px": 927.0 + }, + { + "image_points_px": [ + [ + 1170.0, + 638.0 + ], + [ + 1201.0, + 655.0 + ], + [ + 1185.0, + 676.0 + ], + [ + 1154.0, + 658.0 + ] + ], + "center_px": [ + 1177.5, + 656.75 + ], + "area_px": 915.5 + }, + { + "image_points_px": [ + [ + 878.0, + 654.0 + ], + [ + 906.0, + 672.0 + ], + [ + 887.0, + 693.0 + ], + [ + 859.0, + 675.0 + ] + ], + "center_px": [ + 882.5, + 673.5 + ], + "area_px": 930.0 + }, + { + "image_points_px": [ + [ + 829.0, + 656.0 + ], + [ + 856.0, + 674.0 + ], + [ + 837.0, + 695.0 + ], + [ + 809.0, + 677.0 + ] + ], + "center_px": [ + 832.75, + 675.5 + ], + "area_px": 928.5 + }, + { + "image_points_px": [ + [ + 42.0, + 619.0 + ], + [ + 62.0, + 637.0 + ], + [ + 33.0, + 657.0 + ], + [ + 14.0, + 639.0 + ] + ], + "center_px": [ + 37.75, + 638.0 + ], + "area_px": 903.0 + }, + { + "image_points_px": [ + [ + 530.0, + 633.0 + ], + [ + 555.0, + 650.0 + ], + [ + 532.0, + 671.0 + ], + [ + 507.0, + 653.0 + ] + ], + "center_px": [ + 531.0, + 651.75 + ], + "area_px": 915.0 + }, + { + "image_points_px": [ + [ + 1074.0, + 643.0 + ], + [ + 1103.0, + 661.0 + ], + [ + 1086.0, + 681.0 + ], + [ + 1056.0, + 664.0 + ] + ], + "center_px": [ + 1079.75, + 662.25 + ], + "area_px": 911.0 + }, + { + "image_points_px": [ + [ + 1025.0, + 646.0 + ], + [ + 1054.0, + 664.0 + ], + [ + 1036.0, + 684.0 + ], + [ + 1007.0, + 666.0 + ] + ], + "center_px": [ + 1030.5, + 665.0 + ], + "area_px": 904.0 + }, + { + "image_points_px": [ + [ + 725.0, + 622.0 + ], + [ + 751.0, + 639.0 + ], + [ + 731.0, + 660.0 + ], + [ + 704.0, + 642.0 + ] + ], + "center_px": [ + 727.75, + 640.75 + ], + "area_px": 902.0 + }, + { + "image_points_px": [ + [ + 1219.0, + 635.0 + ], + [ + 1249.0, + 653.0 + ], + [ + 1234.0, + 673.0 + ], + [ + 1203.0, + 655.0 + ] + ], + "center_px": [ + 1226.25, + 654.0 + ], + "area_px": 889.0 + }, + { + "image_points_px": [ + [ + 628.0, + 627.0 + ], + [ + 653.0, + 644.0 + ], + [ + 632.0, + 665.0 + ], + [ + 606.0, + 647.0 + ] + ], + "center_px": [ + 629.75, + 645.75 + ], + "area_px": 899.0 + }, + { + "image_points_px": [ + [ + 579.0, + 630.0 + ], + [ + 604.0, + 647.0 + ], + [ + 581.0, + 668.0 + ], + [ + 557.0, + 650.0 + ] + ], + "center_px": [ + 580.25, + 648.75 + ], + "area_px": 896.0 + }, + { + "image_points_px": [ + [ + 481.0, + 635.0 + ], + [ + 505.0, + 653.0 + ], + [ + 482.0, + 673.0 + ], + [ + 458.0, + 656.0 + ] + ], + "center_px": [ + 481.5, + 654.25 + ], + "area_px": 894.5 + }, + { + "image_points_px": [ + [ + 822.0, + 617.0 + ], + [ + 849.0, + 634.0 + ], + [ + 829.0, + 654.0 + ], + [ + 802.0, + 637.0 + ] + ], + "center_px": [ + 825.5, + 635.5 + ], + "area_px": 880.0 + }, + { + "image_points_px": [ + [ + 774.0, + 619.0 + ], + [ + 800.0, + 637.0 + ], + [ + 780.0, + 657.0 + ], + [ + 754.0, + 640.0 + ] + ], + "center_px": [ + 777.0, + 638.25 + ], + "area_px": 883.0 + }, + { + "image_points_px": [ + [ + 1062.0, + 604.0 + ], + [ + 1091.0, + 621.0 + ], + [ + 1074.0, + 641.0 + ], + [ + 1045.0, + 624.0 + ] + ], + "center_px": [ + 1068.0, + 622.5 + ], + "area_px": 869.0 + }, + { + "image_points_px": [ + [ + 1157.0, + 599.0 + ], + [ + 1187.0, + 616.0 + ], + [ + 1170.0, + 636.0 + ], + [ + 1141.0, + 618.0 + ] + ], + "center_px": [ + 1163.75, + 617.25 + ], + "area_px": 864.0 + }, + { + "image_points_px": [ + [ + 337.0, + 604.0 + ], + [ + 358.0, + 621.0 + ], + [ + 333.0, + 641.0 + ], + [ + 311.0, + 624.0 + ] + ], + "center_px": [ + 334.75, + 622.5 + ], + "area_px": 863.5 + }, + { + "image_points_px": [ + [ + 385.0, + 601.0 + ], + [ + 407.0, + 618.0 + ], + [ + 382.0, + 638.0 + ], + [ + 360.0, + 621.0 + ] + ], + "center_px": [ + 383.5, + 619.5 + ], + "area_px": 865.0 + }, + { + "image_points_px": [ + [ + 1014.0, + 607.0 + ], + [ + 1042.0, + 623.0 + ], + [ + 1025.0, + 644.0 + ], + [ + 996.0, + 626.0 + ] + ], + "center_px": [ + 1019.25, + 625.0 + ], + "area_px": 867.5 + }, + { + "image_points_px": [ + [ + 966.0, + 609.0 + ], + [ + 994.0, + 626.0 + ], + [ + 977.0, + 646.0 + ], + [ + 948.0, + 629.0 + ] + ], + "center_px": [ + 971.25, + 627.5 + ], + "area_px": 867.5 + }, + { + "image_points_px": [ + [ + 918.0, + 612.0 + ], + [ + 946.0, + 629.0 + ], + [ + 927.0, + 649.0 + ], + [ + 900.0, + 632.0 + ] + ], + "center_px": [ + 922.75, + 630.5 + ], + "area_px": 864.5 + }, + { + "image_points_px": [ + [ + 870.0, + 615.0 + ], + [ + 897.0, + 631.0 + ], + [ + 878.0, + 652.0 + ], + [ + 851.0, + 634.0 + ] + ], + "center_px": [ + 874.0, + 633.0 + ], + "area_px": 863.0 + }, + { + "image_points_px": [ + [ + 976.0, + 649.0 + ], + [ + 1004.0, + 667.0 + ], + [ + 986.0, + 687.0 + ], + [ + 959.0, + 669.0 + ] + ], + "center_px": [ + 981.25, + 668.0 + ], + "area_px": 865.0 + }, + { + "image_points_px": [ + [ + 676.0, + 625.0 + ], + [ + 702.0, + 642.0 + ], + [ + 682.0, + 662.0 + ], + [ + 656.0, + 645.0 + ] + ], + "center_px": [ + 679.0, + 643.5 + ], + "area_px": 860.0 + }, + { + "image_points_px": [ + [ + 625.0, + 589.0 + ], + [ + 650.0, + 606.0 + ], + [ + 628.0, + 625.0 + ], + [ + 603.0, + 608.0 + ] + ], + "center_px": [ + 626.5, + 607.0 + ], + "area_px": 849.0 + }, + { + "image_points_px": [ + [ + 529.0, + 594.0 + ], + [ + 553.0, + 611.0 + ], + [ + 531.0, + 630.0 + ], + [ + 506.0, + 613.0 + ] + ], + "center_px": [ + 529.75, + 612.0 + ], + "area_px": 848.0 + }, + { + "image_points_px": [ + [ + 673.0, + 586.0 + ], + [ + 698.0, + 603.0 + ], + [ + 677.0, + 623.0 + ], + [ + 652.0, + 606.0 + ] + ], + "center_px": [ + 675.0, + 604.5 + ], + "area_px": 857.0 + }, + { + "image_points_px": [ + [ + 927.0, + 652.0 + ], + [ + 954.0, + 668.0 + ], + [ + 937.0, + 689.0 + ], + [ + 909.0, + 671.0 + ] + ], + "center_px": [ + 931.75, + 670.0 + ], + "area_px": 847.5 + }, + { + "image_points_px": [ + [ + 1204.0, + 597.0 + ], + [ + 1234.0, + 613.0 + ], + [ + 1219.0, + 633.0 + ], + [ + 1189.0, + 616.0 + ] + ], + "center_px": [ + 1211.5, + 614.75 + ], + "area_px": 832.5 + }, + { + "image_points_px": [ + [ + 720.0, + 584.0 + ], + [ + 746.0, + 600.0 + ], + [ + 726.0, + 620.0 + ], + [ + 700.0, + 603.0 + ] + ], + "center_px": [ + 723.0, + 601.75 + ], + "area_px": 837.0 + }, + { + "image_points_px": [ + [ + 768.0, + 581.0 + ], + [ + 794.0, + 598.0 + ], + [ + 774.0, + 617.0 + ], + [ + 748.0, + 600.0 + ] + ], + "center_px": [ + 771.0, + 599.0 + ], + "area_px": 834.0 + }, + { + "image_points_px": [ + [ + 1109.0, + 602.0 + ], + [ + 1138.0, + 618.0 + ], + [ + 1122.0, + 638.0 + ], + [ + 1093.0, + 621.0 + ] + ], + "center_px": [ + 1115.5, + 619.75 + ], + "area_px": 829.5 + }, + { + "image_points_px": [ + [ + 576.0, + 592.0 + ], + [ + 601.0, + 608.0 + ], + [ + 579.0, + 628.0 + ], + [ + 555.0, + 611.0 + ] + ], + "center_px": [ + 577.75, + 609.75 + ], + "area_px": 832.5 + }, + { + "image_points_px": [ + [ + 1051.0, + 566.0 + ], + [ + 1079.0, + 583.0 + ], + [ + 1062.0, + 602.0 + ], + [ + 1034.0, + 585.0 + ] + ], + "center_px": [ + 1056.5, + 584.0 + ], + "area_px": 821.0 + }, + { + "image_points_px": [ + [ + 284.0, + 646.0 + ], + [ + 305.0, + 664.0 + ], + [ + 279.0, + 684.0 + ], + [ + 261.0, + 669.0 + ] + ], + "center_px": [ + 282.25, + 665.75 + ], + "area_px": 823.5 + }, + { + "image_points_px": [ + [ + 863.0, + 576.0 + ], + [ + 889.0, + 593.0 + ], + [ + 871.0, + 612.0 + ], + [ + 844.0, + 596.0 + ] + ], + "center_px": [ + 866.75, + 594.25 + ], + "area_px": 822.0 + }, + { + "image_points_px": [ + [ + 910.0, + 574.0 + ], + [ + 937.0, + 590.0 + ], + [ + 918.0, + 610.0 + ], + [ + 892.0, + 593.0 + ] + ], + "center_px": [ + 914.25, + 591.75 + ], + "area_px": 822.0 + }, + { + "image_points_px": [ + [ + 815.0, + 579.0 + ], + [ + 841.0, + 595.0 + ], + [ + 822.0, + 615.0 + ], + [ + 796.0, + 598.0 + ] + ], + "center_px": [ + 818.5, + 596.75 + ], + "area_px": 820.5 + }, + { + "image_points_px": [ + [ + 1097.0, + 564.0 + ], + [ + 1126.0, + 580.0 + ], + [ + 1110.0, + 599.0 + ], + [ + 1081.0, + 583.0 + ] + ], + "center_px": [ + 1103.5, + 581.5 + ], + "area_px": 807.0 + }, + { + "image_points_px": [ + [ + 622.0, + 552.0 + ], + [ + 647.0, + 568.0 + ], + [ + 625.0, + 587.0 + ], + [ + 601.0, + 570.0 + ] + ], + "center_px": [ + 623.75, + 569.25 + ], + "area_px": 808.0 + }, + { + "image_points_px": [ + [ + 1191.0, + 559.0 + ], + [ + 1219.0, + 575.0 + ], + [ + 1204.0, + 595.0 + ], + [ + 1175.0, + 578.0 + ] + ], + "center_px": [ + 1197.25, + 576.75 + ], + "area_px": 811.5 + }, + { + "image_points_px": [ + [ + 387.0, + 564.0 + ], + [ + 408.0, + 581.0 + ], + [ + 384.0, + 599.0 + ], + [ + 362.0, + 583.0 + ] + ], + "center_px": [ + 385.25, + 581.75 + ], + "area_px": 802.0 + }, + { + "image_points_px": [ + [ + 956.0, + 572.0 + ], + [ + 984.0, + 588.0 + ], + [ + 967.0, + 607.0 + ], + [ + 939.0, + 590.0 + ] + ], + "center_px": [ + 961.5, + 589.25 + ], + "area_px": 798.5 + }, + { + "image_points_px": [ + [ + 1144.0, + 561.0 + ], + [ + 1172.0, + 579.0 + ], + [ + 1157.0, + 597.0 + ], + [ + 1128.0, + 580.0 + ] + ], + "center_px": [ + 1150.25, + 579.25 + ], + "area_px": 798.5 + }, + { + "image_points_px": [ + [ + 1004.0, + 569.0 + ], + [ + 1031.0, + 585.0 + ], + [ + 1014.0, + 605.0 + ], + [ + 987.0, + 588.0 + ] + ], + "center_px": [ + 1009.0, + 586.75 + ], + "area_px": 807.0 + }, + { + "image_points_px": [ + [ + 809.0, + 542.0 + ], + [ + 835.0, + 558.0 + ], + [ + 815.0, + 577.0 + ], + [ + 790.0, + 561.0 + ] + ], + "center_px": [ + 812.25, + 559.5 + ], + "area_px": 796.5 + }, + { + "image_points_px": [ + [ + 716.0, + 547.0 + ], + [ + 741.0, + 563.0 + ], + [ + 721.0, + 582.0 + ], + [ + 696.0, + 566.0 + ] + ], + "center_px": [ + 718.5, + 564.5 + ], + "area_px": 795.0 + }, + { + "image_points_px": [ + [ + 1237.0, + 557.0 + ], + [ + 1266.0, + 573.0 + ], + [ + 1251.0, + 592.0 + ], + [ + 1222.0, + 575.0 + ] + ], + "center_px": [ + 1244.0, + 574.25 + ], + "area_px": 784.0 + }, + { + "image_points_px": [ + [ + 575.0, + 554.0 + ], + [ + 599.0, + 570.0 + ], + [ + 578.0, + 589.0 + ], + [ + 554.0, + 573.0 + ] + ], + "center_px": [ + 576.5, + 571.5 + ], + "area_px": 792.0 + }, + { + "image_points_px": [ + [ + 948.0, + 535.0 + ], + [ + 974.0, + 550.0 + ], + [ + 957.0, + 570.0 + ], + [ + 930.0, + 553.0 + ] + ], + "center_px": [ + 952.25, + 552.0 + ], + "area_px": 783.5 + }, + { + "image_points_px": [ + [ + 902.0, + 537.0 + ], + [ + 928.0, + 553.0 + ], + [ + 910.0, + 572.0 + ], + [ + 884.0, + 556.0 + ] + ], + "center_px": [ + 906.0, + 554.5 + ], + "area_px": 782.0 + }, + { + "image_points_px": [ + [ + 762.0, + 545.0 + ], + [ + 788.0, + 561.0 + ], + [ + 768.0, + 579.0 + ], + [ + 743.0, + 563.0 + ] + ], + "center_px": [ + 765.25, + 562.0 + ], + "area_px": 771.0 + }, + { + "image_points_px": [ + [ + 669.0, + 549.0 + ], + [ + 693.0, + 565.0 + ], + [ + 674.0, + 584.0 + ], + [ + 649.0, + 568.0 + ] + ], + "center_px": [ + 671.25, + 566.5 + ], + "area_px": 777.5 + }, + { + "image_points_px": [ + [ + 855.0, + 540.0 + ], + [ + 881.0, + 555.0 + ], + [ + 863.0, + 574.0 + ], + [ + 837.0, + 558.0 + ] + ], + "center_px": [ + 859.0, + 556.75 + ], + "area_px": 760.0 + }, + { + "image_points_px": [ + [ + 711.0, + 512.0 + ], + [ + 736.0, + 527.0 + ], + [ + 716.0, + 545.0 + ], + [ + 691.0, + 529.0 + ] + ], + "center_px": [ + 713.5, + 528.25 + ], + "area_px": 747.5 + }, + { + "image_points_px": [ + [ + 1223.0, + 521.0 + ], + [ + 1251.0, + 536.0 + ], + [ + 1237.0, + 555.0 + ], + [ + 1208.0, + 539.0 + ] + ], + "center_px": [ + 1229.75, + 537.75 + ], + "area_px": 752.0 + }, + { + "image_points_px": [ + [ + 894.0, + 502.0 + ], + [ + 920.0, + 518.0 + ], + [ + 902.0, + 535.0 + ], + [ + 876.0, + 520.0 + ] + ], + "center_px": [ + 898.0, + 518.75 + ], + "area_px": 734.0 + }, + { + "image_points_px": [ + [ + 757.0, + 509.0 + ], + [ + 782.0, + 524.0 + ], + [ + 763.0, + 542.0 + ], + [ + 738.0, + 527.0 + ] + ], + "center_px": [ + 760.0, + 525.5 + ], + "area_px": 735.0 + }, + { + "image_points_px": [ + [ + 848.0, + 505.0 + ], + [ + 874.0, + 520.0 + ], + [ + 855.0, + 538.0 + ], + [ + 830.0, + 522.0 + ] + ], + "center_px": [ + 851.75, + 521.25 + ], + "area_px": 733.0 + }, + { + "image_points_px": [ + [ + 665.0, + 514.0 + ], + [ + 689.0, + 529.0 + ], + [ + 670.0, + 547.0 + ], + [ + 645.0, + 532.0 + ] + ], + "center_px": [ + 667.25, + 530.5 + ], + "area_px": 733.5 + }, + { + "image_points_px": [ + [ + 803.0, + 507.0 + ], + [ + 828.0, + 522.0 + ], + [ + 809.0, + 540.0 + ], + [ + 784.0, + 524.0 + ] + ], + "center_px": [ + 806.0, + 523.25 + ], + "area_px": 732.0 + }, + { + "image_points_px": [ + [ + 1209.0, + 486.0 + ], + [ + 1237.0, + 501.0 + ], + [ + 1222.0, + 519.0 + ], + [ + 1194.0, + 503.0 + ] + ], + "center_px": [ + 1215.5, + 502.25 + ], + "area_px": 722.5 + }, + { + "image_points_px": [ + [ + 436.0, + 526.0 + ], + [ + 456.0, + 541.0 + ], + [ + 433.0, + 559.0 + ], + [ + 412.0, + 543.0 + ] + ], + "center_px": [ + 434.25, + 542.25 + ], + "area_px": 723.0 + }, + { + "image_points_px": [ + [ + 887.0, + 468.0 + ], + [ + 912.0, + 483.0 + ], + [ + 894.0, + 500.0 + ], + [ + 868.0, + 485.0 + ] + ], + "center_px": [ + 890.25, + 484.0 + ], + "area_px": 711.0 + }, + { + "image_points_px": [ + [ + 618.0, + 517.0 + ], + [ + 643.0, + 531.0 + ], + [ + 623.0, + 549.0 + ], + [ + 599.0, + 534.0 + ] + ], + "center_px": [ + 620.75, + 532.75 + ], + "area_px": 711.5 + }, + { + "image_points_px": [ + [ + 120.0, + 473.0 + ], + [ + 138.0, + 488.0 + ], + [ + 112.0, + 505.0 + ], + [ + 94.0, + 490.0 + ] + ], + "center_px": [ + 116.0, + 489.0 + ], + "area_px": 696.0 + }, + { + "image_points_px": [ + [ + 481.0, + 523.0 + ], + [ + 501.0, + 539.0 + ], + [ + 480.0, + 557.0 + ], + [ + 459.0, + 541.0 + ] + ], + "center_px": [ + 480.25, + 540.0 + ], + "area_px": 713.0 + }, + { + "image_points_px": [ + [ + 797.0, + 473.0 + ], + [ + 821.0, + 487.0 + ], + [ + 803.0, + 505.0 + ], + [ + 778.0, + 490.0 + ] + ], + "center_px": [ + 799.75, + 488.75 + ], + "area_px": 697.0 + }, + { + "image_points_px": [ + [ + 707.0, + 477.0 + ], + [ + 731.0, + 492.0 + ], + [ + 712.0, + 509.0 + ], + [ + 688.0, + 495.0 + ] + ], + "center_px": [ + 709.5, + 493.25 + ], + "area_px": 695.5 + }, + { + "image_points_px": [ + [ + 752.0, + 475.0 + ], + [ + 776.0, + 490.0 + ], + [ + 758.0, + 507.0 + ], + [ + 733.0, + 492.0 + ] + ], + "center_px": [ + 754.75, + 491.0 + ], + "area_px": 694.0 + }, + { + "image_points_px": [ + [ + 1239.0, + 451.0 + ], + [ + 1267.0, + 465.0 + ], + [ + 1254.0, + 482.0 + ], + [ + 1225.0, + 467.0 + ] + ], + "center_px": [ + 1246.25, + 466.25 + ], + "area_px": 666.0 + }, + { + "image_points_px": [ + [ + 661.0, + 480.0 + ], + [ + 685.0, + 494.0 + ], + [ + 667.0, + 511.0 + ], + [ + 642.0, + 496.0 + ] + ], + "center_px": [ + 663.75, + 495.25 + ], + "area_px": 672.5 + }, + { + "image_points_px": [ + [ + 81.0, + 442.0 + ], + [ + 99.0, + 456.0 + ], + [ + 73.0, + 473.0 + ], + [ + 56.0, + 458.0 + ] + ], + "center_px": [ + 77.25, + 457.25 + ], + "area_px": 658.5 + }, + { + "image_points_px": [ + [ + 1196.0, + 453.0 + ], + [ + 1223.0, + 467.0 + ], + [ + 1209.0, + 484.0 + ], + [ + 1181.0, + 469.0 + ] + ], + "center_px": [ + 1202.25, + 468.25 + ], + "area_px": 664.0 + }, + { + "image_points_px": [ + [ + 835.0, + 438.0 + ], + [ + 860.0, + 452.0 + ], + [ + 843.0, + 468.0 + ], + [ + 817.0, + 454.0 + ] + ], + "center_px": [ + 838.75, + 453.0 + ], + "area_px": 653.0 + }, + { + "image_points_px": [ + [ + 841.0, + 471.0 + ], + [ + 866.0, + 485.0 + ], + [ + 849.0, + 502.0 + ], + [ + 824.0, + 488.0 + ] + ], + "center_px": [ + 845.0, + 486.5 + ], + "area_px": 663.0 + }, + { + "image_points_px": [ + [ + 747.0, + 442.0 + ], + [ + 771.0, + 456.0 + ], + [ + 752.0, + 473.0 + ], + [ + 729.0, + 459.0 + ] + ], + "center_px": [ + 749.75, + 457.5 + ], + "area_px": 658.5 + }, + { + "image_points_px": [ + [ + 791.0, + 440.0 + ], + [ + 815.0, + 454.0 + ], + [ + 797.0, + 471.0 + ], + [ + 773.0, + 456.0 + ] + ], + "center_px": [ + 794.0, + 455.25 + ], + "area_px": 657.0 + }, + { + "image_points_px": [ + [ + 702.0, + 445.0 + ], + [ + 726.0, + 458.0 + ], + [ + 707.0, + 475.0 + ], + [ + 684.0, + 461.0 + ] + ], + "center_px": [ + 704.75, + 459.75 + ], + "area_px": 637.5 + }, + { + "image_points_px": [ + [ + 1183.0, + 421.0 + ], + [ + 1210.0, + 435.0 + ], + [ + 1196.0, + 451.0 + ], + [ + 1169.0, + 437.0 + ] + ], + "center_px": [ + 1189.5, + 436.0 + ], + "area_px": 628.0 + }, + { + "image_points_px": [ + [ + 1226.0, + 419.0 + ], + [ + 1253.0, + 433.0 + ], + [ + 1239.0, + 449.0 + ], + [ + 1212.0, + 435.0 + ] + ], + "center_px": [ + 1232.5, + 434.0 + ], + "area_px": 628.0 + }, + { + "image_points_px": [ + [ + 742.0, + 411.0 + ], + [ + 766.0, + 424.0 + ], + [ + 748.0, + 440.0 + ], + [ + 724.0, + 426.0 + ] + ], + "center_px": [ + 745.0, + 425.25 + ], + "area_px": 615.0 + }, + { + "image_points_px": [ + [ + 786.0, + 408.0 + ], + [ + 809.0, + 422.0 + ], + [ + 791.0, + 438.0 + ], + [ + 768.0, + 424.0 + ] + ], + "center_px": [ + 788.5, + 423.0 + ], + "area_px": 620.0 + }, + { + "image_points_px": [ + [ + 68.0, + 395.0 + ], + [ + 45.0, + 410.0 + ], + [ + 26.0, + 397.0 + ], + [ + 50.0, + 382.0 + ] + ], + "center_px": [ + 47.25, + 396.0 + ], + "area_px": 583.0 + }, + { + "image_points_px": [ + [ + 1171.0, + 390.0 + ], + [ + 1197.0, + 403.0 + ], + [ + 1183.0, + 419.0 + ], + [ + 1157.0, + 405.0 + ] + ], + "center_px": [ + 1177.0, + 404.25 + ], + "area_px": 592.0 + }, + { + "image_points_px": [ + [ + 1200.0, + 358.0 + ], + [ + 1227.0, + 371.0 + ], + [ + 1213.0, + 386.0 + ], + [ + 1187.0, + 373.0 + ] + ], + "center_px": [ + 1206.75, + 372.0 + ], + "area_px": 573.0 + }, + { + "image_points_px": [ + [ + 1117.0, + 362.0 + ], + [ + 1143.0, + 375.0 + ], + [ + 1128.0, + 390.0 + ], + [ + 1103.0, + 377.0 + ] + ], + "center_px": [ + 1122.75, + 376.0 + ], + "area_px": 571.0 + }, + { + "image_points_px": [ + [ + 1242.0, + 356.0 + ], + [ + 1268.0, + 369.0 + ], + [ + 1255.0, + 384.0 + ], + [ + 1229.0, + 371.0 + ] + ], + "center_px": [ + 1248.5, + 370.0 + ], + "area_px": 559.0 + }, + { + "image_points_px": [ + [ + 1159.0, + 360.0 + ], + [ + 1184.0, + 373.0 + ], + [ + 1171.0, + 388.0 + ], + [ + 1145.0, + 375.0 + ] + ], + "center_px": [ + 1164.75, + 374.0 + ], + "area_px": 558.0 + }, + { + "image_points_px": [ + [ + 1000.0, + 398.0 + ], + [ + 1025.0, + 411.0 + ], + [ + 1010.0, + 427.0 + ], + [ + 987.0, + 415.0 + ] + ], + "center_px": [ + 1005.5, + 412.75 + ], + "area_px": 571.0 + }, + { + "image_points_px": [ + [ + 1229.0, + 327.0 + ], + [ + 1255.0, + 340.0 + ], + [ + 1242.0, + 354.0 + ], + [ + 1216.0, + 342.0 + ] + ], + "center_px": [ + 1235.5, + 340.75 + ], + "area_px": 539.5 + }, + { + "image_points_px": [ + [ + 1188.0, + 329.0 + ], + [ + 1214.0, + 342.0 + ], + [ + 1201.0, + 356.0 + ], + [ + 1175.0, + 343.0 + ] + ], + "center_px": [ + 1194.5, + 342.5 + ], + "area_px": 533.0 + }, + { + "image_points_px": [ + [ + 1028.0, + 412.0 + ], + [ + 1038.0, + 400.0 + ], + [ + 1068.0, + 411.0 + ], + [ + 1053.0, + 425.0 + ] + ], + "center_px": [ + 1046.75, + 412.0 + ], + "area_px": 507.5 + }, + { + "image_points_px": [ + [ + 1106.0, + 333.0 + ], + [ + 1131.0, + 345.0 + ], + [ + 1117.0, + 360.0 + ], + [ + 1092.0, + 347.0 + ] + ], + "center_px": [ + 1111.5, + 346.25 + ], + "area_px": 537.5 + }, + { + "image_points_px": [ + [ + 1065.0, + 335.0 + ], + [ + 1090.0, + 347.0 + ], + [ + 1075.0, + 362.0 + ], + [ + 1051.0, + 349.0 + ] + ], + "center_px": [ + 1070.25, + 348.25 + ], + "area_px": 536.5 + }, + { + "image_points_px": [ + [ + 1015.0, + 308.0 + ], + [ + 1039.0, + 321.0 + ], + [ + 1024.0, + 335.0 + ], + [ + 1000.0, + 322.0 + ] + ], + "center_px": [ + 1019.5, + 321.5 + ], + "area_px": 531.0 + }, + { + "image_points_px": [ + [ + 47.0, + 310.0 + ], + [ + 24.0, + 323.0 + ], + [ + 7.0, + 312.0 + ], + [ + 30.0, + 297.0 + ] + ], + "center_px": [ + 27.0, + 310.5 + ], + "area_px": 514.0 + }, + { + "image_points_px": [ + [ + 1147.0, + 331.0 + ], + [ + 1172.0, + 343.0 + ], + [ + 1159.0, + 358.0 + ], + [ + 1134.0, + 346.0 + ] + ], + "center_px": [ + 1153.0, + 344.5 + ], + "area_px": 531.0 + }, + { + "image_points_px": [ + [ + 1096.0, + 305.0 + ], + [ + 1121.0, + 317.0 + ], + [ + 1106.0, + 331.0 + ], + [ + 1082.0, + 319.0 + ] + ], + "center_px": [ + 1101.25, + 318.0 + ], + "area_px": 517.0 + }, + { + "image_points_px": [ + [ + 1201.0, + 403.0 + ], + [ + 1213.0, + 389.0 + ], + [ + 1239.0, + 402.0 + ], + [ + 1226.0, + 416.0 + ] + ], + "center_px": [ + 1219.75, + 402.5 + ], + "area_px": 519.5 + }, + { + "image_points_px": [ + [ + 1055.0, + 307.0 + ], + [ + 1080.0, + 319.0 + ], + [ + 1065.0, + 333.0 + ], + [ + 1041.0, + 320.0 + ] + ], + "center_px": [ + 1060.25, + 319.75 + ], + "area_px": 512.0 + }, + { + "image_points_px": [ + [ + 443.0, + 646.0 + ], + [ + 455.0, + 656.0 + ], + [ + 431.0, + 676.0 + ], + [ + 419.0, + 667.0 + ] + ], + "center_px": [ + 437.0, + 661.25 + ], + "area_px": 474.0 + }, + { + "image_points_px": [ + [ + 1217.0, + 299.0 + ], + [ + 1242.0, + 311.0 + ], + [ + 1229.0, + 325.0 + ], + [ + 1204.0, + 313.0 + ] + ], + "center_px": [ + 1223.0, + 312.0 + ], + "area_px": 506.0 + }, + { + "image_points_px": [ + [ + 1136.0, + 303.0 + ], + [ + 1161.0, + 315.0 + ], + [ + 1147.0, + 329.0 + ], + [ + 1123.0, + 317.0 + ] + ], + "center_px": [ + 1141.75, + 316.0 + ], + "area_px": 505.0 + }, + { + "image_points_px": [ + [ + 1177.0, + 301.0 + ], + [ + 1201.0, + 313.0 + ], + [ + 1188.0, + 327.0 + ], + [ + 1163.0, + 315.0 + ] + ], + "center_px": [ + 1182.25, + 314.0 + ], + "area_px": 505.0 + }, + { + "image_points_px": [ + [ + 54.0, + 282.0 + ], + [ + 32.0, + 295.0 + ], + [ + 14.0, + 284.0 + ], + [ + 37.0, + 271.0 + ] + ], + "center_px": [ + 34.25, + 283.0 + ], + "area_px": 475.0 + }, + { + "image_points_px": [ + [ + 1165.0, + 274.0 + ], + [ + 1190.0, + 286.0 + ], + [ + 1177.0, + 299.0 + ], + [ + 1152.0, + 287.0 + ] + ], + "center_px": [ + 1171.0, + 286.5 + ], + "area_px": 481.0 + }, + { + "image_points_px": [ + [ + 1032.0, + 293.0 + ], + [ + 1045.0, + 280.0 + ], + [ + 1070.0, + 291.0 + ], + [ + 1055.0, + 305.0 + ] + ], + "center_px": [ + 1050.5, + 292.25 + ], + "area_px": 485.0 + }, + { + "image_points_px": [ + [ + 1006.0, + 281.0 + ], + [ + 1030.0, + 293.0 + ], + [ + 1016.0, + 306.0 + ], + [ + 992.0, + 295.0 + ] + ], + "center_px": [ + 1011.0, + 293.75 + ], + "area_px": 485.0 + }, + { + "image_points_px": [ + [ + 1112.0, + 289.0 + ], + [ + 1126.0, + 276.0 + ], + [ + 1150.0, + 288.0 + ], + [ + 1137.0, + 301.0 + ] + ], + "center_px": [ + 1131.25, + 288.5 + ], + "area_px": 480.5 + }, + { + "image_points_px": [ + [ + 1072.0, + 291.0 + ], + [ + 1085.0, + 278.0 + ], + [ + 1110.0, + 290.0 + ], + [ + 1096.0, + 303.0 + ] + ], + "center_px": [ + 1090.75, + 290.5 + ], + "area_px": 480.5 + }, + { + "image_points_px": [ + [ + 951.0, + 296.0 + ], + [ + 967.0, + 283.0 + ], + [ + 989.0, + 295.0 + ], + [ + 975.0, + 308.0 + ] + ], + "center_px": [ + 970.5, + 295.5 + ], + "area_px": 479.0 + }, + { + "image_points_px": [ + [ + 1205.0, + 272.0 + ], + [ + 1229.0, + 284.0 + ], + [ + 1217.0, + 297.0 + ], + [ + 1192.0, + 286.0 + ] + ], + "center_px": [ + 1210.75, + 284.75 + ], + "area_px": 474.5 + }, + { + "image_points_px": [ + [ + 983.0, + 268.0 + ], + [ + 998.0, + 255.0 + ], + [ + 1021.0, + 266.0 + ], + [ + 1007.0, + 279.0 + ] + ], + "center_px": [ + 1002.25, + 267.0 + ], + "area_px": 465.0 + }, + { + "image_points_px": [ + [ + 60.0, + 255.0 + ], + [ + 37.0, + 269.0 + ], + [ + 22.0, + 257.0 + ], + [ + 44.0, + 245.0 + ] + ], + "center_px": [ + 40.75, + 256.5 + ], + "area_px": 449.0 + }, + { + "image_points_px": [ + [ + 1232.0, + 284.0 + ], + [ + 1244.0, + 271.0 + ], + [ + 1269.0, + 282.0 + ], + [ + 1257.0, + 295.0 + ] + ], + "center_px": [ + 1250.5, + 283.0 + ], + "area_px": 457.0 + }, + { + "image_points_px": [ + [ + 1062.0, + 264.0 + ], + [ + 1076.0, + 252.0 + ], + [ + 1100.0, + 263.0 + ], + [ + 1087.0, + 275.0 + ] + ], + "center_px": [ + 1081.25, + 263.5 + ], + "area_px": 442.5 + }, + { + "image_points_px": [ + [ + 919.0, + 258.0 + ], + [ + 941.0, + 270.0 + ], + [ + 926.0, + 283.0 + ], + [ + 904.0, + 271.0 + ] + ], + "center_px": [ + 922.5, + 270.5 + ], + "area_px": 466.0 + }, + { + "image_points_px": [ + [ + 504.0, + 613.0 + ], + [ + 481.0, + 633.0 + ], + [ + 469.0, + 624.0 + ], + [ + 491.0, + 605.0 + ] + ], + "center_px": [ + 486.25, + 618.75 + ], + "area_px": 435.0 + }, + { + "image_points_px": [ + [ + 1141.0, + 261.0 + ], + [ + 1154.0, + 248.0 + ], + [ + 1178.0, + 259.0 + ], + [ + 1165.0, + 272.0 + ] + ], + "center_px": [ + 1159.5, + 260.0 + ], + "area_px": 455.0 + }, + { + "image_points_px": [ + [ + 1023.0, + 266.0 + ], + [ + 1037.0, + 253.0 + ], + [ + 1060.0, + 264.0 + ], + [ + 1047.0, + 277.0 + ] + ], + "center_px": [ + 1041.75, + 265.0 + ], + "area_px": 454.0 + }, + { + "image_points_px": [ + [ + 1169.0, + 233.0 + ], + [ + 1182.0, + 221.0 + ], + [ + 1206.0, + 232.0 + ], + [ + 1193.0, + 245.0 + ] + ], + "center_px": [ + 1187.5, + 232.75 + ], + "area_px": 449.5 + }, + { + "image_points_px": [ + [ + 1102.0, + 262.0 + ], + [ + 1115.0, + 250.0 + ], + [ + 1139.0, + 261.0 + ], + [ + 1126.0, + 274.0 + ] + ], + "center_px": [ + 1120.5, + 261.75 + ], + "area_px": 449.5 + }, + { + "image_points_px": [ + [ + 944.0, + 269.0 + ], + [ + 958.0, + 257.0 + ], + [ + 981.0, + 268.0 + ], + [ + 967.0, + 281.0 + ] + ], + "center_px": [ + 962.5, + 268.75 + ], + "area_px": 448.5 + }, + { + "image_points_px": [ + [ + 1053.0, + 238.0 + ], + [ + 1067.0, + 226.0 + ], + [ + 1090.0, + 237.0 + ], + [ + 1076.0, + 250.0 + ] + ], + "center_px": [ + 1071.5, + 237.75 + ], + "area_px": 448.5 + }, + { + "image_points_px": [ + [ + 1180.0, + 259.0 + ], + [ + 1192.0, + 247.0 + ], + [ + 1217.0, + 257.0 + ], + [ + 1205.0, + 270.0 + ] + ], + "center_px": [ + 1198.5, + 258.25 + ], + "area_px": 438.5 + }, + { + "image_points_px": [ + [ + 936.0, + 243.0 + ], + [ + 951.0, + 231.0 + ], + [ + 973.0, + 242.0 + ], + [ + 958.0, + 255.0 + ] + ], + "center_px": [ + 954.5, + 242.75 + ], + "area_px": 447.5 + }, + { + "image_points_px": [ + [ + 975.0, + 242.0 + ], + [ + 988.0, + 230.0 + ], + [ + 1012.0, + 240.0 + ], + [ + 998.0, + 253.0 + ] + ], + "center_px": [ + 993.25, + 241.25 + ], + "area_px": 435.5 + }, + { + "image_points_px": [ + [ + 1014.0, + 240.0 + ], + [ + 1028.0, + 228.0 + ], + [ + 1051.0, + 239.0 + ], + [ + 1037.0, + 251.0 + ] + ], + "center_px": [ + 1032.5, + 239.5 + ], + "area_px": 430.0 + }, + { + "image_points_px": [ + [ + 67.0, + 230.0 + ], + [ + 46.0, + 242.0 + ], + [ + 29.0, + 232.0 + ], + [ + 49.0, + 220.0 + ] + ], + "center_px": [ + 47.75, + 231.0 + ], + "area_px": 415.0 + }, + { + "image_points_px": [ + [ + 1105.0, + 224.0 + ], + [ + 1128.0, + 235.0 + ], + [ + 1115.0, + 248.0 + ], + [ + 1092.0, + 237.0 + ] + ], + "center_px": [ + 1110.0, + 236.0 + ], + "area_px": 442.0 + }, + { + "image_points_px": [ + [ + 58.0, + 352.0 + ], + [ + 66.0, + 370.0 + ], + [ + 51.0, + 380.0 + ], + [ + 34.0, + 367.0 + ] + ], + "center_px": [ + 52.25, + 367.25 + ], + "area_px": 458.5 + }, + { + "image_points_px": [ + [ + 1208.0, + 232.0 + ], + [ + 1219.0, + 220.0 + ], + [ + 1244.0, + 230.0 + ], + [ + 1232.0, + 243.0 + ] + ], + "center_px": [ + 1225.75, + 231.25 + ], + "area_px": 427.0 + }, + { + "image_points_px": [ + [ + 340.0, + 243.0 + ], + [ + 321.0, + 256.0 + ], + [ + 303.0, + 246.0 + ], + [ + 322.0, + 233.0 + ] + ], + "center_px": [ + 321.5, + 244.5 + ], + "area_px": 424.0 + }, + { + "image_points_px": [ + [ + 326.0, + 208.0 + ], + [ + 342.0, + 219.0 + ], + [ + 323.0, + 231.0 + ], + [ + 305.0, + 220.0 + ] + ], + "center_px": [ + 324.0, + 219.5 + ], + "area_px": 424.0 + }, + { + "image_points_px": [ + [ + 106.0, + 228.0 + ], + [ + 85.0, + 241.0 + ], + [ + 69.0, + 230.0 + ], + [ + 90.0, + 218.0 + ] + ], + "center_px": [ + 87.5, + 229.25 + ], + "area_px": 420.5 + }, + { + "image_points_px": [ + [ + 146.0, + 227.0 + ], + [ + 125.0, + 239.0 + ], + [ + 109.0, + 229.0 + ], + [ + 130.0, + 216.0 + ] + ], + "center_px": [ + 127.5, + 227.75 + ], + "area_px": 420.5 + }, + { + "image_points_px": [ + [ + 264.0, + 222.0 + ], + [ + 245.0, + 234.0 + ], + [ + 227.0, + 224.0 + ], + [ + 247.0, + 211.0 + ] + ], + "center_px": [ + 245.75, + 222.75 + ], + "area_px": 423.5 + }, + { + "image_points_px": [ + [ + 224.0, + 223.0 + ], + [ + 204.0, + 236.0 + ], + [ + 187.0, + 225.0 + ], + [ + 208.0, + 213.0 + ] + ], + "center_px": [ + 205.75, + 224.25 + ], + "area_px": 421.5 + }, + { + "image_points_px": [ + [ + 551.0, + 572.0 + ], + [ + 529.0, + 592.0 + ], + [ + 517.0, + 583.0 + ], + [ + 538.0, + 565.0 + ] + ], + "center_px": [ + 533.75, + 578.0 + ], + "area_px": 409.5 + }, + { + "image_points_px": [ + [ + 306.0, + 196.0 + ], + [ + 286.0, + 208.0 + ], + [ + 269.0, + 197.0 + ], + [ + 289.0, + 185.0 + ] + ], + "center_px": [ + 287.5, + 196.5 + ], + "area_px": 424.0 + }, + { + "image_points_px": [ + [ + 480.0, + 201.0 + ], + [ + 498.0, + 212.0 + ], + [ + 479.0, + 224.0 + ], + [ + 461.0, + 213.0 + ] + ], + "center_px": [ + 479.5, + 212.5 + ], + "area_px": 425.0 + }, + { + "image_points_px": [ + [ + 1131.0, + 235.0 + ], + [ + 1143.0, + 223.0 + ], + [ + 1167.0, + 233.0 + ], + [ + 1155.0, + 246.0 + ] + ], + "center_px": [ + 1149.0, + 234.25 + ], + "area_px": 426.0 + }, + { + "image_points_px": [ + [ + 1196.0, + 207.0 + ], + [ + 1208.0, + 195.0 + ], + [ + 1232.0, + 205.0 + ], + [ + 1220.0, + 218.0 + ] + ], + "center_px": [ + 1214.0, + 206.25 + ], + "area_px": 426.0 + }, + { + "image_points_px": [ + [ + 967.0, + 217.0 + ], + [ + 981.0, + 205.0 + ], + [ + 1004.0, + 215.0 + ], + [ + 990.0, + 227.0 + ] + ], + "center_px": [ + 985.5, + 216.0 + ], + "area_px": 416.0 + }, + { + "image_points_px": [ + [ + 1234.0, + 205.0 + ], + [ + 1246.0, + 193.0 + ], + [ + 1270.0, + 204.0 + ], + [ + 1258.0, + 216.0 + ] + ], + "center_px": [ + 1252.0, + 204.5 + ], + "area_px": 420.0 + }, + { + "image_points_px": [ + [ + 1120.0, + 210.0 + ], + [ + 1133.0, + 198.0 + ], + [ + 1156.0, + 209.0 + ], + [ + 1144.0, + 221.0 + ] + ], + "center_px": [ + 1138.25, + 209.5 + ], + "area_px": 419.5 + }, + { + "image_points_px": [ + [ + 1006.0, + 215.0 + ], + [ + 1019.0, + 203.0 + ], + [ + 1042.0, + 214.0 + ], + [ + 1028.0, + 226.0 + ] + ], + "center_px": [ + 1023.75, + 214.5 + ], + "area_px": 418.5 + }, + { + "image_points_px": [ + [ + 303.0, + 220.0 + ], + [ + 284.0, + 232.0 + ], + [ + 266.0, + 222.0 + ], + [ + 285.0, + 210.0 + ] + ], + "center_px": [ + 284.5, + 221.0 + ], + "area_px": 406.0 + }, + { + "image_points_px": [ + [ + 229.0, + 199.0 + ], + [ + 208.0, + 211.0 + ], + [ + 192.0, + 201.0 + ], + [ + 211.0, + 189.0 + ] + ], + "center_px": [ + 210.0, + 200.0 + ], + "area_px": 404.0 + }, + { + "image_points_px": [ + [ + 190.0, + 200.0 + ], + [ + 171.0, + 212.0 + ], + [ + 153.0, + 202.0 + ], + [ + 173.0, + 190.0 + ] + ], + "center_px": [ + 171.75, + 201.0 + ], + "area_px": 405.0 + }, + { + "image_points_px": [ + [ + 73.0, + 205.0 + ], + [ + 53.0, + 217.0 + ], + [ + 36.0, + 207.0 + ], + [ + 57.0, + 195.0 + ] + ], + "center_px": [ + 54.75, + 206.0 + ], + "area_px": 403.0 + }, + { + "image_points_px": [ + [ + 151.0, + 202.0 + ], + [ + 130.0, + 214.0 + ], + [ + 114.0, + 204.0 + ], + [ + 134.0, + 192.0 + ] + ], + "center_px": [ + 132.25, + 203.0 + ], + "area_px": 403.0 + }, + { + "image_points_px": [ + [ + 1035.0, + 189.0 + ], + [ + 1049.0, + 177.0 + ], + [ + 1071.0, + 188.0 + ], + [ + 1057.0, + 200.0 + ] + ], + "center_px": [ + 1053.0, + 188.5 + ], + "area_px": 418.0 + }, + { + "image_points_px": [ + [ + 1082.0, + 212.0 + ], + [ + 1095.0, + 200.0 + ], + [ + 1118.0, + 210.0 + ], + [ + 1106.0, + 222.0 + ] + ], + "center_px": [ + 1100.25, + 211.0 + ], + "area_px": 407.0 + }, + { + "image_points_px": [ + [ + 962.0, + 322.0 + ], + [ + 976.0, + 311.0 + ], + [ + 998.0, + 323.0 + ], + [ + 984.0, + 334.0 + ] + ], + "center_px": [ + 980.0, + 322.5 + ], + "area_px": 410.0 + }, + { + "image_points_px": [ + [ + 1158.0, + 208.0 + ], + [ + 1170.0, + 197.0 + ], + [ + 1194.0, + 207.0 + ], + [ + 1182.0, + 219.0 + ] + ], + "center_px": [ + 1176.0, + 207.75 + ], + "area_px": 402.0 + }, + { + "image_points_px": [ + [ + 1044.0, + 213.0 + ], + [ + 1057.0, + 202.0 + ], + [ + 1080.0, + 212.0 + ], + [ + 1067.0, + 224.0 + ] + ], + "center_px": [ + 1062.0, + 212.75 + ], + "area_px": 401.0 + }, + { + "image_points_px": [ + [ + 1110.0, + 186.0 + ], + [ + 1123.0, + 174.0 + ], + [ + 1146.0, + 185.0 + ], + [ + 1133.0, + 196.0 + ] + ], + "center_px": [ + 1128.0, + 185.25 + ], + "area_px": 401.0 + }, + { + "image_points_px": [ + [ + 112.0, + 203.0 + ], + [ + 93.0, + 215.0 + ], + [ + 75.0, + 205.0 + ], + [ + 95.0, + 194.0 + ] + ], + "center_px": [ + 93.75, + 204.25 + ], + "area_px": 386.5 + }, + { + "image_points_px": [ + [ + 441.0, + 179.0 + ], + [ + 459.0, + 189.0 + ], + [ + 442.0, + 201.0 + ], + [ + 423.0, + 191.0 + ] + ], + "center_px": [ + 441.25, + 190.0 + ], + "area_px": 397.0 + }, + { + "image_points_px": [ + [ + 517.0, + 176.0 + ], + [ + 536.0, + 186.0 + ], + [ + 518.0, + 198.0 + ], + [ + 500.0, + 188.0 + ] + ], + "center_px": [ + 517.75, + 187.0 + ], + "area_px": 397.0 + }, + { + "image_points_px": [ + [ + 116.0, + 446.0 + ], + [ + 135.0, + 461.0 + ], + [ + 119.0, + 471.0 + ], + [ + 101.0, + 457.0 + ] + ], + "center_px": [ + 117.75, + 458.75 + ], + "area_px": 419.0 + }, + { + "image_points_px": [ + [ + 421.0, + 191.0 + ], + [ + 402.0, + 203.0 + ], + [ + 385.0, + 193.0 + ], + [ + 402.0, + 181.0 + ] + ], + "center_px": [ + 402.5, + 192.0 + ], + "area_px": 396.0 + }, + { + "image_points_px": [ + [ + 156.0, + 178.0 + ], + [ + 135.0, + 190.0 + ], + [ + 120.0, + 180.0 + ], + [ + 140.0, + 168.0 + ] + ], + "center_px": [ + 137.75, + 179.0 + ], + "area_px": 391.0 + }, + { + "image_points_px": [ + [ + 267.0, + 197.0 + ], + [ + 248.0, + 209.0 + ], + [ + 231.0, + 199.0 + ], + [ + 250.0, + 187.0 + ] + ], + "center_px": [ + 249.0, + 198.0 + ], + "area_px": 394.0 + }, + { + "image_points_px": [ + [ + 422.0, + 167.0 + ], + [ + 403.0, + 179.0 + ], + [ + 386.0, + 169.0 + ], + [ + 405.0, + 157.0 + ] + ], + "center_px": [ + 404.0, + 168.0 + ], + "area_px": 394.0 + }, + { + "image_points_px": [ + [ + 596.0, + 534.0 + ], + [ + 576.0, + 552.0 + ], + [ + 563.0, + 544.0 + ], + [ + 584.0, + 526.0 + ] + ], + "center_px": [ + 579.75, + 539.0 + ], + "area_px": 389.0 + }, + { + "image_points_px": [ + [ + 1185.0, + 183.0 + ], + [ + 1198.0, + 171.0 + ], + [ + 1220.0, + 181.0 + ], + [ + 1209.0, + 193.0 + ] + ], + "center_px": [ + 1203.0, + 182.0 + ], + "area_px": 396.0 + }, + { + "image_points_px": [ + [ + 1052.0, + 537.0 + ], + [ + 1066.0, + 547.0 + ], + [ + 1050.0, + 564.0 + ], + [ + 1034.0, + 553.0 + ] + ], + "center_px": [ + 1050.5, + 550.25 + ], + "area_px": 426.0 + }, + { + "image_points_px": [ + [ + 284.0, + 235.0 + ], + [ + 299.0, + 246.0 + ], + [ + 280.0, + 258.0 + ], + [ + 264.0, + 247.0 + ] + ], + "center_px": [ + 281.75, + 246.5 + ], + "area_px": 400.5 + }, + { + "image_points_px": [ + [ + 99.0, + 254.0 + ], + [ + 80.0, + 266.0 + ], + [ + 64.0, + 256.0 + ], + [ + 84.0, + 243.0 + ] + ], + "center_px": [ + 81.75, + 254.75 + ], + "area_px": 398.5 + }, + { + "image_points_px": [ + [ + 998.0, + 191.0 + ], + [ + 1011.0, + 179.0 + ], + [ + 1033.0, + 189.0 + ], + [ + 1020.0, + 201.0 + ] + ], + "center_px": [ + 1015.5, + 190.0 + ], + "area_px": 394.0 + }, + { + "image_points_px": [ + [ + 1073.0, + 188.0 + ], + [ + 1086.0, + 176.0 + ], + [ + 1108.0, + 186.0 + ], + [ + 1095.0, + 198.0 + ] + ], + "center_px": [ + 1090.5, + 187.0 + ], + "area_px": 394.0 + }, + { + "image_points_px": [ + [ + 497.0, + 187.0 + ], + [ + 481.0, + 199.0 + ], + [ + 461.0, + 189.0 + ], + [ + 479.0, + 178.0 + ] + ], + "center_px": [ + 479.5, + 188.25 + ], + "area_px": 380.0 + }, + { + "image_points_px": [ + [ + 346.0, + 170.0 + ], + [ + 328.0, + 182.0 + ], + [ + 310.0, + 172.0 + ], + [ + 328.0, + 161.0 + ] + ], + "center_px": [ + 328.0, + 171.25 + ], + "area_px": 378.0 + }, + { + "image_points_px": [ + [ + 499.0, + 164.0 + ], + [ + 516.0, + 153.0 + ], + [ + 535.0, + 163.0 + ], + [ + 518.0, + 174.0 + ] + ], + "center_px": [ + 517.0, + 163.5 + ], + "area_px": 379.0 + }, + { + "image_points_px": [ + [ + 232.0, + 175.0 + ], + [ + 214.0, + 186.0 + ], + [ + 196.0, + 177.0 + ], + [ + 216.0, + 165.0 + ] + ], + "center_px": [ + 214.5, + 175.75 + ], + "area_px": 376.0 + }, + { + "image_points_px": [ + [ + 123.0, + 156.0 + ], + [ + 102.0, + 168.0 + ], + [ + 87.0, + 158.0 + ], + [ + 106.0, + 147.0 + ] + ], + "center_px": [ + 104.5, + 157.25 + ], + "area_px": 374.0 + }, + { + "image_points_px": [ + [ + 47.0, + 159.0 + ], + [ + 26.0, + 171.0 + ], + [ + 11.0, + 161.0 + ], + [ + 31.0, + 150.0 + ] + ], + "center_px": [ + 28.75, + 160.25 + ], + "area_px": 373.0 + }, + { + "image_points_px": [ + [ + 270.0, + 173.0 + ], + [ + 251.0, + 185.0 + ], + [ + 234.0, + 175.0 + ], + [ + 253.0, + 164.0 + ] + ], + "center_px": [ + 252.0, + 174.25 + ], + "area_px": 376.0 + }, + { + "image_points_px": [ + [ + 194.0, + 176.0 + ], + [ + 174.0, + 188.0 + ], + [ + 158.0, + 178.0 + ], + [ + 177.0, + 167.0 + ] + ], + "center_px": [ + 175.75, + 177.25 + ], + "area_px": 375.0 + }, + { + "image_points_px": [ + [ + 117.0, + 179.0 + ], + [ + 98.0, + 191.0 + ], + [ + 81.0, + 181.0 + ], + [ + 101.0, + 170.0 + ] + ], + "center_px": [ + 99.25, + 180.25 + ], + "area_px": 375.0 + }, + { + "image_points_px": [ + [ + 236.0, + 152.0 + ], + [ + 217.0, + 163.0 + ], + [ + 200.0, + 153.0 + ], + [ + 220.0, + 142.0 + ] + ], + "center_px": [ + 218.25, + 152.5 + ], + "area_px": 376.5 + }, + { + "image_points_px": [ + [ + 386.0, + 146.0 + ], + [ + 367.0, + 157.0 + ], + [ + 350.0, + 147.0 + ], + [ + 369.0, + 136.0 + ] + ], + "center_px": [ + 368.0, + 146.5 + ], + "area_px": 377.0 + }, + { + "image_points_px": [ + [ + 640.0, + 496.0 + ], + [ + 620.0, + 514.0 + ], + [ + 607.0, + 506.0 + ], + [ + 628.0, + 489.0 + ] + ], + "center_px": [ + 623.75, + 501.25 + ], + "area_px": 372.5 + }, + { + "image_points_px": [ + [ + 80.0, + 415.0 + ], + [ + 96.0, + 430.0 + ], + [ + 81.0, + 440.0 + ], + [ + 63.0, + 427.0 + ] + ], + "center_px": [ + 80.0, + 428.0 + ], + "area_px": 411.0 + }, + { + "image_points_px": [ + [ + 1148.0, + 184.0 + ], + [ + 1160.0, + 173.0 + ], + [ + 1183.0, + 183.0 + ], + [ + 1172.0, + 194.0 + ] + ], + "center_px": [ + 1165.75, + 183.5 + ], + "area_px": 373.5 + }, + { + "image_points_px": [ + [ + 1223.0, + 181.0 + ], + [ + 1234.0, + 170.0 + ], + [ + 1258.0, + 180.0 + ], + [ + 1246.0, + 191.0 + ] + ], + "center_px": [ + 1240.25, + 180.5 + ], + "area_px": 373.5 + }, + { + "image_points_px": [ + [ + 1101.0, + 163.0 + ], + [ + 1114.0, + 151.0 + ], + [ + 1136.0, + 161.0 + ], + [ + 1124.0, + 172.0 + ] + ], + "center_px": [ + 1118.75, + 161.75 + ], + "area_px": 377.5 + }, + { + "image_points_px": [ + [ + 1211.0, + 158.0 + ], + [ + 1223.0, + 147.0 + ], + [ + 1246.0, + 157.0 + ], + [ + 1234.0, + 168.0 + ] + ], + "center_px": [ + 1228.5, + 157.5 + ], + "area_px": 373.0 + }, + { + "image_points_px": [ + [ + 1027.0, + 166.0 + ], + [ + 1040.0, + 154.0 + ], + [ + 1062.0, + 164.0 + ], + [ + 1049.0, + 175.0 + ] + ], + "center_px": [ + 1044.5, + 164.75 + ], + "area_px": 376.5 + }, + { + "image_points_px": [ + [ + 1064.0, + 164.0 + ], + [ + 1076.0, + 153.0 + ], + [ + 1099.0, + 163.0 + ], + [ + 1086.0, + 174.0 + ] + ], + "center_px": [ + 1081.25, + 163.5 + ], + "area_px": 372.5 + }, + { + "image_points_px": [ + [ + 383.0, + 266.0 + ], + [ + 401.0, + 255.0 + ], + [ + 419.0, + 266.0 + ], + [ + 404.0, + 275.0 + ] + ], + "center_px": [ + 401.75, + 265.5 + ], + "area_px": 360.0 + }, + { + "image_points_px": [ + [ + 54.0, + 137.0 + ], + [ + 32.0, + 148.0 + ], + [ + 18.0, + 138.0 + ], + [ + 38.0, + 128.0 + ] + ], + "center_px": [ + 35.5, + 137.75 + ], + "area_px": 357.0 + }, + { + "image_points_px": [ + [ + 85.0, + 158.0 + ], + [ + 65.0, + 169.0 + ], + [ + 49.0, + 159.0 + ], + [ + 69.0, + 149.0 + ] + ], + "center_px": [ + 67.0, + 158.75 + ], + "area_px": 358.0 + }, + { + "image_points_px": [ + [ + 311.0, + 149.0 + ], + [ + 292.0, + 160.0 + ], + [ + 275.0, + 150.0 + ], + [ + 293.0, + 140.0 + ] + ], + "center_px": [ + 292.75, + 149.75 + ], + "area_px": 359.5 + }, + { + "image_points_px": [ + [ + 1055.0, + 141.0 + ], + [ + 1068.0, + 130.0 + ], + [ + 1090.0, + 140.0 + ], + [ + 1077.0, + 151.0 + ] + ], + "center_px": [ + 1072.5, + 140.5 + ], + "area_px": 372.0 + }, + { + "image_points_px": [ + [ + 1144.0, + 424.0 + ], + [ + 1167.0, + 437.0 + ], + [ + 1152.0, + 453.0 + ], + [ + 1144.0, + 449.0 + ] + ], + "center_px": [ + 1151.75, + 440.75 + ], + "area_px": 381.5 + }, + { + "image_points_px": [ + [ + 478.0, + 155.0 + ], + [ + 497.0, + 164.0 + ], + [ + 480.0, + 176.0 + ], + [ + 462.0, + 166.0 + ] + ], + "center_px": [ + 479.25, + 165.25 + ], + "area_px": 369.5 + }, + { + "image_points_px": [ + [ + 384.0, + 168.0 + ], + [ + 366.0, + 180.0 + ], + [ + 349.0, + 171.0 + ], + [ + 367.0, + 159.0 + ] + ], + "center_px": [ + 366.5, + 169.5 + ], + "area_px": 366.0 + }, + { + "image_points_px": [ + [ + 573.0, + 138.0 + ], + [ + 589.0, + 127.0 + ], + [ + 608.0, + 137.0 + ], + [ + 591.0, + 148.0 + ] + ], + "center_px": [ + 590.25, + 137.5 + ], + "area_px": 368.5 + }, + { + "image_points_px": [ + [ + 537.0, + 162.0 + ], + [ + 554.0, + 151.0 + ], + [ + 572.0, + 161.0 + ], + [ + 556.0, + 172.0 + ] + ], + "center_px": [ + 554.75, + 161.5 + ], + "area_px": 368.5 + }, + { + "image_points_px": [ + [ + 308.0, + 171.0 + ], + [ + 290.0, + 183.0 + ], + [ + 273.0, + 174.0 + ], + [ + 292.0, + 162.0 + ] + ], + "center_px": [ + 290.75, + 172.5 + ], + "area_px": 364.5 + }, + { + "image_points_px": [ + [ + 516.0, + 130.0 + ], + [ + 534.0, + 140.0 + ], + [ + 517.0, + 151.0 + ], + [ + 499.0, + 141.0 + ] + ], + "center_px": [ + 516.5, + 140.5 + ], + "area_px": 368.0 + }, + { + "image_points_px": [ + [ + 273.0, + 150.0 + ], + [ + 254.0, + 162.0 + ], + [ + 238.0, + 152.0 + ], + [ + 256.0, + 141.0 + ] + ], + "center_px": [ + 255.25, + 151.25 + ], + "area_px": 365.5 + }, + { + "image_points_px": [ + [ + 443.0, + 156.0 + ], + [ + 459.0, + 165.0 + ], + [ + 441.0, + 177.0 + ], + [ + 424.0, + 167.0 + ] + ], + "center_px": [ + 441.75, + 166.25 + ], + "area_px": 365.5 + }, + { + "image_points_px": [ + [ + 460.0, + 143.0 + ], + [ + 442.0, + 154.0 + ], + [ + 425.0, + 144.0 + ], + [ + 443.0, + 133.0 + ] + ], + "center_px": [ + 442.5, + 143.5 + ], + "area_px": 367.0 + }, + { + "image_points_px": [ + [ + 1175.0, + 160.0 + ], + [ + 1187.0, + 148.0 + ], + [ + 1209.0, + 158.0 + ], + [ + 1198.0, + 169.0 + ] + ], + "center_px": [ + 1192.25, + 158.75 + ], + "area_px": 368.0 + }, + { + "image_points_px": [ + [ + 682.0, + 461.0 + ], + [ + 662.0, + 478.0 + ], + [ + 650.0, + 470.0 + ], + [ + 670.0, + 453.0 + ] + ], + "center_px": [ + 666.0, + 465.5 + ], + "area_px": 364.0 + }, + { + "image_points_px": [ + [ + 1138.0, + 161.0 + ], + [ + 1150.0, + 150.0 + ], + [ + 1172.0, + 159.0 + ], + [ + 1160.0, + 171.0 + ] + ], + "center_px": [ + 1155.0, + 160.25 + ], + "area_px": 367.0 + }, + { + "image_points_px": [ + [ + 1201.0, + 135.0 + ], + [ + 1212.0, + 124.0 + ], + [ + 1235.0, + 134.0 + ], + [ + 1223.0, + 145.0 + ] + ], + "center_px": [ + 1217.75, + 134.5 + ], + "area_px": 362.5 + }, + { + "image_points_px": [ + [ + 365.0, + 207.0 + ], + [ + 380.0, + 217.0 + ], + [ + 362.0, + 229.0 + ], + [ + 346.0, + 219.0 + ] + ], + "center_px": [ + 363.25, + 218.0 + ], + "area_px": 371.0 + }, + { + "image_points_px": [ + [ + 497.0, + 141.0 + ], + [ + 481.0, + 152.0 + ], + [ + 462.0, + 143.0 + ], + [ + 479.0, + 132.0 + ] + ], + "center_px": [ + 479.75, + 142.0 + ], + "area_px": 352.0 + }, + { + "image_points_px": [ + [ + 571.0, + 138.0 + ], + [ + 555.0, + 149.0 + ], + [ + 536.0, + 140.0 + ], + [ + 553.0, + 129.0 + ] + ], + "center_px": [ + 553.75, + 139.0 + ], + "area_px": 352.0 + }, + { + "image_points_px": [ + [ + 423.0, + 144.0 + ], + [ + 406.0, + 155.0 + ], + [ + 388.0, + 146.0 + ], + [ + 405.0, + 135.0 + ] + ], + "center_px": [ + 405.5, + 145.0 + ], + "area_px": 351.0 + }, + { + "image_points_px": [ + [ + 497.0, + 119.0 + ], + [ + 480.0, + 130.0 + ], + [ + 462.0, + 121.0 + ], + [ + 479.0, + 110.0 + ] + ], + "center_px": [ + 479.5, + 120.0 + ], + "area_px": 351.0 + }, + { + "image_points_px": [ + [ + 348.0, + 147.0 + ], + [ + 331.0, + 158.0 + ], + [ + 313.0, + 149.0 + ], + [ + 331.0, + 138.0 + ] + ], + "center_px": [ + 330.75, + 148.0 + ], + "area_px": 350.0 + }, + { + "image_points_px": [ + [ + 202.0, + 130.0 + ], + [ + 183.0, + 142.0 + ], + [ + 167.0, + 132.0 + ], + [ + 186.0, + 122.0 + ] + ], + "center_px": [ + 184.5, + 131.5 + ], + "area_px": 347.0 + }, + { + "image_points_px": [ + [ + 387.0, + 123.0 + ], + [ + 369.0, + 134.0 + ], + [ + 352.0, + 125.0 + ], + [ + 370.0, + 114.0 + ] + ], + "center_px": [ + 369.5, + 124.0 + ], + "area_px": 349.0 + }, + { + "image_points_px": [ + [ + 239.0, + 129.0 + ], + [ + 221.0, + 140.0 + ], + [ + 204.0, + 131.0 + ], + [ + 224.0, + 120.0 + ] + ], + "center_px": [ + 222.0, + 130.0 + ], + "area_px": 347.0 + }, + { + "image_points_px": [ + [ + 276.0, + 128.0 + ], + [ + 258.0, + 139.0 + ], + [ + 241.0, + 129.0 + ], + [ + 260.0, + 119.0 + ] + ], + "center_px": [ + 258.75, + 128.75 + ], + "area_px": 349.0 + }, + { + "image_points_px": [ + [ + 313.0, + 126.0 + ], + [ + 295.0, + 137.0 + ], + [ + 278.0, + 128.0 + ], + [ + 297.0, + 117.0 + ] + ], + "center_px": [ + 295.75, + 127.0 + ], + "area_px": 348.0 + }, + { + "image_points_px": [ + [ + 91.0, + 135.0 + ], + [ + 71.0, + 146.0 + ], + [ + 56.0, + 137.0 + ], + [ + 75.0, + 126.0 + ] + ], + "center_px": [ + 73.25, + 136.0 + ], + "area_px": 346.0 + }, + { + "image_points_px": [ + [ + 128.0, + 134.0 + ], + [ + 108.0, + 145.0 + ], + [ + 93.0, + 136.0 + ], + [ + 112.0, + 125.0 + ] + ], + "center_px": [ + 110.25, + 135.0 + ], + "area_px": 346.0 + }, + { + "image_points_px": [ + [ + 170.0, + 110.0 + ], + [ + 151.0, + 121.0 + ], + [ + 135.0, + 112.0 + ], + [ + 155.0, + 101.0 + ] + ], + "center_px": [ + 152.75, + 111.0 + ], + "area_px": 346.0 + }, + { + "image_points_px": [ + [ + 165.0, + 132.0 + ], + [ + 146.0, + 143.0 + ], + [ + 130.0, + 134.0 + ], + [ + 150.0, + 123.0 + ] + ], + "center_px": [ + 147.75, + 133.0 + ], + "area_px": 346.0 + }, + { + "image_points_px": [ + [ + 1164.0, + 137.0 + ], + [ + 1176.0, + 126.0 + ], + [ + 1198.0, + 135.0 + ], + [ + 1187.0, + 146.0 + ] + ], + "center_px": [ + 1181.25, + 136.0 + ], + "area_px": 351.0 + }, + { + "image_points_px": [ + [ + 1092.0, + 140.0 + ], + [ + 1104.0, + 129.0 + ], + [ + 1126.0, + 138.0 + ], + [ + 1114.0, + 149.0 + ] + ], + "center_px": [ + 1109.0, + 139.0 + ], + "area_px": 350.0 + }, + { + "image_points_px": [ + [ + 1128.0, + 138.0 + ], + [ + 1139.0, + 128.0 + ], + [ + 1162.0, + 137.0 + ], + [ + 1150.0, + 148.0 + ] + ], + "center_px": [ + 1144.75, + 137.75 + ], + "area_px": 345.5 + }, + { + "image_points_px": [ + [ + 78.0, + 181.0 + ], + [ + 58.0, + 193.0 + ], + [ + 44.0, + 183.0 + ], + [ + 62.0, + 172.0 + ] + ], + "center_px": [ + 60.5, + 182.25 + ], + "area_px": 353.0 + }, + { + "image_points_px": [ + [ + 1118.0, + 116.0 + ], + [ + 1130.0, + 106.0 + ], + [ + 1152.0, + 115.0 + ], + [ + 1140.0, + 126.0 + ] + ], + "center_px": [ + 1135.0, + 115.75 + ], + "area_px": 345.0 + }, + { + "image_points_px": [ + [ + 571.0, + 116.0 + ], + [ + 587.0, + 106.0 + ], + [ + 606.0, + 115.0 + ], + [ + 590.0, + 125.0 + ] + ], + "center_px": [ + 588.5, + 115.5 + ], + "area_px": 334.0 + }, + { + "image_points_px": [ + [ + 60.0, + 115.0 + ], + [ + 40.0, + 125.0 + ], + [ + 25.0, + 116.0 + ], + [ + 44.0, + 106.0 + ] + ], + "center_px": [ + 42.25, + 115.5 + ], + "area_px": 330.5 + }, + { + "image_points_px": [ + [ + 243.0, + 108.0 + ], + [ + 224.0, + 118.0 + ], + [ + 208.0, + 109.0 + ], + [ + 226.0, + 99.0 + ] + ], + "center_px": [ + 225.25, + 108.5 + ], + "area_px": 331.5 + }, + { + "image_points_px": [ + [ + 316.0, + 105.0 + ], + [ + 298.0, + 115.0 + ], + [ + 281.0, + 106.0 + ], + [ + 299.0, + 96.0 + ] + ], + "center_px": [ + 298.5, + 105.5 + ], + "area_px": 332.0 + }, + { + "image_points_px": [ + [ + 610.0, + 137.0 + ], + [ + 626.0, + 126.0 + ], + [ + 644.0, + 135.0 + ], + [ + 630.0, + 146.0 + ] + ], + "center_px": [ + 627.5, + 136.0 + ], + "area_px": 344.0 + }, + { + "image_points_px": [ + [ + 608.0, + 115.0 + ], + [ + 624.0, + 104.0 + ], + [ + 642.0, + 113.0 + ], + [ + 627.0, + 124.0 + ] + ], + "center_px": [ + 625.25, + 114.0 + ], + "area_px": 343.0 + }, + { + "image_points_px": [ + [ + 1190.0, + 113.0 + ], + [ + 1201.0, + 103.0 + ], + [ + 1224.0, + 112.0 + ], + [ + 1213.0, + 122.0 + ] + ], + "center_px": [ + 1207.0, + 112.5 + ], + "area_px": 329.0 + }, + { + "image_points_px": [ + [ + 569.0, + 116.0 + ], + [ + 553.0, + 127.0 + ], + [ + 535.0, + 118.0 + ], + [ + 552.0, + 107.0 + ] + ], + "center_px": [ + 552.25, + 117.0 + ], + "area_px": 341.0 + }, + { + "image_points_px": [ + [ + 96.0, + 113.0 + ], + [ + 76.0, + 124.0 + ], + [ + 62.0, + 115.0 + ], + [ + 82.0, + 104.0 + ] + ], + "center_px": [ + 79.0, + 114.0 + ], + "area_px": 334.0 + }, + { + "image_points_px": [ + [ + 350.0, + 125.0 + ], + [ + 332.0, + 136.0 + ], + [ + 316.0, + 127.0 + ], + [ + 333.0, + 116.0 + ] + ], + "center_px": [ + 332.75, + 126.0 + ], + "area_px": 339.0 + }, + { + "image_points_px": [ + [ + 206.0, + 109.0 + ], + [ + 187.0, + 120.0 + ], + [ + 172.0, + 111.0 + ], + [ + 191.0, + 100.0 + ] + ], + "center_px": [ + 189.0, + 110.0 + ], + "area_px": 336.0 + }, + { + "image_points_px": [ + [ + 1237.0, + 134.0 + ], + [ + 1248.0, + 123.0 + ], + [ + 1270.0, + 132.0 + ], + [ + 1260.0, + 143.0 + ] + ], + "center_px": [ + 1253.75, + 133.0 + ], + "area_px": 342.0 + }, + { + "image_points_px": [ + [ + 1144.0, + 93.0 + ], + [ + 1156.0, + 83.0 + ], + [ + 1178.0, + 92.0 + ], + [ + 1166.0, + 102.0 + ] + ], + "center_px": [ + 1161.0, + 92.5 + ], + "area_px": 328.0 + }, + { + "image_points_px": [ + [ + 66.0, + 93.0 + ], + [ + 47.0, + 103.0 + ], + [ + 31.0, + 95.0 + ], + [ + 50.0, + 85.0 + ] + ], + "center_px": [ + 48.5, + 94.0 + ], + "area_px": 312.0 + }, + { + "image_points_px": [ + [ + 1154.0, + 115.0 + ], + [ + 1166.0, + 104.0 + ], + [ + 1187.0, + 113.0 + ], + [ + 1176.0, + 124.0 + ] + ], + "center_px": [ + 1170.75, + 114.0 + ], + "area_px": 340.0 + }, + { + "image_points_px": [ + [ + 348.0, + 194.0 + ], + [ + 364.0, + 183.0 + ], + [ + 381.0, + 192.0 + ], + [ + 365.0, + 204.0 + ] + ], + "center_px": [ + 364.5, + 193.25 + ], + "area_px": 347.5 + }, + { + "image_points_px": [ + [ + 1226.0, + 112.0 + ], + [ + 1237.0, + 101.0 + ], + [ + 1259.0, + 111.0 + ], + [ + 1248.0, + 121.0 + ] + ], + "center_px": [ + 1242.5, + 111.25 + ], + "area_px": 335.5 + }, + { + "image_points_px": [ + [ + 343.0, + 194.0 + ], + [ + 327.0, + 205.0 + ], + [ + 310.0, + 196.0 + ], + [ + 327.0, + 184.0 + ] + ], + "center_px": [ + 326.75, + 194.75 + ], + "area_px": 346.5 + }, + { + "image_points_px": [ + [ + 1083.0, + 118.0 + ], + [ + 1095.0, + 107.0 + ], + [ + 1116.0, + 116.0 + ], + [ + 1104.0, + 127.0 + ] + ], + "center_px": [ + 1099.5, + 117.0 + ], + "area_px": 339.0 + }, + { + "image_points_px": [ + [ + 644.0, + 113.0 + ], + [ + 659.0, + 103.0 + ], + [ + 678.0, + 112.0 + ], + [ + 664.0, + 122.0 + ] + ], + "center_px": [ + 661.25, + 112.5 + ], + "area_px": 325.5 + }, + { + "image_points_px": [ + [ + 570.0, + 95.0 + ], + [ + 585.0, + 85.0 + ], + [ + 604.0, + 93.0 + ], + [ + 588.0, + 104.0 + ] + ], + "center_px": [ + 586.75, + 94.25 + ], + "area_px": 326.0 + }, + { + "image_points_px": [ + [ + 496.0, + 97.0 + ], + [ + 480.0, + 108.0 + ], + [ + 462.0, + 99.0 + ], + [ + 479.0, + 89.0 + ] + ], + "center_px": [ + 479.25, + 98.25 + ], + "area_px": 324.0 + }, + { + "image_points_px": [ + [ + 499.0, + 119.0 + ], + [ + 515.0, + 109.0 + ], + [ + 533.0, + 118.0 + ], + [ + 518.0, + 128.0 + ] + ], + "center_px": [ + 516.25, + 118.5 + ], + "area_px": 324.5 + }, + { + "image_points_px": [ + [ + 606.0, + 93.0 + ], + [ + 622.0, + 83.0 + ], + [ + 640.0, + 92.0 + ], + [ + 625.0, + 102.0 + ] + ], + "center_px": [ + 623.25, + 92.5 + ], + "area_px": 324.5 + }, + { + "image_points_px": [ + [ + 318.0, + 105.0 + ], + [ + 334.0, + 95.0 + ], + [ + 352.0, + 103.0 + ], + [ + 334.0, + 114.0 + ] + ], + "center_px": [ + 334.5, + 104.25 + ], + "area_px": 323.0 + }, + { + "image_points_px": [ + [ + 568.0, + 95.0 + ], + [ + 553.0, + 105.0 + ], + [ + 534.0, + 96.0 + ], + [ + 551.0, + 86.0 + ] + ], + "center_px": [ + 551.5, + 95.5 + ], + "area_px": 324.0 + }, + { + "image_points_px": [ + [ + 424.0, + 100.0 + ], + [ + 407.0, + 111.0 + ], + [ + 390.0, + 102.0 + ], + [ + 407.0, + 92.0 + ] + ], + "center_px": [ + 407.0, + 101.25 + ], + "area_px": 323.0 + }, + { + "image_points_px": [ + [ + 425.0, + 79.0 + ], + [ + 408.0, + 90.0 + ], + [ + 391.0, + 81.0 + ], + [ + 408.0, + 71.0 + ] + ], + "center_px": [ + 408.0, + 80.25 + ], + "area_px": 323.0 + }, + { + "image_points_px": [ + [ + 279.0, + 106.0 + ], + [ + 262.0, + 116.0 + ], + [ + 245.0, + 108.0 + ], + [ + 263.0, + 97.0 + ] + ], + "center_px": [ + 262.25, + 106.75 + ], + "area_px": 322.0 + }, + { + "image_points_px": [ + [ + 246.0, + 86.0 + ], + [ + 227.0, + 97.0 + ], + [ + 212.0, + 88.0 + ], + [ + 229.0, + 78.0 + ] + ], + "center_px": [ + 228.5, + 87.25 + ], + "area_px": 321.0 + }, + { + "image_points_px": [ + [ + 460.0, + 99.0 + ], + [ + 444.0, + 109.0 + ], + [ + 426.0, + 100.0 + ], + [ + 444.0, + 90.0 + ] + ], + "center_px": [ + 443.5, + 99.5 + ], + "area_px": 323.0 + }, + { + "image_points_px": [ + [ + 133.0, + 112.0 + ], + [ + 115.0, + 122.0 + ], + [ + 99.0, + 114.0 + ], + [ + 118.0, + 103.0 + ] + ], + "center_px": [ + 116.25, + 112.75 + ], + "area_px": 320.0 + }, + { + "image_points_px": [ + [ + 138.0, + 90.0 + ], + [ + 119.0, + 101.0 + ], + [ + 104.0, + 92.0 + ], + [ + 123.0, + 82.0 + ] + ], + "center_px": [ + 121.0, + 91.25 + ], + "area_px": 319.0 + }, + { + "image_points_px": [ + [ + 388.0, + 102.0 + ], + [ + 371.0, + 112.0 + ], + [ + 354.0, + 103.0 + ], + [ + 371.0, + 93.0 + ] + ], + "center_px": [ + 371.0, + 102.5 + ], + "area_px": 323.0 + }, + { + "image_points_px": [ + [ + 210.0, + 88.0 + ], + [ + 192.0, + 98.0 + ], + [ + 176.0, + 89.0 + ], + [ + 195.0, + 79.0 + ] + ], + "center_px": [ + 193.25, + 88.5 + ], + "area_px": 321.5 + }, + { + "image_points_px": [ + [ + 318.0, + 84.0 + ], + [ + 300.0, + 94.0 + ], + [ + 284.0, + 85.0 + ], + [ + 302.0, + 75.0 + ] + ], + "center_px": [ + 301.0, + 84.5 + ], + "area_px": 322.0 + }, + { + "image_points_px": [ + [ + 1215.0, + 91.0 + ], + [ + 1226.0, + 80.0 + ], + [ + 1248.0, + 89.0 + ], + [ + 1237.0, + 99.0 + ] + ], + "center_px": [ + 1231.5, + 89.75 + ], + "area_px": 324.5 + }, + { + "image_points_px": [ + [ + 722.0, + 426.0 + ], + [ + 704.0, + 442.0 + ], + [ + 691.0, + 435.0 + ], + [ + 710.0, + 419.0 + ] + ], + "center_px": [ + 706.75, + 430.5 + ], + "area_px": 329.5 + }, + { + "image_points_px": [ + [ + 1109.0, + 95.0 + ], + [ + 1121.0, + 85.0 + ], + [ + 1142.0, + 93.0 + ], + [ + 1130.0, + 104.0 + ] + ], + "center_px": [ + 1125.5, + 94.25 + ], + "area_px": 322.5 + }, + { + "image_points_px": [ + [ + 639.0, + 71.0 + ], + [ + 653.0, + 62.0 + ], + [ + 673.0, + 70.0 + ], + [ + 657.0, + 80.0 + ] + ], + "center_px": [ + 655.5, + 70.75 + ], + "area_px": 308.0 + }, + { + "image_points_px": [ + [ + 1180.0, + 92.0 + ], + [ + 1191.0, + 82.0 + ], + [ + 1213.0, + 91.0 + ], + [ + 1201.0, + 101.0 + ] + ], + "center_px": [ + 1196.25, + 91.5 + ], + "area_px": 318.5 + }, + { + "image_points_px": [ + [ + 42.0, + 54.0 + ], + [ + 23.0, + 63.0 + ], + [ + 8.0, + 55.0 + ], + [ + 28.0, + 45.0 + ] + ], + "center_px": [ + 25.25, + 54.25 + ], + "area_px": 303.5 + }, + { + "image_points_px": [ + [ + 354.0, + 82.0 + ], + [ + 337.0, + 92.0 + ], + [ + 320.0, + 84.0 + ], + [ + 337.0, + 74.0 + ] + ], + "center_px": [ + 337.0, + 83.0 + ], + "area_px": 306.0 + }, + { + "image_points_px": [ + [ + 102.0, + 92.0 + ], + [ + 83.0, + 102.0 + ], + [ + 68.0, + 94.0 + ], + [ + 85.0, + 84.0 + ] + ], + "center_px": [ + 84.5, + 93.0 + ], + "area_px": 304.0 + }, + { + "image_points_px": [ + [ + 143.0, + 70.0 + ], + [ + 124.0, + 80.0 + ], + [ + 109.0, + 71.0 + ], + [ + 127.0, + 62.0 + ] + ], + "center_px": [ + 125.75, + 70.75 + ], + "area_px": 304.5 + }, + { + "image_points_px": [ + [ + 282.0, + 85.0 + ], + [ + 264.0, + 95.0 + ], + [ + 248.0, + 86.0 + ], + [ + 265.0, + 77.0 + ] + ], + "center_px": [ + 264.75, + 85.75 + ], + "area_px": 305.5 + }, + { + "image_points_px": [ + [ + 174.0, + 89.0 + ], + [ + 156.0, + 99.0 + ], + [ + 140.0, + 91.0 + ], + [ + 158.0, + 81.0 + ] + ], + "center_px": [ + 157.0, + 90.0 + ], + "area_px": 304.0 + }, + { + "image_points_px": [ + [ + 214.0, + 67.0 + ], + [ + 196.0, + 77.0 + ], + [ + 180.0, + 69.0 + ], + [ + 198.0, + 59.0 + ] + ], + "center_px": [ + 197.0, + 68.0 + ], + "area_px": 304.0 + }, + { + "image_points_px": [ + [ + 678.0, + 91.0 + ], + [ + 693.0, + 80.0 + ], + [ + 711.0, + 89.0 + ], + [ + 697.0, + 99.0 + ] + ], + "center_px": [ + 694.75, + 89.75 + ], + "area_px": 317.5 + }, + { + "image_points_px": [ + [ + 642.0, + 92.0 + ], + [ + 657.0, + 82.0 + ], + [ + 675.0, + 90.0 + ], + [ + 660.0, + 101.0 + ] + ], + "center_px": [ + 658.5, + 91.25 + ], + "area_px": 316.5 + }, + { + "image_points_px": [ + [ + 499.0, + 98.0 + ], + [ + 515.0, + 87.0 + ], + [ + 532.0, + 96.0 + ], + [ + 517.0, + 106.0 + ] + ], + "center_px": [ + 515.75, + 96.75 + ], + "area_px": 315.5 + }, + { + "image_points_px": [ + [ + 567.0, + 74.0 + ], + [ + 551.0, + 84.0 + ], + [ + 534.0, + 76.0 + ], + [ + 550.0, + 65.0 + ] + ], + "center_px": [ + 550.5, + 74.75 + ], + "area_px": 314.5 + }, + { + "image_points_px": [ + [ + 1169.0, + 71.0 + ], + [ + 1180.0, + 62.0 + ], + [ + 1202.0, + 70.0 + ], + [ + 1191.0, + 80.0 + ] + ], + "center_px": [ + 1185.5, + 70.75 + ], + "area_px": 302.5 + }, + { + "image_points_px": [ + [ + 1204.0, + 70.0 + ], + [ + 1215.0, + 60.0 + ], + [ + 1237.0, + 69.0 + ], + [ + 1226.0, + 78.0 + ] + ], + "center_px": [ + 1220.5, + 69.25 + ], + "area_px": 302.5 + }, + { + "image_points_px": [ + [ + 569.0, + 74.0 + ], + [ + 584.0, + 64.0 + ], + [ + 602.0, + 73.0 + ], + [ + 586.0, + 83.0 + ] + ], + "center_px": [ + 585.25, + 73.5 + ], + "area_px": 314.5 + }, + { + "image_points_px": [ + [ + 355.0, + 61.0 + ], + [ + 338.0, + 72.0 + ], + [ + 322.0, + 63.0 + ], + [ + 340.0, + 53.0 + ] + ], + "center_px": [ + 338.75, + 62.25 + ], + "area_px": 311.5 + }, + { + "image_points_px": [ + [ + 604.0, + 73.0 + ], + [ + 619.0, + 63.0 + ], + [ + 637.0, + 71.0 + ], + [ + 623.0, + 81.0 + ] + ], + "center_px": [ + 620.75, + 72.0 + ], + "area_px": 301.0 + }, + { + "image_points_px": [ + [ + 159.0, + 155.0 + ], + [ + 141.0, + 166.0 + ], + [ + 127.0, + 157.0 + ], + [ + 144.0, + 146.0 + ] + ], + "center_px": [ + 142.75, + 156.0 + ], + "area_px": 317.0 + }, + { + "image_points_px": [ + [ + 706.0, + 48.0 + ], + [ + 720.0, + 39.0 + ], + [ + 739.0, + 47.0 + ], + [ + 724.0, + 57.0 + ] + ], + "center_px": [ + 722.25, + 47.75 + ], + "area_px": 299.0 + }, + { + "image_points_px": [ + [ + 531.0, + 75.0 + ], + [ + 516.0, + 85.0 + ], + [ + 498.0, + 77.0 + ], + [ + 514.0, + 67.0 + ] + ], + "center_px": [ + 514.75, + 76.0 + ], + "area_px": 299.0 + }, + { + "image_points_px": [ + [ + 567.0, + 54.0 + ], + [ + 583.0, + 44.0 + ], + [ + 600.0, + 52.0 + ], + [ + 585.0, + 62.0 + ] + ], + "center_px": [ + 583.75, + 53.0 + ], + "area_px": 299.0 + }, + { + "image_points_px": [ + [ + 498.0, + 57.0 + ], + [ + 513.0, + 47.0 + ], + [ + 531.0, + 55.0 + ], + [ + 515.0, + 65.0 + ] + ], + "center_px": [ + 514.25, + 56.0 + ], + "area_px": 299.0 + }, + { + "image_points_px": [ + [ + 602.0, + 52.0 + ], + [ + 617.0, + 43.0 + ], + [ + 635.0, + 51.0 + ], + [ + 620.0, + 61.0 + ] + ], + "center_px": [ + 618.5, + 51.75 + ], + "area_px": 298.5 + }, + { + "image_points_px": [ + [ + 356.0, + 82.0 + ], + [ + 371.0, + 73.0 + ], + [ + 389.0, + 81.0 + ], + [ + 372.0, + 91.0 + ] + ], + "center_px": [ + 372.0, + 81.75 + ], + "area_px": 297.5 + }, + { + "image_points_px": [ + [ + 428.0, + 59.0 + ], + [ + 443.0, + 50.0 + ], + [ + 461.0, + 58.0 + ], + [ + 444.0, + 68.0 + ] + ], + "center_px": [ + 444.0, + 58.75 + ], + "area_px": 297.5 + }, + { + "image_points_px": [ + [ + 320.0, + 63.0 + ], + [ + 303.0, + 73.0 + ], + [ + 287.0, + 65.0 + ], + [ + 303.0, + 55.0 + ] + ], + "center_px": [ + 303.25, + 64.0 + ], + "area_px": 297.0 + }, + { + "image_points_px": [ + [ + 390.0, + 60.0 + ], + [ + 374.0, + 70.0 + ], + [ + 357.0, + 62.0 + ], + [ + 374.0, + 52.0 + ] + ], + "center_px": [ + 373.75, + 61.0 + ], + "area_px": 297.0 + }, + { + "image_points_px": [ + [ + 249.0, + 66.0 + ], + [ + 233.0, + 75.0 + ], + [ + 216.0, + 67.0 + ], + [ + 234.0, + 57.0 + ] + ], + "center_px": [ + 233.0, + 66.25 + ], + "area_px": 296.5 + }, + { + "image_points_px": [ + [ + 182.0, + 48.0 + ], + [ + 165.0, + 58.0 + ], + [ + 149.0, + 50.0 + ], + [ + 168.0, + 40.0 + ] + ], + "center_px": [ + 166.0, + 49.0 + ], + "area_px": 294.0 + }, + { + "image_points_px": [ + [ + 178.0, + 68.0 + ], + [ + 161.0, + 78.0 + ], + [ + 145.0, + 70.0 + ], + [ + 163.0, + 60.0 + ] + ], + "center_px": [ + 161.75, + 69.0 + ], + "area_px": 295.0 + }, + { + "image_points_px": [ + [ + 284.0, + 64.0 + ], + [ + 267.0, + 74.0 + ], + [ + 251.0, + 66.0 + ], + [ + 269.0, + 56.0 + ] + ], + "center_px": [ + 267.75, + 65.0 + ], + "area_px": 295.0 + }, + { + "image_points_px": [ + [ + 77.0, + 52.0 + ], + [ + 58.0, + 62.0 + ], + [ + 44.0, + 54.0 + ], + [ + 63.0, + 44.0 + ] + ], + "center_px": [ + 60.5, + 53.0 + ], + "area_px": 292.0 + }, + { + "image_points_px": [ + [ + 112.0, + 51.0 + ], + [ + 93.0, + 61.0 + ], + [ + 79.0, + 53.0 + ], + [ + 98.0, + 43.0 + ] + ], + "center_px": [ + 95.5, + 52.0 + ], + "area_px": 292.0 + }, + { + "image_points_px": [ + [ + 107.0, + 71.0 + ], + [ + 88.0, + 81.0 + ], + [ + 74.0, + 73.0 + ], + [ + 92.0, + 63.0 + ] + ], + "center_px": [ + 90.25, + 72.0 + ], + "area_px": 293.0 + }, + { + "image_points_px": [ + [ + 996.0, + 78.0 + ], + [ + 1008.0, + 68.0 + ], + [ + 1028.0, + 77.0 + ], + [ + 1016.0, + 87.0 + ] + ], + "center_px": [ + 1012.0, + 77.5 + ], + "area_px": 308.0 + }, + { + "image_points_px": [ + [ + 1239.0, + 69.0 + ], + [ + 1249.0, + 59.0 + ], + [ + 1271.0, + 67.0 + ], + [ + 1261.0, + 77.0 + ] + ], + "center_px": [ + 1255.0, + 68.0 + ], + "area_px": 300.0 + }, + { + "image_points_px": [ + [ + 761.0, + 393.0 + ], + [ + 743.0, + 408.0 + ], + [ + 731.0, + 402.0 + ], + [ + 748.0, + 386.0 + ] + ], + "center_px": [ + 745.75, + 397.25 + ], + "area_px": 307.5 + }, + { + "image_points_px": [ + [ + 1031.0, + 77.0 + ], + [ + 1042.0, + 67.0 + ], + [ + 1063.0, + 75.0 + ], + [ + 1052.0, + 85.0 + ] + ], + "center_px": [ + 1047.0, + 76.0 + ], + "area_px": 298.0 + }, + { + "image_points_px": [ + [ + 1135.0, + 73.0 + ], + [ + 1146.0, + 63.0 + ], + [ + 1167.0, + 71.0 + ], + [ + 1156.0, + 81.0 + ] + ], + "center_px": [ + 1151.0, + 72.0 + ], + "area_px": 298.0 + }, + { + "image_points_px": [ + [ + 1194.0, + 50.0 + ], + [ + 1205.0, + 40.0 + ], + [ + 1226.0, + 49.0 + ], + [ + 1216.0, + 58.0 + ] + ], + "center_px": [ + 1210.25, + 49.25 + ], + "area_px": 293.5 + }, + { + "image_points_px": [ + [ + 637.0, + 51.0 + ], + [ + 651.0, + 42.0 + ], + [ + 670.0, + 50.0 + ], + [ + 656.0, + 59.0 + ] + ], + "center_px": [ + 653.5, + 50.5 + ], + "area_px": 283.0 + }, + { + "image_points_px": [ + [ + 497.0, + 37.0 + ], + [ + 512.0, + 28.0 + ], + [ + 530.0, + 35.0 + ], + [ + 514.0, + 45.0 + ] + ], + "center_px": [ + 513.25, + 36.25 + ], + "area_px": 282.5 + }, + { + "image_points_px": [ + [ + 428.0, + 39.0 + ], + [ + 444.0, + 30.0 + ], + [ + 461.0, + 38.0 + ], + [ + 445.0, + 47.0 + ] + ], + "center_px": [ + 444.5, + 38.5 + ], + "area_px": 281.0 + }, + { + "image_points_px": [ + [ + 147.0, + 49.0 + ], + [ + 130.0, + 59.0 + ], + [ + 114.0, + 51.0 + ], + [ + 132.0, + 42.0 + ] + ], + "center_px": [ + 130.75, + 50.25 + ], + "area_px": 278.5 + }, + { + "image_points_px": [ + [ + 48.0, + 34.0 + ], + [ + 30.0, + 43.0 + ], + [ + 15.0, + 36.0 + ], + [ + 34.0, + 26.0 + ] + ], + "center_px": [ + 31.75, + 34.75 + ], + "area_px": 276.5 + }, + { + "image_points_px": [ + [ + 357.0, + 42.0 + ], + [ + 341.0, + 51.0 + ], + [ + 324.0, + 43.0 + ], + [ + 341.0, + 34.0 + ] + ], + "center_px": [ + 340.75, + 42.5 + ], + "area_px": 280.5 + }, + { + "image_points_px": [ + [ + 187.0, + 29.0 + ], + [ + 169.0, + 38.0 + ], + [ + 154.0, + 30.0 + ], + [ + 172.0, + 21.0 + ] + ], + "center_px": [ + 170.5, + 29.5 + ], + "area_px": 279.0 + }, + { + "image_points_px": [ + [ + 1023.0, + 57.0 + ], + [ + 1035.0, + 47.0 + ], + [ + 1055.0, + 55.0 + ], + [ + 1042.0, + 65.0 + ] + ], + "center_px": [ + 1038.75, + 56.0 + ], + "area_px": 295.0 + }, + { + "image_points_px": [ + [ + 675.0, + 70.0 + ], + [ + 690.0, + 60.0 + ], + [ + 707.0, + 68.0 + ], + [ + 694.0, + 78.0 + ] + ], + "center_px": [ + 691.5, + 69.0 + ], + "area_px": 292.0 + }, + { + "image_points_px": [ + [ + 710.0, + 69.0 + ], + [ + 724.0, + 59.0 + ], + [ + 742.0, + 67.0 + ], + [ + 728.0, + 77.0 + ] + ], + "center_px": [ + 726.0, + 68.0 + ], + "area_px": 292.0 + }, + { + "image_points_px": [ + [ + 196.0, + 153.0 + ], + [ + 179.0, + 164.0 + ], + [ + 165.0, + 155.0 + ], + [ + 182.0, + 144.0 + ] + ], + "center_px": [ + 180.5, + 154.0 + ], + "area_px": 307.0 + }, + { + "image_points_px": [ + [ + 1160.0, + 51.0 + ], + [ + 1170.0, + 42.0 + ], + [ + 1192.0, + 50.0 + ], + [ + 1182.0, + 59.0 + ] + ], + "center_px": [ + 1176.0, + 50.5 + ], + "area_px": 278.0 + }, + { + "image_points_px": [ + [ + 672.0, + 50.0 + ], + [ + 687.0, + 40.0 + ], + [ + 704.0, + 48.0 + ], + [ + 690.0, + 58.0 + ] + ], + "center_px": [ + 688.25, + 49.0 + ], + "area_px": 291.0 + }, + { + "image_points_px": [ + [ + 635.0, + 32.0 + ], + [ + 650.0, + 22.0 + ], + [ + 667.0, + 30.0 + ], + [ + 652.0, + 40.0 + ] + ], + "center_px": [ + 651.0, + 31.0 + ], + "area_px": 290.0 + }, + { + "image_points_px": [ + [ + 495.0, + 56.0 + ], + [ + 480.0, + 66.0 + ], + [ + 463.0, + 58.0 + ], + [ + 479.0, + 48.0 + ] + ], + "center_px": [ + 479.25, + 57.0 + ], + "area_px": 289.0 + }, + { + "image_points_px": [ + [ + 425.0, + 59.0 + ], + [ + 409.0, + 69.0 + ], + [ + 393.0, + 61.0 + ], + [ + 409.0, + 51.0 + ] + ], + "center_px": [ + 409.0, + 60.0 + ], + "area_px": 288.0 + }, + { + "image_points_px": [ + [ + 426.0, + 39.0 + ], + [ + 410.0, + 49.0 + ], + [ + 394.0, + 41.0 + ], + [ + 410.0, + 31.0 + ] + ], + "center_px": [ + 410.0, + 40.0 + ], + "area_px": 288.0 + }, + { + "image_points_px": [ + [ + 529.0, + 16.0 + ], + [ + 513.0, + 26.0 + ], + [ + 497.0, + 17.0 + ], + [ + 513.0, + 8.0 + ] + ], + "center_px": [ + 513.0, + 16.75 + ], + "area_px": 288.0 + }, + { + "image_points_px": [ + [ + 495.0, + 18.0 + ], + [ + 479.0, + 27.0 + ], + [ + 463.0, + 19.0 + ], + [ + 479.0, + 9.0 + ] + ], + "center_px": [ + 479.0, + 18.25 + ], + "area_px": 288.0 + }, + { + "image_points_px": [ + [ + 152.0, + 30.0 + ], + [ + 133.0, + 40.0 + ], + [ + 120.0, + 32.0 + ], + [ + 138.0, + 22.0 + ] + ], + "center_px": [ + 135.75, + 31.0 + ], + "area_px": 283.0 + }, + { + "image_points_px": [ + [ + 322.0, + 43.0 + ], + [ + 305.0, + 53.0 + ], + [ + 290.0, + 45.0 + ], + [ + 307.0, + 35.0 + ] + ], + "center_px": [ + 306.0, + 44.0 + ], + "area_px": 286.0 + }, + { + "image_points_px": [ + [ + 1057.0, + 55.0 + ], + [ + 1068.0, + 46.0 + ], + [ + 1089.0, + 54.0 + ], + [ + 1078.0, + 63.0 + ] + ], + "center_px": [ + 1073.0, + 54.5 + ], + "area_px": 277.0 + }, + { + "image_points_px": [ + [ + 1049.0, + 35.0 + ], + [ + 1060.0, + 26.0 + ], + [ + 1081.0, + 34.0 + ], + [ + 1070.0, + 43.0 + ] + ], + "center_px": [ + 1065.0, + 34.5 + ], + "area_px": 277.0 + }, + { + "image_points_px": [ + [ + 427.0, + 122.0 + ], + [ + 442.0, + 112.0 + ], + [ + 458.0, + 120.0 + ], + [ + 443.0, + 131.0 + ] + ], + "center_px": [ + 442.5, + 121.25 + ], + "area_px": 295.5 + }, + { + "image_points_px": [ + [ + 422.0, + 122.0 + ], + [ + 407.0, + 132.0 + ], + [ + 391.0, + 124.0 + ], + [ + 407.0, + 113.0 + ] + ], + "center_px": [ + 406.75, + 122.75 + ], + "area_px": 294.5 + }, + { + "image_points_px": [ + [ + 533.0, + 55.0 + ], + [ + 547.0, + 46.0 + ], + [ + 565.0, + 53.0 + ], + [ + 550.0, + 63.0 + ] + ], + "center_px": [ + 548.75, + 54.25 + ], + "area_px": 275.0 + }, + { + "image_points_px": [ + [ + 741.0, + 47.0 + ], + [ + 755.0, + 38.0 + ], + [ + 773.0, + 46.0 + ], + [ + 760.0, + 55.0 + ] + ], + "center_px": [ + 757.25, + 46.5 + ], + "area_px": 274.5 + }, + { + "image_points_px": [ + [ + 771.0, + 26.0 + ], + [ + 785.0, + 17.0 + ], + [ + 803.0, + 25.0 + ], + [ + 790.0, + 34.0 + ] + ], + "center_px": [ + 787.25, + 25.5 + ], + "area_px": 274.5 + }, + { + "image_points_px": [ + [ + 463.0, + 38.0 + ], + [ + 478.0, + 29.0 + ], + [ + 495.0, + 36.0 + ], + [ + 480.0, + 46.0 + ] + ], + "center_px": [ + 479.0, + 37.25 + ], + "area_px": 274.0 + }, + { + "image_points_px": [ + [ + 703.0, + 29.0 + ], + [ + 717.0, + 20.0 + ], + [ + 735.0, + 28.0 + ], + [ + 721.0, + 37.0 + ] + ], + "center_px": [ + 719.0, + 28.5 + ], + "area_px": 274.0 + }, + { + "image_points_px": [ + [ + 669.0, + 30.0 + ], + [ + 684.0, + 21.0 + ], + [ + 701.0, + 29.0 + ], + [ + 687.0, + 38.0 + ] + ], + "center_px": [ + 685.25, + 29.5 + ], + "area_px": 273.5 + }, + { + "image_points_px": [ + [ + 358.0, + 22.0 + ], + [ + 342.0, + 32.0 + ], + [ + 326.0, + 24.0 + ], + [ + 342.0, + 15.0 + ] + ], + "center_px": [ + 342.0, + 23.25 + ], + "area_px": 272.0 + }, + { + "image_points_px": [ + [ + 391.0, + 40.0 + ], + [ + 375.0, + 50.0 + ], + [ + 359.0, + 42.0 + ], + [ + 375.0, + 33.0 + ] + ], + "center_px": [ + 375.0, + 41.25 + ], + "area_px": 272.0 + }, + { + "image_points_px": [ + [ + 531.0, + 16.0 + ], + [ + 546.0, + 7.0 + ], + [ + 563.0, + 15.0 + ], + [ + 548.0, + 24.0 + ] + ], + "center_px": [ + 547.0, + 15.5 + ], + "area_px": 273.0 + }, + { + "image_points_px": [ + [ + 565.0, + 15.0 + ], + [ + 580.0, + 6.0 + ], + [ + 597.0, + 14.0 + ], + [ + 582.0, + 23.0 + ] + ], + "center_px": [ + 581.0, + 14.5 + ], + "area_px": 273.0 + }, + { + "image_points_px": [ + [ + 287.0, + 44.0 + ], + [ + 270.0, + 54.0 + ], + [ + 255.0, + 46.0 + ], + [ + 271.0, + 37.0 + ] + ], + "center_px": [ + 270.75, + 45.25 + ], + "area_px": 271.0 + }, + { + "image_points_px": [ + [ + 290.0, + 25.0 + ], + [ + 274.0, + 34.0 + ], + [ + 258.0, + 27.0 + ], + [ + 275.0, + 17.0 + ] + ], + "center_px": [ + 274.25, + 25.75 + ], + "area_px": 271.0 + }, + { + "image_points_px": [ + [ + 255.0, + 26.0 + ], + [ + 238.0, + 36.0 + ], + [ + 223.0, + 28.0 + ], + [ + 240.0, + 19.0 + ] + ], + "center_px": [ + 239.0, + 27.25 + ], + "area_px": 270.0 + }, + { + "image_points_px": [ + [ + 221.0, + 28.0 + ], + [ + 203.0, + 37.0 + ], + [ + 189.0, + 29.0 + ], + [ + 205.0, + 20.0 + ] + ], + "center_px": [ + 204.5, + 28.5 + ], + "area_px": 271.0 + }, + { + "image_points_px": [ + [ + 427.0, + 20.0 + ], + [ + 411.0, + 29.0 + ], + [ + 395.0, + 21.0 + ], + [ + 411.0, + 12.0 + ] + ], + "center_px": [ + 411.0, + 20.5 + ], + "area_px": 272.0 + }, + { + "image_points_px": [ + [ + 461.0, + 19.0 + ], + [ + 445.0, + 28.0 + ], + [ + 429.0, + 20.0 + ], + [ + 445.0, + 11.0 + ] + ], + "center_px": [ + 445.0, + 19.5 + ], + "area_px": 272.0 + }, + { + "image_points_px": [ + [ + 324.0, + 24.0 + ], + [ + 308.0, + 33.0 + ], + [ + 292.0, + 25.0 + ], + [ + 309.0, + 16.0 + ] + ], + "center_px": [ + 308.25, + 24.5 + ], + "area_px": 271.5 + }, + { + "image_points_px": [ + [ + 737.0, + 28.0 + ], + [ + 750.0, + 19.0 + ], + [ + 769.0, + 26.0 + ], + [ + 756.0, + 35.0 + ] + ], + "center_px": [ + 753.0, + 27.0 + ], + "area_px": 262.0 + }, + { + "image_points_px": [ + [ + 361.0, + 23.0 + ], + [ + 376.0, + 14.0 + ], + [ + 393.0, + 21.0 + ], + [ + 377.0, + 30.0 + ] + ], + "center_px": [ + 376.75, + 22.0 + ], + "area_px": 257.0 + }, + { + "image_points_px": [ + [ + 122.0, + 12.0 + ], + [ + 106.0, + 21.0 + ], + [ + 90.0, + 14.0 + ], + [ + 107.0, + 5.0 + ] + ], + "center_px": [ + 106.25, + 13.0 + ], + "area_px": 255.0 + }, + { + "image_points_px": [ + [ + 601.0, + 33.0 + ], + [ + 615.0, + 24.0 + ], + [ + 632.0, + 31.0 + ], + [ + 618.0, + 41.0 + ] + ], + "center_px": [ + 616.5, + 32.25 + ], + "area_px": 266.5 + }, + { + "image_points_px": [ + [ + 633.0, + 13.0 + ], + [ + 646.0, + 4.0 + ], + [ + 664.0, + 11.0 + ], + [ + 650.0, + 20.0 + ] + ], + "center_px": [ + 648.25, + 12.0 + ], + "area_px": 252.0 + }, + { + "image_points_px": [ + [ + 599.0, + 14.0 + ], + [ + 613.0, + 5.0 + ], + [ + 630.0, + 12.0 + ], + [ + 617.0, + 21.0 + ] + ], + "center_px": [ + 614.75, + 13.0 + ], + "area_px": 252.0 + }, + { + "image_points_px": [ + [ + 604.0, + 156.0 + ], + [ + 587.0, + 168.0 + ], + [ + 574.0, + 161.0 + ], + [ + 591.0, + 150.0 + ] + ], + "center_px": [ + 589.0, + 158.75 + ], + "area_px": 260.0 + }, + { + "image_points_px": [ + [ + 862.0, + 451.0 + ], + [ + 866.0, + 448.0 + ], + [ + 892.0, + 462.0 + ], + [ + 885.0, + 466.0 + ] + ], + "center_px": [ + 876.25, + 456.75 + ], + "area_px": 165.5 + }, + { + "image_points_px": [ + [ + 69.0, + 72.0 + ], + [ + 53.0, + 82.0 + ], + [ + 39.0, + 74.0 + ], + [ + 57.0, + 64.0 + ] + ], + "center_px": [ + 54.5, + 73.0 + ], + "area_px": 266.0 + }, + { + "image_points_px": [ + [ + 34.0, + 74.0 + ], + [ + 16.0, + 84.0 + ], + [ + 4.0, + 76.0 + ], + [ + 21.0, + 66.0 + ] + ], + "center_px": [ + 18.75, + 75.0 + ], + "area_px": 265.0 + }, + { + "image_points_px": [ + [ + 914.0, + 482.0 + ], + [ + 918.0, + 479.0 + ], + [ + 943.0, + 495.0 + ], + [ + 939.0, + 498.0 + ] + ], + "center_px": [ + 928.5, + 488.5 + ], + "area_px": 139.0 + }, + { + "image_points_px": [ + [ + 464.0, + 78.0 + ], + [ + 478.0, + 69.0 + ], + [ + 494.0, + 77.0 + ], + [ + 481.0, + 86.0 + ] + ], + "center_px": [ + 479.25, + 77.5 + ], + "area_px": 256.5 + }, + { + "image_points_px": [ + [ + 251.0, + 46.0 + ], + [ + 235.0, + 55.0 + ], + [ + 221.0, + 47.0 + ], + [ + 237.0, + 38.0 + ] + ], + "center_px": [ + 236.0, + 46.5 + ], + "area_px": 254.0 + }, + { + "image_points_px": [ + [ + 216.0, + 47.0 + ], + [ + 201.0, + 56.0 + ], + [ + 187.0, + 49.0 + ], + [ + 203.0, + 39.0 + ] + ], + "center_px": [ + 201.75, + 47.75 + ], + "area_px": 244.5 + }, + { + "image_points_px": [ + [ + 115.0, + 31.0 + ], + [ + 98.0, + 41.0 + ], + [ + 86.0, + 33.0 + ], + [ + 102.0, + 24.0 + ] + ], + "center_px": [ + 100.25, + 32.25 + ], + "area_px": 242.5 + }, + { + "image_points_px": [ + [ + 86.0, + 13.0 + ], + [ + 71.0, + 22.0 + ], + [ + 57.0, + 15.0 + ], + [ + 74.0, + 6.0 + ] + ], + "center_px": [ + 72.0, + 14.0 + ], + "area_px": 229.0 + }, + { + "image_points_px": [ + [ + 534.0, + 36.0 + ], + [ + 546.0, + 27.0 + ], + [ + 562.0, + 34.0 + ], + [ + 550.0, + 43.0 + ] + ], + "center_px": [ + 548.0, + 35.0 + ], + "area_px": 228.0 + }, + { + "image_points_px": [ + [ + 763.0, + 392.0 + ], + [ + 768.0, + 390.0 + ], + [ + 791.0, + 402.0 + ], + [ + 784.0, + 406.0 + ] + ], + "center_px": [ + 776.5, + 397.5 + ], + "area_px": 144.0 + }, + { + "image_points_px": [ + [ + 51.0, + 14.0 + ], + [ + 34.0, + 24.0 + ], + [ + 23.0, + 17.0 + ], + [ + 38.0, + 8.0 + ] + ], + "center_px": [ + 36.5, + 15.75 + ], + "area_px": 218.0 + }, + { + "image_points_px": [ + [ + 482.0, + 559.0 + ], + [ + 480.0, + 563.0 + ], + [ + 458.0, + 580.0 + ], + [ + 464.0, + 572.0 + ] + ], + "center_px": [ + 471.0, + 568.5 + ], + "area_px": 60.0 + }, + { + "image_points_px": [ + [ + 194.0, + 243.0 + ], + [ + 205.0, + 238.0 + ], + [ + 221.0, + 249.0 + ], + [ + 211.0, + 255.0 + ] + ], + "center_px": [ + 207.75, + 246.25 + ], + "area_px": 211.5 + }, + { + "image_points_px": [ + [ + 934.0, + 244.0 + ], + [ + 920.0, + 256.0 + ], + [ + 908.0, + 251.0 + ], + [ + 922.0, + 238.0 + ] + ], + "center_px": [ + 921.0, + 247.25 + ], + "area_px": 227.0 + }, + { + "image_points_px": [ + [ + 230.0, + 268.0 + ], + [ + 241.0, + 262.0 + ], + [ + 257.0, + 273.0 + ], + [ + 247.0, + 279.0 + ] + ], + "center_px": [ + 243.75, + 270.5 + ], + "area_px": 214.5 + }, + { + "image_points_px": [ + [ + 83.0, + 272.0 + ], + [ + 61.0, + 286.0 + ], + [ + 56.0, + 283.0 + ], + [ + 77.0, + 269.0 + ] + ], + "center_px": [ + 69.25, + 277.5 + ], + "area_px": 141.5 + }, + { + "image_points_px": [ + [ + 902.0, + 272.0 + ], + [ + 889.0, + 283.0 + ], + [ + 876.0, + 277.0 + ], + [ + 889.0, + 266.0 + ] + ], + "center_px": [ + 889.0, + 274.5 + ], + "area_px": 221.0 + }, + { + "image_points_px": [ + [ + 564.0, + 179.0 + ], + [ + 547.0, + 191.0 + ], + [ + 538.0, + 186.0 + ], + [ + 555.0, + 174.0 + ] + ], + "center_px": [ + 551.0, + 182.5 + ], + "area_px": 193.0 + }, + { + "image_points_px": [ + [ + 965.0, + 217.0 + ], + [ + 951.0, + 229.0 + ], + [ + 940.0, + 224.0 + ], + [ + 954.0, + 211.0 + ] + ], + "center_px": [ + 952.5, + 220.25 + ], + "area_px": 214.5 + }, + { + "image_points_px": [ + [ + 1031.0, + 371.0 + ], + [ + 1057.0, + 386.0 + ], + [ + 1047.0, + 384.0 + ], + [ + 1036.0, + 378.0 + ] + ], + "center_px": [ + 1042.75, + 379.75 + ], + "area_px": 72.5 + }, + { + "image_points_px": [ + [ + 995.0, + 191.0 + ], + [ + 981.0, + 203.0 + ], + [ + 970.0, + 197.0 + ], + [ + 983.0, + 186.0 + ] + ], + "center_px": [ + 982.25, + 194.25 + ], + "area_px": 206.5 + }, + { + "image_points_px": [ + [ + 1025.0, + 166.0 + ], + [ + 1011.0, + 177.0 + ], + [ + 1000.0, + 172.0 + ], + [ + 1013.0, + 161.0 + ] + ], + "center_px": [ + 1012.25, + 169.0 + ], + "area_px": 194.0 + }, + { + "image_points_px": [ + [ + 630.0, + 269.0 + ], + [ + 648.0, + 280.0 + ], + [ + 648.0, + 288.0 + ], + [ + 630.0, + 279.0 + ] + ], + "center_px": [ + 639.0, + 279.0 + ], + "area_px": 162.0 + }, + { + "image_points_px": [ + [ + 1053.0, + 142.0 + ], + [ + 1040.0, + 152.0 + ], + [ + 1029.0, + 148.0 + ], + [ + 1042.0, + 136.0 + ] + ], + "center_px": [ + 1041.0, + 144.5 + ], + "area_px": 186.0 + }, + { + "image_points_px": [ + [ + 739.0, + 330.0 + ], + [ + 760.0, + 330.0 + ], + [ + 767.0, + 334.0 + ], + [ + 744.0, + 333.0 + ] + ], + "center_px": [ + 752.5, + 331.75 + ], + "area_px": 74.0 + }, + { + "image_points_px": [ + [ + 128.0, + 244.0 + ], + [ + 108.0, + 257.0 + ], + [ + 104.0, + 254.0 + ], + [ + 123.0, + 242.0 + ] + ], + "center_px": [ + 115.75, + 249.25 + ], + "area_px": 105.0 + }, + { + "image_points_px": [ + [ + 1080.0, + 118.0 + ], + [ + 1068.0, + 128.0 + ], + [ + 1057.0, + 124.0 + ], + [ + 1069.0, + 113.0 + ] + ], + "center_px": [ + 1068.5, + 120.75 + ], + "area_px": 169.5 + }, + { + "image_points_px": [ + [ + 1107.0, + 95.0 + ], + [ + 1095.0, + 105.0 + ], + [ + 1084.0, + 101.0 + ], + [ + 1096.0, + 90.0 + ] + ], + "center_px": [ + 1095.5, + 97.75 + ], + "area_px": 169.5 + }, + { + "image_points_px": [ + [ + 1122.0, + 502.0 + ], + [ + 1124.0, + 500.0 + ], + [ + 1144.0, + 516.0 + ], + [ + 1136.0, + 515.0 + ] + ], + "center_px": [ + 1131.5, + 508.25 + ], + "area_px": 81.0 + }, + { + "image_points_px": [ + [ + 1133.0, + 73.0 + ], + [ + 1122.0, + 82.0 + ], + [ + 1110.0, + 78.0 + ], + [ + 1122.0, + 68.0 + ] + ], + "center_px": [ + 1121.75, + 75.25 + ], + "area_px": 161.0 + }, + { + "image_points_px": [ + [ + 1158.0, + 51.0 + ], + [ + 1146.0, + 61.0 + ], + [ + 1136.0, + 57.0 + ], + [ + 1146.0, + 47.0 + ] + ], + "center_px": [ + 1146.5, + 54.0 + ], + "area_px": 154.0 + }, + { + "image_points_px": [ + [ + 833.0, + 124.0 + ], + [ + 839.0, + 121.0 + ], + [ + 856.0, + 131.0 + ], + [ + 852.0, + 134.0 + ] + ], + "center_px": [ + 845.0, + 127.5 + ], + "area_px": 104.0 + }, + { + "image_points_px": [ + [ + 1182.0, + 30.0 + ], + [ + 1172.0, + 39.0 + ], + [ + 1160.0, + 35.0 + ], + [ + 1171.0, + 26.0 + ] + ], + "center_px": [ + 1171.25, + 32.5 + ], + "area_px": 145.5 + }, + { + "image_points_px": [ + [ + 1157.0, + 499.0 + ], + [ + 1164.0, + 489.0 + ], + [ + 1177.0, + 496.0 + ], + [ + 1164.0, + 503.0 + ] + ], + "center_px": [ + 1165.5, + 496.75 + ], + "area_px": 140.0 + }, + { + "image_points_px": [ + [ + 186.0, + 654.0 + ], + [ + 191.0, + 660.0 + ], + [ + 177.0, + 669.0 + ], + [ + 173.0, + 664.0 + ] + ], + "center_px": [ + 181.75, + 661.75 + ], + "area_px": 117.0 + }, + { + "image_points_px": [ + [ + 1124.0, + 403.0 + ], + [ + 1131.0, + 398.0 + ], + [ + 1144.0, + 406.0 + ], + [ + 1129.0, + 406.0 + ] + ], + "center_px": [ + 1132.0, + 403.25 + ], + "area_px": 83.0 + }, + { + "image_points_px": [ + [ + 979.0, + 438.0 + ], + [ + 992.0, + 446.0 + ], + [ + 984.0, + 454.0 + ], + [ + 981.0, + 449.0 + ] + ], + "center_px": [ + 984.0, + 446.75 + ], + "area_px": 95.5 + }, + { + "image_points_px": [ + [ + 856.0, + 266.0 + ], + [ + 874.0, + 276.0 + ], + [ + 865.0, + 273.0 + ], + [ + 858.0, + 269.0 + ] + ], + "center_px": [ + 863.25, + 271.0 + ], + "area_px": 24.5 + } + ] +} \ No newline at end of file diff --git a/pipeline/render_1a_camera_pose.json b/pipeline/render_1a_camera_pose.json new file mode 100644 index 0000000..8915e33 --- /dev/null +++ b/pipeline/render_1a_camera_pose.json @@ -0,0 +1,322 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T17:28:00Z", + "source": { + "detection_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1a_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": 8, + "used_marker_ids": [ + 210, + 215, + 211, + 208, + 217, + 206, + 205, + 207 + ], + "history": { + "iters": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "rms": [ + 0.012238825888602721, + 0.0013485501296120386, + 0.0010720241126721643, + 0.0010700649720543842, + 0.0010700240850162058, + 0.0010700235476777484, + 0.0010700235426684534 + ], + "lambda": [ + 0.001, + 0.0005, + 0.00025, + 0.000125, + 6.25e-05, + 3.125e-05, + 1.5625e-05 + ] + }, + "residual_rms_px": 2.6178729686079047, + "residual_median_px": 2.1333107363119628, + "residual_max_px": 4.82379629519028, + "sigma2_normalized": 1.8319206108523286e-06 + }, + "camera_pose": { + "world_to_camera": { + "rotation_matrix": [ + [ + 0.6343430876731873, + -0.7725288271903992, + 0.028426652774214745 + ], + [ + -0.5990912914276123, + -0.5145038366317749, + -0.6134944558143616 + ], + [ + 0.4885677695274353, + 0.37213581800460815, + -0.7891872525215149 + ] + ], + "translation_m": [ + -0.28031569719314575, + 0.19169889390468597, + 0.9508554935455322 + ], + "rvec_rad": [ + 2.289241976117495, + -1.068731724683893, + 0.4028289864991492 + ] + }, + "camera_in_world": { + "position_m": [ + -0.17189589142799377, + -0.4717695116996765, + 0.8759776949882507 + ], + "position_mm": [ + -171.89588928222656, + -471.7695007324219, + 875.9777221679688 + ], + "orientation_deg": { + "roll": 154.75408935546875, + "pitch": -29.24648666381836, + "yaw": -43.362918853759766 + } + }, + "uncertainty": { + "pose_covariance_6x6": [ + [ + 0.0003822156649420553, + -1.9575638833486346e-05, + 0.0002401729876123921, + -3.455538550581847e-07, + -8.434305689855638e-06, + 1.3315803977557116e-06 + ], + [ + -1.9575638833486905e-05, + 9.100791133973781e-06, + -2.6448385490934555e-05, + -2.887449407782355e-06, + 1.4831323494675526e-07, + 8.401000542324975e-06 + ], + [ + 0.00024017298761239858, + -2.644838549093476e-05, + 0.00034334665759104825, + 2.1171203886755963e-05, + -2.499898798243516e-05, + -0.00011374771539950262 + ], + [ + -3.455538550575027e-07, + -2.8874494077824097e-06, + 2.1171203886756332e-05, + 2.9241707740779787e-06, + -1.8547372175826365e-06, + -1.2779915037331006e-05 + ], + [ + -8.434305689856253e-06, + 1.4831323494680423e-07, + -2.4998987982435484e-05, + -1.854737217582637e-06, + 2.922658487863772e-06, + 1.1657487325670498e-05 + ], + [ + 1.331580397752139e-06, + 8.401000542325296e-06, + -0.00011374771539950486, + -1.2779915037331031e-05, + 1.1657487325670554e-05, + 7.504600233719707e-05 + ] + ], + "parameter_std": { + "rvec_std_deg": [ + 1.120151780762652, + 0.17284714323569167, + 1.06166877499303 + ], + "tvec_std_m": [ + 0.00171002069404963, + 0.001709578453263778, + 0.008662909576879875 + ] + }, + "camera_center_std_m": [ + 0.006227818662745683, + 0.020598334020338196, + 0.011112792063136126 + ], + "camera_center_std_mm": [ + 6.227818662745683, + 20.598334020338196, + 11.112792063136126 + ], + "orientation_std_deg": { + "roll": 1.3799127416085473, + "pitch": 0.5042660958147511, + "yaw": 0.1500547605224418 + } + } + }, + "observations": { + "markers": [ + { + "marker_id": 210, + "observed_center_px": [ + 168.5, + 660.5 + ], + "projected_center_px": [ + 169.5629425048828, + 658.793701171875 + ], + "reprojection_error_px": 2.01029909703688, + "confidence": 0.5673043275049208 + }, + { + "marker_id": 215, + "observed_center_px": [ + 548.5, + 487.5 + ], + "projected_center_px": [ + 550.717041015625, + 487.080810546875 + ], + "reprojection_error_px": 2.2563223755870454, + "confidence": 0.7952557920267838 + }, + { + "marker_id": 211, + "observed_center_px": [ + 455.0, + 424.25 + ], + "projected_center_px": [ + 450.42816162109375, + 425.7886047363281 + ], + "reprojection_error_px": 4.82379629519028, + "confidence": 0.6412317666518965 + }, + { + "marker_id": 208, + "observed_center_px": [ + 657.25, + 397.75 + ], + "projected_center_px": [ + 658.3648071289062, + 398.7890625 + ], + "reprojection_error_px": 1.5239572873169531, + "confidence": 0.6280424706698967 + }, + { + "marker_id": 217, + "observed_center_px": [ + 928.5, + 175.25 + ], + "projected_center_px": [ + 930.192626953125, + 175.83822631835938 + ], + "reprojection_error_px": 1.7919252785916733, + "confidence": 0.35596573288593486 + }, + { + "marker_id": 206, + "observed_center_px": [ + 839.25, + 131.25 + ], + "projected_center_px": [ + 836.46923828125, + 131.34689331054688 + ], + "reprojection_error_px": 2.7824492897614843, + "confidence": 0.33820423087105295 + }, + { + "marker_id": 205, + "observed_center_px": [ + 1004.5, + 113.0 + ], + "projected_center_px": [ + 1007.0062255859375, + 112.83639526367188 + ], + "reprojection_error_px": 2.5115599131529316, + "confidence": 0.28724459014346065 + }, + { + "marker_id": 207, + "observed_center_px": [ + 916.5, + 72.25 + ], + "projected_center_px": [ + 915.0281982421875, + 71.42831420898438 + ], + "reprojection_error_px": 1.6856357712913363, + "confidence": 0.27228561618555186 + } + ] + }, + "qa": { + "sanity_notes": [] + } +} \ No newline at end of file diff --git a/pipeline/render_1b.png b/pipeline/render_1b.png new file mode 100644 index 0000000..0b9bcaa Binary files /dev/null and b/pipeline/render_1b.png differ diff --git a/pipeline/render_1b_aruco_detection.json b/pipeline/render_1b_aruco_detection.json new file mode 100644 index 0000000..906aeb9 --- /dev/null +++ b/pipeline/render_1b_aruco_detection.json @@ -0,0 +1,6741 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T17:30:27Z", + "vision_config": { + "MarkerType": "DICT_4X4_250", + "MarkerSize": 0.025 + }, + "camera": { + "camera_id": "cam2", + "intrinsics_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render.npz", + "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 + ] + }, + "image": { + "image_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1b.png", + "image_sha256": "a390fad9e37792bf455b9ea46bc20ebbf8f4bdb0a0270bc341b0c39414ba14a9", + "width_px": 1280, + "height_px": 720 + }, + "aruco": { + "dictionary": "DICT_4X4_250", + "num_detected_markers": 10, + "num_rejected_candidates": 246 + }, + "detections": [ + { + "observation_id": "5ddda9bb-ef56-4319-b537-8f9450e1d44e", + "type": "aruco", + "marker_id": 102, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 758.0, + 527.0 + ], + [ + 824.0, + 522.0 + ], + [ + 824.0, + 581.0 + ], + [ + 758.0, + 586.0 + ] + ], + "center_px": [ + 791.0, + 554.0 + ], + "quality": { + "area_px": 3894.0, + "perimeter_px": 250.3782501220703, + "sharpness": { + "laplacian_var": 1982.7854057117818 + }, + "contrast": { + "p05": 18.0, + "p95": 185.0, + "dynamic_range": 167.0, + "mean_gray": 102.42052469135803, + "std_gray": 78.84797361336926 + }, + "geometry": { + "distance_to_center_norm": 0.3347931206226349, + "distance_to_border_px": 134.0 + }, + "edge_ratio": 1.1218495773056807, + "edge_lengths_px": [ + 66.18912506103516, + 59.0, + 66.18912506103516, + 59.0 + ] + }, + "confidence": 0.8913851020933449 + }, + { + "observation_id": "69e26460-7011-4a82-87f7-2231111352bb", + "type": "aruco", + "marker_id": 124, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 837.0, + 613.0 + ], + [ + 893.0, + 632.0 + ], + [ + 895.0, + 694.0 + ], + [ + 838.0, + 677.0 + ] + ], + "center_px": [ + 865.75, + 654.0 + ], + "quality": { + "area_px": 3532.5, + "perimeter_px": 244.65658950805664, + "sharpness": { + "laplacian_var": 1260.039697900723 + }, + "contrast": { + "p05": 7.0, + "p95": 149.0, + "dynamic_range": 142.0, + "mean_gray": 64.90349768225875, + "std_gray": 65.77948098307361 + }, + "geometry": { + "distance_to_center_norm": 0.5047972202301025, + "distance_to_border_px": 26.0 + }, + "edge_ratio": 1.0823934976132112, + "edge_lengths_px": [ + 59.13543701171875, + 62.032249450683594, + 59.4810905456543, + 64.0078125 + ] + }, + "confidence": 0.4804167810936165 + }, + { + "observation_id": "2bbb8f28-0738-4dfe-94d1-ac75e4f1f9b1", + "type": "aruco", + "marker_id": 243, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 568.0, + 219.0 + ], + [ + 626.0, + 221.0 + ], + [ + 625.0, + 274.0 + ], + [ + 568.0, + 272.0 + ] + ], + "center_px": [ + 596.75, + 246.5 + ], + "quality": { + "area_px": 3048.5, + "perimeter_px": 221.07898330688477, + "sharpness": { + "laplacian_var": 1452.2944966126818 + }, + "contrast": { + "p05": 38.0, + "p95": 191.0, + "dynamic_range": 153.0, + "mean_gray": 89.23336594911937, + "std_gray": 68.72982957783024 + }, + "geometry": { + "distance_to_center_norm": 0.16541028022766113, + "distance_to_border_px": 219.0 + }, + "edge_ratio": 1.0949900645130086, + "edge_lengths_px": [ + 58.03447341918945, + 53.00943374633789, + 57.03507614135742, + 53.0 + ] + }, + "confidence": 0.9132502955127223 + }, + { + "observation_id": "e7724220-b16f-4850-99b8-6e67cefa270d", + "type": "aruco", + "marker_id": 122, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 833.0, + 365.0 + ], + [ + 884.0, + 385.0 + ], + [ + 886.0, + 437.0 + ], + [ + 834.0, + 419.0 + ] + ], + "center_px": [ + 859.25, + 401.5 + ], + "quality": { + "area_px": 2701.0, + "perimeter_px": 215.8563575744629, + "sharpness": { + "laplacian_var": 1090.279514702366 + }, + "contrast": { + "p05": 7.0, + "p95": 147.0, + "dynamic_range": 140.0, + "mean_gray": 45.70519262981575, + "std_gray": 58.68517648892059 + }, + "geometry": { + "distance_to_center_norm": 0.3038843870162964, + "distance_to_border_px": 283.0 + }, + "edge_ratio": 1.0574348240198506, + "edge_lengths_px": [ + 54.7813835144043, + 52.038448333740234, + 55.02726745605469, + 54.00925827026367 + ] + }, + "confidence": 0.9456847621099602 + }, + { + "observation_id": "16df4158-c086-4f72-98f0-5993e1cb88f3", + "type": "aruco", + "marker_id": 247, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 696.0, + 145.0 + ], + [ + 753.0, + 148.0 + ], + [ + 753.0, + 192.0 + ], + [ + 694.0, + 189.0 + ] + ], + "center_px": [ + 724.0, + 168.5 + ], + "quality": { + "area_px": 2555.0, + "perimeter_px": 204.20054244995117, + "sharpness": { + "laplacian_var": 2174.5957776743603 + }, + "contrast": { + "p05": 10.0, + "p95": 175.0, + "dynamic_range": 165.0, + "mean_gray": 87.22021028037383, + "std_gray": 76.8486951831822 + }, + "geometry": { + "distance_to_center_norm": 0.28477779030799866, + "distance_to_border_px": 145.0 + }, + "edge_ratio": 1.3426413969560103, + "edge_lengths_px": [ + 57.07889175415039, + 44.0, + 59.07622146606445, + 44.04542922973633 + ] + }, + "confidence": 0.7448005120854795 + }, + { + "observation_id": "4960f97c-8e49-413e-a603-30a4e53f4426", + "type": "aruco", + "marker_id": 246, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 784.0, + 149.0 + ], + [ + 841.0, + 151.0 + ], + [ + 842.0, + 195.0 + ], + [ + 784.0, + 193.0 + ] + ], + "center_px": [ + 812.75, + 172.0 + ], + "quality": { + "area_px": 2529.0, + "perimeter_px": 203.08091354370117, + "sharpness": { + "laplacian_var": 1783.5126581780073 + }, + "contrast": { + "p05": 9.0, + "p95": 174.0, + "dynamic_range": 165.0, + "mean_gray": 56.36337039204213, + "std_gray": 69.80847051245013 + }, + "geometry": { + "distance_to_center_norm": 0.34769952297210693, + "distance_to_border_px": 149.0 + }, + "edge_ratio": 1.3189653049815784, + "edge_lengths_px": [ + 57.03507614135742, + 44.0113639831543, + 58.03447341918945, + 44.0 + ] + }, + "confidence": 0.7581700566520715 + }, + { + "observation_id": "cfc19188-1dd2-4b55-ab4b-e469e4798d29", + "type": "aruco", + "marker_id": 215, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 663.0, + 390.0 + ], + [ + 709.0, + 392.0 + ], + [ + 707.0, + 430.0 + ], + [ + 661.0, + 428.0 + ] + ], + "center_px": [ + 685.0, + 410.0 + ], + "quality": { + "area_px": 1752.0, + "perimeter_px": 168.19210815429688, + "sharpness": { + "laplacian_var": 2012.2572993055553 + }, + "contrast": { + "p05": 12.0, + "p95": 176.0, + "dynamic_range": 164.0, + "mean_gray": 73.73916666666666, + "std_gray": 73.94727490565303 + }, + "geometry": { + "distance_to_center_norm": 0.0916082039475441, + "distance_to_border_px": 290.0 + }, + "edge_ratio": 1.2099951279465397, + "edge_lengths_px": [ + 46.04345703125, + 38.05259704589844, + 46.04345703125, + 38.05259704589844 + ] + }, + "confidence": 0.826449608683203 + }, + { + "observation_id": "eab8eafe-5b76-49bf-b5e5-d387ca74653e", + "type": "aruco", + "marker_id": 210, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 260.0, + 270.0 + ], + [ + 302.0, + 272.0 + ], + [ + 296.0, + 306.0 + ], + [ + 252.0, + 305.0 + ] + ], + "center_px": [ + 277.5, + 288.25 + ], + "quality": { + "area_px": 1494.0, + "perimeter_px": 156.48695373535156, + "sharpness": { + "laplacian_var": 2476.0410248637572 + }, + "contrast": { + "p05": 15.0, + "p95": 180.0, + "dynamic_range": 165.0, + "mean_gray": 72.9696376101861, + "std_gray": 71.50378618336399 + }, + "geometry": { + "distance_to_center_norm": 0.5032430291175842, + "distance_to_border_px": 252.0 + }, + "edge_ratio": 1.274754950327127, + "edge_lengths_px": [ + 42.04759216308594, + 34.525352478027344, + 44.0113639831543, + 35.902645111083984 + ] + }, + "confidence": 0.7813266383036261 + }, + { + "observation_id": "c188d429-b96e-4678-a18d-353832db0997", + "type": "aruco", + "marker_id": 229, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 577.0, + 130.0 + ], + [ + 633.0, + 132.0 + ], + [ + 630.0, + 150.0 + ], + [ + 573.0, + 146.0 + ] + ], + "center_px": [ + 603.25, + 139.5 + ], + "quality": { + "area_px": 971.0, + "perimeter_px": 147.91658973693848, + "sharpness": { + "laplacian_var": 1392.1622913580247 + }, + "contrast": { + "p05": 20.0, + "p95": 138.0, + "dynamic_range": 118.0, + "mean_gray": 54.48, + "std_gray": 47.77305113570637 + }, + "geometry": { + "distance_to_center_norm": 0.30442705750465393, + "distance_to_border_px": 130.0 + }, + "edge_ratio": 3.4646323214690695, + "edge_lengths_px": [ + 56.035701751708984, + 18.248287200927734, + 57.14017868041992, + 16.492422103881836 + ] + }, + "confidence": 0.18684041285479083 + }, + { + "observation_id": "c962c6ac-9d98-4f2b-a3a3-0bbbcf32c7a6", + "type": "aruco", + "marker_id": 198, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 590.0, + 71.0 + ], + [ + 641.0, + 73.0 + ], + [ + 639.0, + 87.0 + ], + [ + 587.0, + 85.0 + ] + ], + "center_px": [ + 614.25, + 79.0 + ], + "quality": { + "area_px": 726.0, + "perimeter_px": 131.53760528564453, + "sharpness": { + "laplacian_var": 2340.5056446617955 + }, + "contrast": { + "p05": 22.0, + "p95": 135.0, + "dynamic_range": 113.0, + "mean_gray": 67.08382066276803, + "std_gray": 45.81817177643276 + }, + "geometry": { + "distance_to_center_norm": 0.3842795193195343, + "distance_to_border_px": 71.0 + }, + "edge_ratio": 3.6796739708616246, + "edge_lengths_px": [ + 51.03919982910156, + 14.142135620117188, + 52.038448333740234, + 14.317821502685547 + ] + }, + "confidence": 0.13153339231482716 + } + ], + "rejected_candidates": [ + { + "image_points_px": [ + [ + 59.0, + 668.0 + ], + [ + 97.0, + 669.0 + ], + [ + 89.0, + 705.0 + ], + [ + 50.0, + 703.0 + ] + ], + "center_px": [ + 73.75, + 686.25 + ], + "area_px": 1379.5 + }, + { + "image_points_px": [ + [ + 974.0, + 671.0 + ], + [ + 1012.0, + 673.0 + ], + [ + 1015.0, + 709.0 + ], + [ + 976.0, + 707.0 + ] + ], + "center_px": [ + 994.25, + 690.0 + ], + "area_px": 1381.0 + }, + { + "image_points_px": [ + [ + 217.0, + 675.0 + ], + [ + 255.0, + 677.0 + ], + [ + 249.0, + 712.0 + ], + [ + 210.0, + 711.0 + ] + ], + "center_px": [ + 232.75, + 693.75 + ], + "area_px": 1376.5 + }, + { + "image_points_px": [ + [ + 296.0, + 679.0 + ], + [ + 334.0, + 680.0 + ], + [ + 329.0, + 716.0 + ], + [ + 290.0, + 714.0 + ] + ], + "center_px": [ + 312.25, + 697.25 + ], + "area_px": 1375.0 + }, + { + "image_points_px": [ + [ + 420.0, + 647.0 + ], + [ + 458.0, + 649.0 + ], + [ + 454.0, + 684.0 + ], + [ + 415.0, + 682.0 + ] + ], + "center_px": [ + 436.75, + 665.5 + ], + "area_px": 1356.5 + }, + { + "image_points_px": [ + [ + 656.0, + 658.0 + ], + [ + 694.0, + 659.0 + ], + [ + 693.0, + 695.0 + ], + [ + 655.0, + 693.0 + ] + ], + "center_px": [ + 674.5, + 676.25 + ], + "area_px": 1350.5 + }, + { + "image_points_px": [ + [ + 29.0, + 630.0 + ], + [ + 67.0, + 631.0 + ], + [ + 58.0, + 666.0 + ], + [ + 21.0, + 665.0 + ] + ], + "center_px": [ + 43.75, + 648.0 + ], + "area_px": 1321.0 + }, + { + "image_points_px": [ + [ + 341.0, + 644.0 + ], + [ + 379.0, + 645.0 + ], + [ + 374.0, + 680.0 + ], + [ + 336.0, + 679.0 + ] + ], + "center_px": [ + 357.5, + 662.0 + ], + "area_px": 1335.0 + }, + { + "image_points_px": [ + [ + 498.0, + 651.0 + ], + [ + 536.0, + 652.0 + ], + [ + 533.0, + 687.0 + ], + [ + 495.0, + 686.0 + ] + ], + "center_px": [ + 515.5, + 669.0 + ], + "area_px": 1333.0 + }, + { + "image_points_px": [ + [ + 1011.0, + 637.0 + ], + [ + 1049.0, + 638.0 + ], + [ + 1052.0, + 673.0 + ], + [ + 1014.0, + 672.0 + ] + ], + "center_px": [ + 1031.5, + 655.0 + ], + "area_px": 1327.0 + }, + { + "image_points_px": [ + [ + 185.0, + 637.0 + ], + [ + 222.0, + 638.0 + ], + [ + 216.0, + 673.0 + ], + [ + 178.0, + 672.0 + ] + ], + "center_px": [ + 200.25, + 655.0 + ], + "area_px": 1319.0 + }, + { + "image_points_px": [ + [ + 577.0, + 654.0 + ], + [ + 615.0, + 656.0 + ], + [ + 613.0, + 691.0 + ], + [ + 575.0, + 689.0 + ] + ], + "center_px": [ + 595.0, + 672.5 + ], + "area_px": 1334.0 + }, + { + "image_points_px": [ + [ + 932.0, + 634.0 + ], + [ + 970.0, + 635.0 + ], + [ + 972.0, + 670.0 + ], + [ + 934.0, + 668.0 + ] + ], + "center_px": [ + 952.0, + 651.75 + ], + "area_px": 1308.0 + }, + { + "image_points_px": [ + [ + 1204.0, + 610.0 + ], + [ + 1241.0, + 611.0 + ], + [ + 1247.0, + 646.0 + ], + [ + 1209.0, + 644.0 + ] + ], + "center_px": [ + 1225.25, + 627.75 + ], + "area_px": 1285.5 + }, + { + "image_points_px": [ + [ + 1134.0, + 680.0 + ], + [ + 1171.0, + 680.0 + ], + [ + 1177.0, + 715.0 + ], + [ + 1139.0, + 714.0 + ] + ], + "center_px": [ + 1155.25, + 697.25 + ], + "area_px": 1291.0 + }, + { + "image_points_px": [ + [ + 463.0, + 613.0 + ], + [ + 501.0, + 615.0 + ], + [ + 497.0, + 649.0 + ], + [ + 459.0, + 647.0 + ] + ], + "center_px": [ + 480.0, + 631.0 + ], + "area_px": 1300.0 + }, + { + "image_points_px": [ + [ + 385.0, + 610.0 + ], + [ + 423.0, + 611.0 + ], + [ + 419.0, + 645.0 + ], + [ + 381.0, + 644.0 + ] + ], + "center_px": [ + 402.0, + 627.5 + ], + "area_px": 1296.0 + }, + { + "image_points_px": [ + [ + 996.0, + 251.0 + ], + [ + 1058.0, + 252.0 + ], + [ + 1065.0, + 259.0 + ], + [ + 1004.0, + 259.0 + ] + ], + "center_px": [ + 1030.75, + 255.25 + ], + "area_px": 457.5 + }, + { + "image_points_px": [ + [ + 619.0, + 620.0 + ], + [ + 656.0, + 621.0 + ], + [ + 655.0, + 656.0 + ], + [ + 617.0, + 654.0 + ] + ], + "center_px": [ + 636.75, + 637.75 + ], + "area_px": 1296.0 + }, + { + "image_points_px": [ + [ + 1126.0, + 606.0 + ], + [ + 1163.0, + 608.0 + ], + [ + 1168.0, + 642.0 + ], + [ + 1131.0, + 641.0 + ] + ], + "center_px": [ + 1147.0, + 624.25 + ], + "area_px": 1269.0 + }, + { + "image_points_px": [ + [ + 1054.0, + 676.0 + ], + [ + 1091.0, + 677.0 + ], + [ + 1096.0, + 711.0 + ], + [ + 1059.0, + 711.0 + ] + ], + "center_px": [ + 1075.0, + 693.75 + ], + "area_px": 1274.0 + }, + { + "image_points_px": [ + [ + 154.0, + 600.0 + ], + [ + 191.0, + 601.0 + ], + [ + 184.0, + 635.0 + ], + [ + 147.0, + 634.0 + ] + ], + "center_px": [ + 169.0, + 617.5 + ], + "area_px": 1265.0 + }, + { + "image_points_px": [ + [ + 309.0, + 606.0 + ], + [ + 345.0, + 608.0 + ], + [ + 340.0, + 642.0 + ], + [ + 302.0, + 640.0 + ] + ], + "center_px": [ + 324.0, + 624.0 + ], + "area_px": 1270.0 + }, + { + "image_points_px": [ + [ + 139.0, + 672.0 + ], + [ + 175.0, + 674.0 + ], + [ + 167.0, + 709.0 + ], + [ + 131.0, + 706.0 + ] + ], + "center_px": [ + 153.0, + 690.25 + ], + "area_px": 1262.0 + }, + { + "image_points_px": [ + [ + 1160.0, + 574.0 + ], + [ + 1198.0, + 575.0 + ], + [ + 1202.0, + 608.0 + ], + [ + 1165.0, + 607.0 + ] + ], + "center_px": [ + 1181.25, + 591.0 + ], + "area_px": 1233.0 + }, + { + "image_points_px": [ + [ + 1091.0, + 641.0 + ], + [ + 1127.0, + 642.0 + ], + [ + 1132.0, + 676.0 + ], + [ + 1095.0, + 675.0 + ] + ], + "center_px": [ + 1111.25, + 658.5 + ], + "area_px": 1236.5 + }, + { + "image_points_px": [ + [ + 429.0, + 576.0 + ], + [ + 466.0, + 578.0 + ], + [ + 462.0, + 611.0 + ], + [ + 425.0, + 610.0 + ] + ], + "center_px": [ + 445.5, + 593.75 + ], + "area_px": 1245.5 + }, + { + "image_points_px": [ + [ + 1083.0, + 570.0 + ], + [ + 1120.0, + 572.0 + ], + [ + 1124.0, + 605.0 + ], + [ + 1086.0, + 603.0 + ] + ], + "center_px": [ + 1103.25, + 587.5 + ], + "area_px": 1230.5 + }, + { + "image_points_px": [ + [ + 929.0, + 563.0 + ], + [ + 966.0, + 565.0 + ], + [ + 968.0, + 598.0 + ], + [ + 931.0, + 597.0 + ] + ], + "center_px": [ + 948.5, + 580.75 + ], + "area_px": 1236.5 + }, + { + "image_points_px": [ + [ + 1170.0, + 645.0 + ], + [ + 1206.0, + 645.0 + ], + [ + 1212.0, + 679.0 + ], + [ + 1176.0, + 679.0 + ] + ], + "center_px": [ + 1191.0, + 662.0 + ], + "area_px": 1224.0 + }, + { + "image_points_px": [ + [ + 276.0, + 570.0 + ], + [ + 312.0, + 571.0 + ], + [ + 307.0, + 605.0 + ], + [ + 270.0, + 603.0 + ] + ], + "center_px": [ + 291.25, + 587.25 + ], + "area_px": 1231.0 + }, + { + "image_points_px": [ + [ + 124.0, + 564.0 + ], + [ + 160.0, + 565.0 + ], + [ + 153.0, + 598.0 + ], + [ + 116.0, + 597.0 + ] + ], + "center_px": [ + 138.25, + 581.0 + ], + "area_px": 1212.0 + }, + { + "image_points_px": [ + [ + 264.0, + 641.0 + ], + [ + 300.0, + 643.0 + ], + [ + 293.0, + 677.0 + ], + [ + 257.0, + 674.0 + ] + ], + "center_px": [ + 278.5, + 658.75 + ], + "area_px": 1223.5 + }, + { + "image_points_px": [ + [ + 108.0, + 634.0 + ], + [ + 144.0, + 636.0 + ], + [ + 135.0, + 670.0 + ], + [ + 100.0, + 667.0 + ] + ], + "center_px": [ + 121.75, + 651.75 + ], + "area_px": 1210.5 + }, + { + "image_points_px": [ + [ + 200.0, + 567.0 + ], + [ + 236.0, + 568.0 + ], + [ + 230.0, + 601.0 + ], + [ + 193.0, + 600.0 + ] + ], + "center_px": [ + 214.75, + 584.0 + ], + "area_px": 1211.0 + }, + { + "image_points_px": [ + [ + 78.0, + 597.0 + ], + [ + 114.0, + 599.0 + ], + [ + 105.0, + 632.0 + ], + [ + 69.0, + 629.0 + ] + ], + "center_px": [ + 91.5, + 614.25 + ], + "area_px": 1192.5 + }, + { + "image_points_px": [ + [ + 1118.0, + 538.0 + ], + [ + 1154.0, + 539.0 + ], + [ + 1159.0, + 572.0 + ], + [ + 1121.0, + 570.0 + ] + ], + "center_px": [ + 1138.0, + 554.75 + ], + "area_px": 1196.5 + }, + { + "image_points_px": [ + [ + 1194.0, + 541.0 + ], + [ + 1231.0, + 543.0 + ], + [ + 1236.0, + 575.0 + ], + [ + 1199.0, + 573.0 + ] + ], + "center_px": [ + 1215.0, + 558.0 + ], + "area_px": 1174.0 + }, + { + "image_points_px": [ + [ + 1227.0, + 509.0 + ], + [ + 1264.0, + 511.0 + ], + [ + 1269.0, + 543.0 + ], + [ + 1232.0, + 541.0 + ] + ], + "center_px": [ + 1248.0, + 526.0 + ], + "area_px": 1174.0 + }, + { + "image_points_px": [ + [ + 232.0, + 603.0 + ], + [ + 268.0, + 606.0 + ], + [ + 261.0, + 638.0 + ], + [ + 225.0, + 636.0 + ] + ], + "center_px": [ + 246.5, + 620.75 + ], + "area_px": 1187.5 + }, + { + "image_points_px": [ + [ + 169.0, + 532.0 + ], + [ + 205.0, + 533.0 + ], + [ + 199.0, + 565.0 + ], + [ + 162.0, + 564.0 + ] + ], + "center_px": [ + 183.75, + 548.5 + ], + "area_px": 1174.5 + }, + { + "image_points_px": [ + [ + 19.0, + 525.0 + ], + [ + 55.0, + 526.0 + ], + [ + 46.0, + 559.0 + ], + [ + 11.0, + 557.0 + ] + ], + "center_px": [ + 32.75, + 541.75 + ], + "area_px": 1166.5 + }, + { + "image_points_px": [ + [ + 623.0, + 551.0 + ], + [ + 659.0, + 552.0 + ], + [ + 658.0, + 585.0 + ], + [ + 621.0, + 583.0 + ] + ], + "center_px": [ + 640.25, + 567.75 + ], + "area_px": 1188.5 + }, + { + "image_points_px": [ + [ + 541.0, + 618.0 + ], + [ + 577.0, + 618.0 + ], + [ + 576.0, + 651.0 + ], + [ + 540.0, + 651.0 + ] + ], + "center_px": [ + 558.5, + 634.5 + ], + "area_px": 1188.0 + }, + { + "image_points_px": [ + [ + 244.0, + 535.0 + ], + [ + 280.0, + 536.0 + ], + [ + 275.0, + 568.0 + ], + [ + 238.0, + 567.0 + ] + ], + "center_px": [ + 259.25, + 551.5 + ], + "area_px": 1173.5 + }, + { + "image_points_px": [ + [ + 395.0, + 541.0 + ], + [ + 432.0, + 543.0 + ], + [ + 427.0, + 575.0 + ], + [ + 391.0, + 573.0 + ] + ], + "center_px": [ + 411.25, + 558.0 + ], + "area_px": 1177.0 + }, + { + "image_points_px": [ + [ + 1048.0, + 604.0 + ], + [ + 1084.0, + 605.0 + ], + [ + 1088.0, + 637.0 + ], + [ + 1052.0, + 637.0 + ] + ], + "center_px": [ + 1068.0, + 620.75 + ], + "area_px": 1168.0 + }, + { + "image_points_px": [ + [ + 970.0, + 601.0 + ], + [ + 1006.0, + 601.0 + ], + [ + 1009.0, + 634.0 + ], + [ + 973.0, + 633.0 + ] + ], + "center_px": [ + 989.5, + 617.25 + ], + "area_px": 1168.5 + }, + { + "image_points_px": [ + [ + 965.0, + 532.0 + ], + [ + 1002.0, + 533.0 + ], + [ + 1004.0, + 565.0 + ], + [ + 968.0, + 564.0 + ] + ], + "center_px": [ + 984.75, + 548.5 + ], + "area_px": 1165.5 + }, + { + "image_points_px": [ + [ + 288.0, + 503.0 + ], + [ + 324.0, + 505.0 + ], + [ + 319.0, + 536.0 + ], + [ + 282.0, + 535.0 + ] + ], + "center_px": [ + 303.25, + 519.75 + ], + "area_px": 1158.0 + }, + { + "image_points_px": [ + [ + 139.0, + 497.0 + ], + [ + 175.0, + 498.0 + ], + [ + 168.0, + 530.0 + ], + [ + 132.0, + 528.0 + ] + ], + "center_px": [ + 153.5, + 513.25 + ], + "area_px": 1144.5 + }, + { + "image_points_px": [ + [ + 587.0, + 516.0 + ], + [ + 623.0, + 517.0 + ], + [ + 622.0, + 549.0 + ], + [ + 585.0, + 547.0 + ] + ], + "center_px": [ + 604.25, + 532.25 + ], + "area_px": 1152.0 + }, + { + "image_points_px": [ + [ + 49.0, + 561.0 + ], + [ + 84.0, + 563.0 + ], + [ + 75.0, + 595.0 + ], + [ + 40.0, + 592.0 + ] + ], + "center_px": [ + 62.0, + 577.75 + ], + "area_px": 1125.0 + }, + { + "image_points_px": [ + [ + 1006.0, + 568.0 + ], + [ + 1041.0, + 568.0 + ], + [ + 1046.0, + 600.0 + ], + [ + 1010.0, + 600.0 + ] + ], + "center_px": [ + 1025.75, + 584.0 + ], + "area_px": 1136.0 + }, + { + "image_points_px": [ + [ + 1152.0, + 506.0 + ], + [ + 1188.0, + 508.0 + ], + [ + 1192.0, + 539.0 + ], + [ + 1156.0, + 538.0 + ] + ], + "center_px": [ + 1172.0, + 522.75 + ], + "area_px": 1128.0 + }, + { + "image_points_px": [ + [ + 1001.0, + 500.0 + ], + [ + 1036.0, + 501.0 + ], + [ + 1040.0, + 533.0 + ], + [ + 1004.0, + 532.0 + ] + ], + "center_px": [ + 1020.25, + 516.5 + ], + "area_px": 1132.5 + }, + { + "image_points_px": [ + [ + 214.0, + 500.0 + ], + [ + 249.0, + 501.0 + ], + [ + 243.0, + 533.0 + ], + [ + 207.0, + 531.0 + ] + ], + "center_px": [ + 228.25, + 516.25 + ], + "area_px": 1128.0 + }, + { + "image_points_px": [ + [ + 512.0, + 513.0 + ], + [ + 548.0, + 514.0 + ], + [ + 546.0, + 545.0 + ], + [ + 509.0, + 544.0 + ] + ], + "center_px": [ + 528.75, + 529.0 + ], + "area_px": 1134.0 + }, + { + "image_points_px": [ + [ + 926.0, + 497.0 + ], + [ + 961.0, + 498.0 + ], + [ + 964.0, + 530.0 + ], + [ + 927.0, + 528.0 + ] + ], + "center_px": [ + 944.5, + 513.25 + ], + "area_px": 1131.0 + }, + { + "image_points_px": [ + [ + 353.0, + 574.0 + ], + [ + 389.0, + 576.0 + ], + [ + 383.0, + 608.0 + ], + [ + 348.0, + 605.0 + ] + ], + "center_px": [ + 368.25, + 590.75 + ], + "area_px": 1132.0 + }, + { + "image_points_px": [ + [ + 582.0, + 585.0 + ], + [ + 618.0, + 585.0 + ], + [ + 617.0, + 617.0 + ], + [ + 581.0, + 616.0 + ] + ], + "center_px": [ + 599.5, + 600.75 + ], + "area_px": 1134.5 + }, + { + "image_points_px": [ + [ + 660.0, + 588.0 + ], + [ + 695.0, + 588.0 + ], + [ + 695.0, + 620.0 + ], + [ + 659.0, + 620.0 + ] + ], + "center_px": [ + 677.25, + 604.0 + ], + "area_px": 1136.0 + }, + { + "image_points_px": [ + [ + 507.0, + 580.0 + ], + [ + 542.0, + 583.0 + ], + [ + 538.0, + 615.0 + ], + [ + 503.0, + 612.0 + ] + ], + "center_px": [ + 522.5, + 597.5 + ], + "area_px": 1132.0 + }, + { + "image_points_px": [ + [ + 363.0, + 506.0 + ], + [ + 398.0, + 508.0 + ], + [ + 394.0, + 539.0 + ], + [ + 358.0, + 538.0 + ] + ], + "center_px": [ + 378.25, + 522.75 + ], + "area_px": 1125.0 + }, + { + "image_points_px": [ + [ + 553.0, + 481.0 + ], + [ + 588.0, + 483.0 + ], + [ + 586.0, + 514.0 + ], + [ + 550.0, + 513.0 + ] + ], + "center_px": [ + 569.25, + 497.75 + ], + "area_px": 1122.0 + }, + { + "image_points_px": [ + [ + 627.0, + 485.0 + ], + [ + 663.0, + 486.0 + ], + [ + 661.0, + 517.0 + ], + [ + 625.0, + 516.0 + ] + ], + "center_px": [ + 644.0, + 501.0 + ], + "area_px": 1118.0 + }, + { + "image_points_px": [ + [ + 405.0, + 475.0 + ], + [ + 440.0, + 477.0 + ], + [ + 436.0, + 508.0 + ], + [ + 400.0, + 506.0 + ] + ], + "center_px": [ + 420.25, + 491.5 + ], + "area_px": 1109.5 + }, + { + "image_points_px": [ + [ + 184.0, + 466.0 + ], + [ + 219.0, + 468.0 + ], + [ + 213.0, + 498.0 + ], + [ + 177.0, + 497.0 + ] + ], + "center_px": [ + 198.25, + 482.25 + ], + "area_px": 1092.5 + }, + { + "image_points_px": [ + [ + 478.0, + 479.0 + ], + [ + 514.0, + 480.0 + ], + [ + 511.0, + 511.0 + ], + [ + 475.0, + 509.0 + ] + ], + "center_px": [ + 494.5, + 494.75 + ], + "area_px": 1102.5 + }, + { + "image_points_px": [ + [ + 471.0, + 545.0 + ], + [ + 506.0, + 546.0 + ], + [ + 504.0, + 577.0 + ], + [ + 469.0, + 577.0 + ] + ], + "center_px": [ + 487.5, + 561.25 + ], + "area_px": 1103.5 + }, + { + "image_points_px": [ + [ + 547.0, + 548.0 + ], + [ + 582.0, + 549.0 + ], + [ + 581.0, + 580.0 + ], + [ + 546.0, + 580.0 + ] + ], + "center_px": [ + 564.0, + 564.25 + ], + "area_px": 1103.0 + }, + { + "image_points_px": [ + [ + 331.0, + 472.0 + ], + [ + 366.0, + 474.0 + ], + [ + 361.0, + 505.0 + ], + [ + 326.0, + 503.0 + ] + ], + "center_px": [ + 346.0, + 488.5 + ], + "area_px": 1095.0 + }, + { + "image_points_px": [ + [ + 96.0, + 528.0 + ], + [ + 130.0, + 531.0 + ], + [ + 121.0, + 562.0 + ], + [ + 87.0, + 559.0 + ] + ], + "center_px": [ + 108.5, + 545.0 + ], + "area_px": 1081.0 + }, + { + "image_points_px": [ + [ + 1042.0, + 536.0 + ], + [ + 1077.0, + 536.0 + ], + [ + 1081.0, + 567.0 + ], + [ + 1046.0, + 567.0 + ] + ], + "center_px": [ + 1061.5, + 551.5 + ], + "area_px": 1085.0 + }, + { + "image_points_px": [ + [ + 38.0, + 460.0 + ], + [ + 72.0, + 462.0 + ], + [ + 64.0, + 492.0 + ], + [ + 29.0, + 491.0 + ] + ], + "center_px": [ + 50.75, + 476.25 + ], + "area_px": 1065.0 + }, + { + "image_points_px": [ + [ + 258.0, + 469.0 + ], + [ + 292.0, + 471.0 + ], + [ + 287.0, + 501.0 + ], + [ + 251.0, + 500.0 + ] + ], + "center_px": [ + 272.0, + 485.25 + ], + "area_px": 1076.5 + }, + { + "image_points_px": [ + [ + 321.0, + 538.0 + ], + [ + 355.0, + 540.0 + ], + [ + 350.0, + 571.0 + ], + [ + 315.0, + 569.0 + ] + ], + "center_px": [ + 335.25, + 554.5 + ], + "area_px": 1080.5 + }, + { + "image_points_px": [ + [ + 111.0, + 463.0 + ], + [ + 145.0, + 465.0 + ], + [ + 138.0, + 495.0 + ], + [ + 103.0, + 494.0 + ] + ], + "center_px": [ + 124.25, + 479.25 + ], + "area_px": 1063.5 + }, + { + "image_points_px": [ + [ + 83.0, + 430.0 + ], + [ + 117.0, + 432.0 + ], + [ + 109.0, + 462.0 + ], + [ + 74.0, + 460.0 + ] + ], + "center_px": [ + 95.75, + 446.0 + ], + "area_px": 1052.0 + }, + { + "image_points_px": [ + [ + 155.0, + 433.0 + ], + [ + 189.0, + 435.0 + ], + [ + 182.0, + 465.0 + ], + [ + 147.0, + 463.0 + ] + ], + "center_px": [ + 168.25, + 449.0 + ], + "area_px": 1050.0 + }, + { + "image_points_px": [ + [ + 1077.0, + 504.0 + ], + [ + 1111.0, + 505.0 + ], + [ + 1116.0, + 535.0 + ], + [ + 1081.0, + 535.0 + ] + ], + "center_px": [ + 1096.25, + 519.75 + ], + "area_px": 1050.0 + }, + { + "image_points_px": [ + [ + 66.0, + 494.0 + ], + [ + 100.0, + 496.0 + ], + [ + 92.0, + 526.0 + ], + [ + 58.0, + 524.0 + ] + ], + "center_px": [ + 79.0, + 510.0 + ], + "area_px": 1036.0 + }, + { + "image_points_px": [ + [ + 437.0, + 511.0 + ], + [ + 472.0, + 511.0 + ], + [ + 470.0, + 541.0 + ], + [ + 435.0, + 541.0 + ] + ], + "center_px": [ + 453.5, + 526.0 + ], + "area_px": 1050.0 + }, + { + "image_points_px": [ + [ + 663.0, + 520.0 + ], + [ + 697.0, + 520.0 + ], + [ + 698.0, + 550.0 + ], + [ + 663.0, + 551.0 + ] + ], + "center_px": [ + 680.25, + 535.25 + ], + "area_px": 1052.5 + }, + { + "image_points_px": [ + [ + 55.0, + 398.0 + ], + [ + 89.0, + 400.0 + ], + [ + 81.0, + 429.0 + ], + [ + 46.0, + 427.0 + ] + ], + "center_px": [ + 67.75, + 413.5 + ], + "area_px": 1017.5 + }, + { + "image_points_px": [ + [ + 126.0, + 401.0 + ], + [ + 160.0, + 403.0 + ], + [ + 153.0, + 432.0 + ], + [ + 119.0, + 430.0 + ] + ], + "center_px": [ + 139.5, + 416.5 + ], + "area_px": 1000.0 + }, + { + "image_points_px": [ + [ + 27.0, + 367.0 + ], + [ + 61.0, + 368.0 + ], + [ + 53.0, + 397.0 + ], + [ + 19.0, + 395.0 + ] + ], + "center_px": [ + 40.0, + 381.75 + ], + "area_px": 981.0 + }, + { + "image_points_px": [ + [ + 525.0, + 387.0 + ], + [ + 559.0, + 388.0 + ], + [ + 557.0, + 417.0 + ], + [ + 522.0, + 416.0 + ] + ], + "center_px": [ + 540.75, + 402.0 + ], + "area_px": 1003.0 + }, + { + "image_points_px": [ + [ + 382.0, + 381.0 + ], + [ + 416.0, + 382.0 + ], + [ + 412.0, + 411.0 + ], + [ + 378.0, + 410.0 + ] + ], + "center_px": [ + 397.0, + 396.0 + ], + "area_px": 990.0 + }, + { + "image_points_px": [ + [ + 492.0, + 356.0 + ], + [ + 527.0, + 357.0 + ], + [ + 524.0, + 385.0 + ], + [ + 489.0, + 384.0 + ] + ], + "center_px": [ + 508.0, + 370.5 + ], + "area_px": 983.0 + }, + { + "image_points_px": [ + [ + 98.0, + 370.0 + ], + [ + 132.0, + 371.0 + ], + [ + 124.0, + 400.0 + ], + [ + 91.0, + 398.0 + ] + ], + "center_px": [ + 111.25, + 384.75 + ], + "area_px": 966.0 + }, + { + "image_points_px": [ + [ + 169.0, + 373.0 + ], + [ + 203.0, + 374.0 + ], + [ + 196.0, + 402.0 + ], + [ + 162.0, + 401.0 + ] + ], + "center_px": [ + 182.5, + 387.5 + ], + "area_px": 959.0 + }, + { + "image_points_px": [ + [ + 71.0, + 339.0 + ], + [ + 104.0, + 340.0 + ], + [ + 97.0, + 368.0 + ], + [ + 63.0, + 367.0 + ] + ], + "center_px": [ + 83.75, + 353.5 + ], + "area_px": 945.5 + }, + { + "image_points_px": [ + [ + 141.0, + 342.0 + ], + [ + 174.0, + 343.0 + ], + [ + 168.0, + 371.0 + ], + [ + 134.0, + 370.0 + ] + ], + "center_px": [ + 154.25, + 356.5 + ], + "area_px": 944.5 + }, + { + "image_points_px": [ + [ + 421.0, + 353.0 + ], + [ + 455.0, + 354.0 + ], + [ + 452.0, + 382.0 + ], + [ + 418.0, + 381.0 + ] + ], + "center_px": [ + 436.5, + 367.5 + ], + "area_px": 955.0 + }, + { + "image_points_px": [ + [ + 563.0, + 359.0 + ], + [ + 598.0, + 361.0 + ], + [ + 595.0, + 388.0 + ], + [ + 561.0, + 387.0 + ] + ], + "center_px": [ + 579.25, + 373.75 + ], + "area_px": 952.5 + }, + { + "image_points_px": [ + [ + 350.0, + 353.0 + ], + [ + 386.0, + 352.0 + ], + [ + 381.0, + 379.0 + ], + [ + 347.0, + 378.0 + ] + ], + "center_px": [ + 366.0, + 365.5 + ], + "area_px": 910.0 + }, + { + "image_points_px": [ + [ + 183.0, + 315.0 + ], + [ + 216.0, + 316.0 + ], + [ + 210.0, + 343.0 + ], + [ + 176.0, + 342.0 + ] + ], + "center_px": [ + 196.25, + 329.0 + ], + "area_px": 911.0 + }, + { + "image_points_px": [ + [ + 113.0, + 312.0 + ], + [ + 146.0, + 313.0 + ], + [ + 140.0, + 340.0 + ], + [ + 106.0, + 339.0 + ] + ], + "center_px": [ + 126.25, + 326.0 + ], + "area_px": 911.0 + }, + { + "image_points_px": [ + [ + 18.0, + 280.0 + ], + [ + 51.0, + 281.0 + ], + [ + 43.0, + 308.0 + ], + [ + 10.0, + 307.0 + ] + ], + "center_px": [ + 30.5, + 294.0 + ], + "area_px": 899.0 + }, + { + "image_points_px": [ + [ + 453.0, + 385.0 + ], + [ + 486.0, + 385.0 + ], + [ + 484.0, + 413.0 + ], + [ + 451.0, + 413.0 + ] + ], + "center_px": [ + 468.5, + 399.0 + ], + "area_px": 924.0 + }, + { + "image_points_px": [ + [ + 1136.0, + 383.0 + ], + [ + 1169.0, + 383.0 + ], + [ + 1174.0, + 410.0 + ], + [ + 1140.0, + 410.0 + ] + ], + "center_px": [ + 1154.75, + 396.5 + ], + "area_px": 904.5 + }, + { + "image_points_px": [ + [ + 45.0, + 309.0 + ], + [ + 77.0, + 311.0 + ], + [ + 69.0, + 338.0 + ], + [ + 36.0, + 336.0 + ] + ], + "center_px": [ + 56.75, + 323.5 + ], + "area_px": 894.5 + }, + { + "image_points_px": [ + [ + 1064.0, + 380.0 + ], + [ + 1097.0, + 380.0 + ], + [ + 1101.0, + 407.0 + ], + [ + 1067.0, + 407.0 + ] + ], + "center_px": [ + 1082.25, + 393.5 + ], + "area_px": 904.5 + }, + { + "image_points_px": [ + [ + 155.0, + 285.0 + ], + [ + 187.0, + 286.0 + ], + [ + 181.0, + 313.0 + ], + [ + 148.0, + 312.0 + ] + ], + "center_px": [ + 167.75, + 299.0 + ], + "area_px": 884.0 + }, + { + "image_points_px": [ + [ + 86.0, + 283.0 + ], + [ + 119.0, + 284.0 + ], + [ + 112.0, + 310.0 + ], + [ + 79.0, + 309.0 + ] + ], + "center_px": [ + 99.0, + 296.5 + ], + "area_px": 865.0 + }, + { + "image_points_px": [ + [ + 196.0, + 259.0 + ], + [ + 228.0, + 260.0 + ], + [ + 222.0, + 286.0 + ], + [ + 189.0, + 285.0 + ] + ], + "center_px": [ + 208.75, + 272.5 + ], + "area_px": 851.5 + }, + { + "image_points_px": [ + [ + 992.0, + 384.0 + ], + [ + 1026.0, + 377.0 + ], + [ + 1028.0, + 406.0 + ], + [ + 994.0, + 404.0 + ] + ], + "center_px": [ + 1010.0, + 392.75 + ], + "area_px": 838.0 + }, + { + "image_points_px": [ + [ + 128.0, + 257.0 + ], + [ + 160.0, + 258.0 + ], + [ + 153.0, + 284.0 + ], + [ + 121.0, + 282.0 + ] + ], + "center_px": [ + 140.5, + 270.25 + ], + "area_px": 826.5 + }, + { + "image_points_px": [ + [ + 56.0, + 150.0 + ], + [ + 89.0, + 149.0 + ], + [ + 84.0, + 173.0 + ], + [ + 50.0, + 174.0 + ] + ], + "center_px": [ + 69.75, + 161.5 + ], + "area_px": 798.5 + }, + { + "image_points_px": [ + [ + 168.0, + 231.0 + ], + [ + 200.0, + 232.0 + ], + [ + 195.0, + 257.0 + ], + [ + 162.0, + 256.0 + ] + ], + "center_px": [ + 181.25, + 244.0 + ], + "area_px": 818.0 + }, + { + "image_points_px": [ + [ + 35.0, + 226.0 + ], + [ + 66.0, + 227.0 + ], + [ + 59.0, + 252.0 + ], + [ + 27.0, + 251.0 + ] + ], + "center_px": [ + 46.75, + 239.0 + ], + "area_px": 795.0 + }, + { + "image_points_px": [ + [ + 76.0, + 201.0 + ], + [ + 107.0, + 202.0 + ], + [ + 100.0, + 227.0 + ], + [ + 69.0, + 226.0 + ] + ], + "center_px": [ + 88.0, + 214.0 + ], + "area_px": 782.0 + }, + { + "image_points_px": [ + [ + 181.0, + 179.0 + ], + [ + 213.0, + 180.0 + ], + [ + 207.0, + 204.0 + ], + [ + 175.0, + 203.0 + ] + ], + "center_px": [ + 194.0, + 191.5 + ], + "area_px": 774.0 + }, + { + "image_points_px": [ + [ + 116.0, + 177.0 + ], + [ + 147.0, + 178.0 + ], + [ + 141.0, + 202.0 + ], + [ + 109.0, + 201.0 + ] + ], + "center_px": [ + 128.25, + 189.5 + ], + "area_px": 762.5 + }, + { + "image_points_px": [ + [ + 142.0, + 204.0 + ], + [ + 173.0, + 205.0 + ], + [ + 168.0, + 228.0 + ], + [ + 135.0, + 228.0 + ] + ], + "center_px": [ + 154.5, + 216.25 + ], + "area_px": 755.0 + }, + { + "image_points_px": [ + [ + 60.0, + 255.0 + ], + [ + 91.0, + 255.0 + ], + [ + 86.0, + 279.0 + ], + [ + 55.0, + 280.0 + ] + ], + "center_px": [ + 73.0, + 267.25 + ], + "area_px": 757.0 + }, + { + "image_points_px": [ + [ + 101.0, + 230.0 + ], + [ + 132.0, + 230.0 + ], + [ + 127.0, + 254.0 + ], + [ + 96.0, + 254.0 + ] + ], + "center_px": [ + 114.0, + 242.0 + ], + "area_px": 744.0 + }, + { + "image_points_px": [ + [ + 1230.0, + 117.0 + ], + [ + 1261.0, + 119.0 + ], + [ + 1266.0, + 142.0 + ], + [ + 1235.0, + 141.0 + ] + ], + "center_px": [ + 1248.0, + 129.75 + ], + "area_px": 721.0 + }, + { + "image_points_px": [ + [ + 65.0, + 125.0 + ], + [ + 96.0, + 125.0 + ], + [ + 89.0, + 149.0 + ], + [ + 59.0, + 148.0 + ] + ], + "center_px": [ + 77.25, + 136.75 + ], + "area_px": 720.0 + }, + { + "image_points_px": [ + [ + 1202.0, + 141.0 + ], + [ + 1233.0, + 142.0 + ], + [ + 1237.0, + 166.0 + ], + [ + 1206.0, + 164.0 + ] + ], + "center_px": [ + 1219.5, + 153.25 + ], + "area_px": 722.5 + }, + { + "image_points_px": [ + [ + 155.0, + 153.0 + ], + [ + 186.0, + 154.0 + ], + [ + 181.0, + 176.0 + ], + [ + 149.0, + 176.0 + ] + ], + "center_px": [ + 167.75, + 164.75 + ], + "area_px": 711.5 + }, + { + "image_points_px": [ + [ + 902.0, + 36.0 + ], + [ + 933.0, + 36.0 + ], + [ + 936.0, + 59.0 + ], + [ + 904.0, + 59.0 + ] + ], + "center_px": [ + 918.75, + 47.5 + ], + "area_px": 724.5 + }, + { + "image_points_px": [ + [ + 1165.0, + 115.0 + ], + [ + 1196.0, + 116.0 + ], + [ + 1200.0, + 139.0 + ], + [ + 1169.0, + 138.0 + ] + ], + "center_px": [ + 1182.5, + 127.0 + ], + "area_px": 709.0 + }, + { + "image_points_px": [ + [ + 129.0, + 128.0 + ], + [ + 160.0, + 128.0 + ], + [ + 154.0, + 151.0 + ], + [ + 123.0, + 150.0 + ] + ], + "center_px": [ + 141.5, + 139.25 + ], + "area_px": 700.5 + }, + { + "image_points_px": [ + [ + 232.0, + 106.0 + ], + [ + 262.0, + 107.0 + ], + [ + 257.0, + 130.0 + ], + [ + 226.0, + 129.0 + ] + ], + "center_px": [ + 244.25, + 118.0 + ], + "area_px": 707.0 + }, + { + "image_points_px": [ + [ + 1006.0, + 134.0 + ], + [ + 1037.0, + 135.0 + ], + [ + 1039.0, + 158.0 + ], + [ + 1008.0, + 157.0 + ] + ], + "center_px": [ + 1022.5, + 146.0 + ], + "area_px": 711.0 + }, + { + "image_points_px": [ + [ + 295.0, + 109.0 + ], + [ + 326.0, + 109.0 + ], + [ + 322.0, + 132.0 + ], + [ + 291.0, + 131.0 + ] + ], + "center_px": [ + 308.5, + 120.25 + ], + "area_px": 699.5 + }, + { + "image_points_px": [ + [ + 104.0, + 102.0 + ], + [ + 135.0, + 103.0 + ], + [ + 129.0, + 125.0 + ], + [ + 98.0, + 124.0 + ] + ], + "center_px": [ + 116.5, + 113.5 + ], + "area_px": 688.0 + }, + { + "image_points_px": [ + [ + 1036.0, + 111.0 + ], + [ + 1067.0, + 112.0 + ], + [ + 1069.0, + 135.0 + ], + [ + 1039.0, + 134.0 + ] + ], + "center_px": [ + 1052.75, + 123.0 + ], + "area_px": 699.0 + }, + { + "image_points_px": [ + [ + 971.0, + 108.0 + ], + [ + 1001.0, + 109.0 + ], + [ + 1004.0, + 132.0 + ], + [ + 973.0, + 131.0 + ] + ], + "center_px": [ + 987.25, + 120.0 + ], + "area_px": 699.0 + }, + { + "image_points_px": [ + [ + 1001.0, + 86.0 + ], + [ + 1032.0, + 87.0 + ], + [ + 1034.0, + 109.0 + ], + [ + 1003.0, + 108.0 + ] + ], + "center_px": [ + 1017.5, + 97.5 + ], + "area_px": 680.0 + }, + { + "image_points_px": [ + [ + 168.0, + 104.0 + ], + [ + 197.0, + 105.0 + ], + [ + 193.0, + 127.0 + ], + [ + 162.0, + 127.0 + ] + ], + "center_px": [ + 180.0, + 115.75 + ], + "area_px": 677.5 + }, + { + "image_points_px": [ + [ + 50.0, + 176.0 + ], + [ + 80.0, + 175.0 + ], + [ + 75.0, + 198.0 + ], + [ + 45.0, + 198.0 + ] + ], + "center_px": [ + 62.5, + 186.75 + ], + "area_px": 672.5 + }, + { + "image_points_px": [ + [ + 80.0, + 77.0 + ], + [ + 110.0, + 78.0 + ], + [ + 104.0, + 99.0 + ], + [ + 73.0, + 99.0 + ] + ], + "center_px": [ + 91.75, + 88.25 + ], + "area_px": 659.0 + }, + { + "image_points_px": [ + [ + 1194.0, + 92.0 + ], + [ + 1224.0, + 94.0 + ], + [ + 1228.0, + 116.0 + ], + [ + 1198.0, + 115.0 + ] + ], + "center_px": [ + 1211.0, + 104.25 + ], + "area_px": 669.0 + }, + { + "image_points_px": [ + [ + 143.0, + 79.0 + ], + [ + 173.0, + 80.0 + ], + [ + 167.0, + 102.0 + ], + [ + 137.0, + 101.0 + ] + ], + "center_px": [ + 155.0, + 90.5 + ], + "area_px": 666.0 + }, + { + "image_points_px": [ + [ + 1071.0, + 137.0 + ], + [ + 1101.0, + 137.0 + ], + [ + 1105.0, + 159.0 + ], + [ + 1074.0, + 159.0 + ] + ], + "center_px": [ + 1087.75, + 148.0 + ], + "area_px": 671.0 + }, + { + "image_points_px": [ + [ + 1112.0, + 487.0 + ], + [ + 1148.0, + 488.0 + ], + [ + 1150.0, + 505.0 + ], + [ + 1114.0, + 503.0 + ] + ], + "center_px": [ + 1131.0, + 495.75 + ], + "area_px": 591.0 + }, + { + "image_points_px": [ + [ + 1187.0, + 490.0 + ], + [ + 1223.0, + 491.0 + ], + [ + 1225.0, + 508.0 + ], + [ + 1189.0, + 506.0 + ] + ], + "center_px": [ + 1206.0, + 498.75 + ], + "area_px": 591.0 + }, + { + "image_points_px": [ + [ + 937.0, + 84.0 + ], + [ + 967.0, + 84.0 + ], + [ + 969.0, + 107.0 + ], + [ + 939.0, + 106.0 + ] + ], + "center_px": [ + 953.0, + 95.25 + ], + "area_px": 674.0 + }, + { + "image_points_px": [ + [ + 905.0, + 529.0 + ], + [ + 926.0, + 530.0 + ], + [ + 927.0, + 562.0 + ], + [ + 906.0, + 560.0 + ] + ], + "center_px": [ + 916.0, + 545.25 + ], + "area_px": 660.0 + }, + { + "image_points_px": [ + [ + 1037.0, + 484.0 + ], + [ + 1073.0, + 485.0 + ], + [ + 1074.0, + 502.0 + ], + [ + 1038.0, + 500.0 + ] + ], + "center_px": [ + 1055.5, + 492.75 + ], + "area_px": 592.5 + }, + { + "image_points_px": [ + [ + 810.0, + 79.0 + ], + [ + 840.0, + 80.0 + ], + [ + 841.0, + 102.0 + ], + [ + 810.0, + 101.0 + ] + ], + "center_px": [ + 825.25, + 90.5 + ], + "area_px": 670.5 + }, + { + "image_points_px": [ + [ + 90.0, + 152.0 + ], + [ + 119.0, + 151.0 + ], + [ + 115.0, + 174.0 + ], + [ + 85.0, + 174.0 + ] + ], + "center_px": [ + 102.25, + 162.75 + ], + "area_px": 661.5 + }, + { + "image_points_px": [ + [ + 332.0, + 86.0 + ], + [ + 362.0, + 87.0 + ], + [ + 358.0, + 109.0 + ], + [ + 328.0, + 108.0 + ] + ], + "center_px": [ + 345.0, + 97.5 + ], + "area_px": 664.0 + }, + { + "image_points_px": [ + [ + 1137.0, + 140.0 + ], + [ + 1167.0, + 140.0 + ], + [ + 1171.0, + 162.0 + ], + [ + 1141.0, + 162.0 + ] + ], + "center_px": [ + 1154.0, + 151.0 + ], + "area_px": 660.0 + }, + { + "image_points_px": [ + [ + 118.0, + 55.0 + ], + [ + 148.0, + 56.0 + ], + [ + 142.0, + 77.0 + ], + [ + 112.0, + 77.0 + ] + ], + "center_px": [ + 130.0, + 66.25 + ], + "area_px": 648.0 + }, + { + "image_points_px": [ + [ + 967.0, + 62.0 + ], + [ + 997.0, + 62.0 + ], + [ + 1000.0, + 84.0 + ], + [ + 969.0, + 83.0 + ] + ], + "center_px": [ + 983.25, + 72.75 + ], + "area_px": 654.5 + }, + { + "image_points_px": [ + [ + 32.0, + 29.0 + ], + [ + 62.0, + 30.0 + ], + [ + 55.0, + 51.0 + ], + [ + 25.0, + 50.0 + ] + ], + "center_px": [ + 43.5, + 40.0 + ], + "area_px": 637.0 + }, + { + "image_points_px": [ + [ + 962.0, + 481.0 + ], + [ + 998.0, + 482.0 + ], + [ + 999.0, + 498.0 + ], + [ + 963.0, + 497.0 + ] + ], + "center_px": [ + 980.5, + 489.5 + ], + "area_px": 575.0 + }, + { + "image_points_px": [ + [ + 1213.0, + 24.0 + ], + [ + 1243.0, + 26.0 + ], + [ + 1247.0, + 47.0 + ], + [ + 1217.0, + 46.0 + ] + ], + "center_px": [ + 1230.0, + 35.75 + ], + "area_px": 639.0 + }, + { + "image_points_px": [ + [ + 305.0, + 62.0 + ], + [ + 335.0, + 62.0 + ], + [ + 331.0, + 84.0 + ], + [ + 301.0, + 83.0 + ] + ], + "center_px": [ + 318.0, + 72.75 + ], + "area_px": 647.0 + }, + { + "image_points_px": [ + [ + 55.0, + 54.0 + ], + [ + 85.0, + 53.0 + ], + [ + 79.0, + 75.0 + ], + [ + 49.0, + 74.0 + ] + ], + "center_px": [ + 67.0, + 64.0 + ], + "area_px": 630.0 + }, + { + "image_points_px": [ + [ + 1123.0, + 44.0 + ], + [ + 1153.0, + 45.0 + ], + [ + 1156.0, + 67.0 + ], + [ + 1126.0, + 65.0 + ] + ], + "center_px": [ + 1139.5, + 55.25 + ], + "area_px": 640.5 + }, + { + "image_points_px": [ + [ + 1060.0, + 42.0 + ], + [ + 1090.0, + 43.0 + ], + [ + 1093.0, + 64.0 + ], + [ + 1062.0, + 63.0 + ] + ], + "center_px": [ + 1076.25, + 53.0 + ], + "area_px": 638.0 + }, + { + "image_points_px": [ + [ + 941.0, + 133.0 + ], + [ + 970.0, + 133.0 + ], + [ + 973.0, + 155.0 + ], + [ + 943.0, + 155.0 + ] + ], + "center_px": [ + 956.75, + 144.0 + ], + "area_px": 649.0 + }, + { + "image_points_px": [ + [ + 25.0, + 150.0 + ], + [ + 55.0, + 149.0 + ], + [ + 50.0, + 170.0 + ], + [ + 20.0, + 171.0 + ] + ], + "center_px": [ + 37.5, + 160.0 + ], + "area_px": 625.0 + }, + { + "image_points_px": [ + [ + 875.0, + 131.0 + ], + [ + 905.0, + 130.0 + ], + [ + 907.0, + 152.0 + ], + [ + 877.0, + 152.0 + ] + ], + "center_px": [ + 891.0, + 141.25 + ], + "area_px": 646.0 + }, + { + "image_points_px": [ + [ + 842.0, + 105.0 + ], + [ + 872.0, + 105.0 + ], + [ + 874.0, + 126.0 + ], + [ + 844.0, + 127.0 + ] + ], + "center_px": [ + 858.0, + 115.75 + ], + "area_px": 646.0 + }, + { + "image_points_px": [ + [ + 517.0, + 463.0 + ], + [ + 553.0, + 464.0 + ], + [ + 551.0, + 480.0 + ], + [ + 516.0, + 479.0 + ] + ], + "center_px": [ + 534.25, + 471.5 + ], + "area_px": 569.5 + }, + { + "image_points_px": [ + [ + 778.0, + 55.0 + ], + [ + 808.0, + 56.0 + ], + [ + 809.0, + 77.0 + ], + [ + 779.0, + 77.0 + ] + ], + "center_px": [ + 793.5, + 66.25 + ], + "area_px": 644.5 + }, + { + "image_points_px": [ + [ + 591.0, + 466.0 + ], + [ + 626.0, + 467.0 + ], + [ + 626.0, + 483.0 + ], + [ + 590.0, + 482.0 + ] + ], + "center_px": [ + 608.25, + 474.5 + ], + "area_px": 568.5 + }, + { + "image_points_px": [ + [ + 665.0, + 469.0 + ], + [ + 701.0, + 470.0 + ], + [ + 700.0, + 486.0 + ], + [ + 665.0, + 485.0 + ] + ], + "center_px": [ + 682.75, + 477.5 + ], + "area_px": 568.5 + }, + { + "image_points_px": [ + [ + 94.0, + 31.0 + ], + [ + 123.0, + 32.0 + ], + [ + 117.0, + 53.0 + ], + [ + 87.0, + 52.0 + ] + ], + "center_px": [ + 105.25, + 42.0 + ], + "area_px": 626.0 + }, + { + "image_points_px": [ + [ + 1101.0, + 114.0 + ], + [ + 1130.0, + 114.0 + ], + [ + 1134.0, + 136.0 + ], + [ + 1105.0, + 136.0 + ] + ], + "center_px": [ + 1117.5, + 125.0 + ], + "area_px": 638.0 + }, + { + "image_points_px": [ + [ + 341.0, + 40.0 + ], + [ + 370.0, + 40.0 + ], + [ + 367.0, + 62.0 + ], + [ + 337.0, + 61.0 + ] + ], + "center_px": [ + 353.75, + 50.75 + ], + "area_px": 636.0 + }, + { + "image_points_px": [ + [ + 907.0, + 107.0 + ], + [ + 936.0, + 107.0 + ], + [ + 939.0, + 128.0 + ], + [ + 909.0, + 129.0 + ] + ], + "center_px": [ + 922.75, + 117.75 + ], + "area_px": 635.5 + }, + { + "image_points_px": [ + [ + 443.0, + 460.0 + ], + [ + 479.0, + 461.0 + ], + [ + 477.0, + 477.0 + ], + [ + 442.0, + 475.0 + ] + ], + "center_px": [ + 460.25, + 468.25 + ], + "area_px": 552.5 + }, + { + "image_points_px": [ + [ + 716.0, + 52.0 + ], + [ + 744.0, + 53.0 + ], + [ + 745.0, + 75.0 + ], + [ + 715.0, + 74.0 + ] + ], + "center_px": [ + 730.0, + 63.5 + ], + "area_px": 638.0 + }, + { + "image_points_px": [ + [ + 778.0, + 103.0 + ], + [ + 808.0, + 103.0 + ], + [ + 809.0, + 124.0 + ], + [ + 779.0, + 124.0 + ] + ], + "center_px": [ + 793.5, + 113.5 + ], + "area_px": 630.0 + }, + { + "image_points_px": [ + [ + 1240.0, + 3.0 + ], + [ + 1269.0, + 4.0 + ], + [ + 1274.0, + 25.0 + ], + [ + 1244.0, + 24.0 + ] + ], + "center_px": [ + 1256.75, + 14.0 + ], + "area_px": 615.0 + }, + { + "image_points_px": [ + [ + 315.0, + 16.0 + ], + [ + 344.0, + 17.0 + ], + [ + 340.0, + 38.0 + ], + [ + 310.0, + 37.0 + ] + ], + "center_px": [ + 327.25, + 27.0 + ], + "area_px": 624.0 + }, + { + "image_points_px": [ + [ + 224.0, + 451.0 + ], + [ + 259.0, + 452.0 + ], + [ + 256.0, + 468.0 + ], + [ + 221.0, + 466.0 + ] + ], + "center_px": [ + 240.0, + 459.25 + ], + "area_px": 547.0 + }, + { + "image_points_px": [ + [ + 131.0, + 10.0 + ], + [ + 160.0, + 11.0 + ], + [ + 155.0, + 31.0 + ], + [ + 125.0, + 31.0 + ] + ], + "center_px": [ + 142.75, + 20.75 + ], + "area_px": 607.5 + }, + { + "image_points_px": [ + [ + 297.0, + 454.0 + ], + [ + 332.0, + 455.0 + ], + [ + 330.0, + 470.0 + ], + [ + 294.0, + 469.0 + ] + ], + "center_px": [ + 313.25, + 462.0 + ], + "area_px": 535.0 + }, + { + "image_points_px": [ + [ + 370.0, + 457.0 + ], + [ + 406.0, + 458.0 + ], + [ + 404.0, + 472.0 + ], + [ + 368.0, + 472.0 + ] + ], + "center_px": [ + 387.0, + 464.75 + ], + "area_px": 523.0 + }, + { + "image_points_px": [ + [ + 1222.0, + 71.0 + ], + [ + 1251.0, + 71.0 + ], + [ + 1256.0, + 92.0 + ], + [ + 1227.0, + 92.0 + ] + ], + "center_px": [ + 1239.0, + 81.5 + ], + "area_px": 609.0 + }, + { + "image_points_px": [ + [ + 874.0, + 82.0 + ], + [ + 903.0, + 82.0 + ], + [ + 905.0, + 103.0 + ], + [ + 875.0, + 103.0 + ] + ], + "center_px": [ + 889.25, + 92.5 + ], + "area_px": 619.5 + }, + { + "image_points_px": [ + [ + 1130.0, + 91.0 + ], + [ + 1159.0, + 91.0 + ], + [ + 1163.0, + 112.0 + ], + [ + 1135.0, + 113.0 + ] + ], + "center_px": [ + 1146.75, + 101.75 + ], + "area_px": 615.0 + }, + { + "image_points_px": [ + [ + 1158.0, + 69.0 + ], + [ + 1187.0, + 69.0 + ], + [ + 1191.0, + 90.0 + ], + [ + 1162.0, + 90.0 + ] + ], + "center_px": [ + 1174.5, + 79.5 + ], + "area_px": 609.0 + }, + { + "image_points_px": [ + [ + 1066.0, + 89.0 + ], + [ + 1095.0, + 89.0 + ], + [ + 1098.0, + 110.0 + ], + [ + 1069.0, + 110.0 + ] + ], + "center_px": [ + 1082.0, + 99.5 + ], + "area_px": 609.0 + }, + { + "image_points_px": [ + [ + 594.0, + 4.0 + ], + [ + 623.0, + 4.0 + ], + [ + 622.0, + 25.0 + ], + [ + 592.0, + 24.0 + ] + ], + "center_px": [ + 607.75, + 14.25 + ], + "area_px": 605.5 + }, + { + "image_points_px": [ + [ + 747.0, + 78.0 + ], + [ + 776.0, + 78.0 + ], + [ + 777.0, + 98.0 + ], + [ + 747.0, + 99.0 + ] + ], + "center_px": [ + 761.75, + 88.25 + ], + "area_px": 605.0 + }, + { + "image_points_px": [ + [ + 655.0, + 6.0 + ], + [ + 684.0, + 6.0 + ], + [ + 684.0, + 27.0 + ], + [ + 654.0, + 26.0 + ] + ], + "center_px": [ + 669.25, + 16.25 + ], + "area_px": 605.0 + }, + { + "image_points_px": [ + [ + 779.0, + 10.0 + ], + [ + 808.0, + 11.0 + ], + [ + 808.0, + 32.0 + ], + [ + 779.0, + 31.0 + ] + ], + "center_px": [ + 793.5, + 21.0 + ], + "area_px": 609.0 + }, + { + "image_points_px": [ + [ + 40.0, + 102.0 + ], + [ + 69.0, + 100.0 + ], + [ + 65.0, + 121.0 + ], + [ + 36.0, + 122.0 + ] + ], + "center_px": [ + 52.5, + 111.25 + ], + "area_px": 588.5 + }, + { + "image_points_px": [ + [ + 898.0, + 172.0 + ], + [ + 920.0, + 163.0 + ], + [ + 946.0, + 171.0 + ], + [ + 925.0, + 176.0 + ] + ], + "center_px": [ + 922.25, + 170.5 + ], + "area_px": 314.5 + }, + { + "image_points_px": [ + [ + 70.0, + 8.0 + ], + [ + 99.0, + 9.0 + ], + [ + 93.0, + 29.0 + ], + [ + 64.0, + 28.0 + ] + ], + "center_px": [ + 81.5, + 18.5 + ], + "area_px": 586.0 + }, + { + "image_points_px": [ + [ + 697.0, + 623.0 + ], + [ + 709.0, + 623.0 + ], + [ + 712.0, + 659.0 + ], + [ + 696.0, + 658.0 + ] + ], + "center_px": [ + 703.5, + 640.75 + ], + "area_px": 496.5 + }, + { + "image_points_px": [ + [ + 881.0, + 185.0 + ], + [ + 888.0, + 201.0 + ], + [ + 889.0, + 233.0 + ], + [ + 882.0, + 218.0 + ] + ], + "center_px": [ + 885.0, + 209.25 + ], + "area_px": 212.0 + }, + { + "image_points_px": [ + [ + 1094.0, + 67.0 + ], + [ + 1123.0, + 67.0 + ], + [ + 1127.0, + 87.0 + ], + [ + 1098.0, + 87.0 + ] + ], + "center_px": [ + 1110.5, + 77.0 + ], + "area_px": 580.0 + }, + { + "image_points_px": [ + [ + 1031.0, + 65.0 + ], + [ + 1060.0, + 65.0 + ], + [ + 1063.0, + 85.0 + ], + [ + 1034.0, + 85.0 + ] + ], + "center_px": [ + 1047.0, + 75.0 + ], + "area_px": 580.0 + }, + { + "image_points_px": [ + [ + 997.0, + 41.0 + ], + [ + 1026.0, + 41.0 + ], + [ + 1029.0, + 61.0 + ], + [ + 1000.0, + 61.0 + ] + ], + "center_px": [ + 1013.0, + 51.0 + ], + "area_px": 580.0 + }, + { + "image_points_px": [ + [ + 905.0, + 60.0 + ], + [ + 933.0, + 60.0 + ], + [ + 936.0, + 80.0 + ], + [ + 907.0, + 81.0 + ] + ], + "center_px": [ + 920.25, + 70.25 + ], + "area_px": 585.5 + }, + { + "image_points_px": [ + [ + 841.0, + 59.0 + ], + [ + 870.0, + 58.0 + ], + [ + 872.0, + 78.0 + ], + [ + 843.0, + 79.0 + ] + ], + "center_px": [ + 856.5, + 68.5 + ], + "area_px": 582.0 + }, + { + "image_points_px": [ + [ + 747.0, + 32.0 + ], + [ + 776.0, + 32.0 + ], + [ + 777.0, + 52.0 + ], + [ + 748.0, + 52.0 + ] + ], + "center_px": [ + 762.0, + 42.0 + ], + "area_px": 580.0 + }, + { + "image_points_px": [ + [ + 1186.0, + 47.0 + ], + [ + 1214.0, + 47.0 + ], + [ + 1219.0, + 67.0 + ], + [ + 1190.0, + 67.0 + ] + ], + "center_px": [ + 1202.25, + 57.0 + ], + "area_px": 570.0 + }, + { + "image_points_px": [ + [ + 1026.0, + 19.0 + ], + [ + 1054.0, + 19.0 + ], + [ + 1058.0, + 39.0 + ], + [ + 1029.0, + 39.0 + ] + ], + "center_px": [ + 1041.75, + 29.0 + ], + "area_px": 570.0 + }, + { + "image_points_px": [ + [ + 243.0, + 60.0 + ], + [ + 271.0, + 60.0 + ], + [ + 268.0, + 80.0 + ], + [ + 240.0, + 81.0 + ] + ], + "center_px": [ + 255.5, + 70.25 + ], + "area_px": 572.5 + }, + { + "image_points_px": [ + [ + 872.0, + 36.0 + ], + [ + 900.0, + 36.0 + ], + [ + 903.0, + 56.0 + ], + [ + 875.0, + 57.0 + ] + ], + "center_px": [ + 887.5, + 46.25 + ], + "area_px": 575.5 + }, + { + "image_points_px": [ + [ + 935.0, + 39.0 + ], + [ + 963.0, + 38.0 + ], + [ + 966.0, + 58.0 + ], + [ + 937.0, + 59.0 + ] + ], + "center_px": [ + 950.25, + 48.5 + ], + "area_px": 572.5 + }, + { + "image_points_px": [ + [ + 810.0, + 34.0 + ], + [ + 838.0, + 34.0 + ], + [ + 840.0, + 54.0 + ], + [ + 811.0, + 54.0 + ] + ], + "center_px": [ + 824.75, + 44.0 + ], + "area_px": 570.0 + }, + { + "image_points_px": [ + [ + 17.0, + 76.0 + ], + [ + 45.0, + 76.0 + ], + [ + 41.0, + 95.0 + ], + [ + 12.0, + 96.0 + ] + ], + "center_px": [ + 28.75, + 85.75 + ], + "area_px": 553.5 + }, + { + "image_points_px": [ + [ + 1089.0, + 21.0 + ], + [ + 1116.0, + 21.0 + ], + [ + 1121.0, + 41.0 + ], + [ + 1092.0, + 41.0 + ] + ], + "center_px": [ + 1104.5, + 31.0 + ], + "area_px": 560.0 + }, + { + "image_points_px": [ + [ + 217.0, + 36.0 + ], + [ + 245.0, + 36.0 + ], + [ + 242.0, + 56.0 + ], + [ + 214.0, + 56.0 + ] + ], + "center_px": [ + 229.5, + 46.0 + ], + "area_px": 560.0 + }, + { + "image_points_px": [ + [ + 964.0, + 17.0 + ], + [ + 992.0, + 17.0 + ], + [ + 995.0, + 37.0 + ], + [ + 967.0, + 37.0 + ] + ], + "center_px": [ + 979.5, + 27.0 + ], + "area_px": 560.0 + }, + { + "image_points_px": [ + [ + 840.0, + 13.0 + ], + [ + 869.0, + 13.0 + ], + [ + 870.0, + 32.0 + ], + [ + 842.0, + 33.0 + ] + ], + "center_px": [ + 855.25, + 22.75 + ], + "area_px": 556.5 + }, + { + "image_points_px": [ + [ + 902.0, + 15.0 + ], + [ + 930.0, + 15.0 + ], + [ + 933.0, + 34.0 + ], + [ + 905.0, + 35.0 + ] + ], + "center_px": [ + 917.5, + 24.75 + ], + "area_px": 547.5 + }, + { + "image_points_px": [ + [ + 253.0, + 16.0 + ], + [ + 281.0, + 15.0 + ], + [ + 278.0, + 35.0 + ], + [ + 250.0, + 35.0 + ] + ], + "center_px": [ + 265.5, + 25.25 + ], + "area_px": 544.5 + }, + { + "image_points_px": [ + [ + 717.0, + 9.0 + ], + [ + 745.0, + 9.0 + ], + [ + 746.0, + 28.0 + ], + [ + 717.0, + 28.0 + ] + ], + "center_px": [ + 731.25, + 18.5 + ], + "area_px": 541.5 + }, + { + "image_points_px": [ + [ + 1151.0, + 24.0 + ], + [ + 1179.0, + 24.0 + ], + [ + 1183.0, + 43.0 + ], + [ + 1155.0, + 43.0 + ] + ], + "center_px": [ + 1167.0, + 33.5 + ], + "area_px": 532.0 + }, + { + "image_points_px": [ + [ + 810.0, + 127.0 + ], + [ + 841.0, + 128.0 + ], + [ + 842.0, + 145.0 + ], + [ + 810.0, + 141.0 + ] + ], + "center_px": [ + 825.75, + 135.25 + ], + "area_px": 487.0 + }, + { + "image_points_px": [ + [ + 211.0, + 345.0 + ], + [ + 229.0, + 345.0 + ], + [ + 223.0, + 373.0 + ], + [ + 205.0, + 373.0 + ] + ], + "center_px": [ + 217.0, + 359.0 + ], + "area_px": 504.0 + }, + { + "image_points_px": [ + [ + 650.0, + 97.0 + ], + [ + 674.0, + 98.0 + ], + [ + 671.0, + 121.0 + ], + [ + 649.0, + 120.0 + ] + ], + "center_px": [ + 661.0, + 109.0 + ], + "area_px": 531.0 + }, + { + "image_points_px": [ + [ + 223.0, + 288.0 + ], + [ + 242.0, + 289.0 + ], + [ + 236.0, + 315.0 + ], + [ + 218.0, + 315.0 + ] + ], + "center_px": [ + 229.75, + 301.75 + ], + "area_px": 493.0 + }, + { + "image_points_px": [ + [ + 1108.0, + 415.0 + ], + [ + 1132.0, + 415.0 + ], + [ + 1137.0, + 435.0 + ], + [ + 1111.0, + 435.0 + ] + ], + "center_px": [ + 1122.0, + 425.0 + ], + "area_px": 500.0 + }, + { + "image_points_px": [ + [ + 1181.0, + 419.0 + ], + [ + 1206.0, + 418.0 + ], + [ + 1210.0, + 438.0 + ], + [ + 1186.0, + 439.0 + ] + ], + "center_px": [ + 1195.75, + 428.5 + ], + "area_px": 494.5 + }, + { + "image_points_px": [ + [ + 1035.0, + 413.0 + ], + [ + 1059.0, + 412.0 + ], + [ + 1063.0, + 432.0 + ], + [ + 1038.0, + 433.0 + ] + ], + "center_px": [ + 1048.75, + 422.5 + ], + "area_px": 493.5 + }, + { + "image_points_px": [ + [ + 1213.0, + 390.0 + ], + [ + 1237.0, + 389.0 + ], + [ + 1242.0, + 408.0 + ], + [ + 1218.0, + 409.0 + ] + ], + "center_px": [ + 1227.5, + 399.0 + ], + "area_px": 461.0 + }, + { + "image_points_px": [ + [ + 529.0, + 231.0 + ], + [ + 532.0, + 270.0 + ], + [ + 526.0, + 270.0 + ], + [ + 521.0, + 262.0 + ] + ], + "center_px": [ + 527.0, + 258.25 + ], + "area_px": 226.5 + }, + { + "image_points_px": [ + [ + 911.0, + 673.0 + ], + [ + 929.0, + 676.0 + ], + [ + 928.0, + 701.0 + ], + [ + 912.0, + 698.0 + ] + ], + "center_px": [ + 920.0, + 687.0 + ], + "area_px": 425.0 + }, + { + "image_points_px": [ + [ + 1169.0, + 179.0 + ], + [ + 1193.0, + 173.0 + ], + [ + 1209.0, + 181.0 + ], + [ + 1178.0, + 184.0 + ] + ], + "center_px": [ + 1187.25, + 179.25 + ], + "area_px": 235.0 + }, + { + "image_points_px": [ + [ + 700.0, + 554.0 + ], + [ + 710.0, + 555.0 + ], + [ + 708.0, + 587.0 + ], + [ + 698.0, + 585.0 + ] + ], + "center_px": [ + 704.0, + 570.25 + ], + "area_px": 318.0 + }, + { + "image_points_px": [ + [ + 777.0, + 151.0 + ], + [ + 778.0, + 190.0 + ], + [ + 772.0, + 174.0 + ], + [ + 772.0, + 161.0 + ] + ], + "center_px": [ + 774.75, + 169.0 + ], + "area_px": 141.5 + }, + { + "image_points_px": [ + [ + 703.0, + 488.0 + ], + [ + 712.0, + 489.0 + ], + [ + 711.0, + 519.0 + ], + [ + 701.0, + 518.0 + ] + ], + "center_px": [ + 706.75, + 503.5 + ], + "area_px": 286.5 + }, + { + "image_points_px": [ + [ + 910.0, + 602.0 + ], + [ + 925.0, + 603.0 + ], + [ + 925.0, + 627.0 + ], + [ + 910.0, + 625.0 + ] + ], + "center_px": [ + 917.5, + 614.25 + ], + "area_px": 352.5 + }, + { + "image_points_px": [ + [ + 248.0, + 181.0 + ], + [ + 261.0, + 182.0 + ], + [ + 255.0, + 206.0 + ], + [ + 242.0, + 206.0 + ] + ], + "center_px": [ + 251.5, + 193.75 + ], + "area_px": 321.5 + }, + { + "image_points_px": [ + [ + 842.0, + 622.0 + ], + [ + 869.0, + 629.0 + ], + [ + 869.0, + 638.0 + ], + [ + 844.0, + 633.0 + ] + ], + "center_px": [ + 856.0, + 630.5 + ], + "area_px": 254.0 + }, + { + "image_points_px": [ + [ + 901.0, + 494.0 + ], + [ + 904.0, + 517.0 + ], + [ + 902.0, + 530.0 + ], + [ + 899.0, + 522.0 + ] + ], + "center_px": [ + 901.5, + 515.75 + ], + "area_px": 92.5 + }, + { + "image_points_px": [ + [ + 368.0, + 64.0 + ], + [ + 381.0, + 64.0 + ], + [ + 380.0, + 86.0 + ], + [ + 364.0, + 85.0 + ] + ], + "center_px": [ + 373.25, + 74.75 + ], + "area_px": 313.0 + }, + { + "image_points_px": [ + [ + 757.0, + 242.0 + ], + [ + 756.0, + 267.0 + ], + [ + 747.0, + 274.0 + ], + [ + 747.0, + 253.0 + ] + ], + "center_px": [ + 751.75, + 259.0 + ], + "area_px": 214.0 + }, + { + "image_points_px": [ + [ + 269.0, + 84.0 + ], + [ + 282.0, + 84.0 + ], + [ + 277.0, + 106.0 + ], + [ + 264.0, + 106.0 + ] + ], + "center_px": [ + 273.0, + 95.0 + ], + "area_px": 286.0 + }, + { + "image_points_px": [ + [ + 903.0, + 479.0 + ], + [ + 923.0, + 479.0 + ], + [ + 924.0, + 495.0 + ], + [ + 904.0, + 494.0 + ] + ], + "center_px": [ + 913.5, + 486.75 + ], + "area_px": 309.5 + }, + { + "image_points_px": [ + [ + 1248.0, + 588.0 + ], + [ + 1266.0, + 586.0 + ], + [ + 1272.0, + 600.0 + ], + [ + 1253.0, + 602.0 + ] + ], + "center_px": [ + 1259.75, + 594.0 + ], + "area_px": 270.0 + }, + { + "image_points_px": [ + [ + 600.0, + 412.0 + ], + [ + 602.0, + 394.0 + ], + [ + 622.0, + 397.0 + ], + [ + 612.0, + 408.0 + ] + ], + "center_px": [ + 609.0, + 402.75 + ], + "area_px": 229.0 + }, + { + "image_points_px": [ + [ + 865.0, + 605.0 + ], + [ + 877.0, + 606.0 + ], + [ + 892.0, + 616.0 + ], + [ + 886.0, + 617.0 + ] + ], + "center_px": [ + 880.0, + 611.0 + ], + "area_px": 99.0 + }, + { + "image_points_px": [ + [ + 714.0, + 134.0 + ], + [ + 719.0, + 131.0 + ], + [ + 738.0, + 132.0 + ], + [ + 741.0, + 135.0 + ] + ], + "center_px": [ + 728.0, + 133.0 + ], + "area_px": 70.0 + }, + { + "image_points_px": [ + [ + 625.0, + 125.0 + ], + [ + 626.0, + 120.0 + ], + [ + 647.0, + 121.0 + ], + [ + 647.0, + 126.0 + ] + ], + "center_px": [ + 636.25, + 123.0 + ], + "area_px": 108.0 + }, + { + "image_points_px": [ + [ + 861.0, + 526.0 + ], + [ + 865.0, + 532.0 + ], + [ + 866.0, + 548.0 + ], + [ + 861.0, + 540.0 + ] + ], + "center_px": [ + 863.25, + 536.5 + ], + "area_px": 64.0 + }, + { + "image_points_px": [ + [ + 756.0, + 283.0 + ], + [ + 755.0, + 286.0 + ], + [ + 742.0, + 299.0 + ], + [ + 741.0, + 293.0 + ] + ], + "center_px": [ + 748.5, + 290.25 + ], + "area_px": 63.0 + }, + { + "image_points_px": [ + [ + 862.0, + 361.0 + ], + [ + 879.0, + 365.0 + ], + [ + 882.0, + 369.0 + ], + [ + 876.0, + 370.0 + ] + ], + "center_px": [ + 874.75, + 366.25 + ], + "area_px": 62.0 + }, + { + "image_points_px": [ + [ + 1177.0, + 382.0 + ], + [ + 1193.0, + 381.0 + ], + [ + 1199.0, + 383.0 + ], + [ + 1189.0, + 384.0 + ] + ], + "center_px": [ + 1189.5, + 382.5 + ], + "area_px": 35.0 + }, + { + "image_points_px": [ + [ + 707.0, + 493.0 + ], + [ + 708.0, + 513.0 + ], + [ + 706.0, + 514.0 + ], + [ + 705.0, + 496.0 + ] + ], + "center_px": [ + 706.5, + 504.0 + ], + "area_px": 40.0 + }, + { + "image_points_px": [ + [ + 393.0, + 351.0 + ], + [ + 401.0, + 350.0 + ], + [ + 414.0, + 352.0 + ], + [ + 404.0, + 353.0 + ] + ], + "center_px": [ + 403.0, + 351.5 + ], + "area_px": 30.0 + }, + { + "image_points_px": [ + [ + 683.0, + 423.0 + ], + [ + 701.0, + 422.0 + ], + [ + 702.0, + 424.0 + ], + [ + 688.0, + 425.0 + ] + ], + "center_px": [ + 693.5, + 423.5 + ], + "area_px": 35.0 + } + ] +} \ No newline at end of file diff --git a/pipeline/render_1c.png b/pipeline/render_1c.png new file mode 100644 index 0000000..b3de365 Binary files /dev/null and b/pipeline/render_1c.png differ diff --git a/pipeline/render_1c_aruco_detection.json b/pipeline/render_1c_aruco_detection.json new file mode 100644 index 0000000..edcadb3 --- /dev/null +++ b/pipeline/render_1c_aruco_detection.json @@ -0,0 +1,8904 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T17:30:28Z", + "vision_config": { + "MarkerType": "DICT_4X4_250", + "MarkerSize": 0.025 + }, + "camera": { + "camera_id": "cam3", + "intrinsics_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render.npz", + "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 + ] + }, + "image": { + "image_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1c.png", + "image_sha256": "8a748927859a08f1bd9ceddef730e9035a42bd892a0a9241977eeba56c3cfa18", + "width_px": 1280, + "height_px": 720 + }, + "aruco": { + "dictionary": "DICT_4X4_250", + "num_detected_markers": 7, + "num_rejected_candidates": 339 + }, + "detections": [ + { + "observation_id": "967136ae-fb0e-4a0d-b308-232889f1b2ef", + "type": "aruco", + "marker_id": 102, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 759.0, + 512.0 + ], + [ + 856.0, + 514.0 + ], + [ + 828.0, + 580.0 + ], + [ + 728.0, + 575.0 + ] + ], + "center_px": [ + 792.75, + 545.25 + ], + "quality": { + "area_px": 6456.5, + "perimeter_px": 339.0532913208008, + "sharpness": { + "laplacian_var": 1746.7626307459584 + }, + "contrast": { + "p05": 15.0, + "p95": 184.0, + "dynamic_range": 169.0, + "mean_gray": 98.22575406032483, + "std_gray": 80.16100003153355 + }, + "geometry": { + "distance_to_center_norm": 0.3269830048084259, + "distance_to_border_px": 140.0 + }, + "edge_ratio": 1.4259974156489281, + "edge_lengths_px": [ + 97.02061462402344, + 71.69379425048828, + 100.12492370605469, + 70.21395874023438 + ] + }, + "confidence": 0.7012635429952238 + }, + { + "observation_id": "72cc0a51-78a4-4f5e-b58a-7d4c4c1074b8", + "type": "aruco", + "marker_id": 122, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 940.0, + 361.0 + ], + [ + 1014.0, + 415.0 + ], + [ + 996.0, + 469.0 + ], + [ + 918.0, + 412.0 + ] + ], + "center_px": [ + 967.0, + 414.25 + ], + "quality": { + "area_px": 5100.0, + "perimeter_px": 300.67908477783203, + "sharpness": { + "laplacian_var": 850.8717661526819 + }, + "contrast": { + "p05": 8.0, + "p95": 146.0, + "dynamic_range": 138.0, + "mean_gray": 46.12199413489736, + "std_gray": 57.67161372334323 + }, + "geometry": { + "distance_to_center_norm": 0.45140743255615234, + "distance_to_border_px": 251.0 + }, + "edge_ratio": 1.7393341824971433, + "edge_lengths_px": [ + 91.60785675048828, + 56.920997619628906, + 96.60745239257812, + 55.54277801513672 + ] + }, + "confidence": 0.5749326438029929 + }, + { + "observation_id": "088b69f4-b721-42b7-a9e0-d58d658dc378", + "type": "aruco", + "marker_id": 243, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 656.0, + 185.0 + ], + [ + 728.0, + 199.0 + ], + [ + 714.0, + 275.0 + ], + [ + 642.0, + 260.0 + ] + ], + "center_px": [ + 685.0, + 229.75 + ], + "quality": { + "area_px": 5639.0, + "perimeter_px": 300.4685821533203, + "sharpness": { + "laplacian_var": 1086.9278379738284 + }, + "contrast": { + "p05": 65.0, + "p95": 193.0, + "dynamic_range": 128.0, + "mean_gray": 106.54448017148982, + "std_gray": 57.33337761476221 + }, + "geometry": { + "distance_to_center_norm": 0.1876671463251114, + "distance_to_border_px": 185.0 + }, + "edge_ratio": 1.0535830709016873, + "edge_lengths_px": [ + 73.34848022460938, + 77.27871704101562, + 73.54590606689453, + 76.29547882080078 + ] + }, + "confidence": 0.9491420540234864 + }, + { + "observation_id": "78bc4bb1-6ab4-46fc-8118-890438589196", + "type": "aruco", + "marker_id": 246, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 959.0, + 163.0 + ], + [ + 1037.0, + 176.0 + ], + [ + 1022.0, + 213.0 + ], + [ + 942.0, + 198.0 + ] + ], + "center_px": [ + 990.0, + 187.5 + ], + "quality": { + "area_px": 3068.0, + "perimeter_px": 239.3050994873047, + "sharpness": { + "laplacian_var": 1570.9195771097613 + }, + "contrast": { + "p05": 12.0, + "p95": 173.0, + "dynamic_range": 161.0, + "mean_gray": 57.680861478218304, + "std_gray": 67.95958432161584 + }, + "geometry": { + "distance_to_center_norm": 0.531389057636261, + "distance_to_border_px": 163.0 + }, + "edge_ratio": 2.0918474719224776, + "edge_lengths_px": [ + 79.07591247558594, + 39.924930572509766, + 81.39410400390625, + 38.910152435302734 + ] + }, + "confidence": 0.4780463267147134 + }, + { + "observation_id": "fecb971c-0aa8-469d-b3eb-f9980c273f70", + "type": "aruco", + "marker_id": 247, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 846.0, + 143.0 + ], + [ + 920.0, + 155.0 + ], + [ + 902.0, + 191.0 + ], + [ + 825.0, + 177.0 + ] + ], + "center_px": [ + 873.25, + 166.5 + ], + "quality": { + "area_px": 2896.0, + "perimeter_px": 233.44074630737305, + "sharpness": { + "laplacian_var": 2316.348091048046 + }, + "contrast": { + "p05": 13.0, + "p95": 174.0, + "dynamic_range": 161.0, + "mean_gray": 87.36540429887411, + "std_gray": 74.86083677714068 + }, + "geometry": { + "distance_to_center_norm": 0.41272374987602234, + "distance_to_border_px": 143.0 + }, + "edge_ratio": 1.958396418454695, + "edge_lengths_px": [ + 74.96665954589844, + 40.24922180175781, + 78.26238250732422, + 39.96248245239258 + ] + }, + "confidence": 0.5106218488640142 + }, + { + "observation_id": "f3f164a7-ff80-49a1-8cbb-982557d9f516", + "type": "aruco", + "marker_id": 214, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 1072.0, + 532.0 + ], + [ + 1130.0, + 546.0 + ], + [ + 1122.0, + 583.0 + ], + [ + 1062.0, + 568.0 + ] + ], + "center_px": [ + 1096.5, + 557.25 + ], + "quality": { + "area_px": 2284.0, + "perimeter_px": 196.7303924560547, + "sharpness": { + "laplacian_var": 1314.0889431615756 + }, + "contrast": { + "p05": 9.0, + "p95": 140.0, + "dynamic_range": 131.0, + "mean_gray": 69.38716082064857, + "std_gray": 60.24298092811457 + }, + "geometry": { + "distance_to_center_norm": 0.6772311925888062, + "distance_to_border_px": 137.0 + }, + "edge_ratio": 1.655285901037602, + "edge_lengths_px": [ + 59.66573715209961, + 37.85498809814453, + 61.84658432006836, + 37.36308288574219 + ] + }, + "confidence": 0.6041252446922665 + }, + { + "observation_id": "1b2a57ef-c9fe-473f-a2a8-5fe33588cbed", + "type": "aruco", + "marker_id": 210, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 371.0, + 383.0 + ], + [ + 417.0, + 394.0 + ], + [ + 394.0, + 422.0 + ], + [ + 347.0, + 411.0 + ] + ], + "center_px": [ + 382.25, + 402.5 + ], + "quality": { + "area_px": 1560.5, + "perimeter_px": 168.68052673339844, + "sharpness": { + "laplacian_var": 2842.0926549741707 + }, + "contrast": { + "p05": 17.0, + "p95": 179.0, + "dynamic_range": 162.0, + "mean_gray": 74.39889196675901, + "std_gray": 70.28614302190718 + }, + "geometry": { + "distance_to_center_norm": 0.3557531535625458, + "distance_to_border_px": 298.0 + }, + "edge_ratio": 1.3321269451115116, + "edge_lengths_px": [ + 47.29693603515625, + 36.2353401184082, + 48.27007293701172, + 36.878177642822266 + ] + }, + "confidence": 0.7506792079160973 + } + ], + "rejected_candidates": [ + { + "image_points_px": [ + [ + 194.0, + 191.0 + ], + [ + 177.0, + 206.0 + ], + [ + 50.0, + 221.0 + ], + [ + 69.0, + 206.0 + ] + ], + "center_px": [ + 122.5, + 206.0 + ], + "area_px": 1620.0 + }, + { + "image_points_px": [ + [ + 884.0, + 278.0 + ], + [ + 860.0, + 326.0 + ], + [ + 829.0, + 343.0 + ], + [ + 841.0, + 311.0 + ] + ], + "center_px": [ + 853.5, + 314.5 + ], + "area_px": 1030.0 + }, + { + "image_points_px": [ + [ + 164.0, + 666.0 + ], + [ + 205.0, + 678.0 + ], + [ + 178.0, + 711.0 + ], + [ + 137.0, + 698.0 + ] + ], + "center_px": [ + 171.0, + 688.25 + ], + "area_px": 1670.0 + }, + { + "image_points_px": [ + [ + 316.0, + 671.0 + ], + [ + 358.0, + 683.0 + ], + [ + 335.0, + 716.0 + ], + [ + 293.0, + 703.0 + ] + ], + "center_px": [ + 325.5, + 693.25 + ], + "area_px": 1652.5 + }, + { + "image_points_px": [ + [ + 82.0, + 642.0 + ], + [ + 121.0, + 653.0 + ], + [ + 94.0, + 685.0 + ], + [ + 54.0, + 673.0 + ] + ], + "center_px": [ + 87.75, + 663.25 + ], + "area_px": 1560.5 + }, + { + "image_points_px": [ + [ + 382.0, + 651.0 + ], + [ + 424.0, + 662.0 + ], + [ + 403.0, + 694.0 + ], + [ + 360.0, + 682.0 + ] + ], + "center_px": [ + 392.25, + 672.25 + ], + "area_px": 1586.0 + }, + { + "image_points_px": [ + [ + 534.0, + 655.0 + ], + [ + 577.0, + 667.0 + ], + [ + 559.0, + 699.0 + ], + [ + 515.0, + 687.0 + ] + ], + "center_px": [ + 546.25, + 677.0 + ], + "area_px": 1614.0 + }, + { + "image_points_px": [ + [ + 232.0, + 646.0 + ], + [ + 272.0, + 658.0 + ], + [ + 248.0, + 689.0 + ], + [ + 207.0, + 677.0 + ] + ], + "center_px": [ + 239.75, + 667.5 + ], + "area_px": 1549.5 + }, + { + "image_points_px": [ + [ + 298.0, + 627.0 + ], + [ + 339.0, + 638.0 + ], + [ + 316.0, + 669.0 + ], + [ + 274.0, + 657.0 + ] + ], + "center_px": [ + 306.75, + 647.75 + ], + "area_px": 1536.0 + }, + { + "image_points_px": [ + [ + 150.0, + 622.0 + ], + [ + 189.0, + 633.0 + ], + [ + 164.0, + 664.0 + ], + [ + 124.0, + 652.0 + ] + ], + "center_px": [ + 156.75, + 642.75 + ], + "area_px": 1498.0 + }, + { + "image_points_px": [ + [ + 447.0, + 631.0 + ], + [ + 488.0, + 643.0 + ], + [ + 469.0, + 674.0 + ], + [ + 426.0, + 662.0 + ] + ], + "center_px": [ + 457.5, + 652.5 + ], + "area_px": 1542.0 + }, + { + "image_points_px": [ + [ + 70.0, + 600.0 + ], + [ + 109.0, + 610.0 + ], + [ + 81.0, + 640.0 + ], + [ + 43.0, + 628.0 + ] + ], + "center_px": [ + 75.75, + 619.5 + ], + "area_px": 1419.0 + }, + { + "image_points_px": [ + [ + 216.0, + 604.0 + ], + [ + 255.0, + 614.0 + ], + [ + 231.0, + 644.0 + ], + [ + 191.0, + 633.0 + ] + ], + "center_px": [ + 223.25, + 623.75 + ], + "area_px": 1422.5 + }, + { + "image_points_px": [ + [ + 362.0, + 608.0 + ], + [ + 403.0, + 619.0 + ], + [ + 382.0, + 649.0 + ], + [ + 341.0, + 637.0 + ] + ], + "center_px": [ + 372.0, + 628.25 + ], + "area_px": 1451.0 + }, + { + "image_points_px": [ + [ + 281.0, + 585.0 + ], + [ + 320.0, + 596.0 + ], + [ + 298.0, + 625.0 + ], + [ + 257.0, + 614.0 + ] + ], + "center_px": [ + 289.0, + 605.0 + ], + "area_px": 1413.0 + }, + { + "image_points_px": [ + [ + 60.0, + 559.0 + ], + [ + 97.0, + 569.0 + ], + [ + 71.0, + 597.0 + ], + [ + 33.0, + 587.0 + ] + ], + "center_px": [ + 65.25, + 578.0 + ], + "area_px": 1315.0 + }, + { + "image_points_px": [ + [ + 137.0, + 581.0 + ], + [ + 174.0, + 591.0 + ], + [ + 150.0, + 620.0 + ], + [ + 111.0, + 609.0 + ] + ], + "center_px": [ + 143.0, + 600.25 + ], + "area_px": 1345.5 + }, + { + "image_points_px": [ + [ + 201.0, + 563.0 + ], + [ + 239.0, + 573.0 + ], + [ + 216.0, + 601.0 + ], + [ + 177.0, + 591.0 + ] + ], + "center_px": [ + 208.25, + 582.0 + ], + "area_px": 1313.0 + }, + { + "image_points_px": [ + [ + 124.0, + 542.0 + ], + [ + 161.0, + 552.0 + ], + [ + 136.0, + 579.0 + ], + [ + 99.0, + 569.0 + ] + ], + "center_px": [ + 130.0, + 560.5 + ], + "area_px": 1249.0 + }, + { + "image_points_px": [ + [ + 23.0, + 547.0 + ], + [ + 49.0, + 521.0 + ], + [ + 85.0, + 530.0 + ], + [ + 60.0, + 557.0 + ] + ], + "center_px": [ + 54.25, + 538.75 + ], + "area_px": 1209.5 + }, + { + "image_points_px": [ + [ + 163.0, + 551.0 + ], + [ + 187.0, + 525.0 + ], + [ + 224.0, + 534.0 + ], + [ + 201.0, + 561.0 + ] + ], + "center_px": [ + 193.75, + 542.75 + ], + "area_px": 1217.0 + }, + { + "image_points_px": [ + [ + 87.0, + 530.0 + ], + [ + 112.0, + 505.0 + ], + [ + 148.0, + 514.0 + ], + [ + 124.0, + 539.0 + ] + ], + "center_px": [ + 117.75, + 522.0 + ], + "area_px": 1133.0 + }, + { + "image_points_px": [ + [ + 150.0, + 513.0 + ], + [ + 173.0, + 489.0 + ], + [ + 210.0, + 498.0 + ], + [ + 186.0, + 523.0 + ] + ], + "center_px": [ + 179.75, + 505.75 + ], + "area_px": 1117.5 + }, + { + "image_points_px": [ + [ + 524.0, + 516.0 + ], + [ + 563.0, + 525.0 + ], + [ + 546.0, + 552.0 + ], + [ + 507.0, + 542.0 + ] + ], + "center_px": [ + 535.0, + 533.75 + ], + "area_px": 1195.0 + }, + { + "image_points_px": [ + [ + 14.0, + 510.0 + ], + [ + 39.0, + 485.0 + ], + [ + 74.0, + 494.0 + ], + [ + 49.0, + 519.0 + ] + ], + "center_px": [ + 44.0, + 502.0 + ], + "area_px": 1100.0 + }, + { + "image_points_px": [ + [ + 77.0, + 493.0 + ], + [ + 101.0, + 469.0 + ], + [ + 136.0, + 478.0 + ], + [ + 112.0, + 502.0 + ] + ], + "center_px": [ + 106.5, + 485.5 + ], + "area_px": 1056.0 + }, + { + "image_points_px": [ + [ + 5.0, + 474.0 + ], + [ + 29.0, + 451.0 + ], + [ + 64.0, + 459.0 + ], + [ + 39.0, + 483.0 + ] + ], + "center_px": [ + 34.25, + 466.75 + ], + "area_px": 1019.0 + }, + { + "image_points_px": [ + [ + 211.0, + 497.0 + ], + [ + 233.0, + 473.0 + ], + [ + 268.0, + 481.0 + ], + [ + 247.0, + 506.0 + ] + ], + "center_px": [ + 239.75, + 489.25 + ], + "area_px": 1052.5 + }, + { + "image_points_px": [ + [ + 138.0, + 477.0 + ], + [ + 161.0, + 454.0 + ], + [ + 196.0, + 463.0 + ], + [ + 174.0, + 486.0 + ] + ], + "center_px": [ + 167.25, + 470.0 + ], + "area_px": 1019.0 + }, + { + "image_points_px": [ + [ + 198.0, + 462.0 + ], + [ + 220.0, + 439.0 + ], + [ + 255.0, + 447.0 + ], + [ + 233.0, + 471.0 + ] + ], + "center_px": [ + 226.5, + 454.75 + ], + "area_px": 1009.5 + }, + { + "image_points_px": [ + [ + 66.0, + 458.0 + ], + [ + 90.0, + 436.0 + ], + [ + 124.0, + 444.0 + ], + [ + 101.0, + 467.0 + ] + ], + "center_px": [ + 95.25, + 451.25 + ], + "area_px": 976.0 + }, + { + "image_points_px": [ + [ + 126.0, + 443.0 + ], + [ + 149.0, + 421.0 + ], + [ + 183.0, + 429.0 + ], + [ + 161.0, + 452.0 + ] + ], + "center_px": [ + 154.75, + 436.25 + ], + "area_px": 967.5 + }, + { + "image_points_px": [ + [ + 56.0, + 425.0 + ], + [ + 79.0, + 404.0 + ], + [ + 113.0, + 411.0 + ], + [ + 90.0, + 433.0 + ] + ], + "center_px": [ + 84.5, + 418.25 + ], + "area_px": 903.5 + }, + { + "image_points_px": [ + [ + 185.0, + 429.0 + ], + [ + 206.0, + 407.0 + ], + [ + 240.0, + 415.0 + ], + [ + 220.0, + 437.0 + ] + ], + "center_px": [ + 212.75, + 422.0 + ], + "area_px": 923.0 + }, + { + "image_points_px": [ + [ + 257.0, + 447.0 + ], + [ + 278.0, + 424.0 + ], + [ + 311.0, + 432.0 + ], + [ + 291.0, + 455.0 + ] + ], + "center_px": [ + 284.25, + 439.5 + ], + "area_px": 934.5 + }, + { + "image_points_px": [ + [ + 115.0, + 411.0 + ], + [ + 137.0, + 390.0 + ], + [ + 170.0, + 397.0 + ], + [ + 149.0, + 419.0 + ] + ], + "center_px": [ + 142.75, + 404.25 + ], + "area_px": 881.5 + }, + { + "image_points_px": [ + [ + 172.0, + 397.0 + ], + [ + 194.0, + 376.0 + ], + [ + 227.0, + 384.0 + ], + [ + 205.0, + 405.0 + ] + ], + "center_px": [ + 199.5, + 390.5 + ], + "area_px": 869.0 + }, + { + "image_points_px": [ + [ + 579.0, + 666.0 + ], + [ + 588.0, + 650.0 + ], + [ + 632.0, + 661.0 + ], + [ + 623.0, + 678.0 + ] + ], + "center_px": [ + 605.5, + 663.75 + ], + "area_px": 829.5 + }, + { + "image_points_px": [ + [ + 490.0, + 642.0 + ], + [ + 500.0, + 626.0 + ], + [ + 544.0, + 637.0 + ], + [ + 533.0, + 653.0 + ] + ], + "center_px": [ + 516.75, + 639.5 + ], + "area_px": 811.5 + }, + { + "image_points_px": [ + [ + 243.0, + 414.0 + ], + [ + 263.0, + 393.0 + ], + [ + 297.0, + 401.0 + ], + [ + 277.0, + 422.0 + ] + ], + "center_px": [ + 270.0, + 407.5 + ], + "area_px": 874.0 + }, + { + "image_points_px": [ + [ + 104.0, + 380.0 + ], + [ + 126.0, + 360.0 + ], + [ + 159.0, + 367.0 + ], + [ + 137.0, + 388.0 + ] + ], + "center_px": [ + 131.5, + 373.75 + ], + "area_px": 841.5 + }, + { + "image_points_px": [ + [ + 47.0, + 394.0 + ], + [ + 70.0, + 373.0 + ], + [ + 102.0, + 381.0 + ], + [ + 80.0, + 401.0 + ] + ], + "center_px": [ + 74.75, + 387.25 + ], + "area_px": 835.0 + }, + { + "image_points_px": [ + [ + 1204.0, + 401.0 + ], + [ + 1244.0, + 409.0 + ], + [ + 1241.0, + 431.0 + ], + [ + 1200.0, + 423.0 + ] + ], + "center_px": [ + 1222.25, + 416.0 + ], + "area_px": 919.0 + }, + { + "image_points_px": [ + [ + 229.0, + 383.0 + ], + [ + 248.0, + 363.0 + ], + [ + 282.0, + 370.0 + ], + [ + 262.0, + 391.0 + ] + ], + "center_px": [ + 255.25, + 376.75 + ], + "area_px": 833.0 + }, + { + "image_points_px": [ + [ + 38.0, + 363.0 + ], + [ + 61.0, + 344.0 + ], + [ + 92.0, + 351.0 + ], + [ + 70.0, + 371.0 + ] + ], + "center_px": [ + 65.25, + 357.25 + ], + "area_px": 783.0 + }, + { + "image_points_px": [ + [ + 161.0, + 367.0 + ], + [ + 181.0, + 347.0 + ], + [ + 214.0, + 354.0 + ], + [ + 193.0, + 374.0 + ] + ], + "center_px": [ + 187.25, + 360.5 + ], + "area_px": 793.5 + }, + { + "image_points_px": [ + [ + 405.0, + 618.0 + ], + [ + 416.0, + 602.0 + ], + [ + 457.0, + 613.0 + ], + [ + 446.0, + 629.0 + ] + ], + "center_px": [ + 431.0, + 615.5 + ], + "area_px": 777.0 + }, + { + "image_points_px": [ + [ + 299.0, + 400.0 + ], + [ + 318.0, + 379.0 + ], + [ + 350.0, + 386.0 + ], + [ + 332.0, + 408.0 + ] + ], + "center_px": [ + 324.75, + 393.25 + ], + "area_px": 837.5 + }, + { + "image_points_px": [ + [ + 284.0, + 370.0 + ], + [ + 303.0, + 350.0 + ], + [ + 336.0, + 357.0 + ], + [ + 318.0, + 377.0 + ] + ], + "center_px": [ + 310.25, + 363.5 + ], + "area_px": 799.5 + }, + { + "image_points_px": [ + [ + 95.0, + 350.0 + ], + [ + 116.0, + 331.0 + ], + [ + 148.0, + 338.0 + ], + [ + 126.0, + 358.0 + ] + ], + "center_px": [ + 121.25, + 344.25 + ], + "area_px": 775.5 + }, + { + "image_points_px": [ + [ + 1122.0, + 385.0 + ], + [ + 1161.0, + 392.0 + ], + [ + 1157.0, + 413.0 + ], + [ + 1117.0, + 405.0 + ] + ], + "center_px": [ + 1139.25, + 398.75 + ], + "area_px": 843.5 + }, + { + "image_points_px": [ + [ + 322.0, + 595.0 + ], + [ + 333.0, + 580.0 + ], + [ + 374.0, + 591.0 + ], + [ + 362.0, + 606.0 + ] + ], + "center_px": [ + 347.75, + 593.0 + ], + "area_px": 734.0 + }, + { + "image_points_px": [ + [ + 216.0, + 353.0 + ], + [ + 236.0, + 334.0 + ], + [ + 268.0, + 341.0 + ], + [ + 248.0, + 361.0 + ] + ], + "center_px": [ + 242.0, + 347.25 + ], + "area_px": 774.0 + }, + { + "image_points_px": [ + [ + 1213.0, + 358.0 + ], + [ + 1252.0, + 365.0 + ], + [ + 1249.0, + 385.0 + ], + [ + 1209.0, + 378.0 + ] + ], + "center_px": [ + 1230.75, + 371.5 + ], + "area_px": 814.5 + }, + { + "image_points_px": [ + [ + 1089.0, + 355.0 + ], + [ + 1127.0, + 362.0 + ], + [ + 1122.0, + 382.0 + ], + [ + 1083.0, + 375.0 + ] + ], + "center_px": [ + 1105.25, + 368.5 + ], + "area_px": 808.5 + }, + { + "image_points_px": [ + [ + 30.0, + 335.0 + ], + [ + 52.0, + 316.0 + ], + [ + 82.0, + 323.0 + ], + [ + 60.0, + 342.0 + ] + ], + "center_px": [ + 56.0, + 329.0 + ], + "area_px": 724.0 + }, + { + "image_points_px": [ + [ + 1168.0, + 371.0 + ], + [ + 1206.0, + 378.0 + ], + [ + 1204.0, + 398.0 + ], + [ + 1165.0, + 392.0 + ] + ], + "center_px": [ + 1185.75, + 384.75 + ], + "area_px": 805.5 + }, + { + "image_points_px": [ + [ + 150.0, + 338.0 + ], + [ + 170.0, + 319.0 + ], + [ + 201.0, + 326.0 + ], + [ + 181.0, + 345.0 + ] + ], + "center_px": [ + 175.5, + 332.0 + ], + "area_px": 729.0 + }, + { + "image_points_px": [ + [ + 241.0, + 573.0 + ], + [ + 254.0, + 558.0 + ], + [ + 292.0, + 568.0 + ], + [ + 280.0, + 583.0 + ] + ], + "center_px": [ + 266.75, + 570.5 + ], + "area_px": 702.5 + }, + { + "image_points_px": [ + [ + 203.0, + 325.0 + ], + [ + 223.0, + 307.0 + ], + [ + 254.0, + 313.0 + ], + [ + 235.0, + 332.0 + ] + ], + "center_px": [ + 228.75, + 319.25 + ], + "area_px": 709.5 + }, + { + "image_points_px": [ + [ + 270.0, + 341.0 + ], + [ + 288.0, + 322.0 + ], + [ + 320.0, + 329.0 + ], + [ + 302.0, + 348.0 + ] + ], + "center_px": [ + 295.0, + 335.0 + ], + "area_px": 734.0 + }, + { + "image_points_px": [ + [ + 21.0, + 307.0 + ], + [ + 43.0, + 290.0 + ], + [ + 73.0, + 296.0 + ], + [ + 52.0, + 314.0 + ] + ], + "center_px": [ + 47.25, + 301.75 + ], + "area_px": 673.5 + }, + { + "image_points_px": [ + [ + 85.0, + 322.0 + ], + [ + 106.0, + 304.0 + ], + [ + 136.0, + 310.0 + ], + [ + 116.0, + 329.0 + ] + ], + "center_px": [ + 110.75, + 316.25 + ], + "area_px": 697.5 + }, + { + "image_points_px": [ + [ + 1134.0, + 342.0 + ], + [ + 1172.0, + 349.0 + ], + [ + 1168.0, + 368.0 + ], + [ + 1129.0, + 361.0 + ] + ], + "center_px": [ + 1150.75, + 355.0 + ], + "area_px": 763.0 + }, + { + "image_points_px": [ + [ + 13.0, + 281.0 + ], + [ + 35.0, + 264.0 + ], + [ + 65.0, + 270.0 + ], + [ + 44.0, + 287.0 + ] + ], + "center_px": [ + 39.25, + 275.5 + ], + "area_px": 647.5 + }, + { + "image_points_px": [ + [ + 256.0, + 313.0 + ], + [ + 274.0, + 295.0 + ], + [ + 306.0, + 301.0 + ], + [ + 288.0, + 320.0 + ] + ], + "center_px": [ + 281.0, + 307.25 + ], + "area_px": 709.0 + }, + { + "image_points_px": [ + [ + 1178.0, + 329.0 + ], + [ + 1216.0, + 336.0 + ], + [ + 1212.0, + 356.0 + ], + [ + 1175.0, + 349.0 + ] + ], + "center_px": [ + 1195.25, + 342.5 + ], + "area_px": 774.5 + }, + { + "image_points_px": [ + [ + 76.0, + 295.0 + ], + [ + 96.0, + 278.0 + ], + [ + 127.0, + 284.0 + ], + [ + 106.0, + 302.0 + ] + ], + "center_px": [ + 101.25, + 289.75 + ], + "area_px": 667.0 + }, + { + "image_points_px": [ + [ + 1221.0, + 317.0 + ], + [ + 1259.0, + 324.0 + ], + [ + 1256.0, + 343.0 + ], + [ + 1218.0, + 336.0 + ] + ], + "center_px": [ + 1238.5, + 330.0 + ], + "area_px": 743.0 + }, + { + "image_points_px": [ + [ + 139.0, + 310.0 + ], + [ + 159.0, + 292.0 + ], + [ + 189.0, + 299.0 + ], + [ + 170.0, + 317.0 + ] + ], + "center_px": [ + 164.25, + 304.5 + ], + "area_px": 685.5 + }, + { + "image_points_px": [ + [ + 191.0, + 298.0 + ], + [ + 211.0, + 280.0 + ], + [ + 241.0, + 287.0 + ], + [ + 222.0, + 305.0 + ] + ], + "center_px": [ + 216.25, + 292.5 + ], + "area_px": 685.5 + }, + { + "image_points_px": [ + [ + 1101.0, + 314.0 + ], + [ + 1138.0, + 321.0 + ], + [ + 1133.0, + 340.0 + ], + [ + 1096.0, + 333.0 + ] + ], + "center_px": [ + 1117.0, + 327.0 + ], + "area_px": 738.0 + }, + { + "image_points_px": [ + [ + 128.0, + 284.0 + ], + [ + 148.0, + 267.0 + ], + [ + 178.0, + 273.0 + ], + [ + 159.0, + 290.0 + ] + ], + "center_px": [ + 153.25, + 278.5 + ], + "area_px": 635.5 + }, + { + "image_points_px": [ + [ + 243.0, + 286.0 + ], + [ + 262.0, + 269.0 + ], + [ + 292.0, + 275.0 + ], + [ + 274.0, + 293.0 + ] + ], + "center_px": [ + 267.75, + 280.75 + ], + "area_px": 654.0 + }, + { + "image_points_px": [ + [ + 67.0, + 270.0 + ], + [ + 87.0, + 253.0 + ], + [ + 116.0, + 259.0 + ], + [ + 96.0, + 276.0 + ] + ], + "center_px": [ + 91.5, + 264.5 + ], + "area_px": 613.0 + }, + { + "image_points_px": [ + [ + 180.0, + 272.0 + ], + [ + 198.0, + 256.0 + ], + [ + 229.0, + 262.0 + ], + [ + 211.0, + 278.0 + ] + ], + "center_px": [ + 204.5, + 267.0 + ], + "area_px": 604.0 + }, + { + "image_points_px": [ + [ + 294.0, + 275.0 + ], + [ + 311.0, + 258.0 + ], + [ + 342.0, + 264.0 + ], + [ + 325.0, + 281.0 + ] + ], + "center_px": [ + 318.0, + 269.5 + ], + "area_px": 629.0 + }, + { + "image_points_px": [ + [ + 6.0, + 256.0 + ], + [ + 27.0, + 240.0 + ], + [ + 55.0, + 245.0 + ], + [ + 35.0, + 262.0 + ] + ], + "center_px": [ + 30.75, + 250.75 + ], + "area_px": 583.0 + }, + { + "image_points_px": [ + [ + 1229.0, + 279.0 + ], + [ + 1266.0, + 285.0 + ], + [ + 1263.0, + 303.0 + ], + [ + 1226.0, + 296.0 + ] + ], + "center_px": [ + 1246.0, + 290.75 + ], + "area_px": 667.0 + }, + { + "image_points_px": [ + [ + 231.0, + 261.0 + ], + [ + 248.0, + 245.0 + ], + [ + 279.0, + 250.0 + ], + [ + 261.0, + 267.0 + ] + ], + "center_px": [ + 254.75, + 255.75 + ], + "area_px": 599.5 + }, + { + "image_points_px": [ + [ + 119.0, + 259.0 + ], + [ + 138.0, + 242.0 + ], + [ + 167.0, + 248.0 + ], + [ + 148.0, + 264.0 + ] + ], + "center_px": [ + 143.0, + 253.25 + ], + "area_px": 583.0 + }, + { + "image_points_px": [ + [ + 169.0, + 247.0 + ], + [ + 188.0, + 231.0 + ], + [ + 217.0, + 237.0 + ], + [ + 199.0, + 253.0 + ] + ], + "center_px": [ + 193.25, + 242.0 + ], + "area_px": 583.0 + }, + { + "image_points_px": [ + [ + 58.0, + 245.0 + ], + [ + 78.0, + 229.0 + ], + [ + 106.0, + 234.0 + ], + [ + 86.0, + 251.0 + ] + ], + "center_px": [ + 82.0, + 239.75 + ], + "area_px": 572.0 + }, + { + "image_points_px": [ + [ + 109.0, + 234.0 + ], + [ + 128.0, + 218.0 + ], + [ + 157.0, + 224.0 + ], + [ + 138.0, + 240.0 + ] + ], + "center_px": [ + 133.0, + 229.0 + ], + "area_px": 578.0 + }, + { + "image_points_px": [ + [ + 361.0, + 289.0 + ], + [ + 375.0, + 272.0 + ], + [ + 407.0, + 278.0 + ], + [ + 391.0, + 295.0 + ] + ], + "center_px": [ + 383.5, + 283.5 + ], + "area_px": 617.0 + }, + { + "image_points_px": [ + [ + 1113.0, + 276.0 + ], + [ + 1148.0, + 282.0 + ], + [ + 1144.0, + 300.0 + ], + [ + 1108.0, + 293.0 + ] + ], + "center_px": [ + 1128.25, + 287.75 + ], + "area_px": 650.5 + }, + { + "image_points_px": [ + [ + 1155.0, + 265.0 + ], + [ + 1190.0, + 271.0 + ], + [ + 1187.0, + 288.0 + ], + [ + 1150.0, + 282.0 + ] + ], + "center_px": [ + 1170.5, + 276.5 + ], + "area_px": 636.0 + }, + { + "image_points_px": [ + [ + 281.0, + 250.0 + ], + [ + 298.0, + 234.0 + ], + [ + 328.0, + 240.0 + ], + [ + 311.0, + 256.0 + ] + ], + "center_px": [ + 304.5, + 245.0 + ], + "area_px": 582.0 + }, + { + "image_points_px": [ + [ + 608.0, + 541.0 + ], + [ + 639.0, + 549.0 + ], + [ + 628.0, + 567.0 + ], + [ + 597.0, + 560.0 + ] + ], + "center_px": [ + 618.0, + 554.25 + ], + "area_px": 656.0 + }, + { + "image_points_px": [ + [ + 219.0, + 237.0 + ], + [ + 237.0, + 221.0 + ], + [ + 266.0, + 227.0 + ], + [ + 249.0, + 242.0 + ] + ], + "center_px": [ + 242.75, + 231.75 + ], + "area_px": 553.5 + }, + { + "image_points_px": [ + [ + 393.0, + 252.0 + ], + [ + 409.0, + 236.0 + ], + [ + 439.0, + 242.0 + ], + [ + 424.0, + 258.0 + ] + ], + "center_px": [ + 416.25, + 247.0 + ], + "area_px": 581.0 + }, + { + "image_points_px": [ + [ + 1144.0, + 304.0 + ], + [ + 1180.0, + 309.0 + ], + [ + 1177.0, + 326.0 + ], + [ + 1141.0, + 320.0 + ] + ], + "center_px": [ + 1160.5, + 314.75 + ], + "area_px": 610.5 + }, + { + "image_points_px": [ + [ + 268.0, + 226.0 + ], + [ + 284.0, + 211.0 + ], + [ + 314.0, + 216.0 + ], + [ + 297.0, + 232.0 + ] + ], + "center_px": [ + 290.75, + 221.25 + ], + "area_px": 548.0 + }, + { + "image_points_px": [ + [ + 1124.0, + 240.0 + ], + [ + 1158.0, + 246.0 + ], + [ + 1154.0, + 263.0 + ], + [ + 1119.0, + 257.0 + ] + ], + "center_px": [ + 1138.75, + 251.5 + ], + "area_px": 613.5 + }, + { + "image_points_px": [ + [ + 1187.0, + 292.0 + ], + [ + 1223.0, + 297.0 + ], + [ + 1221.0, + 313.0 + ], + [ + 1185.0, + 308.0 + ] + ], + "center_px": [ + 1204.0, + 302.5 + ], + "area_px": 586.0 + }, + { + "image_points_px": [ + [ + 159.0, + 224.0 + ], + [ + 177.0, + 208.0 + ], + [ + 205.0, + 214.0 + ], + [ + 188.0, + 229.0 + ] + ], + "center_px": [ + 182.25, + 218.75 + ], + "area_px": 538.0 + }, + { + "image_points_px": [ + [ + 42.0, + 198.0 + ], + [ + 61.0, + 184.0 + ], + [ + 89.0, + 189.0 + ], + [ + 69.0, + 204.0 + ] + ], + "center_px": [ + 65.25, + 193.75 + ], + "area_px": 506.0 + }, + { + "image_points_px": [ + [ + 1148.0, + 430.0 + ], + [ + 1170.0, + 426.0 + ], + [ + 1190.0, + 431.0 + ], + [ + 1197.0, + 440.0 + ] + ], + "center_px": [ + 1176.25, + 431.75 + ], + "area_px": 280.5 + }, + { + "image_points_px": [ + [ + 208.0, + 213.0 + ], + [ + 226.0, + 198.0 + ], + [ + 254.0, + 204.0 + ], + [ + 237.0, + 219.0 + ] + ], + "center_px": [ + 231.25, + 208.5 + ], + "area_px": 532.5 + }, + { + "image_points_px": [ + [ + 378.0, + 228.0 + ], + [ + 393.0, + 213.0 + ], + [ + 423.0, + 219.0 + ], + [ + 408.0, + 234.0 + ] + ], + "center_px": [ + 400.5, + 223.5 + ], + "area_px": 540.0 + }, + { + "image_points_px": [ + [ + 1164.0, + 230.0 + ], + [ + 1199.0, + 236.0 + ], + [ + 1195.0, + 252.0 + ], + [ + 1161.0, + 246.0 + ] + ], + "center_px": [ + 1179.75, + 241.0 + ], + "area_px": 573.0 + }, + { + "image_points_px": [ + [ + 425.0, + 218.0 + ], + [ + 439.0, + 203.0 + ], + [ + 469.0, + 208.0 + ], + [ + 455.0, + 224.0 + ] + ], + "center_px": [ + 447.0, + 213.25 + ], + "area_px": 542.0 + }, + { + "image_points_px": [ + [ + 256.0, + 203.0 + ], + [ + 273.0, + 188.0 + ], + [ + 301.0, + 194.0 + ], + [ + 284.0, + 209.0 + ] + ], + "center_px": [ + 278.5, + 198.5 + ], + "area_px": 522.0 + }, + { + "image_points_px": [ + [ + 149.0, + 201.0 + ], + [ + 167.0, + 186.0 + ], + [ + 194.0, + 191.0 + ], + [ + 177.0, + 206.0 + ] + ], + "center_px": [ + 171.75, + 196.0 + ], + "area_px": 500.0 + }, + { + "image_points_px": [ + [ + 435.0, + 518.0 + ], + [ + 445.0, + 500.0 + ], + [ + 475.0, + 507.0 + ], + [ + 463.0, + 525.0 + ] + ], + "center_px": [ + 454.5, + 512.5 + ], + "area_px": 599.0 + }, + { + "image_points_px": [ + [ + 92.0, + 189.0 + ], + [ + 110.0, + 174.0 + ], + [ + 137.0, + 179.0 + ], + [ + 118.0, + 194.0 + ] + ], + "center_px": [ + 114.25, + 184.0 + ], + "area_px": 490.0 + }, + { + "image_points_px": [ + [ + 375.0, + 316.0 + ], + [ + 391.0, + 298.0 + ], + [ + 417.0, + 303.0 + ], + [ + 402.0, + 321.0 + ] + ], + "center_px": [ + 396.25, + 309.5 + ], + "area_px": 554.5 + }, + { + "image_points_px": [ + [ + 244.0, + 181.0 + ], + [ + 260.0, + 167.0 + ], + [ + 289.0, + 172.0 + ], + [ + 273.0, + 186.0 + ] + ], + "center_px": [ + 266.5, + 176.5 + ], + "area_px": 486.0 + }, + { + "image_points_px": [ + [ + 27.0, + 156.0 + ], + [ + 46.0, + 142.0 + ], + [ + 73.0, + 147.0 + ], + [ + 54.0, + 160.0 + ] + ], + "center_px": [ + 50.0, + 151.25 + ], + "area_px": 450.0 + }, + { + "image_points_px": [ + [ + 197.0, + 191.0 + ], + [ + 213.0, + 177.0 + ], + [ + 242.0, + 182.0 + ], + [ + 225.0, + 196.0 + ] + ], + "center_px": [ + 219.25, + 186.5 + ], + "area_px": 481.5 + }, + { + "image_points_px": [ + [ + 364.0, + 205.0 + ], + [ + 378.0, + 191.0 + ], + [ + 408.0, + 196.0 + ], + [ + 393.0, + 211.0 + ] + ], + "center_px": [ + 385.75, + 200.75 + ], + "area_px": 507.5 + }, + { + "image_points_px": [ + [ + 139.0, + 179.0 + ], + [ + 157.0, + 165.0 + ], + [ + 184.0, + 170.0 + ], + [ + 167.0, + 184.0 + ] + ], + "center_px": [ + 161.75, + 174.5 + ], + "area_px": 472.5 + }, + { + "image_points_px": [ + [ + 35.0, + 177.0 + ], + [ + 53.0, + 163.0 + ], + [ + 80.0, + 168.0 + ], + [ + 61.0, + 182.0 + ] + ], + "center_px": [ + 57.25, + 172.5 + ], + "area_px": 463.5 + }, + { + "image_points_px": [ + [ + 83.0, + 167.0 + ], + [ + 100.0, + 154.0 + ], + [ + 128.0, + 158.0 + ], + [ + 110.0, + 172.0 + ] + ], + "center_px": [ + 105.25, + 162.75 + ], + "area_px": 450.0 + }, + { + "image_points_px": [ + [ + 1133.0, + 208.0 + ], + [ + 1167.0, + 212.0 + ], + [ + 1164.0, + 227.0 + ], + [ + 1129.0, + 222.0 + ] + ], + "center_px": [ + 1148.25, + 217.25 + ], + "area_px": 516.0 + }, + { + "image_points_px": [ + [ + 410.0, + 195.0 + ], + [ + 424.0, + 181.0 + ], + [ + 453.0, + 186.0 + ], + [ + 439.0, + 201.0 + ] + ], + "center_px": [ + 431.5, + 190.75 + ], + "area_px": 497.5 + }, + { + "image_points_px": [ + [ + 350.0, + 183.0 + ], + [ + 365.0, + 169.0 + ], + [ + 393.0, + 174.0 + ], + [ + 378.0, + 189.0 + ] + ], + "center_px": [ + 371.5, + 178.75 + ], + "area_px": 488.5 + }, + { + "image_points_px": [ + [ + 1196.0, + 255.0 + ], + [ + 1230.0, + 260.0 + ], + [ + 1229.0, + 274.0 + ], + [ + 1194.0, + 270.0 + ] + ], + "center_px": [ + 1212.25, + 264.75 + ], + "area_px": 507.0 + }, + { + "image_points_px": [ + [ + 1236.0, + 245.0 + ], + [ + 1271.0, + 249.0 + ], + [ + 1269.0, + 264.0 + ], + [ + 1235.0, + 259.0 + ] + ], + "center_px": [ + 1252.75, + 254.25 + ], + "area_px": 507.0 + }, + { + "image_points_px": [ + [ + 130.0, + 158.0 + ], + [ + 148.0, + 144.0 + ], + [ + 174.0, + 149.0 + ], + [ + 157.0, + 163.0 + ] + ], + "center_px": [ + 152.25, + 153.5 + ], + "area_px": 458.5 + }, + { + "image_points_px": [ + [ + 1093.0, + 218.0 + ], + [ + 1126.0, + 222.0 + ], + [ + 1123.0, + 238.0 + ], + [ + 1089.0, + 232.0 + ] + ], + "center_px": [ + 1107.75, + 227.5 + ], + "area_px": 520.0 + }, + { + "image_points_px": [ + [ + 1173.0, + 197.0 + ], + [ + 1207.0, + 202.0 + ], + [ + 1204.0, + 216.0 + ], + [ + 1169.0, + 211.0 + ] + ], + "center_px": [ + 1188.25, + 206.5 + ], + "area_px": 500.5 + }, + { + "image_points_px": [ + [ + 456.0, + 186.0 + ], + [ + 469.0, + 171.0 + ], + [ + 498.0, + 176.0 + ], + [ + 485.0, + 191.0 + ] + ], + "center_px": [ + 477.0, + 181.0 + ], + "area_px": 500.0 + }, + { + "image_points_px": [ + [ + 395.0, + 174.0 + ], + [ + 409.0, + 160.0 + ], + [ + 438.0, + 165.0 + ], + [ + 424.0, + 179.0 + ] + ], + "center_px": [ + 416.5, + 169.5 + ], + "area_px": 476.0 + }, + { + "image_points_px": [ + [ + 186.0, + 169.0 + ], + [ + 203.0, + 156.0 + ], + [ + 230.0, + 160.0 + ], + [ + 214.0, + 174.0 + ] + ], + "center_px": [ + 208.25, + 164.75 + ], + "area_px": 445.5 + }, + { + "image_points_px": [ + [ + 75.0, + 147.0 + ], + [ + 93.0, + 133.0 + ], + [ + 119.0, + 138.0 + ], + [ + 101.0, + 151.0 + ] + ], + "center_px": [ + 97.0, + 142.25 + ], + "area_px": 432.0 + }, + { + "image_points_px": [ + [ + 1212.0, + 187.0 + ], + [ + 1245.0, + 192.0 + ], + [ + 1243.0, + 207.0 + ], + [ + 1209.0, + 201.0 + ] + ], + "center_px": [ + 1227.25, + 196.75 + ], + "area_px": 499.5 + }, + { + "image_points_px": [ + [ + 291.0, + 171.0 + ], + [ + 306.0, + 158.0 + ], + [ + 334.0, + 163.0 + ], + [ + 318.0, + 177.0 + ] + ], + "center_px": [ + 312.25, + 167.25 + ], + "area_px": 456.5 + }, + { + "image_points_px": [ + [ + 21.0, + 136.0 + ], + [ + 38.0, + 123.0 + ], + [ + 65.0, + 127.0 + ], + [ + 46.0, + 140.0 + ] + ], + "center_px": [ + 42.5, + 131.5 + ], + "area_px": 410.0 + }, + { + "image_points_px": [ + [ + 1104.0, + 185.0 + ], + [ + 1137.0, + 190.0 + ], + [ + 1133.0, + 205.0 + ], + [ + 1100.0, + 199.0 + ] + ], + "center_px": [ + 1118.5, + 194.75 + ], + "area_px": 500.5 + }, + { + "image_points_px": [ + [ + 233.0, + 160.0 + ], + [ + 248.0, + 147.0 + ], + [ + 276.0, + 151.0 + ], + [ + 260.0, + 165.0 + ] + ], + "center_px": [ + 254.25, + 155.75 + ], + "area_px": 441.0 + }, + { + "image_points_px": [ + [ + 177.0, + 149.0 + ], + [ + 193.0, + 135.0 + ], + [ + 220.0, + 140.0 + ], + [ + 204.0, + 153.0 + ] + ], + "center_px": [ + 198.5, + 144.25 + ], + "area_px": 436.5 + }, + { + "image_points_px": [ + [ + 278.0, + 151.0 + ], + [ + 293.0, + 138.0 + ], + [ + 321.0, + 142.0 + ], + [ + 306.0, + 155.0 + ] + ], + "center_px": [ + 299.5, + 146.5 + ], + "area_px": 424.0 + }, + { + "image_points_px": [ + [ + 222.0, + 139.0 + ], + [ + 238.0, + 126.0 + ], + [ + 265.0, + 131.0 + ], + [ + 249.0, + 144.0 + ] + ], + "center_px": [ + 243.5, + 135.0 + ], + "area_px": 431.0 + }, + { + "image_points_px": [ + [ + 53.0, + 221.0 + ], + [ + 70.0, + 206.0 + ], + [ + 95.0, + 212.0 + ], + [ + 79.0, + 226.0 + ] + ], + "center_px": [ + 74.25, + 216.25 + ], + "area_px": 460.5 + }, + { + "image_points_px": [ + [ + 102.0, + 210.0 + ], + [ + 119.0, + 196.0 + ], + [ + 144.0, + 202.0 + ], + [ + 128.0, + 216.0 + ] + ], + "center_px": [ + 123.25, + 206.0 + ], + "area_px": 456.0 + }, + { + "image_points_px": [ + [ + 1204.0, + 221.0 + ], + [ + 1237.0, + 225.0 + ], + [ + 1236.0, + 239.0 + ], + [ + 1202.0, + 235.0 + ] + ], + "center_px": [ + 1219.75, + 230.0 + ], + "area_px": 475.0 + }, + { + "image_points_px": [ + [ + 381.0, + 153.0 + ], + [ + 395.0, + 140.0 + ], + [ + 423.0, + 144.0 + ], + [ + 409.0, + 158.0 + ] + ], + "center_px": [ + 402.0, + 148.75 + ], + "area_px": 441.0 + }, + { + "image_points_px": [ + [ + 440.0, + 164.0 + ], + [ + 453.0, + 151.0 + ], + [ + 482.0, + 156.0 + ], + [ + 469.0, + 169.0 + ] + ], + "center_px": [ + 461.0, + 160.0 + ], + "area_px": 442.0 + }, + { + "image_points_px": [ + [ + 1143.0, + 176.0 + ], + [ + 1176.0, + 180.0 + ], + [ + 1173.0, + 194.0 + ], + [ + 1139.0, + 189.0 + ] + ], + "center_px": [ + 1157.75, + 184.75 + ], + "area_px": 468.0 + }, + { + "image_points_px": [ + [ + 1219.0, + 156.0 + ], + [ + 1252.0, + 161.0 + ], + [ + 1249.0, + 175.0 + ], + [ + 1216.0, + 170.0 + ] + ], + "center_px": [ + 1234.0, + 165.5 + ], + "area_px": 477.0 + }, + { + "image_points_px": [ + [ + 67.0, + 127.0 + ], + [ + 85.0, + 114.0 + ], + [ + 110.0, + 118.0 + ], + [ + 93.0, + 131.0 + ] + ], + "center_px": [ + 88.75, + 122.5 + ], + "area_px": 401.5 + }, + { + "image_points_px": [ + [ + 121.0, + 137.0 + ], + [ + 138.0, + 125.0 + ], + [ + 164.0, + 129.0 + ], + [ + 148.0, + 142.0 + ] + ], + "center_px": [ + 142.75, + 133.25 + ], + "area_px": 405.5 + }, + { + "image_points_px": [ + [ + 1073.0, + 215.0 + ], + [ + 1087.0, + 234.0 + ], + [ + 1092.0, + 257.0 + ], + [ + 1085.0, + 253.0 + ] + ], + "center_px": [ + 1084.25, + 239.75 + ], + "area_px": 222.5 + }, + { + "image_points_px": [ + [ + 1182.0, + 165.0 + ], + [ + 1214.0, + 171.0 + ], + [ + 1211.0, + 185.0 + ], + [ + 1178.0, + 179.0 + ] + ], + "center_px": [ + 1196.25, + 175.0 + ], + "area_px": 476.0 + }, + { + "image_points_px": [ + [ + 484.0, + 155.0 + ], + [ + 496.0, + 142.0 + ], + [ + 525.0, + 146.0 + ], + [ + 513.0, + 160.0 + ] + ], + "center_px": [ + 504.5, + 150.75 + ], + "area_px": 445.5 + }, + { + "image_points_px": [ + [ + 112.0, + 118.0 + ], + [ + 129.0, + 106.0 + ], + [ + 155.0, + 110.0 + ], + [ + 139.0, + 122.0 + ] + ], + "center_px": [ + 133.75, + 114.0 + ], + "area_px": 384.0 + }, + { + "image_points_px": [ + [ + 14.0, + 116.0 + ], + [ + 31.0, + 104.0 + ], + [ + 57.0, + 108.0 + ], + [ + 40.0, + 120.0 + ] + ], + "center_px": [ + 35.5, + 112.0 + ], + "area_px": 380.0 + }, + { + "image_points_px": [ + [ + 1076.0, + 163.0 + ], + [ + 1107.0, + 168.0 + ], + [ + 1104.0, + 182.0 + ], + [ + 1071.0, + 177.0 + ] + ], + "center_px": [ + 1089.5, + 172.5 + ], + "area_px": 468.0 + }, + { + "image_points_px": [ + [ + 1115.0, + 154.0 + ], + [ + 1147.0, + 159.0 + ], + [ + 1143.0, + 173.0 + ], + [ + 1111.0, + 168.0 + ] + ], + "center_px": [ + 1129.0, + 163.5 + ], + "area_px": 468.0 + }, + { + "image_points_px": [ + [ + 167.0, + 129.0 + ], + [ + 183.0, + 116.0 + ], + [ + 209.0, + 120.0 + ], + [ + 193.0, + 133.0 + ] + ], + "center_px": [ + 188.0, + 124.5 + ], + "area_px": 402.0 + }, + { + "image_points_px": [ + [ + 1243.0, + 211.0 + ], + [ + 1276.0, + 215.0 + ], + [ + 1276.0, + 228.0 + ], + [ + 1242.0, + 224.0 + ] + ], + "center_px": [ + 1259.25, + 219.5 + ], + "area_px": 437.5 + }, + { + "image_points_px": [ + [ + 410.0, + 124.0 + ], + [ + 424.0, + 111.0 + ], + [ + 451.0, + 116.0 + ], + [ + 438.0, + 129.0 + ] + ], + "center_px": [ + 430.75, + 120.0 + ], + "area_px": 425.0 + }, + { + "image_points_px": [ + [ + 211.0, + 120.0 + ], + [ + 226.0, + 108.0 + ], + [ + 253.0, + 112.0 + ], + [ + 239.0, + 124.0 + ] + ], + "center_px": [ + 232.25, + 116.0 + ], + "area_px": 388.0 + }, + { + "image_points_px": [ + [ + 755.0, + 152.0 + ], + [ + 762.0, + 144.0 + ], + [ + 796.0, + 151.0 + ], + [ + 790.0, + 161.0 + ] + ], + "center_px": [ + 775.75, + 152.0 + ], + "area_px": 362.5 + }, + { + "image_points_px": [ + [ + 367.0, + 133.0 + ], + [ + 381.0, + 120.0 + ], + [ + 408.0, + 124.0 + ], + [ + 395.0, + 137.0 + ] + ], + "center_px": [ + 387.75, + 128.5 + ], + "area_px": 411.5 + }, + { + "image_points_px": [ + [ + 60.0, + 108.0 + ], + [ + 77.0, + 95.0 + ], + [ + 102.0, + 100.0 + ], + [ + 85.0, + 112.0 + ] + ], + "center_px": [ + 81.0, + 103.75 + ], + "area_px": 389.0 + }, + { + "image_points_px": [ + [ + 7.0, + 97.0 + ], + [ + 25.0, + 86.0 + ], + [ + 50.0, + 90.0 + ], + [ + 33.0, + 101.0 + ] + ], + "center_px": [ + 28.75, + 93.5 + ], + "area_px": 350.5 + }, + { + "image_points_px": [ + [ + 441.0, + 241.0 + ], + [ + 455.0, + 226.0 + ], + [ + 480.0, + 230.0 + ], + [ + 467.0, + 246.0 + ] + ], + "center_px": [ + 460.75, + 235.75 + ], + "area_px": 456.0 + }, + { + "image_points_px": [ + [ + 267.0, + 131.0 + ], + [ + 282.0, + 118.0 + ], + [ + 308.0, + 122.0 + ], + [ + 294.0, + 135.0 + ] + ], + "center_px": [ + 287.75, + 126.5 + ], + "area_px": 402.5 + }, + { + "image_points_px": [ + [ + 1005.0, + 165.0 + ], + [ + 1011.0, + 152.0 + ], + [ + 1042.0, + 157.0 + ], + [ + 1036.0, + 171.0 + ] + ], + "center_px": [ + 1023.5, + 161.25 + ], + "area_px": 451.5 + }, + { + "image_points_px": [ + [ + 104.0, + 99.0 + ], + [ + 121.0, + 87.0 + ], + [ + 146.0, + 91.0 + ], + [ + 130.0, + 103.0 + ] + ], + "center_px": [ + 125.25, + 95.0 + ], + "area_px": 372.0 + }, + { + "image_points_px": [ + [ + 511.0, + 126.0 + ], + [ + 523.0, + 113.0 + ], + [ + 551.0, + 118.0 + ], + [ + 539.0, + 131.0 + ] + ], + "center_px": [ + 531.0, + 122.0 + ], + "area_px": 424.0 + }, + { + "image_points_px": [ + [ + 52.0, + 89.0 + ], + [ + 70.0, + 77.0 + ], + [ + 94.0, + 82.0 + ], + [ + 77.0, + 93.0 + ] + ], + "center_px": [ + 73.25, + 85.25 + ], + "area_px": 360.5 + }, + { + "image_points_px": [ + [ + 1226.0, + 127.0 + ], + [ + 1258.0, + 132.0 + ], + [ + 1255.0, + 145.0 + ], + [ + 1223.0, + 140.0 + ] + ], + "center_px": [ + 1240.5, + 136.0 + ], + "area_px": 431.0 + }, + { + "image_points_px": [ + [ + 940.0, + 154.0 + ], + [ + 946.0, + 141.0 + ], + [ + 977.0, + 146.0 + ], + [ + 971.0, + 159.0 + ] + ], + "center_px": [ + 958.5, + 150.0 + ], + "area_px": 433.0 + }, + { + "image_points_px": [ + [ + 979.0, + 145.0 + ], + [ + 985.0, + 132.0 + ], + [ + 1016.0, + 137.0 + ], + [ + 1010.0, + 150.0 + ] + ], + "center_px": [ + 997.5, + 141.0 + ], + "area_px": 433.0 + }, + { + "image_points_px": [ + [ + 158.0, + 110.0 + ], + [ + 174.0, + 97.0 + ], + [ + 199.0, + 102.0 + ], + [ + 183.0, + 114.0 + ] + ], + "center_px": [ + 178.5, + 105.75 + ], + "area_px": 384.5 + }, + { + "image_points_px": [ + [ + 256.0, + 111.0 + ], + [ + 271.0, + 99.0 + ], + [ + 297.0, + 104.0 + ], + [ + 282.0, + 116.0 + ] + ], + "center_px": [ + 276.5, + 107.5 + ], + "area_px": 387.0 + }, + { + "image_points_px": [ + [ + 1189.0, + 137.0 + ], + [ + 1221.0, + 141.0 + ], + [ + 1219.0, + 154.0 + ], + [ + 1186.0, + 149.0 + ] + ], + "center_px": [ + 1203.75, + 145.25 + ], + "area_px": 417.5 + }, + { + "image_points_px": [ + [ + 45.0, + 71.0 + ], + [ + 62.0, + 60.0 + ], + [ + 87.0, + 64.0 + ], + [ + 70.0, + 75.0 + ] + ], + "center_px": [ + 66.0, + 67.5 + ], + "area_px": 343.0 + }, + { + "image_points_px": [ + [ + 471.0, + 208.0 + ], + [ + 485.0, + 193.0 + ], + [ + 510.0, + 198.0 + ], + [ + 496.0, + 212.0 + ] + ], + "center_px": [ + 490.5, + 202.75 + ], + "area_px": 425.5 + }, + { + "image_points_px": [ + [ + 201.0, + 101.0 + ], + [ + 217.0, + 89.0 + ], + [ + 242.0, + 93.0 + ], + [ + 228.0, + 105.0 + ] + ], + "center_px": [ + 222.0, + 97.0 + ], + "area_px": 372.0 + }, + { + "image_points_px": [ + [ + 148.0, + 91.0 + ], + [ + 164.0, + 79.0 + ], + [ + 189.0, + 83.0 + ], + [ + 174.0, + 95.0 + ] + ], + "center_px": [ + 168.75, + 87.0 + ], + "area_px": 368.0 + }, + { + "image_points_px": [ + [ + 354.0, + 113.0 + ], + [ + 368.0, + 101.0 + ], + [ + 394.0, + 105.0 + ], + [ + 381.0, + 118.0 + ] + ], + "center_px": [ + 374.25, + 109.25 + ], + "area_px": 392.0 + }, + { + "image_points_px": [ + [ + 1152.0, + 146.0 + ], + [ + 1184.0, + 150.0 + ], + [ + 1181.0, + 163.0 + ], + [ + 1149.0, + 158.0 + ] + ], + "center_px": [ + 1166.5, + 154.25 + ], + "area_px": 413.5 + }, + { + "image_points_px": [ + [ + 191.0, + 83.0 + ], + [ + 206.0, + 72.0 + ], + [ + 232.0, + 75.0 + ], + [ + 217.0, + 87.0 + ] + ], + "center_px": [ + 211.5, + 79.25 + ], + "area_px": 351.5 + }, + { + "image_points_px": [ + [ + 96.0, + 81.0 + ], + [ + 113.0, + 69.0 + ], + [ + 137.0, + 74.0 + ], + [ + 121.0, + 85.0 + ] + ], + "center_px": [ + 116.75, + 77.25 + ], + "area_px": 356.0 + }, + { + "image_points_px": [ + [ + 397.0, + 105.0 + ], + [ + 410.0, + 93.0 + ], + [ + 437.0, + 97.0 + ], + [ + 424.0, + 109.0 + ] + ], + "center_px": [ + 417.0, + 101.0 + ], + "area_px": 376.0 + }, + { + "image_points_px": [ + [ + 814.0, + 132.0 + ], + [ + 822.0, + 119.0 + ], + [ + 851.0, + 124.0 + ], + [ + 844.0, + 137.0 + ] + ], + "center_px": [ + 832.75, + 128.0 + ], + "area_px": 421.0 + }, + { + "image_points_px": [ + [ + 341.0, + 95.0 + ], + [ + 355.0, + 83.0 + ], + [ + 381.0, + 87.0 + ], + [ + 368.0, + 99.0 + ] + ], + "center_px": [ + 361.25, + 91.0 + ], + "area_px": 372.0 + }, + { + "image_points_px": [ + [ + 1161.0, + 117.0 + ], + [ + 1192.0, + 121.0 + ], + [ + 1189.0, + 134.0 + ], + [ + 1157.0, + 129.0 + ] + ], + "center_px": [ + 1174.75, + 125.25 + ], + "area_px": 409.5 + }, + { + "image_points_px": [ + [ + 1018.0, + 136.0 + ], + [ + 1024.0, + 123.0 + ], + [ + 1054.0, + 128.0 + ], + [ + 1048.0, + 141.0 + ] + ], + "center_px": [ + 1036.0, + 132.0 + ], + "area_px": 420.0 + }, + { + "image_points_px": [ + [ + 454.0, + 115.0 + ], + [ + 466.0, + 103.0 + ], + [ + 493.0, + 107.0 + ], + [ + 481.0, + 120.0 + ] + ], + "center_px": [ + 473.5, + 111.25 + ], + "area_px": 391.5 + }, + { + "image_points_px": [ + [ + 245.0, + 93.0 + ], + [ + 260.0, + 81.0 + ], + [ + 285.0, + 85.0 + ], + [ + 271.0, + 97.0 + ] + ], + "center_px": [ + 265.25, + 89.0 + ], + "area_px": 364.0 + }, + { + "image_points_px": [ + [ + 853.0, + 123.0 + ], + [ + 861.0, + 111.0 + ], + [ + 890.0, + 115.0 + ], + [ + 883.0, + 128.0 + ] + ], + "center_px": [ + 871.75, + 119.25 + ], + "area_px": 402.5 + }, + { + "image_points_px": [ + [ + 495.0, + 107.0 + ], + [ + 507.0, + 95.0 + ], + [ + 534.0, + 99.0 + ], + [ + 523.0, + 111.0 + ] + ], + "center_px": [ + 514.75, + 103.0 + ], + "area_px": 376.0 + }, + { + "image_points_px": [ + [ + 38.0, + 54.0 + ], + [ + 54.0, + 44.0 + ], + [ + 79.0, + 47.0 + ], + [ + 62.0, + 58.0 + ] + ], + "center_px": [ + 58.25, + 50.75 + ], + "area_px": 315.0 + }, + { + "image_points_px": [ + [ + 439.0, + 97.0 + ], + [ + 451.0, + 85.0 + ], + [ + 478.0, + 89.0 + ], + [ + 466.0, + 101.0 + ] + ], + "center_px": [ + 458.5, + 93.0 + ], + "area_px": 372.0 + }, + { + "image_points_px": [ + [ + 877.0, + 143.0 + ], + [ + 884.0, + 130.0 + ], + [ + 913.0, + 135.0 + ], + [ + 906.0, + 148.0 + ] + ], + "center_px": [ + 895.0, + 139.0 + ], + "area_px": 412.0 + }, + { + "image_points_px": [ + [ + 992.0, + 116.0 + ], + [ + 998.0, + 104.0 + ], + [ + 1028.0, + 109.0 + ], + [ + 1023.0, + 121.0 + ] + ], + "center_px": [ + 1010.25, + 112.5 + ], + "area_px": 393.5 + }, + { + "image_points_px": [ + [ + 234.0, + 75.0 + ], + [ + 249.0, + 64.0 + ], + [ + 274.0, + 68.0 + ], + [ + 260.0, + 79.0 + ] + ], + "center_px": [ + 254.25, + 71.5 + ], + "area_px": 338.5 + }, + { + "image_points_px": [ + [ + 276.0, + 67.0 + ], + [ + 291.0, + 56.0 + ], + [ + 316.0, + 60.0 + ], + [ + 302.0, + 71.0 + ] + ], + "center_px": [ + 296.25, + 63.5 + ], + "area_px": 338.5 + }, + { + "image_points_px": [ + [ + 480.0, + 88.0 + ], + [ + 492.0, + 77.0 + ], + [ + 519.0, + 81.0 + ], + [ + 507.0, + 93.0 + ] + ], + "center_px": [ + 499.5, + 84.75 + ], + "area_px": 364.5 + }, + { + "image_points_px": [ + [ + 384.0, + 87.0 + ], + [ + 397.0, + 75.0 + ], + [ + 423.0, + 79.0 + ], + [ + 410.0, + 91.0 + ] + ], + "center_px": [ + 403.5, + 83.0 + ], + "area_px": 364.0 + }, + { + "image_points_px": [ + [ + 140.0, + 73.0 + ], + [ + 155.0, + 62.0 + ], + [ + 180.0, + 66.0 + ], + [ + 165.0, + 77.0 + ] + ], + "center_px": [ + 160.0, + 69.5 + ], + "area_px": 335.0 + }, + { + "image_points_px": [ + [ + 831.0, + 104.0 + ], + [ + 839.0, + 92.0 + ], + [ + 868.0, + 97.0 + ], + [ + 860.0, + 109.0 + ] + ], + "center_px": [ + 849.5, + 100.5 + ], + "area_px": 388.0 + }, + { + "image_points_px": [ + [ + 182.0, + 65.0 + ], + [ + 198.0, + 54.0 + ], + [ + 222.0, + 58.0 + ], + [ + 207.0, + 69.0 + ] + ], + "center_px": [ + 202.25, + 61.5 + ], + "area_px": 331.5 + }, + { + "image_points_px": [ + [ + 89.0, + 64.0 + ], + [ + 104.0, + 53.0 + ], + [ + 129.0, + 56.0 + ], + [ + 114.0, + 67.0 + ] + ], + "center_px": [ + 109.0, + 60.0 + ], + "area_px": 320.0 + }, + { + "image_points_px": [ + [ + 1197.0, + 108.0 + ], + [ + 1228.0, + 113.0 + ], + [ + 1225.0, + 125.0 + ], + [ + 1194.0, + 120.0 + ] + ], + "center_px": [ + 1211.0, + 116.5 + ], + "area_px": 387.0 + }, + { + "image_points_px": [ + [ + 287.0, + 85.0 + ], + [ + 302.0, + 73.0 + ], + [ + 326.0, + 77.0 + ], + [ + 313.0, + 89.0 + ] + ], + "center_px": [ + 307.0, + 81.0 + ], + "area_px": 356.0 + }, + { + "image_points_px": [ + [ + 131.0, + 56.0 + ], + [ + 147.0, + 45.0 + ], + [ + 171.0, + 49.0 + ], + [ + 155.0, + 60.0 + ] + ], + "center_px": [ + 151.0, + 52.5 + ], + "area_px": 328.0 + }, + { + "image_points_px": [ + [ + 537.0, + 99.0 + ], + [ + 547.0, + 87.0 + ], + [ + 575.0, + 91.0 + ], + [ + 564.0, + 103.0 + ] + ], + "center_px": [ + 555.75, + 95.0 + ], + "area_px": 372.0 + }, + { + "image_points_px": [ + [ + 1093.0, + 118.0 + ], + [ + 1098.0, + 106.0 + ], + [ + 1128.0, + 111.0 + ], + [ + 1124.0, + 123.0 + ] + ], + "center_px": [ + 1110.75, + 114.5 + ], + "area_px": 388.5 + }, + { + "image_points_px": [ + [ + 1045.0, + 155.0 + ], + [ + 1050.0, + 143.0 + ], + [ + 1080.0, + 149.0 + ], + [ + 1075.0, + 161.0 + ] + ], + "center_px": [ + 1062.5, + 152.0 + ], + "area_px": 390.0 + }, + { + "image_points_px": [ + [ + 1083.0, + 146.0 + ], + [ + 1088.0, + 134.0 + ], + [ + 1118.0, + 140.0 + ], + [ + 1113.0, + 152.0 + ] + ], + "center_px": [ + 1100.5, + 143.0 + ], + "area_px": 390.0 + }, + { + "image_points_px": [ + [ + 892.0, + 114.0 + ], + [ + 899.0, + 102.0 + ], + [ + 928.0, + 107.0 + ], + [ + 922.0, + 119.0 + ] + ], + "center_px": [ + 910.25, + 110.5 + ], + "area_px": 386.5 + }, + { + "image_points_px": [ + [ + 930.0, + 106.0 + ], + [ + 937.0, + 94.0 + ], + [ + 966.0, + 99.0 + ], + [ + 960.0, + 111.0 + ] + ], + "center_px": [ + 948.25, + 102.5 + ], + "area_px": 386.5 + }, + { + "image_points_px": [ + [ + 577.0, + 90.0 + ], + [ + 587.0, + 79.0 + ], + [ + 615.0, + 83.0 + ], + [ + 604.0, + 95.0 + ] + ], + "center_px": [ + 595.75, + 86.75 + ], + "area_px": 363.5 + }, + { + "image_points_px": [ + [ + 81.0, + 47.0 + ], + [ + 98.0, + 36.0 + ], + [ + 121.0, + 40.0 + ], + [ + 106.0, + 50.0 + ] + ], + "center_px": [ + 101.5, + 43.25 + ], + "area_px": 308.0 + }, + { + "image_points_px": [ + [ + 411.0, + 61.0 + ], + [ + 424.0, + 50.0 + ], + [ + 450.0, + 54.0 + ], + [ + 437.0, + 65.0 + ] + ], + "center_px": [ + 430.5, + 57.5 + ], + "area_px": 338.0 + }, + { + "image_points_px": [ + [ + 32.0, + 38.0 + ], + [ + 49.0, + 27.0 + ], + [ + 72.0, + 31.0 + ], + [ + 56.0, + 41.0 + ] + ], + "center_px": [ + 52.25, + 34.25 + ], + "area_px": 304.5 + }, + { + "image_points_px": [ + [ + 1005.0, + 90.0 + ], + [ + 1010.0, + 78.0 + ], + [ + 1040.0, + 82.0 + ], + [ + 1035.0, + 94.0 + ] + ], + "center_px": [ + 1022.5, + 86.0 + ], + "area_px": 380.0 + }, + { + "image_points_px": [ + [ + 521.0, + 80.0 + ], + [ + 532.0, + 69.0 + ], + [ + 559.0, + 73.0 + ], + [ + 547.0, + 85.0 + ] + ], + "center_px": [ + 539.75, + 76.75 + ], + "area_px": 356.5 + }, + { + "image_points_px": [ + [ + 425.0, + 79.0 + ], + [ + 438.0, + 67.0 + ], + [ + 463.0, + 71.0 + ], + [ + 451.0, + 83.0 + ] + ], + "center_px": [ + 444.25, + 75.0 + ], + "area_px": 356.0 + }, + { + "image_points_px": [ + [ + 25.0, + 22.0 + ], + [ + 41.0, + 12.0 + ], + [ + 65.0, + 15.0 + ], + [ + 49.0, + 25.0 + ] + ], + "center_px": [ + 45.0, + 18.5 + ], + "area_px": 288.0 + }, + { + "image_points_px": [ + [ + 224.0, + 58.0 + ], + [ + 238.0, + 47.0 + ], + [ + 263.0, + 51.0 + ], + [ + 248.0, + 62.0 + ] + ], + "center_px": [ + 243.25, + 54.5 + ], + "area_px": 327.5 + }, + { + "image_points_px": [ + [ + 465.0, + 71.0 + ], + [ + 477.0, + 60.0 + ], + [ + 503.0, + 63.0 + ], + [ + 492.0, + 75.0 + ] + ], + "center_px": [ + 484.25, + 67.25 + ], + "area_px": 345.0 + }, + { + "image_points_px": [ + [ + 968.0, + 98.0 + ], + [ + 974.0, + 86.0 + ], + [ + 1003.0, + 90.0 + ], + [ + 998.0, + 102.0 + ] + ], + "center_px": [ + 985.75, + 94.0 + ], + "area_px": 376.0 + }, + { + "image_points_px": [ + [ + 1067.0, + 100.0 + ], + [ + 1071.0, + 88.0 + ], + [ + 1101.0, + 92.0 + ], + [ + 1097.0, + 104.0 + ] + ], + "center_px": [ + 1084.0, + 96.0 + ], + "area_px": 376.0 + }, + { + "image_points_px": [ + [ + 427.0, + 143.0 + ], + [ + 438.0, + 131.0 + ], + [ + 464.0, + 136.0 + ], + [ + 453.0, + 148.0 + ] + ], + "center_px": [ + 445.5, + 139.5 + ], + "area_px": 367.0 + }, + { + "image_points_px": [ + [ + 470.0, + 134.0 + ], + [ + 481.0, + 122.0 + ], + [ + 507.0, + 127.0 + ], + [ + 496.0, + 139.0 + ] + ], + "center_px": [ + 488.5, + 130.5 + ], + "area_px": 367.0 + }, + { + "image_points_px": [ + [ + 505.0, + 63.0 + ], + [ + 517.0, + 52.0 + ], + [ + 543.0, + 56.0 + ], + [ + 532.0, + 67.0 + ] + ], + "center_px": [ + 524.25, + 59.5 + ], + "area_px": 337.5 + }, + { + "image_points_px": [ + [ + 173.0, + 48.0 + ], + [ + 188.0, + 38.0 + ], + [ + 212.0, + 41.0 + ], + [ + 198.0, + 52.0 + ] + ], + "center_px": [ + 192.75, + 44.75 + ], + "area_px": 308.0 + }, + { + "image_points_px": [ + [ + 214.0, + 41.0 + ], + [ + 228.0, + 31.0 + ], + [ + 253.0, + 34.0 + ], + [ + 238.0, + 45.0 + ] + ], + "center_px": [ + 233.25, + 37.75 + ], + "area_px": 308.0 + }, + { + "image_points_px": [ + [ + 1169.0, + 90.0 + ], + [ + 1199.0, + 94.0 + ], + [ + 1196.0, + 106.0 + ], + [ + 1166.0, + 102.0 + ] + ], + "center_px": [ + 1182.5, + 98.0 + ], + "area_px": 372.0 + }, + { + "image_points_px": [ + [ + 700.0, + 560.0 + ], + [ + 685.0, + 588.0 + ], + [ + 674.0, + 585.0 + ], + [ + 689.0, + 558.0 + ] + ], + "center_px": [ + 687.0, + 572.75 + ], + "area_px": 340.0 + }, + { + "image_points_px": [ + [ + 1204.0, + 82.0 + ], + [ + 1234.0, + 86.0 + ], + [ + 1232.0, + 98.0 + ], + [ + 1201.0, + 93.0 + ] + ], + "center_px": [ + 1217.75, + 89.75 + ], + "area_px": 362.0 + }, + { + "image_points_px": [ + [ + 810.0, + 86.0 + ], + [ + 818.0, + 74.0 + ], + [ + 846.0, + 79.0 + ], + [ + 839.0, + 90.0 + ] + ], + "center_px": [ + 828.25, + 82.25 + ], + "area_px": 361.5 + }, + { + "image_points_px": [ + [ + 371.0, + 69.0 + ], + [ + 383.0, + 58.0 + ], + [ + 409.0, + 62.0 + ], + [ + 397.0, + 73.0 + ] + ], + "center_px": [ + 390.0, + 65.5 + ], + "area_px": 334.0 + }, + { + "image_points_px": [ + [ + 916.0, + 133.0 + ], + [ + 922.0, + 122.0 + ], + [ + 951.0, + 127.0 + ], + [ + 945.0, + 139.0 + ] + ], + "center_px": [ + 933.5, + 130.25 + ], + "area_px": 366.5 + }, + { + "image_points_px": [ + [ + 1133.0, + 99.0 + ], + [ + 1164.0, + 104.0 + ], + [ + 1160.0, + 115.0 + ], + [ + 1130.0, + 110.0 + ] + ], + "center_px": [ + 1146.75, + 107.0 + ], + "area_px": 353.0 + }, + { + "image_points_px": [ + [ + 907.0, + 88.0 + ], + [ + 914.0, + 76.0 + ], + [ + 942.0, + 80.0 + ], + [ + 936.0, + 92.0 + ] + ], + "center_px": [ + 924.75, + 84.0 + ], + "area_px": 368.0 + }, + { + "image_points_px": [ + [ + 870.0, + 96.0 + ], + [ + 877.0, + 84.0 + ], + [ + 905.0, + 88.0 + ], + [ + 899.0, + 100.0 + ] + ], + "center_px": [ + 887.75, + 92.0 + ], + "area_px": 368.0 + }, + { + "image_points_px": [ + [ + 123.0, + 39.0 + ], + [ + 138.0, + 29.0 + ], + [ + 162.0, + 33.0 + ], + [ + 147.0, + 43.0 + ] + ], + "center_px": [ + 142.5, + 36.0 + ], + "area_px": 300.0 + }, + { + "image_points_px": [ + [ + 358.0, + 52.0 + ], + [ + 371.0, + 41.0 + ], + [ + 396.0, + 45.0 + ], + [ + 383.0, + 56.0 + ] + ], + "center_px": [ + 377.0, + 48.5 + ], + "area_px": 327.0 + }, + { + "image_points_px": [ + [ + 561.0, + 73.0 + ], + [ + 571.0, + 62.0 + ], + [ + 598.0, + 65.0 + ], + [ + 587.0, + 77.0 + ] + ], + "center_px": [ + 579.25, + 69.25 + ], + "area_px": 341.5 + }, + { + "image_points_px": [ + [ + 74.0, + 30.0 + ], + [ + 90.0, + 20.0 + ], + [ + 113.0, + 24.0 + ], + [ + 98.0, + 34.0 + ] + ], + "center_px": [ + 93.75, + 27.0 + ], + "area_px": 297.0 + }, + { + "image_points_px": [ + [ + 1142.0, + 72.0 + ], + [ + 1172.0, + 76.0 + ], + [ + 1169.0, + 88.0 + ], + [ + 1139.0, + 83.0 + ] + ], + "center_px": [ + 1155.5, + 79.75 + ], + "area_px": 358.5 + }, + { + "image_points_px": [ + [ + 115.0, + 23.0 + ], + [ + 131.0, + 13.0 + ], + [ + 154.0, + 17.0 + ], + [ + 138.0, + 27.0 + ] + ], + "center_px": [ + 134.5, + 20.0 + ], + "area_px": 294.0 + }, + { + "image_points_px": [ + [ + 1103.0, + 90.0 + ], + [ + 1107.0, + 80.0 + ], + [ + 1137.0, + 84.0 + ], + [ + 1133.0, + 96.0 + ] + ], + "center_px": [ + 1120.0, + 87.5 + ], + "area_px": 350.0 + }, + { + "image_points_px": [ + [ + 1056.0, + 126.0 + ], + [ + 1061.0, + 115.0 + ], + [ + 1090.0, + 120.0 + ], + [ + 1085.0, + 132.0 + ] + ], + "center_px": [ + 1073.0, + 123.25 + ], + "area_px": 361.0 + }, + { + "image_points_px": [ + [ + 583.0, + 48.0 + ], + [ + 594.0, + 37.0 + ], + [ + 620.0, + 41.0 + ], + [ + 610.0, + 52.0 + ] + ], + "center_px": [ + 601.75, + 44.5 + ], + "area_px": 333.5 + }, + { + "image_points_px": [ + [ + 1046.0, + 70.0 + ], + [ + 1075.0, + 74.0 + ], + [ + 1071.0, + 86.0 + ], + [ + 1042.0, + 82.0 + ] + ], + "center_px": [ + 1058.5, + 78.0 + ], + "area_px": 364.0 + }, + { + "image_points_px": [ + [ + 266.0, + 50.0 + ], + [ + 279.0, + 40.0 + ], + [ + 304.0, + 43.0 + ], + [ + 290.0, + 54.0 + ] + ], + "center_px": [ + 284.75, + 46.75 + ], + "area_px": 304.5 + }, + { + "image_points_px": [ + [ + 346.0, + 35.0 + ], + [ + 359.0, + 25.0 + ], + [ + 384.0, + 29.0 + ], + [ + 372.0, + 39.0 + ] + ], + "center_px": [ + 365.25, + 32.0 + ], + "area_px": 305.0 + }, + { + "image_points_px": [ + [ + 955.0, + 124.0 + ], + [ + 961.0, + 113.0 + ], + [ + 989.0, + 118.0 + ], + [ + 984.0, + 130.0 + ] + ], + "center_px": [ + 972.25, + 121.25 + ], + "area_px": 358.0 + }, + { + "image_points_px": [ + [ + 529.0, + 39.0 + ], + [ + 541.0, + 28.0 + ], + [ + 566.0, + 32.0 + ], + [ + 554.0, + 43.0 + ] + ], + "center_px": [ + 547.5, + 35.5 + ], + "area_px": 323.0 + }, + { + "image_points_px": [ + [ + 164.0, + 32.0 + ], + [ + 179.0, + 22.0 + ], + [ + 202.0, + 25.0 + ], + [ + 187.0, + 36.0 + ] + ], + "center_px": [ + 183.0, + 28.75 + ], + "area_px": 294.0 + }, + { + "image_points_px": [ + [ + 205.0, + 25.0 + ], + [ + 219.0, + 15.0 + ], + [ + 243.0, + 18.0 + ], + [ + 230.0, + 28.0 + ] + ], + "center_px": [ + 224.25, + 21.5 + ], + "area_px": 285.5 + }, + { + "image_points_px": [ + [ + 452.0, + 54.0 + ], + [ + 463.0, + 43.0 + ], + [ + 489.0, + 47.0 + ], + [ + 478.0, + 57.0 + ] + ], + "center_px": [ + 470.5, + 50.25 + ], + "area_px": 311.5 + }, + { + "image_points_px": [ + [ + 156.0, + 16.0 + ], + [ + 171.0, + 6.0 + ], + [ + 194.0, + 10.0 + ], + [ + 179.0, + 20.0 + ] + ], + "center_px": [ + 175.0, + 13.0 + ], + "area_px": 290.0 + }, + { + "image_points_px": [ + [ + 1238.0, + 76.0 + ], + [ + 1269.0, + 78.0 + ], + [ + 1267.0, + 89.0 + ], + [ + 1236.0, + 85.0 + ] + ], + "center_px": [ + 1252.5, + 82.0 + ], + "area_px": 316.0 + }, + { + "image_points_px": [ + [ + 848.0, + 78.0 + ], + [ + 855.0, + 67.0 + ], + [ + 883.0, + 71.0 + ], + [ + 876.0, + 82.0 + ] + ], + "center_px": [ + 865.5, + 74.5 + ], + "area_px": 336.0 + }, + { + "image_points_px": [ + [ + 885.0, + 70.0 + ], + [ + 892.0, + 59.0 + ], + [ + 920.0, + 63.0 + ], + [ + 913.0, + 74.0 + ] + ], + "center_px": [ + 902.5, + 66.5 + ], + "area_px": 336.0 + }, + { + "image_points_px": [ + [ + 67.0, + 15.0 + ], + [ + 82.0, + 5.0 + ], + [ + 105.0, + 8.0 + ], + [ + 90.0, + 18.0 + ] + ], + "center_px": [ + 86.0, + 11.5 + ], + "area_px": 275.0 + }, + { + "image_points_px": [ + [ + 1230.0, + 110.0 + ], + [ + 1234.0, + 100.0 + ], + [ + 1263.0, + 105.0 + ], + [ + 1260.0, + 116.0 + ] + ], + "center_px": [ + 1246.75, + 107.75 + ], + "area_px": 329.0 + }, + { + "image_points_px": [ + [ + 981.0, + 72.0 + ], + [ + 987.0, + 61.0 + ], + [ + 1015.0, + 65.0 + ], + [ + 1010.0, + 76.0 + ] + ], + "center_px": [ + 998.25, + 68.5 + ], + "area_px": 335.5 + }, + { + "image_points_px": [ + [ + 1177.0, + 64.0 + ], + [ + 1206.0, + 68.0 + ], + [ + 1204.0, + 79.0 + ], + [ + 1174.0, + 75.0 + ] + ], + "center_px": [ + 1190.25, + 71.5 + ], + "area_px": 334.5 + }, + { + "image_points_px": [ + [ + 1245.0, + 49.0 + ], + [ + 1274.0, + 53.0 + ], + [ + 1272.0, + 64.0 + ], + [ + 1242.0, + 60.0 + ] + ], + "center_px": [ + 1258.25, + 56.5 + ], + "area_px": 334.5 + }, + { + "image_points_px": [ + [ + 437.0, + 37.0 + ], + [ + 449.0, + 27.0 + ], + [ + 474.0, + 31.0 + ], + [ + 463.0, + 41.0 + ] + ], + "center_px": [ + 455.75, + 34.0 + ], + "area_px": 301.0 + }, + { + "image_points_px": [ + [ + 545.0, + 55.0 + ], + [ + 556.0, + 44.0 + ], + [ + 581.0, + 48.0 + ], + [ + 571.0, + 59.0 + ] + ], + "center_px": [ + 563.25, + 51.5 + ], + "area_px": 322.5 + }, + { + "image_points_px": [ + [ + 827.0, + 60.0 + ], + [ + 834.0, + 50.0 + ], + [ + 862.0, + 54.0 + ], + [ + 855.0, + 65.0 + ] + ], + "center_px": [ + 844.5, + 57.25 + ], + "area_px": 325.5 + }, + { + "image_points_px": [ + [ + 424.0, + 21.0 + ], + [ + 436.0, + 11.0 + ], + [ + 461.0, + 15.0 + ], + [ + 449.0, + 25.0 + ] + ], + "center_px": [ + 442.5, + 18.0 + ], + "area_px": 298.0 + }, + { + "image_points_px": [ + [ + 600.0, + 65.0 + ], + [ + 610.0, + 54.0 + ], + [ + 635.0, + 57.0 + ], + [ + 627.0, + 69.0 + ] + ], + "center_px": [ + 618.0, + 61.25 + ], + "area_px": 330.5 + }, + { + "image_points_px": [ + [ + 256.0, + 34.0 + ], + [ + 268.0, + 24.0 + ], + [ + 293.0, + 27.0 + ], + [ + 280.0, + 37.0 + ] + ], + "center_px": [ + 274.25, + 30.5 + ], + "area_px": 282.5 + }, + { + "image_points_px": [ + [ + 245.0, + 18.0 + ], + [ + 259.0, + 8.0 + ], + [ + 282.0, + 11.0 + ], + [ + 270.0, + 21.0 + ] + ], + "center_px": [ + 264.0, + 14.5 + ], + "area_px": 279.0 + }, + { + "image_points_px": [ + [ + 864.0, + 53.0 + ], + [ + 871.0, + 42.0 + ], + [ + 898.0, + 46.0 + ], + [ + 892.0, + 57.0 + ] + ], + "center_px": [ + 881.25, + 49.5 + ], + "area_px": 328.5 + }, + { + "image_points_px": [ + [ + 491.0, + 46.0 + ], + [ + 502.0, + 36.0 + ], + [ + 527.0, + 39.0 + ], + [ + 516.0, + 50.0 + ] + ], + "center_px": [ + 509.0, + 42.75 + ], + "area_px": 301.0 + }, + { + "image_points_px": [ + [ + 1028.0, + 40.0 + ], + [ + 1033.0, + 29.0 + ], + [ + 1061.0, + 33.0 + ], + [ + 1056.0, + 44.0 + ] + ], + "center_px": [ + 1044.5, + 36.5 + ], + "area_px": 328.0 + }, + { + "image_points_px": [ + [ + 807.0, + 44.0 + ], + [ + 814.0, + 33.0 + ], + [ + 841.0, + 37.0 + ], + [ + 834.0, + 48.0 + ] + ], + "center_px": [ + 824.0, + 40.5 + ], + "area_px": 325.0 + }, + { + "image_points_px": [ + [ + 515.0, + 23.0 + ], + [ + 525.0, + 13.0 + ], + [ + 551.0, + 16.0 + ], + [ + 540.0, + 26.0 + ] + ], + "center_px": [ + 532.75, + 19.5 + ], + "area_px": 286.5 + }, + { + "image_points_px": [ + [ + 1031.0, + 107.0 + ], + [ + 1036.0, + 96.0 + ], + [ + 1064.0, + 102.0 + ], + [ + 1059.0, + 112.0 + ] + ], + "center_px": [ + 1047.5, + 104.25 + ], + "area_px": 321.5 + }, + { + "image_points_px": [ + [ + 770.0, + 51.0 + ], + [ + 778.0, + 41.0 + ], + [ + 805.0, + 45.0 + ], + [ + 797.0, + 55.0 + ] + ], + "center_px": [ + 787.5, + 48.0 + ], + "area_px": 302.0 + }, + { + "image_points_px": [ + [ + 714.0, + 42.0 + ], + [ + 722.0, + 32.0 + ], + [ + 749.0, + 36.0 + ], + [ + 741.0, + 46.0 + ] + ], + "center_px": [ + 731.5, + 39.0 + ], + "area_px": 302.0 + }, + { + "image_points_px": [ + [ + 399.0, + 44.0 + ], + [ + 411.0, + 34.0 + ], + [ + 435.0, + 38.0 + ], + [ + 424.0, + 48.0 + ] + ], + "center_px": [ + 417.25, + 41.0 + ], + "area_px": 291.0 + }, + { + "image_points_px": [ + [ + 661.0, + 224.0 + ], + [ + 673.0, + 228.0 + ], + [ + 668.0, + 254.0 + ], + [ + 655.0, + 251.0 + ] + ], + "center_px": [ + 664.25, + 239.25 + ], + "area_px": 350.5 + }, + { + "image_points_px": [ + [ + 386.0, + 28.0 + ], + [ + 398.0, + 18.0 + ], + [ + 422.0, + 22.0 + ], + [ + 410.0, + 32.0 + ] + ], + "center_px": [ + 404.0, + 25.0 + ], + "area_px": 288.0 + }, + { + "image_points_px": [ + [ + 900.0, + 45.0 + ], + [ + 906.0, + 35.0 + ], + [ + 934.0, + 39.0 + ], + [ + 928.0, + 49.0 + ] + ], + "center_px": [ + 917.0, + 42.0 + ], + "area_px": 304.0 + }, + { + "image_points_px": [ + [ + 374.0, + 13.0 + ], + [ + 386.0, + 3.0 + ], + [ + 410.0, + 6.0 + ], + [ + 398.0, + 16.0 + ] + ], + "center_px": [ + 392.0, + 9.5 + ], + "area_px": 276.0 + }, + { + "image_points_px": [ + [ + 659.0, + 33.0 + ], + [ + 668.0, + 23.0 + ], + [ + 694.0, + 27.0 + ], + [ + 685.0, + 37.0 + ] + ], + "center_px": [ + 676.5, + 30.0 + ], + "area_px": 296.0 + }, + { + "image_points_px": [ + [ + 1087.0, + 48.0 + ], + [ + 1092.0, + 38.0 + ], + [ + 1119.0, + 42.0 + ], + [ + 1116.0, + 53.0 + ] + ], + "center_px": [ + 1103.5, + 45.25 + ], + "area_px": 312.0 + }, + { + "image_points_px": [ + [ + 568.0, + 31.0 + ], + [ + 578.0, + 21.0 + ], + [ + 603.0, + 25.0 + ], + [ + 594.0, + 35.0 + ] + ], + "center_px": [ + 585.75, + 28.0 + ], + "area_px": 293.0 + }, + { + "image_points_px": [ + [ + 843.0, + 36.0 + ], + [ + 850.0, + 26.0 + ], + [ + 877.0, + 30.0 + ], + [ + 870.0, + 40.0 + ] + ], + "center_px": [ + 860.0, + 33.0 + ], + "area_px": 298.0 + }, + { + "image_points_px": [ + [ + 1217.0, + 33.0 + ], + [ + 1246.0, + 37.0 + ], + [ + 1244.0, + 47.0 + ], + [ + 1215.0, + 43.0 + ] + ], + "center_px": [ + 1230.5, + 40.0 + ], + "area_px": 298.0 + }, + { + "image_points_px": [ + [ + 751.0, + 35.0 + ], + [ + 759.0, + 25.0 + ], + [ + 785.0, + 28.0 + ], + [ + 777.0, + 39.0 + ] + ], + "center_px": [ + 768.0, + 31.75 + ], + "area_px": 301.0 + }, + { + "image_points_px": [ + [ + 696.0, + 26.0 + ], + [ + 705.0, + 16.0 + ], + [ + 730.0, + 19.0 + ], + [ + 722.0, + 30.0 + ] + ], + "center_px": [ + 713.25, + 22.75 + ], + "area_px": 297.5 + }, + { + "image_points_px": [ + [ + 1113.0, + 65.0 + ], + [ + 1117.0, + 55.0 + ], + [ + 1145.0, + 60.0 + ], + [ + 1141.0, + 70.0 + ] + ], + "center_px": [ + 1129.0, + 62.5 + ], + "area_px": 300.0 + }, + { + "image_points_px": [ + [ + 1063.0, + 32.0 + ], + [ + 1067.0, + 22.0 + ], + [ + 1095.0, + 26.0 + ], + [ + 1091.0, + 36.0 + ] + ], + "center_px": [ + 1079.0, + 29.0 + ], + "area_px": 296.0 + }, + { + "image_points_px": [ + [ + 823.0, + 20.0 + ], + [ + 830.0, + 11.0 + ], + [ + 857.0, + 14.0 + ], + [ + 850.0, + 24.0 + ] + ], + "center_px": [ + 840.0, + 17.25 + ], + "area_px": 281.0 + }, + { + "image_points_px": [ + [ + 879.0, + 29.0 + ], + [ + 885.0, + 19.0 + ], + [ + 912.0, + 23.0 + ], + [ + 906.0, + 33.0 + ] + ], + "center_px": [ + 895.5, + 26.0 + ], + "area_px": 294.0 + }, + { + "image_points_px": [ + [ + 1072.0, + 673.0 + ], + [ + 1094.0, + 679.0 + ], + [ + 1089.0, + 699.0 + ], + [ + 1071.0, + 683.0 + ] + ], + "center_px": [ + 1081.5, + 683.5 + ], + "area_px": 333.0 + }, + { + "image_points_px": [ + [ + 337.0, + 18.0 + ], + [ + 348.0, + 9.0 + ], + [ + 372.0, + 13.0 + ], + [ + 359.0, + 23.0 + ] + ], + "center_px": [ + 354.0, + 15.75 + ], + "area_px": 272.5 + }, + { + "image_points_px": [ + [ + 1005.0, + 24.0 + ], + [ + 1010.0, + 14.0 + ], + [ + 1037.0, + 17.0 + ], + [ + 1033.0, + 27.0 + ] + ], + "center_px": [ + 1021.25, + 20.5 + ], + "area_px": 288.5 + }, + { + "image_points_px": [ + [ + 606.0, + 24.0 + ], + [ + 616.0, + 14.0 + ], + [ + 640.0, + 18.0 + ], + [ + 631.0, + 28.0 + ] + ], + "center_px": [ + 623.25, + 21.0 + ], + "area_px": 283.0 + }, + { + "image_points_px": [ + [ + 1078.0, + 72.0 + ], + [ + 1082.0, + 63.0 + ], + [ + 1110.0, + 68.0 + ], + [ + 1105.0, + 78.0 + ] + ], + "center_px": [ + 1093.75, + 70.25 + ], + "area_px": 286.0 + }, + { + "image_points_px": [ + [ + 553.0, + 16.0 + ], + [ + 563.0, + 6.0 + ], + [ + 587.0, + 9.0 + ], + [ + 578.0, + 19.0 + ] + ], + "center_px": [ + 570.25, + 12.5 + ], + "area_px": 273.5 + }, + { + "image_points_px": [ + [ + 733.0, + 19.0 + ], + [ + 741.0, + 9.0 + ], + [ + 766.0, + 12.0 + ], + [ + 758.0, + 23.0 + ] + ], + "center_px": [ + 749.5, + 15.75 + ], + "area_px": 290.5 + }, + { + "image_points_px": [ + [ + 946.0, + 79.0 + ], + [ + 951.0, + 69.0 + ], + [ + 978.0, + 74.0 + ], + [ + 972.0, + 84.0 + ] + ], + "center_px": [ + 961.75, + 76.5 + ], + "area_px": 292.5 + }, + { + "image_points_px": [ + [ + 788.0, + 28.0 + ], + [ + 795.0, + 18.0 + ], + [ + 821.0, + 21.0 + ], + [ + 814.0, + 31.0 + ] + ], + "center_px": [ + 804.5, + 24.5 + ], + "area_px": 281.0 + }, + { + "image_points_px": [ + [ + 923.0, + 61.0 + ], + [ + 928.0, + 52.0 + ], + [ + 955.0, + 56.0 + ], + [ + 950.0, + 66.0 + ] + ], + "center_px": [ + 939.0, + 58.75 + ], + "area_px": 279.0 + }, + { + "image_points_px": [ + [ + 1097.0, + 25.0 + ], + [ + 1101.0, + 15.0 + ], + [ + 1128.0, + 19.0 + ], + [ + 1124.0, + 29.0 + ] + ], + "center_px": [ + 1112.5, + 22.0 + ], + "area_px": 286.0 + }, + { + "image_points_px": [ + [ + 1148.0, + 57.0 + ], + [ + 1151.0, + 48.0 + ], + [ + 1179.0, + 52.0 + ], + [ + 1175.0, + 62.0 + ] + ], + "center_px": [ + 1163.25, + 54.75 + ], + "area_px": 277.0 + }, + { + "image_points_px": [ + [ + 1018.0, + 63.0 + ], + [ + 1023.0, + 53.0 + ], + [ + 1049.0, + 58.0 + ], + [ + 1045.0, + 68.0 + ] + ], + "center_px": [ + 1033.75, + 60.5 + ], + "area_px": 287.5 + }, + { + "image_points_px": [ + [ + 1039.0, + 16.0 + ], + [ + 1044.0, + 7.0 + ], + [ + 1071.0, + 11.0 + ], + [ + 1067.0, + 20.0 + ] + ], + "center_px": [ + 1055.25, + 13.5 + ], + "area_px": 265.5 + }, + { + "image_points_px": [ + [ + 859.0, + 14.0 + ], + [ + 865.0, + 4.0 + ], + [ + 891.0, + 7.0 + ], + [ + 885.0, + 17.0 + ] + ], + "center_px": [ + 875.0, + 10.5 + ], + "area_px": 278.0 + }, + { + "image_points_px": [ + [ + 1122.0, + 40.0 + ], + [ + 1126.0, + 31.0 + ], + [ + 1153.0, + 36.0 + ], + [ + 1149.0, + 45.0 + ] + ], + "center_px": [ + 1137.5, + 38.0 + ], + "area_px": 263.0 + }, + { + "image_points_px": [ + [ + 1053.0, + 55.0 + ], + [ + 1058.0, + 46.0 + ], + [ + 1084.0, + 51.0 + ], + [ + 1080.0, + 60.0 + ] + ], + "center_px": [ + 1068.75, + 53.0 + ], + "area_px": 261.0 + }, + { + "image_points_px": [ + [ + 994.0, + 46.0 + ], + [ + 999.0, + 37.0 + ], + [ + 1025.0, + 42.0 + ], + [ + 1021.0, + 51.0 + ] + ], + "center_px": [ + 1009.75, + 44.0 + ], + "area_px": 261.0 + }, + { + "image_points_px": [ + [ + 959.0, + 54.0 + ], + [ + 965.0, + 44.0 + ], + [ + 990.0, + 49.0 + ], + [ + 985.0, + 58.0 + ] + ], + "center_px": [ + 974.75, + 51.25 + ], + "area_px": 267.0 + }, + { + "image_points_px": [ + [ + 937.0, + 37.0 + ], + [ + 942.0, + 28.0 + ], + [ + 968.0, + 33.0 + ], + [ + 962.0, + 42.0 + ] + ], + "center_px": [ + 952.25, + 35.0 + ], + "area_px": 257.0 + }, + { + "image_points_px": [ + [ + 538.0, + 263.0 + ], + [ + 545.0, + 264.0 + ], + [ + 546.0, + 285.0 + ], + [ + 524.0, + 280.0 + ] + ], + "center_px": [ + 538.25, + 273.0 + ], + "area_px": 295.0 + }, + { + "image_points_px": [ + [ + 1189.0, + 25.0 + ], + [ + 1192.0, + 17.0 + ], + [ + 1219.0, + 22.0 + ], + [ + 1215.0, + 31.0 + ] + ], + "center_px": [ + 1203.75, + 23.75 + ], + "area_px": 244.5 + }, + { + "image_points_px": [ + [ + 972.0, + 30.0 + ], + [ + 977.0, + 21.0 + ], + [ + 1002.0, + 25.0 + ], + [ + 997.0, + 35.0 + ] + ], + "center_px": [ + 987.0, + 27.75 + ], + "area_px": 260.0 + }, + { + "image_points_px": [ + [ + 915.0, + 21.0 + ], + [ + 921.0, + 12.0 + ], + [ + 945.0, + 17.0 + ], + [ + 940.0, + 26.0 + ] + ], + "center_px": [ + 930.25, + 19.0 + ], + "area_px": 248.0 + }, + { + "image_points_px": [ + [ + 950.0, + 14.0 + ], + [ + 955.0, + 5.0 + ], + [ + 980.0, + 10.0 + ], + [ + 974.0, + 19.0 + ] + ], + "center_px": [ + 964.75, + 12.0 + ], + "area_px": 248.0 + }, + { + "image_points_px": [ + [ + 644.0, + 16.0 + ], + [ + 651.0, + 8.0 + ], + [ + 675.0, + 12.0 + ], + [ + 667.0, + 21.0 + ] + ], + "center_px": [ + 659.25, + 14.25 + ], + "area_px": 233.5 + }, + { + "image_points_px": [ + [ + 479.0, + 29.0 + ], + [ + 488.0, + 20.0 + ], + [ + 510.0, + 24.0 + ], + [ + 502.0, + 33.0 + ] + ], + "center_px": [ + 494.75, + 26.5 + ], + "area_px": 236.5 + }, + { + "image_points_px": [ + [ + 886.0, + 676.0 + ], + [ + 902.0, + 688.0 + ], + [ + 897.0, + 702.0 + ], + [ + 881.0, + 690.0 + ] + ], + "center_px": [ + 891.5, + 689.0 + ], + "area_px": 284.0 + }, + { + "image_points_px": [ + [ + 465.0, + 13.0 + ], + [ + 474.0, + 5.0 + ], + [ + 496.0, + 9.0 + ], + [ + 488.0, + 17.0 + ] + ], + "center_px": [ + 480.75, + 11.0 + ], + "area_px": 214.0 + }, + { + "image_points_px": [ + [ + 737.0, + 52.0 + ], + [ + 745.0, + 48.0 + ], + [ + 768.0, + 52.0 + ], + [ + 763.0, + 57.0 + ] + ], + "center_px": [ + 753.25, + 52.25 + ], + "area_px": 139.5 + }, + { + "image_points_px": [ + [ + 682.0, + 43.0 + ], + [ + 685.0, + 39.0 + ], + [ + 712.0, + 43.0 + ], + [ + 708.0, + 48.0 + ] + ], + "center_px": [ + 696.75, + 43.25 + ], + "area_px": 135.0 + }, + { + "image_points_px": [ + [ + 698.0, + 301.0 + ], + [ + 711.0, + 305.0 + ], + [ + 701.0, + 326.0 + ], + [ + 696.0, + 321.0 + ] + ], + "center_px": [ + 701.5, + 313.25 + ], + "area_px": 211.5 + }, + { + "image_points_px": [ + [ + 941.0, + 167.0 + ], + [ + 936.0, + 196.0 + ], + [ + 933.0, + 178.0 + ], + [ + 935.0, + 171.0 + ] + ], + "center_px": [ + 936.25, + 178.0 + ], + "area_px": 105.5 + }, + { + "image_points_px": [ + [ + 551.0, + 134.0 + ], + [ + 541.0, + 148.0 + ], + [ + 527.0, + 146.0 + ], + [ + 539.0, + 133.0 + ] + ], + "center_px": [ + 539.5, + 140.25 + ], + "area_px": 192.0 + }, + { + "image_points_px": [ + [ + 1081.0, + 375.0 + ], + [ + 1075.0, + 396.0 + ], + [ + 1067.0, + 395.0 + ], + [ + 1073.0, + 375.0 + ] + ], + "center_px": [ + 1074.0, + 385.25 + ], + "area_px": 167.0 + }, + { + "image_points_px": [ + [ + 79.0, + 267.0 + ], + [ + 88.0, + 258.0 + ], + [ + 104.0, + 262.0 + ], + [ + 94.0, + 271.0 + ] + ], + "center_px": [ + 91.25, + 264.5 + ], + "area_px": 177.5 + }, + { + "image_points_px": [ + [ + 576.0, + 106.0 + ], + [ + 565.0, + 119.0 + ], + [ + 553.0, + 117.0 + ], + [ + 564.0, + 105.0 + ] + ], + "center_px": [ + 564.5, + 111.75 + ], + "area_px": 166.5 + }, + { + "image_points_px": [ + [ + 516.0, + 251.0 + ], + [ + 521.0, + 244.0 + ], + [ + 540.0, + 248.0 + ], + [ + 534.0, + 256.0 + ] + ], + "center_px": [ + 527.75, + 249.75 + ], + "area_px": 163.5 + }, + { + "image_points_px": [ + [ + 1085.0, + 332.0 + ], + [ + 1094.0, + 334.0 + ], + [ + 1088.0, + 353.0 + ], + [ + 1081.0, + 352.0 + ] + ], + "center_px": [ + 1087.0, + 342.75 + ], + "area_px": 163.5 + }, + { + "image_points_px": [ + [ + 490.0, + 279.0 + ], + [ + 497.0, + 278.0 + ], + [ + 517.0, + 284.0 + ], + [ + 511.0, + 285.0 + ] + ], + "center_px": [ + 503.75, + 281.5 + ], + "area_px": 59.5 + }, + { + "image_points_px": [ + [ + 449.0, + 350.0 + ], + [ + 455.0, + 349.0 + ], + [ + 476.0, + 355.0 + ], + [ + 471.0, + 356.0 + ] + ], + "center_px": [ + 462.75, + 352.5 + ], + "area_px": 54.5 + }, + { + "image_points_px": [ + [ + 986.0, + 405.0 + ], + [ + 1000.0, + 414.0 + ], + [ + 996.0, + 424.0 + ], + [ + 983.0, + 415.0 + ] + ], + "center_px": [ + 991.25, + 414.5 + ], + "area_px": 166.5 + }, + { + "image_points_px": [ + [ + 887.0, + 248.0 + ], + [ + 879.0, + 264.0 + ], + [ + 873.0, + 269.0 + ], + [ + 882.0, + 253.0 + ] + ], + "center_px": [ + 880.25, + 258.5 + ], + "area_px": 45.5 + }, + { + "image_points_px": [ + [ + 373.0, + 413.0 + ], + [ + 377.0, + 407.0 + ], + [ + 395.0, + 411.0 + ], + [ + 390.0, + 417.0 + ] + ], + "center_px": [ + 383.75, + 412.0 + ], + "area_px": 123.0 + }, + { + "image_points_px": [ + [ + 612.0, + 97.0 + ], + [ + 601.0, + 110.0 + ], + [ + 594.0, + 109.0 + ], + [ + 604.0, + 97.0 + ] + ], + "center_px": [ + 602.75, + 103.25 + ], + "area_px": 99.0 + }, + { + "image_points_px": [ + [ + 1136.0, + 104.0 + ], + [ + 1150.0, + 104.0 + ], + [ + 1158.0, + 108.0 + ], + [ + 1138.0, + 107.0 + ] + ], + "center_px": [ + 1145.5, + 105.75 + ], + "area_px": 57.0 + }, + { + "image_points_px": [ + [ + 746.0, + 553.0 + ], + [ + 745.0, + 570.0 + ], + [ + 739.0, + 571.0 + ], + [ + 737.0, + 569.0 + ] + ], + "center_px": [ + 741.75, + 565.75 + ], + "area_px": 75.5 + }, + { + "image_points_px": [ + [ + 1249.0, + 55.0 + ], + [ + 1260.0, + 55.0 + ], + [ + 1268.0, + 58.0 + ], + [ + 1250.0, + 57.0 + ] + ], + "center_px": [ + 1256.75, + 56.25 + ], + "area_px": 34.0 + } + ] +} \ No newline at end of file diff --git a/pipeline/render_1d.png b/pipeline/render_1d.png new file mode 100644 index 0000000..26aefc7 Binary files /dev/null and b/pipeline/render_1d.png differ diff --git a/pipeline/render_1d_aruco_detection.json b/pipeline/render_1d_aruco_detection.json new file mode 100644 index 0000000..7c3436d --- /dev/null +++ b/pipeline/render_1d_aruco_detection.json @@ -0,0 +1,2316 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T17:27:59Z", + "vision_config": { + "MarkerType": "DICT_4X4_250", + "MarkerSize": 0.025 + }, + "camera": { + "camera_id": "cam3", + "intrinsics_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render.npz", + "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 + ] + }, + "image": { + "image_file": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1d.png", + "image_sha256": "3c460a3850a720386c03bbd7011d1666df3bcc7a2766ef118815bccd53c88cee", + "width_px": 1280, + "height_px": 720 + }, + "aruco": { + "dictionary": "DICT_4X4_250", + "num_detected_markers": 10, + "num_rejected_candidates": 69 + }, + "detections": [ + { + "observation_id": "0d7b2f1a-e5eb-440b-b99c-77a189deb162", + "type": "aruco", + "marker_id": 101, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 869.0, + 397.0 + ], + [ + 858.0, + 343.0 + ], + [ + 893.0, + 344.0 + ], + [ + 904.0, + 399.0 + ] + ], + "center_px": [ + 881.0, + 370.75 + ], + "quality": { + "area_px": 1891.0, + "perimeter_px": 181.26957321166992, + "sharpness": { + "laplacian_var": 2279.71740831458 + }, + "contrast": { + "p05": 27.0, + "p95": 170.0, + "dynamic_range": 143.0, + "mean_gray": 88.31426332288402, + "std_gray": 64.25291047899364 + }, + "geometry": { + "distance_to_center_norm": 0.32852903008461, + "distance_to_border_px": 321.0 + }, + "edge_ratio": 1.6018953055219516, + "edge_lengths_px": [ + 55.10898208618164, + 35.0142822265625, + 56.08921432495117, + 35.05709457397461 + ] + }, + "confidence": 0.6242605222406631 + }, + { + "observation_id": "8c627605-767b-4775-9a3a-2a8397f3aaf5", + "type": "aruco", + "marker_id": 243, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 570.0, + 287.0 + ], + [ + 614.0, + 288.0 + ], + [ + 626.0, + 330.0 + ], + [ + 582.0, + 331.0 + ] + ], + "center_px": [ + 598.0, + 309.0 + ], + "quality": { + "area_px": 1892.0, + "perimeter_px": 177.31040573120117, + "sharpness": { + "laplacian_var": 2210.525986394558 + }, + "contrast": { + "p05": 23.0, + "p95": 191.0, + "dynamic_range": 168.0, + "mean_gray": 80.88333333333334, + "std_gray": 74.92557074052657 + }, + "geometry": { + "distance_to_center_norm": 0.08997403085231781, + "distance_to_border_px": 287.0 + }, + "edge_ratio": 1.0441009192250936, + "edge_lengths_px": [ + 44.0113639831543, + 43.680660247802734, + 44.0113639831543, + 45.607017517089844 + ] + }, + "confidence": 0.9577618231982553 + }, + { + "observation_id": "806bf28e-16c7-4474-b201-63e5c4c6390e", + "type": "aruco", + "marker_id": 219, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 968.0, + 436.0 + ], + [ + 927.0, + 408.0 + ], + [ + 967.0, + 412.0 + ], + [ + 1006.0, + 440.0 + ] + ], + "center_px": [ + 967.0, + 424.0 + ], + "quality": { + "area_px": 932.0, + "perimeter_px": 176.0686264038086, + "sharpness": { + "laplacian_var": 2486.455061563878 + }, + "contrast": { + "p05": 37.0, + "p95": 145.0, + "dynamic_range": 108.0, + "mean_gray": 77.3882175226586, + "std_gray": 44.299605272918285 + }, + "geometry": { + "distance_to_center_norm": 0.45376965403556824, + "distance_to_border_px": 274.0 + }, + "edge_ratio": 1.2993676039593838, + "edge_lengths_px": [ + 49.648765563964844, + 40.199501037597656, + 48.010414123535156, + 38.20994567871094 + ] + }, + "confidence": 0.4781813333194008 + }, + { + "observation_id": "db2acfa2-65f2-4a40-af58-39c204da695f", + "type": "aruco", + "marker_id": 124, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 779.0, + 391.0 + ], + [ + 768.0, + 341.0 + ], + [ + 798.0, + 342.0 + ], + [ + 808.0, + 393.0 + ] + ], + "center_px": [ + 788.25, + 366.75 + ], + "quality": { + "area_px": 1474.0, + "perimeter_px": 162.25239372253418, + "sharpness": { + "laplacian_var": 2120.4502604749932 + }, + "contrast": { + "p05": 27.0, + "p95": 169.0, + "dynamic_range": 142.0, + "mean_gray": 87.66118102613747, + "std_gray": 63.54910910099871 + }, + "geometry": { + "distance_to_center_norm": 0.20210148394107819, + "distance_to_border_px": 327.0 + }, + "edge_ratio": 1.7878617499056952, + "edge_lengths_px": [ + 51.195701599121094, + 30.01666259765625, + 51.97114562988281, + 29.068883895874023 + ] + }, + "confidence": 0.5496323564831004 + }, + { + "observation_id": "7e1c8004-3ff2-4ae0-8fc4-50e01f46bd9e", + "type": "aruco", + "marker_id": 242, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 620.0, + 394.0 + ], + [ + 598.0, + 417.0 + ], + [ + 554.0, + 419.0 + ], + [ + 573.0, + 397.0 + ] + ], + "center_px": [ + 586.25, + 406.75 + ], + "quality": { + "area_px": 972.5, + "perimeter_px": 152.0376205444336, + "sharpness": { + "laplacian_var": 1947.0895482802564 + }, + "contrast": { + "p05": 28.0, + "p95": 145.0, + "dynamic_range": 117.0, + "mean_gray": 65.55653450807635, + "std_gray": 48.21979281499513 + }, + "geometry": { + "distance_to_center_norm": 0.09701235592365265, + "distance_to_border_px": 301.0 + }, + "edge_ratio": 1.620139461605737, + "edge_lengths_px": [ + 31.827661514282227, + 44.04542922973633, + 29.068883895874023, + 47.095645904541016 + ] + }, + "confidence": 0.4001713116047204 + }, + { + "observation_id": "ed959114-4266-43b8-8778-4f3c57b8cc62", + "type": "aruco", + "marker_id": 218, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 821.0, + 423.0 + ], + [ + 783.0, + 398.0 + ], + [ + 814.0, + 401.0 + ], + [ + 850.0, + 426.0 + ] + ], + "center_px": [ + 817.0, + 412.0 + ], + "quality": { + "area_px": 639.0, + "perimeter_px": 149.61505889892578, + "sharpness": { + "laplacian_var": 2941.054937507891 + }, + "contrast": { + "p05": 35.0, + "p95": 141.0, + "dynamic_range": 106.0, + "mean_gray": 71.56629213483146, + "std_gray": 41.98210679261008 + }, + "geometry": { + "distance_to_center_norm": 0.2512321174144745, + "distance_to_border_px": 294.0 + }, + "edge_ratio": 1.560165911582428, + "edge_lengths_px": [ + 45.486263275146484, + 31.14482307434082, + 43.8292121887207, + 29.154760360717773 + ] + }, + "confidence": 0.27304788345742115 + }, + { + "observation_id": "6ff49b7f-85af-4c4d-8660-8ef280bc800e", + "type": "aruco", + "marker_id": 215, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 590.0, + 695.0 + ], + [ + 630.0, + 691.0 + ], + [ + 653.0, + 699.0 + ], + [ + 609.0, + 703.0 + ] + ], + "center_px": [ + 620.5, + 697.0 + ], + "quality": { + "area_px": 420.0, + "perimeter_px": 129.3480625152588, + "sharpness": { + "laplacian_var": 4546.540325161974 + }, + "contrast": { + "p05": 21.0, + "p95": 173.0, + "dynamic_range": 152.0, + "mean_gray": 86.09634551495017, + "std_gray": 62.5571952510232 + }, + "geometry": { + "distance_to_center_norm": 0.4597066640853882, + "distance_to_border_px": 17.0 + }, + "edge_ratio": 2.1431147449676984, + "edge_lengths_px": [ + 40.199501037597656, + 24.351591110229492, + 44.18144226074219, + 20.615528106689453 + ] + }, + "confidence": 0.04442132658717482 + }, + { + "observation_id": "2d909360-f7b7-43eb-a5a0-37bb4e4d8b4e", + "type": "aruco", + "marker_id": 211, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 523.0, + 669.0 + ], + [ + 563.0, + 666.0 + ], + [ + 581.0, + 672.0 + ], + [ + 541.0, + 676.0 + ] + ], + "center_px": [ + 552.0, + 670.75 + ], + "quality": { + "area_px": 323.0, + "perimeter_px": 118.59871673583984, + "sharpness": { + "laplacian_var": 6221.592994735114 + }, + "contrast": { + "p05": 19.0, + "p95": 161.0, + "dynamic_range": 142.0, + "mean_gray": 75.80237154150197, + "std_gray": 57.41032252753604 + }, + "geometry": { + "distance_to_center_norm": 0.43983229994773865, + "distance_to_border_px": 44.0 + }, + "edge_ratio": 2.1186998154843373, + "edge_lengths_px": [ + 40.112342834472656, + 18.973665237426758, + 40.199501037597656, + 19.313207626342773 + ] + }, + "confidence": 0.08943849994625828 + }, + { + "observation_id": "46c5bede-f00b-4855-90fa-e64bc67a71ee", + "type": "aruco", + "marker_id": 217, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 1144.0, + 629.0 + ], + [ + 1172.0, + 626.0 + ], + [ + 1200.0, + 632.0 + ], + [ + 1171.0, + 635.0 + ] + ], + "center_px": [ + 1171.75, + 630.5 + ], + "quality": { + "area_px": 253.5, + "perimeter_px": 113.60929298400879, + "sharpness": { + "laplacian_var": 8183.706666666667 + }, + "contrast": { + "p05": 22.0, + "p95": 153.79999999999998, + "dynamic_range": 131.79999999999998, + "mean_gray": 74.33333333333333, + "std_gray": 42.99509015896789 + }, + "geometry": { + "distance_to_center_norm": 0.8124681711196899, + "distance_to_border_px": 80.0 + }, + "edge_ratio": 1.054092554421771, + "edge_lengths_px": [ + 28.160255432128906, + 28.635643005371094, + 29.154760360717773, + 27.658634185791016 + ] + }, + "confidence": 0.1603274772135223 + }, + { + "observation_id": "e202e463-7a21-41f2-9e79-679bfc2d9c5c", + "type": "aruco", + "marker_id": 214, + "marker_size_m": 0.025, + "image_points_px": [ + [ + 676.0, + 653.0 + ], + [ + 710.0, + 649.0 + ], + [ + 730.0, + 655.0 + ], + [ + 695.0, + 659.0 + ] + ], + "center_px": [ + 702.75, + 654.0 + ], + "quality": { + "area_px": 285.0, + "perimeter_px": 110.26778602600098, + "sharpness": { + "laplacian_var": 3296.7684151785716 + }, + "contrast": { + "p05": 18.0, + "p95": 137.0, + "dynamic_range": 119.0, + "mean_gray": 75.04017857142857, + "std_gray": 46.24568698001822 + }, + "geometry": { + "distance_to_center_norm": 0.40939804911613464, + "distance_to_border_px": 61.0 + }, + "edge_ratio": 1.7680341217288618, + "edge_lengths_px": [ + 34.2344856262207, + 20.880613327026367, + 35.22782897949219, + 19.92485809326172 + ] + }, + "confidence": 0.1074639893342158 + } + ], + "rejected_candidates": [ + { + "image_points_px": [ + [ + 206.0, + 612.0 + ], + [ + 160.0, + 628.0 + ], + [ + 133.0, + 627.0 + ], + [ + 191.0, + 608.0 + ] + ], + "center_px": [ + 172.5, + 618.75 + ], + "area_px": 497.5 + }, + { + "image_points_px": [ + [ + 547.0, + 479.0 + ], + [ + 543.0, + 485.0 + ], + [ + 484.0, + 488.0 + ], + [ + 508.0, + 479.0 + ] + ], + "center_px": [ + 520.5, + 482.75 + ], + "area_px": 346.5 + }, + { + "image_points_px": [ + [ + 830.0, + 570.0 + ], + [ + 799.0, + 594.0 + ], + [ + 774.0, + 592.0 + ], + [ + 804.0, + 569.0 + ] + ], + "center_px": [ + 801.75, + 581.25 + ], + "area_px": 645.0 + }, + { + "image_points_px": [ + [ + 750.0, + 676.0 + ], + [ + 788.0, + 673.0 + ], + [ + 810.0, + 680.0 + ], + [ + 773.0, + 684.0 + ] + ], + "center_px": [ + 780.25, + 678.25 + ], + "area_px": 360.0 + }, + { + "image_points_px": [ + [ + 943.0, + 549.0 + ], + [ + 924.0, + 574.0 + ], + [ + 899.0, + 573.0 + ], + [ + 920.0, + 549.0 + ] + ], + "center_px": [ + 921.5, + 561.25 + ], + "area_px": 598.0 + }, + { + "image_points_px": [ + [ + 1057.0, + 611.0 + ], + [ + 1087.0, + 609.0 + ], + [ + 1108.0, + 613.0 + ], + [ + 1079.0, + 616.0 + ] + ], + "center_px": [ + 1082.75, + 612.25 + ], + "area_px": 186.5 + }, + { + "image_points_px": [ + [ + 1176.0, + 709.0 + ], + [ + 1201.0, + 706.0 + ], + [ + 1225.0, + 711.0 + ], + [ + 1198.0, + 714.0 + ] + ], + "center_px": [ + 1200.0, + 710.0 + ], + "area_px": 199.0 + }, + { + "image_points_px": [ + [ + 1025.0, + 711.0 + ], + [ + 1055.0, + 709.0 + ], + [ + 1073.0, + 713.0 + ], + [ + 1047.0, + 716.0 + ] + ], + "center_px": [ + 1050.0, + 712.25 + ], + "area_px": 176.0 + }, + { + "image_points_px": [ + [ + 1126.0, + 706.0 + ], + [ + 1151.0, + 703.0 + ], + [ + 1173.0, + 708.0 + ], + [ + 1147.0, + 711.0 + ] + ], + "center_px": [ + 1149.25, + 707.0 + ], + "area_px": 192.0 + }, + { + "image_points_px": [ + [ + 52.0, + 695.0 + ], + [ + 18.0, + 698.0 + ], + [ + 13.0, + 693.0 + ], + [ + 47.0, + 691.0 + ] + ], + "center_px": [ + 32.5, + 694.25 + ], + "area_px": 165.5 + }, + { + "image_points_px": [ + [ + 93.0, + 321.0 + ], + [ + 97.0, + 328.0 + ], + [ + 101.0, + 356.0 + ], + [ + 94.0, + 351.0 + ] + ], + "center_px": [ + 96.25, + 339.0 + ], + "area_px": 144.5 + }, + { + "image_points_px": [ + [ + 136.0, + 650.0 + ], + [ + 106.0, + 652.0 + ], + [ + 101.0, + 649.0 + ], + [ + 130.0, + 647.0 + ] + ], + "center_px": [ + 118.25, + 649.5 + ], + "area_px": 99.5 + }, + { + "image_points_px": [ + [ + 173.0, + 652.0 + ], + [ + 144.0, + 654.0 + ], + [ + 138.0, + 651.0 + ], + [ + 167.0, + 649.0 + ] + ], + "center_px": [ + 155.5, + 651.5 + ], + "area_px": 99.0 + }, + { + "image_points_px": [ + [ + 76.0, + 656.0 + ], + [ + 46.0, + 658.0 + ], + [ + 42.0, + 655.0 + ], + [ + 70.0, + 652.0 + ] + ], + "center_px": [ + 58.5, + 655.25 + ], + "area_px": 114.0 + }, + { + "image_points_px": [ + [ + 209.0, + 654.0 + ], + [ + 181.0, + 656.0 + ], + [ + 175.0, + 653.0 + ], + [ + 204.0, + 651.0 + ] + ], + "center_px": [ + 192.25, + 653.5 + ], + "area_px": 96.5 + }, + { + "image_points_px": [ + [ + 87.0, + 639.0 + ], + [ + 58.0, + 641.0 + ], + [ + 53.0, + 638.0 + ], + [ + 81.0, + 636.0 + ] + ], + "center_px": [ + 69.75, + 638.5 + ], + "area_px": 96.5 + }, + { + "image_points_px": [ + [ + 197.0, + 646.0 + ], + [ + 225.0, + 644.0 + ], + [ + 231.0, + 647.0 + ], + [ + 203.0, + 649.0 + ] + ], + "center_px": [ + 214.0, + 646.5 + ], + "area_px": 96.0 + }, + { + "image_points_px": [ + [ + 124.0, + 642.0 + ], + [ + 152.0, + 640.0 + ], + [ + 158.0, + 643.0 + ], + [ + 130.0, + 645.0 + ] + ], + "center_px": [ + 141.0, + 642.5 + ], + "area_px": 96.0 + }, + { + "image_points_px": [ + [ + 160.0, + 644.0 + ], + [ + 188.0, + 642.0 + ], + [ + 194.0, + 645.0 + ], + [ + 167.0, + 647.0 + ] + ], + "center_px": [ + 177.25, + 644.5 + ], + "area_px": 95.5 + }, + { + "image_points_px": [ + [ + 87.0, + 665.0 + ], + [ + 58.0, + 668.0 + ], + [ + 54.0, + 665.0 + ], + [ + 83.0, + 662.0 + ] + ], + "center_px": [ + 70.5, + 665.0 + ], + "area_px": 99.0 + }, + { + "image_points_px": [ + [ + 56.0, + 709.0 + ], + [ + 31.0, + 711.0 + ], + [ + 26.0, + 706.0 + ], + [ + 56.0, + 703.0 + ] + ], + "center_px": [ + 42.25, + 707.25 + ], + "area_px": 157.5 + }, + { + "image_points_px": [ + [ + 63.0, + 646.0 + ], + [ + 34.0, + 648.0 + ], + [ + 30.0, + 645.0 + ], + [ + 58.0, + 643.0 + ] + ], + "center_px": [ + 46.25, + 645.5 + ], + "area_px": 94.5 + }, + { + "image_points_px": [ + [ + 143.0, + 634.0 + ], + [ + 116.0, + 636.0 + ], + [ + 110.0, + 633.0 + ], + [ + 137.0, + 631.0 + ] + ], + "center_px": [ + 126.5, + 633.5 + ], + "area_px": 93.0 + }, + { + "image_points_px": [ + [ + 234.0, + 648.0 + ], + [ + 261.0, + 646.0 + ], + [ + 267.0, + 649.0 + ], + [ + 240.0, + 651.0 + ] + ], + "center_px": [ + 250.5, + 648.5 + ], + "area_px": 93.0 + }, + { + "image_points_px": [ + [ + 179.0, + 636.0 + ], + [ + 152.0, + 638.0 + ], + [ + 146.0, + 635.0 + ], + [ + 172.0, + 633.0 + ] + ], + "center_px": [ + 162.25, + 635.5 + ], + "area_px": 92.5 + }, + { + "image_points_px": [ + [ + 234.0, + 631.0 + ], + [ + 208.0, + 633.0 + ], + [ + 201.0, + 630.0 + ], + [ + 226.0, + 628.0 + ] + ], + "center_px": [ + 217.25, + 630.5 + ], + "area_px": 91.5 + }, + { + "image_points_px": [ + [ + 862.0, + 319.0 + ], + [ + 888.0, + 320.0 + ], + [ + 884.0, + 332.0 + ], + [ + 873.0, + 331.0 + ] + ], + "center_px": [ + 876.75, + 325.5 + ], + "area_px": 218.5 + }, + { + "image_points_px": [ + [ + 182.0, + 637.0 + ], + [ + 209.0, + 635.0 + ], + [ + 214.0, + 638.0 + ], + [ + 187.0, + 640.0 + ] + ], + "center_px": [ + 198.0, + 637.5 + ], + "area_px": 91.0 + }, + { + "image_points_px": [ + [ + 1182.0, + 689.0 + ], + [ + 1199.0, + 687.0 + ], + [ + 1213.0, + 691.0 + ], + [ + 1196.0, + 694.0 + ] + ], + "center_px": [ + 1197.5, + 690.25 + ], + "area_px": 111.5 + }, + { + "image_points_px": [ + [ + 42.0, + 629.0 + ], + [ + 70.0, + 628.0 + ], + [ + 73.0, + 630.0 + ], + [ + 46.0, + 632.0 + ] + ], + "center_px": [ + 57.75, + 629.75 + ], + "area_px": 74.0 + }, + { + "image_points_px": [ + [ + 62.0, + 622.0 + ], + [ + 36.0, + 624.0 + ], + [ + 31.0, + 621.0 + ], + [ + 56.0, + 619.0 + ] + ], + "center_px": [ + 46.25, + 621.5 + ], + "area_px": 87.5 + }, + { + "image_points_px": [ + [ + 39.0, + 628.0 + ], + [ + 12.0, + 630.0 + ], + [ + 9.0, + 627.0 + ], + [ + 36.0, + 626.0 + ] + ], + "center_px": [ + 24.0, + 627.75 + ], + "area_px": 72.0 + }, + { + "image_points_px": [ + [ + 51.0, + 614.0 + ], + [ + 25.0, + 616.0 + ], + [ + 21.0, + 613.0 + ], + [ + 47.0, + 612.0 + ] + ], + "center_px": [ + 36.0, + 613.75 + ], + "area_px": 71.0 + }, + { + "image_points_px": [ + [ + 126.0, + 605.0 + ], + [ + 151.0, + 604.0 + ], + [ + 156.0, + 606.0 + ], + [ + 131.0, + 607.0 + ] + ], + "center_px": [ + 141.0, + 605.5 + ], + "area_px": 55.0 + }, + { + "image_points_px": [ + [ + 11.0, + 606.0 + ], + [ + 37.0, + 605.0 + ], + [ + 40.0, + 607.0 + ], + [ + 15.0, + 608.0 + ] + ], + "center_px": [ + 25.75, + 606.5 + ], + "area_px": 54.5 + }, + { + "image_points_px": [ + [ + 72.0, + 608.0 + ], + [ + 47.0, + 610.0 + ], + [ + 43.0, + 608.0 + ], + [ + 67.0, + 606.0 + ] + ], + "center_px": [ + 57.25, + 608.0 + ], + "area_px": 58.0 + }, + { + "image_points_px": [ + [ + 90.0, + 669.0 + ], + [ + 86.0, + 673.0 + ], + [ + 65.0, + 674.0 + ], + [ + 62.0, + 671.0 + ] + ], + "center_px": [ + 75.75, + 671.75 + ], + "area_px": 85.0 + }, + { + "image_points_px": [ + [ + 114.0, + 598.0 + ], + [ + 138.0, + 597.0 + ], + [ + 143.0, + 599.0 + ], + [ + 118.0, + 600.0 + ] + ], + "center_px": [ + 128.25, + 598.5 + ], + "area_px": 53.5 + }, + { + "image_points_px": [ + [ + 575.0, + 619.0 + ], + [ + 555.0, + 621.0 + ], + [ + 546.0, + 618.0 + ], + [ + 568.0, + 617.0 + ] + ], + "center_px": [ + 561.0, + 618.75 + ], + "area_px": 64.5 + }, + { + "image_points_px": [ + [ + 61.0, + 601.0 + ], + [ + 37.0, + 603.0 + ], + [ + 33.0, + 601.0 + ], + [ + 57.0, + 599.0 + ] + ], + "center_px": [ + 47.0, + 601.0 + ], + "area_px": 56.0 + }, + { + "image_points_px": [ + [ + 53.0, + 595.0 + ], + [ + 77.0, + 594.0 + ], + [ + 81.0, + 596.0 + ], + [ + 57.0, + 597.0 + ] + ], + "center_px": [ + 67.0, + 595.5 + ], + "area_px": 52.0 + }, + { + "image_points_px": [ + [ + 187.0, + 607.0 + ], + [ + 164.0, + 609.0 + ], + [ + 159.0, + 607.0 + ], + [ + 182.0, + 605.0 + ] + ], + "center_px": [ + 173.0, + 607.0 + ], + "area_px": 56.0 + }, + { + "image_points_px": [ + [ + 133.0, + 593.0 + ], + [ + 157.0, + 592.0 + ], + [ + 161.0, + 594.0 + ], + [ + 138.0, + 595.0 + ] + ], + "center_px": [ + 147.25, + 593.5 + ], + "area_px": 51.5 + }, + { + "image_points_px": [ + [ + 174.0, + 600.0 + ], + [ + 151.0, + 602.0 + ], + [ + 146.0, + 600.0 + ], + [ + 168.0, + 598.0 + ] + ], + "center_px": [ + 159.75, + 600.0 + ], + "area_px": 56.0 + }, + { + "image_points_px": [ + [ + 151.0, + 588.0 + ], + [ + 174.0, + 587.0 + ], + [ + 179.0, + 589.0 + ], + [ + 156.0, + 590.0 + ] + ], + "center_px": [ + 165.0, + 588.5 + ], + "area_px": 51.0 + }, + { + "image_points_px": [ + [ + 763.0, + 678.0 + ], + [ + 766.0, + 675.0 + ], + [ + 790.0, + 678.0 + ], + [ + 780.0, + 682.0 + ] + ], + "center_px": [ + 774.75, + 678.25 + ], + "area_px": 94.5 + }, + { + "image_points_px": [ + [ + 593.0, + 614.0 + ], + [ + 615.0, + 613.0 + ], + [ + 621.0, + 615.0 + ], + [ + 600.0, + 616.0 + ] + ], + "center_px": [ + 607.25, + 614.5 + ], + "area_px": 49.5 + }, + { + "image_points_px": [ + [ + 23.0, + 594.0 + ], + [ + 48.0, + 593.0 + ], + [ + 50.0, + 595.0 + ], + [ + 27.0, + 596.0 + ] + ], + "center_px": [ + 37.0, + 594.5 + ], + "area_px": 51.0 + }, + { + "image_points_px": [ + [ + 41.0, + 588.0 + ], + [ + 17.0, + 589.0 + ], + [ + 14.0, + 587.0 + ], + [ + 37.0, + 586.0 + ] + ], + "center_px": [ + 27.25, + 587.5 + ], + "area_px": 50.5 + }, + { + "image_points_px": [ + [ + 879.0, + 419.0 + ], + [ + 898.0, + 421.0 + ], + [ + 905.0, + 426.0 + ], + [ + 886.0, + 423.0 + ] + ], + "center_px": [ + 892.0, + 422.25 + ], + "area_px": 68.0 + }, + { + "image_points_px": [ + [ + 48.0, + 636.0 + ], + [ + 30.0, + 639.0 + ], + [ + 21.0, + 637.0 + ], + [ + 41.0, + 634.0 + ] + ], + "center_px": [ + 35.0, + 636.5 + ], + "area_px": 62.0 + }, + { + "image_points_px": [ + [ + 525.0, + 623.0 + ], + [ + 504.0, + 625.0 + ], + [ + 498.0, + 623.0 + ], + [ + 520.0, + 621.0 + ] + ], + "center_px": [ + 511.75, + 623.0 + ], + "area_px": 54.0 + }, + { + "image_points_px": [ + [ + 322.0, + 689.0 + ], + [ + 306.0, + 692.0 + ], + [ + 295.0, + 691.0 + ], + [ + 311.0, + 688.0 + ] + ], + "center_px": [ + 308.5, + 690.0 + ], + "area_px": 49.0 + }, + { + "image_points_px": [ + [ + 171.0, + 572.0 + ], + [ + 149.0, + 573.0 + ], + [ + 145.0, + 571.0 + ], + [ + 165.0, + 570.0 + ] + ], + "center_px": [ + 157.5, + 571.5 + ], + "area_px": 47.0 + }, + { + "image_points_px": [ + [ + 143.0, + 571.0 + ], + [ + 123.0, + 572.0 + ], + [ + 117.0, + 570.0 + ], + [ + 137.0, + 569.0 + ] + ], + "center_px": [ + 130.0, + 570.5 + ], + "area_px": 46.0 + }, + { + "image_points_px": [ + [ + 709.0, + 606.0 + ], + [ + 693.0, + 608.0 + ], + [ + 683.0, + 606.0 + ], + [ + 701.0, + 604.0 + ] + ], + "center_px": [ + 696.5, + 606.0 + ], + "area_px": 52.0 + }, + { + "image_points_px": [ + [ + 34.0, + 622.0 + ], + [ + 44.0, + 620.0 + ], + [ + 59.0, + 621.0 + ], + [ + 39.0, + 624.0 + ] + ], + "center_px": [ + 44.0, + 621.75 + ], + "area_px": 47.5 + }, + { + "image_points_px": [ + [ + 848.0, + 590.0 + ], + [ + 865.0, + 589.0 + ], + [ + 873.0, + 591.0 + ], + [ + 856.0, + 592.0 + ] + ], + "center_px": [ + 860.5, + 590.5 + ], + "area_px": 42.0 + }, + { + "image_points_px": [ + [ + 924.0, + 583.0 + ], + [ + 940.0, + 582.0 + ], + [ + 949.0, + 584.0 + ], + [ + 933.0, + 585.0 + ] + ], + "center_px": [ + 936.5, + 583.5 + ], + "area_px": 41.0 + }, + { + "image_points_px": [ + [ + 972.0, + 329.0 + ], + [ + 967.0, + 345.0 + ], + [ + 962.0, + 351.0 + ], + [ + 964.0, + 340.0 + ] + ], + "center_px": [ + 966.25, + 341.25 + ], + "area_px": 58.0 + }, + { + "image_points_px": [ + [ + 831.0, + 387.0 + ], + [ + 837.0, + 388.0 + ], + [ + 847.0, + 402.0 + ], + [ + 831.0, + 393.0 + ] + ], + "center_px": [ + 836.5, + 392.5 + ], + "area_px": 85.0 + }, + { + "image_points_px": [ + [ + 213.0, + 622.0 + ], + [ + 204.0, + 624.0 + ], + [ + 190.0, + 623.0 + ], + [ + 211.0, + 620.0 + ] + ], + "center_px": [ + 204.5, + 622.25 + ], + "area_px": 42.5 + }, + { + "image_points_px": [ + [ + 145.0, + 618.0 + ], + [ + 124.0, + 621.0 + ], + [ + 122.0, + 619.0 + ], + [ + 135.0, + 617.0 + ] + ], + "center_px": [ + 131.5, + 618.75 + ], + "area_px": 40.5 + }, + { + "image_points_px": [ + [ + 133.0, + 611.0 + ], + [ + 119.0, + 613.0 + ], + [ + 110.0, + 612.0 + ], + [ + 127.0, + 609.0 + ] + ], + "center_px": [ + 122.25, + 611.25 + ], + "area_px": 42.0 + }, + { + "image_points_px": [ + [ + 809.0, + 594.0 + ], + [ + 827.0, + 593.0 + ], + [ + 832.0, + 595.0 + ], + [ + 817.0, + 596.0 + ] + ], + "center_px": [ + 821.25, + 594.5 + ], + "area_px": 39.5 + }, + { + "image_points_px": [ + [ + 14.0, + 607.0 + ], + [ + 24.0, + 605.0 + ], + [ + 37.0, + 606.0 + ], + [ + 26.0, + 608.0 + ] + ], + "center_px": [ + 25.25, + 606.5 + ], + "area_px": 35.5 + }, + { + "image_points_px": [ + [ + 1078.0, + 610.0 + ], + [ + 1086.0, + 609.0 + ], + [ + 1100.0, + 613.0 + ], + [ + 1092.0, + 614.0 + ] + ], + "center_px": [ + 1089.0, + 611.5 + ], + "area_px": 46.0 + }, + { + "image_points_px": [ + [ + 130.0, + 606.0 + ], + [ + 140.0, + 604.0 + ], + [ + 152.0, + 605.0 + ], + [ + 141.0, + 607.0 + ] + ], + "center_px": [ + 140.75, + 605.5 + ], + "area_px": 33.5 + }, + { + "image_points_px": [ + [ + 118.0, + 599.0 + ], + [ + 128.0, + 597.0 + ], + [ + 139.0, + 598.0 + ], + [ + 129.0, + 600.0 + ] + ], + "center_px": [ + 128.5, + 598.5 + ], + "area_px": 32.0 + } + ] +} \ No newline at end of file diff --git a/pipeline/render_1d_camera_pose.json b/pipeline/render_1d_camera_pose.json new file mode 100644 index 0000000..3ca859b --- /dev/null +++ b/pipeline/render_1d_camera_pose.json @@ -0,0 +1,263 @@ +{ + "schema_version": "1.0", + "created_utc": "2026-05-29T17:28:01Z", + "source": { + "detection_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\pipeline\\render_1d_aruco_detection.json", + "robot_json": "C:\\Users\\kech\\SynologyDrive\\2026-AppServer-AppRobot\\appRobotRendering\\robot.json" + }, + "camera": { + "camera_id": "cam3", + "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": [ + 215, + 211, + 217, + 214 + ], + "history": { + "iters": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "rms": [ + 0.005359920749647414, + 0.0003596216482234802, + 8.587722609457216e-05, + 8.267163975654815e-05, + 8.265774784671822e-05, + 8.265772386013678e-05 + ], + "lambda": [ + 0.001, + 0.0005, + 0.00025, + 0.000125, + 6.25e-05, + 3.125e-05 + ] + }, + "residual_rms_px": 0.20616096991738692, + "residual_median_px": 0.1548259204477796, + "residual_max_px": 0.334368679842534, + "sigma2_normalized": 2.732919724610169e-08 + }, + "camera_pose": { + "world_to_camera": { + "rotation_matrix": [ + [ + 0.8698499202728271, + -0.4933161735534668, + -0.0005609216168522835 + ], + [ + -0.014420680701732635, + -0.02429097332060337, + -0.999600887298584 + ], + [ + 0.4931056499481201, + 0.8695108294487, + -0.028243456035852432 + ] + ], + "translation_m": [ + -0.27209416031837463, + 0.21111075580120087, + 0.8869640231132507 + ], + "rvec_rad": [ + 1.560002049077422, + -0.41202505801423905, + 0.39969676868512233 + ] + }, + "camera_in_world": { + "position_m": [ + -0.1976415067911148, + -0.9003251791000366, + 0.23592481017112732 + ], + "position_mm": [ + -197.64151000976562, + -900.3251953125, + 235.9248046875 + ], + "orientation_deg": { + "roll": 91.86042785644531, + "pitch": -29.544910430908203, + "yaw": -0.94978266954422 + } + }, + "uncertainty": { + "pose_covariance_6x6": [ + [ + 5.572124225215572e-06, + 1.0238412973605017e-06, + -2.623828452852646e-07, + -1.3216478710623785e-07, + 2.464552358706632e-07, + 1.376118360825126e-06 + ], + [ + 1.0238412973606372e-06, + 1.7185895100151877e-06, + -2.1941259661160463e-06, + -2.865276101919663e-07, + 4.866971629745338e-07, + 2.6156272415413096e-06 + ], + [ + -2.6238284528551765e-07, + -2.1941259661160654e-06, + 4.3008457236225806e-06, + 4.5954457611754067e-07, + -1.039277359784403e-06, + -4.284089217370219e-06 + ], + [ + -1.321647871062647e-07, + -2.8652761019196723e-07, + 4.5954457611753834e-07, + 6.213296390305284e-08, + -1.0417950265091419e-07, + -4.7537517694908277e-07 + ], + [ + 2.4645523587072205e-07, + 4.86697162974532e-07, + -1.039277359784392e-06, + -1.0417950265091408e-07, + 2.937313750271454e-07, + 1.119130901469573e-06 + ], + [ + 1.3761183608253812e-06, + 2.615627241541313e-06, + -4.28408921737019e-06, + -4.753751769490844e-07, + 1.1191309014695779e-06, + 5.056766279398004e-06 + ] + ], + "parameter_std": { + "rvec_std_deg": [ + 0.13524867758906906, + 0.07511189357579418, + 0.11882274046633103 + ], + "tvec_std_m": [ + 0.0002492648469059623, + 0.0005419699023258998, + 0.0022487254788875416 + ] + }, + "camera_center_std_m": [ + 0.0012502905713881077, + 0.002327873286067422, + 0.002258043249396123 + ], + "camera_center_std_mm": [ + 1.2502905713881076, + 2.327873286067422, + 2.258043249396123 + ], + "orientation_std_deg": { + "roll": 0.1361099998273254, + "pitch": 0.12490237671576365, + "yaw": 0.050717858739607595 + } + } + }, + "observations": { + "markers": [ + { + "marker_id": 215, + "observed_center_px": [ + 620.5, + 697.0 + ], + "projected_center_px": [ + 620.4794311523438, + 697.0128173828125 + ], + "reprojection_error_px": 0.024235568820809458, + "confidence": 0.04442132658717482 + }, + { + "marker_id": 211, + "observed_center_px": [ + 552.0, + 670.75 + ], + "projected_center_px": [ + 551.7823486328125, + 670.6954345703125 + ], + "reprojection_error_px": 0.22438695094761962, + "confidence": 0.08943849994625828 + }, + { + "marker_id": 217, + "observed_center_px": [ + 1171.75, + 630.5 + ], + "projected_center_px": [ + 1171.666259765625, + 630.4839477539062 + ], + "reprojection_error_px": 0.08526488994793956, + "confidence": 0.1603274772135223 + }, + { + "marker_id": 214, + "observed_center_px": [ + 702.75, + 654.0 + ], + "projected_center_px": [ + 703.079345703125, + 654.0577392578125 + ], + "reprojection_error_px": 0.334368679842534, + "confidence": 0.1074639893342158 + } + ] + }, + "qa": { + "sanity_notes": [] + } +} \ No newline at end of file diff --git a/pipeline/render_3a_aruco_detection.json b/pipeline/render_3a_aruco_detection.json index 5df18e2..d461946 100644 --- a/pipeline/render_3a_aruco_detection.json +++ b/pipeline/render_3a_aruco_detection.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "created_utc": "2026-05-29T11:11:52Z", + "created_utc": "2026-05-29T16:50:05Z", "vision_config": { "MarkerType": "DICT_4X4_250", "MarkerSize": 0.025 @@ -46,7 +46,7 @@ }, "detections": [ { - "observation_id": "3fe0abea-8ee3-488d-afdd-ca173488bac8", + "observation_id": "c902f90e-1647-428c-a5d1-70de2978916f", "type": "aruco", "marker_id": 124, "marker_size_m": 0.025, @@ -100,7 +100,7 @@ "confidence": 0.6862086405991146 }, { - "observation_id": "4ad28655-628f-4970-aee7-ac0464a422dd", + "observation_id": "77ff7696-8a93-4b4f-8483-f8c322717d80", "type": "aruco", "marker_id": 243, "marker_size_m": 0.025, @@ -154,7 +154,7 @@ "confidence": 0.953171926139578 }, { - "observation_id": "39c9ae85-1343-4abd-a776-aead564c7ba2", + "observation_id": "91d2b75d-5528-4aa6-b7e1-225a3cae5400", "type": "aruco", "marker_id": 122, "marker_size_m": 0.025, @@ -208,7 +208,7 @@ "confidence": 0.5964639370258038 }, { - "observation_id": "2d4f27e1-f896-4714-bea0-d66f354f65bc", + "observation_id": "e61c114e-be11-4f43-a6c5-7333afc1129e", "type": "aruco", "marker_id": 102, "marker_size_m": 0.025, @@ -262,7 +262,7 @@ "confidence": 0.3578323322772018 }, { - "observation_id": "2b4ed846-8c7c-40a4-9d78-6dde96b7fb77", + "observation_id": "3c71a3a5-f9c2-4cec-93cf-5fc1cc35f02a", "type": "aruco", "marker_id": 229, "marker_size_m": 0.025, @@ -316,7 +316,7 @@ "confidence": 0.4396423002158715 }, { - "observation_id": "5ce4384f-4a4e-4dfb-9e84-cfb04b1ee009", + "observation_id": "a3790a5f-adc1-44dc-baaf-177ee62c1679", "type": "aruco", "marker_id": 210, "marker_size_m": 0.025, @@ -370,7 +370,7 @@ "confidence": 0.23718801109435655 }, { - "observation_id": "b0f54f4e-c981-4c68-91f4-71796b2e1b7e", + "observation_id": "d4ded05d-c6c5-471d-8f6e-97b4c8902312", "type": "aruco", "marker_id": 198, "marker_size_m": 0.025, @@ -424,7 +424,7 @@ "confidence": 0.27797680859006363 }, { - "observation_id": "2291dd23-8cfd-4fc8-8268-3f51a4f477e3", + "observation_id": "0265278f-0be8-4a5e-ba07-2ca9f230b245", "type": "aruco", "marker_id": 205, "marker_size_m": 0.025, @@ -478,7 +478,7 @@ "confidence": 0.19676029488014599 }, { - "observation_id": "1aa4305f-42cd-4a6b-bbd9-3c4779159e9f", + "observation_id": "8d2f4eef-9217-4f3c-8945-f775942823d2", "type": "aruco", "marker_id": 206, "marker_size_m": 0.025, @@ -532,7 +532,7 @@ "confidence": 0.23511362818048806 }, { - "observation_id": "67d472aa-5a55-495c-84f3-9f9867b5e6bb", + "observation_id": "716fccd5-851b-4b4f-9753-0ec539b0b1af", "type": "aruco", "marker_id": 207, "marker_size_m": 0.025, diff --git a/pipeline/render_3a_camera_pose.json b/pipeline/render_3a_camera_pose.json index 5431454..f6c0841 100644 --- a/pipeline/render_3a_camera_pose.json +++ b/pipeline/render_3a_camera_pose.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "created_utc": "2026-05-29T11:11:53Z", + "created_utc": "2026-05-29T17:30:28Z", "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" diff --git a/pipeline/render_3b_aruco_detection.json b/pipeline/render_3b_aruco_detection.json index 80ce028..b6e5d65 100644 --- a/pipeline/render_3b_aruco_detection.json +++ b/pipeline/render_3b_aruco_detection.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "created_utc": "2026-05-29T11:11:51Z", + "created_utc": "2026-05-29T16:50:05Z", "vision_config": { "MarkerType": "DICT_4X4_250", "MarkerSize": 0.025 @@ -46,7 +46,7 @@ }, "detections": [ { - "observation_id": "d0acb2d6-f00e-4abb-9182-0aba44eb37b5", + "observation_id": "a409d514-f754-44e2-93e4-c995beff6c06", "type": "aruco", "marker_id": 243, "marker_size_m": 0.025, @@ -100,7 +100,7 @@ "confidence": 0.8522257252911212 }, { - "observation_id": "912b4c1f-fa4d-488b-ae55-6c159b08259f", + "observation_id": "7acc74f6-773a-4585-bfd3-dbfc9d9569f1", "type": "aruco", "marker_id": 122, "marker_size_m": 0.025, @@ -154,7 +154,7 @@ "confidence": 0.2874514216623714 }, { - "observation_id": "d713f49f-d43d-4181-b2c3-802fae7ac4c6", + "observation_id": "713c50a6-b73d-4dfb-9773-ec27c88351f5", "type": "aruco", "marker_id": 229, "marker_size_m": 0.025, @@ -208,7 +208,7 @@ "confidence": 0.5547001883002887 }, { - "observation_id": "b540ca35-2454-4485-b5c9-0edcffa7d655", + "observation_id": "1bcbde4a-7b55-4e8b-a2aa-46c1b144cf86", "type": "aruco", "marker_id": 208, "marker_size_m": 0.025, @@ -262,7 +262,7 @@ "confidence": 0.6072727689079809 }, { - "observation_id": "fd9ca8aa-3703-4a75-99dd-61e9d6da7096", + "observation_id": "3eb7f21a-2e30-4df0-b3ae-4a0b7fb33f87", "type": "aruco", "marker_id": 215, "marker_size_m": 0.025, @@ -316,7 +316,7 @@ "confidence": 0.6168610191628993 }, { - "observation_id": "b9dfaf55-fe73-4bac-a011-9ff31f13c491", + "observation_id": "77a1e244-6c0a-4fbe-a7f1-ff6fed89ce07", "type": "aruco", "marker_id": 214, "marker_size_m": 0.025, @@ -370,7 +370,7 @@ "confidence": 0.5524643209674667 }, { - "observation_id": "5e971f45-cf73-466b-ade5-bd09a71a0f8e", + "observation_id": "182c1e0c-8c53-42cd-85a9-434830f57c09", "type": "aruco", "marker_id": 211, "marker_size_m": 0.025, @@ -424,7 +424,7 @@ "confidence": 0.5730650050844824 }, { - "observation_id": "48a160b7-02c3-47de-8eba-01d6c15d5d42", + "observation_id": "d6cb9456-f998-48f0-8eee-deb3b12372bd", "type": "aruco", "marker_id": 198, "marker_size_m": 0.025, @@ -478,7 +478,7 @@ "confidence": 0.4163346774695628 }, { - "observation_id": "3280ec54-b129-45ec-bffb-7eb3c61d3bdd", + "observation_id": "f7a1112f-663d-4235-ba9c-31cc923a6a38", "type": "aruco", "marker_id": 210, "marker_size_m": 0.025, diff --git a/pipeline/render_3b_camera_pose.json b/pipeline/render_3b_camera_pose.json index 069669f..ff5ac47 100644 --- a/pipeline/render_3b_camera_pose.json +++ b/pipeline/render_3b_camera_pose.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "created_utc": "2026-05-29T11:11:52Z", + "created_utc": "2026-05-29T17:30:29Z", "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" diff --git a/pipeline/render_3c_aruco_detection.json b/pipeline/render_3c_aruco_detection.json index 5e57f07..72e3b62 100644 --- a/pipeline/render_3c_aruco_detection.json +++ b/pipeline/render_3c_aruco_detection.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "created_utc": "2026-05-29T11:11:51Z", + "created_utc": "2026-05-29T16:50:04Z", "vision_config": { "MarkerType": "DICT_4X4_250", "MarkerSize": 0.025 @@ -46,7 +46,7 @@ }, "detections": [ { - "observation_id": "342af2b4-08bf-4f4c-9d28-1285278e24c1", + "observation_id": "71ee0e21-7a94-4838-a42f-50154b66dfc2", "type": "aruco", "marker_id": 219, "marker_size_m": 0.025, @@ -100,7 +100,7 @@ "confidence": 0.7735450417034769 }, { - "observation_id": "d02166a7-4c7b-4cd0-8210-124de5d533d7", + "observation_id": "84ac23e0-0c4c-41d7-a42e-5ca454fe49c5", "type": "aruco", "marker_id": 218, "marker_size_m": 0.025, @@ -154,7 +154,7 @@ "confidence": 0.7950075578135153 }, { - "observation_id": "e51ac6d6-f1ea-42af-b51d-903b881ddf01", + "observation_id": "3fea9a30-af3d-4d51-9a5b-9072d862558f", "type": "aruco", "marker_id": 122, "marker_size_m": 0.025, @@ -208,7 +208,7 @@ "confidence": 0.8202207056111124 }, { - "observation_id": "805ea7a3-250b-4fb6-afd6-4641c83240d6", + "observation_id": "75395523-76b2-41d5-b56f-b398f6b3e4af", "type": "aruco", "marker_id": 214, "marker_size_m": 0.025, @@ -262,7 +262,7 @@ "confidence": 0.1700115058329765 }, { - "observation_id": "ccb99515-0a32-410b-b271-75f6e0e0c1c8", + "observation_id": "acc65302-27db-4275-b14c-d701d51f78d3", "type": "aruco", "marker_id": 215, "marker_size_m": 0.025, @@ -316,7 +316,7 @@ "confidence": 0.9372326698512834 }, { - "observation_id": "f689811a-b001-4d21-87a2-e01ad38c1a01", + "observation_id": "bf72359a-f1d1-4ce4-abd0-ed20ea2befee", "type": "aruco", "marker_id": 244, "marker_size_m": 0.025, @@ -370,7 +370,7 @@ "confidence": 0.7137086345973509 }, { - "observation_id": "cc2ffd45-0d44-4dad-83b2-46e2dec24bc0", + "observation_id": "11e5ef60-164f-49b4-8d01-115cfd3f18da", "type": "aruco", "marker_id": 229, "marker_size_m": 0.025, @@ -424,7 +424,7 @@ "confidence": 0.8888336147757544 }, { - "observation_id": "f5127d88-2b68-4a22-b216-fdf19b4d72a2", + "observation_id": "bf21349d-ad58-4393-8701-b53d93650a2a", "type": "aruco", "marker_id": 211, "marker_size_m": 0.025, @@ -478,7 +478,7 @@ "confidence": 0.8902520865668376 }, { - "observation_id": "e418b70b-2b7e-4381-b86d-b7dc425de6f0", + "observation_id": "53c3b400-34c1-4ada-ba8d-5811b2852b1f", "type": "aruco", "marker_id": 246, "marker_size_m": 0.025, @@ -532,7 +532,7 @@ "confidence": 0.7343609168978227 }, { - "observation_id": "d8ade844-05ae-46f2-b1c1-ab84179b062f", + "observation_id": "da992668-1b15-4386-82e0-cf8ef5770050", "type": "aruco", "marker_id": 247, "marker_size_m": 0.025, @@ -586,7 +586,7 @@ "confidence": 0.72560382306579 }, { - "observation_id": "4386d50e-2ec2-4cbb-974a-d147e3498171", + "observation_id": "263d8d62-5a24-4eb6-acd4-2d7db7df60c8", "type": "aruco", "marker_id": 198, "marker_size_m": 0.025, diff --git a/pipeline/render_3c_camera_pose.json b/pipeline/render_3c_camera_pose.json index 6771057..95228f1 100644 --- a/pipeline/render_3c_camera_pose.json +++ b/pipeline/render_3c_camera_pose.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "created_utc": "2026-05-29T11:11:52Z", + "created_utc": "2026-05-29T17:30:29Z", "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" diff --git a/markers.json b/pipeline/render_3positinSet.json similarity index 100% rename from markers.json rename to pipeline/render_3positinSet.json diff --git a/pipeline/run_pipeline_3.bat b/pipeline/run_pipeline_3.bat deleted file mode 100644 index dc54f45..0000000 --- a/pipeline/run_pipeline_3.bat +++ /dev/null @@ -1,10 +0,0 @@ - -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/" diff --git a/pipeline/run_pipeline_multiview.bat b/pipeline/run_pipeline_multiview.bat index 9964ba0..8d804b9 100644 --- a/pipeline/run_pipeline_multiview.bat +++ b/pipeline/run_pipeline_multiview.bat @@ -11,20 +11,22 @@ set ROBOT_JSON=..\robot.json set OUT_DIR=. REM Camera 1 -set IMG_1=render_3c.png +set IMG_1=render_1a.png set NPZ_1=render.npz set CAM_ID_1=cam1 REM Camera 2 (example - you need actual second camera image) -set IMG_2=render_3b.png +set IMG_2=render_1b.png set NPZ_2=render.npz set CAM_ID_2=cam2 REM Camera 3 (example) -set IMG_3=render_3a.png +set IMG_3=render_1c.png set NPZ_3=render.npz set CAM_ID_3=cam3 + + echo [STEP 1] Detect ArUco markers from all cameras python 1_detect_aruco_observations.py -i %IMG_1% -npz %NPZ_1% -robot %ROBOT_JSON% -cameraId %CAM_ID_1% -outDir %OUT_DIR% python 1_detect_aruco_observations.py -i %IMG_2% -npz %NPZ_2% -robot %ROBOT_JSON% -cameraId %CAM_ID_2% -outDir %OUT_DIR% @@ -32,13 +34,13 @@ python 1_detect_aruco_observations.py -i %IMG_3% -npz %NPZ_3% -robot %ROBOT_JSON echo. echo [STEP 2] Estimate camera poses from detections -python 2_estimate_camera_from_observations.py -i render_3c_aruco_detection.json -robot %ROBOT_JSON% -outDir %OUT_DIR% -python 2_estimate_camera_from_observations.py -i render_3b_aruco_detection.json -robot %ROBOT_JSON% -outDir %OUT_DIR% python 2_estimate_camera_from_observations.py -i render_3a_aruco_detection.json -robot %ROBOT_JSON% -outDir %OUT_DIR% +python 2_estimate_camera_from_observations.py -i render_3b_aruco_detection.json -robot %ROBOT_JSON% -outDir %OUT_DIR% +python 2_estimate_camera_from_observations.py -i render_3c_aruco_detection.json -robot %ROBOT_JSON% -outDir %OUT_DIR% echo. echo [STEP 3] Triangulate marker positions from multi-view observations -python 3_multiview_bundle_adjustment_v2.py ^ +python 3_multiview_bundle_adjustment_v6.py ^ -det render_3a_aruco_detection.json ^ -det render_3b_aruco_detection.json ^ -det render_3c_aruco_detection.json ^ diff --git a/robot.json b/robot.json index 754b618..c437647 100644 --- a/robot.json +++ b/robot.json @@ -83,7 +83,7 @@ "color": [0.04, 0.04, 0.04] } }, - "defaultPosition": { + "defaultPosition__": { "x": 10, "y": 4, "z": 20, @@ -92,7 +92,7 @@ "c": 9, "e": 1 }, - "defaultPosition____": { + "defaultPosition": { "x": 140, "y": 46, "z": -70, @@ -110,6 +110,35 @@ "c": null, "e": null }, + "constraint_rules": { + + "rigid_distance": { + "enabled": true, + "mode": "mst", + "weight": 1.0 + }, + + "joint_axis_projection": { + "enabled": true, + "max_pairs": 2, + "weight": 0.35 + }, + + "chain_axis_projection": { + "enabled": false, + "max_depth": 3, + "max_pairs": 2, + "weight": 0.15 + }, + + "axis_alignment_threshold": 0.95 + }, + "observation_weighting": { + "enabled": true, + "distance_weight": true, + "marker_size_weight": true, + "view_angle_weight": true + }, "multiview_calculation": { "combine_mode": "mean", "size_ref_px": 50.0, @@ -136,6 +165,18 @@ "c": null, "e": null }, + "state_pose_params":{ + "numbers_of_Elements_to_consider_start": 3, + "numbers_of_Elements_to_consider_final": 5, + "solver_in_between_geometrical": false, + "solver_after_geometrical": false, + "geometric_passes_per_stage": 2, + "revolute_search_coarse_deg": 5.0, + "revolute_search_fine_deg": 1.0, + "root_pose_min_markers": 3, + "use_marker_normals_flip_tiebreak": true, + "normal_flip_weight": 0.05 + }, "links": { "Board": { "parent": null, diff --git a/runLoop.py b/runLoop.py new file mode 100644 index 0000000..48887bd --- /dev/null +++ b/runLoop.py @@ -0,0 +1,104 @@ +import json +import os +import subprocess +import shutil +import argparse + +def update_robot_json(robot_json_file, camera_position, default_position): + """Aktualisiert die cameraPosition und defaultPosition in der robot.json-Datei.""" + try: + with open(robot_json_file, 'r') as f: + data = json.load(f) + + data['renderingInfo']['cameraPosition'] = camera_position + data['renderingInfo']['defaultPosition'] = default_position + + with open(robot_json_file, 'w') as f: + json.dump(data, f, indent=2) + except FileNotFoundError: + print(f"Fehler: Datei {robot_json_file} nicht gefunden.") + return False + except json.JSONDecodeError: + print(f"Fehler: JSON-Datei {robot_json_file} ist ungültig.") + return False + return True + +def run_blender(blender_executable, script_path, log_level): + """Führt Blender mit dem angegebenen Skript aus.""" + try: + command = [ + blender_executable, + "-b", + "--python", + script_path, + "--log-level", + str(log_level) + ] + subprocess.run(command, check=True, capture_output=True, text=True) + return True + except subprocess.CalledProcessError as e: + print(f"Blender-Skript fehlgeschlagen:\n{e.stderr}") + return False + +def copy_and_rename_file(source_file, destination_dir, new_filename): + """Kopiert die erstellte Bilddatei in den Zielordner und benennt sie um.""" + destination_path = os.path.join(destination_dir, new_filename) + try: + shutil.copy2(source_file, destination_path) # copy2 behält Metadaten + return True + except FileNotFoundError: + print(f"Fehler: Quelldatei {source_file} nicht gefunden.") + return False + except Exception as e: + print(f"Fehler beim Kopieren/Umbenennen der Datei: {e}") + return False + +def main(): + parser = argparse.ArgumentParser(description="Automatisiert die Roboter-Rendering-Pipeline.") + parser.add_argument("robot_json", help="Pfad zur robot.json-Datei.") + parser.add_argument("blender_executable", help="Pfad zur Blender-Executable.") + parser.add_argument("render_script", help="Pfad zum render_robot.py-Skript.") + parser.add_argument("output_dir", help="Zielordner für die gerenderten Bilder.") + parser.add_argument("--log_level", type=int, default=2, help="Log-Level für Blender (Standard: 2).") + args = parser.parse_args() + + # Kamerapositions-Dictionary + camera_positions = { + "a": [-300, -800, 500], + "b": [300, -700, 500], + "c": [600, -500, 600], + "d": [-10, -800, 500], + "e": [-500, 300, 1200], + "f": [1200, 200, 300] + } + + # Robot-Pose-Dictionary + robot_poses = { + "3": {"x": 10, "y": 4, "z": 20, "a": 10, "b": 2, "c": 9, "e": 1}, + "4": {"x": 40, "y": 48, "z": -30, "a": 30, "b": 23, "c": 9, "e": 8}, + "5": {"x": 80, "y": 93, "z": -120, "a": 120, "b": 23, "c": 9, "e": 3}, + "6": {"x": 80, "y": 20, "z": 80, "a": -120, "b": 23, "c": 9, "e": 3} + } + + for set_name, camera_position in camera_positions.items(): + for pose_name, default_position in robot_poses.items(): + + # 1. JSON aktualisieren + if not update_robot_json(args.robot_json, camera_position, default_position): + continue # Gehe zum nächsten Schleifendurchlauf + + # 2. Blender-Skript ausführen + if not run_blender(args.blender_executable, args.render_script, args.log_level): + continue # Gehe zum nächsten Schleifendurchlauf + + # 3. Datei kopieren und umbenennen + render_script_dir = os.path.dirname(args.render_script) + render_file = os.path.join(render_script_dir, "render.png") + new_filename = f"render_set{set_name}_{pose_name}.png" + if not copy_and_rename_file(render_file, args.output_dir, new_filename): + continue # Gehe zum nächsten Schleifendurchlauf + + print(f"Rendering für Set {set_name}, Pose {pose_name} erfolgreich abgeschlossen.") + +if __name__ == "__main__": + main() \ No newline at end of file