728x90
728x90
안녕하세요. 교내 앱개발대회를 AR번역기를 준비하고 있어서 기초부터 다지자는 의미에서 openCV를 이용한 얼굴인식 앱을 만들어보았습니다.
안드로이드 스튜디오를 이용하였습니다.
먼저 앱 화면입니다. 카메라와 우측에는 상단부터 전, 후면 전환 버튼, 화면 캡쳐 버튼, 이미지 속에서 글씨를 읽는 OCR 버튼이 있습니다. 현재는 캡쳐버튼까지만 구현해놓은 상태입니다.
대부분 인터넷 예제를 보고 공부하면서 작성하였습니다.
주요 코드는 다음과 같습니다.
728x90
//self
package hoeun.opencv_ndk;
//기본 패키지
import android.annotation.TargetApi;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
//openCV 패키지
import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
//자바 패키지
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.Semaphore;
//OCR 패키지
/*import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import com.googlecode.tesseract.android.TessBaseAPI;
import java.io.FileNotFoundException;
import java.io.IOException;*/
public class MainActivity extends AppCompatActivity
implements CameraBridgeViewBase.CvCameraViewListener2 {
private static final String TAG = "opencv";
private CameraBridgeViewBase mOpenCvCameraView;
private Mat matInput;
private Mat matResult;
private int cameraType = 1;
//public native void ConvertRGBtoGray(long matAddrInput, long matAddrResult);
public native long loadCascade(String cascadeFileName );
public native void detect(long cascadeClassifier_face,
long cascadeClassifier_eye, long matAddrInput, long matAddrResult);
public long cascadeClassifier_face = 0;
public long cascadeClassifier_eye = 0;
private final Semaphore writeLock = new Semaphore(1);
public void getWriteLock() throws InterruptedException {
writeLock.acquire();
}
public void releaseWriteLock() {
writeLock.release();
}
static {
System.loadLibrary("opencv_java3");
System.loadLibrary("native-lib");
}
private void copyFile(String filename) {
String baseDir = Environment.getExternalStorageDirectory().getPath();
String pathDir = baseDir + File.separator + filename;
AssetManager assetManager = this.getAssets();
InputStream inputStream = null;
OutputStream outputStream = null;
try {
Log.d( TAG, "copyFile :: 다음 경로로 파일복사 "+ pathDir);
inputStream = assetManager.open(filename);
outputStream = new FileOutputStream(pathDir);
byte[] buffer = new byte[1024];
int read;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
inputStream.close();
inputStream = null;
outputStream.flush();
outputStream.close();
outputStream = null;
} catch (Exception e) {
Log.d(TAG, "copyFile :: 파일 복사 중 예외 발생 "+e.toString() );
}
}
private void read_cascade_file(){
copyFile("haarcascade_frontalface_alt.xml");
copyFile("haarcascade_eye_tree_eyeglasses.xml");
Log.d(TAG, "read_cascade_file:");
cascadeClassifier_face = loadCascade( "haarcascade_frontalface_alt.xml");
Log.d(TAG, "read_cascade_file:");
cascadeClassifier_eye = loadCascade( "haarcascade_eye_tree_eyeglasses.xml");
}
private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
@Override
public void onManagerConnected(int status) {
switch (status) {
case LoaderCallbackInterface.SUCCESS:
{
mOpenCvCameraView.enableView();
} break;
default:
{
super.onManagerConnected(status);
} break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_main);
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v)
{
if (cameraType == 1){
cameraType = 0;
}else if (cameraType == 0){
cameraType = 1;
}
}
};
Button stcbutton = (Button) findViewById(R.id.switchbutton);
stcbutton.setOnClickListener(listener);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//퍼미션 상태 확인
if (!hasPermissions(PERMISSIONS)) {
//퍼미션 허가 안되어있다면 사용자에게 요청
requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE);
}
else read_cascade_file(); //추가
}
else read_cascade_file(); //추가
mOpenCvCameraView = (CameraBridgeViewBase)findViewById(R.id.activity_surface_view);
mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
mOpenCvCameraView.setCvCameraViewListener(this);
//mOpenCvCameraView.setCameraIndex(0); // front-camera(1), back-camera(0) 후면 카메라 사용
mOpenCvCameraView.setCameraIndex(cameraType); // front-camera(1), back-camera(0) 전면 카메라 사용
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
try {
getWriteLock();
File path = new File(Environment.getExternalStorageDirectory() + "/Images/");
path.mkdirs();
File file = new File(path, "image.png");
String filename = file.toString();
Imgproc.cvtColor(matResult, matResult, Imgproc.COLOR_BGR2RGB, 4);
boolean ret = Imgcodecs.imwrite( filename, matResult);
if ( ret ) Log.d(TAG, "SUCESS");
else Log.d(TAG, "FAIL");
Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(Uri.fromFile(file));
sendBroadcast(mediaScanIntent);
} catch (InterruptedException e) {
e.printStackTrace();
}
releaseWriteLock();
}
});
}
@Override
public void onPause()
{
super.onPause();
if (mOpenCvCameraView != null)
mOpenCvCameraView.disableView();
}
@Override
public void onResume()
{
super.onResume();
if (!OpenCVLoader.initDebug()) {
Log.d(TAG, "onResume :: Internal OpenCV library not found.");
OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_2_0, this, mLoaderCallback);
} else {
Log.d(TAG, "onResum :: OpenCV library found inside package. Using it!");
mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
}
}
public void onDestroy() {
super.onDestroy();
if (mOpenCvCameraView != null)
mOpenCvCameraView.disableView();
}
@Override
public void onCameraViewStarted(int width, int height) {
}
@Override
public void onCameraViewStopped() {
}
@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
try {
getWriteLock();
matInput = inputFrame.rgba();
//if ( matResult != null ) matResult.release(); fix 2018. 8. 18
if ( matResult == null )
matResult = new Mat(matInput.rows(), matInput.cols(), matInput.type());
//ConvertRGBtoGray(matInput.getNativeObjAddr(), matResult.getNativeObjAddr());
Core.flip(matInput, matInput, 1);
detect(cascadeClassifier_face,cascadeClassifier_eye, matInput.getNativeObjAddr(),
matResult.getNativeObjAddr());
} catch (InterruptedException e) {
e.printStackTrace();
}
releaseWriteLock();
return matResult;
}
//여기서부턴 퍼미션 관련 메소드
static final int PERMISSIONS_REQUEST_CODE = 1000;
//String[] PERMISSIONS = {"android.permission.CAMERA"};
String[] PERMISSIONS = {"android.permission.CAMERA",
"android.permission.WRITE_EXTERNAL_STORAGE"};
private boolean hasPermissions(String[] permissions) {
int result;
//스트링 배열에 있는 퍼미션들의 허가 상태 여부 확인
for (String perms : permissions){
result = ContextCompat.checkSelfPermission(this, perms);
if (result == PackageManager.PERMISSION_DENIED){
//허가 안된 퍼미션 발견
return false;
}
}
//모든 퍼미션이 허가되었음
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch(requestCode){
case PERMISSIONS_REQUEST_CODE:
if (grantResults.length > 0) {
boolean cameraPermissionAccepted = grantResults[0]
== PackageManager.PERMISSION_GRANTED;
//if (!cameraPermissionAccepted)
// showDialogForPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");
boolean writePermissionAccepted = grantResults[1]
== PackageManager.PERMISSION_GRANTED;
if (!cameraPermissionAccepted || !writePermissionAccepted) {
showDialogForPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");
return;
}else
{
read_cascade_file();
}
}
break;
}
}
@TargetApi(Build.VERSION_CODES.M)
private void showDialogForPermission(String msg) {
AlertDialog.Builder builder = new AlertDialog.Builder( MainActivity.this);
builder.setTitle("알림");
builder.setMessage(msg);
builder.setCancelable(false);
builder.setPositiveButton("예", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id){
requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE);
}
});
builder.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface arg0, int arg1) {
finish();
}
});
builder.create().show();
}
}
위는 MainActivity.java 코드입니다.
728x90
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="hoeun.opencv_ndk">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<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"/>
<supports-screens android:resizeable="true"
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity"
android:screenOrientation="landscape"
android:configChanges="keyboardHidden|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
위는 AndroidManifest.xml 코드입니다.
#include <jni.h>
#include "hoeun_opencv_ndk_MainActivity.h"
#include <opencv2/opencv.hpp>
#include <android/log.h>
using namespace cv;
using namespace std;
extern "C"{
/*
JNIEXPORT void JNICALL Java_hoeun_opencv_1ndk_MainActivity_ConvertRGBtoGray(
JNIEnv *env,
jobject instance,
jlong matAddrInput,
jlong matAddrResult){
Mat &matInput = *(Mat *)matAddrInput;
Mat &matResult = *(Mat *)matAddrResult;
cvtColor(matInput, matResult, CV_RGBA2GRAY);
}
}
*/
float resize(Mat img_src, Mat &img_resize, int resize_width){
float scale = resize_width / (float)img_src.cols ;
if (img_src.cols > resize_width) {
int new_height = cvRound(img_src.rows * scale);
resize(img_src, img_resize, Size(resize_width, new_height));
}
else {
img_resize = img_src;
}
return scale;
}
JNIEXPORT jlong JNICALL Java_hoeun_opencv_1ndk_MainActivity_loadCascade
(JNIEnv *env, jobject type, jstring cascadeFileName_){
const char *nativeFileNameString = env->GetStringUTFChars(cascadeFileName_, 0);
string baseDir("/storage/emulated/0/");
baseDir.append(nativeFileNameString);
const char *pathDir = baseDir.c_str();
jlong ret = 0;
ret = (jlong) new CascadeClassifier(pathDir);
if (((CascadeClassifier *) ret)->empty()) {
__android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",
"CascadeClassifier로 로딩 실패 %s", nativeFileNameString);
}
else
__android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",
"CascadeClassifier로 로딩 성공 %s", nativeFileNameString);
env->ReleaseStringUTFChars(cascadeFileName_, nativeFileNameString);
return ret;
}
JNIEXPORT void JNICALL Java_hoeun_opencv_1ndk_MainActivity_detect
(JNIEnv *env, jobject type, jlong cascadeClassifier_face, jlong cascadeClassifier_eye, jlong matAddrInput, jlong matAddrResult){
Mat &img_input = *(Mat *) matAddrInput;
Mat &img_result = *(Mat *) matAddrResult;
img_result = img_input.clone();
std::vector<Rect> faces;
Mat img_gray;
cvtColor(img_input, img_gray, COLOR_BGR2GRAY);
equalizeHist(img_gray, img_gray);
Mat img_resize;
float resizeRatio = resize(img_gray, img_resize, 640);
//-- Detect faces
((CascadeClassifier *) cascadeClassifier_face)->detectMultiScale( img_resize, faces, 1.1, 2, 0|CASCADE_SCALE_IMAGE, Size(30, 30) );
__android_log_print(ANDROID_LOG_DEBUG, (char *) "native-lib :: ",
(char *) "face %d found ", faces.size());
for (int i = 0; i < faces.size(); i++) {
double real_facesize_x = faces[i].x / resizeRatio;
double real_facesize_y = faces[i].y / resizeRatio;
double real_facesize_width = faces[i].width / resizeRatio;
double real_facesize_height = faces[i].height / resizeRatio;
Point center( real_facesize_x + real_facesize_width / 2, real_facesize_y + real_facesize_height/2);
ellipse(img_result, center, Size( real_facesize_width / 2, real_facesize_height / 2), 0, 0, 360,
Scalar(255, 192, 0), 4, 8, 0);
Rect face_area(real_facesize_x, real_facesize_y, real_facesize_width,real_facesize_height);
Mat faceROI = img_gray( face_area );
std::vector<Rect> eyes;
//-- In each face, detect eyes
((CascadeClassifier *) cascadeClassifier_eye)->detectMultiScale( faceROI, eyes, 1.1, 2, 0 |CASCADE_SCALE_IMAGE, Size(30, 30) );
for ( size_t j = 0; j < eyes.size(); j++ )
{
//Point eye_center( real_facesize_x + eyes[j].x + eyes[j].width/2, real_facesize_y + eyes[j].y + eyes[j].height/2 );
//int radius = cvRound( (eyes[j].width + eyes[j].height)*0.25 );
//circle( img_result, eye_center, radius, Scalar( 89, 89, 89 ), 4, 8, 0 );
}
}
}
}
위 코드는 openCV를 담당하는 main.cpp 코드입니다.
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context=".MainActivity"
android:orientation="horizontal">
<org.opencv.android.JavaCameraView
android:id="@+id/activity_surface_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="top"
android:layout_weight="100"
/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="horizontal"
android:background="#000000"
android:layout_weight="1">
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#000000"
android:layout_weight="6"
>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
android:id="@+id/switchbutton"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@drawable/mybutoon"
android:layout_weight="3"
android:text="@string/Arrow"
android:textColor="#0073ff"/>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="8"/>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="0dp"
android:text="@string/string_Capture"
android:layout_weight="10"
android:background="@drawable/mybutoon" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="8" />
<Button
android:id="@+id/OCRbutton"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:text="OCR"
android:background="@drawable/mybutoon"
android:textSize="6pt"
android:textColor="#ff8400"/>
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="horizontal"
android:background="#000000"
android:layout_weight="2">
</LinearLayout>
</LinearLayout>
위 코드는 디자인 부분인 activity_main.xml 코드입니다.
캡쳐 버튼을 누르면 DCIM 폴더 내에 저장됩니다.
이 프로젝트를 진행하여 openCV를 이용해 AR과 OCR 기능을 추가하여 번역기를 만들어보도록 하겠습니다. 기대 많이 해주세요!
728x90
728x90