結果だけでなく過程も見てください

たい焼きさんの日々の奮闘を綴る日記です。

C# + Ironyで構文解析を行う (電卓を作ってみます)

皆様新年明けましておめでとうございます。
相変わらずの更新ペースですが、本年もよろしくお願い致します。
挨拶はこの辺にしてさっそく本題。

なぜ構文解析をするのか?

事の発端ですが、うちにはC/C++で書かれたソースコードが山ほどあり、構文解析してヘンな部分を静的解析でもできたら素敵だな~と考えたのが始まりですが、まぁ難易度的に出来る出来ないは置いといて、この字句解析/構文解析という分野、なかなかちゃんと理解できず、曖昧な知識のまま今に至っているので、ちょいとお勉強したいなとも思ったのです。

Ironyとは?

パーサージェネレーターです。
有名なところではyacc/lex、bison/flexがありますね。
これはyacc/lex独自の文法にBNFを食わせて、字句解析/構文解析を行うC言語のプログラムを出力するものです。

今回使うIronyは、BNFの定義をIronyのクラスインスタンスなどを使って、C#のコード上で指定できるものです。
すべてC#のコードで表現できるので、yacc/lexのような独自の文法が不要になります。

C++のboostにも、spiritというC++のコードでBNFを指定し、字句解析/構文解析を行うクラスインスタンスを作成するみたいなライブラリがありました。
さすがC++!クラス/関数テンプレートさえあればなんでもできるんや!魔境へようこそ!って感じですねぇ…。

目標

とりあえず月並みですが、電卓を作ってみたいと思います。概要は以下。

  • 四則演算+べき乗計算
  • 計算の優先順位のためのカッコ対応
  • マイナス値およびプラス値(-1とか+1とか)
  • 整数のみ
  • 複数の文を定義できる。文ごとに計算され、結果は合算値を表示。
  • //や/* */のコメント部分や、空白文字は無視

こんな式↓を計算して結果を返すものを作ります。

2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2);

準備

当方、Visual Studioのバージョンは2017 Communityです。

C#で適当なプロジェクトを作って(自分はフォームアプリケーションにしました)、
NuGetでIronyのライブラリを取り込んでおく必要があります。やり方は以下。

Visual Studioの[ツール]→[NuGet パッケージ マネージャー]→[ソリューションのNuGetパッケージの管理]を選択、
「Irony」で検索して、「Irony」と「Irony.Interpreter」をインストールします。

BNFのイメージ

詳しくないのであくまでイメージとして記載します。
構文木のトップをProgramとして、複数の文(Statement)を書けるようにします。
文とは計算式の最後にセミコロン(";")をつけたものであり、セミコロン区切りで複数の計算式を定義できるようにします。
式(Expr)は四則演算やカッコなどを含む計算式です。演算子の優先順位を意識する必要があるため、Exprで+, -を解決して、その下にTermを作って*, /を解決して、その下にFactor...とやるのが一般的なようですが、なんか名前がわかりづらいので、ここではExprAddSubやExprMulDivなどの名前にしています。Numberは整数です。

Program ::= Statement*
Statement ::= ExprAddSub ";"
ExprAddSub ::= ExprMulDiv
             |  ExprAddSub "+" ExprAddSub
             |  ExprAddSub "-" ExprAddSub
ExprMulDiv ::= Expr
             |  ExprMulDiv "*" ExprMulDiv
             |  ExprMulDiv "/" ExprMulDiv
             |  ExprMulDiv "**" ExprMulDiv
Expr ::= "(" ExprAddSub ")"
       |  UnOp "(" ExprAddSub ")"
       | UnOp Number
UnOp ::= "+"  | "-"

文法定義部分のC#コード

まずは文法を表現するクラスを定義します。
基本的には上記BNFをまんまコードに落とし込んでいくことになります。

まずはクラスの始まりと、コメントや空白文字、改行などを無視するコード。

using Irony.Interpreter;
using Irony.Parsing;

[Language("MyNumbersGrammar")]
class NumbersGrammar : InterpretedLanguageGrammar
{
    public NumbersGrammar() : base(true)
    {
        // 一行コメント(//)や、複数行コメント(/* ... */)、その他文法上無視するものの指定
        var singleLineComment = new CommentTerminal("SingleLineComment", "//", "\r", "\n", "\u2085", "\u2028", "\u2029");
        var delimitedComment = new CommentTerminal("DelimitedComment", "/*", "*/");
        NonGrammarTerminals.Add(singleLineComment);
        NonGrammarTerminals.Add(delimitedComment);

続いて、終端文字です。
今回は整数と、それ以外のこまごまとして演算子や記号を定義します。

        // 1. Terminals
        NumberLiteral Number = new NumberLiteral("number");
        KeyTerm AddOperator = ToTerm("+");
        KeyTerm SubOperator = ToTerm("-");
        KeyTerm MulOperator = ToTerm("*");
        KeyTerm DivOperator = ToTerm("/");
        KeyTerm PowOperator = ToTerm("**");
        KeyTerm LeftParen = ToTerm("(");
        KeyTerm RightParen = ToTerm(")");
        KeyTerm EndOfSentence = ToTerm(";");

肝の部分の非終端記号です。
上記BNFの非終端記号を1つずつ定義していきます。
NonTerminalの第一引数は構文木を解析したときに、要素を見分けるための文字列です。適当で良いです。
第二引数には、構文木(Abstract Syntax Tree, 以下AST)を解析したあと、それをどう評価するか、ノードごとに評価するコードを持つクラスを指定します。
これらのクラスは後述します。

        // 2. Non-Terminals
        var Program = new NonTerminal("Program", typeof(NumbersGrammarProgramNode));

        var Statement = new NonTerminal("Statement", typeof(NumbersGrammarStatementNode));

        var ExprAddSub = new NonTerminal("ExprAddSub", typeof(NumbersGrammarExprNode));
        var ExprMulDiv = new NonTerminal("ExprMulDiv", typeof(NumbersGrammarExprNode));
        var Expr = new NonTerminal("Expr", typeof(NumbersGrammarExprNode));
        var ParenExpr = new NonTerminal("ParenExpr", typeof(NumbersGrammarParenExprNode));

        var UnExpr = new NonTerminal("UnExpr", typeof(NumbersGrammarUnExprNode));
        var UnOp = new NonTerminal("UnOp", typeof(NumbersGrammarUnOpNode) );

BNFのルールを書いていきます。

        // 3. BNF
        Program.Rule = MakeStarRule(Program, Statement);

        Statement.Rule = ExprAddSub + EndOfSentence;

        ExprAddSub.Rule = ExprMulDiv
                | ExprAddSub + AddOperator + ExprAddSub
                | ExprAddSub + SubOperator + ExprAddSub
                ;

        ExprMulDiv.Rule = Expr
                | ExprMulDiv + MulOperator + ExprMulDiv
                | ExprMulDiv + DivOperator + ExprMulDiv
                | ExprMulDiv + PowOperator + ExprMulDiv
                ;

        Expr.Rule = ParenExpr
               | UnExpr   
               | Number;  

        ParenExpr.Rule = LeftParen + ExprAddSub + RightParen;

        UnExpr.Rule = UnOp + ParenExpr | UnOp + Number;

        UnOp.Rule = ToTerm("+") | "-";

最後に、このルールのトップをthis.Rootに指定し、
演算子の優先順位をRegisterOperators関数で指定します(※)
あとはASTを作成するフラグもONにします。

(※)補足。RegisterOperatorsでの演算子の優先順位がうまく動かないので、BNFとしては"*", "/"が"+", "-"よりも先に評価されるような文法にしています。

        this.Root = Program;

        RegisterOperators(1, "+", "-");
        RegisterOperators(2, "*", "**", "/");

        LanguageFlags = LanguageFlags.CreateAst;
    }
}

