How to compress Bitmap as JPEG with least quality loss on Android? - android

This is not a straightforward problem, please read through!
I want to manipulate a JPEG file and save it again as JPEG. The problem is that even without manipulation there's significant (visible) quality loss.
Question: what option or API am I missing to be able to re-compress JPEG without quality loss (I know it's not exactly possible, but I think what I describe below is not an acceptable level of artifacts, especially with quality=100).
Control
I load it as a Bitmap from the file:
BitmapFactory.Options options = new BitmapFactory.Options();
// explicitly state everything so the configuration is clear
options.inPreferredConfig = Config.ARGB_8888;
options.inDither = false; // shouldn't be used anyway since 8888 can store HQ pixels
options.inScaled = false;
options.inPremultiplied = false; // no alpha, but disable explicitly
options.inSampleSize = 1; // make sure pixels are 1:1
options.inPreferQualityOverSpeed = true; // doesn't make a difference
// I'm loading the highest possible quality without any scaling/sizing/manipulation
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/image.jpg", options);
Now, to have a control image to compare to, let's save the plain Bitmap bytes as PNG:
bitmap.compress(PNG, 100/*ignored*/, new FileOutputStream("/sdcard/image.png"));
I compared this to the original JPEG image on my computer and there's no visual difference.
I also saved the raw int[] from getPixels and loaded it as a raw ARGB file on my computer: there's no visual difference to the original JPEG, nor the PNG saved from Bitmap.
I checked the Bitmap's dimensions and config, they match the source image and the input options: it's decoded as ARGB_8888 as expected.
The above to control checks prove that the pixels in the in-memory Bitmap are correct.
Problem
I want to have JPEG files as a result, so the above PNG and RAW approaches wouldn't work, let's try to save as JPEG 100% first:
// 100% still expected lossy, but not this amount of artifacts
bitmap.compress(JPEG, 100, new FileOutputStream("/sdcard/image.jpg"));
I'm not sure its measure is percent, but it's easier to read and discuss, so I'm gonna use it.
I'm aware that JPEG with the quality of 100% is still lossy, but it shouldn't be so visually lossy that it's noticeable from afar. Here's a comparison of two 100% compressions of the same source.
Open them in separate tabs and click back and forth between to see what I mean. The difference images were made using Gimp: original as bottom layer, re-compressed middle layer with "Grain extract" mode, top layer full white with "Value" mode to enhance badness.
The below images are uploaded to Imgur which also compresses the files, but since all of the images are compressed the same, the original unwanted artifacts remain visible the same way I see it when opening my original files.
Original [560k]:
Imgur's difference to original (not relevant to problem, just to show that it's not causing any extra artifacts when uploading the images):
IrfanView 100% [728k] (visually identical to original):
IrfanView 100%'s difference to original (barely anything)
Android 100% [942k]:
Android 100%'s difference to original (tinting, banding, smearing)
In IrfanView I have to go below 50% [50k] to see remotely similar effects. At 70% [100k] in IrfanView there's no noticable difference, but the size is 9th of Android's.
Background
I created an app that takes a picture from Camera API, that image comes as a byte[] and is an encoded JPEG blob. I saved this file via OutputStream.write(byte[]) method, that was my original source file. decodeByteArray(data, 0, data.length, options) decodes the same pixels as reading from a File, tested with Bitmap.sameAs so it's irrelevant to the issue.
I was using my Samsung Galaxy S4 with Android 4.4.2 to test things out.
Edit: while investigating further I also tried Android 6.0 and N preview emulators and they reproduce the same issue.

