Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a propagate button to copy the (warped) contours from the template image to the selected overlay one #7

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion src/main/java/qupath/ext/align/gui/ImageAlignmentPane.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<BufferedImage> imageDataBase = viewer.getImageData();
ImageData<BufferedImage> 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<BufferedImage> project = qupath.getProject();
ProjectImageEntry<BufferedImage> 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<PathObject> 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);
Expand All @@ -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);
Expand Down
85 changes: 84 additions & 1 deletion src/main/java/qupath/ext/align/gui/ImageServerOverlay.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<Point2> points = roi.getAllPoints();
var nPoints = points.size();

// Convert List<Point2> 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<Point2> 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;
}
}
}

}