これで入力された文字列を読み取り、構文木を作成することができます。

ASTを評価する部分のC#のコード

非終端記号毎に、AstNodeのサブクラスを作成します。
重要な点として、これらのクラスは必ずアクセス修飾子をpublicにする必要があります

今回はInit関数、DoEvaluate関数をオーバーライドしています。
Init関数でノード情報を取得し、DoEvaluate関数で評価した結果(今回は主に整数)を取得します。

トップレベルの非終端記号Programに対応するクラスです。
Programは複数のStatementを所持できるので、AstNodeのリストで管理しています。

public class NumbersGrammarProgramNode : AstNode
{
    // プログラム全体は複数の数字を結果として持つ

    public List<AstNode> numList = new List<AstNode>();

    public override void Init(AstContext context, ParseTreeNode treeNode)
    {
        base.Init(context, treeNode);

        ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();
        foreach ( var node in nodes )
        {
            this.numList.Add(AddChild("numList", node));
        }
    }

    protected override object DoEvaluate(ScriptThread thread)
    {
        thread.CurrentNode = this;

        // すべての文を評価した結果得られた整数値の合計を返す
        int result = 0;
        foreach ( var num in this.numList )
        {
            result += (int)num.Evaluate(thread);
        }

        thread.CurrentNode = Parent;

        return result;
    }
}

次にStatementですが、これは式の最後にセミコロンをつけたものなので、
式のAstNodeだけを所持するようにします。

public class NumbersGrammarStatementNode : AstNode
{
    public AstNode exprNode = null;

    public override void Init(AstContext context, ParseTreeNode treeNode)
    {
        base.Init(context, treeNode);

        ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();
        this.exprNode = AddChild("exprNode", nodes[0]);  // 数字のみ取り出す。nodes[1]の";"は無視。
    }

    protected override object DoEvaluate(ScriptThread thread)
    {
        thread.CurrentNode = this;

        // 単一式を評価して、整数を取得する
        int number = (int)exprNode.Evaluate(thread);

        thread.CurrentNode = Parent;

        return number;
    }
}

式Exprを評価します。
式は構文によって、値が1つだったり、3つだったりするので、
取得した要素分だけ配列サイズを確保して、ノード情報を所持するようにします。

public class NumbersGrammarExprNode : AstNode
{
    public AstNode[] numberNodes = null;

    public override void Init(AstContext context, ParseTreeNode treeNode)
    {
        base.Init(context, treeNode);

        ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();

        this.numberNodes = new AstNode[nodes.Count];

        for (int i = 0; i < nodes.Count; ++i)
        {
            this.numberNodes[i] = AddChild("numberNode", nodes[i]);
        }
    }

    protected override object DoEvaluate(ScriptThread thread)
    {
        thread.CurrentNode = this;

        int number = 0;
        if (this.numberNodes.Length == 1)
        {
            // 要素1つの場合
            number = (int)this.numberNodes[0].Evaluate(thread);
        }
        else
        {
            // 要素3つの場合(と仮定)

            int nLeft = (int)this.numberNodes[0].Evaluate(thread);
            int nRight = (int)this.numberNodes[2].Evaluate(thread);

            // 演算子はこれ以上評価できないので、取得した文字列を直接参照する。
            string strBinOp = this.numberNodes[1].Term.Name;
            if (strBinOp.Equals("+"))
            {
                number = nLeft + nRight;
            }
            else if (strBinOp.Equals("-"))
            {
                number = nLeft - nRight;
            }
            else if (strBinOp.Equals("*"))
            {
                number = nLeft * nRight;
            }
            else if (strBinOp.Equals("/"))
            {
                number = nLeft / nRight;
            }
            else
            {
                // **(べき乗)と仮定する。
                number = (int)Math.Pow(nLeft, nRight);
            }

        }

        thread.CurrentNode = Parent;

        return number;
    }
}

続いてカッコ付の式を評価するクラス。

public class NumbersGrammarParenExprNode : AstNode
{
    public AstNode middleExprNode = null;

    public override void Init(AstContext context, ParseTreeNode treeNode)
    {
        base.Init(context, treeNode);

        ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();

        this.middleExprNode = AddChild("middleExprNode", nodes[1]);   // 両端の(と)は無視して、間だけを評価する
    }

    protected override object DoEvaluate(ScriptThread thread)
    {
        thread.CurrentNode = this;

        int result = (int)this.middleExprNode.Evaluate(thread);

        thread.CurrentNode = Parent;

        return result;
    }
}

次に、負値(とオマケで明示的な正の値)を式として評価するクラス。

public class NumbersGrammarUnExprNode : AstNode
{
    public AstNode unOpNode = null;
    public AstNode termNode = null;

