FlexTail Sensor Evaluation Series

Cleaning-Session Ergonomics: ISO Zones, Bend Events, and Frame-Level Quality

A detailed walkthrough of the cleaning-session analysis: ISO 11226 zone classification, pelvic-tilt escalation, hysteresis-based bend detection, and the configurable lift-evaluation score.

Published

One recording, several complementary questions

A long cleaning session is a useful test case for continuous ergonomics analysis because it contains sustained stooping, repeated reaches, transitions between work areas, and short recovery periods. No single number captures all of that behaviour. The playbook decomposes the recording into four layers:

  1. Session-level posture zones based on sagittal and lateral angles.
  2. Time-resolved bend frequency.
  3. Discrete bend events detected with hysteresis.
  4. A configurable, frame-level quality score summarised within each bend.

The source chapter reports 124,581 samples for the analysed cleaning session. Its mean sagittal angle was 26.2 degrees, mean absolute lateral deviation was 2.4 degrees, and lumbar $P90$ was 34.6 degrees. Under the specified rule set, the resulting session classification was Medium. These are properties of this recording and its configuration, not normative reference values for all cleaning work.

Session-level ISO zone classification

The analysis operationalises a three-zone posture framework informed by ISO 11226:2000 and DIN EN 1005-4. Each sample receives a sagittal zone from $\theta_{\mathrm{sag}}$ and a lateral zone from absolute lateral deviation $|\theta_{\mathrm{lat}}|$:

$$ Z_{\mathrm{sag}}(\theta)= \begin{cases} \mathrm{Neutral}, & \theta < 20^\circ \ \mathrm{Moderate}, & 20^\circ \leq \theta < 40^\circ \ \mathrm{Awkward}, & \theta \geq 40^\circ \end{cases} $$

$$ Z_{\mathrm{lat}}(\theta)= \begin{cases} \mathrm{Neutral}, & |\theta| < 10^\circ \ \mathrm{Moderate}, & 10^\circ \leq |\theta| < 20^\circ \ \mathrm{Awkward}, & |\theta| \geq 20^\circ. \end{cases} $$

The two zones are combined with a cross-matrix. Any combination that includes sagittal or lateral Awkward is High; a neutral-neutral sample is Low; the remaining neutral/moderate combinations are Medium.

Lateral zone / Sagittal zone Neutral Moderate Awkward
Neutral Low Medium High
Moderate Medium Medium High
Awkward High High High

The session base class is the most frequent matrix cell. It is then escalated by one level, clamped at High, if the 90th percentile lumbar angle reaches 40 degrees:

$$ R_{\mathrm{final}} = \min\left(2, R_{\mathrm{base}} + \mathbb{1}\left[Q_{0.90}(\theta_{\mathrm{lumbar}}) \geq 40^\circ\right]\right). $$

The escalation separates two different exposure patterns. A high session mean reflects a sustained forward-lean posture. A moderate mean with high $P90$ reflects repeated deep bends that may not dominate the average. Both should be visible in a report because their interventions can differ: work height and tool design can reduce sustained stooping, while reach strategy and floor-level task design can reduce peak bends.

The exact risk classification in the broader ergo_analysis workflow is concise and inspectable:

sag = df["sagittal_mean"].fillna(0.0)
df["sag_group"] = "Neutral"
df.loc[sag >= 20.0, "sag_group"] = "Moderate"
df.loc[sag >= 40.0, "sag_group"] = "Awkward"

lat = df["lateral_mean"].abs().fillna(0.0)
df["lat_group"] = "Neutral"
df.loc[lat >= 10.0, "lat_group"] = "Moderate"
df.loc[lat >= 20.0, "lat_group"] = "Awkward"

df["base_risk"] = [_RISK_MAP[(sag, lat)] for sag, lat in zip(df["sag_group"], df["lat_group"])]
escalate = df["lumbar_p90"].fillna(0.0) >= 40.0
df["final_risk"] = (df["base_risk"] + escalate.astype(int)).clip(0, 2)

This is a documented operationalisation, not an implementation of every provision in ISO 11226. A formal workplace assessment must account for task duration, recovery, force, load handling, environmental conditions, and the standard's full decision procedure.

Detecting a bend without fragmenting it

Repeated bending is detected from the sagittal channel using a delayed Schmitt trigger. The detector enters a bend as soon as flexion exceeds 30 degrees. It exits only after flexion remains below 15 degrees for one full second. Let $x(t)$ be sagittal flexion and $q(t)\in{0,1}$ the state:

$$ q(t^+) = 1 \quad \text{when } q(t)=0 \text{ and } x(t)>30^\circ, $$

$$ q(t^+) = 0 \quad \text{when } q(t)=1 \text{ and } x(u)<15^\circ \text{ for all } u\in[t-1\ \mathrm{s},t]. $$

The hysteresis band $[15^\circ,30^\circ]$ prevents a signal hovering near one threshold from switching rapidly between bend and non-bend. The delayed exit prevents a brief mid-bend rise above 15 degrees from splitting one compound action into several events.

