From f04c9bcbdf9c622fe08ad43ef858f9af6c69c2c6 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Fri, 30 Aug 2019 17:09:22 +0200 Subject: [PATCH] Fix focus when zooming --- .../cameraview/engine/Meter.java | 192 +++++++++++------- 1 file changed, 122 insertions(+), 70 deletions(-) diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java index abf08b39..751a3c44 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/engine/Meter.java @@ -119,12 +119,6 @@ public class Meter { mCallback = callback; } - @NonNull - private T readCharacteristic(@NonNull CameraCharacteristics.Key key, @NonNull T fallback) { - T value = mCharacteristics.get(key); - return value == null ? fallback : value; - } - /** * Starts a metering sequence. * @param point point @@ -139,13 +133,57 @@ public class Meter { // This is a good Q/A. https://stackoverflow.com/a/33181620/4288782 // At first, the point is relative to the View system and does not account our own cropping. // Will keep updating these two below. - PointF referencePoint = new PointF(mPoint.x, mPoint.y); - Size referenceSize /* = previewSurfaceSize */; + final PointF referencePoint = new PointF(mPoint.x, mPoint.y); + Size referenceSize = mEngine.mPreview.getSurfaceSize(); // 1. Account for cropping. + // This will enlarge the preview size so that aspect ratio matches. + referenceSize = applyPreviewCropping(referenceSize, referencePoint); + + // 2. Scale to the preview stream coordinates. + // This will move to the preview stream coordinates by scaling. + referenceSize = applyPreviewScale(referenceSize, referencePoint); + + // 3. Rotate to the stream coordinate system. + // This leaves us with sensor stream coordinates. + referenceSize = applyPreviewToSensorRotation(referenceSize, referencePoint); + + // 4. Move to the crop region coordinate system. + // The crop region is the union of all currently active streams. + referenceSize = applyCropRegionCoordinates(referenceSize, referencePoint); + + // 5. Move to the active array coordinate system. + referenceSize = applyActiveArrayCoordinates(referenceSize, referencePoint); + + // 6. Now we can compute the metering regions. + // We want to define them as a fraction of the visible size which (apart from cropping) + // can be obtained through the SENSOR rotated preview stream size. + Size visibleSize = mEngine.getPreviewStreamSize(Reference.SENSOR); + //noinspection ConstantConditions + MeteringRectangle area1 = createMeteringRectangle(referenceSize, referencePoint, visibleSize, 0.05F, 1000); + MeteringRectangle area2 = createMeteringRectangle(referenceSize, referencePoint, visibleSize, 0.1F, 100); + List areas = Arrays.asList(area1, area2); + + // 7. And finally dispatch everything + mAutoFocus.startMetering(mCharacteristics, mBuilder, areas); + mAutoWhiteBalance.startMetering(mCharacteristics, mBuilder, areas); + mAutoExposure.startMetering(mCharacteristics, mBuilder, areas); + + // Dispatch to callback + mCallback.onMeteringStarted(mPoint, mGesture); + mMeteringStartTime = System.currentTimeMillis(); + } + + @SuppressWarnings("UnnecessaryLocalVariable") + @NonNull + private Size applyPreviewCropping(@NonNull Size referenceSize, @NonNull PointF referencePoint) { Size previewStreamSize = mEngine.getPreviewStreamSize(Reference.VIEW); - Size previewSurfaceSize = mEngine.mPreview.getSurfaceSize(); - if (previewStreamSize == null) throw new IllegalStateException("getPreviewStreamSize should not be null at this point."); + Size previewSurfaceSize = referenceSize; + if (previewStreamSize == null) { + throw new IllegalStateException("getPreviewStreamSize should not be null at this point."); + } + int referenceWidth = previewSurfaceSize.getWidth(); + int referenceHeight = previewSurfaceSize.getHeight(); AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize); AspectRatio previewSurfaceAspectRatio = AspectRatio.of(previewSurfaceSize); if (mEngine.mPreview.isCropping()) { @@ -154,30 +192,41 @@ public class Meter { // of the surface is not on the left size of stream (it's more to the right). float scale = previewStreamAspectRatio.toFloat() / previewSurfaceAspectRatio.toFloat(); referencePoint.x += previewSurfaceSize.getWidth() * (scale - 1F) / 2F; - + referenceWidth = Math.round(previewSurfaceSize.getWidth() * scale); } else { // Stream is taller. The y coordinate must be increased: a touch on the top side // of the surface is not on the top size of stream (it's a bit lower). float scale = previewSurfaceAspectRatio.toFloat() / previewStreamAspectRatio.toFloat(); - referencePoint.x += previewSurfaceSize.getHeight() * (scale - 1F) / 2F; + referencePoint.y += previewSurfaceSize.getHeight() * (scale - 1F) / 2F; + referenceHeight = Math.round(previewSurfaceSize.getHeight() * scale); } } + return new Size(referenceWidth, referenceHeight); + } - // 2. Scale to the stream coordinates (not the surface). - referencePoint.x *= (float) previewStreamSize.getWidth() / previewSurfaceSize.getWidth(); - referencePoint.y *= (float) previewStreamSize.getHeight() / previewSurfaceSize.getHeight(); - referenceSize = previewStreamSize; + @SuppressWarnings("ConstantConditions") + @NonNull + private Size applyPreviewScale(@NonNull Size referenceSize, @NonNull PointF referencePoint) { + // The referenceSize how has the same aspect ratio of the previewStreamSize, but they + // can still have different size (that is, a scale operation is needed). + Size previewStreamSize = mEngine.getPreviewStreamSize(Reference.VIEW); + referencePoint.x *= (float) previewStreamSize.getWidth() / referenceSize.getWidth(); + referencePoint.y *= (float) previewStreamSize.getHeight() / referenceSize.getHeight(); + return previewStreamSize; + } - // 3. Rotate to the stream coordinate system. - // Not elegant, but the sin/cos way was failing. + @SuppressWarnings("SuspiciousNameCombination") + @NonNull + private Size applyPreviewToSensorRotation(@NonNull Size referenceSize, @NonNull PointF referencePoint) { + // Not elegant, but the sin/cos way was failing for some reason. int angle = mEngine.getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); boolean flip = angle % 180 != 0; - float tempX = referencePoint.x; float tempY = referencePoint.y; + float tempX = referencePoint.x; + float tempY = referencePoint.y; if (angle == 0) { referencePoint.x = tempX; referencePoint.y = tempY; } else if (angle == 90) { - //noinspection SuspiciousNameCombination referencePoint.x = tempY; referencePoint.y = referenceSize.getWidth() - tempX; } else if (angle == 180) { @@ -185,53 +234,42 @@ public class Meter { referencePoint.y = referenceSize.getHeight() - tempY; } else if (angle == 270) { referencePoint.x = referenceSize.getHeight() - tempY; - //noinspection SuspiciousNameCombination referencePoint.y = tempX; } else { throw new IllegalStateException("Unexpected angle " + angle); } - referenceSize = flip ? referenceSize.flip() : referenceSize; + return flip ? referenceSize.flip() : referenceSize; + } - // These points are now referencing the stream rect on the sensor array. - // But we still have to figure out how the stream rect is laid on the sensor array. + @NonNull + private Size applyCropRegionCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { + // The input point and size refer to the stream rect. + // The stream rect is part of the 'crop region', as described below. // https://source.android.com/devices/camera/camera3_crop_reprocess.html - // For sanity, let's assume it is centered. - // For sanity, let's also assume that the crop region is equal to the stream region. - - // 4. Move to the active sensor array coordinate system. - Rect activeRect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, - new Rect(0, 0, referenceSize.getWidth(), referenceSize.getHeight())); - referencePoint.x += (activeRect.width() - referenceSize.getWidth()) / 2F; - referencePoint.y += (activeRect.height() - referenceSize.getHeight()) / 2F; - referenceSize = new Size(activeRect.width(), activeRect.height()); - - // 5. Account for zoom! This only works for mZoomValue = 0. - // We must scale down with respect to the reference size center. If mZoomValue = 1, - // This must leave everything unchanged. - float maxZoom = readCharacteristic(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM, - 1F /* no zoom */); - float currZoom = 1 + mEngine.mZoomValue * (maxZoom - 1); // 1 ... maxZoom - float currReduction = 1 / currZoom; - float referenceCenterX = referenceSize.getWidth() / 2F; - float referenceCenterY = referenceSize.getHeight() / 2F; - referencePoint.x = referenceCenterX + currReduction * (referencePoint.x - referenceCenterX); - referencePoint.y = referenceCenterY + currReduction * (referencePoint.y - referenceCenterY); - - // 6. NOW we can compute the metering regions. - float visibleWidth = referenceSize.getWidth() * currReduction; - float visibleHeight = referenceSize.getHeight() * currReduction; - MeteringRectangle area1 = createMeteringRectangle(referencePoint, referenceSize, visibleWidth, visibleHeight, 0.05F, 1000); - MeteringRectangle area2 = createMeteringRectangle(referencePoint, referenceSize, visibleWidth, visibleHeight, 0.1F, 100); - List areas = Arrays.asList(area1, area2); - - // 7. And finally dispatch everything - mAutoFocus.startMetering(mCharacteristics, mBuilder, areas); - mAutoWhiteBalance.startMetering(mCharacteristics, mBuilder, areas); - mAutoExposure.startMetering(mCharacteristics, mBuilder, areas); + Rect cropRect = mBuilder.get(CaptureRequest.SCALER_CROP_REGION); + // For now, we don't care about x and y position. Rect should be non-null, but let's be safe. + int cropRectWidth = cropRect == null ? referenceSize.getWidth() : cropRect.width(); + int cropRectHeight = cropRect == null ? referenceSize.getHeight() : cropRect.height(); + // The stream is always centered inside the crop region, and one of the dimensions + // should always match. We just increase the other one. + referencePoint.x += (cropRectWidth - referenceSize.getWidth()) / 2F; + referencePoint.y += (cropRectHeight - referenceSize.getHeight()) / 2F; + return new Size(cropRectWidth, cropRectHeight); + } - // Dispatch to callback - mCallback.onMeteringStarted(mPoint, mGesture); - mMeteringStartTime = System.currentTimeMillis(); + @NonNull + private Size applyActiveArrayCoordinates(@NonNull Size referenceSize, @NonNull PointF referencePoint) { + // The input point and size refer to the scaler crop region. + // We can query for the crop region position inside the active array, so this is easy. + Rect cropRect = mBuilder.get(CaptureRequest.SCALER_CROP_REGION); + referencePoint.x += cropRect == null ? 0 : cropRect.left; + referencePoint.y += cropRect == null ? 0 : cropRect.top; + // Finally, get the active rect width and height from characteristics. + Rect activeRect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + if (activeRect == null) { // Should never happen + activeRect = new Rect(0, 0, referenceSize.getWidth(), referenceSize.getHeight()); + } + return new Size(activeRect.width(), activeRect.height()); } /** @@ -243,16 +281,29 @@ public class Meter { */ @NonNull private MeteringRectangle createMeteringRectangle( - @NonNull PointF center, @NonNull Size boundaries, - float visibleWidth, float visibleHeight, - float factor, int weight) { - float halfWidth = factor * visibleWidth / 2F; - float halfHeight = factor * visibleHeight / 2F; + @NonNull Size boundaries, + @NonNull PointF center, + @NonNull Size visibleSize, + float factor, + int weight) { + float rectangleWidth = factor * visibleSize.getWidth(); + float rectangleHeight = factor * visibleSize.getHeight(); + float rectangleLeft = center.x - rectangleWidth / 2F; + float rectangleTop = center.y - rectangleHeight / 2F; + // Respect boundaries + if (rectangleLeft < 0) rectangleLeft = 0; + if (rectangleTop < 0) rectangleTop = 0; + if (rectangleLeft + rectangleWidth > boundaries.getWidth()) { + rectangleWidth = boundaries.getWidth() - rectangleLeft; + } + if (rectangleTop + rectangleHeight > boundaries.getHeight()) { + rectangleHeight = boundaries.getHeight() - rectangleTop; + } return new MeteringRectangle( - (int) Math.max(0, center.x - halfWidth), - (int) Math.max(0, center.y - halfHeight), - (int) Math.min(boundaries.getWidth(), halfWidth * 2F), - (int) Math.min(boundaries.getHeight(), halfHeight * 2F), + (int) rectangleLeft, + (int) rectangleTop, + (int) rectangleWidth, + (int) rectangleHeight, weight ); } @@ -310,7 +361,8 @@ public class Meter { mEngine.mHandler.remove(mResetRunnable); if (mCallback.canResetMetering(mPoint, mGesture)) { LOG.i("Resetting the meter parameters."); - Rect whole = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, new Rect()); + Rect whole = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + if (whole == null) whole = new Rect(); MeteringRectangle rectangle = new MeteringRectangle(whole, MeteringRectangle.METERING_WEIGHT_DONT_CARE); mAutoFocus.resetMetering(mCharacteristics, mBuilder, rectangle); mAutoWhiteBalance.resetMetering(mCharacteristics, mBuilder, rectangle);