From e09709ca9dbf5f65ebf74d04ec926f6df78b9d3a Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Fri, 2 Aug 2013 19:08:04 +0200 Subject: [PATCH 1/3] Improved runtime of color bleeding from O(n^2) to O(n) The speedup brings the runtime down from several minutes to a few seconds. Instead of starting over in each iteration an efficient breadth-first search is done. Initially, only those fully transparent pixels are added to "pending" that have at least one neighbor which is not a fully transparent pixel. This is called the "border". This border is then subsequently extended/shifted in each iteration such that only relevant pixels are considered. In the old algorithm pixels that had only fully transparent neighbors were considered again and again in each iteration until the border finally reached those pixels. Another optimization was done related to reading/writing the rgb array of BufferedImage. --- .../imageprocessing/ColorBleedingEffect.java | 116 +++++++++++++----- 1 file changed, 82 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java b/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java index cafb9e6..541e7b9 100644 --- a/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java +++ b/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java @@ -1,7 +1,8 @@ package com.gemserk.utils.imageprocessing; import java.awt.image.BufferedImage; -import java.util.NoSuchElementException; +import java.util.HashSet; +import java.util.Set; import com.gemserk.utils.imageprocessing.ColorBleedingEffect.Mask.MaskIterator; @@ -10,6 +11,8 @@ public class ColorBleedingEffect { public static int TOPROCESS = 0; public static int INPROCESS = 1; public static int REALDATA = 2; + + public static int[][] offsets = { { -1, -1 }, { 0, -1 }, { 1, -1 }, { -1, 0 }, { 1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 } }; public BufferedImage processImage(BufferedImage image) { return processImage(image, Integer.MAX_VALUE); @@ -19,55 +22,60 @@ public BufferedImage processImage(BufferedImage image, int maxIterations) { int width = image.getWidth(); int height = image.getHeight(); - - BufferedImage processedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - - int[] rgb = image.getRGB(0, 0, width, height, null, 0, width); - // int[] mask = new int[rgb.length]; - - Mask mask = new Mask(rgb); + + int[] rgb; + if (image.getType() == BufferedImage.TYPE_INT_ARGB) { + // fast shortcut + rgb = (int[]) image.getRaster().getDataElements(0, 0, width, height, null); + } else { + rgb = image.getRGB(0, 0, width, height, null, 0, width); + } + + Mask mask = new Mask(rgb, width, height); int iterations = 0; - int lastPending = -1; while (mask.getPendingSize() > 0) { if (iterations >= maxIterations) { System.out.println("DEBUG: Reached max iterations"); break; } - - lastPending = mask.getPendingSize(); + executeIteration(rgb, mask, width, height); iterations++; - - if (mask.getPendingSize() == lastPending) { - System.out.println("WARN: Infinite loop detected ABORT ABORT ABORT EXTERMINATE"); - break; - } } - System.out.println("Procesed image in " + iterations + " iterations"); - - processedImage.setRGB(0, 0, width, height, rgb, 0, width); + System.out.println("Processed image in " + iterations + " iterations"); + BufferedImage processedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + // as the array matches the return image type, we can write directly (and faster) + processedImage.getRaster().setDataElements(0, 0, width, height, rgb); + return processedImage; } - private int getPixelIndex(int width, int x, int y) { + private static int getPixelIndex(int width, int x, int y) { return y * width + x; } SimpleColor simpleColor = new SimpleColor(0); public static class Mask { + int[] rgb; int[] data; int[] pending; int pendingSize = 0; int[] changing; int changingSize; SimpleColor simpleColor = new SimpleColor(0); + + final int width, height; - public Mask(int[] rgb) { + public Mask(int[] rgb, int width, int height) { + this.rgb = rgb; + this.width = width; + this.height = height; data = new int[rgb.length]; pending = new int[rgb.length]; changing = new int[rgb.length]; @@ -77,8 +85,29 @@ public Mask(int[] rgb) { simpleColor.setRGB(pixel); if (simpleColor.getAlpha() == 0) { data[i] = TOPROCESS; - pending[pendingSize] = i; - pendingSize++; + + int x = i % width; + int y = i / width; + + for (int j = 0; j < offsets.length; j++) { + int[] offset = offsets[j]; + int column = x + offset[0]; + int row = y + offset[1]; + + if (column < 0 || column >= width || row < 0 || row >= height) + continue; + + int currentPixelIndex = getPixelIndex(width, column, row); + int pixelData = rgb[currentPixelIndex]; + simpleColor.setRGB(pixelData); + if (simpleColor.getAlpha() != 0) { + // only add to pending if at least one opaque pixel as neighbor + pending[pendingSize] = i; + pendingSize++; + break; + } + } + } else { data[i] = REALDATA; } @@ -93,10 +122,7 @@ public int getMask(int index) { return data[index]; } - private int removeIndex(int index) { - if (index >= pendingSize) - throw new IndexOutOfBoundsException(String.valueOf(index)); - + private int removeFromPending(int index) { int value = pending[index]; pendingSize--; pending[index] = pending[pendingSize]; @@ -107,10 +133,10 @@ public MaskIterator iterator() { return new MaskIterator(this); } - static public class MaskIterator { + public static class MaskIterator { int index; private final Mask mask; - + public MaskIterator(Mask mask) { this.mask = mask; } @@ -120,23 +146,47 @@ public boolean hasNext() { } public int next() { - if (index >= mask.pendingSize) - throw new NoSuchElementException(String.valueOf(index)); return mask.pending[index++]; } public void markAsInProgress() { index--; - int removed = mask.removeIndex(index); + int removed = mask.removeFromPending(index); mask.changing[mask.changingSize] = removed; mask.changingSize++; } public void reset() { + assert mask.pendingSize == 0; + + // used for duplicate checking of new border pixels + Set pending = new HashSet(mask.changingSize * 2); + index = 0; for (int i = 0; i < mask.changingSize; i++) { int index = mask.changing[i]; mask.data[index] = REALDATA; + + int x = index % mask.width; + int y = index / mask.width; + + // find neighbors and add to pending + for (int j = 0; j < offsets.length; j++) { + int[] offset = offsets[j]; + int column = x + offset[0]; + int row = y + offset[1]; + + if (column < 0 || column >= mask.width || row < 0 || row >= mask.height) + continue; + + int currentPixelIndex = getPixelIndex(mask.width, column, row); + if (mask.getMask(currentPixelIndex) == TOPROCESS) { + if (pending.add(currentPixelIndex)) { + mask.pending[mask.pendingSize] = currentPixelIndex; + mask.pendingSize++; + } + } + } } mask.changingSize = 0; } @@ -145,7 +195,6 @@ public void reset() { } private void executeIteration(int[] rgb, Mask mask, int width, int height) { - int[][] offsets = { { -1, -1 }, { 0, -1 }, { 1, -1 }, { -1, 0 }, { 1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 } }; MaskIterator iterator = mask.iterator(); while (iterator.hasNext()) { @@ -168,7 +217,6 @@ private void executeIteration(int[] rgb, Mask mask, int width, int height) { continue; int currentPixelIndes = getPixelIndex(width, column, row); - // System.out.println("" + column + "," + row); int pixelData = rgb[currentPixelIndes]; simpleColor.setRGB(pixelData); if (mask.getMask(currentPixelIndes) == REALDATA) { From f98083adac8ffea83ff73c67b7aaa03d2b7cc8c6 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Sun, 4 Aug 2013 22:53:12 +0200 Subject: [PATCH 2/3] Improved constant runtime of mask initialization When a fully transparent pixel only has fully transparent pixels around it, then this information is saved into a boolean and reused for the following pixel. This allows to reduce the number of lookups of bordering pixels to 3 (the 3 right ones) instead of all 8. On my machine around 100ms got saved with this method for a texture that had a lot of fully transparent pixels. --- .../imageprocessing/ColorBleedingEffect.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java b/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java index 541e7b9..443023b 100644 --- a/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java +++ b/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java @@ -70,6 +70,8 @@ public static class Mask { int changingSize; SimpleColor simpleColor = new SimpleColor(0); + final static int[][] offsetsRight = { { 1, -1 }, { 1, 0 }, { 1, 1 } }; + final int width, height; public Mask(int[] rgb, int width, int height) { @@ -79,6 +81,13 @@ public Mask(int[] rgb, int width, int height) { data = new int[rgb.length]; pending = new int[rgb.length]; changing = new int[rgb.length]; + + /** + * Remembers whether all border pixels of the previous pixel were fully transparent. + * This allows a speedup for the following pixel as only the right-most border pixels + * of it have to be checked (instead of re-checking previous pixels). + */ + boolean allTransparent = false; for (int i = 0; i < rgb.length; i++) { int pixel = rgb[i]; @@ -89,8 +98,15 @@ public Mask(int[] rgb, int width, int height) { int x = i % width; int y = i / width; - for (int j = 0; j < offsets.length; j++) { - int[] offset = offsets[j]; + if ((i-1) / width != y) { + allTransparent = false; + } + + int[][] offsetsToCheck = allTransparent ? offsetsRight : offsets; + + allTransparent = true; + for (int j = 0; j < offsetsToCheck.length; j++) { + int[] offset = offsetsToCheck[j]; int column = x + offset[0]; int row = y + offset[1]; @@ -104,12 +120,14 @@ public Mask(int[] rgb, int width, int height) { // only add to pending if at least one opaque pixel as neighbor pending[pendingSize] = i; pendingSize++; + allTransparent = false; break; } } } else { data[i] = REALDATA; + allTransparent = false; } } } From 9f3cbdbbd2afb27e4a96b395b0f643f2638692fd Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Sun, 4 Aug 2013 23:27:44 +0200 Subject: [PATCH 3/3] Removed unnecessary check The iteration only loops pixels which have at least one non-fully-transparent pixel. --- .../utils/imageprocessing/ColorBleedingEffect.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java b/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java index 443023b..d36dfe0 100644 --- a/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java +++ b/src/main/java/com/gemserk/utils/imageprocessing/ColorBleedingEffect.java @@ -245,11 +245,9 @@ private void executeIteration(int[] rgb, Mask mask, int width, int height) { } } - if (cant != 0) { - simpleColor.setRGB(r / cant, g / cant, b / cant, 0); - rgb[pixelIndex] = simpleColor.getRGB(); - iterator.markAsInProgress(); - } + simpleColor.setRGB(r / cant, g / cant, b / cant, 0); + rgb[pixelIndex] = simpleColor.getRGB(); + iterator.markAsInProgress(); } iterator.reset();