| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- 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
- {
- /// <summary>
- /// 华睿线扫相机延时采集拼接工具。
- ///
- /// ==================== 在 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();
- /// </summary>
- public class LineScanStitchTool : IDisposable
- {
- #region ==================== 输入参数 ====================
- /// <summary>相机索引(华睿 SDK 枚举列表中的序号)</summary>
- public int CameraIndex { get; set; } = 0;
- /// <summary>曝光时间(微秒)</summary>
- public float ExposureTime { get; set; } = 10000f;
- /// <summary>模拟增益(dB)</summary>
- public float Gain { get; set; } = 0f;
- /// <summary>触发模式:Continuous / Software / Hardware</summary>
- public string TriggerMode { get; set; } = "Continuous";
- /// <summary>连续采集并拼接的帧数</summary>
- public int TotalFrames { get; set; } = 100;
- /// <summary>拼接方向:Vertical(垂直) / Horizontal(水平)</summary>
- public string StitchDirection { get; set; } = "Vertical";
- /// <summary>ROI 宽度(像素),0=自动使用相机最大值</summary>
- public long RoiWidth { get; set; } = 2048;
- /// <summary>ROI 高度(像素),0=自动使用相机最大值</summary>
- public long RoiHeight { get; set; } = 2048;
- /// <summary>ROI X 偏移(像素)</summary>
- public long OffsetX { get; set; } = 0;
- /// <summary>ROI Y 偏移(像素)</summary>
- public long OffsetY { get; set; } = 0;
- /// <summary>水平像素合并数(1/2/4,线扫常用4=FourByOne)</summary>
- public int BinningHorizontal { get; set; } = 1;
- /// <summary>垂直像素合并数(1/2/4)</summary>
- public int BinningVertical { get; set; } = 1;
- /// <summary>结果保存路径(.tif),留空不保存</summary>
- public string SaveImagePath { get; set; } = "";
- #endregion
- #region ==================== 输出结果 ====================
- /// <summary>拼接后的完整灰度图像(VM 原生格式)</summary>
- public ImageBaseData StitchedImage { get; private set; }
- /// <summary>实际参与拼接的帧数</summary>
- public int ActualFrames { get; private set; }
- /// <summary>拼接后图像尺寸描述,如 "8192x204800"</summary>
- public string ImageSize { get; private set; }
- /// <summary>状态码:0=成功,-1=相机异常,-2=采图失败,-3=拼接失败</summary>
- public int Status { get; private set; }
- /// <summary>错误描述(仅在 Status != 0 时有内容)</summary>
- public string ErrorMessage { get; private set; }
- #endregion
- #region ==================== 内部字段 ====================
- private HuarayCamera _camera;
- private bool _disposed;
- #endregion
- #region ==================== 公共方法 ====================
- /// <summary>
- /// 执行完整的采集→拼接→输出流程。
- /// 可在 VM「自定义算法」模块的 Run 方法中直接调用。
- /// </summary>
- /// <returns>true=成功,false=失败(检查 Status 和 ErrorMessage)</returns>
- 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;
- }
- }
- /// <summary>
- /// 释放相机资源。
- /// 在 VM 工具从流程中删除或方案关闭时务必调用。
- /// </summary>
- public void Dispose()
- {
- if (!_disposed)
- {
- try
- {
- _camera?.StopGrabbing();
- _camera?.Close();
- _camera?.Dispose();
- }
- catch { /* 静默释放 */ }
- _camera = null;
- _disposed = true;
- }
- }
- #endregion
- #region ==================== 内部实现 ====================
- /// <summary>
- /// 打开华睿相机并配置参数
- /// </summary>
- 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;
- }
- }
- /// <summary>
- /// 采集指定数量的帧
- /// </summary>
- private List<FrameReceivedEventArgs> CaptureFrames()
- {
- int totalFrames = TotalFrames;
- var frames = new List<FrameReceivedEventArgs>();
- var framesLock = new object();
- bool capturing = true;
- // 注册帧接收事件
- EventHandler<FrameReceivedEventArgs> 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;
- }
- /// <summary>
- /// 拼接多帧图像为一张大图。
- /// 使用 Marshal.Copy 逐帧写入目标缓冲区,避免 Bitmap LockBits 跨 Stride 对齐问题。
- /// </summary>
- private byte[] StitchFrames(List<FrameReceivedEventArgs> 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;
- }
- /// <summary>
- /// 将拼接结果保存为 TIFF 文件
- /// </summary>
- 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}");
- }
- }
- /// <summary>
- /// 解析触发模式枚举
- /// </summary>
- 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
- }
- }
|