imagej / pyimagej

Use ImageJ from Python
https://pyimagej.readthedocs.io/
Other
475 stars 82 forks source link

[macOS] Initializing PyImageJ before napari breaks napari #197

Open ctrueden opened 2 years ago

ctrueden commented 2 years ago

Here is a minimal example:

import imagej
ij = imagej.init("2.5.0", add_legacy=False)
#-----
import napari, skimage
napari.view_image(skimage.data.cells3d())

The view_image call then fails. First we see WARNING: no screens available, assuming 24-bit color, and then deep in the stack trace:

quartz.CGDisplayScreenSize(display)

returns 0, and then ZeroDivisionError: float division by zero


WORKAROUND: start napari first:

import napari
napari.Viewer()
#-----
import imagej
ij = imagej.init("2.5.0", add_legacy=False)
#-----
import skimage
napari.view_image(skimage.data.cells3d())

But I found that with the workaround, trying to change the color map caused the napari GUI to hang.

ctrueden commented 2 years ago

Side note from @tlambert03 from a Zulip thread where we discussed this issue:

Re running a Qt app without blocking the main thread: that’s thanks to this chunk of code here: https://github.com/ipython/ipython/blob/master/IPython/terminal/pt_inputhooks/qt.py

It’s what makes the “gui qt” magic work too https://ipython.readthedocs.io/en/stable/config/eventloops.html

ctrueden commented 2 years ago

While debugging this problem, at one point when creating multiple SciJava contexts in the same JVM I saw this message:

2022-04-30 12:16:19.092 python[96732:3183402] [JRSAppKitAWT markAppIsDaemon]: Process manager already initialized: can't fully enable headless mode.
ctrueden commented 2 years ago

I narrowed down the problem to specific SciJava services:

import imagej
#ij = imagej.init("2.5.0", add_legacy=False)
imagej._create_jvm("2.5.0", mode='headless', add_legacy=False)
#-----
from jpype import JArray
def jarray(plist, element_type):
    array = JArray(element_type)(len(plist))
    for i in range(len(plist)):
        array[i] = plist[i]
    return array
#-----
from scyjava import jimport
fail_services = [
    'org.scijava.event.DefaultEventHistory',
    'org.scijava.command.DefaultCommandService',
]
Class = jimport('java.lang.Class')
services = jarray([jimport(s) for s in fail_services], Class)
Context = jimport('org.scijava.Context')
ctx = Context(services)
#-----
import napari, skimage
napari.view_image(skimage.data.cells3d())

But I found that the order of service initialization, not just which services, can affect whether napari works afterward. For example, if you change the above to:

fail_services = [
    'org.scijava.command.DefaultCommandService',
    'org.scijava.event.DefaultEventHistory',
]

Then napari still works.

Further digging needed to understand what's happening when as these services are constructed and initialized.

gselzer commented 2 years ago

I did a little debugging. Turns out that the order of fail_services actually affects the screen size as reported by quartz.

On e.g. linux, vispy calls a different function. This guy runs xdpyinfo, returning a DPI of 96x96 in both cases.

gselzer commented 2 years ago

I did some more hacking to your notebook example, which I turned into a script:

import imagej

from scyjava import config

# config.add_option('-agentlib:jdwp=transport=dt_socket,server=y,address=5286,suspend=y')
#ij = imagej.init("2.5.0", add_legacy=False)
imagej._create_jvm("2.5.0", mode='headless', add_legacy=False)
from jpype import JArray
def jarray(plist, element_type):
    array = JArray(element_type)(len(plist))
    for i in range(len(plist)):
        array[i] = plist[i]
    return array
from scyjava import jimport
fail_services = [
    'org.scijava.event.DefaultEventHistory',
    'org.scijava.command.DefaultCommandService',
]
Class = jimport('java.lang.Class')
services = jarray([jimport(s) for s in fail_services], Class)
Context = jimport('org.scijava.Context')
ctx = Context(jarray([], Class))

# BEGIN NEW STUFF

ServiceHelper = jimport('org.scijava.service.ServiceHelper')
Collections = jimport('java.util.Collections')

from vispy.ext.cocoapy import quartz
for c in services:
    ServiceHelper(ctx, Collections.singletonList(c)).loadServices()
    screensize = quartz.CGDisplayScreenSize(quartz.CGMainDisplayID())
    print(f"Screen size after loading {c}: {screensize.width} x {screensize.height}")

screensize = quartz.CGDisplayScreenSize(quartz.CGMainDisplayID())
print(f"Screen size after loading {c}: {screensize.width} x {screensize.height}")

# END NEW STUFF

import napari, skimage
napari.view_image(skimage.data.cells3d())

this throws some crazy error:

