parent
03b0fb5971
commit
47b7cdd514
@ -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); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue