1. 引言:为什么需要封装海康 SDK?

在工业视觉领域,海康威视(Hikvision)的工业相机因其高稳定性、高帧率和丰富的接口支持,被广泛应用于自动化检测、智能安防、机器人引导等场景。然而,官方提供的 MVSDK 虽然功能强大,却存在几个明显痛点:

  • 接口以 C 风格为主,需手动管理设备句柄、内存和状态;
  • 错误码繁多,异常处理复杂;
  • 像素格式多样(Bayer、Mono、RGB),转换逻辑分散;
  • 缺乏现代 C++ 的 RAII 和回调抽象,代码冗长且易出错。

为提升开发效率与系统健壮性,我决定对海康 SDK 进行二次封装,目标是:一行代码连接相机,一个回调获取图像,无缝对接 OpenCV 与深度学习模型(如 YOLO)

2. 封装设计思路

2.1 核心目标

本次封装围绕以下原则展开:

  • 简洁性:用户无需了解底层 SDK 细节,只需提供 IP 或序列号即可启动采集。
  • 健壮性:支持断线自动重连,避免因网络波动导致程序崩溃。
  • 通用性:自动识别相机输出的像素格式(如 BayerRG8、Mono10),并统一转换为 cv::Mat
  • 高性能:预分配图像缓冲区,避免频繁内存申请;支持 8/10/12-bit 数据高效转 8-bit。
  • 可扩展性:预留参数控制接口(曝光、增益、帧率等),便于后续集成触发模式或同步采集。

2.2 类结构概览

封装的核心是一个 HikCamera 类,其关键接口如下:

#pragma once

#include <string>
#include <functional>
#include <stdexcept>
#include <vector>
#include <opencv2/opencv.hpp>
#include "MvCameraControl.h"


struct ImageFrame {
    std::vector<uint8_t> data;
    unsigned int         width = 0;
    unsigned int         height = 0;
    unsigned int         frameNum = 0;
    MvGvspPixelType      enPixelType = PixelType_Gvsp_Undefined;
};

class HikCamera {
public:
    using ImageCallback = std::function<void(const ImageFrame& frame)>;

    explicit HikCamera(const std::string& identifier,
                       bool useIP = true,
                       int reconnectSec = 10,
                       MvGvspPixelType initialPixelType = PixelType_Gvsp_Undefined);

    ~HikCamera();

    HikCamera(const HikCamera&) = delete;
    HikCamera& operator=(const HikCamera&) = delete;
    HikCamera(HikCamera&&) = delete;
    HikCamera& operator=(HikCamera&&) = delete;
    cv::Mat ConvertToColor(const ImageFrame& frame);

    void StartGrabbing();
    void StopGrabbing();
    bool IsGrabbing() const;

    void SetImageCallback(ImageCallback callback);

    void SetTriggerModeOn(bool useSoftwareTrigger = true);  // true: 软触发;false: 硬触发
    void SendSoftwareTrigger();                             // 仅在软触发模式下有效

    // 参数设置(自动关闭自动模式)
    void SetExposureTime(float us);
    float GetExposureTime() const;

    void SetGain(float gain);
    float GetGain() const;

    void SetFrameRate(float fps);
    void EnableFrameRateControl(bool enable);

    void SetTriggerModeOff();

    void LoadParameters(const std::string& filePath);
    void SaveParameters(const std::string& filePath);

    MvGvspPixelType GetPixelFormat() const;
    void SetPixelFormat(MvGvspPixelType pixelType);
    std::string GetPixelFormatName(MvGvspPixelType pixelType) const;

    bool IsConnected() const;
    std::string GetSerialNumber() const;
    std::string GetModelName() const;

    void PrintDeviceInfo() const;  // 新增:打印相机信息(IP、SN、宽度、高度)

private:
    void*               handle_ = nullptr;
    std::string         serialNumber_;
    std::string         identifier_;
    bool                useIP_;
    int                 reconnectTimeoutSec_;
    ImageCallback       userCallback_;
    bool                grabbing_ = false;
    ImageFrame preAllocatedFrame;  // 预分配的帧数据

