| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.hardware.camera2.legacy; |
| |
| import android.graphics.Matrix; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.hardware.Camera; |
| import android.hardware.Camera.Area; |
| import android.hardware.camera2.legacy.ParameterUtils.MeteringData; |
| import android.hardware.camera2.legacy.ParameterUtils.ZoomData; |
| import android.hardware.camera2.params.Face; |
| import android.hardware.camera2.params.MeteringRectangle; |
| import android.hardware.camera2.utils.ListUtils; |
| import android.hardware.camera2.utils.ParamsUtils; |
| import android.hardware.camera2.utils.SizeAreaComparator; |
| import android.util.Size; |
| import android.util.SizeF; |
| |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import static com.android.internal.util.Preconditions.*; |
| |
| /** |
| * Various utilities for dealing with camera API1 parameters. |
| */ |
| @SuppressWarnings("deprecation") |
| public class ParameterUtils { |
| /** Upper/left minimal point of a normalized rectangle */ |
| public static final int NORMALIZED_RECTANGLE_MIN = -1000; |
| /** Lower/right maximal point of a normalized rectangle */ |
| public static final int NORMALIZED_RECTANGLE_MAX = 1000; |
| /** The default normalized rectangle spans the entire size of the preview viewport */ |
| public static final Rect NORMALIZED_RECTANGLE_DEFAULT = new Rect( |
| NORMALIZED_RECTANGLE_MIN, |
| NORMALIZED_RECTANGLE_MIN, |
| NORMALIZED_RECTANGLE_MAX, |
| NORMALIZED_RECTANGLE_MAX); |
| /** The default normalized area uses the default normalized rectangle with a weight=1 */ |
| public static final Camera.Area CAMERA_AREA_DEFAULT = |
| new Camera.Area(new Rect(NORMALIZED_RECTANGLE_DEFAULT), |
| /*weight*/1); |
| /** Empty rectangle {@code 0x0+0,0} */ |
| public static final Rect RECTANGLE_EMPTY = |
| new Rect(/*left*/0, /*top*/0, /*right*/0, /*bottom*/0); |
| |
| /** |
| * Calculate effective/reported zoom data from a user-specified crop region. |
| */ |
| public static class ZoomData { |
| /** Zoom index used by {@link Camera.Parameters#setZoom} */ |
| public final int zoomIndex; |
| /** Effective crop-region given the zoom index, coordinates relative to active-array */ |
| public final Rect previewCrop; |
| /** Reported crop-region given the zoom index, coordinates relative to active-array */ |
| public final Rect reportedCrop; |
| |
| public ZoomData(int zoomIndex, Rect previewCrop, Rect reportedCrop) { |
| this.zoomIndex = zoomIndex; |
| this.previewCrop = previewCrop; |
| this.reportedCrop = reportedCrop; |
| } |
| } |
| |
| /** |
| * Calculate effective/reported metering data from a user-specified metering region. |
| */ |
| public static class MeteringData { |
| /** |
| * The metering area scaled to the range of [-1000, 1000]. |
| * <p>Values outside of this range are clipped to be within the range.</p> |
| */ |
| public final Camera.Area meteringArea; |
| /** |
| * Effective preview metering region, coordinates relative to active-array. |
| * |
| * <p>Clipped to fit inside of the (effective) preview crop region.</p> |
| */ |
| public final Rect previewMetering; |
| /** |
| * Reported metering region, coordinates relative to active-array. |
| * |
| * <p>Clipped to fit inside of the (reported) resulting crop region.</p> |
| */ |
| public final Rect reportedMetering; |
| |
| public MeteringData(Area meteringArea, Rect previewMetering, Rect reportedMetering) { |
| this.meteringArea = meteringArea; |
| this.previewMetering = previewMetering; |
| this.reportedMetering = reportedMetering; |
| } |
| } |
| |
| /** |
| * A weighted rectangle is an arbitrary rectangle (the coordinate system is unknown) with an |
| * arbitrary weight. |
| * |
| * <p>The user of this class must know what the coordinate system ahead of time; it's |
| * then possible to convert to a more concrete type such as a metering rectangle or a face. |
| * </p> |
| * |
| * <p>When converting to a more concrete type, out-of-range values are clipped; this prevents |
| * possible illegal argument exceptions being thrown at runtime.</p> |
| */ |
| public static class WeightedRectangle { |
| /** Arbitrary rectangle (the range is user-defined); never {@code null}. */ |
| public final Rect rect; |
| /** Arbitrary weight (the range is user-defined). */ |
| public final int weight; |
| |
| /** |
| * Create a new weighted-rectangle from a non-{@code null} rectangle; the {@code weight} |
| * can be unbounded. |
| */ |
| public WeightedRectangle(Rect rect, int weight) { |
| this.rect = checkNotNull(rect, "rect must not be null"); |
| this.weight = weight; |
| } |
| |
| /** |
| * Convert to a metering rectangle, clipping any of the values to stay within range. |
| * |
| * <p>If values are clipped, a warning is printed to logcat.</p> |
| * |
| * @return a new metering rectangle |
| */ |
| public MeteringRectangle toMetering() { |
| int weight = clip(this.weight, |
| MeteringRectangle.METERING_WEIGHT_MIN, |
| MeteringRectangle.METERING_WEIGHT_MAX, |
| rect, |
| "weight"); |
| |
| int x = clipLower(rect.left, /*lo*/0, rect, "left"); |
| int y = clipLower(rect.top, /*lo*/0, rect, "top"); |
| int w = clipLower(rect.width(), /*lo*/0, rect, "width"); |
| int h = clipLower(rect.height(), /*lo*/0, rect, "height"); |
| |
| return new MeteringRectangle(x, y, w, h, weight); |
| } |
| |
| /** |
| * Convert to a face; the rect is considered to be the bounds, and the weight |
| * is considered to be the score. |
| * |
| * <p>If the score is out of range of {@value Face#SCORE_MIN}, {@value Face#SCORE_MAX}, |
| * the score is clipped first and a warning is printed to logcat.</p> |
| * |
| * <p>If the id is negative, the id is changed to 0 and a warning is printed to |
| * logcat.</p> |
| * |
| * <p>All other parameters are passed-through as-is.</p> |
| * |
| * @return a new face with the optional features set |
| */ |
| public Face toFace( |
| int id, Point leftEyePosition, Point rightEyePosition, Point mouthPosition) { |
| int idSafe = clipLower(id, /*lo*/0, rect, "id"); |
| int score = clip(weight, |
| Face.SCORE_MIN, |
| Face.SCORE_MAX, |
| rect, |
| "score"); |
| |
| return new Face(rect, score, idSafe, leftEyePosition, rightEyePosition, mouthPosition); |
| } |
| |
| /** |
| * Convert to a face; the rect is considered to be the bounds, and the weight |
| * is considered to be the score. |
| * |
| * <p>If the score is out of range of {@value Face#SCORE_MIN}, {@value Face#SCORE_MAX}, |
| * the score is clipped first and a warning is printed to logcat.</p> |
| * |
| * <p>All other parameters are passed-through as-is.</p> |
| * |
| * @return a new face without the optional features |
| */ |
| public Face toFace() { |
| int score = clip(weight, |
| Face.SCORE_MIN, |
| Face.SCORE_MAX, |
| rect, |
| "score"); |
| |
| return new Face(rect, score); |
| } |
| |
| private static int clipLower(int value, int lo, Rect rect, String name) { |
| return clip(value, lo, /*hi*/Integer.MAX_VALUE, rect, name); |
| } |
| |
| private static int clip(int value, int lo, int hi, Rect rect, String name) { |
| if (value < lo) { |
| Log.w(TAG, "toMetering - Rectangle " + rect + " " |
| + name + " too small, clip to " + lo); |
| value = lo; |
| } else if (value > hi) { |
| Log.w(TAG, "toMetering - Rectangle " + rect + " " |
| + name + " too small, clip to " + hi); |
| value = hi; |
| } |
| |
| return value; |
| } |
| } |
| |
| private static final String TAG = "ParameterUtils"; |
| private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); |
| |
| /** getZoomRatios stores zoom ratios in 1/100 increments, e.x. a zoom of 3.2 is 320 */ |
| private static final int ZOOM_RATIO_MULTIPLIER = 100; |
| |
| /** |
| * Convert a camera API1 size into a util size |
| */ |
| public static Size convertSize(Camera.Size size) { |
| checkNotNull(size, "size must not be null"); |
| |
| return new Size(size.width, size.height); |
| } |
| |
| /** |
| * Convert a camera API1 list of sizes into a util list of sizes |
| */ |
| public static List<Size> convertSizeList(List<Camera.Size> sizeList) { |
| checkNotNull(sizeList, "sizeList must not be null"); |
| |
| List<Size> sizes = new ArrayList<>(sizeList.size()); |
| for (Camera.Size s : sizeList) { |
| sizes.add(new Size(s.width, s.height)); |
| } |
| return sizes; |
| } |
| |
| /** |
| * Convert a camera API1 list of sizes into an array of sizes |
| */ |
| public static Size[] convertSizeListToArray(List<Camera.Size> sizeList) { |
| checkNotNull(sizeList, "sizeList must not be null"); |
| |
| Size[] array = new Size[sizeList.size()]; |
| int ctr = 0; |
| for (Camera.Size s : sizeList) { |
| array[ctr++] = new Size(s.width, s.height); |
| } |
| return array; |
| } |
| |
| /** |
| * Check if the camera API1 list of sizes contains a size with the given dimens. |
| */ |
| public static boolean containsSize(List<Camera.Size> sizeList, int width, int height) { |
| checkNotNull(sizeList, "sizeList must not be null"); |
| for (Camera.Size s : sizeList) { |
| if (s.height == height && s.width == width) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the largest supported picture size, as compared by its area. |
| */ |
| public static Size getLargestSupportedJpegSizeByArea(Camera.Parameters params) { |
| checkNotNull(params, "params must not be null"); |
| |
| List<Size> supportedJpegSizes = convertSizeList(params.getSupportedPictureSizes()); |
| return SizeAreaComparator.findLargestByArea(supportedJpegSizes); |
| } |
| |
| /** |
| * Convert a camera area into a human-readable string. |
| */ |
| public static String stringFromArea(Camera.Area area) { |
| if (area == null) { |
| return null; |
| } else { |
| StringBuilder sb = new StringBuilder(); |
| Rect r = area.rect; |
| |
| sb.setLength(0); |
| sb.append("(["); sb.append(r.left); sb.append(','); |
| sb.append(r.top); sb.append("]["); sb.append(r.right); |
| sb.append(','); sb.append(r.bottom); sb.append(']'); |
| |
| sb.append(','); |
| sb.append(area.weight); |
| sb.append(')'); |
| |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * Convert a camera area list into a human-readable string |
| * @param areaList a list of areas (null is ok) |
| */ |
| public static String stringFromAreaList(List<Camera.Area> areaList) { |
| StringBuilder sb = new StringBuilder(); |
| |
| if (areaList == null) { |
| return null; |
| } |
| |
| int i = 0; |
| for (Camera.Area area : areaList) { |
| if (area == null) { |
| sb.append("null"); |
| } else { |
| sb.append(stringFromArea(area)); |
| } |
| |
| if (i != areaList.size() - 1) { |
| sb.append(", "); |
| } |
| |
| i++; |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Calculate the closest zoom index for the user-requested crop region by rounding |
| * up to the closest (largest or equal) possible zoom crop. |
| * |
| * <p>If the requested crop region exceeds the size of the active array, it is |
| * shrunk to fit inside of the active array first.</p> |
| * |
| * <p>Since all api1 camera devices only support a discrete set of zooms, we have |
| * to translate the per-pixel-granularity requested crop region into a per-zoom-index |
| * granularity.</p> |
| * |
| * <p>Furthermore, since the zoom index and zoom levels also depends on the field-of-view |
| * of the preview, the current preview {@code streamSize} is also used.</p> |
| * |
| * <p>The calculated crop regions are then written to in-place to {@code reportedCropRegion} |
| * and {@code previewCropRegion}, in coordinates relative to the active array.</p> |
| * |
| * @param params non-{@code null} camera api1 parameters |
| * @param activeArray active array dimensions, in sensor space |
| * @param streamSize stream size dimensions, in pixels |
| * @param cropRegion user-specified crop region, in active array coordinates |
| * @param reportedCropRegion (out parameter) what the result for {@code cropRegion} looks like |
| * @param previewCropRegion (out parameter) what the visual preview crop is |
| * @return |
| * the zoom index inclusively between 0 and {@code Parameters#getMaxZoom}, |
| * where 0 means the camera is not zoomed |
| * |
| * @throws NullPointerException if any of the args were {@code null} |
| */ |
| public static int getClosestAvailableZoomCrop( |
| Camera.Parameters params, Rect activeArray, Size streamSize, Rect cropRegion, |
| /*out*/ |
| Rect reportedCropRegion, |
| Rect previewCropRegion) { |
| checkNotNull(params, "params must not be null"); |
| checkNotNull(activeArray, "activeArray must not be null"); |
| checkNotNull(streamSize, "streamSize must not be null"); |
| checkNotNull(reportedCropRegion, "reportedCropRegion must not be null"); |
| checkNotNull(previewCropRegion, "previewCropRegion must not be null"); |
| |
| Rect actualCrop = new Rect(cropRegion); |
| |
| /* |
| * Shrink requested crop region to fit inside of the active array size |
| */ |
| if (!actualCrop.intersect(activeArray)) { |
| Log.w(TAG, "getClosestAvailableZoomCrop - Crop region out of range; " + |
| "setting to active array size"); |
| actualCrop.set(activeArray); |
| } |
| |
| Rect previewCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize); |
| |
| // Make the user-requested crop region the same aspect ratio as the preview stream size |
| Rect cropRegionAsPreview = |
| shrinkToSameAspectRatioCentered(previewCrop, actualCrop); |
| |
| if (VERBOSE) { |
| Log.v(TAG, "getClosestAvailableZoomCrop - actualCrop = " + actualCrop); |
| Log.v(TAG, |
| "getClosestAvailableZoomCrop - previewCrop = " + previewCrop); |
| Log.v(TAG, |
| "getClosestAvailableZoomCrop - cropRegionAsPreview = " + cropRegionAsPreview); |
| } |
| |
| /* |
| * Iterate all available zoom rectangles and find the closest zoom index |
| */ |
| Rect bestReportedCropRegion = null; |
| Rect bestPreviewCropRegion = null; |
| int bestZoomIndex = -1; |
| |
| List<Rect> availableReportedCropRegions = |
| getAvailableZoomCropRectangles(params, activeArray); |
| List<Rect> availablePreviewCropRegions = |
| getAvailablePreviewZoomCropRectangles(params, activeArray, streamSize); |
| |
| if (VERBOSE) { |
| Log.v(TAG, |
| "getClosestAvailableZoomCrop - availableReportedCropRegions = " + |
| ListUtils.listToString(availableReportedCropRegions)); |
| Log.v(TAG, |
| "getClosestAvailableZoomCrop - availablePreviewCropRegions = " + |
| ListUtils.listToString(availablePreviewCropRegions)); |
| } |
| |
| if (availableReportedCropRegions.size() != availablePreviewCropRegions.size()) { |
| throw new AssertionError("available reported/preview crop region size mismatch"); |
| } |
| |
| for (int i = 0; i < availableReportedCropRegions.size(); ++i) { |
| Rect currentPreviewCropRegion = availablePreviewCropRegions.get(i); |
| Rect currentReportedCropRegion = availableReportedCropRegions.get(i); |
| |
| boolean isBest; |
| if (bestZoomIndex == -1) { |
| isBest = true; |
| } else if (currentPreviewCropRegion.width() >= cropRegionAsPreview.width() && |
| currentPreviewCropRegion.height() >= cropRegionAsPreview.height()) { |
| isBest = true; |
| } else { |
| isBest = false; |
| } |
| |
| // Sizes are sorted largest-to-smallest, so once the available crop is too small, |
| // we the rest are too small. Furthermore, this is the final best crop, |
| // since its the largest crop that still fits the requested crop |
| if (isBest) { |
| bestPreviewCropRegion = currentPreviewCropRegion; |
| bestReportedCropRegion = currentReportedCropRegion; |
| bestZoomIndex = i; |
| } else { |
| break; |
| } |
| } |
| |
| if (bestZoomIndex == -1) { |
| // Even in the worst case, we should always at least return 0 here |
| throw new AssertionError("Should've found at least one valid zoom index"); |
| } |
| |
| // Write the rectangles in-place |
| reportedCropRegion.set(bestReportedCropRegion); |
| previewCropRegion.set(bestPreviewCropRegion); |
| |
| return bestZoomIndex; |
| } |
| |
| /** |
| * Calculate the effective crop rectangle for this preview viewport; |
| * assumes the preview is centered to the sensor and scaled to fit across one of the dimensions |
| * without skewing. |
| * |
| * <p>The preview size must be a subset of the active array size; the resulting |
| * rectangle will also be a subset of the active array rectangle.</p> |
| * |
| * <p>The unzoomed crop rectangle is calculated only.</p> |
| * |
| * @param activeArray active array dimensions, in sensor space |
| * @param previewSize size of the preview buffer render target, in pixels (not in sensor space) |
| * @return a rectangle which serves as the preview stream's effective crop region (unzoomed), |
| * in sensor space |
| * |
| * @throws NullPointerException |
| * if any of the args were {@code null} |
| * @throws IllegalArgumentException |
| * if {@code previewSize} is wider or taller than {@code activeArray} |
| */ |
| private static Rect getPreviewCropRectangleUnzoomed(Rect activeArray, Size previewSize) { |
| if (previewSize.getWidth() > activeArray.width()) { |
| throw new IllegalArgumentException("previewSize must not be wider than activeArray"); |
| } else if (previewSize.getHeight() > activeArray.height()) { |
| throw new IllegalArgumentException("previewSize must not be taller than activeArray"); |
| } |
| |
| float aspectRatioArray = activeArray.width() * 1.0f / activeArray.height(); |
| float aspectRatioPreview = previewSize.getWidth() * 1.0f / previewSize.getHeight(); |
| |
| float cropH, cropW; |
| if (aspectRatioPreview < aspectRatioArray) { |
| // The new width must be smaller than the height, so scale the width by AR |
| cropH = activeArray.height(); |
| cropW = cropH * aspectRatioPreview; |
| } else { |
| // The new height must be smaller (or equal) than the width, so scale the height by AR |
| cropW = activeArray.width(); |
| cropH = cropW / aspectRatioPreview; |
| } |
| |
| Matrix translateMatrix = new Matrix(); |
| RectF cropRect = new RectF(/*left*/0, /*top*/0, cropW, cropH); |
| |
| // Now center the crop rectangle so its center is in the center of the active array |
| translateMatrix.setTranslate(activeArray.exactCenterX(), activeArray.exactCenterY()); |
| translateMatrix.postTranslate(-cropRect.centerX(), -cropRect.centerY()); |
| |
| translateMatrix.mapRect(/*inout*/cropRect); |
| |
| // Round the rect corners towards the nearest integer values |
| return ParamsUtils.createRect(cropRect); |
| } |
| |
| /** |
| * Shrink the {@code shrinkTarget} rectangle to snugly fit inside of {@code reference}; |
| * the aspect ratio of {@code shrinkTarget} will change to be the same aspect ratio as |
| * {@code reference}. |
| * |
| * <p>At most a single dimension will scale (down). Both dimensions will never be scaled.</p> |
| * |
| * @param reference the rectangle whose aspect ratio will be used as the new aspect ratio |
| * @param shrinkTarget the rectangle which will be scaled down to have a new aspect ratio |
| * |
| * @return a new rectangle, a subset of {@code shrinkTarget}, |
| * whose aspect ratio will match that of {@code reference} |
| */ |
| private static Rect shrinkToSameAspectRatioCentered(Rect reference, Rect shrinkTarget) { |
| float aspectRatioReference = reference.width() * 1.0f / reference.height(); |
| float aspectRatioShrinkTarget = shrinkTarget.width() * 1.0f / shrinkTarget.height(); |
| |
| float cropH, cropW; |
| if (aspectRatioShrinkTarget < aspectRatioReference) { |
| // The new width must be smaller than the height, so scale the width by AR |
| cropH = reference.height(); |
| cropW = cropH * aspectRatioShrinkTarget; |
| } else { |
| // The new height must be smaller (or equal) than the width, so scale the height by AR |
| cropW = reference.width(); |
| cropH = cropW / aspectRatioShrinkTarget; |
| } |
| |
| Matrix translateMatrix = new Matrix(); |
| RectF shrunkRect = new RectF(shrinkTarget); |
| |
| // Scale the rectangle down, but keep its center in the same place as before |
| translateMatrix.setScale(cropW / reference.width(), cropH / reference.height(), |
| shrinkTarget.exactCenterX(), shrinkTarget.exactCenterY()); |
| |
| translateMatrix.mapRect(/*inout*/shrunkRect); |
| |
| return ParamsUtils.createRect(shrunkRect); |
| } |
| |
| /** |
| * Get the available 'crop' (zoom) rectangles for this camera that will be reported |
| * via a {@code CaptureResult} when a zoom is requested. |
| * |
| * <p>These crops ignores the underlying preview buffer size, and will always be reported |
| * the same values regardless of what configuration of outputs is used.</p> |
| * |
| * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size, |
| * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p> |
| * |
| * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize}, |
| * by shrinking the rectangle if necessary.</p> |
| * |
| * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize} |
| * = {@code activeArray size}.</p> |
| * |
| * @param params non-{@code null} camera api1 parameters |
| * @param activeArray active array dimensions, in sensor space |
| * @param streamSize stream size dimensions, in pixels |
| * |
| * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed |
| */ |
| public static List<Rect> getAvailableZoomCropRectangles( |
| Camera.Parameters params, Rect activeArray) { |
| checkNotNull(params, "params must not be null"); |
| checkNotNull(activeArray, "activeArray must not be null"); |
| |
| return getAvailableCropRectangles(params, activeArray, ParamsUtils.createSize(activeArray)); |
| } |
| |
| /** |
| * Get the available 'crop' (zoom) rectangles for this camera. |
| * |
| * <p>This is the effective (real) crop that is applied by the camera api1 device |
| * when projecting the zoom onto the intermediate preview buffer. Use this when |
| * deciding which zoom ratio to apply.</p> |
| * |
| * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size, |
| * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p> |
| * |
| * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize}, |
| * by shrinking the rectangle if necessary.</p> |
| * |
| * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize} |
| * = {@code activeArray size}.</p> |
| * |
| * @param params non-{@code null} camera api1 parameters |
| * @param activeArray active array dimensions, in sensor space |
| * @param streamSize stream size dimensions, in pixels |
| * |
| * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed |
| */ |
| public static List<Rect> getAvailablePreviewZoomCropRectangles(Camera.Parameters params, |
| Rect activeArray, Size previewSize) { |
| checkNotNull(params, "params must not be null"); |
| checkNotNull(activeArray, "activeArray must not be null"); |
| checkNotNull(previewSize, "previewSize must not be null"); |
| |
| return getAvailableCropRectangles(params, activeArray, previewSize); |
| } |
| |
| /** |
| * Get the available 'crop' (zoom) rectangles for this camera. |
| * |
| * <p>When zoom is supported, this will return a list of {@code 1 + #getMaxZoom} size, |
| * where each crop rectangle corresponds to a zoom ratio (and is centered at the middle).</p> |
| * |
| * <p>Each crop rectangle is changed to have the same aspect ratio as {@code streamSize}, |
| * by shrinking the rectangle if necessary.</p> |
| * |
| * <p>To get the reported crop region when applying a zoom to the sensor, use {@code streamSize} |
| * = {@code activeArray size}.</p> |
| * |
| * @param params non-{@code null} camera api1 parameters |
| * @param activeArray active array dimensions, in sensor space |
| * @param streamSize stream size dimensions, in pixels |
| * |
| * @return a list of available zoom rectangles, sorted from least zoomed to most zoomed |
| */ |
| private static List<Rect> getAvailableCropRectangles(Camera.Parameters params, |
| Rect activeArray, Size streamSize) { |
| checkNotNull(params, "params must not be null"); |
| checkNotNull(activeArray, "activeArray must not be null"); |
| checkNotNull(streamSize, "streamSize must not be null"); |
| |
| // TODO: change all uses of Rect activeArray to Size activeArray, |
| // since we want the crop to be active-array relative, not pixel-array relative |
| |
| Rect unzoomedStreamCrop = getPreviewCropRectangleUnzoomed(activeArray, streamSize); |
| |
| if (!params.isZoomSupported()) { |
| // Trivial case: No zoom -> only support the full size as the crop region |
| return new ArrayList<>(Arrays.asList(unzoomedStreamCrop)); |
| } |
| |
| List<Rect> zoomCropRectangles = new ArrayList<>(params.getMaxZoom() + 1); |
| Matrix scaleMatrix = new Matrix(); |
| RectF scaledRect = new RectF(); |
| |
| for (int zoom : params.getZoomRatios()) { |
| float shrinkRatio = ZOOM_RATIO_MULTIPLIER * 1.0f / zoom; // normalize to 1.0 and smaller |
| |
| // set scaledRect to unzoomedStreamCrop |
| ParamsUtils.convertRectF(unzoomedStreamCrop, /*out*/scaledRect); |
| |
| scaleMatrix.setScale( |
| shrinkRatio, shrinkRatio, |
| activeArray.exactCenterX(), |
| activeArray.exactCenterY()); |
| |
| scaleMatrix.mapRect(scaledRect); |
| |
| Rect intRect = ParamsUtils.createRect(scaledRect); |
| |
| // Round the rect corners towards the nearest integer values |
| zoomCropRectangles.add(intRect); |
| } |
| |
| return zoomCropRectangles; |
| } |
| |
| /** |
| * Get the largest possible zoom ratio (normalized to {@code 1.0f} and higher) |
| * that the camera can support. |
| * |
| * <p>If the camera does not support zoom, it always returns {@code 1.0f}.</p> |
| * |
| * @param params non-{@code null} camera api1 parameters |
| * @return normalized max zoom ratio, at least {@code 1.0f} |
| */ |
| public static float getMaxZoomRatio(Camera.Parameters params) { |
| if (!params.isZoomSupported()) { |
| return 1.0f; // no zoom |
| } |
| |
| List<Integer> zoomRatios = params.getZoomRatios(); // sorted smallest->largest |
| int zoom = zoomRatios.get(zoomRatios.size() - 1); // largest zoom ratio |
| float zoomRatio = zoom * 1.0f / ZOOM_RATIO_MULTIPLIER; // normalize to 1.0 and smaller |
| |
| return zoomRatio; |
| } |
| |
| /** |
| * Returns the component-wise zoom ratio (each greater or equal than {@code 1.0}); |
| * largest values means more zoom. |
| * |
| * @param activeArraySize active array size of the sensor (e.g. max jpeg size) |
| * @param cropSize size of the crop/zoom |
| * |
| * @return {@link SizeF} with width/height being the component-wise zoom ratio |
| * |
| * @throws NullPointerException if any of the args were {@code null} |
| * @throws IllegalArgumentException if any component of {@code cropSize} was {@code 0} |
| */ |
| private static SizeF getZoomRatio(Size activeArraySize, Size cropSize) { |
| checkNotNull(activeArraySize, "activeArraySize must not be null"); |
| checkNotNull(cropSize, "cropSize must not be null"); |
| checkArgumentPositive(cropSize.getWidth(), "cropSize.width must be positive"); |
| checkArgumentPositive(cropSize.getHeight(), "cropSize.height must be positive"); |
| |
| float zoomRatioWidth = activeArraySize.getWidth() * 1.0f / cropSize.getWidth(); |
| float zoomRatioHeight = activeArraySize.getHeight() * 1.0f / cropSize.getHeight(); |
| |
| return new SizeF(zoomRatioWidth, zoomRatioHeight); |
| } |
| |
| /** |
| * Convert the user-specified crop region into zoom data; which can be used |
| * to set the parameters to a specific zoom index, or to report back to the user what the |
| * actual zoom was, or for other calculations requiring the current preview crop region. |
| * |
| * <p>None of the parameters are mutated.</p> |
| * |
| * @param activeArraySize active array size of the sensor (e.g. max jpeg size) |
| * @param cropRegion the user-specified crop region |
| * @param previewSize the current preview size (in pixels) |
| * @param params the current camera parameters (not mutated) |
| * |
| * @return the zoom index, and the effective/reported crop regions (relative to active array) |
| */ |
| public static ZoomData convertScalerCropRegion(Rect activeArraySize, Rect |
| cropRegion, Size previewSize, Camera.Parameters params) { |
| Rect activeArraySizeOnly = new Rect( |
| /*left*/0, /*top*/0, |
| activeArraySize.width(), activeArraySize.height()); |
| |
| Rect userCropRegion = cropRegion; |
| |
| if (userCropRegion == null) { |
| userCropRegion = activeArraySizeOnly; |
| } |
| |
| if (VERBOSE) { |
| Log.v(TAG, "convertScalerCropRegion - user crop region was " + userCropRegion); |
| } |
| |
| final Rect reportedCropRegion = new Rect(); |
| final Rect previewCropRegion = new Rect(); |
| final int zoomIdx = ParameterUtils.getClosestAvailableZoomCrop(params, activeArraySizeOnly, |
| previewSize, userCropRegion, |
| /*out*/reportedCropRegion, /*out*/previewCropRegion); |
| |
| if (VERBOSE) { |
| Log.v(TAG, "convertScalerCropRegion - zoom calculated to: " + |
| "zoomIndex = " + zoomIdx + |
| ", reported crop region = " + reportedCropRegion + |
| ", preview crop region = " + previewCropRegion); |
| } |
| |
| return new ZoomData(zoomIdx, previewCropRegion, reportedCropRegion); |
| } |
| |
| /** |
| * Calculate the actual/effective/reported normalized rectangle data from a metering |
| * rectangle. |
| * |
| * <p>If any of the rectangles are out-of-range of their intended bounding box, |
| * the {@link #RECTANGLE_EMPTY empty rectangle} is substituted instead |
| * (with a weight of {@code 0}).</p> |
| * |
| * <p>The metering rectangle is bound by the crop region (effective/reported respectively). |
| * The metering {@link Camera.Area area} is bound by {@code [-1000, 1000]}.</p> |
| * |
| * <p>No parameters are mutated; returns the new metering data.</p> |
| * |
| * @param activeArraySize active array size of the sensor (e.g. max jpeg size) |
| * @param meteringRect the user-specified metering rectangle |
| * @param zoomData the calculated zoom data corresponding to this request |
| * |
| * @return the metering area, the reported/effective metering rectangles |
| */ |
| public static MeteringData convertMeteringRectangleToLegacy( |
| Rect activeArray, MeteringRectangle meteringRect, ZoomData zoomData) { |
| Rect previewCrop = zoomData.previewCrop; |
| |
| float scaleW = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f / |
| previewCrop.width(); |
| float scaleH = (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN) * 1.0f / |
| previewCrop.height(); |
| |
| Matrix transform = new Matrix(); |
| // Move the preview crop so that top,left is at (0,0), otherwise after scaling |
| // the corner bounds will be outside of [-1000, 1000] |
| transform.setTranslate(-previewCrop.left, -previewCrop.top); |
| // Scale into [0, 2000] range about the center of the preview |
| transform.postScale(scaleW, scaleH); |
| // Move so that top left of a typical rect is at [-1000, -1000] |
| transform.postTranslate(/*dx*/NORMALIZED_RECTANGLE_MIN, /*dy*/NORMALIZED_RECTANGLE_MIN); |
| |
| /* |
| * Calculate the preview metering region (effective), and the camera1 api |
| * normalized metering region. |
| */ |
| Rect normalizedRegionUnbounded = ParamsUtils.mapRect(transform, meteringRect.getRect()); |
| |
| /* |
| * Try to intersect normalized area with [-1000, 1000] rectangle; otherwise |
| * it's completely out of range |
| */ |
| Rect normalizedIntersected = new Rect(normalizedRegionUnbounded); |
| |
| Camera.Area meteringArea; |
| if (!normalizedIntersected.intersect(NORMALIZED_RECTANGLE_DEFAULT)) { |
| Log.w(TAG, |
| "convertMeteringRectangleToLegacy - metering rectangle too small, " + |
| "no metering will be done"); |
| normalizedIntersected.set(RECTANGLE_EMPTY); |
| meteringArea = new Camera.Area(RECTANGLE_EMPTY, |
| MeteringRectangle.METERING_WEIGHT_DONT_CARE); |
| } else { |
| meteringArea = new Camera.Area(normalizedIntersected, |
| meteringRect.getMeteringWeight()); |
| } |
| |
| /* |
| * Calculate effective preview metering region |
| */ |
| Rect previewMetering = meteringRect.getRect(); |
| if (!previewMetering.intersect(previewCrop)) { |
| previewMetering.set(RECTANGLE_EMPTY); |
| } |
| |
| /* |
| * Calculate effective reported metering region |
| * - Transform the calculated metering area back into active array space |
| * - Clip it to be a subset of the reported crop region |
| */ |
| Rect reportedMetering; |
| { |
| Camera.Area normalizedAreaUnbounded = new Camera.Area( |
| normalizedRegionUnbounded, meteringRect.getMeteringWeight()); |
| WeightedRectangle reportedMeteringRect = convertCameraAreaToActiveArrayRectangle( |
| activeArray, zoomData, normalizedAreaUnbounded, /*usePreviewCrop*/false); |
| reportedMetering = reportedMeteringRect.rect; |
| } |
| |
| if (VERBOSE) { |
| Log.v(TAG, String.format( |
| "convertMeteringRectangleToLegacy - activeArray = %s, meteringRect = %s, " + |
| "previewCrop = %s, meteringArea = %s, previewMetering = %s, " + |
| "reportedMetering = %s, normalizedRegionUnbounded = %s", |
| activeArray, meteringRect, |
| previewCrop, stringFromArea(meteringArea), previewMetering, |
| reportedMetering, normalizedRegionUnbounded)); |
| } |
| |
| return new MeteringData(meteringArea, previewMetering, reportedMetering); |
| } |
| |
| /** |
| * Convert the normalized camera area from [-1000, 1000] coordinate space |
| * into the active array-based coordinate space. |
| * |
| * <p>Values out of range are clipped to be within the resulting (reported) crop |
| * region. It is possible to have values larger than the preview crop.</p> |
| * |
| * <p>Weights out of range of [0, 1000] are clipped to be within the range.</p> |
| * |
| * @param activeArraySize active array size of the sensor (e.g. max jpeg size) |
| * @param zoomData the calculated zoom data corresponding to this request |
| * @param area the normalized camera area |
| * |
| * @return the weighed rectangle in active array coordinate space, with the weight |
| */ |
| public static WeightedRectangle convertCameraAreaToActiveArrayRectangle( |
| Rect activeArray, ZoomData zoomData, Camera.Area area) { |
| return convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, area, |
| /*usePreviewCrop*/true); |
| } |
| |
| /** |
| * Convert an api1 face into an active-array based api2 face. |
| * |
| * <p>Out-of-ranges scores and ids will be clipped to be within range (with a warning).</p> |
| * |
| * @param face a non-{@code null} api1 face |
| * @param activeArraySize active array size of the sensor (e.g. max jpeg size) |
| * @param zoomData the calculated zoom data corresponding to this request |
| * |
| * @return a non-{@code null} api2 face |
| * |
| * @throws NullPointerException if the {@code face} was {@code null} |
| */ |
| public static Face convertFaceFromLegacy(Camera.Face face, Rect activeArray, |
| ZoomData zoomData) { |
| checkNotNull(face, "face must not be null"); |
| |
| Face api2Face; |
| |
| Camera.Area fakeArea = new Camera.Area(face.rect, /*weight*/1); |
| |
| WeightedRectangle faceRect = |
| convertCameraAreaToActiveArrayRectangle(activeArray, zoomData, fakeArea); |
| |
| Point leftEye = face.leftEye, rightEye = face.rightEye, mouth = face.mouth; |
| if (leftEye != null && rightEye != null && mouth != null) { |
| leftEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData, |
| leftEye, /*usePreviewCrop*/true); |
| rightEye = convertCameraPointToActiveArrayPoint(activeArray, zoomData, |
| leftEye, /*usePreviewCrop*/true); |
| mouth = convertCameraPointToActiveArrayPoint(activeArray, zoomData, |
| leftEye, /*usePreviewCrop*/true); |
| |
| api2Face = faceRect.toFace(face.id, leftEye, rightEye, mouth); |
| } else { |
| api2Face = faceRect.toFace(); |
| } |
| |
| return api2Face; |
| } |
| |
| private static Point convertCameraPointToActiveArrayPoint( |
| Rect activeArray, ZoomData zoomData, Point point, boolean usePreviewCrop) { |
| Rect pointedRect = new Rect(point.x, point.y, point.x, point.y); |
| Camera.Area pointedArea = new Area(pointedRect, /*weight*/1); |
| |
| WeightedRectangle adjustedRect = |
| convertCameraAreaToActiveArrayRectangle(activeArray, |
| zoomData, pointedArea, usePreviewCrop); |
| |
| Point transformedPoint = new Point(adjustedRect.rect.left, adjustedRect.rect.top); |
| |
| return transformedPoint; |
| } |
| |
| private static WeightedRectangle convertCameraAreaToActiveArrayRectangle( |
| Rect activeArray, ZoomData zoomData, Camera.Area area, boolean usePreviewCrop) { |
| Rect previewCrop = zoomData.previewCrop; |
| Rect reportedCrop = zoomData.reportedCrop; |
| |
| float scaleW = previewCrop.width() * 1.0f / |
| (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN); |
| float scaleH = previewCrop.height() * 1.0f / |
| (NORMALIZED_RECTANGLE_MAX - NORMALIZED_RECTANGLE_MIN); |
| |
| /* |
| * Calculate the reported metering region from the non-intersected normalized region |
| * by scaling and translating back into active array-relative coordinates. |
| */ |
| Matrix transform = new Matrix(); |
| |
| // Move top left from (-1000, -1000) to (0, 0) |
| transform.setTranslate(/*dx*/NORMALIZED_RECTANGLE_MAX, /*dy*/NORMALIZED_RECTANGLE_MAX); |
| |
| // Scale from [0, 2000] back into the preview rectangle |
| transform.postScale(scaleW, scaleH); |
| |
| // Move the rect so that the [-1000,-1000] point ends up at the preview [left, top] |
| transform.postTranslate(previewCrop.left, previewCrop.top); |
| |
| Rect cropToIntersectAgainst = usePreviewCrop ? previewCrop : reportedCrop; |
| |
| // Now apply the transformation backwards to get the reported metering region |
| Rect reportedMetering = ParamsUtils.mapRect(transform, area.rect); |
| // Intersect it with the crop region, to avoid reporting out-of-bounds |
| // metering regions |
| if (!reportedMetering.intersect(cropToIntersectAgainst)) { |
| reportedMetering.set(RECTANGLE_EMPTY); |
| } |
| |
| int weight = area.weight; |
| if (weight < MeteringRectangle.METERING_WEIGHT_MIN) { |
| Log.w(TAG, |
| "convertCameraAreaToMeteringRectangle - rectangle " |
| + stringFromArea(area) + " has too small weight, clip to 0"); |
| weight = 0; |
| } |
| |
| return new WeightedRectangle(reportedMetering, area.weight); |
| } |
| |
| |
| private ParameterUtils() { |
| throw new AssertionError(); |
| } |
| } |