English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french
查看: 15|回复: 0

海康工业相机的应用部署不是简简单单!?

[复制链接]
查看: 15|回复: 0

海康工业相机的应用部署不是简简单单!?

[复制链接]
查看: 15|回复: 0

319

主题

0

回帖

967

积分

高级会员

积分
967
wdc

319

主题

0

回帖

967

积分

高级会员

积分
967
2025-2-6 23:31:44 | 显示全部楼层 |阅读模式
作者:SkyXZ
CSDN:SkyXZ~-CSDN博客
博客园:SkyXZ - 博客园
笔者使用的设备及环境:WSL2-Ubuntu22.04+MV-CS016-10UC
        不会吧?不会吧?不会还有人拿到海康工业相机还是一脸懵叭?不会还有人觉得海康相机的API使用很难叭?不用慌!这篇文章从官方文档涵盖了海康相机官方MVS软件的使用以及Linux海康SDK-API包的理解上手,一篇文章带你走出海康相机使用新手村!!!让你一文读完之后便可自信的对朋友说:“海康相机?简简单单!!!”
参考资料:
一、海康官方MVS客户端使用教程

        我们首先根据自己的系统在官网下载我们的MVS客户端,我的系统为Linux,因此我选择下载第二个Linux版本的客户端

        下载完成后我们会得到一个ZIP压缩包,我们解压后可以看到有很多不同CPU架构的版本,我们选择适合自己系统的版本使用如下命令即可完成安装,安装完成后的客户端将在/opt/MVS目录下

sudo dpkg -i MVS-***********.deb #自行替换适合自己的系统版本安装包        我们使用如下命令即可进入MVS安装环境并且启动我们的客户端
cd /opt/MVS/bin #进入软件目录./MVS.sh #运行客户端        运行客户端后我们可以看到我们的客户端主界面如下,左边主要为我们的设备区在这个区域我们可以看到当前连接到我们电脑的所有海康相机设备,最上方为菜单栏提供文件、视图、设置、工具和帮助的功能,下方为控制工具条可以为相机参数保存等操作提供快捷入口

        接着我们选中我们的相机可以看到我们进入了如下界面,在这个界面中左下方为接口和设备信息获取窗口可以显示设备详细信息,中间为图像预览窗口上方左一按钮点击后即可开启相机实时取流查看图像,右边为属性设置窗口,可以对相机的基本参数进行设置,如:触发模式、增益、曝光时间、帧率、像素格式等等

        由于属性设置客户端中有中文介绍,那么我们接着主要开始讲解我们的SDK-API的使用方法叭
二、海康相机开发实战上手

        在我们上一节安装的MVS目录下存在着海康相机驱动所需要的全部头文件以及链接库,我们首先在我们的工作空间下面新建一个文件夹,接着将MVS安装目录下的include和lib文件全部复制进来
mkdir -P HK_Camera/src #创建工作目录及源码文件夹cd HK_Camera/ #进入工作目录cp -r /opt/MVS/include/ HK_Camera/  #复制头文件cp -r /opt/MVS/lib/ HK_Camera/ #复制链接库touch CmakeLists.txt #创建Cmake文件        我们首先来看一下海康相机组件包的项目结构叭,要驱动海康工业相机需要有两个主要的驱动包include和lib,其具体的结构以及对应的作用请看下图:
HK_Camera/
├── CmakeLists.txt
├── include/
│   ├── CameraParams.h
│   ├── MvCameraControl.h
│   ├── MvErrorDefine.h
│   ├── MvISPErrorDefine.h
│   ├── MvObsoleteInterfaces.h
│   ├── ObsoleteCamParams.h
│   └── PixelType.h
├── lib/
│   ├── 32/
│   ├── 64/
│   └── CLProtocol/
└── src/


  • CameraParams.h :定义了相机相关的参数结构体和枚举类型,包含了相机的基本信息结构(如GigE相机信息、USB相机信息等),定义了图像采集相关的参数(如图像大小、像素格式等),定义了相机工作模式(如单帧模式、连续采集模式等),定义了网络传输相关的参数
  • MvCameraControl.h :定义了相机控制的主要API接口,包含相机的初始化、连接、断开等基本操作函数,包含图像采集相关的函数(开始采集、停止采集、获取图像等),包含参数设置和获取的函数,包含文件操作相关的函数,是SDK的主要接口文件


  • MvErrorDefine.h :定义了SDK所有的错误码,包含通用错误码(0x80000000-0x800000FF)、GenICam错误码(0x80000100-0x800001FF)、GigE相关错误码(0x80000200-0x800002FF)、USB相关错误码(0x80000300-0x800003FF)、固件升级相关错误码(0x80000400-0x800004FF)
  • MvISPErrorDefine.h :定义了图像处理(ISP)相关的错误码,包含通用错误码、内存相关错误码、图像格式相关错误码、降噪处理相关错误码、去污点相关错误码
  • PixelType.h :定义了相机支持的所有像素格式类型(enum MvGvspPixelType),主要包含以下几类像素格式:Mono(单色): Mono8/10/12/16等、Bayer: BayerGR8/10/12, BayerRG8/10/12等、RGB: RGB8_Packed, BGR8_Packed等、YUV: YUV411/422/444等、3D点云相关格式
  • ObsoleteCamParams.h :定义了一些已过时但仍支持的相机参数结构体
  • MvObsoleteInterfaces.h :定义了一些已过时但仍支持的接口函数
        由于海康的这些头文件里的API有着非常非常详细的双语介绍(如下图),因此我们将以海康工业相机MV-CS016-10UC来主要介绍使用海康相机的API使用及相机的使用流程,类似于OpenCV调用免驱摄像头一般,海康摄像头的使用也主要分为四步,分别是初始化相机——>设置相机参数——>开始取图——>停止采集和清理资源,接下来我们将一边介绍主要使用的API一边手把手带着大家使能我们的海康相机,首先我们创建我们的main.cc
touch src/main.cc #创建文件        接着我们开始编写我们的Cmake文件,由于Cmake比较简单,我们便不详细的展开说明了:
#step 1 设置我们的项目以及版本最小需求cmake_minimum_required(VERSION 3.10)project(HK_Camera)#step 2 设置C++标准set(CMAKE_CXX_STANDARD 14)set(CMAKE_CXX_STANDARD_REQUIRED ON)#step 3 设置编译类型if(NOT CMAKE_BUILD_TYPE)    set(CMAKE_BUILD_TYPE Release)endif()#step 4 添加海康相机SDK路径set(MVCAM_SDK_PATH "${PROJECT_SOURCE_DIR}/lib/64")  # SDK库文件路径set(MVCAM_INCLUDE_PATH "${PROJECT_SOURCE_DIR}/include")  # SDK头文件路径#step 5 添加头文件路径include_directories(    ${PROJECT_SOURCE_DIR}/include    ${MVCAM_INCLUDE_PATH})#step 6 添加库文件路径link_directories(    ${MVCAM_SDK_PATH})#step 7 添加源文件add_executable(${PROJECT_NAME}     src/main.cc) #step 8 链接海康相机库target_link_libraries(${PROJECT_NAME}    MvCameraControl  # 海康相机主库    pthread         # 线程库)#step 9 设置编译选项target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -O2)#step 10 安装目标install(TARGETS ${PROJECT_NAME}    RUNTIME DESTINATION bin) (1)海康相机使用基本部署流程

        Cmake编写完成后我们开始编写我们的代码,首先导入一些我们需要的头文件
