Home | History | Annotate | Download | only in imagebackend
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.camera.processing.imagebackend;
     18 
     19 import android.graphics.ImageFormat;
     20 import android.graphics.Rect;
     21 import android.location.Location;
     22 import android.media.CameraProfile;
     23 import android.net.Uri;
     24 
     25 import com.android.camera.Exif;
     26 import com.android.camera.app.OrientationManager.DeviceOrientation;
     27 import com.android.camera.debug.Log;
     28 import com.android.camera.exif.ExifInterface;
     29 import com.android.camera.one.v2.camera2proxy.CaptureResultProxy;
     30 import com.android.camera.one.v2.camera2proxy.ImageProxy;
     31 import com.android.camera.one.v2.camera2proxy.TotalCaptureResultProxy;
     32 import com.android.camera.processing.memory.LruResourcePool;
     33 import com.android.camera.processing.memory.LruResourcePool.Resource;
     34 import com.android.camera.session.CaptureSession;
     35 import com.android.camera.util.ExifUtil;
     36 import com.android.camera.util.JpegUtilNative;
     37 import com.android.camera.util.Size;
     38 import com.google.common.base.Optional;
     39 import com.google.common.util.concurrent.FutureCallback;
     40 import com.google.common.util.concurrent.Futures;
     41 import com.google.common.util.concurrent.ListenableFuture;
     42 
     43 import java.nio.ByteBuffer;
     44 import java.util.HashMap;
     45 import java.util.Map;
     46 import java.util.concurrent.ExecutionException;
     47 import java.util.concurrent.Executor;
     48 
     49 /**
     50  * Implements the conversion of a YUV_420_888 image to compressed JPEG byte
     51  * array, using the native implementation of the Camera Application. If the
     52  * image is already JPEG, then it passes it through properly with the assumption
     53  * that the JPEG is already encoded in the proper orientation.
     54  */
     55 public class TaskCompressImageToJpeg extends TaskJpegEncode {
     56 
     57     /**
     58      *  Loss-less JPEG compression  is usually about a factor of 5,
     59      *  and is a safe lower bound for this value to use to reduce the memory
     60      *  footprint for encoding the final jpg.
     61      */
     62     private static final int MINIMUM_EXPECTED_JPG_COMPRESSION_FACTOR = 2;
     63     private final LruResourcePool<Integer, ByteBuffer> mByteBufferDirectPool;
     64 
     65     /**
     66      * Constructor
     67      *
     68      * @param image Image required for computation
     69      * @param executor Executor to run events
     70      * @param imageTaskManager Link to ImageBackend for reference counting
     71      * @param captureSession Handler for UI/Disk events
     72      */
     73     TaskCompressImageToJpeg(ImageToProcess image, Executor executor,
     74             ImageTaskManager imageTaskManager,
     75             CaptureSession captureSession,
     76             LruResourcePool<Integer, ByteBuffer> byteBufferResourcePool) {
     77         super(image, executor, imageTaskManager, ProcessingPriority.SLOW, captureSession);
     78         mByteBufferDirectPool = byteBufferResourcePool;
     79     }
     80 
     81     /**
     82      * Wraps the static call to JpegUtilNative for testability. {@see
     83      * JpegUtilNative#compressJpegFromYUV420Image}
     84      */
     85     public int compressJpegFromYUV420Image(ImageProxy img, ByteBuffer outBuf, int quality,
     86             Rect crop, int degrees) {
     87         return JpegUtilNative.compressJpegFromYUV420Image(img, outBuf, quality, crop, degrees);
     88     }
     89 
     90     /**
     91      * Encapsulates the required EXIF Tag parse for Image processing.
     92      *
     93      * @param exif EXIF data from which to extract data.
     94      * @return A Minimal Map from ExifInterface.Tag value to values required for Image processing
     95      */
     96     public Map<Integer, Integer> exifGetMinimalTags(ExifInterface exif) {
     97         Map<Integer, Integer> map = new HashMap<>();
     98         map.put(ExifInterface.TAG_ORIENTATION,
     99                 ExifInterface.getRotationForOrientationValue((short) Exif.getOrientation(exif)));
    100         map.put(ExifInterface.TAG_PIXEL_X_DIMENSION, exif.getTagIntValue(
    101                 ExifInterface.TAG_PIXEL_X_DIMENSION));
    102         map.put(ExifInterface.TAG_PIXEL_Y_DIMENSION, exif.getTagIntValue(
    103                 ExifInterface.TAG_PIXEL_Y_DIMENSION));
    104         return map;
    105     }
    106 
    107     @Override
    108     public void run() {
    109         ImageToProcess img = mImage;
    110         mSession.getCollector().markProcessingTimeStart();
    111         final Rect safeCrop;
    112 
    113         // For JPEG, it is the capture devices responsibility to get proper
    114         // orientation.
    115 
    116         TaskImage inputImage, resultImage;
    117         byte[] writeOut;
    118         int numBytes;
    119         ByteBuffer compressedData;
    120         ExifInterface exifData = null;
    121         Resource<ByteBuffer> byteBufferResource = null;
    122 
    123         switch (img.proxy.getFormat()) {
    124             case ImageFormat.JPEG:
    125                 try {
    126                     // In the cases, we will request a zero-oriented JPEG from
    127                     // the HAL; the HAL may deliver its orientation in the JPEG
    128                     // encoding __OR__ EXIF -- we don't know. We need to read
    129                     // the EXIF setting from byte payload and the EXIF reader
    130                     // doesn't work on direct buffers. So, we make a local
    131                     // copy in a non-direct buffer.
    132                     ByteBuffer origBuffer = img.proxy.getPlanes().get(0).getBuffer();
    133                     compressedData = ByteBuffer.allocate(origBuffer.limit());
    134 
    135                     // On memory allocation failure, fail gracefully.
    136                     if (compressedData == null) {
    137                         // TODO: Put memory allocation failure code here.
    138                         mSession.finishWithFailure(-1, true);
    139                         return;
    140                     }
    141 
    142                     origBuffer.rewind();
    143                     compressedData.put(origBuffer);
    144                     origBuffer.rewind();
    145                     compressedData.rewind();
    146 
    147                     // For JPEG, always use the EXIF orientation as ground
    148                     // truth on orientation, width and height.
    149                     Integer exifOrientation = null;
    150                     Integer exifPixelXDimension = null;
    151                     Integer exifPixelYDimension = null;
    152 
    153                     if (compressedData.array() != null) {
    154                         exifData = Exif.getExif(compressedData.array());
    155                         Map<Integer, Integer> minimalExifTags = exifGetMinimalTags(exifData);
    156 
    157                         exifOrientation = minimalExifTags.get(ExifInterface.TAG_ORIENTATION);
    158                         exifPixelXDimension = minimalExifTags
    159                                 .get(ExifInterface.TAG_PIXEL_X_DIMENSION);
    160                         exifPixelYDimension = minimalExifTags
    161                                 .get(ExifInterface.TAG_PIXEL_Y_DIMENSION);
    162                     }
    163 
    164                     final DeviceOrientation exifDerivedRotation;
    165                     if (exifOrientation == null) {
    166                         // No existing rotation value is assumed to be 0
    167                         // rotation.
    168                         exifDerivedRotation = DeviceOrientation.CLOCKWISE_0;
    169                     } else {
    170                         exifDerivedRotation = DeviceOrientation
    171                                 .from(exifOrientation);
    172                     }
    173 
    174                     final int imageWidth;
    175                     final int imageHeight;
    176                     // Crop coordinate space is in original sensor coordinates.  We need
    177                     // to calculate the proper rotation of the crop to be applied to the
    178                     // final JPEG artifact.
    179                     final DeviceOrientation combinedRotationFromSensorToJpeg =
    180                             addOrientation(img.rotation, exifDerivedRotation);
    181 
    182                     if (exifPixelXDimension == null || exifPixelYDimension == null) {
    183                         Log.w(TAG,
    184                                 "Cannot parse EXIF for image dimensions, passing 0x0 dimensions");
    185                         imageHeight = 0;
    186                         imageWidth = 0;
    187                         // calculate crop from exif info with image proxy width/height
    188                         safeCrop = guaranteedSafeCrop(img.proxy,
    189                                 rotateBoundingBox(img.crop, combinedRotationFromSensorToJpeg));
    190                     } else {
    191                         imageWidth = exifPixelXDimension;
    192                         imageHeight = exifPixelYDimension;
    193                         // calculate crop from exif info with combined rotation
    194                         safeCrop = guaranteedSafeCrop(imageWidth, imageHeight,
    195                                 rotateBoundingBox(img.crop, combinedRotationFromSensorToJpeg));
    196                     }
    197 
    198                     // Ignore the device rotation on ImageToProcess and use the EXIF from
    199                     // byte[] payload
    200                     inputImage = new TaskImage(
    201                             exifDerivedRotation,
    202                             imageWidth,
    203                             imageHeight,
    204                             img.proxy.getFormat(), safeCrop);
    205 
    206                     if(requiresCropOperation(img.proxy, safeCrop)) {
    207                         // Crop the image
    208                         resultImage = new TaskImage(
    209                                 exifDerivedRotation,
    210                                 safeCrop.width(),
    211                                 safeCrop.height(),
    212                                 img.proxy.getFormat(), null);
    213 
    214                         byte[] croppedResult = decompressCropAndRecompressJpegData(
    215                                 compressedData.array(), safeCrop,
    216                                 getJpegCompressionQuality());
    217 
    218                         compressedData = ByteBuffer.allocate(croppedResult.length);
    219                         compressedData.put(ByteBuffer.wrap(croppedResult));
    220                         compressedData.rewind();
    221                     } else {
    222                         // Pass-though the JPEG data
    223                         resultImage = inputImage;
    224                     }
    225                 } finally {
    226                     // Release the image now that you have a usable copy in
    227                     // local memory
    228                     // Or you failed to process
    229                     mImageTaskManager.releaseSemaphoreReference(img, mExecutor);
    230                 }
    231 
    232                 onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE);
    233 
    234                 numBytes = compressedData.limit();
    235                 break;
    236             case ImageFormat.YUV_420_888:
    237                 safeCrop = guaranteedSafeCrop(img.proxy, img.crop);
    238                 try {
    239                     inputImage = new TaskImage(img.rotation, img.proxy.getWidth(),
    240                             img.proxy.getHeight(),
    241                             img.proxy.getFormat(), safeCrop);
    242                     Size resultSize = getImageSizeForOrientation(img.crop.width(),
    243                             img.crop.height(),
    244                             img.rotation);
    245 
    246                     // Resulting image will be rotated so that viewers won't
    247                     // have to rotate. That's why the resulting image will have 0
    248                     // rotation.
    249                     resultImage = new TaskImage(
    250                             DeviceOrientation.CLOCKWISE_0, resultSize.getWidth(),
    251                             resultSize.getHeight(),
    252                             ImageFormat.JPEG, null);
    253                     // Image rotation is already encoded into the bytes.
    254 
    255                     onStart(mId, inputImage, resultImage, TaskInfo.Destination.FINAL_IMAGE);
    256 
    257                     // WARNING:
    258                     // This reduces the size of the buffer that is created
    259                     // to hold the final jpg. It is reduced by the "Minimum expected
    260                     // jpg compression factor" to reduce memory allocation consumption.
    261                     // If the final jpg is more than this size the image will be
    262                     // corrupted. The maximum size of an image is width * height *
    263                     // number_of_channels. We artificially reduce this number based on
    264                     // what we expect the compression ratio to be to reduce the
    265                     // amount of memory we are required to allocate.
    266                     int maxPossibleJpgSize = 3 * resultImage.width * resultImage.height;
    267                     int jpgBufferSize = maxPossibleJpgSize /
    268                           MINIMUM_EXPECTED_JPG_COMPRESSION_FACTOR;
    269 
    270                     byteBufferResource = mByteBufferDirectPool.acquire(jpgBufferSize);
    271                     compressedData = byteBufferResource.get();
    272 
    273                     // On memory allocation failure, fail gracefully.
    274                     if (compressedData == null) {
    275                         // TODO: Put memory allocation failure code here.
    276                         mSession.finishWithFailure(-1, true);
    277                         byteBufferResource.close();
    278                         return;
    279                     }
    280 
    281                     // Do the actual compression here.
    282                     numBytes = compressJpegFromYUV420Image(
    283                             img.proxy, compressedData, getJpegCompressionQuality(),
    284                             img.crop, inputImage.orientation.getDegrees());
    285 
    286                     // If the compression overflows the size of the buffer, the
    287                     // actual number of bytes will be returned.
    288                     if (numBytes > jpgBufferSize) {
    289                         byteBufferResource.close();
    290                         mByteBufferDirectPool.acquire(maxPossibleJpgSize);
    291                         compressedData = byteBufferResource.get();
    292 
    293                         // On memory allocation failure, fail gracefully.
    294                         if (compressedData == null) {
    295                             // TODO: Put memory allocation failure code here.
    296                             mSession.finishWithFailure(-1, true);
    297                             byteBufferResource.close();
    298                             return;
    299                         }
    300 
    301                         numBytes = compressJpegFromYUV420Image(
    302                               img.proxy, compressedData, getJpegCompressionQuality(),
    303                               img.crop, inputImage.orientation.getDegrees());
    304                     }
    305 
    306                     if (numBytes < 0) {
    307                         byteBufferResource.close();
    308                         throw new RuntimeException("Error compressing jpeg.");
    309                     }
    310                     compressedData.limit(numBytes);
    311                 } finally {
    312                     // Release the image now that you have a usable copy in local memory
    313                     // Or you failed to process
    314                     mImageTaskManager.releaseSemaphoreReference(img, mExecutor);
    315                 }
    316                 break;
    317             default:
    318                 mImageTaskManager.releaseSemaphoreReference(img, mExecutor);
    319                 throw new IllegalArgumentException(
    320                         "Unsupported input image format for TaskCompressImageToJpeg");
    321         }
    322 
    323         writeOut = new byte[numBytes];
    324         compressedData.get(writeOut);
    325         compressedData.rewind();
    326 
    327         if (byteBufferResource != null) {
    328             byteBufferResource.close();
    329         }
    330 
    331         onJpegEncodeDone(mId, inputImage, resultImage, writeOut,
    332                 TaskInfo.Destination.FINAL_IMAGE);
    333 
    334         // In rare cases, TaskCompressImageToJpeg might complete before
    335         // TaskConvertImageToRGBPreview. However, session should take care
    336         // of out-of-order completion.
    337         // EXIF tags are rewritten so that output from this task is normalized.
    338         final TaskImage finalInput = inputImage;
    339         final TaskImage finalResult = resultImage;
    340 
    341         final ExifInterface exif = createExif(Optional.fromNullable(exifData), resultImage,
    342                 img.metadata);
    343         mSession.getCollector().decorateAtTimeWriteToDisk(exif);
    344         ListenableFuture<Optional<Uri>> futureUri = mSession.saveAndFinish(writeOut,
    345                 resultImage.width, resultImage.height, resultImage.orientation.getDegrees(), exif);
    346         Futures.addCallback(futureUri, new FutureCallback<Optional<Uri>>() {
    347             @Override
    348             public void onSuccess(Optional<Uri> uriOptional) {
    349                 if (uriOptional.isPresent()) {
    350                     onUriResolved(mId, finalInput, finalResult, uriOptional.get(),
    351                             TaskInfo.Destination.FINAL_IMAGE);
    352                 }
    353             }
    354 
    355             @Override
    356             public void onFailure(Throwable throwable) {
    357             }
    358         });
    359 
    360         final ListenableFuture<TotalCaptureResultProxy> requestMetadata = img.metadata;
    361         // If TotalCaptureResults are available add them to the capture event.
    362         // Otherwise, do NOT wait for them, since we'd be stalling the ImageBackend
    363         if (requestMetadata.isDone()) {
    364             try {
    365                 mSession.getCollector()
    366                         .decorateAtTimeOfCaptureRequestAvailable(requestMetadata.get());
    367             } catch (InterruptedException e) {
    368                 Log.e(TAG,
    369                         "CaptureResults not added to photoCaptureDoneEvent event due to Interrupted Exception.");
    370             } catch (ExecutionException e) {
    371                 Log.w(TAG,
    372                         "CaptureResults not added to photoCaptureDoneEvent event due to Execution Exception.");
    373             } finally {
    374                 mSession.getCollector().photoCaptureDoneEvent();
    375             }
    376         } else {
    377             Log.w(TAG, "CaptureResults unavailable to photoCaptureDoneEvent event.");
    378             mSession.getCollector().photoCaptureDoneEvent();
    379         }
    380     }
    381 
    382     /**
    383      * Wraps a possible log message to be overridden for testability purposes.
    384      *
    385      * @param message
    386      */
    387     protected void logWrapper(String message) {
    388         // Do nothing.
    389     }
    390 
    391     /**
    392      * Wraps EXIF Interface for JPEG Metadata creation. Can be overridden for
    393      * testing
    394      *
    395      * @param image Metadata for a jpeg image to create EXIF Interface
    396      * @return the created Exif Interface
    397      */
    398     protected ExifInterface createExif(Optional<ExifInterface> exifData, TaskImage image,
    399                                        ListenableFuture<TotalCaptureResultProxy> totalCaptureResultProxyFuture) {
    400         ExifInterface exif;
    401         if (exifData.isPresent()) {
    402             exif = exifData.get();
    403         } else {
    404             exif = new ExifInterface();
    405         }
    406         Optional<Location> location = Optional.fromNullable(mSession.getLocation());
    407 
    408         try {
    409             new ExifUtil(exif).populateExif(Optional.of(image),
    410                     Optional.<CaptureResultProxy>of(totalCaptureResultProxyFuture.get()), location);
    411         } catch (InterruptedException | ExecutionException e) {
    412             new ExifUtil(exif).populateExif(Optional.of(image),
    413                     Optional.<CaptureResultProxy>absent(), location);
    414         }
    415 
    416         return exif;
    417     }
    418 
    419     /**
    420      * @return Quality level to use for JPEG compression.
    421      */
    422     protected int getJpegCompressionQuality () {
    423         return CameraProfile.getJpegEncodingQualityParameter(CameraProfile.QUALITY_HIGH);
    424     }
    425 
    426     /**
    427      * @param originalWidth the width of the original image captured from the
    428      *            camera
    429      * @param originalHeight the height of the original image captured from the
    430      *            camera
    431      * @param orientation the rotation to apply, in degrees.
    432      * @return The size of the final rotated image
    433      */
    434     private Size getImageSizeForOrientation(int originalWidth, int originalHeight,
    435             DeviceOrientation orientation) {
    436         if (orientation == DeviceOrientation.CLOCKWISE_0
    437                 || orientation == DeviceOrientation.CLOCKWISE_180) {
    438             return new Size(originalWidth, originalHeight);
    439         } else if (orientation == DeviceOrientation.CLOCKWISE_90
    440                 || orientation == DeviceOrientation.CLOCKWISE_270) {
    441             return new Size(originalHeight, originalWidth);
    442         } else {
    443             // Unsupported orientation. Get rid of this once UNKNOWN is gone.
    444             return new Size(originalWidth, originalHeight);
    445         }
    446     }
    447 }
    448