C#でTesseract 5

矢野です。
社内向けに作成していたC#アプリケーションで利用するTesseractのバージョンを4系から5系に更新した概要を簡単にメモしておきます。Tesseractに限らずC#での画像認識に関する情報って、Pythonなどに比べると日本語でも英語でもかなり少ないですよね。ビジネスロジックをつくっていく段階であれば、もちろん他のプログラミング言語向けの情報でも大いに参考になるのですけれど、最初の一歩して「とりあえず動くようにする」ところがその言語なり、ライブラリなりの固有の手順が必要で、そういうところで引っ掛かりがちです。 .NET Framework, .NET向けに作成されたTesseractのラッパーライブラリとしてよく使われているのは恐らくcharlesw/tesseractだと思います。私もこれまでお世話になってきていたのですが、残念ながらTesseract 4.1までしかサポートされていません。今すぐ5系が必要な状況というわけではないですが、余力のある時に乗り換えておいた方が良いこともあるだろうと思って、対応したライブラリを探してみました。 上記のcharleswさんのところから派生したSicos1977/TesseractOCRを導入することで、ほんの少しのコードの修正でTesseract 4系から5系へ移行することができました。
NuGetパッケージマネージャーからTesseractOCRをインストール
Visual Studioからであれば、NuGetパッケージとして簡単にインストールできます。 以降前のcharlesw/tesseract向けのコードは、非常に簡素に書くとこんな感じ。

using Tesseract;

// 中略

// PictureBoxに表示
pictureBox1.Image = new Bitmap("image\\lipsum.png");

// 文字認識
var engine = new TesseractEngine("tessdataフォルダのパス", "eng");
using (var pix = Pix.LoadFromFile("image\\lipsum.png"))
{
    var page = engine.Process(pix);

    // 結果表示
    textBox1.Text = page.GetText();
}


これを、Sicos1977/TesseractOCR向けに修正すると、

using TesseractOCR;

// 中略

// PictureBoxに表示
pictureBox1.Image = new Bitmap("image\\lipsum.png");

// 文字認識
var engine = new Engine("tessdataフォルダのパス", "eng");
using (var pix = TesseractOCR.Pix.Image.LoadFromFile("image\\lipsum.png"))
{
    var page = engine.Process(pix);

    // 結果表示
    textBox1.Text = page.Text;
 }


名前空間とクラス名、オブジェクトの構成を少し修正するだけで、ほぼそのまま使えました。
分かりにくいけど上が認識対象の画像、下がTesseract 5.2での認識結果です。
えっ? たったこれだけ? そうなんです、たったこれだけでした。その先は、これまでのTesseract 4系向けにつくっていたのと同じようにビジネスロジックをつくり込んでいくことができました(同時期にEAST text detectorを組み込んでみたりもしたけど、それは別の話)。これまで未経験の技術とか、「たかが」ライブラリのアップデートとか、どうしても気後れしちゃうこともありますが、一歩踏み出してしまえば「たったこれだけ」で進めちゃうこともよくあるよね、というお話でした。

UWPアプリからOpenCVしてみる(その2)

