Add meter package

pull/580/head
Mattia Iavarone 6 years ago
parent 03b0fb5971
commit 47b7cdd514
  1. 1
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  2. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  3. 7
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/Actions.java
  4. 17
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/action/BaseAction.java
  5. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/lock/BaseLock.java
  6. 55
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseMeter.java
  7. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/BaseReset.java
  8. 141
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureMeter.java
  9. 50
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/ExposureReset.java
  10. 89
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusMeter.java
  11. 47
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/FocusReset.java
  12. 251
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterAction.java
  13. 34
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/MeterResetAction.java
  14. 86
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceMeter.java
  15. 38
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/meter/WhiteBalanceReset.java
  16. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoExposure.java
  17. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/metering/AutoFocus.java

@ -565,7 +565,6 @@ public class Camera2Engine extends CameraEngine implements ImageReader.OnImageAv
@Override @Override
protected Task<Void> onStopPreview() { protected Task<Void> onStopPreview() {
LOG.i("onStopPreview:", "About to clean up."); LOG.i("onStopPreview:", "About to clean up.");
// TODO clear actions?
if (mVideoRecorder != null) { if (mVideoRecorder != null) {
// This should synchronously call onVideoResult that will reset the repeating builder // This should synchronously call onVideoResult that will reset the repeating builder
// to the PREVIEW template. This is very important. // to the PREVIEW template. This is very important.

@ -227,6 +227,11 @@ public abstract class CameraEngine implements
mPreview.setSurfaceCallback(this); mPreview.setSurfaceCallback(this);
} }
@NonNull
public CameraPreview getPreview() {
return mPreview;
}
//region Error handling //region Error handling
/** /**

@ -109,7 +109,9 @@ public class Actions {
} }
private void increaseRunningAction() { private void increaseRunningAction() {
if (runningAction == actions.size() - 1) { boolean first = runningAction == -1;
boolean last = runningAction == actions.size() - 1;
if (last) {
// This was the last action. We're done. // This was the last action. We're done.
setState(STATE_COMPLETED); setState(STATE_COMPLETED);
} else { } else {
@ -123,6 +125,9 @@ public class Actions {
} }
} }
}); });
if (!first) {
actions.get(runningAction).onStart(getHolder());
}
} }
} }

@ -1,5 +1,6 @@
package com.otaliastudios.cameraview.engine.action; package com.otaliastudios.cameraview.engine.action;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult; import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.TotalCaptureResult;
@ -100,6 +101,22 @@ public abstract class BaseAction implements Action {
return holder; return holder;
} }
/**
* Reads a characteristic with a fallback.
* @param key key
* @param fallback fallback
* @param <T> key type
* @return value or fallback
*/
@SuppressWarnings("WeakerAccess")
@NonNull
protected <T> T readCharacteristic(@NonNull CameraCharacteristics.Key<T> key,
@NonNull T fallback) {
T value = holder.getCharacteristics(this).get(key);
return value == null ? fallback : value;
}
@Override @Override
public void addCallback(@NonNull ActionCallback callback) { public void addCallback(@NonNull ActionCallback callback) {
if (!callbacks.contains(callback)) { if (!callbacks.contains(callback)) {

@ -12,14 +12,6 @@ import com.otaliastudios.cameraview.engine.action.BaseAction;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class BaseLock extends BaseAction { public abstract class BaseLock extends BaseAction {
@SuppressWarnings("WeakerAccess")
@NonNull
protected <T> T readCharacteristic(@NonNull CameraCharacteristics.Key<T> key,
@NonNull T fallback) {
T value = getHolder().getCharacteristics(this).get(key);
return value == null ? fallback : value;
}
@Override @Override
protected final void onStart(@NonNull ActionHolder holder) { protected final void onStart(@NonNull ActionHolder holder) {
super.onStart(holder); super.onStart(holder);

@ -0,0 +1,55 @@
package com.otaliastudios.cameraview.engine.meter;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class BaseMeter extends BaseAction {
private final List<MeteringRectangle> areas;
private boolean isSuccessful;
private boolean skipIfPossible;
@SuppressWarnings("WeakerAccess")
protected BaseMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) {
this.areas = areas;
this.skipIfPossible = skipIfPossible;
}
@Override
protected final void onStart(@NonNull ActionHolder holder) {
super.onStart(holder);
boolean isSkipped = skipIfPossible && checkShouldSkip(holder);
boolean isSupported = checkIsSupported(holder);
if (isSupported && !isSkipped) {
onStarted(holder, areas);
} else {
setSuccessful(true);
setState(STATE_COMPLETED);
}
}
protected abstract void onStarted(@NonNull ActionHolder holder,
@NonNull List<MeteringRectangle> areas);
protected abstract boolean checkShouldSkip(@NonNull ActionHolder holder);
protected abstract boolean checkIsSupported(@NonNull ActionHolder holder);
@SuppressWarnings("WeakerAccess")
protected void setSuccessful(boolean successful) {
isSuccessful = successful;
}
public boolean isSuccessful() {
return isSuccessful;
}
}

@ -0,0 +1,40 @@
package com.otaliastudios.cameraview.engine.meter;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public abstract class BaseReset extends BaseAction {
private boolean resetArea;
@SuppressWarnings("WeakerAccess")
protected BaseReset(boolean resetArea) {
this.resetArea = resetArea;
}
@Override
protected final void onStart(@NonNull ActionHolder holder) {
super.onStart(holder);
MeteringRectangle area = null;
if (resetArea) {
Rect rect = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE, new Rect());
area = new MeteringRectangle(rect, MeteringRectangle.METERING_WEIGHT_DONT_CARE);
}
onStarted(holder, area);
}
protected abstract void onStarted(@NonNull ActionHolder holder,
@Nullable MeteringRectangle area);
}

@ -0,0 +1,141 @@
package com.otaliastudios.cameraview.engine.meter;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class ExposureMeter extends BaseMeter {
private static final String TAG = ExposureMeter.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
private static final int STATE_WAITING_PRECAPTURE = 0;
private static final int STATE_WAITING_PRECAPTURE_END = 1;
public ExposureMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) {
super(areas, skipIfPossible);
}
// TODO set trigger to null?
@Override
protected boolean checkIsSupported(@NonNull ActionHolder holder) {
// In our case, this means checking if we support the AE precapture trigger.
boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1)
!= CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
Integer aeMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AE_MODE);
boolean isAEOn = aeMode != null &&
(aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON
|| aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_ALWAYS_FLASH
|| aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH
|| aeMode == CameraCharacteristics.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
|| aeMode == 5 /* CameraCharacteristics.CONTROL_AE_MODE_ON_EXTERNAL_FLASH, API 28 */);
boolean result = isNotLegacy && isAEOn;
LOG.i("checkIsSupported:", result);
return result;
}
@Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer aeState = holder.getLastResult(this).get(CaptureResult.CONTROL_AE_STATE);
boolean result = aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED;
LOG.i("checkShouldSkip:", result);
return result;
}
@Override
protected void onStarted(@NonNull ActionHolder holder, @NonNull List<MeteringRectangle> areas) {
// Launch the precapture trigger.
holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
// Check the regions.
int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0);
if (!areas.isEmpty() && maxRegions > 0) {
int max = Math.min(maxRegions, areas.size());
holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_REGIONS,
areas.subList(0, max).toArray(new MeteringRectangle[]{}));
}
// Apply
holder.applyBuilder(this);
setState(STATE_WAITING_PRECAPTURE);
}
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
Integer aeTriggerState = result.get(CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER);
LOG.i("onCaptureCompleted:", "aeState:", aeState, "aeTriggerState:", aeTriggerState);
if (aeState == null) return;
if (getState() == STATE_WAITING_PRECAPTURE) {
switch (aeState) {
case CaptureResult.CONTROL_AE_STATE_PRECAPTURE: {
setState(STATE_WAITING_PRECAPTURE_END);
break;
}
case CaptureResult.CONTROL_AE_STATE_CONVERGED:
case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED: {
// PRECAPTURE is a transient state. Being here might mean that precapture run
// and was successful, OR that the trigger was not even received yet. To
// distinguish, check the trigger state.
if (aeTriggerState != null
&& aeTriggerState == CaptureResult.CONTROL_AE_PRECAPTURE_TRIGGER_START) {
setSuccessful(true);
setState(STATE_COMPLETED);
}
break;
}
case CaptureResult.CONTROL_AE_STATE_LOCKED: {
// There's nothing we can do, AE was locked, triggers are ignored.
setSuccessful(false);
setState(STATE_COMPLETED);
break;
}
case CaptureResult.CONTROL_AE_STATE_INACTIVE:
case CaptureResult.CONTROL_AE_STATE_SEARCHING: {
// Wait...
break;
}
}
}
if (getState() == STATE_WAITING_PRECAPTURE_END) {
switch (aeState) {
case CaptureResult.CONTROL_AE_STATE_CONVERGED:
case CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED: {
setSuccessful(true);
setState(STATE_COMPLETED);
break;
}
case CaptureResult.CONTROL_AE_STATE_LOCKED: {
// There's nothing we can do, AE was locked, triggers are ignored.
setSuccessful(false);
setState(STATE_COMPLETED);
break;
}
case CaptureResult.CONTROL_AE_STATE_PRECAPTURE:
case CaptureResult.CONTROL_AE_STATE_INACTIVE:
case CaptureResult.CONTROL_AE_STATE_SEARCHING: {
// Wait...
break;
}
}
}
}
}

@ -0,0 +1,50 @@
package com.otaliastudios.cameraview.engine.meter;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class ExposureReset extends BaseReset {
private static final String TAG = ExposureReset.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
public ExposureReset(boolean resetArea) {
super(resetArea);
}
@Override
protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) {
boolean changed = false;
int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0);
if (area != null && maxRegions > 0) {
holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_REGIONS,
new MeteringRectangle[]{area});
changed = true;
}
// NOTE: precapture might not be supported, in which case I think it will be ignored.
Integer trigger = holder.getBuilder(this).get(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER);
LOG.w("onStarted:", "current precapture trigger is", trigger);
if (trigger == null || trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) {
LOG.w("onStarted:", "canceling precapture.");
int newTrigger = Build.VERSION.SDK_INT >= 23
? CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
: CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE;
holder.getBuilder(this).set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, newTrigger);
changed = true;
}
if (changed) holder.applyBuilder(this);
setState(STATE_COMPLETED);
}
}

