ParameterUtils.java revision 0a1ef4dbf39aa3dfae1a91daf972ae3457ce27fe
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     * Returns the largest supported picture size, as compared by its area.
257     */
258    public static Size getLargestSupportedJpegSizeByArea(Camera.Parameters params) {
259        checkNotNull(params, "params must not be null");
260
261        List<Size> supportedJpegSizes = convertSizeList(params.getSupportedPictureSizes());
262        return SizeAreaComparator.findLargestByArea(supportedJpegSizes);
263    }
264
265    /**
266     * Convert a camera area into a human-readable string.
267     */
268    public static String stringFromArea(Camera.Area area) {
269        if (area == null) {
270            return null;
271        } else {
272            StringBuilder sb = new StringBuilder();
273            Rect r = area.rect;
274
275            sb.setLength(0);
276            sb.append("(["); sb.append(r.left); sb.append(',');
277            sb.append(r.top); sb.append("]["); sb.append(r.right);
278            sb.append(','); sb.append(r.bottom); sb.append(']');
279
280            sb.append(',');
281            sb.append(area.weight);
282            sb.append(')');
283
284            return sb.toString();
285        }
286    }
287
288    /**
289     * Convert a camera area list into a human-readable string
290     * @param areaList a list of areas (null is ok)
291     */
292    public static String stringFromAreaList(List<Camera.Area> areaList) {
293        StringBuilder sb = new StringBuilder();
294
295        if (areaList == null) {
296            return null;
297        }
298
299        int i = 0;
300        for (Camera.Area area : areaList) {
301            if (area == null) {
302                sb.append("null");
303            } else {
304                sb.append(stringFromArea(area));
305            }
306
307            if (i != areaList.size() - 1) {
308                sb.append(", ");
309            }
310
311            i++;
312        }
313
314        return sb.toString();
315    }
316
317    /**
318     * Calculate the closest zoom index for the user-requested crop region by rounding
319     * up to the closest (largest or equal) possible zoom crop.
320     *
321     * <p>If the requested crop region exceeds the size of the active array, it is
322     * shrunk to fit inside of the active array first.</p>
323     *
324     * <p>Since all api1 camera devices only support a discrete set of zooms, we have
325     * to translate the per-pixel-granularity requested crop region into a per-zoom-index
326     * granularity.</p>
327     *
328     * <p>Furthermore, since the zoom index and zoom levels also depends on the field-of-view
329     * of the preview, the current preview {@code streamSize} is also used.</p>
330     *
331     * <p>The calculated crop regions are then written to in-place to {@code reportedCropRegion}
332     * and {@code previewCropRegion}, in coordinates relative to the active array.</p>
333     *
334     * @param params non-{@code null} camera api1 parameters
335     * @param activeArray active array dimensions, in sensor space
336     * @param streamSize stream size dimensions, in pixels
337     * @param cropRegion user-specified crop region, in active array coordinates
338     * @param reportedCropRegion (out parameter) what the result for {@code cropRegion} looks like
339     * @param previewCropRegion (out parameter) what the visual preview crop is
340     * @return
341     *          the zoom index inclusively between 0 and {@code Parameters#getMaxZoom},
342     *          where 0 means the camera is not zoomed
343     *
344     * @throws NullPointerException if any of the args were {@code null}
345     */
346    public static int getClosestAvailableZoomCrop(
347            Camera.Parameters params, Rect activeArray, Size streamSize, Rect cropRegion,
348            /*out*/
349            Rect reportedCropRegion,
350            Rect previewCropRegion) {
351        checkNotNull(params, "params must not be null");
352        checkNotNull(activeArray, "activeArray must not be null");
353        checkNotNull(streamSize, "streamSize must not be null");
354        checkNotNull(reportedCropRegion, "reportedCropRegion must not be null");
355        checkNotNull(previewCropRegion, "previewCropRegion must not be null");
356
357        Rect actualCrop = new Rect(cropRegion);
358
359        /*
360         * Shrink requested crop region to fit inside of the active array size
361         */
362        if (!actualCrop.intersect(activeArray)) {
363            Log.w(TAG, "getClosestAvailableZoomCrop - Crop region out of range; " +
364                    "setting to active array size");
365            actualCrop.set(activeArray);
366        }
367
368        Rect previewCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize);
369
370        // Make the user-requested crop region the same aspect ratio as the preview stream size
371        Rect cropRegionAsPreview =
372                shrinkToSameAspectRatioCentered(previewCrop, actualCrop);
373
374        if (VERBOSE) {
375            Log.v(TAG, "getClosestAvailableZoomCrop - actualCrop = " + actualCrop);
376            Log.v(TAG,
377                    "getClosestAvailableZoomCrop - previewCrop = " + previewCrop);
378            Log.v(TAG,
379                    "getClosestAvailableZoomCrop - cropRegionAsPreview = " + cropRegionAsPreview);
380        }
381
382        /*
383         * Iterate all available zoom rectangles and find the closest zoom index
384         */
385        Rect bestReportedCropRegion = null;
386        Rect bestPreviewCropRegion = null;
387        int bestZoomIndex = -1;
388
389        List<Rect> availableReportedCropRegions =
390                getAvailableZoomCropRectangles(params, activeArray);
391        List<Rect> availablePreviewCropRegions =
392                getAvailablePreviewZoomCropRectangles(params, activeArray, streamSize);
393
394        if (VERBOSE) {
395            Log.v(TAG,
396                    "getClosestAvailableZoomCrop - availableReportedCropRegions = " +
397                            ListUtils.listToString(availableReportedCropRegions));
398            Log.v(TAG,
399                    "getClosestAvailableZoomCrop - availablePreviewCropRegions = " +
400                            ListUtils.listToString(availablePreviewCropRegions));
401        }
402
403        if (availableReportedCropRegions.size() != availablePreviewCropRegions.size()) {
404            throw new AssertionError("available reported/preview crop region size mismatch");
405        }
406
407        for (int i = 0; i < availableReportedCropRegions.size(); ++i) {
408            Rect currentPreviewCropRegion = availablePreviewCropRegions.get(i);
409            Rect currentReportedCropRegion = availableReportedCropRegions.get(i);
410
411            boolean isBest;
412            if (bestZoomIndex == -1) {
413                isBest = true;
414            } else if (currentPreviewCropRegion.width() >= cropRegionAsPreview.width() &&
415                    currentPreviewCropRegion.height() >= cropRegionAsPreview.height()) {
416                isBest = true;
417            } else {
418                isBest = false;
419            }
420
421            // Sizes are sorted largest-to-smallest, so once the available crop is too small,
422            // we the rest are too small. Furthermore, this is the final best crop,
423            // since its the largest crop that still fits the requested crop
424            if (isBest) {
425                bestPreviewCropRegion = currentPreviewCropRegion;
426                bestReportedCropRegion = currentReportedCropRegion;
427                bestZoomIndex = i;
428            } else {
429                break;
430            }
431        }
432
433        if (bestZoomIndex == -1) {
434            // Even in the worst case, we should always at least return 0 here
435            throw new AssertionError("Should've found at least one valid zoom index");
436        }
437
438        // Write the rectangles in-place
439        reportedCropRegion.set(bestReportedCropRegion);
440        previewCropRegion.set(bestPreviewCropRegion);
441
442        return bestZoomIndex;
443    }
444
445    /**
446     * Calculate the effective crop rectangle for this preview viewport;
447     * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions
448     * without skewing.
449     *
450     * <p>The preview size must be a subset of the active array size; the resulting
451     * rectangle will also be a subset of the active array rectangle.</p>
452     *
453     * <p>The unzoomed crop rectangle is calculated only.</p>
454     *
455     * @param activeArray active array dimensions, in sensor space
456     * @param previewSize size of the preview buffer render target, in pixels (not in sensor space)
457     * @return a rectangle which serves as the preview stream's effective crop region (unzoomed),
458     *         in sensor space
459     *
460     * @throws NullPointerException
461     *          if any of the args were {@code null}
462     * @throws IllegalArgumentException
463     *          if {@code previewSize} is wider or taller than {@code activeArray}
464     */
465    private static Rect getPreviewCropRectangleUnzoomed(Rect activeArray, Size previewSize) {
466        if (previewSize.getWidth() > activeArray.width()) {
467            throw new IllegalArgumentException("previewSize must not be wider than activeArray");
468        } else if (previewSize.getHeight() > activeArray.height()) {
469            throw new IllegalArgumentException("previewSize must not be taller than activeArray");
470        }
471
472        float aspectRatioArray = activeArray.width() * 1.0f / activeArray.height();
473        float aspectRatioPreview = previewSize.getWidth() * 1.0f / previewSize.getHeight();
474
475        float cropH, cropW;
476        if (aspectRatioPreview < aspectRatioArray) {
477            // The new width must be smaller than the height, so scale the width by AR
478            cropH = activeArray.height();
479            cropW = cropH * aspectRatioPreview;
480        } else {
481            // The new height must be smaller (or equal) than the width, so scale the height by AR
482            cropW = activeArray.width();
483            cropH = cropW / aspectRatioPreview;
484        }
485
486        Matrix translateMatrix = new Matrix();
487        RectF cropRect = new RectF(/*left*/0, /*top*/0, cropW, cropH);
488
489        // Now center the crop rectangle so its center is in the center of the active array
490        translateMatrix.setTranslate(activeArray.exactCenterX(), activeArray.exactCenterY());
491        translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY());
492
493        translateMatrix.mapRect(/*inout*/cropRect);
494
495        // Round the rect corners towards the nearest integer values
496        return ParamsUtils.createRect(cropRect);
497    }
498
499    /**
500     * Shrink the {@code shrinkTarget} rectangle to snugly fit inside of {@code reference};
501     * the aspect ratio of {@code shrinkTarget} will change to be the same aspect ratio as
502     * {@code reference}.
503     *
504     * <p>At most a single dimension will scale (down). Both dimensions will never be scaled.</p>
505     *
506     * @param reference the rectangle whose aspect ratio will be used as the new aspect ratio
507     * @param shrinkTarget the rectangle which will be scaled down to have a new aspect ratio
508     *
509     * @return a new rectangle, a subset of {@code shrinkTarget},
510     *          whose aspect ratio will match that of {@code reference}
511     */
512    private static Rect shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget) {
513        float aspectRatioReference = reference.width() * 1.0f / reference.height();
514        float aspectRatioShrinkTarget = shrinkTarget.width() * 1.0f / shrinkTarget.height();
515
516        float cropH, cropW;
517        if (aspectRatioShrinkTarget < aspectRatioReference) {
518            // The new width must be smaller than the height, so scale the width by AR
519            cropH = reference.height();
520            cropW = cropH * aspectRatioShrinkTarget;
521        } else {
522            // The new height must be smaller (or equal) than the width, so scale the height by AR
523            cropW = reference.width();
524            cropH = cropW / aspectRatioShrinkTarget;
525        }
526
527        Matrix translateMatrix = new Matrix();
528        RectF shrunkRect = new RectF(shrinkTarget);
529
530        // Scale the rectangle down, but keep its center in the same place as before
531        translateMatrix.setScale(cropW / reference.width(), cropH / reference.height(),
532                shrinkTarget.exactCenterX(), shrinkTarget.exactCenterY());
533
534        translateMatrix.mapRect(/*inout*/shrunkRect);
535
536        return ParamsUtils.createRect(shrunkRect);
537    }
538
539    /**
540     * Get the available 'crop' (zoom) rectangles for this camera that will be reported
541     * via a {@code CaptureResult} when a zoom is requested.
542     *
543     * <p>These crops ignores the underlying preview buffer size, and will always be reported
544     * the same values regardless of what configuration of outputs is used.</p>
545     *
546     * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
547     * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
548     *
549     * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
550     * by shrinking the rectangle if necessary.</p>
551     *
552     * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
553     * = {@code activeArray size}.</p>
554     *
555     * @param params non-{@code null} camera api1 parameters
556     * @param activeArray active array dimensions, in sensor space
557     * @param streamSize stream size dimensions, in pixels
558     *
559     * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
560     */
561    public static List<Rect> getAvailableZoomCropRectangles(
562            Camera.Parameters params, Rect activeArray) {
563        checkNotNull(params, "params must not be null");
564        checkNotNull(activeArray, "activeArray must not be null");
565
566        return getAvailableCropRectangles(params, activeArray, ParamsUtils.createSize(activeArray));
567    }
568
569    /**
570     * Get the available 'crop' (zoom) rectangles for this camera.
571     *
572     * <p>This is the effective (real) crop that is applied by the camera api1 device
573     * when projecting the zoom onto the intermediate preview buffer. Use this when
574     * deciding which zoom ratio to apply.</p>
575     *
576     * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
577     * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
578     *
579     * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
580     * by shrinking the rectangle if necessary.</p>
581     *
582     * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
583     * = {@code activeArray size}.</p>
584     *
585     * @param params non-{@code null} camera api1 parameters
586     * @param activeArray active array dimensions, in sensor space
587     * @param streamSize stream size dimensions, in pixels
588     *
589     * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
590     */
591    public static List<Rect> getAvailablePreviewZoomCropRectangles(Camera.Parameters params,
592            Rect activeArray, Size previewSize) {
593        checkNotNull(params, "params must not be null");
594        checkNotNull(activeArray, "activeArray must not be null");
595        checkNotNull(previewSize, "previewSize must not be null");
596
597        return getAvailableCropRectangles(params, activeArray, previewSize);
598    }
599
600    /**
601     * Get the available 'crop' (zoom) rectangles for this camera.
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    private static List<Rect> getAvailableCropRectangles(Camera.Parameters params,
619            Rect activeArray, Size streamSize) {
620        checkNotNull(params, "params must not be null");
621        checkNotNull(activeArray, "activeArray must not be null");
622        checkNotNull(streamSize, "streamSize must not be null");
623
624        // TODO: change all uses of Rect activeArray to Size activeArray,
625        // since we want the crop to be active-array relative, not pixel-array relative
626
627        Rect unzoomedStreamCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize);
628
629        if (!params.isZoomSupported()) {
630            // Trivial case: No zoom -> only support the full size as the crop region
631            return new ArrayList<>(Arrays.asList(unzoomedStreamCrop));
632        }
633
634        List<Rect> zoomCropRectangles = new ArrayList<>(params.getMaxZoom() + 1);
635        Matrix scaleMatrix = new Matrix();
636        RectF scaledRect = new RectF();
637
638        for (int zoom : params.getZoomRatios()) {
639            float shrinkRatio = ZOOM_RATIO_MULTIPLIER * 1.0f / zoom; // normalize to 1.0 and smaller
640
641            // set scaledRect to unzoomedStreamCrop
642            ParamsUtils.convertRectF(unzoomedStreamCrop, /*out*/scaledRect);
643
644            scaleMatrix.setScale(
645                    shrinkRatio, shrinkRatio,
646                    activeArray.exactCenterX(),
647                    activeArray.exactCenterY());
648
649            scaleMatrix.mapRect(scaledRect);
650
651            Rect intRect = ParamsUtils.createRect(scaledRect);
652
653            // Round the rect corners towards the nearest integer values
654            zoomCropRectangles.add(intRect);
655        }
656
657        return zoomCropRectangles;
658    }
659
660    /**
661     * Get the largest possible zoom ratio (normalized to {@code 1.0f} and higher)
662     * that the camera can support.
663     *
664     * <p>If the camera does not support zoom, it always returns {@code 1.0f}.</p>
665     *
666     * @param params non-{@code null} camera api1 parameters
667     * @return normalized max zoom ratio, at least {@code 1.0f}
668     */
669    public static float getMaxZoomRatio(Camera.Parameters params) {
670        if (!params.isZoomSupported()) {
671            return 1.0f; // no zoom
672        }
673
674        List<Integer> zoomRatios = params.getZoomRatios(); // sorted smallest->largest
675        int zoom = zoomRatios.get(zoomRatios.size() - 1); // largest zoom ratio
676        float zoomRatio = zoom * 1.0f / ZOOM_RATIO_MULTIPLIER; // normalize to 1.0 and smaller
677
678        return zoomRatio;
679    }
680
681    /**
682     * Returns the component-wise zoom ratio (each greater or equal than {@code 1.0});
683     * largest values means more zoom.
684     *
685     * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
686     * @param cropSize size of the crop/zoom
687     *
688     * @return {@link SizeF} with width/height being the component-wise zoom ratio
689     *
690     * @throws NullPointerException if any of the args were {@code null}
691     * @throws IllegalArgumentException if any component of {@code cropSize} was {@code 0}
692     */
693    private static SizeF getZoomRatio(Size activeArraySize, Size cropSize) {
694        checkNotNull(activeArraySize, "activeArraySize must not be null");
695        checkNotNull(cropSize, "cropSize must not be null");
696        checkArgumentPositive(cropSize.getWidth(), "cropSize.width must be positive");
697        checkArgumentPositive(cropSize.getHeight(), "cropSize.height must be positive");
698
699        float zoomRatioWidth = activeArraySize.getWidth() * 1.0f / cropSize.getWidth();
700        float zoomRatioHeight = activeArraySize.getHeight() * 1.0f / cropSize.getHeight();
701
702        return new SizeF(zoomRatioWidth, zoomRatioHeight);
703    }
704
705    /**
706     * Convert the user-specified crop region into zoom data; which can be used
707     * to set the parameters to a specific zoom index, or to report back to the user what the
708     * actual zoom was, or for other calculations requiring the current preview crop region.
709     *
710     * <p>None of the parameters are mutated.</p>
711     *
712     * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
713     * @param cropRegion the user-specified crop region
714     * @param previewSize the current preview size (in pixels)
715     * @param params the current camera parameters (not mutated)
716     *
717     * @return the zoom index, and the effective/reported crop regions (relative to active array)
718     */
719    public static ZoomData convertScalerCropRegion(Rect activeArraySize, Rect
720            cropRegion, Size previewSize, Camera.Parameters params) {
721        Rect activeArraySizeOnly = new Rect(
722                /*left*/0, /*top*/0,
723                activeArraySize.width(), activeArraySize.height());
724
725        Rect userCropRegion = cropRegion;
726
727        if (userCropRegion == null) {
728            userCropRegion = activeArraySizeOnly;
729        }
730
731        if (VERBOSE) {
732            Log.v(TAG, "convertScalerCropRegion - user crop region was " + userCropRegion);
733        }
734
735        final Rect reportedCropRegion = new Rect();
736        final Rect previewCropRegion = new Rect();
737        final int zoomIdx = ParameterUtils.getClosestAvailableZoomCrop(params, activeArraySizeOnly,
738                previewSize, userCropRegion,
739                /*out*/reportedCropRegion, /*out*/previewCropRegion);
740
741        if (VERBOSE) {
742            Log.v(TAG, "convertScalerCropRegion - zoom calculated to: " +
743                    "zoomIndex = " + zoomIdx +
744                    ", reported crop region = " + reportedCropRegion +
745                    ", preview crop region = " + previewCropRegion);
746        }
747
748        return new ZoomData(zoomIdx, previewCropRegion, reportedCropRegion);
749    }
750
751    /**
752     * Calculate the actual/effective/reported normalized rectangle data from a metering
753     * rectangle.
754     *
755     * <p>If any of the rectangles are out-of-range of their intended bounding box,
756     * the {@link #RECTANGLE_EMPTY empty rectangle} is substituted instead
757     * (with a weight of {@code 0}).</p>
758     *
759     * <p>The metering rectangle is bound by the crop region (effective/reported respectively).
760     * The metering {@link Camera.Area area} is bound by {@code [-1000, 1000]}.</p>
761     *
762     * <p>No parameters are mutated; returns the new metering data.</p>
763     *
764     * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
765     * @param meteringRect the user-specified metering rectangle
766     * @param zoomData the calculated zoom data corresponding to this request
767     *
768     * @return the metering area, the reported/effective metering rectangles
769     */
770    public static MeteringData convertMeteringRectangleToLegacy(
771            Rect activeArray, MeteringRectangle meteringRect, ZoomData zoomData) {
772        Rect previewCrop = zoomData.previewCrop;
773
774        float scaleW = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f /
775                previewCrop.width();
776        float scaleH = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f /
777                previewCrop.height();
778
779        Matrix transform = new Matrix();
780        // Move the preview crop so that top,left is at (0,0), otherwise after scaling
781        // the corner bounds will be outside of [-1000, 1000]
782        transform.setTranslate(-previewCrop.left, -previewCrop.top);
783        // Scale into [0, 2000] range about the center of the preview
784        transform.postScale(scaleW, scaleH);
785        // Move so that top left of a typical rect is at [-1000, -1000]
786        transform.postTranslate(/*dx*/NORMALIZED_RECTANGLE_MIN, /*dy*/NORMALIZED_RECTANGLE_MIN);
787
788        /*
789         * Calculate the preview metering region (effective), and the camera1 api
790         * normalized metering region.
791         */
792        Rect normalizedRegionUnbounded = ParamsUtils.mapRect(transform, meteringRect.getRect());
793
794        /*
795         * Try to intersect normalized area with [-1000, 1000] rectangle; otherwise
796         * it's completely out of range
797         */
798        Rect normalizedIntersected = new Rect(normalizedRegionUnbounded);
799
800        Camera.Area meteringArea;
801        if (!normalizedIntersected.intersect(NORMALIZED_RECTANGLE_DEFAULT)) {
802            Log.w(TAG,
803                    "convertMeteringRectangleToLegacy - metering rectangle too small, " +
804                    "no metering will be done");
805            normalizedIntersected.set(RECTANGLE_EMPTY);
806            meteringArea = new Camera.Area(RECTANGLE_EMPTY,
807                    MeteringRectangle.METERING_WEIGHT_DONT_CARE);
808        } else {
809            meteringArea = new Camera.Area(normalizedIntersected,
810                    meteringRect.getMeteringWeight());
811        }
812
813        /*
814         * Calculate effective preview metering region
815         */
816        Rect previewMetering = meteringRect.getRect();
817        if (!previewMetering.intersect(previewCrop)) {
818            previewMetering.set(RECTANGLE_EMPTY);
819        }
820
821        /*
822         * Calculate effective reported metering region
823         * - Transform the calculated metering area back into active array space
824         * - Clip it to be a subset of the reported crop region
825         */
826        Rect reportedMetering;
827        {
828            Camera.Area normalizedAreaUnbounded = new Camera.Area(
829                    normalizedRegionUnbounded, meteringRect.getMeteringWeight());
830            WeightedRectangle reportedMeteringRect = convertCameraAreaToActiveArrayRectangle(
831                    activeArray, zoomData, normalizedAreaUnbounded, /*usePreviewCrop*/false);
832            reportedMetering = reportedMeteringRect.rect;
833        }
834
835        if (VERBOSE) {
836            Log.v(TAG, String.format(
837                    "convertMeteringRectangleToLegacy - activeArray = %s, meteringRect = %s, " +
838                    "previewCrop = %s, meteringArea = %s, previewMetering = %s, " +
839                    "reportedMetering = %s, normalizedRegionUnbounded = %s",
840                    activeArray, meteringRect,
841                    previewCrop, stringFromArea(meteringArea), previewMetering,
842                    reportedMetering, normalizedRegionUnbounded));
843        }
844
845        return new MeteringData(meteringArea, previewMetering, reportedMetering);
846    }
847
848    /**
849     * Convert the normalized camera area from [-1000, 1000] coordinate space
850     * into the active array-based coordinate space.
851     *
852     * <p>Values out of range are clipped to be within the resulting (reported) crop
853     * region. It is possible to have values larger than the preview crop.</p>
854     *
855     * <p>Weights out of range of [0, 1000] are clipped to be within the range.</p>
856     *
857     * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
858     * @param zoomData the calculated zoom data corresponding to this request
859     * @param area the normalized camera area
860     *
861     * @return the weighed rectangle in active array coordinate space, with the weight
862     */
863    public static WeightedRectangle convertCameraAreaToActiveArrayRectangle(
864            Rect activeArray, ZoomData zoomData, Camera.Area area) {
865        return convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, area,
866                /*usePreviewCrop*/true);
867    }
868
869    /**
870     * Convert an api1 face into an active-array based api2 face.
871     *
872     * <p>Out-of-ranges scores and ids will be clipped to be within range (with a warning).</p>
873     *
874     * @param face a non-{@code null} api1 face
875     * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
876     * @param zoomData the calculated zoom data corresponding to this request
877     *
878     * @return a non-{@code null} api2 face
879     *
880     * @throws NullPointerException if the {@code face} was {@code null}
881     */
882    public static Face convertFaceFromLegacy(Camera.Face face, Rect activeArray,
883            ZoomData zoomData) {
884        checkNotNull(face, "face must not be null");
885
886        Face api2Face;
887
888        Camera.Area fakeArea = new Camera.Area(face.rect, /*weight*/1);
889
890        WeightedRectangle faceRect =
891                convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, fakeArea);
892
893        Point leftEye = face.leftEye, rightEye = face.rightEye, mouth = face.mouth;
894        if (leftEye != null && rightEye != null && mouth != null) {
895            leftEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
896                    leftEye, /*usePreviewCrop*/true);
897            rightEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
898                    leftEye, /*usePreviewCrop*/true);
899            mouth = convertCameraPointToActiveArrayPoint(activeArray, zoomData,
900                    leftEye, /*usePreviewCrop*/true);
901
902            api2Face = faceRect.toFace(face.id, leftEye, rightEye, mouth);
903        } else {
904            api2Face = faceRect.toFace();
905        }
906
907        return api2Face;
908    }
909
910    private static Point convertCameraPointToActiveArrayPoint(
911            Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop) {
912        Rect pointedRect = new Rect(point.x, point.y, point.x, point.y);
913        Camera.Area pointedArea = new Area(pointedRect, /*weight*/1);
914
915        WeightedRectangle adjustedRect =
916                convertCameraAreaToActiveArrayRectangle(activeArray,
917                        zoomData, pointedArea, usePreviewCrop);
918
919        Point transformedPoint = new Point(adjustedRect.rect.left, adjustedRect.rect.top);
920
921        return transformedPoint;
922    }
923
924    private static WeightedRectangle convertCameraAreaToActiveArrayRectangle(
925            Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop) {
926        Rect previewCrop = zoomData.previewCrop;
927        Rect reportedCrop = zoomData.reportedCrop;
928
929        float scaleW = previewCrop.width() * 1.0f /
930                (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN);
931        float scaleH = previewCrop.height() * 1.0f /
932                (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN);
933
934        /*
935         * Calculate the reported metering region from the non-intersected normalized region
936         * by scaling and translating back into active array-relative coordinates.
937         */
938        Matrix transform = new Matrix();
939
940        // Move top left from (-1000, -1000) to (0, 0)
941        transform.setTranslate(/*dx*/NORMALIZED_RECTANGLE_MAX, /*dy*/NORMALIZED_RECTANGLE_MAX);
942
943        // Scale from [0, 2000] back into the preview rectangle
944        transform.postScale(scaleW, scaleH);
945
946        // Move the rect so that the [-1000,-1000] point ends up at the preview [left, top]
947        transform.postTranslate(previewCrop.left, previewCrop.top);
948
949        Rect cropToIntersectAgainst = usePreviewCrop ? previewCrop : reportedCrop;
950
951        // Now apply the transformation backwards to get the reported metering region
952        Rect reportedMetering = ParamsUtils.mapRect(transform, area.rect);
953        // Intersect it with the crop region, to avoid reporting out-of-bounds
954        // metering regions
955        if (!reportedMetering.intersect(cropToIntersectAgainst)) {
956            reportedMetering.set(RECTANGLE_EMPTY);
957        }
958
959        int weight = area.weight;
960        if (weight < MeteringRectangle.METERING_WEIGHT_MIN) {
961            Log.w(TAG,
962                    "convertCameraAreaToMeteringRectangle - rectangle "
963                            + stringFromArea(area) + " has too small weight, clip to 0");
964            weight = 0;
965        }
966
967        return new WeightedRectangle(reportedMetering, area.weight);
968    }
969
970
971    private ParameterUtils() {
972        throw new AssertionError();
973    }
974}
975