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);
}
}
}
-
\$\begingroup\$ This question contains an unfinished \$\endgroup\$Roland Illig– Roland Illig2020年06月30日 06:56:02 +00:00Commented Jun 30, 2020 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– Roland Illig2020年06月30日 07:04:17 +00:00Commented Jun 30, 2020 at 7:04
-
\$\begingroup\$ @RolandIllig I want to save the image to disk. \$\endgroup\$Hither Joe– Hither Joe2020年07月01日 12:37:47 +00:00Commented Jul 1, 2020 at 12:37
3 Answers 3
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;
}
-
2\$\begingroup\$ Really nice answer, I didn't know the
SplittableRandom
class. \$\endgroup\$dariosicily– dariosicily2020年06月19日 08:00:28 +00:00Commented Jun 19, 2020 at 8:00 -
\$\begingroup\$ Did you copy-paste the same snippet twice? I can't spot the difference in the parallelized example. \$\endgroup\$TorbenPutkonen– TorbenPutkonen2020年06月29日 11:42:19 +00:00Commented Jun 29, 2020 at 11:42
-
\$\begingroup\$ @TorbenPutkonen thank you, indeed a copy paste error. Fixed. \$\endgroup\$Marv– Marv2020年06月29日 13:03:16 +00:00Commented Jun 29, 2020 at 13:03
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:
- generate a random number, X, from 0-970000
- fill the first 30000 image pixels with 30000 values from the million pixels, starting at X
- 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.
-
\$\begingroup\$ Using
WritableRaster
instead ofsetRGB
like I suggested in my answer yields a greater improvement, but this suggestion yields about another speed doubling when usingWritableRaster
, depending on how many values you generate. It definitely doesn't scale linearly with the amount of pixels. \$\endgroup\$Marv– Marv2020年06月19日 02:34:19 +00:00Commented Jun 19, 2020 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– James Huddle2020年06月20日 11:43:59 +00:00Commented Jun 20, 2020 at 11:43
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.
-
\$\begingroup\$ There is no need to go quite that low level, you can use the fact that
BufferedImage
exposes it's underlyingWritableRaster
viaBufferedImage.getRaster
, see my answer for a full explanation. Also I doubt unfolding the inner loop will yield any measurable performance benefit. \$\endgroup\$Marv– Marv2020年06月18日 21:07:30 +00:00Commented Jun 18, 2020 at 21:07 -
\$\begingroup\$ The unfolded inner loop will be slower if you really put two integer divisions in it. \$\endgroup\$Roland Illig– Roland Illig2020年06月30日 07:02:22 +00:00Commented Jun 30, 2020 at 7:02
-
\$\begingroup\$ @RolandIllig Hmm yeah, I wasn't sure about that. I don't know how Java optimizes it. \$\endgroup\$SirPython– SirPython2020年07月01日 00:22:13 +00:00Commented Jul 1, 2020 at 0:22
Explore related questions
See similar questions with these tags.