Screen size after loading class org.scijava.event.DefaultEventHistory: 602.0740650318287 x 341.1940247265261
Screen size after loading class org.scijava.command.DefaultCommandService: 602.0740650318287 x 341.1940247265261
Screen size after loading class org.scijava.command.DefaultCommandService: 602.0740650318287 x 341.1940247265261
/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/skimage/io/manage_plugins.py:23: UserWarning: Your installed pillow version is < 8.1.2. Several security issues (CVE-2021-27921, CVE-2021-25290, CVE-2021-25291, CVE-2021-25293, and more) have been fixed in pillow 8.1.2 or higher. We recommend to upgrade this library.
  from .collection import imread_collection_wrapper
Screen size width: 602.0740650318287; height: 341.1940247265261
Screen size dpi: 67.25000100999367
2022-05-02 17:09:04.934 python[96234:7598623] *** Assertion failure in -[NSThemeFrame initWithFrame:], NSView.m:1353
2022-05-02 17:09:04.936 python[96234:7598623] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Frame containing non-finite values {{0, 0}, {640, nan}} passed to [NSThemeFrame initWithFrame:]'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff204ef29b __exceptionPreprocess + 242
    1   libobjc.A.dylib                     0x00007fff20228d92 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff20518422 +[NSException raise:format:arguments:] + 88
    3   Foundation                          0x00007fff212d74e2 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
    4   AppKit                              0x00007fff22c94350 -[NSView initWithFrame:] + 482
    5   AppKit                              0x00007fff22c94072 -[NSFrameView initWithFrame:styleMask:owner:] + 74
    6   AppKit                              0x00007fff22c93eeb -[NSThemeFrame initWithFrame:styleMask:owner:] + 72
    7   AppKit                              0x00007fff22c91d82 -[NSWindow _commonInitFrame:styleMask:backing:defer:] + 633
    8   AppKit                              0x00007fff22c915dc -[NSWindow _initContent:styleMask:backing:defer:contentView:] + 1098
    9   AppKit                              0x00007fff22c9118b -[NSWindow initWithContentRect:styleMask:backing:defer:] + 42
    10  AppKit                              0x00007fff22f9b43c -[NSWindow initWithContentRect:styleMask:backing:defer:screen:] + 52
    11  libqcocoa.dylib                     0x00000009c7a7e725 -[QNSWindow initWithContentRect:styleMask:backing:defer:screen:platformWindow:] + 197
    12  libqcocoa.dylib                     0x00000009c7a6bcac _ZN12QCocoaWindow14createNSWindowEb + 1324
    13  libqcocoa.dylib                     0x00000009c7a65672 _ZN12QCocoaWindow22recreateWindowIfNeededEv + 1266
    14  libqcocoa.dylib                     0x00000009c7a650fe _ZN12QCocoaWindow10initializeEv + 318
    15  QtGui                               0x00000009c6215eb7 _ZN14QWindowPrivate6createEby + 151
    16  QtWidgets                           0x00000009c6e49771 _ZN14QWidgetPrivate6createEv + 1185
    17  QtWidgets                           0x00000009c6e48414 _ZN7QWidget6createEybb + 324
    18  QtWidgets                           0x00000009c6f702c7 _ZN11QMainWindow30setUnifiedTitleAndToolBarOnMacEb + 55
    19  QtWidgets.abi3.so                   0x00000009c6a59307 _ZL47meth_QMainWindow_setUnifiedTitleAndToolBarOnMacP7_objectS0_ + 87
    20  python                              0x000000010b8c296a cfunction_call + 90
    21  python                              0x000000010b8638b6 _PyObject_MakeTpCall + 134
    22  python                              0x000000010b99b5d7 call_function + 311
    23  python                              0x000000010b994348 _PyEval_EvalFrameDefault + 27608
    24  python                              0x000000010b98be09 _PyEval_EvalCode + 473
    25  python                              0x000000010b86453c _PyFunction_Vectorcall + 236
    26  python                              0x000000010b863b4d _PyObject_FastCallDictTstate + 109
    27  python                              0x000000010b8ed7a0 slot_tp_init + 192
    28  python                              0x000000010b8f89f0 type_call + 272
    29  python                              0x000000010b8638b6 _PyObject_MakeTpCall + 134
    30  python                              0x000000010b99b5d7 call_function + 311
    31  python                              0x000000010b9935dd _PyEval_EvalFrameDefault + 24173
    32  python                              0x000000010b98be09 _PyEval_EvalCode + 473
    33  python                              0x000000010b86453c _PyFunction_Vectorcall + 236
    34  python                              0x000000010b863bcd _PyObject_FastCallDictTstate + 237
    35  python                              0x000000010b8ed7a0 slot_tp_init + 192
    36  python                              0x000000010b8f89f0 type_call + 272
    37  python                              0x000000010b8638b6 _PyObject_MakeTpCall + 134
    38  python                              0x000000010b99b5d7 call_function + 311
    39  python                              0x000000010b98e54e _PyEval_EvalFrameDefault + 3550
    40  python                              0x000000010b98be09 _PyEval_EvalCode + 473
    41  python                              0x000000010b86453c _PyFunction_Vectorcall + 236
    42  python                              0x000000010b863b4d _PyObject_FastCallDictTstate + 109
    43  python                              0x000000010b8ed7a0 slot_tp_init + 192
    44  python                              0x000000010b8f89f0 type_call + 272
    45  python                              0x000000010b86430c _PyObject_Call + 108
    46  python                              0x000000010b992dc2 _PyEval_EvalFrameDefault + 22098
    47  python                              0x000000010b98be09 _PyEval_EvalCode + 473
    48  python                              0x000000010b86453c _PyFunction_Vectorcall + 236
    49  python                              0x000000010b99b54e call_function + 174
    50  python                              0x000000010b9935dd _PyEval_EvalFrameDefault + 24173
    51  python                              0x000000010b98be09 _PyEval_EvalCode + 473
    52  python                              0x000000010b86453c _PyFunction_Vectorcall + 236
    53  python                              0x000000010b99b54e call_function + 174
    54  python                              0x000000010b994348 _PyEval_EvalFrameDefault + 27608
    55  python                              0x000000010b98be09 _PyEval_EvalCode + 473
    56  python                              0x000000010b9fc003 pyrun_file + 339
    57  python                              0x000000010b9fb7e8 pyrun_simple_file + 408
    58  python                              0x000000010b9fb5fd PyRun_SimpleFileExFlags + 109
    59  python                              0x000000010ba24b49 pymain_run_file + 329
    60  python                              0x000000010ba24101 pymain_run_python + 417
    61  python                              0x000000010ba23f15 Py_RunMain + 37
    62  python                              0x000000010ba25590 pymain_main + 64
    63  python                              0x000000010b7f95b8 main + 56
    64  libdyld.dylib                       0x00007fff20398f3d start + 1
)
libc++abi: terminating with uncaught exception of type NSException
zsh: abort      python tmp.py

