use of qupath.lib.color.ColorDeconvolutionStains in project qupath by qupath.
the class ImageData method setColorDeconvolutionStains.
/**
* Set the color deconvolution stain vectors for the current image type.
* <p>
* If the type is not brightfield, an IllegalArgumentException is thrown.
*
* @param stains
*/
public void setColorDeconvolutionStains(ColorDeconvolutionStains stains) {
if (!isBrightfield())
throw new IllegalArgumentException("Cannot set color deconvolution stains for image type " + type);
logger.trace("Setting stains to {}", stains);
ColorDeconvolutionStains stainsOld = stainMap.put(type, stains);
pcs.firePropertyChange("stains", stainsOld, stains);
addColorDeconvolutionStainsToWorkflow(this);
// logger.error("WARNING: Setting color deconvolution stains is not yet scriptable!!!!");
changes = true;
}
use of qupath.lib.color.ColorDeconvolutionStains in project qupath by qupath.
the class ImageDetailsPane method editStainVector.
void editStainVector(Object value) {
if (!(value instanceof StainVector || value instanceof double[]))
return;
// JOptionPane.showMessageDialog(null, "Modifying stain vectors not yet implemented...");
ImageData<BufferedImage> imageData = qupath.getImageData();
if (imageData == null)
return;
ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
// Default to background values
int num = -1;
String name = null;
String message = null;
if (value instanceof StainVector) {
StainVector stainVector = (StainVector) value;
if (stainVector.isResidual() && imageData.getImageType() != ImageType.BRIGHTFIELD_OTHER) {
logger.warn("Cannot set residual stain vector - this is computed from the known vectors");
return;
}
num = stains.getStainNumber(stainVector);
if (num <= 0) {
logger.error("Could not identify stain vector " + stainVector + " inside " + stains);
return;
}
name = stainVector.getName();
message = "Set stain vector from ROI?";
} else
message = "Set color deconvolution background values from ROI?";
ROI pathROI = imageData.getHierarchy().getSelectionModel().getSelectedROI();
boolean wasChanged = false;
String warningMessage = null;
boolean editableName = imageData.getImageType() == ImageType.BRIGHTFIELD_OTHER;
if (pathROI != null) {
if ((pathROI instanceof RectangleROI) && !pathROI.isEmpty() && ((RectangleROI) pathROI).getArea() < 500 * 500) {
if (Dialogs.showYesNoDialog("Color deconvolution stains", message)) {
ImageServer<BufferedImage> server = imageData.getServer();
BufferedImage img = null;
try {
img = server.readBufferedImage(RegionRequest.createInstance(server.getPath(), 1, pathROI));
} catch (IOException e) {
Dialogs.showErrorMessage("Set stain vector", "Unable to read image region");
logger.error("Unable to read region", e);
}
int rgb = ColorDeconvolutionHelper.getMedianRGB(img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth()));
if (num >= 0) {
StainVector vectorValue = ColorDeconvolutionHelper.generateMedianStainVectorFromPixels(name, img.getRGB(0, 0, img.getWidth(), img.getHeight(), null, 0, img.getWidth()), stains.getMaxRed(), stains.getMaxGreen(), stains.getMaxBlue());
if (!Double.isFinite(vectorValue.getRed() + vectorValue.getGreen() + vectorValue.getBlue())) {
Dialogs.showErrorMessage("Set stain vector", "Cannot set stains for the current ROI!\n" + "It might be too close to the background color.");
return;
}
value = vectorValue;
} else {
// Update the background
value = new double[] { ColorTools.red(rgb), ColorTools.green(rgb), ColorTools.blue(rgb) };
}
wasChanged = true;
}
} else {
warningMessage = "Note: To set stain values from an image region, draw a small, rectangular ROI first";
}
}
// Prompt to set the name / verify stains
ParameterList params = new ParameterList();
String title;
String nameBefore = null;
String valuesBefore = null;
String collectiveNameBefore = stains.getName();
String suggestedName;
if (collectiveNameBefore.endsWith("default"))
suggestedName = collectiveNameBefore.substring(0, collectiveNameBefore.lastIndexOf("default")) + "modified";
else
suggestedName = collectiveNameBefore;
params.addStringParameter("collectiveName", "Collective name", suggestedName, "Enter collective name for all 3 stains (e.g. H-DAB Scanner A, H&E Scanner B)");
if (value instanceof StainVector) {
nameBefore = ((StainVector) value).getName();
valuesBefore = ((StainVector) value).arrayAsString(Locale.getDefault(Category.FORMAT));
params.addStringParameter("name", "Name", nameBefore, "Enter stain name").addStringParameter("values", "Values", valuesBefore, "Enter 3 values (red, green, blue) defining color deconvolution stain vector, separated by spaces");
title = "Set stain vector";
} else {
nameBefore = "Background";
valuesBefore = GeneralTools.arrayToString(Locale.getDefault(Category.FORMAT), (double[]) value, 2);
params.addStringParameter("name", "Stain name", nameBefore);
params.addStringParameter("values", "Stain values", valuesBefore, "Enter 3 values (red, green, blue) defining background, separated by spaces");
params.setHiddenParameters(true, "name");
title = "Set background";
}
if (warningMessage != null)
params.addEmptyParameter(warningMessage);
// Disable editing the name if it should be fixed
ParameterPanelFX parameterPanel = new ParameterPanelFX(params);
parameterPanel.setParameterEnabled("name", editableName);
;
if (!Dialogs.showConfirmDialog(title, parameterPanel.getPane()))
return;
// Check if anything changed
String collectiveName = params.getStringParameterValue("collectiveName");
String nameAfter = params.getStringParameterValue("name");
String valuesAfter = params.getStringParameterValue("values");
if (collectiveName.equals(collectiveNameBefore) && nameAfter.equals(nameBefore) && valuesAfter.equals(valuesBefore) && !wasChanged)
return;
double[] valuesParsed = ColorDeconvolutionStains.parseStainValues(Locale.getDefault(Category.FORMAT), valuesAfter);
if (valuesParsed == null) {
logger.error("Input for setting color deconvolution information invalid! Cannot parse 3 numbers from {}", valuesAfter);
return;
}
if (num >= 0) {
try {
stains = stains.changeStain(StainVector.createStainVector(nameAfter, valuesParsed[0], valuesParsed[1], valuesParsed[2]), num);
} catch (Exception e) {
logger.error("Error setting stain vectors", e);
Dialogs.showErrorMessage("Set stain vectors", "Requested stain vectors are not valid!\nAre two stains equal?");
}
} else {
// Update the background
stains = stains.changeMaxValues(valuesParsed[0], valuesParsed[1], valuesParsed[2]);
}
// Set the collective name
stains = stains.changeName(collectiveName);
imageData.setColorDeconvolutionStains(stains);
qupath.getViewer().repaintEntireImage();
}
use of qupath.lib.color.ColorDeconvolutionStains in project qupath by qupath.
the class PositivePixelCounterIJ method getDefaultParameterList.
@Override
public ParameterList getDefaultParameterList(final ImageData<BufferedImage> imageData) {
ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
String stain1Name = stains == null ? "Hematoxylin" : stains.getStain(1).getName();
String stain2Name = stains == null ? "DAB" : stains.getStain(2).getName();
ParameterList params = new ParameterList().addIntParameter("downsampleFactor", "Downsample factor", 4, "", 1, 32, "Amount to downsample image prior to detection - higher values lead to smaller images (and faster but less accurate processing)").addDoubleParameter("gaussianSigmaMicrons", "Gaussian sigma", 2, GeneralTools.micrometerSymbol(), "Gaussian filter size - higher values give a smoother (less-detailed) result").addDoubleParameter("thresholdStain1", stain1Name + " threshold ('Negative')", 0.1, "OD units", "Threshold to use for 'Negative' detection").addDoubleParameter("thresholdStain2", stain2Name + " threshold ('Positive')", 0.3, "OD units", "Threshold to use for 'Positive' stain detection").addBooleanParameter("addSummaryMeasurements", "Add summary measurements to parent", true, "Add summary measurements to parent objects").addBooleanParameter("clearParentMeasurements", "Clear existing parent measurements", true, "Remove any existing measurements from parent objects").addBooleanParameter("appendDetectionParameters", "Add parameters to measurement names", false, "Append the detection parameters to any measurement names").addBooleanParameter("legacyMeasurements0.1.2", "Use legacy measurements (v0.1.2)", false, "Generate measurements compatible with QuPath v0.1.2");
return params;
}
use of qupath.lib.color.ColorDeconvolutionStains in project qupath by qupath.
the class EstimateStainVectors method estimateStains.
/**
* Check colors only currently applies to H&E.
*
* @param rgbPacked
* @param redOD
* @param greenOD
* @param blueOD
* @param stainsOriginal
* @param minStain
* @param maxStain
* @param ignorePercentage
* @param checkColors
* @return
*/
public static ColorDeconvolutionStains estimateStains(final int[] rgbPacked, final float[] redOD, final float[] greenOD, final float[] blueOD, final ColorDeconvolutionStains stainsOriginal, final double minStain, final double maxStain, final double ignorePercentage, final boolean checkColors) {
double alpha = ignorePercentage / 100;
int n = rgbPacked.length;
if (redOD.length != n || greenOD.length != n || blueOD.length != n)
throw new IllegalArgumentException("All pixel arrays must be the same length!");
int[] rgb = Arrays.copyOf(rgbPacked, n);
float[] red = Arrays.copyOf(redOD, n);
float[] green = Arrays.copyOf(greenOD, n);
float[] blue = Arrays.copyOf(blueOD, n);
// Check if we do color sanity test
boolean doColorTestForHE = checkColors && stainsOriginal.isH_E();
// boolean doColorTestForHDAB = checkColors && stainsOriginal.isH_DAB();
boolean doGrayTest = checkColors && (stainsOriginal.isH_E() || stainsOriginal.isH_DAB());
double sqrt3 = 1 / Math.sqrt(3);
double grayThreshold = Math.cos(0.15);
// Loop through and discard pixels that are too faintly or densely stained
int keepCount = 0;
double maxStainSq = maxStain * maxStain;
for (int i = 0; i < rgb.length; i++) {
float r = red[i];
float g = green[i];
float b = blue[i];
double magSquared = r * r + g * g + b * b;
if (magSquared > maxStainSq || r < minStain || g < minStain || b < minStain || magSquared <= 0)
continue;
// Check for consistency with H&E staining, if required (i.e. only keep red/pink/purple/blue pixels and the like)
if (doColorTestForHE && (r > g || b > g)) {
continue;
}
// Exclude very 'gray' pixels
if (doGrayTest && (r * sqrt3 + g * sqrt3 + b * sqrt3) / Math.sqrt(magSquared) >= grayThreshold) {
continue;
}
// Update the arrays
red[keepCount] = r;
green[keepCount] = g;
blue[keepCount] = b;
rgb[keepCount] = rgb[i];
keepCount++;
}
if (keepCount <= 1)
throw new IllegalArgumentException("Not enough pixels remain after applying stain thresholds!");
// Trim the arrays
if (keepCount < rgb.length) {
red = Arrays.copyOf(red, keepCount);
green = Arrays.copyOf(green, keepCount);
blue = Arrays.copyOf(blue, keepCount);
rgb = Arrays.copyOf(rgb, keepCount);
}
double[][] cov = new double[3][3];
cov[0][0] = covariance(red, red);
cov[1][1] = covariance(green, green);
cov[2][2] = covariance(blue, blue);
cov[0][1] = covariance(red, green);
cov[0][2] = covariance(red, blue);
cov[1][2] = covariance(green, blue);
cov[2][1] = cov[1][2];
cov[2][0] = cov[0][2];
cov[1][0] = cov[0][1];
RealMatrix mat = MatrixUtils.createRealMatrix(cov);
logger.debug("Covariance matrix:\n {}", getMatrixAsString(mat.getData()));
EigenDecomposition eigen = new EigenDecomposition(mat);
double[] eigenValues = eigen.getRealEigenvalues();
int[] eigenOrder = rank(eigenValues);
double[] eigen1 = eigen.getEigenvector(eigenOrder[2]).toArray();
double[] eigen2 = eigen.getEigenvector(eigenOrder[1]).toArray();
logger.debug("First eigenvector: " + getVectorAsString(eigen1));
logger.debug("Second eigenvector: " + getVectorAsString(eigen2));
double[] phi = new double[keepCount];
for (int i = 0; i < keepCount; i++) {
double r = red[i];
double g = green[i];
double b = blue[i];
phi[i] = Math.atan2(r * eigen1[0] + g * eigen1[1] + b * eigen1[2], r * eigen2[0] + g * eigen2[1] + b * eigen2[2]);
}
/*
* Rather than projecting onto the plane (which might be a bit wrong),
* select the vectors directly from the data.
* This is effectively like a region selection, but where the region has
* been chosen automatically.
*/
int[] inds = rank(phi);
int ind1 = inds[Math.max(0, (int) (alpha * keepCount + .5))];
int ind2 = inds[Math.min(inds.length - 1, (int) ((1 - alpha) * keepCount + .5))];
// Create new stain vectors
StainVector s1 = StainVector.createStainVector(stainsOriginal.getStain(1).getName(), red[ind1], green[ind1], blue[ind1]);
StainVector s2 = StainVector.createStainVector(stainsOriginal.getStain(2).getName(), red[ind2], green[ind2], blue[ind2]);
// If working with H&E, we can use the simple heuristic of comparing the red values
if (stainsOriginal.isH_E()) {
// Need to check within the stain vectors (*not* original indexed values) because normalisation is important (I think... there were errors before)
if (s1.getRed() < s2.getRed()) {
s1 = StainVector.createStainVector(stainsOriginal.getStain(1).getName(), red[ind2], green[ind2], blue[ind2]);
s2 = StainVector.createStainVector(stainsOriginal.getStain(2).getName(), red[ind1], green[ind1], blue[ind1]);
}
} else {
// Check we've got the closest match - if not, switch the order
double angle11 = StainVector.computeAngle(s1, stainsOriginal.getStain(1));
double angle12 = StainVector.computeAngle(s1, stainsOriginal.getStain(2));
double angle21 = StainVector.computeAngle(s2, stainsOriginal.getStain(1));
double angle22 = StainVector.computeAngle(s2, stainsOriginal.getStain(2));
if (Math.min(angle12, angle21) < Math.min(angle11, angle22)) {
s1 = StainVector.createStainVector(stainsOriginal.getStain(1).getName(), red[ind2], green[ind2], blue[ind2]);
s2 = StainVector.createStainVector(stainsOriginal.getStain(2).getName(), red[ind1], green[ind1], blue[ind1]);
}
}
ColorDeconvolutionStains stains = new ColorDeconvolutionStains(stainsOriginal.getName(), s1, s2, stainsOriginal.getMaxRed(), stainsOriginal.getMaxGreen(), stainsOriginal.getMaxBlue());
return stains;
}
use of qupath.lib.color.ColorDeconvolutionStains in project qupath by qupath.
the class ImageData method addColorDeconvolutionStainsToWorkflow.
// TODO: REINTRODUCE LOGGING!
private static void addColorDeconvolutionStainsToWorkflow(ImageData<?> imageData) {
// logger.warn("Color deconvolution stain logging not currently enabled!");
ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
if (stains == null) {
return;
}
String arg = ColorDeconvolutionStains.getColorDeconvolutionStainsAsString(imageData.getColorDeconvolutionStains(), 5);
Map<String, String> map = GeneralTools.parseArgStringValues(arg);
WorkflowStep lastStep = imageData.getHistoryWorkflow().getLastStep();
String commandName = "Set color deconvolution stains";
WorkflowStep newStep = new DefaultScriptableWorkflowStep(commandName, map, "setColorDeconvolutionStains(\'" + arg + "');");
// else
if (!Objects.equals(newStep, lastStep))
imageData.getHistoryWorkflow().addStep(newStep);
// ColorDeconvolutionStains stains = imageData.getColorDeconvolutionStains();
// if (stains == null)
// return;
//
// String arg = ColorDeconvolutionStains.getColorDeconvolutionStainsAsString(imageData.getColorDeconvolutionStains(), 5);
// Map<String, String> map = GeneralTools.parseArgStringValues(arg);
// WorkflowStep lastStep = imageData.getWorkflow().getLastStep();
// String commandName = "Set color deconvolution stains";
// WorkflowStep newStep = new DefaultScriptableWorkflowStep(commandName,
// map,
// QP.class.getSimpleName() + ".setColorDeconvolutionStains(\'" + arg + "');");
//
// if (lastStep != null && commandName.equals(lastStep.getName()))
// imageData.getWorkflow().replaceLastStep(newStep);
// else
// imageData.getWorkflow().addStep(newStep);
}
Aggregations