C#海康工业相机对接及问题汇总
using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.IO.Packaging; using System.Linq; using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Timers; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Media3D; using WaveformStressPatterninSpection.Models; using WaveformStressPatterninSpection.Server.IServer; namespace WaveformStressPatterninSpection.Server.Base { /// <summary> /// 相机操作类 /// </summary> public class CameraHelper { private readonly ICancheService _cancheService; public string savePathByDB = Path.Combine(Environment.CurrentDirectory, "productPic"); public MyCamera.cbOutputExdelegate cbImage; public CameraHelper(ICancheService cancheService) { _cancheService = cancheService; cbImage = new MyCamera.cbOutputExdelegate(ImageCallBack); } /// <summary> /// 获取和更新相机信息,同时创建、打开相机 /// </summary> /// <returns></returns> public string DeviceListAcq() { // ch:创建设备列表 | en:Create Device List System.GC.Collect(); MyCamera.MV_CC_DEVICE_INFO_LIST m_stDeviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST(); m_stDeviceList.nDeviceNum = 0; List<CameraInfo> cbDeviceList = new List<CameraInfo>(); //这里枚举了所有类型,根据实际情况,选择合适的枚举类型即可 int nRet = MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE | MyCamera.MV_GENTL_GIGE_DEVICE | MyCamera.MV_GENTL_CAMERALINK_DEVICE | MyCamera.MV_GENTL_CXP_DEVICE | MyCamera.MV_GENTL_XOF_DEVICE, ref m_stDeviceList); if (0 != nRet || 0 == m_stDeviceList.nDeviceNum) return HK_ErrorCode(" 未找到设备: ", nRet); // ch:在窗体列表中显示设备名 | en:Display device name in the form list for (int i = 0; i < m_stDeviceList.nDeviceNum; i++) { var snNo = ""; var chManufacturerName = ""; CameraInfo info = new CameraInfo() { CanmeraId = i }; MyCamera.MV_CC_DEVICE_INFO device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[i], typeof(MyCamera.MV_CC_DEVICE_INFO)); string strUserDefinedName = ""; if (MyCamera.MV_GIGE_DEVICE == device.nTLayerType) { MyCamera.MV_GIGE_DEVICE_INFO stGigEDeviceInfo = (MyCamera.MV_GIGE_DEVICE_INFO)MyCamera.ByteToStruct(device.SpecialInfo.stGigEInfo, typeof(MyCamera.MV_GIGE_DEVICE_INFO)); string SerialNumber = stGigEDeviceInfo.chSerialNumber; snNo = stGigEDeviceInfo.chSerialNumber; chManufacturerName = stGigEDeviceInfo.chManufacturerName; } else if (MyCamera.MV_USB_DEVICE == device.nTLayerType) { MyCamera.MV_USB3_DEVICE_INFO stUsb3DeviceInfo = (MyCamera.MV_USB3_DEVICE_INFO)MyCamera.ByteToStruct(device.SpecialInfo.stUsb3VInfo, typeof(MyCamera.MV_USB3_DEVICE_INFO)); snNo = stUsb3DeviceInfo.chSerialNumber; chManufacturerName = stUsb3DeviceInfo.chUserDefinedName; } MyCamera m_MyCamera = new MyCamera(); // ch:打开设备 | en:Open device nRet = m_MyCamera.MV_CC_CreateDevice_NET(ref device); if (MyCamera.MV_OK != nRet) { continue; } nRet = m_MyCamera.MV_CC_OpenDevice_NET(); if (MyCamera.MV_OK != nRet) { continue; } // ch:探测网络最佳包大小(只对GigE相机有效) | en:Detection network optimal package size(It only works for the GigE camera) if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE) { int nPacketSize = m_MyCamera.MV_CC_GetOptimalPacketSize_NET(); if (nPacketSize > 0) { nRet = m_MyCamera.MV_CC_SetIntValueEx_NET("GevSCPSPacketSize", nPacketSize); if (nRet != MyCamera.MV_OK) { continue; } } else { continue; } } // ch:设置采集连续模式 | en:Set Continues Aquisition Mode m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS); m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF); // ch:开始采集 | en:Start Grabbing nRet = m_MyCamera.MV_CC_StartGrabbing_NET(); if (MyCamera.MV_OK != nRet) { continue; } info.CanmeraId = i; info.snNo = snNo; info.chManufacturerName = chManufacturerName; info.CanmeraName = snNo; info.isOpenCamera = false; info.isStartCatchPic = true; info.device = device; info.device_Hik = m_MyCamera; info.isOpenCamera = true; info.m_BufForSaveImageLock = new object(); info.m_nFrames = new int(); info.m_nSaveImageBufSize = 0; info.m_pSaveImageBuf = IntPtr.Zero; info.m_stFrameInfo = new MyCamera.MV_FRAME_OUT_INFO_EX(); info.isSoft = false; cbDeviceList.Add(info); } if (cbDeviceList == null || cbDeviceList.Count < 1) return "海康相机不存在或被其他程序占用"; _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{HK_ErrorCode("海康相机不存在或被其他程序占用: ", 0)}"), StartTime = DateTime.UtcNow }); _cancheService.UpdateCameraSettingByCache(cbDeviceList); //更新缓存中的相机信息 //更新并获取数据库的相机信息进行参数下发 var cameraListByDb = _cancheService.GetCameraList(); foreach (var cameraInfo in cameraListByDb) { SettingParameter(cameraInfo); //参数下发 } //硬触发 foreach (var cameraInfoByCache in cbDeviceList) { TriggerMode(cameraInfoByCache); } return ""; } /// <summary> /// 触发模式 /// </summary> /// <param name="cameraInfo"></param> /// <returns></returns> public string TriggerMode(CameraInfo cameraInfo) { int nRet; //如果已经开启抓图需要先关闭抓图 if (cameraInfo.isStartCatchPic) { nRet = cameraInfo.device_Hik.MV_CC_StopGrabbing_NET(); if (nRet != MyCamera.MV_OK) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraInfo.snNo}{HK_ErrorCode("相机关闭失败: ", nRet)}"), StartTime = DateTime.UtcNow }); return ""; } } nRet = cameraInfo.device_Hik.MV_CC_StartGrabbing_NET(); if (MyCamera.MV_OK != nRet) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraInfo.snNo}{HK_ErrorCode("相机开启抓图失败: ", nRet)}"), StartTime = DateTime.UtcNow }); return ""; } cameraInfo.device_Hik.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_ON); //软触发一次之后需要修改会硬触发 if (cameraInfo.isSoft) { cameraInfo.device_Hik.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE); nRet = cameraInfo.device_Hik.MV_CC_SetCommandValue_NET("TriggerSoftware"); if (MyCamera.MV_OK != nRet) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraInfo.snNo}{HK_ErrorCode("相机软触发失败: ", nRet)}"), StartTime = DateTime.UtcNow }); return ""; } MyCamera.MV_FRAME_OUT_INFO_EX pFrameInfo = new MyCamera.MV_FRAME_OUT_INFO_EX(); var picUrl = StartCatchPic(cameraInfo, pFrameInfo); cameraInfo.isSoft = false; TriggerMode(cameraInfo); _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraInfo.snNo}{HK_ErrorCode("相机成像成功: ", 0)}"), StartTime = DateTime.UtcNow }); return picUrl; } else { cameraInfo.device_Hik.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_LINE0); } return ""; } /// <summary> /// 参数下发 /// </summary> public string SettingParameter(CameraSetting cameraByDb) { var cameraInfoByCache = _cancheService.GetCameraListByCache().FirstOrDefault(x => x.snNo == cameraByDb.SnNo); if (cameraInfoByCache == null) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraByDb.SnNo}{HK_ErrorCode("参数下发失败: ", 0)}"), StartTime = DateTime.UtcNow }); return "参数下发失败"; } int nRet = cameraInfoByCache.device_Hik.MV_CC_SetFloatValue_NET("ExposureTime", float.Parse(cameraByDb.ExposureTime)); if (nRet != MyCamera.MV_OK) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraByDb.SnNo}{HK_ErrorCode("曝光时间参数下发失败: ", nRet)}"), StartTime = DateTime.UtcNow }); return "曝光时间参数下发失败"; } nRet = cameraInfoByCache.device_Hik.MV_CC_SetFloatValue_NET("Gain", float.Parse(cameraByDb.Gain.ToString())); if (nRet != MyCamera.MV_OK) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraByDb.SnNo}{HK_ErrorCode("增益参数下发失败: ", nRet)}"), StartTime = DateTime.UtcNow }); return "增益参数下发失败"; } int x = int.Parse(cameraByDb.OffsetX); int y = int.Parse(cameraByDb.OffsetY.ToString()); int w = int.Parse(cameraByDb.Width.ToString()); int h = int.Parse(cameraByDb.Height.ToString()); //检查ROI状态 uint w_ = 0, h_ = 0, w_m = 0, h_m = 0; MyCamera.MVCC_INTVALUE stParam = new MyCamera.MVCC_INTVALUE(); cameraInfoByCache.device_Hik.MV_CC_GetIntValue_NET("Width", ref stParam); w_ = stParam.nCurValue; cameraInfoByCache.device_Hik.MV_CC_GetIntValue_NET("Height", ref stParam); h_ = stParam.nCurValue; cameraInfoByCache.device_Hik.MV_CC_GetIntValue_NET("WidthMax", ref stParam); w_m = stParam.nCurValue; cameraInfoByCache.device_Hik.MV_CC_GetIntValue_NET("HeightMax", ref stParam); h_m = stParam.nCurValue; //初始化宽高最大值 nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("OffsetX", 0); nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("OffsetY", 0); nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("Width", w_m); nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("Height", h_m); nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("Width", (uint)w); nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("Height", (uint)h); nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("OffsetX", (uint)x); nRet = cameraInfoByCache.device_Hik.MV_CC_SetIntValue_NET("OffsetY", (uint)y); if (0 != nRet) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{cameraByDb.SnNo}{HK_ErrorCode("ROI参数下发失败: ", nRet)}"), StartTime = DateTime.UtcNow }); return "ROI参数下发失败"; } return ""; } /// <summary> /// 硬回调抓取 /// </summary> /// <param name="pData"></param> /// <param name="pFrameInfo"></param> /// <param name="pUser"></param> public void ImageCallBack(IntPtr pData, ref MyCamera.MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUser) { int nIndex = (int)pUser; var cameraList = _cancheService.GetCameraListByCache(); lock (cameraList[nIndex].m_BufForSaveImageLock) { if (!cameraList[nIndex].isSoft) { //int nRet = CameraInfos[nIndex].device_Hik.MV_CC_StartGrabbing_NET(); // if (MyCamera.MV_OK != nRet) // { // _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{CameraInfos[nIndex].snNo}开始抓取失败"), StartTime = DateTime.UtcNow }); // return; // } //开始 StartCatchPic(cameraList[nIndex], pFrameInfo, pData); } } } /// <summary> /// 开始抓图 /// </summary> /// <param name="m_MyCameraInfo">相机信息</param> public string StartCatchPic(CameraInfo m_MyCameraInfo, MyCamera.MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pData = default) { int nRet = 0; if (m_MyCameraInfo.isSoft) { //软触发 MyCamera.MV_FRAME_OUT stFrameInfo = new MyCamera.MV_FRAME_OUT(); nRet = m_MyCameraInfo.device_Hik.MV_CC_GetImageBuffer_NET(ref stFrameInfo, 1000); if (nRet == MyCamera.MV_OK) { if (m_MyCameraInfo.m_pSaveImageBuf == IntPtr.Zero || stFrameInfo.stFrameInfo.nFrameLen > m_MyCameraInfo.m_nSaveImageBufSize) { if (m_MyCameraInfo.m_pSaveImageBuf != IntPtr.Zero) { Marshal.Release(m_MyCameraInfo.m_pSaveImageBuf); m_MyCameraInfo.m_pSaveImageBuf = IntPtr.Zero; } m_MyCameraInfo.m_pSaveImageBuf = Marshal.AllocHGlobal((Int32)stFrameInfo.stFrameInfo.nFrameLen); if (m_MyCameraInfo.m_pSaveImageBuf == IntPtr.Zero) { return ""; } m_MyCameraInfo.m_nSaveImageBufSize = stFrameInfo.stFrameInfo.nFrameLen; } m_MyCameraInfo.m_stFrameInfo = stFrameInfo.stFrameInfo; CopyMemory(m_MyCameraInfo.m_pSaveImageBuf, stFrameInfo.pBufAddr, stFrameInfo.stFrameInfo.nFrameLen); } else { _cancheService.UpdateLogging(new Logging() { Content = string.Format($" {m_MyCameraInfo.CanmeraId}相机抓图失败"), StartTime = DateTime.UtcNow }); } } else { //硬触发 if (m_MyCameraInfo.m_pSaveImageBuf == IntPtr.Zero || pFrameInfo.nFrameLen > m_MyCameraInfo.m_nSaveImageBufSize) { if (m_MyCameraInfo.m_pSaveImageBuf != IntPtr.Zero) { Marshal.Release(m_MyCameraInfo.m_pSaveImageBuf); m_MyCameraInfo.m_pSaveImageBuf = IntPtr.Zero; } m_MyCameraInfo.m_pSaveImageBuf = Marshal.AllocHGlobal((Int32)pFrameInfo.nFrameLen); if (m_MyCameraInfo.m_pSaveImageBuf == IntPtr.Zero) { return ""; } m_MyCameraInfo.m_nSaveImageBufSize = pFrameInfo.nFrameLen; } m_MyCameraInfo.m_stFrameInfo = pFrameInfo; CopyMemory(m_MyCameraInfo.m_pSaveImageBuf, pData, pFrameInfo.nFrameLen); } if (m_MyCameraInfo.m_stFrameInfo.nFrameLen != 0) { m_MyCameraInfo.isStartCatchPic = true; var curTime = DateTime.UtcNow; var fileName = string.Format($"Dev{m_MyCameraInfo.CanmeraId}_ti{m_MyCameraInfo.m_stFrameInfo.nHostTimeStamp}.jpeg"); if (!Directory.Exists(savePathByDB)) { Directory.CreateDirectory(savePathByDB); } MyCamera.MV_SAVE_IMG_TO_FILE_PARAM stSaveFileParam = new MyCamera.MV_SAVE_IMG_TO_FILE_PARAM(); stSaveFileParam.enImageType = MyCamera.MV_SAVE_IAMGE_TYPE.MV_Image_Jpeg; stSaveFileParam.enPixelType = m_MyCameraInfo.m_stFrameInfo.enPixelType; stSaveFileParam.pData = m_MyCameraInfo.m_pSaveImageBuf; stSaveFileParam.nDataLen = m_MyCameraInfo.m_stFrameInfo.nFrameLen; stSaveFileParam.nHeight = m_MyCameraInfo.m_stFrameInfo.nHeight; stSaveFileParam.nWidth = m_MyCameraInfo.m_stFrameInfo.nWidth; stSaveFileParam.nQuality = 80; stSaveFileParam.iMethodValue = 2; stSaveFileParam.pImagePath = Path.Combine(savePathByDB, fileName); nRet = m_MyCameraInfo.device_Hik.MV_CC_SaveImageToFile_NET(ref stSaveFileParam); if (MyCamera.MV_OK != nRet) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($" {m_MyCameraInfo.CanmeraId}图片保存失败"), StartTime = DateTime.UtcNow }); return ""; } else { //先清除字典中的无效数据 _cancheService.ClearInVainData(); //插入玻璃信息 var fileNameByDb = string.Format($"pack://siteoforigin:,,,/productPic/{fileName}"); var temperName = _cancheService.GetCurMaxTemperNo(); var insertRes = _cancheService.UpdateTemperingHistory(new TemperingHistory() { TemperName = temperName, TemperNo = _cancheService.GetCurMaxTemperNo(), ImageSource = fileNameByDb, StartTime = curTime, Num = 1, Status = true, GlassSize = "", }); //插入日志 if (insertRes) { _cancheService.UpdateLogging(new Logging() { Content = string.Format($"{temperName}成像完成"), StartTime = curTime }); } return fileNameByDb; } } return ""; } /// <summary> /// 更新检测相机状态 /// </summary> public bool UpdateCameraStatu() { var isNeedUpdate = false; var cameraList = _cancheService.GetCameraListByCache(); var parameterSettings = _cancheService.GetParameterSettings(); if (parameterSettings != null) { foreach (var item in cameraList) { var curStatu = item.device_Hik.MV_CC_IsDeviceConnected_NET(); if (item.isOpenCamera != curStatu) { isNeedUpdate = true; } item.isOpenCamera = curStatu; } if (isNeedUpdate) { parameterSettings.IsStart = string.Join(",", cameraList.Select(c => c.isOpenCamera)); _cancheService.UpdateParameterSettings(parameterSettings); } } return isNeedUpdate; } /// <summary> /// 关闭相机 /// </summary> /// <returns></returns> public string HK_Close() { var parameterSettings = _cancheService.GetParameterSettings(); var cameraList = _cancheService.GetCameraListByCache(); if (parameterSettings != null) { for (int i = 0; i < cameraList.Count; i++) { if (!cameraList[i].isOpenCamera) continue; var device_Hik = cameraList[i].device_Hik; var nRet = cameraList[i].nRet; if (cameraList[i].isStartCatchPic) { cameraList[i].isStartCatchPic = false; } nRet = device_Hik.MV_CC_StopGrabbing_NET(); nRet = device_Hik.MV_CC_CloseDevice_NET(); if (0 != nRet) if (0 != nRet) return " 海康相机: " + cameraList[i].snNo + HK_ErrorCode(" 海康相机关闭设备失败: ", nRet); nRet = device_Hik.MV_CC_DestroyDevice_NET(); if (0 != nRet) return " 海康相机: " + cameraList[i].snNo + HK_ErrorCode(" 海康相机销毁句柄失败: ", nRet); cameraList[i].isOpenCamera = false; parameterSettings.IsStart = string.Join(",", cameraList.Select(c => c.isOpenCamera)); _cancheService.UpdateParameterSettings(parameterSettings); _cancheService.UpdateLogging(new Logging() { Content = string.Format($"海康相机{parameterSettings.ChManufacturerName}关闭成功"), StartTime = DateTime.UtcNow }); } } return ""; } /// <summary> /// 错误代码 /// </summary> /// <param name="csMessage"></param> /// <param name="nErrorNum"></param> /// <returns></returns> private string HK_ErrorCode(string csMessage, int nErrorNum) { string errorMsg; if (nErrorNum == 0) { errorMsg = csMessage; } else { errorMsg = csMessage + ": Error =" + String.Format("{0:X}", nErrorNum); } switch (nErrorNum) { case MyCamera.MV_E_HANDLE: errorMsg += " Error or invalid handle "; break; case MyCamera.MV_E_SUPPORT: errorMsg += " Not supported function "; break; case MyCamera.MV_E_BUFOVER: errorMsg += " Cache is full "; break; case MyCamera.MV_E_CALLORDER: errorMsg += " Function calling order error "; break; case MyCamera.MV_E_PARAMETER: errorMsg += " Incorrect parameter "; break; case MyCamera.MV_E_RESOURCE: errorMsg += " Applying resource failed "; break; case MyCamera.MV_E_NODATA: errorMsg += " No data "; break; case MyCamera.MV_E_PRECONDITION: errorMsg += " Precondition error, or running environment changed "; break; case MyCamera.MV_E_VERSION: errorMsg += " Version mismatches "; break; case MyCamera.MV_E_NOENOUGH_BUF: errorMsg += " Insufficient memory "; break; case MyCamera.MV_E_UNKNOW: errorMsg += " Unknown error "; break; case MyCamera.MV_E_GC_GENERIC: errorMsg += " General error "; break; case MyCamera.MV_E_GC_ACCESS: errorMsg += " Node accessing condition error "; break; case MyCamera.MV_E_ACCESS_DENIED: errorMsg += " No permission "; break; case MyCamera.MV_E_BUSY: errorMsg += " Device is busy, or network disconnected "; break; case MyCamera.MV_E_NETER: errorMsg += " Network error "; break; } return errorMsg; } [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)] private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); } }
/// <summary> /// 相机信息 /// </summary> public class CameraInfo { public int CanmeraId { get; set; } public string CanmeraName { get; set; } public MyCamera device_Hik { get; set; } public string snNo { get; set; } public string chManufacturerName { get; set; } public int nRet { get; set; } /// <summary> /// 是否开启了抓图 /// </summary> public bool isStartCatchPic { get; set; } /// <summary> /// 是否打开了相机 /// </summary> public bool isOpenCamera { get; set; } /// <summary> /// 静态对象,用于锁住线程 /// </summary> public object m_BufForSaveImageLock { get; set; } = new Object(); public int m_nFrames { get; set; } public uint m_nSaveImageBufSize { get; set; } public IntPtr m_pSaveImageBuf { get; set; } public MyCamera.MV_FRAME_OUT_INFO_EX m_stFrameInfo { get; set; } /// <summary> /// 当前是否软触发 /// </summary> public bool isSoft { get; set; } public MyCamera.MV_CC_DEVICE_INFO device { get; set; } }
以上包含软触发和硬触发两种模式,通过TriggerMode方法触发
错误代码对照:
https://blog.csdn.net/qq_38960753/article/details/127374053
https://blog.csdn.net/qq_43445867/article/details/126283026
常见问题:
1.无法加载 dll“mvcameracontrol.dll”: 找不到指定的模块;缺少运行时,官网上有一个runtime需要安装【官网地址:https://www.hikrobotics.com/cn/machinevision/service/download/?module=0】

2.dll引用,分为两种方式,一种是引用dll,另一种是直接引用resource下的源文件,如果有框架版本的问题,建议直接使用第二种方式,直接拷贝源码文件【而且还能看到对应方法的注释】
3.mvs工具可以添加虚拟相机,方便代码调试,而且安装目录下面有对应的开发demo

浙公网安备 33010602011771号