using CCDCount.MODEL.ConfigModel; using CCDCount.MODEL.ShuLiModel; using LogClass; using MvCameraControl; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CCDCount.DLL { public class ShuLiClass { #region 变量 /// /// 当活跃物体转变为历史物体时的回调事件 /// public event EventHandler WorkCompleted; private List activeObjects = new List(); // 当前跟踪中的物体 private List historyActiveObjects = new List(); // 历史物体 private ConcurrentQueue IFrameDatas = new ConcurrentQueue(); //图像数据队列 private Thread IdentifyImageProcessThread = null; // 识别线程 private bool IsIdentify = false; //线程是否开始识别的标志 private long currentLine = 0; //行数记录 private ShuLiConfigClass shuLiConfig = null;// 数粒参数配置文件 public List ChannelsRoi { get { return _ChannelsRoi; } } private List _ChannelsRoi = new List(); private int ChannelWidth = 0;//每个区域的宽度 private int IdentifyImageWidth = -1; private static readonly object _lockObj = new object(); // 专用锁对象\ private int ObjectNum = 0; public int ImageNum { get { return IFrameDatas.Count; } } #endregion #region 公共方法 /// /// 初始化构造方法 /// public ShuLiClass() { // 加载默认参数 shuLiConfig = new ShuLiConfigClass() { Channel = 8, PandingCode = 2 }; } public ShuLiClass(ShuLiConfigClass config) { if (config.IsLoadCanfig) { // 加载传出的参数 shuLiConfig = config; } else { // 加载默认参数 shuLiConfig = new ShuLiConfigClass() { Channel = 8, PandingCode = 2 }; } InitChannel(); } public long InitCurrentLine(int line) { currentLine = line; return currentLine; } public int InitNum(int num) { ObjectNum = num; return ObjectNum; } /// /// 处理图像序列的主入口 /// /// 图像像素数据 /// 图像宽 /// 当前行数 /// 检测到的物体总数 public bool ProcessImageSequence(IImage image) { bool result = false; for (int i = 0; i < image.Height; i++) { result = ProcessLine(image, i); currentLine += 1; } return result; } /// /// 返回最后一个历史物品 /// /// public ActiveObjectClass GetLastActive() { if (historyActiveObjects.Count() == 0) return null; return historyActiveObjects.Last(); } /// /// 返回历史物品 /// /// public List GetHistoryActive() { lock (_lockObj) // 加锁 { return historyActiveObjects.ToList(); } } /// /// 返回缓存在内存的历史物品的总数量 /// /// public int GetHistoryActiveNum() { lock (_lockObj) // 加锁 return historyActiveObjects.Count(); } /// /// 获取历史数据中,正常数据数量 /// /// public int GetOkHistoryNum() { lock (_lockObj) return historyActiveObjects.Where(o => o.StateCode == 0).Count(); } /// /// 获取历史数据中,异常数据数量 /// /// public int GetNgHistoryNum() { lock (_lockObj) return historyActiveObjects.Where(o => o.StateCode != 0).Count(); } /// /// 清除历史数据 /// /// public bool ClearHistoryActive() { lock (_lockObj) { historyActiveObjects.Clear(); return true; } } /// /// 开启识别 /// public void StartIdentifyFuntion(int ImaageWidth) { UpdateIdentifyImageWidth(ImaageWidth); InitChannel(); try { // 标志位置位true IsIdentify = true; // 打开识别线程 IdentifyImageProcessThread = new Thread(IdentifyImageProcess) { Priority = ThreadPriority.Highest }; IdentifyImageProcessThread.Start(); } catch (Exception ex) { FaultLog.RecordErrorMessage("Start thread failed!, " + ex.Message); throw; } } /// /// 关闭识别 /// public void StopIdentifyFuntion() { try { // 标志位设为false IsIdentify = false; if (IdentifyImageProcessThread != null && IdentifyImageProcessThread.IsAlive) IdentifyImageProcessThread.Join(); } catch (Exception ex) { FaultLog.RecordErrorMessage("Stop thread failed!, " + ex.Message); throw; } } /// /// 向识别队列添加一个数据 /// /// public void SetOnceIdentifyImageData(IImage items) { IFrameDatas.Enqueue(items.Clone() as IImage); } /// /// 保存参数 /// public void SaveConfig() { if (!Directory.Exists(".\\Config\\")) Directory.CreateDirectory(".\\Config\\"); XmlStorage.SerializeToXml(shuLiConfig, ".\\Config\\ShuLiConfig.xml"); } /// /// 更新检测宽度信息 /// /// public void UpdateIdentifyImageWidth(int Width) { IdentifyImageWidth = Width; } /// /// 初始化通道划分 /// /// public void InitChannel() { _ChannelsRoi.Clear(); shuLiConfig.ImageWidth = IdentifyImageWidth == -1 ? shuLiConfig.ImageWidth : IdentifyImageWidth; if (shuLiConfig.Channel > 0) { if (shuLiConfig.IsIdentifyRoiOpen) { ChannelWidth = (shuLiConfig.IdentifyStopX - shuLiConfig.IdentifyStartX) / shuLiConfig.Channel; } else { ChannelWidth = shuLiConfig.ImageWidth / shuLiConfig.Channel; } for (int i = 0; i < shuLiConfig.Channel; i++) { _ChannelsRoi.Add(ChannelWidth + i * ChannelWidth); } } } /// /// 获取配置信息 /// /// public ShuLiConfigClass GetConfigValue() { ShuLiConfigClass result = shuLiConfig; return result; } public int GetConfigImageWidth() { int result = -1; if (shuLiConfig != null) result = shuLiConfig.ImageWidth; return result; } #endregion #region 私有方法 /// /// 对外通知事件 /// private void OnWorkCompleted(List activeObject) { ActiveObjectEventArgsClass activeObjectEventArgs = new ActiveObjectEventArgsClass(activeObject); // 触发事件 WorkCompleted?.Invoke(this, activeObjectEventArgs); } private bool IsPrintLightOnError = false; /// /// 处理单行像素数据 /// 返回值为false的时候无活跃物体转变为历史物体 /// 返回值为true的时候有活跃物体转变为历史物体 /// /// 当前行像素数组 private bool ProcessLine(IImage imagedata, int RowNo) { bool result = false; // 步骤1:检测当前行的有效区域 var currentRegions = FindValidRegions(imagedata, RowNo); if (currentRegions.Count == 1) { if (currentRegions[0].End - (currentRegions[0]).Start + 1 == imagedata.Width) { if (!IsPrintLightOnError) { FaultLog.RecordLogMessage("当前行有效区域为整行,检查视野和光源", 5); IsPrintLightOnError = true; } return false; } IsPrintLightOnError = false; } lock (_lockObj) { // 步骤2:处理当前行每个区域 for (int i = 0; i < currentRegions.Count; i++) { var region = currentRegions[i]; // 查找全部可合并的活跃物体(有重叠+在允许间隔内) var matcheds = activeObjects.Where(o => IsOverlapping(o, region) && (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP).ToList(); //当有多个可合并的活跃物体时,将多个物体合并 if (matcheds.Count >= 2) { // 合并有效区域队列 var CopeRowsData = new List(); matcheds.ForEach(o => CopeRowsData = CopeRowsData.Concat(o.RowsData).ToList()); // 合并有效区域并保存在新的区域中 var MergeMatched = new ActiveObjectClass { MinStartCol = matcheds.Min(o => o.MinStartCol), MaxEndCol = matcheds.Max(o => o.MaxEndCol), StartLine = matcheds.Min(o => o.StartLine), LastSeenLine = matcheds.Max(o => o.LastSeenLine), LastSeenLineStartCol = matcheds.Min(o => o.LastSeenLineStartCol), LastSeenLineEndCol = matcheds.Max(o => o.LastSeenLineEndCol), StartCheckTime = matcheds.Min(o => o.StartCheckTime), EndCheckTime = matcheds.Max(o => o.EndCheckTime), Area = matcheds.Sum(o => o.Area), RowsData = CopeRowsData, ImageWidth = matcheds.FirstOrDefault().ImageWidth, //StateCode = 8 }; // 从活跃区域中删除被合并的区域 matcheds.ForEach(o => activeObjects.Remove(o)); // 保存新的区域到活跃区域中 activeObjects.Add(MergeMatched); } // 搜获可用且可合并的活跃区域 var matched = activeObjects.FirstOrDefault(o => IsOverlapping(o, region) && (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP); if (matched != null) { // 合并区域:扩展物体边界并更新状态 matched.MinStartCol = Math.Min(matched.MinStartCol, region.Start); matched.MaxEndCol = Math.Max(matched.MaxEndCol, region.End); matched.Area += region.End - region.Start + 1; matched.LastSeenLine = currentLine; matched.RowsData.Add(new RowStartEndCol { StartCol = region.Start, EndCol = region.End, RowsCol = currentLine, }); } else { // 创建新物体(首次出现的区域) activeObjects.Add(new ActiveObjectClass { MinStartCol = region.Start, MaxEndCol = region.End, StartLine = currentLine, LastSeenLine = currentLine, LastSeenLineStartCol = region.Start, LastSeenLineEndCol = region.End, StartCheckTime = DateTime.Now, Area = region.End - region.Start + 1, ImageWidth = IdentifyImageWidth, RowsData = new List { new RowStartEndCol { StartCol = region.Start, EndCol = region.End, RowsCol = currentLine, } } }); } } currentRegions.Clear(); // 更新有效物体的最后一行的起始点 activeObjects.Where(o => o.LastSeenLine == currentLine).ToList().ForEach(o => o.LastSeenLineStartCol = o.RowsData.Where(p => p.RowsCol == currentLine).Min(p => p.StartCol)); activeObjects.Where(o => o.LastSeenLine == currentLine).ToList().ForEach(o => o.LastSeenLineEndCol = o.RowsData.Where(p => p.RowsCol == currentLine).Max(p => p.EndCol)); // 步骤3:清理超时未更新的物体 var lostObjects = activeObjects .Where(o => (currentLine - o.LastSeenLine) > shuLiConfig.MAX_GAP || (o.LastSeenLine - o.StartLine) > shuLiConfig.MAX_Idetify_Height) .ToList(); List OneActive = new List(); // 有物体转变为活跃物体,返回值转为true if (lostObjects.Count() > 0) { result = true; foreach (var item in lostObjects) { //噪点判定 if (item.Area < shuLiConfig.NoiseFilter_Threshold) { item.StateCode = 9; continue; } //转为历史物体,添加缺少的参数 item.Num = ObjectNum += 1; item.ChannelNO = ActiveChannel(item); item.EndCheckTime = DateTime.Now; OneActive.Add(item); item.MaxLength = GetActionMaxLength(item.RowsData); if ((item.LastSeenLine - item.StartLine) > shuLiConfig.MAX_Idetify_Height) { item.StateCode = 7; FaultLog.RecordLogMessage("ShuLiClass-ProcessLine:非颗粒,视野异常", 3); LOG.log(string.Format("ShuLiClass-ProcessLine:非颗粒,视野异常"), 6); Console.WriteLine("ShuLiClass-ProcessLine:非颗粒,视野异常"); } else if (shuLiConfig.PandingCode != -1) { if (item.StateCode != -1) { if (item.StateCode == 8) { LOG.log(string.Format("颗粒编号{0}:疑似叠片或缺损", item.Num)); } } else if (item.Area < shuLiConfig.MinArea && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1)) { item.StateCode = 5; LOG.log(string.Format("颗粒编号{0}:面积过小", item.Num)); Console.WriteLine("颗粒编号{0}:面积过小", item.Num); } else if (item.Area > shuLiConfig.MaxArea && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1)) { item.StateCode = 6; LOG.log(string.Format("颗粒编号{0}:面积过大", item.Num)); Console.WriteLine("颗粒编号{0}:面积过大", item.Num); } else if (item.MaxLength < shuLiConfig.MIN_Object_LENGTH && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0)) { item.StateCode = 2; LOG.log(string.Format("颗粒编号{0}:超短粒", item.Num)); Console.WriteLine("颗粒编号{0}:超短粒", item.Num); } else if (item.MaxLength > shuLiConfig.MAX_Object_LENGTH && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0)) { item.StateCode = 1; LOG.log(string.Format("颗粒编号{0}:超长粒", item.Num)); Console.WriteLine("颗粒编号{0}:超长粒", item.Num); } //else if (item.LastSeenLine - item.StartLine < shuLiConfig.MIN_OBJECT_HEIGHT // && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0)) //{ // item.StateCode = 2; // LOG.log(string.Format("颗粒编号{0}:超短粒", item.Num)); // Console.WriteLine("颗粒编号{0}:超短粒", item.Num); //} //else if (item.LastSeenLine - item.StartLine > shuLiConfig.MAX_OBJECT_HEIGHT // && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0)) //{ // item.StateCode = 1; // LOG.log(string.Format("颗粒编号{0}:超长粒", item.Num)); // Console.WriteLine("颗粒编号{0}:超长粒", item.Num); //} //else if (item.RowsData.Max(o => o.EndCol - o.StartCol) > shuLiConfig.MAX_OBJECT_WIDTH // && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0)) //{ // item.StateCode = 3; // LOG.log(string.Format("颗粒编号{0}:超宽粒", item.Num)); // Console.WriteLine("颗粒编号{0}:超宽粒", item.Num); //} //else if (item.RowsData.Max(o => o.EndCol - o.StartCol) < shuLiConfig.MIN_OBJECT_WIDTH // && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0)) //{ // item.StateCode = 4; // LOG.log(string.Format("颗粒编号{0}:超窄粒", item.Num)); // Console.WriteLine("颗粒编号{0}:超窄粒", item.Num); //} else { item.StateCode = 0; LOG.log(string.Format("颗粒编号{0}:正常粒", item.Num)); Console.WriteLine("颗粒编号{0}:正常粒", item.Num); } } } if (OneActive.Count > 0) { //触发回调事件 Task.Run(() => { OnWorkCompleted(OneActive); }); } } else { OneActive = null; } // 累加到总数并从活跃物体转移到历史物体 lostObjects.Where(o => o.Area >= shuLiConfig.NoiseFilter_Threshold && o.StateCode != 7 && o.StateCode != 9).ToList().ForEach(o => TryAdd(historyActiveObjects, o, 2000)); lostObjects.ForEach(o => activeObjects.Remove(o)); } return result; } /// /// 检测有效物体区域(横向连续黑色像素段) /// /// 当前行像素数组 /// 有效区域列表(起始/结束位置) private List<(int Start, int End)> FindValidRegions(IImage image, int RowNo) { List<(int Start, int End)> regions = new List<(int Start, int End)>(); int start = -1; // 当前区域起始标记 // 遍历所有像素列 if (shuLiConfig.IsIdentifyRoiOpen) { for (int i = (int)image.Width * RowNo + shuLiConfig.IdentifyStartX; i < (int)image.Width * RowNo + shuLiConfig.IdentifyStopX; i++) { if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素 { if (start == -1) start = i % (int)image.Width; // 开始新区域 } else if (start != -1) // 遇到白色像素且存在进行中的区域 { // 检查区域宽度是否达标 if (i - start >= shuLiConfig.MIN_OBJECT_WIDTH) { regions.Add((start, (i - 1) % (int)image.Width)); // 记录有效区域 } start = -1; // 重置区域标记 } } } else { for (int i = (int)image.Width * RowNo; i < (int)image.Width * (RowNo + 1); i++) { if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素 { if (start == -1) start = i % (int)image.Width; // 开始新区域 } else if (start != -1) // 遇到白色像素且存在进行中的区域 { // 检查区域宽度是否达标 if (i - start >= shuLiConfig.MIN_OBJECT_WIDTH) { regions.Add((start, (i - 1) % (int)image.Width)); // 记录有效区域 } start = -1; // 重置区域标记 } } } // 处理行尾未闭合的区域 if (start != -1 && image.Width - start >= shuLiConfig.MIN_OBJECT_WIDTH) { regions.Add((start, (int)image.Width - 1)); } return regions; } /// /// 判断区域重叠(与活跃物体的横向坐标重叠检测) /// /// 活跃物体 /// 当前区域 /// 是否发生重叠 private bool IsOverlapping(ActiveObjectClass obj, (int Start, int End) region) { // 判断区域是否不相交的逆条件 return !(region.End < obj.LastSeenLineStartCol || region.Start > obj.LastSeenLineEndCol); } /// /// 通道区域判定 /// /// /// private int ActiveChannel(ActiveObjectClass activeObject) { int result = -1; int StartChannel = activeObject.MinStartCol / ChannelWidth; int EndChannel = activeObject.MaxEndCol / ChannelWidth; if (StartChannel == EndChannel) { result = StartChannel; } else if (EndChannel - StartChannel > 1) { Console.WriteLine("ActiveChannel-Error"); //error } else { result = _ChannelsRoi[StartChannel] - activeObject.MinStartCol > activeObject.MaxEndCol - _ChannelsRoi[StartChannel] ? StartChannel : EndChannel; } return result; } /// /// 获取结果最长长边 /// /// /// private double GetActionMaxLength(List Rows) { List points = ConvexHull(Rows); return RotatingCalipers(points); } /// /// 凸包点集合获取 /// /// /// private List ConvexHull(List Rows) { List points = Rows.Select(o => new Point(o.StartCol, (int)o.RowsCol)).ToList(); points.AddRange(Rows.Select(o => new Point(o.EndCol, (int)o.RowsCol)).ToList()); points = points.OrderBy(o => o.X).ThenBy(o => o.Y).ToList(); var lower = new List(); foreach (var p in points) { while (lower.Count >= 2 && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0) lower.RemoveAt(lower.Count - 1); lower.Add(p); } var upper = new List(); for (int i = points.Count - 1; i >= 0; i--) { var p = points[i]; while (upper.Count >= 2 && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0) upper.RemoveAt(upper.Count - 1); upper.Add(p); } lower.RemoveAt(lower.Count - 1); upper.RemoveAt(upper.Count - 1); lower.AddRange(upper); return lower; } /// /// 凸包最长边 /// /// /// private double RotatingCalipers(List hull) { int n = hull.Count; if (n == 1) return 0; if (n == 2) return Distance(hull[0], hull[1]); int k = 1; double maxDist = 0; for (int i = 0; i < n; i++) { while (Area2(hull[i], hull[(i + 1) % n], hull[(k + 1) % n]) > Area2(hull[i], hull[(i + 1) % n], hull[k])) { k = (k + 1) % n; } double currentDist = DistanceToLine(hull[i], hull[(i + 1) % n], hull[k]); maxDist = Math.Max(maxDist, currentDist); } return maxDist; } private double DistanceToLine(Point a, Point b, Point c) { double area = Math.Abs(Area2(a, b, c)); double baseLength = Distance(a, b); return area / baseLength; } // 计算向量叉积 private int Cross(Point o, Point a, Point b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); // 计算三角形面积的两倍 private int Area2(Point a, Point b, Point c) => (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X); // 计算两点间距离 private double Distance(Point a, Point b) { int dx = a.X - b.X; int dy = a.Y - b.Y; return Math.Sqrt(dx * dx + dy * dy); } private bool TryAdd(List list, ActiveObjectClass item, int maxSize) { list.Add(item); if (list.Count > maxSize) { list[list.Count - 2000].RowsData.Clear(); } return true; } #endregion #region 线程方法 /// /// 识别图像线程 /// private void IdentifyImageProcess() { //Stopwatch stopwatch = Stopwatch.StartNew(); IImage IframeData = null; while (IsIdentify) { //判断队列中是否有数据 if (IFrameDatas.Count() > 0) { //stopwatch.Restart(); IFrameDatas.TryDequeue(out IframeData); //是否成功取得数据 if (IframeData != null) { //识别 ProcessImageSequence(IframeData); } else { Console.WriteLine("识别数据为空"); continue; } //输出耗时 //stopwatch.Stop(); //Console.WriteLine($"识别线程识别一张图片耗时:{stopwatch.Elapsed.ToString()},待识别队列剩余数量{IFrameDatas.Count()}"); } else { Thread.Sleep(1); } } } #endregion } }