Add SimpleFilter (#552)

* Add SimpleFilter, tests for all filter package

* Add docs
pull/562/head
Mattia Iavarone 5 years ago committed by GitHub
parent 0c04d6405c
commit 445b742455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseEglTest.java
  2. 177
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/BaseFilterTest.java
  3. 30
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FiltersTest.java
  4. 35
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/NoFilterTest.java
  5. 43
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/SimpleFilterTest.java
  6. 25
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/overlay/OverlayDrawerTest.java
  7. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java
  8. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java
  9. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/SimpleFilter.java
  10. 11
      docs/_posts/2019-08-06-filters.md

@ -0,0 +1,56 @@
package com.otaliastudios.cameraview;
import android.graphics.Canvas;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface;
import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.size.Size;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@SuppressWarnings("WeakerAccess")
public abstract class BaseEglTest extends BaseTest {
protected final static int WIDTH = 100;
protected final static int HEIGHT = 100;
protected EglCore eglCore;
protected EglBaseSurface eglSurface;
@Before
public void setUp() {
eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
eglSurface = new EglBaseSurface(eglCore);
eglSurface.createOffscreenSurface(WIDTH, HEIGHT);
eglSurface.makeCurrent();
}
@After
public void tearDown() {
eglSurface.releaseEglSurface();
eglSurface = null;
eglCore.release();
eglCore = null;
}
}

@ -0,0 +1,177 @@
package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.cameraview.internal.egl.EglViewport;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.HashSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BaseFilterTest extends BaseEglTest {
public static class TestFilter extends BaseFilter implements TwoParameterFilter {
private float param1;
private float param2;
@NonNull
@Override
public String getFragmentShader() {
return createDefaultFragmentShader();
}
@Override
public void setParameter1(float value) {
param1 = value;
}
@Override
public void setParameter2(float value) {
param2 = value;
}
@Override
public float getParameter1() {
return param1;
}
@Override
public float getParameter2() {
return param2;
}
}
private TestFilter filter;
@Test
public void testCreateDefaultFragmentShader() {
filter = new TestFilter();
filter.fragmentTextureCoordinateName = "XXX";
String defaultFragmentShader = filter.createDefaultFragmentShader();
assertNotNull(defaultFragmentShader);
assertTrue(defaultFragmentShader.contains(filter.fragmentTextureCoordinateName));
}
@Test
public void testCreateDefaultVertexShader() {
filter = new TestFilter();
filter.vertexModelViewProjectionMatrixName = "AAA";
filter.vertexPositionName = "BBB";
filter.vertexTextureCoordinateName = "CCC";
filter.vertexTransformMatrixName = "DDD";
filter.fragmentTextureCoordinateName = "EEE";
String defaultVertexShader = filter.createDefaultVertexShader();
assertNotNull(defaultVertexShader);
assertTrue(defaultVertexShader.contains(filter.vertexModelViewProjectionMatrixName));
assertTrue(defaultVertexShader.contains(filter.vertexPositionName));
assertTrue(defaultVertexShader.contains(filter.vertexTextureCoordinateName));
assertTrue(defaultVertexShader.contains(filter.vertexTransformMatrixName));
assertTrue(defaultVertexShader.contains(filter.fragmentTextureCoordinateName));
}
@Test
public void testOnProgramCreate() {
filter = new TestFilter();
int handle = GlUtils.createProgram(filter.getVertexShader(), filter.getFragmentShader());
filter.onCreate(handle);
assertTrue(filter.programHandle >= 0);
filter.onDestroy();
assertTrue(filter.programHandle < 0);
GLES20.glDeleteProgram(handle);
}
@Test
public void testDraw_whenInvalid() {
filter = spy(new TestFilter());
float[] matrix = new float[16];
filter.draw(matrix);
verify(filter, never()).onPreDraw(matrix);
verify(filter, never()).onDraw();
verify(filter, never()).onPostDraw();
}
@Test
public void testDraw() {
// Use an EglViewport which cares about GL setup.
filter = spy(new TestFilter());
EglViewport viewport = new EglViewport(filter);
int texture = viewport.createTexture();
float[] matrix = new float[16];
viewport.drawFrame(texture, matrix);
verify(filter, times(1)).onPreDraw(matrix);
verify(filter, times(1)).onDraw();
verify(filter, times(1)).onPostDraw();
viewport.release();
}
@Test(expected = RuntimeException.class)
public void testOnCopy_invalid() {
// Anonymous inner classes do not have a public constructor.
Filter filter = new BaseFilter() {
@NonNull
@Override
public String getFragmentShader() {
return "whatever";
}
};
filter.copy();
}
@Test
public void testOnCopy() {
filter = new TestFilter();
BaseFilter other = filter.copy();
assertTrue(other instanceof TestFilter);
}
@Test
public void testCopy_withSize() {
filter = new TestFilter();
filter.setSize(WIDTH, HEIGHT);
BaseFilter other = filter.copy();
assertEquals(WIDTH, other.size.getWidth());
assertEquals(HEIGHT, other.size.getHeight());
}
@Test
public void testCopy_withParameter1() {
filter = new TestFilter();
filter.setParameter1(0.5F);
TestFilter other = (TestFilter) filter.copy();
assertEquals(filter.getParameter1(), other.getParameter1(), 0.001F);
}
@Test
public void testCopy_withParameter2() {
filter = new TestFilter();
filter.setParameter2(0.5F);
TestFilter other = (TestFilter) filter.copy();
assertEquals(filter.getParameter2(), other.getParameter2(), 0.001F);
}
}

@ -0,0 +1,30 @@
package com.otaliastudios.cameraview.filter;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FiltersTest extends BaseTest {
@Test
public void testNewInstance() {
// At least tests that all our default filters have a no-args constructor.
Filters[] filtersArray = Filters.values();
for (Filters filters : filtersArray) {
Filter filter = filters.newInstance();
assertNotNull(filter);
}
}
}

@ -0,0 +1,35 @@
package com.otaliastudios.cameraview.filter;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NoFilterTest extends BaseTest {
public static class DummyFilter extends BaseFilter {
@NonNull
@Override
public String getFragmentShader() {
return "whatever";
}
}
@Test
public void testGetFragmentShader() {
NoFilter filter = new NoFilter();
String defaultFragmentShader = new DummyFilter().createDefaultFragmentShader();
assertEquals(defaultFragmentShader, filter.getFragmentShader());
}
}

@ -0,0 +1,43 @@
package com.otaliastudios.cameraview.filter;
import android.content.res.TypedArray;
import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.R;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.TestCase.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SimpleFilterTest extends BaseTest {
@Test
public void testGetFragmentShader() {
String shader = "shader";
SimpleFilter filter = new SimpleFilter(shader);
assertEquals(shader, filter.getFragmentShader());
}
@Test
public void testCopy() {
String shader = "shader";
SimpleFilter filter = new SimpleFilter(shader);
BaseFilter copy = filter.copy();
assertTrue(copy instanceof SimpleFilter);
SimpleFilter simpleCopy = (SimpleFilter) copy;
assertEquals(shader, simpleCopy.getFragmentShader());
}
}

@ -13,6 +13,7 @@ import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.otaliastudios.cameraview.BaseEglTest;
import com.otaliastudios.cameraview.BaseTest; import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.internal.egl.EglBaseSurface; import com.otaliastudios.cameraview.internal.egl.EglBaseSurface;
import com.otaliastudios.cameraview.internal.egl.EglCore; import com.otaliastudios.cameraview.internal.egl.EglCore;
@ -45,29 +46,7 @@ import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class OverlayDrawerTest extends BaseTest { public class OverlayDrawerTest extends BaseEglTest {
private final static int WIDTH = 100;
private final static int HEIGHT = 100;
private EglCore eglCore;
private EglBaseSurface eglSurface;
@Before
public void setUp() {
eglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
eglSurface = new EglBaseSurface(eglCore);
eglSurface.createOffscreenSurface(WIDTH, HEIGHT);
eglSurface.makeCurrent();
}
@After
public void tearDown() {
eglSurface.releaseEglSurface();
eglSurface = null;
eglCore.release();
eglCore = null;
}
@Test @Test
public void testDraw() { public void testDraw() {

@ -3,6 +3,7 @@ package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20; import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlUtils; import com.otaliastudios.cameraview.internal.GlUtils;
@ -96,8 +97,8 @@ public abstract class BaseFilter implements Filter {
private int vertexTranformMatrixLocation = -1; private int vertexTranformMatrixLocation = -1;
private int vertexPositionLocation = -1; private int vertexPositionLocation = -1;
private int vertexTextureCoordinateLocation = -1; private int vertexTextureCoordinateLocation = -1;
private int programHandle = -1; @VisibleForTesting int programHandle = -1;
private Size size; @VisibleForTesting Size size;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected String vertexPositionName = DEFAULT_VERTEX_POSITION_NAME; protected String vertexPositionName = DEFAULT_VERTEX_POSITION_NAME;
@ -120,7 +121,6 @@ public abstract class BaseFilter implements Filter {
fragmentTextureCoordinateName); fragmentTextureCoordinateName);
} }
@SuppressWarnings("WeakerAccess")
@NonNull @NonNull
protected String createDefaultFragmentShader() { protected String createDefaultFragmentShader() {
return createDefaultFragmentShader(fragmentTextureCoordinateName); return createDefaultFragmentShader(fragmentTextureCoordinateName);
@ -210,7 +210,9 @@ public abstract class BaseFilter implements Filter {
@Override @Override
public final BaseFilter copy() { public final BaseFilter copy() {
BaseFilter copy = onCopy(); BaseFilter copy = onCopy();
if (size != null) {
copy.setSize(size.getWidth(), size.getHeight()); copy.setSize(size.getWidth(), size.getHeight());
}
if (this instanceof OneParameterFilter) { if (this instanceof OneParameterFilter) {
((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1()); ((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1());
} }
@ -220,7 +222,6 @@ public abstract class BaseFilter implements Filter {
return copy; return copy;
} }
@SuppressWarnings("WeakerAccess")
protected BaseFilter onCopy() { protected BaseFilter onCopy() {
try { try {
return getClass().newInstance(); return getClass().newInstance();

@ -4,7 +4,10 @@ import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter; import com.otaliastudios.cameraview.filter.BaseFilter;
public class NoFilter extends BaseFilter { /**
* A {@link Filter} that draws frames without any modification.
*/
public final class NoFilter extends BaseFilter {
@NonNull @NonNull
@Override @Override

@ -0,0 +1,40 @@
package com.otaliastudios.cameraview.filter;
import androidx.annotation.NonNull;
/**
* The simplest possible filter that accepts a fragment shader in its constructor.
* This can be used when your fragment shader is static and has no 'runtime' parameters
* that influence its behavior.
*
* The given fragment shader should respect the default variable names, as listed
* in the {@link BaseFilter} class.
*
* NOTE: SimpleFilter is not meant to be subclassed!
* Subclassing it would require you to override {@link #onCopy()}, which would make
* this class useless. Instead, you can extend {@link BaseFilter} directly.
*/
public final class SimpleFilter extends BaseFilter {
private final String fragmentShader;
/**
* Creates a new filter with the given fragment shader.
* @param fragmentShader a fragment shader
*/
@SuppressWarnings("WeakerAccess")
public SimpleFilter(@NonNull String fragmentShader) {
this.fragmentShader = fragmentShader;
}
@NonNull
@Override
public String getFragmentShader() {
return fragmentShader;
}
@Override
protected BaseFilter onCopy() {
return new SimpleFilter(fragmentShader);
}
}

@ -96,6 +96,17 @@ camera.mapGesture(Gesture.SCROLL_VERTICAL, GestureAction.FILTER_CONTROL_2);
Advanced users with OpenGL experience can create their own filters by implementing the `Filter` interface Advanced users with OpenGL experience can create their own filters by implementing the `Filter` interface
and passing in a fragment shader and a vertex shader that will be used for drawing. and passing in a fragment shader and a vertex shader that will be used for drawing.
#### Simple filters
For very simple filters that have a static fragment shader, you can create a working filter
implementation by simply creating an instance of `SimpleFilter`:
```java
Filter filter = new SimpleFilter(myFragmentShader);
```
#### More complex filters
We recommend: We recommend:
- Subclassing `BaseFilter` instead of implementing `Filter`, since that takes care of most of the work - Subclassing `BaseFilter` instead of implementing `Filter`, since that takes care of most of the work

Loading…
Cancel
Save