多相机协同拍照原理底层刨析

张开发
2026/5/30 15:29:04 15 分钟阅读
多相机协同拍照原理底层刨析
逻辑部分该章针对支持Gige协议的相机进行多相机控制分析例如海康康耐视等相机的高速拍照控制关于相机拍照的触发形式以及代码层参考Gige多相机高速拍照模式补充-CSDN博客该章主要讲述工作流方面环境概述多相机对应多网卡多网段工作目的周期性同时相对控制相机高速拍照结果需求多相机同时反馈相关节点图象要求不丢图以上工作均在PC端完成建议相关项目后面均用PLC进行电信号控制UDP进行传图下发指令电脑端-相机端关键点Gige 相机的相关控制指令为串行指令一次只能处理一个控制指令。PC 通过 UDP 向相机发送拍照指令相机必须处理完成并通过 UDP 回馈 ACK 给 PC 后才允许接收下一条控制指令。否则会报 “拒绝写入寄存器”新来的指令会被相机直接丢弃。例如28 台相机同时下发拍照指令每台相机内部依然是串行执行。这是【相机硬件 GigE 协议】限制和 PCIe 无关。电脑端通过28个线程向28个相机通过软触发模式发送拍照指令流程上位机 CPU 发 GVCP/UDP 控制指令→网线到相机嵌入式 CPU 解析→回 UDP-ACK 确认→相机感光采图→FPGA 打包 GVSP 图像 UDP 流→网卡 DMA 直传→写入电脑内存供算法取用相机的交互控制采取软触发控制Demo前台触发连续拍照指令通过CameraManager管理统一向预设好的相机发送广播拍照我们是定制的服务器每个相机均为直连网口每个单独的独立相机均可看作为一个单独的相机。单独相机的控制由CameraWork来实现具体的工作流对流数据进行Frameid的比较以确保为最新的图像而不是缓存帧将相机传输的数据进行多线程入栈入栈后将相机图像进行删除将入栈后的图像进行落盘入站的图像进行清理释放内存。来实现自动化流程代码实现部分CameraManager连接打开部分本文主要刨析拍照部分连接部分暂且略过详细解析打开部分因为打开部分集合了相机的注册回调打开流设置软件触发等关键操作代码部分void GigeCameraManager::openPreset(const QListint CAslots) { // 清空在线相机列表 m_activeSlots.clear(); // 遍历要打开的相机槽位 for (int Cameraslot : CAslots) { // 越界检查防止崩溃 if (Cameraslot 0 || Cameraslot 32) continue; //拿到这个相机的IP检查是否有效 const char* ipC kCameraIPs[Cameraslot]; if (!ipC || strlen(ipC) 7) continue; //跨线程调用让 worker 打开相机 QMetaObject::invokeMethod( m_nodes[Cameraslot].worker, open, Qt::QueuedConnection//队列异步调用 ); QThread::msleep(100); } }通过Qt的函数 Qt::QueuedConnection来向预设好的相机地址发送打开相机的操作其中具体实现是在Work里面其中注意的是顺序必须固定注册回调 → 打开流 → 设置软件触发注册回调的目的在于当相机底层自动触发后自动执行处理规则SetFrameReadyCallback/SetFrameFailCallback其中SetFrameFailCallback反馈的Status表代码如下void GigeCameraWorker::open() { QMutexLocker lk(m_lock); if (m_opened) { emit sigLog(m_slot, 已连接忽略重复打开); emit sigConnected(m_slot, true); return; } m_GigeMan.Close(); m_cam nullptr; tagGigeIP ipBuf { 0 }; QByteArray ipBytes m_ip.toLatin1(); strncpy_s(ipBuf, sizeof(ipBuf), ipBytes.constData(), _TRUNCATE); if (m_GigeMan.AddDeviceByIP(ipBuf) ! COMMON_OK) { emit sigLog(m_slot, AddDeviceByIP 失败); emit sigConnected(m_slot, false); return; } if (m_GigeMan.GetDeviceList() 0) { emit sigLog(m_slot, GetDeviceList 返回0); emit sigConnected(m_slot, false); return; } //Gige已经推流1~2帧率 m_cam m_GigeMan.GetGige(0); if (!m_cam) { emit sigLog(m_slot, GetGige(0) 返回空指针); emit sigConnected(m_slot, false); return; } tagErrMsg errMsg { 0 }; if (!COMMON_SUCCEEDED(m_cam-Open(errMsg))) { emit sigLog(m_slot, QString(Open 失败: %1).arg(QString::fromLocal8Bit(errMsg))); emit sigConnected(m_slot, false); return; } QThread::msleep(100); //在打开流之前标记初始化状态并注册回调 m_isInitializing true;//防止初始状态垃圾帧 m_cam-SetFrameReadyCallback([this](int /*slot*/, uint64_t frameId, bool ready) { // 初始化期间忽略所有回调 if (m_isInitializing) { return; } //记录 readyfalse 的情况但不处理 if (!ready) { return; } { std::lock_guardstd::mutex lk(m_frameMutex); m_lastReadyFrameId frameId; } m_frameCv.notify_one(); }); m_cam-SetFrameFailCallback([this](int status, uint64_t frameId) { // 初始化期间忽略所有失败 if (m_isInitializing) { return; } m_packetLossStats.totalFrames; if (status ! 0) { { std::lock_guardstd::mutex lk(m_frameMutex); m_lastFailFrameId frameId; m_lastFailStatus status; } m_frameCv.notify_one(); } }); // 重置回调状态确保第一帧能被检测到 { std::lock_guardstd::mutex lk(m_frameMutex); m_lastReadyFrameId 0; m_lastFailFrameId 0; m_lastFailStatus 0; } // 现在才打开流回调已就绪第二次推流.stream_callback m_nErrCode m_cam-OpenStream(m_szErrMsg); if (m_nErrCode ! 0) { emit sigLog(m_slot, QString(OpenStream 失败: %1).arg(QString::fromLocal8Bit(m_szErrMsg))); emit sigConnected(m_slot, false); m_isInitializing false; // 失败时重置状态 return; } // 启用软件触发 m_nErrCode m_cam-SetSoftwareTrigger(true, m_szErrMsg); if (m_nErrCode ! 0) { emit sigLog(m_slot, QString(软件触发模式失败: %1).arg(QString::fromLocal8Bit(m_szErrMsg))); emit sigConnected(m_slot, false); m_isInitializing false; return; } QThread::msleep(100); // 重置回调时间 m_cam-ResetCallbackTime(); // 【新增调试】打开相机成功后清零计数器 if (m_cam) m_cam-ResetDebugPhotoCounters(); // 提升线程优先级 HANDLE hThread GetCurrentThread(); if (!SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST)) { emit sigLog(m_slot, QString(设置优先级失败 err%1).arg(GetLastError())); } m_opened true; { std::lock_guardstd::mutex frameLk(m_frameMutex); uint64_t baseFid m_cam-GetLastFrameId(); // 不再依赖预热通常此时为0或当前已有FID m_lastReadyFrameId baseFid; m_lastFailFrameId baseFid; m_lastFailStatus 0; m_lastAcceptedFrameId baseFid; } m_isInitializing false; }拍照部分初始部分清除残留信息绑定CPU线程绑定CPU线程目的在于增加cpu亲和性与稳定性避免高速拍照时由于CPU负载切换核导致系统的不稳定相机丢帧等问题void GigeCameraManager::startBurstAll(int intervalMs, const QString /*saveDir*/) { //清零所有相机的计数器 for (int slot : m_activeSlots) { QMetaObject::invokeMethod( m_nodes[slot].worker, resetBurstCounters, Qt::QueuedConnection ); } //为所有相机绑定CPU核心 bindAllCamerasToCPU(); QThread::msleep(50); m_sched-startSchedule(intervalMs); m_isBursting true; }CPU绑定代码void GigeCameraManager::bindAllCamerasToCPU() { //获取系统 CPU 核心数 SYSTEM_INFO sysInfo; GetSystemInfo(sysInfo); int numCpus sysInfo.dwNumberOfProcessors; //获取当前在线相机列表 QListint slotList m_activeSlots.values(); std::sort(slotList.begin(), slotList.end()); std::vectorint availableCpus; // NUMA Node 0: CPU 24-63 for (int i 24; i 63; i) { availableCpus.push_back(i); } // NUMA Node 1: CPU 88-127 if (slotList.size() availableCpus.size()) { for (int i 88; i 127; i) { availableCpus.push_back(i); } } for (int i 0; i slotList.size(); i) { int slot slotList[i]; int cpuId availableCpus[i % availableCpus.size()]; QMetaObject::invokeMethod( m_nodes[slot].worker, setCpuAffinity, Qt::QueuedConnection, Q_ARG(int, cpuId) ); int numaNode (cpuId 64) ? 0 : 1; } }下面是Work中继续拍照其中为防止相机在UDP-ACK的过程中接收控制性指令导致命令被T掉进行入队式处理代码如下void GigeCameraWorker::photoBurst(qint64 seq, qint64 tickTime) { // 收到拍照指令计数 if (m_cam) { m_cam-IncrementPhotoCommandReceived(); } //加锁防止Manage与woker混乱 { QMutexLocker lk(m_taskMutex); // 防止同一序号入队 //QTcontains有无 if (m_pendingSeqs.contains(seq)) { return; } //将任务放进队列 // enqueue入队 //先进先出 m_pendingSeqs.enqueue(seq); //相机正在拍照直接返回m_isBusy为判定 if (m_isBusy.load()) { emit sigLog(m_slot, QString([队列] seq%1 已加入队列当前队列长度%2) .arg(seq).arg(m_pendingSeqs.size())); return; } } // 如果空闲立即开始处理 processPendingTasks(); }若该条不忙则进入实际拍照代码如下void GigeCameraWorker::processPendingTasks() { while (true) { qint64 currentSeq -1; //从队列取任务 { QMutexLocker lk(m_taskMutex); if (m_pendingSeqs.isEmpty()) { m_isBusy.store(false, std::memory_order_release); return; } currentSeq m_pendingSeqs.dequeue(); m_isBusy.store(true, std::memory_order_release); } // 正常拍一次 int waitMs (currentSeq 1) ? 3000 : 1800; bool timeoutOnly false; bool success captureOneFrameInternal(currentSeq, waitMs, timeoutOnly); //如果是“本轮第一拍”且失败原因是 TIMEOUT则自动补拍一次 if (!success currentSeq 1 timeoutOnly) { emit sigLog(m_slot, QString(seq1 首拍TIMEOUT自动补拍一次)); QThread::msleep(10); // 小间隔避免紧贴上一拍 bool timeoutOnly2 false; success captureOneFrameInternal(currentSeq, waitMs, timeoutOnly2); if (success) { emit sigLog(m_slot, QString(seq1 首拍补拍成功)); } else { emit sigLog(m_slot, QString( seq1 首拍补拍仍失败)); } } // 统一发送完成信号 emit sigPhotoDone(m_slot, currentSeq, success); } }其中第一帧大概率会出现丢帧是因为初始流需要稳定相机相关参数已得到官方确认。对于第一帧采取多于措施其余均正常captureOneFrameInternal。前台work把图像推入批次队列BatchBuffer::pushFrame(...)再将 图像放入FrameStack 内存队列之后Batchreader去读FrameStack内部的图像FrameSaver::writeFrame写入硬盘代码如下其中通过FramePacket包的形式将图像的相关数据打包给下载线程工作下载线程是根据建立新批次时自动启动的代码——ensureBatchReadersStarted();bool GigeCameraManager::newBatch(const QString saveDir) { if (m_batchOpen) { endBatch(); } if (m_activeSlots.isEmpty()) { emit sigLog(-1, newBatch: 无在线相机); return false; } QString base QDir(saveDir.isEmpty() ? QString(../autoimage) : saveDir).absolutePath(); QString dirName output_ QDateTime::currentDateTime().toString(yyyyMMdd_hhmmss); m_batchRoot QDir(base).filePath(dirName); QDir root(m_batchRoot); if (!root.mkpath(.)) { return false; } for (int slot : m_activeSlots) { root.mkpath(QString(cam_%1).arg(slot)); } ensureBatchBuffer(); ensureBatchReadersStarted(); m_batchSegmentId m_batchBuffer-beginSegment( dirName, m_batchRoot, QString() ); if (m_batchSegmentId 0) { emit sigLog(-1, newBatch: beginSegment 失败); return false; } m_batchSeq 0; //记录本批次开始时的基准用差值隔离批次 { QMutexLocker lk(m_batchDoneMutex); m_batchBaseIssuedCommands m_batchIssuedCommands; m_batchBaseReceivedDones m_batchReceivedDones; } m_batchReadyToEndNotified false; m_batchOpen true; return true; }void GigeCameraManager::ensureBatchReadersStarted() { if (m_batchReadersStarted) return; createSaverReaders(); m_batchReadersStarted true; }createSaverReaders();此时下载线程绑定批次下载信息以及线程启动void GigeCameraManager::createSaverReaders() { QListint slotList m_activeSlots.values(); std::sort(slotList.begin(), slotList.end()); int readerId 0; for (int slot : slotList) { QListint mySlots; mySlots.append(slot); auto* reader new BatchStackReader( saver, readerId, m_batchBuffer, mySlots, [this](int segmentId, const QString batchId, const FramePacket pkt) - bool { Q_UNUSED(batchId); auto meta m_batchBuffer-segmentMeta(segmentId); if (meta.rawDir.isEmpty()) { return false; } bool ok FrameSaver::writeFrame(pkt, meta.rawDir); if (ok) { m_batchBuffer-markSaved(segmentId, pkt.slot); } return ok; }, this ); connect(reader, BatchStackReader::sigLog, this, GigeCameraManager::sigLog); connect(reader, QThread::finished, reader, QObject::deleteLater); reader-start(); m_batchReaders.append(reader); } }取图代码将Pkt传入下载线程bool GigeCameraWorker::captureOneFrameInternal(qint64 currentSeq, int waitMs, bool timeoutOnly) { timeoutOnly false; QElapsedTimer timer; timer.start(); QMutexLocker lk(m_lock); //检查相机状态 if (!m_opened || !m_cam) { emit sigLog(m_slot, QString(相机未打开 seq%1).arg(currentSeq)); emit sigGrabStat(m_slot, -1, false, (int)CamGrabMode::Auto); return false; } int w m_cam-GetWidth(); int h m_cam-GetHeight(); if (w 0 || h 0) { emit sigLog(m_slot, QString(尺寸无效 w%1 h%2 seq%3) .arg(w).arg(h).arg(currentSeq)); emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } //记录触发前 frameId uint64_t oldFrameId m_cam-GetLastFrameId(); { std::lock_guardstd::mutex frameLk(m_frameMutex); m_lastReadyFrameId oldFrameId; m_lastFailFrameId oldFrameId; m_lastFailStatus 0; } lk.unlock(); // 解锁避免阻塞回调线程 //触发拍照 m_nErrCode m_cam-ExecuteSoftwareTrigger(m_szErrMsg); if (m_nErrCode ! 0) { emit sigLog(m_slot, QString(触发失败 err%1 重试中... seq%2) .arg(m_nErrCode).arg(currentSeq)); for (int retry 0; retry 2; retry) { QThread::msleep(20); m_nErrCode m_cam-ExecuteSoftwareTrigger(m_szErrMsg); if (m_nErrCode 0) break; } if (m_nErrCode ! 0) { lk.relock(); m_burstTriggerFailCount; lk.unlock(); emit sigLog(m_slot, QString(触发失败3次 err%1 msg%2 seq%3) .arg(m_nErrCode) .arg(QString::fromLocal8Bit(m_szErrMsg)) .arg(currentSeq)); emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } } QThread::msleep(5); //等待 ready/fail 回调 bool gotReadyFrame false; bool gotFailFrame false; int failStatus 0; { std::unique_lockstd::mutex frameLk(m_frameMutex); bool signaled m_frameCv.wait_for( frameLk, std::chrono::milliseconds(waitMs), [] { return (m_lastReadyFrameId oldFrameId) || (m_lastFailFrameId oldFrameId); } ); if (signaled) { if (m_lastReadyFrameId oldFrameId) { gotReadyFrame true; } else { gotFailFrame true; failStatus m_lastFailStatus; } } } // 5. fail callback 分支 if (gotFailFrame) { QString statusText; switch (failStatus) { case 0: statusText SUCCESS; break; case 1: statusText CLEARED; break; case 2: statusText TIMEOUT; break; case 3: statusText MISSING_PACKETS; break; case 4: statusText WRONG_PACKET_ID; break; case 5: statusText SIZE_MISMATCH; break; case 6: statusText FILLING; break; case 7: statusText ABORTED; break; case 8: statusText PAYLOAD_NOT_SUPPORTED; break; default: statusText UNKNOWN; break; } emit sigLog(m_slot, QString(seq%1 帧失败 status%2(%3)跳过本帧) .arg(currentSeq).arg(failStatus).arg(statusText)); if (failStatus 2) { timeoutOnly true; } emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } // 6. wait_for 超时分支 if (!gotReadyFrame) { lk.relock(); m_burstTimeoutCount; uint64_t completed 0, failed 0, underrun 0; m_cam-GetStreamStats(completed, failed, underrun); double callbackIntervalMs m_cam-GetLastCallbackIntervalMs(); LARGE_INTEGER now, freq, lastCallback; QueryPerformanceCounter(now); QueryPerformanceFrequency(freq); lastCallback m_cam-GetLastCallbackTime(); double sinceLastCallbackMs 0.0; if (lastCallback.QuadPart 0) { sinceLastCallbackMs (double)(now.QuadPart - lastCallback.QuadPart) * 1000.0 / freq.QuadPart; } emit sigLog(m_slot, QString( 【超时详情】seq%1 wait%2ms\n 触发前FID%3 → 最后readyFID%4\n SDK统计: ok%5 fail%6 underrun%7\n lastInterval%8ms\n 距上次callback%9ms\n 差异%10ms) .arg(currentSeq) .arg(waitMs) .arg((qulonglong)oldFrameId) .arg((qulonglong)m_lastReadyFrameId) .arg((qulonglong)completed) .arg((qulonglong)failed) .arg((qulonglong)underrun) .arg(callbackIntervalMs, 0, f, 1) .arg(sinceLastCallbackMs, 0, f, 1) .arg(sinceLastCallbackMs - callbackIntervalMs, 0, f, 1)); lk.unlock(); timeoutOnly true; emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } lk.relock(); //获取图像 FrameSnapshot frame; if (!m_cam-GetFrameSnapshot(frame) || !frame.valid) { emit sigLog(m_slot, QString(获取帧快照失败 seq%1).arg(currentSeq)); lk.unlock(); emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } if (frame.frameId m_lastAcceptedFrameId) { emit sigLog(m_slot, QString(仍是旧帧 seq%1 frameId%2 lastAccepted%3) .arg(currentSeq) .arg((qulonglong)frame.frameId) .arg((qulonglong)m_lastAcceptedFrameId)); lk.unlock(); emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } m_lastAcceptedFrameId frame.frameId; lk.unlock(); //组包并写入 BatchBuffer FramePacket pkt; pkt.slot m_slot; pkt.seq currentSeq; pkt.w frame.width; pkt.h frame.height; pkt.stride frame.stride; pkt.frameId frame.frameId; pkt.systemTimestamp frame.systemTimestamp; pkt.cameraTimestamp frame.cameraTimestamp; pkt.raw std::make_sharedstd::vectoruint8_t(std::move(frame.raw)); if (!m_manager) { emit sigLog(m_slot, QString(BatchManager为空 seq%1).arg(currentSeq)); emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } BatchBuffer* batch m_manager-currentBatch(); if (!batch) { emit sigLog(m_slot, QString(BatchBuffer为空 seq%1).arg(currentSeq)); emit sigGrabStat(m_slot, (int)timer.elapsed(), false, (int)CamGrabMode::Auto); return false; } batch-pushFrame(m_slot, pkt); lk.relock(); m_burstSuccessCount; lk.unlock(); return true; }该方法可以实现多相机同时拍照接近少量延时。设备配置Cpu208核32个网口千兆网络避免带宽风险

更多文章