Stage 4: Head movement between marker measurements

Author: Hadi Zaatiti <hadi.zaatiti@nyu.edu>

What & why

Stage 4 of the KIT general pipeline reports how much the head moved during a session, from the only head-position information the KIT system records. The KIT has no continuous head localisation: the position of the head relative to the sensors is measured only when a marker measurement is taken, that is, when the five HPI coils glued to the head are energised and localised in MEG device space (*_markers.mrk). Between two measurements the head is assumed still.

Comparing the coil positions of two marker measurements therefore gives the movement between them, which is attributed to the recordings they bound (marker, recording, marker). It never shows movement inside a recording, or when it happened. The stage makes this explicit by drawing a timeline of the session with one figure per subject and session, and by writing the numbers in two tables that the report and the pipeline console display.

Why it matters: a large displacement between the marker before and the marker after a recording means the device-to-head transform used for that recording (stage 2) is only valid for part of it, sensor-level topographies are blurred, and any source estimate (stages 14 and 15) is mislocalised. A flagged recording is a reason to inspect it, to exclude it, or to split the analysis, not a measurement of brain-to-sensor movement by itself: the displacement also contains the marker measurement error and a coil that slipped on the skin.

What you need to provide

  • The stage-2 outputs: kit2fiff_summary.csv with its mrk_paths column (the marker files paired with each recording). Stage 4 does not pair markers itself; it uses the pairing stage 2 recorded, which comes from the session scans.tsv (marker_before / marker_after columns) or from the chronological chain. See the stage-2 page for the pairing rules.

  • Every marker file of the session (sub-<id>[_ses-<id>]_task-rest_acq-<k>_space-ALS_markers.mrk), read with mne.io.kit.read_mrk; the first panel of the figure uses all of them, not only the paired ones.

  • Recording times: the session scans.tsv with acq_time and duration when present; otherwise the .con header (meas_date and number of samples). Marker measurement times come from the .mrk file’s modification time. Every time is put on the UTC clock (an acq_time without a time zone is read as UTC, which is what mne-bids and the stage-2 draft write; the KIT header stamp is a UTC epoch in MNE), the same clock stage 21 uses, so the two session clocks agree. These times only place the bars and points on the session clock; the displacements do not depend on them.

Configuration file

head_movement:
  enabled: true
  warn_mm: 5.0       # flag recordings whose coil displacement (RMS over the coils) exceeds this
  save_plots: true
  save_svg: false    # also write an SVG next to the PNG

warn_mm is a reporting threshold, not a rejection: flagged recordings keep their outputs and the later stages process them. Choose the value with the analysis in mind (source analyses tolerate less than sensor-level ERPs). project, subjects and bids_selection select the recordings as in every stage.

How to run

conda activate neurowaves
python -m pipeline.mne_pipelines.kit_general_pipelines.4-kit_head_movement \
    -c pipeline/mne_pipelines/kit_general_pipelines/pipeline_config_files/<project>_config.yml

Options:

  • --no-plots: write the two tables only.

Run it after stage 2 (and after redoing any conversion with --overwrite, since the marker pairing may change). The stage is fast (no MEG data is loaded) and can be rerun at any time; its tables are rebuilt from the current stage-2 summary on every run. The VS Code launch entries are named <project> 4 Head movement between marker measurements.

What the script does

Per subject and session:

  1. Collect the recordings of the session from kit2fiff_summary.csv (rows with status success or skipped) with their mrk_paths.

  2. Read the coil positions of every marker file in the session folder (five coils, metres, device space) and sort the measurements by their acq-<k> number, which is chronological by convention.

  3. For every marker measurement, compute its displacement from the first measurement of the session and from the previous one: the distance moved by each coil, their mean, maximum and RMS, and the rigid-body translation and rotation that best maps one coil set onto the other (Kabsch least-squares fit; translation of the coil centroid in mm, rotation angle in degrees). Coils with a non-finite position are dropped; fewer than three usable coils is an error.

  4. For every recording, compute the same quantities between the marker measured before it and the one measured after it. The RMS over the coils is the value compared with warn_mm and the same quantity as the pre/post displacement of the stage-2 coregistration overview. A recording bounded by a single marker (the last run of a session, a session whose last measurement failed) gets status: single_marker and no value.

  5. Draw the figure and write the tables.