Before detection, the evaluation pipeline applies a causal moving average of five samples:

$$ \widetilde x_t = \frac{1}{\min(w,t+1)} \sum_{k=0}^{\min(w-1,t)}x_{t-k}, \qquad w=5. $$

The source implementation keeps the filter causal, which is appropriate for real-time feedback but introduces phase lag. Offline analysis can compare this output with a zero-phase filter or no smoothing to quantify whether event boundaries materially change.

trigger = DelayedPredicateSchmittTrigger(
    enter_deg=30.0,
    exit_deg=15.0,
    low_duration=1.0,
)

states = np.array([
    trigger.process(sagittal[i], elapsed_seconds[i])
    for i in range(len(sagittal))
])

# False -> True transitions are bend starts.
event_starts = int(np.sum(np.diff(states.astype(int)) > 0))

In the analysed cleaning recording, the chapter reports 246 detected bend events. This count is conditional on the smoothing, thresholds, channel sign convention, and timing data. It is not a direct count of every manual action. Always show a representative time-series with the detected phases overlaid and inspect a sample of events manually.

Frequency, duration, and cumulative exposure

Within a window $w$ of duration $T_w$, bend frequency is:

$$ f_w = \frac{n_w}{T_w}\times 60 \quad [\mathrm{bends/min}]. $$

For each detected event $e$, retain its duration $D_e$, peak angle $A_e=\max_{t\in e}|x(t)|$, and active-time fraction. Reporting these separately is more informative than reporting an event count alone. Two sessions with 20 events may differ substantially if one contains brief shallow bends and the other contains long bends with high peaks.

A simple descriptive dose is total active bend time:

$$ T_{\mathrm{active}} = \sum_{e=1}^{E}D_e. $$

It is still only a descriptive quantity. It does not incorporate external load, speed, lifting distance, or individual capacity, and should not be presented as a biomechanical force estimate.

The frame-level quality score

The lift-evaluation engine calculates four component scores at every frame: lumbar, sagittal, lateral, and twist. For a symmetric component with angle $a$ in radians, the score is:

$$ S(a) = \operatorname{clamp}\left(o - m|a|, 0, 100\right). $$

The default configuration uses $o=120$ and $m=114.59$. This creates approximate component breakpoints of 100 at 0 degrees, 80 at 20 degrees, 40 at 40 degrees, and 0 at 60 degrees. The sagittal scorer treats forward and backward motion separately; by default, backward extension receives zero penalty.

With component weights $w_L,w_S,w_{Lat},w_T$, the composite frame score is:

$$ S_{\mathrm{frame}}= \operatorname{clamp}\left( \frac{w_LS_L+w_SS_S+w_{Lat}S_{Lat}+w_TS_T} {w_L+w_S+w_{Lat}+w_T},0,100\right). $$

The source engine makes the calibration explicit:

def score_component(angle_rad: float, offset: float, modifier: float) -> float:
    return max(0.0, min(100.0, offset - modifier * abs(angle_rad)))

def calculate_weighted_score(s_lumbar, s_sagittal, s_lateral, s_twist, weights):
    total_weight = weights.lumbar + weights.sagittal + weights.lateral + weights.twist
    raw = (
        weights.lumbar * s_lumbar
        + weights.sagittal * s_sagittal
        + weights.lateral * s_lateral
        + weights.twist * s_twist
    ) / total_weight
    return max(0.0, min(100.0, raw))

The event score is the mean of active-frame composite scores. It is converted to a quality scale:

$$ q_e=\operatorname{clamp}\left(\frac{\overline S_e-60}{90-60},0,1\right). $$

The implementation labels $q_e\geq2/3$ as Good, $q_e\geq1/3$ as Fair, and lower values as Poor. With the default constants, that maps approximately to mean scores of 80 and 70. These labels are scoring categories, not clinical grades or validated injury classifications. Researchers should report the numerical components and configuration alongside the label.

Validation checks before using the workflow

The four layers can disagree, and that disagreement is informative. For example, a session can be Medium at the session level while containing a small number of deep, low-scoring bends. Conversely, a high frequency of shallow bends can create a large event count without a high $P90$. Do not collapse them before inspecting the pattern.

Before comparing participants, tasks, or intervention periods:

  • Verify the sensor's sagittal sign convention using a known movement.
  • Confirm timestamps are monotonic and calculate the actual sampling interval.
  • Plot raw and smoothed channels around a random sample of detected events.
  • Vary the 30/15 degree thresholds and one-second holdoff in a sensitivity analysis.
  • Preserve the component scores, not only the composite, so the source of a low score remains visible.
  • Keep task force, object mass, reach distance, and work height as separate observations when they are relevant.

Sources

  • International Organization for Standardization. ISO 11226:2000 Ergonomics - Evaluation of Static Working Postures.
  • Deutsches Institut fur Normung. DIN EN 1005-4:2005 Safety of Machinery - Human Physical Performance - Evaluation of Working Postures and Movements in Relation to Machinery.