Note
Go to the end to download the full example code.
Reorientation Method DM#
This example shows how to use the ReorientationMethodDM algorithm to detect and correct persistent IMU misorientation in lower-back-worn devices.
ReorientationMethodDM corrects the most common mounting errors for
lower-back-worn, flat rectangular sensors. It assumes that one of the large
flat sensor surfaces is mounted against the body and that, under correct
mounting, sensor x points along IS, sensor y points along ML, and sensor z
points along PA. The method corrects 90 deg and 180 deg mounting rotations,
not arbitrary small misalignments.
from __future__ import annotations
Import useful modules and packages
from typing import TYPE_CHECKING
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
from mobgap._gaitmap.utils.rotations import flip_dataset
from mobgap.data import LabExampleDataset
from mobgap.re_orientation import ReorientationMethodDM
from scipy.spatial.transform import Rotation
if TYPE_CHECKING:
from matplotlib.axes import Axes
Loading some example data#
Note
More information about data loading can be found in the data loading example.
We load example data from the lab dataset together with the INDIP reference system.
example_data = LabExampleDataset(
reference_system="INDIP", reference_para_level="wb"
)
Scope and assumptions#
The algorithm groups possible gravity-alignment errors into four orientation families:
is_up: gravity points up in sensor x (correct orientation).is_down: gravity points down in sensor x (180 deg rotation around sensor z).ml_up: gravity points up in sensor y (90 deg rotation around sensor z).ml_down: gravity points down in sensor y (90 deg rotation around sensor z, then 180 deg rotation around sensor x).
Independently of the gravity direction, the sensor can also be flipped
front-to-back around the vertical IS axis. The algorithm estimates this PA
direction from the cross-spectral phase between acc_x and acc_z after
gravity alignment.
These assumptions are only valid for mostly upright walking bouts. Strongly hunched postures, non-walking activities, or very pathological gait patterns can make the reorientation fail.
Choosing a correction mode#
The best reorientation strategy is a good mounting protocol. Knowing the sensor orientation is always better than trying to recover it from the data. If the orientation is known, create and apply the required manual rotation before using mobgap and never add data-driven orientation correction on top.
If orientation errors are still possible, decide at which temporal scale they can occur. If errors can only affect an entire recording or session, try to recover the orientation from protocol information first. For example, operators can take photos of the mounted sensor or record structured mounting notes. If this recovers the orientation, create a manual rotation matrix and apply it to the complete recording before using mobgap.
If a recording-level orientation error cannot be recovered from protocol
information, use the reorientation method as a diagnostic tool. Apply it in
full mode to all detected gait sequences, or run the full pipeline with
reorientation enabled. Then inspect whether an unusually large number of gait
sequences report the same misorientation, and inspect the raw data manually.
Based on that combined evidence, decide whether to rotate the full recording
with a custom orientation matrix.
If orientation errors can occur within a recording, for example because the sensor is loosely attached or participants can remove and reapply it, GS/WB level correction can be appropriate. In this case, choose between the available correction modes:
trust_gravityfirst corrects the gravity alignment. If gravity already points up along sensor x (is_up), it trusts that the mounting orientation is correct and skips the PA-direction correction. This intentionally ignores the possible front-to-back flip within theis_upfamily.fullapplies the PA-direction correction to every walking bout. This can correct the front-to-back flip withinis_up, but it also gives the PA-direction classifier more opportunities to introduce false corrections when most walking bouts were already mounted correctly.
The GS/WB-level mode choice is a prevalence problem. If you simulate all
supported orientations with equal prevalence, full can look better because
every error class is equally likely. In realistic free-living use, however,
the identity orientation is often much more common than any single mounting
error. By intentionally ignoring one potential error case, we can
significantly reduce the likelihood that a correctly oriented sensor is
falsely corrected.
In the current TVS-based analysis, the rough break-even point between
full and trust_gravity is around 33% total misorientation prevalence,
or around 7.5% prevalence for the specific is_up front-back flip that
trust_gravity intentionally ignores. Treat these values as prevalence
guidance, not universal constants.
def _add_decision_box(
ax: Axes,
center: tuple[float, float],
size: tuple[float, float],
text: str,
*,
facecolor: str = "#f7f7f7",
edgecolor: str = "#4b5563",
) -> None:
x, y = center
width, height = size
box = FancyBboxPatch(
(x - width / 2, y - height / 2),
width,
height,
boxstyle="round,pad=0.02,rounding_size=0.02",
facecolor=facecolor,
edgecolor=edgecolor,
linewidth=1.4,
transform=ax.transAxes,
)
ax.add_patch(box)
ax.text(
x,
y,
text,
ha="center",
va="center",
fontsize=9,
linespacing=1.25,
transform=ax.transAxes,
)
def _add_decision_arrow(
ax: Axes,
start: tuple[float, float],
end: tuple[float, float],
label: str | None = None,
label_xy: tuple[float, float] | None = None,
) -> None:
ax.annotate(
"",
xy=end,
xytext=start,
xycoords=ax.transAxes,
textcoords=ax.transAxes,
arrowprops={"arrowstyle": "->", "color": "#374151", "linewidth": 1.4},
)
if label and label_xy is not None:
ax.text(
*label_xy,
label,
ha="center",
va="center",
fontsize=8,
color="#374151",
bbox={"facecolor": "white", "edgecolor": "none", "pad": 1.5},
transform=ax.transAxes,
)
fig, ax = plt.subplots(figsize=(13.5, 8.0))
ax.set_axis_off()
_add_decision_box(
ax,
(0.5, 0.92),
(0.42, 0.1),
"Can you ensure and know\nproper mounting orientation?",
facecolor="#e0f2fe",
)
_add_decision_box(
ax,
(0.22, 0.75),
(0.34, 0.15),
(
"Known orientation\n"
"Apply a manual rotation before mobgap.\n"
"Never add data-driven\n"
"orientation correction."
),
facecolor="#dcfce7",
)
_add_decision_box(
ax,
(0.72, 0.75),
(0.34, 0.13),
("Unknown or potentially wrong\nAt what temporal scale\ncan errors occur?"),
facecolor="#fef3c7",
)
_add_decision_box(
ax,
(0.43, 0.53),
(0.31, 0.13),
(
"Only whole recording/session\n"
"Try to recover orientation\n"
"from protocol information."
),
)
_add_decision_box(
ax,
(0.22, 0.29),
(0.27, 0.16),
(
"Recovered\n"
"Use photos / notes / logs\n"
"to create a manual rotation\n"
"for the full recording."
),
facecolor="#dcfce7",
)
_add_decision_box(
ax,
(0.48, 0.29),
(0.25, 0.18),
(
"Not recoverable\n"
"Diagnostic use:\n"
"run full mode on all GS/WBs,\n"
"inspect repeated same errors\n"
"and raw data, then decide."
),
facecolor="#fef9c3",
)
_add_decision_box(
ax,
(0.79, 0.53),
(0.27, 0.13),
("Within recording possible\nUse GS/WB-level\ncorrection."),
facecolor="#fee2e2",
)
_add_decision_box(
ax,
(0.72, 0.25),
(0.21, 0.18),
(
"Low error prevalence\n"
"or is_up PA flips unlikely\n"
"Use trust_gravity\n"
"to reduce false corrections."
),
facecolor="#fef9c3",
)
_add_decision_box(
ax,
(0.9, 0.25),
(0.15, 0.18),
("PA flips possible\nor high error\nprevalence\nUse full."),
facecolor="#fee2e2",
)
_add_decision_arrow(ax, (0.42, 0.87), (0.29, 0.81), "yes", (0.35, 0.84))
_add_decision_arrow(
ax, (0.58, 0.87), (0.66, 0.81), "no / uncertain", (0.62, 0.84)
)
_add_decision_arrow(
ax,
(0.63, 0.69),
(0.45, 0.59),
"recording-level only",
(0.5, 0.65),
)
_add_decision_arrow(
ax,
(0.8, 0.69),
(0.79, 0.59),
"can change mid-recording",
(0.82, 0.65),
)
_add_decision_arrow(ax, (0.36, 0.47), (0.25, 0.37), "yes", (0.29, 0.42))
_add_decision_arrow(ax, (0.48, 0.47), (0.49, 0.38), "no", (0.5, 0.42))
_add_decision_arrow(ax, (0.76, 0.47), (0.72, 0.34), "lower risk", (0.7, 0.4))
_add_decision_arrow(ax, (0.83, 0.47), (0.9, 0.34), "higher risk", (0.9, 0.4))
fig.tight_layout()
fig.show()

