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:
Size | Sequential | Parallel |
---|---|---|
100 | 0.19389 | 0.69368 |
500 | 2.52478 | 18.44609 |
1000 | 9.83310 | 82.51152 |
2500 | 59.34847 | 528.95678 |
5000 | 226.03198 | Not tested |
10000 | 904.63635 | Not tested |
Streams created with the iterate() method:
Size | Sequential | Parallel |
---|---|---|
100 | 0.17763 | 0.90946 |
500 | 2.38955 | 20.80894 |
1000 | 8.73398 | 83.54772 |
2500 | 54.71999 | 519.07294 |
5000 | 226.25446 | Not tested |
10000 | 913.62030 | Not tested |
Iterative solution with an explicit loop:
Size | Sequential |
---|---|
100 | 0.15455 |
500 | 2.23185 |
1000 | 8.91831 |
2500 | 55.97316 |
5000 | 224.81226 |
10000 | 893.46052 |