.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/turning/_01_td_elgohary.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_turning__01_td_elgohary.py: ElGohary Turning Algo ===================== .. warning:: The ElGohary turning algorithm could not be properly validated as part of the Mobilise-D project, as no clear consensus exists what a "turn" is. This makes it difficult to design a suitable reference system, which nature of calculating turns does not bias the results fundamentally. This example shows how to use the ElGohary turning algorithm. It uses the angular velocity around the SI axis of a lower back IMU to detect turns. .. GENERATED FROM PYTHON SOURCE LINES 15-29 Loading data ------------ .. note :: More infos about data loading can be found in the :ref:`data loading example `. We load example data from the lab dataset together with the Stereophoto reference system. We will use the Stereophoto output for turns ("td") as ground truth, as it is the most accurate reference system available in the dataset. Still, the turn detection might not be fully reliable. Note, that the INDIP system, also uses just a single lower back IMU to calculate the turns. Hence, it can not really be considered a reference system, in this context. The turn algorithm requires the data to be in the body frame (see the User Guide on this topic). We use the ``to_body_frame`` function to transform the data, as we know that the sensor was well aligned, with the expected sensor frame conventions. .. GENERATED FROM PYTHON SOURCE LINES 29-44 .. code-block:: default from mobgap.data import LabExampleDataset from mobgap.utils.conversions import to_body_frame example_data = LabExampleDataset( reference_system="Stereophoto", reference_para_level="wb" ) single_test = example_data.get_subset( cohort="HA", participant_id="001", test="Test11", trial="Trial1" ) imu_data = to_body_frame(single_test.data_ss) reference_wbs = single_test.reference_parameters_.wb_list sampling_rate_hz = single_test.sampling_rate_hz .. GENERATED FROM PYTHON SOURCE LINES 45-47 Note, that the reference turns don't use the 45 deg lower cutoff for turns by default. Hence, we apply this here for consistency. .. GENERATED FROM PYTHON SOURCE LINES 47-51 .. code-block:: default ref_turns = single_test.reference_parameters_.turn_parameters.query( "angle_deg.abs() >= 45" ) .. GENERATED FROM PYTHON SOURCE LINES 52-61 Applying the algorithm ---------------------- In a typical pipeline, we first identify the gait sequences and then apply the turning detection algorithm to each gait sequence individually. However, the turning algorithm can also be applied to the whole recording at once. Though, it might produce false positives, in "non-walking" segments. Below we show both approaches, starting with the whole recording. This allows us to visualize how the algorithm works. .. GENERATED FROM PYTHON SOURCE LINES 61-69 .. code-block:: default from mobgap.turning import TdElGohary turning_detector = TdElGohary() turning_detector.detect(imu_data, sampling_rate_hz=sampling_rate_hz) turn_list = turning_detector.turn_list_ turn_list .. raw:: html
start end duration_s angle_deg direction
turn_id
0 2728 2958 2.30 -115.475805 right
1 3740 3932 1.92 103.987027 left
2 3938 4160 2.22 -127.839317 right
3 4167 4544 3.77 165.759383 left
4 4554 4799 2.45 -155.613733 right
5 7779 8136 3.57 -149.787554 right
6 8401 8657 2.56 103.291685 left
7 9267 9622 3.55 -157.879151 right
8 9771 10153 3.82 121.471579 left
9 12402 12686 2.84 80.281528 left
10 13024 13242 2.18 -56.923185 right
11 13330 13606 2.76 -174.197791 right


