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.csvwith itsmrk_pathscolumn (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 sessionscans.tsv(marker_before/marker_aftercolumns) 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 withmne.io.kit.read_mrk; the first panel of the figure uses all of them, not only the paired ones.Recording times: the session
scans.tsvwithacq_timeanddurationwhen present; otherwise the.conheader (meas_dateand number of samples). Marker measurement times come from the.mrkfile’s modification time. Every time is put on the UTC clock (anacq_timewithout 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:
Collect the recordings of the session from
kit2fiff_summary.csv(rows with statussuccessorskipped) with theirmrk_paths.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.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.
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_mmand 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) getsstatus: single_markerand no value.Draw the figure and write the tables.
The figure
Two panels share the session clock (minutes from the first marker measurement):
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 iswarn_mm.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_pathsis no longer in the session folder; the recording getsstatus: errorwithpaired_marker_files_not_found_in_session. Rerun stage 2 if the files were renamed.Unknown recording time: no
scans.tsvrow and no readable.conheader; the displacement is still computed and the bar is placed from the marker times with the warningrecording_time_unknown.A coil that slipped: one grey curve far from the others in panel 1 and a large
displacement_max_coil_mmagainst 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_pairingcolumn of the stage-2 summary). Declare the pairs inscans.tsv(--write-scans-draftin 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_failedin the warning column; the traceback is in the stage output. Matplotlib is in the environment; a headless machine needs no display for this stage.