After some investigation I found the culprit: Skia's YCbCr conversion. Repro, code for investigation and solutions can be found at TWiStErRob/AndroidJPEG.
Discovery
After not getting a positive response on this question (neither from http://b.android.com/206128) I started digging deeper. I found numerous half-informed SO answers which helped me tremendously in discovering bits and pieces. One such answer was https://stackoverflow.com/a/13055615/253468 which made me aware of YuvImage which converts an YUV NV21 byte array into a JPEG compressed byte array:
YuvImage yuv = new YuvImage(yuvData, ImageFormat.NV21, width, height, null);
yuv.compressToJpeg(new Rect(0, 0, width, height), 100, jpeg);
There's a lot of freedom going into creating the YUV data, with varying constants and precision. From my question it's clear that Android uses an incorrect algorithm.
While playing around with the algorithms and constants I found online I always got a bad image: either the brightness changed or had the same banding issues as in the question.
Digging deeper
YuvImage is actually not used when calling Bitmap.compress, here's the stack for Bitmap.compress:
libjpeg/jpeg_write_scanlines(jcapistd.c:77)
skia/rgb2yuv_32(SkImageDecoder_libjpeg.cpp:913)
skia/writer(=Write_32_YUV).write(SkImageDecoder_libjpeg.cpp:961)
[WE_CONVERT_TO_YUV is unconditionally defined]
SkJPEGImageEncoder::onEncode(SkImageDecoder_libjpeg.cpp:1046)
SkImageEncoder::encodeStream(SkImageEncoder.cpp:15)
Bitmap_compress(Bitmap.cpp:383)
Bitmap.nativeCompress(Bitmap.java:1573)
Bitmap.compress(Bitmap.java:984)
app.saveBitmapAsJPEG()
and the stack for using YuvImage
libjpeg/jpeg_write_raw_data(jcapistd.c:120)
YuvToJpegEncoder::compress(YuvToJpegEncoder.cpp:71)
YuvToJpegEncoder::encode(YuvToJpegEncoder.cpp:24)
YuvImage_compressToJpeg(YuvToJpegEncoder.cpp:219)
YuvImage.nativeCompressToJpeg(YuvImage.java:141)
YuvImage.compressToJpeg(YuvImage.java:123)
app.saveNV21AsJPEG()
By using the constants in rgb2yuv_32 from the Bitmap.compress flow I was able to recreate the same banding effect using YuvImage, not an achievement, just a confirmation that it's indeed the YUV conversion that is messed up. I double-checked that the problem is not during YuvImage calling libjpeg: by converting the Bitmap's ARGB to YUV and back to RGB then dumping the resulting pixel blob as a raw image, the banding was already there.
While doing this I realized that the NV21/YUV420SP layout is lossy as it samples the color information every 4th pixel, but it keeps the value (brightness) of each pixel which means that some color info is lost, but most of the info for people's eyes are in the brightness anyway. Take a look at the example on wikipedia, the Cb and Cr channel makes barely recognisable images, so lossy sampling on it doesn't matter much.
Solution
So, at this point I knew that libjpeg does the right conversion when it is passed the right raw data. This is when I set up the NDK and integrated the latest LibJPEG from http://www.ijg.org. I was able to confirm that indeed passing the RGB data from the Bitmap's pixels array yields the expected result. I like to avoid using native components when not absolutely necessary, so aside of going for a native library that encodes a Bitmap I found a neat workaround. I've essentially taken the rgb_ycc_convert function from jcolor.c and rewrote it in Java using the skeleton from https://stackoverflow.com/a/13055615/253468. The below is not optimized for speed, but readability, some constants were removed for brevity, you can find them in libjpeg code or my example project.
private static final int JSAMPLE_SIZE = 255 + 1;
private static final int CENTERJSAMPLE = 128;
private static final int SCALEBITS = 16;
private static final int CBCR_OFFSET = CENTERJSAMPLE << SCALEBITS;
private static final int ONE_HALF = 1 << (SCALEBITS - 1);
private static final int[] rgb_ycc_tab = new int[TABLE_SIZE];
static { // rgb_ycc_start
for (int i = 0; i <= JSAMPLE_SIZE; i++) {
rgb_ycc_tab[R_Y_OFFSET + i] = FIX(0.299) * i;
rgb_ycc_tab[G_Y_OFFSET + i] = FIX(0.587) * i;
rgb_ycc_tab[B_Y_OFFSET + i] = FIX(0.114) * i + ONE_HALF;
rgb_ycc_tab[R_CB_OFFSET + i] = -FIX(0.168735892) * i;
rgb_ycc_tab[G_CB_OFFSET + i] = -FIX(0.331264108) * i;
rgb_ycc_tab[B_CB_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[R_CR_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[G_CR_OFFSET + i] = -FIX(0.418687589) * i;
rgb_ycc_tab[B_CR_OFFSET + i] = -FIX(0.081312411) * i;
}
}
static void rgb_ycc_convert(int[] argb, int width, int height, byte[] ycc) {
int[] tab = LibJPEG.rgb_ycc_tab;
final int frameSize = width * height;
int yIndex = 0;
int uvIndex = frameSize;
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int r = (argb[index] & 0x00ff0000) >> 16;
int g = (argb[index] & 0x0000ff00) >> 8;
int b = (argb[index] & 0x000000ff) >> 0;
byte Y = (byte)((tab[r + R_Y_OFFSET] + tab[g + G_Y_OFFSET] + tab[b + B_Y_OFFSET]) >> SCALEBITS);
byte Cb = (byte)((tab[r + R_CB_OFFSET] + tab[g + G_CB_OFFSET] + tab[b + B_CB_OFFSET]) >> SCALEBITS);
byte Cr = (byte)((tab[r + R_CR_OFFSET] + tab[g + G_CR_OFFSET] + tab[b + B_CR_OFFSET]) >> SCALEBITS);
ycc[yIndex++] = Y;
if (y % 2 == 0 && index % 2 == 0) {
ycc[uvIndex++] = Cr;
ycc[uvIndex++] = Cb;
}
index++;
}
}
}
static byte[] compress(Bitmap bitmap) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] argb = new int[w * h];
bitmap.getPixels(argb, 0, w, 0, 0, w, h);
byte[] ycc = new byte[w * h * 3 / 2];
rgb_ycc_convert(argb, w, h, ycc);
argb = null; // let GC do its job
ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
YuvImage yuvImage = new YuvImage(ycc, ImageFormat.NV21, w, h, null);
yuvImage.compressToJpeg(new Rect(0, 0, w, h), quality, jpeg);
return jpeg.toByteArray();
}
The magic key seems to be ONE_HALF - 1 the rest looks an awful lot like the math in Skia. That's a good direction for future investigation, but for me the above is sufficiently simple to be a good solution for working around Android's builtin weirdness, albeit slower. Note that this solution uses the NV21 layout which loses 3/4 of the color info (from Cr/Cb), but this loss is much less than the errors created by Skia's math. Also note that YuvImage doesn't support odd-sized images, for more info see NV21 format and odd image dimensions.

Please use the following method:
public String convertBitmaptoSmallerSizetoString(String image){
File imageFile = new File(image);
Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath());
int nh = (int) (bitmap.getHeight() * (512.0 / bitmap.getWidth()));
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, 512, nh, true);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
scaled.compress(Bitmap.CompressFormat.PNG, 90, stream);
byte[] imageByte = stream.toByteArray();
String img_str = Base64.encodeToString(imageByte, Base64.NO_WRAP);
return img_str;
}

Below is my Code:
public static String compressImage(Context context, String imagePath)
{
final float maxHeight = 1024.0f;
final float maxWidth = 1024.0f;
Bitmap scaledBitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(imagePath, options);
int actualHeight = options.outHeight;
int actualWidth = options.outWidth;
float imgRatio = (float) actualWidth / (float) actualHeight;
float maxRatio = maxWidth / maxHeight;
if (actualHeight > maxHeight || actualWidth > maxWidth) {
if (imgRatio < maxRatio) {
imgRatio = maxHeight / actualHeight;
actualWidth = (int) (imgRatio * actualWidth);
actualHeight = (int) maxHeight;
} else if (imgRatio > maxRatio) {
imgRatio = maxWidth / actualWidth;
actualHeight = (int) (imgRatio * actualHeight);
actualWidth = (int) maxWidth;
} else {
actualHeight = (int) maxHeight;
actualWidth = (int) maxWidth;
}
}
options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight);
options.inJustDecodeBounds = false;
options.inDither = false;
options.inPurgeable = true;
options.inInputShareable = true;
options.inTempStorage = new byte[16 * 1024];
try {
bmp = BitmapFactory.decodeFile(imagePath, options);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
try {
scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.RGB_565);
} catch (OutOfMemoryError exception) {
exception.printStackTrace();
}
float ratioX = actualWidth / (float) options.outWidth;
float ratioY = actualHeight / (float) options.outHeight;
float middleX = actualWidth / 2.0f;
float middleY = actualHeight / 2.0f;
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);
assert scaledBitmap != null;
Canvas canvas = new Canvas(scaledBitmap);
canvas.setMatrix(scaleMatrix);
canvas.drawBitmap(bmp, middleX - bmp.getWidth() / 2, middleY - bmp.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));
if (bmp != null) {
bmp.recycle();
}
ExifInterface exif;
try {
exif = new ExifInterface(imagePath);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
Matrix matrix = new Matrix();
if (orientation == 6) {
matrix.postRotate(90);
} else if (orientation == 3) {
matrix.postRotate(180);
} else if (orientation == 8) {
matrix.postRotate(270);
}
scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
} catch (IOException e) {
e.printStackTrace();
}
FileOutputStream out = null;
String filepath = getFilename(context);
try {
out = new FileOutputStream(filepath);
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 80, out);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return filepath;
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
final float totalPixels = width * height;
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
return inSampleSize;
}
public static String getFilename(Context context) {
File mediaStorageDir = new File(Environment.getExternalStorageDirectory()
+ "/Android/data/"
+ context.getApplicationContext().getPackageName()
+ "/Files/Compressed");
if (!mediaStorageDir.exists()) {
mediaStorageDir.mkdirs();
}
String mImageName = "IMG_" + String.valueOf(System.currentTimeMillis()) + ".jpg";
return (mediaStorageDir.getAbsolutePath() + "/" + mImageName);
}

Related

Android TensorFlow - prevent resize Image

I am working on TensorFlow stylize image. But, the problem I am facing is that it resize my actual image. I want to apply style on whole image itself. For example, if my image resolution is 1280x960, it should be the same after I apply style on it.
I am not using default INPUT_SIZE value 256. Using default value it works fine. Here is my code I am using to prevent resize image.
private TensorFlowInferenceInterface inferenceInterface;
private void applyStyle(){
inferenceInterface = new TensorFlowInferenceInterface(mActivity.getAssets(), "bossK_float.pb");
Bitmap bitmap = getBitmapFromPath();
bitmap=Bitmap.createBitmap(bitmap,0,bitmap.getWidth(),bitmap.getHeight(), matrix, true);
INPUT_SIZE_WIDTH = bitmap.getWidth();
INPUT_SIZE_HEIGHT = bitmap.getHeight();
mStyledBitmap = stylizeImage(bitmap);
}
private Bitmap stylizeImage(Bitmap bitmap) {
Bitmap scaledBitmap = scaleBitmap(bitmap, INPUT_SIZE_WIDTH, INPUT_SIZE_HEIGHT);
intValues = new int[INPUT_SIZE_WIDTH * INPUT_SIZE_HEIGHT];
floatValues = new float[INPUT_SIZE_WIDTH * INPUT_SIZE_HEIGHT * 3];
scaledBitmap.getPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
scaledBitmap = scaledBitmap.copy(Bitmap.Config.ARGB_8888, true);
for (int i = 0; i < intValues.length; ++i) {
final int val = intValues[i];
floatValues[i * 3 + 0] = ((val >> 16) & 0xFF) * 1.0f;
floatValues[i * 3 + 1] = ((val >> 8) & 0xFF) * 1.0f;
floatValues[i * 3 + 2] = (val & 0xFF) * 1.0f;
}
Trace.beginSection("feed");
inferenceInterface.feed(INPUT_NAME, floatValues, INPUT_SIZE_WIDTH, INPUT_SIZE_HEIGHT, 3);
Trace.endSection();
Trace.beginSection("run");
inferenceInterface.run(new String[]{OUTPUT_NAME});
Trace.endSection();
Trace.beginSection("fetch");
inferenceInterface.fetch(OUTPUT_NAME, floatValues);
Trace.endSection();
for (int i = 0; i < intValues.length; ++i) {
intValues[i] =
0xFF000000
| (((int) (floatValues[i * 3 + 0])) << 16)
| (((int) (floatValues[i * 3 + 1])) << 8)
| ((int) (floatValues[i * 3 + 2]));
}
scaledBitmap.setPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
return scaledBitmap;
}
private Bitmap scaleBitmap(Bitmap origin, int newWidth, int newHeight) {
if (origin == null) {
return null;
}
int height = origin.getHeight();
int width = origin.getWidth();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
Bitmap newBitmap = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
return newBitmap;
}
When I change my INPUT_SIZE values to INPUT_SIZE_WIDTH and INPUT_SIZE_HEIGHT, my application stops without error message. I debug this code, but it stucks on this piece of code and stop my app:
Trace.beginSection("run");
inferenceInterface.run(new String[]{OUTPUT_NAME});
Trace.endSection();
Please let me know, how can I style whole image using TensorFlow.
Thank You!
Your code stops there because of the differences in size. You probably must be getting an ArrayOutOfBound Exception.
The model is to be trained to accept images of a particular size. So, whenever you classify, the image is to be reduced to that particular size.
Even your training data which when creating a pb/lite/tflite file will be converted to accept the same size images you mention within the model creation. The results will not affect to a larger extinct. You can give that a try.

How to reduce an Image file size

