├── .github ├── build.sh ├── scripts │ ├── get_chip.py │ ├── install_model.py │ └── run_model.py ├── setup.sh └── workflows │ ├── build.yml │ ├── model_inference_test_fiji_latest.yaml │ └── model_inference_test_fiji_stable.yaml ├── .gitignore ├── LICENSE.txt ├── README.md ├── images ├── NOT-only-return-largest-roi.png ├── allROI-largestROI.png ├── annotation-examples.png ├── complete-workflow-gif.gif ├── embryo-nuclei-labeling.png ├── embryo.png ├── installing-gif.gif ├── manual-prompts-samj.gif ├── only-return-largest-roi.png ├── preset-prompts-samj.gif ├── samj-installing-gif.gif ├── update-site-example.png └── usage-example.png ├── pom.xml └── src └── main ├── java └── ai │ └── nets │ └── samj │ └── ij │ ├── SAMJ_Annotator.java │ ├── ui │ ├── Consumer.java │ ├── IJComboBoxItem.java │ └── commands │ │ ├── AddRoiCommand.java │ │ ├── Command.java │ │ └── DeleteRoiCommand.java │ └── utils │ ├── Constants.java │ └── RoiManagerPrivateViolator.java └── resources ├── .samj_ij_properties └── plugins.config /.github/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-build.sh 3 | sh ci-build.sh 4 | -------------------------------------------------------------------------------- /.github/scripts/get_chip.py: -------------------------------------------------------------------------------- 1 | from io.bioimage.modelrunner.system import PlatformDetection 2 | 3 | 4 | chip = PlatformDetection.getArch() 5 | rosseta = PlatformDetection.isUsingRosseta() 6 | 7 | print("Chip: " + chip) 8 | print("Rosseta: " + str(rosseta)) -------------------------------------------------------------------------------- /.github/scripts/install_model.py: -------------------------------------------------------------------------------- 1 | from ai.nets.samj.communication.model import SAM2Tiny 2 | from java.lang import System 3 | from java.util.function import Consumer 4 | from ai.nets.samj.ij.utils import Constants 5 | from ai.nets.samj.install import Sam2EnvManager 6 | 7 | from io.bioimage.modelrunner.system import PlatformDetection 8 | 9 | from java.io import File 10 | import os 11 | 12 | 13 | class PrintConsumer(Consumer): 14 | def accept(self, s): 15 | System.out.println(s) 16 | 17 | if PlatformDetection.isUsingRosseta() or PlatformDetection.getArch() == PlatformDetection.ARCH_ARM64: 18 | relative_mamba = "appose_" + PlatformDetection.ARCH_ARM64 19 | else: 20 | relative_mamba = "appose_" + PlatformDetection.getArch() 21 | 22 | manager = Sam2EnvManager.create(os.path.join(Constants.FIJI_FOLDER, relative_mamba), "tiny") 23 | model = SAM2Tiny(manager) 24 | model.getInstallationManger().setConsumer(PrintConsumer()); 25 | model.getInstallationManger().installEverything(); 26 | 27 | assert model.isInstalled(), "Model not installed correctly" -------------------------------------------------------------------------------- /.github/scripts/run_model.py: -------------------------------------------------------------------------------- 1 | from ai.nets.samj.communication.model import SAM2Tiny 2 | from ai.nets.samj.ij import SAMJ_Annotator 3 | 4 | from java.util import ArrayList 5 | 6 | from time import time 7 | 8 | from ij import IJ 9 | from jarray import array 10 | from net.imglib2.img.display.imagej import ImageJFunctions 11 | 12 | 13 | blobs = IJ.openImage("https://imagej.net/images/blobs.gif") 14 | wrapImg = ImageJFunctions.convertFloat(blobs) 15 | 16 | model = SAM2Tiny() 17 | 18 | point_prompt = ArrayList() 19 | 20 | 21 | my_seq = (104, 113) 22 | 23 | arr1 = array(my_seq,'i') 24 | 25 | 26 | point_prompt.add(arr1) 27 | 28 | start_time = time() 29 | mask = SAMJ_Annotator.samJReturnMask(model, wrapImg, point_prompt, None) 30 | end_time = time() 31 | 32 | mask_sum = 0 33 | cursor = mask.cursor() 34 | 35 | while cursor.hasNext(): 36 | cursor.next() 37 | mask_sum += cursor.get().getRealDouble() 38 | 39 | print("Total non-zero pixels: " + str(mask_sum)) 40 | print("Total time: " + str(end_time - start_time)) 41 | -------------------------------------------------------------------------------- /.github/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-setup-github-actions.sh 3 | sh ci-setup-github-actions.sh 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - .github/workflows/model_inference_test_fiji_stable.yaml 9 | - .github/workflows/model_inference_test_fiji_latest.yaml 10 | tags: 11 | - "*-[0-9]+.*" 12 | pull_request: 13 | branches: 14 | - main 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Java 23 | uses: actions/setup-java@v3 24 | with: 25 | java-version: '8' 26 | distribution: 'zulu' 27 | cache: 'maven' 28 | - name: Set up CI environment 29 | run: .github/setup.sh 30 | - name: Execute the build 31 | run: .github/build.sh 32 | env: 33 | GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} 34 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 35 | MAVEN_USER: ${{ secrets.MAVEN_USER }} 36 | MAVEN_PASS: ${{ secrets.MAVEN_PASS }} 37 | OSSRH_PASS: ${{ secrets.OSSRH_PASS }} 38 | SIGNING_ASC: ${{ secrets.SIGNING_ASC }} 39 | -------------------------------------------------------------------------------- /.github/workflows/model_inference_test_fiji_latest.yaml: -------------------------------------------------------------------------------- 1 | name: Model inference test Fiji latest 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | run_tests: 7 | description: "Trigger the model‐inference‐test workflow by hand" 8 | required: false 9 | default: "true" 10 | 11 | jobs: 12 | integration-tests: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - name: ubuntu 18 | os: ubuntu-latest 19 | url_file_name: fiji-latest-linux64-jdk.zip 20 | fiji_executable: fiji-linux-x64 21 | - name: windows 22 | os: windows-latest 23 | url_file_name: fiji-latest-win64-jdk.zip 24 | fiji_executable: fiji-windows-x64.exe 25 | - name: macos-arm64-14 26 | os: macos-14 27 | url_file_name: fiji-latest-macos-arm64-jdk.zip 28 | fiji_executable: Contents/MacOS/fiji-macos-arm64 29 | - name: macos-arm64-15 30 | os: macos-15 31 | url_file_name: fiji-latest-macos-arm64-jdk.zip 32 | fiji_executable: Contents/MacOS/fiji-macos-arm64 33 | - name: macos-x86_64 34 | os: macos-13 35 | url_file_name: fiji-latest-macos64-jdk.zip 36 | fiji_executable: Contents/MacOS/fiji-macos-x64 37 | runs-on: ${{ matrix.os }} 38 | 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v4 42 | 43 | - name: Set up JDK 21 44 | uses: actions/setup-java@v4 45 | with: 46 | java-version: '21' 47 | distribution: 'zulu' 48 | 49 | - name: Set up Fiji 50 | shell: bash 51 | run: | 52 | mkdir -p fiji 53 | curl -L -o fiji.zip https://downloads.imagej.net/fiji/latest/${{ matrix.url_file_name }} 54 | unzip fiji.zip -d fiji 55 | 56 | - name: Install SAMJ 57 | shell: bash 58 | run: | 59 | fiji/Fiji/${{ matrix.fiji_executable }} --headless --update add-update-site "SAMJ" "https://sites.imagej.net/SAMJ/" 60 | fiji/Fiji/${{ matrix.fiji_executable }} --headless --update update 61 | 62 | - name: rm SAMJ-IJ file 63 | shell: bash 64 | run: | 65 | rm -f /fiji/Fiji/plugins/SAMJ-IJ-*.jar 66 | 67 | - name: Build Plugin 68 | shell: bash 69 | run: mvn clean package 70 | 71 | - name: Get plugin name and version 72 | shell: bash 73 | run: | 74 | MVN_VERSION=$(mvn -q \ 75 | -Dexec.executable=echo \ 76 | -Dexec.args='${project.version}' \ 77 | --non-recursive \ 78 | exec:exec) 79 | MVN_NAME=$(mvn -q \ 80 | -Dexec.executable=echo \ 81 | -Dexec.args='${project.artifactId}' \ 82 | --non-recursive \ 83 | exec:exec) 84 | echo "version=${MVN_VERSION}" >> $GITHUB_OUTPUT 85 | echo "name=${MVN_NAME}" >> $GITHUB_OUTPUT 86 | id: mvn_info 87 | 88 | - name: Copy Plugin to Fiji 89 | shell: bash 90 | run: cp target/${{steps.mvn_info.outputs.name}}-${{steps.mvn_info.outputs.version}}.jar fiji/Fiji/plugins/ 91 | 92 | - name: Print information about the OS 93 | shell: bash 94 | run: | 95 | python -c "import platform; print(platform.platform())" 96 | 97 | - name: Install SAM model 98 | shell: bash 99 | run: | 100 | fiji/Fiji/${{ matrix.fiji_executable }} --headless --console .github/scripts/install_model.py 101 | 102 | - name: Get chip 103 | shell: bash 104 | run: | 105 | fiji/Fiji/${{ matrix.fiji_executable }} --headless --console .github/scripts/get_chip.py 106 | 107 | - name: Check if mps is available (only macos) 108 | if: runner.os == 'macOS' 109 | shell: bash 110 | run: | 111 | python -c "import os; print(os.listdir());" 112 | fiji/Fiji/appose_*/envs/sam2/bin/python -c "import torch; print(torch.backends.mps.is_built()); print(torch.backends.mps.is_available());" 113 | 114 | - name: Run Macro for annotation 115 | shell: bash 116 | run: | 117 | fiji/Fiji/${{ matrix.fiji_executable }} --headless --console .github/scripts/run_model.py -------------------------------------------------------------------------------- /.github/workflows/model_inference_test_fiji_stable.yaml: -------------------------------------------------------------------------------- 1 | name: Model inference test Fiji stable 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - .github/workflows/model_inference_test_fiji_latest.yaml 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | integration-tests: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - name: ubuntu 18 | os: ubuntu-latest 19 | url_file_name: fiji-stable-linux64-jdk.zip 20 | fiji_executable: ImageJ-linux64 21 | - name: windows 22 | os: windows-latest 23 | url_file_name: fiji-stable-win64-jdk.zip 24 | fiji_executable: ImageJ-win64.exe 25 | - name: macos-arm64-14 26 | os: macos-14 27 | url_file_name: fiji-stable-macosx-jdk.zip 28 | fiji_executable: Contents/MacOS/ImageJ-macosx 29 | - name: macos-arm64-15 30 | os: macos-15 31 | url_file_name: fiji-stable-macosx-jdk.zip 32 | fiji_executable: Contents/MacOS/ImageJ-macosx 33 | - name: macos-x86_64 34 | os: macos-13 35 | url_file_name: fiji-stable-macosx-jdk.zip 36 | fiji_executable: Contents/MacOS/ImageJ-macosx 37 | runs-on: ${{ matrix.os }} 38 | 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v4 42 | 43 | - name: Set up JDK 8 44 | uses: actions/setup-java@v4 45 | with: 46 | java-version: '8' 47 | distribution: 'zulu' 48 | 49 | - name: Set up Fiji 50 | shell: bash 51 | run: | 52 | mkdir -p fiji 53 | curl -L -o fiji.zip https://downloads.imagej.net/fiji/stable/${{ matrix.url_file_name }} 54 | unzip fiji.zip -d fiji 55 | 56 | - name: Install SAMJ 57 | shell: bash 58 | run: | 59 | fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --update add-update-site "SAMJ" "https://sites.imagej.net/SAMJ/" 60 | fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --update update 61 | 62 | - name: rm SAMJ-IJ file 63 | shell: bash 64 | run: | 65 | rm -f /fiji/Fiji.app/plugins/SAMJ-IJ-*.jar 66 | 67 | - name: Build Plugin 68 | shell: bash 69 | run: mvn clean package 70 | 71 | - name: Get plugin name and version 72 | shell: bash 73 | run: | 74 | MVN_VERSION=$(mvn -q \ 75 | -Dexec.executable=echo \ 76 | -Dexec.args='${project.version}' \ 77 | --non-recursive \ 78 | exec:exec) 79 | MVN_NAME=$(mvn -q \ 80 | -Dexec.executable=echo \ 81 | -Dexec.args='${project.artifactId}' \ 82 | --non-recursive \ 83 | exec:exec) 84 | echo "version=${MVN_VERSION}" >> $GITHUB_OUTPUT 85 | echo "name=${MVN_NAME}" >> $GITHUB_OUTPUT 86 | id: mvn_info 87 | 88 | - name: Copy Plugin to Fiji 89 | shell: bash 90 | run: cp target/${{steps.mvn_info.outputs.name}}-${{steps.mvn_info.outputs.version}}.jar fiji/Fiji.app/plugins/ 91 | 92 | - name: Print information about the OS 93 | shell: bash 94 | run: | 95 | python -c "import platform; print(platform.platform())" 96 | 97 | - name: Install SAM model 98 | shell: bash 99 | run: | 100 | fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --console .github/scripts/install_model.py 101 | 102 | - name: Get chip 103 | shell: bash 104 | run: | 105 | fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --console .github/scripts/get_chip.py 106 | 107 | - name: Check if mps is available (only macos) 108 | if: runner.os == 'macOS' 109 | shell: bash 110 | run: | 111 | python -c "import os; print(os.listdir());" 112 | fiji/Fiji.app/appose_*/envs/sam2/bin/python -c "import torch; print(torch.backends.mps.is_built()); print(torch.backends.mps.is_available());" 113 | 114 | - name: Run Macro for annotation 115 | shell: bash 116 | run: | 117 | fiji/Fiji.app/${{ matrix.fiji_executable }} --headless --console .github/scripts/run_model.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # maven specific files 2 | /target/ 3 | .idea 4 | 5 | # eclipse specific files 6 | /.settings/ 7 | .classpath 8 | .project 9 | 10 | # icy specific files 11 | /plugins/ 12 | /workspace/ 13 | /miniconda3/ 14 | setting.xml 15 | 16 | # models directory 17 | /models/ 18 | /models2/ 19 | /downloads/ 20 | /engines/ 21 | /deepicy.jar 22 | /bin/ 23 | /.DS_Store 24 | /miniconda3-x86_64/ 25 | /appose/ 26 | /appose_x86_64/ 27 | /_appose_x86_64/ 28 | /appose_arm64/ 29 | 30 | # VIM editor 31 | *.sw* 32 | 33 | prep.sh 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/segment-anything-models-java/SAMJ-IJ/actions/workflows/build.yml/badge.svg)](https://github.com/segment-anything-models-java/SAMJ-IJ/actions/workflows/build.yml) 2 | 3 | # SAMJ-IJ 4 | 5 | The SAMJ-IJ is a powerful Fiji plugin for annotating microscopy images using various versions of the [Segment Anything](https://github.com/facebookresearch/segment-anything) Model (SAM). This README provides detailed instructions on how to use the plugin for image annotation. In this first version of the plugin, the SAMJ-IJ Annotator is delivered to annotate images through the usage of prompts. The plugin is designed to be user-friendly and efficient, allowing for easy and accurate image annotation for further analysis. 6 | 7 | > [!NOTE] 8 | > This is an **EARLY RELEASE**, many more improvements are coming! Your valuable suggestions for enhancements are encouraged in the [Issues section](https://github.com/segment-anything-models-java/SAMJ-IJ/issues) or on the [image.sc forum](https://forum.image.sc/). 9 | 10 | ## Contents 11 | - [Installation](#installation) 12 | - [Model Installation](#model-installation) 13 | - [Annotating Images](#annotating-images) 14 | - [Saving Annotations](#saving-annotations) 15 | - [Use Cases](#use-cases) 16 | - [Contributors](#contributors) 17 | - [Notes](#notes) 18 | 19 | ## Fiji and Plugin Installation 20 | 21 | Before you can annotate images using SAMJ-IJ, you need to install the plugin in Fiji: 22 | 23 | 1. **Install Fiji**: If you haven't already, download and install [Fiji](https://fiji.sc/). 24 | 25 | > [!IMPORTANT] 26 | > For MacOS users, if your Fiji instance is launched from the Downloads folder, SAMJ will not work! Move Fiji to another folder, Documents or Desktop, for example. 27 | 28 | 2. **Install SAMJ Plugin**: Open Fiji and navigate to `Help > Update...`. In the `Manage update sites` window, and look for an update site named `SAMJ`, select it, click on `Apply and close` and then `Apply changes`. Finally restart Fiji. 29 | 30 | If you cannot find `SAMJ` among the update sites list, click on `Add update site`/`Add unlisted site`, write `SAMJ` in the `Name` field and `https://sites.imagej.net/SAMJ/` in the `URL` field. Click on `Apply and close`, click on `Apply changes` and restart Fiji. 31 | ![SAMJ Update site](./images/update-site-example.png) 32 | 5. **Open SAMJ-IJ Annotator**: Start Fiji and navigate to `Plugins > SAMJ > SAMJ Annotator` to open the plugin. 33 | 34 | ## Model Installation 35 | 36 | The different models available to install can change over time as new models are added or removed. Up to this date, the models available for installation are SAM2 Tiny, SAM2 Small, SAM2 Large, EfficientSAM, and EfficientViTSAM-l2. All these models do not need the use of GPU but the CPU of your workstation can impact its performance. 37 | 38 | Model references and github repositories can be found directly on the SAMJ plugin. 39 | 40 | > [!WARNING] 41 | > Users with a low-end computer are advised not to use the **EfficientSAM** model as it might take up to 10 minutes to load the first time, or the computer can even be frozen. The fastest and lightest model is **EfficientViTSAM-L2**, but low-resource machines might take up to 2-3 minutes to load the first time. Subsequent loading times will be much faster (~10s). 42 | 43 | These are the steps to install a model: 44 | 1. Open the SAMJ Annotator plugin as described above. 45 | 2. Choose a SAM model from the list provided within the plugin. 46 | 3. Click on the `Install` button next to the selected model. 47 | 4. Wait for the installation process to complete. This may take some time, depending on the model size, your computer, and your internet connection. 48 | 49 | > [!CAUTION] 50 | > Model installation times vary based on your machine's specifications, ranging from seconds to up to 20 minutes. Please be patient. 51 | 52 | 53 | This video demonstrates the live installation of EfficientViTSAM-l1 on a Mac M1. 54 | ![Installing Model](./images/samj-installing-gif.gif) 55 | 56 | 57 | 58 | ## Annotating Images 59 | 60 | Once you have one model installed, you can start to annotate your images. Firstly, you will need to open you image in Fiji and then encode it by clicking the "Go!" button in the SAMJ Annotator plugin. This will encode your image so you can start annotating it. 61 | 62 | There are two different ways to annotate your images: using manual prompts (*Manual*) or with prompts that are already given by a previous routine (*Preset prompts*), for example, in Fiji. 63 | 64 | ### Manual Prompts 65 | Once the image is encoded, to annotate, choose either rectangles or points to draw ROIs or mark points on the image. The annotations will be sent to the ROI Manager, where you can manage them. 66 | When you are done annotating, you can export the resulting mask. 67 | 68 | Check this video to see how to annotate with manual prompts. 69 | ![Annotating images with Manual Prompts](./images/manual-prompts-samj.gif) 70 | 71 | 72 | 73 | ### Preset Prompts 74 | Again, after your image is encoded, you will be able to use preset prompts for previous routines or workflows. SAMJ only needs points in the image or ROIs in the ROI Manager to annotate the image. These ROIs or points can be created by any other plugin or routine in Fiji. 75 | 76 | As an example, in this video you can see some points created through "Find Maxima" in Fiji and then used to annotate the image with SAMJ. 77 | ![Annotating images with Preset Prompts](./images/preset-prompts-samj.gif) 78 | 79 | ## Saving Annotations 80 | 81 | ### Return all ROIs vs. Only return largest ROI 82 | When doing your annotations, depending on the nature of your images and the final goal of your annotations, you can choose to return all ROIs of the image or only the largest ROI. This can be done by checking or unchecking the `Only return largest ROI` checkbox in the ROI Manager. 83 | 84 | In this table you can see the difference between these two options over the same image. 85 | 86 | Only return largest ROI activated | Only return largest ROI NOT activated 87 | :-------------------------:|:-------------------------: 88 | ![](./images/only-return-largest-roi.png) | ![](./images/NOT-only-return-largest-roi.png) 89 | 90 | ### Export to Labelling 91 | This button simplifies the process of exporting your annotations, which are saved as semantic annotations where each marked region is assigned a distinct value. For enhanced visual clarity, we suggest altering the Look-Up Table (LUT) in Fiji when necessary(Image > Lookup Tables > Glasbey or choose another option). 92 | 93 | 94 | ## Macros 95 | 96 | 97 | ## Use Cases 98 | This Fiji plugin is intended to work with microscopy images. To show its versatility among different images, here are some use cases. 99 | 100 | ![Use Cases of different annotations in microscpy images](images/annotation-examples.png) 101 | 102 | #### a) Astrocytes stained for actin 103 | The original image (top left in the figure) displays astrocytes stained for actin following mechanical deformation as part of a study exploring the mechanical and functional responses of astrocytes using magneto-active substrates [1]. The annotated image (bottom left in the figure) highlights individual astrocytes for detailed analysis. 104 | This annotation was accomplished using the "Points Prompt" feature coupled with the "Return Only Largest ROI" option to annotate each astrocyte visible in the image selectively. The primary goal of this annotation is to facilitate a comparative study of astrocyte morphology pre- and post-deformation, thus contributing valuable insights into the biomechanical properties and adaptive responses of astrocytes under stress. 105 | 106 | #### b) Bacterial mobility on agar plates 107 | The images (top center and bottom center in the figure) showcase the results of mobility assays for *Pseudomonas aeruginosa* strains on agar plates [2]. These assays are crucial for studying the surface motility of bacteria, which is considered a key factor in pathogenicity due to its role in chemotaxis, biofilm formation, and overall virulence. The original images depict the spread of bacteria on agar plates following incubation, captured using the Chemi DOC™ image system. The annotations made using the SAMJ plugin allow for precise measurement and analysis of the spread area, significantly automating a task that was previously manual, tedious, and time-consuming. By leveraging SAMJ for these annotations, researchers can efficiently quantify bacterial motility, facilitating deeper insights into bacterial behavior and its implications on disease spreading and antimicrobial resistance. This enhances the plugin's value in microbial research, providing a robust tool for assessing bacterial dynamics in a consistent and reproducible manner. 108 | 109 | #### c) Organoids 110 | The images (top right and bottom right in the figure) illustrate organoids captured for the purpose of segmentation, counting, and analysis of morphological features such as area and eccentricity [3]. These organoids are typically used to model biological processes in vitro, providing a robust platform for studies in developmental biology, disease pathology, and drug screening. The original images capture the diverse shapes and sizes of organoids, which can be challenging to quantify manually. Using the SAMJ plugin, researchers can automate the segmentation and counting of organoids, and accurately measure their area and eccentricity. This annotation capability not only enhances the precision and efficiency of the analysis but also supports high-throughput screening and detailed morphometric assessments. The ability of SAMJ to handle such complex image data demonstrates its utility in advanced biological research and experimental reproducibility. 111 | 112 | 113 | 114 | #### References 115 | [1] Gomez‐Cruz, C., Fernandez‐de la Torre, M., Lachowski, D., Prados‐de‐Haro, M., del Río Hernández, A. E., Perea, G., ... & Garcia‐Gonzalez, D. (2024). Mechanical and Functional Responses in Astrocytes under Alternating Deformation Modes Using Magneto‐Active Substrates. Advanced Materials, 2312497. 116 | 117 | [2] Casado-Garcia, A., Chichón, G., Dominguez, C., Garcia-Dominguez, M., Heras, J., Ines, A., ... & Saenz, Y. (2021). MotilityJ: An open-source tool for the classification and segmentation of bacteria on motility images. Computers in biology and medicine, 136, 104673. 118 | 119 | [3] Segmentation, counting, and measurement of area and eccentricity (circularity) of organoids in [image.sc forum](https://forum.image.sc/t/segmentation-counting-measurement-of-area-and-eccentricity-circularity-of-organoids/90751) 120 | 121 | ## Contributors 122 | 123 | **Carlos García-López-de-Haro**, *Bioimage Analysis Unit, Institut Pasteur, Université Paris Cité, Paris, France* - [@carlosuc3m](https://github.com/carlosuc3m) 124 | **Caterina Fuster-Barceló**, *Bioengineering Department, Universidad Carlos III de Madrid, Leganés, Spain* - [@cfusterbarcelo](https://github.com/cfusterbarcelo) 125 | **Curtis T. Rueden**, *Center for Quantitative Cell Imaging, University of Wisconsin, Madison, USA* - [@ctrueden](https://github.com/ctrueden) 126 | **Jónathan Heras**, *Department of Mathematics and Computer Science, University of La Rioja, Logroño, Spain* - [@joheras](https://github.com/joheras) 127 | **Vladimir Ulman**, *IT4Innovations, VSB - Technical University of Ostrava, Ostrava, Czech Republic* - [@xulman](https://github.com/xulman) 128 | **Adrián Inés**, *Department of Mathematics and Computer Science, University of La Rioja, Logroño, Spain* - [@adines](https://github.com/adines) 129 | **Kevin Eliceri**, *Center for Quantitative Cell Imaging, University of Wisconsin, Madison, USA* - [@eliceiri](https://github.com/eliceiri) 130 | **J.C. Olivo-Marin**, *CNRS UMR 3691, Institut Pasteur, Paris, France* 131 | **Daniel Sage**, *Biomedical Imaging Group and Center for Imaging, École Polytechnique Fédérale de Lausanne (EPFL), Lausanne, Switzerland* - [@dasv74](https://github.com/dasv74) 132 | **Arrate Muñoz-Barrutia**, *Bioengineering Department, Universidad Carlos III de Madrid, Leganés, Spain* - [@arratemunoz](https://github.com/arratemunoz) 133 | 134 | 135 | ## Notes 136 | 137 | - This plugin is intended to use with microscopy images. 138 | - The documentation here is for users only. Developer documentation, including contribution guidelines, will be available in a separate repository. 139 | - For further assistance or to report issues, please visit the [plugin's repository](https://github.com/segment-anything-models-java/SAMJ-IJ). 140 | 141 | Thank you for using the SAMJ-IJ Fiji plugin! 142 | -------------------------------------------------------------------------------- /images/NOT-only-return-largest-roi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/NOT-only-return-largest-roi.png -------------------------------------------------------------------------------- /images/allROI-largestROI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/allROI-largestROI.png -------------------------------------------------------------------------------- /images/annotation-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/annotation-examples.png -------------------------------------------------------------------------------- /images/complete-workflow-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/complete-workflow-gif.gif -------------------------------------------------------------------------------- /images/embryo-nuclei-labeling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/embryo-nuclei-labeling.png -------------------------------------------------------------------------------- /images/embryo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/embryo.png -------------------------------------------------------------------------------- /images/installing-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/installing-gif.gif -------------------------------------------------------------------------------- /images/manual-prompts-samj.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/manual-prompts-samj.gif -------------------------------------------------------------------------------- /images/only-return-largest-roi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/only-return-largest-roi.png -------------------------------------------------------------------------------- /images/preset-prompts-samj.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/preset-prompts-samj.gif -------------------------------------------------------------------------------- /images/samj-installing-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/samj-installing-gif.gif -------------------------------------------------------------------------------- /images/update-site-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/update-site-example.png -------------------------------------------------------------------------------- /images/usage-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segment-anything-models-java/SAMJ-IJ/a9515e6cb9afc02310cfc2280080309df5470fb1/images/usage-example.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.scijava 7 | pom-scijava 8 | 40.0.0 9 | 10 | 11 | 12 | ai.nets 13 | samj-IJ 14 | 0.0.3-SNAPSHOT 15 | 16 | SAM network inside Fiji 17 | A Fiji plugin for interactive segmentation using the SAM network 18 | https://github.com/segment-anything-models-java/SAMJ-IJ 19 | 2024 20 | 21 | SAMJ 22 | https://github.com/segment-anything-models-java 23 | 24 | 25 | 26 | Apache Software License, Version 2.0 27 | https://www.apache.org/licenses/LICENSE-2.0.txt 28 | repo 29 | 30 | 31 | 32 | 33 | 34 | carlosuc3m 35 | Carlos Garcia 36 | 37 | developer 38 | 39 | 40 | 41 | 42 | 43 | Curtis Rueden 44 | https://imagej.net/people/ctrueden 45 | ctrueden 46 | 47 | 48 | Vladimir Ulman 49 | https://github.com/xulman 50 | xulman 51 | 52 | 53 | Daniel Sage 54 | https://github.com/dasv74 55 | dasv74 56 | 57 | 58 | Caterina Fuster-Barcelo 59 | https://github.com/cfusterbarcelo 60 | cfusterbarcelo 61 | 62 | 63 | Arrate Muñoz-Barrutia 64 | https://github.com/arrmunoz 65 | arrmunoz 66 | 67 | 68 | Jonathan Heras 69 | https://github.com/joheras 70 | joheras 71 | 72 | 73 | Adrian Ines 74 | https://github.com/adines 75 | adines 76 | 77 | 78 | 79 | 80 | 81 | Image.sc Forum 82 | https://forum.image.sc/tag/samj 83 | 84 | 85 | 86 | 87 | scm:git:https://github.com/segment-anything-models-java/SAMJ-IJ 88 | scm:git:git@github.com:segment-anything-models-java/SAMJ-IJ 89 | HEAD 90 | https://github.com/segment-anything-models-java/SAMJ-IJ 91 | 92 | 93 | GitHub Issues 94 | https://github.com/segment-anything-models-java/SAMJ-IJ/issues 95 | 96 | 97 | GitHub Actions 98 | https://github.com/segment-anything-models-java/SAMJ-IJ/actions 99 | 100 | 101 | 102 | ai.nets.samj 103 | apache_v2 104 | SAMJ developers. 105 | Plugin to help image annotation with SAM-based Deep Learning models 106 | 107 | 108 | sign,deploy-to-scijava 109 | 0.0.3-SNAPSHOT 110 | 111 | 112 | 113 | 114 | scijava.public 115 | https://maven.scijava.org/content/groups/public 116 | 117 | 118 | 119 | 120 | 121 | net.imagej 122 | ij 123 | 124 | 125 | ai.nets 126 | samj 127 | ${samj.version} 128 | 129 | 130 | net.imglib2 131 | imglib2-ij 132 | 133 | 134 | 135 | 136 | 137 | src/main/resources 138 | false 139 | 140 | 141 | src/main/resources 142 | true 143 | 144 | .samj_ij_properties 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/SAMJ_Annotator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Plugin to help image annotation with SAM-based Deep Learning models 4 | * %% 5 | * Copyright (C) 2024 SAMJ developers. 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package ai.nets.samj.ij; 21 | 22 | import java.awt.GraphicsEnvironment; 23 | import java.awt.Rectangle; 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | import javax.swing.SwingUtilities; 30 | 31 | import ai.nets.samj.annotation.Mask; 32 | import ai.nets.samj.communication.model.SAM2Tiny; 33 | import ai.nets.samj.communication.model.SAMModel; 34 | import ai.nets.samj.gui.MainGUI; 35 | import ai.nets.samj.ui.SAMJLogger; 36 | import ij.IJ; 37 | import ij.ImageJ; 38 | import ij.Macro; 39 | import ij.gui.GUI; 40 | import ij.plugin.PlugIn; 41 | import ij.plugin.frame.Recorder; 42 | import io.bioimage.modelrunner.system.PlatformDetection; 43 | import net.imglib2.RandomAccessibleInterval; 44 | import net.imglib2.type.NativeType; 45 | import net.imglib2.type.numeric.RealType; 46 | import net.imglib2.type.numeric.integer.UnsignedShortType; 47 | import ai.nets.samj.ij.ui.Consumer; 48 | import ai.nets.samj.ij.utils.Constants; 49 | import ai.nets.samj.install.SamEnvManagerAbstract; 50 | import ai.nets.samj.models.AbstractSamJ.BatchCallback; 51 | 52 | // TODO I (Carlos) don't know how to develop in IJ2 @Plugin(type = Command.class, menuPath = "Plugins>SAMJ>Annotator") 53 | //TODO I (Carlos) don't know how to develop in IJ2 public class Plugin_SamJAnnotator implements Command { 54 | 55 | /** 56 | * ImageJ plugin that implements the SAMJ default GUI that can help annotating images 57 | * using SAM-based Deep Learning models 58 | * @author Carlos Garcia 59 | * @author Vladimir Ulman 60 | */ 61 | public class SAMJ_Annotator implements PlugIn { 62 | 63 | private String macroModel; 64 | 65 | private String macroMaskPrompt; 66 | 67 | private String macroExport; 68 | 69 | final static long MAX_IMAGE_SIZE_IN_BYTES = ((long)4)<<30; //4 GB 70 | 71 | private final static String MACRO_INFO = "https://github.com/segment-anything-models-java/SAMJ-IJ/blob/main/README.md#macros"; 72 | 73 | final static String MACRO_RECORD_COMMENT = "" 74 | + System.lineSeparator() 75 | + "// Note: SAMJ macros are supported only in BatchSAMize mode with preset prompts." + System.lineSeparator() 76 | + "// The macro recording feature will capture the command 'run(\"SAMJ Annotator\");', but executing it will have no effect." + System.lineSeparator() 77 | + "// To record something, please click the 'SAMJ BatchSAMize' button." + System.lineSeparator() 78 | + "// For more information, visit:" + System.lineSeparator() 79 | + "// " + MACRO_INFO + System.lineSeparator() 80 | + System.lineSeparator(); 81 | /** 82 | * Optional keys to run SAMJ run with a macro or in headless mode 83 | */ 84 | private final static String[] macroOptionalKeys = new String[] {"model", "maskPrompt", "export"}; 85 | 86 | private static Consumer MACRO_CONSUMER; 87 | 88 | private final static BatchCallback MACRO_CALLBACK = new BatchCallback() { 89 | @Override 90 | public void setTotalNumberOfRois(int nRois) {} 91 | @Override 92 | public void updateProgress(int n) {} 93 | 94 | @Override 95 | public void drawRoi(List masks) { 96 | SwingUtilities.invokeLater(() -> MACRO_CONSUMER.addPolygonsFromGUI(masks)); 97 | 98 | } 99 | 100 | @Override 101 | public void deletePointPrompt(List promptList) { 102 | SwingUtilities.invokeLater(() -> promptList.forEach(proi -> MACRO_CONSUMER.deletePointRoi(proi))); 103 | } 104 | 105 | @Override 106 | public void deleteRectPrompt(List promptList) { 107 | SwingUtilities.invokeLater(() -> promptList.stream() 108 | .map(rect -> new Rectangle(rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1])) 109 | .forEach(roi -> MACRO_CONSUMER.deleteRectRoi(roi))); 110 | } 111 | 112 | }; 113 | 114 | static 115 | { 116 | // IMPORTANT: Set the DEFAULT DIR WHERE MODELS ARE DOWNLOADED TO THE WANTED DIR. 117 | // IF NOT IN MACS IT WILL DEFAULT TO "/" AND RAISE AN ERROR 118 | SamEnvManagerAbstract.DEFAULT_DIR = Constants.FIJI_FOLDER + File.separator + "appose_" 119 | + ((!PlatformDetection.isMacOS() || !PlatformDetection.isUsingRosseta()) ? PlatformDetection.getArch() 120 | : PlatformDetection.ARCH_ARM64 ); 121 | } 122 | 123 | // TODO I (Carlos) don't know how to develop in IJ2 @Parameter 124 | //private LogService logService = new LogService(); 125 | 126 | /** 127 | * Run the plugin 128 | * @throws InterruptedException if there is any thread interruption error 129 | * @throws IOException if there is any file error 130 | */ 131 | public void run() throws IOException, InterruptedException { 132 | if (Recorder.record) 133 | Recorder.recordString(MACRO_RECORD_COMMENT); 134 | 135 | // TODO I (Carlos) don't know how to develop in IJ2 final Logger log = logService.subLogger("SAMJ"); 136 | try { 137 | 138 | // TODO I (Carlos) don't know how to develop in IJ2 Logger guiSublogger = log.subLogger("PromptsResults window"); 139 | SAMJLogger guilogger = new SAMJLogger() { 140 | @Override 141 | public void info(String text) {System.out.println(text);} 142 | @Override 143 | public void warn(String text) {System.out.println(text);} 144 | @Override 145 | public void error(String text) {System.out.println(text);} 146 | }; 147 | 148 | // TODO I (Carlos) don't know how to develop in IJ2 Logger networkSublogger = log.subLogger("Networks window"); 149 | SAMJLogger networkLogger = new SAMJLogger() { 150 | @Override 151 | public void info(String text) {System.out.println("network -- " + text);} 152 | @Override 153 | public void warn(String text) {System.out.println("network -- " + text);} 154 | @Override 155 | public void error(String text) {System.out.println("network -- " + text);} 156 | }; 157 | 158 | SwingUtilities.invokeLater(() -> { 159 | MainGUI samjDialog = new MainGUI(new Consumer()); 160 | GUI.center(samjDialog); 161 | }); 162 | } catch (RuntimeException e) { 163 | e.printStackTrace(); 164 | } 165 | } 166 | 167 | /** 168 | * method for tesitng during development 169 | * @param args 170 | * nothing 171 | * @throws InterruptedException if there is any thread related error 172 | * @throws IOException if there is any file related error 173 | */ 174 | public static void main(String[] args) throws IOException, InterruptedException { 175 | ImageJ ij = new ImageJ(); 176 | new SAMJ_Annotator().run(); 177 | } 178 | 179 | 180 | @Override 181 | public void run(String arg) { 182 | boolean isMacro = IJ.isMacro(); 183 | boolean isHeadless = GraphicsEnvironment.isHeadless(); 184 | try { 185 | if (isMacro) { 186 | runMacro(); 187 | } else if (isHeadless) { 188 | } else { 189 | run(); 190 | } 191 | } catch (IOException | InterruptedException e) { 192 | e.printStackTrace(); 193 | } 194 | } 195 | 196 | /** 197 | * Processes an image using SAM2 Tiny to generate segmentation masks based on provided prompts. 198 | * This method is suitable for scripting within ImageJ/Fiji environments. 199 | * If you want to use another SAM variant use {@link #samJReturnContours(SAMModel, RandomAccessibleInterval, List, List)} 200 | * 201 | *

