如何将多个PNG组合成一个大的PNG文件?

时间:2010-10-13 09:25:25

标签: java image scala image-processing png

我有约。 6000个PNG文件(256 * 256像素),并希望将它们组合成一个以编程方式保存所有这些文件的大PNG。

最好/最快的方法是什么?

(目的是在纸上打印,因此使用某些网络技术不是一种选择,只有一个单个图片文件将消除许多使用错误。)

我尝试了fahd的建议,但当我尝试创建宽度为24576像素,高度为15360像素的NullPointerException时,我得到BufferedImage。有什么想法吗?

10 个答案:

答案 0 :(得分:53)

创建一个您要写入的大图像。根据您想要的行数和列数来计算其维度。

    BufferedImage result = new BufferedImage(
                               width, height, //work these out
                               BufferedImage.TYPE_INT_RGB);
    Graphics g = result.getGraphics();

现在遍历你的图像并绘制它们:

    for(String image : images){
        BufferedImage bi = ImageIO.read(new File(image));
        g.drawImage(bi, x, y, null);
        x += 256;
        if(x > result.getWidth()){
            x = 0;
            y += bi.getHeight();
        }
    }

最后把它写到文件:

    ImageIO.write(result,"png",new File("result.png"));

答案 1 :(得分:7)

前段时间我有一些相似的需求(巨大的图像 - 而且,我的情况是16位深度 - 将它们完全放在内存中不是一种选择)。我结束了PNG库的编码,以顺序方式进行读/写。如果有人发现它有用,那就是here

更新:这是一个示例代码:

/**
 * Takes several tiles and join them in a single image
 * 
 * @param tiles            Filenames of PNG files to tile
 * @param dest            Destination PNG filename
 * @param nTilesX            How many tiles per row?
 */
public class SampleTileImage {

        public static void doTiling(String tiles[], String dest, int nTilesX) {
                int ntiles = tiles.length;
                int nTilesY = (ntiles + nTilesX - 1) / nTilesX; // integer ceil
                ImageInfo imi1, imi2; // 1:small tile   2:big image
                PngReader pngr = new PngReader(new File(tiles[0]));
                imi1 = pngr.imgInfo;
                PngReader[] readers = new PngReader[nTilesX];
                imi2 = new ImageInfo(imi1.cols * nTilesX, imi1.rows * nTilesY, imi1.bitDepth, imi1.alpha, imi1.greyscale,
                                imi1.indexed);
                PngWriter pngw = new PngWriter(new File(dest), imi2, true);
                // copy palette and transparency if necessary (more chunks?)
                pngw.copyChunksFrom(pngr.getChunksList(), ChunkCopyBehaviour.COPY_PALETTE
                                | ChunkCopyBehaviour.COPY_TRANSPARENCY);
                pngr.readSkippingAllRows(); // reads only metadata             
                pngr.end(); // close, we'll reopen it again soon
                ImageLineInt line2 = new ImageLineInt(imi2);
                int row2 = 0;
                for (int ty = 0; ty < nTilesY; ty++) {
                        int nTilesXcur = ty < nTilesY - 1 ? nTilesX : ntiles - (nTilesY - 1) * nTilesX;
                        Arrays.fill(line2.getScanline(), 0);
                        for (int tx = 0; tx < nTilesXcur; tx++) { // open several readers
                                readers[tx] = new PngReader(new File(tiles[tx + ty * nTilesX]));
                                readers[tx].setChunkLoadBehaviour(ChunkLoadBehaviour.LOAD_CHUNK_NEVER);
                                if (!readers[tx].imgInfo.equals(imi1))
                                        throw new RuntimeException("different tile ? " + readers[tx].imgInfo);
                        }
                        for (int row1 = 0; row1 < imi1.rows; row1++, row2++) {
                                for (int tx = 0; tx < nTilesXcur; tx++) {
                                        ImageLineInt line1 = (ImageLineInt) readers[tx].readRow(row1); // read line
                                        System.arraycopy(line1.getScanline(), 0, line2.getScanline(), line1.getScanline().length * tx,
                                                        line1.getScanline().length);
                                }
                                pngw.writeRow(line2, row2); // write to full image
                        }
                        for (int tx = 0; tx < nTilesXcur; tx++)
                                readers[tx].end(); // close readers
                }
                pngw.end(); // close writer
        }