矢野です。UWPアプリからOpenCVを使う話の決着をつけておきます。 Visual Studioを起動して、メインページにボタンを追加します。 
NuGetを使って、OpenCvSharp(OpenCvSharp4, OpenCvSharp4.runtime.uwp)をインストールします。 ボタンのクリックイベントハンドラ(find_Click)に、カメラからのキャプチャ、データ変換、ARマーカーの検出、結果を表示という流れでコードを書いていきます。
private async void find_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    ImageEncodingProperties encProp = ImageEncodingProperties.CreateBmp();

    var source = captureElement.Source;

    SoftwareBitmap bitmap = null;
    using (var captureStream = new InMemoryRandomAccessStream())
    {
        await source.CapturePhotoToStreamAsync(encProp, captureStream);
        await captureStream.FlushAsync();
        captureStream.Seek(0);

        var decoder = await BitmapDecoder.CreateAsync(captureStream);
        bitmap = await decoder.GetSoftwareBitmapAsync(
            BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    }

    Mat mat = null;
    using (var stream = new InMemoryRandomAccessStream())
    {
        var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, stream);
        encoder.SetSoftwareBitmap(bitmap);
        await encoder.FlushAsync();

        mat = Mat.FromStream(stream.AsStreamForRead(), ImreadModes.Color);
    }

    var grayMat = new Mat();
    Cv2.CvtColor(mat, grayMat, ColorConversionCodes.BGR2GRAY);

    CvAruco.DetectMarkers(grayMat,
        CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50),
        out Point2f[][] corners, out int[] ids, DetectorParameters.Create(), out Point2f[][] rejectedImgPoints);

    CvAruco.DrawDetectedMarkers(mat, corners, ids);

    using (var stream = new MemoryStream())
    {
        mat.WriteToStream(stream);
        using (var raStream = stream.AsRandomAccessStream())
        {
            var decoder = await BitmapDecoder.CreateAsync(raStream);
            bitmap = await decoder.GetSoftwareBitmapAsync(
                BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }
    }

    var imageSource = new SoftwareBitmapSource();
    await imageSource.SetBitmapAsync(bitmap);
    image.Source = imageSource;
}
OpenCvSharpは優れたライブラリで、.NETアプリケーションからOpenCVを使いやすくラップしてくれています。おかげでOpenCVでのARマーカー検出、描画は難しくありませんでした。 ところが、それ以前にUWPのSoftwareBitmapとOpenCVのMat型の相互の変換についてはなかなか消化することができず、あちこちのオンラインドキュメントやブログやフォーラムを何度も何度も巡回しつつ、試行錯誤しつつ行ったり来たりを繰り返してようやくそれらしい形にたどり着きました。 あまりそんなことばっかりやっているわけにはいきませんが、ある程度余裕を持て、藻掻いて、悩む時間も時には大事ですね。
これでもって改めて前回の記事をカメラで撮ってみる(横着)と、無事にARマーカーとそのID(3)が認識されていることが確認できました。

UWPアプリからOpenCVしてみる(その1)

矢野です。

プロトタイプ的にWindows用のアプリケーションを新たに作成するにあたり、ちょっと考えました。Windows Formsなら自分もそれなり多少は経験もあるけど、新規でつくるのに今さらFormsというのもどうなのかしら? でもWPFやUWPも結局のところあまり普及してきている感じがしないし、枯れたFormsに比べれば、情報はかなり少ない。XAML Islands? 何それ?
このプロトタイプは、PCに接続したWebカメラで撮影した画像をOpenCVでちょちょいと弄れるだけでいいのですが、でもFormsからカメラなどメディア関連の制御をするのは結構大変です。DirectShowとかMedia Foundationなどを使ってC++でゴリゴリ書く気になれば色々細かいところまで自由になるけれど、プロトタイプなのにそのへんをめちゃくちゃ頑張る気にはなれないし、とはいっても入手が容易なC#向けのラッパーだとちょっと古かったり、ちょっと不便だったり……。
で、けっきょくUWPアプリをC#で書くことにしました。それでも周回遅れなのかもしれませんが、せっかくの機会ですから少しでも新しい仕組みに追いついておきたいということで。
  • UWPアプリ上でMediaCaptureを使ってWebカメラにアクセス
  • Webカメラから取り込んだ画像をOpenCVで処理
  • 処理した画像を表示
これらを実現する簡単なアプリをつくってみたいと思います。 Visual Studio 2019を起動し、C#/Windows/UWPの「空白のアプリ(ユニバーサルWindows)」を新しいプロジェクトとして作成。
ソリューションエクスプローラーからPackage.appxmanifestをダブルクリック→機能タブの[Webカメラ]と「マイク]にチェックを入れて保存。
MainPage上にツールボックスからCaptureElementを配置し、適当な名前(captureElement)を付ける。StretchプロパティをUniformToFillにしておく。 MainPageのOnNavigatedTo(NavigationEventArgs)メソッドをオーバーライドする(awaitな呼び出しを行うので、メソッドにasync修飾子を追加します)。
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
    var capture = new MediaCapture();

    await capture.InitializeAsync();

    var props = capture.VideoDeviceController
        .GetMediaStreamProperties(MediaStreamType.VideoPreview) as VideoEncodingProperties;

    await capture.VideoDeviceController
        .SetMediaStreamPropertiesAsync(MediaStreamType.VideoPreview, props);

    captureElement.Source = capture;
    await capture.StartPreviewAsync();
}
プロジェクトをビルドして起動します。

アクセス許可を確認するダイアログが表示された場合は、許可します。
  • マイクへのアクセスを許可しますか?→[はい]
  • カメラにアクセスすることを許可しますか?→[はい]
たったこれだけで、リアルタイムにカメラの画像が表示されました! すごい! 泣きそうなりながらC++でMedia Foundationで書いた時とは大違い!(大げさ)
その2に続きます。

イメージ・マジックで今一番アツいレンズマウントは?

