6
\$\begingroup\$

(A sample cover art follows.) enter image description here

I'm currently working on replacing all the covers of my music files to have a unified theme, and I've started this project in Java to refresh my knowledge after a gap of 2-3 years.

The program generates random pixels in the upper-left quadrant of the image, mirrors the quadrant to the other three quadrants, and then simulates Conway's Game of Life to create an interesting symmetric pattern. Finally, the pixels are colored to create a circular colored gradient.

If anything catches your eye, please let me know. However, I'm primarily interested in improving performance and whether I've gone overboard with the use of parallel streams.

Cover.java

package cover; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.Random; import java.util.stream.IntStream; import javax.imageio.ImageIO; import static utils.Constants.*; public class Cover { boolean[][] pixelData = new boolean[HEIGHT][WIDTH]; public Cover() { generatePixelData(); } /* * Generates PIXEL_PERCENTAGE of random pixels for the upper left quadrant of the cover. */ private void generatePixelData() { Random random = new Random(); IntStream.range(0, HEIGHT / 2).parallel().forEach(y -> { for (int x = 0; x < WIDTH / 2; x++) { pixelData[y][x] = random.nextInt(100) < PIXEL_PERCENTAGE; } }); mirrorPixelData(); } /* * Mirrors the pattern of the upper left corner to the other three quadrants. */ private void mirrorPixelData() { IntStream.range(0, HEIGHT / 2).parallel().forEach(y -> { // Mirror to top right IntStream.range(0, WIDTH / 2).parallel().forEach(x -> { pixelData[y][WIDTH - 1 - x] = pixelData[y][x]; }); // Mirror to bottom left IntStream.range(0, WIDTH / 2).parallel().forEach(x -> { pixelData[HEIGHT - 1 - y][x] = pixelData[y][x]; }); // Mirror to bottom right IntStream.range(0, WIDTH / 2).parallel().forEach(x -> { pixelData[HEIGHT - 1 - y][WIDTH - 1 - x] = pixelData[y][x]; }); }); simulateConwaysWayOfLife(); } /* * Simulates Conways way of life for the random pixels. * Each step the following transitions occur: * 1. Live pixel with <2 neighbors dies. * 2. Live pixel with =2 or =3 neighrbors lives on. * 3. Live pixel with >3 neighbors dies. * 4. Dead pixel with =3 neighbors becomes alive. */ private void simulateConwaysWayOfLife() { for (int simulation = 0; simulation < SIMULATIONS; simulation++) { boolean[][] simulatedPixelData = new boolean[HEIGHT][WIDTH]; IntStream.range(0, HEIGHT).parallel().forEach(y -> { IntStream.range(0, WIDTH).parallel().forEach(x -> { // Checks for every pixel how many neighbor pixels are alive. int alive = 0; for (int ny = -1; ny < 2; ny++) { for (int nx = -1; nx < 2; nx++) { if (y + ny >= 0 && y + ny < HEIGHT && x + nx >= 0 && x + nx < WIDTH) { if (pixelData[y + ny][x + nx]) { alive++; } } } } // The pixels itself does not count as a neighbor. if (pixelData[y][x]) alive--; // Conways way of life rules implementation. switch (alive) { case 0: case 1: simulatedPixelData[y][x] = false; break; case 2: simulatedPixelData[y][x] = pixelData[y][x]; break; case 3: simulatedPixelData[y][x] = true; break; default: simulatedPixelData[y][x] = false; break; } }); }); pixelData = simulatedPixelData; } scalePixelData(); } /* * Scales the cover to a higher resolution. */ private void scalePixelData() { boolean[][] scaledData = new boolean[SCALED_HEIGHT][SCALED_WIDTH]; IntStream.range(0, HEIGHT).parallel().forEach(y -> { IntStream.range(0, WIDTH).parallel().forEach(x -> { boolean value = pixelData[y][x]; IntStream.range(0, SCALE_FACTOR).parallel().forEach(scaleY -> { IntStream.range(0, SCALE_FACTOR).parallel().forEach(scaleX -> { scaledData[y * SCALE_FACTOR + scaleY][x * SCALE_FACTOR + scaleX] = value; }); }); }); }); pixelData = scaledData; roundEdges(); } /* * Rounds the corners of the scaled cover to get a smoother look. */ private void roundEdges() { boolean[][] roundedPixels = new boolean[SCALED_HEIGHT][SCALED_WIDTH]; IntStream.range(0, SCALED_HEIGHT).parallel().forEach(y -> { IntStream.range(0, SCALED_WIDTH).parallel().forEach(x -> { // Checks for every pixel how many neighbor pixels are alive. int alive = 0; for (int ny = -1; ny < 2; ny++) { for (int nx = -1; nx < 2; nx++) { if (y + ny >= 0 && y + ny < SCALED_HEIGHT && x + nx >= 0 && x + nx < SCALED_WIDTH) { if (pixelData[y + ny][x + nx]) { alive++; } } } } // The pixel itself does not count as neighbor. if (pixelData[y][x]) alive--; roundedPixels[y][x] = alive > 4; }); }); pixelData = roundedPixels; convertToImageData(); } /* * For every pixel determines its color to create a gradient and make the background transparent. */ private void convertToImageData() { Random random = new Random(); // Selects random color gradient. CircleGradient circleGradient = new CircleGradient(getAllColorGradients().get(random.nextInt(getAllColorGradients().size()))); int argbChannelCount = 4; int[] imageData = new int[SCALED_HEIGHT * SCALED_WIDTH* argbChannelCount]; IntStream.range(0, SCALED_HEIGHT).parallel().forEach(y -> { IntStream.range(0, SCALED_WIDTH).parallel().forEach(x -> { int index = y * argbChannelCount * SCALED_WIDTH + x * argbChannelCount; int[] colors = circleGradient.getColors(x, y); // Sets rgba values. imageData[index] = pixelData[y][x] ? colors[0] : 0; imageData[index + 1] = pixelData[y][x] ? colors[1] : 0; imageData[index + 2] = pixelData[y][x] ? colors[2] : 0; imageData[index + 3] = pixelData[y][x] ? 255 : 0; }); }); createImage(imageData); } /* * Converts the imagedata to an actual image. */ public static void createImage(int[] imageData) { final BufferedImage bufferedImage = new BufferedImage(SCALED_WIDTH, SCALED_HEIGHT, BufferedImage.TYPE_INT_ARGB); bufferedImage.getRaster().setPixels(0, 0, SCALED_WIDTH, SCALED_HEIGHT, imageData); try { File file = new File("cover.png"); ImageIO.write(bufferedImage, "png", file); } catch(IOException e) { System.out.println("Error: " + e); } } } 

Constants.java

package utils; import java.util.ArrayList; import java.util.List; import cover.ColorGradient; public final class Constants { public static final int WIDTH = 100; public static final int HEIGHT = 100; public static final int SCALE_FACTOR = 5; public static final int SCALED_WIDTH = WIDTH * SCALE_FACTOR; public static final int SCALED_HEIGHT = HEIGHT * SCALE_FACTOR; public static final int PIXEL_PERCENTAGE = 30; public static final int SIMULATIONS = 10; public static final ColorGradient OCEAN_BLUE = new ColorGradient(new int[]{46,49,146}, new int[]{27,255,255}); public static final ColorGradient SANGUINE = new ColorGradient(new int[]{212,20,90}, new int[]{251,176,59}); public static final ColorGradient LUSCIOUS_LIME = new ColorGradient(new int[]{0,146,69}, new int[]{252,238,33}); public static final ColorGradient PURPLE_LAKE = new ColorGradient(new int[]{102,45,140}, new int[]{237,30,121}); public static final ColorGradient GREEN_BEACH = new ColorGradient(new int[]{2,170,189}, new int[]{0,205,172}); public static List<ColorGradient> getAllColorGradients() { List<ColorGradient> gradients = new ArrayList<>(); gradients.add(OCEAN_BLUE); gradients.add(SANGUINE); gradients.add(LUSCIOUS_LIME); gradients.add(PURPLE_LAKE); gradients.add(GREEN_BEACH); return gradients; } } 

ColorGradient.java

package cover; public class ColorGradient { private int[] color1; private int[] color2; public ColorGradient(int[] color1, int[] color2) { if (color1.length != 3 || color2.length != 3) { throw new IllegalArgumentException("Colors must consist of three integers."); } this.color1 = color1; this.color2 = color2; } // Calculate the difference between the individual color channels. public int redComponentDifference() { return color1[0] - color2[0]; } public int greenComponentDifference() { return color1[1] - color2[1]; } public int blueComponentDifference() { return color1[2] - color2[2]; } // Getter methods. public int[] getColor1() { return color1; } public int[] getColor2() { return color2; } } 

CircleGradient.java

package cover; import static utils.Constants.SCALED_HEIGHT; import static utils.Constants.SCALED_WIDTH; public class CircleGradient { private ColorGradient colorGradient; private double maxDistanceFromCenter = euclideanDistance(0, 0); public CircleGradient(ColorGradient colorGradient) { this.colorGradient = colorGradient; } public int[] getColors(int x, int y) { double distanceFactor = distanceFactor(x, y); int[] colors = new int[3]; colors[0] = (int) (colorGradient.getColor2()[0] + (colorGradient.redComponentDifference() * distanceFactor)); colors[1] = (int) (colorGradient.getColor2()[1] + (colorGradient.greenComponentDifference() * distanceFactor)); colors[2] = (int) (colorGradient.getColor2()[2] + (colorGradient.blueComponentDifference() * distanceFactor)); return colors; } private double distanceFactor(int x, int y) { return euclideanDistance(x, y) / maxDistanceFromCenter; } private double euclideanDistance(int x, int y) { double deltaX = Math.abs(SCALED_WIDTH / 2 - x); double deltaY = Math.abs(SCALED_HEIGHT / 2 - y); return Math.sqrt(deltaX * deltaX + deltaY * deltaY); } } 

Benchmark

Intrigued by coderoddes' comment about the performance of streams, I stumbled upon the following post and benchmarked the method generatePixels().

package com.zipity.cover; import java.util.Random; import java.util.function.IntFunction; import java.util.stream.IntStream; public final class CoverArtGeneratorBenchmark { public static boolean[][] seqGeneratePixelsRange(int n) { boolean pixelData[][] = new boolean[n][n]; Random random = new Random(); IntStream.range(0, n).forEach(y -> { for (int x = 0; x < n; x++) { pixelData[y][x] = random.nextInt(100) < 30; } }); return pixelData; } public static boolean[][] paraGeneratePixelsRange(int n) { boolean pixelData[][] = new boolean[n][n]; Random random = new Random(); IntStream.range(0, n).parallel().forEach(y -> { for (int x = 0; x < n; x++) { pixelData[y][x] = random.nextInt(100) < 30; } }); return pixelData; } public static boolean[][] seqGeneratePixelsIterate(int n) { boolean pixelData[][] = new boolean[n][n]; Random random = new Random(); IntStream.iterate(1, i -> i + 1).limit(n).forEach(y -> { for (int x = 0; x < n; x++) { pixelData[y - 1][x] = random.nextInt(100) < 30; } }); return pixelData; } public static boolean[][] paraGeneratePixelsIterate(int n) { boolean pixelData[][] = new boolean[n][n]; Random random = new Random(); IntStream.iterate(1, i -> i + 1).limit(n).parallel().forEach(y -> { for (int x = 0; x < n; x++) { pixelData[y - 1][x] = random.nextInt(100) < 30; } }); return pixelData; } public static boolean[][] iterGeneratePixelsLoop(int n) { boolean pixelData[][] = new boolean[n][n]; Random random = new Random(); for (int y = 0; y < n; y++) { for (int x = 0; x < n; x++) { pixelData[y][x] = random.nextInt(100) < 30; } } return pixelData; } /* * Applies the function parameter func, passing n as parameter. * Returns the average time (ms.) to execute the function 100 times. */ public static <R> double measurePerf(IntFunction<R> funcs, int n) { int numOfExecutions = 100; double totTime = 0.0; for (int i = 0; i < numOfExecutions; i++) { double start = System.nanoTime(); funcs.apply(n); double duration = (System.nanoTime() - start)/1_000_000; totTime += duration; } double avgTime = totTime/numOfExecutions; return avgTime; } /* * Executes the functions in the vararg funcs for different stream sizes. */ public static <R> void xqtFunctions(@SuppressWarnings("unchecked") IntFunction<R>... funcs) { int[] sizes = {100, 500, 1000, 2500}; for (int i = 0; i < sizes.length; ++i) { System.out.printf("%7d", sizes[i]); for (int j = 0; j < funcs.length; ++j) { System.out.printf("%10.5f", measurePerf(funcs[j], sizes[i])); } System.out.println(); } } @SuppressWarnings("unchecked") public static void main(String[] args) { System.out.println("Streams created with the range() method:"); System.out.println(" Size Sequential Parallel"); xqtFunctions(CoverArtGeneratorBenchmark::seqGeneratePixelsRange, CoverArtGeneratorBenchmark::paraGeneratePixelsRange); System.out.println("Streams created with the iterate() method:"); System.out.println(" Size Sequential Parallel"); xqtFunctions(CoverArtGeneratorBenchmark::seqGeneratePixelsIterate, CoverArtGeneratorBenchmark::paraGeneratePixelsIterate); System.out.println("Iterative solution with an explicit loop:"); System.out.println(" Size Iterative"); xqtFunctions(CoverArtGeneratorBenchmark::iterGeneratePixelsLoop); } } 

Streams created with the range() method:

SizeSequentialParallel
1000.193890.69368
5002.5247818.44609
10009.8331082.51152
250059.34847528.95678
5000226.03198Not tested
10000904.63635Not tested

Streams created with the iterate() method:

SizeSequentialParallel
1000.177630.90946
5002.3895520.80894
10008.7339883.54772
250054.71999519.07294
5000226.25446Not tested
10000913.62030Not tested

Iterative solution with an explicit loop:

SizeSequential
1000.15455
5002.23185
10008.91831
250055.97316
5000224.81226
10000893.46052
\$\endgroup\$
0

    1 Answer 1

    3
    \$\begingroup\$

    Advice I - Add missing main(String[])

    I suggest you add

    public static void main(String[] args) { new Cover(); } 

    Advice II - Can make the pixelDataprivate:

    You have the following:

    boolean[][] pixelData = new boolean[HEIGHT][WIDTH]; 

    I suggest you add the private keyword:

    private boolean[][] pixelData = new boolean[HEIGHT][WIDTH]; 

    Advice III - More verbose package names

    You have two packages: cover and utils. Why not:

    package com.zipity.image.coverart; 

    Advice IV - Get rid of parallel()

    First of all, each parallel stream goes through 100 pixels. That is too little in order to parallel() offer any efficiency gains. I tried to run your program with both parallel() and without it. Without it, the program was around 2+ times faster.

    Advice V - More verbose name of the main class

    I suggest you rename Cover.java to CoverGenerator.java or even more verbosely CoverArtGenerator.java.

    \$\endgroup\$
    2
    • \$\begingroup\$Thank you, I have adopted the changes - I always struggle with meaningful names. Also, intrigued by your comment, I started to benchmark the streams, and it's even worse than you said (I added the results to the post).\$\endgroup\$
      – Zipity
      CommentedApr 15, 2024 at 18:09
    • \$\begingroup\$I wonder if, somewhere in between the amount of OP's used parallels and zero parallels, there might be a sweet spot where using one parallel would actually speed up the performance\$\endgroup\$CommentedApr 19, 2024 at 7:30

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.