diff --git a/.github/workflows/build-windows-QT5.yml b/.github/workflows/build-windows-QT5.yml index c48b0769..5a0d17f6 100644 --- a/.github/workflows/build-windows-QT5.yml +++ b/.github/workflows/build-windows-QT5.yml @@ -354,6 +354,7 @@ jobs: Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_highgui460.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_imgcodecs460.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_imgproc460.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" + Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_videoio460.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\qwt-${{env.QWT_version}}\lib\qwt.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\${{env.QT_version}}\mingw81_64\bin\Qt5OpenGL.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\tools\mingw${{env.mingw_version}}_64\bin\libquadmath-0.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index b8e50bbd..da17d21a 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -358,6 +358,7 @@ jobs: Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_highgui4120.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_imgcodecs4120.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_imgproc4120.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" + Copy-Item ".\build_openCV\install\x64\mingw\bin\libopencv_videoio4120.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\qwt-${{env.QWT_version}}\lib\qwt.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\${{env.QT_version}}\mingw_64\bin\Qt6OpenGL.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" Copy-Item ".\tools\mingw${{env.mingw_version}}_64\bin\libgfortran-5.dll" -Destination ".\DFTFringe\DFTFringeInstaller\packages\com.githubdoe.DFTFringe\data" diff --git a/.gitignore b/.gitignore index b17c711f..02973ec8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ moc_* qrc_* ui_* build +*.user # Linux build DFTFringe diff --git a/Circleoutline.h b/Circleoutline.h index dfbf9a85..24093e19 100644 --- a/Circleoutline.h +++ b/Circleoutline.h @@ -47,5 +47,6 @@ class CircleOutline { QPointF m_p1; QPointF m_p2; }; - +// Declare it for the meta-object system +Q_DECLARE_METATYPE(CircleOutline) #endif // CIRCLEOUTLINE_H diff --git a/DFTFringe.pro b/DFTFringe.pro index 07b86821..61d8601f 100644 --- a/DFTFringe.pro +++ b/DFTFringe.pro @@ -55,6 +55,8 @@ win32 { LIBS += -L$$PWD\..\build_openCV\install\x64\mingw\bin -llibopencv_highgui4120 LIBS += -L$$PWD\..\build_openCV\install\x64\mingw\bin -llibopencv_imgcodecs4120 LIBS += -L$$PWD\..\build_openCV\install\x64\mingw\bin -llibopencv_imgproc4120 + LIBS += -L$$PWD\..\build_openCV\install\x64\mingw\bin -llibopencv_video4120 + LIBS += -L$$PWD\..\build_openCV\install\x64\mingw\bin -llibopencv_videoio4120 LIBS += -ldbghelp # for SetUnhandledExceptionFilter LIBS += -lz # zip compression library needed for cnpy.cpp @@ -81,6 +83,8 @@ unix: !mac { LIBS += -lopencv_highgui LIBS += -lopencv_imgcodecs LIBS += -lopencv_imgproc + LIBS += -lopencv_video + LIBS += -lopencv_videoio LIBS += -L/usr/local/qwt-6.3.0/lib -lqwt LIBS += -lz # zip compression library needed for cnpy.cpp } @@ -123,6 +127,8 @@ macx { LIBS += -lopencv_highgui LIBS += -lopencv_imgcodecs LIBS += -lopencv_imgproc + LIBS += -lopencv_video + LIBS += -lopencv_videoio QWT_FRAMEWORK_HEADERS = $$system(pkg-config --variable=libdir Qt6Qwt6)/qwt.framework/Headers exists($$QWT_FRAMEWORK_HEADERS): INCLUDEPATH += $$QWT_FRAMEWORK_HEADERS @@ -190,6 +196,9 @@ SOURCES += SingleApplication/singleapplication.cpp \ intensityplot.cpp \ jitteroutlinedlg.cpp \ lensetablemodel.cpp \ + liveimageview.cpp \ + liveviewdialog.cpp \ + liveviewhistory.cpp \ main.cpp \ mainwindow.cpp \ messagereceiver.cpp \ @@ -250,6 +259,7 @@ SOURCES += SingleApplication/singleapplication.cpp \ usercolormapdlg.cpp \ userdrawnprofiledlg.cpp \ utils.cpp \ + videostreamworker.cpp \ vortexdebug.cpp \ wavefront.cpp \ wavefrontaveragefilterdlg.cpp \ @@ -313,6 +323,9 @@ HEADERS += bezier/bezier.h \ intensityplot.h \ jitteroutlinedlg.h \ lensetablemodel.h \ + liveimageview.h \ + liveviewdialog.h \ + liveviewhistory.h \ mainwindow.h \ messagereceiver.h \ metricsdisplay.h \ @@ -373,6 +386,7 @@ HEADERS += bezier/bezier.h \ usercolormapdlg.h \ userdrawnprofiledlg.h \ utils.h \ + videostreamworker.h \ vortexdebug.h \ wavefront.h \ wavefrontaveragefilterdlg.h \ @@ -547,4 +561,5 @@ DISTFILES += buildingDFTFringe64.txt \ COPYING.LESSER.txt \ COPYING.txt \ README.md \ - RevisionHistory.html + RevisionHistory.html \ + liveViewHelp.md diff --git a/DFTFringe_Dale.pro b/DFTFringe_Dale.pro index 082bf3b9..1e4103ce 100644 --- a/DFTFringe_Dale.pro +++ b/DFTFringe_Dale.pro @@ -15,6 +15,7 @@ QT += network \ widgets QT += concurrent widgets QT += charts + qtHaveModule(printsupport): QT += printsupport QT += core gui QT += opengl widgets @@ -36,7 +37,11 @@ SOURCES += main.cpp \ custom3dinputhandler.cpp \ defocusdlg.cpp \ edgeplot.cpp \ + frameprocessworker.cpp \ hotkeysdlg.cpp \ + liveimageview.cpp \ + liveviewdialog.cpp \ + liveviewhistory.cpp \ mainwindow.cpp \ igramarea.cpp \ circleoutline.cpp \ @@ -57,6 +62,7 @@ SOURCES += main.cpp \ surfacegraph.cpp \ surfacelightingproxy.cpp \ userdrawnprofiledlg.cpp \ + videostreamworker.cpp \ wavefront.cpp \ contourplot.cpp \ contourtools.cpp \ @@ -160,10 +166,14 @@ HEADERS += mainwindow.h \ IgramArea.h \ circleoutline.h \ cnpy/cnpy.h \ + frameprocessworker.h \ graphicsutilities.h \ dfttools.h \ dftarea.h \ hotkeysdlg.h \ + liveimageview.h \ + liveviewdialog.h \ + liveviewhistory.h \ oglrendered.h \ pdfcalibrationdlg.h \ percentCorrectionSurface.h \ @@ -178,6 +188,7 @@ HEADERS += mainwindow.h \ surfacegraph.h \ surfacelightingproxy.h \ userdrawnprofiledlg.h \ + videostreamworker.h \ wavefront.h \ contourplot.h \ contourtools.h \ @@ -376,7 +387,8 @@ LIBS += D:\opencv\opencv-3.4.12\build\bin\libopencv_imgcodecs3412.dll LIBS += D:\opencv\opencv-3.4.12\build\bin\libopencv_imgproc3412.dll LIBS += D:\opencv\opencv-3.4.12\build\bin\libopencv_features2d3412.dll LIBS += D:\opencv\opencv-3.4.12\build\bin\libopencv_calib3d3412.dll - +LIBS += D:\opencv\opencv-3.4.12\build\bin\libopencv_video3412.dll +LIBS += D:\opencv\opencv-3.4.12\build\bin\libopencv_videoio3412.dll #LIBS += D:\armadillo\bin\libarmadillo.dll LIBS += D:\lapack\build64\bin\liblapack.dll LIBS += D:\lapack\build64\bin\libblas.dll @@ -413,7 +425,7 @@ RC_FILE = DFTFringe.rc QMAKE_CXXFLAGS += -std=c++11 # The application version -VERSION = 3Dmouse_08/24/26 +VERSION = LiveView_08/29/26 # Define the preprocessor macro to get the application version in our application. DEFINES += APP_VERSION=\\\"$$VERSION\\\" @@ -500,6 +512,7 @@ DISTFILES += \ COPYING.txt \ RevisionHistory.html \ README.md \ + liveViewHelp.md TRANSLATIONS = dftfringe_fr.ts diff --git a/DFTFringe_QT5.pro b/DFTFringe_QT5.pro index f14f748c..75295621 100644 --- a/DFTFringe_QT5.pro +++ b/DFTFringe_QT5.pro @@ -55,6 +55,8 @@ win32 { LIBS += -L..\build_openCV\install\x64\mingw\bin -llibopencv_highgui460 LIBS += -L..\build_openCV\install\x64\mingw\bin -llibopencv_imgcodecs460 LIBS += -L..\build_openCV\install\x64\mingw\bin -llibopencv_imgproc460 + LIBS += -L..\build_openCV\install\x64\mingw\bin -llibopencv_video460 + LIBS += -L..\build_openCV\install\x64\mingw\bin -llibopencv_videoio460 LIBS += -ldbghelp # for SetUnhandledExceptionFilter LIBS += -lz # zip compression library needed for cnpy.cpp @@ -78,7 +80,8 @@ unix: !mac { LIBS += -lopencv_highgui LIBS += -lopencv_imgcodecs LIBS += -lopencv_imgproc - LIBS += -lopencv_imgproc + LIBS += -lopencv_video + LIBS += -lopencv_videoio LIBS += -lqwt-qt5 LIBS += -lz # zip compression library needed for cnpy.cpp @@ -194,6 +197,9 @@ SOURCES += SingleApplication/singleapplication.cpp \ intensityplot.cpp \ jitteroutlinedlg.cpp \ lensetablemodel.cpp \ + liveimageview.cpp \ + liveviewdialog.cpp \ + liveviewhistory.cpp \ main.cpp \ mainwindow.cpp \ messagereceiver.cpp \ @@ -254,6 +260,7 @@ SOURCES += SingleApplication/singleapplication.cpp \ usercolormapdlg.cpp \ userdrawnprofiledlg.cpp \ utils.cpp \ + videostreamworker.cpp \ vortexdebug.cpp \ wavefront.cpp \ wavefrontaveragefilterdlg.cpp \ @@ -317,6 +324,9 @@ HEADERS += bezier/bezier.h \ intensityplot.h \ jitteroutlinedlg.h \ lensetablemodel.h \ + liveimageview.h \ + liveviewdialog.h \ + liveviewhistory.h \ mainwindow.h \ messagereceiver.h \ metricsdisplay.h \ @@ -377,6 +387,7 @@ HEADERS += bezier/bezier.h \ usercolormapdlg.h \ userdrawnprofiledlg.h \ utils.h \ + videostreamworker.h \ vortexdebug.h \ wavefront.h \ wavefrontaveragefilterdlg.h \ @@ -551,7 +562,8 @@ DISTFILES += buildingDFTFringe64.txt \ COPYING.LESSER.txt \ COPYING.txt \ README.md \ - RevisionHistory.html + RevisionHistory.html \ + liveViewHelp.md diff --git a/DFTResources.qrc b/DFTResources.qrc index baf94d83..959a5ac9 100644 --- a/DFTResources.qrc +++ b/DFTResources.qrc @@ -18,5 +18,6 @@ res/wats2.png res/mirror_stand.png + liveViewHelp.md diff --git a/IgramArea.h b/IgramArea.h index 2af7f3c3..911b82ea 100644 --- a/IgramArea.h +++ b/IgramArea.h @@ -112,6 +112,7 @@ class IgramArea : public QWidget IgramArea(QWidget *parent = 0, void *mwp = 0 ); void *m_mw; + bool openImage(QImage img, bool showBoundary, QString = "stream"); bool openImage(const QString &fileName, bool showBoundary = true); bool saveImage(const QString &fileName, const char *fileFormat); void setPenColor(const QColor &newColor); @@ -127,12 +128,13 @@ class IgramArea : public QWidget void deleteOutline(); void readOutlines(); void autoTraceOutline(); - + void setOutsideOutline(QPointF center, double radius); void SideOutLineActive(bool checked); void CenterOutlineActive(bool checked); void PolyAreaActive(bool checked); void edgeMaskOutLineActive(bool checked); - void save(); void nextStep(); + void save(); + void nextStep(); bool sideOutlineIsActive; @@ -176,6 +178,7 @@ public slots: void zoomFull(); void toggleHideOutline(); void edgeMode(); + signals: void enableShiftButtons(bool); void statusBarUpdate(QString, int); @@ -185,6 +188,7 @@ public slots: void dftCenterFilter(double); void imageSize(QString); void doDFT(); + void boundary(CircleOutline outside, bool hasBeenCropped, QPointF cropVal, CircleOutline inside); protected: bool eventFilter(QObject *object, QEvent *event); void wheelEvent(QWheelEvent * event); @@ -201,7 +205,7 @@ public slots: QPushButton *m_demo; QWidget *outlinePlotWindow; QDockWidget *dock; - void drawBoundary(); + void resizeImage(); void zoom(int del, QPointF zoompt); bool modified; @@ -225,6 +229,7 @@ public slots: double m_edgeMaskWidth; void saveRegions(); public: + void drawBoundary(); void deleteRegions(); QImage igramColor; QImage igramDisplay; // gray with outlines @@ -266,8 +271,7 @@ public slots: QPointF dragStart; int crop_dx; int crop_dy; - int cropTotalDx, cropTotalDy; - double cropScale; + dftThumb *m_dftThumb; QTimer *m_outlineTimer; bool hasBeenCropped; @@ -287,6 +291,7 @@ public slots: public: QImage getBestChannel(QImage &img); int m_current_boundry; + int cropTotalDx, cropTotalDy; public slots: void addregion(); void deleteregion(int); diff --git a/astigstatsdlg.cpp b/astigstatsdlg.cpp index 9992af56..e74d760d 100644 --- a/astigstatsdlg.cpp +++ b/astigstatsdlg.cpp @@ -168,7 +168,7 @@ astigStatsDlg::astigStatsDlg(const QVector &wavefronts, QWidget *pa Qt::RightButton ); ui->mPlot->setPalette( Qt::white ); -qDebug() << "what the heck"; + //ui->bestFitCB->hide(); plot(); } diff --git a/autoinvertdlg.cpp b/autoinvertdlg.cpp index 25da2cba..6e69453d 100644 --- a/autoinvertdlg.cpp +++ b/autoinvertdlg.cpp @@ -54,8 +54,4 @@ void autoInvertDlg::enableConic(bool b) { ui->btnUseConic->setEnabled(b); } -void autoInvertDlg::on_InvertWithoutAskingRb_clicked(bool checked) -{ - Q_UNUSED(checked); -} diff --git a/autoinvertdlg.h b/autoinvertdlg.h index 647af4fd..c010d3f8 100644 --- a/autoinvertdlg.h +++ b/autoinvertdlg.h @@ -27,7 +27,6 @@ private slots: void on_btnOutside_clicked(); - void on_InvertWithoutAskingRb_clicked(bool checked); private: Ui::autoInvertDlg *ui; diff --git a/contourplot.cpp b/contourplot.cpp index 59d84ea3..ac32f865 100644 --- a/contourplot.cpp +++ b/contourplot.cpp @@ -46,7 +46,7 @@ #include #include "spdlog/spdlog.h" #include - +#include double zOffset = 0; double lastx = -1.; @@ -312,7 +312,7 @@ void ContourPlot::showContoursChanged(double val){ d_spectrogram->setContourLevels( contourLevels ); d_spectrogram->setDisplayMode( QwtPlotSpectrogram::ContourMode,true ); set.setValue("contourShowLines", true); - d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPen) : QPen(Qt::NoPen)); + d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPenColor, m_countourPenWidth) : QPen(Qt::NoPen)); } replot(); } @@ -580,10 +580,10 @@ QString ContourPlot::m_zRangeMode("Auto"); double ContourPlot::m_zOffset = 0.; ContourPlot::ContourPlot( QWidget *parent, ContourTools *tools, bool minimal ): - QwtPlot( parent ),m_wf(0),m_tools(tools),m_minimal(minimal), m_linkProfile(true), m_inZoomOperation(false), m_contourPen(Qt::white) + QwtPlot( parent ),m_wf(0),m_tools(tools),m_minimal(minimal), m_linkProfile(true), m_inZoomOperation(false), m_contourPenColor(Qt::white) { spdlog::get("logger")->trace("ContourPlot::ContourPlot"); - d_spectrogram = new QwtPlotSpectrogram(); + d_spectrogram = new CustomSpectrogram(); picker_ = new QwtPlotPicker(this->canvas()); picker_->setStateMachine(new QwtPickerClickPointMachine); @@ -598,7 +598,9 @@ ContourPlot::ContourPlot( QWidget *parent, ContourTools *tools, bool minimal ): QSettings settings; m_colorMapNdx = settings.value("colorMapType",0).toInt(); contourRange = settings.value("contourRange", .1).toDouble(); - m_contourPen = QColor(settings.value("ContourLineColor", "white").toString()); + m_contourPenColor = QColor(settings.value("ContourLineColor", "white").toString()); + m_countourPenWidth = settings.value("ContourLineWidth",1).toInt(); + d_spectrogram->contourWidth = m_countourPenWidth; m_do_fill = settings.value("contourShowFill", true).toBool(); m_rulerPen = QPen(QColor(settings.value("ContourRulerColor", "grey").toString())); m_radialDeg = settings.value("contourRulerRadialDeg", 30).toInt(); @@ -613,7 +615,7 @@ void ContourPlot::initPlot(){ QSettings settings; bool fill = settings.value("contourShowFill", true).toBool(); d_spectrogram->setRenderThreadCount( 0 ); // use system specific thread count - d_spectrogram->setDefaultContourPen( fill ? QPen( m_contourPen): QPen(Qt::NoPen)); + d_spectrogram->setDefaultContourPen( fill ? QPen( m_contourPenColor,m_countourPenWidth): QPen(Qt::NoPen)); d_spectrogram->setColorMap( new dftColorMap(settings.value("colorMapType",0).toInt()) ); d_spectrogram->setCachePolicy( QwtPlotRasterItem::PaintCache ); @@ -656,17 +658,25 @@ void ContourPlot::setTool(ContourTools *tool){ void ContourPlot::on_line_color_changed(QColor c) { - m_contourPen = c; - d_spectrogram->setDefaultContourPen(QPen(c)); + m_contourPenColor = c; + d_spectrogram->setDefaultContourPen(QPen(c,m_countourPenWidth)); QSettings settings; settings.setValue("ContourLineColor", c.name()); replot(); } - +void ContourPlot::on_line_width_changed(int width) +{ + m_countourPenWidth = width; + d_spectrogram->contourWidth = width; + d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPenColor, m_countourPenWidth) : QPen(Qt::NoPen)); + QSettings settings; + settings.setValue("ContourLineWidth", width); + replot(); +} void ContourPlot::contourFillChanged(int val) { if (val){ - d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPen) : QPen(Qt::NoPen)); + d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPenColor,m_countourPenWidth) : QPen(Qt::NoPen)); } replot(); @@ -678,7 +688,7 @@ void ContourPlot::showContour( bool on ) set.setValue("contourShowLines", on); d_spectrogram->setDisplayMode( QwtPlotSpectrogram::ContourMode, on ); if (on) - d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPen) : QPen(Qt::NoPen)); + d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPenColor,m_countourPenWidth) : QPen(Qt::NoPen)); replot(); } @@ -689,7 +699,7 @@ void ContourPlot::showSpectrogram(bool on ) m_do_fill = on; d_spectrogram->setDisplayMode( QwtPlotSpectrogram::ImageMode, on ); - d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPen) : QPen(Qt::NoPen)); + d_spectrogram->setDefaultContourPen(m_do_fill ? QPen(m_contourPenColor,m_countourPenWidth) : QPen(Qt::NoPen)); replot(); } @@ -810,3 +820,111 @@ void ContourPlot::printPlot() } #endif + +void ContourPlot::printActualSizeTiled() +{ + if (m_wf == nullptr) { + spdlog::get("logger")->warn("printActualSizeTiled: m_wf is null"); + return; + } + + QString fileName = QFileDialog::getSaveFileName( + this, + tr("Save Tiled Mirror PDF"), + "mirror_tiled_1to1.pdf", + tr("PDF Files (*.pdf)") + ); + + if (fileName.isEmpty()) + return; + + QPrinter printer(QPrinter::HighResolution); + printer.setOutputFormat(QPrinter::PdfFormat); + printer.setOutputFileName(fileName); + + int dpiX = printer.logicalDpiX(); + int dpiY = printer.logicalDpiY(); + + // 1. Calculate exact physical dimensions based on mirror diameter in mm + double mirrorDiameterMm = m_wf->diameter; + double mirrorRadiusMm = mirrorDiameterMm / 2.0; + double pixelsPerMm = m_wf->m_outside.m_radius / mirrorRadiusMm; + + // Total master image size in pixels matching the physical mirror size + int totalWidthPx = qRound((mirrorDiameterMm / 25.4) * dpiX); + int totalHeightPx = qRound((mirrorDiameterMm / 25.4) * dpiY); + + // Temporarily hide UI chrome for clean output + bool oldMinVal = m_minimal; + m_minimal = true; + enableAxis(QwtPlot::yLeft, false); + enableAxis(QwtPlot::xBottom, false); + enableAxis(QwtPlot::yRight, false); + replot(); + + // 2. Render the plot into a master image sized to the exact physical mirror proportions + QImage mirrorImage(totalWidthPx, totalHeightPx, QImage::Format_ARGB32); + mirrorImage.fill(Qt::white); + + QPainter imgPainter(&mirrorImage); + QwtPlotRenderer renderer; + renderer.setDiscardFlag(QwtPlotRenderer::DiscardBackground, true); + + // Map the plot view exactly to the mirror bounds + double cx = m_wf->data.cols / 2.0; + double cy = m_wf->data.rows / 2.0; + double radiusPx = mirrorRadiusMm * pixelsPerMm; + + setAxisScale(QwtPlot::xBottom, cx - radiusPx, cx + radiusPx); + setAxisScale(QwtPlot::yLeft, cy - radiusPx, cy + radiusPx); + replot(); + + renderer.render(this, &imgPainter, QRectF(0, 0, totalWidthPx, totalHeightPx)); + imgPainter.end(); + + // Restore UI state + m_minimal = oldMinVal; + enableAxis(QwtPlot::yLeft, !m_minimal); + enableAxis(QwtPlot::xBottom, !m_minimal); + updateAspectRatio(); + replot(); + + // 3. Determine printable page dimensions in pixels + QPageLayout pageLayout = printer.pageLayout(); + QRectF printableRectMm = pageLayout.paintRect(QPageLayout::Millimeter); + + int pageWidthPx = qRound((printableRectMm.width() / 25.4) * dpiX); + int pageHeightPx = qRound((printableRectMm.height() / 25.4) * dpiY); + + int overlapPx = qRound((10.0 / 25.4) * dpiX); + int effectiveWidthPx = pageWidthPx - overlapPx; + int effectiveHeightPx = pageHeightPx - overlapPx; + + int numCols = qCeil((double)totalWidthPx / effectiveWidthPx); + int numRows = qCeil((double)totalHeightPx / effectiveHeightPx); + + // 4. Slice the master image and write each tile. + // printerPainter.end() is called explicitly at the end to release the file lock. + QPainter printerPainter; + if (!printerPainter.begin(&printer)) + return; + + bool firstPage = true; + for (int row = 0; row < numRows; ++row) { + for (int col = 0; col < numCols; ++col) { + if (!firstPage) { + printer.newPage(); + } + firstPage = false; + + int srcX = col * effectiveWidthPx; + int srcY = row * effectiveHeightPx; + int srcW = qMin(pageWidthPx, totalWidthPx - srcX); + int srcH = qMin(pageHeightPx, totalHeightPx - srcY); + + QImage tile = mirrorImage.copy(srcX, srcY, srcW, srcH); + printerPainter.drawImage(0, 0, tile); + } + } + printerPainter.end(); // Releases the file lock immediately +} diff --git a/contourplot.h b/contourplot.h index cf86c67c..7cb16b04 100644 --- a/contourplot.h +++ b/contourplot.h @@ -31,6 +31,17 @@ #include class MyZoomer; +class CustomSpectrogram : public QwtPlotSpectrogram { +public: + qreal contourWidth = 3; + +protected: + QPen contourPen(double level) const override { + QPen pen = QwtPlotSpectrogram::contourPen(level); // Retrieves color from color map + pen.setWidthF(contourWidth); // Enforces your custom width + return pen; + } +}; class ContourPlot: public QwtPlot { @@ -40,7 +51,7 @@ class ContourPlot: public QwtPlot double m_lastAngle; public: - QwtPlotSpectrogram *d_spectrogram; + CustomSpectrogram *d_spectrogram; const wavefront* m_wf; ContourTools *m_tools; static bool m_useMiddleOffset; @@ -64,7 +75,7 @@ class ContourPlot: public QwtPlot int m_radialDeg; bool m_do_fill; bool m_inZoomOperation; - + int m_countourPenWidth = 1; signals: void setMinMaxValues(double,double); @@ -87,6 +98,8 @@ public slots: void newDisplayErrorRange(double min, double max); void drawProfileLine(const double ang); void clearZoomFlag(); + void printActualSizeTiled(); + void on_line_width_changed(int width); #ifndef QT_NO_PRINTER void printPlot(); #endif @@ -96,7 +109,8 @@ public slots: void initPlot(); bool eventFilter(QObject *obj, QEvent *event); - QColor m_contourPen; + QColor m_contourPenColor; + void ruler(); double m_min; diff --git a/contourview.cpp b/contourview.cpp index 5970c6c5..479ebedf 100644 --- a/contourview.cpp +++ b/contourview.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include contourView::contourView(QWidget *parent, ContourTools *tools) : QWidget(parent), zoomed(false), ui(new Ui::contourView), tools(tools) @@ -77,7 +79,29 @@ void contourView::showContextMenu(QPoint pos) QMenu myMenu; QString txt = (zoomed)? tr("Restore to MainWindow") : tr("FullScreen"); myMenu.addAction(txt, this, &contourView::zoom); - + QString txt2 = "Print full sized"; + myMenu.addAction(txt2, ui->widget, &ContourPlot::printActualSizeTiled); + // Direct lambda action to trigger QColorDialog and call your public slot + myMenu.addAction("Select Contour Line Color...", [this]() { + QColor chosenColor = QColorDialog::getColor(Qt::white, this, tr("Select Contour Line Color")); + if (chosenColor.isValid()) { + ui->widget->on_line_color_changed(chosenColor); + } + }); + myMenu.addAction("Select Contour Line Width...", [this]() { + bool ok; + int newWidth = QInputDialog::getInt( + this, + tr("Contour Line Width"), + tr("Width (pixels):"), + ui->widget->m_countourPenWidth, + 1, 10, 1, + &ok + ); + if (ok) { + ui->widget->on_line_width_changed(newWidth); + } + }); // Show context menu at handling position myMenu.exec(globalPos); } @@ -139,4 +163,5 @@ void contourView::on_LinkProfileCB_clicked(bool checked) QSettings set; set.setValue("linkProfilePlot", checked); getPlot()->m_linkProfile = checked; + } diff --git a/dftarea.cpp b/dftarea.cpp index fa7d9962..dedda2c0 100644 --- a/dftarea.cpp +++ b/dftarea.cpp @@ -137,11 +137,12 @@ DFTArea::DFTArea(QWidget *mparent, IgramArea *ip, DFTTools * tools, vortexDebug tools->connectTo(this); capture = false; QSettings set; - m_center_filter = set.value("DFT Center Filter", 10).toDouble(); - qDebug() << "init center" << m_center_filter; - // TODO I muted clazy warning as this is a false positive. - // It might be good to find a different way to call setCenterFilterValue - emit updateFilterSize(m_center_filter); // clazy:exclude=incorrect-emit + + QTimer::singleShot(0, this, [this]() { + emit updateFilterSize(m_center_filter); + }); + + installEventFilter(this); /* @@ -226,6 +227,9 @@ void DFTArea::dftCenterFilter(double v){ QSettings set; set.setValue("DFT Center Filter", v); emit updateFilterSize(v); + double frequencyBin = v * (static_cast(m_dftsize)/(m_outside.m_radius * 2 * m_scaleFactor)); + emit filterSizeScale(frequencyBin); + update(); } @@ -282,38 +286,38 @@ cv::Mat DFTArea::grayComplexMatfromImage(QImage &img){ cv::Mat iMat(img.height(), img.width(), CV_8UC3, img.bits(), img.bytesPerLine()); cv::Mat tmp = iMat.clone(); cv::Mat roi = iMat(cv::Rect((int)left,(int)top,(int)width,(int)height)).clone(); - + m_roiSize = roi.cols; // save to send to live view if it is watching so it can compute the true filter circle size double centerDx = centerX - igramArea->m_center.m_center.x(); double centerDy = centerY - igramArea->m_center.m_center.y(); roi.convertTo(roi,CV_32FC3); QSettings set; int dftSize = set.value("DFTSize", 640).toInt(); - double scaleFactor = (double)dftSize/roi.cols; + m_scaleFactor = (double)dftSize/roi.cols; m_outside = CircleOutline(QPointF(xCenterShift,yCenterShift), rad); m_center = CircleOutline(QPointF(xCenterShift - centerDx, yCenterShift - centerDy), igramArea->m_center.m_radius); //scaleFactor = 1; - if (scaleFactor < 1.){ + if (m_scaleFactor < 1.){ - cv::resize(roi,roi, cv::Size(0,0), scaleFactor, scaleFactor,INTER_AREA); + cv::resize(roi,roi, cv::Size(0,0), m_scaleFactor, m_scaleFactor,INTER_AREA); double roicx = (roi.cols-1)/2.; double roicy = (roi.rows-1)/2.; m_outside = CircleOutline(QPointF(roicx,roicy),roicx); - m_center = CircleOutline(QPointF((roicx - centerDx * scaleFactor), (roicy - centerDy * scaleFactor)), - m_center.m_radius * scaleFactor); + m_center = CircleOutline(QPointF((roicx - centerDx * m_scaleFactor), (roicy - centerDy * m_scaleFactor)), + m_center.m_radius * m_scaleFactor); } else { - scaleFactor = 1.; + m_scaleFactor = 1.; } m_poly.clear(); for (int n = 0; n < igramArea->m_polygons.size(); ++n){ m_poly.append(std::vector< cv::Point>()); for (unsigned int i = 0; i < igramArea->m_polygons[n].size(); ++i){ - int x = round((igramArea->m_polygons[n][i].x - left) * scaleFactor); - int y = round((igramArea->m_polygons[n][i].y - top) * scaleFactor); + int x = round((igramArea->m_polygons[n][i].x - left) * m_scaleFactor); + int y = round((igramArea->m_polygons[n][i].y - top) * m_scaleFactor); // make sure x and y values of regions are inside our matrix if (x < 0) @@ -357,7 +361,7 @@ cv::Mat DFTArea::grayComplexMatfromImage(QImage &img){ Mat complexI; merge(planes, 2, complexI); // Add to the expanded another plane with zeros - if (scaleFactor == 1.){ + if (m_scaleFactor == 1.){ tools->imageSize(QString("DFT Size will be %1").arg(width)); } else @@ -447,7 +451,7 @@ void DFTArea::doDFT(){ QImage img = igramArea->igramGray; cv::Mat complexI = grayComplexMatfromImage(img); - + m_dftsize = complexI.size().width; cv::Mat planes[2]; split(complexI,planes); @@ -470,8 +474,6 @@ void DFTArea::doDFT(){ m_dft = complexI/complexI.size().area(); magIImage = showMag(complexI,false,"", true, m_gamma); - - magIImage = magIImage.scaled(magIImage.width() , magIImage.height() ); setMinimumSize(magIImage.size()); emit selectDFTTab(); @@ -492,6 +494,10 @@ void DFTArea::doDFT(){ //if (Settings2::showDFT()) //emit dftReady(magIImage); //Creates a thumbnail dft area QApplication::restoreOverrideCursor(); + double frequencyBin = m_center_filter * (static_cast(m_dftsize)/(m_outside.m_radius * 2 * m_scaleFactor)); + + emit filterSizeScale(frequencyBin); + } void DFTArea::gamma(int i){ double v = 1. + 5. * (double)i/99.; @@ -1048,7 +1054,7 @@ void DFTArea::makeSurface(){ showData("result surface", result.clone()); } - +//qDebug() << "dftarea make surface (wavefront) uses igram and emits newWavefront mainwindow calls make wavefront from phase map"; emit newWavefront(result, m_outside, m_center, QFileInfo(igramArea->m_filename).baseName(), WavefrontOrigin::Igram, m_poly); QApplication::restoreOverrideCursor(); @@ -1073,8 +1079,13 @@ void DFTArea::mouseMoveEvent(QMouseEvent *event){ int ydel = pos.y() - ycenter; int rad = sqrt(pow(xdel,2)+pow(ydel,2)); emit updateFilterSize(rad/zoom); -} + //double percent = double(rad/zoom)/(magIImage.size().width()/2.); + //emit centerFilterPercent(percent); +} +void DFTArea::mouseMovedExternal(int rad){ + emit updateFilterSize(rad); +} void DFTArea::mousePressEvent(QMouseEvent *event) { @@ -1431,7 +1442,7 @@ void DFTArea::doPSIstep4(const cv::Mat &images, QVector phases){ qDebug() << "outline name" << outlineName; igramArea->writeOutlines(outlineName); } -qDebug() << "dlg" << dlg.m_x << dlg.m_rad; + int left = dlg.m_x - dlg.m_rad; int right = dlg.m_x + dlg.m_rad; int top = dlg.m_y - dlg.m_rad; @@ -1443,7 +1454,7 @@ qDebug() << "dlg" << dlg.m_x << dlg.m_rad; right = mag2d.cols-1; if (top < 0) top = 0; -qDebug() << "rec" << left << top << width << height; + cv::Mat roi = surface(cv::Rect(left,top, width,height)); surface = roi.clone(); diff --git a/dftarea.h b/dftarea.h index 422e3ca7..e0df55bc 100644 --- a/dftarea.h +++ b/dftarea.h @@ -58,6 +58,7 @@ class DFTArea : public QWidget QString dftSizeStr; QImage magIImage; double m_center_filter; + double m_scaleFactor = 1.; cv::Mat vortex(QImage &img, double low); QVector getPhases(); @@ -84,14 +85,17 @@ public slots: void zoomPlus(); void zoomMinus(); void zoomFit(); + void mouseMovedExternal(int rad); signals: void setDftSizeVal(int); void selectDFTTab(); - void updateFilterSize(int); + void updateFilterSize(double); + void filterSizeScale(double frequencyBin); void newWavefront(cv::Mat, CircleOutline, CircleOutline, const QString &, WavefrontOrigin origin, QVector >); void dftReady(QImage); void statusBarUpdate(QString, int); + void centerFilterSize(double radius, double scale, int DFTSize); private: static DFTArea *m_Instance; Ui::DFTArea *ui; @@ -127,6 +131,10 @@ public slots: int m_psiRows; int m_psiCols; double zoom; + int m_dftsize = 1000; + int m_roiSize; +public: + }; diff --git a/dfttools.cpp b/dfttools.cpp index c2d0ee76..c7055f4b 100644 --- a/dfttools.cpp +++ b/dfttools.cpp @@ -43,11 +43,8 @@ void DFTTools::imageSize(const QString &txt){ ui->imageSize->setText(txt); } +void DFTTools::setCenterFilterValue(double v){ - - - -void DFTTools::setCenterFilterValue(int v){ ui->filterAdjuster->setValue(v); } diff --git a/dfttools.h b/dfttools.h index 8ff895c1..5e7e255e 100644 --- a/dfttools.h +++ b/dfttools.h @@ -52,7 +52,7 @@ class DFTTools : public QDockWidget private slots: - void setCenterFilterValue(int); + void setCenterFilterValue(double); void on_filterAdjuster_valueChanged(double arg1); diff --git a/igramarea.cpp b/igramarea.cpp index be3ef1ba..5e90e6e6 100644 --- a/igramarea.cpp +++ b/igramarea.cpp @@ -86,8 +86,8 @@ QImage cvMatToImage(const cv::Mat &out){ } IgramArea::IgramArea(QWidget *parent, void *mw) : QWidget(parent),m_mw(mw),m_hideOutlines(false),scale(1.),outterPcount(0), innerPcount(0), - zoomIndex(1),dragMode(false),cropTotalDx(0), cropTotalDy(0), hasBeenCropped(false), - m_zoomMode(NORMALZOOM),m_current_boundry(OutSideOutline) + zoomIndex(1),dragMode(false),hasBeenCropped(false), m_zoomMode(NORMALZOOM), m_current_boundry(OutSideOutline), + cropTotalDx(0),cropTotalDy(0) { leftMargin = 0; searchOutlineScale = 1.; @@ -260,10 +260,10 @@ void IgramArea::doGamma(double gammaV){ } cv::Mat IgramArea::qImageToMat(QImage &img){ - QImage::Format f = img.format(); + int depth = img.depth(); - int planesCnt = img.bitPlaneCount(); - qDebug() << "format " << f << depth << planesCnt; + + cv::Mat iMat; switch (depth){ case 24: @@ -935,7 +935,7 @@ void IgramArea::useLastOutline(){ drawBoundary(); } } - qDebug() << "uselastoutline" << m_outside.m_center.x(); + } void IgramArea::adjustCenterandRegions(){ double xoffset , yoffset = 0; @@ -977,26 +977,7 @@ void IgramArea::adjustCenterandRegions(){ } } } - -bool IgramArea::openImage(const QString &fileName, bool showBoundary) - -{ - cropTotalDx = cropTotalDy = 0; - foreach(QWidget *widget, QApplication::topLevelWidgets()) { - if(widget->objectName() == "MainWindow") - widget->setWindowTitle(fileName); - } - - QApplication::setOverrideCursor(Qt::WaitCursor); - QImage loadedImage; - if (!loadedImage.load(fileName)) { - QMessageBox::warning(NULL,"","Image "+fileName + " could not be read."); - QApplication::restoreOverrideCursor(); - return false; - } - // convert image to 3 channel image - if (loadedImage.format() != QImage::Format_RGB888) - loadedImage = loadedImage.convertToFormat(QImage::Format_RGB888); +bool IgramArea::openImage(QImage loadedImage, bool showBoundary, QString fileName){ if (Settings2::getInstance()->m_igram->m_removeDistortion){ cv::Mat raw = cv::imread(fileName.toStdString().c_str()); @@ -1076,12 +1057,11 @@ bool IgramArea::openImage(const QString &fileName, bool showBoundary) m_polygons.clear(); m_regionEdit->clear(); m_regionEdit->hide(); - qDebug() << "Checking for ouline file"; // check for an outline file QFileInfo finfo(makeOutlineName()); emit showTab(0); qApp->processEvents(); - if (finfo.exists()){ + if (!fileName.startsWith("stream") and finfo.exists()){ qDebug() << "file exists"; deleteRegions(); qDebug() << "del regions"; @@ -1106,6 +1086,8 @@ bool IgramArea::openImage(const QString &fileName, bool showBoundary) } + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + cropTotalDx = cropTotalDy = 0; SideOutLineActive(true); @@ -1116,6 +1098,30 @@ bool IgramArea::openImage(const QString &fileName, bool showBoundary) QApplication::restoreOverrideCursor(); return true; } + +bool IgramArea::openImage(const QString &fileName, bool showBoundary) + +{ + cropTotalDx = cropTotalDy = 0; + foreach(QWidget *widget, QApplication::topLevelWidgets()) { + if(widget->objectName() == "MainWindow") + widget->setWindowTitle(fileName); + } + + QApplication::setOverrideCursor(Qt::WaitCursor); + QImage loadedImage; + if (!loadedImage.load(fileName)) { + QMessageBox::warning(NULL,"","Image "+fileName + " could not be read."); + QApplication::restoreOverrideCursor(); + return false; + } + // convert image to 3 channel image + if (loadedImage.format() != QImage::Format_RGB888) + loadedImage = loadedImage.convertToFormat(QImage::Format_RGB888); + return openImage(loadedImage, showBoundary,fileName); + + +} void IgramArea::syncRegions(){ // copy m_polygons to region editor m_regionEdit->clear(); @@ -1267,6 +1273,8 @@ void IgramArea::increaseValue(int i) { else if (m_current_boundry == PolyArea && m_polygons.size() > 0){ increaseRegion(polyndx,1.1); } + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + drawBoundary(); } @@ -1289,6 +1297,8 @@ void IgramArea::decrease(){ else if (m_current_boundry == PolyArea && m_polygons.size() > 0){ increaseRegion(polyndx, .9); } + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + drawBoundary(); } void IgramArea::zoomIn(){ @@ -1341,6 +1351,9 @@ void IgramArea::zoom(int del, QPointF zoompt){ gscrollArea->setWidgetResizable(true); } drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + + } void IgramArea::wheelEvent (QWheelEvent *e) @@ -1367,6 +1380,8 @@ void IgramArea::wheelEvent (QWheelEvent *e) } drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + return; } QPointF pos = e->position(); @@ -1407,7 +1422,10 @@ void IgramArea::mousePressEvent(QMouseEvent *event) dragMode = true; cntrlPressed = event->modifiers() & Qt::ControlModifier; lastPoint = Raw/scale; + drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + return; } if (m_current_boundry == OutSideOutline) m_outside.m_radius = 0; @@ -1523,6 +1541,8 @@ void IgramArea::mousePressEvent(QMouseEvent *event) } drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + } } @@ -1546,6 +1566,8 @@ void IgramArea::mouseMoveEvent(QMouseEvent *event) mirrorDlg &md = *mirrorDlg::get_Instance(); md.m_verticalAxis = md.diameter * e; drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + return; } if ((event->buttons() & Qt::LeftButton) && scribbling) { @@ -1570,6 +1592,8 @@ void IgramArea::mouseMoveEvent(QMouseEvent *event) } drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + } if ( regionMode){ if (m_regionEdit->m_doFreeform && event->buttons() & Qt::LeftButton){ @@ -1577,6 +1601,8 @@ void IgramArea::mouseMoveEvent(QMouseEvent *event) } lastPoint = scaledPos; drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + } else if ((event->buttons() & Qt::LeftButton) & dragMode){ if( outterPcount == 2) { @@ -1612,6 +1638,8 @@ void IgramArea::mouseMoveEvent(QMouseEvent *event) } } drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + lastPoint = scaledPos; } } @@ -1652,6 +1680,8 @@ void IgramArea::mouseReleaseEvent(QMouseEvent *event) } drawBoundary(); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + scribbling = false; verticalTracking = false; dragMode = false; @@ -1938,7 +1968,7 @@ void IgramArea::paintEvent(QPaintEvent *event) topy = 0; } - qDebug() << "viewW" << viewW << "scale"<< zoomIndex; + QImage roi = m_withOutlines.copy(topx,topy, viewW * 2, viewW); smallPainter.drawImage(viewW ,0, roi); @@ -1995,6 +2025,7 @@ void IgramArea::saveRegions(){ } + void IgramArea::crop() { // add current bounds to crop history. @@ -2007,8 +2038,8 @@ void IgramArea::crop() { QSettings set; set.setValue("lastOutsideRad", radx); - set.setValue("lastOutsideCx",cx); - set.setValue("lastOutsideCy",cy); + set.setValue("lastOutsideCx",cx+cropTotalDx); + set.setValue("lastOutsideCy",cy+cropTotalDy); int width = igramGray.width(); int height = igramGray.height(); @@ -2039,7 +2070,6 @@ void IgramArea::crop() { cropTotalDx += x; cropTotalDy += y; - x = igramGray.width()/2; y = igramGray.height()/2; @@ -2052,14 +2082,12 @@ void IgramArea::crop() { } m_outside.translate(QPointF(-crop_dx,-crop_dy)); - cx = m_outside.m_center.x() + crop_dx; - cy = m_outside.m_center.y() + crop_dy; set.setValue("lastinsideRad", m_center.m_radius); m_center.translate(QPointF(-crop_dx,-crop_dy)); - set.setValue("lastinsideCx", m_center.m_center.x() + crop_dx); - set.setValue("lastInsideCy", m_center.m_center.y() + crop_dy); + set.setValue("lastinsideCx", m_center.m_center.x() + cropTotalDx); + set.setValue("lastInsideCy", m_center.m_center.y() + cropTotalDy); // need to rescale p1 and p2 because of the crop scale = (double)(this->height())/y; m_OutterP1 = m_outside.m_p1; @@ -2077,6 +2105,8 @@ void IgramArea::crop() { igramGray.size().width()).arg( igramGray.size() .height())); emit upateColorChannels(qImageToMat(igramColor)); + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); + } void IgramArea::dftReady(const QImage &img){ @@ -2113,7 +2143,17 @@ void IgramArea::CenterOutlineActive(bool checked){ drawBoundary(); update(); } +void IgramArea::setOutsideOutline(QPointF center, double radius){ + m_outside = CircleOutline(center,radius); + if (hasBeenCropped){ + m_outside.translate(QPointF(-cropTotalDx, -cropTotalDy)); + } + m_OutterP1 = m_outside.m_p1; + m_OutterP2 = m_outside.m_p2; + drawBoundary(); + update(); +} void IgramArea::loadOutlineFile(const QString &fileName){ QFile loadFile(fileName); @@ -2404,7 +2444,6 @@ void IgramArea::writeOutlines(QString fileName){ return; } QFileInfo info(saveFile); - qDebug() << info.absoluteFilePath(); saveFile.write(jsondoc.toJson()); saveFile.close(); @@ -2473,6 +2512,7 @@ void IgramArea::shiftoutline(QPointF p) { m_innerP2 = m_center.m_p2; m_centerHist.push(igramGray, m_center); } + emit boundary( m_outside, hasBeenCropped, QPointF(cropTotalDx, cropTotalDy), m_center); drawBoundary(); } @@ -2501,8 +2541,7 @@ void IgramArea::nextStep(){ writeOutlines(makeOutlineName()); } } - if (!hasBeenCropped) - crop(); + crop(); emit doDFT(); m_current_boundry = OutSideOutline; m_regionEdit->hide(); diff --git a/liveViewHelp.md b/liveViewHelp.md new file mode 100644 index 00000000..f7a9b322 --- /dev/null +++ b/liveViewHelp.md @@ -0,0 +1,94 @@ +### Live View using Camera or video stream +___ +**Live view** can show videos from a URL or a USB camera. + + +When in loop mode it samples the video stream and process each sample creating a temporary wave front file. The RMS of that file is examined and if lower than the RMS threshold you set it is added to an average. It displays that average back on the normal 3D surface of DFTFringe. If that RMS is greater than the threshold then it display that frame instead of the average so you can see what might be wrong with the setup. When ever the current RMS value drops below that threshold it will then display the average wavefront. When you stop the loop it will save the average wave front in the wave front list. This can quickly remove variations due to air currents and random vibrations. + +Depending on you computer and camera the sample and analyze rate can be as fast as one per second or every few seconds. + +### +### Connecting to a video feed or camera + +Live view can show videos from a URL or a USB camera. +USB cameras are selected on a windows machine by their ID number. + +**Settings tab** (Lower right) lets you select the source of the video. Use 0, 1, or 2 for USB attached cameras. + +A selection for a URL stream might look like this: 'http://192.168.50.5:5000/video_feed' +### +**Connection problem** +___ +Some cameras take several blank frames to start with. The code thinks that so many blank frames indicate a connection problem. The default is 15. You can set how many blank frames to allow to get your camera connected in the settings. + +When a connection was working but drops for some reason you will get a status message stating that. You must go to settings and click on the device once again to get it to start. + + +### +### Live video view +The Live Video view (left side of the controls) shows the cameras view. In it you will see the interferogram and if enabled the DFT of that igram. +### +### Automated Live Analysis + +Before starting the automated analysis loop, ensure the following steps are completed: + +1. Press the **Grab igram** button To import the igram into DFTFringe and outline it as usual. + +2. Press the **Auto outline** button or outline the igram yourself. The analysis loop will then use that outline for all the rest of the samples until you change it. + +2. Press the **Outline OK** button to then see the DFT and adjust the blue filter if necessary. + +3. Now you are ready to start the analysis of each video frame that it can grab. Press the **Start Loop** button. + +It will loop taking an image, analyze it, compute its RMS and display the RMS and average RMS and the 3D surface plot will update in the standard DFTFringe 3D display. The zernike values will display the current sample's Values and the contour plot will also display the current sample's values. The 3D plot will display the Average of all the samples at or below the Max RMS value. + +### +### Max RMS +___ +**Max RMS value** You can set the Max RMS value where values higher than that will not be used in the analysis. + +### +### Auto RMS Setup + +Enable the checkbox if you want the Max RMS value to be set to the value of the first analyzed wavefront times a percentage. This will happen the first time you **Start** the analysis. From then on the Max value will not be modified by the program. You can still modify it yourself however. + +### + +If a wavefront's RMS is equal or below the max RMS value it will added to the average and the average will be displayed in the 3D view. Otherwise that wave front will be displayed in the 3D view and a status field will turn red. + +Once the looping has started you might want to pause it to adjust some settings without resetting the averaging. + +The **Start** button always resets the averaging if it was selected to be done. + +The **Stop** button always stops the current looping and any averaging happening. + +The average is saved you press Stop or press the "Save Average" button. +### +### Trend View +___ +The trend view below the live view shows a graph of two values computed from each analysis over time. +* RMS - shows the average RMS value. It scale is on the left side. + +* Best Fit Conic or SA - Show either of those values as a running average of the last 5 samples. It's scale is on the right side of the graph. You can select either of the two values it will show in the settings. + +* Split bar - the Trend view has a border line between it and the live view that can be moved to change the size of the live view and Trend view. You can drag that bar all the way down to show only the Live view or all the way up to show only the Trend graph. + +### +### Controls +___ +The panel on the right has several controls. +* Video zoom factor - usually fit to window is best +* DFT Size - Usually **512** is best but if your fringes are very close together and you have a large image then **1024 x 1024** is better. The larger the DFT the slower the video is sampled. +* DFT Transparency - values from 0 to 255. 99 seems to be a good value. Sets how visible the igram is under the DFT. +* DFT Contrast - changes the colors of the DFT. .7 seems to be a good value. + +### +### More Settings +___ + +Other settings values in the settings menu. +* Camera Resolution - you can change some camera's resolution. If the camera does not accept this or one of the values it is ignored. Typically you can set this to a resolution much higher than your camera's ability and your camera will pick it's highest resolution and inform DFTF. +* Do not add wave front to wave front list - If not set it Will save each analyzed wave front while in the analysis loop to the wave front list. +* Show Best fit conic in trend display - if not set it will display the SA value after the null is removed. + + diff --git a/liveimageview.cpp b/liveimageview.cpp new file mode 100644 index 00000000..83577055 --- /dev/null +++ b/liveimageview.cpp @@ -0,0 +1,232 @@ +#include "liveimageview.h" +#include +#include +#include +#include +#include +#include +#include +LiveImageView::LiveImageView(QWidget *parent) : QLabel(parent) { + setFocusPolicy(Qt::StrongFocus); +} + +void LiveImageView::setZoomFactor(double zoom) { + m_zoomFactor = zoom; + update(); +} + +void LiveImageView::setGreenCircle(const QPointF ¢er, double radius) { + m_nativeCenter = center; + m_nativeRadius = radius; + m_hasCircle = true; + +} + +void LiveImageView::setYellowCircle(const QPointF ¢er, double radius) { + m_yellowCenter = center; + m_yellowRadius = radius; + m_hasYellowCircle = true; + +} + +QPoint LiveImageView::mapToImageCoordinates(const QPoint &widgetPos) const { + if (m_zoomFactor <= 0.0) return widgetPos; + int imgX = static_cast(widgetPos.x() / m_zoomFactor); + int imgY = static_cast(widgetPos.y() / m_zoomFactor); + return QPoint(imgX, imgY); +} + +void LiveImageView::mousePressEvent(QMouseEvent *event) { + QPoint clickImg = mapToImageCoordinates(event->pos()); + + if (event->button() == Qt::RightButton) { + if (m_hasYellowCircle) { + + m_state = InteractionState::ResizingYellowRadius; + setCursor(Qt::BlankCursor); + double dx = clickImg.x() - m_yellowCenter.x(); + double dy = clickImg.y() - m_yellowCenter.y(); + m_yellowRadius = std::hypot(dx, dy); + emit yellowRadiusChanged(m_yellowRadius); + return; + } + } + if (m_helpOverlay) + m_helpOverlay->hide(); + + if (event->button() == Qt::LeftButton and m_hasCircle) { + + m_state = InteractionState::DraggingGreenCenter; + m_dragOffsetImg = clickImg - m_nativeCenter.toPoint(); + setCursor(Qt::ClosedHandCursor); + event->accept(); + + emit outlineChanging(true); + return; + + + } +} + +void LiveImageView::mouseMoveEvent(QMouseEvent *event) { + QPoint currentPoint = mapToImageCoordinates(event->pos()); + + if (m_state == InteractionState::ResizingYellowRadius) { + double dx = currentPoint.x() - m_yellowCenter.x(); + double dy = currentPoint.y() - m_yellowCenter.y(); + m_yellowRadius = std::hypot(dx, dy); + emit yellowRadiusChanged(m_yellowRadius); + } + + else if (m_state == InteractionState::DraggingGreenCenter) { + m_nativeCenter = currentPoint - m_dragOffsetImg; + emit mirrorDefined(m_nativeCenter, m_nativeRadius); + } + +} + +void LiveImageView::mouseReleaseEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) { + + if (m_state == InteractionState::DraggingGreenCenter) { + + } + + m_state = InteractionState::None; + setCursor(Qt::ArrowCursor); + event->accept(); + } +} + +void LiveImageView::keyPressEvent(QKeyEvent *event) { + // Determine step size (e.g., hold Shift for a larger step, say 10 pixels) + double step = 1; + + + bool handled = true; + switch (event->key()) { + case Qt::Key_Left: + m_nativeCenter.rx() -= step; + break; + case Qt::Key_Right: + m_nativeCenter.rx() += step; + break; + case Qt::Key_Up: + m_nativeCenter.ry() -= step; + break; + case Qt::Key_Down: + m_nativeCenter.ry() += step; + break; + case Qt::Key_Plus: + m_nativeRadius += 1; + break; + case Qt::Key_Minus: + m_nativeRadius -= 1; + break; + case Qt::Key_Shift: + if (!event->isAutoRepeat()) { + emit shiftStateChanged(true); + } + break; + case Qt::Key_Slash: + if (m_helpOverlay && m_helpOverlay->isVisible()) { + m_helpOverlay->hide(); + }else if (m_helpOverlay){ + m_helpOverlay->show(); + } + break; + handled = false; // Let default handling run if needed + break; + default: + handled = false; + break; + } + + if (handled) { + update(); // Redraw the label to show the new point position + emit mirrorDefined(m_nativeCenter, m_nativeRadius); // Notify listeners + event->accept(); + } else { + QLabel::keyPressEvent(event); + } +} + +void LiveImageView::keyReleaseEvent(QKeyEvent *event) { + if (event->key() == Qt::Key_Shift) { + if (!event->isAutoRepeat()) { + emit shiftStateChanged(false); + emit mirrorDefined(m_nativeCenter, m_nativeRadius); + emit outlineChanging(false); + + } + } + QWidget::keyReleaseEvent(event); +} + +void LiveImageView::wheelEvent(QWheelEvent *event) { + int numDegres = event->angleDelta().y() / 8; + int numSteps = -numDegres / 15; + + if (numSteps == 0) return; + + // Check Yellow Circle Hover + if (m_hasYellowCircle) { + double scaleFactor = 1.0 + (numSteps * 0.05); + m_yellowRadius = std::max(5.0, m_yellowRadius * scaleFactor); + emit yellowRadiusChanged(m_yellowRadius); + event->accept(); + return; + } + +} + + + + +#include + +void LiveImageView::showEvent(QShowEvent *event) { + QLabel::showEvent(event); + + // If you only want this to happen the *first* time it's opened: + if (m_hasShownHelp) return; + m_hasShownHelp = true; + + // 1. Create the overlay lazily as a single QLabel if it doesn't exist yet + if (!m_helpOverlay) { + QLabel *helpLbl = new QLabel(this); + helpLbl->setAttribute(Qt::WA_TransparentForMouseEvents); // Let clicks pass through + + // Single stylesheet handles background, text color, rounded corners, and larger padding/font + helpLbl->setStyleSheet( + "background-color: rgba(0, 100, 100, 70);" // Slightly more opaque + "color: #ffffff;" + "border-radius: 8px;" + "padding: 16px;" // More padding for breathing room + "font-size: 20px;" // Increased base font size + ); + + // Use HTML formatting with a larger title + helpLbl->setText( + " Left Click + Drag: Adjust mirror outline green circle
" + " Right Click Set filter diameter.
" + " Mouse Wheel: Increase\\decrease filter
" + " Arrow Keys: Nudge green outline
" + " +\\- enlarge \\ decrease mirror outline
" + " Hold Shift: Toggle fullscreen view" + "