        public static void main(String[] args) {
                doTiling(new String[] { "t1.png", "t2.png", "t3.png", "t4.png", "t5.png", "t6.png" }, "tiled.png", 2);
                System.out.println("done");
        }
}

答案 2 :(得分:6)

我没有看到“如果没有处理和重新编码”,它将如何成为可能。如果您坚持使用Java,那么我建议您使用JAI(项目页面here)。有了这个,你会create one big BufferedImageload smaller imagesdraw them on the bigger one

或者只使用ImageMagick montage

montage *.png output.png

有关montage的详情,请参阅usage

答案 3 :(得分:3)

PNG格式不支持平铺,因此您无法逃脱至少解压缩和重新压缩数据流。如果所有图像的调色板都相同(或全部不存在),那么这是您真正需要做的唯一事情。 (我也假设图像没有隔行扫描。)

您可以以流式方式执行此操作,一次只打开一个“行”PNG,从其数据流中读取适当大小的块并将它们写入输出流。这样您就不需要将整个图像保存在内存中。最有效的方法是自己编写libpng。由于像素预测,您可能需要在内存中保留略多于一个像素的扫描线。

但是只使用ImageMagick的命令行实用程序,netpbm或类似功能可以为您节省大量的开发时间。

答案 4 :(得分:3)

正如其他人所指出的那样,使用Java并不一定是最好的选择。

如果您要使用Java,最好的选择 - 假设您的内存足够短,以至于您无法将整个数据集多次读入内存然后再将其写出来 - 就是实现RenderedImage有一个类,可以根据需要从磁盘读取PNG。如果您只是创建自己的新BufferedImage然后尝试将其写出来,PNG编写器将创建一个额外的数据副本。如果您创建自己的RenderedImage,则可以将其传递给ImageIO.write(myImageSet,"png",myFileName)。您可以复制第一个PNG中的SampleModelColorModel信息 - 希望它们完全相同。

如果您假装整个图像是多个图块(每个源图像一个图块),那么ImageIO.write将创建一个WritableRaster,它是整个图像数据集的大小,并将调用您的实施RenderedImage.copyData以填充数据。如果你有足够的内存,这是一个简单的方法(因为你获得了一个庞大的目标数据集,可以使用setRect(dx,dy,Raster)方法将所有图像数据转储到其中 - 然后不要不得不再担心它。我没有测试过这是否可以节省内存,但在我看来应该这样做。

或者,如果您假装整个图像是单个图块,ImageIO.write将使用getTile(0,0)询问与整个图像对应的光栅。因此,您必须创建自己的Raster,这反过来会让您创建自己的DataBuffer。当我尝试这种方法时,成功写入15360x25600 RGB PNG的最小内存使用量为-Xmx1700M(在Scala中,顺便提一下),每个像素的写入图像仅略高于4个字节,因此一个上面的开销非常小记忆中的完整图像。

PNG数据格式本身并不需要内存中的整个图像 - 它可以在块中正常工作 - 但遗憾的是,PNG编写器的默认实现假定它将整个像素阵列存储在内存中。

答案 5 :(得分:2)

梳理图片

private static void combineALLImages(String screenNames, int screens) throws IOException, InterruptedException {
    System.out.println("screenNames --> D:\\screenshots\\screen   screens --> 0,1,2 to 10/..");
    int rows = screens + 1;
    int cols = 1;
    int chunks = rows * cols ; 

     File[] imgFiles = new File[chunks];
    String files = "";
    for (int i = 0; i < chunks; i++) {
        files = screenNames + i + ".jpg";
        imgFiles[i] = new File(files);          
        System.out.println(screenNames + i + ".jpg"+"\t Screens : "+screens);    

    }

    BufferedImage sample = ImageIO.read(imgFiles[0]);
    //Initializing the final image
    BufferedImage finalImg = new BufferedImage(sample.getWidth() * cols, sample.getHeight() * rows, sample.getType());

    int index = 0;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            BufferedImage temp = ImageIO.read(imgFiles[index]);
            finalImg.createGraphics().drawImage(temp, sample.getWidth() * j, sample.getHeight() * i, null);
            System.out.println(screenNames + index + ".jpg");
            index++;
        }
    }
    File final_Image = new File("D:\\Screenshots\\FinalImage.jpg");
    ImageIO.write(finalImg, "jpeg", final_Image);

}

答案 6 :(得分:1)

你可能最好从另一种(无损)图像格式中剔除东西。 PPM很容易使用(并且以编程方式放置磁贴;它只是磁盘上的一个大数组,所以你最多只需要存储一行磁贴),但是它非常浪费空间(12每个像素的字节数!)。

然后使用标准转换器(例如ppm2png),它采用中间格式并将其转换为巨型PNG。

答案 7 :(得分:1)

将瓷砖连接成一个大图像的简单python脚本:

import Image

TILESIZE = 256
ZOOM = 15
def merge_images( xmin, xmax, ymin, ymax, output) :
    out = Image.new( 'RGB', ((xmax-xmin+1) * TILESIZE, (ymax-ymin+1) * TILESIZE) ) 

    imx = 0;
    for x in range(xmin, xmax+1) :
        imy = 0
        for y in range(ymin, ymax+1) :
            tile = Image.open( "%s_%s_%s.png" % (ZOOM, x, y) )
            out.paste( tile, (imx, imy) )
            imy += TILESIZE
        imx += TILESIZE

    out.save( output )

运行:

merge_images(18188, 18207, 11097, 11111, "output.png")

适用于名为%ZOOM_%XCORD_%YCORD.png的文件,例如15_18188_11097.png

答案 8 :(得分:1)

像这样使用imagemagick的蒙太奇:

montage *.png montage.png

您可以找到有关参数here

的更多信息 祝你好运

答案 9 :(得分:1)

我一直回到这个问题,因为我有一个类似的问题,并在另一个线程中找到了一个可接受的解决方案,我将在此处链接以供将来参考。

它并不能完全解决 OP 问题,但它确实允许将水平切片(“平铺线”)拼接在一起,而无需使用 AWT API 同时将所有内容加载到内存中。

Merge small images into one without allocating full image in memory

链接的存储库不再可用,但有 mirrors 可用。

/*******************************************************************************
 * Copyright (c) MOBAC developers
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package mobac.utilities.imageio;

/*
 * PNGWriter.java
 *
 * Copyright (c) 2007 Matthias Mann - www.matthiasmann.de
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

import static mobac.utilities.imageio.PngConstants.*;

import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import javax.activation.UnsupportedDataTypeException;

/**
 * A PNG writer that is able to write extra large PNG images using incremental
 * writing.
 * <p>
 * The image is processed incremental in "tile lines" - e.g. an PNG image of
 * 30000 x 20000 pixels (width x height) can be written by 200 "tile lines" of
 * size 30000 x 100 pixels. Each tile line can be written via the method
 * {@link #writeTileLine(BufferedImage)}. After writing the last line you have
 * to call {@link #finish()} which will write the final PNG structure
 * information into the {@link OutputStream}.
 * </p>
 * <p>
 * Please note that this writer creates 24bit/truecolor PNGs. Transparency and
 * alpha masks are not supported.
 * </p>
 * Bases on the PNGWriter written by Matthias Mann - www.matthiasmann.de
 * 
 * @author r_x
 */
public class PngXxlWriter {

    private static final int BUFFER_SIZE = 128 * 1024;

    private int width;
    private int height;
    private DataOutputStream dos;

    ImageDataChunkWriter imageDataChunkWriter;

    /**
     * Creates an PNG writer instance for an image with the specified width and
     * height.
     * 
     * @param width
     *            width of the PNG image to be written
     * @param height
     *            height of the PNG image to be written
     * @param os
     *            destination to write the PNG image data to
     * @throws IOException
     */
    public PngXxlWriter(int width, int height, OutputStream os) throws IOException {
        this.width = width;
        this.height = height;
        this.dos = new DataOutputStream(os);

        dos.write(SIGNATURE);

        PngChunk cIHDR = new PngChunk(IHDR);
        cIHDR.writeInt(this.width);
        cIHDR.writeInt(this.height);
        cIHDR.writeByte(8); // 8 bit per component
        cIHDR.writeByte(COLOR_TRUECOLOR);
        cIHDR.writeByte(COMPRESSION_DEFLATE);
        cIHDR.writeByte(FILTER_SET_1);
        cIHDR.writeByte(INTERLACE_NONE);
        cIHDR.writeTo(dos);
        imageDataChunkWriter = new ImageDataChunkWriter(dos);
    }

    /**
     * 
     * @param tileLineImage
     * @throws IOException
     */
    public void writeTileLine(BufferedImage tileLineImage) throws IOException {

        int tileLineHeight = tileLineImage.getHeight();
        int tileLineWidth = tileLineImage.getWidth();

        if (width != tileLineWidth)
            throw new RuntimeException("Invalid width");

        ColorModel cm = tileLineImage.getColorModel();

        if (!(cm instanceof DirectColorModel))
            throw new UnsupportedDataTypeException(
                    "Image uses wrong color model. Only DirectColorModel is supported!");

        // We process the image line by line, from head to bottom
        Rectangle rect = new Rectangle(0, 0, tileLineWidth, 1);

        DataOutputStream imageDataStream = imageDataChunkWriter.getStream();

        byte[] curLine = new byte[width * 3];
        for (int line = 0; line < tileLineHeight; line++) {
            rect.y = line;
            DataBuffer db = tileLineImage.getData(rect).getDataBuffer();
            if (db.getNumBanks() > 1)
                throw new UnsupportedDataTypeException("Image data has more than one data bank");
            if (db instanceof DataBufferByte)
                curLine = ((DataBufferByte) db).getData();
            else if (db instanceof DataBufferInt) {
                int[] intLine = ((DataBufferInt) db).getData();
                int c = 0;
                for (int i = 0; i < intLine.length; i++) {
                    int pixel = intLine[i];
                    curLine[c++] = (byte) (pixel >> 16 & 0xFF);
                    curLine[c++] = (byte) (pixel >> 8 & 0xFF);
                    curLine[c++] = (byte) (pixel & 0xFF);
                }
            } else
                throw new UnsupportedDataTypeException(db.getClass().getName());

            imageDataStream.write(FILTER_TYPE_NONE);
            imageDataStream.write(curLine);
        }
    }

    public void finish() throws IOException {
        imageDataChunkWriter.finish();
        PngChunk cIEND = new PngChunk(IEND);
        cIEND.writeTo(dos);
        cIEND.close();
        dos.flush();
    }

    static class ImageDataChunkWriter extends OutputStream {

        DeflaterOutputStream dfos;
        DataOutputStream stream;
        DataOutputStream out;
        CRC32 crc = new CRC32();

        public ImageDataChunkWriter(DataOutputStream out) throws IOException {
            this.out = out;
            dfos = new DeflaterOutputStream(new BufferedOutputStream(this, BUFFER_SIZE),
                    new Deflater(Deflater.BEST_COMPRESSION));
            stream = new DataOutputStream(dfos);
        }

        public DataOutputStream getStream() {
            return stream;
        }

        public void finish() throws IOException {
            stream.flush();
            stream.close();
            dfos.finish();
            dfos = null;
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            crc.reset();
            out.writeInt(len);
            out.writeInt(IDAT);
            out.write(b, off, len);
            crc.update("IDAT".getBytes());
            crc.update(b, off, len);
            out.writeInt((int) crc.getValue());
        }

        @Override
        public void write(byte[] b) throws IOException {
            write(b, 0, b.length);
        }

        @Override
        public void write(int b) throws IOException {
            throw new IOException("Simgle byte writing not supported");
        }
    }
}