Improve realtime filters (#535)

* Simplify Filters class

* Simplify filter switching in demo app

* Create FilterCameraPreview, improve GlCameraPreview

* Add comments

* Cleanup EglViewport

* Rename setPreviewingViewSize

* Create Filter interface and BaseFilter abstract class

* Move GL drawing code into BaseFilter

* Avoid releasing location pointers

* Add more docs and Filter.copy()

* Split two packages

* Remove filters package from code coverage computation

* Document all filters, implement onCopy, suppress warnings

* Add javadocs in Filters class

* Move NoFilter, add string resources

* XML support, require experimental flag

* Update first 6 filters with onPreDraw

* Update DuotoneFilter with onPreDraw

* Update FillLightFilter with onPreDraw

* Update Gamma, Grain, Grayscale, Hue, InvertColors, Lomoish with onPreDraw

* Update Posterize, Saturation, Sepia with onPreDraw

* Update all filters with onPreDraw

* Add OneParameterFilter and TwoParameterFilter

* Implement OneParameterFilter and TwoParameterFilter in all filters

* Improve comments

* Remove commented out code in demo

* Add FilterParser test

* Add GlCameraPreview and CameraView tests

* Add documentation

* Fix tests
pull/541/head
Mattia Iavarone 6 years ago committed by GitHub
parent facd26f11d
commit bf41489279
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 3
      cameraview/build.gradle
  3. 31
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  4. 16
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/CameraIntegrationTest.java
  5. 52
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/filter/FilterParserTest.java
  6. 6
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/CameraPreviewTest.java
  7. 21
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/GlCameraPreviewTest.java
  8. 15
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  9. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreviewTest.java
  10. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/TextureCameraPreviewTest.java
  11. 70
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  12. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/Preview.java
  13. 226
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/BaseFilter.java
  14. 91
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filter.java
  15. 32
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/FilterParser.java
  16. 121
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/Filters.java
  17. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/NoFilter.java
  18. 30
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/OneParameterFilter.java
  19. 31
      cameraview/src/main/java/com/otaliastudios/cameraview/filter/TwoParameterFilter.java
  20. 164
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/AutoFixFilter.java
  21. 37
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/BlackAndWhiteFilter.java
  22. 96
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/BrightnessFilter.java
  23. 97
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/ContrastFilter.java
  24. 72
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/CrossProcessFilter.java
  25. 27
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/CustomFilter.java
  26. 169
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DocumentaryFilter.java
  27. 171
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/DuotoneFilter.java
  28. 130
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/FillLightFilter.java
  29. 128
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filter.java
  30. 124
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/Filters.java
  31. 95
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GammaFilter.java
  32. 186
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrainFilter.java
  33. 29
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GrayscaleFilter.java
  34. 31
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/GreyScaleFilter.java
  35. 130
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/HueFilter.java
  36. 40
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/InvertColorsFilter.java
  37. 143
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/LamoishFilter.java
  38. 165
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/LomoishFilter.java
  39. 12
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/NoFilter.java
  40. 39
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/PosterizeFilter.java
  41. 174
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SaturationFilter.java
  42. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SepiaFilter.java
  43. 149
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/SharpnessFilter.java
  44. 115
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TemperatureFilter.java
  45. 119
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/TintFilter.java
  46. 201
      cameraview/src/main/java/com/otaliastudios/cameraview/filters/VignetteFilter.java
  47. 35
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/GlUtils.java
  48. 7
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglBaseSurface.java
  49. 171
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/egl/EglViewport.java
  50. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
  51. 39
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/FilterCameraPreview.java
  52. 48
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  53. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererFrameCallback.java
  54. 29
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  55. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
  56. 2
      cameraview/src/main/res/values/attrs.xml
  57. 23
      cameraview/src/main/res/values/strings.xml
  58. 89
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  59. 2
      docs/_posts/2018-12-20-debugging.md
  60. 2
      docs/_posts/2018-12-20-error-handling.md
  61. 2
      docs/_posts/2018-12-20-more-features.md
  62. 2
      docs/_posts/2018-12-20-previews.md
  63. 2
      docs/_posts/2018-12-20-runtime-permissions.md
  64. 102
      docs/_posts/2019-08-06-filters.md
  65. 1
      docs/index.md

@ -21,6 +21,7 @@ api 'com.otaliastudios:cameraview:2.0.0'
- Fast & reliable - Fast & reliable
- Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures.html) - Gestures support [[docs]](https://natario1.github.io/CameraView/docs/gestures.html)
- Real-time filters [[docs]](https://natario1.github.io/CameraView/docs/filters.html)
- Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews.html) - Camera1 or Camera2 powered engine [[docs]](https://natario1.github.io/CameraView/docs/previews.html)
- Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing.html) - Frame processing support [[docs]](https://natario1.github.io/CameraView/docs/frame-processing.html)
- Watermarks & animated overlays [[docs]](https://natario1.github.io/CameraView/docs/watermarks-and-overlays.html) - Watermarks & animated overlays [[docs]](https://natario1.github.io/CameraView/docs/watermarks-and-overlays.html)
@ -101,6 +102,7 @@ motivation boost to push the library forward.
app:cameraAutoFocusResetDelay="@integer/autofocus_delay" app:cameraAutoFocusResetDelay="@integer/autofocus_delay"
app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker" app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"
app:cameraUseDeviceOrientation="true|false" app:cameraUseDeviceOrientation="true|false"
app:cameraFilter="@string/real_time_filter"
app:cameraExperimental="false|true"> app:cameraExperimental="false|true">
<!-- Watermark! --> <!-- Watermark! -->

@ -245,6 +245,9 @@ task mergedCoverageReport(type: JacocoReport) {
// TODO these below could be testable ALSO outside of the integration tests // TODO these below could be testable ALSO outside of the integration tests
classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*') classFilter.add('**/com/otaliastudios/cameraview/video/encoding/**.*')
} }
// We don't test OpenGL filters.
classFilter.add('**/com/otaliastudios/cameraview/filters/**.*')
classDirectories = fileTree(dir: classDir, excludes: classFilter); classDirectories = fileTree(dir: classDir, excludes: classFilter);
reports.html.enabled = true reports.html.enabled = true

@ -22,6 +22,9 @@ import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash; import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.engine.CameraEngine; 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.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
@ -65,7 +68,7 @@ public class CameraViewTest extends BaseTest {
private CameraView cameraView; private CameraView cameraView;
private MockCameraEngine mockController; private MockCameraEngine mockController;
private CameraPreview mockPreview; private MockCameraPreview mockPreview;
private boolean hasPermissions; private boolean hasPermissions;
@Before @Before
@ -825,4 +828,30 @@ public class CameraViewTest extends BaseTest {
//endregion //endregion
// TODO: test permissions // TODO: test permissions
//region Filter
@Test(expected = RuntimeException.class)
public void testSetFilter_notExperimental() {
cameraView.setExperimental(false);
cameraView.setFilter(Filters.AUTO_FIX.newInstance());
}
@Test
public void testSetFilter_notExperimental_noFilter() {
cameraView.setExperimental(false);
cameraView.setFilter(Filters.NONE.newInstance());
// no exception thrown
}
@Test
public void testSetFilter() {
cameraView.setExperimental(true);
Filter filter = Filters.AUTO_FIX.newInstance();
cameraView.setFilter(filter);
verify(mockPreview, times(1)).setFilter(filter);
assertEquals(filter, cameraView.getFilter());
//noinspection ResultOfMethodCallIgnored
verify(mockPreview, times(1)).getCurrentFilter();
}
} }

@ -65,7 +65,6 @@ public abstract class CameraIntegrationTest extends BaseTest {
private final static CameraLogger LOG = CameraLogger.create(CameraIntegrationTest.class.getSimpleName()); private final static CameraLogger LOG = CameraLogger.create(CameraIntegrationTest.class.getSimpleName());
private final static long DELAY = 8000; private final static long DELAY = 8000;
private final static long VIDEO_DELAY = 16000;
@Rule @Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class); public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
@ -183,7 +182,20 @@ public abstract class CameraIntegrationTest extends BaseTest {
})); }));
// Wait for onVideoTaken and check. // Wait for onVideoTaken and check.
VideoResult result = video.await(VIDEO_DELAY); VideoResult result = video.await(DELAY);
// It seems that when running all the tests together, the device can go in some
// power saving mode which makes the CPU extremely slow. This is especially problematic
// with video snapshots where we do lots of processing. The videoEnd callback can return
// long after the actual stop() call, so if we're still processing, let's wait more.
if (expectSuccess && camera.isTakingVideo()) {
while (camera.isTakingVideo()) {
video.listen();
result = video.await(DELAY);
}
}
// Now we should be OK.
if (expectSuccess) { if (expectSuccess) {
assertEquals("Should call onVideoRecordingEnd", 0, onVideoRecordingEnd.getCount()); assertEquals("Should call onVideoRecordingEnd", 0, onVideoRecordingEnd.getCount());
assertNotNull("Should end video", result); assertNotNull("Should end video", result);

@ -0,0 +1,52 @@
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.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FilterParserTest extends BaseTest {
@Test
public void testFallback() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(false);
when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(null);
FilterParser parser = new FilterParser(array);
assertNotNull(parser.getFilter());
assertTrue(parser.getFilter() instanceof NoFilter);
}
@Test
public void testConstructor() {
TypedArray array = mock(TypedArray.class);
when(array.hasValue(R.styleable.CameraView_cameraFilter)).thenReturn(true);
when(array.getString(R.styleable.CameraView_cameraFilter)).thenReturn(MyFilter.class.getName());
FilterParser parser = new FilterParser(array);
assertNotNull(parser.getFilter());
assertTrue(parser.getFilter() instanceof MyFilter);
}
public static class MyFilter extends BaseFilter {
@NonNull
@Override
public String getFragmentShader() {
return createDefaultFragmentShader();
}
}
}

@ -23,16 +23,16 @@ import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public abstract class CameraPreviewTest extends BaseTest { public abstract class CameraPreviewTest<T extends CameraPreview> extends BaseTest {
private final static long DELAY = 4000; private final static long DELAY = 4000;
protected abstract CameraPreview createPreview(Context context, ViewGroup parent); protected abstract T createPreview(Context context, ViewGroup parent);
@Rule @Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class); public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
protected CameraPreview preview; protected T preview;
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
protected Size surfaceSize; protected Size surfaceSize;
private CameraPreview.SurfaceCallback callback; private CameraPreview.SurfaceCallback callback;

@ -7,24 +7,37 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filter.Filters;
import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class GlCameraPreviewTest extends CameraPreviewTest { public class GlCameraPreviewTest extends CameraPreviewTest<GlCameraPreview> {
@Override @Override
protected CameraPreview createPreview(Context context, ViewGroup parent) { protected GlCameraPreview createPreview(Context context, ViewGroup parent) {
return new GlCameraPreview(context, parent); return new GlCameraPreview(context, parent);
} }
@Override @Override
protected float getCropScaleY() { protected float getCropScaleY() {
return 1F / ((GlCameraPreview) preview).mCropScaleY; return 1F / preview.mCropScaleY;
} }
@Override @Override
protected float getCropScaleX() { protected float getCropScaleX() {
return 1F / ((GlCameraPreview) preview).mCropScaleX; return 1F / preview.mCropScaleX;
}
@Test
public void testSetFilter() {
Filter filter = Filters.BLACK_AND_WHITE.newInstance();
preview.setFilter(filter);
assertEquals(filter, preview.getCurrentFilter());
} }
} }

@ -7,15 +7,17 @@ import androidx.annotation.NonNull;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.CameraPreview;
public class MockCameraPreview extends CameraPreview<View, Void> { public class MockCameraPreview extends FilterCameraPreview<View, Void> {
public MockCameraPreview(Context context, ViewGroup parent) { public MockCameraPreview(Context context, ViewGroup parent) {
super(context, parent); super(context, parent);
} }
private View rootView; private View rootView;
private Filter filter;
@Override @Override
public boolean supportsCropping() { public boolean supportsCropping() {
@ -47,4 +49,15 @@ public class MockCameraPreview extends CameraPreview<View, Void> {
public View getRootView() { public View getRootView() {
return rootView; return rootView;
} }
@Override
public void setFilter(@NonNull Filter filter) {
this.filter = filter;
}
@NonNull
@Override
public Filter getCurrentFilter() {
return filter;
}
} }

@ -11,10 +11,10 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class SurfaceCameraPreviewTest extends CameraPreviewTest { public class SurfaceCameraPreviewTest extends CameraPreviewTest<SurfaceCameraPreview> {
@Override @Override
protected CameraPreview createPreview(Context context, ViewGroup parent) { protected SurfaceCameraPreview createPreview(Context context, ViewGroup parent) {
return new SurfaceCameraPreview(context, parent); return new SurfaceCameraPreview(context, parent);
} }

@ -11,10 +11,10 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
public class TextureCameraPreviewTest extends CameraPreviewTest { public class TextureCameraPreviewTest extends CameraPreviewTest<TextureCameraPreview> {
@Override @Override
protected CameraPreview createPreview(Context context, ViewGroup parent) { protected TextureCameraPreview createPreview(Context context, ViewGroup parent) {
return new TextureCameraPreview(context, parent); return new TextureCameraPreview(context, parent);
} }

@ -47,8 +47,10 @@ import com.otaliastudios.cameraview.engine.Camera1Engine;
import com.otaliastudios.cameraview.engine.Camera2Engine; import com.otaliastudios.cameraview.engine.Camera2Engine;
import com.otaliastudios.cameraview.engine.CameraEngine; import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference; import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.filters.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filters.Filters; import com.otaliastudios.cameraview.filter.FilterParser;
import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.frame.FrameProcessor;
import com.otaliastudios.cameraview.gesture.Gesture; import com.otaliastudios.cameraview.gesture.Gesture;
@ -68,6 +70,7 @@ import com.otaliastudios.cameraview.markers.MarkerLayout;
import com.otaliastudios.cameraview.markers.MarkerParser; import com.otaliastudios.cameraview.markers.MarkerParser;
import com.otaliastudios.cameraview.overlay.OverlayLayout; import com.otaliastudios.cameraview.overlay.OverlayLayout;
import com.otaliastudios.cameraview.preview.CameraPreview; import com.otaliastudios.cameraview.preview.CameraPreview;
import com.otaliastudios.cameraview.preview.FilterCameraPreview;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.SurfaceCameraPreview; import com.otaliastudios.cameraview.preview.SurfaceCameraPreview;
import com.otaliastudios.cameraview.preview.TextureCameraPreview; import com.otaliastudios.cameraview.preview.TextureCameraPreview;
@ -109,6 +112,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
private HashMap<Gesture, GestureAction> mGestureMap = new HashMap<>(4); private HashMap<Gesture, GestureAction> mGestureMap = new HashMap<>(4);
private Preview mPreview; private Preview mPreview;
private Engine mEngine; private Engine mEngine;
private Filter mPendingFilter;
// Components // Components
@VisibleForTesting CameraCallbacks mCameraCallbacks; @VisibleForTesting CameraCallbacks mCameraCallbacks;
@ -177,6 +181,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
SizeSelectorParser sizeSelectors = new SizeSelectorParser(a); SizeSelectorParser sizeSelectors = new SizeSelectorParser(a);
GestureParser gestures = new GestureParser(a); GestureParser gestures = new GestureParser(a);
MarkerParser markers = new MarkerParser(a); MarkerParser markers = new MarkerParser(a);
FilterParser filters = new FilterParser(a);
a.recycle(); a.recycle();
@ -234,6 +239,9 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
// Apply markers // Apply markers
setAutoFocusMarker(markers.getAutoFocusMarker()); setAutoFocusMarker(markers.getAutoFocusMarker());
// Apply filters
setFilter(filters.getFilter());
if (!isInEditMode()) { if (!isInEditMode()) {
mOrientationHelper = new OrientationHelper(context, mCameraCallbacks); mOrientationHelper = new OrientationHelper(context, mCameraCallbacks);
} }
@ -261,6 +269,10 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
mCameraPreview = instantiatePreview(mPreview, getContext(), this); mCameraPreview = instantiatePreview(mPreview, getContext(), this);
LOG.w("doInstantiateEngine:", "instantiated. preview:", mCameraPreview.getClass().getSimpleName()); LOG.w("doInstantiateEngine:", "instantiated. preview:", mCameraPreview.getClass().getSimpleName());
mCameraEngine.setPreview(mCameraPreview); mCameraEngine.setPreview(mCameraPreview);
if (mPendingFilter != null) {
setFilter(mPendingFilter);
mPendingFilter = null;
}
} }
@ -2132,18 +2144,58 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
//endregion //endregion
//region Effects //region Filters
public void setFilter(@NonNull Filters filter) { /**
setFilter(filter.newInstance()); * Applies a real-time filter to the camera preview, if it supports it.
* The only preview type that does so is currently {@link Preview#GL_SURFACE}.
*
* The filter will be applied to any picture snapshot taken with
* {@link #takePictureSnapshot()} and any video snapshot taken with
* {@link #takeVideoSnapshot(File)}.
*
* Use {@link NoFilter} to clear the existing filter,
* and take a look at the {@link Filters} class for commonly used filters.
*
* This method will throw an exception if the current preview does not support real-time filters.
* Make sure you use {@link Preview#GL_SURFACE} (the default).
*
* @see Filters
* @param filter a new filter
*/
public void setFilter(@NonNull Filter filter) {
if (mCameraPreview == null) {
mPendingFilter = filter;
} else if (!(filter instanceof NoFilter) && !mExperimental) {
throw new RuntimeException("Filters are an experimental features and need the experimental flag set.");
} else if (mCameraPreview instanceof FilterCameraPreview) {
((FilterCameraPreview) mCameraPreview).setFilter(filter);
} else {
throw new RuntimeException("Filters are only supported by the GL_SURFACE preview. Current:" + mPreview);
}
} }
public void setFilter(@NonNull Filter filter) { /**
if (mCameraPreview instanceof GlCameraPreview) { * Returns the current real-time filter applied to the camera preview.
((GlCameraPreview) mCameraPreview).setShaderEffect(filter); *
* This method will throw an exception if the current preview does not support real-time filters.
* Make sure you use {@link Preview#GL_SURFACE} (the default).
*
* @see #setFilter(Filter)
* @return the current filter
*/
@NonNull
public Filter getFilter() {
if (!mExperimental) {
throw new RuntimeException("Filters are an experimental features and need the experimental flag set.");
} else if (mCameraPreview == null) {
return mPendingFilter;
} else if (mCameraPreview instanceof FilterCameraPreview) {
return ((FilterCameraPreview) mCameraPreview).getCurrentFilter();
} else { } else {
LOG.w("setFilter", "setFilter is supported only for GLSurfaceView"); throw new RuntimeException("Filters are only supported by the GL_SURFACE preview. Current:" + mPreview);
} }
} }
//endregion //endregion

@ -29,7 +29,8 @@ public enum Preview implements Control {
/** /**
* Preview engine based on {@link android.opengl.GLSurfaceView}. * Preview engine based on {@link android.opengl.GLSurfaceView}.
* This is the best engine available. Supports video snapshots, * This is the best engine available. Supports video snapshots,
* and picture snapshots while taking videos. * supports picture snapshots while taking videos, supports
* watermarks and overlays, supports real-time filters.
*/ */
GL_SURFACE(2); GL_SURFACE(2);

@ -0,0 +1,226 @@
package com.otaliastudios.cameraview.filter;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlUtils;
import com.otaliastudios.cameraview.size.Size;
import java.nio.FloatBuffer;
/**
* A base implementation of {@link Filter} that just leaves the fragment shader to subclasses.
* See {@link NoFilter} for a non-abstract implementation.
*
* This class offers a default vertex shader implementation which in most cases is not required
* to be changed. Most effects can be rendered by simply changing the fragment shader, thus
* by overriding {@link #getFragmentShader()}.
*
* All {@link BaseFilter}s should have a no-op public constructor.
* This class will try to automatically implement {@link #copy()} thanks to this.
* If your filter implements public parameters, please implement {@link OneParameterFilter}
* and {@link TwoParameterFilter} to handle them and have them passed automatically to copies.
*
* NOTE - This class expects variable to have a certain name:
* - {@link #vertexPositionName}
* - {@link #vertexTransformMatrixName}
* - {@link #vertexModelViewProjectionMatrixName}
* - {@link #vertexTextureCoordinateName}
* - {@link #fragmentTextureCoordinateName}
* You can either change these variables, for example in your constructor, or change your
* vertex and fragment shader code to use them.
*
* NOTE - the {@link android.graphics.SurfaceTexture} restrictions apply:
* We only support the {@link android.opengl.GLES11Ext#GL_TEXTURE_EXTERNAL_OES} texture target
* and it must be specified in the fragment shader as a samplerExternalOES texture.
* You also have to explicitly require the extension: see {@link #createDefaultFragmentShader(String)}.
*
*/
public abstract class BaseFilter implements Filter {
private final static String TAG = BaseFilter.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
private final static String DEFAULT_VERTEX_POSITION_NAME = "aPosition";
private final static String DEFAULT_VERTEX_TEXTURE_COORDINATE_NAME = "aTextureCoord";
private final static String DEFAULT_VERTEX_MVP_MATRIX_NAME = "uMVPMatrix";
private final static String DEFAULT_VERTEX_TRANSFORM_MATRIX_NAME = "uTexMatrix";
private final static String DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME = "vTextureCoord";
@NonNull
private static String createDefaultVertexShader(@NonNull String vertexPositionName,
@NonNull String vertexTextureCoordinateName,
@NonNull String vertexModelViewProjectionMatrixName,
@NonNull String vertexTransformMatrixName,
@NonNull String fragmentTextureCoordinateName) {
return "uniform mat4 "+vertexModelViewProjectionMatrixName+";\n" +
"uniform mat4 "+vertexTransformMatrixName+";\n" +
"attribute vec4 "+vertexPositionName+";\n" +
"attribute vec4 "+vertexTextureCoordinateName+";\n" +
"varying vec2 "+fragmentTextureCoordinateName+";\n" +
"void main() {\n" +
" gl_Position = "+vertexModelViewProjectionMatrixName+" * "+vertexPositionName+";\n" +
" vTextureCoord = ("+vertexTransformMatrixName+" * "+vertexTextureCoordinateName+").xy;\n" +
"}\n";
}
@NonNull
private static String createDefaultFragmentShader(@NonNull String fragmentTextureCoordinateName) {
return "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 "+fragmentTextureCoordinateName+";\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "void main() {\n"
+ " gl_FragColor = texture2D(sTexture, "+fragmentTextureCoordinateName+");\n"
+ "}\n";
}
// When the model/view/projection matrix is identity, this will exactly cover the viewport.
private FloatBuffer vertexPosition = GlUtils.floatBuffer(new float[]{
-1.0f, -1.0f, // 0 bottom left
1.0f, -1.0f, // 1 bottom right
-1.0f, 1.0f, // 2 top left
1.0f, 1.0f, // 3 top right
});
private FloatBuffer textureCoordinates = GlUtils.floatBuffer(new float[]{
0.0f, 0.0f, // 0 bottom left
1.0f, 0.0f, // 1 bottom right
0.0f, 1.0f, // 2 top left
1.0f, 1.0f // 3 top right
});
private int vertexModelViewProjectionMatrixLocation = -1;
private int vertexTranformMatrixLocation = -1;
private int vertexPositionLocation = -1;
private int vertexTextureCoordinateLocation = -1;
private Size outputSize;
@SuppressWarnings("WeakerAccess")
protected String vertexPositionName = DEFAULT_VERTEX_POSITION_NAME;
@SuppressWarnings("WeakerAccess")
protected String vertexTextureCoordinateName = DEFAULT_VERTEX_TEXTURE_COORDINATE_NAME;
@SuppressWarnings("WeakerAccess")
protected String vertexModelViewProjectionMatrixName = DEFAULT_VERTEX_MVP_MATRIX_NAME;
@SuppressWarnings("WeakerAccess")
protected String vertexTransformMatrixName = DEFAULT_VERTEX_TRANSFORM_MATRIX_NAME;
@SuppressWarnings({"unused", "WeakerAccess"})
protected String fragmentTextureCoordinateName = DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME;
@SuppressWarnings("WeakerAccess")
@NonNull
protected String createDefaultVertexShader() {
return createDefaultVertexShader(vertexPositionName,
vertexTextureCoordinateName,
vertexModelViewProjectionMatrixName,
vertexTransformMatrixName,
fragmentTextureCoordinateName);
}
@SuppressWarnings("WeakerAccess")
@NonNull
protected String createDefaultFragmentShader() {
return createDefaultFragmentShader(fragmentTextureCoordinateName);
}
@Override
public void onCreate(int programHandle) {
vertexPositionLocation = GLES20.glGetAttribLocation(programHandle, vertexPositionName);
GlUtils.checkLocation(vertexPositionLocation, vertexPositionName);
vertexTextureCoordinateLocation = GLES20.glGetAttribLocation(programHandle, vertexTextureCoordinateName);
GlUtils.checkLocation(vertexTextureCoordinateLocation, vertexTextureCoordinateName);
vertexModelViewProjectionMatrixLocation = GLES20.glGetUniformLocation(programHandle, vertexModelViewProjectionMatrixName);
GlUtils.checkLocation(vertexModelViewProjectionMatrixLocation, vertexModelViewProjectionMatrixName);
vertexTranformMatrixLocation = GLES20.glGetUniformLocation(programHandle, vertexTransformMatrixName);
GlUtils.checkLocation(vertexTranformMatrixLocation, vertexTransformMatrixName);
}
@Override
public void onDestroy() {
vertexPositionLocation = -1;
vertexTextureCoordinateLocation = -1;
vertexModelViewProjectionMatrixLocation = -1;
vertexTranformMatrixLocation = -1;
}
@NonNull
@Override
public String getVertexShader() {
return createDefaultVertexShader();
}
@Override
public void setSize(int width, int height) {
outputSize = new Size(width, height);
}
@Override
public void draw(float[] transformMatrix) {
onPreDraw(transformMatrix);
onDraw();
onPostDraw();
}
protected void onPreDraw(float[] transformMatrix) {
// Copy the model / view / projection matrix over.
GLES20.glUniformMatrix4fv(vertexModelViewProjectionMatrixLocation, 1, false, GlUtils.IDENTITY_MATRIX, 0);
GlUtils.checkError("glUniformMatrix4fv");
// Copy the texture transformation matrix over.
GLES20.glUniformMatrix4fv(vertexTranformMatrixLocation, 1, false, transformMatrix, 0);
GlUtils.checkError("glUniformMatrix4fv");
// Enable the "aPosition" vertex attribute.
// Connect vertexBuffer to "aPosition".
GLES20.glEnableVertexAttribArray(vertexPositionLocation);
GlUtils.checkError("glEnableVertexAttribArray: " + vertexPositionLocation);
GLES20.glVertexAttribPointer(vertexPositionLocation, 2, GLES20.GL_FLOAT, false, 8, vertexPosition);
GlUtils.checkError("glVertexAttribPointer");
// Enable the "aTextureCoord" vertex attribute.
// Connect texBuffer to "aTextureCoord".
GLES20.glEnableVertexAttribArray(vertexTextureCoordinateLocation);
GlUtils.checkError("glEnableVertexAttribArray");
GLES20.glVertexAttribPointer(vertexTextureCoordinateLocation, 2, GLES20.GL_FLOAT, false, 8, textureCoordinates);
GlUtils.checkError("glVertexAttribPointer");
}
@SuppressWarnings("WeakerAccess")
protected void onDraw() {
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GlUtils.checkError("glDrawArrays");
}
@SuppressWarnings("WeakerAccess")
protected void onPostDraw() {
GLES20.glDisableVertexAttribArray(vertexPositionLocation);
GLES20.glDisableVertexAttribArray(vertexTextureCoordinateLocation);
}
@NonNull
@Override
public final BaseFilter copy() {
BaseFilter copy = onCopy();
copy.setSize(outputSize.getWidth(), outputSize.getHeight());
if (this instanceof OneParameterFilter) {
((OneParameterFilter) copy).setParameter1(((OneParameterFilter) this).getParameter1());
}
if (this instanceof TwoParameterFilter) {
((TwoParameterFilter) copy).setParameter2(((TwoParameterFilter) this).getParameter2());
}
return copy;
}
@SuppressWarnings("WeakerAccess")
protected BaseFilter onCopy() {
try {
return getClass().newInstance();
} catch (IllegalAccessException e) {
throw new RuntimeException("Filters should have a public no-op constructor.", e);
} catch (InstantiationException e) {
throw new RuntimeException("Filters should have a public no-op constructor.", e);
}
}
}

@ -0,0 +1,91 @@
package com.otaliastudios.cameraview.filter;
import com.otaliastudios.cameraview.CameraView;
import androidx.annotation.NonNull;
import java.io.File;
/**
* A Filter is a real time filter that operates onto the camera preview, plus any
* snapshot media taken with {@link CameraView#takePictureSnapshot()} and
* {@link CameraView#takeVideoSnapshot(File)}.
*
* You can apply filters to the camera engine using {@link CameraView#setFilter(Filter)}.
* The default filter is called {@link NoFilter} and can be used to restore the normal preview.
* A lof of other filters are collected in the {@link Filters} class.
*
* Advanced users can create custom filters using GLES.
* It is recommended to extend {@link BaseFilter} instead of this class.
*
* All {@link Filter}s should have a no-op public constructor.
* This ensures that you can pass the filter class to XML attribute {@code app:cameraFilter},
* and also helps {@link BaseFilter} automatically make a copy of the filter.
*
* Parameterized filters can implement {@link OneParameterFilter} and {@link TwoParameterFilter}
* to receive parameters in the 0F-1F range. This helps in making filter copies and also let us
* map the filter parameter to gestures.
*/
public interface Filter {
/**
* Returns a String containing the vertex shader.
* Together with {@link #getFragmentShader()}, this will be used to
* create the OpenGL program.
*
* @return vertex shader
*/
@NonNull
String getVertexShader();
/**
* Returns a String containing the fragment shader.
* Together with {@link #getVertexShader()}, this will be used to
* create the OpenGL program.
*
* @return fragment shader
*/
@NonNull
String getFragmentShader();
/**
* The filter program was just created. We pass in a handle to the OpenGL
* program that was created, so you can fetch pointers.
*
* @param programHandle handle
*/
void onCreate(int programHandle);
/**
* The filter program is about to be destroyed.
*
*/
void onDestroy();
/**
* Called to render the actual texture. The given transformation matrix
* should be applied.
*
* @param transformMatrix matrix
*/
void draw(float[] transformMatrix);
/**
* Called anytime the output size changes.
*
* @param width width
* @param height height
*/
void setSize(int width, int height);
/**
* Clones this filter creating a new instance of it.
* If it has any important parameters, these should be passed
* to the new instance.
*
* @return a clone
*/
@NonNull
Filter copy();
}

@ -0,0 +1,32 @@
package com.otaliastudios.cameraview.filter;
import android.content.res.TypedArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.otaliastudios.cameraview.R;
/**
* Parses filters from XML attributes.
*/
public class FilterParser {
private Filter filter = null;
public FilterParser(@NonNull TypedArray array) {
String filterName = array.getString(R.styleable.CameraView_cameraFilter);
try {
//noinspection ConstantConditions
Class<?> filterClass = Class.forName(filterName);
filter = (Filter) filterClass.newInstance();
} catch (Exception ignore) {
filter = new NoFilter();
}
}
@NonNull
public Filter getFilter() {
return filter;
}
}

@ -0,0 +1,121 @@
package com.otaliastudios.cameraview.filter;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filters.AutoFixFilter;
import com.otaliastudios.cameraview.filters.BlackAndWhiteFilter;
import com.otaliastudios.cameraview.filters.BrightnessFilter;
import com.otaliastudios.cameraview.filters.ContrastFilter;
import com.otaliastudios.cameraview.filters.CrossProcessFilter;
import com.otaliastudios.cameraview.filters.DocumentaryFilter;
import com.otaliastudios.cameraview.filters.DuotoneFilter;
import com.otaliastudios.cameraview.filters.FillLightFilter;
import com.otaliastudios.cameraview.filters.GammaFilter;
import com.otaliastudios.cameraview.filters.GrainFilter;
import com.otaliastudios.cameraview.filters.GrayscaleFilter;
import com.otaliastudios.cameraview.filters.HueFilter;
import com.otaliastudios.cameraview.filters.InvertColorsFilter;
import com.otaliastudios.cameraview.filters.LomoishFilter;
import com.otaliastudios.cameraview.filters.PosterizeFilter;
import com.otaliastudios.cameraview.filters.SaturationFilter;
import com.otaliastudios.cameraview.filters.SepiaFilter;
import com.otaliastudios.cameraview.filters.SharpnessFilter;
import com.otaliastudios.cameraview.filters.TemperatureFilter;
import com.otaliastudios.cameraview.filters.TintFilter;
import com.otaliastudios.cameraview.filters.VignetteFilter;
/**
* Contains commonly used {@link Filter}s.
*
* You can use {@link #newInstance()} to create a new instance and
* pass it to {@link com.otaliastudios.cameraview.CameraView#setFilter(Filter)}.
*/
public enum Filters {
/** @see NoFilter */
NONE(NoFilter.class),
/** @see AutoFixFilter */
AUTO_FIX(AutoFixFilter.class),
/** @see BlackAndWhiteFilter */
BLACK_AND_WHITE(BlackAndWhiteFilter.class),
/** @see BrightnessFilter */
BRIGHTNESS(BrightnessFilter.class),
/** @see ContrastFilter */
CONTRAST(ContrastFilter.class),
/** @see CrossProcessFilter */
CROSS_PROCESS(CrossProcessFilter.class),
/** @see DocumentaryFilter */
DOCUMENTARY(DocumentaryFilter.class),
/** @see DuotoneFilter */
DUOTONE(DuotoneFilter.class),
/** @see FillLightFilter */
FILL_LIGHT(FillLightFilter.class),
/** @see GammaFilter */
GAMMA(GammaFilter.class),
/** @see GrainFilter */
GRAIN(GrainFilter.class),
/** @see GrayscaleFilter */
GRAYSCALE(GrayscaleFilter.class),
/** @see HueFilter */
HUE(HueFilter.class),
/** @see InvertColorsFilter */
INVERT_COLORS(InvertColorsFilter.class),
/** @see LomoishFilter */
LOMOISH(LomoishFilter.class),
/** @see PosterizeFilter */
POSTERIZE(PosterizeFilter.class),
/** @see SaturationFilter */
SATURATION(SaturationFilter.class),
/** @see SepiaFilter */
SEPIA(SepiaFilter.class),
/** @see SharpnessFilter */
SHARPNESS(SharpnessFilter.class),
/** @see TemperatureFilter */
TEMPERATURE(TemperatureFilter.class),
/** @see TintFilter */
TINT(TintFilter.class),
/** @see VignetteFilter */
VIGNETTE(VignetteFilter.class);
private Class<? extends Filter> filterClass;
Filters(@NonNull Class<? extends Filter> filterClass) {
this.filterClass = filterClass;
}
/**
* Returns a new instance of the given filter.
* @return a new instance
*/
@NonNull
public Filter newInstance() {
try {
return filterClass.newInstance();
} catch (IllegalAccessException e) {
return new NoFilter();
} catch (InstantiationException e) {
return new NoFilter();
}
}
}

@ -0,0 +1,14 @@
package com.otaliastudios.cameraview.filter;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
public class NoFilter extends BaseFilter {
@NonNull
@Override
public String getFragmentShader() {
return createDefaultFragmentShader();
}
}

@ -0,0 +1,30 @@
package com.otaliastudios.cameraview.filter;
/**
* A special {@link Filter} that accepts a float parameter.
*
* The parameters will always be between 0F and 1F, so subclasses should
* map this range to their internal range if needed.
*
* A standardized range is useful for different applications. For example:
* - Filter parameters can be easily mapped to gestures since the range is fixed
* - {@link BaseFilter} can use this setters and getters to make a filter copy
*/
public interface OneParameterFilter extends Filter {
/**
* Sets the parameter.
* The value should always be between 0 and 1.
*
* @param value parameter
*/
void setParameter1(float value);
/**
* Returns the parameter.
* The returned value should always be between 0 and 1.
*
* @return parameter
*/
float getParameter1();
}

@ -0,0 +1,31 @@
package com.otaliastudios.cameraview.filter;
/**
* A special {@link Filter} that accepts two floats parameters.
* This is done by extending {@link OneParameterFilter}.
*
* The parameters will always be between 0F and 1F, so subclasses should
* map this range to their internal range if needed.
*
* A standardized range is useful for different applications. For example:
* - Filter parameters can be easily mapped to gestures since the range is fixed
* - {@link BaseFilter} can use this setters and getters to make a filter copy
*/
public interface TwoParameterFilter extends OneParameterFilter {
/**
* Sets the second parameter.
* The value should always be between 0 and 1.
*
* @param value parameter
*/
void setParameter2(float value);
/**
* Returns the second parameter.
* The returned value should always be between 0 and 1.
*
* @return parameter
*/
float getParameter2();
}

@ -1,96 +1,124 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Attempts to auto-fix the preview based on histogram equalization. * Attempts to auto-fix the frames based on histogram equalization.
*/ */
public class AutoFixFilter extends Filter { public class AutoFixFilter extends BaseFilter implements OneParameterFilter {
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES tex_sampler_0;\n"
+ "uniform samplerExternalOES tex_sampler_1;\n"
+ "uniform samplerExternalOES tex_sampler_2;\n"
+ "uniform float scale;\n"
+ "float shift_scale;\n"
+ "float hist_offset;\n"
+ "float hist_scale;\n"
+ "float density_offset;\n"
+ "float density_scale;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " shift_scale = " + (1.0f / 256f) + ";\n"
+ " hist_offset = " + (0.5f / 766f) + ";\n"
+ " hist_scale = " + (765f / 766f) + ";\n"
+ " density_offset = " + (0.5f / 1024f) + ";\n"
+ " density_scale = " + (1023f / 1024f) + ";\n"
+ " const vec3 weights = vec3(0.33333, 0.33333, 0.33333);\n"
+ " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n"
+ " float energy = dot(color.rgb, weights);\n"
+ " float mask_value = energy - 0.5;\n"
+ " float alpha;\n"
+ " if (mask_value > 0.0) {\n"
+ " alpha = (pow(2.0 * mask_value, 1.5) - 1.0) * scale + 1.0;\n"
+ " } else { \n"
+ " alpha = (pow(2.0 * mask_value, 2.0) - 1.0) * scale + 1.0;\n"
+ " }\n"
+ " float index = energy * hist_scale + hist_offset;\n"
+ " vec4 temp = texture2D(tex_sampler_1, vec2(index, 0.5));\n"
+ " float value = temp.g + temp.r * shift_scale;\n"
+ " index = value * density_scale + density_offset;\n"
+ " temp = texture2D(tex_sampler_2, vec2(index, 0.5));\n"
+ " value = temp.g + temp.r * shift_scale;\n"
+ " float dst_energy = energy * alpha + value * (1.0 - alpha);\n"
+ " float max_energy = energy / max(color.r, max(color.g, color.b));\n"
+ " if (dst_energy > max_energy) {\n"
+ " dst_energy = max_energy;\n"
+ " }\n"
+ " if (energy == 0.0) {\n"
+ " gl_FragColor = color;\n"
+ " } else {\n"
+ " gl_FragColor = vec4(color.rgb * dst_energy / energy, color.a);\n"
+ " }\n"
+ "}\n";
private float scale = 1.0f; private float scale = 1.0f;
private int scaleLocation = -1;
public AutoFixFilter() { }
/** /**
* Initialize Effect * A parameter between 0 and 1. Zero means no adjustment, while 1 indicates
* the maximum amount of adjustment.
*
* @param scale scale
*/ */
public AutoFixFilter() { public void setScale(float scale) {
if (scale < 0.0f) scale = 0.0f;
if (scale > 1.0f) scale = 1.0f;
this.scale = scale;
} }
/**
* Returns the current scale.
*
* @see #setScale(float)
* @return current scale
*/
public float getScale() { public float getScale() {
return scale; return scale;
} }
/** @Override
* @param scale Float, between 0 and 1. Zero means no adjustment, while 1 public void setParameter1(float value) {
* indicates the maximum amount of adjustment. setScale(value);
*/ }
public void setScale(float scale) {
if (scale < 0.0f)
scale = 0.0f;
else if (scale > 1.0f)
scale = 1.0f;
this.scale = scale; @Override
public float getParameter1() {
return getScale();
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
}
String shader = "#extension GL_OES_EGL_image_external : require\n" @Override
+ "precision mediump float;\n" public void onCreate(int programHandle) {
+ "uniform samplerExternalOES tex_sampler_0;\n" super.onCreate(programHandle);
+ "uniform samplerExternalOES tex_sampler_1;\n" scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
+ "uniform samplerExternalOES tex_sampler_2;\n" GlUtils.checkLocation(scaleLocation, "scale");
+ " float scale;\n" + " float shift_scale;\n" }
+ " float hist_offset;\n" + " float hist_scale;\n"
+ " float density_offset;\n" + " float density_scale;\n"
+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
+ " shift_scale = "
+ (1.0f / 256f)
+ ";\n"
+ " hist_offset = "
+ (0.5f / 766f)
+ ";\n"
+ " hist_scale = "
+ (765f / 766f)
+ ";\n"
+ " density_offset = "
+ (0.5f / 1024f)
+ ";\n"
+ " density_scale = "
+ (1023f / 1024f)
+ ";\n"
+ " scale = "
+ scale
+ ";\n"
+ " const vec3 weights = vec3(0.33333, 0.33333, 0.33333);\n"
+ " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n"
+ " float energy = dot(color.rgb, weights);\n"
+ " float mask_value = energy - 0.5;\n"
+ " float alpha;\n"
+ " if (mask_value > 0.0) {\n"
+ " alpha = (pow(2.0 * mask_value, 1.5) - 1.0) * scale + 1.0;\n"
+ " } else { \n"
+ " alpha = (pow(2.0 * mask_value, 2.0) - 1.0) * scale + 1.0;\n"
+ " }\n"
+ " float index = energy * hist_scale + hist_offset;\n"
+ " vec4 temp = texture2D(tex_sampler_1, vec2(index, 0.5));\n"
+ " float value = temp.g + temp.r * shift_scale;\n"
+ " index = value * density_scale + density_offset;\n"
+ " temp = texture2D(tex_sampler_2, vec2(index, 0.5));\n"
+ " value = temp.g + temp.r * shift_scale;\n"
+ " float dst_energy = energy * alpha + value * (1.0 - alpha);\n"
+ " float max_energy = energy / max(color.r, max(color.g, color.b));\n"
+ " if (dst_energy > max_energy) {\n"
+ " dst_energy = max_energy;\n"
+ " }\n"
+ " if (energy == 0.0) {\n"
+ " gl_FragColor = color;\n"
+ " } else {\n"
+ " gl_FragColor = vec4(color.rgb * dst_energy / energy, color.a);\n"
+ " }\n" + "}\n";
return shader; @Override
public void onDestroy() {
super.onDestroy();
scaleLocation = -1;
}
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
GLES20.glUniform1f(scaleLocation, scale);
GlUtils.checkError("glUniform1f");
} }
} }

@ -2,34 +2,29 @@ package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
/** /**
* Converts the preview into black and white colors * Converts the frames into black and white colors.
*/ */
public class BlackAndWhiteFilter extends Filter { public class BlackAndWhiteFilter extends BaseFilter {
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n" + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float colorR = (color.r + color.g + color.b) / 3.0;\n"
+ " float colorG = (color.r + color.g + color.b) / 3.0;\n"
+ " float colorB = (color.r + color.g + color.b) / 3.0;\n"
+ " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n"
+ "}\n";
/** public BlackAndWhiteFilter() { }
* Initialize effect
*/
public BlackAndWhiteFilter() {
}
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n" + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float colorR = (color.r + color.g + color.b) / 3.0;\n"
+ " float colorG = (color.r + color.g + color.b) / 3.0;\n"
+ " float colorB = (color.r + color.g + color.b) / 3.0;\n"
+ " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n"
+ "}\n";
return shader;
} }
} }

