OpenCvの.NET向けラッパーであるOpenCvSharpでは、Matクラスを用いて行列を操作します。画像データもMatクラスを中心に操作します。
何か画像処理したいなーと思いまして、こまめに処理経過/処理結果を確認したいなーと思いまして、こまめに動かすならLINQPadだよなーと思いまして、でもMatを直接画像として表示する機能はないんだよなーと思いまして、自分でコード用意しました。
拡張メソッド定義による表示と、LINQPadの機能として用意されているToDumpメソッドの定義による表示の2つを紹介します。
DumpImage拡張メソッドの定義
素直な方から紹介します。
public static class DumpExtensions
{
public static Mat DumpImage(this Mat mat, string description = null)
{
using (var ms = new MemoryStream())
{
mat.WriteToStream(ms);
ms.Position = 0;
Util.Image(ms.ReadFully()).Dump(description);
return mat;
}
}
}
public static class StreamExtension
{
static readonly byte[] buffer = new byte[16 * 1024];
/// <summary>ストリーム全体をbyte[]で返します</summary>
public static byte[] ReadFully(this Stream input)
{
using (MemoryStream ms = new MemoryStream())
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
}
使用例1
void Main()
{
using (var origin = new Mat(@"C:\tmp\Lenna.png"))
{
origin.DumpImage();
}
}
実行結果
拡張メソッドの定義も使用方法もシンプルですね。
DumpImage拡張メソッドの戻り値が呼び出し元のインスタンスなので、下記のように処理経過をガシガシ表示することが出来ます。
使用例2
void Main()
{
using (var origin = new Mat(@"C:\tmp\Lenna.png").DumpImage("origin"))
using (var half = origin.Resize(new Size(origin.Width / 2, origin.Height / 2)).DumpImage("half"))
using (var gray = half.CvtColor(ColorConversionCodes.BGR2GRAY).DumpImage("gray"))
using (var binary = gray.Threshold(0, 255, ThresholdTypes.Otsu).DumpImage("binary"))
{
// なんか処理とかするする
}
}
実行結果
DumpImage拡張メソッドの問題点
便利なのですが、不満な点もあります。
アチコチで使うためにMyExtensionsに定義したくなりますが、Matクラスを扱うためMyExtensionsにOpenCvSharpをインストールする必要があります。他のスクリプトはMyExtensionを参照することになり、それらのスクリプトまでOpenCvSharpと関係を持つことになってしまいます。辛い。
かといって、DumpImageを使う全てのスクリプトにDumpImageの定義をコピペしていくのも面倒です。辛い。
別DllにDumpImageを定義して各スクリプトから個別に参照させるという手もあります。しかし、別Dllに定義してしまうとLINQPadからサクッと閲覧・編集ができなくなってしまいます。辛い。
後述するUtilクラスの恩恵を受けづらくなってしまします。ちょっと辛い。
上記問題を解決する方法の一つとして、LINQPadのToDumpメソッドで拡張する方法を紹介します。
ToDumpメソッドで拡張するver
ToDumpメソッドを定義すると、Dumpメソッドを呼んだときにココを通るようになります。LINQPad側で用意してくれた拡張機能ですので、そういうもんだと思って使いましょう。
ToDumpメソッドの定義
static object ToDump(object input)
{
var typeFullName = input.GetType().FullName;
// OpenCvSharpのMatクラスに対してのDumpだった場合、画像にして表示する
if (typeFullName == "OpenCvSharp.Mat")
{
using (var ms = new MemoryStream())
{
dynamic mat = input; // Matのメソッドに触れるためdynamicに突っ込む
mat.WriteToStream(ms);
ms.Position = 0;
return Util.Image(ms.ReadFully());
}
}
// 特に操作せずに返却→デフォルトのDumpによる表示が行われる
return input;
}
使用例1
void Main()
{
using (var origin = new Mat(@"C:\tmp\Lenna.png").Dump("origin"))
using (var half = origin.Resize(new Size(origin.Width / 2, origin.Height / 2)).Dump("half"))
using (var gray = half.CvtColor(ColorConversionCodes.BGR2GRAY).Dump("gray"))
using (var binary = gray.Threshold(0, 255, ThresholdTypes.Otsu).Dump("binary"))
{
// なんか処理とかするする
}
}
実行結果
DumpImage拡張メソッドの画像が縦に並んだ実行結果と同じです。
使用例2
Dumpを拡張したことにより、LINQPadが用意してくれたUtilクラスの機能を利用できるようになります。
void Main()
{
using (var origin = new Mat(@"C:\tmp\Lenna.png"))
using (var half = origin.Resize(new Size(origin.Width / 2, origin.Height / 2)))
using (var gray = half.CvtColor(ColorConversionCodes.BGR2GRAY))
using (var binary = gray.Threshold(0, 255, ThresholdTypes.Otsu))
{
// 横に並べて表示してくれるやつ。
// 文字列がキャプションになって、それ以降の引数のobjectに対してDumpメソッドを呼んでくれる。
Util.HorizontalRun("origin,half,gray,binary", origin, half, gray, binary).Dump();
}
}
実行結果
画像を横に並べて目視比較したいって場面は多いので、大変助かります。
ToDumpメソッド定義の問題点
Matクラス特有の問題がありまして。実はMatクラス、そもそもDumpメソッドが定義されてるんですよね(!)。記載時点のソースコードはコチラ。今回最大のハマリポイントでした。
挙動としては下記のとおりでした。
- Dumpメソッドの引数を空にすると、MatクラスのDumpメソッドが優先されて呼び出され、stringが返却されます
- Dumpメソッドの引数に文字列とか明示的にnullとかいれることでLINQPadのDumpが呼ばれ、画像を表示しつつ元インスタンスが返却されます
多少手間ですが、Dumpメソッドの引数を操作することで回避できます。MatクラスのDumpに存在しないオーバーロードを選択することでLINQPadのDumpを呼び出せます。後者のDumpはオーバーロードがたくさんあるので、そのどれかを利用できるように引数を突っ込めば良いです。個人的に許容できる手間なのでコレで十分だと思いますが、絶対我慢できないマンはOpenCvSharpをフォークして自分でDumpメソッドを書き換えるといいんじゃないですかね。ないですね。はい。
また、「ToDumpで拡張しちゃうとMatのプロパティ表示したいときにサクッと見れないやん!」という問題もありますが、Matクラスは幅、高さの数値データ以外はポインタの保持ばっかで大した情報は見れないので気にしなくて良いと感じました。
まとめ
DumpImageの長所
- オーバーロードを簡単に足せる(表示スケールとか指定できるようにしておくと便利)
DumpImageの短所
- 拡張の定義場所をドコにするかが悩ましい
- My ExtensionsにするとOpenCvSharpの参照がばらまかれる
- 全スクリプトに定義するのは非現実的
- 外部Dllに定義するとスクリプティング作業における軽量さが失われる
ToDumpの長所
- Utilクラスの表示補助機能を有効活用できる
ToDumpの短所
- image.Dump()だとMatクラスのDumpメソッドが呼ばれてしまい、戸惑う
- Dumpの引数に毎回nullとか文字列とか入れないといけない
現在は前者のDumpImageをMy Extensionsに定義して作業してます。拡張しやすいので。参照がばらまかれる問題も、まぁ私個人の趣味開発環境だし別にいいやと放置してます。