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