ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Apriltag Nav] 3. 안드로이드 프로젝트에 apriltag ndk 적용시키기
    개발/안드로이드 2023. 3. 10. 16:25

    NDK와 JNI

    apriltag 오픈소스는 c언어로 작성된 소스입니다.

    c언어 소스를 자바 환경에서 실행시키기 위해서는 ndk를 사용해야 합니다.

     

    Ndk란?

    네이티브 개발 키트(Native Development Kit)란 Android 환경에서 c 및 c++ 코드를 사용할 수 있게 해주는 도구입니다.

    sdk보다 빠르다는 장점이 있습니다.

    Jni란?

    자바 네이티브 인터페이스 (Java Native Interface)란 Java코드와 c, c++ 코드를 연결시켜주는 인터페이스 입니다.

    Jni 함수를 활용해서 c, c++로 작성한 함수를 호출할 수 있습니다.

     

     

     

    Ndk & Jni 환경 세팅하기

    아랫글을 참고하셔서 apriltag ndk를 프로젝트 내에 적용시키면 됩니다.

    https://web-inf.tistory.com/98

     

    Android NDK&JNI(C/C++) 사용하기 (JNI-Hello, World)

    안녕하세요. 안드로이드 포스팅을 오래간만에 진행해보겠습니다. 안드로이드(JAVA) 환경에서 C혹은 C++(네이티브) 소스를 연동/연결하여 상호 호환하는 기술을 주제로 시작하겠습니다. 사용 환경

    web-inf.tistory.com

     

     

    apriltag ndk 적용 모습

     

    apriltag 오픈소스에 있는 모든 파일을 올려도 되고, dependency를 쫓아가며 필요한 파일만 올려도 됩니다.

    저 같은 경우는 python wrapper 관련 파일을 제외하고 올렸습니다.

     

    이제 apriltag_native_lib.c를 통해 c언어로 작성된 함수를 사용할 수 있게 되었습니다.

     

    Jni native 함수

    <apriltag_native_lib.c>

    // apriltag_native_lib.c
    
    #include <jni.h>
    #include <string.h>
    #include <android/bitmap.h>
    #include <android/log.h>
    #include "apriltag.h"
    #include "tag36h10.h"
    #include "tag36h11.h"
    #include "tag36artoolkit.h"
    #include "tag25h9.h"
    #include "apriltag/tag16h5.h"
    #include "tagStandard41h12.h"
    #include "tagStandard52h13.h"
    
    static struct {
        apriltag_detector_t *td;
        apriltag_family_t *tf;
        void(*tf_destroy)(apriltag_family_t*);
    
        jclass al_cls;
        jmethodID al_constructor, al_add;
        jclass ad_cls;
        jmethodID ad_constructor;
        jfieldID ad_id_field, ad_hamming_field, ad_c_field, ad_p_field;
    } state;
    
    JNIEXPORT void JNICALL
    Java_apriltag_ApriltagNative_apriltag_1native_1init(
            JNIEnv *env, jclass cls){
        // Just do method lookups once and cache the results
    
        // Get ArrayList methods
        jclass al_cls = (*env)->FindClass(env, "java/util/ArrayList");
        if (!al_cls) {
            __android_log_write(ANDROID_LOG_ERROR, "apriltag_jni",
                                "couldn't find ArrayList class");
            return;
        }
        state.al_cls = (*env)->NewGlobalRef(env, al_cls);
    
        state.al_constructor = (*env)->GetMethodID(env, al_cls, "<init>", "()V");
        state.al_add = (*env)->GetMethodID(env, al_cls, "add", "(Ljava/lang/Object;)Z");
        if (!state.al_constructor || !state.al_add) {
            __android_log_write(ANDROID_LOG_ERROR, "apriltag_jni",
                                "couldn't find ArrayList methods");
            return;
        }
    
        // Get ApriltagDetection class
        jclass ad_cls = (*env)->FindClass(env, "apriltag/ApriltagDetection");
        if (!ad_cls) {
            __android_log_write(ANDROID_LOG_ERROR, "apriltag_jni",
                                "couldn't find ApriltagDetection class");
            return;
        }
        state.ad_cls = (*env)->NewGlobalRef(env, ad_cls);
    
        state.ad_constructor = (*env)->GetMethodID(env, ad_cls, "<init>", "()V");
        if (!state.ad_constructor) {
            __android_log_write(ANDROID_LOG_ERROR, "apriltag_jni",
                                "couldn't find ApriltagDetection constructor");
            return;
        }
    
        state.ad_id_field = (*env)->GetFieldID(env, ad_cls, "id", "I");
        state.ad_hamming_field = (*env)->GetFieldID(env, ad_cls, "hamming", "I");
        state.ad_c_field = (*env)->GetFieldID(env, ad_cls, "c", "[D");
        state.ad_p_field = (*env)->GetFieldID(env, ad_cls, "p", "[D");
    
        if (!state.ad_id_field ||
            !state.ad_hamming_field ||
            !state.ad_c_field ||
            !state.ad_p_field) {
            __android_log_write(ANDROID_LOG_ERROR, "apriltag_jni",
                                "couldn't find ApriltagDetection fields");
            return;
        }
    }
    
    /*
     * Class:     Java_apriltag_ApriltagNative
     * Method:    apriltag_init
     * Signature: (Ljava/lang/String;IDDI)V
     */
    JNIEXPORT void JNICALL
    Java_apriltag_ApriltagNative_apriltag_1init(
            JNIEnv *env, jclass cls, jstring _tfname, jint errorbits, jdouble decimate,
            jdouble sigma, jint nthreads) {
        // Do cleanup in case we're already initialized
        if (state.td) {
            apriltag_detector_destroy(state.td);
            state.td = NULL;
        }
        if (state.tf) {
            state.tf_destroy(state.tf);
            state.tf = NULL;
        }
    
        // Initialize state
        const char *tfname = (*env)->GetStringUTFChars(env, _tfname, NULL);
    
        if (!strcmp(tfname, "tag36h11")) {
            state.tf = tag36h11_create();
            state.tf_destroy = tag36h11_destroy;
        } else if (!strcmp(tfname, "tag36h10")) {
            state.tf = tag36h10_create();
            state.tf_destroy = tag36h10_destroy;
        }  else if (!strcmp(tfname, "tag25h9")) {
            state.tf = tag25h9_create();
            state.tf_destroy = tag25h9_destroy;
        } else if (!strcmp(tfname, "tag16h5")) {
            state.tf = tag16h5_create();
            state.tf_destroy = tag16h5_destroy;
        } else if (!strcmp(tfname, "tagStandard41h12")) {
            state.tf = tagStandard41h12_create();
            state.tf_destroy = tagStandard41h12_destroy;
        } else if (!strcmp(tfname, "tagStandard52h13")) {
            state.tf = tagStandard52h13_create();
            state.tf_destroy = tagStandard52h13_destroy;
        } else {
            __android_log_print(ANDROID_LOG_ERROR, "apriltag_jni",
                                "invalid tag family: %s", tfname);
            (*env)->ReleaseStringUTFChars(env, _tfname, tfname);
            return;
        }
        (*env)->ReleaseStringUTFChars(env, _tfname, tfname);
    
        state.td = apriltag_detector_create();
        apriltag_detector_add_family_bits(state.td, state.tf, errorbits);
        state.td->quad_decimate = decimate;
        state.td->quad_sigma = sigma;
        state.td->nthreads = nthreads;
    }
    
    /*
     * Class:     Java_apriltag_ApriltagNative
     * Method:    apriltag_detect
     * Signature: ([BII)Ljava/util/ArrayList;
     */
    JNIEXPORT jobject JNICALL
    Java_apriltag_ApriltagNative_apriltag_1detect(
            JNIEnv *env, jclass cls, jbyteArray _buf, jint width, jint height){
        // If not initialized, init with default settings
        if (!state.td) {
            state.tf = tagStandard41h12_create();
            state.td = apriltag_detector_create();
            apriltag_detector_add_family_bits(state.td, state.tf, 2);
            state.td->quad_decimate = 4.0f;
            state.td->quad_sigma = 0.0f;
            state.td->nthreads = 4;
            __android_log_write(ANDROID_LOG_INFO, "apriltag_jni",
                                "using default parameters");
        }
    
        // Use the luma channel (the first width*height elements)
        // as grayscale input image
        jbyte *buf = (*env)->GetByteArrayElements(env, _buf, NULL);
        image_u8_t im = {
                .buf = (uint8_t*)buf,
                .height = height,
                .width = width,
                .stride = width
        };
    
        zarray_t *detections = apriltag_detector_detect(state.td, &im);
        (*env)->ReleaseByteArrayElements(env, _buf, buf, 0);
    
        // al = new ArrayList();
        jobject al = (*env)->NewObject(env, state.al_cls, state.al_constructor);
        for (int i = 0; i < zarray_size(detections); i += 1) {
            apriltag_detection_t *det;
            zarray_get(detections, i, &det);
    
            // ad = new ApriltagDetection();
            jobject ad = (*env)->NewObject(env, state.ad_cls, state.ad_constructor);
            (*env)->SetIntField(env, ad, state.ad_id_field, det->id);
            (*env)->SetIntField(env, ad, state.ad_hamming_field, det->hamming);
            jdoubleArray ad_c = (*env)->GetObjectField(env, ad, state.ad_c_field);
            (*env)->SetDoubleArrayRegion(env, ad_c, 0, 2, det->c);
            jdoubleArray ad_p = (*env)->GetObjectField(env, ad, state.ad_p_field);
            (*env)->SetDoubleArrayRegion(env, ad_p, 0, 8, (double*)det->p);
    
            // al.add(ad);
            (*env)->CallBooleanMethod(env, al, state.al_add, ad);
    
            // Need to respect the local reference limit
            (*env)->DeleteLocalRef(env, ad);
            (*env)->DeleteLocalRef(env, ad_c);
            (*env)->DeleteLocalRef(env, ad_p);
        }
    
        // Cleanup
        apriltag_detections_destroy(detections);
    
        return al;
    }

     

    <MainActivity.kt>

    class MainActivity : AppCompatActivity() {
    
        companion object {
            init {
                System.loadLibrary("apriltag_native_lib")
            }
        }
        
        // ...
        
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            ApriltagNative.apriltag_native_init()
            ApriltagNative.apriltag_init("tagStandard52h13", 2, 4.0, 0.0, 1)
        }
    }

    코틀린에서 native 라이브러리를 사용하는 방법입니다.

    companion object에서 init으로 라이브러리를 로드하고, onCreate 메소드에서 apriltag 관련 초기화를 해줍니다.

     

    apriltag_init( tagFamily , errorBits, decimateFactor, blurSigma, nthreads ) 함수 인자에 원하는 값을 넣어 초기화합니다.

    • tagFamily : 사용할 tag family를 넣으면 됩니다. 위 코드는 tagStandard52h13을 넣었습니다.
    • errorBits : 오차에 관한 값으로 기본값 2를 사용하겠습니다.
    • decimateFactor : 기본값은 1이고, 커질수록 속도가 빨라지지만 감지 거리는 짧아집니다. 어떤 인자보다 성능에 영향을 크게 주기 때문에 매우 중요합니다.
    • blurSigma : 가우시안 블러링에 관한 값입니다.
    • nthreads : 쓰레드의 개수입니다. 

    실제 프로젝트에서 중요한 값은 tagFamily와 decimateFactor입니다.

    다른 값에 대한 자세한 정보는 apriltag 공식 홈페이지를 참고해주세요.

     

     

    MainActivity에서 필요한 값을 초기화 했다면, 원하는 곳에서 아래 함수를 호출하여 detection을 할 수 있습니다.

    mDetections = ApriltagNative.apriltag_detect(bytes, w, h);

    여기서 bytes는 이미지 버퍼가 되고, w와 h는 이후 나올 카메라 프리뷰의 width와 height입니다.

    카메라를 통해 이미지 버퍼를 얻는 과정은 다음 시간에 자세히 알아보도록 하겠습니다.

     

    apriltag_detect(bytes, w, h)함수를 호출하면 bytes 이미지 버퍼에 apriltag가 들어있는지 detection을 진행합니다.

    함수의 내부를 자세히 알기보다는 input과 output에 집중합시다.

     

    함수의 output으로는 detection의 배열이 나오게 됩니다. detect한 태그가 여러 개일 수 있기 때문이죠.

    detection이 어떤 형태인지는 지난 포스팅에서 다루었습니다.

    이제 나온 mDetections 결과물로 개발을 진행할 수 있습니다!!

     

    다음번에는 어떻게 이미지 버퍼를 받아오는지와, 카메라 프리뷰에 대해 알아보도록 하겠습니다.

    댓글

Designed by Tistory.