一、背景介绍

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_OUTMV_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
分类: articles