From 17bb867a399fd42ba671177f44bc7bcb7ed62024 Mon Sep 17 00:00:00 2001 From: mdoube Date: Thu, 19 Feb 2026 14:00:59 +0100 Subject: [PATCH 01/10] Use ROIs from the IJ1 ROI Manager --- .../ElementFractionWrapper.java | 167 +++++++++++++----- 1 file changed, 125 insertions(+), 42 deletions(-) diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index 2928d930..845788a0 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -30,7 +30,6 @@ package org.bonej.wrapperPlugins; -import static java.util.stream.Collectors.toList; import static org.bonej.wrapperPlugins.CommonMessages.NOT_BINARY; import static org.bonej.wrapperPlugins.CommonMessages.NO_IMAGE_OPEN; import static org.bonej.wrapperPlugins.CommonMessages.WEIRD_SPATIAL; @@ -41,26 +40,29 @@ import net.imagej.axis.AxisType; import net.imagej.ops.OpService; import net.imagej.units.UnitService; -import net.imglib2.IterableInterval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.type.NativeType; -import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.RealType; import net.imglib2.view.Views; + import org.bonej.utilities.AxisUtils; import org.bonej.utilities.ElementUtil; import org.bonej.utilities.SharedTable; -import org.bonej.wrapperPlugins.wrapperUtils.Common; -import org.bonej.wrapperPlugins.wrapperUtils.HyperstackUtils; -import org.bonej.wrapperPlugins.wrapperUtils.HyperstackUtils.Subspace; import org.bonej.wrapperPlugins.wrapperUtils.ResultUtils; import org.scijava.app.StatusService; import org.scijava.command.Command; import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; +import ij.ImagePlus; +import ij.gui.Roi; +import ij.gui.ShapeRoi; +import ij.plugin.frame.RoiManager; + +import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; /** * This command estimates the size of the given sample by counting its @@ -70,6 +72,7 @@ * results table. Results are shown in calibrated units, if possible. * * @author Richard Domander + * @author Michael Doube */ @Plugin(type = Command.class, menuPath = "Plugins>BoneJ>Fraction>Area/Volume fraction") @@ -84,6 +87,8 @@ public class ElementFractionWrapper & NativeType> exten private UnitService unitService; @Parameter private StatusService statusService; + @Parameter + private RoiManager roiManager; /** Header of the foreground (bone) volume column in the results table */ private String boneSizeHeader; @@ -97,41 +102,85 @@ public class ElementFractionWrapper & NativeType> exten @Override public void run() { statusService.showStatus("Element fraction: initializing"); - findSubspaces(inputImage); +// findSubspaces(inputImage); prepareResultDisplay(); final String name = inputImage.getName(); - for (int i = 0; i < subspaces.size(); i++) { - final Subspace subspace = subspaces.get(i); - statusService.showStatus("Element fraction: calculating subspace #" + (i + - 1)); - statusService.showProgress(i, subspaces.size()); - // The value of each foreground element in a bit type image is 1, so we - // can count their number just by summing - final IterableInterval interval = Views.flatIterable( - subspace.interval); - final double foregroundSize = opService.stats().sum(interval) - .getRealDouble() * elementSize; - final double totalSize = interval.size() * elementSize; - final double ratio = foregroundSize / totalSize; - final String suffix = subspace.toString(); - final String label = suffix.isEmpty() ? name : name + " " + suffix; - addResults(label, foregroundSize, totalSize, ratio); - } - resultsTable = SharedTable.getTable(); - } + + //get the number of slices, channels and time points to iterate over + int xIdx = axisIndex(inputImage, Axes.X); + int yIdx = axisIndex(inputImage, Axes.Y); + int zIdx = axisIndex(inputImage, Axes.Z); + int tIdx = axisIndex(inputImage, Axes.TIME); + int cIdx = axisIndex(inputImage, Axes.CHANNEL); - private void findSubspaces(final ImgPlus inputImage) { - if (AxisUtils.countSpatialDimensions(inputImage) == 3) { - subspaces = find3DSubspaces(inputImage); - } else { - subspaces = find2DSubspaces(inputImage); - } - } + //if an axis is missing, set its size to 1, otherwise get its size. + int w = (int) inputImage.dimension(xIdx); + int h = (int) inputImage.dimension(yIdx); + int cSize = (cIdx >= 0) ? (int) inputImage.dimension(cIdx) : 1; + int zSize = (zIdx >= 0) ? (int) inputImage.dimension(zIdx) : 1; + int tSize = (tIdx >= 0) ? (int) inputImage.dimension(tIdx) : 1; + + //check if there are any ROIs in the ROI manager and if not, ignore ROIs + final boolean limitToRoi = roiManager.getCount() > 0; + + //histogram accumulator + long[] histogramSum = new long[256]; + + //default roi for the slice + final Roi wholeSliceRoi = new Roi(0, 0, w, h); + + //iterate over all the slices, and for each slice do the channels and time points too. + for (int z = 0; z < zSize; z++) { + + for (int t = 0; t < tSize; t++) { + //use an ROI covering the whole slice if no ROI has been defined in the ROI Manager + ShapeRoi sliceRoi = new ShapeRoi(wholeSliceRoi); + //get the ROIs for this slice from the ROI manager as a unioned mask + if (limitToRoi) { + sliceRoi = getUnionRoi(z, t); + //if no rois and limitToRoi is true, continue + if (sliceRoi == null) continue; + } + + for (int c = 0; c < cSize; c++) { + //create a 2D view into the data + RandomAccessibleInterval xyView = inputImage; + if (cIdx >= 0) xyView = Views.hyperSlice(xyView, cIdx, c); + if (zIdx >= 0) xyView = Views.hyperSlice(xyView, zIdx, z); + if (tIdx >= 0) xyView = Views.hyperSlice(xyView, tIdx, t); + + //access the pixels of the xyView using an IJ1 ImagePlus + ImagePlus imp = ImageJFunctions.wrap(xyView, "XY View"); + imp.setRoi(sliceRoi); + int[] histogram = imp.getProcessor().getHistogram(); + + //don't run on non-8-bit images (using IJ1 binary convention of 0 & 255) + if (histogram.length != 256) + cancelMacroSafe(this, NOT_BINARY); + + + //accumulate the histogram into the accumulator + for (int i = 0; i < 256; i++) { + //detect any non-binary pixels (using the IJ1 convention of 0 & 255) + if (i > 0 && i < 255 && histogram[i] > 0) + cancelMacroSafe(this, NOT_BINARY); - private List> find2DSubspaces(final ImgPlus inputImage) { - final List axisTypes = Stream.of(Axes.X, Axes.Y).collect(toList()); - final ImgPlus bitImgPlus = Common.toBitTypeImgPlus(opService, inputImage); - return HyperstackUtils.splitSubspaces(bitImgPlus, axisTypes).collect(toList()); + histogramSum[i] += histogram[i]; + } + } + } + } + + //summarise the result + long tvPix = histogramSum[0] + histogramSum[255]; + long bvPix = histogramSum[255]; + double pixelSize = ElementUtil.calibratedSpatialElementSize(inputImage, unitService); + + double bv = bvPix * pixelSize; + double tv = tvPix * pixelSize; + addResults(name, bv, tv, bv/tv); + + resultsTable = SharedTable.getTable(); } private void addResults(final String label, final double foregroundSize, @@ -164,14 +213,48 @@ private void validateImage() { return; } - if (!ElementUtil.isBinary(inputImage)) { - cancelMacroSafe(this, NOT_BINARY); - } - final long spatialDimensions = AxisUtils.countSpatialDimensions(inputImage); if (spatialDimensions < 2 || spatialDimensions > 3) { cancelMacroSafe(this, WEIRD_SPATIAL); } } + + /** Return the index of the given axis in the ImgPlus, or -1 if absent. */ + private static int axisIndex(ImgPlus img, AxisType axis) { + for (int d = 0; d < img.numDimensions(); d++) { + if (img.axis(d).type().equals(axis)) return d; + } + return -1; + } + + /** + * Get the union of all the ROIs on this slice for this timepoint + * + * If ROIs don't have z or time position, they are applied to all z or time positions + * + * @param z the 0-indexed z coordinate (NOT the 1-indexed slice) + * @param t the 0-indexed time coordinate (NOT the 1-indexed timepoint) + * @return the union of all the ROIs, or null if there are none for this z and t coordinate. + */ + ShapeRoi getUnionRoi(int z, int t) { + Roi[] allRois = roiManager.getRoisAsArray(); + List sliceRois = new ArrayList<>(); + for (Roi roi : allRois) { + if ((roi.getZPosition() == z + 1 || roi.getZPosition() == 0) && + //keep the ROI if it is for this timepoint or doesn't have a timepoint + (roi.getTPosition() == t + 1 || roi.getTPosition() == 0)) { + ShapeRoi sr = (roi instanceof ShapeRoi) ? (ShapeRoi) roi : new ShapeRoi(roi); + sliceRois.add(sr); + } + } + if (sliceRois.size() == 0) return null; + ShapeRoi sliceRoi = sliceRois.get(0); + for (int i = 1; i < sliceRois.size(); i++) { + + sliceRoi.or(sliceRois.get(i)); + } + return sliceRoi; + } + // endregion } From 6a3b8782bd8a76ef1ff27bfe2560592204c28d00 Mon Sep 17 00:00:00 2001 From: mdoube Date: Thu, 19 Feb 2026 21:25:59 +0100 Subject: [PATCH 02/10] Handle out of order axes and parallelise z-iteration --- .../ElementFractionWrapper.java | 175 +++++++++++++----- 1 file changed, 125 insertions(+), 50 deletions(-) diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index 845788a0..b9bd36a6 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -62,6 +62,8 @@ import ij.plugin.frame.RoiManager; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; /** @@ -102,7 +104,6 @@ public class ElementFractionWrapper & NativeType> exten @Override public void run() { statusService.showStatus("Element fraction: initializing"); -// findSubspaces(inputImage); prepareResultDisplay(); final String name = inputImage.getName(); @@ -112,74 +113,113 @@ public void run() { int zIdx = axisIndex(inputImage, Axes.Z); int tIdx = axisIndex(inputImage, Axes.TIME); int cIdx = axisIndex(inputImage, Axes.CHANNEL); - + System.out.println("axisIndex (w,h,d,t,c) = ("+xIdx+", "+yIdx+", "+zIdx+", "+tIdx+", "+cIdx+")"); + //if an axis is missing, set its size to 1, otherwise get its size. int w = (int) inputImage.dimension(xIdx); int h = (int) inputImage.dimension(yIdx); int cSize = (cIdx >= 0) ? (int) inputImage.dimension(cIdx) : 1; int zSize = (zIdx >= 0) ? (int) inputImage.dimension(zIdx) : 1; int tSize = (tIdx >= 0) ? (int) inputImage.dimension(tIdx) : 1; + System.out.println("(w,h,d,t,c) = ("+w+", "+h+", "+zSize+", "+tSize+", "+cSize+")"); //check if there are any ROIs in the ROI manager and if not, ignore ROIs final boolean limitToRoi = roiManager.getCount() > 0; - //histogram accumulator - long[] histogramSum = new long[256]; - //default roi for the slice final Roi wholeSliceRoi = new Roi(0, 0, w, h); - //iterate over all the slices, and for each slice do the channels and time points too. - for (int z = 0; z < zSize; z++) { - - for (int t = 0; t < tSize; t++) { - //use an ROI covering the whole slice if no ROI has been defined in the ROI Manager - ShapeRoi sliceRoi = new ShapeRoi(wholeSliceRoi); - //get the ROIs for this slice from the ROI manager as a unioned mask - if (limitToRoi) { - sliceRoi = getUnionRoi(z, t); - //if no rois and limitToRoi is true, continue - if (sliceRoi == null) continue; - } - - for (int c = 0; c < cSize; c++) { + //iterate over all the timpoints and channels, and for each iterate over z. + for (int t = 0; t < tSize; t++) { + final int time = t; + for (int c = 0; c < cSize; c++) { + final int channel = c; + statusService.showStatus("Element fraction: channel "+c+", time "+t); + + //histogram accumulator, to sum over all slices in a (t, c) +// long[] histogramSum = new long[256]; + + //per-thread counters of 0 and 255 + long[] fgCount = new long[zSize]; + long[] bgCount = new long[zSize]; + + ArrayList sliceNumbers = new ArrayList<>(); + for (int z = 0; z < zSize; z++) { + sliceNumbers.add(z); + } + sliceNumbers.parallelStream().forEach(z -> { +// for (int z = 0; z < zSize; z++) { + //use an ROI covering the whole slice if no ROI has been defined in the ROI Manager + ShapeRoi sliceRoi = new ShapeRoi(wholeSliceRoi); + //get the ROIs for this slice from the ROI manager as a unioned mask + if (limitToRoi) { + sliceRoi = getUnionRoi(z, time); + //if no rois and limitToRoi is true stop this thread + if (sliceRoi == null) return; + } + //create a 2D view into the data - RandomAccessibleInterval xyView = inputImage; - if (cIdx >= 0) xyView = Views.hyperSlice(xyView, cIdx, c); - if (zIdx >= 0) xyView = Views.hyperSlice(xyView, zIdx, z); - if (tIdx >= 0) xyView = Views.hyperSlice(xyView, tIdx, t); - - //access the pixels of the xyView using an IJ1 ImagePlus - ImagePlus imp = ImageJFunctions.wrap(xyView, "XY View"); - imp.setRoi(sliceRoi); - int[] histogram = imp.getProcessor().getHistogram(); - - //don't run on non-8-bit images (using IJ1 binary convention of 0 & 255) - if (histogram.length != 256) - cancelMacroSafe(this, NOT_BINARY); +// RandomAccessibleInterval xyView = inputImage; + RandomAccessibleInterval xyView = get2DSlice(inputImage, z, time, channel); + +// if (cIdx >= 0) xyView = Views.hyperSlice(xyView, cIdx, c); +// if (zIdx >= 0) xyView = Views.hyperSlice(xyView, zIdx, z); +// if (tIdx >= 0) xyView = Views.hyperSlice(xyView, tIdx, t); - - //accumulate the histogram into the accumulator - for (int i = 0; i < 256; i++) { - //detect any non-binary pixels (using the IJ1 convention of 0 & 255) - if (i > 0 && i < 255 && histogram[i] > 0) - cancelMacroSafe(this, NOT_BINARY); + //access the pixels of the xyView using an IJ1 ImagePlus + ImagePlus imp = ImageJFunctions.wrap(xyView, "XY View"); + imp.setRoi(sliceRoi); + int[] histogram = imp.getProcessor().getHistogram(); - histogramSum[i] += histogram[i]; - } + //don't run on non-8-bit images (using IJ1 binary convention of 0 & 255) + if (histogram.length != 256) + cancelMacroSafe(this, NOT_BINARY); + + //accumulate the histogram into the accumulator +// for (int i = 0; i < 256; i++) { +// //detect any non-binary pixels (using the IJ1 convention of 0 & 255) +// if (i > 0 && i < 255 && histogram[i] > 0) +// cancelMacroSafe(this, NOT_BINARY); +// +// histogramSum[i] += histogram[i]; +// } + + //check binary-ness as we go + for (int i = 1; i < 255; i++) + if (histogram[i] > 0) + cancelMacroSafe(this, NOT_BINARY); + + //write the counts to a per-slice accumulator array + fgCount[z] = histogram[255]; + bgCount[z] = histogram[0]; + }); + //after summing over all slices, add the result to the table + //summarise the result + + long fg = 0; + long bg = 0; + for (int z = 0; z < zSize; z++) { + fg += fgCount[z]; + bg += bgCount[z]; } + + double tv = (fg + bg) * elementSize; + double bv = fg * elementSize; + + String label = name; + + //add a channel suffix if there is more than one channel + label = cSize > 1 ? label + " Channel: " + c : label; + //add a comma between channel and timepoint if both are more than one + label = (cSize > 1 && tSize > 1) ? label +"," : label; + //add a time suffix if there is more than one timepoint + label = tSize > 1 ? label + " Time: " + t : label; + + addResults(label, bv, tv, bv/tv); + } } - //summarise the result - long tvPix = histogramSum[0] + histogramSum[255]; - long bvPix = histogramSum[255]; - double pixelSize = ElementUtil.calibratedSpatialElementSize(inputImage, unitService); - - double bv = bvPix * pixelSize; - double tv = tvPix * pixelSize; - addResults(name, bv, tv, bv/tv); - resultsTable = SharedTable.getTable(); } @@ -256,5 +296,40 @@ ShapeRoi getUnionRoi(int z, int t) { return sliceRoi; } - // endregion + public static RandomAccessibleInterval get2DSlice(ImgPlus img, int z, int t, int c) { + // Get the indices of Z, TIME, and CHANNEL + int zIdx = axisIndex(img, Axes.Z); + int tIdx = axisIndex(img, Axes.TIME); + int cIdx = axisIndex(img, Axes.CHANNEL); + + // Collect all non-spatial dimensions (Z, TIME, CHANNEL) and their target slice indices + List dimensionsToSlice = new ArrayList<>(); + if (zIdx >= 0 && img.dimension(zIdx) > 1) dimensionsToSlice.add(new DimensionSlice(zIdx, z)); + if (tIdx >= 0 && img.dimension(tIdx) > 1) dimensionsToSlice.add(new DimensionSlice(tIdx, t)); + if (cIdx >= 0 && img.dimension(cIdx) > 1) dimensionsToSlice.add(new DimensionSlice(cIdx, c)); + + // Sort dimensions by index in descending order + Collections.sort(dimensionsToSlice, Comparator.comparingInt(ds -> -ds.index)); + + // Slice along dimensions in descending order of their indices + RandomAccessibleInterval view = img; + for (DimensionSlice ds : dimensionsToSlice) { + view = Views.hyperSlice(view, ds.index, ds.slice); + } + + // The result is now a 2D XY slice + return view; + } + + + // Helper class to store dimension index and slice index + private static class DimensionSlice { + int index; + int slice; + + DimensionSlice(int index, int slice) { + this.index = index; + this.slice = slice; + } + } } From 178ed952e8c539a3d4114caa529f9d455b6da465 Mon Sep 17 00:00:00 2001 From: mdoube Date: Fri, 20 Feb 2026 11:04:39 +0100 Subject: [PATCH 03/10] Working version but some points to improve: Very slow performance on hyperstacks (cycling IJ1->IJ2->IJ1) Don't pop up ROI manager, and give option to load a file @Parameter is sticky on the input and doesn't detect change to type --- .../ElementFractionWrapper.java | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index b9bd36a6..e237bd45 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -44,6 +44,7 @@ import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.view.Views; @@ -113,7 +114,7 @@ public void run() { int zIdx = axisIndex(inputImage, Axes.Z); int tIdx = axisIndex(inputImage, Axes.TIME); int cIdx = axisIndex(inputImage, Axes.CHANNEL); - System.out.println("axisIndex (w,h,d,t,c) = ("+xIdx+", "+yIdx+", "+zIdx+", "+tIdx+", "+cIdx+")"); +// System.out.println("axisIndex (w,h,d,t,c) = ("+xIdx+", "+yIdx+", "+zIdx+", "+tIdx+", "+cIdx+")"); //if an axis is missing, set its size to 1, otherwise get its size. int w = (int) inputImage.dimension(xIdx); @@ -121,11 +122,11 @@ public void run() { int cSize = (cIdx >= 0) ? (int) inputImage.dimension(cIdx) : 1; int zSize = (zIdx >= 0) ? (int) inputImage.dimension(zIdx) : 1; int tSize = (tIdx >= 0) ? (int) inputImage.dimension(tIdx) : 1; - System.out.println("(w,h,d,t,c) = ("+w+", "+h+", "+zSize+", "+tSize+", "+cSize+")"); +// System.out.println("(w,h,d,t,c) = ("+w+", "+h+", "+zSize+", "+tSize+", "+cSize+")"); //check if there are any ROIs in the ROI manager and if not, ignore ROIs final boolean limitToRoi = roiManager.getCount() > 0; - + //default roi for the slice final Roi wholeSliceRoi = new Roi(0, 0, w, h); @@ -135,10 +136,7 @@ public void run() { for (int c = 0; c < cSize; c++) { final int channel = c; statusService.showStatus("Element fraction: channel "+c+", time "+t); - - //histogram accumulator, to sum over all slices in a (t, c) -// long[] histogramSum = new long[256]; - + //per-thread counters of 0 and 255 long[] fgCount = new long[zSize]; long[] bgCount = new long[zSize]; @@ -148,46 +146,32 @@ public void run() { sliceNumbers.add(z); } sliceNumbers.parallelStream().forEach(z -> { + if (this.isCanceled()) return; // for (int z = 0; z < zSize; z++) { //use an ROI covering the whole slice if no ROI has been defined in the ROI Manager ShapeRoi sliceRoi = new ShapeRoi(wholeSliceRoi); //get the ROIs for this slice from the ROI manager as a unioned mask if (limitToRoi) { - sliceRoi = getUnionRoi(z, time); + sliceRoi = getUnionRoi(z, time, channel); //if no rois and limitToRoi is true stop this thread if (sliceRoi == null) return; } //create a 2D view into the data -// RandomAccessibleInterval xyView = inputImage; RandomAccessibleInterval xyView = get2DSlice(inputImage, z, time, channel); -// if (cIdx >= 0) xyView = Views.hyperSlice(xyView, cIdx, c); -// if (zIdx >= 0) xyView = Views.hyperSlice(xyView, zIdx, z); -// if (tIdx >= 0) xyView = Views.hyperSlice(xyView, tIdx, t); - //access the pixels of the xyView using an IJ1 ImagePlus ImagePlus imp = ImageJFunctions.wrap(xyView, "XY View"); imp.setRoi(sliceRoi); int[] histogram = imp.getProcessor().getHistogram(); - //don't run on non-8-bit images (using IJ1 binary convention of 0 & 255) - if (histogram.length != 256) - cancelMacroSafe(this, NOT_BINARY); - - //accumulate the histogram into the accumulator -// for (int i = 0; i < 256; i++) { -// //detect any non-binary pixels (using the IJ1 convention of 0 & 255) -// if (i > 0 && i < 255 && histogram[i] > 0) -// cancelMacroSafe(this, NOT_BINARY); -// -// histogramSum[i] += histogram[i]; -// } - //check binary-ness as we go for (int i = 1; i < 255; i++) - if (histogram[i] > 0) + if (histogram[i] > 0) { + System.out.println("Cancelled non-binary on slice histogram check"); cancelMacroSafe(this, NOT_BINARY); + return; + } //write the counts to a per-slice accumulator array fgCount[z] = histogram[255]; @@ -195,7 +179,10 @@ public void run() { }); //after summing over all slices, add the result to the table //summarise the result - + + //don't show any results for cancelled plugins + if (this.isCanceled()) return; + long fg = 0; long bg = 0; for (int z = 0; z < zSize; z++) { @@ -256,6 +243,19 @@ private void validateImage() { final long spatialDimensions = AxisUtils.countSpatialDimensions(inputImage); if (spatialDimensions < 2 || spatialDimensions > 3) { cancelMacroSafe(this, WEIRD_SPATIAL); + inputImage = null; + return; + } +// + T type = inputImage.firstElement(); + //enforce 8-bit (IJ1 binary is 0 and 255) + if (type instanceof UnsignedByteType) + return; + else { + System.out.println("Cancelled non-binary at initial validation"); + cancelMacroSafe(this, NOT_BINARY); + inputImage = null; + return; } } @@ -268,21 +268,25 @@ private static int axisIndex(ImgPlus img, AxisType axis) { } /** - * Get the union of all the ROIs on this slice for this timepoint + * Get the union of all the ROIs on this slice for this timepoint and channel * - * If ROIs don't have z or time position, they are applied to all z or time positions + * If ROIs don't have z, time or channel position, they are applied to all z or time or channel positions * * @param z the 0-indexed z coordinate (NOT the 1-indexed slice) * @param t the 0-indexed time coordinate (NOT the 1-indexed timepoint) - * @return the union of all the ROIs, or null if there are none for this z and t coordinate. + * @param c the 0-indexed channel coordinate (NOT the 1-indexed channel) + * @return the union of all the ROIs, or null if there are none for this z, t and c coordinate. */ - ShapeRoi getUnionRoi(int z, int t) { + ShapeRoi getUnionRoi(int z, int t, int c) { Roi[] allRois = roiManager.getRoisAsArray(); List sliceRois = new ArrayList<>(); for (Roi roi : allRois) { + //keep the ROI if if is for this slice or doesn't have a slice AND if ((roi.getZPosition() == z + 1 || roi.getZPosition() == 0) && - //keep the ROI if it is for this timepoint or doesn't have a timepoint - (roi.getTPosition() == t + 1 || roi.getTPosition() == 0)) { + //keep the ROI if it is for this timepoint or doesn't have a timepoint AND + (roi.getTPosition() == t + 1 || roi.getTPosition() == 0) && + //keep the ROI if it is for this channel or doesn't have a channel + (roi.getCPosition() == c + 1 || roi.getCPosition() == 0)) { ShapeRoi sr = (roi instanceof ShapeRoi) ? (ShapeRoi) roi : new ShapeRoi(roi); sliceRois.add(sr); } @@ -290,7 +294,6 @@ ShapeRoi getUnionRoi(int z, int t) { if (sliceRois.size() == 0) return null; ShapeRoi sliceRoi = sliceRois.get(0); for (int i = 1; i < sliceRois.size(); i++) { - sliceRoi.or(sliceRois.get(i)); } return sliceRoi; @@ -320,7 +323,6 @@ public static RandomAccessibleInterval get2DSlice(ImgPlus img, int z, // The result is now a 2D XY slice return view; } - // Helper class to store dimension index and slice index private static class DimensionSlice { From a06c212c3019ca3bec730262ee4bdb770b4ac679 Mon Sep 17 00:00:00 2001 From: mdoube Date: Fri, 20 Feb 2026 11:28:20 +0100 Subject: [PATCH 04/10] Don't @Parameter the ROIManager. This stops it from popping up on command execution. --- .../org/bonej/wrapperPlugins/ElementFractionWrapper.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index e237bd45..0547b9a0 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -90,8 +90,6 @@ public class ElementFractionWrapper & NativeType> exten private UnitService unitService; @Parameter private StatusService statusService; - @Parameter - private RoiManager roiManager; /** Header of the foreground (bone) volume column in the results table */ private String boneSizeHeader; @@ -101,6 +99,8 @@ public class ElementFractionWrapper & NativeType> exten private String ratioHeader; /** The calibrated size of an element in the image */ private double elementSize; + private RoiManager roiManager; + @Override public void run() { @@ -124,8 +124,10 @@ public void run() { int tSize = (tIdx >= 0) ? (int) inputImage.dimension(tIdx) : 1; // System.out.println("(w,h,d,t,c) = ("+w+", "+h+", "+zSize+", "+tSize+", "+cSize+")"); + roiManager = RoiManager.getInstance2(); + //check if there are any ROIs in the ROI manager and if not, ignore ROIs - final boolean limitToRoi = roiManager.getCount() > 0; + final boolean limitToRoi = (roiManager != null && roiManager.getCount() > 0); //default roi for the slice final Roi wholeSliceRoi = new Roi(0, 0, w, h); From 8982ade6df78df4eb728cf89d74978dc5eafc69b Mon Sep 17 00:00:00 2001 From: mdoube Date: Mon, 2 Mar 2026 16:51:24 +0100 Subject: [PATCH 05/10] Use more IJ2 way to make IJ2 masks from IJ1 ROIs Still need to short-circuit cases when: - No ROI on a slice and ROIs are being handled (skip slice processing) - No ROI in ROI Manager - no need to make masks or check them. --- .../org/bonej/utilities/RoiManagerUtil.java | 170 +++++++++++++++++- Modern/wrapperPlugins/pom.xml | 1 + .../ElementFractionWrapper.java | 139 ++++---------- 3 files changed, 202 insertions(+), 108 deletions(-) diff --git a/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java b/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java index 94bcb0b3..46227a1c 100644 --- a/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java +++ b/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java @@ -2,7 +2,7 @@ * #%L * Utility methods for BoneJ2 * %% - * Copyright (C) 2015 - 2025 Michael Doube, BoneJ developers + * Copyright (C) 2015 - 2026 Michael Doube, BoneJ developers * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: @@ -32,7 +32,17 @@ import ij.gui.Roi; import ij.plugin.frame.RoiManager; +import ij.process.ImageProcessor; +import net.imglib2.Cursor; +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.type.logic.BitType; +import net.imglib2.view.Views; +import java.awt.Rectangle; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -71,10 +81,158 @@ public static boolean isActiveOnAllSlices(final int sliceNumber) { public static List pointROICoordinates(final RoiManager manager) { final Roi[] rois = manager.getRoisAsArray(); return Arrays.stream(rois).filter(roi -> roi.getType() == Roi.POINT).map( - roi -> { - final int z = roi.getZPosition(); - return Arrays.stream(roi.getContainedPoints()).distinct().map( - p -> new Vector3d(p.x, p.y, z)); - }).flatMap(s -> s).distinct().collect(Collectors.toList()); + roi -> { + final int z = roi.getZPosition(); + return Arrays.stream(roi.getContainedPoints()).distinct().map( + p -> new Vector3d(p.x, p.y, z)); + }).flatMap(s -> s).distinct().collect(Collectors.toList()); + } + + /** + * Determine whether the ROI Manager is not active or has no entries + * + * @return true if there are no ROIs to process + */ + public static boolean isEmpty() { + final RoiManager rm = RoiManager.getInstance2(); + //if there are no ROIs or no ROI Manager + return (rm == null || rm.getCount() == 0); + } + + /** + * Build a 2D union mask (BitType) for the given XY view and (z,t,c) plane using ROIs from the + * (visible) IJ1 RoiManager. The returned mask is aligned to the view's interval. + * + * @param xyView 2D view (may have non-zero min). + * @param z1 1-based Z position (IJ1 convention) + * @param t1 1-based T position (IJ1 convention) + * @param c1 1-based C position (IJ1 convention) + */ + public static RandomAccessibleInterval unionMaskFromRoiManager( + final RandomAccessibleInterval xyView, + final int z1, + final int t1, + final int c1, + final boolean treatEmptyRoiManagerAsFull + ) { + final RoiManager rm = RoiManager.getInstance2(); + //if there are no ROIs or no ROI Manager + if (isEmpty()) { + if (treatEmptyRoiManagerAsFull) + //no ROIs so use all image pixels (apply an all-true mask) + return fullMaskLike(xyView); + else + // No ROIs so use no image pixels (apply an all-false mask) + return emptyMaskLike(xyView); + } + + final Roi[] all = rm.getRoisAsArray(); + final List rois = new ArrayList<>(); + for (final Roi r : all) { + if (matchesPlane(r, z1, t1, c1)) rois.add(r); + } + if (rois.isEmpty()) { + return emptyMaskLike(xyView); + } + + // View geometry + final long minX = xyView.min(0); + final long minY = xyView.min(1); + final int w = (int) xyView.dimension(0); + final int h = (int) xyView.dimension(1); + + // Bit mask aligned to xyView interval + final RandomAccessibleInterval mask = + ArrayImgs.bits(w, h); // min at (0,0) for now + final RandomAccessibleInterval alignedMask = + Views.translate(mask, minX, minY); // now mask coords match xyView coords + + // Fill union by OR-ing each ROI's raster mask into alignedMask + for (final Roi roi : rois) { + orRoiIntoMask(roi, alignedMask); + } + + return alignedMask; + } + + private static boolean matchesPlane(final Roi roi, final int z1, final int t1, final int c1) { + final int rz = roi.getZPosition(); + final int rt = roi.getTPosition(); + final int rc = roi.getCPosition(); + return (rz == 0 || rz == z1) && (rt == 0 || rt == t1) && (rc == 0 || rc == c1); + } + + /** + * Create a mask that matches this view in size and position and which has all true (1) values + * + * @param xyView + * @return an all-true mask that matches the given xyView + */ + private static RandomAccessibleInterval fullMaskLike(final RandomAccessibleInterval xyView) { + final int w = (int) xyView.dimension(0); + final int h = (int) xyView.dimension(1); + final long minX = xyView.min(0); + final long minY = xyView.min(1); + final RandomAccessibleInterval m = ArrayImgs.bits(w, h); + final Cursor cursor = Views.flatIterable(m).cursor(); + while (cursor.hasNext()) { + cursor.next().set(true); + } + return Views.translate(m, minX, minY); + } + + private static RandomAccessibleInterval emptyMaskLike(final RandomAccessibleInterval xyView) { + final int w = (int) xyView.dimension(0); + final int h = (int) xyView.dimension(1); + final long minX = xyView.min(0); + final long minY = xyView.min(1); + final RandomAccessibleInterval m = ArrayImgs.bits(w, h); + return Views.translate(m, minX, minY); + } + + /** + * OR a single IJ1 Roi into an ImgLib2 BitType mask. + * The mask must be in the same pixel coordinate system as the Roi (typically image coordinates). + */ + private static void orRoiIntoMask(final Roi roi, final RandomAccessibleInterval mask) { + final Rectangle b = roi.getBounds(); // ROI bounds in image pixel coords + final ImageProcessor ipMask = roi.getMask(); // ROI-local mask (may be null for rectangle ROIs) + + // Intersect bounds with mask interval to stay safe + final long x0 = Math.max(b.x, mask.min(0)); + final long y0 = Math.max(b.y, mask.min(1)); + final long x1 = Math.min(b.x + b.width - 1L, mask.max(0)); + final long y1 = Math.min(b.y + b.height - 1L, mask.max(1)); + if (x1 < x0 || y1 < y0) return; + + if (ipMask == null) { + // Rectangle ROI: set all pixels in intersected bounds + final RandomAccessibleInterval view = Views.interval(mask, new FinalInterval(new long[]{x0, y0}, new long[]{x1, y1})); + for (final BitType bt : Views.flatIterable(view)) bt.setOne(); + return; + } + + // Non-rectangular ROI: use ROI-local mask pixels and copy into global mask with OR semantics. + // ipMask is in ROI-local coordinates: (0..b.width-1, 0..b.height-1) + final ImageProcessor byteMask = ipMask.convertToByte(false); + final byte[] mp = (byte[]) byteMask.getPixels(); + final int mw = byteMask.getWidth(); + + for (long yy = y0; yy <= y1; yy++) { + final int my = (int) (yy - b.y); + final int mRow = my * mw; + + for (long xx = x0; xx <= x1; xx++) { + final int mx = (int) (xx - b.x); + if ((mp[mRow + mx] & 0xff) != 0) { + mask.randomAccess().setPositionAndGet(xx, 0); // can't do two dims at once + // Use a small RA helper for clarity/efficiency: + final RandomAccess ra = mask.randomAccess(); + ra.setPosition(xx, 0); + ra.setPosition(yy, 1); + ra.get().setOne(); + } + } + } } } diff --git a/Modern/wrapperPlugins/pom.xml b/Modern/wrapperPlugins/pom.xml index 81f3ed8e..21d746e8 100644 --- a/Modern/wrapperPlugins/pom.xml +++ b/Modern/wrapperPlugins/pom.xml @@ -119,6 +119,7 @@ org.bonej bonej-utilities + 7.1.10-SNAPSHOT org.bonej diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index 0547b9a0..1462d7cd 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -40,9 +40,10 @@ import net.imagej.axis.AxisType; import net.imagej.ops.OpService; import net.imagej.units.UnitService; +import net.imglib2.Cursor; import net.imglib2.RandomAccessibleInterval; -import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.type.NativeType; +import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.view.Views; @@ -50,6 +51,7 @@ import org.bonej.utilities.AxisUtils; import org.bonej.utilities.ElementUtil; +import org.bonej.utilities.RoiManagerUtil; import org.bonej.utilities.SharedTable; import org.bonej.wrapperPlugins.wrapperUtils.ResultUtils; import org.scijava.app.StatusService; @@ -57,10 +59,7 @@ import org.scijava.plugin.Parameter; import org.scijava.plugin.Plugin; -import ij.ImagePlus; -import ij.gui.Roi; -import ij.gui.ShapeRoi; -import ij.plugin.frame.RoiManager; +//import ij.plugin.frame.RoiManager; import java.util.ArrayList; import java.util.Collections; @@ -99,7 +98,6 @@ public class ElementFractionWrapper & NativeType> exten private String ratioHeader; /** The calibrated size of an element in the image */ private double elementSize; - private RoiManager roiManager; @Override @@ -109,90 +107,59 @@ public void run() { final String name = inputImage.getName(); //get the number of slices, channels and time points to iterate over - int xIdx = axisIndex(inputImage, Axes.X); - int yIdx = axisIndex(inputImage, Axes.Y); int zIdx = axisIndex(inputImage, Axes.Z); int tIdx = axisIndex(inputImage, Axes.TIME); int cIdx = axisIndex(inputImage, Axes.CHANNEL); // System.out.println("axisIndex (w,h,d,t,c) = ("+xIdx+", "+yIdx+", "+zIdx+", "+tIdx+", "+cIdx+")"); //if an axis is missing, set its size to 1, otherwise get its size. - int w = (int) inputImage.dimension(xIdx); - int h = (int) inputImage.dimension(yIdx); int cSize = (cIdx >= 0) ? (int) inputImage.dimension(cIdx) : 1; int zSize = (zIdx >= 0) ? (int) inputImage.dimension(zIdx) : 1; int tSize = (tIdx >= 0) ? (int) inputImage.dimension(tIdx) : 1; -// System.out.println("(w,h,d,t,c) = ("+w+", "+h+", "+zSize+", "+tSize+", "+cSize+")"); - - roiManager = RoiManager.getInstance2(); - - //check if there are any ROIs in the ROI manager and if not, ignore ROIs - final boolean limitToRoi = (roiManager != null && roiManager.getCount() > 0); - - //default roi for the slice - final Roi wholeSliceRoi = new Roi(0, 0, w, h); - - //iterate over all the timpoints and channels, and for each iterate over z. + + long fg = 0; + long total = 0; + //iterate over all the timepoints and channels, and for each iterate over z. for (int t = 0; t < tSize; t++) { final int time = t; for (int c = 0; c < cSize; c++) { final int channel = c; statusService.showStatus("Element fraction: channel "+c+", time "+t); - - //per-thread counters of 0 and 255 - long[] fgCount = new long[zSize]; - long[] bgCount = new long[zSize]; - ArrayList sliceNumbers = new ArrayList<>(); - for (int z = 0; z < zSize; z++) { - sliceNumbers.add(z); - } - sliceNumbers.parallelStream().forEach(z -> { - if (this.isCanceled()) return; -// for (int z = 0; z < zSize; z++) { - //use an ROI covering the whole slice if no ROI has been defined in the ROI Manager - ShapeRoi sliceRoi = new ShapeRoi(wholeSliceRoi); - //get the ROIs for this slice from the ROI manager as a unioned mask - if (limitToRoi) { - sliceRoi = getUnionRoi(z, time, channel); - //if no rois and limitToRoi is true stop this thread - if (sliceRoi == null) return; - } + for (int z = 0; z < zSize; z++) { //create a 2D view into the data RandomAccessibleInterval xyView = get2DSlice(inputImage, z, time, channel); + + //get a mask for this xyView from ROIs in the ROI Manager + RandomAccessibleInterval mask = + RoiManagerUtil.unionMaskFromRoiManager(xyView, z + 1, t + 1, c + 1, true); - //access the pixels of the xyView using an IJ1 ImagePlus - ImagePlus imp = ImageJFunctions.wrap(xyView, "XY View"); - imp.setRoi(sliceRoi); - int[] histogram = imp.getProcessor().getHistogram(); - - //check binary-ness as we go - for (int i = 1; i < 255; i++) - if (histogram[i] > 0) { - System.out.println("Cancelled non-binary on slice histogram check"); - cancelMacroSafe(this, NOT_BINARY); - return; - } + //Iterate over the mask and the slice + Cursor sliceCursor = Views.flatIterable(xyView).cursor(); + Cursor maskCursor = Views.flatIterable(mask).cursor(); - //write the counts to a per-slice accumulator array - fgCount[z] = histogram[255]; - bgCount[z] = histogram[0]; - }); - //after summing over all slices, add the result to the table - //summarise the result + while (maskCursor.hasNext()) { + maskCursor.fwd(); + sliceCursor.fwd(); + //if we are inside an ROI + if (maskCursor.get().get()) { + total++; + final double v = sliceCursor.get().getRealDouble(); + //if foreground + if (v == 255.0) { + fg++; + } else if (v != 0.0) { + cancelMacroSafe(this, NOT_BINARY); + } + } + } + } //don't show any results for cancelled plugins if (this.isCanceled()) return; - - long fg = 0; - long bg = 0; - for (int z = 0; z < zSize; z++) { - fg += fgCount[z]; - bg += bgCount[z]; - } - - double tv = (fg + bg) * elementSize; + + double tv = total * elementSize; double bv = fg * elementSize; String label = name; @@ -244,19 +211,19 @@ private void validateImage() { final long spatialDimensions = AxisUtils.countSpatialDimensions(inputImage); if (spatialDimensions < 2 || spatialDimensions > 3) { - cancelMacroSafe(this, WEIRD_SPATIAL); inputImage = null; + cancelMacroSafe(this, WEIRD_SPATIAL); return; } -// + T type = inputImage.firstElement(); //enforce 8-bit (IJ1 binary is 0 and 255) if (type instanceof UnsignedByteType) return; else { System.out.println("Cancelled non-binary at initial validation"); - cancelMacroSafe(this, NOT_BINARY); inputImage = null; + cancelMacroSafe(this, NOT_BINARY); return; } } @@ -269,38 +236,6 @@ private static int axisIndex(ImgPlus img, AxisType axis) { return -1; } - /** - * Get the union of all the ROIs on this slice for this timepoint and channel - * - * If ROIs don't have z, time or channel position, they are applied to all z or time or channel positions - * - * @param z the 0-indexed z coordinate (NOT the 1-indexed slice) - * @param t the 0-indexed time coordinate (NOT the 1-indexed timepoint) - * @param c the 0-indexed channel coordinate (NOT the 1-indexed channel) - * @return the union of all the ROIs, or null if there are none for this z, t and c coordinate. - */ - ShapeRoi getUnionRoi(int z, int t, int c) { - Roi[] allRois = roiManager.getRoisAsArray(); - List sliceRois = new ArrayList<>(); - for (Roi roi : allRois) { - //keep the ROI if if is for this slice or doesn't have a slice AND - if ((roi.getZPosition() == z + 1 || roi.getZPosition() == 0) && - //keep the ROI if it is for this timepoint or doesn't have a timepoint AND - (roi.getTPosition() == t + 1 || roi.getTPosition() == 0) && - //keep the ROI if it is for this channel or doesn't have a channel - (roi.getCPosition() == c + 1 || roi.getCPosition() == 0)) { - ShapeRoi sr = (roi instanceof ShapeRoi) ? (ShapeRoi) roi : new ShapeRoi(roi); - sliceRois.add(sr); - } - } - if (sliceRois.size() == 0) return null; - ShapeRoi sliceRoi = sliceRois.get(0); - for (int i = 1; i < sliceRois.size(); i++) { - sliceRoi.or(sliceRois.get(i)); - } - return sliceRoi; - } - public static RandomAccessibleInterval get2DSlice(ImgPlus img, int z, int t, int c) { // Get the indices of Z, TIME, and CHANNEL int zIdx = axisIndex(img, Axes.Z); From 8a7a26c68c6460d11c08e29565fc81e6a82477e6 Mon Sep 17 00:00:00 2001 From: mdoube Date: Tue, 3 Mar 2026 11:01:49 +0100 Subject: [PATCH 06/10] Skip slices that have no ROI and use all pixels if there are no ROIs Only considering ROIs in the ROI Manager; ROIs drawn but not added are ignored. --- .../org/bonej/utilities/RoiManagerUtil.java | 22 +++--- .../ElementFractionWrapper.java | 74 +++++++++++++------ 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java b/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java index 46227a1c..7a526f89 100644 --- a/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java +++ b/Modern/utilities/src/main/java/org/bonej/utilities/RoiManagerUtil.java @@ -93,7 +93,7 @@ public static List pointROICoordinates(final RoiManager manager) { * * @return true if there are no ROIs to process */ - public static boolean isEmpty() { + public static boolean roiManagerIsEmpty() { final RoiManager rm = RoiManager.getInstance2(); //if there are no ROIs or no ROI Manager return (rm == null || rm.getCount() == 0); @@ -107,24 +107,22 @@ public static boolean isEmpty() { * @param z1 1-based Z position (IJ1 convention) * @param t1 1-based T position (IJ1 convention) * @param c1 1-based C position (IJ1 convention) + * @return a BitType mask that represents all the ROIs active on this XY slice or null if the ROI + * Manager is null or empty, or if there are no ROIs active on this XY slice. */ public static RandomAccessibleInterval unionMaskFromRoiManager( final RandomAccessibleInterval xyView, final int z1, final int t1, - final int c1, - final boolean treatEmptyRoiManagerAsFull + final int c1 ) { - final RoiManager rm = RoiManager.getInstance2(); + //if there are no ROIs or no ROI Manager - if (isEmpty()) { - if (treatEmptyRoiManagerAsFull) - //no ROIs so use all image pixels (apply an all-true mask) - return fullMaskLike(xyView); - else - // No ROIs so use no image pixels (apply an all-false mask) - return emptyMaskLike(xyView); + if (roiManagerIsEmpty()) { + return null; } + + final RoiManager rm = RoiManager.getInstance2(); final Roi[] all = rm.getRoisAsArray(); final List rois = new ArrayList<>(); @@ -132,7 +130,7 @@ public static RandomAccessibleInterval unionMaskFromRoiManager( if (matchesPlane(r, z1, t1, c1)) rois.add(r); } if (rois.isEmpty()) { - return emptyMaskLike(xyView); + return null; } // View geometry diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index 1462d7cd..35bafd5b 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -120,42 +120,68 @@ public void run() { long fg = 0; long total = 0; //iterate over all the timepoints and channels, and for each iterate over z. + long start = System.nanoTime(); + for (int t = 0; t < tSize; t++) { final int time = t; for (int c = 0; c < cSize; c++) { - final int channel = c; - statusService.showStatus("Element fraction: channel "+c+", time "+t); - - for (int z = 0; z < zSize; z++) { + final int channel = c; + for (int z = 0; z < zSize; z++) { + statusService.showStatus("Element fraction: channel "+c+", time "+t+", z "+z); //create a 2D view into the data RandomAccessibleInterval xyView = get2DSlice(inputImage, z, time, channel); - - //get a mask for this xyView from ROIs in the ROI Manager - RandomAccessibleInterval mask = - RoiManagerUtil.unionMaskFromRoiManager(xyView, z + 1, t + 1, c + 1, true); - - //Iterate over the mask and the slice - Cursor sliceCursor = Views.flatIterable(xyView).cursor(); - Cursor maskCursor = Views.flatIterable(mask).cursor(); - - while (maskCursor.hasNext()) { - maskCursor.fwd(); - sliceCursor.fwd(); - //if we are inside an ROI - if (maskCursor.get().get()) { + + //If the ROI Manager contains ROIs, use them + if (!RoiManagerUtil.roiManagerIsEmpty()) { + + //get a mask for this xyView from ROIs in the ROI Manager + RandomAccessibleInterval mask = + RoiManagerUtil.unionMaskFromRoiManager(xyView, z + 1, t + 1, c + 1); + + //don't process slices that lack a mask + if (mask == null) continue; + + //Iterate over the mask and the slice + Cursor sliceCursor = Views.flatIterable(xyView).cursor(); + Cursor maskCursor = Views.flatIterable(mask).cursor(); + + while (maskCursor.hasNext()) { + maskCursor.fwd(); + sliceCursor.fwd(); + //if we are inside an ROI + if (maskCursor.get().get()) { + total++; + final double v = sliceCursor.get().getRealDouble(); + //if foreground + if (v == 255.0) { + fg++; + } else if (v != 0.0) { + cancelMacroSafe(this, NOT_BINARY); + } + } + } + //Otherwise process all pixels in the image + } else { + Cursor sliceCursor = Views.flatIterable(xyView).cursor(); + while (sliceCursor.hasNext()) { + sliceCursor.fwd(); total++; final double v = sliceCursor.get().getRealDouble(); - //if foreground - if (v == 255.0) { - fg++; - } else if (v != 0.0) { - cancelMacroSafe(this, NOT_BINARY); - } + //if foreground + if (v == 255.0) { + fg++; + } else if (v != 0.0) { + cancelMacroSafe(this, NOT_BINARY); + } } } } + long end = System.nanoTime(); + + System.out.println("Volume fraction took "+(end-start) / 1E6+" ms"); + //don't show any results for cancelled plugins if (this.isCanceled()) return; From 73bd9cf2f9e4479a377a7300b03b38a9fe364c38 Mon Sep 17 00:00:00 2001 From: mdoube Date: Tue, 3 Mar 2026 12:39:37 +0100 Subject: [PATCH 07/10] Parallelise over z-slices --- .../ElementFractionWrapper.java | 310 ++++++++++-------- 1 file changed, 166 insertions(+), 144 deletions(-) diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index 35bafd5b..3be845cf 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -65,6 +65,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.stream.IntStream; /** * This command estimates the size of the given sample by counting its @@ -77,7 +78,7 @@ * @author Michael Doube */ @Plugin(type = Command.class, - menuPath = "Plugins>BoneJ>Fraction>Area/Volume fraction") +menuPath = "Plugins>BoneJ>Fraction>Area/Volume fraction") public class ElementFractionWrapper & NativeType> extends BoneJCommand { @@ -98,115 +99,137 @@ public class ElementFractionWrapper & NativeType> exten private String ratioHeader; /** The calibrated size of an element in the image */ private double elementSize; - + @Override public void run() { statusService.showStatus("Element fraction: initializing"); prepareResultDisplay(); final String name = inputImage.getName(); - + //get the number of slices, channels and time points to iterate over - int zIdx = axisIndex(inputImage, Axes.Z); - int tIdx = axisIndex(inputImage, Axes.TIME); - int cIdx = axisIndex(inputImage, Axes.CHANNEL); -// System.out.println("axisIndex (w,h,d,t,c) = ("+xIdx+", "+yIdx+", "+zIdx+", "+tIdx+", "+cIdx+")"); - - //if an axis is missing, set its size to 1, otherwise get its size. - int cSize = (cIdx >= 0) ? (int) inputImage.dimension(cIdx) : 1; - int zSize = (zIdx >= 0) ? (int) inputImage.dimension(zIdx) : 1; - int tSize = (tIdx >= 0) ? (int) inputImage.dimension(tIdx) : 1; - - long fg = 0; - long total = 0; - //iterate over all the timepoints and channels, and for each iterate over z. + int zIdx = axisIndex(inputImage, Axes.Z); + int tIdx = axisIndex(inputImage, Axes.TIME); + int cIdx = axisIndex(inputImage, Axes.CHANNEL); + // System.out.println("axisIndex (w,h,d,t,c) = ("+xIdx+", "+yIdx+", "+zIdx+", "+tIdx+", "+cIdx+")"); + + //if an axis is missing, set its size to 1, otherwise get its size. + int cSize = (cIdx >= 0) ? (int) inputImage.dimension(cIdx) : 1; + int zSize = (zIdx >= 0) ? (int) inputImage.dimension(zIdx) : 1; + int tSize = (tIdx >= 0) ? (int) inputImage.dimension(tIdx) : 1; + + //thread-safe counters + long[] fgCounts = new long[zSize]; + long[] totalCounts = new long[zSize]; + + //iterate over all the timepoints and channels, and for each iterate over z. long start = System.nanoTime(); - - for (int t = 0; t < tSize; t++) { - final int time = t; - for (int c = 0; c < cSize; c++) { - final int channel = c; - for (int z = 0; z < zSize; z++) { - statusService.showStatus("Element fraction: channel "+c+", time "+t+", z "+z); - - //create a 2D view into the data - RandomAccessibleInterval xyView = get2DSlice(inputImage, z, time, channel); - - //If the ROI Manager contains ROIs, use them - if (!RoiManagerUtil.roiManagerIsEmpty()) { - - //get a mask for this xyView from ROIs in the ROI Manager - RandomAccessibleInterval mask = - RoiManagerUtil.unionMaskFromRoiManager(xyView, z + 1, t + 1, c + 1); - - //don't process slices that lack a mask - if (mask == null) continue; - - //Iterate over the mask and the slice - Cursor sliceCursor = Views.flatIterable(xyView).cursor(); - Cursor maskCursor = Views.flatIterable(mask).cursor(); - - while (maskCursor.hasNext()) { - maskCursor.fwd(); - sliceCursor.fwd(); - //if we are inside an ROI - if (maskCursor.get().get()) { - total++; - final double v = sliceCursor.get().getRealDouble(); - //if foreground - if (v == 255.0) { - fg++; - } else if (v != 0.0) { - cancelMacroSafe(this, NOT_BINARY); - } - } - } - //Otherwise process all pixels in the image - } else { - Cursor sliceCursor = Views.flatIterable(xyView).cursor(); - while (sliceCursor.hasNext()) { - sliceCursor.fwd(); - total++; - final double v = sliceCursor.get().getRealDouble(); - //if foreground - if (v == 255.0) { - fg++; - } else if (v != 0.0) { - cancelMacroSafe(this, NOT_BINARY); - } - } - } - } - - long end = System.nanoTime(); - - System.out.println("Volume fraction took "+(end-start) / 1E6+" ms"); - - //don't show any results for cancelled plugins - if (this.isCanceled()) return; - - double tv = total * elementSize; - double bv = fg * elementSize; - - String label = name; - - //add a channel suffix if there is more than one channel - label = cSize > 1 ? label + " Channel: " + c : label; - //add a comma between channel and timepoint if both are more than one - label = (cSize > 1 && tSize > 1) ? label +"," : label; - //add a time suffix if there is more than one timepoint - label = tSize > 1 ? label + " Time: " + t : label; - - addResults(label, bv, tv, bv/tv); - - } - } - + + for (int t = 0; t < tSize; t++) { + final int time = t; + for (int c = 0; c < cSize; c++) { + final int channel = c; + boolean aborted = IntStream.range(0, zSize).parallel().anyMatch(z -> { + statusService.showStatus("Element fraction: channel "+channel+", time "+time+", z "+z); + //counters for this thread + long total = 0; + long fg = 0; + + //create a 2D view into the data + RandomAccessibleInterval xyView = get2DSlice(inputImage, z, time, channel); + + //If the ROI Manager contains ROIs, use them + if (!RoiManagerUtil.roiManagerIsEmpty()) { + + //get a mask for this xyView from ROIs in the ROI Manager + RandomAccessibleInterval mask = + RoiManagerUtil.unionMaskFromRoiManager(xyView, z + 1, time + 1, channel + 1); + + //don't process slices that lack a mask + if (mask == null) return false; + + //Iterate over the mask and the slice + Cursor sliceCursor = Views.flatIterable(xyView).cursor(); + Cursor maskCursor = Views.flatIterable(mask).cursor(); + + while (maskCursor.hasNext()) { + maskCursor.fwd(); + sliceCursor.fwd(); + //if we are inside an ROI + if (maskCursor.get().get()) { + total++; + final double v = sliceCursor.get().getRealDouble(); + //if foreground + if (v == 255.0) { + fg++; + } else if (v != 0.0) { + cancelMacroSafe(this, NOT_BINARY); + return true; + } + } + } + //Otherwise process all pixels in the image + } else { + Cursor sliceCursor = Views.flatIterable(xyView).cursor(); + while (sliceCursor.hasNext()) { + sliceCursor.fwd(); + total++; + final double v = sliceCursor.get().getRealDouble(); + //if foreground + if (v == 255.0) { + fg++; + } else if (v != 0.0) { + cancelMacroSafe(this, NOT_BINARY); + return true; + } + } + } + totalCounts[z] = total; + fgCounts[z] = fg; + //successful completion (aborted = false) + return false; + }); + + if (aborted) { + //no need to do anything + } + + long end = System.nanoTime(); + + System.out.println("Volume fraction took "+(end-start) / 1E6+" ms"); + + //don't show any results for cancelled plugins + if (this.isCanceled()) return; + + long total = 0; + long fg = 0; + for (int z = 0; z < zSize; z++) { + total += totalCounts[z]; + fg += fgCounts[z]; + } + + double tv = total * elementSize; + double bv = fg * elementSize; + + String label = name; + + //add a channel suffix if there is more than one channel + label = cSize > 1 ? label + " Channel: " + c : label; + //add a comma between channel and timepoint if both are more than one + label = (cSize > 1 && tSize > 1) ? label +"," : label; + //add a time suffix if there is more than one timepoint + label = tSize > 1 ? label + " Time: " + t : label; + + addResults(label, bv, tv, bv/tv); + + } + } + resultsTable = SharedTable.getTable(); } private void addResults(final String label, final double foregroundSize, - final double totalSize, final double ratio) + final double totalSize, final double ratio) { SharedTable.add(label, boneSizeHeader, foregroundSize); SharedTable.add(label, totalSizeHeader, totalSize); @@ -217,14 +240,14 @@ private void addResults(final String label, final double foregroundSize, private void prepareResultDisplay() { final char exponent = ResultUtils.getExponent(inputImage); final String unitHeader = ResultUtils.getUnitHeader(inputImage, unitService, - String.valueOf(exponent)); + String.valueOf(exponent)); final String sizeDescription = ResultUtils.getSizeDescription(inputImage); boneSizeHeader = "B" + sizeDescription + " " + unitHeader; totalSizeHeader = "T" + sizeDescription + " " + unitHeader; ratioHeader = "B" + sizeDescription + "/T" + sizeDescription; elementSize = ElementUtil.calibratedSpatialElementSize(inputImage, - unitService); + unitService); } @@ -247,54 +270,53 @@ private void validateImage() { if (type instanceof UnsignedByteType) return; else { - System.out.println("Cancelled non-binary at initial validation"); inputImage = null; cancelMacroSafe(this, NOT_BINARY); return; } } - + /** Return the index of the given axis in the ImgPlus, or -1 if absent. */ - private static int axisIndex(ImgPlus img, AxisType axis) { - for (int d = 0; d < img.numDimensions(); d++) { - if (img.axis(d).type().equals(axis)) return d; - } - return -1; - } - - public static RandomAccessibleInterval get2DSlice(ImgPlus img, int z, int t, int c) { - // Get the indices of Z, TIME, and CHANNEL - int zIdx = axisIndex(img, Axes.Z); - int tIdx = axisIndex(img, Axes.TIME); - int cIdx = axisIndex(img, Axes.CHANNEL); - - // Collect all non-spatial dimensions (Z, TIME, CHANNEL) and their target slice indices - List dimensionsToSlice = new ArrayList<>(); - if (zIdx >= 0 && img.dimension(zIdx) > 1) dimensionsToSlice.add(new DimensionSlice(zIdx, z)); - if (tIdx >= 0 && img.dimension(tIdx) > 1) dimensionsToSlice.add(new DimensionSlice(tIdx, t)); - if (cIdx >= 0 && img.dimension(cIdx) > 1) dimensionsToSlice.add(new DimensionSlice(cIdx, c)); - - // Sort dimensions by index in descending order - Collections.sort(dimensionsToSlice, Comparator.comparingInt(ds -> -ds.index)); - - // Slice along dimensions in descending order of their indices - RandomAccessibleInterval view = img; - for (DimensionSlice ds : dimensionsToSlice) { - view = Views.hyperSlice(view, ds.index, ds.slice); - } - - // The result is now a 2D XY slice - return view; - } - - // Helper class to store dimension index and slice index - private static class DimensionSlice { - int index; - int slice; - - DimensionSlice(int index, int slice) { - this.index = index; - this.slice = slice; - } - } + private static int axisIndex(ImgPlus img, AxisType axis) { + for (int d = 0; d < img.numDimensions(); d++) { + if (img.axis(d).type().equals(axis)) return d; + } + return -1; + } + + public static RandomAccessibleInterval get2DSlice(ImgPlus img, int z, int t, int c) { + // Get the indices of Z, TIME, and CHANNEL + int zIdx = axisIndex(img, Axes.Z); + int tIdx = axisIndex(img, Axes.TIME); + int cIdx = axisIndex(img, Axes.CHANNEL); + + // Collect all non-spatial dimensions (Z, TIME, CHANNEL) and their target slice indices + List dimensionsToSlice = new ArrayList<>(); + if (zIdx >= 0 && img.dimension(zIdx) > 1) dimensionsToSlice.add(new DimensionSlice(zIdx, z)); + if (tIdx >= 0 && img.dimension(tIdx) > 1) dimensionsToSlice.add(new DimensionSlice(tIdx, t)); + if (cIdx >= 0 && img.dimension(cIdx) > 1) dimensionsToSlice.add(new DimensionSlice(cIdx, c)); + + // Sort dimensions by index in descending order + Collections.sort(dimensionsToSlice, Comparator.comparingInt(ds -> -ds.index)); + + // Slice along dimensions in descending order of their indices + RandomAccessibleInterval view = img; + for (DimensionSlice ds : dimensionsToSlice) { + view = Views.hyperSlice(view, ds.index, ds.slice); + } + + // The result is now a 2D XY slice + return view; + } + + // Helper class to store dimension index and slice index + private static class DimensionSlice { + int index; + int slice; + + DimensionSlice(int index, int slice) { + this.index = index; + this.slice = slice; + } + } } From 49f07ded163b24d075a739a91ca1fae0d4db3b36 Mon Sep 17 00:00:00 2001 From: mdoube Date: Tue, 3 Mar 2026 13:08:13 +0100 Subject: [PATCH 08/10] Reset the slice counters each time we re-enter the z-stream --- .../java/org/bonej/wrapperPlugins/ElementFractionWrapper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index 3be845cf..d7de8404 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -131,9 +131,11 @@ public void run() { final int channel = c; boolean aborted = IntStream.range(0, zSize).parallel().anyMatch(z -> { statusService.showStatus("Element fraction: channel "+channel+", time "+time+", z "+z); - //counters for this thread + //counters for this thread and z-position long total = 0; long fg = 0; + totalCounts[z] = 0; + fgCounts[z] = 0; //create a 2D view into the data RandomAccessibleInterval xyView = get2DSlice(inputImage, z, time, channel); From ef6fb38b685184d42cd90fbdf018db2f2b9e85df Mon Sep 17 00:00:00 2001 From: mdoube Date: Tue, 3 Mar 2026 19:14:30 +0100 Subject: [PATCH 09/10] Reject images with any non-XYZTC axes --- .../java/org/bonej/utilities/AxisUtils.java | 30 +++++++++++++++++++ .../bonej/wrapperPlugins/CommonMessages.java | 1 + .../ElementFractionWrapper.java | 5 ++++ 3 files changed, 36 insertions(+) diff --git a/Modern/utilities/src/main/java/org/bonej/utilities/AxisUtils.java b/Modern/utilities/src/main/java/org/bonej/utilities/AxisUtils.java index de961350..3202c79a 100644 --- a/Modern/utilities/src/main/java/org/bonej/utilities/AxisUtils.java +++ b/Modern/utilities/src/main/java/org/bonej/utilities/AxisUtils.java @@ -140,6 +140,36 @@ private AxisUtils() {} return axisStream(space).anyMatch(a -> a.type() == Axes.TIME); } + /** + * Checks whether the given annotated space contains any dimension (axis) + * that is not one of the standard X, Y, Z, Channel, or Time axes. + * + *

This method is useful for detecting the presence of non-standard axes + * (e.g., Angle, Lambda, Phase, etc.) in an image or dataset, which may require + * special handling in algorithms or processing pipelines.

+ * + * @param the type of the annotated space, which must extend {@code AnnotatedSpace} + * @param the type of the axis, which must extend {@code TypedAxis} + * @param space the annotated space (e.g., image or dataset) to check for non-standard dimensions + * @return {@code true} if the space contains at least one axis that is not + * X, Y, Z, Channel, or Time; {@code false} otherwise + * + * @see AnnotatedSpace + * @see TypedAxis + * @see Axes + */ + public static , A extends TypedAxis> boolean + hasNonXYZCTDimension(final S space) + { + return axisStream(space).anyMatch(a -> ( + a.type() != Axes.X && + a.type() != Axes.Y && + a.type() != Axes.Z && + a.type() != Axes.CHANNEL && + a.type() != Axes.TIME + )); + } + /** * Checks if the spatial axes in the space have the same i.e. isotropic * scaling. diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/CommonMessages.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/CommonMessages.java index a1edfa04..22098a9b 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/CommonMessages.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/CommonMessages.java @@ -45,6 +45,7 @@ public final class CommonMessages { static final String NOT_8_BIT_BINARY_IMAGE = "Need an 8-bit binary image"; static final String NO_IMAGE_OPEN = "No image open"; static final String NO_SKELETONS = "Image contained no skeletons"; + static final String HAS_NONSTANDARD_DIMENSIONS = "Image has non-standard dimensions"; private CommonMessages() {} } diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index d7de8404..054de990 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -266,6 +266,11 @@ private void validateImage() { cancelMacroSafe(this, WEIRD_SPATIAL); return; } + + if (AxisUtils.hasNonXYZCTDimension(inputImage)) { + inputImage = null; + cancelMacroSafe(this, CommonMessages.HAS_NONSTANDARD_DIMENSIONS); + } T type = inputImage.firstElement(); //enforce 8-bit (IJ1 binary is 0 and 255) From 8f3ce67dc3ff047935e0ab9999ea590d2373ba0f Mon Sep 17 00:00:00 2001 From: mdoube Date: Wed, 4 Mar 2026 10:40:11 +0100 Subject: [PATCH 10/10] Make sure the wrapper tests pass. Since we are now enforcing IJ1-style binary image convention (8-bit, 0,255) BitType images are not valid input. Note also the resultTable has to be 'getted' to be not null. --- .../ElementFractionWrapper.java | 14 +++---- .../wrapperPlugins/CommonWrapperTests.java | 4 +- .../ElementFractionWrapperTest.java | 39 +++++++++++-------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java index 054de990..a1199fbd 100644 --- a/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java +++ b/Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/ElementFractionWrapper.java @@ -123,7 +123,7 @@ public void run() { long[] totalCounts = new long[zSize]; //iterate over all the timepoints and channels, and for each iterate over z. - long start = System.nanoTime(); +// long start = System.nanoTime(); for (int t = 0; t < tSize; t++) { final int time = t; @@ -196,9 +196,9 @@ public void run() { //no need to do anything } - long end = System.nanoTime(); +// long end = System.nanoTime(); - System.out.println("Volume fraction took "+(end-start) / 1E6+" ms"); +// System.out.println("Volume fraction took "+(end-start) / 1E6+" ms"); //don't show any results for cancelled plugins if (this.isCanceled()) return; @@ -223,11 +223,11 @@ public void run() { label = tSize > 1 ? label + " Time: " + t : label; addResults(label, bv, tv, bv/tv); - + + // Ensure SharedTable is populated + resultsTable = SharedTable.getTable(); } - } - - resultsTable = SharedTable.getTable(); + } } private void addResults(final String label, final double foregroundSize, diff --git a/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/CommonWrapperTests.java b/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/CommonWrapperTests.java index 0539c387..00214122 100644 --- a/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/CommonWrapperTests.java +++ b/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/CommonWrapperTests.java @@ -153,7 +153,7 @@ static void testNonBinaryImageCancelsPlugin( // VERIFY assertTrue( - "An image with more than two colours should have cancelled the plugin", + "An image with more than two values should have cancelled the plugin", module.isCanceled()); assertEquals("Cancel reason is incorrect", CommonMessages.NOT_BINARY, module .getCancelReason()); @@ -189,7 +189,7 @@ static void testNonBinaryImagePlusCancelsPlugin( // VERIFY assertTrue( - "An image with more than two colours should have cancelled the plugin", + "An image with more than two values should have cancelled the plugin", module.isCanceled()); assertEquals("Cancel reason is incorrect", CommonMessages.NOT_8_BIT_BINARY_IMAGE, module.getCancelReason()); diff --git a/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/ElementFractionWrapperTest.java b/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/ElementFractionWrapperTest.java index 406af297..460e5ab7 100644 --- a/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/ElementFractionWrapperTest.java +++ b/Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/ElementFractionWrapperTest.java @@ -48,7 +48,7 @@ import net.imagej.axis.DefaultLinearAxis; import net.imglib2.img.Img; import net.imglib2.img.array.ArrayImgs; -import net.imglib2.type.logic.BitType; +import net.imglib2.type.numeric.integer.UnsignedByteType; import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.view.Views; @@ -80,6 +80,7 @@ public void testNullImageCancelsElementFraction() { @Test public void testResults3DHyperstack() throws Exception { + System.out.println("testResults3DHyperstack()"); // SETUP final String unit = "mm"; final double scale = 0.9; @@ -98,25 +99,25 @@ public void testResults3DHyperstack() throws Exception { expectedRatios }; final String[] expectedHeaders = { "BV (" + unit + "³)", "TV (" + unit + "³)", "BV/TV" }; - // Create an hyperstack Img with a cube at (channel:0, frame:0) and (c:1, - // f:1) - final Img img = ArrayImgs.bits(stackSide, stackSide, stackSide, 2, - 2); - Views.interval(img, new long[] { 1, 1, 1, 0, 0 }, new long[] { 5, 5, 5, 0, - 0 }).forEach(BitType::setOne); - Views.interval(img, new long[] { 1, 1, 1, 1, 1 }, new long[] { 5, 5, 5, 1, - 1 }).forEach(BitType::setOne); + // Create a hyperstack Img with a cube at (channel:0, frame:0) and (c:1, f:1) + final Img img = ArrayImgs.unsignedBytes(stackSide, stackSide, stackSide, 2, 2); + Views.interval(img, new long[] { 1, 1, 1, 0, 0 }, new long[] { 5, 5, 5, 0, 0 }).forEach(t -> t.set(255)); + Views.interval(img, new long[] { 1, 1, 1, 1, 1 }, new long[] { 5, 5, 5, 1, 1 }).forEach(t -> t.set(255)); + // Wrap Img in a calibrated ImgPlus final double[] calibration = { scale, scale, scale, 1.0, 1.0 }; final String[] units = { unit, unit, unit, "", "" }; final AxisType[] axes = { Axes.X, Axes.Y, Axes.Z, Axes.TIME, Axes.CHANNEL }; - final ImgPlus imgPlus = new ImgPlus<>(img, "Cube", axes, - calibration, units); - + final ImgPlus imgPlus = new ImgPlus<>(img, "Cube", axes, + calibration, units); + // EXECUTE final CommandModule module = command().run( ElementFractionWrapper.class, true, "inputImage", imgPlus).get(); + //Make sure the plugin wasn't cancelled + assertTrue(module.getCancelReason(), !module.isCanceled()); + // VERIFY @SuppressWarnings("unchecked") final List> table = @@ -138,6 +139,7 @@ public void testResults3DHyperstack() throws Exception { @Test public void testResultsComposite2D() throws Exception { + System.out.println("testResultsComposite2D()"); // SETUP final String unit = "mm"; final int squareSide = 5; @@ -154,24 +156,27 @@ public void testResultsComposite2D() throws Exception { final String[] expectedHeaders = { "BA (" + unit + "\u00B2)", "TA (" + unit + "\u00B2)", "BA/TA" }; // Create an 2D image with two channels with a square drawn on channel 2 - final Img img = ArrayImgs.bits(stackSide, stackSide, 2); - Views.interval(img, new long[] { 1, 1, 1 }, new long[] { 5, 5, 1 }).forEach( - BitType::setOne); + final Img img = ArrayImgs.unsignedBytes(stackSide, stackSide, 2); + // Set the square region to 255 (foreground) + Views.interval(img, new long[] { 1, 1, 1 }, new long[] { 5, 5, 1 }).forEach(t -> t.set(255)); // Wrap Img in an ImgPlus final double[] calibration = { 1.0, 1.0, 1.0 }; final String[] units = { unit, unit, "" }; final AxisType[] axes = { Axes.X, Axes.Y, Axes.CHANNEL }; - final ImgPlus imgPlus = new ImgPlus<>(img, "Square", axes, - calibration, units); + final ImgPlus imgPlus = new ImgPlus<>(img, "Square", axes, calibration, units); // EXECUTE final CommandModule module = command().run( ElementFractionWrapper.class, true, "inputImage", imgPlus).get(); + + //Make sure the plugin wasn't cancelled + assertTrue(module.getCancelReason(), !module.isCanceled()); // VERIFY @SuppressWarnings("unchecked") final List> table = (List>) module.getOutput("resultsTable"); + assertNotNull(table); assertEquals("Wrong number of columns", 3, table.size()); for (int i = 0; i < 3; i++) {