ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Android OpenCV 예제 - SURF를 사용한 오브젝트 검출 테스트
    OpenCV/Android 개발 환경 및 예제 2019. 5. 21. 20:32

     

     

     

     

    안드로이드에서 Surf를 사용하여 이미지를 매칭하는 예제입니다.



    우선 다음 포스트 내용을 진행하여 안드로이드용 OpenCV를 새로 빌드해서 사용해야 합니다.

     

    Android용 OpenCV 빌드하는 방법(contrib 포함)

    https://webnautes.tistory.com/1268




    이후 다음 영상을 따라 진행하세요..

    (업로드 중입니다.)

     

    1. styles.xml

           <!-- No Title Bar-->
           <item name="windowActionBar">false</item>
           <item name="windowNoTitle">true</item>

     

    2. activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.45"
            android:id="@+id/imageViewObject"
            app:srcCompat="@drawable/ic_image"/>
    
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.45"
            android:id="@+id/imageViewScene"
            app:srcCompat="@drawable/ic_image"/>
    
    
        <Button
            android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="0.1"
            android:text="Surf" />
    
    
    </LinearLayout>
    

     

    3. image.svg

    image.zip
    0.00MB




    4. AndroidManifest.xml

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />



    5. MainActivity.java

    androidx라면 다음 세줄을 수정하세요. androidx의 경우에는 새로 추가된 OpenCV 4.1.1 빌드후에만 동작할 수도 있습니다.

     

    변경전

    import android.support.annotation.NonNull;

    import android.support.v7.app.AlertDialog;

    import android.support.v7.app.AppCompatActivity;



    변경후

    import androidx.annotation.NonNull;

    import androidx.appcompat.app.AlertDialog;

    import androidx.appcompat.app.AppCompatActivity;

     

     

    import android.content.DialogInterface;
    import android.content.Intent;
    import android.content.pm.PackageManager;
    import android.database.Cursor;
    import android.graphics.Bitmap;
    import android.graphics.Matrix;
    import android.media.ExifInterface;
    import android.net.Uri;
    import android.os.Build;
    import android.provider.MediaStore;
    import android.support.annotation.NonNull;
    import android.support.v7.app.AlertDialog;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.View;
    import android.widget.Button;
    import android.widget.ImageView;
    
    import org.opencv.android.Utils;
    import org.opencv.core.Mat;
    
    import java.io.IOException;
    
    
    public class MainActivity extends AppCompatActivity {
    
    
        static {
            System.loadLibrary("opencv_java4");
            System.loadLibrary("native-lib");
        }
    
    
        ImageView imageVIewObject;
        ImageView imageVIewScene;
        private Mat img_object;
        private Mat img_scene;
    
    
        private static final String TAG = "opencv";
        private final int GET_GALLERY_IMAGE1 = 200;
        private final int GET_GALLERY_IMAGE2 = 300;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            imageVIewObject = (ImageView)findViewById(R.id.imageViewObject);
            imageVIewScene = (ImageView)findViewById(R.id.imageViewScene);
    
            Button Button = (Button)findViewById(R.id.button);
            Button.setOnClickListener(new View.OnClickListener(){
                public void onClick(View v){
    
                    imageprocess_and_showResult();
                }
            });
    
    
            imageVIewObject.setOnClickListener(new View.OnClickListener() {
                public void onClick(View v) {
                    Intent intent = new Intent(Intent.ACTION_PICK);
                    intent.setData(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
                    intent.setType("image/*");
                    startActivityForResult(intent, GET_GALLERY_IMAGE1);
                }
            });
    
    
            imageVIewScene.setOnClickListener(new View.OnClickListener() {
                public void onClick(View v) {
                    Intent intent = new Intent(Intent.ACTION_PICK);
                    intent.setData(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
                    intent.setType("image/*");
                    startActivityForResult(intent, GET_GALLERY_IMAGE2);
                }
            });
    
    
            if (!hasPermissions(PERMISSIONS)) { //퍼미션 허가를 했었는지 여부를 확인
                requestNecessaryPermissions(PERMISSIONS);//퍼미션 허가안되어 있다면 사용자에게 요청
            }
    
        }
    
    
        public native void imageprocessing(long objectImage, long sceneImage);
    
        private void imageprocess_and_showResult() {
    
            if (img_scene == null)
                img_scene = new Mat();
    
            Log.d("native-lib", "start");
            imageprocessing(img_object.getNativeObjAddr(), img_scene.getNativeObjAddr());
            Log.d("native-lib", "end");
    
            Bitmap bitmapOutput = Bitmap.createBitmap(img_scene.cols(), img_scene.rows(), Bitmap.Config.ARGB_8888);
            Utils.matToBitmap(img_scene, bitmapOutput);
            imageVIewScene.setImageBitmap(bitmapOutput);
        }
    
    
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    
            if ( requestCode == GET_GALLERY_IMAGE1){
    
    
                if (data.getData() != null) {
                    Uri uri = data.getData();
    
                    try {
                        String path = getRealPathFromURI(uri);
                        int orientation = getOrientationOfImage(path); // 런타임 퍼미션 필요
                        Bitmap temp = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
                        Bitmap bitmap = getRotatedBitmap(temp, orientation);
                        imageVIewObject.setImageBitmap(bitmap);
    
                        img_object = new Mat();
                        Bitmap bmp32 = bitmap.copy(Bitmap.Config.ARGB_8888, true);
                        Utils.bitmapToMat(bmp32, img_object);
    
    
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
    
                }
    
            }else if ( requestCode == GET_GALLERY_IMAGE2){
    
    
                if (data.getData() != null) {
                    Uri uri = data.getData();
    
                    try {
                        String path = getRealPathFromURI(uri);
                        int orientation = getOrientationOfImage(path); // 런타임 퍼미션 필요
                        Bitmap temp = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
                        Bitmap bitmap = getRotatedBitmap(temp, orientation);
                        imageVIewScene.setImageBitmap(bitmap);
    
                        img_scene = new Mat();
                        Bitmap bmp32 = bitmap.copy(Bitmap.Config.ARGB_8888, true);
                        Utils.bitmapToMat(bmp32, img_scene);
    
    
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
    
                }
    
            }
        }
    
    
        private String getRealPathFromURI(Uri contentUri) {
    
            String[] proj = {MediaStore.Images.Media.DATA};
            Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
            cursor.moveToFirst();
            int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
    
            return cursor.getString(column_index);
        }
    
        // 출처 - http://snowdeer.github.io/android/2016/02/02/android-image-rotation/
        public int getOrientationOfImage(String filepath) {
            ExifInterface exif = null;
    
            try {
                exif = new ExifInterface(filepath);
            } catch (IOException e) {
                Log.d("@@@", e.toString());
                return -1;
            }
    
            int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
    
            if (orientation != -1) {
                switch (orientation) {
                    case ExifInterface.ORIENTATION_ROTATE_90:
                        return 90;
    
                    case ExifInterface.ORIENTATION_ROTATE_180:
                        return 180;
    
                    case ExifInterface.ORIENTATION_ROTATE_270:
                        return 270;
                }
            }
    
            return 0;
        }
    
        public Bitmap getRotatedBitmap(Bitmap bitmap, int degrees) throws Exception {
            if(bitmap == null) return null;
            if (degrees == 0) return bitmap;
    
            Matrix m = new Matrix();
            m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
    
            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
        }
    
    
    
        // 퍼미션 코드
        static final int PERMISSION_REQUEST_CODE = 1;
        String[] PERMISSIONS  = {"android.permission.WRITE_EXTERNAL_STORAGE"};
    
        private boolean hasPermissions(String[] permissions) {
            int ret = 0;
            //스트링 배열에 있는 퍼미션들의 허가 상태 여부 확인
            for (String perms : permissions){
                ret = checkCallingOrSelfPermission(perms);
                if (!(ret == PackageManager.PERMISSION_GRANTED)){
                    //퍼미션 허가 안된 경우
                    return false;
                }
    
            }
            //모든 퍼미션이 허가된 경우
            return true;
        }
    
        private void requestNecessaryPermissions(String[] permissions) {
            //마시멜로( API 23 )이상에서 런타임 퍼미션(Runtime Permission) 요청
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                requestPermissions(permissions, PERMISSION_REQUEST_CODE);
            }
        }
    
    
    
        @Override
        public void onRequestPermissionsResult(int permsRequestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
            switch(permsRequestCode){
    
                case PERMISSION_REQUEST_CODE:
                    if (grantResults.length > 0) {
                        boolean writeAccepted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
    
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
                            if (!writeAccepted )
                            {
                                showDialogforPermission("앱을 실행하려면 퍼미션을 허가하셔야합니다.");
                                return;
                            }
                        }
                    }
                    break;
            }
        }
    
        private void showDialogforPermission(String msg) {
    
            final AlertDialog.Builder myDialog = new AlertDialog.Builder(  MainActivity.this);
            myDialog.setTitle("알림");
            myDialog.setMessage(msg);
            myDialog.setCancelable(false);
            myDialog.setPositiveButton("예", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface arg0, int arg1) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        requestPermissions(PERMISSIONS, PERMISSION_REQUEST_CODE);
                    }
    
                }
            });
            myDialog.setNegativeButton("아니오", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface arg0, int arg1) {
                    finish();
                }
            });
            myDialog.show();
        }
    }
    




    6-1. native-lib.cpp

    #include "opencv2/core.hpp"
    #include "opencv2/core/utility.hpp"
    #include "opencv2/core/ocl.hpp"
    #include "opencv2/imgcodecs.hpp"
    #include "opencv2/highgui.hpp"
    #include "opencv2/features2d.hpp"
    #include "opencv2/calib3d.hpp"
    #include "opencv2/imgproc.hpp"
    #include "opencv2/xfeatures2d.hpp"
    #include <iostream>
    #include <android/log.h>
    
    using namespace cv;
    using namespace cv::xfeatures2d;
    using namespace std;
    
    const int LOOP_NUM = 10;
    const int GOOD_PTS_MAX = 50;
    const float GOOD_PORTION = 0.15f;
    
    int64 work_begin = 0;
    int64 work_end = 0;
    
    static void workBegin()
    {
        work_begin = getTickCount();
    }
    
    static void workEnd()
    {
        work_end = getTickCount() - work_begin;
    }
    
    static double getTime()
    {
        return work_end / ((double)getTickFrequency()) * 1000.;
    }
    
    struct SURFDetector
    {
        Ptr<Feature2D> surf;
        SURFDetector(double hessian = 800.0)
        {
            surf = SURF::create(hessian);
        }
        template<class T>
        void operator()(const T& in, const T& mask, std::vector<cv::KeyPoint>& pts, T& descriptors, bool useProvided = false)
        {
            surf->detectAndCompute(in, mask, pts, descriptors, useProvided);
        }
    };
    
    template<class KPMatcher>
    struct SURFMatcher
    {
        KPMatcher matcher;
        template<class T>
        void match(const T& in1, const T& in2, std::vector<cv::DMatch>& matches)
        {
            matcher.match(in1, in2, matches);
        }
    };
    
    static Mat drawGoodMatches(
            const Mat& img1,
            const Mat& img2,
            const std::vector<KeyPoint>& keypoints1,
            const std::vector<KeyPoint>& keypoints2,
            std::vector<DMatch>& matches,
            std::vector<Point2f>& scene_corners_
    )
    {
        //-- Sort matches and preserve top 10% matches
        std::sort(matches.begin(), matches.end());
        std::vector< DMatch > good_matches;
        double minDist = matches.front().distance;
        double maxDist = matches.back().distance;
    
        const int ptsPairs = std::min(GOOD_PTS_MAX, (int)(matches.size() * GOOD_PORTION));
        for (int i = 0; i < ptsPairs; i++)
        {
            good_matches.push_back(matches[i]);
        }
        std::cout << "\nMax distance: " << maxDist << std::endl;
        std::cout << "Min distance: " << minDist << std::endl;
    
        std::cout << "Calculating homography using " << ptsPairs << " point pairs." << std::endl;
    
        // drawing the results
        Mat img_matches;
    
    
        drawMatches(img1, keypoints1, img2, keypoints2,
                    good_matches, img_matches, Scalar::all(-1), Scalar::all(-1),
                    std::vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
    
    
        //-- Localize the object
        std::vector<Point2f> obj;
        std::vector<Point2f> scene;
    
        for (size_t i = 0; i < good_matches.size(); i++)
        {
            //-- Get the keypoints from the good matches
            obj.push_back(keypoints1[good_matches[i].queryIdx].pt);
            scene.push_back(keypoints2[good_matches[i].trainIdx].pt);
        }
        //-- Get the corners from the image_1 ( the object to be "detected" )
        std::vector<Point2f> obj_corners(4);
        obj_corners[0] = Point(0, 0);
        obj_corners[1] = Point(img1.cols, 0);
        obj_corners[2] = Point(img1.cols, img1.rows);
        obj_corners[3] = Point(0, img1.rows);
        std::vector<Point2f> scene_corners(4);
    
        Mat H = findHomography(obj, scene, RANSAC);
        perspectiveTransform(obj_corners, scene_corners, H);
    
        scene_corners_ = scene_corners;
    
        //-- Draw lines between the corners (the mapped object in the scene - image_2 )
        line(img_matches,
             scene_corners[0] + Point2f((float)img1.cols, 0), scene_corners[1] + Point2f((float)img1.cols, 0),
             Scalar(0, 255, 0), 2, LINE_AA);
        line(img_matches,
             scene_corners[1] + Point2f((float)img1.cols, 0), scene_corners[2] + Point2f((float)img1.cols, 0),
             Scalar(0, 255, 0), 2, LINE_AA);
        line(img_matches,
             scene_corners[2] + Point2f((float)img1.cols, 0), scene_corners[3] + Point2f((float)img1.cols, 0),
             Scalar(0, 255, 0), 2, LINE_AA);
        line(img_matches,
             scene_corners[3] + Point2f((float)img1.cols, 0), scene_corners[0] + Point2f((float)img1.cols, 0),
             Scalar(0, 255, 0), 2, LINE_AA);
    
        return img_matches;
    }
    
    
    float resize(UMat img_src, UMat &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;
    }
    



    6-2. native-lib.cpp

    ocl::setUseOpenCL(true);
    
        UMat img1, img2;
    
        Mat &img_object = *(Mat *) objectImage;
        Mat &img_scene = *(Mat *) sceneImage;
    
    
    
        img_object.copyTo(img1);
        img_scene.copyTo(img2);
    
        float resizeRatio = resize(img2, img2, 800);
        resize(img1, img1, 800);
    
    
        cvtColor( img1, img1, COLOR_RGBA2GRAY);
        cvtColor( img2, img2, COLOR_RGBA2GRAY);
    
    
        double surf_time = 0.;
    
        //declare input/output
        std::vector<KeyPoint> keypoints1, keypoints2;
        std::vector<DMatch> matches;
    
        UMat _descriptors1, _descriptors2;
        Mat descriptors1 = _descriptors1.getMat(ACCESS_RW),
                descriptors2 = _descriptors2.getMat(ACCESS_RW);
    
        //instantiate detectors/matchers
        SURFDetector surf;
    
        SURFMatcher<BFMatcher> matcher;
    
        //-- start of timing section
    
        for (int i = 0; i <= LOOP_NUM; i++)
        {
            if (i == 1) workBegin();
            surf(img1.getMat(ACCESS_READ), Mat(), keypoints1, descriptors1);
            surf(img2.getMat(ACCESS_READ), Mat(), keypoints2, descriptors2);
            matcher.match(descriptors1, descriptors2, matches);
        }
        workEnd();
    
        __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",
                            "%d keypoints on object image", keypoints1.size());
        __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",
                            "%d keypoints on scene image", keypoints2.size());
    
        surf_time = getTime();
        __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ",
                            "SURF run time:  %f ms", surf_time / LOOP_NUM);
    
    
        std::vector<Point2f> corner;
        Mat img_matches = drawGoodMatches(img1.getMat(ACCESS_READ), img2.getMat(ACCESS_READ), keypoints1, keypoints2, matches, corner);
    
    
        line(img_scene, Point2f(corner[0].x/resizeRatio, corner[0].y/resizeRatio), Point2f(corner[1].x/resizeRatio, corner[1].y/resizeRatio), Scalar(0, 255, 0, 255), 10);
        line(img_scene, Point2f(corner[1].x/resizeRatio, corner[1].y/resizeRatio), Point2f(corner[2].x/resizeRatio, corner[2].y/resizeRatio), Scalar(0, 255, 0, 255), 10);
        line(img_scene, Point2f(corner[2].x/resizeRatio, corner[2].y/resizeRatio), Point2f(corner[3].x/resizeRatio, corner[3].y/resizeRatio), Scalar(0, 255, 0, 255), 10);
        line(img_scene, Point2f(corner[3].x/resizeRatio, corner[3].y/resizeRatio), Point2f(corner[0].x/resizeRatio, corner[0].y/resizeRatio), Scalar(0, 255, 0, 255), 10);
    
        __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ", "draw box %f %f", corner[0].x/resizeRatio, corner[0].y/resizeRatio );
        __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ", "draw box %f %f", corner[1].x/resizeRatio, corner[1].y/resizeRatio );
        __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ", "draw box %f %f", corner[2].x/resizeRatio, corner[2].y/resizeRatio );
        __android_log_print(ANDROID_LOG_DEBUG, "native-lib :: ", "draw box %f %f", corner[3].x/resizeRatio, corner[3].y/resizeRatio );
    



    7. CMakeLists.txt

    # For more information about using CMake with Android Studio, read the
    # documentation: https://d.android.com/studio/projects/add-native-code.html
    
    # Sets the minimum version of CMake required to build the native library.
    
    cmake_minimum_required(VERSION 3.4.1)
    
    set(pathPROJECT C:/Users/webnautes/AndroidStudioProjects/UseOpenCVwithCMake) # 수정필요
    set(pathOPENCV ${pathPROJECT}/opencv)
    set(pathLIBOPENCV_JAVA ${pathOPENCV}/native/libs/${ANDROID_ABI}/libopencv_java4.so)
    
    set(CMAKE_VERBOSE_MAKEFILE on)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
    
    include_directories(${pathOPENCV}/native/jni/include)
    
    
    # Creates and names a library, sets it as either STATIC
    # or SHARED, and provides the relative paths to its source code.
    # You can define multiple libraries, and CMake builds them for you.
    # Gradle automatically packages shared libraries with your APK.
    
    add_library( # Sets the name of the library.
            native-lib
    
            # Sets the library as a shared library.
            SHARED
    
            # Provides a relative path to your source file(s).
            ${pathPROJECT}/app/src/main/cpp/native-lib.cpp )
    
    
    
    add_library( lib_opencv SHARED IMPORTED )
    
    set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION ${pathLIBOPENCV_JAVA})
    
    
    # Searches for a specified prebuilt library and stores the path as a
    # variable. Because CMake includes system libraries in the search path by
    # default, you only need to specify the name of the public NDK library
    # you want to add. CMake verifies that the library exists before
    # completing its build.
    
    find_library( # Sets the name of the path variable.
            log-lib
    
            # Specifies the name of the NDK library that
            # you want CMake to locate.
            log )
    
    # Specifies libraries CMake should link to your target library. You
    # can link multiple libraries, such as libraries you define in this
    # build script, prebuilt third-party libraries, or system libraries.
    
    target_link_libraries( # Specifies the target library.
            native-lib
    
            lib_opencv
    
            # Links the target library to the log library
            # included in the NDK.
            ${log-lib} )
    



    포스트 작성시에는 문제 없었지만 이후 문제가 생길 수 있습니다.
    댓글로 알려주시면 빠른 시일내에 답변을 드리겠습니다.

    여러분의 응원으로 좋은 컨텐츠가 만들어집니다. 지금 본 내용이 도움이 되었다면 유튜브 구독 부탁드립니다. 감사합니다 : )

    유튜브 구독하기


    댓글 55

    • 이전 댓글 더보기
    • sh감기약 2019.05.22 00:38


      덕분에 해결이 됬습니다.
      말씀하신데로 nonfree 옵션 문제 인것 같습니다.
      감사합니다.

    • Jo 2019.05.22 03:45


      게시글 보고 응용하고 있는데
      오브젝트 이미지를 사용자가 선택하는게 아니라
      미리 어플내에서 이미지를 정해놓고 싶다면
      코드의 어느부분을 어떻게 수정하면 될까요?

    • LeeJae 2019.05.22 19:36


      Build command failed.
      Error while executing process C:\Users\JG\AppData\Local\Android\Sdk\cmake\3.6.4111459\bin\cmake.exe with arguments {--build C:\Users\JG\AndroidStudioProjects\SurfExample\app\.externalNativeBuild\cmake\debug\arm64-v8a --target native-lib}
      [1/2] Building CXX object CMakeFiles/native-lib.dir/native-lib.cpp.o
      FAILED: C:\Users\JG\AppData\Local\Android\Sdk\ndk-bundle\toolchains\llvm\prebuilt\windows-x86_64\bin\clang++.exe --target=aarch64-none-linux-android21 --gcc-toolchain=C:/Users/JG/AppData/Local/Android/Sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64 --sysroot=C:/Users/JG/AppData/Local/Android/Sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/sysroot -Dnative_lib_EXPORTS -IC:/Users/jg/AndroidStudioProjects/SurfExample/opencv/native/jni/include -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -Werror=format-security -stdlib=libc++ -std=gnu++11 -O0 -fno-limit-debug-info -fPIC -MD -MT CMakeFiles/native-lib.dir/native-lib.cpp.o -MF CMakeFiles\native-lib.dir\native-lib.cpp.o.d -o CMakeFiles/native-lib.dir/native-lib.cpp.o -c C:\Users\JG\AndroidStudioProjects\SurfExample\app\src\main\cpp\native-lib.cpp
      C:\Users\JG\AndroidStudioProjects\SurfExample\app\src\main\cpp\native-lib.cpp:10:10: fatal error: 'opencv2/xfeatures2d.hpp' file not found

      #include "opencv2/xfeatures2d.hpp"

      ^~~~~~~~~~~~~~~~~~~~~~~~~

      1 error generated.

      ninja: build stopped: subcommand failed.


      -----------------------------------------------
      안녕하세요 .. 다음과 같은 오류가 나는데 ninja 설치를 잘모한건가요 ??

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.05.22 19:40 신고


        opencv를 새로 빌드할때 nonfree 옵션이 활성화 안되서 그렇습니다. 안드로이드용 opencv 빌드 방법 다룬 포스트를 보고 다시 진행하세요.

    • abcdef 2019.05.22 22:15


      Build command failed.
      Error while executing process D:\android-studio-sdk\cmake\3.10.2.4988404\bin\cmake.exe with arguments {--build D:\opencv_test_surf_example_no_00001\app\.externalNativeBuild\cmake\debug\armeabi-v7a --target native-lib}
      [1/1] Linking CXX shared library D:\opencv_test_surf_example_no_00001\app\build\intermediates\cmake\debug\obj\armeabi-v7a\libnative-lib.so
      FAILED: D:/opencv_test_surf_example_no_00001/app/build/intermediates/cmake/debug/obj/armeabi-v7a/libnative-lib.so
      cmd.exe /C "cd . && D:\android-studio-sdk\ndk-bundle\toolchains\llvm\prebuilt\windows-x86_64\bin\clang++.exe --target=armv7-none-linux-androideabi21 --gcc-toolchain=D:/android-studio-sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64 --sysroot=D:/android-studio-sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/sysroot -fPIC -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -mfpu=vfpv3-d16 -fno-addrsig -march=armv7-a -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -stdlib=libc++ -std=gnu++11 -O0 -fno-limit-debug-info -Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libatomic.a -static-libstdc++ -Wl,--build-id -Wl,--warn-shared-textrel -Wl,--fatal-warnings -Wl,--exclude-libs,libunwind.a -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -shared -Wl,-soname,libnative-lib.so -o D:\opencv_test_surf_example_no_00001\app\build\intermediates\cmake\debug\obj\armeabi-v7a\libnative-lib.so CMakeFiles/native-lib.dir/native-lib.cpp.o D:/opencv_test_surf_example_no_00001/opencv/native/libs/armeabi-v7a/libopencv_java4.so -llog -latomic -lm && cd ."
      D:/opencv_test_surf_example_no_00001/app/src/main/cpp/native-lib.cpp:45: error: undefined reference to 'cv::xfeatures2d::SURF::create(double, int, int, bool, bool)'
      clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)
      ninja: build stopped: subcommand failed.


      안녕하세요.... 빌드하다가 이런 에러가 뜨는데 혹시 에러원인을 알 수 있을까요 ???
      알려주시면 감사하겠습니다.

    • leejae 2019.05.23 01:12


      항상 좋은글 너무 감사합니다.!...
      이미지가 아닌 영상에서 추출을 하려면 어떻게 해야할까요 .. 어느부분을 손봐야할까요 ...............

    • sh감기약 2019.05.23 17:10


      어플 내에서 minDist 값을 표시하고 싶습니다.
      minDist 값을 어떻게 가져와야 하는지 알려주시면 감사하겠습니다.

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.05.23 18:02 신고


        자바에서 리턴값을 double 로 바꾸고

        public native double imageprocessing(long objectImage, long sceneImage);


        cpp에서는 리턴값을 jdouble로 바꾸면..

        cpp 함수의 리턴값을 자바에서 받을 수있습니다.

    • sh감기약 2019.05.23 18:55


      좋은 정보 알려주셔서 감사합니다.
      덕분에 문제가 해결 되었습니다.
      정말로 감사합니다.

    • lsy 2019.05.23 21:14


      해당자료를 올려주셔서 학습할 수 있게 해주신 것에 감사 드립니다.
      해당 자료를 그대로 따라하고 핸드폰에 연결하여 빌드하고 실행은 가능 했지만
      build apk를 실행 했을때 아래와 같은 코드가 나오면서 오류가 나옵니다.

      Build command failed.
      Error while executing process
      [1/2] Building CXX object CMakeFiles/native-lib.dir/native-lib.cpp.o
      [2/2] Linking CXX shared library
      error: linker command failed with exit code 1 (use -v to see invocation)
      clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)
      ninja: build stopped: subcommand failed.

      이 프로젝트를 어떻게 해야 build apk 할 수 있나요?


    • 히히히 2019.05.27 23:17


      안녕하세요! 항상 잘 보고있습니다 :)
      출력되는 사진의 사각형의 그려주는 부분이
      line(img_scene, Point2f(corner[0].x/resizeRatio, corner[0].y/resizeRatio), Point2f(corner[1].x/resizeRatio, corner[1].y/resizeRatio), Scalar(0, 255, 0, 255), 10);
      line(img_scene, Point2f(corner[1].x/resizeRatio, corner[1].y/resizeRatio), Point2f(corner[2].x/resizeRatio, corner[2].y/resizeRatio), Scalar(0, 255, 0, 255), 10);
      line(img_scene, Point2f(corner[2].x/resizeRatio, corner[2].y/resizeRatio), Point2f(corner[3].x/resizeRatio, corner[3].y/resizeRatio), Scalar(0, 255, 0, 255), 10);
      line(img_scene, Point2f(corner[3].x/resizeRatio, corner[3].y/resizeRatio), Point2f(corner[0].x/resizeRatio, corner[0].y/resizeRatio), Scalar(0, 255, 0, 255), 10);

      인거죠? 만약 사각형 사진을 그려준후 어떤 동작을 시켜주려고 다음 코드 뒤에 적어봤는데 안되서 이렇게 문의드려요.

    • LeeJae 2019.05.28 01:22


      코드

      원본

      찍은사진

      ---

      연어



      개발과정



      C:\Users\JG\AndroidStudioProjects\SurfExample\app\src\main\res\drawable\F1.jpg: Error: 'F' is not a valid file-based resource name character: File-based resource names must contain only lowercase a-z, 0-9, or underscore


      objectImage

      1. 오브젝트 이미지를 사용자가 선택이아닌 미리 어플내에 정해 놓고싶은데 onActivityResult 에서


      protected void onActivityResult(int requestCode, int resultCode, Intent data) {

      if ( requestCode == GET_GALLERY_IMAGE1){


      if (data.getData() != null) {
      Uri uri = data.getData();

      try {
      String path = getRealPathFromURI(uri);
      int orientation = getOrientationOfImage(path); // 런타임 퍼미션 필요
      Bitmap temp = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
      Bitmap bitmap = getRotatedBitmap(temp, orientation);
      imageVIewObject.setImageBitmap(bitmap);

      img_object = new Mat();
      Bitmap bmp32 = bitmap.copy(Bitmap.Config.ARGB_8888, true);
      Utils.bitmapToMat(bmp32, img_object);


      } catch (Exception e) {
      e.printStackTrace();
      }

      }

      object에 이미지를 어떻게 넣어야하나요 ..

      2. 여러 장의 사진을 넣을수있나요?

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.05.28 05:43 신고


        1.
        assets 디렉토리에 이미지를 넣어서 불러오는 방법이 있습니다.

        https://stackoverflow.com/a/7645606을 참고하세요


        2.
        가능합니다.

        오브젝트 이미지 1장과 오브젝트가 포함된 여러장의 이미지를 사용할수 있습니다.

        하지만 검출시에는 오브젝트 이미지 1장과 오브젝트가 포함된 이미지 1장씩 비교하는 것을 반복해야합니다.


    • sh감기약 2019.05.28 12:47


      안녕하세요.
      개발자님의 블로그를 통해 opencv를 공부하고 있는 학생입니다.
      다름이 아니라 위의 코드와 방법을 동일시 하고 스마트폰으로 run을 하면 매우 잘 동작하지만
      build apk를 실행하려고 하면 다음과 같은 오류를 출력합니다.

      -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

      Build command failed.
      Error while executing process D:\android_studio_sdk\cmake\3.10.2.4988404\bin\cmake.exe with arguments {--build D:\android_app_surf_test_0001\app\.externalNativeBuild\cmake\debug\x86_64 --target native-lib}
      [1/2] Building CXX object CMakeFiles/native-lib.dir/native-lib.cpp.o
      D:/android_app_surf_test_0001/app/src/main/cpp/native-lib.cpp:199:57: warning: format specifies type 'int' but the argument has type 'std::__ndk1::vector<cv::KeyPoint, std::__ndk1::allocator<cv::KeyPoint> >::size_type' (aka 'unsigned long') [-Wformat]
      "%d keypoints on object image", keypoints1.size());
      ~~ ^~~~~~~~~~~~~~~~~
      %lu
      D:/android_app_surf_test_0001/app/src/main/cpp/native-lib.cpp:201:56: warning: format specifies type 'int' but the argument has type 'std::__ndk1::vector<cv::KeyPoint, std::__ndk1::allocator<cv::KeyPoint> >::size_type' (aka 'unsigned long') [-Wformat]
      "%d keypoints on scene image", keypoints2.size());
      ~~ ^~~~~~~~~~~~~~~~~
      %lu
      2 warnings generated.
      [2/2] Linking CXX shared library D:\android_app_surf_test_0001\app\build\intermediates\cmake\debug\obj\x86_64\libnative-lib.so
      FAILED: D:/android_app_surf_test_0001/app/build/intermediates/cmake/debug/obj/x86_64/libnative-lib.so
      cmd.exe /C "cd . && D:\android_studio_sdk\ndk-bundle\toolchains\llvm\prebuilt\windows-x86_64\bin\clang++.exe --target=x86_64-none-linux-android21 --gcc-toolchain=D:/android_studio_sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64 --sysroot=D:/android_studio_sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/sysroot -fPIC -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -fno-addrsig -Wa,--noexecstack -Wformat -Werror=format-security -stdlib=libc++ -std=gnu++11 -O0 -fno-limit-debug-info -Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libatomic.a -static-libstdc++ -Wl,--build-id -Wl,--warn-shared-textrel -Wl,--fatal-warnings -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -shared -Wl,-soname,libnative-lib.so -o D:\android_app_surf_test_0001\app\build\intermediates\cmake\debug\obj\x86_64\libnative-lib.so CMakeFiles/native-lib.dir/native-lib.cpp.o D:/android_app_surf_test_0001/opencv/native/libs/x86_64/libopencv_java4.so -llog -latomic -lm && cd ."
      D:/android_app_surf_test_0001/app/src/main/cpp/native-lib.cpp:41: error: undefined reference to 'cv::xfeatures2d::SURF::create(double, int, int, bool, bool)'
      clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)
      ninja: build stopped: subcommand failed.

      -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

      혹시 해당 오류들의 의미와 발생하는 이유에 대해서 알려주시면 감사하겠습니다.

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.05.28 13:11 신고


        arm64-v8a를 위해서만 OpenCV를 빌드했기 때문인듯합니다.

        다음처럼 build.gradle에 추가해보세요..

        android {

        ..........................................

        ndk {
        abiFilters "arm64-v8a"
        }
        }

    • sh감기약 2019.05.28 14:00


      build.gradle app 에서 추가한 결과 입니다.

      ERROR: Could not find method ndk() for arguments [build_1j9xtbbsu2kpdrn14exnradaj$_run_closure1$_closure6@97844f1] on object of type com.android.build.gradle.internal.dsl.BaseAppModuleExtension.

    • sh감기약 2019.05.28 14:05


      여기를 보고 해결 했습니다.
      개발자님 정말로 좋은 답변 감사합니다.
      ㅎㅎ 정말 감사합니다.

    • 또끼심심 2019.05.30 18:00


      Build command failed.
      Error while executing process C:\Users\ub\AppData\Local\Android\Sdk\cmake\3.10.2.4988404\bin\cmake.exe with arguments {--build C:\Users\ub\AndroidStudioProjects\surf2\app\.externalNativeBuild\cmake\debug\armeabi-v7a --target native-lib}
      [1/1] Linking CXX shared library C:\Users\ub\AndroidStudioProjects\surf2\app\build\intermediates\cmake\debug\obj\armeabi-v7a\libnative-lib.so
      FAILED: C:/Users/ub/AndroidStudioProjects/surf2/app/build/intermediates/cmake/debug/obj/armeabi-v7a/libnative-lib.so
      cmd.exe /C "cd . && C:\Users\ub\AppData\Local\Android\Sdk\ndk-bundle\toolchains\llvm\prebuilt\windows-x86_64\bin\clang++.exe --target=armv7-none-linux-androideabi21 --gcc-toolchain=C:/Users/ub/AppData/Local/Android/Sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64 --sysroot=C:/Users/ub/AppData/Local/Android/Sdk/ndk-bundle/toolchains/llvm/prebuilt/windows-x86_64/sysroot -fPIC -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -mfpu=vfpv3-d16 -fno-addrsig -march=armv7-a -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -stdlib=libc++ -std=gnu++11 -O0 -fno-limit-debug-info -Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libatomic.a -static-libstdc++ -Wl,--build-id -Wl,--warn-shared-textrel -Wl,--fatal-warnings -Wl,--exclude-libs,libunwind.a -Wl,--no-undefined -Qunused-arguments -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -shared -Wl,-soname,libnative-lib.so -o C:\Users\ub\AndroidStudioProjects\surf2\app\build\intermediates\cmake\debug\obj\armeabi-v7a\libnative-lib.so CMakeFiles/native-lib.dir/native-lib.cpp.o C:/Users/ub/AndroidStudioProjects/surf2/opencv/native/libs/armeabi-v7a/libopencv_java4.so -llog -latomic -lm && cd ."
      C:/Users/ub/AndroidStudioProjects/surf2/app/src/main/cpp/native-lib.cpp:45: error: undefined reference to 'cv::xfeatures2d::SURF::create(double, int, int, bool, bool)'
      clang++.exe: error: linker command failed with exit code 1 (use -v to see invocation)
      ninja: build stopped: subcommand failed.

      잘 모르겠어요.. ㅠ,.ㅠ 쌩

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.05.30 18:17 신고


        아래 포스트 보고 빌드한 것을 사용했나요?

        Android용 OpenCV 빌드하는 방법(contrib 포함)

        https://webnautes.tistory.com/1268

      • 또끼심심 2019.05.31 16:14


        내 여기에 나와 있는 Android용 OpenCV 빌드하는 방법(contrib 포함)
        대로 했습니다

        카메라 예제 및 프로젝트 생성방법(CMake 사용) 해봤는데 잘 돌아가요

        그럼 빌드 잘못은 아닌건가요?

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.05.31 17:01 신고


        혹시 빌드한 후.. 새로 프로젝트를 만들어서 SURF 테스트한건가요?

        이상하게도
        헤더파일은 NONFREE 옵션이 추가되었을때 꺼 같고..
        라이브러리 파일은 NONFREE 옵션이 추가안된 상태같습니다.




      • 2019.06.04 13:35


        비밀댓글입니다

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.06.04 13:56 신고


        안드로이드 스튜디오에서 프로젝트를 새로 생성해도 그런가요?

      • 또끼심심 2019.06.04 17:51


        네 새로운 프로젝트에서 새로 작성 했습니다.

    • LeeJae 2019.06.03 23:31


      안녕하세요.. 질문이 있습니다.
      예제를 보던중 (OpenCV의 SURF 동영상 매칭 테스트) 이것과 (Android OpenCV 예제 - SURF를 사용한 오브젝트 검출 테스트) 둘다 surf를 사용하기떄문에 계산식이 같다는 것을 보고

      안드로이드에서 실시간 동영상으로 화면을 보면서 물체가 잡히면 테두리가 생기게 하고싶어서 질문드립니다...

      1. Vvisual studio에서는 캡처를 하고 캡처이미지와 현재 들어오는 영상과 비교를 진행했는데 android에서 캡처 대신에 drawable에 이미지를 넣어두고 들어오는 이미지와 비교를 하고싶은데
      어떻게 해야할지 ,,, 조언좀 해주세요 ㅠㅠ

    • 또끼심심 2019.06.04 17:45


      "라이브러리 파일은 NONFREE 옵션이 추가안된 상태같습니다."
      몇번을 따라 했는데. 어디서 어떻게 추가 해야 하는지 모르겠어요.

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.06.04 17:54 신고


        아래 포스트에 NONFREE 옵션이 추가되어 있습니다.. 기존에 진행했던 빌드 결과물을 삭제후 해보세요..

        Android용 OpenCV 빌드하는 방법(contrib 포함)
        https://webnautes.tistory.com/1268

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.06.04 17:57 신고


        cmake 실행시 NONFREE 옵션이 인식되었다면 cmake 실행결과의 다음 항목에
        OpenCV modules:
        -- To be built:

        xfeatures2d가 포함되어있습니다. 확인해보세요.

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.06.04 17:59 신고


        혹시 기존에 사용하던 안드로이드용 OpenCV를 제거 안하고 사용하나요?

        헤더파일은 되는데 라이브러리 파일은 인식안되는 게 이상하네요..

    • cho 2019.06.21 11:35


      안녕하세요, 올려주신 자료를 통해 많이 배울 수 있어서 감사드립니다 ^^
      혹시 가능하시다면 안드로이드에 비디오 파일을 frame별로 처리하는 주제로도 컨텐츠를 볼 수 있으면 좋을 것 같습니다.
      항상 좋은 자료에 감사드립니다.

    • jojo 2019.07.18 16:12


      OpenCV camera를 사용하지 않고 일반 카메라로 SURF를 사용할 수 있을까요??

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.07.18 16:13 신고


        OpenCV 카메라 API를 사용하지 않고 Android 카메라 API를 사용한다는 의미인가요?

      • jojo 2019.07.18 16:28


        네..
        지금 SURF를 사용하고 있는데 속도가 너무 느려서요. 이미지 사이즈도 줄여보고 반복문 숫자도 줄여봤는데 속도가 빠르지 않습니다. 혹시 카메라를 바꾸면 좀 달라질까 해서 여쭤보았습니다.

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.07.18 16:33 신고


        카메라를 바꾼다고 개선되지는 않습니다.

        surf 함수의 파라미터를 조정해야 합니다.
        파라미터에 대한 걸 찾아봐야 합니다.

      • jojo 2019.07.18 16:35


        그렇군요..
        감사합니다:)

    • 신동철 2019.11.23 16:06


      안녕하세요.
      잘보고있습니다. 주인장님.
      그런데, opencv에서 contrib를 포함시킨 빌드를 수행하는법 글을 보고 이제는 예제를 수행해보려고
      이 글에서의 surf 예제를 보고있는데요.
      말씀해주신대로 따라서 지금까지 했으나, cannot find 'opencv2' 라고하면서 인식이 되지가 않습니다.
      왜그럴까요?...
      C:\
      OpenCV-android-sdk
      \sdk
      native
      \jni\
      include\
      opencv2
      이 경로에 해당파일들은 잘있는데 인식이 안되네요 ..

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.11.24 16:41 신고


        CMakeLists.txt 문제일 가능성이 있습니다.
        다음 세줄을 확인해보세요.

        set(pathPROJECT C:/Users/webnautes/AndroidStudioProjects/UseOpenCVwithCMake) # 수정필요
        set(pathOPENCV ${pathPROJECT}/opencv)
        set(pathLIBOPENCV_JAVA ${pathOPENCV}/native/libs/${ANDROID_ABI}/libopencv_java4.so)

    • 신동철 2019.11.28 01:24


      webnautes님 이전 답글 감사합니다. 덕분에 그부분은 해결이 되었는데요.

      현재는 새로운문제에 봉착했습니다. 아래에 오류메세지 작성하였습니다..

      cmake문제같은데 이것저것해봐도 잘안되서 또다시 도음을 요청해봅니다...

      제 컴퓨터의 환경은 webnautes 님이 말씀하신것 다설치하였는데요 , 제가 생각하기에 문제는 android studio를 설치할

      때 sdk manager에서 기본으로 설정되어있는 파일도 같이 설치되서 그런것같아요.
      cmake도 기본으로 3.10.2 로 되어있어서 내비두었어요.
      이런것도 해제하고 해야하나요? 그러면 해제하고나서 제가 수동으로 설치한 cmake 3.15.2는 어떻게 안드로이드 스튜디오하고 연결시켜야할까요..??

      Build command failed.
      Error while executing process C:\Users\Shin-dong-cheul\AppData\Local\Android\Sdk\cmake\3.10.2.4988404\bin\cmake.exe with arguments {--build C:\Users\Shin-dong-cheul\Desktop\Surfex\app\.externalNativeBuild\cmake\debug\x86 --target native-lib}

      ninja: error: 'C:/Users/Shin-dong-cheul/Desktop/Surfex/opencv/native/libs/x86/libopencv_java4.so', needed by 'C:/Users/Shin-dong-cheul/Desktop/Surfex/app/build/intermediates/cmake/debug/obj/x86/libnative-lib.so', missing and no known rule to make it

      • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2019.11.28 08:48 신고


        CMakeLists.txt에 있는 다음 세줄 경로를 다시 확인해보세요..

        set(pathPROJECT C:/Users/webnautes/AndroidStudioProjects/UseOpenCVwithCMake) # 수정필요
        set(pathOPENCV ${pathPROJECT}/opencv)
        set(pathLIBOPENCV_JAVA ${pathOPENCV}/native/libs/${ANDROID_ABI}/libopencv_java4.so)

Designed by Tistory.