Add startAutoFocus(Rect) (#724)
* Create MeteringRegion(s), MeteringTransform, add startAutoFocus(RectF) API * Improve docs * Changelog * Testspull/725/head
parent
9b916f12fd
commit
df139994d7
@ -0,0 +1,137 @@ |
|||||||
|
package com.otaliastudios.cameraview.metering; |
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.content.res.TypedArray; |
||||||
|
import android.graphics.PointF; |
||||||
|
import android.graphics.Rect; |
||||||
|
import android.graphics.RectF; |
||||||
|
import android.view.View; |
||||||
|
import android.view.ViewGroup; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.Nullable; |
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4; |
||||||
|
import androidx.test.filters.SmallTest; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.BaseTest; |
||||||
|
import com.otaliastudios.cameraview.R; |
||||||
|
import com.otaliastudios.cameraview.markers.AutoFocusMarker; |
||||||
|
import com.otaliastudios.cameraview.markers.AutoFocusTrigger; |
||||||
|
import com.otaliastudios.cameraview.markers.MarkerParser; |
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
import org.mockito.invocation.InvocationOnMock; |
||||||
|
import org.mockito.stubbing.Answer; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import static junit.framework.TestCase.assertNotNull; |
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertNull; |
||||||
|
import static org.junit.Assert.assertTrue; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.Mockito.atLeastOnce; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.times; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class) |
||||||
|
@SmallTest |
||||||
|
public class MeteringRegionsTest extends BaseTest { |
||||||
|
|
||||||
|
private final Size bounds = new Size(1000, 1000); |
||||||
|
|
||||||
|
private void checkRegion(@NonNull MeteringRegion region, @NonNull PointF center, int weight) { |
||||||
|
assertEquals(center.x, region.mRegion.centerX(), 0.01F); |
||||||
|
assertEquals(center.y, region.mRegion.centerY(), 0.01F); |
||||||
|
assertEquals(weight, region.mWeight); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testFromPoint() { |
||||||
|
PointF center = new PointF(500, 500); |
||||||
|
MeteringRegions regions = MeteringRegions.fromPoint(bounds, center); |
||||||
|
assertEquals(2, regions.mRegions.size()); |
||||||
|
MeteringRegion first = regions.mRegions.get(0); |
||||||
|
MeteringRegion second = regions.mRegions.get(1); |
||||||
|
checkRegion(first, center, MeteringRegion.MAX_WEIGHT); |
||||||
|
checkRegion(second, center, |
||||||
|
Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testFromArea() { |
||||||
|
RectF area = new RectF(400, 400, 600, 600); |
||||||
|
MeteringRegions regions = MeteringRegions.fromArea(bounds, area); |
||||||
|
assertEquals(1, regions.mRegions.size()); |
||||||
|
MeteringRegion region = regions.mRegions.get(0); |
||||||
|
checkRegion(region, new PointF(area.centerX(), area.centerY()), MeteringRegion.MAX_WEIGHT); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testFromArea_withBlur() { |
||||||
|
RectF area = new RectF(400, 400, 600, 600); |
||||||
|
MeteringRegions regions = MeteringRegions.fromArea(bounds, area, |
||||||
|
MeteringRegion.MAX_WEIGHT, true); |
||||||
|
assertEquals(2, regions.mRegions.size()); |
||||||
|
MeteringRegion first = regions.mRegions.get(0); |
||||||
|
MeteringRegion second = regions.mRegions.get(1); |
||||||
|
PointF center = new PointF(area.centerX(), area.centerY()); |
||||||
|
checkRegion(first, center, MeteringRegion.MAX_WEIGHT); |
||||||
|
checkRegion(second, center, |
||||||
|
Math.round(MeteringRegions.BLUR_FACTOR_WEIGHT * MeteringRegion.MAX_WEIGHT)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testTransform() { |
||||||
|
MeteringTransform transform = mock(MeteringTransform.class); |
||||||
|
when(transform.transformMeteringPoint(any(PointF.class))).then(new Answer<PointF>() { |
||||||
|
@Override |
||||||
|
public PointF answer(InvocationOnMock invocation) { |
||||||
|
PointF in = invocation.getArgument(0); |
||||||
|
// This will swap x and y coordinates
|
||||||
|
//noinspection SuspiciousNameCombination
|
||||||
|
return new PointF(in.y, in.x); |
||||||
|
} |
||||||
|
}); |
||||||
|
RectF area = new RectF(0, 0, 100, 500); // tall area
|
||||||
|
RectF expected = new RectF(0, 0, 500, 100); // wide area
|
||||||
|
MeteringRegions regions = MeteringRegions.fromArea(bounds, area); |
||||||
|
MeteringRegions transformed = regions.transform(transform); |
||||||
|
verify(transform, times(4)).transformMeteringPoint(any(PointF.class)); |
||||||
|
assertEquals(1, transformed.mRegions.size()); |
||||||
|
assertEquals(expected, transformed.mRegions.get(0).mRegion); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGet() { |
||||||
|
MeteringTransform<Integer> transform = new MeteringTransform<Integer>() { |
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public PointF transformMeteringPoint(@NonNull PointF point) { |
||||||
|
return point; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public Integer transformMeteringRegion(@NonNull RectF region, int weight) { |
||||||
|
return weight; |
||||||
|
} |
||||||
|
}; |
||||||
|
MeteringRegions regions = MeteringRegions.fromArea(bounds, |
||||||
|
new RectF(400, 400, 600, 600), |
||||||
|
900, |
||||||
|
true); |
||||||
|
assertEquals(2, regions.mRegions.size()); |
||||||
|
List<Integer> result = regions.get(1, transform); |
||||||
|
assertEquals(1, result.size()); |
||||||
|
assertEquals(900, (int) result.get(0)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
package com.otaliastudios.cameraview.engine.metering; |
||||||
|
|
||||||
|
import android.graphics.PointF; |
||||||
|
import android.graphics.Rect; |
||||||
|
import android.graphics.RectF; |
||||||
|
import android.hardware.Camera; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.CameraLogger; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Angles; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Axis; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Reference; |
||||||
|
import com.otaliastudios.cameraview.metering.MeteringTransform; |
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
public class Camera1MeteringTransform implements MeteringTransform<Camera.Area> { |
||||||
|
|
||||||
|
protected static final String TAG = Camera1MeteringTransform.class.getSimpleName(); |
||||||
|
protected static final CameraLogger LOG = CameraLogger.create(TAG); |
||||||
|
|
||||||
|
private final int displayToSensor; |
||||||
|
private final Size previewSize; |
||||||
|
|
||||||
|
public Camera1MeteringTransform(@NonNull Angles angles, @NonNull Size previewSize) { |
||||||
|
this.displayToSensor = -angles.offset(Reference.SENSOR, Reference.VIEW, Axis.ABSOLUTE); |
||||||
|
this.previewSize = previewSize; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public PointF transformMeteringPoint(@NonNull PointF point) { |
||||||
|
// First, rescale to the -1000 ... 1000 range.
|
||||||
|
PointF scaled = new PointF(); |
||||||
|
scaled.x = -1000F + (point.x / previewSize.getWidth()) * 2000F; |
||||||
|
scaled.y = -1000F + (point.y / previewSize.getHeight()) * 2000F; |
||||||
|
|
||||||
|
// Apply rotation to this point.
|
||||||
|
// https://academo.org/demos/rotation-about-point/
|
||||||
|
PointF rotated = new PointF(); |
||||||
|
double theta = ((double) displayToSensor) * Math.PI / 180; |
||||||
|
rotated.x = (float) (scaled.x * Math.cos(theta) - scaled.y * Math.sin(theta)); |
||||||
|
rotated.y = (float) (scaled.x * Math.sin(theta) + scaled.y * Math.cos(theta)); |
||||||
|
LOG.i("scaled:", scaled, "rotated:", rotated); |
||||||
|
return rotated; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public Camera.Area transformMeteringRegion(@NonNull RectF region, int weight) { |
||||||
|
Rect rect = new Rect(); |
||||||
|
region.round(rect); |
||||||
|
return new Camera.Area(rect, weight); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,195 @@ |
|||||||
|
package com.otaliastudios.cameraview.engine.metering; |
||||||
|
|
||||||
|
import android.graphics.PointF; |
||||||
|
import android.graphics.Rect; |
||||||
|
import android.graphics.RectF; |
||||||
|
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.RequiresApi; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.CameraLogger; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Angles; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Axis; |
||||||
|
import com.otaliastudios.cameraview.engine.offset.Reference; |
||||||
|
import com.otaliastudios.cameraview.metering.MeteringTransform; |
||||||
|
import com.otaliastudios.cameraview.size.AspectRatio; |
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) |
||||||
|
public class Camera2MeteringTransform implements MeteringTransform<MeteringRectangle> { |
||||||
|
|
||||||
|
protected static final String TAG = Camera2MeteringTransform.class.getSimpleName(); |
||||||
|
protected static final CameraLogger LOG = CameraLogger.create(TAG); |
||||||
|
|
||||||
|
private final Angles angles; |
||||||
|
private final Size previewSize; |
||||||
|
private final Size previewStreamSize; |
||||||
|
private final boolean previewIsCropping; |
||||||
|
private final CameraCharacteristics characteristics; |
||||||
|
private final CaptureRequest.Builder builder; |
||||||
|
|
||||||
|
public Camera2MeteringTransform(@NonNull Angles angles, |
||||||
|
@NonNull Size previewSize, |
||||||
|
@NonNull Size previewStreamSize, |
||||||
|
boolean previewIsCropping, |
||||||
|
@NonNull CameraCharacteristics characteristics, |
||||||
|
@NonNull CaptureRequest.Builder builder) { |
||||||
|
this.angles = angles; |
||||||
|
this.previewSize = previewSize; |
||||||
|
this.previewStreamSize = previewStreamSize; |
||||||
|
this.previewIsCropping = previewIsCropping; |
||||||
|
this.characteristics = characteristics; |
||||||
|
this.builder = builder; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public MeteringRectangle transformMeteringRegion(@NonNull RectF region, int weight) { |
||||||
|
Rect round = new Rect(); |
||||||
|
region.round(round); |
||||||
|
return new MeteringRectangle(round, weight); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public PointF transformMeteringPoint(@NonNull PointF point) { |
||||||
|
// 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 = previewSize; |
||||||
|
|
||||||
|
// 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); |
||||||
|
LOG.i("input:", point, "output (before clipping):", referencePoint); |
||||||
|
|
||||||
|
// 6. Probably not needed, but make sure we clip.
|
||||||
|
if (referencePoint.x < 0) referencePoint.x = 0; |
||||||
|
if (referencePoint.y < 0) referencePoint.y = 0; |
||||||
|
if (referencePoint.x > referenceSize.getWidth()) referencePoint.x = referenceSize.getWidth(); |
||||||
|
if (referencePoint.y > referenceSize.getHeight()) referencePoint.y = referenceSize.getHeight(); |
||||||
|
LOG.i("input:", point, "output (after clipping):", referencePoint); |
||||||
|
return referencePoint; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("UnnecessaryLocalVariable") |
||||||
|
@NonNull |
||||||
|
private Size applyPreviewCropping(@NonNull Size referenceSize, @NonNull PointF referencePoint) { |
||||||
|
Size previewStreamSize = this.previewStreamSize; |
||||||
|
Size previewSurfaceSize = referenceSize; |
||||||
|
int referenceWidth = previewSurfaceSize.getWidth(); |
||||||
|
int referenceHeight = previewSurfaceSize.getHeight(); |
||||||
|
AspectRatio previewStreamAspectRatio = AspectRatio.of(previewStreamSize); |
||||||
|
AspectRatio previewSurfaceAspectRatio = AspectRatio.of(previewSurfaceSize); |
||||||
|
if (previewIsCropping) { |
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
@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 = this.previewStreamSize; |
||||||
|
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 = angles.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 = builder.get(CaptureRequest.SCALER_CROP_REGION); |
||||||
|
// For now we don't care about x and y position. Rect should not be 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 = builder.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 = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); |
||||||
|
if (activeRect == null) { // Should never happen
|
||||||
|
activeRect = new Rect(0, 0, referenceSize.getWidth(), |
||||||
|
referenceSize.getHeight()); |
||||||
|
} |
||||||
|
return new Size(activeRect.width(), activeRect.height()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
package com.otaliastudios.cameraview.metering; |
||||||
|
|
||||||
|
import android.graphics.PointF; |
||||||
|
import android.graphics.RectF; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
class MeteringRegion implements Comparable<MeteringRegion> { |
||||||
|
|
||||||
|
final static int MAX_WEIGHT = 1000; |
||||||
|
|
||||||
|
final RectF mRegion; |
||||||
|
final int mWeight; |
||||||
|
|
||||||
|
MeteringRegion(@NonNull RectF region, int weight) { |
||||||
|
mRegion = region; |
||||||
|
mWeight = weight; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
MeteringRegion transform(@NonNull MeteringTransform transform) { |
||||||
|
RectF result = new RectF(Float.MAX_VALUE, Float.MAX_VALUE, |
||||||
|
-Float.MAX_VALUE, -Float.MAX_VALUE); |
||||||
|
PointF point = new PointF(); |
||||||
|
// top-left
|
||||||
|
point.set(mRegion.left, mRegion.top); |
||||||
|
point = transform.transformMeteringPoint(point); |
||||||
|
updateRect(result, point); |
||||||
|
// top-right
|
||||||
|
point.set(mRegion.right, mRegion.top); |
||||||
|
point = transform.transformMeteringPoint(point); |
||||||
|
updateRect(result, point); |
||||||
|
// bottom-right
|
||||||
|
point.set(mRegion.right, mRegion.bottom); |
||||||
|
point = transform.transformMeteringPoint(point); |
||||||
|
updateRect(result, point); |
||||||
|
// bottom-left
|
||||||
|
point.set(mRegion.left, mRegion.bottom); |
||||||
|
point = transform.transformMeteringPoint(point); |
||||||
|
updateRect(result, point); |
||||||
|
return new MeteringRegion(result, mWeight); |
||||||
|
} |
||||||
|
|
||||||
|
private void updateRect(@NonNull RectF rect, @NonNull PointF point) { |
||||||
|
rect.left = Math.min(rect.left, point.x); |
||||||
|
rect.top = Math.min(rect.top, point.y); |
||||||
|
rect.right = Math.max(rect.right, point.x); |
||||||
|
rect.bottom = Math.max(rect.bottom, point.y); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
MeteringRegion clip(@NonNull Size bounds) { |
||||||
|
return clip(new RectF(0, 0, bounds.getWidth(), bounds.getHeight())); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
@NonNull |
||||||
|
MeteringRegion clip(@NonNull RectF bounds) { |
||||||
|
RectF region = new RectF(); |
||||||
|
region.set( |
||||||
|
Math.max(bounds.left, mRegion.left), |
||||||
|
Math.max(bounds.top, mRegion.top), |
||||||
|
Math.min(bounds.right, mRegion.right), |
||||||
|
Math.min(bounds.bottom, mRegion.bottom) |
||||||
|
); |
||||||
|
return new MeteringRegion(region, mWeight); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int compareTo(@NonNull MeteringRegion o) { |
||||||
|
//noinspection UseCompareMethod
|
||||||
|
return -Integer.valueOf(mWeight).compareTo(o.mWeight); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
package com.otaliastudios.cameraview.metering; |
||||||
|
|
||||||
|
import android.graphics.PointF; |
||||||
|
import android.graphics.RectF; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.VisibleForTesting; |
||||||
|
|
||||||
|
import com.otaliastudios.cameraview.size.Size; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public class MeteringRegions { |
||||||
|
private final static float POINT_AREA = 0.05F; |
||||||
|
|
||||||
|
@VisibleForTesting |
||||||
|
final static float BLUR_FACTOR_WEIGHT = 0.1F; |
||||||
|
private final static float BLUR_FACTOR_SIZE = 1.5F; |
||||||
|
|
||||||
|
@VisibleForTesting |
||||||
|
final List<MeteringRegion> mRegions; |
||||||
|
|
||||||
|
private MeteringRegions(@NonNull List<MeteringRegion> regions) { |
||||||
|
mRegions = regions; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public MeteringRegions transform(@NonNull MeteringTransform transform) { |
||||||
|
List<MeteringRegion> regions = new ArrayList<>(); |
||||||
|
for (MeteringRegion region : mRegions) { |
||||||
|
regions.add(region.transform(transform)); |
||||||
|
} |
||||||
|
return new MeteringRegions(regions); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public <T> List<T> get(int atMost, @NonNull MeteringTransform<T> transform) { |
||||||
|
List<T> result = new ArrayList<>(); |
||||||
|
Collections.sort(mRegions); |
||||||
|
for (MeteringRegion region : mRegions) { |
||||||
|
result.add(transform.transformMeteringRegion(region.mRegion, region.mWeight)); |
||||||
|
} |
||||||
|
atMost = Math.min(atMost, result.size()); |
||||||
|
return result.subList(0, atMost); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public static MeteringRegions fromPoint(@NonNull Size bounds, |
||||||
|
@NonNull PointF point) { |
||||||
|
return fromPoint(bounds, point, MeteringRegion.MAX_WEIGHT); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public static MeteringRegions fromPoint(@NonNull Size bounds, |
||||||
|
@NonNull PointF point, |
||||||
|
int weight) { |
||||||
|
float width = POINT_AREA * bounds.getWidth(); |
||||||
|
float height = POINT_AREA * bounds.getHeight(); |
||||||
|
RectF rectF = expand(point, width, height); |
||||||
|
return fromArea(bounds, rectF, weight, true); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public static MeteringRegions fromArea(@NonNull Size bounds, |
||||||
|
@NonNull RectF area) { |
||||||
|
return fromArea(bounds, area, MeteringRegion.MAX_WEIGHT); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public static MeteringRegions fromArea(@NonNull Size bounds, |
||||||
|
@NonNull RectF area, |
||||||
|
int weight) { |
||||||
|
return fromArea(bounds, area, weight, false); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public static MeteringRegions fromArea(@NonNull Size bounds, |
||||||
|
@NonNull RectF area, |
||||||
|
int weight, |
||||||
|
boolean blur) { |
||||||
|
List<MeteringRegion> regions = new ArrayList<>(); |
||||||
|
final PointF center = new PointF(area.centerX(), area.centerY()); |
||||||
|
final float width = area.width(); |
||||||
|
final float height = area.height(); |
||||||
|
regions.add(new MeteringRegion(area, weight)); |
||||||
|
if (blur) { |
||||||
|
RectF background = expand(center, |
||||||
|
BLUR_FACTOR_SIZE * width, |
||||||
|
BLUR_FACTOR_SIZE * height); |
||||||
|
regions.add(new MeteringRegion(background, |
||||||
|
Math.round(BLUR_FACTOR_WEIGHT * weight))); |
||||||
|
} |
||||||
|
List<MeteringRegion> clipped = new ArrayList<>(); |
||||||
|
for (MeteringRegion region : regions) { |
||||||
|
clipped.add(region.clip(bounds)); |
||||||
|
} |
||||||
|
return new MeteringRegions(clipped); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
private static RectF expand(@NonNull PointF center, float width, float height) { |
||||||
|
return new RectF( |
||||||
|
center.x - width / 2F, |
||||||
|
center.y - height / 2F, |
||||||
|
center.x + width / 2F, |
||||||
|
center.y + height / 2F |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package com.otaliastudios.cameraview.metering; |
||||||
|
|
||||||
|
import android.graphics.PointF; |
||||||
|
import android.graphics.RectF; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
|
||||||
|
public interface MeteringTransform<T> { |
||||||
|
@NonNull |
||||||
|
PointF transformMeteringPoint(@NonNull PointF point); |
||||||
|
|
||||||
|
@NonNull |
||||||
|
T transformMeteringRegion(@NonNull RectF region, int weight); |
||||||
|
} |
Loading…
Reference in new issue