@ -0,0 +1,89 @@
package com.otaliastudios.cameraview.engine.meter;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class FocusMeter extends BaseMeter {
private static final String TAG = FocusMeter.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
public FocusMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) {
super(areas, skipIfPossible);
}
@Override
protected boolean checkIsSupported(@NonNull ActionHolder holder) {
// Exclude OFF and EDOF as per docs. These do no support the trigger.
Integer afMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AF_MODE);
boolean result = afMode != null &&
(afMode == CameraCharacteristics.CONTROL_AF_MODE_AUTO
|| afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE
|| afMode == CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_VIDEO
|| afMode == CameraCharacteristics.CONTROL_AF_MODE_MACRO);
LOG.i("checkIsSupported:", result);
return result;
}
@Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer afState = holder.getLastResult(this).get(CaptureResult.CONTROL_AF_STATE);
boolean result = afState != null &&
(afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED ||
afState == CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED);
LOG.i("checkShouldSkip:", result);
return result;
}
@Override
protected void onStarted(@NonNull ActionHolder holder, @NonNull List<MeteringRectangle> areas) {
holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_START);
int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0);
if (!areas.isEmpty() && maxRegions > 0) {
int max = Math.min(maxRegions, areas.size());
holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_REGIONS,
areas.subList(0, max).toArray(new MeteringRectangle[]{}));
}
holder.applyBuilder(this);
}
// TODO Set trigger to null?
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
LOG.i("onCaptureCompleted:", "afState:", afState);
if (afState == null) return;
switch (afState) {
case CaptureRequest.CONTROL_AF_STATE_FOCUSED_LOCKED: {
setSuccessful(true);
setState(STATE_COMPLETED);
break;
}
case CaptureRequest.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED: {
setSuccessful(false);
setState(STATE_COMPLETED);
break;
}
case CaptureRequest.CONTROL_AF_STATE_INACTIVE: break;
case CaptureRequest.CONTROL_AF_STATE_ACTIVE_SCAN: break;
default: break;
}
}
}

