070324_tilesaver.gif

High-resolution images from Processing using tiling (1.6% / 20%)

A while back I posted about rendering very high-resolution images from Processing using a tiling technique. I had implemented a working version of a solution first described by "surelyyoujest", but didn’t have time to post a clean version of the code. (I did however post ImageStitcher.pde, which is code for stitching these tiled images back together.) But after a long delay, here is finally the more useful bit of code.

The code works by panning the camera over the original viewport area, subdividing the image into tiles. This way, OpenGL’s limitations on maximum resolution can be circumvented. As long as enough memory is allocated, the images created can be very big indeed. The images shown above are from a 15360 x 15360 pixel image, shown at 1.6% and 20% respectively. With 1.5 GB assigned to Java I have so far successfully saved 20k x 20k images. That’s large enough to print 2×2 meter prints at 260 DPI.

What follows is the aTileSaver class and a simple demo application. I will post a more complex example soon.

Update: I’ve changed a few minor details in the code. Make note of the version number if you have download it already. This code is likely to change..

Source code - aTileSaver.pde

The aTileSaver class is fairly easy to use. Drop it into any Processing sketch, add a trigger to start the tiling process and call Tilesaver.pre() and Tilesaver.post() at the beginning and end of draw(). The only thing to be aware of is that the shapes drawn should not change in any way while the tiling is in progress, otherwise the results will be mangled.

// aTileSaver.pde - v0.12 2007.0326
// Marius Watz - http://workshop.evolutionzone.com
//
// Class for rendering high-resolution images by splitting them into
// tiles using the viewport.
//
// Builds heavily on solution by "surelyyoujest":
// http://processing.org/discourse/yabb_beta/YaBB.cgi?
// board=OpenGL;action=display;num=1159148942

class aTileSaver {
  public boolean isTiling=false,done=true;
  public boolean doSavePreview=true;

  PApplet p;
  float FOV=60; // initial field of view
  float cameraZ, width, height;
  int tileNum=10,tileNumSq; // number of tiles
  int tileImgCnt, tileX, tileY, tilePad;
  boolean firstFrame=false, secondFrame=false;
  String tileFilename,tileFileExt=".tga";
  PImage tileImg;
  float perc,percMilestone;

// The constructor takes a PApplet reference to your sketch.
  public aTileSaver(PApplet _p) {
    p=_p;
  }

// If init() is called without specifying number of tiles, getMaxTiles()
// will be called to estimate number of tiles according to free memory.
  public void init(String _filename) {
    init(_filename,getMaxTiles(p.width));
  }

// Initialize using a filename to output to and number of tiles to use.
  public void init(String _filename,int _num) {
    tileFilename=_filename;
    tileNum=_num;
    tileNumSq=(tileNum*tileNum);

    width=p.width;
    height=p.height;
    cameraZ=(height/2.0f)/p.tan(p.PI*FOV/360.0f);
    p.println("aTileSaver: "+tileNum+" tilesnResolution: "+
      (p.width*tileNum)+"x"+(p.height*tileNum));

    // remove extension from filename
    if(!new java.io.File(tileFilename).isAbsolute())
      tileFilename=p.dataPath(tileFilename);
    tileFilename=noExt(tileFilename);
    p.createPath(tileFilename);

    // save preview
    if(doSavePreview) p.g.save(tileFilename+"_preview.png");

    // set up off-screen buffer for saving tiled images
    tileImg=new PImage(p.width*tileNum, p.height*tileNum);

    // start tiling
    done=false;
    isTiling=false;
    perc=0;
    percMilestone=0;
    tileInc();
  }

  // set filetype, default is TGA. pass a valid image extension as parameter.
  public void setSaveType(String extension) {
    tileFileExt=extension;
    if(tileFileExt.indexOf(".")==-1) tileFileExt="."+tileFileExt;
  }

  // pre() handles initialization of each frame.
  // It should be called in draw() before any drawing occurs.
  public void pre() {
    if(!isTiling) return;
    if(firstFrame) firstFrame=false;
    else if(secondFrame) {
      secondFrame=false;
      tileInc();
    }
    setupCamera();
  }

  // post() handles tile update and image saving.
  // It should be called at the very end of draw(), after any drawing.
  public void post() {
    // If first or second frame, don't update or save.
    if(firstFrame||secondFrame|| (!isTiling)) return;

    // Find image ID from reverse row order
    int imgid=tileImgCnt%tileNum+
	  (tileNum-tileImgCnt/tileNum-1)*tileNum;
    int idx=(imgid%tileNum);
    int idy=(imgid/tileNum);

    // Get current image from sketch and draw it into buffer
    p.loadPixels();
    tileImg.set(idx*p.width, idy*p.height, p.g);

    // Increment tile index
    tileImgCnt++;
    perc=100*((float)tileImgCnt/(float)tileNumSq);
    if(perc-percMilestone> 5 || perc>99) {
      p.println(p.nf(perc,3,2)+"% completed. "+tileImgCnt+
	    "/"+tileNumSq+" images saved.");
      percMilestone=perc;
    }

    if(tileImgCnt==tileNumSq) tileFinish();
    else tileInc();
  }

