ShuLiClass.cs 35 KB

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