Home | History | Annotate | Download | only in legacy
      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 android.hardware.camera2.legacy;
     18 
     19 import android.graphics.Matrix;
     20 import android.graphics.Point;
     21 import android.graphics.Rect;
     22 import android.graphics.RectF;
     23 import android.hardware.Camera;
     24 import android.hardware.Camera.Area;
     25 import android.hardware.camera2.params.Face;
     26 import android.hardware.camera2.params.MeteringRectangle;
     27 import android.hardware.camera2.utils.ListUtils;
     28 import android.hardware.camera2.utils.ParamsUtils;
     29 import android.hardware.camera2.utils.SizeAreaComparator;
     30 import android.util.Size;
     31 import android.util.SizeF;
     32 
     33 import android.util.Log;
     34 
     35 import java.util.ArrayList;
     36 import java.util.Arrays;
     37 import java.util.List;
     38 
     39 import static com.android.internal.util.Preconditions.*;
     40 
     41 /**
     42  * Various utilities for dealing with camera API1 parameters.
     43  */
     44 @SuppressWarnings("deprecation")
     45 public class ParameterUtils {
     46     /** Upper/left minimal point of a normalized rectangle */
     47     public static final int NORMALIZED_RECTANGLE_MIN = -1000;
     48     /** Lower/right maximal point of a normalized rectangle */
     49     public static final int NORMALIZED_RECTANGLE_MAX = 1000;
     50     /** The default normalized rectangle spans the entire size of the preview viewport */
     51     public static final Rect NORMALIZED_RECTANGLE_DEFAULT = new Rect(
     52             NORMALIZED_RECTANGLE_MIN,
     53             NORMALIZED_RECTANGLE_MIN,
     54             NORMALIZED_RECTANGLE_MAX,
     55             NORMALIZED_RECTANGLE_MAX);
     56     /** The default normalized area uses the default normalized rectangle with a weight=1 */
     57     public static final Camera.Area CAMERA_AREA_DEFAULT =
     58             new Camera.Area(new Rect(NORMALIZED_RECTANGLE_DEFAULT),
     59                             /*weight*/1);
     60     /** Empty rectangle {@code 0x0+0,0} */
     61     public static final Rect RECTANGLE_EMPTY =
     62             new Rect(/*left*/0, /*top*/0, /*right*/0, /*bottom*/0);
     63 
     64     private static final double ASPECT_RATIO_TOLERANCE = 0.05f;
     65 
     66     /**
     67      * Calculate effective/reported zoom data from a user-specified crop region.
     68      */
     69     public static class ZoomData {
     70         /** Zoom index used by {@link Camera.Parameters#setZoom} */
     71         public final int zoomIndex;
     72         /** Effective crop-region given the zoom index, coordinates relative to active-array */
     73         public final Rect previewCrop;
     74         /** Reported crop-region given the zoom index, coordinates relative to active-array */
     75         public final Rect reportedCrop;
     76 
     77         public ZoomData(int zoomIndex, Rect previewCrop, Rect reportedCrop) {
     78             this.zoomIndex = zoomIndex;
     79             this.previewCrop = previewCrop;
     80             this.reportedCrop = reportedCrop;
     81         }
     82     }
     83 
     84     /**
     85      * Calculate effective/reported metering data from a user-specified metering region.
     86      */
     87     public static class MeteringData {
     88         /**
     89          * The metering area scaled to the range of [-1000, 1000].
     90          * <p>Values outside of this range are clipped to be within the range.</p>
     91          */
     92         public final Camera.Area meteringArea;
     93         /**
     94          * Effective preview metering region, coordinates relative to active-array.
     95          *
     96          * <p>Clipped to fit inside of the (effective) preview crop region.</p>
     97          */
     98         public final Rect previewMetering;
     99         /**
    100          * Reported metering region, coordinates relative to active-array.
    101          *
    102          * <p>Clipped to fit inside of the (reported) resulting crop region.</p>
    103          */
    104         public final Rect reportedMetering;
    105 
    106         public MeteringData(Area meteringArea, Rect previewMetering, Rect reportedMetering) {
    107             this.meteringArea = meteringArea;
    108             this.previewMetering = previewMetering;
    109             this.reportedMetering = reportedMetering;
    110         }
    111     }
    112 
    113     /**
    114      * A weighted rectangle is an arbitrary rectangle (the coordinate system is unknown) with an
    115      * arbitrary weight.
    116      *
    117      * <p>The user of this class must know what the coordinate system ahead of time; it's
    118      * then possible to convert to a more concrete type such as a metering rectangle or a face.
    119      * </p>
    120      *
    121      * <p>When converting to a more concrete type, out-of-range values are clipped; this prevents
    122      * possible illegal argument exceptions being thrown at runtime.</p>
    123      */
    124     public static class WeightedRectangle {
    125         /** Arbitrary rectangle (the range is user-defined); never {@code null}. */
    126         public final Rect rect;
    127         /** Arbitrary weight (the range is user-defined). */
    128         public final int weight;
    129 
    130         /**
    131          * Create a new weighted-rectangle from a non-{@code null} rectangle; the {@code weight}
    132          * can be unbounded.
    133          */
    134         public WeightedRectangle(Rect rect, int weight) {
    135             this.rect = checkNotNull(rect, "rect must not be null");
    136             this.weight = weight;
    137         }
    138 
    139         /**
    140          * Convert to a metering rectangle, clipping any of the values to stay within range.
    141          *
    142          * <p>If values are clipped, a warning is printed to logcat.</p>
    143          *
    144          * @return a new metering rectangle
    145          */
    146         public MeteringRectangle toMetering() {
    147             int weight = clip(this.weight,
    148                     MeteringRectangle.METERING_WEIGHT_MIN,
    149                     MeteringRectangle.METERING_WEIGHT_MAX,
    150                     rect,
    151                     "weight");
    152 
    153             int x = clipLower(rect.left, /*lo*/0, rect, "left");
    154             int y = clipLower(rect.top, /*lo*/0, rect, "top");
    155             int w = clipLower(rect.width(), /*lo*/0, rect, "width");
    156             int h = clipLower(rect.height(), /*lo*/0, rect, "height");
    157 
    158             return new MeteringRectangle(x, y, w, h, weight);
    159         }
    160 
    161         /**
    162          * Convert to a face; the rect is considered to be the bounds, and the weight
    163          * is considered to be the score.
    164          *
    165          * <p>If the score is out of range of {@value Face#SCORE_MIN}, {@value Face#SCORE_MAX},
    166          * the score is clipped first and a warning is printed to logcat.</p>
    167          *
    168          * <p>If the id is negative, the id is changed to 0 and a warning is printed to
    169          * logcat.</p>
    170          *
    171          * <p>All other parameters are passed-through as-is.</p>
    172          *
    173          * @return a new face with the optional features set
    174          */
    175         public Face toFace(
    176                 int id, Point leftEyePosition, Point rightEyePosition, Point mouthPosition) {
    177             int idSafe = clipLower(id, /*lo*/0, rect, "id");
    178             int score = clip(weight,
    179                     Face.SCORE_MIN,
    180                     Face.SCORE_MAX,
    181                     rect,
    182                     "score");
    183 
    184             return new Face(rect, score, idSafe, leftEyePosition, rightEyePosition, mouthPosition);
    185         }
    186 
    187         /**
    188          * Convert to a face; the rect is considered to be the bounds, and the weight
    189          * is considered to be the score.
    190          *
    191          * <p>If the score is out of range of {@value Face#SCORE_MIN}, {@value Face#SCORE_MAX},
    192          * the score is clipped first and a warning is printed to logcat.</p>
    193          *
    194          * <p>All other parameters are passed-through as-is.</p>
    195          *
    196          * @return a new face without the optional features
    197          */
    198         public Face toFace() {
    199             int score = clip(weight,
    200                     Face.SCORE_MIN,
    201                     Face.SCORE_MAX,
    202                     rect,
    203                     "score");
    204 
    205             return new Face(rect, score);
    206         }
    207 
    208         private static int clipLower(int value, int lo, Rect rect, String name) {
    209             return clip(value, lo, /*hi*/Integer.MAX_VALUE, rect, name);
    210         }
    211 
    212         private static int clip(int value, int lo, int hi, Rect rect, String name) {
    213             if (value < lo) {
    214                 Log.w(TAG, "toMetering - Rectangle " + rect + " "
    215                         + name + " too small, clip to " + lo);
    216                 value = lo;
    217             } else if (value > hi) {
    218                 Log.w(TAG, "toMetering - Rectangle " + rect + " "
    219                         + name + " too small, clip to " + hi);
    220                 value = hi;
    221             }
    222 
    223             return value;
    224         }
    225     }
    226 
    227     private static final String TAG = "ParameterUtils";
    228     private static final boolean DEBUG = false;
    229 
    230     /** getZoomRatios stores zoom ratios in 1/100 increments, e.x. a zoom of 3.2 is 320 */
    231     private static final int ZOOM_RATIO_MULTIPLIER = 100;
    232 
    233     /**
    234      * Convert a camera API1 size into a util size
    235      */
    236     public static Size convertSize(Camera.Size size) {
    237         checkNotNull(size, "size must not be null");
    238 
    239         return new Size(size.width, size.height);
    240     }
    241 
    242     /**
    243      * Convert a camera API1 list of sizes into a util list of sizes
    244      */
    245     public static List<Size> convertSizeList(List<Camera.Size> sizeList) {
    246         checkNotNull(sizeList, "sizeList must not be null");
    247 
    248         List<Size> sizes = new ArrayList<>(sizeList.size());
    249         for (Camera.Size s : sizeList) {
    250             sizes.add(new Size(s.width, s.height));
    251         }
    252         return sizes;
    253     }
    254 
    255     /**
    256      * Convert a camera API1 list of sizes into an array of sizes
    257      */
    258     public static Size[] convertSizeListToArray(List<Camera.Size> sizeList) {
    259         checkNotNull(sizeList, "sizeList must not be null");
    260 
    261         Size[] array = new Size[sizeList.size()];
    262         int ctr = 0;
    263         for (Camera.Size s : sizeList) {
    264             array[ctr++] = new Size(s.width, s.height);
    265         }
    266         return array;
    267     }
    268 
    269     /**
    270      * Check if the camera API1 list of sizes contains a size with the given dimens.
    271      */
    272     public static boolean containsSize(List<Camera.Size> sizeList, int width, int height) {
    273         checkNotNull(sizeList, "sizeList must not be null");
    274         for (Camera.Size s : sizeList) {
    275             if (s.height == height && s.width == width) {
    276                 return true;
    277             }
    278         }
    279         return false;
    280     }
    281 
    282     /**
    283      * Returns the largest supported picture size, as compared by its area.
    284      */
    285     public static Size getLargestSupportedJpegSizeByArea(Camera.Parameters params) {
    286         checkNotNull(params, "params must not be null");
    287 
    288         List<Size> supportedJpegSizes = convertSizeList(params.getSupportedPictureSizes());
    289         return SizeAreaComparator.findLargestByArea(supportedJpegSizes);
    290     }
    291 
    292     /**
    293      * Convert a camera area into a human-readable string.
    294      */
    295     public static String stringFromArea(Camera.Area area) {
    296         if (area == null) {
    297             return null;
    298         } else {
    299             StringBuilder sb = new StringBuilder();
    300             Rect r = area.rect;
    301 
    302             sb.setLength(0);
    303             sb.append("(["); sb.append(r.left); sb.append(',');
    304             sb.append(r.top); sb.append("]["); sb.append(r.right);
    305             sb.append(','); sb.append(r.bottom); sb.append(']');
    306 
    307             sb.append(',');
    308             sb.append(area.weight);
    309             sb.append(')');
    310 
    311             return sb.toString();
    312         }
    313     }
    314 
    315     /**
    316      * Convert a camera area list into a human-readable string
    317      * @param areaList a list of areas (null is ok)
    318      */
    319     public static String stringFromAreaList(List<Camera.Area> areaList) {
    320         StringBuilder sb = new StringBuilder();
    321 
    322         if (areaList == null) {
    323             return null;
    324         }
    325 
    326         int i = 0;
    327         for (Camera.Area area : areaList) {
    328             if (area == null) {
    329                 sb.append("null");
    330             } else {
    331                 sb.append(stringFromArea(area));
    332             }
    333 
    334             if (i != areaList.size() - 1) {
    335                 sb.append(", ");
    336             }
    337 
    338             i++;
    339         }
    340 
    341         return sb.toString();
    342     }
    343 
    344     /**
    345      * Calculate the closest zoom index for the user-requested crop region by rounding
    346      * up to the closest (largest or equal) possible zoom crop.
    347      *
    348      * <p>If the requested crop region exceeds the size of the active array, it is
    349      * shrunk to fit inside of the active array first.</p>
    350      *
    351      * <p>Since all api1 camera devices only support a discrete set of zooms, we have
    352      * to translate the per-pixel-granularity requested crop region into a per-zoom-index
    353      * granularity.</p>
    354      *
    355      * <p>Furthermore, since the zoom index and zoom levels also depends on the field-of-view
    356      * of the preview, the current preview {@code streamSize} is also used.</p>
    357      *
    358      * <p>The calculated crop regions are then written to in-place to {@code reportedCropRegion}
    359      * and {@code previewCropRegion}, in coordinates relative to the active array.</p>
    360      *
    361      * @param params non-{@code null} camera api1 parameters
    362      * @param activeArray active array dimensions, in sensor space
    363      * @param streamSize stream size dimensions, in pixels
    364      * @param cropRegion user-specified crop region, in active array coordinates
    365      * @param reportedCropRegion (out parameter) what the result for {@code cropRegion} looks like
    366      * @param previewCropRegion (out parameter) what the visual preview crop is
    367      * @return
    368      *          the zoom index inclusively between 0 and {@code Parameters#getMaxZoom},
    369      *          where 0 means the camera is not zoomed
    370      *
    371      * @throws NullPointerException if any of the args were {@code null}
    372      */
    373     public static int getClosestAvailableZoomCrop(
    374             Camera.Parameters params, Rect activeArray, Size streamSize, Rect cropRegion,
    375             /*out*/
    376             Rect reportedCropRegion,
    377             Rect previewCropRegion) {
    378         checkNotNull(params, "params must not be null");
    379         checkNotNull(activeArray, "activeArray must not be null");
    380         checkNotNull(streamSize, "streamSize must not be null");
    381         checkNotNull(reportedCropRegion, "reportedCropRegion must not be null");
    382         checkNotNull(previewCropRegion, "previewCropRegion must not be null");
    383 
    384         Rect actualCrop = new Rect(cropRegion);
    385 
    386         /*
    387          * Shrink requested crop region to fit inside of the active array size
    388          */
    389         if (!actualCrop.intersect(activeArray)) {
    390             Log.w(TAG, "getClosestAvailableZoomCrop - Crop region out of range; " +
    391                     "setting to active array size");
    392             actualCrop.set(activeArray);
    393         }
    394 
    395         Rect previewCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize);
    396 
    397         // Make the user-requested crop region the same aspect ratio as the preview stream size
    398         Rect cropRegionAsPreview =
    399                 shrinkToSameAspectRatioCentered(previewCrop, actualCrop);
    400 
    401         if (DEBUG) {
    402             Log.v(TAG, "getClosestAvailableZoomCrop - actualCrop = " + actualCrop);
    403             Log.v(TAG,
    404                     "getClosestAvailableZoomCrop - previewCrop = " + previewCrop);
    405             Log.v(TAG,
    406                     "getClosestAvailableZoomCrop - cropRegionAsPreview = " + cropRegionAsPreview);
    407         }
    408 
    409         /*
    410          * Iterate all available zoom rectangles and find the closest zoom index
    411          */
    412         Rect bestReportedCropRegion = null;
    413         Rect bestPreviewCropRegion = null;
    414         int bestZoomIndex = -1;
    415 
    416         List<Rect> availableReportedCropRegions =
    417                 getAvailableZoomCropRectangles(params, activeArray);
    418         List<Rect> availablePreviewCropRegions =
    419                 getAvailablePreviewZoomCropRectangles(params, activeArray, streamSize);
    420 
    421         if (DEBUG) {
    422             Log.v(TAG,
    423                     "getClosestAvailableZoomCrop - availableReportedCropRegions = " +
    424                             ListUtils.listToString(availableReportedCropRegions));
    425             Log.v(TAG,
    426                     "getClosestAvailableZoomCrop - availablePreviewCropRegions = " +
    427                             ListUtils.listToString(availablePreviewCropRegions));
    428         }
    429 
    430         if (availableReportedCropRegions.size() != availablePreviewCropRegions.size()) {
    431             throw new AssertionError("available reported/preview crop region size mismatch");
    432         }
    433 
    434         for (int i = 0; i < availableReportedCropRegions.size(); ++i) {
    435             Rect currentPreviewCropRegion = availablePreviewCropRegions.get(i);
    436             Rect currentReportedCropRegion = availableReportedCropRegions.get(i);
    437 
    438             boolean isBest;
    439             if (bestZoomIndex == -1) {
    440                 isBest = true;
    441             } else if (currentPreviewCropRegion.width() >= cropRegionAsPreview.width() &&
    442                     currentPreviewCropRegion.height() >= cropRegionAsPreview.height()) {
    443                 isBest = true;
    444             } else {
    445                 isBest = false;
    446             }
    447 
    448             // Sizes are sorted largest-to-smallest, so once the available crop is too small,
    449             // we the rest are too small. Furthermore, this is the final best crop,
    450             // since its the largest crop that still fits the requested crop
    451             if (isBest) {
    452                 bestPreviewCropRegion = currentPreviewCropRegion;
    453                 bestReportedCropRegion = currentReportedCropRegion;
    454                 bestZoomIndex = i;
    455             } else {
    456                 break;
    457             }
    458         }
    459 
    460         if (bestZoomIndex == -1) {
    461             // Even in the worst case, we should always at least return 0 here
    462             throw new AssertionError("Should've found at least one valid zoom index");
    463         }
    464 
    465         // Write the rectangles in-place
    466         reportedCropRegion.set(bestReportedCropRegion);
    467         previewCropRegion.set(bestPreviewCropRegion);
    468 
    469         return bestZoomIndex;
    470     }
    471 
    472     /**
    473      * Calculate the effective crop rectangle for this preview viewport;
    474      * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions
    475      * without skewing.
    476      *
    477      * <p>The preview size must be a subset of the active array size; the resulting
    478      * rectangle will also be a subset of the active array rectangle.</p>
    479      *
    480      * <p>The unzoomed crop rectangle is calculated only.</p>
    481      *
    482      * @param activeArray active array dimensions, in sensor space
    483      * @param previewSize size of the preview buffer render target, in pixels (not in sensor space)
    484      * @return a rectangle which serves as the preview stream's effective crop region (unzoomed),
    485      *         in sensor space
    486      *
    487      * @throws NullPointerException
    488      *          if any of the args were {@code null}
    489      * @throws IllegalArgumentException
    490      *          if {@code previewSize} is wider or taller than {@code activeArray}
    491      */
    492     private static Rect getPreviewCropRectangleUnzoomed(Rect activeArray, Size previewSize) {
    493         if (previewSize.getWidth() > activeArray.width()) {
    494             throw new IllegalArgumentException("previewSize must not be wider than activeArray");
    495         } else if (previewSize.getHeight() > activeArray.height()) {
    496             throw new IllegalArgumentException("previewSize must not be taller than activeArray");
    497         }
    498 
    499         float aspectRatioArray = activeArray.width() * 1.0f / activeArray.height();
    500         float aspectRatioPreview = previewSize.getWidth() * 1.0f / previewSize.getHeight();
    501 
    502         float cropH, cropW;
    503         if (Math.abs(aspectRatioPreview - aspectRatioArray) < ASPECT_RATIO_TOLERANCE) {
    504             cropH = activeArray.height();
    505             cropW = activeArray.width();
    506         } else if (aspectRatioPreview < aspectRatioArray) {
    507             // The new width must be smaller than the height, so scale the width by AR
    508             cropH = activeArray.height();
    509             cropW = cropH * aspectRatioPreview;
    510         } else {
    511             // The new height must be smaller (or equal) than the width, so scale the height by AR
    512             cropW = activeArray.width();
    513             cropH = cropW / aspectRatioPreview;
    514         }
    515 
    516         Matrix translateMatrix = new Matrix();
    517         RectF cropRect = new RectF(/*left*/0, /*top*/0, cropW, cropH);
    518 
    519         // Now center the crop rectangle so its center is in the center of the active array
    520         translateMatrix.setTranslate(activeArray.exactCenterX(), activeArray.exactCenterY());
    521         translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY());
    522 
    523         translateMatrix.mapRect(/*inout*/cropRect);
    524 
    525         // Round the rect corners towards the nearest integer values
    526         return ParamsUtils.createRect(cropRect);
    527     }
    528 
    529     /**
    530      * Shrink the {@code shrinkTarget} rectangle to snugly fit inside of {@code reference};
    531      * the aspect ratio of {@code shrinkTarget} will change to be the same aspect ratio as
    532      * {@code reference}.
    533      *
    534      * <p>At most a single dimension will scale (down). Both dimensions will never be scaled.</p>
    535      *
    536      * @param reference the rectangle whose aspect ratio will be used as the new aspect ratio
    537      * @param shrinkTarget the rectangle which will be scaled down to have a new aspect ratio
    538      *
    539      * @return a new rectangle, a subset of {@code shrinkTarget},
    540      *          whose aspect ratio will match that of {@code reference}
    541      */
    542     private static Rect shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget) {
    543         float aspectRatioReference = reference.width() * 1.0f / reference.height();
    544         float aspectRatioShrinkTarget = shrinkTarget.width() * 1.0f / shrinkTarget.height();
    545 
    546         float cropH, cropW;
    547         if (aspectRatioShrinkTarget < aspectRatioReference) {
    548             // The new width must be smaller than the height, so scale the width by AR
    549             cropH = reference.height();
    550             cropW = cropH * aspectRatioShrinkTarget;
    551         } else {
    552             // The new height must be smaller (or equal) than the width, so scale the height by AR
    553             cropW = reference.width();
    554             cropH = cropW / aspectRatioShrinkTarget;
    555         }
    556 
    557         Matrix translateMatrix = new Matrix();
    558         RectF shrunkRect = new RectF(shrinkTarget);
    559 
    560         // Scale the rectangle down, but keep its center in the same place as before
    561         translateMatrix.setScale(cropW / reference.width(), cropH / reference.height(),
    562                 shrinkTarget.exactCenterX(), shrinkTarget.exactCenterY());
    563 
    564         translateMatrix.mapRect(/*inout*/shrunkRect);
    565 
    566         return ParamsUtils.createRect(shrunkRect);
    567     }
    568 
    569     /**
    570      * Get the available 'crop' (zoom) rectangles for this camera that will be reported
    571      * via a {@code CaptureResult} when a zoom is requested.
    572      *
    573      * <p>These crops ignores the underlying preview buffer size, and will always be reported
    574      * the same values regardless of what configuration of outputs is used.</p>
    575      *
    576      * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
    577      * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
    578      *
    579      * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
    580      * by shrinking the rectangle if necessary.</p>
    581      *
    582      * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
    583      * = {@code activeArray size}.</p>
    584      *
    585      * @param params non-{@code null} camera api1 parameters
    586      * @param activeArray active array dimensions, in sensor space
    587      * @param streamSize stream size dimensions, in pixels
    588      *
    589      * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
    590      */
    591     public static List<Rect> getAvailableZoomCropRectangles(
    592             Camera.Parameters params, Rect activeArray) {
    593         checkNotNull(params, "params must not be null");
    594         checkNotNull(activeArray, "activeArray must not be null");
    595 
    596         return getAvailableCropRectangles(params, activeArray, ParamsUtils.createSize(activeArray));
    597     }
    598 
    599     /**
    600      * Get the available 'crop' (zoom) rectangles for this camera.
    601      *
    602      * <p>This is the effective (real) crop that is applied by the camera api1 device
    603      * when projecting the zoom onto the intermediate preview buffer. Use this when
    604      * deciding which zoom ratio to apply.</p>
    605      *
    606      * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
    607      * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
    608      *
    609      * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
    610      * by shrinking the rectangle if necessary.</p>
    611      *
    612      * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
    613      * = {@code activeArray size}.</p>
    614      *
    615      * @param params non-{@code null} camera api1 parameters
    616      * @param activeArray active array dimensions, in sensor space
    617      * @param streamSize stream size dimensions, in pixels
    618      *
    619      * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
    620      */
    621     public static List<Rect> getAvailablePreviewZoomCropRectangles(Camera.Parameters params,
    622             Rect activeArray, Size previewSize) {
    623         checkNotNull(params, "params must not be null");
    624         checkNotNull(activeArray, "activeArray must not be null");
    625         checkNotNull(previewSize, "previewSize must not be null");
    626 
    627         return getAvailableCropRectangles(params, activeArray, previewSize);
    628     }
    629 
    630     /**
    631      * Get the available 'crop' (zoom) rectangles for this camera.
    632      *
    633      * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
    634      * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
    635      *
    636      * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
    637      * by shrinking the rectangle if necessary.</p>
    638      *
    639      * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
    640      * = {@code activeArray size}.</p>
    641      *
    642      * @param params non-{@code null} camera api1 parameters
    643      * @param activeArray active array dimensions, in sensor space
    644      * @param streamSize stream size dimensions, in pixels
    645      *
    646      * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
    647      */
    648     private static List<Rect> getAvailableCropRectangles(Camera.Parameters params,
    649             Rect activeArray, Size streamSize) {
    650         checkNotNull(params, "params must not be null");
    651         checkNotNull(activeArray, "activeArray must not be null");
    652         checkNotNull(streamSize, "streamSize must not be null");
    653 
    654         // TODO: change all uses of Rect activeArray to Size activeArray,
    655         // since we want the crop to be active-array relative, not pixel-array relative
    656 
    657         Rect unzoomedStreamCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize);
    658 
    659         if (!params.isZoomSupported()) {
    660             // Trivial case: No zoom -> only support the full size as the crop region
    661             return new ArrayList<>(Arrays.asList(unzoomedStreamCrop));
    662         }
    663 
    664         List<Rect> zoomCropRectangles = new ArrayList<>(params.getMaxZoom() + 1);
    665         Matrix scaleMatrix = new Matrix();
    666         RectF scaledRect = new RectF();
    667 
    668         for (int zoom : params.getZoomRatios()) {
    669             float shrinkRatio = ZOOM_RATIO_MULTIPLIER * 1.0f / zoom; // normalize to 1.0 and smaller
    670 
    671             // set scaledRect to unzoomedStreamCrop
    672             ParamsUtils.convertRectF(unzoomedStreamCrop, /*out*/scaledRect);
    673 
    674             scaleMatrix.setScale(
    675                     shrinkRatio, shrinkRatio,
    676                     activeArray.exactCenterX(),
    677                     activeArray.exactCenterY());
    678 
    679             scaleMatrix.mapRect(scaledRect);
    680 
    681             Rect intRect = ParamsUtils.createRect(scaledRect);
    682 
    683             // Round the rect corners towards the nearest integer values
    684             zoomCropRectangles.add(intRect);
    685         }
    686 
    687         return zoomCropRectangles;
    688     }
    689 
    690     /**
    691      * Get the largest possible zoom ratio (normalized to {@code 1.0f} and higher)
    692      * that the camera can support.
    693      *
    694      * <p>If the camera does not support zoom, it always returns {@code 1.0f}.</p>
    695      *
    696      * @param params non-{@code null} camera api1 parameters
    697      * @return normalized max zoom ratio, at least {@code 1.0f}
    698      */
    699     public static float getMaxZoomRatio(Camera.Parameters params) {
    700         if (!params.isZoomSupported()) {
    701             return 1.0f; // no zoom
    702         }
    703 
    704         List<Integer> zoomRatios = params.getZoomRatios(); // sorted smallest->largest
    705         int zoom = zoomRatios.get(zoomRatios.size() - 1); // largest zoom ratio
    706         float zoomRatio = zoom * 1.0f / ZOOM_RATIO_MULTIPLIER; // normalize to 1.0 and smaller
    707 
    708         return zoomRatio;
    709     }
    710 
    711     /**
    712      * Returns the component-wise zoom ratio (each greater or equal than {@code 1.0});
    713      * largest values means more zoom.
    714      *
    715      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    716      * @param cropSize size of the crop/zoom
    717      *
    718      * @return {@link SizeF} with width/height being the component-wise zoom ratio
    719      *
    720      * @throws NullPointerException if any of the args were {@code null}
    721      * @throws IllegalArgumentException if any component of {@code cropSize} was {@code 0}
    722      */
    723     private static SizeF getZoomRatio(Size activeArraySize, Size cropSize) {
    724         checkNotNull(activeArraySize, "activeArraySize must not be null");
    725         checkNotNull(cropSize, "cropSize must not be null");
    726         checkArgumentPositive(cropSize.getWidth(), "cropSize.width must be positive");
    727         checkArgumentPositive(cropSize.getHeight(), "cropSize.height must be positive");
    728 
    729         float zoomRatioWidth = activeArraySize.getWidth() * 1.0f / cropSize.getWidth();
    730         float zoomRatioHeight = activeArraySize.getHeight() * 1.0f / cropSize.getHeight();
    731 
    732         return new SizeF(zoomRatioWidth, zoomRatioHeight);
    733     }
    734 
    735     /**
    736      * Convert the user-specified crop region into zoom data; which can be used
    737      * to set the parameters to a specific zoom index, or to report back to the user what the
    738      * actual zoom was, or for other calculations requiring the current preview crop region.
    739      *
    740      * <p>None of the parameters are mutated.</p>
    741      *
    742      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    743      * @param cropRegion the user-specified crop region
    744      * @param previewSize the current preview size (in pixels)
    745      * @param params the current camera parameters (not mutated)
    746      *
    747      * @return the zoom index, and the effective/reported crop regions (relative to active array)
    748      */
    749     public static ZoomData convertScalerCropRegion(Rect activeArraySize, Rect
    750             cropRegion, Size previewSize, Camera.Parameters params) {
    751         Rect activeArraySizeOnly = new Rect(
    752                 /*left*/0, /*top*/0,
    753                 activeArraySize.width(), activeArraySize.height());
    754 
    755         Rect userCropRegion = cropRegion;
    756 
    757         if (userCropRegion == null) {
    758             userCropRegion = activeArraySizeOnly;
    759         }
    760 
    761         if (DEBUG) {
    762             Log.v(TAG, "convertScalerCropRegion - user crop region was " + userCropRegion);
    763         }
    764 
    765         final Rect reportedCropRegion = new Rect();
    766         final Rect previewCropRegion = new Rect();
    767         final int zoomIdx = ParameterUtils.getClosestAvailableZoomCrop(params, activeArraySizeOnly,
    768                 previewSize, userCropRegion,
    769                 /*out*/reportedCropRegion, /*out*/previewCropRegion);
    770 
    771         if (DEBUG) {
    772             Log.v(TAG, "convertScalerCropRegion - zoom calculated to: " +
    773                     "zoomIndex = " + zoomIdx +
    774                     ", reported crop region = " + reportedCropRegion +
    775                     ", preview crop region = " + previewCropRegion);
    776         }
    777 
    778         return new ZoomData(zoomIdx, previewCropRegion, reportedCropRegion);
    779     }
    780 
    781     /**
    782      * Calculate the actual/effective/reported normalized rectangle data from a metering
    783      * rectangle.
    784      *
    785      * <p>If any of the rectangles are out-of-range of their intended bounding box,
    786      * the {@link #RECTANGLE_EMPTY empty rectangle} is substituted instead
    787      * (with a weight of {@code 0}).</p>
    788      *
    789      * <p>The metering rectangle is bound by the crop region (effective/reported respectively).
    790      * The metering {@link Camera.Area area} is bound by {@code [-1000, 1000]}.</p>
    791      *
    792      * <p>No parameters are mutated; returns the new metering data.</p>
    793      *
    794      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    795      * @param meteringRect the user-specified metering rectangle
    796      * @param zoomData the calculated zoom data corresponding to this request
    797      *
    798      * @return the metering area, the reported/effective metering rectangles
    799      */
    800     public static MeteringData convertMeteringRectangleToLegacy(
    801             Rect activeArray, MeteringRectangle meteringRect, ZoomData zoomData) {
    802         Rect previewCrop = zoomData.previewCrop;
    803 
    804         float scaleW = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f /
    805                 previewCrop.width();
    806         float scaleH = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f /
    807                 previewCrop.height();
    808 
    809         Matrix transform = new Matrix();
    810         // Move the preview crop so that top,left is at (0,0), otherwise after scaling
    811         // the corner bounds will be outside of [-1000, 1000]
    812         transform.setTranslate(-previewCrop.left, -previewCrop.top);
    813         // Scale into [0, 2000] range about the center of the preview
    814         transform.postScale(scaleW, scaleH);
    815         // Move so that top left of a typical rect is at [-1000, -1000]
    816         transform.postTranslate(/*dx*/NORMALIZED_RECTANGLE_MIN, /*dy*/NORMALIZED_RECTANGLE_MIN);
    817 
    818         /*
    819          * Calculate the preview metering region (effective), and the camera1 api
    820          * normalized metering region.
    821          */
    822         Rect normalizedRegionUnbounded = ParamsUtils.mapRect(transform, meteringRect.getRect());
    823 
    824         /*
    825          * Try to intersect normalized area with [-1000, 1000] rectangle; otherwise
    826          * it's completely out of range
    827          */
    828         Rect normalizedIntersected = new Rect(normalizedRegionUnbounded);
    829 
    830         Camera.Area meteringArea;
    831         if (!normalizedIntersected.intersect(NORMALIZED_RECTANGLE_DEFAULT)) {
    832             Log.w(TAG,
    833                     "convertMeteringRectangleToLegacy - metering rectangle too small, " +
    834                     "no metering will be done");
    835             normalizedIntersected.set(RECTANGLE_EMPTY);
    836             meteringArea = new Camera.Area(RECTANGLE_EMPTY,
    837                     MeteringRectangle.METERING_WEIGHT_DONT_CARE);
    838         } else {
    839             meteringArea = new Camera.Area(normalizedIntersected,
    840                     meteringRect.getMeteringWeight());
    841         }
    842 
    843         /*
    844          * Calculate effective preview metering region
    845          */
    846         Rect previewMetering = meteringRect.getRect();
    847         if (!previewMetering.intersect(previewCrop)) {
    848             previewMetering.set(RECTANGLE_EMPTY);
    849         }
    850 
    851         /*
    852          * Calculate effective reported metering region
    853          * - Transform the calculated metering area back into active array space
    854          * - Clip it to be a subset of the reported crop region
    855          */
    856         Rect reportedMetering;
    857         {
    858             Camera.Area normalizedAreaUnbounded = new Camera.Area(
    859                     normalizedRegionUnbounded, meteringRect.getMeteringWeight());
    860             WeightedRectangle reportedMeteringRect = convertCameraAreaToActiveArrayRectangle(
    861                     activeArray, zoomData, normalizedAreaUnbounded, /*usePreviewCrop*/false);
    862             reportedMetering = reportedMeteringRect.rect;
    863         }
    864 
    865         if (DEBUG) {
    866             Log.v(TAG, String.format(
    867                     "convertMeteringRectangleToLegacy - activeArray = %s, meteringRect = %s, " +
    868                     "previewCrop = %s, meteringArea = %s, previewMetering = %s, " +
    869                     "reportedMetering = %s, normalizedRegionUnbounded = %s",
    870                     activeArray, meteringRect,
    871                     previewCrop, stringFromArea(meteringArea), previewMetering,
    872                     reportedMetering, normalizedRegionUnbounded));
    873         }
    874 
    875         return new MeteringData(meteringArea, previewMetering, reportedMetering);
    876     }
    877 
    878     /**
    879      * Convert the normalized camera area from [-1000, 1000] coordinate space
    880      * into the active array-based coordinate space.
    881      *
    882      * <p>Values out of range are clipped to be within the resulting (reported) crop
    883      * region. It is possible to have values larger than the preview crop.</p>
    884      *
    885      * <p>Weights out of range of [0, 1000] are clipped to be within the range.</p>
    886      *
    887      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    888      * @param zoomData the calculated zoom data corresponding to this request
    889      * @param area the normalized camera area
    890      *
    891      * @return the weighed rectangle in active array coordinate space, with the weight
    892      */
    893     public static WeightedRectangle convertCameraAreaToActiveArrayRectangle(
    894             Rect activeArray, ZoomData zoomData, Camera.Area area) {
    895         return convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, area,
    896                 /*usePreviewCrop*/true);
    897     }
    898 
    899     /**
    900      * Convert an api1 face into an active-array based api2 face.
    901      *
    902      * <p>Out-of-ranges scores and ids will be clipped to be within range (with a warning).</p>
    903      *
    904      * @param face a non-{@code null} api1 face
    905      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    906      * @param zoomData the calculated zoom data corresponding to this request
    907      *
    908      * @return a non-{@code null} api2 face
    909      *
    910      * @throws NullPointerException if the {@code face} was {@code null}
    911      */
    912     public static Face convertFaceFromLegacy(Camera.Face face, Rect activeArray,
    913             ZoomData zoomData) {
    914         checkNotNull(face, "face must not be null");
    915 
    916         Face api2Face;
    917 
    918         Camera.Area fakeArea = new Camera.Area(face.rect, /*weight*/1);
    919 
    920         WeightedRectangle faceRect =
    921                 convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, fakeArea);
    922 
    923         Point leftEye = face.leftEye, rightEye = face.rightEye, mouth = face.mouth;
    924         if (leftEye != null && rightEye != null && mouth != null && leftEye.x != -2000 &&
    925                 leftEye.y != -2000 && rightEye.x != -2000 && rightEye.y != -2000 &&
    926                 mouth.x != -2000 && mouth.y != -2000) {
    927             leftEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
    928                     leftEye, /*usePreviewCrop*/true);
    929             rightEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
    930                     leftEye, /*usePreviewCrop*/true);
    931             mouth = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
    932                     leftEye, /*usePreviewCrop*/true);
    933 
    934             api2Face = faceRect.toFace(face.id, leftEye, rightEye, mouth);
    935         } else {
    936             api2Face = faceRect.toFace();
    937         }
    938 
    939         return api2Face;
    940     }
    941 
    942     private static Point convertCameraPointToActiveArrayPoint(
    943             Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop) {
    944         Rect pointedRect = new Rect(point.x, point.y, point.x, point.y);
    945         Camera.Area pointedArea = new Area(pointedRect, /*weight*/1);
    946 
    947         WeightedRectangle adjustedRect =
    948                 convertCameraAreaToActiveArrayRectangle(activeArray,
    949                         zoomData, pointedArea, usePreviewCrop);
    950 
    951         Point transformedPoint = new Point(adjustedRect.rect.left, adjustedRect.rect.top);
    952 
    953         return transformedPoint;
    954     }
    955 
    956     private static WeightedRectangle convertCameraAreaToActiveArrayRectangle(
    957             Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop) {
    958         Rect previewCrop = zoomData.previewCrop;
    959         Rect reportedCrop = zoomData.reportedCrop;
    960 
    961         float scaleW = previewCrop.width() * 1.0f /
    962                 (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN);
    963         float scaleH = previewCrop.height() * 1.0f /
    964                 (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN);
    965 
    966         /*
    967          * Calculate the reported metering region from the non-intersected normalized region
    968          * by scaling and translating back into active array-relative coordinates.
    969          */
    970         Matrix transform = new Matrix();
    971 
    972         // Move top left from (-1000, -1000) to (0, 0)
    973         transform.setTranslate(/*dx*/NORMALIZED_RECTANGLE_MAX, /*dy*/NORMALIZED_RECTANGLE_MAX);
    974 
    975         // Scale from [0, 2000] back into the preview rectangle
    976         transform.postScale(scaleW, scaleH);
    977 
    978         // Move the rect so that the [-1000,-1000] point ends up at the preview [left, top]
    979         transform.postTranslate(previewCrop.left, previewCrop.top);
    980 
    981         Rect cropToIntersectAgainst = usePreviewCrop ? previewCrop : reportedCrop;
    982 
    983         // Now apply the transformation backwards to get the reported metering region
    984         Rect reportedMetering = ParamsUtils.mapRect(transform, area.rect);
    985         // Intersect it with the crop region, to avoid reporting out-of-bounds
    986         // metering regions
    987         if (!reportedMetering.intersect(cropToIntersectAgainst)) {
    988             reportedMetering.set(RECTANGLE_EMPTY);
    989         }
    990 
    991         int weight = area.weight;
    992         if (weight < MeteringRectangle.METERING_WEIGHT_MIN) {
    993             Log.w(TAG,
    994                     "convertCameraAreaToMeteringRectangle - rectangle "
    995                             + stringFromArea(area) + " has too small weight, clip to 0");
    996             weight = 0;
    997         }
    998 
    999         return new WeightedRectangle(reportedMetering, area.weight);
   1000     }
   1001 
   1002 
   1003     private ParameterUtils() {
   1004         throw new AssertionError();
   1005     }
   1006 }
   1007