//----------------------------------------------------------------------------// // // // P i c t u r e // // // //----------------------------------------------------------------------------// // <editor-fold defaultstate="collapsed" desc="hdr"> // // Copyright (C) Hervé Bitteur and Brenton Partridge 2000-2013. // // This software is released under the GNU General Public License. // // Goto http://kenai.com/projects/audiveris to report bugs or suggestions. // //----------------------------------------------------------------------------// // </editor-fold> package omr.sheet.picture; import omr.constant.Constant; import omr.constant.ConstantSet; import omr.run.PixelSource; import omr.selection.LocationEvent; import omr.selection.MouseMovement; import omr.selection.PixelLevelEvent; import omr.selection.SelectionService; import org.bushe.swing.event.EventSubscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.awt.image.renderable.ParameterBlock; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; /** * Class {@code Picture} encapsulates an image, allowing modifications * and rendering. * Its current implementation is based on JAI (Java Advanced Imaging). * * <p> Operations allow : <ul> * <li> To <b>render</b> the (original) image in a graphic context </li> * <li> To report current image <b>dimension</b> parameters</li> * <li> To <b>read</b> a pixel knowing its location in the current image </li> * </ul> </p> * * <p>TODO: Rather than the custom grayfactor trick, consider using the standard * normalized form of ColorModel. * <p>TODO: When an alpha channel is involved, perform the alpha multiplication * if the components are not yet premultiplied. * * @author Hervé Bitteur * @author Brenton Partridge */ public class Picture implements PixelSource, EventSubscriber<LocationEvent> { //~ Static fields/initializers --------------------------------------------- /** Specific application parameters */ private static final Constants constants = new Constants(); /** Usual logger utility */ private static final Logger logger = LoggerFactory.getLogger(Picture.class); /** Identity transformation used for display */ private static final AffineTransform identity = new AffineTransform(); //~ Instance fields -------------------------------------------------------- // /** Dimension of current image. */ private Dimension dimension; /** Current image. */ private PlanarImage image; /** Service object where gray level of pixel is to be written to * when so asked for by the onEvent() method. */ private final SelectionService levelService; /** The image (read-only) raster. */ private Raster raster; /** The factor to apply to raw pixel value to get gray level on 0..255 */ private int grayFactor = 1; /** * The implicit (maximum) value for foreground pixels, as determined * by the picture itself, null if undetermined. */ private Integer implicitForeground; //~ Constructors ----------------------------------------------------------- // //---------// // Picture // //---------// /** * Build a picture instance from a given image. * * @param image the provided image * @param levelService service where pixel events are to be written * @throws ImageFormatException */ public Picture (RenderedImage image, SelectionService levelService) throws ImageFormatException { this.levelService = levelService; setImage(image); } //~ Methods ---------------------------------------------------------------- //--------// // invert // //--------// public static PlanarImage invert (RenderedImage image) { return JAI.create( "Invert", new ParameterBlock().addSource(image).add(null), null); } //----------// // getPixel // //----------// /** * Report the pixel element read at location (x, y) in the picture. * * @param x abscissa value * @param y ordinate value * @return the pixel value */ @Override public final int getPixel (int x, int y) { int[] pixel = raster.getPixel(x, y, (int[]) null); // Allocates pixel! if (grayFactor == 1) { // Speed up the normal case return pixel[0]; } else { return (grayFactor / 2) + (grayFactor * pixel[0]); } } // //-------// // close // //-------// /** * Release the resources linked to the picture image. */ public void close () { if (image != null) { image.dispose(); } } //---------------// // dumpRectangle // //---------------// /** * Debugging routine, that prints a basic representation of a * rectangular portion of the picture. * * @param title an optional title for this image dump * @param xMin x first coord * @param xMax x last coord * @param yMin y first coord * @param yMax y last coord */ public void dumpRectangle (String title, int xMin, int xMax, int yMin, int yMax) { StringBuilder sb = new StringBuilder(); sb.append(String.format("%n")); if (title != null) { sb.append(String.format("%s%n", title)); } // Abscissae sb.append(" "); for (int x = xMin; x <= xMax; x++) { sb.append(String.format("%4d", x)); } sb.append(String.format("%n +")); for (int x = xMin; x <= xMax; x++) { sb.append(" ---"); } sb.append(String.format("%n")); // Pixels for (int y = yMin; y <= yMax; y++) { sb.append(String.format("%4d", y)); sb.append("|"); for (int x = xMin; x <= xMax; x++) { int pix = getPixel(x, y); if (pix == 255) { sb.append(" ."); } else { sb.append(String.format("%4d", pix)); } } sb.append(String.format("%n")); } sb.append(String.format("%n")); logger.info(sb.toString()); } //--------------// // getDimension // //--------------// /** * Report (a copy of) the dimension in pixels of the current image. * * @return the image dimension */ public Dimension getDimension () { return new Dimension(dimension.width, dimension.height); } //-----------// // getHeight // //-----------// /** * Report the picture height in pixels. * * @return the height value */ @Override public int getHeight () { return dimension.height; } //----------// // getImage // //----------// /** * Report the underlying image. * * @return the image */ public RenderedImage getImage () { return image; } //-----------------------// // getImplicitForeground // //-----------------------// public Integer getImplicitForeground () { return implicitForeground; } //---------// // getName // //---------// /** * Report the name for this Observer. * * @return Observer name */ public String getName () { return "Picture"; } //----------// // getWidth // //----------// /** * Report the current width of the picture image. * Note that it may have been modified by a rotation. * * @return the current width value, in pixels. */ @Override public int getWidth () { return dimension.width; } //---------// // onEvent // //---------// /** * Call-back triggered when sheet location has been modified. * Based on sheet location, we forward the pixel gray level to whoever is * interested in it. * * @param event the (sheet) location event */ @Override public void onEvent (LocationEvent event) { try { // Ignore RELEASING if (event.movement == MouseMovement.RELEASING) { return; } Integer level = null; // Compute and forward pixel gray level Rectangle rect = event.getData(); if (rect != null) { Point pt = rect.getLocation(); // Check that we are not pointing outside the image if ((pt.x >= 0) && (pt.x < getWidth()) && (pt.y >= 0) && (pt.y < getHeight())) { level = Integer.valueOf(getPixel(pt.x, pt.y)); } } levelService.publish( new PixelLevelEvent(this, event.hint, event.movement, level)); } catch (Exception ex) { logger.warn(getClass().getName() + " onEvent error", ex); } } //--------// // render // //--------// /** * Paint the picture image in the provided graphic context. * * @param g the Graphics context */ public void render (Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.drawRenderedImage(image, identity); } //----------// // toString // //----------// @Override public String toString () { return getName(); } //------------// // RGBAToGray // //------------// private static PlanarImage RGBAToGray (PlanarImage image) { logger.info("Discarding alpha band ..."); PlanarImage pi = JAI.create("bandselect", image, new int[]{0, 1, 2}); return RGBToGray(pi); } //-----------// // RGBToGray // //-----------// private static PlanarImage RGBToGray (PlanarImage image) { logger.info("Converting RGB image to gray ..."); if (constants.useMaxChannelInColorToGray.isSet()) { // We use the max value among the RGB channels int width = image.getWidth(); int height = image.getHeight(); BufferedImage im = new BufferedImage( width, height, BufferedImage.TYPE_BYTE_GRAY); WritableRaster raster = im.getRaster(); Raster source = image.getData(); int[] levels = new int[3]; int maxLevel; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { source.getPixel(x, y, levels); maxLevel = 0; for (int level : levels) { if (maxLevel < level) { maxLevel = level; } } raster.setSample(x, y, 0, maxLevel); } } return PlanarImage.wrapRenderedImage(im); } else { // We use luminance value based on standard RGB combination double[][] matrix = { {0.114d, 0.587d, 0.299d, 0.0d} }; return JAI.create( "bandcombine", new ParameterBlock().addSource(image).add(matrix), null); } } //------------// // checkImage // //------------// private void checkImage () throws ImageFormatException { // Check that the whole image has been loaded if ((image.getWidth() == -1) || (image.getHeight() == -1)) { throw new RuntimeException("Unusable image for Picture"); } else { // Check & cache all parameters updateParams(); } } //------------------// // checkImageFormat // //------------------// /** * Check if the image format (and especially its color model) is * properly handled by Audiveris. * * @throws ImageFormatException is the format is not supported */ private void checkImageFormat () throws ImageFormatException { ColorModel colorModel = image.getColorModel(); int pixelSize = colorModel.getPixelSize(); boolean hasAlpha = colorModel.hasAlpha(); logger.debug("{}", colorModel); if (pixelSize == 1) { ///image = binaryToGray(image); // Only if rotation is needed! implicitForeground = 0; } // Check nb of bands SampleModel sampleModel = image.getSampleModel(); int numBands = sampleModel.getNumBands(); logger.debug("numBands={}", numBands); if (numBands == 1) { // Pixel gray value. Nothing to do } else if ((numBands == 2) && hasAlpha) { // Pixel + alpha // Discard alpha (TODO: check if premultiplied!!!) image = JAI.create("bandselect", image, new int[]{}); } else if ((numBands == 3) && !hasAlpha) { // RGB image = RGBToGray(image); } else if ((numBands == 4) && hasAlpha) { // RGB + alpha image = RGBAToGray(image); } else { throw new ImageFormatException( "Unsupported sample model numBands=" + numBands); } } //-------------// // printBounds // //-------------// private void printBounds () { logger.info( "minX:{} minY:{} maxX:{} maxY:{}", image.getMinX(), image.getMinY(), image.getMaxX(), image.getMaxY()); } //----------// // setImage // //----------// private void setImage (RenderedImage renderedImage) throws ImageFormatException { image = PlanarImage.wrapRenderedImage(renderedImage); checkImage(); } //--------------// // updateParams // //--------------// private void updateParams () throws ImageFormatException { checkImageFormat(); // Cache dimensions dimension = new Dimension(image.getWidth(), image.getHeight()); raster = Raster.createRaster( image.getData().getSampleModel(), image.getData().getDataBuffer(), null); logger.debug("raster={}", raster); // Check pixel size and compute grayFactor accordingly ColorModel colorModel = image.getColorModel(); int pixelSize = colorModel.getPixelSize(); logger.debug("colorModel={} pixelSize={}", colorModel, pixelSize); if (pixelSize == 1) { grayFactor = 1; } else if (pixelSize <= 8) { grayFactor = (int) Math.rint(128 / Math.pow(2, pixelSize - 1)); } else if (pixelSize <= 16) { grayFactor = (int) Math.rint(32768 / Math.pow(2, pixelSize - 1)); } else { throw new RuntimeException("Unsupported pixel size: " + pixelSize); } logger.debug("grayFactor={}", grayFactor); } //~ Inner Classes ---------------------------------------------------------- //-----------// // Constants // //-----------// private static final class Constants extends ConstantSet { //~ Instance fields ---------------------------------------------------- Constant.Boolean useMaxChannelInColorToGray = new Constant.Boolean( true, "Should we use max channel rather than standard luminance?"); } }