use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class QuPathGUI method loadPathClasses.
/**
* Load PathClasses from preferences.
* Note that this also sets the color of any PathClass that is loads,
* and is really only intended for use when initializing.
*
* @return
*/
private static List<PathClass> loadPathClasses() {
byte[] bytes = PathPrefs.getUserPreferences().getByteArray("defaultPathClasses", null);
if (bytes == null || bytes.length == 0)
return Collections.emptyList();
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
try (ObjectInputStream in = new ObjectInputStream(stream)) {
List<PathClass> pathClassesOriginal = (List<PathClass>) in.readObject();
List<PathClass> pathClasses = new ArrayList<>();
for (PathClass pathClass : pathClassesOriginal) {
PathClass singleton = PathClassFactory.getSingletonPathClass(pathClass);
// Ensure the color is set
if (singleton != null && pathClass.getColor() != null)
singleton.setColor(pathClass.getColor());
pathClasses.add(singleton);
}
return pathClasses;
} catch (Exception e) {
logger.error("Error loading classes", e);
return Collections.emptyList();
}
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class PathObjectTools method mergePointsForAllClasses.
/**
* Merge point annotations sharing the same {@link PathClass} and {@link ImagePlane},
* creating multi-point annotations for all matching points and removing the (previously-separated) annotations.
*
* @param hierarchy object hierarchy to modify
* @return true if changes are made to the hierarchy, false otherwise
*/
public static boolean mergePointsForAllClasses(PathObjectHierarchy hierarchy) {
if (hierarchy == null)
return false;
var pathClasses = hierarchy.getAnnotationObjects().stream().filter(p -> p.getROI().isPoint()).map(p -> p.getPathClass()).collect(Collectors.toSet());
boolean changes = false;
for (PathClass pathClass : pathClasses) changes = changes || mergePointsForClass(hierarchy, pathClass);
return changes;
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class PathObjectTools method convertToPoints.
/**
* Convert a collection of PathObjects to Point annotations, based on ROI centroids.
* Each output annotation contains all points corresponding to input objects with the same classification.
* Consequently, the size of the output collection is equal to the number of distinct classifications
* found among the input objects.
*
* @param pathObjects input objects; these are expected to have ROIs
* @param preferNucleus if true, request the nucleus ROI from cell objects where possible; if false, request the outer ROI.
* This has no effect if the object is not a cell, or does not have two ROIs.
* @return a collection of annotations with point ROIs
*
* @see #convertToPoints(PathObjectHierarchy, Collection, boolean, boolean)
*/
public static Collection<PathObject> convertToPoints(Collection<PathObject> pathObjects, boolean preferNucleus) {
// Create Points lists for each class
Map<PathClass, Map<ImagePlane, List<Point2>>> pointsMap = new HashMap<>();
for (PathObject pathObject : pathObjects) {
var roi = PathObjectTools.getROI(pathObject, preferNucleus);
if (roi == null)
continue;
var plane = roi.getImagePlane();
PathClass pathClass = pathObject.getPathClass();
var pointsMapByClass = pointsMap.computeIfAbsent(pathClass, p -> new HashMap<>());
var points = pointsMapByClass.computeIfAbsent(plane, p -> new ArrayList<>());
points.add(new Point2(roi.getCentroidX(), roi.getCentroidY()));
}
// Create & add annotation objects to hierarchy
List<PathObject> annotations = new ArrayList<>();
for (Entry<PathClass, Map<ImagePlane, List<Point2>>> entry : pointsMap.entrySet()) {
var pathClass = entry.getKey();
for (var entry2 : entry.getValue().entrySet()) {
var plane = entry2.getKey();
var points = entry2.getValue();
PathObject pointObject = PathObjects.createAnnotationObject(ROIs.createPointsROI(points, plane), pathClass);
annotations.add(pointObject);
}
}
return annotations;
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class PathHierarchyPaintingHelper method paintObject.
/**
* Paint an object (or, more precisely, its ROI), optionally along with the ROIs of any child objects.
*
* This is subject to the OverlayOptions, and therefore may not actually end up painting anything
* (if the settings are such that objects of the class provided are not to be displayed)
*
* @param pathObject
* @param paintChildren
* @param g
* @param boundsDisplayed
* @param overlayOptions
* @param selectionModel
* @param downsample
* @return true if anything was painted, false otherwise
*/
public static boolean paintObject(PathObject pathObject, boolean paintChildren, Graphics2D g, Rectangle boundsDisplayed, OverlayOptions overlayOptions, PathObjectSelectionModel selectionModel, double downsample) {
if (pathObject == null)
return false;
// g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
// g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
// Always paint the selected object
// Note: this makes the assumption that child ROIs are completely contained within their parents;
// this probably should be the case, but isn't guaranteed
boolean isSelected = (selectionModel != null && selectionModel.isSelected(pathObject)) && (PathPrefs.useSelectedColorProperty().get() || !PathObjectTools.hasPointROI(pathObject));
boolean isDetectedObject = pathObject.isDetection() || (pathObject.isTile() && pathObject.hasMeasurements());
// Check if the PathClass isn't being shown
PathClass pathClass = pathObject.getPathClass();
if (!isSelected && overlayOptions != null && overlayOptions.isPathClassHidden(pathClass))
return false;
boolean painted = false;
// See if we need to check the children
ROI pathROI = pathObject.getROI();
if (pathROI != null) {
double roiBoundsX = pathROI.getBoundsX();
double roiBoundsY = pathROI.getBoundsY();
double roiBoundsWidth = pathROI.getBoundsWidth();
double roiBoundsHeight = pathROI.getBoundsHeight();
if (PathObjectTools.hasPointROI(pathObject) || boundsDisplayed == null || pathROI instanceof LineROI || boundsDisplayed.intersects(roiBoundsX, roiBoundsY, Math.max(roiBoundsWidth, 1), Math.max(roiBoundsHeight, 1))) {
// Paint the ROI, if necessary
if (isSelected || (overlayOptions.getShowDetections() && isDetectedObject) || (overlayOptions.getShowAnnotations() && pathObject.isAnnotation()) || (overlayOptions.getShowTMAGrid() && pathObject.isTMACore())) {
boolean doFill = overlayOptions.getFillDetections() || pathObject instanceof ParallelTileObject;
boolean doOutline = true;
Color color = null;
boolean useMapper = false;
double fillOpacity = .75;
if (isSelected && PathPrefs.useSelectedColorProperty().get() && PathPrefs.colorSelectedObjectProperty().getValue() != null)
color = ColorToolsAwt.getCachedColor(PathPrefs.colorSelectedObjectProperty().get());
else {
MeasurementMapper mapper = overlayOptions.getMeasurementMapper();
useMapper = mapper != null && mapper.isValid() && pathObject.isDetection();
if (useMapper) {
if (pathObject.hasMeasurements()) {
Integer rgb = mapper.getColorForObject(pathObject);
// If the mapper returns null, the object shouldn't be painted
if (rgb == null)
return false;
// , mapper.getColorMapper().hasAlpha());
color = ColorToolsAwt.getCachedColor(rgb);
} else
color = null;
// System.out.println(color + " - " + pathObject.getMeasurementList().getMeasurementValue(mapper.));
fillOpacity = 1.0;
// Outlines are not so helpful with the measurement mapper
if (doFill)
doOutline = doOutline && !pathObject.isTile();
} else {
Integer rgb = ColorToolsFX.getDisplayedColorARGB(pathObject);
color = ColorToolsAwt.getCachedColor(rgb);
}
// color = PathObjectHelpers.getDisplayedColor(pathObject);
}
// Check if we have only one or two pixels to draw - if so, we can be done quickly
if (isDetectedObject && downsample > 4 && roiBoundsWidth / downsample < 3 && roiBoundsHeight / downsample < 3) {
int x = (int) roiBoundsX;
int y = (int) roiBoundsY;
// Prefer rounding up, lest we lose a lot of regions unnecessarily
int w = (int) (roiBoundsWidth + .9);
int h = (int) (roiBoundsHeight + .9);
if (w > 0 && h > 0) {
g.setColor(color);
// g.setColor(DisplayHelpers.getMoreTranslucentColor(color));
// g.setStroke(getCachedStroke(overlayOptions.strokeThinThicknessProperty().get()));
g.fillRect(x, y, w, h);
}
painted = true;
} else {
Stroke stroke = null;
// Decide whether to fill or not
Color colorFill = doFill && (isDetectedObject || PathObjectTools.hasPointROI(pathObject)) ? color : null;
if (colorFill != null && fillOpacity != 1) {
if (pathObject instanceof ParallelTileObject)
colorFill = ColorToolsAwt.getMoreTranslucentColor(colorFill);
else if (pathObject instanceof PathCellObject && overlayOptions.getShowCellBoundaries() && overlayOptions.getShowCellNuclei()) {
// if (isSelected)
// colorFill = ColorToolsAwt.getTranslucentColor(colorFill);
// else
colorFill = ColorToolsAwt.getMoreTranslucentColor(colorFill);
} else if (pathObject.getParent() instanceof PathDetectionObject) {
colorFill = ColorToolsAwt.getTranslucentColor(colorFill);
} else if (pathObject instanceof PathTileObject && pathClass == null && color != null && color.getRGB() == PathPrefs.colorTileProperty().get()) {
// Don't fill in empty, unclassified tiles
// DisplayHelpers.getMoreTranslucentColor(colorFill);
colorFill = null;
}
}
// Color colorStroke = doOutline ? (colorFill == null ? color : (downsample > overlayOptions.strokeThinThicknessProperty().get() ? null : DisplayHelpers.darkenColor(color))) : null;
Color colorStroke = doOutline ? (colorFill == null ? color : ColorToolsAwt.darkenColor(color)) : null;
// For thick lines, antialiasing is very noticeable... less so for thin lines (of which there may be a huge number)
if (isDetectedObject) {
// Detections inside detections get half the line width
if (pathObject.getParent() instanceof PathDetectionObject)
stroke = getCachedStroke(PathPrefs.detectionStrokeThicknessProperty().get() / 2.0);
else
stroke = getCachedStroke(PathPrefs.detectionStrokeThicknessProperty().get());
} else {
double thicknessScale = downsample * (isSelected && !PathPrefs.useSelectedColorProperty().get() ? 1.6 : 1);
float thickness = (float) (PathPrefs.annotationStrokeThicknessProperty().get() * thicknessScale);
if (isSelected && pathObject.getParent() == null && PathPrefs.selectionModeProperty().get()) {
stroke = getCachedStrokeDashed(thickness);
} else {
stroke = getCachedStroke(thickness);
}
}
g.setStroke(stroke);
boolean paintSymbols = overlayOptions.getDetectionDisplayMode() == DetectionDisplayMode.CENTROIDS && pathObject.isDetection() && !pathObject.isTile();
if (paintSymbols) {
pathROI = PathObjectTools.getROI(pathObject, true);
double x = pathROI.getCentroidX();
double y = pathROI.getCentroidY();
double radius = PathPrefs.detectionStrokeThicknessProperty().get() * 2.0;
if (pathObject.getParent() instanceof PathDetectionObject)
radius /= 2.0;
Shape shape;
int nSubclasses = 0;
if (pathClass != null) {
nSubclasses = PathClassTools.splitNames(pathClass).size();
}
switch(nSubclasses) {
case 0:
var ellipse = localEllipse2D.get();
ellipse.setFrame(x - radius, y - radius, radius * 2, radius * 2);
shape = ellipse;
break;
case 1:
var rect = localRect2D.get();
rect.setFrame(x - radius, y - radius, radius * 2, radius * 2);
shape = rect;
break;
case 2:
var triangle = localPath2D.get();
double sqrt3 = Math.sqrt(3.0);
triangle.reset();
triangle.moveTo(x, y - radius * 2.0 / sqrt3);
triangle.lineTo(x - radius, y + radius / sqrt3);
triangle.lineTo(x + radius, y + radius / sqrt3);
triangle.closePath();
shape = triangle;
break;
case 3:
var plus = localPath2D.get();
plus.reset();
plus.moveTo(x, y - radius);
plus.lineTo(x, y + radius);
plus.moveTo(x - radius, y);
plus.lineTo(x + radius, y);
shape = plus;
break;
default:
var cross = localPath2D.get();
cross.reset();
radius /= Math.sqrt(2);
cross.moveTo(x - radius, y - radius);
cross.lineTo(x + radius, y + radius);
cross.moveTo(x + radius, y - radius);
cross.lineTo(x - radius, y + radius);
shape = cross;
break;
}
paintShape(shape, g, colorStroke, stroke, colorFill);
} else if (pathObject instanceof PathCellObject) {
PathCellObject cell = (PathCellObject) pathObject;
if (overlayOptions.getShowCellBoundaries())
paintROI(pathROI, g, colorStroke, stroke, colorFill, downsample);
if (overlayOptions.getShowCellNuclei())
paintROI(cell.getNucleusROI(), g, colorStroke, stroke, colorFill, downsample);
painted = true;
} else {
if ((overlayOptions.getFillAnnotations() && pathObject.isAnnotation() && pathObject.getPathClass() != PathClassFactory.getPathClass(StandardPathClasses.REGION) && (pathObject.getPathClass() != null || !pathObject.hasChildren())) || (pathObject.isTMACore() && overlayOptions.getShowTMACoreLabels()))
paintROI(pathROI, g, colorStroke, stroke, ColorToolsAwt.getMoreTranslucentColor(colorStroke), downsample);
else
paintROI(pathROI, g, colorStroke, stroke, colorFill, downsample);
painted = true;
}
}
}
}
}
// Paint the children, if necessary
if (paintChildren) {
for (PathObject childObject : pathObject.getChildObjectsAsArray()) {
// Only call the painting method if required
ROI childROI = childObject.getROI();
if ((childROI != null && boundsDisplayed.intersects(childROI.getBoundsX(), childROI.getBoundsY(), childROI.getBoundsWidth(), childROI.getBoundsHeight())) || childObject.hasChildren())
painted = paintObject(childObject, paintChildren, g, boundsDisplayed, overlayOptions, selectionModel, downsample) | painted;
}
}
return painted;
}
use of qupath.lib.objects.classes.PathClass in project qupath by qupath.
the class ObservableMeasurementTableDataTest method test.
@SuppressWarnings("javadoc")
@Test
public void test() {
// See https://github.com/locationtech/jts/issues/571
for (int counter = 0; counter < 50; counter++) {
ImageData<BufferedImage> imageData = new ImageData<>(null);
PathClass tumorClass = PathClassFactory.getPathClass(StandardPathClasses.TUMOR);
PathClass stromaClass = PathClassFactory.getPathClass(StandardPathClasses.STROMA);
// PathClass otherClass = PathClassFactory.getDefaultPathClass(PathClasses.OTHER);
PathClass artefactClass = PathClassFactory.getPathClass("Artefact");
PathObjectHierarchy hierarchy = imageData.getHierarchy();
// Add a parent annotation
PathObject parent = PathObjects.createAnnotationObject(ROIs.createRectangleROI(500, 500, 1000, 1000, ImagePlane.getDefaultPlane()));
// Create 100 tumor detections
// ROI emptyROI = ROIs.createEmptyROI();
ROI smallROI = ROIs.createRectangleROI(500, 500, 1, 1, ImagePlane.getDefaultPlane());
for (int i = 0; i < 100; i++) {
if (i < 25)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getNegative(tumorClass)));
else if (i < 50)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getOnePlus(tumorClass)));
else if (i < 75)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getTwoPlus(tumorClass)));
else if (i < 100)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getThreePlus(tumorClass)));
}
// Create 100 stroma detections
for (int i = 0; i < 100; i++) {
if (i < 50)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getNegative(stromaClass)));
else if (i < 60)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getOnePlus(stromaClass)));
else if (i < 70)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getTwoPlus(stromaClass)));
else if (i < 100)
parent.addPathObject(PathObjects.createDetectionObject(smallROI, PathClassFactory.getThreePlus(stromaClass)));
}
// Create 50 artefact detections
for (int i = 0; i < 50; i++) {
parent.addPathObject(PathObjects.createDetectionObject(smallROI, artefactClass));
}
hierarchy.addPathObject(parent);
ObservableMeasurementTableData model = new ObservableMeasurementTableData();
model.setImageData(imageData, Collections.singletonList(parent));
// Check tumor counts
assertEquals(100, model.getNumericValue(parent, "Num Tumor (base)"), EPSILON);
assertEquals(25, model.getNumericValue(parent, "Num Tumor: Negative"), EPSILON);
assertEquals(25, model.getNumericValue(parent, "Num Tumor: 1+"), EPSILON);
assertEquals(25, model.getNumericValue(parent, "Num Tumor: 2+"), EPSILON);
assertEquals(25, model.getNumericValue(parent, "Num Tumor: 3+"), EPSILON);
assertTrue(Double.isNaN(model.getNumericValue(parent, "Num Tumor: 4+")));
// Check tumor H-score, Allred score & positive %
assertEquals(150, model.getNumericValue(parent, "Tumor: H-score"), EPSILON);
assertEquals(75, model.getNumericValue(parent, "Tumor: Positive %"), EPSILON);
assertEquals(2, model.getNumericValue(parent, "Tumor: Allred intensity"), EPSILON);
assertEquals(5, model.getNumericValue(parent, "Tumor: Allred proportion"), EPSILON);
assertEquals(7, model.getNumericValue(parent, "Tumor: Allred score"), EPSILON);
// Check tumor H-score unaffected when tumor detections added without intensity classification
for (int i = 0; i < 10; i++) parent.addPathObject(PathObjects.createDetectionObject(smallROI, tumorClass));
hierarchy.fireHierarchyChangedEvent(this);
model.refreshEntries();
// model.setImageData(imageData, Collections.singletonList(parent));
assertEquals(100, model.getNumericValue(parent, "Num Stroma (base)"), EPSILON);
assertEquals(50, model.getNumericValue(parent, "Num Stroma: Negative"), EPSILON);
assertEquals(150, model.getNumericValue(parent, "Tumor: H-score"), EPSILON);
assertEquals(75, model.getNumericValue(parent, "Tumor: Positive %"), EPSILON);
// Check stroma scores
assertEquals(100, model.getNumericValue(parent, "Num Stroma (base)"), EPSILON);
assertEquals(120, model.getNumericValue(parent, "Stroma: H-score"), EPSILON);
// Check complete scores
assertEquals(135, model.getNumericValue(parent, "Stroma + Tumor: H-score"), EPSILON);
// Add a new parent that completely contains the current object, and confirm complete scores agree
PathObject parentNew = PathObjects.createAnnotationObject(ROIs.createRectangleROI(0, 0, 2000, 2000, ImagePlane.getDefaultPlane()));
hierarchy.addPathObject(parentNew);
model.refreshEntries();
assertEquals(135, model.getNumericValue(parent, "Stroma + Tumor: H-score"), EPSILON);
assertEquals(135, model.getNumericValue(parentNew, "Stroma + Tumor: H-score"), EPSILON);
// Create a new object and demonstrate Allred dependence on a single cell
PathObject parentAllred = PathObjects.createAnnotationObject(ROIs.createRectangleROI(4000, 4000, 1000, 1000, ImagePlane.getDefaultPlane()));
ROI newROI = ROIs.createEllipseROI(4500, 4500, 10, 10, ImagePlane.getDefaultPlane());
for (int i = 0; i < 100; i++) parentAllred.addPathObject(PathObjects.createDetectionObject(newROI, PathClassFactory.getNegative(tumorClass)));
hierarchy.addPathObject(parentAllred);
model.refreshEntries();
assertEquals(0, model.getNumericValue(parentAllred, "Tumor: Allred score"), EPSILON);
parentAllred.addPathObject(PathObjects.createDetectionObject(newROI, PathClassFactory.getThreePlus(tumorClass)));
hierarchy.fireHierarchyChangedEvent(parentAllred);
model.refreshEntries();
assertEquals(4, model.getNumericValue(parentAllred, "Tumor: Allred score"), EPSILON);
}
}
Aggregations