package edu.harvard.mcb.leschziner.core; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.image.BufferedImage; import java.awt.image.Kernel; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.nio.ByteBuffer; import java.util.Collection; import java.util.Stack; import java.util.Vector; import javax.imageio.ImageIO; import com.googlecode.javacv.cpp.opencv_core; import com.googlecode.javacv.cpp.opencv_core.CvMat; import com.googlecode.javacv.cpp.opencv_core.CvPoint; import com.googlecode.javacv.cpp.opencv_core.CvRect; import com.googlecode.javacv.cpp.opencv_core.CvSize; import com.googlecode.javacv.cpp.opencv_core.IplImage; import com.googlecode.javacv.cpp.opencv_highgui; import com.googlecode.javacv.cpp.opencv_imgproc; import edu.harvard.mcb.leschziner.convert.AutoImageReader; import edu.harvard.mcb.leschziner.util.MatrixUtils; /** * An image of a single particle from an EM image, generally a single * protein/object. The particle is enclosed in a square box, with some of the * surround. * * @author spartango * */ public class Particle implements Serializable, Cloneable { /** * */ private static final long serialVersionUID = 8805574980503468420L; private static final int CHANNELS = 1; // Image can't be serialized, will be transferred manually private transient IplImage image; // Image properties private final int size; private final int depth; // Particle ancestry info private long sourceId; private final int generation; // Stack of reversible transformation operations performed on this particle private Stack<AffineTransform> transforms; private Particle(int size, int depth, long sourceId, int generation) { this.size = size; this.depth = depth; this.sourceId = sourceId; this.generation = generation; transforms = new Stack<AffineTransform>(); } /** * Builds a particle from a square image * * @param image */ public Particle(BufferedImage image) { this(image, (long) (Long.MAX_VALUE * Math.random()), 0); } /** * Builds a particle from a square image * * @param image */ public Particle(BufferedImage image, long sourceId, int generation) { this(image.getWidth(), 8, sourceId, generation); IplImage tempImage = IplImage.createFrom(image); // Grayscale this image setImage(tempImage); } /** * Builds a particle from a square image * * @param image */ public Particle(IplImage image) { this(image, (long) (Long.MAX_VALUE * Math.random()), 0); } /** * Builds a particle from a square image * * @param image */ public Particle(IplImage image, long sourceId, int generation) { this(image.width(), image.depth(), sourceId, generation); // Grayscale this image setImage(image); } public long getSourceId() { return sourceId; } public void setSourceId(long sourceId) { this.sourceId = sourceId; } public int getGeneration() { return generation; } public int nextGeneration() { return generation + 1; } /** * Gets the size of the particle's box * * @return particle box size in pixels */ public int getSize() { return size; } public int getDepth() { return depth; } public int getChannels() { return CHANNELS; } // I/O methods /** * Gets the RGB value of a single pixel * * @param x * position * @param y * position * @return RGB pixel value */ public int getPixel(int x, int y) { return getPixelChannel(x, y, 0); } public int getPixelChannel(int x, int y, int channel) { // Find row, go to channel byte, compensate for unsigned value return image.getByteBuffer().get(y * image.widthStep() + CHANNELS * x + channel) & 0xFF; } /** * Provides a buffer with all the RGB pixels in the particle * * @param x * @param y * @param width * @param height * @return An Array of RGB pixels, in row major order */ public ByteBuffer getPixelBuffer() { return image.getByteBuffer(); } /** * Sets the value of a pixel at a given location * * @param x * position * @param y * position * @param RGB * value */ public void setPixel(int x, int y, int value) { setPixelChannel(x, y, 0, value); } private void setPixelChannel(int x, int y, int channel, int value) { image.getByteBuffer().put(y * image.widthStep() + CHANNELS * x + channel, (byte) (value)); } /** * Records a transformation in this particle's history of transformations * * @param affine * transform */ public void pushTransform(AffineTransform t) { // Push a transform that was just executed transforms.push(t); } /** * Sequentially undoes all the transformations previously performed on this * particle, returning a new particle in the original state * * @return new particle in untransformed (inverse transformed) state */ public Particle untransformed() { Particle result = this.clone(); // Pop off each transform Stack<AffineTransform> operationStack = (Stack<AffineTransform>) this.transforms.clone(); while (!operationStack.isEmpty()) { // Invert it if possible, try { AffineTransform transform = operationStack.pop() .createInverse(); // Apply it to the target result = transform(result, transform); } catch (NoninvertibleTransformException e) { e.printStackTrace(); } } // Wipe the target's history clean result.transforms = operationStack; return result; } /** * Provides a clone of this particle */ @Override public Particle clone() { Particle result = new Particle(image.clone(), sourceId, nextGeneration()); return result; } /** * Writes this particle to a stream (for serialization) * * @param out * @throws IOException */ private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); ImageIO.write(image.getBufferedImage(), "png", out); } /** * Reads this particle from a stream (for deserialization) * * @param in * @throws IOException * @throws ClassNotFoundException */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // read buff with imageIO from in setImage(IplImage.createFrom(ImageIO.read(in))); } /** * Returns a new particle that is a smaller area of this particle * * @param x * @param y * @param size * @return new subparticle */ public Particle subParticle(int x, int y, int size) { // Prepare a target image IplImage dst = IplImage.create(new CvSize(size, size), depth, CHANNELS); // set an ROI opencv_core.cvSetImageROI(image, new CvRect(x, y, size, size)); // Copy the ROI opencv_core.cvCopy(image, dst); // Unset the roi opencv_core.cvResetImageROI(image); return new Particle(dst, sourceId, nextGeneration()); } /** * Provides a buffered image representation of this particle (for drawing * etc) * * @return a buffered image */ public BufferedImage asBufferedImage() { return image.getBufferedImage(); } private void setImage(IplImage tempImage) { // Force the image to the right channel & colorscheme if (tempImage.nChannels() == 3) { this.image = IplImage.create(new CvSize(size, size), depth, CHANNELS); opencv_imgproc.cvCvtColor(tempImage, this.image, opencv_imgproc.CV_BGR2GRAY); } else { this.image = tempImage; } } public Particle createCompatible() { return new Particle(IplImage.createCompatible(image), sourceId, nextGeneration()); } public IplImage getImage() { return image; } /** * Writes this particle to a PNG file * * @param filename * @throws IOException */ public void toFile(String filename) throws IOException { opencv_highgui.cvSaveImage(filename, image); } public byte[] toPng() throws IOException { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); writeToStream(outStream); return outStream.toByteArray(); } public void writeToStream(OutputStream outStream) throws IOException { ImageIO.write(image.getBufferedImage(), "png", outStream); } public static Particle fromFile(String filename) throws IOException { BufferedImage image = AutoImageReader.readImage(filename); if (image == null) { throw new IOException("Couldn't read image file: " + filename); } return new Particle(image); } public static Collection<Particle> stackFromFile(String filename) throws IOException { Collection<BufferedImage> images = AutoImageReader.readStack(filename); if (images == null) { throw new IOException("Couldn't read image stack: " + filename); } Vector<Particle> particles = new Vector<>(images.size()); for (BufferedImage image : images) { particles.add(new Particle(image)); } return particles; } // Primitive operations // ----------------------------------------------------------------- // // All operations on a particle generate a new particle rather than // operating in place /** * Transforms a particle given a transformation matrix (affine transform) * * @param target * particle * @param matrix * (2x2 or 3x3) * @return new, transformed particle */ public static Particle transform(Particle target, double[][] matrix) { // Reformat the matrix double[] kernel = MatrixUtils.flatten(matrix); // TODO validate CvMat kernelMat = CvMat.create(matrix.length, matrix[0].length); kernelMat.put(0, kernel, 0, kernel.length); // Apply the transformation return transform(target, kernelMat); } /** * Transforms a particle given an affine transform * * @param target * particle * @param xform * , an affine transform * @return a new, transformed particle */ public static Particle transform(Particle target, AffineTransform xform) { // Grab data from the transform double[] matrix = new double[6]; // stored as { m00 m10 m01 m11 m02 m12 } xform.getMatrix(matrix); // Build an appropriate CvMat CvMat kernelMat = CvMat.create(2, 3); kernelMat.put(0, 0, matrix[0]); kernelMat.put(1, 0, matrix[1]); kernelMat.put(0, 1, matrix[2]); kernelMat.put(1, 1, matrix[3]); kernelMat.put(0, 2, matrix[4]); kernelMat.put(1, 2, matrix[5]); Particle result = transform(target, kernelMat); result.pushTransform(xform); return result; } public static Particle transform(Particle target, CvMat kernel) { IplImage dst = IplImage.createCompatible(target.image); // Apply the transform opencv_imgproc.cvWarpAffine(target.image, dst, kernel); return new Particle(dst, target.sourceId, target.nextGeneration()); } /** * Convolves a particle with a matrix * * @param target * particle * @param kernel * , the matrix to be convolved with the particle * @return a new, convolved particle */ public static Particle convolve(Particle target, float[][] kernel) { int kernelHeight = kernel.length; int kernelWidth = kernel[0].length; double[] basisKernel = MatrixUtils.upConvertArray(MatrixUtils.flatten(kernel)); // Build a Kernel CvMat kernelMat = CvMat.create(kernelHeight, kernelWidth); kernelMat.put(0, basisKernel, 0, basisKernel.length); return convolve(target, kernelMat); } /** * Convolves a kernel with a particle * * @param target * particle * @param kernel * , to be convolved with the particle * @return a new, convolved particle */ public static Particle convolve(Particle target, Kernel kernel) { double[] kernelData = MatrixUtils.upConvertArray(kernel.getKernelData(null)); // Build a CvMat CvMat kernelMat = CvMat.create(kernel.getHeight(), kernel.getWidth()); // Copy in kernel data kernelMat.put(0, kernelData, 0, kernelData.length); return convolve(target, kernelMat); } public static Particle convolve(Particle target, CvMat kernel) { IplImage dst = IplImage.createCompatible(target.image); opencv_imgproc.cvFilter2D(target.image, dst, kernel, new CvPoint(-1, -1)); return new Particle(dst, target.sourceId, target.nextGeneration()); } /** * Scales the values of a particle (scalar multiplication) * * @param target * particle * @param scaleFactor * to be multiplied at each pixel * @return new, scaled particle */ public static Particle scale(Particle target, float scaleFactor) { IplImage dst = IplImage.createCompatible(target.image); opencv_core.cvScale(target.image, dst, scaleFactor, 0); return new Particle(dst, target.sourceId, target.nextGeneration()); } /** * Adds a scalar value to each pixel * * @param target * particle * @param offset * to be added to each pixel * @return a new particle with each pixel increased by the scalar */ public static Particle addScalar(Particle target, float offset) { IplImage dst = IplImage.createCompatible(target.image); opencv_core.cvScale(target.image, dst, 1.0, offset); return new Particle(dst, target.sourceId, target.nextGeneration()); } public static Particle subtract(Particle firstParticle, Particle secondParticle) { IplImage dst = IplImage.createCompatible(firstParticle.image); opencv_core.cvSub(firstParticle.image, secondParticle.image, dst, null); return new Particle(dst); } }