0
\$\begingroup\$

I want to create a very big gigantic PNG image. The image pixel are generated randomly, being

How can I improve the below code to create a fast random pixel image in java?

import java.io.File; 
import java.io.IOException; 
import java.awt.image.BufferedImage; 
import javax.imageio.ImageIO; 
public class RandomImage 
{ 
    public static void main(String args[])throws IOException 
    { 
        // Image file dimensions 
        int width = 30000, height = 24000; 

        // Create buffered image object 
        BufferedImage img = null; 
        img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 

        // file object 
        File f = null; 

        // create random values pixel by pixel 
        for (int y = 0; y < height; y++) 
        { 
            for (int x = 0; x < width; x++) 
            { 
                int a = (int)(Math.random()*256); //generating 
                int r = (int)(Math.random()*256); //values 
                int g = (int)(Math.random()*256); //less than 
                int b = (int)(Math.random()*256); //256 

                int p = (a<<24) | (r<<16) | (g<<8) | b; //pixel 

                img.setRGB(x, y, p); 
            } 
        } 

        // write image 
        try
        { 
            f = new File("G:\\Out.png"); 
            ImageIO.write(img, "png", f); 
        } 
        catch(IOException e) 
        { 
        System.out.println("Error: " + e); 
        } 
    } 
} 
\$\endgroup\$
3
  • \$\begingroup\$ This question contains an unfinished \$\endgroup\$ – Roland Illig Jun 30 '20 at 6:56
  • \$\begingroup\$ The question leaves unanswered what you want to do with the random image, after you generated it. That's much more interesting than generating it fast, and there may even be completely different approaches. \$\endgroup\$ – Roland Illig Jun 30 '20 at 7:04
  • \$\begingroup\$ @RolandIllig I want to save the image to disk. \$\endgroup\$ – Hither Joe Jul 1 '20 at 12:37
5
\$\begingroup\$

You can write arrays of Integers directly on the WritableRaster of the BufferedImage:

public static BufferedImage createRandomImage(final int width, final int height) {
    final BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

    final long bytesPerPixel = 4L;

    final int[] pixelData = new SplittableRandom().ints(bytesPerPixel * width * height, 0, 256).toArray();

    result.getRaster().setPixels(0, 0, width, height, pixelData);

    return result;
}

This performs more than three times faster on my machine in a very crude benchmark (3.92S vs 1.26S), with the caveat that for very big images like the one you desire, you have a lot of data duplication, because you basically have to allocate twice the amount of memory, since you're writing the random data to a buffer first.

This can easily be solved by writing arrays of random Integers line by line though, which nets comparable performance:

public static BufferedImage createRandomImage(final int width, final int height) {
    final BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

    final long bytesPerPixel = 4L;

    for (int y = 0; y < height; y++) {
        final int[] pixelData = new SplittableRandom().ints(bytesPerPixel * width, 0, 256)
                                                      .toArray();

        result.getRaster().setPixels(0, y, width, 1, pixelData);
    }

    return result;
}

BufferedImage.setRGB is very slow, because for every pixel you set, it has to go fetch the corresponding data element:

public void setRGB(int x, int y, int rgb) {
    raster.setDataElements(x, y, colorModel.getDataElements(rgb, null));
}

which is a comparatively heavy operation.


Parallelization

Crude parallelization improves performance slightly, but it does not scale linearly with number of cores. On my 24 thread machine, the following yields a 30-40% improvement compared to the loop WritableRaster variant:

public static BufferedImage createRandomImage(final int width, final int height) {
    final BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

    final long bytesPerPixel = 4L;

    IntStream.range(0, height).parallel().forEach(y -> {
        final int[] pixelData = new SplittableRandom().ints(bytesPerPixel * width, 0, 256)
                                                      .toArray();
        result.getRaster().setPixels(0, y, width, 1, pixelData);
    });

    return result;
}
\$\endgroup\$
3
  • 2
    \$\begingroup\$ Really nice answer, I didn't know the SplittableRandom class. \$\endgroup\$ – dariosicily Jun 19 '20 at 8:00
  • \$\begingroup\$ Did you copy-paste the same snippet twice? I can't spot the difference in the parallelized example. \$\endgroup\$ – TorbenPutkonen Jun 29 '20 at 11:42
  • \$\begingroup\$ @TorbenPutkonen thank you, indeed a copy paste error. Fixed. \$\endgroup\$ – Marv Jun 29 '20 at 13:03
