Search in sources :

Example 1 with StainVector

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;
    }
}
Also used : StainVector(qupath.lib.color.StainVector) BufferedImage(java.awt.image.BufferedImage) DefaultColorDeconvolutionStains(qupath.lib.color.ColorDeconvolutionStains.DefaultColorDeconvolutionStains) ColorDeconvolutionStains(qupath.lib.color.ColorDeconvolutionStains)

Example 2 with StainVector

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();
}
Also used : StainVector(qupath.lib.color.StainVector) IOException(java.io.IOException) RectangleROI(qupath.lib.roi.RectangleROI) ROI(qupath.lib.roi.interfaces.ROI) BufferedImage(java.awt.image.BufferedImage) ParameterPanelFX(qupath.lib.gui.dialogs.ParameterPanelFX) IOException(java.io.IOException) RectangleROI(qupath.lib.roi.RectangleROI) ParameterList(qupath.lib.plugins.parameters.ParameterList) ColorDeconvolutionStains(qupath.lib.color.ColorDeconvolutionStains)

Example 3 with StainVector

use of qupath.lib.color.StainVector in project qupath by qupath.

the class EstimateStainVectors method estimateStains.

/**
 * Check colors only currently applies to H&amp;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;
}
Also used : StainVector(qupath.lib.color.StainVector) EigenDecomposition(org.apache.commons.math3.linear.EigenDecomposition) RealMatrix(org.apache.commons.math3.linear.RealMatrix) ColorDeconvolutionStains(qupath.lib.color.ColorDeconvolutionStains)

Aggregations

ColorDeconvolutionStains (qupath.lib.color.ColorDeconvolutionStains)3 StainVector (qupath.lib.color.StainVector)3 BufferedImage (java.awt.image.BufferedImage)2 IOException (java.io.IOException)1 EigenDecomposition (org.apache.commons.math3.linear.EigenDecomposition)1 RealMatrix (org.apache.commons.math3.linear.RealMatrix)1 DefaultColorDeconvolutionStains (qupath.lib.color.ColorDeconvolutionStains.DefaultColorDeconvolutionStains)1 ParameterPanelFX (qupath.lib.gui.dialogs.ParameterPanelFX)1 ParameterList (qupath.lib.plugins.parameters.ParameterList)1 RectangleROI (qupath.lib.roi.RectangleROI)1 ROI (qupath.lib.roi.interfaces.ROI)1