    void OpenDeviceByIdentifier();
    void SetupOptimalPacketSize();
    void RegisterCallbacks();

    static void __stdcall ImageCallbackWrapper(MV_FRAME_OUT* pFrame, void* pUser, bool bAutoFree);
    static void __stdcall ExceptionCallbackWrapper(unsigned int nMsgType, void* pUser);

    void Reconnect();

    // 辅助:关闭自动曝光/增益
    void DisableAutoExposure();
    void DisableAutoGain();
};

具体实现源码细节如下:

// HikCamera.cpp
#include "HikCamera.hpp"
#include <opencv2/opencv.hpp>
#include <iostream>
#include <chrono>
#include <thread>
#include <cstring>
#include <map>

void CheckRet(int nRet, const std::string& msg) {
    if (nRet != MV_OK) {
        throw std::runtime_error(msg + ": 0x" + std::to_string(static_cast<unsigned int>(nRet)));
    }
}

// ==================== HikCamera ====================
HikCamera::HikCamera(const std::string& identifier, bool useIP, int reconnectSec, MvGvspPixelType initialPixelType)
    : identifier_(identifier), useIP_(useIP), reconnectTimeoutSec_(reconnectSec) {
    CheckRet(MV_CC_Initialize(), "MV_CC_Initialize failed");

    OpenDeviceByIdentifier();
    SetupOptimalPacketSize();
    SetTriggerModeOff();

    // 关闭自动模式
    DisableAutoExposure();
    DisableAutoGain();

    if (initialPixelType != PixelType_Gvsp_Undefined) {
        SetPixelFormat(initialPixelType);
    }

    RegisterCallbacks();

    // 预分配缓冲区:根据当前分辨率估算最大缓冲(支持 16-bit 和 RGB)
    MVCC_INTVALUE widthVal = {0}, heightVal = {0};
    MV_CC_GetIntValue(handle_, "Width", &widthVal);
    MV_CC_GetIntValue(handle_, "Height", &heightVal);

    size_t estimatedBufferSize = static_cast<size_t>(widthVal.nCurValue) *
                                 static_cast<size_t>(heightVal.nCurValue) * 4;  // 最大 4 字节/像素
    preAllocatedFrame.data.reserve(estimatedBufferSize);

    PrintDeviceInfo();
}

HikCamera::~HikCamera() {
    StopGrabbing();
    if (handle_) {
        MV_CC_CloseDevice(handle_);
        MV_CC_DestroyHandle(handle_);
        handle_ = nullptr;
    }
    MV_CC_Finalize();
}

void HikCamera::OpenDeviceByIdentifier() {
    MV_CC_DEVICE_INFO_LIST stDeviceList = {0};
    CheckRet(MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList), "EnumDevices failed");

    unsigned int targetIndex = static_cast<unsigned int>(-1);
    for (unsigned int i = 0; i < stDeviceList.nDeviceNum; ++i) {
        auto* pInfo = stDeviceList.pDeviceInfo[i];
        if (useIP_ && pInfo->nTLayerType == MV_GIGE_DEVICE) {
            char ipStr[16];
            sprintf(ipStr, "%d.%d.%d.%d",
                    (pInfo->SpecialInfo.stGigEInfo.nCurrentIp >> 24) & 0xFF,
                    (pInfo->SpecialInfo.stGigEInfo.nCurrentIp >> 16) & 0xFF,
                    (pInfo->SpecialInfo.stGigEInfo.nCurrentIp >> 8) & 0xFF,
                    pInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0xFF);
            if (identifier_ == ipStr) {
                targetIndex = i;
                serialNumber_ = reinterpret_cast<char*>(pInfo->SpecialInfo.stGigEInfo.chSerialNumber);
                break;
            }
        } else if (!useIP_) {
            const char* sn = nullptr;
            if (pInfo->nTLayerType == MV_GIGE_DEVICE) sn = reinterpret_cast<char*>(pInfo->SpecialInfo.stGigEInfo.chSerialNumber);
            else if (pInfo->nTLayerType == MV_USB_DEVICE) sn = reinterpret_cast<char*>(pInfo->SpecialInfo.stUsb3VInfo.chSerialNumber);
            if (sn && identifier_ == sn) {
                targetIndex = i;
                serialNumber_ = identifier_;
                break;
            }
        }
    }

    if (targetIndex == static_cast<unsigned int>(-1)) {
        throw std::runtime_error("Camera not found: " + identifier_);
    }

    CheckRet(MV_CC_CreateHandle(&handle_, stDeviceList.pDeviceInfo[targetIndex]), "CreateHandle failed");
    CheckRet(MV_CC_OpenDevice(handle_), "OpenDevice failed");
}

