Repository: naver/kapture (language: python)

Issue:
Fix bug in the kapture_import_bundler.py
Fix the data precision. Otherwise the np.array generates np.float64 array by default, which leads to completely wrongly read data.

 An example:

keypoints of "IMG_9614.MOV_frame000100.png" as numpy array of float32 and shape (5304, 2):

> [[ 0.00000000e+00  4.25390625e+00]
>  [-1.07374184e+08  4.08701134e+00]
>  [ 0.00000000e+00  4.62646484e+00]
>  ...
>  [ 0.00000000e+00  3.49609375e+00]
>  [ 0.00000000e+00  4.24218750e+00]
>  [ 0.00000000e+00  3.73046875e+00]]

After modification:

> array([1102.,  117., 1213., ...,  720., 1195.,  710.], dtype=float32)


Discussion / hints:

Fix patch:
diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc
index 06dcfe6..82fc7b9 100644
--- a/CONTRIBUTING.adoc
+++ b/CONTRIBUTING.adoc
@@ -16,7 +16,7 @@ There are many ways to contribute to the kapture project:
 * provide scripts to create data in kapture format (e.g. local/global feature extraction)
 * propose a new feature and implement it
 
-We're happy to help you make kapture better, so please mailto:kapture@naverlabs.com[contact us] for additional information.
+We're happy to help you make kapture better, so please mail us at kapture at naverlabs+com for additional information.
 
 The rest of the document concerns software contributions.
 
diff --git a/README.adoc b/README.adoc
index 9c172b1..bd09775 100644
--- a/README.adoc
+++ b/README.adoc
@@ -245,4 +245,4 @@ If you wish to contribute, please refer to the  link:CONTRIBUTING.adoc[CONTRIBUT
 Software license is detailed in the link:LICENSE[LICENSE] file.
 
 == Contact Us
-You can contact us through https://github.com/naver/kapture[GitHub], or at kapture@naverlabs.com
+You can contact us through https://github.com/naver/kapture[GitHub], or at kapture at naverlabs + com
diff --git a/kapture/converter/colmap/database.py b/kapture/converter/colmap/database.py
index f1b7bec..ffaa6cd 100755
--- a/kapture/converter/colmap/database.py
+++ b/kapture/converter/colmap/database.py
@@ -204,7 +204,9 @@ def add_matches(self, image_id1, image_id2, matches):
             (pair_id,) + matches.shape + (array_to_blob(matches),))
 
     def add_two_view_geometry(self, image_id1, image_id2, matches,
-                              F=np.eye(3), E=np.eye(3), H=np.eye(3), config=2):
+                              F=np.eye(3), E=np.eye(3), H=np.eye(3),
+                              qvec=np.array([1.0, 0.0, 0.0, 0.0]),
+                              tvec=np.zeros(3), config=2):
         assert(len(matches.shape) == 2)
         assert(matches.shape[1] == 2)
 
@@ -216,10 +218,19 @@ def add_two_view_geometry(self, image_id1, image_id2, matches,
         F = np.asarray(F, dtype=np.float64)
         E = np.asarray(E, dtype=np.float64)
         H = np.asarray(H, dtype=np.float64)
-        self.execute(
-            "INSERT INTO two_view_geometries VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
-            (pair_id,) + matches.shape + (array_to_blob(matches), config,
-                                          array_to_blob(F), array_to_blob(E), array_to_blob(H)))
+        qvec = np.asarray(qvec, dtype=np.float64)
+        tvec = np.asarray(tvec, dtype=np.float64)
+        try:
+            self.execute(
+                "INSERT INTO two_view_geometries VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+                (pair_id,) + matches.shape + (array_to_blob(matches), config,
+                                              array_to_blob(F), array_to_blob(E), array_to_blob(H)))
+        except sqlite3.OperationalError:
+            self.execute(
+                "INSERT INTO two_view_geometries VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+                (pair_id,) + matches.shape + (array_to_blob(matches), config,
+                                              array_to_blob(F), array_to_blob(E), array_to_blob(H),
+                                              array_to_blob(qvec), array_to_blob(tvec)))
 
 
 def example_usage():
@@ -244,10 +255,8 @@ def example_usage():
 
     # Create dummy cameras.
 
-    model1, width1, height1, params1 = \
-        0, 1024, 768, np.array((1024., 512., 384.))
-    model2, width2, height2, params2 = \
-        2, 1024, 768, np.array((1024., 512., 384., 0.1))
+    model1, width1, height1, params1 = 0, 1024, 768, np.array((1024., 512., 384.))
+    model2, width2, height2, params2 = 2, 1024, 768, np.array((1024., 512., 384., 0.1))
 
     camera_id1 = db.add_camera(model1, width1, height1, params1)
     camera_id2 = db.add_camera(model2, width2, height2, params2)
diff --git a/kapture/converter/openmvg/export_openmvg.py b/kapture/converter/openmvg/export_openmvg.py
index 606ef34..17ceb4c 100644
--- a/kapture/converter/openmvg/export_openmvg.py
+++ b/kapture/converter/openmvg/export_openmvg.py
@@ -245,7 +245,7 @@ def export_openmvg_views(
         polymorphic_registry: CerealPointerRegistry,
         ptr_wrapper_registry: CerealPointerRegistry,
         image_path_flatten: bool,
-):
+) -> List:
     """
 
     :param kapture_cameras:
@@ -256,7 +256,7 @@ def export_openmvg_views(
     :param polymorphic_registry: input/output polymorphic IDs status
     :param ptr_wrapper_registry: input/output polymorphic IDs status
     :param image_path_flatten: flatten image path (eg. to avoid image name collision in openMVG regions).
-    :return:
+    :return: views to be serialized
     """
     views = []
     # process all images
@@ -313,13 +313,13 @@ def export_openmvg_poses(
         kapture_images: kapture.RecordsCamera,
         kapture_trajectories: kapture.Trajectories,
         kapture_to_openmvg_view_ids: Dict[str, int],
-):
+) -> List:
     """
 
     :param kapture_images:
     :param kapture_trajectories:
     :param kapture_to_openmvg_view_ids: input dict that maps kapture image ids to openMVG view ids.
-    :return:
+    :return: extrinsics to be serialized
     """
     extrinsics = []
     # process all images
@@ -350,7 +350,7 @@ def export_openmvg_structure(
 ):
     # early check
     if kapture_points_3d is None:
-        logger.warning(f'no 3D points to export.')
+        logger.warning('no 3D points to export.')
         return
 
     xyz_coordinates = kapture_points_3d[:, 0:3]
@@ -454,7 +454,7 @@ def export_openmvg_sfm_data(
     polymorphic_registry = CerealPointerRegistry(id_key=JSON_KEY.POLYMORPHIC_ID, value_key=JSON_KEY.POLYMORPHIC_NAME)
     ptr_wrapper_registry = CerealPointerRegistry(id_key=JSON_KEY.ID, value_key=JSON_KEY.DATA)
 
-    logger.debug(f'exporting intrinsics ...')
+    logger.debug('exporting intrinsics ...')
     openmvg_sfm_data_intrinsics = export_openmvg_intrinsics(
         kapture_cameras=kapture_data.cameras,
         kapture_to_openmvg_cam_ids=kapture_to_openmvg_cam_ids,
@@ -462,7 +462,7 @@ def export_openmvg_sfm_data(
         ptr_wrapper_registry=ptr_wrapper_registry,
     )
 
-    logger.debug(f'exporting views ...')
+    logger.debug('exporting views ...')
     openmvg_sfm_data_views = export_openmvg_views(
         kapture_cameras=kapture_data.cameras,
         kapture_images=kapture_data.records_camera,
@@ -474,14 +474,14 @@ def export_openmvg_sfm_data(
         image_path_flatten=image_path_flatten,
     )
 
-    logger.debug(f'exporting poses ...')
+    logger.debug('exporting poses ...')
     openmvg_sfm_data_poses = export_openmvg_poses(
         kapture_images=kapture_data.records_camera,
         kapture_trajectories=kapture_data.trajectories,
         kapture_to_openmvg_view_ids=kapture_to_openmvg_view_ids)
 
     # structure : correspond to kapture observations + 3D points
-    logger.debug(f'exporting structure ...')
+    logger.debug('exporting structure ...')
     openmvg_sfm_data_structure = export_openmvg_structure(
         kapture_points_3d=kapture_data.points3d,
         kapture_to_openmvg_view_ids=kapture_to_openmvg_view_ids,
diff --git a/kapture/converter/openmvg/import_openmvg.py b/kapture/converter/openmvg/import_openmvg.py
index 1c910cc..0360ca1 100644
--- a/kapture/converter/openmvg/import_openmvg.py
+++ b/kapture/converter/openmvg/import_openmvg.py
@@ -402,5 +402,5 @@ def import_openmvg_matches(
     # look for the "image_describer.json"
     # matches.*.bin files use cereal over a
     # map < pair <uint32_t, uint32_t>, std::vector<uint64_t> >
-    matches = np.fromfile(matches_file_path, dtype=np.uint8)
-    #
\ No newline at end of file
+    # matches = np.fromfile(matches_file_path, dtype=np.uint8)
+    pass  # Ongoing work ...
diff --git a/kapture/core/Records.py b/kapture/core/Records.py
index 3aed094..7711376 100644
--- a/kapture/core/Records.py
+++ b/kapture/core/Records.py
@@ -34,7 +34,6 @@ def __setitem__(self,
 
         :param key: can be timestamp or (timestamp, sensor_id)
         :param value: can be respectively a dict{sensor_id: DataRecords} or DataRecords
-        :return:
         """
         # enforce type checking
         if isinstance(key, tuple):
@@ -83,6 +82,30 @@ def __getitem__(self, key: Union[int, Tuple[int, str]]) -> Union[Dict[str, T], T
         else:
             raise TypeError('key must be Union[int, Tuple[int, str]]')
 
+    def __delitem__(self, key: Union[int, Tuple[int, str]]):
+        """
+        Delete an item or items.
+
+        :param key: can be timestamp or (timestamp, sensor_id)
+        """
+        if isinstance(key, tuple):
+            # key is a pair of (timestamp, device_id)
+            timestamp = key[0]
+            device_id = key[1]
+            if not isinstance(timestamp, int):
+                raise TypeError('invalid timestamp')
+            if not isinstance(device_id, str):
+                raise TypeError('invalid device_id')
+            super(RecordsBase, self).__getitem__(timestamp).__delitem__(device_id)
+            if len(super(RecordsBase, self).__getitem__(timestamp)) == 0:
+                # Cleaning upper level
+                super(RecordsBase, self).__delitem__(timestamp)
+        elif isinstance(key, int):
+            # key is a timestamp
+            super(RecordsBase, self).__delitem__(key)
+        else:
+            raise TypeError('key must be Union[int, Tuple[int, str]]')
+
     def key_pairs(self) -> List[Tuple[int, str]]:
         """
         Returns a list of (timestamp, device_id) contained in records.
diff --git a/kapture/core/Trajectories.py b/kapture/core/Trajectories.py
index 9020541..364385a 100644
--- a/kapture/core/Trajectories.py
+++ b/kapture/core/Trajectories.py
@@ -1,10 +1,24 @@
 # Copyright 2020-present NAVER Corp. Under BSD 3-clause license
 
+"""
+The trajectories points. We store for every timestamp and device a pose.
+So you can have two different devices for the same timestamp with a different pose.
+The timestamps are integers, and device identifiers strings.
+The timestamps are often epoch timestamps (in micro or milliseconds), but can be any set of integers.
+Note that the timestamps are usually ordered ascending (as the time goes), but this is not guaranteed,
+and might change if you manipulate them, save and reload them from disk.
+If you need them ordered, do it yourself.
+"""
+
+import quaternion
+
 from .PoseTransform import PoseTransform
 from .Rigs import Rigs
 from .flatten import flatten
-from typing import Union, Dict, List, Tuple, Optional
+import kapture.utils.computation as computation
+from bisect import bisect_left
 from copy import deepcopy
+from typing import Union, Dict, List, Tuple, Optional
 
 
 class Trajectories(Dict[int, Dict[str, PoseTransform]]):
@@ -17,6 +31,10 @@ class Trajectories(Dict[int, Dict[str, PoseTransform]]):
             with <PoseTransform> transforming points from world to device.
     """
 
+    def __init__(self):
+        super().__init__()
+        self._timestamps_sorted_list = []
+
     def __setitem__(self,
                     key: Union[int, Tuple[int, str]],
                     value: Union[Dict[str, PoseTransform], PoseTransform]):
@@ -32,6 +50,7 @@ def __setitem__(self,
             if not isinstance(value, PoseTransform):
                 raise TypeError('invalid pose')
             self.setdefault(timestamp, {})[device_id] = value
+            self._timestamps_sorted_list = []
         elif isinstance(key, int):
             # key is a timestamp
             timestamp = key
@@ -43,6 +62,7 @@ def __setitem__(self,
             if not all(isinstance(v, PoseTransform) for v in value.values()):
                 raise TypeError('invalid Pose')
             super(Trajectories, self).__setitem__(timestamp, value)
+            self._timestamps_sorted_list = []
         else:
             raise TypeError('key must be Union[int, Tuple[int, str]]')
 
@@ -62,6 +82,51 @@ def __getitem__(self, key: Union[int, Tuple[int, str]]) -> Union[Dict[str, PoseT
         else:
             raise TypeError('key must be Union[int, Tuple[int, str]]')
 
+    def __delitem__(self, key: Union[int, Tuple[int, str]]):
+        if isinstance(key, tuple):
+            # key is a pair of (timestamp, device_id)
+            timestamp = key[0]
+            device_id = key[1]
+            if not isinstance(timestamp, int):
+                raise TypeError('invalid timestamp')
+            if not isinstance(device_id, str):
+                raise TypeError('invalid device_id')
+            super(Trajectories, self).__getitem__(timestamp).__delitem__(device_id)
+            if len(super(Trajectories, self).__getitem__(timestamp)) == 0:
+                # Cleaning upper level
+                super(Trajectories, self).__delitem__(timestamp)
+                self._timestamps_sorted_list = []
+        elif isinstance(key, int):
+            # key is a timestamp
+            super(Trajectories, self).__delitem__(key)
+            self._timestamps_sorted_list = []
+        else:
+            raise TypeError('key must be Union[int, Tuple[int, str]]')
+
+    def timestamps_sorted_list(self) -> List[int]:
+        """
+        Get the list of timestamps is ascending sorted order
+        """
+        if len(self._timestamps_sorted_list) == 0:
+            # Need to sort
+            self._timestamps_sorted_list = sorted(list(self.keys()))
+        return self._timestamps_sorted_list
+
+    def timestamp_length(self) -> int:
+        """
+        Compute the trajectory timestamp length, which can be 10, 13 or 16 if these are epoch values, and anything.
+
+        :return: Length of the timestamps as a positive integer, or -1 if it is variable
+        """
+        timestamps = self.timestamps_sorted_list()
+        base_length = computation.num_digits(timestamps[0]) if len(timestamps) > 0 else -1
+        indexes = [1, 2, 3, 4, 5, -1, -2, -3, -4] if len(timestamps) > 10 else list(range(1, len(timestamps)))
+        for n in indexes:
+            length = computation.num_digits(timestamps[n])
+            if length != base_length:
+                return -1
+        return base_length
+
     def key_pairs(self) -> List[Tuple[int, str]]:
         """
         Returns the list of (timestamp, device_id) contained in trajectories.
@@ -123,7 +188,6 @@ def rigs_remove_inplace(trajectories: Trajectories, rigs: Rigs, max_depth: int =
     :param trajectories: input/output Trajectories where the rigs has to be replaced
     :param rigs: input Rigs that defines the rigs/sensors relationship.
     :param max_depth: maximum nested rig depth.
-    :return:
     """
     assert isinstance(rigs, Rigs)
     assert isinstance(trajectories, Trajectories)
@@ -191,7 +255,7 @@ def rigs_recover_inplace(
     :param trajectories: input/output Trajectories.
     :param rigs: input Rigs configuration.
     :param master_sensors: input If given, only compute rig poses for the given sensors.
-    :return:
+    :param max_depth: maximum nested rig depth.
     """
 
     # sensor_id -> rig_id, pose_rig_from_sensor
@@ -231,3 +295,26 @@ def rigs_recover_inplace(
             # it may end up to inconsistent results.
             pose_rig_from_world = PoseTransform.compose([pose_rig_from_sensor, pose_sensor_from_world])
             trajectories[timestamp, rig_id] = pose_rig_from_world
+
+
+def compute_intermediate_pose(timestamp: int,
+                              low_ts: int, low_p: PoseTransform,
+                              up_ts: int, up_p: PoseTransform) -> PoseTransform:
+    """
+    Compute an intermediate pose between two poses.
+    It does not come from the recorded data, but is purely computed by interpolation based on given poses.
+    We suppose that we move at a regular speed.
+
+    :param timestamp: the timestamp at which time to compute the pose
+    :param low_ts: the first timestamp
+    :param low_p: the first pose
+    :param up_ts: the second timestamp
+    :param up_p: the second pose
+    :return: the computed pose
+    """
+    rotation = quaternion.slerp(low_p.r, up_p.r, low_ts, up_ts, timestamp)
+    # translation = t0 + (ts-ts0)/(ts1-ts0) * (t1 - t0)
+    translation = [low_p.t[0] + (timestamp - low_ts) / (up_ts - low_ts) * (up_p.t[0] - low_p.t[0]),
+                   low_p.t[1] + (timestamp - low_ts) / (up_ts - low_ts) * (up_p.t[1] - low_p.t[1]),
+                   low_p.t[2] + (timestamp - low_ts) / (up_ts - low_ts) * (up_p.t[2] - low_p.t[2])]
+    return PoseTransform(rotation, translation)
diff --git a/kapture/utils/computation.py b/kapture/utils/computation.py
new file mode 100644
index 0000000..b07aed0
--- /dev/null
+++ b/kapture/utils/computation.py
@@ -0,0 +1,20 @@
+# Copyright 2021-present NAVER Corp. Under BSD 3-clause license
+
+"""
+Computation helper operations.
+"""
+
+
+def num_digits(n) -> int:
+    """
+    Compute the number of digits of a number
+
+    :return: number of digits
+    """
+    if not isinstance(n, int):
+        raise TypeError(f'Invalid n: not an integer {n}')
+    count = 1
+    while (int(n / 10)) != 0:
+        count += 1
+        n = int(n / 10)
+    return count
diff --git a/kapture_format.adoc b/kapture_format.adoc
index 6ff8d91..2d16f64 100644
--- a/kapture_format.adoc
+++ b/kapture_format.adoc
@@ -277,6 +277,8 @@ rig, phone01wifi, 1, 0, 0, 0, -10, 0, 0
 This file contains the pose of all devices (sensor or rig) in the dataset using one line per (timestamp, device_id).
 This file is optional. However, it is required by many algorithms, so if it is missing,
 only a subset of the tools will be useful.
+The file is usually ordered by timestamp and device_id, but it is only by implementation choice. Do not rely on this,
+or order the data yourself if you need an ordering.
 
 ==== syntax
 
diff --git a/setup.py b/setup.py
index 95d34ad..fda2609 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@ def read_doc(filepath):
             content = read_file(md_filepath)
 
     except FileNotFoundError:
-        warnings.warn(f'cannot convert asciidoc to markdown.')
+        warnings.warn('cannot convert asciidoc to markdown.')
         content = read_file(filepath)
 
     return content
diff --git a/tools/kapture_import_bundler.py b/tools/kapture_import_bundler.py
index aaf46f6..e84f5a2 100755
--- a/tools/kapture_import_bundler.py
+++ b/tools/kapture_import_bundler.py
@@ -105,7 +105,7 @@ def import_bundler(bundler_path: str,
     trajectories = kapture.Trajectories() if not ignore_trajectories else None
     points3d = [] if add_reconstruction else None
     keypoints = None
-    observations = kapture.Observations() if add_reconstruction else None if add_reconstruction else None
+    observations = kapture.Observations() if add_reconstruction else None
     image_mapping = []  # bundler camera_id -> (name, width, height)
     for i in range(0, number_of_cameras):
         start_index = i * number_of_lines_per_camera + offset
@@ -173,7 +173,7 @@ def import_bundler(bundler_path: str,
                 camera_id = int(view_list[1 + 4 * j + 0])
                 keypoint_id = int(view_list[1 + 4 * j + 1])
                 x = float(view_list[1 + 4 * j + 2])
-                y = float(view_list[1 + 4 * j + 2])
+                y = float(view_list[1 + 4 * j + 3])
 
                 file_name, width, height = image_mapping[camera_id]
                 # put (0,0) in upper left corner
@@ -196,7 +196,7 @@ def import_bundler(bundler_path: str,
         # finally, convert local_keypoints to np.ndarray and add them to the global keypoints variable
         sift_keypoints = kapture.Keypoints(LOCAL_FEATURE_TYPE, np.float32, 2)
         for image_filename, keypoints_array in local_keypoints.items():
-            keypoints_np_array = np.array(keypoints_array)
+            keypoints_np_array = np.array(keypoints_array, dtype=np.float32)
             keypoints_out_path = kapture.io.features.get_keypoints_fullpath(LOCAL_FEATURE_TYPE,
                                                                             kapture_dir_path,
                                                                             image_filename)
diff --git a/tools/kapture_import_nvm.py b/tools/kapture_import_nvm.py
index 527e489..5163040 100755
--- a/tools/kapture_import_nvm.py
+++ b/tools/kapture_import_nvm.py
@@ -310,7 +310,7 @@ def parse_points3d(kapture_path: str,
 
     # finally, convert local_keypoints to np.ndarray and add them to the global keypoints variable
     for image_filename, keypoints_array in local_keypoints.items():
-        keypoints_np_array = np.array(keypoints_array)
+        keypoints_np_array = np.array(keypoints_array, dtype=np.float32)
         keypoints_filepath = kapture.io.features.get_keypoints_fullpath(LOCAL_FEATURE_TYPE,
                                                                         kapture_path,
                                                                         image_filename)


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 4fc4d0d..c084a6d 100755
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,6 +1,10 @@
 #!/usr/bin/env python3
 # Copyright 2020-present NAVER Corp. Under BSD 3-clause license
 
+"""
+Tests of the kapture core module.
+"""
+
 import unittest
 import numpy as np
 import quaternion
@@ -345,6 +349,9 @@ def test_type_checking(self):
         self.assertRaises(TypeError, traj.__contains__, valid_ts, invalid_id)
         self.assertRaises(TypeError, traj.__contains__, invalid_ts, invalid_id)
 
+        self.assertRaises(TypeError, traj.__delitem__, invalid_ts)
+        self.assertRaises(TypeError, traj.__delitem__, (valid_ts, invalid_id))
+
     def test_rig_remove(self):
         rigs = kapture.Rigs()
         rigs['rig0', 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[100, 0, 0])
@@ -365,6 +372,53 @@ def test_rig_remove(self):
         self.assertAlmostEqual(trajectories_[2, 'cam1'].t_raw, [-100.0, 0.0, 20.0])
         self.assertAlmostEqual(trajectories_[2, 'cam1'].r_raw, [1.0, 0.0, 0.0, 0.0])
 
+    def test_remove(self):
+        trajectories = kapture.Trajectories()
+        trajectories[0, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 0])
+        trajectories[1, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 10])
+        trajectories[2, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 20])
+        trajectories[2, 'cam1'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[10, 0, 20])
+        self.assertEqual(len(trajectories), 3)
+        self.assertEqual(len(trajectories[2]), 2)
+
+        del trajectories[2, 'cam0']
+        self.assertEqual(len(trajectories), 3)
+        self.assertEqual(len(trajectories[2]), 1)
+
+        del trajectories[1]
+        self.assertEqual(len(trajectories), 2)
+        del trajectories[2, 'cam1']
+        self.assertEqual(len(trajectories), 1)
+
+    def test_timestamps_list(self):
+        trajectories = kapture.Trajectories()
+        trajectories[2, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 20])
+        trajectories[0, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 0])
+        trajectories[2, 'cam1'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[10, 0, 20])
+        self.assertEqual(trajectories.timestamps_sorted_list(), [0, 2])
+        trajectories[1, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 10])
+        self.assertEqual(len(trajectories), 3)
+        self.assertEqual(trajectories.timestamps_sorted_list(), [0, 1, 2])
+
+        del trajectories[2, 'cam0']
+        self.assertEqual(trajectories.timestamps_sorted_list(), [0, 1, 2])
+
+        del trajectories[1]
+        self.assertEqual(trajectories.timestamps_sorted_list(), [0, 2])
+        del trajectories[2, 'cam1']
+        self.assertEqual(trajectories.timestamps_sorted_list(), [0, ])
+
+    def test_timestamps_length(self):
+        trajectories = kapture.Trajectories()
+        self.assertEqual(trajectories.timestamp_length(), -1)
+        trajectories[1614362592378, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 20])
+        trajectories[1614362592634, 'cam0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 0])
+        trajectories[1614362592378, 'cam1'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[10, 0, 20])
+        trajectories[1614362593123, 'cam1'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[10, 0, 30])
+        self.assertEqual(trajectories.timestamp_length(), 13)
+        # Check that if we have timestamps of different precision, we can not compute a common length
+        trajectories[1614362594, 'lidar0'] = kapture.PoseTransform(r=[1, 0, 0, 0], t=[0, 0, 0])
+        self.assertEqual(trajectories.timestamp_length(), -1)
 
 # REMOVE/RESTORE RIGS in TRAJECTORIES ##################################################################################
 class TestTrajectoriesRig(unittest.TestCase):
@@ -439,32 +493,45 @@ class TestRecords(unittest.TestCase):
     def test_init_camera(self):
         timestamp0, timestamp1 = 0, 1
         device_id0, device_id1 = 'cam0', 'cam1'
+        record_cam0_image0 = 'cam0/image000.jpg'
+        record_cam0_image1 = 'cam0/image001.jpg'
+        record_cam1_image0 = 'cam1/image000.jpg'
+        record_cam1_image1 = 'cam1/image001.jpg'
+        # Test insertions
         records_camera = kapture.RecordsCamera()
-        records_camera[timestamp0, device_id0] = 'cam0/image000.jpg'
+        records_camera[timestamp0, device_id0] = record_cam0_image0
         kapture_data = kapture.Kapture(records_camera=records_camera)
         self.assertEqual(1, len(kapture_data.records_camera.keys()))
         self.assertEqual(1, len(kapture_data.records_camera.key_pairs()))
         self.assertIn(timestamp0, kapture_data.records_camera)
         self.assertIn(device_id0, kapture_data.records_camera[timestamp0])
         self.assertIn((timestamp0, device_id0), kapture_data.records_camera)
-        self.assertEqual('cam0/image000.jpg', kapture_data.records_camera[timestamp0, device_id0])
-        records_camera[timestamp1, device_id0] = 'cam0/image001.jpg'
+        self.assertEqual(record_cam0_image0, kapture_data.records_camera[timestamp0, device_id0])
+        records_camera[timestamp1, device_id0] = record_cam0_image1
         self.assertEqual(2, len(kapture_data.records_camera.keys()))
         self.assertEqual(2, len(kapture_data.records_camera.key_pairs()))
-        kapture_data.records_camera[timestamp0][device_id1] = 'cam1/image000.jpg'
+        kapture_data.records_camera[timestamp0][device_id1] = record_cam1_image0
         self.assertEqual(2, len(kapture_data.records_camera.keys()))
         self.assertEqual(3, len(kapture_data.records_camera.key_pairs()))
-        records_camera[timestamp1][device_id1] = 'cam1/image001.jpg'
+        records_camera[timestamp1][device_id1] = record_cam1_image1
         self.assertEqual(2, len(kapture_data.records_camera.keys()))
         self.assertEqual(4, len(kapture_data.records_camera.key_pairs()))
-        self.assertEqual('cam0/image000.jpg', kapture_data.records_camera[timestamp0, device_id0])
-        self.assertEqual('cam1/image000.jpg', kapture_data.records_camera[timestamp0, device_id1])
-        self.assertEqual('cam0/image001.jpg', kapture_data.records_camera[timestamp1, device_id0])
-        self.assertEqual('cam1/image001.jpg', kapture_data.records_camera[timestamp1, device_id1])
+        self.assertEqual(record_cam0_image0, kapture_data.records_camera[timestamp0, device_id0])
+        self.assertEqual(record_cam1_image0, kapture_data.records_camera[timestamp0, device_id1])
+        self.assertEqual(record_cam0_image1, kapture_data.records_camera[timestamp1, device_id0])
+        self.assertEqual(record_cam1_image1, kapture_data.records_camera[timestamp1, device_id1])
 
         self.assertNotIn((timestamp1, 'cam2'), kapture_data.records_camera)
         self.assertNotIn((2, device_id0), kapture_data.records_camera)
 
+        # Test deletion
+        del kapture_data.records_camera[(timestamp0, device_id0)]
+        self.assertEqual(2, len(kapture_data.records_camera.keys()))
+        self.assertEqual(3, len(kapture_data.records_camera.key_pairs()))
+        del kapture_data.records_camera[(timestamp0, device_id1)]
+        self.assertEqual(1, len(kapture_data.records_camera.keys()))
+        self.assertEqual(2, len(kapture_data.records_camera.key_pairs()))
+
     def test_init_lidar(self):
         records_lidar = kapture.RecordsLidar()
         self.assertIsNotNone(records_lidar, "Records Lidar created")
@@ -516,6 +583,27 @@ def test_init_gnss_epsg(self):
         self.assertEqual(15., records_gnss[0, gps_id1].z)
         self.assertEqual(9., records_gnss[0, gps_id1].dop)
 
+    def test_type_checking(self):
+        records_camera = kapture.RecordsCamera()
+        valid_ts, valid_id, valid_record = 0, 'cam0', 'cam0/image0.jpg'
+        invalid_ts, invalid_id, invalid_record = '0', float(0), kapture.PoseTransform()
+        self.assertRaises(TypeError, records_camera.__setitem__, (invalid_ts, valid_id), valid_record)
+        self.assertRaises(TypeError, records_camera.__setitem__, (valid_ts, invalid_id), valid_record)
+        self.assertRaises(TypeError, records_camera.__setitem__, (valid_ts, valid_id), invalid_record)
+        self.assertRaises(TypeError, records_camera.__setitem__, (invalid_ts, invalid_id), invalid_record)
+
+        self.assertRaises(TypeError, records_camera.__setitem__, invalid_ts, {valid_id: valid_record})
+        self.assertRaises(TypeError, records_camera.__setitem__, valid_ts, {invalid_id: valid_record})
+        self.assertRaises(TypeError, records_camera.__setitem__, valid_ts, {valid_id: invalid_record})
+        self.assertRaises(TypeError, records_camera.__setitem__, invalid_ts, valid_record)
+
+        self.assertRaises(TypeError, records_camera.__contains__, invalid_ts, valid_id)
+        self.assertRaises(TypeError, records_camera.__contains__, valid_ts, invalid_id)
+        self.assertRaises(TypeError, records_camera.__contains__, invalid_ts, invalid_id)
+
+        self.assertRaises(TypeError, records_camera.__delitem__, invalid_ts)
+        self.assertRaises(TypeError, records_camera.__delitem__, (valid_ts, invalid_id))
+
 
 # Keypoints ############################################################################################################
 class TestKeypoints(unittest.TestCase):
@@ -547,12 +635,12 @@ def test_init_descriptors_unknown(self):
 # GlobalFeatures #######################################################################################################
 class TestGlobalFeatures(unittest.TestCase):
     def test_init_global_features_unknown(self):
-        gloabl_features = kapture.GlobalFeatures('BOW', float, 4, 'CSI',
+        global_features = kapture.GlobalFeatures('BOW', float, 4, 'CSI',
                                                  ['a/a.jpg', 'b/b.jpg', 'c/c.jpg', 'c/c.jpg'])
-        self.assertEqual('BOW', gloabl_features.type_name)
-        self.assertEqual('CSI', gloabl_features.metric_type)
-        self.assertEqual(3, len(gloabl_features))
-        self.assertIn('a/a.jpg', gloabl_features)
+        self.assertEqual('BOW', global_features.type_name)
+        self.assertEqual('CSI', global_features.metric_type)
+        self.assertEqual(3, len(global_features))
+        self.assertIn('a/a.jpg', global_features)
 
 
 # Points3d #############################################################################################################
diff --git a/tests/test_openmvg.py b/tests/test_openmvg.py
index 25177bd..a95b9b2 100755
--- a/tests/test_openmvg.py
+++ b/tests/test_openmvg.py
@@ -146,7 +146,7 @@ def test_kapture_to_openmvg(self) -> None:
             basler = None
             lidar = None
             for intrinsic in intrinsics:
-                camera_num = intrinsic.get(JSON_KEY.KEY)
+                # camera_num = intrinsic.get(JSON_KEY.KEY)
                 camera_id = intrinsic.get(JSON_KEY.VALUE, {}).get(JSON_KEY.PTR_WRAPPER, {}).get(JSON_KEY.ID)
                 camera_ids[camera_id] = camera_id
                 if camera_id == FIRST_BASLER_ID:
@@ -179,7 +179,8 @@ def test_kapture_to_openmvg(self) -> None:
             self.assertIsNotNone(image_record, "4th image record")
             local_path = image_record.get(JSON_KEY.LOCAL_PATH)
             # self.assertEqual(FIRST_BASLER_ID, local_path, "Local path is the camera id")
-            # self.assertEqual(FIRST_BASLER_ID, image_record.get(JSON_KEY.ID_INTRINSIC), "id_intrinsic is the camera id")
+            # self.assertEqual(FIRST_BASLER_ID, image_record.get(JSON_KEY.ID_INTRINSIC),
+            #                  "id_intrinsic is the camera id")
             self.assertEqual(camera_params.get(JSON_KEY.WIDTH), image_record.get(JSON_KEY.WIDTH),
                              "Image has camera width")
             self.assertEqual(camera_params.get(JSON_KEY.HEIGHT), image_record.get(JSON_KEY.HEIGHT),
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 0aca876..82d15a0 100755
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,12 +5,14 @@
 Test some of the utils functions and classes.
 """
 
+import datetime
 import os.path as path
 import tempfile
 import unittest
 # kapture
 import path_to_kapture  # enables import kapture  # noqa: F401
 import kapture
+import kapture.utils.computation as computation
 import kapture.utils.paths
 
 
@@ -44,6 +46,23 @@ def testPrependToFile(self):
             first_line = f.readline()
             self.assertEqual(additional, first_line, "Successful prepend")
 
+    def testDigitsNumNotAnInt(self):
+        now = datetime.datetime.now().timestamp()
+        self.assertRaises(TypeError, computation.num_digits, now, msg="must be called with an integer")
+
+    def testDigitsZero(self):
+        number = 0
+        self.assertEqual(computation.num_digits(number), 1, "1 digit")
+
+    def testDigitsNum(self):
+        number = 1234567890
+        self.assertEqual(computation.num_digits(number), 10, "10 digits")
+
+    def testDigitsNumEpoch(self):
+        # Computes number of digits of typical epoch in microseconds
+        now = int(datetime.datetime.now().timestamp()*1000000)
+        self.assertEqual(computation.num_digits(now), 16, "Epoch now has 16 digits")
+
     def tearDown(self) -> None:
         """
         Clean up after every test