2
\$\begingroup\$

Since you are constructing a full, 32-bit word from random bits, you could skip the niceties and simply generate a 32-bit random number as your p. What is unclear is how the edge cases would behave. For instance, I would have used an unsigned int instead of an int to hold your four values (0-255), as the leftmost one (R) might mess with the sign of p. etc. That should go roughly 4x faster than using 4 calls and multiplies for each pixel.

Also, do you really need 720 million different random values? or does it just need to look real random? You could have an array of say 1,000,000 pixels, instead of p, have p[1000000]. That's a pretty fast gen, 1M v 720M. Then, simply generate random values between 0 and 999999, do your img.setRGB() with random selections from that pallette. And yes, that is no faster than the above code. But consider NOT generating random values from 0-999999. Consider, instead, taking these already-random pixels in a loop, from first to last, and then repeating that loop, over and over, as you progress through your image. Sure it will "repeat" but visually, it will be kind of hard to see a pattern.

As a further alternative, having very quickly generated a million random pixels, fill your image array by doing the following:

  1. generate a random number, X, from 0-970000
  2. fill the first 30000 image pixels with 30000 values from the million pixels, starting at X
  3. repeat for the next 30000 image pixels (a total of 24000 times. 24000 add'l calls to Math.random()).

This should have a decided speed advantage over calling Math.random() 720,000,000 times and still should be uber random looking.

\$\endgroup\$
2
  • \$\begingroup\$ Using WritableRaster instead of setRGB like I suggested in my answer yields a greater improvement, but this suggestion yields about another speed doubling when using WritableRaster, depending on how many values you generate. It definitely doesn't scale linearly with the amount of pixels. \$\endgroup\$ – Marv Jun 19 '20 at 2:34
  • \$\begingroup\$ I have to confess that I did not read your original answer very carefully! If Java actually allows direct access to video raster space, then the part where random BLocks are Transferred from the million pixel source might be handled by BLiT routines. This is all new to me (in Java)! Thank you @Marv; very nice low-level info! \$\endgroup\$ – James Huddle Jun 20 '20 at 11:43
-1
\$\begingroup\$

In regards to speed, I'm not seeing a whole lot you could improve. One thing that comes to mind is the use of lower level APIs -- i.e. don't use File and BufferedImage. You could, for example, use a DataOutputStream (https://docs.oracle.com/javase/10/docs/api/java/io/DataOutputStream.html) which, from my understanding, is one of Java's lowest level APIs for writing data to a file. You could do something like this:

DataOutputStream stream = new DataOutputStream(new FileOutputStream("path/to/file"));

Then with this, you can use methods like writeByte to continuously populate the image file, rather than doing it in one whole buffer. I don't know if in the end this produces a faster result, but it produces a result such that the user could stop the program partway through and still end up with an image.

The caveat here is that you need to research what the proper metadata for a PNG file looks like and output that manually.


Something you could also try is using one loop as opposed to two.

To do that, you would do something like this:

for(int i = 0; i < width * height; i++) {
    //row=floor(n / width)
    //col=n-(row*width)
}

(I think I did the math right on those row and col numbers but you might want to check me).

This might be faster -- I don't know if the extra computations will slow things down, but it'd be worthy to try out.


Finally, unrelated to speed: a lot of things in your code are hardcoded right now (i.e. the width, height, and file name).

I would instead export everything to a separate function for generating random images, and that function could take parameters width, height, and file name. Then, in your main function, you use some sort of user input to determine what to pass to that function (e.g. command-line arguments or STDIN).

Additionally, having a separate function makes it unit-testable.

\$\endgroup\$
4
  • \$\begingroup\$ There is no need to go quite that low level, you can use the fact that BufferedImage exposes it's underlying WritableRaster via BufferedImage.getRaster, see my answer for a full explanation. Also I doubt unfolding the inner loop will yield any measurable performance benefit. \$\endgroup\$ – Marv Jun 18 '20 at 21:07
  • \$\begingroup\$ @Marv Thank you for the info! \$\endgroup\$ – SirPython Jun 19 '20 at 3:24
  • \$\begingroup\$ The unfolded inner loop will be slower if you really put two integer divisions in it. \$\endgroup\$ – Roland Illig Jun 30 '20 at 7:02
  • \$\begingroup\$ @RolandIllig Hmm yeah, I wasn't sure about that. I don't know how Java optimizes it. \$\endgroup\$ – SirPython Jul 1 '20 at 0:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.