LineScanStitchTool.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Drawing.Imaging;
  5. using System.IO;
  6. using System.Runtime.InteropServices;
  7. using HuarayVMCore;
  8. using HuarayVMCore.Camera;
  9. using HuarayVMCore.Core;
  10. using VM.Core;
  11. using VM.PlatformSDKCS;
  12. namespace HuarayVMTool
  13. {
  14. /// <summary>
  15. /// 华睿线扫相机延时采集拼接工具。
  16. ///
  17. /// ==================== 在 VisionMaster 中的使用方法 ====================
  18. ///
  19. /// 方式一(推荐):VM「自定义算法」模块内嵌
  20. /// 在 VM 流程中拖入"自定义算法"模块,将本文件代码粘贴到脚本编辑器,
  21. /// 设置输入输出参数后即可使用。
  22. ///
  23. /// 方式二:编译为 DLL 后作为独立工具
  24. /// 编译本项目生成 HuarayVMTool.dll,放入 VM 安装目录的 Modules 文件夹下,
  25. /// 重启 VM 后在工具箱「图像采集」分类下可见"华睿线扫拼接"工具。
  26. ///
  27. /// ==================== 工具参数说明 ====================
  28. ///
  29. /// 【输入参数】
  30. /// CameraIndex : int - 相机索引(默认 0),对应华睿 SDK 枚举列表中的序号
  31. /// ExposureTime : float - 曝光时间(微秒),范围 10 ~ 1000000,默认 10000
  32. /// Gain : float - 模拟增益(dB),范围 0 ~ 40,默认 0
  33. /// TriggerMode : string - 触发模式:"Continuous" / "Software" / "Hardware"
  34. /// TotalFrames : int - 拼接帧数,范围 1 ~ 10000,默认 100
  35. /// StitchDirection : string - 拼接方向:"Vertical"(垂直) / "Horizontal"(水平)
  36. /// RoiWidth : long - ROI 宽度(像素),0=自动,默认 2048
  37. /// RoiHeight : long - ROI 高度(像素),0=自动,默认 2048
  38. /// OffsetX : long - ROI X 偏移,默认 0
  39. /// OffsetY : long - ROI Y 偏移,默认 0
  40. /// BinningHorizontal: int - 水平像素合并(1/2/4),默认 1(不合并)
  41. /// BinningVertical : int - 垂直像素合并(1/2/4),默认 1(不合并)
  42. /// SaveImagePath : string - 结果保存路径(.tif),留空则不保存
  43. ///
  44. /// 【输出参数】
  45. /// StitchedImage : Image - 拼接后的完整灰度图像
  46. /// ActualFrames : int - 实际采集并参与拼接的帧数
  47. /// ImageSize : string - 拼接后图像尺寸描述,如 "8192x204800"
  48. /// Status : int - 状态码:0=成功,-1=相机异常,-2=采图失败,-3=拼接失败
  49. /// ErrorMessage : string - 错误描述(仅在失败时有内容)
  50. ///
  51. /// ==================== 使用示例 ====================
  52. ///
  53. /// // C# 代码调用(回调取相模式)
  54. /// var tool = new LineScanStitchTool();
  55. /// tool.CameraIndex = 0;
  56. /// tool.ExposureTime = 15000;
  57. /// tool.TotalFrames = 498;
  58. /// tool.StitchDirection = "Vertical";
  59. /// tool.RoiWidth = 2048;
  60. /// tool.RoiHeight = 2048;
  61. /// tool.BinningHorizontal = 4; // FourByOne,8192→2048
  62. /// tool.SaveImagePath = @"D:\Result\stitched.tif";
  63. /// bool ok = tool.Run();
  64. /// if (ok) {
  65. /// ImageBaseData result = tool.StitchedImage;
  66. /// // 传给 VM 下游模块处理...
  67. /// }
  68. /// tool.Dispose();
  69. /// </summary>
  70. public class LineScanStitchTool : IDisposable
  71. {
  72. #region ==================== 输入参数 ====================
  73. /// <summary>相机索引(华睿 SDK 枚举列表中的序号)</summary>
  74. public int CameraIndex { get; set; } = 0;
  75. /// <summary>曝光时间(微秒)</summary>
  76. public float ExposureTime { get; set; } = 10000f;
  77. /// <summary>模拟增益(dB)</summary>
  78. public float Gain { get; set; } = 0f;
  79. /// <summary>触发模式:Continuous / Software / Hardware</summary>
  80. public string TriggerMode { get; set; } = "Continuous";
  81. /// <summary>连续采集并拼接的帧数</summary>
  82. public int TotalFrames { get; set; } = 100;
  83. /// <summary>拼接方向:Vertical(垂直) / Horizontal(水平)</summary>
  84. public string StitchDirection { get; set; } = "Vertical";
  85. /// <summary>ROI 宽度(像素),0=自动使用相机最大值</summary>
  86. public long RoiWidth { get; set; } = 2048;
  87. /// <summary>ROI 高度(像素),0=自动使用相机最大值</summary>
  88. public long RoiHeight { get; set; } = 2048;
  89. /// <summary>ROI X 偏移(像素)</summary>
  90. public long OffsetX { get; set; } = 0;
  91. /// <summary>ROI Y 偏移(像素)</summary>
  92. public long OffsetY { get; set; } = 0;
  93. /// <summary>水平像素合并数(1/2/4,线扫常用4=FourByOne)</summary>
  94. public int BinningHorizontal { get; set; } = 1;
  95. /// <summary>垂直像素合并数(1/2/4)</summary>
  96. public int BinningVertical { get; set; } = 1;
  97. /// <summary>结果保存路径(.tif),留空不保存</summary>
  98. public string SaveImagePath { get; set; } = "";
  99. #endregion
  100. #region ==================== 输出结果 ====================
  101. /// <summary>拼接后的完整灰度图像(VM 原生格式)</summary>
  102. public ImageBaseData StitchedImage { get; private set; }
  103. /// <summary>实际参与拼接的帧数</summary>
  104. public int ActualFrames { get; private set; }
  105. /// <summary>拼接后图像尺寸描述,如 "8192x204800"</summary>
  106. public string ImageSize { get; private set; }
  107. /// <summary>状态码:0=成功,-1=相机异常,-2=采图失败,-3=拼接失败</summary>
  108. public int Status { get; private set; }
  109. /// <summary>错误描述(仅在 Status != 0 时有内容)</summary>
  110. public string ErrorMessage { get; private set; }
  111. #endregion
  112. #region ==================== 内部字段 ====================
  113. private HuarayCamera _camera;
  114. private bool _disposed;
  115. #endregion
  116. #region ==================== 公共方法 ====================
  117. /// <summary>
  118. /// 执行完整的采集→拼接→输出流程。
  119. /// 可在 VM「自定义算法」模块的 Run 方法中直接调用。
  120. /// </summary>
  121. /// <returns>true=成功,false=失败(检查 Status 和 ErrorMessage)</returns>
  122. public bool Run()
  123. {
  124. try
  125. {
  126. Status = 0;
  127. ErrorMessage = "";
  128. // ===== 第一步:打开相机 =====
  129. if (!OpenCamera())
  130. {
  131. Status = -1;
  132. ErrorMessage = "相机连接失败";
  133. return false;
  134. }
  135. // ===== 第二步:配置并开始采集 =====
  136. var frames = CaptureFrames();
  137. if (frames == null || frames.Count == 0)
  138. {
  139. Status = -2;
  140. ErrorMessage = "采图失败:未收到任何帧数据";
  141. return false;
  142. }
  143. // ===== 第三步:按时间排序(线扫相机按采集时间从上到下拼接) =====
  144. frames.Sort((a, b) => a.FrameNumber.CompareTo(b.FrameNumber));
  145. ActualFrames = frames.Count;
  146. // ===== 第四步:拼接 =====
  147. bool isVertical = StitchDirection == "Vertical";
  148. byte[] stitchedData = StitchFrames(frames, isVertical);
  149. // ===== 第五步:输出结果 =====
  150. int frameW = frames[0].Width;
  151. int frameH = frames[0].Height;
  152. int totalW = isVertical ? frameW : frameW * frames.Count;
  153. int totalH = isVertical ? frameH * frames.Count : frameH;
  154. // 封装为 VM 原生图像格式,可直接传给下游模块
  155. StitchedImage = new ImageBaseData(
  156. stitchedData,
  157. (uint)stitchedData.Length,
  158. totalW,
  159. totalH,
  160. VMPixelFormat.VM_PIXEL_MONO_08);
  161. ImageSize = $"{totalW}x{totalH}";
  162. Status = 0;
  163. // ===== 第六步:可选保存到磁盘 =====
  164. if (!string.IsNullOrEmpty(SaveImagePath))
  165. {
  166. SaveStitchedImage(stitchedData, totalW, totalH, SaveImagePath);
  167. }
  168. // 释放原始帧数据引用(相机内部管理生命周期)
  169. frames.Clear();
  170. return true;
  171. }
  172. catch (Exception ex)
  173. {
  174. Status = -3;
  175. ErrorMessage = $"拼接异常: {ex.Message}";
  176. return false;
  177. }
  178. }
  179. /// <summary>
  180. /// 释放相机资源。
  181. /// 在 VM 工具从流程中删除或方案关闭时务必调用。
  182. /// </summary>
  183. public void Dispose()
  184. {
  185. if (!_disposed)
  186. {
  187. try
  188. {
  189. _camera?.StopGrabbing();
  190. _camera?.Close();
  191. _camera?.Dispose();
  192. }
  193. catch { /* 静默释放 */ }
  194. _camera = null;
  195. _disposed = true;
  196. }
  197. }
  198. #endregion
  199. #region ==================== 内部实现 ====================
  200. /// <summary>
  201. /// 打开华睿相机并配置参数
  202. /// </summary>
  203. private bool OpenCamera()
  204. {
  205. try
  206. {
  207. // 如果已有相机实例且已打开,先关闭
  208. if (_camera != null)
  209. {
  210. _camera.Close();
  211. _camera.Dispose();
  212. _camera = null;
  213. }
  214. // 枚举设备
  215. var devices = HuarayCamera.EnumerateDevices();
  216. if (devices.Count == 0)
  217. {
  218. LogHelper.LogError("未检测到华睿相机设备");
  219. return false;
  220. }
  221. int idx = Math.Min(CameraIndex, devices.Count - 1);
  222. LogHelper.LogInfo($"连接相机: {devices[idx].ModelName} (SN:{devices[idx].SerialNumber})");
  223. _camera = new HuarayCamera();
  224. if (!_camera.Open(idx))
  225. {
  226. LogHelper.LogError($"打开相机失败,索引={idx}");
  227. return false;
  228. }
  229. // 配置相机参数
  230. var config = new CameraConfig
  231. {
  232. ExposureTime = ExposureTime,
  233. Gain = Gain,
  234. TriggerMode = ParseTriggerMode(TriggerMode),
  235. TriggerSource = TriggerSource.Software,
  236. Width = RoiWidth,
  237. Height = RoiHeight,
  238. OffsetX = OffsetX,
  239. OffsetY = OffsetY,
  240. BinningHorizontal = BinningHorizontal,
  241. BinningVertical = BinningVertical,
  242. };
  243. _camera.ApplyConfig(config);
  244. LogHelper.LogInfo($"相机配置完成: 曝光={ExposureTime}μs, 增益={Gain}dB, 触发={TriggerMode}, ROI={RoiWidth}x{RoiHeight}, 合像={BinningHorizontal}x{BinningVertical}");
  245. return true;
  246. }
  247. catch (Exception ex)
  248. {
  249. LogHelper.LogError($"打开相机异常: {ex.Message}");
  250. return false;
  251. }
  252. }
  253. /// <summary>
  254. /// 采集指定数量的帧
  255. /// </summary>
  256. private List<FrameReceivedEventArgs> CaptureFrames()
  257. {
  258. int totalFrames = TotalFrames;
  259. var frames = new List<FrameReceivedEventArgs>();
  260. var framesLock = new object();
  261. bool capturing = true;
  262. // 注册帧接收事件
  263. EventHandler<FrameReceivedEventArgs> handler = null;
  264. handler = (sender, e) =>
  265. {
  266. lock (framesLock)
  267. {
  268. if (capturing && frames.Count < totalFrames)
  269. {
  270. frames.Add(e);
  271. }
  272. }
  273. };
  274. _camera.FrameReceived += handler;
  275. try
  276. {
  277. _camera.StartGrabbing();
  278. LogHelper.LogInfo($"开始采集,目标帧数: {totalFrames}...");
  279. // 等待采集完成,超时 60 秒
  280. var sw = System.Diagnostics.Stopwatch.StartNew();
  281. int timeoutMs = Math.Max(60000, totalFrames * 200); // 每帧至少 200ms 超时
  282. while (true)
  283. {
  284. lock (framesLock)
  285. {
  286. if (frames.Count >= totalFrames) break;
  287. }
  288. if (sw.ElapsedMilliseconds > timeoutMs)
  289. {
  290. LogHelper.LogError($"采图超时,已收到 {frames.Count}/{totalFrames} 帧");
  291. break;
  292. }
  293. System.Threading.Thread.Sleep(5);
  294. }
  295. }
  296. finally
  297. {
  298. capturing = false;
  299. _camera.FrameReceived -= handler;
  300. _camera.StopGrabbing();
  301. }
  302. LogHelper.LogInfo($"采集完成,共收到 {frames.Count} 帧");
  303. return frames;
  304. }
  305. /// <summary>
  306. /// 拼接多帧图像为一张大图。
  307. /// 使用 Marshal.Copy 逐帧写入目标缓冲区,避免 Bitmap LockBits 跨 Stride 对齐问题。
  308. /// </summary>
  309. private byte[] StitchFrames(List<FrameReceivedEventArgs> frames, bool isVertical)
  310. {
  311. int frameW = frames[0].Width;
  312. int frameH = frames[0].Height;
  313. int count = frames.Count;
  314. if (count == 0)
  315. return new byte[0];
  316. // 计算拼接后总尺寸
  317. int totalW = isVertical ? frameW : frameW * count;
  318. int totalH = isVertical ? frameH * count : frameH;
  319. // 分配结果缓冲区(灰度单通道,每像素 1 字节)
  320. byte[] result = new byte[totalW * totalH];
  321. for (int i = 0; i < count; i++)
  322. {
  323. byte[] frameData = frames[i].ImageData;
  324. if (isVertical)
  325. {
  326. // 垂直拼接:第 i 帧放在第 i * frameH 行
  327. int dstOffset = i * frameH * totalW;
  328. Array.Copy(frameData, 0, result, dstOffset, frameData.Length);
  329. }
  330. else
  331. {
  332. // 水平拼接:逐行写入,第 i 帧的每一行放在目标行 i*frameW 列处
  333. for (int y = 0; y < frameH; y++)
  334. {
  335. int srcOffset = y * frameW;
  336. int dstOffset = y * totalW + i * frameW;
  337. Array.Copy(frameData, srcOffset, result, dstOffset, frameW);
  338. }
  339. }
  340. }
  341. LogHelper.LogInfo($"拼接完成: {totalW}x{totalH}, {count} 帧, {result.Length / 1024 / 1024}MB");
  342. return result;
  343. }
  344. /// <summary>
  345. /// 将拼接结果保存为 TIFF 文件
  346. /// </summary>
  347. private void SaveStitchedImage(byte[] data, int width, int height, string path)
  348. {
  349. try
  350. {
  351. var dir = Path.GetDirectoryName(path);
  352. if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
  353. Directory.CreateDirectory(dir);
  354. using (var bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed))
  355. {
  356. // 设置灰度调色板
  357. var pal = bmp.Palette;
  358. for (int i = 0; i < 256; i++)
  359. pal.Entries[i] = Color.FromArgb(i, i, i);
  360. bmp.Palette = pal;
  361. // 锁定并写入像素数据
  362. var bmpData = bmp.LockBits(
  363. new Rectangle(0, 0, width, height),
  364. ImageLockMode.WriteOnly,
  365. PixelFormat.Format8bppIndexed);
  366. Marshal.Copy(data, 0, bmpData.Scan0, data.Length);
  367. bmp.UnlockBits(bmpData);
  368. bmp.Save(path, ImageFormat.Tiff);
  369. }
  370. LogHelper.LogInfo($"结果已保存: {path}");
  371. }
  372. catch (Exception ex)
  373. {
  374. LogHelper.LogError($"保存图像失败: {ex.Message}");
  375. }
  376. }
  377. /// <summary>
  378. /// 解析触发模式枚举
  379. /// </summary>
  380. private static TriggerMode ParseTriggerMode(string mode)
  381. {
  382. switch (mode)
  383. {
  384. case "Software": return HuarayVMCore.Camera.TriggerMode.Software;
  385. case "Hardware": return HuarayVMCore.Camera.TriggerMode.Hardware;
  386. default: return HuarayVMCore.Camera.TriggerMode.Continuous;
  387. }
  388. }
  389. #endregion
  390. }
  391. }