.. _demo_imu2body-label: ========================================= IMU-frame to body-frame transformations ========================================= Consider loading the :py:mod:`logging` module and setting up a logger to monitor progress: .. jupyter-execute:: # Set up import importlib.resources as rsrc import numpy as np import pandas as pd import matplotlib.pyplot as plt import skdiveMove.imutools as imutools from scipy.spatial.transform import Rotation as R from skdiveMove.imutools.imu import (_ACCEL_NAME, _OMEGA_NAME, _MAGNT_NAME) from skdiveMove.imutools.imu2body import (_LEG3X1_ANCHOR, _euler_ctr2body) _FIG1X1 = (11, 5) _FIG3D1X1 = (11, 8) _FIG3X1 = (11, 11) _FIG4X1 = (10, 10) pd.set_option("display.precision", 3) np.set_printoptions(precision=3, sign="+") %matplotlib inline Load saved pre-processed (properly calibrated and in a right-handed, front-left-up frame) data: .. jupyter-execute:: :linenos: icdf = (rsrc.files("skdiveMove") / "tests" / "data" / "gertrude" / "gert_imu_frame.nc") icsv = (rsrc.files("skdiveMove") / "tests" / "data" / "gertrude" / "gert_long_srfc.csv") Set up an instance to use throughout: .. jupyter-execute:: :linenos: # If data are in NetCDF and CSV files, one can take advantage of the # class method `IMU2Body.from_csv_nc` to instantiate directly from # the file names: imu2whale = imutools.IMU2Body.from_csv_nc(icsv, icdf, savgol_parms=(99, 2)) # Print summary info print(imu2whale) # Choose an index from surface details table idx = imu2whale.surface_details.index[33] idx_title = idx.strftime("%Y%m%dT%H%M%S") Visualize low-pass filter (Savitzky-Golay filter; :func:`~scipy.signal.savgol_filter`) job: .. jupyter-execute:: :hide-code: fig, axs = plt.subplots(3, 1, figsize=(12, 12), sharex=True) for i, ax in enumerate(axs): (imu2whale.get_surface_vectors(idx, _ACCEL_NAME)[:, i] .plot.line(ax=axs[i], label="measured")) (imu2whale.get_surface_vectors(idx, _ACCEL_NAME, smoothed_accel=True)[:, i] .plot.line(ax=axs[i], linestyle="--", label="SG filtered")) axs[i].set_xlabel("") axs[i].set_title("") axs[0].set_title(idx_title) axs[0].margins(x=0) axs[-1].tick_params(labelrotation=0, which="both") leg = axs[2].legend(loc=9, bbox_to_anchor=_LEG3X1_ANCHOR, frameon=False, borderaxespad=0, ncol=2) leg.get_texts()[0].set_text("measured") leg.get_texts()[1].set_text("SG filtered"); Quick plots of smoothed acceleration and magnetic density from the segment: .. jupyter-execute:: :linenos: acc_imu = imu2whale.get_surface_vectors(idx, _ACCEL_NAME, smoothed_accel=True) depth = imu2whale.get_surface_vectors(idx, "depth") # Alternatively, use the function of the same name as method below ax = imu2whale.scatterIMU3D(idx, _MAGNT_NAME, normalize=True, animate=False, figsize=_FIG3D1X1) ax.view_init(azim=-30); Below shows that the IMU was deployed facing forward and on the left side of the whale, so in the above plot negative `x` is forward and negative `y` is left as per our right-handed coordinate system. As above, we can use the method of the same name to produce the plot: .. jupyter-execute:: imu2whale.tsplotIMU_depth(_ACCEL_NAME, idx, smoothed_accel=True, figsize=_FIG4X1); Calculate orientation for the segment above, and produce an animated plot of the orientation. This can be done in a single step with :meth:`imutools.IMU2Body.get_orientation`. .. jupyter-execute:: :linenos: :hide-output: Rctr2i, svd = imu2whale.get_orientation(idx, plot=False, animate=False) anim_file = "source/.static/video/gert_imu_{}.mp4".format(idx_title) imutools.scatterIMU_svd(acc_imu, svd, Rctr2i, normalize=True, center=True, animate=True, animate_file=anim_file, title="IMU-Frame Centered Acceleration [g]", figsize=_FIG3D1X1) .. raw:: html Apply the inverse transformation to get to the animal frame: .. jupyter-execute:: :linenos: # Orient the surface segment using the estimated rotation imu_bodyi = imu2whale.orient_surfacing(idx, Rctr2i) # Have a look at corrected acceleration acci = imu_bodyi[_ACCEL_NAME] An animation may be useful to visualize the normalized animal-frame data: .. jupyter-execute:: :linenos: :hide-output: anim_file = "source/.static/video/gert_body_{}.mp4".format(idx_title) imutools.scatterIMU3D(acci, imu_bodyi["depth"].to_numpy().flatten(), normalize=True, animate=True, animate_file=anim_file, title=r"Normalized Animal-Frame Acceleration [$g$]", cbar_label="Depth [m]", figsize=_FIG3D1X1) .. raw:: html Obtain orientations for all surface periods, and retrieve the quality indices for each estimate: .. jupyter-execute:: orientations = imu2whale.get_orientations() # Unpack quality of the estimates qual = orientations["quality"].apply(pd.Series) qual.columns = ["q_index", "phi_std"] Look at the quality of the orientation estimates: .. jupyter-execute:: :hide-code: # Plot the quality index fig = plt.figure(figsize=_FIG1X1) ax = fig.add_subplot(111) ax.set_ylabel("Quality index") qual["q_index"].plot(ax=ax, style="-o", rot=0) qual["phi_std"].plot(ax=ax, style="-o", rot=0) ax.axhline(0.05, linestyle="dashed", color="k") ax.set_xlabel("") leg = ax.legend(loc=9, bbox_to_anchor=(0.5, -0.08), frameon=False, borderaxespad=0, ncol=2) leg.get_texts()[0].set_text("quality index") leg.get_texts()[1].set_text("rolling std") Remove bad quality estimates: .. jupyter-execute:: :linenos: imu2whale.filter_surfacings((0.04, 0.06)) Plot "ok" Euler angles: .. jupyter-execute:: :hide-code: fig, axs = plt.subplots(3, 1, sharex=True, figsize=_FIG3X1) (imu2whale.orientations[["phi", "theta", "psi"]] .plot(ax=axs, style="o-", subplots=True, legend=False, rot=0)) axs[0].set_ylabel(r"$\phi$ [deg]") axs[1].set_ylabel(r"$\theta$ [deg]") axs[2].set_ylabel(r"$\psi$ [deg]") for ax in axs: ax.axhline(0, linestyle="dashed", color="k") # Summary imu2whale.orientations[["phi", "theta", "psi"]].describe() Can we use an average (median?) rotation matrix? This requires retrieving the direction cosine matrices of the centered data, which can be expressed as Euler angles with respect to the centered-data frame: .. jupyter-execute:: :linenos: euler_xyz = (imu2whale.orientations["R"] .apply(lambda x: x.as_euler("XYZ", degrees=True)) .apply(pd.Series)) euler_xyz.rename(columns={0: "phi_ctr", 1: "theta_ctr", 2: "psi_ctr"}, inplace=True) euler_avg = euler_xyz.mean() Rctr2i_avg = R.from_euler("XYZ", euler_avg.values, degrees=True) Rctr2i_avg.as_euler("XYZ", degrees=True) _euler_ctr2body(Rctr2i_avg) Check the effect of using this common transformation with the first remaining period: .. jupyter-execute:: :linenos: idx0 = imu2whale.surface_details.index[0] imu_bodyi = imu2whale.orient_surfacing(idx0, Rctr2i_avg) # Plot the time series; not bad imutools.tsplotIMU_depth(imu_bodyi[_ACCEL_NAME], imu_bodyi["depth"], figsize=_FIG4X1); Orient all surface periods with average rotation -- note we get a hierarchical dataframe output: .. jupyter-execute:: :linenos: imu_bodys = imu2whale.orient_surfacings(R_all=Rctr2i_avg) # Check out plot of a random sample idxs = imu_bodys.coords.get("endasc") # values in topmost level idx_rnd = idxs[np.random.choice(idxs.size)] idx_rnd_title = idx_rnd.dt.strftime("%Y%m%dT%H%M%S").item() # Compare with period-specific orientation Rctr2i = imu2whale.orientations.loc[idx_rnd.to_pandas()]["R"] imu_bodyi = imu2whale.orient_surfacing(idx_rnd.to_pandas(), Rctr2i) .. jupyter-execute:: :hide-code: fig, axs = plt.subplots(3, 1, sharex=True, figsize=_FIG3X1) axs[0].set_title(idx_rnd_title) for i, ax in enumerate(axs): (imu_bodyi[_ACCEL_NAME][:, i] .plot.line(x="timestamp", ax=ax, label="segment-specific")) (imu_bodys.sel(endasc=idx_rnd)[_ACCEL_NAME][:, i] .plot.line(x="timestamp", ax=ax, linestyle="dashed", label="common")) axs[i].set_xlabel("") if i < 2: hlev = 0 else: hlev = 1 ax.axhline(hlev, linestyle="dashed", color="k") axs[0].margins(x=0) axs[-1].tick_params(labelrotation=0, which="both") leg = axs[2].legend(loc=9, bbox_to_anchor=_LEG3X1_ANCHOR, frameon=False, borderaxespad=0, ncol=2); Orient the entire :class:`~imutools.IMU2Body` object with common rotation: .. jupyter-execute:: gert_frame = imu2whale.orient_IMU(Rctr2i_avg) Or with segment-specific rotations: .. jupyter-execute:: :linenos: gert_frames = imu2whale.orient_IMU() # Check out IMUs idx = imu2whale.surface_details.index[1] imu_i = gert_frames.sel(endasc=idx) imutools.tsplotIMU_depth(imu_i[_ACCEL_NAME], imu_i["depth"], figsize=_FIG4X1); Feel free to download a copy of this demo (:jupyter-download-script:`demo_imu2body`).