void HikCamera::SetupOptimalPacketSize() {
    if (!handle_) return;
    int nPacketSize = MV_CC_GetOptimalPacketSize(handle_);
    if (nPacketSize > 0) {
        MV_CC_SetIntValueEx(handle_, "GevSCPSPacketSize", nPacketSize);
    }
}

void HikCamera::DisableAutoExposure() {
    MV_CC_SetEnumValue(handle_, "ExposureAuto", 0);  // 0 = Off
}

void HikCamera::DisableAutoGain() {
    MV_CC_SetEnumValue(handle_, "GainAuto", 0);      // 0 = Off
}

void HikCamera::SetTriggerModeOff() {
    MV_CC_SetEnumValue(handle_, "TriggerMode", MV_TRIGGER_MODE_OFF);
}

void HikCamera::RegisterCallbacks() {
    MV_CC_RegisterImageCallBackEx2(handle_, ImageCallbackWrapper, this, true);
    MV_CC_RegisterExceptionCallBack(handle_, ExceptionCallbackWrapper, this);
}

void HikCamera::StartGrabbing() {
    if (grabbing_) return;
    CheckRet(MV_CC_StartGrabbing(handle_), "StartGrabbing failed");
    grabbing_ = true;
}

void HikCamera::StopGrabbing() {
    if (!grabbing_) return;
    MV_CC_StopGrabbing(handle_);
    grabbing_ = false;
}

bool HikCamera::IsGrabbing() const { return grabbing_; }

void HikCamera::SetImageCallback(ImageCallback callback) {
    userCallback_ = std::move(callback);
}

void __stdcall HikCamera::ImageCallbackWrapper(MV_FRAME_OUT* pFrame, void* pUser, bool bAutoFree) {
    auto* self = static_cast<HikCamera*>(pUser);
    if (!self || !self->userCallback_ || !pFrame) return;

    ImageFrame& frame = self->preAllocatedFrame;
    frame.width    = pFrame->stFrameInfo.nExtendWidth;
    frame.height   = pFrame->stFrameInfo.nExtendHeight;
    frame.frameNum = pFrame->stFrameInfo.nFrameNum;
    frame.enPixelType = pFrame->stFrameInfo.enPixelType;

    size_t frameLen = pFrame->stFrameInfo.nFrameLenEx;
    if (frame.data.size() < frameLen) {
        frame.data.resize(frameLen);
    }

    memcpy(frame.data.data(), pFrame->pBufAddr, frameLen);

    self->userCallback_(frame);

    if (!bAutoFree) {
        MV_CC_FreeImageBuffer(self->handle_, pFrame);
    }
}

void __stdcall HikCamera::ExceptionCallbackWrapper(unsigned int nMsgType, void* pUser) {
    if (nMsgType == MV_EXCEPTION_DEV_DISCONNECT) {
        auto* self = static_cast<HikCamera*>(pUser);
        if (self) {
            std::cout << "[HikCamera] Device disconnected! Attempting reconnect..." << std::endl;
            self->Reconnect();
        }
    }
}

// 关键:Reconnect 实现(之前缺失导致链接错误)
void HikCamera::Reconnect() {
    StopGrabbing();

    if (handle_) {
        MV_CC_CloseDevice(handle_);
        MV_CC_DestroyHandle(handle_);
        handle_ = nullptr;
    }

    auto startTime = std::chrono::steady_clock::now();

    while (true) {
        try {
            OpenDeviceByIdentifier();
            SetupOptimalPacketSize();
            SetTriggerModeOff();
            DisableAutoExposure();
            DisableAutoGain();
            RegisterCallbacks();

            if (grabbing_) {
                StartGrabbing();
            }

            std::cout << "[HikCamera] Reconnect successful!" << std::endl;
            return;
        } catch (const std::exception& e) {
            std::cerr << "[HikCamera] Reconnect failed: " << e.what() << ". Retrying in 1 second..." << std::endl;
        }

        std::this_thread::sleep_for(std::chrono::seconds(1));

        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
            std::chrono::steady_clock::now() - startTime).count();

        if (elapsed > reconnectTimeoutSec_) {
            throw std::runtime_error("Reconnect timeout after " + std::to_string(reconnectTimeoutSec_) + " seconds");
        }
    }
}

