VisionStaticAccuracyAnalyzerViewModel.cs 51 KB

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