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のインストール
なにはともあれ、コンパイラをインストールします。
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 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]ボタンを押下します。
今回は、AutoItをデフォルト32-bitで動作させているので、OpenCVも32-bitでビルドします。
そのため今回は「Win64が付いていない」プロジェクトを選択します。
再びCMake-guiの画面に戻ります。
32-bit版の場合、cuda関連のビルドが通らないという噂がありますので、「WITH_CUDA」のチェックを外します(最近のは最初から外れてるかも)
また「OPENCV_EXTRA_MODULES_PATH」に「C:/temp/opencv_contrib-3.4.3/modules」を指定します。
何度も言いますが、パス区切り文字は「/」にしてください。
コンソールに「Configuring done」が出力されたら成功です。
続いて「Generate」ボタンを押下します。
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 -> 見つからなかった ; その他 -> エラー発生 ; 検知した座標はおそらく($x,$y)からのもの。今回ほしいのは実際のデスクトップ上の座標なので、加算して格納する。 If ($ret[0] = 0) Then $detectedX = $x + $ret[6] $detectedY = $y + $ret[7] 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(サマーポケッツ) 島モンファイト エサと場所で入手できる島モン組み合わせ一覧
発売日に予約していたものの時間が取れず、積んでおりました。昨日クリアしました…。
いや、大変よかったですね!各所でボロボロ泣きました。
特に後半は泣きすぎてマウスがクリックできなかったほど。
今回は個別ルート含め全体的にレベルが高い!
だーまえは原案だけということでちょっと心配でしたが、杞憂に終わりました。よかったよかった。
ただちょっと短かったかなぁ。
後半もうちょっとガッツリ作ってくれれば大満足でした。とりあえずファンディスク期待。
って、ちがう!!
この記事は島モンファイトの攻略記事です!
感想を書いてる場合じゃない!
島モンファイトはゲーム内のミニゲームで島モンを集めて、島の人たちとバトルをするもので、
まぁなんていうかポ○モンみたいなものです。いちおう専用のエンディングがあったりします。
ここでは島モンファイトについて、筆者が確認した範囲で情報をまとめたいと思います。
島モンは、同じ場所・同じエサの組み合わせでも複数候補の中からランダムに入手されるので、これで全部ということはありません。
たぶんREFLECTION BLUE版も同じと思われます。色々間違ってたらごめんネ。
島モンルートへの入り方
7/29にうみちゃんのアイコンを選択し、選択肢「うみちゃんと同じ物を1つ」を選ぶ→その後「参加を決める」を選ぶ。
エサ一覧と入手方法
エサ名 | エサの入手方法 |
---|---|
砂糖水(★1) | 初回島モン開始時にうみちゃんがくれる |
スイカの皮(★1) | 初回島モン開始時にうみちゃんがくれる |
ミミズ(★1) | 初回島モン開始時にうみちゃんがくれる |
アイスの棒(★1) | 初回島モン開始時にうみちゃんがくれる |
昆虫ゼリー(★1) | 島モンファイトで天善に勝利するともらえる |
かき氷のシロップ(★2) | 秘密基地に通い続ける。7/31にもらえる |
ナスビ(★2) | 島モンファイトで主婦を全員撃破 |
ユムシ(★2) | 島モンファイトで漁師、おっさんを全員撃破 |
鰹節(★2) | 島モンファイトで亀、犬、猫を撃破 |
島産蜂蜜(★3) | 7/29の秘密基地の蜂退治のイベントで「頼んだぞ、良一!」「お前ならやれる、天善!」「俺が行くしかないだろう!」の3つの選択肢すべてを閲覧しているとイベント後に入手できる |
ちょっとHな本(★3) | 7/30に蒼ルートを選び、その後の宝探し中に秘密基地を選択する |
失敗したチャーハン(★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) |
島産蜂蜜(★3) | オオスズメバチ(★2) | プラナリア(★2) | 17年ゼミ(★3) | タヌキ(★2) | 人魚(★4) | 17年ゼミ(★3)、テンゼン(★4)[空] | チュパカブラ(★4) | マメハチドリ(★3) | ギラファノコギリクワガタ(★3) | ヘラクレスオオカブト(★3) |
ちょっとHな本(★3) | イナリ(★4) | プラナリア(★2) | テンゼン(★4)[空] | オオカマキリ(★2) | ラブカ(★4) | テイオウムカシヤンマ(★3) | ニホンオオカミ(★4) | ダイオウイカ(★3) | ヘラクレスオオカブト(★3) | エチゼンクラゲ(★3) |
失敗したチャーハン(★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 PiがmicroSDカードを正しく認識し、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を使用するのが良いようです。