use of qupath.lib.color.StainVector in project qupath by qupath.
the class GuiTools method estimateImageType.
/**
* Make a semi-educated guess at the image type of a PathImageServer.
*
* @param server
* @param imgThumbnail Thumbnail for the image. This is now a required parameter (previously <= 0.1.2 it was optional).
*
* @return
*/
public static ImageData.ImageType estimateImageType(final ImageServer<BufferedImage> server, final BufferedImage imgThumbnail) {
if (!server.isRGB())
return ImageData.ImageType.FLUORESCENCE;
BufferedImage img = imgThumbnail;
// BufferedImage img;
// if (imgThumbnail == null)
// img = server.getBufferedThumbnail(220, 220, 0);
// else {
// img = imgThumbnail;
// // Rescale if necessary
// if (img.getWidth() * img.getHeight() > 400*400) {
// imgThumbnail.getS
// }
// }
int w = img.getWidth();
int h = img.getHeight();
int[] rgb = img.getRGB(0, 0, w, h, null, 0, w);
long rSum = 0;
long gSum = 0;
long bSum = 0;
int nDark = 0;
int nLight = 0;
int n = 0;
int darkThreshold = 25;
int lightThreshold = 220;
for (int v : rgb) {
int r = ColorTools.red(v);
int g = ColorTools.green(v);
int b = ColorTools.blue(v);
if (r < darkThreshold & g < darkThreshold && b < darkThreshold)
nDark++;
else if (r > lightThreshold & g > lightThreshold && b > lightThreshold)
nLight++;
else {
n++;
rSum += r;
gSum += g;
bSum += b;
}
}
if (nDark == 0 && nLight == 0)
return ImageData.ImageType.OTHER;
// If we have more dark than light pixels, assume fluorescence
if (nDark >= nLight)
return ImageData.ImageType.FLUORESCENCE;
if (n == 0) {
logger.warn("Unable to estimate brightfield stains (no stained pixels found)");
return ImageData.ImageType.BRIGHTFIELD_OTHER;
}
// Color color = new Color(
// (int)(rSum/n + .5),
// (int)(gSum/n + .5),
// (int)(bSum/n + .5));
// logger.debug("Color: " + color.toString());
// Compare optical density vector angles with the defaults for hematoxylin, eosin & DAB
ColorDeconvolutionStains stainsH_E = ColorDeconvolutionStains.makeDefaultColorDeconvolutionStains(DefaultColorDeconvolutionStains.H_E);
double rOD = ColorDeconvolutionHelper.makeOD(rSum / n, stainsH_E.getMaxRed());
double gOD = ColorDeconvolutionHelper.makeOD(gSum / n, stainsH_E.getMaxGreen());
double bOD = ColorDeconvolutionHelper.makeOD(bSum / n, stainsH_E.getMaxBlue());
StainVector stainMean = StainVector.createStainVector("Mean Stain", rOD, gOD, bOD);
double angleH = StainVector.computeAngle(stainMean, stainsH_E.getStain(1));
double angleE = StainVector.computeAngle(stainMean, stainsH_E.getStain(2));
ColorDeconvolutionStains stainsH_DAB = ColorDeconvolutionStains.makeDefaultColorDeconvolutionStains(DefaultColorDeconvolutionStains.H_DAB);
double angleDAB = StainVector.computeAngle(stainMean, stainsH_DAB.getStain(2));
// For H&E staining, eosin is expected to predominate... if it doesn't, assume H-DAB
logger.debug("Angle hematoxylin: " + angleH);
logger.debug("Angle eosin: " + angleE);
logger.debug("Angle DAB: " + angleDAB);
if (angleDAB < angleE || angleH < angleE) {
logger.info("Estimating H-DAB staining");
return ImageData.ImageType.BRIGHTFIELD_H_DAB;
} else {
logger.info("Estimating H & E staining");
return ImageData.ImageType.BRIGHTFIELD_H_E;
}
}
use of qupath.lib.color.StainVector 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.StainVector 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;
}
Aggregations