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

Make ImageJ GUI work on macOS #23

Closed
ctrueden opened this issue Jan 7, 2019 · 18 comments
Closed

Make ImageJ GUI work on macOS #23

ctrueden opened this issue Jan 7, 2019 · 18 comments

Comments

@ctrueden
Copy link
Member

ctrueden commented Jan 7, 2019

When launching ImageJ with a display from Python on macOS, it hangs. See 68dad62 for the current workaround and links to more information. The pyimagej code should try to be clever and start the Cocoa event loop itself before spinning up Java.

See also
https://github.com/imglib/imglyb/blob/1ff6fd12ae5270093be6f3ce340247bde1a45dfc/imglyb/OSXAWTwrapper.py

@oeway
Copy link
Contributor

oeway commented Oct 26, 2019

Same issue here, the script hangs when headless=False. I get the GUI by following the suggestions in #39, but without the FIJI menu (instead, it shows only a single menu item named python).

  1. run conda active pyimagej
  2. run pip install pyobjc
  3. download OSXAWTwrapper.py
  4. write a test script named startPyImageJGUI.py with the following content:
import time
import imagej

ij = imagej.init('/Applications/Fiji.app', headless=False)
ij.ui().showUI()

while True:
    time.sleep(1)
  1. run python OSXAWTwrapper.py startImageJ.py and I got GUI work:

Screen Shot 2019-10-26 at 8 05 07 PM

However, the fiji menu didn't show as expected, any idea? @ctrueden

@ctrueden
Copy link
Member Author

@oeway Hmm. I guess that since Java is started inside the Python process, the usual macOS menu bar logic does not work?

I don't immediately know how to fix it. But one quick workaround is to use:

ij = imagej.init('/Applications/Fiji.app', headless=False)
ij.ui().showUI("swing")

This will pop up the Swing UI instead of the legacy ImageJ1 UI. In my quick experimentation, major things including legacy IJ1 plugins do still work—although you may encounter rough edges in some cases.

Relatedly: I am exploring integrating the OSXAWTWrapper logic directly into pyimagej so that you don't have to do so many extra steps manually.

@imagesc-bot
Copy link

This issue has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/pyimagej-imagej-py-show-issues-on-macos/37472/1

@mmarras
Copy link

mmarras commented Nov 13, 2020

Relatedly: I am exploring integrating the OSXAWTWrapper logic directly into pyimagej so that you don't have to do so many extra steps manually.

Has this been integrated?

@ctrueden
Copy link
Member Author

ctrueden commented Dec 1, 2020

Has this been integrated?

@mmarras Unfortunately not. I tried hard last December with @hanslovsky to figure out how to do it, but it's deceptively tricky to make it totally seamless.

We just finished with the 1.0.0 release though, which is now built on JPype rather than PyJNIus. It is possible that JPype somehow avoids this problem; haven't tested it yet. I'll report back soon. If this issue is still a problem, we'll keep trying.

@ctrueden ctrueden removed this from the 1.0.0 milestone Dec 1, 2020
@Thrameos
Copy link

Thrameos commented Dec 1, 2020

Not sure if this is relevant but there was a piece of code for darwin to deal with app issues. I have never used is so not sure if it helps.

https://github.com/jpype-project/jpype/blob/master/jpype/_gui.py

@pvtodorov
Copy link

I am still encountering this issue with the latest install.
Running

import imagej
ij = imagej.init('sc.fiji:fiji:2.1.1', headless=False)

Causes the notebook to hang.

Is there a workaround or fix for this on MacOS?
Running Catalina 10.15.7 (19H15)

Conda env:

name: imagej
channels:
  - conda-forge
  - defaults
dependencies:
  - appnope=0.1.2=py38h50d1736_1
  - argon2-cffi=20.1.0=py38h5406a74_2
  - async_generator=1.10=py_0
  - attrs=20.3.0=pyhd3deb0d_0
  - backcall=0.2.0=pyh9f0ad1d_0
  - backports=1.0=py_2
  - backports.functools_lru_cache=1.6.1=py_0
  - bleach=3.3.0=pyh44b312d_0
  - ca-certificates=2020.12.5=h033912b_0
  - certifi=2020.12.5=py38h50d1736_1
  - cffi=1.14.5=py38ha97d567_0
  - cycler=0.10.0=py_2
  - dbus=1.13.6=h0c50699_1
  - decorator=4.4.2=py_0
  - defusedxml=0.7.1=pyhd8ed1ab_0
  - entrypoints=0.3=pyhd8ed1ab_1003
  - expat=2.2.10=h1c7c35f_0
  - freetype=2.10.4=h4cff582_1
  - gettext=0.19.8.1=h7937167_1005
  - glib=2.66.7=he49afe7_1
  - glib-tools=2.66.7=he49afe7_1
  - icu=68.1=h74dc148_0
  - imglyb=1.0.0=pyh050c7b8_0
  - importlib-metadata=3.7.3=py38h50d1736_0
  - ipykernel=5.5.0=py38h9bb44b7_1
  - ipython=7.21.0=py38h9bb44b7_0
  - ipython_genutils=0.2.0=py_1
  - ipywidgets=7.6.3=pyhd3deb0d_0
  - jedi=0.18.0=py38h50d1736_2
  - jgo=1.0.0=pyhd8ed1ab_0
  - jinja2=2.11.3=pyh44b312d_0
  - jpeg=9d=hbcb3906_0
  - jpype1=1.2.1=py38hd9c93a9_0
  - jsonschema=3.2.0=pyhd8ed1ab_3
  - jupyter=1.0.0=py38h50d1736_6
  - jupyter_client=6.1.12=pyhd8ed1ab_0
  - jupyter_console=6.3.0=pyhd8ed1ab_0
  - jupyter_core=4.7.1=py38h50d1736_0
  - jupyterlab_pygments=0.1.2=pyh9f0ad1d_0
  - jupyterlab_widgets=1.0.0=pyhd8ed1ab_1
  - kiwisolver=1.3.1=py38hd9c93a9_1
  - krb5=1.17.2=h60d9502_0
  - lcms2=2.12=h577c468_0
  - libblas=3.9.0=8_openblas
  - libcblas=3.9.0=8_openblas
  - libclang=11.1.0=default_he082bbe_0
  - libcxx=11.1.0=habf9029_0
  - libedit=3.1.20191231=h0678c8f_2
  - libffi=3.3=h046ec9c_2
  - libgfortran=5.0.0=9_3_0_h6c81a4c_20
  - libgfortran5=9.3.0=h6c81a4c_20
  - libglib=2.66.7=hd556434_1
  - libiconv=1.16=haf1e3a3_0
  - liblapack=3.9.0=8_openblas
  - libllvm11=11.1.0=hd011deb_0
  - libopenblas=0.3.12=openmp_h54245bb_1
  - libpng=1.6.37=h7cec526_2
  - libpq=13.1=h052a64a_2
  - libsodium=1.0.18=hbcb3906_1
  - libtiff=4.2.0=h355d032_0
  - libwebp-base=1.2.0=h0d85af4_2
  - llvm-openmp=11.0.1=h7c73e74_0
  - lz4-c=1.9.3=h046ec9c_0
  - markupsafe=1.1.1=py38h5406a74_3
  - matplotlib=3.3.4=py38h50d1736_0
  - matplotlib-base=3.3.4=py38hb24f337_0
  - maven=3.6.0=0
  - mistune=0.8.4=py38h5406a74_1003
  - mysql-common=8.0.23=h694c41f_1
  - mysql-libs=8.0.23=hbeb7981_1
  - nbclient=0.5.3=pyhd8ed1ab_0
  - nbconvert=6.0.7=py38h50d1736_3
  - nbformat=5.1.2=pyhd8ed1ab_1
  - ncurses=6.2=h2e338ed_4
  - nest-asyncio=1.4.3=pyhd8ed1ab_0
  - notebook=6.2.0=py38h50d1736_0
  - nspr=4.30=hcd9eead_0
  - nss=3.47=hc0980d9_0
  - numpy=1.20.1=py38h64deac9_0
  - olefile=0.46=pyh9f0ad1d_1
  - openjdk=8.0.282=h0d85af4_0
  - openssl=1.1.1j=hbcf498f_0
  - packaging=20.9=pyh44b312d_0
  - pandas=1.2.3=py38h1588c1c_0
  - pandoc=2.12=h0d85af4_0
  - pandocfilters=1.4.2=py_1
  - parso=0.8.1=pyhd8ed1ab_0
  - pcre=8.44=hb1e8313_0
  - pexpect=4.8.0=pyh9f0ad1d_2
  - pickleshare=0.7.5=py_1003
  - pillow=8.1.2=py38h83525de_0
  - pip=21.0.1=pyhd8ed1ab_0
  - prometheus_client=0.9.0=pyhd3deb0d_0
  - prompt-toolkit=3.0.17=pyha770c72_0
  - prompt_toolkit=3.0.17=hd8ed1ab_0
  - psutil=5.8.0=py38h5406a74_1
  - ptyprocess=0.7.0=pyhd3deb0d_0
  - pycparser=2.20=pyh9f0ad1d_2
  - pygments=2.8.1=pyhd8ed1ab_0
  - pyimagej=1.0.0=py38h50d1736_0
  - pyparsing=2.4.7=pyh9f0ad1d_0
  - pyqt=5.12.3=py38h50d1736_7
  - pyqt-impl=5.12.3=py38h721a93c_7
  - pyqt5-sip=4.19.18=py38h5745d40_7
  - pyqtchart=5.12=py38h721a93c_7
  - pyqtwebengine=5.12.1=py38h721a93c_7
  - pyrsistent=0.17.3=py38h5406a74_2
  - python=3.8.8=h4e93d89_0_cpython
  - python-dateutil=2.8.1=py_0
  - python_abi=3.8=1_cp38
  - pytz=2021.1=pyhd8ed1ab_0
  - pyzmq=22.0.3=py38hd3b92b6_1
  - qt=5.12.9=h126340a_4
  - qtconsole=5.0.3=pyhd8ed1ab_0
  - qtpy=1.9.0=py_0
  - readline=8.0=h0678c8f_2
  - scyjava=1.1.0=pyhd8ed1ab_0
  - send2trash=1.5.0=py_0
  - setuptools=49.6.0=py38h50d1736_3
  - six=1.15.0=pyh9f0ad1d_0
  - sqlite=3.34.0=h17101e1_0
  - terminado=0.9.2=py38h50d1736_0
  - testpath=0.4.4=py_0
  - tk=8.6.10=h0419947_1
  - tornado=6.1=py38h5406a74_1
  - traitlets=5.0.5=py_0
  - wcwidth=0.2.5=pyh9f0ad1d_2
  - webencodings=0.5.1=py_1
  - wheel=0.36.2=pyhd3deb0d_0
  - widgetsnbextension=3.5.1=py38h50d1736_4
  - xarray=0.17.0=pyhd8ed1ab_0
  - xz=5.2.5=haf1e3a3_1
  - zeromq=4.3.4=h1c7c35f_0
  - zipp=3.4.1=pyhd8ed1ab_0
  - zlib=1.2.11=h7795811_1010
  - zstd=1.4.9=h582d3a0_0
