C#で写真の中の線を抽出してみた

C#でOpenCvSharpを使って、グレースケール画像に書き込まれた線を抽出してみました。例えば、X線写真の後処理で書き込まれた線を抽出するという感じです。

目次

  1. サンプル
  2. OpenCvSharpをWPFアプリケーションで使う準備
  3. 各色を取り出す手順
    1. XAML
    2. 彩度が高い画素を抽出する
    3. 白色の画素を抽出する
    4. マスクを合成する
    5. 合成したマスクを画像に適用する
  4. 背景を白色にする

サンプル

グレースケールの写真に各色で書き込まれた線があります。

元画像

この写真から、各色で描かれた線を取り出してみます。

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値化して得られる画像はこのようになります。(上記のプログラム)

彩度を抽出して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インスタンス

白線を黒色でコピーしたら、白色以外の線を同様にコピーします。コピー元は先に生成済みの線の抜き出し画像(最初のインプット画像でも 大丈夫だと思う)に、マスクは色つき線を抜き出した際のマスクを使用します。

出力はこうなります。(上記のプログラム)

抜き出し済み画像(白背景)

公開日

広告