JavaCV を使用して 三国志大戦4 解任済み武将カード の 武将 を判定する

kameyatakefumi.hatenablog.com

前回は難しい事をして失敗したので、今回は単純にテンプレート画像を縮小してマッチングしてみる。

以下、テンプレート画像のリサイズをしてマッチングしている。

public class TemplateMatching {

    private static final float SIKITI = 0.95f;

    public static void main(String[] args) {

        List<Mat> images = Arrays.asList(
                imread("{武将カードリスト画像}"),
                imread("{武将カードリスト画像}"),
                imread("{武将カードリスト画像}"));

        Mat template = imread("{テンプレート画像}", CV_LOAD_IMAGE_GRAYSCALE);

        // サイズ調整
        resize(template, template, new Size(40, 64), 0, 0, INTER_AREA);

        for (Mat image : images) {

            Mat grey = new Mat(image.size(), CV_8UC1);
            cvtColor(image, grey, COLOR_BGR2GRAY);

            Size size = new Size(grey.cols() - template.cols() + 1, grey.rows() - template.rows() + 1);
            Mat result = new Mat(size, CV_32FC1);
            matchTemplate(grey, template, result, TM_CCORR_NORMED);

            getPointsFromMatAboveThreshold(result, SIKITI).stream().forEach((point) -> {
                rectangle(image, new Rect(point.x(), point.y(), template.cols(), template.rows()), Scalar.RED, 2, 0, 0);
            });

            imshow("Result", image);
            waitKey(0);
        }

        destroyAllWindows();
    }

    public static List<Point> getPointsFromMatAboveThreshold(Mat m, float t) {
        List<Point> matches = new ArrayList<>();
        FloatIndexer indexer = m.createIndexer();
        for (int y = 0; y < m.rows(); y++) {
            for (int x = 0; x < m.cols(); x++) {
                if (indexer.get(y, x) > t) {
                    System.out.println("(" + x + "," + y + ") = " + indexer.get(y, x));
                    matches.add(new Point(x, y));
                }
            }
        }
        return matches;
    }

}

リサイズは 横40 縦64 としている。
この数値は 三国志大戦.NET から参照している。

以下、実行結果。
f:id:kameya_takefumi:20171108101250j:plain
f:id:kameya_takefumi:20171108101320j:plain
いい感じに判定されている!
画像を縮小させる事は大事ですね。

ただ比較対象の画像サイズによっては 横40 縦64 でもうまく判定されないものがあります。
機器によって画面サイズが違うのでズレが生じるのかな。

より精度を上げるには比較する画像から武将カードのサイズを取得して、それに合わせてテンプレート画像をリサイズする必要がありますね。

うーん、どんどん深い所へと行っていますね。
肝となる部分なので後回しにしても痛い目を見そうだ。
どうしようかな。

JavaCV を使用して 三国志大戦4 解任済み武将カード の 武将 を 特徴点 から判定する(失敗)

kameyatakefumi.hatenablog.com

前回、リストから1単位の武将データを抽出する事ができた。
今回、そこから武将を判定したい。

テキストである武将名は読み取りが難しいうえに、蜀の劉備、漢の劉備、レアリティが違う劉備、性能が一緒だけど絵柄が劉備など違うパターンがありぎすて正解を得るのに考慮する事が多すぎる。
絵柄は重複する事がないので、絵柄から武将を判定する。

テンプレートマッチングでやってみても、うまく判定されない。
どうやら画像サイズが違うと判定されないみたいね。

色々調べてみると特徴点でマッチングする手法を知りました。

以下、色んなサイトを見て作成。

public class PointMatching {

    public static void main(String[] args) {

        Mat mat1 = imread("{1単位の武将データの画像}");
        Mat mat2 = imread("{武将の画像}");

        // 特徴点抽出
        KeyPointVector keyPointVector1 = new KeyPointVector();
        KeyPointVector keyPointVector2 = new KeyPointVector();
        ORB orb = ORB.create();
        orb.detect(mat1, keyPointVector1);
        orb.detect(mat2, keyPointVector2);

        // 特徴記述
        Mat matDescriptor1 = new Mat();
        Mat matDescriptor2 = new Mat();
        orb.compute(mat1, keyPointVector1, matDescriptor1);
        orb.compute(mat2, keyPointVector2, matDescriptor2);

        // 特徴点マッチング
        DMatchVector vector = new DMatchVector();
        DescriptorMatcher matcher = DescriptorMatcher.create("BruteForce-Hamming");
        matcher.match(matDescriptor1, matDescriptor2, vector);

        // 結果作成
        Mat result = new Mat();
        drawMatches(mat1, keyPointVector1, mat2, keyPointVector2, vector, result);

        imshow("Resutl", result);
        waitKey(0);
        destroyAllWindows();
    }

}

