C#で写真の中の線を抽出してみた
C#でOpenCvSharpを使って、グレースケール画像に書き込まれた線を抽出してみました。例えば、X線写真の後処理で書き込まれた線を抽出するという感じです。
目次
サンプル
グレースケールの写真に各色で書き込まれた線があります。
この写真から、各色で描かれた線を取り出してみます。
OpenCvSharpをWPFアプリケーションで使う準備
OpenCvSharpをWPFアプリに適用するには、NuGetでいくつかのパッケージをインストールする必要があります。
Visual Studioのプロジェクトメニューの中のNuGetパッケージ管理から、下記を追加してください。
OpenCvSharp4
OpenCvSharp4.runtime.win
OpenCvSharp4.WpfExtensions
各色を取り出す手順
色が付いている線については、彩度が高い画素を抽出してマスクを作ります。そのマスクを元画像にかけることで、色が付いた画素を抽出します。
白色の線については、輝度が明らかに高い画素を抽出します。それを白色の画素とします。
XAML
本稿で作成するアプリのXAMLは全て共通です。
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="384" Width="1024">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Image Name="imgOriginal" Grid.Column="0" Stretch="None" />
<Image Name="imgPickuped" Grid.Column="1" Stretch="None" />
</Grid>
</Window>
WindowにImageを横に並べました。左のImageに元画像を、右のImageに変換後の画像を表示します。
彩度が高い画素を抽出する
画像の中から彩度が高い画素を抽出します。
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Windows.Media.Imaging;
namespace WpfApp1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
Mat src = new Mat("test.png", ImreadModes.Color);
Mat HSVImage = new Mat();
Cv2.CvtColor(src, HSVImage, ColorConversionCodes.BGR2HSV);
Mat[] SplitedHSV = Cv2.Split(HSVImage);
BitmapSource gray_bitmap = BitmapSourceConverter.ToBitmapSource(src);
imgOriginal.Source = gray_bitmap;
BitmapSource cny_bitmap = BitmapSourceConverter.ToBitmapSource(SplitedHSV[1]);
imgPickuped.Source = cny_bitmap;
src.Dispose();
}
}
}
Matクラスのコンストラクターで、画像ファイルの読み込みをします。
Mat m = new Mat(fn, imagemode);
変数 |
型 |
内容 |
---|---|---|
fn |
string |
読み込む画像ファイル名 |
imagemode |
ImageModes |
画像のモード |
m |
Mat |
OpenCV用の行列 |
ImageModesには画像のモードを指定します。今回読み込む画像はカラー画像なので、ImreadModes.Colorのようにカラー画像であると指定します。そうすると、読み込んだMatクラスのインスタンスは、BGRのマトリクスになります。
BGR形式の画像マトリクスをHSV形式に変換します。Hが色相、Sが彩度、Vが明度ですね。
Cv2.CvtColor(src, dst, code, cn);
変数 |
型 |
内容 |
---|---|---|
src |
Mat |
入力画像のマトリクス |
dst |
Mat |
出力先のマトリクス |
code |
ColorConversionCodes |
変換モード |
cn |
int |
省略可。出力するチャネル |
戻り値はありません。出力先のMatインスタンスを引数に指定します。
GBR形式をHSV形式に変換するには、ColorConversionCodes.BGR2HSVを指定します。
HSV形式のMatインスタンスを作ったら、そこからSチャネルを取り出します。
Mat [] m = Cv2.split(src);
変数 |
型 |
内容 |
---|---|---|
src |
Mat |
入力画像のマトリクス |
m |
Matの配列 |
入力マトリクスをチャネル毎に分離したマトリクスの配列 |
splitメソッドの戻り値は、Matクラスの配列になります。元がHSV形式のMatインスタンスの場合、Hだけ、Sだけ、VだけのMatインスタンスを配列にしたものなります。配列のインデックスを指定すれば、Sチャネルだけを取り出すことができるようになるわけです。
Sチャネルだけを取り出した画像を表示してみましょう。(上記コード)
白以外のは取り出せているように見えますね。
念のために、閾値を決めて2値化しておきます。
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Windows.Media.Imaging;
namespace WpfApp1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
Mat src = new Mat("test.png", ImreadModes.Color);
Mat HSVImage = new Mat();
Cv2.CvtColor(src, HSVImage, ColorConversionCodes.BGR2HSV);
Mat[] SplitedHSV = Cv2.Split(HSVImage);
Mat SImage = new Mat();
Cv2.Threshold(SplitedHSV[1], SImage, 200, 255, ThresholdTypes.Binary);
BitmapSource gray_bitmap = BitmapSourceConverter.ToBitmapSource(src);
imgOriginal.Source = gray_bitmap;
BitmapSource cny_bitmap = BitmapSourceConverter.ToBitmapSource(SplitedHSV[1]);
imgPickuped.Source = cny_bitmap;
src.Dispose();
}
}
}
2値化するには、Thresholdというメソッドを使用します。
double d = Cv2.Threshold(src, dst, thresh, maxval, type);
変数 |
型 |
内容 |
---|---|---|
src |
Mat |
入力画像のマトリクス |
dst |
Mat |
出力用のMatインスタンス |
thresh |
Double |
閾値 |
maxval |
Double |
ThresholdTypesがBinaryかBinaryInvの際に使用する最大値 |
type |
ThresholdTypes |
2値化するさいの閾値の決め方 |
d |
Double |
ThresholdTypesがOtsuの場合の閾値 |
戻り値は、判定基準に大津の方法を指定した場合の閾値です。大津の方法の場合は自動的に閾値を計算するので、それを戻すわけですね。2値化したマトリクスの出力先となるMatインスタンスは、引数に指定する必要があります。
ThresholdTypesはいくつかあります。(Binary、BinaryInv、Trunc、Tozero、TozeroInv、Otsu、Triangle)
今回は判定基準をBinaryにして閾値を200に設定しましたが、これは線の色が彩度がかなり高いということがわかっていたからです。画像に合わせて調整してください。
閾値をどう指定するか迷ったら、とりあえずOtsuにしてみるのも手です。
2値化して得られる画像はこのようになります。(上記のプログラム)
白色の画素を抽出する
白、黒、グレーは彩度が低いので、彩度で画素を抽出すると白線部分が得られません。
そこで、白線は白色部分だけで抽出を行います。
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Windows.Media.Imaging;
namespace WpfApp1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
Mat src = new Mat("test.png", ImreadModes.Color);
Mat HSVImage = new Mat();
Cv2.CvtColor(src, HSVImage, ColorConversionCodes.BGR2HSV);
Mat[] SplitedHSV = Cv2.Split(HSVImage);
Mat SImage = new Mat();
Cv2.Threshold(SplitedHSV[1], SImage, 200, 255, ThresholdTypes.Binary);
Mat GrayImage = new Mat();
Cv2.CvtColor(src, GrayImage, ColorConversionCodes.BGR2GRAY);
Mat WhiteMask = new Mat();
Cv2.Threshold(GrayImage, WhiteMask, 253, 255, ThresholdTypes.Binary);
BitmapSource gray_bitmap = BitmapSourceConverter.ToBitmapSource(src);
imgOriginal.Source = gray_bitmap;
BitmapSource cny_bitmap = BitmapSourceConverter.ToBitmapSource(WhiteMask);
imgPickuped.Source = cny_bitmap;
src.Dispose();
}
}
}
CvtColorメソッドで元画像をグレースケールに変換します。
そして、Thresholdメソッドで2値化します。
そうすると、このように白色部分だけが抜き出せます。(上記のプログラム)
マスクを合成する
以上で彩度が高い部分と白色部分のそれぞれのマスクができました。
そうしたら、この2つのマスクを合成します。
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Windows.Media.Imaging;
namespace WpfApp1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
Mat src = new Mat("test.png", ImreadModes.Color);
Mat HSVImage = new Mat();
Cv2.CvtColor(src, HSVImage, ColorConversionCodes.BGR2HSV);
Mat[] SplitedHSV = Cv2.Split(HSVImage);
Mat SImage = new Mat();
Cv2.Threshold(SplitedHSV[1], SImage, 200, 255, ThresholdTypes.Binary);
Mat GrayImage = new Mat();
Cv2.CvtColor(src, GrayImage, ColorConversionCodes.BGR2GRAY);
Mat WhiteMask = new Mat();
Cv2.Threshold(GrayImage, WhiteMask, 253, 255, ThresholdTypes.Binary);
Mat MargedMask = new Mat(SImage.Size(), SImage.Type());
Cv2.BitwiseOr(SImage, WhiteMask, MargedMask);
BitmapSource gray_bitmap = BitmapSourceConverter.ToBitmapSource(src);
imgOriginal.Source = gray_bitmap;
BitmapSource cny_bitmap = BitmapSourceConverter.ToBitmapSource(WhiteMask);
imgPickuped.Source = cny_bitmap;
src.Dispose();
}
}
}
Matインスタンスを合成するためには、先に合成先のMatインスタンスを作っておきます。その際に、マトリクスのサイズを同じにする必要があります。
そこで、Matクラスのコンストラクターにサイズを指定してインスタンスを作成します。
Mat m = new Mat(size, type);
変数 |
型 |
内容 |
---|---|---|
size |
Size |
作成するMatインスタンスのサイズ(cols, rows) |
type |
MatType |
画像のモード (深度やチャネル) |
m |
Mat |
作成されたMatインスタンス |
MatインスタンスのSizeメソッドでそのインスタンスのサイズが、Typeメソッドでそのインスタンスのモードがえら得るので、それをコンストラクターの引数にしました。 こうすると、同じ条件の空のMatインスタンスを作成できます。
合成にはBitwiseOrというメソッドを使用します。
Cv2.BitwiseOr(src1, src2, dst, mask);
変数 |
型 |
内容 |
---|---|---|
src1 |
Mat |
入力マトリクス1つめ |
src2 |
Mat |
入力マトリクス2つめ |
dst |
Mat |
出力マトリクス |
mask |
Mat |
省略可。マスク用マトリクス |
これは2つのマトリクスのビット演算をするメソッドです。
マスク用のMatインスタンスを2つ入力してOR演算をすることで、両方のマスクを適用したMatインスタンスを作成します。
できあがったマスク画像はこのようになります。(上記のプログラム)
合成したマスクを画像に適用する
できあがったマスク画像を画像に適用すると、画像から各色だけを抜き出すことができます。
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Windows.Media.Imaging;
namespace WpfApp1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
Mat src = new Mat("test.png", ImreadModes.Color);
Mat HSVImage = new Mat();
Cv2.CvtColor(src, HSVImage, ColorConversionCodes.BGR2HSV);
Mat[] SplitedHSV = Cv2.Split(HSVImage);
Mat SImage = new Mat();
Cv2.Threshold(SplitedHSV[1], SImage, 200, 255, ThresholdTypes.Binary);
Mat GrayImage = new Mat();
Cv2.CvtColor(src, GrayImage, ColorConversionCodes.BGR2GRAY);
Mat WhiteMask = new Mat();
Cv2.Threshold(GrayImage, WhiteMask, 253, 255, ThresholdTypes.Binary);
Mat MargedMask = new Mat(SImage.Size(), SImage.Type());
Cv2.BitwiseOr(SImage, WhiteMask, MargedMask);
Mat dst = new Mat(src.Size(), src.Type());
Cv2.BitwiseAnd(src, src, dst, MargedMask);
BitmapSource gray_bitmap = BitmapSourceConverter.ToBitmapSource(src);
imgOriginal.Source = gray_bitmap;
BitmapSource cny_bitmap = BitmapSourceConverter.ToBitmapSource(WhiteMask);
imgPickuped.Source = cny_bitmap;
src.Dispose();
}
}
}
元画像にマスクを適用するためにBitwizeAndというメソッドを使用しています。
Cv2.BitwiseAnd(src1, src2, dst, mask);
変数 |
型 |
内容 |
---|---|---|
src1 |
Mat |
入力マトリクス1つめ |
src2 |
Mat |
入力マトリクス2つめ |
dst |
Mat |
出力マトリクス |
mask |
Mat |
省略可。マスク用マトリクス |
これはAnd演算をするメソッドです。
ここでは入力するMatインスタンスを同じにしています。そうすると、And演算の結果は同じになります。mask引数にマスクを指定することで、マスクを適用した画像を生成しています。
出力はこうなります。(上記のプログラム)
抜き出せましたね。
背景を白色にする
背景色が黒色ですと何かと使い勝手が悪いこともありますので、背景色を白色にして白色の線を黒色で表示するようにしてみます。
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Windows.Media.Imaging;
namespace WpfApp1
{
public partial class MainWindow : System.Windows.Window
{
public MainWindow()
{
InitializeComponent();
Mat src = new Mat("test.png", ImreadModes.Color);
Mat HSVImage = new Mat();
Cv2.CvtColor(src, HSVImage, ColorConversionCodes.BGR2HSV);
Mat[] SplitedHSV = Cv2.Split(HSVImage);
Mat SImage = new Mat();
Cv2.Threshold(SplitedHSV[1], SImage, 200, 255, ThresholdTypes.Binary);
Mat GrayImage = new Mat();
Cv2.CvtColor(src, GrayImage, ColorConversionCodes.BGR2GRAY);
Mat WhiteMask = new Mat();
Cv2.Threshold(GrayImage, WhiteMask, 253, 255, ThresholdTypes.Binary);
Mat MargedMask = new Mat(SImage.Size(), SImage.Type());
Cv2.BitwiseOr(SImage, WhiteMask, MargedMask);
Mat dst = new Mat(src.Size(), src.Type());
Cv2.BitwiseAnd(src, src, dst, MargedMask);
Mat WhiteDst = new Mat(src.Size(), src.Type(), new Scalar(255, 255, 255));
Mat BlackImage = new Mat(src.Size(), src.Type(), new Scalar(0, 0, 0));
Cv2.CopyTo(BlackImage, WhiteDst, WhiteMask);
Cv2.CopyTo(dst, WhiteDst, SImage);
BitmapSource gray_bitmap = BitmapSourceConverter.ToBitmapSource(src);
imgOriginal.Source = gray_bitmap;
BitmapSource cny_bitmap = BitmapSourceConverter.ToBitmapSource(WhiteMask);
imgPickuped.Source = cny_bitmap;
src.Dispose();
}
}
}
Matクラスのコンストラクターを使って、白色一色のMatインスタンスを作成します。
Mat m = new Mat(size, type, s);
変数 |
型 |
内容 |
---|---|---|
size |
Size |
作成するMatインスタンスのサイズ(cols, rows) |
type |
MatType |
画像のモード (深度やチャネル) |
s |
Scalar |
省略可。作成されるMatインスタンスの各要素の初期値 |
m |
Mat |
作成されたMatインスタンス |
Scalarと言っても数値ではありません。Scalarクラスのインスタンスです。
Scalar s = new Scalar(v0, v1, v2);
変数 |
型 |
内容 |
---|---|---|
v0 |
Double |
要素の1次元目の値。例えば要素がBGRの場合はBの値。 |
v1 |
Double |
要素の2次元目の値。例えば要素がBGRの場合はGの値。 |
v2 |
Double |
要素の3次元目の値。例えば要素がBGRの場合はRの値。 |
s |
Scalar |
Scalarインスタンス |
白色の場合は(255, 255, 255)、黒色の場合は(0, 0, 0)です。
白一色のMatインスタンスに、線を書き込んでいきます。
まず、白色の線を色で書き込みます。書き込みように、黒一色のMatインスタンスを作成します。
そして、CopyToメソッドを使って、白色のMatインスタンスに黒色のマットインスタンスをコピーします。その際に、白色線のマスクを指定することで、マスク部分だけがコピーします。
Cv2.CopyTo(src, dst, mask);
変数 |
型 |
内容 |
---|---|---|
src |
Mat |
コピー元のMatインスタンス |
dst |
Mat |
コピー先のMatインスタンス |
mask |
Mat |
省略可。マスク用Matインスタンス |
白線を黒色でコピーしたら、白色以外の線を同様にコピーします。コピー元は先に生成済みの線の抜き出し画像(最初のインプット画像でも 大丈夫だと思う)に、マスクは色つき線を抜き出した際のマスクを使用します。
出力はこうなります。(上記のプログラム)
公開日
広告
C#で画像処理カテゴリの投稿
- C#でBitmapImageをByte配列に変換してみた
- C#でBitmapで描いた画像をImageコントロールに表示してみた
- C#でHSBで色指定してラスタ画像を描いてみた
- C#でOpenCVを使う
- C#でラスタ画像を描いてみた
- C#で写真の中の線を抽出してみた
- C#で描いた画像をファイルに保存してみた
- C#で画像の色を反転してみた
- C#で画像を描いてみた(WPFでBitmapSource.Create編)
- C#で画像を描いてみた(WPFでWritableBitmap編)
- C#で画像ファイルを表示してみた
- C#のOpenCvSharpでMatとBitmapSourceを変換する
- C#のWPFアプリでPNG形式の画像ファイルの読み込みと書き出しをする