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