Skip to content

Commit fa6f540

Browse files
vjluxLukas Gruber
andauthored
Parses navvis origin file and converts into Capture::Lamar csv file (#65)
--------- Co-authored-by: Lukas Gruber <[email protected]>
1 parent 0846a07 commit fa6f540

File tree

10 files changed

+287
-3
lines changed

10 files changed

+287
-3
lines changed

CAPTURE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ location1/ # a Capture directory
2323
│   │   ├── sensors.txt # list of all sensors with specs
2424
│   │   ├── trajectories.txt # pose for each (timestamp, sensor)
2525
│   │   ├── wifi.txt # list of wifi measurements
26+
| | ├── origins.txt # list of NavVis session origins (label and alignment pose)
2627
│   │   ├── raw_data/ # root path of images, point clouds, etc.
2728
│   │   │   ├── images_undistorted/
2829
│   │   │   ├── render/ # root path for the rgb and depth maps renderings

pipelines/pipeline_navvis_rig.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
├── rigs.txt
6969
├── sensors.txt
7070
├── trajectories.txt
71+
├── origins.txt
7172
└── wifi.txt
7273
7374
1. **Mesh Generation**

scantools/capture/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
RecordWifi, RecordWifiSignal, RecordsWifi)
1010
from .pose import Pose
1111
from .proc import Proc
12+
from .proc import GlobalAlignment
1213
from .misc import KeyType

