use of qupath.lib.objects.hierarchy.TMAGrid in project qupath by qupath.
the class TMACommands method promptToRelabelTMAGrid.
/**
* Prompt to relabel the core names within a TMA grid.
* @param imageData image containing the TMA grid
*/
public static void promptToRelabelTMAGrid(ImageData<?> imageData) {
String title = "Relabel TMA grid";
if (imageData == null) {
Dialogs.showNoImageError(title);
return;
}
if (imageData.getHierarchy().getTMAGrid() == null) {
Dialogs.showErrorMessage(title, "No TMA grid selected!");
return;
}
ParameterList params = new ParameterList();
params.addStringParameter("labelsHorizontal", "Column labels", columnLabelsProperty.get(), "Enter column labels.\nThis can be a continuous range of letters or numbers (e.g. 1-10 or A-J),\nor a discontinuous list separated by spaces (e.g. A B C E F G).");
params.addStringParameter("labelsVertical", "Row labels", rowLabelsProperty.get(), "Enter row labels.\nThis can be a continuous range of letters or numbers (e.g. 1-10 or A-J),\nor a discontinuous list separated by spaces (e.g. A B C E F G).");
params.addChoiceParameter("labelOrder", "Label order", rowFirstProperty.get() ? "Row first" : "Column first", Arrays.asList("Column first", "Row first"), "Create TMA labels either in the form Row-Column or Column-Row");
if (!Dialogs.showParameterDialog(title, params))
return;
// Parse the arguments
String labelsHorizontal = params.getStringParameterValue("labelsHorizontal");
String labelsVertical = params.getStringParameterValue("labelsVertical");
boolean rowFirst = "Row first".equals(params.getChoiceParameterValue("labelOrder"));
// Figure out if this will work
TMAGrid grid = imageData.getHierarchy().getTMAGrid();
String[] columnLabels = PathObjectTools.parseTMALabelString(labelsHorizontal);
String[] rowLabels = PathObjectTools.parseTMALabelString(labelsVertical);
if (columnLabels.length < grid.getGridWidth()) {
Dialogs.showErrorMessage(title, "Not enough column labels specified!");
return;
}
if (rowLabels.length < grid.getGridHeight()) {
Dialogs.showErrorMessage(title, "Not enough row labels specified!");
return;
}
// Apply the labels
QP.relabelTMAGrid(imageData.getHierarchy(), labelsHorizontal, labelsVertical, rowFirst);
// Add to workflow history
imageData.getHistoryWorkflow().addStep(new DefaultScriptableWorkflowStep("Relabel TMA grid", String.format("relabelTMAGrid(\"%s\", \"%s\", %s)", GeneralTools.escapeFilePath(labelsHorizontal), GeneralTools.escapeFilePath(labelsVertical), Boolean.toString(rowFirst))));
// Store values
rowLabelsProperty.set(labelsVertical);
columnLabelsProperty.set(labelsHorizontal);
rowFirstProperty.set(rowFirst);
}
use of qupath.lib.objects.hierarchy.TMAGrid in project qupath by qupath.
the class TMACommands method promptToDeleteTMAGridRowOrColumn.
private static boolean promptToDeleteTMAGridRowOrColumn(ImageData<?> imageData, TMARemoveType type) {
String typeString = type.toString();
String title = "Delete TMA " + typeString;
boolean removeRow = type == TMARemoveType.ROW;
if (imageData == null) {
Dialogs.showNoImageError(title);
return false;
}
if (imageData.getHierarchy().getTMAGrid() == null) {
Dialogs.showErrorMessage(title, "No image with dearrayed TMA cores selected!");
return false;
}
PathObjectHierarchy hierarchy = imageData.getHierarchy();
PathObject selected = hierarchy.getSelectionModel().getSelectedObject();
TMACoreObject selectedCore = null;
if (selected != null)
selectedCore = PathObjectTools.getAncestorTMACore(selected);
// Try to identify the row/column that we want
TMAGrid grid = hierarchy.getTMAGrid();
int row = -1;
int col = -1;
if (selectedCore != null) {
for (int y = 0; y < grid.getGridHeight(); y++) {
for (int x = 0; x < grid.getGridWidth(); x++) {
if (grid.getTMACore(y, x) == selectedCore) {
row = y;
col = x;
break;
}
}
}
}
// We need a selected core to know what to remove
if (row < 0 || col < 0) {
Dialogs.showErrorMessage(title, "Please select a TMA core to indicate which " + typeString + " to remove");
return false;
}
// Check we have enough rows/columns - if not, this is just a clear operation
if ((removeRow && grid.getGridHeight() <= 1) || (!removeRow && grid.getGridWidth() <= 1)) {
if (Dialogs.showConfirmDialog(title, "Are you sure you want to delete the entire TMA grid?"))
hierarchy.setTMAGrid(null);
return false;
}
// Confirm the removal - add 1 due to 'base 0' probably not being expected by most users...
int num = removeRow ? row : col;
if (!Dialogs.showConfirmDialog(title, "Are you sure you want to delete " + typeString + " " + (num + 1) + " from TMA grid?"))
return false;
// Create a new grid
List<TMACoreObject> coresNew = new ArrayList<>();
for (int r = 0; r < grid.getGridHeight(); r++) {
if (removeRow && row == r)
continue;
for (int c = 0; c < grid.getGridWidth(); c++) {
if (!removeRow && col == c)
continue;
coresNew.add(grid.getTMACore(r, c));
}
}
int newWidth = removeRow ? grid.getGridWidth() : grid.getGridWidth() - 1;
TMAGrid gridNew = DefaultTMAGrid.create(coresNew, newWidth);
hierarchy.setTMAGrid(gridNew);
hierarchy.getSelectionModel().clearSelection();
// Request new labels
promptToRelabelTMAGrid(imageData);
return true;
}
use of qupath.lib.objects.hierarchy.TMAGrid in project qupath by qupath.
the class TMASummaryViewer method updateSurvivalCurves.
private void updateSurvivalCurves() {
String colID = null;
String colScore = null;
colCensored = null;
for (String nameOrig : model.getAllNames()) {
if (nameOrig.equals(TMACoreObject.KEY_UNIQUE_ID))
colID = nameOrig;
else // else if (nameOrig.equals(TMACoreObject.KEY_CENSORED))
// colCensored = nameOrig;
// else if (!Number.class.isAssignableFrom())
// continue;
{
if (nameOrig.trim().length() == 0 || !model.getMeasurementNames().contains(nameOrig))
continue;
String name = nameOrig.toLowerCase();
if (name.equals("h-score"))
colScore = nameOrig;
else if (name.equals("positive %") && colScore == null)
colScore = nameOrig;
}
}
// Check for a column with the exact requested name
String colCensoredRequested = null;
String colSurvival = getSurvivalColumn();
if (colSurvival != null) {
colCensoredRequested = getRequestedSurvivalCensoredColumn(colSurvival);
if (model.getAllNames().contains(colCensoredRequested))
colCensored = colCensoredRequested;
else // Check for a general 'censored' column... less secure since it doesn't specify OS or RFS (but helps with backwards-compatibility)
if (model.getAllNames().contains("Censored")) {
logger.warn("Correct censored column for \"{}\" unavailable - should be \"{}\", but using \"Censored\" column instead", colSurvival, colCensoredRequested);
colCensored = "Censored";
}
}
if (colCensored == null && colSurvival != null) {
logger.warn("Unable to find censored column - survival data will be uncensored");
} else
logger.info("Survival column: {}, Censored column: {}", colSurvival, colCensored);
colScore = comboMainMeasurement.getSelectionModel().getSelectedItem();
if (colID == null || colSurvival == null || colCensored == null) {
// Adjust priority depending on whether we have any data at all..
if (!model.getItems().isEmpty())
logger.warn("No survival data found!");
else
logger.trace("No entries or survival data available");
return;
}
// Generate a pseudo TMA core hierarchy
Map<String, List<TMAEntry>> scoreMap = createScoresMap(model.getItems(), colScore, colID);
// System.err.println("Score map size: " + scoreMap.size() + "\tEntries: " + model.getEntries().size());
List<TMACoreObject> cores = new ArrayList<>(scoreMap.size());
double[] scores = new double[15];
for (Entry<String, List<TMAEntry>> entry : scoreMap.entrySet()) {
TMACoreObject core = new TMACoreObject();
core.setName("ID: " + entry.getKey());
MeasurementList ml = core.getMeasurementList();
Arrays.fill(scores, Double.POSITIVE_INFINITY);
List<TMAEntry> list = entry.getValue();
// Increase array size, if needed
if (list.size() > scores.length)
scores = new double[list.size()];
for (int i = 0; i < list.size(); i++) {
scores[i] = model.getNumericValue(list.get(i), colScore);
// scores[i] = list.get(i).getMeasurement(colScore).doubleValue();
}
Arrays.sort(scores);
int n = list.size();
double score;
if (n % 2 == 1)
score = scores[n / 2];
else
score = (scores[n / 2 - 1] + scores[n / 2]) / 2;
core.putMetadataValue(TMACoreObject.KEY_UNIQUE_ID, entry.getKey());
// System.err.println("Putting: " + list.get(0).getMeasurement(colSurvival).doubleValue() + " LIST: " + list.size());
ml.putMeasurement(colSurvival, list.get(0).getMeasurementAsDouble(colSurvival));
ml.putMeasurement(colCensoredRequested, list.get(0).getMeasurementAsDouble(colCensored));
if (colScore != null)
ml.putMeasurement(colScore, score);
cores.add(core);
// logger.info(entry.getKey() + "\t" + score);
}
TMAGrid grid = DefaultTMAGrid.create(cores, 1);
PathObjectHierarchy hierarchy = new PathObjectHierarchy();
hierarchy.setTMAGrid(grid);
kmDisplay.setHierarchy(hierarchy, colSurvival, colCensoredRequested);
kmDisplay.setScoreColumn(comboMainMeasurement.getSelectionModel().getSelectedItem());
// new KaplanMeierPlotTMA.KaplanMeierDisplay(hierarchy, colScore).show(frame, colScore);
}
use of qupath.lib.objects.hierarchy.TMAGrid in project qupath by qupath.
the class TMADataIO method writeTMAData.
/**
* Write TMA data in a human-readable (and viewable) way, with JPEGs and TXT/CSV files.
*
* @param file
* @param imageData
* @param overlayOptions
* @param downsampleFactor The downsample factor used for the TMA cores. If NaN, an automatic downsample value will be selected (>= 1). If <= 0, no cores are exported.
*/
public static void writeTMAData(File file, final ImageData<BufferedImage> imageData, OverlayOptions overlayOptions, final double downsampleFactor) {
if (imageData == null || imageData.getHierarchy() == null || imageData.getHierarchy().getTMAGrid() == null) {
logger.error("No TMA data available to save!");
return;
}
final ImageServer<BufferedImage> server = imageData.getServer();
String coreExt = imageData.getServer().isRGB() ? ".jpg" : ".tif";
if (file == null) {
file = Dialogs.promptToSaveFile("Save TMA data", null, ServerTools.getDisplayableImageName(server), "TMA data", "qptma");
if (file == null)
return;
} else if (file.isDirectory() || (!file.exists() && file.getAbsolutePath().endsWith(File.pathSeparator))) {
// Put inside the specified directory
file = new File(file, ServerTools.getDisplayableImageName(server) + TMA_DEARRAYING_DATA_EXTENSION);
if (!file.getParentFile().exists())
file.getParentFile().mkdirs();
}
final File dirData = new File(file + ".data");
if (!dirData.exists())
dirData.mkdir();
// Write basic file info
String delimiter = "\t";
TMAGrid tmaGrid = imageData.getHierarchy().getTMAGrid();
try {
PrintWriter writer = new PrintWriter(file);
writer.println(server.getPath());
writer.println(ServerTools.getDisplayableImageName(server));
writer.println();
writer.println("TMA grid width: " + tmaGrid.getGridWidth());
writer.println("TMA grid height: " + tmaGrid.getGridHeight());
writer.println("Core name" + delimiter + "X" + delimiter + "Y" + delimiter + "Width" + delimiter + "Height" + delimiter + "Present" + delimiter + TMACoreObject.KEY_UNIQUE_ID);
for (int row = 0; row < tmaGrid.getGridHeight(); row++) {
for (int col = 0; col < tmaGrid.getGridWidth(); col++) {
TMACoreObject core = tmaGrid.getTMACore(row, col);
if (!core.hasROI()) {
writer.println(core.getName() + delimiter + delimiter + delimiter + delimiter);
continue;
}
ROI pathROI = core.getROI();
int x = (int) pathROI.getBoundsX();
int y = (int) pathROI.getBoundsY();
int w = (int) Math.ceil(pathROI.getBoundsWidth());
int h = (int) Math.ceil(pathROI.getBoundsHeight());
String id = core.getUniqueID() == null ? "" : core.getUniqueID();
writer.println(core.getName() + delimiter + x + delimiter + y + delimiter + w + delimiter + h + delimiter + !core.isMissing() + delimiter + id);
}
}
writer.close();
} catch (Exception e) {
logger.error("Error writing TMA data: " + e.getLocalizedMessage(), e);
return;
}
// Save the summary results
ObservableMeasurementTableData tableData = new ObservableMeasurementTableData();
tableData.setImageData(imageData, tmaGrid.getTMACoreList());
SummaryMeasurementTableCommand.saveTableModel(tableData, new File(dirData, "TMA results - " + ServerTools.getDisplayableImageName(server) + ".txt"), Collections.emptyList());
boolean outputCoreImages = Double.isNaN(downsampleFactor) || downsampleFactor > 0;
if (outputCoreImages) {
// Create new overlay options, if we don't have some already
if (overlayOptions == null) {
overlayOptions = new OverlayOptions();
overlayOptions.setFillDetections(true);
}
final OverlayOptions options = overlayOptions;
// Write an overall TMA map (for quickly checking if the dearraying is ok)
File fileTMAMap = new File(dirData, "TMA map - " + ServerTools.getDisplayableImageName(server) + ".jpg");
double downsampleThumbnail = Math.max(1, (double) Math.max(server.getWidth(), server.getHeight()) / 1024);
RegionRequest request = RegionRequest.createInstance(server.getPath(), downsampleThumbnail, 0, 0, server.getWidth(), server.getHeight());
OverlayOptions optionsThumbnail = new OverlayOptions();
optionsThumbnail.setShowTMAGrid(true);
optionsThumbnail.setShowGrid(false);
optionsThumbnail.setShowAnnotations(false);
optionsThumbnail.setShowDetections(false);
try {
var renderedServer = new RenderedImageServer.Builder(imageData).layers(new TMAGridOverlay(overlayOptions)).downsamples(downsampleThumbnail).build();
ImageWriterTools.writeImageRegion(renderedServer, request, fileTMAMap.getAbsolutePath());
// ImageWriters.writeImageRegionWithOverlay(imageData.getServer(), Collections.singletonList(new TMAGridOverlay(overlayOptions, imageData)), request, fileTMAMap.getAbsolutePath());
} catch (IOException e) {
logger.warn("Unable to write image overview: " + e.getLocalizedMessage(), e);
}
final double downsample = Double.isNaN(downsampleFactor) ? (server.getPixelCalibration().hasPixelSizeMicrons() ? ServerTools.getDownsampleFactor(server, preferredExportPixelSizeMicrons) : 1) : downsampleFactor;
// Creating a plugin makes it possible to parallelize & show progress easily
var renderedImageServer = new RenderedImageServer.Builder(imageData).layers(new HierarchyOverlay(null, options, imageData)).downsamples(downsample).build();
ExportCoresPlugin plugin = new ExportCoresPlugin(dirData, renderedImageServer, downsample, coreExt);
PluginRunner<BufferedImage> runner;
var qupath = QuPathGUI.getInstance();
if (qupath == null || qupath.getImageData() != imageData) {
runner = new CommandLinePluginRunner<>(imageData);
plugin.runPlugin(runner, null);
} else {
try {
qupath.runPlugin(plugin, null, false);
} catch (Exception e) {
logger.error("Error writing TMA data: " + e.getLocalizedMessage(), e);
}
// new Thread(() -> qupath.runPlugin(plugin, null, false)).start();
// runner = new PluginRunnerFX(QuPathGUI.getInstance());
// new Thread(() -> plugin.runPlugin(runner, null)).start();
}
}
}
use of qupath.lib.objects.hierarchy.TMAGrid in project qupath by qupath.
the class TMAScoreImporter method importFromCSV.
private static int importFromCSV(final Map<String, List<String>> map, final PathObjectHierarchy hierarchy) {
TMAGrid tmaGrid = hierarchy.getTMAGrid();
if (tmaGrid == null || tmaGrid.nCores() == 0) {
logger.error("No TMA grid found!");
return 0;
}
// Try to get a 'core' column
String coreKey = null;
for (String key : map.keySet()) {
if (key.trim().toLowerCase().equals("core")) {
coreKey = key;
break;
}
}
List<String> coreNames = coreKey == null ? null : map.remove(coreKey);
// Try to get a unique ID column
List<String> coreIDs = map.remove(TMACoreObject.KEY_UNIQUE_ID);
// If we don't have a core column OR a unique ID column, we can't do anything
if (coreNames == null && coreIDs == null) {
logger.error("No column with header 'core' or '" + TMACoreObject.KEY_UNIQUE_ID + "' found");
return 0;
}
// int n = coreNames == null ? coreIDs.size() : coreNames.size();
// Get a list of cores ordered by whatever info we have
Map<Integer, List<TMACoreObject>> cores = new HashMap<>();
boolean coresFound = false;
// If we have any unique IDs, use these and change names accordingly - if possible, and necessary
if (coreIDs != null) {
int i = 0;
for (String id : coreIDs) {
List<TMACoreObject> coresByID = new ArrayList<>();
for (TMACoreObject coreTemp : tmaGrid.getTMACoreList()) if (id != null && id.equals(coreTemp.getUniqueID()))
coresByID.add(coreTemp);
if (!coresByID.isEmpty()) {
cores.put(i, coresByID);
coresFound = true;
if (coreNames != null && coresByID.size() == 1) {
String currentName = coresByID.get(0).getName();
String newName = coreNames.get(i);
if (!newName.equals(currentName)) {
coresByID.get(0).setName(newName);
if (currentName != null)
logger.warn("Core name changed from {} to {}", currentName, newName);
}
}
}
i++;
}
}
// If we didn't have any unique IDs, we need to work with core names instead
if (!coresFound && coreNames != null) {
int i = 0;
for (String name : coreNames) {
TMACoreObject core = tmaGrid.getTMACore(name);
if (core != null) {
cores.put(i, Collections.singletonList(core));
coresFound = true;
if (coreIDs != null) {
String currentID = core.getUniqueID();
String newID = coreIDs.get(i);
if (newID != null && !newID.equals(currentID)) {
core.setUniqueID(newID);
// It shouldn't occur that an existing ID is changed... although it's possible if there are duplicates
if (currentID != null)
logger.warn("Core unique ID changed from {} to {}", currentID, newID);
}
}
}
i++;
}
}
// Add extra columns from the map, either as metadata or measurements
for (Entry<String, List<String>> entry : map.entrySet()) {
// Skip columns without headers
if (entry.getKey() == null || entry.getKey().trim().length() == 0)
continue;
// If we have survival data, or else can't parse numeric values, add as metadata
boolean isOverallSurvival = entry.getKey().equalsIgnoreCase(TMACoreObject.KEY_OVERALL_SURVIVAL);
boolean isRecurrenceFreeSurvival = entry.getKey().equalsIgnoreCase(TMACoreObject.KEY_RECURRENCE_FREE_SURVIVAL);
boolean isOSCensored = entry.getKey().equalsIgnoreCase(TMACoreObject.KEY_OS_CENSORED);
boolean isRFSCensored = entry.getKey().equalsIgnoreCase(TMACoreObject.KEY_RFS_CENSORED);
// Try to parse numeric data, if we can
boolean isSurvivalRelated = isOverallSurvival || isRecurrenceFreeSurvival || isOSCensored || isRFSCensored;
double[] vals = parseNumeric(entry.getValue(), !isSurvivalRelated);
if (isSurvivalRelated || vals == null || vals.length == GeneralTools.numNaNs(vals)) {
for (int i : cores.keySet()) {
for (TMACoreObject core : cores.get(i)) {
if (core == null)
continue;
if (isOverallSurvival)
core.getMeasurementList().putMeasurement(TMACoreObject.KEY_OVERALL_SURVIVAL, vals[i]);
else if (isRecurrenceFreeSurvival)
core.getMeasurementList().putMeasurement(TMACoreObject.KEY_RECURRENCE_FREE_SURVIVAL, vals[i]);
else if (isOSCensored)
core.getMeasurementList().putMeasurement(TMACoreObject.KEY_OS_CENSORED, vals[i] > 0 ? 1 : 0);
else if (isRFSCensored)
core.getMeasurementList().putMeasurement(TMACoreObject.KEY_RFS_CENSORED, vals[i] > 0 ? 1 : 0);
else
core.putMetadataValue(entry.getKey(), entry.getValue().get(i));
}
}
} else {
// If we have a numeric column, add to measurement list
for (int i : cores.keySet()) {
for (TMACoreObject core : cores.get(i)) {
core.getMeasurementList().addMeasurement(entry.getKey(), vals[i]);
}
}
}
}
// Loop through and close any measurement lists, recording anywhere changes were made
Set<PathObject> changed = new HashSet<>();
for (List<TMACoreObject> coreList : cores.values()) {
for (TMACoreObject core : coreList) {
if (core == null)
continue;
core.getMeasurementList().close();
changed.add(core);
}
}
hierarchy.fireObjectsChangedEvent(null, changed);
return changed.size();
}
Aggregations