以下、実行結果です。
f:id:kameya_takefumi:20171102172801j:plain
左の画像と右の画像の 特徴点 を線でつなげています。

うーん、ぜんぜんダメやん。
しきい値を設定して 特徴点 を選別しても大して変わりません。
大きい画像を用意しておけば空気を読んでマッチングしてくれるやろっと甘い考えでした。

特徴点マッチングの難しい所は特徴点がどこまで一致していたら同じと判定するかですね。
うーん、向いていない事をやっている感があります。

これなら大きめの画像を用意しておいて、比較する画像に合わせて縮小させてパターンマッチングするほうが楽で正確でしょうね。
比較しあう画像を同サイズにするって、パターンマッチングにおいては最適化しているといえますね。
一見、画像を縮小すると情報が抜け落ちている感覚ですが、不要な情報を削っているといえるのかな。

次はテンプレートとなる大きめの武将カードを用意しておいて、リストから抽出した1単位の画像サイズに合わせて縮小処理をしてパターンマッチングしてみるだな。

JavaCV を使用して 三国志大戦4 解任済み武将カード の1単位を判定する

kameyatakefumi.hatenablog.com

こちらの続きとして 解任済み武将カード のリストから1単位を判定したい。
リストから1単位を判定できれば、あとは抽出して個別処理を行うだけになる。

最初は枠を判定して、そこからゴリゴリと座標計算して~みたいな事になるのかなぁと考えていた。
OpenCV ができる事を見ていると 輪郭検出 というワードがあり、調べてみると要件に非常に合っていたので、こちらで行う事にした。

キッカケとなったのは以下のサイト。

AVFoundation+OpenCVで矩形検出(「名刺撮影用カメラ」みたいなやつ作ってみました) | Developers.IO

サイトでは名刺を判定している。
これを 武将カード1単位 に置き換えできると思った。
画像処理に関しては右も左もわからないですが、処理に対して1つ1つ解説があって非常に助かりました。
感謝しかありません。

以下のサイトも非常に参考になった。

機械学習のためのOpenCV入門 - Qiita

全体と各工程が非常にわかりやすく解説されている。
処理をする意図がわかると理解が進むので助かります。

サイトの処理を参考にしつつ以下を作成して実行。

public class FindContours {

    public static void main(String[] args) {

        // 画像を読込
        Mat mat = imread("{画像パス}");

        // グレースケール化
        Mat gray = new Mat(mat.size(), CV_8UC1);
        cvtColor(mat, gray, COLOR_BGR2GRAY);

        // しきい値処理
        threshold(gray, gray, 0, 255, CV_THRESH_OTSU);

        // 輪郭検出
        MatVector contours = new MatVector();
        Mat hierarchy = new Mat();
        findContours(gray, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_TC89_L1);

        List<Mat> mats = new ArrayList<>();
        int max_level = 0;
        for (int i = 0; i < contours.size(); i++) {

            // ある程度の面積が有るものだけに絞る
            double area = contourArea(contours.get(i), false);
            if (area > 15000) {

                //輪郭を直線近似する
                Mat approx = new Mat();
                approxPolyDP(contours.get(i), approx, 0.01 * arcLength(contours.get(i), true), true);

                // 矩形のみ取得
                if (approx.size().area() == 4) {

                    // 1単位を収集
                    Rect rect = boundingRect(approx);
                    mats.add(new Mat(mat, rect));

                    // 輪郭を塗る
                    drawContours(mat, contours, i, Scalar.RED, 2, CV_AA, hierarchy, max_level, new Point());
                }
            }
        }

        imshow("Result", mat);
        waitKey(0);
        destroyAllWindows();
    }

}

以下、実行結果。
f:id:kameya_takefumi:20171101134526p:plain
素晴らしい!
1単位を判定しライン赤色でカッコっています。

1単位は見えた、あとはそこを抽出するだけだ。
f:id:kameya_takefumi:20171101135059p:plain
できた。
素晴らしい!

ただ課題もある。

以下は見切れているパターン。
f:id:kameya_takefumi:20171101140057p:plain
これに関しては諦めるかも。
個別処理用に特殊な事をする事になりそう。
対応するコストが高い気がしている。

以下はヘッダが判定されている。
f:id:kameya_takefumi:20171101140307p:plain
輪郭検出なので処理結果としては正しいんだけど 武将カード ではないので除外する必要がある。
ろ過作業みたいな事をしないといけない。