  public boolean checkStatus() {
    return isTiling;
  }

  // tileFinish() handles saving of the tiled image
  public void tileFinish() {
    isTiling=false;

    restoreCamera();

    // save large image to TGA
    tileFilename+="_"+(p.width*tileNum)+"x"+
            (p.height*tileNum)+tileFileExt;
    p.println("Save: "+
      tileFilename.substring(
        tileFilename.lastIndexOf(java.io.File.separator)+1));
    tileImg.save(tileFilename);
    p.println("Done tiling.n");

    // clear buffer for garbage collection
    tileImg=null;
    done=true;
  }

  // Increment tile coordinates
  public void tileInc() {
    if(!isTiling) {
      isTiling=true;
      firstFrame=true;
      secondFrame=true;
      tileImgCnt=0;
    } else {
      if(tileX==tileNum-1) {
        tileX=0;
        tileY=(tileY+1)%tileNum;
      } else
        tileX++;
    }
  }

  // set up camera correctly for the current tile
  public void setupCamera() {
    p.camera(width/2.0f, height/2.0f, cameraZ,
      width/2.0f, height/2.0f, 0, 0, 1, 0);

    if(isTiling) {
      float mod=1f/10f;
      p.frustum(width*((float)tileX/(float)tileNum-.5f)*mod,
        width*((tileX+1)/(float)tileNum-.5f)*mod,
        height*((float)tileY/(float)tileNum-.5f)*mod,
        height*((tileY+1)/(float)tileNum-.5f)*mod,
        cameraZ*mod, 10000);
    }

  }

  // restore camera once tiling is done
  public void restoreCamera() {
    float mod=1f/10f;
    p.camera(width/2.0f, height/2.0f, cameraZ,
      width/2.0f, height/2.0f, 0, 0, 1, 0);
    p.frustum(-(width/2)*mod, (width/2)*mod,
      -(height/2)*mod, (height/2)*mod,
      cameraZ*mod, 10000);
  }

  // checks free memory and gives a suggestion for maximum tile
  // resolution. It should work well in most cases, I've been able
  // to generate 20k x 20k pixel images with 1.5 GB RAM allocated.
  public int getMaxTiles(int width) {

    // get an instance of java.lang.Runtime, force garbage collection
    java.lang.Runtime runtime=java.lang.Runtime.getRuntime();
    runtime.gc();

    // calculate free memory for ARGB (4 byte) data, giving some slack
    // to out of memory crashes.
    int num=(int)(Math.sqrt(
      (float)(runtime.freeMemory()/4)*0.925f))/width;
    p.println(((float)runtime.freeMemory()/(1024*1024))+"/"+
      ((float)runtime.totalMemory()/(1024*1024)));

    // warn if low memory
    if(num==1) {
      p.println("Memory is low. Consider increasing memory.");
      num=2;
    }

    return num;
  }

  // strip extension from filename
  String noExt(String name) {
    int last=name.lastIndexOf(".");
    if(last>0)
      return name.substring(0, last);

    return name;
  }
}
Source code - aTileSaverSimpleTest.pde
// aTileSaverSimpleTest.pde - v0.1 2007.0325
// Marius Watz - http://workshop.evolutionzone.com
//
// Basic demo of the aTileSaver class.

import processing.opengl.*;

aTileSaver tiler;

void setup() {
  size(500,500, OPENGL);
  noStroke();
  tiler=new aTileSaver(this);
}

public void draw() {
  if(tiler==null) return; // Not initialized

  // call aTileSaver.pre() to prepare frame and setup camera if it exists.
  tiler.pre();

  smooth();
  background(0,50,0);
  lights();

  translate(width/2,height/2);
  rotateY(PI/4);
  rotateX(PI/4);
  scale(8);
  fill(240,255,0,220);
  box(10,50,10);
  fill(150,255,0,220);
  box(50,10,10);
  fill(255,150,0,220);
  box(10,10,50);

  // call aTileSaver.post() to update tiles if tiler is active
  tiler.post();
}

// Saves tiled imaged when 't' is pressed
public void keyPressed() {
  if(key=='t') tiler.init("Simple"+nf(frameCount,5),5);
}