矢野です。去年(2018年)から、フルサイズなミラーレスカメラが非常に盛り上がっていますね。これまで私は機動性重視で小型軽量なマイクロフォーサーズを愛用してきましたが、これだけにわかに盛り上がってくると、多少大きくても(マイクロフォーサーズもいつの間にかずいぶん大きいのが多くなってきたし)フルサイズについつい目移りしてしまうものです(お値段はがもうちょっとどうにかなっていただきたいものではありますが……)。個人的には、Lマウントのシグマのカメラがどんななのかが非常に気になります。 では、我らがイメージ・マジックで人気のカメラはどんなものなんでしょう? 今回は、社内のごくごく一部(?)で非常に盛り上がっているレンズマウントをご紹介しましょう。Sマウントです!

Sマウントとは

「Sマウント」と言っても、戦後から1950年代ごろまで製造されたニコンのレンズマウントのことではありません。そっちはそっちで面白そうですが、脱線は我慢。工業用とか、監視カメラ用の小型の規格です。「M12マウント」(内径が12mm)とも呼ばれるようです。
Sマウント用レンズ
レンズは単焦点です。側面に螺子が切ってあって、これの締め具合でフォーカスの調整をします。特にストッパーなども無く、奥までねじ込んじゃうとセンサーと多分ぶつかるのでちょっとコワいです。
Sマウント対応のカメラモジュール
同じSマウントに準拠していればレンズやモジュールの交換は可能ですから、カメラの用途や対象物との距離などによって適宜使い分けることもできます。

Sマウントのカメラで撮影する

レンズを取り付けたカメラモジュール(PCにはUSBケーブルで接続)
レンズのマウントの形式は、当然ですがカメラ制御そのものにはあまり影響しません。UVC(USB Video Class)に対応したカメラモジュールであれば、PCに接続するだけで基本的には使用できるようになります。WindowsならMedia FoundationやDirectShow、その他の画像を扱うライブラリ等からアクセス可能です。カメラモジュール独自の機能を使いたい場合は、カメラ専用のAPIを呼び出すことになります。 というわけで、「使うだけ」ならさほど難しくないSマウントのカメラですが、これをどう使うか、価値を生み出すように使えるかが私たちの腕の見せ所です。

「ブラックマーク」は縁の下の力持ち

はじめに

矢野です。小石川オフィスに引っ越してきて、1か月あまりが経ちました。小石川や、それに近い西片・本郷界隈は起伏が激しくて入り組んだ道が多く、引っ越し直後はランチの後にちょっと散歩するとすぐに迷子になっていたものです。それでもだんだんと脳内の地図が整理されてきて、かなり歩きやすくなってきました。

ブラックマークって何?

ちょっとググってみると、「ドリフト走行などで路面にできるタイヤの黒い跡」とか「汚点」とか、今年公開のそういうアクション映画があるとか、色々出てきます。ですが、私たちが扱っているブラックマークとは、「印刷用紙の裏側の黒い印」のことです。

ブラックマーク

ブラックマークの例。用紙を横切るような帯状のものや、紙面の中央に四角く描かれたものなど。

生産管理に使用する小さなラベルや完成品をお届けするための配送伝票などのシールの台紙となる剥離紙の裏側に印刷されている黒い印や帯のことです。シールとして貼る際に捨てられてしまうので、お客様の目に触れることはありません。いわば縁の下の力持ちですね。

何のために使うの?

私たちが取り扱うラベルプリンターには印字ヘッドの近くに光学式のセンサーが内蔵されていて、それで台紙の裏側をスキャンできます。ブラックマークを検知するまでは用紙を送り出して、検知したところで位置合わせ。そこから印刷を始めて、1枚分印刷できたら台紙ごとカットする(これもプリンターに内蔵されたカッターがやってくれます)。1枚ごとの長さが決まっているロール式のラベルを正確に取り扱うための、重要なマークです。

と、ある程度取り扱えるようになってから言うのは簡単ですが、プリンターと用紙がそれぞれ専用の組み合わせでない場合は、用紙の向き、サイズ、光学センサーの位置、検知の方式(透過式? 反射式?)、マークからの相対的な位置の設定などなど、いろいろな苦闘がありました。用紙のフィードがいつまでも止まらなかったり、おかしなところでちょん切られたり……。

形のないソフトウェアの世界と、プリンターやRFIDリーダーなどのハードウェア、印刷メディアなどの間を行ったり来たりしながら、私たちは開発の仕事をしています。

印刷しているのは、商品だけじゃありません

はじめに

