diff --git a/pom.xml b/pom.xml index 550cad5..b0dfa83 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ org.scijava pom-scijava - 27.0.1 + 28.0.0 de.csbdresden StarDist_ - 0.1.0 + 0.2.0 StarDist StarDist - Object Detection with Star-convex Shapes @@ -69,6 +69,16 @@ https://github.com/frauzufall reviewer + + Olivier Burri + https://github.com/lacan + developer + + + Romain Guiet + https://github.com/romainGuiet + developer + @@ -79,14 +89,14 @@ - scm:git:git://github.com/uschmidt83/stardist-imagej - scm:git:git://github.com/uschmidt83/stardist-imagej + scm:git:git://github.com/mpicbg-csbd/stardist-imagej + scm:git:git://github.com/mpicbg-csbd/stardist-imagej HEAD - https://github.com/uschmidt83/stardist-imagej + https://github.com/mpicbg-csbd/stardist-imagej GitHub Issues - https://github.com/uschmidt83/stardist-imagej/issues + https://github.com/mpicbg-csbd/stardist-imagej/issues None diff --git a/src/main/java/de/csbdresden/stardist/Candidates.java b/src/main/java/de/csbdresden/stardist/Candidates.java index 97eacc5..951656c 100644 --- a/src/main/java/de/csbdresden/stardist/Candidates.java +++ b/src/main/java/de/csbdresden/stardist/Candidates.java @@ -12,6 +12,9 @@ import de.lighti.clipper.Path; import de.lighti.clipper.Paths; import de.lighti.clipper.Point.LongPoint; +import ij.gui.PointRoi; +import ij.gui.PolygonRoi; +import ij.gui.Roi; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessibleInterval; import net.imglib2.type.numeric.real.FloatType; @@ -29,6 +32,9 @@ public class Candidates { private final boolean[] suppressed; private final boolean verbose; private final LogService log; + + // scale all coordinates by this value and divide later to get subpixel resolution + private static final long S = 100; public Candidates(RandomAccessibleInterval prob, RandomAccessibleInterval dist) { this(prob, dist, 0.4); @@ -65,8 +71,8 @@ public Candidates(RandomAccessibleInterval prob, RandomAccessibleInte for (int k = 0; k < nrays; k++) { s.setPosition(k, 2); FloatType d = s.get(); - long x = Math.round(i + d.getRealDouble() * Math.cos(phis[k])); - long y = Math.round(j + d.getRealDouble() * Math.sin(phis[k])); + long x = Math.round(S * (i + d.getRealDouble() * Math.cos(phis[k]))); + long y = Math.round(S * (j + d.getRealDouble() * Math.sin(phis[k]))); xmin = Math.min(xmin,x); ymin = Math.min(ymin,y); xmax = Math.max(xmax,x); @@ -75,7 +81,7 @@ public Candidates(RandomAccessibleInterval prob, RandomAccessibleInte } polygons.add(poly); bboxes.add(new Box2D(xmin,xmax,ymin,ymax)); - origins.add(new Point2D(i,j)); + origins.add(new Point2D(S*i,S*j)); scores.add(score); areas.add(poly.area()); } @@ -165,24 +171,36 @@ public List getSorted() { return score_indices; } - public Point2D getOrigin(int i) { - return origins.get(i); - } +// public Point2D getOrigin(int i) { +// return origins.get(i); +// } - public Path getPolygon(int i) { - return polygons.get(i); - } +// public Path getPolygon(int i) { +// return polygons.get(i); +// } - public Box2D getBbox(int i) { - return bboxes.get(i); - } +// public Box2D getBbox(int i) { +// return bboxes.get(i); +// } - public float getScore(int i) { - return scores.get(i); - } +// public float getScore(int i) { +// return scores.get(i); +// } - public double getArea(int i) { - return areas.get(i); +// public double getArea(int i) { +// return areas.get(i); +// } + + public PolygonRoi getPolygonRoi(int i) { + return Utils.toPolygonRoi(polygons.get(i), S); + } + + public PointRoi getOriginRoi(int i) { + return Utils.toPointRoi(origins.get(i), S); + } + + public Roi getBboxRoi(int i) { + return Utils.toBoxRoi(bboxes.get(i), S); } diff --git a/src/main/java/de/csbdresden/stardist/Opt.java b/src/main/java/de/csbdresden/stardist/Opt.java index 69864d9..7e061dd 100644 --- a/src/main/java/de/csbdresden/stardist/Opt.java +++ b/src/main/java/de/csbdresden/stardist/Opt.java @@ -29,6 +29,10 @@ public class Opt { static final String NUM_TILES = "Number of Tiles"; static final String EXCLUDE_BNDRY = "Boundary Exclusion"; + static final String ROI_POSITION = "ROI Position"; + static final String ROI_POSITION_AUTO = "Automatic"; + static final String ROI_POSITION_STACK = "Stack"; + static final String ROI_POSITION_HYPERSTACK = "Hyperstack"; static final String VERBOSE = "Verbose"; static final String CSBDEEP_PROGRESS_WINDOW = "Show CNN Progress"; static final String SHOW_PROB_DIST = "Show CNN Output"; @@ -48,6 +52,7 @@ public class Opt { DEFAULTS.put(OUTPUT_TYPE, OUTPUT_BOTH); DEFAULTS.put(NUM_TILES, 1); DEFAULTS.put(EXCLUDE_BNDRY, 2); + DEFAULTS.put(ROI_POSITION, ROI_POSITION_AUTO); DEFAULTS.put(VERBOSE, false); DEFAULTS.put(CSBDEEP_PROGRESS_WINDOW, false); DEFAULTS.put(SHOW_PROB_DIST, false); diff --git a/src/main/java/de/csbdresden/stardist/StarDist2D.java b/src/main/java/de/csbdresden/stardist/StarDist2D.java index d076778..35289a5 100644 --- a/src/main/java/de/csbdresden/stardist/StarDist2D.java +++ b/src/main/java/de/csbdresden/stardist/StarDist2D.java @@ -3,6 +3,7 @@ import static de.csbdresden.stardist.StarDist2DModel.MODELS; import static de.csbdresden.stardist.StarDist2DModel.MODEL_DSB2018_HEAVY_AUGMENTATION; import static de.csbdresden.stardist.StarDist2DModel.MODEL_DSB2018_PAPER; +import static de.csbdresden.stardist.StarDist2DModel.MODEL_HE_HEAVY_AUGMENTATION; import java.io.File; import java.io.IOException; @@ -54,7 +55,7 @@ public class StarDist2D extends StarDist2DBase implements Command { private final String msgTitle = "" + "
" + "

