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.legacy.ParameterUtils.MeteringData;
     26 import android.hardware.camera2.legacy.ParameterUtils.ZoomData;
     27 import android.hardware.camera2.params.Face;
     28 import android.hardware.camera2.params.MeteringRectangle;
     29 import android.hardware.camera2.utils.ListUtils;
     30 import android.hardware.camera2.utils.ParamsUtils;
     31 import android.hardware.camera2.utils.SizeAreaComparator;
     32 import android.util.Size;
     33 import android.util.SizeF;
     34 
     35 import android.util.Log;
     36 
     37 import java.util.ArrayList;
     38 import java.util.Arrays;
     39 import java.util.List;
     40 
     41 import static com.android.internal.util.Preconditions.*;
     42 
     43 /**
     44  * Various utilities for dealing with camera API1 parameters.
     45  */
     46 @SuppressWarnings("deprecation")
     47 public class ParameterUtils {
     48     /** Upper/left minimal point of a normalized rectangle */
     49     public static final int NORMALIZED_RECTANGLE_MIN = -1000;
     50     /** Lower/right maximal point of a normalized rectangle */
     51     public static final int NORMALIZED_RECTANGLE_MAX = 1000;
     52     /** The default normalized rectangle spans the entire size of the preview viewport */
     53     public static final Rect NORMALIZED_RECTANGLE_DEFAULT = new Rect(
     54             NORMALIZED_RECTANGLE_MIN,
     55             NORMALIZED_RECTANGLE_MIN,
     56             NORMALIZED_RECTANGLE_MAX,
     57             NORMALIZED_RECTANGLE_MAX);
     58     /** The default normalized area uses the default normalized rectangle with a weight=1 */
     59     public static final Camera.Area CAMERA_AREA_DEFAULT =
     60             new Camera.Area(new Rect(NORMALIZED_RECTANGLE_DEFAULT),
     61                             /*weight*/1);
     62     /** Empty rectangle {@code 0x0+0,0} */
     63     public static final Rect RECTANGLE_EMPTY =
     64             new Rect(/*left*/0, /*top*/0, /*right*/0, /*bottom*/0);
     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 VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
    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 (VERBOSE) {
    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 (VERBOSE) {
    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 (aspectRatioPreview < aspectRatioArray) {
    504             // The new width must be smaller than the height, so scale the width by AR
    505             cropH = activeArray.height();
    506             cropW = cropH * aspectRatioPreview;
    507         } else {
    508             // The new height must be smaller (or equal) than the width, so scale the height by AR
    509             cropW = activeArray.width();
    510             cropH = cropW / aspectRatioPreview;
    511         }
    512 
    513         Matrix translateMatrix = new Matrix();
    514         RectF cropRect = new RectF(/*left*/0, /*top*/0, cropW, cropH);
    515 
    516         // Now center the crop rectangle so its center is in the center of the active array
    517         translateMatrix.setTranslate(activeArray.exactCenterX(), activeArray.exactCenterY());
    518         translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY());
    519 
    520         translateMatrix.mapRect(/*inout*/cropRect);
    521 
    522         // Round the rect corners towards the nearest integer values
    523         return ParamsUtils.createRect(cropRect);
    524     }
    525 
    526     /**
    527      * Shrink the {@code shrinkTarget} rectangle to snugly fit inside of {@code reference};
    528      * the aspect ratio of {@code shrinkTarget} will change to be the same aspect ratio as
    529      * {@code reference}.
    530      *
    531      * <p>At most a single dimension will scale (down). Both dimensions will never be scaled.</p>
    532      *
    533      * @param reference the rectangle whose aspect ratio will be used as the new aspect ratio
    534      * @param shrinkTarget the rectangle which will be scaled down to have a new aspect ratio
    535      *
    536      * @return a new rectangle, a subset of {@code shrinkTarget},
    537      *          whose aspect ratio will match that of {@code reference}
    538      */
    539     private static Rect shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget) {
    540         float aspectRatioReference = reference.width() * 1.0f / reference.height();
    541         float aspectRatioShrinkTarget = shrinkTarget.width() * 1.0f / shrinkTarget.height();
    542 
    543         float cropH, cropW;
    544         if (aspectRatioShrinkTarget < aspectRatioReference) {
    545             // The new width must be smaller than the height, so scale the width by AR
    546             cropH = reference.height();
    547             cropW = cropH * aspectRatioShrinkTarget;
    548         } else {
    549             // The new height must be smaller (or equal) than the width, so scale the height by AR
    550             cropW = reference.width();
    551             cropH = cropW / aspectRatioShrinkTarget;
    552         }
    553 
    554         Matrix translateMatrix = new Matrix();
    555         RectF shrunkRect = new RectF(shrinkTarget);
    556 
    557         // Scale the rectangle down, but keep its center in the same place as before
    558         translateMatrix.setScale(cropW / reference.width(), cropH / reference.height(),
    559                 shrinkTarget.exactCenterX(), shrinkTarget.exactCenterY());
    560 
    561         translateMatrix.mapRect(/*inout*/shrunkRect);
    562 
    563         return ParamsUtils.createRect(shrunkRect);
    564     }
    565 
    566     /**
    567      * Get the available 'crop' (zoom) rectangles for this camera that will be reported
    568      * via a {@code CaptureResult} when a zoom is requested.
    569      *
    570      * <p>These crops ignores the underlying preview buffer size, and will always be reported
    571      * the same values regardless of what configuration of outputs is used.</p>
    572      *
    573      * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
    574      * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
    575      *
    576      * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
    577      * by shrinking the rectangle if necessary.</p>
    578      *
    579      * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
    580      * = {@code activeArray size}.</p>
    581      *
    582      * @param params non-{@code null} camera api1 parameters
    583      * @param activeArray active array dimensions, in sensor space
    584      * @param streamSize stream size dimensions, in pixels
    585      *
    586      * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
    587      */
    588     public static List<Rect> getAvailableZoomCropRectangles(
    589             Camera.Parameters params, Rect activeArray) {
    590         checkNotNull(params, "params must not be null");
    591         checkNotNull(activeArray, "activeArray must not be null");
    592 
    593         return getAvailableCropRectangles(params, activeArray, ParamsUtils.createSize(activeArray));
    594     }
    595 
    596     /**
    597      * Get the available 'crop' (zoom) rectangles for this camera.
    598      *
    599      * <p>This is the effective (real) crop that is applied by the camera api1 device
    600      * when projecting the zoom onto the intermediate preview buffer. Use this when
    601      * deciding which zoom ratio to apply.</p>
    602      *
    603      * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
    604      * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
    605      *
    606      * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
    607      * by shrinking the rectangle if necessary.</p>
    608      *
    609      * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
    610      * = {@code activeArray size}.</p>
    611      *
    612      * @param params non-{@code null} camera api1 parameters
    613      * @param activeArray active array dimensions, in sensor space
    614      * @param streamSize stream size dimensions, in pixels
    615      *
    616      * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
    617      */
    618     public static List<Rect> getAvailablePreviewZoomCropRectangles(Camera.Parameters params,
    619             Rect activeArray, Size previewSize) {
    620         checkNotNull(params, "params must not be null");
    621         checkNotNull(activeArray, "activeArray must not be null");
    622         checkNotNull(previewSize, "previewSize must not be null");
    623 
    624         return getAvailableCropRectangles(params, activeArray, previewSize);
    625     }
    626 
    627     /**
    628      * Get the available 'crop' (zoom) rectangles for this camera.
    629      *
    630      * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
    631      * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
    632      *
    633      * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
    634      * by shrinking the rectangle if necessary.</p>
    635      *
    636      * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
    637      * = {@code activeArray size}.</p>
    638      *
    639      * @param params non-{@code null} camera api1 parameters
    640      * @param activeArray active array dimensions, in sensor space
    641      * @param streamSize stream size dimensions, in pixels
    642      *
    643      * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
    644      */
    645     private static List<Rect> getAvailableCropRectangles(Camera.Parameters params,
    646             Rect activeArray, Size streamSize) {
    647         checkNotNull(params, "params must not be null");
    648         checkNotNull(activeArray, "activeArray must not be null");
    649         checkNotNull(streamSize, "streamSize must not be null");
    650 
    651         // TODO: change all uses of Rect activeArray to Size activeArray,
    652         // since we want the crop to be active-array relative, not pixel-array relative
    653 
    654         Rect unzoomedStreamCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize);
    655 
    656         if (!params.isZoomSupported()) {
    657             // Trivial case: No zoom -> only support the full size as the crop region
    658             return new ArrayList<>(Arrays.asList(unzoomedStreamCrop));
    659         }
    660 
    661         List<Rect> zoomCropRectangles = new ArrayList<>(params.getMaxZoom() + 1);
    662         Matrix scaleMatrix = new Matrix();
    663         RectF scaledRect = new RectF();
    664 
    665         for (int zoom : params.getZoomRatios()) {
    666             float shrinkRatio = ZOOM_RATIO_MULTIPLIER * 1.0f / zoom; // normalize to 1.0 and smaller
    667 
    668             // set scaledRect to unzoomedStreamCrop
    669             ParamsUtils.convertRectF(unzoomedStreamCrop, /*out*/scaledRect);
    670 
    671             scaleMatrix.setScale(
    672                     shrinkRatio, shrinkRatio,
    673                     activeArray.exactCenterX(),
    674                     activeArray.exactCenterY());
    675 
    676             scaleMatrix.mapRect(scaledRect);
    677 
    678             Rect intRect = ParamsUtils.createRect(scaledRect);
    679 
    680             // Round the rect corners towards the nearest integer values
    681             zoomCropRectangles.add(intRect);
    682         }
    683 
    684         return zoomCropRectangles;
    685     }
    686 
    687     /**
    688      * Get the largest possible zoom ratio (normalized to {@code 1.0f} and higher)
    689      * that the camera can support.
    690      *
    691      * <p>If the camera does not support zoom, it always returns {@code 1.0f}.</p>
    692      *
    693      * @param params non-{@code null} camera api1 parameters
    694      * @return normalized max zoom ratio, at least {@code 1.0f}
    695      */
    696     public static float getMaxZoomRatio(Camera.Parameters params) {
    697         if (!params.isZoomSupported()) {
    698             return 1.0f; // no zoom
    699         }
    700 
    701         List<Integer> zoomRatios = params.getZoomRatios(); // sorted smallest->largest
    702         int zoom = zoomRatios.get(zoomRatios.size() - 1); // largest zoom ratio
    703         float zoomRatio = zoom * 1.0f / ZOOM_RATIO_MULTIPLIER; // normalize to 1.0 and smaller
    704 
    705         return zoomRatio;
    706     }
    707 
    708     /**
    709      * Returns the component-wise zoom ratio (each greater or equal than {@code 1.0});
    710      * largest values means more zoom.
    711      *
    712      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    713      * @param cropSize size of the crop/zoom
    714      *
    715      * @return {@link SizeF} with width/height being the component-wise zoom ratio
    716      *
    717      * @throws NullPointerException if any of the args were {@code null}
    718      * @throws IllegalArgumentException if any component of {@code cropSize} was {@code 0}
    719      */
    720     private static SizeF getZoomRatio(Size activeArraySize, Size cropSize) {
    721         checkNotNull(activeArraySize, "activeArraySize must not be null");
    722         checkNotNull(cropSize, "cropSize must not be null");
    723         checkArgumentPositive(cropSize.getWidth(), "cropSize.width must be positive");
    724         checkArgumentPositive(cropSize.getHeight(), "cropSize.height must be positive");
    725 
    726         float zoomRatioWidth = activeArraySize.getWidth() * 1.0f / cropSize.getWidth();
    727         float zoomRatioHeight = activeArraySize.getHeight() * 1.0f / cropSize.getHeight();
    728 
    729         return new SizeF(zoomRatioWidth, zoomRatioHeight);
    730     }
    731 
    732     /**
    733      * Convert the user-specified crop region into zoom data; which can be used
    734      * to set the parameters to a specific zoom index, or to report back to the user what the
    735      * actual zoom was, or for other calculations requiring the current preview crop region.
    736      *
    737      * <p>None of the parameters are mutated.</p>
    738      *
    739      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    740      * @param cropRegion the user-specified crop region
    741      * @param previewSize the current preview size (in pixels)
    742      * @param params the current camera parameters (not mutated)
    743      *
    744      * @return the zoom index, and the effective/reported crop regions (relative to active array)
    745      */
    746     public static ZoomData convertScalerCropRegion(Rect activeArraySize, Rect
    747             cropRegion, Size previewSize, Camera.Parameters params) {
    748         Rect activeArraySizeOnly = new Rect(
    749                 /*left*/0, /*top*/0,
    750                 activeArraySize.width(), activeArraySize.height());
    751 
    752         Rect userCropRegion = cropRegion;
    753 
    754         if (userCropRegion == null) {
    755             userCropRegion = activeArraySizeOnly;
    756         }
    757 
    758         if (VERBOSE) {
    759             Log.v(TAG, "convertScalerCropRegion - user crop region was " + userCropRegion);
    760         }
    761 
    762         final Rect reportedCropRegion = new Rect();
    763         final Rect previewCropRegion = new Rect();
    764         final int zoomIdx = ParameterUtils.getClosestAvailableZoomCrop(params, activeArraySizeOnly,
    765                 previewSize, userCropRegion,
    766                 /*out*/reportedCropRegion, /*out*/previewCropRegion);
    767 
    768         if (VERBOSE) {
    769             Log.v(TAG, "convertScalerCropRegion - zoom calculated to: " +
    770                     "zoomIndex = " + zoomIdx +
    771                     ", reported crop region = " + reportedCropRegion +
    772                     ", preview crop region = " + previewCropRegion);
    773         }
    774 
    775         return new ZoomData(zoomIdx, previewCropRegion, reportedCropRegion);
    776     }
    777 
    778     /**
    779      * Calculate the actual/effective/reported normalized rectangle data from a metering
    780      * rectangle.
    781      *
    782      * <p>If any of the rectangles are out-of-range of their intended bounding box,
    783      * the {@link #RECTANGLE_EMPTY empty rectangle} is substituted instead
    784      * (with a weight of {@code 0}).</p>
    785      *
    786      * <p>The metering rectangle is bound by the crop region (effective/reported respectively).
    787      * The metering {@link Camera.Area area} is bound by {@code [-1000, 1000]}.</p>
    788      *
    789      * <p>No parameters are mutated; returns the new metering data.</p>
    790      *
    791      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    792      * @param meteringRect the user-specified metering rectangle
    793      * @param zoomData the calculated zoom data corresponding to this request
    794      *
    795      * @return the metering area, the reported/effective metering rectangles
    796      */
    797     public static MeteringData convertMeteringRectangleToLegacy(
    798             Rect activeArray, MeteringRectangle meteringRect, ZoomData zoomData) {
    799         Rect previewCrop = zoomData.previewCrop;
    800 
    801         float scaleW = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f /
    802                 previewCrop.width();
    803         float scaleH = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f /
    804                 previewCrop.height();
    805 
    806         Matrix transform = new Matrix();
    807         // Move the preview crop so that top,left is at (0,0), otherwise after scaling
    808         // the corner bounds will be outside of [-1000, 1000]
    809         transform.setTranslate(-previewCrop.left, -previewCrop.top);
    810         // Scale into [0, 2000] range about the center of the preview
    811         transform.postScale(scaleW, scaleH);
    812         // Move so that top left of a typical rect is at [-1000, -1000]
    813         transform.postTranslate(/*dx*/NORMALIZED_RECTANGLE_MIN, /*dy*/NORMALIZED_RECTANGLE_MIN);
    814 
    815         /*
    816          * Calculate the preview metering region (effective), and the camera1 api
    817          * normalized metering region.
    818          */
    819         Rect normalizedRegionUnbounded = ParamsUtils.mapRect(transform, meteringRect.getRect());
    820 
    821         /*
    822          * Try to intersect normalized area with [-1000, 1000] rectangle; otherwise
    823          * it's completely out of range
    824          */
    825         Rect normalizedIntersected = new Rect(normalizedRegionUnbounded);
    826 
    827         Camera.Area meteringArea;
    828         if (!normalizedIntersected.intersect(NORMALIZED_RECTANGLE_DEFAULT)) {
    829             Log.w(TAG,
    830                     "convertMeteringRectangleToLegacy - metering rectangle too small, " +
    831                     "no metering will be done");
    832             normalizedIntersected.set(RECTANGLE_EMPTY);
    833             meteringArea = new Camera.Area(RECTANGLE_EMPTY,
    834                     MeteringRectangle.METERING_WEIGHT_DONT_CARE);
    835         } else {
    836             meteringArea = new Camera.Area(normalizedIntersected,
    837                     meteringRect.getMeteringWeight());
    838         }
    839 
    840         /*
    841          * Calculate effective preview metering region
    842          */
    843         Rect previewMetering = meteringRect.getRect();
    844         if (!previewMetering.intersect(previewCrop)) {
    845             previewMetering.set(RECTANGLE_EMPTY);
    846         }
    847 
    848         /*
    849          * Calculate effective reported metering region
    850          * - Transform the calculated metering area back into active array space
    851          * - Clip it to be a subset of the reported crop region
    852          */
    853         Rect reportedMetering;
    854         {
    855             Camera.Area normalizedAreaUnbounded = new Camera.Area(
    856                     normalizedRegionUnbounded, meteringRect.getMeteringWeight());
    857             WeightedRectangle reportedMeteringRect = convertCameraAreaToActiveArrayRectangle(
    858                     activeArray, zoomData, normalizedAreaUnbounded, /*usePreviewCrop*/false);
    859             reportedMetering = reportedMeteringRect.rect;
    860         }
    861 
    862         if (VERBOSE) {
    863             Log.v(TAG, String.format(
    864                     "convertMeteringRectangleToLegacy - activeArray = %s, meteringRect = %s, " +
    865                     "previewCrop = %s, meteringArea = %s, previewMetering = %s, " +
    866                     "reportedMetering = %s, normalizedRegionUnbounded = %s",
    867                     activeArray, meteringRect,
    868                     previewCrop, stringFromArea(meteringArea), previewMetering,
    869                     reportedMetering, normalizedRegionUnbounded));
    870         }
    871 
    872         return new MeteringData(meteringArea, previewMetering, reportedMetering);
    873     }
    874 
    875     /**
    876      * Convert the normalized camera area from [-1000, 1000] coordinate space
    877      * into the active array-based coordinate space.
    878      *
    879      * <p>Values out of range are clipped to be within the resulting (reported) crop
    880      * region. It is possible to have values larger than the preview crop.</p>
    881      *
    882      * <p>Weights out of range of [0, 1000] are clipped to be within the range.</p>
    883      *
    884      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    885      * @param zoomData the calculated zoom data corresponding to this request
    886      * @param area the normalized camera area
    887      *
    888      * @return the weighed rectangle in active array coordinate space, with the weight
    889      */
    890     public static WeightedRectangle convertCameraAreaToActiveArrayRectangle(
    891             Rect activeArray, ZoomData zoomData, Camera.Area area) {
    892         return convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, area,
    893                 /*usePreviewCrop*/true);
    894     }
    895 
    896     /**
    897      * Convert an api1 face into an active-array based api2 face.
    898      *
    899      * <p>Out-of-ranges scores and ids will be clipped to be within range (with a warning).</p>
    900      *
    901      * @param face a non-{@code null} api1 face
    902      * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
    903      * @param zoomData the calculated zoom data corresponding to this request
    904      *
    905      * @return a non-{@code null} api2 face
    906      *
    907      * @throws NullPointerException if the {@code face} was {@code null}
    908      */
    909     public static Face convertFaceFromLegacy(Camera.Face face, Rect activeArray,
    910             ZoomData zoomData) {
    911         checkNotNull(face, "face must not be null");
    912 
    913         Face api2Face;
    914 
    915         Camera.Area fakeArea = new Camera.Area(face.rect, /*weight*/1);
    916 
    917         WeightedRectangle faceRect =
    918                 convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, fakeArea);
    919 
    920         Point leftEye = face.leftEye, rightEye = face.rightEye, mouth = face.mouth;
    921         if (leftEye != null && rightEye != null && mouth != null && leftEye.x != -2000 &&
    922                 leftEye.y != -2000 && rightEye.x != -2000 && rightEye.y != -2000 &&
    923                 mouth.x != -2000 && mouth.y != -2000) {
    924             leftEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
    925                     leftEye, /*usePreviewCrop*/true);
    926             rightEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
    927                     leftEye, /*usePreviewCrop*/true);
    928             mouth = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
    929                     leftEye, /*usePreviewCrop*/true);
    930 
    931             api2Face = faceRect.toFace(face.id, leftEye, rightEye, mouth);
    932         } else {
    933             api2Face = faceRect.toFace();
    934         }
    935 
    936         return api2Face;
    937     }
    938 
    939     private static Point convertCameraPointToActiveArrayPoint(
    940             Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop) {
    941         Rect pointedRect = new Rect(point.x, point.y, point.x, point.y);
    942         Camera.Area pointedArea = new Area(pointedRect, /*weight*/1);
    943 
    944         WeightedRectangle adjustedRect =
    945                 convertCameraAreaToActiveArrayRectangle(activeArray,
    946                         zoomData, pointedArea, usePreviewCrop);
    947 
    948         Point transformedPoint = new Point(adjustedRect.rect.left, adjustedRect.rect.top);
    949 
    950         return transformedPoint;
    951     }
    952 
    953     private static WeightedRectangle convertCameraAreaToActiveArrayRectangle(
    954             Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop) {
    955         Rect previewCrop = zoomData.previewCrop;
    956         Rect reportedCrop = zoomData.reportedCrop;
    957 
    958         float scaleW = previewCrop.width() * 1.0f /
    959                 (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN);
    960         float scaleH = previewCrop.height() * 1.0f /
    961                 (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN);
    962 
    963         /*
    964          * Calculate the reported metering region from the non-intersected normalized region
    965          * by scaling and translating back into active array-relative coordinates.
    966          */
    967         Matrix transform = new Matrix();
    968 
    969         // Move top left from (-1000, -1000) to (0, 0)
    970         transform.setTranslate(/*dx*/NORMALIZED_RECTANGLE_MAX, /*dy*/NORMALIZED_RECTANGLE_MAX);
    971 
    972         // Scale from [0, 2000] back into the preview rectangle
    973         transform.postScale(scaleW, scaleH);
    974 
    975         // Move the rect so that the [-1000,-1000] point ends up at the preview [left, top]
    976         transform.postTranslate(previewCrop.left, previewCrop.top);
    977 
    978         Rect cropToIntersectAgainst = usePreviewCrop ? previewCrop : reportedCrop;
    979 
    980         // Now apply the transformation backwards to get the reported metering region
    981         Rect reportedMetering = ParamsUtils.mapRect(transform, area.rect);
    982         // Intersect it with the crop region, to avoid reporting out-of-bounds
    983         // metering regions
    984         if (!reportedMetering.intersect(cropToIntersectAgainst)) {
    985             reportedMetering.set(RECTANGLE_EMPTY);
    986         }
    987 
    988         int weight = area.weight;
    989         if (weight < MeteringRectangle.METERING_WEIGHT_MIN) {
    990             Log.w(TAG,
    991                     "convertCameraAreaToMeteringRectangle - rectangle "
    992                             + stringFromArea(area) + " has too small weight, clip to 0");
    993             weight = 0;
    994         }
    995 
    996         return new WeightedRectangle(reportedMetering, area.weight);
    997     }
    998 
    999 
   1000     private ParameterUtils() {
   1001         throw new AssertionError();
   1002     }
   1003 }
   1004