ChannelDetections.java

// SPDX-FileCopyrightText: 2024 Carlo Castoldi <carlo.castoldi@outlook.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package qupath.ext.braian;

import qupath.ext.braian.config.WatershedCellDetectionConfig;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.objects.PathObject;
import qupath.lib.objects.classes.PathClass;
import qupath.lib.objects.hierarchy.PathObjectHierarchy;
import qupath.lib.scripting.QP;

import java.util.*;

/**
 * This class allows to manage detections computed with {@link qupath.imagej.detect.cells.WatershedCellDetection}
 * on a given image channel. It does so by leveraging {@link AbstractDetections} interface
 */
public class ChannelDetections extends AbstractDetections {
    public static final String FULL_IMAGE_DETECTIONS_NAME = "AllDetections";

    /**
     * Returns the annotation used when working with detections on the whole image
     * @param hierarchy where to find/compute the detections
     * @return the full image detection named as {@link ChannelDetections#FULL_IMAGE_DETECTIONS_NAME}
     */
    public static PathAnnotationObject getFullImageDetectionAnnotation(PathObjectHierarchy hierarchy) {
        List<PathObject> fullImageAnnotations = hierarchy.getAnnotationObjects().stream()
                .filter(a -> FULL_IMAGE_DETECTIONS_NAME.equals(a.getName())).toList();
        switch (fullImageAnnotations.size()) {
            case 0:
                PathAnnotationObject fullImageAnnotation = (PathAnnotationObject) QP.createFullImageAnnotation(true);
                fullImageAnnotation.setName(FULL_IMAGE_DETECTIONS_NAME);
                return fullImageAnnotation;
            case 1:
                return (PathAnnotationObject) fullImageAnnotations.get(0);
            default:
                throw new RuntimeException("There are multiple annotations called '"+FULL_IMAGE_DETECTIONS_NAME+"'. Delete them!");
        }
    }

    public static PathClass createClassification(String channelName) {
        return PathClass.fromString(channelName);
    }

    /**
     * Creates an instance based on pre-computed cell detections.
     * It selects all detections that are computed in a container identified by {@code channelName}.
     * @param channelName the name of the channel to which the detections are linked to
     * @param hierarchy where to find the detections
     * @throws NoCellContainersFoundException if no pre-computed detection was found in the given hierarchy
     * @see #getContainersName()
     * @see AbstractDetections
     */
    public ChannelDetections(String channelName, PathObjectHierarchy hierarchy) throws NoCellContainersFoundException {
        super(channelName, List.of(createClassification(channelName)), hierarchy);
    }

    /**
     * Creates an instance based on pre-computed cell detections.
     * It selects all detections that are computed in a container identified by {@code channel}.
     * @param channel the channel to which the detections are linked to
     * @param hierarchy where to find the detections
     * @throws NoCellContainersFoundException if no pre-computed detection was found in the given hierarchy
     * @see #getContainersName()
     * @see AbstractDetections
     */
    public ChannelDetections(ImageChannelTools channel, PathObjectHierarchy hierarchy) throws NoCellContainersFoundException {
        this(channel.getName(), hierarchy);
    }

    /**
     * Computes the detections using {@link qupath.imagej.detect.cells.WatershedCellDetection} algorithm inside the given annotations
     * @param channel the channel to which the detections are linked to
     * @param annotations the annotations inside of which to compute the detections. If null or empty, it will compute them on the whole image
     * @param config parameters to give to the {@link qupath.imagej.detect.cells.WatershedCellDetection}
     * @param hierarchy where to compute the detections
     * @throws NoCellContainersFoundException
     * @see #getFullImageDetectionAnnotation(PathObjectHierarchy)
     */
    public ChannelDetections(ImageChannelTools channel,
                             Collection<PathAnnotationObject> annotations,
                             WatershedCellDetectionConfig config,
                             PathObjectHierarchy hierarchy) throws NoCellContainersFoundException {
        this(channel, hierarchy);

        if(annotations == null) {
            PathAnnotationObject fullImage = ChannelDetections.getFullImageDetectionAnnotation(hierarchy);
            annotations = List.of(fullImage);
        } else if (annotations.isEmpty()) {
            throw new IllegalArgumentException("You must give at least one annotation on which to compute the detections");
        }
        Map<String, ?> params = config.build(channel);
        // TODO: check if the given annotations overlap. If they do, throw an error as that would duplicate detections
        List<PathAnnotationObject> containers = annotations.stream().map(annotation -> {
            annotation.setLocked(true);
            PathAnnotationObject container = this.createContainer(annotation, true);
            return ChannelDetections.compute(container, params);
        }).toList();

        this.fireUpdate();
    }

    /**
     * Computes the detections using {@link qupath.imagej.detect.cells.WatershedCellDetection} algorithm inside the given annotations
     * @param channel the channel to which the detections are linked to
     * @param annotation the annotation inside of which to compute the detections. If null, it will compute them on the whole image
     * @param config parameters to give to the {@link qupath.imagej.detect.cells.WatershedCellDetection}
     * @param hierarchy where to compute the detections
     * @throws NoCellContainersFoundException
     * @see #getFullImageDetectionAnnotation(PathObjectHierarchy)
     */
    public ChannelDetections(ImageChannelTools channel,
                             PathAnnotationObject annotation,
                             WatershedCellDetectionConfig config,
                             PathObjectHierarchy hierarchy) throws NoCellContainersFoundException {
        this(channel, annotation != null ? List.of(annotation) : null, config, hierarchy);
    }

    private static PathAnnotationObject compute(PathAnnotationObject container, Map<String,?> params) {
        QP.selectObjects(container);
        try {
            QP.runPlugin("qupath.imagej.detect.cells.WatershedCellDetection", params);
            ChannelDetections.getChildrenDetections(container).forEach(detection -> detection.setPathClass(container.getPathClass()));
            return container;
        } catch (InterruptedException e) {
            BraiAnExtension.logger.warn("Watershed cell detection interrupted. Returning empty list of detections for "+container+"!");
            return container;
        }
    }

    @Override
    public String getContainersName() {
        return this.getId()+" cells";
    }
}