VisionStaticAccuracyAnalyzerViewModel.cs 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165
  1. using Cognex.VisionPro;
  2. using Cognex.VisionPro.ToolBlock;
  3. using MaterialDesignThemes.Wpf;
  4. using Prism.Commands;
  5. using Prism.Events;
  6. using Prism.Ioc;
  7. using Prism.Mvvm;
  8. using Prism.Regions;
  9. using Prism.Services.Dialogs;
  10. using System;
  11. using System.Collections.Generic;
  12. using System.Collections.ObjectModel;
  13. using System.Data;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Threading;
  17. using System.Threading.Tasks;
  18. using Team.FFFeederService;
  19. using Team.FFFeederService.Interfaces;
  20. using TeamAAS_VP.Core;
  21. using TeamAAS_VP.Data;
  22. using TeamAAS_VP.Enums;
  23. using TeamAAS_VP.Events;
  24. using TeamAAS_VP.Interfaces;
  25. using TeamAAS_VP.Models;
  26. using TeamAAS_VP.Models.Calibration;
  27. using TeamAAS_VP.Resources.Languages;
  28. using TeamAAS_VP.Services;
  29. using TeamAAS_VP.Views.Setting;
  30. using static TeamAAS_VP.Core.StabilityAnalyzer;
  31. namespace TeamAAS_VP.ViewModels.Product
  32. {
  33. public class VisionStaticAccuracyAnalyzerViewModel : BindableBase, IDialogAware
  34. {
  35. IRegionManager _regionManager;
  36. IEventAggregator _eventAggregator;
  37. IContainerProvider _container;
  38. IDialogService _dialogService;
  39. IFeederService _feederService;
  40. IRobotService _robotService;
  41. ICameraService _cameraService;
  42. ISystemDatabaseService _systemDatabaseService;
  43. ICalibrationService _calibrationService;
  44. IConfigService _configService;
  45. //测试过程中控制取消的CTS
  46. CancellationTokenSource _cts;
  47. #region 属性
  48. private ICogImage _Image;
  49. public ICogImage Image
  50. {
  51. get { return _Image; }
  52. set { SetProperty(ref _Image, value); }
  53. }
  54. private Cognex.VisionPro.CogGraphicCollection _Graphic;
  55. public Cognex.VisionPro.CogGraphicCollection Graphic
  56. {
  57. get { return _Graphic; }
  58. set { SetProperty(ref _Graphic, value); }
  59. }
  60. private ProcedureModel _SelectProcedure;
  61. /// <summary>
  62. /// 选中的流程
  63. /// </summary>
  64. public ProcedureModel SelectProcedure
  65. {
  66. get { return _SelectProcedure; }
  67. set
  68. {
  69. SetProperty(ref _SelectProcedure, value);
  70. }
  71. }
  72. private ICamera _Camera;
  73. public ICamera Camera
  74. {
  75. get { return _Camera; }
  76. set { SetProperty(ref _Camera, value); }
  77. }
  78. private string _Message = "等待开始...";
  79. public string Message
  80. {
  81. get { return _Message; }
  82. set { SetProperty(ref _Message, value); }
  83. }
  84. private bool _IsRunning;
  85. public bool IsRunning
  86. {
  87. get { return _IsRunning; }
  88. set { SetProperty(ref _IsRunning, value); }
  89. }
  90. private CogToolBlock _VisionTool;
  91. public CogToolBlock VisionTool
  92. {
  93. get { return _VisionTool; }
  94. set { SetProperty(ref _VisionTool, value); }
  95. }
  96. private int _RepeatCount = 10;
  97. /// <summary>
  98. /// 重复次数
  99. /// </summary>
  100. public int RepeatCount
  101. {
  102. get { return _RepeatCount; }
  103. set { SetProperty(ref _RepeatCount, value); }
  104. }
  105. private int _CurrentProgress;
  106. /// <summary>
  107. /// 当前进度
  108. /// </summary>
  109. public int CurrentProgress
  110. {
  111. get { return _CurrentProgress; }
  112. set { SetProperty(ref _CurrentProgress, value); }
  113. }
  114. private int _SuccessCount;
  115. /// <summary>
  116. /// 成功次数
  117. /// </summary>
  118. public int SuccessCount
  119. {
  120. get { return _SuccessCount; }
  121. set { SetProperty(ref _SuccessCount, value); }
  122. }
  123. private int _FailCount;
  124. /// <summary>
  125. /// 失败次数
  126. /// </summary>
  127. public int FailCount
  128. {
  129. get { return _FailCount; }
  130. set { SetProperty(ref _FailCount, value); }
  131. }
  132. private double _SuccessRate;
  133. /// <summary>
  134. /// 成功率
  135. /// </summary>
  136. public double SuccessRate
  137. {
  138. get { return _SuccessRate; }
  139. set { SetProperty(ref _SuccessRate, value); }
  140. }
  141. private DataTable _TestResult;
  142. /// <summary>
  143. /// 测试结果列表DataTable
  144. /// </summary>
  145. public DataTable TestResult
  146. {
  147. get { return _TestResult; }
  148. set { SetProperty(ref _TestResult, value); }
  149. }
  150. private ObservableCollection<DataColumnInfo> _ResultDataColumns;
  151. /// <summary>
  152. /// DataTable的列集合
  153. /// </summary>
  154. public ObservableCollection<DataColumnInfo> ResultDataColumns
  155. {
  156. get { return _ResultDataColumns; }
  157. set { SetProperty(ref _ResultDataColumns, value); }
  158. }
  159. //
  160. private DataColumnInfo _SelectedDataColumn;
  161. /// <summary>
  162. /// 选中的列
  163. /// </summary>
  164. public DataColumnInfo SelectedDataColumn
  165. {
  166. get { return _SelectedDataColumn; }
  167. set { SetProperty(ref _SelectedDataColumn, value); }
  168. }
  169. private SnackbarMessageQueue _MessageQueue;
  170. public SnackbarMessageQueue MessageQueue
  171. {
  172. get { return _MessageQueue; }
  173. set { SetProperty(ref _MessageQueue, value); }
  174. }
  175. //选中列分析结果
  176. private StabilityMetrics _StabilityMetrics;
  177. public StabilityMetrics StabilityMetrics
  178. {
  179. get { return _StabilityMetrics; }
  180. set { SetProperty(ref _StabilityMetrics, value); }
  181. }
  182. #endregion
  183. #region 命令
  184. private DelegateCommand _ConfirmCommand;
  185. public DelegateCommand ConfirmCommand =>
  186. _ConfirmCommand ?? (_ConfirmCommand = new DelegateCommand(ExecuteConfirmCommand, () => !IsRunning).ObservesProperty(() => IsRunning));
  187. private DelegateCommand _CancelCommand;
  188. public DelegateCommand CancelCommand =>
  189. _CancelCommand ?? (_CancelCommand = new DelegateCommand(ExecuteCancelCommand, () => !IsRunning).ObservesProperty(() => IsRunning));
  190. private DelegateCommand _StartTestCommand;
  191. public DelegateCommand StartTestCommand =>
  192. _StartTestCommand ?? (_StartTestCommand = new DelegateCommand(ExecuteStartTestCommand, CanExecuteStartTestCommand).ObservesProperty(() => IsRunning));
  193. private DelegateCommand _StopTestCommand;
  194. public DelegateCommand StopTestCommand =>
  195. _StopTestCommand ?? (_StopTestCommand = new DelegateCommand(() => { _cts?.Cancel(); }, () => IsRunning).ObservesProperty(() => IsRunning));
  196. //暂停测试命令
  197. private DelegateCommand _PauseTestCommand;
  198. public DelegateCommand PauseTestCommand =>
  199. _PauseTestCommand ?? (_PauseTestCommand = new DelegateCommand(() => { _cts?.Cancel(); }, () => IsRunning).ObservesProperty(() => IsRunning));
  200. private DelegateCommand _ExportCSVCommand;
  201. public DelegateCommand ExportCSVCommand =>
  202. _ExportCSVCommand ?? (_ExportCSVCommand = new DelegateCommand(ExecuteExportCSVCommand, () => !IsRunning).ObservesProperty(() => IsRunning));
  203. private DelegateCommand _ExportReportCommand;
  204. public DelegateCommand ExportReportCommand =>
  205. _ExportReportCommand ?? (_ExportReportCommand = new DelegateCommand(ExecuteExportReportCommand, () => !IsRunning).ObservesProperty(() => IsRunning));
  206. private DelegateCommand _ClearDataCommand;
  207. public DelegateCommand ClearDataCommand =>
  208. _ClearDataCommand ?? (_ClearDataCommand = new DelegateCommand(ExecuteClearDataCommand, () => !IsRunning).ObservesProperty(() => IsRunning));
  209. private DelegateCommand _SelectColumnCommand;
  210. public DelegateCommand SelectColumnCommand =>
  211. _SelectColumnCommand ?? (_SelectColumnCommand = new DelegateCommand(ExecuteSelectColumnCommand));
  212. #endregion
  213. #region 事件
  214. #endregion
  215. public VisionStaticAccuracyAnalyzerViewModel(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IDialogService dialogService,
  216. IFeederService feederService, IRobotService robotService, ICameraService cameraService, ISystemDatabaseService systemDatabaseService, ICalibrationService calibrationService,
  217. IConfigService configService)
  218. {
  219. _regionManager = regionManager;
  220. _eventAggregator = ea;
  221. _container = container;
  222. _dialogService = dialogService;
  223. _feederService = feederService;
  224. _robotService = robotService;
  225. _cameraService = cameraService;
  226. _systemDatabaseService = systemDatabaseService;
  227. MessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(1));
  228. _calibrationService = calibrationService;
  229. _configService = configService;
  230. }
  231. #region 方法
  232. /// <summary>
  233. /// 确定
  234. /// </summary>
  235. void ExecuteConfirmCommand()
  236. {
  237. _cts?.Cancel();
  238. IDialogParameters parameters = new DialogParameters();
  239. //parameters.Add("Tool", Tool);
  240. //parameters.Add("RobotCameraRotationMatrix", RobotCameraRotationMatrix);
  241. //parameters.Add("Angle", Angle);
  242. RequestClose?.Invoke(new DialogResult(ButtonResult.OK, parameters));
  243. }
  244. /// <summary>
  245. /// 取消
  246. /// </summary>
  247. void ExecuteCancelCommand()
  248. {
  249. _cts?.Cancel();
  250. IDialogParameters parameters = new DialogParameters();
  251. //parameters.Add("Tool", Tool);
  252. RequestClose?.Invoke(new DialogResult(ButtonResult.Cancel, parameters));
  253. }
  254. /// <summary>
  255. /// 开始测试
  256. /// </summary>
  257. async void ExecuteStartTestCommand()
  258. {
  259. IsRunning = true;
  260. _cts = new CancellationTokenSource();
  261. CurrentProgress = 0;
  262. SuccessCount = 0;
  263. SuccessRate = 0;
  264. FailCount = 0;
  265. Message = "测试进行中...";
  266. try
  267. {
  268. while (CurrentProgress < RepeatCount)
  269. {
  270. if (_cts.Token.IsCancellationRequested)
  271. {
  272. Message = "测试已取消!";
  273. break;
  274. }
  275. (bool isSuccess, object[] Result) = await ExecutePhotoEx(CurrentProgress);
  276. if (isSuccess)
  277. {
  278. SuccessCount++;
  279. }
  280. else
  281. {
  282. FailCount++;
  283. }
  284. TestResult.Rows.Add(Result);
  285. ExecuteSelectColumnCommand();
  286. CurrentProgress++;
  287. SuccessRate = (double)SuccessCount / CurrentProgress * 100;
  288. SendTaskMessage($"测试进行中... {CurrentProgress}/{RepeatCount}");
  289. }
  290. if (CurrentProgress >= RepeatCount)
  291. {
  292. Message = "测试完成!";
  293. }
  294. }
  295. catch (Exception ex)
  296. {
  297. LogHelper.WriteLogError("视觉静态重复测试时出错!", ex);
  298. }
  299. finally
  300. {
  301. IsRunning = false;
  302. }
  303. }
  304. bool CanExecuteStartTestCommand()
  305. {
  306. return !IsRunning;
  307. }
  308. /// <summary>
  309. /// 清除数据
  310. /// </summary>
  311. void ExecuteClearDataCommand()
  312. {
  313. if(TestResult!=null)
  314. {
  315. TestResult.Rows.Clear();
  316. }
  317. }
  318. /// <summary>
  319. /// 导出测试报告
  320. /// 使用 iTextSharp 生成 PDF,包含:标题、测试摘要、数据表格、每列稳定性分析结果
  321. /// 详细伪代码:
  322. /// 1. 校验 TestResult 是否存在数据,若无则提示并返回。
  323. /// 2. 弹出保存对话框,获取保存路径,若取消返回。
  324. /// 3. 创建 iTextSharp Document 与 PdfWriter,打开文档。
  325. /// 4. 创建支持中文的 BaseFont(例如 STSongStd-Light + UniGB-UCS2-H)。
  326. /// 5. 写入标题(居中、大号字体)。
  327. /// 6. 写入测试摘要信息(测试时间、重复次数、成功/失败/成功率等),每项为单独段落或表格。
  328. /// 7. 构建 PdfPTable:列数 = TestResult.Columns.Count,写入表头(加粗),逐行写入数据:
  329. /// - 对于数值使用 InvariantCulture 格式化,空值写空字符串。
  330. /// 8. 写入分析结果标题。
  331. /// 9. 对于 ResultDataColumns 中的每列:
  332. /// - 从 TestResult 读取该列所有数值(尝试转换为 double,忽略无法转换的项)。
  333. /// - 如果样本数为 0,写入“样本数为0”提示并跳过。
  334. /// - 调用 StabilityAnalyzer.AnalyzeStability(values) 获取指标对象。
  335. /// - 遍历指标对象的公开属性,将属性名与值写入一个两列的 PdfPTable(或段落)。
  336. /// 10. 关闭文档并释放资源。
  337. /// 11. 捕获异常并记录日志,提示用户失败信息。
  338. /// </summary>
  339. void ExecuteExportReportCommand()
  340. {
  341. try
  342. {
  343. if (TestResult == null || TestResult.Columns.Count == 0 || TestResult.Rows.Count == 0)
  344. {
  345. SendTaskMessage("无测试数据,无法导出报告。");
  346. return;
  347. }
  348. var dlg = new Microsoft.Win32.SaveFileDialog
  349. {
  350. DefaultExt = "pdf",
  351. Filter = "PDF 文件 (*.pdf)|*.pdf|所有文件 (*.*)|*.*",
  352. FileName = "TestReport.pdf",
  353. Title = "保存测试报告为 PDF"
  354. };
  355. bool? dlgResult = dlg.ShowDialog();
  356. if (dlgResult != true)
  357. return;
  358. string path = dlg.FileName;
  359. // 创建文档(A4, 边距)
  360. var doc = new iTextSharp.text.Document(iTextSharp.text.PageSize.A4, 36, 36, 54, 54);
  361. using (var fs = System.IO.File.Create(path))
  362. {
  363. var writer = iTextSharp.text.pdf.PdfWriter.GetInstance(doc, fs);
  364. doc.Open();
  365. // --- 字体加载:优先载入系统中文字体并以 IDENTITY_H 编码嵌入,保证中文显示 ---
  366. iTextSharp.text.Font titleFont;
  367. iTextSharp.text.Font headerFont;
  368. iTextSharp.text.Font normalFont;
  369. iTextSharp.text.pdf.BaseFont baseFont = null;
  370. try
  371. {
  372. var fontsFolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
  373. var candidates = new[]
  374. {
  375. "msyh.ttf",
  376. "msyhbd.ttf",
  377. "simsun.ttc,0", // 添加 ",0" 指定第一个字体
  378. "simsun.ttc,1", // 第二个字体
  379. "Microsoft YaHei.ttf",
  380. "msyh.ttc,0", // 添加 ",0"
  381. "msyh.ttc,1", // 添加 ",1"
  382. "simhei.ttf"
  383. };
  384. foreach (var f in candidates)
  385. {
  386. var path1 = Path.Combine(fontsFolder, f);
  387. try
  388. {
  389. baseFont = iTextSharp.text.pdf.BaseFont.CreateFont(
  390. path1,
  391. iTextSharp.text.pdf.BaseFont.IDENTITY_H,
  392. iTextSharp.text.pdf.BaseFont.EMBEDDED
  393. );
  394. if (baseFont != null)
  395. {
  396. Console.WriteLine($"成功加载字体: {f}");
  397. break;
  398. }
  399. }
  400. catch (Exception ex)
  401. {
  402. Console.WriteLine($"字体 {f} 加载失败: {ex.Message}");
  403. continue;
  404. }
  405. }
  406. }
  407. catch
  408. {
  409. baseFont = null;
  410. }
  411. if (baseFont != null)
  412. {
  413. titleFont = new iTextSharp.text.Font(baseFont, 16, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
  414. headerFont = new iTextSharp.text.Font(baseFont, 10, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
  415. normalFont = new iTextSharp.text.Font(baseFont, 10, iTextSharp.text.Font.NORMAL, iTextSharp.text.BaseColor.BLACK);
  416. }
  417. else
  418. {
  419. // 回退:如果未找到系统中文字体,使用内置 Helvetica(注意:可能无法正确显示中文)
  420. titleFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 16, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
  421. headerFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 10, iTextSharp.text.Font.BOLD, iTextSharp.text.BaseColor.BLACK);
  422. normalFont = iTextSharp.text.FontFactory.GetFont(iTextSharp.text.FontFactory.HELVETICA, 10, iTextSharp.text.Font.NORMAL, iTextSharp.text.BaseColor.BLACK);
  423. }
  424. // 标题
  425. var title = new iTextSharp.text.Paragraph("视觉静态精度测试报告", titleFont)
  426. {
  427. Alignment = iTextSharp.text.Element.ALIGN_CENTER,
  428. SpacingAfter = 12f
  429. };
  430. doc.Add(title);
  431. // 测试摘要
  432. var metaTable = new iTextSharp.text.pdf.PdfPTable(2) { WidthPercentage = 100f };
  433. metaTable.SetWidths(new float[] { 1f, 2f });
  434. void AddMeta(string name, string value)
  435. {
  436. var cellName = new iTextSharp.text.pdf.PdfPCell(new iTextSharp.text.Phrase(name, headerFont)) { Border = 0, PaddingBottom = 6f };
  437. var cellVal = new iTextSharp.text.pdf.PdfPCell(new iTextSharp.text.Phrase(value, normalFont)) { Border = 0, PaddingBottom = 6f };
  438. metaTable.AddCell(cellName);
  439. metaTable.AddCell(cellVal);
  440. }
  441. // include device info (line, station, fixture id) but exclude MES flags/urls
  442. var device = _configService?.GetDeviceInfo() ?? _configService?.GetDeviceInfo();
  443. if (device != null)
  444. {
  445. AddMeta("线别", device.Line ?? string.Empty);
  446. AddMeta("站别", device.Station ?? string.Empty);
  447. AddMeta("机台号", device.FixtureId ?? string.Empty);
  448. }
  449. AddMeta("导出时间", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
  450. AddMeta("测试次数(目标)", RepeatCount.ToString());
  451. AddMeta("当前进度", $"{CurrentProgress} / {RepeatCount}");
  452. AddMeta("成功次数", SuccessCount.ToString());
  453. AddMeta("失败次数", FailCount.ToString());
  454. AddMeta("成功率(%)", SuccessRate.ToString("F2", System.Globalization.CultureInfo.InvariantCulture));
  455. doc.Add(metaTable);
  456. doc.Add(new iTextSharp.text.Paragraph(" ")); // 空行
  457. // 数据表格
  458. int colCount = TestResult.Columns.Count;
  459. var table = new iTextSharp.text.pdf.PdfPTable(colCount) { WidthPercentage = 100f };
  460. // 表头
  461. foreach (System.Data.DataColumn col in TestResult.Columns)
  462. {
  463. var cell = new iTextSharp.text.pdf.PdfPCell(new iTextSharp.text.Phrase(col.ColumnName, headerFont))
  464. {
  465. HorizontalAlignment = iTextSharp.text.Element.ALIGN_CENTER,
  466. BackgroundColor = new iTextSharp.text.BaseColor(230, 230, 230),
  467. Padding = 4f
  468. };
  469. table.AddCell(cell);
  470. }
  471. // 数据行
  472. foreach (System.Data.DataRow row in TestResult.Rows)
  473. {
  474. for (int c = 0; c < colCount; c++)
  475. {
  476. object val = row[c];
  477. string s;
  478. if (val == null || val == DBNull.Value)
  479. s = "";
  480. else if (val is double || val is float || val is decimal)
  481. s = Convert.ToDouble(val).ToString("G", System.Globalization.CultureInfo.InvariantCulture);
  482. else
  483. s = val.ToString();
  484. var cell = new iTextSharp.text.pdf.PdfPCell(new iTextSharp.text.Phrase(s, normalFont)) { Padding = 4f };
  485. table.AddCell(cell);
  486. }
  487. }
  488. doc.Add(table);
  489. doc.NewPage();
  490. // 分析结果
  491. var analysisTitle = new iTextSharp.text.Paragraph("分析结果", titleFont) { SpacingAfter = 8f };
  492. doc.Add(analysisTitle);
  493. if (ResultDataColumns != null && ResultDataColumns.Count > 0)
  494. {
  495. foreach (var colInfo in ResultDataColumns)
  496. {
  497. try
  498. {
  499. // 收集数值
  500. var values = new System.Collections.Generic.List<double>();
  501. foreach (System.Data.DataRow row in TestResult.Rows)
  502. {
  503. object v = row[colInfo.ColumnIndex];
  504. if (v == null || v == DBNull.Value) continue;
  505. double d;
  506. if (v is double) d = (double)v;
  507. else if (v is float) d = Convert.ToDouble((float)v);
  508. else if (v is decimal) d = Convert.ToDouble((decimal)v);
  509. else if (v is int) d = Convert.ToDouble((int)v);
  510. else if (v is long) d = Convert.ToDouble((long)v);
  511. else
  512. {
  513. if (!double.TryParse(v.ToString(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out d))
  514. continue;
  515. }
  516. values.Add(d);
  517. }
  518. var colHeader = new iTextSharp.text.Paragraph(colInfo.ColumnName, headerFont) { SpacingBefore = 6f, SpacingAfter = 4f };
  519. doc.Add(colHeader);
  520. if (values.Count == 0)
  521. {
  522. doc.Add(new iTextSharp.text.Paragraph("样本数为 0,无法计算。", normalFont));
  523. continue;
  524. }
  525. // 计算稳定性指标
  526. StabilityMetrics metrics = StabilityAnalyzer.AnalyzeStability(values.ToArray());
  527. // 将指标写成两列表(属性名 / 值)
  528. var metricsTable = new iTextSharp.text.pdf.PdfPTable(2) { WidthPercentage = 60f, SpacingAfter = 6f };
  529. metricsTable.SetWidths(new float[] { 1f, 1f });
  530. var props = metrics.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
  531. foreach (var p in props)
  532. {
  533. object pv = p.GetValue(metrics);
  534. string pvStr;
  535. if (pv == null) pvStr = "";
  536. else if (pv is double) pvStr = ((double)pv).ToString("G", System.Globalization.CultureInfo.InvariantCulture);
  537. else pvStr = pv.ToString();
  538. var pc = new iTextSharp.text.pdf.PdfPCell(new iTextSharp.text.Phrase(p.Name, normalFont)) { Padding = 4f };
  539. var vc = new iTextSharp.text.pdf.PdfPCell(new iTextSharp.text.Phrase(pvStr, normalFont)) { Padding = 4f };
  540. metricsTable.AddCell(pc);
  541. metricsTable.AddCell(vc);
  542. }
  543. doc.Add(metricsTable);
  544. }
  545. catch (Exception exCol)
  546. {
  547. LogHelper.WriteLogError($"导出报告时处理列[{colInfo.ColumnName}]出错", exCol);
  548. doc.Add(new iTextSharp.text.Paragraph($"处理列 {colInfo.ColumnName} 时出错: {exCol.Message}", normalFont));
  549. }
  550. }
  551. }
  552. else
  553. {
  554. doc.Add(new iTextSharp.text.Paragraph("无用于分析的数值列。", normalFont));
  555. }
  556. doc.Close();
  557. writer.Close();
  558. }
  559. SendTaskMessage($"已导出测试报告到:{path}");
  560. }
  561. catch (Exception ex)
  562. {
  563. LogHelper.WriteLogError("导出测试报告时出错", ex);
  564. SendTaskMessage($"导出测试报告失败:{ex.Message}");
  565. }
  566. }
  567. /// <summary>
  568. /// 导出测试数据为CSV文件
  569. /// </summary>
  570. void ExecuteExportCSVCommand()
  571. {
  572. try
  573. {
  574. if (TestResult == null || TestResult.Columns.Count == 0 || TestResult.Rows.Count == 0)
  575. {
  576. SendTaskMessage("无测试数据,无法导出。");
  577. return;
  578. }
  579. var dlg = new Microsoft.Win32.SaveFileDialog
  580. {
  581. DefaultExt = "csv",
  582. Filter = "CSV 文件 (*.csv)|*.csv|所有文件 (*.*)|*.*",
  583. FileName = "TestResult.csv",
  584. Title = "保存测试结果为 CSV"
  585. };
  586. bool? dlgResult = dlg.ShowDialog();
  587. if (dlgResult != true)
  588. return;
  589. string path = dlg.FileName;
  590. var sb = new System.Text.StringBuilder();
  591. // 辅助:CSV 字段转义
  592. Func<string, string> EscapeCsv = (s) =>
  593. {
  594. if (s == null) return "";
  595. bool mustQuote = s.Contains(",") || s.Contains("\"") || s.Contains("\r") || s.Contains("\n");
  596. string esc = s.Replace("\"", "\"\"");
  597. return mustQuote ? $"\"{esc}\"" : esc;
  598. };
  599. // include device info at top of CSV
  600. var device = _configService?.GetDeviceInfo();
  601. if (device != null)
  602. {
  603. sb.AppendLine("设备信息");
  604. sb.AppendLine($"线别,{EscapeCsv(device.Line)}");
  605. sb.AppendLine($"站别,{EscapeCsv(device.Station)}");
  606. sb.AppendLine($"机台号,{EscapeCsv(device.FixtureId)}");
  607. sb.AppendLine();
  608. }
  609. // 1. 写入表头
  610. for (int c = 0; c < TestResult.Columns.Count; c++)
  611. {
  612. if (c > 0) sb.Append(",");
  613. sb.Append(EscapeCsv(TestResult.Columns[c].ColumnName));
  614. }
  615. sb.AppendLine();
  616. // 2. 写入数据行
  617. foreach (System.Data.DataRow row in TestResult.Rows)
  618. {
  619. for (int c = 0; c < TestResult.Columns.Count; c++)
  620. {
  621. if (c > 0) sb.Append(",");
  622. object val = row[c];
  623. if (val == DBNull.Value || val == null)
  624. {
  625. sb.Append("");
  626. }
  627. else
  628. {
  629. // 保持数值格式,其他转为字符串
  630. string outStr;
  631. if (val is double || val is float || val is decimal)
  632. outStr = Convert.ToString(val, System.Globalization.CultureInfo.InvariantCulture);
  633. else if (val is int || val is long || val is short || val is byte)
  634. outStr = val.ToString();
  635. else if (val is bool)
  636. outStr = (bool)val ? "True" : "False";
  637. else
  638. outStr = val.ToString();
  639. sb.Append(EscapeCsv(outStr));
  640. }
  641. }
  642. sb.AppendLine();
  643. }
  644. // 3. 在末尾追加分析结果
  645. sb.AppendLine(); // 空行
  646. sb.AppendLine("分析结果");
  647. sb.AppendLine("列名,指标,值"); // CSV 表头:列名,指标,值
  648. if (ResultDataColumns != null)
  649. {
  650. foreach (var colInfo in ResultDataColumns)
  651. {
  652. try
  653. {
  654. List<double> values = new List<double>();
  655. foreach (System.Data.DataRow row in TestResult.Rows)
  656. {
  657. object v = row[colInfo.ColumnIndex];
  658. if (v != DBNull.Value && v != null)
  659. {
  660. double d;
  661. // 支持不同数字类型
  662. if (v is double) d = (double)v;
  663. else if (v is float) d = Convert.ToDouble((float)v);
  664. else if (v is decimal) d = Convert.ToDouble((decimal)v);
  665. else if (v is int) d = Convert.ToDouble((int)v);
  666. else if (v is long) d = Convert.ToDouble((long)v);
  667. else
  668. {
  669. if (!double.TryParse(v.ToString(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out d))
  670. continue;
  671. }
  672. values.Add(d);
  673. }
  674. }
  675. if (values.Count == 0)
  676. {
  677. sb.AppendLine($"{EscapeCsv(colInfo.ColumnName)},{EscapeCsv("样本数")},{0}");
  678. continue;
  679. }
  680. // 调用稳定性分析
  681. StabilityMetrics metrics = StabilityAnalyzer.AnalyzeStability(values.ToArray());
  682. // 使用反射枚举 metrics 的公开属性并写入 CSV
  683. var props = metrics.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
  684. foreach (var p in props)
  685. {
  686. object pv = p.GetValue(metrics);
  687. string pvStr = pv == null ? "" : (pv is double ? Convert.ToString((double)pv, System.Globalization.CultureInfo.InvariantCulture) : pv.ToString());
  688. sb.AppendLine($"{EscapeCsv(colInfo.ColumnName)},{EscapeCsv(p.Name)},{EscapeCsv(pvStr)}");
  689. }
  690. }
  691. catch (Exception exCol)
  692. {
  693. LogHelper.WriteLogError($"导出分析结果时处理列[{colInfo.ColumnName}]出错", exCol);
  694. sb.AppendLine($"{EscapeCsv(colInfo.ColumnName)},{EscapeCsv("Error")},{EscapeCsv(exCol.Message)}");
  695. }
  696. }
  697. }
  698. // 写入文件(UTF8,无 BOM,若需 BOM 可使用 new UTF8Encoding(true))
  699. System.IO.File.WriteAllText(path, sb.ToString(), System.Text.Encoding.UTF8);
  700. SendTaskMessage($"已导出测试结果到:{path}");
  701. }
  702. catch (Exception ex)
  703. {
  704. LogHelper.WriteLogError("导出CSV时出错", ex);
  705. SendTaskMessage($"导出CSV失败:{ex.Message}");
  706. }
  707. }
  708. /// <summary>
  709. /// 选中列时触发
  710. /// </summary>
  711. void ExecuteSelectColumnCommand()
  712. {
  713. try
  714. {
  715. //如果选中列为空,则直接返回
  716. if (SelectedDataColumn == null)
  717. return;
  718. //获取表的指定列的所有数据集合
  719. List<double> dataList = new List<double>();
  720. foreach (DataRow row in TestResult.Rows)
  721. {
  722. if (row[SelectedDataColumn.ColumnIndex] != DBNull.Value)
  723. {
  724. dataList.Add(Convert.ToDouble(row[SelectedDataColumn.ColumnIndex]));
  725. }
  726. }
  727. //如果数据量小于10,则不进行分析
  728. if (dataList.Count < 10)
  729. {
  730. //MessageQueue.Enqueue("选中列的数据量小于10,无法进行统计分析!");
  731. return;
  732. }
  733. //进行稳定性分析
  734. StabilityMetrics metrics = StabilityAnalyzer.AnalyzeStability(dataList.ToArray());
  735. App.Current.Dispatcher.Invoke(() =>
  736. {
  737. StabilityMetrics = metrics;
  738. });
  739. }
  740. catch (Exception ex)
  741. {
  742. LogHelper.WriteLogError("选中列的数据分析数据稳定性时出错!", ex);
  743. }
  744. }
  745. /// <summary>
  746. /// 执行相机拍照并运行 ToolBlock,返回是否成功并通过 out 参数返回 Outputs、图像和图形集合。
  747. /// </summary>
  748. /// <param name="index"></param>
  749. /// <returns></returns>
  750. public Task<(bool isSuccess, object[] Result)> ExecutePhotoEx(int index)
  751. {
  752. return Task.Run(() =>
  753. {
  754. CogToolBlockTerminalCollection outputCollection;
  755. List<object> result = new List<object>();
  756. result.Add(index);
  757. try
  758. {
  759. // 1. 采集图像
  760. bool succed = Camera.SetExposureTime(SelectProcedure.ExposureTime);
  761. if (!succed)
  762. {
  763. SendTaskMessage(Lang.相机曝光设置失败);
  764. }
  765. succed = Camera.SetGain(SelectProcedure.Gain);
  766. if (!succed)
  767. {
  768. SendTaskMessage(Lang.相机增益设置失败);
  769. }
  770. DateTime nowtime = DateTime.Now;
  771. LogHelper.WriteLogInfo(Lang.开始采集图像);
  772. var image = Camera.Grab();
  773. if (image == null)
  774. {
  775. SendTaskMessage(Lang.图像采集失败);
  776. outputCollection = null;
  777. return (false, result.ToArray());
  778. }
  779. //Image = image;
  780. VisionTool.Inputs["InputImage"].Value = image;
  781. LogHelper.WriteLogInfo(string.Format(Lang.采集图像完成用时0ms, (DateTime.Now - nowtime).Milliseconds));
  782. // 2. 为ToolBlock传入其他输入终端
  783. //if (InputTerminal != null)
  784. //{
  785. // LogHelper.WriteLogInfo(Lang.为ToolBlock传入输入终端);
  786. // foreach (var item in InputTerminal)
  787. // {
  788. // if (VisionTool.Inputs.Contains(item.Key))
  789. // {
  790. // LogHelper.WriteLogInfo($"传入[{item.Key}]={item.Value}");
  791. // VisionTool.Inputs[item.Key].Value = item.Value;
  792. // }
  793. // else
  794. // {
  795. // LogHelper.WriteLogInfo($"创建并传入[{item.Key}]={item.Value}");
  796. // VisionTool.Inputs.Add(new CogToolBlockTerminal(item.Key, item.Value));
  797. // }
  798. // }
  799. //}
  800. Image = VisionTool.Inputs["InputImage"].Value as ICogImage;
  801. // 3. 运行视觉工具
  802. LogHelper.WriteLogInfo(Lang.开始运行视觉工具);
  803. VisionTool.Run(); //运行ToolBlock
  804. if (VisionTool.RunStatus.Result == CogToolResultConstants.Accept)
  805. {
  806. SendTaskMessage(Lang.视觉流程执行耗时.Replace("{0}", SelectProcedure.Name).Replace("{1}", $"{VisionTool.RunStatus.ProcessingTime:F1}"));
  807. outputCollection = VisionTool.Outputs; //获取输出终端集合
  808. foreach (CogToolBlockTerminal terminal in outputCollection)
  809. {
  810. //如果类型是常见的字符串、数字、布尔类型,则创建对应的列
  811. if (terminal.ValueType == typeof(string))
  812. {
  813. //如果输出的名称是"Point",则要判断是否是点位格式,点位格式为"x1,y1,u1;x2,y2,u2;...."
  814. if (terminal.Name == "Point")
  815. {
  816. //是否需要转换为绝对坐标
  817. needConvertToAbsolute = false;
  818. //如果当前流程的校准为空,则不需要转换为绝对坐标
  819. if (SelectProcedure.CalibrationId != Guid.Empty)
  820. {
  821. var calibration = _calibrationService.GetCalibration(SelectProcedure.CalibrationId);
  822. if (calibration != null)
  823. {
  824. needConvertToAbsolute = true;
  825. }
  826. }
  827. //判断是否是点位格式
  828. string strpoints = terminal.Value.ToString();
  829. string[] items = strpoints.Split(';');
  830. bool isPointFormat = true;
  831. foreach (string item in items)
  832. {
  833. string[] subitems = item.Split(',');
  834. if (subitems.Length < 3)
  835. {
  836. isPointFormat = false;
  837. break;
  838. }
  839. double x, y, u;
  840. if (!double.TryParse(subitems[0], out x) || !double.TryParse(subitems[1], out y) || !double.TryParse(subitems[2], out u))
  841. {
  842. isPointFormat = false;
  843. break;
  844. }
  845. }
  846. if (isPointFormat)
  847. {
  848. //是点位格式,则创建多列
  849. for (int i = 0; i < items.Length; i++)
  850. {
  851. string[] subitems = items[i].Split(',');
  852. //先分别创建像素X、像素Y、角度U三列
  853. result.Add(double.Parse(subitems[0]));
  854. result.Add(double.Parse(subitems[1]));
  855. result.Add(double.Parse(subitems[2]));
  856. //如果需要转换为绝对坐标,则再创建绝对X、绝对Y两列
  857. if (needConvertToAbsolute)
  858. {
  859. var calibration = _calibrationService.GetCalibration(SelectProcedure.CalibrationId);
  860. (bool IsSucceed, double X, double Y, double U) = _calibrationService.ConvertPixelToPosition((double.Parse(subitems[0]), double.Parse(subitems[1]), double.Parse(subitems[2])), null, calibration);
  861. result.Add(Math.Round(X, 5));
  862. result.Add(Math.Round(Y, 5));
  863. }
  864. }
  865. }
  866. else
  867. {
  868. result.Add(terminal.Value.ToString());
  869. }
  870. }
  871. else
  872. {
  873. result.Add(terminal.Value.ToString());
  874. }
  875. }
  876. else if (terminal.ValueType == typeof(int))
  877. result.Add((int)terminal.Value);
  878. else if (terminal.ValueType == typeof(double))
  879. result.Add((double)terminal.Value);
  880. else if (terminal.ValueType == typeof(bool))
  881. result.Add((bool)terminal.Value);
  882. }
  883. Graphic = outputCollection["Graphic"].Value as CogGraphicCollection;
  884. return (true, result.ToArray());
  885. }
  886. else
  887. {
  888. SendTaskMessage(Lang.视觉流程执行出错耗时.Replace("{0}", SelectProcedure.Name).Replace("{1}", $"{VisionTool.RunStatus.ProcessingTime:F1}"));
  889. SendTaskMessage(VisionTool.RunStatus.Message);
  890. outputCollection = null;
  891. Graphic = outputCollection["Graphic"].Value as CogGraphicCollection;
  892. return (false, result.ToArray());
  893. }
  894. }
  895. catch (Exception ex)
  896. {
  897. SendTaskMessage(ex.Message);
  898. LogHelper.WriteLogError(Lang.执行相机取图并执行视觉工具组时出错, ex);
  899. outputCollection = null;
  900. return (false, result.ToArray());
  901. }
  902. });
  903. }
  904. public void SendTaskMessage(string msg)
  905. {
  906. App.Current.Dispatcher.Invoke(() =>
  907. {
  908. MessageQueue.Clear();
  909. MessageQueue.Enqueue(msg);
  910. });
  911. }
  912. bool needConvertToAbsolute = false;
  913. /// <summary>
  914. /// 根据VisionTool的输出终端,创建DataTable的列
  915. /// </summary>
  916. /// <param name="outputCollection"></param>
  917. /// <returns></returns>
  918. private DataTable CreateResultDataTable(CogToolBlockTerminalCollection outputCollection)
  919. {
  920. DataTable dt = new DataTable();
  921. ResultDataColumns = new ObservableCollection<DataColumnInfo>();
  922. //第一列添加序号
  923. dt.Columns.Add("序号", typeof(int));
  924. //列索引
  925. foreach (CogToolBlockTerminal terminal in outputCollection)
  926. {
  927. //如果类型是常见的字符串、数字、布尔类型,则创建对应的列
  928. if (terminal.ValueType == typeof(string))
  929. {
  930. //如果输出的名称是"Point",则要判断是否是点位格式,点位格式为"x1,y1,u1;x2,y2,u2;...."
  931. if (terminal.Name == "Point")
  932. {
  933. //是否需要转换为绝对坐标
  934. needConvertToAbsolute = false;
  935. //如果当前流程的校准为空,则不需要转换为绝对坐标
  936. if (SelectProcedure.CalibrationId != Guid.Empty)
  937. {
  938. var calibration = _calibrationService.GetCalibration(SelectProcedure.CalibrationId);
  939. if (calibration != null)
  940. {
  941. needConvertToAbsolute = true;
  942. }
  943. }
  944. //判断是否是点位格式
  945. string strpoints = terminal.Value.ToString();
  946. string[] items = strpoints.Split(';');
  947. bool isPointFormat = true;
  948. foreach (string item in items)
  949. {
  950. string[] subitems = item.Split(',');
  951. if (subitems.Length < 3)
  952. {
  953. isPointFormat = false;
  954. break;
  955. }
  956. double x, y, u;
  957. if (!double.TryParse(subitems[0], out x) || !double.TryParse(subitems[1], out y) || !double.TryParse(subitems[2], out u))
  958. {
  959. isPointFormat = false;
  960. break;
  961. }
  962. }
  963. if (isPointFormat)
  964. {
  965. //是点位格式,则创建多列
  966. for (int i = 0; i < items.Length; i++)
  967. {
  968. //先分别创建像素X、像素Y、角度U三列
  969. dt.Columns.Add($"{terminal.Name}_Pixel_X{i + 1}", typeof(double));
  970. ResultDataColumns.Add(new DataColumnInfo($"{terminal.Name}_Pixel_X{i + 1}", dt.Columns.Count - 1));
  971. dt.Columns.Add($"{terminal.Name}_Pixel_Y{i + 1}", typeof(double));
  972. ResultDataColumns.Add(new DataColumnInfo($"{terminal.Name}_Pixel_Y{i + 1}", dt.Columns.Count - 1));
  973. dt.Columns.Add($"{terminal.Name}_Angle_U{i + 1}", typeof(double));
  974. ResultDataColumns.Add(new DataColumnInfo($"{terminal.Name}_Angle_U{i + 1}", dt.Columns.Count - 1));
  975. //如果需要转换为绝对坐标,则再创建绝对X、绝对Y两列
  976. if (needConvertToAbsolute)
  977. {
  978. dt.Columns.Add($"{terminal.Name}_Absolute_X{i + 1}", typeof(double));
  979. ResultDataColumns.Add(new DataColumnInfo($"{terminal.Name}_Absolute_X{i + 1}", dt.Columns.Count - 1));
  980. dt.Columns.Add($"{terminal.Name}_Absolute_Y{i + 1}", typeof(double));
  981. ResultDataColumns.Add(new DataColumnInfo($"{terminal.Name}_Absolute_Y{i + 1}", dt.Columns.Count - 1));
  982. }
  983. }
  984. }
  985. else
  986. {
  987. dt.Columns.Add(terminal.Name, typeof(string));
  988. }
  989. }
  990. else
  991. {
  992. dt.Columns.Add(terminal.Name, typeof(string));
  993. }
  994. }
  995. else if (terminal.ValueType == typeof(int))
  996. {
  997. dt.Columns.Add(terminal.Name, typeof(int));
  998. ResultDataColumns.Add(new DataColumnInfo(terminal.Name, dt.Columns.Count - 1));
  999. }
  1000. else if (terminal.ValueType == typeof(double))
  1001. {
  1002. dt.Columns.Add(terminal.Name, typeof(double));
  1003. ResultDataColumns.Add(new DataColumnInfo(terminal.Name, dt.Columns.Count - 1));
  1004. }
  1005. else if (terminal.ValueType == typeof(bool))
  1006. {
  1007. dt.Columns.Add(terminal.Name, typeof(bool));
  1008. }
  1009. //else
  1010. // dt.Columns.Add(terminal.Name, typeof(string));
  1011. }
  1012. return dt;
  1013. }
  1014. #endregion
  1015. #region 继承
  1016. public string Title { get; set; } = "视觉静态精度分析仪";
  1017. public event Action<IDialogResult> RequestClose;
  1018. public bool CanCloseDialog()
  1019. {
  1020. return true;
  1021. }
  1022. public void OnDialogClosed()
  1023. {
  1024. //关闭时,停止测试
  1025. _cts?.Cancel();
  1026. }
  1027. public void OnDialogOpened(IDialogParameters parameters)
  1028. {
  1029. SelectProcedure = parameters.GetValue<ProcedureModel>("SelectedProcedure");
  1030. Camera = _cameraService.GetCamera(SelectProcedure.CameraId);
  1031. VisionTool = parameters.GetValue<CogToolBlock>("ToolBlock");
  1032. VisionTool.Run();
  1033. TestResult = CreateResultDataTable(VisionTool.Outputs);
  1034. }
  1035. #endregion
  1036. }
  1037. //辅助类,用于定义DataTable的列,仅用来对于数值列进行统计分析
  1038. public class DataColumnInfo
  1039. {
  1040. public string ColumnName { get; set; }
  1041. //列索引
  1042. public int ColumnIndex { get; set; }
  1043. public DataColumnInfo(string columnName, int columnIndex)
  1044. {
  1045. ColumnName = columnName;
  1046. ColumnIndex = columnIndex;
  1047. }
  1048. }
  1049. }