Skip to content

Commit 932b12b

Browse files
committed
Improve headless calling of intertrabecular angles (#318)
* Initialise the pixel spacing class variable * ITA: improve handling of calibrated images and test them explicitly Only isotropic images are tested so far, anisotropic pixel spacing causes the tests to cancel. * Update README.md (#302) * ITA: add tests for calibrated isotropic pixel spacing * ITA: add tests that run in headless mode Includes a new base class AbstractWrapperHeadlessTest that other tests of headless functionality can extend. This is useful for checking that wrapper plugins behave as expected when called from macros and Python scripts, or from other Java code. * Remove redundant copyright statement
1 parent 3949a0f commit 932b12b

File tree

4 files changed

+318
-38
lines changed

4 files changed

+318
-38
lines changed

Modern/wrapperPlugins/src/main/java/org/bonej/wrapperPlugins/IntertrabecularAngleWrapper.java

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
import org.bonej.utilities.ImagePlusUtil;
5757
import org.bonej.utilities.SharedTable;
5858
import org.bonej.wrapperPlugins.wrapperUtils.Common;
59-
import org.bonej.wrapperPlugins.wrapperUtils.ResultUtils;
6059
import org.joml.Vector3d;
6160
import org.scijava.ItemIO;
6261
import org.scijava.app.StatusService;
@@ -117,18 +116,16 @@ public class IntertrabecularAngleWrapper extends BoneJCommand {
117116
style = NumberWidget.SLIDER_STYLE, persistKey = "ITA_max_valence",
118117
callback = "enforceValidRange")
119118
private int maximumValence = 3;
120-
@Parameter(label = "Minimum trabecular length (px)", min = "0",
121-
stepSize = "1",
122-
description = "Minimum length for a trabecula to be kept from being fused into a node",
123-
style = NumberWidget.SPINNER_STYLE, callback = "calculateRealLength",
124-
persist = false, initializer = "initRealLength")
125-
private int minimumTrabecularLength;
126-
@Parameter(label = "Margin (px)", min = "0", stepSize = "1",
127-
description = "Nodes with centroids closer than this value to any image boundary will not be included in results",
119+
@Parameter(label = "Minimum trabecular length (real units)", min = "0.00",
120+
stepSize = "0.1",
121+
description = "Minimum length in calibrated units for a trabecula to be kept from being fused into a node",
122+
style = NumberWidget.SPINNER_STYLE,
123+
persist = false)
124+
private double minimumTrabecularLength;
125+
@Parameter(label = "Margin (pixels)", min = "0", stepSize = "1",
126+
description = "Nodes with centroids closer than this value in pixels to any image boundary will not be included in results",
128127
style = NumberWidget.SPINNER_STYLE)
129128
private int marginCutOff;
130-
@SuppressWarnings("unused")
131-
private String realLength = "";
132129
@Parameter(label = "Iterate pruning",
133130
description = "If true, iterate pruning as long as short edges remain, or stop after a single pass",
134131
required = false, persistKey = "ITA_iterate")
@@ -169,12 +166,13 @@ public class IntertrabecularAngleWrapper extends BoneJCommand {
169166
@Parameter
170167
private LogService logService;
171168

172-
private double[] coefficients;
169+
private double[] pixelSpacing;
173170
private boolean anisotropyWarned;
174171

175172
@Override
176173
public void run() {
177174
statusService.showStatus("Intertrabecular angles: Initialising...");
175+
initRealLength();
178176
statusService.showStatus("Intertrabecular angles: skeletonising");
179177
final ImagePlus skeleton = skeletonise();
180178
if (showSkeleton)
@@ -193,7 +191,7 @@ public void run() {
193191
statusService.showProgress(1, PROGRESS_STEPS);
194192
final ValuePair<Graph, double[]> pruningResult = GraphPruning
195193
.pruneShortEdges(largestGraph, minimumTrabecularLength, iteratePruning,
196-
useClusters, coefficients);
194+
useClusters, pixelSpacing);
197195
final Graph cleanGraph = pruningResult.a;
198196
statusService.showStatus(
199197
"Intertrabecular angles: valence sorting trabeculae");
@@ -229,15 +227,6 @@ private Graph[] analyzeSkeleton(final ImagePlus skeleton) {
229227
return analyser.getGraphs();
230228
}
231229

232-
@SuppressWarnings("unused")
233-
private void calculateRealLength() {
234-
final double calibratedMinimumLength = minimumTrabecularLength *
235-
coefficients[0];
236-
final String unit = ResultUtils.getUnitHeader(inputImage);
237-
realLength = String.join(" ", String.format("%.2g",
238-
calibratedMinimumLength), unit);
239-
}
240-
241230
private Map<Integer, DoubleStream> createRadianMap(
242231
final Map<Integer, List<Vertex>> valenceMap)
243232
{
@@ -295,21 +284,17 @@ private void imageValidater() {
295284
}
296285
}
297286

298-
@SuppressWarnings("unused")
299287
private void initRealLength() {
300288
if (inputImage == null || inputImage.getCalibration() == null) {
301-
coefficients = new double[] { 1.0, 1.0, 1.0 };
302-
realLength = String.join(" ", String.format("%.2g",
303-
(double) minimumTrabecularLength));
289+
pixelSpacing = new double[] { 1.0, 1.0, 1.0 };
304290
}
305291
else {
306292
final Calibration calibration = inputImage.getCalibration();
307-
coefficients = new double[] { calibration.pixelWidth,
293+
pixelSpacing = new double[] { calibration.pixelWidth,
308294
calibration.pixelHeight, calibration.pixelDepth };
309-
calculateRealLength();
310295
}
311296
}
312-
297+
313298
private boolean isCloseToBoundary(final Vertex v) {
314299
final Vector3d centroid = PointUtils.centroid(v.getPoints());
315300
final int width = inputImage.getWidth();

Modern/wrapperPlugins/src/test/java/org/bonej/wrapperPlugins/IntertrabecularAngleWrapperTest.java

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,14 @@
3939
import static org.junit.Assert.assertTrue;
4040
import static org.mockito.ArgumentMatchers.any;
4141
import static org.mockito.ArgumentMatchers.anyString;
42-
import static org.mockito.ArgumentMatchers.eq;
43-
import static org.mockito.ArgumentMatchers.startsWith;
44-
import static org.mockito.Mockito.after;
4542
import static org.mockito.Mockito.doNothing;
46-
import static org.mockito.Mockito.mock;
4743
import static org.mockito.Mockito.timeout;
4844
import static org.mockito.Mockito.verify;
49-
import static org.mockito.Mockito.when;
50-
import static org.scijava.ui.DialogPrompt.MessageType.WARNING_MESSAGE;
5145

5246
import ij.IJ;
5347
import ij.ImagePlus;
5448
import ij.gui.NewImage;
49+
import ij.measure.Calibration;
5550

5651
import java.net.URL;
5752
import java.util.Iterator;
@@ -67,7 +62,6 @@
6762
import org.junit.experimental.categories.Category;
6863
import org.scijava.command.CommandModule;
6964
import org.scijava.table.DefaultColumn;
70-
import org.scijava.ui.swing.sdi.SwingDialogPrompt;
7165

7266
/**
7367
* Tests for {@link IntertrabecularAngleWrapper}
@@ -80,14 +74,14 @@
8074
public class IntertrabecularAngleWrapperTest extends AbstractWrapperTest {
8175

8276
@Test
83-
public void testAngleResults() throws Exception {
77+
public void testAngleResultsUncalibratedPixels() throws Exception {
8478
// SETUP
8579
final Predicate<Double> nonEmpty = Objects::nonNull;
8680
final URL resource = getClass().getClassLoader().getResource(
8781
"test-skelly.zip");
8882
assert resource != null;
8983
final ImagePlus skelly = IJ.openImage(resource.getFile());
90-
84+
9185
// EXECUTE
9286
final CommandModule module = command().run(
9387
IntertrabecularAngleWrapper.class, true, "inputImage", skelly,
@@ -115,7 +109,91 @@ public void testAngleResults() throws Exception {
115109
assertEquals(2, fiveColumn.stream().filter(nonEmpty)
116110
.filter(d -> d == Math.PI / 2).count());
117111
}
112+
113+
@Test
114+
public void testAngleResultsIsotropicBigPixels() throws Exception {
115+
// SETUP
116+
final Predicate<Double> nonEmpty = Objects::nonNull;
117+
final URL resource = getClass().getClassLoader().getResource(
118+
"test-skelly.zip");
119+
assert resource != null;
120+
final ImagePlus skelly = IJ.openImage(resource.getFile());
121+
Calibration cal = new Calibration();
122+
cal.pixelDepth = 2;
123+
cal.pixelHeight = 2;
124+
cal.pixelWidth = 2;
125+
skelly.setCalibration(cal);
118126

127+
// EXECUTE
128+
final CommandModule module = command().run(
129+
IntertrabecularAngleWrapper.class, true, "inputImage", skelly,
130+
"minimumValence", 3, "maximumValence", 50, "minimumTrabecularLength", 4,
131+
"marginCutOff", 0, "useClusters", true, "iteratePruning", false).get();
132+
133+
// VERIFY
134+
@SuppressWarnings("unchecked")
135+
final List<DefaultColumn<Double>> table =
136+
(List<DefaultColumn<Double>>) module.getOutput("resultsTable");
137+
assertNotNull(table);
138+
assertEquals(2, table.size());
139+
final DefaultColumn<Double> threeColumn = table.get(0);
140+
assertEquals("3", threeColumn.getHeader());
141+
assertEquals(10, threeColumn.size());
142+
assertEquals(3, threeColumn.stream().filter(nonEmpty).count());
143+
assertEquals(2, threeColumn.stream().filter(nonEmpty).distinct().count());
144+
final DefaultColumn<Double> fiveColumn = table.get(1);
145+
assertEquals("5", fiveColumn.getHeader());
146+
assertEquals(10, fiveColumn.size());
147+
assertEquals(10, fiveColumn.stream().filter(nonEmpty).count());
148+
assertEquals(6, fiveColumn.stream().filter(nonEmpty).distinct().count());
149+
assertEquals(1, fiveColumn.stream().filter(nonEmpty)
150+
.filter(d -> d == Math.PI).count());
151+
assertEquals(2, fiveColumn.stream().filter(nonEmpty)
152+
.filter(d -> d == Math.PI / 2).count());
153+
}
154+
155+
@Test
156+
public void testAngleResultsIsotropicSmallPixels() throws Exception {
157+
// SETUP
158+
final Predicate<Double> nonEmpty = Objects::nonNull;
159+
final URL resource = getClass().getClassLoader().getResource(
160+
"test-skelly.zip");
161+
assert resource != null;
162+
final ImagePlus skelly = IJ.openImage(resource.getFile());
163+
Calibration cal = new Calibration();
164+
cal.pixelDepth = 0.1;
165+
cal.pixelHeight = 0.1;
166+
cal.pixelWidth = 0.1;
167+
skelly.setCalibration(cal);
168+
169+
// EXECUTE
170+
final CommandModule module = command().run(
171+
IntertrabecularAngleWrapper.class, true, "inputImage", skelly,
172+
"minimumValence", 3, "maximumValence", 50, "minimumTrabecularLength", 0.2,
173+
"marginCutOff", 0, "useClusters", true, "iteratePruning", false).get();
174+
175+
// VERIFY
176+
@SuppressWarnings("unchecked")
177+
final List<DefaultColumn<Double>> table =
178+
(List<DefaultColumn<Double>>) module.getOutput("resultsTable");
179+
assertNotNull(table);
180+
assertEquals(2, table.size());
181+
final DefaultColumn<Double> threeColumn = table.get(0);
182+
assertEquals("3", threeColumn.getHeader());
183+
assertEquals(10, threeColumn.size());
184+
assertEquals(3, threeColumn.stream().filter(nonEmpty).count());
185+
assertEquals(2, threeColumn.stream().filter(nonEmpty).distinct().count());
186+
final DefaultColumn<Double> fiveColumn = table.get(1);
187+
assertEquals("5", fiveColumn.getHeader());
188+
assertEquals(10, fiveColumn.size());
189+
assertEquals(10, fiveColumn.stream().filter(nonEmpty).count());
190+
assertEquals(6, fiveColumn.stream().filter(nonEmpty).distinct().count());
191+
assertEquals(1, fiveColumn.stream().filter(nonEmpty)
192+
.filter(d -> d == Math.PI).count());
193+
assertEquals(2, fiveColumn.stream().filter(nonEmpty)
194+
.filter(d -> d == Math.PI / 2).count());
195+
}
196+
119197
@Test
120198
public void testAnisotropicImageShowsWarningDialog() {
121199
CommonWrapperTests.testAnisotropyWarning(imageJ(),
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*-
2+
* #%L
3+
* High-level BoneJ2 commands.
4+
* %%
5+
* Copyright (C) 2015 - 2020 Michael Doube, BoneJ developers
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.bonej.wrapperPlugins.headless;
31+
32+
import net.imagej.ImageJ;
33+
import org.bonej.utilities.SharedTable;
34+
import org.junit.After;
35+
import org.junit.AfterClass;
36+
import org.junit.Before;
37+
import org.junit.BeforeClass;
38+
import org.scijava.command.CommandService;
39+
40+
/**
41+
* An abstract base test class that handles basic setup and tear down
42+
* for testing wrapper plugins in headless mode.
43+
* <p>
44+
* NB Remember to call the methods in this class if you override/hide the methods!
45+
* </p>
46+
* @author Richard Domander
47+
* @author Michael Doube
48+
*/
49+
public abstract class AbstractWrapperHeadlessTest {
50+
private static ImageJ imageJ;
51+
private static CommandService commandService;
52+
53+
protected static CommandService command() {
54+
return commandService;
55+
}
56+
57+
protected static ImageJ imageJ() {
58+
return imageJ;
59+
}
60+
61+
@BeforeClass
62+
public static void basicOneTimeSetup() {
63+
imageJ = new ImageJ();
64+
commandService = imageJ.command();
65+
}
66+
67+
@Before
68+
public void setup() {
69+
imageJ.ui().setHeadless(true);
70+
}
71+
72+
@After
73+
public void tearDown() {
74+
SharedTable.reset();
75+
}
76+
77+
@AfterClass
78+
public static void basicOneTimeTearDown() {
79+
imageJ.context().dispose();
80+
imageJ = null;
81+
commandService = null;
82+
}
83+
}

0 commit comments

Comments
 (0)