From e70b72b6b539527d61f3e1f3c009638e2799740b Mon Sep 17 00:00:00 2001 From: Dorna Raj Gyawali Date: Sun, 7 Dec 2025 06:16:56 +0000 Subject: [PATCH 1/5] ENH: Add hpi_colors and hpi_labels for clear visualization Signed-off-by: Dorna Raj Gyawali --- mne/viz/_3d.py | 118 ++++++++++++++++++++++++++++++--------- mne/viz/tests/test_3d.py | 36 ++++++++++++ 2 files changed, 129 insertions(+), 25 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index cc660fe4986..19551f0590d 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -552,6 +552,8 @@ def plot_alignment( fig=None, interaction="terrain", sensor_colors=None, + hpi_colors="auto", + hpi_labels=False, *, sensor_scales=None, verbose=None, @@ -645,6 +647,16 @@ def plot_alignment( .. versionchanged:: 1.6 Support for passing a ``dict`` was added. + + hpi_colors : 'auto' | list | dict + Colors for HPI coils when ``dig=True``. + ``'auto'`` (default): use standard MEGIN cable colors for Elekta/MEGIN data + (1=red, 2=blue, 3=green, 4=yellow, 5=magenta, 6=cyan). + Can also be a list of colors or ``{ident: color}`` dict. + + hpi_labels : bool + If True, show the HPI coil number (ident) as text above each coil. + %(sensor_scales)s .. versionadded:: 1.9 @@ -900,7 +912,9 @@ def plot_alignment( _check_option("dig", dig, (True, False, "fiducials")) if dig: if dig is True: - _plot_hpi_coils(renderer, info, to_cf_t) + _plot_hpi_coils( + renderer, info, to_cf_t, hpi_colors=hpi_colors, hpi_labels=hpi_labels + ) _plot_head_shape_points(renderer, info, to_cf_t) _plot_head_fiducials(renderer, info, to_cf_t, fid_colors) @@ -1292,34 +1306,88 @@ def _plot_hpi_coils( surf=None, check_inside=None, nearest=None, + hpi_colors="auto", + hpi_labels=False, ): + from matplotlib.colors import to_rgba + defaults = DEFAULTS["coreg"] scale = defaults["hpi_scale"] if scale is None else scale - hpi_loc = np.array( - [ - d["r"] - for d in (info["dig"] or []) - if ( - d["kind"] == FIFF.FIFFV_POINT_HPI - and d["coord_frame"] == FIFF.FIFFV_COORD_HEAD - ) + + hpi_digs = [ + d + for d in (info["dig"] or []) + if ( + d["kind"] == FIFF.FIFFV_POINT_HPI + and d["coord_frame"] == FIFF.FIFFV_COORD_HEAD + ) + ] + if not hpi_digs: + return [] + + hpi_idents = [d["ident"] for d in hpi_digs] + hpi_locs = apply_trans(to_cf_t["head"], [d["r"] for d in hpi_digs]) + + if hpi_colors == "auto": + megin_colors = { + 1: "red", + 2: "blue", + 3: "green", + 4: "yellow", + 5: "magenta", + 6: "cyan", + } + colors = [ + megin_colors.get(ident, defaults["hpi_color"]) for ident in hpi_idents ] - ) - hpi_loc = apply_trans(to_cf_t["head"], hpi_loc) - actor, _ = _plot_glyphs( - renderer=renderer, - loc=hpi_loc, - color=defaults["hpi_color"], - scale=scale, - opacity=opacity, - orient_glyphs=orient_glyphs, - scale_by_distance=scale_by_distance, - surf=surf, - backface_culling=True, - check_inside=check_inside, - nearest=nearest, - ) - return actor + elif isinstance(hpi_colors, dict): + colors = [hpi_colors.get(ident, defaults["hpi_color"]) for ident in hpi_idents] + elif isinstance(hpi_colors, (list, tuple)): + if len(hpi_colors) != len(hpi_digs): + raise ValueError( + f"""hpi_colors list length + {len(hpi_colors)} != number of HPI coils {len(hpi_digs)} + """ + ) + colors = hpi_colors + else: + colors = [hpi_colors] * len(hpi_digs) + + actors = [] + + for loc, color, ident in zip(hpi_locs, colors, hpi_idents): + color_rgba = to_rgba(color) + + result = _plot_glyphs( + renderer=renderer, + loc=np.array([loc]), + color=color_rgba, + scale=scale, + opacity=opacity, + orient_glyphs=orient_glyphs, + scale_by_distance=scale_by_distance, + surf=surf, + backface_culling=True, + check_inside=check_inside, + nearest=nearest, + ) + + if result is not None: + actor = result[0] if isinstance(result, tuple) else result + actors.append(actor) + + if hpi_labels: + offset = np.array([0, 0, scale * 1.3]) + renderer.text3d( + x=loc[0], + y=loc[1], + z=loc[2] + offset[2], + text=str(ident), + scale=scale * 0.7, + color=color_rgba, + ) + + return actors def _get_nearest(nearest, check_inside, project_to_trans, proj_rr): diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index ab24e6a70db..70c91b1f5f6 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -899,6 +899,42 @@ def test_plot_alignment_basic(tmp_path, renderer, mixed_fwd_cov_evoked): ) +@testing.requires_testing_data +def test_plot_alignment_hpi_colors_and_labels(renderer): + """Test hpi_colors and hpi_labels parameters.""" + import mne + + raw = mne.io.read_raw_fif(data_dir / "MEG" / "sample" / "sample_audvis_raw.fif") + info = raw.info + + for hpi_colors in [ + "auto", + ["red", "red", "blue", "green", "yellow"], + {1: "purple", 4: "orange"}, + "pink", + ]: + for hpi_labels in [False, True]: + fig = plot_alignment( + info=info, + dig=True, + surfaces=[], + coord_frame="head", + hpi_colors=hpi_colors, + hpi_labels=hpi_labels, + ) + assert len(fig.plotter.renderer.actors) > 0 + + fig_no_label = plot_alignment( + info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=False + ) + fig_with_label = plot_alignment( + info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=True + ) + assert len(fig_with_label.plotter.renderer.actors) > len( + fig_no_label.plotter.renderer.actors + ) + + @testing.requires_testing_data def test_plot_alignment_fnirs(renderer, tmp_path): """Test fNIRS plotting.""" From 0f95ae40b68869dbad28b959e8d74fb7ea2e856d Mon Sep 17 00:00:00 2001 From: Dorna Raj Gyawali Date: Fri, 12 Dec 2025 14:00:57 +0000 Subject: [PATCH 2/5] fixed/plot_aligment --- mne/viz/_3d.py | 19 ++++++++------ mne/viz/tests/test_3d.py | 57 +++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 19551f0590d..b5460195f46 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -552,9 +552,9 @@ def plot_alignment( fig=None, interaction="terrain", sensor_colors=None, + *, hpi_colors="auto", hpi_labels=False, - *, sensor_scales=None, verbose=None, ): @@ -646,17 +646,18 @@ def plot_alignment( %(sensor_colors)s .. versionchanged:: 1.6 - Support for passing a ``dict`` was added. - + Support for passing a ``dict`` was added. hpi_colors : 'auto' | list | dict Colors for HPI coils when ``dig=True``. - ``'auto'`` (default): use standard MEGIN cable colors for Elekta/MEGIN data + ``'auto'`` (default): use official MEGIN/Elekta cable colors (1=red, 2=blue, 3=green, 4=yellow, 5=magenta, 6=cyan). Can also be a list of colors or ``{ident: color}`` dict. + .. versionadded:: 1.11 hpi_labels : bool - If True, show the HPI coil number (ident) as text above each coil. + If ``True``, show the HPI coil ident number as 3D text above each coil. + .. versionadded:: 1.11 %(sensor_scales)s .. versionadded:: 1.9 @@ -1329,6 +1330,10 @@ def _plot_hpi_coils( hpi_locs = apply_trans(to_cf_t["head"], [d["r"] for d in hpi_digs]) if hpi_colors == "auto": + # MEGIN/Elekta HPI coil cable colors(MNE community convention from user reports) + # 1 = red, 2 = blue, 3 = green, 4 = yellow, 5 = magenta, 6 = cyan + # Coil 1 is confirmed as "red" in Elekta TRIUX manual + # Full mapping is standard practice in MNE; no official 6-color list. megin_colors = { 1: "red", 2: "blue", @@ -1372,9 +1377,7 @@ def _plot_hpi_coils( nearest=nearest, ) - if result is not None: - actor = result[0] if isinstance(result, tuple) else result - actors.append(actor) + actors.append(result) if hpi_labels: offset = np.array([0, 0, scale * 1.3]) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 70c91b1f5f6..ff444e9d75f 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -904,35 +904,44 @@ def test_plot_alignment_hpi_colors_and_labels(renderer): """Test hpi_colors and hpi_labels parameters.""" import mne - raw = mne.io.read_raw_fif(data_dir / "MEG" / "sample" / "sample_audvis_raw.fif") + raw_path = ( + mne.datasets.testing.data_path(download=False) + / "MEG" + / "sample" + / "sample_audvis_raw.fif" + ) + raw = mne.io.read_raw_fif(raw_path, preload=False) info = raw.info - for hpi_colors in [ - "auto", - ["red", "red", "blue", "green", "yellow"], - {1: "purple", 4: "orange"}, - "pink", - ]: - for hpi_labels in [False, True]: - fig = plot_alignment( - info=info, - dig=True, - surfaces=[], - coord_frame="head", - hpi_colors=hpi_colors, - hpi_labels=hpi_labels, - ) - assert len(fig.plotter.renderer.actors) > 0 + cases = [ + ("auto", False), + ("auto", True), + (["red", "red", "blue", "green", "yellow"], False), + (["red", "red", "blue", "green", "yellow"], True), + ({1: "purple", 4: "orange"}, False), + ({1: "purple", 4: "orange"}, True), + ("pink", False), + ("pink", True), + ] - fig_no_label = plot_alignment( - info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=False - ) - fig_with_label = plot_alignment( - info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=True + for hpi_colors, hpi_labels in cases: + fig = plot_alignment( + info=info, + dig=True, + surfaces=[], + coord_frame="head", + hpi_colors=hpi_colors, + hpi_labels=hpi_labels, + ) + assert len(fig.plotter.renderer.actors) > 0 + + fig1 = plot_alignment( + info=info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=False ) - assert len(fig_with_label.plotter.renderer.actors) > len( - fig_no_label.plotter.renderer.actors + fig2 = plot_alignment( + info=info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=True ) + assert len(fig2.plotter.renderer.actors) > len(fig1.plotter.renderer.actors) @testing.requires_testing_data From 45f167d4155a2102d2f93b72ef5ed1916cc43ad0 Mon Sep 17 00:00:00 2001 From: Dorna Raj Gyawali Date: Tue, 16 Dec 2025 06:28:34 +0000 Subject: [PATCH 3/5] refactor/testcase --- mne/viz/tests/test_3d.py | 42 +++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index ff444e9d75f..1b948960eac 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -900,18 +900,12 @@ def test_plot_alignment_basic(tmp_path, renderer, mixed_fwd_cov_evoked): @testing.requires_testing_data -def test_plot_alignment_hpi_colors_and_labels(renderer): +def test_plot_alignment_hpi_colors_and_labels(renderer, tmp_path): """Test hpi_colors and hpi_labels parameters.""" - import mne + info = read_info(evoked_fname) - raw_path = ( - mne.datasets.testing.data_path(download=False) - / "MEG" - / "sample" - / "sample_audvis_raw.fif" - ) - raw = mne.io.read_raw_fif(raw_path, preload=False) - info = raw.info + hpi_points = [d for d in info["dig"] if d["kind"] == FIFF.FIFFV_POINT_HPI] + assert len(hpi_points) == 4 cases = [ ("auto", False), @@ -933,15 +927,31 @@ def test_plot_alignment_hpi_colors_and_labels(renderer): hpi_colors=hpi_colors, hpi_labels=hpi_labels, ) - assert len(fig.plotter.renderer.actors) > 0 + _assert_n_actors(fig, renderer, 4) - fig1 = plot_alignment( - info=info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=False + fig_no_labels = plot_alignment( + info=info, + dig=True, + surfaces=(), + hpi_colors="auto", + hpi_labels=False, + subjects_dir=tmp_path, ) - fig2 = plot_alignment( - info=info, dig=True, surfaces=[], hpi_colors="auto", hpi_labels=True + fig_with_labels = plot_alignment( + info=info, + dig=True, + surfaces=(), + hpi_colors="auto", + hpi_labels=True, + subjects_dir=tmp_path, ) - assert len(fig2.plotter.renderer.actors) > len(fig1.plotter.renderer.actors) + + base_count = len(fig_no_labels.renderer._actors) + labeled_count = len(fig_with_labels.renderer._actors) + + assert labeled_count == base_count + len(hpi_points) + _assert_n_actors(fig_no_labels, renderer, base_count) + _assert_n_actors(fig_with_labels, renderer, base_count + 4) @testing.requires_testing_data From 6fea7ce7b3eed46f1a99bf01f381b981ec692cfb Mon Sep 17 00:00:00 2001 From: Dorna Raj Gyawali Date: Wed, 17 Dec 2025 06:13:33 +0000 Subject: [PATCH 4/5] refactor: testcase --- mne/viz/tests/test_3d.py | 64 +++++++++++++++------------------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 1b948960eac..d6c87a35de1 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -900,58 +900,42 @@ def test_plot_alignment_basic(tmp_path, renderer, mixed_fwd_cov_evoked): @testing.requires_testing_data -def test_plot_alignment_hpi_colors_and_labels(renderer, tmp_path): +def test_plot_alignment_hpi_colors_and_labels(renderer): """Test hpi_colors and hpi_labels parameters.""" info = read_info(evoked_fname) - - hpi_points = [d for d in info["dig"] if d["kind"] == FIFF.FIFFV_POINT_HPI] - assert len(hpi_points) == 4 - - cases = [ - ("auto", False), - ("auto", True), - (["red", "red", "blue", "green", "yellow"], False), - (["red", "red", "blue", "green", "yellow"], True), - ({1: "purple", 4: "orange"}, False), - ({1: "purple", 4: "orange"}, True), - ("pink", False), - ("pink", True), - ] - - for hpi_colors, hpi_labels in cases: - fig = plot_alignment( - info=info, - dig=True, - surfaces=[], - coord_frame="head", - hpi_colors=hpi_colors, - hpi_labels=hpi_labels, - ) - _assert_n_actors(fig, renderer, 4) - - fig_no_labels = plot_alignment( + fig = plot_alignment( info=info, dig=True, - surfaces=(), + surfaces=[], + coord_frame="head", + meg=[], + eeg=[], + ecog=False, + seeg=False, + fnirs=False, + dbs=False, + show_axes=False, hpi_colors="auto", hpi_labels=False, - subjects_dir=tmp_path, ) - fig_with_labels = plot_alignment( + _assert_n_actors(fig, renderer, 7) + + fig = plot_alignment( info=info, dig=True, - surfaces=(), + surfaces=[], + coord_frame="head", + meg=[], + eeg=[], + ecog=False, + seeg=False, + fnirs=False, + dbs=False, + show_axes=False, hpi_colors="auto", hpi_labels=True, - subjects_dir=tmp_path, ) - - base_count = len(fig_no_labels.renderer._actors) - labeled_count = len(fig_with_labels.renderer._actors) - - assert labeled_count == base_count + len(hpi_points) - _assert_n_actors(fig_no_labels, renderer, base_count) - _assert_n_actors(fig_with_labels, renderer, base_count + 4) + _assert_n_actors(fig, renderer, 11) @testing.requires_testing_data From 973883afafad456e5c5fe512d952743e3d311f54 Mon Sep 17 00:00:00 2001 From: Dorna Raj Gyawali Date: Wed, 17 Dec 2025 17:59:06 +0000 Subject: [PATCH 5/5] refactor case --- mne/viz/tests/test_3d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index d6c87a35de1..0704441ebf2 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -918,7 +918,7 @@ def test_plot_alignment_hpi_colors_and_labels(renderer): hpi_colors="auto", hpi_labels=False, ) - _assert_n_actors(fig, renderer, 7) + _assert_n_actors(fig, renderer, 8) fig = plot_alignment( info=info, @@ -935,7 +935,7 @@ def test_plot_alignment_hpi_colors_and_labels(renderer): hpi_colors="auto", hpi_labels=True, ) - _assert_n_actors(fig, renderer, 11) + _assert_n_actors(fig, renderer, 12) @testing.requires_testing_data