The more interesting thing, though, is if you remove some of the print statements, you get different returns. I get this printout when I run the script, after removing the screen size checks in the for loop. I wonder if some caching is going on here...

(napari-imagej-dev) gselzer@dyn-144-92-48-223 imagej % python tmp.py
Screen size after loading class org.scijava.command.DefaultCommandService: 0.0 x 0.0
/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/skimage/io/manage_plugins.py:23: UserWarning: Your installed pillow version is < 8.1.2. Several security issues (CVE-2021-27921, CVE-2021-25290, CVE-2021-25291, CVE-2021-25293, and more) have been fixed in pillow 8.1.2 or higher. We recommend to upgrade this library.
  from .collection import imread_collection_wrapper
WARNING: no screens available, assuming 24-bit color
Screen size width: 0.0; height: 0.0
Traceback (most recent call last):
  File "/Users/gselzer/code/imagej/tmp.py", line 41, in <module>
    napari.view_image(skimage.data.cells3d())
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/view_layers.py", line 143, in view_image
    return _make_viewer_then('add_image', args, kwargs)
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/view_layers.py", line 122, in _make_viewer_then
    viewer = Viewer(**vkwargs)
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/viewer.py", line 57, in __init__
    self._window = Window(self, show=show)
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/_qt/qt_main_window.py", line 418, in __init__
    self._qt_window = _QtMainWindow(viewer)
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/_qt/qt_main_window.py", line 81, in __init__
    self._qt_viewer = QtViewer(viewer, show_welcome_screen=True)
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/_qt/qt_viewer.py", line 263, in __init__
    self._create_canvas()
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/_qt/qt_viewer.py", line 350, in _create_canvas
    self.canvas = VispyCanvas(
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/napari/_vispy/canvas.py", line 34, in __init__
    super().__init__(*args, **kwargs)
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/vispy/scene/canvas.py", line 135, in __init__
    super(SceneCanvas, self).__init__(
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/vispy/app/canvas.py", line 143, in __init__
    dpi = get_dpi(raise_error=False)
  File "/Users/gselzer/miniconda3/envs/napari-imagej-dev/lib/python3.9/site-packages/vispy/util/dpi/_quartz.py", line 27, in get_dpi
    dpi = (px.width/mm.width + px.height/mm.height) * 0.5 * 25.4
ZeroDivisionError: float division by zero

I can probably look more tomorrow.