/
klischa
/
AstraScanner2
Обзор
Документация
Войти
/
klischa
/
AstraScanner2
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/gui/MainWindow.cpp
1 673 строки
72 KB
k k
fix: viewer display, cloud counter, RGB preview, timer initialization
15 июл 2026, 12:58
15 июл 2026, 12:58
cb20a77
Код
Авторство
О чём код?
#include "MainWindow.h" #include "../calibration/CameraCalibrator.h" #include "HelpDialog.h" #include "SettingsDialog.h" #include "../filters/PointCloudFilters.h" #include "../settings/SettingsManager.h" #include "../export/ExportManager.h" #include "../project/ProjectManager.h" #include "../controllers/HomeTabBinder.h" #include "../controllers/StatusBarBinder.h" #include "../controllers/MenuBarBinder.h" #include "../controllers/ViewerCoordinator.h" #include "../controllers/ProjectUiController.h" #include "../controllers/ProjectTabBinder.h" #include "../controllers/ScanTabBinder.h" #include "../controllers/CalibrationTabBinder.h" #include "../controllers/LogTabBinder.h" #include "../controllers/ProcessingTabBinder.h" #include "../controllers/TurntableController.h" #include "../controllers/CaptureSessionController.h" #include "../controllers/ProcessingController.h" #include "../controllers/ScanWorkflow.h" #include "../controllers/ProcessingWorkflow.h" #include "../controllers/ProjectWorkflow.h" #include <QTabWidget> #include <QVBoxLayout> #include <QHBoxLayout> #include <QPushButton> #include <QLabel> #include <QProgressBar> #include <QStatusBar> #include <QStackedWidget> #include <QCoreApplication> #include <QElapsedTimer> #include <QTextEdit> #include <QScrollBar> #include <QComboBox> #include <QMessageBox> #include <QCheckBox> #include <QGroupBox> #include <QSpinBox> #include <QDoubleSpinBox> #include <QSlider> #include <QApplication> #include <QListWidget> #include <QMenu> #include <QMenuBar> #include <QAction> #include <QFileDialog> #include <QInputDialog> #include <QLineEdit> #include <QDateTime> #include <QDir> #include <QSignalBlocker> #include <QVTKOpenGLNativeWidget.h> #include <vtkGenericOpenGLRenderWindow.h> #include <pcl/visualization/pcl_visualizer.h> #include <pcl/filters/voxel_grid.h> #include <QDebug> #include <algorithm> #include <cmath> #include <cstdint> #include <filesystem> namespace { QString trackingModeToData(TrackingMode mode) { switch (mode) { case TrackingMode::MarkerBased: return QStringLiteral("marker_based"); case TrackingMode::NeuralBased: return QStringLiteral("neural_based"); case TrackingMode::ICP: default: return QStringLiteral("icp"); } } TrackingMode trackingModeFromData(const QString &modeStr) { if (modeStr == QLatin1String("marker_based")) { return TrackingMode::MarkerBased; } if (modeStr == QLatin1String("neural_based")) { return TrackingMode::NeuralBased; } return TrackingMode::ICP; } } std::atomic<MainWindow *> g_mainWindow{nullptr}; MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) , m_accumulatedCloud(new pcl::PointCloud<pcl::PointXYZRGB>) , m_surfelCoverageTracker(std::make_unique<SurfelCoverageTracker>()) { g_mainWindow.store(this, std::memory_order_release); std::error_code ec; std::filesystem::create_directories("data", ec); // Создаём дефолтную директорию проектов, если она ещё не существует. { const QString projDir = SettingsManager::instance().projectsDirectory(); QDir().mkpath(projDir); } m_processingController = new ProcessingController(this); m_filters = m_processingController->filters(); m_project = new ProjectManager(this); m_project->setTrackingMode(SettingsManager::instance().trackingMode()); m_exporter = new ExportManager(this); m_homeTabBinder = new HomeTabBinder(this); m_statusBarBinder = new StatusBarBinder(this); m_menuBarBinder = new MenuBarBinder(this); m_viewerCoordinator = new ViewerCoordinator(this); m_projectUiController = new ProjectUiController(this, m_project, m_exporter, this); m_projectTabBinder = new ProjectTabBinder(this); m_scanTabBinder = new ScanTabBinder(this); m_calibrationTabBinder = new CalibrationTabBinder(this); m_logTabBinder = new LogTabBinder(this); m_processingTabBinder = new ProcessingTabBinder(this); m_turntableController = new TurntableController(this); m_captureSessionController = new CaptureSessionController(this); m_captureSessionController->setProjectManager(m_project); connect(m_captureSessionController, &CaptureSessionController::scannerPoseForProject, this, [this](const Eigen::Affine3f& pose) { if (m_project) { m_project->setScannerPose(pose); } }); connect(m_captureSessionController, &CaptureSessionController::trackingModeChangedForProject, this, [this](TrackingMode mode) { if (m_project) { m_project->setTrackingMode(mode); } }); setupUI(); updateProjectStatusUi(); syncTrackingModeComboFromProject(); setupVisualizer(); // Create timers before passing to ScanWorkflow m_viewerUpdateTimer = new QTimer(this); m_scanTimeoutTimer = new QTimer(this); // Create ScanWorkflow and bind widgets m_scanWorkflow = new ScanWorkflow(m_captureSessionController, m_project, m_viewerCoordinator, this); m_scanWorkflow->bindWidgets({ m_previewBtn, m_scanBtn, m_pauseBtn, m_stopBtn, m_clearBtn, m_rgbLabel, m_depthLabel, m_scannerPoseLabel, m_trackingQualityLabel, m_alignmentQualityLabel, m_depthQualityLabel, m_driftQualityLabel, m_coverageSummaryLabel, m_timeLabel, m_frameCountLabel, m_fpsLabel, m_vtkWidget, m_logTextEdit, m_viewerUpdateTimer, m_scanTimeoutTimer, m_calibRgbLabel, m_homeCalibRgbLabel }); // Delegate scan signals to ScanWorkflow connect(m_captureSessionController, &CaptureSessionController::frameCaptured, m_scanWorkflow, &ScanWorkflow::onNewFrame); connect(m_captureSessionController, &CaptureSessionController::pointCloudReady, m_scanWorkflow, &ScanWorkflow::onPointCloudReady); connect(m_captureSessionController, &CaptureSessionController::error, m_scanWorkflow, &ScanWorkflow::onCaptureError); connect(m_captureSessionController, &CaptureSessionController::warning, m_scanWorkflow, &ScanWorkflow::onCaptureWarning); connect(m_captureSessionController, &CaptureSessionController::frameProcessed, m_scanWorkflow, &ScanWorkflow::onFrameProcessed); connect(m_captureSessionController, &CaptureSessionController::markersDetected, m_scanWorkflow, &ScanWorkflow::onMarkersDetected); connect(m_captureSessionController, &CaptureSessionController::scanQualityUpdated, m_scanWorkflow, &ScanWorkflow::updateScanQualityPanel); connect(m_scanWorkflow, &ScanWorkflow::cloudSizeChanged, this, &MainWindow::cloudSizeChanged); resetScanQualityPanel(); // Create ProcessingWorkflow m_processingWorkflow = new ProcessingWorkflow(m_processingController, m_project, m_viewerCoordinator, this); m_processingWorkflow->bindWidgets({ m_mergeBtn, m_addMergedBtn, m_meshStatusLabel, m_icpStatusLabel, m_poissonProgress, m_icpProgress, m_processingProgress, m_logTextEdit }); connect(m_processingWorkflow, &ProcessingWorkflow::cloudProcessed, this, [this](pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud) { if (cloud && !cloud->empty()) { QMutexLocker locker(&m_cloudMutex); *m_accumulatedCloud = *cloud; } updateViewer(); }); connect(m_processingWorkflow, &ProcessingWorkflow::cloudSizeChanged, this, &MainWindow::cloudSizeChanged); // Create ProjectWorkflow m_projectWorkflow = new ProjectWorkflow(this, m_project, m_exporter, this); m_projectWorkflow->setStatusBar(statusBar()); m_projectWorkflow->bindWidgets({ m_scansList, m_projectStatusLabel, m_homeProjectStatusLabel, m_trackingModeCombo }); connect(m_project, &ProjectManager::projectChanged, m_projectWorkflow, &ProjectWorkflow::updateProjectStatusUi); connect(m_project, &ProjectManager::scansChanged, m_projectWorkflow, &ProjectWorkflow::refreshScansList); // Timers are now managed by ScanWorkflow resize(1400, 900); m_calibrator = new CameraCalibrator(this); m_calibrator->setBoardSize(9, 6); m_calibrator->setSquareSize(25.0f); connect(m_calibrator, &CameraCalibrator::statusChanged, this, &MainWindow::onCalibrationStatus); connect(m_calibrator, &CameraCalibrator::frameAdded, this, [this](int count) { const QString text = QString("Кадров: %1 / 12").arg(count); if (m_calibFrameCountLabel) m_calibFrameCountLabel->setText(text); if (m_homeCalibFrameCountLabel) m_homeCalibFrameCountLabel->setText(text); const bool canCalibrate = count >= 5; if (m_calibCalibrateBtn) m_calibCalibrateBtn->setEnabled(canCalibrate); if (m_homeCalibCalibrateBtn) m_homeCalibCalibrateBtn->setEnabled(canCalibrate); }); if (m_calibrator->loadFromFile("data/camera_calibration.xml")) { const QString loadedText = QStringLiteral("Калибровка загружена"); if (m_calibStatusLabel) m_calibStatusLabel->setText(loadedText); if (m_homeCalibStatusLabel) m_homeCalibStatusLabel->setText(loadedText); } qInfo() << "Application started"; } MainWindow::~MainWindow() { // 1. Kill logger target first — fileMessageHandler must not invoke appendLog on dying object g_mainWindow.store(nullptr, std::memory_order_release); // 2. Stop all timers that fire into this object if (m_viewerUpdateTimer) m_viewerUpdateTimer->stop(); if (m_scanTimeoutTimer) m_scanTimeoutTimer->stop(); // 3. Disconnect all signal sources before tearing down if (m_captureSessionController) { disconnect(m_captureSessionController, nullptr, this, nullptr); } if (m_processingController) { disconnect(m_processingController, nullptr, this, nullptr); if (m_processingController->filters()) { disconnect(m_processingController->filters(), nullptr, this, nullptr); } m_processingController->cancelAll(); m_processingController->waitForOperations(); } // 4. Remove any MetaCall events already queued for this object QCoreApplication::removePostedEvents(this, QEvent::MetaCall); stopCapture(); if (m_viewerCoordinator) { m_viewerCoordinator->close(); } } void MainWindow::setFilterButtonsEnabled(bool enabled) { if (m_sorBtn) m_sorBtn->setEnabled(enabled); if (m_rorBtn) m_rorBtn->setEnabled(enabled); if (m_voxelBtn) m_voxelBtn->setEnabled(enabled); if (m_magicWandBtn) m_magicWandBtn->setEnabled(enabled); if (enabled) updateProcViewer(); } void MainWindow::setMeshFilterButtonsEnabled(bool enabled) { if (m_bgBtn) m_bgBtn->setEnabled(enabled); if (m_segBtn) m_segBtn->setEnabled(enabled); if (m_smoothBtn) m_smoothBtn->setEnabled(enabled); if (m_simplBtn) m_simplBtn->setEnabled(enabled); if (m_holeBtn) m_holeBtn->setEnabled(enabled); } void MainWindow::appendLog(const QString &text) { if (m_logTextEdit) { m_logTextEdit->append(text); QScrollBar *sb = m_logTextEdit->verticalScrollBar(); sb->setValue(sb->maximum()); } } // ========== Проект (делегирование в ProjectWorkflow) ========== void MainWindow::updateProjectStatusUi() { if (m_projectWorkflow) m_projectWorkflow->updateProjectStatusUi(); } void MainWindow::syncTrackingModeComboFromProject() { if (m_projectWorkflow) m_projectWorkflow->syncTrackingModeComboFromProject(); } void MainWindow::updateScanQualityPanel(const QString &trackingText, int trackingLevel, const QString &alignmentText, int alignmentLevel, const QString &depthText, int depthLevel, const QString &driftText, int driftLevel) { auto applyLevel = [](QLabel *label, const QString &text, int level) { if (!label) return; QString background = "#f4f6f9"; QString border = "#b8c0cb"; QString color = "#1f2733"; switch (level) { case 0: background = "#eef8ef"; border = "#78b48a"; color = "#1d5a33"; break; case 1: background = "#fff7e8"; border = "#d8aa57"; color = "#8b5d10"; break; case 2: default: background = "#fdeeee"; border = "#d27a7a"; color = "#8d2d2d"; break; } label->setText(text); label->setStyleSheet(QString( "padding: 6px 10px; border: 1px solid %1; border-radius: 6px; background: %2; color: %3; font-weight: 600;") .arg(border, background, color)); }; m_lastTrackingQualityLevel = trackingLevel; m_lastAlignmentQualityLevel = alignmentLevel; m_lastDepthQualityLevel = depthLevel; m_lastDriftQualityLevel = driftLevel; applyLevel(m_trackingQualityLabel, trackingText, trackingLevel); applyLevel(m_alignmentQualityLabel, alignmentText, alignmentLevel); applyLevel(m_depthQualityLabel, depthText, depthLevel); applyLevel(m_driftQualityLabel, driftText, driftLevel); } void MainWindow::updateCoverageSummary() { if (!m_coverageSummaryLabel) { return; } if (!m_surfelCoverageTracker) { m_coverageSummaryLabel->setText(QStringLiteral("Coverage: нет данных")); return; } const SurfelCoverageStats s = m_surfelCoverageTracker->stats(); if (s.surfelCount <= 0) { m_coverageSummaryLabel->setText(QStringLiteral("Coverage: нет данных")); return; } const int goodPct = static_cast<int>(std::lround(100.0 * s.goodSurfels / s.surfelCount)); const int warningPct = static_cast<int>(std::lround(100.0 * s.warningSurfels / s.surfelCount)); int poorPct = 100 - goodPct - warningPct; if (poorPct < 0) { poorPct = 0; } m_coverageSummaryLabel->setText( QString("Coverage: %1%% good / %2%% medium / %3%% weak") .arg(goodPct) .arg(warningPct) .arg(poorPct)); } void MainWindow::resetScanQualityPanel() { updateScanQualityPanel(QStringLiteral("Ожидание данных"), 1, QStringLiteral("Нет live-оценки"), 1, QStringLiteral("Нет оценки"), 1, QStringLiteral("Не оценён"), 1); updateCoverageSummary(); } bool MainWindow::confirmDiscardDirtyProject(const QString &title, const QString &question) { return m_projectUiController ? m_projectUiController->confirmDiscardDirtyProject(title, question) : true; } QString MainWindow::chooseProjectDirectory(const QString &title, bool existingOnly) { return m_projectUiController ? m_projectUiController->chooseProjectDirectory(title, existingOnly) : QString(); } bool MainWindow::ensureProjectOpenForSave() { return m_projectUiController ? m_projectUiController->ensureProjectOpenForSave([this]() { onSaveProjectAs(); }) : false; } pcl::PointCloud<pcl::PointXYZRGB>::Ptr MainWindow::currentCloudSnapshot() { QMutexLocker locker(&m_cloudMutex); if (!m_accumulatedCloud || m_accumulatedCloud->empty()) { return nullptr; } return pcl::make_shared<pcl::PointCloud<pcl::PointXYZRGB>>(*m_accumulatedCloud); } bool MainWindow::loadProjectScanIntoCurrentCloud(int index) { if (!m_project) return false; auto cloud = m_project->scanCloud(index); if (!cloud) { QMessageBox::warning(this, "Ошибка", m_project->lastError()); return false; } { QMutexLocker locker(&m_cloudMutex); *m_accumulatedCloud = *cloud; } resetConfidenceCoverage(); emit cloudSizeChanged(static_cast<int>(cloud->size())); updateViewer(); updateProcViewer(); return true; } QString MainWindow::chooseExportFilename(const QString &title, const QStringList &filters) { return m_projectUiController ? m_projectUiController->chooseExportFilename(title, filters) : QString(); } void MainWindow::rememberExportDirectory(const QString &filename) { if (m_projectUiController) { m_projectUiController->rememberExportDirectory(filename); } } bool MainWindow::tryExportPointCloud(const pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr &cloud, const QString &dialogTitle) { return m_projectUiController ? m_projectUiController->tryExportPointCloud(cloud, dialogTitle, statusBar()) : false; } void MainWindow::resetConfidenceCoverage() { if (m_surfelCoverageTracker) { m_surfelCoverageTracker->reset(); } updateCoverageSummary(); } constexpr double kPi = 3.14159265358979323846; void MainWindow::updateScannerPoseLabel(const Eigen::Affine3f &pose, bool showEuler) { if (!m_scannerPoseLabel) return; if (showEuler) { const Eigen::Vector3f euler = pose.rotation().eulerAngles(2, 1, 0); // yaw, pitch, roll const double roll = euler[2] * 180.0 / kPi; const double pitch = euler[1] * 180.0 / kPi; const double yaw = euler[0] * 180.0 / kPi; m_scannerPoseLabel->setText(QString("Поза: roll=%1 pitch=%2 yaw=%3") .arg(roll, 0, 'f', 1) .arg(pitch, 0, 'f', 1) .arg(yaw, 0, 'f', 1)); return; } m_scannerPoseLabel->setText(QString("Pose: tx=%1 ty=%2 tz=%3") .arg(pose.translation().x(), 0, 'f', 3) .arg(pose.translation().y(), 0, 'f', 3) .arg(pose.translation().z(), 0, 'f', 3)); } void MainWindow::updateScannerPoseLabelFromProject(bool showEuler) { if (!m_project) return; updateScannerPoseLabel(m_project->getScannerPose(), showEuler); } void MainWindow::setTurntableStatusText(const QString &text) { if (m_turntableStatusLabel) { m_turntableStatusLabel->setText(text); } } void MainWindow::stopTurntableMode(bool uncheckCheckbox) { if (m_turntableTimer) { m_turntableTimer->stop(); } if (uncheckCheckbox && m_turntableEnableChk) { m_turntableEnableChk->setChecked(false); } } void MainWindow::setupStatusBarWidgets() { // Status bar composition moved to StatusBarBinder; method kept for compatibility. } void MainWindow::applyProcessedCloudResult(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &filtered, bool usesProcessingProgress) { if (!filtered || filtered->empty()) { if (usesProcessingProgress && m_processingProgress) m_processingProgress->setValue(0); if (usesProcessingProgress) m_activeProgressBar = nullptr; setFilterButtonsEnabled(true); QMessageBox::warning(this, QStringLiteral("Обработка"), QStringLiteral("Операция вернула пустой результат.")); return; } { QMutexLocker locker(&m_cloudMutex); *m_accumulatedCloud = *filtered; } resetConfidenceCoverage(); emit cloudSizeChanged(static_cast<int>(filtered->size())); { QMutexLocker locker(&m_meshMutex); m_meshSaved = false; } updateViewer(); if (usesProcessingProgress && m_processingProgress) m_processingProgress->setValue(100); if (usesProcessingProgress) m_activeProgressBar = nullptr; setFilterButtonsEnabled(true); } void MainWindow::applyProcessedMeshResult(const pcl::PolygonMesh &mesh) { { QMutexLocker locker(&m_meshMutex); m_lastMesh = mesh; m_meshSaved = true; } if (m_viewerCoordinator) { m_viewerCoordinator->showMesh(mesh, QStringLiteral("processed_mesh")); } if (m_processingProgress) m_processingProgress->setValue(100); m_activeProgressBar = nullptr; setMeshFilterButtonsEnabled(true); } void MainWindow::handleCloudProcessingCanceled(const QString &logMessage, bool usesProcessingProgress) { if (m_logTextEdit && !logMessage.isEmpty()) appendLog(logMessage); if (usesProcessingProgress && m_processingProgress) m_processingProgress->setValue(0); if (usesProcessingProgress) m_activeProgressBar = nullptr; setFilterButtonsEnabled(true); } void MainWindow::handleMeshProcessingCanceled(const QString &logMessage) { if (m_logTextEdit && !logMessage.isEmpty()) appendLog(logMessage); if (m_processingProgress) m_processingProgress->setValue(0); m_activeProgressBar = nullptr; setMeshFilterButtonsEnabled(true); } void MainWindow::runCloudProcessingAction(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &snapshot, const ProcessingController::CloudOperation &operation, const QString &cancelMessage, bool usesProcessingProgress) { if (!snapshot) return; setFilterButtonsEnabled(false); if (usesProcessingProgress) { if (m_processingProgress) m_processingProgress->setValue(0); m_activeProgressBar = m_processingProgress; } m_processingController->runCloudOperation( this, operation, [this, usesProcessingProgress](pcl::PointCloud<pcl::PointXYZRGB>::Ptr filtered) { applyProcessedCloudResult(filtered, usesProcessingProgress); }, [this, cancelMessage, usesProcessingProgress]() { handleCloudProcessingCanceled(cancelMessage, usesProcessingProgress); }); } bool MainWindow::currentMeshSnapshot(pcl::PolygonMesh &meshCopy) { if (!m_meshSaved) { QMessageBox::warning(this, "Постобработка", "Сначала постройте меш через Poisson-реконструкцию"); setMeshFilterButtonsEnabled(true); return false; } { QMutexLocker locker(&m_meshMutex); meshCopy = m_lastMesh; } if (meshCopy.cloud.data.empty() || meshCopy.polygons.empty()) { QMessageBox::warning(this, "Постобработка", "Меш пустой - сначала постройте меш через Poisson-реконструкцию"); setMeshFilterButtonsEnabled(true); return false; } return true; } void MainWindow::runMeshProcessingAction(const pcl::PolygonMesh &meshCopy, const ProcessingController::MeshOperation &operation, const QString &cancelMessage) { setMeshFilterButtonsEnabled(false); if (m_processingProgress) m_processingProgress->setValue(0); m_activeProgressBar = m_processingProgress; m_processingController->runMeshOperation( this, operation, [this](const pcl::PolygonMesh &mesh) { applyProcessedMeshResult(mesh); }, [this, cancelMessage]() { handleMeshProcessingCanceled(cancelMessage); }); } void MainWindow::handlePoissonResult(const pcl::PolygonMesh &mesh) { for (QPushButton *btn : findChildren<QPushButton*>()) { if (btn->text() == "Построить меш") btn->setEnabled(true); } if (mesh.polygons.empty()) { statusBar()->showMessage("Poisson: реконструкция не удалась", 5000); if (m_meshStatusLabel) m_meshStatusLabel->setText("Меш не построен (см. лог)"); if (m_poissonProgress) m_poissonProgress->setValue(0); m_activeProgressBar = nullptr; QMessageBox::warning(this, "Реконструкция", "Poisson не смог построить меш. Возможные причины: " "слишком редкое облако, неверно ориентированные нормали, " "нехватка памяти. Подробности — на вкладке «Логи»."); return; } { QMutexLocker locker(&m_meshMutex); m_lastMesh = mesh; m_meshSaved = true; } const qulonglong nPoly = static_cast<qulonglong>(mesh.polygons.size()); if (m_meshStatusLabel) m_meshStatusLabel->setText(QString("Меш построен: %1 полигонов").arg(nPoly)); statusBar()->showMessage(QString("Poisson: готов меш из %1 полигонов").arg(nPoly), 5000); if (m_poissonProgress) m_poissonProgress->setValue(100); m_activeProgressBar = nullptr; if (m_viewerCoordinator) { m_viewerCoordinator->showMesh(m_lastMesh, QStringLiteral("poisson_mesh")); } } void MainWindow::handlePoissonCanceled() { for (QPushButton *btn : findChildren<QPushButton*>()) { if (btn->text() == "Построить меш") btn->setEnabled(true); } if (m_meshStatusLabel) m_meshStatusLabel->setText("Poisson: отменено пользователем"); if (m_poissonProgress) m_poissonProgress->setValue(0); m_activeProgressBar = nullptr; statusBar()->showMessage("Poisson: отменено", 3000); } void MainWindow::handleMergeResult(const pcl::PointCloud<pcl::PointXYZRGB>::Ptr &merged) { if (m_mergeBtn) m_mergeBtn->setEnabled(true); if (!merged || merged->empty()) { if (m_icpStatusLabel) m_icpStatusLabel->setText("Объединение не удалось (см. лог)"); statusBar()->showMessage("ICP: пустой результат", 5000); if (m_icpProgress) m_icpProgress->setValue(0); m_activeProgressBar = nullptr; QMessageBox::warning(this, "Объединение", "ICP вернул пустое облако. Возможно, все сканы были пустыми или не сошлись."); return; } m_lastMerged = merged; resetConfidenceCoverage(); const int npts = static_cast<int>(merged->size()); if (m_icpStatusLabel) m_icpStatusLabel->setText(QString("Готово: %1 точек в объединённом облаке").arg(npts)); statusBar()->showMessage(QString("ICP: объединено в %1 точек").arg(npts), 5000); if (m_icpProgress) m_icpProgress->setValue(100); m_activeProgressBar = nullptr; if (m_addMergedBtn) m_addMergedBtn->setEnabled(true); { QMutexLocker locker(&m_cloudMutex); if (!m_accumulatedCloud) { m_accumulatedCloud.reset(new pcl::PointCloud<pcl::PointXYZRGB>); } *m_accumulatedCloud = *merged; } emit cloudSizeChanged(npts); if (m_viewerCoordinator) { m_viewerCoordinator->showAccumulatedCloud(m_accumulatedCloud, true); } else { updateViewer(); } } void MainWindow::handleMergeCanceled() { if (m_icpStatusLabel) m_icpStatusLabel->setText("Объединение отменено пользователем"); if (m_icpProgress) m_icpProgress->setValue(0); m_activeProgressBar = nullptr; statusBar()->showMessage("ICP: отменено", 3000); if (m_mergeBtn) m_mergeBtn->setEnabled(true); if (m_addMergedBtn) m_addMergedBtn->setEnabled(false); } void MainWindow::setupUI() { QWidget* centralWidget = new QWidget(this); setCentralWidget(centralWidget); QVBoxLayout* mainLayout = new QVBoxLayout(centralWidget); // --- Меню «Файл» / «Экспорт» --- m_menuBarBinder->bind( menuBar(), this, [this]() { onNewProject(); }, [this]() { onOpenProject(); }, [this]() { onSaveProject(); }, [this]() { onSaveProjectAs(); }, [this]() { onExportCurrentCloud(); }, [this]() { onExportMesh(); }, [this]() { onShowSettingsDialog(); }, [this]() { onShowHelpDialog(); }); m_tabWidget = new QTabWidget(this); mainLayout->addWidget(m_tabWidget); // Вкладка "Главная" { HomeTabBinder::Widgets homeWidgets = m_homeTabBinder->bind(m_tabWidget, this); m_homeProjectStatusLabel = homeWidgets.projectStatusLabel; m_homeCalibStatusLabel = homeWidgets.homeCalibStatusLabel; m_homeCalibFrameCountLabel = homeWidgets.homeCalibFrameCountLabel; m_homeCalibCalibrateBtn = homeWidgets.homeCalibCalibrateBtn; m_homeCalibRgbLabel = homeWidgets.homeCalibRgbLabel; connect(homeWidgets.newProjectBtn, &QPushButton::clicked, this, &MainWindow::onNewProject); connect(homeWidgets.openProjectBtn, &QPushButton::clicked, this, &MainWindow::onOpenProject); connect(homeWidgets.saveProjectBtn, &QPushButton::clicked, this, &MainWindow::onSaveProject); connect(homeWidgets.saveProjectAsBtn, &QPushButton::clicked, this, &MainWindow::onSaveProjectAs); connect(homeWidgets.showHelpBtn, &QPushButton::clicked, this, &MainWindow::onShowHelpDialog); connect(homeWidgets.goToScanBtn, &QPushButton::clicked, this, [this]() { if (m_tabWidget && m_tabWidget->count() > 1) { m_tabWidget->setCurrentIndex(1); } }); connect(homeWidgets.homeCalibPreviewBtn, &QPushButton::clicked, this, &MainWindow::onCalibPreviewClicked); connect(homeWidgets.homeCalibCaptureBtn, &QPushButton::clicked, this, &MainWindow::onCalibCaptureClicked); connect(homeWidgets.homeCalibCalibrateBtn, &QPushButton::clicked, this, &MainWindow::onCalibCalibrateClicked); connect(homeWidgets.homeCalibResetBtn, &QPushButton::clicked, this, &MainWindow::onCalibResetClicked); connect(homeWidgets.applyColormapBtn, &QPushButton::clicked, this, [this, combo = homeWidgets.colormapCombo]() { m_depthColormap = combo->currentData().toInt(); statusBar()->showMessage("Цветовая карта применена", 2000); }); } // Вкладка "Сканирование" { ScanTabBinder::Widgets scanWidgets = m_scanTabBinder->bind(m_tabWidget, this); m_trackingModeCombo = scanWidgets.trackingModeCombo; m_previewBtn = scanWidgets.previewBtn; m_scanBtn = scanWidgets.scanBtn; m_pauseBtn = scanWidgets.pauseBtn; m_stopBtn = scanWidgets.stopBtn; m_clearBtn = scanWidgets.clearBtn; m_rgbLabel = scanWidgets.rgbLabel; m_depthLabel = scanWidgets.depthLabel; m_distanceIndicator = scanWidgets.distanceIndicator; m_scannerPoseLabel = scanWidgets.scannerPoseLabel; m_trackingQualityLabel = scanWidgets.trackingQualityLabel; m_alignmentQualityLabel = scanWidgets.alignmentQualityLabel; m_depthQualityLabel = scanWidgets.depthQualityLabel; m_driftQualityLabel = scanWidgets.driftQualityLabel; m_coverageSummaryLabel = scanWidgets.coverageSummaryLabel; m_vtkWidget = scanWidgets.vtkWidget; m_turntableEnableChk = scanWidgets.turntableEnableChk; m_turntableIntervalSpin = scanWidgets.turntableIntervalSpin; m_turntableCountSpin = scanWidgets.turntableCountSpin; m_turntableModeCombo = scanWidgets.turntableModeCombo; m_turntableStatusLabel = scanWidgets.turntableStatusLabel; if (scanWidgets.viewerStack) { scanWidgets.viewerStack->setCurrentIndex(0); } if (scanWidgets.show3dBtn && scanWidgets.viewerStack) { connect(scanWidgets.show3dBtn, &QPushButton::clicked, this, [this, stack = scanWidgets.viewerStack]() { m_scanViewColorMode = ScanViewColorMode::Rgb; stack->setCurrentIndex(0); updateViewer(); if (m_vtkWidget && m_vtkWidget->renderWindow()) { m_vtkWidget->renderWindow()->Render(); } }); } if (scanWidgets.showQualityBtn && scanWidgets.viewerStack) { connect(scanWidgets.showQualityBtn, &QPushButton::clicked, this, [this, stack = scanWidgets.viewerStack]() { m_scanViewColorMode = ScanViewColorMode::Quality; stack->setCurrentIndex(0); updateViewer(); if (m_vtkWidget && m_vtkWidget->renderWindow()) { m_vtkWidget->renderWindow()->Render(); } }); } if (scanWidgets.showRgbBtn && scanWidgets.viewerStack) { connect(scanWidgets.showRgbBtn, &QPushButton::clicked, this, [stack = scanWidgets.viewerStack]() { stack->setCurrentIndex(1); }); } if (scanWidgets.showDepthBtn && scanWidgets.viewerStack) { connect(scanWidgets.showDepthBtn, &QPushButton::clicked, this, [stack = scanWidgets.viewerStack]() { stack->setCurrentIndex(2); }); } if (scanWidgets.trackingHelpBtn) { connect(scanWidgets.trackingHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("tracking-modes")); }); } if (scanWidgets.turntableHelpBtn) { connect(scanWidgets.turntableHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("turntable-mode")); }); } if (scanWidgets.coverageHelpBtn) { connect(scanWidgets.coverageHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("quality-mode")); }); } } // Инициализируем комбобокс из сохранённых настроек. { const QString modeStr = trackingModeToData(SettingsManager::instance().trackingMode()); const int idx = m_trackingModeCombo->findData(modeStr); if (idx >= 0) { m_trackingModeCombo->setCurrentIndex(idx); } } // Обновляем режим трекинга при изменении в комбобоксе connect(m_trackingModeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() { const TrackingMode mode = trackingModeFromData(m_trackingModeCombo->currentData().toString()); SettingsManager::instance().setTrackingMode(mode); if (m_project) { m_project->setTrackingMode(mode); } if (m_captureSessionController) { m_captureSessionController->setProjectManager(m_project); } }); m_turntableTimer = new QTimer(this); connect(m_turntableTimer, &QTimer::timeout, this, &MainWindow::onTurntableTick); connect(m_turntableEnableChk, &QCheckBox::toggled, this, &MainWindow::onTurntableToggled); // Вкладка "Калибровка" { CalibrationTabBinder::Widgets calibrationWidgets = m_calibrationTabBinder->bind(m_tabWidget, this); m_calibPreviewBtn = calibrationWidgets.calibPreviewBtn; m_calibCaptureBtn = calibrationWidgets.calibCaptureBtn; m_calibCalibrateBtn = calibrationWidgets.calibCalibrateBtn; m_calibResetBtn = calibrationWidgets.calibResetBtn; m_calibRgbLabel = calibrationWidgets.calibRgbLabel; m_calibFrameCountLabel = calibrationWidgets.calibFrameCountLabel; m_calibStatusLabel = calibrationWidgets.calibStatusLabel; } connect(m_calibPreviewBtn, &QPushButton::clicked, this, &MainWindow::onCalibPreviewClicked); connect(m_calibCaptureBtn, &QPushButton::clicked, this, &MainWindow::onCalibCaptureClicked); connect(m_calibCalibrateBtn, &QPushButton::clicked, this, &MainWindow::onCalibCalibrateClicked); connect(m_calibResetBtn, &QPushButton::clicked, this, &MainWindow::onCalibResetClicked); // Вкладка "Обработка" ProcessingTabBinder::Widgets processingWidgets = m_processingTabBinder->bind(m_tabWidget, this); m_procVtkWidget = processingWidgets.procVtkWidget; m_sorBtn = processingWidgets.sorBtn; m_rorBtn = processingWidgets.rorBtn; m_voxelBtn = processingWidgets.voxelBtn; m_magicWandBtn = processingWidgets.magicWandBtn; m_bgBtn = processingWidgets.bgBtn; m_segBtn = processingWidgets.segBtn; m_smoothBtn = processingWidgets.smoothBtn; m_simplBtn = processingWidgets.simplBtn; m_holeBtn = processingWidgets.holeBtn; m_mergeBtn = processingWidgets.mergeScansBtn; m_addMergedBtn = processingWidgets.addMergedBtn; m_icpProgress = processingWidgets.icpProgress; m_icpStatusLabel = processingWidgets.icpStatusLabel; m_poissonProgress = processingWidgets.poissonProgress; m_meshStatusLabel = processingWidgets.meshStatusLabel; m_processingProgress = processingWidgets.processingProgress; m_poissonFlipNormalsChk = processingWidgets.poissonFlipNormalsChk; m_poissonConsistentOrientChk = processingWidgets.poissonConsistentOrientChk; m_poissonCustomVpChk = processingWidgets.poissonCustomVpChk; m_poissonVpX = processingWidgets.poissonVpX; m_poissonVpY = processingWidgets.poissonVpY; m_poissonVpZ = processingWidgets.poissonVpZ; QSpinBox *sorMeanKSpin = processingWidgets.sorMeanKSpin; QDoubleSpinBox *sorThreshSpin = processingWidgets.sorThreshSpin; QPushButton *sorBtn = processingWidgets.sorBtn; QDoubleSpinBox *rorRadiusSpin = processingWidgets.rorRadiusSpin; QSpinBox *rorNeighborsSpin = processingWidgets.rorNeighborsSpin; QPushButton *rorBtn = processingWidgets.rorBtn; QDoubleSpinBox *voxelSizeSpin = processingWidgets.voxelSizeSpin; QPushButton *voxelBtn = processingWidgets.voxelBtn; QPushButton *magicWandBtn = processingWidgets.magicWandBtn; QDoubleSpinBox *bgDistSpin = processingWidgets.bgDistSpin; QSpinBox *bgIterSpin = processingWidgets.bgIterSpin; QPushButton *bgBtn = processingWidgets.bgBtn; QDoubleSpinBox *segTolSpin = processingWidgets.segTolSpin; QSpinBox *segMinSpin = processingWidgets.segMinSpin; QSpinBox *segMaxSpin = processingWidgets.segMaxSpin; QPushButton *segBtn = processingWidgets.segBtn; QSpinBox *smoothIterSpin = processingWidgets.smoothIterSpin; QDoubleSpinBox *smoothConvSpin = processingWidgets.smoothConvSpin; QPushButton *smoothBtn = processingWidgets.smoothBtn; QSlider *simplSlider = processingWidgets.simplSlider; QLabel *simplValueLabel = processingWidgets.simplValueLabel; QPushButton *simplBtn = processingWidgets.simplBtn; QDoubleSpinBox *holeSizeSpin = processingWidgets.holeSizeSpin; QPushButton *holeBtn = processingWidgets.holeBtn; QDoubleSpinBox *icpMaxCorrSpin = processingWidgets.icpMaxCorrSpin; QSpinBox *icpIterSpin = processingWidgets.icpIterSpin; QDoubleSpinBox *icpVoxelSpin = processingWidgets.icpVoxelSpin; QCheckBox *icpSkipCheck = processingWidgets.icpSkipCheck; QPushButton *mergeScansBtn = processingWidgets.mergeScansBtn; QPushButton *addMergedBtn = processingWidgets.addMergedBtn; QSpinBox *depthSpin = processingWidgets.depthSpin; QDoubleSpinBox *pointWeightSpin = processingWidgets.pointWeightSpin; QDoubleSpinBox *samplesSpin = processingWidgets.samplesSpin; QDoubleSpinBox *normalRadiusSpin = processingWidgets.normalRadiusSpin; QSpinBox *kNearestSpin = processingWidgets.kNearestSpin; QPushButton *reconstructBtn = processingWidgets.reconstructBtn; QPushButton *reconstructLightweightBtn = processingWidgets.reconstructLightweightBtn; QPushButton *showCloudBtn = processingWidgets.showCloudBtn; QPushButton *exportMeshBtnP = processingWidgets.exportMeshBtn; if (processingWidgets.cleanupHelpBtn) { connect(processingWidgets.cleanupHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("cloud-cleanup")); }); } if (processingWidgets.objectPrepHelpBtn) { connect(processingWidgets.objectPrepHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("object-prep")); }); } if (processingWidgets.icpHelpBtn) { connect(processingWidgets.icpHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("icp-merge")); }); } if (processingWidgets.poissonHelpBtn) { connect(processingWidgets.poissonHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("poisson")); }); } if (processingWidgets.meshPostHelpBtn) { connect(processingWidgets.meshPostHelpBtn, &QPushButton::clicked, this, [this]() { showHelpSection(QStringLiteral("mesh-post")); }); } connect(m_poissonCustomVpChk, &QCheckBox::toggled, this, [this](bool on) { m_poissonVpX->setEnabled(on); m_poissonVpY->setEnabled(on); m_poissonVpZ->setEnabled(on); }); // progressUpdated используется и для Poisson, и для ICP. Направляем // обновления только в тот индикатор, который сейчас актуален. connect(m_processingController, &ProcessingController::progressUpdated, this, [this](int pct) { if (m_activeProgressBar) m_activeProgressBar->setValue(pct); }); connect(reconstructBtn, &QPushButton::clicked, this, [this, depthSpin, pointWeightSpin, samplesSpin, normalRadiusSpin, kNearestSpin, reconstructBtn]() { PointCloudFilters::PoissonParams p; p.depth = depthSpin->value(); p.pointWeight = static_cast<float>(pointWeightSpin->value()); p.samplesPerNode = static_cast<float>(samplesSpin->value()); p.normalSearchRadius = normalRadiusSpin->value(); p.kNearest = kNearestSpin->value(); // Ручная переориентация нормалей, если пользователь включил. p.flipNormals = m_poissonFlipNormalsChk && m_poissonFlipNormalsChk->isChecked(); p.consistentOrientation = m_poissonConsistentOrientChk && m_poissonConsistentOrientChk->isChecked(); if (m_poissonCustomVpChk && m_poissonCustomVpChk->isChecked()) { p.useCustomViewpoint = true; p.viewpointX = static_cast<float>(m_poissonVpX->value()); p.viewpointY = static_cast<float>(m_poissonVpY->value()); p.viewpointZ = static_cast<float>(m_poissonVpZ->value()); } SettingsManager &s = SettingsManager::instance(); s.setPoissonDepth(p.depth); s.setPoissonPointWeight(p.pointWeight); s.setPoissonSamplesPerNode(p.samplesPerNode); s.setPoissonNormalRadius(p.normalSearchRadius); s.setPoissonKNearest(p.kNearest); reconstructBtn->setEnabled(false); onReconstructMeshClicked(p); }); connect(reconstructLightweightBtn, &QPushButton::clicked, this, &MainWindow::onReconstructLightweightClicked); connect(showCloudBtn, &QPushButton::clicked, this, &MainWindow::onShowCloudClicked); // connect(reconstructLightweightBtn, &QPushButton::clicked, this, &MainWindow::onReconstructLightweightClicked); // Заменено на общий обработчик "в разработке" connect(exportMeshBtnP, &QPushButton::clicked, this, &MainWindow::onExportMesh); connect(sorBtn, &QPushButton::clicked, this, [this, sorMeanKSpin, sorThreshSpin]() { const auto snapshot = currentCloudSnapshot(); const int meanK = sorMeanKSpin->value(); const double thresh = sorThreshSpin->value(); runCloudProcessingAction( snapshot, [snapshot, meanK, thresh](PointCloudFilters *filters) { return filters->applyStatisticalOutlierRemoval(snapshot, meanK, thresh); }, "[SOR] Операция отменена пользователем", false); }); connect(rorBtn, &QPushButton::clicked, this, [this, rorRadiusSpin, rorNeighborsSpin]() { const auto snapshot = currentCloudSnapshot(); const double radius = rorRadiusSpin->value(); const int neighbors = rorNeighborsSpin->value(); runCloudProcessingAction( snapshot, [snapshot, radius, neighbors](PointCloudFilters *filters) { return filters->applyRadiusOutlierRemoval(snapshot, radius, neighbors); }, "[ROR] Операция отменена пользователем", false); }); connect(voxelBtn, &QPushButton::clicked, this, [this, voxelSizeSpin]() { const auto snapshot = currentCloudSnapshot(); const double leafSize = voxelSizeSpin->value(); runCloudProcessingAction( snapshot, [snapshot, leafSize](PointCloudFilters *filters) { return filters->applyVoxelGrid(snapshot, leafSize); }, "[Voxel] Операция отменена пользователем", false); }); connect(magicWandBtn, &QPushButton::clicked, this, [this]() { const auto snapshot = currentCloudSnapshot(); runCloudProcessingAction( snapshot, [snapshot](PointCloudFilters *filters) { return filters->applyMagicWand(snapshot); }, "[Magic Wand] Операция отменена пользователем", false); }); // === Обработчики постобработки (PR #1) === // Удаление фона connect(bgBtn, &QPushButton::clicked, this, [this, bgDistSpin, bgIterSpin]() { const auto snapshot = currentCloudSnapshot(); const double distThresh = bgDistSpin->value(); const int maxIter = bgIterSpin->value(); runCloudProcessingAction( snapshot, [snapshot, distThresh, maxIter](PointCloudFilters *filters) { return filters->removeBackgroundPlane(snapshot, static_cast<float>(distThresh), maxIter); }, "[Remove Background] Операция отменена пользователем", true); }); // Сегментация Region Growing connect(segBtn, &QPushButton::clicked, this, [this, segTolSpin, segMinSpin, segMaxSpin]() { const auto snapshot = currentCloudSnapshot(); const float clusterTol = segTolSpin->value(); const int minSize = segMinSpin->value(); const int maxSize = segMaxSpin->value(); runCloudProcessingAction( snapshot, [snapshot, clusterTol, minSize, maxSize](PointCloudFilters *filters) { return filters->applyRegionGrowingSegmentation(snapshot, clusterTol, minSize, maxSize); }, "[Region Growing] Операция отменена пользователем", true); }); // Сглаживание меша (работает с текущим мешом, если есть) connect(smoothBtn, &QPushButton::clicked, this, [this, smoothIterSpin, smoothConvSpin]() { pcl::PolygonMesh meshCopy; if (!currentMeshSnapshot(meshCopy)) return; const int numIter = smoothIterSpin->value(); const float convergence = smoothConvSpin->value(); runMeshProcessingAction( meshCopy, [meshCopy, numIter, convergence](PointCloudFilters *filters) { return filters->applyMeshSmoothing(meshCopy, numIter, convergence); }, "[Mesh Smoothing] Операция отменена пользователем"); }); // Сохраняем настройки PR #1 при использовании connect(smoothIterSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int value) { SettingsManager::instance().setSmoothingNumIterations(value); }); connect(smoothConvSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [this](double value) { SettingsManager::instance().setSmoothingConvergence(static_cast<float>(value)); }); // === PR #2: Упрощение меша === connect(simplSlider, &QSlider::valueChanged, this, [simplValueLabel](int value) { simplValueLabel->setText(QString::number(value) + "%"); }); connect(simplBtn, &QPushButton::clicked, this, [this, simplSlider]() { pcl::PolygonMesh meshCopy; if (!currentMeshSnapshot(meshCopy)) return; float targetReduction = simplSlider->value() / 100.0f; runMeshProcessingAction( meshCopy, [meshCopy, targetReduction](PointCloudFilters *filters) { return filters->simplifyMesh(meshCopy, targetReduction); }, "[Mesh Simplification] Операция отменена пользователем"); }); // === PR #2: Закрытие дыр === connect(holeBtn, &QPushButton::clicked, this, [this, holeSizeSpin]() { pcl::PolygonMesh meshCopy; if (!currentMeshSnapshot(meshCopy)) return; float maxHoleSize = holeSizeSpin->value(); runMeshProcessingAction( meshCopy, [meshCopy, maxHoleSize](PointCloudFilters *filters) { return filters->fillMeshHoles(meshCopy, maxHoleSize); }, "[Mesh Hole Filling] Операция отменена пользователем"); }); // Сохраняем настройки PR #2 при изменении connect(holeSizeSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, [this](double value) { SettingsManager::instance().setMeshHoleFillingMaxHoleSize(static_cast<float>(value)); }); // === AI Сегментация NPMFF-Net === // Обработчик уже заменен на QMessageBox в connect из aiSegmentGroup // connect(segmentAIBtn, &QPushButton::clicked, ...) был удален и заменен на общий обработчик "в разработке" // connect(filterByIndicesBtn, &QPushButton::clicked, this, [this]() { // QMessageBox::information(this, "В разработке", "Функция временно недоступна"); // }); // === Enhance SuperPC === // Обработчик уже заменен на QMessageBox в connect из aiSegmentGroup // connect(enhanceSuperPCBtn, &QPushButton::clicked, ...) был удален и заменен на общий обработчик "в разработке" // === Full AI Pipeline === // Обработчик уже заменен на QMessageBox в connect из aiSegmentGroup // connect(fullPipelineBtn, &QPushButton::clicked, ...) был удален и заменен на общий обработчик "в разработке" connect(mergeScansBtn, &QPushButton::clicked, this, [this, icpMaxCorrSpin, icpIterSpin, icpVoxelSpin, icpSkipCheck]() { PointCloudFilters::MergeParams p; p.maxCorrespondenceDistance = icpMaxCorrSpin->value(); p.maximumIterations = icpIterSpin->value(); p.voxelLeafOut = icpVoxelSpin->value(); p.skipNonConverged = icpSkipCheck->isChecked(); SettingsManager &s = SettingsManager::instance(); s.setIcpMaxCorrespondenceDistance(p.maxCorrespondenceDistance); s.setIcpMaxIterations(p.maximumIterations); s.setIcpVoxelLeafOut(p.voxelLeafOut); s.setIcpSkipNonConverged(p.skipNonConverged); onMergeScansClicked(p); }); // === AI Registration (BUFFER-X / DINO) === // Обработчик уже заменен на QMessageBox в connect из aiRegGroup // connect(bufferXAlignBtn, &QPushButton::clicked, ...) был удален и заменен на общий обработчик "в разработке" connect(addMergedBtn, &QPushButton::clicked, this, &MainWindow::onSaveMergedToProject); m_icpProgress = processingWidgets.icpProgress; connect(m_processingController, &ProcessingController::filterCompleted, this, [this](const QString &filter, int before, int after) { statusBar()->showMessage(QString("%1: %2 -> %3 точек").arg(filter).arg(before).arg(after), 3000); }); // Вкладка "Проект" — список сохранённых сканов + кнопки управления. { const ProjectTabBinder::Widgets projectTabWidgets = m_projectTabBinder->bind( m_tabWidget, this, [this]() { onAddCurrentCloudToProject(); }, [this]() { onExportCurrentCloud(); }, [this]() { onExportMesh(); }, [this](const QPoint &pos) { onScansListContextMenu(pos); }, [this](QListWidgetItem *item) { onScansListDoubleClicked(item); }); m_projectStatusLabel = projectTabWidgets.projectStatusLabel; m_scansList = projectTabWidgets.scansList; } // Вкладка "Логи" — fileMessageHandler форвардит сюда все сообщения Qt. { LogTabBinder::Widgets logWidgets = m_logTabBinder->bind(m_tabWidget, this); m_logTextEdit = logWidgets.logTextEdit; connect(logWidgets.clearLogBtn, &QPushButton::clicked, m_logTextEdit, &QTextEdit::clear); } // Статусная строка { StatusBarBinder::Widgets statusWidgets = m_statusBarBinder->bind(statusBar(), this); m_fpsLabel = statusWidgets.fpsLabel; m_frameCountLabel = statusWidgets.frameCountLabel; m_timeLabel = statusWidgets.timeLabel; connect(this, &MainWindow::cloudSizeChanged, statusWidgets.cloudSizeLabel, [label = statusWidgets.cloudSizeLabel](int size) { label->setText(QString("Cloud: %1").arg(size)); }); } connect(m_previewBtn, &QPushButton::clicked, this, &MainWindow::onPreviewClicked); connect(m_scanBtn, &QPushButton::clicked, this, &MainWindow::onScanClicked); connect(m_pauseBtn, &QPushButton::clicked, this, &MainWindow::onPauseClicked); connect(m_stopBtn, &QPushButton::clicked, this, &MainWindow::onStopClicked); connect(m_clearBtn, &QPushButton::clicked, this, &MainWindow::onClearClicked); } void MainWindow::setupVisualizer() { m_viewerCoordinator->setupMainViewer(m_vtkWidget); m_viewerCoordinator->setupProcessingViewer(m_procVtkWidget); qDebug() << "Visualizer setup complete"; } void MainWindow::updateProcViewer() { if (!m_viewerCoordinator) return; QMutexLocker locker(&m_cloudMutex); m_viewerCoordinator->updateProcessingCloud(m_accumulatedCloud); } // ========== Основные кнопки (делегирование в ScanWorkflow) ========== void MainWindow::onPreviewClicked() { if (m_scanWorkflow) m_scanWorkflow->onPreviewClicked(); } void MainWindow::onScanClicked() { if (m_scanWorkflow) m_scanWorkflow->onScanClicked(); } void MainWindow::checkScanTimeout() { if (m_scanWorkflow) m_scanWorkflow->checkScanTimeout(); } void MainWindow::onPauseClicked() { if (m_scanWorkflow) m_scanWorkflow->onPauseClicked(); } void MainWindow::onStopClicked() { if (m_scanWorkflow) m_scanWorkflow->onStopClicked(); } void MainWindow::onClearClicked() { if (m_scanWorkflow) m_scanWorkflow->onClearClicked(); } // ========== Управление потоком ========== void MainWindow::startCapture(bool enableCloudProcessing) { if (m_captureSessionController->isRunning() && !stopCapture()) return; const SettingsManager &settings = SettingsManager::instance(); const bool neuralTrackingEnabled = (m_project && m_project->getTrackingMode() == TrackingMode::NeuralBased); if (neuralTrackingEnabled) { statusBar()->showMessage("Загрузка модели нейросети..."); QApplication::processEvents(); } m_captureSessionController->setProjectManager(m_project); m_captureSessionController->start(enableCloudProcessing, static_cast<float>(settings.depthMin()), static_cast<float>(settings.depthMax()), settings.colorCameraEnabled(), neuralTrackingEnabled, settings.useCudaForNeural()); if (neuralTrackingEnabled) { statusBar()->showMessage(QString(), 0); } qDebug() << "Capture started, cloud:" << enableCloudProcessing; } bool MainWindow::stopCapture() { if (!m_captureSessionController) { return true; } if (!m_captureSessionController->stop()) { qCritical() << "Capture thread did not stop within timeout"; statusBar()->showMessage("Ошибка: поток захвата не остановился", 5000); return false; } qDebug() << "Capture stopped"; return true; } // ========== Обработка кадров (делегирование в ScanWorkflow) ========== void MainWindow::onNewFrame(QSharedPointer<cv::Mat>, QSharedPointer<cv::Mat>) {} void MainWindow::onMarkersDetected(QSharedPointer<cv::Mat>) {} void MainWindow::onFrameProcessed(int) {} void MainWindow::onCaptureWarning(const QString &) {} void MainWindow::onPointCloudReady(pcl::PointCloud<pcl::PointXYZRGB>::Ptr) {} void MainWindow::updateViewer() { if (m_scanWorkflow) m_scanWorkflow->updateViewer(); } void MainWindow::onCaptureError(const QString &) {} // ========== Калибровка ========== void MainWindow::onCalibPreviewClicked() { onPreviewClicked(); } void MainWindow::onCalibCaptureClicked() { if (m_lastColorFrame.empty()) { m_calibrator->notifyStatus("Нет доступного RGB кадра. Запустите Preview."); return; } m_calibrator->addFrame(m_lastColorFrame); } void MainWindow::onCalibCalibrateClicked() { if (m_calibrator->calibrate()) { const double rms = m_calibrator->reprojectionError(); if (rms > 2.0) { qWarning() << "Calibration RMS is high:" << rms << "px. Result may be inaccurate."; m_calibrator->notifyStatus( QString("Калибровка выполнена с большой ошибкой (RMS=%1). " "Рекомендуется повторить.").arg(rms, 0, 'f', 3)); } m_calibrator->saveToFile("data/camera_calibration.xml"); qInfo() << "Calibration saved, RMS:" << rms; } } void MainWindow::onCalibResetClicked() { m_calibrator->reset(); if (m_calibFrameCountLabel) m_calibFrameCountLabel->setText("Кадров: 0 / 12"); if (m_homeCalibFrameCountLabel) m_homeCalibFrameCountLabel->setText("Кадров: 0 / 12"); if (m_calibCalibrateBtn) m_calibCalibrateBtn->setEnabled(false); if (m_homeCalibCalibrateBtn) m_homeCalibCalibrateBtn->setEnabled(false); } void MainWindow::onCalibrationStatus(const QString &msg) { if (m_calibStatusLabel) m_calibStatusLabel->setText(msg); if (m_homeCalibStatusLabel) m_homeCalibStatusLabel->setText(msg); } // ========== Проект ========== // ========== Проект (делегирование в ProjectWorkflow) ========== void MainWindow::onNewProject() { if (m_projectWorkflow) m_projectWorkflow->onNewProject(); } void MainWindow::onOpenProject() { if (m_projectWorkflow) m_projectWorkflow->onOpenProject(); } void MainWindow::onSaveProject() { if (m_projectWorkflow) m_projectWorkflow->onSaveProject(); } void MainWindow::onSaveProjectAs() { if (m_projectWorkflow) m_projectWorkflow->onSaveProjectAs(); } void MainWindow::onAddCurrentCloudToProject() { if (m_projectWorkflow) m_projectWorkflow->onAddCurrentCloudToProject(); } void MainWindow::onExportCurrentCloud() { if (m_projectWorkflow) m_projectWorkflow->onExportCurrentCloud(); } void MainWindow::onExportMesh() { if (m_projectWorkflow) m_projectWorkflow->onExportMesh(); } void MainWindow::onScansListContextMenu(const QPoint &pos) { if (m_projectWorkflow) m_projectWorkflow->onScansListContextMenu(pos); } void MainWindow::onScansListDoubleClicked(QListWidgetItem *item) { if (m_projectWorkflow && item) m_projectWorkflow->onScansListDoubleClicked(m_scansList->row(item)); } void MainWindow::refreshScansList() { if (m_projectWorkflow) m_projectWorkflow->refreshScansList(); } // ========== Poisson-реконструкция ========== void MainWindow::onReconstructMeshClicked(const PointCloudFilters::PoissonParams ¶ms) { pcl::PointCloud<pcl::PointXYZRGB>::Ptr snapshot; { QMutexLocker locker(&m_cloudMutex); if (!m_accumulatedCloud || m_accumulatedCloud->empty()) { QMessageBox::information(this, "Реконструкция", "Облако пустое."); return; } // Проверка на размер облака перед Poisson if (m_accumulatedCloud->size() > 2000000) { auto result = QMessageBox::question(this, "Большое облако", QString("Облако содержит %1 точек (>%2). Poisson может завершиться с ошибкой по памяти.\n\n" "Выберите действие:\n\n" "- Применить: автоматически применить воксельный фильтр (лист 0.005 м)\n" "- Пропустить: попробовать запустить Poisson без фильтрации\n" "- Отменить: вернуться в интерфейс") .arg(m_accumulatedCloud->size()).arg(2000000), QMessageBox::Apply | QMessageBox::Ignore | QMessageBox::Cancel, QMessageBox::Apply); if (result == QMessageBox::Cancel) { return; } if (result == QMessageBox::Apply) { // Примен��ем воксельный фильтр с листом 0.005 м pcl::VoxelGrid<pcl::PointXYZRGB> voxel; voxel.setLeafSize(0.005f, 0.005f, 0.005f); voxel.setSaveLeafLayout(false); voxel.setInputCloud(m_accumulatedCloud); pcl::PointCloud<pcl::PointXYZRGB>::Ptr filtered(new pcl::PointCloud<pcl::PointXYZRGB>); voxel.filter(*filtered); // Обновляем отображение *m_accumulatedCloud = *filtered; emit cloudSizeChanged(static_cast<int>(filtered->size())); updateViewer(); QMessageBox::information(this, "Фильтрация", QString("Применен воксельный фильтр. Облако уменьшено до %1 точек.").arg(filtered->size())); } // При Ignore продолжаем без фильтрации } // Копия, чтобы не держать mutex пока Poisson работает секунды-минуты. snapshot = pcl::make_shared<pcl::PointCloud<pcl::PointXYZRGB>>(*m_accumulatedCloud); } if (m_processingController->isReconstructionRunning()) { QMessageBox::information(this, "Реконструкция", "Реконструкция уже запущена — дождитесь её завершения."); return; } if (m_meshStatusLabel) m_meshStatusLabel->setText( QString("Выполняется реконструкция… (%1 точек, depth=%2)") .arg(snapshot->size()).arg(params.depth)); if (m_poissonProgress) m_poissonProgress->setValue(0); m_activeProgressBar = m_poissonProgress; statusBar()->showMessage("Poisson: идёт реконструкция…"); m_processingController->runReconstruction( this, snapshot, params, [this](const pcl::PolygonMesh &mesh) { handlePoissonResult(mesh); }, [this]() { handlePoissonCanceled(); }); } // ========== ICP-регистрация (merge проекта) ========== void MainWindow::onMergeScansClicked(const PointCloudFilters::MergeParams ¶ms) { if (m_processingWorkflow) m_processingWorkflow->onMergeScansClicked(params); } void MainWindow::onSaveMergedToProject() { if (m_processingWorkflow) m_processingWorkflow->onSaveMergedToProject(); } void MainWindow::onReconstructLightweightClicked() { QMessageBox::information(this, "В разработке", "Функция временно недоступна"); } void MainWindow::onShowCloudClicked() { if (m_viewerCoordinator) { QMutexLocker locker(&m_cloudMutex); m_viewerCoordinator->showAccumulatedCloud(m_accumulatedCloud, true); } } // ========== Настройки → Параметры… ========== void MainWindow::onShowSettingsDialog() { SettingsDialog dlg(this); dlg.exec(); // SettingsManager::settingsChanged уже эмитится при каждой записи, // так что подписчики получат обновления // автоматически. Существующие UI-виджеты на вкладках читают QSettings // только при запуске — при следующем взаимодействии они подхватят // новые значения через соответствующие click-handler'ы. } void MainWindow::showHelpSection(const QString &anchor) { if (!m_helpDialog) { m_helpDialog = new HelpDialog(this); m_helpDialog->setAttribute(Qt::WA_DeleteOnClose, false); } m_helpDialog->show(); m_helpDialog->raise(); m_helpDialog->activateWindow(); m_helpDialog->openSection(anchor); } void MainWindow::onShowHelpDialog() { showHelpSection(QStringLiteral("quickstart")); } // ========== Поворотный стол / авто-сохранение ========== void MainWindow::onTurntableToggled(bool enabled) { if (!m_turntableTimer) return; if (!enabled) { stopTurntableMode(false); setTurntableStatusText(QString("Остановлено. Сохранено %1 сканов").arg(m_turntableController->capturedCount())); return; } auto toggle = m_turntableController->handleToggle(true, m_project && m_project->isOpen(), m_turntableIntervalSpin->value(), m_turntableCountSpin->value()); if (!toggle.warningText.isEmpty()) { QMessageBox::warning(this, "Поворотный стол", toggle.warningText); } if (toggle.stopTimer) stopTurntableMode(toggle.uncheckCheckbox); if (!toggle.startTimer) return; m_turntableTimer->start(toggle.intervalMs); setTurntableStatusText(toggle.statusText); qInfo() << "[Turntable] started: interval =" << toggle.intervalMs / 1000 << "s, count =" << m_turntableCountSpin->value(); } void MainWindow::onTurntableTick() { qDebug() << "[Turntable] tick: scanning =" << m_scanning << ", captured so far =" << m_turntableController->capturedCount(); auto prep = m_turntableController->beginTick(m_scanning, m_project && m_project->isOpen()); if (!prep.warningText.isEmpty()) qWarning() << prep.warningText; if (prep.stopTimer) stopTurntableMode(prep.uncheckCheckbox); if (!prep.proceed) return; // Обновляем отображение позы сканера updateScannerPoseLabelFromProject(true); QString mode = m_turntableModeCombo ? m_turntableModeCombo->currentData().toString() : "separate"; pcl::PointCloud<pcl::PointXYZRGB>::Ptr snapshot; { QMutexLocker locker(&m_cloudMutex); if (!m_accumulatedCloud || m_accumulatedCloud->empty()) { qWarning() << "[Turntable] tick: accumulated cloud empty, skipping"; setTurntableStatusText(m_turntableController->onEmptyCloud().statusText); return; } snapshot = pcl::make_shared<pcl::PointCloud<pcl::PointXYZRGB>>(*m_accumulatedCloud); // При накоплении - НЕ очищаем облако if (mode != "accumulate") { m_accumulatedCloud->clear(); } } if (mode == "accumulate") { // Режим накопления: объединяем с предыдущими сканами через ICP. // При первом тике создаём скан 0; на последующих — обновляем его. if (m_project->scanCount() == 0) { // Первый тик: создаём начальный скан const int idx = m_project->addScan(snapshot, QStringLiteral("accumulated")); if (idx < 0) { qCritical() << "[Turntable] accumulate: addScan failed:" << m_project->lastError(); stopTurntableMode(true); return; } qInfo() << "[Turntable] accumulate: created initial scan 0"; } else { auto lastCloud = m_project->scanCloud(0); // скан 0 — накопленное облако if (lastCloud && !lastCloud->empty()) { // Выравниваем через ICP перед объединением double maxCorr = 0.05; // 5cm int maxIter = 50; bool icpConverged = false; auto aligned = m_filters->registerPointCloudsICP(snapshot, lastCloud, maxCorr, maxIter, &icpConverged); if (icpConverged && aligned && !aligned->empty()) { qInfo() << "[Turntable] ICP aligned:" << aligned->size() << "points"; *aligned += *lastCloud; snapshot = aligned; } else { qWarning() << "[Turntable] ICP failed, using simple concatenation"; *snapshot += *lastCloud; } } // Обновляем скан 0 (перезаписываем) m_project->setScanCloud(0, snapshot); } auto step = m_turntableController->onAccumulateStep(snapshot->size(), m_turntableCountSpin->value()); setTurntableStatusText(step.statusText); qInfo() << "[Turntable] accumulate:" << snapshot->size() << "points"; // Обновить список сканов и окно просмотра refreshScansList(); updateViewer(); qInfo() << "[Turntable] captured" << step.capturedCount << "/" << m_turntableCountSpin->value(); if (step.completed) { // Сохраняем проект на диск перед остановкой m_project->saveProject(); stopTurntableMode(true); statusBar()->showMessage("Готово!", 3000); // Останавливаем захват после завершения всех поворотов QTimer::singleShot(100, this, &MainWindow::onStopClicked); // Обновить после завершения накопления refreshScansList(); updateViewer(); qInfo() << "[Turntable] All turns completed"; } return; } // Режим "separate" - старый код const QString name = QString("turntable_%1_%2") .arg(m_turntableController->nextSeparateOrdinal(), 3, 10, QChar('0')) .arg(QDateTime::currentDateTime().toString("HHmmss")); const int idx = m_project->addScan(snapshot, name); if (idx < 0) { qCritical() << "[Turntable] addScan failed:" << m_project->lastError(); stopTurntableMode(true); QMessageBox::critical(this, "Поворотный стол", QString("Не удалось сохранить скан:\n%1").arg(m_project->lastError())); return; } auto step = m_turntableController->onSeparateStep(snapshot->size(), m_turntableCountSpin->value()); // Обновляем GUI: счётчики облака, вьюер. emit cloudSizeChanged(0); if (m_viewerCoordinator) m_viewerCoordinator->clearMainViewer(); const int target = m_turntableCountSpin->value(); setTurntableStatusText(step.statusText); qInfo() << "[Turntable] saved scan" << step.capturedCount << "/" << target << "(" << snapshot->size() << "points) as index" << idx; if (step.completed) { stopTurntableMode(true); statusBar()->showMessage( QString("Поворотный стол: готово, сохранено %1 сканов").arg(step.capturedCount), 5000); QMessageBox::information(this, "Поворотный стол", QString("Готово. Сохранено %1 сканов в проекте.\n" "Перейдите на вкладку «Обработка» → «Объединить все сканы проекта», " "чтобы склеить их через ICP.").arg(step.capturedCount)); // Останавливаем захват после завершения onStopClicked(); qInfo() << "[Turntable] All saves completed, capture stopped"; } } // ========== AI Service ==========