不要な情報の除去って輪郭検出の前と後、どちらでやるんだろうか。
状況にもよるんだろうけど、ここも考えていかないといけないね。

Ver.1.1.0B バージョンアップ

久しぶりに三国志大戦のみの記事を書くなぁ。
象兵の追加と 南蛮王の大号令 がまさかの上方修正でテンションあがって象兵デッキで全国に行っている。

以下デッキの感想。

  • SR 孟獲 復活減少
  • SR 祝融 兵力上昇
  • R 孟優 復活減少
  • SR 鄒氏 士気上昇
  • R 賈詡 士気上昇

SR 孟獲

かなり好きなカード。
前作は回転率が凄かったですが、今回はバランス型って感じなのかな。
総合的な性能は上がっているんじゃないかな。
作家が 風間雷太先生 なので下手に武力下げられない圧力を感じる。

将器復活(6秒)と特技復活(5秒)で復活カウントが 11秒 マイナスされる。
復活までにかかる秒数 30 - 11 = 19秒

1カウント は 2.5秒 なので 8カウントぐらいで復活できる。
ピッタリはないから撤退したら 9カウント ぐらいで帰ってくるって覚えておこうかな。

通常だと撤退したら復活するまで 12カウント なので 3-4カウント ほど早く帰ってくる。
副将器も揃えたら 5カウント ぐらい差がつきそう。

SR 祝融

姉御って感じで良いですね。
スペックが非常に高い。

計略は基本的に使うつもりはない。
状況によるけど祝融の計略を使うぐらいなら士気2貯まるまで耐えて 南蛮王の大号令 をやったほうがリターンがでかいと思う。
相当苦しい時に守りで使う事を想定している。

デッキの生命線だと思っていて一番丁寧に使っている。

R 孟優

こっちは 10カウント ほどで帰ってくる。
将器復活(6秒)で 再起の法 が復活までの秒を 24秒 減少なので撤退して即 再起の法 を叩いてもすぐに帰ってくるの良いね。

はじき戦法は、はじくより攻城とるために使う事が多いなぁ。

SR 鄒氏

必須カード。
とりあえず入れとけ。

R 賈詡

スペック採用。
全国で一度も計略を使っていない。
なので減少値も把握していない。

群雄で知力9の伏兵は偉大。
開幕を凌ぎやすくなるので助かる。

賈詡を採用すると制圧ランクが B になってしまうのは仕方ないと思っている。

Twitter4J を使用して 三国志大戦4 に関係のある ハッシュタグ のツイートを取得する

kameyatakefumi.hatenablog.com

前回の続きではないが入力情報として使用する #三国志大戦登用 のツイートをプログラムで取得したい。
ゴールは #三国志大戦登用 のツイートから 解任済み武将カード の 画像URL を取得するまで。

twitter.com

JavaTwitter と言えば Twitter4J なので、こちらを使用する。

Twitter4J - A Java library for the Twitter API

プログラムから Twitter を操作するには Key と Token が必要になるみたい。
Twitter Application Management にアクセスして発行する。

Twitter Application Management

以下、公式のサンプルとJavaDocを眺めつつ参考になるサイトを見て作成。

public class HashTag {

    public static void main(String[] args) {

        Twitter twitter = new TwitterFactory().getInstance();

        try {

            Query query = new Query("#三国志大戦登用 -rt -bot");
            query.setSince("2017-10-23");

            QueryResult result;
            do {

                result = twitter.search(query);
                List<Status> tweets = result.getTweets();

                for (Status tweet : tweets) {
                    for (MediaEntity entity : tweet.getMediaEntities()) {
                        System.out.println(entity.getMediaURL());
                    }
                    System.out.println("----------------------------------------------");
                }

            } while ((query = result.nextQuery()) != null);
            System.exit(0);

        } catch (TwitterException te) {
            te.printStackTrace();
            System.out.println("Failed to search tweets: " + te.getMessage());
            System.exit(-1);
        }
    }

}

重要なのは検索ワードに -rt -bot を含める事。
最低でも -rt は必須。
これを付けないと取得結果にリツイートが含まれる事になり重複した値を何件も取得する事になる。

以下、取得結果です。

...
http://pbs.twimg.com/media/DMzoUU0U8AArVPc.jpg
----------------------------------------------
http://pbs.twimg.com/media/DIHiVpqUMAAi7wh.jpg
----------------------------------------------
http://pbs.twimg.com/media/DMzZzOsVAAALLG6.jpg
----------------------------------------------
----------------------------------------------
http://pbs.twimg.com/media/DMzL2TcVAAAgRt9.jpg
----------------------------------------------
...

ちゃんと画像のみが取得できてます。
Twitter4J まじ便利。