Question

I have a photo editing app. All it does is add images onto a base photo. The output photo is lower quality than the original photo which is taken by the camera. This is expected, but maybe I can improve on what I currently have. It's the same quality at 500px as 1000px which is very concerning... I can see I'm limiting quality somewhere other than pixels. The original photos in my camera gallery are JPG files. Here is everything I do to get the photo, create the bitmap from it, and then save it. Can you tell me where in the code the photo quality may get lower?

open gallery intent:

Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
        photoPickerIntent.setType("image/*");
        startActivityForResult(photoPickerIntent, 1);

OnActivityResult() for the chosen photo:

    if (intent != null && resultcode == RESULT_OK) 
                          {
                              mProfilePicPath = ih.getSelectedImageFilePathFromGallery(this.getApplicationContext(), intent);
                              mPortraitPhoto = ih.decodeSampledBitmapFromImagePath(mProfilePicPath, 
                                      GlobalConstants.PROFILE_PICTURE_RESOLUTION, 
                                      GlobalConstants.PROFILE_PICTURE_RESOLUTION);
                          }

        public String getSelectedImageFilePathFromGallery(Context ctx,
                    Intent intent) {
                Uri selectedImage = intent.getData();
                String[] filePathColumn = {MediaStore.Images.Media.DATA};
                Cursor cursor = ctx.getContentResolver().query(selectedImage, filePathColumn, null, null, null);
                cursor.moveToFirst();
                int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
                String filePath = cursor.getString(columnIndex);
                cursor.close();
                return filePath;
            }

    public Bitmap decodeSampledBitmapFromImagePath(String imagePath, int reqWidth, int reqHeight) {
            // First decode with inJustDecodeBounds=true to check dimensions
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(imagePath, options);

            int imageHeight = options.outHeight;
            int imageWidth = options.outWidth;
            String imageType = options.outMimeType;
            Log.d("Image dims", imageType + ", " + imageHeight + ", " + imageWidth + "size.");

            // Calculate inSampleSize
            options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

            // Decode bitmap with inSampleSize set
            options.inJustDecodeBounds = false;
            Bitmap portraitPhoto = ImageHelper.convertToPortraitOrientation(options, imagePath);
            return portraitPhoto;

        }

public static 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) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

    public static Bitmap convertToPortraitOrientation(BitmapFactory.Options options, String path) {
            Uri actualUri = Uri.parse(path);
            float degree = 0;

            Bitmap bmp = BitmapFactory.decodeFile(path, options);
            try {
                ExifInterface exif = new ExifInterface(actualUri.getPath());
                String exifOrientation = exif
                        .getAttribute(ExifInterface.TAG_ORIENTATION);

                if (bmp != null) {
                    degree = getDegree(exifOrientation);
                    if (degree != 0)
                        bmp = createRotatedBitmap(bmp, degree);
                }
            }
            catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bmp;
        }

    public static Bitmap createRotatedBitmap(Bitmap bm, float degree) {
            Bitmap bitmap = null;
            if (degree != 0) {
                Matrix matrix = new Matrix();
                matrix.preRotate(degree);
                bitmap = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(),
                        bm.getHeight(), matrix, true);
            }
            return bitmap;
        }

    public File createProfilePicSaveFileInternal(Context ctx) {

            //mBackgroundImage.setImageBitmap(ih.decodeSampledBitmapFromImagePath(mProfilePicPath, 500, 500));


            String path = ctx.getFilesDir() + File.separator + "My Folder";
            File outputDir= new File(path);
            outputDir.mkdirs();
            File newFile = new File(path + "/" + mName + ".jpg");

            return newFile;
        }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
        public void saveImage( Bitmap bitmap, Context ctx, File newFile) {
            FileOutputStream fos;
            newFile.setReadable(true, false);
            try {
                fos = new FileOutputStream(newFile);
                bitmap.compress(CompressFormat.JPEG, 100, fos);
                fos.flush();
                fos.close();
            } catch (FileNotFoundException e) {
                Log.e("GREC", e.getMessage(), e);
            } catch (IOException e) {
                Log.e("GREC", e.getMessage(), e);
            }
            ctx.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));

        }