#include <iostream>#include <string> #include <cstring>  // for memset#include "MvCameraControl.h"#include "CameraParams.h"  // for MVCC_INTVALUE#include "PixelType.h"     // for PixelType_Gvsp_BGR8_Packed        接着为了便于我们的使用,我们首先创建一个HKCamera类,其中包含有五个主要的函数分别是InitCamera()、SetParameters()、StartGrabbing()、GetOneFrame()、StopGrabbing()分别用来初始化我们的相机、设置相机参数、开始取流、获取一帧图像以及停止采集释放资源
class HKCamera{    public:            bool InitCamera(); // 初始化相机            bool SetParameters(); // 设置相机参数            bool StartGrabbing(); // 开始取图            bool GetOneFrame(); // 获取一帧图像            void StopGrabbing() // 停止采集}        我们开始首先完成初始化相机的函数,要使用我们的海康相机那么首先肯定就需要先知道当前设备链接了哪些相机,因此我们便需要先使用MV_CC_EnumDevices函数来枚举当前链接上主机的网口orUSB海康相机,海康官方的API以及MV_CC_DEVICE_INFO_LIST结构体解析如下:
typedef struct _MV_CC_DEVICE_INFO_LIST_{    unsigned int        nDeviceNum;                    ///< [OUT] \~chinese 在线设备数量               MV_CC_DEVICE_INFO*  pDeviceInfo[MV_MAX_DEVICE_NUM];///< [OUT] \~chinese 支持最多256个设备      }MV_CC_DEVICE_INFO_LIST;/********************************************************************//** *  @~chinese *  @brief  枚举设备 *  @param  nTLayerType      [IN]            枚举传输层, 参数定义参见CameraParams.h定义, 如: #define MV_GIGE_DEVICE 0x00000001 GigE设备 *  @param  pstDevList       [IN][OUT]       设备列表 *  @return 成功,返回MV_OK;错误,返回错误码  *  @remarks 设备列表的内存是在SDK内部分配的,多线程调用该接口时会进行设备列表内存的释放和申请,建议尽量避免多线程枚举操作。 *  @remarks 参数枚举传输层,适配传入MV_GIGE_DEVICE、MV_1394_DEVICE、MV_USB_DEVICE、MV_CAMERALINK_DEVICE;MV_GIGE_DEVICE该参数             传出所有GiGE相关的设备信息(包含虚拟GiGE和GenTL下的GiGE设备),MV_USB_DEVICE该参数传出所有USB设备,包含虚拟USB设备。 ************************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_EnumDevices(IN unsigned int nTLayerType, IN OUT MV_CC_DEVICE_INFO_LIST* pstDevList);        因此根据API的描述,我们首先需要创建一个MV_CC_DEVICE_INFO_LIST类型的结构体,同时我们还需要定义一个nRet变量来获取调用API后返回的值来判断我们是否成功使用了API,因此我们的代码应该为下述形式:
MV_CC_DEVICE_INFO_LIST stDeviceList;//定义MV_CC_DEVICE_INFO_LIST类型的结构体stDeviceListmemset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));//将结构体stDeviceList内存清零,防止垃圾值影响int nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);// 枚举设备if (MV_OK != nRet) { //判断返回值为MV_OK即正常运行    std::cout << "枚举设备失败! nRet [" << nRet << "]" << std::endl;    return false;}        在枚举了相机之后如果相机数量不为零我们便可以选择开启我们的海康相机啦,我们首先查看一下打开相机的API介绍
/********************************************************************//** *  @~chinese *  @brief  打开设备 *  @param  handle        [IN] 设备句柄 *  @param  nAccessMode   [IN] 访问权限, 参数定义参见CameraParams.h定义, 如:#define MV_ACCESS_Exclusive 1  (仅对 MV_GIGE_DEVICE/MV_GENTL_GIGE_DEVICE 类型的设备有效) *  @param  nSwitchoverKey[IN] 切换访问权限时的密钥(仅对 MV_GIGE_DEVICE 类型的设备有效) *  @return 成功,返回MV_OK;错误,返回错误码 *  @remarks 根据设置的设备参数,找到对应的设备,连接设备, 调用接口时可不传入nAccessMode和nSwitchoverKey,此时默认设备访问模式为独占权限。            MV_GIGE_DEVICE 类型设备,目前相机固件暂不支持MV_ACCESS_ExclusiveWithSwitch、MV_ACCESS_ControlWithSwitch、MV_ACCESS_ControlSwitchEnable、MV_ACCESS_ControlSwitchEnableWithKey这四种抢占模式, SDK接口支持设置            MV_GENTL_GIGE_DEVICE 设备只支持 nAccessMode 是 MV_ACCESS_Exclusive 、MV_ACCESS_Control 、MV_ACCESS_Monitor权限            对于U3V设备,CXP,Cameralink(MV_CAMERALINK_DEVICE、MV_GENTL_CAMERALINK_DEVICE), Xof设备, 虚拟GEV, 虚拟U3V设备:nAccessMode、nSwitchoverKey这两个参数无效; 默认以控制权限打开设备;            该接口支持网口设备不枚举直接打开,不支持U口和GenTL设备不枚举打开设备 ************************************************************************/#ifndef __cplusplus //用于区分C和C++编译环境// C语言版本的函数声明MV_CAMCTRL_API int __stdcall MV_CC_OpenDevice(IN void* handle, IN unsigned int nAccessMode, IN unsigned short nSwitchoverKey);#else// C++语言版本的函数声明(带默认参数)MV_CAMCTRL_API int __stdcall MV_CC_OpenDevice(IN void* handle, IN unsigned int nAccessMode = MV_ACCESS_Exclusive, IN unsigned short nSwitchoverKey = 0);#endif        根据这个API的说明我们可以知道我们在使用这个函数前还需要利用MV_CC_CreateHandle这个API创建一个句柄同时一个设备同时只能被一个进程以独占方式打开,并且关闭设备时需要调用MV_CC_CloseDevice函数,同时我们了解到这个API还有一个可选参数nAccessMode,可以用来设置访问权限,分别有MV_ACCESS_Exclusive独占权限、MV_ACCESS_Control控制权限、MV_ACCESS_Monitor监控权限、为了打开我们的摄像头我们还需再次查看MV_CC_CreateHandleAPI介绍
/********************************************************************//** *  @~chinese *  @brief  创建设备句柄 *  @param  handle                      [IN][OUT]       设备句柄 *  @param  pstDevInfo                  [IN]            设备信息结构体 *  @return 成功,返回MV_OK;错误,返回错误码  *  @remarks 根据输入的设备信息,创建库内部必须的资源和初始化内部模块             通过该接口创建句柄,调用SDK接口,会默认生成SDK日志文件,如果不需要生成日志文件,可以将日志配置文件中的日志等级改成off ************************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_CreateHandle(IN OUT void ** handle, IN const MV_CC_DEVICE_INFO* pstDevInfo);        根据这个API的说明,我们需要使用之前创建的MV_CC_DEVICE_INFO_LIST类型结构体stDeviceList中的成员变量pDeviceInfo,于是我们的代码便应该如下:
if (stDeviceList.nDeviceNum > 0) {    nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[0]);    // 创建句柄    if (MV_OK != nRet) {        std::cout << "创建句柄失败! nRet [" << nRet << "]" << std::endl;        return false;    }    nRet = MV_CC_OpenDevice(handle);    // 打开设备    if (MV_OK != nRet) {        std::cout << "打开设备失败! nRet [" << nRet << "]" << std::endl;        return false;    }} else {    std::cout << "未找到设备!" << std::endl;    return false;}        至此我们的InitCamera()函数便搭建完成啦,完整代码如下:
bool InitCamera() {    MV_CC_DEVICE_INFO_LIST stDeviceList;    memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));    // 枚举设备    int nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);    if (MV_OK != nRet) {        std::cout << "枚举设备失败! nRet [" << nRet << "]" << std::endl;        return false;    }    if (stDeviceList.nDeviceNum > 0) {        // 创建句柄        nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[0]);        if (MV_OK != nRet) {            std::cout << "创建句柄失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 打开设备        nRet = MV_CC_OpenDevice(handle);        if (MV_OK != nRet) {            std::cout << "打开设备失败! nRet [" << nRet << "]" << std::endl;            return false;        }    } else {        std::cout << "未找到设备!" << std::endl;        return false;    }    return true;}        完成了初始化相机的函数接下来我们便来完成我们SetParameters()设置相机基本参数的函数,首先我们根据海康的用户手册可以知道我们有几个基本的参数可以进行设置,分别是相机的触发模式像素格式曝光时间增益Buff相机帧率等,我们在这里只对这四个常见参数进行设定,通过查阅API手册我们可以知道这四个参数设置分别对应以下两个APIMV_CC_SetEnumValue、MV_CC_SetFloatValue同时我们可以通过MV_CC_GetFloatValueAPI来获取每一个属性键值的可调节参数范围,这些函数的API以及参数列表中涉及到的结构体介绍如下:
/********************************************************************//** *  @~chinese *  @brief  设置Enum型属性值 *  @param  handle                      [IN]            设备句柄/采集卡句柄 *  @param  strKey                      [IN]            属性键值,如获取像素格式信息则为"PixelFormat" *  @param  nValue                      [IN]            想要设置的设备的属性值 *  @return 成功,返回MV_OK,失败,返回错误码 *  @remarks 连接设备之后调用该接口可以设置Enum类型的指定节点的值。 ************************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_SetEnumValue(IN void* handle,IN const char* strKey,IN unsigned int nValue);/********************************************************************//** *  @~chinese *  @brief  设置float型属性值 *  @param  handle                      [IN]            设备句柄/采集卡句柄 *  @param  strKey                      [IN]            属性键值 *  @param  fValue                      [IN]            想要设置的设备的属性值 *  @return 成功,返回MV_OK,失败,返回错误码 *  @remarks 连接设备之后调用该接口可以设置float类型的指定节点的值。 ************************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_SetFloatValue(IN void* handle,IN const char* strKey,IN float fValue);/********************************************************************//** *  @~chinese *  @brief  获取Float属性值 *  @param  handle                      [IN]            设备句柄/采集卡句柄 *  @param  strKey                      [IN]            属性键值 *  @param  pstFloatValue               [IN][OUT]       返回给调用者有关设备属性结构体指针 *  @return 成功,返回MV_OK,失败,返回错误码 *  @remarks 连接设备之后调用该接口可以获取float类型的指定节点的值。 ************************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_GetFloatValue(IN void* handle,IN const char* strKey,IN OUT MVCC_FLOATVALUE *pstFloatValue);/// \~chinese Float类型值               \~english Float Valuetypedef struct _MVCC_FLOATVALUE_T{    float               fCurValue;     ///< [OUT] \~chinese 当前值                     float               fMax;          ///< [OUT] \~chinese 最大值                   float               fMin;          ///< [OUT] \~chinese 最小值                unsigned int        nReserved[4];  ///<       \~chinese 预留      }MVCC_FLOATVALUE;        我们通过查阅用户手册可以知道海康工业相机的属性键值遵循GenICam标准,因此我们能调整和获取的常用属性键值有如下几个:
"AcquisitionFrameRate"      // 采集帧率"AcquisitionFrameRateEnable" // 帧率控制使能"ExposureTime"              // 曝光时间"ExposureAuto"              // 自动曝光"Gain"                      // 增益"GainAuto"                  // 自动增益"TriggerMode"               // 触发模式"TriggerSource"             // 触发源"Width"                     // 图像宽度"Height"                    // 图像高度        因此,根据上述的分析,我们首先设置我们相机的触发模式围为OFF内触发模式,同时设置我们的像素格式为BayerRG8
int nRet;nRet = MV_CC_SetEnumValue(handle, "TriggerMode", 0);// 设置触发模式为offif (MV_OK != nRet) {    std::cout << "设置触发模式失败! nRet [" << nRet << "]" << std::endl;    return false;}// 设置像素格式为BGR8nRet = MV_CC_SetEnumValue(handle, "PixelFormat", PixelType_Gvsp_BGR8_Packed);if (MV_OK != nRet) {    std::cout << "设置像素格式失败! nRet [" << nRet << "]" << std::endl;    return false;}std::cout << "设置像素格式为BGR8_Packed" << std::endl;        接着为了避免我们的参数设置有问题以及为了便于自检,我们首先利用MV_CC_GetFloatValue来获取当前我们使用的相机的每一个属性参数的可调节范围,再根据获取到的可调节范围来检查我们设置参数的可用性并进一步修改我们的参数,因此我们以曝光时间的设置为例子代码应该为如下形式:我们首先创建一个MVCC_FLOATVALUE形式的结构体stExposureTime用来获取曝光时间的可调节信息,接着判定我们设置的exposureTime是否超过了stExposureTime.fMin和stExposureTime.fMax的范围,如果没有那么我们便将exposureTime使用MV_CC_SetFloatValueAPI进行设置
// 获取和设置曝光时间MVCC_FLOATVALUE stExposureTime = {0};nRet = MV_CC_GetFloatValue(handle, "ExposureTime", &stExposureTime);if (MV_OK == nRet) {    float exposureTime = 10000.0f;  // 默认10ms    if (exposureTime < stExposureTime.fMin) exposureTime = stExposureTime.fMin;    if (exposureTime > stExposureTime.fMax) exposureTime = stExposureTime.fMax;    std::cout << "曝光时间范围: [" << stExposureTime.fMin << ", " << stExposureTime.fMax               << "], 当前设置: " << exposureTime << std::endl;    nRet = MV_CC_SetFloatValue(handle, "ExposureTime", exposureTime);    if (MV_OK != nRet) {        std::cout << "设置曝光时间失败! nRet [" << nRet << "]" << std::endl;        return false;    }}        至此我们的InitCamera()函数便搭建完成啦,完整代码如下:
bool SetParameters() {// 设置相机参数    int nRet;    nRet = MV_CC_SetEnumValue(handle, "TriggerMode", 0);// 设置触发模式为off    if (MV_OK != nRet) {        std::cout << "设置触发模式失败! nRet [" << nRet << "]" << std::endl;        return false;    }    // 设置像素格式为BGR8    nRet = MV_CC_SetEnumValue(handle, "PixelFormat", PixelType_Gvsp_BGR8_Packed);    if (MV_OK != nRet) {        std::cout << "设置像素格式失败! nRet [" << nRet << "]" << std::endl;        return false;    }    std::cout << "设置像素格式为BGR8_Packed" << std::endl;    // 获取和设置曝光时间    MVCC_FLOATVALUE stExposureTime = {0};    nRet = MV_CC_GetFloatValue(handle, "ExposureTime", &stExposureTime);    if (MV_OK == nRet) {        float exposureTime = 10000.0f;  // 默认10ms        if (exposureTime < stExposureTime.fMin) exposureTime = stExposureTime.fMin;        if (exposureTime > stExposureTime.fMax) exposureTime = stExposureTime.fMax;        std::cout << "曝光时间范围: [" << stExposureTime.fMin << ", " << stExposureTime.fMax                   << "], 当前设置: " << exposureTime << std::endl;        nRet = MV_CC_SetFloatValue(handle, "ExposureTime", exposureTime);        if (MV_OK != nRet) {            std::cout << "设置曝光时间失败! nRet [" << nRet << "]" << std::endl;            return false;        }    }    // 获取和设置增益    MVCC_FLOATVALUE stGain = {0};    nRet = MV_CC_GetFloatValue(handle, "Gain", &stGain);    if (MV_OK == nRet) {        std::cout << "增益范围: [" << stGain.fMin << ", " << stGain.fMax                   << "], 当前值: " << stGain.fCurValue << std::endl;        if (stGain.fCurValue < stGain.fMin || stGain.fCurValue > stGain.fMax) {            float gain = stGain.fMin;  // 使用最小值            nRet = MV_CC_SetFloatValue(handle, "Gain", gain);            if (MV_OK != nRet) {                std::cout << "设置增益失败! nRet [" << nRet << "]" << std::endl;                return false;            }        }    }    // 获取和设置帧率    MVCC_FLOATVALUE stFrameRate = {0};    nRet = MV_CC_GetFloatValue(handle, "AcquisitionFrameRate", &stFrameRate);    if (MV_OK == nRet) {        float frameRate = 30.0f;  // 默认30fps        if (frameRate < stFrameRate.fMin) frameRate = stFrameRate.fMin;        if (frameRate > stFrameRate.fMax) frameRate = stFrameRate.fMax;        std::cout << "帧率范围: [" << stFrameRate.fMin << ", " << stFrameRate.fMax                   << "], 当前设置: " << frameRate << std::endl;        nRet = MV_CC_SetFloatValue(handle, "AcquisitionFrameRate", frameRate);        if (MV_OK != nRet) {            std::cout << "设置帧率失败! nRet [" << nRet << "]" << std::endl;            // 这个错误不影响主要功能,可以继续        }    }    return true;}        完成了相机基本参数的设置我们便可以开始取流啦!!!这部分有同学就会问了,欸?我们前面就已经打开摄像头了,为什么这里还会有取流和取帧两个函数呢?这就是海康摄像头和OpenCV使用免驱摄像头逻辑上的区别了,我们可以把这个过程类比于我们电动抽水机,前面打开摄像头的函数可以理解为给抽水机上电,而我们这里的开始取流StartGrabbing()便是让图像暂存在相机内部的缓存中,可以理解为让抽水机开始工作一直出水,而我们的取帧函数GetOneFrame()便是从相机的缓存中读取一帧图像并将图像数据复制到我们准备好的内存中,可以理解为拿一个水桶来接水,这样我们便可以随接随用
        理解了为什么我们便开始讲解取流函数StartGrabbing(),在海康的API里面有一个专门的函数便是用来取流的,他的API介绍如下:
/********************************************************************//** *  @~chinese *  @brief  开始取流 *  @param  handle                      [IN]            设备句柄 *  @return 成功,返回MV_OK;错误,返回错误码 *  @remarks 该接口不支持MV_CAMERALINK_DEVICE 类型的设备。 ***********************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_StartGrabbing(IN void* handle);        可以看到使用这个函数非常的简单,和我们上面的操作非常类似,只要将我们创建的句柄传入其中即可,按照如下代码调用了之后我们的相机便会开始取流并将图像存储在相机内部的缓存里面临时存储
int nRet = MV_CC_StartGrabbing(handle);// 开始取流if (MV_OK != nRet) {    std::cout << "开始取流失败! nRet [" << nRet << "]" << std::endl;    return false;}        接着我们便可以获取当前属性参数下的图像数据包的大小,由于这个属性参数不变那么每帧的图像数据大小的范围便也不会变,因此我们只需要获取一次数据包的大小便可以,所以我们将数据包大小获取放在开始取流的函数里面,我们看到他的API介绍如下:
/************************************************************************ *  @fn     MV_CAMCTRL_API int __stdcall MV_CC_GetIntValue(IN void* handle,                                                           IN const char* strKey,                                                           OUT MVCC_INTVALUE *pIntValue); *  @brief  获取Integer属性值(建议改用MV_CC_GetIntValueEx接口) *  @param  void* handle                [IN]        相机句柄 *  @param  char* strKey                [IN]        属性键值,如获取宽度信息则为"Width" *  @param  MVCC_INTVALUE* pstValue     [IN][OUT]   返回给调用者有关相机属性结构体指针 *  @return 成功,返回MV_OK,失败,返回错误码 ************************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_GetIntValue(IN void* handle,IN const char* strKey,OUT MVCC_INTVALUE *pIntValue);        其中还涉及到了一个数据包结构体MVCC_INTVALUE,求定义如下:
typedef struct _MVCC_INTVALUE_T{    unsigned int        nCurValue; ///< [OUT] \~chinese 当前值                     unsigned int        nMax;      ///< [OUT] \~chinese 最大值                  unsigned int        nMin;      ///< [OUT] \~chinese 最小值              unsigned int        nInc;      ///< [OUT] \~chinese                 unsigned int        nReserved[4];///<       \~chinese 预留             }MVCC_INTVALUE;        了解了其定义后我们便可以使用这个API来获取数据包的大小并用malloc函数来分配我们主机的内存
// 获取数据包大小MVCC_INTVALUE stParam;nRet = MV_CC_GetIntValue(handle, "PayloadSize", &stParam);if (MV_OK != nRet) {    std::cout << "获取数据包大小失败! nRet [" << nRet << "]" << std::endl;    return false;}// 分配资源nDataSize = stParam.nCurValue;pData = (unsigned char*)malloc(nDataSize);if (pData == nullptr) {    std::cout << "内存分配失败!" << std::endl;    return false;}        至此我们的StartGrabbing()函数便搭建完成啦,完整代码如下:
bool StartGrabbing() {    // 开始取流    int nRet = MV_CC_StartGrabbing(handle);    if (MV_OK != nRet) {        std::cout << "开始取流失败! nRet [" << nRet << "]" << std::endl;        return false;    }    // 获取数据包大小    MVCC_INTVALUE stParam;    nRet = MV_CC_GetIntValue(handle, "PayloadSize", &stParam);    if (MV_OK != nRet) {        std::cout << "获取数据包大小失败! nRet [" << nRet << "]" << std::endl;        return false;    }    // 分配资源    nDataSize = stParam.nCurValue;    pData = (unsigned char*)malloc(nDataSize);    if (pData == nullptr) {        std::cout << "内存分配失败!" << std::endl;        return false;    }    return true;}        接着我们再来编写我们的取帧函数GetOneFrame(),海康给了我们一个专门的获取一帧图像的API,具体其介绍如下,我们可以看到这个API采用的是超时机制,因此SDK内部会一直等待直到有数据时才会返回一帧图像
/********************************************************************//** *  @~chinese *  @brief  采用超时机制获取一帧图片,SDK内部等待直到有数据时返回 *  @param  handle                      [IN]            设备句柄 *  @param  pData                       [IN][OUT]       图像数据接收指针 *  @param  nDataSize                   [IN]            接收缓存大小 *  @param  pstFrameInfo                [IN][OUT]       图像信息结构体 *  @param  nMsec                       [IN]            等待超时时间 *  @return 成功,返回MV_OK;错误,返回错误码 *  @remarks 调用该接口获取图像数据帧之前需要先调用MV_CC_StartGrabbing启动图像采集             该接口为主动式获取帧数据,上层应用程序需要根据帧率,控制好调用该接口的频率             该接口支持设置超时时间,SDK内部等待直到有数据时返回,可以增加取流平稳性,适合用于对平稳性要求较高的场合             该接口对于U3V、GIGE设备均可支持             该接口不支持CameraLink设备。 ***********************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_GetOneFrameTimeout(IN void* handle, IN OUT unsigned char* pData , IN unsigned int nDataSize, IN OUT MV_FRAME_OUT_INFO_EX* pstFrameInfo, IN unsigned int nMsec);        因此我们便可以用如下的方式来使用这个API,同时我们在这里再加上一次错误判定,以防止句柄或者分配的内存地址指针为空:
if (handle == nullptr || pData == nullptr) {    return false;}int nRet = MV_CC_GetOneFrameTimeout(handle, pData, nDataSize, &stImageInfo, 1000);if (MV_OK != nRet) {    std::cout << "获取一帧图像失败! nRet [" << nRet << "]" << std::endl;    return false;}std::cout << "获取一帧图像成功: Width[" << stImageInfo.nWidth       << "] Height[" << stImageInfo.nHeight           << "] FrameNum[" << stImageInfo.nFrameNum << "]" << std::endl;return true;        至此我们的StartGrabbing()函数便搭建完成啦,完整代码如下:
// 获取一帧图像bool GetOneFrame() {    if (handle == nullptr || pData == nullptr) {        return false;    }    int nRet = MV_CC_GetOneFrameTimeout(handle, pData, nDataSize, &stImageInfo, 1000);    if (MV_OK != nRet) {        std::cout << "获取一帧图像失败! nRet [" << nRet << "]" << std::endl;        return false;    }    std::cout << "获取一帧图像成功: Width[" << stImageInfo.nWidth               << "] Height[" << stImageInfo.nHeight               << "] FrameNum[" << stImageInfo.nFrameNum << "]" << std::endl;    return true;}        最后我们只需要编写释放资源的函数StopGrabbing()便大功告成啦!释放资源比较简单,我们只需要按照我们开始的顺序倒叙关闭即可,即:停止取流、关闭相机、摧毁句柄,最后释放我们主机的内存即可,他们对应的API介绍如下:
/********************************************************************//** *  @~chinese *  @brief  停止取流 *  @param  handle                      [IN]            设备句柄 *  @return 成功,返回MV_OK;错误,返回错误码 *  @remarks 该接口不支持MV_CAMERALINK_DEVICE 类型的设备。 ***********************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_StopGrabbing(IN void* handle);/********************************************************************//** *  @~chinese *  @brief  关闭设备 *  @param  handle                      [IN]            设备句柄 *  @return 成功,返回MV_OK;错误,返回错误码 *  @remarks 通过MV_CC_OpenDevice连接设备后,可以通过该接口断开设备连接,释放资源 ***********************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_CloseDevice(IN void* handle);/********************************************************************//** *  @~chinese *  @brief  销毁设备句柄 *  @param  handle                      [IN]            设备句柄 *  @return 成功,返回MV_OK;错误,返回错误码  *  @remarks MV_CC_DestroyHandle 如果传入采集卡句柄,其效果和 MV_CC_DestroyInterface 相同; ************************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_DestroyHandle(IN void * handle);        因此我们最后的函数的代码长这样:
// 停止采集void StopGrabbing() {    if (handle != nullptr) {        MV_CC_StopGrabbing(handle);        MV_CC_CloseDevice(handle);        MV_CC_DestroyHandle(handle);        handle = nullptr;    }    if (pData != nullptr) {        free(pData);        pData = nullptr;    }}        最后我们只需要在主函数依次进行调用即可,这部分代码比较简单,我相信来了解海康工业相机的同学这部分不需要再展开细讲啦:
int main() {    HKCamera camera;    // 初始化相机    if (!camera.InitCamera()) {        std::cout << "相机初始化失败!" << std::endl;        return -1;    }    std::cout << "相机初始化成功!" << std::endl;    // 设置参数    if (!camera.SetParameters()) {        std::cout << "设置相机参数失败!" << std::endl;        return -1;    }    std::cout << "设置相机参数成功!" << std::endl;    // 开始取图    if (!camera.StartGrabbing()) {        std::cout << "开始取图失败!" << std::endl;        return -1;    }    std::cout << "开始取图成功!" << std::endl;    // 获取10帧图像    for (int i = 0; i < 10; i++) {        if (!camera.GetOneFrame()) {            break;        }    }    // 停止采集    camera.StopGrabbing();    std::cout << "停止采集完成!" << std::endl;    return 0;}        最后我们完整的代码及运行效果如下:(但是又有同学要问了,我们该如何显示图像呢?请继续翻到下面,我将在下面进行介绍)

#include <iostream>#include <string>#include <cstring>  // for memset#include "MvCameraControl.h"#include "CameraParams.h"  // for MVCC_INTVALUE#include "PixelType.h"     // for PixelType_Gvsp_BGR8_Packedclass HKCamera {private:    void* handle = nullptr;    MVCC_INTVALUE stParam;    MV_FRAME_OUT_INFO_EX stImageInfo = {0};    unsigned char* pData = nullptr;    unsigned int nDataSize = 0;public:    bool InitCamera() {        MV_CC_DEVICE_INFO_LIST stDeviceList;        memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));        // 枚举设备        int nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);        if (MV_OK != nRet) {            std::cout << "枚举设备失败! nRet [" << nRet << "]" << std::endl;            return false;        }        if (stDeviceList.nDeviceNum > 0) {            // 创建句柄            nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[0]);            if (MV_OK != nRet) {                std::cout << "创建句柄失败! nRet [" << nRet << "]" << std::endl;                return false;            }            // 打开设备            nRet = MV_CC_OpenDevice(handle);            if (MV_OK != nRet) {                std::cout << "打开设备失败! nRet [" << nRet << "]" << std::endl;                return false;            }        } else {            std::cout << "未找到设备!" << std::endl;            return false;        }        return true;    }    // 设置相机参数    bool SetParameters() {        int nRet;        // 设置触发模式为off        nRet = MV_CC_SetEnumValue(handle, "TriggerMode", 0);        if (MV_OK != nRet) {            std::cout << "设置触发模式失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 设置像素格式为BGR8        nRet = MV_CC_SetEnumValue(handle, "PixelFormat", PixelType_Gvsp_BGR8_Packed);        if (MV_OK != nRet) {            std::cout << "设置像素格式失败! nRet [" << nRet << "]" << std::endl;            return false;        }        std::cout << "设置像素格式为BGR8_Packed" << std::endl;        // 获取和设置曝光时间        MVCC_FLOATVALUE stExposureTime = {0};        nRet = MV_CC_GetFloatValue(handle, "ExposureTime", &stExposureTime);        if (MV_OK == nRet) {            float exposureTime = 10000.0f;  // 默认10ms            if (exposureTime < stExposureTime.fMin) exposureTime = stExposureTime.fMin;            if (exposureTime > stExposureTime.fMax) exposureTime = stExposureTime.fMax;                        std::cout << "曝光时间范围: [" << stExposureTime.fMin << ", " << stExposureTime.fMax                       << "], 当前设置: " << exposureTime << std::endl;                        nRet = MV_CC_SetFloatValue(handle, "ExposureTime", exposureTime);            if (MV_OK != nRet) {                std::cout << "设置曝光时间失败! nRet [" << nRet << "]" << std::endl;                return false;            }        }        // 获取和设置增益        MVCC_FLOATVALUE stGain = {0};        nRet = MV_CC_GetFloatValue(handle, "Gain", &stGain);        if (MV_OK == nRet) {            std::cout << "增益范围: [" << stGain.fMin << ", " << stGain.fMax                       << "], 当前值: " << stGain.fCurValue << std::endl;                        if (stGain.fCurValue < stGain.fMin || stGain.fCurValue > stGain.fMax) {                float gain = stGain.fMin;  // 使用最小值                nRet = MV_CC_SetFloatValue(handle, "Gain", gain);                if (MV_OK != nRet) {                    std::cout << "设置增益失败! nRet [" << nRet << "]" << std::endl;                    return false;                }            }        }        // 获取和设置帧率        MVCC_FLOATVALUE stFrameRate = {0};        nRet = MV_CC_GetFloatValue(handle, "AcquisitionFrameRate", &stFrameRate);        if (MV_OK == nRet) {            float frameRate = 30.0f;  // 默认30fps            if (frameRate < stFrameRate.fMin) frameRate = stFrameRate.fMin;            if (frameRate > stFrameRate.fMax) frameRate = stFrameRate.fMax;                        std::cout << "帧率范围: [" << stFrameRate.fMin << ", " << stFrameRate.fMax                       << "], 当前设置: " << frameRate << std::endl;                        nRet = MV_CC_SetFloatValue(handle, "AcquisitionFrameRate", frameRate);            if (MV_OK != nRet) {                std::cout << "设置帧率失败! nRet [" << nRet << "]" << std::endl;                // 这个错误不影响主要功能,可以继续            }        }        return true;    }    // 开始取图    bool StartGrabbing() {        // 开始取流        int nRet = MV_CC_StartGrabbing(handle);        if (MV_OK != nRet) {            std::cout << "开始取流失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 获取数据包大小        MVCC_INTVALUE stParam;        nRet = MV_CC_GetIntValue(handle, "PayloadSize", &stParam);        if (MV_OK != nRet) {            std::cout << "获取数据包大小失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 分配资源        nDataSize = stParam.nCurValue;        pData = (unsigned char*)malloc(nDataSize);        if (pData == nullptr) {            std::cout << "内存分配失败!" << std::endl;            return false;        }        return true;    }    // 获取一帧图像    bool GetOneFrame() {        if (handle == nullptr || pData == nullptr) {            return false;        }        int nRet = MV_CC_GetOneFrameTimeout(handle, pData, nDataSize, &stImageInfo, 1000);        if (MV_OK != nRet) {            std::cout << "获取一帧图像失败! nRet [" << nRet << "]" << std::endl;            return false;        }        std::cout << "获取一帧图像成功: Width[" << stImageInfo.nWidth                   << "] Height[" << stImageInfo.nHeight                   << "] FrameNum[" << stImageInfo.nFrameNum << "]" << std::endl;        return true;    }    // 停止采集    void StopGrabbing() {        if (handle != nullptr) {            MV_CC_StopGrabbing(handle);            MV_CC_CloseDevice(handle);            MV_CC_DestroyHandle(handle);            handle = nullptr;        }        if (pData != nullptr) {            free(pData);            pData = nullptr;        }    }    ~HKCamera() {        StopGrabbing();    }};int main() {    HKCamera camera;    // 初始化相机    if (!camera.InitCamera()) {        std::cout << "相机初始化失败!" << std::endl;        return -1;    }    std::cout << "相机初始化成功!" << std::endl;    // 设置参数    if (!camera.SetParameters()) {        std::cout << "设置相机参数失败!" << std::endl;        return -1;    }    std::cout << "设置相机参数成功!" << std::endl;    // 开始取图    if (!camera.StartGrabbing()) {        std::cout << "开始取图失败!" << std::endl;        return -1;    }    std::cout << "开始取图成功!" << std::endl;    // 获取10帧图像    for (int i = 0; i < 10; i++) {        if (!camera.GetOneFrame()) {            break;        }    }    // 停止采集    camera.StopGrabbing();    std::cout << "停止采集完成!" << std::endl;    return 0;}(2)海康相机+OpenCV实时取流部署教程

        如果我们要显示采集的图像的话我们有两个方法,首先便是我们的OpenCV,再然后便是海康提供给我们的图像显示API,我们首先介绍大家都会的OpenCV的方案,我们先新引入OpenCV的头文件:
#include <opencv2/opencv.hpp>        接着我们在构造函数部分新增CV图像显示的Frame以及我们在构造函数中初始化我们的输出帧和数据包大小结构体:
class HKCamera {    private:            cv::Mat frame;  // OpenCV图像    public:        HKCamera() {            // 在构造函数中初始化结构体            memset(&stParam, 0, sizeof(MVCC_INTVALUE));            memset(&stImageInfo, 0, sizeof(MV_FRAME_OUT_INFO_EX));        }        然后我们把GetOneFrame()函数变为GetOneFrameAndShow()然后在函数里面新增OpenCV获取帧的代码,将海康与OpenCV进行桥接,由于我们前面设置的采集格式为BGR因此我们可以直接使用BGR数据创建Mat:
bool GetOneFrameAndShow() {    if (handle == nullptr || pData == nullptr) {        return false;    }    int nRet = MV_CC_GetOneFrameTimeout(handle, pData, nDataSize, &stImageInfo, 1000);    if (MV_OK != nRet) {        std::cout << "获取一帧图像失败! nRet [" << nRet << "]" << std::endl;        return false;    }    std::cout << "获取一帧图像成功: Width[" << stImageInfo.nWidth               << "] Height[" << stImageInfo.nHeight               << "] FrameNum[" << stImageInfo.nFrameNum << "]" << std::endl;/*---------------------添加以下图像转换及显示代码-----------------------*/    // 转换为OpenCV格式并显示    if (stImageInfo.enPixelType == PixelType_Gvsp_BGR8_Packed) {        // 直接使用BGR数据创建Mat        frame = cv::Mat(stImageInfo.nHeight, stImageInfo.nWidth, CV_8UC3, pData);        // 简单的图像增强        cv::Mat temp;        // 轻微提升对比度        frame.convertTo(temp, -1, 1.1, 0);        frame = temp;    } else {        std::cout << "不支持的像素格式: 0x" << std::hex << stImageInfo.enPixelType << std::dec << std::endl;        return false;    }    if (!frame.empty()) {        cv::imshow("Camera", frame);        cv::waitKey(1);    }/*---------------------添加以上图像转换及显示代码-----------------------*/    return true;}        然后我们在主函数里把原先的获取10帧图像修改为while循环持续获得图像,这样便可以实时显示我们摄像头获取的图像:
// 持续获取并显示图像,直到按下ESC键while (true) {    if (!camera.GetOneFrameAndShow()) {        break;    }    // 检查ESC键是否按下    char key = cv::waitKey(1);    if (key == 27) {  // ESC键的ASCII码        break;    }        最后我们在CmakeLists.txt中添加OpenCV的依赖即可:
# 查找OpenCV包find_package(OpenCV REQUIRED)include_directories(    ${PROJECT_SOURCE_DIR}/include    ${MVCAM_INCLUDE_PATH}    ${OpenCV_INCLUDE_DIRS})# 链接海康相机库和OpenCV库target_link_libraries(${PROJECT_NAME}    MvCameraControl  # 海康相机主库    pthread         # 线程库    ${OpenCV_LIBS}  # OpenCV库)        最后我们OpenCV显示图像的完整的代码及显示效果如下:


#include <iostream>#include <string>#include <cstring>  // for memset#include <opencv2/opencv.hpp>#include "MvCameraControl.h"#include "CameraParams.h"  // for MVCC_INTVALUE#include "PixelType.h"     // for PixelType_Gvsp_BGR8_Packedclass HKCamera {private:    void* handle = nullptr;    MVCC_INTVALUE stParam;    MV_FRAME_OUT_INFO_EX stImageInfo = {0};    unsigned char* pData = nullptr;    unsigned int nDataSize = 0;    cv::Mat frame;  // OpenCV图像public:    HKCamera() {        // 在构造函数中初始化结构体        memset(&stParam, 0, sizeof(MVCC_INTVALUE));        memset(&stImageInfo, 0, sizeof(MV_FRAME_OUT_INFO_EX));    }    bool InitCamera() {        MV_CC_DEVICE_INFO_LIST stDeviceList;        memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));        // 枚举设备        int nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);        if (MV_OK != nRet) {            std::cout << "枚举设备失败! nRet [" << nRet << "]" << std::endl;            return false;        }        if (stDeviceList.nDeviceNum > 0) {            // 创建句柄            nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[0]);            if (MV_OK != nRet) {                std::cout << "创建句柄失败! nRet [" << nRet << "]" << std::endl;                return false;            }            // 打开设备            nRet = MV_CC_OpenDevice(handle);            if (MV_OK != nRet) {                std::cout << "打开设备失败! nRet [" << nRet << "]" << std::endl;                return false;            }        } else {            std::cout << "未找到设备!" << std::endl;            return false;        }        return true;    }    // 设置相机参数    bool SetParameters() {        int nRet;        // 设置触发模式为off        nRet = MV_CC_SetEnumValue(handle, "TriggerMode", 0);        if (MV_OK != nRet) {            std::cout << "设置触发模式失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 设置像素格式为BGR8        nRet = MV_CC_SetEnumValue(handle, "PixelFormat", PixelType_Gvsp_BGR8_Packed);        if (MV_OK != nRet) {            std::cout << "设置像素格式失败! nRet [" << nRet << "]" << std::endl;            return false;        }        std::cout << "设置像素格式为BGR8_Packed" << std::endl;        // 获取和设置曝光时间        MVCC_FLOATVALUE stExposureTime = {0};        nRet = MV_CC_GetFloatValue(handle, "ExposureTime", &stExposureTime);        if (MV_OK == nRet) {            float exposureTime = 10000.0f;  // 默认10ms            if (exposureTime < stExposureTime.fMin) exposureTime = stExposureTime.fMin;            if (exposureTime > stExposureTime.fMax) exposureTime = stExposureTime.fMax;                        std::cout << "曝光时间范围: [" << stExposureTime.fMin << ", " << stExposureTime.fMax                       << "], 当前设置: " << exposureTime << std::endl;                        nRet = MV_CC_SetFloatValue(handle, "ExposureTime", exposureTime);            if (MV_OK != nRet) {                std::cout << "设置曝光时间失败! nRet [" << nRet << "]" << std::endl;                return false;            }        }        // 获取和设置增益        MVCC_FLOATVALUE stGain = {0};        nRet = MV_CC_GetFloatValue(handle, "Gain", &stGain);        if (MV_OK == nRet) {            std::cout << "增益范围: [" << stGain.fMin << ", " << stGain.fMax                       << "], 当前值: " << stGain.fCurValue << std::endl;            if (stGain.fCurValue < stGain.fMin || stGain.fCurValue > stGain.fMax) {                float gain = stGain.fMin;  // 使用最小值                nRet = MV_CC_SetFloatValue(handle, "Gain", gain);                if (MV_OK != nRet) {                    std::cout << "设置增益失败! nRet [" << nRet << "]" << std::endl;                    return false;                }            }        }        // 获取和设置帧率        MVCC_FLOATVALUE stFrameRate = {0};        nRet = MV_CC_GetFloatValue(handle, "AcquisitionFrameRate", &stFrameRate);        if (MV_OK == nRet) {            float frameRate = 30.0f;  // 默认30fps            if (frameRate < stFrameRate.fMin) frameRate = stFrameRate.fMin;            if (frameRate > stFrameRate.fMax) frameRate = stFrameRate.fMax;                        std::cout << "帧率范围: [" << stFrameRate.fMin << ", " << stFrameRate.fMax                       << "], 当前设置: " << frameRate << std::endl;            nRet = MV_CC_SetFloatValue(handle, "AcquisitionFrameRate", frameRate);            if (MV_OK != nRet) {                std::cout << "设置帧率失败! nRet [" << nRet << "]" << std::endl;                // 这个错误不影响主要功能,可以继续            }        }        return true;    }    // 开始取图    bool StartGrabbing() {        // 开始取流        int nRet = MV_CC_StartGrabbing(handle);        if (MV_OK != nRet) {            std::cout << "开始取流失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 获取数据包大小        MVCC_INTVALUE stParam;        nRet = MV_CC_GetIntValue(handle, "PayloadSize", &stParam);        if (MV_OK != nRet) {            std::cout << "获取数据包大小失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 分配资源        nDataSize = stParam.nCurValue;        pData = (unsigned char*)malloc(nDataSize);        if (pData == nullptr) {            std::cout << "内存分配失败!" << std::endl;            return false;        }        return true;    }    // 获取一帧图像并显示    bool GetOneFrameAndShow() {        if (handle == nullptr || pData == nullptr) {            return false;        }        int nRet = MV_CC_GetOneFrameTimeout(handle, pData, nDataSize, &stImageInfo, 1000);        if (MV_OK != nRet) {            std::cout << "获取一帧图像失败! nRet [" << nRet << "]" << std::endl;            return false;        }        std::cout << "获取一帧图像成功: Width[" << stImageInfo.nWidth                   << "] Height[" << stImageInfo.nHeight                   << "] PixelType[0x" << std::hex << stImageInfo.enPixelType << std::dec                  << "] FrameNum[" << stImageInfo.nFrameNum << "]" << std::endl;        // 转换为OpenCV格式并显示        if (stImageInfo.enPixelType == PixelType_Gvsp_BGR8_Packed) {            // 直接使用BGR数据创建Mat            frame = cv::Mat(stImageInfo.nHeight, stImageInfo.nWidth, CV_8UC3, pData);            // 简单的图像增强            cv::Mat temp;            // 轻微提升对比度            frame.convertTo(temp, -1, 1.1, 0);            frame = temp;        } else {            std::cout << "不支持的像素格式: 0x" << std::hex << stImageInfo.enPixelType << std::dec << std::endl;            return false;        }        if (!frame.empty()) {            cv::imshow("Camera", frame);            cv::waitKey(1);        }        return true;    }    // 停止采集    void StopGrabbing() {        if (handle != nullptr) {            MV_CC_StopGrabbing(handle);            MV_CC_CloseDevice(handle);            MV_CC_DestroyHandle(handle);            handle = nullptr;        }        if (pData != nullptr) {            free(pData);            pData = nullptr;        }    }    ~HKCamera() {        StopGrabbing();    }};int main() {    HKCamera camera;    // 初始化相机    if (!camera.InitCamera()) {        std::cout << "相机初始化失败!" << std::endl;        return -1;    }    std::cout << "相机初始化成功!" << std::endl;    // 设置参数    if (!camera.SetParameters()) {        std::cout << "设置相机参数失败!" << std::endl;        return -1;    }    std::cout << "设置相机参数成功!" << std::endl;    // 开始取图    if (!camera.StartGrabbing()) {        std::cout << "开始取图失败!" << std::endl;        return -1;    }    std::cout << "开始取图成功!" << std::endl;    // 持续获取并显示图像,直到按下ESC键    while (true) {        if (!camera.GetOneFrameAndShow()) {            break;        }        // 检查ESC键是否按下        char key = cv::waitKey(1);        if (key == 27) {  // ESC键的ASCII码            break;        }    }    // 停止采集    camera.StopGrabbing();    std::cout << "停止采集完成!" << std::endl;    return 0;}(3)海康相机官方简易API+X11窗口实时取流部署教程

        但是我们会发现,如果我们摄像头采集的是BGR格式那么用OpenCV会比较方便,但是如果我们采集的不是BGR格式呢?那用OpenCV来显示图像便还需要进行图像的转换,非常的复杂!!!但是如果我们用海康官方的API再加Linux的OpenGL来显示图像那么我们便无需关心图像格式的转换问题啦!接下来我们便开始用海康官方显示API+Linux-OpenGL-X11来实时显示我们的图像,海康API中有关显示的函数有三个(其中一个即将被废除),具体介绍如下:
/********************************************************************//** *  @~chinese *  @brief  显示一帧图像 *  @param  handle                      [IN]            设备句柄 *  @param  pstDisplayInfo              [IN]            图像信息 *  @return 成功,返回MV_OK;错误,返回错误码  *  @remarks 与设备类型无关,渲染模式为D3D时,支持的最大分辨率为16384 * 163840 ***********************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_DisplayOneFrame(IN void* handle, IN MV_DISPLAY_FRAME_INFO* pstDisplayInfo);/********************************************************************//** *  @~chinese *  @brief  显示一帧图像 *  @param  handle                      [IN]            设备句柄 *  @param  hWnd                        [IN]            窗口句柄 *  @param  pstDisplayInfo              [IN]            图像信息 *  @return 成功,返回MV_OK;错误,返回错误码 *  @remarks 该接口支持渲染宽高大小至int类型 *           渲染模式为D3D时,支持的最大分辨率为16384 * 163840 ***********************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_DisplayOneFrameEx(IN void* handle, IN void* hWnd, IN MV_DISPLAY_FRAME_INFO_EX* pstDisplayInfo);/********************************************************************//** *  @~chinese *  @brief  显示一帧图像 *  @param  handle                      [IN]            设备句柄 *  @param  hWnd                        [IN]            窗口句柄 *  @param  pstImage                    [IN]            图像信息 *  @param  enRenderMode                [IN]            渲染方式,Windows:0-GDI 1-D3D 2-OpenGL Linux:0-OpenGL  *  @return 成功,返回MV_OK;错误,返回错误码 *  @remarks 可选择OpenGL渲染模式,支持PixelType_Gvsp_RGB8_Packed,PixelType_Gvsp_BGR8_Packed,PixelType_Gvsp_Mono8三种像素格式图像大小超过4GB的渲染,其他渲染模式不支持。             若图像大小未超过4GB,支持宽高大小至int类型             调用时需要输入MV_CC_IMAGE结构体中nImageLen的值                         渲染模式为D3D时,支持的最大分辨率为16384 * 163840 ***********************************************************************/MV_CAMCTRL_API int __stdcall MV_CC_DisplayOneFrameEx2(IN void* handle, IN void* hWnd, IN MV_CC_IMAGE* pstImage, unsigned int enRenderMode);        MV_CC_DisplayOneFrame()函数为基础函数他的功能最简单但分辨率限制较大并且即将被废除、MV_CC_DisplayOneFrameEx()函数为扩展函数,他支持更大分辨率也支持Windows上的D3D渲染但是不支持超大图像而MV_CC_DisplayOneFrameEx2()函数支持选择渲染模式支持超大图像也支持更多像素格式且性能最好
        接下来我们将以最简单的MV_CC_DisplayOneFrame()函数来完成示例,我们使用X11窗口来显示图像,首先我们导入X11包,接着我们在类中新增创建窗口句柄函数和显示函数并且修改我们的取帧函数:
#include <X11/Xlib.h>class HKCamera {private:    void* displayHandle = nullptr;  // 显示窗口句柄public:    HKCamera() {        // 在构造函数中正确初始化结构体        memset(&stParam, 0, sizeof(MVCC_INTVALUE));        memset(&stImageInfo, 0, sizeof(MV_FRAME_OUT_INFO_EX));    }    bool SetDisplayWindow(void* windowHandle) //创建显示窗口句柄    bool DisplayOneFrame() //新建显示    bool GetOneFrameAndShow()//修改GetOneFrame()函数        接着我们需要利用我们的SetDisplayWindow(void* windowHandle)函数来创建一个X11窗口的句柄:
// 设置显示窗口bool SetDisplayWindow(void* windowHandle) {    displayHandle = windowHandle;    return true;}        然后我们来创建我们的DisplayOneFrame()函数,根据我们上面的函数介绍,我们需要在使用MV_CC_DisplayOneFrame()函数前先定义一个MV_DISPLAY_FRAME_INFO类型的结构体
typedef struct _MV_DISPLAY_FRAME_INFO_{        void*                   hWnd;         ///< [IN] \~chinese 窗口句柄                \~english HWND        unsigned char*          pData;        ///< [IN] \~chinese 显示的数据              \~english Data Buffer        unsigned int            nDataLen;     ///< [IN] \~chinese 数据长度                \~english Data Size        unsigned short          nWidth;       ///< [IN] \~chinese 图像宽                  \~english Width        unsigned short          nHeight;      ///< [IN] \~chinese 图像高                  \~english Height        enum MvGvspPixelType    enPixelType;  ///< [IN] \~chinese 像素格式                \~english Pixel format        unsigned int            enRenderMode; ///  [IN] \~chinese 图像渲染方式Windows:0-GDI(默认), 1-D3D, 2-OPENGL Linux: 0-OPENGL(默认)         unsigned int            nRes[3];      ///<      \~chinese 保留                    \~english Reserved}MV_DISPLAY_FRAME_INFO;        根据上述结构体的定义,我们先行配置我们的stDisplayInfo参数
MV_DISPLAY_FRAME_INFO stDisplayInfo = {0};stDisplayInfo.hWnd = displayHandle;stDisplayInfo.pData = pData;stDisplayInfo.nDataLen = stImageInfo.nFrameLen;stDisplayInfo.nWidth = stImageInfo.nWidth;stDisplayInfo.nHeight = stImageInfo.nHeight;stDisplayInfo.enPixelType = stImageInfo.enPixelType;        在定义完这个结构体之后我们便可以使用显示函数API啦,具体的使用代码如下:
// 显示一帧图像bool DisplayOneFrame() {    if (handle == nullptr || pData == nullptr || displayHandle == nullptr) {        return false;    }    // 准备显示信息    MV_DISPLAY_FRAME_INFO stDisplayInfo = {0};    stDisplayInfo.hWnd = displayHandle;    stDisplayInfo.pData = pData;    stDisplayInfo.nDataLen = stImageInfo.nFrameLen;    stDisplayInfo.nWidth = stImageInfo.nWidth;    stDisplayInfo.nHeight = stImageInfo.nHeight;    stDisplayInfo.enPixelType = stImageInfo.enPixelType;    // 显示图像    int nRet = MV_CC_DisplayOneFrame(handle, &stDisplayInfo);    if (MV_OK != nRet) {        std::cout << "显示图像失败! nRet [" << nRet << "]" << std::endl;        return false;    }    return true;}        接着我们将原来的GetOneFrame()函数中间添加图像显示函数DisplayOneFrame()即可
// 获取一帧图像并显示bool GetOneFrameAndShow() {    if (handle == nullptr || pData == nullptr) {        return false;    }    int nRet = MV_CC_GetOneFrameTimeout(handle, pData, nDataSize, &stImageInfo, 1000);    if (MV_OK != nRet) {        std::cout << "获取一帧图像失败! nRet [" << nRet << "]" << std::endl;        return false;    }    std::cout << "获取一帧图像成功: Width[" << stImageInfo.nWidth               << "] Height[" << stImageInfo.nHeight               << "] FrameNum[" << stImageInfo.nFrameNum << "]" << std::endl;    /* --------------显示图像 -----------------------*/    if (displayHandle != nullptr) {        return DisplayOneFrame();    }    /* --------------显示图像 -----------------------*/    return true;}        之后便开始修改我们的主函数啦首先在主函数中创建X11窗口并且捕获屏幕信息:
// 创建X11窗口Display* display = XOpenDisplay(NULL);if (!display) {    std::cout << "无法连接到X服务器!" << std::endl;    return -1;}// 获取屏幕信息int screen = DefaultScreen(display);Window root = DefaultRootWindow(display);        接着我们设置窗口的基本参数,并设置窗口标题和窗口关闭事件
// 创建窗口Window window = XCreateSimpleWindow(    display,        // Display    root,          // 父窗口    0, 0,          // 位置    1440, 1080,    // 大小(使用相机分辨率)    1,             // 边框宽度    BlackPixel(display, screen),  // 边框颜色    WhitePixel(display, screen)   // 背景颜色);// 设置窗口标题XStoreName(display, window, "Camera Display");// 设置窗口接收关闭事件Atom wmDeleteMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);XSetWMProtocols(display, window, &wmDeleteMessage, 1);最后我们显示窗口并持续捕获图像并使用X11事件显示即可
// 显示窗口XMapWindow(display, window);XFlush(display);// 设置显示窗口camera.SetDisplayWindow((void*)window);// 开始取图if (!camera.StartGrabbing()) {    std::cout << "开始取图失败!" << std::endl;    XCloseDisplay(display);    return -1;}std::cout << "开始取图成功!" << std::endl;// 持续获取并显示图像bool running = true;while (running) {    if (!camera.GetOneFrameAndShow()) {        break;    }    // 处理X11事件    while (XPending(display)) {        XEvent event;        XNextEvent(display, &event);        // 处理窗口关闭事件        if (event.type == ClientMessage) {            if (event.xclient.data.l[0] == wmDeleteMessage) {                running = false;            }        }    }}        接着我们修改一下我们的Cmake即可:
# 查找X11包find_package(X11 REQUIRED)include_directories(    ${PROJECT_SOURCE_DIR}/include    ${MVCAM_INCLUDE_PATH}    ${OpenCV_INCLUDE_DIRS})# 链接海康相机库和X11库target_link_libraries(${PROJECT_NAME}    MvCameraControl  # 海康相机主库    pthread         # 线程库    ${X11_LIBRARIES}  # X11库)        最后我们X11串口+海康的显示API的完整的代码及效果如下:


#include <iostream>#include <string>#include <cstring>  // for memset#include <X11/Xlib.h>#include "MvCameraControl.h"#include "CameraParams.h"  // for MVCC_INTVALUE#include "PixelType.h"     // for PixelType_Gvsp_BGR8_Packedclass HKCamera {private:    void* handle = nullptr;    MVCC_INTVALUE stParam;    MV_FRAME_OUT_INFO_EX stImageInfo = {0};    unsigned char* pData = nullptr;    unsigned int nDataSize = 0;    void* displayHandle = nullptr;  // 显示窗口句柄public:    HKCamera() {        // 在构造函数中正确初始化结构体        memset(&stParam, 0, sizeof(MVCC_INTVALUE));        memset(&stImageInfo, 0, sizeof(MV_FRAME_OUT_INFO_EX));    }    bool InitCamera() {        MV_CC_DEVICE_INFO_LIST stDeviceList;        memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));        // 枚举设备        int nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);        if (MV_OK != nRet) {            std::cout << "枚举设备失败! nRet [" << nRet << "]" << std::endl;            return false;        }        if (stDeviceList.nDeviceNum > 0) {            // 创建句柄            nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[0]);            if (MV_OK != nRet) {                std::cout << "创建句柄失败! nRet [" << nRet << "]" << std::endl;                return false;            }            // 打开设备            nRet = MV_CC_OpenDevice(handle);            if (MV_OK != nRet) {                std::cout << "打开设备失败! nRet [" << nRet << "]" << std::endl;                return false;            }        } else {            std::cout << "未找到设备!" << std::endl;            return false;        }        return true;    }    // 设置相机参数    bool SetParameters() {        int nRet;        // 设置触发模式为off        nRet = MV_CC_SetEnumValue(handle, "TriggerMode", 0);        if (MV_OK != nRet) {            std::cout << "设置触发模式失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 设置像素格式为BGR8        nRet = MV_CC_SetEnumValue(handle, "PixelFormat", PixelType_Gvsp_BGR8_Packed);        if (MV_OK != nRet) {            std::cout << "设置像素格式失败! nRet [" << nRet << "]" << std::endl;            return false;        }        std::cout << "设置像素格式为BGR8_Packed" << std::endl;        // 获取和设置曝光时间        MVCC_FLOATVALUE stExposureTime = {0};        nRet = MV_CC_GetFloatValue(handle, "ExposureTime", &stExposureTime);        if (MV_OK == nRet) {            float exposureTime = 10000.0f;  // 默认10ms            if (exposureTime < stExposureTime.fMin) exposureTime = stExposureTime.fMin;            if (exposureTime > stExposureTime.fMax) exposureTime = stExposureTime.fMax;                        std::cout << "曝光时间范围: [" << stExposureTime.fMin << ", " << stExposureTime.fMax                       << "], 当前设置: " << exposureTime << std::endl;                        nRet = MV_CC_SetFloatValue(handle, "ExposureTime", exposureTime);            if (MV_OK != nRet) {                std::cout << "设置曝光时间失败! nRet [" << nRet << "]" << std::endl;                return false;            }        }        // 获取和设置增益        MVCC_FLOATVALUE stGain = {0};        nRet = MV_CC_GetFloatValue(handle, "Gain", &stGain);        if (MV_OK == nRet) {            std::cout << "增益范围: [" << stGain.fMin << ", " << stGain.fMax                       << "], 当前值: " << stGain.fCurValue << std::endl;                        if (stGain.fCurValue < stGain.fMin || stGain.fCurValue > stGain.fMax) {                float gain = stGain.fMin;  // 使用最小值                nRet = MV_CC_SetFloatValue(handle, "Gain", gain);                if (MV_OK != nRet) {                    std::cout << "设置增益失败! nRet [" << nRet << "]" << std::endl;                    return false;                }            }        }        // 获取和设置帧率        MVCC_FLOATVALUE stFrameRate = {0};        nRet = MV_CC_GetFloatValue(handle, "AcquisitionFrameRate", &stFrameRate);        if (MV_OK == nRet) {            float frameRate = 30.0f;  // 默认30fps            if (frameRate < stFrameRate.fMin) frameRate = stFrameRate.fMin;            if (frameRate > stFrameRate.fMax) frameRate = stFrameRate.fMax;                        std::cout << "帧率范围: [" << stFrameRate.fMin << ", " << stFrameRate.fMax                       << "], 当前设置: " << frameRate << std::endl;                        nRet = MV_CC_SetFloatValue(handle, "AcquisitionFrameRate", frameRate);            if (MV_OK != nRet) {                std::cout << "设置帧率失败! nRet [" << nRet << "]" << std::endl;                // 这个错误不影响主要功能,可以继续            }        }        return true;    }    // 开始取图    bool StartGrabbing() {        // 开始取流        int nRet = MV_CC_StartGrabbing(handle);        if (MV_OK != nRet) {            std::cout << "开始取流失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 获取数据包大小        MVCC_INTVALUE stParam;        nRet = MV_CC_GetIntValue(handle, "PayloadSize", &stParam);        if (MV_OK != nRet) {            std::cout << "获取数据包大小失败! nRet [" << nRet << "]" << std::endl;            return false;        }        // 分配资源        nDataSize = stParam.nCurValue;        pData = (unsigned char*)malloc(nDataSize);        if (pData == nullptr) {            std::cout << "内存分配失败!" << std::endl;            return false;        }        return true;    }    // 设置显示窗口    bool SetDisplayWindow(void* windowHandle) {        displayHandle = windowHandle;        return true;    }    // 显示一帧图像    bool DisplayOneFrame() {        if (handle == nullptr || pData == nullptr || displayHandle == nullptr) {            return false;        }        // 准备显示信息        MV_DISPLAY_FRAME_INFO stDisplayInfo = {0};        stDisplayInfo.hWnd = displayHandle;        stDisplayInfo.pData = pData;        stDisplayInfo.nDataLen = stImageInfo.nFrameLen;        stDisplayInfo.nWidth = stImageInfo.nWidth;        stDisplayInfo.nHeight = stImageInfo.nHeight;        stDisplayInfo.enPixelType = stImageInfo.enPixelType;        // 显示图像        int nRet = MV_CC_DisplayOneFrame(handle, &stDisplayInfo);        if (MV_OK != nRet) {            std::cout << "显示图像失败! nRet [" << nRet << "]" << std::endl;            return false;        }        return true;    }    // 获取一帧图像并显示    bool GetOneFrameAndShow() {        if (handle == nullptr || pData == nullptr) {            return false;        }        int nRet = MV_CC_GetOneFrameTimeout(handle, pData, nDataSize, &stImageInfo, 1000);        if (MV_OK != nRet) {            std::cout << "获取一帧图像失败! nRet [" << nRet << "]" << std::endl;            return false;        }        std::cout << "获取一帧图像成功: Width[" << stImageInfo.nWidth                   << "] Height[" << stImageInfo.nHeight                   << "] FrameNum[" << stImageInfo.nFrameNum << "]" << std::endl;        // 显示图像        if (displayHandle != nullptr) {            return DisplayOneFrame();        }        return true;    }    // 停止采集    void StopGrabbing() {        if (handle != nullptr) {            MV_CC_StopGrabbing(handle);            MV_CC_CloseDevice(handle);            MV_CC_DestroyHandle(handle);            handle = nullptr;        }        if (pData != nullptr) {            free(pData);            pData = nullptr;        }    }    ~HKCamera() {        StopGrabbing();    }};int main() {    HKCamera camera;    // 初始化相机    if (!camera.InitCamera()) {        std::cout << "相机初始化失败!" << std::endl;        return -1;    }    std::cout << "相机初始化成功!" << std::endl;    // 设置参数    if (!camera.SetParameters()) {        std::cout << "设置相机参数失败!" << std::endl;        return -1;    }    std::cout << "设置相机参数成功!" << std::endl;    // 创建X11窗口    Display* display = XOpenDisplay(NULL);    if (!display) {        std::cout << "无法连接到X服务器!" << std::endl;        return -1;    }    // 获取屏幕信息    int screen = DefaultScreen(display);    Window root = DefaultRootWindow(display);    // 创建窗口    Window window = XCreateSimpleWindow(        display,        // Display        root,          // 父窗口        0, 0,          // 位置        1440, 1080,    // 大小(使用相机分辨率)        1,             // 边框宽度        BlackPixel(display, screen),  // 边框颜色        WhitePixel(display, screen)   // 背景颜色    );    // 设置窗口标题    XStoreName(display, window, "Camera Display");    // 设置窗口接收关闭事件    Atom wmDeleteMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);    XSetWMProtocols(display, window, &wmDeleteMessage, 1);    // 显示窗口    XMapWindow(display, window);    XFlush(display);    // 设置显示窗口    camera.SetDisplayWindow((void*)window);    // 开始取图    if (!camera.StartGrabbing()) {        std::cout << "开始取图失败!" << std::endl;        XCloseDisplay(display);        return -1;    }    std::cout << "开始取图成功!" << std::endl;    // 持续获取并显示图像    bool running = true;    while (running) {        if (!camera.GetOneFrameAndShow()) {            break;        }        // 处理X11事件        while (XPending(display)) {            XEvent event;            XNextEvent(display, &event);                        // 处理窗口关闭事件            if (event.type == ClientMessage) {                if (event.xclient.data.l[0] == wmDeleteMessage) {                    running = false;                }            }        }    }    // 停止采集    camera.StopGrabbing();    std::cout << "停止采集完成!" << std::endl;    // 关闭X11连接    XCloseDisplay(display);    return 0;}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

319

主题

0

回帖

967

积分

高级会员

积分
967

QQ|智能设备 | 粤ICP备2024353841号-1

GMT+8, 2025-3-10 15:24 , Processed in 1.099850 second(s), 30 queries .

Powered by 智能设备

©2025

|网站地图