Last active
December 2, 2020 19:04
-
-
Save petebankhead/db8548a0112bad089492061bf8046430 to your computer and use it in GitHub Desktop.
Demo script showing one way to use the DeepCell Kiosk ImageJ plugin from QuPath
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Run cell detection using the DeepCell Kiosk ImageJ plugin from | |
* https://github.com/vanvalenlab/kiosk-imagej-plugin | |
* | |
* For authorship & license of Deep Cell see http://deepcell.org | |
* | |
* To use the script, drag the Kiosk plugin on top of QuPath to make it available as an extension. | |
* Then select a (small!) region of interest in a compatible image and run this script from QuPath's script editor. | |
* | |
* Note that the image needs to be in a compatible format - | |
* see https://github.com/vanvalenlab/intro-to-deepcell/tree/master/deepcell_dot_org#formatting-data-for-web-based-models | |
* | |
* This is really just a proof-of-concept, which doesn't do any fancy tiling / error-checking. | |
* | |
* See https://twitter.com/NoahGreenwald/status/1334153336178704384 for beginnings. | |
* | |
* @author Pete Bankhead (just QuPath & this script... not DeepCell) | |
*/ | |
import ij.ImagePlus | |
import ij.io.Opener | |
import org.vanvalenlab.KioskJob | |
import org.vanvalenlab.KioskJobManager | |
import org.vanvalenlab.exceptions.KioskJobFailedException | |
import qupath.imagej.processing.RoiLabeling | |
import qupath.imagej.tools.IJTools | |
import qupath.lib.analysis.features.ObjectMeasurements | |
import qupath.lib.common.ThreadTools | |
import qupath.lib.gui.prefs.PathPrefs | |
import qupath.lib.images.servers.ImageServer | |
import qupath.lib.images.servers.TransformedServerBuilder | |
import qupath.lib.objects.PathObject | |
import qupath.lib.objects.PathObjects | |
import qupath.lib.regions.RegionRequest | |
import java.awt.image.BufferedImage | |
import java.util.concurrent.Callable | |
import java.util.concurrent.Executors | |
import java.util.concurrent.Future | |
import java.util.concurrent.TimeUnit | |
import static qupath.lib.gui.scripting.QPEx.* | |
/** | |
* Set the main parameters | |
*/ | |
def options = new KioskOptions(jobType: "multiplex") // Specify jobType (may be multiplex or segmentation) | |
boolean includeMeasurements = true // Optionally include default shape/intensity measurements | |
int[] channels = [] // Optionally specify channels to extract (0-based) | |
double requestedPixelSize = -1 // Optionally specify the target pixel size for resizing the image | |
int nThreads = PathPrefs.numCommandThreadsProperty().value // Do multithreading | |
// Get Current image | |
def imageData = getCurrentImageData() | |
def server = imageData.getServer() | |
// Extract channels if necessary | |
if (channels) | |
server = new TransformedServerBuilder(server).extractChannels(channels as int[]).build() | |
// Determine downsample | |
double downsample = 1 | |
if (requestedPixelSize > 0) | |
downsample = requestedPixelSize / server.getPixelCalibration().getAveragedPixelSize() | |
// Loop through selected annotations, or use the full image if nothing is selected | |
def hierarchy = imageData.getHierarchy() | |
def selected = new ArrayList<>(hierarchy.getSelectionModel().getSelectedObjects()) | |
if (selected.isEmpty()) | |
selected = [hierarchy.getRootObject()] | |
// Run multithreaded | |
def pool = Executors.newFixedThreadPool(nThreads, ThreadTools.createThreadFactory("deepCell", true)) | |
def newObjects = new LinkedHashMap<PathObject, Future<List<PathObject>>>() | |
for (def parent in selected) { | |
def parent2 = parent | |
def future = pool.submit({ -> | |
return detectCells(server, parent2, downsample, includeMeasurements, options) | |
} as Callable) | |
newObjects.put(parent2, future) | |
} | |
print "Waiting for ${selected.size()} task(s) to complete" | |
pool.shutdown() | |
pool.awaitTermination(24, TimeUnit.HOURS) | |
// Don't update objects until everything is done | |
for (entry in newObjects.entrySet()) { | |
def future = entry.getValue() | |
def parent = entry.getKey() | |
if (future.isCancelled()) { | |
println 'Cancelled ' + parent | |
continue | |
} | |
def detections = future.get() | |
println 'Returned: ' + detections.size() | |
parent.clearPathObjects() | |
parent.addPathObjects(detections) | |
} | |
hierarchy.fireHierarchyChangedEvent(this) | |
print "Done!" | |
/** | |
* Detect cells corresponding to an image region in QuPath | |
* | |
* @param server | |
* @param parent | |
* @param downsample | |
* @param includeMeasurements | |
* @param options | |
* @return | |
*/ | |
static List<PathObject> detectCells(ImageServer<BufferedImage> server, PathObject parent, double downsample, boolean includeMeasurements, KioskOptions options) { | |
RegionRequest request | |
def parentROI = parent.getROI() | |
if (parentROI) | |
request = RegionRequest.createInstance(server.getPath(), downsample, parentROI) | |
else | |
request = RegionRequest.createInstance(server, downsample) | |
def pathImage = IJTools.convertToImagePlus(server, request) | |
def imp = pathImage.getImage() | |
imp.setFileInfo(null) | |
def impLabeled = runJob(KioskJobManager.getFilePath(imp), options) | |
def ip = impLabeled.getProcessor() | |
int n = ip.getStatistics().max | |
def roisIJ = RoiLabeling.labelsToConnectedROIs(impLabeled.getProcessor(), n) | |
getQuPath().logger.info('ROIs: ' + roisIJ.size()) | |
def measurements = ObjectMeasurements.Measurements.values() | |
def compartments = ObjectMeasurements.Compartments.values() | |
def detections = [] | |
for (r in roisIJ) { | |
def roi = IJTools.convertToROI(r, pathImage) | |
if (parentROI != null && !parentROI.contains(roi.getCentroidX(), roi.getCentroidY())) | |
continue | |
def detection = PathObjects.createDetectionObject(roi) | |
if (includeMeasurements) { | |
ObjectMeasurements.addShapeMeasurements(detection, server.getPixelCalibration(), ObjectMeasurements.ShapeFeatures.values()) | |
ObjectMeasurements.addIntensityMeasurements(server, detection, downsample, measurements as List, compartments as List) | |
} | |
detections.add(detection) | |
} | |
getQuPath().logger.info('Detections: ' + detections.size()) | |
return detections | |
} | |
/** | |
* Run the job using the Kiosk plugin | |
* | |
* @param imageFile | |
* @param options | |
* @return | |
* @throws IOException | |
* @throws KioskJobFailedException | |
*/ | |
static ImagePlus runJob(String imageFile, KioskOptions options) throws IOException, KioskJobFailedException { | |
def job = new KioskJob(options.host, options.jobType) | |
job.create(imageFile) | |
String finalStatus = job.waitForFinalStatus(options.statusUpdateMillis) | |
job.expire(options.jobExpiresSec) | |
if (finalStatus.equals("failed")) { | |
throw new KioskJobFailedException(job.getErrorReason()) | |
} else if (finalStatus.equals("done")) { | |
def outputPath = job.getOutputPath() | |
Opener opener = new Opener() | |
return opener.openURL(outputPath) | |
} else { | |
throw new RuntimeException("Unknown final status: " + finalStatus) | |
} | |
} | |
/** | |
* Helper class for storing key options | |
*/ | |
class KioskOptions { | |
String jobType = "Multiplex" | |
String host = "http://deepcell.org" | |
int statusUpdateMillis = 5000 | |
int jobExpiresSec = 3600 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment