using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using HuarayVMCore; using HuarayVMCore.Camera; using HuarayVMCore.Core; using VM.Core; using VM.PlatformSDKCS; namespace HuarayVMTool { /// /// 华睿线扫相机延时采集拼接工具。 /// /// ==================== 在 VisionMaster 中的使用方法 ==================== /// /// 方式一(推荐):VM「自定义算法」模块内嵌 /// 在 VM 流程中拖入"自定义算法"模块,将本文件代码粘贴到脚本编辑器, /// 设置输入输出参数后即可使用。 /// /// 方式二:编译为 DLL 后作为独立工具 /// 编译本项目生成 HuarayVMTool.dll,放入 VM 安装目录的 Modules 文件夹下, /// 重启 VM 后在工具箱「图像采集」分类下可见"华睿线扫拼接"工具。 /// /// ==================== 工具参数说明 ==================== /// /// 【输入参数】 /// CameraIndex : int - 相机索引(默认 0),对应华睿 SDK 枚举列表中的序号 /// ExposureTime : float - 曝光时间(微秒),范围 10 ~ 1000000,默认 10000 /// Gain : float - 模拟增益(dB),范围 0 ~ 40,默认 0 /// TriggerMode : string - 触发模式:"Continuous" / "Software" / "Hardware" /// TotalFrames : int - 拼接帧数,范围 1 ~ 10000,默认 100 /// StitchDirection : string - 拼接方向:"Vertical"(垂直) / "Horizontal"(水平) /// RoiWidth : long - ROI 宽度(像素),0=自动,默认 2048 /// RoiHeight : long - ROI 高度(像素),0=自动,默认 2048 /// OffsetX : long - ROI X 偏移,默认 0 /// OffsetY : long - ROI Y 偏移,默认 0 /// BinningHorizontal: int - 水平像素合并(1/2/4),默认 1(不合并) /// BinningVertical : int - 垂直像素合并(1/2/4),默认 1(不合并) /// SaveImagePath : string - 结果保存路径(.tif),留空则不保存 /// /// 【输出参数】 /// StitchedImage : Image - 拼接后的完整灰度图像 /// ActualFrames : int - 实际采集并参与拼接的帧数 /// ImageSize : string - 拼接后图像尺寸描述,如 "8192x204800" /// Status : int - 状态码:0=成功,-1=相机异常,-2=采图失败,-3=拼接失败 /// ErrorMessage : string - 错误描述(仅在失败时有内容) /// /// ==================== 使用示例 ==================== /// /// // C# 代码调用(回调取相模式) /// var tool = new LineScanStitchTool(); /// tool.CameraIndex = 0; /// tool.ExposureTime = 15000; /// tool.TotalFrames = 498; /// tool.StitchDirection = "Vertical"; /// tool.RoiWidth = 2048; /// tool.RoiHeight = 2048; /// tool.BinningHorizontal = 4; // FourByOne,8192→2048 /// tool.SaveImagePath = @"D:\Result\stitched.tif"; /// bool ok = tool.Run(); /// if (ok) { /// ImageBaseData result = tool.StitchedImage; /// // 传给 VM 下游模块处理... /// } /// tool.Dispose(); /// public class LineScanStitchTool : IDisposable { #region ==================== 输入参数 ==================== /// 相机索引(华睿 SDK 枚举列表中的序号) public int CameraIndex { get; set; } = 0; /// 曝光时间(微秒) public float ExposureTime { get; set; } = 10000f; /// 模拟增益(dB) public float Gain { get; set; } = 0f; /// 触发模式:Continuous / Software / Hardware public string TriggerMode { get; set; } = "Continuous"; /// 连续采集并拼接的帧数 public int TotalFrames { get; set; } = 100; /// 拼接方向:Vertical(垂直) / Horizontal(水平) public string StitchDirection { get; set; } = "Vertical"; /// ROI 宽度(像素),0=自动使用相机最大值 public long RoiWidth { get; set; } = 2048; /// ROI 高度(像素),0=自动使用相机最大值 public long RoiHeight { get; set; } = 2048; /// ROI X 偏移(像素) public long OffsetX { get; set; } = 0; /// ROI Y 偏移(像素) public long OffsetY { get; set; } = 0; /// 水平像素合并数(1/2/4,线扫常用4=FourByOne) public int BinningHorizontal { get; set; } = 1; /// 垂直像素合并数(1/2/4) public int BinningVertical { get; set; } = 1; /// 结果保存路径(.tif),留空不保存 public string SaveImagePath { get; set; } = ""; #endregion #region ==================== 输出结果 ==================== /// 拼接后的完整灰度图像(VM 原生格式) public ImageBaseData StitchedImage { get; private set; } /// 实际参与拼接的帧数 public int ActualFrames { get; private set; } /// 拼接后图像尺寸描述,如 "8192x204800" public string ImageSize { get; private set; } /// 状态码:0=成功,-1=相机异常,-2=采图失败,-3=拼接失败 public int Status { get; private set; } /// 错误描述(仅在 Status != 0 时有内容) public string ErrorMessage { get; private set; } #endregion #region ==================== 内部字段 ==================== private HuarayCamera _camera; private bool _disposed; #endregion #region ==================== 公共方法 ==================== /// /// 执行完整的采集→拼接→输出流程。 /// 可在 VM「自定义算法」模块的 Run 方法中直接调用。 /// /// true=成功,false=失败(检查 Status 和 ErrorMessage) public bool Run() { try { Status = 0; ErrorMessage = ""; // ===== 第一步:打开相机 ===== if (!OpenCamera()) { Status = -1; ErrorMessage = "相机连接失败"; return false; } // ===== 第二步:配置并开始采集 ===== var frames = CaptureFrames(); if (frames == null || frames.Count == 0) { Status = -2; ErrorMessage = "采图失败:未收到任何帧数据"; return false; } // ===== 第三步:按时间排序(线扫相机按采集时间从上到下拼接) ===== frames.Sort((a, b) => a.FrameNumber.CompareTo(b.FrameNumber)); ActualFrames = frames.Count; // ===== 第四步:拼接 ===== bool isVertical = StitchDirection == "Vertical"; byte[] stitchedData = StitchFrames(frames, isVertical); // ===== 第五步:输出结果 ===== int frameW = frames[0].Width; int frameH = frames[0].Height; int totalW = isVertical ? frameW : frameW * frames.Count; int totalH = isVertical ? frameH * frames.Count : frameH; // 封装为 VM 原生图像格式,可直接传给下游模块 StitchedImage = new ImageBaseData( stitchedData, (uint)stitchedData.Length, totalW, totalH, VMPixelFormat.VM_PIXEL_MONO_08); ImageSize = $"{totalW}x{totalH}"; Status = 0; // ===== 第六步:可选保存到磁盘 ===== if (!string.IsNullOrEmpty(SaveImagePath)) { SaveStitchedImage(stitchedData, totalW, totalH, SaveImagePath); } // 释放原始帧数据引用(相机内部管理生命周期) frames.Clear(); return true; } catch (Exception ex) { Status = -3; ErrorMessage = $"拼接异常: {ex.Message}"; return false; } } /// /// 释放相机资源。 /// 在 VM 工具从流程中删除或方案关闭时务必调用。 /// public void Dispose() { if (!_disposed) { try { _camera?.StopGrabbing(); _camera?.Close(); _camera?.Dispose(); } catch { /* 静默释放 */ } _camera = null; _disposed = true; } } #endregion #region ==================== 内部实现 ==================== /// /// 打开华睿相机并配置参数 /// private bool OpenCamera() { try { // 如果已有相机实例且已打开,先关闭 if (_camera != null) { _camera.Close(); _camera.Dispose(); _camera = null; } // 枚举设备 var devices = HuarayCamera.EnumerateDevices(); if (devices.Count == 0) { LogHelper.LogError("未检测到华睿相机设备"); return false; } int idx = Math.Min(CameraIndex, devices.Count - 1); LogHelper.LogInfo($"连接相机: {devices[idx].ModelName} (SN:{devices[idx].SerialNumber})"); _camera = new HuarayCamera(); if (!_camera.Open(idx)) { LogHelper.LogError($"打开相机失败,索引={idx}"); return false; } // 配置相机参数 var config = new CameraConfig { ExposureTime = ExposureTime, Gain = Gain, TriggerMode = ParseTriggerMode(TriggerMode), TriggerSource = TriggerSource.Software, Width = RoiWidth, Height = RoiHeight, OffsetX = OffsetX, OffsetY = OffsetY, BinningHorizontal = BinningHorizontal, BinningVertical = BinningVertical, }; _camera.ApplyConfig(config); LogHelper.LogInfo($"相机配置完成: 曝光={ExposureTime}μs, 增益={Gain}dB, 触发={TriggerMode}, ROI={RoiWidth}x{RoiHeight}, 合像={BinningHorizontal}x{BinningVertical}"); return true; } catch (Exception ex) { LogHelper.LogError($"打开相机异常: {ex.Message}"); return false; } } /// /// 采集指定数量的帧 /// private List CaptureFrames() { int totalFrames = TotalFrames; var frames = new List(); var framesLock = new object(); bool capturing = true; // 注册帧接收事件 EventHandler handler = null; handler = (sender, e) => { lock (framesLock) { if (capturing && frames.Count < totalFrames) { frames.Add(e); } } }; _camera.FrameReceived += handler; try { _camera.StartGrabbing(); LogHelper.LogInfo($"开始采集,目标帧数: {totalFrames}..."); // 等待采集完成,超时 60 秒 var sw = System.Diagnostics.Stopwatch.StartNew(); int timeoutMs = Math.Max(60000, totalFrames * 200); // 每帧至少 200ms 超时 while (true) { lock (framesLock) { if (frames.Count >= totalFrames) break; } if (sw.ElapsedMilliseconds > timeoutMs) { LogHelper.LogError($"采图超时,已收到 {frames.Count}/{totalFrames} 帧"); break; } System.Threading.Thread.Sleep(5); } } finally { capturing = false; _camera.FrameReceived -= handler; _camera.StopGrabbing(); } LogHelper.LogInfo($"采集完成,共收到 {frames.Count} 帧"); return frames; } /// /// 拼接多帧图像为一张大图。 /// 使用 Marshal.Copy 逐帧写入目标缓冲区,避免 Bitmap LockBits 跨 Stride 对齐问题。 /// private byte[] StitchFrames(List frames, bool isVertical) { int frameW = frames[0].Width; int frameH = frames[0].Height; int count = frames.Count; if (count == 0) return new byte[0]; // 计算拼接后总尺寸 int totalW = isVertical ? frameW : frameW * count; int totalH = isVertical ? frameH * count : frameH; // 分配结果缓冲区(灰度单通道,每像素 1 字节) byte[] result = new byte[totalW * totalH]; for (int i = 0; i < count; i++) { byte[] frameData = frames[i].ImageData; if (isVertical) { // 垂直拼接:第 i 帧放在第 i * frameH 行 int dstOffset = i * frameH * totalW; Array.Copy(frameData, 0, result, dstOffset, frameData.Length); } else { // 水平拼接:逐行写入,第 i 帧的每一行放在目标行 i*frameW 列处 for (int y = 0; y < frameH; y++) { int srcOffset = y * frameW; int dstOffset = y * totalW + i * frameW; Array.Copy(frameData, srcOffset, result, dstOffset, frameW); } } } LogHelper.LogInfo($"拼接完成: {totalW}x{totalH}, {count} 帧, {result.Length / 1024 / 1024}MB"); return result; } /// /// 将拼接结果保存为 TIFF 文件 /// private void SaveStitchedImage(byte[] data, int width, int height, string path) { try { var dir = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); using (var bmp = new Bitmap(width, height, PixelFormat.Format8bppIndexed)) { // 设置灰度调色板 var pal = bmp.Palette; for (int i = 0; i < 256; i++) pal.Entries[i] = Color.FromArgb(i, i, i); bmp.Palette = pal; // 锁定并写入像素数据 var bmpData = bmp.LockBits( new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); Marshal.Copy(data, 0, bmpData.Scan0, data.Length); bmp.UnlockBits(bmpData); bmp.Save(path, ImageFormat.Tiff); } LogHelper.LogInfo($"结果已保存: {path}"); } catch (Exception ex) { LogHelper.LogError($"保存图像失败: {ex.Message}"); } } /// /// 解析触发模式枚举 /// private static TriggerMode ParseTriggerMode(string mode) { switch (mode) { case "Software": return HuarayVMCore.Camera.TriggerMode.Software; case "Hardware": return HuarayVMCore.Camera.TriggerMode.Hardware; default: return HuarayVMCore.Camera.TriggerMode.Continuous; } } #endregion } }