prefix: /usr/local/anaconda3/envs/imagej

@ctrueden ctrueden added this to the m1 milestone Nov 5, 2021
@ctrueden ctrueden added this to To do in Road to publication via automation Nov 5, 2021
ctrueden added a commit that referenced this issue Dec 7, 2021
This uses jpype.setupGuiEnvironment. It works, but:

* You must have pyobjc installed from pip
  (conda packages did not work for me).

* With this change, it is now inconsistent whether
  imagej.init(headless=False) pops a GUI or not. On macOS yes, on
  Linux and Windows no, with ij.ui().showUI() needing to be called.

* This change does not fix the fact that imagej.init(headless=False)
  is non-blocking on Linux and Windows, but blocking on macOS. This
  is because on macOS the main thread must eternal stay busy doing
  the console event loop from this point forward.

See #23.
@ctrueden
Copy link
Member Author

ctrueden commented Dec 7, 2021

One year later, finally had time to look at this again. @Thrameos As usual you are correct: that code is exactly what we needed, thank you. Much more concise than the OSXAWTwrapper script. But still requires pyobjc to be installed.

I have now pushed a couple of commits making the situation less horrible on macOS: 27aea88 and f54e59e. @hinerm @elevans I would value your feedback. In particular:

  • There are now two private functions _create_jvm and _create_gateway, whose names are still up for debate, which perform the two major parts of initialization.
  • More importantly, while this update makes imagej.init(headless=False) do something useful on macOS, it does something that is reasonable but different from what happens on Linux or Windows: A) it automatically shows the GUI (by calling ij.ui().showUI() under the hood), and B) it blocks the imagej.init call forever, because the main thread becomes tied up doing the console event loop, which is necessary for Java AWT to work. I do not know how to avoid this—we could start new threads in Python, but I don't think it works to invoke AppHelper.runConsoleEventLoop() from non-main threads; I tried via a new threading.Thread and got a SIGSEGV.

