ShuLiClass.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. using CCDCount.MODEL.ConfigModel;
  2. using CCDCount.MODEL.ShuLiClass;
  3. using LogClass;
  4. using MvCameraControl;
  5. using System;
  6. using System.Collections.Concurrent;
  7. using System.Collections.Generic;
  8. using System.Diagnostics;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Threading;
  12. namespace CCDCount.DLL
  13. {
  14. public class ShuLiClass
  15. {
  16. #region 变量
  17. /// <summary>
  18. /// 当活跃物体转变为历史物体时的回调事件
  19. /// </summary>
  20. public event EventHandler<ActiveObjectEventArgsClass> WorkCompleted;
  21. private List<ActiveObjectClass> activeObjects = new List<ActiveObjectClass>(); // 当前跟踪中的物体
  22. private List<ActiveObjectClass> historyActiveObjects = new List<ActiveObjectClass>(); // 历史物体
  23. private ConcurrentQueue<IImage> IFrameDatas = new ConcurrentQueue<IImage>(); //图像数据队列
  24. private Thread IdentifyImageProcessThread = null; // 识别线程
  25. private bool IsIdentify = false; //线程是否开始识别的标志
  26. private long currentLine = 0; //行数记录
  27. private ShuLiConfigClass shuLiConfig = null;// 数粒参数配置文件
  28. private List<int> ChannelsRoi = new List<int>();
  29. private int ChannelWidth = 0;//每个区域的宽度
  30. private int IdentifyImageWidth = -1;
  31. private static readonly object _lockObj = new object(); // 专用锁对象\
  32. private int ObjectNum = 0;
  33. public int ImageNum { get { return IFrameDatas.Count; } }
  34. #endregion
  35. #region 公共方法
  36. /// <summary>
  37. /// 初始化构造方法
  38. /// </summary>
  39. public ShuLiClass()
  40. {
  41. // 加载默认参数
  42. shuLiConfig = new ShuLiConfigClass()
  43. {
  44. Channel = 8,
  45. PandingCode = 2
  46. };
  47. }
  48. public ShuLiClass(ShuLiConfigClass config)
  49. {
  50. if(config.IsLoadCanfig)
  51. {
  52. // 加载传出的参数
  53. shuLiConfig = config;
  54. }
  55. else
  56. {
  57. // 加载默认参数
  58. shuLiConfig = new ShuLiConfigClass()
  59. {
  60. Channel = 8,
  61. PandingCode = 2
  62. };
  63. }
  64. }
  65. /// <summary>
  66. /// 处理图像序列的主入口
  67. /// </summary>
  68. /// <param name="image">图像像素数据</param>
  69. /// <param name="ImageWidth">图像宽</param>
  70. /// <param name="currentLine">当前行数</param>
  71. /// <returns>检测到的物体总数</returns>
  72. public bool ProcessImageSequence(IImage image)
  73. {
  74. bool result = false;
  75. for (int i = 0;i<image.Height;i++)
  76. {
  77. result = ProcessLine(image,i);
  78. currentLine += 1;
  79. }
  80. return result;
  81. }
  82. /// <summary>
  83. /// 返回最后一个历史物品
  84. /// </summary>
  85. /// <returns></returns>
  86. public ActiveObjectClass GetLastActive()
  87. {
  88. if (historyActiveObjects.Count() == 0)
  89. return null;
  90. return historyActiveObjects.Last();
  91. }
  92. /// <summary>
  93. /// 返回历史物品
  94. /// </summary>
  95. /// <returns></returns>
  96. public List<ActiveObjectClass> GetHistoryActive()
  97. {
  98. lock (_lockObj) // 加锁
  99. {
  100. return historyActiveObjects.ToList();
  101. }
  102. }
  103. /// <summary>
  104. /// 返回缓存在内存的历史物品的总数量
  105. /// </summary>
  106. /// <returns></returns>
  107. public int GetHistoryActiveNum()
  108. {
  109. lock (_lockObj) // 加锁
  110. return historyActiveObjects.Count();
  111. }
  112. /// <summary>
  113. /// 清除缓存的内存的中的历史数据
  114. /// </summary>
  115. public void ClearHistoryActive()
  116. {
  117. historyActiveObjects.Clear();
  118. }
  119. /// <summary>
  120. /// 获取历史数据中,正常数据数量
  121. /// </summary>
  122. /// <returns></returns>
  123. public int GetOkHistoryNum()
  124. {
  125. lock (_lockObj)
  126. return historyActiveObjects.Where(o=>o.StateCode == 0).Count();
  127. }
  128. /// <summary>
  129. /// 获取历史数据中,异常数据数量
  130. /// </summary>
  131. /// <returns></returns>
  132. public int GetNgHistoryNum()
  133. {
  134. lock (_lockObj)
  135. return historyActiveObjects.Where(o=>o.StateCode != 0).Count();
  136. }
  137. /// <summary>
  138. /// 开启识别
  139. /// </summary>
  140. public void StartIdentifyFuntion(int ImaageWidth)
  141. {
  142. UpdateIdentifyImageWidth(ImaageWidth);
  143. InitChannel();
  144. try
  145. {
  146. // 标志位置位true
  147. IsIdentify = true;
  148. // 打开识别线程
  149. IdentifyImageProcessThread = new Thread(IdentifyImageProcess)
  150. {
  151. Priority = ThreadPriority.Highest
  152. };
  153. IdentifyImageProcessThread.Start();
  154. }
  155. catch (Exception ex)
  156. {
  157. LOG.error("Start thread failed!, " + ex.Message);
  158. throw;
  159. }
  160. }
  161. /// <summary>
  162. /// 关闭识别
  163. /// </summary>
  164. public void StopIdentifyFuntion()
  165. {
  166. try
  167. {
  168. // 标志位设为false
  169. IsIdentify = false;
  170. if(IdentifyImageProcessThread!=null&& IdentifyImageProcessThread.IsAlive)
  171. IdentifyImageProcessThread.Join();
  172. }
  173. catch (Exception ex)
  174. {
  175. LOG.error("Stop thread failed!, " + ex.Message);
  176. throw;
  177. }
  178. }
  179. /// <summary>
  180. /// 向识别队列添加一个数据
  181. /// </summary>
  182. /// <param name="items"></param>
  183. public void SetOnceIdentifyImageData(IImage items)
  184. {
  185. IFrameDatas.Enqueue(items.Clone() as IImage);
  186. }
  187. /// <summary>
  188. /// 保存参数
  189. /// </summary>
  190. public void SaveConfig()
  191. {
  192. if(!Directory.Exists(".\\Config\\")) Directory.CreateDirectory(".\\Config\\");
  193. XmlStorage.SerializeToXml(shuLiConfig, ".\\Config\\ShuLiConfig.xml");
  194. }
  195. /// <summary>
  196. /// 更新检测宽度信息
  197. /// </summary>
  198. /// <param name="Width"></param>
  199. public void UpdateIdentifyImageWidth(int Width)
  200. {
  201. IdentifyImageWidth = Width;
  202. }
  203. /// <summary>
  204. /// 初始化通道划分
  205. /// </summary>
  206. /// <param name="ImageWidth"></param>
  207. public void InitChannel()
  208. {
  209. shuLiConfig.ImageWidth = IdentifyImageWidth;
  210. if (shuLiConfig.Channel > 0)
  211. {
  212. if (shuLiConfig.IsIdentifyRoiOpen)
  213. {
  214. ChannelWidth = (shuLiConfig.IdentifyStopX - shuLiConfig.IdentifyStartX) / shuLiConfig.Channel;
  215. }
  216. else
  217. {
  218. ChannelWidth = shuLiConfig.ImageWidth / shuLiConfig.Channel;
  219. }
  220. for (int i = 0; i < shuLiConfig.Channel; i++)
  221. {
  222. ChannelsRoi.Add(ChannelWidth + i * ChannelWidth);
  223. }
  224. }
  225. }
  226. /// <summary>
  227. /// 获取配置信息
  228. /// </summary>
  229. /// <returns></returns>
  230. public ShuLiConfigClass GetConfigValue()
  231. {
  232. ShuLiConfigClass result = shuLiConfig;
  233. return result;
  234. }
  235. #endregion
  236. #region 私有方法
  237. /// <summary>
  238. /// 对外通知事件
  239. /// </summary>
  240. private void OnWorkCompleted(List<ActiveObjectClass> activeObject)
  241. {
  242. ActiveObjectEventArgsClass activeObjectEventArgs = new ActiveObjectEventArgsClass(activeObject);
  243. // 触发事件
  244. WorkCompleted?.Invoke(this, activeObjectEventArgs);
  245. }
  246. /// <summary>
  247. /// 处理单行像素数据
  248. /// 返回值为false的时候无活跃物体转变为历史物体
  249. /// 返回值为true的时候有活跃物体转变为历史物体
  250. /// </summary>
  251. /// <param name="image">当前行像素数组</param>
  252. private bool ProcessLine(IImage imagedata,int RowNo)
  253. {
  254. bool result = false;
  255. // 步骤1:检测当前行的有效区域
  256. var currentRegions = FindValidRegions(imagedata,RowNo);
  257. if (currentRegions.Count == 1)
  258. {
  259. if (currentRegions[0].End - (currentRegions[0]).Start + 1 == imagedata.Width)
  260. {
  261. LOG.error("当前行有效区域为整行,检查视野和光源");
  262. return false;
  263. }
  264. }
  265. // 步骤2:处理当前行每个区域
  266. for (int i = 0; i < currentRegions.Count; i++)
  267. {
  268. var region = currentRegions[i];
  269. // 查找全部可合并的活跃物体(有重叠+在允许间隔内)
  270. var matcheds = activeObjects.Where(o =>
  271. IsOverlapping(o, region) &&
  272. (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP).ToList();
  273. //当有多个可合并的活跃物体时,将多个物体合并
  274. if (matcheds.Count >= 2)
  275. {
  276. // 合并有效区域队列
  277. var CopeRowsData = new List<RowStartEndCol>();
  278. matcheds.ForEach(o => CopeRowsData = CopeRowsData.Concat(o.RowsData).ToList());
  279. // 合并有效区域并保存在新的区域中
  280. var MergeMatched = new ActiveObjectClass
  281. {
  282. MinStartCol = matcheds.Min(o => o.MinStartCol),
  283. MaxEndCol = matcheds.Max(o => o.MaxEndCol),
  284. StartLine = matcheds.Min(o => o.StartLine),
  285. LastSeenLine = matcheds.Max(o => o.LastSeenLine),
  286. LastSeenLineStartCol = matcheds.Min(o => o.LastSeenLineStartCol),
  287. LastSeenLineEndCol = matcheds.Max(o => o.LastSeenLineEndCol),
  288. StartCheckTime = matcheds.Min(o => o.StartCheckTime),
  289. EndCheckTime = matcheds.Max(o => o.EndCheckTime),
  290. Area = matcheds.Sum(o => o.Area),
  291. RowsData = CopeRowsData,
  292. ImageWidth = matcheds.FirstOrDefault().ImageWidth,
  293. };
  294. // 从活跃区域中删除被合并的区域
  295. matcheds.ForEach(o => activeObjects.Remove(o));
  296. // 保存新的区域到活跃区域中
  297. activeObjects.Add(MergeMatched);
  298. }
  299. // 搜获可用且可合并的活跃区域
  300. var matched = activeObjects.FirstOrDefault(o =>
  301. IsOverlapping(o, region) &&
  302. (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP);
  303. if (matched != null)
  304. {
  305. // 合并区域:扩展物体边界并更新状态
  306. matched.MinStartCol = Math.Min(matched.MinStartCol, region.Start);
  307. matched.MaxEndCol = Math.Max(matched.MaxEndCol, region.End);
  308. matched.Area += region.End - region.Start + 1;
  309. matched.LastSeenLine = currentLine;
  310. matched.RowsData.Add(new RowStartEndCol
  311. {
  312. StartCol = region.Start,
  313. EndCol = region.End,
  314. RowsCol = currentLine,
  315. });
  316. }
  317. else
  318. {
  319. // 创建新物体(首次出现的区域)
  320. activeObjects.Add(new ActiveObjectClass
  321. {
  322. MinStartCol = region.Start,
  323. MaxEndCol = region.End,
  324. StartLine = currentLine,
  325. LastSeenLine = currentLine,
  326. LastSeenLineStartCol = region.Start,
  327. LastSeenLineEndCol = region.End,
  328. StartCheckTime = DateTime.Now,
  329. Area = region.End - region.Start + 1,
  330. ImageWidth = IdentifyImageWidth,
  331. RowsData = new List<RowStartEndCol> {
  332. new RowStartEndCol {
  333. StartCol = region.Start,
  334. EndCol = region.End,
  335. RowsCol = currentLine,
  336. }
  337. }
  338. });
  339. }
  340. }
  341. currentRegions.Clear();
  342. // 更新有效物体的最后一行的起始点
  343. activeObjects.Where(o => o.LastSeenLine == currentLine).ToList().ForEach(o => o.LastSeenLineStartCol = o.RowsData.Where(p => p.RowsCol == currentLine).Min(p => p.StartCol));
  344. activeObjects.Where(o => o.LastSeenLine == currentLine).ToList().ForEach(o => o.LastSeenLineEndCol = o.RowsData.Where(p => p.RowsCol == currentLine).Max(p => p.EndCol));
  345. // 步骤3:清理超时未更新的物体
  346. var lostObjects = activeObjects
  347. .Where(o => (currentLine - o.LastSeenLine) > shuLiConfig.MAX_GAP || (o.LastSeenLine - o.StartLine) > shuLiConfig.MAX_Idetify_Height)
  348. .ToList();
  349. List<ActiveObjectClass> OneActive = new List<ActiveObjectClass>();
  350. // 有物体转变为活跃物体,返回值转为true
  351. if (lostObjects.Count() > 0)
  352. {
  353. result = true;
  354. foreach (var item in lostObjects)
  355. {
  356. //噪点判定
  357. if (item.LastSeenLine - item.StartLine < shuLiConfig.NoiseFilter_Threshold ||
  358. item.RowsData.Max(o => o.EndCol - o.StartCol) < shuLiConfig.NoiseFilter_Threshold)
  359. continue;
  360. //转为历史物体,添加缺少的参数
  361. item.Num = ObjectNum += 1;
  362. item.ChannelNO = ActiveChannel(item);
  363. item.EndCheckTime = DateTime.Now;
  364. OneActive.Add(item);
  365. if ((item.LastSeenLine - item.StartLine) > shuLiConfig.MAX_Idetify_Height)
  366. {
  367. item.StateCode = 7;
  368. LOG.error("ShuLiClass-ProcessLine:非颗粒,视野异常");
  369. Console.WriteLine("ShuLiClass-ProcessLine:非颗粒,视野异常");
  370. }
  371. else if (shuLiConfig.PandingCode != -1)
  372. {
  373. if (item.Area < shuLiConfig.MinArea
  374. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1))
  375. {
  376. item.StateCode = 5;
  377. LOG.log(string.Format("颗粒编号{0}:面积过小", item.Num));
  378. Console.WriteLine("颗粒编号{0}:面积过小", item.Num);
  379. }
  380. else if (item.Area > shuLiConfig.MaxArea
  381. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1))
  382. {
  383. item.StateCode = 6;
  384. LOG.log(string.Format("颗粒编号{0}:面积过大", item.Num));
  385. Console.WriteLine("颗粒编号{0}:面积过大", item.Num);
  386. }
  387. else if (item.LastSeenLine - item.StartLine < shuLiConfig.MIN_OBJECT_HEIGHT
  388. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  389. {
  390. item.StateCode = 2;
  391. LOG.log(string.Format("颗粒编号{0}:超短粒", item.Num));
  392. Console.WriteLine("颗粒编号{0}:超短粒", item.Num);
  393. }
  394. else if (item.LastSeenLine - item.StartLine > shuLiConfig.MAX_OBJECT_HEIGHT
  395. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  396. {
  397. item.StateCode = 1;
  398. LOG.log(string.Format("颗粒编号{0}:超长粒", item.Num));
  399. Console.WriteLine("颗粒编号{0}:超长粒", item.Num);
  400. }
  401. else if (item.RowsData.Max(o => o.EndCol - o.StartCol) > shuLiConfig.MAX_OBJECT_WIDTH
  402. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  403. {
  404. item.StateCode = 3;
  405. LOG.log(string.Format("颗粒编号{0}:超宽粒", item.Num));
  406. Console.WriteLine("颗粒编号{0}:超宽粒", item.Num);
  407. }
  408. else if (item.RowsData.Max(o => o.EndCol - o.StartCol) < shuLiConfig.MIN_OBJECT_WIDTH
  409. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  410. {
  411. item.StateCode = 4;
  412. LOG.log(string.Format("颗粒编号{0}:超窄粒", item.Num));
  413. Console.WriteLine("颗粒编号{0}:超窄粒", item.Num);
  414. }
  415. else
  416. {
  417. item.StateCode = 0;
  418. LOG.log(string.Format("颗粒编号{0}:正常粒", item.Num));
  419. Console.WriteLine("颗粒编号{0}:正常粒", item.Num);
  420. }
  421. }
  422. }
  423. if (OneActive.Count > 0)
  424. //触发回调事件
  425. OnWorkCompleted(OneActive);
  426. }
  427. else
  428. {
  429. OneActive = null;
  430. }
  431. lock (_lockObj)
  432. {
  433. // 累加到总数并从活跃物体转移到历史物体
  434. lostObjects.Where(o => o.LastSeenLine - o.StartLine >= shuLiConfig.NoiseFilter_Threshold && o.StateCode != 7).ToList().ForEach(o => historyActiveObjects.Add(o));
  435. lostObjects.ForEach(o => activeObjects.Remove(o));
  436. lostObjects.ForEach(o => historyActiveObjects.Where(P => P.Num == o.Num - 100).ToList().ForEach(P => P.RowsData.Clear()));
  437. }
  438. return result;
  439. }
  440. /// <summary>
  441. /// 检测有效物体区域(横向连续黑色像素段)
  442. /// </summary>
  443. /// <param name="line">当前行像素数组</param>
  444. /// <returns>有效区域列表(起始/结束位置)</returns>
  445. private List<(int Start, int End)> FindValidRegions(IImage image,int RowNo)
  446. {
  447. List<(int Start, int End)> regions = new List<(int Start, int End)>();
  448. int start = -1; // 当前区域起始标记
  449. // 遍历所有像素列
  450. if (shuLiConfig.IsIdentifyRoiOpen)
  451. {
  452. for (int i = (int)image.Width*RowNo + shuLiConfig.IdentifyStartX; i < (int)image.Width * RowNo + shuLiConfig.IdentifyStopX; i++)
  453. {
  454. if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素
  455. {
  456. if (start == -1) start = i%(int)image.Width; // 开始新区域
  457. }
  458. else if (start != -1) // 遇到白色像素且存在进行中的区域
  459. {
  460. // 检查区域宽度是否达标
  461. if (i - start >= shuLiConfig.MIN_OBJECT_WIDTH)
  462. {
  463. regions.Add((start, (i - 1)% (int)image.Width)); // 记录有效区域
  464. }
  465. start = -1; // 重置区域标记
  466. }
  467. }
  468. }
  469. else
  470. {
  471. for (int i = (int)image.Width * RowNo; i < (int)image.Width * (RowNo+1); i++)
  472. {
  473. if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素
  474. {
  475. if (start == -1) start = i % (int)image.Width; // 开始新区域
  476. }
  477. else if (start != -1) // 遇到白色像素且存在进行中的区域
  478. {
  479. // 检查区域宽度是否达标
  480. if (i - start >= shuLiConfig.MIN_OBJECT_WIDTH)
  481. {
  482. regions.Add((start, (i - 1) % (int)image.Width)); // 记录有效区域
  483. }
  484. start = -1; // 重置区域标记
  485. }
  486. }
  487. }
  488. // 处理行尾未闭合的区域
  489. if (start != -1 && image.Width - start >= shuLiConfig.MIN_OBJECT_WIDTH)
  490. {
  491. regions.Add((start, (int)image.Width - 1));
  492. }
  493. return regions;
  494. }
  495. /// <summary>
  496. /// 判断区域重叠(与活跃物体的横向坐标重叠检测)
  497. /// </summary>
  498. /// <param name="obj">活跃物体</param>
  499. /// <param name="region">当前区域</param>
  500. /// <returns>是否发生重叠</returns>
  501. private bool IsOverlapping(ActiveObjectClass obj, (int Start, int End) region)
  502. {
  503. // 判断区域是否不相交的逆条件
  504. return !(region.End < obj.LastSeenLineStartCol || region.Start > obj.LastSeenLineEndCol);
  505. }
  506. /// <summary>
  507. /// 通道区域判定
  508. /// </summary>
  509. /// <param name="activeObject"></param>
  510. /// <returns></returns>
  511. private int ActiveChannel(ActiveObjectClass activeObject)
  512. {
  513. int result = -1;
  514. int StartChannel = activeObject.MinStartCol / ChannelWidth;
  515. int EndChannel = activeObject.MaxEndCol / ChannelWidth;
  516. if (StartChannel == EndChannel)
  517. {
  518. result = StartChannel;
  519. }
  520. else if (EndChannel - StartChannel>1)
  521. {
  522. Console.WriteLine("ActiveChannel-Error");
  523. //error
  524. }
  525. else
  526. {
  527. result = ChannelsRoi[StartChannel] - activeObject.MinStartCol > activeObject.MaxEndCol - ChannelsRoi[StartChannel]? StartChannel: EndChannel;
  528. }
  529. return result;
  530. }
  531. #endregion
  532. #region 线程方法
  533. /// <summary>
  534. /// 识别图像线程
  535. /// </summary>
  536. private void IdentifyImageProcess()
  537. {
  538. //Stopwatch stopwatch = Stopwatch.StartNew();
  539. IImage IframeData = null;
  540. while (IsIdentify)
  541. {
  542. //判断队列中是否有数据
  543. if (IFrameDatas.Count() > 0)
  544. {
  545. //stopwatch.Restart();
  546. IFrameDatas.TryDequeue(out IframeData);
  547. //是否成功取得数据
  548. if (IframeData != null)
  549. {
  550. //识别
  551. ProcessImageSequence(IframeData);
  552. }
  553. else
  554. {
  555. Console.WriteLine("识别数据为空");
  556. continue;
  557. }
  558. //输出耗时
  559. //stopwatch.Stop();
  560. ///Console.WriteLine("识别线程单次运行耗时:" + stopwatch.Elapsed.ToString());
  561. }
  562. else
  563. {
  564. Thread.Sleep(1);
  565. }
  566. }
  567. }
  568. #endregion
  569. }
  570. }