@ -1,56 +1,94 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Adjusts the brightness of the preview. * Adjusts the brightness of the frames.
*/ */
public class BrightnessFilter extends Filter { public class BrightnessFilter extends BaseFilter implements OneParameterFilter {
private float brightnessValue = 2.0f;
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float brightness;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " gl_FragColor = brightness * color;\n"
+ "}\n";
private float brightness = 2.0f; // 1.0F...2.0F
private int brightnessLocation = -1;
public BrightnessFilter() { }
/** /**
* Initialize Effect * Sets the brightness adjustment.
* 1.0: normal brightness.
* 2.0: high brightness.
*
* @param brightness brightness.
*/ */
public BrightnessFilter() { @SuppressWarnings({"WeakerAccess", "unused"})
public void setBrightness(float brightness) {
if (brightness < 1.0f) brightness = 1.0f;
if (brightness > 2.0f) brightness = 2.0f;
this.brightness = brightness;
} }
/** /**
* setBrightnessValue * Returns the current brightness.
* *
* @param brightnessvalue Range should be between 0.0- 1.0 with 0.0 being normal. * @see #setBrightness(float)
* @return brightness
*/ */
public void setBrightnessValue(float brightnessvalue) { @SuppressWarnings({"unused", "WeakerAccess"})
if (brightnessvalue < 0.0f) public float getBrightness() {
brightnessvalue = 0.0f; return brightness;
else if (brightnessvalue > 1.0f) }
brightnessvalue = 1.0f;
//since the shader excepts a range of 1.0 - 2.0 @Override
// will add the 1.0 to every value public void setParameter1(float value) {
this.brightnessValue = 1.0f + brightnessvalue; // parameter is 0...1, brightness is 1...2.
setBrightness(value + 1);
} }
public float getBrightnessValue() { @Override
//since the shader excepts a range of 1.0 - 2.0 public float getParameter1() {
//to keep it between 0.0f - 1.0f range, will subtract the 1.0 to every value // parameter is 0...1, brightness is 1...2.
return brightnessValue - 1.0f; return getBrightness() - 1F;
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
}
String shader = "#extension GL_OES_EGL_image_external : require\n" @Override
+ "precision mediump float;\n" public void onCreate(int programHandle) {
+ "uniform samplerExternalOES sTexture;\n" super.onCreate(programHandle);
+ "float brightness ;\n" + "varying vec2 vTextureCoord;\n" brightnessLocation = GLES20.glGetUniformLocation(programHandle, "brightness");
+ "void main() {\n" + " brightness =" + brightnessValue GlUtils.checkLocation(brightnessLocation, "brightness");
+ ";\n" }
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " gl_FragColor = brightness * color;\n" + "}\n";
return shader;
@Override
public void onDestroy() {
super.onDestroy();
brightnessLocation = -1;
} }
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
GLES20.glUniform1f(brightnessLocation, brightness);
GlUtils.checkError("glUniform1f");
}
} }