    public override void Init(AstContext context, ParseTreeNode treeNode)
    {
        base.Init(context, treeNode);

        ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();

        this.unOpNode = AddChild("unOpNode", nodes[0]);
        this.termNode = AddChild("termNode", nodes[1]);
    }

    protected override object DoEvaluate(ScriptThread thread)
    {
        thread.CurrentNode = this;

        // "-"なら-1を掛けて負の値にする。それ以外は+とする。
        int result = (int)this.termNode.Evaluate(thread) * (((string)this.unOpNode.Evaluate(thread)).Equals("-") ? -1 : +1);

        thread.CurrentNode = Parent;

        return result;
    }
}

負値および正の値の前につける"-"または"+"を評価するクラス。

public class NumbersGrammarUnOpNode : AstNode
{
    public AstNode unOpNode = null;

    public override void Init(AstContext context, ParseTreeNode treeNode)
    {
        base.Init(context, treeNode);

        ParseTreeNodeList nodes = treeNode.GetMappedChildNodes();

        this.unOpNode = AddChild("unOpNode", nodes[0]);
    }

    protected override object DoEvaluate(ScriptThread thread)
    {
        thread.CurrentNode = this;

        string result = (string)this.unOpNode.Term.ToString();

        thread.CurrentNode = Parent;

        return result;
    }
}

電卓を使用してみる!

最後に、上記コードで作成した電卓に値を渡し、結果を取得するコードをご紹介します。
Main関数に書いたり、フォームアプリケーションでボタンが押されたときのコールバック関数に書くと良いでしょう。

また自分は入力データをtbExprというテキストボックスから取得し、
tbResultというテキストボックスに結果を出力しています。

var grammar  = new NumbersGrammar();
var language = new LanguageData(grammar);

try
{
    var app = new ScriptApp(language);
    int result = (int)app.Evaluate(this.tbExpr.Text);

    this.tbResult.Text = result.ToString();
}
catch (ScriptException ex)
{
    Console.Error.WriteLine("{0} {1}", ex.Location, ex.Message);
}

電卓に渡す値の例

機能を一通り使った式
2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2);

結果

50
複数の文を渡す

セミコロンで区切れば、複数の式を渡すこともできます。

2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2);
((2 + -2 )/3 + 5) * 2 + 1;

結果

61
コメント

以下のように、コメントは無視されます。

2 + 3 *(((1 + -2)*(-1)) + 3 ) * ( 2 + 4/2);
((2 + -2 )/3 + 5) * 2 + 1;
// 100 * 5
/*
300 * 5;
400 * 5;
*/
+5;

結果

66

途中から解説が面倒になってコードを張り付けるだけみたいな感じになってしまいました。
解説も非常に適当で、間違ってそうなところがちらほらと・・・。
後日修正するかもしれません。修正するんじゃないかな?さぼったらごめんなさい。

AutoIt+OpenCVでデスクトップから任意の画像を曖昧検索(テンプレートマッチング)する

AutoItで画像の曖昧検索がしたい

UWSCにはchkimgという画像検索関数が標準で用意されていました。また有志の方がchkimgxという画像の曖昧検索を行う外部ライブラリを作成されていたようですが、アップローダーなどでのみ配布されていたようで現在では入手が難しくなっています。

一方、AutoItには画像検索をする標準関数が存在せず、searchImageという外部ライブラリを使う必要があるようです。
しかしこのsearchImage、2008年頃までしかメンテされていないようで、自分の環境でもいまいちちゃんと動いてくれず、単にdllの配置場所を間違えてたかもしれない件は置いといて、まぁ自分で画像マッチング出来た方が曖昧さ加減を調整出来たり、判定方法を自由にカスタマイズ出来たり、今後のことを考えると自作した方がいいかなぁなんて思いまして、OpenCVを触ってみることにしました。

こちらの環境です。

OS Windows 10
コンパイラ Visual Studio Community 2017
CMake 3.12.1(64-bit版)
OpenCV, OpenCV contrib 3.4.3
AutoIt v3.3.14.5

Visual Studio Community 2017のインストール

なにはともあれ、コンパイラをインストールします。

ダウンロード | IDE、Code、Team Foundation Server | Visual Studio

CMakeのインストール

OpenCVのビルドには、CMakeも必要です。インストールします。
ご自分の環境合わせてインストールしてください。自分の場合は「cmake-3.12.2-win64-x64.msi」でした。
https://cmake.org/download/

CMakeインストール中に「Add CMake to the system PATH for all users」を選択して、環境変数PathにCMakeのパスを追加してください。

↑を忘れてしまった場合は、手動で環境変数Pathに以下のCMakeのパスを追加してください。

C:\Program Files\CMake\bin

OpenCV, OpenCV contribのビルド

まずは以下からソースコードをダウンロードしましょう。

OpenCV

Releases · opencv/opencv · GitHub

opencv-3.4.3-vc14_vc15.exe」などのリンクをクリックしてダウンロードします。

OpenCV contrib

Releases · opencv/opencv_contrib · GitHub

「zip」と書かれたリンクをクリックしてダウンロードします。

上記ページのopencvのファイル名(例:opencv-3.4.3-vc14_vc15.exe)にあるvc14とかvc15とかが、
Visual Studioのバージョンに対応しているので、今後バージョンが上がった場合は注意して確認してみてください。

vc14 Visual Studio 2015
vc15 Visual Studio 2017

OpenCVのビルド

ここでは例として、「C:\temp」に上記ファイルをダウンロードします。

opencv-3.4.3-vc14_vc15.exe」をダブルクリックすると「opencv」というフォルダ名で展開されます。
opencv_contrib-3.4.3.zip」をダブルクリックすると「opencv_contrib-3.4.3」というフォルダ名で展開されます。

[スタート]メニューからcmake-guiを探して起動します。
[Where is the source code]に「C:/temp/opencv/sources」を指定します。
[Where to build the binaries]に「C:/temp/opencv/build」を指定します。
手動でパスを入力する場合、パス区切り文字は「/」にしてください
入力したら[Configure]ボタンを押下します。
f:id:taiyakisun:20180912000136p:plain

今回は、AutoItをデフォルト32-bitで動作させているので、OpenCVも32-bitでビルドします
そのため今回は「Win64が付いていない」プロジェクトを選択します。
f:id:taiyakisun:20180912000140p:plain

再びCMake-guiの画面に戻ります。
32-bit版の場合、cuda関連のビルドが通らないという噂がありますので、「WITH_CUDA」のチェックを外します(最近のは最初から外れてるかも)
また「OPENCV_EXTRA_MODULES_PATH」に「C:/temp/opencv_contrib-3.4.3/modules」を指定します。
何度も言いますが、パス区切り文字は「/」にしてください
f:id:taiyakisun:20180911221717p:plain
f:id:taiyakisun:20180911221720p:plain

コンソールに「Configuring done」が出力されたら成功です。
続いて「Generate」ボタンを押下します。
f:id:taiyakisun:20180911222335p:plain

C:\temp\opencv\build直下にOpenCV.slnが出来ているので、Visual Studioで開きます。
INSTALLプロジェクトをReleaseビルドしてください。

出力コンソールにこんな感じの表示が出れば成功です。

========== すべてリビルド: 134 正常終了、0 失敗、0 スキップ ==========

AutoItで使用するテンプレートマッチングを行うDLLの作成

Visual Studioで新規プロジェクトを作成します。
DLLのプロジェクトを作るか、プロジェクトの[構成プロパティ]→[全般]→[構成の種類]を「ダイナミック ライブラリ(.dll)」などにしてください。

同じプロジェクトのプロパティページの[C/C++]→[全般]→[追加のインクルードディレクトリ]に以下を入れます。

C:\temp\opencv\build\install\include

[リンカー]→[全般]→[追加のライブラリディレクトリ]に以下を入れます。

C:\temp\opencv\build\install\x86\vc15\lib
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

#include <map>
using namespace std;

#include <Windows.h>

/**
* @brief ウィンドウハンドルhwndのイメージcv::Matを作成する。
* @param [in] hwnd	     ウィンドウハンドル。デスクトップを対象にしたい場合、GetDesktopWindow()の戻り値を渡すべし。
* @param [in] nPosLeft       検索対象となる左上のx座標
* @param [in] nPosTop        検索対象となる左上のy座標
* @param [in] nDestWidth     検索対象となる範囲のnPosLeftからの幅
* @param [in] nDestHeight    検索対象となる範囲のnPosTopからの高さ
*/
cv::Mat hwnd2mat(HWND hwnd, int nPosLeft, int nPosTop, int nDestWidth, int nDestHeight)
{
    HDC hwindowDC = GetDC(hwnd);
    HDC hwindowCompatibleDC = CreateCompatibleDC(hwindowDC);

    HBITMAP           hbwindow = NULL;
    cv::Mat           src;
    BITMAPINFOHEADER  bi;

    SetStretchBltMode(hwindowCompatibleDC, COLORONCOLOR);

    RECT windowsize;    // get the height and width of the screen
    GetClientRect(hwnd, &windowsize);

    // ここはデスクトップの幅と高さ
    int srcheight = windowsize.bottom;
    int srcwidth  = windowsize.right;

    src.create(nDestHeight, nDestWidth, CV_8UC3);

    // create a bitmap
    hbwindow = CreateCompatibleBitmap(hwindowDC, nDestWidth, nDestHeight);
    if (hbwindow == NULL)
    {
        src.data = NULL;
        return src;
    }

    bi.biSize = sizeof(BITMAPINFOHEADER);    // http://msdn.microsoft.com/en-us/library/windows/window/dd183402%28v=vs.85%29.aspx
    bi.biWidth         = nDestWidth;
    bi.biHeight        = -nDestHeight;
    bi.biPlanes        = 1;
    bi.biBitCount      = 24;
    bi.biCompression   = BI_RGB;
    bi.biSizeImage     = 0;
    bi.biXPelsPerMeter = 0;
    bi.biYPelsPerMeter = 0;
    bi.biClrUsed       = 0;
    bi.biClrImportant  = 0;

    // use the previously created device context with the bitmap
    SelectObject(hwindowCompatibleDC, hbwindow);

    // copy from the window device context to the bitmap device context
    StretchBlt(hwindowCompatibleDC, 0, 0, nDestWidth, nDestHeight, hwindowDC, nPosLeft, nPosTop, nDestWidth, nDestHeight, SRCCOPY);	// change SRCCOPY to NOTSRCCOPY for wacky colors !
    GetDIBits(hwindowCompatibleDC, hbwindow, 0, nDestHeight, src.data, (BITMAPINFO *)&bi, DIB_RGB_COLORS);		// copy from hwindowCompatibleDC to hbwindow

     // avoid memory leak
    if (hbwindow != NULL)
    {
        DeleteObject(hbwindow);
        hbwindow = NULL;
    }

    if (hwindowCompatibleDC != NULL)
    {
        DeleteDC(hwindowCompatibleDC);
        hwindowCompatibleDC = NULL;
    }

    if (hwindowDC != NULL)
    {
        ReleaseDC(hwnd, hwindowDC);
        hwindowDC = NULL;
    }

    return src;
}