/ Toggle to display\\hide this or left mouse click to remove." + ); + + + + m_helpOverlay = helpLbl; + } + + // 2. Position and size it appropriately (auto-sizes to content, but we can set a fixed width/height) + m_helpOverlay->adjustSize(); + m_helpOverlay->move(10, 10); // Top-left corner with a 10px margin + m_helpOverlay->show(); + m_helpOverlay->raise(); + + +} diff --git a/liveimageview.h b/liveimageview.h new file mode 100644 index 00000000..2817d385 --- /dev/null +++ b/liveimageview.h @@ -0,0 +1,61 @@ +#ifndef LIVEIMAGEVIEW_H +#define LIVEIMAGEVIEW_H + +#include +#include +#include +#include +#include +class LiveImageView : public QLabel { + Q_OBJECT +public: + explicit LiveImageView(QWidget *parent = nullptr); + + void setZoomFactor(double zoom); + void setGreenCircle(const QPointF ¢er, double radius); + void setYellowCircle(const QPointF ¢er, double radius); + +signals: + void mirrorDefined(QPointF center, double radius); + void yellowRadiusChanged(int rad); + void requestZoomChange(double newZoom); + void outlineChanging(bool isManip); + void shiftStateChanged(bool pressed); + +protected: + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void showEvent(QShowEvent *event)override; +private: + QPoint mapToImageCoordinates(const QPoint &widgetPos) const; + bool m_hasShownHelp = false; + QLabel * m_helpOverlay = nullptr; + enum class InteractionState { + None, + DrawingGreenRadius, + DraggingGreenCenter, + ResizingYellowRadius + }; + + InteractionState m_state = InteractionState::None; + double m_zoomFactor = 1.0; + + // Green Circle Cache (Fully adjustable: center & radius) + QPointF m_nativeCenter; + QPointF m_firstEdgePoint; + double m_nativeRadius = 0.0; + bool m_hasCircle = false; + + // Yellow Circle Cache (Radius-only adjustment, fixed center from parent) + QPointF m_yellowCenter; + int m_yellowRadius = 10; + bool m_hasYellowCircle = false; + + QPoint m_dragOffsetImg; +}; + +#endif // LIVEIMAGEVIEW_H diff --git a/liveviewdialog.cpp b/liveviewdialog.cpp new file mode 100644 index 00000000..dd794f4c --- /dev/null +++ b/liveviewdialog.cpp @@ -0,0 +1,1235 @@ +#include "liveviewdialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "videostreamworker.h" +#include +#include +#include +#include +#include +#include +#include +#include "zernikes.h" +#include + + +// ========================================== +// LiveViewDialog Implementation +// ========================================== + +LiveViewDialog::LiveViewDialog(QWidget *parent) + : QDialog(parent), tabWidget(nullptr) { + + setWindowTitle("DFTFringe - Live View"); + setAttribute(Qt::WA_DeleteOnClose, true); + setWindowFlags(windowFlags() | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint); + QSettings settings; + + if (settings.contains("LiveViewDialog/geometry")) { + restoreGeometry(settings.value("LiveViewDialog/geometry").toByteArray()); + } else { + resize(600, 600); + } + QString savedUrl = settings.value("LiveView/streamUrl", 0).toString(); + // history plot + history = new liveViewHistory(); + initSettingsDialog(); + + + setupUI(savedUrl); + statusLeft->setText("Connecting to camera/stream... Please wait."); + // Initialize video stream thread and worker + m_thread = new QThread(this); + m_worker = new VideoStreamWorker(savedUrl); + m_worker->moveToThread(m_thread); + + connect(m_thread, &QThread::started, m_worker, &VideoStreamWorker::startStream); + connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater); + + qRegisterMetaType("cv::Mat"); + + // Single unified connection for frame handling and pulling the next frame + connect(m_worker, &VideoStreamWorker::frameReady, this, [this](cv::Mat frame) { + m_latestFrame = frame; + renderCurrentFrame(); + // Pull the next frame only after rendering finishes + }, Qt::QueuedConnection); + + connect(m_worker, &VideoStreamWorker::streamError, this, [this](const QString &msg) { + if (imageLabel) { + statusLeft->setWordWrap(true); + statusLeft->setText(QString("%1") + .arg(msg + " Go to settings to set the stream number. You may have to close and restart this dialog.")); + imageLabel->adjustSize(); + m_settingsDlg->exec(); + } + emit streamDisconnected(); + }); + + connect(this, &LiveViewDialog::requestChangeSource, m_worker, &VideoStreamWorker::changeSource, Qt::QueuedConnection); + connect(this, &LiveViewDialog::requestSetResolution, m_worker, &VideoStreamWorker::setResolution, Qt::QueuedConnection); + connect(this, &LiveViewDialog::requestFrame, m_worker, &VideoStreamWorker::fetchNextFrame, Qt::QueuedConnection); + connect(this, &LiveViewDialog::requestCameraSetting, m_worker, &VideoStreamWorker::requestCameraSetting); + + + // Trigger the very first frame pull once the worker confirms the stream is open + connect(m_worker, &VideoStreamWorker::streamStarted, this, [this]() { + + QSize selectedRes = resolutionCombo->currentData().toSize(); + QMetaObject::invokeMethod(m_worker, "setResolution", Qt::QueuedConnection, + Q_ARG(int, selectedRes.width()), + Q_ARG(int, selectedRes.height())); + statusLeft->setText("Stream Connected"); + + // + // brightness/gain slider + // + + double gain; + bool bAvailable; + bAvailable = m_worker->getCameraGainRange(m_gain_mode, m_gainMinimum, m_gainMaximum, gain); + if (m_gainMaximum != m_gainMinimum) + m_gain_scale = 20/(m_gainMaximum - m_gainMinimum); + + m_brightnessSlider->blockSignals(true); + m_brightnessSlider->setEnabled(bAvailable); + m_brightnessSlider->setRange(floor(m_gainMinimum * m_gain_scale), ceil(m_gainMaximum * m_gain_scale)); + m_brightnessSlider->setValue(round(gain * m_gain_scale)); + m_brightnessSlider->blockSignals(false); + + + // + // exposure slider + // + + double exp; + bAvailable = m_worker->getCameraExposureRange(m_exposureMinimum, m_exposureMaximum,exp); + if (m_exposureMaximum != m_exposureMinimum) + m_exposure_scale = 20/(m_exposureMaximum - m_exposureMinimum); + m_exposureSlider->blockSignals(true); + m_exposureSlider->setEnabled(bAvailable); + m_exposureSlider->setRange(floor(m_exposureMinimum * m_exposure_scale), ceil(m_exposureMaximum * m_exposure_scale)); + m_exposureSlider->setValue(round(exp * m_exposure_scale)); + m_exposureSlider->blockSignals(false); + + // auto exposure checkbox + bool bAuto; + m_worker->getAutoExpSetting(bAvailable, bAuto); + m_autoExposureCheck->blockSignals(true); + if (bAvailable == false) { + m_autoExposureCheck->setEnabled(false); + } else { + m_autoExposureCheck->setChecked(bAuto); + } + m_autoExposureCheck->blockSignals(false); + + + //emit requestFrame(); + // Configure it once during initialization + m_throttle.setSingleShot(true); + connect(&m_throttle, &QTimer::timeout, this, [this]() { + emit requestFrame(); + }); + m_throttle.start(10); + // Clear the status message after 5 seconds (5000 milliseconds) + QTimer::singleShot(5000, this, [this]() { + // Only clear if it still says "Stream Connected" so we don't overwrite a newer message + if (statusLeft->text() == "Stream Connected") { + statusLeft->clear(); + } + }); + }, Qt::QueuedConnection); + + // Kick off the thread start safely after checking user preferences + QTimer::singleShot(50, this, [this, savedUrl]() { + QSettings settings; + bool skipPrompt = settings.value("LiveView/skipUSBStartupPrompt", false).toBool(); + bool autoConnect = settings.value("LiveView/autoConnectUSB", true).toBool(); + + // If it targets default USB device "0" and the user hasn't suppressed prompts + if (!skipPrompt) { + QMessageBox msgBox(this); + msgBox.setWindowTitle("USB Camera Connection"); + msgBox.setText(QString("Attempt to connect to device %1 on startup?").arg(savedUrl)); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + + QCheckBox dontAskBox("Never ask me again", &msgBox); + msgBox.setCheckBox(&dontAskBox); + + int ret = msgBox.exec(); + if (dontAskBox.isChecked()) { + settings.setValue("LiveView/skipUSBStartupPrompt", true); + } + if (ret == QMessageBox::Yes) { + settings.setValue("LiveView/autoConnectUSB", true); + autoConnect = true; + } else { + settings.setValue("LiveView/autoConnectUSB", false); + autoConnect = false; + } + } + + // Only start the thread if allowed + if ((savedUrl != "0") || autoConnect) { + if (m_thread && !m_thread->isRunning()) { + m_thread->start(); + } + } else { + statusLeft->setText("USB connection skipped. Open settings to connect."); + } + }); + m_throttle.start(10); + if (m_fitToWindow) + setFitToWindowZoom(); +} + +LiveViewDialog::~LiveViewDialog() { + if (m_worker) { + m_worker->stop(); + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); // Wait safely for the background loop to exit + } +} + +void LiveViewDialog::closeEvent(QCloseEvent *event) { + + if (loopRunning) { + // 1. Tell the main window/loop to stop + m_stopRequested = true; + } else { + // Safe to close normally + loopRunning = false; + QSettings set; // Or use existing app settings key + set.setValue("LiveViewDialog/splitterGeometry", leftSplitter->saveState()); + set.setValue("LiveViewDialog/geometry", saveGeometry()); + } + hide(); + // ignore the close event so the object isn't destroyed out from under the loop yet + event->ignore(); +} + +double clamp_within(double val, double _min, double _max) { + return fmin(fmax(val, _min), _max); // keep val between min and max +} + +void LiveViewDialog::setupUI(const QString &defaultStreamUrl) { + setWindowTitle("DFTFringe - Live View"); + + QSettings set; + // Root layout for the dialog (Vertical) + QVBoxLayout *rootLayout = new QVBoxLayout(this); + rootLayout->setContentsMargins(8, 8, 8, 8); + rootLayout->setSpacing(6); + + // ========================================== + // TOP HEADER BAR (RMS & Average Mode) + // ========================================== + QWidget *headerWidget = new QWidget(this); + QHBoxLayout *headerLayout = new QHBoxLayout(headerWidget); + headerLayout->setContentsMargins(0, 0, 0, 0); + + averageMode = new QCheckBox("Compute and show average when loop is running", this); + averageMode->setChecked(set.value("liveViewAverageMode", false).toBool()); + connect(averageMode, &QCheckBox::stateChanged, this,[](int val){ + QSettings set; + set.setValue("liveViewAverageMode", val); + }); + + maxRMS = new QDoubleSpinBox(this); + maxRMS->setRange(0.0, 100.0); + maxRMS->setValue(.4); + maxRMS->setSingleStep(.05); + + headerLayout->addWidget(averageMode); + + headerLayout->addWidget(new QLabel("Delete if RMS >", this)); + headerLayout->addWidget(maxRMS); + headerLayout->addSpacing(100); + + rootLayout->addWidget(headerWidget); + + // ========================================== + // CENTER SPLITTER (Video Feed vs Sidebar) + // ========================================== + QSplitter *mainSplitter = new QSplitter(Qt::Horizontal, this); + leftSplitter = new QSplitter(Qt::Vertical, this); + if (set.contains("LiveViewDialog/splitterGeometry")) { + leftSplitter->restoreState(set.value("LiveViewDialog/splitterGeometry").toByteArray()); + } + // --- Left Side: Live Image View --- + QWidget *leftContainer = new QWidget(this); + QVBoxLayout *leftLayout = new QVBoxLayout(leftContainer); + leftLayout->setContentsMargins(0, 0, 0, 0); + imageSize = new QLabel(this); + statusRight = new QLabel(this); + statusLeft = new QLabel(this); + QHBoxLayout *statusLayout = new QHBoxLayout(); + statusLayout->addWidget(imageSize,0); + statusLayout->addWidget(statusLeft,10); + statusLeft->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + statusLayout->addWidget(statusRight,0); + statusLayout->addStretch(1); + leftLayout->addLayout(statusLayout); + imageLabel = new LiveImageView(this); + imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + imageLabel->setStyleSheet("background-color: black;"); + imageLabel->setAlignment(Qt::AlignCenter); + + connect(imageLabel, &LiveImageView::mirrorDefined, this, &LiveViewDialog::setOutsidecircle); + connect(imageLabel, &LiveImageView::yellowRadiusChanged, this, &LiveViewDialog::onYellowRadiusChanged); + connect(imageLabel, &LiveImageView::requestZoomChange, this, &LiveViewDialog::onRequestZoomChange); + + + connect(imageLabel, &LiveImageView::shiftStateChanged, this, [this](bool pressed) { + if (pressed) { + this->m_outlineChanging = true; + m_userMirrorRect = QRect(0,0,0,0); + setFitToWindowZoom(); + m_normalGeometry = geometry(); // Save current size right before expanding + showFullScreen(); + } else { + this->m_outlineChanging = false; + showNormal(); + setGeometry(m_normalGeometry); // Restore exact default size and position + setFitToWindowZoom(); + } + }); + scrollArea = new ResizableScrollArea(this); + scrollArea->setWidget(imageLabel); + scrollArea->setWidgetResizable(false); + scrollArea->setBackgroundRole(QPalette::Dark); + + // Wire up the resize callback to trigger your view update + scrollArea->onResized = [this]() { + if (zoomCombo && zoomCombo->currentData().toDouble() < 0) { // Check if "Fit to Window" is active (-1.0) + onZoomChanged(zoomCombo->currentIndex()); // Re-triggers your zoom adjustment with the new dimensions + } + }; + + + + leftSplitter->addWidget(scrollArea); + leftSplitter->addWidget(history); + leftSplitter->setSizes({2000, 10}); + + leftSplitter->setHandleWidth(4); // Sets the handle width to 8 pixels + leftSplitter->setStyleSheet( + "QSplitter::handle {" + " background-color: #ccffff;" // A clear, contrasting gray color + " border: 3px outset #004545;" + "}" + ); + + leftLayout->addWidget(leftSplitter); + + // --- Right Side: Scrollable Control Sidebar --- + QScrollArea *sidebarScrollArea = new QScrollArea(this); + sidebarScrollArea->setWidgetResizable(true); + sidebarScrollArea->setMinimumWidth(260); + sidebarScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + QWidget *sidebarContent = new QWidget(sidebarScrollArea); + QVBoxLayout *sidebarLayout = new QVBoxLayout(sidebarContent); + sidebarLayout->setContentsMargins(6, 6, 6, 6); + + // Zoom Control + sidebarLayout->addWidget(new QLabel("Zoom:", this)); + zoomCombo = new QComboBox(this); + zoomCombo->addItem("Fit to Window", -1.0); + zoomCombo->addItem("50%", 0.5); + zoomCombo->addItem("100%", 1.0); + zoomCombo->addItem("150%", 1.5); + zoomCombo->addItem("200%", 2.0); + zoomCombo->addItem("300%", 3.0); + zoomCombo->addItem("400%", 4.0); + zoomCombo->setCurrentIndex(set.value("liveViewZoom",1).toInt()); + onZoomChanged(zoomCombo->currentIndex()); + connect(zoomCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &LiveViewDialog::onZoomChanged); + sidebarLayout->addWidget(zoomCombo); + + // DFT Resolution Combo + sidebarLayout->addWidget(new QLabel("DFT Size:", this)); + dftresolutionCombo = new QComboBox(this); + dftresolutionCombo->addItem("256 x 256", 256); + dftresolutionCombo->addItem("512 x 512", 512); + dftresolutionCombo->addItem("1024 x 1024", 1024); + + dftresolutionCombo->setCurrentIndex(set.value("liveViewDftSize", 1).toInt()); + onDFTSizeChanged(dftresolutionCombo->currentIndex()); + connect(dftresolutionCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &LiveViewDialog::onDFTSizeChanged); + sidebarLayout->addWidget(dftresolutionCombo); + QGroupBox *DFTGroup = new QGroupBox("DFTColor", this); + QVBoxLayout *dftLayout = new QVBoxLayout(DFTGroup); + // DFT Floor values below this lever are transparent. + dftLayout->addWidget(new QLabel("DFT Transparancy:", this)); + DFTLowThreshold = new QSpinBox(this); + DFTLowThreshold->setRange(-1, 255); + DFTLowThreshold->setSpecialValueText("Auto"); + + DFTLowThreshold->setValue(set.value("liveViewDFTLow", -1).toInt()); + connect(DFTLowThreshold, QOverload::of(&QSpinBox::valueChanged), this, [=](int val) { + QSettings s; + s.setValue("liveViewDFTLow", val); + }); + dftLayout->addWidget(DFTLowThreshold); + + // DFT Contrast + dftLayout->addWidget(new QLabel("DFT Contrast:", this)); + vivid = new QDoubleSpinBox(this); + vivid->setValue(.7); + vivid->setSingleStep(.05); + dftLayout->addWidget(vivid); + sidebarLayout->addWidget(DFTGroup); + // Camera Hardware Sliders (Brightness / Exposure) + sidebarLayout->addWidget(new QLabel("Gain:", this)); + + // + // brightness slider + // + + m_brightnessSlider = new QSlider(Qt::Horizontal, this); + m_brightnessSlider->setRange(-64, 64); + m_brightnessSlider->setValue(0); + connect(m_brightnessSlider, &QSlider::valueChanged, this, [this](int val) { + // this was too slow: emit requestCameraSetting("Gain", clamp_within(val / m_gain_scale, m_gainMinimum, m_gainMaximum)); + m_worker->requestCameraSetting("Gain", clamp_within(val / m_gain_scale, m_gainMinimum, m_gainMaximum)); + }); + m_brightnessSlider->installEventFilter(this); + connect(this, &LiveViewDialog::gainSliderRightClicked, this, [this]() { + // User right clicked slider + bool ok; + QString currentVal=""; + struct VideoStreamWorker::CameraConfiguration config; + if (m_worker != nullptr && m_worker->getCameraConfiguration(config)) + currentVal = QString("%1").arg(config.gain); + QString text = QInputDialog::getText(this, "Gain", + QString("Gain (%1 to %2)").arg(m_gainMinimum).arg(m_gainMaximum), + QLineEdit::Normal, + currentVal, &ok); + if (ok && !text.isEmpty()) { + // User clicked OK and entered a value, use 'text' here + bool ok; + double val = text.toDouble(&ok); + if (ok) + emit requestCameraSetting("Gain", val); + } + }); + + + sidebarLayout->addWidget(m_brightnessSlider); + + QHBoxLayout *exposureGroupLayout = new QHBoxLayout(); + + // + // exposure slider + // + + sidebarLayout->addWidget(new QLabel("Exposure / Shutter:", this)); + m_exposureSlider = new QSlider(Qt::Horizontal, this); + m_exposureSlider->setRange(-16, 0); + m_exposureSlider->setValue(-6); + connect(m_exposureSlider, &QSlider::valueChanged, this, [this](int val) { + // this was too slow: emit requestCameraSetting("Exposure", clamp_within(val / m_exposure_scale, m_exposureMinimum, m_exposureMaximum)); + m_worker->requestCameraSetting("Exposure", clamp_within(val / m_exposure_scale, m_exposureMinimum, m_exposureMaximum)); + }); + m_exposureSlider->installEventFilter(this); + connect(this, &LiveViewDialog::expSliderRightClicked, this, [this]() { + // User right clicked slider + bool ok; + QString currentVal=""; + struct VideoStreamWorker::CameraConfiguration config; + if (m_worker != nullptr && m_worker->getCameraConfiguration(config)) + currentVal = QString("%1").arg(config.exposure); + QString text = QInputDialog::getText(this, "Exposure", + QString("Exposure (%1 to %2)").arg(m_exposureMinimum).arg(m_exposureMaximum), + QLineEdit::Normal, + currentVal, &ok); + if (ok && !text.isEmpty()) { + // User clicked OK and entered a value, use 'text' here + bool ok; + double val = text.toDouble(&ok); + if (ok) + emit requestCameraSetting("Exposure", val); + } + }); + exposureGroupLayout->addWidget(m_exposureSlider, 1); + + // auto exposure checkbox + m_autoExposureCheck = new QCheckBox("Auto", this); + connect(m_autoExposureCheck, &QCheckBox::stateChanged, this, [this]() { + m_worker->setAutoExpSetting(m_autoExposureCheck->isChecked()); + }); + + exposureGroupLayout->addWidget(m_autoExposureCheck,0); + + sidebarLayout->addLayout(exposureGroupLayout); + + + sidebarLayout->addStretch(1); + + // Popup Settings & Help Buttons + QPushButton *settingsBtn = new QPushButton("Settings...", this); + QPushButton *helpBtn = new QPushButton("Help & Instructions...", this); + + connect(settingsBtn, &QPushButton::clicked, this, [this, defaultStreamUrl]() { + m_settingsDlg->exec(); // Just show the persistent dialog + }); + + connect(helpBtn, &QPushButton::clicked, this, [this]() { + QDialog helpDlg(this); + helpDlg.setWindowTitle("Live View Help"); + + QVBoxLayout *dlgLayout = new QVBoxLayout(&helpDlg); + QTextBrowser *helpText = new QTextBrowser(&helpDlg); + helpText->setOpenExternalLinks(true); + + // Load cleanly from the Qt Resource System + QFile file(":/liveViewHelp.md"); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + helpText->setMarkdown(QString::fromUtf8(file.readAll())); + } else { + helpText->setPlainText("Error: Could not load help documentation."); + } + + dlgLayout->addWidget(helpText); + helpDlg.resize(1000, 800); + helpDlg.exec(); + }); + + + sidebarLayout->addWidget(settingsBtn); + sidebarLayout->addWidget(helpBtn); + + sidebarContent->setLayout(sidebarLayout); + sidebarScrollArea->setWidget(sidebarContent); + + mainSplitter->addWidget(leftContainer); + mainSplitter->addWidget(sidebarScrollArea); + + mainSplitter->setStretchFactor(0, 4); + mainSplitter->setStretchFactor(1, 1); + + rootLayout->addWidget(mainSplitter, 1); + + // ========================================== + // BOTTOM ACTION BAR + // ========================================== + QHBoxLayout *bottomControlLayout = new QHBoxLayout(); + + grabButton = new QPushButton("Grab Igram", this); + grabButton->setToolTip("Load igram from video stream"); + grabButton->setStyleSheet("QPushButton { background-color: #f39c12; color: white; font-weight: bold; }" + "QPushButton QWidget { color: black; }"); + connect(grabButton, &QPushButton::clicked, this, &LiveViewDialog::onGrabClicked); + + OutlineBtn = new QPushButton("Auto outline", this); + OutlineBtn->setStyleSheet( "QPushButton { background-color: #f39c12; color: white; font-weight: bold; }" + "QPushButton QWidget { color: black; }"); + OutlineBtn->setToolTip("auto outline the igram"); + + OutlineOkBtn = new QPushButton("Outline is ok",this); + OutlineOkBtn->setStyleSheet("QPushButton { background-color: #f39c12; color: white; font-weight: bold; }" + "QPushButton QWidget { color: black; }"); + OutlineOkBtn->setToolTip("Same as pressing the 'done' button on the igram outline control\n" + "Causes the DFT to be displayed in the usual analysis."); + + startAnalysisBtn = new QPushButton("Start Loop", this); + startAnalysisBtn->setStyleSheet("QPushButton { background-color: #f39c12; color: white; font-weight: bold; }" + "QPushButton QWidget { color: black; }"); + startAnalysisBtn->setToolTip("Run loop that processes current video frame and then the next one"); + startAnalysisBtn->hide(); + + pauseAnalyBtn = new QPushButton("Pause", this); + pauseAnalyBtn->setStyleSheet("QPushButton { background-color: #f39c12; color: white; font-weight: bold; }" + "QPushButton QWidget { color: black; }"); + pauseAnalyBtn->setToolTip("Pause the processing loop to makee some change without loosing the current average if averaging was enabled"); + pauseAnalyBtn->hide(); + + stopAnalysisBtn = new QPushButton("Stop Loop", this); + stopAnalysisBtn->setStyleSheet("QPushButton { background-color: #f39c12; color: white; font-weight: bold; }" + "QPushButton QWidget { color: black; }"); + stopAnalysisBtn->setToolTip("Stop the loop and save the average if averaging was being computed"); + stopAnalysisBtn->hide(); + + saveAverageBtn = new QPushButton("Save average", this); + saveAverageBtn->setStyleSheet("QPushButton { background-color: #f39c12; color: white; font-weight: bold; }" + "QPushButton QWidget { color: black; }"); + saveAverageBtn->setToolTip("save current average to wavefront list and continue looping"); + saveAverageBtn->hide(); + connect(saveAverageBtn, &QPushButton::clicked, this, [this](){ + this->saveAverage = true; + }); + connect(OutlineBtn, &QPushButton::clicked, this,[this](){ + emit autoOutLine(); + }); + connect(OutlineOkBtn, &QPushButton::clicked, this,[this](){ + emit outlineOk(); + startAnalysisBtn->show(); + OutlineBtn->hide(); + OutlineOkBtn->hide(); + + }); + + dftCheckBox = new QCheckBox("Show DFT", this); + dftCheckBox->setChecked(false); + connect(dftCheckBox, &QCheckBox::toggled, this, &LiveViewDialog::renderCurrentFrame); + bottomControlLayout->addWidget(grabButton); + bottomControlLayout->addWidget(OutlineBtn); + bottomControlLayout->addWidget(OutlineOkBtn); + bottomControlLayout->addWidget(startAnalysisBtn); + bottomControlLayout->addWidget(pauseAnalyBtn); + bottomControlLayout->addWidget(stopAnalysisBtn); + bottomControlLayout->addWidget(saveAverageBtn); + bottomControlLayout->addSpacing(20); + bottomControlLayout->addWidget(dftCheckBox); + bottomControlLayout->addStretch(1); + + rootLayout->addLayout(bottomControlLayout); + setLayout(rootLayout); +} +void LiveViewDialog::initSettingsDialog() { + m_settingsDlg = new QDialog(this); + m_settingsDlg->setWindowTitle("Live View Settings"); + QVBoxLayout *settingsLayout = new QVBoxLayout(m_settingsDlg); + QGroupBox *connectGroup = new QGroupBox("connecton",m_settingsDlg); + QVBoxLayout *connectLayout = new QVBoxLayout(connectGroup); + connectLayout->addWidget(new QLabel("Select or Manage Recent Streams / Camera IDs:", connectGroup)); + + urlListWidget = new QListWidget(connectGroup); + urlListWidget->setMaximumHeight(120); + + + QSettings settings; + QStringList urlHistory = settings.value("LiveView/urlHistory").toStringList(); + if (urlHistory.isEmpty()) { + urlHistory << "0" << "1" << "http://192.168.50.5:5000/video_feed"; + } + urlListWidget->addItems(urlHistory); + + QString currentUrl = settings.value("LiveView/streamUrl", urlHistory.first()).toString(); + QList matches = urlListWidget->findItems(currentUrl, Qt::MatchExactly); + if (!matches.isEmpty()) { + urlListWidget->setCurrentItem(matches.first()); + } else if (urlListWidget->count() > 0) { + urlListWidget->setCurrentRow(0); + } + + + urlLineEdit = new QLineEdit(); + urlLineEdit->setPlaceholderText("Or type a new URL / ID here..."); + urlLineEdit->setText(currentUrl); + connectLayout->addWidget(urlLineEdit); + connectLayout->addWidget(urlListWidget); + connect(urlLineEdit, &QLineEdit::editingFinished, this, &LiveViewDialog::restartStream); + connect(urlListWidget, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) { + statusLeft->setText("Connecting to camera/stream... Please wait."); + + if (m_thread && !m_thread->isRunning()) { + m_thread->start(); + } + if (item) { + urlLineEdit->setText(item->text().trimmed()); + onApplySettings(); + } + }); + QHBoxLayout *connectTimeoutLayout = new QHBoxLayout(); + connectTimeoutLayout->addWidget(new QLabel("number of blank frames before connect fails:")); + QSpinBox *frameTimeout = new QSpinBox(connectGroup); + frameTimeout->setValue(settings.value("liveViewConnectFrames",15).toInt()); + connectTimeoutLayout->addWidget(frameTimeout); + connectLayout->addLayout(connectTimeoutLayout); + + connect(frameTimeout, QOverload::of (&QSpinBox::valueChanged), this, [this](int val){ + QSettings set; + set.setValue("liveViewConnectFrames",val); + restartStream(); + }); + QGroupBox *rmsGroup = new QGroupBox("RMS settings", m_settingsDlg); + QVBoxLayout *rmsLayout = new QVBoxLayout(rmsGroup); + + + autoRMSatStarup = new QCheckBox("Compute Max rms as a percentage of first analysis", rmsGroup); + connect(autoRMSatStarup, &QCheckBox::toggled, this, [](bool checked) { + QSettings settings; + settings.setValue("liveViewAutoRMSCheckBox", checked); + }); + autoRMSatStarup->setChecked(settings.value("liveViewAutoRMSCheckBox", false).toBool()); + rmsLayout->addWidget(autoRMSatStarup); + + // make RMS margin settings; + QHBoxLayout *percentLayout = new QHBoxLayout(); + percentLayout->addWidget(new QLabel("Percent above RMS of first analysis:")); + + RMSMargin = new QDoubleSpinBox(); + connect(RMSMargin, QOverload::of (&QDoubleSpinBox::valueChanged), this, [](double val){ + QSettings settings; + settings.setValue("liveViewAutoRMSValue", val); + }); + RMSMargin->setSingleStep(.25); + RMSMargin->setValue(settings.value("liveViewAutoRMSValue", 1.5).toDouble()); + percentLayout->addWidget(RMSMargin); + percentLayout->addStretch((1)); + rmsLayout->addLayout(percentLayout); + + QHBoxLayout *resLayout = new QHBoxLayout(); + resLayout->addWidget(new QLabel("Camera Resolution:", m_settingsDlg)); + + resolutionCombo = new QComboBox(m_settingsDlg); + resolutionCombo->addItem("Default (Auto)", QSize(0, 0)); + resolutionCombo->addItem("640 x 480 (VGA)", QSize(640, 480)); + resolutionCombo->addItem("1280 x 720 (HD)", QSize(1280, 720)); + resolutionCombo->addItem("1280 x 960 (4:3)", QSize( 1280, 960)); + resolutionCombo->addItem("1920 x 1080 (FHD)", QSize(1920, 1080)); + resolutionCombo->addItem("2560 x 1440 (QHD)", QSize(2560, 1440)); + resolutionCombo->addItem("3840 x 2160 (4K)", QSize(3840, 2160)); + + QString savedResStr = settings.value("LiveView/resolution", "Default (Auto)").toString(); + int resIndex = resolutionCombo->findText(savedResStr); + if (resIndex != -1) { + resolutionCombo->setCurrentIndex(resIndex); + } + resLayout->addWidget(resolutionCombo); + resLayout->addStretch(1); + + connect(resolutionCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int){ + onApplySettings(); + }); + + + deleteIntermidiateWaveFront = new QCheckBox("Do not add Wave fronts to list except for averages", m_settingsDlg); + deleteIntermidiateWaveFront->setChecked(settings.value("LiveView/deleteIntermittent", false).toBool()); + connect(deleteIntermidiateWaveFront, &QCheckBox::toggled, this, [](bool checked) { + QSettings s; + s.setValue("LiveView/deleteIntermittent", checked); + }); + + // History trend setup + QHBoxLayout *zernlayout = new QHBoxLayout(); + zernListWidget = new QListWidget(); + zernListWidget->addItem("Best Fit Conic"); + int count = sizeof(zernsNames) / sizeof(zernsNames[0]); + // Populate the list widget directly from your array + for (int i = 0; i < count; ++i) { + zernListWidget->addItem(QString::fromUtf8(zernsNames[i])); + } + + // Get the saved trend name + QString savedTrend = settings.value("LiveView/TrendName", "Best Fit Conic").toString(); + history->setItem(savedTrend); + + // Find the item matching the saved text and set it as current/selected + QList matchingItems = zernListWidget->findItems(savedTrend, Qt::MatchExactly); + if (!matchingItems.isEmpty()) { + zernListWidget->setCurrentItem(matchingItems.first()); + } else { + zernListWidget->setCurrentRow(0); // Fallback to "Best Fit Conic" if not found + } + + connect(zernListWidget, &QListWidget::currentTextChanged, this, [this](QString name){ + history->setItem(name); + QSettings set; + set.setValue("LiveView/TrendName", name); + }); + + zernlayout->addWidget(new QLabel("Item to Track in trend graph:")); + zernlayout->addWidget(zernListWidget); + + settingsLayout->addWidget(connectGroup); + settingsLayout->addWidget(rmsGroup); + settingsLayout->addLayout(resLayout); + settingsLayout->addWidget(deleteIntermidiateWaveFront); + settingsLayout->addLayout(zernlayout); + settingsLayout->addStretch(); + + QDialogButtonBox *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, m_settingsDlg); + connect(btnBox, &QDialogButtonBox::accepted, m_settingsDlg, &QDialog::accept); + connect(btnBox, &QDialogButtonBox::rejected, m_settingsDlg, &QDialog::reject); + settingsLayout->addWidget(btnBox); + + m_settingsDlg->resize(550, 450); +} + +void LiveViewDialog::restartStream(){ + onApplySettings(); + +} + +void LiveViewDialog::onApplySettings() { + QString newUrl = urlLineEdit->text().trimmed(); + if (newUrl.isEmpty()) return; + + QSettings settings; + settings.setValue("LiveView/streamUrl", newUrl); + settings.setValue("LiveView/resolution", resolutionCombo->currentText()); + + // Tell the worker thread to switch sources safely + + if (m_worker) { + qDebug() << "apply emitting requestChangeSource:" << newUrl; + emit requestChangeSource(newUrl); + } + + // Apply selected resolution if valid + QSize selectedRes = resolutionCombo->currentData().toSize(); + + + if (m_worker && selectedRes.width() > 0 && selectedRes.height() > 0) { + QMetaObject::invokeMethod(m_worker, "setResolution", Qt::QueuedConnection, + Q_ARG(int, selectedRes.width()), + Q_ARG(int, selectedRes.height())); + setFitToWindowZoom(); + } +} +void LiveViewDialog::onGrabClicked() { + if (!m_latestFrame.empty()) { + emit igramCaptured(); // Emit a safe deep copy of the frame + OutlineBtn->show(); + OutlineOkBtn->show(); + startAnalysisBtn->hide(); + } +} + + +void LiveViewDialog::onDFTSizeChanged(int index){ + int data = dftresolutionCombo->itemData(index).toDouble(); + QSettings set; + set.setValue("liveViewDFTSize",index ); + m_dftSize = data; +} + +void LiveViewDialog::onZoomChanged(int index) { + double data = zoomCombo->itemData(index).toDouble(); + QSettings set; + set.setValue("liveViewZoom", index); + if (data < 0) { + m_fitToWindow = true; + setFitToWindowZoom(); + } else { + m_fitToWindow = false; + m_zoomFactor = data; + imageLabel->setZoomFactor(m_zoomFactor); + + // Re-apply scaled circle if we have one active + if (m_hasActiveCircle) { + + imageLabel->setGreenCircle(m_mirrorOutlineCenter , m_mirrorOutlineRadius); + } + + renderCurrentFrame(); + } +} +void LiveViewDialog::resizeEvent(QResizeEvent *event) { + QDialog::resizeEvent(event); + if (m_fitToWindow) + setFitToWindowZoom(); // Your custom routine +} + +void LiveViewDialog::setFitToWindowZoom() { + if (m_latestFrame.empty()) return; + + QSize viewportSize = scrollArea->viewport()->size(); + + // Default to full frame dimensions + int imgW = m_latestFrame.cols; + int imgH = m_latestFrame.rows; + + // Match the cropping logic from renderCurrentFrame() + if (!m_outlineChanging && m_mirrorOutlineRadius > 0) { + int pad = 10; + int xMin = static_cast(m_mirrorOutlineCenter.x() - m_mirrorOutlineRadius - pad); + int yMin = static_cast(m_mirrorOutlineCenter.y() - m_mirrorOutlineRadius - pad); + int xMax = static_cast(m_mirrorOutlineCenter.x() + m_mirrorOutlineRadius + pad); + int yMax = static_cast(m_mirrorOutlineCenter.y() + m_mirrorOutlineRadius + pad); + + cv::Rect desiredRoi(xMin, yMin, xMax - xMin, yMax - yMin); + cv::Rect cropRoi = desiredRoi & cv::Rect(0, 0, m_latestFrame.cols, m_latestFrame.rows); + + if (cropRoi.width > 0 && cropRoi.height > 0) { + imgW = cropRoi.width; + imgH = cropRoi.height; + } + m_userMirrorRect = QRect(cropRoi.x, cropRoi.y, cropRoi.width, cropRoi.height); + } else if (m_userMirrorRect.width() > 0 && m_userMirrorRect.height() > 0) { + int pad = 10; + cv::Rect desiredRoi(m_userMirrorRect.x() - pad, m_userMirrorRect.y() - pad, + m_userMirrorRect.width() + 2 * pad, m_userMirrorRect.height() + 2 * pad); + cv::Rect cropRoi = desiredRoi & cv::Rect(0, 0, m_latestFrame.cols, m_latestFrame.rows); + + if (cropRoi.width > 0 && cropRoi.height > 0) { + imgW = cropRoi.width; + imgH = cropRoi.height; + } + } + + if (imgW <= 0 || imgH <= 0) return; + + double scaleX = static_cast(viewportSize.width()) / imgW; + double scaleY = static_cast(viewportSize.height()) / imgH; + + m_zoomFactor = std::min(scaleX, scaleY); + // Clamp to a reasonable minimum + m_zoomFactor = std::max(0.1, m_zoomFactor); + + // Re-apply scaled circle if we have one active + if (m_hasActiveCircle) { + imageLabel->setGreenCircle(m_mirrorOutlineCenter, m_mirrorOutlineRadius); + } + imageLabel->setZoomFactor(m_zoomFactor); + renderCurrentFrame(); +} +void LiveViewDialog::setCenterFilter(double spatialFreqBin) { + m_centerFilterRadius = spatialFreqBin ; + + int centerx = m_userMirrorRect.width() / 2; + int centery = m_userMirrorRect.height() / 2; + + double scaledMirrorRadius = m_mirrorOutlineRadius * m_DftToimageDownScale; + double binDftSpace = m_centerFilterRadius * (static_cast(m_dftSize) / (scaledMirrorRadius * 2.0)); + int bin = static_cast(binDftSpace * m_DFTscale); + + m_binToBinScale = spatialFreqBin/ static_cast(bin); + imageLabel->setYellowCircle(QPointF(centerx,centery),bin); +} + +void LiveViewDialog::onResolutionChanged(int index) { + m_dftSize = resolutionCombo->itemData(index).toInt(); +} + +QRect LiveViewDialog::getMirrorRect(){ + int radius = m_mirrorOutlineRadius + 1; + int left = m_mirrorOutlineCenter.x() - radius; + int top = m_mirrorOutlineCenter.y() - radius; + return QRect(left, top, radius * 2, radius * 2); +} + + +void LiveViewDialog::onRequestZoomChange(double){ + +} + +void LiveViewDialog::onYellowRadiusChanged(int rad){ + int outbin = rad * m_binToBinScale; + emit blueCircle(outbin); +} + + + +#include // Make sure this is included in your cpp file if not already + +void LiveViewDialog::renderCurrentFrame() { + if (m_latestFrame.empty()) return; + + // Start the high-resolution timer + QElapsedTimer timer; + timer.start(); + + static int cnt = 0; + cv::Mat displayMat = m_latestFrame.clone(); + if (displayMat.channels() == 1) { + cv::cvtColor(displayMat, displayMat, cv::COLOR_GRAY2BGR); + } + + // 1. Determine ROI around the mirror outline + 10 pixel padding if outline exists + cv::Rect cropRoi(0, 0, displayMat.cols, displayMat.rows); + bool hasOutline = (m_mirrorOutlineRadius > 0) && !m_outlineChanging; + + if (hasOutline) { + int pad = 10; + int xMin = static_cast(m_mirrorOutlineCenter.x() - m_mirrorOutlineRadius - pad); + int yMin = static_cast(m_mirrorOutlineCenter.y() - m_mirrorOutlineRadius - pad); + int xMax = static_cast(m_mirrorOutlineCenter.x() + m_mirrorOutlineRadius + pad); + int yMax = static_cast(m_mirrorOutlineCenter.y() + m_mirrorOutlineRadius + pad); + + cv::Rect desiredRoi(xMin, yMin, xMax - xMin, yMax - yMin); + cropRoi = desiredRoi & cv::Rect(0, 0, displayMat.cols, displayMat.rows); + + if (cropRoi.width > 0 && cropRoi.height > 0) { + displayMat = displayMat(cropRoi).clone(); + } else { + cropRoi = cv::Rect(0, 0, displayMat.cols, displayMat.rows); + } + } + + if (dftCheckBox->isChecked()) { + // 2. Compute raw DFT + cv::Mat dftRaw = computeLiveDFT(m_latestFrame, m_dftSize, m_userMirrorRect); + if (dftRaw.empty()) return; + + // 3. Convert to float and apply logarithmic scaling on the raw DFT directly + cv::Mat dftFloat; + dftRaw.convertTo(dftFloat, CV_32F); + + cv::Mat dftLog = dftFloat; + + // 4. Compute statistics on the true DFT data + double minVal, maxVal; + cv::minMaxLoc(dftLog, &minVal, &maxVal); + + cv::Scalar meanVal, stdDevVal; + cv::meanStdDev(dftLog, meanVal, stdDevVal); + + // 5. Clip dynamic range based on statistics + double floorVal = meanVal[0] + 2 * stdDevVal[0]; + + int val = DFTLowThreshold->value(); + if (val != -1) { + floorVal = val; + } + + double ceilVal = meanVal[0] + (maxVal - meanVal[0])/vivid->value(); + + cv::Mat dftClamped; + cv::threshold(dftLog, dftClamped, floorVal, 0, cv::THRESH_TOZERO); + + cv::Mat dftShifted = dftClamped - floorVal; + double range = ceilVal - floorVal; + if (range < 1e-5) range = 1.0; + + cv::Mat dftNorm; + dftShifted.convertTo(dftNorm, CV_8U, 255.0 / range); + + // 6. Apply Jet colormap to the square DFT + cv::Mat dftColorSquare; + cv::applyColorMap(dftNorm, dftColorSquare, cv::COLORMAP_JET); + + cv::Mat alphaMaskSquare; + dftNorm.convertTo(alphaMaskSquare, CV_32F, 1.0 / 255.0); + + // 7. Resize and center to fit the cropped displayMat while keeping aspect ratio + m_DFTscale = std::min( + static_cast(displayMat.cols) / dftColorSquare.cols, + static_cast(displayMat.rows) / dftColorSquare.rows + ); + + int newWidth = static_cast(dftColorSquare.cols * m_DFTscale); + int newHeight = static_cast(dftColorSquare.rows * m_DFTscale); + + int x = (displayMat.cols - newWidth) / 2; + int y = (displayMat.rows - newHeight) / 2; + cv::Rect roi(x, y, newWidth, newHeight); + + roi &= cv::Rect(0, 0, displayMat.cols, displayMat.rows); + if (roi.width <= 0 || roi.height <= 0) return; + + cv::Mat resizedColor, resizedAlpha; + cv::resize(dftColorSquare, resizedColor, roi.size(), 0, 0, cv::INTER_LINEAR); + cv::resize(alphaMaskSquare, resizedAlpha, roi.size(), 0, 0, cv::INTER_LINEAR); + + // 8. Localized Alpha Blending + cv::Mat scaledAlpha; + cv::multiply(resizedAlpha, 2.7, scaledAlpha); + cv::threshold(scaledAlpha, scaledAlpha, 1.0, 1.0, cv::THRESH_TRUNC); + + cv::Mat bgFloat, fgFloat; + displayMat(roi).convertTo(bgFloat, CV_32FC3); + resizedColor.convertTo(fgFloat, CV_32FC3); + + std::vector alphaChannels(3, scaledAlpha); + cv::Mat alpha3C; + cv::merge(alphaChannels, alpha3C); + + cv::Mat blended; + cv::multiply(fgFloat, alpha3C, fgFloat); + cv::multiply(bgFloat, cv::Scalar::all(1.0) - alpha3C, bgFloat); + cv::add(bgFloat, fgFloat, blended); + + blended.convertTo(displayMat(roi), CV_8UC3); + } + + // Display via OpenCV conversion + QImage img = matToQImage(displayMat); + QPainter dftpainter(&img); + + if (dftCheckBox->isChecked() && !m_outlineChanging) { + dftpainter.setBrush(QColor(0, 0, 100, 70)); + dftpainter.setPen(QPen(Qt::yellow, 2)); + int centerx = img.width() / 2; + int centery = img.height() / 2; + + double effectiveMirrorRadius = m_mirrorOutlineRadius * m_DftToimageDownScale; + double binDftSpace = m_centerFilterRadius * (static_cast(m_dftSize) / (effectiveMirrorRadius * 2.0)); + int bin = static_cast(binDftSpace * m_DFTscale); + + dftpainter.drawEllipse(QPointF(centerx, centery), bin, bin); + } + + if (m_mirrorOutlineRadius != 0) { + dftpainter.setPen(QPen(Qt::green, 2)); + QPointF adjustedCenter(m_mirrorOutlineCenter.x() - cropRoi.x, m_mirrorOutlineCenter.y() - cropRoi.y); + dftpainter.drawEllipse(adjustedCenter, m_mirrorOutlineRadius, m_mirrorOutlineRadius); + } + + int targetWidth = static_cast(img.width() * m_zoomFactor); + int targetHeight = static_cast(img.height() * m_zoomFactor); + + imageSize->setText(QString("%1 x %2 frame:%3") + .arg(img.size().width()) + .arg(img.size().height()) + .arg(cnt++)); + + imageLabel->setPixmap(QPixmap::fromImage(img).scaled(targetWidth, targetHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + imageLabel->resize(targetWidth, targetHeight); + imageLabel->repaint(); + + // Calculate elapsed time (in milliseconds with fractional decimal places) + //double elapsedMs = static_cast(timer.nsecsElapsed()) / 1000000.0; + + // Append the render time to statusLeft text + if (statusLeft) { + //QString baseStatus = statusLeft->text(); // Or whatever base text statusLeft normally holds + //statusLeft->setText(QString(" Time: %1 ms").arg(elapsedMs, 0, 'f', 2)); + } + + m_throttle.start(10); +} + +cv::Mat LiveViewDialog::computeLiveDFT(const cv::Mat &inputFrame, int targetSize, const QRect &roi) { + cv::Mat gray; + if (inputFrame.channels() == 3) { + cv::cvtColor(inputFrame, gray, cv::COLOR_BGR2GRAY); + } else { + gray = inputFrame; + } + + cv::Mat workingArea; + if (roi.isValid() && roi.width() > 10 && roi.height() > 10) { + QRect clamped = roi.intersected(QRect(0, 0, gray.cols, gray.rows)); + if (!clamped.isEmpty()) { + cv::Rect cvRoi(clamped.x(), clamped.y(), clamped.width(), clamped.height()); + workingArea = gray(cvRoi).clone(); + + cv::Point center(workingArea.cols / 2, workingArea.rows / 2); + int radius = std::min(center.x, center.y); + cv::Mat mask = cv::Mat::zeros(workingArea.size(), CV_8UC1); + cv::circle(mask, center, radius, cv::Scalar(255), -1); + workingArea.setTo(0, mask == 0); + } else { + workingArea = gray; + } + } else { + workingArea = gray; + } + + // 1. Calculate uniform scale, ensuring we ONLY scale down (cap scale at 1.0) + int maxDim = std::min(workingArea.cols, workingArea.rows); + double scale = static_cast(targetSize) / maxDim; + if (scale > 1.0) { + scale = 1.0; + } + + // Save to member variable so display code can use it for PPM/circle scaling + m_DftToimageDownScale = scale; + + int newW = std::round(workingArea.cols * scale); + int newH = std::round(workingArea.rows * scale); + + // Safety fallback if dimensions are somehow invalid + if (newW <= 0) newW = 1; + if (newH <= 0) newH = 1; + + cv::Mat resized; + cv::resize(workingArea, resized, cv::Size(newW, newH), 0, 0, cv::INTER_AREA); + + // 2. Create target canvas and center the resized image with strict bounds protection + cv::Mat padded = cv::Mat::zeros(targetSize, targetSize, workingArea.type()); + + int xOffset = std::max(0, (targetSize - newW) / 2); + int yOffset = std::max(0, (targetSize - newH) / 2); + + int copyW = std::min(newW, targetSize - xOffset); + int copyH = std::min(newH, targetSize - yOffset); + + if (copyW > 0 && copyH > 0 && xOffset >= 0 && yOffset >= 0 && + (xOffset + copyW) <= targetSize && (yOffset + copyH) <= targetSize) { + cv::Rect targetRoi(xOffset, yOffset, copyW, copyH); + cv::Rect sourceRoi(0, 0, copyW, copyH); + resized(sourceRoi).copyTo(padded(targetRoi)); + } + + cv::Mat floatImg; + padded.convertTo(floatImg, CV_32F); + + cv::Mat complexImg; + cv::dft(floatImg, complexImg, cv::DFT_COMPLEX_OUTPUT); + + std::vector planes; + cv::split(complexImg, planes); + cv::Mat mag; + cv::magnitude(planes[0], planes[1], mag); + + mag += cv::Scalar::all(1); + cv::log(mag, mag); + + mag = mag(cv::Rect(0, 0, mag.cols & -2, mag.rows & -2)); + int cx = mag.cols / 2; + int cy = mag.rows / 2; + + cv::Mat q0(mag, cv::Rect(0, 0, cx, cy)); + cv::Mat q1(mag, cv::Rect(cx, 0, cx, cy)); + cv::Mat q2(mag, cv::Rect(0, cy, cx, cy)); + cv::Mat q3(mag, cv::Rect(cx, cy, cx, cy)); + + cv::Mat tmp; + q0.copyTo(tmp); + q3.copyTo(q0); + tmp.copyTo(q3); + + q1.copyTo(tmp); + q2.copyTo(q1); + tmp.copyTo(q2); + + cv::normalize(mag, mag, 0, 255, cv::NORM_MINMAX, CV_8UC1); + return mag; +} + +QImage LiveViewDialog::matToQImage(const cv::Mat &mat) { + if (mat.type() == CV_8UC3) { + cv::Mat rgb; + cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); + return QImage(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888).copy(); + } else if (mat.type() == CV_8UC1) { + return QImage(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8).copy(); + } + return QImage(); +} + +void LiveViewDialog::setOutsidecircle(QPointF center, double radius) { + m_mirrorOutlineCenter = center; + m_mirrorOutlineRadius = radius; + m_hasActiveCircle = true; + + + imageLabel->setGreenCircle(center, radius); + setFitToWindowZoom(); +} + +bool LiveViewDialog::eventFilter(QObject *obj, QEvent *event) { + // Check if the event comes from an object we care about + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *mouseEvent = static_cast(event); + + // Check if it was a right-click + if (mouseEvent->button() == Qt::RightButton) { + if (obj == m_exposureSlider) + emit expSliderRightClicked(); + if (obj == m_brightnessSlider) + emit gainSliderRightClicked(); + + return true; // Handled the event, don't pass it further + } + } + + // Pass unhandled events to the base class + return QWidget::eventFilter(obj, event); +} diff --git a/liveviewdialog.h b/liveviewdialog.h new file mode 100644 index 00000000..5559903e --- /dev/null +++ b/liveviewdialog.h @@ -0,0 +1,168 @@ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "liveimageview.h" +#include +#include +#include +#include +#include +#include "videostreamworker.h" +#include +#include +#include "liveviewhistory.h" +#include +class FrameProcessorWorker; + +class ResizableScrollArea : public QScrollArea { + Q_OBJECT +public: + explicit ResizableScrollArea(QWidget *parent = nullptr) : QScrollArea(parent) {} + + std::function onResized; + +protected: + void resizeEvent(QResizeEvent *event) override { + QScrollArea::resizeEvent(event); + if (onResized) { + onResized(); // Triggers your fit-to-window or resize logic + } + } +}; + +class LiveViewDialog : public QDialog { + Q_OBJECT +public: + explicit LiveViewDialog( QWidget *parent = nullptr); + ~LiveViewDialog(); + QImage getFrame() {return matToQImage(m_latestFrame.clone());}; +protected: + void closeEvent(QCloseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; +private slots: + void onResolutionChanged(int index); + void onZoomChanged(int index); + void setFitToWindowZoom(); + void onYellowRadiusChanged(int radius); + void onRequestZoomChange(double newZoom); + void onApplySettings(); + void onDFTSizeChanged(int index); + void restartStream(); +public slots: + void setOutsidecircle(QPointF center, double radius); + void onGrabClicked(); + void setCenterFilter(double freqBin) ; +signals: + void gainSliderRightClicked(); + void expSliderRightClicked(); + void igramCaptured(); + void streamDisconnected(); + void requestChangeSource(QString newSource); + void requestSetResolution(int width, int height); + void stopLiveLoopRequested(); + void requestFrame(); + void requestCameraSetting(QString prop , double val); + void outline(QPointF center, double rad); + void blueCircle(double rad); + void autoOutLine(); + void outlineOk(); +public: + QPushButton *startAnalysisBtn; + QPushButton *stopAnalysisBtn; + QPushButton *pauseAnalyBtn; + QPushButton *grabButton; + QPushButton *OutlineBtn; + QPushButton *OutlineOkBtn; + QSlider *brightnessSlider; + QCheckBox *deleteIgramAfter; + QDoubleSpinBox *maxRMS; + QCheckBox *averageMode; + QLabel *imageSize; + QLabel *statusLeft; + QLabel *statusRight; + bool m_tmpShowLive = false; + QCheckBox *autoRMSatStarup; + QDoubleSpinBox *RMSMargin; + QCheckBox *deleteIntermidiateWaveFront = nullptr; + + bool FirstWaveFrontSeen = false; + bool saveAverage = false; + QPushButton *saveAverageBtn; + bool loopRunning = false; + bool m_stopRequested = false;// used to signal the dialog is trying to close. + bool FirsdtWaveFrontSeen = false; + LiveImageView *imageLabel; + liveViewHistory *history; + bool m_showBestFit = true; + QListWidget *zernListWidget; +private: + VideoStreamWorker *m_worker; + QThread *m_thread; + QThread *m_procThread; + FrameProcessorWorker *m_processor; + + ResizableScrollArea *scrollArea; + QCheckBox *dftCheckBox; + + + QListWidget *urlListWidget; + QLineEdit *urlLineEdit; + QTabWidget *tabWidget; + QDoubleSpinBox *vivid; // makes the DFT Vivid; + QSplitter *leftSplitter; + QSpinBox *DFTLowThreshold; + + int m_centerFilterRadius = 0; + double m_RMSMargin = 1.; + QTimer *m_rmsTimer; + + QComboBox *resolutionCombo; + QComboBox *dftresolutionCombo; + QComboBox *zoomCombo; + + + cv::VideoCapture cap; + + int m_dftSize = 1024; + double m_zoomFactor = 1.0; + QImage m_latestImage; + cv::Mat m_latestFrame; + QRect m_userMirrorRect; + + QImage matToQImage(const cv::Mat &mat); + cv::Mat computeLiveDFT(const cv::Mat &inputFrame, int targetSize, const QRect &roi); + void renderCurrentFrame(); + QRect getMirrorRect(); + void setupUI(const QString &defaultStreamUrl); + void initSettingsDialog(); + QDialog *m_settingsDlg; + double m_dftPercent = .7; + double m_DFTscale = 1.; + bool m_hasActiveCircle = false; + bool m_fitToWindow = true; + bool m_outlineChanging = false; + + QPointF m_mirrorOutlineCenter; + double m_mirrorOutlineRadius = 0.; + double m_DftToimageDownScale = 1.; + QTimer m_throttle; + double m_binToBinScale = 1.; + QRect m_normalGeometry; + double m_exposureMinimum=-16; + double m_exposureMaximum=3; + double m_gainMinimum=0; + double m_gainMaximum=255; + bool m_gain_mode; + double m_gain_scale=1; + double m_exposure_scale=1; + QSlider * m_exposureSlider = nullptr; + QSlider * m_brightnessSlider = nullptr; + QCheckBox * m_autoExposureCheck = nullptr; +}; diff --git a/liveviewhistory.cpp b/liveviewhistory.cpp new file mode 100644 index 00000000..29e6c912 --- /dev/null +++ b/liveviewhistory.cpp @@ -0,0 +1,189 @@ +#include "liveviewhistory.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +liveViewHistory::liveViewHistory(QWidget *parent) : QWidget(parent) { + // Force the widget's background color to match the dark theme edge-to-edge + setStyleSheet("background-color: #2D2D30; color: #DCDCDC;"); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(4, 4, 4, 4); // Small clean padding inside splitter + mainLayout->setSpacing(4); + + // Setup Chart + chart = new QChart(); + chart->setTitle(QString("Trend")); + chart->setMargins(QMargins(0, 0, 0, 0)); + // Configure Legend Appearance for High Contrast + chart->legend()->setVisible(true); + chart->legend()->setAlignment(Qt::AlignTop); + chart->legend()->setLabelBrush(QBrush(QColor(240, 240, 240))); // Bright off-white for maximum legibility + chart->legend()->setBackgroundVisible(false); + // Dark Theme Backgrounds + chart->setBackgroundBrush(QBrush(QColor(45, 45, 48))); + chart->setPlotAreaBackgroundBrush(QBrush(QColor(30, 30, 30))); + chart->setPlotAreaBackgroundVisible(true); + chart->setTitleBrush(QBrush(QColor(220, 220, 220))); + + // Series Setup + rmsSeries = new QLineSeries(); + rmsSeries->setName("Avg RMS"); + + QPen rmsPen(QColor(51, 181, 229)); + rmsPen.setWidth(2); + rmsSeries->setPen(rmsPen); + + saSeries = new QLineSeries(); + saSeries->setName(m_itemName); + QPen saPen(QColor(255, 187, 51)); + saPen.setWidth(2); + saSeries->setPen(saPen); + + chart->addSeries(rmsSeries); + chart->addSeries(saSeries); + + // Axes Setup + axisX = new QValueAxis(); + axisX->setTitleText("minutes"); + axisX->setRange(0, 1); + chart->addAxis(axisX, Qt::AlignBottom); + rmsSeries->attachAxis(axisX); + saSeries->attachAxis(axisX); + + axisY_RMS = new QValueAxis(); + axisY_RMS->setTitleText("Avg RMS"); + axisY_RMS->setTickCount(5); + axisY_RMS->setRange(0, 1); + + chart->addAxis(axisY_RMS, Qt::AlignLeft); + rmsSeries->attachAxis(axisY_RMS); + + axisY_SA = new QValueAxis(); + axisY_SA->setTitleText(m_itemName); + axisY_SA->setTickCount(5); + axisY_SA->setRange(-100, 100); + chart->addAxis(axisY_SA, Qt::AlignRight); + saSeries->attachAxis(axisY_SA); + + // Style Axes + QColor axisColor(180, 180, 180); + QColor gridColor(60, 60, 60); + + auto styleAxis = [axisColor, gridColor](QValueAxis *axis) { + axis->setLabelsColor(axisColor); + axis->setTitleBrush(QBrush(axisColor)); + axis->setLinePen(QPen(axisColor)); + axis->setGridLinePen(QPen(gridColor, 1, Qt::DashLine)); + }; + + styleAxis(axisX); + styleAxis(axisY_RMS); + styleAxis(axisY_SA); + + chartView = new QChartView(chart); + chartView->setRenderHint(QPainter::Antialiasing); + chartView->setFrameShape(QFrame::NoFrame); + mainLayout->addWidget(chartView); + + // Reset Control Button & Connection + QPushButton *resetBtn = new QPushButton("Reset Data", this); + resetBtn->setStyleSheet("background-color: #3E3E42; color: white; border: 1px solid #555; padding: 5px;"); + connect(resetBtn, &QPushButton::clicked, this, &liveViewHistory::onResetClicked); + mainLayout->addWidget(resetBtn); + + resize(800, 500); +} + +void liveViewHistory::addSample(double rawRms, double rawSa) { + QDateTime currentTime = QDateTime::currentDateTime(); + + if (!firstSampleTime.isValid()) { + firstSampleTime = currentTime; + } + + double elapsedMinutes = static_cast(firstSampleTime.msecsTo(currentTime)) / 60000.0; + + rawRmsData.append(rawRms); + rawSaData.append(rawSa); + + double avgRms = computeRunningAverage(rawRmsData, 5); + double avgSa = computeRunningAverage(rawSaData, 10); + + rmsSeries->append(elapsedMinutes, avgRms); + saSeries->append(elapsedMinutes, avgSa); // fixed syntax + + axisX->setRange(0, std::max(1.0, elapsedMinutes)); + + // --- Rounded RMS Axis Range --- + double maxRms = 0.001; // prevent zero bounds + for (const auto &point : rmsSeries->points()) { + if (point.y() > maxRms) maxRms = point.y(); + } + + // Snap maxRms to a clean upper bound (e.g., multiples of 0.05 or 0.1) + double targetRmsMax = maxRms * 1.15; + double rmsUpper = std::ceil(targetRmsMax * 20.0) / 20.0; // Snaps to nearest 0.05 + if (rmsUpper < 0.05) rmsUpper = 0.05; + axisY_RMS->setRange(0, rmsUpper); + + // --- Rounded SA / Zernike Axis Range --- + if (!saSeries->points().isEmpty()) { + double minSa = saSeries->points().at(0).y(); + double maxSa = saSeries->points().at(0).y(); + for (const auto &point : saSeries->points()) { + if (point.y() < minSa) minSa = point.y(); + if (point.y() > maxSa) maxSa = point.y(); + } + + double span = std::max(0.01, maxSa - minSa); + double padding = span * 0.15; + + // Snap min/max to clean decimals + double saMin = std::floor((minSa - padding) * 10.0) / 10.0; + double saMax = std::ceil((maxSa + padding) * 10.0) / 10.0; + if (saMin == saMax) { saMin -= 1.0; saMax += 1.0; } + + axisY_SA->setRange(saMin, saMax); + } +} + +void liveViewHistory::setItem(QString name) { + m_itemName = name; // ensure member variable updates if you track it + saSeries->setName(name); + axisY_SA->setTitleText(QString("live %1").arg(name)); + + if (name == "Best Fit Conic"){ + axisY_SA->setRange(-2.0, 2.0); // Clean bounds + } + else{ + axisY_SA->setRange(-10.0, 10.0); // Clean bounds + } + onResetClicked(); +} + +void liveViewHistory::onResetClicked() { + rawRmsData.clear(); + rawSaData.clear(); + rmsSeries->clear(); + saSeries->clear(); + firstSampleTime = QDateTime(); // Reset start time so the next sample acts as the new epoch + axisX->setRange(0, 1); + axisY_RMS->setRange(0, 1); + axisY_SA->setRange(-100, 100); +} + +double liveViewHistory::computeRunningAverage(const QVector &data, int windowSize) { + int start = std::max(0, (int)(data.size() - windowSize)); + int count = data.size() - start; + double sum = 0.0; + for (int i = start; i < data.size(); ++i) { + sum += data[i]; + } + return count > 0 ? sum / count : 0.0; +} diff --git a/liveviewhistory.h b/liveviewhistory.h new file mode 100644 index 00000000..035cab27 --- /dev/null +++ b/liveviewhistory.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +QT_CHARTS_USE_NAMESPACE +#endif + +class liveViewHistory : public QWidget { + Q_OBJECT +public: + explicit liveViewHistory(QWidget *parent = nullptr); + void addSample(double rawRms, double rawSa); + void setItem(QString name); + QString m_itemName; +private slots: + void onResetClicked(); + +private: + double computeRunningAverage(const QVector &data, int windowSize); + + QChart *chart; + QChartView *chartView; + QLineSeries *rmsSeries; + QLineSeries *saSeries; + QValueAxis *axisX; + QValueAxis *axisY_RMS; + QValueAxis *axisY_SA; + + QVector rawRmsData; + QVector rawSaData; + + QDateTime firstSampleTime; // Tracks the start time reference +}; diff --git a/mainwindow.cpp b/mainwindow.cpp index 86c48b67..9c33bcc3 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -56,7 +56,8 @@ #include "zapm_interface.h" #include "zernikeprocess.h" - +#include "liveimageview.h" +#include "liveviewdialog.h" using namespace QtConcurrent; std::vector g_wavefronts; @@ -76,7 +77,6 @@ MainWindow::MainWindow(QWidget *parent) : a->menu()->setToolTipsVisible(true); } spdlog::get("logger")->info("DFTFringe {} started", APP_VERSION); - //const QString toolButtonStyle("QToolButton {" // "border-style: outset;" // "border-width: 3px;" @@ -192,7 +192,7 @@ MainWindow::MainWindow(QWidget *parent) : m_surfaceManager = SurfaceManager::get_instance(this,m_surfTools, m_profilePlot, m_contourView, m_ogl->m_surface, metrics); connect(m_contourView, &contourView::showAllContours, m_surfaceManager, &SurfaceManager::showAllContours); - connect(m_dftArea, &DFTArea::newWavefront, m_surfaceManager, &SurfaceManager::createSurfaceFromPhaseMap); + connect(m_dftArea, &DFTArea::newWavefront, m_surfaceManager, &SurfaceManager::createWaveFrontFromPhaseMap); connect(m_surfaceManager, &SurfaceManager::diameterChanged,this,&MainWindow::diameterChanged); connect(m_surfaceManager, &SurfaceManager::showTab, ui->tabWidget, &QTabWidget::setCurrentIndex); connect(m_surfTools, &surfaceAnalysisTools::updateSelected, m_surfaceManager, &SurfaceManager::backGroundUpdate); @@ -223,12 +223,53 @@ MainWindow::MainWindow(QWidget *parent) : shortcut = new QShortcut(QKeySequence(Qt::Key_U), this); QObject::connect(shortcut, &QShortcut::activated, this, &MainWindow::load_from_url); - QShortcut *shortcutl = new QShortcut(QKeySequence(Qt::Key_L), this); - QObject::connect(shortcutl, &QShortcut::activated, this, &MainWindow::on_actionLoad_Interferogram_triggered); + // Create the dialog once as a top-level, non-modal window + m_helpOverlay = new QDialog(nullptr); + m_helpOverlay->setWindowTitle("Hot key Help"); + m_helpOverlay->setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); // Stays above 3D views, has native minimize/close/drag + + QVBoxLayout *dialogLayout = new QVBoxLayout(m_helpOverlay); + dialogLayout->setContentsMargins(16, 16, 16, 16); + + QLabel *helpTextLbl = new QLabel(m_helpOverlay); + helpTextLbl->setStyleSheet( + "color: #ffffff;" + "font-size: 32px;" + ); + dialogLayout->addWidget(helpTextLbl); + + // Style the dialog background to match your teal theme + m_helpOverlay->setStyleSheet("QDialog { background-color: rgb(46,137,137); }"); + m_helpOverlay->installEventFilter(this); + m_helpOverlay->hide(); + // Wire up the '/' shortcut + QShortcut *shortcuth = new QShortcut(QKeySequence(Qt::Key_Slash), this); + QObject::connect(shortcuth, &QShortcut::activated, this, [this](){ + if (m_helpOverlay->isVisible()) { + m_helpOverlay->hide(); + } else { + // Find the label inside the dialog and update text + QLabel *lbl = m_helpOverlay->findChild(); + if (lbl) { + lbl->setText(getCurrentHotKeyHelp()); + } + + m_helpOverlay->adjustSize(); + + // Center on screen + QRect screenGeometry = QGuiApplication::primaryScreen()->availableGeometry(); + int x = (screenGeometry.width() - m_helpOverlay->width()) / 2; + int y = (screenGeometry.height() - m_helpOverlay->height()) / 2; + m_helpOverlay->move(x, y); + + m_helpOverlay->show(); + m_helpOverlay->raise(); + } + }); - QShortcut *shortcut1 = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_O), this); - QObject::connect(shortcut1, &QShortcut::activated, this, &MainWindow::on_actionLoad_Interferogram_triggered); + QShortcut *shortcutl = new QShortcut(QKeySequence(Qt::Key_L), this); + QObject::connect(shortcutl, &QShortcut::activated, this, &MainWindow::on_actionLoad_Interferogram_triggered); QShortcut *shortcut2 = new QShortcut(QKeySequence(Qt::Key_S), this); QObject::connect(shortcut2, &QShortcut::activated, this, &MainWindow::on_actionSave_Wavefront_triggered); @@ -293,7 +334,37 @@ MainWindow::MainWindow(QWidget *parent) : ui->showColorIgram->setChecked(settings.value("colorChannelShowColor", false).toBool()); openWaveFrontonInit(args); + // Display the help overlay automatically on startup + // Find the label inside the dialog and update text + QLabel *lbl = m_helpOverlay->findChild(); + if (lbl) { + lbl->setText(getCurrentHotKeyHelp()); + } + m_helpOverlay->adjustSize(); + + QRect screenGeometry = QGuiApplication::primaryScreen()->availableGeometry(); + int x = (screenGeometry.width() - m_helpOverlay->width()) / 2; + int y = (screenGeometry.height() - m_helpOverlay->height()) / 2; + m_helpOverlay->move(x, y); + + m_helpOverlay->show(); + m_helpOverlay->raise(); + + QTimer::singleShot(5000, this, [this]() { + m_helpOverlay->hide(); + }); +} +bool MainWindow::eventFilter(QObject *obj, QEvent *event) { + if (obj == m_helpOverlay && event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + // If the user hits '/' or 'Escape' while the help is open, hide it! + if (keyEvent->key() == Qt::Key_Slash || keyEvent->key() == Qt::Key_Escape) { + m_helpOverlay->hide(); + return true; // Event handled + } + } + return QMainWindow::eventFilter(obj, event); } void MainWindow::importIgram() { QSettings set; @@ -316,7 +387,67 @@ void MainWindow::importIgram() { else on_actionLoad_Interferogram_triggered(); } +QString MainWindow::getCurrentHotKeyHelp() const { + int currentTab = ui->tabWidget->currentIndex(); + + auto row = [](const QString &key, const QString &desc) { + return QString(" %1  %3  %2") + .arg(key, desc) + .arg((key == "") ? "":"-"); + }; + + if (currentTab == 2) { + return R"(

Hot Keys - for results tab

)" + "" + + row("i", "Load most recent igram from URL Directory.") + + row("l", "Load igram from file system.") + + row("u", "Load igram from url stream.") + + row("s", "Save selected wave fronts.") + + "
" + "

" + "/      -      Toggle to display\\hide this"; + } + if (currentTab == 1){ + return R"(

Hot Keys - for Analysis (DFT) tab

)" + "" + + row("f", "fit to window") + + row("+", "zoom in") + + row("-", "zoom out") + + row("","")+ + row("mouse wheel", "zoom in\\out")+ + + "
" + "

" + "/      -      Toggle to display\\hide this"; + } + else if (currentTab == 0) { + return R"(

Hot Keys - for igram tab

)" + "" + + row("e", "show Edge zoom")+ + row("f", "show zoomed out") + + row("h", "Toggle outline display") + + + row("","")+ + row("i", "Load most recent igram from URL Directory.") + + row("l", "Load igram from file system.") + + row("u", "Load igram from url stream.") + + row("arrow keys", "move outline one pixel")+ + + "
" + "
" + "

Mouse Controls

" + "" + + row("left click drag","start outline and drag to other side of mirror")+ + row("shift left click drag", "move outline")+ + row("ctrl wheel"," increse \\ decrease outline diameter")+ + row("wheel", "zoom igram image")+ + "

" + "/      -      Toggle to display\\hide this"; + } + + return "

Hot Keys

No active hot keys for this view.

"; +} int showmem(const QString &t); void MainWindow::openWaveFrontonInit(QStringList args){ QProgressDialog pd(" Loading wavefronts in progress.", "Cancel", 0, 100); @@ -868,7 +999,7 @@ void MainWindow::on_actionWavefront_triggered() rad -= border; cv::Mat result = zernikeProcess::get_Instance()->makeSurfaceFromZerns(border, false); - m_surfaceManager->createSurfaceFromPhaseMap(result, + m_surfaceManager->createWaveFrontFromPhaseMap(result, CircleOutline(QPointF(xcen,ycen),rad), CircleOutline(QPointF(0,0),0), QString("Simulated_Wavefront"), WavefrontOrigin::Simulation); @@ -1978,7 +2109,6 @@ void MainWindow::on_autoOutlineHelp_clicked() QString link = qApp->applicationDirPath() + "/res/Help/outlineing.html"; QDesktopServices::openUrl(QUrl(link)); } -#include "transformwavefrontdlg.h" void MainWindow::on_actionwave_front_transforms_triggered() { m_surfaceManager->transform(); @@ -2150,30 +2280,66 @@ void MainWindow::on_actionStop_auto_invert_triggered() } /* - This is called iwth shortcut Qt::Key_U + This is called With shortcut Qt::Key_U or by the live view automation loop It's a sort of easter egg for people using skysolve camera with their Bath setup. */ -void MainWindow::load_from_url(){ - - // Construct the URL with IP address and port - QUrl url; - url.setScheme("http"); // or "https" if using SSL - url.setHost("192.168.50.5"); // Replace with your IP address - url.setPort(5000); // Specify the port - url.setPath("/downloadImage"); - showMessage("Connecting to " + url.toString(),1); - //downloader.startDownload(url); - - QEventLoop loop; - QNetworkAccessManager nam; - QNetworkRequest req(url); - QNetworkReply *reply = nam.get(req); - connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - //connect(reply, &QNetworkReply::downloadProgress, this, [&](qint64 bytesReceived, qint64 bytesTotal){ - // showMessage(QString("byes received %1").arg( bytesReceived) + QString(" total %1").arg(bytesTotal),1); - //}); - loop.exec(); - QByteArray buffer = reply->readAll(); +QString MainWindow::load_from_url(){ + // if live view is open use it + QImage image; + + //if live view is not available. + if (!m_viewDlg || !m_viewDlg->isVisible()){ + + + // Construct the URL with IP address and port + QUrl url; + url.setScheme("http"); // or "https" if using SSL + url.setHost("192.168.50.5"); // Replace with your IP address + url.setPort(5000); // Specify the port + url.setPath("/downloadImage"); + showMessage("Connecting to " + url.toString(),1); + //downloader.startDownload(url); + + QEventLoop loop; + QNetworkAccessManager nam; + QNetworkRequest req(url); + QNetworkReply *reply = nam.get(req); + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + //connect(reply, &QNetworkReply::downloadProgress, this, [&](qint64 bytesReceived, qint64 bytesTotal){ + // showMessage(QString("byes received %1").arg( bytesReceived) + QString(" total %1").arg(bytesTotal),1); + //}); + loop.exec(); + QByteArray buffer = reply->readAll(); + QSettings set; + QString dirPath = set.value("importIgramPath",".").toString(); + // Get the current date and time + QDateTime currentDateTime = QDateTime::currentDateTime(); + + // Format the date and time into a string suitable for a filename + // Using "yyyyMMdd_HHmmss" for a clear, sortable format without illegal characters + QString dateTimeString = currentDateTime.toString("yyyyMMdd_HHmmss"); + + // Construct the full filename + QString fileName = dirPath + "/" + dateTimeString +".jpg"; + // Create a QImage from the QByteArray + + if (image.loadFromData(buffer, "jpg")) { // Specify format if known, otherwise omit + showMessage("Image loaded successfully " + fileName,1); + // Now you can use the 'image' QImage object + // Example: Save it to another file + image.save(fileName, "JPG"); + importIgram(); + } else { + showMessage("Failed to load image!",2); + } + } + else { + image = m_viewDlg->getFrame(); + if (image.isNull()){ + return (""); + + } + } QSettings set; QString dirPath = set.value("importIgramPath",".").toString(); // Get the current date and time @@ -2185,17 +2351,515 @@ void MainWindow::load_from_url(){ // Construct the full filename QString fileName = dirPath + "/" + dateTimeString +".jpg"; - qDebug() << "download filename" << fileName; - // Create a QImage from the QByteArray - QImage image; - if (image.loadFromData(buffer, "jpg")) { // Specify format if known, otherwise omit - showMessage("Image loaded successfully " + fileName,1); - // Now you can use the 'image' QImage object - // Example: Save it to another file - image.save(fileName, "JPG"); - importIgram(); + + image.save(fileName, "JPG"); + importIgram(); + return fileName; +} + +void MainWindow::on_actionLive_view_triggered() +{ + if (!m_viewDlg) { + if (!m_liveViewRMSTimer){ + m_liveViewRMSTimer = new QTimer(); + } + + m_viewDlg = new LiveViewDialog(this);//("http://192.168.50.5:5000/video_feed"); + // compute the size of the current filter circle and set it to the viewDLG + connect(m_liveViewRMSTimer, &QTimer::timeout, this, [this](){ + if (m_viewDlg) + m_viewDlg->m_tmpShowLive = false; + + }); + connect(m_igramArea, &IgramArea::boundary, this, [this](CircleOutline outside, bool cropped, QPointF offset) { + + if (cropped){ + outside.translate(offset); + } + + m_viewDlg->setOutsidecircle(outside.m_center, outside.m_radius); + }); + + // Automatically nullify the pointer when Qt deletes the dialog on close + connect(m_viewDlg, &QObject::destroyed, this, [this]() { + m_viewDlg = nullptr; + }); + + connect(m_viewDlg, &LiveViewDialog::igramCaptured, this, &MainWindow::load_from_url); + // Connect the Start button on the dialog to trigger your analysis loop + connect(m_viewDlg->startAnalysisBtn, &QPushButton::clicked, this, [this]() { + // Using single-shot timer ensures the click event finishes before entering the loop + QTimer::singleShot(0, this, &MainWindow::startLiveButton_clicked); + }); + + // Connect the Stop button to signal the loop to terminate + connect(m_viewDlg->stopAnalysisBtn, &QPushButton::clicked, this,&MainWindow::stopLiveButton_clicked); + + // Connect the Stop button to signal the loop to terminate + connect(m_viewDlg, &LiveViewDialog::streamDisconnected, this, &MainWindow::stopLiveButton_clicked); + connect(m_viewDlg->pauseAnalyBtn, &QPushButton::clicked,this, &MainWindow::pauseLiveButton_clicked); + + connect(m_viewDlg->imageLabel, &LiveImageView::mirrorDefined, this, [this](const QPointF center, double radius){ + + m_igramArea->blockSignals(true); + m_igramArea->setOutsideOutline(center,radius); + m_igramArea->blockSignals(false); + + }); + connect(m_viewDlg, &LiveViewDialog::blueCircle, m_dftArea,&DFTArea::mouseMovedExternal); + connect(m_dftArea, &DFTArea::filterSizeScale, m_viewDlg, &LiveViewDialog::setCenterFilter ); + connect(m_viewDlg, &LiveViewDialog::autoOutLine, m_igramArea, &IgramArea::autoTraceOutline); + connect(m_viewDlg, &LiveViewDialog::outlineOk, this,[this](){ + m_igramArea->nextStep(); + }); + m_viewDlg->show(); + } else { + // If it's already open, just bring it to the front + m_viewDlg->raise(); + m_viewDlg->activateWindow(); + } + + m_viewDlg->show(); +} + +void MainWindow::startLiveButton_clicked() { + + // If already running, do nothing or treat as restart + if (m_liveState == State_Running) return; + + if (m_liveState == State_Stopped) { + // Fresh start: clear previous average data + m_liveValidCount = 0; + m_liveSum.release(); + if (!m_liveAverageWf) { + m_liveAverageWf = new wavefront; + } + } + setLiveViewMode(true); + m_liveState = State_Running; + m_viewDlg->startAnalysisBtn->hide(); + m_viewDlg->pauseAnalyBtn->setText("Pause"); + m_viewDlg->pauseAnalyBtn->show(); + m_viewDlg->stopAnalysisBtn->show(); + m_viewDlg->saveAverageBtn->hide(); + m_viewDlg->grabButton->hide(); + m_viewDlg->OutlineBtn->hide(); + m_viewDlg->OutlineOkBtn->hide(); + + + + // If the loop was completely stopped, start the execution flow again + if (!m_liveLoopActive) { + runLiveAnalysisLoop(); + } +} + +void MainWindow::pauseLiveButton_clicked() { + if (m_liveState == State_Running) { + m_liveState = State_Paused; + m_viewDlg->pauseAnalyBtn->setText("Resume"); + m_viewDlg->grabButton->show(); + m_viewDlg->OutlineBtn->show(); + m_viewDlg->OutlineOkBtn->show(); + setLiveViewMode(false); + + + } else if (m_liveState == State_Paused) { + m_liveState = State_Running; + m_viewDlg->pauseAnalyBtn->setText("Pause"); + m_viewDlg->saveAverageBtn->hide(); + m_igramArea->hide(); + m_dftArea->hide(); + m_viewDlg->grabButton->hide(); + m_viewDlg->OutlineBtn->hide(); + m_viewDlg->OutlineOkBtn->hide(); + + setLiveViewMode(true); + + + } +} + +void MainWindow::stopLiveButton_clicked() { + m_liveState = State_Stopped; + m_liveLoopActive = false; + m_viewDlg->pauseAnalyBtn->setText("Pause"); + m_viewDlg->pauseAnalyBtn->hide(); + m_viewDlg->stopAnalysisBtn->hide(); + m_viewDlg->startAnalysisBtn->show(); + m_viewDlg->saveAverageBtn->hide(); + m_viewDlg->grabButton->show(); + m_viewDlg->OutlineBtn->show(); + m_viewDlg->OutlineOkBtn->show(); + setLiveViewMode(false); + +} + +void MainWindow::setLiveViewMode(bool active) { + if (active) { + // --- LOCKDOWN: Enter Live Mode --- + ui->tabWidget->setCurrentIndex(2); + m_igramArea->setUpdatesEnabled(false); + scrollAreaDft->setUpdatesEnabled(false); + + // Sever automatic tab-switching signals during loop execution + disconnect(m_igramArea, &IgramArea::showTab, ui->tabWidget, &QTabWidget::setCurrentIndex); + disconnect(m_surfaceManager, &SurfaceManager::showTab, ui->tabWidget, &QTabWidget::setCurrentIndex); + + m_ogl->setLivePreviewMode(true); } else { - showMessage("Failed to load image!",2); + // --- RESTORATION: Exit Live Mode --- + m_igramArea->show(); + connect(m_igramArea, &IgramArea::showTab, ui->tabWidget, &QTabWidget::setCurrentIndex); + connect(m_surfaceManager, &SurfaceManager::showTab, ui->tabWidget, &QTabWidget::setCurrentIndex); + + m_dftArea->show(); + + ui->tabWidget->blockSignals(false); + ui->tabWidget->setUpdatesEnabled(true); + + scrollAreaDft->setUpdatesEnabled(true); + m_igramArea->setUpdatesEnabled(true); + + m_ogl->setLivePreviewMode(false); + + } +} +void MainWindow::runLiveAnalysisLoop() { + if (m_liveLoopActive) return; + static mirrorDlg *md = mirrorDlg::get_Instance(); + m_liveLoopActive = true; + double z8Null = md->z8 * md->cc; + cv::Scalar mean; + cv::Scalar stddev; + QFont tFont("Serif", 18); + + // Reset average tracking for the start of the session + m_liveValidCount = 1; + m_liveSum.release(); + + int totalFrames = 1; // Local frame counter for non-averaged views + m_viewDlg->loopRunning = true; + // have the 3D view switch to a widget that does not blink + + bool computeAverage = m_viewDlg->averageMode->isChecked(); + while (m_liveState != State_Stopped && !m_viewDlg->m_stopRequested) { + + computeAverage = m_viewDlg->averageMode->isChecked(); + bool displayAverage = computeAverage; + QApplication::processEvents(); + + // --------------------------------------------------------------------- + // PAUSED STATE HANDLER + // --------------------------------------------------------------------- + if (m_liveState == State_Paused) { + QApplication::processEvents(); + + if (m_liveAverageWf && computeAverage) { + m_viewDlg->saveAverageBtn->show(); + } + + if (m_viewDlg->saveAverage && m_liveAverageWf) { + // Store a DEEP COPY snapshot so future frame accumulation won't alter this saved record + wavefront *savedAvg = new wavefront(*m_liveAverageWf); + savedAvg->name = QString("Average_%1").arg(m_liveValidCount - 1); + + m_surfaceManager->m_wavefronts << savedAvg; + m_surfTools->addWaveFront(savedAvg->name); + m_viewDlg->saveAverage = false; + } + continue; + } + + if (m_liveState == State_Stopped) break; + + + + // 2. Open the saved image in the pipeline + QImage img = m_viewDlg->getFrame(); + + m_igramArea->openImage(img, true, QString("stream%1").arg(m_liveValidCount)); + QApplication::processEvents(); + if (m_liveState == State_Stopped) break; + + // 3. Check outline validity + if (m_igramArea->m_outside.m_radius == 0) { + m_viewDlg->statusLeft->setText( + " The outline is NOT valid for this image " + ); + break; + } + + QApplication::processEvents(); + if (m_liveState == State_Stopped) break; + + // 4. Advance through processing steps + m_igramArea->nextStep(); + QApplication::processEvents(); + if (m_liveState == State_Stopped) break; + + // 5. Switch to DFT tab and generate surface + ui->tabWidget->setCurrentIndex(2); + m_dftTools->wasPressed = true; + + m_dftArea->makeSurface(); + + QApplication::processEvents(); + if (!m_dftArea->success || m_surfaceManager->m_wavefronts.isEmpty()) { + qDebug() << "Warning: Surface generation failed or wavefront list empty. Skipping frame."; + continue; + } + + // Deep copy the newly generated wavefront from m_wavefronts.back() + wavefront *wf = new wavefront(*m_surfaceManager->m_wavefronts.back()); + + if (!(m_viewDlg->FirstWaveFrontSeen) && m_viewDlg->autoRMSatStarup->isChecked()) { + m_viewDlg->maxRMS->setValue(wf->std * m_viewDlg->RMSMargin->value()); + m_viewDlg->FirstWaveFrontSeen = true; + } + + // Defensive check: Ensure matrix data is allocated + if (wf->workData.empty() || wf->workData.rows <= 0 || wf->workData.cols <= 0) { + qDebug() << "Error: Wavefront workData is empty or invalid!"; + delete wf; + continue; + } + + + // --------------------------------------------------------------------- + // ACCUMULATION / AVERAGE CALCULATION + // --------------------------------------------------------------------- + m_viewDlg->m_tmpShowLive = false; + if (computeAverage) { + if (wf->std > m_viewDlg->maxRMS->value()) { + m_viewDlg->m_tmpShowLive = true; + m_liveViewRMSTimer->start(2000); // Display live for 2 seconds + } else { + // First valid frame in the averaging run + if (m_liveValidCount == 1 || m_liveSum.empty()) { + m_liveSum = cv::Mat::zeros(wf->workData.rows, wf->workData.cols, wf->workData.type()); + m_liveDataSum = cv::Mat::zeros(wf->workData.rows, wf->workData.cols, wf->data.type()); + if (m_liveAverageWf) { + delete m_liveAverageWf; + } + m_liveAverageWf = new wavefront(*wf); // Deep copy initial wavefront properties + m_liveAverageWf->m_origin = WavefrontOrigin::Average; + } + + // Verify dimensions before accumulating + if (m_liveSum.rows == wf->workData.rows && m_liveSum.cols == wf->workData.cols) { + m_liveSum += wf->workData; + m_liveDataSum += wf->data; + + cv::Mat result = m_liveSum / static_cast(m_liveValidCount); + + // Compute statistics strictly inside the pupil mask + cv::meanStdDev(result, mean, stddev, m_liveAverageWf->mask); + + // Update surface matrices and calculated statistics + m_liveAverageWf->workData = result.clone(); + m_liveAverageWf->data = result.clone(); + m_liveAverageWf->nulledData = result.clone(); + m_liveAverageWf->std = stddev[0] * md->lambda / outputLambda; + m_liveAverageWf->mean = mean[0]; + if (!m_viewDlg->m_tmpShowLive) { + m_ogl->m_surface->setSurface(m_liveAverageWf); + } + + m_liveValidCount++; + } else { + m_viewDlg->statusRight->setText( "Error: Mismatched matrix dimensions during live accumulation!"); + stopLiveButton_clicked(); // cancel loop + } + } // end RMS check + } + + // If displaying current frame, update 3D view + if (!displayAverage || m_viewDlg->m_tmpShowLive) { + m_ogl->m_surface->setSurface(wf); + } + + // 6. RENDER SURFACE + img = m_ogl->m_surface->render(1000, 1000); + m_ogl->m_liveLabel->setPixmap(QPixmap::fromImage(img)); + // Update UI Status Bar + QString statusRightText; + + if (computeAverage) { + QString liveMsg = ""; + if (m_viewDlg->m_tmpShowLive) { + liveMsg = "  RMS too big   "; + } + statusRightText = QString("Averaged: %1 | Avg RMS: %2 %3") + .arg(m_liveValidCount - 1) + .arg(m_liveAverageWf->std, 0, 'f', 3) + .arg(liveMsg); + } else { + qDebug() << "rms" << wf->std; + statusRightText = QString("Frame: %1 | RMS: %2") + .arg(totalFrames) + .arg(wf->std, 0, 'f', 3); + } + double value = 1.; + + int ndx = m_viewDlg->zernListWidget->currentRow() -1; + + if (ndx == -1){ // do best fit conic + value = wf->InputZerns[8]/m_mirrorDlg->z8; + } + else value = wf->InputZerns[ndx]; + if (ndx == 8){ // if primary SA then remove null if needed + if (m_mirrorDlg->doNull){ + value -= z8Null; + qDebug() << "sa" << value; + } + } + m_viewDlg->history->addSample(m_liveAverageWf->std,value); + m_viewDlg->statusRight->setText(statusRightText); + + + // Wavefront list cleanup & memory management + bool discardFrame = (wf->std > m_viewDlg->maxRMS->value()) || m_viewDlg->deleteIntermidiateWaveFront->isChecked(); + if (discardFrame) { + m_surfaceManager->m_wavefronts.removeLast(); + m_surfTools->deleteLast(); + m_surfaceManager->m_wf = nullptr; + } + + // Free our local temporary wavefront + delete wf; + + QThread::msleep(200); + totalFrames++; } + // ------------------------------------------------------------------------- + // LOOP EXIT: SAVE FINAL AVERAGE IF APPLICABLE + // ------------------------------------------------------------------------- + if (computeAverage && m_liveAverageWf && m_liveValidCount > 1) { + wavefront *savedAvg = new wavefront(*m_liveAverageWf); + m_liveDataSum /= m_liveValidCount; + savedAvg->data = m_liveDataSum.clone(); + savedAvg->m_origin = WavefrontOrigin::Average; + savedAvg->wasSmoothed = false; + savedAvg->dirtyZerns = true; + + savedAvg->regions.clear(); + m_surfaceManager->makeMask(savedAvg); + m_surfaceManager->generateSurfacefromWavefront(savedAvg); + + savedAvg->name = QString("Average_%1").arg(m_liveValidCount - 1); + + m_surfaceManager->m_wavefronts << savedAvg; + m_surfTools->addWaveFront(savedAvg->name); + m_viewDlg->saveAverage = false; + m_surfaceManager->m_currentNdx = m_surfaceManager->m_wavefronts.size()-1; + + m_surfaceManager->m_surfaceTools->select(m_surfaceManager->m_currentNdx); + } + + // Reset UI & Loop State Flags + m_liveLoopActive = false; + m_viewDlg->m_stopRequested = false; + m_viewDlg->loopRunning = false; + m_liveState = State_Stopped; + +} + + + +void MainWindow::on_actionReset_to_Factory_Defaults_triggered() +{ + // 1. Show the confirmation warning box + QMessageBox::StandardButton reply; + reply = QMessageBox::warning(this, + "Reset Settings", + "Do you really want to reset all settings to factory defaults?\nThis cannot be undone.", + QMessageBox::Yes | QMessageBox::No); + + // 2. Proceed only if the user clicks "Yes" + if (reply == QMessageBox::Yes) { + QSettings settings; + QString filePath = settings.fileName(); + + // Clear all keys + settings.clear(); + settings.sync(); + + // Optional: Delete the underlying file for a complete hard reset + if (QFile::exists(filePath)) { + QFile::remove(filePath); + } + + qDebug() << "Settings have been reset to factory defaults."; + + // Optional: Notify the user or prompt them to restart the app + QMessageBox::information(this, "Reset Complete", "Settings have been reset. Please restart the application."); + } +} + +// Helper to copy keys between any two QSettings instances +void copySettingsData(QSettings &src, QSettings &dest) { + QStringListIterator it(src.allKeys()); + while (it.hasNext()) { + QString key = it.next(); + dest.setValue(key, src.value(key)); + } } + + +void MainWindow::on_actionSave_settomgs_to_a_file_triggered() +{ + QString filePath = QFileDialog::getSaveFileName(this, + "Export Settings", + "my_app_settings.ini", + "Settings Files (*.ini)"); + if (filePath.isEmpty()) { + return; // User cancelled + } + + QSettings liveSettings; + liveSettings.sync(); + + // Open an INI file target + QSettings exportFile(filePath, QSettings::IniFormat); + exportFile.clear(); // Clear old content in the target file if it exists + + copySettingsData(liveSettings, exportFile); + exportFile.sync(); + + QMessageBox::information(this, "Export Successful", "Your settings have been successfully exported."); + +} + + +void MainWindow::on_actionRestore_settings_from_a_file_triggered() +{ + QString filePath = QFileDialog::getOpenFileName(this, + "Import Settings", + "", + "Settings Files (*.ini)"); + if (filePath.isEmpty()) { + return; // User cancelled + } + + QMessageBox::StandardButton reply = QMessageBox::warning(this, + "Import Settings", + "Importing settings will overwrite your current configuration. Continue?", + QMessageBox::Yes | QMessageBox::No); + + if (reply != QMessageBox::Yes) { + return; + } + + QSettings importFile(filePath, QSettings::IniFormat); + QSettings liveSettings; + + // Clear current registry settings and replace with imported ones + liveSettings.clear(); + copySettingsData(importFile, liveSettings); + liveSettings.sync(); + + QMessageBox::information(this, "Import Complete", "Settings have been restored. Please restart the application for all changes to take effect."); +} + diff --git a/mainwindow.h b/mainwindow.h index 7aa53a7d..32628247 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -47,6 +47,15 @@ #include "wavefrontfilterdlg.h" #include "outlineplots.h" #include "cameracalibwizard.h" +#include "liveviewdialog.h" +#include +enum LiveLoopState { + State_Stopped, + State_Running, + State_Paused +}; + + class regionEditTools; class jitterOutlineDlg; @@ -57,11 +66,12 @@ class MainWindow; class MainWindow : public QMainWindow { Q_OBJECT - +protected: + bool eventFilter(QObject *obj, QEvent *event) override; public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); - void closeEvent(QCloseEvent *event); + void closeEvent(QCloseEvent *event) override; void updateMetrics(wavefront& wf); int addTab(QWidget* w, const QString &name); mirrorDlg *m_mirrorDlg; @@ -124,6 +134,7 @@ private slots: void on_actionPreferences_triggered(); void mainTabChanged(int); + QString getCurrentHotKeyHelp() const ; void on_actionMirror_triggered(); void on_saveOutline_clicked(); @@ -275,7 +286,15 @@ private slots: void on_actionStop_auto_invert_triggered(); - void load_from_url(); + QString load_from_url(); + + void on_actionLive_view_triggered(); + + void on_actionReset_to_Factory_Defaults_triggered(); + + void on_actionSave_settomgs_to_a_file_triggered(); + + void on_actionRestore_settings_from_a_file_triggered(); private: @@ -287,8 +306,10 @@ private slots: void updateRecentFileActions(); QString strippedName(const QString &fullFileName); QMenu* m_view_menu; + QTimer *m_liveViewRMSTimer = NULL; void createActions(); void createDockWindows(); + void runLiveAnalysisLoop(); public: IgramArea *m_igramArea; DFTTools *m_dftTools; @@ -330,14 +351,17 @@ private slots: bool m_skipItem; bool m_OutlineDoneInBatch; bool m_batchMakeSurfaceReady; + bool m_liveLoopActive = false; astigStatsDlg *m_astigStatsDlg; - + bool m_hasShownHelp = false; + QDialog *m_helpOverlay = nullptr; enum { MaxRecentFiles = 5 }; QAction *recentFileActs[MaxRecentFiles]; ColorChannelDisplay *m_colorChannels; igramIntensity *m_intensityPlot; vortexDebug *m_vortexDebugTool; + QPointer m_viewDlg; batchIgramWizard *batchWiz; QStringList m_igramsToProcess; QWidget *oglFv; @@ -347,6 +371,20 @@ private slots: QVector > batchZerns; QPointer m_cameraCalibWizard; + + // Live view variables + // Average tracking variables (persists across pause/resume, resets on fresh start) + wavefront *m_liveAverageWf = nullptr; + cv::Mat m_liveSum; //Sum used to make the live display + cv::Mat m_liveDataSum; // The sum used to make the actual wavefront. + int m_liveValidCount = 0; + LiveLoopState m_liveState = State_Stopped; + void setLiveViewMode(bool active); +public slots: + void startLiveButton_clicked(); + void pauseLiveButton_clicked(); + void stopLiveButton_clicked(); + }; #endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui index 9caf7c09..c9902a48 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -59,7 +59,7 @@ 0 0 667 - 22 + 24 @@ -67,6 +67,7 @@ Files + @@ -103,6 +104,9 @@ + + + @@ -161,8 +165,8 @@ - 250 - 791 + 348 + 856 @@ -430,7 +434,7 @@ QPushButton:hover{rgb(85, 255, 255)} - 82 + 94 0 @@ -502,7 +506,7 @@ QPushButton:hover{rgb(85, 255, 255)} - 82 + 94 30 @@ -608,7 +612,7 @@ em; - 82 + 94 0 @@ -654,7 +658,7 @@ em; - 82 + 94 0 @@ -697,7 +701,7 @@ em; - 82 + 94 0 @@ -746,7 +750,7 @@ em; - 82 + 94 0 @@ -789,7 +793,7 @@ em; - 82 + 94 0 @@ -838,7 +842,7 @@ em; - 82 + 94 0 @@ -881,7 +885,7 @@ em; - 50 + 56 0 @@ -972,7 +976,7 @@ em; - 82 + 94 0 @@ -1520,6 +1524,26 @@ em; Stop auto invert + + + Capture from Video device + + + + + Reset to Factory Defaults + + + + + Export settings + + + + + Import settings + + diff --git a/oglview.cpp b/oglview.cpp index b929f071..17d70618 100644 --- a/oglview.cpp +++ b/oglview.cpp @@ -66,10 +66,16 @@ OGLView::OGLView(QWidget *parent, ContourTools *m_tool) : QLabel *colorLedgend = new QLabel(this); + m_liveLabel = new QLabel(this); + m_liveLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + m_liveLabel->setScaledContents(true); + m_liveLabel->hide(); // Hidden by default + m_surface->setLegend(colorLedgend); + + QGroupBox *heightMapGroupBox = new QGroupBox(QStringLiteral("Waves 550nm")); - //heightMapGroupBox->setStyleSheet({"background: grey;"}); QVBoxLayout *colorMapVBox = new QVBoxLayout; colorMapVBox->addWidget(colorLedgend); heightMapGroupBox->setLayout(colorMapVBox); @@ -91,6 +97,7 @@ OGLView::OGLView(QWidget *parent, ContourTools *m_tool) : connect(m_fullScreenPb, &QAbstractButton::pressed, this, &OGLView::fullScreenPressed); lh->addStretch(); lv->addWidget(m_container); + lv->addWidget(m_liveLabel); // will show when live view is looping analysis topH->addLayout(lv,10); topH->addLayout(rightcontrols,1); setLayout(topH); @@ -232,3 +239,13 @@ void OGLView::showSelected() // show all selected wavefronts as 3D plots w->show(); QApplication::restoreOverrideCursor(); } + +void OGLView::setLivePreviewMode(bool liveActive) { + if (liveActive) { + m_container->hide(); + m_liveLabel->show(); + } else { + m_liveLabel->hide(); + m_container->show(); + } +} diff --git a/oglview.h b/oglview.h index 3d5dad65..9d442736 100644 --- a/oglview.h +++ b/oglview.h @@ -45,9 +45,12 @@ class OGLView : public QWidget QPushButton *backgroundPb; QColor m_background; QWidget *m_container; + QLabel *m_liveLabel; QSize sizeHint() const; int m_spinRate; + + void setLivePreviewMode(bool liveActive); signals: void fullScreen(); public slots: diff --git a/profileplot.cpp b/profileplot.cpp index 6b97bbd1..41ba28a4 100644 --- a/profileplot.cpp +++ b/profileplot.cpp @@ -656,12 +656,12 @@ void ProfilePlot::populate() if (m_wf == 0) return; QSettings settings; - double smoothing = settings.value("GBValue", 20).toInt(); m_plot->detachItems(QwtPlotItem::Rtti_PlotTextLabel); + SurfaceManager *sm = SurfaceManager::get_instance(); + double smoothing = sm->getGaussBlurOfSelectedWavefronts(); - - if (m_wf->m_outside.m_radius > 0 && settings.value("GBlur", false).toBool()){ + if (smoothing > 0){ double val = .01 * (m_wf->diameter) * smoothing; QString t = QString("Surface Smoothing diameter %1% of surface diameter %2 mm") .arg(smoothing, 6, 'f', 2) @@ -794,7 +794,6 @@ void ProfilePlot::populate() g_angle = startAngle; } if (m_showAvg){ - qDebug() << "inside avg" << y_offset; QColor penColor = QColor(Settings2::m_profile->getColor(i%10)); // plot the average profile std::vector avgRadius = compute_average_radial_profile(wf->workData, @@ -868,7 +867,7 @@ void ProfilePlot::populate() muY->setLabel( QString::fromLatin1( " 1/8 wave on wavefront" ) ); muY->setLabelAlignment( Qt::AlignRight | Qt::AlignTop ); muY->setLineStyle( QwtPlotMarker::HLine ); - muY->setLinePen( Qt::red, 0, Qt::DashDotLine ); + muY->setLinePen( Qt::red, 3, Qt::DashDotLine ); muY->setYValue( 0.125 * mul ); muY->attach( m_plot ); @@ -877,7 +876,7 @@ void ProfilePlot::populate() mlY->setLabel( QString::fromLatin1( " -1/8 wave on wavefront" ) ); mlY->setLabelAlignment( Qt::AlignRight | Qt::AlignTop ); mlY->setLineStyle( QwtPlotMarker::HLine ); - mlY->setLinePen( Qt::red, 0, Qt::DashDotLine ); + mlY->setLinePen( Qt::red, 3, Qt::DashDotLine ); mlY->setYValue( -0.125 * mul ); mlY->attach( m_plot ); @@ -982,7 +981,7 @@ void ProfilePlot::saveXscaleSettings(){ } void ProfilePlot::toggleShow16(){ m_show_16_diameters = !m_show_16_diameters; -qDebug() << "show 16"; + populate(); m_plot->replot(); } @@ -994,7 +993,7 @@ void ProfilePlot::toggleShowAvg(){ } void ProfilePlot::toggleOneAngle(){ m_show_oneAngle = !m_show_oneAngle; -qDebug() << "show one"; + populate(); m_plot->replot(); } @@ -1179,7 +1178,7 @@ void ProfilePlot::CreateWaveFrontFromAverage(){ } cv::Mat result = createInterpolatedCircularSurface(avgRadius); SurfaceManager *sm = SurfaceManager::get_instance(); - sm->createSurfaceFromPhaseMap(result, + sm->createWaveFrontFromPhaseMap(result, m_wf->m_outside, m_wf->m_inside, QString(m_wf->name.replace(".wft","") + "_avg"), WavefrontOrigin::Average); diff --git a/surfaceanalysistools.cpp b/surfaceanalysistools.cpp index f8c7aa08..c22a82c1 100644 --- a/surfaceanalysistools.cpp +++ b/surfaceanalysistools.cpp @@ -61,7 +61,6 @@ surfaceAnalysisTools::surfaceAnalysisTools(QWidget *parent) : } void surfaceAnalysisTools::enableControls(bool flag){ - setEnabled(flag); } @@ -78,6 +77,11 @@ void surfaceAnalysisTools::setGaussianControls(bool enabled, double value){ ui->surfaceSmoothGausianBlurr->setEnabled(enabled); } +void surfaceAnalysisTools::setGaussianStateText(const QString & str){ + ui->lblGaussState->setText(str); +} + + void surfaceAnalysisTools::addWaveFront(const QString &name){ QStringList list = name.split('/'); @@ -367,4 +371,15 @@ void surfaceAnalysisTools::closeDefocus(int /*result*/){ } +void surfaceAnalysisTools::deleteLast(){ + if (ui->wavefrontList->count() > 0) { + // Calculate the index of the last item + int lastRow = ui->wavefrontList->count() - 1; + // Remove the item from the widget (returns a pointer to the item) + QListWidgetItem *item = ui->wavefrontList->takeItem(lastRow); + + // Explicitly delete the item to prevent memory leaks + delete item; + } +} diff --git a/surfaceanalysistools.h b/surfaceanalysistools.h index 9abc25c3..69ad9db3 100644 --- a/surfaceanalysistools.h +++ b/surfaceanalysistools.h @@ -49,8 +49,10 @@ class surfaceAnalysisTools : public QDockWidget double m_defocusInmm; void setBlurText(const QString &txt); void setGaussianControls(bool enabled, double value); + void setGaussianStateText(const QString & str); void nameChangedN(int, const QString&); void select(int item); + void deleteLast(); signals: void doxform(QList); diff --git a/surfaceanalysistools.ui b/surfaceanalysistools.ui index 4e5de216..39f2d6cb 100644 --- a/surfaceanalysistools.ui +++ b/surfaceanalysistools.ui @@ -19,7 +19,7 @@ 250 - 497 + 501 @@ -265,6 +265,17 @@ Surface Filter + + + + + + Currently 20.00% + + + + + @@ -313,7 +324,7 @@ - + -- diff --git a/surfacegraph.cpp b/surfacegraph.cpp index 076e4819..4edf513d 100644 --- a/surfacegraph.cpp +++ b/surfacegraph.cpp @@ -333,7 +333,7 @@ void SurfaceGraph::yOffsetChanged(int val){ void SurfaceGraph::scaleY(int d){ m_yGridHeight = maxHeightSelections[d]; - qDebug() << "yscale" << m_yGridHeight << m_gradientScale << d; + m_graph->axisY()->setRange(0.0f, m_yGridHeight); setSurface(m_wf); } @@ -386,7 +386,7 @@ void SurfaceGraph::adjustXMin(int min) // m_axisMaxSliderX->setValue(max); // } -qDebug() << "range" << minX << maxX; + m_graph->axisX()->setRange(minX, maxX); } @@ -483,11 +483,6 @@ QSize SurfaceGraph::Size(){ return m_graph->size(); } QImage SurfaceGraph::render(int width, int height){ - QSize orgsize = m_graph->size(); - if (orgsize.width() < width){ - m_graph->resize(width, height); - } - QImage result = m_graph->renderToImage(0, QSize(width,height)); - m_graph->resize(orgsize); - return result; + // Render directly at the widget's current display size without resizing the live view + return m_graph->renderToImage(0, m_graph->size()); } diff --git a/surfacemanager.cpp b/surfacemanager.cpp index bad80138..6caf6c87 100644 --- a/surfacemanager.cpp +++ b/surfacemanager.cpp @@ -344,7 +344,6 @@ void SurfaceManager::generateSurfacefromWavefront(int wavefrontNdx) { wavefront *wf = m_wavefronts[wavefrontNdx]; generateSurfacefromWavefront(wf); - surfaceGenFinished(); } @@ -355,8 +354,10 @@ void SurfaceManager::generateSurfacefromWavefront(wavefront * wf){ } else if (wf->zernEnablesApplied.size() != zernEnables.size()) { wf->zernEnablesApplied.resize(zernEnables.size(), true); } - m_GB_enabled = wf->gbEnabled; - m_gbValue = wf->gbValue; + + wf->gbEnabled = m_GB_enabled; + wf->gbValue = m_gbValue; + if (wf->dirtyZerns){ if (mirrorDlg::get_Instance()->isEllipse()){ wf->nulledData = wf->data.clone(); @@ -381,7 +382,6 @@ void SurfaceManager::generateSurfacefromWavefront(wavefront * wf){ return; } - //compute zernike values mirrorDlg *md = mirrorDlg::get_Instance(); @@ -438,10 +438,7 @@ void SurfaceManager::generateSurfacefromWavefront(wavefront * wf){ zp.fillVoid(*wf); makeMask(wf, true); // null out desired terms. - //cv::Mat tiltremoved = zp.null_unwrapped(*wf, wf->InputZerns, zernEnables, 0,3); - //wf->data = tiltremoved; - //zp.unwrap_to_zernikes(*wf); - wf->nulledData = zp.null_unwrapped(*wf, wf->InputZerns, wf->zernEnablesApplied,0,Z_TERMS ); + wf->nulledData = zp.null_unwrapped(*wf, wf->InputZerns, zernEnables,0,Z_TERMS ); wf->dirtyZerns = false; } @@ -549,7 +546,6 @@ SurfaceManager::SurfaceManager(QObject *parent, surfaceAnalysisTools *tools, m_inverseMode(invNOTSET),m_ignoreInverse(false), m_standAstigWizard(nullptr), workToDo(0), m_applyFloatingZernEnables(false), m_wftStats(0) { - okToUpdateSurfacesOnGenerateComplete = true; m_simView = SimulationsView::getInstance(0); pd = new QProgressDialog(); @@ -745,6 +741,7 @@ void SurfaceManager::wftNameChanged(int ndx, const QString &name){ } void SurfaceManager::sendSurface(wavefront* wf){ +//qDebug() << "sending surface" << wf->name; emit currentNdxChanged(m_currentNdx); computeMetrics(wf); @@ -753,6 +750,7 @@ void SurfaceManager::sendSurface(wavefront* wf){ m_SurfaceGraph->setSurface(wf); m_simView->setSurface(wf); foucaultView::get_Instance()->setSurface(wf); + updateBlurText(); QFile fn(wf->name); QFileInfo fileInfo(fn.fileName()); @@ -823,7 +821,7 @@ void SurfaceManager::useDemoWaveFront(){ - createSurfaceFromPhaseMap(result, + createWaveFrontFromPhaseMap(result, CircleOutline(QPointF(xcen,ycen),rad), CircleOutline(QPointF(0,0),0), QString("Demo"), WavefrontOrigin::Demo); @@ -833,7 +831,6 @@ void SurfaceManager::waveFrontClickedSlot(int ndx) { m_currentNdx = ndx; - syncGaussianStateForWavefront(m_wavefronts[ndx]); QString msg = QString(" %1x%2 ").arg(m_wavefronts[ndx]->data.cols).arg(m_wavefronts[ndx]->data.rows); ((MainWindow*)parent())->statusBar()->showMessage(msg); sendSurface(m_wavefronts[ndx]); @@ -853,13 +850,48 @@ void SurfaceManager::wavefrontDClicked(const QString & name){ for (int i = 0; i < m_wavefronts.size(); ++i){ if (m_wavefronts[i]->name.endsWith(name)){ //TODO JST 2023/09/11 this does not work on some name combinations. To be fixed m_currentNdx = i; - syncGaussianStateForWavefront(m_wavefronts[i]); sendSurface(m_wavefronts[i]); break; } } } +double SurfaceManager::getGaussBlurOfSelectedWavefronts(){ + // returns: + // blur value if all selected wavefronts are the same blur value + // 0 if all wavefronts have gauss blur disabled + // -1 different wavefronts have different amounts of blur + // -2 if nothing selected + QList doThese = m_surfaceTools->SelectedWaveFronts(); + double blur = -2; + foreach(int ndx , doThese){ + wavefront * wf = m_wavefronts[ndx]; + double val = (wf->gbEnabled ? wf->gbValue : 0); + if (blur < 0) { + blur = val; + continue; + } + if (blur != val) + return -1; + } + return blur; +} + +void SurfaceManager::updateBlurText(){ + if (m_surfaceTools == nullptr) + return; + + double blur = getGaussBlurOfSelectedWavefronts(); + if (blur == -2) // nothing selected + m_surfaceTools->setGaussianStateText(""); + else if (blur == -1) // mixed + m_surfaceTools->setGaussianStateText("Multiple Blur Amounts"); + else if (blur == 0) // Gaussian blur disabled + m_surfaceTools->setGaussianStateText("Currently No Blurring"); + else + m_surfaceTools->setGaussianStateText("Current Gaussian Blur "+QString("%1").arg(blur, 5, 'f', 1)+" %"); +} + void SurfaceManager::surfaceSmoothGBValue(double value){ QSettings settings; @@ -903,19 +935,6 @@ void SurfaceManager::surfaceSmoothGBEnabled(bool b){ m_waveFrontTimer->start(500); } -void SurfaceManager::syncGaussianStateForWavefront(wavefront *wf){ - if (wf == nullptr) { - return; - } - - m_GB_enabled = wf->gbEnabled; - m_gbValue = wf->gbValue; - m_surfaceTools->setGaussianControls(wf->gbEnabled, wf->gbValue); - - mirrorDlg *md = mirrorDlg::get_Instance(); - m_surfaceTools->setBlurText(QString("%1 mm").arg(.01 * wf->gbValue * md->diameter, 6, 'f', 2)); -} - void SurfaceManager::computeMetrics(wavefront *wf){ mirrorDlg *md = mirrorDlg::get_Instance(); cv::Scalar mean,std; @@ -1117,7 +1136,7 @@ void SurfaceManager::SaveWavefronts(bool saveNulled){ } QApplication::restoreOverrideCursor(); } -void SurfaceManager::createSurfaceFromPhaseMap(cv::Mat phase, CircleOutline outside, +void SurfaceManager::createWaveFrontFromPhaseMap(cv::Mat phase, CircleOutline outside, CircleOutline center, const QString &name, WavefrontOrigin origin, QVector > polyArea){ @@ -1176,9 +1195,10 @@ void SurfaceManager::createSurfaceFromPhaseMap(cv::Mat phase, CircleOutline outs m_currentNdx = m_wavefronts.size()-1; makeMask(m_currentNdx); - + //qDebug() << "surface manager create wave front from phasee map calling generate surface from wave front"; generateSurfacefromWavefront(m_currentNdx); - loadComplete(); + + //loadComplete(); m_surfaceTools->select(m_currentNdx); emit showTab(2); } @@ -1549,7 +1569,7 @@ void SurfaceManager::deleteCurrent(){ if (m_currentNdx > 0) --m_currentNdx; } - showmem("After delete"); + if (m_wavefronts.length() > 0) { sendSurface(m_wavefronts[m_currentNdx]); @@ -1589,7 +1609,6 @@ void SurfaceManager::next(){ ++m_currentNdx; else m_currentNdx = 0; - syncGaussianStateForWavefront(m_wavefronts[m_currentNdx]); sendSurface(m_wavefronts[m_currentNdx]); @@ -1604,7 +1623,6 @@ void SurfaceManager::previous(){ else m_currentNdx = m_wavefronts.length()-1; - syncGaussianStateForWavefront(m_wavefronts[m_currentNdx]); sendSurface(m_wavefronts[m_currentNdx]); } QVector histo(const std::vector &data, int bins, double min, double max){ @@ -1646,22 +1664,22 @@ void SurfaceManager::surfaceGenFinished() { emit progress(++workProgress); m_surface_finished = true; - if (okToUpdateSurfacesOnGenerateComplete){ - loadComplete(); - } + loadComplete(); + if (workProgress == workToDo) workToDo = 0; } void SurfaceManager::loadComplete(){ + m_toolsEnableTimer->start(1000); if (m_wavefronts.size() > 0){ sendSurface(m_wavefronts[m_currentNdx]); computeMetrics(m_wavefronts[m_currentNdx]); } } - +int cnt = 0; // Update all surfaces since some control has changed. Skip current surface it has already been done void SurfaceManager::backGroundUpdate(){ @@ -1676,6 +1694,7 @@ void SurfaceManager::backGroundUpdate(){ const bool applyFloatingZerns = m_applyFloatingZernEnables; m_applyFloatingZernEnables = false; foreach (int i, doThese){ +qDebug() << "updating these" << doThese << i << ++cnt; if (applyFloatingZerns) { m_wavefronts[i]->zernEnablesApplied = zernEnables; } @@ -1691,8 +1710,8 @@ void SurfaceManager::backGroundUpdate(){ break; } } +qDebug() << "done with these"; m_ignoreInverse = false; - loadComplete(); } diff --git a/surfacemanager.h b/surfacemanager.h index bf719734..b57627e0 100644 --- a/surfacemanager.h +++ b/surfacemanager.h @@ -77,7 +77,9 @@ class SurfaceManager : public QObject void initWaveFrontLoad(); void averageWavefrontFiles(const QStringList &files); void downSizeWf(wavefront *wf); - void syncGaussianStateForWavefront(wavefront *wf); + void updateBlurText(); + double getGaussBlurOfSelectedWavefronts(); + void process(int wavefront_index, SurfaceManager *sm); wavefront *readWaveFront(const QString &fileName); inline wavefront *getCurrent(){ @@ -118,7 +120,6 @@ class SurfaceManager : public QObject configRESPONSE rocResp; configRESPONSE lambdResp; int okToContinue; - bool okToUpdateSurfacesOnGenerateComplete; void makeMask(wavefront* wf, bool useInsideCircle = true); void generateSurfacefromWavefront(int ndx); void generateSurfacefromWavefront(wavefront *wf); @@ -177,7 +178,7 @@ private slots: public slots: void rotateThese(double angle, QList list); - void createSurfaceFromPhaseMap(cv::Mat phase, CircleOutline outside, + void createWaveFrontFromPhaseMap(cv::Mat phase, CircleOutline outside, CircleOutline center, const QString &name, WavefrontOrigin origin, QVector > polyArea= QVector >()); void invert(QList list); diff --git a/usercolormapdlg.cpp b/usercolormapdlg.cpp index ed627550..ae6b20d9 100644 --- a/usercolormapdlg.cpp +++ b/usercolormapdlg.cpp @@ -31,6 +31,7 @@ #include #include "colormapviewerdlg.h" #include "plotcolor.h" +#include /* m_listeWidget->setViewMode(QListWidget::IconMode); diff --git a/videostreamworker.cpp b/videostreamworker.cpp new file mode 100644 index 00000000..dbba8ffb --- /dev/null +++ b/videostreamworker.cpp @@ -0,0 +1,578 @@ +#include "videostreamworker.h" +#include +#include +#include +#include "spdlog/spdlog.h" + +VideoStreamWorker::VideoStreamWorker(const QString &source, QObject *parent) + : QObject(parent), m_source(source) {} + +VideoStreamWorker::~VideoStreamWorker() { + stop(); +} + +void VideoStreamWorker::startStream() { + QMutexLocker locker(&m_controlMutex); + + if (m_running) return; + + bool isInt = false; + int camId = m_source.toInt(&isInt); + qputenv("OPENCV_FFMPEG_CAPTURE_OPTIONS", "stimeout;2000000;"); + + if (m_cap.isOpened()) { + m_cap.release(); + } + + if (isInt) { +#if defined(_WIN32) || defined(_WIN64) + m_cap.open(camId, cv::CAP_DSHOW); +#else + m_cap.open(camId); +#endif + } else { + m_cap.open(m_source.toStdString()); + } + + if (!m_cap.isOpened()) { + emit streamError("Failed to open stream source: " + m_source); + m_running = false; + return; + } + + spdlog::get("logger")->trace("Stream started successfully"); + + m_running = true; + + // Launch the background capture loop in a dedicated std::thread + m_captureThread = std::unique_ptr(new std::thread(&VideoStreamWorker::captureLoop, this)); + + emit streamStarted(); +} + + + +void VideoStreamWorker::captureLoop() { + int errorCount = 0; + QSettings set; + int maxErrorLimit = set.value("liveViewConnectFrames",15).toInt(); // Number of failed attempts before giving up + + while (m_running) { + cv::Mat frame; + bool success = false; + + { + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened()) { + + success = m_cap.read(frame) && !frame.empty(); + + } + } + + if (success) { + // Successful read: reset error counter and update live frame + errorCount = 0; + QMutexLocker locker(&m_frameMutex); + m_latestFrame = frame.clone(); + + cv::Scalar meanValue = cv::mean(frame); + double averagePixel = meanValue[0]; + //qDebug() << "avg: " << averagePixel; + if (averagePixel > 0) + continue; + + // error! - we need to reconnect + spdlog::get("logger")->error("resetting connection because frame is blank"); + { + QMutexLocker locker2(&m_controlMutex); + m_cap.release(); + bool isInt = false; + int camId = m_source.toInt(&isInt); + m_source.toInt(&isInt); + if (isInt) { + #if defined(_WIN32) || defined(_WIN64) + m_cap.open(camId, cv::CAP_DSHOW); + #else + m_cap.open(camId); + #endif + } else { + m_cap.open(m_source.toStdString()); + } + } + if (m_cap.isOpened()) { + setResolution(savedResolutionWidth, savedResolutionHeight); + } + + } else { + errorCount++; + + spdlog::get("logger")->trace("errorCount {}", errorCount); + // Create a status/error image canvas (e.g., 800x600 dark background) + cv::Mat errorFrame(600, 800, CV_8UC3, cv::Scalar(30, 30, 30)); + + std::string msg = "Connecting / Waiting for Stream..."; + std::string countMsg = "Attempt: " + std::to_string(errorCount) + " of " + std::to_string(maxErrorLimit); + + int baseline = 0; + double fontScale = 0.8; + int thickness = 2; + + // Draw primary warning message (Centered, Orange/Yellow) + cv::Size textSize = cv::getTextSize(msg, cv::FONT_HERSHEY_SIMPLEX, fontScale, thickness, &baseline); + cv::Point textOrg((errorFrame.cols - textSize.width) / 2, (errorFrame.rows / 2) - 20); + cv::putText(errorFrame, msg, textOrg, cv::FONT_HERSHEY_SIMPLEX, fontScale, cv::Scalar(0, 165, 255), thickness); + + // Draw error count below it (Centered, White) + double countScale = 0.6; + cv::Size countSize = cv::getTextSize(countMsg, cv::FONT_HERSHEY_SIMPLEX, countScale, 1, &baseline); + cv::Point countOrg((errorFrame.cols - countSize.width) / 2, (errorFrame.rows / 2) + 25); + cv::putText(errorFrame, countMsg, countOrg, cv::FONT_HERSHEY_SIMPLEX, countScale, cv::Scalar(255, 255, 255), 1); + + // Publish the status frame so the client UI can show it + { + QMutexLocker locker(&m_frameMutex); + m_latestFrame = errorFrame; + } + + // If we've exceeded the limit, give up and close the stream + if (errorCount >= maxErrorLimit) { + if (m_running) { + emit streamError("Stream connection failed after multiple attempts."); + m_running = false; + } + break; + } + + // Sleep briefly between retry attempts so it doesn't spin at 100% CPU + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } +} + +void VideoStreamWorker::fetchNextFrame() { + if (!m_running) return; + static cv::Mat testpattern; + if (false) { // use a file for the test patttern. + if (testpattern.empty()){ + // 1. Open a file dialog to select an image + QString filePath = QFileDialog::getOpenFileName( + nullptr, + "Open Image", + "", + "Image Files (*.png *.jpg *.jpeg *.bmp)" + ); + + if (filePath.isEmpty()) { + return; // User canceled + } + testpattern = cv::imread(filePath.toStdString(), cv::IMREAD_COLOR); + } + emit frameReady(testpattern.clone()); + return; + } + if (false){// for noraml op make this false. This creates a calibration target for debug. THe first side lobe will be at bin 32. + + + if (testpattern.empty()){ // that will be 32 cycles per mirror diameter. + int innerWidth = 800; + int stripeWidth = 16; + int borderWidth = 10; // Extra room for the outline/crop boundary + int totalSize = innerWidth + (borderWidth * 2); + + // Create a larger canvas filled with black (the border background) + cv::Mat img(totalSize, totalSize, CV_8UC1, cv::Scalar(0)); + + // Define the region for the 1024x1024 striped circle, offset by the border + cv::Rect roiRect(borderWidth, borderWidth, innerWidth, innerWidth); + cv::Mat innerRegion = img(roiRect); + + // Draw the 16-pixel stripes inside the inner region + for (int x = 0; x < innerWidth; x += stripeWidth) { + int currentWidth = std::min(stripeWidth, innerWidth - x); + if ((x / stripeWidth) % 2 == 1) { + cv::Mat stripeRoi = innerRegion(cv::Rect(x, 0, currentWidth, innerWidth)); + stripeRoi.setTo(cv::Scalar(255)); + } + } + + // Apply a circular mask to the inner region so it matches the expected mirror outline + cv::Mat circularMask(innerWidth, innerWidth, CV_8UC1, cv::Scalar(0)); + cv::Point center(innerWidth / 2.0, innerWidth / 2.0); + double radius = innerWidth / 2.0; + cv::circle(circularMask, center, radius, cv::Scalar(255), -1); + + cv::Mat maskedInner; + innerRegion.copyTo(maskedInner, circularMask); + maskedInner.copyTo(innerRegion); // Copy back into the bordered canvas + testpattern = img.clone(); + } + emit frameReady(testpattern.clone()); + return; + } + cv::Mat frameToSend; + { + QMutexLocker locker(&m_frameMutex); + if (m_latestFrame.empty()) return; + frameToSend = m_latestFrame.clone(); + } + + emit frameReady(frameToSend); +} + + +void VideoStreamWorker::stop() { + m_running = false; + + // Wait for the background capture thread to finish cleanly + if (m_captureThread && m_captureThread->joinable()) { + m_captureThread->join(); + } + + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened()) { + m_cap.release(); + } +} + +void VideoStreamWorker::changeSource(QString newSource) { + // Stop current stream and join thread safely + stop(); + + { + QMutexLocker locker(&m_controlMutex); + spdlog::get("logger")->trace("Changing source to: {}", newSource.toStdString()); + + m_source = newSource; + } + + // Restart with the new source + startStream(); +} + +void VideoStreamWorker::setResolution(int width, int height) { + + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened() && width > 0 && height > 0) { + bool isInt = false; + m_source.toInt(&isInt); + + // Only apply resolution changes to local USB cameras, not URL streams + if (isInt) { + m_cap.set(cv::CAP_PROP_FRAME_WIDTH, width); + m_cap.set(cv::CAP_PROP_FRAME_HEIGHT, height); + spdlog::get("logger")->trace("VideoStreamWorker: Resolution requested to set -> {} x {}", width, height); + savedResolutionWidth = width; + savedResolutionHeight = height; + } + } +} + +void VideoStreamWorker::requestCameraSetting(QString prop , double val) { + spdlog::get("logger")->trace("setting camera property"); + + QMutexLocker locker(&m_controlMutex); + bool isInt = false; + m_source.toInt(&isInt); + if (isInt == false) + return; + + if (prop == "Exposure") { + setAutoExpSetting(false, false); + m_cap.set(cv::CAP_PROP_EXPOSURE, val); + } + + if (prop == "Gain") { + setAutoExpSetting(false, false); + int prop_id = cv::CAP_PROP_GAIN; + if (m_gain_mode == false) + prop_id = cv::CAP_PROP_BRIGHTNESS; + m_cap.set(prop_id, val); + } +} + +void VideoStreamWorker::dumpAll() { + // send all camera properties to debug stream + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened()) { + bool isInt = false; + m_source.toInt(&isInt); + + // Only apply resolution changes to local USB cameras, not URL streams + if (isInt) { + struct PropertyMap { + cv::VideoCaptureProperties id; + const char* name; + }; + const std::vector properties = { + { cv::CAP_PROP_POS_MSEC, "CAP_PROP_POS_MSEC" }, + { cv::CAP_PROP_POS_FRAMES, "CAP_PROP_POS_FRAMES" }, + { cv::CAP_PROP_POS_AVI_RATIO, "CAP_PROP_POS_AVI_RATIO" }, + { cv::CAP_PROP_FRAME_WIDTH, "CAP_PROP_FRAME_WIDTH" }, + { cv::CAP_PROP_FRAME_HEIGHT, "CAP_PROP_FRAME_HEIGHT" }, + { cv::CAP_PROP_FPS, "CAP_PROP_FPS" }, + { cv::CAP_PROP_FOURCC, "CAP_PROP_FOURCC" }, + { cv::CAP_PROP_FRAME_COUNT, "CAP_PROP_FRAME_COUNT" }, + { cv::CAP_PROP_FORMAT, "CAP_PROP_FORMAT" }, + { cv::CAP_PROP_MODE, "CAP_PROP_MODE" }, + { cv::CAP_PROP_BRIGHTNESS, "CAP_PROP_BRIGHTNESS" }, + { cv::CAP_PROP_CONTRAST, "CAP_PROP_CONTRAST" }, + { cv::CAP_PROP_SATURATION, "CAP_PROP_SATURATION" }, + { cv::CAP_PROP_HUE, "CAP_PROP_HUE" }, + { cv::CAP_PROP_GAIN, "CAP_PROP_GAIN" }, + { cv::CAP_PROP_EXPOSURE, "CAP_PROP_EXPOSURE" }, + { cv::CAP_PROP_CONVERT_RGB, "CAP_PROP_CONVERT_RGB" }, + { cv::CAP_PROP_WHITE_BALANCE_BLUE_U, "CAP_PROP_WHITE_BALANCE_BLUE_U" }, + { cv::CAP_PROP_RECTIFICATION, "CAP_PROP_RECTIFICATION" }, + { cv::CAP_PROP_MONOCHROME, "CAP_PROP_MONOCHROME" }, + { cv::CAP_PROP_SHARPNESS, "CAP_PROP_SHARPNESS" }, + { cv::CAP_PROP_AUTO_EXPOSURE, "CAP_PROP_AUTO_EXPOSURE" }, + { cv::CAP_PROP_GAMMA, "CAP_PROP_GAMMA" }, + { cv::CAP_PROP_TEMPERATURE, "CAP_PROP_TEMPERATURE" }, + { cv::CAP_PROP_TRIGGER, "CAP_PROP_TRIGGER" }, + { cv::CAP_PROP_TRIGGER_DELAY, "CAP_PROP_TRIGGER_DELAY" }, + { cv::CAP_PROP_WHITE_BALANCE_RED_V, "CAP_PROP_WHITE_BALANCE_RED_V" }, + { cv::CAP_PROP_ZOOM, "CAP_PROP_ZOOM" }, + { cv::CAP_PROP_FOCUS, "CAP_PROP_FOCUS" }, + { cv::CAP_PROP_GUID, "CAP_PROP_GUID" }, + { cv::CAP_PROP_ISO_SPEED, "CAP_PROP_ISO_SPEED" }, + { cv::CAP_PROP_BACKLIGHT, "CAP_PROP_BACKLIGHT" }, + { cv::CAP_PROP_PAN, "CAP_PROP_PAN" }, + { cv::CAP_PROP_TILT, "CAP_PROP_TILT" }, + { cv::CAP_PROP_ROLL, "CAP_PROP_ROLL" }, + { cv::CAP_PROP_IRIS, "CAP_PROP_IRIS" }, + { cv::CAP_PROP_SETTINGS, "CAP_PROP_SETTINGS" }, + { cv::CAP_PROP_BUFFERSIZE, "CAP_PROP_BUFFERSIZE" }, + { cv::CAP_PROP_AUTOFOCUS, "CAP_PROP_AUTOFOCUS" } + }; + + for (const auto& prop : properties) { + double value = m_cap.get(prop.id); + + // OpenCV returns -1 or 0 for unsupported properties depending on the backend + if (value == -1) { + qDebug().noquote() << QString("%1: NOT SUPPORTED").arg(prop.name, -30); + } else { + qDebug().noquote() << QString("%1: %2").arg(prop.name, -30).arg(value); + } + } + + } + } +} + +bool VideoStreamWorker::getCameraConfiguration(CameraConfiguration &config) { + // meant to be called only from Ui thread - return false if config is invalid + + spdlog::get("logger")->trace("getCameraConfiguration"); + + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened() == false) + return false; + bool isInt = false; + m_source.toInt(&isInt); + if (isInt == false) + return false; + config.auto_exposure = m_cap.get(cv::CAP_PROP_AUTO_EXPOSURE); + config.exposure = m_cap.get(cv::CAP_PROP_EXPOSURE); + config.brightness = m_cap.get(cv::CAP_PROP_BRIGHTNESS); + config.gain = m_cap.get(cv::CAP_PROP_GAIN); + config.resolution.setHeight(m_cap.get(cv::CAP_PROP_FRAME_HEIGHT)); + config.resolution.setWidth(m_cap.get(cv::CAP_PROP_FRAME_WIDTH)); + + return true; +} + +void findMinMaxBinarySearch(cv::VideoCapture &m_cap, int propid, double &min, double &max, double &val) { + double high = 10000; + double low = val; + double guess; + double actual; + + // + // search for max + // + + while(true) { + if (high-low <= 1) { + max = low; + break; + } + + guess = (high+low)/2; + guess = std::round(guess); + m_cap.set(propid, guess); + actual = m_cap.get(propid); + if (guess == actual) + low = guess; + else + high = guess; + + } + + // + // search for min + // + + high = val; + low = -10000; + while(true) { + if (high-low <= 1) { + min = high; + break; + } + + guess = (high+low)/2; + guess = std::round(guess); + m_cap.set(propid, guess); + actual = m_cap.get(propid); + if (guess == actual) + high = guess; + else + low = guess; + } + m_cap.set(propid, val); +} + +bool VideoStreamWorker::getCameraGainRange(bool &bGain, double &min, double &max, double &gain) { + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened() == false) + return false; + bool isInt = false; + m_source.toInt(&isInt); + if (isInt == false) + return false; + spdlog::get("logger")->trace("getCameraGainRange"); + bGain = true; + gain = m_cap.get(cv::CAP_PROP_GAIN); + if (gain == -1) { + bGain = false; + gain = m_cap.get(cv::CAP_PROP_BRIGHTNESS); + } + m_gain_mode = gain; + + // now set to -10,000 and 10,000 to find min and max + int propid = cv::CAP_PROP_GAIN; + if (bGain == false) + propid = cv::CAP_PROP_BRIGHTNESS; + + m_cap.set(propid,-10000.0); + min = m_cap.get(propid); + m_cap.set(propid,10000.0); + max = m_cap.get(propid); + m_cap.set(propid,gain); + + + if (max != min) { + return true; + } + + // max is min. We need to do a binary search. + findMinMaxBinarySearch(m_cap, propid, min, max, gain); + + return true; +} + +void VideoStreamWorker::setCameraGain(bool bGain, double gain) { + spdlog::get("logger")->trace("setCameraGain"); + + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened() == false) + return; + bool isInt = false; + m_source.toInt(&isInt); + if (isInt == false) + return; + int propid = cv::CAP_PROP_GAIN; + if (bGain == false) + propid = cv::CAP_PROP_BRIGHTNESS; + m_cap.set(propid,gain); +} + + +bool VideoStreamWorker::getCameraExposureRange(double &min, double &max, double &exposure) { + spdlog::get("logger")->trace("getCameraExposureRange"); + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened() == false) + return false; + bool isInt = false; + m_source.toInt(&isInt); + if (isInt == false) + return false; + + exposure = m_cap.get(cv::CAP_PROP_EXPOSURE); + + m_cap.set(cv::CAP_PROP_EXPOSURE,-10000.0); + min = m_cap.get(cv::CAP_PROP_EXPOSURE); + m_cap.set(cv::CAP_PROP_EXPOSURE,10000.0); + max = m_cap.get(cv::CAP_PROP_EXPOSURE); + m_cap.set(cv::CAP_PROP_EXPOSURE,exposure); + + if (max != min) { + return true; + } + + // max is min. We need to do a binary search. + findMinMaxBinarySearch(m_cap, cv::CAP_PROP_EXPOSURE, min, max, exposure); + + return true; +} + +void VideoStreamWorker::getAutoExpSetting(bool &bFeatureAvailable, bool &bAuto) { + // .75 auto on, .25 auto off alternately 3=auto on and 1 auto off; -1 means unavailable + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened() == false) + return; + bool isInt = false; + m_source.toInt(&isInt); + if (isInt == false) { + bFeatureAvailable=false; + return; + } + double val = m_cap.get(cv::CAP_PROP_AUTO_EXPOSURE); + if (val == -1) { + bFeatureAvailable = false; + return; + } + bFeatureAvailable = true; + if (val == .25 || val == 1) { + bAuto = false; + return; + } + if (val == 0.75 || val == 3) { + bAuto = true; + return; + } + bFeatureAvailable = false; + spdlog::get("logger")->error("strange auto exp value: {}", val); + +} + +void VideoStreamWorker::setAutoExpSetting(bool bAuto, bool bLockMutex) { + if (bLockMutex) + QMutexLocker locker(&m_controlMutex); + if (m_cap.isOpened() == false) + return; + bool isInt = false; + m_source.toInt(&isInt); + if (isInt == false) + return; + double val = m_cap.get(cv::CAP_PROP_AUTO_EXPOSURE); + if (bAuto && val == .25) { + m_cap.set(cv::CAP_PROP_AUTO_EXPOSURE, .75); + return; + } + if (bAuto && val == 1) { + m_cap.set(cv::CAP_PROP_AUTO_EXPOSURE, 3); + return; + } + if (!bAuto && val == .75) { + m_cap.set(cv::CAP_PROP_AUTO_EXPOSURE, .25); + return; + } + if (!bAuto && val == 3) { + m_cap.set(cv::CAP_PROP_AUTO_EXPOSURE, 1); + return; + } + // if we get here, either we don't know what value to use or the feature is unavailable +} diff --git a/videostreamworker.h b/videostreamworker.h new file mode 100644 index 00000000..5589d838 --- /dev/null +++ b/videostreamworker.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class VideoStreamWorker : public QObject { + Q_OBJECT +public: + explicit VideoStreamWorker(const QString &source, QObject *parent = nullptr); + ~VideoStreamWorker(); + struct CameraConfiguration { + QSize resolution; + double exposure; + double auto_exposure; + double brightness; + double gain; + }; + bool getCameraConfiguration(CameraConfiguration &config); + bool getCameraGainRange(bool &bGain, double &min, double &max, double &gain); + void setCameraGain(bool bGain, double gain); + bool getCameraExposureRange(double &min, double &max, double &exposure); + void getAutoExpSetting(bool &bFeatureAvailable, bool &bAuto); + void setAutoExpSetting(bool bAuto, bool bLockMutex = true); + +public slots: + void startStream(); + void fetchNextFrame(); + void stop(); + void changeSource(QString newSource); + void setResolution(int width, int height); + void dumpAll(); + void requestCameraSetting(QString prop , double val); + + +signals: + void streamStarted(); + void streamError(const QString &errorStr); + void frameReady(const cv::Mat &frame); + +private: + void captureLoop(); + + QString m_source; + cv::VideoCapture m_cap; + cv::Mat m_latestFrame; + + QMutex m_frameMutex; // Protects m_latestFrame + QMutex m_controlMutex; // Protects m_cap and stream state changes + + std::atomic m_running{false}; + std::unique_ptr m_captureThread; + + bool m_gain_mode=true; // this is set to false if gain is unavailable and we will be using brightness settings in lieu of gain + int savedResolutionWidth = 640; + int savedResolutionHeight = 480; + +}; diff --git a/wavefront.cpp b/wavefront.cpp index 361e9577..9d125a39 100644 --- a/wavefront.cpp +++ b/wavefront.cpp @@ -30,3 +30,34 @@ void wavefront::cloneMatricesFrom(const wavefront &wf) workData = wf.workData.clone(); workMask = wf.workMask.clone(); } + +wavefront::wavefront( const wavefront &wf): + data(wf.data.clone()), + nulledData(wf.nulledData.clone()), + mask(wf.mask.clone()), + workData(wf.workData.clone()), + workMask(wf.workMask.clone()), + InputZerns(wf.InputZerns), + regions(wf.regions), + zernEnablesApplied(wf.zernEnablesApplied), + gaussian_diameter(wf.gaussian_diameter), + m_outside(wf.m_outside), + m_inside(wf.m_inside), + GBSmoothingValue(wf.GBSmoothingValue), + gbValue(wf.gbValue), + m_origin(wf.m_origin), + name(wf.name), + lambda(wf.lambda), + diameter(wf.diameter), + roc(wf.roc), + min(wf.min), + max(wf.max), + std(wf.std), + mean(wf.mean), + dirtyZerns(wf.dirtyZerns), + regions_have_been_expanded(wf.regions_have_been_expanded), + gbEnabled(wf.gbEnabled), + wasSmoothed(wf.wasSmoothed), + useSANull(wf.useSANull), + m_manuallyInverted(wf.m_manuallyInverted) +{} diff --git a/wavefront.h b/wavefront.h index 489c4797..24fe4e69 100644 --- a/wavefront.h +++ b/wavefront.h @@ -32,13 +32,11 @@ enum class WavefrontOrigin : std::uint8_t { Zernikes, Smoothed }; -class wavefront -{ -public: +class wavefront { public: wavefront(); ~wavefront() = default; - wavefront( const wavefront &wf) = default; - wavefront( wavefront && ) = default; + wavefront( const wavefront &wf); + wavefront( wavefront && ); wavefront& operator=( const wavefront & ) = default; wavefront& operator=(wavefront &&) = default; void cloneMatricesFrom(const wavefront &wf); @@ -49,20 +47,19 @@ class wavefront cv::Mat_ workData; cv::Mat_ workMask; std::vector InputZerns; + + QVector > regions; + std::vector zernEnablesApplied; + double gaussian_diameter; - bool gbEnabled; - double gbValue; - bool wasSmoothed; - bool useSANull; + CircleOutline m_outside; + CircleOutline m_inside; double GBSmoothingValue; + double gbValue; WavefrontOrigin m_origin; - bool m_manuallyInverted; // true if user inverted this wavefront - QString name; double lambda; - CircleOutline m_outside; - CircleOutline m_inside; double diameter; double roc; double min; @@ -70,10 +67,11 @@ class wavefront double std; double mean; bool dirtyZerns; - QVector > regions; bool regions_have_been_expanded; - - + bool gbEnabled; + bool wasSmoothed; + bool useSANull; + bool m_manuallyInverted; // true if user inverted this wavefront }; #endif // WAVEFRONT_H diff --git a/zernikeeditdlg.cpp b/zernikeeditdlg.cpp index f7815d2d..994c1a0c 100644 --- a/zernikeeditdlg.cpp +++ b/zernikeeditdlg.cpp @@ -84,7 +84,7 @@ void zernikeEditDlg::on_createSurface_clicked() } } - m_sm->createSurfaceFromPhaseMap(result, CircleOutline(QPointF(xcen,ycen),rad), + m_sm->createWaveFrontFromPhaseMap(result, CircleOutline(QPointF(xcen,ycen),rad), CircleOutline(QPointF(0,0),0), QString("Zernike_Wavefront"), WavefrontOrigin::Zernikes); } diff --git a/zernikes.h b/zernikes.h index 2a7667fa..d2121982 100644 --- a/zernikes.h +++ b/zernikes.h @@ -31,7 +31,7 @@ const char* const zernsNames[] = "X Coma", "Y Coma", "Spherical", - "X Trefoi", + "X Trefoil", "Y Trefoil", "X 2nd Astig", "Y 2nd Astig", diff --git a/zernikesmoothingdlg.cpp b/zernikesmoothingdlg.cpp index 80f47aff..158b5917 100644 --- a/zernikesmoothingdlg.cpp +++ b/zernikesmoothingdlg.cpp @@ -133,7 +133,7 @@ void ZernikeSmoothingDlg::on_createWaveFront_clicked() l.back().replace(".wft",""); l.back().append(QString("_sm%1").arg(m_noOfTerms)); - m_sm->createSurfaceFromPhaseMap(result, m_wf.m_outside, m_wf.m_inside + m_sm->createWaveFrontFromPhaseMap(result, m_wf.m_outside, m_wf.m_inside ,l.back(), WavefrontOrigin::Smoothed); if (ui->showResidual->isChecked()){