OpenCVのぼかし処理等のフィルタで画像を加工し保存するアプリです。
今回フィルタのパラメータをJSONファイルとして記憶するようにしてみました。
実行環境
Windows10 2004
dotnet –version 5.0.104
Visual Studio Code
PowerShell 5.1
dotnet –version 5.0.104
Visual Studio Code
PowerShell 5.1
プロジェクトの作成
mkdir プロジェクト名
cd プロジェクト名
dotnet new wpf
dotnet add package Microsoft.Xaml.Behaviors.Wpf
dotnet add package ReactiveProperty.WPF
dotnet add package OpenCvSharp4.Windows
code .
ソースコード
ファイル名:MainWindow.xaml
<Window x:Class="WpfSample15OpenCVFilter.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:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
xmlns:local="clr-namespace:WpfSample15OpenCVFilter"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" Name="Winn">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<interactivity:EventToReactiveCommand Command="{Binding LoadedCommand}" />
</i:EventTrigger>
<i:EventTrigger EventName="Closing">
<interactivity:EventToReactiveCommand Command="{Binding ClosingCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="AUTO"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<x:Code>
<![CDATA[
private void PreviewDragOrver(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Effects = DragDropEffects.Copy;
e.Handled = true;
return;
}
e.Effects = DragDropEffects.None;
e.Handled = false;
}
private void OriginalImage_SizeChanged(object sender, SizeChangedEventArgs e)
{
OriginalCanvas.Height = e.NewSize.Height;
OriginalCanvas.Width = e.NewSize.Width;
}
private void ModifiedImage_SizeChanged(object sender, SizeChangedEventArgs e)
{
ModifiedCanvas.Height = e.NewSize.Height;
ModifiedCanvas.Width = e.NewSize.Width;
}
private void Original_ScaleTransform_Changed(object sender, EventArgs e)
{
/*
Winn.Title = string.Format(
"och:{0},oih:{1},zs:{2}",
OriginalCanvas.Height,
OriginalImage.Source.Height,
ZoomSlider.Value
);
*/
if (OriginalImage.Source == null) return;
OriginalCanvas.Height = OriginalImage.Source.Height * ZoomSlider.Value;
OriginalCanvas.Width = OriginalImage.Source.Width * ZoomSlider.Value;
}
private void Modified_ScaleTransform_Changed(object sender, EventArgs e)
{
if (ModifiedImage.Source == null) return;
ModifiedCanvas.Height = ModifiedImage.Source.Height * ZoomSlider.Value;
ModifiedCanvas.Width = ModifiedImage.Source.Width * ZoomSlider.Value;
}
private void Original_ScrollChanged(object sender, EventArgs e)
{
if (ModifiedImage.Source == null) return;
if (OriginalScrollViewer.HorizontalOffset != ModifiedScrollViewer.HorizontalOffset)
{
ModifiedScrollViewer.ScrollToHorizontalOffset(OriginalScrollViewer.HorizontalOffset);
}
if (OriginalScrollViewer.VerticalOffset != ModifiedScrollViewer.VerticalOffset)
{
ModifiedScrollViewer.ScrollToVerticalOffset(OriginalScrollViewer.VerticalOffset);
}
}
]]>
</x:Code>
<ScrollViewer
Name="OriginalScrollViewer"
ScrollChanged="Original_ScrollChanged"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"
Margin="10,10,10,0"
Background="Azure"
Grid.Column="0">
<Canvas
AllowDrop="True"
PreviewDragOver="PreviewDragOrver"
Name="OriginalCanvas"
Margin="0,0,0,0"
Background="AliceBlue">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewDrop">
<interactivity:EventToReactiveCommand Command="{Binding PreviewDropCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Canvas.RenderTransform>
<ScaleTransform Changed="Original_ScaleTransform_Changed" ScaleX="{Binding ZoomScale.Value}" ScaleY="{Binding ZoomScale.Value}"/>
</Canvas.RenderTransform>
<Image
Name="OriginalImage"
Stretch="None"
SizeChanged="OriginalImage_SizeChanged"
Source="{Binding OriginalImage.Value}">
</Image>
</Canvas>
</ScrollViewer>
<StackPanel
Grid.Column="1">
<Expander
Header="ぼかし処理"
IsExpanded="True"
IsEnabled="{Binding FilterEnabled.Value}"
BorderBrush="Gray">
<StackPanel>
<StackPanel Orientation=" Horizontal">
<Label Content="回数:" />
<TextBox Text="{Binding BlurNumberOfTimes.Value}" />
</StackPanel>
<Slider
Minimum="0"
Maximum="32"
Value="{Binding BlurNumberOfTimes.Value}"
TickPlacement="Both" />
</StackPanel>
</Expander>
<Expander
Header="ノンローカルミーンフィルタ"
IsExpanded="True"
IsEnabled="{Binding FilterEnabled.Value}"
BorderBrush="Gray">
<StackPanel>
<StackPanel Orientation=" Horizontal">
<Label Content="h:" />
<TextBox Text="{Binding NonLocalMeanH.Value}" />
</StackPanel>
<Slider
Minimum="0"
Maximum="32"
Value="{Binding NonLocalMeanH.Value}"
TickPlacement="Both" />
</StackPanel>
</Expander>
<Expander
Header="ラプラシアンフィルタ"
IsExpanded="True"
IsEnabled="{Binding FilterEnabled.Value}"
BorderBrush="Gray">
<StackPanel>
<StackPanel Orientation=" Horizontal">
<Label Content="ksize:" />
<TextBox Text="{Binding LaplacianKsize.Value}" />
</StackPanel>
<Slider
SmallChange="2"
LargeChange="2"
Minimum="0"
Maximum="15"
Value="{Binding LaplacianKsize.Value}"
TickPlacement="Both" />
</StackPanel>
</Expander>
<Expander
Header="アンシャープマスキングフィルタ"
IsExpanded="True"
IsEnabled="{Binding FilterEnabled.Value}"
BorderBrush="Gray">
<StackPanel>
<StackPanel Orientation=" Horizontal">
<Label Content="K:" />
<TextBox Text="{Binding UnsharpMaskingK.Value}" />
</StackPanel>
<Slider
SmallChange="10"
LargeChange="10"
Minimum="0"
Maximum="150"
Value="{Binding UnsharpMaskingK.Value}"
TickPlacement="Both" />
</StackPanel>
</Expander>
<Expander
Header="ズーム"
IsExpanded="True"
IsEnabled="{Binding FilterEnabled.Value}"
BorderBrush="Gray">
<StackPanel>
<StackPanel Orientation=" Horizontal">
<Label Content="倍率:" />
<TextBox Text="{Binding ZoomScale.Value}" />
</StackPanel>
<Slider
Name="ZoomSlider"
Minimum="1"
Maximum="16"
Value="{Binding ZoomScale.Value}"
TickPlacement="Both" />
</StackPanel>
</Expander>
<Button
Content="フィルター処理"
IsEnabled="{Binding FilterEnabled.Value}"
Command="{Binding FilterCommand}" />
<Button
Content="クリア"
IsEnabled="{Binding FilterEnabled.Value}"
Command="{Binding ClearCommand}" />
<Button
Content="保存"
IsEnabled="{Binding FilterEnabled.Value}"
Command="{Binding SaveCommand}" />
<Label
AllowDrop="True"
PreviewDragOver="PreviewDragOrver"
Content="こちらにファイルをドラックアンドドロップ" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewDrop">
<interactivity:EventToReactiveCommand Command="{Binding PreviewDropCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Label>
</StackPanel>
<ScrollViewer
Name="ModifiedScrollViewer"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"
Margin="10,10,10,0"
Background="Azure"
Grid.Column="2">
<Canvas
Name="ModifiedCanvas"
Margin="0,0,0,0"
Background="AliceBlue">
<Canvas.RenderTransform>
<ScaleTransform Changed="Modified_ScaleTransform_Changed" ScaleX="{Binding ZoomScale.Value}" ScaleY="{Binding ZoomScale.Value}"/>
</Canvas.RenderTransform>
<Image
Name="ModifiedImage"
SizeChanged="ModifiedImage_SizeChanged"
Stretch="None"
Source="{Binding ModifiedImage.Value}">
</Image>
</Canvas>
</ScrollViewer>
</Grid>
</Window>
ファイル名:MainWindowViewModel.cs
using System.Diagnostics;
using System;
using System.Windows;
using Reactive.Bindings;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Media.Imaging;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Collections.Generic;
namespace WpfSample15OpenCVFilter
{
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ReactiveCommand<DragEventArgs> PreviewDropCommand { get; } = new ReactiveCommand<DragEventArgs>();
// オリジナル画像のOpenCVオブジェクト
private Mat _orginalMat;
// オリジナル画像の表示用
public ReactiveProperty<WriteableBitmap> OriginalImage { get; private set;} = new ReactiveProperty<WriteableBitmap>();
// フィルター処理可能フラグ
public ReactiveProperty<bool> FilterEnabled { get; private set;} = new ReactiveProperty<bool>(false);
// ぼかし処理の繰り返し回数
public ReactiveProperty<int> BlurNumberOfTimes { get; private set;} = new ReactiveProperty<int>(6);
// ノンローカルミーンフィルタ
public ReactiveProperty<int> NonLocalMeanH { get; private set;} = new ReactiveProperty<int>(12);
// ズーム倍率
public ReactiveProperty<int> ZoomScale { get; private set;} = new ReactiveProperty<int>(1);
// 加工画像のOpenCVオブジェクト
private Mat _modifiedMat;
// 加工画像の表示用
public ReactiveProperty<WriteableBitmap> ModifiedImage { get; private set;} = new ReactiveProperty<WriteableBitmap>();
// フィルター処理
public ReactiveCommand FilterCommand { get; } = new ReactiveCommand();
// クリア処理
public ReactiveCommand ClearCommand { get; } = new ReactiveCommand();
// ラプラシアンフィルタksize
public ReactiveProperty<int> LaplacianKsize { get; private set;} = new ReactiveProperty<int>(1);
// アンシャープマスキングフィルタK
public ReactiveProperty<int> UnsharpMaskingK { get; private set;} = new ReactiveProperty<int>(15);
// 画像の保存処理
public ReactiveCommand SaveCommand { get; } = new ReactiveCommand();
// オリジナル画像ファイルのパス
private string _originalImageFilePath = "";
// ロード時の処理
public ReactiveCommand LoadedCommand { get; } = new ReactiveCommand();
// クローズ時の処理
public ReactiveCommand ClosingCommand { get; } = new ReactiveCommand();
private WriteableBitmap LoadPicture(string path)
{
if (_orginalMat != null) _orginalMat.Dispose();
_orginalMat = new Mat(path, ImreadModes.Grayscale);
var bitmapSource = (WriteableBitmap)BitmapSourceConverter.ToBitmapSource(_orginalMat);
bitmapSource.Freeze();
return bitmapSource;
//OriginalImage.Value = bitmapSource;
}
protected virtual void OnPropertyChanged(string name)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
// 加工画像の初期化
private void initializeModifiedImage()
{
if (_modifiedMat != null) _modifiedMat.Dispose();
_modifiedMat = null;
ModifiedImage.Value = null;
}
// 画像の保存
private string SavePicture()
{
// 保存用のパス生成
var dir = System.Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
dir = System.IO.Path.Combine(dir, System.DateTime.Now.ToString("yyyyMMdd"));
if (System.IO.Directory.Exists(dir) == false)
{
System.IO.Directory.CreateDirectory(dir);
}
var f = System.IO.Path.GetFileNameWithoutExtension(_originalImageFilePath);
var path = System.IO.Path.Combine(dir, (f + ".png"));
// 保存処理
using (System.IO.FileStream stream = new System.IO.FileStream(path, System.IO.FileMode.Create))
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(ModifiedImage.Value));
encoder.Save(stream);
}
return path;
}
// フィルター処理
private WriteableBitmap filters()
{
double[,] kernel = { { 0.0, 1.0/16.0, 0.0},
{ 1.0/16.0, 12.0/16.0, 1.0/16.0},
{ 0.0, 1.0/16.0, 0.0},
};
_modifiedMat = _orginalMat.Clone();
// ぼかし処理
for(int x=0; x<BlurNumberOfTimes.Value; ++x)
{
Cv2.Filter2D(_modifiedMat, _modifiedMat, -1, InputArray.Create(kernel));
}
// ラプラシアンフィルタ
Mat edge = _modifiedMat.Clone();
// 奇数の場合のみ処理
if (LaplacianKsize.Value % 2 == 1)
{
Cv2.Laplacian(_modifiedMat, edge, MatType.CV_8UC1, LaplacianKsize.Value);
// ノンローカルミーンフィルタ
Cv2.FastNlMeansDenoising(_modifiedMat, _modifiedMat, (float)NonLocalMeanH.Value);
/*
Cv2.Laplacian(_modifiedMat, edge, MatType.CV_64FC1, LaplacianKsize.Value);
// 8bitに変換
edge = edge * 255;
edge.ConvertTo(edge, MatType.CV_8UC1);
*/
// 減算
_modifiedMat = _modifiedMat - edge;
// ノンローカルミーンフィルタ
Cv2.FastNlMeansDenoising(_modifiedMat, _modifiedMat, (float)NonLocalMeanH.Value);
}
else
{
// ノンローカルミーンフィルタ
Cv2.FastNlMeansDenoising(_modifiedMat, _modifiedMat, (float)NonLocalMeanH.Value);
}
// アンシャープマスキングフィルタ
double k = (double)UnsharpMaskingK.Value / 10.0;
double[,] unsharpKernel = { { -k/9.0, -k/9.0, -k/9.0},
{ -k/9.0, 1.0+8.0*k/9.0, -k/9.0},
{ -k/9.0, -k/9.0, -k/9.0},
};
if (UnsharpMaskingK.Value > 0)
{
Cv2.Filter2D(_modifiedMat, _modifiedMat, -1, InputArray.Create(unsharpKernel));
}
// Mat => WritableBitmap 変換
//var b = (WriteableBitmap)BitmapSourceConverter.ToBitmapSource(_modifiedMat);
var b = WriteableBitmapConverter.ToWriteableBitmap(_modifiedMat);
b.Freeze();
return b;
}
private string getSettingJsonPath()
{
var path = System.Reflection.Assembly.GetEntryAssembly().Location;
var dir = System.IO.Path.GetDirectoryName(path);
var settingJsonPath = System.IO.Path.Combine(dir, "setting.json");
return settingJsonPath;
}
public MainWindowViewModel()
{
PropertyChanged += (o, e) => {};
// ドラッグアンドドロップ
PreviewDropCommand.Subscribe(async e => {
if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
var path = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
if (System.IO.File.Exists(path) == false) return;
var ext = System.IO.Path.GetExtension(path).ToUpper();
var validExtensions = new List<string> {".PNG", ".JPEG", ".JPG", ".BMP"};
if (validExtensions.Contains(ext) == false) return;
FilterEnabled.Value = false;
var b = await Task.Run(() => LoadPicture(path));
_originalImageFilePath = path;
OriginalImage.Value = b;
FilterEnabled.Value = true;
});
// フィルター処理
FilterCommand.Subscribe(async _ => {
// ボタン類の無効化
FilterEnabled.Value = false;
initializeModifiedImage();
// フィルター処理
var b = await Task.Run(() => filters());
ModifiedImage.Value = b;
// ボタン類の有効化
FilterEnabled.Value = true;
});
// クリア処理
ClearCommand.Subscribe(_=>{
initializeModifiedImage();
});
// 画像の保存処理
SaveCommand.Subscribe(async _=>{
if (_originalImageFilePath == "") return;
if (ModifiedImage.Value == null) return;
// ボタン類の無効化
FilterEnabled.Value = false;
var s = await Task.Run(() => SavePicture());
// ボタン類の有効化
FilterEnabled.Value = true;
});
// ロード時の処理
LoadedCommand.Subscribe(_ => {
var settingJsonPath = getSettingJsonPath();
if (System.IO.File.Exists(settingJsonPath) == false) return;
string jsonStr = "";
var encoding = System.Text.Encoding.GetEncoding("utf-8");
using(var reader = new System.IO.StreamReader(settingJsonPath, encoding))
{
jsonStr = reader.ReadToEnd();
}
var settingInfo = System.Text.Json.JsonSerializer.Deserialize<SettingInfo>(jsonStr);
BlurNumberOfTimes.Value = settingInfo.BlurNumberOfTimes;
LaplacianKsize.Value = settingInfo.LaplacianKsize;
NonLocalMeanH.Value = settingInfo.NonLocalMeanH;
UnsharpMaskingK.Value = settingInfo.UnsharpMaskingK;
});
// クローズ時の処理
ClosingCommand.Subscribe(_ => {
var settingJsonPath = getSettingJsonPath();
var settingInfo = new SettingInfo{
BlurNumberOfTimes = BlurNumberOfTimes.Value,
LaplacianKsize = LaplacianKsize.Value,
NonLocalMeanH = NonLocalMeanH.Value,
UnsharpMaskingK = UnsharpMaskingK.Value,
};
var jsonStr = System.Text.Json.JsonSerializer.Serialize(settingInfo);
var encoding = System.Text.Encoding.GetEncoding("utf-8");
using(var writer = new System.IO.StreamWriter(settingJsonPath, false, encoding))
{
writer.WriteLine(jsonStr);
}
});
}
}
}
ファイル名:SettingInfo.cs
namespace WpfSample15OpenCVFilter
{
public class SettingInfo
{
public int BlurNumberOfTimes{ get; set; }
public int NonLocalMeanH{ get; set; }
public int LaplacianKsize{ get; set; }
public int UnsharpMaskingK{ get; set; }
}
}
他のソースファイルに変更なし。
使い方
操作方法は以前の記事のサンプルと同じになります。
ClosingのイベントでフィルタのパラメータをJSON形式で保存します。
次回起動時Loadedイベントで保存したJSONファイルを読み込んでフィルタパラメータをセットしています。
JSONのシリアライズ用にSettingInfoクラスを用意してみましたが、もっとシンプルな方法がありそうです。
WPFやReactivePropertyについて、もっと深く学ぶ必要がありそうです。
C#でWPF学習中「OpenCVSharp」
PythonでOpenCVを使った画像加工をしているのですが、好みの画像となる設定を探すため、フィルターに引き渡す値の調整をし何度もスクリプトを実行しています。スクリプトだと、その調整作業が面倒なのでWPFで簡単なGUIを作ってみました。実...
次回起動時Loadedイベントで保存したJSONファイルを読み込んでフィルタパラメータをセットしています。
JSONのシリアライズ用にSettingInfoクラスを用意してみましたが、もっとシンプルな方法がありそうです。
WPFやReactivePropertyについて、もっと深く学ぶ必要がありそうです。
右側の画像領域でもドラッグアンドドロップを受けい入れるようにしてみました。
拡大時のスクロール領域処理修正。
2倍以上に拡大した状態でのフィルタ処理を実行するとスクロール位置がおかしくなる不具合有
コメント