scantools/capture/records.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,6 @@ class RecordsBluetooth(RecordsArray[RecordBluetooth]):
276276
records[timestamp, sensor_id] = <RecordBluetooth>
277277
"""
278278
record_type = RecordBluetooth
279+
280+
281+

scantools/capture/session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .rigs import Rigs
99
from .trajectories import Trajectories
1010
from .records import RecordsBluetooth, RecordsCamera, RecordsDepth, RecordsLidar, RecordsWifi
11-
from .proc import Proc
11+
from .proc import Proc, GlobalAlignment
1212
from .pose import Pose
1313

1414
logger = logging.getLogger(__name__)
@@ -42,6 +42,7 @@ class Session:
4242
bt: Optional[RecordsBluetooth] = None
4343
proc: Optional[Proc] = None
4444
id: Optional[str] = None
45+
origins: Optional[GlobalAlignment] = None
4546

4647
data_dirname = 'raw_data'
4748
proc_dirname = 'proc'

scantools/run_navvis_to_capture.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .capture import (
1414
Capture, Session, Sensors, create_sensor, Trajectories, Rigs, Pose,
1515
RecordsCamera, RecordsLidar, RecordBluetooth, RecordBluetoothSignal,
16-
RecordsBluetooth, RecordWifi, RecordWifiSignal, RecordsWifi)
16+
RecordsBluetooth, RecordWifi, RecordWifiSignal, RecordsWifi, GlobalAlignment)
1717
from .utils.misc import add_bool_arg
1818
from .utils.io import read_image, write_image
1919

@@ -231,9 +231,21 @@ def run(input_path: Path, capture: Capture, tiles_format: str, session_id: Optio
231231
bluetooth_signals[timestamp_us, sensor_id] = RecordBluetooth()
232232
bluetooth_signals[timestamp_us, sensor_id][id] = RecordBluetoothSignal(rssi_dbm=rssi_dbm)
233233

234+
# Read the NavVis origin.json file if present and use proc.GlobalAlignment to save it.
235+
navvis_origin = None
236+
if nv.load_origin():
237+
origin_qvec, origin_tvec, origin_crs = nv.get_origin()
238+
navvis_origin = GlobalAlignment()
239+
navvis_origin[origin_crs, navvis_origin.no_ref] = (
240+
Pose(r=origin_qvec, t=origin_tvec),
241+
[],
242+
)
243+
logger.info("Loaded NavVis origin.json")
244+
234245
session = Session(
235246
sensors=sensors, rigs=rigs, trajectories=trajectory,
236-
images=images, pointclouds=pointclouds, wifi=wifi_signals, bt=bluetooth_signals)
247+
images=images, pointclouds=pointclouds, wifi=wifi_signals, bt=bluetooth_signals,
248+
origins=navvis_origin)
237249
capture.sessions[session_id] = session
238250
capture.save(capture.path, session_ids=[session_id])
239251

scantools/scanners/navvis/navvis.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .camera_tiles import Tiles, TileFormat
1111
from .ibeacon_parser import parse_navvis_ibeacon_packet, BluetoothMeasurement
1212
from .iwconfig_parser import parse_iwconfig, WifiMeasurement
13+
from .origin_parser import parse_navvis_origin_file, get_pose_from_navvis_origin, get_crs_from_navvis_origin
1314
from . import ocamlib
1415
from ...utils import transform
1516
from ...utils.io import read_csv, convert_dng_to_jpg
@@ -29,6 +30,7 @@ def __init__(self, input_path: Path, output_path: Optional[Path] = None,
2930
self._pointcloud_file_path = None
3031
self._trace_path = None
3132
self._imu = None
33+
self._origin_file_path = None
3234

3335
self._output_path = None
3436
self._output_image_path = None
@@ -37,6 +39,7 @@ def __init__(self, input_path: Path, output_path: Optional[Path] = None,
3739
self.__cameras = {}
3840
self.__frames = {}
3941
self.__trace = {}
42+
self.__origin_data = {}
4043

4144
# upright fix
4245
self.__upright = upright
@@ -72,6 +75,9 @@ def _set_dataset_paths(self, input_path: Path, output_path: Optional[Path], tile
7275
self._input_path = Path(input_path).absolute()
7376
if not self._input_path.exists():
7477
raise FileNotFoundError(f'Input path {self._input_path}.')
78+
79+
# Origin file path
80+
self._origin_file_path = self._input_path / "anchors" / "origin.json"
7581

7682
# Images path
7783
self._input_image_path = self._input_path / "cam"
@@ -677,6 +683,27 @@ def read_wifi(self):
677683
wifi_measurements.append(wifi_measurement)
678684

679685
return wifi_measurements
686+
687+
def get_origin(self):
688+
"""Returns the NavVis origin transformation vectors and coordinate reference
689+
system (CRS) name.
690+
Returns
691+
-------
692+
Tuple: Tuple containing the quaternion, translation vector, and CRS.
693+
"""
694+
695+
crs = get_crs_from_navvis_origin(self.__origin_data)
696+
qvec, tvec = get_pose_from_navvis_origin(self.__origin_data)
697+
return qvec, tvec, crs
698+
699+
def load_origin(self):
700+
"""Tries loading the NavVis origin from file.
701+
Returns
702+
-------
703+
Bool : True if origin was successfully loaded, False otherwise.
704+
"""
705+
self.__origin_data = parse_navvis_origin_file(self._origin_file_path)
706+
return self.__origin_data != {}
680707

681708
#
682709
# auxiliary function for parallel computing
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import json
2+
import logging
3+
from pathlib import Path
4+
5+
logger = logging.getLogger(__name__)
6+
7+
UNKNOWN_CRS_NAME = 'UNKNOWN'
8+
9+
def is_navvis_origin_valid(navvis_origin : dict):
10+
"""
11+
Check if the NavVis origin dictionary is valid.
12+
CRS is optional. Pose is required.
13+
:param navvis_origin: NavVis origin dictionary
14+
:return: True if valid, False otherwise
15+
:rtype: bool
16+
"""
17+
if navvis_origin['Pose']['position']['x'] is None or \
18+
navvis_origin['Pose']['position']['y'] is None or \
19+
navvis_origin['Pose']['position']['z'] is None or \
20+
navvis_origin['Pose']['orientation']['w'] is None or \
21+
navvis_origin['Pose']['orientation']['x'] is None or \
22+
navvis_origin['Pose']['orientation']['y'] is None or \
23+
navvis_origin['Pose']['orientation']['z'] is None:
24+
return False
25+
return True
26+
27+
def parse_navvis_origin_file(file_path : Path):
28+
"""
29+
The origin.json file is optional and if present it can be found
30+
in the anchors folder.
31+
32+
The origin.json file contains two important values: pose and CRS.
33+
34+
The CRS value stands for Coordinate Reference System (CRS) and explains in which
35+
coordinate system the origin itself is defined. Example: EPSG:25834 https://epsg.io/25834
36+
37+
The pose transforms dataset entities into the origin. The origin
38+
of the dataset can be created in many different ways:
39+
0 - The origin is the NavVis 'dataset' origin, where a dataset equals a NavVis session.
40+
The origin then defaults to identity and the origin.json file might not be even present.
41+
1 - NavVis software allows relative alignment between dataset via the NavVis IVION Dataset Web Editor
42+
but also via the NavVis local processing software which is soon to be deprecated.
43+
2 - The origin is the NavVis Site origin. NavVis organizes datasets in the same physical location
44+
via Sites. The origin file contains then the transformation which moves all the entities of a
45+
NavVis dataset into the Site origin. Additionally NavVis IVION allows to register the Site origin
46+
to a global coordinate system. Hence, many NavVis sessions can be registered then to the same
47+
global coordinate system. Note that this is achieved via the NavVis IVION Dataset Web Editor.
48+
3 - The origin lies in a Coordinate Reference System (CRS) like EPSG:25834 https://epsg.io/25834.
49+
The transformation is computed via geo-referenced Control Points which are registered during
50+
capture. More information about control points and the origin can be found here:
51+
https://knowledge.navvis.com/v1/docs/creating-the-control-point-poses-file
52+
https://knowledge.navvis.com/docs/what-coordinate-system-do-we-use-for-the-control-points-related-tasks
53+
54+
:param file_path: Path to the file
55+
:return: NavVis anchor origin dictionary
56+
:rtype: Dict
57+
"""
58+
if not file_path.exists():
59+
print(f"Warning: Origin '{file_path}' does not exist.")
60+
return {}
61+
62+
try:
63+
with file_path.open() as f:
64+
origin = json.load(f)
65+
if not is_navvis_origin_valid(origin):
66+
print("Invalid origin.json file", json.dumps(origin, indent=4))
67+
return origin
68+
except Exception as e:
69+
logger.warning(
70+
"Failed reading origin.json file. %s", e)
71+
return {}
72+
73+
74+
def get_crs_from_navvis_origin(navvis_origin : dict):
75+
"""
76+
Get the label from the NavVis origin
77+
:param navvis_origin: NavVis origin dictionary
78+
:return: Label
79+
:rtype: str
80+
"""
81+
82+
return navvis_origin.get('CRS', UNKNOWN_CRS_NAME)
83+
84+
85+
def get_pose_from_navvis_origin(navvis_origin : dict):
86+
"""
87+
Extract the pose from the NavVis origin dictionary
88+
:param navvis_origin: NavVis origin dictionary
89+
:return: Quaternion and translation vector
90+
:rtype: qvec, tvec
91+
"""
92+
93+
qvec = [1, 0, 0, 0]
94+
tvec = [0, 0, 0]
95+
if navvis_origin:
96+
orientation = navvis_origin['Pose']['orientation']
97+
position = navvis_origin['Pose']['position']
98+
qvec = [orientation['w'], orientation['x'], orientation['y'], orientation['z']]
99+
tvec = [position['x'], position['y'], position['z']]
100+
return qvec, tvec
101+
102+
103+
def convert_navvis_origin_to_csv(navvis_origin : dict):
104+
csv_str = "# CRS, qw, qx, qy, qz, tx, ty, tz\n"
105+
if 'CRS' in navvis_origin:
106+
crs = navvis_origin['CRS']
107+
else:
108+
crs = UNKNOWN_CRS_NAME
109+
position = navvis_origin['Pose']['position']
110+
orientation = navvis_origin['Pose']['orientation']
111+
csv_str += (f"{crs},"
112+
f"{orientation['w']},"
113+
f"{orientation['x']},"
114+
f"{orientation['y']},"
115+
f"{orientation['z']},"
116+
f"{position['x']},"
117+
f"{position['y']},"
118+
f"{position['z']}\n")
119+
return csv_str

scantools/tests/test_navvis.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,4 @@ def test_get_image_filename(m6_object, m6_testdata):
404404

405405
res_image_filename = m6_object.get_image_filename(test_frame.id, test_frame.pose.camera_id)
406406
assert res_image_filename == exp_image_filename
407+
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
""" Tests for origin_parse.py """
2+
import pytest
3+
import json
4+
import os
5+
import numpy as np
6+
7+
from ..capture import Pose, GlobalAlignment
8+
from ..scanners.navvis import origin_parser
9+
10+
GLOBAL_ALIGNMENT_TABLE_HEADER = "# label, reference_id, qw, qx, qy, qz, tx, ty, tz, [info]+\n"
11+
12+
@pytest.mark.parametrize("navvis_origin, expected_csv_output", [({
13+
"CRS": "EPSG:25834",
14+
"Pose": {
15+
"orientation": {
16+
"w": 0.7071068,
17+
"x": 0,
18+
"y": 0,
19+
"z": -0.7071068
20+
},
21+
"position": {
22+
"x": 6.3,
23+
"y": 2.4,
24+
"z": 99.95
25+
}
26+
}},
27+
GLOBAL_ALIGNMENT_TABLE_HEADER + "EPSG:25834, __absolute__," +
28+
" 0.7071067811865476, 0.0, 0.0, -0.7071067811865476," +
29+
" 6.3, 2.4, 99.95\n"),
30+
({
31+
"Pose": {
32+
"orientation": {
33+
"w": 1,
34+
"x": 0,
35+
"y": 0,
36+
"z": 0
37+
},
38+
"position": {
39+
"x": 0,
40+
"y": 0,
41+
"z": 0
42+
}
43+
}
44+
}, GLOBAL_ALIGNMENT_TABLE_HEADER + origin_parser.UNKNOWN_CRS_NAME + ", __absolute__," +
45+
" 1.0, 0.0, 0.0, 0.0," +
46+
" 0.0, 0.0, 0.0\n"),
47+
])
48+
def test_parse_navvis_origin(navvis_origin, expected_csv_output, tmp_path):
49+
navvis_origin_path = tmp_path / "navvis_origin.json"
50+
with open(navvis_origin_path, 'w') as file:
51+
json.dump(navvis_origin, file)
52+
53+
navvis_origin_loaded = origin_parser.parse_navvis_origin_file(navvis_origin_path)
54+
assert navvis_origin_loaded == navvis_origin
55+
os.remove(navvis_origin_path)
56+
57+
alignment = GlobalAlignment()
58+
crs = origin_parser.get_crs_from_navvis_origin(navvis_origin_loaded)
59+
qvec, tvec = origin_parser.get_pose_from_navvis_origin(navvis_origin_loaded)
60+
alignment_pose = Pose(qvec, tvec)
61+
alignment[crs, alignment.no_ref] = (
62+
alignment_pose, [])
63+
alignment_path = tmp_path / 'origin.txt'
64+
alignment.save(alignment_path)
65+
66+
with open(alignment_path, 'r') as file:
67+
csv_output = file.read()
68+
print(csv_output)
69+
print(expected_csv_output)
70+
assert csv_output == expected_csv_output
71+
72+
alignment_loaded = GlobalAlignment().load(alignment_path)
73+
os.remove(alignment_path)
74+
75+
alignment_pose_loaded = alignment_loaded.get_abs_pose(crs)
76+
assert np.allclose(alignment_pose_loaded.qvec,
77+
alignment_pose.qvec, 1e-10)
78+
assert np.allclose(alignment_pose_loaded.t,
79+
alignment_pose.t, 1e-10)
80+
81+
82+
@pytest.mark.parametrize("bad_json_keys_origin", [{
83+
"CRS": "EPSG:25834",
84+
"Pose": {
85+
"orientation": {
86+
"w": 0.8,
87+
"x": 0,
88+
"y": 0,
89+
"z": -0.5
90+
},
91+
"positon": { # misspelled key
92+
"x": 6.3,
93+
"y": 2.4,
94+
"z": 99.95
95+
}
96+
}},
97+
{
98+
"Pose": {
99+
"orentation": { # misspelled key
100+
"w": 0.5,
101+
"x": 0,
102+
"y": 0,
103+
"z": 0
104+
},
105+
"position": {
106+
"x": 0,
107+
"y": 0,
108+
"z": 0
109+
}
110+
}
111+
}
112+
])
113+
def test_parse_navvis_origin_bad_input(bad_json_keys_origin, tmp_path):
114+
temp_origin_path = tmp_path / "bad_json_keys_origin.json"
115+
with open(temp_origin_path, 'w') as file:
116+
json.dump(bad_json_keys_origin, file)
117+
assert not origin_parser.parse_navvis_origin_file(temp_origin_path)
118+
os.remove(temp_origin_path)

0 commit comments

Comments
 (0)