So: thoughts? Ways to improve this, so we can finally put this issue to rest? Perhaps something as simple as adjusting the imagej.init API so it's consistent across platforms, but fails fast when certain modes are requested? E.g. imagej.init(mode="gui") could work on all platforms and have the macOS behavior of automatically showing the GUI and blocking, whereas imagej.init(mode="interactive") could show the GUI but not block, and fail fast on macOS?

@ctrueden ctrueden moved this from To do to In progress in Road to publication Dec 7, 2021
@ctrueden
Copy link
Member Author

ctrueden commented Dec 8, 2021

My current thinking is to add a new mode argument to imagej.init that takes one of three values: imagej.Mode.HEADLESS, imagej.Mode.GUI, or imagej.Mode.INTERACTIVE. The mapping would be:

  • init(headless=True)init(mode=Mode.HEADLESS)
  • init(headless=False)init(mode=Mode.INTERACTIVE) except on macOS where it would throw an exception; and
  • init(mode=Mode.GUI) is a new thing that works on all three platforms, calling ij.ui().showUI() automatically and then blocking.

For Mode.GUI we probably don't want to make any promises about when the blocking ends, if ever...

For the old headless flag, I would deprecate it (print a warning if used), but leave it in for backwards compatibility.

@elevans
Copy link
Member

elevans commented Dec 8, 2021

Here are my 2 cents:

  • _create_jvm and _create_gateway make sense to me and I don't have any issues (or anything better to suggest) with regards to their names.
  • Adding this mode parameter makes sense to me and obviously gives our MacOS users a better experience. If I understand this issue correctly, mode=Mode.GUI blocks calling the main thread which would prevent new imagej.init calls. I don't have a mac, but would that also mean the REPL is also tied up (i.e. not interactive)?

Road to publication automation moved this from In progress to Complete Dec 8, 2021
@ctrueden
Copy link
Member Author

ctrueden commented Dec 8, 2021

@elevans Correct on all counts. At least this way, you can get a GUI on macOS. Previously, if you tried, it would just hang forever. I do not know a way to get a GUI on macOS without blocking, within the same process/subprocess. We could do it using multiprocessing maybe to start a separate python instance, but I don't know the performance ramifications of that relating to passing of numpy memory pointers. Maybe @hinerm can comment more specifically on that.

prevent new imagej.init calls.

From the main thread, yes. But if someone is running Python code on other threads, those could presumably still make new ImageJ2 gateways.

ctrueden added a commit that referenced this issue Feb 18, 2022
With #23 being fixed, it is probably not relevant anymore.

If someone reports that the default (legacy) UI does not work on macOS
for them, but the ImageJ2 Swing UI does, we can reintroduce something.
@NicoKiaru
Copy link

Is there an issue that we can track regarding the absence of interactive mode for Mac OSX ?

@imagesc-bot
Copy link

This issue has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/abba-aligning-big-brains-and-atlases-v0-8-0-released/91229/1

@ctrueden
Copy link
Member Author

Is there an issue that we can track regarding the absence of interactive mode for Mac OSX ?

Issues relating to this problem are tagged with macos-gui.

@Thrameos
Copy link

For a long time I have been trying to get an option in JPype which will launch the Java JVM in another thread automatically. Unfortunately I have never had access to a Mac so I have not had any opportunities to test the solution.

The required mods would be to

  • have startJVM launch a thread rather than the JVM, then wait for the thread to to signal.
  • The thread calls the logic for startJVM signals the startJVM to continue, then waits for a signal to quit.
  • The shutdownJVM would then signal the JVM thread to proceed to the shutdown the JVM and then wait for the JVM to complete.

It should be 20 liner for someone who knows Python threading system.

If someone can mod JPype, test in on a Mac to confirm that getting the JVM off the main thread fixes the gui issue, and the submit it I would appreciate it.

@elevans
Copy link
Member

elevans commented Jan 30, 2024

Hi @Thrameos,