@ -1,55 +1,96 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Adjusts the contrast of the preview. * Adjusts the contrast.
*/ */
public class ContrastFilter extends Filter { public class ContrastFilter extends BaseFilter implements OneParameterFilter {
private float contrast = 2.0f;
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float contrast;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " color -= 0.5;\n"
+ " color *= contrast;\n"
+ " color += 0.5;\n"
+ " gl_FragColor = color;\n"
+ "}\n";
private float contrast = 2F;
private int contrastLocation = -1;
public ContrastFilter() { }
/** /**
* Initialize Effect * Sets the current contrast adjustment.
* 1.0: no adjustment
* 2.0: increased contrast
*
* @param contrast contrast
*/ */
public ContrastFilter() { @SuppressWarnings("WeakerAccess")
public void setContrast(float contrast) {
if (contrast < 1.0f) contrast = 1.0f;
if (contrast > 2.0f) contrast = 2.0f;
this.contrast = contrast;
} }
/** /**
* setContrast * Returns the current contrast.
* *
* @param contrast Range should be between 0.0- 1.0 with 0.0 being normal. * @see #setContrast(float)
* @return contrast
*/ */
public void setContrast(float contrast) { @SuppressWarnings({"unused", "WeakerAccess"})
if (contrast < 0.0f) public float getContrast() {
contrast = 0.0f; return contrast;
else if (contrast > 1.0f) }
contrast = 1.0f;
//since the shader excepts a range of 1.0 - 2.0 @Override
//will add the 1.0 to every value public void setParameter1(float value) {
this.contrast = contrast + 1.0f; // parameter is 0...1, contrast is 1...2.
setContrast(value + 1);
} }
public float getContrast() { @Override
//since the shader excepts a range of 1.0 - 2.0 public float getParameter1() {
//to keep it between 0.0f - 1.0f range, will subtract the 1.0 to every value // parameter is 0...1, contrast is 1...2.
return contrast - 1.0f; return getContrast() - 1F;
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
}
String shader = "#extension GL_OES_EGL_image_external : require\n" @Override
+ "precision mediump float;\n" public void onCreate(int programHandle) {
+ "uniform samplerExternalOES sTexture;\n" super.onCreate(programHandle);
+ " float contrast;\n" + "varying vec2 vTextureCoord;\n" contrastLocation = GLES20.glGetUniformLocation(programHandle, "contrast");
+ "void main() {\n" + " contrast =" + contrast + ";\n" GlUtils.checkLocation(contrastLocation, "contrast");
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n" }
+ " color -= 0.5;\n" + " color *= contrast;\n"
+ " color += 0.5;\n" + " gl_FragColor = color;\n" + "}\n";
return shader;
@Override
public void onDestroy() {
super.onDestroy();
contrastLocation = -1;
} }
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
GLES20.glUniform1f(contrastLocation, contrast);
GlUtils.checkError("glUniform1f");
}
} }

@ -2,43 +2,53 @@ package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
/** /**
* Applies a cross process effect on preview, in which the red and green channels * Applies a cross process effect, in which the red and green channels
* are enhanced while the blue channel is restricted. * are enhanced while the blue channel is restricted.
*/ */
public class CrossProcessFilter extends Filter { public class CrossProcessFilter extends BaseFilter {
/** private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
* Initialize Effect + "precision mediump float;\n"
*/ + "uniform samplerExternalOES sTexture;\n"
public CrossProcessFilter() { + "varying vec2 vTextureCoord;\n"
} + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 ncolor = vec3(0.0, 0.0, 0.0);\n"
+ " float value;\n"
+ " if (color.r < 0.5) {\n"
+ " value = color.r;\n"
+ " } else {\n"
+ " value = 1.0 - color.r;\n"
+ " }\n"
+ " float red = 4.0 * value * value * value;\n"
+ " if (color.r < 0.5) {\n"
+ " ncolor.r = red;\n"
+ " } else {\n"
+ " ncolor.r = 1.0 - red;\n"
+ " }\n"
+ " if (color.g < 0.5) {\n"
+ " value = color.g;\n"
+ " } else {\n"
+ " value = 1.0 - color.g;\n"
+ " }\n"
+ " float green = 2.0 * value * value;\n"
+ " if (color.g < 0.5) {\n"
+ " ncolor.g = green;\n"
+ " } else {\n"
+ " ncolor.g = 1.0 - green;\n"
+ " }\n"
+ " ncolor.b = color.b * 0.5 + 0.25;\n"
+ " gl_FragColor = vec4(ncolor.rgb, color.a);\n"
+ "}\n";
public CrossProcessFilter() { }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 ncolor = vec3(0.0, 0.0, 0.0);\n" + " float value;\n"
+ " if (color.r < 0.5) {\n" + " value = color.r;\n"
+ " } else {\n" + " value = 1.0 - color.r;\n" + " }\n"
+ " float red = 4.0 * value * value * value;\n"
+ " if (color.r < 0.5) {\n" + " ncolor.r = red;\n"
+ " } else {\n" + " ncolor.r = 1.0 - red;\n" + " }\n"
+ " if (color.g < 0.5) {\n" + " value = color.g;\n"
+ " } else {\n" + " value = 1.0 - color.g;\n" + " }\n"
+ " float green = 2.0 * value * value;\n"
+ " if (color.g < 0.5) {\n" + " ncolor.g = green;\n"
+ " } else {\n" + " ncolor.g = 1.0 - green;\n" + " }\n"
+ " ncolor.b = color.b * 0.5 + 0.25;\n"
+ " gl_FragColor = vec4(ncolor.rgb, color.a);\n" + "}\n";
return shader;
} }
} }

@ -1,27 +0,0 @@
package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull;
/**
* This class is to implement any custom effect.
*/
public class CustomFilter extends Filter {
/**
* Parameterized constructor with vertex and fragment shader as parameter
*
* @param vertexShader
* @param fragmentShader
*/
public CustomFilter(String vertexShader, String fragmentShader) {
this.mVertexShader = vertexShader;
this.mFragmentShader = fragmentShader;
}
@NonNull
@Override
public String getFragmentShader() {
return mFragmentShader;
}
}

@ -1,96 +1,115 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Date; import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import java.util.Random; import java.util.Random;
/** /**
* Applies black and white documentary style effect on preview. * Applies black and white documentary style effect.
*/ */
public class DocumentaryFilter extends Filter { public class DocumentaryFilter extends BaseFilter {
private Random mRandom;
private final static Random RANDOM = new Random();
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "vec2 seed;\n"
+ "float stepsize;\n"
+ "uniform float inv_max_dist;\n"
+ "uniform vec2 scale;\n"
+ "varying vec2 vTextureCoord;\n"
+ "float rand(vec2 loc) {\n"
+ " float theta1 = dot(loc, vec2(0.9898, 0.233));\n"
+ " float theta2 = dot(loc, vec2(12.0, 78.0));\n"
+ " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n"
+
// keep value of part1 in range: (2^-14 to 2^14).
" float temp = mod(197.0 * value, 1.0) + value;\n"
+ " float part1 = mod(220.0 * temp, 1.0) + temp;\n"
+ " float part2 = value * 0.5453;\n"
+ " float part3 = cos(theta1 + theta2) * 0.43758;\n"
+ " return fract(part1 + part2 + part3);\n"
+ "}\n"
+ "void main() {\n"
+ " seed[0] = " + RANDOM.nextFloat() + ";\n"
+ " seed[1] = " + RANDOM.nextFloat() + ";\n"
+ " stepsize = " + 1.0f / 255.0f + ";\n"
// black white
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float dither = rand(vTextureCoord + seed);\n"
+ " vec3 xform = clamp(2.0 * color.rgb, 0.0, 1.0);\n"
+ " vec3 temp = clamp(2.0 * (color.rgb + stepsize), 0.0, 1.0);\n"
+ " vec3 new_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n"
// grayscale
+ " float gray = dot(new_color, vec3(0.299, 0.587, 0.114));\n"
+ " new_color = vec3(gray, gray, gray);\n"
// vignette
+ " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n"
+ " float dist = length(coord * scale);\n"
+ " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.83) * 20.0)) + 0.15;\n"
+ " gl_FragColor = vec4(new_color * lumen, color.a);\n"
+ "}\n";
private int mWidth = 1;
private int mHeight = 1;
private int mScaleLocation = -1;
private int mMaxDistLocation = -1;
public DocumentaryFilter() { public DocumentaryFilter() { }
mRandom = new Random(new Date().getTime());
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
mWidth = width;
mHeight = height;
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float scale[] = new float[2]; return FRAGMENT_SHADER;
if (mPreviewingViewWidth > mPreviewingViewHeight) { }
@Override
public void onCreate(int programHandle) {
super.onCreate(programHandle);
mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(mScaleLocation, "scale");
mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist");
}
@Override
public void onDestroy() {
super.onDestroy();
mScaleLocation = -1;
mMaxDistLocation = -1;
}
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
float[] scale = new float[2];
if (mWidth > mHeight) {
scale[0] = 1f; scale[0] = 1f;
scale[1] = ((float) mPreviewingViewHeight) / mPreviewingViewWidth; scale[1] = ((float) mHeight) / mWidth;
} else { } else {
scale[0] = ((float) mPreviewingViewWidth) / mPreviewingViewHeight; scale[0] = ((float) mWidth) / mHeight;
scale[1] = 1f; scale[1] = 1f;
} }
float max_dist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] GLES20.glUniform2fv(mScaleLocation, 1, scale, 0);
* scale[1])) * 0.5f; GlUtils.checkError("glUniform2fv");
float seed[] = {mRandom.nextFloat(), mRandom.nextFloat()}; float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
float invMaxDist = 1F / maxDist;
String scaleString[] = new String[2]; GLES20.glUniform1f(mMaxDistLocation, invMaxDist);
String seedString[] = new String[2]; GlUtils.checkError("glUniform1f");
scaleString[0] = "scale[0] = " + scale[0] + ";\n";
scaleString[1] = "scale[1] = " + scale[1] + ";\n";
seedString[0] = "seed[0] = " + seed[0] + ";\n";
seedString[1] = "seed[1] = " + seed[1] + ";\n";
String inv_max_distString = "inv_max_dist = " + 1.0f / max_dist + ";\n";
String stepsizeString = "stepsize = " + 1.0f / 255.0f + ";\n";
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ " vec2 seed;\n"
+ " float stepsize;\n"
+ " float inv_max_dist;\n"
+ " vec2 scale;\n"
+ "varying vec2 vTextureCoord;\n"
+ "float rand(vec2 loc) {\n"
+ " float theta1 = dot(loc, vec2(0.9898, 0.233));\n"
+ " float theta2 = dot(loc, vec2(12.0, 78.0));\n"
+ " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n"
+
// keep value of part1 in range: (2^-14 to 2^14).
" float temp = mod(197.0 * value, 1.0) + value;\n"
+ " float part1 = mod(220.0 * temp, 1.0) + temp;\n"
+ " float part2 = value * 0.5453;\n"
+ " float part3 = cos(theta1 + theta2) * 0.43758;\n"
+ " return fract(part1 + part2 + part3);\n"
+ "}\n"
+ "void main() {\n"
// Parameters that were created above
+ scaleString[0]
+ scaleString[1]
+ seedString[0]
+ seedString[1]
+ inv_max_distString
+ stepsizeString
// black white
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float dither = rand(vTextureCoord + seed);\n"
+ " vec3 xform = clamp(2.0 * color.rgb, 0.0, 1.0);\n"
+ " vec3 temp = clamp(2.0 * (color.rgb + stepsize), 0.0, 1.0);\n"
+ " vec3 new_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n"
+
// grayscale
" float gray = dot(new_color, vec3(0.299, 0.587, 0.114));\n"
+ " new_color = vec3(gray, gray, gray);\n"
+
// vignette
" vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n"
+ " float dist = length(coord * scale);\n"
+ " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.83) * 20.0)) + 0.15;\n"
+ " gl_FragColor = vec4(new_color * lumen, color.a);\n"
+ "}\n";
return shader;
} }
} }

@ -1,85 +1,158 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.graphics.Color; import android.graphics.Color;
import android.opengl.GLES20;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.TwoParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Representation of preview using only two color tones. * Representation of input frames using only two color tones.
*/ */
public class DuotoneFilter extends Filter { public class DuotoneFilter extends BaseFilter implements TwoParameterFilter {
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform vec3 first;\n"
+ "uniform vec3 second;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float energy = (color.r + color.g + color.b) * 0.3333;\n"
+ " vec3 new_color = (1.0 - energy) * first + energy * second;\n"
+ " gl_FragColor = vec4(new_color.rgb, color.a);\n"
+ "}\n";
// Default values // Default values
private int mFirstColor = Color.MAGENTA; private int mFirstColor = Color.MAGENTA;
private int mSecondColor = Color.YELLOW; private int mSecondColor = Color.YELLOW;
private int mFirstColorLocation = -1;
private int mSecondColorLocation = -1;
public DuotoneFilter() { }
/** /**
* Initialize effect * Sets the two duotone ARGB colors.
* @param firstColor first
* @param secondColor second
*/
@SuppressWarnings({"unused"})
public void setColors(@ColorInt int firstColor, @ColorInt int secondColor) {
setFirstColor(firstColor);
setSecondColor(secondColor);
}
/**
* Sets the first of the duotone ARGB colors.
* Defaults to {@link Color#MAGENTA}.
*
* @param color first color
*/ */
public DuotoneFilter() { @SuppressWarnings("WeakerAccess")
public void setFirstColor(@ColorInt int color) {
mFirstColor = color;
} }
/** /**
* setDuoToneColors * Sets the second of the duotone ARGB colors.
* Defaults to {@link Color#YELLOW}.
* *
* @param firstColor Integer, representing an ARGB color with 8 bits per channel. * @param color second color
* May be created using Color class.
* @param secondColor Integer, representing an ARGB color with 8 bits per channel.
* May be created using Color class.
*/ */
public void setDuoToneColors(int firstColor, int secondColor) { @SuppressWarnings("WeakerAccess")
this.mFirstColor = firstColor; public void setSecondColor(@ColorInt int color) {
this.mSecondColor = secondColor; mSecondColor = color;
} }
/**
* Returns the first color.
*
* @see #setFirstColor(int)
* @return first
*/
@SuppressWarnings({"unused", "WeakerAccess"})
@ColorInt
public int getFirstColor() { public int getFirstColor() {
return mFirstColor; return mFirstColor;
} }
/**
* Returns the second color.
*
* @see #setSecondColor(int)
* @return second
*/
@SuppressWarnings({"unused", "WeakerAccess"})
@ColorInt
public int getSecondColor() { public int getSecondColor() {
return mSecondColor; return mSecondColor;
} }
@Override
public void setParameter1(float value) {
// no easy way to transform 0...1 into a color.
setFirstColor((int) (value * Integer.MAX_VALUE));
}
@Override
public float getParameter1() {
return (float) getFirstColor() / Integer.MAX_VALUE;
}
@Override
public void setParameter2(float value) {
// no easy way to transform 0...1 into a color.
setSecondColor((int) (value * Integer.MAX_VALUE));
}
@Override
public float getParameter2() {
return (float) getSecondColor() / Integer.MAX_VALUE;
}
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float first[] = {Color.red(mFirstColor) / 255f, return FRAGMENT_SHADER;
Color.green(mFirstColor) / 255f, Color.blue(mFirstColor) / 255f}; }
float second[] = {Color.red(mSecondColor) / 255f,
@Override
public void onCreate(int programHandle) {
super.onCreate(programHandle);
mFirstColorLocation = GLES20.glGetUniformLocation(programHandle, "first");
GlUtils.checkLocation(mFirstColorLocation, "first");
mSecondColorLocation = GLES20.glGetUniformLocation(programHandle, "second");
GlUtils.checkLocation(mSecondColorLocation, "second");
}
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
float[] first = new float[]{
Color.red(mFirstColor) / 255f,
Color.green(mFirstColor) / 255f,
Color.blue(mFirstColor) / 255f
};
float[] second = new float[]{
Color.red(mSecondColor) / 255f,
Color.green(mSecondColor) / 255f, Color.green(mSecondColor) / 255f,
Color.blue(mSecondColor) / 255f}; Color.blue(mSecondColor) / 255f
};
String firstColorString[] = new String[3]; GLES20.glUniform3fv(mFirstColorLocation, 1, first, 0);
String secondColorString[] = new String[3]; GlUtils.checkError("glUniform3fv");
GLES20.glUniform3fv(mSecondColorLocation, 1, second, 0);
firstColorString[0] = "first[0] = " + first[0] + ";\n"; GlUtils.checkError("glUniform3fv");
firstColorString[1] = "first[1] = " + first[1] + ";\n"; }
firstColorString[2] = "first[2] = " + first[2] + ";\n";
secondColorString[0] = "second[0] = " + second[0] + ";\n";
secondColorString[1] = "second[1] = " + second[1] + ";\n";
secondColorString[2] = "second[2] = " + second[2] + ";\n";
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ " vec3 first;\n"
+ " vec3 second;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
// Parameters that were created above
+ firstColorString[0]
+ firstColorString[1]
+ firstColorString[2]
+ secondColorString[0]
+ secondColorString[1]
+ secondColorString[2]
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float energy = (color.r + color.g + color.b) * 0.3333;\n"
+ " vec3 new_color = (1.0 - energy) * first + energy * second;\n"
+ " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n";
return shader;
@Override
public void onDestroy() {
super.onDestroy();
mFirstColorLocation = -1;
mSecondColorLocation = -1;
} }
} }

