We will let the user have up to one channel mixing filter, one curve filter, and one convolution filter active at any time. For each filter category, we will provide a menu button that lets the user cycle through the available filters, or no filter.
Let's start by editing the relevant resource files to define the menu buttons and their text. We should add the following strings in res/values/strings.xml
:
<string name="menu_next_curve_filter">Next Curve</string> <string name="menu_next_mixer_filter">Next Mixer</string> <string name="menu_next_convolution_filter">Next Kernel</string>
Then, we should edit res/menu/activity_camera.xml
as follows:
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/menu_next_curve_filter" android:orderInCategory="100" android:showAsAction="ifRoom|withText" android:title="@string/menu_next_curve_filter" /> <item android:id="@+id/menu_next_mixer_filter" android:orderInCategory="100" android:showAsAction="ifRoom|withText" android:title="@string/menu_next_mixer_filter" /> <item android:id="@+id/menu_next_convolution_filter" android:orderInCategory="100" android:showAsAction="ifRoom|withText" android:title="@string/menu_next_convolution_filter" /> <item android:id="@+id/menu_next_camera" android:orderInCategory="100" android:showAsAction="ifRoom|withText" android:title="@string/menu_next_camera" /> <item android:id="@+id/menu_take_photo" android:orderInCategory="100" android:showAsAction="always|withText" android:title="@string/menu_take_photo" /> </menu>
To store the information about the available and selected filters, we need several new variables in CameraActivity
. The available filters are just Filter[]
arrays. The indices of the selected filters are stored in the same way as the index of the selected camera device, that is, by serializing and deserializing (saving and restoring) an integer to/from an Android Bundle
object. The following are the variable declarations that we must add to CameraActivity
:
// Keys for storing the indices of the active filters. private static final String STATE_CURVE_FILTER_INDEX = "curveFilterIndex"; private static final String STATE_MIXER_FILTER_INDEX = "mixerFilterIndex"; private static final String STATE_CONVOLUTION_FILTER_INDEX = "convolutionFilterIndex"; // The filters. private Filter[] mCurveFilters; private Filter[] mMixerFilters; private Filter[] mConvolutionFilters; // The indices of the active filters. private int mCurveFilterIndex; private int mMixerFilterIndex; private int mConvolutionFilterIndex;
Since our Filter
implementations rely on classes in OpenCV they cannot be instantiated until the OpenCV library is loaded. Thus, our BaseLoaderCallback
object is responsible for initializing the Filter[]
arrays. We should edit it as follows:
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) { @Override public void onManagerConnected(final int status) { switch (status) { case LoaderCallbackInterface.SUCCESS: Log.d(TAG, "OpenCV loaded successfully"); mCameraView.enableView(); mBgr = new Mat(); mCurveFilters = new Filter[] { new NoneFilter(), new PortraCurveFilter(), new ProviaCurveFilter(), new VelviaCurveFilter(), new CrossProcessCurveFilter() }; mMixerFilters = new Filter[] { new NoneFilter(), new RecolorRCFilter(), new RecolorRGVFilter(), new RecolorCMVFilter() }; mConvolutionFilters = new Filter[] { new NoneFilter(), new StrokeEdgesFilter() }; break; default: super.onManagerConnected(status); break; } } };
The onCreate
method can initialize the selected filter indices or load them from the savedInstanceState
argument. Let's edit the method as follows:
protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Window window = getWindow(); window.addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); if (savedInstanceState != null) { mCameraIndex = savedInstanceState.getInt( STATE_CAMERA_INDEX, 0); mCurveFilterIndex = savedInstanceState.getInt( STATE_CURVE_FILTER_INDEX, 0); mMixerFilterIndex = savedInstanceState.getInt( STATE_MIXER_FILTER_INDEX, 0); mConvolutionFilterIndex = savedInstanceState.getInt( STATE_CONVOLUTION_FILTER_INDEX, 0); } else { mCameraIndex = 0; mCurveFilterIndex = 0; mMixerFilterIndex = 0; mConvolutionFilterIndex = 0; } // ... }
Similarly, the
onSaveInstanceState
method should save the selected filter indices to the savedInstanceState
argument. Let's edit the method as follows:
public void onSaveInstanceState(Bundle savedInstanceState) { // Save the current camera index. savedInstanceState.putInt(STATE_CAMERA_INDEX, mCameraIndex); // Save the current filter indices. savedInstanceState.putInt(STATE_CURVE_FILTER_INDEX, mCurveFilterIndex); savedInstanceState.putInt(STATE_MIXER_FILTER_INDEX, mMixerFilterIndex); savedInstanceState.putInt(STATE_CONVOLUTION_FILTER_INDEX, mConvolutionFilterIndex); super.onSaveInstanceState(savedInstanceState); }
To make each of the new menu items functional, we just need to add some boilerplate code that updates the relevant filter index. Let's edit the onOptionsItemSelected
method as follows:
public boolean onOptionsItemSelected(final MenuItem item) { if (mIsMenuLocked) { return true; } switch (item.getItemId()) { case R.id.menu_next_curve_filter: mCurveFilterIndex++; if (mCurveFilterIndex == mCurveFilters.length) { mCurveFilterIndex = 0; } return true; case R.id.menu_next_mixer_filter: mMixerFilterIndex++; if (mMixerFilterIndex == mMixerFilters.length) { mMixerFilterIndex = 0; } return true; case R.id.menu_next_convolution_filter: mConvolutionFilterIndex++; if (mConvolutionFilterIndex == mConvolutionFilters.length) { mConvolutionFilterIndex = 0; } return true; // ... default: return super.onOptionsItemSelected(item); } }
Now, in the onCameraFrame
callback method, we should apply each selected filter to the image. The following is the new implementation:
public Mat onCameraFrame(final CvCameraViewFrame inputFrame) { final Mat rgba = inputFrame.rgba(); // Apply the active filters. mCurveFilters[mCurveFilterIndex].apply(rgba, rgba); mMixerFilters[mMixerFilterIndex].apply(rgba, rgba); mConvolutionFilters[mConvolutionFilterIndex].apply( rgba, rgba); if (mIsPhotoPending) { mIsPhotoPending = false; takePhoto(rgba); } if (mIsCameraFrontFacing) { // Mirror (horizontally flip) the preview. Core.flip(rgba, rgba, 1); } return rgba; }
That's all! Run the app, select filters, take some photos, and share them. As an example of how the app should look, here is a screenshot with RecolorRCFilter
and StrokeEdgesFilter
enabled: