├── .github ├── build.sh ├── setup.sh └── workflows │ ├── build-main.yml │ └── build-pr.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── pom.xml └── src └── main └── java └── sc └── fiji └── imageFocus ├── Images.java ├── Main.java └── MicroscopeImageFocusQualityClassifier.java /.github/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/ci-build.sh 3 | sh ci-build.sh 4 | -------------------------------------------------------------------------------- /.github/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/ci-setup-github-actions.sh 3 | sh ci-setup-github-actions.sh 4 | -------------------------------------------------------------------------------- /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "*-[0-9]+.*" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Java 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '8' 20 | distribution: 'zulu' 21 | cache: 'maven' 22 | - name: Set up CI environment 23 | run: .github/setup.sh 24 | - name: Execute the build 25 | run: .github/build.sh 26 | env: 27 | GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} 28 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 29 | MAVEN_USER: ${{ secrets.MAVEN_USER }} 30 | MAVEN_PASS: ${{ secrets.MAVEN_PASS }} 31 | OSSRH_PASS: ${{ secrets.OSSRH_PASS }} 32 | SIGNING_ASC: ${{ secrets.SIGNING_ASC }} 33 | -------------------------------------------------------------------------------- /.github/workflows/build-pr.yml: -------------------------------------------------------------------------------- 1 | name: build PR 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Java 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: '8' 18 | distribution: 'zulu' 19 | cache: 'maven' 20 | - name: Set up CI environment 21 | run: .github/setup.sh 22 | - name: Execute the build 23 | run: .github/build.sh 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven # 2 | /target/ 3 | 4 | # Eclipse # 5 | /.classpath 6 | /.project 7 | /.settings/ 8 | -------------------------------------------------------------------------------- /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 | [![](https://github.com/fiji/microscope-image-quality/actions/workflows/build-main.yml/badge.svg)](https://github.com/fiji/microscope-image-quality/actions/workflows/build-main.yml) 2 | 3 | This project is an [ImageJ](https://imagej.net) plugin for Google's 4 | [microscope image focus quality classifier](https://github.com/google/microscopeimagequality). 5 | 6 | ## Publication 7 | 8 | Yang, S. J., Berndl, M., Ando, D. M., Barch, M., Narayanaswamy, A. , 9 | Christiansen, E., Hoyer, S., Roat, C., Hung, J., Rueden, C. T., Shankar, A., 10 | Finkbeiner, S., & and Nelson, P. (2018), "[Assessing microscope image focus 11 | quality with deep learning](https://doi.org/10.1186/s12859-018-2087-4)", 12 | BMC BioInformatics 19(1). 13 | 14 | ## Quickstart 15 | 16 | Assuming you already have [Apache Maven](https://maven.apache.org) installed: 17 | 18 | ```sh 19 | mvn -Pexec 20 | ``` 21 | Instructions for installing [Apache Maven](https://maven.apache.org) might be 22 | as simple as `apt-get install maven` on Ubuntu and `brew install maven` on OS X 23 | with [homebrew](https://brew.sh). 24 | 25 | See the [ImageJ Development](https://imagej.net/Development) page for 26 | further details. 27 | 28 | ## Installation in Fiji 29 | 30 | If you have [Fiji](https://fiji.sc) installed and want to incorporate this 31 | plugin into your installation: 32 | 33 | ```sh 34 | # Set this to the path there Fiji.app is installed 35 | FIJI_APP_PATH="/Users/me/Desktop/Fiji.app" 36 | mvn -Dimagej.app.directory="${FIJI_APP_PATH}" 37 | ``` 38 | 39 | Then restart Fiji and search for "quality", or navigate to 40 | _Plugins › Classification › Microscope Image Focus Quality_ in the menu. 41 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.scijava 7 | pom-scijava 8 | 29.2.1 9 | 10 | 11 | 12 | sc.fiji 13 | microscope-image-quality 14 | 1.0.2-SNAPSHOT 15 | 16 | Microscopy Image Focus Quality Classifier 17 | ImageJ plugin to analyze focus quality of microscope images. 18 | https://github.com/fiji/microscope-image-quality 19 | 2017 20 | 21 | Fiji 22 | http://fiji.sc/ 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 | ctrueden 35 | Curtis Rueden 36 | https://imagej.net/User:Rueden 37 | 38 | founder 39 | lead 40 | developer 41 | debugger 42 | reviewer 43 | support 44 | maintainer 45 | 46 | 47 | 48 | 49 | 50 | Samuel Yang 51 | samueljyang 52 | founder 53 | 54 | 55 | Asim Shankar 56 | asimshankar 57 | founder 58 | 59 | 60 | 61 | 62 | 63 | Image.sc Forum 64 | https://forum.image.sc/tags/fiji 65 | 66 | 67 | 68 | 69 | scm:git:https://github.com/fiji/microscope-image-quality 70 | scm:git:git@github.com:fiji/microscope-image-quality 71 | HEAD 72 | https://github.com/fiji/microscope-image-quality 73 | 74 | 75 | GitHub Issues 76 | https://github.com/fiji/microscope-image-quality/issues 77 | 78 | 79 | GitHub Actions 80 | https://github.com/fiji/microscope-image-quality/actions 81 | 82 | 83 | 84 | sc.fiji.imageFocus.Main 85 | sc.fiji.imageFocus 86 | apache_v2 87 | Google, Inc. and Board of Regents 88 | of the University of Wisconsin-Madison. 89 | 90 | 91 | sign,deploy-to-scijava 92 | 93 | 94 | 95 | 96 | scijava.public 97 | https://maven.scijava.org/content/groups/public 98 | 99 | 100 | 101 | 102 | 103 | net.imagej 104 | imagej 105 | 106 | 107 | 108 | net.imagej 109 | imagej-legacy 110 | 111 | 112 | net.imagej 113 | imagej-tensorflow 114 | 115 | 116 | org.tensorflow 117 | tensorflow 118 | 119 | 120 | org.tensorflow 121 | proto 122 | 123 | 124 | 125 | junit 126 | junit 127 | test 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/main/java/sc/fiji/imageFocus/Images.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * ImageJ plugin to analyze focus quality of microscope images. 4 | * %% 5 | * Copyright (C) 2017 - 2020 Google, Inc. and Board of Regents 6 | * of the University of Wisconsin-Madison. 7 | * %% 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * #L% 20 | */ 21 | 22 | package sc.fiji.imageFocus; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.stream.Collectors; 27 | 28 | import net.imglib2.RandomAccessibleInterval; 29 | import net.imglib2.converter.Converter; 30 | import net.imglib2.converter.Converters; 31 | import net.imglib2.type.numeric.RealType; 32 | import net.imglib2.type.numeric.real.FloatType; 33 | import net.imglib2.util.Util; 34 | import net.imglib2.view.IntervalView; 35 | import net.imglib2.view.Views; 36 | 37 | /** 38 | * Utility methods for manipulating images. 39 | * 40 | * @author Curtis Rueden 41 | */ 42 | public final class Images { 43 | 44 | private Images() { 45 | // NB: Prevent instantiation of utility class. 46 | } 47 | 48 | /** Normalizes an image to {@link FloatType} in range {@code [0, 1]}. */ 49 | public static > RandomAccessibleInterval 50 | normalize(final RandomAccessibleInterval image) 51 | { 52 | final T sample = Util.getTypeFromInterval(image); 53 | final double min = sample.getMinValue(); 54 | final double max = sample.getMaxValue(); 55 | final Converter normalizer = // 56 | (in, out) -> out.setReal((in.getRealDouble() - min) / (max - min)); 57 | return Converters.convert(image, normalizer, new FloatType()); 58 | } 59 | 60 | /** TODO */ 61 | public static RandomAccessibleInterval tile( 62 | final RandomAccessibleInterval image, final long xTileCount, 63 | final long yTileCount, final long xTileSize, final long yTileSize) 64 | { 65 | final long[] min = new long[2]; 66 | final long[] max = new long[2]; 67 | final long width = image.dimension(0); 68 | final long height = image.dimension(1); 69 | final ArrayList> strips = new ArrayList<>(); 70 | final ArrayList> tiles = new ArrayList<>(); 71 | for (long ty = 0; ty < yTileCount; ty++) { 72 | tiles.clear(); 73 | final long y = offset(ty, yTileCount, yTileSize, height); 74 | for (long tx = 0; tx < xTileCount; tx++) { 75 | final long x = offset(tx, xTileCount, xTileSize, width); 76 | min[0] = x; 77 | min[1] = y; 78 | max[0] = x + xTileSize - 1; 79 | max[1] = y + yTileSize - 1; 80 | tiles.add(Views.interval(image, min, max)); 81 | } 82 | strips.add(concatenateX(tiles)); 83 | } 84 | return Views.concatenate(1, strips); 85 | } 86 | 87 | /** TODO */ 88 | public static long offset(final long i, final long total, 89 | final long tileSize, final long dimSize) 90 | { 91 | // The total amount of space needing to be distributed between tiles. 92 | final long space = dimSize - tileSize * total; 93 | // Return the tile offset plus the gap offset, minimizing rounding error. 94 | return i * tileSize + (i + 1) * space / (total + 1); 95 | } 96 | 97 | /** Work around a limitation when concatenating dims before the last one. */ 98 | private static RandomAccessibleInterval concatenateX( 99 | final ArrayList> tiles) 100 | { 101 | final List> flippedTiles = tiles.stream() // 102 | .map(tile -> Views.permute(tile, 0, 1)) // 103 | .collect(Collectors.toList()); 104 | final RandomAccessibleInterval concatenated = // 105 | Views.concatenate(1, flippedTiles); 106 | return Views.permute(concatenated, 1, 0); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/sc/fiji/imageFocus/Main.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * ImageJ plugin to analyze focus quality of microscope images. 4 | * %% 5 | * Copyright (C) 2017 - 2020 Google, Inc. and Board of Regents 6 | * of the University of Wisconsin-Madison. 7 | * %% 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * #L% 20 | */ 21 | 22 | package sc.fiji.imageFocus; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | 27 | import net.imagej.ImageJ; 28 | 29 | import org.scijava.widget.FileWidget; 30 | 31 | /** 32 | * Test drive for the {@link MicroscopeImageFocusQualityClassifier} command. 33 | * 34 | * @author Curtis Rueden 35 | */ 36 | public final class Main { 37 | 38 | public static void main(final String[] args) throws IOException { 39 | // Launch ImageJ. 40 | final ImageJ ij = new ImageJ(); 41 | ij.launch(args); 42 | 43 | // Load an image. 44 | final File file = ij.ui().chooseFile(null, FileWidget.OPEN_STYLE); 45 | if (file == null) return; 46 | final Object data = ij.io().open(file.getAbsolutePath()); 47 | ij.ui().show(data); 48 | 49 | // Run the command. 50 | ij.command().run(MicroscopeImageFocusQualityClassifier.class, true); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/sc/fiji/imageFocus/MicroscopeImageFocusQualityClassifier.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * #%L 3 | * ImageJ plugin to analyze focus quality of microscope images. 4 | * %% 5 | * Copyright (C) 2017 - 2020 Google, Inc. and Board of Regents 6 | * of the University of Wisconsin-Madison. 7 | * %% 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * #L% 20 | */ 21 | 22 | package sc.fiji.imageFocus; 23 | 24 | import ij.ImagePlus; 25 | import ij.gui.Line; 26 | import ij.gui.Overlay; 27 | import ij.gui.Roi; 28 | import ij.gui.TextRoi; 29 | 30 | import java.awt.Color; 31 | import java.io.IOException; 32 | import java.util.Arrays; 33 | import java.util.List; 34 | 35 | import net.imagej.Dataset; 36 | import net.imagej.DatasetService; 37 | import net.imagej.ImgPlus; 38 | import net.imagej.axis.Axes; 39 | import net.imagej.axis.AxisType; 40 | import net.imagej.display.ColorTables; 41 | import net.imagej.tensorflow.TensorFlowService; 42 | import net.imagej.tensorflow.Tensors; 43 | import net.imglib2.RandomAccess; 44 | import net.imglib2.RandomAccessibleInterval; 45 | import net.imglib2.display.ColorTable8; 46 | import net.imglib2.img.Img; 47 | import net.imglib2.type.numeric.RealType; 48 | import net.imglib2.type.numeric.integer.UnsignedShortType; 49 | import net.imglib2.type.numeric.real.FloatType; 50 | 51 | import org.scijava.Initializable; 52 | import org.scijava.ItemIO; 53 | import org.scijava.command.Command; 54 | import org.scijava.command.Previewable; 55 | import org.scijava.io.http.HTTPLocation; 56 | import org.scijava.log.LogService; 57 | import org.scijava.plugin.Parameter; 58 | import org.scijava.plugin.Plugin; 59 | import org.scijava.widget.NumberWidget; 60 | import org.tensorflow.SavedModelBundle; 61 | import org.tensorflow.Tensor; 62 | import org.tensorflow.framework.MetaGraphDef; 63 | import org.tensorflow.framework.SignatureDef; 64 | import org.tensorflow.framework.TensorInfo; 65 | 66 | /** 67 | * Command to apply the Microscopy image focus quality classifier model on an 68 | * input (16-bit, greyscale image). 69 | *

