use of qupath.lib.objects.PathObject in project qupath by qupath.
the class EstimateStainVectorsCommand method promptToEstimateStainVectors.
public static void promptToEstimateStainVectors(ImageData<BufferedImage> imageData) {
if (imageData == null) {
Dialogs.showNoImageError(TITLE);
return;
}
if (imageData == null || !imageData.isBrightfield() || imageData.getServer() == null || !imageData.getServer().isRGB()) {
Dialogs.showErrorMessage(TITLE, "No brightfield, RGB image selected!");
return;
}
ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
if (stains == null || !stains.getStain(3).isResidual()) {
Dialogs.showErrorMessage(TITLE, "Sorry, stain editing is only possible for brightfield, RGB images with 2 stains");
return;
}
PathObject pathObject = imageData.getHierarchy().getSelectionModel().getSelectedObject();
ROI roi = pathObject == null ? null : pathObject.getROI();
if (roi == null)
roi = ROIs.createRectangleROI(0, 0, imageData.getServer().getWidth(), imageData.getServer().getHeight(), ImagePlane.getDefaultPlane());
double downsample = Math.max(1, Math.sqrt((roi.getBoundsWidth() * roi.getBoundsHeight()) / MAX_PIXELS));
RegionRequest request = RegionRequest.createInstance(imageData.getServerPath(), downsample, roi);
BufferedImage img = null;
try {
img = imageData.getServer().readBufferedImage(request);
} catch (IOException e) {
Dialogs.showErrorMessage("Estimate stain vectors", e);
logger.error("Unable to obtain pixels for " + request.toString(), e);
}
// Apply small amount of smoothing to reduce compression artefacts
img = EstimateStainVectors.smoothImage(img);
// Check modes for background
int[] rgb = img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth());
int[] rgbMode = EstimateStainVectors.getModeRGB(rgb);
int rMax = rgbMode[0];
int gMax = rgbMode[1];
int bMax = rgbMode[2];
// Check if the background values may need to be changed
if (rMax != stains.getMaxRed() || gMax != stains.getMaxGreen() || bMax != stains.getMaxBlue()) {
DialogButton response = Dialogs.showYesNoCancelDialog(TITLE, String.format("Modal RGB values %d, %d, %d do not match current background values - do you want to use the modal values?", rMax, gMax, bMax));
if (response == DialogButton.CANCEL)
return;
else if (response == DialogButton.YES) {
stains = stains.changeMaxValues(rMax, gMax, bMax);
imageData.setColorDeconvolutionStains(stains);
}
}
ColorDeconvolutionStains stainsUpdated = null;
logger.info("Requesting region for stain vector editing: ", request);
try {
stainsUpdated = showStainEditor(img, stains);
} catch (Exception e) {
Dialogs.showErrorMessage(TITLE, "Error with stain estimation: " + e.getLocalizedMessage());
logger.error("{}", e.getLocalizedMessage(), e);
// JOptionPane.showMessageDialog(qupath.getFrame(), "Error with stain estimation: " + e.getLocalizedMessage(), "Estimate stain vectors", JOptionPane.ERROR_MESSAGE, null);
return;
}
if (!stains.equals(stainsUpdated)) {
String suggestedName;
String collectiveNameBefore = stainsUpdated.getName();
if (collectiveNameBefore.endsWith("default"))
suggestedName = collectiveNameBefore.substring(0, collectiveNameBefore.lastIndexOf("default")) + "estimated";
else
suggestedName = collectiveNameBefore;
String newName = Dialogs.showInputDialog(TITLE, "Set name for stain vectors", suggestedName);
if (newName == null)
return;
if (!newName.isBlank())
stainsUpdated = stainsUpdated.changeName(newName);
imageData.setColorDeconvolutionStains(stainsUpdated);
}
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class ParallelTileObject method resolveOverlaps.
/**
* Request that the tile object attempts to resolve overlaps with its neighboring tiles.
*/
public synchronized void resolveOverlaps() {
// // If we don't have any children, notify that the test is complete
// if (!hasChildren()) {
// for (ParallelTileObject pto : map.keySet())
// pto.notifyTestComplete(this);
// map.clear();
// checkAllTestsComplete();
// return;
// }
long startTime = System.currentTimeMillis();
int nRemoved = 0;
boolean preferNucleus = false;
// If we do have children, loop through & perform tests
Iterator<Entry<ParallelTileObject, Rectangle2D>> iterMap = map.entrySet().iterator();
while (iterMap.hasNext()) {
Entry<ParallelTileObject, Rectangle2D> entry = iterMap.next();
// If the parallel tile object hasn't been processed yet, then just continue - nothing to compare
ParallelTileObject pto = entry.getKey();
if (!pto.isComplete())
continue;
ParallelTileObject first, second;
// Choose a consistent order for the comparison
if (this.getROI().getBoundsX() > pto.getROI().getBoundsX() || this.getROI().getBoundsY() > pto.getROI().getBoundsY()) {
first = this;
second = pto;
} else {
first = pto;
second = this;
}
// ROI firstBounds = first.getROI();
// ROI secondBounds = second.getROI();
// Compare this object's lists with that object's list
List<PathObject> listFirst = first.getObjectsForRegion(entry.getValue());
List<PathObject> listSecond = second.getObjectsForRegion(entry.getValue());
// Only need to compare potential overlaps if both lists are non-empty
if (!listFirst.isEmpty() && !listSecond.isEmpty()) {
Map<ROI, Geometry> cache = new HashMap<>();
Iterator<PathObject> iterFirst = listFirst.iterator();
while (iterFirst.hasNext()) {
PathObject firstObject = iterFirst.next();
ROI firstROI = PathObjectTools.getROI(firstObject, preferNucleus);
ImageRegion firstRegion = ImageRegion.createInstance(firstROI);
Geometry firstGeometry = null;
double firstArea = Double.NaN;
Iterator<PathObject> iterSecond = listSecond.iterator();
while (iterSecond.hasNext()) {
PathObject secondObject = iterSecond.next();
ROI secondROI = PathObjectTools.getROI(secondObject, preferNucleus);
// Do quick overlap test
if (!firstRegion.intersects(secondROI.getBoundsX(), secondROI.getBoundsY(), secondROI.getBoundsWidth(), secondROI.getBoundsHeight()))
continue;
// Get geometries
if (firstGeometry == null) {
firstGeometry = firstROI.getGeometry();
firstArea = firstGeometry.getArea();
}
Geometry secondGeometry = cache.get(secondROI);
if (secondGeometry == null) {
secondGeometry = secondROI.getGeometry();
cache.put(secondROI, secondGeometry);
}
Geometry intersection;
try {
// Get the intersection
if (!firstGeometry.intersects(secondGeometry))
continue;
intersection = firstGeometry.intersection(secondGeometry);
} catch (Exception e) {
logger.warn("Error resolving overlaps: {}", e.getLocalizedMessage());
logger.debug(e.getLocalizedMessage(), e);
continue;
}
if (intersection.isEmpty())
continue;
// Check areas
double intersectionArea = intersection.getArea();
double secondArea = secondGeometry.getArea();
double threshold = 0.1;
if (firstArea >= secondArea) {
if (intersectionArea / secondArea > threshold) {
second.removePathObject(secondObject);
nRemoved++;
}
} else {
if (intersectionArea / firstArea > threshold) {
first.removePathObject(firstObject);
nRemoved++;
break;
}
}
}
}
}
// Remove the neighbor from the map
iterMap.remove();
pto.notifyTestComplete(this);
}
checkAllTestsComplete();
long endTime = System.currentTimeMillis();
logger.debug(String.format("Resolved %d overlaps: %.2f seconds", nRemoved, (endTime - startTime) / 1000.));
// logger.info(String.format("Resolved %d possible overlaps with %d iterations (tested %d of %d): %.2f seconds", nOverlaps, counter, detectedCounter-skipCounter, detectedCounter, (endTime2 - startTime2) / 1000.));
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class PathObjectTileCache method addToCache.
/**
* Add a PathObject to the cache, optionally including children.
*
* @param pathObject
* @param includeChildren
*/
private void addToCache(PathObject pathObject, boolean includeChildren, Class<? extends PathObject> limitToClass) {
// If the cache isn't active, we can ignore this... it will be constructed when it is needed
if (!isActive())
return;
if (pathObject.hasROI()) {
Class<? extends PathObject> cls = pathObject.getClass();
if (limitToClass == null || cls == limitToClass) {
SpatialIndex mapObjects = map.get(cls);
if (mapObjects == null) {
mapObjects = createSpatialIndex();
map.put(cls, mapObjects);
}
Envelope envelope = getEnvelope(pathObject);
mapObjects.insert(envelope, pathObject);
}
}
// Add the children
if (includeChildren && !(pathObject instanceof TemporaryObject) && pathObject.hasChildren()) {
for (PathObject child : pathObject.getChildObjectsAsArray()) addToCache(child, includeChildren, limitToClass);
}
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class PathObjectTileCache method getObjectsForRegion.
// /**
// * Add a PathObject to the cache. Child objects are not added.
// * @param pathObject
// */
// private void addToCache(PathObject pathObject) {
// addToCache(pathObject, false);
// }
/**
* Get all the PathObjects stored in this cache of a specified type and having ROIs with bounds overlapping a specified region.
* This does not guarantee that the ROI (which may not be rectangular) overlaps the region...
* but a quick test is preferred over a more expensive one.
* <p>
* Note that pathObjects will be added to the collection provided, if there is one.
* The same object will be added to this collection multiple times if it overlaps different tiles -
* again in the interests of speed, no check is made.
* However this can be addressed by using a Set as the collection.
* <p>
* If a collection is not provided, another Collection is created & used instead.
*
* @param cls a PathObject class, or null if all object types should be returned
* @param region an image region, or null if all objects with ROIs should be return
* @param pathObjects an (optional) existing collection to which PathObjects should be added
* @param includeSubclasses true if subclasses of the specified class should be included
* @return
*/
public Collection<PathObject> getObjectsForRegion(Class<? extends PathObject> cls, ImageRegion region, Collection<PathObject> pathObjects, boolean includeSubclasses) {
ensureCacheConstructed();
var envelope = region == null ? MAX_ENVELOPE : getEnvelope(region);
int z = region == null ? -1 : region.getZ();
int t = region == null ? -1 : region.getT();
r.lock();
try {
// Iterate through all the classes, getting objects of the specified class or subclasses thereof
for (Entry<Class<? extends PathObject>, SpatialIndex> entry : map.entrySet()) {
if (cls == null || (includeSubclasses && cls.isAssignableFrom(entry.getKey())) || cls.isInstance(entry.getKey())) {
if (entry.getValue() != null) {
var list = entry.getValue().query(envelope);
if (list.isEmpty())
continue;
if (pathObjects == null)
pathObjects = new HashSet<PathObject>();
// Add all objects that have a parent, i.e. might be in the hierarchy
for (PathObject pathObject : (List<PathObject>) list) {
var roi = pathObject.getROI();
if (roi == null || region == null || (roi.getZ() == z && roi.getT() == t)) {
if (pathObject.getParent() != null || pathObject.isRootObject()) {
if (envelope.intersects(getEnvelope(pathObject)))
pathObjects.add(pathObject);
}
}
}
}
// pathObjects = entry.getValue().getObjectsForRegion(region, pathObjects);
}
}
// logger.info("Objects for " + region + ": " + (pathObjects == null ? 0 : pathObjects.size()));
if (pathObjects == null)
return Collections.emptySet();
return pathObjects;
} finally {
r.unlock();
}
}
use of qupath.lib.objects.PathObject in project qupath by qupath.
the class PathObjectTileCache method hierarchyChanged.
// public synchronized Collection<PathObject> getObjectsForRegion(Class<? extends PathObject> cls, Rectangle region, Collection<PathObject> pathObjects) {
// ensureCacheConstructed();
//
// if (pathObjects == null)
// pathObjects = new HashSet<>();
//
// // Iterate through all the classes, getting objects of the specified class or subclasses thereof
// if (cls == null) {
// for (Class<? extends PathObject> tempClass : map.keySet())
// getObjectsForRegion(tempClass, region, pathObjects);
// return pathObjects;
// }
//
// // Extract the map for the type
// PathObjectTileMap mapObjects = map.get(cls);
// if (mapObjects == null)
// return pathObjects;
//
// // Get the objects
// return mapObjects.getObjectsForRegion(region, pathObjects);
// }
// @Override
// public void pathObjectChanged(PathObjectHierarchy pathObjectHierarchy, PathObject pathObject) {
// // Remove, then re-add the object - ignoring children (which shouldn't be changed, as no structural change is associated with this event)
// removeFromCache(pathObject, false);
// addToCache(pathObject, false);
// }
@Override
public void hierarchyChanged(final PathObjectHierarchyEvent event) {
w.lock();
try {
boolean singleChange = event.getChangedObjects().size() == 1;
PathObject singleObject = singleChange ? event.getChangedObjects().get(0) : null;
if (singleChange && event.getEventType() == HierarchyEventType.ADDED) {
removeFromCache(singleObject, false);
addToCache(singleObject, false, singleObject.getClass());
} else if (singleChange && event.getEventType() == HierarchyEventType.REMOVED) {
removeFromCache(singleObject, false);
} else if (event.getEventType() == HierarchyEventType.OTHER_STRUCTURE_CHANGE || event.getEventType() == HierarchyEventType.CHANGE_OTHER) {
// System.err.println(event);
if (!event.isChanging())
resetCache();
}
} finally {
w.unlock();
}
// else if (event.getEventType() == HierarchyEventType.OBJECT_CHANGE)
// resetCache(); // TODO: Check if full change is necessary for object change events
}
Aggregations