ProcessingAlgorithm_CCDShuLi.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. using MvCameraControl;
  2. using MvvmScaffoldFrame48.DLL.LogTools;
  3. using MvvmScaffoldFrame48.DLL.SystemTools;
  4. using MvvmScaffoldFrame48.DLL.ThreadManager;
  5. using MvvmScaffoldFrame48.Model.StorageModel.ImageAlgorithm.ShuLI;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Diagnostics;
  9. using System.Drawing;
  10. using System.Linq;
  11. using System.Text;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. namespace MvvmScaffoldFrame48.DLL.ImageAlgorithm
  15. {
  16. public class ProcessingAlgorithm_CCDShuLi : IImageProcessingAlgorithmHikVision
  17. {
  18. private List<ActiveObjectClassModel> activeObjects = new List<ActiveObjectClassModel>(); // 当前跟踪中的物体
  19. private List<ActiveObjectClassModel> lostObjects = new List<ActiveObjectClassModel>();
  20. private List<ActiveObjectClassModel> historyActiveObjects = new List<ActiveObjectClassModel>(); // 历史物体
  21. BoundRectangleClass BoundRectangle = new BoundRectangleClass();
  22. private long currentLine = 0; //行数记录
  23. private int ObjectNum = 0;
  24. private int ChannelWidth = 0;//每个区域的宽度
  25. public int HistoryNum { get { return _HistoryNum; } }
  26. private int _HistoryNum = 0;
  27. public int HistoryOkNum { get { return _HistoryOkNum; } }
  28. private int _HistoryOkNum = 0;
  29. public int HistoryNgNum { get { return _HistoryNgNum; } }
  30. private int _HistoryNgNum = 0;
  31. private ShuLiConfigClassModel shuLiConfig = new ShuLiConfigClassModel();
  32. public List<int> ChannelsRoi { get { return _ChannelsRoi; } }
  33. private List<int> _ChannelsRoi = new List<int>();
  34. private bool IsPrintLightOnError = false;
  35. public string AlgorithmName => "ProcessingAlgorithm_CCDShuLi";
  36. public void Configure(string parameters)
  37. {
  38. throw new NotImplementedException();
  39. }
  40. public object GetParameters()
  41. {
  42. throw new NotImplementedException();
  43. }
  44. public string GetSaveJson()
  45. {
  46. throw new NotImplementedException();
  47. }
  48. public object ProcessImage(IFrameOut imageData, int cameraId)
  49. {
  50. bool result = ProcessImageSequence(imageData, out List<ActiveObjectClassModel> resultValue);
  51. if (result)
  52. {
  53. return resultValue;
  54. }
  55. else
  56. {
  57. return null;
  58. }
  59. }
  60. /// <summary>
  61. /// 处理图像序列的主入口
  62. /// </summary>
  63. /// <param name="image">图像像素数据</param>
  64. /// <param name="ImageWidth">图像宽</param>
  65. /// <param name="currentLine">当前行数</param>
  66. /// <returns>检测到的物体总数</returns>
  67. public bool ProcessImageSequence(IFrameOut image,out List<ActiveObjectClassModel> resultValue)
  68. {
  69. bool result = false;
  70. for (int i = 0; i < image.Image.Height; i++)
  71. {
  72. ProcessLine(image, i);
  73. currentLine += 1;
  74. }
  75. //识别到结果并输出
  76. // 清理超时未更新的物体
  77. lostObjects = activeObjects
  78. .Where(o => (currentLine - o.LastSeenLine) > shuLiConfig.MAX_GAP || (o.LastSeenLine - o.StartLine) > shuLiConfig.MAX_Idetify_Height)
  79. .ToList();
  80. lostObjects.ForEach(o => activeObjects.Remove(o));
  81. resultValue = new List<ActiveObjectClassModel>();
  82. // 有物体转变为活跃物体,返回值转为true
  83. if (lostObjects.Count > 0)
  84. {
  85. foreach (var item in lostObjects)
  86. {
  87. //噪点判定
  88. if (item.Area < shuLiConfig.NoiseFilter_Threshold)
  89. {
  90. item.StateCode = 9;
  91. //LOG.log(string.Format("噪点过滤,噪点面积:{0}", item.Area), 6);
  92. continue;
  93. }
  94. //转为历史物体,添加缺少的参数
  95. item.Num = Interlocked.Increment(ref ObjectNum);
  96. //item.ChannelNO = ActiveChannel(item);
  97. item.EndCheckTime = DateTime.Now;
  98. item.MaxLength = GetActionMaxLength(item.RowsData);
  99. item.PictureEndReadTime = TimeStampTools.FromUnixTimestamp((long)image.HostTimeStamp);
  100. var QutuYanshiTime = (DateTime.Now - item.PictureEndReadTime).TotalMilliseconds;
  101. if (QutuYanshiTime > 30)
  102. {
  103. Console.WriteLine($"结果输出延时超过了30ms,耗时{QutuYanshiTime}");
  104. }
  105. if ((item.LastSeenLine - item.StartLine) > shuLiConfig.MAX_Idetify_Height)
  106. {
  107. item.StateCode = 7;
  108. //FaultLog.RecordLogMessage("ShuLiClass-ProcessLine:非颗粒,视野异常", 3);
  109. //TxtLog.log(string.Format("ShuLiClass-ProcessLine:非颗粒,视野异常"), 6);
  110. //Console.WriteLine("ShuLiClass-ProcessLine:非颗粒,视野异常");
  111. }
  112. else if (shuLiConfig.PandingCode != -1)
  113. {
  114. if (item.StateCode != -1)
  115. {
  116. if (item.StateCode == 8)
  117. {
  118. //TxtLog.log(string.Format("颗粒编号{0}:疑似叠片或缺损", item.Num));
  119. }
  120. }
  121. else if (item.Area < shuLiConfig.MinArea
  122. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1))
  123. {
  124. item.StateCode = 5;
  125. //TxtLog.log(string.Format("颗粒编号{0}:面积过小", item.Num));
  126. //Console.WriteLine("颗粒编号{0}:面积过小", item.Num);
  127. }
  128. else if (item.Area > shuLiConfig.MaxArea
  129. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 1))
  130. {
  131. item.StateCode = 6;
  132. //TxtLog.log(string.Format("颗粒编号{0}:面积过大", item.Num));
  133. //Console.WriteLine("颗粒编号{0}:面积过大", item.Num);
  134. }
  135. else if (item.MaxLength < shuLiConfig.MIN_Object_LENGTH
  136. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  137. {
  138. item.StateCode = 2;
  139. //TxtLog.log(string.Format("颗粒编号{0}:超短粒", item.Num));
  140. //Console.WriteLine("颗粒编号{0}:超短粒", item.Num);
  141. }
  142. else if (item.MaxLength > shuLiConfig.MAX_Object_LENGTH
  143. && (shuLiConfig.PandingCode == 2 || shuLiConfig.PandingCode == 0))
  144. {
  145. item.StateCode = 1;
  146. //TxtLog.log(string.Format("颗粒编号{0}:超长粒", item.Num));
  147. //Console.WriteLine("颗粒编号{0}:超长粒", item.Num);
  148. }
  149. else
  150. {
  151. item.StateCode = 0;
  152. //TxtLog.log(string.Format("颗粒编号{0}:正常粒", item.Num));
  153. //Console.WriteLine("颗粒编号{0}:正常粒", item.Num);
  154. }
  155. }
  156. resultValue.Add(item);
  157. }
  158. if (resultValue.Count > 0)
  159. {
  160. result = true;
  161. //LOG.log(string.Format("识别完成,首个颗粒编号:{0},颗粒数量:{1}", OneActive[0].Num, OneActive.Count), 6);
  162. //触发回调事件
  163. //Task.Run(() =>
  164. //{
  165. // OnWorkCompleted(OneActive);
  166. //});
  167. //ThreadPool.QueueUserWorkItem(_ => OnWorkCompleted(OneActive));
  168. }
  169. }
  170. else
  171. {
  172. resultValue.Clear();
  173. }
  174. // 累加到总数并从活跃物体转移到历史物体
  175. resultValue.ForEach(o => TryAdd(historyActiveObjects, o, 2500));
  176. return result;
  177. }
  178. /// <summary>
  179. /// 处理单行像素数据
  180. /// 返回值为false的时候无活跃物体转变为历史物体
  181. /// 返回值为true的时候有活跃物体转变为历史物体
  182. /// </summary>
  183. /// <param name="image">当前行像素数组</param>
  184. private bool ProcessLine(IFrameOut imagedata, int RowNo)
  185. {
  186. Stopwatch stopwatch = Stopwatch.StartNew();
  187. bool result = false;
  188. // 步骤1:检测当前行的有效区域
  189. var currentRegions = FindValidRegions(imagedata.Image, RowNo);
  190. if (currentRegions.Count == 1)
  191. {
  192. if (currentRegions[0].End - (currentRegions[0]).Start + 1 == imagedata.Image.Width)
  193. {
  194. if (!IsPrintLightOnError)
  195. {
  196. //FaultLog.RecordLogMessage("当前行有效区域为整行,检查视野和光源", 5);
  197. IsPrintLightOnError = true;
  198. }
  199. return false;
  200. }
  201. }
  202. IsPrintLightOnError = false;
  203. stopwatch.Stop();
  204. if (stopwatch.ElapsedMilliseconds > 1)
  205. {
  206. //FaultLog.RecordErrorMessage($"ShuLiClass-ProcessLine:图像连通域检测超时,此次识别耗时:{stopwatch.Elapsed}");
  207. }
  208. stopwatch.Restart();
  209. //lock (_lockObj)
  210. //{
  211. foreach (var region in currentRegions)
  212. {
  213. // 查找全部可合并的活跃物体(有重叠+在允许间隔内)
  214. var matcheds = activeObjects.Where(o =>
  215. IsOverlapping(o, region) &&
  216. (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP).ToList();
  217. //当有多个可合并的活跃物体时,将多个物体合并
  218. if (matcheds.Count >= 2)
  219. {
  220. // 合并有效区域队列
  221. var CopeRowsData = new List<RowStartEndColModel>();
  222. matcheds.ForEach(o => CopeRowsData = CopeRowsData.Concat(o.RowsData).ToList());
  223. // 合并有效区域并保存在新的区域中
  224. var MergeMatched = new ActiveObjectClassModel
  225. {
  226. MinStartCol = matcheds.Min(o => o.MinStartCol),
  227. MaxEndCol = matcheds.Max(o => o.MaxEndCol),
  228. StartLine = matcheds.Min(o => o.StartLine),
  229. LastSeenLine = matcheds.Max(o => o.LastSeenLine),
  230. LastSeenLineStartCol = matcheds.Min(o => o.LastSeenLineStartCol),
  231. LastSeenLineEndCol = matcheds.Max(o => o.LastSeenLineEndCol),
  232. StartCheckTime = matcheds.Min(o => o.StartCheckTime),
  233. EndCheckTime = matcheds.Max(o => o.EndCheckTime),
  234. PictureStartReadTime = matcheds.Min(o=>o.PictureStartReadTime),
  235. Area = matcheds.Sum(o => o.Area),
  236. RowsData = CopeRowsData,
  237. ImageWidth = matcheds.FirstOrDefault().ImageWidth,
  238. //StateCode = 8
  239. };
  240. // 从活跃区域中删除被合并的区域
  241. matcheds.ForEach(o => activeObjects.Remove(o));
  242. // 保存新的区域到活跃区域中
  243. activeObjects.Add(MergeMatched);
  244. }
  245. // 搜获可用且可合并的活跃区域
  246. var matched = activeObjects.FirstOrDefault(o =>
  247. IsOverlapping(o, region) &&
  248. (currentLine - o.LastSeenLine - 1) <= shuLiConfig.MAX_GAP);
  249. if (matched != null)
  250. {
  251. // 合并区域:扩展物体边界并更新状态
  252. matched.MinStartCol = Math.Min(matched.MinStartCol, region.Start);
  253. matched.MaxEndCol = Math.Max(matched.MaxEndCol, region.End);
  254. matched.Area += region.End - region.Start + 1;
  255. matched.LastSeenLine = currentLine;
  256. matched.RowsData.Add(new RowStartEndColModel
  257. {
  258. StartCol = region.Start,
  259. EndCol = region.End,
  260. RowsCol = currentLine,
  261. });
  262. matched.LastSeenLineStartCol = region.Start;
  263. matched.LastSeenLineEndCol = region.End;
  264. }
  265. else
  266. {
  267. // 创建新物体(首次出现的区域)
  268. activeObjects.Add(new ActiveObjectClassModel
  269. {
  270. MinStartCol = region.Start,
  271. MaxEndCol = region.End,
  272. StartLine = currentLine,
  273. LastSeenLine = currentLine,
  274. LastSeenLineStartCol = region.Start,
  275. LastSeenLineEndCol = region.End,
  276. StartCheckTime = DateTime.Now,
  277. PictureStartReadTime = TimeStampTools.FromUnixTimestamp((long)imagedata.HostTimeStamp),
  278. Area = region.End - region.Start + 1,
  279. ImageWidth = shuLiConfig.ImageWidth,
  280. RowsData = new List<RowStartEndColModel> {
  281. new RowStartEndColModel {
  282. StartCol = region.Start,
  283. EndCol = region.End,
  284. RowsCol = currentLine,
  285. }
  286. }
  287. });
  288. }
  289. }
  290. //}
  291. stopwatch.Stop();
  292. if (stopwatch.ElapsedMilliseconds > 1)
  293. {
  294. //FaultLog.RecordErrorMessage($"ShuLiClass-ProcessLine:图像区域合并超时,此次识别耗时:{stopwatch.Elapsed}");
  295. }
  296. currentRegions.Clear();
  297. return result;
  298. }
  299. List<ValidRegionModel> regions = new List<ValidRegionModel>();
  300. /// <summary>
  301. /// 检测有效物体区域(横向连续黑色像素段)
  302. /// </summary>
  303. /// <param name="line">当前行像素数组</param>
  304. /// <returns>有效区域列表(起始/结束位置)</returns>
  305. private List<ValidRegionModel> FindValidRegions(IImage image, int RowNo)
  306. {
  307. regions.Clear();
  308. int start = -1; // 当前区域起始标记
  309. int end = -1;
  310. // 遍历所有像素列
  311. if (shuLiConfig.IsIdentifyRoiOpen)
  312. {
  313. for (int i = (int)image.Width * RowNo + shuLiConfig.IdentifyStartX; i < (int)image.Width * RowNo + shuLiConfig.IdentifyStopX; i++)
  314. {
  315. if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素
  316. {
  317. if (start == -1) start = i % (int)image.Width; // 开始新区域
  318. }
  319. else if (start != -1) // 遇到白色像素且存在进行中的区域
  320. {
  321. end = (i - 1) % (int)image.Width;
  322. // 检查区域宽度是否达标
  323. if (end - start >= shuLiConfig.NoiseFilter_Threshold)
  324. {
  325. regions.Add(new ValidRegionModel()
  326. {
  327. Start = start,
  328. End = end
  329. }); // 记录有效区域
  330. }
  331. start = -1; // 重置区域标记
  332. end = -1;
  333. }
  334. }
  335. }
  336. else
  337. {
  338. for (int i = (int)image.Width * RowNo; i < (int)image.Width * (RowNo + 1); i++)
  339. {
  340. if (image.PixelData[i] < shuLiConfig.RegionThreshold) // 发现黑色像素
  341. {
  342. if (start == -1) start = i % (int)image.Width; // 开始新区域
  343. }
  344. else if (start != -1) // 遇到白色像素且存在进行中的区域
  345. {
  346. end = (i - 1) % (int)image.Width;
  347. // 检查区域宽度是否达标
  348. if (end - start >= shuLiConfig.NoiseFilter_Threshold)
  349. {
  350. regions.Add(new ValidRegionModel()
  351. {
  352. Start = start,
  353. End = end
  354. }); // 记录有效区域
  355. }
  356. start = -1; // 重置区域标记
  357. end = -1;
  358. }
  359. }
  360. }
  361. // 处理行尾未闭合的区域
  362. if (start != -1 && image.Width - start >= shuLiConfig.NoiseFilter_Threshold)
  363. {
  364. regions.Add(new ValidRegionModel()
  365. {
  366. Start = start,
  367. End = (int)image.Width - 1
  368. });
  369. }
  370. return regions;
  371. }
  372. /// <summary>
  373. /// 判断区域重叠(与活跃物体的横向坐标重叠检测)
  374. /// </summary>
  375. /// <param name="obj">活跃物体</param>
  376. /// <param name="region">当前区域</param>
  377. /// <returns>是否发生重叠</returns>
  378. private bool IsOverlapping(ActiveObjectClassModel obj, ValidRegionModel region)
  379. {
  380. // 判断区域是否不相交的逆条件
  381. return !(region.End < obj.LastSeenLineStartCol || region.Start > obj.LastSeenLineEndCol);
  382. }
  383. /// <summary>
  384. /// 历史数据队列添加数据
  385. /// </summary>
  386. /// <param name="list">历史数据队列</param>
  387. /// <param name="item">添加的数据</param>
  388. /// <param name="maxSize">队列最大数据量</param>
  389. /// <returns></returns>
  390. private bool TryAdd(List<ActiveObjectClassModel> list, ActiveObjectClassModel item, int maxSize)
  391. {
  392. list.Add(item);
  393. Interlocked.Increment(ref _HistoryNum);
  394. if(item.StateCode == 0)
  395. {
  396. Interlocked.Increment(ref _HistoryOkNum);
  397. }
  398. else
  399. {
  400. Interlocked.Increment(ref _HistoryNgNum);
  401. }
  402. if (list.Count > maxSize)
  403. {
  404. list.Remove(list[list.Count - maxSize]);
  405. }
  406. return true;
  407. }
  408. /// <summary>
  409. /// 获取结果最长长边
  410. /// </summary>
  411. /// <param name="Rows"></param>
  412. /// <returns></returns>
  413. private double GetActionMaxLength(List<RowStartEndColModel> Rows)
  414. {
  415. //外轮廓点集
  416. List<Point> points = Rows.Select(o => new Point(o.StartCol, (int)o.RowsCol)).ToList();
  417. points.AddRange(Rows.Select(o => new Point(o.EndCol, (int)o.RowsCol)).ToList());
  418. //获取凸包点集
  419. points = BoundRectangle.ConvexHull(points);
  420. //获取凸包长度最长的外接矩形
  421. var result = BoundRectangle.RotatingCalipers(points);
  422. if (result != null)
  423. {
  424. return result.Height;
  425. }
  426. else
  427. {
  428. return 0;
  429. }
  430. }
  431. /// <summary>
  432. /// 通道区域判定
  433. /// </summary>
  434. /// <param name="activeObject"></param>
  435. /// <returns></returns>
  436. private int ActiveChannel(ActiveObjectClassModel activeObject)
  437. {
  438. int result = -1;
  439. int StartChannel = activeObject.MinStartCol / ChannelWidth;
  440. int EndChannel = activeObject.MaxEndCol / ChannelWidth;
  441. if (StartChannel == EndChannel)
  442. {
  443. result = StartChannel;
  444. }
  445. else if (EndChannel - StartChannel > 1)
  446. {
  447. Console.WriteLine("ActiveChannel-Error");
  448. //error
  449. }
  450. else
  451. {
  452. result = _ChannelsRoi[StartChannel] - activeObject.MinStartCol > activeObject.MaxEndCol - _ChannelsRoi[StartChannel] ? StartChannel : EndChannel;
  453. }
  454. return result;
  455. }
  456. }
  457. }