use of qupath.lib.roi.PolygonROI in project qupath by qupath.
the class IJTools method convertToIJRoi.
/**
* Convert a QuPath ROI to an ImageJ Roi.
* @param <T>
* @param pathROI
* @param xOrigin x-origin indicating relationship of ImagePlus to the original image, as stored in ImageJ Calibration object
* @param yOrigin y-origin indicating relationship of ImagePlus to the original image, as stored in ImageJ Calibration object
* @param downsampleFactor downsample factor at which the ImagePlus was extracted from the full-resolution image
* @return
*/
public static <T extends PathImage<ImagePlus>> Roi convertToIJRoi(ROI pathROI, double xOrigin, double yOrigin, double downsampleFactor) {
if (pathROI instanceof PolygonROI)
return ROIConverterIJ.convertToPolygonROI((PolygonROI) pathROI, xOrigin, yOrigin, downsampleFactor);
if (pathROI instanceof RectangleROI)
return ROIConverterIJ.getRectangleROI((RectangleROI) pathROI, xOrigin, yOrigin, downsampleFactor);
if (pathROI instanceof EllipseROI)
return ROIConverterIJ.convertToOvalROI((EllipseROI) pathROI, xOrigin, yOrigin, downsampleFactor);
if (pathROI instanceof LineROI)
return ROIConverterIJ.convertToLineROI((LineROI) pathROI, xOrigin, yOrigin, downsampleFactor);
if (pathROI instanceof PolylineROI)
return ROIConverterIJ.convertToPolygonROI((PolylineROI) pathROI, xOrigin, yOrigin, downsampleFactor);
if (pathROI instanceof PointsROI)
return ROIConverterIJ.convertToPointROI((PointsROI) pathROI, xOrigin, yOrigin, downsampleFactor);
// If we have any other kind of shape, create a general shape roi
if (pathROI != null && pathROI.isArea()) {
// TODO: Deal with non-AWT area ROIs!
Shape shape = RoiTools.getArea(pathROI);
// "scaleX", "shearY", "shearX", "scaleY", "translateX", "translateY"
shape = new AffineTransform(1.0 / downsampleFactor, 0, 0, 1.0 / downsampleFactor, xOrigin, yOrigin).createTransformedShape(shape);
return ROIConverterIJ.setIJRoiProperties(new ShapeRoi(shape), pathROI);
}
// TODO: Integrate ROI not supported exception...?
return null;
}
use of qupath.lib.roi.PolygonROI in project qupath by qupath.
the class ObservableMeasurementTableData method updateMeasurementList.
/**
* Update the entire measurement list for the current objects.
* @see #setImageData(ImageData, Collection)
*/
public synchronized void updateMeasurementList() {
// PathPrefs.setAllredMinPercentagePositive(0);
builderMap.clear();
// Add the image name
if (!PathPrefs.maskImageNamesProperty().get())
builderMap.put("Image", new ImageNameMeasurementBuilder(imageData));
// Check if we have any annotations / TMA cores
boolean containsDetections = false;
boolean containsAnnotations = false;
// boolean containsParentAnnotations = false;
boolean containsTMACores = false;
boolean containsRoot = false;
List<PathObject> pathObjectListCopy = new ArrayList<>(list);
for (PathObject temp : pathObjectListCopy) {
if (temp instanceof PathAnnotationObject) {
// if (temp.hasChildren())
// containsParentAnnotations = true;
containsAnnotations = true;
} else if (temp instanceof TMACoreObject) {
containsTMACores = true;
} else if (temp instanceof PathDetectionObject) {
containsDetections = true;
} else if (temp.isRootObject())
containsRoot = true;
}
boolean detectionsAnywhere = imageData == null ? containsDetections : !imageData.getHierarchy().getDetectionObjects().isEmpty();
// Include the object displayed name
// if (containsDetections || containsAnnotations || containsTMACores)
builderMap.put("Name", new ObjectNameMeasurementBuilder());
// Include the class
if (containsAnnotations || containsDetections) {
builderMap.put("Class", new PathClassMeasurementBuilder());
// Get the name of the containing TMA core if we have anything other than cores
if (imageData != null && imageData.getHierarchy().getTMAGrid() != null) {
builderMap.put("TMA core", new TMACoreNameMeasurementBuilder());
}
// Get the name of the first parent object
builderMap.put("Parent", new ParentNameMeasurementBuilder());
}
// Include the TMA missing status, if appropriate
if (containsTMACores) {
builderMap.put("Missing", new MissingTMACoreMeasurementBuilder());
}
if (containsAnnotations || containsDetections) {
builderMap.put("ROI", new ROINameMeasurementBuilder());
}
// Add centroids
if (containsAnnotations || containsDetections || containsTMACores) {
// ROICentroidMeasurementBuilder builder = new ROICentroidMeasurementBuilder(imageData, CentroidType.X);
// builderMap.put("Centroid X", builder);
// builder = new ROICentroidMeasurementBuilder(imageData, CentroidType.Y);
// builderMap.put("Centroid Y", builder);
ROICentroidMeasurementBuilder builder = new ROICentroidMeasurementBuilder(imageData, CentroidType.X);
builderMap.put(builder.getName(), builder);
builder = new ROICentroidMeasurementBuilder(imageData, CentroidType.Y);
builderMap.put(builder.getName(), builder);
}
// If we have metadata, store it
Set<String> metadataNames = new LinkedHashSet<>();
metadataNames.addAll(builderMap.keySet());
for (PathObject pathObject : pathObjectListCopy) {
if (pathObject instanceof MetadataStore) {
metadataNames.addAll(((MetadataStore) pathObject).getMetadataKeys());
}
}
// Ensure we have suitable builders
for (String name : metadataNames) {
if (!builderMap.containsKey(name))
builderMap.put(name, new StringMetadataMeasurementBuilder(name));
}
// Get all the 'built-in' feature measurements, stored in the measurement list
Collection<String> features = PathClassifierTools.getAvailableFeatures(pathObjectListCopy);
// Add derived measurements if we don't have only detections
if (containsAnnotations || containsTMACores || containsRoot) {
if (detectionsAnywhere) {
var builder = new ObjectTypeCountMeasurementBuilder(PathDetectionObject.class);
builderMap.put(builder.getName(), builder);
features.add(builder.getName());
}
// Here, we allow TMA cores to act like annotations
manager = new DerivedMeasurementManager(getImageData(), containsAnnotations || containsTMACores);
for (MeasurementBuilder<?> builder2 : manager.getMeasurementBuilders()) {
builderMap.put(builder2.getName(), builder2);
features.add(builder2.getName());
}
}
// If we have an annotation, add shape features
if (containsAnnotations) {
boolean anyPoints = false;
boolean anyAreas = false;
boolean anyLines = false;
@SuppressWarnings("unused") boolean anyPolygons = false;
for (PathObject pathObject : pathObjectListCopy) {
if (!pathObject.isAnnotation())
continue;
ROI roi = pathObject.getROI();
if (roi == null)
continue;
if (roi.isPoint())
anyPoints = true;
if (roi.isArea())
anyAreas = true;
if (roi.isLine())
anyLines = true;
if (pathObject.getROI() instanceof PolygonROI)
anyPolygons = true;
}
// Add point count, if needed
if (anyPoints) {
MeasurementBuilder<?> builder = new NumPointsMeasurementBuilder();
builderMap.put(builder.getName(), builder);
features.add(builder.getName());
}
// Add spatial measurements, if needed
if (anyAreas) {
MeasurementBuilder<?> builder = new AreaMeasurementBuilder(imageData);
builderMap.put(builder.getName(), builder);
features.add(builder.getName());
builder = new PerimeterMeasurementBuilder(imageData);
builderMap.put(builder.getName(), builder);
features.add(builder.getName());
}
if (anyLines) {
MeasurementBuilder<?> builder = new LineLengthMeasurementBuilder(imageData);
builderMap.put(builder.getName(), builder);
features.add(builder.getName());
}
// if (anyPolygons) {
// MeasurementBuilder<?> builder = new MaxDiameterMeasurementBuilder(imageData);
// builderMap.put(builder.getName(), builder);
// features.add(builder.getName());
//
// builder = new MinDiameterMeasurementBuilder(imageData);
// builderMap.put(builder.getName(), builder);
// features.add(builder.getName());
// }
}
if (containsAnnotations || containsTMACores || containsRoot) {
var pixelClassifier = getPixelLayer(imageData);
if (pixelClassifier instanceof ImageServer<?>) {
ImageServer<BufferedImage> server = (ImageServer<BufferedImage>) pixelClassifier;
if (server.getMetadata().getChannelType() == ImageServerMetadata.ChannelType.CLASSIFICATION || server.getMetadata().getChannelType() == ImageServerMetadata.ChannelType.PROBABILITY) {
var pixelManager = new PixelClassificationMeasurementManager(server);
for (String name : pixelManager.getMeasurementNames()) {
// String nameLive = name + " (live)";
String nameLive = "(Live) " + name;
builderMap.put(nameLive, new PixelClassifierMeasurementBuilder(pixelManager, name));
features.add(nameLive);
}
}
}
}
// Update all the lists, if necessary
boolean changes = false;
if (metadataNames.size() != metadataList.size() || !metadataNames.containsAll(metadataList)) {
changes = metadataList.setAll(metadataNames);
}
if (features.size() != measurementList.size() || !features.containsAll(measurementList))
changes = measurementList.setAll(features);
if (changes) {
if (metadataList.isEmpty())
fullList.setAll(measurementList);
else {
fullList.setAll(metadataList);
fullList.addAll(measurementList);
}
}
}
use of qupath.lib.roi.PolygonROI in project qupath by qupath.
the class AbstractPathROITool method mousePressed.
@Override
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
if (!e.isPrimaryButtonDown() || e.isConsumed()) {
return;
}
var viewer = getViewer();
PathObjectHierarchy hierarchy = viewer.getHierarchy();
if (hierarchy == null)
return;
PathObject currentObject = viewer.getSelectedObject();
ROI currentROI = currentObject == null ? null : currentObject.getROI();
RoiEditor editor = viewer.getROIEditor();
boolean adjustingPolygon = (currentROI instanceof PolygonROI || currentROI instanceof PolylineROI) && editor.getROI() == currentROI && (editor.isTranslating() || editor.hasActiveHandle());
// If we're adjusting a polygon/polyline with an appropriate tool, return at leave it up to the tool to handle the custom things
if (adjustingPolygon) {
if (viewer.getActiveTool() == PathTools.POLYGON || viewer.getActiveTool() == PathTools.POLYLINE)
return;
else {
viewer.getHierarchy().getSelectionModel().clearSelection();
viewer.getHierarchy().fireHierarchyChangedEvent(currentObject);
}
}
// Find out the coordinates in the image domain
Point2D p2 = mouseLocationToImage(e, false, requestPixelSnapping());
double xx = p2.getX();
double yy = p2.getY();
if (xx < 0 || yy < 0 || xx >= viewer.getServerWidth() || yy >= viewer.getServerHeight())
return;
// If we are double-clicking & we don't have a polygon, see if we can access a ROI
if (!PathPrefs.selectionModeProperty().get() && e.getClickCount() > 1) {
// Reset parent... for now
resetConstrainedAreaParent();
tryToSelect(xx, yy, e.getClickCount() - 2, false);
e.consume();
return;
}
// Set the current parent object based on the first click
setConstrainedAreaParent(hierarchy, xx, yy, Collections.emptyList());
// Create a new annotation
PathObject pathObject = createNewAnnotation(e, xx, yy);
if (pathObject == null)
return;
// Start editing the ROI immediately
editor.setROI(pathObject.getROI());
editor.grabHandle(xx, yy, viewer.getMaxROIHandleSize() * 1.5, e.isShiftDown());
}
use of qupath.lib.roi.PolygonROI in project qupath by qupath.
the class SimpleTissueDetection2 method convertToPathObjects.
private static List<PathObject> convertToPathObjects(ByteProcessor bp, double minArea, boolean smoothCoordinates, Calibration cal, double downsample, double maxHoleArea, boolean excludeOnBoundary, boolean singleAnnotation, ImagePlane plane, List<PathObject> pathObjects) {
//
// var roiIJ = new ThresholdToSelection().convert(bp);
// var roi = IJTools.convertToROI(roiIJ, cal, downsample, plane);
// roi = RoiTools.removeSmallPieces(roi, minArea, maxHoleArea);
// List<PathObject> annotations = new ArrayList<>();
// if (singleAnnotation)
// annotations.add(PathObjects.createAnnotationObject(roi));
// else {
// for (var roi2 : RoiTools.splitROI(roi)) {
// annotations.add(PathObjects.createAnnotationObject(roi2));
// }
// }
// for (var annotation : annotations)
// annotation.setLocked(true);
// return annotations;
//
List<PolygonRoi> rois = RoiLabeling.getFilledPolygonROIs(bp, Wand.FOUR_CONNECTED);
if (pathObjects == null)
pathObjects = new ArrayList<>(rois.size());
// We might need a copy of the original image
boolean fillAllHoles = maxHoleArea <= 0;
ByteProcessor bpOrig = !fillAllHoles ? (ByteProcessor) bp.duplicate() : null;
bp.setValue(255);
for (PolygonRoi r : rois) {
// Check for boundary overlap
if (excludeOnBoundary) {
Rectangle bounds = r.getBounds();
if (bounds.x <= 0 || bounds.y <= 0 || bounds.x + bounds.width >= bp.getWidth() - 1 || bounds.y + bounds.height >= bp.getHeight() - 1)
continue;
}
bp.setRoi(r);
if (bp.getStatistics().area < minArea)
continue;
// Fill holes as we go - it might matter later
bp.fill(r);
// if (smoothCoordinates) {
// // r = new PolygonRoi(r.getInterpolatedPolygon(2.5, false), Roi.POLYGON);
// r = new PolygonRoi(r.getInterpolatedPolygon(Math.min(2.5, r.getNCoordinates()*0.1), false), Roi.POLYGON); // TODO: Check this smoothing - it can be troublesome, causing nuclei to be outside cells
// }
PolygonROI pathPolygon = IJTools.convertToPolygonROI(r, cal, downsample, plane);
// Smooth the coordinates, if we downsampled quite a lot
if (smoothCoordinates) {
pathPolygon = ROIs.createPolygonROI(ShapeSimplifier.smoothPoints(pathPolygon.getAllPoints()), ImagePlane.getPlaneWithChannel(pathPolygon));
pathPolygon = ShapeSimplifier.simplifyPolygon(pathPolygon, downsample / 2);
}
pathObjects.add(PathObjects.createAnnotationObject(pathPolygon));
}
if (Thread.currentThread().isInterrupted())
return null;
// TODO: Optimise this - the many 'containsObject' calls are a (potentially easy-to-fix) bottleneck
if (!fillAllHoles) {
// Get the holes alone
bp.copyBits(bpOrig, 0, 0, Blitter.DIFFERENCE);
// new ImagePlus("Binary", bp).show();
bp.setThreshold(127, Double.POSITIVE_INFINITY, ImageProcessor.NO_LUT_UPDATE);
List<PathObject> holes = convertToPathObjects(bp, maxHoleArea, smoothCoordinates, cal, downsample, 0, false, false, plane, null);
// For each object, fill in any associated holes
List<Area> areaList = new ArrayList<>();
for (int ind = 0; ind < pathObjects.size(); ind++) {
if (holes.isEmpty())
break;
PathObject pathObject = pathObjects.get(ind);
var geom = PreparedGeometryFactory.prepare(pathObject.getROI().getGeometry());
areaList.clear();
Iterator<PathObject> iter = holes.iterator();
while (iter.hasNext()) {
PathObject hole = iter.next();
if (geom.covers(hole.getROI().getGeometry())) {
areaList.add(RoiTools.getArea(hole.getROI()));
iter.remove();
}
}
if (areaList.isEmpty())
continue;
// If we have some areas, combine them
// TODO: FIX MAJOR BOTTLENECK HERE!!!
Area hole = areaList.get(0);
for (int i = 1; i < areaList.size(); i++) {
hole.add(areaList.get(i));
if (i % 100 == 0) {
logger.debug("Added hole " + i + "/" + areaList.size());
if (Thread.currentThread().isInterrupted())
return null;
}
}
// Now subtract & create a new object
ROI pathROI = pathObject.getROI();
if (RoiTools.isShapeROI(pathROI)) {
Area areaMain = RoiTools.getArea(pathROI);
areaMain.subtract(hole);
pathROI = RoiTools.getShapeROI(areaMain, pathROI.getImagePlane());
pathObjects.set(ind, PathObjects.createAnnotationObject(pathROI));
}
}
}
// This is a clumsy way to do it...
if (singleAnnotation) {
ROI roi = null;
for (PathObject annotation : pathObjects) {
ROI currentShape = annotation.getROI();
if (roi == null)
roi = currentShape;
else
roi = RoiTools.combineROIs(roi, currentShape, CombineOp.ADD);
}
pathObjects.clear();
if (roi != null)
pathObjects.add(PathObjects.createAnnotationObject(roi));
}
// Lock the objects
for (PathObject pathObject : pathObjects) ((PathAnnotationObject) pathObject).setLocked(true);
return pathObjects;
}
use of qupath.lib.roi.PolygonROI in project qupath by qupath.
the class Commands method promptToSimplifySelectedAnnotations.
/**
* Show a prompt to selected annotations in a hierarchy.
* @param imageData the current image data
* @param altitudeThreshold default altitude value for simplification
*/
public static void promptToSimplifySelectedAnnotations(ImageData<?> imageData, double altitudeThreshold) {
PathObjectHierarchy hierarchy = imageData.getHierarchy();
List<PathObject> pathObjects = hierarchy.getSelectionModel().getSelectedObjects().stream().filter(p -> p.isAnnotation() && p.hasROI() && p.isEditable() && !p.getROI().isPoint()).collect(Collectors.toList());
if (pathObjects.isEmpty()) {
Dialogs.showErrorMessage("Simplify annotations", "No unlocked shape annotations selected!");
return;
}
String input = Dialogs.showInputDialog("Simplify shape", "Set altitude threshold in pixels.\nHigher values give simpler shapes.", Double.toString(altitudeThreshold));
if (input == null || !(input instanceof String) || ((String) input).trim().length() == 0)
return;
try {
altitudeThreshold = Double.parseDouble(((String) input).trim());
} catch (NumberFormatException e) {
logger.error("Could not parse altitude threshold from {}", input);
return;
}
if (altitudeThreshold <= 0) {
Dialogs.showErrorMessage("Simplify shape", "Amplitude threshold should be greater than zero!");
return;
}
long startTime = System.currentTimeMillis();
for (var pathObject : pathObjects) {
ROI pathROI = pathObject.getROI();
if (pathROI instanceof PolygonROI) {
PolygonROI polygonROI = (PolygonROI) pathROI;
pathROI = ShapeSimplifier.simplifyPolygon(polygonROI, altitudeThreshold);
} else {
pathROI = ShapeSimplifier.simplifyShape(pathROI, altitudeThreshold);
}
((PathAnnotationObject) pathObject).setROI(pathROI);
}
long endTime = System.currentTimeMillis();
logger.debug("Shapes simplified in " + (endTime - startTime) + " ms");
hierarchy.fireObjectsChangedEvent(hierarchy, pathObjects);
}
Aggregations