-
Notifications
You must be signed in to change notification settings - Fork 1.4k
13204: Fix label border rendering issue on flat brain surfaces #13219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 30 commits
b5acfa9
8ef14b5
4026798
9087c45
41e62aa
becdb73
fa7b0d8
f4d4a53
05cb361
335cbe7
f1e404d
221a4c0
0a66f07
093f127
f110d7c
5bfdd68
9929701
83466da
2c7b564
6166d8b
3e1fd5c
dee8cf3
65ad5cd
9b2f1c3
58c4fb0
8e2a106
0c2c00c
fb12fc8
8d9dc1a
518b61c
91c19a8
710561a
cc26434
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import numpy as np | ||
import pytest | ||
|
||
import mne | ||
|
||
|
||
class MockBrain: | ||
"""Mock class to simulate the Brain object for testing label borders.""" | ||
|
||
def __init__(self, subject: str, hemi: str, surf: str): | ||
"""Initialize MockBrain with subject, hemisphere, and surface type.""" | ||
self.subject = subject | ||
self.hemi = hemi | ||
self.surf = surf | ||
|
||
def add_label(self, label: mne.Label, borders: bool = False) -> str: | ||
""" | ||
Simulate adding a label and handling borders logic. | ||
|
||
Parameters | ||
---------- | ||
label : instance of Label | ||
The label to be added. | ||
borders : bool | ||
Whether to add borders to the label. | ||
|
||
Returns | ||
------- | ||
str | ||
The action taken with respect to borders. | ||
""" | ||
if borders: | ||
if self.surf == "flat": | ||
# Skip borders on flat surfaces without warning | ||
return f"Skipping borders for label: {label.name} (flat surface)" | ||
return f"Adding borders to label: {label.name}" | ||
return f"Adding label without borders: {label.name}" | ||
|
||
def _project_to_flat_surface(self, label: mne.Label) -> np.ndarray: | ||
""" | ||
Project the 3D vertices of the label onto a 2D plane. | ||
|
||
Parameters | ||
---------- | ||
label : instance of Label | ||
The label whose vertices are to be projected. | ||
|
||
Returns | ||
------- | ||
np.ndarray | ||
The 2D projection of the label's vertices. | ||
""" | ||
return np.array([vertex[:2] for vertex in label.vertices]) | ||
|
||
def _render_label_borders(self, label_2d: np.ndarray) -> list: | ||
""" | ||
Render the label borders on the flat surface using 2D projected vertices. | ||
|
||
Parameters | ||
---------- | ||
label_2d : np.ndarray | ||
The 2D projection of the label's vertices. | ||
|
||
Returns | ||
------- | ||
list | ||
The borders to be rendered. | ||
""" | ||
return list(label_2d) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"surf, borders, expected", | ||
[ | ||
("flat", True, "Skipping borders"), | ||
("flat", False, "Adding label without borders"), | ||
("inflated", True, "Adding borders"), | ||
("inflated", False, "Adding label without borders"), | ||
], | ||
) | ||
def test_label_borders(surf, borders, expected): | ||
"""Test adding labels with and without borders on different brain surfaces.""" | ||
brain = MockBrain(subject="fsaverage", hemi="lh", surf=surf) | ||
label = mne.Label( | ||
np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]), name="test_label", hemi="lh" | ||
) | ||
result = brain.add_label(label, borders=borders) | ||
assert expected in result | ||
|
||
# Use internal projection and rendering functions to avoid vulture error | ||
projected = brain._project_to_flat_surface(label) | ||
borders_rendered = brain._render_label_borders(projected) | ||
assert isinstance(borders_rendered, list) | ||
assert all(len(vertex) == 2 for vertex in borders_rendered) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
import warnings | ||
from functools import partial | ||
from io import BytesIO | ||
from warnings import warn | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. revert. we have our own internal |
||
|
||
import numpy as np | ||
from scipy.interpolate import interp1d | ||
|
@@ -51,7 +52,6 @@ | |
logger, | ||
use_log_level, | ||
verbose, | ||
warn, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. revert. |
||
) | ||
from .._3d import ( | ||
_check_views, | ||
|
@@ -2260,17 +2260,38 @@ def add_label( | |
|
||
scalars = np.zeros(self.geo[hemi].coords.shape[0]) | ||
scalars[ids] = 1 | ||
|
||
is_flat = self._hemi_surfs[hemi]["surface"] == "flat" | ||
|
||
if borders: | ||
keep_idx = _mesh_borders(self.geo[hemi].faces, scalars) | ||
show = np.zeros(scalars.size, dtype=np.int64) | ||
if isinstance(borders, int): | ||
for _ in range(borders): | ||
keep_idx = np.isin(self.geo[hemi].faces.ravel(), keep_idx) | ||
keep_idx.shape = self.geo[hemi].faces.shape | ||
keep_idx = self.geo[hemi].faces[np.any(keep_idx, axis=1)] | ||
keep_idx = np.unique(keep_idx) | ||
show[keep_idx] = 1 | ||
scalars *= show | ||
if is_flat: | ||
# Instead of warning, calculate and show the borders for flat surfaces | ||
keep_idx = _mesh_borders(self.geo[hemi].faces, scalars) | ||
show = np.zeros(scalars.size, dtype=np.int64) | ||
if isinstance(borders, int): | ||
for _ in range(borders): | ||
# Refine border calculation by checking neighboring borders | ||
keep_idx = np.isin(self.geo[hemi].faces.ravel(), keep_idx) | ||
keep_idx.shape = self.geo[hemi].faces.shape | ||
keep_idx = self.geo[hemi].faces[np.any(keep_idx, axis=1)] | ||
keep_idx = np.unique(keep_idx) | ||
show[keep_idx] = 1 | ||
scalars *= show # Apply the border filter to the scalars | ||
|
||
else: | ||
# For non-flat surfaces, proceed with the existing logic | ||
keep_idx = _mesh_borders(self.geo[hemi].faces, scalars) | ||
show = np.zeros(scalars.size, dtype=np.int64) | ||
if isinstance(borders, int): | ||
for _ in range(borders): | ||
keep_idx = np.isin(self.geo[hemi].faces.ravel(), keep_idx) | ||
keep_idx.shape = self.geo[hemi].faces.shape | ||
keep_idx = self.geo[hemi].faces[np.any(keep_idx, axis=1)] | ||
keep_idx = np.unique(keep_idx) | ||
show[keep_idx] = 1 | ||
scalars *= show | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these |
||
|
||
# Add the overlay to the mesh | ||
for _, _, v in self._iter_views(hemi): | ||
mesh = self._layered_meshes[hemi] | ||
mesh.add_overlay( | ||
|
@@ -2280,6 +2301,7 @@ def add_label( | |
opacity=alpha, | ||
name=label_name, | ||
) | ||
|
||
if self.time_viewer and self.show_traces and self.traces_mode == "label": | ||
label._color = orig_color | ||
label._line = line | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove this file. If a file is needed for testing, we typically either create it on the fly, or incorporate it into https://github.com/mne-tools/mne-testing-data There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please guide me about this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a vulture allowlist for genuine false-positives
tools/vulture_allowlist.py