70 | * The model has been trained on raw 16-bit microscope images with 71 | * integer-valued inputs in {@code [0, 65535]}, where the black level is usually 72 | * in {@code [0, ~1000]} and the typical brightness of cells is in 73 | * {@code [~1000, ~10,000]}. 74 | *

75 | *

76 | * This command optionally produces a multi-channel image of probability values 77 | * where each channel corresponds to one focus class, and optionally adds an 78 | * overlay annotation to the input image to visualize the most likely focus 79 | * class of each region. 80 | *

81 | */ 82 | @Plugin(type = Command.class, 83 | menuPath = "Plugins>Classification>Microscope Image Focus Quality") 84 | public class MicroscopeImageFocusQualityClassifier> 85 | implements Command, Initializable, Previewable 86 | { 87 | 88 | private static final String MODEL_URL = 89 | "https://downloads.imagej.net/fiji/models/microscope-image-quality-model.zip"; 90 | 91 | private static final String MODEL_NAME = "microscope-image-quality"; 92 | 93 | // Same as the tag used in export_saved_model in the Python code. 94 | private static final String MODEL_TAG = "inference"; 95 | 96 | // Same as 97 | // tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY 98 | // in Python. Perhaps this should be an exported constant in TensorFlow's Java 99 | // API. 100 | private static final String DEFAULT_SERVING_SIGNATURE_DEF_KEY = 101 | "serving_default"; 102 | 103 | private static final int TILE_SIZE = 84; 104 | 105 | @Parameter 106 | private TensorFlowService tensorFlowService; 107 | 108 | @Parameter 109 | private DatasetService datasetService; 110 | 111 | @Parameter 112 | private LogService log; 113 | 114 | @Parameter(label = "Microscope Image") 115 | private Img originalImage; 116 | 117 | /** 118 | * ImageJ 1.x version of the image to process. 119 | *

120 | * Only used for overlaying patches. 121 | *

122 | */ 123 | @Parameter(required = false) 124 | private ImagePlus originalImagePlus; 125 | 126 | private Overlay originalOverlay; 127 | 128 | @Parameter(label = "Number of tiles in X", persist = false, 129 | callback = "refreshTilePreview", min = "1", 130 | description = "The number of tiles to process in the X direction. " + 131 | "The smaller this value, the less
of the image will be covered " + 132 | "horizontally, but the faster the processing will be.") 133 | private long tileCountX = 1; 134 | 135 | @Parameter(label = "Number of tiles in Y", persist = false, 136 | callback = "refreshTilePreview", min = "1", 137 | description = "The number of tiles to process in the Y direction. " + 138 | "The smaller this value, the less
of the image will be covered " + 139 | "vertically, but the faster the processing will be.") 140 | private long tileCountY = 1; 141 | 142 | @Parameter(label = "Generate probability image", 143 | description = "When checked, a multi-channel image will be created " + 144 | "with one channel per focus level,
and each value corresponding " + 145 | "to the probability of that sample being at that focus level.") 146 | private boolean createProbabilityImage = true; 147 | 148 | @Parameter(label = "Overlay probability patches", 149 | description = "When checked, each classified region of the image " + 150 | "will be overlaid with a color
whose hue denotes the most likely " + 151 | "focus level and whose brightness denotes
the confidence " + 152 | "(i.e., probability) of the region being at that level.") 153 | private boolean overlayPatches = true; 154 | 155 | @Parameter(label = "Show patches as solid rectangles", 156 | description = "When checked, overlaid probability patches will be " + 157 | "filled semi-transparent
and solid; when unchecked, they will be " + 158 | "drawn as hollow boundary boxes.") 159 | private boolean solidPatches; 160 | 161 | @Parameter(label = "Displayed patch border width", // 162 | min = "1", max = "10", style = NumberWidget.SCROLL_BAR_STYLE, 163 | description = "When drawing probability patches as boundary boxes, " + 164 | "this option controls the box thickness.") 165 | private int borderWidth = 4; 166 | 167 | @Parameter(type = ItemIO.OUTPUT) 168 | private Dataset probDataset; 169 | 170 | @Override 171 | public void run() { 172 | try { 173 | validateFormat(originalImage); 174 | final RandomAccessibleInterval tiledImage = // 175 | Images.tile(originalImage, tileCountX, tileCountY, TILE_SIZE, TILE_SIZE); 176 | final RandomAccessibleInterval normalizedImage = // 177 | Images.normalize(tiledImage); 178 | 179 | final long loadModelStart = System.nanoTime(); 180 | final HTTPLocation source = new HTTPLocation(MODEL_URL); 181 | try (final SavedModelBundle model = // 182 | tensorFlowService.loadModel(source, MODEL_NAME, MODEL_TAG)) { 183 | final long loadModelEnd = System.nanoTime(); 184 | log.info(String.format( 185 | "Loaded microscope focus image quality model in %dms", (loadModelEnd - 186 | loadModelStart) / 1000000)); 187 | 188 | // Extract names from the model signature. 189 | // The strings "input", "probabilities" and "patches" are meant to be 190 | // in sync with the model exporter (export_saved_model()) in Python. 191 | final SignatureDef sig = MetaGraphDef.parseFrom(model.metaGraphDef()) 192 | .getSignatureDefOrThrow(DEFAULT_SERVING_SIGNATURE_DEF_KEY); 193 | try (final Tensor inputTensor = Tensors.tensor(normalizedImage)) { 194 | // Run the model. 195 | final long runModelStart = System.nanoTime(); 196 | final List> fetches = model.session().runner() // 197 | .feed(opName(sig.getInputsOrThrow("input")), inputTensor) // 198 | .fetch(opName(sig.getOutputsOrThrow("probabilities"))) // 199 | .fetch(opName(sig.getOutputsOrThrow("patches"))) // 200 | .run(); 201 | final long runModelEnd = System.nanoTime(); 202 | log.info(String.format("Ran image through model in %dms", // 203 | (runModelEnd - runModelStart) / 1000000)); 204 | 205 | // Process the results. 206 | try (final Tensor probabilities = fetches.get(0); 207 | final Tensor patches = fetches.get(1)) 208 | { 209 | processPatches(probabilities, patches); 210 | } 211 | } 212 | } 213 | } 214 | catch (final Exception exc) { 215 | // Use the LogService to report the error. 216 | log.error(exc); 217 | } 218 | } 219 | 220 | @Override 221 | public void initialize() { 222 | if (originalImage == null) return; 223 | tileCountX = Math.max(1, originalImage.dimension(0) / TILE_SIZE); 224 | tileCountY = Math.max(1, originalImage.dimension(1) / TILE_SIZE); 225 | if (originalImagePlus != null) { 226 | originalOverlay = originalImagePlus.getOverlay(); 227 | } 228 | refreshTilePreview(); 229 | } 230 | 231 | @Override 232 | public void preview() { 233 | // NB: No action needed. 234 | } 235 | 236 | @Override 237 | public void cancel() { 238 | if (originalImagePlus != null && originalOverlay != null) { 239 | originalImagePlus.setOverlay(originalOverlay); 240 | } 241 | } 242 | 243 | /** Callback method for {@link #tileCountX} and {@link #tileCountY}. */ 244 | private void refreshTilePreview() { 245 | if (originalImagePlus == null) return; 246 | 247 | final int tileOpacity = 64; 248 | final Color evenColor = new Color(100, 255, 255, tileOpacity); 249 | final Color oddColor = new Color(255, 255, 100, tileOpacity); 250 | 251 | final long w = originalImage.dimension(0); 252 | final long h = originalImage.dimension(1); 253 | 254 | final Overlay overlay = new Overlay(); 255 | for (long y = 0; y < tileCountY; y++) { 256 | final long offsetY = Images.offset(y, tileCountY, TILE_SIZE, h); 257 | for (long x = 0; x < tileCountX; x++) { 258 | final long offsetX = Images.offset(x, tileCountX, TILE_SIZE, w); 259 | final Roi tile = new Roi(offsetX, offsetY, TILE_SIZE, TILE_SIZE); 260 | tile.setFillColor((x + y) % 2 == 0 ? evenColor : oddColor); 261 | overlay.add(tile); 262 | } 263 | } 264 | originalImagePlus.setOverlay(overlay); 265 | } 266 | 267 | private void validateFormat(final Img image) throws IOException { 268 | final int ndims = image.numDimensions(); 269 | if (ndims != 2) { 270 | final long[] dims = new long[ndims]; 271 | image.dimensions(dims); 272 | throw new IOException("Can only process 2D images, not an image with " + 273 | ndims + " dimensions (" + Arrays.toString(dims) + ")"); 274 | } 275 | if (!(image.firstElement() instanceof UnsignedShortType)) { 276 | throw new IOException("Can only process uint16 images. " + 277 | "Please convert your image first via Image > Type > 16-bit."); 278 | } 279 | } 280 | 281 | private void processPatches(final Tensor probabilities, 282 | final Tensor patches) 283 | { 284 | // Extract probability values. 285 | final long[] probShape = probabilities.shape(); 286 | log.debug("Probabilities shape: " + Arrays.toString(probShape)); 287 | final int probPatchCount = (int) probShape[0]; 288 | final int classCount = (int) probShape[1]; 289 | final float[][] probValues = new float[probPatchCount][classCount]; 290 | probabilities.copyTo(probValues); 291 | 292 | // Extract and validate patch layout. 293 | final long[] patchShape = patches.shape(); 294 | log.debug("Patches shape: " + Arrays.toString(patchShape)); 295 | assert patchShape.length == 4; 296 | final int patchCount = (int) patchShape[0]; 297 | assert patchCount == probPatchCount; 298 | final int patchHeight = (int) patchShape[1]; 299 | final int patchWidth = (int) patchShape[2]; 300 | assert patchShape[3] == 1; 301 | 302 | // Dump probabilities to the log. 303 | for (int i = 0; i < probShape[0]; ++i) { 304 | log.info(String.format("Patch %02d probabilities: %s", i, // 305 | Arrays.toString(probValues[i]))); 306 | } 307 | 308 | // Synthesize matched-size image with computed probabilities. 309 | if (createProbabilityImage) { 310 | createProbabilityImage(classCount, probValues, patchHeight, patchWidth); 311 | } 312 | 313 | // Add ImageJ 1.x overlay to the active image. 314 | if (overlayPatches && originalImagePlus != null) { 315 | addOverlay(probValues, patchWidth, patchHeight); 316 | } 317 | } 318 | 319 | private void createProbabilityImage(final int classCount, 320 | final float[][] probValues, final int patchHeight, final int patchWidth) 321 | { 322 | // Create probability image. 323 | final long width = originalImage.dimension(0); 324 | final long height = originalImage.dimension(1); 325 | final long[] dims = { width, height, classCount }; 326 | final AxisType[] axes = { Axes.X, Axes.Y, Axes.CHANNEL }; 327 | final FloatType type = new FloatType(); 328 | probDataset = datasetService.create(type, dims, "Probabilities", axes, 329 | false); 330 | 331 | // Set the probability image to normalized grayscale. 332 | probDataset.initializeColorTables(classCount); 333 | for (int c = 0; c < classCount; c++) { 334 | probDataset.setColorTable(ColorTables.GRAYS, c); 335 | probDataset.setChannelMinimum(c, 0); 336 | probDataset.setChannelMaximum(c, 1); 337 | } 338 | 339 | final ImgPlus probImg = probDataset.typedImg(type); 340 | 341 | // Cover the probability image with NaNs. 342 | // Real values will be written only to tile-covered areas. 343 | for (final FloatType sample : probImg) { 344 | sample.set(Float.NaN); 345 | } 346 | 347 | // Populate the probability image's sample values. 348 | final RandomAccess access = probImg.randomAccess(); 349 | for (int t = 0; t < probValues.length; t++) { 350 | for (int c = 0; c < probValues[t].length; c++) { 351 | // Compute tile coordinates from probability value index. 352 | final long tx = t % tileCountX; 353 | final long ty = t / tileCountX; 354 | 355 | // Compute offset of tile in original image. 356 | final long offsetX = Images.offset(tx, tileCountX, patchWidth, width); 357 | final long offsetY = Images.offset(ty, tileCountY, patchHeight, height); 358 | 359 | // Copy the current value to every sample within the tile. 360 | final float value = probValues[t][c]; 361 | access.setPosition(c, 2); 362 | access.setPosition(offsetY, 1); 363 | for (int y = 0; y < TILE_SIZE; y++) { 364 | access.setPosition(offsetX, 0); 365 | for (int x = 0; x < TILE_SIZE; x++) { 366 | access.get().set(value); 367 | access.fwd(0); 368 | } 369 | access.fwd(1); 370 | } 371 | } 372 | } 373 | } 374 | 375 | private void addOverlay(final float[][] probValues, // 376 | final int patchWidth, final int patchHeight) 377 | { 378 | final int patchCount = probValues.length; 379 | final int classCount = probValues[0].length; 380 | 381 | final Overlay overlay = new Overlay(); 382 | 383 | final int strokeWidth = solidPatches ? 0 : borderWidth; 384 | final ColorTable8 lut = ColorTables.SPECTRUM; 385 | final int lutMaxIndex = 172; 386 | 387 | final long width = originalImage.dimension(0); 388 | final long height = originalImage.dimension(1); 389 | 390 | for (int p = 0; p < patchCount; p++) { 391 | final long tx = p % tileCountX; 392 | final long ty = p / tileCountX; 393 | final long offsetX = Images.offset(tx, tileCountX, patchWidth, width) + strokeWidth / 2; 394 | final long offsetY = Images.offset(ty, tileCountY, patchHeight, height) + strokeWidth / 2; 395 | 396 | final Roi roi = new Roi(offsetX, offsetY, // 397 | patchWidth - strokeWidth, patchHeight - strokeWidth); 398 | final int classIndex = maxIndex(probValues[p]); 399 | final double confidence = probValues[p][classIndex]; 400 | 401 | // NB: We scale to (0, 172) here instead of (0, 255) to avoid the high 402 | // indices looping from blue and purple back into red where we started. 403 | final int lutIndex = lutMaxIndex * classIndex / (classCount - 1); 404 | 405 | final int r = (int) (lut.get(0, lutIndex) * confidence); 406 | final int g = (int) (lut.get(1, lutIndex) * confidence); 407 | final int b = (int) (lut.get(2, lutIndex) * confidence); 408 | final int opacity = solidPatches ? 128 : 255; 409 | final Color color = new Color(r, g, b, opacity); 410 | if (solidPatches) { 411 | roi.setFillColor(color); 412 | } 413 | else { 414 | roi.setStrokeColor(color); 415 | roi.setStrokeWidth(strokeWidth); 416 | } 417 | overlay.add(roi); 418 | } 419 | 420 | // Add color bar with legend. 421 | 422 | final int barHeight = 24, barPad = 5; 423 | final int barY = originalImagePlus.getHeight() - barHeight - barPad; 424 | 425 | final TextRoi labelGood = new TextRoi(barPad, barY, "In focus"); 426 | labelGood.setStrokeColor(Color.white); 427 | overlay.add(labelGood); 428 | 429 | final int barOffset = 2 * barPad + (int) labelGood.getBounds().getWidth(); 430 | 431 | final TextRoi labelBad = new TextRoi(barOffset + lutMaxIndex + barPad, 432 | barY, "Out of focus"); 433 | labelBad.setStrokeColor(Color.white); 434 | overlay.add(labelBad); 435 | 436 | for (int i = 0; i < lutMaxIndex; i++) { 437 | final int barX = barOffset + i; 438 | final Roi line = new Line(barX, barY, barX, barY + barHeight); 439 | final int r = lut.get(0, i); 440 | final int g = lut.get(1, i); 441 | final int b = lut.get(2, i); 442 | line.setStrokeColor(new Color(r, g, b)); 443 | overlay.add(line); 444 | } 445 | 446 | originalImagePlus.setOverlay(overlay); 447 | } 448 | 449 | private static int maxIndex(final float[] values) { 450 | float max = values[0]; 451 | int index = 0; 452 | for (int i = 1; i < values.length; i++) { 453 | if (values[i] > max) { 454 | max = values[i]; 455 | index = i; 456 | } 457 | } 458 | return index; 459 | } 460 | 461 | /** 462 | * The SignatureDef inputs and outputs contain names of the form 463 | * {@code :}, where for this model, 464 | * {@code } is always 0. This function trims the {@code :0} 465 | * suffix to get the operation name. 466 | */ 467 | private static String opName(final TensorInfo t) { 468 | final String n = t.getName(); 469 | if (n.endsWith(":0")) { 470 | return n.substring(0, n.lastIndexOf(":0")); 471 | } 472 | return n; 473 | } 474 | } 475 | --------------------------------------------------------------------------------