Commit 129421c9 authored by Vincent POLLET's avatar Vincent POLLET
Browse files

Allow to update calibration file with markers values, text changes, formatting

parent 0adce84f
Pipeline #52530 failed with stage
in 11 minutes and 20 seconds
......@@ -20,8 +20,7 @@ def load_camera_config(filepath, camera_name=None):
R = np.array(camera_config['relative_rotation'])
T = np.array(camera_config['relative_translation'])
markers = np.array(camera_config['markers'])
# TODO
assert camera_config['reference'] == 'nir_left'
camera_list[name] = Camera(camera_matrix, dist_coeffs, R, T, markers)
if camera_name is not None:
return camera_list[camera_name]
......
......@@ -3,28 +3,46 @@
# Copyright (c) 2021 Idiap Research Institute, https://www.idiap.ch/
#
"""Calibration script to find markers in each stream image and output a calibration usefull for linear transformation (warp).
This scripts performs the following steps:
1 Load the configuration JSON for the calibration
2 Load capture file (hdf5) with the captures of a chessboard for each camera.
3 Find the chessboards corners and keep the 4 internal corners as markers coordinates.
If this fails, the capture is displayed to the user, which should click on the corners with the mouse.
The mouse coordinates are recorded for the markers coordinates (to skip a stream, simply close the window).
4 (Optional) Load a previous calibration from disk and simply updates the markers coordinates.
5 Write the output calibration as a JSON file.
"""
import argparse
import numpy as np
import copy
import json
import numpy as np
import cv2
from .calibration import detect_chessboard_corners, preprocess_image
from bob.io.stream import Stream, StreamFile
from bob.io.image import bob_to_opencvbgr
from .calibration import detect_chessboard_corners, preprocess_image
pixels_xy = []
def point_picking_event_cb(event, x, y, flags, params):
"""
This function used as a callback when a mouse left button event is activated
by clicking on the opencv image. It saves the pixel selected in a global
list 'pixels_xy'.
"""
global pixels_xy
if event == cv2.EVENT_LBUTTONDOWN:
pixels_xy.append([x,y])
pixels_xy.append([x, y])
print("Point selected for marker {}: {}".format(len(pixels_xy), pixels_xy))
def warp_calibrate(h5_file, config, verbosity):
"""
This function load the streams saved in datasets of an hdf5 file, with images
......@@ -32,7 +50,7 @@ def warp_calibrate(h5_file, config, verbosity):
grayscale <H,W>, process if necessary and try to detect a target. The four side
corners are saved as markers to be later used for linear warp calibration in
bob.ip.stereo.
If the target is not deteted, an interactive figure open, where the user
If the target is not detected, an interactive figure is opened, where the user
should click on the 4 corners (markers), in this order:
1. down-left
2. up-left
......@@ -52,6 +70,9 @@ def warp_calibrate(h5_file, config, verbosity):
:py:class:`dict`
A dictionnary with detected markers for each streams.
"""
# The detection findes the inner points of the chessboard pattern, which
# are 1 less than the actual number of squares in each direction
rows = config["pattern_rows"] - 1
columns = config["pattern_columns"] - 1
pattern_size = (rows, columns)
......@@ -70,7 +91,7 @@ def warp_calibrate(h5_file, config, verbosity):
if params is not None:
if params["invert"]:
image = np.ones_like(image)*255 - image
image = np.ones_like(image) * 255 - image
prep_image = preprocess_image(image, params["closing"], params["threshold"])
else:
prep_image = None
......@@ -78,12 +99,16 @@ def warp_calibrate(h5_file, config, verbosity):
if ret:
print("Stream : {} : Detected chessboard corners.".format(stream.name))
markers = np.stack([ptrn_pts[0][0],
ptrn_pts[rows-1][0],
ptrn_pts[rows*(columns-1)][0],
ptrn_pts[rows*columns -1][0]])
markers = np.stack(
[
ptrn_pts[0][0],
ptrn_pts[rows - 1][0],
ptrn_pts[rows * (columns - 1)][0],
ptrn_pts[rows * columns - 1][0],
]
)
else:
print( "Stream : {} : Failed to detect chessboard corners.".format(stream.name))
print("Stream : {} : Failed to detect chessboard corners.".format(stream.name))
window_name = "image_{}".format(stream.name)
cv2.imshow(window_name, image)
global pixels_xy
......@@ -92,14 +117,22 @@ def warp_calibrate(h5_file, config, verbosity):
while True:
key = cv2.waitKey(1)
if len(pixels_xy) == 4:
print( "Stream : {} : Finished to collect manually\
the corners.".format(stream.name))
print(
"Stream : {} : Finished to collect manually\
the corners.".format(
stream.name
)
)
cv2.destroyAllWindows()
markers = np.stack(copy.copy(pixels_xy))
break
if key == 27 or cv2.getWindowProperty(window_name, cv2.WND_PROP_VISIBLE) < 1:
print( "Stream : {} : Failed to collect manually\
the corners.".format(stream.name))
print(
"Stream : {} : Failed to collect manually\
the corners.".format(
stream.name
)
)
cv2.destroyAllWindows()
markers = None
break
......@@ -110,10 +143,13 @@ def warp_calibrate(h5_file, config, verbosity):
return calibration
def fulfill_calibration(calibration, reference):
"""
This function fulfill the calibration keys to be compatible with
bob.ip.stereo standard calibration saved in JSON.
def fill_calibration(calibration, reference):
"""Add more elements (keys) to the calibration dictionnary to make it work with bob.ip.stereo standard
bob.ip.stereo expects some keys to be present in a calibration JSON files, however when using only warp transform
most of them are not required. This function simply adds them with values None. If the keys are already present, do
not change them ; if they are missing, set them to None.
Parameters
----------
......@@ -125,31 +161,51 @@ def fulfill_calibration(calibration, reference):
Returns
-------
:py:class:`dict`
The fulfilled calibration.
The filled calibration.
"""
# TODO: To not duplicate, verify if the standard keys defined somewhere else.
# Maybe it should be defined in bob.ip.stereo.Camera.load_camera_config()
standard_calibration_keys = ["camera_matrix",
"distortion_coefs",
"markers",
"relative_rotation",
"relative_translation"]
# Make sure that all the keys used in bob.ip.stereo.Camera.load_camera_config()
# are defined.
standard_calibration_keys = [
"camera_matrix",
"distortion_coefs",
"markers",
"relative_rotation",
"relative_translation",
]
# load calibration config
fulfill_calibration = copy.copy(calibration)
fill_calibration = copy.copy(calibration)
for camera, camera_config in calibration.items():
for key in standard_calibration_keys:
if key not in camera_config.keys():
# Projection markers as the 4 external corners
fulfill_calibration[camera][key] = None
# TODO: bob.ip.stereo currently only accept "nir_left" as a reference.
# https://gitlab.idiap.ch/bob/bob.ip.stereo/-/blob/calibration/bob/ip/stereo/camera.py#L24
# While not corrected, the reference is written as "nir_left", but won't
# be used for warp alignement anyway. Once solve, the reference will be
# saved correctly.
fulfill_calibration[camera]["reference"] = "nir_left"
# if "reference" not in camera_config.keys():
# updated_config[camera]["reference"] = reference
return fulfill_calibration
fill_calibration[camera][key] = None
if "reference" not in camera_config.keys():
fill_calibration[camera]["reference"] = reference
return fill_calibration
def update_calibration(markers_calibration, bobipstereo_calibration):
"""Updates the markers in bobipstereo_calibration with values from markers_calibration
Parameters
----------
markers_calibration : dict
Calibration dictionary containing only markers values per camera
bobipstereo_calibration : dict
Calibration dictionnary with all keys needed for bob.ip.stereo usage.
"""
if set(bobipstereo_calibration.keys()) != set(bobipstereo_calibration.keys()):
raise ValueError("Stream names in warp calibration config and loaded config to update do not match!")
for camera in bobipstereo_calibration.keys():
if camera in markers_calibration.keys():
bobipstereo_calibration[camera]["markers"] = markers_calibration[camera]["markers"]
return bobipstereo_calibration
def parse_arguments():
parser = argparse.ArgumentParser(description=__doc__)
......@@ -158,21 +214,17 @@ def parse_arguments():
"--config",
type=str,
help="An absolute path to the JSON file containing the configuration \
to run the warp calibration."
)
parser.add_argument(
"-r",
"--reference",
type=str,
default=None,
help="Reference stream."
to run the warp calibration.",
)
parser.add_argument("-r", "--reference", type=str, default=None, help="Reference stream.")
parser.add_argument("-i", "--h5-file", type=str, default=None, help="Input hdf5 file.")
parser.add_argument("-o", "--output-file", type=str, default=None, help="Output calibration JSON file.")
parser.add_argument(
"-o", "--output-file",
"-u",
"--update_calibration",
type=str,
default=None,
help="Output calibration JSON file."
help="This calibration will used for the output values, except for the markers position which are updated by the calibration.",
)
parser.add_argument(
"-v",
......@@ -184,6 +236,7 @@ def parse_arguments():
)
return parser.parse_args()
def main():
args = parse_arguments()
with open(args.config) as f:
......@@ -197,7 +250,17 @@ def main():
for stream, duplicate_stream in config["duplicate_calibration"].items():
calibration[duplicate_stream] = calibration[stream]
calibration = fulfill_calibration(calibration, args.reference)
# If there is a configuration to update: we load it and update the markers
# fields of the cameras
if args.update_calibration is not None:
with open(args.update_calibration) as calib_f:
to_update_calib = json.load(calib_f)
calibration = update_calibration(calibration, to_update_calib)
# If not we still add some keys to the calibration dictionnary to comply
# with what bob.ip.stereo expects.
else:
calibration = fill_calibration(calibration, args.reference)
print(calibration)
if args.output_file is not None:
......@@ -205,5 +268,6 @@ def main():
json.dump(calibration, f, indent=4, sort_keys=True)
print("{} written.".format(args.output_file))
if __name__ == "__main__":
main()
......@@ -4,11 +4,19 @@
Warp Calibration
----------------
Without depth map / 3D pointcloud, the remapping of streams on a reference is not useable. To align images in different streams from bounding boxes defined in the reference stream, a linear transform (warp) can be used.
When depth map / 3D pointcloud information is not available, the remapping of streams onto a reference is not useable.
It is however still possible to align images in different streams, by using bounding boxes defined in the reference
stream and applying a linear transform (warp).
To create such transform, it is necessary to collect four fixed points static in all streams. First, capture an hdf5 file with a chessboard target kept static during the whole capture, in front of the camears and at a defined distance.
This linear calibration is dependent of the distance camera-target: it simply is the shift of a point from on
camera's image to another camera, at a certain distance. Therefore the transform should only be used for images where
the subject is at the same distance from the cameras than during the calibration.
To create such a transform, it is necessary to collect four fixed points, static in all streams. First, capture an hdf5
file with a chessboard target kept static during the whole capture, in front of the camears and at a defined distance.
The calibration can then be performed with the ``warp_calibration.py`` script.
This linear calibration is dependent of the distance camera-target, thus the result of an alignement will depend of this distance.
Configuration
-------------
......@@ -20,7 +28,7 @@ Here is an example of JSON configuration to create for a warp calibration.
{
"stream_config" : "/path/to/streams_config.json",
"pattern_rows": 9,
"pattern_columns": 6
"pattern_columns": 6,
"frame" : 0,
"streams": {
"camera1_rgb": null,
......@@ -43,13 +51,16 @@ Here is an example of JSON configuration to create for a warp calibration.
}
| - ``stream_config`` : Path to the JSON data config.
| - ``pattern_rows/columns`` : Number of square rows/columns in the chessboard target. See the image below that correspond to 9x6.
| - ``frame`` : Frame number to select in the dataset.
| - ``streams`` : Streams to calibrate. It must correspond to the datasets in the hdf5 file.
| - ``pattern_rows/columns`` : Number of square rows/columns in the chessboard target. See the image below that
corresponds to 9x6.
| - ``frame`` : Frame number to select in each dataset in the ``hdf5`` files.
| - ``streams`` : Streams to calibrate. The names must correspond to the stream names in the ``stream_config``.
| - ``threshold`` : Pixel value (0-255) used to binary threshold grayscale image.
| - ``closing`` : Closing size in pixel.
| - ``invert`` : Invert the pixel intensity (used for thermal images, when black square render white pixel under halogen illumination).
| - ``duplicate_calibration`` : To copy the calibration of one streams to another. Used by example for depth stream that is aligned with a NIR streams.
| - ``invert`` : Invert the pixel intensity (used for thermal images, when black square render white pixel under
halogen illumination).
| - ``duplicate_calibration`` : To copy the calibration of one streams to another. Used by example for depth stream that
is aligned with a NIR streams.
calibration
......@@ -59,7 +70,8 @@ calibration
$ warp_calibration.py -i /path/to/capture.h5 -c warp_config.json -vvv -o /path/to/calibration.json
If the chessboard pattern is not found, the image is displayed. The user must click on the internal 4 side-corners of the chessboard in this order:
If the chessboard pattern is not automatically found, the image is displayed. The user must click on the internal 4
side-corners of the chessboard in this order:
1. down-left (in blue)
2. up-left (still in blue)
......@@ -71,4 +83,3 @@ If the chessboard pattern is not found, the image is displayed. The user must cl
:align: center
If the user does not want to output markers, the window can be closed, the stream will not appear in the JSON output.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment