package mpicbg.trakem2.transform; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.PixelGrabber; import java.util.Arrays; import java.util.List; import ij.process.ByteProcessor; import ij.process.ColorProcessor; import ij.process.ImageProcessor; import ij.process.ShortProcessor; import ini.trakem2.display.MipMapImage; import ini.trakem2.display.Patch; import ini.trakem2.persistence.Loader; import mpicbg.models.CoordinateTransformMesh; import mpicbg.trakem2.transform.TransformMeshMappingWithMasks.ImageProcessorWithMasks; import mpicbg.trakem2.util.Pair; public class ExportARGB { static public final Pair< ColorProcessor, ByteProcessor > makeFlatImageARGB( final List<Patch> patches, final Rectangle roi, final double backgroundValue, final double scale, final boolean use_mipmaps) { return use_mipmaps ? makeFlatImageARGBFromMipMaps( patches, roi, backgroundValue, scale ) : makeFlatImageARGBFromOriginals( patches, roi, backgroundValue, scale ); } static public final int[] extractARGBIntArray( final Image img ) { final int[] pix = new int[img.getWidth(null) * img.getHeight(null)]; PixelGrabber pg = new PixelGrabber( img, 0, 0, img.getWidth(null), img.getHeight(null), pix, 0, img.getWidth(null) ); try { pg.grabPixels(); } catch (InterruptedException ie) {} return pix; } /** * * Returns nonsense or throws an Exception if mipmaps are not available. * Limited to 2GB arrays for the final image. * * @param patches * @param roi * @param backgroundValue * @param scale * @return */ static public final Pair< ColorProcessor, ByteProcessor > makeFlatImageARGBFromMipMaps( final List<Patch> patches, final Rectangle roi, final double backgroundValue, final double scale) { final int width = (int)(roi.width * scale); final int height = (int)(roi.height * scale); // Process the three channels separately in order to use proper alpha composition final ColorProcessor target = new ColorProcessor( width, height ); target.setInterpolationMethod( ImageProcessor.BILINEAR ); final ByteProcessor targetMask = new ByteProcessor( width, height ); targetMask.setInterpolationMethod( ImageProcessor.BILINEAR ); final Loader loader = patches.get(0).getProject().getLoader(); for (final Patch patch : patches) { // MipMap image, already including any coordinate transforms and the alpha mask (if any), by definition. final MipMapImage mipMap = loader.fetchImage(patch, scale); /// DEBUG: is there an alpha channel at all? //new ij.ImagePlus("alpha of " + patch.getTitle(), new ByteProcessor( mipMap.image.getWidth(null), mipMap.image.getHeight(null), new ColorProcessor( mipMap.image ).getChannel( 4 ))).show(); // Yes, there is, even though the mipmap images have the alpha pre-multiplied // Work-around strange bug that makes mipmap-loaded images paint with 7-bit depth instead of 8-bit depth final BufferedImage bi = new BufferedImage(mipMap.image.getWidth(null), mipMap.image.getHeight(null), BufferedImage.TYPE_INT_ARGB); final Graphics2D g2d = bi.createGraphics(); g2d.drawImage(mipMap.image, 0, 0, null); g2d.dispose(); final int[] pix = extractARGBIntArray(bi); bi.flush(); // DEBUG: does the BufferedImage have the alpha channel? //{ // final byte[] aa = new byte[pix.length]; // for (int i=0; i<aa.length; ++i) aa[i] = (byte)((pix[i] & 0xff000000) >> 24); // new ij.ImagePlus("alpha of BI of " + patch.getTitle(), new ByteProcessor(bi.getWidth(), bi.getHeight(), aa)).show(); //} // YES: the alpha, containing the outside too. All fine. final ByteProcessor alpha; final ColorProcessor rgb = new ColorProcessor( bi.getWidth(), bi.getHeight(), pix ); if ( patch.hasAlphaChannel() ) { // The mipMap has the alpha channel in it, even if the alpha is pre-multiplied as well onto the images. final byte[] a = new byte[pix.length]; for (int i=0; i<a.length; ++i) { a[i] = (byte )((pix[i] & 0xff000000) >> 24); } alpha = new ByteProcessor(bi.getWidth(), bi.getHeight(), a); } else { alpha = new ByteProcessor( bi.getWidth(), bi.getHeight() ); Arrays.fill( (byte[]) alpha.getPixels(), (byte)255 ); } // The affine to apply to the MipMap.image final AffineTransform atc = new AffineTransform(); atc.scale( scale, scale ); atc.translate( -roi.x, -roi.y ); final AffineTransform at = new AffineTransform(); at.preConcatenate( atc ); at.concatenate( patch.getAffineTransform() ); at.scale( mipMap.scaleX, mipMap.scaleY ); final AffineModel2D aff = new AffineModel2D(); aff.set( at ); final CoordinateTransformMesh mesh = new CoordinateTransformMesh( aff, patch.getMeshResolution(), bi.getWidth(), bi.getHeight() ); final TransformMeshMappingWithMasks< CoordinateTransformMesh > mapping = new TransformMeshMappingWithMasks< CoordinateTransformMesh >( mesh ); alpha.setInterpolationMethod( ImageProcessor.NEAREST_NEIGHBOR ); // no interpolation rgb.setInterpolationMethod( ImageProcessor.BILINEAR ); mapping.map(rgb, alpha, target, targetMask); } return new Pair< ColorProcessor, ByteProcessor >( target, targetMask ); } /** * Limited to 2GB arrays for the requested image. * * @param patches * @param roi * @param backgroundValue * @param scale * @return */ static public final Pair< ColorProcessor, ByteProcessor > makeFlatImageARGBFromOriginals( final List<Patch> patches, final Rectangle roi, final double backgroundValue, final double scale) { final ColorProcessor target = new ColorProcessor((int)(roi.width * scale), (int)(roi.height * scale)); target.setInterpolationMethod( ImageProcessor.BILINEAR ); final ByteProcessor targetMask = new ByteProcessor( target.getWidth(), target.getHeight() ); targetMask.setInterpolationMethod( ImageProcessor.BILINEAR ); for (final Patch patch : patches) { final Patch.PatchImage pai = patch.createTransformedImage(); final ColorProcessor fp = (ColorProcessor) pai.target.convertToRGB(); final ByteProcessor alpha; System.out.println("IMAGE:" + patch.getTitle()); System.out.println("mask: " + pai.mask); System.out.println("outside: " + pai.outside); if ( null == pai.mask ) { if ( null == pai.outside ) { alpha = new ByteProcessor( fp.getWidth(), fp.getHeight() ); Arrays.fill( ( byte[] )alpha.getPixels(), (byte)255 ); // fully opaque } else { alpha = pai.outside; } } else { alpha = pai.mask; } // The affine to apply final AffineTransform atc = new AffineTransform(); atc.scale( scale, scale ); atc.translate( -roi.x, -roi.y ); final AffineTransform at = new AffineTransform(); at.preConcatenate( atc ); at.concatenate( patch.getAffineTransform() ); final AffineModel2D aff = new AffineModel2D(); aff.set( at ); final CoordinateTransformMesh mesh = new CoordinateTransformMesh( aff, patch.getMeshResolution(), fp.getWidth(), fp.getHeight() ); final TransformMeshMappingWithMasks< CoordinateTransformMesh > mapping = new TransformMeshMappingWithMasks< CoordinateTransformMesh >( mesh ); fp.setInterpolationMethod( ImageProcessor.BILINEAR ); alpha.setInterpolationMethod( ImageProcessor.BILINEAR ); mapping.map( fp, alpha, target, targetMask ); } return new Pair< ColorProcessor, ByteProcessor >( target, targetMask ); } }