こんにちは、イメージ・マジックの矢野です。入社から2か月が経過し、順調に机の上がゴチャゴチャしてきました。カメラが2台、バーコードプリンターが2台、小さなスピーカーが1台、さらにカードリーダーが1台、デスクの上に転がっています。

印刷しているもの

当社が印刷しているのは、Tシャツやパーカー、マグカップ、カーペットなどの製品そのものももちろんですが、それだけではお客様のお手元に製品を届けることはできません。生産工程でそれらを管理するためのラベルシールや、納品書、配送業者さんにお願いする送り状なども自社のシステムで印刷しています。当然といえば当然なのですが。

ラベルシールの印刷には、専用のプリンターを使用しています。私は転職してきて初めて触りました。ずんぐりむっくりでかわいい形ですが、言うことを聞かせるのはなかなか難しいです。プリンター専用のツールやライブラリを使ってバーコードや文字列や画像を出力するように指示することもできますが、プリンターの制御用コマンドを直接(「直接」とは言っても、通り抜けなければならないレイヤーは多数あります)送信することもできます。

ところで、「プリンターで何かを印刷する」というのはどういうことなのでしょう? それは文字列や画像、バーコードを印刷しなさいというコマンドの積み重ねです。ラベルシールにバーコードを印刷する場合には、最終的にたくさんのしましまが紙面に出力されるようなコマンドをアプリケーションで生成して、それをプリンターに送信する必要があるわけです。

たくさんのしましまを、どうやってプリンターに送信しましょうか?

  1. しましまを1本1本引くコマンドを生成して、順番に実行する
  2. バーコード全体の画像をPNGなどのラスター画像で生成して、画像を印刷するコマンドを実行する
  3. 「このデータ(ID)に対応するバーコードを、このバーコード規格を使ってイイ感じに生成してよ」とプリンターに任せるコマンドを実行する
  4. その他

1も2も3も正解ですが、1は特に厳しい。バーコードの仕様に従って、1本1本のしましまの太さと間隔をどうにかこうにかしなければなりません。2は割とよくあると思います。バーコード画像を生成するライブラリを使用すれば、割とサクっとできるでしょう。でも、せっかく専用のプリンターを使っているのですから、彼(彼女?)に任せてしまってみましょう。

イイ感じにやってもらう

Windowsで、Win32 APIを使って実装してみます。プリンターによろしくやってもらう代わりに、プリンター固有のお作法に従わなくてはなりません。ここでは、Zebra Technologies社のプリンタ向け言語、ZPL(Zebra Programming Language)のコマンドを実行しています。

int PrintRawCommand()
{
	HANDLE hPrinter;

	if (!OpenPrinter((LPSTR)"Printer Name", &hPrinter, NULL))
	{
		// エラー処理

		return FALSE;
	}

	DOC_INFO_1 docInfo;
	docInfo.pDocName = (LPSTR)"Barcode sample";
	docInfo.pOutputFile = NULL;
	docInfo.pDatatype = (LPSTR)"RAW";

	LPSTR command = (LPSTR)"^XA^FO40,40^BY3^BEN,40,Y,N^FD123456789012^FS^XZ";

	if (StartDocPrinter(hPrinter, 1, (LPBYTE)&docInfo))
	{
		if (StartPagePrinter(hPrinter))
		{
			DWORD writtenBytes;
			if (WritePrinter(hPrinter, command, strlen(command), &writtenBytes))
			{
				// 印刷成功時の処理
			}
			else
			{
				// エラー処理

				return FALSE;
			}
		}
		else
		{
			// エラー処理

			return FALSE;
		}
	}
	else
	{
		// エラー処理

		return FALSE;
	}

	EndPagePrinter(hPrinter);
	EndDocPrinter(hPrinter);
	ClosePrinter(hPrinter);

	return TRUE;
}

15行目で、プリンタへ送信するデータの形式をRAW(生=Zebraのプリンターが解するコマンドを直接送信する)に指定します。17行目の文字列が実際にプリンタへ送信されるZPLのコマンドです。レイアウト、フィールド、バーコードの表現形式(EAN-13)、バーコード化したいデータ(123456789012)などを、 ^ で始まるコマンドで指定します。

これの出力結果がこうなります。

EAN-13の詳細な仕様を隅々まで探らなくても、難しい画像操作系のAPIを駆使しなくても、美しいバーコードを得ることができました。

Node.jsのネイティブなアドオンを書く

はじめに

こんにちは。イメージ・マジックの矢野です。入社からもうすぐ1か月です。