@ -0,0 +1,47 @@
package com.otaliastudios.cameraview.engine.meter;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class FocusReset extends BaseReset {
private static final String TAG = FocusReset.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
public FocusReset(boolean resetArea) {
super(resetArea);
}
@Override
protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) {
boolean changed = false;
int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AF, 0);
if (area != null && maxRegions > 0) {
holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_REGIONS,
new MeteringRectangle[]{area});
changed = true;
}
// NOTE: trigger might not be supported, in which case I think it will be ignored.
Integer trigger = holder.getBuilder(this).get(CaptureRequest.CONTROL_AF_TRIGGER);
LOG.w("onStarted:", "current focus trigger is", trigger);
if (trigger == null || trigger == CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) {
holder.getBuilder(this).set(CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
changed = true;
}
if (changed) holder.applyBuilder(this);
setState(STATE_COMPLETED);
}
}

@ -0,0 +1,251 @@
package com.otaliastudios.cameraview.engine.meter;
import android.graphics.PointF;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.ActionWrapper;
import com.otaliastudios.cameraview.engine.action.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class MeterAction extends ActionWrapper {
private List<BaseMeter> meters;
private BaseAction action;
private ActionHolder holder;
private final PointF point;
private final CameraEngine engine;
private final boolean skipIfPossible;
public MeterAction(@NonNull CameraEngine engine, @Nullable PointF point,
boolean skipIfPossible) {
this.point = point;
this.engine = engine;
this.skipIfPossible = skipIfPossible;
}
@NonNull
@Override
public BaseAction getAction() {
return action;
}
@Nullable
public PointF getPoint() {
return point;
}
public boolean isSuccessful() {
for (BaseMeter meter : meters) {
if (!meter.isSuccessful()) {
return false;
}
}
return true;
}
@Override
protected void onStart(@NonNull ActionHolder holder) {
initialize(holder);
super.onStart(holder);
}
private void initialize(@NonNull ActionHolder holder) {
this.holder = holder;
List<MeteringRectangle> areas = new ArrayList<>();
if (point != null) {
// 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.
final PointF referencePoint = new PointF(point.x, point.y);
Size referenceSize = engine.getPreview().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 = engine.getPreviewStreamSize(Reference.SENSOR);
//noinspection ConstantConditions
MeteringRectangle area1 = createMeteringRectangle(referenceSize, referencePoint,
visibleSize, 0.05F, 1000);
MeteringRectangle area2 = createMeteringRectangle(referenceSize, referencePoint,
visibleSize, 0.1F, 100);
areas.add(area1);
areas.add(area2);
}
BaseMeter ae = new ExposureMeter(areas, skipIfPossible);
BaseMeter af = new FocusMeter(areas, skipIfPossible);
BaseMeter awb = new WhiteBalanceMeter(areas, skipIfPossible);
meters = Arrays.asList(ae, af, awb);
action = Actions.together(ae, af, awb);
}
@SuppressWarnings("UnnecessaryLocalVariable")
@NonNull
private Size applyPreviewCropping(@NonNull Size referenceSize, @NonNull PointF referencePoint) {
Size previewStreamSize = engine.getPreviewStreamSize(Reference.VIEW);
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 (engine.getPreview().isCropping()) {
if (previewStreamAspectRatio.toFloat() > previewSurfaceAspectRatio.toFloat()) {
// Stream is larger. The x coordinate must be increased: a touch on the left side
// 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.y += previewSurfaceSize.getHeight() * (scale - 1F) / 2F;
referenceHeight = Math.round(previewSurfaceSize.getHeight() * scale);
}
}
return new Size(referenceWidth, referenceHeight);
}
@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 = engine.getPreviewStreamSize(Reference.VIEW);
referencePoint.x *= (float) previewStreamSize.getWidth() / referenceSize.getWidth();
referencePoint.y *= (float) previewStreamSize.getHeight() / referenceSize.getHeight();
return previewStreamSize;
}
@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 = engine.getAngles().offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE);
boolean flip = angle % 180 != 0;
float tempX = referencePoint.x;
float tempY = referencePoint.y;
if (angle == 0) {
referencePoint.x = tempX;
referencePoint.y = tempY;
} else if (angle == 90) {
referencePoint.x = tempY;
referencePoint.y = referenceSize.getWidth() - tempX;
} else if (angle == 180) {
referencePoint.x = referenceSize.getWidth() - tempX;
referencePoint.y = referenceSize.getHeight() - tempY;
} else if (angle == 270) {
referencePoint.x = referenceSize.getHeight() - tempY;
referencePoint.y = tempX;
} else {
throw new IllegalStateException("Unexpected angle " + angle);
}
return flip ? referenceSize.flip() : referenceSize;
}
@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
Rect cropRect = holder.getBuilder(this).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);
}
@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 = holder.getBuilder(this).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 = readCharacteristic(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE,
new Rect(0, 0, referenceSize.getWidth(), referenceSize.getHeight()));
return new Size(activeRect.width(), activeRect.height());
}
/**
* Creates a metering rectangle around the center point.
* The rectangle will have a size that's a factor of the visible width and height.
* The rectangle will also be constrained to be inside the given boundaries,
* so we don't exceed them in case the center point is exactly on one side for example.
* @return a new rectangle
*/
@NonNull
private MeteringRectangle createMeteringRectangle(
@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) rectangleLeft,
(int) rectangleTop,
(int) rectangleWidth,
(int) rectangleHeight,
weight
);
}
}

