Skip to content

Commit

Permalink
FIX: Fixorama
Browse files Browse the repository at this point in the history
  • Loading branch information
larsoner committed Sep 19, 2023
1 parent f8413d7 commit d653d71
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 66 deletions.
8 changes: 6 additions & 2 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1104,9 +1104,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
docdict[
"distance"
] = """
distance : float | None
distance : float | "auto" | None
The distance from the camera rendering the view to the focalpoint
in plot units (either m or mm).
in plot units (either m or mm). If "auto", the bounds of visible objects will be
used to set a reasonable distance.
.. versionchanged:: 1.6
``None`` will no longer change the distance, use ``"auto"`` instead.
"""

docdict[
Expand Down
34 changes: 16 additions & 18 deletions mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,6 @@ def __init__(
self._annots = {"lh": list(), "rh": list()}
self._layered_meshes = dict()
self._actors = dict()
self._elevation_rng = [15, 165] # range of motion of camera on theta
self._cleaned = False
# default values for silhouette
self._silhouette = {
Expand Down Expand Up @@ -1281,17 +1280,15 @@ def _shift_time(self, shift_func):
TimeChange(time=shift_func(self._current_time, self.playback_speed)),
)

def _rotate_azimuth(self, value):
azimuth = (self._renderer.figure._azimuth + value) % 360
self._renderer.set_camera(azimuth=azimuth, reset_camera=False)

def _rotate_elevation(self, value):
elevation = np.clip(
self._renderer.figure._elevation + value,
self._elevation_rng[0],
self._elevation_rng[1],
)
self._renderer.set_camera(elevation=elevation, reset_camera=False)
def _rotate_camera(self, which, value):
roll, distance, azimuth, elevation, focalpoint = self._renderer.get_camera()
if which == "azimuth":
kwargs = dict(azimuth=azimuth + value % 360)
else:
assert which == "elevation", which
elevation = np.clip(elevation + value, 15, 165)
kwargs = dict(elevation=elevation)
self._renderer.set_camera(reset_camera=False, **kwargs)

def _configure_shortcuts(self):
# Remove the default key binding
Expand All @@ -1308,13 +1305,14 @@ def _configure_shortcuts(self):
self.plotter.add_key_event(
"b", partial(self._shift_time, shift_func=lambda x, y: x - y)
)
for key, func, sign in (
("Left", self._rotate_azimuth, 1),
("Right", self._rotate_azimuth, -1),
("Up", self._rotate_elevation, 1),
("Down", self._rotate_elevation, -1),
for key, func, which, sign in (
("Left", self._rotate_camera, "azimuth", 1),
("Right", self._rotate_camera, "azimuth", -1),
("Up", self._rotate_camera, "elevation", 1),
("Down", self._rotate_camera, "elevation", -1),
):
self.plotter.add_key_event(key, partial(func, sign * _ARROW_MOVE))
self.plotter.clear_events_for_key(key)
self.plotter.add_key_event(key, partial(func, which, sign * _ARROW_MOVE))

def _configure_menu(self):
self._renderer._menu_initialize()
Expand Down
16 changes: 8 additions & 8 deletions mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,7 @@ def test_image_screenshot(
):
"""Test screenshot and image saving."""
size = (300, 300)
brain = _create_testing_brain(hemi="lh", size=size)
cam = brain._renderer.figure.plotter.camera
brain = _create_testing_brain(hemi="rh", size=size)
azimuth, elevation = 180.0, 90.0
fname = tmp_path / "test.png"
assert not fname.is_file()
Expand All @@ -615,12 +614,13 @@ def test_image_screenshot(
fp = (fp[1::2] + fp[::2]) * 0.5
for view_args in (
dict(azimuth=azimuth, elevation=elevation, focalpoint="auto"),
dict(view="lateral", hemi="lh"),
dict(view="lateral", hemi="rh"),
):
brain.show_view(**view_args)
assert_allclose(brain._renderer.figure._azimuth % 360, azimuth % 360)
assert_allclose(brain._renderer.figure._elevation % 180, elevation % 180)
assert_allclose(cam.GetFocalPoint(), fp, atol=1e-6)
_, _, a_, e_, f_ = brain.get_view()
assert_allclose(a_ % 360, azimuth % 360, atol=1e-6)
assert_allclose(e_ % 180, elevation % 180)
assert_allclose(f_, fp, atol=1e-6)
img = brain.screenshot(mode="rgba")
want_size = np.array([size[0] * pixel_ratio, size[1] * pixel_ratio, 4])
# on macOS sometimes matplotlib is HiDPI and VTK is not...
Expand Down Expand Up @@ -800,8 +800,8 @@ def test_brain_time_viewer(renderer_interactive_pyvistaqt, pixel_ratio, brain_gc
_assert_brain_range(brain, [4.0, 12.0])
brain._shift_time(shift_func=lambda x, y: x + y)
brain._shift_time(shift_func=lambda x, y: x - y)
brain._rotate_azimuth(15)
brain._rotate_elevation(15)
brain._rotate_camera("azimuth", 15)
brain._rotate_camera("elevation", 15)
brain.toggle_interface()
brain.toggle_interface(value=False)
brain.set_playback_speed(0.1)
Expand Down
73 changes: 35 additions & 38 deletions mne/viz/backends/_pyvista.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ def _init(
self._plotter_class = Plotter

self._nrows, self._ncols = self.store["shape"]
self._azimuth = self._elevation = None

def _build(self):
if self.plotter is None:
Expand Down Expand Up @@ -827,7 +826,7 @@ def set_camera(
self,
azimuth=None,
elevation=None,
distance=None,
distance="auto",
focalpoint="auto",
roll=None,
reset_camera=True,
Expand Down Expand Up @@ -1145,21 +1144,22 @@ def _close_all():
_FIGURES.clear()


def _get_camera_direction(focalpoint, position):
def _get_user_camera_direction(plotter, rigid):
position = np.array(plotter.camera.position, float)
focalpoint = np.array(plotter.camera.focal_point, float)
if rigid is not None:
position = apply_trans(rigid, position, move=False)
focalpoint = apply_trans(rigid, focalpoint, move=False)
return tuple(_cart_to_sph(position - focalpoint)[0])


def _get_3d_view(figure, *, rigid=None):
position = np.array(figure.plotter.camera.position, float)
focalpoint = np.array(figure.plotter.camera.focal_point, float)
rigid = np.eye(4) if rigid is None else np.linalg.inv(rigid)
position = apply_trans(rigid, position)
focalpoint = apply_trans(rigid, focalpoint)
_, phi, theta = _get_camera_direction(focalpoint, position)
_, phi, theta = _get_user_camera_direction(figure.plotter, rigid)
azimuth, elevation = np.rad2deg(phi), np.rad2deg(theta)
return (
figure.plotter.camera.GetRoll(),
figure.plotter.camera.GetDistance(),
figure.plotter.camera.roll,
figure.plotter.camera.distance,
azimuth,
elevation,
focalpoint,
Expand All @@ -1171,43 +1171,46 @@ def _set_3d_view(
azimuth=None,
elevation=None,
focalpoint="auto",
distance=None,
distance="auto",
roll=None,
reset_camera=True,
rigid=None,
update=True,
):
camera = figure.plotter.camera
rigid = np.eye(4) if rigid is None else rigid
position = np.array(camera.position)
bounds = np.array(figure.plotter.renderer.ComputeVisiblePropBounds())
# Only compute bounds if we need to
bounds = None
if isinstance(focalpoint, str) or isinstance(distance, str):
bounds = np.array(figure.plotter.renderer.ComputeVisiblePropBounds(), float)

# camera slides along the vector defined from camera position to focal point until
# all of the actors can be seen (quoting PyVista's docs)
if reset_camera:
figure.plotter.reset_camera(render=False)

# Figure out our current parameters in the transformed space
_, phi, theta = _get_user_camera_direction(figure.plotter, rigid)

# focalpoint: if 'auto', we use the center of mass of the visible
# bounds, if None, we use the existing camera focal point otherwise
# we use the values given by the user
if isinstance(focalpoint, str):
_check_option("focalpoint", focalpoint, ("auto",), extra="when a string")
focalpoint = (bounds[1::2] + bounds[::2]) * 0.5
elif focalpoint is None:
focalpoint = camera.focal_point
focalpoint = np.array(focalpoint, float)

# work in the transformed space
position = apply_trans(rigid, position)
focalpoint = apply_trans(rigid, focalpoint)
_, phi, theta = _get_camera_direction(focalpoint, position)
focalpoint = figure.plotter.camera.focal_point
focalpoint = np.array(focalpoint, float) # in real-world coords
if distance is None:
distance = figure.plotter.camera.distance
elif isinstance(distance, str):
_check_option("distance", distance, ("auto",), extra="when a string")
distance = max(bounds[1::2] - bounds[::2]) * 2.0
distance = float(distance)

if azimuth is not None:
phi = np.deg2rad(azimuth)
if elevation is not None:
theta = np.deg2rad(elevation)

# set the distance
if distance is None:
distance = max(bounds[1::2] - bounds[::2]) * 2.0

# Now calculate the view_up vector of the camera. If the view up is
# close to the 'z' axis, the view plane normal is parallel to the
# camera which is unacceptable, so we use a different view up.
Expand All @@ -1218,20 +1221,14 @@ def _set_3d_view(

position = _sph_to_cart([distance, phi, theta])[0]

# TODO: We should remove this hack and compute from the camera
figure._azimuth = np.rad2deg(phi)
figure._elevation = np.rad2deg(theta)

# restore to the original frame
rigid = np.linalg.inv(rigid)
position = apply_trans(rigid, position)
focalpoint = apply_trans(rigid, focalpoint)
view_up = apply_trans(rigid, view_up, move=False)
camera.position = position
camera.focal_point = focalpoint
camera.view_up = view_up
if rigid is not None:
rigid_inv = np.linalg.inv(rigid)
position = apply_trans(rigid_inv, position, move=False)
view_up = apply_trans(rigid_inv, view_up, move=False)
figure.plotter.camera_position = [position, focalpoint, view_up]
if roll is not None:
camera.SetRoll(roll)
figure.plotter.camera.roll = roll

if update:
figure.plotter.update()
Expand Down

0 comments on commit d653d71

Please sign in to comment.