Java
OpenCV
OpenCVDay 23

JavaでOpenCV_Contrib(ArUco)を使う!(後編 - プログラミング編)

はじめに

本当はもっと早く書こうと思ってたんですけれど、遅れました。(大晦日前の夜に書きはじめた…)
一応、OpenCV Advent Calendar 2018に登録しています。(2019年の正月に投稿しておいて何言ってんだこいつ)

この記事は、前後編となっております。ArUcoライブラリのビルドをされてない方は、
JavaでOpenCV_Contrib(ArUco)を使う!(前編 - ビルド編) を参照してください。

また、今回使ったソースコードはだいたいOpenCV_ArucoTest[GitHub]にありますので、参考にしていただければ幸いです。

ArUcoをEclipseで使う準備

既存のOpenCVをインポートする手順と同様に、Contrib内包のjarファイルで行ってください。

Marker画像の生成

ArUcoライブラリがちゃんとビルド、セットアップされているか確認をするために
Marker画像の生成を行ってみましょう。

使う関数は、
・Aruco.drawMarker(dictionary, markerID, sidePixels, markerImage)
dictionary: Markerの種類を決めるものです。四角の大きさや解像度、種類の数が異なります。
markerID: Dictionaryに定義されたMarkerのIDで、それぞれで形が異なります。
sidePixels: 解像度を定めます。

以下ソースコード(詳細: createMarker[GitHub])

    public static void createMarker() {
        Dictionary dictionary = Aruco.getPredefinedDictionary(Aruco.DICT_4X4_50);

        final int markerID = 0;
        final int sidePixels = 200;
        Mat markerImage = new Mat();
        Aruco.drawMarker(dictionary, markerID, sidePixels, markerImage);

        Imgcodecs.imwrite("F:\\users\\smk7758\\Desktop\\marker_2018-12-01.png", markerImage);
    }

marker_2018-12-01.png
こんなのが生成されるはずです。

Markerの認識

さて、ここから本番です。

使う関数は次の2つ
・Aruco.detectMarkers(inputImage, dictionary, corners, markerIds, parameters);
・Aruco.drawDetectedMarkers(inputImage, corners, markerIds);
まぁ関数の通りですが、detectMarkersは複数のマーカーを認識します。
その後、cornersを用いて、drawDetectedMarkersではMarkerの四隅をなぞるようなことをします。
corners: Markerの画面上の座標を返却します。(なんでListかと言うと、複数のMarkerの認識をすることと、Matは行列であり座標を保管できるからです)

(他の引数は省略)

以下ソースコード(詳細: detectMarker[GitHub])

    public static void detectMarker() {
        Dictionary dictionary = Aruco.getPredefinedDictionary(Aruco.DICT_4X4_50);

        Mat inputImage = Imgcodecs.imread("F:\\users\\smk7758\\Desktop\\marker_2018-12-01_test.png");
        List<Mat> corners = new ArrayList<>();
        Mat markerIds = new Mat();
        DetectorParameters parameters = DetectorParameters.create();
        Aruco.detectMarkers(inputImage, dictionary, corners, markerIds, parameters);

        Aruco.drawDetectedMarkers(inputImage, corners, markerIds);

        Imgcodecs.imwrite("F:\\users\\smk7758\\Desktop\\marker_2018-12-01_detected.png", inputImage);
    }

marker_2018-12-01_test.png
適当にGIMPで変形したものですが、上の画像では、

marker_2018-12-01_detected.png
こんな感じになってくれるはずです。

カメラを用いたリアルタイムMarker認識

正直ここまで来たら、ほとんど自己満足みたいなものですが、次はJavaFXとVideoCapture(OpenCV)を組み合わせて、リアルタイム認識を行いました。

先ほどと違うのはJavaFXで動かすので、ScheduledServiceというやつを使って、返す値はImage型ということです。
ScheduledServiceとは、JavaFXでマルチスレッド処理を行う際に使うクラスで、Taskを自動的に復帰させることができるものだそうです。詳細は公式JavaDocを参照してください。
また、Image型で返す理由は、Controllerクラス側で定義されている画像を表示する要素が、引数に
Imageをとるためです。MatからImageに変換するやつは適当に調べて出てきたものを使いました。

