本文纲要:
TNoFourCornersRect结构及Contains函数
本文内容:
规划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,重复第①~③步
⑤开辟四个线程,分别从Left、Top、Right、Bottom开始,寻找扫描整块Board需要FOV的情况
这四步中,最终要的第③步,代码如下:
查找FOV代码
/*---------------------------------------------------------------- * 下面的扫描以Board的Left边开始, * 可开辟四个线程,分别从Left、Top、Right、Bottom进行扫描 ----------------------------------------------------------------*/ /// <summary> /// 滑动窗口扫描,寻找FOV /// </summary> /// <param name="referItem">参考Pad(即最左侧的那个Pad)</param> /// <param name="itemList">Board中剩余的未包含在已知FOV中的Pad</param> /// <param name="boardSizeX">Board宽度</param> /// <param name="boardSizeY">Board高度</param> /// <param name="minPosX">扫描时X方向移动步宽</param> /// <param name="minPosY">扫描时Y方向移动步宽</param> /// <param name="bindingItemList">找到一个FOV是与之对应的Pad加入到此List</param> /// <param name="left">输出参数:找到的FOV的左边</param> /// <param name="right">输出参数:找到的FOV的右边</param> /// <param name="top">输出参数:找到的FOV的上边</param> /// <param name="bottom">输出参数:找到的FOV的下边</param> /// <param name="bestPos">记录找到的FOV的起点</param> /// <param name="pos">本文中从Left边搜索扫描</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,其中有一个重要的函数,判断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; } }
延伸与拓展:图形验证方法的重要性
这个程序的结果并不是最重要的,重要的是,怎么保证自己写出的这些代码的正确性,还有,如果中间程序运行不下去,问题出在哪?怎么找出问题所在?程序的运行过程是抽象的,中间有很多数据处理,何况,这几个函数只是程序的一部分,再加上外层函数的调用,中间又有很多FOV生成,每个FOV都要滑动很多次,这些都是细节。
仅靠逻辑和数学等知识去冥想,甚至加上VS的调试工具,都很难发现问题出现的哪。这个过程中,我用的方法是:将程序运行的一些关键节点用直观的方式写入图像文件,例如:矩形,FOV等,这样,程序运行到哪一步出问题,出问题的时候都有哪些图形完成并呈现,哪些没有达到预期,当时的具体情况会一目了然。
例如,在写Contains函数的时候,就用了如下的方法验证:每一个Pad绑定一个bool变量,当该Pad包含在FOV时,就在图片中该Pad上面写上true,当Pad不包含在FOV 时,就在图片该位置标记为false,很容易就找出Contains函数中有哪些情况没有考虑到以及逻辑是否有问题。最后增加redundantSafety这个变量来对double变量的比较做安全冗余也是通过这样的方法。