Skip to content

Commit 77ba263

Browse files
authored
BUG: Fix bug with get_view (#12000)
1 parent d2883d5 commit 77ba263

File tree

11 files changed

+269
-177
lines changed

11 files changed

+269
-177
lines changed

doc/changes/devel.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ Bugs
4848
- Fix bug with ``pca=False`` in :func:`mne.minimum_norm.compute_source_psd` (:gh:`11927` by `Alex Gramfort`_)
4949
- Fix bug with notebooks when using PyVista 0.42 by implementing ``trame`` backend support (:gh:`11956` by `Eric Larson`_)
5050
- Removed preload parameter from :func:`mne.io.read_raw_eyelink`, because data are always preloaded no matter what preload is set to (:gh:`11910` by `Scott Huberty`_)
51+
- Fix bug with :meth:`mne.viz.Brain.get_view` where calling :meth:`~mne.viz.Brain.show_view` with returned parameters would change the view (:gh:`12000` by `Eric Larson`_)
52+
- Fix bug with :meth:`mne.viz.Brain.show_view` where ``distance=None`` would change the view distance (:gh:`12000` by `Eric Larson`_)
5153
- Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_)
5254
- Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_)
5355
- Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_)
@@ -58,3 +60,4 @@ API changes
5860
- ``mne.preprocessing.apply_maxfilter`` and ``mne maxfilter`` have been deprecated and will be removed in 1.7. Use :func:`mne.preprocessing.maxwell_filter` (see :ref:`this tutorial <tut-artifact-sss>`) in Python or the command-line utility from MEGIN ``maxfilter`` and :func:`mne.bem.fit_sphere_to_headshape` instead (:gh:`11938` by `Eric Larson`_)
5961
- :func:`mne.io.kit.read_mrk` reading pickled files is deprecated using something like ``np.savetxt(fid, pts, delimiter="\t", newline="\n")`` to save your points instead (:gh:`11937` by `Eric Larson`_)
6062
- Replace legacy ``inst.pick_channels`` and ``inst.pick_types`` with ``inst.pick`` (where ``inst`` is an instance of :class:`~mne.io.Raw`, :class:`~mne.Epochs`, or :class:`~mne.Evoked`) wherever possible (:gh:`11907` by `Clemens Brunner`_)
63+
- The ``reset_camera`` parameter has been removed in favor of ``distance="auto"`` in :func:`mne.viz.set_3d_view`, :meth:`mne.viz.Brain.show_view`, and related functions (:gh:`12000` by `Eric Larson`_)

