Loading...
Logo
Processing Forum
ignotus's Profile
9 Posts
40 Responses
0 Followers

Activity Trend

Last 30 days

Loading Chart...

Show:
Private Message
    This problem may have first been discussed in the post: http://forum.processing.org/topic/another-memory-leak-in-pimage.

    I have a small, concrete example running in Processing 2.0 that demonstrates the problem. I have tested the code in Processing 1.5.1, where there is no problem. I have confirmed that Java garbage collection works correctly in 1.5.1 using JConsole to monitor the Java heap. JConsole confirms that no garbage collection is taking place in 2.0. I have also used Eclipse Memory Analyzer to inspect a heap dump from both versions of my code, running under 1.5.1 and running under 2.0b6, current as of today.

    I am testing garbage collection by rotating and/or resizing an image. When I load an image that is 800 x 1181 pixels and zoom it to fit the screen ('F' key), I can rotate it ('T' key) about 120 or so times and then I will get a crash:

    Exception in thread "Animation Thread" java.lang.OutOfMemoryError: Java heap space
        at processing.core.PImage.init(PImage.java:235)
        at processing.core.PImage.<init>(PImage.java:221)
        at processing.core.PApplet.createImage(PApplet.java:1824)
        at ImageLeakTest2x.rotateImage(ImageLeakTest2x.java:209)
        at ImageLeakTest2x.rotatePixels(ImageLeakTest2x.java:192)
        at ImageLeakTest2x.decode(ImageLeakTest2x.java:158)
        at ImageLeakTest2x.keyPressed(ImageLeakTest2x.java:135)
        at processing.core.PApplet.handleKeyEvent(PApplet.java:2756)
        at processing.core.PApplet.dequeueKeyEvents(PApplet.java:2699)
        at processing.core.PApplet.handleDraw(PApplet.java:2140)
        at processing.core.PGraphicsJava2D.requestDraw(PGraphicsJava2D.java:190)
        at processing.core.PApplet.run(PApplet.java:2006)
        at java.lang.Thread.run(Thread.java:680)


    This is very obvious in JConsole, where the heap grows and never shrinks. It does shrink when the same code is run in Processing 1.5.1, and does not crash.

    Here's the code:

    1. // testing a memory leak in PImage
      // by Paul Hertz, 2012
      // http://paulhertz.net/

      // press 'o' to open a file
      // press 's' to save a timestamped .png file
      // press 'r' to revert to the current file
      // press 't' to turn the image 90 clockwise
      // press 'v' to turn verbose output on and off
      // press 'h' to show help message

      import java.awt.Container;
      import java.awt.Frame;
      import java.io.*;


      /** the currently selected file */
      File selectedFile;
      /** the primary image to display and glitch */
      PImage img;
      /** a version of the image scaled to fit the screen dimensions, for display only */
      PImage fitImg;
      /** true if image should fit screen, otherwise false */
      boolean isFitToScreen = false;
      /** maximum width for the display window */
      int maxWindowWidth;
      /** maximum height for the display window */
      int maxWindowHeight;
      /** width of the image when scaled to fit the display window */
      int scaledWidth;
      /** height of the image when scaled to fit the display window */
      int scaledHeight;
      /** current width of the frame (display window) */
      int frameWidth;
      /** current height of the frame (display window) */
      int frameHeight;
      /** reference to the frame (display window) */
      Frame myFrame;
      /**  */
      boolean verbose = false;
      // file count for filenames in sequence
      int fileCount = 0;
      // timestamp for filenames
      String timestamp;


      public void setup() {
        // println("Screen: "+ screenWidth +", "+ screenHeight);
        println("Display: "+ displayWidth +", "+ displayHeight);
        size(640, 480);
        smooth();
        // max window width is the screen width
        maxWindowWidth = displayWidth;
        // leave window height some room for title bar, etc.
        maxWindowHeight = displayHeight - 56;
        // image to display
        img = createImage(width, height, ARGB);
        // okay now to open an image file
        openFile();
        // Processing initializes the frame and hands it to you in the "frame" field.
        // Eclipse does things differently. Use findFrame method to get the frame in Eclipse.
        myFrame = findFrame();
        myFrame.setResizable(true);
        // the first time around, window won't be resized, a reload should resize it
        revert();
        // initialize timestamp, used in filename
        // timestamp = year() + nf(month(),2) + nf(day(),2) + "-"  + nf(hour(),2) + nf(minute(),2) + nf(second(),2);
        timestamp = nf(day(),2) + nf(hour(),2) + nf(minute(),2) + nf(second(),2);
        printHelp();
      }
       
      /**
       * @return   Frame where Processing draws, useful method in Eclipse
       */
      public Frame findFrame() {
        Container f = this.getParent();
        while (!(f instanceof Frame) && f!=null)
          f = f.getParent();
        return (Frame) f;
      }

      /**
       * Prints help message to the console
       */
      public void printHelp() {
        println("press 'o' to open a file");
        println("press 's' to save a timestamped .png file");
        println("press 'r' to revert to the current file");
        println("press 't' to turn the image 90 clockwise");
        println("press 'h' to show help message");
      }


      public void draw() {
        if (isFitToScreen) {
          image(fitImg, 0, 0);
        }
        else {
          background(255);
          image(img, 0, 0);
        }
      }


      /* (non-Javadoc)
       * handles key presses intended as commands
       * @see processing.core.PApplet#keyPressed()
       */
      public void keyPressed() {
        if (key != CODED) {
          decode(key);
        }
      }
       
      /**
       * associates characters input from keyboard with commands
       * @param ch   a char value representing a command
       */
      public void decode(char ch) {
        if (ch == 'o' || ch == 'O') {
          openFile();
        }
        else if (ch == 'v' || ch == 'V') {
          verbose = !verbose;
          println("verbose is "+ verbose);
        }
        else if (ch == 's' || ch == 'S') {
          saveFile();
        }
        else if (ch == 'r' || ch == 'R') {
          revert();
        }
        else if (ch == 't' || ch == 'T') {
          rotatePixels();
        }
        else if (ch == 'h' || ch == 'H') {
          printHelp();
        }
        else if (ch == 'f' || ch == 'F') {
          fitPixels(!isFitToScreen);
        }
      }


      /********************************************/
      /*                                          */
      /*             >>> COMMANDS <<<             */
      /*                                          */
      /********************************************/


      /**
       * reverts display and display buffer to last opened file
       */
      public void revert() {
        if (null != selectedFile) {
          loadFile();  
          if (isFitToScreen) fitPixels(true);
        }
      }

      int rCount = 0;
      /**
       * rotates image and backup image 90 degrees clockwise
       */
      public void rotatePixels() {
        if (null == img) return;
        img = rotateImage(img);
        fitPixels(isFitToScreen);
        // rotate undo buffer image, don't rotate snapshot
        println(rCount++);
      }

      /**
       * rotates image pixels 90 degrees clockwise
       * @param image   the image to rotate
       * @return        the rotated image
       */
      public PImage rotateImage(PImage image) {
        // rotate image 90 degrees
        image.loadPixels();
        int h = image.height;
        int w = image.width;
        int i = 0;
        PImage newImage = createImage(h, w, ARGB);
        newImage.loadPixels();
        for (int ry = 0; ry < w; ry++) {
          for (int rx = 0; rx < h; rx++) {
            newImage.pixels[i++] = image.pixels[(h - 1 - rx) * image.width + ry];
          }
        }
        newImage.updatePixels();
        return newImage;
      }

      /**
       * Fits large images to the screen, zooms small images to full screen. Displays as much of a large image
       * as fits the screen if every pixel is displayed.
       *
       * @param fitToScreen   true if image should be fit to screen, false if every pixel should displayed
       */
      public void fitPixels(boolean fitToScreen) {
        if (fitToScreen) {
          fitImg = createImage(img.width, img.height, ARGB);
          scaledWidth = fitImg.width;
          scaledHeight = fitImg.height;
          fitImg.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
          // calculate proportions of window and image,
          // be sure to convert ints to floats to get the math right
          // ratio of the window height to the window width
          float windowRatio = maxWindowHeight/(float)maxWindowWidth;
          // ratio of the image height to the image width
          float imageRatio = fitImg.height/(float)fitImg.width;
          if (verbose) {
            println("maxWindowWidth "+ maxWindowWidth +", maxWindowHeight "+ maxWindowHeight +", screen ratio "+ windowRatio);
            println("image width "+ fitImg.width +", image height "+ fitImg.height +", image ratio "+ imageRatio);
          }
          if (imageRatio > windowRatio) {
            // image is proportionally taller than the display window,
            // so scale image height to fit the window height
            scaledHeight = maxWindowHeight;
            // and scale image width by window height divided by image height
            scaledWidth = Math.round(fitImg.width * (maxWindowHeight / (float)fitImg.height));
          }
          else {
            // image is proportionally equal to or wider than the display window,
            // so scale image width to fit the windwo width
            scaledWidth = maxWindowWidth;
            // and scale image height by window width divided by image width
            scaledHeight = Math.round(fitImg.height * (maxWindowWidth / (float)fitImg.width));
          }
          fitImg.resize(scaledWidth, scaledHeight);
          if (null != myFrame) myFrame.setSize(scaledWidth, scaledHeight + 48);
        }
        else {
          scaledWidth = img.width;
          scaledHeight = img.height;
          if (null != myFrame) {
            frameWidth = scaledWidth <= maxWindowWidth ? scaledWidth : maxWindowWidth;
            frameHeight = scaledHeight <= maxWindowHeight ? scaledHeight : maxWindowHeight;
            myFrame.setSize(frameWidth, frameHeight + 38);
          }
        }
        if (verbose) println("scaledWidth = "+ scaledWidth +", scaledHeight = "+ scaledHeight +", frameWidth = "+ frameWidth +", frameHeight = "+ frameHeight);
        isFitToScreen = fitToScreen;
      }


      /********************************************/
      /*                                          */
      /*             >>> FILE I/O <<<             */
      /*                                          */
      /********************************************/

      /**
       * opens a user-specified file (JPEG, GIF or PNG only)
       */
      public void openFile() {
        selectInput("Choose an image file.", "fileSelected");   
      }

      void fileSelected(File selection) {
         if (selection == null) {
           println("----- Window was closed or the user hit cancel.");
         }
         else {
           selectedFile = selection;
           println("User selected " + selection.getAbsolutePath());
           loadFile();
         }
      }
      /**
       * saves current image to a uniquely-named file
       */
      public void saveFile() {
        String shortName = selectedFile.getName();
        String[] parts = shortName.split("\\.");
        String fName = parts[0] +"_"+ timestamp +"_"+ fileCount +".png";
        fileCount++;
        println("saving "+ fName);
        img.save(fName);
      }

      /**
       * loads a file into variable img.
       */
      public void loadFile() {
        println("selected file "+ selectedFile.getAbsolutePath());
        img = loadImage(selectedFile.getAbsolutePath());
        fitPixels(isFitToScreen);
        println("image width "+ img.width +", image height "+ img.height);
      }




    Maybe I am misunderstanding garbage collection and it's really version 1.5.1 that has things wrong, but it's not obvious to me. I would say that newImage (in rotateImage) goes out of scope, should be garbage collected, and that pointing img at the return value from rotateImage should not make any difference. Instead, each call to rotateImage leaves an image on the heap and it just stays there, glowering.

    I await good answers from my smart friends.

    cheers,

    -- Paul


    ----- |(*,+,#,=)(#,=,*,+)(=,#,+,*)(+,*,=,#)| ---
    IgnoCodeLib: http://ignotus.com/ignocodelib/

    IgnoCodeLib implements complex graphics objects consisting of lines and cubic Bézier curves, simple text, geometric transforms, hierarchical display list and document structure, simple facilities for permutations and random numbers, and above all, export of graphics and text to the Adobe Illustrator 7.0 file format. This format can be read by all current versions of AI. All components in the document structure support cascading calls to draw(), transform() and write() methods. Drawing to display and writing to file can sometimes be accomplished with a single line of code. Geometric transforms are nearly as easy. IgnoCodeLib is distributed under the Gnu Lesser General Public License.

    All the images in this Flickr set were created with this software.

    The code is far from perfect, as is the documentation, but there are several examples that I hope will help people to use the code. The Javadocs are good enough to help people who read Javadocs, or so I hope. I will write tutorials, but at this moment it's better to release the library so people can hammer on it. There comes a point when you have have to shove your children out the door to play because they have too much energy to stay indoors.

    Thanks to phi.lho and amnon.owed for testing and advice.

    regards,

    -- Paul


    Processing library classes often are initialized with a reference to the host PApplet, as in

    CustomObjectClass custom = new CustomObjectClass(this);

    On the other hand, there are often good reasons for having a no-argument initializer for a class.

    In the case of graphics objects, one passing the PApplet reference would seem to be useful. The current graphics state can be obtained from the PApplet and used to set the attributes of the custom object.

    1.     public void setColors(PApplet parent) {
    2.         if (parent.g.fill) {
    3.             this.setFillColor(parent.g.fillColor);
    4.         }
    5.         else {
    6.             setNoFill();
    7.         }
    8.         if (parent.g.stroke) {
    9.             this.setStrokeColor(parent.g.strokeColor);
    10.             this.setWeight(parent.g.strokeWeight);
    11.         }
    12.         else {
    13.             setNoStroke();
    14.         }
    15.     }
    where "g" is the PGraphics renderer associated with the PApplet. It has other useful information, notatbly the current transformation matrix:

    1.     public void setCtm(PApplet parent) {
    2.         processing.core.PMatrix2D m = parent.getMatrix(new PMatrix2D());
    3.         this.getCtm().setCTM(m.m00, m.m01, m.m10, m.m11, m.m02, m.m12);
    4.     }

    I had been using a no-argument constructor, basically because it set all defaults to a given state. Users could then call the two methods above to set attributes and have a record of the transformation matrix at the moment when the instance was created (or at any later time, for that matter).

    But now I'm inclined to think that initializing a custom graphics object with a PApplet instance may the way to go. At least for users who don't want to write extra code, having an object that behaves like any other Processing graphics object and takes on the current fill and stroke settings is probably a more intuitive way to go.

    I wonder if anyone has opinions on this matter?

    I'm at the point of releasing a library. That's why this issue has come up. It's easy to go one way or the other, but it would be hard to go back once I release it. Having both a PApplet constructor and a no-argument constructor seems needlessly complicated, and so likely to make the class harder to use.

    cheers,

    -- Paul




    I am looking for someone who uses Adobe Illustrator and Processing on a PC and could spare about an hour to do some testing. I have developed a Processing library that exports Processing graphics to Illustrator, but I have only tested it on a Mac. It's nearly ready for release. It would be good to say that it works on both platforms when I post the announcement (it should, but you never know).

    -- Paul
    I've been trying to create an alpha channel from a PGraphics as follows:

    1.         PGraphics pg = createGraphics(640, 950, P2D);
    2.         pg.beginDraw();
    3.         myRegions[i].draw(this); // draw this shape
    4.         pg.endDraw();

    Doing this:

    1.         PImage img = loadImage("raspberries.jpg");
    2.         img.mask(pg);
    3.         image(img,0,0);
    just draws my mask image. Same for calling img.mask(pg.pixels). Creating a new PImage from the java.awt.Image returned by pg.getImage() is not only needlessly complicated, but has the same result: just the mask show up.

    Saving the graphics out to a file and loading it works just as you'd hope:

    1. img.mask(loadImage("mask.png"));
    But this is hardly an efficient way to do things--and I want to use this technique in real time interaction.

    Seems like a useful technique, creating an offscreen buffer with a PGraphics that serves as an alpha channel for  a PImage. I"m sure someone has done it before, but a brief search hasn't turned up the answer.

    thanks,

    -- Paul





    I'm running a simple test app that is posting an error apparently from a windowing routine in MacOS's Cocoa system library.

    1. import java.awt.*;
    2. int[] pts1 = { 0,0, 1,1, 2,1, 3,2, 3,1, 3,0, 2,0, 1,0, 0,0 };
    3. int[] pts2 = { 3,0, 3,1, 3,2, 3,3, 4,2, 4,1, 3,0 };
    4. Point[] arr1;
    5. Point[] arr2;

    6. void setup() {
    7.   size(481, 481);
    8.   background(255);
    9.   arr1 = arrayAsPoints(pts1);
    10.   arr2 = arrayAsPoints(pts2);
    11.   noLoop();
    12. }

    13. void draw() {
    14.   println("area 1 = " + area(arr1));
    15.   println("area 2 = " + area(arr2));
    16. }

    17. /**
    18.  * outputs 2 * area of polygon, negative if vertices are in CCW order.
    19.  */
    20. float area(Point[] coords) {
    21.   int len = coords.length;
    22.   float result = coords[0].x * (coords[1].y - coords[len-2].y);
    23.   for (int i = 1; i < len - 1; i++) {
    24.     result += coords[i].x * (coords[(i+1) % len].y - coords[i-1].y);
    25.   }
    26.   return result;
    27. }

    28. Point[] arrayAsPoints( int[] arr ) {
    29.   Point[] ptArray = new Point[arr.length/2];
    30.   int index = 0;
    31.   for ( int i = 0; i < arr.length; i += 2 ) {
    32.     ptArray[index] = new Point( arr[i], arr[i+1] );
    33.     index++;
    34.   }
    35.   return ptArray;
    36. }
    Not much going on there, as you can see--just a test for an area calculation.

    Here's the error, followed by the application output:

    2010-06-23 08:31:08.569 java[899:170f] *** Assertion failure in -[CocoaAppWindow _changeJustMain], /SourceCache/AppKit/AppKit-1038.32/AppKit.subproj/NSWindow.m:9470
    2010-06-23 08:31:08.571 java[899:170f] *** CPerformer: ignoring exception 'Invalid parameter not satisfying: [self canBecomeMainWindow]' raised during perform of selector 'requestFocus:' on target 'FocusManager' with args '<AWTComponentSetFocusData: 0x1205040>'
    area 1 = -6.0
    area 2 = -4.0

    The error doesn't seem to cause any problems, since it gets ignored. I've seen it before. Maybe there's no need to avoid it, if it's really innocuous, but I can't help but wonder why it occurs and if I can prevent it.

    cheers,

    --Paul