Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[camera] android-rework part 6: Android exposure- and focus point features #4039

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/camera/camera/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ android {
dependencies {
compileOnly 'androidx.annotation:annotation:1.1.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-inline:3.5.13'
testImplementation 'org.mockito:mockito-inline:3.11.1'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.robolectric:robolectric:4.3'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.camera;

import android.annotation.TargetApi;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.Arrays;

/**
* Utility class offering functions to calculate values regarding the camera boundaries.
*
* <p>The functions are used to calculate focus and exposure settings.
*/
public final class CameraRegionUtils {

/**
* Obtains the boundaries for the currently active camera, that can be used for calculating
* MeteringRectangle instances required for setting focus or exposure settings.
*
* @param cameraProperties - Collection of the characteristics for the current camera device.
* @param requestBuilder - The request builder for the current capture request.
* @return The boundaries for the current camera device.
*/
public static Size getCameraBoundaries(
@NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& supportsDistortionCorrection(cameraProperties)) {
// Get the current distortion correction mode.
Integer distortionCorrectionMode =
requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE);

// Return the correct boundaries depending on the mode.
android.graphics.Rect rect;
if (distortionCorrectionMode == null
|| distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) {
rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize();
} else {
rect = cameraProperties.getSensorInfoActiveArraySize();
}

return SizeFactory.create(rect.width(), rect.height());
} else {
// No distortion correction support.
return cameraProperties.getSensorInfoPixelArraySize();
}
}

/**
* Converts a point into a {@link MeteringRectangle} with the supplied coordinates as the center
* point.
*
* <p>Since the Camera API (due to cross-platform constraints) only accepts a point when
* configuring a specific focus or exposure area and Android requires a rectangle to configure
* these settings there is a need to convert the point into a rectangle. This method will create
* the required rectangle with an arbitrarily size that is a 10th of the current viewport and the
* coordinates as the center point.
*
* @param boundaries - The camera boundaries to calculate the metering rectangle for.
* @param x x - 1 >= coordinate >= 0.
* @param y y - 1 >= coordinate >= 0.
* @return The dimensions of the metering rectangle based on the supplied coordinates and
* boundaries.
*/
public static MeteringRectangle convertPointToMeteringRectangle(
@NonNull Size boundaries, double x, double y) {
assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0);
assert (x >= 0 && x <= 1);
assert (y >= 0 && y <= 1);

// Interpolate the target coordinate.
int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1)));
int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1)));
// Determine the dimensions of the metering rectangle (10th of the viewport).
int targetWidth = (int) Math.round(((double) boundaries.getWidth()) / 10d);
int targetHeight = (int) Math.round(((double) boundaries.getHeight()) / 10d);
// Adjust target coordinate to represent top-left corner of metering rectangle.
targetX -= targetWidth / 2;
targetY -= targetHeight / 2;
// Adjust target coordinate as to not fall out of bounds.
if (targetX < 0) {
targetX = 0;
}
if (targetY < 0) {
targetY = 0;
}
int maxTargetX = boundaries.getWidth() - 1 - targetWidth;
int maxTargetY = boundaries.getHeight() - 1 - targetHeight;
if (targetX > maxTargetX) {
targetX = maxTargetX;
}
if (targetY > maxTargetY) {
targetY = maxTargetY;
}

// Build the metering rectangle.
return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1);
}

@TargetApi(Build.VERSION_CODES.P)
private static boolean supportsDistortionCorrection(CameraProperties cameraProperties) {
int[] availableDistortionCorrectionModes =
cameraProperties.getDistortionCorrectionAvailableModes();
if (availableDistortionCorrectionModes == null) {
availableDistortionCorrectionModes = new int[0];
}
long nonOffModesSupported =
Arrays.stream(availableDistortionCorrectionModes)
.filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF)
.count();
return nonOffModesSupported > 0;
}

/** Factory class that assists in creating a {@link MeteringRectangle} instance. */
static class MeteringRectangleFactory {
/**
* Creates a new instance of the {@link MeteringRectangle} class.
*
* <p>This method is visible for testing purposes only and should never be used outside this *
* class.
*
* @param x coordinate >= 0.
* @param y coordinate >= 0.
* @param width width >= 0.
* @param height height >= 0.
* @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and
* {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively
* @return new instance of the {@link MeteringRectangle} class.
* @throws IllegalArgumentException if any of the parameters were negative.
*/
@VisibleForTesting
public static MeteringRectangle create(
int x, int y, int width, int height, int meteringWeight) {
return new MeteringRectangle(x, y, width, height, meteringWeight);
}
}

