项目框架: WPF(.net 8.0) , Opencvsharp4.
核心方法就是 Cv2.FloodFill的实现,即选择一个点,然后算法会自动填充与该点颜色相似的所有相邻区域。
效果预览如下
xaml设计界面
viewmodel定义
#region 输入输出图像路径
private string _srcImgPath;
public string SrcImgPath
{
get => _srcImgPath;
set
{
_srcImgPath = value;
OnPropertyChanged();
}
}
private string _saveImagePath;
public string SaveImagePath
{
get => _saveImagePath;
set
{
_saveImagePath = value;
OnPropertyChanged();
}
}
#endregion 输入输出图像路径
#region 图像集合 用于保存每步操作 可撤销
private ObservableCollection<Mat> _matCollection = new ObservableCollection<Mat>();
public ObservableCollection<Mat> MatCollection => _matCollection;
private Mat _lastMat;
public Mat LastMat
{
get => _lastMat;
set
{
_lastMat = value;
OnPropertyChanged();
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
LastMat = _matCollection.LastOrDefault();
}
private void Push(Mat mat)
{
_matCollection.Add(mat);
}
private Mat Pop()
{
if (_matCollection.Count <= 1)
{
return null;
}
var last = _matCollection.Last();
_matCollection.RemoveAt(_matCollection.Count - 1);
return last;
}
#endregion 图像集合 用于保存每步操作 可撤销
private System.Windows.Media.Color _floodFillColor = Colors.Red;
public System.Windows.Media.Color FloodFillColor
{
get { return _floodFillColor; }
set
{
_floodFillColor = value;
OnPropertyChanged();
}
}
#region 正负差值
private int _downDiff = 0;
public int DownDiff
{
get => _downDiff;
set
{
_downDiff = value;
OnPropertyChanged();
}
}
private int _upDiff = 0;
public int UpDiff
{
get => _upDiff;
set
{
_upDiff = value;
OnPropertyChanged();
}
}
#endregion 正负差值
private bool _isCoordinateModeEnabled = false;
private CoordinateModel _currentCoordinate = new CoordinateModel();
private CoordinateModel _selectedCoordinate = new CoordinateModel();
private string _statusMessage;
public bool IsCoordinateModeEnabled
{
get => _isCoordinateModeEnabled;
set
{
_isCoordinateModeEnabled = value;
OnPropertyChanged();
// 更新状态信息
if (value)
{
StatusMessage = "坐标模式已启用 - 在图片上移动鼠标查看坐标,点击选择坐标";
}
else
{
StatusMessage = "坐标模式已禁用";
// 重置当前坐标显示
CurrentCoordinate.X = 0;
CurrentCoordinate.Y = 0;
}
}
}
public string StatusMessage
{
get => _statusMessage;
set
{
_statusMessage = value;
OnPropertyChanged();
}
}
public CoordinateModel CurrentCoordinate
{
get => _currentCoordinate;
set
{
_currentCoordinate = value;
OnPropertyChanged();
}
}
public CoordinateModel SelectedCoordinate
{
get => _selectedCoordinate;
set
{
_selectedCoordinate = value;
OnPropertyChanged();
}
}
需要注意几点。
撤销操作,我们第一时间想到的可能是用栈stack,但可能不太好实现mvvm,所以我就使用了ObservableCollection,同时实现了类似于stack的Push和Pop方法,并将集合的最后一个元素抽出来单独给xaml上的Image进行绑定。
水漫操作的填充颜色,颜色选择框使用的是Winform的ColorDialog,需要单独引用Winform库,其返回值类型是System.Drawing.Color,而CV2.FloodFill 使用的颜色是 Opevcvsharp.Scalar ,xaml上使用的Background是System.Windows.Media.Brush。所以我在vm中定义了一个 System.Windows.Media.Color 类型的属性,在选择颜色时可以通过 System.Windows.Media.Color.FromArgb 将 System.Drawing.Color转化成 System.Windows.Media.Color;在xaml绑定的时候可以使用 new SolidColorBrush,将System.Windows.Media.Color转换成 System.Windows.Media.Brush;在CV2.FloodFill的时候 通过 Scalar.FromRgb 可以将 System.Windows.Media.Color转化成 Opevcvsharp.Scalar。
CoordinateModel 类如下
public class CoordinateModel : ViewModelBase
{
private double _x;
private double _y;
private bool _isSelected;
public double X
{
get => _x;
set
{
_x = value;
OnPropertyChanged();
}
}
public double Y
{
get => _y;
set
{
_y = value;
OnPropertyChanged();
}
}
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
OnPropertyChanged();
}
}
}
同时注意坐标的转化,Image控件中的 Stretch="Fill",我们鼠标选中的坐标是相对于的Image控件的坐标,而不是图像的坐标,需要做个比例转化。 在viewmodel中定义以下方法
/// <summary>
/// 将控件坐标转换为图像坐标(Stretch="Fill")
/// </summary>
private System.Windows.Point ConvertToImageCoordinates(System.Windows.Point controlPosition, System.Windows.Size controlSize)
{
if (LastMat == null || controlSize.Width == 0 || controlSize.Height == 0)
return new System.Windows.Point(0, 0);
// 获取图像原始尺寸
double imageWidth = LastMat.Width;
double imageHeight = LastMat.Height;
// Stretch="Fill" 的简单计算
// 直接按比例映射
double imageX = (controlPosition.X / controlSize.Width) * imageWidth;
double imageY = (controlPosition.Y / controlSize.Height) * imageHeight;
// 确保坐标在图像范围内
imageX = Math.Max(0, Math.Min(imageX, imageWidth));
imageY = Math.Max(0, Math.Min(imageY, imageHeight));
return new System.Windows.Point(imageX, imageY);
}
// 处理鼠标移动
public void HandleMouseMove(System.Windows.Point position, System.Windows.Size imageSize)
{
if (!IsCoordinateModeEnabled || LastMat == null) return;
var imagePosition = ConvertToImageCoordinates(position, imageSize);
CurrentCoordinate.X = Math.Round(imagePosition.X, 1);
CurrentCoordinate.Y = Math.Round(imagePosition.Y, 1);
//StatusMessage = $"当前坐标: X={CurrentCoordinate.X}, Y={CurrentCoordinate.Y}";
StatusMessage = $"坐标模式开启中";
}
// 处理鼠标点击
public void HandleMouseClick(System.Windows.Point position, System.Windows.Size imageSize)
{
if (!IsCoordinateModeEnabled || LastMat == null) return;
var imagePosition = ConvertToImageCoordinates(position, imageSize);
SelectedCoordinate.X = Math.Round(imagePosition.X, 1);
SelectedCoordinate.Y = Math.Round(imagePosition.Y, 1);
SelectedCoordinate.IsSelected = true;
// StatusMessage = $"已选择坐标: X={SelectedCoordinate.X}, Y={SelectedCoordinate.Y}";
StatusMessage = $"已选择坐标";
// 可选:短暂显示选中效果
Task.Delay(1000).ContinueWith(_ =>
{
Application.Current.Dispatcher.Invoke(() =>
{
SelectedCoordinate.IsSelected = false;
});
});
}
同时需要在xaml.cs中将选中的坐标和Image控件的大小传给vm,代码如下
/// <summary>
/// FloodFillUserControl.xaml 的交互逻辑
/// </summary>
public partial class FloodFillUserControl : UserControl
{
private FloodFillViewModel _viewModel;
public FloodFillUserControl()
{
InitializeComponent();
_viewModel = new FloodFillViewModel();
this.DataContext = _viewModel;
TargetImage.Loaded += OnImageLoaded;
}
private void OnImageLoaded(object sender, RoutedEventArgs e)
{
var image = sender as Image;
// 移除旧的事件处理
image.MouseMove -= OnImageMouseMove;
image.MouseLeftButtonDown -= OnImageMouseClick;
// 添加新的事件处理
image.MouseMove += OnImageMouseMove;
image.MouseLeftButtonDown += OnImageMouseClick;
}
private void OnImageMouseMove(object sender, MouseEventArgs e)
{
if (_viewModel.IsCoordinateModeEnabled && _viewModel.LastMat != null)
{
Point position = e.GetPosition(TargetImage);
var imageSize = new Size(TargetImage.ActualWidth, TargetImage.ActualHeight);
_viewModel.HandleMouseMove(position, imageSize);
}
}
private void OnImageMouseClick(object sender, MouseButtonEventArgs e)
{
if (_viewModel.IsCoordinateModeEnabled && _viewModel.LastMat != null)
{
var position = e.GetPosition(TargetImage);
var controlSize = new Size(TargetImage.ActualWidth, TargetImage.ActualHeight);
_viewModel.HandleMouseClick(position, controlSize);
}
}
}
其余命令如下
#region 水漫命令
public ICommand FloodFillCommand { get; }
private void ProcessFloodFill()
{
if (LastMat == null)
{
MessageBox.Show("未选择图像");
return;
}
if (SelectedCoordinate.X == 0 || SelectedCoordinate.Y == 0)
{
MessageBox.Show("未选择坐标");
return;
}
Mat srcImg = LastMat.Clone();
Mat dstImg = new Mat();
// 水漫图像需要 BGR三通道图片
Cv2.CvtColor(srcImg, dstImg, ColorConversionCodes.BGRA2BGR);
OpenCvSharp.Rect re;
// 正负差最大值对应的颜色
Scalar loDiffColor = new OpenCvSharp.Scalar(_downDiff, _downDiff, _downDiff);
Scalar upDiffColor = new OpenCvSharp.Scalar(_upDiff, _upDiff, _upDiff);
Scalar fillScalar = Scalar.FromRgb(_floodFillColor.R, _floodFillColor.G, _floodFillColor.B);
OpenCvSharp.Point location = new OpenCvSharp.Point(_selectedCoordinate.X, _selectedCoordinate.Y);
Cv2.FloodFill(dstImg, location, fillScalar, out re, loDiffColor, upDiffColor, FloodFillFlags.Link8);
// 判断图像水漫前后是否有变化,有变化才加入栈
if (MatEquall(srcImg, dstImg) == false)
{
Push(dstImg);
}
}
// 判断两张多通道的图像是否相等
private bool MatEquall(Mat m1, Mat m2)
{
if (m1.Empty() && m2.Empty())
{
return true;
}
if (m1.Cols != m2.Cols || m1.Rows != m2.Rows || m1.Dims != m2.Dims)
{
return false;
}
if (m1.Size() != m2.Size() || m1.Channels() != m2.Channels() || m1.Type() != m2.Type())
{
return false;
}
if (m1.Total() * m1.ElemSize() != m2.Total() * m2.ElemSize())
{
return false;
}
Mat tempMat = new Mat();
Cv2.BitwiseXor(m1, m2, tempMat);
Mat[] mm = Cv2.Split(tempMat);
foreach (Mat mm1 in mm)
{
if (Cv2.CountNonZero(mm1) != 0)
return false;
}
return true;
}
#endregion 水漫命令
#region 撤销命令
public ICommand RevocationCommand { get; }
private void ProcessRevocation()
{
Pop();
IsCoordinateModeEnabled = false;
SelectedCoordinate.X = 0;
SelectedCoordinate.Y = 0;
SelectedCoordinate.IsSelected = false;
CurrentCoordinate.X = 0;
CurrentCoordinate.Y = 0;
}
#endregion 撤销命令
#region 复原命令
public ICommand RecoverCommand { get; }
private void Recover()
{
if (_matCollection.Count <= 0)
{
MessageBox.Show("未选择图像");
return;
}
Mat mat = _matCollection[0];
_matCollection.Clear();
_matCollection.Add(mat);
IsCoordinateModeEnabled = false;
SelectedCoordinate.X = 0;
SelectedCoordinate.Y = 0;
SelectedCoordinate.IsSelected = false;
CurrentCoordinate.X = 0;
CurrentCoordinate.Y = 0;
}
#endregion 复原命令