@ -1,73 +1,111 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Applies back-light filling to the preview. * Applies back-light filling to the frames.
*/ */
public class FillLightFilter extends Filter { public class FillLightFilter extends BaseFilter implements OneParameterFilter {
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float mult;\n"
+ "uniform float igamma;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " const vec3 color_weights = vec3(0.25, 0.5, 0.25);\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float lightmask = dot(color.rgb, color_weights);\n"
+ " float backmask = (1.0 - lightmask);\n"
+ " vec3 ones = vec3(1.0, 1.0, 1.0);\n"
+ " vec3 diff = pow(mult * color.rgb, igamma * ones) - color.rgb;\n"
+ " diff = min(diff, 1.0);\n"
+ " vec3 new_color = min(color.rgb + diff * backmask, 1.0);\n"
+ " gl_FragColor = vec4(new_color, color.a);\n"
+ "}\n";
private float strength = 0.5f; private float strength = 0.5f;
private int multiplierLocation = -1;
private int gammaLocation = -1;
/** public FillLightFilter() { }
* Initialize Effect
*/
public FillLightFilter() {
}
/** /**
* setStrength * Sets the current strength.
* 0.0: no change.
* 1.0: max strength.
* *
* @param strength Float, between 0.0 and 1.0 where 0.0 means no change. * @param strength strength
*/ */
@SuppressWarnings("WeakerAccess")
public void setStrength(float strength) { public void setStrength(float strength) {
if (strength < 0.0f) if (strength < 0.0f) strength = 0f;
strength = 0f; if (strength > 1.0f) strength = 1f;
else if (strength > 1.0f)
strength = 1f;
this.strength = strength; this.strength = strength;
} }
/**
* Returns the current strength.
*
* @see #setStrength(float)
* @return strength
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public float getStrength() { public float getStrength() {
return strength; return strength;
} }
@Override
public void setParameter1(float value) {
setStrength(value);
}
@Override
public float getParameter1() {
return getStrength();
}
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float fade_gamma = 0.3f; return FRAGMENT_SHADER;
float amt = 1.0f - strength; }
float mult = 1.0f / (amt * 0.7f + 0.3f);
float faded = fade_gamma + (1.0f - fade_gamma) * mult; @Override
float igamma = 1.0f / faded; public void onCreate(int programHandle) {
super.onCreate(programHandle);
String multString = "mult = " + mult + ";\n"; multiplierLocation = GLES20.glGetUniformLocation(programHandle, "mult");
String igammaString = "igamma = " + igamma + ";\n"; GlUtils.checkLocation(multiplierLocation, "mult");
gammaLocation = GLES20.glGetUniformLocation(programHandle, "igamma");
String shader = "#extension GL_OES_EGL_image_external : require\n" GlUtils.checkLocation(gammaLocation, "igamma");
+ "precision mediump float;\n" }
+ "uniform samplerExternalOES sTexture;\n"
+ " float mult;\n"
+ " float igamma;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main()\n"
+ "{\n"
// Parameters that were created above
+ multString
+ igammaString
+ " const vec3 color_weights = vec3(0.25, 0.5, 0.25);\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float lightmask = dot(color.rgb, color_weights);\n"
+ " float backmask = (1.0 - lightmask);\n"
+ " vec3 ones = vec3(1.0, 1.0, 1.0);\n"
+ " vec3 diff = pow(mult * color.rgb, igamma * ones) - color.rgb;\n"
+ " diff = min(diff, 1.0);\n"
+ " vec3 new_color = min(color.rgb + diff * backmask, 1.0);\n"
+ " gl_FragColor = vec4(new_color, color.a);\n" + "}\n";
return shader;
@Override
public void onDestroy() {
super.onDestroy();
multiplierLocation = -1;
gammaLocation = -1;
} }
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
float amount = 1.0f - strength;
float multiplier = 1.0f / (amount * 0.7f + 0.3f);
GLES20.glUniform1f(multiplierLocation, multiplier);
GlUtils.checkError("glUniform1f");
float fadeGamma = 0.3f;
float faded = fadeGamma + (1.0f - fadeGamma) * multiplier;
float gamma = 1.0f / faded;
GLES20.glUniform1f(gammaLocation, gamma);
GlUtils.checkError("glUniform1f");
}
} }

@ -1,128 +0,0 @@
package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull;
/**
* A Base abstract class that every effect must extend so that there is a common getShader method.
* <p>
* This class has a default Vertex Shader implementation which in most cases not required to touch.
* In Effects like sepia, B/W any many, only pixel color changes which can be managed by only fragment shader.
* If there is some other effect which requires vertex shader also change, you can override it.
* <p>
* If your provide your own vertex and fragment shader,
* please set the {@link #mPositionVariableName}, {@link #mTextureCoordinateVariableName},
* {@link #mMVPMatrixVariableName}, {@link #mTextureMatrixVariableName}
* according to your shader code.
* <p>
* Please note that these shader applies on live preview as well as pictureSnapshot and videoSnapshot,
* we only support GLES11Ext.GL_TEXTURE_EXTERNAL_OES
* check EglViewport()
* <p>
* The default implementation of this class is NoEffect.
*/
public abstract class Filter {
/**
* Vertex shader code written in Shader Language (C) and stored as String.
* This wil be used by GL to apply any effect.
*/
@NonNull
String mVertexShader =
"uniform mat4 uMVPMatrix;\n" +
"uniform mat4 uTexMatrix;\n" +
"attribute vec4 aPosition;\n" +
"attribute vec4 aTextureCoord;\n" +
"varying vec2 vTextureCoord;\n" +
"void main() {\n" +
" gl_Position = uMVPMatrix * aPosition;\n" +
" vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
"}\n";
/**
* Fragment shader code written in Shader Language (C) and stored as String.
* This wil be used by GL to apply any effect.
*/
@NonNull
String mFragmentShader =
"#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "void main() {\n"
+ " gl_FragColor = texture2D(sTexture, vTextureCoord);\n"
+ "}\n";
/**
* Width and height of previewing GlSurfaceview.
* This will be used by a few effects.
*/
int mPreviewingViewWidth = 0;
int mPreviewingViewHeight = 0;
public void setPreviewingViewSize(int width, int height) {
mPreviewingViewWidth = width;
mPreviewingViewHeight = height;
}
/**
* Local variable name which were used in the shader code.
* These will be used by openGL program to render these vertex and fragment shader
*/
private String mPositionVariableName = "aPosition";
private String mTextureCoordinateVariableName = "aTextureCoord";
private String mMVPMatrixVariableName = "uMVPMatrix";
private String mTextureMatrixVariableName = "uTexMatrix";
public String getPositionVariableName() {
return mPositionVariableName;
}
public void setPositionVariableName(String positionVariableName) {
this.mPositionVariableName = positionVariableName;
}
public String getTexttureCoordinateVariableName() {
return mTextureCoordinateVariableName;
}
public void setTexttureCoordinateVariableName(String texttureCoordinateVariableName) {
this.mTextureCoordinateVariableName = texttureCoordinateVariableName;
}
public String getMVPMatrixVariableName() {
return mMVPMatrixVariableName;
}
public void setMVPMatrixVariableName(String mvpMatrixVariableName) {
this.mMVPMatrixVariableName = mvpMatrixVariableName;
}
public String getTextureMatrixVariableName() {
return mTextureMatrixVariableName;
}
public void setTextureMatrixVariableName(String textureMatrixVariableName) {
this.mTextureMatrixVariableName = textureMatrixVariableName;
}
/**
* Get vertex Shader code
*
* @return complete shader code in C
*/
@NonNull
public String getVertexShader() {
return mVertexShader;
}
/**
* Get fragment Shader code
*
* @return complete shader code in C
*/
@NonNull
public abstract String getFragmentShader();
}

@ -1,124 +0,0 @@
package com.otaliastudios.cameraview.filters;
public enum Filters {
NO_FILTER,
AUTO_FIX_FILTER,
BLACK_AND_WHITE_FILTER,
BRIGHTNESS_FILTER,
CONTRAST_FILTER,
CROSS_PROCESS_FILTER,
DOCUMENTARY_FILTER,
DUO_TONE_COLOR_FILTER,
FILL_LIGHT_FILTER,
GAMMA_FILTER,
GRAIN_FILTER,
GREY_SCALE_FILTER,
HUE_FILTER,
INVERT_COLOR_FILTER,
LAMOISH_FILTER,
POSTERIZE_FILTER,
SATURATION_FILTER,
SEPIA_FILTER,
SHARPNESS_FILTER,
TEMPERATURE_FILTER,
TINT_FILTER,
VIGNETTE_FILTER;
public Filter newInstance() {
Filter shaderEffect;
switch (this) {
case AUTO_FIX_FILTER:
shaderEffect = new AutoFixFilter();
break;
case BLACK_AND_WHITE_FILTER:
shaderEffect = new BlackAndWhiteFilter();
break;
case BRIGHTNESS_FILTER:
shaderEffect = new BrightnessFilter();
break;
case CONTRAST_FILTER:
shaderEffect = new ContrastFilter();
break;
case CROSS_PROCESS_FILTER:
shaderEffect = new CrossProcessFilter();
break;
case DOCUMENTARY_FILTER:
shaderEffect = new DocumentaryFilter();
break;
case DUO_TONE_COLOR_FILTER:
shaderEffect = new DuotoneFilter();
break;
case FILL_LIGHT_FILTER:
shaderEffect = new FillLightFilter();
break;
case GAMMA_FILTER:
shaderEffect = new GammaFilter();
break;
case GRAIN_FILTER:
shaderEffect = new GrainFilter();
break;
case GREY_SCALE_FILTER:
shaderEffect = new GreyScaleFilter();
break;
case HUE_FILTER:
shaderEffect = new HueFilter();
break;
case INVERT_COLOR_FILTER:
shaderEffect = new InvertColorsFilter();
break;
case LAMOISH_FILTER:
shaderEffect = new LamoishFilter();
break;
case POSTERIZE_FILTER:
shaderEffect = new PosterizeFilter();
break;
case SATURATION_FILTER:
shaderEffect = new SaturationFilter();
break;
case SEPIA_FILTER:
shaderEffect = new SepiaFilter();
break;
case SHARPNESS_FILTER:
shaderEffect = new SharpnessFilter();
break;
case TEMPERATURE_FILTER:
shaderEffect = new TemperatureFilter();
break;
case TINT_FILTER:
shaderEffect = new TintFilter();
break;
case VIGNETTE_FILTER:
shaderEffect = new VignetteFilter();
break;
case NO_FILTER:
default:
shaderEffect = new NoFilter();
}
return shaderEffect;
}
}

@ -1,59 +1,90 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Apply Gamma Effect on preview being played * Applies gamma correction to the frames.
*/ */
public class GammaFilter extends Filter { public class GammaFilter extends BaseFilter implements OneParameterFilter {
private float gammaValue = 2.0f;
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float gamma;\n"
+ "void main() {\n"
+ " vec4 textureColor = texture2D(sTexture, vTextureCoord);\n"
+ " gl_FragColor = vec4(pow(textureColor.rgb, vec3(gamma)), textureColor.w);\n"
+ "}\n";
private float gamma = 2.0f;
private int gammaLocation = -1;
public GammaFilter() { }
/** /**
* Initialize Effect * Sets the new gamma value in the 0.0 - 2.0 range.
* The 1.0 value means no correction will be applied.
*
* @param gamma gamma value
*/ */
public GammaFilter() { @SuppressWarnings("WeakerAccess")
public void setGamma(float gamma) {
if (gamma < 0.0f) gamma = 0.0f;
if (gamma > 2.0f) gamma = 2.0f;
this.gamma = gamma;
} }
/** /**
* setGammaValue * Returns the current gamma.
* *
* @param gammaValue Range should be between 0.0 - 1.0 with 0.5 being normal. * @see #setGamma(float)
* @return gamma
*/ */
public void setGammaValue(float gammaValue) { @SuppressWarnings("WeakerAccess")
if (gammaValue < 0.0f) public float getGamma() {
gammaValue = 0.0f; return gamma;
else if (gammaValue > 1.0f) }
gammaValue = 1.0f;
//since the shader excepts a range of 0.0 - 2.0 @Override
//will multiply the 2.0 to every value public void setParameter1(float value) {
this.gammaValue = gammaValue * 2.0f; setGamma(value * 2F);
} }
public float getGammaValue() { @Override
//since the shader excepts a range of 0.0 - 2.0 public float getParameter1() {
//to keep it between 0.0f - 1.0f range, will divide it with 2.0 return getGamma() / 2F;
return gammaValue / 2.0f;
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
}
String shader = "#extension GL_OES_EGL_image_external : require\n" @Override
+ "precision mediump float;\n" public void onCreate(int programHandle) {
super.onCreate(programHandle);
+ "varying vec2 vTextureCoord;\n" gammaLocation = GLES20.glGetUniformLocation(programHandle, "gamma");
+ "uniform samplerExternalOES sTexture;\n" GlUtils.checkLocation(gammaLocation, "gamma");
+ "float gamma=" + gammaValue + ";\n" }
+ "void main() {\n"
+ "vec4 textureColor = texture2D(sTexture, vTextureCoord);\n"
+ "gl_FragColor = vec4(pow(textureColor.rgb, vec3(gamma)), textureColor.w);\n"
+ "}\n"; @Override
public void onDestroy() {
super.onDestroy();
gammaLocation = -1;
}
return shader; @Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
GLES20.glUniform1f(gammaLocation, gamma);
GlUtils.checkError("glUniform1f");
} }
} }

@ -1,98 +1,144 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Date; import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import java.util.Random; import java.util.Random;
/** /**
* Applies film grain effect to preview. * Applies film grain effect to the frames.
*/ */
public class GrainFilter extends Filter { public class GrainFilter extends BaseFilter implements OneParameterFilter {
private final static Random RANDOM = new Random();
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "vec2 seed;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES tex_sampler_0;\n"
+ "uniform samplerExternalOES tex_sampler_1;\n"
+ "uniform float scale;\n"
+ "uniform float stepX;\n"
+ "uniform float stepY;\n"
+ "float rand(vec2 loc) {\n"
+ " float theta1 = dot(loc, vec2(0.9898, 0.233));\n"
+ " float theta2 = dot(loc, vec2(12.0, 78.0));\n"
+ " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n"
// keep value of part1 in range: (2^-14 to 2^14).
+ " float temp = mod(197.0 * value, 1.0) + value;\n"
+ " float part1 = mod(220.0 * temp, 1.0) + temp;\n"
+ " float part2 = value * 0.5453;\n"
+ " float part3 = cos(theta1 + theta2) * 0.43758;\n"
+ " float sum = (part1 + part2 + part3);\n"
+ " return fract(sum)*scale;\n"
+ "}\n"
+ "void main() {\n"
+ " seed[0] = " + RANDOM.nextFloat() + ";\n"
+ " seed[1] = " + RANDOM.nextFloat() + ";\n"
+ " float noise = texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, -stepY)).r * 0.224;\n"
+ " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, stepY)).r * 0.224;\n"
+ " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, -stepY)).r * 0.224;\n"
+ " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, stepY)).r * 0.224;\n"
+ " noise += 0.4448;\n"
+ " noise *= scale;\n"
+ " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n"
+ " float energy = 0.33333 * color.r + 0.33333 * color.g + 0.33333 * color.b;\n"
+ " float mask = (1.0 - sqrt(energy));\n"
+ " float weight = 1.0 - 1.333 * mask * noise;\n"
+ " gl_FragColor = vec4(color.rgb * weight, color.a);\n"
+ " gl_FragColor = gl_FragColor+vec4(rand(vTextureCoord + seed), rand(vTextureCoord + seed),rand(vTextureCoord + seed),1);\n"
+ "}\n";
private float strength = 0.5f; private float strength = 0.5f;
private Random mRandom; private int width = 1;
private int height = 1;
private int strengthLocation = -1;
private int stepXLocation = -1;
private int stepYLocation = -1;
/** @SuppressWarnings("WeakerAccess")
* Initialize Effect public GrainFilter() { }
*/
public GrainFilter() { @Override
mRandom = new Random(new Date().getTime()); public void setSize(int width, int height) {
super.setSize(width, height);
this.width = width;
this.height = height;
} }
/** /**
* setDistortionStrength * Sets the current distortion strength.
* 0.0: no distortion.
* 1.0: maximum distortion.
* *
* @param strength Float, between 0.0f and 1.0. Zero means no distortion, while 1 * @param strength strength
* indicates the maximum amount of adjustment.
*/ */
public void setDistortionStrength(float strength) { @SuppressWarnings("WeakerAccess")
if (strength < 0.0f) public void setStrength(float strength) {
strength = 0.0f; if (strength < 0.0f) strength = 0.0f;
else if (strength > 1.0f) if (strength > 1.0f) strength = 1.0f;
strength = 1.0f;
this.strength = strength; this.strength = strength;
} }
/**
* Returns the current strength.
*
* @see #setStrength(float)
* @return strength
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public float getStrength() { public float getStrength() {
return strength; return strength;
} }
@Override
public void setParameter1(float value) {
setStrength(value);
}
@Override
public float getParameter1() {
return getStrength();
}
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float seed[] = {mRandom.nextFloat(), mRandom.nextFloat()}; return FRAGMENT_SHADER;
String scaleString = "scale = " + strength + ";\n"; }
String seedString[] = new String[2];
seedString[0] = "seed[0] = " + seed[0] + ";\n"; @Override
seedString[1] = "seed[1] = " + seed[1] + ";\n"; public void onCreate(int programHandle) {
String stepX = "stepX = " + 0.5f / mPreviewingViewWidth + ";\n"; super.onCreate(programHandle);
String stepY = "stepY = " + 0.5f / mPreviewingViewHeight + ";\n"; strengthLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(strengthLocation, "scale");
// locString[1] = "loc[1] = loc[1]+" + seedString[1] + ";\n"; stepXLocation = GLES20.glGetUniformLocation(programHandle, "stepX");
GlUtils.checkLocation(stepXLocation, "stepX");
String shader = "#extension GL_OES_EGL_image_external : require\n" stepYLocation = GLES20.glGetUniformLocation(programHandle, "stepY");
+ "precision mediump float;\n" GlUtils.checkLocation(stepYLocation, "stepY");
+ " vec2 seed;\n" }
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES tex_sampler_0;\n" @Override
+ "uniform samplerExternalOES tex_sampler_1;\n" public void onDestroy() {
+ "float scale;\n" super.onDestroy();
+ " float stepX;\n" strengthLocation = -1;
+ " float stepY;\n" stepXLocation = -1;
+ "float rand(vec2 loc) {\n" stepYLocation = -1;
+ " float theta1 = dot(loc, vec2(0.9898, 0.233));\n" }
+ " float theta2 = dot(loc, vec2(12.0, 78.0));\n"
+ " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n"
+
// keep value of part1 in range: (2^-14 to 2^14).
" float temp = mod(197.0 * value, 1.0) + value;\n"
+ " float part1 = mod(220.0 * temp, 1.0) + temp;\n"
+ " float part2 = value * 0.5453;\n"
+ " float part3 = cos(theta1 + theta2) * 0.43758;\n"
+ " float sum = (part1 + part2 + part3);\n"
+ " return fract(sum)*scale;\n"
+ "}\n"
+ "void main() {\n"
// Parameters that were created above
+ seedString[0]
+ seedString[1]
+ scaleString
+ stepX
+ stepY
+ " float noise = texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, -stepY)).r * 0.224;\n"
+ " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(-stepX, stepY)).r * 0.224;\n"
+ " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, -stepY)).r * 0.224;\n"
+ " noise += texture2D(tex_sampler_1, vTextureCoord + vec2(stepX, stepY)).r * 0.224;\n"
+ " noise += 0.4448;\n"
+ " noise *= scale;\n"
+ " vec4 color = texture2D(tex_sampler_0, vTextureCoord);\n"
+ " float energy = 0.33333 * color.r + 0.33333 * color.g + 0.33333 * color.b;\n"
+ " float mask = (1.0 - sqrt(energy));\n"
+ " float weight = 1.0 - 1.333 * mask * noise;\n"
+ " gl_FragColor = vec4(color.rgb * weight, color.a);\n"
+ " gl_FragColor = gl_FragColor+vec4(rand(vTextureCoord + seed), rand(vTextureCoord + seed),rand(vTextureCoord + seed),1);\n"
+ "}\n";
return shader;
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
GLES20.glUniform1f(strengthLocation, strength);
GlUtils.checkError("glUniform1f");
GLES20.glUniform1f(stepXLocation, 0.5f / width);
GlUtils.checkError("glUniform1f");
GLES20.glUniform1f(stepYLocation, 0.5f / height);
GlUtils.checkError("glUniform1f");
} }
} }

@ -0,0 +1,29 @@
package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
/**
* Converts frames to gray scale.
*/
public class GrayscaleFilter extends BaseFilter {
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float y = dot(color, vec4(0.299, 0.587, 0.114, 0));\n"
+ " gl_FragColor = vec4(y, y, y, color.a);\n"
+ "}\n";
public GrayscaleFilter() { }
@NonNull
@Override
public String getFragmentShader() {
return FRAGMENT_SHADER;
}
}

@ -1,31 +0,0 @@
package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull;
/**
* Converts preview to GreyScale.
*/
public class GreyScaleFilter extends Filter {
/**
* Initialize Effect
*/
public GreyScaleFilter() {
}
@NonNull
@Override
public String getFragmentShader() {
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float y = dot(color, vec4(0.299, 0.587, 0.114, 0));\n"
+ " gl_FragColor = vec4(y, y, y, color.a);\n" + "}\n";
;
return shader;
}
}

@ -1,72 +1,106 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Apply Hue effect on the preview * Applies a hue effect on the input frames.
*/ */
public class HueFilter extends Filter { public class HueFilter extends BaseFilter implements OneParameterFilter {
float hueValue = 0.0f;
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float hue;\n"
+ "void main() {\n"
+ " vec4 kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);\n"
+ " vec4 kRGBToI = vec4 (0.595716, -0.274453, -0.321263, 0.0);\n"
+ " vec4 kRGBToQ = vec4 (0.211456, -0.522591, 0.31135, 0.0);\n"
+ " vec4 kYIQToR = vec4 (1.0, 0.9563, 0.6210, 0.0);\n"
+ " vec4 kYIQToG = vec4 (1.0, -0.2721, -0.6474, 0.0);\n"
+ " vec4 kYIQToB = vec4 (1.0, -1.1070, 1.7046, 0.0);\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float YPrime = dot(color, kRGBToYPrime);\n"
+ " float I = dot(color, kRGBToI);\n"
+ " float Q = dot(color, kRGBToQ);\n"
+ " float chroma = sqrt (I * I + Q * Q);\n"
+ " Q = chroma * sin (hue);\n"
+ " I = chroma * cos (hue);\n"
+ " vec4 yIQ = vec4 (YPrime, I, Q, 0.0);\n"
+ " color.r = dot (yIQ, kYIQToR);\n"
+ " color.g = dot (yIQ, kYIQToG);\n"
+ " color.b = dot (yIQ, kYIQToB);\n"
+ " gl_FragColor = color;\n"
+ "}\n";
private float hue = 0.0f;
private int hueLocation = -1;
public HueFilter() { }
/** /**
* Initialize Effect * Sets the hue value in degrees. See the values chart:
* https://cloud.githubusercontent.com/assets/2201511/21810115/b99ac22a-d74a-11e6-9f6c-ef74d15c88c7.jpg
*
* @param hue hue degrees
*/ */
public HueFilter() { @SuppressWarnings({"unused", "WeakerAccess"})
public void setHue(float hue) {
this.hue = hue % 360;
} }
/** /**
* Hue value chart - https://cloud.githubusercontent.com/assets/2201511/21810115/b99ac22a-d74a-11e6-9f6c-ef74d15c88c7.jpg" * Returns the current hue value.
* *
* @param hueDegrees Range of value should be between 0 to 360 degrees as described in the image above * @see #setHue(float)
* @return hue
*/ */
public void setHueDegreeValue(float hueDegrees) { @SuppressWarnings("WeakerAccess")
// manipulating input value so that we can map it on 360 degree circle public float getHue() {
hueValue = ((hueDegrees - 45) / 45f + 0.5f) * -1; return hue;
} }
@NonNull
@Override @Override
public String getFragmentShader() { public void setParameter1(float value) {
setHue(value * 360F);
String shader = "#extension GL_OES_EGL_image_external : require\n" }
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "float hue=" + hueValue + ";\n"
+ "void main() {\n"
+ "vec4 kRGBToYPrime = vec4 (0.299, 0.587, 0.114, 0.0);\n"
+ "vec4 kRGBToI = vec4 (0.595716, -0.274453, -0.321263, 0.0);\n"
+ "vec4 kRGBToQ = vec4 (0.211456, -0.522591, 0.31135, 0.0);\n"
+ "vec4 kYIQToR = vec4 (1.0, 0.9563, 0.6210, 0.0);\n"
+ "vec4 kYIQToG = vec4 (1.0, -0.2721, -0.6474, 0.0);\n"
+ "vec4 kYIQToB = vec4 (1.0, -1.1070, 1.7046, 0.0);\n"
+ "vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ "float YPrime = dot(color, kRGBToYPrime);\n"
+ "float I = dot(color, kRGBToI);\n"
+ "float Q = dot(color, kRGBToQ);\n"
+ "float chroma = sqrt (I * I + Q * Q);\n"
+ "Q = chroma * sin (hue);\n"
+ "I = chroma * cos (hue);\n" @Override
public float getParameter1() {
return getHue() / 360F;
}
+ "vec4 yIQ = vec4 (YPrime, I, Q, 0.0);\n" @NonNull
@Override
public String getFragmentShader() {
return FRAGMENT_SHADER;
}
+ "color.r = dot (yIQ, kYIQToR);\n" @Override
+ "color.g = dot (yIQ, kYIQToG);\n" public void onCreate(int programHandle) {
+ "color.b = dot (yIQ, kYIQToB);\n" super.onCreate(programHandle);
+ "gl_FragColor = color;\n" hueLocation = GLES20.glGetUniformLocation(programHandle, "hue");
GlUtils.checkLocation(hueLocation, "hue");
}
+ "}\n"; @Override
public void onDestroy() {
super.onDestroy();
hueLocation = -1;
}
return shader; @Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
// map it on 360 degree circle
float shaderHue = ((hue - 45) / 45f + 0.5f) * -1;
GLES20.glUniform1f(hueLocation, shaderHue);
GlUtils.checkError("glUniform1f");
} }
} }

