This question was mentioned in The 2nd Monitor as one which could use some more input. I looked at it, and decided that the Android aspects are not where my expertise lies, but then, when I looked further, I found there are other things to consider too.
First up, I am going to assume that the android BitMap
and Color
classes can be loosely represented with the AWT BufferedImage
and Color
classes too. This is a stretch, I know, and it is something you need to take in to consideration when you read my answer. But, I assume the similarities are close enough for "engineering purposes".
Now, I took your code, and "redid" it in AWT constructs. It looks something like:
public class PlayHSV {
// process every pixel and transform the Value to a fixed magnitude.
private static void setHSV(final int[] pixels, final float value) {
for (int i = 0; i < pixels.length; i++) {
pixels[i] = transformAWT(pixels[i], value);
}
}
// Using AWT tools to compute the HSV (HSB in AWT terms - B for Brightness)
private static int transformAWT(final int color, final float bright) {
float[] hsbvals = Color.RGBtoHSB((color >>> 16) & 0xff, (color >>> 8) & 0xff, color & 0xff, null);
return Color.HSBtoRGB(hsbvals[0], hsbvals[1], bright);
}
// Main method to process input files to an output folder.
// includes some detailed timing.
public static void main(String[] args) throws IOException {
File outdir = new File("output");
outdir.mkdirs();
for (String a : args) {
long start = System.nanoTime();
File file = new File(a);
System.out.println("Processing " + file);
BufferedImage image = loadImage(file);
long loadtime = System.nanoTime();
final int width = image.getWidth();
final int height = image.getHeight();
int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
long pixtime = System.nanoTime();
setHSV(pixels, 1.0f);
long transtime = System.nanoTime();
image.setRGB(0, 0, width, height, pixels, 0, width);
long applytime = System.nanoTime();
File ofile = new File(outdir, file.getName() + ".mod.jpg");
ImageIO.write(image, "jpeg", ofile);
long donetime = System.nanoTime();
System.out.printf("Dimensions %d (%d x %d)\nTimes:\n Load %.3fms\n Pixelate %.3fms\n "
+ "Transform %.3fms\n Apply %.3fms\n Write %.3fms\n Total %.3fms\n PerMegapix %.5fms\n",
width * height,
width,
height,
millis(loadtime - start),
millis(pixtime - loadtime),
millis(transtime - pixtime),
millis(applytime - transtime),
millis(donetime - applytime),
millis(donetime - start),
millis(transtime - pixtime) / ((width * height) / (1024.0 * 1024.0)));
}
}
private static BufferedImage loadImage(File file) throws IOException {
return ImageIO.read(file);
}
private static double millis(long nanos) {
return nanos / 1000000.0;
}
}
Note that I have a main method which processes image files from the arguments, and reports on their timing of various stages. Specifically, I report the time to load the image, the time to extract the array of pixels, the time to transform the pixels, the time to write the transformed pixels back, and finally the time to write the image back to disk.
Note here, that the times for a large picture that I have on my computer, are long:
Processing /home/rolf/Pictures/Img20130710-115003_30_Rolf.jpg
Dimensions 36152320 (7360 x 4912)
Times:
Load 1219.637ms
Pixelate 1508.946ms
Transform 1282.783ms
Apply 1755.958ms
Write 1199.912ms
Total 6967.236ms
PerMegapix 37.20635ms
That is a 36 megapixel image, and it takes 7.0 seconds to process. The transformation of those pixels takes 1.3 seconds at a rate of 37.2ms per megapixel.
A small file on the other hand, looks like:
Processing /home/rolf/Pictures/TooChatty.png
Dimensions 114798 (1007 x 114)
Times:
Load 5.797ms
Pixelate 8.461ms
Transform 1.428ms
Apply 5.066ms
Write 9.737ms
Total 30.489ms
PerMegapix 13.04379ms
Again, though, note that the total time was 30ms, and the actual transformation time was only 1.5ms.
In your situation, you are not reading, or writing the file to disk, but you are extracting and re-applying full arrays of pixel values, and, in my estimates, that is about 75% or more of the time.
Or, put another way, and relating it back to your code, here's your method, and I will apply estimates of timing for that:
// the following 2 lines will take about 40% of the time
int[] rgbs = new int[width * height];
bitmap.getPixels(rgbs, 0, width, 0, 0, width, height);
// the for loop will take about 20% of the time.
for(int rgbi = 0; rgbi < rgbs.length; rgbi++)
{
float[] hsv = new float[3];
Color.colorToHSV(rgbs[rgbi], hsv);
hsv[2] = newV;
rgbs[rgbi] = Color.HSVToColor(hsv);
}
// the createBitmap will take about 40% of the time.
return Bitmap.createBitmap(rgbs, width, height, Bitmap.Config.ARGB_8888);
Now, those numbers are scary..... even if you do a no-operation transformation, you will save only 20% of your time...
That's the answer to your major question:
I would like to know if there is a faster way, as this is taking in
the range of 30 seconds to process a 1920 x 1080 pixels image on an LG
D802 phone. I'd like to get it down to the 1 second range, or to know
if that's not possible.
No, it's not possible using the system you do, to get it down to 1 second.... of course, though, I strongly suggest you add some timing information to the Android version to check that the actual times are about what I say they will be.
On the other hand, there are 4 things I can recommend you consider when evaluating this issue:
do you need to do the transfomation at all?
is there a native implementation that can help - perhaps one which uses a lot of parallelism to process the data in different chunks? I am sorry I can't be more helpful in that aspect.
Can you do things a pixel at a time instead of pulling the whole int array (I tried this in Java AWT, and it was slower....). I used this code:
private static void transform(final BufferedImage image, final float value) {
final int width = image.getWidth();
final int height = image.getHeight();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
image.setRGB(x, y, transformAWT(image.getRGB(x, y), value));
}
}
}
improve the performance of what you have.
That last point is an interesting one to consider. Can your for-loop be faster? There are a few things I would recommend.
Move the float[] hsv
array outside the loop, and make it final. Reusing the same array may help prevent excessive collection of it. Try that, and see.
Convert the inner part of the loop to a function call. In android it may not make much of a difference but in Java, with JIT compilation, it may get compiled down faster/better as it is called more often. The inlined version will be just as good.
I then had a look at what the actual RGB->HSV and the reverse HSV->RGB transformation does, and I decided that by re-coding it, you can save some time by eliminating steps that the fixed-magnitude value allows. So, I re-coded the following to see if it would be faster, as it does less work. Consider the following (messy) code that does a fixed-v transform:
// Transform an input ARGB value to an ARGB color
// where the color is first transformed to HSV format,
// and then the V component is coerced to be the input value
// before transforming the HSV back to RGB. The Alpha component
// of the input value is copied to the output value.
public static final int transform(final int color, final float value) {
// The following code is largely based on the following web-page
// http://www.cs.rit.edu/~ncs/color/t_convert.html
// It has been "tuned" to allow shorter-paths for the fixed-value transform.
// additionally, it merges the to-HSV and from-HSV functions, and it skips some
// redundant steps since the intermediate values are not always important.
// In addition, it has been "ported" to Java, and variables renamed to be clearer, etc.
if ((color & 0xffffff) == 0) {
// black.
return color;
}
final float r = toFloat(color >>> 16);
final float g = toFloat(color >> 8);
final float b = toFloat(color);
final float max = max(r, g, b);
final float min = min(r, g, b);
final float delta = max - min;
if (delta == 0.0f) {
// grey - no saturation.
int val = (int)(max * 255.0f);
return val | (val << 8) | (val << 16);
}
final float saturation = delta / max;
final float tempHue = r == max ? hue(0.0f, g, b, delta) : (g == max ? hue(2.0f, b, r, delta) : hue(4.0f, r, g, delta));
final float hue = tempHue < 0 ? (tempHue + 6.0f) : tempHue;
// Right, original color converted to hue, sat, and we have the passed-in value.
// convert that back to an RGB.
// note that hue is normally in degrees (0..360), but in the above result, it is in 6 sectors.... (0.0 to 6.0)
final int sector = (int)hue;
final float fractional = hue - sector;
final float p = value * ( 1.0f - saturation );
final float q = value * ( 1.0f - saturation * fractional );
final float t = value * ( 1.0f - saturation * ( 1 - fractional ) );
final int alpha = color & 0xff000000;
switch (sector) {
case 0:
return toARGB(alpha, value, t, p);
case 1:
return toARGB(alpha, q, value, p);
case 2:
return toARGB(alpha, p, value, t);
case 3:
return toARGB(alpha, p, q, value);
case 4:
return toARGB(alpha, t, p, value);
case 5:
return toARGB(alpha, value, p, q);
}
throw new IllegalStateException("Unexpected sector " + sector);
}
private static final int toARGB(final int alpha, final float r, final float g, final float b) {
return alpha | (fromFloat(r) << 16) | (fromFloat(g) << 8) | fromFloat(b);
}
private static final int fromFloat(float r) {
return (int)(r * 255.0f);
}
private static final float toFloat(int value) {
return (value & 0xff) / 255.0f;
}
private static float hue(final float sectorBase, final float p, final float q, final float delta) {
return sectorBase + (p - q) / delta;
}
private static final float min(final float a, final float b) {
return a < b ? a : b;
}
private static final float min(final float r, final float g, final float b) {
return min(r, min(g, b));
}
private static final float max(final float a, final float b) {
return a > b ? a : b;
}
private static final float max(final float r, final float g, final float b) {
return max(r, max(g, b));
}
The above code shaves about 25% off the transformation time... My results look like:
Processing /home/rolf/Pictures/Img20130710-115003_30_Rolf.jpg
Dimensions 36152320 (7360 x 4912)
Times:
Load 1248.057ms
Pixelate 1515.967ms
Transform 1030.899ms
Apply 1750.349ms
Write 1203.219ms
Total 6748.491ms
PerMegapix 29.90059ms
It's debatable whether that code is worth it, but you may want to try it in your environment because it may compile a whole lot better (or worse....) on Android.