From fec5ceee42ff1991b50b5e3e3178bc31d16f1339 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Wed, 16 Nov 2022 13:29:13 -0600 Subject: [PATCH 01/12] In GUI mode, stash ImageJ gateway in a global var Otherwise, there is no way to access it from other threads. This may be useful for scenarios like napari-imagej, where access to Python scripting is available from threads other than the blocked one. --- src/imagej/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/imagej/__init__.py b/src/imagej/__init__.py index 211fb443..cd1d91db 100644 --- a/src/imagej/__init__.py +++ b/src/imagej/__init__.py @@ -1214,10 +1214,12 @@ def init( if mode == Mode.GUI: # Show the GUI and block. + global gateway if macos: # NB: This will block the calling (main) thread forever! try: - setupGuiEnvironment(lambda: _create_gateway().ui().showUI()) + gateway = _create_gateway() + setupGuiEnvironment(lambda: gateway.ui().showUI()) except ModuleNotFoundError as e: if e.msg == "No module named 'PyObjCTools'": advice = ( @@ -1243,7 +1245,8 @@ def init( # TODO: Poll using something better than ui().isVisible(). while gateway.ui().isVisible(): time.sleep(1) - return None + del gateway + return None else: # HEADLESS or INTERACTIVE mode: create the gateway and return it. return _create_gateway() From bd2e4d32b73a2deac02fd59526a0b6b7694bd9e6 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 30 Jan 2023 10:07:22 -0600 Subject: [PATCH 02/12] Make rai_lock property private This is an internal variable, not intended as public API. --- src/imagej/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/imagej/__init__.py b/src/imagej/__init__.py index cd1d91db..9078a8ff 100644 --- a/src/imagej/__init__.py +++ b/src/imagej/__init__.py @@ -60,7 +60,7 @@ __version__ = sj.get_version("pyimagej") _logger = logging.getLogger(__name__) -rai_lock = threading.Lock() +_rai_lock = threading.Lock() # Enable debug logging if DEBUG environment variable is set. try: @@ -1007,14 +1007,14 @@ def _op(self): def _ra(self): threadLocal = getattr(self, "_threadLocal", None) if threadLocal is None: - with rai_lock: + with _rai_lock: threadLocal = getattr(self, "_threadLocal", None) if threadLocal is None: threadLocal = threading.local() self._threadLocal = threadLocal ra = getattr(threadLocal, "ra", None) if ra is None: - with rai_lock: + with _rai_lock: ra = getattr(threadLocal, "ra", None) if ra is None: ra = self.randomAccess() From f38b200200e6acd3d6ca65cc673cb8d462cba27c Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 30 Jan 2023 21:29:52 -0600 Subject: [PATCH 03/12] Add when_imagej_starts callback mechanism --- src/imagej/__init__.py | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/imagej/__init__.py b/src/imagej/__init__.py index 9078a8ff..eb16a533 100644 --- a/src/imagej/__init__.py +++ b/src/imagej/__init__.py @@ -60,6 +60,7 @@ __version__ = sj.get_version("pyimagej") _logger = logging.getLogger(__name__) +_init_callbacks = [] _rai_lock = threading.Lock() # Enable debug logging if DEBUG environment variable is set. @@ -1212,14 +1213,25 @@ def init( if not success: raise RuntimeError("Failed to create a JVM with the requested environment.") + def run_callbacks(ij): + # invoke registered callback functions + for callback in _init_callbacks: + callback(ij) + return ij + if mode == Mode.GUI: # Show the GUI and block. global gateway + + def show_gui_and_run_callbacks(ij): + ij.ui().showUI() + run_callbacks(ij) + if macos: # NB: This will block the calling (main) thread forever! try: gateway = _create_gateway() - setupGuiEnvironment(lambda: gateway.ui().showUI()) + setupGuiEnvironment(lambda: show_gui_and_run_callbacks(gateway)) except ModuleNotFoundError as e: if e.msg == "No module named 'PyObjCTools'": advice = ( @@ -1240,16 +1252,34 @@ def init( else: # Create and show the application. gateway = _create_gateway() - gateway.ui().showUI() + show_gui_and_run_callbacks(gateway) # We are responsible for our own blocking. # TODO: Poll using something better than ui().isVisible(). while gateway.ui().isVisible(): time.sleep(1) + del gateway return None - else: - # HEADLESS or INTERACTIVE mode: create the gateway and return it. - return _create_gateway() + + # HEADLESS or INTERACTIVE mode: create the gateway and return it. + return run_callbacks(_create_gateway()) + + +def when_imagej_starts(f) -> None: + """ + Registers a function to be called immediately after ImageJ2 starts. + This is useful especially with GUI mode, to perform additional + configuration and operations following initialization of ImageJ2, + because the use of GUI mode blocks the calling thread indefinitely. + + :param f: Single-argument function to invoke during imagej.init(). + The function will be passed the newly created ImageJ2 Gateway + as its sole argument, and called as the final action of the + init function before it returns or blocks. + """ + # Add function to the list of callbacks to invoke upon start_jvm(). + global _init_callbacks + _init_callbacks.append(f) def imagej_main(): From 29c2f9e87b023258c71679dcfd0a7791ee885baa Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 23 Jan 2024 02:58:40 -0600 Subject: [PATCH 04/12] Simplify name of ImageJ2 gateway fixture It doesn't need to be "ij_wrapper" or "ij_fixture". It's OK to be ij! --- conftest.py | 8 +-- tests/test_ctypes.py | 6 +- tests/test_fiji.py | 16 ++--- tests/test_image_conversion.py | 116 ++++++++++++++++----------------- tests/test_labeling.py | 28 ++++---- tests/test_legacy.py | 70 ++++++++++---------- tests/test_ops.py | 26 ++++---- 7 files changed, 134 insertions(+), 136 deletions(-) diff --git a/conftest.py b/conftest.py index d7cd1130..9c723387 100644 --- a/conftest.py +++ b/conftest.py @@ -34,7 +34,7 @@ def pytest_addoption(parser): @pytest.fixture(scope="session") -def ij_fixture(request): +def ij(request): """ Create an ImageJ instance to be used by the whole testing environment :param request: Pytest variable passed in to fixtures @@ -44,11 +44,11 @@ def ij_fixture(request): headless = request.config.getoption("--headless") mode = "headless" if headless else "interactive" - ij_wrapper = imagej.init(ij_dir, mode=mode, add_legacy=legacy) + ij = imagej.init(ij_dir, mode=mode, add_legacy=legacy) - yield ij_wrapper + yield ij - ij_wrapper.dispose() + ij.dispose() def str2bool(v): diff --git a/tests/test_ctypes.py b/tests/test_ctypes.py index 38fd159c..ecbcca12 100644 --- a/tests/test_ctypes.py +++ b/tests/test_ctypes.py @@ -29,14 +29,14 @@ @pytest.mark.parametrize(argnames="ctype,jtype_str,value", argvalues=parameters) -def test_ctype_to_realtype(ij_fixture, ctype, jtype_str, value): +def test_ctype_to_realtype(ij, ctype, jtype_str, value): py_type = ctype(value) # Convert the ctype into a RealType - converted = ij_fixture.py.to_java(py_type) + converted = ij.py.to_java(py_type) jtype = sj.jimport(jtype_str) assert isinstance(converted, jtype) assert converted.get() == value # Convert the RealType back into a ctype - converted_back = ij_fixture.py.from_java(converted) + converted_back = ij.py.from_java(converted) assert isinstance(converted_back, ctype) assert converted_back.value == value diff --git a/tests/test_fiji.py b/tests/test_fiji.py index 608a18f6..16c7d6dc 100644 --- a/tests/test_fiji.py +++ b/tests/test_fiji.py @@ -4,23 +4,23 @@ # -- Tests -- -def test_plugins_load_using_pairwise_stitching(ij_fixture): +def test_plugins_load_using_pairwise_stitching(ij): try: sj.jimport("plugin.Stitching_Pairwise") except TypeError: pytest.skip("No Pairwise Stitching plugin available. Skipping test.") - if not ij_fixture.legacy: + if not ij.legacy: pytest.skip("No original ImageJ. Skipping test.") - if ij_fixture.ui().isHeadless(): + if ij.ui().isHeadless(): pytest.skip("No GUI. Skipping test.") - tile1 = ij_fixture.IJ.createImage("Tile1", "8-bit random", 512, 512, 1) - tile2 = ij_fixture.IJ.createImage("Tile2", "8-bit random", 512, 512, 1) + tile1 = ij.IJ.createImage("Tile1", "8-bit random", 512, 512, 1) + tile2 = ij.IJ.createImage("Tile2", "8-bit random", 512, 512, 1) args = {"first_image": tile1.getTitle(), "second_image": tile2.getTitle()} - ij_fixture.py.run_plugin("Pairwise stitching", args) - result_name = ij_fixture.WindowManager.getCurrentImage().getTitle() + ij.py.run_plugin("Pairwise stitching", args) + result_name = ij.WindowManager.getCurrentImage().getTitle() - ij_fixture.IJ.run("Close All", "") + ij.IJ.run("Close All", "") assert result_name == "Tile1<->Tile2" diff --git a/tests/test_image_conversion.py b/tests/test_image_conversion.py index 74643227..07d8ace6 100644 --- a/tests/test_image_conversion.py +++ b/tests/test_image_conversion.py @@ -14,12 +14,12 @@ # -- Image helpers -- -def get_img(ij_fixture): +def get_img(ij): # Create img dims = sj.jarray("j", [5]) for i in range(len(dims)): dims[i] = i + 1 - img = ij_fixture.op().run("create.img", dims) + img = ij.op().run("create.img", dims) # Populate img with random data cursor = img.cursor() @@ -30,13 +30,13 @@ def get_img(ij_fixture): return img -def get_imgplus(ij_fixture): +def get_imgplus(ij): """Get a 7D ImgPlus.""" # get java resources Random = sj.jimport("java.util.Random") Axes = sj.jimport("net.imagej.axis.Axes") UnsignedByteType = sj.jimport("net.imglib2.type.numeric.integer.UnsignedByteType") - DatasetService = ij_fixture.get("net.imagej.DatasetService") + DatasetService = ij.get("net.imagej.DatasetService") # test image parameters foo = Axes.get("foo") @@ -194,9 +194,9 @@ def assert_xarray_coords_equal_to_rai_coords(xarr, rai): assert xarr_dim_coords[i] == rai_dim_coords[i] -def assert_inverted_xarr_equal_to_xarr(dataset, ij_fixture, xarr): +def assert_inverted_xarr_equal_to_xarr(dataset, ij, xarr): # Reversing back to xarray yields original results - invert_xarr = ij_fixture.py.from_java(dataset) + invert_xarr = ij.py.from_java(dataset) assert (xarr.values == invert_xarr.values).all() assert list(xarr.dims) == list(invert_xarr.dims) for key in xarr.coords: @@ -313,8 +313,8 @@ def assert_permuted_rai_equal_to_source_rai(imgplus): ), sample_name -def assert_xarray_equal_to_dataset(ij_fixture, xarr, dataset): - dataset = ij_fixture.py.to_java(xarr) +def assert_xarray_equal_to_dataset(ij, xarr, dataset): + dataset = ij.py.to_java(xarr) axes = [dataset.axis(axnum) for axnum in range(5)] labels = [axis.type().getLabel() for axis in axes] @@ -331,52 +331,52 @@ def assert_xarray_equal_to_dataset(ij_fixture, xarr, dataset): expected_labels = ["X", "Y", "Z", "Time", "Channel"] assert expected_labels == labels - assert xarr.attrs == ij_fixture.py.from_java(dataset.getProperties()) - assert xarr.name == ij_fixture.py.from_java(dataset.getName()) + assert xarr.attrs == ij.py.from_java(dataset.getProperties()) + assert xarr.name == ij.py.from_java(dataset.getName()) -def convert_img_and_assert_equality(ij_fixture, img): - nparr = ij_fixture.py.from_java(img) +def convert_img_and_assert_equality(ij, img): + nparr = ij.py.from_java(img) assert_ndarray_equal_to_img(img, nparr) -def convert_ndarray_and_assert_equality(ij_fixture, nparr): - img = ij_fixture.py.to_java(nparr) +def convert_ndarray_and_assert_equality(ij, nparr): + img = ij.py.to_java(nparr) assert_ndarray_equal_to_img(img, nparr) # -- Tests -- -def test_ndarray_converts_to_img(ij_fixture): - convert_ndarray_and_assert_equality(ij_fixture, get_nparr()) +def test_ndarray_converts_to_img(ij): + convert_ndarray_and_assert_equality(ij, get_nparr()) -def test_img_converts_to_ndarray(ij_fixture): - convert_img_and_assert_equality(ij_fixture, get_img(ij_fixture)) +def test_img_converts_to_ndarray(ij): + convert_img_and_assert_equality(ij, get_img(ij)) -def test_cstyle_array_with_labeled_dims_converts(ij_fixture): +def test_cstyle_array_with_labeled_dims_converts(ij): xarr = get_xarr() - assert_xarray_equal_to_dataset(ij_fixture, xarr, ij_fixture.py.to_java(xarr)) + assert_xarray_equal_to_dataset(ij, xarr, ij.py.to_java(xarr)) -def test_fstyle_array_with_labeled_dims_converts(ij_fixture): +def test_fstyle_array_with_labeled_dims_converts(ij): xarr = get_xarr("F") - assert_xarray_equal_to_dataset(ij_fixture, xarr, ij_fixture.py.to_java(xarr)) + assert_xarray_equal_to_dataset(ij, xarr, ij.py.to_java(xarr)) -def test_7d_rai_to_python_permute(ij_fixture): - assert_permuted_rai_equal_to_source_rai(get_imgplus(ij_fixture)) +def test_7d_rai_to_python_permute(ij): + assert_permuted_rai_equal_to_source_rai(get_imgplus(ij)) -def test_dataset_converts_to_xarray(ij_fixture): +def test_dataset_converts_to_xarray(ij): xarr = get_xarr() - dataset = ij_fixture.py.to_java(xarr) - assert_inverted_xarr_equal_to_xarr(dataset, ij_fixture, xarr) + dataset = ij.py.to_java(xarr) + assert_inverted_xarr_equal_to_xarr(dataset, ij, xarr) -def test_image_metadata_conversion(ij_fixture): +def test_image_metadata_conversion(ij): # Create a ImageMetadata DefaultImageMetadata = sj.jimport("io.scif.DefaultImageMetadata") IdentityAxis = sj.jimport("net.imagej.axis.IdentityAxis") @@ -386,7 +386,7 @@ def test_image_metadata_conversion(ij_fixture): lengths[1] = 2 metadata.populate( "test", # name - ij_fixture.py.to_java([IdentityAxis(), IdentityAxis()]), # axes + ij.py.to_java([IdentityAxis(), IdentityAxis()]), # axes lengths, 4, # pixelType 8, # bitsPerPixel @@ -402,7 +402,7 @@ def test_image_metadata_conversion(ij_fixture): metadata.setThumbSizeY(metadata.getThumbSizeY()) metadata.setInterleavedAxisCount(metadata.getInterleavedAxisCount()) # Convert to python - py_data = ij_fixture.py.from_java(metadata) + py_data = ij.py.from_java(metadata) # Assert equality assert py_data["thumbSizeX"] == metadata.getThumbSizeX() assert py_data["thumbSizeY"] == metadata.getThumbSizeY() @@ -423,40 +423,40 @@ def test_image_metadata_conversion(ij_fixture): assert py_data["tables"] == metadata.getTables() -def test_rgb_image_maintains_correct_dim_order_on_conversion(ij_fixture): +def test_rgb_image_maintains_correct_dim_order_on_conversion(ij): xarr = get_xarr() - dataset = ij_fixture.py.to_java(xarr) + dataset = ij.py.to_java(xarr) axes = [dataset.axis(axnum) for axnum in range(5)] labels = [axis.type().getLabel() for axis in axes] assert ["X", "Y", "Z", "Time", "Channel"] == labels # Test that automatic axis swapping works correctly - numpy_image = ij_fixture.py.initialize_numpy_image(dataset) - raw_values = ij_fixture.py.rai_to_numpy(dataset, numpy_image) + numpy_image = ij.py.initialize_numpy_image(dataset) + raw_values = ij.py.rai_to_numpy(dataset, numpy_image) assert (xarr.values == np.moveaxis(raw_values, 0, -1)).all() - assert_inverted_xarr_equal_to_xarr(dataset, ij_fixture, xarr) + assert_inverted_xarr_equal_to_xarr(dataset, ij, xarr) -def test_no_coords_or_dims_in_xarr(ij_fixture): +def test_no_coords_or_dims_in_xarr(ij): xarr = get_xarr("NoDims") - dataset = ij_fixture.py.from_java(xarr) - assert_inverted_xarr_equal_to_xarr(dataset, ij_fixture, xarr) + dataset = ij.py.from_java(xarr) + assert_inverted_xarr_equal_to_xarr(dataset, ij, xarr) -def test_linear_coord_on_xarr_conversion(ij_fixture): +def test_linear_coord_on_xarr_conversion(ij): xarr = get_xarr() - dataset = ij_fixture.py.to_java(xarr) + dataset = ij.py.to_java(xarr) axes = dataset.dim_axes # all axes should be DefaultLinearAxis for ax in axes: assert isinstance(ax, jc.DefaultLinearAxis) -def test_non_linear_coord_on_xarr_conversion(ij_fixture): +def test_non_linear_coord_on_xarr_conversion(ij): xarr = get_non_linear_coord_xarr() - dataset = ij_fixture.py.to_java(xarr) + dataset = ij.py.to_java(xarr) axes = dataset.dim_axes # axes [0, 1] should be EnumeratedAxis with axis 2 as DefaultLinearAxis for i in range(2): @@ -464,18 +464,18 @@ def test_non_linear_coord_on_xarr_conversion(ij_fixture): assert isinstance(axes[-1], jc.DefaultLinearAxis) -def test_non_numeric_coord_on_xarr_conversion(ij_fixture): +def test_non_numeric_coord_on_xarr_conversion(ij): xarr = get_non_numeric_coord_xarr() - dataset = ij_fixture.py.to_java(xarr) + dataset = ij.py.to_java(xarr) axes = dataset.dim_axes # all axes should be DefaultLinearAxis for ax in axes: assert isinstance(ax, jc.DefaultLinearAxis) -def test_index_image_converts_to_imglib_roi(ij_fixture): +def test_index_image_converts_to_imglib_roi(ij): index_narr = get_index_narr() - roi_tree = convert.index_img_to_roi_tree(ij_fixture, index_narr) + roi_tree = convert.index_img_to_roi_tree(ij, index_narr) # ROI dimensions (max_a, max_b, min_a, min_b) ref_roi_dims = ( (150.0, 150.0, 50.0, 50.0), @@ -488,8 +488,8 @@ def test_index_image_converts_to_imglib_roi(ij_fixture): rois.append(roi_tree.children().get(i).data()) # contour/ROI dimensions should match roi_dims for i in range(3): - max_dims = ij_fixture.py.from_java(rois[i].maxAsDoubleArray()) - min_dims = ij_fixture.py.from_java(rois[i].minAsDoubleArray()) + max_dims = ij.py.from_java(rois[i].maxAsDoubleArray()) + min_dims = ij.py.from_java(rois[i].minAsDoubleArray()) roi_dims = np.concatenate((max_dims, min_dims)) for j in range(4): assert ref_roi_dims[i][j] == roi_dims[j] @@ -568,21 +568,21 @@ def test_index_image_converts_to_imglib_roi(ij_fixture): argvalues=dataset_conversion_parameters, ) def test_direct_to_dataset_conversions( - ij_fixture, im_req, obj_type, new_dims, exp_dims, exp_shape + ij, im_req, obj_type, new_dims, exp_dims, exp_shape ): # get image data if obj_type == "java": - im_data = im_req(ij_fixture) + im_data = im_req(ij) else: im_data = im_req() # convert the image data to net.image.Dataset - ds_out = ij_fixture.py.to_dataset(im_data, dim_order=new_dims) + ds_out = ij.py.to_dataset(im_data, dim_order=new_dims) assert ds_out.dims == exp_dims assert ds_out.shape == exp_shape if hasattr(im_data, "coords") and obj_type == "python": assert_xarray_coords_equal_to_rai_coords(im_data, ds_out) if images.is_xarraylike(im_data): - assert_xarray_equal_to_dataset(ij_fixture, im_data, ds_out) + assert_xarray_equal_to_dataset(ij, im_data, ds_out) if (images.is_arraylike is True) and (images.is_xarraylike is False): assert_ndarray_equal_to_img(ds_out, im_data) @@ -590,14 +590,14 @@ def test_direct_to_dataset_conversions( @pytest.mark.parametrize( argnames="im_req,obj_type,new_dims,exp_shape", argvalues=img_conversion_parameters ) -def test_direct_to_img_conversions(ij_fixture, im_req, obj_type, new_dims, exp_shape): +def test_direct_to_img_conversions(ij, im_req, obj_type, new_dims, exp_shape): # get image data if obj_type == "java": - im_data = im_req(ij_fixture) + im_data = im_req(ij) else: im_data = im_req() # convert the image data to Img - img_out = ij_fixture.py.to_img(im_data, dim_order=new_dims) + img_out = ij.py.to_img(im_data, dim_order=new_dims) assert img_out.shape == exp_shape if images.is_xarraylike(im_data): assert_ndarray_equal_to_img( @@ -612,15 +612,15 @@ def test_direct_to_img_conversions(ij_fixture, im_req, obj_type, new_dims, exp_s argvalues=xarr_conversion_parameters, ) def test_direct_to_xarray_conversion( - ij_fixture, im_req, obj_type, new_dims, exp_dims, exp_shape + ij, im_req, obj_type, new_dims, exp_dims, exp_shape ): # get image data if obj_type == "java": - im_data = im_req(ij_fixture) + im_data = im_req(ij) else: im_data = im_req() # convert the image data to xarray - xarr_out = ij_fixture.py.to_xarray(im_data, dim_order=new_dims) + xarr_out = ij.py.to_xarray(im_data, dim_order=new_dims) assert xarr_out.dims == exp_dims assert xarr_out.shape == exp_shape if hasattr(im_data, "dim_axes") and obj_type == "java": diff --git a/tests/test_labeling.py b/tests/test_labeling.py index 0d423541..24a11680 100644 --- a/tests/test_labeling.py +++ b/tests/test_labeling.py @@ -31,16 +31,16 @@ def py_labeling(): @pytest.fixture(scope="module") -def java_labeling(ij_fixture): +def java_labeling(ij): img = np.zeros((4, 4), dtype=np.int32) img[:2, :2] = 6 img[:2, 2:] = 3 img[2:, :2] = 7 img[2:, 2:] = 4 - img_java = ij_fixture.py.to_java(img) + img_java = ij.py.to_java(img) example_lists = [[], [1], [2], [1, 2], [2, 3], [3], [1, 4], [3, 4]] sets = [set(example) for example in example_lists] - sets_java = ij_fixture.py.to_java(sets) + sets_java = ij.py.to_java(sets) ImgLabeling = sj.jimport("net.imglib2.roi.labeling.ImgLabeling") return ImgLabeling.fromImageAndLabelSets(img_java, sets_java) @@ -61,21 +61,21 @@ def assert_labels_equality( # -- Tests -- -def test_py_to_java(ij_fixture, py_labeling, java_labeling): - j_convert = ij_fixture.py.to_java(py_labeling) +def test_py_to_java(ij, py_labeling, java_labeling): + j_convert = ij.py.to_java(py_labeling) # Assert indexImg equality - expected_img = ij_fixture.py.from_java(java_labeling.getIndexImg()) - actual_img = ij_fixture.py.from_java(j_convert.getIndexImg()) + expected_img = ij.py.from_java(java_labeling.getIndexImg()) + actual_img = ij.py.from_java(j_convert.getIndexImg()) assert np.array_equal(expected_img, actual_img) # Assert label sets equality - expected_labels = ij_fixture.py.from_java(java_labeling.getMapping().getLabelSets()) - actual_labels = ij_fixture.py.from_java(j_convert.getMapping().getLabelSets()) + expected_labels = ij.py.from_java(java_labeling.getMapping().getLabelSets()) + actual_labels = ij.py.from_java(j_convert.getMapping().getLabelSets()) assert expected_labels == actual_labels -def test_java_to_py(ij_fixture, py_labeling, java_labeling): +def test_java_to_py(ij, py_labeling, java_labeling): # Convert - p_convert = ij_fixture.py.from_java(java_labeling) + p_convert = ij.py.from_java(java_labeling) # Assert indexImg equality exp_img, exp_labels = py_labeling.get_result() act_img, act_labels = p_convert.get_result() @@ -88,10 +88,10 @@ def test_java_to_py(ij_fixture, py_labeling, java_labeling): ) -def test_py_java_py(ij_fixture, py_labeling): +def test_py_java_py(ij, py_labeling): # Convert - to_java = ij_fixture.py.to_java(py_labeling) - back_to_py = ij_fixture.py.from_java(to_java) + to_java = ij.py.to_java(py_labeling) + back_to_py = ij.py.from_java(to_java) print(py_labeling.label_sets) print(back_to_py.label_sets) # Assert indexImg equality diff --git a/tests/test_legacy.py b/tests/test_legacy.py index 217d2aba..84077c33 100644 --- a/tests/test_legacy.py +++ b/tests/test_legacy.py @@ -14,8 +14,8 @@ def arr(): @pytest.fixture(scope="module") -def results_table(ij_fixture): - if ij_fixture.legacy and ij_fixture.legacy.isActive(): +def results_table(ij): + if ij.legacy and ij.legacy.isActive(): ResultsTable = sj.jimport("ij.measure.ResultsTable") rt = ResultsTable.getResultsTable() @@ -55,13 +55,13 @@ def ensure_legacy_enabled(ij): # -- Tests -- -def test_convert_imageplus_to_python(ij_fixture): - ensure_legacy_enabled(ij_fixture) +def test_convert_imageplus_to_python(ij): + ensure_legacy_enabled(ij) w = 30 h = 20 - imp = ij_fixture.IJ.createImage("Ramp", "16-bit ramp", w, h, 2, 3, 5) - xarr = ij_fixture.py.from_java(imp) + imp = ij.IJ.createImage("Ramp", "16-bit ramp", w, h, 2, 3, 5) + xarr = ij.py.from_java(imp) assert xarr.dims == ("t", "pln", "row", "col", "ch") assert xarr.shape == (5, 3, h, w, 2) @@ -82,11 +82,11 @@ def test_convert_imageplus_to_python(ij_fixture): assert all((plane == xarr[t, z, :, :, c]).data.flatten()) -def test_run_plugin(ij_fixture): - ensure_legacy_enabled(ij_fixture) +def test_run_plugin(ij): + ensure_legacy_enabled(ij) - ramp = ij_fixture.IJ.createImage("Tile1", "8-bit ramp", 10, 10, 1) - ij_fixture.py.run_plugin("Gaussian Blur...", args={"sigma": 3}, imp=ramp) + ramp = ij.IJ.createImage("Tile1", "8-bit ramp", 10, 10, 1) + ij.py.run_plugin("Gaussian Blur...", args={"sigma": 3}, imp=ramp) values = [ramp.getPixel(x, y)[0] for x in range(10) for y in range(10)] # fmt: off assert values == [ @@ -104,54 +104,54 @@ def test_run_plugin(ij_fixture): # fmt: on -def test_get_imageplus_synchronizes_from_imagej_to_imagej2(ij_fixture, arr): - ensure_legacy_enabled(ij_fixture) - ensure_gui_available(ij_fixture) +def test_get_imageplus_synchronizes_from_imagej_to_imagej2(ij, arr): + ensure_legacy_enabled(ij) + ensure_gui_available(ij) original = arr[0, 0] - ds = ij_fixture.py.to_java(arr) - ij_fixture.ui().show(ds) + ds = ij.py.to_java(arr) + ij.ui().show(ds) macro = """run("Add...", "value=5");""" - ij_fixture.py.run_macro(macro) + ij.py.run_macro(macro) assert arr[0, 0] == original + 5 -def test_synchronize_from_imagej_to_numpy(ij_fixture, arr): - ensure_legacy_enabled(ij_fixture) - ensure_gui_available(ij_fixture) +def test_synchronize_from_imagej_to_numpy(ij, arr): + ensure_legacy_enabled(ij) + ensure_gui_available(ij) original = arr[0, 0] - ds = ij_fixture.py.to_dataset(arr) - ij_fixture.ui().show(ds) - imp = ij_fixture.py.active_imageplus() + ds = ij.py.to_dataset(arr) + ij.ui().show(ds) + imp = ij.py.active_imageplus() imp.getProcessor().add(5) - ij_fixture.py.sync_image(imp) + ij.py.sync_image(imp) assert arr[0, 0] == original + 5 -def test_window_to_numpy_converts_active_image_to_xarray(ij_fixture, arr): - ensure_legacy_enabled(ij_fixture) - ensure_gui_available(ij_fixture) +def test_window_to_numpy_converts_active_image_to_xarray(ij, arr): + ensure_legacy_enabled(ij) + ensure_gui_available(ij) - ds = ij_fixture.py.to_dataset(arr) - ij_fixture.ui().show(ds) - new_arr = ij_fixture.py.active_xarray() + ds = ij.py.to_dataset(arr) + ij.ui().show(ds) + new_arr = ij.py.active_xarray() assert (arr == new_arr.values).all -def test_functions_throw_warning_if_legacy_not_enabled(ij_fixture): - ensure_legacy_disabled(ij_fixture) +def test_functions_throw_warning_if_legacy_not_enabled(ij): + ensure_legacy_disabled(ij) with pytest.raises(ImportError): - ij_fixture.py.active_imageplus() + ij.py.active_imageplus() -def test_results_table_to_pandas_dataframe(ij_fixture, results_table): - ensure_legacy_enabled(ij_fixture) +def test_results_table_to_pandas_dataframe(ij, results_table): + ensure_legacy_enabled(ij) - df = ij_fixture.py.from_java(results_table) + df = ij.py.from_java(results_table) for col in range(5): rt_col = list(results_table.getColumn(col)) df_col = df[f"Column {col}"].tolist() diff --git a/tests/test_ops.py b/tests/test_ops.py index be134ab0..b14c312f 100644 --- a/tests/test_ops.py +++ b/tests/test_ops.py @@ -4,13 +4,13 @@ # -- Tests -- -def test_frangi(ij_fixture): +def test_frangi(ij): input_array = np.array( [[1000, 1000, 1000, 2000, 3000], [5000, 8000, 13000, 21000, 34000]] ) result = np.zeros(input_array.shape) - ij_fixture.op().filter().frangiVesselness( - ij_fixture.py.to_java(result), ij_fixture.py.to_java(input_array), [1, 1], 4 + ij.op().filter().frangiVesselness( + ij.py.to_java(result), ij.py.to_java(input_array), [1, 1], 4 ) correct_result = np.array( [[0, 0, 0, 0.94282, 0.94283], [0, 0, 0, 0.94283, 0.94283]] @@ -19,14 +19,12 @@ def test_frangi(ij_fixture): assert (result == correct_result).all() -def test_gaussian(ij_fixture): +def test_gaussian(ij): input_array = np.array( [[1000, 1000, 1000, 2000, 3000], [5000, 8000, 13000, 21000, 34000]] ) sigmas = [10.0] * 2 - output_array = ( - ij_fixture.op().filter().gauss(ij_fixture.py.to_java(input_array), sigmas) - ) + output_array = ij.op().filter().gauss(ij.py.to_java(input_array), sigmas) result = [] correct_result = [8435, 8435, 8435, 8435] ra = output_array.randomAccess() @@ -37,7 +35,7 @@ def test_gaussian(ij_fixture): assert result == correct_result -def test_top_hat(ij_fixture): +def test_top_hat(ij): ArrayList = sj.jimport("java.util.ArrayList") HyperSphereShape = sj.jimport("net.imglib2.algorithm.neighborhood.HyperSphereShape") Views = sj.jimport("net.imglib2.view.Views") @@ -49,12 +47,12 @@ def test_top_hat(ij_fixture): [[1000, 1000, 1000, 2000, 3000], [5000, 8000, 13000, 21000, 34000]] ) output_array = np.zeros(input_array.shape) - java_out = Views.iterable(ij_fixture.py.to_java(output_array)) - java_in = ij_fixture.py.to_java(input_array) + java_out = Views.iterable(ij.py.to_java(output_array)) + java_in = ij.py.to_java(input_array) shapes = ArrayList() shapes.add(HyperSphereShape(5)) - ij_fixture.op().morphology().topHat(java_out, java_in, shapes) + ij.op().morphology().topHat(java_out, java_in, shapes) itr = java_out.iterator() while itr.hasNext(): result.append(itr.next().get()) @@ -62,15 +60,15 @@ def test_top_hat(ij_fixture): assert result == correct_result -def test_image_math(ij_fixture): +def test_image_math(ij): Views = sj.jimport("net.imglib2.view.Views") input_array = np.array([[1, 1, 2], [3, 5, 8]]) result = [] correct_result = [192, 198, 205, 192, 198, 204] - java_in = Views.iterable(ij_fixture.py.to_java(input_array)) + java_in = Views.iterable(ij.py.to_java(input_array)) java_out = ( - ij_fixture.op() + ij.op() .image() .equation(java_in, "64 * (Math.sin(0.1 * p[0]) + Math.cos(0.1 * p[1])) + 128") ) From ccb0e81cb3103e0756623185ddef8dac77effd03 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Mon, 13 Mar 2023 13:23:00 -0500 Subject: [PATCH 05/12] Add a simple test of when_imagej_starts callback This test checks that functions registered via when_imagej_starts get called as part of imagej.init. --- conftest.py | 2 ++ tests/test_callbacks.py | 7 +++++++ 2 files changed, 9 insertions(+) create mode 100644 tests/test_callbacks.py diff --git a/conftest.py b/conftest.py index 9c723387..21bfbb14 100644 --- a/conftest.py +++ b/conftest.py @@ -43,6 +43,8 @@ def ij(request): legacy = request.config.getoption("--legacy") headless = request.config.getoption("--headless") + imagej.when_imagej_starts(lambda ij: setattr(ij, "_when_imagej_starts_result", "success")) + mode = "headless" if headless else "interactive" ij = imagej.init(ij_dir, mode=mode, add_legacy=legacy) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py new file mode 100644 index 00000000..9e3f5ab5 --- /dev/null +++ b/tests/test_callbacks.py @@ -0,0 +1,7 @@ +def test_when_imagej_starts(ij): + """ + The ImageJ2 gateway test fixture registers a callback function via + when_imagej_starts, which injects a small piece of data into the gateway + object. We check for that data here to make sure the callback happened. + """ + assert "success" == getattr(ij, "_when_imagej_starts_result", None) From 221da1e0e984e0423a64811030aff70302895c15 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 25 May 2023 15:35:10 -0500 Subject: [PATCH 06/12] Create gateway, run callbacks in same function --- src/imagej/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/imagej/__init__.py b/src/imagej/__init__.py index eb16a533..25bf753d 100644 --- a/src/imagej/__init__.py +++ b/src/imagej/__init__.py @@ -1223,15 +1223,17 @@ def run_callbacks(ij): # Show the GUI and block. global gateway - def show_gui_and_run_callbacks(ij): - ij.ui().showUI() - run_callbacks(ij) + def show_gui_and_run_callbacks(): + global gateway + gateway = _create_gateway() + gateway.ui().showUI() + run_callbacks(gateway) + return gateway if macos: # NB: This will block the calling (main) thread forever! try: - gateway = _create_gateway() - setupGuiEnvironment(lambda: show_gui_and_run_callbacks(gateway)) + setupGuiEnvironment(show_gui_and_run_callbacks) except ModuleNotFoundError as e: if e.msg == "No module named 'PyObjCTools'": advice = ( @@ -1251,15 +1253,13 @@ def show_gui_and_run_callbacks(ij): raise else: # Create and show the application. - gateway = _create_gateway() - show_gui_and_run_callbacks(gateway) + gateway = show_gui_and_run_callbacks() # We are responsible for our own blocking. # TODO: Poll using something better than ui().isVisible(). while gateway.ui().isVisible(): time.sleep(1) - del gateway - return None + return gateway # HEADLESS or INTERACTIVE mode: create the gateway and return it. return run_callbacks(_create_gateway()) From 3bae68b9201896460ef4d134a1501765952c63f3 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 24 May 2024 15:05:14 -0500 Subject: [PATCH 07/12] Add docs section on how to use when_imagej_starts This commit adds section 5.2 to the "Convenience methods of PyImageJ" that describes how to use the when_image_starts callback mechanism. --- conftest.py | 4 +++- doc/05-Convenience-methods-of-PyImageJ.ipynb | 25 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 21bfbb14..7a345454 100644 --- a/conftest.py +++ b/conftest.py @@ -43,7 +43,9 @@ def ij(request): legacy = request.config.getoption("--legacy") headless = request.config.getoption("--headless") - imagej.when_imagej_starts(lambda ij: setattr(ij, "_when_imagej_starts_result", "success")) + imagej.when_imagej_starts( + lambda ij: setattr(ij, "_when_imagej_starts_result", "success") + ) mode = "headless" if headless else "interactive" ij = imagej.init(ij_dir, mode=mode, add_legacy=legacy) diff --git a/doc/05-Convenience-methods-of-PyImageJ.ipynb b/doc/05-Convenience-methods-of-PyImageJ.ipynb index ff201784..59f94a93 100644 --- a/doc/05-Convenience-methods-of-PyImageJ.ipynb +++ b/doc/05-Convenience-methods-of-PyImageJ.ipynb @@ -129,6 +129,31 @@ "source": [ "Note the warnings! We're currently in headless mode. The many legacy ImageJ functions operate limitedly or not at all in headless mode. For example the `RoiManager` is not functional in a true headless enviornment." ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5.2 Register functions to start with ImageJ\n", + "\n", + "Functions can be executed during ImageJ's initialization routine by registering the functions with PyImageJ's callback mechanism `when_imagej_starts()`. This is particularly useful for macOS users in `gui` mode, allowing functions to be called before the Python [REPL/interpreter](https://docs.python.org/3/tutorial/interpreter.html) is [blocked](Initialization.md/#gui-mode).\n", + "\n", + "The following example uses `when_imagej_starts()` callback display a to `uint16` 2D NumPy array it with ImageJ's viewer, print it's dimensions (_i.e._ shape) and open the `RoiManager` while ImageJ initializes.\n", + "\n", + "```python\n", + "import imagej\n", + "import numpy as np\n", + "\n", + "# register functions\n", + "arr = np.random.randint(0, 2**16, size=(256, 256), dtype=np.uint16) # create random 16-bit array\n", + "imagej.when_imagej_starts(lambda ij: ij.RoiManager.getRoiManager()) # open the RoiManager\n", + "imagej.when_imagej_starts(lambda ij: ij.ui().show(ij.py.to_dataset(arr))) # convert and display the array\n", + "imagej.when_imagej_starts(lambda _: print(f\"array shape: {arr.shape}\"))\n", + "\n", + "# initialize imagej\n", + "ij = imagej.init(mode='interactive')\n", + "```" + ] } ], "metadata": { From 735304b303bed3776b43b6d50b7e1756e3eb3644 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 12 Jul 2024 14:14:53 -0500 Subject: [PATCH 08/12] Add check to determine if current thread is main This commit introduces a check to determine if the current running thread is the main thread. If True, then we continue to block interactive mode on macOS (it is not possible to share the main thread with GUI event loop needed for the ImageJ GUI). If False, then the current thread is *not* the main thread (e.g. a jaunched session) and interactive mode for macOS can proceed. --- src/imagej/__init__.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/imagej/__init__.py b/src/imagej/__init__.py index 25bf753d..1d4bea2a 100644 --- a/src/imagej/__init__.py +++ b/src/imagej/__init__.py @@ -38,6 +38,7 @@ import sys import threading import time +from ctypes import cdll from enum import Enum from functools import lru_cache from pathlib import Path @@ -1206,7 +1207,11 @@ def init( macos = sys.platform == "darwin" if macos and mode == Mode.INTERACTIVE: - raise EnvironmentError("Sorry, the interactive mode is not available on macOS.") + # check for main thread only on macOS + if _macos_is_main_thread(): + raise EnvironmentError( + "Sorry, the interactive mode is not available on macOS." + ) if not sj.jvm_started(): success = _create_jvm(ij_dir_or_version_or_endpoint, mode, add_legacy) @@ -1517,6 +1522,24 @@ def _includes_imagej_legacy(items: list): return any(item.startswith("net.imagej:imagej-legacy") for item in items) +def _macos_is_main_thread(): + """Detect if the current thread is the main thread on macOS. + + :return: Boolean indicating if the current thread is the main thread. + """ + # try to load the pthread library + try: + pthread = cdll.LoadLibrary("libpthread.dylib") + except OSError as e: + print("No pthread library found.", e) + + # detect if main thread + if pthread.pthread_main_np() == 1: + return True + else: + return False + + def _set_ij_env(ij_dir): """ Create a list of required jars and add to the java classpath. From 7667a0904a3738652c930b6ac109752fdddfc9eb Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 15 Jul 2024 10:10:03 -0500 Subject: [PATCH 09/12] Use descriptive attribute name for callback test Use a more descriptive attribute name and value for when_imagej_starts() test. --- conftest.py | 4 +--- tests/test_callbacks.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/conftest.py b/conftest.py index 7a345454..45b9ddd7 100644 --- a/conftest.py +++ b/conftest.py @@ -43,9 +43,7 @@ def ij(request): legacy = request.config.getoption("--legacy") headless = request.config.getoption("--headless") - imagej.when_imagej_starts( - lambda ij: setattr(ij, "_when_imagej_starts_result", "success") - ) + imagej.when_imagej_starts(lambda ij: setattr(ij, "_testing", True)) mode = "headless" if headless else "interactive" ij = imagej.init(ij_dir, mode=mode, add_legacy=legacy) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 9e3f5ab5..564d0c53 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -4,4 +4,4 @@ def test_when_imagej_starts(ij): when_imagej_starts, which injects a small piece of data into the gateway object. We check for that data here to make sure the callback happened. """ - assert "success" == getattr(ij, "_when_imagej_starts_result", None) + assert True is getattr(ij, "_testing", True) From 972592dda94f983b51ea3c3e1f9b2ae390ddbde9 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Fri, 19 Jul 2024 09:59:23 -0500 Subject: [PATCH 10/12] Improve macOS pthread detection exception handling This commit adds the pthread library loading exception to the logger and assumes that the current thread is the main thread if no pthread library is found. --- src/imagej/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/imagej/__init__.py b/src/imagej/__init__.py index 1d4bea2a..fe4c9130 100644 --- a/src/imagej/__init__.py +++ b/src/imagej/__init__.py @@ -1530,10 +1530,13 @@ def _macos_is_main_thread(): # try to load the pthread library try: pthread = cdll.LoadLibrary("libpthread.dylib") - except OSError as e: - print("No pthread library found.", e) + except OSError as exc: + _log_exception(_logger, exc) + print("No pthread library found.") + # assume the current thread is the main thread + return True - # detect if main thread + # detect if the current thread is the main thread if pthread.pthread_main_np() == 1: return True else: From b2ce4cb21836bacc68338c6b2dc84fc879e081fd Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 14 Oct 2024 09:55:53 -0500 Subject: [PATCH 11/12] Use None as the getattr default, not True The default here should be None, not True, otherwise this test does not actually test for the `_testing` attribute value. --- tests/test_callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py index 564d0c53..ff4e030b 100644 --- a/tests/test_callbacks.py +++ b/tests/test_callbacks.py @@ -4,4 +4,4 @@ def test_when_imagej_starts(ij): when_imagej_starts, which injects a small piece of data into the gateway object. We check for that data here to make sure the callback happened. """ - assert True is getattr(ij, "_testing", True) + assert True is getattr(ij, "_testing", None) From 45c1448795a69a3cc6db4c6291eebe3e601c5e26 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Tue, 15 Oct 2024 17:30:19 -0500 Subject: [PATCH 12/12] Improve gui mode gateway clean up This commit improves how we clean up the gateway once the UI is closed in gui mode. Although it is highly unlikely, it is possible for a user to run into a NameError when deleting the gateway if a user enters gui mode but is NOT on macos and successfully exits the gui without crashing the session (see next below, this requires some playing around). This is resolved by assigning the gateway to None, ensuring there is *something* to delete if no gateway object is assigned. This commit also adds an additional check for a running JVM instance when in gui mode on non-macos systems. This prevents a `jpype._core.JVMNotRunning: Java Virtual Machine is not running` error that is produced when the imagej ui visbility is polled but is no longer available because the JVM has been shut down via gui interactions. --- src/imagej/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/imagej/__init__.py b/src/imagej/__init__.py index fe4c9130..72d4014b 100644 --- a/src/imagej/__init__.py +++ b/src/imagej/__init__.py @@ -1227,6 +1227,7 @@ def run_callbacks(ij): if mode == Mode.GUI: # Show the GUI and block. global gateway + gateway = None def show_gui_and_run_callbacks(): global gateway @@ -1261,10 +1262,11 @@ def show_gui_and_run_callbacks(): gateway = show_gui_and_run_callbacks() # We are responsible for our own blocking. # TODO: Poll using something better than ui().isVisible(). - while gateway.ui().isVisible(): + while sj.jvm_started() and gateway.ui().isVisible(): time.sleep(1) - return gateway + del gateway + return None # HEADLESS or INTERACTIVE mode: create the gateway and return it. return run_callbacks(_create_gateway())