@ -2,32 +2,30 @@ package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
/** /**
* Inverts the preview colors. This can also be known as negative Effect. * Inverts the input colors. This is also known as negative effect.
*/ */
public class InvertColorsFilter extends Filter { public class InvertColorsFilter extends BaseFilter {
/**
* Initialize Effect private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
*/ + "precision mediump float;\n"
public InvertColorsFilter() { + "varying vec2 vTextureCoord;\n"
} + "uniform samplerExternalOES sTexture;\n"
+ "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float colorR = (1.0 - color.r) / 1.0;\n"
+ " float colorG = (1.0 - color.g) / 1.0;\n"
+ " float colorB = (1.0 - color.b) / 1.0;\n"
+ " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n"
+ "}\n";
public InvertColorsFilter() { }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n" + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float colorR = (1.0 - color.r) / 1.0;\n"
+ " float colorG = (1.0 - color.g) / 1.0;\n"
+ " float colorB = (1.0 - color.b) / 1.0;\n"
+ " gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n"
+ "}\n";
return shader;
} }
} }

@ -1,143 +0,0 @@
package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull;
import java.util.Date;
import java.util.Random;
/**
* Applies lomo-camera style effect to preview.
*/
public class LamoishFilter extends Filter {
private Random mRandom;
/**
* Initialize Effect
*/
public LamoishFilter() {
mRandom = new Random(new Date().getTime());
}
@NonNull
@Override
public String getFragmentShader() {
float scale[] = new float[2];
if (mPreviewingViewWidth > mPreviewingViewHeight) {
scale[0] = 1f;
scale[1] = ((float) mPreviewingViewHeight) / mPreviewingViewWidth;
} else {
scale[0] = ((float) mPreviewingViewWidth) / mPreviewingViewHeight;
scale[1] = 1f;
}
float max_dist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1]
* scale[1])) * 0.5f;
float seed[] = {mRandom.nextFloat(), mRandom.nextFloat()};
String scaleString[] = new String[2];
String seedString[] = new String[2];
scaleString[0] = "scale[0] = " + scale[0] + ";\n";
scaleString[1] = "scale[1] = " + scale[1] + ";\n";
seedString[0] = "seed[0] = " + seed[0] + ";\n";
seedString[1] = "seed[1] = " + seed[1] + ";\n";
String inv_max_distString = "inv_max_dist = " + 1.0f / max_dist + ";\n";
String stepsizeString = "stepsize = " + 1.0f / 255.0f + ";\n";
String stepsizeXString = "stepsizeX = " + 1.0f / mPreviewingViewWidth + ";\n";
String stepsizeYString = "stepsizeY = " + 1.0f / mPreviewingViewHeight + ";\n";
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ " vec2 seed;\n"
+ " float stepsizeX;\n"
+ " float stepsizeY;\n"
+ " float stepsize;\n"
+ " vec2 scale;\n"
+ " float inv_max_dist;\n"
+ "varying vec2 vTextureCoord;\n"
+ "float rand(vec2 loc) {\n"
+ " float theta1 = dot(loc, vec2(0.9898, 0.233));\n"
+ " float theta2 = dot(loc, vec2(12.0, 78.0));\n"
+ " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n"
+
// keep value of part1 in range: (2^-14 to 2^14).
" float temp = mod(197.0 * value, 1.0) + value;\n"
+ " float part1 = mod(220.0 * temp, 1.0) + temp;\n"
+ " float part2 = value * 0.5453;\n"
+ " float part3 = cos(theta1 + theta2) * 0.43758;\n"
+ " return fract(part1 + part2 + part3);\n" + "}\n"
+ "void main() {\n"
// Parameters that were created above
+ scaleString[0]
+ scaleString[1]
+ seedString[0]
+ seedString[1]
+ inv_max_distString
+ stepsizeString
+ stepsizeXString
+ stepsizeYString
// sharpen
+ " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n"
+ " vec2 coord;\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n"
+ " coord.y = vTextureCoord.y - stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x - stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " vec3 s_color = vec3(color.rgb + 0.3 * nbr_color);\n"
+
// cross process
" vec3 c_color = vec3(0.0, 0.0, 0.0);\n"
+ " float value;\n"
+ " if (s_color.r < 0.5) {\n"
+ " value = s_color.r;\n"
+ " } else {\n"
+ " value = 1.0 - s_color.r;\n"
+ " }\n"
+ " float red = 4.0 * value * value * value;\n"
+ " if (s_color.r < 0.5) {\n"
+ " c_color.r = red;\n"
+ " } else {\n"
+ " c_color.r = 1.0 - red;\n"
+ " }\n"
+ " if (s_color.g < 0.5) {\n"
+ " value = s_color.g;\n"
+ " } else {\n"
+ " value = 1.0 - s_color.g;\n"
+ " }\n"
+ " float green = 2.0 * value * value;\n"
+ " if (s_color.g < 0.5) {\n"
+ " c_color.g = green;\n"
+ " } else {\n"
+ " c_color.g = 1.0 - green;\n"
+ " }\n"
+ " c_color.b = s_color.b * 0.5 + 0.25;\n"
+
// blackwhite
" float dither = rand(vTextureCoord + seed);\n"
+ " vec3 xform = clamp((c_color.rgb - 0.15) * 1.53846, 0.0, 1.0);\n"
+ " vec3 temp = clamp((color.rgb + stepsize - 0.15) * 1.53846, 0.0, 1.0);\n"
+ " vec3 bw_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n"
+
// vignette
" coord = vTextureCoord - vec2(0.5, 0.5);\n"
+ " float dist = length(coord * scale);\n"
+ " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.73) * 20.0)) + 0.15;\n"
+ " gl_FragColor = vec4(bw_color * lumen, color.a);\n" + "}\n";
;
return shader;
}
}

@ -0,0 +1,165 @@
package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import java.util.Date;
import java.util.Random;
/**
* Applies a lomo-camera style effect to the input frames.
*/
public class LomoishFilter extends BaseFilter {
private final static Random RANDOM = new Random();
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float stepsizeX;\n"
+ "uniform float stepsizeY;\n"
+ "uniform vec2 scale;\n"
+ "uniform float inv_max_dist;\n"
+ "vec2 seed;\n"
+ "float stepsize;\n"
+ "varying vec2 vTextureCoord;\n"
+ "float rand(vec2 loc) {\n"
+ " float theta1 = dot(loc, vec2(0.9898, 0.233));\n"
+ " float theta2 = dot(loc, vec2(12.0, 78.0));\n"
+ " float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n"
// keep value of part1 in range: (2^-14 to 2^14).
+ " float temp = mod(197.0 * value, 1.0) + value;\n"
+ " float part1 = mod(220.0 * temp, 1.0) + temp;\n"
+ " float part2 = value * 0.5453;\n"
+ " float part3 = cos(theta1 + theta2) * 0.43758;\n"
+ " return fract(part1 + part2 + part3);\n"
+ "}\n"
+ "void main() {\n"
+ " seed[0] = " + RANDOM.nextFloat() + ";\n"
+ " seed[1] = " + RANDOM.nextFloat() + ";\n"
+ " stepsize = " + 1.0f / 255.0f + ";\n"
// sharpen
+ " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n"
+ " vec2 coord;\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n"
+ " coord.y = vTextureCoord.y - stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x - stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " vec3 s_color = vec3(color.rgb + 0.3 * nbr_color);\n"
// cross process
+ " vec3 c_color = vec3(0.0, 0.0, 0.0);\n"
+ " float value;\n"
+ " if (s_color.r < 0.5) {\n"
+ " value = s_color.r;\n"
+ " } else {\n"
+ " value = 1.0 - s_color.r;\n"
+ " }\n"
+ " float red = 4.0 * value * value * value;\n"
+ " if (s_color.r < 0.5) {\n"
+ " c_color.r = red;\n"
+ " } else {\n"
+ " c_color.r = 1.0 - red;\n"
+ " }\n"
+ " if (s_color.g < 0.5) {\n"
+ " value = s_color.g;\n"
+ " } else {\n"
+ " value = 1.0 - s_color.g;\n"
+ " }\n"
+ " float green = 2.0 * value * value;\n"
+ " if (s_color.g < 0.5) {\n"
+ " c_color.g = green;\n"
+ " } else {\n"
+ " c_color.g = 1.0 - green;\n"
+ " }\n"
+ " c_color.b = s_color.b * 0.5 + 0.25;\n"
// blackwhite
+ " float dither = rand(vTextureCoord + seed);\n"
+ " vec3 xform = clamp((c_color.rgb - 0.15) * 1.53846, 0.0, 1.0);\n"
+ " vec3 temp = clamp((color.rgb + stepsize - 0.15) * 1.53846, 0.0, 1.0);\n"
+ " vec3 bw_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n"
// vignette
+ " coord = vTextureCoord - vec2(0.5, 0.5);\n"
+ " float dist = length(coord * scale);\n"
+ " float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.73) * 20.0)) + 0.15;\n"
+ " gl_FragColor = vec4(bw_color * lumen, color.a);\n"
+ "}\n";
private int width = 1;
private int height = 1;
private int scaleLocation = -1;
private int maxDistLocation = -1;
private int stepSizeXLocation = -1;
private int stepSizeYLocation = -1;
public LomoishFilter() { }
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
this.width = width;
this.height = height;
}
@NonNull
@Override
public String getFragmentShader() {
return FRAGMENT_SHADER;
}
@Override
public void onCreate(int programHandle) {
super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale");
maxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(maxDistLocation, "inv_max_dist");
stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX");
GlUtils.checkLocation(stepSizeXLocation, "stepsizeX");
stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY");
GlUtils.checkLocation(stepSizeYLocation, "stepsizeY");
}
@Override
public void onDestroy() {
super.onDestroy();
scaleLocation = -1;
maxDistLocation = -1;
stepSizeXLocation = -1;
stepSizeYLocation = -1;
}
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
float[] scale = new float[2];
if (width > height) {
scale[0] = 1f;
scale[1] = ((float) height) / width;
} else {
scale[0] = ((float) width) / height;
scale[1] = 1f;
}
float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
GLES20.glUniform2fv(scaleLocation, 1, scale, 0);
GlUtils.checkError("glUniform2fv");
GLES20.glUniform1f(maxDistLocation, 1.0F / maxDist);
GlUtils.checkError("glUniform1f");
GLES20.glUniform1f(stepSizeXLocation, 1.0F / width);
GlUtils.checkError("glUniform1f");
GLES20.glUniform1f(stepSizeYLocation, 1.0F / height);
GlUtils.checkError("glUniform1f");
}
}

@ -1,12 +0,0 @@
package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull;
public class NoFilter extends Filter {
@NonNull
@Override
public String getFragmentShader() {
return mFragmentShader;
}
}

@ -2,31 +2,30 @@ package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
/** /**
* Applies Posterization effect to Preview. * Applies a posterization effect to the input frames.
*/ */
public class PosterizeFilter extends Filter { public class PosterizeFilter extends BaseFilter {
/**
* Initialize Effect private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
*/ + "precision mediump float;\n"
public PosterizeFilter() { + "uniform samplerExternalOES sTexture;\n"
} + "varying vec2 vTextureCoord;\n" + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 pcolor;\n"
+ " pcolor.r = (color.r >= 0.5) ? 0.75 : 0.25;\n"
+ " pcolor.g = (color.g >= 0.5) ? 0.75 : 0.25;\n"
+ " pcolor.b = (color.b >= 0.5) ? 0.75 : 0.25;\n"
+ " gl_FragColor = vec4(pcolor, color.a);\n"
+ "}\n";
public PosterizeFilter() { }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 pcolor;\n"
+ " pcolor.r = (color.r >= 0.5) ? 0.75 : 0.25;\n"
+ " pcolor.g = (color.g >= 0.5) ? 0.75 : 0.25;\n"
+ " pcolor.b = (color.b >= 0.5) ? 0.75 : 0.25;\n"
+ " gl_FragColor = vec4(pcolor, color.a);\n" + "}\n";
return shader;
} }
} }

@ -1,102 +1,124 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Adjusts color saturation of preview. * Adjusts color saturation.
*/ */
public class SaturationFilter extends Filter { public class SaturationFilter extends BaseFilter implements OneParameterFilter {
private float scale = 1.0f;
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float scale;\n"
+ "uniform vec3 exponents;\n"
+ "float shift;\n"
+ "vec3 weights;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " weights[0] = " + 2f / 8f + ";\n"
+ " weights[1] = " + 5f / 8f + ";\n"
+ " weights[2] = " + 1f / 8f + ";\n"
+ " shift = " + 1.0f / 255.0f + ";\n"
+ " vec4 oldcolor = texture2D(sTexture, vTextureCoord);\n"
+ " float kv = dot(oldcolor.rgb, weights) + shift;\n"
+ " vec3 new_color = scale * oldcolor.rgb + (1.0 - scale) * kv;\n"
+ " gl_FragColor = vec4(new_color, oldcolor.a);\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float de = dot(color.rgb, weights);\n"
+ " float inv_de = 1.0 / de;\n"
+ " vec3 verynew_color = de * pow(color.rgb * inv_de, exponents);\n"
+ " float max_color = max(max(max(verynew_color.r, verynew_color.g), verynew_color.b), 1.0);\n"
+ " gl_FragColor = gl_FragColor+vec4(verynew_color / max_color, color.a);\n"
+ "}\n";
private float scale = 1F; // -1...1
private int scaleLocation = -1;
private int exponentsLocation = -1;
public SaturationFilter() { }
/** /**
* Initialize Effect * Sets the saturation correction value:
* -1.0: fully desaturated, grayscale.
* 0.0: no change.
* +1.0: fully saturated.
*
* @param value new value
*/ */
public SaturationFilter() { @SuppressWarnings("WeakerAccess")
public void setSaturation(float value) {
if (value < -1F) value = -1F;
if (value > 1F) value = 1F;
scale = value;
} }
/** /**
* @param value Float, between 0.0 and 1. 0 means no change, while 0.0 indicates * Returns the current saturation.
* full desaturated, i.e. grayscale. *
* and 1.0 indicates full saturation * @see #setSaturation(float)
* @return saturation
*/ */
public void setSaturationValue(float value) { @SuppressWarnings("WeakerAccess")
if (value < 0.0f) public float getSaturation() {
value = 0.0f; return scale;
else if (value > 1.0f) }
value = 1.0f;
//since the shader excepts a range of -1.0 to 1.0 @Override
//will multiply it by 2.0f and subtract 1.0 to every value public void setParameter1(float value) {
this.scale = (2.0f * value) - 1.0f; setSaturation(2F * value - 1F);
} }
public float getSaturationValue() { @Override
//since the shader excepts a range of -1.0 to 1.0 public float getParameter1() {
//will add 1.0 to every value and divide it by 2.0f return (getSaturation() + 1F) / 2F;
return (scale + 1.0f) / 2.0f;
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float shift = 1.0f / 255.0f; return FRAGMENT_SHADER;
float weights[] = {2f / 8f, 5f / 8f, 1f / 8f}; }
float exponents[] = new float[3];
String weightsString[] = new String[3];
String exponentsString[] = new String[3];
exponentsString[0] = "";
exponentsString[1] = "";
exponentsString[2] = "";
String scaleString = "";
if (scale > 0.0f) {
exponents[0] = (0.9f * scale) + 1.0f;
exponents[1] = (2.1f * scale) + 1.0f;
exponents[2] = (2.7f * scale) + 1.0f;
exponentsString[0] = "exponents[0] = " + exponents[0] + ";\n";
exponentsString[1] = "exponents[1] = " + exponents[1] + ";\n";
exponentsString[2] = "exponents[2] = " + exponents[2] + ";\n";
} else
scaleString = "scale = " + (1.0f + scale) + ";\n";
weightsString[0] = "weights[0] = " + weights[0] + ";\n";
weightsString[1] = "weights[1] = " + weights[1] + ";\n";
weightsString[2] = "weights[2] = " + weights[2] + ";\n";
String shiftString = "shift = " + shift + ";\n";
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n" + " float scale;\n"
+ " float shift;\n" + " vec3 weights;\n" + " vec3 exponents;\n"
+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
// Parameters that were created above
+ weightsString[0]
+ weightsString[1]
+ weightsString[2]
+ shiftString
+ scaleString
+ " vec4 oldcolor = texture2D(sTexture, vTextureCoord);\n"
+ " float kv = dot(oldcolor.rgb, weights) + shift;\n"
+ " vec3 new_color = scale * oldcolor.rgb + (1.0 - scale) * kv;\n"
+ " gl_FragColor= vec4(new_color, oldcolor.a);\n"
// Parameters that were created above
+ weightsString[0]
+ weightsString[1]
+ weightsString[2]
+ exponentsString[0]
+ exponentsString[1]
+ exponentsString[2]
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float de = dot(color.rgb, weights);\n"
+ " float inv_de = 1.0 / de;\n"
+ " vec3 verynew_color = de * pow(color.rgb * inv_de, exponents);\n"
+ " float max_color = max(max(max(verynew_color.r, verynew_color.g), verynew_color.b), 1.0);\n"
+ " gl_FragColor = gl_FragColor+vec4(verynew_color / max_color, color.a);\n"
+ "}\n";
return shader; @Override
public void onCreate(int programHandle) {
super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(scaleLocation, "scale");
exponentsLocation = GLES20.glGetUniformLocation(programHandle, "exponents");
GlUtils.checkLocation(exponentsLocation, "exponents");
}
@Override
public void onDestroy() {
super.onDestroy();
scaleLocation = -1;
exponentsLocation = -1;
} }
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
if (scale > 0.0f) {
GLES20.glUniform1f(scaleLocation, 0F);
GlUtils.checkError("glUniform1f");
GLES20.glUniform3f(exponentsLocation,
(0.9f * scale) + 1.0f,
(2.1f * scale) + 1.0f,
(2.7f * scale) + 1.0f
);
GlUtils.checkError("glUniform3f");
} else {
GLES20.glUniform1f(scaleLocation, 1.0F + scale);
GlUtils.checkError("glUniform1f");
GLES20.glUniform3f(exponentsLocation, 0F, 0F, 0F);
GlUtils.checkError("glUniform3f");
}
}
} }

@ -2,47 +2,38 @@ package com.otaliastudios.cameraview.filters;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
/** /**
* Converts preview to Sepia tone. * Converts frames to sepia tone.
*/ */
public class SepiaFilter extends Filter { public class SepiaFilter extends BaseFilter {
/**
* Initialize Effect private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
*/ + "precision mediump float;\n"
public SepiaFilter() { + "uniform samplerExternalOES sTexture;\n"
} + "mat3 matrix;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " matrix[0][0]=" + 805.0f / 2048.0f + ";\n"
+ " matrix[0][1]=" + 715.0f / 2048.0f + ";\n"
+ " matrix[0][2]=" + 557.0f / 2048.0f + ";\n"
+ " matrix[1][0]=" + 1575.0f / 2048.0f + ";\n"
+ " matrix[1][1]=" + 1405.0f / 2048.0f + ";\n"
+ " matrix[1][2]=" + 1097.0f / 2048.0f + ";\n"
+ " matrix[2][0]=" + 387.0f / 2048.0f + ";\n"
+ " matrix[2][1]=" + 344.0f / 2048.0f + ";\n"
+ " matrix[2][2]=" + 268.0f / 2048.0f + ";\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 new_color = min(matrix * color.rgb, 1.0);\n"
+ " gl_FragColor = vec4(new_color.rgb, color.a);\n"
+ "}\n";
public SepiaFilter() { }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float weights[] = {805.0f / 2048.0f, 715.0f / 2048.0f, return FRAGMENT_SHADER;
557.0f / 2048.0f, 1575.0f / 2048.0f, 1405.0f / 2048.0f,
1097.0f / 2048.0f, 387.0f / 2048.0f, 344.0f / 2048.0f,
268.0f / 2048.0f};
String matrixString[] = new String[9];
matrixString[0] = " matrix[0][0]=" + weights[0] + ";\n";
matrixString[1] = " matrix[0][1]=" + weights[1] + ";\n";
matrixString[2] = " matrix[0][2]=" + weights[2] + ";\n";
matrixString[3] = " matrix[1][0]=" + weights[3] + ";\n";
matrixString[4] = " matrix[1][1]=" + weights[4] + ";\n";
matrixString[5] = " matrix[1][2]=" + weights[5] + ";\n";
matrixString[6] = " matrix[2][0]=" + weights[6] + ";\n";
matrixString[7] = " matrix[2][1]=" + weights[7] + ";\n";
matrixString[8] = " matrix[2][2]=" + weights[8] + ";\n";
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n" + " mat3 matrix;\n"
+ "varying vec2 vTextureCoord;\n" + "void main() {\n"
+ matrixString[0] + matrixString[1] + matrixString[2]
+ matrixString[3] + matrixString[4] + matrixString[5]
+ matrixString[6] + matrixString[7] + matrixString[8]
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 new_color = min(matrix * color.rgb, 1.0);\n"
+ " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n";
return shader;
} }
} }

@ -1,75 +1,128 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Sharpens the preview. * Sharpens the input frames.
*/ */
public class SharpnessFilter extends Filter { public class SharpnessFilter extends BaseFilter implements OneParameterFilter {
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float scale;\n"
+ "uniform float stepsizeX;\n"
+ "uniform float stepsizeY;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n"
+ " vec2 coord;\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n"
+ " coord.y = vTextureCoord.y - stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x - stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " gl_FragColor = vec4(color.rgb - 2.0 * scale * nbr_color, color.a);\n"
+ "}\n";
private float scale = 0.5f; private float scale = 0.5f;
private int width = 1;
private int height = 1;
private int scaleLocation = -1;
private int stepSizeXLocation = -1;
private int stepSizeYLocation = -1;
public SharpnessFilter() { }
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
this.width = width;
this.height = height;
}
/** /**
* Initialize Effect * Sets the current sharpness value:
* 0.0: no change.
* 1.0: maximum sharpness.
*
* @param value new sharpness
*/ */
public SharpnessFilter() { @SuppressWarnings("WeakerAccess")
public void setSharpness(float value) {
if (value < 0.0f) value = 0.0f;
if (value > 1.0f) value = 1.0f;
this.scale = value;
} }
/** /**
* @param value Float, between 0 and 1. 0 means no change. * Returns the current sharpness.
*
* @see #setSharpness(float)
* @return sharpness
*/ */
public void setSharpnessValue(float value) { @SuppressWarnings("WeakerAccess")
if (value < 0.0f) public float getSharpness() {
value = 0.0f; return scale;
else if (value > 1.0f) }
value = 1.0f;
this.scale = value; @Override
public void setParameter1(float value) {
setSharpness(value);
} }
public float getSharpnessValue() { @Override
return scale; public float getParameter1() {
return getSharpness();
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
}
String stepsizeXString = "stepsizeX = " + 1.0f / mPreviewingViewWidth + ";\n"; @Override
String stepsizeYString = "stepsizeY = " + 1.0f / mPreviewingViewHeight + ";\n"; public void onCreate(int programHandle) {
String scaleString = "scale = " + scale + ";\n"; super.onCreate(programHandle);
scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
String shader = "#extension GL_OES_EGL_image_external : require\n" GlUtils.checkLocation(scaleLocation, "scale");
+ "precision mediump float;\n" stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX");
+ "uniform samplerExternalOES sTexture;\n" GlUtils.checkLocation(stepSizeXLocation, "stepsizeX");
+ " float scale;\n" stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY");
+ " float stepsizeX;\n" GlUtils.checkLocation(stepSizeYLocation, "stepsizeY");
+ " float stepsizeY;\n" }
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
// Parameters that were created above
+ stepsizeXString
+ stepsizeYString
+ scaleString
+ " vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n"
+ " vec2 coord;\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " coord.x = vTextureCoord.x - 0.5 * stepsizeX;\n"
+ " coord.y = vTextureCoord.y - stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x - stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y - 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " coord.x = vTextureCoord.x + stepsizeX;\n"
+ " coord.y = vTextureCoord.y + 0.5 * stepsizeY;\n"
+ " nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
+ " gl_FragColor = vec4(color.rgb - 2.0 * scale * nbr_color, color.a);\n"
+ "}\n";
return shader;
@Override
public void onDestroy() {
super.onDestroy();
scaleLocation = -1;
stepSizeXLocation = -1;
stepSizeYLocation = -1;
} }
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
GLES20.glUniform1f(scaleLocation, scale);
GlUtils.checkError("glUniform1f");
GLES20.glUniform1f(stepSizeXLocation, 1.0F / width);
GlUtils.checkError("glUniform1f");
GLES20.glUniform1f(stepSizeYLocation, 1.0F / height);
GlUtils.checkError("glUniform1f");
}
} }

@ -1,61 +1,102 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Adjusts color temperature of the preview. * Adjusts color temperature.
*/ */
public class TemperatureFilter extends Filter { public class TemperatureFilter extends BaseFilter implements OneParameterFilter {
private float scale = 0f;
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float scale;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 new_color = color.rgb;\n"
+ " new_color.r = color.r + color.r * ( 1.0 - color.r) * scale;\n"
+ " new_color.b = color.b - color.b * ( 1.0 - color.b) * scale;\n"
+ " if (scale > 0.0) { \n"
+ " new_color.g = color.g + color.g * ( 1.0 - color.g) * scale * 0.25;\n"
+ " }\n"
+ " float max_value = max(new_color.r, max(new_color.g, new_color.b));\n"
+ " if (max_value > 1.0) { \n"
+ " new_color /= max_value;\n"
+ " } \n"
+ " gl_FragColor = vec4(new_color, color.a);\n"
+ "}\n";
private float scale = 1F; // -1...1
private int scaleLocation = -1;
public TemperatureFilter() { }
/** /**
* Initialize Effect * Sets the new temperature value:
* -1.0: cool colors
* 0.0: no change
* 1.0: warm colors
*
* @param value new value
*/ */
public TemperatureFilter() { @SuppressWarnings("WeakerAccess")
public void setTemperature(float value) {
if (value < -1F) value = -1F;
if (value > 1F) value = 1F;
this.scale = value;
} }
/** /**
* @param scale Float, between 0 and 1, with 0 indicating cool, and 1 * Returns the current temperature.
* indicating warm. A value of of 0.5 indicates no change. *
* @see #setTemperature(float)
* @return temperature
*/ */
public void setTemperatureScale(float scale) { @SuppressWarnings("WeakerAccess")
if (scale < 0.0f) public float getTemperature() {
scale = 0.0f; return scale;
else if (scale > 1.0f)
scale = 1.0f;
this.scale = scale;
} }
public float getTemperatureScale() { @Override
return scale; public void setParameter1(float value) {
setTemperature((2F * value - 1F));
}
@Override
public float getParameter1() {
return (getTemperature() + 1F) / 2F;
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
return FRAGMENT_SHADER;
}
String scaleString = "scale = " + (2.0f * scale - 1.0f) + ";\n"; @Override
public void onCreate(int programHandle) {
String shader = "#extension GL_OES_EGL_image_external : require\n" super.onCreate(programHandle);
+ "precision mediump float;\n" scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
+ "uniform samplerExternalOES sTexture;\n" GlUtils.checkLocation(scaleLocation, "scale");
+ " float scale;\n" }
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n" // Parameters that were created above
+ scaleString
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " vec3 new_color = color.rgb;\n"
+ " new_color.r = color.r + color.r * ( 1.0 - color.r) * scale;\n"
+ " new_color.b = color.b - color.b * ( 1.0 - color.b) * scale;\n"
+ " if (scale > 0.0) { \n"
+ " new_color.g = color.g + color.g * ( 1.0 - color.g) * scale * 0.25;\n"
+ " }\n"
+ " float max_value = max(new_color.r, max(new_color.g, new_color.b));\n"
+ " if (max_value > 1.0) { \n"
+ " new_color /= max_value;\n" + " } \n"
+ " gl_FragColor = vec4(new_color, color.a);\n" + "}\n";
return shader;
@Override
public void onDestroy() {
super.onDestroy();
scaleLocation = -1;
}
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
GLES20.glUniform1f(scaleLocation, scale);
GlUtils.checkError("glUniform1f");
} }
} }

@ -1,66 +1,101 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.graphics.Color; import android.graphics.Color;
import android.opengl.GLES20;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.OneParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Tints the preview with specified color.. * Tints the frames with specified color.
*/ */
public class TintFilter extends Filter { public class TintFilter extends BaseFilter implements OneParameterFilter {
private int mTint = 0xFFFF0000;
private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform vec3 tint;\n"
+ "vec3 color_ratio;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " color_ratio[0] = " + 0.21f + ";\n"
+ " color_ratio[1] = " + 0.71f + ";\n"
+ " color_ratio[2] = " + 0.07f + ";\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float avg_color = dot(color_ratio, color.rgb);\n"
+ " vec3 new_color = min(0.8 * avg_color + 0.2 * tint, 1.0);\n"
+ " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n";
private int tint = Color.RED;
private int tintLocation = -1;
public TintFilter() { }
/** /**
* Initialize Effect * Sets the current tint.
* @param color current tint
*/ */
public TintFilter() { @SuppressWarnings("WeakerAccess")
public void setTint(@ColorInt int color) {
this.tint = color;
} }
public void setTintColor(int color) { /**
this.mTint = color; * Returns the current tint.
*
* @see #setTint(int)
* @return tint
*/
@SuppressWarnings("WeakerAccess")
@ColorInt
public int getTint() {
return tint;
}
@Override
public void setParameter1(float value) {
// no easy way to transform 0...1 into a color.
setTint((int) (value * Integer.MAX_VALUE));
} }
public int getTintColor() { @Override
return mTint; public float getParameter1() {
return (float) getTint() / Integer.MAX_VALUE;
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float color_ratio[] = {0.21f, 0.71f, 0.07f}; return FRAGMENT_SHADER;
String color_ratioString[] = new String[3]; }
color_ratioString[0] = "color_ratio[0] = " + color_ratio[0] + ";\n";
color_ratioString[1] = "color_ratio[1] = " + color_ratio[1] + ";\n"; @Override
color_ratioString[2] = "color_ratio[2] = " + color_ratio[2] + ";\n"; public void onCreate(int programHandle) {
super.onCreate(programHandle);
float tint_color[] = {Color.red(mTint) / 255f, tintLocation = GLES20.glGetUniformLocation(programHandle, "tint");
Color.green(mTint) / 255f, Color.blue(mTint) / 255f}; GlUtils.checkLocation(tintLocation, "tint");
}
String tintString[] = new String[3];
tintString[0] = "tint[0] = " + tint_color[0] + ";\n";
tintString[1] = "tint[1] = " + tint_color[1] + ";\n";
tintString[2] = "tint[2] = " + tint_color[2] + ";\n";
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ " vec3 tint;\n"
+ " vec3 color_ratio;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
// Parameters that were created above
+ color_ratioString[0]
+ color_ratioString[1]
+ color_ratioString[2]
+ tintString[0]
+ tintString[1]
+ tintString[2]
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " float avg_color = dot(color_ratio, color.rgb);\n"
+ " vec3 new_color = min(0.8 * avg_color + 0.2 * tint, 1.0);\n"
+ " gl_FragColor = vec4(new_color.rgb, color.a);\n" + "}\n";
return shader;
@Override
public void onDestroy() {
super.onDestroy();
tintLocation = -1;
}
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
float[] channels = new float[]{
Color.red(tint) / 255f,
Color.green(tint) / 255f,
Color.blue(tint) / 255f
};
GLES20.glUniform3fv(tintLocation, 1, channels, 0);
GlUtils.checkError("glUniform3fv");
} }
} }

@ -1,99 +1,174 @@
package com.otaliastudios.cameraview.filters; package com.otaliastudios.cameraview.filters;
import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.filter.TwoParameterFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
/** /**
* Applies lomo-camera style effect to your preview. * Applies a vignette effect to input frames.
*/ */
public class VignetteFilter extends Filter { public class VignetteFilter extends BaseFilter implements TwoParameterFilter {
private float mScale = 0.85f;
private float mShade = 0.5f; private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ "uniform float range;\n"
+ "uniform float inv_max_dist;\n"
+ "uniform float shade;\n"
+ "uniform vec2 scale;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
+ " const float slope = 20.0;\n"
+ " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n"
+ " float dist = length(coord * scale);\n"
+ " float lumen = shade / (1.0 + exp((dist * inv_max_dist - range) * slope)) + (1.0 - shade);\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " gl_FragColor = vec4(color.rgb * lumen, color.a);\n"
+ "}\n";
private float mScale = 0.85f; // 0...1
private float mShade = 0.5f; // 0...1
private int mWidth = 1;
private int mHeight = 1;
private int mRangeLocation = -1;
private int mMaxDistLocation = -1;
private int mShadeLocation = -1;
private int mScaleLocation = -1;
public VignetteFilter() { }
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
mWidth = width;
mHeight = height;
}
/** /**
* Initialize Effect * Sets the vignette effect scale (0.0 - 1.0).
* @param scale new scale
*/ */
public VignetteFilter() { @SuppressWarnings("WeakerAccess")
public void setVignetteScale(float scale) {
if (scale < 0.0f) scale = 0.0f;
if (scale > 1.0f) scale = 1.0f;
mScale = scale;
} }
/** /**
* setVignetteEffectScale * Sets the vignette effect shade (0.0 - 1.0).
* @param shade new shade
*/
@SuppressWarnings("WeakerAccess")
public void setVignetteShade(float shade) {
if (shade < 0.0f) shade = 0.0f;
if (shade > 1.0f) shade = 1.0f;
this.mShade = shade;
}
/**
* Gets the current vignette scale.
* *
* @param scale Float, between 0.0 and 1. 0 * @see #setVignetteScale(float)
* @return scale
*/ */
public void setVignetteEffectScale(float scale) { @SuppressWarnings("WeakerAccess")
if (scale < 0.0f) public float getVignetteScale() {
scale = 0.0f; return mScale;
else if (scale > 1.0f)
scale = 1.0f;
this.mScale = scale;
} }
/** /**
* setVignetteEffectShade * Gets the current vignette shade.
* *
* @param shade Float, between 0.0 and 1. 0 * @see #setVignetteShade(float)
* @return shade
*/ */
public void setVignetteEffectShade(float shade) { @SuppressWarnings("WeakerAccess")
if (shade < 0.0f) public float getVignetteShade() {
shade = 0.0f; return mShade;
else if (shade > 1.0f) }
shade = 1.0f;
this.mShade = shade;
@Override
public void setParameter1(float value) {
setVignetteScale(value);
}
@Override
public float getParameter1() {
return getVignetteScale();
}
@Override
public void setParameter2(float value) {
setVignetteShade(value);
}
@Override
public float getParameter2() {
return getVignetteShade();
} }
@NonNull @NonNull
@Override @Override
public String getFragmentShader() { public String getFragmentShader() {
float scale[] = new float[2]; return FRAGMENT_SHADER;
if (mPreviewingViewWidth > mPreviewingViewHeight) { }
@Override
public void onCreate(int programHandle) {
super.onCreate(programHandle);
mRangeLocation = GLES20.glGetUniformLocation(programHandle, "range");
GlUtils.checkLocation(mRangeLocation, "range");
mMaxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
GlUtils.checkLocation(mMaxDistLocation, "inv_max_dist");
mShadeLocation = GLES20.glGetUniformLocation(programHandle, "shade");
GlUtils.checkLocation(mShadeLocation, "shade");
mScaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
GlUtils.checkLocation(mScaleLocation, "scale");
}
@Override
public void onDestroy() {
super.onDestroy();
mRangeLocation = -1;
mMaxDistLocation = -1;
mShadeLocation = -1;
mScaleLocation = -1;
}
@Override
protected void onPreDraw(float[] transformMatrix) {
super.onPreDraw(transformMatrix);
float[] scale = new float[2];
if (mWidth > mHeight) {
scale[0] = 1f; scale[0] = 1f;
scale[1] = ((float) mPreviewingViewHeight) / mPreviewingViewWidth; scale[1] = ((float) mHeight) / mWidth;
} else { } else {
scale[0] = ((float) mPreviewingViewWidth) / mPreviewingViewHeight; scale[0] = ((float) mWidth) / mHeight;
scale[1] = 1f; scale[1] = 1f;
} }
float max_dist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] GLES20.glUniform2fv(mScaleLocation, 1, scale, 0);
* scale[1])) * 0.5f; GlUtils.checkError("glUniform2fv");
String scaleString[] = new String[2]; float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
GLES20.glUniform1f(mMaxDistLocation, 1F / maxDist);
GlUtils.checkError("glUniform1f");
scaleString[0] = "scale[0] = " + scale[0] + ";\n"; GLES20.glUniform1f(mShadeLocation, mShade);
scaleString[1] = "scale[1] = " + scale[1] + ";\n"; GlUtils.checkError("glUniform1f");
String inv_max_distString = "inv_max_dist = " + 1.0f / max_dist + ";\n";
String shadeString = "shade = " + mShade + ";\n";
// The 'range' is between 1.3 to 0.6. When scale is zero then range is // The 'range' is between 1.3 to 0.6. When scale is zero then range is 1.3
// 1.3
// which means no vignette at all because the luminousity difference is // which means no vignette at all because the luminousity difference is
// less than 1/256 and will cause nothing. // less than 1/256 and will cause nothing.
String rangeString = "range = " float range = (1.30f - (float) Math.sqrt(mScale) * 0.7f);
+ (1.30f - (float) Math.sqrt(mScale) * 0.7f) + ";\n"; GLES20.glUniform1f(mRangeLocation, range);
GlUtils.checkError("glUniform1f");
String shader = "#extension GL_OES_EGL_image_external : require\n"
+ "precision mediump float;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ " float range;\n"
+ " float inv_max_dist;\n"
+ " float shade;\n"
+ " vec2 scale;\n"
+ "varying vec2 vTextureCoord;\n"
+ "void main() {\n"
// Parameters that were created above
+ scaleString[0]
+ scaleString[1]
+ inv_max_distString
+ shadeString
+ rangeString
+ " const float slope = 20.0;\n"
+ " vec2 coord = vTextureCoord - vec2(0.5, 0.5);\n"
+ " float dist = length(coord * scale);\n"
+ " float lumen = shade / (1.0 + exp((dist * inv_max_dist - range) * slope)) + (1.0 - shade);\n"
+ " vec4 color = texture2D(sTexture, vTextureCoord);\n"
+ " gl_FragColor = vec4(color.rgb * lumen, color.a);\n"
+ "}\n";
return shader;
} }
} }