@ -0,0 +1,34 @@
package com.otaliastudios.cameraview.engine.meter;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.action.ActionWrapper;
import com.otaliastudios.cameraview.engine.action.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.engine.lock.ExposureLock;
import com.otaliastudios.cameraview.engine.lock.FocusLock;
import com.otaliastudios.cameraview.engine.lock.WhiteBalanceLock;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class MeterResetAction extends ActionWrapper {
private final BaseAction action;
public MeterResetAction(boolean resetAreas) {
this.action = Actions.together(
new ExposureReset(resetAreas),
new FocusReset(resetAreas),
new WhiteBalanceReset(resetAreas)
);
}
@NonNull
@Override
public BaseAction getAction() {
return action;
}
}

@ -0,0 +1,86 @@
package com.otaliastudios.cameraview.engine.meter;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import com.otaliastudios.cameraview.engine.metering.Parameter;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class WhiteBalanceMeter extends BaseMeter {
private static final String TAG = WhiteBalanceMeter.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
public WhiteBalanceMeter(@NonNull List<MeteringRectangle> areas, boolean skipIfPossible) {
super(areas, skipIfPossible);
}
@Override
protected boolean checkIsSupported(@NonNull ActionHolder holder) {
boolean isNotLegacy = readCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, -1)
!= CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
Integer awbMode = holder.getBuilder(this).get(CaptureRequest.CONTROL_AWB_MODE);
boolean result = isNotLegacy && awbMode != null && awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO;
LOG.i("checkIsSupported:", result);
return result;
}
@Override
protected boolean checkShouldSkip(@NonNull ActionHolder holder) {
Integer awbState = holder.getLastResult(this).get(CaptureResult.CONTROL_AWB_STATE);
boolean result = awbState != null && awbState == CaptureRequest.CONTROL_AWB_STATE_CONVERGED;
LOG.i("checkShouldSkip:", result);
return result;
}
@Override
protected void onStarted(@NonNull ActionHolder holder, @NonNull List<MeteringRectangle> areas) {
int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0);
if (!areas.isEmpty() && maxRegions > 0) {
int max = Math.min(maxRegions, areas.size());
holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_REGIONS,
areas.subList(0, max).toArray(new MeteringRectangle[]{}));
holder.applyBuilder(this);
}
}
@Override
public void onCaptureCompleted(@NonNull ActionHolder holder, @NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(holder, request, result);
Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE);
LOG.i("onCaptureCompleted:", "awbState:", awbState);
if (awbState == null) return;
switch (awbState) {
case CaptureRequest.CONTROL_AWB_STATE_CONVERGED: {
setSuccessful(true);
setState(STATE_COMPLETED);
break;
}
case CaptureRequest.CONTROL_AWB_STATE_LOCKED: {
// Nothing we can do if AWB was locked.
setSuccessful(false);
setState(STATE_COMPLETED);
break;
}
case CaptureRequest.CONTROL_AWB_STATE_INACTIVE:
case CaptureRequest.CONTROL_AWB_STATE_SEARCHING: {
// Wait...
break;
}
}
}
}