extern "C"
{
    /**
     * @brief 教師画像が指定されたデスクトップ範囲に存在するかあいまい検索(テンプレートマッチング)する
     * @param [in]  pszFilePath    教師(テンプレート)画像のフルパス
     * @param [in]  nPosLeft       検索対象となるデスクトップの左上のx座標
     * @param [in]  nPosTop        検索対象となるデスクトップの左上のy座標
     * @param [in]  nDestWidth     検索対象となる範囲のnPosLeftからの幅
     *                             4の倍数でないと必ずマッチングしないため、4の倍数に増やす方向で調整をする。
     * @param [in]  nDestHeight    検索対象となる範囲のnPosTopからの高さ
     * @param [out] pDetectedPosX  検知した場合のX座標。検知してない場合なにも入らない。
     * @param [out] pDetectedPosY  検知した場合のY座標。検知してない場合なにも入らない。
     * @retval 0  正常終了(見つかった)
     * @retval 1  正常終了(見つからなかった)
     * @retval 10 教師画像ロード失敗
     * @retval 11 デスクトップ情報確保失敗
     * @retval 12 テンプレートマッチングでエラー
     */
    DllExport int searchImg(const char*  pszFilePath,
                            int          nPosLeft, 
                            int          nPosTop, 
                            int          nDestWidth, 
                            int          nDestHeight, 
                            int*         pDetectedPosX, 
                            int*         pDetectedPosY)
    {
        int nActualWidth = nDestWidth + (4 - (nDestWidth % 4));   // 4の倍数に増やす方向で合わせる

        cv::Mat template_image = cv::imread(pszFilePath);
        if (template_image.data == NULL)
        {
            return 10;
        }

        cv::Mat srcDesktop = hwnd2mat(GetDesktopWindow(), nPosLeft, nPosTop, nActualWidth, nDestHeight);
        if (srcDesktop.data == NULL)
        {
            return 11;
        }

        cv::Mat result;
        try
        {
            cv::matchTemplate(srcDesktop, template_image, result, cv::TM_CCORR_NORMED);
        }
        catch (cv::Exception ex)
        {
            return 12;
        }

        // Multiple results

        // 最もそれっぽい画像選ぶためのMap。同じ値の場合最初に検知したものを優先するため、insert自体しないこととする。
        // 比較をgreator<float>にすることにより降順に並ぶ。
        map<float, cv::Point, greater<float> > dectetedPointMap;

        const float threshold = 0.981f;     // TODO:ここも指定できるようにしたい

        for (int y = 0; y < result.rows; ++y)
        {
            for (int x = 0; x < result.cols; ++x)
            {
                if (result.at<float>(y, x) > threshold)
                {
                    if (dectetedPointMap.find(result.at<float>(y, x)) != dectetedPointMap.end())
                    {
                        // すでに同じ値がある場合、最初に見つかったものを採用するため、mapには追加しない
                        continue;
                    }

                    dectetedPointMap.insert(pair<float, cv::Point>(result.at<float>(y, x), cv::Point(x,y)));
                }
            }
        }

        
        if (dectetedPointMap.empty())
        {
            // 見つからなかった
            return 1;
        }
        else
        {
            // 最も似ている点(複数ある場合は最初に見つかった点)を採用する
            *pDetectedPosX = (dectetedPointMap.cbegin()->second.x + template_image.cols / 2);
            *pDetectedPosY = (dectetedPointMap.cbegin()->second.y + template_image.rows / 2);

            return 0;
        }
    }
}

とりあえず複数見つかった場合は以下のルールに従って、単一の結果を返すようにします。返すデータはマッチングした画像の中心座標です。

  • もっとも類似度の高いもの
  • 最初に見つかったもの

ビルドしてdllを作成します。
includeやlibの設定が間違っていなければビルドが通るはず(通らなかったらごめんなさい)。

AutoItからDLLを呼び出す

AutoItのインストール手順などは省略します。
適当にAutoItのファイルを作成し、以下のDLLをAutoItのファイルと同じ位置に配置します。

上記で作成した自作のDLL
opencv_ccalib342.dll
opencv_core342.dll
opencv_highgui342.dll
opencv_imgcodecs342.dll
opencv_imgproc342.dll

AutoItのコードは以下です。
まずはDLL関数のラッパー関数。

; @param [in] $hDll	   DllOpen()済のハンドル。ここファイル名を渡すこともできるが、そうすると毎回オープン/クローズ
;			           するせい(?)なのか、連続して呼び出すとアプリケーションエラーが発生するので、必ずハンドルを渡すこと。
; @param [in] $x       デスクトップ上のx座標
; @param [in] $y       デスクトップ上のy座標
; @param [in] $width   $xからの幅
; @param [in] $height  $yからの幅
; @retval -1 DllCallでエラー発生
; @retval 0  イメージが正常に判定し、見つかった。イメージの中心座標が$detectedXと$detectedYに格納される。
; @retval 1  イメージを正常に判定し、見つからなかった
; @retval 10 イメージのファイルが見つからなかった
; @retval 11 デスクトップ画面の取得で失敗
; @retval 12 マッチング処理でエラー発生
Func searchImageCore($hDll, $imgFileName, $x, $y, $width, $height, ByRef $detectedX, ByRef $detectedY)

	Dim $ret = DllCall($hDll, _
			"int:cdecl", _
			"searchImg", _
			"str", $imgFileName, _
			"int", $x, _
			"int", $y, _
			"int", $width, _
			"int", $height, _
			"int*", $detectedX, _
			"int*", $detectedY)
	If (@error <> 0) Then
		ConsoleWrite( "DllCall error!." & @LF )
		MsgBox(0, "error", "DllCall returned " & @error & "  imgFile=" & $imgFileName & @CRLF)
		Return -1
	EndIf

	; 0    -> 見つかった
	; 1    -> 見つからなかった
	; その他 -> エラー発生

	; 見つかった場合だけ、値を格納して、戻り値はコール元にそのまま返す
	If ($ret[0] = 0) Then

		$detectedX = $x
		$detectedY = $y

	EndIf

	Return $ret[0]

EndFunc   ;==>searchImageCore

次に、ラッパー関数を呼び出すAutoItコード。

Global $g_hDll = DllOpen( "<自作のDLLファイル名>.dll" )

Local detectedX = 0
Local detectedY = 0

Local $ret = searchImageCore($g_hDll, _
	                "abc.bmp", _
			0, _
			0, _
			1920, _
			1080, _
			$detectedX, _
			$detectedY)
If ($ret = 0) Then
	; 見つかった
	ConsoleWrite("searchImage detected!  dx=" & $detectedX & "  dy=" & $detectedY & @CRLF)
Else
	; 普通に見つからなかったか、なんらかのエラー発生。見つからなかったとして帰る。
	If ($ret <> 1) Then
		; 普通に見つからない場合以外はログを出しておく
		ConsoleWriteError("[Error] searchImage returned " & $ret & "  imgFile=" & $imgFile & @CRLF)
	EndIf
EndIf

思ったより長くなってしまった。あぁ疲れた…。

AutoItでDLLを呼び出す方法

つい先日からUWSCスマホゲーWindows上の作業を自動化していたのですが、
UWSCは最近公式サイトがリンク切れになったり、32-bitのバイナリしかなかったり、VMのゲストOSでの動作がうまくいかなかったりと、
将来的にいろいろ不安なので、AutoItに乗り換えられないか色々試してます。


AutoItについてはWikipediaでもみてください。
https://ja.wikipedia.org/wiki/AutoIt


こちらの環境です。

OS Windows 10
C++コンパイラ Visual Studio 2017 Community
AutoIt v3.3.14.5

