Web Analytics

こつこつエンジニア

現役アプリ開発者によるIT系特化ブログ

【WPF+OpenCV(C++)】バインディングで画像表示をしてみる(2)

f:id:madai21:20211014191020j:plain

はじめに

前回はWPFアプリのバインディングを使用して固定ファイルの画像表示とウィンドウサイズ変更すると、表示している画像サイズも連動して縮小拡大を行うようにしてみました。
madai21.hatenablog.com

今回は固定ファイルではなくファイルを選択させて画像を切り替えられるようにしてOpenCV Wrapperプロジェクト(C++/CLI)で生成した画像を表示させてみたいと思います。

  1. C# WPFアプリでバインディングの仕組みを利用して画像ファイルから読み込んだ画像を表示する
  2. ウィンドウサイズが変わると連動して画像サイズも変わるようにする
  3. C# WPFアプリでバインディングの仕組みを利用してメニューから特定のActionを実行する ※【今回】※
  4. OpenCV Wrapperプロジェクト(C++/CLI)でベース画像の上に透明な別画像を重ねた画像データ(Overlay画像)をC# WPFアプリで表示する ※【今回】※

環境

この記事で作成したプロジェクトを元に作成していきます。
madai21.hatenablog.com

3. C# WPFアプリでバインディングの仕組みを利用してメニューから特定のActionを実行する

今のままだと画像ファイルが固定のものを使用するようになっているので、ファイルの選択できるようにしてみたいと思います。
ボタンを押したらファイル選択画面を出すようにしてもいいですが、今回はメニューを作ってメニュー項目が押されたらファイルの選択やImageの表示を行うようにしましょう^^


MainWindow.xamlのGridを以下の内容に修正します。
メニューのBaseでベース画像を、Overlayで重ねる画像を、Viewでそれらの画像をOpenCVで生成してImageに表示させるActionを行います。

<Grid>
    <DockPanel>
        <Menu x:Name="menu" DockPanel.Dock="Top">
            <MenuItem Header="Base" IsCheckable="false" Command="{Binding OpenBaseImgFile}"/>
            <MenuItem Header="Overlay" IsCheckable="false" Command="{Binding OpenOverlayImgFile}"/>
            <MenuItem Header="View" IsCheckable="false" Command="{Binding ViewImgFiles}"/>
        </Menu>
        <Image
            Source="{Binding Img}"
            Height="{Binding ImgHeight}"
            Width="{Binding ImgWidth}"
        />
    </DockPanel>
</Grid>


ファイルを選択する処理はOpenBaseImgFileとOpenOverlayImgFileの共通で必要になるはずなので、まずはUtilityクラスを用意しましょう。
Utility.csを新規作成し、以下のように編集します。

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace WpfShowImage
{
    class Utility
    {
        public static string OpenFile(string title, string filter)
        {
            string ret = string.Empty;
            var dialog = new OpenFileDialog()
            {
                Title = title,
                Filter = filter
            };
 
            if (dialog.ShowDialog() == true)
            {
                ret = dialog.FileName;
            }
 
            return ret;
        }
    }
}


次にCommandの処理を行うベースクラスを用意します。
BaseCommand.csを新規作成し、以下のように編集します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
 
namespace WpfShowImage
{
    class BaseCommand : ICommand
    {
        private Action execAction = null;
 
        public event EventHandler CanExecuteChanged;
 
        public BaseCommand(Action action)
        {
            execAction = action;
        }
 
        public bool CanExecute(object parameter)
        {
            return true;
        }
 
        public void Execute(object parameter)
        {
            if (execAction == null)
            {
                return;
            }
 
            execAction();
        }
    }
}


ViewModel.csに以下のコードを追加します。

・
・
・
using System.Windows;
using System.Windows.Input;
・
・
・
namespace WpfShowImage
{
    class ViewModel : INotifyPropertyChanged
    {
        private string baseImgFilePath = string.Empty;
        private string overlayImgFilePath = string.Empty;
       ・
       ・
       ・
        public ICommand OpenBaseImgFile
        {
            get
            {
                return new BaseCommand(new Action(() =>
                {
                    var filePath = Utility.OpenFile("Open Base Img File", "All files (*.*)|*.*");
                    if (!string.IsNullOrEmpty(filePath))
                    {
                        baseImgFilePath = filePath;
                        MessageBox.Show("Set Base Img File", "Info", MessageBoxButton.OK);
                    }
                }));
            }
        }
        public ICommand OpenOverlayImgFile
        {
            get
            {
                return new BaseCommand(new Action(() =>
                {
                    var filePath = Utility.OpenFile("Open Overlay Img File", "All files (*.*)|*.*");
                    if (!string.IsNullOrEmpty(filePath))
                    {
                        overlayImgFilePath = filePath;
                        MessageBox.Show("Set Overlay Img File", "Info", MessageBoxButton.OK);
                    }
                }));
            }
        }

        public ICommand ViewImgFiles
        {
            get
            {
                return new BaseCommand(new Action(() =>
                {
                }));
            }
        }
    }
}


MainWindow.xaml.csを以下の内容に修正します。

