Skip to content

Commit

Permalink
Merge pull request #39 from UCL/34-calibration-checker
Browse files Browse the repository at this point in the history
34 calibration checker
  • Loading branch information
thompson318 authored Jun 9, 2021
2 parents a928fda + be0df3d commit da7ca07
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 7 deletions.
8 changes: 8 additions & 0 deletions sksVideoCalibrationChecker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys

from sksurgerycalibration.ui.video_calibration_checker_command_line import main

if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
6 changes: 3 additions & 3 deletions sksurgerycalibration/ui/video_calibration_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
# pylint:disable=too-many-nested-blocks,too-many-branches


def run_video_calibration(configuration, save_dir = None, prefix = None):
def run_video_calibration(configuration = None, save_dir = None, prefix = None):
"""
Performs Video Calibration using OpenCV
source and scikit-surgerycalibration.
Currently only chessboards are supported
:param config_file: mandatory location of config file.
:param config_file: location of a configuration file.
:param save_dir: optional directory name to dump calibrations to.
:param prefix: file name prefix when saving
Expand All @@ -39,6 +39,7 @@ def run_video_calibration(configuration, save_dir = None, prefix = None):
corners = configuration.get("corners", [14, 10])
corners = (corners[0], corners[1])
size = configuration.get("square size in mm", 3)
window_size = configuration.get("window size", None)
min_num_views = configuration.get("minimum number of views", 5)
keypress_delay = configuration.get("keypress delay", 10)
interactive = configuration.get("interactive", True)
Expand All @@ -48,7 +49,6 @@ def run_video_calibration(configuration, save_dir = None, prefix = None):
if not cap.isOpened():
raise RuntimeError("Failed to open camera.")

window_size = configuration.get("window size", None)
if window_size is not None:
cap.set(cv2.CAP_PROP_FRAME_WIDTH, window_size[0])
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, window_size[1])
Expand Down
136 changes: 136 additions & 0 deletions sksurgerycalibration/ui/video_calibration_checker_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# coding=utf-8

""" Application to detect chessboards, and assess calibration accuracy. """

import numpy as np
import cv2
import sksurgeryimage.calibration.chessboard_point_detector as cpd
from sksurgerycalibration.video.video_calibration_params import \
MonoCalibrationParams

# pylint: disable=too-many-branches

def run_video_calibration_checker(configuration = None,
calib_dir = './', prefix = None):
"""
Application that detects a calibration pattern, runs
solvePnP, and prints information out to enable you to
check how accurate a calibration actually is.
:param config_file: location of configuration file.
:param calib_dir: the location of the calibration directory you want to
check
:param prefix: the file prefix for the calibration data you want to check
:raises ValueError: if no configuration provided.
:raises RuntimeError: if can't open source.
"""
if configuration is None:
raise ValueError("Calibration Checker requires a config file")

source = configuration.get("source", 0)
corners = configuration.get("corners", [14, 10])
corners = (corners[0], corners[1])
size = configuration.get("square size in mm", 3)
window_size = configuration.get("window size", None)
keypress_delay = configuration.get("keypress delay", 10)
interactive = configuration.get("interactive", True)
sample_frequency = configuration.get("sample frequency", 1)

existing_calibration = MonoCalibrationParams()
existing_calibration.load_data(calib_dir, prefix, halt_on_ioerror = False)
intrinsics = existing_calibration.camera_matrix
distortion = existing_calibration.dist_coeffs

cap = cv2.VideoCapture(source)
if not cap.isOpened():
raise RuntimeError("Failed to open camera:" + str(source))

if window_size is not None:
cap.set(cv2.CAP_PROP_FRAME_WIDTH, window_size[0])
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, window_size[1])
print("Video feed set to ("
+ str(window_size[0]) + " x " + str(window_size[1]) + ")")
else:
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print("Video feed defaults to ("
+ str(width) + " x " + str(height) + ")")

# For detecting the chessboard points
detector = cpd.ChessboardPointDetector(corners, size)
num_pts = corners[0] * corners[1]
captured_positions = np.zeros((0, 3))
frames_sampled = 0
while True:
frame_ok, frame = cap.read()

key = None
frames_sampled += 1

if not frame_ok:
print("Reached end of video source or read failure.")
key = ord('q')
else:
undistorted = cv2.undistort(frame, intrinsics, distortion)
if interactive:
cv2.imshow("live image", undistorted)
key = cv2.waitKey(keypress_delay)
else:
if frames_sampled % sample_frequency == 0:
key = ord('a')

if key == ord('q'):
break

image_points = np.array([])
if key in [ord('c'), ord('m'), ord('t'), ord('a')]:
_, object_points, image_points = \
detector.get_points(undistorted)

pnp_ok = False
img = None
tvec = None
if image_points.shape[0] > 0:
img = cv2.drawChessboardCorners(undistorted, corners,
image_points,
num_pts)
if interactive:
cv2.imshow("detected points", img)

pnp_ok, _, tvec = cv2.solvePnP(object_points,
image_points,
intrinsics,
None)
if pnp_ok:
captured_positions = np.append(captured_positions,
np.transpose(tvec),
axis=0)

if key in [ord('t'), ord('a')] and captured_positions.shape[0] > 1:
print(str(captured_positions[-1][0]
- captured_positions[-2][0]) + " "
+ str(captured_positions[-1][1]
- captured_positions[-2][1]) + " "
+ str(captured_positions[-1][2]
- captured_positions[-2][2]) + " ")
if key in [ord('m'), ord('a')] and \
captured_positions.shape[0] > 1:
print("Mean:"
+ str(np.mean(captured_positions, axis=0)))
print("StdDev:"
+ str(np.std(captured_positions, axis=0)))

if key in [ord('c'), ord('a')] and pnp_ok:
print("Pose" + str(tvec[0][0]) + " "
+ str(tvec[1][0]) + " "
+ str(tvec[2][0]))

if not pnp_ok and image_points.shape[0] > 0:
print("Failed to solve PnP")

if image_points.shape[0] == 0:
print("Failed to detect points")

cap.release()
cv2.destroyAllWindows()
49 changes: 49 additions & 0 deletions sksurgerycalibration/ui/video_calibration_checker_command_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# coding=utf-8

""" CLI for sksurgerycalibration video calibration checker. """

import argparse

from sksurgerycore.configuration.configuration_manager import \
ConfigurationManager
from sksurgerycalibration import __version__
from sksurgerycalibration.ui.video_calibration_checker_app \
import run_video_calibration_checker


def main(args=None):

""" Entry point for bardVideoCalibrationChecker application. """

parser = argparse.ArgumentParser(
description='SciKit-Surgery Calibration '
'Video Calibration Checker')

parser.add_argument("-c", "--config",
required=True,
type=str,
help="Configuration file containing the parameters.")

parser.add_argument("-d", "--calib_dir",
required=True,
type=str,
help="Directory containing calibration data.")

parser.add_argument("-p", "--prefix",
required=True,
type=str,
help="Prefix for calibration data.")

version_string = __version__
friendly_version_string = version_string if version_string else 'unknown'
parser.add_argument(
"--version",
action='version',
version='scikit-surgerycalibration version ' + friendly_version_string)

args = parser.parse_args(args)

configurer = ConfigurationManager(args.config)
configuration = configurer.get_copy()

run_video_calibration_checker(configuration, args.calib_dir, args.prefix)
1 change: 0 additions & 1 deletion sksurgerycalibration/video/video_calibration_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

# pylint: disable=missing-function-docstring, invalid-name


def get_calib_prefix(file_prefix: str):

prefix = 'calib'
Expand Down
17 changes: 14 additions & 3 deletions sksurgerycalibration/video/video_calibration_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,16 @@ def save_data(self,

def load_data(self,
dir_name: str,
file_prefix: str
file_prefix: str,
halt_on_ioerror = True
):
"""
Loads calibration parameters from a directory.
:param dir_name: directory to load from
:param file_prefix: prefix for all files
:param halt_on_ioerror: if false, and handeye or pattern2marker
are not found they will be left as None
"""
self.reinit()

Expand All @@ -135,12 +138,20 @@ def load_data(self,
handeye_file = sksio.get_handeye_file_name(dir_name,
file_prefix)

self.handeye_matrix = np.loadtxt(handeye_file)
try:
self.handeye_matrix = np.loadtxt(handeye_file)
except IOError:
if halt_on_ioerror:
raise

p2m_file = sksio.get_pattern2marker_file_name(dir_name,
file_prefix)

self.pattern2marker_matrix = np.loadtxt(p2m_file)
try:
self.pattern2marker_matrix = np.loadtxt(p2m_file)
except IOError:
if halt_on_ioerror:
raise

extrinsic_files = sksio.get_extrinsic_file_names(dir_name,
file_prefix)
Expand Down
17 changes: 17 additions & 0 deletions tests/ui/test_video_calibration_app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for command line application """
import copy
import os
import pytest
from sksurgerycalibration.ui.video_calibration_app import run_video_calibration