void HikCamera::SetExposureTime(float us) {
    CheckRet(MV_CC_SetFloatValue(handle_, "ExposureTime", us), "SetExposureTime failed");
}

float HikCamera::GetExposureTime() const {
    MVCC_FLOATVALUE val = {0};
    CheckRet(MV_CC_GetFloatValue(handle_, "ExposureTime", &val), "GetExposureTime failed");
    return val.fCurValue;
}

void HikCamera::SetGain(float gain) {
    CheckRet(MV_CC_SetFloatValue(handle_, "Gain", gain), "SetGain failed");
}

float HikCamera::GetGain() const {
    MVCC_FLOATVALUE val = {0};
    CheckRet(MV_CC_GetFloatValue(handle_, "Gain", &val), "GetGain failed");
    return val.fCurValue;
}

void HikCamera::EnableFrameRateControl(bool enable) {
    MV_CC_SetBoolValue(handle_, "AcquisitionFrameRateEnable", enable);
}

void HikCamera::SetFrameRate(float fps) {
    CheckRet(MV_CC_SetFloatValue(handle_, "AcquisitionFrameRate", fps), "SetFrameRate failed");
}

void HikCamera::LoadParameters(const std::string& filePath) {
    CheckRet(MV_CC_FeatureLoad(handle_, filePath.c_str()), "LoadParameters failed");
}

void HikCamera::SaveParameters(const std::string& filePath) {
    CheckRet(MV_CC_FeatureSave(handle_, filePath.c_str()), "SaveParameters failed");
}

MvGvspPixelType HikCamera::GetPixelFormat() const {
    MVCC_ENUMVALUE val = {0};
    CheckRet(MV_CC_GetEnumValue(handle_, "PixelFormat", &val), "GetPixelFormat failed");
    return static_cast<MvGvspPixelType>(val.nCurValue);
}

void HikCamera::SetPixelFormat(MvGvspPixelType pixelType) {
    CheckRet(MV_CC_SetEnumValue(handle_, "PixelFormat", static_cast<unsigned int>(pixelType)),
             "SetPixelFormat failed");
}

std::string HikCamera::GetPixelFormatName(MvGvspPixelType pixelType) const {
    // 你要求不修改这个函数,所以保持原样
    static const std::map<MvGvspPixelType, std::string> map = {
        {PixelType_Gvsp_Mono8, "Mono8"},
        {PixelType_Gvsp_Mono10, "Mono10"},
        {PixelType_Gvsp_Mono10_Packed, "Mono10_Packed"},
        {PixelType_Gvsp_Mono12, "Mono12"},
        {PixelType_Gvsp_Mono12_Packed, "Mono12_Packed"},
        {PixelType_Gvsp_Mono16, "Mono16"},

        // Bayer RGGB pattern
        {PixelType_Gvsp_BayerRG8, "BayerRG8"},
        {PixelType_Gvsp_BayerRG10, "BayerRG10"},
        {PixelType_Gvsp_BayerRG10_Packed, "BayerRG10_Packed"},
        {PixelType_Gvsp_BayerRG12, "BayerRG12"},
        {PixelType_Gvsp_BayerRG12_Packed, "BayerRG12_Packed"},

        // Bayer BGGR
        {PixelType_Gvsp_BayerBG8, "BayerBG8"},
        {PixelType_Gvsp_BayerBG10, "BayerBG10"},
        {PixelType_Gvsp_BayerBG10_Packed, "BayerBG10_Packed"},
        {PixelType_Gvsp_BayerBG12, "BayerBG12"},
        {PixelType_Gvsp_BayerBG12_Packed, "BayerBG12_Packed"},

        // Bayer GRBG
        {PixelType_Gvsp_BayerGR8, "BayerGR8"},
        {PixelType_Gvsp_BayerGR10, "BayerGR10"},
        {PixelType_Gvsp_BayerGR12, "BayerGR12"},

        // Bayer GBRG
        {PixelType_Gvsp_BayerGB8, "BayerGB8"},
        {PixelType_Gvsp_BayerGB10, "BayerGB10"},
        {PixelType_Gvsp_BayerGB12, "BayerGB12"},

        // RGB/BGR
        {PixelType_Gvsp_RGB8_Packed, "RGB8_Packed"},
        {PixelType_Gvsp_BGR8_Packed, "BGR8_Packed"},
        {PixelType_Gvsp_YUV422_Packed, "YUV422_Packed"},
        {PixelType_Gvsp_YUV422_YUYV_Packed, "YUV422_YUYV_Packed"},

        // Others
        {PixelType_Gvsp_Undefined, "Undefined"}
    };

    auto it = map.find(pixelType);
    if (it != map.end()) {
        return it->second;
    }
    return "Unknown(0x" + std::to_string(static_cast<unsigned int>(pixelType)) + ")";
}

cv::Mat HikCamera::ConvertToColor(const ImageFrame& frame) {
    // Step 1: 检查相机像素格式
    int cvDepth = CV_8UC1;          // 默认 8-bit 单通道
    bool is16Bit = false;

    if (frame.enPixelType == PixelType_Gvsp_BayerRG10  || frame.enPixelType == PixelType_Gvsp_BayerRG12  ||
        frame.enPixelType == PixelType_Gvsp_BayerBG10  || frame.enPixelType == PixelType_Gvsp_BayerBG12  ||
        frame.enPixelType == PixelType_Gvsp_BayerGR10  || frame.enPixelType == PixelType_Gvsp_BayerGR12  ||
        frame.enPixelType == PixelType_Gvsp_BayerGB10  || frame.enPixelType == PixelType_Gvsp_BayerGB12  ||
        frame.enPixelType == PixelType_Gvsp_Mono10     || frame.enPixelType == PixelType_Gvsp_Mono12) {
        cvDepth = CV_16UC1;
        is16Bit = true;
    }

    // Step 2: 构造原始 raw 图像(单通道 Bayer 或 Mono)
    cv::Mat raw(frame.height, frame.width, cvDepth, const_cast<unsigned char*>(frame.data.data()));

    // Step 3: 根据像素格式选择处理方式
    cv::Mat colorImage;
    int cvtCode = -1;  // Debayer 转换码

    switch (frame.enPixelType) {
        // ==================== Bayer 彩色格式 ====================
        case PixelType_Gvsp_BayerRG8:
        case PixelType_Gvsp_BayerRG10:
        case PixelType_Gvsp_BayerRG12:
            cvtCode = cv::COLOR_BayerRG2RGB;
            break;
        case PixelType_Gvsp_BayerBG8:
        case PixelType_Gvsp_BayerBG10:
        case PixelType_Gvsp_BayerBG12:
            cvtCode = cv::COLOR_BayerBG2RGB;
            break;
        case PixelType_Gvsp_BayerGR8:
        case PixelType_Gvsp_BayerGR10:
        case PixelType_Gvsp_BayerGR12:
            cvtCode = cv::COLOR_BayerGR2RGB;
            break;
        case PixelType_Gvsp_BayerGB8:
        case PixelType_Gvsp_BayerGB10:
        case PixelType_Gvsp_BayerGB12:
            cvtCode = cv::COLOR_BayerGB2RGB;
            break;

        // ==================== 相机已输出 3 通道 ====================
        case PixelType_Gvsp_RGB8_Packed:
            colorImage = cv::Mat(frame.height, frame.width, CV_8UC3, const_cast<unsigned char*>(frame.data.data()));
            break;
        case PixelType_Gvsp_BGR8_Packed:
            colorImage = cv::Mat(frame.height, frame.width, CV_8UC3, const_cast<unsigned char*>(frame.data.data()));
            break;

        // ==================== 灰度相机 ====================
        case PixelType_Gvsp_Mono8:
        case PixelType_Gvsp_Mono10:
        case PixelType_Gvsp_Mono12:
        case PixelType_Gvsp_Mono16:
            if (is16Bit) {
                raw.convertTo(colorImage, CV_8U, 1.0 / 256.0);  // 16→8 bit 简单右移
            } else {
                colorImage = raw;
            }
            break;

        // ==================== 不支持的格式 ====================
        default:
            std::cerr << "警告: 不支持的像素格式 0x" << std::hex << static_cast<unsigned int>(frame.enPixelType)
                      << ",跳过此帧处理" << std::dec << std::endl;
            return cv::Mat();  // 返回空 Mat,避免后续保存崩溃
    }

    // Step 4: 执行 Debayer 转换(仅 Bayer 格式需要)
    if (cvtCode != -1) {
        cv::cvtColor(raw, colorImage, cvtCode);

        // 如果是 16-bit Bayer,转为 8-bit 输出(YOLO 需要 8-bit)
        if (is16Bit) {
            colorImage.convertTo(colorImage, CV_8U, 1.0 / 256.0);
        }
    }

    return colorImage;
}