There are 25 comments to "Code: aTileSaver.pde". You may leave your own comment.
1. lenny, March 24th, 2007 at 14:31

yeah. i’ve been waiting for this…. thanks marius. this is great. the plotter will get hot.

2. ez, March 26th, 2007 at 23:13

Hi,
I just tried adding the Tiler to one of my sketches, it’s working quite well, but I think my shapes are changing during the tiling operation because they are not perfectly aligned.
I saw “The only thing to be aware of is that the shapes drawn should not change in any way while the tiling is in progress, otherwise the results will be mangled.” but I am not sure when exactly the tiling is in progress. Do you mean that no changes should occur between pre() and post()?

3. ez, March 26th, 2007 at 23:38

Me again,
I just realized that it also does not seem to work when I don’t call background(…) at the beginning of draw(). I end up with the same image tiled over and over again instead of one large image.
I could send you the application I am using if you want.

4. marius watz, March 27th, 2007 at 14:40

Hi Ez,

You have two issues:

1. Sounds like you are changing the model or the viewport while the tiling is happening. The tiling will take as many frames as there are tiles. You can always use tiler.checkStatus() to check if tiling is in progress, it returns true if that is the case.

2. Tiling won’t work for graphics that are created by “trails”, i.e. by not calling background() and thereby overdrawing what is already on the screen. For tiling to work properly you must be able to draw the complete model for every frame.

5. christianmeinke.com » Blog Archive » TileSaver Class For Processing, April 1st, 2007 at 18:26

[...] Rendering high-resolution images from inside Processing using a tiling technique [...]

6. ez, April 3rd, 2007 at 21:10

Hi Marius,
Thanks for the response.
1. I was able to fix this by wrapping all the draw() code inside an “if (!tiler.checkStatus()) { … }” statement and it works properly now.
2. That’s too bad, I guess my other option would be to export to a vector image…

7. victor, May 1st, 2007 at 15:35

a trick to solve the 2nd point would be to save the frame into a image and then display that image on a window with the same size. here you will be able to use background() and tile that screenshot into a much larger image..

8. victor, May 1st, 2007 at 21:22

ofcourse it wouldn’t work for obvious reason.. stupid me

9. christianmeinke.com » Blog Archive » Textured Surfaces, June 12th, 2007 at 14:05

[...] Since yesterday I’m testing the texture implementation of Andreas’ surface library (thanks for sending the early version) in combination with various blending modes (already documented and described by Robert Hodgin a few weeks ago - Mini-tutorial: Additive Blending pt. 1 / pt. 2). After increasing the Eclipse startup memory settings, I was able to load even highres textures and to write highres stills using the TileSaver class by Marius once again. [...]

10. lenny, July 9th, 2007 at 05:31

hi marius
i need to export huge images for print. therefore i need it at 300dpi.
your code generates huge images at 72 dpi. i then use photoshop to change the resolution to 300 dpi. i’d like to get rid of that additional step and i wonder if it’s possible to do that directly in processing. so it saves huge stuff at 300dpi. the best thing would be an additional parameter for dpi in the init-function. but i guess if it was that easy you would already have done this…
???

11. marius watz, July 9th, 2007 at 07:47

Sorry, I have no idea how to add DPI meta information in the images. If anyone has a suggestion I’d be happy to try to add it. You could always make a batch action in Photoshop, that would take care of it.

I did consider outputting PSD directly, but couldn’t find any code for it. Anyone know about how to do it?

12. sajid, July 18th, 2007 at 13:26

Hi,
I am also faced with a similar problem as EZ.

I am creating trailing graphics (overdrawing layers on top of one another).
Have you been able to find a solution to export high res images for sketches drawn using this technique?

Any help or suggestion is greatly valued :)

Thanks

13. marius watz, July 19th, 2007 at 06:17

Sajid, the only way to do use TileSaver with trails is to store all the drawing commands in memory. You’d have to write a custom mechanism for remembering all commands related to the drawing, then replay them while using the TileSaver script.

14. lenny, July 27th, 2007 at 03:35

hi marius
the dpi-thing again.
i found this on the processing-forum:
http://processing.org/discourse/yabb_beta/YaBB.cgi?board=os_libraries_tools;action=display;num=1153344411
JosephDuchesne wrote a function that saves tiffs. it can save PImage and PGraphics. and it takes a param for dpi. works for me and is pretty amazing.
this might be the way for you to make the tilesaver take a param for dpi…

15. marius watz, July 31st, 2007 at 20:58

Thanks, Lenny. I haven’t got much time to fix things at the moment, Dave Bollinger sent me a great mod which would allow even bigger images, I’d like to try and include both these hacks in a future release.

