Introduction to Image Compression Methods in Android
Image compression is a very common development scenario in Android. There are mainly two compression methods: quality compression and downsampling compression.
The former changes the storage size of the image without altering its dimensions, while the latter reduces the image dimensions to achieve the same goal.
Quality Compression
In Android, the typical implementation for quality compression is as follows:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); //quality ranges from 0 to 100, where 0 represents the smallest size and 100 represents the highest quality, corresponding to the largest size bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
In the above code, the compression format we chose is CompressFormat.JPEG. Besides this, there are two other options:
First, CompressFormat.PNG. The PNG format is lossless and cannot undergo quality compression, so the 'quality' parameter is ineffective and will be ignored, resulting in no change in the final file size of the image.
Second, CompressFormat.WEBP. This format, introduced by Google, is more space-efficient than JPEG. Practical tests show it can optimize space usage by approximately 30%.
In some application scenarios where a bitmap needs to be converted into a ByteArrayOutputStream, the choice between CompressFormat.PNG and Bitmap.CompressFormat.JPEG depends on the image format you want to compress. In this case, the quality should be set to 100.
The Android quality compression logic involves a series of Java layer calls in the 'compress' function, eventually reaching a native function as shown below:
//Bitmap.cpp static jboolean Bitmap_compress(JNIEnv* env, jobject clazz, jlong bitmapHandle, jint format, jint quality, jobject jstream, jbyteArray jstorage) { LocalScopedBitmap bitmap(bitmapHandle); SkImageEncoder::Type fm; switch (format) { case kJPEG_JavaEncodeFormat: fm = SkImageEncoder::kJPEG_Type; break; case kPNG_JavaEncodeFormat: fm = SkImageEncoder::kPNG_Type; break; case kWEBP_JavaEncodeFormat: fm = SkImageEncoder::kWEBP_Type; break; default: return JNI_FALSE; } if (!bitmap.valid()) { return JNI_FALSE; } bool success = false; std::unique_ptr<SkWStream> strm(CreateJavaOutputStreamAdaptor(env, jstream, jstorage)); if (!strm.get()) { return JNI_FALSE; } std::unique_ptr<SkImageEncoder> encoder(SkImageEncoder::Create(fm)); if (encoder.get()) { SkBitmap skbitmap; bitmap->getSkBitmap(&skbitmap); success = encoder->encodeStream(strm.get(), skbitmap, quality); } return success ? JNI_TRUE : JNI_FALSE; }
It can be seen that the function ultimately calls encoder->encodeStream(...) to encode and save locally. This function uses the Skia engine to encode and compress the image. An introduction to Skia will be discussed later.
Size Compression
Nearest Neighbour Resampling
BitmapFactory.Options options = new BitmapFactory.Options(); //or use inDensity with inTargetDensity, the algorithm is the same as inSampleSize options.inSampleSize = 2; //Set the scaling ratio (width and height) of the image. Google recommends using multiples of 2: Bitmap bitmap = BitmapFactory.decodeFile("xxx.png"); Bitmap compress = BitmapFactory.decodeFile("xxx.png", options);
Here, let's focus on 'inSampleSize'. Literally, it means 'set the sampling size'. Its function is: after setting the value of inSampleSize (an integer type), if set to 4, both width and height become 1/4 of the original, naturally reducing memory usage.
Referring to Google's official documentation, we can see that x (x being a multiple of 2) pixels ultimately correspond to one pixel. Since the sampling rate is set to 1/2, two pixels generate one pixel.
The nearest neighbor sampling method is quite crude, directly selecting one pixel as the generated pixel and discarding the other, resulting in the image turning pure green, meaning the red pixel is discarded.
The algorithm used in nearest neighbor sampling is called the nearest neighbor interpolation algorithm.
Bilinear Resampling
There are generally two ways to use Bilinear Resampling in Android:
Bitmap bitmap = BitmapFactory.decodeFile("xxx.png"); Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true); Or directly using matrix for scaling Bitmap bitmap = BitmapFactory.decodeFile("xxx.png"); Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);
Looking at the source code, we can see that the 'createScaledBitmap' function ultimately uses the second method with matrix for scaling. Bilinear sampling uses the bilinear interpolation algorithm, which, unlike the nearest neighbor interpolation algorithm, does not simply and crudely select one pixel. Instead, it refers to the values of the surrounding 2x2 points of the source pixel's corresponding position, takes the corresponding weights based on relative positions, and calculates the target image.
The bilinear interpolation algorithm has anti-aliasing functionality in image scaling processing and is the simplest and most common image scaling algorithm. When the bilinear interpolation algorithm is applied to adjacent 2x2 pixels, the resulting surface matches at the neighborhood, but the slope does not. The smoothing effect of the bilinear interpolation algorithm may cause degradation of image details, especially noticeable during upsampling.
The advantages of bilinear sampling over nearest neighbor sampling are:
Its coefficients can be decimals, not necessarily integers, which is particularly effective under certain compression constraints.
For images with a lot of text, bilinear sampling performs better in display effects.
There are also bicubic sampling and Lanczos sampling, etc. For detailed analysis, you can refer to the article 'Image Compression Analysis in Android (Part 2)' shared by a QQ Music expert.
Summary
In Android, the first two sampling methods can be chosen based on actual needs. If time is not a critical factor, bilinear sampling is preferred for scaling images. If high image quality is required and bilinear sampling is insufficient, consider introducing other algorithms for image processing. However, it's important to note that the latter two algorithms use convolution kernels to calculate generated pixels, which involves relatively large computational loads, with Lanczos being the most computationally intensive. In actual development, choose the algorithm based on requirements. Often, we use a combination of size compression and quality compression.