diff --git a/src/main/java/qupath/ext/biop/abba/ABBAExtension.java b/src/main/java/qupath/ext/biop/abba/ABBAExtension.java index f185dc6..fb4929b 100644 --- a/src/main/java/qupath/ext/biop/abba/ABBAExtension.java +++ b/src/main/java/qupath/ext/biop/abba/ABBAExtension.java @@ -3,29 +3,39 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import qupath.lib.common.Version; +import qupath.lib.gui.ActionTools; import qupath.lib.gui.QuPathGUI; import qupath.lib.gui.extensions.GitHubProject; import qupath.lib.gui.extensions.QuPathExtension; +import qupath.lib.gui.tools.MenuTools; /** - * Install Warpy as an extension. + * Install ABBA extension as an extension. *

- * Installs Warpy into QuPath, adding some metadata and adds the necessary global variables to QuPath's Preferences + * Installs ABBA extension into QuPath, adding some metadata and adds the necessary global variables to QuPath's Preferences * * @author Nicolas Chiaruttini */ public class ABBAExtension implements QuPathExtension, GitHubProject { private final static Logger logger = LoggerFactory.getLogger(ABBAExtension.class); - @Override public GitHubRepo getRepository() { return GitHubRepo.create("QuPath ABBA Extension", "biop", "qupath-extension-abba"); } + private static boolean alreadyInstalled = false; + @Override public void installExtension(QuPathGUI qupath) { - + if (alreadyInstalled) + return; + alreadyInstalled = true; + var actionLoadAtlasRois = ActionTools.createAction(new LoadAtlasRoisToQuPathCommand(qupath), "Load Atlas Annotations into Open Image"); + + MenuTools.addMenuItems(qupath.getMenu("Extensions", false), + MenuTools.createMenu("ABBA",actionLoadAtlasRois) + ); } @Override diff --git a/src/main/java/qupath/ext/biop/abba/AtlasTools.java b/src/main/java/qupath/ext/biop/abba/AtlasTools.java new file mode 100644 index 0000000..f762f0d --- /dev/null +++ b/src/main/java/qupath/ext/biop/abba/AtlasTools.java @@ -0,0 +1,236 @@ +package qupath.ext.biop.abba; + +import ij.gui.Roi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qupath.ext.biop.abba.struct.AtlasHelper; +import qupath.ext.biop.abba.struct.AtlasNode; +import qupath.ext.biop.abba.struct.AtlasOntology; +import qupath.imagej.tools.IJTools; +import qupath.lib.common.ColorTools; +import qupath.lib.gui.QuPathGUI; +import qupath.lib.images.ImageData; +import qupath.lib.images.servers.ImageServer; +import qupath.lib.images.servers.RotatedImageServer; +import qupath.lib.measurements.MeasurementList; +import qupath.lib.measurements.MeasurementListFactory; +import qupath.lib.objects.PathObject; +import qupath.lib.objects.PathObjectTools; +import qupath.lib.objects.PathObjects; +import qupath.lib.projects.Project; +import qupath.lib.projects.ProjectImageEntry; +import qupath.lib.projects.Projects; +import qupath.lib.roi.RoiTools; +import qupath.lib.roi.interfaces.ROI; +import qupath.lib.scripting.QP; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class AtlasTools { + + final static Logger logger = LoggerFactory.getLogger(AtlasTools.class); + + private static QuPathGUI qupath = QuPathGUI.getInstance(); + + private static String title = "Load ABBA RoiSets from current QuPath project"; + + static private PathObject createAnnotationHierarchy(List annotations) { + + // Map the ID of the annotation to ease finding parents + Map mappedAnnotations = + annotations + .stream() + .collect( + Collectors.toMap(e -> (int) (e.getMeasurementList().getMeasurementValue("ID")), e -> e) + ); + + AtomicReference rootReference = new AtomicReference<>(); + + mappedAnnotations.forEach((id, annotation) -> { + PathObject parent = mappedAnnotations.get((int) annotation.getMeasurementList().getMeasurementValue("Parent ID")); + if (parent != null) { + parent.addPathObject(annotation); + } else { + // Found the root Path Object + rootReference.set(annotation); + } + }); + + // Return just the root annotation from the atlas + return rootReference.get(); + } + + static PathObject getWarpedAtlasRegions(ImageData imageData, String atlasName, boolean splitLeftRight) { + + List annotations = getFlattenedWarpedAtlasRegions(imageData, atlasName, splitLeftRight); // TODO + if (splitLeftRight) { + List annotationsLeft = annotations + .stream() + .filter(po -> po.getPathClass().isDerivedFrom(QP.getPathClass("Left"))) + .collect(Collectors.toList()); + + List annotationsRight = annotations + .stream() + .filter(po -> po.getPathClass().isDerivedFrom(QP.getPathClass("Right"))) + .collect(Collectors.toList()); + + PathObject rootLeft = createAnnotationHierarchy(annotationsLeft); + PathObject rootRight = createAnnotationHierarchy(annotationsRight); + ROI rootFused = RoiTools.combineROIs(rootLeft.getROI(), rootRight.getROI(), RoiTools.CombineOp.ADD); + PathObject rootObject = PathObjects.createAnnotationObject(rootFused); + rootObject.setName("Root"); + rootObject.addPathObject(rootLeft); + rootObject.addPathObject(rootRight); + return rootObject; // TODO + } else { + return createAnnotationHierarchy(annotations); + } + + } + + public static List getFlattenedWarpedAtlasRegions(ImageData imageData, String atlasName, boolean splitLeftRight) { + Project project = qupath.getProject(); + + // Get the project folder and get the ontology + Path ontologyPath = Paths.get(Projects.getBaseDirectory(project).getAbsolutePath(), atlasName+"-Ontology.json"); + AtlasOntology ontology = AtlasHelper.openOntologyFromJsonFile(ontologyPath.toString()); + + // Loop through each ImageEntry + ProjectImageEntry entry = project.getEntry(imageData); + + Path roisetPath = Paths.get(entry.getEntryPath().toString(), "ABBA-Roiset-"+atlasName+".zip"); + if (!Files.exists(roisetPath)) { + logger.info("No RoiSets found in {}", roisetPath); + return null; + } + + // Get all the ROIs and add them as PathAnnotations + List rois = RoiSetLoader.openRoiSet(roisetPath.toAbsolutePath().toFile()); + logger.info("Loading {} Atlas Regions for {}", rois.size(), entry.getImageName()); + + Roi left = rois.get(rois.size() - 2); + Roi right = rois.get(rois.size() - 1); + + rois.remove(left); + rois.remove(right); + + // Rotation for rotated servers + ImageServer server = imageData.getServer(); + + AffineTransform transform = null; + + if (server instanceof RotatedImageServer) { + // The roi will need to be transformed before being imported + // First : get the rotation + RotatedImageServer ris = (RotatedImageServer) server; + switch (ris.getRotation()) { + case ROTATE_NONE: // No rotation. + break; + case ROTATE_90: // Rotate 90 degrees clockwise. + transform = AffineTransform.getRotateInstance(Math.PI/2.0); + transform.translate(0, -server.getWidth()); + break; + case ROTATE_180: // Rotate 180 degrees. + transform = AffineTransform.getRotateInstance(Math.PI); + transform.translate(-server.getWidth(), -server.getHeight()); + break; + case ROTATE_270: // Rotate 270 degrees + transform = AffineTransform.getRotateInstance(Math.PI*3.0/2.0); + transform.translate(-server.getHeight(), 0); + break; + default: + System.err.println("Unknow rotation for rotated image server: "+ris.getRotation()); + } + } + + AffineTransform finalTransform = transform; + + List annotations = rois.stream().map(roi -> { + // Create the PathObject + + //PathObject object = IJTools.convertToAnnotation( imp, imageData.getServer(), roi, 1, null ); + PathObject object = PathObjects.createAnnotationObject(IJTools.convertToROI(roi, 0, 0, 1, null)); + + // Handles rotated image server + if (finalTransform !=null) { + object = PathObjectTools.transformObject(object, finalTransform, true); + } + + // Add metadata to object as acquired from the Ontology + int object_id = Integer.parseInt(roi.getName()); + // Get associated information + AtlasNode node = ontology.getNodeFromId(object_id); + String name = node.data().get(ontology.getNamingProperty()); + System.out.println("node:"+node.getId()+":"+name); + object.setName(name); + object.getMeasurementList().putMeasurement("ID", node.getId()); + if (node.parent()!=null) { + object.getMeasurementList().putMeasurement("Parent ID", node.parent().getId()); + } + object.getMeasurementList().putMeasurement("Side", 0); + object.setPathClass(QP.getPathClass(name)); + object.setLocked(true); + int[] rgba = node.getColor(); + int color = ColorTools.packRGB(rgba[0], rgba[1], rgba[2]); + object.setColorRGB(color); + return object; + }).collect(Collectors.toList()); + + if (splitLeftRight) { + ROI leftROI = IJTools.convertToROI(left, 0, 0, 1, null); + ROI rightROI = IJTools.convertToROI(right, 0, 0, 1, null); + List splitObjects = new ArrayList<>(); + for (PathObject annotation : annotations) { + ROI shapeLeft = RoiTools.combineROIs(leftROI, annotation.getROI(), RoiTools.CombineOp.INTERSECT); + if (!shapeLeft.isEmpty()) { + PathObject objectLeft = PathObjects.createAnnotationObject(shapeLeft, annotation.getPathClass(), duplicateMeasurements(annotation.getMeasurementList())); + objectLeft.setName(annotation.getName()); + objectLeft.setPathClass(QP.getDerivedPathClass(QP.getPathClass("Left"), annotation.getPathClass().getName())); + objectLeft.setColorRGB(annotation.getColorRGB()); + objectLeft.setLocked(true); + splitObjects.add(objectLeft); + } + + ROI shapeRight = RoiTools.combineROIs(rightROI, annotation.getROI(), RoiTools.CombineOp.INTERSECT); + if (!shapeRight.isEmpty()) { + PathObject objectRight = PathObjects.createAnnotationObject(shapeRight, annotation.getPathClass(), duplicateMeasurements(annotation.getMeasurementList())); + objectRight.setName(annotation.getName()); + objectRight.setPathClass(QP.getDerivedPathClass(QP.getPathClass("Right"), annotation.getPathClass().getName())); + objectRight.setColorRGB(annotation.getColorRGB()); + objectRight.setLocked(true); + splitObjects.add(objectRight); + } + + } + return splitObjects; + } else { + return annotations; + } + } + + public static void loadWarpedAtlasAnnotations(ImageData imageData, String atlasName, boolean splitLeftRight) { + imageData.getHierarchy().addPathObject(getWarpedAtlasRegions(imageData, atlasName, splitLeftRight)); + imageData.getHierarchy().fireHierarchyChangedEvent(AtlasTools.class); + } + + private static MeasurementList duplicateMeasurements(MeasurementList measurements) { + MeasurementList list = MeasurementListFactory.createMeasurementList(measurements.size(), MeasurementList.MeasurementListType.GENERAL); + + for (int i = 0; i < measurements.size(); i++) { + String name = measurements.getMeasurementName(i); + double value = measurements.getMeasurementValue(i); + list.addMeasurement(name, value); + } + return list; + } + +} diff --git a/src/main/java/qupath/ext/biop/abba/Dummy.java b/src/main/java/qupath/ext/biop/abba/Dummy.java deleted file mode 100644 index 5a85afd..0000000 --- a/src/main/java/qupath/ext/biop/abba/Dummy.java +++ /dev/null @@ -1,8 +0,0 @@ -package qupath.ext.biop.abba; - -public class Dummy { - - public void test() { - - } -} diff --git a/src/main/java/qupath/ext/biop/abba/LoadAtlasRoisToQuPathCommand.java b/src/main/java/qupath/ext/biop/abba/LoadAtlasRoisToQuPathCommand.java new file mode 100644 index 0000000..33defe2 --- /dev/null +++ b/src/main/java/qupath/ext/biop/abba/LoadAtlasRoisToQuPathCommand.java @@ -0,0 +1,49 @@ +package qupath.ext.biop.abba; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import qupath.lib.display.ImageDisplay; +import qupath.lib.gui.QuPathGUI; +import qupath.lib.gui.dialogs.Dialogs; +import qupath.lib.images.ImageData; + +public class LoadAtlasRoisToQuPathCommand implements Runnable { + + private static QuPathGUI qupath; + + private boolean splitLeftRight; + private boolean doRun; + + public LoadAtlasRoisToQuPathCommand( final QuPathGUI qupath) { + this.qupath = qupath; + } + + public void run() { + + String splitMode = + Dialogs.showChoiceDialog("Load Brain RoiSets into Image", + "This will load any RoiSets Exported using the ABBA tool onto the current image.\nContinue?", new String[]{"Split Left and Right Regions", "Do not split"}, "Do not split"); + + switch (splitMode) { + case "Do not split" : + splitLeftRight = false; + doRun = true; + break; + case "Split Left and Right Regions" : + splitLeftRight = true; + doRun = true; + break; + default: + // null returned -> cancelled + doRun = false; + return; + } + if (doRun) { + ImageData imageData = qupath.getImageData(); + // TODO : Find atlas name + AtlasTools.loadWarpedAtlasAnnotations(imageData, "Adult Mouse Brain - Allen Brain Atlas V3", splitLeftRight); + System.out.println("Import DONE"); + } + } + +} \ No newline at end of file diff --git a/src/main/java/qupath/ext/biop/abba/RoiSetLoader.java b/src/main/java/qupath/ext/biop/abba/RoiSetLoader.java new file mode 100644 index 0000000..f1c0d3e --- /dev/null +++ b/src/main/java/qupath/ext/biop/abba/RoiSetLoader.java @@ -0,0 +1,69 @@ +package qupath.ext.biop.abba; + +import ij.gui.Roi; +import ij.io.RoiDecoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class RoiSetLoader { + final static Logger logger = LoggerFactory.getLogger( RoiSetLoader.class); + + // Taken directly from the RoiManager, so as to be able to run it concurrently + // since the RoiManager only allows for one instance of itself to exist... + public static ArrayList openRoiSet( File path ) { + ZipInputStream in = null; + ByteArrayOutputStream out = null; + int nRois = 0; + ArrayList rois = new ArrayList<>(); + try { + in = new ZipInputStream(new FileInputStream(path)); + byte[] buf = new byte[1024]; + int len; + ZipEntry entry = in.getNextEntry(); + while (entry != null) { + String name = entry.getName(); + if (name.endsWith(".roi")) { + out = new ByteArrayOutputStream(); + while ((len = in.read(buf)) > 0) + out.write(buf, 0, len); + out.close(); + byte[] bytes = out.toByteArray(); + RoiDecoder rd = new RoiDecoder(bytes, name); + Roi roi = rd.getRoi(); + if (roi != null) { + name = name.substring(0, name.length() - 4); + rois.add(roi); + nRois++; + } + } + entry = in.getNextEntry(); + } + in.close(); + } catch ( IOException e) { + e.printStackTrace(); + } finally { + if (in != null) + try { + in.close(); + } catch (IOException e) { + } + if (out != null) + try { + out.close(); + } catch (IOException e) { + } + } + if (nRois == 0) { + logger.error("This ZIP archive does not contain '.roi' files: {}", path); + } + return rois; + } +} diff --git a/src/main/java/qupath/ext/biop/abba/struct/AtlasHelper.java b/src/main/java/qupath/ext/biop/abba/struct/AtlasHelper.java new file mode 100644 index 0000000..5085519 --- /dev/null +++ b/src/main/java/qupath/ext/biop/abba/struct/AtlasHelper.java @@ -0,0 +1,186 @@ +/*- + * #%L + * Repo containing a standard API for Atlases and some example ones + * %% + * Copyright (C) 2021 EPFL + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package qupath.ext.biop.abba.struct; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.*; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Static atlas ontology helper functions + */ +public class AtlasHelper { + + public static Map buildIdToAtlasNodeMap(AtlasNode root) { + Map result = new HashMap<>(); + return appendToIdToAtlasNodeMap(result, root); + } + + private static Map appendToIdToAtlasNodeMap(Map map, AtlasNode node) { + map.put(node.getId(), node); + node.children().forEach(child -> { + appendToIdToAtlasNodeMap(map, child); + }); + return map; + } + + public static AtlasOntology openOntologyFromJsonFile(String path) { + File ontologyFile = new File(path); + if (ontologyFile.exists()) { + Gson gson = new Gson(); + try { + FileReader fr = new FileReader(ontologyFile.getAbsoluteFile()); + SerializableOntology ontology = gson.fromJson(new FileReader(ontologyFile.getAbsoluteFile()), SerializableOntology.class); + ontology.initialize(); + fr.close(); + return ontology; + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } else return null; + } + + public static class SerializableOntology implements AtlasOntology{ + String name; + String namingProperty; + SerializableAtlasNode root; + transient Map idToAtlasNodeMap; + + public SerializableOntology(AtlasOntology ontology) { + this.name = ontology.getName(); + this.root = new SerializableAtlasNode(ontology.getRoot(), null); + this.namingProperty = ontology.getNamingProperty(); + } + + static void setAllParents(SerializableAtlasNode node) { + node.children.forEach(child -> { + child.setParent(node); + setAllParents(child); + } + ); + } + + @Override + public String getName() { + return name; + } + + @Override + public void initialize() throws Exception { + setAllParents(root); + idToAtlasNodeMap = AtlasHelper.buildIdToAtlasNodeMap(root); + } + + @Override + public void setDataSource(URL dataSource) { + + } + + @Override + public URL getDataSource() { + return null; + } + + @Override + public AtlasNode getRoot() { + return root; + } + + @Override + public AtlasNode getNodeFromId(int id) { + return idToAtlasNodeMap.get(id); + } + + @Override + public String getNamingProperty() { + return namingProperty; + } + + @Override + public void setNamingProperty(String namingProperty) { + this.namingProperty = namingProperty; + } + } + + public static class SerializableAtlasNode implements AtlasNode { + + final public int id; + final public int[] color; + final public Map data; + final public List children; + transient public SerializableAtlasNode parent; + + public SerializableAtlasNode(AtlasNode node, SerializableAtlasNode parent) { + this.id = node.getId(); + this.data = node.data(); + this.parent = parent; + this.color = node.getColor(); + children = new ArrayList<>(); + node.children().forEach(n -> { + children.add(new SerializableAtlasNode(n, SerializableAtlasNode.this)); + }); + } + + @Override + public Integer getId() { + return id; + } + + @Override + public int[] getColor() { + return color; + } + + @Override + public Map data() { + return data; + } + + @Override + public AtlasNode parent() { + return parent; + } + + public void setParent(SerializableAtlasNode parent) { + this.parent = parent; + } + + @Override + public List children() { + return children; + } + } + +} diff --git a/src/main/java/qupath/ext/biop/abba/struct/AtlasNode.java b/src/main/java/qupath/ext/biop/abba/struct/AtlasNode.java new file mode 100644 index 0000000..7410057 --- /dev/null +++ b/src/main/java/qupath/ext/biop/abba/struct/AtlasNode.java @@ -0,0 +1,46 @@ +/*- + * #%L + * Repo containing a standard API for Atlases and some example ones + * %% + * Copyright (C) 2021 EPFL + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package qupath.ext.biop.abba.struct; + +import java.util.List; +import java.util.Map; + +public interface AtlasNode { + Integer getId(); + + int[] getColor(); + + /** Gets the data associated with the node. */ + Map data(); + + /** Gets the parent of this node. */ + AtlasNode parent(); + + /** + * Gets the node's children. If this list is mutated, the children will be + * affected accordingly. It is the responsibility of the caller to ensure + * continued integrity, particularly of parent linkages. + */ + List children(); + + +} diff --git a/src/main/java/qupath/ext/biop/abba/struct/AtlasOntology.java b/src/main/java/qupath/ext/biop/abba/struct/AtlasOntology.java new file mode 100644 index 0000000..8931d6f --- /dev/null +++ b/src/main/java/qupath/ext/biop/abba/struct/AtlasOntology.java @@ -0,0 +1,44 @@ +/*- + * #%L + * Repo containing a standard API for Atlases and some example ones + * %% + * Copyright (C) 2021 EPFL + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package qupath.ext.biop.abba.struct; + +import java.net.URL; + +public interface AtlasOntology { + + String getName(); + + void initialize() throws Exception; + + void setDataSource(URL dataSource); + + URL getDataSource(); + + AtlasNode getRoot(); + + AtlasNode getNodeFromId(int id); + + String getNamingProperty(); + + void setNamingProperty(String namingProperty); + +}