16. lenny, October 30th, 2007 at 03:23

hi marius
just another idea…
i’ve tested the p5sunflow renderer. i looks awesome, so i thought i might use it together with the tilesaver… to render huge and goodlooking images. somehow it didn’t work out…..
maybe this is a stupid idea, but if it’s possible to do that, then this might be another fancy thing for a future release…..

17. marius watz, October 30th, 2007 at 12:02

That would be a great idea indeed, it should be possible. I haven’t played with Sunflow myself, so I don’t know how the camera works compared to the Processing camera.

18. marius watz, October 30th, 2007 at 18:12

I just tried running the P5Sunflow quick start , turns out it won’t run with 0133. It works with 0125 though.

By turning up the memory for Processing I was able to render the frame at 11800 x 11800 pixels, large enough for 1×1 meter print. That seems promising.

19. lenny, November 15th, 2007 at 05:57

won’t run with 0133 ?
do you mean the P5Sunflow-library itself or the combination of the library and the tileSaver ?
i’m able to do sunflowrendering with 0133. so i dropped in the tilesaver. once this worked i switched from opengl to the sunflowrenderer. it gives all progressmessages like it’s supposed to. it also saves the image, but it’s only white. so, i wonder if that is the problem with 0133 ?
just asking because i’m on a imac, running leopard and i can’t run anything else but 0133.
when i try to run 0125 it gives me an error: java.lang.NumberFormatException: null
doesen’t even open a window….
however… you’re saying it worked out to render tiled images with P5sunflow, right ?
that would be very promising indeed….

20. marius watz, November 16th, 2007 at 01:32

I meant P5Sunflow. I can’t get 0133 to run with P5Sunflow on Windows, I get an error: “java.lang.UnsupportedClassVersionError: hipstersinc/P5Sunflow (Unsupported major.minor version 49.0)”

I haven’t worked on making Tilesaver work with P5Sunflow, just looked at it enough to see that it uses a different camera model. So the math would have to be redone for Sunflow.

21. Mauro De Giorgi, January 6th, 2008 at 15:14

Hi, i’m a beginner, so my question may be very stupid: i tried the class and the example but don’t work, and i have this error: Semantic Error: Type “aTileSaver” was not found.
I copied the class code in a file called aTileSaver and the exmple in a file colled aTileSaverSampleTest and i put the 2 files in the same dir. When i open a file, processing open both files in the dir, but when i try to run the sketch i have that error. Where is my error?

22. marius watz, January 8th, 2008 at 21:54

Mauro, the fault is mine not yours. In the code above I named the class “TileSaver” but referred to it as “aTileSaver”. It should be “aTileSaver”. I have corrected the class definition, just copy it into your sketch and delete the old TileSaver class.

This class should be folded into my unlekkerLib in the future.

23. Derek Kinsman, January 8th, 2008 at 22:51

Hi Marius, I’m slightly above being a beginner but not by much :) I’ve been trying to get aTileSaver to work and I’m getting a “camera() can only be used with P3D or OPENGL” warning. however the first 2 lines of my sketch are:


import processing.opengl.*;
aTileSaver tiler;

and the PDE is pointing me to this line of code in the tileSaver.pde file:

p.camera(width/2.0f, height/2.0f, cameraZ,

near the bottom of the file. I’m not sure what’s going on with it. Any help would be super.

Cheers,
+dk

24. Jamie, February 19th, 2008 at 13:04

Hi,
sorry to bother, but with the TilSaver Lib, i only get fully black pictures. even tough the preview image is correct, but small.

My code is like this:

import unlekker.util.*;
import unlekker.geom.*;
import unlekker.data.*;

TileSaver newTil = new TileSaver(this);

void setup(){};

void draw(){
newTil.pre();

// Fuunction for drawing 3d Graphics (opengl) with noLoop(); at the end
function3D();

newTil.setupCamera();
newTil.init(”imageTest”,2);
newTil.setSaveType(”.jpg”);
println(newTil.checkStatus());
newTil.tileFinish();

newTil.restoreCamera();
newTil.post();

}

Thanks a lot for every Help!
If i am wrong here, tell me and i move it.
Best, Jamie

25. marius watz, February 22nd, 2008 at 10:34

Jamie: TileSaver needs the draw() loop to keep running, so noLoop() will break the output. You shouldn’t be calling any of the TileSaver functions except pre() and post() inside draw().

Try something like the following:

void setup(){
newTil.init(”imageTest”,2);
newTil.setSaveType(”.jpg”);
}

void draw(){
newTil.pre();

function3D(); // remove noLoop()
println(newTil.checkStatus());

newTil.post();
}

Comment on this entry

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>