/
klischa
/
AstraScanner2
Обзор
Документация
Войти
/
klischa
/
AstraScanner2
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/controllers/ScanWorkflow.cpp
484 строки
17 KB
k k
fix: mirror only applies to RGB, not depth
15 июл 2026, 13:52
15 июл 2026, 13:52
b1b1c50
Код
Авторство
О чём код?
#include "ScanWorkflow.h" #include "CaptureSessionController.h" #include "../project/ProjectManager.h" #include "ViewerCoordinator.h" #include "../settings/SettingsManager.h" #include <QTimer> #include <QLabel> #include <QPushButton> #include <QTextEdit> #include <QDebug> #include <QApplication> #include <QStatusBar> #include <QVTKOpenGLNativeWidget.h> #include <vtkGenericOpenGLRenderWindow.h> #include <opencv2/imgproc.hpp> #include <pcl/filters/voxel_grid.h> ScanWorkflow::ScanWorkflow(CaptureSessionController *captureSession, ProjectManager *project, ViewerCoordinator *viewer, QObject *parent) : QObject(parent) , m_captureSession(captureSession) , m_project(project) , m_viewerCoordinator(viewer) , m_accumulatedCloud(new pcl::PointCloud<pcl::PointXYZRGB>) , m_surfelCoverageTracker(std::make_unique<SurfelCoverageTracker>()) { } ScanWorkflow::~ScanWorkflow() = default; void ScanWorkflow::bindWidgets(const Widgets &w) { m_w = w; // Connect viewer update timer if (m_w.viewerUpdateTimer) { connect(m_w.viewerUpdateTimer, &QTimer::timeout, this, &ScanWorkflow::updateViewer); } // Connect scan timeout timer if (m_w.scanTimeoutTimer) { m_w.scanTimeoutTimer->setSingleShot(true); connect(m_w.scanTimeoutTimer, &QTimer::timeout, this, &ScanWorkflow::checkScanTimeout); } } bool ScanWorkflow::isCapturing() const { return m_captureSession && m_captureSession->isRunning(); } // ========== Scan Control ========== void ScanWorkflow::onPreviewClicked() { qDebug() << "Preview clicked"; // Bug 19: Stop timers from previous scan mode if (m_w.scanTimeoutTimer) m_w.scanTimeoutTimer->stop(); if (m_w.viewerUpdateTimer) m_w.viewerUpdateTimer->stop(); if (m_captureSession && m_captureSession->isRunning() && !stopCapture()) return; m_scanning = false; m_cloudProcessing = false; setCaptureButtonsState(false, true, false, true); resetCaptureStats(); resetScanQualityPanel(); startCapture(false); } void ScanWorkflow::onScanClicked() { qDebug() << "=== Scan clicked ==="; if (m_captureSession && m_captureSession->isRunning() && !stopCapture()) return; m_scanning = true; m_cloudProcessing = true; setCaptureButtonsState(false, false, true, true); resetCaptureStats(); resetScanQualityPanel(); clearAccumulatedCloud(); startCapture(true); // Start timers unconditionally — isCapturing() may return false due to // queued lambda from previous terminated thread nulling m_captureThread. if (m_w.scanTimeoutTimer) { m_w.scanTimeoutTimer->start(SettingsManager::instance().scanTimeoutSec() * 1000); } if (m_w.viewerUpdateTimer) { m_w.viewerUpdateTimer->start(500); } } void ScanWorkflow::onPauseClicked() { m_scanning = !m_scanning; m_cloudProcessing = m_scanning; if (m_captureSession) { m_captureSession->setCloudProcessingEnabled(m_cloudProcessing); } if (m_w.pauseBtn) m_w.pauseBtn->setText(m_scanning ? "Pause" : "Resume"); } void ScanWorkflow::onStopClicked() { qDebug() << "Stop clicked"; if (m_w.scanTimeoutTimer) m_w.scanTimeoutTimer->stop(); if (m_w.viewerUpdateTimer) m_w.viewerUpdateTimer->stop(); m_scanning = false; m_cloudProcessing = false; autoSaveAccumulatedCloudToProject(); stopCapture(); // Bug 7: Always restore UI state regardless of stopCapture result setCaptureButtonsState(true, true, false, false); resetScanQualityPanel(); if (m_w.pauseBtn) m_w.pauseBtn->setText("Pause"); updateViewer(); emit scanStopped(); } void ScanWorkflow::onClearClicked() { qDebug() << "Clear clicked"; clearAccumulatedCloud(); if (m_viewerCoordinator) { m_viewerCoordinator->clearMainViewer(); if (m_w.vtkWidget && m_w.vtkWidget->renderWindow()) m_w.vtkWidget->renderWindow()->Render(); } resetCaptureStats(); } void ScanWorkflow::checkScanTimeout() { if (m_scanning) { qWarning() << "Scan timeout, stopping"; onStopClicked(); } } void ScanWorkflow::startCapture(bool enableCloudProcessing) { if (m_captureSession->isRunning() && !stopCapture()) return; const SettingsManager &settings = SettingsManager::instance(); const bool neuralEnabled = (m_project && m_project->getTrackingMode() == TrackingMode::NeuralBased); m_captureSession->setProjectManager(m_project); m_captureSession->start(enableCloudProcessing, static_cast<float>(settings.depthMin()), static_cast<float>(settings.depthMax()), settings.colorCameraEnabled(), neuralEnabled, settings.useCudaForNeural()); qDebug() << "Capture started, cloud:" << enableCloudProcessing; } bool ScanWorkflow::stopCapture() { if (!m_captureSession) return true; if (!m_captureSession->stop()) { qCritical() << "Capture thread did not stop within timeout"; return false; } qDebug() << "Capture stopped"; return true; } // ========== Frame Handlers ========== void ScanWorkflow::onNewFrame(QSharedPointer<cv::Mat> color, QSharedPointer<cv::Mat> depth) { static QElapsedTimer guiTimer; if (!guiTimer.isValid()) guiTimer.start(); bool updateGui = (guiTimer.elapsed() > 100); if (updateGui) guiTimer.restart(); if (color && !color->empty()) updateRgbPreviewFrame(*color, updateGui); if (depth && !depth->empty()) updateDepthPreviewFrame(*depth, updateGui); updateElapsedTimeLabel(); } void ScanWorkflow::onPointCloudReady(pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud) { if (!cloud || cloud->empty()) { qInfo() << "[ScanWF] onPointCloudReady: empty/null"; return; } if (!m_viewerCoordinator) { qInfo() << "[ScanWF] onPointCloudReady: no viewerCoordinator"; return; } if (!m_cloudProcessing) { qInfo() << "[ScanWF] onPointCloudReady: m_cloudProcessing=false"; return; } qInfo() << "[ScanWF] onPointCloudReady: size=" << cloud->size() << "scanning=" << m_scanning; if (m_scanning) { accumulateIncomingCloud(cloud); } else { showLivePreviewCloud(cloud); } } void ScanWorkflow::onFrameProcessed(int totalFrames) { updateFrameStats(totalFrames); } void ScanWorkflow::onCaptureWarning(const QString &msg) { qWarning() << "[Capture warning]" << msg; if (m_w.logTextEdit) appendLog(QString("[WARN] %1").arg(msg)); } void ScanWorkflow::onCaptureError(const QString &msg) { qCritical() << "[Capture error]" << msg; if (m_w.logTextEdit) appendLog(QString("[ERROR] %1").arg(msg)); emit captureError(msg); } void ScanWorkflow::onMarkersDetected(QSharedPointer<cv::Mat> markersImage) { static int markerFrameCounter = 0; if (++markerFrameCounter % 5 != 0) return; if (markersImage && !markersImage->empty() && m_w.rgbLabel) { QImage qimg(markersImage->data, markersImage->cols, markersImage->rows, markersImage->step, QImage::Format_BGR888); QPixmap pix = QPixmap::fromImage(qimg).scaled(m_w.rgbLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation); m_w.rgbLabel->setPixmap(pix); } } // ========== Cloud Accumulation ========== void ScanWorkflow::accumulateIncomingCloud(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &cloud) { if (!cloud || cloud->empty()) return; m_frameCounter++; if (m_frameCounter % m_frameSkip != 0) { qInfo() << "[ScanWF] accumulate: skipped frame" << m_frameCounter << "(mod" << m_frameSkip << ")"; return; } qInfo() << "[ScanWF] accumulate: adding" << cloud->size() << "points, total before:" << m_accumulatedCloud->size(); accumulateConfidenceCoverage(cloud); try { QMutexLocker locker(&m_cloudMutex); *m_accumulatedCloud += *cloud; // Periodic downsample to prevent OOM on long scans if (m_accumulatedCloud->size() > 500000) { pcl::VoxelGrid<pcl::PointXYZRGB> vg; vg.setLeafSize(0.005f, 0.005f, 0.005f); vg.setSaveLeafLayout(false); vg.setInputCloud(m_accumulatedCloud); auto downsampled = pcl::make_shared<pcl::PointCloud<pcl::PointXYZRGB>>(); vg.filter(*downsampled); *m_accumulatedCloud = *downsampled; } } catch (const std::bad_alloc &) { qCritical() << "[ScanWorkflow] Out of memory during cloud accumulation"; m_accumulatedCloud->clear(); } catch (...) { qCritical() << "[ScanWorkflow] Exception during cloud accumulation"; } if (m_w.frameCountLabel) m_w.frameCountLabel->setText(QString("Frames: %1").arg(m_frameCounter)); emit cloudSizeChanged(static_cast<int>(m_accumulatedCloud->size())); } void ScanWorkflow::showLivePreviewCloud(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &cloud) { if (m_viewerCoordinator) m_viewerCoordinator->showAccumulatedCloud(cloud); } void ScanWorkflow::clearAccumulatedCloud() { QMutexLocker locker(&m_cloudMutex); m_accumulatedCloud->clear(); } // ========== Quality ========== void ScanWorkflow::resetConfidenceCoverage() { if (m_surfelCoverageTracker) m_surfelCoverageTracker->reset(); updateCoverageSummary(); } void ScanWorkflow::accumulateConfidenceCoverage(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &cloud) { if (!m_surfelCoverageTracker || !cloud || cloud->empty()) return; Eigen::Vector3f origin = Eigen::Vector3f::Zero(); m_surfelCoverageTracker->integrateFrame(cloud, origin); } void ScanWorkflow::updateCoverageSummary() { if (!m_w.coverageSummaryLabel || !m_surfelCoverageTracker) return; const auto s = m_surfelCoverageTracker->stats(); m_w.coverageSummaryLabel->setText( QString("Surfels: %1 | Good: %2 | Warn: %3 | Poor: %4") .arg(s.surfelCount).arg(s.goodSurfels) .arg(s.warningSurfels).arg(s.poorSurfels)); } void ScanWorkflow::updateScanQualityPanel(const QString &trackingText, int trackingLevel, const QString &alignmentText, int alignmentLevel, const QString &depthText, int depthLevel, const QString &driftText, int driftLevel) { auto styleForLevel = [](int level) -> QString { switch (level) { case 0: return QStringLiteral("background-color: #eef8ef; color: #1d5a33; border: 1px solid #78b48a;"); case 1: return QStringLiteral("background-color: #fff7e8; color: #8b5d10; border: 1px solid #d8aa57;"); default: return QStringLiteral("background-color: #fdeeee; color: #8d2d2d; border: 1px solid #d27a7a;"); } }; if (m_w.trackingQualityLabel) { m_w.trackingQualityLabel->setText(trackingText); m_w.trackingQualityLabel->setStyleSheet(styleForLevel(trackingLevel)); } if (m_w.alignmentQualityLabel) { m_w.alignmentQualityLabel->setText(alignmentText); m_w.alignmentQualityLabel->setStyleSheet(styleForLevel(alignmentLevel)); } if (m_w.depthQualityLabel) { m_w.depthQualityLabel->setText(depthText); m_w.depthQualityLabel->setStyleSheet(styleForLevel(depthLevel)); } if (m_w.driftQualityLabel) { m_w.driftQualityLabel->setText(driftText); m_w.driftQualityLabel->setStyleSheet(styleForLevel(driftLevel)); } m_lastTrackingQualityLevel = trackingLevel; m_lastAlignmentQualityLevel = alignmentLevel; m_lastDepthQualityLevel = depthLevel; m_lastDriftQualityLevel = driftLevel; } void ScanWorkflow::resetScanQualityPanel() { updateScanQualityPanel("—", 1, "—", 1, "—", 1, "—", 1); } // ========== UI Updates ========== void ScanWorkflow::setCaptureButtonsState(bool preview, bool scan, bool pause, bool stop) { if (m_w.previewBtn) m_w.previewBtn->setEnabled(preview); if (m_w.scanBtn) m_w.scanBtn->setEnabled(scan); if (m_w.pauseBtn) m_w.pauseBtn->setEnabled(pause); if (m_w.stopBtn) m_w.stopBtn->setEnabled(stop); } void ScanWorkflow::resetCaptureStats() { m_frameCounter = 0; m_totalFrames = 0; m_scanTimer.start(); if (m_w.frameCountLabel) m_w.frameCountLabel->setText("Frames: 0"); if (m_w.fpsLabel) m_w.fpsLabel->setText("FPS: 0"); if (m_w.elapsedTimeLabel) m_w.elapsedTimeLabel->setText("00:00"); QMutexLocker locker(&m_cloudMutex); m_accumulatedCloud->clear(); resetConfidenceCoverage(); } void ScanWorkflow::updateFrameStats(int totalFrames) { m_totalFrames = totalFrames; if (m_w.frameCountLabel) m_w.frameCountLabel->setText(QString("Frames: %1").arg(totalFrames)); if (m_scanTimer.isValid() && m_scanTimer.elapsed() > 0) { double fps = totalFrames * 1000.0 / m_scanTimer.elapsed(); if (m_w.fpsLabel) m_w.fpsLabel->setText(QString("FPS: %1").arg(fps, 0, 'f', 1)); } } void ScanWorkflow::updateElapsedTimeLabel() { if (!m_w.elapsedTimeLabel || !m_scanTimer.isValid()) return; qint64 ms = m_scanTimer.elapsed(); int sec = static_cast<int>(ms / 1000) % 60; int min = static_cast<int>(ms / 60000); m_w.elapsedTimeLabel->setText(QString("%1:%2") .arg(min, 2, 10, QChar('0')) .arg(sec, 2, 10, QChar('0'))); } void ScanWorkflow::updateRgbPreviewFrame(const cv::Mat &color, bool updateGui) { if (!updateGui) return; cv::Mat display = color; if (SettingsManager::instance().mirrorRgb()) { cv::flip(color, display, 1); } QImage qimg(display.data, display.cols, display.rows, display.step, QImage::Format_BGR888); QPixmap pix = QPixmap::fromImage(qimg); if (m_w.rgbLabel) { m_w.rgbLabel->setPixmap(pix.scaled(m_w.rgbLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } if (m_w.calibRgbLabel) { m_w.calibRgbLabel->setPixmap(pix.scaled(m_w.calibRgbLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } if (m_w.homeCalibRgbLabel) { m_w.homeCalibRgbLabel->setPixmap(pix.scaled(m_w.homeCalibRgbLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } } void ScanWorkflow::updateDepthPreviewFrame(const cv::Mat &depth, bool updateGui) { if (!updateGui || !m_w.depthLabel) return; cv::Mat colored; cv::normalize(depth, colored, 0, 255, cv::NORM_MINMAX, CV_8UC1); cv::applyColorMap(colored, colored, m_depthColormap); QImage qimg(colored.data, colored.cols, colored.rows, colored.step, QImage::Format_BGR888); m_w.depthLabel->setPixmap(QPixmap::fromImage(qimg).scaled( m_w.depthLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } void ScanWorkflow::updateViewer() { if (m_viewerBusy) { qInfo() << "[ScanWF] updateViewer: busy, skip"; return; } m_viewerBusy = true; try { if (!m_viewerCoordinator) { qInfo() << "[ScanWF] updateViewer: no viewerCoordinator"; m_viewerBusy = false; return; } if (!m_accumulatedCloud) { qInfo() << "[ScanWF] updateViewer: no accumulatedCloud"; m_viewerBusy = false; return; } pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloudCopy; { QMutexLocker locker(&m_cloudMutex); if (m_accumulatedCloud->empty()) { qInfo() << "[ScanWF] updateViewer: cloud empty"; m_viewerBusy = false; return; } cloudCopy = pcl::make_shared<pcl::PointCloud<pcl::PointXYZRGB>>(*m_accumulatedCloud); } qInfo() << "[ScanWF] updateViewer: showing" << cloudCopy->size() << "points"; if (cloudCopy->size() >= 3) { auto &p0 = cloudCopy->points[0]; auto &p1 = cloudCopy->points[cloudCopy->size()/2]; auto &p2 = cloudCopy->points.back(); qInfo() << "[ScanWF] [0]=" << p0.x << p0.y << p0.z << "rgb=" << (int)p0.r << (int)p0.g << (int)p0.b; qInfo() << "[ScanWF] [mid]=" << p1.x << p1.y << p1.z << "rgb=" << (int)p1.r << (int)p1.g << (int)p1.b; qInfo() << "[ScanWF] [end]=" << p2.x << p2.y << p2.z << "rgb=" << (int)p2.r << (int)p2.g << (int)p2.b; } m_viewerCoordinator->showAccumulatedCloud(cloudCopy); } catch (...) { qCritical() << "Exception in updateViewer"; } m_viewerBusy = false; } void ScanWorkflow::autoSaveAccumulatedCloudToProject() { if (!m_project || !m_project->isOpen()) return; QMutexLocker locker(&m_cloudMutex); if (m_accumulatedCloud->empty()) return; auto cloudCopy = pcl::make_shared<pcl::PointCloud<pcl::PointXYZRGB>>(*m_accumulatedCloud); locker.unlock(); m_project->addScan(cloudCopy, QString("scan_%1").arg( QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"))); qDebug() << "Auto-saved scan:" << cloudCopy->size() << "points"; } void ScanWorkflow::appendLog(const QString &text) { if (m_w.logTextEdit) m_w.logTextEdit->append(text); } // ========== Turntable ========== void ScanWorkflow::onTurntableToggled(bool enabled) { if (enabled) { m_scanning = true; m_cloudProcessing = true; setCaptureButtonsState(false, false, true, true); clearAccumulatedCloud(); startCapture(true); } else { stopTurntableMode(false); } } void ScanWorkflow::onTurntableTick() { autoSaveAccumulatedCloudToProject(); clearAccumulatedCloud(); startCapture(true); } void ScanWorkflow::setTurntableStatusText(const QString &text) { qDebug() << "[Turntable]" << text; } void ScanWorkflow::stopTurntableMode(bool uncheckCheckbox) { m_scanning = false; m_cloudProcessing = false; autoSaveAccumulatedCloudToProject(); stopCapture(); setCaptureButtonsState(true, true, false, false); resetScanQualityPanel(); }