diff --git a/src/org/doube/util/ImageCheck.java b/src/org/doube/util/ImageCheck.java index 2a9d21c4..150f1953 100644 --- a/src/org/doube/util/ImageCheck.java +++ b/src/org/doube/util/ImageCheck.java @@ -8,9 +8,9 @@ /** * Check if an image conforms to the type defined by each method. - * + * * @author Michael Doube - * + * */ public class ImageCheck { @@ -24,17 +24,17 @@ public class ImageCheck { * are not included. */ public static final String[] blacklistedIJVersions = { - // introduced bug where ROIs added to the ROI Manager - // lost their z-position information - "1.48a" }; + // introduced bug where ROIs added to the ROI Manager + // lost their z-position information + "1.48a" }; /** * Check if image is binary - * + * * @param imp * @return true if image is binary */ - public boolean isBinary(final ImagePlus imp) { + public static boolean isBinary(ImagePlus imp) { if (imp == null) { IJ.noImage(); return false; @@ -42,7 +42,7 @@ public boolean isBinary(final ImagePlus imp) { if (imp.getType() != ImagePlus.GRAY8) return false; - final ImageStatistics stats = imp.getStatistics(); + ImageStatistics stats = imp.getStatistics(); if (stats.histogram[0] + stats.histogram[255] != stats.pixelCount) return false; return true; @@ -50,11 +50,11 @@ public boolean isBinary(final ImagePlus imp) { /** * Check if an image is a multi-slice image stack - * + * * @param imp * @return true if the image has >= 2 slices */ - public boolean isMultiSlice(final ImagePlus imp) { + public static boolean isMultiSlice(ImagePlus imp) { if (imp == null) { IJ.noImage(); return false; @@ -68,57 +68,61 @@ public boolean isMultiSlice(final ImagePlus imp) { /** * Check if the image's voxels are isotropic in all 3 dimensions (i.e. are * placed on a cubic grid) - * + * * @param imp * image to test * @param tolerance * tolerated fractional deviation from equal length * @return true if voxel width == height == depth */ - public boolean isVoxelIsotropic(final ImagePlus imp, final double tolerance) { + public static boolean isVoxelIsotropic(ImagePlus imp, double tolerance) { if (imp == null) { - IJ.noImage(); + IJ.error("No image", "Image is null"); return false; } - final Calibration cal = imp.getCalibration(); + Calibration cal = imp.getCalibration(); final double vW = cal.pixelWidth; final double vH = cal.pixelHeight; - final double vD = cal.pixelDepth; final double tLow = 1 - tolerance; final double tHigh = 1 + tolerance; + final double widthHeightRatio = vW > vH ? vW / vH : vH / vW; final boolean isStack = (imp.getStackSize() > 1); - if (vW < vH * tLow || vW > vH * tHigh) - return false; - if ((vW < vD * tLow || vW > vD * tHigh) && isStack) - return false; - if ((vH < vD * tLow || vH > vD * tHigh) && isStack) + if (widthHeightRatio < tLow || widthHeightRatio > tHigh) { return false; + } - return true; + if(!isStack) { + return true; + } + + final double vD = cal.pixelDepth; + final double widthDepthRatio = vW > vD ? vW / vD : vD / vW; + + return (widthDepthRatio >= tLow && widthDepthRatio <= tHigh); } /** * Run isVoxelIsotropic() with a default tolerance of 0% - * + * * @param imp * input image * @return false if voxel dimensions are not equal */ - public boolean isVoxelIsotropic(final ImagePlus imp) { + public static boolean isVoxelIsotropic(ImagePlus imp) { return isVoxelIsotropic(imp, 0); } /** * Check that the voxel thickness is correct - * + * * @param imp * @return voxel thickness based on DICOM header information. Returns -1 if * there is no DICOM slice position information. */ - public double dicomVoxelDepth(final ImagePlus imp) { - final Calibration cal = imp.getCalibration(); - final double vD = cal.pixelDepth; + public static double dicomVoxelDepth(ImagePlus imp) { + Calibration cal = imp.getCalibration(); + double vD = cal.pixelDepth; String position = getDicomAttribute(imp, 1, "0020,0032"); if (position == null) { @@ -140,17 +144,19 @@ public double dicomVoxelDepth(final ImagePlus imp) { else return -1; - final double sliceSpacing = Math.abs((last - first) / (imp.getStackSize() - 1)); + double sliceSpacing = Math.abs((last - first) + / (imp.getStackSize() - 1)); - final String units = cal.getUnits(); + String units = cal.getUnits(); - final double error = Math.abs((sliceSpacing - vD) / sliceSpacing) * 100; + double error = Math.abs((sliceSpacing - vD) / sliceSpacing) * 100; if (vD != sliceSpacing) { - IJ.log(imp.getTitle() + ":\n" + "Current voxel depth disagrees by " + error - + "% with DICOM header slice spacing.\n" + "Current voxel depth: " + IJ.d2s(vD, 6) + " " + units - + "\n" + "DICOM slice spacing: " + IJ.d2s(sliceSpacing, 6) + " " + units + "\n" - + "Updating image properties..."); + IJ.log(imp.getTitle() + ":\n" + "Current voxel depth disagrees by " + + error + "% with DICOM header slice spacing.\n" + + "Current voxel depth: " + IJ.d2s(vD, 6) + " " + units + + "\n" + "DICOM slice spacing: " + IJ.d2s(sliceSpacing, 6) + + " " + units + "\n" + "Updating image properties..."); cal.pixelDepth = sliceSpacing; imp.setCalibration(cal); } else @@ -160,16 +166,16 @@ public double dicomVoxelDepth(final ImagePlus imp) { /** * Get the value associated with a DICOM tag from an ImagePlus header - * + * * @param imp * @param slice * @param tag * , in 0000,0000 format. * @return the value associated with the tag */ - private String getDicomAttribute(final ImagePlus imp, final int slice, final String tag) { - final ImageStack stack = imp.getImageStack(); - final String header = stack.getSliceLabel(slice); + private static String getDicomAttribute(ImagePlus imp, int slice, String tag) { + ImageStack stack = imp.getImageStack(); + String header = stack.getSliceLabel(slice); // tag must be in format 0000,0000 if (slice < 1 || slice > stack.getSize()) { return null; @@ -179,9 +185,9 @@ private String getDicomAttribute(final ImagePlus imp, final int slice, final Str } String attribute = " "; String value = " "; - final int idx1 = header.indexOf(tag); - final int idx2 = header.indexOf(":", idx1); - final int idx3 = header.indexOf("\n", idx2); + int idx1 = header.indexOf(tag); + int idx2 = header.indexOf(":", idx1); + int idx3 = header.indexOf("\n", idx2); if (idx1 >= 0 && idx2 >= 0 && idx3 >= 0) { try { attribute = header.substring(idx1 + 9, idx2); @@ -190,7 +196,7 @@ private String getDicomAttribute(final ImagePlus imp, final int slice, final Str value = value.trim(); // IJ.log("tag = " + tag + ", attribute = " + attribute // + ", value = " + value); - } catch (final Throwable e) { + } catch (Throwable e) { return " "; } } @@ -200,21 +206,27 @@ private String getDicomAttribute(final ImagePlus imp, final int slice, final Str /** * Show a message and return false if the version of IJ is too old for BoneJ * or is a known bad version - * + * * @return false if the IJ version is too old or blacklisted */ private static boolean checkIJVersion() { if (isIJVersionBlacklisted()) { - IJ.error("Bad ImageJ version", - "The version of ImageJ you are using (v" + IJ.getVersion() + IJ.error( + "Bad ImageJ version", + "The version of ImageJ you are using (v" + + IJ.getVersion() + ") is known to run BoneJ incorrectly.\n" + "Please up- or downgrade your ImageJ using Help-Update ImageJ."); return false; } if (requiredIJVersion.compareTo(IJ.getVersion()) > 0) { - IJ.error("Update ImageJ", "You are using an old version of ImageJ, v" + IJ.getVersion() + ".\n" - + "Please update to at least ImageJ v" + requiredIJVersion + " using Help-Update ImageJ."); + IJ.error( + "Update ImageJ", + "You are using an old version of ImageJ, v" + + IJ.getVersion() + ".\n" + + "Please update to at least ImageJ v" + + requiredIJVersion + " using Help-Update ImageJ."); return false; } return true; @@ -223,21 +235,23 @@ private static boolean checkIJVersion() { /** * Show a message a return false if any requirement of the environment is * missing - * + * * @return */ public static boolean checkEnvironment() { try { Class.forName("javax.media.j3d.VirtualUniverse"); - } catch (final ClassNotFoundException e) { - IJ.showMessage("Java 3D libraries are not installed.\n" + "Please install and run the ImageJ 3D Viewer,\n" + } catch (ClassNotFoundException e) { + IJ.showMessage("Java 3D libraries are not installed.\n" + + "Please install and run the ImageJ 3D Viewer,\n" + "which will automatically install Java's 3D libraries."); return false; } try { Class.forName("ij3d.ImageJ3DViewer"); - } catch (final ClassNotFoundException e) { - IJ.showMessage("ImageJ 3D Viewer is not installed.\n" + "Please install and run the ImageJ 3D Viewer."); + } catch (ClassNotFoundException e) { + IJ.showMessage("ImageJ 3D Viewer is not installed.\n" + + "Please install and run the ImageJ 3D Viewer."); return false; } if (!checkIJVersion()) @@ -247,16 +261,17 @@ public static boolean checkEnvironment() { /** * Check that IJ has enough memory to do the job - * + * * @param memoryRequirement * Estimated required memory * @return True if there is enough memory or if the user wants to continue. * False if the user wants to continue despite a risk of * insufficient memory */ - public static boolean checkMemory(final long memoryRequirement) { + public static boolean checkMemory(long memoryRequirement) { if (memoryRequirement > IJ.maxMemory()) { - final String message = "You might not have enough memory to run this job.\n" + "Do you want to continue?"; + String message = "You might not have enough memory to run this job.\n" + + "Do you want to continue?"; if (IJ.showMessageWithCancel("Memory Warning", message)) { return true; } else { @@ -267,8 +282,9 @@ public static boolean checkMemory(final long memoryRequirement) { } } - public static boolean checkMemory(final ImagePlus imp, final double ratio) { - double size = ((double) imp.getWidth() * imp.getHeight() * imp.getStackSize()); + public static boolean checkMemory(ImagePlus imp, double ratio) { + double size = ((double) imp.getWidth() * imp.getHeight() * imp + .getStackSize()); switch (imp.getType()) { case ImagePlus.GRAY8: case ImagePlus.COLOR_256: @@ -283,20 +299,21 @@ public static boolean checkMemory(final ImagePlus imp, final double ratio) { size *= 4.0; break; } - final long memoryRequirement = (long) (size * ratio); + long memoryRequirement = (long) (size * ratio); return checkMemory(memoryRequirement); } /** * Guess whether an image is Hounsfield unit calibrated - * + * * @param imp * @return true if the image might be HU calibrated */ - public static boolean huCalibrated(final ImagePlus imp) { - final Calibration cal = imp.getCalibration(); - final double[] coeff = cal.getCoefficients(); - if (!cal.calibrated() || cal == null || (cal.getCValue(0) == 0 && coeff[1] == 1) + public static boolean huCalibrated(ImagePlus imp) { + Calibration cal = imp.getCalibration(); + double[] coeff = cal.getCoefficients(); + if (!cal.calibrated() || cal == null + || (cal.getCValue(0) == 0 && coeff[1] == 1) || (cal.getCValue(0) == Short.MIN_VALUE && coeff[1] == 1)) { return false; } else @@ -305,11 +322,11 @@ public static boolean huCalibrated(final ImagePlus imp) { /** * Check if the version of IJ has been blacklisted as a known broken release - * + * * @return true if the IJ version is blacklisted, false otherwise */ public static boolean isIJVersionBlacklisted() { - for (final String version : blacklistedIJVersions) { + for (String version : blacklistedIJVersions) { if (version.equals(IJ.getVersion())) return true; } diff --git a/test/org/doube/util/ImageCheckTest.java b/test/org/doube/util/ImageCheckTest.java new file mode 100644 index 00000000..04d8682f --- /dev/null +++ b/test/org/doube/util/ImageCheckTest.java @@ -0,0 +1,51 @@ +package org.doube.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; + +import ij.ImagePlus; +import ij.measure.Calibration; + +/** + * Unit tests for the org.doube.util.ImageCheck class + * + * Richard Domander + */ +public class ImageCheckTest { + @Test + public void testIsVoxelIsotropicReturnsFalseIfImageIsNull() throws Exception { + boolean result = ImageCheck.isVoxelIsotropic(null); + assertFalse("Null image should not be isotropic", result); + } + + @Test + public void testIsVoxelIsotropic() throws Exception { + ImagePlus testImage = mock(ImagePlus.class); + Calibration anisotropicCalibration = new Calibration(); + + // 2D anisotropic image with 0 tolerance + anisotropicCalibration.pixelWidth = 2; + anisotropicCalibration.pixelHeight = 1; + + when(testImage.getCalibration()).thenReturn(anisotropicCalibration); + when(testImage.getStackSize()).thenReturn(1); + + boolean result = ImageCheck.isVoxelIsotropic(testImage, 0.0); + assertFalse("Image where width > height should not be isotropic", result); + + // 2D image where anisotropy is within tolerance + result = ImageCheck.isVoxelIsotropic(testImage, 1.0); + assertTrue("Image should be isotropic if anisotropy is within tolerance", result); + + // 3D image where depth anisotropy is beyond tolerance + anisotropicCalibration.pixelDepth = 1000; + when(testImage.getStackSize()).thenReturn(100); + + result = ImageCheck.isVoxelIsotropic(testImage, 1.0); + assertFalse("Pixel depth too great to be anisotropic within tolerance", result); + } +} \ No newline at end of file