I use this function to reduce the size of image before uploading it,But using below method my file size is increasing
Before use below code my file size---> 157684
after using this code my file size ----->177435
Can some one help me please how can i reduce file size before upload to server
code:
public File saveBitmapToFile(File file){
try {
// BitmapFactory options to downsize the image
BitmapFactory.Options o = new BitmapFactory.Options();
o.inJustDecodeBounds = true;
o.inSampleSize = 6;
// factor of downsizing the image
FileInputStream inputStream = new FileInputStream(file);
//Bitmap selectedBitmap = null;
BitmapFactory.decodeStream(inputStream, null, o);
inputStream.close();
// The new size we want to scale to
final int REQUIRED_SIZE=75;
// Find the correct scale value. It should be the power of 2.
int scale = 1;
while(o.outWidth / scale / 2 >= REQUIRED_SIZE &&
o.outHeight / scale / 2 >= REQUIRED_SIZE) {
scale *= 2;
}
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
inputStream = new FileInputStream(file);
Bitmap selectedBitmap = BitmapFactory.decodeStream(inputStream, null, o2);
inputStream.close();
// here i override the original image file
file.createNewFile();
FileOutputStream outputStream = new FileOutputStream(file);
selectedBitmap.compress(Bitmap.CompressFormat.JPEG, 100 , outputStream);
return file;
} catch (Exception e) {
return null;
}
}
This what I use to reduce my image size without compressing :
public static Bitmap getResizedBitmap(Bitmap bitmap, int newWidth, int newHeight) {
Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
float ratioX = newWidth / (float) bitmap.getWidth();
float ratioY = newHeight / (float) bitmap.getHeight();
float middleX = newWidth / 2.0f;
float middleY = newHeight / 2.0f;
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);
Canvas canvas = new Canvas(scaledBitmap);
canvas.setMatrix(scaleMatrix);
canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2, middleY - bitmap.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));
return scaledBitmap;
}
You just need to enter correct new height and width to fit your needs
We want to make thumbnail of an image, so we need to first take the ByteArrayOutputStream and then pass it into Bitmap.compress() method.
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
youBitmapImage.compress(Bitmap.CompressFormat.JPEG, 90, bytes);
more about the function, from the docs
If your output file is larger:
it can mean that scale is wrong. And you save the file with 100% quality so it can grow
compression on the input file is extremely heavy and even though you scale it, using no compression on the output still generates a larger file
Change this line:
selectedBitmap.compress(Bitmap.CompressFormat.JPEG, 100 , outputStream);
to
int mCompressedSize = 50; // 0 is lowest and 100 original
selectedBitmap.compress(Bitmap.CompressFormat.PNG, mCompressedSize, outputStream);
Hope this will help.
Try
int compressionRatio = 2; //1 == originalImage, 2 = 50% compression, 4=25% compress
File file = new File (imageUrl);
try {
Bitmap bitmap = BitmapFactory.decodeFile (file.getPath ());
bitmap.compress (Bitmap.CompressFormat.JPEG, compressionRatio, new FileOutputStream (file));
}
catch (Throwable t) {
Log.e("ERROR", "Error compressing file." + t.toString ());
t.printStackTrace ();
}

How to reduce image size into 1MB

I want my application to upload image with no size limit, but in the code, I want to resize the image into 1MB if the image size exceeds. I have tried many ways but I couldn't find any code for the requirement I have mentioned above.
For once, I have tried this:
public void scaleDown() {
int width = stdImageBmp.getWidth();
int height = stdImageBmp.getHeight();
Matrix matrix = new Matrix();
float scaleWidth = ((float) MAX_WIDTH) / width;
float scaleHeight = ((float) MAX_HEIGHT) / height;
matrix.postScale(scaleWidth, scaleHeight);
stdImageBmp = Bitmap.createBitmap(stdImageBmp, 0, 0, width, height, matrix, true);
File Image = new File("path");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//compress bmp
stdImageBmp.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
imgViewStd.setImageBitmap(stdImageBmp);
Log.d("resizedBitmap", stdImageBmp.toString());
width = stdImageBmp.getWidth();
height = stdImageBmp.getHeight();
System.out.println("imgWidth" + width);
System.out.println("imgHeight" + height);
}
you can use this code to resize a bitmap and for image size < 1MB i recommend use resolution of 480x640
public Bitmap getResizedBitmap(Bitmap bm, int newWidth, int newHeight) {
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// CREATE A MATRIX FOR THE MANIPULATION
Matrix matrix = new Matrix();
// RESIZE THE BIT MAP
matrix.postScale(scaleWidth, scaleHeight);
// "RECREATE" THE NEW BITMAP
return Bitmap.createBitmap(
bm, 0, 0, width, height, matrix, false);
}
If you really want the Bitmap that scales down to the Bitmap that is the closest to a given amount of bytes, heres the method I use. (It does not uses a while loop)
NOTE: This method only works if passed bitmap is in ARGB_8888 configuration.
See: Compress bitmap to a specific byte size in Android for the conversion method.
/**
* Method to scale the Bitmap to respect the max bytes
*
* #param input the Bitmap to scale if too large
* #param maxBytes the amount of bytes the Image may be
* #return The scaled bitmap or the input if already valid
* #Note: The caller of this function is responsible for recycling once the input is no longer needed
*/
public static Bitmap scaleBitmap(final Bitmap input, final long maxBytes) {
final int currentWidth = input.getWidth();
final int currentHeight = input.getHeight();
final int currentPixels = currentWidth * currentHeight;
// Get the amount of max pixels:
// 1 pixel = 4 bytes (R, G, B, A)
final long maxPixels = maxBytes / 4; // Floored
if (currentPixels <= maxPixels) {
// Already correct size:
return input;
}
// Scaling factor when maintaining aspect ratio is the square root since x and y have a relation:
final double scaleFactor = Math.sqrt(maxPixels / (double) currentPixels);
final int newWidthPx = (int) Math.floor(currentWidth * scaleFactor);
final int newHeightPx = (int) Math.floor(currentHeight * scaleFactor);
Timber.i("Scaled bitmap sizes are %1$s x %2$s when original sizes are %3$s x %4$s and currentPixels %5$s and maxPixels %6$s and scaled total pixels are: %7$s",
newWidthPx, newHeightPx, currentWidth, currentHeight, currentPixels, maxPixels, (newWidthPx * newHeightPx));
final Bitmap output = Bitmap.createScaledBitmap(input, newWidthPx, newHeightPx, true);
return output;
}
Where the Sample use would look something like:
// (1 MB)
final long maxBytes = 1024 * 1024;
// Scale it
final Bitmap scaledBitmap = BitmapUtils.scaleBitmap(yourBitmap, maxBytes);
if(scaledBitmap != yourBitmap){
// Recycle the bitmap since we can use the scaled variant:
yourBitmap.recycle();
}
// ... do something with the scaled bitmap
I've tried to comment it but the comment become too big.
So, I've tested many solutions and it seems like there are only TWO solutions for this problem, which give some results.
Lets discuss Koen solution first. What it actually does is creates a scaled JPG
Bitmap.createScaledBitmap(input, newWidthPx, newHeightPx, true)
Seems like it does not compress it at all but just cuts off the resolution.
I've tested this code and when I pass MAX_IMAGE_SIZE = 1024000 it gives me 350kb compressed image out of 2.33Mb original image. Bug?
Also it lacks quality. I was unable to recognize a text on A4 sheet of paper photo made by Google Pixel.
There is another solution to this problem, which gives good quality, but lacks in speed.
A WHILE LOOP!
Basically you just loop through image size, until you get the desired size
private fun scaleBitmap() {
if (originalFile.length() > MAX_IMAGE_SIZE) {
var streamLength = MAX_IMAGE_SIZE
var compressQuality = 100
val bmpStream = ByteArrayOutputStream()
while (streamLength >= MAX_IMAGE_SIZE) {
bmpStream.use {
it.flush()
it.reset()
}
compressQuality -= 8
val bitmap = BitmapFactory.decodeFile(originalFile.absolutePath, BitmapFactory.Options())
bitmap.compress(Bitmap.CompressFormat.JPEG, compressQuality, bmpStream)
streamLength = bmpStream.toByteArray().size
}
FileOutputStream(compressedFile).use {
it.write(bmpStream.toByteArray())
}
}
}
I think that this approach will consume exponential time depending on image resolution.
9mb image takes up to 12 seconds to compress down to 1mb.
Quality is good.
You can tweak this by reducing the original bitmap resolution(which seems like a constant operation), by doing:
options.inSampleSize = 2;
What we need to do is to somehow calculate compressQuality for any image.
There should be a math around this, so we can determinate compressQuality from original image size or width + height.

