本示例是《OpenCV3编程入门》中6.5.4的示例程序的C# + EMGU 3.4.1版,和C++程序相比,有以下几点不同:
1. 使用winform实现,主要控件有:MenuStrip(用于显示程序操作说明),ImageBox(用于显示图片),RichTextBox(用于记录并显示程序操作过程),TrackBar(用于变更负差最大值和正差最大值)。
2. 使用KeyDown事件,实现键盘按键的实时监控,该事件绑定到Form1窗体上。注意:Form1窗体的KeyPreview属性值需要设置为True,原因如下:
问题:当新建一个窗体时,添加KeyDown事件后,会正常处理,但是当添加有控件,比如Button,TextBox时,则不会再触发窗体的KeyDown事件,也不会调用KeyDown事件的处理程序。
原因:由于窗体中添加了控件,于是焦点就在其中一个控件上,如果我们要处理的事件,窗体和获取焦点的控件都同时拥有,系统就会将键盘的操作键值直接传递给这个获取焦点的控件,就会出现上面的问题。
解决方案:此时需要将窗体的KeyPreview属性设置为true,将系统传入的键值先传递给窗体,再传递给控件。MSDN关于KeyPreview的说明:如果窗体将接收所有键事件,则为 true;如果窗体上当前选定控件接收键事件,则为 false,默认为 false。
3. 使用MouseDown事件,实现鼠标点击的实时监控,该事件绑定到ImageBox控件上,当鼠标点击ImageBox中图片的某一点时,将以该点为seed point,进行漫水填充。
4. EMGU的FloodFill函数原型如下,与OpenCv相比,EMGU中FloodFill函数只有一种形式,而OpenCv中有两种,分别为带掩膜mask和不带掩膜mask。
public static int FloodFill(IInputOutputArray src, IInputOutputArray mask, Point seedPoint, MCvScalar newVal, out Rectangle rect, MCvScalar loDiff, MCvScalar upDiff, Connectivity connectivity = Connectivity.FourConnected, FloodFillType flags = FloodFillType.Default);
5. FloodFill函数中rect参数类型为Rectangle,可通过Rectangle ccomp = new Rectangle();语句定义其实参,调用函数时,该实参前需要使用out关键字进行修饰,表示参数通过引用来传递。
6. 通过Canny边缘检测算法,生成掩膜mask,实现真正意义上的mask功能(原书示例中虽然有此部分代码,但并未实现mask功能的可视化比较确认,只是函数调用形式上的差异而已)。注意:Canny算法生成的OutputArray,其上下左右需要各扩展一行/列,以匹配mask的大小(rows和cols需要比原图各多2)。
通过以下两条语句生成掩膜:
//边缘检测后的输出将作为FloodFill函数中的掩膜,黑色区域可以被填充,白色区域则保留原来的颜色
CvInvoke.Canny(g_grayImage, g_cannyImage,10,240);
//边缘检测后的输出上下左右各扩展一行/列(否则会抛异常),以匹配g_maskImage的大小
CvInvoke.CopyMakeBorder(g_cannyImage, g_cannyImage, 1, 1, 1, 1, BorderType.Constant);
并将生成的g_cannyImage拷贝给g_maskImage:
g_cannyImage.CopyTo(g_maskImage);
程序使用的原始图片如下:
程序代码如下:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
namespace floodfill
{
public partial class Form1 : Form
{
//定义原始图、目标图、灰度图、掩膜图
Mat g_srcImage = new Mat();
Mat g_dstImage = new Mat();
Mat g_grayImage = new Mat();
Mat g_maskImage = new Mat();
Mat g_cannyImage = new Mat();
int g_nFillMode = 1; //漫水填充的模式
int g_nLowDifference = 20, g_nUpDifference = 20; //负差最大值、正差最大值
Connectivity g_nConnectivity = Connectivity.FourConnected; //表示floodFill函数的连通值
bool g_bIsColor = true; //是否为彩色的标识符布尔值
bool g_bUseMask = false; //是否显示掩膜窗口的布尔值
//int g_nNewMaskVal = 255; //新的重新绘制的像素值(OpenCv中该值用于设置flags的中间8位,而EMGU中无此设置方式)
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//载入源图
g_srcImage = CvInvoke.Imread("home.jpg", ImreadModes.Color);
if (g_srcImage.IsEmpty)
MessageBox.Show("读取图片错误!", "Warning");
g_srcImage.CopyTo(g_dstImage); //复制源图到目标图
CvInvoke.CvtColor(g_srcImage, g_grayImage, ColorConversion.Bgr2Gray); //三通道bgr图转换到灰度图
g_maskImage.Create(g_srcImage.Rows + 2, g_srcImage.Cols + 2, DepthType.Cv8U, 1); //利用源图的尺寸来初始化掩膜mask
//边缘检测后的输出将作为FloodFill函数中的掩膜,黑色区域可以被填充,白色区域则保留原来的颜色
CvInvoke.Canny(g_grayImage, g_cannyImage,10,240);
//边缘检测后的输出上下左右各扩展一行/列(否则会抛异常),以匹配g_maskImage的大小
CvInvoke.CopyMakeBorder(g_cannyImage, g_cannyImage, 1, 1, 1, 1, BorderType.Constant);
//显示TrackBar的初始值
lowDiffValueLable.Text = lowDiffTrackBar.Value.ToString();
upDiffValueLable.Text = upDiffTrackBar.Value.ToString();
//先显示源图
imageBox1.Image = g_bIsColor ? g_dstImage : g_grayImage;
}
//鼠标单击事件
private void imageBox1_MouseDown(object sender, MouseEventArgs e)
{
Point seed = new Point(e.X, e.Y); //获取当前坐标,作为种子点
//当为空范围的漫水填充时,此值设为0,否则设为全局的g_nLowDifference
int LowDifference = g_nFillMode == 0 ? 0 : g_nLowDifference;
//当为空范围的漫水填充时,此值设为0,否则设为全局的g_nUpDifference
int UpDifference = g_nFillMode == 0 ? 0 : g_nUpDifference;
//标识符flags,其值为0时,对应设置为default
FloodFillType flags = g_nFillMode == 1 ? FloodFillType.FixedRange : FloodFillType.Default;
//随机生成bgr值,使用Guid类是为了得到“真”随机数
int b = new Random(Guid.NewGuid().GetHashCode()).Next(0, 255); //随机返回一个0~255之间的值
int g = new Random(Guid.NewGuid().GetHashCode()).Next(0, 255); //随机返回一个0~255之间的值
int r = new Random(Guid.NewGuid().GetHashCode()).Next(0, 255); //随机返回一个0~255之间的值
Rectangle ccomp = new Rectangle(); //定义重绘区域的最小边界矩形区域(conneted components缩写为ccomp)
//在重绘区域的像素的新值,若是彩色模式,取MCvScalar(b, g, r)
//若是灰度模式,取MCvScalar(r * 0.299 + g * 0.587 + b * 0.114)
MCvScalar newVal = g_bIsColor ? new MCvScalar(b, g, r) : new MCvScalar(r * 0.299 + g * 0.587 + b * 0.114);
Mat dst = g_bIsColor ? g_dstImage : g_grayImage; //dst赋值
int area = 0; //统计被重绘像素的数量
if (g_bUseMask)
{
//threshold函数作用为将g_maskImage二值化,其值大于1值,取值为128,反之取值为0
//CvInvoke.Threshold(g_maskImage, g_maskImage, 1, 128, ThresholdType.Binary);
//注意:Rectangle变量ccomp前需要使用out关键字,表示参数通过引用来传递
g_cannyImage.CopyTo(g_maskImage); //边缘检测后的输出将作为FloodFill函数中的掩膜,黑色区域可以被填充,白色区域则保留原来的颜色
area = CvInvoke.FloodFill(dst, g_maskImage, seed, newVal, out ccomp, new MCvScalar(LowDifference, LowDifference, LowDifference),
new MCvScalar(UpDifference, UpDifference, UpDifference), g_nConnectivity, flags);
//CvInvoke.Imshow("mask", g_maskImage);
}
else
{
//将mask所有元素设置为0(此时相当于没有mask)
g_maskImage = Mat.Zeros(g_maskImage.Rows, g_maskImage.Cols, g_maskImage.Depth, g_maskImage.NumberOfChannels);
area = CvInvoke.FloodFill(dst, g_maskImage, seed, newVal, out ccomp, new MCvScalar(LowDifference, LowDifference, LowDifference),
new MCvScalar(UpDifference, UpDifference, UpDifference), g_nConnectivity, flags);
}
imageBox1.Image = dst;
ShowMessage(area.ToString() + "个像素被重绘\n");
}
//获取键盘按键
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
//判断ESC是否按下,若按下则退出程序
if (e.KeyCode == Keys.Escape)
{
MessageBox.Show("程序退出......");
Application.Exit();
}
//根据按键的不同,执行各种操作
switch (e.KeyCode)
{
//如果键盘按键"1"被按下,效果图在灰度图和彩色图之间切换
case Keys.D1:
if (g_bIsColor) //若原来为彩图,则转为灰度图,并将掩膜mask所有元素设置为0
{
ShowMessage("按键“1”被按下,切换彩色/灰度模式,当前操作将【彩色模式】切换为【灰度模式】\n");
CvInvoke.CvtColor(g_srcImage, g_grayImage, ColorConversion.Bgr2Gray);
g_maskImage = Mat.Zeros(g_maskImage.Rows, g_maskImage.Cols, g_maskImage.Depth, g_maskImage.NumberOfChannels);
g_bIsColor = false;
}
else //若原来为灰度图,则将原来的彩图srcImage再次复制给dstImage,并将掩膜所有元素设置为0
{
ShowMessage("按键“1”被按下,切换彩色/灰度模式,当前操作将【灰度模式】切换为【彩色模式】\n");
g_srcImage.CopyTo(g_dstImage);
g_maskImage = Mat.Zeros(g_maskImage.Rows, g_maskImage.Cols, g_maskImage.Depth, g_maskImage.NumberOfChannels);
g_bIsColor = true;
}
break;
//如果键盘按键"2"被按下,显示/隐藏掩膜窗口
case Keys.D2:
if (g_bUseMask)
{
CvInvoke.DestroyWindow("mask");
g_bUseMask = false;
}
else
{
CvInvoke.NamedWindow("mask", 0);
g_cannyImage.CopyTo(g_maskImage); //边缘检测后的输出将作为FloodFill函数中的掩膜
CvInvoke.Imshow("mask", g_maskImage);
g_bUseMask = true;
}
break;
//如果键盘按键"3"被按下,则恢复为未进行漫水填充的图像
case Keys.D3:
ShowMessage("按键“3”被按下,恢复原始图像\n");
g_srcImage.CopyTo(g_dstImage);
CvInvoke.CvtColor(g_dstImage, g_grayImage, ColorConversion.Bgr2Gray);
g_maskImage = Mat.Zeros(g_maskImage.Rows, g_maskImage.Cols, g_maskImage.Depth, g_maskImage.NumberOfChannels);
imageBox1.Image = g_bIsColor ? g_dstImage : g_grayImage;
break;
//如果键盘按键“4”被按下,使用空范围的漫水填充
case Keys.D4:
ShowMessage("按键“4”被按下,使用空范围的漫水填充\n");
g_nFillMode = 0;
break;
//如果键盘按键“5”被按下,使用渐变、固定范围的漫水填充
case Keys.D5:
ShowMessage("按键“5”被按下,使用渐变、固定范围的漫水填充\n");
g_nFillMode = 1;
break;
//如果键盘按键“6”被按下,使用渐变、浮动范围的漫水填充
case Keys.D6:
ShowMessage("按键“6”被按下,使用渐变、浮动范围的漫水填充\n");
g_nFillMode = 2;
break;
//如果键盘按键“7”被按下,使用4位的连接模式
case Keys.D7:
ShowMessage("按键“7”被按下,使用4位的连接模式\n");
g_nConnectivity = Connectivity.FourConnected;
break;
//如果键盘按键“8”被按下,使用8位的连接模式
case Keys.D8:
ShowMessage("按键“8”被按下,使用8位的连接模式\n");
g_nConnectivity = Connectivity.EightConnected;
break;
}
}
private void ToolStripMenuItem_Click(object sender, EventArgs e)
{
Form form2 = new Form(); //创建新的窗口form2
form2.Size = new Size(400, 220); //设置新窗口的尺寸
form2.Show(); //显示新窗口
form2.Text = "按键操作说明"; //窗口标题栏
RichTextBox rtb = new RichTextBox(); //新建RichTextBox
rtb.Dock = DockStyle.Fill; //设置其Dock方式为Fill
rtb.AppendText("鼠标点击图中区域 - 进行漫水填充操作\n");
rtb.AppendText("键盘按键【ESC】 - 退出程序\n");
rtb.AppendText("键盘按键【1】 - 切换彩色图/灰色图模式\n");
rtb.AppendText("键盘按键【2】 - 显示/隐藏掩膜窗口\n");
rtb.AppendText("键盘按键【3】 - 恢复原始图像\n");
rtb.AppendText("键盘按键【4】 - 使用空范围的漫水填充\n");
rtb.AppendText("键盘按键【5】 - 使用渐变、固定范围的漫水填充\n");
rtb.AppendText("键盘按键【6】 - 使用渐变、浮动范围的漫水填充\n");
rtb.AppendText("键盘按键【7】 - 操作标识符的低八位使用4位的连接模式\n");
rtb.AppendText("键盘按键【8】 - 操作标识符的低八位使用8位的连接模式");
form2.Controls.Add(rtb); //将rtb添加到窗口中
}
private void lowDiffTrackBar_Scroll(object sender, EventArgs e)
{
g_nLowDifference = lowDiffTrackBar.Value; //负差最大值随TrackBar的值而变
lowDiffValueLable.Text = lowDiffTrackBar.Value.ToString();
}
private void upDiffTrackBar_Scroll(object sender, EventArgs e)
{
g_nUpDifference = upDiffTrackBar.Value; //正差最大值随TrackBar的值而变
upDiffValueLable.Text = upDiffTrackBar.Value.ToString();
}
//自定义函数,用于记录操作过程
void ShowMessage(string s)
{
OperationLogRichTxtBox.AppendText(s);
}
}
}
运行截图:
如果需要完整的程序资源,可到如下链接下载: