def make_image_collection(length, **kwargs): images = [make_image(idx) for idx in range(length)] timestamps = [idx * 0.9 for idx in range(len(images))] for image in images: image.save() image_collection = ImageCollection( images=images, timestamps=timestamps, sequence_type=ImageSequenceType.SEQUENTIAL, **kwargs) image_collection.save() return image_collection
def setUpClass(cls): dbconn.setup_image_manager() cls.temp_folder.mkdir(parents=True, exist_ok=True) cls.path_manager = PathManager([Path(__file__).parent], cls.temp_folder) image_builder = DemoImageBuilder(mode=ImageMode.STEREO, stereo_offset=0.15, width=320, height=240, num_stars=500, length=cls.max_time * cls.speed, speed=cls.speed, min_size=4, max_size=50) # Make an image source from the image builder images = [] for time in range(cls.num_frames): image = image_builder.create_frame(time) images.append(image) cls.image_collection = ImageCollection( images=images, timestamps=list(range(len(images))), sequence_type=ImageSequenceType.SEQUENTIAL)
def setUp(self): self.path_manager = PathManager(['~'], '~/tmp') mock_importer.reset() image = mock_types.make_image() self.image_collection = ImageCollection( images=[image], timestamps=[1.2], sequence_type=ImageSequenceType.SEQUENTIAL)
def make_image_collection(length=3) -> ImageCollection: """ A quick helper for making and saving image collections :param length: :return: """ images = [] timestamps = [] for idx in range(length): image = mock_types.make_image() image.save() images.append(image) timestamps.append(idx * 0.9) image_collection = ImageCollection( images=images, timestamps=timestamps, sequence_type=ImageSequenceType.SEQUENTIAL) image_collection.save() return image_collection
def test_result_saves(self): # Make an image collection with some number of images images = [] image_builder = DemoImageBuilder(mode=ImageMode.STEREO, stereo_offset=0.15, width=160, height=120) num_images = 10 for time in range(num_images): image = image_builder.create_frame(time / num_images) image.save() images.append(image) image_collection = ImageCollection( images=images, timestamps=list(range(len(images))), sequence_type=ImageSequenceType.SEQUENTIAL) image_collection.save() subject = LibVisOStereoSystem() subject.save() # Actually run the system using mocked images subject.set_camera_intrinsics(image_builder.get_camera_intrinsics(), 1 / 10) subject.set_stereo_offset(image_builder.get_stereo_offset()) subject.start_trial(ImageSequenceType.SEQUENTIAL) for time, image in enumerate(images): subject.process_image(image, time) result = subject.finish_trial() self.assertIsInstance(result, SLAMTrialResult) self.assertEqual(len(image_collection), len(result.results)) result.image_source = image_collection result.save() # Load all the entities all_entities = list(SLAMTrialResult.objects.all()) self.assertGreaterEqual(len(all_entities), 1) self.assertEqual(all_entities[0], result) all_entities[0].delete() SLAMTrialResult._mongometa.collection.drop() ImageCollection._mongometa.collection.drop() StereoImage._mongometa.collection.drop()
def setUpClass(cls): dbconn.connect_to_test_db() dbconn.setup_image_manager() cls.image = mock_types.make_image() cls.image.save() cls.image_collection = ImageCollection( images=[cls.image], timestamps=[1.2], sequence_type=ImageSequenceType.SEQUENTIAL) cls.image_collection.save()
def make_image_sequence(timestep, width, height, length): images = [] for _ in range(length): pixels = np.random.randint(0, 255, size=(height, width, 3), dtype=np.uint8) image = Image( pixels=pixels, metadata=imeta.make_metadata( pixels, source_type=imeta.ImageSourceType.SYNTHETIC, intrinsics=CameraIntrinsics(800, 600, 550.2, 750.2, 400, 300), ), additional_metadata={'test': True} ) image.save() images.append(image) sequence = ImageCollection( images=images, timestamps=[idx * timestep for idx in range(length)], sequence_type=ImageSequenceType.SEQUENTIAL ) sequence.save() return sequence
def import_sequence(root_folder: Path, left_path: Path, right_path: Path, depth_quality: DepthNoiseQuality = DepthNoiseQuality.KINECT_NOISE) -> ImageCollection: """ Import the sequence, as a bunch of stereo images, and then organised into an ImageCollection. ImageCollection and StereoImage objects are saved. :param root_folder: The root folder to import from, containing the timestamps and settings files :param left_path: The path to the left image sequences :param right_path: The path to the right image sequences :param depth_quality: Noisy depth is generated as we go, the quality to use when doing so :return: The imported image collection """ # Read the timestamps and the generator settings # These are saved from python, so need for special loading with (root_folder / 'settings.json').open('r') as fp: settings = json_load(fp) with (root_folder / 'timestamps.json').open('r') as fp: timestamps = json_load(fp) # Read the camera settings from file left_camera_intrinsics = read_camera_intrinsics(left_path / '_camera_settings.json') right_camera_intrinsics = read_camera_intrinsics(right_path / '_camera_settings.json') # left_object_labels = read_object_classes(left_path / '_object_settings.json') # right_object_labels = read_object_classes(right_path / '_object_settings.json') max_img_id = min( find_max_img_id(lambda idx: left_path / IMG_TEMPLATE.format(idx)), find_max_img_id(lambda idx: right_path / IMG_TEMPLATE.format(idx)), ) if len(timestamps) != max_img_id + 1: raise RuntimeError(f"Maximum image id {max_img_id} didn't match the number " f"of available timestamps ({len(timestamps)}), cannot associate.") # Read meta-information from the generator, including the timestamps sequence_name = root_folder.name ( trajectory_id, environment_type, light_level, time_of_day, simulation_world, lighting_model, texture_mipmap_bias, normal_maps_enabled, roughness_enabled, min_object_size, geometry_decimation ) = parse_settings(settings) # Import all the images images = [] image_group = sequence_name origin = None # Open the image manager for writing once, so that we're not constantly opening and closing it with each image with arvet.database.image_manager.get().get_group(image_group, allow_write=True): for img_idx in range(max_img_id + 1): logging.getLogger(__name__).info(f"Loading image {img_idx}...") # Read the raw data for the left image left_frame_data = read_json(left_path / DATA_TEMPLATE.format(img_idx)) left_pixels = image_utils.read_colour(left_path / IMG_TEMPLATE.format(img_idx)) # left_label_image = image_utils.read_colour(left_path / INSTANCE_TEMPLATE.format(img_idx)) left_true_depth = load_depth_image(left_path / DEPTH_TEMPLATE.format(img_idx)) # Read the raw data for the right image right_frame_data = read_json(right_path / DATA_TEMPLATE.format(img_idx)) right_pixels = image_utils.read_colour(right_path / IMG_TEMPLATE.format(img_idx)) # right_label_image = image_utils.read_colour(right_path / INSTANCE_TEMPLATE.format(img_idx)) right_true_depth = load_depth_image(right_path / DEPTH_TEMPLATE.format(img_idx)) # Ensure all images are c_contiguous if not left_pixels.flags.c_contiguous: left_pixels = np.ascontiguousarray(left_pixels) # if not left_label_image.flags.c_contiguous: # left_label_image = np.ascontiguousarray(left_label_image) if not left_true_depth.flags.c_contiguous: left_true_depth = np.ascontiguousarray(left_true_depth) if not right_pixels.flags.c_contiguous: right_pixels = np.ascontiguousarray(right_pixels) # if not right_label_image.flags.c_contiguous: # right_label_image = np.ascontiguousarray(right_label_image) if not right_true_depth.flags.c_contiguous: right_true_depth = np.ascontiguousarray(right_true_depth) # Extract the poses left_camera_pose = read_camera_pose(left_frame_data) right_camera_pose = read_camera_pose(right_frame_data) # If we have object data, extract labels for them as well # Not working? removed # if len(left_object_labels) > 0: # left_labelled_objects = find_labelled_objects(left_label_image, left_frame_data, left_object_labels) # else: # left_labelled_objects = [] # if len(right_object_labels) > 0: # right_labelled_objects = find_labelled_objects(right_label_image, right_frame_data, right_object_labels) # else: # right_labelled_objects = [] left_labelled_objects = [] right_labelled_objects = [] # Compute a noisy depth image noisy_depth = create_noisy_depth_image( left_true_depth=left_true_depth, right_true_depth=right_true_depth, camera_intrinsics=left_camera_intrinsics, right_camera_relative_pose=left_camera_pose.find_relative(right_camera_pose), quality_level=depth_quality ) # Re-centre the camera poses relative to the first frame if origin is None: origin = left_camera_pose left_camera_pose = origin.find_relative(left_camera_pose) right_camera_pose = origin.find_relative(right_camera_pose) left_metadata = imeta.make_metadata( pixels=left_pixels, depth=left_true_depth, camera_pose=left_camera_pose, intrinsics=left_camera_intrinsics, source_type=imeta.ImageSourceType.SYNTHETIC, environment_type=environment_type, light_level=light_level, time_of_day=time_of_day, simulation_world=simulation_world, lighting_model=lighting_model, texture_mipmap_bias=texture_mipmap_bias, normal_maps_enabled=normal_maps_enabled, roughness_enabled=roughness_enabled, geometry_decimation=geometry_decimation, minimum_object_volume=min_object_size, labelled_objects=left_labelled_objects ) right_metadata = imeta.make_right_metadata( pixels=right_pixels, depth=right_true_depth, camera_pose=right_camera_pose, intrinsics=right_camera_intrinsics, labelled_objects=right_labelled_objects, left_metadata=left_metadata ) image = StereoImage( pixels=left_pixels, right_pixels=right_pixels, depth=noisy_depth, true_depth=left_true_depth, right_true_depth=right_true_depth, image_group=image_group, metadata=left_metadata, right_metadata=right_metadata, ) try: image.save() except KeyError as exp: logging.getLogger(__name__).info(f"Key error while saving image {img_idx} in sequence {sequence_name}, " f"read from {left_path / IMG_TEMPLATE.format(img_idx)}") raise exp images.append(image) # Create and save the image collection collection = ImageCollection( images=images, image_group=sequence_name, timestamps=timestamps, sequence_type=ImageSequenceType.SEQUENTIAL, dataset="generated-SLAM-data", sequence_name=sequence_name, trajectory_id=trajectory_id ) collection.save() return collection
def verify_sequence(image_collection: ImageCollection, root_folder: Path) -> bool: """ Load a dataset produced by the Nvidia dataset generator :return: """ root_folder = Path(root_folder) # depth_quality = DepthNoiseQuality(depth_quality) valid = True sequence_name = image_collection.sequence_name # Step 0: Check the root folder to see if it needs to be extracted from a tarfile sequence_folder = root_folder / sequence_name delete_when_done = None if not sequence_folder.is_dir(): sequence_tarfile = root_folder / (sequence_name + '.tar.gz') if sequence_tarfile.is_file() and tarfile.is_tarfile(sequence_tarfile): logging.getLogger(__name__).info( f"Extracting sequence from {sequence_tarfile}") delete_when_done = sequence_folder with tarfile.open(sequence_tarfile) as tar_fp: tar_fp.extractall(sequence_folder) else: # Could find neither a dir nor a tarfile to extract from raise NotADirectoryError( f"Neither {sequence_folder} nor {sequence_tarfile} exists for us to extract" ) sequence_folder, left_path, right_path = ndds_loader.find_files( sequence_folder) logging.getLogger(__name__).info( f"{sequence_name}: Selected {sequence_folder}, {left_path}, {right_path} as source folders" ) # Read the maximum image id, and make sure it matches the collection max_img_id = min( ndds_loader.find_max_img_id( lambda idx: left_path / ndds_loader.IMG_TEMPLATE.format(idx)), ndds_loader.find_max_img_id( lambda idx: right_path / ndds_loader.IMG_TEMPLATE.format(idx)), ) if len(image_collection) != max_img_id + 1: logging.getLogger(__name__).warning( f"Maximum image id {max_img_id} did not match the length of the " f"image collection ({len(image_collection)})") # make sure we don't try and get images from the collection it doesn't have max_img_id = min(max_img_id, len(image_collection) - 1) valid = False # Verify the images # Open the image manager for writing once, so that we're not constantly opening and closing it with each image total_invalid_images = 0 with arvet.database.image_manager.get().get_group( image_collection.get_image_group()): for img_idx in range(max_img_id + 1): img_valid = True # Expand the file paths for this image left_img_path = left_path / ndds_loader.IMG_TEMPLATE.format( img_idx) left_depth_path = left_path / ndds_loader.DEPTH_TEMPLATE.format( img_idx) right_img_path = right_path / ndds_loader.IMG_TEMPLATE.format( img_idx) right_depth_path = right_path / ndds_loader.DEPTH_TEMPLATE.format( img_idx) # Read the raw data for the left image left_pixels = image_utils.read_colour(left_img_path) left_true_depth = ndds_loader.load_depth_image(left_depth_path) # Read the raw data for the right image right_pixels = image_utils.read_colour(right_img_path) right_true_depth = ndds_loader.load_depth_image(right_depth_path) # Ensure all images are c_contiguous if not left_pixels.flags.c_contiguous: left_pixels = np.ascontiguousarray(left_pixels) if not left_true_depth.flags.c_contiguous: left_true_depth = np.ascontiguousarray(left_true_depth) if not right_pixels.flags.c_contiguous: right_pixels = np.ascontiguousarray(right_pixels) if not right_true_depth.flags.c_contiguous: right_true_depth = np.ascontiguousarray(right_true_depth) # Compute image hashes left_hash = bytes(xxhash.xxh64(left_pixels).digest()) right_hash = bytes(xxhash.xxh64(right_pixels).digest()) # Compute a noisy depth image # noisy_depth = create_noisy_depth_image( # left_ground_truth_depth=left_ground_truth_depth, # right_ground_truth_depth=right_ground_truth_depth, # camera_intrinsics=left_camera_intrinsics, # right_camera_relative_pose=left_camera_pose.find_relative(right_camera_pose), # quality_level=depth_quality # ) # Load the image from the database try: _, image = image_collection[img_idx] left_actual_pixels = image.left_pixels left_actual_ground_truth_depth = image.left_true_depth right_actual_pixels = image.right_pixels right_actual_ground_truth_depth = image.right_true_depth except (KeyError, IOError, RuntimeError): logging.getLogger(__name__).exception( f"Error loading image {img_idx}") valid = False continue # Compare the loaded image data to the data read from disk if not np.array_equal(left_pixels, left_actual_pixels): logging.getLogger(__name__).error( f"Image {img_idx}: Left pixels do not match data read from {left_img_path}" ) total_invalid_images += 1 valid = False if left_hash != bytes(image.metadata.img_hash): logging.getLogger(__name__).error( f"Image {img_idx}: Left hash does not match metadata {image.metadata.img_hash}" ) valid = False img_valid = False if not np.array_equal(left_true_depth, left_actual_ground_truth_depth): logging.getLogger(__name__).error( f"Image {img_idx}: Left depth does not match data read from {left_depth_path}" ) valid = False img_valid = False if not np.array_equal(right_pixels, right_actual_pixels): logging.getLogger(__name__).error( f"Image {img_idx}: Right pixels do not match data read from {right_img_path}" ) valid = False img_valid = False if right_hash != bytes(image.right_metadata.img_hash): logging.getLogger(__name__).error( f"Image {img_idx}: Right hash does not match metadata {image.right_metadata.img_hash}" ) valid = False img_valid = False if not np.array_equal(right_true_depth, right_actual_ground_truth_depth): logging.getLogger(__name__).error( f"Image {img_idx}: Right depth does not match data read from {right_depth_path}" ) valid = False img_valid = False if not img_valid: total_invalid_images += 1 if delete_when_done is not None and delete_when_done.exists(): # We're done and need to clean up after ourselves shutil.rmtree(delete_when_done) if valid: logging.getLogger(__name__).info( f"Verification of {sequence_name} successful.") else: logging.getLogger(__name__).info( f"Verification of {sequence_name} ({image_collection.pk}) " f"FAILED, ({total_invalid_images} images failed)") return valid
def import_dataset(root_folder, sequence_number, **_): """ Load a KITTI image sequences into the database. :return: """ sequence_number = int(sequence_number) if not 0 <= sequence_number < 11: raise ValueError("Cannot import sequence {0}, it is invalid".format(sequence_number)) root_folder = find_root(root_folder, sequence_number) data = pykitti.odometry(root_folder, sequence="{0:02}".format(sequence_number)) # dataset.calib: Calibration data are accessible as a named tuple # dataset.timestamps: Timestamps are parsed into a list of timedelta objects # dataset.poses: Generator to load ground truth poses T_w_cam0 # dataset.camN: Generator to load individual images from camera N # dataset.gray: Generator to load monochrome stereo pairs (cam0, cam1) # dataset.rgb: Generator to load RGB stereo pairs (cam2, cam3) # dataset.velo: Generator to load velodyne scans as [x,y,z,reflectance] image_group = f"KITTI_{sequence_number:06}" images = [] timestamps = [] with arvet.database.image_manager.get().get_group(image_group, allow_write=True): for left_image, right_image, timestamp, pose in zip(data.cam2, data.cam3, data.timestamps, data.poses): left_image = np.array(left_image) right_image = np.array(right_image) camera_pose = make_camera_pose(pose) # camera pose is for cam0, we want cam2, which is 6cm (0.06m) to the left # Except that we don't need to control for that, since we want to be relative to the first pose anyway # camera_pose = camera_pose.find_independent(tf.Transform(location=(0, 0.06, 0), rotation=(0, 0, 0, 1), # w_first=False)) # Stereo offset is 0.54m (http://www.cvlibs.net/datasets/kitti/setup.php) right_camera_pose = camera_pose.find_independent(Transform(location=(0, -0.54, 0), rotation=(0, 0, 0, 1), w_first=False)) camera_intrinsics = CameraIntrinsics( height=left_image.shape[0], width=left_image.shape[1], fx=data.calib.K_cam2[0, 0], fy=data.calib.K_cam2[1, 1], cx=data.calib.K_cam2[0, 2], cy=data.calib.K_cam2[1, 2]) right_camera_intrinsics = CameraIntrinsics( height=right_image.shape[0], width=right_image.shape[1], fx=data.calib.K_cam3[0, 0], fy=data.calib.K_cam3[1, 1], cx=data.calib.K_cam3[0, 2], cy=data.calib.K_cam3[1, 2]) left_metadata = imeta.make_metadata( pixels=left_image, camera_pose=camera_pose, intrinsics=camera_intrinsics, source_type=imeta.ImageSourceType.REAL_WORLD, environment_type=imeta.EnvironmentType.OUTDOOR_URBAN, light_level=imeta.LightingLevel.WELL_LIT, time_of_day=imeta.TimeOfDay.AFTERNOON, ) right_metadata = imeta.make_right_metadata( pixels=right_image, left_metadata=left_metadata, camera_pose=right_camera_pose, intrinsics=right_camera_intrinsics ) image = StereoImage( pixels=left_image, right_pixels=right_image, image_group=image_group, metadata=left_metadata, right_metadata=right_metadata ) image.save() images.append(image) timestamps.append(timestamp.total_seconds()) # Create and save the image collection collection = ImageCollection( images=images, timestamps=timestamps, sequence_type=ImageSequenceType.SEQUENTIAL, dataset='KITTI', sequence_name="{0:02}".format(sequence_number), trajectory_id="KITTI_{0:02}".format(sequence_number) ) collection.save() return collection
def import_dataset(root_folder, dataset_name, **_): """ Load a TUM RGB-D sequence into the database. :return: """ root_folder = Path(root_folder) # Step 0: Check the root folder to see if it needs to be extracted from a tarfile delete_when_done = None if not root_folder.is_dir(): if (root_folder.parent / dataset_name).is_dir(): # The root was a tarball, but the extracted data already exists, just use that as the root root_folder = root_folder.parent / dataset_name else: candidate_tar_file = root_folder.parent / (dataset_name + '.tgz') if candidate_tar_file.is_file() and tarfile.is_tarfile( candidate_tar_file): # Root is actually a tarfile, extract it. find_roots with handle folder structures with tarfile.open(candidate_tar_file) as tar_fp: tar_fp.extractall(root_folder.parent / dataset_name) root_folder = root_folder.parent / dataset_name delete_when_done = root_folder else: # Could find neither a dir nor a tarfile to extract from raise NotADirectoryError( "'{0}' is not a directory".format(root_folder)) # Step 1: Find the relevant metadata files root_folder, rgb_path, depth_path, trajectory_path = find_files( root_folder) # Step 2: Read the metadata from them image_files = read_image_filenames(rgb_path) trajectory = read_trajectory(trajectory_path, image_files.keys()) depth_files = read_image_filenames(depth_path) # Step 3: Associate the different data types by timestamp all_metadata = associate_data(image_files, trajectory, depth_files) # Step 3: Load the images from the metadata first_timestamp = None image_group = dataset_name images = [] timestamps = [] with arvet.database.image_manager.get().get_group(image_group, allow_write=True): for timestamp, image_file, camera_pose, depth_file in all_metadata: # Re-zero the timestamps if first_timestamp is None: first_timestamp = timestamp timestamp = (timestamp - first_timestamp) rgb_data = image_utils.read_colour( os.path.join(root_folder, image_file)) depth_data = image_utils.read_depth( os.path.join(root_folder, depth_file)) depth_data = depth_data / 5000 # Re-scale depth to meters camera_intrinsics = get_camera_intrinsics(root_folder) metadata = imeta.make_metadata( pixels=rgb_data, depth=depth_data, camera_pose=camera_pose, intrinsics=camera_intrinsics, source_type=imeta.ImageSourceType.REAL_WORLD, environment_type=environment_types.get( dataset_name, imeta.EnvironmentType.INDOOR_CLOSE), light_level=imeta.LightingLevel.WELL_LIT, time_of_day=imeta.TimeOfDay.DAY, ) image = Image(pixels=rgb_data, depth=depth_data, image_group=image_group, metadata=metadata) image.save() images.append(image) timestamps.append(timestamp) # Create and save the image collection collection = ImageCollection(images=images, timestamps=timestamps, sequence_type=ImageSequenceType.SEQUENTIAL, dataset='TUM RGB-D', sequence_name=dataset_name, trajectory_id=dataset_name) collection.save() if delete_when_done is not None and delete_when_done.exists(): # We're done and need to clean up after ourselves shutil.rmtree(delete_when_done) return collection
def verify_dataset(image_collection: ImageCollection, root_folder: typing.Union[str, Path], dataset_name: str, repair: bool = False): """ Load a TUM RGB-D sequence into the database. :return: """ root_folder = Path(root_folder) dataset_name = str(dataset_name) repair = bool(repair) valid = True irreparable = False image_group = dataset_name # Check the root folder to see if it needs to be extracted from a tarfile delete_when_done = None if not root_folder.is_dir(): if (root_folder.parent / dataset_name).is_dir(): # The root was a tarball, but the extracted data already exists, just use that as the root root_folder = root_folder.parent / dataset_name else: candidate_tar_file = root_folder.parent / (dataset_name + '.tgz') if candidate_tar_file.is_file() and tarfile.is_tarfile( candidate_tar_file): # Root is actually a tarfile, extract it. find_roots with handle folder structures with tarfile.open(candidate_tar_file) as tar_fp: tar_fp.extractall(root_folder.parent / dataset_name) root_folder = root_folder.parent / dataset_name delete_when_done = root_folder else: # Could find neither a dir nor a tarfile to extract from raise NotADirectoryError( "'{0}' is not a directory".format(root_folder)) # Check the image group on the image collection if image_collection.image_group != image_group: if repair: image_collection.image_group = image_group image_collection.save() logging.getLogger(__name__).info( f"Fixed incorrect image group for {image_collection.sequence_name}" ) else: logging.getLogger(__name__).warning( f"{image_collection.sequence_name} has incorrect image group {image_collection.image_group}" ) valid = False # Find the relevant metadata files root_folder, rgb_path, depth_path, trajectory_path = tum_loader.find_files( root_folder) # Step 2: Read the metadata from them image_files = tum_loader.read_image_filenames(rgb_path) trajectory = tum_loader.read_trajectory(trajectory_path, image_files.keys()) depth_files = tum_loader.read_image_filenames(depth_path) # Step 3: Associate the different data types by timestamp all_metadata = tum_loader.associate_data(image_files, trajectory, depth_files) # Step 3: Load the images from the metadata total_invalid_images = 0 total_fixed_images = 0 with arvet.database.image_manager.get().get_group(image_group, allow_write=repair): for img_idx, (timestamp, image_file, camera_pose, depth_file) in enumerate(all_metadata): changed = False img_valid = True img_path = root_folder / image_file depth_path = root_folder / depth_file rgb_data = image_utils.read_colour(img_path) depth_data = image_utils.read_depth(depth_path) depth_data = depth_data / 5000 # Re-scale depth to meters img_hash = bytes(xxhash.xxh64(rgb_data).digest()) # Load the image from the database try: _, image = image_collection[img_idx] except (KeyError, IOError, RuntimeError): logging.getLogger(__name__).exception( f"Error loading image object {img_idx}") valid = False total_invalid_images += 1 continue # First, check the image group if image.image_group != image_group: if repair: image.image_group = image_group changed = True logging.getLogger(__name__).warning( f"Image {img_idx} has incorrect group {image.image_group}") valid = False img_valid = False # Load the pixels from the image try: actual_pixels = image.pixels except (KeyError, IOError, RuntimeError): actual_pixels = None try: actual_depth = image.depth except (KeyError, IOError, RuntimeError): actual_depth = None # Compare the loaded image data to the data read from disk if actual_pixels is None or not np.array_equal( rgb_data, actual_pixels): if repair: image.store_pixels(rgb_data) changed = True else: logging.getLogger(__name__).error( f"Image {img_idx}: Pixels do not match data read from {img_path}" ) valid = False img_valid = False if img_hash != bytes(image.metadata.img_hash): if repair: image.metadata.img_hash = img_hash changed = True else: logging.getLogger(__name__).error( f"Image {img_idx}: Image hash does not match metadata") valid = False img_valid = False if actual_depth is None or not np.array_equal( depth_data, actual_depth): if repair: image.store_depth(depth_data) changed = True else: logging.getLogger(__name__).error( f"Image {img_idx}: Depth does not match data read from {depth_path}" ) valid = False img_valid = False if changed and repair: logging.getLogger(__name__).warning( f"Image {img_idx}: repaired") image.save() total_fixed_images += 1 if not img_valid: total_invalid_images += 1 if irreparable: # Images are missing entirely, needs re-import logging.getLogger(__name__).error( f"Image Collection {image_collection.pk} for sequence {dataset_name} " "is IRREPARABLE, invalidate and re-import") elif repair: # Re-save the modified image collection logging.getLogger(__name__).info( f"{image_collection.sequence_name} repaired successfully " f"({total_fixed_images} image files fixed).") elif valid: logging.getLogger(__name__).info( f"Verification of {image_collection.sequence_name} successful.") else: logging.getLogger(__name__).error( f"Verification of {image_collection.sequence_name} ({image_collection.pk}) " f"FAILED, ({total_invalid_images} images failed)") if delete_when_done is not None and delete_when_done.exists(): # We're done and need to clean up after ourselves shutil.rmtree(delete_when_done) return valid
def verify_dataset(image_collection: ImageCollection, root_folder: typing.Union[str, PurePath], dataset_name: str, repair: bool = False): """ Examine an existing Autonomous Systems Lab dataset in the database, and check if for errors See http://projects.asl.ethz.ch/datasets/doku.php?id=kmavvisualinertialdatasets#downloads Some information drawn from the ethz_asl dataset tools, see: https://github.com/ethz-asl/dataset_tools :param image_collection: The existing image collection from the loader :param root_folder: The body folder, containing body.yaml (i.e. the extracted mav0 folder) :param dataset_name: The name of the dataset, see the manager for the list of valid values. :param repair: If possible, fix missing images in the dataset :return: """ root_folder = Path(root_folder) dataset_name = str(dataset_name) repair = bool(repair) if not root_folder.is_dir(): raise NotADirectoryError( "'{0}' is not a directory".format(root_folder)) image_group = dataset_name valid = True irreparable = False # Check the image group on the image collection if image_collection.image_group != image_group: if repair: image_collection.image_group = image_group image_collection.save() logging.getLogger(__name__).info( f"Fixed incorrect image group for {image_collection.sequence_name}" ) else: logging.getLogger(__name__).warning( f"{image_collection.sequence_name} has incorrect image group {image_group}" ) valid = False # Find the various files containing the data (that's a 6-element tuple unpack for the return value) (root_folder, left_rgb_path, left_camera_intrinsics_path, right_rgb_path, right_camera_intrinsics_path, trajectory_path) = euroc_loader.find_files(root_folder) # Read the meta-information from the files (that's a 6-element tuple unpack for the return value) left_image_files = euroc_loader.read_image_filenames(left_rgb_path) left_extrinsics, left_intrinsics = euroc_loader.get_camera_calibration( left_camera_intrinsics_path) right_image_files = euroc_loader.read_image_filenames(left_rgb_path) right_extrinsics, right_intrinsics = euroc_loader.get_camera_calibration( right_camera_intrinsics_path) # Create stereo rectification matrices from the intrinsics left_x, left_y, left_intrinsics, right_x, right_y, right_intrinsics = euroc_loader.rectify( left_extrinsics, left_intrinsics, right_extrinsics, right_intrinsics) # Associate the different data types by timestamp. Trajectory last because it's bigger than the stereo. all_metadata = euroc_loader.associate_data(left_image_files, right_image_files) # Load the images from the metadata total_invalid_images = 0 total_fixed_images = 0 image_index = 0 with arvet.database.image_manager.get().get_group(image_group, allow_write=repair): for timestamp, left_image_file, right_image_file in all_metadata: changed = False img_valid = True # Skip if we've hit the end of the data if image_index >= len(image_collection): logging.getLogger(__name__).error( f"Image {image_index} is missing from the dataset") irreparable = True valid = False total_invalid_images += 1 continue left_img_path = root_folder / 'cam0' / 'data' / left_image_file right_img_path = root_folder / 'cam1' / 'data' / right_image_file left_pixels = image_utils.read_colour(left_img_path) right_pixels = image_utils.read_colour(right_img_path) # Error check the loaded image data # The EuRoC Sequences MH_04_difficult and V2_03_difficult are missing the first right frame # So we actually start loading from # In general, frames that are missing are skipped, and do not increment image index if left_pixels is None or left_pixels.size is 0: logging.getLogger(__name__).warning( f"Could not read left image \"{left_img_path}\", result is empty. Image is skipped." ) continue if right_pixels is None or right_pixels.size is 0: logging.getLogger(__name__).warning( f"Could not read right image \"{right_img_path}\", result is empty. Image is skipped." ) continue left_pixels = cv2.remap(left_pixels, left_x, left_y, cv2.INTER_LINEAR) right_pixels = cv2.remap(right_pixels, right_x, right_y, cv2.INTER_LINEAR) left_hash = bytes(xxhash.xxh64(left_pixels).digest()) right_hash = bytes(xxhash.xxh64(right_pixels).digest()) # Load the image from the database try: _, image = image_collection[image_index] except (KeyError, IOError, RuntimeError): logging.getLogger(__name__).exception( f"Error loading image object {image_index}") valid = False image_index += 1 # Index is valid, increment when done continue # First, check the image group if image.image_group != image_group: if repair: image.image_group = image_group changed = True logging.getLogger(__name__).warning( f"Image {image_index} has incorrect group {image.image_group}" ) valid = False img_valid = False # Load the pixels from the image try: left_actual_pixels = image.left_pixels except (KeyError, IOError, RuntimeError): left_actual_pixels = None try: right_actual_pixels = image.right_pixels except (KeyError, IOError, RuntimeError): right_actual_pixels = None # Compare the loaded image data to the data read from disk if left_actual_pixels is None or not np.array_equal( left_pixels, left_actual_pixels): if repair: image.store_pixels(left_pixels) changed = True else: logging.getLogger(__name__).error( f"Image {image_index}: Left pixels do not match data read from {left_img_path}" ) img_valid = False valid = False if left_hash != bytes(image.metadata.img_hash): if repair: image.metadata.img_hash = left_hash changed = True else: logging.getLogger(__name__).error( f"Image {image_index}: Left hash does not match metadata" ) valid = False img_valid = False if right_actual_pixels is None or not np.array_equal( right_pixels, right_actual_pixels): if repair: image.store_right_pixels(right_pixels) changed = True else: logging.getLogger(__name__).error( f"Image {image_index}: Right pixels do not match data read from {right_img_path}" ) valid = False img_valid = False if right_hash != bytes(image.right_metadata.img_hash): if repair: image.right_metadata.img_hash = right_hash changed = True else: logging.getLogger(__name__).error( f"Image {image_index}: Right hash does not match metadata" ) valid = False img_valid = False if changed and repair: logging.getLogger(__name__).warning( f"Image {image_index}: repaired") image.save() total_fixed_images += 1 if not img_valid: total_invalid_images += 1 image_index += 1 if irreparable: # Images are missing entirely, needs re-import logging.getLogger(__name__).error( f"Image Collection {image_collection.pk} for sequence {dataset_name} " "is IRREPARABLE, invalidate and re-import") elif repair: # Re-save the modified image collection logging.getLogger(__name__).info( f"{image_collection.sequence_name} repaired successfully " f"({total_fixed_images} image files fixed).") elif valid: logging.getLogger(__name__).info( f"Verification of {image_collection.sequence_name} successful.") else: logging.getLogger(__name__).error( f"Verification of {image_collection.sequence_name} ({image_collection.pk}) " f"FAILED, ({total_invalid_images} images failed)") return valid
def verify_dataset(image_collection: ImageCollection, root_folder: typing.Union[str, Path], sequence_number: int, repair: bool = False): """ Load a KITTI image sequences into the database. :return: """ root_folder = Path(root_folder) sequence_number = int(sequence_number) repair = bool(repair) if not 0 <= sequence_number < 11: raise ValueError("Cannot import sequence {0}, it is invalid".format(sequence_number)) root_folder = kitti_loader.find_root(root_folder, sequence_number) data = pykitti.odometry(root_folder, sequence="{0:02}".format(sequence_number)) image_group = f"KITTI_{sequence_number:06}" valid = True irreparable = False # Check the Image Collection if image_collection.image_group != image_group: if repair: image_collection.image_group = image_group image_collection.save() logging.getLogger(__name__).info( f"Fixed incorrect image group for {image_collection.sequence_name}") else: logging.getLogger(__name__).warning( f"{image_collection.sequence_name} has incorrect image group {image_collection.image_group}") valid = False # dataset.calib: Calibration data are accessible as a named tuple # dataset.timestamps: Timestamps are parsed into a list of timedelta objects # dataset.poses: Generator to load ground truth poses T_w_cam0 # dataset.camN: Generator to load individual images from camera N # dataset.gray: Generator to load monochrome stereo pairs (cam0, cam1) # dataset.rgb: Generator to load RGB stereo pairs (cam2, cam3) # dataset.velo: Generator to load velodyne scans as [x,y,z,reflectance] total_invalid_images = 0 total_fixed_images = 0 with arvet.database.image_manager.get().get_group(image_group, allow_write=repair): for img_idx, (left_image, right_image, timestamp, pose) in enumerate( zip(data.cam2, data.cam3, data.timestamps, data.poses)): changed = False img_valid = True if img_idx >= len(image_collection): logging.getLogger(__name__).error(f"Image {img_idx} is missing from the dataset") irreparable = True valid = False continue left_image = np.array(left_image) right_image = np.array(right_image) left_hash = bytes(xxhash.xxh64(left_image).digest()) right_hash = bytes(xxhash.xxh64(right_image).digest()) # Load the image object from the database try: _, image = image_collection[img_idx] except (KeyError, IOError, RuntimeError): logging.getLogger(__name__).exception(f"Error loading image object {img_idx}") valid = False total_invalid_images += 1 continue # First, check the image group if image.image_group != image_group: if repair: image.image_group = image_group changed = True logging.getLogger(__name__).warning(f"Image {img_idx} has incorrect group {image.image_group}") valid = False # Load the pixels from the image try: left_actual_pixels = image.left_pixels except (KeyError, IOError, RuntimeError): left_actual_pixels = None try: right_actual_pixels = image.right_pixels except (KeyError, IOError, RuntimeError): right_actual_pixels = None # Compare the loaded image data to the data read from disk if left_actual_pixels is None or not np.array_equal(left_image, left_actual_pixels): if repair: image.store_pixels(left_image) changed = True else: logging.getLogger(__name__).error(f"Image {img_idx}: Left pixels do not match data read from disk") valid = False img_valid = False if left_hash != bytes(image.metadata.img_hash): if repair: image.metadata.img_hash = left_hash changed = True else: logging.getLogger(__name__).error(f"Image {img_idx}: Left hash does not match metadata") valid = False img_valid = False if right_actual_pixels is None or not np.array_equal(right_image, right_actual_pixels): if repair: image.store_right_pixels(right_image) changed = True else: logging.getLogger(__name__).error(f"Image {img_idx}: Right pixels do not match data read from disk") valid = False img_valid = False if right_hash != bytes(image.right_metadata.img_hash): if repair: image.right_metadata.img_hash = right_hash changed = True else: logging.getLogger(__name__).error(f"Image {img_idx}: Right hash does not match metadata") valid = False img_valid = False if changed and repair: logging.getLogger(__name__).warning(f"Image {img_idx}: repaired") image.save() total_fixed_images += 1 if not img_valid: total_invalid_images += 1 if irreparable: # Images are missing entirely, needs re-import logging.getLogger(__name__).error(f"Image Collection {image_collection.pk} for sequence " f"{image_collection.sequence_name} is IRREPARABLE, invalidate and re-import") elif repair: # Re-save the modified image collection logging.getLogger(__name__).info(f"{image_collection.sequence_name} repaired successfully " f"({total_fixed_images} image files fixed).") elif valid: logging.getLogger(__name__).info(f"Verification of {image_collection.sequence_name} successful.") else: logging.getLogger(__name__).error( f"Verification of {image_collection.sequence_name} ({image_collection.pk}) " f"FAILED, ({total_invalid_images} images failed)") return valid
def import_dataset(root_folder, dataset_name, **_): """ Load an Autonomous Systems Lab dataset into the database. See http://projects.asl.ethz.ch/datasets/doku.php?id=kmavvisualinertialdatasets#downloads Some information drawn from the ethz_asl dataset tools, see: https://github.com/ethz-asl/dataset_tools :param root_folder: The body folder, containing body.yaml (i.e. the extracted mav0 folder) :param dataset_name: The name of the dataset, see the manager for the list of valid values. :return: """ if not os.path.isdir(root_folder): raise NotADirectoryError( "'{0}' is not a directory".format(root_folder)) # Step 1: Find the various files containing the data (that's a 6-element tuple unpack for the return value) (root_folder, left_rgb_path, left_camera_intrinsics_path, right_rgb_path, right_camera_intrinsics_path, trajectory_path) = find_files(root_folder) # Step 2: Read the meta-information from the files (that's a 6-element tuple unpack for the return value) left_image_files = read_image_filenames(left_rgb_path) left_extrinsics, left_intrinsics = get_camera_calibration( left_camera_intrinsics_path) right_image_files = read_image_filenames(left_rgb_path) right_extrinsics, right_intrinsics = get_camera_calibration( right_camera_intrinsics_path) trajectory = read_trajectory(trajectory_path, left_image_files.keys()) # Step 3: Create stereo rectification matrices from the intrinsics left_x, left_y, left_intrinsics, right_x, right_y, right_intrinsics = rectify( left_extrinsics, left_intrinsics, right_extrinsics, right_intrinsics) # Change the coordinates correctly on the extrinsics. Has to happen after rectification left_extrinsics = fix_coordinates(left_extrinsics) right_extrinsics = fix_coordinates(right_extrinsics) # Step 4: Associate the different data types by timestamp. Trajectory last because it's bigger than the stereo. all_metadata = associate_data(left_image_files, right_image_files, trajectory) # Step 5: Load the images from the metadata first_timestamp = None image_group = dataset_name images = [] timestamps = [] with arvet.database.image_manager.get().get_group(image_group, allow_write=True): for timestamp, left_image_file, right_image_file, robot_pose in all_metadata: # Timestamps are in POSIX nanoseconds, re-zero them to the start of the dataset, and scale to seconds if first_timestamp is None: first_timestamp = timestamp timestamp = (timestamp - first_timestamp) / 1e9 left_data = image_utils.read_colour( os.path.join(root_folder, 'cam0', 'data', left_image_file)) right_data = image_utils.read_colour( os.path.join(root_folder, 'cam1', 'data', right_image_file)) # Error check the loaded image data if left_data is None or left_data.size is 0: logging.getLogger(__name__).warning( "Could not read left image \"{0}\", result is empty. Skipping." .format( os.path.join(root_folder, 'cam0', 'data', left_image_file))) continue if right_data is None or right_data.size is 0: logging.getLogger(__name__).warning( "Could not read right image \"{0}\", result is empty. Skipping." .format( os.path.join(root_folder, 'cam1', 'data', right_image_file))) continue left_data = cv2.remap(left_data, left_x, left_y, cv2.INTER_LINEAR) right_data = cv2.remap(right_data, right_x, right_y, cv2.INTER_LINEAR) left_pose = robot_pose.find_independent(left_extrinsics) right_pose = robot_pose.find_independent(right_extrinsics) left_metadata = imeta.make_metadata( pixels=left_data, camera_pose=left_pose, intrinsics=left_intrinsics, source_type=imeta.ImageSourceType.REAL_WORLD, environment_type=environment_types.get( dataset_name, imeta.EnvironmentType.INDOOR_CLOSE), light_level=imeta.LightingLevel.WELL_LIT, time_of_day=imeta.TimeOfDay.DAY, ) right_metadata = imeta.make_right_metadata( pixels=right_data, left_metadata=left_metadata, camera_pose=right_pose, intrinsics=right_intrinsics) image = StereoImage(pixels=left_data, right_pixels=right_data, image_group=image_group, metadata=left_metadata, right_metadata=right_metadata) image.save() images.append(image) timestamps.append(timestamp) # Create and save the image collection collection = ImageCollection(images=images, timestamps=timestamps, sequence_type=ImageSequenceType.SEQUENTIAL, dataset='EuRoC MAV', sequence_name=dataset_name, trajectory_id=dataset_name) collection.save() return collection