when a button is clicked:

    File newInternalFile = ih.createProfilePicSaveFileInternal(this.getApplicationContext());
            boolean s = newInternalFile.exists();
            long length = newInternalFile.length();
            ih.saveImage(mPortraitPhoto, this.getApplicationContext(), newInternalFile);
            mPortraitPhoto = null;

public File createProfilePicSaveFileInternal(Context ctx) {

        //mBackgroundImage.setImageBitmap(ih.decodeSampledBitmapFromImagePath(mProfilePicPath, 500, 500));


        String path = ctx.getFilesDir() + File.separator + "My Folder";
        File outputDir= new File(path);
        outputDir.mkdirs();
        File newFile = new File(path + "/" + mName + ".jpg");

        return newFile;
    }

After my image is first saved, I then get that saved image in it's true form by doing this (inSampleSize is now 1):

public Bitmap getPortraitBitmapNotSampled(String imagePath){
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(imagePath, options);

        int imageHeight = options.outHeight;
        int imageWidth = options.outWidth;
        String imageType = options.outMimeType;
        Log.d("Dressing room photo dims", imageType + ", " + imageHeight + ", " + imageWidth + "size.");

        // Calculate inSampleSize
        options.inSampleSize = 1;

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        Bitmap portraitPhoto = ImageHelper.convertToPortraitOrientation(options, imagePath);
        return portraitPhoto;
    }

I end up taking a screen shot to get the output image like this:

public Bitmap takeScreenshot() {
        View v = findViewById(R.id.DressBig);
    v.setDrawingCacheEnabled(true);
    return v.getDrawingCache();
    }

Then the final save:

private void saveClicked(){
        finishedOutfit = takeScreenshot();
        File newFile = ih.createOutfitSaveFileExternal();
        File newInternalFile = ih.createOutfitSaveFileInternal(this.getApplicationContext());
        ih.saveImage(finishedOutfit, this.getApplicationContext(), newFile);
        ih.saveImage(finishedOutfit, this.getApplicationContext(), newInternalFile);
        String newFilePath = newInternalFile.toString();
        String newExternalFilePath = newFile.toString();
        Log.d("db file path: ", newFilePath);
        Log.d("external file path: ", newExternalFilePath);
        insertOutfitInDB(newFilePath, newExternalFilePath);
        showImageSavedDialog();
    }
Was it helpful?

Solution

The problem you see here is definitely caused by JPEG compression artifacts. You can see this by the characteristic 8x8 pixel blocks formed that you can see when zooming in.

JPEGs are tricky. They have very good compression ratios, but they use these 8x8 blocks for compression (technical: the 8x8 block is the area of the quantized discrete cosine transform used for content encoding). When a JPEG is opened and saved again, these blocks are re-compressed.

Normally, if the same image is opened, saved, and closed multiple times, there won't be a gigantic difference in image quality. This is because the same blocks are encoded and quantized identically, so the lost information was already lost.

However, if a JPEG image is cropped so that the 8x8 artifacting is no longer aligned with the 8x8 compression grid used, then things get really messy. The JPEG compressor won't understand that it should really be compressing a grid shifted by a few pixels, and will try to mash pieces of each of the old blocks into new blocks. In this case, there's a LOT of information that is again quantized, and you get some very significant degredation.

The simplest way to (mostly) avoid this artifacting when working with JPEGs is to enforce alignment with the 8x8 image grid when cropping. However, if you really want to get lossless-quality cropping, you have to manipulate the un-decompressed DCT blocks inside the JFIF wrapper. It's ugly, but doable. I do not know of an Android implementation of this, however.

Takeaway: JPEGs are finicky when cropped and recompressed - try to force alignment with an 8x8 grid to reduce quality loss.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top