From f32de35db4879ea8fb0f44ca04794f31e9df1804 Mon Sep 17 00:00:00 2001 From: Sebastian Rassmann Date: Tue, 18 Aug 2020 15:54:49 +0200 Subject: [PATCH] v0.2.0 release -implement function to process time frames -fix issues on MacOS (see https://github.com/sRassmann/canny3d-thresholder/commit/63a5affac94a981aa8d3378c1ca11042fb9ce3ae ) -update and fix Readme --- README.md | 42 +++--- pom.xml | 2 +- src/main/java/canny3d_thresholder/Main.java | 2 +- .../canny3d_thresholder/ProcessSettings.java | 2 +- .../java/canny3d_thresholder/Processing.java | 124 +++++++++++------- src/main/resources/plugins.config | 4 +- 6 files changed, 101 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 0e54df3..b88afa0 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,19 @@ An ImageJ Plugin implementing a modified [Canny 3d Edge Detection](https://en.wi Developed and optimized for [CiliaQ](https://github.com/hansenjn/CiliaQ). ## Plugin Description - -<<<<<<< HEAD + The plugin implements a [custom batch processing handler](https://github.com/sRassmann/imageJ-plugin-template) and performs the following steps: -1. Smoothes the image stack to suppress random noise via a 2D Gaussian blur filter (the z dimension is assumed to have a lower resolution and a blur effect due to confocal imaging) -======= -The plugin implements a [custom batch processing handler](https://github.com/sRassmann/imageJ-plugin-template) and performs the follwoing steps: -1. Smoothes the image stack to supress random noise via a 2D Gaussian blur filter (the z dimension is assumed to have a lower resolution and a blur efect due to confocal imaging) ->>>>>>> branch 'master' of https://github.com/sRassmann/canny3d-thresholder -2. Detect edges using a 3D Sobel kernel -3. Performing a 3D Hysterisis Threshold: All pixels above a defined high threshold are kept, pixel below the defined low threshold are neglected, pixels in between the low and high threshold are only kept if they are connected to pixels above the high threshold. +1. Smoothes the image stack to suppress random noise via a 2D Gaussian blur filter (the z dimension is assumed to have a lower resolution and a blur effect due to confocal imaging). +2. Detects edges using a 3D Sobel kernel. +3. Performs a 3D Hysterisis Threshold: All pixels above a defined high threshold are kept, pixel below the defined low threshold are neglected, pixels in between the low and high threshold are only kept if they are connected to pixels above the high threshold. High and low thresholds can be defined using either custom values or can be calculated using ImageJ's thresholding methods based on the histogram of the whole stack. -4. Holes encapsulated in all dimensions are filled -<<<<<<< HEAD +4. Holes encapsulated in all dimensions are filled. 5. The original intensity values within the detected objects are kept, whereas the remaining voxels are set to 0. Thus, the output image is pseudo binarized and intensity values within the objects can be evaluated in downstream analysis (e.g. CiliaQ). -======= -5. The original intensity values within the detected objects are kept, wheares the remaining voxels are set to 0. Thus, the output image is pseudo binarized and intensity values within the objects can be evaluated in downstream analysis (e.g. CiliaQ). ->>>>>>> branch 'master' of https://github.com/sRassmann/canny3d-thresholder +The plugin can also save the remaining channels of the stack, thus, can act as an additional channel splitter. ## User Guide - -<<<<<<< HEAD + ### installation 1. This plugin requires packages included in the [Fiji release](https://imagej.net/Fiji/Downloads) of ImageJ. Core ImageJ might work if the dependencies are installed manually but is not recommended! @@ -36,7 +27,7 @@ High and low thresholds can be defined using either custom values or can be calc ### input -As of v0.0.2 the plugin can handle multi-channel (up to 4 channels) TIFF stacks as well as common Bioformats (e.g. Nikon's .nd2 or Olympus' .oib). The tool extracts a defined channel and performs the thresholding and can also save the remaining channels. Thus, the tool can be used as a channel splitter. +The plugin can handle multi-channel (up to 4 channels) timelapse image stacks ("4D") provided either as TIFF stack or as a common Bioformat (e.g. Nikon's .nd2 or Olympus' .oib). ### processing 1. Open ImageJ and select *Plugin*>*SR*>*Canny 3D Thresholder* @@ -51,8 +42,8 @@ As of v0.0.2 the plugin can handle multi-channel (up to 4 channels) TIFF stacks 3. If the manual file selection mode was choose a file selection dialog will apear: * Click on *select files individually* to select single files from the file system. * The option *select files by pattern* allows to load all files containing a specific character pattern within a defined root directory. Select a directory to start the search in the *Choose directory to start pattern matching* dialog and define the pattern which the name of each file should contain (e.g. '.nd2' for all microscopy files in the folder) - all files containing this character string at least on time will be added to the file list. Further, it is possible to neglect all files containing a certain pattern (e.g. a date) or which are located in directory containing a defined pattern in the name (e.g. directories marked with 'processed'). For more advanced selection (e.g. any date followed by an specific experiment ID) you can choose *Input as Regex* and specify patterns as regular expressions (regex). See [here](https://www.vogella.com/tutorials/JavaRegularExpressions/article.html) for an introduction. - * Within the Multi-File-Manager selection dialog individual files can be manually excluded by selecting the name of the file and pressing *remove selected file*. -======= + * Within the Multi-File-Manager selection dialog individual files can be manually excluded by selecting the name of the file and pressing *remove selected file*. + ### Installation 1. This plugin requires packages included in the [Fiji release](https://imagej.net/Fiji/Downloads) of ImageJ. Core ImageJ might work if the dependencies are installed manually but is not recommended! @@ -68,25 +59,24 @@ As of v0.0.2 the plugin can handle multi-channel (up to 4 channels) TIFF stacks 1. Open ImageJ and select *Plugin*>*SR*>*Canny 3D Thresholder* 2. In the user dialog the following options are available: * File selection mode: Allows to use the active (=last used) image in ImageJ (*active image in FIJI*), all images open images (*all images open in FIJI*), or to load a previously defined set of files from .txt containing the full paths to the images in separate lines (*use list*). The pre-selected option (*manual file selection*) lets the user select files in a subsequent dialog (*Multi-File-Manager*). - * Select image input image format: Choose between single channel stack, multi channel stack, or a raw microscopy file. If multi channel stack or raw microscopy file is chosen another dialog the thresholding channel and the set of addtionally saved channels need to be defined in a subsequent dialog. - * Sigma for Gaussian blur: Defines the amount of blurr added to the image as the sigma ("radius") of the Gaussian distribution in pixels. + * Select image input image format: Choose between single channel stack, multi channel stack, or a raw microscopy file. If multi channel stack or raw microscopy file is chosen another dialog the thresholding channel and the set of additionaly saved channels need to be defined in a subsequent dialog. + * Sigma for Gaussian blur: Defines the amount of blur added to the image as the sigma ("radius") of the Gaussian distribution in pixels. In the test data a sigma of 1 and 0.5 pixels yielded good results for images with 0.1 and 0.2 µm/pixel, respectively. This values might vary with the quality (less noise might allow for less blur and, thus a lower value for sigma). * Alpha: The parameters defines the sensitivity of the edge detection. - * Method for low and high threshold: Select one of ImageJ's core thresholding algorithms to calculate the low and high threshold or choose *Custom Value* to define a fixed value. + * Method for low and high threshold: Select one of ImageJ's core thresholding algorithms to calculate the low and high threshold or choose *Custom Value* to define a fixed value. * Output to new Folder: Allows to select a folder where all output data is saved. If this option is not chosen, each output file will be saved in the same directory as the corresponding raw input file. 3. If the manual file selection mode was choose a file selection dialog will apear: * Click on *select files individually* to select single files from the file system. * The option *select files by pattern* allows to load all files containing a specific character pattern within a defined root directory. Select a directory to start the search in the *Choose directory to start pattern matching* dialog and define the pattern which the name of each file should contain (e.g. '.nd2' for all microscopy files in the folder) - all files containing this character string at least on time will be added to the file list. Further, it is possible to neglect all files containing a certaing pattern (e.g. a date) or which are located in directory containg a defined pattern in the name (e.g. directories marked with 'processed'). For more advanced selection (e.g. any date followed by an specific experiment ID) you can choose *Input as Regex* and specify patterns as regular expressions (regex). See [here](https://www.vogella.com/tutorials/JavaRegularExpressions/article.html) for an introduction. - * Within the Multi-File-Manager selection dialog indivdual files can be manually excluded by selecting the name of the file and pressing *remove selected file*. ->>>>>>> branch 'master' of https://github.com/sRassmann/canny3d-thresholder + * Within the Multi-File-Manager selection dialog individual files can be manually excluded by selecting the name of the file and pressing *remove selected file*. * Press '*start processing*' to confirm the set of selected files. 4. A progress bar will appear to inform over the progress in processing. Note: During processing some windows might pop up for some microseconds and the log dialog from ImageJ will appear. Unfortunatly, this happens within the used plugins, so for now there is no way to turn it off. ### output If single channel images are processed the thresholded images are saved with suffix '*_canny3d.tif*' as a 16 bit grayscale image. -If multi channel images are processed the separated channels are stored with the suffix '*_C*<*channel index*>*.tif*' . The thresholded images are stored as '*_C*<*channel index*>*_canny3d.tif*'. +If multi channel images are processed the separated channels are stored with the suffix '*_C*<*channel index*>*.tif*'. The thresholded images are stored as '*_C*<*channel index*>*_canny3d.tif*'. -Jointly with the files a log file (suffix '*_C*<*channel index*>*_canny3d_log.txt*') is saved. +A log file (suffix '*_C*<*channel index*>*_canny3d_log.txt*') is saved jointly with the output files. diff --git a/pom.xml b/pom.xml index 5360d56..33a45e6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ 17.0.0 - 0.1.0 + 0.2.0 https://github.com/sRassmann/canny3d-thresholder 2020 diff --git a/src/main/java/canny3d_thresholder/Main.java b/src/main/java/canny3d_thresholder/Main.java index 040703a..adc5045 100644 --- a/src/main/java/canny3d_thresholder/Main.java +++ b/src/main/java/canny3d_thresholder/Main.java @@ -9,7 +9,7 @@ public class Main implements PlugIn { static String pluginName = "Canny 3D Thresholder"; - static String pluginVersion = "0.1.0"; + static String pluginVersion = "0.2.0"; ProgressDialog progressDialog; boolean processingDone = false; boolean continueProcessing = true; diff --git a/src/main/java/canny3d_thresholder/ProcessSettings.java b/src/main/java/canny3d_thresholder/ProcessSettings.java index 6e53123..c6c7bd5 100644 --- a/src/main/java/canny3d_thresholder/ProcessSettings.java +++ b/src/main/java/canny3d_thresholder/ProcessSettings.java @@ -104,7 +104,7 @@ public static ProcessSettings initByGD(String pluginName, String pluginVersion) ProcessSettings.pluginVersion = pluginVersion; GenericDialog gd = new GenericDialog(pluginName + " - Image Processing Settings"); - gd.addMessage(pluginName + " - Version " + pluginVersion + " (© 2020 Sebastian Rassmann)", + gd.addMessage(pluginName + " - Version " + pluginVersion + " (© 2020 SR)", new Font("Sansserif", Font.BOLD, 14)); gd.addMessage("Insert Processing settings", new Font("Sansserif", Font.PLAIN, 14)); diff --git a/src/main/java/canny3d_thresholder/Processing.java b/src/main/java/canny3d_thresholder/Processing.java index d0fcd52..e76afce 100644 --- a/src/main/java/canny3d_thresholder/Processing.java +++ b/src/main/java/canny3d_thresholder/Processing.java @@ -6,6 +6,7 @@ import ij.IJ; import ij.ImagePlus; +import ij.plugin.Duplicator; import ij.text.TextPanel; public class Processing { @@ -17,8 +18,8 @@ public class Processing { * @param name name of the image - path/image should be exact path of the * image to be processed * @param outputDir Path to dir where the output should be saved - * @param pS ProcessSettings of the task - * @param pD Reference to ProgressDialog + * @param pS ProcessSettings of the task + * @param pD Reference to ProgressDialog * @return */ @@ -33,18 +34,52 @@ static boolean doProcessing(String path, String name, String outputDir, ProcessS imp.hide(); ImagePlus thrChannel = splitAndSaveChannels(imp, pS, name, outputDir); // does nothing if plain // thresholding channel is input + double[][] thresholds = new double[thrChannel.getNFrames()][2]; + for (int f = 1; f <= thrChannel.getNFrames(); f++) { + thresholds[f - 1] = processSlice(pD, f, imp.getNFrames(), thrChannel, pS); + } - pD.updateBarText("performing edge detection"); - IJ.run(thrChannel, "Gaussian Blur...", "sigma=" + pS.gaussSigma + " stack"); - IJ.run(thrChannel, "3D Edge and Symmetry Filter", + IJ.save(thrChannel, + outputDir + System.getProperty("file.separator") + ProcessSettings.removeFileSuffix(name) + + ((pS.selectedInputFormat == ProcessSettings.INPUTFORMATS[0]) ? "" + : ("_" + ProcessSettings.CHANNELSUFFIX + (pS.thrChannel + 1))) + + ("_canny3d.tif")); + thrChannel.changes = false; + thrChannel.close(); + saveLogFile(path, name, outputDir, pS, thresholds); // TODO modify log file + return true; + } + + /** + * performs the image processing logic (modified Canny object detection) on each + * frame, returns the obtained high and low threshold value + * + * @param pD + * @param currentFrame + * @param nFrames + * @param thrChannel + * @param pS + * @return + */ + + private static double[] processSlice(ProgressDialog pD, int currentFrame, int nFrames, ImagePlus thrChannel, + ProcessSettings pS) { + pD.updateBarText("Frame " + currentFrame + "/" + nFrames + " - performing edge detection"); + ImagePlus slice = new Duplicator().run(thrChannel, 1, 1, 1, thrChannel.getNSlices(), currentFrame, + currentFrame); + IJ.run(slice, "Gaussian Blur...", "sigma=" + pS.gaussSigma + " stack"); + IJ.run(slice, "3D Edge and Symmetry Filter", "alpha=" + pS.cannyAlpha + " radius=10 normalization=10 scaling=2 improved"); + slice.changes = false; + slice.close(); ImagePlus edges = IJ.getImage(); thrChannel.changes = false; thrChannel.hide(); edges.hide(); - pD.updateBarText("thresholding image"); + pD.updateBarText("Frame " + currentFrame + "/" + nFrames + " - thresholding image"); + double[] thresholds = new double[2]; double lowThr, highThr; if (pS.lowThrAlgorithm == "Custom") { lowThr = pS.lowThr; // use custom value @@ -58,42 +93,41 @@ static boolean doProcessing(String path, String name, String outputDir, ProcessS IJ.setAutoThreshold(edges, pS.lowThrAlgorithm + " dark stack"); highThr = edges.getProcessor().getMinThreshold(); // calculate from stack } + thresholds[0] = lowThr; + thresholds[1] = highThr; IJ.run(edges, "3D Hysteresis Thresholding", "high=" + highThr + " low=" + lowThr); - ImagePlus bin = IJ.getImage(); + ImagePlus mask = IJ.getImage(); edges.changes = false; edges.close(); - bin.hide(); + mask.hide(); - IJ.run(bin, "8-bit", ""); - IJ.run(bin, "3D Fill Holes", ""); - IJ.run(bin, "16-bit", ""); + IJ.run(mask, "8-bit", ""); + IJ.run(mask, "3D Fill Holes", ""); - rewriteOriginalIntensities(bin, thrChannel); - thrChannel.close(); - IJ.save(bin, - outputDir + System.getProperty("file.separator") + ProcessSettings.removeFileSuffix(name) - + ((pS.selectedInputFormat == ProcessSettings.INPUTFORMATS[0]) ? "" - : ("_" + ProcessSettings.CHANNELSUFFIX + (pS.thrChannel + 1))) - + ("_canny3d.tif")); - bin.changes = false; - bin.close(); - saveLogFile(path, name, outputDir, pS, lowThr, highThr); - return true; + rewriteOriginalIntensities(mask, thrChannel, currentFrame); + + mask.changes = false; + mask.close(); + return thresholds; } - private static void rewriteOriginalIntensities(ImagePlus bin, ImagePlus thrChannel) { - int t, z, x, y, stackindex; - for (t = 0; t < bin.getNFrames(); t++) { - for (z = 0; z < bin.getNSlices(); z++) { - stackindex = bin.getStackIndex(1, z + 1, t + 1) - 1; // handling IJs indexing - bin.setSliceWithoutUpdate(stackindex); - thrChannel.setSliceWithoutUpdate(stackindex); - for (y = 0; y < bin.getHeight(); y++) { - for (x = 0; x < bin.getWidth(); x++) { - if (bin.getProcessor().get(x, y) != 0) { - bin.getProcessor().set(x, y, thrChannel.getProcessor().get(x, y)); - } + /** + * @param mask binarised image - intensities > 0 are considered positive + * @param thrChannel + * @param frameIndex index in IJ's indexing convention (1 based) + */ + private static void rewriteOriginalIntensities(ImagePlus mask, ImagePlus thrChannel, int frameIndex) { + int z, x, y, stackindexThr, stackindexMask; + for (z = 1; z <= mask.getNSlices(); z++) { + stackindexThr = thrChannel.getStackIndex(1, z, frameIndex) - 1; // handling IJs indexing + stackindexMask = mask.getStackIndex(1, z, 1) - 1; + thrChannel.setSliceWithoutUpdate(stackindexThr); + mask.setSliceWithoutUpdate(stackindexMask); + for (y = 0; y < mask.getHeight(); y++) { + for (x = 0; x < mask.getWidth(); x++) { + if (mask.getStack().getVoxel(x, y, stackindexMask) == 0) { + thrChannel.getStack().setVoxel(x, y, stackindexThr, 0); } } } @@ -132,23 +166,25 @@ private static ImagePlus splitAndSaveChannels(ImagePlus imp, ProcessSettings pS, imp.close(); return thrChannel; } - - private static void saveLogFile(String path, String name, String outputDir, ProcessSettings pS, double lowThr, double highThr) { + private static void saveLogFile(String path, String name, String outputDir, ProcessSettings pS, + double[][] thresholds) { TextPanel sb = new TextPanel(); sb.append("Log File for: " + path + name); sb.append("\nprocessed on " + new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss").format(new Date()) + "\n"); sb.append(pS.toString()); - - sb.append("\nLow Threshold Value (calculated): " + lowThr); - sb.append("High Threshold Value (calculated): " + highThr); - + + sb.append("\nFrame # Low Threshold High Threshold"); + for (int i = 0; i < thresholds.length; i -= -1) { + sb.append("\n" + (i+1) + " " + thresholds[i][0] + " " + thresholds[i][1]); + } + sb.saveAs(outputDir + System.getProperty("file.separator") + ProcessSettings.removeFileSuffix(name) - + ((pS.selectedInputFormat == ProcessSettings.INPUTFORMATS[0]) ? "" - : ("_" + ProcessSettings.CHANNELSUFFIX + (pS.thrChannel + 1))) - + ("_canny3d_log.txt")); - + + ((pS.selectedInputFormat == ProcessSettings.INPUTFORMATS[0]) ? "" + : ("_" + ProcessSettings.CHANNELSUFFIX + (pS.thrChannel + 1))) + + ("_canny3d_log.txt")); + } } diff --git a/src/main/resources/plugins.config b/src/main/resources/plugins.config index ffae265..e28a2d4 100644 --- a/src/main/resources/plugins.config +++ b/src/main/resources/plugins.config @@ -22,8 +22,8 @@ # Name: canny3d_thresholder # Author: Sebastian Rassmann -# Version: 0.1.0 -# Date: 2020/04/29 +# Version: 0.2.0 +# Date: 2020/08/18 # Requires: ImageJ Plugins>SR, "Canny 3D Thresholder", canny3d_thresholder.Main \ No newline at end of file