GUIアプリからOpenCVの画像加工系のフィルターを実行するGUIアプリのプロトタイプを作成してみました。
対応フィルターは少ないですが、アプリとして使えそうなら後から追加する予定です。
ソースコード
ファイル名:GFilterUITemp01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
</ItemGroup>
</Project>
ファイル名:MainWindow.xaml.cs
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using NxLib.Helper;
namespace GFilterUITemp01;
public partial class MainWindow : System.Windows.Window
{
Mat? _srcMat;
Mat? _dstMat;
bool _isSrcView = false;
public MainWindow()
{
InitializeComponent();
// 画像拡大
Img1.OnWheelZoom();
// D&D
Wiring.AcceptFiles(this, files =>
{
var bmp = BmpSrc.FromFile(files[0]);
if (bmp is null) return;
_srcMat?.Dispose();
_srcMat = bmp.ToMat();
_dstMat?.Dispose();
_dstMat = null;
Img1.Source = bmp;
_isSrcView = true;
StatusBarItem1.Content = $"D&D:{files[0]}";
});
// Ctrl+C = Copy
Wiring.Hotkey(this, Key.C, ModifierKeys.Control,
() =>
{
if (Img1.Source is BitmapSource bmp)
{
ClipBD.SetPNG((BitmapSource)Img1.Source);
StatusBarItem1.Content = $"クリップボードへコピー";
}
},
() => Img1.Source is not null);
// Ctrl+V = Paste
Wiring.Hotkey(this, Key.V, ModifierKeys.Control,
() =>
{
var bmp = ClipBD.GetPNG();
if (bmp is null) return;
_srcMat?.Dispose();
_srcMat = bmp.ToMat();
_dstMat?.Dispose();
_dstMat = null;
Img1.Source = bmp;
_isSrcView = true;
StatusBarItem1.Content = $"クリップボードからペースト";
},
() =>
{
return true;
});
// ClosedイベントでDisposeする
this.Closed += OnClosed;
// Imageのクリックで表示画像の切り替え
Img1.OnLeftClick(_ =>
{
if (_srcMat is null) return;
if (_isSrcView)
{
// フィルタ処理
if (_dstMat is null)
{
string tag = combobox1.SelectedValue.ToString() ?? "";
switch (tag)
{
case "Gray":
_dstMat = Cv2Ex.ToGray(_srcMat);
break;
case "InvertGray":
_dstMat = Cv2Ex.InvertGray(_srcMat);
break;
case "Gaussian":
int n = int.Parse(GausianKernel.SelectedValue.ToString() ?? "15");
_dstMat = Cv2Ex.GaussianBlur(_srcMat, new OpenCvSharp.Size(n, n));
break;
default:
_dstMat = Cv2Ex.ToGray(_srcMat);
break;
}
}
Img1.Source = _dstMat.ToBitmapSource();
StatusBarItem1.Content = "加工後画像を表示中";
}
else
{
Img1.Source = _srcMat.ToBitmapSource();
StatusBarItem1.Content = "元画像を表示中";
}
_isSrcView = !_isSrcView;
});
}
private void OnClosed(object? sender, EventArgs e)
{
// nullチェックしてDispose
_srcMat?.Dispose();
_dstMat?.Dispose();
}
private void FilterReset(object? sender, SelectionChangedEventArgs e)
{
if (sender is null) return;
_dstMat?.Dispose();
_dstMat = null;
}
}
ファイル名:Helpers\BmpSrc.cs
// ビットマップソース
using System.IO;
using System.Windows.Media.Imaging;
namespace NxLib.Helper;
public static class BmpSrc
{
// ファイルパスから読み込み(ロックしない/Freeze 済み)
public static BitmapSource FromFile(string path, int? decodeW = null, int? decodeH = null)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
return FromStream(stream, decodeW, decodeH);
}
// ストリームから読み込み(必要なら)
public static BitmapSource FromStream(Stream stream, int? decodeW = null, int? decodeH = null)
{
var bi = new BitmapImage();
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad; // EndInit後にstreamを閉じられる
bi.StreamSource = stream;
if (decodeW.HasValue) bi.DecodePixelWidth = decodeW.Value;
if (decodeH.HasValue) bi.DecodePixelHeight = decodeH.Value;
bi.EndInit();
bi.Freeze();
return bi;
}
// 例外を投げたくない場合用
public static bool TryFromFile(string path, out BitmapSource? bmp, int? decodeW = null, int? decodeH = null)
{
try { bmp = FromFile(path, decodeW, decodeH); return true; }
catch { bmp = null; return false; }
}
}
ファイル名:Helpers\ClipBD.cs
// クリップボード
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;
namespace NxLib.Helper;
public static class ClipBD
{
public static void SetPNG(BitmapSource bmp)
{
var pngEnc = new PngBitmapEncoder();
pngEnc.Frames.Add(BitmapFrame.Create(bmp));
using var mem = new MemoryStream();
pngEnc.Save(mem);
Clipboard.SetData("PNG", mem);
}
public static BitmapSource? GetPNG()
{
var obj = (System.IO.MemoryStream)Clipboard.GetData("PNG");
if (obj is null)
{
var bmp = Clipboard.GetImage();
return bmp;
}
var bi = new BitmapImage();
bi.BeginInit();
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.StreamSource = obj;
bi.EndInit();
bi.Freeze();
return bi;
}
}
ファイル名:Helpers\Cv2Ex.cs
using OpenCvSharp;
namespace NxLib.Helper;
public static class Cv2Ex
{
public static Mat GaussianBlur(Mat src, OpenCvSharp.Size ksize)
{
Mat dst = new Mat();
Cv2.GaussianBlur(src, dst, ksize, 0);
return dst;
}
// グレースケール化
public static Mat ToGray(Mat src)
{
if (src.Empty()) return new Mat();
Mat dst = new Mat();
switch (src.Channels())
{
case 1:
dst?.Dispose();
return src.Clone();
case 3:
Cv2.CvtColor(src, dst, ColorConversionCodes.BGR2GRAY);
break;
case 4:
Cv2.CvtColor(src, dst, ColorConversionCodes.BGRA2GRAY);
break;
default:
throw new ArgumentException("Unsupported number of channels: " + src.Channels());
}
return dst;
}
// グレスケールを反転
public static Mat InvertGray(Mat src)
{
if (src.Empty()) return new Mat();
Mat gray = ToGray(src);
Mat dst = new Mat(gray.Size(), gray.Type());
for (int y = 0; y < gray.Rows; y++)
{
for (int x = 0; x < gray.Cols; x++)
{
byte value = gray.At<byte>(y, x);
dst.Set(y, x, (byte)(255 - value));
}
}
gray.Dispose();
return dst;
}
}
ファイル名:Helpers\Wiring.cs
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace NxLib.Helper;
public static class Wiring
{
// D&D: 指定拡張子だけ受け付ける(exts 省略可)
public static void AcceptFiles(FrameworkElement el, Action<string[]> onFiles, params string[] exts)
{
el.AllowDrop = true;
el.Drop += (_, e) =>
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
var files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
if (exts is { Length: > 0 })
files = files
.Where(f => exts.Any(x => f.EndsWith(x, StringComparison.OrdinalIgnoreCase)))
.ToArray();
if (files.Length > 0)
onFiles(files);
};
}
// ホットキー登録
public static void Hotkey(Window w, Key key, ModifierKeys mods, Action action, Func<bool>? canExecute = null)
{
var cmd = new RoutedUICommand();
ExecutedRoutedEventHandler exec = (_, __) => action();
CanExecuteRoutedEventHandler can = (_, e) => e.CanExecute = canExecute?.Invoke() ?? true;
var cb = new CommandBinding(cmd, exec, can);
var kb = new KeyBinding(cmd, key, mods);
w.CommandBindings.Add(cb);
w.InputBindings.Add(kb);
}
// ホイールで拡大するイベントを追加する拡張メソッド
public static T OnWheelZoom<T>(this T el, bool consume = true)
where T : FrameworkElement
{
el.PreviewMouseWheel += (_, e) =>
{
// Ctrlキーが押されている場合のみ拡大縮小
if ((Keyboard.Modifiers & ModifierKeys.Control) == 0)
return;
// 現在の拡大率を取得
ScaleTransform? st = el.LayoutTransform as ScaleTransform;
if (st is null)
st = new ScaleTransform(1.0, 1.0);
double scale = st.ScaleX;
// ホイールの方向で拡大縮小
if (e.Delta > 0)
scale *= 1.1; // 拡大
else
scale /= 1.1; // 縮小
// 範囲制限
scale = Math.Max(1.0, Math.Min(8.0, scale));
// 拡大率を設定
st.ScaleX = scale;
st.ScaleY = scale;
el.LayoutTransform = st;
// スクロールイベントを親に伝えない
if (consume) e.Handled = true;
};
return el;
}
// 左クリックで発火
public static T OnLeftClick<T>(this T el, Action<Point> onClick, bool consume = true)
where T : FrameworkElement
{
el.MouseLeftButtonUp += (_, e) =>
{
onClick(e.GetPosition(el));
if (consume) e.Handled = true;
};
return el;
}
}
ファイル名:MainWindow.xaml
<Window x:Class="GFilterUITemp01.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:GFilterUITemp01"
mc:Ignorable="d"
AllowDrop="True"
Title="MainWindow" Height="450" Width="800">
<Grid>
<DockPanel>
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem
x:Name="StatusBarItem1"
Content="ステータス" />
</StatusBar>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Image x:Name="Img1"
Stretch="None"
SnapsToDevicePixels="True"
RenderOptions.BitmapScalingMode="NearestNeighbor" />
</ScrollViewer>
<GridSplitter
Grid.Column="1"
HorizontalAlignment="Stretch" />
<StackPanel
x:Name="RightPanel"
Margin="6"
Grid.Column="2">
<ComboBox
x:Name = "combobox1"
SelectionChanged="FilterReset"
SelectedValuePath="Tag"
SelectedValue = "Gray">
<ComboBoxItem Content="グレイスケール化" Tag="Gray" />
<ComboBoxItem Content="グレイ反転" Tag="InvertGray" />
<ComboBoxItem Content="ガウシアンフィルタ" Tag="Gaussian" />
</ComboBox>
<Expander Header="ガウシアンフィルタ設定"
IsExpanded="False"
Margin="0,6,0,0">
<StackPanel>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center"
Margin="30,6,0,0">
<TextBlock Text="カーネルサイズ:" />
<ComboBox
x:Name = "GausianKernel"
SelectedValuePath="Tag"
SelectionChanged="FilterReset"
Margin="6,0,0,0"
SelectedValue = "15">
<ComboBoxItem Content="15x15" Tag="15" />
<ComboBoxItem Content="13x13" Tag="13" />
<ComboBoxItem Content="11x11" Tag="11" />
<ComboBoxItem Content="9x9" Tag="9" />
<ComboBoxItem Content="7x7" Tag="7" />
<ComboBoxItem Content="5x5" Tag="5" />
<ComboBoxItem Content="3x3" Tag="3" />
</ComboBox>
</StackPanel>
</StackPanel>
</Expander>
</StackPanel>
</Grid>
</DockPanel>
</Grid>
</Window>
実行イメージ
・起動

・画像ファイルををD&D

・画像をクリックでフィルター実行→もう一度クリックで元画像に戻る
その他機能
- Ctrl + マウスホイール画像拡大
- Ctr + C表示されている画像をクリップボードへコピー
- Ctr + Vクリップボードから画像を貼り付け

コメント