#include "VrEyeViewWidget.h" #include "../SpinBoxPasteHelper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include VrEyeViewWidget::VrEyeViewWidget(QWidget* parent) : QWidget(parent) , m_eyeDevice(nullptr) , m_detector(nullptr) , m_leftImageLabel(nullptr) , m_rightImageLabel(nullptr) , m_isConnected(false) , m_isCapturing(false) , m_hasNewImage(false) { // 创建设备和检测器 IVrEyeDevice::CreateObject(&m_eyeDevice); m_detector = CreateChessboardDetectorInstance(); setupUI(); // 为所有 SpinBox 安装粘贴过滤器 SpinBoxPasteHelper::install(this); // 初始化设备 if (m_eyeDevice) { m_eyeDevice->InitDevice(); } // 创建显示定时器 m_displayTimer = new QTimer(this); connect(m_displayTimer, &QTimer::timeout, this, &VrEyeViewWidget::onUpdateDisplay); m_displayTimer->start(33); // 约30fps } VrEyeViewWidget::~VrEyeViewWidget() { if (m_isCapturing) { onStopCapture(); } if (m_isConnected) { onDisconnectCamera(); } if (m_detector) { DestroyChessboardDetectorInstance(m_detector); m_detector = nullptr; } if (m_eyeDevice) { delete m_eyeDevice; m_eyeDevice = nullptr; } } void VrEyeViewWidget::setupUI() { QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(4); // ===== 左右目图像并排显示区域 ===== QHBoxLayout* imageLayout = new QHBoxLayout(); QVBoxLayout* leftLayout = new QVBoxLayout(); leftLayout->setSpacing(0); QLabel* leftTitle = new QLabel("左目", this); leftTitle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); leftLayout->addWidget(leftTitle); m_leftImageLabel = new QLabel(this); m_leftImageLabel->setMinimumSize(320, 240); m_leftImageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_leftImageLabel->setAlignment(Qt::AlignCenter); m_leftImageLabel->setStyleSheet("QLabel { background-color: black; }"); leftLayout->addWidget(m_leftImageLabel, 1); imageLayout->addLayout(leftLayout); QVBoxLayout* rightLayout = new QVBoxLayout(); rightLayout->setSpacing(0); QLabel* rightTitle = new QLabel("右目", this); rightTitle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); rightLayout->addWidget(rightTitle); m_rightImageLabel = new QLabel(this); m_rightImageLabel->setMinimumSize(320, 240); m_rightImageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_rightImageLabel->setAlignment(Qt::AlignCenter); m_rightImageLabel->setStyleSheet("QLabel { background-color: black; }"); rightLayout->addWidget(m_rightImageLabel, 1); imageLayout->addLayout(rightLayout); mainLayout->addLayout(imageLayout, 1); // ===== 第一行:相机连接 + 参数 + 采集按钮 ===== QHBoxLayout* row1 = new QHBoxLayout(); row1->setSpacing(4); // 相机IP + 连接/断开 row1->addWidget(new QLabel("IP:", this)); m_editCameraIP = new QLineEdit("192.168.1.100", this); m_editCameraIP->setFixedWidth(120); row1->addWidget(m_editCameraIP); m_btnConnect = new QPushButton("连接", this); m_btnDisconnect = new QPushButton("断开", this); m_btnDisconnect->setEnabled(false); connect(m_btnConnect, &QPushButton::clicked, this, &VrEyeViewWidget::onConnectCamera); connect(m_btnDisconnect, &QPushButton::clicked, this, &VrEyeViewWidget::onDisconnectCamera); row1->addWidget(m_btnConnect); row1->addWidget(m_btnDisconnect); // 分隔 row1->addSpacing(8); // 曝光/增益 row1->addWidget(new QLabel("曝光:", this)); m_sbExposure = new QSpinBox(this); m_sbExposure->setRange(100, 10000); m_sbExposure->setValue(1000); row1->addWidget(m_sbExposure); row1->addWidget(new QLabel("增益:", this)); m_sbGain = new QSpinBox(this); m_sbGain->setRange(0, 255); m_sbGain->setValue(100); row1->addWidget(m_sbGain); // 分隔 row1->addSpacing(8); // 采集 + 加载按钮 m_btnStartCapture = new QPushButton("开始采集", this); m_btnStopCapture = new QPushButton("停止采集", this); QPushButton* btnLoadLeftImage = new QPushButton("加载左图", this); QPushButton* btnLoadRightImage = new QPushButton("加载右图", this); m_btnStartCapture->setEnabled(false); m_btnStopCapture->setEnabled(false); connect(m_btnStartCapture, &QPushButton::clicked, this, &VrEyeViewWidget::onStartCapture); connect(m_btnStopCapture, &QPushButton::clicked, this, &VrEyeViewWidget::onStopCapture); connect(btnLoadLeftImage, &QPushButton::clicked, this, &VrEyeViewWidget::onLoadLeftImage); connect(btnLoadRightImage, &QPushButton::clicked, this, &VrEyeViewWidget::onLoadRightImage); row1->addWidget(m_btnStartCapture); row1->addWidget(m_btnStopCapture); row1->addWidget(btnLoadLeftImage); row1->addWidget(btnLoadRightImage); row1->addStretch(); mainLayout->addLayout(row1); // ===== 第二行:相机内参(左) + 标定板检测(右) ===== QHBoxLayout* row2 = new QHBoxLayout(); row2->setSpacing(4); // 相机内参(3x3矩阵) QGroupBox* intrinsicsGroup = new QGroupBox("相机内参", this); QGridLayout* intrinsicsLayout = new QGridLayout(intrinsicsGroup); intrinsicsLayout->setSpacing(2); intrinsicsLayout->setContentsMargins(4, 8, 4, 4); auto createIntrinsicSpinBox = [this](double minVal, double maxVal, double defaultVal) { QDoubleSpinBox* sb = new QDoubleSpinBox(this); sb->setRange(minVal, maxVal); sb->setValue(defaultVal); sb->setDecimals(2); return sb; }; auto createFixedLabel = [this](const QString& text) { QLabel* lbl = new QLabel(text, this); lbl->setAlignment(Qt::AlignCenter); lbl->setStyleSheet("QLabel { color: gray; }"); return lbl; }; m_sbFx = createIntrinsicSpinBox(100, 5000, 2384.8520129909352); m_sbFx->setPrefix("fx: "); m_sbFx->setToolTip("fx: X方向焦距(像素)"); m_sbFy = createIntrinsicSpinBox(100, 5000, 2384.8520129909352); m_sbFy->setPrefix("fy: "); m_sbFy->setToolTip("fy: Y方向焦距(像素)"); m_sbCx = createIntrinsicSpinBox(0, 2000, 232.37469863891602); m_sbCx->setPrefix("cx: "); m_sbCx->setToolTip("cx: 主点X坐标(像素)"); m_sbCy = createIntrinsicSpinBox(0, 2000, 1054.649814605713); m_sbCy->setPrefix("cy: "); m_sbCy->setToolTip("cy: 主点Y坐标(像素)"); intrinsicsLayout->addWidget(m_sbFx, 0, 0); intrinsicsLayout->addWidget(createFixedLabel("0"), 0, 1); intrinsicsLayout->addWidget(m_sbCx, 0, 2); intrinsicsLayout->addWidget(createFixedLabel("0"), 1, 0); intrinsicsLayout->addWidget(m_sbFy, 1, 1); intrinsicsLayout->addWidget(m_sbCy, 1, 2); intrinsicsLayout->addWidget(createFixedLabel("0"), 2, 0); intrinsicsLayout->addWidget(createFixedLabel("0"), 2, 1); intrinsicsLayout->addWidget(createFixedLabel("1"), 2, 2); row2->addWidget(intrinsicsGroup); // 标定板检测 QGroupBox* detectionGroup = new QGroupBox("标定板检测", this); QGridLayout* detectionLayout = new QGridLayout(detectionGroup); detectionLayout->setSpacing(2); detectionLayout->setContentsMargins(4, 8, 4, 4); detectionLayout->addWidget(new QLabel("角点宽:", this), 0, 0); m_sbPatternWidth = new QSpinBox(this); m_sbPatternWidth->setRange(2, 20); m_sbPatternWidth->setValue(8); detectionLayout->addWidget(m_sbPatternWidth, 0, 1); detectionLayout->addWidget(new QLabel("角点高:", this), 0, 2); m_sbPatternHeight = new QSpinBox(this); m_sbPatternHeight->setRange(2, 20); m_sbPatternHeight->setValue(11); detectionLayout->addWidget(m_sbPatternHeight, 0, 3); detectionLayout->addWidget(new QLabel("格子(mm):", this), 1, 0); m_sbSquareSize = new QDoubleSpinBox(this); m_sbSquareSize->setRange(1.0, 100.0); m_sbSquareSize->setValue(30.0); m_sbSquareSize->setDecimals(2); detectionLayout->addWidget(m_sbSquareSize, 1, 1); m_cbAdaptiveThresh = new QCheckBox("自适应阈值", this); m_cbAdaptiveThresh->setChecked(true); m_cbNormalizeImage = new QCheckBox("归一化图像", this); m_cbNormalizeImage->setChecked(true); detectionLayout->addWidget(m_cbAdaptiveThresh, 1, 2); detectionLayout->addWidget(m_cbNormalizeImage, 1, 3); m_cbAutoDetect = new QCheckBox("自动检测", this); detectionLayout->addWidget(m_cbAutoDetect, 2, 0, 1, 2); m_btnDetect = new QPushButton("计算标定板信息", this); m_btnDetect->setEnabled(false); connect(m_btnDetect, &QPushButton::clicked, this, &VrEyeViewWidget::onDetectChessboard); detectionLayout->addWidget(m_btnDetect, 2, 2, 1, 2); row2->addWidget(detectionGroup); mainLayout->addLayout(row2); // ===== 状态栏 ===== m_lblStatus = new QLabel("状态: 未连接", this); m_lblDetectionResult = new QLabel("检测结果: 无", this); mainLayout->addWidget(m_lblStatus); mainLayout->addWidget(m_lblDetectionResult); } void VrEyeViewWidget::SetDetectionCallback(DetectionCallback callback) { m_detectionCallback = callback; } void VrEyeViewWidget::onConnectCamera() { if (!m_eyeDevice) return; QString ip = m_editCameraIP->text(); int ret = m_eyeDevice->OpenDevice(ip.toStdString().c_str(), false, false, false); if (ret == 0) { m_isConnected = true; m_btnConnect->setEnabled(false); m_btnDisconnect->setEnabled(true); m_btnStartCapture->setEnabled(true); m_lblStatus->setText("状态: 已连接"); // 设置相机参数 unsigned int exposure = m_sbExposure->value(); unsigned int gain = m_sbGain->value(); m_eyeDevice->SetEyeExpose(exposure); m_eyeDevice->SetEyeGain(gain); } else { QMessageBox::warning(this, "错误", QString("连接相机失败,错误码: %1").arg(ret)); } } void VrEyeViewWidget::onDisconnectCamera() { if (!m_eyeDevice) return; if (m_isCapturing) { onStopCapture(); } m_eyeDevice->CloseDevice(); m_isConnected = false; m_btnConnect->setEnabled(true); m_btnDisconnect->setEnabled(false); m_btnStartCapture->setEnabled(false); m_lblStatus->setText("状态: 未连接"); } void VrEyeViewWidget::onStartCapture() { if (!m_eyeDevice || !m_isConnected) return; int ret = m_eyeDevice->StartCapture(OnImageCallback, this); if (ret == 0) { m_isCapturing = true; m_btnStartCapture->setEnabled(false); m_btnStopCapture->setEnabled(true); m_btnDetect->setEnabled(true); m_lblStatus->setText("状态: 采集中"); } else { QMessageBox::warning(this, "错误", QString("开始采集失败,错误码: %1").arg(ret)); } } void VrEyeViewWidget::onStopCapture() { if (!m_eyeDevice) return; m_eyeDevice->StopCapture(); m_isCapturing = false; m_btnStartCapture->setEnabled(true); m_btnStopCapture->setEnabled(false); m_lblStatus->setText("状态: 已连接"); } void VrEyeViewWidget::OnImageCallback(SVzNLImageData* pLeftImage, SVzNLImageData* pRightImage, SVzNLImageData* /*pCenterImage*/, const SVzOutputFrameProps* /*pFrameProps*/, void* pUserData) { VrEyeViewWidget* pThis = static_cast(pUserData); if (pThis) { pThis->ProcessImageData(pLeftImage, pRightImage); } } /** * @brief 将 SDK 图像数据转为 QImage */ static QImage SvzImageToQImage(SVzNLImageData* pImage) { if (!pImage || !pImage->pBuffer || pImage->nWidth == 0 || pImage->nHeight == 0) { return QImage(); } int w = static_cast(pImage->nWidth); int h = static_cast(pImage->nHeight); if (pImage->eImageType == keVzNLImageType_RGB888) { // RGB888: 直接拷贝 return QImage(pImage->pBuffer, w, h, w * 3, QImage::Format_RGB888).copy(); } else if (pImage->eImageType == keVzNLImageType_BGR888) { // BGR888: 转为 RGB QImage img(pImage->pBuffer, w, h, w * 3, QImage::Format_RGB888); return img.rgbSwapped(); } else if (pImage->eImageType == keVzNLImageType_GRAY) { // 灰度图 return QImage(pImage->pBuffer, w, h, w, QImage::Format_Grayscale8).copy(); } else if (pImage->eImageType == keVzNLImageType_BGRA8888) { // BGRA -> ARGB32 QImage img(pImage->pBuffer, w, h, w * 4, QImage::Format_ARGB32); return img.copy(); } // 其他格式:按灰度处理 if (pImage->nChannels == 1) { return QImage(pImage->pBuffer, w, h, w, QImage::Format_Grayscale8).copy(); } else if (pImage->nChannels == 3) { return QImage(pImage->pBuffer, w, h, w * 3, QImage::Format_RGB888).copy(); } return QImage(); } void VrEyeViewWidget::ProcessImageData(SVzNLImageData* pLeftImage, SVzNLImageData* pRightImage) { QImage left = SvzImageToQImage(pLeftImage); QImage right = SvzImageToQImage(pRightImage); { std::lock_guard lock(m_captureMutex); if (!left.isNull()) m_captureLeft = left; if (!right.isNull()) m_captureRight = right; m_hasNewImage = true; } } void VrEyeViewWidget::onUpdateDisplay() { if (!m_hasNewImage) return; { std::lock_guard lock(m_captureMutex); if (!m_captureLeft.isNull()) m_leftImage = m_captureLeft; if (!m_captureRight.isNull()) m_rightImage = m_captureRight; m_hasNewImage = false; } // 清除旧的检测角点 m_lastLeftCorners.clear(); m_lastRightCorners.clear(); UpdateImageDisplay(); } void VrEyeViewWidget::onDetectChessboard() { if (!m_detector) return; if (m_leftImage.isNull() || m_rightImage.isNull()) { m_lblDetectionResult->setText("检测结果: 请先加载左右目图片"); return; } // 设置检测参数 m_detector->SetDetectionFlags( m_cbAdaptiveThresh->isChecked(), m_cbNormalizeImage->isChecked(), false); // 准备相机内参 CameraIntrinsics intrinsics; intrinsics.fx = m_sbFx->value(); intrinsics.fy = m_sbFy->value(); intrinsics.cx = m_sbCx->value(); intrinsics.cy = m_sbCy->value(); // 检测左目标定板 QImage leftRgb = m_leftImage.convertToFormat(QImage::Format_RGB888); ChessboardDetectResult leftResult; int ret = m_detector->DetectChessboardWithPose( leftRgb.bits(), leftRgb.width(), leftRgb.height(), 3, m_sbPatternWidth->value(), m_sbPatternHeight->value(), m_sbSquareSize->value(), intrinsics, leftResult); // 检测右目标定板 QImage rightRgb = m_rightImage.convertToFormat(QImage::Format_RGB888); ChessboardDetectResult rightResult; int retRight = m_detector->DetectChessboardWithPose( rightRgb.bits(), rightRgb.width(), rightRgb.height(), 3, m_sbPatternWidth->value(), m_sbPatternHeight->value(), m_sbSquareSize->value(), intrinsics, rightResult); // 在两张图上绘制角点并显示 if (ret == 0 && leftResult.detected) { m_lastLeftCorners = leftResult.corners; } else { m_lastLeftCorners.clear(); } if (retRight == 0 && rightResult.detected) { m_lastRightCorners = rightResult.corners; } else { m_lastRightCorners.clear(); } // 在副本上绘制角点并显示(不修改原图) QImage leftDisplay = m_leftImage.copy(); QImage rightDisplay = m_rightImage.copy(); DrawCorners(leftDisplay, m_lastLeftCorners); DrawCorners(rightDisplay, m_lastRightCorners); QPixmap leftPm = QPixmap::fromImage(leftDisplay); m_leftImageLabel->setPixmap( leftPm.scaled(m_leftImageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); QPixmap rightPm = QPixmap::fromImage(rightDisplay); m_rightImageLabel->setPixmap( rightPm.scaled(m_rightImageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); if (ret == 0 && leftResult.detected && retRight == 0 && rightResult.detected) { m_lastDetection.detected = true; if (leftResult.hasPose) { m_lastDetection.x = leftResult.center.x; m_lastDetection.y = leftResult.center.y; m_lastDetection.z = leftResult.center.z; m_lastDetection.nx = leftResult.normal.x; m_lastDetection.ny = leftResult.normal.y; m_lastDetection.nz = leftResult.normal.z; m_lastDetection.rx = leftResult.eulerAngles[0]; m_lastDetection.ry = leftResult.eulerAngles[1]; m_lastDetection.rz = leftResult.eulerAngles[2]; QString resultText = QString("检测结果: " "左目 %1 个角点, 右目 %2 个角点 | " "位置: (%3, %4, %5)mm | " "法向量: (%6, %7, %8) | " "姿态: (%9, %10, %11)") .arg(m_lastLeftCorners.size()) .arg(m_lastRightCorners.size()) .arg(m_lastDetection.x, 0, 'f', 3) .arg(m_lastDetection.y, 0, 'f', 3) .arg(m_lastDetection.z, 0, 'f', 3) .arg(m_lastDetection.nx, 0, 'f', 3) .arg(m_lastDetection.ny, 0, 'f', 3) .arg(m_lastDetection.nz, 0, 'f', 3) .arg(m_lastDetection.rx, 0, 'f', 3) .arg(m_lastDetection.ry, 0, 'f', 3) .arg(m_lastDetection.rz, 0, 'f', 3); m_lblDetectionResult->setText(resultText); if (m_detectionCallback) { m_detectionCallback(m_lastDetection); } emit chessboardDetected(m_lastDetection); } } else { m_lastDetection.detected = false; QString errorMsg = "检测结果: "; if (ret != 0 || !leftResult.detected) { errorMsg += "左目未检测到标定板 "; } if (retRight != 0 || !rightResult.detected) { errorMsg += "右目未检测到标定板"; } m_lblDetectionResult->setText(errorMsg); } } void VrEyeViewWidget::onLoadLeftImage() { QString fileName = QFileDialog::getOpenFileName( this, "选择左目图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*.*)"); if (fileName.isEmpty()) return; QImage image(fileName); if (image.isNull()) { QMessageBox::warning(this, "错误", QString("无法加载左目图片:\n%1").arg(fileName)); return; } m_leftImage = image; UpdateImageDisplay(); QFileInfo fi(fileName); m_lblStatus->setText(QString("状态: 已加载左目 %1 (%2x%3)") .arg(fi.fileName()) .arg(image.width()).arg(image.height())); if (!m_leftImage.isNull() && !m_rightImage.isNull()) { m_btnDetect->setEnabled(true); } } void VrEyeViewWidget::onLoadRightImage() { QString fileName = QFileDialog::getOpenFileName( this, "选择右目图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*.*)"); if (fileName.isEmpty()) return; QImage image(fileName); if (image.isNull()) { QMessageBox::warning(this, "错误", QString("无法加载右目图片:\n%1").arg(fileName)); return; } m_rightImage = image; UpdateImageDisplay(); QFileInfo fi(fileName); m_lblStatus->setText(QString("状态: 已加载右目 %1 (%2x%3)") .arg(fi.fileName()) .arg(image.width()).arg(image.height())); if (!m_leftImage.isNull() && !m_rightImage.isNull()) { m_btnDetect->setEnabled(true); } } void VrEyeViewWidget::UpdateImageDisplay() { // 左目:保持比例缩放到 label 大小,不拉伸 if (!m_leftImage.isNull()) { QImage leftDisplay = m_leftImage.copy(); if (!m_lastLeftCorners.empty()) { DrawCorners(leftDisplay, m_lastLeftCorners); } QPixmap pm = QPixmap::fromImage(leftDisplay); m_leftImageLabel->setPixmap( pm.scaled(m_leftImageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } // 右目 if (!m_rightImage.isNull()) { QImage rightDisplay = m_rightImage.copy(); if (!m_lastRightCorners.empty()) { DrawCorners(rightDisplay, m_lastRightCorners); } QPixmap pm = QPixmap::fromImage(rightDisplay); m_rightImageLabel->setPixmap( pm.scaled(m_rightImageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } } void VrEyeViewWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); UpdateImageDisplay(); } void VrEyeViewWidget::DrawCorners(QImage& image, const std::vector& corners) { if (corners.empty()) return; QPainter painter(&image); painter.setPen(QPen(Qt::green, 3)); for (const auto& pt : corners) { painter.drawEllipse(QPointF(pt.x, pt.y), 5, 5); } }