The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval} 202 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating 203 | * regions or points of interest. 204 | * 205 | * @param 206 | * the ImgLib2 data type of the input image 207 | * @param rai 208 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C) 209 | * @param pointPrompts 210 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos} 211 | * @param rectPrompts 212 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height} 213 | * @return 214 | * a {@link RandomAccessibleInterval} of type {@link UnsignedShortType} containing segmentation masks corresponding to each provided prompt 215 | * @throws IOException 216 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly 217 | * @throws RuntimeException 218 | * if an error occurs during the segmentation process 219 | * @throws InterruptedException 220 | * if the segmentation process is unexpectedly interrupted 221 | */ 222 | public static < T extends RealType< T > & NativeType< T > > 223 | RandomAccessibleInterval samJReturnMask(RandomAccessibleInterval rai, 224 | List pointPrompts, 225 | List rectPrompts) throws IOException, RuntimeException, InterruptedException { 226 | return samJReturnMask(new SAM2Tiny(), rai, pointPrompts, rectPrompts); 227 | } 228 | 229 | /** 230 | * Processes an image using a Segment Anything Model (SAM) variant to generate segmentation masks based on provided prompts. 231 | * This method is suitable for scripting within ImageJ/Fiji environments. 232 | * 233 | *

The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval} 234 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating 235 | * regions or points of interest. 236 | * 237 | * @param 238 | * the ImgLib2 data type of the input image 239 | * @param model 240 | * the SAM model instance used for segmentation (e.g., SAM2Tiny, SAM2Small, SAM2Large, EfficientSAM). 241 | * Example instantiation: 242 | *

{@code
243 | 	 *   import ai.nets.samj.communication.model.SAM2Tiny;
244 | 	 *   SAMModel model = new SAM2Tiny();
245 | 	 *   }
246 | * @param rai 247 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C) 248 | * @param pointPrompts 249 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos} 250 | * @param rectPrompts 251 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height} 252 | * @return 253 | * a {@link RandomAccessibleInterval} of type {@link UnsignedShortType} containing segmentation masks corresponding to each provided prompt 254 | * @throws IOException 255 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly 256 | * @throws RuntimeException 257 | * if an error occurs during the segmentation process 258 | * @throws InterruptedException 259 | * if the segmentation process is unexpectedly interrupted 260 | */ 261 | public static < T extends RealType< T > & NativeType< T > > 262 | RandomAccessibleInterval samJReturnMask(SAMModel model, RandomAccessibleInterval rai, 263 | List pointPrompts, 264 | List rectPrompts) throws IOException, RuntimeException, InterruptedException { 265 | List masks = samJReturnContours(model, rai, pointPrompts, rectPrompts); 266 | return Mask.getMask(rai.dimensionsAsLongArray()[0], rai.dimensionsAsLongArray()[1], masks); 267 | } 268 | 269 | /** 270 | * Processes an image using a SAM2 Tiny to generate segmentation contours based on provided prompts. 271 | * This method is suitable for scripting within ImageJ/Fiji environments. 272 | * If you want to use another SAM variant use {@link #samJReturnContours(SAMModel, RandomAccessibleInterval, List, List)} 273 | * 274 | *

The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval} 275 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating 276 | * regions or points of interest. 277 | * 278 | * @param 279 | * the ImgLib2 data type of the input image 280 | * @param rai 281 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C) 282 | * @param pointPrompts 283 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos} 284 | * @param rectPrompts 285 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height} 286 | * @return 287 | * a List of {@link Mask} that contain the polygons that define the contour of each of the segmented objects. 288 | * @throws IOException 289 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly 290 | * @throws RuntimeException 291 | * if an error occurs during the segmentation process 292 | * @throws InterruptedException 293 | * if the segmentation process is unexpectedly interrupted 294 | */ 295 | public static < T extends RealType< T > & NativeType< T > > 296 | List samJReturnContours(RandomAccessibleInterval rai, List pointPrompts, List rectPrompts) throws IOException, RuntimeException, InterruptedException { 297 | return samJReturnContours(new SAM2Tiny(), rai, pointPrompts, rectPrompts); 298 | } 299 | 300 | /** 301 | * Processes an image using a Segment Anything Model (SAM) variant to generate segmentation contours based on provided prompts. 302 | * This method is suitable for scripting within ImageJ/Fiji environments. 303 | * 304 | *

The user must specify a SAM model variant, supply an input image as a {@link RandomAccessibleInterval} 305 | * with dimensions structured as (X, Y, C), and provide point and/or rectangular prompts indicating 306 | * regions or points of interest. 307 | * 308 | * @param 309 | * the ImgLib2 data type of the input image 310 | * @param model 311 | * the SAM model instance used for segmentation (e.g., SAM2Tiny, SAM2Small, SAM2Large, EfficientSAM). 312 | * Example instantiation: 313 | *

{@code
314 | 	 *   import ai.nets.samj.communication.model.SAM2Tiny;
315 | 	 *   SAMModel model = new SAM2Tiny();
316 | 	 *   }
317 | * @param rai 318 | * the input image as a {@link RandomAccessibleInterval} with dimensions ordered as (X, Y, C) 319 | * @param pointPrompts 320 | * a list of point-based prompts, where each prompt is defined by an integer array {x_pos, y_pos} 321 | * @param rectPrompts 322 | * a list of rectangular prompts, where each rectangle is defined by an integer array {x_pos, y_pos, width, height} 323 | * @return 324 | * a List of {@link Mask} that contain the polygons that define the contour of each of the segmented objects. 325 | * @throws IOException 326 | * if an error occurs while loading the SAM model environment or if the model has not been installed correctly 327 | * @throws RuntimeException 328 | * if an error occurs during the segmentation process 329 | * @throws InterruptedException 330 | * if the segmentation process is unexpectedly interrupted 331 | */ 332 | public static < T extends RealType< T > & NativeType< T > > 333 | List samJReturnContours(SAMModel model, RandomAccessibleInterval rai, List pointPrompts, List rectPrompts) throws IOException, RuntimeException, InterruptedException { 334 | if ((pointPrompts == null || pointPrompts.size() == 0) && (rectPrompts == null || rectPrompts.size() == 0)) 335 | throw new IllegalArgumentException("Please provide at least one point prompt or rectangular prompt."); 336 | if (MACRO_CONSUMER == null) 337 | MACRO_CONSUMER = new Consumer(); 338 | SAMModel selected = MainGUI.DEFAULT_MODEL_LIST.stream() 339 | .filter(mm -> mm.getName().equals(model.getName())).findFirst().orElse(null); 340 | if (selected == null) 341 | throw new IllegalArgumentException("Specified model does not exist. Please, for more info visit: " 342 | + MACRO_INFO); 343 | selected.setImage(rai, null); 344 | selected.setReturnOnlyBiggest(true); 345 | RandomAccessibleInterval maskRai = null; 346 | List callbackedContours = new ArrayList(); 347 | BatchCallback callback = new BatchCallback() { 348 | @Override 349 | public void setTotalNumberOfRois(int nRois) {} 350 | @Override 351 | public void updateProgress(int n) {} 352 | @Override 353 | public void drawRoi(List masks) { 354 | callbackedContours.addAll(masks); 355 | } 356 | @Override 357 | public void deletePointPrompt(List promptList) {} 358 | @Override 359 | public void deleteRectPrompt(List promptList) {} 360 | 361 | }; 362 | List contours = selected.processBatchOfPrompts(pointPrompts, rectPrompts, maskRai, callback); 363 | callbackedContours.addAll(contours); 364 | 365 | 366 | selected.closeProcess(); 367 | return callbackedContours; 368 | } 369 | 370 | private void runMacro() throws IOException, RuntimeException, InterruptedException { 371 | if (Macro.getOptions() == null) 372 | return; 373 | parseCommand(); 374 | 375 | if (macroModel == null && macroExport.equals("true")) { 376 | macroExport(); 377 | } else if (macroModel != null && macroExport.equals("true")) { 378 | macroRunSAMJ(); 379 | macroExport(); 380 | } else if (macroModel != null ) { 381 | macroRunSAMJ(); 382 | } 383 | } 384 | 385 | private void macroExport() { 386 | if (MACRO_CONSUMER == null) 387 | throw new IllegalArgumentException("In order to be able to export annotations to mask, " 388 | + "some annotations with the SAMJ Macro should have been done first."); 389 | MACRO_CONSUMER.exportImageLabeling(); 390 | } 391 | 392 | private < T extends RealType< T > & NativeType< T > > 393 | void macroRunSAMJ() throws IOException, RuntimeException, InterruptedException { 394 | if (MACRO_CONSUMER == null) 395 | MACRO_CONSUMER = new Consumer(); 396 | MACRO_CONSUMER.setFocusedImage(MACRO_CONSUMER.getFocusedImage()); 397 | SAMModel selected = MainGUI.DEFAULT_MODEL_LIST.stream() 398 | .filter(mm -> mm.getName().equals(macroModel)).findFirst().orElse(null); 399 | if (selected == null) 400 | throw new IllegalArgumentException("Specified model does not exist. Please, for more info visit: " 401 | + MACRO_INFO); 402 | MACRO_CONSUMER.setModel(selected); 403 | RandomAccessibleInterval rai = MACRO_CONSUMER.getFocusedImageAsRai(); 404 | selected.setImage(rai, null); 405 | selected.setReturnOnlyBiggest(true); 406 | List pointPrompts = MACRO_CONSUMER.getPointRoisOnFocusImage(); 407 | List rectPrompts = MACRO_CONSUMER.getRectRoisOnFocusImage(); 408 | RandomAccessibleInterval maskRai = null; 409 | if (macroMaskPrompt != null) 410 | maskRai = null; 411 | selected.processBatchOfPrompts(pointPrompts, rectPrompts, maskRai, MACRO_CALLBACK); 412 | pointPrompts.stream().forEach(pp -> MACRO_CONSUMER.deletePointRoi(pp)); 413 | rectPrompts.stream().forEach(pp -> MACRO_CONSUMER.deleteRectRoi(pp)); 414 | 415 | selected.closeProcess(); 416 | } 417 | 418 | private void parseCommand() { 419 | String macroArg = Macro.getOptions(); 420 | 421 | macroModel = parseArg(macroArg, macroOptionalKeys[0], false); 422 | macroMaskPrompt = parseArg(macroArg, macroOptionalKeys[1], false); 423 | macroExport = parseArg(macroArg, macroOptionalKeys[2], false); 424 | if (macroModel == null && macroExport == null) 425 | throw new IllegalArgumentException("SAMJ macro requires the parameter 'model' to be" 426 | + " specified to make annotations or the parameter 'export' if the " 427 | + "user wants to export already annotated masks. More info at: " + MACRO_INFO); 428 | if (macroExport == null) 429 | macroExport = "false"; 430 | macroExport = macroExport.toLowerCase().equals("false") ? "false" : macroExport; 431 | macroExport = macroExport.toLowerCase().equals("true") ? "true" : macroExport; 432 | if (!macroExport.equals("false") && !macroExport.equals("true")) 433 | throw new IllegalArgumentException("The SAMJ macro argument 'export' can only be true or false." 434 | + " For more info: " + MACRO_INFO); 435 | } 436 | 437 | private static String parseArg(String macroArg, String arg, boolean required) { 438 | String value = Macro.getValue(macroArg, arg, null); 439 | if (value != null && value.equals("")) 440 | value = null; 441 | if (value == null && required) 442 | throw new IllegalArgumentException("SAMJ macro requires to the variable '" + arg + "'. " 443 | + "For more info, please visit: " + MACRO_INFO); 444 | return value; 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/ui/Consumer.java: -------------------------------------------------------------------------------- 1 | package ai.nets.samj.ij.ui; 2 | 3 | import java.awt.Color; 4 | import java.awt.Component; 5 | import java.awt.Polygon; 6 | import java.awt.Rectangle; 7 | import java.awt.event.KeyEvent; 8 | import java.awt.event.KeyListener; 9 | import java.awt.event.MouseEvent; 10 | import java.awt.event.MouseListener; 11 | import java.awt.event.WindowEvent; 12 | import java.awt.event.WindowListener; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | import java.util.Enumeration; 17 | import java.util.Iterator; 18 | import java.util.List; 19 | import java.util.Stack; 20 | import java.util.stream.Collectors; 21 | import java.util.stream.IntStream; 22 | 23 | import javax.swing.DefaultListModel; 24 | import javax.swing.JList; 25 | import javax.swing.JScrollPane; 26 | import javax.swing.JViewport; 27 | import javax.swing.event.ListDataEvent; 28 | import javax.swing.event.ListDataListener; 29 | 30 | import ai.nets.samj.annotation.Mask; 31 | import ai.nets.samj.gui.components.ComboBoxItem; 32 | import ai.nets.samj.ij.ui.commands.AddRoiCommand; 33 | import ai.nets.samj.ij.ui.commands.Command; 34 | import ai.nets.samj.ij.ui.commands.DeleteRoiCommand; 35 | import ai.nets.samj.ij.utils.RoiManagerPrivateViolator; 36 | import ai.nets.samj.models.AbstractSamJ; 37 | import ai.nets.samj.ui.ConsumerInterface; 38 | import ij.IJ; 39 | import ij.IJEventListener; 40 | import ij.ImageListener; 41 | import ij.ImagePlus; 42 | import ij.Prefs; 43 | import ij.WindowManager; 44 | import ij.gui.ImageCanvas; 45 | import ij.gui.ImageWindow; 46 | import ij.gui.Overlay; 47 | import ij.gui.PointRoi; 48 | import ij.gui.PolygonRoi; 49 | import ij.gui.Roi; 50 | import ij.gui.Toolbar; 51 | import ij.plugin.CompositeConverter; 52 | import ij.plugin.OverlayLabels; 53 | import ij.plugin.frame.Recorder; 54 | import ij.plugin.frame.RoiManager; 55 | import io.bioimage.modelrunner.system.PlatformDetection; 56 | import net.imglib2.FinalInterval; 57 | import net.imglib2.Interval; 58 | import net.imglib2.Localizable; 59 | import net.imglib2.Point; 60 | import net.imglib2.RandomAccessibleInterval; 61 | import net.imglib2.img.Img; 62 | import net.imglib2.img.display.imagej.ImageJFunctions; 63 | import net.imglib2.type.NativeType; 64 | import net.imglib2.type.numeric.RealType; 65 | import net.imglib2.type.numeric.integer.UnsignedShortType; 66 | 67 | /** 68 | * 69 | * @author Carlos Garcia Lopez de Haro 70 | */ 71 | public class Consumer extends ConsumerInterface implements MouseListener, KeyListener, WindowListener, IJEventListener, ListDataListener { 72 | /** 73 | * The image being processed 74 | */ 75 | private ImagePlus activeImage; 76 | /** 77 | * Canvas of the image selected. Used to record the prompts drawn by the user 78 | */ 79 | private ImageCanvas activeCanvas; 80 | /** 81 | * Window of the selected image. Used to record the prompts drawn by the user 82 | */ 83 | private ImageWindow activeWindow; 84 | /** 85 | * Instance of the ROI manager to save the ROIs created 86 | */ 87 | private RoiManager roiManager; 88 | /** 89 | * Instance of the list displayed in the ROI manager 90 | */ 91 | private DefaultListModel listModel; 92 | /** 93 | * Whether to add the ROIs created to the ROI manager or not 94 | */ 95 | private boolean isAddingToRoiManager = true; 96 | /** 97 | * Counter of the ROIs created 98 | */ 99 | private int promptsCreatedCnt = 0; 100 | /** 101 | * A list to save several ROIs that are being created for the same prompt. 102 | * Whenever the prompt is sent to the model, this list is emptied 103 | */ 104 | private List temporalROIs = new ArrayList(); 105 | /** 106 | * A list to save several ROIs that are being created from the same prompt. 107 | * This list saves only the "negative" ROIs, those that are not part of the instance of interest, 108 | * but part of the background. 109 | * Whenever the prompt is sent to the model, this list is emptied. 110 | */ 111 | private List temporalNegROIs = new ArrayList(); 112 | /** 113 | * For the point prompts, whether if hte user is collecting several prompts (pressing the ctrl key) 114 | * or just one 115 | */ 116 | private boolean isCollectingPoints = false; 117 | /** 118 | * All the points being collected that reference the instance of interest 119 | */ 120 | private List collectedPoints = new ArrayList(); 121 | /** 122 | * All the points being collected that reference the background (ctrl + alt) 123 | */ 124 | private List collecteNegPoints = new ArrayList(); 125 | /** 126 | * List of the annotated masks on an image 127 | */ 128 | private Stack annotatedMask = new Stack(); 129 | /** 130 | * List that keeps track of the annotated masks 131 | */ 132 | private Stack redoAnnotatedMask = new Stack(); 133 | /** 134 | * Tracks if Ctrl+Z has already been handled 135 | */ 136 | private boolean undoPressed = false; 137 | /** 138 | * Tracks if Ctrl+Y has already been handled 139 | */ 140 | private boolean redoPressed = false; 141 | /** 142 | * Whether the SAMJ specific listeners are registered or not. 143 | */ 144 | private boolean registered = false; 145 | /** 146 | * Whether the delete operation comes from ctrl+Z or ctrl+Y or from the roi manager 147 | */ 148 | private boolean isCommand = false; 149 | 150 | public Consumer() { 151 | IJ.addEventListener(this); 152 | ImagePlus.addImageListener(new ImageListener() { 153 | 154 | @Override 155 | public void imageOpened(ImagePlus imp) {} 156 | @Override 157 | public void imageUpdated(ImagePlus imp) {} 158 | 159 | @Override 160 | public void imageClosed(ImagePlus imp) { 161 | if (guiCallback == null) 162 | return; 163 | if (imp != Consumer.this.activeImage) 164 | return; 165 | Consumer.this.deactivateListeners(); 166 | Consumer.this.guiCallback.run(); 167 | } 168 | 169 | }); 170 | } 171 | 172 | @Override 173 | /** 174 | * {@inheritDoc} 175 | * 176 | * GEt the list of open images in ImageJ 177 | */ 178 | public List getListOfOpenImages() { 179 | return Arrays.stream(WindowManager.getImageTitles()) 180 | .map(title -> new IJComboBoxItem((Object) WindowManager.getImage(title))) 181 | .collect(Collectors.toList()); 182 | } 183 | 184 | @Override 185 | public void addPolygonsFromGUI(List masks) { 186 | // TODO improve the naming 187 | this.addToRoiManager(masks, "batch"); 188 | } 189 | 190 | @Override 191 | public List getPolygonsFromRoiManager() { 192 | return Arrays.stream(roiManager.getRoisAsArray()).map(i -> i.getPolygon()).collect(Collectors.toList()); 193 | } 194 | 195 | @Override 196 | public void enableAddingToRoiManager(boolean shouldBeAdding) { 197 | this.isAddingToRoiManager = shouldBeAdding; 198 | } 199 | 200 | @Override 201 | public void exportImageLabeling() { 202 | if (Recorder.record) 203 | Recorder.recordString("run(\"SAMJ Annotator\", \"export=true\");" + System.lineSeparator()); 204 | int width = activeImage.getWidth(); 205 | int height = activeImage.getHeight(); 206 | List masks = new ArrayList(); 207 | List doNotInclude = new ArrayList(); 208 | for (int i = this.annotatedMask.size() - 1; i >= 0; i --) { 209 | Command maskList = annotatedMask.get(i); 210 | if (maskList instanceof DeleteRoiCommand) { 211 | for (Mask mm: maskList.getMasks()) 212 | doNotInclude.add(mm.getName()); 213 | } else if (maskList instanceof AddRoiCommand) { 214 | for (Mask mm : maskList.getMasks()) { 215 | if (doNotInclude.contains(mm.getName())) 216 | continue; 217 | masks.add(mm); 218 | } 219 | } 220 | } 221 | RandomAccessibleInterval raiMask = Mask.getMask(width, height, masks); 222 | ImagePlus impMask = ImageJFunctions.show(raiMask); 223 | impMask.setTitle(activeImage.getTitle() + "-labeling"); 224 | impMask.getProcessor().setMinAndMax(0, annotatedMask.size()); 225 | } 226 | 227 | @Override 228 | public void activateListeners() { 229 | if (registered) return; 230 | activeCanvas.addMouseListener(this); 231 | activeCanvas.addKeyListener(this); 232 | activeWindow.addWindowListener(this); 233 | activeWindow.addKeyListener(this); 234 | 235 | activeCanvas.removeKeyListener(IJ.getInstance()); 236 | activeWindow.removeKeyListener(IJ.getInstance()); 237 | registered = true; 238 | } 239 | 240 | @Override 241 | public void deactivateListeners() { 242 | if (!registered) return; 243 | activeCanvas.removeMouseListener(this); 244 | activeCanvas.removeKeyListener(this); 245 | activeWindow.removeWindowListener(this); 246 | activeWindow.removeKeyListener(this); 247 | 248 | activeWindow.addKeyListener(IJ.getInstance()); 249 | activeCanvas.addKeyListener(IJ.getInstance()); 250 | registered = false; 251 | } 252 | 253 | @Override 254 | public boolean isValidPromptSelected() { 255 | return Toolbar.getToolName().equals("rectangle") 256 | || Toolbar.getToolName().equals("point") 257 | || Toolbar.getToolName().equals("multipoint"); 258 | } 259 | 260 | @Override 261 | public void setFocusedImage(Object image) { 262 | boolean changed = activeImage != (ImagePlus) image; 263 | if (!changed) { 264 | WindowManager.setCurrentWindow(activeWindow); 265 | return; 266 | } 267 | activeImage = (ImagePlus) image; 268 | this.activeCanvas = this.activeImage.getCanvas(); 269 | this.activeWindow = this.activeImage.getWindow(); 270 | if (this.isAddingToRoiManager) 271 | this.roiManager = startRoiManager(); 272 | } 273 | 274 | @Override 275 | public void deselectImage() { 276 | activeImage = null; 277 | this.activeCanvas = null; 278 | this.activeWindow = null; 279 | } 280 | 281 | @Override 282 | public Object getFocusedImage() { 283 | return WindowManager.getCurrentImage(); 284 | } 285 | 286 | @Override 287 | public String getFocusedImageName() { 288 | return WindowManager.getCurrentImage().getTitle(); 289 | } 290 | 291 | @Override 292 | public & NativeType> RandomAccessibleInterval getFocusedImageAsRai() { 293 | ImagePlus imp = WindowManager.getCurrentImage(); 294 | boolean isColorRGB = imp.getType() == ImagePlus.COLOR_RGB; 295 | Img image = ImageJFunctions.wrap(isColorRGB ? CompositeConverter.makeComposite(imp) : imp); 296 | return image; 297 | } 298 | 299 | @Override 300 | public List getRectRoisOnFocusImage() { 301 | Roi roi = WindowManager.getCurrentImage().getRoi(); 302 | List list = getRectRoisFromRoiManager(); 303 | if (roi == null) 304 | return list; 305 | if (roi.getType() != Roi.RECTANGLE) 306 | return list; 307 | if (list.stream().anyMatch(a -> a.equals(roi.getBounds()))) 308 | return list; 309 | list.add(roi.getBounds()); 310 | return list; 311 | } 312 | 313 | @Override 314 | public List getPointRoisOnFocusImage() { 315 | Roi roi = WindowManager.getCurrentImage().getRoi(); 316 | List list = getPointRoisFromRoiManager(); 317 | if (roi == null) 318 | return list; 319 | if (roi.getType() != Roi.POINT) 320 | return list; 321 | Iterator it = roi.iterator(); 322 | while (it.hasNext()) { 323 | java.awt.Point p = it.next(); 324 | int[] arr = new int[] {(int) p.getX(), (int) p.getY()}; 325 | if (list.stream().anyMatch(a -> Arrays.equals(a, arr))) 326 | continue; 327 | list.add(arr); 328 | } 329 | return list; 330 | } 331 | 332 | @Override 333 | public void deletePointRoi(int[] pp) { 334 | Roi[] roiManagerRois = RoiManager.getInstance().getRoisAsArray(); 335 | int ii = -1; 336 | if (roiManagerRois != null) { 337 | ii ++; 338 | for (Roi managerRoi : roiManagerRois) { 339 | if (managerRoi.getType() != Roi.POINT) 340 | continue; 341 | PointRoi pRoi = (PointRoi) managerRoi; 342 | Iterator iter = pRoi.iterator(); 343 | while (iter.hasNext()) { 344 | java.awt.Point point = iter.next(); 345 | if (point.x == pp[0] && point.y == pp[1]) { 346 | pRoi.deleteHandle(pp[0], pp[1]); 347 | if (!iter.hasNext()) { 348 | try { 349 | RoiManagerPrivateViolator.deleteRoiAtPosition(RoiManager.getInstance(), ii); 350 | } catch (NoSuchFieldException | SecurityException | NoSuchMethodException 351 | | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 352 | e.printStackTrace(); 353 | } 354 | } 355 | return; 356 | } 357 | } 358 | } 359 | } 360 | Roi roi = WindowManager.getCurrentImage().getRoi(); 361 | if (roi != null && roi.getType() == Roi.POINT) { 362 | PointRoi pRoi = (PointRoi) roi; 363 | Iterator iter = roi.iterator(); 364 | while (iter.hasNext()) { 365 | java.awt.Point point = iter.next(); 366 | if (point.x == pp[0] && point.y == pp[1]) { 367 | pRoi.deleteHandle(pp[0], pp[1]); 368 | return; 369 | } 370 | } 371 | } 372 | } 373 | 374 | @Override 375 | public void deleteRectRoi(Rectangle rect) { 376 | Roi[] roiManagerRois = RoiManager.getInstance().getRoisAsArray(); 377 | if (roiManagerRois != null) { 378 | for (int i = 0; i < roiManagerRois.length; i ++) { 379 | Roi managerRoi = roiManagerRois[i]; 380 | if (managerRoi.getType() != Roi.RECTANGLE) 381 | continue; 382 | if (managerRoi.getBounds().equals(rect)) { 383 | try { 384 | RoiManagerPrivateViolator.deleteRoiAtPosition(RoiManager.getInstance(), i); 385 | } catch (NoSuchFieldException | SecurityException | NoSuchMethodException | IllegalAccessException 386 | | IllegalArgumentException | InvocationTargetException e) { 387 | e.printStackTrace(); 388 | } 389 | return; 390 | } 391 | } 392 | } 393 | Roi roi = WindowManager.getCurrentImage().getRoi(); 394 | if (roi != null && roi.getType() == Roi.RECTANGLE) { 395 | if (roi.getBounds().equals(rect)) { 396 | activeImage.deleteRoi(); 397 | return; 398 | } 399 | } 400 | } 401 | 402 | private List getRectRoisFromRoiManager() { 403 | List list = new ArrayList(); 404 | Roi[] rois = RoiManager.getInstance().getRoisAsArray(); 405 | if (rois.length == 0) 406 | return list; 407 | list = Arrays.stream(rois).filter(rr -> rr.getType() == Roi.RECTANGLE) 408 | .map(rr -> { 409 | rr.setImage(activeImage); 410 | return rr.getBounds(); 411 | }).collect(Collectors.toList()); 412 | return list; 413 | } 414 | 415 | private List getPointRoisFromRoiManager() { 416 | List list = new ArrayList(); 417 | Roi[] rois = RoiManager.getInstance().getRoisAsArray(); 418 | if (rois.length == 0) 419 | return list; 420 | List roiList = Arrays.stream(rois).filter(rr -> rr.getType() == Roi.POINT).collect(Collectors.toList()); 421 | for (Roi rr : roiList) { 422 | Iterator it = rr.iterator(); 423 | while (it.hasNext()) { 424 | java.awt.Point p = it.next(); 425 | list.add(new int[] {(int) p.getX(), (int) p.getY()}); 426 | } 427 | rr.setImage(activeImage); 428 | } 429 | return list; 430 | } 431 | 432 | @Override 433 | /** 434 | * when the plugin is closed, close everything 435 | */ 436 | public void windowClosed(WindowEvent e) { 437 | roiManager.close(); 438 | this.selectedModel.closeProcess(); 439 | this.selectedModel = null; 440 | this.deactivateListeners(); 441 | this.activeImage = null; 442 | this.activeCanvas = null; 443 | this.activeWindow = null; 444 | } 445 | 446 | 447 | @Override 448 | public void keyPressed(KeyEvent e) { 449 | if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_Z && this.annotatedMask.size() != 0 && !redoPressed) { 450 | redoPressed = true; 451 | isCommand = true; 452 | Command command = annotatedMask.pop(); 453 | command.undo(); 454 | this.redoAnnotatedMask.push(command); 455 | } else if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_Y && this.redoAnnotatedMask.size() != 0 && !undoPressed) { 456 | undoPressed = true; 457 | isCommand = true; 458 | Command command = redoAnnotatedMask.pop(); 459 | command.execute(); 460 | this.annotatedMask.push(command); 461 | } 462 | e.consume(); 463 | } 464 | 465 | @Override 466 | /** 467 | * Monitor when the control key is being released for the point prompts. 468 | * Whenever it is released and the point prompt is selected, the points that have already been drawn 469 | * are sent to SAMJ 470 | */ 471 | public void keyReleased(KeyEvent e) { 472 | if ((e.getKeyCode() == KeyEvent.VK_CONTROL && !PlatformDetection.isMacOS()) 473 | || (e.getKeyCode() == KeyEvent.VK_META && PlatformDetection.isMacOS())) { 474 | submitAndClearPoints(); 475 | } 476 | if (e.getKeyCode() == KeyEvent.VK_Z) { 477 | redoPressed = false; 478 | } 479 | if (e.getKeyCode() == KeyEvent.VK_Y) { 480 | undoPressed = false; 481 | } 482 | } 483 | 484 | @Override 485 | public void mouseReleased(MouseEvent e) { 486 | if (activeImage.getRoi() == null) 487 | return; 488 | if (Toolbar.getToolName().equals("rectangle")) { 489 | annotateRect(); 490 | } else if (Toolbar.getToolName().equals("point") || Toolbar.getToolName().equals("multipoint")) { 491 | annotatePoints(e); 492 | } else if (Toolbar.getToolName().equals("freeline")) { 493 | annotateBrush(e); 494 | } else { 495 | return; 496 | } 497 | if (!isCollectingPoints) activeImage.deleteRoi(); 498 | } 499 | 500 | private void annotateRect() { 501 | final Roi roi = activeImage.getRoi(); 502 | final Rectangle rectBounds = roi.getBounds(); 503 | final Interval rectInterval = new FinalInterval( 504 | new long[] { rectBounds.x, rectBounds.y }, 505 | new long[] { rectBounds.x+rectBounds.width-1, rectBounds.y+rectBounds.height-1 } ); 506 | submitRectPrompt(rectInterval); 507 | } 508 | 509 | private void submitRectPrompt(Interval rectInterval) { 510 | try { 511 | addToRoiManager(this.selectedModel.fetch2dSegmentation(rectInterval), "rect"); 512 | } catch (Exception ex) { 513 | ex.printStackTrace();; 514 | } 515 | } 516 | 517 | private void annotatePoints(MouseEvent e) { 518 | final Roi roi = activeImage.getRoi(); 519 | // TODO think what to do with negative points 520 | if (e.isControlDown() && e.isAltDown() && false) { 521 | roi.setFillColor(Color.red); 522 | //add point to the list only 523 | isCollectingPoints = true; 524 | Iterator iterator = roi.iterator(); 525 | java.awt.Point p = iterator.next(); 526 | while (iterator.hasNext()) p = iterator.next(); 527 | collecteNegPoints.add( new Point(p.x,p.y) ); //NB: add ImgLib2 Point 528 | //TODO log.info("Image window: collecting points..., already we have: "+collectedPoints.size()); 529 | } else if ((e.isControlDown() && !PlatformDetection.isMacOS()) || (e.isMetaDown() && PlatformDetection.isMacOS())) { 530 | //add point to the list only 531 | isCollectingPoints = true; 532 | Iterator iterator = roi.iterator(); 533 | java.awt.Point p = iterator.next(); 534 | while (iterator.hasNext()) p = iterator.next(); 535 | collectedPoints.add( new Point(p.x,p.y) ); //NB: add ImgLib2 Point 536 | //TODO log.info("Image window: collecting points..., already we have: "+collectedPoints.size()); 537 | } else { 538 | isCollectingPoints = false; 539 | //collect this last one 540 | Iterator iterator = roi.iterator(); 541 | java.awt.Point p = iterator.next(); 542 | while (iterator.hasNext()) p = iterator.next(); 543 | collectedPoints.add( new Point(p.x,p.y) ); 544 | submitAndClearPoints(); 545 | } 546 | } 547 | 548 | /** 549 | * Send the point prompts to SAM and clear the lists collecting them 550 | */ 551 | private void submitAndClearPoints() { 552 | if (this.selectedModel == null) return; 553 | if (collectedPoints.size() == 0) return; 554 | 555 | //TODO log.info("Image window: Processing now points, this count: "+collectedPoints.size()); 556 | isCollectingPoints = false; 557 | activeImage.deleteRoi(); 558 | Rectangle zoomedRectangle = this.activeCanvas.getSrcRect(); 559 | try { 560 | if (activeImage.getWidth() * activeImage.getHeight() > Math.pow(AbstractSamJ.MAX_ENCODED_AREA_RS, 2) 561 | || activeImage.getWidth() > AbstractSamJ.MAX_ENCODED_SIDE || activeImage.getHeight() > AbstractSamJ.MAX_ENCODED_SIDE) 562 | addToRoiManager(selectedModel.fetch2dSegmentation(collectedPoints, collecteNegPoints, zoomedRectangle), 563 | (collectedPoints.size() > 1 ? "points" : "point")); 564 | else 565 | addToRoiManager(selectedModel.fetch2dSegmentation(collectedPoints, collecteNegPoints), 566 | (collectedPoints.size() > 1 ? "points" : "point")); 567 | } catch (Exception ex) { 568 | ex.printStackTrace(); 569 | } 570 | collectedPoints = new ArrayList(); 571 | collecteNegPoints = new ArrayList(); 572 | temporalROIs = new ArrayList(); 573 | temporalNegROIs = new ArrayList(); 574 | } 575 | 576 | private void annotateBrush(MouseEvent e) { 577 | final Roi roi = activeImage.getRoi(); 578 | // TODO this is not a real mask prompt, it is just taking 579 | // TODO all the points in a line and using them, modify it for a true mask 580 | if (e.isControlDown() && e.isAltDown()) { 581 | temporalNegROIs.add(roi); 582 | roi.setStrokeColor(Color.red); 583 | isCollectingPoints = true; 584 | Iterator it = roi.iterator(); 585 | while (it.hasNext()) { 586 | java.awt.Point p = it.next(); 587 | collecteNegPoints.add(new Point(p.x,p.y)); 588 | } 589 | addTemporalRois(); 590 | } else if (e.isControlDown()) { 591 | temporalROIs.add(roi); 592 | isCollectingPoints = true; 593 | Iterator it = roi.iterator(); 594 | while (it.hasNext()) { 595 | java.awt.Point p = it.next(); 596 | collectedPoints.add(new Point(p.x,p.y)); 597 | } 598 | addTemporalRois(); 599 | } else { 600 | isCollectingPoints = false; 601 | Rectangle rect = roi.getBounds(); 602 | if (rect.height == 1) { 603 | for (int i = 0; i < rect.width; i ++) { 604 | collectedPoints.add(new Point(rect.x + i, rect.y)); 605 | } 606 | } else if (rect.width == 1) { 607 | for (int i = 0; i < rect.height; i ++) { 608 | collectedPoints.add(new Point(rect.x, rect.y + i)); 609 | } 610 | } else { 611 | Iterator it = roi.iterator(); 612 | while (it.hasNext()) { 613 | java.awt.Point p = it.next(); 614 | collectedPoints.add(new Point(p.x,p.y)); 615 | } 616 | } 617 | // TODO move this logic to SAMJ into the masks option 618 | if (collectedPoints.size() > 1 && collectedPoints.size() < 6) 619 | collectedPoints = Arrays.asList(new Localizable[] {collectedPoints.get(1)}); 620 | else if (collectedPoints.size() > 1 && collectedPoints.size() < 50) { 621 | List newCollectedPoints = new ArrayList(); 622 | while (newCollectedPoints.size() == 0) { 623 | for (Localizable pp : collectedPoints) { 624 | if (Math.random() < 0.2) newCollectedPoints.add(pp); 625 | } 626 | } 627 | collectedPoints = newCollectedPoints; 628 | } else if (collectedPoints.size() > 50) { 629 | List newCollectedPoints = new ArrayList(); 630 | while (newCollectedPoints.size() < 10) { 631 | for (Localizable pp : collectedPoints) { 632 | if (Math.random() < Math.min(0.1, 50.0 / collectedPoints.size())) newCollectedPoints.add(pp); 633 | } 634 | } 635 | collectedPoints = newCollectedPoints; 636 | } 637 | submitAndClearPoints(); 638 | } 639 | } 640 | 641 | private RoiManager startRoiManager() { 642 | RoiManager roiManager = RoiManager.getInstance(); 643 | if (roiManager == null) { 644 | roiManager = new RoiManager(); 645 | for (Component comp :roiManager.getComponents()) { 646 | if (comp instanceof JScrollPane) { 647 | for (Component comp2 : ((JScrollPane) comp).getComponents()) { 648 | if (comp2 instanceof JViewport) { 649 | for (Component comp3 : ((JViewport) comp2).getComponents()) { 650 | if (comp3 instanceof JList) { 651 | listModel = (DefaultListModel) ((JList) comp3).getModel(); 652 | listModel.addListDataListener(this); 653 | break; 654 | } 655 | } 656 | } 657 | } 658 | } 659 | } 660 | } 661 | // TODO what to do? roiManager.reset(); 662 | roiManager.setVisible(true); 663 | roiManager.setTitle("SAM Roi Manager"); 664 | Prefs.useNamesAsLabels = true; 665 | Roi imRoi = activeImage.getRoi(); 666 | deleteOtherImageRois(); 667 | roiManager.setEditMode(activeImage, true); 668 | activeImage.setRoi(imRoi); 669 | return roiManager; 670 | } 671 | 672 | private void deleteOtherImageRois() { 673 | try { 674 | int n = RoiManager.getInstance().getCount() - 1; 675 | int originalSize = this.annotatedMask.size(); 676 | for (int i = 0; i < originalSize; i ++) { 677 | List maskList = annotatedMask.pop().getMasks(); 678 | for (int j = maskList.size() - 1; j > -1; j --) { 679 | Polygon pol = maskList.get(j).getContour(); 680 | for (int k = n; k > -1; k --) { 681 | Roi roi = this.roiManager.getRoi(k); 682 | Polygon roiPol = roi.getPolygon(); 683 | if (pol.npoints != roiPol.npoints) continue; 684 | boolean equal = IntStream.range(0, pol.npoints) 685 | .allMatch(ii -> pol.xpoints[ii] == roiPol.xpoints[ii] && 686 | pol.ypoints[ii] == roiPol.ypoints[ii]); 687 | if (equal) { 688 | RoiManagerPrivateViolator.deleteRoiAtPosition(this.roiManager, k); 689 | n --; 690 | break; 691 | } 692 | 693 | } 694 | } 695 | } 696 | this.redoAnnotatedMask.clear(); 697 | } catch (Exception ex) { 698 | } 699 | } 700 | 701 | private void addTemporalRois() { 702 | //Overlay overlay = activeCanvas.getOverlay(); 703 | Overlay overlay = OverlayLabels.createOverlay(); 704 | for (Roi rr : this.roiManager.getRoisAsArray()) 705 | overlay.add(rr); 706 | this.temporalROIs.stream().forEach(r -> overlay.add(r)); 707 | this.temporalNegROIs.stream().forEach(r -> overlay.add(r)); 708 | activeCanvas.setShowAllList(overlay); 709 | this.activeImage.draw(); 710 | } 711 | 712 | /** 713 | * Add a single polygon to the ROI manager 714 | * @param pRoi 715 | */ 716 | public void addToRoiManager(final PolygonRoi pRoi ) { 717 | if (isAddingToRoiManager) roiManager.addRoi(pRoi); 718 | } 719 | 720 | /** 721 | * Add the new roi to the ROI manager 722 | * @param polys 723 | * list of polygons that will be converted into polygon ROIs and sent to the ROI manager 724 | * @param promptShape 725 | * String giving information about which prompt was used to generate the ROI 726 | */ 727 | void addToRoiManager(final List polys, final String promptShape) { 728 | if (this.roiManager.getCount() == 0 && annotatedMask.size() != 0) 729 | annotatedMask.clear(); 730 | this.redoAnnotatedMask.clear(); 731 | AddRoiCommand command = new AddRoiCommand(this.roiManager, polys); 732 | command.setModelName(this.selectedModel.getName()); 733 | command.setPromptShape(promptShape); 734 | command.setPromptCount(++ promptsCreatedCnt); 735 | command.setAddingToRoiManager(this.isAddingToRoiManager); 736 | command.execute(); 737 | this.annotatedMask.push(command); 738 | } 739 | 740 | @Override 741 | public void eventOccurred(int eventID) { 742 | if (eventID != IJEventListener.TOOL_CHANGED || callback == null) 743 | return; 744 | boolean isvalid = IJ.getToolName().equals("rectangle") 745 | || IJ.getToolName().equals("point") 746 | || IJ.getToolName().equals("multipoint"); 747 | this.callback.validPromptChosen(isvalid); 748 | } 749 | 750 | @Override 751 | /** 752 | * {@inheritDoc} 753 | * 754 | * For more info about how the macros work, please go to 755 | * https://github.com/segment-anything-models-java/SAMJ-IJ/blob/main/README.md#macros 756 | */ 757 | public void notifyBatchSamize(String modelName, String maskPrompt) { 758 | if (!Recorder.record) 759 | return; 760 | 761 | String formatedMacro = "run(\"SAMJ Annotator\", \"model=[%s]%s export=false\");" + System.lineSeparator(); 762 | String formatedMaskPrompt = " maskPrompt=[%s]"; 763 | String promptArg = maskPrompt == null ? "" : String.format(formatedMaskPrompt, maskPrompt); 764 | Recorder.recordString(String.format(formatedMacro, modelName, promptArg)); 765 | } 766 | 767 | // ===== unused events ===== 768 | @Override 769 | public void mouseEntered(MouseEvent e) {} 770 | @Override 771 | public void mouseClicked(MouseEvent e) {} 772 | @Override 773 | public void mousePressed(MouseEvent e) {} 774 | @Override 775 | public void mouseExited(MouseEvent e) {} 776 | @Override 777 | public void windowOpened(WindowEvent e) {} 778 | @Override 779 | public void windowClosing(WindowEvent e) {} 780 | @Override 781 | public void windowIconified(WindowEvent e) {} 782 | @Override 783 | public void windowDeiconified(WindowEvent e) {} 784 | @Override 785 | public void windowActivated(WindowEvent e) {} 786 | @Override 787 | public void windowDeactivated(WindowEvent e) {} 788 | @Override 789 | public void keyTyped(KeyEvent e) {} 790 | @Override 791 | public void contentsChanged(ListDataEvent e) {} 792 | @Override 793 | public void intervalAdded(ListDataEvent e) {} 794 | 795 | @Override 796 | public void intervalRemoved(ListDataEvent e) { 797 | if (isCommand) { 798 | isCommand = false; 799 | return; 800 | } 801 | List roiManagerNames = new ArrayList(); 802 | List deleteList = new ArrayList(); 803 | Enumeration elems = listModel.elements(); 804 | while (elems.hasMoreElements()) 805 | roiManagerNames.add(elems.nextElement()); 806 | List deletedNames = new ArrayList(); 807 | for (int i = annotatedMask.size() - 1; i >= 0; i --) { 808 | if (annotatedMask.get(i) instanceof DeleteRoiCommand) { 809 | deletedNames.addAll( 810 | annotatedMask.get(i).getMasks().stream() 811 | .map(mm -> mm.getName()).collect(Collectors.toList()) 812 | ); 813 | continue; 814 | } 815 | for (int j = annotatedMask.get(i).getMasks().size() - 1; j >= 0; j --) { 816 | if (roiManagerNames.contains(annotatedMask.get(i).getMasks().get(j).getName()) 817 | || deletedNames.contains(annotatedMask.get(i).getMasks().get(j).getName())) 818 | continue; 819 | deleteList.add(annotatedMask.get(i).getMasks().get(j)); 820 | 821 | } 822 | } 823 | Command command = new DeleteRoiCommand(this.roiManager, deleteList); 824 | command.setAddingToRoiManager(this.isAddingToRoiManager); 825 | //command.execute(); 826 | this.annotatedMask.push(command); 827 | this.redoAnnotatedMask.clear(); 828 | } 829 | 830 | 831 | } 832 | -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/ui/IJComboBoxItem.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Plugin to help image annotation with SAM-based Deep Learning models 4 | * %% 5 | * Copyright (C) 2024 SAMJ developers. 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package ai.nets.samj.ij.ui; 21 | 22 | 23 | import ai.nets.samj.gui.components.ComboBoxItem; 24 | import ij.ImagePlus; 25 | import ij.plugin.CompositeConverter; 26 | import net.imglib2.RandomAccessibleInterval; 27 | import net.imglib2.img.Img; 28 | import net.imglib2.img.display.imagej.ImageJFunctions; 29 | import net.imglib2.type.NativeType; 30 | import net.imglib2.type.numeric.RealType; 31 | 32 | /** 33 | * Implementation of the SAMJ interface {@link ComboBoxItem} that provides the SAMJ GUI 34 | * an item for the combobox that references ImageJ {@link ImagePlus} 35 | * 36 | * @author Carlos Garcia 37 | */ 38 | public class IJComboBoxItem extends ComboBoxItem { 39 | 40 | /** 41 | * 42 | * Combobox item that contains an Object associated to a unique identifier. 43 | * For ImageJ the object is an ImageJ {@link ImagePlus} 44 | * @param seq 45 | * the object of interest, whihc in the case of ImageJ is and {@link ImagePlus} 46 | */ 47 | public IJComboBoxItem(Object seq) { 48 | super(seq); 49 | } 50 | 51 | /** 52 | * Create an empty {@link ComboBoxItem}. Its id is -1 53 | */ 54 | public IJComboBoxItem() { 55 | super(); 56 | } 57 | 58 | @Override 59 | /** 60 | * {@inheritDoc} 61 | * 62 | * For ImageJ is the name of the ImageJ {@link ImagePlus} 63 | */ 64 | public String getImageName() { 65 | return ((ImagePlus) this.getValue()).getTitle(); 66 | } 67 | 68 | @Override 69 | /** 70 | * {@inheritDoc} 71 | * 72 | * Convert the {@link ImagePlus} into a {@link RandomAccessibleInterval} 73 | */ 74 | public & NativeType> RandomAccessibleInterval getImageAsImgLib2() { 75 | ImagePlus imp = (ImagePlus) this.getValue(); 76 | boolean isColorRGB = imp.getType() == ImagePlus.COLOR_RGB; 77 | Img image = ImageJFunctions.wrap(isColorRGB ? CompositeConverter.makeComposite(imp) : imp); 78 | return image; 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/ui/commands/AddRoiCommand.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Plugin to help image annotation with SAM-based Deep Learning models 4 | * %% 5 | * Copyright (C) 2024 SAMJ developers. 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package ai.nets.samj.ij.ui.commands; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | import java.util.concurrent.ThreadLocalRandom; 26 | 27 | import ai.nets.samj.annotation.Mask; 28 | import ai.nets.samj.ij.utils.RoiManagerPrivateViolator; 29 | import ij.gui.PolygonRoi; 30 | import ij.plugin.frame.RoiManager; 31 | 32 | public class AddRoiCommand implements Command { 33 | private RoiManager roiManager; 34 | private final List polys; 35 | private List rois; 36 | private boolean isAddingToRoiManager = true; 37 | private String shape = ""; 38 | private int promptCount = ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE); 39 | private String modelName = ""; 40 | 41 | public AddRoiCommand(RoiManager roiManager, List polys) { 42 | this.roiManager = roiManager; 43 | this.polys = polys; 44 | } 45 | 46 | public void setPromptShape(String shape) { 47 | this.shape = shape; 48 | } 49 | 50 | public void setPromptCount(int promptCount) { 51 | this.promptCount = promptCount; 52 | } 53 | 54 | public void setModelName(String modelName) { 55 | this.modelName = modelName; 56 | } 57 | 58 | public void setAddingToRoiManager(boolean addToRoiManager) { 59 | this.isAddingToRoiManager = addToRoiManager; 60 | } 61 | 62 | public List getImageJRois(){ 63 | return rois; 64 | } 65 | 66 | public List getMasks(){ 67 | return polys; 68 | } 69 | 70 | @Override 71 | public void execute() { 72 | rois = new ArrayList(); 73 | int resNo = 1; 74 | for (Mask m : polys) { 75 | final PolygonRoi pRoi = new PolygonRoi(m.getContour(), PolygonRoi.POLYGON); 76 | String name = promptCount + "." + (resNo ++) + "_"+shape + "_" + modelName; 77 | if (shape.equals("") && modelName.equals("")) 78 | name = "" + promptCount; 79 | else if (modelName.equals("")) 80 | name = promptCount + "." + (resNo) + "_"+shape; 81 | else if (shape.equals("")) 82 | name = promptCount + "." + (resNo) + "_"+modelName; 83 | 84 | pRoi.setName(name); 85 | m.setName(name); 86 | rois.add(pRoi); 87 | if (isAddingToRoiManager) roiManager.addRoi(pRoi);; 88 | } 89 | } 90 | 91 | @Override 92 | public void undo() { 93 | try { 94 | for (PolygonRoi rr2 : rois) { 95 | for (int n = this.roiManager.getCount() - 1; n >= 0; n --) { 96 | PolygonRoi rr = (PolygonRoi) roiManager.getRoi(n); 97 | if (!Arrays.equals(rr.getXCoordinates(), rr2.getXCoordinates())) 98 | continue; 99 | if (!Arrays.equals(rr.getYCoordinates(), rr2.getYCoordinates())) 100 | continue; 101 | RoiManagerPrivateViolator.deleteRoiAtPosition(this.roiManager, n); 102 | break; 103 | } 104 | 105 | } 106 | } catch (Exception ex) { 107 | ex.printStackTrace(); 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/ui/commands/Command.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Plugin to help image annotation with SAM-based Deep Learning models 4 | * %% 5 | * Copyright (C) 2024 SAMJ developers. 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package ai.nets.samj.ij.ui.commands; 21 | 22 | import java.util.List; 23 | 24 | import ai.nets.samj.annotation.Mask; 25 | import ij.gui.PolygonRoi; 26 | 27 | 28 | public interface Command { 29 | public void execute(); 30 | 31 | public void undo(); 32 | 33 | public void setAddingToRoiManager(boolean addToRoiManager); 34 | 35 | public List getImageJRois(); 36 | 37 | public List getMasks(); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/ui/commands/DeleteRoiCommand.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Plugin to help image annotation with SAM-based Deep Learning models 4 | * %% 5 | * Copyright (C) 2024 SAMJ developers. 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package ai.nets.samj.ij.ui.commands; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | 26 | import ai.nets.samj.annotation.Mask; 27 | import ai.nets.samj.ij.utils.RoiManagerPrivateViolator; 28 | import ij.gui.PolygonRoi; 29 | import ij.gui.Roi; 30 | import ij.plugin.frame.RoiManager; 31 | 32 | public class DeleteRoiCommand implements Command { 33 | private RoiManager roiManager; 34 | private final List polys; 35 | private final List rois; 36 | private boolean isAddingToRoiManager = true; 37 | 38 | public DeleteRoiCommand(RoiManager roiManager, List polys) { 39 | this.roiManager = roiManager; 40 | this.polys = polys; 41 | rois = new ArrayList(); 42 | for (Mask m : polys) { 43 | PolygonRoi roi = new PolygonRoi(m.getContour(), PolygonRoi.POLYGON); 44 | roi.setName(m.getName()); 45 | rois.add(roi); 46 | } 47 | } 48 | 49 | public void execute() { 50 | if (!isAddingToRoiManager) 51 | return; 52 | try { 53 | for (PolygonRoi rr2 : rois) { 54 | for (int n = this.roiManager.getCount() - 1; n >= 0; n --) { 55 | PolygonRoi rr = (PolygonRoi) roiManager.getRoi(n); 56 | if (!Arrays.equals(rr.getXCoordinates(), rr2.getXCoordinates())) 57 | continue; 58 | if (!Arrays.equals(rr.getYCoordinates(), rr2.getYCoordinates())) 59 | continue; 60 | RoiManagerPrivateViolator.deleteRoiAtPosition(this.roiManager, n); 61 | break; 62 | } 63 | 64 | } 65 | } catch (Exception ex) { 66 | ex.printStackTrace(); 67 | } 68 | } 69 | 70 | public void undo() { 71 | for (Roi m : rois) { 72 | if (isAddingToRoiManager) roiManager.addRoi(m);; 73 | } 74 | } 75 | 76 | @Override 77 | public void setAddingToRoiManager(boolean addToRoiManager) { 78 | this.isAddingToRoiManager = addToRoiManager; 79 | } 80 | 81 | @Override 82 | public List getImageJRois() { 83 | return rois; 84 | } 85 | 86 | @Override 87 | public List getMasks(){ 88 | return polys; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/utils/Constants.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Plugin to help image annotation with SAM-based Deep Learning models 4 | * %% 5 | * Copyright (C) 2024 SAMJ developers. 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package ai.nets.samj.ij.utils; 21 | 22 | import java.io.File; 23 | 24 | import io.bioimage.modelrunner.system.PlatformDetection; 25 | 26 | public class Constants { 27 | 28 | /** 29 | * The folder of Fiji 30 | */ 31 | public static final String FIJI_FOLDER = getFijiFolder(); 32 | 33 | private static String getFijiFolder() { 34 | File jvmFolder = new File(System.getProperty("java.home")); 35 | String imageJExecutable; 36 | if (PlatformDetection.isWindows()) 37 | imageJExecutable = "fiji-windows-x64.exe"; 38 | else if (PlatformDetection.isLinux()) 39 | imageJExecutable = "fiji-linux-x64"; 40 | else if (PlatformDetection.isMacOS() && PlatformDetection.getArch().equals(PlatformDetection.ARCH_ARM64)) 41 | imageJExecutable = "Fiji.App/Contents/MacOS/fiji-macos-arm64"; 42 | else if (PlatformDetection.isMacOS()) 43 | imageJExecutable = "Fiji.App/Contents/MacOS/fiji-macos-x64"; 44 | else 45 | throw new IllegalArgumentException("Unsupported Operating System"); 46 | while (true && jvmFolder != null) { 47 | jvmFolder = jvmFolder.getParentFile(); 48 | if (new File(jvmFolder + File.separator + imageJExecutable).isFile()) 49 | return jvmFolder.getAbsolutePath(); 50 | } 51 | return getImageJFolder(); 52 | } 53 | 54 | private static String getImageJFolder() { 55 | File jvmFolder = new File(System.getProperty("java.home")); 56 | String imageJExecutable; 57 | if (PlatformDetection.isWindows()) 58 | imageJExecutable = "ImageJ-win64.exe"; 59 | else if (PlatformDetection.isLinux()) 60 | imageJExecutable = "ImageJ-linux64"; 61 | else if (PlatformDetection.isMacOS()) 62 | imageJExecutable = "Contents/MacOS/ImageJ-macosx"; 63 | else 64 | throw new IllegalArgumentException("Unsupported Operating System"); 65 | while (true && jvmFolder != null) { 66 | jvmFolder = jvmFolder.getParentFile(); 67 | if (new File(jvmFolder + File.separator + imageJExecutable).isFile()) 68 | return jvmFolder.getAbsolutePath(); 69 | } 70 | return new File("").getAbsolutePath(); 71 | // TODO remove throw new RuntimeException("Unable to find the path to the ImageJ/Fiji being used."); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/ai/nets/samj/ij/utils/RoiManagerPrivateViolator.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * Plugin to help image annotation with SAM-based Deep Learning models 4 | * %% 5 | * Copyright (C) 2024 SAMJ developers. 6 | * %% 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * #L% 19 | */ 20 | package ai.nets.samj.ij.utils; 21 | 22 | import java.awt.EventQueue; 23 | import java.lang.reflect.Field; 24 | import java.lang.reflect.InvocationTargetException; 25 | import java.lang.reflect.Method; 26 | import java.util.ArrayList; 27 | import java.util.Objects; 28 | 29 | import javax.swing.DefaultListModel; 30 | 31 | import ij.gui.Roi; 32 | import ij.plugin.frame.RoiManager; 33 | 34 | public class RoiManagerPrivateViolator { 35 | 36 | @SuppressWarnings("unchecked") 37 | public static void deleteRoiAtPosition(RoiManager roiM, int position) throws NoSuchFieldException, SecurityException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { 38 | Objects.requireNonNull(roiM, "Please provide an initialized RoiManager."); 39 | int nRois = roiM.getCount(); 40 | if (position >= nRois) return; 41 | 42 | Field roisPrivate = RoiManager.class.getDeclaredField("rois"); 43 | roisPrivate.setAccessible(true); 44 | Field listModelPrivate = RoiManager.class.getDeclaredField("listModel"); 45 | listModelPrivate.setAccessible(true); 46 | 47 | // Access private method 48 | Method deleteOnEDTPrivate = RoiManager.class.getDeclaredMethod("deleteOnEDT", int.class); 49 | deleteOnEDTPrivate.setAccessible(true); 50 | Method updateShowAllPrivate = RoiManager.class.getDeclaredMethod("updateShowAll"); 51 | updateShowAllPrivate.setAccessible(true); 52 | 53 | ArrayList rois = ((ArrayList) roisPrivate.get(roiM)); 54 | DefaultListModel listModel = ((DefaultListModel) listModelPrivate.get(roiM)); 55 | 56 | 57 | if (EventQueue.isDispatchThread()) { 58 | rois.remove(position); 59 | listModel.remove(position); 60 | } else { 61 | deleteOnEDTPrivate.invoke(roiM, position); 62 | } 63 | updateShowAllPrivate.invoke(roiM); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/resources/.samj_ij_properties: -------------------------------------------------------------------------------- 1 | version=${project.version} 2 | name=${project.artifactId} 3 | -------------------------------------------------------------------------------- /src/main/resources/plugins.config: -------------------------------------------------------------------------------- 1 | ### 2 | # #%L 3 | # Plugin to help image annotation with SAM-based Deep Learning models 4 | # %% 5 | # Copyright (C) 2024 SAMJ developers. 6 | # %% 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # #L% 19 | ### 20 | Plugins>SAMJ, "SAMJ Annotator", ai.nets.samj.ij.SAMJ_Annotator 21 | --------------------------------------------------------------------------------