AutoItは世界ではわりとメジャーだそうで、困ったことがあっても調べればすぐ解決方法が出てくるのも魅力ですね~。
今回は自作のDLL呼び出しでちょっと詰まったので、解決方法をご紹介します。

C++(DLL)のソースコード

サンプルで簡単な足し算の関数を定義します。

結果を戻り値で受け取るバージョンと、引数経由で受け取るバージョンの2つ。
これをVisualStudio等でビルド・dllを作成し、AutoItのソースコードと同じパスに配置しておいてください。

#define DllExport __declspec( dllexport )

extern "C"
{
    DllExport int Add(int a, int b)
    {
        return (a + b);
    }

    DllExport void AddRef(int a, int b, int* pResult)
    {
        *pResult = (a + b);
    }
}

AutoItのソースコード

Add関数の呼び方
Dim $result = DllCall("NinaImageSearch.dll", "int:cdecl", "Add", "int", 3, "int", 5)
If (@error <> 0) Then
	MsgBox(0, "error", "DllCall returned " & @error)
	Exit @error
Else
	MsgBox(0, "Result", "$result=" & $result[0])
EndIf
ポイント
  • 第一引数にはDLLのファイル名(またはDllOpenした結果(ハンドル))を渡す
  • 第二引数には関数の戻り値の型(※)を指定する
  • 呼び出す関数の呼び出し規約がcdeclの場合は「型:cdecl」と指定する
  • 第三引数に関数名を指定する
  • それ以降は型名(※)と値のペアを指定する
  • エラーが発生した場合はマクロ「@error」に0以外の数値が入る
  • 戻り値がintだろうと、結果は配列で返るので値を参照するときは$result[0]として参照する

ポイント多いですね!
この呼び出しを成功させるだけで、30分くらい格闘した覚えがあります…。

(※)型一覧はこのあたりをみてください。
https://open-shelf.appspot.com/AutoIt3.3.6.1j/html/functions/DllCall.htm

AddRef関数の呼び方

変数の参照(リファレンス)を渡して、そこに結果を格納する方法ですが、こいつがわかりづらい。

Dim $sum
Dim $ret = DllCall("NinaImageSearch.dll", "none:cdecl", "AddRef", "int", 3, "int", 20, "int*", $sum)
If (@error <> 0) Then
	MsgBox(0, "error", "DllCall returned " & @error)
	Exit @error
Else
	$sum = $ret[3]
	MsgBox(0, "Result", "$sum=" & $sum)
EndIf
ポイント
  • 戻り値voidの場合は「none」という識別子を使う。(第二引数)
  • 変数の参照(リファレンス)を渡すときは型の末尾に「*」を指定する(第八引数)
  • DllCallに成功しても、なんと$sumには値が入らない!
  • 結果は$retに格納されており、$retは以下のような配列構造になっている。
  • $ret[0] ←戻り値(noneの場合は空文字)
  • $ret[1] ←3 AddRef関数の第一引数に渡す値
  • $ret[2] ←20 AddRef関数の第二引数に渡す値
  • $ret[3] ←23 これがAddRef関数の戻り値!
  • つまり必要に応じて結果($ret[3])を変数に格納して使う。ってなんじゃこのわかりづらいのは~!

この呼び出しを成功させるだけで、1時間くらい格闘した覚えがあります…。

その他疑問

呼び出し規約をstdcallとかにすると、DllCallが3(関数が見つからない)が返ってくる。なぜだ。

Windowsで関数がどのlibファイルに含まれているかを確認する方法

Windowsで他人のライブラリをビルドして使用する場合など、
サンプルソースを動かしてみたら関数が未解決でリンクエラー…どのlibファイルをリンクすりゃいいんだ?
………なんて経験があると思います。今回はこの解消法をご紹介します。

ここではVisual Studioを使っていることを前提とします。

Visual Studioでは、使用するlibをプロジェクトのプロパティ→[リンカー]→[入力]→[追加の依存ファイル]に指定する必要がありますが、サンプルソースの関数がどのlibファイルに含まれているのかわからん!!といった場合は以下の方法を使います。

 

Visual Studioの開発者用コマンドプロンプトを開く

スタートメニューから開いてください。
例)Visual Studio 2017 (Community)の場合では「開発者コマンド プロンプト for VS 2017」という名前でした。

libファイルの内容をダンプする

libファイル群があるフォルダまで移動して、以下のコマンドを実行してダンプします。

link /dump /exports *.lib>liblist.txt

liblist.txtの内容を確認

あとは普通にテキストエディタでliblist.txtをオープンし、使いたい関数名で検索するだけ。liblist.txtの内容はこんな内容になっているはずです。

Dump of file maincore300.lib
File Type: LIBRARY
Exports
ordinal name
          ??abcfunc@@@QAE@ABV01@@Z (public: __thiscall abcfunc())
              :
          ??targetfunc@....

探したい関数がtargetfuncだったとすると、それが含まれるlibファイルはmaincore300.libということになります。

Summer Pockets(サマーポケッツ) 島モンファイト エサと場所で入手できる島モン組み合わせ一覧

発売日に予約していたものの時間が取れず、積んでおりました。昨日クリアしました…。

いや、大変よかったですね!各所でボロボロ泣きました。
特に後半は泣きすぎてマウスがクリックできなかったほど。

今回は個別ルート含め全体的にレベルが高い!
だーまえは原案だけということでちょっと心配でしたが、杞憂に終わりました。よかったよかった。

ただちょっと短かったかなぁ。
後半もうちょっとガッツリ作ってくれれば大満足でした。とりあえずファンディスク期待。

って、ちがう!!

この記事は島モンファイトの攻略記事です!
感想を書いてる場合じゃない!

島モンファイトはゲーム内のミニゲームで島モンを集めて、島の人たちとバトルをするもので、
まぁなんていうかポ○モンみたいなものです。いちおう専用のエンディングがあったりします。

ここでは島モンファイトについて、筆者が確認した範囲で情報をまとめたいと思います。
島モンは、同じ場所・同じエサの組み合わせでも複数候補の中からランダムに入手されるので、これで全部ということはありません。
そもそもまだエサもコンプリートしてないのです…。

あと色々間違ってたらごめんネ。見やすさも後日考えます。

エサ一覧と入手方法

