この記事はOpenCV Advent Calendar 2021の18日目の記事です。
記事が大変長いので、今北産業でまとめると...
HSP3 で OpenCV4系を使用できるように、
同じOpenCV4系のラッパーである OpenCvSharp を活用し、
HSP用のDLLヘッダ定義と日本語機械翻訳リファレンス(C++/HSP用)を作成した
という、お話しになります。DLLヘッダ定義などの生成物のダウンロードはこちら。
対象読者
- HSP3 で OpenCV4系を使いたい方
- HSP3 で他言語にあるフレームワーク/ライブラリを使用できるように移植したい方
- OpenCV が標準サポートしていない言語で使用できるように移植したい方
はじめに
2021年にリリースされた最新版の HSP 3.6 には、OpenCV の機能を使用できる hspcv.dll が同梱されています。ところが、OpenCV のバージョンが 1.0 と古く、Webカメラのキャプチャが正しく動作しなかったり、ディープラーニングによる画像認識など、新しい OpenCV の機能を HSP から使用することができません。
本記事は、OpenCV4系を HSP で使用できるように、必要なDLL定義やリファレンスを移植した際の技術メモとなります。
環境
以下の環境で動作を確認しました。
- Windows 10 64bit
- HSP 3.6
- Microsoft Visual Studio 2017 Community ※ 2022でも大丈夫かと
- OpenCV 4.5.3
- OpenCVSharp 4.5.3 (20210821)
使用ツール
- DeepL Pro 翻訳ツールとして使用
- Doxygen 1.9.2 C++用のHTMLドキュメント作成ツール
- AngleSharp C#でHTMLを解析するライブラリ
- Microsoft.CodeAnalysis.CSharp Roslyn CodeAnalysisでC#ソースの解析
今回作成したもの
OpenCV4系をHSPで使用するために、以下の作業を行いました。
- OpenCV4系の関数をHSPから使用できるように、HSP用DLLヘッダ定義ファイルを作成
- 32bit/64bit版 HSPランタイムに対応する
- 今回は CV_8UC3 などの定数値の定義は作成しない
- OpenCV(C++) のリファレンスを日本語化(機械翻訳)
- OpenCV(HSP) の HSP Docs Library用の日本語リファレンスを作成
1. HSP用DLLヘッダ定義ファイルを作成
今回は、.NET実装(ラッパー)である OpenCvSharp を活用してHSPへの移植を行いました。
以下が理由として挙げられます。
- C++版 OpenCV 4系 は C形式のインターフェースが提供されておらず、HSP から直接使用するのが困難である。何かしらのラッパーを作成しないといけない。
- 一方で、OpenCvSharp に OpenCvSharpExtern.dll が同梱されている。これは、C++版 OpenCV を C形式のインターフェースで DLL 化したものであり、これを活用することで HSP から容易に使用することが可能。
- 32bit/64bit 両方のDLL(.NET 的にはアンマネージドDLL)用意されている。
- C++ の STLを操作するための関数が一部用意されている。
- 理論上は、VBScript とか 日本語プログラミング言語なでしこ Ver.1 でも OpenCV を呼ぶことができるかと。(今回はやりませんけど)
- OpenCvSharp が放置されず、定期的にメンテナンスされていること。
- C# で OpenCvSharpExtern.dll の P/Invoke 定義が記述されているのもポイント。
以降は、OpenCvSharp を活用した、HSPのヘッダーファイルの作成手順を記述します。
OpenCvSharpExtern の DLL定義の調査
HSPでは外部DLLを使用する際に、以下のような外部関数定義をあらかじめ行う必要があります。
#uselib "DLL名"
#func HSP上の関数名 "DLL内部の関数名" 引数パラメタ...
今回は、OpenCVSharpExtern.dll を使用しますので、DLL がエクスポートしている関数定義を 2,762 個記述する必要があります。
#uselib "OpenCvSharpExtern32.dll" // 32bit版
#func core_Mat_new1 "core_Mat_new1" var
#func core_Mat_new2 "core_Mat_new2" int,int,int,var
// 以降、#func が 2,760 行分続きます...
しかも、HSP上の 変数名 や 関数名 は 59文字以下 という制限がありますが、ximgproc_segmentation_SelectiveSearchSegmentationStrategyMultiple_clearStrategies()
のような 59文字 を大幅に超える関数が OpenCVSharpExtern.dll にてエクスポートされています。HSP上の関数定義では短くなるように加工する必要もあります。
#uselib "OpenCvSharpExtern32.dll" // 32bit版
#func ximgproc_seg_SeleSchSegStratMultiple_clearStrategies "ximgproc_segmentation_SelectiveSearchSegmentationStrategyMultiple_clearStrategies" sptr
はい、手でなんかでやってられません!
幸いなことに、OpenCvSharp 内に OpenCvSharpExtern.dll がエクスポートしている関数の定義情報(P/Invoke定義)を記載した、以下のような C# のファイルが多数存在しています。
[Pure, DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern ExceptionStatus core_Mat_new1(out IntPtr returnValue);
[Pure, DllImport(DllExtern, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern ExceptionStatus core_Mat_new2(int rows, int cols, int type, out IntPtr returnValue);
OpenCvSharp 内の P/Invoke定義が記述されたソースコードを活用して、HSP用のDLL定義に変換すれば良さそうです。
C# のソースコードのパース方法ですが、正規表現で頑張るという方法も考えましたが、今回は Microsoft.CodeAnalysis.CSharp を使用しました。HTMLのDOM操作の感覚でC#のソースコードのパースが行えるので、とても便利でした!
OpenCVSharp の P/Invoke定義(C#)をMicrosoft.CodeAnalysis.CSharp を使って解析し、HSP3用のDLL定義へ変換するということをしています。
— いのびあ@vaccinated(fully) (@hk1v) November 3, 2021
C#のソースコードがツリー情報で返ってくるので、HTMLのDOMを操作するような感覚で情報が取得できました。便利! pic.twitter.com/6skuo6mJyQ
なお、変換部分のソースはごっちゃごっちゃしているので、現状非公開とさせていただきます。
作成したHSP用の OpenCvSharpExtern.dll 定義ファイルはこちらにあります。
また、変換にあたって作成した資料はこちらにまとめて置いてあります。
移植にあたってのビット数に関する注意点
基本的には HSP は 32bit アプリケーションですが、64bit 版 HSPランタイムもベータ版的扱いで存在します。両ビットを考慮して作成はしていますが、64bit版HSPランタイムに関しては 2021年12月現在、HSP側の制限事項が多いため、本移植が不完全な実装であることをご了承ください。
詳しくは省略しますが、Windows アプリケーションの32bit/64bitではポインタサイズや呼び出し規約が異なっています。その影響により、HSPからDLL内の関数を呼び出す際にビット数によって引数の数が異なる場合があります。特に OpenCV には構造体の値渡しを行うパラメータが多くあり、ビット数によってHSP上でのパラメータの指定方法に顕著な差が出てきます。
移植にあたっては、ビット数の差も考慮に入れる必要があります。
今回、OpenCVで使用される引数の型とビット数ごとのデータサイズをまとめた一覧を作成しました。
作成資料
移植例
core_Mat_new3()
を例にしますと、C++ 上ではパラメータが 5つ になります。
HSPの 32bit版 では 8つ になりますが、64bit版 では 5つ になります。
struct MyCvScalar
{
double val[4];
};
// 引数は5つ
CVAPI(ExceptionStatus) core_Mat_new3(int rows, int cols, int type, MyCvScalar scalar, cv::Mat **returnValue)
{
BEGIN_WRAP
*returnValue = new cv::Mat(rows, cols, type, cpp(scalar));
END_WRAP
}
#include "OpenCvSharpExtern32.as"
#define CV_8UC3 16
pMat = 0
ddim scalar, 4
scalar = 0.0, 0.0, 0.0, 0.0 // BGRA
// 引数は8つ
// #func global core_Mat_new3 "core_Mat_new3" int,int,int,double,double,double,double,var
core_Mat_new3 480, 640, CV_8UC3, scalar(0), scalar(1), scalar(2), scalar(3), pMat
#include "hsp3_64.as"
#include "hspint64.as"
#include "OpenCvSharpExtern64.as"
#define CV_8UC3 16
pMat = int64(0)
ddim scalar, 4
scalar = 0.0, 0.0, 0.0, 0.0 // BGRA
// 引数は5つ
// #func global core_Mat_new3 "core_Mat_new3" int,int,int,var,var
core_Mat_new3 480, 640, CV_8UC3, scalar, pMat
2. OpenCV(C++) のリファレンスを日本語化(機械翻訳)
公式でもリファレンスが提供されていますが、英語のみとなっています。
また、OpenCV.jp による正確な日本語翻訳が提供されていますが、2.2系 と少し古くなっています。
インターネット上の OpenCV を使用した参考資料やソースコードは C++ や Python が多いので、HSPの利用者が移植の手助けになるように、OpenCV 4.5.3(C++) のリファレンスを日本語機械翻訳しました。
機械翻訳ですので文章の正確性は欠けますが、OpenCV でやりたいことの手がかりを日本語で探すという観点では有用と考えています。
OpenCV(C++) リファレンスの機械翻訳手順
今回は、DeepL Pro を使用して機械翻訳を実施しました。
無料版では翻訳可能な文字数に制限があり、リファレンスを丸ごと翻訳するのには向いていないため、有料版を使用しています。
OpenCV は Doxygen で出力したHTML形式リファレンスを提供していますが、DeepL は HTMLファイルをそのまま渡して翻訳させることができません。ですので、HTMLファイル内から翻訳すべき文字列を抽出し、DeepLにて翻訳したのちに、元のHTMLに翻訳結果を書き戻すという作業を実施しました。
HTMLのパースは AngleSharp を使用しました。
作成した対訳辞書は CSV形式 でこちらにあります。
なお、実際のHTMLパース処理、対訳辞書の作成機能、翻訳処理部分のソースは、現状非公開とさせていただきます。
作成したOpenCV 4.5.3のリファレンス(日本語機械翻訳)はこちら。
3. OpenCV(HSP) の HSP Docs Library用の日本語リファレンスを作成
HSP には、HSP Docs Library (以下、HDL)という HSP のドキュメントやリファレンスをまとめて 検索・閲覧する仕組みがあり、専用のHS形式を作成することで検索対象として追加することができます。OpenCvSharp4HSP 用のHSファイルを作成して、HDL上でOpenCV4の関数を日本語で検索できるようにしてみました。
OpenCV(HSP) リファレンスの作成手順
OpenCV(C++)の日本語訳のリファレンスは、2. OpenCV(C++) のリファレンスを日本語化(機械翻訳) にて作成しておりますので、これを元ネタとしてHSP用のリファレンスを作成します。
ところが… 1つ大きな問題点があり、OpenCVのC++クラス名/関数名からHSP上の関数名を求めるには(逆の場合も同様)、OpenCvSharpExtern.dll 上の関数名 と OpenCVのC++クラス名の対応をマッピングした辞書(図中はC/C++マッピング辞書)が中間に必要になります。
そして、C/C++マッピング辞書を作成するには、実際に OpenCVSharpExtern 内の実装を確認しないといけません。
以下のような感じですべての関数が実装されていれば、素直に抽出できそうですが…
C側関数名 | C++側クラス名/関数名/変数名 |
---|---|
aruco_drawAxis()関数 | cv::aruco クラス drawAxis()関数 |
CVAPI(ExceptionStatus) aruco_drawAxis(
cv::_InputOutputArray *image,
cv::_InputArray *cameraMatrix,
cv::_InputArray *distCoeffs,
cv::_InputArray *rvec,
cv::_InputArray *tvec,
float length)
{
BEGIN_WRAP
cv::aruco::drawAxis(*image, *cameraMatrix, *distCoeffs, *rvec, *tvec, length);
END_WRAP
}
以下のように、引数にクラスインスタンスを渡して処理をしている場合もあり、手動できちんと中身を確認しないといけません。
C側関数名 | C++側クラス名/関数名/変数名 |
---|---|
aruco_Dictionary_setMaxCorrectionBits()関数 | cv::aruco::Dictionary クラス maxCorrectionBits 変数 |
CVAPI(ExceptionStatus) aruco_Dictionary_setMaxCorrectionBits(cv::aruco::Dictionary *obj, int value)
{
BEGIN_WRAP
obj->maxCorrectionBits = value;
END_WRAP
}
OpenCVSharpExtern.dll には 2,762 個の外部エクスポート関数があるんだよなぁ…
というわけで頑張って、全関数を手動で確認しました!!
作成資料
作成したOpenCV 4.5.3のHSP用リファレンス(日本語機械翻訳)はこちら。
実際に動かしてみた!
Darknet + YOLO による物体検出が HSP でもできるようになります!
少し前に Twitter で話題になりました、ウマ娘の画像をディープラーニングで判定してみた もHSP で出来るように!!
アニメで見たウマ娘を深層学習に通すとどうなるのかな〜と思ってゲームのチュートリアル画面をdarknetに通してみたら、
— Ar-Ray (@Ray255Ar) December 5, 2021
本当に馬だと判定されてしまい困惑しています🐎 pic.twitter.com/XVgqo5zD1s
↓↓↓ HSP での実行例 ↓↓↓
画像上にテキストを表示する機能に不具合があるため、別ウィンドウ上に結果を表示しています。
サンプルコードはこちら。
※ 「ウマ娘 プリティーダービー」をプレイする際の配信ガイドライン に従い、スクリーンショット画像を利用しております。
お客様は本コンテンツから取り込んだゲームプレイの動画や静止画を、共有サイトに投稿することができます。
OpenCV 内部例外について
OpenCV 内部(OpenCVSharpExtern.dll)ではエラー時にC++例外(cv::Exception)を送出するようになっています。ところが、HSPのランタイム内部でもC++例外の仕組みを使用したエラー管理システムが存在していますが、例外の取得処理が catch-all になっているため、OpenCV内部で例外が送出されるとHSPのランタイムはシステムエラー扱いで強制終了してしまいます。
抜本的対策は、HSPランタイムの修正かOpenCVSharpExtern.dllにて例外を送出しないように修正することになります。今回は、例外時の詳細メッセージを取得することを行いたいので、Windows ベクトル化例外処理の仕組み(AddVectoredExceptionHandler)を活用して、HSPランタイムを修正することなく例外時のOpenCVのエラーメッセージを取得できるように対応してみました。
(スローされた例外をなかったことにはしていないので、結局強制終了してしまいますが…)
詳しい説明に関しましては省略しますが、以下 C++ソースをDLL化し、HSPで呼ばれるようにしました。ビルド済みのものはこちら。
// 例外発生時処理関数 (32bit版しか考慮していません)
static LONG WINAPI TopLevelExceptionFilter(EXCEPTION_POINTERS* pExceptionInfo)
{
// https://devblogs.microsoft.com/oldnewthing/20100730-00/?p=13273
const auto& eCode = (pExceptionInfo->ExceptionRecord)->ExceptionCode;
// C++ 例外
if ( eCode == 0xE06D7363)
{
// 32bit (64bitは別対応が必要)
const auto& prm2 = (pExceptionInfo->ExceptionRecord)->ExceptionInformation[2];
if (!prm2)
{
return EXCEPTION_EXECUTE_HANDLER;
}
const auto& a = (int*)prm2;
const auto& b = a[3];
const auto& c = (int*)b;
const auto& d = c[1];
const auto& e = (int*)d;
const auto& f = e[1];
const auto& g = (char*)f + 0x8 + 0x1; // 64bit = 0x10 + 0x1
char szName[256 + 1] = { 0 };
::UnDecorateSymbolName(g, szName, 256, UNDNAME_NO_ARGUMENTS);
std::string strName(szName);
if ( strName == "class cv::Exception")
{
// OpenCVっぽい?
const auto& ex = reinterpret_cast <std::exception*>
((pExceptionInfo->ExceptionRecord)->ExceptionInformation[1]);
::MessageBoxA(::GetActiveWindow(), ex->what(), "OpenCV Exception!!", MB_OK | MB_ICONERROR);
return EXCEPTION_EXECUTE_HANDLER;
}
else if (strName == "enum HSPERROR")
{
return EXCEPTION_CONTINUE_SEARCH; // HSPエラーは転送する
}
else {
// それ以外の C++ 例外
::MessageBoxA(::GetActiveWindow(), szName, "C++ Exception", MB_OK | MB_ICONERROR);
return EXCEPTION_EXECUTE_HANDLER;
}
}
return EXCEPTION_EXECUTE_HANDLER;
}
BOOL Register()
{
::AddVectoredExceptionHandler(0, TopLevelExceptionFilter);
return TRUE;
}
#include "OpenCvSharpExtern32.as"
#include "hsperror_patch32.as"
// OpenCVの例外メッセージ取得するように登録
hsperror_patch_Register
// 例外を起こすような処理
pNet = 0
dnn_readNetFromDarknet_Windows "存在しないパス", "存在しないパス", pNet
OpenCV内部で例外スロー時、HSPエラーより先に例外メッセージを取得するように対応してみた図
生成物
アルファ版ということで、仕様/名称などが大きく変更される場合がございます。
予めご了承ください。定数値は未実装です。
また、64bit版のHSPランタイム対応は不完全です。
参考サイト
- x64 での呼び出し規則
- Decoding the parameters of a thrown C++ exception (0xE06D7363)
- ウマ娘はレース場で「馬」となる?(darknet・ディープラーニング)
- YOLO - object detection
おわりに
盛りすぎたと若干後悔…