Skip to content

Commit 2eec3ab

Browse files
authored
Add camera to robot calibration support (#140)
* Add camera to robot calibration support * allow for relative naming * formatting * cleanup * final * fixes to address brandons review
1 parent de13721 commit 2eec3ab

File tree

5 files changed

+219
-106
lines changed

5 files changed

+219
-106
lines changed

spot_wrapper/calibration/README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Automatic Robotic Stereo Camera Calibration Utility with Charuco Target (a.k.a Multi-Stereo Madness)
22

3+
## Find where your cameras are relative to each other, and relative to your robot
4+
## Find how your cameras project 3D points into pixels
5+
36
### Recommended Setup
47

58
![spot eye in hand cal](spot_eye_in_hand_setup.jpg)
@@ -22,13 +25,18 @@
2225
7. [Recreate the Core Calibration CLI Tool Without Depending On Spot Wrapper](#recreate-the-core-calibration-cli-tool-without-depending-on-spot-wrapper)
2326

2427
# Overview
28+
2529
This utility streamlines automatic
2630
camera calibration to **solve for the intrinsic and extrinsic parameters for two or more
2731
cameras mounted in a fixed pose relative to each other on a robot**
2832
based off of moving the robot to view a Charuco target from different poses. If you already
2933
have an existing dataset of synchronized stereo (or multi-stereo) photos of a Charuco target from different viewpoints,
30-
you can use the CLI tool to compute the intrinsic/extrinsic parameters. Additionally,
31-
the CLI tool's saving capability allows to store multiple unique calibration runs in one configuration file
34+
you can use the CLI tool to compute the intrinsic/extrinsic parameters. Additionally, if you have saved the poses the images are taken at (as homogenous 4x4 transforms from the "world" frame [most likely the robot base] to the robot planning frame [most likely the
35+
robot end-effector]), you can also calibrate
36+
the camera to robot extrinsic (eye-to-hand registration). If you don't have a dataset,
37+
you can use this tool both to generate the dataset and calibrate the cameras.
38+
39+
The CLI tool's saving capability allows to store multiple unique calibration runs in one configuration file
3240
with calibration metadata, to document related runs with different setups or parameters.
3341

3442
This was developed to calibrate the two cameras at the
@@ -205,8 +213,15 @@ existing_dataset/
205213
│ ├── 0.png # taken at viewpoint 0
206214
│ ├── 1.png
207215
│ └── 2.png
216+
├── poses/ # optional, for camera to robot cal
217+
│ ├── 0.npy # base to planning frame 4x4 homgenous transform at viewpoint 0
218+
│ ├── 1.npy # .npy files generated with np.save(FILENAME, 4x4_POSE)
219+
│ └── 2.npy
208220
```
209221

222+
Optionally, you can also include pose information, to find the camera to robot extrinsic.
223+
224+
210225
To see all possible arguments for calibration, please run ```python3 calibrate_multistereo_cameras_with_charuco_cli.py -h```.
211226
Many parameters such as board proportions and Agmruco dictionary are customizable.
212227

@@ -240,6 +255,9 @@ default:
240255
0: # reference camera index, second sublevel
241256
R: flattened_3x3_rotation_matrix_from_primary_to_reference_camera_now_9x1
242257
T: 3x1_translation_matrix_from_primary_to_reference_camera
258+
planning_frame: # the planning frame (in Spot's case, the hand)
259+
R: flattened_3x3_rotation_matrix_from_primary_to_robot_planning_frame_now_9x1
260+
T: 3x1_translation_matrix_from_primary_to_robot_planning_frame
243261
run_param:
244262
num_images: 729
245263
timestamp: '2024-08-19 03:43:06'

spot_wrapper/calibration/calibrate_multistereo_cameras_with_charuco_cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
create_charuco_board,
1414
create_ideal_charuco_image,
1515
detect_charuco_corners,
16-
load_images_from_path,
16+
load_dataset_from_path,
1717
multistereo_calibration_charuco,
1818
save_calibration_parameters,
1919
)
@@ -64,6 +64,7 @@ def calibration_helper(
6464
args: argparse.Namespace,
6565
charuco: cv2.aruco_CharucoBoard,
6666
aruco_dict: cv2.aruco_Dictionary,
67+
poses: np.ndarray,
6768
):
6869
logger.warning(
6970
f"Calibrating from {len(images)} images.. for every "
@@ -84,6 +85,7 @@ def calibration_helper(
8485
desired_stereo_pairs=args.stereo_pairs,
8586
charuco_board=charuco,
8687
aruco_dict=aruco_dict,
88+
poses=poses,
8789
)
8890
logger.info(f"Finished script, obtained {calibration}")
8991
logger.info("Saving calibration param")
@@ -115,8 +117,8 @@ def main():
115117
logger.info(f"Loading images from {args.data_path}")
116118

117119
if args.data_path is not None:
118-
images = load_images_from_path(args.data_path)
119-
calibration_helper(images=images, args=args, charuco=charuco, aruco_dict=aruco_dict)
120+
images, poses = load_dataset_from_path(args.data_path)
121+
calibration_helper(images=images, args=args, charuco=charuco, aruco_dict=aruco_dict, poses=poses)
120122
else:
121123
logger.warning("Could not load any images to calibrate from, supply --data_path")
122124

spot_wrapper/calibration/calibrate_spot_hand_camera_cli.py

Lines changed: 88 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import argparse
44
import logging
5-
from time import sleep
5+
from typing import Tuple
66

7+
import cv2
78
import numpy as np
89

10+
from spot_wrapper.calibration.automatic_camera_calibration_robot import AutomaticCameraCalibrationRobot
911
from spot_wrapper.calibration.calibrate_multistereo_cameras_with_charuco_cli import (
1012
calibration_helper,
1113
calibrator_cli,
1214
setup_calibration_param,
1315
)
1416
from spot_wrapper.calibration.calibration_util import (
1517
get_multiple_perspective_camera_calibration_dataset,
16-
load_images_from_path,
18+
load_dataset_from_path,
1719
)
1820
from spot_wrapper.calibration.spot_in_hand_camera_calibration import (
1921
SpotInHandCalibration,
@@ -26,34 +28,46 @@
2628
logger = logging.getLogger(__name__)
2729

2830

31+
def create_robot(
32+
args: argparse.ArgumentParser, charuco: cv2.aruco_CharucoBoard, aruco_dict: cv2.aruco_Dictionary
33+
) -> Tuple[AutomaticCameraCalibrationRobot, argparse.Namespace]:
34+
# Replace with your AutomaticCameraCalibrationRobot
35+
in_hand_bot = SpotInHandCalibration(args.ip, args.username, args.password)
36+
in_hand_bot._set_localization_param(
37+
charuco_board=charuco,
38+
aruco_dict=aruco_dict,
39+
resolution=(
40+
args.spot_rgb_photo_width,
41+
args.spot_rgb_photo_height,
42+
),
43+
)
44+
try:
45+
args.robot_name = in_hand_bot.robot.get_cached_robot_id().nickname
46+
except Exception:
47+
logger.warning("Could not determine cached robot nickname, saving name as unknown")
48+
args.robot_name = "unknown"
49+
return in_hand_bot, args
50+
51+
52+
def create_robot_parser() -> argparse.ArgumentParser:
53+
parser = calibrate_robot_cli()
54+
return spot_cli(parser=parser) # Replace with robot specific parsing
55+
56+
2957
def spot_main() -> None:
30-
parser = spot_cli(calibrator_cli())
58+
parser = create_robot_parser()
3159
args, aruco_dict, charuco = setup_calibration_param(parser)
3260

3361
if not args.from_data:
3462
logger.warning("This script moves the robot around. !!! USE AT YOUR OWN RISK !!!")
3563
logger.warning("HOLD Ctrl + C NOW TO CANCEL")
3664
logger.warning("The calibration board should be about a meter away with nothing within a meter of the robot.")
3765
logger.warning("The robot should NOT be docked, and nobody should have robot control")
38-
sleep(5)
39-
40-
in_hand_bot = SpotInHandCalibration(args.ip, args.username, args.password)
41-
in_hand_bot._set_localization_param(
42-
charuco_board=charuco,
43-
aruco_dict=aruco_dict,
44-
resolution=(
45-
args.spot_rgb_photo_width,
46-
args.spot_rgb_photo_height,
47-
),
48-
)
66+
# sleep(5)
4967

50-
try:
51-
args.robot_name = in_hand_bot.robot.get_cached_robot_id().nickname
52-
except Exception:
53-
logger.warning("Could not determine cached robot nickname, saving name as unknown")
54-
args.robot_name = "unknown"
68+
in_hand_bot, args = create_robot(args, charuco=charuco, aruco_dict=aruco_dict)
5569

56-
images = get_multiple_perspective_camera_calibration_dataset(
70+
images, poses = get_multiple_perspective_camera_calibration_dataset(
5771
auto_cam_cal_robot=in_hand_bot,
5872
max_num_images=args.max_num_images,
5973
distances=np.arange(*args.dist_from_board_viewpoint_range),
@@ -67,41 +81,14 @@ def spot_main() -> None:
6781
)
6882
else:
6983
logger.info(f"Loading images from {args.data_path}")
70-
images = load_images_from_path(args.data_path)
84+
images, poses = load_dataset_from_path(args.data_path)
7185

72-
calibration_helper(images=images, args=args, charuco=charuco, aruco_dict=aruco_dict)
86+
calibration_helper(images=images, args=args, charuco=charuco, aruco_dict=aruco_dict, poses=poses)
7387

7488

75-
def spot_cli(parser=argparse.ArgumentParser) -> argparse.ArgumentParser:
89+
def calibrate_robot_cli(parser: argparse.ArgumentParser = None) -> argparse.ArgumentParser:
7690
if parser is None:
7791
parser = calibrator_cli()
78-
parser.add_argument(
79-
"--ip",
80-
"-i",
81-
"-ip",
82-
dest="ip",
83-
type=str,
84-
help="The IP address of the Robot to calibrate",
85-
required=True,
86-
)
87-
parser.add_argument(
88-
"--user",
89-
"-u",
90-
"--username",
91-
dest="username",
92-
type=str,
93-
help="Robot Username",
94-
required=True,
95-
)
96-
parser.add_argument(
97-
"--pass",
98-
"-pw",
99-
"--password",
100-
dest="password",
101-
type=str,
102-
help="Robot Password",
103-
required=True,
104-
)
10592

10693
parser.add_argument(
10794
"--dist_from_board_viewpoint_range",
@@ -167,22 +154,6 @@ def spot_cli(parser=argparse.ArgumentParser) -> argparse.ArgumentParser:
167154
help="How long to wait after movement to take a picture; don't want motion blur",
168155
)
169156

170-
parser.add_argument(
171-
"--spot_rgb_photo_width",
172-
"-dpw",
173-
type=int,
174-
default=640,
175-
dest="spot_rgb_photo_width",
176-
help="What resolution use for Spot's RGB Hand Camera (width). Currently, only 640 and 1920 are supported",
177-
)
178-
179-
parser.add_argument(
180-
"--spot_rgb_photo_height",
181-
"-dph",
182-
type=int,
183-
default=480,
184-
help="What resolution use for Spot's RGB Hand Camera (width). Currently, only 480 and 1080 are supported",
185-
)
186157
# path things
187158
parser.add_argument(
188159
"--save_data",
@@ -203,5 +174,56 @@ def spot_cli(parser=argparse.ArgumentParser) -> argparse.ArgumentParser:
203174
return parser
204175

205176

177+
def spot_cli(parser: argparse.ArgumentParser = None) -> argparse.ArgumentParser:
178+
if parser is None:
179+
parser = calibrate_robot_cli()
180+
181+
parser.add_argument(
182+
"--ip",
183+
"-i",
184+
"-ip",
185+
dest="ip",
186+
type=str,
187+
help="The IP address of the Robot to calibrate",
188+
required=True,
189+
)
190+
parser.add_argument(
191+
"--user",
192+
"-u",
193+
"--username",
194+
dest="username",
195+
type=str,
196+
help="Robot Username",
197+
required=True,
198+
)
199+
parser.add_argument(
200+
"--pass",
201+
"-pw",
202+
"--password",
203+
dest="password",
204+
type=str,
205+
help="Robot Password",
206+
required=True,
207+
)
208+
209+
parser.add_argument(
210+
"--spot_rgb_photo_width",
211+
"-dpw",
212+
type=int,
213+
default=640,
214+
dest="spot_rgb_photo_width",
215+
help="What resolution use for Spot's RGB Hand Camera (width). Currently, only 640 and 1920 are supported",
216+
)
217+
218+
parser.add_argument(
219+
"--spot_rgb_photo_height",
220+
"-dph",
221+
type=int,
222+
default=480,
223+
help="What resolution use for Spot's RGB Hand Camera (width). Currently, only 480 and 1080 are supported",
224+
)
225+
return parser
226+
227+
206228
if __name__ == "__main__":
207229
spot_main()

0 commit comments

Comments
 (0)