效果如下:
重点是Mvvm实现,而不是介绍图像的处理方法。
页面布局:Grid布局,两列。
第二列放一个StackPanel 里面放一个 输入图像控件和输出图像控件。
在viewModel中定义两个属性,供绑定
#region 输入图像
private string _srcImagePath;
public string SrcImagePath
{
get => _srcImagePath;
set
{
_srcImagePath = value;
OnPropertyChanged();
}
}
#endregion 输入图像
#region 输出图像
private Mat _dstMat;
public Mat DstMat
{
get => _dstMat;
set
{
_dstMat = value;
OnPropertyChanged();
}
}
#endregion 输出图像
xaml中绑定
<!--#region 第二列-->
<Border Grid.Column="1" BorderBrush="#11aabd" BorderThickness="1,0,0,0" />
<StackPanel Grid.Column="1" Orientation="Vertical">
<local:SrcImgShowUserControl x:Name="srcImageUC" SrcImageSource="{Binding SrcImagePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Border Height="2" BorderThickness="0,0,0,1" BorderBrush="#11aabd" />
<local:DstImgShowUserControl x:Name="dstImageUC" Margin="0,1,0,2" DstImageSource="{Binding DstMat, Mode=TwoWay}" />
</StackPanel>
<!--#endregion-->
其中的Border是控件中间的分割线。
第一列的按钮操作部分,分为三类,是根据图像处理方法输入参数来分类的。
第一类 无其他参数输入
即方法只有一个输入参数为输入图像,例如下方法
/// <summary>
/// CvtColor颜色空间转Gray
/// </summary>
/// <param name="srcImg"></param>
/// <returns></returns>
public static Mat CvtColorToGray(Mat srcImg)
{
Mat dstImg = new Mat();
Cv2.CvtColor(srcImg, dstImg, ColorConversionCodes.BGR2GRAY);
return dstImg;
}
/// <summary>
/// CvtColor颜色空间转HSV
/// </summary>
/// <param name="srcImg"></param>
/// <returns></returns>
public static Mat CvtColorToHSV(Mat srcImg)
{
Mat dstImg = new Mat();
Cv2.CvtColor(srcImg, dstImg, ColorConversionCodes.BGR2HSV);
return dstImg;
}
自己进行了一些简单的封装。
viewmodel中定义属性
public ObservableCollection<ImgOperateNoneParam> OperationsNoneParam { get; } = new();
ImgOperateNoneParam类:
/// <summary>
// 图像操作 无其他参数
/// </summary>
public class ImgOperateNoneParam
{
public string DisplayName { get; }
public ImgOpNoneParamEnum OperationType { get; }
public Func<Mat, Mat> Func { get; }
public ImgOperateNoneParam(string displayName, ImgOpNoneParamEnum operationType, Func<Mat, Mat> func)
{
DisplayName = displayName;
OperationType = operationType;
Func = func;
}
}
/// <summary>
/// 图像单操作 无参数
/// </summary>
public enum ImgOpNoneParamEnum
{
/// <summary>
/// 转为灰度图像
/// </summary>
CvtColorToGray,
CvtColorToHSV,
CvtColorToLab,
CvtColorToRGB,
Otsu,
...
}
其中ImgOpNoneParamEnum 为每个方法对应一个枚举,用来界面按钮的CommandParameter绑定,就不需要定义对个按钮了,直接循环处理即可。
初始化方案集合
private void InitImgOpNoneParam()
{
OperationsNoneParam.Add(new ImgOperateNoneParam("灰度图像", ImgOpNoneParamEnum.CvtColorToGray, ImageOperateMethods.CvtColorToGray));
OperationsNoneParam.Add(new ImgOperateNoneParam("Lab空间", ImgOpNoneParamEnum.CvtColorToLab, ImageOperateMethods.CvtColorToLab));
OperationsNoneParam.Add(new ImgOperateNoneParam("HSV空间", ImgOpNoneParamEnum.CvtColorToHSV, ImageOperateMethods.CvtColorToHSV));
OperationsNoneParam.Add(new ImgOperateNoneParam("RGB图像", ImgOpNoneParamEnum.CvtColorToRGB, ImageOperateMethods.CvtColorToRGB));
OperationsNoneParam.Add(new ImgOperateNoneParam("直方图均衡化", ImgOpNoneParamEnum.EqualizeHist, ImageOperateMethods.EqualizeHist));
OperationsNoneParam.Add(new ImgOperateNoneParam("图像取反", ImgOpNoneParamEnum.Negation, ImageOperateMethods.Negation));
OperationsNoneParam.Add(new ImgOperateNoneParam("Otsu二值化", ImgOpNoneParamEnum.Otsu, ImageOperateMethods.Otsu));
......
}
命令处理
public ICommand NoneParamOpCommand { get; }
/// <summary>
/// 图像处理 无参数方法
/// </summary>
/// <param name="operation"></param>
private void ProcessImageNoneParam(ImgOpNoneParamEnum operation)
{
if (string.IsNullOrEmpty(_srcImagePath))
{
MessageBox.Show("未选择输入图像");
return;
}
if (File.Exists(_srcImagePath) == false)
{
MessageBox.Show($"图像:{_srcImagePath}不存在");
return;
}
Mat srcImg = new Mat(_srcImagePath);
ImgOperateNoneParam singalOperate = OperationsNoneParam.First(x => x.OperationType == operation);
if (singalOperate == null)
{
MessageBox.Show($"方法:{operation}未注册");
return;
}
DstMat = singalOperate.Func(srcImg);
}
xmal绑定 直接使用 ItemsControl即可
<!--#region 无参数方法-->
<ItemsControl Margin="40,20,0,0" ItemsSource="{Binding OperationsNoneParam}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button
Width="140"
Style="{StaticResource ImgOperateBtnStyle}"
Command="{Binding DataContext.NoneParamOpCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
Content="{Binding DisplayName}"
FontSize="16"
CommandParameter="{Binding OperationType}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--#endregion-->
第二类 一个参数输入 int/double 类型参数
方法例如
/// <summary>
/// 图像添加指定数量的盐噪声点
/// </summary>
/// <param name="srcImg"></param>
/// <param name="num"></param>
/// <returns></returns>
public static Mat AddSaltNosie(Mat srcImg, int num)
{
Mat dstImg = srcImg.Clone();
Random r = new Random();
for (int i = 0; i < num; i++)
{
int x = r.Next(srcImg.Rows);
int y = r.Next(srcImg.Cols);
dstImg.Set(x, y, new OpenCvSharp.Vec3b(255, 255, 255));
}
return dstImg;
}
/// <summary>
/// 图像添加指定数量的椒噪声点
/// </summary>
/// <param name="srcImg"></param>
/// <param name="num"></param>
/// <returns></returns>
public static Mat AddPepperNoise(Mat srcImg, int num)
{
Mat dstImg = srcImg.Clone();
Random r = new Random();
for (int i = 0; i < num; i++)
{
int x = r.Next(srcImg.Rows);
int y = r.Next(srcImg.Cols);
dstImg.Set(x, y, new OpenCvSharp.Vec3b(0, 0, 0));
}
return dstImg;
}
/// <summary>
/// 图像添加指定数量的椒盐噪声点
/// </summary>
/// <param name="srcImg"></param>
/// <param name="num"></param>
/// <returns></returns>
public static Mat AddSaltPepperNoise(Mat srcImg, int num)
{
OpenCvSharp.Vec3b[] SaltPepperNoises = new OpenCvSharp.Vec3b[2]
{ new OpenCvSharp.Vec3b(0, 0, 0), // 椒噪声
new OpenCvSharp.Vec3b(255, 255, 255) // 盐噪声
};
Mat dstImg = srcImg.Clone();
Random r = new Random();
for (int i = 0; i < num; i++)
{
int x = r.Next(srcImg.Rows);
int y = r.Next(srcImg.Cols);
int index = r.Next(0, 2);
Vec3b nosie = SaltPepperNoises[index];
dstImg.Set(x, y, nosie);
}
return dstImg;
}
....
viewmodel中类似于第一类无参数的定义
/// <summary>
/// 图像操作集合 一个参数 数值类
/// </summary>
public ObservableCollection<ImgOpOneParamNumBase> OperationsOneParamNum { get; } = new();
类定义
public class ImgOperateOneParamNum<T> : ImgOpOneParamNumBase where T : INumber<T>
{
public ImgOperateOneParamNum(string buttonText, string labelText, ImgOpOneParamNumEnum operationType, Func<Mat, T, Mat> func, string toolTips = "", string defeatValue = "0")
: base()
{
ButtonText = buttonText;
LabelText = labelText;
Func = func;
OperationType = operationType;
InputParam = defeatValue;
ParamToolText = toolTips;
}
public Func<Mat, T, Mat> Func { get; }
public override bool CheckParamFormat()
{
if (string.IsNullOrWhiteSpace(InputParam))
{
InputParam = "0";
return true;
}
if (typeof(T) == typeof(int))
{
if (int.TryParse(InputParam, out int intValue))
{
return intValue >= 0;
}
}
else if (typeof(T) == typeof(double))
{
if (double.TryParse(InputParam, out double doubleValue))
{
return doubleValue >= 0;
}
}
else if (typeof(T) == typeof(float))
{
if (float.TryParse(InputParam, out float floatValue))
{
return floatValue >= 0;
}
}
return false;
}
public override Mat Execute(Mat srcImg)
{
T para = ConvertInputValue(InputParam);
return Func(srcImg, para);
}
private T ConvertInputValue(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return T.Zero;
}
if (typeof(T) == typeof(int))
{
if (int.TryParse(input, out int intValue))
return (T)(object)intValue;
}
else if (typeof(T) == typeof(double))
{
if (double.TryParse(input, out double doubleValue))
return (T)(object)doubleValue;
}
else if (typeof(T) == typeof(float))
{
if (float.TryParse(input, out float floatValue))
return (T)(object)floatValue;
}
throw new ArgumentException($"无法将 '{input}' 转换为 {typeof(T).Name}");
}
}
/// <summary>
/// 图像单操作 一个参数 数字
/// </summary>
public enum ImgOpOneParamNumEnum
{
/// <summary>
/// 素描
/// </summary>
Sketch,
/// <summary>
/// 浮雕
/// </summary>
Emboss,
/// <summary>
/// 快速去雾
/// </summary>
FastDehazing,
/// <summary>
/// 毛玻璃
/// </summary>
GroundGlass,
/// <summary>
/// 透明度
/// </summary>
Lucency,
/// <summary>
/// 加盐噪声
/// </summary>
AddSaltNoise,
...
}
基类 同时供第三类方法使用
public abstract class ImgOpOneParamNumBase : ViewModelBase
{
public string ButtonText { get; protected set; }
public string LabelText { get; protected set; }
public ImgOpOneParamNumEnum OperationType { get; protected set; }
public string ParamToolText { get; protected set; }
private string _inputParam;
public string InputParam
{
get => _inputParam;
set
{
_inputParam = value;
OnPropertyChanged();
}
}
/// <summary>
/// 执行方法
/// </summary>
/// <param name="srcImg"></param>
/// <returns></returns>
public abstract Mat Execute(Mat srcImg);
/// <summary>
/// 检查格式是否正确
/// </summary>
/// <returns></returns>
public abstract bool CheckParamFormat();
private string[] _itemsArray = Array.Empty<string>();
public string[] ItemsArray
{
get => _itemsArray;
set
{
_itemsArray = value;
OnPropertyChanged();
}
}
}
集合初始化和命令
private void InitImgOpOneParamNum()
{
OperationsOneParamNum.Add(new ImgOperateOneParamNum<int>(
"素描", "像素值",
ImgOpOneParamNumEnum.Sketch, ImageOperateMethods.Sketch,
"Int类型", "128"));
OperationsOneParamNum.Add(new ImgOperateOneParamNum<int>(
"浮雕", "像素值",
ImgOpOneParamNumEnum.Emboss, ImageOperateMethods.Emboss,
"", "128"));
OperationsOneParamNum.Add(new ImgOperateOneParamNum<int>(
"毛玻璃", "邻域大小",
ImgOpOneParamNumEnum.GroundGlass, ImageOperateMethods.GroundGlass,
"int类型,奇数,大于3", "5"));
OperationsOneParamNum.Add(new ImgOperateOneParamNum<int>(
"加盐噪声", "噪声点数",
ImgOpOneParamNumEnum.AddSaltNoise, ImageOperateMethods.AddSaltNosie,
"", "2500"));
OperationsOneParamNum.Add(new ImgOperateOneParamNum<int>(
"加椒噪声", "噪声点数",
ImgOpOneParamNumEnum.AddPepperNoise, ImageOperateMethods.AddPepperNoise,
"", "2500"));
OperationsOneParamNum.Add(new ImgOperateOneParamNum<int>(
"加椒盐噪声", "噪声点数",
ImgOpOneParamNumEnum.AddSaltPepperNoise, ImageOperateMethods.AddSaltPepperNoise,
"", "2500"));
.....
}
public ICommand OneParamNumOpCommand { get; }
/// <summary>
/// 图像处理 一个参数方法 数值类
/// </summary>
/// <param name="operation"></param>
private void ProcessImageOneParamNum(ImgOpOneParamNumEnum operation)
{
if (string.IsNullOrEmpty(_srcImagePath))
{
MessageBox.Show("未选择输入图像");
return;
}
if (File.Exists(_srcImagePath) == false)
{
MessageBox.Show($"图像:{_srcImagePath}不存在");
return;
}
Mat srcImg = new Mat(_srcImagePath);
ImgOpOneParamNumBase operate = OperationsOneParamNum.First(x => x.OperationType == operation);
if (operate == null)
{
MessageBox.Show($"方法:{operation}未注册");
return;
}
if (operate.CheckParamFormat() == false)
{
MessageBox.Show($"参数:{operate.LabelText} 格式错误");
return;
}
DstMat = operate.Execute(srcImg);
}
xaml绑定
<!--#region 一个参数 int 或 double-->
<ScrollViewer Grid.Row="1" MaxHeight="540" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding OperationsOneParamNum}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#11aabd" BorderThickness="1,1,1,1">
<StackPanel Orientation="Horizontal">
<TextBlock
Grid.Column="0"
Width="90"
Margin="10,0,10,0"
VerticalAlignment="Center"
FontSize="16"
Foreground="White"
TextAlignment="Center"
Text="{Binding LabelText}" />
<TextBox
Grid.Column="1"
Width="50"
Margin="0,0,10,0"
Padding="5,2"
VerticalAlignment="Center"
TextAlignment="Center"
ToolTip="{Binding ParamToolText, Converter={StaticResource emptyStringToNullConverter}}"
Text="{Binding InputParam, UpdateSourceTrigger=PropertyChanged}" />
<Button
Grid.Column="2"
Width="120"
Height="30"
Margin="0,5,10,5"
VerticalAlignment="Center"
FontSize="16"
Style="{StaticResource ImgOperateBtnStyle}"
Content="{Binding ButtonText}"
Command="{Binding DataContext.OneParamNumOpCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding OperationType}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<!--#endregion-->
第三类 一个参数输入 枚举 类型参数
方法如下
/// <summary>
/// 图像伪颜色加强
/// </summary>
/// <param name="srcImg"></param>
/// <param name="types"></param>
/// <returns></returns>
public static Mat ApplyColorMap(Mat srcImg, OpenCvSharp.ColormapTypes types)
{
Mat dstImg = new Mat();
Cv2.ApplyColorMap(srcImg, dstImg, types);
return dstImg;
}
/// <summary>
/// 图像向上或向下翻转半
/// </summary>
/// <param name="srcImg"></param>
/// <param name="upOrDown"></param>
/// <returns></returns>
public static Mat CompleteSymm(Mat srcImg, SymmDirection direction)
{
//转化成方阵
int ss = Math.Min(srcImg.Rows, srcImg.Cols);
OpenCvSharp.Size size = new OpenCvSharp.Size(ss, ss);
Cv2.Resize(srcImg, srcImg, size);
bool direct = direction == SymmDirection.LowerToUpper;
Cv2.CompleteSymm(srcImg, direct);
return srcImg;
}
viewmodel定义
/// <summary>
/// 图像操作集合 一个参数 枚举
/// </summary>
public ObservableCollection<ImgOpOneParamEnumBase> OperationsOneParamEnum { get; } = new();
private void InitImgOpOneParamEnum()
{
OperationsOneParamEnum.Add(new ImgOperateOneParamEnum<FlipMode>(
"反转", "方向",
ImgOpOneParamEnumEnum.Flip, ImageOperateMethods.Flip,
""));
OperationsOneParamEnum.Add(new ImgOperateOneParamEnum<ColormapTypes>(
"伪颜色增强", "方式",
ImgOpOneParamEnumEnum.ApplyColorMap, ImageOperateMethods.ApplyColorMap,
""));
OperationsOneParamEnum.Add(new ImgOperateOneParamEnum<EdegTypeEnum>(
"边缘检测", "检测算法",
ImgOpOneParamEnumEnum.Edge, ImageOperateMethods.Edge,
""));
OperationsOneParamEnum.Add(new ImgOperateOneParamEnum<ClassifierEnum>(
"特征检测", "分类器",
ImgOpOneParamEnumEnum.FeatureRecogn, ImageOperateMethods.FeatureRecogn,
""));
OperationsOneParamEnum.Add(new ImgOperateOneParamEnum<SkinDefectEnum>(
"皮肤检测", "算法",
ImgOpOneParamEnumEnum.SkinDefect, ImageOperateMethods.SkinDefect,
""));
OperationsOneParamEnum.Add(new ImgOperateOneParamEnum<SymmDirection>(
"斜对折", "方向",
ImgOpOneParamEnumEnum.CompleteSymm, ImageOperateMethods.CompleteSymm,
""));
OperationsOneParamEnum.Add(new ImgOperateOneParamEnum<BGREnum>(
"单通道显示", "通道",
ImgOpOneParamEnumEnum.BGRSingle, ImageOperateMethods.BGRSingle,
""));
}
public ICommand OneParamEnumOpCommand { get; }
/// <summary>
/// 图像处理 一个参数方法 数值类
/// </summary>
/// <param name="operation"></param>
private void ProcessImageOneParamEnum(ImgOpOneParamEnumEnum operation)
{
if (string.IsNullOrEmpty(_srcImagePath))
{
MessageBox.Show("未选择输入图像");
return;
}
if (File.Exists(_srcImagePath) == false)
{
MessageBox.Show($"图像:{_srcImagePath}不存在");
return;
}
Mat srcImg = new Mat(_srcImagePath);
ImgOpOneParamEnumBase operate = OperationsOneParamEnum.First(x => x.OperationType == operation);
if (operate == null)
{
MessageBox.Show($"方法:{operation}未注册");
return;
}
if (operate.CheckParamFormat() == false)
{
MessageBox.Show($"参数:{operate.LabelText} 格式错误");
return;
}
DstMat = operate.Execute(srcImg);
}
ImgOperateOneParamEnum类
public class ImgOperateOneParamEnum<T> : ImgOpOneParamEnumBase where T : Enum
{
public ImgOperateOneParamEnum(string buttonText, string labelText, ImgOpOneParamEnumEnum operationType, Func<Mat, T, Mat> func, string toolTips = "", string defeatValue = "0")
: base()
{
ButtonText = buttonText;
LabelText = labelText;
Func = func;
OperationType = operationType;
InputParam = defeatValue;
ParamToolText = toolTips;
ItemsArray = Enum.GetNames(typeof(T));
}
public Func<Mat, T, Mat> Func { get; }
public override bool CheckParamFormat()
{
int index = Array.IndexOf(ItemsArray, InputParam);
return index >= 0;
}
public override Mat Execute(Mat srcImg)
{
T para = ConvertInputValue(InputParam);
return Func(srcImg, para);
}
private T ConvertInputValue(string input)
{
if (Enum.TryParse(typeof(T), input, true, out object result))
{
return (T)result;
}
//throw new ArgumentException($"无法将 '{input}' 转换为 {typeof(T).Name}");
return default;
}
}
/// <summary>
/// 图像单操作 一个参数 枚举
/// </summary>
public enum ImgOpOneParamEnumEnum
{
Flip,
ApplyColorMap,
Edge,
/// <summary>
/// 特征检测
/// </summary>
FeatureRecogn,
/// <summary>
/// 皮肤检测
/// </summary>
SkinDefect,
/// <summary>
/// 图像斜折叠
/// </summary>
CompleteSymm,
BGRSingle,
}
xaml绑定
<ScrollViewer
Grid.Row="1"
MaxHeight="600"
Margin="20,0,0,0"
VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding OperationsOneParamEnum}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#11aabd" BorderThickness="1,1,1,1">
<StackPanel Orientation="Horizontal">
<TextBlock
Grid.Column="0"
Width="80"
Margin="10,0,10,0"
VerticalAlignment="Center"
FontSize="18"
Foreground="White"
TextAlignment="Center"
Text="{Binding LabelText}" />
<ComboBox
Grid.Column="1"
Width="150"
Height="25"
Margin="0,0,10,0"
Padding="5,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsEditable="False"
ItemsSource="{Binding ItemsArray}"
SelectedIndex="0"
Style="{StaticResource ParameterizedComboBoxStyle}"
Background="#495660"
BorderBrush="White"
Foreground="White"
ToolTip="{Binding ParamToolText, Converter={StaticResource emptyStringToNullConverter}}"
Text="{Binding InputParam, UpdateSourceTrigger=PropertyChanged}" />
<Button
Grid.Column="2"
Width="100"
Height="30"
Margin="0,5,10,5"
VerticalAlignment="Center"
FontSize="16"
Style="{StaticResource ImgOperateBtnStyle}"
Content="{Binding ButtonText}"
Command="{Binding DataContext.OneParamEnumOpCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding OperationType}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>