use of qupath.lib.regions.ImagePlane in project qupath by qupath.
the class DelaunayTools method createFromCentroids.
/**
* Create a {@link Subdivision} using the centroid coordinates of ROIs.
* <p>
* Note: centroids must be distinct. If multiple objects have identical centroids, one or more objects may be lost
* from the resulting {@link Subdivision}.
*
* @param pathObjects collection of objects from which to construct the {@link Subdivision}
* @param preferNucleusROI if true, prefer the nucleus ROI when extracting the centroid from a cell
* @return a new {@link Subdivision} computed from the centroids of the provided objects
*
* @see #createFromGeometryCoordinates(Collection, boolean, double)
*/
public static Subdivision createFromCentroids(Collection<PathObject> pathObjects, boolean preferNucleusROI) {
logger.debug("Creating subdivision from ROI centroids for {} objects", pathObjects.size());
var coords = new HashMap<Coordinate, PathObject>();
ImagePlane plane = null;
var precisionModel = GeometryTools.getDefaultFactory().getPrecisionModel();
for (var pathObject : pathObjects) {
var roi = PathObjectTools.getROI(pathObject, preferNucleusROI);
if (plane == null)
plane = roi.getImagePlane();
else if (!plane.equals(roi.getImagePlane())) {
logger.warn("Non-matching image planes: {} and {}! Object will be skipped...", plane, roi.getImagePlane());
continue;
}
double x = precisionModel.makePrecise(roi.getCentroidX());
double y = precisionModel.makePrecise(roi.getCentroidY());
var coord = new Coordinate(x, y);
coords.put(coord, pathObject);
}
return new Subdivision(createSubdivision(coords.keySet(), 0.01), pathObjects, coords, plane);
}
use of qupath.lib.regions.ImagePlane in project qupath by qupath.
the class PathObjectTools method mergePointsForAllClasses.
/**
* Merge point annotations sharing the same {@link PathClass} and {@link ImagePlane},
* creating multi-point annotations for all matching points and removing the (previously-separated) annotations.
*
* @param hierarchy object hierarchy to modify
* @return true if changes are made to the hierarchy, false otherwise
*/
public static boolean mergePointsForAllClasses(PathObjectHierarchy hierarchy) {
if (hierarchy == null)
return false;
var pathClasses = hierarchy.getAnnotationObjects().stream().filter(p -> p.getROI().isPoint()).map(p -> p.getPathClass()).collect(Collectors.toSet());
boolean changes = false;
for (PathClass pathClass : pathClasses) changes = changes || mergePointsForClass(hierarchy, pathClass);
return changes;
}
use of qupath.lib.regions.ImagePlane in project qupath by qupath.
the class PointIO method writePoints.
/**
* Write a list of point annotations to a stream.
* @param stream
* @param pathObjects
* @throws IOException
*/
public static void writePoints(OutputStream stream, Collection<? extends PathObject> pathObjects) throws IOException {
// Check that all PathObjects contain only point annotations
int unfilteredSize = pathObjects.size();
pathObjects = pathObjects.stream().filter(p -> p.getROI() instanceof PointsROI).collect(Collectors.toList());
int filteredSize = pathObjects.size();
if (unfilteredSize != filteredSize)
logger.warn(unfilteredSize - filteredSize + " of the " + filteredSize + " elements in list is/are not point annotations. These will be skipped.");
try (Writer writer = new BufferedWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) {
List<String> cols = new ArrayList<>();
cols.addAll(Arrays.asList("x", "y"));
String sep = "\t";
ImagePlane defaultPlane = ImagePlane.getDefaultPlane();
boolean hasClass = pathObjects.stream().anyMatch(p -> p.getPathClass() != null);
boolean hasName = pathObjects.stream().anyMatch(p -> p.getName() != null);
boolean hasColor = pathObjects.stream().anyMatch(p -> p.getColorRGB() != null);
boolean hasC = pathObjects.stream().anyMatch(p -> p.getROI().getC() > defaultPlane.getC());
boolean hasZ = pathObjects.stream().anyMatch(p -> p.getROI().getZ() > defaultPlane.getZ());
boolean hasT = pathObjects.stream().anyMatch(p -> p.getROI().getT() > defaultPlane.getT());
if (hasC)
cols.add("c");
if (hasZ)
cols.add("z");
if (hasT)
cols.add("t");
if (hasClass)
cols.add("class");
if (hasName)
cols.add("name");
if (hasColor)
cols.add("color");
for (String col : cols) writer.write(col + sep);
writer.write(System.lineSeparator());
for (PathObject pathObject : pathObjects) {
if (!PathObjectTools.hasPointROI(pathObject))
continue;
PointsROI points = (PointsROI) pathObject.getROI();
for (Point2 point : points.getAllPoints()) {
String[] row = new String[cols.size()];
row[cols.indexOf("x")] = point.getX() + "";
row[cols.indexOf("y")] = sep + point.getY();
if (hasC)
row[cols.indexOf("c")] = sep + points.getC();
if (hasZ)
row[cols.indexOf("z")] = sep + points.getZ();
if (hasT)
row[cols.indexOf("t")] = sep + points.getT();
if (hasClass)
row[cols.indexOf("class")] = pathObject.getPathClass() != null ? sep + pathObject.getPathClass() : sep;
if (hasName)
row[cols.indexOf("name")] = pathObject.getName() != null ? sep + pathObject.getName() : sep;
if (hasColor)
row[cols.indexOf("color")] = pathObject.getColorRGB() != null ? sep + pathObject.getColorRGB() : sep;
for (String val : row) writer.write(val);
writer.write(System.lineSeparator());
}
}
}
}
use of qupath.lib.regions.ImagePlane in project qupath by qupath.
the class BrushTool method getUpdatedObject.
private PathObject getUpdatedObject(MouseEvent e, ROI shapeROI, PathObject currentObject, double flatness) {
Point2D p = mouseLocationToImage(e, true, requestPixelSnapping());
var viewer = getViewer();
ImagePlane plane = shapeROI == null ? ImagePlane.getPlane(viewer.getZPosition(), viewer.getTPosition()) : shapeROI.getImagePlane();
Geometry shapeNew;
boolean subtractMode = isSubtractMode(e);
Geometry shapeCurrent = shapeROI == null ? null : shapeROI.getGeometry();
Geometry shapeDrawn = createShape(e, p.getX(), p.getY(), PathPrefs.useTileBrushProperty().get() && !e.isShiftDown(), subtractMode ? null : shapeCurrent);
if (shapeDrawn == null)
return currentObject;
// Do our pixel snapping now, with the simpler geometry (rather than latter when things are already complex)
if (requestPixelSnapping())
shapeDrawn = GeometryTools.roundCoordinates(shapeDrawn);
lastPoint = p;
try {
if (shapeROI != null) {
// Check to see if any changes are required at all
if (shapeDrawn == null || (subtractMode && !shapeCurrent.intersects(shapeDrawn)) || (!subtractMode && shapeCurrent.covers(shapeDrawn)))
return currentObject;
// TODO: Consider whether a preference should be used rather than the shift key?
// Anyhow, this will switch to 'dodge' mode, and avoid overlapping existing annotations
boolean avoidOtherAnnotations = requestParentClipping(e);
if (subtractMode) {
// If subtracting... then just subtract
shapeNew = shapeROI.getGeometry().difference(shapeDrawn);
} else if (avoidOtherAnnotations) {
shapeNew = shapeCurrent.union(shapeDrawn);
shapeNew = refineGeometryByParent(shapeNew);
} else {
// Just add, regardless of whether there are other annotations below or not
var temp = shapeROI.getGeometry();
try {
shapeNew = temp.union(shapeDrawn);
} catch (Exception e2) {
shapeNew = shapeROI.getGeometry();
}
}
} else {
shapeNew = shapeDrawn;
}
// If we aren't snapping, at least remove some vertices
if (!requestPixelSnapping()) {
try {
shapeNew = VWSimplifier.simplify(shapeNew, 0.1);
} catch (Exception e2) {
logger.error("Error simplifying ROI: " + e2.getLocalizedMessage(), e2);
}
}
// Make sure we fit inside the image
shapeNew = GeometryTools.constrainToBounds(shapeNew, 0, 0, viewer.getServerWidth(), viewer.getServerHeight());
// Sometimes we can end up with a GeometryCollection containing lines/non-areas... if so, remove these
if (shapeNew instanceof GeometryCollection) {
shapeNew = GeometryTools.ensurePolygonal(shapeNew);
}
ROI roiNew = GeometryTools.geometryToROI(shapeNew, plane);
if (currentObject instanceof PathAnnotationObject) {
((PathAnnotationObject) currentObject).setROI(roiNew);
return currentObject;
}
// shapeNew = new PathAreaROI(new Area(shapeNew.getShape()));
PathObject pathObjectNew = PathObjects.createAnnotationObject(roiNew, PathPrefs.autoSetAnnotationClassProperty().get());
if (currentObject != null) {
pathObjectNew.setName(currentObject.getName());
pathObjectNew.setColorRGB(currentObject.getColorRGB());
pathObjectNew.setPathClass(currentObject.getPathClass());
}
return pathObjectNew;
} catch (Exception ex) {
logger.error("Error updating ROI", ex);
return currentObject;
}
}
use of qupath.lib.regions.ImagePlane in project qupath by qupath.
the class QP method makeInverseAnnotation.
/**
* Make an annotation, for which the ROI is obtained by subtracting the ROIs of the specified objects from the closest
* common ancestor ROI (or entire image if the closest ancestor is the root).
* <p>
* In an inverted annotation can be created, it is added to the hierarchy and set as selected.
*
* @param imageData the image containing the annotation
* @param pathObjects the annotation to invert
* @return true if an inverted annotation is added to the hierarchy, false otherwise.
*/
public static boolean makeInverseAnnotation(final ImageData<?> imageData, Collection<PathObject> pathObjects) {
if (imageData == null)
return false;
var map = pathObjects.stream().filter(p -> p.hasROI() && p.getROI().isArea()).collect(Collectors.groupingBy(p -> p.getROI().getImagePlane()));
if (map.isEmpty()) {
logger.warn("No area annotations available - cannot created inverse ROI!");
return false;
}
if (map.size() > 1) {
logger.error("Cannot merge annotations from different image planes!");
return false;
}
ImagePlane plane = map.keySet().iterator().next();
List<PathObject> pathObjectList = map.get(plane);
PathObjectHierarchy hierarchy = imageData.getHierarchy();
// Try to get the best candidate parent
Collection<PathObject> parentSet = pathObjectList.stream().map(p -> p.getParent()).collect(Collectors.toCollection(HashSet::new));
PathObject parent;
if (parentSet.size() > 1) {
parentSet.clear();
boolean firstTime = true;
for (PathObject temp : pathObjectList) {
if (firstTime)
parentSet.addAll(PathObjectTools.getAncestorList(temp));
else
parentSet.retainAll(PathObjectTools.getAncestorList(temp));
firstTime = false;
}
List<PathObject> parents = new ArrayList<>(parentSet);
Collections.sort(parents, Comparator.comparingInt(PathObject::getLevel).reversed().thenComparingDouble(p -> p.hasROI() ? p.getROI().getArea() : Double.MAX_VALUE));
parent = parents.get(0);
} else
parent = parentSet.iterator().next();
// Get the parent area
Geometry geometryParent;
if (parent == null || parent.isRootObject() || !parent.hasROI())
geometryParent = GeometryTools.createRectangle(0, 0, imageData.getServer().getWidth(), imageData.getServer().getHeight());
else
geometryParent = parent.getROI().getGeometry();
// Get the parent area to use
var union = GeometryTools.union(pathObjectList.stream().map(p -> p.getROI().getGeometry()).collect(Collectors.toList()));
var geometry = geometryParent.difference(union);
// Create the new ROI
ROI shapeNew = GeometryTools.geometryToROI(geometry, plane);
PathObject pathObjectNew = PathObjects.createAnnotationObject(shapeNew);
parent.addPathObject(pathObjectNew);
hierarchy.fireHierarchyChangedEvent(parent);
hierarchy.getSelectionModel().setSelectedObject(pathObjectNew);
return true;
}
Aggregations