I'm Building Taskito
I'm Building Taskito
Get it on Google Play Download on the App Store

Face Detection using OpenCV Haarcascades in Android


I was working on this app which required face detection and pose estimation. I was already using Native code for some other processes (edge detection, etc) so I figured I’d use Haarcascades in native code only. It was a bit of hassle to configure how to send xml file to JNI or whether it would load the cascade once I pass the filename to the native code.

So I looked around a bit. I tried to look into the examples provided with opencv sdk and found out that I had to write the whole xml file to FileOutputStream so that it could be accessed in the native code.

Making XML available to the native code

First I stored the XML file in assets/ folder.

final InputStream is;
FileOutputStream os;
try {
    is = getResources().getAssets().open("face_frontal.xml");
    File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
    mCascadeFile = new File(cascadeDir, "face_frontal.xml");
    
    FileOutputStream os;
    os = new FileOutputStream(mCascadeFile);
            
    byte[] buffer = new byte[4096];
    int bytesRead;
    while ((bytesRead = is.read(buffer)) != -1) {
        os.write(buffer, 0, bytesRead);
    }

    is.close();
    os.close();
} catch (IOException e) {
    Log.i(TAG, "face cascade not found");
}

Once this is done, you can pass the file and relevant Mat objects to the native code.

JNI call

faceFound = detectFace(mCascadeFile.getAbsolutePath(),
                       mRgba.getNativeObjAddr(),
                       mretVal.getNativeObjAddr());

// Image is loaded in mRgba
// mretVal will contain the output Mat 

// At the end of the class declaring the native function
public native int detectFace(String filename, long matAddrRgba,
                             long matAddrRetVal);

Native code

I’m not editing the native function name here so you need to change it appropriately.

JNIEXPORT jint JNICALL Java_com_fenchtose_cardscanner_ScanActivity_detectFace(JNIEnv* jenv, jobject, jstring jFileName, jlong addrRgba, jlong addrRetVal)
{
    const char* jnamestr = jenv->GetStringUTFChars(jFileName, NULL);
    string stdFileName(jnamestr);

    Mat& mRgba = *(Mat*)addrRgba;
    Mat& retValMat = *(Mat*)addrRetVal;
    Mat gray;
    vector<Rect> faces;

    jint retVal;
    int faceFound=0;

    mRgba.copyTo(retValMat);

    cvtColor(mRgba, gray, CV_RGBA2GRAY);

    CascadeClassifier face_cascade;
    face_cascade.load(stdFileName);
    LOGD("cascade loaded\n");

    face_cascade.detectMultiScale(gray, faces, 2, 1,
                CV_HAAR_FIND_BIGGEST_OBJECT |  CV_HAAR_SCALE_IMAGE,
                Size(30, 30), Size(900, 900));
    LOGD("detectMultiScale\n");

    if (faces.size() > 0)
    {
        int index;
        Rect face;
        for(index=0; index<faces.size(); index++){
            face = faces[index];
            rectangle(retValMat, face, Scalar(255, 0, 0), 3);
        }
        faceFound = 1;
        LOGD("face found\n");
    }
    else
    {
        LOGD("face not found\n");
        faceFound = 0;
    }
    
    retVal = (jint)faceFound;

    return retVal;
}

This returns if face is found or not. If face is found it draws on the retvalMat. So this was pretty easy to do and return a good output.

P.S. Learning more about Android OpenCV.

Playing around with Android UI

Articles focusing on Android UI - playing around with ViewPagers, CoordinatorLayout, meaningful motions and animations, implementing difficult customized views, etc.

Read next