Skip to content

Commit 2330ed8

Browse files
authored
Added stationary position error plot support. (#325)
# New Features - Added `--reference` option to plot pose error compared with a known truth source - Currently supports stationary position error only
2 parents 47cb284 + c9fc070 commit 2330ed8

File tree

1 file changed

+52
-10
lines changed

1 file changed

+52
-10
lines changed

python/fusion_engine_client/analysis/analyzer.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,8 @@ def plot_solution_type(self):
724724

725725
self._add_figure(name="solution_type", figure=figure, title="Solution Type")
726726

727-
def _plot_displacement(self, source, time, solution_type, displacement_enu_m, std_enu_m):
727+
def _plot_displacement(self, source, time, solution_type, displacement_enu_m, std_enu_m,
728+
title='Displacement'):
728729
"""!
729730
@brief Generate a topocentric (top-down) plot of position displacement, as well as plot of displacement over
730731
time.
@@ -734,7 +735,7 @@ def _plot_displacement(self, source, time, solution_type, displacement_enu_m, st
734735

735736
# Setup the figure.
736737
topo_figure = make_subplots(rows=1, cols=1, print_grid=False, shared_xaxes=False,
737-
subplot_titles=['Displacement'])
738+
subplot_titles=[title])
738739
topo_figure['layout']['xaxis1'].update(title="East (m)")
739740
topo_figure['layout']['yaxis1'].update(title="North (m)")
740741

@@ -743,10 +744,10 @@ def _plot_displacement(self, source, time, solution_type, displacement_enu_m, st
743744
time_figure['layout'].update(showlegend=True, modebar_add=['v1hovermode'])
744745
for i in range(4):
745746
time_figure['layout']['xaxis%d' % (i + 1)].update(title=self.p1_time_label, showticklabels=True)
746-
time_figure['layout']['yaxis1'].update(title="Displacement (m)")
747-
time_figure['layout']['yaxis2'].update(title="Displacement (m)")
748-
time_figure['layout']['yaxis3'].update(title="Displacement (m)")
749-
time_figure['layout']['yaxis4'].update(title="Displacement (m)")
747+
time_figure['layout']['yaxis1'].update(title=f"{title} (m)")
748+
time_figure['layout']['yaxis2'].update(title=f"{title} (m)")
749+
time_figure['layout']['yaxis3'].update(title=f"{title} (m)")
750+
time_figure['layout']['yaxis4'].update(title=f"{title} (m)")
750751

751752
# Remove invalid solutions.
752753
valid_idx = np.logical_and(~np.isnan(time), solution_type != SolutionType.Invalid)
@@ -815,13 +816,25 @@ def _plot_data(name, idx, marker_style=None):
815816

816817
name = source.replace(' ', '_').lower()
817818
self._add_figure(name=f"{name}_top_down", figure=topo_figure, title=f"{source}: Top-Down (Topocentric)")
818-
self._add_figure(name=f"{name}_displacement", figure=time_figure, title=f"{source}: vs. Time")
819+
self._add_figure(name=f"{name}_vs_time", figure=time_figure, title=f"{source}: vs. Time")
820+
821+
def plot_stationary_position_error(self, truth_lla_deg):
822+
"""!
823+
@brief Plot position error vs. a known stationary location.
824+
825+
@param truth_lla_deg The truth LLA location (in degrees/meters).
826+
"""
827+
truth_ecef_m = np.array(geodetic2ecef(*truth_lla_deg, deg=True))
828+
self._plot_pose_displacement(title='Position Error', center_ecef_m=truth_ecef_m)
819829

820830
def plot_pose_displacement(self):
821831
"""!
822832
@brief Generate a topocentric (top-down) plot of position displacement, as well as plot of displacement over
823833
time.
824834
"""
835+
self._plot_pose_displacement()
836+
837+
def _plot_pose_displacement(self, title='Pose Displacement', center_ecef_m=None):
825838
if self.output_dir is None:
826839
return
827840

@@ -847,12 +860,18 @@ def plot_pose_displacement(self):
847860
# Convert to ENU displacement with respect to the median position (we use median instead of centroid just in
848861
# case there are one or two huge outliers).
849862
position_ecef_m = np.array(geodetic2ecef(lat=lla_deg[0, :], lon=lla_deg[1, :], alt=lla_deg[2, :], deg=True))
850-
center_ecef_m = np.median(position_ecef_m, axis=1)
863+
864+
if center_ecef_m is None:
865+
center_ecef_m = np.median(position_ecef_m, axis=1)
866+
851867
displacement_ecef_m = position_ecef_m - center_ecef_m.reshape(3, 1)
852868
c_enu_ecef = get_enu_rotation_matrix(*lla_deg[0:2, 0], deg=True)
853869
displacement_enu_m = c_enu_ecef.dot(displacement_ecef_m)
854870

855-
self._plot_displacement('Pose Displacement', time, solution_type, displacement_enu_m, std_enu_m)
871+
axis_title = 'Error' if title == 'Position Error' else 'Displacement'
872+
873+
self._plot_displacement(source=title, title=axis_title, time=time, solution_type=solution_type,
874+
displacement_enu_m=displacement_enu_m, std_enu_m=std_enu_m)
856875

857876
def plot_relative_position(self):
858877
"""!
@@ -882,7 +901,7 @@ def plot_relative_position(self):
882901
displacement_enu_m = relative_position_data.relative_position_enu_m[:, valid_idx]
883902
std_enu_m = relative_position_data.position_std_enu_m[:, valid_idx]
884903

885-
self._plot_displacement('Relative Position vs.Base Station', time, solution_type, displacement_enu_m, std_enu_m)
904+
self._plot_displacement('Position vs. Base Station', time, solution_type, displacement_enu_m, std_enu_m)
886905

887906
def plot_map(self, mapbox_token):
888907
"""!
@@ -2787,6 +2806,11 @@ def main():
27872806
"\n"
27882807
"\nTruncation is disabled if --plot is specified." %
27892808
(Analyzer.LONG_LOG_DURATION_SEC / 3600.0, Analyzer.HIGH_MEASUREMENT_RATE_HZ))
2809+
plot_group.add_argument(
2810+
'--reference', '--truth',
2811+
help="Specify a reference data to use as a truth source for position, velocity, and orientation. Supported "
2812+
"formats:"
2813+
"\n- Stationary LLA position: 37.1234, -122.526335, 102.34")
27902814

27912815
plot_function_names = [n for n in dir(Analyzer) if n.startswith('plot_')]
27922816
plot_group.add_argument(
@@ -2899,6 +2923,16 @@ def main():
28992923
_logger.error('Source identifiers must be integers. Exiting.')
29002924
sys.exit(1)
29012925

2926+
# Parse truth data if specified.
2927+
truth_lla_deg = None
2928+
if options.reference is not None:
2929+
m = re.match(r'^(-?\d+(?:\.\d+)),\s*(-?\d+(?:\.\d+)),\s*(-?\d+(?:\.\d+))$', options.reference)
2930+
if m:
2931+
truth_lla_deg = np.array((float(m.group(1)), float(m.group(2)), float(m.group(3))))
2932+
else:
2933+
_logger.error('Unrecognized reference data format.')
2934+
sys.exit(1)
2935+
29022936
# Read pose data from the file.
29032937
analyzer = Analyzer(file=input_path, output_dir=output_dir, ignore_index=options.ignore_index,
29042938
prefix=options.prefix + '.' if options.prefix is not None else '',
@@ -2927,6 +2961,9 @@ def main():
29272961
# LG69T-AH), separate from other sensor measurements controlled by --measurements.
29282962
analyzer.plot_heading_measurements()
29292963

2964+
if truth_lla_deg is not None:
2965+
analyzer.plot_stationary_position_error(truth_lla_deg)
2966+
29302967
if options.measurements:
29312968
analyzer.plot_imu()
29322969
analyzer.plot_wheel_data()
@@ -2960,6 +2997,11 @@ def main():
29602997
analyzer.plot_map(mapbox_token=options.mapbox_token)
29612998
elif func == 'plot_skyplot':
29622999
analyzer.plot_gnss_skyplot(decimate=False)
3000+
elif func == 'plot_stationary_position_error':
3001+
if truth_lla_deg is not None:
3002+
analyzer.plot_stationary_position_error(truth_lla_deg)
3003+
else:
3004+
_logger.warning('No truth data available. Cannot plot position error.')
29633005
else:
29643006
getattr(analyzer, func)()
29653007

0 commit comments

Comments
 (0)