diff --git a/.github/workflows/check_compatibility_deepimagej.yaml b/.github/workflows/check_compatibility_deepimagej.yaml new file mode 100644 index 00000000..86ab9d0b --- /dev/null +++ b/.github/workflows/check_compatibility_deepimagej.yaml @@ -0,0 +1,80 @@ +--- +name: check compatibility deepimagej +concurrency: deepimagej +on: + push: + branches: + - main + paths: + - .github/workflows/check_compatibility_deepimagej.yaml + - scripts/check_compatibility_deepimagej.py + - scripts/deepimagej_jython_scripts/** + workflow_dispatch: + schedule: + - cron: 0 1 * * * +jobs: + run: + strategy: + fail-fast: false + matrix: + include: + - name: ubuntu + os: ubuntu-latest + url_file_name: fiji-linux64.zip + fiji_executable: ImageJ-linux64 + runs-on: ${{ matrix.os }} + environment: 'production' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + - name: Install backoffice + run: pip install . + - run: wget https://uk1s3.embassy.ebi.ac.uk/public-datasets/bioimage.io/all_versions_draft.json #wget https://${{vars.S3_HOST}}/${{vars.S3_BUCKET}}/${{vars.S3_FOLDER}}/all_versions_draft.json + - run: wget https://uk1s3.embassy.ebi.ac.uk/public-datasets/bioimage.io/all_versions.json #wget https://${{vars.S3_HOST}}/${{vars.S3_BUCKET}}/${{vars.S3_FOLDER}}/all_versions.json + - name: Set up Fiji + shell: bash + run: | + mkdir -p fiji + curl -L -o fiji.zip https://downloads.imagej.net/fiji/latest/${{ matrix.url_file_name }} + unzip fiji.zip -d fiji + - name: Install deepimageJ + run: | + fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --update add-update-site "DeepImageJ" "https://sites.imagej.net/DeepImageJ/" + fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --update update + - name: Install engines + shell: bash + run: fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --console + scripts/deepimagej_jython_scripts/deepimagej_download_engines.py -engines_path + fiji/Fiji.app/engines + #- name: Run deepImageJ tests for drafts + # run: python scripts/check_compatibility_deepimagej.py all_versions_draft.json generated-reports fiji/Fiji.app/${{ matrix.fiji_executable }} fiji/Fiji.app + # env: + # JSON_OUTS_FNAME: dij_ouputs_file${{ github.run_id }}.json + # MACRO_NAME: dij_macro_ci${{ github.run_id }}.ijm + #- name: Save reports for debugging purposes for drafts + # uses: actions/upload-artifact@v4 + # with: + # name: generated-reports-dij-drafts + # path: generated-reports + - name: Run deepImageJ tests for pusblished versions + run: python scripts/check_compatibility_deepimagej.py all_versions.json generated-reports fiji/Fiji.app/${{ matrix.fiji_executable }} fiji/Fiji.app + env: + JSON_OUTS_FNAME: dij_ouputs_file${{ github.run_id }}.json + MACRO_NAME: dij_macro_ci${{ github.run_id }}.ijm + - name: Save reports for debugging purposes + uses: actions/upload-artifact@v4 + with: + name: generated-reports-dij + path: generated-reports + - name: Upload reports + run: python scripts/upload_reports.py generated-reports + env: + S3_HOST: ${{vars.S3_HOST}} + S3_BUCKET: ${{vars.S3_BUCKET}} + S3_FOLDER: ${{vars.S3_FOLDER}} + S3_ACCESS_KEY_ID: ${{secrets.S3_ACCESS_KEY_ID}} + S3_SECRET_ACCESS_KEY: ${{secrets.S3_SECRET_ACCESS_KEY}} + RUN_URL: ${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 671d4066..aa48971c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ __pycache__/ *.egg-info/ docs/ *.pyc +TEMP/ +bioimageio-gh-pages +scripts/check_compatibility_java_software/test_summaries_null_null +scripts/check_compatibility_java_software/test_summaries_default_default \ No newline at end of file diff --git a/scripts/check_compatibility_deepimagej.py b/scripts/check_compatibility_deepimagej.py new file mode 100644 index 00000000..9e7478eb --- /dev/null +++ b/scripts/check_compatibility_deepimagej.py @@ -0,0 +1,269 @@ +import argparse +from pathlib import Path +from typing import List, Dict, Any +import os +import re +import json + +import urllib.request +import subprocess +from functools import partial +import traceback + +from script_utils import CompatibilityReportDict, check_tool_compatibility, download_rdf + +try: + from ruyaml import YAML +except ImportError: + from ruamel.yaml import YAML + +yaml = YAML(typ="safe") + + +def find_expected_output(outputs_dir, name): + for ff in os.listdir(outputs_dir): + if ff.endswith("_" + name + ".tif") or ff.endswith("_" + name + ".tiff"): + return True + return False + + +def check_dij_macro_generated_outputs(model_dir: str): + with open(os.path.join(model_dir, os.getenv("JSON_OUTS_FNAME")), 'r') as f: + expected_outputs = json.load(f) + + for output in expected_outputs: + name = output["name"] + dij_output = output["dij"] + if not os.path.exists(dij_output): + return False + if not find_expected_output(dij_output, name): + return False + return True + +def remove_processing_and_halo(model_dir: str): + data = None + with open(os.path.join(model_dir, "rdf.yaml")) as stream: + data = yaml.load(stream) + for out in data["outputs"]: + if not isinstance(out["axes"][0], dict): + out.pop('halo', None) + continue + for ax in out["axes"]: + ax.pop('halo', None) + with open(os.path.join(model_dir, "rdf.yaml"), 'w') as outfile: + yaml.dump(data, outfile) + + + +def test_model_deepimagej(rdf_url: str, fiji_executable: str, fiji_path: str): + yaml_file = os.path.abspath("rdf.yaml") + try: + urllib.request.urlretrieve(rdf_url, yaml_file) + except Exception as e: + report = CompatibilityReportDict( + status="failed", + error=f"unable to download the yaml file", + details=e.stderr + os.linesep + e.stdout if isinstance(e, subprocess.CalledProcessError) else traceback.format_exc(), + links=["deepimagej/deepimagej"], + ) + return report + try: + read_yaml = subprocess.run( + [ + fiji_executable, + "--headless", + "--console", + "scripts/deepimagej_jython_scripts/deepimagej_read_yaml.py", + "-yaml_fpath", + yaml_file + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + except BaseException as e: + report = CompatibilityReportDict( + status="failed", + error=f"unable to read the yaml file", + details=e.stderr + os.linesep + e.stdout if isinstance(e, subprocess.CalledProcessError) else traceback.format_exc(), + links=["deepimagej/deepimagej"], + ) + return report + model_dir = None + try: + download_result = subprocess.run( + [ + fiji_executable, + "--headless", + "--console", + "scripts/deepimagej_jython_scripts/deepimagej_download_model.py", + "-yaml_fpath", + yaml_file, + "-models_dir", + os.path.join(fiji_path, 'models') + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + model_dir = download_result.stdout.strip().splitlines()[-1] + except BaseException as e: + report = CompatibilityReportDict( + status="failed", + error=f"unable to download the model", + details=e.stderr + os.linesep + e.stdout if isinstance(e, subprocess.CalledProcessError) else traceback.format_exc(), + links=["deepimagej/deepimagej"], + ) + return report + remove_processing_and_halo(model_dir) + macro_path = os.path.join(model_dir, str(os.getenv("MACRO_NAME"))) + try: + run = subprocess.run( + [ + fiji_executable, + "--headless", + "--console", + "-macro", + macro_path, + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + out_str = run.stdout + if not check_dij_macro_generated_outputs(model_dir): + report = CompatibilityReportDict( + status="failed", + error=f"error running the model", + details=out_str, + links=["deepimagej/deepimagej"], + ) + return report + except BaseException as e: + report = CompatibilityReportDict( + status="failed", + error=f"error running the model", + details=e.stderr + os.linesep + e.stdout if isinstance(e, subprocess.CalledProcessError) else traceback.format_exc(), + links=["deepimagej/deepimagej"], + ) + return report + try: + check_outputs = subprocess.run( + [ + fiji_executable, + "--headless", + "--console", + "scripts/deepimagej_jython_scripts/deepimagej_check_outputs.py", + "-model_dir", + model_dir, + ], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + except BaseException as e: + report = CompatibilityReportDict( + status="failed", + error=f"error comparing expected outputs and actual outputs", + details=e.stderr + os.linesep + e.stdout if isinstance(e, subprocess.CalledProcessError) else traceback.format_exc(), + links=["deepimagej/deepimagej"], + ) + return report + report = CompatibilityReportDict( + status="passed", + error=None, + details=None, + links=["deepimagej/deepimagej"], + ) + return report + + + + + + +def check_compatibility_deepimagej_impl( + rdf_url: str, + sha256: str, + fiji_executable: str = "", + fiji_path: str = "fiji", +) -> CompatibilityReportDict: + """Create a `CompatibilityReport` for a resource description. + + Args: + rdf_url: URL to the rdf.yaml file + sha256: SHA-256 value of **rdf_url** content + """ + assert fiji_executable != "", "please provide the fiji executable path" + + rdf = download_rdf(rdf_url, sha256) + + if rdf["type"] != "model": + report = CompatibilityReportDict( + status="not-applicable", + error=None, + details="only 'model' resources can be used in deepimagej.", + links=["deepimagej/deepimagej"], + ) + + elif len(rdf["inputs"]) > 1 :#or len(rdf["outputs"]) > 1: + report = CompatibilityReportDict( + status="failed", + #error=f"deepimagej only supports single tensor input/output (found {len(rdf['inputs'])}/{len(rdf['outputs'])})", + error=f"deepimagej only supports single tensor input (found {len(rdf['inputs'])})", + details=None, + links=["deepimagej/deepimagej"], + ) + else: + report = test_model_deepimagej(rdf_url, fiji_executable, fiji_path) + + return report + + +def check_compatibility_deepimagej( + deepimagej_version: str, all_version_path: Path, output_folder: Path, fiji_executable: str, fiji_path: str, +): + partial_impl = partial( + check_compatibility_deepimagej_impl, + fiji_executable=fiji_executable, + fiji_path=fiji_path + ) + check_tool_compatibility( + "deepimagej", + deepimagej_version, + all_version_path=all_version_path, + output_folder=output_folder, + check_tool_compatibility_impl=partial_impl, + applicable_types={"model"}, + ) + +def get_dij_version(fiji_path): + plugins_path = os.path.join(fiji_path, "plugins") + pattern = re.compile(r"^deepimagej-(\d+\.\d+\.\d+(?:-snapshot)?)\.jar$") + + matching_files = [ + file.lower() + for file in os.listdir(plugins_path) + if pattern.match(file.lower()) + ] + assert len(matching_files) > 0, "No deepImageJ plugin found, review your installation" + version = pattern.search(matching_files[0]).group(1) + return version + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + _ = parser.add_argument("all_versions", type=Path) + _ = parser.add_argument("output_folder", type=Path) + _ = parser.add_argument("fiji_executable", type=str) + _ = parser.add_argument("fiji_path", type=str) + + args = parser.parse_args() + fiji_path = os.path.abspath(args.fiji_path) + check_compatibility_deepimagej( + get_dij_version(fiji_path), args.all_versions, args.output_folder, fiji_executable=args.fiji_executable, fiji_path=fiji_path + ) diff --git a/scripts/deepimagej_jython_scripts/deepimagej_check_outputs.py b/scripts/deepimagej_jython_scripts/deepimagej_check_outputs.py new file mode 100644 index 00000000..04f61fee --- /dev/null +++ b/scripts/deepimagej_jython_scripts/deepimagej_check_outputs.py @@ -0,0 +1,60 @@ +""" +Python script that checks that the wanted objects have actually been created and correspond to the expected ones +""" + +import json +import os +import argparse + +from net.imglib2.img.display.imagej import ImageJFunctions + +from ij import IJ + + +def find_expected_output(outputs_dir, name): + for ff in os.listdir(outputs_dir): + if ff.endswith("_" + name + ".tif") or ff.endswith("_" + name + ".tiff"): + return os.path.join(outputs_dir, ff) + raise Exception("Expected output for " + name + " not found") + + +def main(model_dir): + with open(os.path.join(model_dir, os.getenv("JSON_OUTS_FNAME")), 'r') as f: + expected_outputs = json.load(f) + + for output in expected_outputs: + name = output["name"] + dij_output_path = output["dij"] + expected_output = output["expected"] + if not os.path.exists(dij_output_path) or len(os.listdir(dij_output_path)) != len(expected_outputs): + raise Exception("Output " + name + " was not generated by deepimagej") + elif not os.path.exists(expected_output): + raise Exception("Cannot find expected output " + name) + dij_output = find_expected_output(dij_output_path, name) + dij_rai = ImageJFunctions.wrap(IJ.openImage(dij_output)) + expected_rai = ImageJFunctions.wrap(IJ.openImage(expected_output)) + dij_shape = dij_rai.dimensionsAsLongArray() + expected_shape = expected_rai.dimensionsAsLongArray() + assert dij_shape == expected_shape, "Output " + name + " in deepimagej has different shape " + str(dij_shape) + " vs " + str(expected_shape) + dij_cursor = dij_rai.cursor() + expected_cursor = expected_rai.cursor() + while dij_cursor.hasNext(): + dij_cursor.fwd() + expected_cursor.fwd() + if abs(dij_cursor.get().getRealFloat() - expected_cursor.get().getRealFloat()) > (1.5 * 1e-4 + 1e-4 * abs(expected_cursor.get().getRealFloat())): + raise Exception("Values of output " + name + " differ") + + +if __name__ == '__main__': + # Create the argument parser + parser = argparse.ArgumentParser() + + # Add the arguments + parser.add_argument('-model_dir', type=str, required=True) + + + # Parse the arguments + args = parser.parse_args() + + model_dir = args.model_dir + main(model_dir) \ No newline at end of file diff --git a/scripts/deepimagej_jython_scripts/deepimagej_download_engines.py b/scripts/deepimagej_jython_scripts/deepimagej_download_engines.py new file mode 100644 index 00000000..a569c6cd --- /dev/null +++ b/scripts/deepimagej_jython_scripts/deepimagej_download_engines.py @@ -0,0 +1,47 @@ +# Copyright (C) 2024 deepImageJ developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ + +""" +Jython script that downloads the basic engines needed to run most of the Deep Learning models +""" + +from io.bioimage.modelrunner.engine.installation import EngineInstall + +import os +import argparse + + +# Create the argument parser +parser = argparse.ArgumentParser() + +# Add the arguments +parser.add_argument('-engines_path', type=str, default="engines", required=False, + help='Path where the engines are going to be installed') + + +# Parse the arguments +args = parser.parse_args() + +engines_path = args.engines_path + + +if not os.path.exists(engines_path) or not os.path.isdir(engines_path): + os.makedirs(engines_path) + +installer = EngineInstall.createInstaller(engines_path) +installer.basicEngineInstallation() + +print(os.path.abspath(engines_path)) +print(os.listdir(os.path.abspath(engines_path))) \ No newline at end of file diff --git a/scripts/deepimagej_jython_scripts/deepimagej_download_model.py b/scripts/deepimagej_jython_scripts/deepimagej_download_model.py new file mode 100644 index 00000000..85d71ca4 --- /dev/null +++ b/scripts/deepimagej_jython_scripts/deepimagej_download_model.py @@ -0,0 +1,110 @@ +# Copyright (C) 2024 deepImageJ developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ + +""" +Jython script that downloads the wanted model(s) from the Bioimage.io repo and +creates a macro to run the model(s) downloaded on the sample input with deepimageJ +""" + +from io.bioimage.modelrunner.bioimageio import BioimageioRepo +from io.bioimage.modelrunner.bioimageio.description import ModelDescriptorFactory +from io.bioimage.modelrunner.numpy import DecodeNumpy + +from deepimagej.tools import ImPlusRaiManager + +from ij import IJ + +import os +import argparse +import json + +MACRO_STR = "run(\"DeepImageJ Run\", \"modelPath={model_path} inputPath={input_path} outputFolder={output_folder} displayOutput=null\")" +CREATED_INPUT_SAMPLE_NAME = "converted_sample_input_0.tif" +CREATED_OUTPUT_SAMPLE_NAME = "converted_sample_ouput_0.tif" + + + +def convert_npy_to_tif(folder_path, test_name, axesOrder, name=CREATED_INPUT_SAMPLE_NAME): + rai = DecodeNumpy.loadNpy(os.path.join(folder_path, test_name)) + imp = ImPlusRaiManager.convert(rai, axesOrder) + out_path = os.path.join(folder_path, name) + IJ.saveAsTiff(imp, out_path) + + + +# Create the argument parser +parser = argparse.ArgumentParser() + +# Add the arguments +parser.add_argument('-yaml_fpath', type=str, required=True, help='Path to the yaml file that contains the rdf.yaml file') +parser.add_argument('-models_dir', type=str, required=True, help='Directory where models are going to be saved') + + +# Parse the arguments +args = parser.parse_args() +rdf_fname = args.yaml_fpath +models_dir = args.models_dir + +descriptor = ModelDescriptorFactory.readFromLocalFile(rdf_fname) + + +if not os.path.exists(models_dir) or not os.path.isdir(models_dir): + os.makedirs(models_dir) + + +br = BioimageioRepo.connect() +mfp = br.downloadModelByID(descriptor.getModelID(), models_dir) + +macro_path = os.path.join(mfp, os.getenv("MACRO_NAME")) + +outputs_json = [] + + +with open(macro_path, "a") as file: + + sample_name = descriptor.getInputTensors().get(0).getSampleTensorName() + if sample_name is None: + test_name = descriptor.getInputTensors().get(0).getTestTensorName() + print(descriptor.getName() + ": " + test_name) + if test_name is None: + raise Exception("There are no test inputs for model: " + descriptor.getModelID()) + convert_npy_to_tif(mfp, test_name, descriptor.getInputTensors().get(0).getAxesOrder()) + sample_name = CREATED_INPUT_SAMPLE_NAME + if " " in mfp: + macro = MACRO_STR.format(model_path="[" + mfp + "]", input_path="[" + os.path.join(mfp, sample_name) + "]", output_folder="[" + os.path.join(mfp, "outputs") + "]") + else: + macro = MACRO_STR.format(model_path=mfp, input_path=os.path.join(mfp, sample_name), output_folder=os.path.join(mfp, "outputs")) + macro = macro.replace(os.sep, os.sep + os.sep) + file.write(macro + os.linesep) + +for n in range(descriptor.getOutputTensors().size()): + test_name = descriptor.getOutputTensors().get(n).getTestTensorName() + if test_name is None: + sample_name = descriptor.getOutputTensors().get(n).getSampleTensorName() + if sample_name is None: + raise Exception("There are no test ouputs for model: " + descriptor.getModelID()) + else: + convert_npy_to_tif(mfp, test_name, descriptor.getOutputTensors().get(n).getAxesOrder(), name=CREATED_OUTPUT_SAMPLE_NAME) + sample_name = CREATED_OUTPUT_SAMPLE_NAME + out_dict = {} + out_dict["name"] = descriptor.getOutputTensors().get(n).getName() + out_dict["dij"] = os.path.join(mfp, "outputs") + out_dict["expected"] = os.path.join(mfp, CREATED_OUTPUT_SAMPLE_NAME) + outputs_json.append(out_dict) + +with open(os.path.join(mfp, os.getenv("JSON_OUTS_FNAME")), 'w') as f: + json.dump(outputs_json, f) + +print(mfp) \ No newline at end of file diff --git a/scripts/deepimagej_jython_scripts/deepimagej_read_yaml.py b/scripts/deepimagej_jython_scripts/deepimagej_read_yaml.py new file mode 100644 index 00000000..024a72b5 --- /dev/null +++ b/scripts/deepimagej_jython_scripts/deepimagej_read_yaml.py @@ -0,0 +1,37 @@ +# Copyright (C) 2024 deepImageJ developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ + +""" +Jython script that checks if a yaml file corresponds to the bioimage.io format and is supported by deepimagej +""" + +from io.bioimage.modelrunner.bioimageio.description import ModelDescriptorFactory + + + +import argparse + +# Create the argument parser +parser = argparse.ArgumentParser() + +# Add the arguments +parser.add_argument('-yaml_fpath', type=str, required=True, help='Path to the yaml file that contains the rdf.yaml file') + + +# Parse the arguments +args = parser.parse_args() +yaml_fpath = args.yaml_fpath + +descriptor = ModelDescriptorFactory.readFromLocalFile(yaml_fpath) \ No newline at end of file diff --git a/scripts/script_utils.py b/scripts/script_utils.py index 738c1c53..8ff2433b 100644 --- a/scripts/script_utils.py +++ b/scripts/script_utils.py @@ -90,7 +90,7 @@ def check_tool_compatibility( report_url = ( "/".join(rdf_url.split("/")[:-2]) - + f"/compatibility/ilastik_{tool_version}.yaml" + + f"/compatibility/{tool_name}_{tool_version}.yaml" ) r = requests.head(report_url) if r.status_code != 404: