一、背景介绍
MVS(Machine Vision Software),是海康机器人出品的工业相机客户端。
调用MVS SDK获取照片的手段一般有两种,一种是使用触发(外部触发或者软触发)后主动获取,类似于按快门;另一种是关闭触发,注册回调函数来接收,由相机自动连拍。
主动触发的流程是:
- 主动显式地发送一个触发信号;
- 在程序中传入一个MV_FRAME_OUT对象来调用MV_CC_GetImageBuffer();
- 从MV_FRAME_OUT.stFrameInfo获取到MV_FRAME_OUT_INFO_EX;
- 使用内存拷贝获取BitmapRawData;使用MV_CC_FreeImageBuffer()释放资源。
二、最简单的一个例子
Windows环境里调用的是MvCameraControl.Net.dll。
在Linux里可以安装Runtime或者MVS软件来获取对应的.so文件。x64系统下的位置是/opt/MVS/lib/64/libMvCameraControl.so,32位系统在/opt/MVS/lib/32/libMvCameraControl.so,arm64系统在/opt/MVS/lib/aarch64/libMvCameraControl.so(安装过程就不赘述了,下载、解压、执行sudo ./setup.sh就行)。
几个条件编译符号定义在了.csproj里(开发环境是windows,发布环境可能是linux x64,也可能是linux arm64。
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<DefineConstants>LINUX64</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|arm64'">
<DefineConstants>LINUXARM64</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>WIN64</DefineConstants>
</PropertyGroup>
下面是MyCamera类里比较重要的几个定义,如果电脑里装了MVS软件,可以到MVS\Development目录下面找示例或者参考文档。
#if WIN64
public const string CAM_DLL = "MvCameraControl.dll";
#elif LINUX64
public const string CAM_DLL = "/opt/MVS/lib/64/libMvCameraControl.so";
#elif LINUXARM64
public const string CAM_DLL = "/opt/MVS/lib/aarch64/libMvCameraControl.so";
#endif
两个结构体:MV_FRAME_OUT,MV_FRAME_OUT_INFO_EX
public struct MV_FRAME_OUT_INFO_EX
{
public ushort nWidth; // 图像宽
public ushort nHeight; // 图像高
public MvGvspPixelType enPixelType; // 像素格式
public uint nFrameNum; // 帧号
public uint nDevTimeStampHigh; // 时间戳高32位
public uint nDevTimeStampLow; // 时间戳低32位
public uint nReserved0; // 保留,8字节对齐
public long nHostTimeStamp; // 主机生成的时间戳
public uint nFrameLen;
// 以下为chunk新增水印信息
// 设备水印时标
public uint nSecondCount;
public uint nCycleCount;
public uint nCycleOffset;
public float fGain;
public float fExposureTime;
public uint nAverageBrightness; //平均亮度
// 白平衡相关
public uint nRed;
public uint nGreen;
public uint nBlue;
public uint nFrameCounter;
public uint nTriggerIndex; //触发计数
//Line 输入/输出
public uint nInput; //输入
public uint nOutput; //输出
// ROI区域
public ushort nOffsetX;
public ushort nOffsetY;
public ushort nChunkWidth;
public ushort nChunkHeight;
public uint nLostPacket;
public uint nUnparsedChunkNum;
[StructLayout(LayoutKind.Explicit)]
public struct UNPARSED_CHUNK_LIST
{
[FieldOffset(0)]
public nint pUnparsedChunkContent;
[FieldOffset(0)]
public long nAligning;
}
public UNPARSED_CHUNK_LIST UnparsedChunkList;
public uint nExtendWidth; // 图像宽扩展
public uint nExtendHeight; // 图像高扩展
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 34)]
public uint[] nReserved; // 保留字节
}
/// <summary>ch: 图像结构体,输出图像地址及图像信息 | en: Image Struct, output the pointer of Image and the information of the specific image</summary>
public struct MV_FRAME_OUT
{
public nint pBufAddr;
public MV_FRAME_OUT_INFO_EX stFrameInfo;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
public uint[] nReserved; // 保留字节
}
几个相关的方法
[DllImport(CAM_DLL, EntryPoint = "MV_CC_GetImageBuffer")]
private static extern int MV_CC_GetImageBuffer(nint handle, ref MV_FRAME_OUT pFrame, int nMsec);
[DllImport(CAM_DLL, EntryPoint = "MV_CC_FreeImageBuffer")]
private static extern int MV_CC_FreeImageBuffer(nint handle, ref MV_FRAME_OUT pFrame);
[DllImport(CAM_DLL, EntryPoint = "MV_CC_StartGrabbing")]
private static extern int MV_CC_StartGrabbing(nint handle)
[DllImport(CAM_DLL, EntryPoint = "MV_CC_StopGrabbing")]
private static extern int MV_CC_StopGrabbing(nint handle)
public int MV_CC_GetImageBuffer_NET(ref MV_FRAME_OUT pFrame, int nMsec)
{
return MV_CC_GetImageBuffer(handle, ref pFrame, nMsec);
}
public int MV_CC_FreeImageBuffer_NET(ref MV_FRAME_OUT pFrame)
{
return MV_CC_FreeImageBuffer(handle, ref pFrame);
}
public int MV_CC_StartGrabbing_NET()
{
return MV_CC_StartGrabbing(handle);
}
public int MV_CC_StopGrabbing_NET()
{
return MV_CC_StopGrabbing(handle);
}
回调函数定义
public int MV_CC_RegisterImageCallBackEx_NET(cbOutputExdelegate cbOutput, nint pUser)
{
return MV_CC_RegisterImageCallBackEx(handle, cbOutput, pUser);
}
[DllImport(CAM_DLL, EntryPoint = "MV_CC_RegisterImageCallBackEx")]
private static extern int MV_CC_RegisterImageCallBackEx(nint handle, cbOutputExdelegate cbOutput, nint pUser);
主程序:连接相机
先定义一个公共的MyCamera对象
static MyCamera myCamera = new();
static MyCamera.MV_CC_DEVICE_INFO_LIST m_stDeviceList;
相机发现、连接
/// <summary>
/// id=1代表usb,其它值代表网络
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public IActionResult Index(int id = 0)
{
var handle = myCamera.GetCameraHandle();
var result = myCamera.MV_CC_IsDeviceConnected_NET() && handle != 0 && handle != MyCamera.MV_E_HANDLE;
if (result)
{
ViewBag.Info = $"无需连接,{ip}";
return View();
}
m_stDeviceList.nDeviceNum = 0;
int nRet = MyCamera.MV_CC_EnumDevices_NET((uint)(id == 1 ? MyCamera.MV_USB_DEVICE : MyCamera.MV_GIGE_DEVICE), ref m_stDeviceList);
if (0 != nRet)
{
ViewBag.Info = $"查找设备失败: {nRet}";
return View();
}
if (m_stDeviceList.nDeviceNum == 0)
{
ViewBag.Info = "没有可用设备";
return View();
}
Console.WriteLine($"找到{m_stDeviceList.nDeviceNum}相机");
var device =
(MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[0], typeof(MyCamera.MV_CC_DEVICE_INFO))!;
if (id == 1)
{
var usbInfo =
(MyCamera.MV_USB3_DEVICE_INFO)MyCamera.ByteToStruct(device.stSpecialInfo.stGigEInfo, typeof(MyCamera.MV_USB3_DEVICE_INFO))!;
string key = $"{usbInfo.chModelName}:{usbInfo.chSerialNumber}";
nRet = myCamera.MV_CC_CreateDevice_NET(ref device);
Console.WriteLine($"{usbInfo.chDeviceGUID}CREATE HANDLE: {myCamera.GetCameraHandle()}");
if (MyCamera.MV_OK != nRet)
{
Console.WriteLine($"{key}创建USB相机设备失败: {nRet}");
return View();
}
nRet = myCamera.MV_CC_OpenDevice_NET();
if (MyCamera.MV_OK != nRet)
{
myCamera.MV_CC_DestroyDevice_NET();
Console.WriteLine($"{key}打开USB相机失败: {nRet}");
return View();
}
//_ = myCamera.MV_CC_SetIntValue_NET("OffsetX", 912);
//_ = myCamera.MV_CC_SetIntValue_NET("Width", 3648);
//_ = usbCam.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF);
//_ = usbCam.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE);
//myCamera.MV_CC_StartGrabbing_NET();
ViewBag.Info = $"打开相机成功:{key},DeviceVersion: {usbInfo.chDeviceVersion}, \r\n" +
$"FamilyName: {usbInfo.chFamilyName}, VENDOR: {usbInfo.chManufacturerName}#{usbInfo.chVendorName}#{usbInfo.idVendor}, MODEL: {usbInfo.chModelName}\r\n" +
$"{usbInfo.chSerialNumber}, {usbInfo.nDeviceNumber}, {usbInfo.nDeviceAddress}";
}
else
{
//获取相机网络信息
var gigInfo =
(MyCamera.MV_GIGE_DEVICE_INFO)MyCamera.ByteToStruct(device.stSpecialInfo.stGigEInfo, typeof(MyCamera.MV_GIGE_DEVICE_INFO))!;
var camIp = gigInfo.nCurrentIp;//相机ip
ip = "";
uint camNet = camIp & gigInfo.nCurrentSubNetMask;//相机掩码,用于计算相机所属网段
var nics = NetworkInterface.GetAllNetworkInterfaces();
bool foundNic = false;
//遍历所有网卡
foreach (var nic in nics)
{
//查找该网卡所有IPv4地址
foreach (var ipAddr in nic.GetIPProperties().UnicastAddresses)
{
if (ipAddr.Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) continue;
byte[] ipBytes = ipAddr.Address.GetAddressBytes();
byte[] subnetMaskBytes = ipAddr.IPv4Mask.GetAddressBytes();
uint nicNet = 0;
for (int i = 0; i < ipBytes.Length; i++) nicNet |= ((uint)(ipBytes[i] & subnetMaskBytes[i])) << (8 * (3 - i));
if (nicNet != camNet) continue;
//网卡IP网段地址和相机的网段地址相同,说明这个网卡连接相机
double speed = nic.Speed / 1_000_000D;//计算网卡速率 bps -> Kbps
ip = $"当前相机:{(camIp & 0xff000000) >> 24}.{(camIp & 0xff0000) >> 16}.{(camIp & 0xff00) >> 8}.{camIp & 0xff}" +
(speed switch
{
< 20 => $",带宽严重不足",//不足20Mbps
>= 20 and < 900 => $",带宽不足",//20Mbps到千兆之间
_ => $""
}) + $" ({speed}Mbps)";
Console.WriteLine($"找到相机:{ip}");
foundNic = true;
break;//找到连相机的ip以后,其它ip不用再看了
}
if (foundNic) break;//找到连相机的网卡以后,其它网卡就不用再看了
}
//找不到相机对应网卡,按理不会发生,因为前面MV_CC_EnumDevices_NET()有结果
if (!foundNic)
{
ViewBag.Info = $"MV_CC_EnumDevices_NET能找到相机:{camIp},但找不到相机对应网卡: {nRet}";
return View();
}
nRet = myCamera.MV_CC_CreateDevice_NET(ref device);
Console.WriteLine($"CREATE HANDLE: {myCamera.GetCameraHandle()}");
if (MyCamera.MV_OK != nRet)
{
ViewBag.Info = $"创建相机设备失败: {nRet}";
return View();
}
nRet = myCamera.MV_CC_OpenDevice_NET();
if (MyCamera.MV_OK != nRet)
{
//General.myCamera.MV_CC_DestroyDevice_NET();
ViewBag.Info = $"打开相机失败: {nRet}";
return View();
}
int nPacketSize = myCamera.MV_CC_GetOptimalPacketSize_NET();
if (nPacketSize > 0)
{
_ = myCamera.MV_CC_SetIntValue_NET("GevSCPSPacketSize", (uint)nPacketSize);
Console.WriteLine($"设置数据包大小: {nPacketSize}");
}
//_ = myCamera.MV_CC_SetIntValue_NET("OffsetX", 912);
//_ = myCamera.MV_CC_SetIntValue_NET("Width", 3648);
//_ = myCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF);
//_ = myCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE);
//myCamera.MV_CC_StartGrabbing_NET();
ViewBag.Info = $"打开相机成功:{ip}";
}
return View();
}
手动抓拍
public IActionResult Image()
{
int nRet = myCamera.MV_CC_SetCommandValue_NET("TriggerSoftware");
if (MyCamera.MV_OK != nRet)
{
Console.WriteLine($"HANDLE: {myCamera.GetCameraHandle()}, {myCamera.MV_CC_IsDeviceConnected_NET()}");
return Content($"拍照Trigger失败:{nRet}");
}
Console.WriteLine($"Trigger:{nRet}");
MyCamera.MV_FRAME_OUT outFrame = new();
int counter = 0;
try
{
while (outFrame.pBufAddr == IntPtr.Zero)
{
if (counter > 2) break;
counter++;
nRet = myCamera.MV_CC_GetImageBuffer_NET(ref outFrame, 500);
}
if (nRet == MyCamera.MV_OK)
{
Console.WriteLine($"GET_BUFFER_outFrame:{outFrame.stFrameInfo.nFrameLen}, Addr: {outFrame.pBufAddr}, SIZE: {Marshal.SizeOf(outFrame)}");
MyCamera.MV_FRAME_OUT_INFO_EX outFrameInfo = outFrame.stFrameInfo;
Console.WriteLine($"GET_BUFFER2_outFrameInfo:{outFrameInfo.nFrameLen}, pxType: {outFrameInfo.enPixelType}, {outFrameInfo.nWidth}*{outFrameInfo.nHeight}, SIZE: {Marshal.SizeOf(outFrameInfo)}");
var h = outFrameInfo.nHeight;
var w = outFrameInfo.nWidth;
byte[] data = new byte[w * h];
Console.WriteLine($"data.Length = {outFrameInfo.nFrameLen}");
Marshal.Copy(outFrame.pBufAddr, data, 0, data.Length);
Console.WriteLine($"Save Buffer");
myCamera.MV_CC_FreeImageBuffer_NET(ref outFrame);
Console.WriteLine($"Free Buffer");
//Linux里无法正常获取nFrameLen和enPixelType,但可以正常拿到Bitmap的RawData
//此处不做照片类型判断
//if (outFrameInfo.enPixelType != MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8)
//{
// myCamera.MV_CC_CloseDevice_NET();
// myCamera.MV_CC_DestroyDevice_NET();
// return Content($"PixelType {outFrameInfo.enPixelType}, not momo8");
//}
SKBitmap skBitmap = new(w, h, SKColorType.Gray8, SKAlphaType.Opaque);
IntPtr pixels = skBitmap.GetPixels();
Console.WriteLine($"Copy data to SKBitmap");
Marshal.Copy(data, 0, pixels, data.Length);
using SKData encodedImage = skBitmap.Encode(SKEncodedImageFormat.Jpeg, 100);
//using FileStream fs = new($"out-{DateTime.Now.Ticks}.jpg", FileMode.OpenOrCreate);
//encodedImage.SaveTo(fs);
//测试DEMO的代码,为了避免直接关闭程序导致相机未关闭导致其它调用方无法连接
//生产环境不应该这么处理,专门从接口关闭
//myCamera.MV_CC_CloseDevice_NET();
//myCamera.MV_CC_DestroyDevice_NET();
return File(encodedImage.ToArray(), "image/jpeg");
}
else
{
myCamera.MV_CC_FreeImageBuffer_NET(ref outFrame);
//myCamera.MV_CC_CloseDevice_NET();
//myCamera.MV_CC_DestroyDevice_NET();
Console.WriteLine($"GET_BUFFER_FAILED: {nRet}");
return Content("拍照后,获取Buffer失败");
}
}
catch (Exception eexx)
{
//myCamera.MV_CC_CloseDevice_NET();
//myCamera.MV_CC_DestroyDevice_NET();
Console.WriteLine(eexx.Message);
return Content(eexx.Message);
}
}
自动连拍+回调
MyCamera.cbOutputExdelegate cb;
public HomeController(ILogger<HomeController> logger)
{
cb = new MyCamera.cbOutputExdelegate(ImageCallback);
}
private void ImageCallback(IntPtr pData, ref MyCamera.MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUser)
{
Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}: " +
$"{pFrameInfo.nFrameLen >> 10} KB, " +
$"Counter: {pFrameInfo.nFrameCounter}, " +
$"Num: {pFrameInfo.nFrameNum}");
var h = pFrameInfo.nHeight;
var w = pFrameInfo.nWidth;
byte[] data = new byte[w * h];
Marshal.Copy(pData, data, 0, data.Length);
//不能用Bitmap,非Windows环境已经不支持System.Drawing了
SKBitmap skBitmap = new(w, h, SKColorType.Gray8, SKAlphaType.Opaque);
IntPtr pixels = skBitmap.GetPixels();
Marshal.Copy(data, 0, pixels, data.Length);
using SKData encodedImage = skBitmap.Encode(SKEncodedImageFormat.Jpeg, 100);
//using FileStream fs = new($"images/out-{DateTime.Now:hhmmss.fff}.jpg", FileMode.OpenOrCreate);
//encodedImage.SaveTo(fs);
}
public string Grab()
{
if (!Directory.Exists("images")) Directory.CreateDirectory("images");
_ = myCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF);
myCamera.MV_CC_RegisterImageCallBackEx_NET(cb, IntPtr.Zero);
myCamera.MV_CC_StartGrabbing_NET();
return "started";
}
以上就是连接相机、自动抓拍并用回调接收照片、手动拍摄的示例。
部署到Windows下面进行测试,或者直接F5调试运行的时候,一切正常。但是在Linux下运行的时候,debug输出的照片长度nFrameLen是3G,enPixelType的值一直是0。
无论在ubuntu x64环境,还是arm64环境,都存在这个问题,所以怀疑是linux下的.so调用出了问题,尝试了各种方案都没有结果。最后在windows下面和linux下面分别dump了MV_FRAME_OUT(下面的对比图)和MV_FRAME_OUT_INFO_EX的内存进行比对,这才发现问题所在(虽然仍然不知道成因)。
int size = Marshal.SizeOf(outFrameInfo);
bytes = new byte[size];
ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(outFrameInfo, ptr, false);
Marshal.Copy(ptr, bytes, 0, size);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
System.IO.File.WriteAllBytes("outFrameInfo.dat", bytes);
通过对比可以看到,整个结构体除了枚举enPixelType之外,都能对得上。而enPixelType这个字段则是在前后各多了4个字节的0x00,导致LINUX下的内存结构整体错位(虽然还是不知道是什么原因造成的)。
找到了问题位置剩下的就好办,修改结构体定义,在Linux下enPixelType字段前后各插入一个4B的uint来占掉这8个字节即可。虽然三言两语就描述完过程,但实际上折腾了将近一周时间,好在最后解决了,回调注册也没有碰到这个问题,大吉大利。
修改MyCamera类中结构体 MV_FRAME_OUT_INFO_EX 的定义
#if LINUX64 || LINUXARM64
public uint nAlignBegin;
#endif
public MvGvspPixelType enPixelType; // 像素格式
#if LINUX64 || LINUXARM64
public uint nAlignEnd;
#endif
#if LINUX64 || LINUXARM64
// 4 + 4 + 8 + 30 * 4 = 136
// 某些版本的手册里会多出来这么一个字段,可写可不写
// 不管写不写,最终要调整nReserved长度,对齐到136B
public long nFrameLenEx;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]
public uint[] nReserved; // 保留字节
//Linux下MvGvspPixelType的前后都有4字节的0x00空白,所以此处扣掉4+4字节的空白,以及nFrameLenEx的8字节进行内存对齐
#endif
#if WIN64
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 34)]
public uint[] nReserved; // 保留字节
#endif