feat(android): autoFocusPointOfInterest, Camera & Camera2 (#1974)

* Android autoFocusPointOfInterest, Camera & Camera2

* updated example with touch to focus
This commit is contained in:
Craig Tuttle 2019-03-30 13:45:44 -07:00 committed by Sibelius Seraphini
parent d8922ac90e
commit 7bb9a1205c
7 changed files with 362 additions and 75 deletions

View File

@ -17,11 +17,13 @@
package com.google.android.cameraview;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.support.v4.util.SparseArrayCompat;
import android.util.Log;
import android.view.SurfaceHolder;
@ -30,6 +32,7 @@ import com.facebook.react.bridge.ReadableMap;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
@ -63,6 +66,12 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
WB_MODES.put(Constants.WB_INCANDESCENT, Camera.Parameters.WHITE_BALANCE_INCANDESCENT);
}
private static final int FOCUS_AREA_SIZE_DEFAULT = 300;
private static final int FOCUS_METERING_AREA_WEIGHT_DEFAULT = 1000;
private static final int DELAY_MILLIS_BEFORE_RESETTING_FOCUS = 3000;
private Handler mHandler = new Handler();
private int mCameraId;
private final AtomicBoolean isPictureCaptureInProgress = new AtomicBoolean(false);
@ -712,6 +721,105 @@ class Camera1 extends CameraViewImpl implements MediaRecorder.OnInfoListener,
}
}
// Most credit: https://github.com/CameraKit/camerakit-android/blob/master/camerakit-core/src/main/api16/com/wonderkiln/camerakit/Camera1.java
void setFocusArea(float x, float y) {
if (mCamera != null) {
Camera.Parameters parameters = mCamera.getParameters();
if (parameters == null) return;
String focusMode = parameters.getFocusMode();
Rect rect = calculateFocusArea(x, y);
List<Camera.Area> meteringAreas = new ArrayList<>();
meteringAreas.add(new Camera.Area(rect, FOCUS_METERING_AREA_WEIGHT_DEFAULT));
if (parameters.getMaxNumFocusAreas() != 0 && focusMode != null &&
(focusMode.equals(Camera.Parameters.FOCUS_MODE_AUTO) ||
focusMode.equals(Camera.Parameters.FOCUS_MODE_MACRO) ||
focusMode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) ||
focusMode.equals(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO))
) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
parameters.setFocusAreas(meteringAreas);
if (parameters.getMaxNumMeteringAreas() > 0) {
parameters.setMeteringAreas(meteringAreas);
}
if (!parameters.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
return; //cannot autoFocus
}
mCamera.setParameters(parameters);
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
resetFocus(success, camera);
}
});
} else if (parameters.getMaxNumMeteringAreas() > 0) {
if (!parameters.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
return; //cannot autoFocus
}
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
parameters.setFocusAreas(meteringAreas);
parameters.setMeteringAreas(meteringAreas);
mCamera.setParameters(parameters);
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
resetFocus(success, camera);
}
});
} else {
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
mCamera.cancelAutoFocus();
}
});
}
}
}
private void resetFocus(final boolean success, final Camera camera) {
mHandler.removeCallbacksAndMessages(null);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mCamera != null) {
mCamera.cancelAutoFocus();
Camera.Parameters parameters = mCamera.getParameters();
if (parameters == null) return;
if (parameters.getFocusMode() != Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) {
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
parameters.setFocusAreas(null);
parameters.setMeteringAreas(null);
mCamera.setParameters(parameters);
}
mCamera.cancelAutoFocus();
}
}
}, DELAY_MILLIS_BEFORE_RESETTING_FOCUS);
}
private Rect calculateFocusArea(float x, float y) {
int padding = FOCUS_AREA_SIZE_DEFAULT / 2;
int centerX = (int) (x * 2000);
int centerY = (int) (y * 2000);
int left = centerX - padding;
int top = centerY - padding;
int right = centerX + padding;
int bottom = centerY + padding;
if (left < 0) left = 0;
if (right > 2000) right = 2000;
if (top < 0) top = 0;
if (bottom > 2000) bottom = 2000;
return new Rect(left - 1000, top - 1000, right - 1000, bottom - 1000);
}
/**
* Calculate display orientation
* https://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)

View File

@ -26,9 +26,12 @@ import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.media.Image;
@ -74,6 +77,10 @@ class Camera2 extends CameraViewImpl implements MediaRecorder.OnInfoListener, Me
*/
private static final int MAX_PREVIEW_HEIGHT = 1080;
private static final int FOCUS_AREA_SIZE_DEFAULT = 300;
private static final int FOCUS_METERING_AREA_WEIGHT_DEFAULT = 1000;
private final CameraManager mCameraManager;
private final CameraDevice.StateCallback mCameraDeviceCallback
@ -1036,6 +1043,87 @@ class Camera2 extends CameraViewImpl implements MediaRecorder.OnInfoListener, Me
}
}
/**
* Auto focus on input coordinates
*/
// Much credit - https://gist.github.com/royshil/8c760c2485257c85a11cafd958548482
void setFocusArea(float x, float y) {
CameraCaptureSession.CaptureCallback captureCallbackHandler = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
if (request.getTag() == "FOCUS_TAG") {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, null);
try {
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to manual focus.", e);
}
}
}
@Override
public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
super.onCaptureFailed(session, request, failure);
Log.e(TAG, "Manual AF failure: " + failure);
}
};
try {
mCaptureSession.stopRepeating();
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to manual focus.", e);
}
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
try {
mCaptureSession.capture(mPreviewRequestBuilder.build(), captureCallbackHandler, null);
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to manual focus.", e);
}
if (isMeteringAreaAFSupported()) {
MeteringRectangle focusAreaTouch = calculateFocusArea(x, y);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_REGIONS, new MeteringRectangle[]{focusAreaTouch});
}
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);
mPreviewRequestBuilder.setTag("FOCUS_TAG");
try {
mCaptureSession.capture(mPreviewRequestBuilder.build(), captureCallbackHandler, null);
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to manual focus.", e);
}
}
private boolean isMeteringAreaAFSupported() {
return mCameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) >= 1;
}
private MeteringRectangle calculateFocusArea(float x, float y) {
final Rect sensorArraySize = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
// Current iOS spec has a requirement on sensor orientation that doesn't change, spec followed here.
final int xCoordinate = (int)(y * (float)sensorArraySize.height());
final int yCoordinate = (int)(x * (float)sensorArraySize.width());
final int halfTouchWidth = 150; //TODO: this doesn't represent actual touch size in pixel. Values range in [3, 10]...
final int halfTouchHeight = 150;
MeteringRectangle focusAreaTouch = new MeteringRectangle(Math.max(yCoordinate - halfTouchWidth, 0),
Math.max(xCoordinate - halfTouchHeight, 0),
halfTouchWidth * 2,
halfTouchHeight * 2,
MeteringRectangle.METERING_WEIGHT_MAX - 1);
return focusAreaTouch;
}
/**
* Captures a still picture.
*/

