The Dishwasher Demo
This notebook provides a straightforward overview of ergonomic analysis for unloading a dishwasher, using sensor data to detect bending periods and evaluate posture quality. The data featured here is from a person unloading a dishwasher for 1 minute, measured at a frequency of 25 Hz.
The data analysis includes:
- Loading and processing time series data for body angles and movement.
- Detecting “bending forward” actions
- Scoring posture quality during each bending period based on lumbar angle and torso twist.
# flexlib is a python library written by Minktec to facilitate easy flextail data processing
import flexlib
from flexlib import MeasurementEvaluationMetric as mem
from flexlib import CountingPredicateSchmittTrigger, SchmittState
import seaborn as sns # plotting
import matplotlib.pyplot as plt # plotting
import pandas as pd # dataframes / tables
import numpy as np # math
After importing the libraries, we can load the data recorded with the FlexTail-Sensor.
The create_dataframe
method takes the raw values and computes metrics derived from the sensor data. This makes it easy to work with the sensor data. If more complicated evaluations are necessary, the sensor angles or coordinates can also be computed.
measurements : flexlib.AnnotatedRecording = flexlib.FlexReader().parse("./test_data/dishwasher.rsf")
df : pd.DataFrame = flexlib.create_dataframe(measurements)
df
Time | lumbarAngle | twist | lateral | sagital | thoracicAngle | acceleration | gyro | |
---|---|---|---|---|---|---|---|---|
0 | 2024-12-10 08:30:19.051 | -0.302 | -0.033 | 0.021 | -0.080 | 0.310 | 0.983 | 1.660 |
1 | 2024-12-10 08:30:19.118 | -0.306 | -0.018 | 0.005 | -0.078 | 0.306 | 1.002 | 1.360 |
2 | 2024-12-10 08:30:19.169 | -0.305 | -0.022 | 0.012 | -0.069 | 0.308 | 0.983 | 1.160 |
3 | 2024-12-10 08:30:19.200 | -0.280 | 0.032 | 0.036 | -0.049 | 0.312 | 0.968 | 0.800 |
… | … | … | … | … | … | … | … | … |
1481 | 2024-12-10 08:31:19.743 | 0.124 | 0.084 | 0.309 | 0.848 | 0.474 | 0.867 | 3.460 |
1482 | 2024-12-10 08:31:19.829 | 0.125 | 0.082 | 0.337 | 0.843 | 0.470 | 0.853 | 2.940 |
1483 | 2024-12-10 08:31:19.860 | 0.125 | 0.092 | 0.347 | 0.845 | 0.469 | 0.856 | 2.840 |
1484 | 2024-12-10 08:31:19.888 | 0.124 | 0.087 | 0.341 | 0.853 | 0.471 | 0.841 | 2.060 |
1485 rows × 8 columns
Lumbar Angle:
The lumbar angle is computed from sensor data placed on the lower back, representing the flexion or extension of the lumbar. It quantifies how much the lower back bends forward or backward during movement.
Sagittal
The sagittal angle is the value of the torso’s forward/backward tilt in the sagittal plane, derived from the sensor shape and the case position.
Both values are reported in radians where positive means bending forward.
plt.figure(figsize=(12, 6))
plt.plot(df['Time'], df['lumbarAngle'], label='Lumbar Angle')
plt.plot(df['Time'], df['sagital'], label='Sagittal')
plt.xlabel('Time')
plt.ylabel('Angle (radians)')
plt.title('Lumbar Angle and Sagittal Over Time')
plt.legend()
plt.tight_layout()
plt.show()
Schmitt Trigger for Bending Detection
Next, we want to know when the person is bending forward. For this, we use a Schmitt trigger.
This allows us to efficiently filter out noise or jitter caused by the person.
If we were to count everything as bending where the sagittal angle is > 30 degrees, it could lead to very short or undefined edges in the detected timestamps. The Schmitt trigger allows us to set a trigger threshold (high), in this example 30 degrees, and a release threshold (low), for example 25 degrees.
In this case, we use a CountingSchmittTrigger
, which only changes the state when the set thresholds are met for a certain number of measurements.
# Define thresholds and counts for the Schmitt trigger
sagital_high = 0.15
sagital_low = 0.05
high_count = 3 # Number of consecutive points above high to trigger
low_count = 3 # Number of consecutive points below low to reset
# Define predicates for the sagital signal
high_predicate = lambda x: x > sagital_high
low_predicate = lambda x: x < sagital_low
# Create the CountingPredicateSchmittTrigger
trigger = CountingPredicateSchmittTrigger(
high_predicate=high_predicate,
low_predicate=low_predicate,
high_count=high_count,
low_count=low_count
)
# Track the trigger state for each time point
bending_mask = []
for val in df['sagital']:
trigger.add(val)
bending_mask.append(trigger.state == SchmittState.HIGH)
df['bending'] = bending_mask
plt.figure(figsize=(12, 6))
plt.plot(df['Time'], df['lumbarAngle'], label='Lumbar Angle')
plt.plot(df['Time'], df['sagital'], label='Sagittal')
plt.fill_between(df['Time'], df['lumbarAngle'].min(), df['sagital'].max(), where=df['bending'], color='orange', alpha=0.2, label='Bending Forward')
plt.xlabel('Time')
plt.ylabel('Sagittal Angle (radians)')
plt.title('Sagittal Angle with Bending Forward Periods')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()
Great. We can clearly see that the periods marked as being bend forward match our expecations and are clearly separated. In this case one “Bending Forward” block represents taking a mug or plate out of the dishwasher.
Next we want to do a simple evaluation of the ergonomic quality of the movements.
For that we use a combination of the bending of the lumbar spine and the rotation of the thorso twist
.
We are going to rate the movements from 0 (bad) to 1 (good).
lumbar = df['lumbarAngle']
twist = df['twist']
# Score: 1 is perfect, 0 is bad; penalize more when both are bad
# This score is kind of arbitrary and can be adjusted based on the specific requirements
lumbar_norm = (lumbar / 0.3).clip(0, 1)
twist_norm = (twist.abs() / 0.15).clip(0, 1)
df['score'] = 1 - (lumbar_norm + twist_norm) / 3 - (lumbar_norm * twist_norm)
# Find contiguous bending forward periods using pandas magic
bending_periods = [group.index.tolist() for _, group in df[df['bending']].groupby((~df['bending']).cumsum())]
# Compute average score for each bending period (simple version)
period_scores = []
for i, period in enumerate(bending_periods):
avg = df['score'].loc[period].mean()
period_scores.append({'period': str(i+1), 'score': avg})
period_scores_df = pd.DataFrame(period_scores)
plt.figure(figsize=(10, 5))
plt.bar(period_scores_df['period'], period_scores_df['score'])
plt.xlabel('Bending Forward Period')
plt.ylabel('Average Score')
plt.title('Bending Forward Quality per Period')
plt.ylim(-0.05, 1.05)
x_pos = 5.5
plt.axvline(x=x_pos)
plt.text(x_pos+0.05, 0.95, 'Start emptying bottom drawer', rotation=90, va='top', fontsize=11, fontweight='bold')
plt.tight_layout()
plt.show()
Based on our simple score we can clearly see that once the person starts unloading the bottom tray of the dishwasher, the ergonomic quality starts to degrade.
Summary
We have:
- Loaded sensor data
- Created a plot
- Detected bending forward periods
- Evaluated the ergonomic impact
and all that in 88 lines of code (including comments, imports and plots)