mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
135 lines
3.1 KiB
Python
135 lines
3.1 KiB
Python
|
|
import base64
|
||
|
|
from dataclasses import dataclass
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import cv2
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Resolution:
|
||
|
|
width: int
|
||
|
|
height: int
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class JsonMatOfDoubles:
|
||
|
|
rows: int
|
||
|
|
cols: int
|
||
|
|
type: int
|
||
|
|
data: list[float]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class JsonMat:
|
||
|
|
rows: int
|
||
|
|
cols: int
|
||
|
|
type: int
|
||
|
|
data: str # Base64-encoded PNG data
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Point2:
|
||
|
|
x: float
|
||
|
|
y: float
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Translation3d:
|
||
|
|
x: float
|
||
|
|
y: float
|
||
|
|
z: float
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Quaternion:
|
||
|
|
X: float
|
||
|
|
Y: float
|
||
|
|
Z: float
|
||
|
|
W: float
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Rotation3d:
|
||
|
|
quaternion: Quaternion
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Pose3d:
|
||
|
|
translation: Translation3d
|
||
|
|
rotation: Rotation3d
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Point3:
|
||
|
|
x: float
|
||
|
|
y: float
|
||
|
|
z: float
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Observation:
|
||
|
|
# Expected feature 3d location in the camera frame
|
||
|
|
locationInObjectSpace: list[Point3]
|
||
|
|
# Observed location in pixel space
|
||
|
|
locationInImageSpace: list[Point2]
|
||
|
|
# (measured location in pixels) - (expected from FK)
|
||
|
|
reprojectionErrors: list[Point2]
|
||
|
|
# Solver optimized board poses
|
||
|
|
optimisedCameraToObject: Pose3d
|
||
|
|
# If we should use this observation when re-calculating camera calibration
|
||
|
|
includeObservationInCalibration: bool
|
||
|
|
snapshotName: str
|
||
|
|
# The actual image the snapshot is from
|
||
|
|
snapshotData: JsonMat
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class CameraCalibration:
|
||
|
|
resolution: Resolution
|
||
|
|
cameraIntrinsics: JsonMatOfDoubles
|
||
|
|
distCoeffs: JsonMatOfDoubles
|
||
|
|
observations: list[Observation]
|
||
|
|
|
||
|
|
|
||
|
|
def convert_photon_to_mrcal(photon_cal_json_path: str, output_folder: str):
|
||
|
|
"""
|
||
|
|
Unpack a Photon calibration JSON (eg, photon_calibration_Microsoft_LifeCam_HD-3000_800x600.json) into
|
||
|
|
the output_folder directory with images and corners.vnl file for use with mrcal.
|
||
|
|
"""
|
||
|
|
with open(photon_cal_json_path, "r") as cal_json:
|
||
|
|
# Convert to nested objects instead of nameddicts on json-loads
|
||
|
|
class Generic:
|
||
|
|
@classmethod
|
||
|
|
def from_dict(cls, dict):
|
||
|
|
obj = cls()
|
||
|
|
obj.__dict__.update(dict)
|
||
|
|
return obj
|
||
|
|
|
||
|
|
camera_cal_data: CameraCalibration = json.loads(
|
||
|
|
cal_json.read(), object_hook=Generic.from_dict
|
||
|
|
)
|
||
|
|
|
||
|
|
# Create output_folder if not exists
|
||
|
|
if not os.path.exists(output_folder):
|
||
|
|
os.makedirs(output_folder)
|
||
|
|
|
||
|
|
# Decode each image and save it as a png
|
||
|
|
for obs in camera_cal_data.observations:
|
||
|
|
image = obs.snapshotData.data
|
||
|
|
decoded_data = base64.b64decode(image)
|
||
|
|
np_data = np.frombuffer(decoded_data, np.uint8)
|
||
|
|
img = cv2.imdecode(np_data, cv2.IMREAD_UNCHANGED)
|
||
|
|
cv2.imwrite(f"{output_folder}/{obs.snapshotName}", img)
|
||
|
|
|
||
|
|
# And create a VNL file for use with mrcal
|
||
|
|
with open(f"{output_folder}/corners.vnl", "w+") as vnl_file:
|
||
|
|
vnl_file.write("# filename x y level\n")
|
||
|
|
|
||
|
|
for obs in camera_cal_data.observations:
|
||
|
|
for corner in obs.locationInImageSpace:
|
||
|
|
# Always level zero
|
||
|
|
vnl_file.write(f"{obs.snapshotName} {corner.x} {corner.y} 0\n")
|
||
|
|
|
||
|
|
vnl_file.flush()
|