use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class DensityMaps method threshold.
/**
* Threshold one or more channels of a density map to generate new annotations.
*
* @param hierarchy hierarchy to which objects should be added
* @param densityServer density map
* @param thresholds map between channel numbers and thresholds
* @param pathClassName name of the classification to apply to the generated annotations
* @param options additional options to customize how annotations are created
* @return true if changes were made, false otherwise
* @throws IOException
*/
public static boolean threshold(PathObjectHierarchy hierarchy, ImageServer<BufferedImage> densityServer, Map<Integer, ? extends Number> thresholds, String pathClassName, CreateObjectOptions... options) throws IOException {
logger.debug("Thresholding {} with thresholds {}, options", densityServer, thresholds, Arrays.asList(options));
// Apply threshold to densities
PathClass lessThan = PathClassFactory.getPathClass(StandardPathClasses.IGNORE);
PathClass greaterThan = PathClassFactory.getPathClass(pathClassName);
// If we request to delete existing objects, apply this only to annotations with the target class
var optionsList = Arrays.asList(options);
boolean changes = false;
if (optionsList.contains(CreateObjectOptions.DELETE_EXISTING)) {
Collection<PathObject> toRemove;
if (hierarchy.getSelectionModel().noSelection())
toRemove = hierarchy.getAnnotationObjects().stream().filter(p -> p.getPathClass() == greaterThan).collect(Collectors.toList());
else {
toRemove = new HashSet<>();
var selectedObjects = new LinkedHashSet<>(hierarchy.getSelectionModel().getSelectedObjects());
for (var selected : selectedObjects) {
PathObjectTools.getDescendantObjects(selected, toRemove, PathAnnotationObject.class);
}
// Don't remove selected objects
toRemove.removeAll(selectedObjects);
}
if (!toRemove.isEmpty()) {
hierarchy.removeObjects(toRemove, true);
changes = true;
}
// Remove option
options = optionsList.stream().filter(o -> o != CreateObjectOptions.DELETE_EXISTING).toArray(CreateObjectOptions[]::new);
}
var thresholdedServer = PixelClassifierTools.createThresholdServer(densityServer, thresholds, lessThan, greaterThan);
return PixelClassifierTools.createAnnotationsFromPixelClassifier(hierarchy, thresholdedServer, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, options) | changes;
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class DensityMaps method findHotspots.
/**
* Find hotspots in a density map.
*
* @param hierarchy hierarchy used to obtain selected objects and add hotspots
* @param densityServer the density map to query
* @param channel channel in which to find hotspots (usually 0)
* @param nHotspots maximum number of hotspots to find per selected annotation
* @param radius hotspot radius, in calibrated units
* @param minCount minimum value required in the 'count' channel (the last channel)
* @param hotspotClass the classification to apply to hotspots
* @param deleteExisting optionally delete existing annotations identified as hotspots
* @param peaksOnly optionally restrict hotspots to only include intensity peaks
* @throws IOException
*/
public static void findHotspots(PathObjectHierarchy hierarchy, ImageServer<BufferedImage> densityServer, int channel, int nHotspots, double radius, double minCount, PathClass hotspotClass, boolean deleteExisting, boolean peaksOnly) throws IOException {
if (nHotspots <= 0) {
logger.warn("Number of hotspots requested is {}!", nHotspots);
return;
}
logger.debug("Finding {} hotspots in {} for channel {}, radius {}", nHotspots, densityServer, channel, radius);
Collection<PathObject> parents = new ArrayList<>(hierarchy.getSelectionModel().getSelectedObjects());
if (parents.isEmpty())
parents = Collections.singleton(hierarchy.getRootObject());
double downsample = densityServer.getDownsampleForResolution(0);
var toDelete = new HashSet<PathObject>();
// Handle deleting existing hotspots
if (deleteExisting) {
toDelete.addAll(hierarchy.getAnnotationObjects().stream().filter(p -> p.getPathClass() == hotspotClass && p.isAnnotation() && p.getName() != null && p.getName().startsWith("Hotspot")).collect(Collectors.toList()));
}
// Convert radius to pixels
double radiusPixels = radius / densityServer.getPixelCalibration().getAveragedPixelSize().doubleValue();
try (@SuppressWarnings("unchecked") var scope = new PointerScope()) {
for (var parent : parents) {
ROI roi = parent.getROI();
// We need a ROI to define the area of interest
if (roi == null) {
if (densityServer.nTimepoints() > 1 || densityServer.nZSlices() > 1) {
logger.warn("Hotspot detection without a parent object not supported for images with multiple z-slices/timepoints.");
logger.warn("I will apply detection to the first plane only. If you need hotspots elsewhere, create an annotation first and use it to define the ROI.");
}
roi = ROIs.createRectangleROI(0, 0, densityServer.getWidth(), densityServer.getHeight(), ImagePlane.getDefaultPlane());
}
// Erode the ROI & see if any hotspot could fit
var roiEroded = RoiTools.buffer(roi, -radiusPixels);
if (roiEroded.isEmpty() || roiEroded.getArea() == 0) {
logger.warn("ROI is too small! Cannot detected hotspots with radius {} in {}", radius, parent);
continue;
}
// Read the image
var plane = roi.getImagePlane();
RegionRequest request = RegionRequest.createInstance(densityServer.getPath(), downsample, 0, 0, densityServer.getWidth(), densityServer.getHeight(), plane.getZ(), plane.getT());
var img = densityServer.readBufferedImage(request);
// Create a mask
var imgMask = BufferedImageTools.createROIMask(img.getWidth(), img.getHeight(), roiEroded, request);
// Switch to OpenCV
var mat = OpenCVTools.imageToMat(img);
var matMask = OpenCVTools.imageToMat(imgMask);
// Find hotspots
var channels = OpenCVTools.splitChannels(mat);
var density = channels.get(channel);
if (minCount > 0) {
var thresholdMask = opencv_core.greaterThan(channels.get(channels.size() - 1), minCount).asMat();
opencv_core.bitwise_and(matMask, thresholdMask, matMask);
thresholdMask.close();
}
// TODO: Limit to peaks
if (peaksOnly) {
var matMaxima = OpenCVTools.findRegionalMaxima(density);
var matPeaks = OpenCVTools.shrinkLabels(matMaxima);
matPeaks.put(opencv_core.greaterThan(matPeaks, 0));
opencv_core.bitwise_and(matMask, matPeaks, matMask);
matPeaks.close();
matMaxima.close();
}
// Sort in descending order
var maxima = new ArrayList<>(OpenCVTools.getMaskedPixels(density, matMask));
Collections.sort(maxima, Comparator.comparingDouble((IndexedPixel p) -> p.getValue()).reversed());
// Try to get as many maxima as we need
// Impose minimum separation
var points = maxima.stream().map(p -> new Point2(p.getX() * downsample, p.getY() * downsample)).collect(Collectors.toList());
var hotspotCentroids = new ArrayList<Point2>();
double distSqThreshold = radiusPixels * radiusPixels * 4;
for (var p : points) {
// Check not too close to an existing hotspot
boolean skip = false;
for (var p2 : hotspotCentroids) {
if (p.distanceSq(p2) < distSqThreshold) {
skip = true;
break;
}
}
if (!skip) {
hotspotCentroids.add(p);
if (hotspotCentroids.size() == nHotspots)
break;
}
}
var hotspots = new ArrayList<PathObject>();
int i = 0;
for (var p : hotspotCentroids) {
i++;
var ellipse = ROIs.createEllipseROI(p.getX() - radiusPixels, p.getY() - radiusPixels, radiusPixels * 2, radiusPixels * 2, roi.getImagePlane());
var hotspot = PathObjects.createAnnotationObject(ellipse, hotspotClass);
hotspot.setName("Hotspot " + i);
hotspots.add(hotspot);
}
if (hotspots.isEmpty())
logger.warn("No hotspots found in {}", parent);
else if (hotspots.size() < nHotspots) {
logger.warn("Only {}/{} hotspots could be found in {}", hotspots.size(), nHotspots, parent);
}
parent.addPathObjects(hotspots);
}
hierarchy.fireHierarchyChangedEvent(DensityMaps.class);
if (!toDelete.isEmpty())
hierarchy.removeObjects(toDelete, true);
}
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class QP method classifySelected.
/**
* Set the classification of the selected objects.
*
* @param hierarchy
* @param pathClassName
*/
public static void classifySelected(final PathObjectHierarchy hierarchy, final String pathClassName) {
PathClass pathClass = PathClassFactory.getPathClass(pathClassName);
Collection<PathObject> selected = hierarchy.getSelectionModel().getSelectedObjects();
if (selected.isEmpty()) {
logger.info("No objects selected");
return;
}
for (PathObject pathObject : selected) {
pathObject.setPathClass(pathClass);
}
if (selected.size() == 1)
logger.info("{} object classified as {}", selected.size(), pathClassName);
else
logger.info("{} objects classified as {}", selected.size(), pathClassName);
hierarchy.fireObjectClassificationsChangedEvent(null, selected);
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class QP method mergeAnnotations.
/**
* Merge the specified annotations to create a new annotation containing the union of their ROIs.
* <p>
* Note:
* <ul>
* <li>The existing annotations will be removed from the hierarchy if possible, therefore should be duplicated first
* if this is not desired.</li>
* <li>The new object will be set to be the selected object in the hierarchy (which can be used to retrieve it if needed).</li>
* </ul>
*
* @param hierarchy
* @param annotations
* @return true if changes are made to the hierarchy, false otherwise
*/
public static boolean mergeAnnotations(final PathObjectHierarchy hierarchy, final Collection<PathObject> annotations) {
if (hierarchy == null)
return false;
// Get all the selected annotations with area
ROI shapeNew = null;
List<PathObject> merged = new ArrayList<>();
Set<PathClass> pathClasses = new HashSet<>();
for (PathObject annotation : annotations) {
if (annotation.isAnnotation() && annotation.hasROI() && (annotation.getROI().isArea() || annotation.getROI().isPoint())) {
if (shapeNew == null)
// .duplicate();
shapeNew = annotation.getROI();
else if (shapeNew.getImagePlane().equals(annotation.getROI().getImagePlane()))
shapeNew = RoiTools.combineROIs(shapeNew, annotation.getROI(), RoiTools.CombineOp.ADD);
else {
logger.warn("Cannot merge ROIs across different image planes!");
return false;
}
if (annotation.getPathClass() != null)
pathClasses.add(annotation.getPathClass());
merged.add(annotation);
}
}
// Check if we actually merged anything
if (merged.isEmpty() || merged.size() == 1)
return false;
// Create and add the new object, removing the old ones
PathObject pathObjectNew = PathObjects.createAnnotationObject(shapeNew);
if (pathClasses.size() == 1)
pathObjectNew.setPathClass(pathClasses.iterator().next());
else
logger.warn("Cannot assign class unambiguously - " + pathClasses.size() + " classes represented in selection");
hierarchy.removeObjects(merged, true);
hierarchy.addPathObject(pathObjectNew);
hierarchy.getSelectionModel().setSelectedObject(pathObjectNew);
// hierarchy.fireHierarchyChangedEvent(pathObject);
return true;
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class TestCompositeClassifier method test_getPathClass.
@Test
public void test_getPathClass() {
List<PathClass> outputPathClasses = new ArrayList<>();
for (PathClass pathClass : new PathClass[] { pathClass1, pathClass2, pathClass3 }) {
outputPathClasses.add(PathClassFactory.getPositive(pathClass));
outputPathClasses.add(PathClassFactory.getNegative(pathClass));
}
var posNegClasses = Arrays.asList(PathClassFactory.getPositive(null), PathClassFactory.getNegative(null));
assertTrue(outputPathClasses.size() == cp1.getPathClasses().size() && outputPathClasses.containsAll(cp1.getPathClasses()));
assertTrue(cp2.getPathClasses().size() == posNegClasses.size() && cp2.getPathClasses().containsAll(posNegClasses));
assertTrue(cp2.getPathClasses().size() == posNegClasses.size() && cp2.getPathClasses().containsAll(posNegClasses));
assertTrue(cpInvalid.getPathClasses().size() == 0);
}
Aggregations