View File

@ -18,6 +18,8 @@ package com.google.android.cameraview;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.os.Build;
import android.os.Parcel;
@ -29,6 +31,7 @@ import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.graphics.SurfaceTexture;
@ -492,6 +495,16 @@ public class CameraView extends FrameLayout {
public int getCameraOrientation() {
return mImpl.getCameraOrientation();
}
/**
* Sets the auto focus point.
*
* @param x sets the x coordinate for camera auto focus
* @param y sets the y coordinate for camera auto focus
*/
public void setAutoFocusPointOfInterest(float x, float y) {
mImpl.setFocusArea(x, y);
}
public void setFocusDepth(float value) {
mImpl.setFocusDepth(value);

View File

@ -92,6 +92,8 @@ abstract class CameraViewImpl {
abstract void setDisplayOrientation(int displayOrientation);
abstract void setDeviceOrientation(int deviceOrientation);
abstract void setFocusArea(float x, float y);
abstract void setFocusDepth(float value);

View File

@ -2,6 +2,7 @@ package org.reactnative.camera;
import android.support.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
@ -97,6 +98,13 @@ public class CameraViewManager extends ViewGroupManager<RNCameraView> {
view.setFocusDepth(depth);
}
@ReactProp(name = "autoFocusPointOfInterest")
public void setAutoFocusPointOfInterest(RNCameraView view, ReadableMap coordinates) {
float x = (float) coordinates.getDouble("x");
float y = (float) coordinates.getDouble("y");
view.setAutoFocusPointOfInterest(x, y);
}
@ReactProp(name = "zoom")
public void setZoom(RNCameraView view, float zoom) {
view.setZoom(zoom);

View File

@ -179,7 +179,7 @@ Most cameras have a Auto Focus feature. It adjusts your camera lens position aut
Use the `autoFocus` property to specify the auto focus setting of your camera. `RNCamera.Constants.AutoFocus.on` turns it ON, `RNCamera.Constants.AutoFocus.off` turns it OFF.
#### `iOS` `autoFocusPointOfInterest`
#### `autoFocusPointOfInterest`
Values: Object `{ x: 0.5, y: 0.5 }`.
@ -187,6 +187,9 @@ Setting this property causes the auto focus feature of the camera to attempt to
Coordinates values are measured as floats from `0` to `1.0`. `{ x: 0, y: 0 }` will focus on the top left of the image, `{ x: 1, y: 1 }` will be the bottom right. Values are based on landscape mode with the home button on the right—this applies even if the device is in portrait mode.
Hint:
for portrait orientation, apply 90° clockwise rotation + translation: [Example](https://gist.github.com/Craigtut/6632a9ac7cfff55e74fb561862bc4edb)
#### `captureAudio`
Values: boolean `true` (default) | `false`

View File

@ -1,6 +1,14 @@
/* eslint-disable no-console */
import React from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Slider } from 'react-native';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Slider,
TouchableWithoutFeedback,
Dimensions,
} from 'react-native';
// eslint-disable-next-line import/no-unresolved
import { RNCamera } from 'react-native-camera';
@ -27,6 +35,13 @@ export default class CameraScreen extends React.Component {
flash: 'off',
zoom: 0,
autoFocus: 'on',
autoFocusPoint: {
normalized: { x: 0.5, y: 0.5 }, // normalized values required for autoFocusPointOfInterest
drawRectPosition: {
x: Dimensions.get('window').width * 0.5 - 32,
y: Dimensions.get('window').height * 0.5 - 32,
},
},
depth: 0,
type: 'back',
whiteBalance: 'auto',
@ -69,6 +84,28 @@ export default class CameraScreen extends React.Component {
});
}
touchToFocus(event) {
const { pageX, pageY } = event.nativeEvent;
const screenWidth = Dimensions.get('window').width;
const screenHeight = Dimensions.get('window').height;
const isPortrait = screenHeight > screenWidth;
let x = pageX / screenWidth;
let y = pageY / screenHeight;
// Coordinate transform for portrait. See autoFocusPointOfInterest in docs for more info
if (isPortrait) {
x = pageY / screenHeight;
y = -(pageX / screenWidth) + 1;
}
this.setState({
autoFocusPoint: {
normalized: { x, y },
drawRectPosition: { x: pageX, y: pageY },
},
});
}
zoomOut() {
this.setState({
zoom: this.state.zoom - 0.1 < 0 ? 0 : this.state.zoom - 0.1,
@ -236,6 +273,11 @@ export default class CameraScreen extends React.Component {
renderCamera() {
const { canDetectFaces, canDetectText, canDetectBarcode } = this.state;
const drawFocusRingPosition = {
top: this.state.autoFocusPoint.drawRectPosition.y - 32,
left: this.state.autoFocusPoint.drawRectPosition.x - 32,
};
return (
<RNCamera
ref={ref => {
@ -243,10 +285,12 @@ export default class CameraScreen extends React.Component {
}}
style={{
flex: 1,
justifyContent: 'space-between',
}}
type={this.state.type}
flashMode={this.state.flash}
autoFocus={this.state.autoFocus}
autoFocusPointOfInterest={this.state.autoFocusPoint.normalized}
zoom={this.state.zoom}
whiteBalance={this.state.whiteBalance}
ratio={this.state.ratio}
@ -262,9 +306,19 @@ export default class CameraScreen extends React.Component {
onTextRecognized={canDetectText ? this.textRecognized : null}
onGoogleVisionBarcodesDetected={canDetectBarcode ? this.barcodeRecognized : null}
>
<View style={StyleSheet.absoluteFill}>
<View style={[styles.autoFocusBox, drawFocusRingPosition]} />
<TouchableWithoutFeedback onPress={this.touchToFocus.bind(this)}>
<View style={{ flex: 1 }} />
</TouchableWithoutFeedback>
</View>
<View
style={{
flex: 0.5,
height: 72,
backgroundColor: 'transparent',
flexDirection: 'row',
justifyContent: 'space-around',
}}
>
<View
@ -308,82 +362,84 @@ export default class CameraScreen extends React.Component {
</TouchableOpacity>
</View>
</View>
<View
style={{
flex: 0.4,
backgroundColor: 'transparent',
flexDirection: 'row',
alignSelf: 'flex-end',
}}
>
<Slider
style={{ width: 150, marginTop: 15, alignSelf: 'flex-end' }}
onValueChange={this.setFocusDepth.bind(this)}
step={0.1}
disabled={this.state.autoFocus === 'on'}
/>
</View>
<View
style={{
flex: 0.1,
backgroundColor: 'transparent',
flexDirection: 'row',
alignSelf: 'flex-end',
}}
>
<TouchableOpacity
style={[
styles.flipButton,
{
flex: 0.3,
alignSelf: 'flex-end',
backgroundColor: this.state.isRecording ? 'white' : 'darkred',
},
]}
onPress={this.state.isRecording ? () => {} : this.takeVideo.bind(this)}
<View style={{ bottom: 0 }}>
<View
style={{
height: 20,
backgroundColor: 'transparent',
flexDirection: 'row',
alignSelf: 'flex-end',
}}
>
{this.state.isRecording ? (
<Text style={styles.flipText}> </Text>
) : (
<Text style={styles.flipText}> REC </Text>
)}
</TouchableOpacity>
</View>
{this.state.zoom !== 0 && (
<Text style={[styles.flipText, styles.zoomText]}>Zoom: {this.state.zoom}</Text>
)}
<View
style={{
flex: 0.1,
backgroundColor: 'transparent',
flexDirection: 'row',
alignSelf: 'flex-end',
}}
>
<TouchableOpacity
style={[styles.flipButton, { flex: 0.1, alignSelf: 'flex-end' }]}
onPress={this.zoomIn.bind(this)}
<Slider
style={{ width: 150, marginTop: 15, alignSelf: 'flex-end' }}
onValueChange={this.setFocusDepth.bind(this)}
step={0.1}
disabled={this.state.autoFocus === 'on'}
/>
</View>
<View
style={{
height: 56,
backgroundColor: 'transparent',
flexDirection: 'row',
alignSelf: 'flex-end',
}}
>
<Text style={styles.flipText}> + </Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.flipButton, { flex: 0.1, alignSelf: 'flex-end' }]}
onPress={this.zoomOut.bind(this)}
<TouchableOpacity
style={[
styles.flipButton,
{
flex: 0.3,
alignSelf: 'flex-end',
backgroundColor: this.state.isRecording ? 'white' : 'darkred',
},
]}
onPress={this.state.isRecording ? () => {} : this.takeVideo.bind(this)}
>
{this.state.isRecording ? (
<Text style={styles.flipText}> </Text>
) : (
<Text style={styles.flipText}> REC </Text>
)}
</TouchableOpacity>
</View>
{this.state.zoom !== 0 && (
<Text style={[styles.flipText, styles.zoomText]}>Zoom: {this.state.zoom}</Text>
)}
<View
style={{
height: 56,
backgroundColor: 'transparent',
flexDirection: 'row',
alignSelf: 'flex-end',
}}
>
<Text style={styles.flipText}> - </Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.flipButton, { flex: 0.25, alignSelf: 'flex-end' }]}
onPress={this.toggleFocus.bind(this)}
>
<Text style={styles.flipText}> AF : {this.state.autoFocus} </Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.flipButton, styles.picButton, { flex: 0.3, alignSelf: 'flex-end' }]}
onPress={this.takePicture.bind(this)}
>
<Text style={styles.flipText}> SNAP </Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.flipButton, { flex: 0.1, alignSelf: 'flex-end' }]}
onPress={this.zoomIn.bind(this)}
>
<Text style={styles.flipText}> + </Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.flipButton, { flex: 0.1, alignSelf: 'flex-end' }]}
onPress={this.zoomOut.bind(this)}
>
<Text style={styles.flipText}> - </Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.flipButton, { flex: 0.25, alignSelf: 'flex-end' }]}
onPress={this.toggleFocus.bind(this)}
>
<Text style={styles.flipText}> AF : {this.state.autoFocus} </Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.flipButton, styles.picButton, { flex: 0.3, alignSelf: 'flex-end' }]}
onPress={this.takePicture.bind(this)}
>
<Text style={styles.flipText}> SNAP </Text>
</TouchableOpacity>
</View>
</View>
{!!canDetectFaces && this.renderFaces()}
{!!canDetectFaces && this.renderLandmarks()}
@ -417,6 +473,15 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
autoFocusBox: {
position: 'absolute',
height: 64,
width: 64,
borderRadius: 12,
borderWidth: 2,
borderColor: 'white',
opacity: 0.4,
},
flipText: {
color: 'white',
fontSize: 15,