Initial commit: Speckle-Scanner 3D pipeline with setup README

This commit is contained in:
2026-06-10 03:09:05 +05:00
commit 1765934846
375 changed files with 123081 additions and 0 deletions
@@ -0,0 +1,136 @@
"""JSON schema for per-image feature detection results."""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
FEATURE_JSON_VERSION = 1
@dataclass
class FeatureRecord:
image_path: Path
json_path: Path
camera_folder: str
feature_type: str
success: bool
board_size: Optional[Tuple[int, int]] = None
square_size: Optional[float] = None
corners: Optional[np.ndarray] = None # Nx1x2 float32
center: Optional[Tuple[float, float]] = None
ellipse: Optional[Dict[str, Any]] = None
timestamp_sec: Optional[float] = None
pair_key: Optional[str] = None
preprocessing: Optional[str] = None
error: Optional[str] = None
@property
def is_chessboard(self) -> bool:
return self.success and self.feature_type == "chessboard" and self.corners is not None
@property
def corner_count(self) -> int:
if self.corners is None:
return 0
return int(self.corners.shape[0])
def corners_to_list(corners: np.ndarray) -> List[List[float]]:
flat = corners.reshape(-1, 2)
return [[float(x), float(y)] for x, y in flat]
def corners_from_list(data: List[List[float]]) -> np.ndarray:
arr = np.array(data, dtype=np.float32).reshape(-1, 1, 2)
return arr
def save_feature_json(record: FeatureRecord) -> None:
payload: Dict[str, Any] = {
"version": FEATURE_JSON_VERSION,
"image": record.image_path.name,
"camera_folder": record.camera_folder,
"feature_type": record.feature_type,
"success": record.success,
"preprocessing": record.preprocessing,
"timestamp_sec": record.timestamp_sec,
"pair_key": record.pair_key,
}
if record.board_size is not None:
payload["board_size"] = [int(record.board_size[0]), int(record.board_size[1])]
if record.square_size is not None:
payload["square_size"] = float(record.square_size)
if record.corners is not None:
payload["corners"] = corners_to_list(record.corners)
if record.center is not None:
payload["center"] = [float(record.center[0]), float(record.center[1])]
if record.ellipse is not None:
payload["ellipse"] = record.ellipse
if record.error:
payload["error"] = record.error
record.json_path.parent.mkdir(parents=True, exist_ok=True)
with open(record.json_path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
def load_feature_json(json_path: Path, image_path: Optional[Path] = None) -> FeatureRecord:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
if image_path is not None:
img = Path(image_path)
else:
stem = json_path.stem
parent = json_path.parent
img = parent / data.get("image", stem)
if not img.exists():
for ext in (".bmp", ".png", ".jpg", ".jpeg"):
candidate = parent / f"{stem}{ext}"
if candidate.exists():
img = candidate
break
board_size = None
if "board_size" in data and data["board_size"]:
board_size = (int(data["board_size"][0]), int(data["board_size"][1]))
corners = None
if data.get("corners"):
corners = corners_from_list(data["corners"])
center = None
if data.get("center"):
center = (float(data["center"][0]), float(data["center"][1]))
return FeatureRecord(
image_path=Path(img),
json_path=Path(json_path),
camera_folder=data.get("camera_folder", ""),
feature_type=data.get("feature_type", "unknown"),
success=bool(data.get("success", False)),
board_size=board_size,
square_size=data.get("square_size"),
corners=corners,
center=center,
ellipse=data.get("ellipse"),
timestamp_sec=data.get("timestamp_sec"),
pair_key=data.get("pair_key"),
preprocessing=data.get("preprocessing"),
error=data.get("error"),
)
def load_folder_features(camera_dir: Path) -> List[FeatureRecord]:
records = []
for json_path in sorted(camera_dir.glob("*.json")):
try:
records.append(load_feature_json(json_path))
except (json.JSONDecodeError, OSError) as exc:
print(f"[WARN] Skipping invalid JSON {json_path}: {exc}")
return records