How to add texture from android to unity?

I know I can get it with Unity with these lines:
Texture2D texture = Resources.Load ("TextureTest") as Texture2D;
GameObject obPlane = GameObject.Find("Plane");
obPlane.renderer.material.mainTexture = texture ;
But it's not my goal. I want to do it from Android to Unity.
I've tried (in Java):
private boolean updateTexture(int IDTexture) {
Bitmap b = getBitMapFromFile("sample.bmp");
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, IDTexture);;
ByteBuffer bf = extract(b);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, 256, 256, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, bf);
return true;
}
And in Unity I get the textureID with cTexture.GetNativeTextureID():
void Start () {
cTexture = new Texture2D (width, height,TextureFormat.RGBA32,false);
cTexture.Apply();
obPlane.renderer.material.mainTexture = cTexture;
}
if(GUI.Button(new Rect(a,b,c,c), "Button")) {
activity.Call("updateTexture", cTexture.GetNativeTextureID());
}
But when I press the button, nothing happens.
A few things to remember when doing texturing with plugins:
Any time something alters Unity's graphical state externally, these alterations must be performed from the same thread as the renderer runs on. This means that your calling function should originate in one of the graphical Monobehaviour methods such as OnPreRender() and OnPreCull() to name but a few. I cant find an explicit list in the docs, but it should be easy enough to work out which are relevant. MonoBehaviour Docs The script that these calls are performed from must also be attached to the main camera in the scene.
Any time the graphical state of the engine is altered externally (like you are doing here) you must call GL.InvalidateState() afterwards. The docs explain this well:
"This invalidates any cached renderstates tied to the GL context. If for example a (native) plugin alters the renderstate settings then Unity's rendering architecture must be made aware of that to not assume the GL context is preserved." - GL.InvaliateState Docs
Slightly off topic
Let me also add that I attempted something identical to you, and failed to get the texture to update at all. While I'm not sure why this is the case, i can provide a much less efficient, but working method if speed is not critical. If you do get this working, please let me know on the question I've asked here, I'll be sure to update this answer if i find a proper solution.
This method will take in a bitmap from a path, scale and/or compress it (Unity has a max texture size of 4096x4096) into a byte array, which Unity then reads into a texture.
byte[] m_image;
File inFile = new File(_fileName);
Bitmap bm;
int width, height;
height = width = 4096;
String logTag = "BitmapHandler";
//Use BitmapFactory to check that bitmap is not too large.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
bm = BitmapFactory.decodeFile(inFile.getAbsolutePath(), options);
m_StartWidth = options.outWidth;
m_StartHeight = options.outHeight;
//If it is, calculate a sampleSize to reduce the image by
if (options.outWidth > 4096 || options.outHeight > 4096) {
options.inSampleSize = calculateInSampleSize(options, 4096, 4096);
}
else {
width = options.outWidth;
height = options.outHeight;
options.inSampleSize = 1;
}
options.inJustDecodeBounds = false;
bm = BitmapFactory.decodeFile(_fileName, options);
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 100, out);
m_image = out.toByteArray();
m_width = bm.getWidth();
m_height = bm.getHeight();
out.flush();
out.close();
}
catch (Exception e) {
m_image = null;
m_height = -1;
m_width = -1;
return null;
}
return m_image;
How sample size is worked out - scales the image by a power of two so that both width and height are smaller than 4096 pixels:
private int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
while (height / inSampleSize > reqHeight || width / inSampleSize > reqWidth) {
inSampleSize *=2;
}
}
return inSampleSize;
}
I'm sorry that I can't directly solve the problem, but i hope that the info I've given will be of use to you!

