Real-time filters gestures (#537)

* Add FILTER_CONTROL_1 and FILTER_CONTROL_2 to control filters with gestures

* Improve TintFilter and DuotoneFilter

* Display current filter in DemoApp

* Fix potential bug

* Rename outputSize

* Fix tests
pull/541/head
Mattia Iavarone 5 years ago committed by GitHub
parent bf41489279
commit 95b1b2cdc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      README.md
  2. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java
  3. 85
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  4. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  5. 26
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  6. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java
  7. 18
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java
  8. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java
  9. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/Gesture.java
  10. 21
      cameraview/src/main/java/com/otaliastudios/cameraview/gesture/GestureAction.java
  11. 6
      cameraview/src/main/res/values/attrs.xml
  12. 4
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  13. 4
      demo/src/main/res/layout/activity_camera.xml
  14. 30
      docs/_posts/2018-12-20-gestures.md
  15. 12
      docs/_posts/2019-08-06-filters.md

@ -82,9 +82,9 @@ motivation boost to push the library forward.
app:cameraAudioBitRate="@integer/audio_bit_rate"
app:cameraGestureTap="none|autoFocus|takePicture"
app:cameraGestureLongTap="none|autoFocus|takePicture"
app:cameraGesturePinch="none|zoom|exposureCorrection"
app:cameraGestureScrollHorizontal="none|zoom|exposureCorrection"
app:cameraGestureScrollVertical="none|zoom|exposureCorrection"
app:cameraGesturePinch="none|zoom|exposureCorrection|filterControl1|filterControl2"
app:cameraGestureScrollHorizontal="none|zoom|exposureCorrection|filterControl1|filterControl2"
app:cameraGestureScrollVertical="none|zoom|exposureCorrection|filterControl1|filterControl2"
app:cameraEngine="camera1|camera2"
app:cameraPreview="glSurface|surface|texture"
app:cameraFacing="back|front"

@ -221,6 +221,8 @@ public class CameraOptions1Test extends BaseTest {
assertTrue(o.supports(GestureAction.TAKE_PICTURE));
assertTrue(o.supports(GestureAction.NONE));
assertTrue(o.supports(GestureAction.ZOOM));
assertTrue(o.supports(GestureAction.FILTER_CONTROL_1));
assertTrue(o.supports(GestureAction.FILTER_CONTROL_2));
assertFalse(o.supports(GestureAction.EXPOSURE_CORRECTION));
}

@ -25,6 +25,7 @@ import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.gesture.Gesture;
@ -355,6 +356,90 @@ public class CameraViewTest extends BaseTest {
assertTrue(mockController.mExposureCorrectionChanged);
}
@Test
public void testGestureAction_filterControl1() {
mockController.setMockEngineState(true);
mockController.setMockCameraOptions(mock(CameraOptions.class));
DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
filter.setParameter1(0F);
filter = spy(filter);
cameraView.setExperimental(true);
cameraView.setFilter(filter);
mockController.mExposureCorrectionChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
final FactorHolder factor = new FactorHolder();
uiSync(new Runnable() {
@Override
public void run() {
cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) {
@Override
protected boolean handleTouchEvent(@NonNull MotionEvent event) {
setGesture(Gesture.SCROLL_HORIZONTAL);
return true;
}
@Override
protected float getFactor() {
return factor.value;
}
};
cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_1);
}
});
// If factor is 0, we return the same value. The filter should not be notified.
factor.value = 0f;
cameraView.dispatchTouchEvent(event);
verify(filter, never()).setParameter1(anyFloat());
// For larger factors, the value is scaled. The filter should be notified.
factor.value = 1f;
cameraView.dispatchTouchEvent(event);
verify(filter, times(1)).setParameter1(anyFloat());
}
@Test
public void testGestureAction_filterControl2() {
mockController.setMockEngineState(true);
mockController.setMockCameraOptions(mock(CameraOptions.class));
DuotoneFilter filter = new DuotoneFilter(); // supports two parameters
filter.setParameter2(0F);
filter = spy(filter);
cameraView.setExperimental(true);
cameraView.setFilter(filter);
mockController.mExposureCorrectionChanged = false;
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
final FactorHolder factor = new FactorHolder();
uiSync(new Runnable() {
@Override
public void run() {
cameraView.mScrollGestureFinder = new ScrollGestureFinder(cameraView.mCameraCallbacks) {
@Override
protected boolean handleTouchEvent(@NonNull MotionEvent event) {
setGesture(Gesture.SCROLL_HORIZONTAL);
return true;
}
@Override
protected float getFactor() {
return factor.value;
}
};
cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_2);
}
});
// If factor is 0, we return the same value. The filter should not be notified.
factor.value = 0f;
cameraView.dispatchTouchEvent(event);
verify(filter, never()).setParameter2(anyFloat());
// For larger factors, the value is scaled. The filter should be notified.
factor.value = 1f;
cameraView.dispatchTouchEvent(event);
verify(filter, times(1)).setParameter2(anyFloat());
}
//endregion
//region testMeasure