bool HikCamera::IsConnected() const { return handle_ != nullptr; }

std::string HikCamera::GetSerialNumber() const { return serialNumber_; }

std::string HikCamera::GetModelName() const {
    MVCC_STRINGVALUE val = {0};
    CheckRet(MV_CC_GetStringValue(handle_, "DeviceModelName", &val), "GetModelName failed");
    return std::string(val.chCurValue);
}

void HikCamera::SetTriggerModeOn(bool useSoftwareTrigger) {
    if (grabbing_) {
        StopGrabbing();
    }

    CheckRet(MV_CC_SetEnumValue(handle_, "TriggerMode", MV_TRIGGER_MODE_ON), "Set TriggerMode ON failed");

    if (useSoftwareTrigger) {
        CheckRet(MV_CC_SetEnumValue(handle_, "TriggerSource", MV_TRIGGER_SOURCE_SOFTWARE),
                 "Set TriggerSource to Software failed");
    }

    StartGrabbing();
}

void HikCamera::SendSoftwareTrigger() {
    CheckRet(MV_CC_SetCommandValue(handle_, "TriggerSoftware"), "SendSoftwareTrigger failed");
}

// 打印相机信息
void HikCamera::PrintDeviceInfo() const {
    std::cout << "=== 相机信息 ===" << std::endl;
    std::cout << "序列号 (Serial Number): " << GetSerialNumber() << std::endl;
    std::cout << "型号 (Model Name): " << GetModelName() << std::endl;

    MVCC_STRINGVALUE ipVal = {0};
    if (MV_CC_GetStringValue(handle_, "DeviceIPAddress", &ipVal) == MV_OK) {
        std::cout << "当前 IP 地址: " << ipVal.chCurValue << std::endl;
    } else {
        std::cout << "当前 IP 地址: N/A (非 GigE 相机)" << std::endl;
    }

    MVCC_STRINGVALUE userNameVal = {0};
    if (MV_CC_GetStringValue(handle_, "DeviceUserID", &userNameVal) == MV_OK) {
        std::cout << "用户自定义名字 (UserDefinedName): " << userNameVal.chCurValue << std::endl;
    } else {
        std::cout << "用户自定义名字 (UserDefinedName): N/A" << std::endl;
    }

    MVCC_INTVALUE widthVal = {0}, heightVal = {0};
    MV_CC_GetIntValue(handle_, "Width", &widthVal);
    MV_CC_GetIntValue(handle_, "Height", &heightVal);
    std::cout << "图像宽度 (Width): " << widthVal.nCurValue << std::endl;
    std::cout << "图像高度 (Height): " << heightVal.nCurValue << std::endl;
    std::cout << "=================" << std::endl;
}

3. 关键技术点详解

3.1 设备发现与连接(IP vs SN)

HikCamera 构造函数中,首先调用 MV_CC_EnumDevices 枚举所有在线设备。根据 useIP 参数,分别匹配设备的 chIpAddrchSerialNumber 字段:

