use of qupath.lib.images.ImageData in project qupath by qupath.
the class PathIO method readImageDataSerialized.
@SuppressWarnings("unchecked")
private static <T> ImageData<T> readImageDataSerialized(final InputStream stream, ImageData<T> imageData, ImageServer<T> server, Class<T> cls) throws IOException {
long startTime = System.currentTimeMillis();
Locale locale = Locale.getDefault(Category.FORMAT);
boolean localeChanged = false;
try (ObjectInputStream inStream = new ObjectInputStream(new BufferedInputStream(stream))) {
ServerBuilder<T> serverBuilder = null;
PathObjectHierarchy hierarchy = null;
ImageData.ImageType imageType = null;
ColorDeconvolutionStains stains = null;
Workflow workflow = null;
Map<String, Object> propertyMap = null;
String firstLine = inStream.readUTF();
// int versionNumber = -1;
if (!firstLine.startsWith("Data file version")) {
logger.error("Input stream does not contain valid QuPath data!");
}
// else {
// // Could try to parse version number... although frankly, at this time, we don't really care...
// try {
// versionNumber = NumberFormat.getInstance(Locale.US).parse(firstLine.substring("Data file version".length()).trim()).intValue();
// } catch (Exception e) {
// logger.warn("Unable to parse version number from {}", firstLine);
// }
// }
String serverString = (String) inStream.readObject();
// Don't log warnings if we are provided with a server
serverBuilder = extractServerBuilder(serverString, server == null);
while (true) {
// logger.debug("Starting read: " + inStream.available());
try {
// Try to read a relevant object from the stream
Object input = inStream.readObject();
logger.debug("Read: {}", input);
// If we have a Locale, then set it
if (input instanceof Locale) {
if (input != locale) {
Locale.setDefault(Category.FORMAT, (Locale) input);
localeChanged = true;
}
} else if (input instanceof PathObjectHierarchy)
hierarchy = (PathObjectHierarchy) input;
else if (input instanceof ImageData.ImageType)
imageType = (ImageData.ImageType) input;
else if (input instanceof String && "EOF".equals(input)) {
// else if ("EOF".equals(input)) {
break;
// }
} else if (input instanceof ColorDeconvolutionStains)
stains = (ColorDeconvolutionStains) input;
else if (input instanceof Workflow)
workflow = (Workflow) input;
else if (input instanceof Map)
propertyMap = (Map<String, Object>) input;
else if (input == null) {
logger.debug("Null object will be skipped");
} else
logger.warn("Unsupported object of class {} will be skipped: {}", input.getClass().getName(), input);
} catch (ClassNotFoundException e) {
logger.error("Unable to find class: " + e.getLocalizedMessage(), e);
} catch (EOFException e) {
// Try to recover from EOFExceptions - we may already have enough info
logger.error("Reached end of file...");
if (hierarchy == null)
logger.error(e.getLocalizedMessage(), e);
break;
}
}
// Create an entirely new ImageData if necessary
var existingBuilder = imageData == null || imageData.getServer() == null ? null : imageData.getServer().getBuilder();
if (imageData == null || !Objects.equals(serverBuilder, existingBuilder)) {
// Create a new server if we need to
if (server == null) {
try {
server = serverBuilder.build();
} catch (Exception e) {
logger.error(e.getLocalizedMessage());
}
;
if (server == null) {
logger.error("Warning: Unable to build server with " + serverBuilder);
// throw new RuntimeException("Warning: Unable to create server for path " + serverPath);
}
}
// TODO: Make this less clumsy... but for now we need to ensure we have a fully-initialized hierarchy (which deserialization alone doesn't achieve)
PathObjectHierarchy hierarchy2 = new PathObjectHierarchy();
hierarchy2.setHierarchy(hierarchy);
hierarchy = hierarchy2;
imageData = new ImageData<>(server, hierarchy, imageType);
} else {
if (imageType != null)
imageData.setImageType(imageType);
// Set the new hierarchy
if (hierarchy != null)
imageData.getHierarchy().setHierarchy(hierarchy);
}
// Set the other properties we have just read
if (workflow != null) {
imageData.getHistoryWorkflow().clear();
imageData.getHistoryWorkflow().addSteps(workflow.getSteps());
}
if (stains != null) {
imageData.setColorDeconvolutionStains(stains);
}
if (propertyMap != null) {
for (Entry<String, Object> entry : propertyMap.entrySet()) imageData.setProperty(entry.getKey(), entry.getValue());
}
long endTime = System.currentTimeMillis();
// if (hierarchy == null) {
// logger.error(String.format("%s does not contain a valid QUPath object hierarchy!", file.getAbsolutePath()));
// return null;
// }
logger.debug(String.format("Hierarchy with %d object(s) read in %.2f seconds", hierarchy.nObjects(), (endTime - startTime) / 1000.));
} catch (ClassNotFoundException e1) {
logger.warn("Class not found reading image data", e1);
} finally {
if (localeChanged)
Locale.setDefault(Category.FORMAT, locale);
}
return imageData;
}
use of qupath.lib.images.ImageData in project qupath by qupath.
the class GuiTools method promptToClearAllSelectedObjects.
/**
* Prompt user to select all currently-selected objects (except TMA core objects).
*
* @param imageData
* @return
*/
public static boolean promptToClearAllSelectedObjects(final ImageData<?> imageData) {
// Get all non-TMA core objects
PathObjectHierarchy hierarchy = imageData.getHierarchy();
Collection<PathObject> selectedRaw = hierarchy.getSelectionModel().getSelectedObjects();
List<PathObject> selected = selectedRaw.stream().filter(p -> !(p instanceof TMACoreObject)).collect(Collectors.toList());
if (selected.isEmpty()) {
if (selectedRaw.size() > selected.size())
Dialogs.showErrorMessage("Delete selected objects", "No valid objects selected! \n\nNote: Individual TMA cores cannot be deleted with this method.");
else
Dialogs.showErrorMessage("Delete selected objects", "No objects selected!");
return false;
}
int n = selected.size();
String message;
if (n == 1)
message = "Delete selected object?";
else
message = "Delete " + n + " selected objects?";
if (Dialogs.showYesNoDialog("Delete objects", message)) {
// Check for descendants
List<PathObject> children = new ArrayList<>();
for (PathObject temp : selected) {
children.addAll(temp.getChildObjects());
}
children.removeAll(selected);
boolean keepChildren = true;
if (!children.isEmpty()) {
Dialogs.DialogButton response = Dialogs.showYesNoCancelDialog("Delete objects", "Keep descendant objects?");
if (response == Dialogs.DialogButton.CANCEL)
return false;
keepChildren = response == Dialogs.DialogButton.YES;
}
hierarchy.removeObjects(selected, keepChildren);
hierarchy.getSelectionModel().clearSelection();
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Delete selected objects", "clearSelectedObjects(" + keepChildren + ");"));
if (keepChildren)
logger.info(selected.size() + " object(s) deleted");
else
logger.info(selected.size() + " object(s) deleted with descendants");
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Delete selected objects", "clearSelectedObjects();"));
logger.info(selected.size() + " object(s) deleted");
return true;
} else
return false;
}
use of qupath.lib.images.ImageData in project qupath by qupath.
the class QuPathViewer method setImageData.
/**
* Set the current image for this viewer.
* @param imageDataNew
*/
public void setImageData(ImageData<BufferedImage> imageDataNew) {
if (this.imageDataProperty.get() == imageDataNew)
return;
imageDataChanging.set(true);
// Remove listeners for previous hierarchy
ImageData<BufferedImage> imageDataOld = this.imageDataProperty.get();
if (imageDataOld != null) {
imageDataOld.getHierarchy().removePathObjectListener(this);
imageDataOld.getHierarchy().getSelectionModel().removePathObjectSelectionListener(this);
}
// Determine if the server has remained the same, so we can avoid shifting the viewer
boolean sameServer = false;
if (imageDataOld != null && imageDataNew != null && imageDataOld.getServerPath().equals(imageDataNew.getServerPath()))
sameServer = true;
this.imageDataProperty.set(imageDataNew);
ImageServer<BufferedImage> server = imageDataNew == null ? null : imageDataNew.getServer();
PathObjectHierarchy hierarchy = imageDataNew == null ? null : imageDataNew.getHierarchy();
long startTime = System.currentTimeMillis();
if (imageDisplay != null) {
boolean keepDisplay = PathPrefs.keepDisplaySettingsProperty().get();
// This is a bit of a hack to avoid calling internal methods for ImageDisplay
// See https://github.com/qupath/qupath/issues/601
boolean displaySet = false;
if (imageDataNew != null && keepDisplay) {
if (imageDisplay.getImageData() != null && serversCompatible(imageDataNew.getServer(), imageDisplay.getImageData().getServer())) {
imageDisplay.setImageData(imageDataNew, keepDisplay);
displaySet = true;
} else {
for (var viewer : QuPathGUI.getInstance().getViewers()) {
if (this == viewer || viewer.getImageData() == null)
continue;
var tempServer = viewer.getServer();
var currentServer = imageDataNew.getServer();
if (serversCompatible(tempServer, currentServer)) {
var json = viewer.getImageDisplay().toJSON(false);
imageDataNew.setProperty(ImageDisplay.class.getName(), json);
imageDisplay.setImageData(imageDataNew, false);
displaySet = true;
break;
}
}
}
}
if (!displaySet)
imageDisplay.setImageData(imageDataNew, keepDisplay);
// See https://github.com/qupath/qupath/issues/843
if (server != null && !server.isRGB()) {
var colors = imageDisplay.availableChannels().stream().filter(c -> c instanceof DirectServerChannelInfo).map(c -> c.getColor()).collect(Collectors.toList());
if (server.nChannels() == colors.size())
updateServerChannels(server, colors);
}
}
long endTime = System.currentTimeMillis();
logger.debug("Setting ImageData time: {} ms", endTime - startTime);
initializeForServer(server);
if (!sameServer) {
setDownsampleFactorImpl(getZoomToFitDownsampleFactor(), -1, -1);
centerImage();
}
fireImageDataChanged(imageDataOld, imageDataNew);
if (imageDataNew != null) {
// hierarchyPainter = new PathHierarchyPainter(hierarchy);
hierarchy.addPathObjectListener(this);
hierarchy.getSelectionModel().addPathObjectSelectionListener(this);
}
setSelectedObject(null);
// TODO: Consider shifting, fixing magnification, repainting etc.
if (isShowing())
repaint();
logger.info("Image data set to {}", imageDataNew);
}
use of qupath.lib.images.ImageData in project qupath by qupath.
the class MeasurementExporter method exportMeasurements.
/**
* Exports the measurements of one or more entries in the project.
* This function first opens all the images in the project to store
* all the column names and values of the measurements.
* Then, it loops through the maps containing the values to write
* them to the given output stream.
* @param stream
*/
public void exportMeasurements(OutputStream stream) {
long startTime = System.currentTimeMillis();
Map<ProjectImageEntry<?>, String[]> imageCols = new HashMap<>();
Map<ProjectImageEntry<?>, Integer> nImageEntries = new HashMap<>();
List<String> allColumns = new ArrayList<>();
Multimap<String, String> valueMap = LinkedListMultimap.create();
String pattern = "(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)";
for (ProjectImageEntry<?> entry : imageList) {
try {
ImageData<?> imageData = entry.readImageData();
ObservableMeasurementTableData model = new ObservableMeasurementTableData();
Collection<PathObject> pathObjects = imageData == null ? Collections.emptyList() : imageData.getHierarchy().getObjects(null, type);
if (filter != null)
pathObjects = pathObjects.stream().filter(filter).collect(Collectors.toList());
model.setImageData(imageData, pathObjects);
List<String> data = SummaryMeasurementTableCommand.getTableModelStrings(model, separator, excludeColumns);
// Get header
String[] header;
String headerString = data.get(0);
if (headerString.chars().filter(e -> e == '"').count() > 1)
header = headerString.split(separator.equals("\t") ? "\\" + separator : separator + pattern, -1);
else
header = headerString.split(separator);
imageCols.put(entry, header);
nImageEntries.put(entry, data.size() - 1);
for (String col : header) {
if (!allColumns.contains(col) && !excludeColumns.contains(col))
allColumns.add(col);
}
// To keep the same column order, just delete non-relevant columns
if (!includeOnlyColumns.isEmpty())
allColumns.removeIf(n -> !includeOnlyColumns.contains(n));
for (int i = 1; i < data.size(); i++) {
String[] row;
String rowString = data.get(i);
// Check if some values in the row are escaped
if (rowString.chars().filter(e -> e == '"').count() > 1)
row = rowString.split(separator.equals("\t") ? "\\" + separator : separator + pattern, -1);
else
row = rowString.split(separator);
// Put value in map
for (int elem = 0; elem < row.length; elem++) {
if (allColumns.contains(header[elem]))
valueMap.put(header[elem], row[elem]);
}
}
} catch (Exception e) {
logger.error(e.getLocalizedMessage(), e);
}
}
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(stream, StandardCharsets.UTF_8))) {
writer.write(String.join(separator, allColumns));
writer.write(System.lineSeparator());
Iterator[] its = new Iterator[allColumns.size()];
for (int col = 0; col < allColumns.size(); col++) {
its[col] = valueMap.get(allColumns.get(col)).iterator();
}
for (ProjectImageEntry<?> entry : imageList) {
for (int nObject = 0; nObject < nImageEntries.get(entry); nObject++) {
for (int nCol = 0; nCol < allColumns.size(); nCol++) {
if (Arrays.stream(imageCols.get(entry)).anyMatch(allColumns.get(nCol)::equals)) {
String val = (String) its[nCol].next();
// NaN values -> blank
if (val.equals("NaN"))
val = "";
writer.write(val);
}
if (nCol < allColumns.size() - 1)
writer.write(separator);
}
writer.write(System.lineSeparator());
}
}
} catch (Exception e) {
logger.error("Error writing to file: " + e.getLocalizedMessage(), e);
}
long endTime = System.currentTimeMillis();
long timeMillis = endTime - startTime;
String time = null;
if (timeMillis > 1000 * 60)
time = String.format("Total processing time: %.2f minutes", timeMillis / (1000.0 * 60.0));
else if (timeMillis > 1000)
time = String.format("Total processing time: %.2f seconds", timeMillis / (1000.0));
else
time = String.format("Total processing time: %d milliseconds", timeMillis);
logger.info("Processed {} images", imageList.size());
logger.info(time);
}
use of qupath.lib.images.ImageData 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