Node.jsのアドオンを、何らかの理由でネイティブのコードで書かなければならないことがあります。Node.jsのエンジンであるV8のお作法に則って書くことになりますが、V8がバージョンアップして仕様が変われば、そのお作法も変わります。お作法が変わればアドオンの書き方も変えなければならず、毎度毎度書き直すのは大変。そこで、Node.jsのネイティブなアドオンの実装をイイ感じにラップして抽象化してくれる、NAN(Native Abstractions for Node.js)というものがつくられたとのこと(READMEの冒頭を読むと、クスリとさせられます)。もちろんNANを使わなくてむ作れるわけですが、せっかく便利なものがあるのですから、使わない手はありません。

準備

Node.jsのアドオンをつくるのですから、当然Node.jsはインストールされていないといけません。

NANはnode-gypでビルドするため、node-gypもインストールしておかなければいけません。さらにgypはPythonスクリプトなので、Pythonが入っていなければインストールしておかなければなりません。

ネイティブコードをビルドするために、コンパイラなども必要です。今回はWindowsを使って開発しているので、Visual Studio 2017をインストールしておきます。

作成

準備ができたところで、作業用のディレクトリを作成します。

mkdir hoge
cd hoge

NANをインストールします。

npm install --save nan

binding.gypを作成します。このテキストファイルには、アドオンの構成を記述します。ネイティブならではのアドオンをばっちり作り込むのはそれなりに大変なので、今回はOSのバージョンを取得するだけの簡単なものにしようと思います。

{
  "targets": [
    {
      "target_name": "getosversion",
      "sources": [
        "src/addon.cpp",
      ],
      "include_dirs": [
        "<!(node -e \"require('nan')\")",
      ],
    }
  ]
}

ソースファイルを格納するディレクトリ(src)をつくり、ソースファイルを形だけでも(空ファイルでOK)つくっておきます(src/addon.cpp)。

node-gypconfigureすると、上記の構成に従ってVisual StudioのC++プロジェクトやソリューションを生成してくれます。

node-gyp configure

buildディレクトリの下にいろいろファイルができています。

│  binding.gyp
│
├─build
│      binding.sln
│      config.gypi
│      getosversion.vcxproj
│      getosversion.vcxproj.filters
│
└─src
        addon.cpp

binding.slnを開くと、Visual Studioが起動しますので、addon.cppの中身を書いていきます。NANの恩恵を受けるには、ヘッダファイルnan.hをincludeしておく必要があります。
Win32 APIのGetVersion()を呼び出して、OSのバージョン情報を取得してみます。ちなみにこの関数は現在「非推奨」になっていますが、Win32がメインの記事ではないので、そのまま使ってしまいます(業務ではこんなことしませんよ)。

#include <sstream>
#include <nan.h>

NAN_METHOD(GetOsVersion)
{
	DWORD dwVersion = GetVersion();

	DWORD dwMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
	DWORD dwMinorVersion = (DWORD)(HIBYTE(LOWORD(dwVersion)));

	DWORD dwBuild = 0;
	if (dwVersion < 0x80000000)
	{
		dwBuild = (DWORD)(HIWORD(dwVersion));
	}

	std::stringstream ss;
	ss << dwMajorVersion << "." << dwMinorVersion << "." << dwBuild;

	info.GetReturnValue().Set(Nan::New<v8::String>(ss.str()).ToLocalChecked());
}

NAN_MODULE_INIT(init)
{
	Nan::SetMethod(target, "GetOsVersion", GetOsVersion);
}

NODE_MODULE(getosversion, init)

ビルドします。

node-gyp build

もろもろビルドされます。肝心のアドオンは、build/Release/getosversion.nodeとして生成されます。

│  binding.gyp
│
├─build
│  │  binding.sln
│  │  config.gypi
│  │  getosversion.vcxproj
│  │  getosversion.vcxproj.filters
│  │
│  └─Release
│      │  getosversion.exp
│      │  getosversion.iobj
│      │  getosversion.ipdb
│      │  getosversion.lib
│      │  getosversion.map
│      │  getosversion.node
│      │  getosversion.pdb
│      │
│      └─obj
│          └─getosversion
│              │  addon.obj
│              │  vc141.pdb
│              │  win_delay_load_hook.obj
│              │
│              └─getosversion.tlog
│                      ... (省略) ...
│
└─src
        addon.cpp

これを実際に動かしてみましょう。テスト用コードをこんな感じで書いてみます(test.js)。

var addon = require('./build/Release/getosversion');

var version = addon.GetOsVersion();

console.log(result);

nodeコマンドから実行してみると、それらしい値が取れました。

node test.js
10.0.16299