ShuLiClass.cs 41 KB

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