ParameterUtils.java revision df6242e374b81e802a38cb891477f05d3e4b3cbc
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.Rect;
21import android.graphics.RectF;
22import android.hardware.Camera;
23import android.hardware.camera2.utils.ListUtils;
24import android.hardware.camera2.utils.ParamsUtils;
25import android.hardware.camera2.utils.SizeAreaComparator;
26import android.util.Size;
27import android.util.SizeF;
28
29import android.util.Log;
30
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.List;
34
35import static com.android.internal.util.Preconditions.*;
36
37/**
38 * Various utilities for dealing with camera API1 parameters.
39 */
40public class ParameterUtils {
41    private static final String TAG = "ParameterUtils";
42    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
43
44    /** getZoomRatios stores zoom ratios in 1/100 increments, e.x. a zoom of 3.2 is 320 */
45    private static final int ZOOM_RATIO_MULTIPLIER = 100;
46
47    /**
48     * Convert a camera API1 size into a util size
49     */
50    public static Size convertSize(Camera.Size size) {
51        checkNotNull(size, "size must not be null");
52
53        return new Size(size.width, size.height);
54    }
55
56    /**
57     * Convert a camera API1 list of sizes into a util list of sizes
58     */
59    public static List<Size> convertSizeList(List<Camera.Size> sizeList) {
60        checkNotNull(sizeList, "sizeList must not be null");
61
62        List<Size> sizes = new ArrayList<>(sizeList.size());
63        for (Camera.Size s : sizeList) {
64            sizes.add(new Size(s.width, s.height));
65        }
66        return sizes;
67    }
68
69    /**
70     * Returns the largest supported picture size, as compared by its area.
71     */
72    public static Size getLargestSupportedJpegSizeByArea(Camera.Parameters params) {
73        checkNotNull(params, "params must not be null");
74
75        List<Size> supportedJpegSizes = convertSizeList(params.getSupportedPictureSizes());
76        return SizeAreaComparator.findLargestByArea(supportedJpegSizes);
77    }
78
79    /**
80     * Convert a camera area into a human-readable string.
81     */
82    public static String stringFromArea(Camera.Area area) {
83        if (area == null) {
84            return null;
85        } else {
86            StringBuilder sb = new StringBuilder();
87            Rect r = area.rect;
88
89            sb.setLength(0);
90            sb.append("(["); sb.append(r.left); sb.append(',');
91            sb.append(r.top); sb.append("]["); sb.append(r.right);
92            sb.append(','); sb.append(r.bottom); sb.append(']');
93
94            sb.append(',');
95            sb.append(area.weight);
96            sb.append(')');
97
98            return sb.toString();
99        }
100    }
101
102    /**
103     * Calculate the closest zoom index for the user-requested crop region by rounding
104     * up to the closest (largest or equal) possible zoom crop.
105     *
106     * <p>If the requested crop region exceeds the size of the active array, it is
107     * shrunk to fit inside of the active array first.</p>
108     *
109     * <p>Since all api1 camera devices only support a discrete set of zooms, we have
110     * to translate the per-pixel-granularity requested crop region into a per-zoom-index
111     * granularity.</p>
112     *
113     * <p>Furthermore, since the zoom index and zoom levels also depends on the field-of-view
114     * of the preview, the current preview {@code streamSize} is also used.</p>
115     *
116     * <p>The calculated crop regions are then written to in-place to {@code reportedCropRegion}
117     * and {@code previewCropRegion}, in coordinates relative to the active array.</p>
118     *
119     * @param params non-{@code null} camera api1 parameters
120     * @param activeArray active array dimensions, in sensor space
121     * @param streamSize stream size dimensions, in pixels
122     * @param cropRegion user-specified crop region, in active array coordinates
123     * @param reportedCropRegion (out parameter) what the result for {@code cropRegion} looks like
124     * @param previewCropRegion (out parameter) what the visual preview crop is
125     * @return
126     *          the zoom index inclusively between 0 and {@code Parameters#getMaxZoom},
127     *          where 0 means the camera is not zoomed
128     *
129     * @throws NullPointerException if any of the args were {@code null}
130     */
131    public static int getClosestAvailableZoomCrop(
132            Camera.Parameters params, Rect activeArray, Size streamSize, Rect cropRegion,
133            /*out*/
134            Rect reportedCropRegion,
135            Rect previewCropRegion) {
136        checkNotNull(params, "params must not be null");
137        checkNotNull(activeArray, "activeArray must not be null");
138        checkNotNull(streamSize, "streamSize must not be null");
139        checkNotNull(reportedCropRegion, "reportedCropRegion must not be null");
140        checkNotNull(previewCropRegion, "previewCropRegion must not be null");
141
142        Rect actualCrop = new Rect(cropRegion);
143
144        /*
145         * Shrink requested crop region to fit inside of the active array size
146         */
147        if (!actualCrop.intersect(activeArray)) {
148            Log.w(TAG, "getClosestAvailableZoomCrop - Crop region out of range; " +
149                    "setting to active array size");
150            actualCrop.set(activeArray);
151        }
152
153        Rect previewCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize);
154
155        // Make the user-requested crop region the same aspect ratio as the preview stream size
156        Rect cropRegionAsPreview =
157                shrinkToSameAspectRatioCentered(previewCrop, actualCrop);
158
159        if (VERBOSE) {
160            Log.v(TAG, "getClosestAvailableZoomCrop - actualCrop = " + actualCrop);
161            Log.v(TAG,
162                    "getClosestAvailableZoomCrop - previewCrop = " + previewCrop);
163            Log.v(TAG,
164                    "getClosestAvailableZoomCrop - cropRegionAsPreview = " + cropRegionAsPreview);
165        }
166
167        /*
168         * Iterate all available zoom rectangles and find the closest zoom index
169         */
170        Rect bestReportedCropRegion = null;
171        Rect bestPreviewCropRegion = null;
172        int bestZoomIndex = -1;
173
174        List<Rect> availableReportedCropRegions =
175                getAvailableZoomCropRectangles(params, activeArray);
176        List<Rect> availablePreviewCropRegions =
177                getAvailablePreviewZoomCropRectangles(params, activeArray, streamSize);
178
179        if (VERBOSE) {
180            Log.v(TAG,
181                    "getClosestAvailableZoomCrop - availableReportedCropRegions = " +
182                            ListUtils.listToString(availableReportedCropRegions));
183            Log.v(TAG,
184                    "getClosestAvailableZoomCrop - availablePreviewCropRegions = " +
185                            ListUtils.listToString(availablePreviewCropRegions));
186        }
187
188        if (availableReportedCropRegions.size() != availablePreviewCropRegions.size()) {
189            throw new AssertionError("available reported/preview crop region size mismatch");
190        }
191
192        for (int i = 0; i < availableReportedCropRegions.size(); ++i) {
193            Rect currentPreviewCropRegion = availablePreviewCropRegions.get(i);
194            Rect currentReportedCropRegion = availableReportedCropRegions.get(i);
195
196            boolean isBest;
197            if (bestZoomIndex == -1) {
198                isBest = true;
199            } else if (currentPreviewCropRegion.width() >= cropRegionAsPreview.width() &&
200                    currentPreviewCropRegion.height() >= cropRegionAsPreview.height()) {
201                isBest = true;
202            } else {
203                isBest = false;
204            }
205
206            // Sizes are sorted largest-to-smallest, so once the available crop is too small,
207            // we the rest are too small. Furthermore, this is the final best crop,
208            // since its the largest crop that still fits the requested crop
209            if (isBest) {
210                bestPreviewCropRegion = currentPreviewCropRegion;
211                bestReportedCropRegion = currentReportedCropRegion;
212                bestZoomIndex = i;
213            } else {
214                break;
215            }
216        }
217
218        if (bestZoomIndex == -1) {
219            // Even in the worst case, we should always at least return 0 here
220            throw new AssertionError("Should've found at least one valid zoom index");
221        }
222
223        // Write the rectangles in-place
224        reportedCropRegion.set(bestReportedCropRegion);
225        previewCropRegion.set(bestPreviewCropRegion);
226
227        return bestZoomIndex;
228    }
229
230    /**
231     * Calculate the effective crop rectangle for this preview viewport;
232     * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions
233     * without skewing.
234     *
235     * <p>The preview size must be a subset of the active array size; the resulting
236     * rectangle will also be a subset of the active array rectangle.</p>
237     *
238     * <p>The unzoomed crop rectangle is calculated only.</p>
239     *
240     * @param activeArray active array dimensions, in sensor space
241     * @param previewSize size of the preview buffer render target, in pixels (not in sensor space)
242     * @return a rectangle which serves as the preview stream's effective crop region (unzoomed),
243     *         in sensor space
244     *
245     * @throws NullPointerException
246     *          if any of the args were {@code null}
247     * @throws IllegalArgumentException
248     *          if {@code previewSize} is wider or taller than {@code activeArray}
249     */
250    private static Rect getPreviewCropRectangleUnzoomed(Rect activeArray, Size previewSize) {
251        if (previewSize.getWidth() > activeArray.width()) {
252            throw new IllegalArgumentException("previewSize must not be wider than activeArray");
253        } else if (previewSize.getHeight() > activeArray.height()) {
254            throw new IllegalArgumentException("previewSize must not be taller than activeArray");
255        }
256
257        float aspectRatioArray = activeArray.width() * 1.0f / activeArray.height();
258        float aspectRatioPreview = previewSize.getWidth() * 1.0f / previewSize.getHeight();
259
260        float cropH, cropW;
261        if (aspectRatioPreview < aspectRatioArray) {
262            // The new width must be smaller than the height, so scale the width by AR
263            cropH = activeArray.height();
264            cropW = cropH * aspectRatioPreview;
265        } else {
266            // The new height must be smaller (or equal) than the width, so scale the height by AR
267            cropW = activeArray.width();
268            cropH = cropW / aspectRatioPreview;
269        }
270
271        Matrix translateMatrix = new Matrix();
272        RectF cropRect = new RectF(/*left*/0, /*top*/0, cropW, cropH);
273
274        // Now center the crop rectangle so its center is in the center of the active array
275        translateMatrix.setTranslate(activeArray.exactCenterX(), activeArray.exactCenterY());
276        translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY());
277
278        translateMatrix.mapRect(/*inout*/cropRect);
279
280        // Round the rect corners towards the nearest integer values
281        return ParamsUtils.createRect(cropRect);
282    }
283
284    /**
285     * Shrink the {@code shrinkTarget} rectangle to snugly fit inside of {@code reference};
286     * the aspect ratio of {@code shrinkTarget} will change to be the same aspect ratio as
287     * {@code reference}.
288     *
289     * <p>At most a single dimension will scale (down). Both dimensions will never be scaled.</p>
290     *
291     * @param reference the rectangle whose aspect ratio will be used as the new aspect ratio
292     * @param shrinkTarget the rectangle which will be scaled down to have a new aspect ratio
293     *
294     * @return a new rectangle, a subset of {@code shrinkTarget},
295     *          whose aspect ratio will match that of {@code reference}
296     */
297    private static Rect shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget) {
298        float aspectRatioReference = reference.width() * 1.0f / reference.height();
299        float aspectRatioShrinkTarget = shrinkTarget.width() * 1.0f / shrinkTarget.height();
300
301        float cropH, cropW;
302        if (aspectRatioShrinkTarget < aspectRatioReference) {
303            // The new width must be smaller than the height, so scale the width by AR
304            cropH = reference.height();
305            cropW = cropH * aspectRatioShrinkTarget;
306        } else {
307            // The new height must be smaller (or equal) than the width, so scale the height by AR
308            cropW = reference.width();
309            cropH = cropW / aspectRatioShrinkTarget;
310        }
311
312        Matrix translateMatrix = new Matrix();
313        RectF shrunkRect = new RectF(shrinkTarget);
314
315        // Scale the rectangle down, but keep its center in the same place as before
316        translateMatrix.setScale(cropW / reference.width(), cropH / reference.height(),
317                shrinkTarget.exactCenterX(), shrinkTarget.exactCenterY());
318
319        translateMatrix.mapRect(/*inout*/shrunkRect);
320
321        return ParamsUtils.createRect(shrunkRect);
322    }
323
324    /**
325     * Get the available 'crop' (zoom) rectangles for this camera that will be reported
326     * via a {@code CaptureResult} when a zoom is requested.
327     *
328     * <p>These crops ignores the underlying preview buffer size, and will always be reported
329     * the same values regardless of what configuration of outputs is used.</p>
330     *
331     * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
332     * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
333     *
334     * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
335     * by shrinking the rectangle if necessary.</p>
336     *
337     * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
338     * = {@code activeArray size}.</p>
339     *
340     * @param params non-{@code null} camera api1 parameters
341     * @param activeArray active array dimensions, in sensor space
342     * @param streamSize stream size dimensions, in pixels
343     *
344     * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
345     */
346    public static List<Rect> getAvailableZoomCropRectangles(
347            Camera.Parameters params, Rect activeArray) {
348        checkNotNull(params, "params must not be null");
349        checkNotNull(activeArray, "activeArray must not be null");
350
351        return getAvailableCropRectangles(params, activeArray, ParamsUtils.createSize(activeArray));
352    }
353
354    /**
355     * Get the available 'crop' (zoom) rectangles for this camera.
356     *
357     * <p>This is the effective (real) crop that is applied by the camera api1 device
358     * when projecting the zoom onto the intermediate preview buffer. Use this when
359     * deciding which zoom ratio to apply.</p>
360     *
361     * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
362     * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
363     *
364     * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
365     * by shrinking the rectangle if necessary.</p>
366     *
367     * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
368     * = {@code activeArray size}.</p>
369     *
370     * @param params non-{@code null} camera api1 parameters
371     * @param activeArray active array dimensions, in sensor space
372     * @param streamSize stream size dimensions, in pixels
373     *
374     * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
375     */
376    public static List<Rect> getAvailablePreviewZoomCropRectangles(Camera.Parameters params,
377            Rect activeArray, Size previewSize) {
378        checkNotNull(params, "params must not be null");
379        checkNotNull(activeArray, "activeArray must not be null");
380        checkNotNull(previewSize, "previewSize must not be null");
381
382        return getAvailableCropRectangles(params, activeArray, previewSize);
383    }
384
385    /**
386     * Get the available 'crop' (zoom) rectangles for this camera.
387     *
388     * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size,
389     * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p>
390     *
391     * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize},
392     * by shrinking the rectangle if necessary.</p>
393     *
394     * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize}
395     * = {@code activeArray size}.</p>
396     *
397     * @param params non-{@code null} camera api1 parameters
398     * @param activeArray active array dimensions, in sensor space
399     * @param streamSize stream size dimensions, in pixels
400     *
401     * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed
402     */
403    private static List<Rect> getAvailableCropRectangles(Camera.Parameters params,
404            Rect activeArray, Size streamSize) {
405        checkNotNull(params, "params must not be null");
406        checkNotNull(activeArray, "activeArray must not be null");
407        checkNotNull(streamSize, "streamSize must not be null");
408
409        // TODO: change all uses of Rect activeArray to Size activeArray,
410        // since we want the crop to be active-array relative, not pixel-array relative
411
412        Rect unzoomedStreamCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize);
413
414        if (!params.isZoomSupported()) {
415            // Trivial case: No zoom -> only support the full size as the crop region
416            return new ArrayList<>(Arrays.asList(unzoomedStreamCrop));
417        }
418
419        List<Rect> zoomCropRectangles = new ArrayList<>(params.getMaxZoom() + 1);
420        Matrix scaleMatrix = new Matrix();
421        RectF scaledRect = new RectF();
422
423        for (int zoom : params.getZoomRatios()) {
424            float shrinkRatio = ZOOM_RATIO_MULTIPLIER * 1.0f / zoom; // normalize to 1.0 and smaller
425
426            // set scaledRect to unzoomedStreamCrop
427            ParamsUtils.convertRectF(unzoomedStreamCrop, /*out*/scaledRect);
428
429            scaleMatrix.setScale(
430                    shrinkRatio, shrinkRatio,
431                    activeArray.exactCenterX(),
432                    activeArray.exactCenterY());
433
434            scaleMatrix.mapRect(scaledRect);
435
436            Rect intRect = ParamsUtils.createRect(scaledRect);
437
438            // Round the rect corners towards the nearest integer values
439            zoomCropRectangles.add(intRect);
440        }
441
442        return zoomCropRectangles;
443    }
444
445    /**
446     * Get the largest possible zoom ratio (normalized to {@code 1.0f} and higher)
447     * that the camera can support.
448     *
449     * <p>If the camera does not support zoom, it always returns {@code 1.0f}.</p>
450     *
451     * @param params non-{@code null} camera api1 parameters
452     * @return normalized max zoom ratio, at least {@code 1.0f}
453     */
454    public static float getMaxZoomRatio(Camera.Parameters params) {
455        if (!params.isZoomSupported()) {
456            return 1.0f; // no zoom
457        }
458
459        List<Integer> zoomRatios = params.getZoomRatios(); // sorted smallest->largest
460        int zoom = zoomRatios.get(zoomRatios.size() - 1); // largest zoom ratio
461        float zoomRatio = zoom * 1.0f / ZOOM_RATIO_MULTIPLIER; // normalize to 1.0 and smaller
462
463        return zoomRatio;
464    }
465
466    /**
467     * Returns the component-wise zoom ratio (each greater or equal than {@code 1.0});
468     * largest values means more zoom.
469     *
470     * @param activeArraySize active array size of the sensor (e.g. max jpeg size)
471     * @param cropSize size of the crop/zoom
472     *
473     * @return {@link SizeF} with width/height being the component-wise zoom ratio
474     *
475     * @throws NullPointerException if any of the args were {@code null}
476     * @throws IllegalArgumentException if any component of {@code cropSize} was {@code 0}
477     */
478    private static SizeF getZoomRatio(Size activeArraySize, Size cropSize) {
479        checkNotNull(activeArraySize, "activeArraySize must not be null");
480        checkNotNull(cropSize, "cropSize must not be null");
481        checkArgumentPositive(cropSize.getWidth(), "cropSize.width must be positive");
482        checkArgumentPositive(cropSize.getHeight(), "cropSize.height must be positive");
483
484        float zoomRatioWidth = activeArraySize.getWidth() * 1.0f / cropSize.getWidth();
485        float zoomRatioHeight = activeArraySize.getHeight() * 1.0f / cropSize.getHeight();
486
487        return new SizeF(zoomRatioWidth, zoomRatioHeight);
488    }
489
490    private ParameterUtils() {
491        throw new AssertionError();
492    }
493}
494