เป็นการหยิบ Feature ส่วนของ Computer Vision ใน OpenCV มาใช้กับการพัฒนาแอพพลิเคชันบนระบบปฏิบัติการ Android เพื่อ Censor ภาพด้วย Pixels สำหรับผู้เริ่มต้นครับ
ก่อนจะเข้ามาทดลองทำ Labs นี้ผมแนะนำให้ลองศึกษาบทเรียนก่อนหน้านี้ก่อนครับ
อันที่จริงตัวอย่างนี้ก็อยู่ใน Sample Project ของ OpenCV for Android อยู่แล้วครับ แต่เราจะหยิบมาใช้สำหรับการพัฒนาแอพพลิเคชันใหม่ตั้งแต่ New Project กันเลยครับ
ก่อนอื่นเปิด ADT, eClipse ขึ้นมาครับ ทำการ New Android Project ขึ้นมาครับ
ตั้งค่าให้เรียบร้อย icon แอพฯ ต่างๆ
คลิกขวาที่ Project ของเราครับ เลือก Properties แล้วไปที่ Android
กดที่ปุ่ม Add.. ในช่อง Library แล้วทำการเลือก OpenCV (ตัวอย่างคือเวอร์ชัน 3.0.0)
เมื่อเสร็จแล้วดังรูปตัวอย่างข้างล่างก็กด Apply
เราจะสามารถเรียกใช้ Library ของ OpenCV ได้แล้วครับโดยที่ Project ไม่ต้องอยู่ Path เดียวกันกับ Library ก็ได้
ไปที่ Layout ของ Android ครับที่ res/activity_main.xml
แก้ไข XML ให้เป็นตามตัวอย่างนี้ครับ
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <org.opencv.android.JavaCameraView android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/main_activity" /> </LinearLayout>
เป็นการเรียก opencv ส่วนของ CameraView ที่เป็นพื้นฐานของ Java บน OpenCV มาอยู่บน Layout ของ Android ครับตั้ง id ว่า main_activity
ไปที่ MainActivity.java ครับ ประกาศตัวแปรบน Header ตามนี้ก่อน
import org.opencv.android.BaseLoaderCallback; import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame; import org.opencv.android.LoaderCallbackInterface; import org.opencv.android.OpenCVLoader; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfFloat; import org.opencv.core.MatOfInt; import org.opencv.core.Point; import org.opencv.core.Scalar; import org.opencv.core.Size; import org.opencv.android.CameraBridgeViewBase; import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2; import org.opencv.imgproc.Imgproc; import com.daydev.censorcamera.MainActivity; import com.daydev.censorcamera.R; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; import android.view.WindowManager;
ในที่นี้เราจะ เรียกใช้งาน CameraControl เป็นหลักเลย แก้ไข Class หลักเป็นดังนี้ครับ
public class MainActivity extends Activity implements CvCameraViewListener2{ ... }
เพื่อเรียก CvCameraViewListener มาใช้งาน ประกาศตัวแปรต่อไปนี้
private static final String TAG = "OCVSample::Activity"; public static final int VIEW_MODE_CENSOR = 0; private MenuItem mItemCameraCensor; private CameraBridgeViewBase mOpenCvCameraView; private Size mSize0; private Mat mIntermediateMat; private Mat mMat0; private MatOfInt mChannels[]; private MatOfInt mHistSize; private int mHistSizeNum = 25; private MatOfFloat mRanges; private Scalar mColorsRGB[]; private Scalar mColorsHue[]; private Scalar mWhilte; private Point mP1; private Point mP2; private float mBuff[]; private Mat mSepiaKernel; public static int viewMode = VIEW_MODE_CENSOR;
สังเกตที่
public static int viewMode = VIEW_MODE_CENSOR;
เป็นการเรียกโหมด ที่เราตั้งชื่อว่า VIEW_MODE_CENSOR โดยมีค่า Array Default เริ่มต้นเป็น 0 (กรณีมีหลายๆโหมดใช้ 0,1,3…,n)
public static final int VIEW_MODE_CENSOR = 0;
ต่อไปนี้เป็นคำสั่งทำงานของ OpenCV ในการประมวลผลรูปภาพแบบ Pixels ครับ
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) { @Override public void onManagerConnected(int status) { switch (status) { case LoaderCallbackInterface.SUCCESS: { Log.i(TAG, "OpenCV loaded successfully"); mOpenCvCameraView.enableView(); } break; default: { super.onManagerConnected(status); } break; } } }; public MainActivity() { Log.i(TAG, "Instantiated new " + this.getClass()); } @Override protected void onCreate(Bundle savedInstanceState) { Log.i(TAG, "called onCreate"); super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); setContentView(R.layout.activity_main); mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.main_activity); mOpenCvCameraView.setCvCameraViewListener(this); mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE); mOpenCvCameraView.setCvCameraViewListener(this); } @Override public void onPause() { super.onPause(); if (mOpenCvCameraView != null) mOpenCvCameraView.disableView(); } @Override public void onResume() { super.onResume(); if (!OpenCVLoader.initDebug()) { Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization"); OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback); } else { Log.d(TAG, "OpenCV library found inside package. Using it!"); mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS); } } public void onDestroy() { super.onDestroy(); if (mOpenCvCameraView != null) mOpenCvCameraView.disableView(); }
ตามด้วย Code ชุดนี้จาก Sample 2 ของ OpenCV ครับ นั่นคือเราจะหยิบมาแค่การปรับ imageProc หลักๆเท่านั้น
public boolean onCreateOptionsMenu(Menu menu) { Log.i(TAG, "called onCreateOptionsMenu"); mItemCameraCensor = menu.add("Censor"); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { Log.i(TAG, "called onOptionsItemSelected; selected item: " + item); if (item == mItemCameraCensor) viewMode = VIEW_MODE_CENSOR; return true; } public void onCameraViewStarted(int width, int height) { mIntermediateMat = new Mat(); mSize0 = new Size(); mChannels = new MatOfInt[] { new MatOfInt(0), new MatOfInt(1), new MatOfInt(2) }; mBuff = new float[mHistSizeNum]; mHistSize = new MatOfInt(mHistSizeNum); mRanges = new MatOfFloat(0f, 256f); mMat0 = new Mat(); mWhilte = Scalar.all(255); mP1 = new Point(); mP2 = new Point(); } public void onCameraViewStopped() { // Explicitly deallocate Mats if (mIntermediateMat != null) mIntermediateMat.release(); mIntermediateMat = null; } public Mat onCameraFrame(CvCameraViewFrame inputFrame) { Mat rgba = inputFrame.rgba(); Size sizeRgba = rgba.size(); Mat rgbaInnerWindow; int rows = (int) sizeRgba.height; int cols = (int) sizeRgba.width; int left = cols / 4; int top = rows / 4; int width = cols * 2 / 4; int height = rows * 2 / 4; switch (MainActivity.viewMode) { case MainActivity.VIEW_MODE_CENSOR: rgbaInnerWindow = rgba.submat(top, top + height, left, left + width); Imgproc.resize(rgbaInnerWindow, mIntermediateMat, mSize0, 0.1, 0.1, Imgproc.INTER_NEAREST); Imgproc.resize(mIntermediateMat, rgbaInnerWindow, rgbaInnerWindow.size(), 0., 0., Imgproc.INTER_NEAREST); rgbaInnerWindow.release(); break; } return rgba; }
ภาพรวมของ Code หน้า MainActivity.java เป็นดังนี้
package com.daydev.censorcamera; import org.opencv.android.BaseLoaderCallback; import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame; import org.opencv.android.LoaderCallbackInterface; import org.opencv.android.OpenCVLoader; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfFloat; import org.opencv.core.MatOfInt; import org.opencv.core.Point; import org.opencv.core.Scalar; import org.opencv.core.Size; import org.opencv.android.CameraBridgeViewBase; import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2; import org.opencv.imgproc.Imgproc; import com.daydev.censorcamera.MainActivity; import com.daydev.censorcamera.R; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceView; import android.view.WindowManager; public class MainActivity extends Activity implements CvCameraViewListener2{ private static final String TAG = "OCVSample::Activity"; public static final int VIEW_MODE_CENSOR = 0; private MenuItem mItemCameraCensor; private CameraBridgeViewBase mOpenCvCameraView; private Size mSize0; private Mat mIntermediateMat; private Mat mMat0; private MatOfInt mChannels[]; private MatOfInt mHistSize; private int mHistSizeNum = 25; private MatOfFloat mRanges; private Scalar mColorsRGB[]; private Scalar mColorsHue[]; private Scalar mWhilte; private Point mP1; private Point mP2; private float mBuff[]; private Mat mSepiaKernel; public static int viewMode = VIEW_MODE_CENSOR; private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) { @Override public void onManagerConnected(int status) { switch (status) { case LoaderCallbackInterface.SUCCESS: { Log.i(TAG, "OpenCV loaded successfully"); mOpenCvCameraView.enableView(); } break; default: { super.onManagerConnected(status); } break; } } }; public MainActivity() { Log.i(TAG, "Instantiated new " + this.getClass()); } @Override protected void onCreate(Bundle savedInstanceState) { Log.i(TAG, "called onCreate"); super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); setContentView(R.layout.activity_main); mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.main_activity); mOpenCvCameraView.setCvCameraViewListener(this); mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE); mOpenCvCameraView.setCvCameraViewListener(this); } @Override public void onPause() { super.onPause(); if (mOpenCvCameraView != null) mOpenCvCameraView.disableView(); } @Override public void onResume() { super.onResume(); if (!OpenCVLoader.initDebug()) { Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization"); OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback); } else { Log.d(TAG, "OpenCV library found inside package. Using it!"); mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS); } } public void onDestroy() { super.onDestroy(); if (mOpenCvCameraView != null) mOpenCvCameraView.disableView(); } @Override public boolean onCreateOptionsMenu(Menu menu) { Log.i(TAG, "called onCreateOptionsMenu"); mItemCameraCensor = menu.add("Censor"); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { Log.i(TAG, "called onOptionsItemSelected; selected item: " + item); if (item == mItemCameraCensor) viewMode = VIEW_MODE_CENSOR; return true; } public void onCameraViewStarted(int width, int height) { mIntermediateMat = new Mat(); mSize0 = new Size(); mChannels = new MatOfInt[] { new MatOfInt(0), new MatOfInt(1), new MatOfInt(2) }; mBuff = new float[mHistSizeNum]; mHistSize = new MatOfInt(mHistSizeNum); mRanges = new MatOfFloat(0f, 256f); mMat0 = new Mat(); mWhilte = Scalar.all(255); mP1 = new Point(); mP2 = new Point(); } public void onCameraViewStopped() { // Explicitly deallocate Mats if (mIntermediateMat != null) mIntermediateMat.release(); mIntermediateMat = null; } public Mat onCameraFrame(CvCameraViewFrame inputFrame) { Mat rgba = inputFrame.rgba(); Size sizeRgba = rgba.size(); Mat rgbaInnerWindow; int rows = (int) sizeRgba.height; int cols = (int) sizeRgba.width; int left = cols / 4; int top = rows / 4; int width = cols * 2 / 4; int height = rows * 2 / 4; switch (MainActivity.viewMode) { case MainActivity.VIEW_MODE_CENSOR: rgbaInnerWindow = rgba.submat(top, top + height, left, left + width); Imgproc.resize(rgbaInnerWindow, mIntermediateMat, mSize0, 0.1, 0.1, Imgproc.INTER_NEAREST); Imgproc.resize(mIntermediateMat, rgbaInnerWindow, rgbaInnerWindow.size(), 0., 0., Imgproc.INTER_NEAREST); rgbaInnerWindow.release(); break; } return rgba; } }
เราจะต้องทำงานกับกล้องของ Device ดังนั้นไปที่ไฟล์ AndroidManifest.xml ครับเพิ่ม Permission เข้าไปดังนี้
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" android:screenOrientation="landscape" android:configChanges="keyboardHidden|orientation">
และ
<uses-permission android:name="android.permission.CAMERA"/> <uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/> <uses-feature android:name="android.hardware.camera.front" android:required="false"/> <uses-feature android:name="android.hardware.camera.front.autofocus" android:required="false"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
ก็เป็นอันจบครับ ทำการทดสอบกันดีกว่า Run บน Device จริงเท่านั้น
ทดสอบกันหน่อย เลือกภาพที่จะทำการ Censor
ลองทดสอบแอพพลิเคชันของเรา
ดาวน์โหลด Source Code ที่: https://drive.google.com/folderview?id=0B1kwQ1abTIRrflVFMUQzMDNPZFFQelZkOG9aclAxQlplR0lXbk1RMGExRGtUbXFGS1FPQnM&usp=sharing