・
・
・
namespace WpfShowImage
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private ViewModel vm;
 
        public MainWindow()
        {
            InitializeComponent();

            vm = new ViewModel();
            DataContext = vm;

#if false //ここを削除
            using (var ms = new MemoryStream(File.ReadAllBytes("baseImage.jpg")))
            {
                vm.Img = new WriteableBitmap(BitmapFrame.Create(ms));
                ms.Close();
            }
#endif
        }

        private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            FrameworkElement frameworkElement = Content as FrameworkElement;
            vm.ImgHeight = frameworkElement.ActualHeight - menu.ActualHeight; //ここを修正
            vm.ImgWidth = frameworkElement.ActualWidth;
        }
    }
}

4. OpenCV(C++/CLI Wrapper)でベース画像の上に透明な別画像を重ねた画像データ(Overlay画像)をC# WPFアプリで表示する

まずC++ CLIプロジェクトを新規作成しソリューションに追加しましょう。
f:id:madai21:20210822205559p:plain


プロジェクト名はOpenCVWrapperにします。
使用するフレームワーク.NET Framework 4.7.2」を選択しておきます。
f:id:madai21:20210822205655p:plain


プロジェクトのプロパティ(x64)を更新し、OpenCVを使用できるようにしておきます。
設定についてはこの記事を参考にして下さい。
madai21.hatenablog.com


OpenCVWrapper.hを以下のように編集しましょう。

#pragma once
#include <opencv2/opencv.hpp>
#include <msclr/marshal_cppstd.h>
using namespace System;
using namespace System::Windows;
using namespace System::Drawing;
using namespace msclr::interop;
namespace OpenCVWrapper {
	public ref class OpenCVWrapperCls
	{
	public:
		static Bitmap^ GetImageBitmap(String^ baseFilePath, String^ overlayFilePath)
		{
			auto base = cv::imread(marshal_as<std::string>(baseFilePath));
			auto overlay = cv::imread(marshal_as<std::string>(overlayFilePath));
			auto dst = base.clone();
 
			if (!overlay.empty()) {
				cv::resize(overlay, overlay, base.size());
				cv::addWeighted(base, 1.0, overlay, 0.4, 0.0, dst);
			}
 
			if (!dst.empty()) {
				Bitmap^ ret = gcnew Bitmap(dst.cols, dst.rows, System::Drawing::Imaging::PixelFormat::Format32bppArgb);
				for (int y = 0; y < dst.rows; y++) {
					for (int x = 0; x < dst.cols; x++) {
						auto a = 255;
						auto r = dst.data[(y * dst.step) + (x * 3) + 2];
						auto g = dst.data[(y * dst.step) + (x * 3) + 1];
						auto b = dst.data[(y * dst.step) + (x * 3) + 0];
						auto color = Color::FromArgb(a, r, g, b);
						ret->SetPixel(x, y, color);
					}
				}
				return ret;
			}
 
			return nullptr;
		}
	};
}


ソリューションプラットフォームをx64に変更しておきます。
初期の状態ではSystem::Drawingを使えないので、参照に追加しておきましょう。
右クリックで参照の追加を選択します。
f:id:madai21:20210823002645p:plain


アセンブリからSystem.Drawingにチェックを入れてOKボタンを押します。
f:id:madai21:20210822211206p:plain


WpfShowImageプロジェクトに戻ります。
参照にOpenCVWrapperを追加しておきます。
f:id:madai21:20210823002705p:plain



ViewModel.csを以下の内容に修正します。

・
・
・
using System.IO;
・
・
・
namespace WpfShowImage
{
    class ViewModel : INotifyPropertyChanged
    {
       ・
       ・
       ・
        public ICommand ViewImgFiles
        {
            get
            {
                return new BaseCommand(new Action(() =>
                {
                    //↓↓↓修正ここから↓↓↓
                    if (!string.IsNullOrEmpty(baseImgFilePath))
                    {
                        try
                        {
                            var bitmap = OpenCVWrapper.OpenCVWrapperCls.GetImageBitmap(baseImgFilePath, overlayImgFilePath);
                            using (var ms = new MemoryStream())
                            {
                                bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
                                ms.Seek(0, SeekOrigin.Begin);
                                Img = new WriteableBitmap(BitmapFrame.Create(ms));
                            }
                        }
                        catch
                        {
                        }
                    }
                    //↑↑↑修正ここまで↑↑↑
                }));
            }
        }
    }
}


WpfShowImageプロジェクトも初期の状態ではSystem::Drawingを使えないのでSystem.Drawingを参照に追加しておきましょう。


それでは実行してみます。
まずはメニューのBaseから画像ファイルを選択した後にメニューのViewを押します。
すると「System.BadImageFormatException」が発生しました。。
f:id:madai21:20210823002950p:plain

dllファイルは所定の場所にあることを確認し、WpfShowImageプロジェクトのプロパティを見てみます。
「32ビットを選ぶ」にチェックがついているのがまずそうですね^^;
対象プラットフォームをx64に変えておきましょう。
f:id:madai21:20210823003548p:plain

再度実行してみます。
無事に表示されましたね^^v
f:id:madai21:20210823003830p:plain

次はこの画像をOverlay画像として設定した後にViewで表示させてみましょう。
f:id:madai21:20210823004020p:plain:w180:h180

無事、合成した画像を表示することができましたね^^
f:id:madai21:20210823004413p:plain

おわりに

2回に渡ってOpenCVで生成した画像をWPF上のUIで表示させてみました。
これで、WPFアプリには影響なくOpenCVWrapperだけの修正でOpenCVでもっと画像を加工させることが可能となりました^^

今回作成したプロジェクトは以下の場所から取得できます。
ご自由にご利用下さい。
github.com