Initial commit: Speckle-Scanner 3D pipeline with setup README
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user