From 3b4bcb09e4d9e666589e5c6bc508530c01734a36 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Tue, 25 Jun 2019 12:22:27 -0400 Subject: [PATCH] Added camera module, tests Former-commit-id: 48298e646cdb964b76bc2853f78ae75b69977e7a --- opendm/camera.py | 72 ++++++++++++++++++++++++++++++++ opendm/config.py | 32 ++++++++++++++ opendm/osfm.py | 43 ++++++------------- tests/assets/reconstruction.json | 23 ++++++++++ tests/test_camera.py | 41 ++++++++++++++++++ 5 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 opendm/camera.py create mode 100644 tests/assets/reconstruction.json create mode 100644 tests/test_camera.py diff --git a/opendm/camera.py b/opendm/camera.py new file mode 100644 index 00000000..ee78caff --- /dev/null +++ b/opendm/camera.py @@ -0,0 +1,72 @@ +import os, json +from opendm import log + +def get_cameras_from_opensfm(reconstruction_file): + """ + Extract the cameras from OpenSfM's reconstruction.json + """ + if os.path.exists(reconstruction_file): + with open(reconstruction_file, 'r') as fin: + reconstructions = json.loads(fin.read()) + + result = {} + for recon in reconstructions: + if 'cameras' in recon: + for camera_id in recon['cameras']: + # Strip "v2" from OpenSfM camera IDs + new_camera_id = camera_id + if new_camera_id.startswith("v2 "): + new_camera_id = new_camera_id[3:] + + result[new_camera_id] = recon['cameras'][camera_id] + + # Remove "_prior" keys + keys = list(result[new_camera_id].keys()) + for k in keys: + if k.endswith('_prior'): + result[new_camera_id].pop(k) + return result + else: + raise RuntimeError("%s does not exist." % reconstruction_file) + + +def get_opensfm_camera_models(cameras): + """ + Convert cameras to a format OpenSfM can understand + (opposite of get_cameras_from_opensfm) + """ + if isinstance(cameras, dict): + result = {} + for camera_id in cameras: + # Quick check on IDs + if len(camera_id.split(" ")) < 6: + raise RuntimeError("Invalid cameraID: %s" % camera_id) + + # Add "v2" to camera ID + if not camera_id.startswith("v2 "): + osfm_camera_id = "v2 " + camera_id + else: + osfm_camera_id = camera_id + + # Add "_prior" keys + camera = cameras[camera_id] + prior_fields = ["focal","focal_x","focal_y","c_x","c_y","k1","k2","p1","p2","k3"] + valid_fields = ["id","width","height","projection_type"] + prior_fields + [f + "_prior" for f in prior_fields] + + keys = list(camera.keys()) + for param in keys: + param_prior = param + "_prior" + if param in prior_fields and not param_prior in camera: + camera[param_prior] = camera[param] + + # Remove invalid keys + keys = list(camera.keys()) + for k in keys: + if not k in valid_fields: + camera.pop(k) + log.ODM_WARNING("Invalid camera key ignored: %s" % k) + + result[osfm_camera_id] = camera + return result + else: + raise RuntimeError("Invalid cameras format: %s. Expected dict." % str(cameras)) diff --git a/opendm/config.py b/opendm/config.py index f0f97b1b..bcd9d916 100644 --- a/opendm/config.py +++ b/opendm/config.py @@ -1,4 +1,5 @@ import argparse +import json from opendm import context from opendm import io from opendm import log @@ -23,6 +24,25 @@ def alphanumeric_string(string): raise argparse.ArgumentTypeError(msg) return string +def path_or_json_string(string): + if string == "": + return {} + + if string.startswith("[") or string.startswith("{"): + try: + return json.loads(string) + except: + raise argparse.ArgumentTypeError("{0} is not a valid JSON string.".format(string)) + elif io.file_exists(string): + try: + with open(string, 'r') as f: + return json.loads(f.read()) + except: + raise argparse.ArgumentTypeError("{0} is not a valid JSON file.".format(string)) + else: + raise argparse.ArgumentTypeError("{0} is not a valid JSON file or string.".format(string)) + + # Django URL validation regex def url_string(string): import re @@ -137,6 +157,16 @@ def config(): default=False, help='Turn off camera parameter optimization during bundler') + parser.add_argument('--cameras', + default='', + metavar='', + type=path_or_json_string, + help='Use the camera parameters computed from ' + 'another dataset instead of calculating them. ' + 'Can be specified either as path to a cameras.json file or as a ' + 'JSON string representing the contents of a ' + 'cameras.json file. Default: %(default)s') + parser.add_argument('--max-concurrency', metavar='', default=context.num_cores, @@ -556,6 +586,8 @@ def config(): '%(default)s')) args = parser.parse_args() + print(args.cameras) + exit(1) # check that the project path setting has been set properly if not args.project_path: diff --git a/opendm/osfm.py b/opendm/osfm.py index 3f08c406..68e6caad 100644 --- a/opendm/osfm.py +++ b/opendm/osfm.py @@ -8,6 +8,7 @@ from opendm import io from opendm import log from opendm import system from opendm import context +from opendm import camera from opensfm.large import metadataset from opensfm.large import tools @@ -85,13 +86,14 @@ class OSFMContext: log.ODM_DEBUG("Copied image_groups.txt to OpenSfM directory") io.copy(image_groups_file, os.path.join(self.opensfm_project_path, "image_groups.txt")) - # check for cameras.json - # TODO: use config.cameras - camera_models_file = os.path.join(args.project_path, "cameras.json") - has_camera_calibration = io.file_exists(camera_models_file) - if has_camera_calibration: - log.ODM_DEBUG("Copied cameras.json to OpenSfM directory (camera_models_overrides.json)") - io.copy(camera_models_file, os.path.join(self.opensfm_project_path, "camera_models_overrides.json")) + # check for cameras + if args.cameras: + try: + with open(os.path.join(self.opensfm_project_path, "camera_models_overrides.json"), 'r') as f: + f.write(json.dumps(camera.get_opensfm_camera_models(args.cameras))) + log.ODM_DEBUG("Wrote camera_models_overrides.json to OpenSfM directory (camera_models_overrides.json)") + except Exception as e: + log.ODM_WARNING("Cannot set camera_models_overrides.json: %s" % str(e)) # create config file for OpenSfM config = [ @@ -196,30 +198,11 @@ class OSFMContext: def extract_cameras(self, output, rerun=False): reconstruction_file = self.path("reconstruction.json") if not os.path.exists(output) or rerun: - if os.path.exists(reconstruction_file): - result = {} - with open(reconstruction_file, 'r') as fin: - json_f = json.loads(fin.read()) - for recon in json_f: - if 'cameras' in recon: - for camera_id in recon['cameras']: - # Strip "v2" from OpenSfM camera IDs - new_camera_id = camera_id - if new_camera_id.startswith("v2 "): - new_camera_id = new_camera_id[3:] - - result[new_camera_id] = recon['cameras'][camera_id] - - # Remove "_prior" keys - keys = list(result[new_camera_id].keys()) - for k in keys: - if k.endswith('_prior'): - result[new_camera_id].pop(k) - + try: with open(output, 'w') as fout: - fout.write(json.dumps(result)) - else: - log.ODM_WARNING("Cannot export cameras to %s. reconstruction.json does not exist." % output) + fout.write(json.dumps(camera.get_cameras_from_opensfm(reconstruction_file))) + except Exception as e: + log.ODM_WARNING("Cannot export cameras to %s. %s." % (output, str(e))) else: log.ODM_INFO("Already extracted cameras") diff --git a/tests/assets/reconstruction.json b/tests/assets/reconstruction.json new file mode 100644 index 00000000..d547fef7 --- /dev/null +++ b/tests/assets/reconstruction.json @@ -0,0 +1,23 @@ +[ + { + "reference_lla": { + "latitude": 46.84265666666667, + "altitude": 0, + "longitude": -91.99400802469134 + }, + "cameras": { + "v2 dji fc300s 4000 2250 perspective 0.5555": { + "focal_prior": 0.5555555555555556, + "width": 4000, + "k1": 0.0052219633739416784, + "k2": 0.015202301516716735, + "k1_prior": 0.0, + "k2_prior": 0.0, + "projection_type": "perspective", + "focal": 0.5614307793062748, + "height": 2250 + } + }, + "shots": {} + } +] \ No newline at end of file diff --git a/tests/test_camera.py b/tests/test_camera.py new file mode 100644 index 00000000..fb0045f2 --- /dev/null +++ b/tests/test_camera.py @@ -0,0 +1,41 @@ +import time +import unittest +import os +import shutil + +from opendm import camera + + +class TestCamera(unittest.TestCase): + def setUp(self): + if os.path.exists("tests/assets/output"): + shutil.rmtree("tests/assets/output") + os.makedirs("tests/assets/output") + + def test_camera(self): + c = camera.get_cameras_from_opensfm("tests/assets/reconstruction.json") + self.assertEqual(len(c.keys()), 1) + camera_id = c.keys()[0] + self.assertTrue('v2 ' not in camera_id) + + self.assertRaises(RuntimeError, camera.get_cameras_from_opensfm, 'tests/assets/nonexistant.json') + self.assertRaises(ValueError, camera.get_cameras_from_opensfm, 'tests/assets/gcp_extras.txt') + self.assertFalse('k1_prior' in c[camera_id]) + + # Add bogus field + c[camera_id]['test'] = 0 + + osfm_c = camera.get_opensfm_camera_models(c) + self.assertEqual(len(osfm_c.keys()), 1) + c1 = osfm_c[osfm_c.keys()[0]] + self.assertTrue('k1_prior' in c1) + self.assertTrue('k2_prior' in c1) + self.assertFalse('test' in c1) + self.assertEqual(c1['k1'], c1['k1_prior']) + self.assertEqual(c1['k2'], c1['k2_prior']) + self.assertEqual(c1['focal'], c1['focal_prior']) + self.assertTrue('width_prior' not in c1) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file