Skip to content

Commit

Permalink
Merge pull request #17 from petebankhead/cell-objects
Browse files Browse the repository at this point in the history
Support cell object creation
  • Loading branch information
petebankhead authored May 2, 2024
2 parents 949dc22 + f94fc85 commit 4415d4a
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 45 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ base {

ext.qupathVersion = gradle.ext.qupathVersion

// Should be Java 17 for QuPath v0.5.0
// Should be Java 21 for QuPath v0.6.0
ext.qupathJavaVersion = 21

/**
Expand Down
4 changes: 2 additions & 2 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ pluginManagement {
// Specifying it here, rather than build.gradle, makes it possible
// to include the extension as a subproject of QuPath itself
// (which is useful during development)
id 'org.bytedeco.gradle-javacpp-platform' version '1.5.9'
id 'org.bytedeco.gradle-javacpp-platform' version '1.5.10'
}
}

rootProject.name = 'qupath-extension-instanseg'
gradle.ext.qupathVersion = "0.6.0"
gradle.ext.qupathVersion = "0.6.0-SNAPSHOT"

dependencyResolutionManagement {

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/qupath/ext/instanseg/core/InstanSegTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import static qupath.lib.gui.scripting.QPEx.createTaskRunner;

public class InstanSegTask extends Task<Void> {

private static final Logger logger = LoggerFactory.getLogger(InstanSegTask.class);

private final int tileSize, nThreads;
private final List<ColorTransforms.ColorTransform> channels;
private final double downsample;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.List;

public class InstanSegUtils {

private static final Logger logger = LoggerFactory.getLogger(InstanSegUtils.class);

private InstanSegUtils() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,64 @@

import org.bytedeco.opencv.opencv_core.Mat;
import org.locationtech.jts.geom.Envelope;
import qupath.lib.experimental.pixels.OpenCVProcessor;
import qupath.lib.analysis.images.ContourTracing;
import qupath.lib.experimental.pixels.OutputHandler;
import qupath.lib.experimental.pixels.Parameters;
import qupath.lib.experimental.pixels.PixelProcessorUtils;
import qupath.lib.objects.PathObject;
import qupath.lib.objects.classes.PathClass;
import qupath.lib.objects.PathObjects;
import qupath.lib.roi.GeometryTools;
import qupath.lib.scripting.QP;
import qupath.lib.roi.interfaces.ROI;
import qupath.opencv.tools.OpenCVTools;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;

class OutputToObjectConverter implements OutputHandler.OutputToObjectConverter<Mat, Mat, Mat> {

private final OutputHandler.OutputToObjectConverter<Mat, Mat, Mat> converter = OpenCVProcessor.createDetectionConverter();
private static long seed = 1243;

@Override
public List<PathObject> convertToObjects(Parameters<Mat, Mat> params, Mat output) {
List<PathObject> detections = new ArrayList<>();
var channels = OpenCVTools.splitChannels(output);
IntStream.range(0, channels.size()).parallel().forEach(
i -> {
List<PathObject> pathObjects = converter.convertToObjects(params, channels.get(i));
PathClass pathClass = switch(i) {
case 0 -> QP.getPathClass("Nucleus3", QP.makeRGB(200, 70, 70));
case 1 -> QP.getPathClass("Cell5", QP.makeRGB(90, 220, 90));
default -> throw new IllegalArgumentException("Dunno what to make of channel " + i);
};
for (var p: pathObjects) {
p.setPathClass(pathClass);
}
detections.addAll(pathObjects);
}
);
return detections;
public List<PathObject> convertToObjects(Parameters params, Mat output) {
int nChannels = output.channels();
if (nChannels < 1 || nChannels > 2)
throw new IllegalArgumentException("Expected 1 or 2 channels, but found " + nChannels);

List<Map<Number, ROI>> roiMaps = new ArrayList<>();
for (var mat : OpenCVTools.splitChannels(output)) {
var image = OpenCVTools.matToSimpleImage(mat, 0);
roiMaps.add(
ContourTracing.createROIs(image, params.getRegionRequest(), 1, -1)
);
}
if (roiMaps.size() == 1) {
// One-channel detected, represent using detection objects
return roiMaps.get(0).values().stream()
.map(roi -> PathObjects.createDetectionObject(roi))
.collect(Collectors.toList());
} else {
// Two channels detected, represent using cell objects
// We assume that the labels are matched - and we can't have a nucleus without a cell
Map<Number, ROI> nucleusROIs = roiMaps.get(0);
Map<Number, ROI> cellROIs = roiMaps.get(1);
List<PathObject> cells = new ArrayList<>();
var rng = new Random(seed);
for (var entry : cellROIs.entrySet()) {
var cell = entry.getValue();
var nucleus = nucleusROIs.getOrDefault(entry.getKey(), null);
var cellObject = PathObjects.createCellObject(cell, nucleus);
cellObject.setColor(
rng.nextInt(255),
rng.nextInt(255),
rng.nextInt(255)
);
cells.add(cellObject);
}
return cells;
}
}

static class PruneObjectOutputHandler<S, T, U> implements OutputHandler<S, T, U> {
Expand Down Expand Up @@ -66,7 +87,7 @@ public boolean handleOutput(Parameters<S, T> params, U output) {
parentOrProxy.clearChildObjects();

// remove features within N pixels of the region request boundaries
var regionRequest = GeometryTools.createRectangle(
var bounds = GeometryTools.createRectangle(
params.getRegionRequest().getX(), params.getRegionRequest().getY(),
params.getRegionRequest().getWidth(), params.getRegionRequest().getHeight());

Expand All @@ -76,7 +97,7 @@ public boolean handleOutput(Parameters<S, T> params, U output) {
// QP.addObject(PathObjects.createAnnotationObject(GeometryTools.geometryToROI(regionRequest, ImagePlane.getPlane(0,0))));

newObjects = newObjects.parallelStream()
.filter(p -> doesntTouchBoundaries(p.getROI().getGeometry().getEnvelopeInternal(), regionRequest.getEnvelopeInternal(), boundaryThreshold, maxX, maxY))
.filter(p -> doesntTouchBoundaries(p.getROI().getGeometry().getEnvelopeInternal(), bounds.getEnvelopeInternal(), boundaryThreshold, maxX, maxY))
.toList();

if (!newObjects.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import java.awt.image.BufferedImage;
import java.util.Collection;
import java.util.List;
import java.util.ResourceBundle;

/**
Expand Down
16 changes: 0 additions & 16 deletions src/main/java/qupath/ext/instanseg/ui/PytorchManager.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
/**
* Copyright 2023 University of Edinburgh
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package qupath.ext.instanseg.ui;

import ai.djl.engine.Engine;
Expand Down

0 comments on commit 4415d4a

Please sign in to comment.