@ -1,27 +1,29 @@
package com.otaliastudios.cameraview.internal.egl; package com.otaliastudios.cameraview.internal;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.opengl.Matrix; import android.opengl.Matrix;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
class EglElement { public class GlUtils {
private final static CameraLogger LOG = CameraLogger.create(EglElement.class.getSimpleName()); private final static String TAG = GlUtils.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
// Identity matrix for general use. // Identity matrix for general use.
protected static final float[] IDENTITY_MATRIX = new float[16]; public static final float[] IDENTITY_MATRIX = new float[16];
static { static {
Matrix.setIdentityM(IDENTITY_MATRIX, 0); Matrix.setIdentityM(IDENTITY_MATRIX, 0);
} }
// Check for GLES errors. public static void checkError(@NonNull String opName) {
protected static void check(String opName) {
int error = GLES20.glGetError(); int error = GLES20.glGetError();
if (error != GLES20.GL_NO_ERROR) { if (error != GLES20.GL_NO_ERROR) {
String message = LOG.e("Error during", opName, "glError 0x", Integer.toHexString(error)); String message = LOG.e("Error during", opName, "glError 0x", Integer.toHexString(error));
@ -29,18 +31,17 @@ class EglElement {
} }
} }
// Check for valid location. public static void checkLocation(int location, @NonNull String name) {
protected static void checkLocation(int location, String label) {
if (location < 0) { if (location < 0) {
String message = LOG.e("Unable to locate", label, "in program"); String message = LOG.e("Unable to locate", name, "in program");
throw new RuntimeException(message); throw new RuntimeException(message);
} }
} }
// Compiles the given shader, returns a handle. // Compiles the given shader, returns a handle.
protected static int loadShader(int shaderType, String source) { @SuppressWarnings("WeakerAccess")
public static int loadShader(int shaderType, @NonNull String source) {
int shader = GLES20.glCreateShader(shaderType); int shader = GLES20.glCreateShader(shaderType);
check("glCreateShader type=" + shaderType); checkError("glCreateShader type=" + shaderType);
GLES20.glShaderSource(shader, source); GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader); GLES20.glCompileShader(shader);
int[] compiled = new int[1]; int[] compiled = new int[1];
@ -54,21 +55,21 @@ class EglElement {
} }
// Creates a program with given vertex shader and pixel shader. // Creates a program with given vertex shader and pixel shader.
protected static int createProgram(String vertexSource, String fragmentSource) { public static int createProgram(@NonNull String vertexSource, @NonNull String fragmentSource) {
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) return 0; if (vertexShader == 0) return 0;
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) return 0; if (pixelShader == 0) return 0;
int program = GLES20.glCreateProgram(); int program = GLES20.glCreateProgram();
check("glCreateProgram"); checkError("glCreateProgram");
if (program == 0) { if (program == 0) {
LOG.e("Could not create program"); LOG.e("Could not create program");
} }
GLES20.glAttachShader(program, vertexShader); GLES20.glAttachShader(program, vertexShader);
check("glAttachShader"); checkError("glAttachShader");
GLES20.glAttachShader(program, pixelShader); GLES20.glAttachShader(program, pixelShader);
check("glAttachShader"); checkError("glAttachShader");
GLES20.glLinkProgram(program); GLES20.glLinkProgram(program);
int[] linkStatus = new int[1]; int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
@ -81,7 +82,7 @@ class EglElement {
} }
// Allocates a direct float buffer, and populates it with the float array data. // Allocates a direct float buffer, and populates it with the float array data.
protected static FloatBuffer floatBuffer(float[] coords) { public static FloatBuffer floatBuffer(@NonNull float[] coords) {
// Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it. // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * 4); ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * 4);
bb.order(ByteOrder.nativeOrder()); bb.order(ByteOrder.nativeOrder());

@ -25,6 +25,7 @@ import androidx.annotation.RequiresApi;
import android.util.Log; import android.util.Log;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.GlUtils;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -40,7 +41,7 @@ import java.nio.ByteOrder;
* There can be multiple surfaces associated with a single context. * There can be multiple surfaces associated with a single context.
*/ */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class EglBaseSurface extends EglElement { public class EglBaseSurface {
protected static final String TAG = EglBaseSurface.class.getSimpleName(); protected static final String TAG = EglBaseSurface.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG); private final static CameraLogger LOG = CameraLogger.create(TAG);
@ -187,7 +188,7 @@ public class EglBaseSurface extends EglElement {
buf.order(ByteOrder.LITTLE_ENDIAN); buf.order(ByteOrder.LITTLE_ENDIAN);
GLES20.glReadPixels(0, 0, width, height, GLES20.glReadPixels(0, 0, width, height,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
check("glReadPixels"); GlUtils.checkError("glReadPixels");
buf.rewind(); buf.rewind();
BufferedOutputStream bos = null; BufferedOutputStream bos = null;
@ -229,7 +230,7 @@ public class EglBaseSurface extends EglElement {
ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4); ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4);
buf.order(ByteOrder.LITTLE_ENDIAN); buf.order(ByteOrder.LITTLE_ENDIAN);
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
check("glReadPixels"); GlUtils.checkError("glReadPixels");
buf.rewind(); buf.rewind();
ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.array().length); ByteArrayOutputStream bos = new ByteArrayOutputStream(buf.array().length);

@ -7,186 +7,89 @@ import android.opengl.GLES20;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.filters.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filters.NoFilter; import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.internal.GlUtils;
import java.nio.FloatBuffer;
/** public class EglViewport {
* This is a mix of 3 grafika classes, FullFrameRect, Texture2dProgram, Drawable2d.
*/
public class EglViewport extends EglElement {
private final static CameraLogger LOG = CameraLogger.create(EglViewport.class.getSimpleName()); private final static CameraLogger LOG = CameraLogger.create(EglViewport.class.getSimpleName());
// Stuff from Drawable2d.FULL_RECTANGLE private int mProgramHandle = -1;
// A full square, extending from -1 to +1 in both dimensions.
// When the model/view/projection matrix is identity, this will exactly cover the viewport.
private static final float[] FULL_RECTANGLE_COORDS = {
-1.0f, -1.0f, // 0 bottom left
1.0f, -1.0f, // 1 bottom right
-1.0f, 1.0f, // 2 top left
1.0f, 1.0f, // 3 top right
};
// Stuff from Drawable2d.FULL_RECTANGLE
// A full square, extending from -1 to +1 in both dimensions.
private static final float[] FULL_RECTANGLE_TEX_COORDS = {
0.0f, 0.0f, // 0 bottom left
1.0f, 0.0f, // 1 bottom right
0.0f, 1.0f, // 2 top left
1.0f, 1.0f // 3 top right
};
// Stuff from Drawable2d.FULL_RECTANGLE
private final static int VERTEX_COUNT = FULL_RECTANGLE_COORDS.length / 2;
private FloatBuffer mVertexCoordinatesArray = floatBuffer(FULL_RECTANGLE_COORDS);
private FloatBuffer mTextureCoordinatesArray = floatBuffer(FULL_RECTANGLE_TEX_COORDS);
// Stuff from Texture2dProgram
private int mProgramHandle;
private int mTextureTarget; private int mTextureTarget;
private int mTextureUnit; private int mTextureUnit;
// Program attributes private Filter mFilter;
private int muMVPMatrixLocation; private boolean mFilterChanged = false;
private int muTexMatrixLocation;
private int maPositionLocation;
private int maTextureCoordLocation;
// private int muKernelLoc; // Used for filtering
// private int muTexOffsetLoc; // Used for filtering
// private int muColorAdjustLoc; // Used for filtering
private Filter mShaderEffect;
private boolean mIsShaderChanged = false;
public EglViewport() { public EglViewport() {
mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; this(new NoFilter());
mTextureUnit = GLES20.GL_TEXTURE0;
//init the default shader effect
mShaderEffect = new NoFilter();
initProgram();
} }
private void initProgram() { public EglViewport(@NonNull Filter filter) {
release(); mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
mProgramHandle = createProgram(mShaderEffect.getVertexShader(), mShaderEffect.getFragmentShader()); mTextureUnit = GLES20.GL_TEXTURE0;
maPositionLocation = GLES20.glGetAttribLocation(mProgramHandle, mShaderEffect.getPositionVariableName()); mFilter = filter;
checkLocation(maPositionLocation, mShaderEffect.getPositionVariableName()); createProgram();
maTextureCoordLocation = GLES20.glGetAttribLocation(mProgramHandle, mShaderEffect.getTexttureCoordinateVariableName());
checkLocation(maTextureCoordLocation, mShaderEffect.getTexttureCoordinateVariableName());
muMVPMatrixLocation = GLES20.glGetUniformLocation(mProgramHandle, mShaderEffect.getMVPMatrixVariableName());
checkLocation(muMVPMatrixLocation, mShaderEffect.getMVPMatrixVariableName());
muTexMatrixLocation = GLES20.glGetUniformLocation(mProgramHandle, mShaderEffect.getTextureMatrixVariableName());
checkLocation(muTexMatrixLocation, mShaderEffect.getTextureMatrixVariableName());
} }
public void release(boolean doEglCleanup) { private void createProgram() {
if (doEglCleanup) GLES20.glDeleteProgram(mProgramHandle); release(); // Release old program if present.
mProgramHandle = -1; mProgramHandle = GlUtils.createProgram(mFilter.getVertexShader(), mFilter.getFragmentShader());
mFilter.onCreate(mProgramHandle);
} }
public void release() { public void release() {
release(true); if (mProgramHandle != -1) {
mFilter.onDestroy();
GLES20.glDeleteProgram(mProgramHandle);
mProgramHandle = -1;
}
} }
public int createTexture() { public int createTexture() {
int[] textures = new int[1]; int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0); GLES20.glGenTextures(1, textures, 0);
check("glGenTextures"); GlUtils.checkError("glGenTextures");
int texId = textures[0]; int texId = textures[0];
GLES20.glActiveTexture(mTextureUnit); GLES20.glActiveTexture(mTextureUnit);
GLES20.glBindTexture(mTextureTarget, texId); GLES20.glBindTexture(mTextureTarget, texId);
check("glBindTexture " + texId); GlUtils.checkError("glBindTexture " + texId);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
check("glTexParameter"); GlUtils.checkError("glTexParameter");
return texId; return texId;
} }
public void changeShaderFilter(@NonNull Filter shaderEffect){ public void setFilter(@NonNull Filter filter){
this.mShaderEffect = shaderEffect; this.mFilter = filter;
mIsShaderChanged = true; mFilterChanged = true;
} }
public void drawFrame(int textureId, float[] textureMatrix) { public void drawFrame(int textureId, float[] textureMatrix) {
drawFrame(textureId, textureMatrix, if (mFilterChanged) {
mVertexCoordinatesArray, createProgram();
mTextureCoordinatesArray); mFilterChanged = false;
}
/**
* The issue with the CIRCLE shader is that if the textureMatrix has a scale value,
* it fails miserably, not taking the scale into account.
* So what we can do here is
*
* - read textureMatrix scaleX and scaleY values. This is pretty much impossible to do from the matrix itself
* without making risky assumptions over the order of operations.
* https://www.opengl.org/discussion_boards/showthread.php/159215-Is-it-possible-to-extract-rotation-translation-scale-given-a-matrix
* So we prefer passing scaleX and scaleY here to the draw function.
* - pass these values to the vertex shader
* - pass them to the fragment shader
* - in the fragment shader, take this scale value into account
*/
private void drawFrame(int textureId, float[] textureMatrix,
FloatBuffer vertexBuffer,
FloatBuffer texBuffer) {
if (mIsShaderChanged){
initProgram();
mIsShaderChanged = false;
} }
check("draw start"); GlUtils.checkError("draw start");
// Select the program. // Select the program and the active texture.
GLES20.glUseProgram(mProgramHandle); GLES20.glUseProgram(mProgramHandle);
check("glUseProgram"); GlUtils.checkError("glUseProgram");
// Set the texture.
GLES20.glActiveTexture(mTextureUnit); GLES20.glActiveTexture(mTextureUnit);
GLES20.glBindTexture(mTextureTarget, textureId); GLES20.glBindTexture(mTextureTarget, textureId);
// Copy the model / view / projection matrix over. // Draw.
GLES20.glUniformMatrix4fv(muMVPMatrixLocation, 1, false, IDENTITY_MATRIX, 0); mFilter.draw(textureMatrix);
check("glUniformMatrix4fv");
// Copy the texture transformation matrix over.
GLES20.glUniformMatrix4fv(muTexMatrixLocation, 1, false, textureMatrix, 0);
check("glUniformMatrix4fv");
// Enable the "aPosition" vertex attribute.
// Connect vertexBuffer to "aPosition".
GLES20.glEnableVertexAttribArray(maPositionLocation);
check("glEnableVertexAttribArray");
GLES20.glVertexAttribPointer(maPositionLocation, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);
check("glVertexAttribPointer");
// Enable the "aTextureCoord" vertex attribute.
// Connect texBuffer to "aTextureCoord".
GLES20.glEnableVertexAttribArray(maTextureCoordLocation);
check("glEnableVertexAttribArray");
GLES20.glVertexAttribPointer(maTextureCoordLocation, 2, GLES20.GL_FLOAT, false, 8, texBuffer);
check("glVertexAttribPointer");
// Draw the rect.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT);
check("glDrawArrays");
// Done -- disable vertex array, texture, and program. // Release.
GLES20.glDisableVertexAttribArray(maPositionLocation);
GLES20.glDisableVertexAttribArray(maTextureCoordLocation);
GLES20.glBindTexture(mTextureTarget, 0); GLES20.glBindTexture(mTextureTarget, 0);
GLES20.glUseProgram(0); GLES20.glUseProgram(0);
} }

@ -26,7 +26,7 @@ import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread; import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.filters.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
@ -97,15 +97,14 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
@RendererThread @RendererThread
@Override @Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY, Filter shaderEffect) { public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) {
mPreview.removeRendererFrameCallback(this); mPreview.removeRendererFrameCallback(this);
SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY, shaderEffect); SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY);
} }
@Override @Override
public void public void onFilterChanged(@NonNull Filter filter) {
onFilterChanged(@NonNull Filter filter) { mViewport.setFilter(filter.copy());
mViewport.changeShaderFilter(filter);
} }
}); });
} }
@ -151,7 +150,7 @@ public class SnapshotGlPictureRecorder extends PictureRecorder {
*/ */
@RendererThread @RendererThread
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
private void onRendererFrame(final @NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY, @NonNull Filter filter) { private void onRendererFrame(final @NonNull SurfaceTexture surfaceTexture, final float scaleX, final float scaleY) {
// Get egl context from the RendererThread, which is the one in which we have created // Get egl context from the RendererThread, which is the one in which we have created
// the textureId and the overlayTextureId, managed by the GlSurfaceView. // the textureId and the overlayTextureId, managed by the GlSurfaceView.
// Next operations can then be performed on different threads using this handle. // Next operations can then be performed on different threads using this handle.

@ -0,0 +1,39 @@
package com.otaliastudios.cameraview.preview;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filter.Filter;
/**
* A preview that support GL filters defined through the {@link Filter} interface.
*
* The preview has the responsibility of calling {@link Filter#setSize(int, int)}
* whenever the preview size changes and as soon as the filter is applied.
*/
public abstract class FilterCameraPreview<T extends View, Output> extends CameraPreview<T, Output> {
@SuppressWarnings("WeakerAccess")
public FilterCameraPreview(@NonNull Context context, @NonNull ViewGroup parent) {
super(context, parent);
}
/**
* Sets a new filter.
* @param filter new filter
*/
public abstract void setFilter(@NonNull Filter filter);
/**
* Returns the currently used filter.
* @return currently used filter
*/
@SuppressWarnings("unused")
@NonNull
public abstract Filter getCurrentFilter();
}

@ -15,8 +15,8 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.R; import com.otaliastudios.cameraview.R;
import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.utils.Op; import com.otaliastudios.cameraview.internal.utils.Op;
import com.otaliastudios.cameraview.filters.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.filters.NoFilter; import com.otaliastudios.cameraview.filter.NoFilter;
import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.AspectRatio;
import java.util.Collections; import java.util.Collections;
@ -58,7 +58,7 @@ import javax.microedition.khronos.opengles.GL10;
* Callbacks are guaranteed to be called on the renderer thread, which means that we can fetch * Callbacks are guaranteed to be called on the renderer thread, which means that we can fetch
* the GL context that was created and is managed by the {@link GLSurfaceView}. * the GL context that was created and is managed by the {@link GLSurfaceView}.
*/ */
public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture> { public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceTexture> {
private boolean mDispatched; private boolean mDispatched;
private final float[] mTransformMatrix = new float[16]; private final float[] mTransformMatrix = new float[16];
@ -69,8 +69,7 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
@VisibleForTesting float mCropScaleX = 1F; @VisibleForTesting float mCropScaleX = 1F;
@VisibleForTesting float mCropScaleY = 1F; @VisibleForTesting float mCropScaleY = 1F;
private View mRootView; private View mRootView;
private Filter mCurrentFilter;
private Filter mCurrentShaderEffect;
public GlCameraPreview(@NonNull Context context, @NonNull ViewGroup parent) { public GlCameraPreview(@NonNull Context context, @NonNull ViewGroup parent) {
super(context, parent); super(context, parent);
@ -142,7 +141,8 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
@RendererThread @RendererThread
@Override @Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) { public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mOutputViewport = new EglViewport(); if (mCurrentFilter == null) mCurrentFilter = new NoFilter();
mOutputViewport = new EglViewport(mCurrentFilter);
mOutputTextureId = mOutputViewport.createTexture(); mOutputTextureId = mOutputViewport.createTexture();
mInputSurfaceTexture = new SurfaceTexture(mOutputTextureId); mInputSurfaceTexture = new SurfaceTexture(mOutputTextureId);
getView().queueEvent(new Runnable() { getView().queueEvent(new Runnable() {
@ -165,15 +165,13 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
getView().requestRender(); // requestRender is thread-safe. getView().requestRender(); // requestRender is thread-safe.
} }
}); });
//init the default shader effect
mCurrentShaderEffect = new NoFilter();
} }
@RendererThread @RendererThread
@Override @Override
public void onSurfaceChanged(GL10 gl, final int width, final int height) { public void onSurfaceChanged(GL10 gl, final int width, final int height) {
gl.glViewport(0, 0, width, height); gl.glViewport(0, 0, width, height);
mCurrentFilter.setSize(width, height);
if (!mDispatched) { if (!mDispatched) {
dispatchOnSurfaceAvailable(width, height); dispatchOnSurfaceAvailable(width, height);
mDispatched = true; mDispatched = true;
@ -216,7 +214,7 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
synchronized (mRendererFrameCallbacks) { synchronized (mRendererFrameCallbacks) {
// Need to synchronize when iterating the Collections.synchronizedSet // Need to synchronize when iterating the Collections.synchronizedSet
for (RendererFrameCallback callback : mRendererFrameCallbacks) { for (RendererFrameCallback callback : mRendererFrameCallbacks) {
callback.onRendererFrame(mInputSurfaceTexture, mCropScaleX, mCropScaleY, mCurrentShaderEffect); callback.onRendererFrame(mInputSurfaceTexture, mCropScaleX, mCropScaleY);
} }
} }
} }
@ -284,7 +282,7 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
public void run() { public void run() {
mRendererFrameCallbacks.add(callback); mRendererFrameCallbacks.add(callback);
if (mOutputTextureId != 0) callback.onRendererTextureCreated(mOutputTextureId); if (mOutputTextureId != 0) callback.onRendererTextureCreated(mOutputTextureId);
callback.onFilterChanged(mCurrentShaderEffect); callback.onFilterChanged(mCurrentFilter);
} }
}); });
} }
@ -317,14 +315,30 @@ public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture
return new Renderer(); return new Renderer();
} }
public void setShaderEffect(@NonNull Filter shaderEffect){ //region Filters
shaderEffect.setPreviewingViewSize(getView().getWidth(), getView().getHeight());
mCurrentShaderEffect = shaderEffect;
@NonNull
@Override
public Filter getCurrentFilter() {
return mCurrentFilter;
}
mOutputViewport.changeShaderFilter(shaderEffect); @Override
public void setFilter(@NonNull Filter filter) {
if (hasSurface()) {
filter.setSize(mOutputSurfaceWidth, mOutputSurfaceHeight);
}
mCurrentFilter = filter;
if (mOutputViewport != null) {
mOutputViewport.setFilter(filter);
}
for (RendererFrameCallback callback : mRendererFrameCallbacks) { // Need to synchronize when iterating the Collections.synchronizedSet
callback.onFilterChanged(shaderEffect); synchronized (mRendererFrameCallbacks) {
for (RendererFrameCallback callback : mRendererFrameCallbacks) {
callback.onFilterChanged(filter);
}
} }
} }
} }

