use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class PathClassifierTools method setIntensityClassification.
/**
* Assign cell classifications as positive or negative based upon a specified measurement, using up to 3 intensity bins.
*
* An IllegalArgumentException is thrown if < 1 or > 3 intensity thresholds are provided.<p>
* If the object does not have the required measurement, its {@link PathClass} will be set to its
* first 'non-intensity' ancestor {@link PathClass}.
* <p>
* Note that as of v0.3.0, all ignored classes (see {@link PathClassTools#isIgnoredClass(PathClass)} are ignored and therefore
* will not be 'intensity classified'.
*
* @param pathObject the object to classify.
* @param measurementName the name of the measurement to use for thresholding.
* @param thresholds between 1 and 3 intensity thresholds, used to indicate negative/positive, or negative/1+/2+/3+
* @return the PathClass of the object after running this method.
*/
public static PathClass setIntensityClassification(final PathObject pathObject, final String measurementName, final double... thresholds) {
if (thresholds.length == 0 || thresholds.length > 3)
throw new IllegalArgumentException("Between 1 and 3 intensity thresholds required!");
// Can't perform any classification if measurement is null or blank
if (measurementName == null || measurementName.isEmpty())
throw new IllegalArgumentException("Measurement name cannot be empty or null!");
PathClass baseClass = PathClassTools.getNonIntensityAncestorClass(pathObject.getPathClass());
// Don't do anything with the 'ignore' class
if (!PathClassTools.isNullClass(baseClass) && PathClassTools.isIgnoredClass(baseClass))
return pathObject.getPathClass();
double intensityValue = pathObject.getMeasurementList().getMeasurementValue(measurementName);
boolean singleThreshold = thresholds.length == 1;
if (// If the measurement is missing, reset to base class
Double.isNaN(intensityValue))
pathObject.setPathClass(baseClass);
else if (intensityValue < thresholds[0])
pathObject.setPathClass(PathClassFactory.getNegative(baseClass));
else {
if (singleThreshold)
pathObject.setPathClass(PathClassFactory.getPositive(baseClass));
else if (thresholds.length >= 3 && intensityValue >= thresholds[2])
pathObject.setPathClass(PathClassFactory.getThreePlus(baseClass));
else if (thresholds.length >= 2 && intensityValue >= thresholds[1])
pathObject.setPathClass(PathClassFactory.getTwoPlus(baseClass));
else if (intensityValue >= thresholds[0])
pathObject.setPathClass(PathClassFactory.getOnePlus(baseClass));
}
return pathObject.getPathClass();
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class DistanceTools method detectionToAnnotationDistances.
/**
* Compute the distance for all detection object centroids to the closest annotation with each valid, not-ignored classification and add
* the result to the detection measurement list.
* @param imageData
* @param splitClassNames if true, split the classification name. For example, if an image contains classifications for both "CD3: CD4" and "CD3: CD8",
* distances will be calculated for all components (e.g. "CD3", "CD4" and "CD8").
*/
public static void detectionToAnnotationDistances(ImageData<?> imageData, boolean splitClassNames) {
var server = imageData.getServer();
var hierarchy = imageData.getHierarchy();
var annotations = hierarchy.getAnnotationObjects();
var detections = hierarchy.getCellObjects();
if (detections.isEmpty())
detections = hierarchy.getDetectionObjects();
// TODO: Support TMA cores
if (hierarchy.getTMAGrid() != null)
logger.warn("Detection to annotation distances command currently ignores TMA grid information!");
var pathClasses = annotations.stream().map(p -> p.getPathClass()).filter(p -> p != null && p.isValid() && !PathClassTools.isIgnoredClass(p)).collect(Collectors.toSet());
var cal = server.getPixelCalibration();
String xUnit = cal.getPixelWidthUnit();
String yUnit = cal.getPixelHeightUnit();
double pixelWidth = cal.getPixelWidth().doubleValue();
double pixelHeight = cal.getPixelHeight().doubleValue();
if (!xUnit.equals(yUnit))
throw new IllegalArgumentException("Pixel width & height units do not match! Width " + xUnit + ", height " + yUnit);
String unit = xUnit;
for (PathClass pathClass : pathClasses) {
if (splitClassNames) {
var names = PathClassTools.splitNames(pathClass);
for (var name : names) {
logger.debug("Computing distances for {}", pathClass);
var filteredAnnotations = annotations.stream().filter(a -> PathClassTools.containsName(a.getPathClass(), name)).collect(Collectors.toList());
if (!filteredAnnotations.isEmpty()) {
String measurementName = "Distance to annotation with " + name + " " + unit;
centroidToBoundsDistance2D(detections, filteredAnnotations, pixelWidth, pixelHeight, measurementName);
}
}
} else {
logger.debug("Computing distances for {}", pathClass);
var filteredAnnotations = annotations.stream().filter(a -> a.getPathClass() == pathClass).collect(Collectors.toList());
if (!filteredAnnotations.isEmpty()) {
String name = "Distance to annotation " + pathClass + " " + unit;
centroidToBoundsDistance2D(detections, filteredAnnotations, pixelWidth, pixelHeight, name);
}
}
}
hierarchy.fireObjectMeasurementsChangedEvent(DistanceTools.class, detections);
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class SimpleThresholdCommand method updateClassification.
private void updateClassification() {
// for (var viewer : qupath.getViewers()) {
// var imageData = viewer.getImageData();
// if (imageData == null) {
// selectedOverlay.set(null);
// viewer.resetCustomPixelLayerOverlay();
// }
// }
var channel = selectedChannel.get();
var thresholdValue = threshold.get();
var resolution = selectedResolution.get();
if (channel == null || thresholdValue == null || resolution == null) {
resetOverlays();
return;
}
var feature = selectedPrefilter.get();
double sigmaValue = sigma.get();
PixelClassifier classifier;
List<ImageOp> ops = new ArrayList<>();
if (feature != null && sigmaValue > 0) {
ops.add(feature.buildOp(sigmaValue));
}
ops.add(ImageOps.Threshold.threshold(threshold.get()));
Map<Integer, PathClass> classifications = new LinkedHashMap<>();
classifications.put(0, classificationsBelow.getSelectionModel().getSelectedItem());
classifications.put(1, classificationsAbove.getSelectionModel().getSelectedItem());
var op = ImageOps.Core.sequential(ops);
var transformer = ImageOps.buildImageDataOp(channel).appendOps(op);
classifier = PixelClassifiers.createClassifier(transformer, resolution.getPixelCalibration(), classifications);
// Create classifier
var overlay = PixelClassificationOverlay.create(qupath.getOverlayOptions(), classifier);
overlay.setLivePrediction(true);
var previousOverlay = selectedOverlay.get();
if (previousOverlay != null)
previousOverlay.stop();
selectedOverlay.set(overlay);
this.currentClassifier.set(classifier);
ensureOverlays();
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class PixelClassifierTraining method updateTrainingData.
private synchronized ClassifierTrainingData updateTrainingData(Map<PathClass, Integer> labelMap, Collection<ImageData<BufferedImage>> imageDataCollection) throws IOException {
if (imageDataCollection.isEmpty()) {
resetTrainingData();
return null;
}
Map<PathClass, Integer> labels = new LinkedHashMap<>();
boolean hasLockedAnnotations = false;
if (labelMap == null) {
Set<PathClass> pathClasses = new TreeSet<>((p1, p2) -> p1.toString().compareTo(p2.toString()));
for (var imageData : imageDataCollection) {
// Get labels for all annotations
Collection<PathObject> annotations = imageData.getHierarchy().getAnnotationObjects();
for (var annotation : annotations) {
if (isTrainableAnnotation(annotation, true)) {
var pathClass = annotation.getPathClass();
pathClasses.add(pathClass);
// We only use boundary classes for areas
if (annotation.getROI().isArea()) {
var boundaryClass = boundaryStrategy.getBoundaryClass(pathClass);
if (boundaryClass != null)
pathClasses.add(boundaryClass);
}
} else if (isTrainableAnnotation(annotation, false))
hasLockedAnnotations = true;
}
}
int lab = 0;
for (PathClass pathClass : pathClasses) {
Integer temp = Integer.valueOf(lab);
labels.put(pathClass, temp);
lab++;
}
} else {
labels.putAll(labelMap);
}
List<Mat> allFeatures = new ArrayList<>();
List<Mat> allTargets = new ArrayList<>();
for (var imageData : imageDataCollection) {
// Get features & targets for all the tiles that we need
var featureServer = getFeatureServer(imageData);
if (featureServer != null) {
var tiles = featureServer.getTileRequestManager().getAllTileRequests();
for (var tile : tiles) {
var tileFeatures = getTileFeatures(tile.getRegionRequest(), featureServer, boundaryStrategy, labels);
if (tileFeatures != null) {
allFeatures.add(tileFeatures.getFeatures());
allTargets.add(tileFeatures.getTargets());
}
}
} else {
logger.warn("Unable to generate features for {}", imageData);
}
}
// We need at least two classes for anything very meaningful to happen
int nTargets = labels.size();
if (nTargets <= 1) {
logger.warn("Unlocked annotations for at least two classes are required to train a classifier!");
if (hasLockedAnnotations)
logger.warn("Image contains annotations that *could* be used for training, except they are currently locked. Please unlock them if they should be used.");
resetTrainingData();
return null;
}
if (matTraining == null)
matTraining = new Mat();
if (matTargets == null)
matTargets = new Mat();
opencv_core.vconcat(new MatVector(allFeatures.toArray(Mat[]::new)), matTraining);
opencv_core.vconcat(new MatVector(allTargets.toArray(Mat[]::new)), matTargets);
logger.debug("Training data: {} x {}, Target data: {} x {}", matTraining.rows(), matTraining.cols(), matTargets.rows(), matTargets.cols());
if (matTraining.rows() == 0) {
logger.warn("No training data found - if you have training annotations, check the features are compatible with the current image.");
return null;
}
return new ClassifierTrainingData(labels, matTraining, matTargets);
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class SmoothFeaturesPlugin method smoothMeasurements.
/**
* Using the centroids of the ROIs within PathObjects, 'smooth' measurements by summing up the corresponding measurements of
* nearby objects, weighted by centroid distance.
*
* @param pathObjects
* @param measurements
* @param fwhmPixels
* @param fwhmString
* @param withinClass
* @param useLegacyNames
*/
// public static Set<String> smoothMeasurements(List<PathObject> pathObjects, List<String> measurements, double fwhmPixels) {
public static void smoothMeasurements(List<PathObject> pathObjects, List<String> measurements, double fwhmPixels, String fwhmString, boolean withinClass, boolean useLegacyNames) {
if (measurements.isEmpty() || pathObjects.size() <= 1)
// Collections.emptySet();
return;
if (fwhmString == null)
fwhmString = String.format("%.2f px", fwhmPixels);
double fwhmPixels2 = fwhmPixels * fwhmPixels;
double sigmaPixels = fwhmPixels / Math.sqrt(8 * Math.log(2));
double sigma2 = 2 * sigmaPixels * sigmaPixels;
double maxDist = sigmaPixels * 3;
// Maximum separation
double maxDistSq = maxDist * maxDist;
int nObjects = pathObjects.size();
// int counter = 0;
// Sort by x-coordinate - this gives us a method of breaking early
Collections.sort(pathObjects, new Comparator<PathObject>() {
@Override
public int compare(PathObject o1, PathObject o2) {
double x1 = o1.getROI().getCentroidX();
double x2 = o2.getROI().getCentroidX();
// System.out.println(String.format("(%.2f, %.2f) vs (%.2f, %.2f)", o1.getROI().getCentroidX(), o1.getROI().getCentroidY(), o2.getROI().getCentroidX(), o2.getROI().getCentroidY())); }
return Double.compare(x1, x2);
// if (x1 > x2)
// return 1;
// if (x2 < x1)
// return -1;
// System.out.println(x1 + " vs. " + x2);
// System.out.println(String.format("(%.2f, %.2f) vs (%.2f, %.2f)", o1.getROI().getCentroidX(), o1.getROI().getCentroidY(), o2.getROI().getCentroidX(), o2.getROI().getCentroidY()));
// return 0;
// return (int)Math.signum(o1.getROI().getCentroidX() - o2.getROI().getCentroidX());
}
});
// Create a LUT for distances - calculating exp every time is expensive
double[] distanceWeights = new double[(int) (maxDist + .5) + 1];
for (int i = 0; i < distanceWeights.length; i++) {
distanceWeights[i] = Math.exp(-(i * i) / sigma2);
}
System.currentTimeMillis();
float[] xCentroids = new float[nObjects];
float[] yCentroids = new float[nObjects];
PathClass[] pathClasses = new PathClass[nObjects];
int[] nearbyDetectionCounts = new int[nObjects];
float[][] measurementsWeighted = new float[nObjects][measurements.size()];
float[][] measurementDenominators = new float[nObjects][measurements.size()];
float[][] measurementValues = new float[nObjects][measurements.size()];
for (int i = 0; i < nObjects; i++) {
PathObject pathObject = pathObjects.get(i);
if (withinClass)
pathClasses[i] = pathObject.getPathClass() == null ? null : pathObject.getPathClass().getBaseClass();
ROI roi = pathObject.getROI();
xCentroids[i] = (float) roi.getCentroidX();
yCentroids[i] = (float) roi.getCentroidY();
MeasurementList measurementList = pathObject.getMeasurementList();
int ind = 0;
for (String name : measurements) {
float value = (float) measurementList.getMeasurementValue(name);
// Used to cache values
measurementValues[i][ind] = value;
// Based on distances and measurements
measurementsWeighted[i][ind] = value;
// Based on distances along
measurementDenominators[i][ind] = 1;
ind++;
}
}
String prefix, postfix, denomName, countsName;
// Use previous syntax for naming smoothed measurements
if (useLegacyNames) {
prefix = "";
postfix = String.format(" - Smoothed (FWHM %s)", fwhmString);
denomName = String.format("Smoothed denominator (local density, FWHM %s)", fwhmString);
countsName = String.format("Nearby detection counts (radius %s)", fwhmString);
} else {
prefix = String.format("Smoothed: %s: ", fwhmString);
postfix = "";
// prefix + "Weighted density";
denomName = null;
countsName = prefix + "Nearby detection counts";
// denomName = prefix + "Denominator (local density)";
// countsName = prefix + "Nearby detection counts";
}
// Loop through objects, computing predominant class based on distance weighting
for (int i = 0; i < nObjects; i++) {
// Extract the current class index
PathObject pathObject = pathObjects.get(i);
PathClass pathClass = pathClasses[i];
MeasurementList measurementList = pathObject.getMeasurementList();
float[] mValues = measurementValues[i];
float[] mWeighted = measurementsWeighted[i];
float[] mDenominator = measurementDenominators[i];
// Compute centroid distances
double xi = xCentroids[i];
double yi = yCentroids[i];
for (int j = i + 1; j < nObjects; j++) {
double xj = xCentroids[j];
double yj = yCentroids[j];
// Break early if we are already too far away
if (Math.abs(xj - xi) > maxDist) {
break;
}
double distSq = (xj - xi) * (xj - xi) + (yj - yi) * (yj - yi);
// // Check if we are close enough to have an influence
if (distSq > maxDistSq || Double.isNaN(distSq))
continue;
// Check if the class is ok, if check needed
if (withinClass && pathClass != pathClasses[j])
continue;
// Update the counts, if close enough
if (distSq < fwhmPixels2) {
nearbyDetectionCounts[i]++;
nearbyDetectionCounts[j]++;
}
// Update the class weights for both objects currently being tested
// Compute weight based on centroid distances
// double weight = Math.exp(-distSq/sigma2);
// * pathObjects.get(j).getClassProbability();
double weight = distanceWeights[(int) (Math.sqrt(distSq) + .5)];
float[] temp = measurementValues[j];
float[] tempWeighted = measurementsWeighted[j];
float[] tempDenominator = measurementDenominators[j];
for (int ind = 0; ind < measurements.size(); ind++) {
float tempVal = temp[ind];
if (Float.isNaN(tempVal))
continue;
mWeighted[ind] += tempVal * weight;
mDenominator[ind] += weight;
float tempVal2 = mValues[ind];
if (Float.isNaN(tempVal2))
continue;
tempWeighted[ind] += tempVal2 * weight;
tempDenominator[ind] += weight;
}
}
// Store the measurements
int ind = 0;
float maxDenominator = Float.NEGATIVE_INFINITY;
for (String name : measurements) {
// if (name.contains(" - Smoothed (FWHM ") || name.startsWith("Smoothed denominator (local density, ") || name.startsWith("Nearby detection counts"))
// continue;
float denominator = mDenominator[ind];
if (denominator > maxDenominator)
maxDenominator = denominator;
String nameToAdd = prefix + name + postfix;
measurementList.putMeasurement(nameToAdd, mWeighted[ind] / denominator);
// measurementsAdded.add(nameToAdd);
// measurementList.putMeasurement(name + " - weighted sum", mWeighted[ind]); // TODO: Support optionally providing weighted sums
// measurementList.addMeasurement(name + " - smoothed", mWeighted[ind] / mDenominator[ind]);
ind++;
}
if (pathObject instanceof PathDetectionObject && denomName != null) {
measurementList.putMeasurement(denomName, maxDenominator);
// measurementsAdded.add(denomName);
}
if (pathObject instanceof PathDetectionObject && countsName != null) {
measurementList.putMeasurement(countsName, nearbyDetectionCounts[i]);
// measurementsAdded.add(countsName);
}
measurementList.close();
}
System.currentTimeMillis();
// return measurementsAdded;
}
Aggregations