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
}
}