以下ソースコード(詳細: detectMarkerByCamera - MarkerDetectorService.java [GitHub])

    @Override
    protected Task<Image> createTask() {
        return new Task<Image>() {
            @Override
            protected Image call() throws Exception {
                if (!vc.isOpened()) {
                    System.err.println("VC is not opened.");
                    this.cancel();
                    return null;
                }

                Mat inputImage = new Mat();

                if (!vc.read(inputImage) || inputImage == null) {
                    System.err.println("Cannot load camera image.");
                    this.cancel();
                    return null;
                }

                List<Mat> corners = new ArrayList<>();
                Mat markerIds = new Mat();
                // DetectorParameters parameters = DetectorParameters.create();
                Aruco.detectMarkers(inputImage, dictionary, corners, markerIds);

                Aruco.drawDetectedMarkers(inputImage, corners, markerIds);

                return convertMatToImage(inputImage);
            }
        };
    }

    private Image convertMatToImage(Mat inputImage) {
        MatOfByte byte_mat = new MatOfByte();
        Imgcodecs.imencode(".bmp", inputImage, byte_mat);

        return new Image(new ByteArrayInputStream(byte_mat.toArray()));
    }

SC_test_2018-12-2_21-37-9_No-00.png
こんな感じになってくれるはずです。

また、detectMarker(Center)CoordinatesByCamera [GitHub]では、OpenCV側の関数を利用して、Markerの4隅の点から、中心(重心)の座標を求めて点を描き込んでいます。

カメラを用いたリアルタイムMarker姿勢推定

いよいよ一番やりたいでしょう、姿勢推定です。

さて、早速やっていきたいところでしょうが実は姿勢推定を行う前にやることがあります。
カメラキャリブレーションというものです。こちらで得られたMatがなければ次の関数は実行できませんので先に(ここにカメラキャリブレーションの解説記事)を参照していただきたいです。
(実はこの前の章との間に開発上は、2~3週間かかっちゃってます。遅れた原因はこれ)

改めまして、関数から入っていきましょう。
・Aruco.estimatePoseSingleMarkers(corners, 0.05f, cameraMatrix, distortionCoefficients,
rotationMatrix, translationVectors);
・Aruco.drawAxis(inputImage, cameraMatrix, distortionCoefficients,
rotationMatrix, translationVectors, 0.1f);

上の関数は、名前の通りMarkerの姿勢推定を行い、回転行列と平行移動ベクトルを返してきてくれます。
そして下の関数では、Axisつまり座標軸を描画してくれます。

以下ソースコード(詳細: detectMarkerPoseByCamera [GitHub])

    List<Mat> corners = new ArrayList<>();
    Mat markerIds = new Mat();
    // DetectorParameters parameters = DetectorParameters.create();
    Aruco.detectMarkers(inputImage, dictionary, corners, markerIds);

    Aruco.drawDetectedMarkers(inputImage, corners, markerIds);

    Mat rotationMatrix = new Mat(), translationVectors = new Mat(); // 受け取る
    Aruco.estimatePoseSingleMarkers(corners, 0.05f, cameraMatrix, distortionCoefficients,rotationMatrix, translationVectors);

    for (int i = 0; i < markerIds.size().height; i++) { // TODO
        Aruco.drawAxis(inputImage, cameraMatrix, distortionCoefficients, rotationMatrix, translationVectors, 0.1f);
    }

SC_test_2018-12-31_0-36-22_No-00.png
SC_test_2018-12-31_0-36-13_No-00.png
SC_test_2018-12-31_0-35-37_No-00.png
こんな感じになってくれるはずです。

おわりに

そこそこ時間がかかりましたが、とりあえず動くようになりました。
特にカメラキャリブレーションの理解が大変でしたね…。
これからもJava版OpenCVに精進して行きたい所存です。

(2018年最後に楽しめました)[記事自体は2019年最初に書いてますが]

また、この記事は初学者が書いておりますので、間違いや良くない点がございましたらコメントまたはツイッターにてご意見いただけたら幸いです。