@ -0,0 +1,38 @@
package com.otaliastudios.cameraview.engine.meter;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.MeteringRectangle;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.action.ActionHolder;
import java.util.List;
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class WhiteBalanceReset extends BaseReset {
private static final String TAG = WhiteBalanceReset.class.getSimpleName();
private static final CameraLogger LOG = CameraLogger.create(TAG);
public WhiteBalanceReset(boolean resetArea) {
super(resetArea);
}
@Override
protected void onStarted(@NonNull ActionHolder holder, @Nullable MeteringRectangle area) {
int maxRegions = readCharacteristic(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0);
if (area != null && maxRegions > 0) {
holder.getBuilder(this).set(CaptureRequest.CONTROL_AWB_REGIONS, new MeteringRectangle[]{area});
holder.applyBuilder(this);
}
setState(STATE_COMPLETED);
}
}

@ -80,10 +80,6 @@ public class AutoExposure extends Parameter {
if (changed) { if (changed) {
notifyBuilderChanged(false); notifyBuilderChanged(false);
// Remove any problematic control for future requests
// NOTE: activating this invalidates the logic for early exit in processCapture
/* builder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); */
} }
} }

@ -71,9 +71,6 @@ public class AutoFocus extends Parameter {
if (changed) { if (changed) {
notifyBuilderChanged(false); notifyBuilderChanged(false);
// Remove any problematic control for future requests
/* builder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_IDLE); */
} }
} }

Loading…
Cancel
Save