Search in sources :

Example 11 with PathClass

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 &lt; 1 or &gt; 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();
}
Also used : PathClass(qupath.lib.objects.classes.PathClass)

Example 12 with PathClass

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);
}
Also used : PointOnGeometryLocator(org.locationtech.jts.algorithm.locate.PointOnGeometryLocator) PathClassTools(qupath.lib.objects.classes.PathClassTools) PointPairDistance(org.locationtech.jts.algorithm.distance.PointPairDistance) LoggerFactory(org.slf4j.LoggerFactory) Coordinate(org.locationtech.jts.geom.Coordinate) TreeSet(java.util.TreeSet) ArrayList(java.util.ArrayList) Location(org.locationtech.jts.geom.Location) Puntal(org.locationtech.jts.geom.Puntal) DistanceToPoint(org.locationtech.jts.algorithm.distance.DistanceToPoint) GeometryTools(qupath.lib.roi.GeometryTools) AffineTransformation(org.locationtech.jts.geom.util.AffineTransformation) GeometryCombiner(org.locationtech.jts.geom.util.GeometryCombiner) ImageData(qupath.lib.images.ImageData) ItemBoundable(org.locationtech.jts.index.strtree.ItemBoundable) Logger(org.slf4j.Logger) Collection(java.util.Collection) PathClass(qupath.lib.objects.classes.PathClass) IndexedPointInAreaLocator(org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator) Polygonal(org.locationtech.jts.geom.Polygonal) Collectors(java.util.stream.Collectors) Lineal(org.locationtech.jts.geom.Lineal) PathObjectTools(qupath.lib.objects.PathObjectTools) PathObject(qupath.lib.objects.PathObject) List(java.util.List) Geometry(org.locationtech.jts.geom.Geometry) PrecisionModel(org.locationtech.jts.geom.PrecisionModel) Envelope(org.locationtech.jts.geom.Envelope) ItemDistance(org.locationtech.jts.index.strtree.ItemDistance) STRtree(org.locationtech.jts.index.strtree.STRtree) PathClass(qupath.lib.objects.classes.PathClass)

Example 13 with PathClass

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();
}
Also used : PathClass(qupath.lib.objects.classes.PathClass) ImageOp(qupath.opencv.ops.ImageOp) PixelClassifier(qupath.lib.classifiers.pixel.PixelClassifier) ArrayList(java.util.ArrayList) LinkedHashMap(java.util.LinkedHashMap)

Example 14 with PathClass

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);
}
Also used : Mat(org.bytedeco.opencv.opencv_core.Mat) ArrayList(java.util.ArrayList) LinkedHashMap(java.util.LinkedHashMap) PathClass(qupath.lib.objects.classes.PathClass) PathObject(qupath.lib.objects.PathObject) TreeSet(java.util.TreeSet) MatVector(org.bytedeco.opencv.opencv_core.MatVector)

Example 15 with PathClass

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;
}
Also used : PathDetectionObject(qupath.lib.objects.PathDetectionObject) MeasurementList(qupath.lib.measurements.MeasurementList) ROI(qupath.lib.roi.interfaces.ROI) PathClass(qupath.lib.objects.classes.PathClass) PathObject(qupath.lib.objects.PathObject)

Aggregations

PathClass (qupath.lib.objects.classes.PathClass)66 ArrayList (java.util.ArrayList)42 PathObject (qupath.lib.objects.PathObject)34 List (java.util.List)29 Map (java.util.Map)25 IOException (java.io.IOException)21 Logger (org.slf4j.Logger)20 LoggerFactory (org.slf4j.LoggerFactory)20 Collections (java.util.Collections)17 Collectors (java.util.stream.Collectors)17 BufferedImage (java.awt.image.BufferedImage)16 LinkedHashMap (java.util.LinkedHashMap)16 ROI (qupath.lib.roi.interfaces.ROI)16 HashMap (java.util.HashMap)15 ImageData (qupath.lib.images.ImageData)15 PathClassFactory (qupath.lib.objects.classes.PathClassFactory)15 PathObjectHierarchy (qupath.lib.objects.hierarchy.PathObjectHierarchy)15 ParameterList (qupath.lib.plugins.parameters.ParameterList)15 Collection (java.util.Collection)14 TreeMap (java.util.TreeMap)11