实现效果如上图。
我将我之前的方法进行重构了,优化后方便后续的拓展。
下面就介绍下具体的实现吧。
我是在winform内实现的,当我们在界面上用鼠标选取ROI时,可以在picturebox的mousedown事件内记录下第一点,再在mousemove事件中获取实时的鼠标位置当做第二点,根据这两点我们就能得到一个矩形,即OpencvSharp内的Rect ,然后我们就可以在这个区域内进行我们的绘制,当然我还需要进行ROI的旋转 所以就用的 RotatedRect。两者其实差别并不大。
因为目前这些形状都能通过一个矩形 和其他参数来确定唯一的,所以 我定义了 以RotatedRect 为基础的抽象类 ,通过后续不同的继承来实现不同的形状,主要原理我在代码的注释中都写上了
直接放代码:
这是我的基类 后面所有的形状都是继承这个类的
///
/// 父类ROI 可有不同形状 绘制方式根据一个矩形 可以在内部绘制不同的曲线 从而实现不同的ROI
///
abstract class ROI
{
public RotatedRect rotatedRect;
///
/// 包围ROI的最小正矩形
///
public Rect BoundingRect => Cv2.BoundingRect(GetPoints());
///
/// ROI的中心点 即所有顶点之和的平均值
/// 一般用于当做获取ROI掩膜图像进行水漫过程的种子点 因为该点绝大部分在ROI内部
///
public OpenCvSharp.Point Center
{
get
{
OpenCvSharp.Point point = new Point();
foreach (Point pp in GetPoints())
{
point += pp;
}
point = point * (1.0 / GetPoints().Length);
return point;
}
}
///
/// 在图像上绘制出ROI
///
/// 输入图像
/// 画笔颜色
/// 画笔粗细
/// 绘制好的图像
public abstract Mat DrawROI(Mat src, Scalar scalar, int thinckness = 1);
///
/// 获取ROI顶点
///
///
public abstract Point[] GetPoints();
///
/// 提取ROI图像
/// 大概步骤为
/// 1、得到掩膜图像 即 只有ROI部分不为0 其余地方均为0的图像
/// 2、确定ROI范围 即 得到包围ROI的最小正矩形 分别从输入图像和掩膜中提取对应的矩形部分
/// 3、利用图像自身的与运算结合掩膜就能提取出掩膜部分图像了
///
///
///
public Mat ExtractROI(Mat src)
{
Mat mask = GetMaskFloodFill(src.Size());
Rect rect = BoundingRect;
src = new Mat(src, rect);
Mat maskRoI = new Mat(mask, rect);
Cv2.CvtColor(maskRoI, maskRoI, ColorConversionCodes.BGR2GRAY);
Mat dstImg = new Mat();
Cv2.BitwiseAnd(src, src, dstImg, maskRoI);
return dstImg;
}
///
/// 对ROI部分进行Inpaint 原理同提取ROI图像 只是将最后的与运算改为 Inpaint 方法
///
///
///
public Mat InPaintROI(Mat src)
{
Mat mask = GetMaskFloodFill(src.Size());
Cv2.CvtColor(mask, mask, ColorConversionCodes.BGR2GRAY);
Mat dstImg = new Mat();
Cv2.Inpaint(src, mask, dstImg, 1, InpaintMethod.NS);
return dstImg;
}
///
/// 获取指定大小的ROI部分的掩膜单通道图像
///
/// 最小大小为 BoundingRect.Size
///
public Mat GetMaskFloodFill(OpenCvSharp.Size size)
{
if (size.Width < BoundingRect.Size.Width || size.Height < BoundingRect.Size.Height)
size = BoundingRect.Size;
Mat mask = Mat.Zeros(size, MatType.CV_8UC3);
mask = DrawROI(mask, Scalar.Red);
//水漫的种子点 要求在ROI区域内部
OpenCvSharp.Point pt = new OpenCvSharp.Point(Center.X, Center.Y);
Cv2.FloodFill(mask, pt, Scalar.Red);
mask.ConvertTo(mask, MatType.CV_8UC1);
return mask;
}
///
/// 重置 清零
///
public void Reset()
{
rotatedRect = new RotatedRect();
}
///
/// 判断ROI是否超限图像
///
///
///
///
public bool OverRun(Mat src, int gap = 3)
{
bool en = true;
Rect rect = rotatedRect.BoundingRect();
en = en && rect.X >= gap;
en = en && rect.Y >= gap;
en = en && rect.Right <= src.Width - gap;
en = en && rect.Bottom <= src.Height - gap;
return !en;
}
///
/// 判断ROI是否为空 下面各种ROI都是在RotatedRect的里面进行绘制的 即只需要判断RotatedRect是否是空的
///
public bool IsNull => rotatedRect.Size == new Size2f(0, 0);
///
/// ROI的移动 得到ROI在指定图像内移动后的新图像
///
///
///
///
///
///
public Mat MoveROI(Mat src, yVars.eMovingDirections directions, int step, int gap = 3)
{
Mat dstImg = src.Clone();
OpenCvSharp.Point2f[] pts = rotatedRect.Points();
switch (directions)
{
case yVars.eMovingDirections.上:
PointsArrRank(ref pts, false, true);
if (pts[0].Y - step <= gap)
{
rotatedRect.Center.Y -= (int)(pts[0].Y - gap);
}
else
{
rotatedRect.Center.Y -= (int)(step);
}
break;
case yVars.eMovingDirections.下:
PointsArrRank(ref pts, false, false);
if (pts[0].Y + step >= src.Height - gap)
{
rotatedRect.Center.Y += (int)(src.Height - pts[0].Y - gap);
}
else
{
rotatedRect.Center.Y += step;
}
break;
case yVars.eMovingDirections.左:
PointsArrRank(ref pts, true, true);
if (pts[0].X - step <= gap)
{
rotatedRect.Center.X -= (int)(pts[0].X - gap);
}
else
{
rotatedRect.Center.X -= step;
}
break;
case yVars.eMovingDirections.右:
PointsArrRank(ref pts, true, false);
if (pts[0].X + step >= src.Width - gap)
{
rotatedRect.Center.X += (int)(src.Width - pts[0].X - gap);
}
else
{
rotatedRect.Center.X += step;
}
break;
default:
break;
}
dstImg = DrawROI(dstImg, Scalar.Red, 1);
return dstImg;
}
///
/// ROI调整大小 ROI在图像内调整大小后的图像
///
///
///
///
///
///
public Mat ResizeROI(Mat src, yVars.eResizeModes modes, bool IsAdd, int step)
{
Mat dstImg = src.Clone();
switch (modes)
{
case yVars.eResizeModes.All:
rotatedRect.Size.Width += (IsAdd == true ? 1 : -1) * step;
rotatedRect.Size.Height += (IsAdd == true ? 1 : -1) * step;
break;
case yVars.eResizeModes.Width:
rotatedRect.Size.Width += (IsAdd == true ? 1 : -1) * step;
break;
case yVars.eResizeModes.Height:
rotatedRect.Size.Height += (IsAdd == true ? 1 : -1) * step;
break;
}
Point2f[] points = rotatedRect.Points(); // 获得矩形四个顶点坐标
//大小改变后的坐标需要判断是否超限了
for (int i = 0; i < points.Length; i++)
{
// 有一个超限后 大小需要更改回原来的 结束
if (points[i].X <= 0 || points[i].Y <= 0 || points[i].X >= src.Width || points[i].Y >= src.Height)
{
switch (modes)
{
//矩形的长宽同时变
case yVars.eResizeModes.All:
rotatedRect.Size.Width -= (IsAdd == true ? 1 : -1) * step;
rotatedRect.Size.Height -= (IsAdd == true ? 1 : -1) * step;
break;
// 只变矩形长 即宽度
case yVars.eResizeModes.Width:
rotatedRect.Size.Width -= (IsAdd == true ? 1 : -1) * step;
break;
// 只变矩形的宽 即高度
case yVars.eResizeModes.Height:
rotatedRect.Size.Height -= (IsAdd == true ? 1 : -1) * step;
break;
}
}
}
dstImg = DrawROI(dstImg, Scalar.Red, 1);
return dstImg;
}
///
/// ROI的旋转 ROI在图像内旋转后的图像
///
///
///
///
///
public Mat RotatedROI(Mat src, yVars.eRotatedDirections directions, int step)
{
Mat dstImg = src.Clone();
switch (directions)
{
case yVars.eRotatedDirections.顺时针:
rotatedRect.Angle += step;
break;
case yVars.eRotatedDirections.逆时针:
rotatedRect.Angle -= step;
break;
}
Point2f[] points = rotatedRect.Points(); // 获得矩形四个顶点坐标
//旋转后的坐标需要判断是否超限了
for (int i = 0; i < points.Length; i++)
{
// 超限后旋转角度需要更改回原来的
if (points[i].X <= 0 || points[i].Y <= 0 || points[i].X >= src.Width || points[i].Y >= src.Height)
{
switch (directions)
{
case yVars.eRotatedDirections.顺时针:
rotatedRect.Angle -= step;
break;
case yVars.eRotatedDirections.逆时针:
rotatedRect.Angle += step;
break;
}
}
}
dstImg = DrawROI(dstImg, Scalar.Red, 1);
return dstImg;
}
///
/// 根据移动方向对移动的所有点位进行排序
/// 例 往左运动 ,则按照升序和所有点位X坐标进行排序
/// 保证第一个点是X最小的那个点 即 运动方向最前面的那个点
/// 那么 在判断ROI的移动过程中 只需要判断运动方向最前面的那个点有无超限即可 就不需要判断所有的点了
///
///
///
///
private void PointsArrRank(ref OpenCvSharp.Point2f[] pts, bool IsX, bool IsAc = true)
{
OpenCvSharp.Point2f p0 = new Point2f();
// X
if (IsX == true)
{
if (IsAc == true)
{
for (int i = 0; i < pts.Length - 1; i++)
{
for (int j = 0; j < pts.Length - 1 - i; j++)
{
if (pts[j].X >= pts[j + 1].X)
{
p0 = pts[j];
pts[j] = pts[j + 1];
pts[j + 1] = p0;
}
}
}
}
else
{
for (int i = 0; i < pts.Length - 1; i++)
{
for (int j = 0; j < pts.Length - 1 - i; j++)
{
if (pts[j].X <= pts[j + 1].X)
{
p0 = pts[j];
pts[j] = pts[j + 1];
pts[j + 1] = p0;
}
}
}
}
}
else //Y
{
if (IsAc == true)
{
for (int i = 0; i < pts.Length - 1; i++)
{
for (int j = 0; j < pts.Length - 1 - i; j++)
{
if (pts[j].Y >= pts[j + 1].Y)
{
p0 = pts[j];
pts[j] = pts[j + 1];
pts[j + 1] = p0;
}
}
}
}
else
{
for (int i = 0; i < pts.Length - 1; i++)
{
for (int j = 0; j < pts.Length - 1 - i; j++)
{
if (pts[j].Y <= pts[j + 1].Y)
{
p0 = pts[j];
pts[j] = pts[j + 1];
pts[j + 1] = p0;
}
}
}
}
}
}
}
public enum eMovingDirections // 移动方向
{
上 = 1,
下 = 2,
左 = 3,
右 = 4
}
public enum eRotatedDirections //旋转方向
{
顺时针 = 0,
逆时针 = 1,
}
public enum eResizeModes // 大小调整方式
{
All = 0, // 长宽同时增加
Width = 1, // 只变宽度 长
Height = 2, // 只变高度 宽
}
后续不同的形状只需要继承上面父类 再对其父类中的几个抽象方法进行不同的重载即可,主要是DrawROI()和GetPoints()这两个方法,进行ROI的提取只需要通过子类对象调用父类里面的ExtractROI()就行 其他方法也是类似的。
下面是我写的部分形状的ROI吧
椭圆
///
/// 椭圆ROI 即在矩形内绘制一个椭圆
///
class ROIEllipse : ROI
{
public ROIEllipse()
{
rotatedRect = new RotatedRect();
}
public ROIEllipse(RotatedRect rect)
{
rotatedRect = rect;
}
public override Mat DrawROI(Mat src, Scalar scalar, int thickness = 1)
{
Mat dstImg = src.Clone();
try
{
Cv2.Ellipse(dstImg, rotatedRect, scalar, thickness, LineTypes.AntiAlias);
}
catch
{
dstImg = src.Clone();
}
return dstImg;
}
///
/// 椭圆的顶点就返回rotatedRect的四个顶点 不能为空
///
///
public override Point[] GetPoints()
{
return (from pp in rotatedRect.Points() select new OpenCvSharp.Point(pp.X, pp.Y)).ToArray();
}
}
五边形:
///
/// 五边形ROI 即在矩形内绘制一个指定方向的五边形 五个顶点都在边上
///
class ROIPentagon : ROI
{
//五边形顶点方向; 0 1 2 3 对应 上 右 下 左
public int Flag { set; get; }
public ROIPentagon()
{
rotatedRect = new RotatedRect();
Flag = 0;
}
///
/// 0 1 2 3 对应 上 右 下 左
///
///
/// 五边形一个顶点所在位置方向
public ROIPentagon(RotatedRect rect, int flag = 0)
{
rotatedRect = rect;
Flag = flag;
}
public override OpenCvSharp.Mat DrawROI(Mat src, Scalar scalar, int thickness = 1)
{
Mat dstImg = src.Clone();
OpenCvSharp.Point[] points1 = GetPoints();
try
{
for (int i = 0; i <= 4; i++)
{
if (i != 4)
{
Cv2.Line(dstImg, points1[i], points1[i + 1], scalar, thickness, LineTypes.AntiAlias);
}
else
{
Cv2.Line(dstImg, points1[4], points1[0], scalar, thickness, LineTypes.AntiAlias);
}
}
}
catch
{
dstImg = src.Clone();
}
return dstImg;
}
public override Point[] GetPoints()
{
OpenCvSharp.Point[] points = (from pp in rotatedRect.Points() select new OpenCvSharp.Point(pp.X, pp.Y)
).ToArray();
OpenCvSharp.Point[] resultPoints = new Point[5];
switch (Flag)
{
default:
case 0:
resultPoints[0] = points[1] * 0.5f + points[2] * 0.5f;
resultPoints[1] = points[2] * 0.625f + points[3] * 0.375f;
resultPoints[2] = points[3] * 0.833f + points[0] * 0.167f;
resultPoints[3] = points[3] * 0.167f + points[0] * 0.833f;
resultPoints[4] = points[0] * 0.375f + points[1] * 0.625f;
break;
case 1:
resultPoints[0] = points[2] * 0.5f + points[3] * 0.5f;
resultPoints[1] = points[3] * 0.625f + points[0] * 0.375f;
resultPoints[2] = points[0] * 0.833f + points[1] * 0.167f;
resultPoints[3] = points[0] * 0.167f + points[1] * 0.833f;
resultPoints[4] = points[1] * 0.375f + points[2] * 0.625f;
break;
case 2:
resultPoints[0] = points[3] * 0.5f + points[0] * 0.5f;
resultPoints[1] = points[0] * 0.625f + points[1] * 0.375f;
resultPoints[2] = points[1] * 0.833f + points[2] * 0.167f;
resultPoints[3] = points[1] * 0.167f + points[2] * 0.833f;
resultPoints[4] = points[2] * 0.375f + points[3] * 0.625f;
break;
case 3:
resultPoints[0] = points[0] * 0.5f + points[1] * 0.5f;
resultPoints[1] = points[1] * 0.625f + points[2] * 0.375f;
resultPoints[2] = points[2] * 0.833f + points[3] * 0.167f;
resultPoints[3] = points[2] * 0.167f + points[3] * 0.833f;
resultPoints[4] = points[3] * 0.375f + points[0] * 0.625f;
break;
}
return resultPoints;
}
public new void Reset()
{
base.Reset();
Flag = 0;
}
}
四角星
// 四角星
class ROIShuriken : ROI
{
public float Ratio { set; get; } = 0.75f;
public ROIShuriken()
{
rotatedRect = new RotatedRect();
}
///
/// 通过一个 RotatedRect 和比例来确定唯一的四角星
///
///
/// 比例
public ROIShuriken(RotatedRect rect, float ratio = 0.75f)
{
rotatedRect = rect;
if (ratio < 0 || ratio > 0.75) ratio = 0.75f;
Ratio = ratio;
}
///
/// 在图像上画出该ROI
///
/// 输入图像
/// 画笔颜色
/// 画笔粗细
/// 绘制好的图像,若绘制失败则返回原图
public override OpenCvSharp.Mat DrawROI(Mat src, Scalar scalar, int thickness = 1)
{
Mat dstImg = src.Clone();
float angle = rotatedRect.Angle;
OpenCvSharp.Point[] points = (from x in rotatedRect.Points()
select new OpenCvSharp.Point(x.X, x.Y)).ToArray();
OpenCvSharp.Point[] points1 = GetPoints();
try
{
for (int i = 0; i <= points1.Length-1; i++)
{
if (i != points1.Length - 1)
{
Cv2.Line(dstImg, points1[i], points1[i + 1], scalar, thickness, LineTypes.AntiAlias);
}
else
{
Cv2.Line(dstImg, points1[points1.Length - 1], points1[0], scalar, thickness, LineTypes.AntiAlias);
}
}
}
catch
{
dstImg = src.Clone();
}
return dstImg;
}
///
/// 四角星的八个顶点 按顺时针顺序
///
///
public override Point[] GetPoints()
{
OpenCvSharp.Point[] points = (from pp in rotatedRect.Points() select new OpenCvSharp.Point(pp.X, pp.Y)
).ToArray();
OpenCvSharp.Point[] resultPoints = new Point[8];
OpenCvSharp.Point cc = new Point(rotatedRect.Center.X, rotatedRect.Center.Y);
resultPoints[0] = (points[1] + points[2]) * 0.5;
resultPoints[1] = cc * Ratio + points[2] * (1 - Ratio);
resultPoints[2] = points[2] * 0.5 + points[3] * 0.5;
resultPoints[3] = cc * Ratio + points[3] * (1 - Ratio);
resultPoints[4] = points[0] * 0.5 + points[3] * 0.5;
resultPoints[5] = cc * Ratio + points[0] * (1 - Ratio);
resultPoints[6] = points[0] * 0.5 + points[1] * 0.5;
resultPoints[7] = cc * Ratio + points[1] * (1 - Ratio);
return resultPoints;
}
}
其他形状的ROI原理都跟上面类似 只需要根据矩形的四个顶点来确定好ROI所有顶点的位置即可,在一一画线连起来即可。