Performance on a single lab trial#
Below we apply the algorithm to a lab trial, where we extract a single walking bout.
The reorientation algorithm expects sensor-frame input and returns body-frame output.
single_test = example_data.get_subset(
cohort="HA", participant_id="001", test="Test11", trial="Trial1"
)
reference_wbs = single_test.reference_parameters_.wb_list
sampling_rate_hz = single_test.sampling_rate_hz
# Including only 1 WB as the example
start = reference_wbs.iloc[2]["start"]
end = reference_wbs.iloc[2]["end"]
imu_data = single_test.data.get("LowerBack")
imu_data = imu_data.reset_index(drop=True)
first_wb = imu_data.loc[start:end].copy()
Introducing artificial misorientation#
To demonstrate the algorithm, we artificially introduce a misorientation.
Below we rotate the sensor frame by 180 degrees around the sensor z-axis
(is_down).
first_wb = flip_dataset(first_wb, Rotation.from_euler("z", 180, degrees=True))
print(first_wb)
acc_x acc_y acc_z gyr_x gyr_y gyr_z
3853 -9.063879 1.950392 -2.698835 -103.430341 -4.274265 -41.917592
3854 -9.299348 1.808525 -2.505942 -101.356234 -8.731877 -43.029130
3855 -9.544249 1.662204 -2.348470 -98.319558 -13.819742 -44.106291
3856 -9.610913 1.419515 -2.283629 -96.875704 -17.933579 -43.716680
3857 -9.598021 1.340494 -2.252059 -97.654927 -19.537861 -43.304150
... ... ... ... ... ... ...
5081 -9.108987 0.367603 0.026448 -53.181943 30.550110 1.604282
5082 -9.004980 0.281791 -0.018345 -50.489041 30.160498 0.561499
5083 -8.968239 0.251924 -0.323411 -49.652523 32.039800 0.687549
5084 -9.135023 0.414165 -0.945541 -50.844275 37.070369 0.928192
5085 -9.307359 0.753164 -1.742972 -52.036027 43.602088 1.283425
[1233 rows x 6 columns]
Visualising the misoriented walking bout#
We can visualise the walking bout before correction.

Applying the reorientation algorithm#
Below we apply the ReorientationMethodDM algorithm to the misoriented walking
bout.
We use trust_gravity here to demonstrate the low-prevalence mode. For real
data, choose the mode based on the decision tree above.
reoriented = ReorientationMethodDM(
correction_mode="trust_gravity"
).detect_correct(first_wb, sampling_rate_hz=sampling_rate_hz)
print(f"\nDetected orientation family: {reoriented.result_.family}")
print(f"Correction applied: {reoriented.result_.correction_applied}")
print(f"Correction action: {reoriented.result_.correction_action}")
corrected = reoriented.corrected_data_
Detected orientation family: is_down
Correction applied: True
Correction action: rotated 180 deg around sensor z-axis
Visualising the corrected walking bout#
After correction, we can access the corrected data via the corrected_data_
attribute.

Total running time of the script: (0 minutes 2.433 seconds)
Estimated memory usage: 86 MB