逻辑部分
该章针对支持Gige协议的相机进行多相机控制分析,例如海康,康耐视等相机的高速拍照控制
关于相机拍照的触发形式以及代码层参考
++该章主要讲述工作流方面++
++环境概述:多相机对应多网卡,多网段++
++工作目的:周期性同时(相对)控制相机高速拍照++
++结果需求:多相机同时反馈相关节点图象,要求不丢图++
以上工作均在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
连接+打开部分
++本文主要刨析拍照部分,连接部分暂且略过,详细解析打开部分,因为打开部分集合了相机的注册回调,打开流,设置软件触发等关键操作++
代码部分
cpp
void GigeCameraManager::openPreset(const QList<int>& 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表

代码如下
cpp
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;
}
//记录 ready=false 的情况,但不处理
if (!ready) {
return;
}
{
std::lock_guard<std::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_guard<std::mutex> lk(m_frameMutex);
m_lastFailFrameId = frameId;
m_lastFailStatus = status;
}
m_frameCv.notify_one();
}
});
// 重置回调状态(确保第一帧能被检测到)
{
std::lock_guard<std::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_guard<std::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负载切换核导致系统的不稳定,相机丢帧等问题
cpp
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绑定代码
cpp
void GigeCameraManager::bindAllCamerasToCPU()
{
//获取系统 CPU 核心数
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
int numCpus = sysInfo.dwNumberOfProcessors;
//获取当前在线相机列表
QList<int> slotList = m_activeSlots.values();
std::sort(slotList.begin(), slotList.end());
std::vector<int> 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掉,进行入队式处理
代码如下
cpp
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();
}
若该条不忙则进入实际拍照
代码如下
cpp
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("seq=1 首拍TIMEOUT,自动补拍一次"));
QThread::msleep(10); // 小间隔,避免紧贴上一拍
bool timeoutOnly2 = false;
success = captureOneFrameInternal(currentSeq, waitMs, timeoutOnly2);
if (success) {
emit sigLog(m_slot, QString("seq=1 首拍补拍成功"));
}
else {
emit sigLog(m_slot, QString(" seq=1 首拍补拍仍失败"));
}
}
// 统一发送完成信号
emit sigPhotoDone(m_slot, currentSeq, success);
}
}
其中第一帧大概率会出现丢帧是因为初始流需要稳定相机相关参数,已得到官方确认。对于第一帧采取多于措施,其余均正常captureOneFrameInternal。++前台work把图像推入批次队列BatchBuffer::pushFrame(...),再将 图像放入FrameStack 内存队列,之后Batchreader去读FrameStack内部的图像,FrameSaver::writeFrame写入硬盘++
代码如下
其中通过FramePacket包的形式将图像的相关数据打包给下载线程工作
下载线程是根据建立新批次时自动启动的代码------ensureBatchReadersStarted();
cpp
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;
}
cpp
void GigeCameraManager::ensureBatchReadersStarted()
{
if (m_batchReadersStarted) return;
createSaverReaders();
m_batchReadersStarted = true;
}
createSaverReaders();此时下载线程绑定批次,下载信息以及线程启动
cpp
void GigeCameraManager::createSaverReaders()
{
QList<int> slotList = m_activeSlots.values();
std::sort(slotList.begin(), slotList.end());
int readerId = 0;
for (int slot : slotList) {
QList<int> 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传入下载线程
cpp
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_guard<std::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_lock<std::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_shared<std::vector<uint8_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;
}