From 02b26ce112d642d044a9fdd77830d6e73fb0e583 Mon Sep 17 00:00:00 2001 From: gr5 Date: Sun, 18 Oct 2026 13:05:01 -0400 Subject: [PATCH 1/4] Added exposure and gain control behind sliders. Also added "auto" checkbox for auto exposure. camera itself remembers settings so didn't use QSettings --- dftarea.cpp | 3 +- igramarea.cpp | 4 +- liveviewdialog.cpp | 169 ++++++++++++++++++--- liveviewdialog.h | 24 +-- mainwindow.cpp | 3 +- videostreamworker.cpp | 335 +++++++++++++++++++++++++++++++++++++++++- videostreamworker.h | 18 +++ 7 files changed, 519 insertions(+), 37 deletions(-) diff --git a/dftarea.cpp b/dftarea.cpp index 3ad3f19b..09b4c7fd 100644 --- a/dftarea.cpp +++ b/dftarea.cpp @@ -1086,8 +1086,7 @@ void DFTArea::mouseMoveEvent(QMouseEvent *event){ } void DFTArea::mouseMovedExternal(int rad){ - double frequencyBin = rad / (static_cast(m_dftsize)/(m_outside.m_radius * 2 * m_scaleFactor)); - emit updateFilterSize(rad); + emit updateFilterSize(rad); } void DFTArea::mousePressEvent(QMouseEvent *event) diff --git a/igramarea.cpp b/igramarea.cpp index fcbcfc49..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.; diff --git a/liveviewdialog.cpp b/liveviewdialog.cpp index 071532fd..235605f2 100644 --- a/liveviewdialog.cpp +++ b/liveviewdialog.cpp @@ -17,7 +17,7 @@ #include #include #include "zernikes.h" - +#include // ========================================== @@ -76,6 +76,8 @@ LiveViewDialog::LiveViewDialog(QWidget *parent) 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]() { @@ -86,6 +88,49 @@ LiveViewDialog::LiveViewDialog(QWidget *parent) 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); @@ -172,6 +217,10 @@ void LiveViewDialog::closeEvent(QCloseEvent *event) { 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"); @@ -190,9 +239,9 @@ void LiveViewDialog::setupUI(const QString &defaultStreamUrl) { 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){ + connect(averageMode, &QCheckBox::checkStateChanged, this,[](int val){ QSettings set; - set.setValue("liveViewAverageMode",val); + set.setValue("liveViewAverageMode", (val==Qt::Checked)); }); maxRMS = new QDoubleSpinBox(this); @@ -343,23 +392,89 @@ void LiveViewDialog::setupUI(const QString &defaultStreamUrl) { dftLayout->addWidget(vivid); sidebarLayout->addWidget(DFTGroup); // Camera Hardware Sliders (Brightness / Exposure) - sidebarLayout->addWidget(new QLabel("Brightness:", this)); - brightnessSlider = new QSlider(Qt::Horizontal, this); - brightnessSlider->setRange(-64, 64); - brightnessSlider->setValue(0); - connect(brightnessSlider, &QSlider::valueChanged, this, [this](int val) { - emit requestCameraSetting(CameraProperty::Brightness, val); + 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(brightnessSlider); + + + sidebarLayout->addWidget(m_brightnessSlider); + + QHBoxLayout *exposureGroupLayout = new QHBoxLayout(); + + // + // exposure slider + // sidebarLayout->addWidget(new QLabel("Exposure / Shutter:", this)); - QSlider *exposureSlider = new QSlider(Qt::Horizontal, this); - exposureSlider->setRange(-13, 0); - exposureSlider->setValue(-6); - connect(exposureSlider, &QSlider::valueChanged, this, [this](int val) { - emit requestCameraSetting(CameraProperty::Exposure, val); + 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)); }); - sidebarLayout->addWidget(exposureSlider); + 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::checkStateChanged, this, [this]() { + m_worker->setAutoExpSetting(m_autoExposureCheck->isChecked()); + }); + + exposureGroupLayout->addWidget(m_autoExposureCheck,0); + + sidebarLayout->addLayout(exposureGroupLayout); + sidebarLayout->addStretch(1); @@ -529,7 +644,7 @@ void LiveViewDialog::initSettingsDialog() { } }); QHBoxLayout *connectTimeoutLayout = new QHBoxLayout(); - connectTimeoutLayout->addWidget(new QLabel("number of blank frames before connect failes:")); + 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); @@ -661,7 +776,6 @@ void LiveViewDialog::onApplySettings() { // Tell the worker thread to switch sources safely if (m_worker) { - qDebug() << "apply emitting requestChangeSource:" << newUrl; emit requestChangeSource(newUrl); } @@ -1100,3 +1214,22 @@ void LiveViewDialog::setOutsidecircle(QPointF center, double 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 index 0e876f3d..5559903e 100644 --- a/liveviewdialog.h +++ b/liveviewdialog.h @@ -19,13 +19,7 @@ #include "liveviewhistory.h" #include class FrameProcessorWorker; -enum class CameraProperty { - Brightness, - Contrast, - Exposure, // Shutter speed - Gain -}; -Q_DECLARE_METATYPE(CameraProperty) + class ResizableScrollArea : public QScrollArea { Q_OBJECT public: @@ -51,8 +45,8 @@ class LiveViewDialog : public QDialog { 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(); @@ -66,13 +60,15 @@ public slots: 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(CameraProperty prop , int val); + void requestCameraSetting(QString prop , double val); void outline(QPointF center, double rad); void blueCircle(double rad); void autoOutLine(); @@ -159,4 +155,14 @@ public slots: 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/mainwindow.cpp b/mainwindow.cpp index 59644c00..9c33bcc3 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -2109,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(); @@ -2372,7 +2371,7 @@ void MainWindow::on_actionLive_view_triggered() m_viewDlg->m_tmpShowLive = false; }); - connect(m_igramArea, &IgramArea::boundary, this, [this](CircleOutline outside, bool cropped, QPointF offset, CircleOutline inside) { + connect(m_igramArea, &IgramArea::boundary, this, [this](CircleOutline outside, bool cropped, QPointF offset) { if (cropped){ outside.translate(offset); diff --git a/videostreamworker.cpp b/videostreamworker.cpp index 9d45c1d3..cad3e0b8 100644 --- a/videostreamworker.cpp +++ b/videostreamworker.cpp @@ -2,6 +2,8 @@ #include #include #include +#include "spdlog/spdlog.h" + VideoStreamWorker::VideoStreamWorker(const QString &source, QObject *parent) : QObject(parent), m_source(source) {} @@ -38,7 +40,8 @@ void VideoStreamWorker::startStream() { return; } - qDebug() << "Stream started successfully"; + spdlog::get("logger")->trace("Stream started successfully"); + m_running = true; // Launch the background capture loop in a dedicated std::thread @@ -59,6 +62,7 @@ void VideoStreamWorker::captureLoop() { bool success = false; { + QMutexLocker locker(&m_controlMutex); if (m_cap.isOpened()) { success = m_cap.read(frame) && !frame.empty(); @@ -73,7 +77,7 @@ void VideoStreamWorker::captureLoop() { m_latestFrame = frame.clone(); } else { errorCount++; -qDebug() << "errorCount" << 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)); @@ -207,7 +211,8 @@ void VideoStreamWorker::changeSource(QString newSource) { { QMutexLocker locker(&m_controlMutex); - qDebug() << "Changing source to:" << newSource; + spdlog::get("logger")->trace("Changing source to: {}", newSource.toStdString()); + m_source = newSource; } @@ -226,7 +231,329 @@ void VideoStreamWorker::setResolution(int width, int height) { if (isInt) { m_cap.set(cv::CAP_PROP_FRAME_WIDTH, width); m_cap.set(cv::CAP_PROP_FRAME_HEIGHT, height); - qDebug() << "VideoStreamWorker: Resolution requested to set ->" << width << "x" << height; + spdlog::get("logger")->trace("VideoStreamWorker: Resolution requested to set -> {} x {}", width, 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 { + // Special formatting for FOURCC to make it readable + if (prop.id == cv::CAP_PROP_FOURCC && value > 0) { + int fourcc = static_cast(value); + char fcc[] = { + static_cast(fourcc & 0XFF), + static_cast((fourcc >> 8) & 0XFF), + static_cast((fourcc >> 16) & 0XFF), + static_cast((fourcc >> 24) & 0XFF), + 0 + }; + qDebug().noquote() << QString("%1: %2 (Raw: %3)").arg(prop.name, -30).arg(fcc).arg(value); + } 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 index 1a1dda2e..865fc2d6 100644 --- a/videostreamworker.h +++ b/videostreamworker.h @@ -6,12 +6,26 @@ #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(); @@ -19,6 +33,8 @@ public slots: void stop(); void changeSource(QString newSource); void setResolution(int width, int height); + void dumpAll(); + void requestCameraSetting(QString prop , double val); signals: @@ -39,4 +55,6 @@ public slots: 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 + }; From 0f74d0bb7522402580361a231580c29036e1c87e Mon Sep 17 00:00:00 2001 From: gr5 Date: Sun, 18 Oct 2026 15:20:33 -0400 Subject: [PATCH 2/4] Small changes to get it to compile on QT5 --- liveviewdialog.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/liveviewdialog.cpp b/liveviewdialog.cpp index 235605f2..dd794f4c 100644 --- a/liveviewdialog.cpp +++ b/liveviewdialog.cpp @@ -239,9 +239,9 @@ void LiveViewDialog::setupUI(const QString &defaultStreamUrl) { averageMode = new QCheckBox("Compute and show average when loop is running", this); averageMode->setChecked(set.value("liveViewAverageMode", false).toBool()); - connect(averageMode, &QCheckBox::checkStateChanged, this,[](int val){ + connect(averageMode, &QCheckBox::stateChanged, this,[](int val){ QSettings set; - set.setValue("liveViewAverageMode", (val==Qt::Checked)); + set.setValue("liveViewAverageMode", val); }); maxRMS = new QDoubleSpinBox(this); @@ -467,7 +467,7 @@ void LiveViewDialog::setupUI(const QString &defaultStreamUrl) { // auto exposure checkbox m_autoExposureCheck = new QCheckBox("Auto", this); - connect(m_autoExposureCheck, &QCheckBox::checkStateChanged, this, [this]() { + connect(m_autoExposureCheck, &QCheckBox::stateChanged, this, [this]() { m_worker->setAutoExpSetting(m_autoExposureCheck->isChecked()); }); From 342d33d658f4852951b03a99f6a88727554c5a80 Mon Sep 17 00:00:00 2001 From: gr5 Date: Mon, 19 Oct 2026 17:19:13 -0400 Subject: [PATCH 3/4] Removed some code that isn't needed or used and Clang was complaining about. --- videostreamworker.cpp | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/videostreamworker.cpp b/videostreamworker.cpp index cad3e0b8..889968c2 100644 --- a/videostreamworker.cpp +++ b/videostreamworker.cpp @@ -321,20 +321,7 @@ void VideoStreamWorker::dumpAll() { if (value == -1) { qDebug().noquote() << QString("%1: NOT SUPPORTED").arg(prop.name, -30); } else { - // Special formatting for FOURCC to make it readable - if (prop.id == cv::CAP_PROP_FOURCC && value > 0) { - int fourcc = static_cast(value); - char fcc[] = { - static_cast(fourcc & 0XFF), - static_cast((fourcc >> 8) & 0XFF), - static_cast((fourcc >> 16) & 0XFF), - static_cast((fourcc >> 24) & 0XFF), - 0 - }; - qDebug().noquote() << QString("%1: %2 (Raw: %3)").arg(prop.name, -30).arg(fcc).arg(value); - } else { - qDebug().noquote() << QString("%1: %2").arg(prop.name, -30).arg(value); - } + qDebug().noquote() << QString("%1: %2").arg(prop.name, -30).arg(value); } } From cfae2136c19466449c41acc166320dc5d17e0679 Mon Sep 17 00:00:00 2001 From: gr5 Date: Tue, 20 Oct 2026 12:57:42 -0400 Subject: [PATCH 4/4] Auto connects to camera when it starts sending blank/zeroed frames. Before I added the cv::mean() function it was happening every 10-30 seconds. Now about once per minute so that alone made it better, lol. The restart takes about 3 seconds versus having to reclick the "0" in settings which then takes about 7 seconds. So this is a big improvement. --- videostreamworker.cpp | 32 ++++++++++++++++++++++++++++++++ videostreamworker.h | 2 ++ 2 files changed, 34 insertions(+) diff --git a/videostreamworker.cpp b/videostreamworker.cpp index 889968c2..dbba8ffb 100644 --- a/videostreamworker.cpp +++ b/videostreamworker.cpp @@ -75,8 +75,38 @@ void VideoStreamWorker::captureLoop() { 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)); @@ -232,6 +262,8 @@ void VideoStreamWorker::setResolution(int width, int height) { 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; } } } diff --git a/videostreamworker.h b/videostreamworker.h index 865fc2d6..5589d838 100644 --- a/videostreamworker.h +++ b/videostreamworker.h @@ -56,5 +56,7 @@ public slots: 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; };