// 按 IP 连接
if (useIP && std::string(pDeviceInfo[i].SpecialInfo.stGigEInfo.chIpAddr) == identifier)

// 按 SN 连接
else if (!useIP && std::string(pDeviceInfo[i].SpecialInfo.stGigEInfo.chSerialNumber) == identifier)

这种方式极大提升了部署灵活性——调试时可用 IP 快速连接,量产时则用唯一 SN 防止设备混淆。

3.2 像素格式自动识别与转换

工业相机常输出 Bayer 格式(如 RG/BG/GR/GB)或 Mono 数据。我们在 ConvertToColor 中实现自动判断与转换:

if (frame.pixelFormat.find("Bayer") != std::string::npos) {
    int code = GetBayerCode(frame.pixelFormat); // 如 CV_BayerBG2BGR
    cv::cvtColor(rawMat, colorMat, code);
} else if (frame.pixelFormat.find("Mono") != std::string::npos) {
    if (depth > 8) rawMat.convertTo(colorMat, CV_8U, 1.0 / 256.0); // 10/12-bit → 8-bit
    else colorMat = rawMat.clone();
}

3.3 回调机制与内存管理

我们注册了海康的图像回调函数 MV_CC_RegisterImageCallBackEx2,并在内部使用预分配的 preAllocatedFrame.data 缓冲区接收图像数据。这样避免了每帧 malloc/free,显著降低延迟。

用户只需提供一个形如 void(const ImageFrame&) 的函数对象,例如:

camera.SetImageCallback([](const ImageFrame& f) {
    std::cout << "Got frame: " << f.width << "x" << f.height << std::endl;
});

3.4 自动重连机制

工业现场网络不稳定是常态。为此,我们注册了异常回调 ExceptionCallbackWrapper,当检测到 MV_EXCEPTION_DEV_DISCONNECT 时,启动后台重连线程:

while (!reconnected && retryCount < MAX_RETRY) {
    Close();  // 清理旧资源
    std::this_thread::sleep_for(2s);
    if (InitCamera()) {  // 重新初始化
        StartGrabbing();
        reconnected = true;
    }
    retryCount++;
}

3.5 相机参数动态控制

为满足不同光照条件下的成像需求,封装了常用参数接口:

hik_camera.SetExposureTime(5000);   // 5ms 曝光
hik_camera.SetGain(12.0);           // 12dB 增益
hik_camera.EnableFrameRateControl(true, 25.0); // 限帧率 25fps

4 结合YOLO实现实时推理

配合YOLO部署模型接口,在主文件夹调用接口实现高性能实时推理,嗲用接口如下:

#include <iostream>
#include <vector>
#include <filesystem>
#include <fstream>
#include <opencv2/opencv.hpp>

#include "TrtModel.hpp"       // 确保路径正确
#include "utils.hpp"          // 包含 DetectionInfo
#include "HikCamera.hpp"

namespace fs = std::filesystem;

std::string g_saveDir = "yolo_results/";
TrtModel yolo_model( "weights/yolo11s.onnx", true, true );
HikCamera hik_camera("169.254.131.105"); // 相机 IP
// HikCamera hik_camera("DA7743579", false);  // false 表示用 SN使用序列号连接

// 独立的图像处理函数(保存原始彩色图)
void processAndSaveImage(const ImageFrame& frame) {
    cv::Mat colorImage = hik_camera.ConvertToColor(frame);  // 调用封装函数获取彩色图像

    if (colorImage.empty()) {
        return;  // 转换失败,跳过
    }

    // YOLO 推理
    auto detections = yolo_model.doInference(colorImage);
    // 绘制检测框
    yolo_model.draw(colorImage, detections);

    // 生成带毫秒的时间戳文件名
    auto now = std::chrono::system_clock::now();
    auto t_c = std::chrono::system_clock::to_time_t(now);
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;

    std::stringstream ss;
    ss << std::put_time(std::localtime(&t_c), "%Y%m%d%H%M%S");
    ss << "_" << std::setfill('0') << std::setw(3) << ms.count();
    std::string filename = g_saveDir + ss.str() + ".jpg";

    if (cv::imwrite(filename, colorImage)) {
        std::cout << "图像保存:  " << filename << std::endl;
    } else {
        std::cerr << "保存失败: " << filename << std::endl;
    }
}