@ -273,6 +273,8 @@ public class CameraOptions {
case AUTO_FOCUS:
return isAutoFocusSupported();
case TAKE_PICTURE:
case FILTER_CONTROL_1:
case FILTER_CONTROL_2:
case NONE:
return true;
case ZOOM:

@ -51,6 +51,8 @@ import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.FilterParser;
import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.filter.TwoParameterFilter;
import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.gesture.Gesture;
@ -627,6 +629,30 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mCameraEngine.setExposureCorrection(newValue, bounds, points, true);
}
break;
case FILTER_CONTROL_1:
if (!mExperimental) break;
if (getFilter() instanceof OneParameterFilter) {
OneParameterFilter filter = (OneParameterFilter) getFilter();
oldValue = filter.getParameter1();
newValue = source.computeValue(oldValue, 0, 1);
if (newValue != oldValue) {
filter.setParameter1(newValue);
}
}
break;
case FILTER_CONTROL_2:
if (!mExperimental) break;
if (getFilter() instanceof TwoParameterFilter) {
TwoParameterFilter filter = (TwoParameterFilter) getFilter();
oldValue = filter.getParameter2();
newValue = source.computeValue(oldValue, 0, 1);
if (newValue != oldValue) {
filter.setParameter2(newValue);
}
}
break;
}
}