.. GENERATED FROM PYTHON SOURCE LINES 70-73 We can also extract additional debug information from the algorithm. The yaw-angle gives us the estimated orientation of the lower back in the axis of the turning. The ``raw_turn_list_`` gives us the raw detected turns, before filtering them based on duration and angle. .. GENERATED FROM PYTHON SOURCE LINES 73-78 .. code-block:: default yaw_angle = turning_detector.yaw_angle_ raw_turns = turning_detector.raw_turn_list_ raw_turns .. raw:: html
start end duration_s angle_deg direction center
turn_id
0 971 1154 1.83 -32.498510 right 1060
1 1797 1922 1.25 -17.743185 right 1858
2 2728 2958 2.30 -115.475805 right 2860
3 2969 3051 0.82 24.768106 left 3005
4 3192 3348 1.56 37.216817 left 3287
5 3361 3485 1.24 -27.285854 right 3415
6 3740 3932 1.92 103.987027 left 3853
7 3938 4160 2.22 -127.839317 right 4039
8 4167 4544 3.77 165.759383 left 4232
9 4167 4544 3.77 165.759383 left 4414
10 4554 4799 2.45 -155.613733 right 4668
11 4906 5001 0.95 -9.446011 right 4957
12 5020 5156 1.36 22.854312 left 5078
13 7627 7763 1.36 19.440286 left 7703
14 7779 8136 3.57 -149.787554 right 7985
15 8247 8392 1.45 -39.650341 right 8328
16 8401 8657 2.56 103.291685 left 8486
17 8671 8831 1.60 -37.328372 right 8743
18 9126 9236 1.10 28.357114 left 9177
19 9267 9622 3.55 -157.879151 right 9437
20 9771 10153 3.82 121.471579 left 9974
21 10518 10801 2.83 39.876258 left 10737
22 10820 10947 1.27 -17.846604 right 10880
23 10970 11142 1.72 37.359197 left 11063
24 11159 11266 1.07 -20.853315 right 11209
25 12202 12391 1.89 38.349268 left 12306
26 12402 12686 2.84 80.281528 left 12517
27 12733 12920 1.87 -34.607109 right 12831
28 13024 13242 2.18 -56.923185 right 13123
29 13330 13606 2.76 -174.197791 right 13464
30 13622 13759 1.37 18.128614 left 13691


.. GENERATED FROM PYTHON SOURCE LINES 79-89 To better understand, how things work, we can plot all the results together. We can see that after filtering the signal, the algorithm identifies peaks in the signal that are higher in absolute values than the ``min_peak_angle_velocity_dps`` (dotted lines). Around these "turn-centers" the turn is defined as the region until the signal drops below the ``lower_threshold_velocity_dps`` (dashed lines). We then look at the duration and the angle of the detected turns and filter them based on the provided thresholds. We can see that at the end, only a small number of turns remain. Most of the raw turns are filtered out by the ``allowed_turn_angle_deg`` threshold, which is set to 45 degrees. .. GENERATED FROM PYTHON SOURCE LINES 89-175 .. code-block:: default import matplotlib.pyplot as plt def plot_turns(algo_with_results: TdElGohary): fig, axs = plt.subplots(3, 1, figsize=(10, 6), sharex=True) axs[0].set_ylabel("gyr_is [dps]") if algo_with_results.global_frame_data_ is None: data = algo_with_results.data axs[0].set_title("Raw gyr_is data") axis = "gyr_is" else: data = algo_with_results.global_frame_data_ axs[0].set_title("Raw gyr_is data (global frame)") axis = "gyr_gis" data.reset_index(drop=True).plot(y=axis, ax=axs[0]) axs[1].set_title("Filtered IMU signal with raw turns and thresholds.") axs[1].set_ylabel("filtered gyr_is [dps]") filtered_data = ( algo_with_results.smoothing_filter.clone() .filter(data[axis], sampling_rate_hz=sampling_rate_hz) .filtered_data_ ) filtered_data.reset_index(drop=True).plot(ax=axs[1]) raw_turn_list = algo_with_results.raw_turn_list_ # Plot turn centeres axs[1].plot( raw_turn_list["center"], filtered_data.iloc[raw_turn_list["center"]], "o", ) # Plot start and end of turns as regions for i, row in raw_turn_list.iterrows(): axs[1].axvspan( row["start"], row["end"], alpha=0.5, color="gray" if row["direction"] == "left" else "blue", ) # Draw dashed line at +/- velocity_dps axs[1].axhline( algo_with_results.lower_threshold_velocity_dps, color="green", linestyle="--", label="velocity_dps", ) axs[1].axhline( -algo_with_results.lower_threshold_velocity_dps, color="gray", linestyle="--", ) # Draw dottet line at +/- height axs[1].axhline( algo_with_results.min_peak_angle_velocity_dps, color="green", linestyle=":", label="height", ) axs[1].axhline( -algo_with_results.min_peak_angle_velocity_dps, color="gray", linestyle=":", ) axs[2].set_title("Yaw angle with final turns") axs[2].set_ylabel("Yaw angle [deg]") axs[2].set_xlabel("samples [#]") algo_with_results.yaw_angle_.reset_index(drop=True).plot(ax=axs[2]) # Plot start and end of turns as regions for i, row in algo_with_results.turn_list_.iterrows(): axs[2].axvspan( row["start"], row["end"], alpha=0.5, color="gray" if row["direction"] == "left" else "blue", ) fig.show() plot_turns(turning_detector) .. image-sg:: /auto_examples/turning/images/sphx_glr__01_td_elgohary_001.png :alt: Raw gyr_is data, Filtered IMU signal with raw turns and thresholds., Yaw angle with final turns :srcset: /auto_examples/turning/images/sphx_glr__01_td_elgohary_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 176-179 Now that we understand how the algorithm works, we apply it in the context of a typical pipeline using the ``GsIterator`` in combination with the reference WBs. This allows us to compare the results to the reference system. .. GENERATED FROM PYTHON SOURCE LINES 179-192 .. code-block:: default from mobgap.pipeline import GsIterator iterator = GsIterator() for (gs, data), result in iterator.iterate(imu_data, reference_wbs): td = turning_detector.clone().detect( data, sampling_rate_hz=sampling_rate_hz ) result.turn_list = td.turn_list_ turns = iterator.results_.turn_list turns .. raw:: html
start end duration_s angle_deg direction
wb_id turn_id
1 0 3833 3931 0.98 76.820978 left
1 3937 4160 2.23 -127.870142 right
2 4167 4544 3.77 165.759383 left
3 4554 4803 2.49 -155.695780 right
2 0 7777 8136 3.59 -149.706194 right
3 0 9381 9628 2.47 -139.641274 right


