Skip to content
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

MRG, ENH: Add a screenshot button to the notebook 3d backend #8708

Merged
merged 37 commits into from
Jan 21, 2021

Conversation

GuillaumeFavelier
Copy link
Contributor

@GuillaumeFavelier GuillaumeFavelier commented Jan 7, 2021

There is actually a button to save effectively plus a text field to change the file name:

image

I also switched to icon + tooltip because it was a bit cramped with all those descriptions:

image

It's an item of #8704.

@agramfort
Copy link
Member

did you consider using a dialog box to be able to save the screenshot where you want on disk?

see eg https://pypi.org/project/ipyfilechooser/

just a quick google search...

@GuillaumeFavelier
Copy link
Contributor Author

GuillaumeFavelier commented Jan 7, 2021

I considered it but this solution is simple and does not add any dependency.

@larsoner
Copy link
Member

larsoner commented Jan 7, 2021

Would be good to add some "interaction" tests in test_notebook if possible. This seems like a good PR where this could be investigated and done.

@GuillaumeFavelier GuillaumeFavelier changed the title Add a screenshot button to the notebook 3d backend WIP: Add a screenshot button to the notebook 3d backend Jan 8, 2021
@GuillaumeFavelier
Copy link
Contributor Author

After e3198f7, there is a tool bar for standard 3d plots too:

image

@GuillaumeFavelier
Copy link
Contributor Author

Known issue

When I resize the page in my web browser, the 3d viewer and the matplotlib widget don't scale the same way leading to issues when I save the screenshot. For example 80%:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/source/mne-python/mne/viz/backends/_notebook.py in <lambda>(x)
     28         from ipywidgets import Button
     29         button = Button(tooltip=desc, icon=icon_name)
---> 30         button.on_click(lambda x: func())
     31         return button
     32 

~/source/mne-python/mne/viz/_brain/_brain.py in _screenshot(self)
   1222             fname = self._renderer.screenshot_filename \
   1223                 if len(fname) == 0 else fname
-> 1224             img = self.screenshot(fname, time_viewer=True)
   1225             Image.fromarray(img).save(fname)
   1226         else:

~/source/mne-python/mne/viz/_brain/_brain.py in screenshot(self, mode, time_viewer)
   2634                 start = delta // 2
   2635                 trace_img = trace_img[:, start:start + img.shape[1]]
-> 2636             img = np.concatenate([img, trace_img], axis=0)
   2637         return img
   2638 

<__array_function__ internals> in concatenate(*args, **kwargs)

ValueError: all the input array dimensions for the concatenation axis must match exactly, but along dimension 1, the array at index 0 has size 800 and the array at index 1 has size 799

When everything works fine at 100%:

screenshot

@GuillaumeFavelier
Copy link
Contributor Author

If this is not a big issue, we can leave this as is otherwise I can mitigate by using the standard screenshot function but the matplotlib figure won't be present.

@larsoner
Copy link
Member

larsoner commented Jan 8, 2021

I would write a mne.viz.utils.concatenate_images that allows you to pass a list of array/images and concatenates them vertically (or horizontally, could be an axis argument that must be 0 or 1) padding and centering the smaller one(s) as needed. This will require a bgcolor argument, which could be 'auto' to use for example the most common color amongst all 4 corner pixels across all images or something.

I've written something like this and needed it multiple times for paper publications and the like so making this public would be useful. We can probably even use it in the publication figure example.

@agramfort
Copy link
Member

screenshot copy

here is the screenshot I get on my system :(

anyone else can try? is it just me?

@larsoner
Copy link
Member

larsoner commented Jan 9, 2021

That looks like a hidpi issue

@GuillaumeFavelier
Copy link
Contributor Author

That looks like a hidpi issue

Oh no, not again... 😟

@agramfort
Copy link
Member

agramfort commented Jan 9, 2021 via email

@larsoner
Copy link
Member

Also at some point we must have broken screenshot size on macOS, on the test locally I get:

    assert img_nv.shape == want
E   assert (558, 600, 3) == (600, 600, 3)

So even without the time viewer the correct size is not preserved. :(

@GuillaumeFavelier
Copy link
Contributor Author

Also at some point we must have broken screenshot size on macOS

Does it mean that ensure_minimum_sizes() does not work on MacOS?

@larsoner
Copy link
Member

Does it mean that ensure_minimum_sizes() does not work on MacOS?

At least it doesn't on my macOS system (with HiDPI), and apparently Azure macOS (no HiDPI) agrees:

E     - (300, 300, 3)
E     + (284, 300, 3)

I can git bisect to see which PR broke things. I think it worked a while back at least, but maybe it never did. In a worst case we can add an extra vertical tolerance to this PR and fix the sizing issue elsewhere.

@larsoner
Copy link
Member

Looks like I broke it back in #8082. Do you want to try to fix it? You probably need a macOS system to be able to try different things out effectively so if you don't have one, I can do it

@GuillaumeFavelier
Copy link
Contributor Author

I looked at #8082 again and I did not see anything obvious except maybe the changes of ensure_minimum_sizes(). The following is not present anymore:

            # 5. Resize the window (again!) to the correct size
            #    (not sure why, but this is required on macOS at least)
            self.plotter.window_size = (sz.width(), sz.height())

probably need a macOS system

You are right. I'll push one last commit but I can't try locally so you can go for it if you have the time

@GuillaumeFavelier
Copy link
Contributor Author

I got no luck with 930fdcf

Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GuillaumeFavelier feel free to take over again based on my comments, but in theory in the meantime the CIs should be green

Comment on lines 2630 to 2640
fig = self.mpl_canvas.fig
with BytesIO() as output:
# Need to pass dpi here so it uses the physical (HiDPI) DPI
# rather than logical DPI when saving
fig.savefig(output, dpi=fig.get_dpi(), format='raw')
output.seek(0)
trace_img = np.reshape(
np.frombuffer(output.getvalue(), dtype=np.uint8),
newshape=(-1, img.shape[1], 4))[:, :, :3]
img = concatenate_images(
[img, trace_img], bgcolor=self._brain_color[:3])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GuillaumeFavelier this seems to work for both code paths now on Linux and macOS for me (notebook and standard renderer)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome news!

" number_of_buttons += 1\n",
" assert number_of_buttons == total_number_of_buttons\n",
" img_nv = brain.screenshot()\n",
" assert img_nv.shape == (300, 300, 3), img_nv.shape\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notebook always renders non-HiDPI. At some point we should look into how to enable HiDPI support in the notebook -- basically we need the background renderer to be ratio (usually 2) times the size, and we need to tell it to display the image in native resolution (rather than treating physical and logical pixels the same). I suspect this is supported at some level in browsers, but TBD whether ipyvtk_simple allows setting this display resolution factor (i.e., to make images that are 600x600 pixels show up in a 300x300 logical pixel HTML element). Could be a cool PR there. Then we'd need a way to probe the physical-to-logical pixel ratio. Ideally this would come from the notebook but if not, we can quickly import Qt and query the screens and take a best guess...

" img_v = brain.screenshot(time_viewer=True)\n",
" assert img_v.shape[1:] == (300, 3), img_v.shape\n",
" # XXX This rtol is not very good, ideally would be zero\n",
" assert_allclose(img_v.shape[0], img_nv.shape[0] * 1.25, err_msg=img_nv.shape, rtol=0.1)\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HiDPI aside, on Linux I was getting values that were off by ~7%. It would be good if this were as good as the non-notebook backend (which uses atol=3 instead of the rtol=0.1 here).

" finally:\n",
" plt.interactive(old)\n",
"\n",
"with interactive(False):\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how critical the iteration over interactive levels was so I killed it to save time now that this isn't marked slowtest.

@agramfort
Copy link
Member

😱

toto

@GuillaumeFavelier GuillaumeFavelier changed the title MRG: Add a screenshot button to the notebook 3d backend WIP: Add a screenshot button to the notebook 3d backend Jan 21, 2021
@larsoner
Copy link
Member

@agramfort what is your mne.sys_info? I can't replicate in my env in pure Python or Jupyter. I wonder if it's because you have PyQt >= 5.14 on macOS (which we don't do in our environment.yml or requirements.txt), where VTK doesn't do HiDPI, but matplotlib probably does... it would explain this monstrosity. :)

I'll push a fix to deal with it, but shame on you for having a non-standard env...

@larsoner
Copy link
Member

@agramfort feel free to try this, works for me on macOS in pure Python and Jupyter with PyQt5 < 5.14 (5.13.2) and >= 5.14 (5.15.2)

@larsoner larsoner changed the title WIP: Add a screenshot button to the notebook 3d backend MRG, ENH: Add a screenshot button to the notebook 3d backend Jan 21, 2021
# (e.g., macOS w/Qt 5.14+ and VTK9) then things won't work,
# so let's just calculate the DPI we need to get
# the correct size output based on the widths being equal
dpi = img.shape[1] / fig.get_size_inches()[0]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this @larsoner

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also works for me on Linux in pure Python and Jupyter

@agramfort agramfort merged commit 72c8f61 into mne-tools:master Jan 21, 2021
@agramfort
Copy link
Member

thanks @GuillaumeFavelier success !!!

@GuillaumeFavelier GuillaumeFavelier deleted the enh/notebook_screenshot branch January 21, 2021 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants