ProjectsConfig.java
// SPDX-FileCopyrightText: 2024 Carlo Castoldi <carlo.castoldi@outlook.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
package qupath.ext.braian.config;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.error.YAMLException;
import qupath.ext.braian.BraiAnExtension;
import qupath.ext.braian.utils.BraiAn;
import qupath.lib.objects.PathAnnotationObject;
import qupath.lib.objects.hierarchy.PathObjectHierarchy;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import static qupath.ext.braian.BraiAnExtension.getLogger;
/**
* This class reads a YAML file and can be used to apply the given parameters to the computation offered by BraiAn extension
* For more information, read <a href="https://github.com/carlocastoldi/qupath-extension-braian/blob/master/BraiAn.yml">this config file example</a>.
*/
public class ProjectsConfig {
/**
* Reads a BraiAn configuration file
* @param yamlFileName the name of the file, not the path.
* It will then search it first into the project's directory and, if it wasn't there, in its parent directory.
* If it cannot still find it, it throws {@link java.io.FileNotFoundException}
* @return an instance of <code>ProjectsConfig</code>
* @throws IOException if it found the config file, but it had problems while reading it.
* @throws YAMLException if it found and read the config file, but it was badly formatted.
*/
public static ProjectsConfig read(String yamlFileName) throws IOException, YAMLException {
Path filePath = BraiAn.resolvePath(yamlFileName);
getLogger().info("using '{}' configuration file.", filePath);
String configStream = Files.readString(filePath, StandardCharsets.UTF_8);
try {
Constructor c = new Constructor(ProjectsConfig.class, new LoaderOptions());
return new Yaml(c).load(configStream);
} catch (YAMLException e) {
getLogger().error("Could not interpret the file '{}'. Please check that it is correctly formatted!", filePath);
throw e;
}
}
private String classForDetections = null;
private DetectionsCheckConfig detectionsCheck = new DetectionsCheckConfig();
private List<ChannelDetectionsConfig> channelDetections = List.of();
public String getClassForDetections() {
return classForDetections;
}
/**
* Finds (or creates) the annotations chosen for computing the detections, accordingly to the configuration file.
* It reads the value of 'classForDetections' in the YAML and searches all annotations having the appointed classification
* @param hierarchy where to search the annotations in
* @return the annotations to be used for computing the detections
* @see qupath.ext.braian.ChannelDetections
*/
public Collection<PathAnnotationObject> getAnnotationsForDetections(PathObjectHierarchy hierarchy) {
String classForDetections = this.getClassForDetections();
if(classForDetections == null)
return null;
return hierarchy.getAnnotationObjects()
.stream()
.filter(annotation -> annotation.getPathClass() != null && classForDetections.equals(annotation.getPathClass().getName()))
//.filter(annotation -> classForDetections.equals(annotation.getName()))
.map(annotation -> (PathAnnotationObject) annotation)
.toList();
}
public void setClassForDetections(String classForDetections) {
this.classForDetections = classForDetections;
}
public DetectionsCheckConfig getDetectionsCheck() {
return detectionsCheck;
}
public void setDetectionsCheck(DetectionsCheckConfig detectionsCheck) {
this.detectionsCheck = detectionsCheck;
}
/**
* Retrieves the name of the channel to be used as control in the overlapping
* @return an empty optional if no overlapping is desired or if there there is only one image channel to compute the detections for
* @see qupath.ext.braian.OverlappingDetections
*/
public Optional<String> getControlChannel() {
if (!this.detectionsCheck.getApply() || this.channelDetections.size() < 2) // if there is only one channel with detections, it is useless to have a controlChannel
return Optional.empty();
String name = this.detectionsCheck.getControlChannel();
if (name == null)
name = this.channelDetections.get(0).getName();
return Optional.of(name);
}
public List<ChannelDetectionsConfig> getChannelDetections() {
return channelDetections;
}
public void setChannelDetections(List<ChannelDetectionsConfig> channelDetections) {
this.channelDetections = channelDetections;
}
}