package edu.harvard.mcb.leschziner.analyze; import java.awt.Rectangle; import java.io.Serializable; import java.util.Vector; import com.googlecode.javacv.cpp.opencv_core.CvMat; import com.googlecode.javacv.cpp.opencv_core.CvRect; import edu.harvard.mcb.leschziner.core.Particle; /** * Extracts blobs from a thresholded (targets are white, everything else is * black) image. Utilizes connected components, and selects only blobs within * epsilon of the targeted size. Provides square regions surrounding each blob. * * @author spartango * */ public class BlobExtractor implements Serializable { /** * */ private static final long serialVersionUID = 8979444781712887357L; // Value of an unlabeled pixel in the labeled blob regions private static final int UNLABELED = 0; // Kernel Sectors for an 8-connected kernel (Connected Components) private static final int SECTOR_A = 0; private static final int SECTOR_B = 1; private static final int SECTOR_C = 2; // private static final int SECTOR_X = 3; private static final int SECTOR_D = 4; private static final int BLACK = 0; // The expected size of the particle we're finding in pixels private final int targetSize; // Amount of variability allowed in the particles chosen (+/- epsilon // pixels) private final int epsilon; /** * Constructs a blob extractor * * @param size * : expected side length of blobs * @param epsilon * : size variability in selected blobs, +/- pixels from size */ public BlobExtractor(int size, int epsilon) { this.targetSize = size; this.epsilon = epsilon; } /** * Extracts blobs from the selected Particle using a 2-Pass Connected * components approach. Blobs are described by their bounding squares * * @param target * : particle from which blobs are to be extracted * @return Array of squares containing blobs */ public Rectangle[] extract(Particle target) { int size = target.getSize(); // Pass 1: labeling int[][] labelBuffer = new int[size][size]; int labels = labelPass(target, labelBuffer, size); // Pass 2: Region aggregation & Bounds Rectangle[] rects = extractPass(labelBuffer, labels, size); // Filtering by size Vector<Rectangle> filteredBounds = selectionPass(rects); return filteredBounds.toArray(new Rectangle[filteredBounds.size()]); } public Rectangle[] extract(CvMat target) { int size = target.cols(); // Pass 1: labeling int[][] labelBuffer = new int[size][size]; int labels = labelPass(target, labelBuffer, size); // Pass 2: Region aggregation & Bounds Rectangle[] rects = extractPass(labelBuffer, labels, size); // Filtering by size Vector<Rectangle> filteredBounds = selectionPass(rects); return filteredBounds.toArray(new Rectangle[filteredBounds.size()]); } public static int labelPass(CvMat target, int[][] labelBuffer, int size) { // Labels are generated to identify each individual blob int currentLabel = 1; // Labeling starts at 1. // This will hold labels as they're generated // Pass 1: Region labeling int[] labelKernel = new int[6]; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { // Get the pixel value from the particle double xPixel = target.get(x, y); // Pixel is in foreground (is a blob) if (xPixel != BLACK) { currentLabel = labelFgPoint(labelBuffer, labelKernel, currentLabel, x, y, size); } } } return currentLabel; } private static int labelPass(Particle target, int[][] labelBuffer, int size) { // Labels are generated to identify each individual blob int currentLabel = 1; // Labeling starts at 1. // This will hold labels as they're generated // Pass 1: Region labeling int[] labelKernel = new int[6]; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { // Get the pixel value from the particle int xPixel = target.getPixel(x, y); // Pixel is in foreground (is a blob) if (xPixel != BLACK) { currentLabel = labelFgPoint(labelBuffer, labelKernel, currentLabel, x, y, size); } } } return currentLabel; } private static int labelFgPoint(int[][] labelBuffer, int[] labelKernel, int currentLabel, int x, int y, int size) { // Check for pre-existing labels around the target pixel, if // those pixels exist labelKernel[SECTOR_A] = (x > 0 && y > 0 ? labelBuffer[y - 1][x - 1] : UNLABELED); labelKernel[SECTOR_B] = (y > 0 ? labelBuffer[y - 1][x] : UNLABELED); labelKernel[SECTOR_C] = (x < size - 1 && y > 0 ? labelBuffer[y - 1][x + 1] : UNLABELED); labelKernel[SECTOR_D] = (x > 0 ? labelBuffer[y][x - 1] : UNLABELED); // If none of the surrounding pixels are part of blobs if (labelKernel[SECTOR_A] == UNLABELED && labelKernel[SECTOR_B] == UNLABELED && labelKernel[SECTOR_C] == UNLABELED && labelKernel[SECTOR_D] == UNLABELED) { // Assign a new label labelBuffer[y][x] = currentLabel; currentLabel++; } else { // At least one of the surrounding pixels is already in // a labeled blob // Find the lowest label > 0 int minLabel = currentLabel; for (int i = SECTOR_A; i <= SECTOR_D; i++) { if (labelKernel[i] > UNLABELED && labelKernel[i] < minLabel) { minLabel = labelKernel[i]; } } // Label the target labelBuffer[y][x] = minLabel; // Assign all sectors that value if (labelKernel[SECTOR_A] != UNLABELED) { labelBuffer[y - 1][x - 1] = minLabel; } if (labelKernel[SECTOR_B] != UNLABELED) { labelBuffer[y - 1][x] = minLabel; } if (labelKernel[SECTOR_C] != UNLABELED) { labelBuffer[y - 1][x + 1] = minLabel; } if (labelKernel[SECTOR_D] != UNLABELED) { labelBuffer[y][x - 1] = minLabel; } } return currentLabel; } private static Rectangle[] extractPass(int[][] labelBuffer, int currentLabel, int size) { // Preallocate a vector to hold our rectangles Rectangle[] rects = new Rectangle[currentLabel - 1]; // Scan across the label buffer for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { int labelPixel = labelBuffer[y][x]; // Check if the pixel is labeled if (labelPixel != UNLABELED) { if (rects[labelPixel - 1] == null) { // Create a new bounding box for this label if none // exists rects[labelPixel - 1] = new Rectangle(x, y, 1, 1); } else if (!rects[labelPixel - 1].contains(x, y)) { // Adjust the bounding box of this label to include this // pixel rects[labelPixel - 1].add(x, y); } } } } return rects; } private Vector<Rectangle> selectionPass(Rectangle[] rects) { Vector<Rectangle> filteredBounds = new Vector<Rectangle>(); for (Rectangle rect : rects) { // Make the rectangle a square, as we use square boxes if (rect != null && rect.getWidth() != rect.getHeight()) { int maxBound = (int) Math.max(rect.getWidth(), rect.getHeight()); rect.setSize(maxBound, maxBound); } // Check the side-length against the target side length +/- // epsilon // Also excludes particles extending beyond the frame if (rect != null && Math.abs(rect.getWidth() - targetSize) <= epsilon) { filteredBounds.add(rect); } } return filteredBounds; } public static CvRect cvRectFromRectangle(Rectangle target) { return new CvRect(target.x, target.y, target.width, target.height); } public static Rectangle RectangleFromCvRect(CvRect target) { return new Rectangle(target.x(), target.y(), target.width(), target.height()); } }