.. GENERATED FROM PYTHON SOURCE LINES 193-194 We can compare this to the reference turns. .. GENERATED FROM PYTHON SOURCE LINES 194-196 .. code-block:: default ref_turns .. raw:: html
start end duration_s angle_deg direction
wb_id turn_id
1 0 3833 3935 1.01 81.339600 left
1 3934 4172 2.37 -122.848641 right
2 4277 4452 1.74 111.712823 left
3 4534 4789 2.54 -178.884837 right
2 0 7777 8204 4.26 -153.358653 right
1 8405 8552 1.46 68.104292 left
3 0 9381 9714 3.32 -166.100777 right
5 0 13108 13245 1.36 -52.132837 right
1 13274 13443 1.68 -61.836477 right


.. GENERATED FROM PYTHON SOURCE LINES 197-199 The results look reasonable, but we can see that in gs 3 and 5 the algorithm was not able to detect the smaller turns with lower turning angles. .. GENERATED FROM PYTHON SOURCE LINES 201-217 Working in the global coordinate system --------------------------------------- The original ElGohary paper uses on-board sensor fusion to track the orientation of the sensor. This information is used to transform all sensor data into the global coordinate system. This should make the identification of the inferior-superior (IS) axis more robust. We did not use this approach within Mobilise-D, to avoid introducing an additional source of error through a sensor fusion algorithm. However, the result of the algorithms can be significantly influenced by that decision, in particular if participants have a lot of body sway of walk bend forward. Below we show two approached on how you could use the algorithm with a global frame estimation. 1. Using the internal global frame estimation --------------------------------------------- For this we pass an instance of the MadgwickAHRS algorithm to estimate the global orientation of the sensor to the algorithm. .. GENERATED FROM PYTHON SOURCE LINES 217-221 .. code-block:: default from mobgap.orientation_estimation import MadgwickAHRS orientation_estimator = MadgwickAHRS() .. GENERATED FROM PYTHON SOURCE LINES 222-224 Now we can apply the algorithm again. We are going to apply it to the entire recording to show the step-by-step process. .. GENERATED FROM PYTHON SOURCE LINES 224-231 .. code-block:: default turning_detector_global = TdElGohary( orientation_estimation=orientation_estimator ) turning_detector_global.detect(imu_data, sampling_rate_hz=sampling_rate_hz) plot_turns(turning_detector_global) .. image-sg:: /auto_examples/turning/images/sphx_glr__01_td_elgohary_002.png :alt: Raw gyr_is data (global frame), Filtered IMU signal with raw turns and thresholds., Yaw angle with final turns :srcset: /auto_examples/turning/images/sphx_glr__01_td_elgohary_002.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 232-237 Based on the plotted results, we can see that the algorithm identifies basically the same turns as before. The same can be observed, when applying the algorithm per GS. The turns are almost identical as before, indicating, that the global frame transformation was not really required for this specific dataset. .. GENERATED FROM PYTHON SOURCE LINES 237-248 .. code-block:: default iterator = GsIterator() for (gs, data), result in iterator.iterate(imu_data, reference_wbs): td = turning_detector.clone().detect( data, sampling_rate_hz=sampling_rate_hz ) result.turn_list = td.turn_list_ turns_global_per_gs = iterator.results_.turn_list turns_global_per_gs .. raw:: html
start end duration_s angle_deg direction
wb_id turn_id
1 0 3833 3931 0.98 76.820978 left
1 3937 4160 2.23 -127.870142 right
2 4167 4544 3.77 165.759383 left
3 4554 4803 2.49 -155.695780 right
2 0 7777 8136 3.59 -149.706194 right
3 0 9381 9628 2.47 -139.641274 right


.. GENERATED FROM PYTHON SOURCE LINES 249-260 Alternatively, we could also use the global frame estimation to transform the data into the global frame before applying the algorithm. 2. Transforming the data into the global frame ---------------------------------------------- For this, we first estimate the global frame for the entire recording and then put the rotated data into the Gait Sequence Iterator. Theoretically, this should yield slightly better results, as the Madgwick algorithm always needs a certain amount of time to converge to the correct orientation. By running it once over the entire data, these convergence period should only happen once at the beginning of the entire recording and not affect the start of each gait sequence. .. GENERATED FROM PYTHON SOURCE LINES 260-276 .. code-block:: default ori_estimator = orientation_estimator.clone().estimate( imu_data, sampling_rate_hz=sampling_rate_hz ) imu_data_global = ori_estimator.rotated_data_ iterator = GsIterator() for (gs, data), result in iterator.iterate(imu_data_global, reference_wbs): td = turning_detector.clone().detect( data, sampling_rate_hz=sampling_rate_hz ) result.turn_list = td.turn_list_ turns_global = iterator.results_.turn_list turns_global .. raw:: html
start end duration_s angle_deg direction
wb_id turn_id
1 0 3833 3933 1.00 80.306769 left
1 3939 4162 2.23 -132.738788 right
2 4169 4545 3.76 172.303622 left
3 4555 4803 2.48 -167.290717 right
2 0 7777 8137 3.60 -157.465603 right
3 0 9381 9628 2.47 -145.699818 right


.. GENERATED FROM PYTHON SOURCE LINES 277-279 .. code-block:: default ref_turns .. raw:: html
start end duration_s angle_deg direction
wb_id turn_id
1 0 3833 3935 1.01 81.339600 left
1 3934 4172 2.37 -122.848641 right
2 4277 4452 1.74 111.712823 left
3 4534 4789 2.54 -178.884837 right
2 0 7777 8204 4.26 -153.358653 right
1 8405 8552 1.46 68.104292 left
3 0 9381 9714 3.32 -166.100777 right
5 0 13108 13245 1.36 -52.132837 right
1 13274 13443 1.68 -61.836477 right


.. GENERATED FROM PYTHON SOURCE LINES 280-282 Which of the two approaches is better, depends on multiple factors. If the global frame should be used generally, further investigation is needed. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 3.557 seconds) **Estimated memory usage:** 13 MB .. _sphx_glr_download_auto_examples_turning__01_td_elgohary.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: _01_td_elgohary.py <_01_td_elgohary.py>` .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: _01_td_elgohary.ipynb <_01_td_elgohary.ipynb>` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_