FOV规划的原因
当一次拍照不能完全覆盖目标区域时,需要分多次拍摄以完成目标识别,当需要拍照的区域较多时,为提高效率,FOV的路径规划和排序就有了必要。
FOV去四角
一个FOV的四角通常亮度较差或畸变较大,所以实际的拍照过程中,将四个角排除在兴趣区域以外,形成了如下图的形状,实际的兴趣区域类似一个十字架形状,四角的大小可以通过参数调节,这个结构体在下面有代码→TNoFourCornersRect,其中有判断Pad是否包含在这个十字架中的函数的定义:
做如下定义和声明:
假设有一块电路板:Board,
Board上是印刷锡膏的焊点作为检测目标,这些焊点定义为Pad,
Pad、Board均为矩形,使用(X,Y)坐标定义四个顶点和确定四条边,例如:如果一个Pad的LeftTop顶点坐标为(20,35),则该Pad的Left边为LeftTop的X坐标即20;
Board和Pad的四个边分别使用Left、Top、Right、Bottom代表
FOV查找思路
①首先,从Board的Left边开始规划FOV
②找到电路板上所有Pad中Left值最小的Pad,即最左边的Pad
③在兴趣区域包含第②步Pad的情况下,让FOV的Left边与该Pad的Left边重合,上下移动兴趣区域进行查找,当兴趣区域包含的Pad最多时,即选出一个FOV
④第③步进行完后,Board上未被包含的Pad变少,再找到剩余Pad中最左侧的Pad,重复第①~③步
这四步中,最终要的第③步,代码如下:
/*---------------------------------------------------------------- * 下面的扫描以Board的Left边开始, * 可开辟四个线程,分别从Left、Top、Right、Bottom进行扫描 ----------------------------------------------------------------*/ /// <summary> /// 滑动窗口扫描 /// </summary> /// <param name="referItem">参考的item</param> /// <param name="itemList">搜索的item的列表</param> /// <param name="minPosX">x方向的最小步进距离</param> /// <param name="minPosY">y方向的最小步进距离</param> /// <param name="bindingItemList">绑定的Item列表</param> /// <param name="left">包含最多矩形的fov的左边</param> /// <param name="right">包含最多矩形的fov的右边</param> /// <param name="top">包含最多矩形的fov的上边</param> /// <param name="bottom">包含最多矩形的fov的下边</param> /// <param name="bestPos">最佳位置</param> /// <param name="pos">搜索的方向</param> protected void SlidingScan(TargetBase referItem, List<TargetBase> itemList, double boardSizeX, double boardSizeY, double minPosX, double minPosY, List<TargetBase> bindingItemList, ref double left, ref double right, ref double top, ref double bottom, ref double bestPos, SingleBoundFovDivider.DividionStarts pos) { double Universal = FovScheduler.FovSizeX / 8.0; //四角长宽通用数值 double redundantSafety = 1.0; //这个参数是安全冗余,double类型的值作比较,冗余很重要, //当初程序运行一直不通过,调试老费劲了, //最后才发现double的最后几位并不准确,导致两个double变量比较大小时结果与期望不符 //此处注意:FOV共有四个角,现讨论四个角的长度宽度相等 //都等于FovScheduler.FovSizeX / 8.0,与Universal的值也想等 //四个角的长度宽度可自定义的情况原理相同 double leftTopCornerX = FovScheduler.FovSizeX / 8.0; double leftTopCornerY = FovScheduler.FovSizeX / 8.0; double leftBottomCornerX = FovScheduler.FovSizeX / 8.0; double leftBottomCornerY = FovScheduler.FovSizeX / 8.0; double startSild = 0;//滑动窗口的起始位置 double endSild = 0;//滑动窗口的结束位置 double fix = 0;//固定的起始位置 double minStep = 0;//最小的步进距离 if (pos == SingleBoundFovDivider.DividionStarts.Left) { //去四角的FOV滑动起点的选择比普通矩形时要复杂,注意:是包含在中间的十字架区域,并要考虑Board的边界做阈值保护 startSild = Math.Max(referItem.ScheduleRect.Bottom - (FovScheduler.FovSizeY - leftBottomCornerY - leftTopCornerY) + redundantSafety, 0); endSild = Math.Min(referItem.ScheduleRect.Top - redundantSafety, boardSizeY - (FovScheduler.FovSizeY - leftBottomCornerY - leftTopCornerY)); if (pos == SingleBoundFovDivider.DividionStarts.Left) { fix = Math.Min(referItem.ScheduleRect.Left + leftTopCornerX - redundantSafety, boardSizeX - (FovScheduler.FovSizeX - leftTopCornerX)); } minStep = minPosY; } bestPos = 0; if (bindingItemList == null) bindingItemList = new List<TargetBase>(); bindingItemList.Clear(); int validcount = 0; int maxCount = 0; //开始遍历 for (double start = startSild; start <= endSild; start += minStep) { TNoFourCornersRect roi = new TNoFourCornersRect(); if (pos == SingleBoundFovDivider.DividionStarts.Left) { roi = new TNoFourCornersRect(fix, start, FovScheduler.FovSizeX, FovScheduler.FovSizeY, Universal); } //统计在roi内的最大item数量 double leftTemp = referItem.ScheduleRect.Left; double rightTemp = referItem.ScheduleRect.Right; double topTemp = referItem.ScheduleRect.Top; double bottomTemp = referItem.ScheduleRect.Bottom; var bindingItemListTemp = new List<TargetBase>(); validcount = 0; for (int i = 0; i < itemList.Count; i++) { var item = itemList[i]; var itemRect = item.ScheduleRect; if (roi.Contains(itemRect)) { validcount++; leftTemp = roi.LeftOuter; rightTemp = roi.RightOuter; topTemp = roi.TopOuter; bottomTemp = roi.BottomOuter; if (!bindingItemListTemp.Contains(item)) bindingItemListTemp.Add(item); } } //更新最大值 if (validcount > maxCount) { if (bindingItemList == null) bindingItemList = new List<TargetBase>(); bindingItemList.Clear(); bindingItemList.AddRange(bindingItemListTemp); maxCount = validcount; bestPos = start; left = leftTemp; right = rightTemp; top = topTemp; bottom = bottomTemp; } } }
上述 代码中用到一个重要的结构:TNoFourCornersRect,其中有一个重要的函数,判断TNoFourCornersRect的对象是否包含一个Pad,定义如下:
public struct TNoFourCornersRect { //左上角的内角点坐标对应XY public double X; public double Y; //内宽高 public double WidthInner; public double HeightInner; //外宽高 public double WidthOuter; public double HeightOuter; //外界矩形四个顶点 public Point2D64 leftTop; public Point2D64 rightTop; public Point2D64 rightBottom; public Point2D64 leftBottom; //内角点 public Point2D64 LeftTopInner; public Point2D64 RightTopInner; public Point2D64 RightBottomInner; public Point2D64 LeftBottomInner; //外角点 public Point2D64 LeftTopLeftOuter; public Point2D64 LeftTopTopOuter; public Point2D64 RightTopTopOuter; public Point2D64 RightTopRightOuter; public Point2D64 RightBottomRightOuter; public Point2D64 RightBottomBottomOuter; public Point2D64 LeftBottomBottomOuter; public Point2D64 LeftBottomLeftOuter; //内边 public double LeftInner; public double TopInner; public double RightInner; public double BottomInner; //外边 public double LeftOuter; public double TopOuter; public double RightOuter; public double BottomOuter; //构造函数:四个角的宽高相同 public TNoFourCornersRect(double x, double y, double widthOuter, double heigthOuter, double Universal) { //if (2 * Universal > WidthOuter || 2 * Universal > HeightOuter) //{ // //增加保护,或者在外部增加保护; //} X = x; Y = y; WidthOuter = widthOuter; HeightOuter = heigthOuter; WidthInner = WidthOuter - 2 * Universal; HeightInner = HeightOuter - 2 * Universal; leftTop = new Point2D64(X - Universal, Y - Universal); rightTop = new Point2D64(X + WidthInner + Universal, Y - Universal); rightBottom = new Point2D64(X + WidthInner + Universal, Y + HeightInner + Universal); leftBottom = new Point2D64(X - Universal, Y + HeightInner + Universal); LeftTopInner = new Point2D64(X, Y); RightTopInner = new Point2D64(X + WidthInner, Y); RightBottomInner = new Point2D64(X + WidthInner, Y + HeightInner); LeftBottomInner = new Point2D64(X, Y + HeightInner); LeftTopLeftOuter = new Point2D64(X - Universal, Y); LeftTopTopOuter = new Point2D64(X, Y - Universal); RightTopTopOuter = new Point2D64(X + WidthInner, Y - Universal); RightTopRightOuter = new Point2D64(X + WidthInner + Universal, Y); RightBottomRightOuter = new Point2D64(X + WidthInner + Universal, Y + HeightInner); RightBottomBottomOuter = new Point2D64(X + WidthInner, Y + HeightInner + Universal); LeftBottomBottomOuter = new Point2D64(X, Y + HeightInner + Universal); LeftBottomLeftOuter = new Point2D64(X - Universal, Y + HeightInner); LeftInner = X; TopInner = Y; RightInner = X + WidthInner; BottomInner = Y + HeightInner; LeftOuter = X - Universal; TopOuter = Y - Universal; RightOuter = X + WidthInner + Universal; BottomOuter = Y + HeightInner + Universal; } /// <summary> /// 判断Rect64的对象是否包含与此类的对象 /// </summary> /// <param name="item">待判断的目标</param> /// <returns></returns> public bool Contains(Rect64 item) { if (item.LeftTop.X >= leftTop.X && item.LeftTop.Y >= leftTop.Y && item.RightTop.X <= rightTop.X && item.RightTop.Y >= rightTop.Y && item.RightBottom.X <= rightBottom.X && item.RightBottom.Y <= rightBottom.Y && item.LeftBottom.X >= leftBottom.X && item.LeftBottom.Y <= leftBottom.Y) { if (item.LeftTop.X < LeftTopInner.X && item.LeftTop.Y < LeftTopInner.Y) { return false; } if (item.RightTop.X > RightTopInner.X && item.RightTop.Y < RightTopInner.Y) { return false; } if (item.RightBottom.X > RightBottomInner.X && item.RightBottom.Y > RightBottomInner.Y) { return false; } if (item.LeftBottom.X < LeftBottomInner.X && item.LeftBottom.Y > LeftBottomInner.Y) { return false; } return true; } return false; } }
其中Contains函数完全是受另一个算法的启发,网上有判断两个矩形是否相交的算法,仅仅判断一些顶点坐标之间的关系就判断两个矩形是否相交,逻辑清楚简介,这给我带来不少启发,也因此,用了很少几行代码就能判断出一个矩形是否包含在这个十字架形状中。网上的判断矩形是否相交的函数如下:
/// <summary> ///判断两个Rect64矩形是否相交 /// </summary> /// <param name="rect1"></param> /// <param name="rect2"></param> /// <returns>相交则返回为true</returns> private bool IsRect64Intersect(Rect64 rect1, Rect64 rect2) { double minX, minY, maxX, maxY; minX = Math.Max(rect1.LeftTop.X, rect2.LeftTop.X); minY = Math.Max(rect1.LeftTop.Y, rect2.LeftTop.Y); maxX = Math.Min(rect1.RightBottom.X, rect2.RightBottom.X); maxY = Math.Min(rect1.RightBottom.Y, rect2.RightBottom.Y); if (minX > maxX || minY > maxY) { return false; } else { return true; } }
延伸与拓展
这个程序的结果并不是最重要的,重要的是,程序的运行过程很抽象,中间有很多数据处理,如果中途有bug,很难发现问题出在哪里,在调试的过程中,我发现,将一些程序运行的重要节点写入图像文件,但后观察是否是自己想要的结果这个方法非常实用,例如,每一个Pad绑定一个bool变量,当该Pad包含在FOV时,就在图片中该Pad上面写上false,很容易就找出Contains函数中有哪些情况没有考虑到。所以,对一个程序的直观验证比逻辑、数学知识要强大的多。
镜头覆盖范围FOV 的拍照路径规划与FOV去四角