Expand All @@ -13,14 +14,30 @@
"sample frequency" : 2
}

def _clean_up(prefix):
"""Helper to clean up calibration results"""
for i in range(5):
os.remove(prefix + ".extrinsics." + str(i) + ".txt")
os.remove(prefix + ".ids." + str(i) + ".txt")
os.remove(prefix + ".image_points." + str(i) + ".txt")
os.remove(prefix + ".object_points." + str(i) + ".txt")
os.remove(prefix + ".images." + str(i) + ".png")
os.remove(prefix + ".distortion.txt")
os.remove(prefix + ".handeye.txt")
os.remove(prefix + ".intrinsics.txt")
os.remove(prefix + ".pattern2marker.txt")


def test_with_save_prefix():
""" Run command line app with a save prefix"""
run_video_calibration(config, prefix = "testjunk")
_clean_up("testjunk")

def test_with_save_directory():
""" Run command line app with a save prefix"""
run_video_calibration(config, save_dir = "testjunk")
_clean_up("testjunk/calib")
os.rmdir("testjunk")

def test_with_invalid_method():
"""Should throw a value error if method is not supported"""
Expand Down
21 changes: 21 additions & 0 deletions tests/ui/test_video_calibration_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Tests for command line application """
import pytest
from sksurgerycalibration.ui.video_calibration_checker_command_line import main

def test_cl_no_config():
""" Run command line app with no config file. The parser should
raise SystemExit due to missing required argument"""
with pytest.raises(SystemExit) as pytest_wrapped_e:
main([])

#I'm not sure how useful the next 2 asserts are. We already know it's
#a SystemExit, if the code value specific to the parser?
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 2


def test_cl_with_config():
""" Run command line app with config """
main(['-c', 'config/recorded_chessboard.json',
'-d', 'tests/data/laparoscope_calibration/cbh-viking/',
'-p', 'calib.left'])
48 changes: 48 additions & 0 deletions tests/ui/test_video_calibration_checker_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Tests for command line application """
import copy
import pytest
from sksurgerycalibration.ui.video_calibration_checker_app import \
run_video_calibration_checker

config = { "method": "chessboard",
"source": "tests/data/laparoscope_calibration/left/left.ogv",
"corners": [14, 10],
"square size in mm": 6,
"minimum number of views": 5,
"keypress delay": 0,
"interactive" : False,
"sample frequency" : 2
}

def test_with_no_config():
"""It shouldn't run with no configuration file"""
with pytest.raises(ValueError):
run_video_calibration_checker(None,
calib_dir = 'tests/data/laparoscope_calibration/cbh-viking',
prefix = "calib.right")


def test_with_prefix():
""" Run command line app with an existing calibration"""
run_video_calibration_checker(config,
calib_dir = 'tests/data/laparoscope_calibration/cbh-viking',
prefix = "calib.right")


def test_with_invalid_capture():
"""Should throw a runtime error if we can't open video capture"""
duff_config = copy.deepcopy(config)
duff_config['source'] = 'bad source'
with pytest.raises(RuntimeError):
run_video_calibration_checker(duff_config,
calib_dir = 'tests/data/laparoscope_calibration/cbh-viking',
prefix = "calib.right")


def test_with_custome_window_size():
"""We should be able to set the window size in config"""
ok_config = copy.deepcopy(config)
ok_config['window size'] = [640, 480]
run_video_calibration_checker(ok_config,
calib_dir = 'tests/data/laparoscope_calibration/cbh-viking',
prefix = "calib.right")

0 comments on commit da7ca07

Please sign in to comment.