The figure

Two panels share the session clock (minutes from the first marker measurement):

  1. Head position: the distance of each coil (grey) and the mean over the coils (blue) from its position at the first measurement, one point per marker measurement, labelled mrk k. A rising curve is a head that drifts over the session; one coil that departs from the others is a coil that slipped. The dashed line is warn_mm.

  2. Movement attributed to each recording: one bar from the start to the end of each recording, at the height of the RMS coil displacement between its two markers, labelled with the task, run and value. Orange bars exceed warn_mm. Hatched bars at zero are recordings bounded by a single marker: nothing can be measured for them. Dotted vertical lines are the marker measurements.

When one measurement bounds several recordings (marker, task A, task B, marker, declared in scans.tsv), the same displacement is drawn and reported for each of them; the figure shows it as adjacent bars of the same height between the same two dotted lines.

Outputs

Under <bids_root>/derivatives/4-kit_head_movement/:

head_movement_summary.csv
head_movement_markers.csv
configs/config_<timestamp>.yml
sub-<id>/[ses-<id>/]
  sub-<id>[_ses-<id>]_desc-headmovement.png   (+ .svg when save_svg is true)

head_movement_summary.csv has one row per recording: the BIDS entities and con_path, recording_start, recording_end, duration_s, marker_before, marker_after, displacement_rms_mm, displacement_mean_mm, displacement_max_coil_mm, translation_mm, rotation_deg, position_after_vs_first_marker_mm (mean coil distance of the “after” marker from the first measurement), coils_used, status (success | single_marker | error), warning (movement_above_<warn_mm>_mm, recording_time_unknown, figure_failed), error, plot and mne_version.

head_movement_markers.csv has one row per marker measurement: marker_file, acq, time, minutes_from_session_start, the mean and maximum coil distance, translation and rotation from the first marker, the same from the previous marker, and the per-coil distances from the first marker.

Report

The report opens its stage-4 section with a Sessions table (one row per subject and session: recordings, measured, single marker, errors, largest displacement, largest position change against the first marker, and the number of recordings flagged above warn_mm), then shows per session the figure, the recordings table and the marker table. The pipeline console shows the displacement of each recording in its stage-4 column, in the warning colour when it exceeds warn_mm and with the warning text as a tooltip, so a session with movement is visible without opening the report.

Edge cases & safeguards

  • Marker file not found: the file named in mrk_paths is no longer in the session folder; the recording gets status: error with paired_marker_files_not_found_in_session. Rerun stage 2 if the files were renamed.

  • Unknown recording time: no scans.tsv row and no readable .con header; the displacement is still computed and the bar is placed from the marker times with the warning recording_time_unknown.

  • A coil that slipped: one grey curve far from the others in panel 1 and a large displacement_max_coil_mm against a moderate RMS. The rigid fit is still computed over all coils; consider re-running stage 2 for that recording with that coil deselected in the GUI (the marker selection is saved next to the FIF).

  • Sessions with one recording and two markers work; sessions with a single marker produce a figure with one point and hatched bars only.

Testing

tests/test_head_movement.py exercises the geometry (rigid fit, rotation angle, displacement, dropped coils) and the figure on synthetic coil positions, without any dataset:

python -m pytest tests/test_head_movement.py -q

Troubleshooting

  • Bars all hatched: stage 2 paired every recording with one marker only (mrk_pairing column of the stage-2 summary). Declare the pairs in scans.tsv (--write-scans-draft in stage 2 writes a draft) and redo the conversions.

  • Values look too large everywhere: check that the marker files of the session belong to the same subject and that acq-<k> numbers follow the recording order; a measurement from another session in the folder shifts every curve of panel 1.

  • Figure missing, tables present: figure_failed in the warning column; the traceback is in the stage output. Matplotlib is in the environment; a headless machine needs no display for this stage.