ChannelClassifierConfig.java

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

package qupath.ext.braian.config;

import qupath.ext.braian.ChannelDetections;
import qupath.ext.braian.PartialClassifier;
import qupath.ext.braian.SingleClassifier;
import qupath.ext.braian.utils.BraiAn;
import qupath.lib.classifiers.object.ObjectClassifier;
import qupath.lib.classifiers.object.ObjectClassifiers;
import qupath.lib.io.UriResource;
import qupath.lib.io.UriUpdater;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.objects.hierarchy.PathObjectHierarchy;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;

import static qupath.lib.scripting.QP.getProject;

public class ChannelClassifierConfig {
    private String channel;
    private String name;
    private List<String> annotationsToClassify; // names of the annotations to classify

    public String getChannel() {
        return this.channel;
    }

    public void setChannel(String channel) {
        if (channel == null)
            throw new IllegalArgumentException("'channel' must be non-null value.");
        this.channel = channel;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<String> getAnnotationsToClassify() {
        return annotationsToClassify;
    }

    public void setAnnotationsToClassify(List<String> annotationsToClassify) {
        this.annotationsToClassify = annotationsToClassify;
    }

    /**
     * Loads the object classifier specified in the configuration file.
     * If the specified name is equals to <code>"ALL"</code>, it returns a {@link SingleClassifier}
     * that classifies all detections as part positive cells from this configuration's image channel.
     * Else, it searches for the a JSON file named after the specified string, as described by
     * {@link BraiAn#resolvePath(String)} and reads it.
     * @return an instance of <code>ObjectClassifier</code> loaded based on the configuration file
     * @throws IOException if any problem raises when reading the JSON file, supposedly corresponding to a QuPath
     * object classifier.
     */
    public <T> ObjectClassifier<T> loadClassifier() throws IOException {
        if (this.getName().equalsIgnoreCase("all"))
            return new SingleClassifier<>(ChannelDetections.createClassification(this.channel));
        Path classifierPath = BraiAn.resolvePath(this.getName()+".json");
        // inspired by QP.loadObjectClassifier()
        ObjectClassifier<T> classifier = null;
        Exception exception = null;

        try {
            classifier = ObjectClassifiers.readClassifier(classifierPath);
        } catch (Exception e) { exception = e; }
        if (classifier == null)
            throw new IOException("Unable to find object classifier " + classifierPath, exception);

        // Try to fix URIs, if we can
        if (classifier instanceof UriResource)
            UriUpdater.fixUris((UriResource)classifier, getProject());

        return classifier;
    }

    /**
     * It searches all annotations specified by their name in the configuration file. If a name does not correspond
     * to any annotation in the current hierarchy, it silently skips it.
     * @param hierarchy where to search teh annotations in
     * @return a collection of annotations, if they were specified in the configuration file.
     * Otherwise, <code>null</code>.
     */
    public Collection<PathAnnotationObject> getAnnotationsToClassify(PathObjectHierarchy hierarchy) {
        if(annotationsToClassify == null)
            return null;
        return hierarchy.getAnnotationObjects()
                .stream()
                .filter(annotation -> annotationsToClassify.contains(annotation.getName()))
                .map(annotation -> (PathAnnotationObject) annotation)
                .toList();
    }

    /** Loads the classifier and associates it to the annotations whose name is listed in the configuration file.
     * @param hierarchy where to search the annotations in
     * @return an instance of <code>PartialClassifier</code>
     * @see ChannelClassifierConfig#loadClassifier()
     * @see ChannelClassifierConfig#getAnnotationsToClassify()
     */
    public <T> PartialClassifier<T> toPartialClassifier(PathObjectHierarchy hierarchy) throws IOException {
        ObjectClassifier<T> classifier = this.loadClassifier();
        return new PartialClassifier<>(classifier, this.getAnnotationsToClassify(hierarchy));
    }
}