int main(){

    try{
        fs::create_directories(g_saveDir);

        // ====== 简单相机配置 ======
        hik_camera.SetExposureTime(50000.0f);     // 曝光时间50ms
        hik_camera.SetGain(10.0f);
        hik_camera.EnableFrameRateControl(true);
        // camera.SetFrameRate(30.0f);

        // 可选:强制相机输出 RGB(性能更高,带宽更高)
        // camera.SetPixelFormat(PixelType_Gvsp_RGB8_Packed);

        // 设置回调:只调用封装好的处理函数
        hik_camera.SetImageCallback(processAndSaveImage);

        hik_camera.StartGrabbing();

        std::cout << "\n=== 相机开始采集 ===\n";
        std::cout << "图像实时保存至: " << g_saveDir << std::endl;
        std::cout << "按回车键停止...\n";

        std::cin.get();

    } catch (const std::exception& e) {
        std::cerr << "异常: " << e.what() << std::endl;
        return -1;
    }

    std::cout << "程序退出。" << std::endl;
    return 0;
}

5. 使用说明与注意事项

5.1 编译依赖

5.2 常见问题排查

问题现象 可能原因 解决方案
相机无法连接 IP 不在同一网段 修改 PC 网卡 IP 为 169.254.x.x/16
图像全黑 曝光/增益过低 调用 SetExposureTime() 和 SetGain()
颜色异常(偏绿/紫) Bayer 格式识别错误 在 GetBayerCode() 中确认相机实际排列(RG/BG/GR/GB)
程序崩溃 未初始化 SDK 确保 MV_CC_Initialize() 成功返回

5.3 配置文件


cmake_minimum_required(VERSION 3.15)
project(DEPLOY LANGUAGES CXX CUDA)

# 设置源码和头文件路径
set(SRC_DIR src)
set(INCLUDE_DIR include)


# 配置标准
add_definitions(-std=c++17)
add_definitions(-DAPI_EXPORTS)
option(CUDA_USE_STATIC_CUDA_RUNTIME ON)
set(CMAKE_BUILD_TYPE Debug)

# CUDA 配置
# set(CMAKE_CUDA_COMPILER /usr/local/cuda-12.3/bin/nvcc)
enable_language(CUDA)

find_package(OpenCV REQUIRED)


# 包含目录配置
include_directories(
    ${INCLUDE_DIR}           # 项目头文件目录
    /usr/local/cuda-12.3/include  # CUDA
    /opt/TensorRT/TensorRT-10.8.0.43/include/  # TensorRT
    # /usr/local/include/opencv4  # OpenCV
    /opt/MVS/include/
)

# 链接目录配置
link_directories(
    /usr/local/cuda-12.3/lib64
    /opt/TensorRT/TensorRT-10.8.0.43/lib/
    /usr/local/lib/
    /opt/MVS/lib/64/
)

# glog & modbus
find_library(GLOG_LIB glog REQUIRED)
find_library(MODBUS_LIB modbus REQUIRED)


# 收集源文件
file(GLOB SRC_FILES 
    ${SRC_DIR}/*.cpp
    ${SRC_DIR}/*.cu
)

# 创建可执行文件
add_executable(build yolo_hksdk.cpp ${SRC_FILES})

# 设置 CUDA 架构
set_target_properties(build PROPERTIES 
    CUDA_ARCHITECTURES "61;70;75"
    CUDA_SEPARABLE_COMPILATION ON
)

# 链接库配置
# target_link_libraries(build ${OpenCV_LIBS})
target_link_libraries(build PRIVATE
    ${OpenCV_LIBS}
    
    # TensorRT
    nvinfer nvinfer_plugin nvonnxparser
    
    # CUDA
    cudart 

    # hksdk
    MvCameraControl

    pthread

    dl

)

# 附加编译选项(可选)
target_compile_options(build PRIVATE
    -Wall
    -Wno-deprecated-declarations
    -O2
)

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