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 
ZoomData(int zoomIndex, Rect previewCrop, Rect reportedCrop)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 
MeteringData(Area meteringArea, Rect previewMetering, Rect reportedMetering)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          */
WeightedRectangle(Rect rect, int weight)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          */
toMetering()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          */
toFace( int id, Point leftEyePosition, Point rightEyePosition, Point mouthPosition)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          */
toFace()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 
clipLower(int value, int lo, Rect rect, String name)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 
clip(int value, int lo, int hi, Rect rect, String name)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      */
convertSize(Camera.Size size)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      */
convertSizeList(List<Camera.Size> sizeList)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      */
convertSizeListToArray(List<Camera.Size> sizeList)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      */
containsSize(List<Camera.Size> sizeList, int width, int height)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      */
getLargestSupportedJpegSizeByArea(Camera.Parameters params)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      */
stringFromArea(Camera.Area area)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      */
stringFromAreaList(List<Camera.Area> areaList)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      */
getClosestAvailableZoomCrop( Camera.Parameters params, Rect activeArray, Size streamSize, Rect cropRegion, Rect reportedCropRegion, Rect previewCropRegion)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      */
getPreviewCropRectangleUnzoomed(Rect activeArray, Size previewSize)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      */
shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget)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      */
getAvailableZoomCropRectangles( Camera.Parameters params, Rect activeArray)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      */
getAvailablePreviewZoomCropRectangles(Camera.Parameters params, Rect activeArray, Size previewSize)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      */
getAvailableCropRectangles(Camera.Parameters params, Rect activeArray, Size streamSize)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      */
getMaxZoomRatio(Camera.Parameters params)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      */
getZoomRatio(Size activeArraySize, Size cropSize)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      */
convertScalerCropRegion(Rect activeArraySize, Rect cropRegion, Size previewSize, Camera.Parameters params)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      */
convertMeteringRectangleToLegacy( Rect activeArray, MeteringRectangle meteringRect, ZoomData zoomData)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      */
convertCameraAreaToActiveArrayRectangle( Rect activeArray, ZoomData zoomData, Camera.Area area)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      */
convertFaceFromLegacy(Camera.Face face, Rect activeArray, ZoomData zoomData)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 
convertCameraPointToActiveArrayPoint( Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop)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 
convertCameraAreaToActiveArrayRectangle( Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop)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 
ParameterUtils()1003     private ParameterUtils() {
1004         throw new AssertionError();
1005     }
1006 }
1007