Android how to create runtime thumbnail

I have a large sized image. At runtime, I want to read the image from storage and scale it so that its weight and size gets reduced and I can use it as a thumbnail. When a user clicks on the thumbnail, I want to display the full-sized image.
Try this
Bitmap ThumbImage = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(imagePath), THUMBSIZE, THUMBSIZE);
This Utility is available from API_LEVEl 8. [Source]
My Solution
byte[] imageData = null;
try
{
final int THUMBNAIL_SIZE = 64;
FileInputStream fis = new FileInputStream(fileName);
Bitmap imageBitmap = BitmapFactory.decodeStream(fis);
imageBitmap = Bitmap.createScaledBitmap(imageBitmap, THUMBNAIL_SIZE, THUMBNAIL_SIZE, false);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
imageData = baos.toByteArray();
}
catch(Exception ex) {
}
The best solution I found is the following. Compared with the other solutions this one does not need to load the full image for creating a thumbnail, so it is more efficient!
Its limit is that you can not have a thumbnail with exact width and height but the solution as near as possible.
File file = ...; // the image file
Options bitmapOptions = new Options();
bitmapOptions.inJustDecodeBounds = true; // obtain the size of the image, without loading it in memory
BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOptions);
// find the best scaling factor for the desired dimensions
int desiredWidth = 400;
int desiredHeight = 300;
float widthScale = (float)bitmapOptions.outWidth/desiredWidth;
float heightScale = (float)bitmapOptions.outHeight/desiredHeight;
float scale = Math.min(widthScale, heightScale);
int sampleSize = 1;
while (sampleSize < scale) {
sampleSize *= 2;
}
bitmapOptions.inSampleSize = sampleSize; // this value must be a power of 2,
// this is why you can not have an image scaled as you would like
bitmapOptions.inJustDecodeBounds = false; // now we want to load the image
// Let's load just the part of the image necessary for creating the thumbnail, not the whole image
Bitmap thumbnail = BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOptions);
// Save the thumbnail
File thumbnailFile = ...;
FileOutputStream fos = new FileOutputStream(thumbnailFile);
thumbnail.compress(Bitmap.CompressFormat.JPEG, 90, fos);
fos.flush();
fos.close();
// Use the thumbail on an ImageView or recycle it!
thumbnail.recycle();
Here is a more complete solution to scaling down a Bitmap to thumbnail size. It expands on the Bitmap.createScaledBitmap solution by maintaining the aspect ratio of the images and also padding them to the same width so that they look good in a ListView.
Also, it would be best to do this scaling once and store the resulting Bitmap as a blob in your Sqlite database. I have included a snippet on how to convert the Bitmap to a byte array for this purpose.
public static final int THUMBNAIL_HEIGHT = 48;
public static final int THUMBNAIL_WIDTH = 66;
imageBitmap = BitmapFactory.decodeByteArray(mImageData, 0, mImageData.length);
Float width = new Float(imageBitmap.getWidth());
Float height = new Float(imageBitmap.getHeight());
Float ratio = width/height;
imageBitmap = Bitmap.createScaledBitmap(imageBitmap, (int)(THUMBNAIL_HEIGHT*ratio), THUMBNAIL_HEIGHT, false);
int padding = (THUMBNAIL_WIDTH - imageBitmap.getWidth())/2;
imageView.setPadding(padding, 0, padding, 0);
imageView.setImageBitmap(imageBitmap);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
imageBitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
byte[] byteArray = baos.toByteArray();
Use BitmapFactory.decodeFile(...) to get your Bitmap object and set it to an ImageView with ImageView.setImageBitmap().
On the ImageView set the layout dimensions to something small, eg:
android:layout_width="66dip" android:layout_height="48dip"
Add an onClickListener to the ImageView and launch a new activity, where you display the image in full size with
android:layout_width="wrap_content" android:layout_height="wrap_content"
or specify some larger size.
/**
* Creates a centered bitmap of the desired size.
*
* #param source original bitmap source
* #param width targeted width
* #param height targeted height
* #param options options used during thumbnail extraction
*/
public static Bitmap extractThumbnail(
Bitmap source, int width, int height, int options) {
if (source == null) {
return null;
}
float scale;
if (source.getWidth() < source.getHeight()) {
scale = width / (float) source.getWidth();
} else {
scale = height / (float) source.getHeight();
}
Matrix matrix = new Matrix();
matrix.setScale(scale, scale);
Bitmap thumbnail = transform(matrix, source, width, height,
OPTIONS_SCALE_UP | options);
return thumbnail;
}
I found an easy way to do this
Bitmap thumbnail = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(mPath),200,200)
Syntax
Bitmap thumbnail = ThumbnailUtils.extractThumbnail(Bitmap source,int width,int height)
OR
use Picasso dependancy
compile 'com.squareup.picasso:picasso:2.5.2'
Picasso.with(context)
.load("file:///android_asset/DvpvklR.png")
.resize(50, 50)
.into(imageView2);
Reference Picasso
If you want high quality result, so use [RapidDecoder][1] library. It is simple as follow:
import rapid.decoder.BitmapDecoder;
...
Bitmap bitmap = BitmapDecoder.from(getResources(), R.drawable.image)
.scale(width, height)
.useBuiltInDecoder(true)
.decode();
Don't forget to use builtin decoder if you want to scale down less than 50% and a HQ result.
This answer is based on the solution presented in https://developer.android.com/topic/performance/graphics/load-bitmap.html (without using of external libraries) with some changes by me to make its functionality better and more practical.
Some notes about this solution:
It is assumed that you want to keep the aspect ratio. In other words:
finalWidth / finalHeight == sourceBitmap.getWidth() / sourceBitmap.getWidth() (Regardless of casting and rounding issues)
It is assumed that you have two values (maxWidth & maxHeight) that you want any of the dimensions of your final bitmap doesn't exceed its corresponding value. In other words:
finalWidth <= maxWidth && finalHeight <= maxHeight
So minRatio has been placed as the basis of calculations (See the implementation). UNLIKE the basic solution that has placed maxRatio as the basis of calculations in actual. Also, the calculation of inSampleSize has been so much better (more logic, brief and efficient).
It is assumed that you want to (at least) one of the final dimensions has exactly the value of its corresponding maxValue (each one was possible, by considering the above assumptions). In other words:
finalWidth == maxWidth || finalHeight == maxHeight
The final additional step in compare to the basic solution (Bitmap.createScaledBitmap(...)) is for this "exactly" constraint. The very important note is you shouldn't take this step at first (like the accepted answer), because of its significant consumption of memory in case of huge images!
It is for decoding a file. You can change it like the basic solution to decode a resource (or everything that BitmapFactory supports).
The implementation:
public static Bitmap decodeSampledBitmap(String pathName, int maxWidth, int maxHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
final float wRatio_inv = (float) options.outWidth / maxWidth,
hRatio_inv = (float) options.outHeight / maxHeight; // Working with inverse ratios is more comfortable
final int finalW, finalH, minRatio_inv /* = max{Ratio_inv} */;
if (wRatio_inv > hRatio_inv) {
minRatio_inv = (int) wRatio_inv;
finalW = maxWidth;
finalH = Math.round(options.outHeight / wRatio_inv);
} else {
minRatio_inv = (int) hRatio_inv;
finalH = maxHeight;
finalW = Math.round(options.outWidth / hRatio_inv);
}
options.inSampleSize = pow2Ceil(minRatio_inv); // pow2Ceil: A utility function that comes later
options.inJustDecodeBounds = false; // Decode bitmap with inSampleSize set
return Bitmap.createScaledBitmap(BitmapFactory.decodeFile(pathName, options),
finalW, finalH, true);
}
/**
* #return the largest power of 2 that is smaller than or equal to number.
* WARNING: return {0b1000000...000} for ZERO input.
*/
public static int pow2Ceil(int number) {
return 1 << -(Integer.numberOfLeadingZeros(number) + 1); // is equivalent to:
// return Integer.rotateRight(1, Integer.numberOfLeadingZeros(number) + 1);
}
Sample Usage, in case of you have an imageView with a determined value for layout_width (match_parent or a explicit value) and a indeterminate value for layout_height (wrap_content) and instead a determined value for maxHeight:
imageView.setImageBitmap(decodeSampledBitmap(filePath,
imageView.getWidth(), imageView.getMaxHeight()));

Categories

Resources