OverlappingDetections.java
// SPDX-FileCopyrightText: 2024 Carlo Castoldi <carlo.castoldi@outlook.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
package qupath.ext.braian;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.objects.PathDetectionObject;
import qupath.lib.objects.PathObjects;
import qupath.lib.objects.classes.PathClass;
import qupath.lib.objects.hierarchy.PathObjectHierarchy;
import qupath.lib.roi.interfaces.ROI;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This class allows to compute and manage double/triple/multiple positive detections.
* It does so by leveraging {@link AbstractDetections} interface
*/
public class OverlappingDetections extends AbstractDetections {
public static final String OVERLAP_DELIMITER = "~";
/**
* Creates all the names of the possible overlaps between the given PathClasses names
* @param primitiveClasses a list of PathClasses names
* @return a list combinations of the given primitiveClasses, delimited by {@link OverlappingDetections#OVERLAP_DELIMITER}
*/
public static List<String> createAllOverlappingClassNames(List<String> primitiveClasses) {
if(primitiveClasses.isEmpty())
return List.of();
String first = primitiveClasses.get(0);
List<String> others = primitiveClasses.subList(1, primitiveClasses.size());
List<String> othersOverlappingClasses = createAllOverlappingClassNames(others);
return Stream.concat(
Stream.of(first),
Stream.concat(
othersOverlappingClasses.stream(),
othersOverlappingClasses.stream().map(postfix -> first+OverlappingDetections.OVERLAP_DELIMITER+postfix))
).toList();
}
private static String createOverlappingClassName(String primary, List<String> others) {
return Stream.concat(Stream.of(primary), others.stream())
.collect(Collectors.joining(OverlappingDetections.OVERLAP_DELIMITER));
}
private static Collection<PathClass> getAllPossibleOverlappingClassifications(AbstractDetections control,
Collection<AbstractDetections> otherDetections) {
if (otherDetections.isEmpty())
throw new IllegalArgumentException("You have to overlap at least two detections; 'others' cannot be empty");
return OverlappingDetections.createAllOverlappingClassNames(
otherDetections.stream().map(AbstractDetections::getId).toList())
.stream()
.map(name -> control.getId()+OverlappingDetections.OVERLAP_DELIMITER+name)
.map(PathClass::fromString)
.toList();
}
/**
* Creates an instance of overlapping detections
* @param control the detections used to check whether the other detections are overlapping between them and the control
* @param others the other detections
* @param compute if true, it will delete any previous overlap and compute the overlap between detections.
* If false, it will try to retrieve pre-computed ovarlappings
* @param hierarchy where to find/compute the overlapping detections
* @throws NoCellContainersFoundException if no pre-computed overlappings were found in the given hierarchy
*/
public OverlappingDetections(AbstractDetections control,
Collection<AbstractDetections> others,
boolean compute, PathObjectHierarchy hierarchy) throws NoCellContainersFoundException {
super(control.getId(), getAllPossibleOverlappingClassifications(control, others), hierarchy);
if (!compute)
return;
this.overlap(control, others);
this.fireUpdate();
}
/**
* Creates an instance based on pre-computed overlapping detections
* @param control the detections used to check whether the other detections are overlapping between them and the control
* @param others the other detections
* @param hierarchy where to find the overlapping detections
* @throws NoCellContainersFoundException if no pre-computed overlappings were found in the given hierarchy
*/
public OverlappingDetections(AbstractDetections control,
Collection<AbstractDetections> others,
PathObjectHierarchy hierarchy) throws NoCellContainersFoundException {
this(control, others, false, hierarchy);
}
@Override
public String getContainersName() {
return this.getId()+" overlaps";
}
private void overlap(AbstractDetections control, Collection<AbstractDetections> otherDetections) {
List<PathDetectionObject> overlaps = control.toStream().flatMap(cell -> copyDetectionIfOverlapping(cell, control, otherDetections).stream()).toList();
this.getHierarchy().addObjects(overlaps);
// add all duplicated overlapping cells to a new annotation
for (PathAnnotationObject container : control.getContainers()) {
PathAnnotationObject containerParent = (PathAnnotationObject) container.getParent();
PathAnnotationObject overlapsContainer = this.createContainer(containerParent, true);
ROI containerRoi = overlapsContainer.getROI();
overlaps.stream()
.filter(overlap -> containerRoi.contains(overlap.getROI().getCentroidX(), overlap.getROI().getCentroidY()))
.forEach(overlap -> this.getHierarchy().addObjectBelowParent(overlapsContainer, overlap, false));
}
}
private static Optional<PathDetectionObject> copyDetectionIfOverlapping(PathDetectionObject cell,
AbstractDetections control,
Collection<AbstractDetections> otherDetections) {
List<String> overlappingDetectionsIds = otherDetections.stream()
.filter(other -> other.getOverlappingObjectIfPresent(cell).isPresent())
.map(AbstractDetections::getId)
.toList();
if (overlappingDetectionsIds.isEmpty())
return Optional.empty();
String className = createOverlappingClassName(control.getId(), overlappingDetectionsIds);
PathClass overlapClass = PathClass.fromString(className);
PathDetectionObject cellCopy = (PathDetectionObject) PathObjects.createDetectionObject(cell.getROI(), overlapClass);
return Optional.of(cellCopy);
}
}