エサ名 エサの入手方法
砂糖水(★1) 初回島モン開始時にうみちゃんがくれる
イカの皮(★1) 初回島モン開始時にうみちゃんがくれる
ミミズ(★1) 初回島モン開始時にうみちゃんがくれる
アイスの棒(★1) 初回島モン開始時にうみちゃんがくれる
昆虫ゼリー(★1) 不明(いつの間にかもってました…)
かき氷のシロップ(★2) 秘密基地に通い続ける。7/31にもらえる。
ナスビ(★2) 島モンファイトで主婦を全員撃破
ユムシ(★2) 島モンファイトで漁師、おっさんを全員撃破
鰹節(★2) 島モンファイトで亀、犬、猫を撃破
島産蜂蜜 (未入手)
ちょっとHな本 (未入手)
失敗したチャーハン(★3) 料理ができなくなっている8位のうみちゃんに勝利する。
シーウッキー(★3) 駄菓子屋のガチャガチャで取得
米氷飴(★4) 駄菓子屋で8/2までに1回はガチャをし、8/3の選択肢で「ガチャしない」を選ぶ。
魚肉ソーセージ(★4) 島モンファイトで少年、少女を全員撃破
すごくHな本(★4) 島モンファイトで蒼に勝利する。
究極のチャーハン(★4) チュートリアルでうみちゃんに勝利し、その後8位のパワーアップしたうみちゃんに勝利する。
賢者になれるHな本(★5) 秘密基地に通い続ける。8/7にもらえる
稀少焼酎・もりいいぞう(★5) 駄菓子屋に通い続ける。8/7にもらえる
うみのパンツ(★5) 神社に通い続ける。選択肢に100円があれば、必ず100円を選ぶ→願い事を聞かれたら「卓球うまくなりたい」→イナリが出たときは50円→1000円の選択肢があれば1000円を選ぶ→その後も100円をお賽銭し続ける。8/7にもらえる。(島モンチュートリアルのときのうみちゃんのセリフが変化する)

エサと場所で入手できる島モン組み合わせ一覧

なんとなく属性がわかりづらいな〜と思った島モンは後ろに属性を記載してます。
しかし、見にくいっすね…。すみません。

エサ\場所  山のなか(★1) 浜辺(★1) ため池(★1) 山の奥(★2) 海岸(★2) 神社(★3) 山の最奥(★3) 絶壁(★4) 海賊の洞窟(★4) 秘境(★5)
砂糖水(★1) クロオオアリ(★1) カメノテ(★1) シオマネキ(★1) ヤマトシジミ(★1) ヤマトシジミ(★1) マメハチドリ(★3) ハナカマキリ(★2) リュウグウノツカイ(★3) リュウグウノツカイ(★3) タヌキ(★2)
イカの皮(★1) トノサマバッタ(★1) マダコ(★1) イカナゴ(★1) アブラゼミ(★1) アカボシゴマダラ(★1) ユリカモメ(★2) ミヤマクワガタ(★2) エチゼンクラゲ(★3) ダイオウイカ(★3) エチゼンクラゲ(★3)
ミミズ(★1) オケラ(★1) アメリカザリガニ(★1) コカマキリ(★1) ニホンヤモリ(★1) アブラゼミ(★1) イワツバメ(★2) アオダイショウ(★2) ピラルク(★3) リュウグウノツカイ(★3) ピラルク(★3)
アイスの棒(★1) アオカナブン(★1) アサリ(★1) ヒグラシ(★1) ハエトリグモ(★1) クマンバチ(★1) 17年ゼミ(★3) タヌキ(★2) ダイオウイカ(★3) ダイオウイカ(★3) 17年ゼミ(★3)
昆虫ゼリー(★1) エンマコオロギ(★1) ミズクラゲ(★1) クマンバチ(★1) アゲハチョウ(★1) ミドリガメ(★1) モルフォチョウ(★2) オンブバッタ(★1) リュウグウノツカイ(★3) リュウグウノツカイ(★3) ヒッコリーホーンドデビル(★3)
かき氷のシロップ(★2) クマンバチ(★1) クマンバチ(★1) ツクシガモ(★2) ミヤマクワガタ(★2) カメノテ(★1)[海] ミヤマクワガタ(★2) アオダイショウ(★2) ヒッコリーホーンドデビル(★3) ヒッコリーホーンドデビル(★3) ケツァルコアトル(★4)
ナスビ(★2) アカボシゴマダラ(★1) カラバガニ(★2) カメムシ(★1) アライグマ(★2) ニホンミツバチ(★1) ケンランカマキリ(★2) ミヤマクワガタ(★2) アレキサンドラトリバネアゲハ(★3) エチゼンクラゲ(★3) ヤタガラス(★4)
ユムシ(★2) キジバト(★1) ヒヨドリ(★1) ライギョ(★2) ニホンカナヘビ(★2) マガモ(★2) マメハチドリ(★3) アライグマ(★2) リュウグウノツカイ(★3) ピラルク(★3) 朱雀(★5)[空]
鰹節(★2) ツクツクボウシ(★1) カラスアゲハ(★1) ツクツクボウシ(★1) ニホンカナヘビ(★2) ユリカモメ(★2) アライグマ(★2) オニヤンマ(★2) ピラルク(★3) ダイオウイカ(★3) ダイオウイカ(★3)
島産蜂蜜                    
ちょっとHな本                    
失敗したチャーハン(★3) オケラ(★1)、ヘラクレスオオカブト(★3) 国産ウナギ(★2) コウモリ(★2) イナリ(★4)、ミヤマクワガタ(★2)、アレキサンドラトリバネアゲハ(★3) コウモリ(★2)、エンゼルフィッシュ(★2) ケツァルコアトル(★4) ジュエルキャタピラー(★3)、ギラファノコギリクワガタ(希少種)(★3) リュウグウノツカイ(★3)、シロハヤブサ(希少種)(★3) リュウグウノツカイ(★3) 17年ゼミ(★3)、ヘラクレスオオカブト(★3)
シーウッキー(★3) ツクシガモ(★2) ハリセンボン(★2)、ダイオウイカ(★3) 国産ウナギ(★2) アライグマ(★2)、シロハヤブサ(★3) オオスズメバチ(★2)、ハリセンボン(★2)、ラブカ(★4) テンゼン(★4) 白虎(★5)、イナリ(★4) オオシャコガイ(★3)、17年ゼミ(★3)、ケツァルコアトル(★4) オオシャコガイ(★3)、エチゼンクラゲ(★3) オオシャコガイ(★3)
米氷飴(★4) ジュエルキャタピラー(★3) ラブカ(★4) オニヤンマ(★2) ジュエルキャタピラー(★3) オオシャコガイ(★3) ヤタガラス(★4) ギラファノコギリクワガタ(★3) リュウグウノツカイ(★3) リュウグウノツカイ(★3) テイオウムカシヤンマ(★3)
魚肉ソーセージ(★4) チュパカブラ(★4) クマゼミ(★2) オオシャコガイ(★3) ヒッコリーホーンドデビル(★3) アレキサンドラトリバネアゲハ(★3) テンゼン(★4) ヒッコリーホーンドデビル(★3) シロハヤブサ(★3) ケツァルコアトル(★4)[空] ヤタガラス(★4)
すごくHな本(★4) 17年ゼミ(★3) ピラルク(★3)、テイオウムカシヤンマ(★3) エチゼンクラゲ(★3) テイオウムカシヤンマ(★3) クマゼミ(★2)、タラバガニ(★2)、ヌシ(★4) 朱雀(★5)[空] アレキサンドラトリバネアゲハ(★3) ライギョ(★2)、ヤタガラス(★4) ダイオウイカ(★3) ニホンオオカミ(★4)、テンゼン(★4)、ケツァルコアトル(★4)[空]
究極のチャーハン(★4) ホンノウサギ(★2)、ケツァルコアトル(★4) リュウグウノツカイ(★3) クリオネ(★2)、ヤタガラス(★4) アレキサンドラトリバネアゲハ(★3)、ニホンオオカミ(★4) タラバガニ(★2)、エチゼンクラゲ(★3) ケツァルコアトル(★4) アルマジロトカゲ(★3)、シロハヤブサ(★3) イナリ(★4) ピラルク(★3) リュウグウノツカイ(★3)
賢者になれるHな本(★5) テンゼン(★4)[空] ユリカモメ(★2) ピラルク(★3) シロハヤブサ(★3) リョウイチ(★4) シロハヤブサ(★3) アレキサンドラトリバネアゲハ(★3) アルマジロトカゲ(★3) ヌシ(★4) ヤタガラス(★4)
稀少焼酎・もりいいぞう(★5) アルマジロトカゲ(★3) リョウイチ(★4) ヌシ(★4) マメハチドリ(★3) オオシャコガイ(★3) 朱雀(★5)[空] 白虎(★5)[山] 青龍(★5)[海] ラブカ(★4) オニヤンマ(★2)
うみのパンツ(★5) アルマジロトカゲ(★3)、17年ゼミ(★3) 人魚(★4) 玄武(★5)[海] テンゼン(★4)[空] シロハヤブサ(★3)、ダイオウイカ(★3) テンゼン(★4) チュパカブラ(★4)[山] ヤシガニ(★2)、ニホンオオカミ(★4) テイオウムカシヤンマ(★3)、人魚(★4) ニホンオオカミ(★4)

Kotlin文法まとめ

最近iPhoneアプリの移植で触り始めました。
メモとして適宜追加していきます。

when文 (Java/Cでいうswitch文)

変数fruitで分岐させる場合。Java/Cでいうbreakは不要です。

val FRUIT_APPLE    :Int = 0
val FRUIT_BANANA   :Int = 1
val FRUIT_MELON    :Int = 2
val FRUIT_LEMON    :Int = 3

 :

when ( fruit )
{
    FRUIT_APPLE              -> print("RINGO")
    FRUIT_BANANA             -> print("BANANA")
    FRUIT_MELON, FRUIT_LEMON -> print("MELON or LEMON")
    else                     -> print("OTHER")
}

ブロックも使えます。また複数の文を書きたい場合は、セミコロンで区切ります。

when ( fruit )
{
    FRUIT_APPLE              -> { print("RINGO"); fruit_name="RINGO" }
    FRUIT_BANANA             -> { print("BANANA"); fruit_name = "BANANA" }
    FRUIT_MELON, FRUIT_LEMON -> { print("MELON or LEMON"); fruit_name="MELON or LEMON" }
    else                     -> { print("OTHER"); fruit_name="OTHER" }
}

私はあまり好みではないですが、引数なしのwhen文という書き方もあるようです。

when
{
    fruit == FRUIT_APPLE                         -> { print("RINGO"); fruit_name="RINGO" }
    fruit == FRUIT_BANANA                        -> { print("BANANA"); fruit_name = "BANANA" }
    fruit == FRUIT_MELON || fruit == FRUIT_LEMON -> { print("MELON or LEMON"); fruit_name="MELON or LEMON" }
    else                                         -> { print("OTHER"); fruit_name="OTHER" }
}

Raspberry Pi 3で64GB以上のmicroSDカードを使う方法

Raspberry Pi始めました!
…んが、開始初日から躓いてしまいました。

64GBのmicro SDカードを購入したのですが、Raspberry Pi 3ではFAT32でフォーマットしたカードしか
認識できないようで、32GB以上のカードの場合は有名なSD card Formatter 5.0(現時点の最新版)を使っても、
自動でexFATフォーマットになってしまいRaspberry Piで正しく認識してくれないのでした。

どーするか。

解決法

SD card Formatter 4.0を使ってください。公式からは見つからず、ダウンロードはこのあたりから…。
https://sd-card-formatter.jp.uptodown.com/windows

で、起動したら[フォーマットオプション]から、[論理サイズ調整]をONにしてからフォーマットを
開始してください。これだけでRaspberry PimicroSDカードを正しく認識し、OSのインストールを進めることができます。

余談

この解決法に辿りつくまでBuffalo製のDisk Formatter Ver.2.08やHP USB Disk Storage Format Tool 2.2.3を
試したのですが、前者はmicroSDカードのライトプロテクトを外してくださいといった意味不明なこと
microSDカードにはWriteProtectはない)を言ってきたり、後者はFAT32でフォーマット出来たにも関わらず
Raspberry PiのOSインストール中にエラーが起こったりと、なかなかうまくいきませんでした。

ここは素直にSD Card Formatter 4.0を使用するのが良いようです。