/** Factory class that assists in creating a {@link Size} instance. */
static class SizeFactory {
/**
* Creates a new instance of the {@link Size} class.
*
* <p>This method is visible for testing purposes only and should never be used outside this *
* class.
*
* @param width width >= 0.
* @param height height >= 0.
* @return new instance of the {@link Size} class.
*/
@VisibleForTesting
public static Size create(int width, int height) {
return new Size(width, height);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,37 @@

import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.MeteringRectangle;
import android.util.Log;
import android.util.Size;
import androidx.annotation.NonNull;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.CameraRegionUtils;
import io.flutter.plugins.camera.features.CameraFeature;
import io.flutter.plugins.camera.features.Point;
import io.flutter.plugins.camera.types.CameraRegions;

/** Exposure point controls where in the frame exposure metering will come from. */
public class ExposurePointFeature extends CameraFeature<Point> {

private final CameraRegions cameraRegions;
private Point currentSetting = new Point(0.0, 0.0);
private Size cameraBoundaries;
private Point exposurePoint;
private MeteringRectangle exposureRectangle;

/**
* Creates a new instance of the {@link ExposurePointFeature}.
*
* @param cameraProperties Collection of the characteristics for the current camera device.
* @param cameraRegions Utility class to assist in calculating exposure boundaries.
*/
public ExposurePointFeature(CameraProperties cameraProperties, CameraRegions cameraRegions) {
public ExposurePointFeature(CameraProperties cameraProperties) {
super(cameraProperties);
this.cameraRegions = cameraRegions;
}

/**
* Sets the camera boundaries that are required for the exposure point feature to function.
*
* @param cameraBoundaries - The camera boundaries to set.
*/
public void setCameraBoundaries(@NonNull Size cameraBoundaries) {
this.cameraBoundaries = cameraBoundaries;
this.buildExposureRectangle();
}

@Override
Expand All @@ -37,18 +46,13 @@ public String getDebugName() {

@Override
public Point getValue() {
return currentSetting;
return exposurePoint;
}

@Override
public void setValue(@NonNull Point value) {
this.currentSetting = value;

if (value.x == null || value.y == null) {
cameraRegions.resetAutoExposureMeteringRectangle();
} else {
cameraRegions.setAutoExposureMeteringRectangleFromPoint(value.x, value.y);
}
public void setValue(Point value) {
this.exposurePoint = (value == null || value.x == null || value.y == null) ? null : value;
this.buildExposureRectangle();
}

// Whether or not this camera can set the exposure point.
Expand All @@ -63,16 +67,22 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
if (!checkIsSupported()) {
return;
}

MeteringRectangle aeRect = null;
try {
aeRect = cameraRegions.getAEMeteringRectangle();
} catch (Exception e) {
Log.w("Camera", "Unable to retrieve the Auto Exposure metering rectangle.", e);
}

requestBuilder.set(
CaptureRequest.CONTROL_AE_REGIONS,
aeRect == null ? null : new MeteringRectangle[] {aeRect});
exposureRectangle == null ? null : new MeteringRectangle[] {exposureRectangle});
}

private void buildExposureRectangle() {
if (this.cameraBoundaries == null) {
throw new AssertionError(
"The cameraBoundaries should be set (using `ExposurePointFeature.setCameraBoundaries(Size)`) before updating the exposure point.");
}
if (this.exposurePoint == null) {
this.exposureRectangle = null;
} else {
this.exposureRectangle =
CameraRegionUtils.convertPointToMeteringRectangle(
this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package io.flutter.plugins.camera.features.focuspoint;

import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.MeteringRectangle;
import android.util.Size;
import androidx.annotation.NonNull;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.CameraRegionUtils;
import io.flutter.plugins.camera.features.CameraFeature;
import io.flutter.plugins.camera.features.Point;

/** Focus point controls where in the frame focus will come from. */
public class FocusPointFeature extends CameraFeature<Point> {

private Size cameraBoundaries;
private Point focusPoint;
private MeteringRectangle focusRectangle;

/**
* Creates a new instance of the {@link FocusPointFeature}.
*
* @param cameraProperties Collection of the characteristics for the current camera device.
*/
public FocusPointFeature(CameraProperties cameraProperties) {
super(cameraProperties);
}

/**
* Sets the camera boundaries that are required for the focus point feature to function.
*
* @param cameraBoundaries - The camera boundaries to set.
*/
public void setCameraBoundaries(@NonNull Size cameraBoundaries) {
this.cameraBoundaries = cameraBoundaries;
this.buildFocusRectangle();
}

@Override
public String getDebugName() {
return "FocusPointFeature";
}

@Override
public Point getValue() {
return focusPoint;
}

@Override
public void setValue(Point value) {
this.focusPoint = value == null || value.x == null || value.y == null ? null : value;
this.buildFocusRectangle();
}

// Whether or not this camera can set the focus point.
@Override
public boolean checkIsSupported() {
Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus();
return supportedRegions != null && supportedRegions > 0;
}

@Override
public void updateBuilder(CaptureRequest.Builder requestBuilder) {
if (!checkIsSupported()) {
return;
}
requestBuilder.set(
CaptureRequest.CONTROL_AF_REGIONS,
focusRectangle == null ? null : new MeteringRectangle[] {focusRectangle});
}

private void buildFocusRectangle() {
if (this.cameraBoundaries == null) {
throw new AssertionError(
"The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point.");
}
if (this.focusPoint == null) {
this.focusRectangle = null;
} else {
this.focusRectangle =
CameraRegionUtils.convertPointToMeteringRectangle(
this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y);
}
}
}
Loading