mne/transforms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ def _cart_to_sph(cart):
793793
Array containing points in spherical coordinates (rad, azimuth, polar)
794794
"""
795795
cart = np.atleast_2d(cart)
796-
assert cart.ndim == 2 and cart.shape[1] == 3
796+
assert cart.ndim == 2 and cart.shape[1] == 3, cart.shape
797797
out = np.empty((len(cart), 3))
798798
out[:, 0] = np.sqrt(np.sum(cart * cart, axis=1))
799799
norm = np.where(out[:, 0] > 0, out[:, 0], 1) # protect against / 0

mne/utils/docs.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,9 +1104,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
11041104
docdict[
11051105
"distance"
11061106
] = """
1107-
distance : float | None
1107+
distance : float | "auto" | None
11081108
The distance from the camera rendering the view to the focalpoint
1109-
in plot units (either m or mm).
1109+
in plot units (either m or mm). If "auto", the bounds of visible objects will be
1110+
used to set a reasonable distance.
1111+
1112+
.. versionchanged:: 1.6
1113+
``None`` will no longer change the distance, use ``"auto"`` instead.
11101114
"""
11111115

11121116
docdict[

mne/viz/_3d.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3392,7 +3392,6 @@ def plot_sparse_source_estimates(
33923392
color=brain_color,
33933393
opacity=opacity,
33943394
backface_culling=True,
3395-
shading=True,
33963395
normals=normals,
33973396
**kwargs,
33983397
)

mne/viz/_brain/_brain.py

Lines changed: 65 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,6 @@
9292
)
9393

9494

95-
_ARROW_MOVE = 10 # degrees per press
96-
97-
9895
@fill_doc
9996
class Brain:
10097
"""Class for visualizing a brain.
@@ -373,7 +370,6 @@ def __init__(
373370
self._annots = {"lh": list(), "rh": list()}
374371
self._layered_meshes = dict()
375372
self._actors = dict()
376-
self._elevation_rng = [15, 165] # range of motion of camera on theta
377373
self._cleaned = False
378374
# default values for silhouette
379375
self._silhouette = {
@@ -473,9 +469,7 @@ def __init__(
473469
alpha=self._silhouette["alpha"],
474470
decimate=self._silhouette["decimate"],
475471
)
476-
self._renderer.set_camera(
477-
update=False, reset_camera=False, **views_dicts[h][v]
478-
)
472+
self._set_camera(**views_dicts[h][v])
479473

480474
self.interaction = interaction
481475
self._closed = False
@@ -497,7 +491,7 @@ def _setup_canonical_rotation(self):
497491
xfm = _estimate_talxfm_rigid(self._subject, self._subjects_dir)
498492
except Exception:
499493
logger.info(
500-
"Could not estimate rigid Talairach alignment, " "using identity matrix"
494+
"Could not estimate rigid Talairach alignment, using identity matrix"
501495
)
502496
else:
503497
self._rigid[:] = xfm
@@ -1192,17 +1186,18 @@ def save_movie(filename):
11921186
shortcut="?",
11931187
)
11941188

1195-
def _rotate_azimuth(self, value):
1196-
azimuth = (self._renderer.figure._azimuth + value) % 360
1197-
self._renderer.set_camera(azimuth=azimuth, reset_camera=False)
1198-
1199-
def _rotate_elevation(self, value):
1200-
elevation = np.clip(
1201-
self._renderer.figure._elevation + value,
1202-
self._elevation_rng[0],
1203-
self._elevation_rng[1],
1204-
)
1205-
self._renderer.set_camera(elevation=elevation, reset_camera=False)
1189+
def _rotate_camera(self, which, value):
1190+
_, _, azimuth, elevation, _ = self._renderer.get_camera(rigid=self._rigid)
1191+
kwargs = dict(update=True)
1192+
if which == "azimuth":
1193+
value = azimuth + value
1194+
# Our view_up threshold is 5/175, so let's be safe here
1195+
if elevation < 7.5 or elevation > 172.5:
1196+
kwargs["elevation"] = np.clip(elevation, 10, 170)
1197+
else:
1198+
value = np.clip(elevation + value, 10, 170)
1199+
kwargs[which] = value
1200+
self._set_camera(**kwargs)
12061201

12071202
def _configure_shortcuts(self):
12081203
# Remove the default key binding
@@ -1213,13 +1208,14 @@ def _configure_shortcuts(self):
12131208
self.plotter.add_key_event("s", self.apply_auto_scaling)
12141209
self.plotter.add_key_event("r", self.restore_user_scaling)
12151210
self.plotter.add_key_event("c", self.clear_glyphs)
1216-
for key, func, sign in (
1217-
("Left", self._rotate_azimuth, 1),
1218-
("Right", self._rotate_azimuth, -1),
1219-
("Up", self._rotate_elevation, 1),
1220-
("Down", self._rotate_elevation, -1),
1211+
for key, which, amt in (
1212+
("Left", "azimuth", 10),
1213+
("Right", "azimuth", -10),
1214+
("Up", "elevation", 10),
1215+
("Down", "elevation", -10),
12211216
):
1222-
self.plotter.add_key_event(key, partial(func, sign * _ARROW_MOVE))
1217+
self.plotter.clear_events_for_key(key)
1218+
self.plotter.add_key_event(key, partial(self._rotate_camera, which, amt))
12231219

12241220
def _configure_menu(self):
12251221
self._renderer._menu_initialize()
@@ -1417,7 +1413,7 @@ def _add_label_glyph(self, hemi, mesh, vertex_id):
14171413
return
14181414

14191415
if hemi == label.hemi:
1420-
self.add_label(label, borders=True, reset_camera=False)
1416+
self.add_label(label, borders=True)
14211417
self.picked_patches[hemi].append(label_id)
14221418

14231419
def _remove_label_glyph(self, hemi, label_id):
@@ -2002,11 +1998,9 @@ def add_data(
20021998
)
20031999
kwargs.update(colorbar_kwargs or {})
20042000
self._scalar_bar = self._renderer.scalarbar(**kwargs)
2005-
self._renderer.set_camera(
2006-
update=False, reset_camera=False, **views_dicts[hemi][v]
2007-
)
2001+
self._set_camera(**views_dicts[hemi][v])
20082002

2009-
# 4) update the scalar bar and opacity
2003+
# 4) update the scalar bar and opacity (and render)
20102004
self._update_colormap_range(alpha=alpha)
20112005

20122006
# 5) enable UI events to interact with the data
@@ -2146,21 +2140,21 @@ def _add_volume_data(self, hemi, src, volume_options):
21462140
self._data[hemi]["grid_volume_pos"] = volume_pos
21472141
self._data[hemi]["grid_volume_neg"] = volume_neg
21482142
actor_pos, _ = self._renderer.plotter.add_actor(
2149-
volume_pos, reset_camera=False, name=None, culling=False, render=False
2143+
volume_pos, name=None, culling=False, reset_camera=False, render=False
21502144
)
21512145
actor_neg = actor_mesh = None
21522146
if volume_neg is not None:
21532147
actor_neg, _ = self._renderer.plotter.add_actor(
2154-
volume_neg, reset_camera=False, name=None, culling=False, render=False
2148+
volume_neg, name=None, culling=False, reset_camera=False, render=False
21552149
)
21562150
grid_mesh = self._data[hemi]["grid_mesh"]
21572151
if grid_mesh is not None:
21582152
actor_mesh, prop = self._renderer.plotter.add_actor(
21592153
grid_mesh,
2160-
reset_camera=False,
21612154
name=None,
21622155
culling=False,
21632156
pickable=False,
2157+
reset_camera=False,
21642158
render=False,
21652159
)
21662160
prop.SetColor(*self._brain_color[:3])
@@ -2188,7 +2182,8 @@ def add_label(
21882182
borders=False,
21892183
hemi=None,
21902184
subdir=None,
2191-
reset_camera=True,
2185+
*,
2186+
reset_camera=None,
21922187
):
21932188
"""Add an ROI label to the image.
21942189
@@ -2221,8 +2216,7 @@ def add_label(
22212216
for ``$SUBJECTS_DIR/$SUBJECT/label/aparc/lh.cuneus.label``
22222217
``brain.add_label('cuneus', subdir='aparc')``).
22232218
reset_camera : bool
2224-
If True, reset the camera view after adding the label. Defaults
2225-
to True.
2219+
Deprecated. Use :meth:`show_view` instead.
22262220
22272221
Notes
22282222
-----
@@ -2329,6 +2323,12 @@ def add_label(
23292323
keep_idx = np.unique(keep_idx)
23302324
show[keep_idx] = 1
23312325
scalars *= show
2326+
if reset_camera is not None:
2327+
warn(
2328+
"reset_camera is deprecated and will be removed in 1.7, "
2329+
"use show_view instead",
2330+
FutureWarning,
2331+
)
23322332
for _, _, v in self._iter_views(hemi):
23332333
mesh = self._layered_meshes[hemi]
23342334
mesh.add_overlay(
@@ -2338,8 +2338,6 @@ def add_label(
23382338
opacity=alpha,
23392339
name=label_name,
23402340
)
2341-
if reset_camera:
2342-
self._renderer.set_camera(update=False, **views_dicts[hemi][v])
23432341
if self.time_viewer and self.show_traces and self.traces_mode == "label":
23442342
label._color = orig_color
23452343
label._line = line
@@ -2492,7 +2490,6 @@ def add_head(self, dense=True, color="gray", alpha=0.5):
24922490
triangles=triangles,
24932491
color=color,
24942492
opacity=alpha,
2495-
reset_camera=False,
24962493
render=False,
24972494
)
24982495
self._add_actor("head", actor)
@@ -2744,7 +2741,8 @@ def add_foci(
27442741
opacity=alpha,
27452742
resolution=resolution,
27462743
)
2747-
self._renderer.set_camera(**views_dicts[hemi][v])
2744+
self._set_camera(**views_dicts[hemi][v])
2745+
self._renderer._update()
27482746

27492747
# Store the foci in the Brain._data dictionary
27502748
data_foci = coords
@@ -3119,7 +3117,7 @@ def show(self):
31193117
_qt_app_exec(self._renderer.figure.store["app"])
31203118

31213119
@fill_doc
3122-
def get_view(self, row=0, col=0):
3120+
def get_view(self, row=0, col=0, *, align=True):
31233121
"""Get the camera orientation for a given subplot display.
31243122
31253123
Parameters
@@ -3128,6 +3126,7 @@ def get_view(self, row=0, col=0):
31283126
The row to use, default is the first one.
31293127
col : int
31303128
The column to check, the default is the first one.
3129+
%(align_view)s
31313130
31323131
Returns
31333132
-------
@@ -3139,10 +3138,11 @@ def get_view(self, row=0, col=0):
31393138
"""
31403139
row = _ensure_int(row, "row")
31413140
col = _ensure_int(col, "col")
3141+
rigid = self._rigid if align else None
31423142
for h in self._hemis:
31433143
for ri, ci, _ in self._iter_views(h):
31443144
if (row == ri) and (col == ci):
3145-
return self._renderer.get_camera()
3145+
return self._renderer.get_camera(rigid=rigid)
31463146
return (None,) * 5
31473147

31483148
@verbose
@@ -3248,24 +3248,39 @@ def show_view(
32483248
param: val for param, val in view_params.items() if val is not None
32493249
} # no overwriting with None
32503250
view_params = dict(views_dicts[hemi].get(view), **view_params)
3251-
xfm = self._rigid if align else None
32523251
for h in self._hemis:
32533252
for ri, ci, _ in self._iter_views(h):
32543253
if (row is None or row == ri) and (col is None or col == ci):
3255-
self._renderer.set_camera(
3256-
**view_params,
3257-
reset_camera=False,
3258-
rigid=xfm,
3259-
update=False,
3260-
)
3254+
self._set_camera(**view_params, align=align)
32613255
if update:
32623256
self._renderer._update()
32633257

3258+
def _set_camera(
3259+
self,
3260+
*,
3261+
distance=None,
3262+
focalpoint=None,
3263+
update=False,
3264+
align=True,
3265+
verbose=None,
3266+
**kwargs,
3267+
):
3268+
# Wrap to self._renderer.set_camera safely, always passing self._rigid
3269+
# and using better no-op-like defaults
3270+
return self._renderer.set_camera(
3271+
distance=distance,
3272+
focalpoint=focalpoint,
3273+
update=update,
3274+
rigid=self._rigid if align else None,
3275+
**kwargs,
3276+
)
3277+
32643278
def reset_view(self):
32653279
"""Reset the camera."""
32663280
for h in self._hemis:
32673281
for _, _, v in self._iter_views(h):
3268-
self._renderer.set_camera(**views_dicts[h][v], reset_camera=False)
3282+
self._set_camera(**views_dicts[h][v])
3283+
self._renderer._update()
32693284

32703285
def save_image(self, filename=None, mode="rgb"):
32713286
"""Save view from all panels to disk.

0 commit comments

Comments
 (0)