Object Detection with Star-convex Shapes

" + - "https://github.com/mpicbg-csbd/stardist" + + "https://imagej.net/StarDist" + "

Please cite our paper if StarDist was helpful for your research. Thanks!" + "
  " + "
" + @@ -70,6 +71,7 @@ public class StarDist2D extends StarDist2DBase implements Command { @Parameter(label=Opt.MODEL, choices={MODEL_DSB2018_HEAVY_AUGMENTATION, + MODEL_HE_HEAVY_AUGMENTATION, MODEL_DSB2018_PAPER, Opt.MODEL_FILE, Opt.MODEL_URL}, style=ChoiceWidget.LIST_BOX_STYLE) @@ -83,7 +85,7 @@ public class StarDist2D extends StarDist2DBase implements Command { @Parameter(label=Opt.PERCENTILE_HIGH, stepSize="0.1", min="0", max="100", style=NumberWidget.SLIDER_STYLE, callback="percentileTopChanged") private double percentileTop = (double) Opt.getDefault(Opt.PERCENTILE_HIGH); - + @Parameter(label=Opt.PROB_IMAGE, type=ItemIO.OUTPUT) private Dataset prob; @@ -123,16 +125,20 @@ public class StarDist2D extends StarDist2DBase implements Command { @Parameter(label=Opt.EXCLUDE_BNDRY, min="0", stepSize="1") private int excludeBoundary = (int) Opt.getDefault(Opt.EXCLUDE_BNDRY); + + @Parameter(label=Opt.ROI_POSITION, choices={Opt.ROI_POSITION_AUTO, Opt.ROI_POSITION_STACK, Opt.ROI_POSITION_HYPERSTACK}, style=ChoiceWidget.RADIO_BUTTON_HORIZONTAL_STYLE) + private String roiPosition = (String) Opt.getDefault(Opt.ROI_POSITION); + private String roiPositionActive = null; @Parameter(label=Opt.VERBOSE) private boolean verbose = (boolean) Opt.getDefault(Opt.VERBOSE); - + @Parameter(label=Opt.CSBDEEP_PROGRESS_WINDOW) private boolean showCsbdeepProgress = (boolean) Opt.getDefault(Opt.CSBDEEP_PROGRESS_WINDOW); - + @Parameter(label=Opt.SHOW_PROB_DIST) private boolean showProbAndDist = (boolean) Opt.getDefault(Opt.SHOW_PROB_DIST); - + // TODO: values for block multiple and overlap @Parameter(label=Opt.SET_THRESHOLDS, callback="setThresholds") @@ -153,6 +159,7 @@ private void restoreDefaults() { outputType = (String) Opt.getDefault(Opt.OUTPUT_TYPE); nTiles = (int) Opt.getDefault(Opt.NUM_TILES); excludeBoundary = (int) Opt.getDefault(Opt.EXCLUDE_BNDRY); + roiPosition = (String) Opt.getDefault(Opt.ROI_POSITION); verbose = (boolean) Opt.getDefault(Opt.VERBOSE); showCsbdeepProgress = (boolean) Opt.getDefault(Opt.CSBDEEP_PROGRESS_WINDOW); showProbAndDist = (boolean) Opt.getDefault(Opt.SHOW_PROB_DIST); @@ -165,7 +172,7 @@ private void percentileBottomChanged() { private void percentileTopChanged() { percentileBottom = Math.min(percentileBottom, percentileTop); } - + private void setThresholds() { switch (modelChoice) { case Opt.MODEL_FILE: @@ -203,7 +210,12 @@ private void checkForCSBDeep() { public void run() { checkForCSBDeep(); if (!checkInputs()) return; - + + if (roiPosition.equals(Opt.ROI_POSITION_AUTO)) + roiPositionActive = input.numDimensions() > 3 ? Opt.ROI_POSITION_HYPERSTACK : Opt.ROI_POSITION_STACK; + else + roiPositionActive = roiPosition; + File tmpModelFile = null; try { final HashMap paramsCNN = new HashMap<>(); @@ -238,13 +250,14 @@ public void run() { paramsCNN.put("blockMultiple", pretrainedModel.sizeDivBy); paramsCNN.put("overlap", pretrainedModel.tileOverlap); } - + final HashMap paramsNMS = new HashMap<>(); paramsNMS.put("probThresh", probThresh); paramsNMS.put("nmsThresh", nmsThresh); paramsNMS.put("excludeBoundary", excludeBoundary); - paramsNMS.put("verbose", verbose); - + paramsNMS.put("roiPosition", roiPositionActive); + paramsNMS.put("verbose", verbose); + final LinkedHashSet inputAxes = Utils.orderedAxesSet(input); final boolean isTimelapse = inputAxes.contains(Axes.TIME); @@ -276,11 +289,12 @@ public void run() { final Future futureNMS = command.run(StarDist2DNMS.class, false, paramsNMS); final Candidates polygons = (Candidates) futureNMS.get().getOutput("polygons"); - export(outputType, polygons, 1+t); - + export(outputType, polygons, 1+t, numFrames, roiPositionActive); + status.showProgress(1+t, (int)numFrames); } - label = labelImageToDataset(outputType); + label = labelImageToDataset(outputType); + // if (roiManager != null) OverlayCommands.listRois(roiManager.getRoisAsArray()); } else { // note: the code below supports timelapse data too. differences to above: @@ -289,7 +303,7 @@ public void run() { // - allows showing prob and dist easily final Future futureCNN = command.run(de.csbdresden.csbdeep.commands.GenericNetwork.class, false, paramsCNN); final Dataset prediction = (Dataset) futureCNN.get().getOutput("output"); - + final Pair probAndDist = splitPrediction(prediction); final Dataset probDS = probAndDist.getA(); final Dataset distDS = probAndDist.getB(); @@ -318,7 +332,7 @@ public void run() { } } } - + // this function is very cumbersome... is there a better way to do this? private Pair splitPrediction(final Dataset prediction) { final RandomAccessibleInterval predictionRAI = (RandomAccessibleInterval) prediction.getImgPlus(); @@ -331,7 +345,7 @@ private Pair splitPrediction(final Dataset prediction) { final long[] predSize = predAxes.stream().mapToLong(axis -> { return axis == Axes.CHANNEL ? prediction.dimension(axis)-1 : prediction.dimension(axis); }).toArray(); - + final RandomAccessibleInterval probRAI = Views.hyperSlice(predictionRAI, predChannelDim, 0); final RandomAccessibleInterval distRAI = Views.offsetInterval(predictionRAI, predStart, predSize); @@ -349,23 +363,23 @@ private boolean checkInputs() { (input.numDimensions() == 3 && axes.containsAll(Arrays.asList(Axes.X, Axes.Y, Axes.CHANNEL))) || (input.numDimensions() == 4 && axes.containsAll(Arrays.asList(Axes.X, Axes.Y, Axes.CHANNEL, Axes.TIME))) )) return showError("Input must be a 2D image or timelapse (with or without channels)."); - - if (!( modelChoice.equals(Opt.MODEL_FILE) || modelChoice.equals(Opt.MODEL_URL) || MODELS.containsKey(modelChoice) )) + + if (!( modelChoice.equals(Opt.MODEL_FILE) || modelChoice.equals(Opt.MODEL_URL) || MODELS.containsKey(modelChoice) )) return showError(String.format("Unsupported Model \"%s\".", modelChoice)); - + return true; } - - + + @Override protected void exportPolygons(Candidates polygons) {} - + @Override protected ImagePlus createLabelImage() { - return IJ.createImage("Labeling", "16-bit black", (int)input.getWidth(), (int)input.getHeight(), 1, 1, (int)input.getFrames()); + return IJ.createImage(Opt.LABEL_IMAGE, "16-bit black", (int)input.getWidth(), (int)input.getHeight(), 1, 1, (int)input.getFrames()); } - + public static void main(final String... args) throws Exception { final ImageJ ij = new ImageJ(); diff --git a/src/main/java/de/csbdresden/stardist/StarDist2DBase.java b/src/main/java/de/csbdresden/stardist/StarDist2DBase.java index 3b805f8..21d5cc7 100644 --- a/src/main/java/de/csbdresden/stardist/StarDist2DBase.java +++ b/src/main/java/de/csbdresden/stardist/StarDist2DBase.java @@ -24,7 +24,7 @@ import net.imglib2.img.display.imagej.ImageJFunctions; public abstract class StarDist2DBase { - + @Parameter protected LogService log; @@ -36,12 +36,12 @@ public abstract class StarDist2DBase { @Parameter protected DatasetService dataset; - + @Parameter protected StatusService status; - + // --------- - + protected boolean exportPointRois = false; protected boolean exportBboxRois = false; @@ -50,77 +50,85 @@ public abstract class StarDist2DBase { protected int labelId = 0; protected long labelCount = 0; protected static final int MAX_LABEL_ID = 65535; - + // --------- - + protected URL getResource(final String name) { return StarDist2DBase.class.getClassLoader().getResource(name); - } - + } + protected boolean showError(String msg) { ui.showDialog(msg, MessageType.ERROR_MESSAGE); // log.error(msg); return false; } - + // --------- - - protected void export(String outputType, Candidates polygons, int framePosition) { + + protected void export(String outputType, Candidates polygons, int framePosition, long numFrames, String roiPosition) { switch (outputType) { case Opt.OUTPUT_ROI_MANAGER: - exportROIs(polygons, framePosition); + exportROIs(polygons, framePosition, numFrames, roiPosition); break; case Opt.OUTPUT_LABEL_IMAGE: exportLabelImage(polygons, framePosition); break; case Opt.OUTPUT_BOTH: - exportROIs(polygons, framePosition); + exportROIs(polygons, framePosition, numFrames, roiPosition); exportLabelImage(polygons, framePosition); break; case Opt.OUTPUT_POLYGONS: exportPolygons(polygons); break; default: - showError(String.format("Unknown output type \"%s\"", outputType)); + showError(String.format("Invalid %s \"%s\"", Opt.OUTPUT_TYPE, outputType)); } } - - protected void exportROIs(Candidates polygons, int framePosition) { + + protected void exportROIs(Candidates polygons, int framePosition, long numFrames, String roiPosition) { if (roiManager == null) { roiManager = RoiManager.getRoiManager(); roiManager.reset(); // clear all rois + // https://github.com/mpicbg-csbd/stardist-imagej/pull/5: + // when setting the RoiManager to invisible, the position of the ROI will be properly saved + // -> the issue is in RoiManager.addRoi(), https://github.com/imagej/imagej1/blob/c4950ee1f19a25828e5ac915ef3f74e5aa13a6e2/ij/plugin/frame/RoiManager.java#L419 + roiManager.setVisible(false); } - // Setting the RoiManager to invisible, the position of the ROI will be properly saved. - // The issue is in RoiManager.addRoi(), https://github.com/imagej/imagej1/blob/c4950ee1f19a25828e5ac915ef3f74e5aa13a6e2/ij/plugin/frame/RoiManager.java#L419 - roiManager.setVisible(false); - for (final int i : polygons.getWinner()) { - final PolygonRoi polyRoi = Utils.toPolygonRoi(polygons.getPolygon(i)); - if (framePosition > 0) polyRoi.setPosition(1, 1, framePosition); - //if (framePosition > 0) polyRoi.setPosition(framePosition); - roiManager.addRoi(polyRoi); + final PolygonRoi polyRoi = polygons.getPolygonRoi(i); + if (framePosition > 0) setRoiPosition(polyRoi, framePosition, roiPosition); + roiManager.add(polyRoi, -1); if (exportPointRois) { - final Point2D o = polygons.getOrigin(i); - final PointRoi pointRoi = new PointRoi(o.x, o.y); - if (framePosition > 0) pointRoi.setPosition(1, 1, framePosition); - //if (framePosition > 0) pointRoi.setPosition(framePosition); - roiManager.addRoi(pointRoi); + final PointRoi pointRoi = polygons.getOriginRoi(i); + if (framePosition > 0) setRoiPosition(pointRoi, framePosition, roiPosition); + roiManager.add(pointRoi, -1); } if (exportBboxRois) { - final Box2D bbox = polygons.getBbox(i); - final Roi bboxRoi = new Roi(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin); - if (framePosition > 0) bboxRoi.setPosition(1, 1, framePosition); - //if (framePosition > 0) bboxRoi.setPosition(framePosition); - roiManager.addRoi(bboxRoi); + final Roi bboxRoi = polygons.getBboxRoi(i); + if (framePosition > 0) setRoiPosition(bboxRoi, framePosition, roiPosition); + roiManager.add(bboxRoi, -1); } } - //Setting the RoiManager back to visible at the end of the processing - roiManager.setVisible(true); - + // make the RoiManager visible after adding the ROIs + if (framePosition == 0 || framePosition == numFrames) + roiManager.setVisible(true); + } + + protected void setRoiPosition(Roi roi, int framePosition, String roiPosition) { + switch (roiPosition) { + case Opt.ROI_POSITION_STACK: + roi.setPosition(framePosition); + break; + case Opt.ROI_POSITION_HYPERSTACK: + roi.setPosition(1, 1, framePosition); + break; + default: + showError(String.format("Invalid %s \"%s\"", Opt.ROI_POSITION, roiPosition)); + } } - + protected void exportLabelImage(Candidates polygons, int framePosition) { if (labelImage == null) labelImage = createLabelImage(); @@ -131,18 +139,18 @@ protected void exportLabelImage(Candidates polygons, int framePosition) { final int numWinners = winner.size(); // winners are ordered by score -> draw from last to first to give priority to higher scores in case of overlaps for (int i = numWinners-1; i >= 0; i--) { - final PolygonRoi polyRoi = Utils.toPolygonRoi(polygons.getPolygon(winner.get(i))); + final PolygonRoi polyRoi = polygons.getPolygonRoi(winner.get(i)); ip.setColor(1 + ((labelId + i) % MAX_LABEL_ID)); ip.fill(polyRoi); } labelCount += numWinners; labelId = (labelId + numWinners) % MAX_LABEL_ID; - } - - abstract protected void exportPolygons(Candidates polygons); - + } + + abstract protected void exportPolygons(Candidates polygons); + abstract protected ImagePlus createLabelImage(); - + protected Dataset labelImageToDataset(String outputType) { if (outputType.equals(Opt.OUTPUT_LABEL_IMAGE) || outputType.equals(Opt.OUTPUT_BOTH)) { if (labelCount > MAX_LABEL_ID) { diff --git a/src/main/java/de/csbdresden/stardist/StarDist2DModel.java b/src/main/java/de/csbdresden/stardist/StarDist2DModel.java index 81b8b25..0a84ec9 100644 --- a/src/main/java/de/csbdresden/stardist/StarDist2DModel.java +++ b/src/main/java/de/csbdresden/stardist/StarDist2DModel.java @@ -14,12 +14,14 @@ public class StarDist2DModel { static final String MODEL_DSB2018_HEAVY_AUGMENTATION = "Versatile (fluorescent nuclei)"; static final String MODEL_DSB2018_PAPER = "DSB 2018 (from StarDist 2D paper)"; - static final String MODEL_DEFAULT = MODEL_DSB2018_HEAVY_AUGMENTATION; + static final String MODEL_HE_HEAVY_AUGMENTATION = "Versatile (H&E nuclei)"; + static final String MODEL_DEFAULT = MODEL_DSB2018_HEAVY_AUGMENTATION; static final Map MODELS = new LinkedHashMap(); static { - MODELS.put(MODEL_DSB2018_PAPER, new StarDist2DModel(StarDist2DModel.class.getClassLoader().getResource("models/2D/dsb2018_paper.zip"), 0.417819, 0.5, 8, 47)); - MODELS.put(MODEL_DSB2018_HEAVY_AUGMENTATION, new StarDist2DModel(StarDist2DModel.class.getClassLoader().getResource("models/2D/dsb2018_heavy_augment.zip"), 0.479071, 0.3, 16, 94)); + MODELS.put(MODEL_DSB2018_PAPER, new StarDist2DModel(StarDist2DModel.class.getClassLoader().getResource("models/2D/dsb2018_paper.zip"), 0.417819, 0.5, 8, 48)); + MODELS.put(MODEL_DSB2018_HEAVY_AUGMENTATION, new StarDist2DModel(StarDist2DModel.class.getClassLoader().getResource("models/2D/dsb2018_heavy_augment.zip"), 0.479071, 0.3, 16, 96)); + MODELS.put(MODEL_HE_HEAVY_AUGMENTATION, new StarDist2DModel(StarDist2DModel.class.getClassLoader().getResource("models/2D/he_heavy_augment.zip"), 0.692478, 0.3, 16, 96)); } // ----------- diff --git a/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java b/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java index 525cbb5..f50bec0 100644 --- a/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java +++ b/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java @@ -32,9 +32,9 @@ @Menu(label = "StarDist"), @Menu(label = "Other"), @Menu(label = "StarDist 2D NMS (postprocessing only)", weight = 2) -}) +}) public class StarDist2DNMS extends StarDist2DBase implements Command { - + @Parameter(label=Opt.PROB_IMAGE) private Dataset prob; @@ -55,7 +55,7 @@ public class StarDist2DNMS extends StarDist2DBase implements Command { @Parameter(label=Opt.OUTPUT_TYPE, choices={Opt.OUTPUT_ROI_MANAGER, Opt.OUTPUT_LABEL_IMAGE, Opt.OUTPUT_BOTH}, style=ChoiceWidget.RADIO_BUTTON_HORIZONTAL_STYLE) private String outputType = (String) Opt.getDefault(Opt.OUTPUT_TYPE); - + // --------- @Parameter(visibility=ItemVisibility.MESSAGE) @@ -63,13 +63,16 @@ public class StarDist2DNMS extends StarDist2DBase implements Command { @Parameter(label=Opt.EXCLUDE_BNDRY, min="0", stepSize="1") private int excludeBoundary = (int) Opt.getDefault(Opt.EXCLUDE_BNDRY); + + @Parameter(label=Opt.ROI_POSITION, choices={Opt.ROI_POSITION_STACK, Opt.ROI_POSITION_HYPERSTACK}, style=ChoiceWidget.RADIO_BUTTON_HORIZONTAL_STYLE) + private String roiPosition = (String) Opt.getDefault(Opt.ROI_POSITION); @Parameter(label=Opt.VERBOSE) private boolean verbose = (boolean) Opt.getDefault(Opt.VERBOSE); @Parameter(label=Opt.RESTORE_DEFAULTS, callback="restoreDefaults") private Button restoreDefaults; - + // --------- private void restoreDefaults() { @@ -77,6 +80,7 @@ private void restoreDefaults() { nmsThresh = (double) Opt.getDefault(Opt.NMS_THRESH); outputType = (String) Opt.getDefault(Opt.OUTPUT_TYPE); excludeBoundary = (int) Opt.getDefault(Opt.EXCLUDE_BNDRY); + roiPosition = (String) Opt.ROI_POSITION_STACK; verbose = (boolean) Opt.getDefault(Opt.VERBOSE); } @@ -88,7 +92,7 @@ public void run() { final RandomAccessibleInterval probRAI = (RandomAccessibleInterval) prob.getImgPlus(); final RandomAccessibleInterval distRAI = (RandomAccessibleInterval) dist.getImgPlus(); - + final LinkedHashSet probAxes = Utils.orderedAxesSet(prob); final LinkedHashSet distAxes = Utils.orderedAxesSet(dist); final boolean isTimelapse = probAxes.contains(Axes.TIME); @@ -103,78 +107,81 @@ public void run() { polygons.nms(nmsThresh); if (verbose) log.info(String.format("frame %03d: %d polygon candidates, %d remain after non-maximum suppression", t, polygons.getSorted().size(), polygons.getWinner().size())); - export(outputType, polygons, 1+t); + export(outputType, polygons, 1+t, numFrames, roiPosition); } } else { final Candidates polygons = new Candidates(probRAI, distRAI, probThresh, excludeBoundary, verbose ? log : null); polygons.nms(nmsThresh); if (verbose) log.info(String.format("%d polygon candidates, %d remain after non-maximum suppression", polygons.getSorted().size(), polygons.getWinner().size())); - export(outputType, polygons, 0); + export(outputType, polygons, 0, 0, roiPosition); } - + label = labelImageToDataset(outputType); // call at the end of the run() method CommandFromMacro.record(this, this.command); } - + private boolean checkInputs() { final LinkedHashSet probAxes = Utils.orderedAxesSet(prob); final LinkedHashSet distAxes = Utils.orderedAxesSet(dist); - + if (!( (prob.numDimensions() == 2 && probAxes.containsAll(Arrays.asList(Axes.X, Axes.Y))) || (prob.numDimensions() == 3 && probAxes.containsAll(Arrays.asList(Axes.X, Axes.Y, Axes.TIME))) )) - return showError("Probability/Score must be a 2D image or timelapse."); + return showError(String.format("%s must be a 2D image or timelapse.", Opt.PROB_IMAGE)); if (!( (dist.numDimensions() == 3 && distAxes.containsAll(Arrays.asList(Axes.X, Axes.Y, Axes.CHANNEL)) && dist.getChannels() >= 3) || (dist.numDimensions() == 4 && distAxes.containsAll(Arrays.asList(Axes.X, Axes.Y, Axes.CHANNEL, Axes.TIME)) && dist.getChannels() >= 3) )) - return showError("Distance must be a 2D image or timelapse with at least three channels."); + return showError(String.format("%s must be a 2D image or timelapse with at least three channels.", Opt.DIST_IMAGE)); if ((prob.numDimensions() + 1) != dist.numDimensions()) - return showError("Axes of Probability/Score and Distance not compatible."); + return showError(String.format("Axes of %s and %s not compatible.", Opt.PROB_IMAGE, Opt.DIST_IMAGE)); if (prob.getWidth() != dist.getWidth() || prob.getHeight() != dist.getHeight()) - return showError("Width or height of Probability/Score and Distance differ."); + return showError(String.format("Width or height of %s and %s differ.", Opt.PROB_IMAGE, Opt.DIST_IMAGE)); if (prob.getFrames() != dist.getFrames()) - return showError("Number of frames of Probability/Score and Distance differ."); - + return showError(String.format("Number of frames of %s and %s differ.", Opt.PROB_IMAGE, Opt.DIST_IMAGE)); + final AxisType[] probAxesArray = probAxes.stream().toArray(AxisType[]::new); final AxisType[] distAxesArray = distAxes.stream().toArray(AxisType[]::new); if (!( probAxesArray[0] == Axes.X && probAxesArray[1] == Axes.Y )) - return showError("First two axes of Probability/Score must be a X and Y."); + return showError(String.format("First two axes of %s must be a X and Y.", Opt.PROB_IMAGE)); if (!( distAxesArray[0] == Axes.X && distAxesArray[1] == Axes.Y )) - return showError("First two axes of Distance must be a X and Y."); + return showError(String.format("First two axes of %s must be a X and Y.", Opt.DIST_IMAGE)); if (!(0 <= nmsThresh && nmsThresh <= 1)) - return showError("NMS Threshold must be between 0 and 1."); + return showError(String.format("%s must be between 0 and 1.", Opt.NMS_THRESH)); if (excludeBoundary < 0) - return showError("Boundary Exclusion must be >= 0"); + return showError(String.format("%s must be >= 0", Opt.EXCLUDE_BNDRY)); if (!(outputType.equals(Opt.OUTPUT_ROI_MANAGER) || outputType.equals(Opt.OUTPUT_LABEL_IMAGE) || outputType.equals(Opt.OUTPUT_BOTH) || outputType.equals(Opt.OUTPUT_POLYGONS))) - return showError(String.format("Output Type must be one of {\"%s\", \"%s\", \"%s\"}.", Opt.OUTPUT_ROI_MANAGER, Opt.OUTPUT_LABEL_IMAGE, Opt.OUTPUT_BOTH)); - + return showError(String.format("%s must be one of {\"%s\", \"%s\", \"%s\"}.", Opt.OUTPUT_TYPE, Opt.OUTPUT_ROI_MANAGER, Opt.OUTPUT_LABEL_IMAGE, Opt.OUTPUT_BOTH)); + if (outputType.equals(Opt.OUTPUT_POLYGONS) && probAxes.contains(Axes.TIME)) - return showError(String.format("Timelapse not supported for output type \"%s\"", Opt.OUTPUT_POLYGONS)); + return showError(String.format("Timelapse not supported for output type \"%s\"", Opt.OUTPUT_POLYGONS)); + + if (!(roiPosition.equals(Opt.ROI_POSITION_STACK) || roiPosition.equals(Opt.ROI_POSITION_HYPERSTACK))) + return showError(String.format("%s must be one of {\"%s\", \"%s\"}.", Opt.ROI_POSITION, Opt.ROI_POSITION_STACK, Opt.ROI_POSITION_HYPERSTACK)); return true; } - + @Override protected void exportPolygons(Candidates polygons) { this.polygons = polygons; } - + @Override protected ImagePlus createLabelImage() { - return IJ.createImage("Labeling", "16-bit black", (int)prob.getWidth(), (int)prob.getHeight(), 1, 1, (int)prob.getFrames()); + return IJ.createImage(Opt.LABEL_IMAGE, "16-bit black", (int)prob.getWidth(), (int)prob.getHeight(), 1, 1, (int)prob.getFrames()); } - + public static void main(final String... args) throws Exception { final ImageJ ij = new ImageJ(); ij.launch(args); diff --git a/src/main/java/de/csbdresden/stardist/Utils.java b/src/main/java/de/csbdresden/stardist/Utils.java index 372da97..51eef2b 100644 --- a/src/main/java/de/csbdresden/stardist/Utils.java +++ b/src/main/java/de/csbdresden/stardist/Utils.java @@ -9,6 +9,7 @@ import de.lighti.clipper.Path; import de.lighti.clipper.Point.LongPoint; +import ij.gui.PointRoi; import ij.gui.PolygonRoi; import ij.gui.Roi; import net.imagej.Dataset; @@ -19,16 +20,28 @@ public class Utils { - public static PolygonRoi toPolygonRoi(Path poly) { + public static PolygonRoi toPolygonRoi(Path poly, float S) { int n = poly.size(); - int[] x = new int[n]; - int[] y = new int[n]; + float[] x = new float[n]; + float[] y = new float[n]; for (int i = 0; i < n; i++) { LongPoint p = poly.get(i); - x[i] = (int) p.getX(); - y[i] = (int) p.getY(); + x[i] = 0.5f + p.getX() / S; + y[i] = 0.5f + p.getY() / S; } - return new PolygonRoi(x,y,n,Roi.POLYGON); + return new PolygonRoi(x, y, n, Roi.POLYGON); + } + + public static PointRoi toPointRoi(Point2D o, float S) { + return new PointRoi(0.5f + o.x / S, 0.5f + o.y / S); + } + + public static Roi toBoxRoi(Box2D bbox, float S) { + double xmin = 0.5 + bbox.xmin / S; + double xmax = 0.5 + bbox.xmax / S; + double ymin = 0.5 + bbox.ymin / S; + double ymax = 0.5 + bbox.ymax / S; + return new Roi(xmin, ymin, xmax - xmin, ymax - ymin); } public static double[] rayAngles(int n) { diff --git a/src/main/resources/models/2D/he_heavy_augment.zip b/src/main/resources/models/2D/he_heavy_augment.zip new file mode 100644 index 0000000..f745bf8 Binary files /dev/null and b/src/main/resources/models/2D/he_heavy_augment.zip differ