@ -96,7 +96,8 @@ public abstract class BaseFilter implements Filter {
private int vertexTranformMatrixLocation = -1;
private int vertexPositionLocation = -1;
private int vertexTextureCoordinateLocation = -1;
private Size outputSize;
private int programHandle = -1;
private Size size;
@SuppressWarnings("WeakerAccess")
protected String vertexPositionName = DEFAULT_VERTEX_POSITION_NAME;
@ -127,6 +128,7 @@ public abstract class BaseFilter implements Filter {
@Override
public void onCreate(int programHandle) {
this.programHandle = programHandle;
vertexPositionLocation = GLES20.glGetAttribLocation(programHandle, vertexPositionName);
GlUtils.checkLocation(vertexPositionLocation, vertexPositionName);
vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle, vertexTextureCoordinateName);
@ -139,6 +141,7 @@ public abstract class BaseFilter implements Filter {
@Override
public void onDestroy() {
programHandle = -1;
vertexPositionLocation = -1;
vertexTextureCoordinateLocation = -1;
vertexModelViewProjectionMatrixLocation = -1;
@ -153,14 +156,18 @@ public abstract class BaseFilter implements Filter {
@Override
public void setSize(int width, int height) {
outputSize = new Size(width, height);
size = new Size(width, height);
}
@Override
public void draw(float[] transformMatrix) {
onPreDraw(transformMatrix);
onDraw();
onPostDraw();
if (programHandle == -1) {
LOG.w("Filter.draw() called after destroying the filter. This can happen rarely because of threading.");
} else {
onPreDraw(transformMatrix);
onDraw();
onPostDraw();
}
}
protected void onPreDraw(float[] transformMatrix) {
@ -203,7 +210,7 @@ public abstract class BaseFilter implements Filter {
@Override
public final BaseFilter copy() {
BaseFilter copy = onCopy();
copy.setSize(outputSize.getWidth(), outputSize.getHeight());
copy.setSize(size.getWidth(), size.getHeight());
if (this instanceof OneParameterFilter) {
((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1());
}

@ -55,7 +55,8 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
*/
@SuppressWarnings("WeakerAccess")
public void setFirstColor(@ColorInt int color) {
mFirstColor = color;
// Remove any alpha.
mFirstColor = Color.rgb(Color.red(color), Color.green(color), Color.blue(color));
}
/**
@ -66,7 +67,8 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
*/
@SuppressWarnings("WeakerAccess")
public void setSecondColor(@ColorInt int color) {
mSecondColor = color;
// Remove any alpha.
mSecondColor = Color.rgb(Color.red(color), Color.green(color), Color.blue(color));
}
/**
@ -96,23 +98,27 @@ public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
@Override
public void setParameter1(float value) {
// no easy way to transform 0...1 into a color.
setFirstColor((int) (value * Integer.MAX_VALUE));
setFirstColor((int) (value * 0xFFFFFF));
}
@Override
public float getParameter1() {
return (float) getFirstColor() / Integer.MAX_VALUE;
int color = getFirstColor();
color = Color.argb(0, Color.red(color), Color.green(color), Color.blue(color));
return (float) color / 0xFFFFFF;
}
@Override
public void setParameter2(float value) {
// no easy way to transform 0...1 into a color.
setSecondColor((int) (value * Integer.MAX_VALUE));
setSecondColor((int) (value * 0xFFFFFF));
}
@Override
public float getParameter2() {
return (float) getSecondColor() / Integer.MAX_VALUE;
int color = getSecondColor();
color = Color.argb(0, Color.red(color), Color.green(color), Color.blue(color));
return (float) color / 0xFFFFFF;
}
@NonNull

@ -42,7 +42,8 @@ public class TintFilter extends BaseFilter implements OneParameterFilter {
*/
@SuppressWarnings("WeakerAccess")
public void setTint(@ColorInt int color) {
this.tint = color;
// Remove any alpha.
this.tint = Color.rgb(Color.red(color), Color.green(color), Color.blue(color));
}
/**
@ -60,12 +61,14 @@ public class TintFilter extends BaseFilter implements OneParameterFilter {
@Override
public void setParameter1(float value) {
// no easy way to transform 0...1 into a color.
setTint((int) (value * Integer.MAX_VALUE));
setTint((int) (value * 0xFFFFFF));
}
@Override
public float getParameter1() {
return (float) getTint() / Integer.MAX_VALUE;
int color = getTint();
color = Color.argb(0, Color.red(color), Color.green(color), Color.blue(color));
return (float) color / 0xFFFFFF;
}
@NonNull

@ -22,6 +22,8 @@ public enum Gesture {
*
* - {@link GestureAction#ZOOM}
* - {@link GestureAction#EXPOSURE_CORRECTION}
* - {@link GestureAction#FILTER_CONTROL_1}
* - {@link GestureAction#FILTER_CONTROL_2}
* - {@link GestureAction#NONE}
*/
PINCH(GestureType.CONTINUOUS),
@ -52,6 +54,8 @@ public enum Gesture {
*
* - {@link GestureAction#ZOOM}
* - {@link GestureAction#EXPOSURE_CORRECTION}
* - {@link GestureAction#FILTER_CONTROL_1}
* - {@link GestureAction#FILTER_CONTROL_2}
* - {@link GestureAction#NONE}
*/
SCROLL_HORIZONTAL(GestureType.CONTINUOUS),
@ -62,6 +66,8 @@ public enum Gesture {
*
* - {@link GestureAction#ZOOM}
* - {@link GestureAction#EXPOSURE_CORRECTION}
* - {@link GestureAction#FILTER_CONTROL_1}
* - {@link GestureAction#FILTER_CONTROL_2}
* - {@link GestureAction#NONE}
*/
SCROLL_VERTICAL(GestureType.CONTINUOUS);

@ -60,8 +60,27 @@ public enum GestureAction {
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS);
EXPOSURE_CORRECTION(4, GestureType.CONTINUOUS),
/**
* Controls the first parameter of a real-time {@link com.otaliastudios.cameraview.filter.Filter},
* if it accepts one. This action can be mapped to continuous gestures:
*
* - {@link Gesture#PINCH}
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
FILTER_CONTROL_1(5, GestureType.CONTINUOUS),
/**
* Controls the second parameter of a real-time {@link com.otaliastudios.cameraview.filter.Filter},
* if it accepts one. This action can be mapped to continuous gestures:
*
* - {@link Gesture#PINCH}
* - {@link Gesture#SCROLL_HORIZONTAL}
* - {@link Gesture#SCROLL_VERTICAL}
*/
FILTER_CONTROL_2(6, GestureType.CONTINUOUS);
final static GestureAction DEFAULT_PINCH = NONE;
final static GestureAction DEFAULT_TAP = NONE;

@ -44,18 +44,24 @@
<enum name="none" value="0" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
<enum name="filterControl1" value="5" />
<enum name="filterControl2" value="6" />
</attr>
<attr name="cameraGestureScrollHorizontal" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
<enum name="filterControl1" value="5" />
<enum name="filterControl2" value="6" />
</attr>
<attr name="cameraGestureScrollVertical" format="enum">
<enum name="none" value="0" />
<enum name="zoom" value="3" />
<enum name="exposureCorrection" value="4" />
<enum name="filterControl1" value="5" />
<enum name="filterControl2" value="6" />
</attr>
<attr name="cameraEngine" format="enum">

@ -331,7 +331,9 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
} else {
mCurrentFilter = 0;
}
camera.setFilter(mAllFilters[mCurrentFilter].newInstance());
Filters filter = mAllFilters[mCurrentFilter];
camera.setFilter(filter.newInstance());
message(filter.toString(), false);
}
@Override

@ -26,8 +26,8 @@
app:cameraGestureTap="autoFocus"
app:cameraGestureLongTap="none"
app:cameraGesturePinch="zoom"
app:cameraGestureScrollHorizontal="exposureCorrection"
app:cameraGestureScrollVertical="none"
app:cameraGestureScrollHorizontal="filterControl1"
app:cameraGestureScrollVertical="exposureCorrection"
app:cameraMode="picture"
app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker">

@ -26,21 +26,35 @@ Simple as that. There are two things to be noted:
|Gesture|Description|Can be mapped to|
|-------------|-----------|----------------|
|`PINCH`|Pinch gesture, typically assigned to the zoom control.|`zoom` `exposureCorrection` `none`|
|`TAP`|Single tap gesture, typically assigned to the focus control.|`autoFocus` `takePicture` `none`|
|`LONG_TAP`|Long tap gesture.|`autoFocus` `takePicture` `none`|
|`SCROLL_HORIZONTAL`|Horizontal movement gesture.|`zoom` `exposureCorrection` `none`|
|`SCROLL_VERTICAL`|Vertical movement gesture.|`zoom` `exposureCorrection` `none`|
|`PINCH`|Pinch gesture, typically assigned to the zoom control.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`|
|`TAP`|Single tap gesture, typically assigned to the focus control.|`AUTO_FOCUS` `TAKE_PICTURE` `NONE`|
|`LONG_TAP`|Long tap gesture.|`AUTO_FOCUS` `TAKE_PICTURE` `NONE`|
|`SCROLL_HORIZONTAL`|Horizontal movement gesture.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`|
|`SCROLL_VERTICAL`|Vertical movement gesture.|`ZOOM` `EXPOSURE_CORRECTION` `FILTER_CONTROL_1` `FILTER_CONTROL_2` `NONE`|
### Gesture Actions
Looking at this from the other side:
|Gesture action|Description|Can be mapped to|
|--------------|-----------|----------------|
|`NONE`|Disables this gesture.|`TAP` `LONG_TAP` `PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
|`AUTO_FOCUS`|Launches an [auto-focus operation](controls.html#auto-focus) on the finger position.|`TAP` `LONG_TAP`|
|`TAKE_PICTURE`|Takes a picture using [takePicture](capturing-media.html).|`TAP` `LONG_TAP`|
|`ZOOM`|[Zooms](controls.html#zoom) in or out.|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
|`EXPOSURE_CORRECTION`|Controls the [exposure correction](controls.html#exposure-correction).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
|`FILTER_CONTROL_1`|Controls the first parameter (if any) of a [real-time filter](filters.html).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
|`FILTER_CONTROL_2`|Controls the second parameter (if any) of a [real-time filter](filters.html).|`PINCH` `SCROLL_HORIZONTAL` `SCROLL_VERTICAL`|
### XML Attributes
```xml
<com.otaliastudios.cameraview.CameraView
app:cameraGesturePinch="zoom|exposureCorrection|none"
app:cameraGesturePinch="zoom|exposureCorrection|filterControl1|filterControl2|none"
app:cameraGestureTap="autoFocus|takePicture|none"
app:cameraGestureLongTap="autoFocus|takePicture|none"
app:cameraGestureScrollHorizontal="zoom|exposureCorrection|none"
app:cameraGestureScrollVertical="zoom|exposureCorrection|none"/>
app:cameraGestureScrollHorizontal="zoom|exposureCorrection|filterControl1|filterControl2|none"
app:cameraGestureScrollVertical="zoom|exposureCorrection|filterControl1|filterControl2|none"/>
```
### Related APIs

@ -70,7 +70,9 @@ filter that was previously set and return back to normal.
|`TintFilter`|`Filters.TINT`|`@string/cameraview_filter_tint`|
|`VignetteFilter`|`Filters.VIGNETTE`|`@string/cameraview_filter_vignette`|
Most of these filters accept input parameters to tune them. For example, `DuotoneFilter` will
### Filters controls
Most of the provided filters accept input parameters to tune them. For example, `DuotoneFilter` will
accept two colors to apply the duotone effect.
```java
@ -81,6 +83,14 @@ duotoneFilter.setSecondColor(Color.GREEN);
You can change these values by acting on the filter object, before or after passing it to `CameraView`.
Whenever something is changed, the updated values will be visible immediately in the next frame.
You can also map the first or second filter control to a gesture (like horizontal or vertical scrolling),
as explained in [the gesture documentation](gestures.html):
```java
camera.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.FILTER_CONTROL_1);
camera.mapGesture(Gesture.SCROLL_VERTICAL, GestureAction.FILTER_CONTROL_2);
```
### Advanced usage
Advanced users with OpenGL experience can create their own filters by implementing the `Filter` interface

Loading…
Cancel
Save