ShuLiClass.cs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. using CCDCount.DLL.AlarmTools;
  2. using CCDCount.MODEL.ConfigModel;
  3. using CCDCount.MODEL.ShuLiModel;
  4. using LogClass;
  5. using MvCameraControl;
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Collections.Generic;
  9. using System.Diagnostics;
  10. using System.Drawing;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Runtime.InteropServices.WindowsRuntime;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. namespace CCDCount.DLL
  17. {
  18. public class ShuLiClass
  19. {
  20. #region 变量
  21. /// <summary>
  22. /// 当活跃物体转变为历史物体时的回调事件
  23. /// </summary>
  24. public event EventHandler<ActiveObjectEventArgsClass> WorkCompleted;
  25. private List<ActiveObjectClass> activeObjects = new List<ActiveObjectClass>(); // 当前跟踪中的物体
  26. private List<ActiveObjectClass> historyActiveObjects = new List<ActiveObjectClass>(); // 历史物体
  27. private ConcurrentQueue<IImage> IFrameDatas = new ConcurrentQueue<IImage>(); //图像数据队列
  28. private Thread IdentifyImageProcessThread = null; // 识别线程
  29. private bool IsIdentify = false; //线程是否开始识别的标志
  30. private long currentLine = 0; //行数记录
  31. private ShuLiConfigClass shuLiConfig = null;// 数粒参数配置文件
  32. public List<int> ChannelsRoi { get { return _ChannelsRoi; } }
  33. private List<int> _ChannelsRoi = new List<int>();
  34. private int ChannelWidth = 0;//每个区域的宽度
  35. private int IdentifyImageWidth = -1;
  36. private static readonly object _lockObj = new object(); // 专用锁对象
  37. private int ObjectNum = 0;
  38. public int ImageNum { get { return IFrameDatas.Count; } }
  39. #endregion
  40. #region 公共方法
  41. /// <summary>
  42. /// 初始化构造方法
  43. /// </summary>
  44. public ShuLiClass()
  45. {
  46. // 加载默认参数
  47. shuLiConfig = new ShuLiConfigClass()
  48. {
  49. Channel = 8,
  50. PandingCode = 2
  51. };
  52. }
  53. public ShuLiClass(ShuLiConfigClass config)
  54. {
  55. if (config.IsLoadCanfig)
  56. {
  57. // 加载传出的参数
  58. shuLiConfig = config;
  59. }
  60. else
  61. {
  62. // 加载默认参数
  63. shuLiConfig = new ShuLiConfigClass()
  64. {
  65. Channel = 8,
  66. PandingCode = 2
  67. };
  68. }
  69. InitChannel();
  70. }
  71. public long InitCurrentLine(int line)
  72. {
  73. currentLine = line;
  74. return currentLine;
  75. }
  76. public int InitNum(int num)
  77. {
  78. ObjectNum = num;
  79. return ObjectNum;
  80. }
  81. /// <summary>
  82. /// 处理图像序列的主入口
  83. /// </summary>
  84. /// <param name="image">图像像素数据</param>
  85. /// <param name="ImageWidth">图像宽</param>
  86. /// <param name="currentLine">当前行数</param>
  87. /// <returns>检测到的物体总数</returns>
  88. public bool ProcessImageSequence(IImage image)
  89. {
  90. bool result = false;
  91. for (int i = 0; i < image.Height; i++)
  92. {
  93. result = ProcessLine(image, i);
  94. currentLine += 1;
  95. }
  96. //识别到结果并输出
  97. lock (_lockObj)
  98. {
  99. // 清理超时未更新的物体
  100. var lostObjects = activeObjects
  101. .Where(o => (currentLine - o.LastSeenLine) > shuLiConfig.MAX_GAP || (o.LastSeenLine - o.StartLine) > shuLiConfig.MAX_Idetify_Height)
  102. .ToList();
  103. List<ActiveObjectClass> OneActive = new List<ActiveObjectClass>();
  104. // 有物体转变为活跃物体,返回值转为true
  105. if (lostObjects.Count() > 0)
  106. {
  107. result = true;
  108. foreach (var item in lostObjects)
  109. {
  110. //噪点判定
  111. if (item.Area < shuLiConfig.NoiseFilter_Threshold)
  112. {
  113. item.StateCode = 9;
  114. //LOG.log(string.Format("噪点过滤,噪点面积:{0}", item.Area), 6);
  115. continue;
  116. }
  117. //转为历史物体,添加缺少的参数
  118. item.Num = ObjectNum += 1;
  119. item.ChannelNO = ActiveChannel(item);
  120. item.EndCheckTime = DateTime.Now;
  121. item.MaxLength = GetActionMaxLength(item.RowsData);
  122. if ((item.LastSeenLine - item.StartLine) > shuLiConfig.MAX_Idetify_Height)
  123. {
  124. item.StateCode = 7;
  125. FaultLog.RecordLogMessage("ShuLiClass-ProcessLine:非颗粒,视野异常", 3);
  126. LOG.log(string.Format("ShuLiClass-ProcessLine:非颗粒,视野异常"), 6);
  127. Console.WriteLine("ShuLiClass-ProcessLine:非颗粒,视野异常");
  128. }
  129. else if (shuLiConfig.PandingCode != -1)
  130. {
  131. if (item.StateCode != -1)
  132. {
  133. if (item.StateCode == 8)
  134. {
  135. LOG.log(string.Format("颗粒编号{0}:疑似叠片或缺损", item.Num));
  136. }
  137. }
  138. else if (item.Area < shuLiConfig.MinArea
  139. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1))
  140. {
  141. item.StateCode = 5;
  142. LOG.log(string.Format("颗粒编号{0}:面积过小", item.Num));
  143. Console.WriteLine("颗粒编号{0}:面积过小", item.Num);
  144. }
  145. else if (item.Area > shuLiConfig.MaxArea
  146. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1))
  147. {
  148. item.StateCode = 6;
  149. LOG.log(string.Format("颗粒编号{0}:面积过大", item.Num));
  150. Console.WriteLine("颗粒编号{0}:面积过大", item.Num);
  151. }
  152. else if (item.MaxLength < shuLiConfig.MIN_Object_LENGTH
  153. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  154. {
  155. item.StateCode = 2;
  156. LOG.log(string.Format("颗粒编号{0}:超短粒", item.Num));
  157. Console.WriteLine("颗粒编号{0}:超短粒", item.Num);
  158. }
  159. else if (item.MaxLength > shuLiConfig.MAX_Object_LENGTH
  160. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  161. {
  162. item.StateCode = 1;
  163. LOG.log(string.Format("颗粒编号{0}:超长粒", item.Num));
  164. Console.WriteLine("颗粒编号{0}:超长粒", item.Num);
  165. }
  166. else
  167. {
  168. item.StateCode = 0;
  169. LOG.log(string.Format("颗粒编号{0}:正常粒", item.Num));
  170. Console.WriteLine("颗粒编号{0}:正常粒", item.Num);
  171. }
  172. }
  173. OneActive.Add(item);
  174. }
  175. if (OneActive.Count > 0)
  176. {
  177. LOG.log(string.Format("识别完成,首个颗粒编号:{0},颗粒数量:{1}", OneActive[0].Num, OneActive.Count), 6);
  178. //触发回调事件
  179. Task.Run(() =>
  180. {
  181. OnWorkCompleted(OneActive);
  182. });
  183. }
  184. }
  185. else
  186. {
  187. OneActive = null;
  188. }
  189. // 累加到总数并从活跃物体转移到历史物体
  190. lostObjects.Where(o => o.Area >= shuLiConfig.NoiseFilter_Threshold && o.StateCode != 7 && o.StateCode != 9).ToList().ForEach(o => TryAdd(historyActiveObjects, o, 2500));
  191. lostObjects.ForEach(o => activeObjects.Remove(o));
  192. }
  193. return result;
  194. }
  195. /// <summary>
  196. /// 返回最后一个历史物品
  197. /// </summary>
  198. /// <returns></returns>
  199. public ActiveObjectClass GetLastActive()
  200. {
  201. if (historyActiveObjects.Count() == 0)
  202. return null;
  203. return historyActiveObjects.Last();
  204. }
  205. /// <summary>
  206. /// 返回历史物品
  207. /// </summary>
  208. /// <returns></returns>
  209. public List<ActiveObjectClass> GetHistoryActive()
  210. {
  211. lock (_lockObj) // 加锁
  212. {
  213. return historyActiveObjects.ToList();
  214. }
  215. }
  216. /// <summary>
  217. /// 返回缓存在内存的历史物品的总数量
  218. /// </summary>
  219. /// <returns></returns>
  220. public int GetHistoryActiveNum()
  221. {
  222. lock (_lockObj) // 加锁
  223. return historyActiveObjects.Count();
  224. }
  225. /// <summary>
  226. /// 获取历史数据中,正常数据数量
  227. /// </summary>
  228. /// <returns></returns>
  229. public int GetOkHistoryNum()
  230. {
  231. lock (_lockObj)
  232. return historyActiveObjects.Where(o => o.StateCode == 0).Count();
  233. }
  234. /// <summary>
  235. /// 获取历史数据中,异常数据数量
  236. /// </summary>
  237. /// <returns></returns>
  238. public int GetNgHistoryNum()
  239. {
  240. lock (_lockObj)
  241. return historyActiveObjects.Where(o => o.StateCode != 0).Count();
  242. }
  243. /// <summary>
  244. /// 清除历史数据
  245. /// </summary>
  246. /// <returns></returns>
  247. public bool ClearHistoryActive()
  248. {
  249. lock (_lockObj)
  250. {
  251. historyActiveObjects.Clear();
  252. return true;
  253. }
  254. }
  255. /// <summary>
  256. /// 开启识别
  257. /// </summary>
  258. public void StartIdentifyFuntion(int ImaageWidth)
  259. {
  260. UpdateIdentifyImageWidth(ImaageWidth);
  261. InitChannel();
  262. try
  263. {
  264. // 标志位置位true
  265. IsIdentify = true;
  266. // 打开识别线程
  267. IdentifyImageProcessThread = new Thread(IdentifyImageProcess)
  268. {
  269. Priority = ThreadPriority.Highest
  270. };
  271. IdentifyImageProcessThread.Start();
  272. SystemAlarm.AlarmCancel(AlarmMessageList.识别线程启动失败);
  273. }
  274. catch (Exception ex)
  275. {
  276. SystemAlarm.AlarmAlert(AlarmMessageList.识别线程启动失败, "Start thread failed!, " + ex.Message, "DLL:ShuLiClass-StartIdentifyFuntion");
  277. //FaultLog.RecordErrorMessage("Start thread failed!, " + ex.Message);
  278. throw;
  279. }
  280. }
  281. /// <summary>
  282. /// 关闭识别
  283. /// </summary>
  284. public void StopIdentifyFuntion()
  285. {
  286. try
  287. {
  288. // 标志位设为false
  289. IsIdentify = false;
  290. if (IdentifyImageProcessThread != null && IdentifyImageProcessThread.IsAlive)
  291. IdentifyImageProcessThread.Join();
  292. SystemAlarm.AlarmCancel(AlarmMessageList.识别线程停止失败);
  293. }
  294. catch (Exception ex)
  295. {
  296. //FaultLog.RecordErrorMessage("Stop thread failed!, " + ex.Message);
  297. SystemAlarm.AlarmAlert(AlarmMessageList.识别线程停止失败, "Stop thread failed!, " + ex.Message, "DLL:ShuLiClass-StopIdentifyFuntion");
  298. throw;
  299. }
  300. }
  301. /// <summary>
  302. /// 向识别队列添加一个数据
  303. /// </summary>
  304. /// <param name="items"></param>
  305. public void SetOnceIdentifyImageData(IImage items)
  306. {
  307. IFrameDatas.Enqueue(items.Clone() as IImage);
  308. }
  309. /// <summary>
  310. /// 保存参数
  311. /// </summary>
  312. public void SaveConfig()
  313. {
  314. if (!Directory.Exists(".\\Config\\")) Directory.CreateDirectory(".\\Config\\");
  315. XmlStorage.SerializeToXml(shuLiConfig, ".\\Config\\ShuLiConfig.xml");
  316. }
  317. /// <summary>
  318. /// 更新检测宽度信息
  319. /// </summary>
  320. /// <param name="Width"></param>
  321. public void UpdateIdentifyImageWidth(int Width)
  322. {
  323. IdentifyImageWidth = Width;
  324. }
  325. /// <summary>
  326. /// 初始化通道划分
  327. /// </summary>
  328. /// <param name="ImageWidth"></param>
  329. public void InitChannel()
  330. {
  331. _ChannelsRoi.Clear();
  332. shuLiConfig.ImageWidth = IdentifyImageWidth == -1 ? shuLiConfig.ImageWidth : IdentifyImageWidth;
  333. if (shuLiConfig.Channel > 0)
  334. {
  335. if (shuLiConfig.IsIdentifyRoiOpen)
  336. {
  337. ChannelWidth = (shuLiConfig.IdentifyStopX - shuLiConfig.IdentifyStartX) / shuLiConfig.Channel;
  338. }
  339. else
  340. {
  341. ChannelWidth = shuLiConfig.ImageWidth / shuLiConfig.Channel;
  342. }
  343. for (int i = 0; i < shuLiConfig.Channel; i++)
  344. {
  345. _ChannelsRoi.Add(ChannelWidth + i * ChannelWidth);
  346. }
  347. }
  348. }
  349. /// <summary>
  350. /// 获取配置信息
  351. /// </summary>
  352. /// <returns></returns>
  353. public ShuLiConfigClass GetConfigValue()
  354. {
  355. ShuLiConfigClass result = shuLiConfig;
  356. return result;
  357. }
  358. public int GetConfigImageWidth()
  359. {
  360. int result = -1;
  361. if (shuLiConfig != null)
  362. result = shuLiConfig.ImageWidth;
  363. return result;
  364. }
  365. #endregion
  366. #region 私有方法
  367. /// <summary>
  368. /// 对外通知事件
  369. /// </summary>
  370. private void OnWorkCompleted(List<ActiveObjectClass> activeObject)
  371. {
  372. ActiveObjectEventArgsClass activeObjectEventArgs = new ActiveObjectEventArgsClass(activeObject);
  373. // 触发事件
  374. WorkCompleted?.Invoke(this, activeObjectEventArgs);
  375. }
  376. private bool IsPrintLightOnError = false;
  377. /// <summary>
  378. /// 处理单行像素数据
  379. /// 返回值为false的时候无活跃物体转变为历史物体
  380. /// 返回值为true的时候有活跃物体转变为历史物体
  381. /// </summary>
  382. /// <param name="image">当前行像素数组</param>
  383. private bool ProcessLine(IImage imagedata, int RowNo)
  384. {
  385. bool result = false;
  386. // 步骤1:检测当前行的有效区域
  387. var currentRegions = FindValidRegions(imagedata, RowNo);
  388. if (currentRegions.Count == 1)
  389. {
  390. if (currentRegions[0].End - (currentRegions[0]).Start + 1 == imagedata.Width)
  391. {
  392. if (!IsPrintLightOnError)
  393. {
  394. FaultLog.RecordLogMessage("当前行有效区域为整行,检查视野和光源", 5);
  395. IsPrintLightOnError = true;
  396. }
  397. return false;
  398. }
  399. }
  400. IsPrintLightOnError = false;
  401. lock (_lockObj)
  402. {
  403. foreach (var region in currentRegions)
  404. {
  405. // 查找全部可合并的活跃物体(有重叠+在允许间隔内)
  406. var matcheds = activeObjects.Where(o =>
  407. IsOverlapping(o, region) &&
  408. (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP).ToList();
  409. //当有多个可合并的活跃物体时,将多个物体合并
  410. if (matcheds.Count >= 2)
  411. {
  412. // 合并有效区域队列
  413. var CopeRowsData = new List<RowStartEndCol>();
  414. matcheds.ForEach(o => CopeRowsData = CopeRowsData.Concat(o.RowsData).ToList());
  415. // 合并有效区域并保存在新的区域中
  416. var MergeMatched = new ActiveObjectClass
  417. {
  418. MinStartCol = matcheds.Min(o => o.MinStartCol),
  419. MaxEndCol = matcheds.Max(o => o.MaxEndCol),
  420. StartLine = matcheds.Min(o => o.StartLine),
  421. LastSeenLine = matcheds.Max(o => o.LastSeenLine),
  422. LastSeenLineStartCol = matcheds.Min(o => o.LastSeenLineStartCol),
  423. LastSeenLineEndCol = matcheds.Max(o => o.LastSeenLineEndCol),
  424. StartCheckTime = matcheds.Min(o => o.StartCheckTime),
  425. EndCheckTime = matcheds.Max(o => o.EndCheckTime),
  426. Area = matcheds.Sum(o => o.Area),
  427. RowsData = CopeRowsData,
  428. ImageWidth = matcheds.FirstOrDefault().ImageWidth,
  429. //StateCode = 8
  430. };
  431. // 从活跃区域中删除被合并的区域
  432. matcheds.ForEach(o => activeObjects.Remove(o));
  433. // 保存新的区域到活跃区域中
  434. activeObjects.Add(MergeMatched);
  435. }
  436. // 搜获可用且可合并的活跃区域
  437. var matched = activeObjects.FirstOrDefault(o =>
  438. IsOverlapping(o, region) &&
  439. (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP);
  440. if (matched != null)
  441. {
  442. // 合并区域:扩展物体边界并更新状态
  443. matched.MinStartCol = Math.Min(matched.MinStartCol, region.Start);
  444. matched.MaxEndCol = Math.Max(matched.MaxEndCol, region.End);
  445. matched.Area += region.End - region.Start + 1;
  446. matched.LastSeenLine = currentLine;
  447. matched.RowsData.Add(new RowStartEndCol
  448. {
  449. StartCol = region.Start,
  450. EndCol = region.End,
  451. RowsCol = currentLine,
  452. });
  453. matched.LastSeenLineStartCol = region.Start;
  454. matched.LastSeenLineEndCol = region.End;
  455. }
  456. else
  457. {
  458. // 创建新物体(首次出现的区域)
  459. activeObjects.Add(new ActiveObjectClass
  460. {
  461. MinStartCol = region.Start,
  462. MaxEndCol = region.End,
  463. StartLine = currentLine,
  464. LastSeenLine = currentLine,
  465. LastSeenLineStartCol = region.Start,
  466. LastSeenLineEndCol = region.End,
  467. StartCheckTime = DateTime.Now,
  468. Area = region.End - region.Start + 1,
  469. ImageWidth = IdentifyImageWidth,
  470. RowsData = new List<RowStartEndCol> {
  471. new RowStartEndCol {
  472. StartCol = region.Start,
  473. EndCol = region.End,
  474. RowsCol = currentLine,
  475. }
  476. }
  477. });
  478. }
  479. }
  480. }
  481. currentRegions.Clear();
  482. return result;
  483. }
  484. /// <summary>
  485. /// 检测有效物体区域(横向连续黑色像素段)
  486. /// </summary>
  487. /// <param name="line">当前行像素数组</param>
  488. /// <returns>有效区域列表(起始/结束位置)</returns>
  489. private List<ValidRegionModelClass> FindValidRegions(IImage image, int RowNo)
  490. {
  491. List<ValidRegionModelClass> regions = new List<ValidRegionModelClass>();
  492. int start = -1; // 当前区域起始标记
  493. int end = -1;
  494. // 遍历所有像素列
  495. if (shuLiConfig.IsIdentifyRoiOpen)
  496. {
  497. for (int i = (int)image.Width * RowNo + shuLiConfig.IdentifyStartX; i < (int)image.Width * RowNo + shuLiConfig.IdentifyStopX; i++)
  498. {
  499. if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素
  500. {
  501. if (start == -1) start = i % (int)image.Width; // 开始新区域
  502. }
  503. else if (start != -1) // 遇到白色像素且存在进行中的区域
  504. {
  505. end = (i - 1) % (int)image.Width;
  506. // 检查区域宽度是否达标
  507. if (end - start >= shuLiConfig.NoiseFilter_Threshold)
  508. {
  509. regions.Add(new ValidRegionModelClass()
  510. {
  511. Start = start,
  512. End = end
  513. }); // 记录有效区域
  514. }
  515. start = -1; // 重置区域标记
  516. end = -1;
  517. }
  518. }
  519. }
  520. else
  521. {
  522. for (int i = (int)image.Width * RowNo; i < (int)image.Width * (RowNo + 1); i++)
  523. {
  524. if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素
  525. {
  526. if (start == -1) start = i % (int)image.Width; // 开始新区域
  527. }
  528. else if (start != -1) // 遇到白色像素且存在进行中的区域
  529. {
  530. end = (i - 1) % (int)image.Width;
  531. // 检查区域宽度是否达标
  532. if (end - start >= shuLiConfig.NoiseFilter_Threshold)
  533. {
  534. regions.Add(new ValidRegionModelClass()
  535. {
  536. Start = start,
  537. End = end
  538. }); // 记录有效区域
  539. }
  540. start = -1; // 重置区域标记
  541. end = -1;
  542. }
  543. }
  544. }
  545. // 处理行尾未闭合的区域
  546. if (start != -1 && image.Width - start >= shuLiConfig.NoiseFilter_Threshold)
  547. {
  548. regions.Add(new ValidRegionModelClass()
  549. {
  550. Start = start,
  551. End = (int)image.Width - 1
  552. });
  553. }
  554. return regions;
  555. }
  556. /// <summary>
  557. /// 判断区域重叠(与活跃物体的横向坐标重叠检测)
  558. /// </summary>
  559. /// <param name="obj">活跃物体</param>
  560. /// <param name="region">当前区域</param>
  561. /// <returns>是否发生重叠</returns>
  562. private bool IsOverlapping(ActiveObjectClass obj, ValidRegionModelClass region)
  563. {
  564. // 判断区域是否不相交的逆条件
  565. return !(region.End < obj.LastSeenLineStartCol || region.Start > obj.LastSeenLineEndCol);
  566. }
  567. /// <summary>
  568. /// 通道区域判定
  569. /// </summary>
  570. /// <param name="activeObject"></param>
  571. /// <returns></returns>
  572. private int ActiveChannel(ActiveObjectClass activeObject)
  573. {
  574. int result = -1;
  575. int StartChannel = activeObject.MinStartCol / ChannelWidth;
  576. int EndChannel = activeObject.MaxEndCol / ChannelWidth;
  577. if (StartChannel == EndChannel)
  578. {
  579. result = StartChannel;
  580. }
  581. else if (EndChannel - StartChannel > 1)
  582. {
  583. Console.WriteLine("ActiveChannel-Error");
  584. //error
  585. }
  586. else
  587. {
  588. result = _ChannelsRoi[StartChannel] - activeObject.MinStartCol > activeObject.MaxEndCol - _ChannelsRoi[StartChannel] ? StartChannel : EndChannel;
  589. }
  590. return result;
  591. }
  592. /// <summary>
  593. /// 获取结果最长长边
  594. /// </summary>
  595. /// <param name="Rows"></param>
  596. /// <returns></returns>
  597. private double GetActionMaxLength(List<RowStartEndCol> Rows)
  598. {
  599. //Stopwatch stopwatch = Stopwatch.StartNew();
  600. List<Point> points = ConvexHull(Rows);
  601. //stopwatch.Stop();
  602. //Console.WriteLine($"凸包计算耗时:{stopwatch.Elapsed}");
  603. //var test = CalculateMinimumBoundingRectangle(points);
  604. var result = RotatingCalipers(points);
  605. if (result!= null)
  606. {
  607. return result.Height;
  608. }
  609. else
  610. {
  611. return 0;
  612. }
  613. }
  614. /// <summary>
  615. /// 凸包点集合获取
  616. /// </summary>
  617. /// <param name="Rows"></param>
  618. /// <returns></returns>
  619. private List<Point> ConvexHull(List<RowStartEndCol> Rows)
  620. {
  621. List<Point> points = Rows.Select(o => new Point(o.StartCol, (int)o.RowsCol)).ToList();
  622. points.AddRange(Rows.Select(o => new Point(o.EndCol, (int)o.RowsCol)).ToList());
  623. points = points.OrderBy(o => o.X).ThenBy(o => o.Y).ToList();
  624. var lower = new List<Point>();
  625. foreach (var p in points)
  626. {
  627. while (lower.Count >= 2 && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0)
  628. lower.RemoveAt(lower.Count - 1);
  629. lower.Add(p);
  630. }
  631. var upper = new List<Point>();
  632. for (int i = points.Count - 1; i >= 0; i--)
  633. {
  634. var p = points[i];
  635. while (upper.Count >= 2 && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0)
  636. upper.RemoveAt(upper.Count - 1);
  637. upper.Add(p);
  638. }
  639. lower.RemoveAt(lower.Count - 1);
  640. upper.RemoveAt(upper.Count - 1);
  641. lower.AddRange(upper);
  642. return lower;
  643. }
  644. /// <summary>
  645. /// 凸包最长边
  646. /// </summary>
  647. /// <param name="hull"></param>
  648. /// <returns></returns>
  649. private BoundingRectangleMdoel RotatingCalipers(List<Point> hull)
  650. {
  651. /* 老方法注释
  652. int n = hull.Count;
  653. if (n == 1) return 0;
  654. if (n == 2) return Distance(hull[0], hull[1]);
  655. int k = 1;
  656. double maxDist = 0;
  657. for (int i = 0; i < n; i++)
  658. {
  659. while (Area2(hull[i], hull[(i + 1) % n], hull[(k + 1) % n]) >
  660. Area2(hull[i], hull[(i + 1) % n], hull[k]))
  661. {
  662. k = (k + 1) % n;
  663. }
  664. double currentDist = DistanceToLine(hull[i], hull[(i + 1) % n], hull[k]);
  665. maxDist = Math.Max(maxDist, currentDist);
  666. }
  667. return maxDist;
  668. */
  669. BoundingRectangleMdoel result = null;
  670. int n = hull.Count;
  671. if (n == 1)
  672. {
  673. return result;
  674. }
  675. if (n == 2)
  676. {
  677. result = new BoundingRectangleMdoel
  678. {
  679. points = hull,
  680. CenterPoint = new Point() { X = (hull[1].X + hull[0].X) / 2, Y = (hull[1].Y + hull[0].Y) / 2 },
  681. Width = 0,
  682. Height = Distance(hull[0], hull[1]),
  683. Angle = Math.Atan2(hull[1].Y - hull[0].Y, hull[1].X - hull[0].X)
  684. };
  685. return result;
  686. }
  687. double maxDist = 0;
  688. int pointIndex1 = 0;
  689. int pointIndex2 = 0;
  690. for (int i = 0; i < n; i++)
  691. {
  692. for (int j = i + 1; j < n; j++)
  693. {
  694. if (Distance(hull[i], hull[j]) > maxDist)
  695. {
  696. maxDist = Distance(hull[i], hull[j]);
  697. pointIndex1 = i;
  698. pointIndex2 = j;
  699. }
  700. }
  701. }
  702. double angle = Math.Atan2(hull[pointIndex2].Y - hull[pointIndex1].Y, hull[pointIndex2].X - hull[pointIndex2].X);
  703. var rotatedPoints = hull.Select(p => RotatePoint(p, -angle)).ToList();
  704. // 找到包围盒
  705. int minX = rotatedPoints.Min(p => p.X);
  706. int maxX = rotatedPoints.Max(p => p.X);
  707. int minY = rotatedPoints.Min(p => p.Y);
  708. int maxY = rotatedPoints.Max(p => p.Y);
  709. int centerX = (minX + maxX) / 2;
  710. int centerY = (minY + maxY) / 2;
  711. int height = maxX - minX;
  712. int width = maxY - minY;
  713. // 构造矩形的四个顶点并旋转回原来的角度
  714. var rectangle = new List<Point>
  715. {
  716. RotatePoint(new Point(minX, minY), angle),
  717. RotatePoint(new Point(maxX, minY), angle),
  718. RotatePoint(new Point(maxX, maxY), angle),
  719. RotatePoint(new Point(minX, maxY), angle)
  720. };
  721. result = new BoundingRectangleMdoel
  722. {
  723. points = rectangle,
  724. CenterPoint = RotatePoint(new Point() { X = centerX, Y = centerY }, angle),
  725. Width = width,
  726. Height = height,
  727. Angle = angle
  728. };
  729. return result;
  730. }
  731. /// <summary>
  732. /// 计算凸包的最小外接矩形
  733. /// </summary>
  734. /// <param name="convexHull">凸包顶点列表(按逆时针顺序排列)</param>
  735. /// <returns>最小外接矩形的四个顶点</returns>
  736. public List<Point> CalculateMinimumBoundingRectangle(List<Point> convexHull)
  737. {
  738. if (convexHull == null || convexHull.Count < 3)
  739. return null;
  740. double minArea = double.MaxValue;
  741. List<Point> minRectangle = null;
  742. int n = convexHull.Count;
  743. // 遍历每一条边作为基准边
  744. for (int i = 0; i < n; i++)
  745. {
  746. Point edgeStart = convexHull[i];
  747. Point edgeEnd = convexHull[(i + 1) % n];
  748. // 计算当前边的角度
  749. double angle = Math.Atan2(edgeEnd.Y - edgeStart.Y, edgeEnd.X - edgeStart.X);
  750. // 将所有点绕原点旋转-angle角度,使当前边与x轴平行
  751. var rotatedPoints = convexHull.Select(p => RotatePoint(p, -angle)).ToList();
  752. // 找到包围盒
  753. int minX = rotatedPoints.Min(p => p.X);
  754. int maxX = rotatedPoints.Max(p => p.X);
  755. int minY = rotatedPoints.Min(p => p.Y);
  756. int maxY = rotatedPoints.Max(p => p.Y);
  757. // 计算面积
  758. double area = (maxX - minX) * (maxY - minY);
  759. // 如果面积更小,则更新最小矩形
  760. if (area < minArea)
  761. {
  762. minArea = area;
  763. // 构造矩形的四个顶点并旋转回原来的角度
  764. var rectangle = new List<Point>
  765. {
  766. RotatePoint(new Point(minX, minY), angle),
  767. RotatePoint(new Point(maxX, minY), angle),
  768. RotatePoint(new Point(maxX, maxY), angle),
  769. RotatePoint(new Point(minX, maxY), angle)
  770. };
  771. minRectangle = rectangle;
  772. }
  773. }
  774. return minRectangle;
  775. }
  776. /// <summary>
  777. /// 绕原点旋转点
  778. /// </summary>
  779. /// <param name="point">待旋转的点</param>
  780. /// <param name="angle">旋转角度(弧度)</param>
  781. /// <returns>旋转后的点</returns>
  782. private static Point RotatePoint(Point point, double angle)
  783. {
  784. double cos = Math.Cos(angle);
  785. double sin = Math.Sin(angle);
  786. return new Point(
  787. (int)(point.X * cos - point.Y * sin),
  788. (int)(point.X * sin + point.Y * cos)
  789. );
  790. }
  791. /// <summary>
  792. /// 计算点到线段的距离
  793. /// </summary>
  794. /// <param name="a">线段点1</param>
  795. /// <param name="b">线段点2</param>
  796. /// <param name="c">点三</param>
  797. /// <returns></returns>
  798. private double DistanceToLine(Point a, Point b, Point c)
  799. {
  800. double area = Math.Abs(Area2(a, b, c));
  801. double baseLength = Distance(a, b);
  802. return area / baseLength;
  803. }
  804. // 计算向量叉积
  805. private int Cross(Point o, Point a, Point b) =>
  806. (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X);
  807. // 计算三角形面积的两倍
  808. private int Area2(Point a, Point b, Point c) =>
  809. (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X);
  810. // 计算两点间距离
  811. private double Distance(Point a, Point b)
  812. {
  813. int dx = a.X - b.X;
  814. int dy = a.Y - b.Y;
  815. return Math.Sqrt(dx * dx + dy * dy);
  816. }
  817. private bool TryAdd(List<ActiveObjectClass> list, ActiveObjectClass item, int maxSize)
  818. {
  819. list.Add(item);
  820. if (list.Count > maxSize)
  821. {
  822. list[list.Count - maxSize].RowsData.Clear();
  823. }
  824. return true;
  825. }
  826. #endregion
  827. #region 线程方法
  828. /// <summary>
  829. /// 识别图像线程
  830. /// </summary>
  831. private void IdentifyImageProcess()
  832. {
  833. //Stopwatch stopwatch = Stopwatch.StartNew();
  834. while (IsIdentify)
  835. {
  836. //判断队列中是否有数据
  837. if (IFrameDatas.Count() > 0)
  838. {
  839. //stopwatch.Restart();
  840. if (IFrameDatas.Count() > 50)
  841. SystemAlarm.AlarmAlert(AlarmMessageList.待识别队列数据堆积, $"图像数据队列中数据过多,请及时处理!当前数据数量为:{IFrameDatas.Count()}", "DLL:ShuLIClass-IdentifyImageProcess");
  842. //FaultLog.RecordErrorMessage($"图像数据队列中数据过多,请及时处理!当前数据数量为:{IFrameDatas.Count()}");
  843. else
  844. SystemAlarm.AlarmCancel(AlarmMessageList.待识别队列数据堆积);
  845. IFrameDatas.TryDequeue(out IImage IframeData);
  846. //是否成功取得数据
  847. if (IframeData != null)
  848. {
  849. //识别
  850. ProcessImageSequence(IframeData);
  851. }
  852. else
  853. {
  854. Console.WriteLine("识别数据为空");
  855. continue;
  856. }
  857. //输出耗时
  858. //stopwatch.Stop();
  859. //Console.WriteLine($"识别线程识别一张图片耗时:{stopwatch.Elapsed},待识别队列剩余数量{IFrameDatas.Count()}");
  860. }
  861. else
  862. {
  863. Thread.Sleep(1);
  864. }
  865. }
  866. }
  867. #endregion
  868. }
  869. }