【问题标题】:How to combine multiple PNGs into one big PNG file?如何将多个PNG组合成一个大的PNG文件?
【发布时间】:2011-04-24 18:00:15
【问题描述】:

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

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

(目的是在纸上打印,所以不能使用一些网络技术,只有一个单一的图片文件可以消除许多使用错误。)

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

【问题讨论】:

  • 你想要一个大的PNG?那将是1536000x1536000像素?我必须说一个基本的图像库在这里会是一个更好的选择。
  • 即使有办法绕过重新编码,重新编码的非常大的图像可能会压缩得更好,尤其是在图像相似的情况下。
  • @kyndigs:更像 15360 x 25600(对于 60 x 100 排列)
  • 无论如何,一个基本的图像库会是一个更好的选择:p
  • ...和一台 300dpi 的 DinA0 打印机,这正是我想要的。

标签: java image scala image-processing png


【解决方案1】:

创建一个您将写入的大图像。根据您想要的行数和列数计算其尺寸。

    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"));

【讨论】:

  • 天哪,这比尝试使用 JAI 和马赛克描述符要容易得多。谢谢!
【解决方案2】:

前段时间我有一些类似的需求(巨大的图像 - 而且,我的情况是 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");
        }
}

【讨论】:

  • 这很有趣,但是这里的代码与当前版本的库相比已经过时了。你有没有类似的例子是当前的?非常感谢。
  • @MattFriedman 你试过“片段”中发布的那个吗? code.google.com/p/pngj/wiki/Snippets
  • 您提供的链接上发布的代码已编译。非常感谢。也许你想继续用新的东西替换上面的代码。只是一个想法。再次感谢。
【解决方案3】:

我看不出“没有处理和重新编码”怎么可能。如果你坚持使用 Java,那么我只建议你使用JAI(项目页面here)。这样你就可以create one big BufferedImageload smaller imagesdraw them on the bigger one

或者直接使用ImageMagickmontage:

montage *.png output.png

有关montage 的更多信息,请参阅usage

【讨论】:

    【解决方案4】:

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

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

    但仅使用 ImageMagick、netpbm 或类似工具的命令行实用程序将为您节省大量开发时间,而收益可能微乎其微。

    【讨论】:

      【解决方案5】:

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

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

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

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

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

      【讨论】:

        【解决方案6】:

        您最好使用另一种(无损)图像格式来实现。 PPM 非常易于使用(并以编程方式放入磁贴;它只是磁盘上的一个大数组,因此您最多只需要存储一行磁贴),但它非常浪费空间(每个 12 字节像素!)。

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

        【讨论】:

          【解决方案7】:

          用于将图块拼接成一张大图的简单 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】:

            组合图片

            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);
            
            }
            

            【讨论】:

              【解决方案9】:

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

              它并不能完全解决 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");
                      }
                  }
              }
              

              【讨论】:

                【解决方案10】:

                像这样使用 imagemagick 的蒙太奇:

                montage *.png montage.png
                

                您可以找到更多关于参数here的信息

                祝你好运

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-09-22
                  • 2015-02-14
                  • 2011-06-16
                  • 2015-07-20
                  • 1970-01-01
                  • 2010-12-26
                  相关资源
                  最近更新 更多