diff --git a/src/main/java/qupath/ext/align/gui/ImageAlignmentPane.java b/src/main/java/qupath/ext/align/gui/ImageAlignmentPane.java index 180e9a7..b0ff3bb 100644 --- a/src/main/java/qupath/ext/align/gui/ImageAlignmentPane.java +++ b/src/main/java/qupath/ext/align/gui/ImageAlignmentPane.java @@ -114,6 +114,7 @@ import qupath.lib.objects.PathObject; import qupath.lib.objects.classes.PathClass; import qupath.lib.objects.classes.PathClassFactory; +import qupath.lib.objects.hierarchy.PathObjectHierarchy; import qupath.lib.projects.Project; import qupath.lib.projects.ProjectImageEntry; import qupath.lib.regions.RegionRequest; @@ -406,6 +407,77 @@ public ImageAlignmentPane(final QuPathGUI qupath) { content.putString(s); Clipboard.getSystemClipboard().setContent(content); }); + Button btnImport = new Button("Propagate"); + btnImport.setOnAction(e -> { + ImageData imageDataBase = viewer.getImageData(); + ImageData imageDataSelected = selectedImageData.get(); + if (imageDataBase == null) { + Dialogs.showNoImageError("Auto-alignment"); + return; + } + if (imageDataSelected == null) { + Dialogs.showErrorMessage("Auto-alignment", "Please ensure an image overlay is selected!"); + return; + } + if (imageDataBase == imageDataSelected) { + Dialogs.showErrorMessage("Auto-alignment", "Please select an image overlay, not the 'base' image from the viewer!"); + return; + } + + var overlay = getSelectedOverlay(); + var transform = overlay == null ? null : overlay.getTransform(); + + if (transform == null) { + logger.warn("No transform found, can't import transformed annotations!"); + return; + } + + var hierarchy = imageDataBase.getHierarchy(); + + Project project = qupath.getProject(); + ProjectImageEntry selectedEntry = project.getEntry(imageDataSelected); + var otherHierarchy = imageDataSelected.getHierarchy(); //selectedEntry.readImageData().getHierarchy(); //readHierarchy(); + + for (var pathObject : otherHierarchy.getAnnotationObjects() ) { + logger.info("Destination: "+pathObject.getName()); + } + + //these are the annotations in the source + logger.info("Importing from "+imageDataBase.getServer().getPath()); + //logger.info("Destination to "+selectedEntry.getImageName()); + + List newObjects = new ArrayList<>(); + for (var pathObject : hierarchy.getAnnotationObjects() ) { + logger.info("Importing: "+pathObject.getName()); + //Transform ROI (via conversion to Java AWT shape) + newObjects.add(overlay.transformObject(pathObject)); + + //otherHierarchy.addPathObject(pathObject); + //otherHierarchy.addPathObject(newObject); + } + if (otherHierarchy != null) { + logger.info("Adding objects!"); + otherHierarchy.addPathObjects(newObjects); + try { + selectedEntry.saveImageData(imageDataSelected); + } catch (IOException e1) { + logger.error("Error saving hieararchy! "+ e1.getLocalizedMessage(), e1); + } + } + + for (var pathObject : otherHierarchy.getAnnotationObjects() ) { + logger.info("Destination: "+pathObject.getName()); + } + + + /* + for (var pathObject : otherHierarchy.getAnnotationObjects() ) { + logger.info("Destination: "+pathObject.getName()); + } + */ + logger.info("Importing done!"); + }); + btnReset.disableProperty().bind(noOverlay); btnReset.setTooltip(new Tooltip("Reset the transform")); btnInvert.disableProperty().bind(noOverlay); @@ -414,8 +486,10 @@ public ImageAlignmentPane(final QuPathGUI qupath) { btnUpdate.setTooltip(new Tooltip("Update the transform using the current text")); btnCopy.disableProperty().bind(noOverlay); btnCopy.setTooltip(new Tooltip("Copy the current transform to clipboard")); + btnImport.disableProperty().bind(noOverlay); + btnImport.setTooltip(new Tooltip("Propagate annotations from base image to selected")); textArea.editableProperty().bind(noOverlay.not()); - paneTransform.add(PaneTools.createColumnGridControls(btnUpdate, btnInvert, btnReset, btnCopy), 0, row++); + paneTransform.add(PaneTools.createColumnGridControls(btnUpdate, btnInvert, btnReset, btnCopy, btnImport), 0, row++); PaneTools.setFillWidth(Boolean.TRUE, paneTransform.getChildren().toArray(Node[]::new)); PaneTools.setHGrowPriority(Priority.ALWAYS, paneTransform.getChildren().toArray(Node[]::new)); paneTransform.setVgap(5.0); diff --git a/src/main/java/qupath/ext/align/gui/ImageServerOverlay.java b/src/main/java/qupath/ext/align/gui/ImageServerOverlay.java index c4ff3c3..efa6205 100644 --- a/src/main/java/qupath/ext/align/gui/ImageServerOverlay.java +++ b/src/main/java/qupath/ext/align/gui/ImageServerOverlay.java @@ -25,6 +25,7 @@ import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +42,22 @@ import qupath.lib.images.servers.ImageServer; import qupath.lib.regions.ImageRegion; +import qupath.lib.roi.RoiTools; +import qupath.lib.roi.PointsROI; +import qupath.lib.roi.ROIs; +import qupath.lib.objects.PathObject; +import qupath.lib.objects.PathObjects; +import qupath.lib.roi.interfaces.ROI; + + +import qupath.lib.objects.PathTileObject; +import qupath.lib.objects.PathCellObject; +import qupath.lib.objects.PathDetectionObject; +import java.util.List; +import java.util.Arrays; +import java.util.ArrayList; +import qupath.lib.geom.Point2; + /** * A {@link PathOverlay} implementation capable of painting one image on top of another, * including an optional affine transformation. @@ -116,6 +133,15 @@ public Affine getAffine() { return affine; } + /** + * Get the affine transform applied to the overlay image. + * Making changes here will trigger repaints in the viewer. + * @return + */ + public AffineTransform getTransform() { + return transform; + } + private void updateTransform() { transform.setTransform( affine.getMxx(), @@ -158,5 +184,62 @@ public void paintOverlay(Graphics2D g2d, ImageRegion imageRegion, double downsam gCopy.dispose(); } + + /** + * Transform object, recursively transforming all child objects + * + * @param pathObject + * @return + */ + public PathObject transformObject(PathObject pathObject) { + // Create a new object with the converted ROI + var roi = pathObject.getROI(); + var roi2 = this.transformROI(roi, transform); + + PathObject newObject = null; + + newObject = PathObjects.createAnnotationObject(roi2, pathObject.getPathClass(), pathObject.getMeasurementList()); + newObject.setName(pathObject.getName()); + + return newObject; + } + + /** + * Transform ROI (via conversion to Java AWT shape) + * + * @param roi + * @param transform + * @return + */ + private ROI transformROI(ROI roi, AffineTransform transform) { + if (roi.getRoiType() == ROI.RoiType.POINT) { + List points = roi.getAllPoints(); + var nPoints = points.size(); + + // Convert List to Point2D[] + Point2D[] pointsArray = points.stream() + .map(point -> new Point2D.Double(point.getX(), point.getY())) + .toArray(Point2D[]::new); + + Point2D[] points2 = new Point2D[nPoints]; + transform.transform(pointsArray,0,points2,0,nPoints); + + // Create a list to store Point2 objects + List pointsList = new ArrayList<>(nPoints); + + // Add Point2 objects to the list + for (int i = 0; i < nPoints; i++) { + Point2D point = points2[i]; + // Create a new Point2 object and add it to the list + pointsList.add(new Point2(point.getX(), point.getY())); + } + return ROIs.createPointsROI(pointsList, roi.getImagePlane()); + } else { + var shape = RoiTools.getShape(roi); // Should be able to use roi.getShape() - but there's currently a bug in it for rectangles/ellipses! + var shape2 = transform.createTransformedShape(shape); + var roi2 = RoiTools.getShapeROI(shape2, roi.getImagePlane(), 0.5); + return roi2; + } + } +} -} \ No newline at end of file