@ -4,7 +4,7 @@ import android.graphics.SurfaceTexture;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.otaliastudios.cameraview.filters.Filter; import com.otaliastudios.cameraview.filter.Filter;
/** /**
* Callback for renderer frames. * Callback for renderer frames.
@ -30,7 +30,7 @@ public interface RendererFrameCallback {
* @param scaleY the scaleY (in REF_VIEW) value * @param scaleY the scaleY (in REF_VIEW) value
*/ */
@RendererThread @RendererThread
void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY, Filter shaderEffect); void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY);
/** /**
* Called on the change of shader filter * Called on the change of shader filter

@ -13,7 +13,7 @@ import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.preview.GlCameraPreview; import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback; import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread; import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.filters.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.size.Size; import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.encoding.AudioConfig; import com.otaliastudios.cameraview.video.encoding.AudioConfig;
import com.otaliastudios.cameraview.video.encoding.AudioMediaEncoder; import com.otaliastudios.cameraview.video.encoding.AudioMediaEncoder;
@ -102,9 +102,17 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
} }
} }
@Override
public void onFilterChanged(@NonNull Filter filter) {
mCurrentFilter = filter.copy();
if (mEncoderEngine != null) {
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
}
}
@RendererThread @RendererThread
@Override @Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY, Filter shaderEffect) { public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY) {
if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) { if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) {
LOG.i("Starting the encoder engine."); LOG.i("Starting the encoder engine.");
@ -160,13 +168,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
// Engine // Engine
mEncoderEngine = new MediaEncoderEngine(mResult.file, videoEncoder, audioEncoder, mEncoderEngine = new MediaEncoderEngine(mResult.file, videoEncoder, audioEncoder,
mResult.maxDuration, mResult.maxSize, SnapshotVideoRecorder.this); mResult.maxDuration, mResult.maxSize, SnapshotVideoRecorder.this);
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
//set current filter
if (mEncoderEngine != null) {
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, mCurrentFilter);
}
mEncoderEngine.start(); mEncoderEngine.start();
mResult.rotation = 0; // We will rotate the result instead. mResult.rotation = 0; // We will rotate the result instead.
mCurrentState = STATE_RECORDING; mCurrentState = STATE_RECORDING;
@ -192,15 +194,6 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
} }
@Override
public void onFilterChanged(@NonNull Filter filter) {
mCurrentFilter = filter;
if (mEncoderEngine != null) {
mEncoderEngine.notify(TextureMediaEncoder.FILTER_EVENT, filter);
}
}
@Override @Override
public void onEncodingStart() { public void onEncodingStart() {
dispatchVideoRecordingStart(); dispatchVideoRecordingStart();

@ -9,7 +9,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger; import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.filters.Filter; import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.internal.egl.EglCore; import com.otaliastudios.cameraview.internal.egl.EglCore;
import com.otaliastudios.cameraview.internal.egl.EglViewport; import com.otaliastudios.cameraview.internal.egl.EglViewport;
import com.otaliastudios.cameraview.internal.egl.EglWindowSurface; import com.otaliastudios.cameraview.internal.egl.EglWindowSurface;
@ -31,7 +31,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
private EglCore mEglCore; private EglCore mEglCore;
private EglWindowSurface mWindow; private EglWindowSurface mWindow;
private EglViewport mViewport; private EglViewport mViewport;
private Pool<Frame> mFramePool = new Pool<>(100, new Pool.Factory<Frame>() { private Pool<Frame> mFramePool = new Pool<>(Integer.MAX_VALUE, new Pool.Factory<Frame>() {
@Override @Override
public Frame create() { public Frame create() {
return new Frame(); return new Frame();
@ -137,7 +137,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
if (event.equals(FILTER_EVENT)) { if (event.equals(FILTER_EVENT)) {
Filter filter = (Filter) data; Filter filter = (Filter) data;
mViewport.changeShaderFilter(filter); mViewport.setFilter(filter);
} else if (event.equals(FRAME_EVENT)) { } else if (event.equals(FRAME_EVENT)) {
Frame frame = (Frame) data; Frame frame = (Frame) data;
if (frame == null) { if (frame == null) {
@ -224,7 +224,7 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
mWindow = null; mWindow = null;
} }
if (mViewport != null) { if (mViewport != null) {
mViewport.release(true); mViewport.release();
mViewport = null; mViewport = null;
} }
if (mEglCore != null) { if (mEglCore != null) {

@ -131,6 +131,8 @@
<attr name="cameraAutoFocusMarker" format="string|reference"/> <attr name="cameraAutoFocusMarker" format="string|reference"/>
<attr name="cameraFilter" format="string|reference"/>
<attr name="cameraUseDeviceOrientation" format="boolean"/> <attr name="cameraUseDeviceOrientation" format="boolean"/>
<attr name="cameraExperimental" format="boolean" /> <attr name="cameraExperimental" format="boolean" />

@ -1,4 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="cameraview_default_autofocus_marker">com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker</string> <string name="cameraview_default_autofocus_marker">com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker</string>
<string name="cameraview_filter_none">com.otaliastudios.cameraview.filter.NoFilter</string>
<string name="cameraview_filter_autofix">com.otaliastudios.cameraview.filters.AutoFixFilter</string>
<string name="cameraview_filter_black_and_white">com.otaliastudios.cameraview.filters.BlackAndWhiteFilter</string>
<string name="cameraview_filter_brightness">com.otaliastudios.cameraview.filters.BrightnessFilter</string>
<string name="cameraview_filter_contrast">com.otaliastudios.cameraview.filters.ContrastFilter</string>
<string name="cameraview_filter_cross_process">com.otaliastudios.cameraview.filters.CrossProcessFilter</string>
<string name="cameraview_filter_documentary">com.otaliastudios.cameraview.filters.DocumentaryFilter</string>
<string name="cameraview_filter_duotone">com.otaliastudios.cameraview.filters.DuotoneFilter</string>
<string name="cameraview_filter_fill_light">com.otaliastudios.cameraview.filters.FillLightFilter</string>
<string name="cameraview_filter_gamma">com.otaliastudios.cameraview.filters.GammaFilter</string>
<string name="cameraview_filter_grain">com.otaliastudios.cameraview.filters.GrainFilter</string>
<string name="cameraview_filter_grayscale">com.otaliastudios.cameraview.filters.GrayscaleFilter</string>
<string name="cameraview_filter_hue">com.otaliastudios.cameraview.filters.HueFilter</string>
<string name="cameraview_filter_invert_colors">com.otaliastudios.cameraview.filters.InvertColorsFilter</string>
<string name="cameraview_filter_lomoish">com.otaliastudios.cameraview.filters.LomoishFilter</string>
<string name="cameraview_filter_posterize">com.otaliastudios.cameraview.filters.PosterizeFilter</string>
<string name="cameraview_filter_saturation">com.otaliastudios.cameraview.filters.SaturationFilter</string>
<string name="cameraview_filter_sepia">com.otaliastudios.cameraview.filters.SepiaFilter</string>
<string name="cameraview_filter_sharpness">com.otaliastudios.cameraview.filters.SharpnessFilter</string>
<string name="cameraview_filter_temperature">com.otaliastudios.cameraview.filters.TemperatureFilter</string>
<string name="cameraview_filter_tint">com.otaliastudios.cameraview.filters.TintFilter</string>
<string name="cameraview_filter_vignette">com.otaliastudios.cameraview.filters.VignetteFilter</string>
</resources> </resources>

@ -27,7 +27,8 @@ import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.Mode; import com.otaliastudios.cameraview.controls.Mode;
import com.otaliastudios.cameraview.VideoResult; import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Preview; import com.otaliastudios.cameraview.controls.Preview;
import com.otaliastudios.cameraview.filters.Filters; import com.otaliastudios.cameraview.filter.Filters;
import com.otaliastudios.cameraview.filters.BrightnessFilter;
import com.otaliastudios.cameraview.frame.Frame; import com.otaliastudios.cameraview.frame.Frame;
import com.otaliastudios.cameraview.frame.FrameProcessor; import com.otaliastudios.cameraview.frame.FrameProcessor;
@ -36,8 +37,6 @@ import java.io.File;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import static com.otaliastudios.cameraview.filters.Filters.*;
public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback { public class CameraActivity extends AppCompatActivity implements View.OnClickListener, OptionView.Callback {
@ -49,7 +48,8 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
private ViewGroup controlPanel; private ViewGroup controlPanel;
private long mCaptureTime; private long mCaptureTime;
private Filters mCurrentEffect = NO_FILTER; private int mCurrentFilter = 0;
private final Filters[] mAllFilters = Filters.values();
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -321,84 +321,17 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
} }
} }
private void changeCurrentFilter(){ private void changeCurrentFilter() {
if (camera.getPreview() != Preview.GL_SURFACE) { if (camera.getPreview() != Preview.GL_SURFACE) {
message("Filters are supported only for GLSurfaceView", true); message("Filters are supported only when preview is Preview.GL_SURFACE.", true);
return; return;
} }
if (mCurrentFilter < mAllFilters.length - 1) {
switch (mCurrentEffect){ mCurrentFilter++;
case NO_FILTER: } else {
mCurrentEffect = AUTO_FIX_FILTER; mCurrentFilter = 0;
break;
case AUTO_FIX_FILTER:
mCurrentEffect = BLACK_AND_WHITE_FILTER;
break;
case BLACK_AND_WHITE_FILTER:
mCurrentEffect = BRIGHTNESS_FILTER;
break;
case BRIGHTNESS_FILTER:
mCurrentEffect = CONTRAST_FILTER;
break;
case CONTRAST_FILTER:
mCurrentEffect = CROSS_PROCESS_FILTER;
break;
case CROSS_PROCESS_FILTER:
mCurrentEffect = DOCUMENTARY_FILTER;
break;
case DOCUMENTARY_FILTER:
mCurrentEffect = DUO_TONE_COLOR_FILTER;
break;
case DUO_TONE_COLOR_FILTER:
mCurrentEffect = FILL_LIGHT_FILTER;
break;
case FILL_LIGHT_FILTER:
mCurrentEffect = GAMMA_FILTER;
break;
case GAMMA_FILTER:
mCurrentEffect = GRAIN_FILTER;
break;
case GRAIN_FILTER:
mCurrentEffect = GREY_SCALE_FILTER;
break;
case GREY_SCALE_FILTER:
mCurrentEffect = HUE_FILTER;
break;
case HUE_FILTER:
mCurrentEffect = INVERT_COLOR_FILTER;
break;
case INVERT_COLOR_FILTER:
mCurrentEffect = LAMOISH_FILTER;
break;
case LAMOISH_FILTER:
mCurrentEffect = POSTERIZE_FILTER;
break;
case POSTERIZE_FILTER:
mCurrentEffect = SATURATION_FILTER;
break;
case SATURATION_FILTER:
mCurrentEffect = SEPIA_FILTER;
break;
case SEPIA_FILTER:
mCurrentEffect = SHARPNESS_FILTER;
break;
case SHARPNESS_FILTER:
mCurrentEffect = TEMPERATURE_FILTER;
break;
case TEMPERATURE_FILTER:
mCurrentEffect = TINT_FILTER;
break;
case TINT_FILTER:
mCurrentEffect = VIGNETTE_FILTER;
break;
case VIGNETTE_FILTER:
default:
mCurrentEffect = NO_FILTER;
break;
} }
camera.setFilter(mAllFilters[mCurrentFilter].newInstance());
camera.setFilter(mCurrentEffect);
} }
@Override @Override

@ -2,7 +2,7 @@
layout: page layout: page
title: "Debugging" title: "Debugging"
category: docs category: docs
order: 13 order: 14
date: 2018-12-20 20:02:38 date: 2018-12-20 20:02:38
disqus: 1 disqus: 1
--- ---

@ -2,7 +2,7 @@
layout: page layout: page
title: "Error Handling" title: "Error Handling"
category: docs category: docs
order: 12 order: 13
date: 2018-12-20 20:02:31 date: 2018-12-20 20:02:31
disqus: 1 disqus: 1
--- ---

@ -4,7 +4,7 @@ title: "More features"
subtitle: "Undocumented features & more" subtitle: "Undocumented features & more"
description: "Undocumented features & more" description: "Undocumented features & more"
category: docs category: docs
order: 14 order: 15
date: 2018-12-20 20:41:20 date: 2018-12-20 20:41:20
disqus: 1 disqus: 1
--- ---

@ -39,7 +39,7 @@ to use all the features available. However, experienced user might prefer a diff
|-------|---------|----| |-------|---------|----|
|`Preview.SURFACE`|A `SurfaceView`|Can be good for battery, but will not work well with dynamic layout changes and similar things. No support for video snapshots.| |`Preview.SURFACE`|A `SurfaceView`|Can be good for battery, but will not work well with dynamic layout changes and similar things. No support for video snapshots.|
|`Preview.TEXTURE`|A `TextureView`|Better. Requires hardware acceleration. No support for video snapshots.| |`Preview.TEXTURE`|A `TextureView`|Better. Requires hardware acceleration. No support for video snapshots.|
|`Preview.GL_SURFACE`|A `GLSurfaceView`|Recommended. Supports video snapshots. Might support GL real time filters in the future.| |`Preview.GL_SURFACE`|A `GLSurfaceView`|Recommended. Supports video snapshots. Supports [overlays](watermarks-and-overlays.html). Supports [real-time filters](filters.html).|
The GL surface, as an extra benefit, has a much more efficient way of capturing picture snapshots, The GL surface, as an extra benefit, has a much more efficient way of capturing picture snapshots,
that avoids OOM errors, rotating the image on the fly, reading EXIF, and other horrible things belonging to v1. that avoids OOM errors, rotating the image on the fly, reading EXIF, and other horrible things belonging to v1.

@ -4,7 +4,7 @@ title: "Runtime Permissions"
subtitle: "Permissions and Manifest setup" subtitle: "Permissions and Manifest setup"
description: "Permissions and Manifest setup" description: "Permissions and Manifest setup"
category: docs category: docs
order: 11 order: 12
date: 2018-12-20 20:03:03 date: 2018-12-20 20:03:03
disqus: 1 disqus: 1
--- ---

@ -0,0 +1,102 @@
---
layout: page
title: "Real-time Filters"
subtitle: "Apply filters to preview and snapshots"
description: "Apply filters to preview and snapshots"
category: docs
order: 11
date: 2019-08-06 17:10:17
disqus: 1
---
Starting from version `2.1.0`, CameraView experimentally supports real-time filters that can modify
the camera frames before they are shown and recorded. Just like [overlays](watermarks-and-overlays.html),
these filters are applied to the preview and to any [picture or video snapshots](capturing-media.html).
Conditions:
- you must set the experimental flag: `app:cameraExperimental="true"`
- you must use `Preview.GL_SURFACE` as a preview
### Simple usage
```xml
<com.otaliastudios.cameraview.CameraView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cameraFilter="@string/cameraview_filter_none"/>
```
Real-time filters are applied at creation time, through the `app:cameraFilter` XML attribute,
or anytime during the camera lifecycle using `cameraView.setFilter()`.
We offers a reasonable amount of filters through the `Filters` class, for example:
```java
cameraView.setFilter(Filters.BLACK_AND_WHITE.newInstance());
cameraView.setFilter(Filters.VIGNETTE.newInstance());
cameraView.setFilter(Filters.SEPIA.newInstance());
```
All of the filters stored in the `Filters` class have an XML string resource that you can use
to quickly setup the camera view. For example, `Filters.BLACK_AND_WHITE` can be set by using
`app:cameraFilter="@string/cameraview_filter_black_and_white`.
The default filter is called `NoFilter` (`Filters.NONE`) and can be used to clear up any other
filter that was previously set and return back to normal.
|Filter class|Filters value|XML resource value|
|------------|-------------|---------------------|
|`NoFilter`|`Filters.NONE`|`@string/cameraview_filter_none`|
|`AutoFixFilter`|`Filters.AUTO_FIX`|`@string/cameraview_filter_autofix`|
|`BlackAndWhiteFilter`|`Filters.BLACK_AND_WHITE`|`@string/cameraview_filter_black_and_white`|
|`BrightnessFilter`|`Filters.BRIGHTNESS`|`@string/cameraview_filter_brightness`|
|`ContrastFilter`|`Filters.CONTRAST`|`@string/cameraview_filter_contrast`|
|`CrossProcessFilter`|`Filters.CROSS_PROCESS`|`@string/cameraview_filter_cross_process`|
|`DocumentaryFilter`|`Filters.DOCUMENTARY`|`@string/cameraview_filter_documentary`|
|`DuotoneFilter`|`Filters.DUOTONE`|`@string/cameraview_filter_duotone`|
|`FillLightFilter`|`Filters.FILL_LIGHT`|`@string/cameraview_filter_fill_light`|
|`GammaFilter`|`Filters.GAMMA`|`@string/cameraview_filter_gamma`|
|`GrainFilter`|`Filters.GRAIN`|`@string/cameraview_filter_grain`|
|`GrayscaleFilter`|`Filters.GRAYSCALE`|`@string/cameraview_filter_grayscale`|
|`HueFilter`|`Filters.HUE`|`@string/cameraview_filter_hue`|
|`InvertColorsFilter`|`Filters.INVERT_COLORS`|`@string/cameraview_filter_invert_colors`|
|`LomoishFilter`|`Filters.LOMOISH`|`@string/cameraview_filter_lomoish`|
|`PosterizeFilter`|`Filters.POSTERIZE`|`@string/cameraview_filter_posterize`|
|`SaturationFilter`|`Filters.SATURATION`|`@string/cameraview_filter_saturation`|
|`SepiaFilter`|`Filters.SEPIA`|`@string/cameraview_filter_sepia`|
|`SharpnessFilter`|`Filters.SHARPNESS`|`@string/cameraview_filter_sharpness`|
|`TemperatureFilter`|`Filters.TEMPERATURE`|`@string/cameraview_filter_temperature`|
|`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
accept two colors to apply the duotone effect.
```java
duotoneFilter.setFirstColor(Color.RED);
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.
### Advanced usage
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.
We recommend:
- Subclassing `BaseFilter` instead of implementing `Filter`, since that takes care of most of the work
- If accepting parameters, implementing `OneParameterFilter` or `TwoParameterFilter` as well
Most of all, the best way of learning is by looking at the current filters implementations in the
`com.otaliastudios.cameraview.filters` package.
### Related APIs
|Method|Description|
|------|-----------|
|`setFilter(Filter)`|Sets a new real-time filter.|
|`getFilter()`|Returns the current real-time filter.|

@ -14,6 +14,7 @@ addressing most of the common issues and needs, and still leaving you with flexi
- Fast & reliable - Fast & reliable
- Gestures support [[docs]](docs/gestures.html) - Gestures support [[docs]](docs/gestures.html)
- Real-time filters [[docs]](docs/filters.html)
- Camera1 or Camera2 powered engine [[docs]](docs/previews.html) - Camera1 or Camera2 powered engine [[docs]](docs/previews.html)
- Frame processing support [[docs]](docs/frame-processing.html) - Frame processing support [[docs]](docs/frame-processing.html)
- Watermarks & animated overlays [[docs]](docs/watermarks-and-overlays.html) - Watermarks & animated overlays [[docs]](docs/watermarks-and-overlays.html)

Loading…
Cancel
Save