Thanks for this reply, I really appreciate it! This problem is a real pain point for a lot of our users. I have access to a Mac here so I'll give what you suggested a try 🚀. I'm pretty busy but I'll try to report back here in a few days.

@elevans
Copy link
Member

elevans commented Jan 30, 2024

Okay I couldn't resist. I forked jpype and tried your suggestions. It works on my system (Ubuntu 22.04.3 LTS) and I'm able to use the ImageJ2 GUI (Java AWT). Strangely however the imagej-legacy layer doesn't work so I'll need to figure that out. Here's the trace if you try to use the legacy layer:

>>> import imagej
>>> ij = imagej.init()
Traceback (most recent call last):
  File "ImageJ.java", line 75, in net.imagej.ImageJ.<init>
java.lang.java.lang.NullPointerException: java.lang.NullPointerException

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "LegacyService.java", line 142, in net.imagej.legacy.LegacyService.<clinit>
java.lang.java.lang.RuntimeException: java.lang.RuntimeException: Found incompatible ImageJ class

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "ImageJ.java", line 75, in net.imagej.ImageJ.<init>
java.lang.java.lang.ExceptionInInitializerError: java.lang.ExceptionInInitializerError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "ImageJ.java", line 75, in net.imagej.ImageJ.<init>
Exception: Java Exception

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/edward/Documents/repos/loci/pyimagej/src/imagej/__init__.py", line 1249, in init
    return _create_gateway()
           ^^^^^^^^^^^^^^^^^
  File "/home/edward/Documents/repos/loci/pyimagej/src/imagej/__init__.py", line 1279, in _create_gateway
    ij = ImageJ()
         ^^^^^^^^
java.lang.java.lang.IllegalArgumentException: java.lang.IllegalArgumentException: Invalid service: net.imagej.legacy.LegacyService

I haven't tried this on a Mac yet. I'll try to get on one tomorrow.

@ctrueden
Copy link
Member Author

ctrueden commented Feb 13, 2024

@NicoKiaru We filed a dedicated issue for getting interactive mode working on macOS: #298.

The investigations @elevans mentioned above did not actually pan out. See #298 for details.

Meanwhile, I was able to get a Python REPL simultaneously with a Java GUI by starting Python in a separate thread using pthread_create, then running the AppKit event loop using CoreFoundation's CFRunLoopRun. This is the same trick we use in the ImageJ Launcher to start Java off the main thread; turns out it also works if you run Py_Main and launch Java via JPype from there. No need for setupGuiEnvironment anymore either if you do it this way.

python.c
#include <stdio.h>
#include <dlfcn.h>

#include <CoreFoundation/CoreFoundation.h>
#include <pthread.h>

static void dummy_call_back(void *info) { }

static int start_python() {
    // Load libpython dynamically.
    const char *libpython_path =
        "/usr/local/Caskroom/mambaforge/base/envs/pyimagej-dev/lib/libpython3.10.dylib";
    void *libpython = dlopen(libpython_path, RTLD_LAZY);
    if (!libpython) {
        fprintf(stderr, "Error loading libpython3.10: %s\n", dlerror());
        return 1;
    }

    typedef int (*Py_MainFunc)(int, char **);
    Py_MainFunc Py_Main = (Py_MainFunc)dlsym(libpython, "Py_Main");
    if (!Py_Main) {
        fprintf(stderr, "Error finding Py_Main function: %s\n", dlerror());
        dlclose(libpython);
        return 1;
    }

    const char *args[] = {};
    int result = Py_Main(0, (char **)args);

    if (result != 0) {
      fprintf(stderr, "Error running Python script: %d\n", result);
      dlclose(libpython);
      return 1;
    }

    return 0;
}

static void *run_python(void *dummy) {
    exit(start_python());
}

int main() {
    // Start Python on a dedicated thread.
    pthread_t thread;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_create(&thread, &attr, run_python, NULL);
    pthread_attr_destroy(&attr);

    // Run the AppKit event loop here on the main thread.
    CFRunLoopSourceContext context;
    memset(&context, 0, sizeof(context));
    context.perform = &dummy_call_back;

    CFRunLoopSourceRef ref = CFRunLoopSourceCreate(NULL, 0, &context);
    CFRunLoopAddSource (CFRunLoopGetCurrent(), ref, kCFRunLoopCommonModes);
    CFRunLoopRun();

    return 0;
}

More to come on issue #298, hopefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

No branches or pull requests

8 participants