Split macro-condition-video.cpp UI into per-widget files
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled

This makes it easier to conditionally exclude video-edit-object.cpp
from the build when OpenCV >= 5 drops CascadeClassifier support.
This commit is contained in:
WarmUpTill 2026-07-18 22:13:54 +02:00 committed by WarmUpTill
parent 1030868a75
commit 107d77f1b8
23 changed files with 1098 additions and 861 deletions

View File

@ -302,6 +302,7 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
log_info "Configure OpenCV (x86_64) ..."
cmake -S . -B build_x86_64 ${opencv_cmake_args_common} \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
-DCMAKE_PROJECT_INCLUDE="${SCRIPT_HOME}/opencv-force-processor-x86_64.cmake" \
-DCMAKE_INSTALL_PREFIX="${opencv_install_x86}" \
-DWITH_IPP=OFF
@ -344,7 +345,7 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
-DSW_BUILD=OFF
-DOPENJPEG_SUPPORT=OFF
-DLIBWEBP_SUPPORT=OFF
-DENABLE_WEBP=OFF
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE

View File

@ -0,0 +1,7 @@
# Injected via CMAKE_PROJECT_INCLUDE after opencv's project() call. Forces
# CMAKE_SYSTEM_PROCESSOR into the cache so that third-party subdirectories
# (mlas) that read the cache directly see x86_64 instead of the arm64 host
# processor on Apple Silicon CI runners.
set(CMAKE_SYSTEM_PROCESSOR
x86_64
CACHE INTERNAL "" FORCE)

View File

@ -6,7 +6,7 @@ on:
description: "Project name detected by parsing build spec file"
value: ${{ jobs.check-event.outputs.pluginName }}
env:
DEP_DIR: .deps/advss-build-dependencies-5
DEP_DIR: .deps/advss-build-dependencies-6
jobs:
check-event:
name: Check GitHub Event Data 🔎

View File

@ -30,10 +30,29 @@ if(OS_LINUX AND NOT Leptonica_FOUND)
endif()
endif()
# --- Check cascade classifier availability ---
# CascadeClassifier was moved to the xobjdetect contrib module in OpenCV 5
if(OpenCV_VERSION_MAJOR LESS 5
OR OpenCV_xobjdetect_FOUND
OR "opencv_xobjdetect" IN_LIST OpenCV_LIBS)
set(ADVSS_CASCADE_SUPPORT ON)
else()
message(
WARNING
"Cascade classifier support disabled!\n"
"OpenCV 5+ requires the opencv_xobjdetect contrib module.\n\n"
"Rebuild OpenCV with -DBUILD_LIST=...,xobjdetect and -DOPENCV_EXTRA_MODULES_PATH=<opencv_contrib>/modules"
)
endif()
# --- End of section ---
add_library(${PROJECT_NAME} MODULE)
if(ADVSS_CASCADE_SUPPORT)
target_compile_definitions(${PROJECT_NAME} PRIVATE ADVSS_CASCADE_SUPPORT)
endif()
if(Leptonica_FOUND AND Tesseract_FOUND)
target_compile_definitions(${PROJECT_NAME} PRIVATE OCR_SUPPORT)
target_link_libraries(${PROJECT_NAME} PRIVATE Tesseract::libtesseract
@ -54,8 +73,11 @@ target_sources(
${PROJECT_NAME}
PRIVATE area-selection.cpp
area-selection.hpp
cascade-classifier-detector.cpp
cascade-classifier-detector.hpp
macro-condition-video.cpp
macro-condition-video.hpp
object-detector.hpp
opencv-helpers.cpp
opencv-helpers.hpp
parameter-wrappers.cpp
@ -63,7 +85,12 @@ target_sources(
preview-dialog.cpp
preview-dialog.hpp
screenshot-dialog.cpp
screenshot-dialog.hpp)
screenshot-dialog.hpp
video-edit-area.cpp
video-edit-brightness.cpp
video-edit-color.cpp
video-edit-object.cpp
video-edit-ocr.cpp)
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")

View File

@ -1,8 +1,9 @@
#include "area-selection.hpp"
#include "obs-module-helper.hpp"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <obs-module-helper.hpp>
#include <QVBoxLayout>
namespace advss {

View File

@ -1,10 +1,11 @@
#pragma once
#include <variable-spinbox.hpp>
#include "variable-spinbox.hpp"
#include <QWidget>
#include <obs-data.h>
#include <opencv2/opencv.hpp>
#include <QWidget>
namespace advss {
struct Size {

View File

@ -0,0 +1,109 @@
#include "cascade-classifier-detector.hpp"
#ifdef ADVSS_CASCADE_SUPPORT
#include "opencv-helpers.hpp"
#include "log-helper.hpp"
#if CV_VERSION_MAJOR < 5
#include <opencv2/objdetect.hpp>
#else
#include <opencv2/xobjdetect.hpp>
#endif
namespace advss {
struct CascadeClassifierDetector::Impl {
cv::CascadeClassifier cascade;
};
CascadeClassifierDetector::CascadeClassifierDetector()
: _impl(std::make_unique<Impl>())
{
}
CascadeClassifierDetector::~CascadeClassifierDetector() = default;
bool CascadeClassifierDetector::IsSupported()
{
return true;
}
bool CascadeClassifierDetector::Load(const std::string &modelPath)
{
try {
if (!_impl->cascade.load(modelPath)) {
blog(LOG_WARNING, "failed to load cascade model \"%s\"",
modelPath.c_str());
return false;
}
} catch (...) {
blog(LOG_WARNING, "failed to load cascade model \"%s\"",
modelPath.c_str());
return false;
}
return !_impl->cascade.empty();
}
bool CascadeClassifierDetector::IsLoaded() const
{
return !_impl->cascade.empty();
}
std::vector<cv::Rect> CascadeClassifierDetector::Detect(QImage &img)
{
if (img.isNull() || _impl->cascade.empty()) {
return {};
}
auto image = QImageToMat(img);
cv::Mat frameGray;
cv::cvtColor(image, frameGray, cv::COLOR_RGBA2GRAY);
cv::equalizeHist(frameGray, frameGray);
std::vector<cv::Rect> objects;
try {
_impl->cascade.detectMultiScale(frameGray, objects, scaleFactor,
minNeighbors, 0, minSize,
maxSize);
} catch (const std::exception &e) {
vblog(LOG_INFO, "detectMultiScale failed: %s", e.what());
}
return objects;
}
} // namespace advss
#else // ADVSS_CASCADE_SUPPORT
namespace advss {
struct CascadeClassifierDetector::Impl {};
CascadeClassifierDetector::CascadeClassifierDetector()
: _impl(std::make_unique<Impl>())
{
}
CascadeClassifierDetector::~CascadeClassifierDetector() = default;
bool CascadeClassifierDetector::IsSupported()
{
return false;
}
bool CascadeClassifierDetector::Load(const std::string &)
{
return false;
}
bool CascadeClassifierDetector::IsLoaded() const
{
return false;
}
std::vector<cv::Rect> CascadeClassifierDetector::Detect(QImage &)
{
return {};
}
} // namespace advss
#endif // ADVSS_CASCADE_SUPPORT

View File

@ -0,0 +1,27 @@
#pragma once
#include "object-detector.hpp"
#include <memory>
namespace advss {
class CascadeClassifierDetector : public ObjectDetector {
public:
CascadeClassifierDetector();
~CascadeClassifierDetector() override;
static bool IsSupported();
bool Load(const std::string &modelPath) override;
bool IsLoaded() const override;
std::vector<cv::Rect> Detect(QImage &img) override;
double scaleFactor = 1.1;
int minNeighbors = 3;
cv::Size minSize{0, 0};
cv::Size maxSize{0, 0};
private:
struct Impl;
std::unique_ptr<Impl> _impl;
};
} // namespace advss

View File

@ -1,17 +1,15 @@
#include "macro-condition-video.hpp"
#include "cascade-classifier-detector.hpp"
#include "screenshot-dialog.hpp"
#include "layout-helpers.hpp"
#include "macro-condition-edit.hpp"
#include "plugin-state-helpers.hpp"
#include "selection-helpers.hpp"
#include "ui-helpers.hpp"
#include <layout-helpers.hpp>
#include <macro-condition-edit.hpp>
#include <plugin-state-helpers.hpp>
#include <QBuffer>
#include <QDesktopServices>
#include <QFileDialog>
#include <QMessageBox>
#include <QtGlobal>
#include <QToolTip>
#include <ui-helpers.hpp>
#include <selection-helpers.hpp>
namespace advss {
@ -22,30 +20,6 @@ bool MacroConditionVideo::_registered = MacroConditionFactory::Register(
{MacroConditionVideo::Create, MacroConditionVideoEdit::Create,
"AdvSceneSwitcher.condition.video"});
const static std::map<VideoCondition, std::string> conditionTypes = {
{VideoCondition::MATCH,
"AdvSceneSwitcher.condition.video.condition.match"},
{VideoCondition::DIFFER,
"AdvSceneSwitcher.condition.video.condition.differ"},
{VideoCondition::HAS_NOT_CHANGED,
"AdvSceneSwitcher.condition.video.condition.hasNotChanged"},
{VideoCondition::HAS_CHANGED,
"AdvSceneSwitcher.condition.video.condition.hasChanged"},
{VideoCondition::NO_IMAGE,
"AdvSceneSwitcher.condition.video.condition.noImage"},
{VideoCondition::PATTERN,
"AdvSceneSwitcher.condition.video.condition.pattern"},
{VideoCondition::OBJECT,
"AdvSceneSwitcher.condition.video.condition.object"},
{VideoCondition::BRIGHTNESS,
"AdvSceneSwitcher.condition.video.condition.brightness"},
#ifdef OCR_SUPPORT
{VideoCondition::OCR, "AdvSceneSwitcher.condition.video.condition.ocr"},
#endif
{VideoCondition::COLOR,
"AdvSceneSwitcher.condition.video.condition.color"},
};
const static std::map<VideoInput::Type, std::string> videoInputTypes = {
{VideoInput::Type::OBS_MAIN_OUTPUT,
"AdvSceneSwitcher.condition.video.type.main"},
@ -64,27 +38,6 @@ const static std::map<cv::TemplateMatchModes, std::string> patternMatchModes = {
"AdvSceneSwitcher.condition.video.patternMatchMode.squaredDifference"},
};
const static std::map<tesseract::PageSegMode, std::string> pageSegModes = {
{tesseract::PageSegMode::PSM_SINGLE_COLUMN,
"AdvSceneSwitcher.condition.video.ocrMode.singleColumn"},
{tesseract::PageSegMode::PSM_SINGLE_BLOCK_VERT_TEXT,
"AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText"},
{tesseract::PageSegMode::PSM_SINGLE_BLOCK,
"AdvSceneSwitcher.condition.video.ocrMode.singleBlock"},
{tesseract::PageSegMode::PSM_SINGLE_LINE,
"AdvSceneSwitcher.condition.video.ocrMode.singleLine"},
{tesseract::PageSegMode::PSM_SINGLE_WORD,
"AdvSceneSwitcher.condition.video.ocrMode.singleWord"},
{tesseract::PageSegMode::PSM_CIRCLE_WORD,
"AdvSceneSwitcher.condition.video.ocrMode.circleWord"},
{tesseract::PageSegMode::PSM_SINGLE_CHAR,
"AdvSceneSwitcher.condition.video.ocrMode.singleChar"},
{tesseract::PageSegMode::PSM_SPARSE_TEXT,
"AdvSceneSwitcher.condition.video.ocrMode.sparseText"},
{tesseract::PageSegMode::PSM_SPARSE_TEXT_OSD,
"AdvSceneSwitcher.condition.video.ocrMode.sparseTextOSD"},
};
static bool requiresFileInput(VideoCondition t)
{
return t == VideoCondition::MATCH || t == VideoCondition::DIFFER ||
@ -94,7 +47,7 @@ static bool requiresFileInput(VideoCondition t)
bool MacroConditionVideo::CheckShouldBeSkipped()
{
if (_condition != VideoCondition::PATTERN &&
_condition != VideoCondition::OBJECT &&
_condition != VideoCondition::OBJECT_CASCADE &&
_condition != VideoCondition::HAS_CHANGED &&
_condition != VideoCondition::HAS_NOT_CHANGED) {
return false;
@ -185,7 +138,7 @@ bool MacroConditionVideo::Save(obs_data_t *obj) const
_blockUntilScreenshotDone);
_brightnessThreshold.Save(obj, "brightnessThreshold");
_patternMatchParameters.Save(obj);
_objMatchParameters.Save(obj);
_cascadeMatchParameters.Save(obj);
_ocrParameters.Save(obj);
_colorParameters.Save(obj);
obs_data_set_bool(obj, "throttleEnabled", _throttleEnabled);
@ -211,7 +164,7 @@ bool MacroConditionVideo::Load(obs_data_t *obj)
_brightnessThreshold.Load(obj, "brightnessThreshold");
}
_patternMatchParameters.Load(obj);
_objMatchParameters.Load(obj);
_cascadeMatchParameters.Load(obj);
_ocrParameters.Load(obj);
_colorParameters.Load(obj);
_throttleEnabled = obs_data_get_bool(obj, "throttleEnabled");
@ -378,15 +331,11 @@ bool MacroConditionVideo::OutputChanged()
bool MacroConditionVideo::ScreenshotContainsObject()
{
auto model = _objMatchParameters.GetModel();
if (!model) {
auto *detector = _cascadeMatchParameters.GetDetector();
if (!detector) {
return false;
}
auto objects = MatchObject(_screenshotData.GetImage(), *model,
_objMatchParameters.scaleFactor,
_objMatchParameters.minNeighbors,
_objMatchParameters.minSize.CV(),
_objMatchParameters.maxSize.CV());
auto objects = detector->Detect(_screenshotData.GetImage());
const auto count = objects.size();
SetTempVarValue("objectCount", std::to_string(count));
return count > 0;
@ -463,7 +412,7 @@ bool MacroConditionVideo::Compare()
return _screenshotData.GetImage().isNull();
case VideoCondition::PATTERN:
return ScreenshotContainsPattern();
case VideoCondition::OBJECT:
case VideoCondition::OBJECT_CASCADE:
return ScreenshotContainsObject();
case VideoCondition::BRIGHTNESS:
return CheckBrightnessThreshold();
@ -531,7 +480,7 @@ void MacroConditionVideo::SetupTempVars()
obs_module_text(
"AdvSceneSwitcher.tempVar.video.matchHeight.description"));
break;
case VideoCondition::OBJECT:
case VideoCondition::OBJECT_CASCADE:
AddTempvar(
"objectCount",
obs_module_text(
@ -584,18 +533,41 @@ static inline void populateVideoInputSelection(QComboBox *list)
static inline void populateConditionSelection(QComboBox *list)
{
const static std::vector<std::pair<VideoCondition, std::string>>
conditionTypes = {
{VideoCondition::MATCH,
"AdvSceneSwitcher.condition.video.condition.match"},
{VideoCondition::DIFFER,
"AdvSceneSwitcher.condition.video.condition.differ"},
{VideoCondition::HAS_NOT_CHANGED,
"AdvSceneSwitcher.condition.video.condition.hasNotChanged"},
{VideoCondition::HAS_CHANGED,
"AdvSceneSwitcher.condition.video.condition.hasChanged"},
{VideoCondition::NO_IMAGE,
"AdvSceneSwitcher.condition.video.condition.noImage"},
{VideoCondition::PATTERN,
"AdvSceneSwitcher.condition.video.condition.pattern"},
{VideoCondition::OBJECT_CASCADE,
"AdvSceneSwitcher.condition.video.condition.object"},
{VideoCondition::BRIGHTNESS,
"AdvSceneSwitcher.condition.video.condition.brightness"},
#ifdef OCR_SUPPORT
{VideoCondition::OCR,
"AdvSceneSwitcher.condition.video.condition.ocr"},
#endif
{VideoCondition::COLOR,
"AdvSceneSwitcher.condition.video.condition.color"},
};
for (auto &[value, name] : conditionTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(value));
}
}
static inline void populatePageSegModeSelection(QComboBox *list)
{
for (const auto &[mode, name] : pageSegModes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(mode));
}
SetRowVisibleByValue(
list,
obs_module_text(
"AdvSceneSwitcher.condition.video.condition.object"),
CascadeClassifierDetector::IsSupported());
}
static inline void populatePatternMatchModeSelection(QComboBox *list)
@ -606,628 +578,6 @@ static inline void populatePatternMatchModeSelection(QComboBox *list)
}
}
BrightnessEdit::BrightnessEdit(QWidget *parent,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_threshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.brightnessThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.brightnessThresholdDescription"))),
_current(new QLabel),
_entryData(data)
{
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_threshold);
layout->addWidget(_current);
setLayout(layout);
QWidget::connect(
_threshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(BrightnessThresholdChanged(
const NumberVariable<double> &)));
QWidget::connect(&_timer, &QTimer::timeout, this,
&BrightnessEdit::UpdateCurrentBrightness);
_timer.start(1000);
_threshold->SetDoubleValue(_entryData->_brightnessThreshold);
_loading = false;
}
void BrightnessEdit::UpdateCurrentBrightness()
{
QString text = obs_module_text(
"AdvSceneSwitcher.condition.video.currentBrightness");
_current->setText(text.arg(_entryData->GetCurrentBrightness()));
}
void BrightnessEdit::BrightnessThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_brightnessThreshold = value;
}
static void openFileInEditor(const std::string &filepath)
{
const auto path = QString::fromStdString(filepath);
const QFileInfo fileInfo(path);
if (!fileInfo.exists()) {
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrOpenConfig.createFailed"));
return;
}
file.close();
}
QUrl fileUrl = QUrl::fromLocalFile(path);
if (!QDesktopServices::openUrl(fileUrl)) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrOpenConfig.openFailed"));
}
}
OCREdit::OCREdit(QWidget *parent, PreviewDialog *previewDialog,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_matchText(new VariableTextEdit(this)),
_regex(new RegexConfigWidget(this)),
_colorButton(new VariableColorButton(
this,
obs_module_text(
"AdvSceneSwitcher.condition.video.selectColor"))),
_colorThreshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
true)),
_pageSegMode(new QComboBox()),
_tesseractBaseDir(new FileSelection(FileSelection::Type::FOLDER)),
_languageCode(new VariableLineEdit(this)),
_useConfig(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrUseConfigFile"))),
_configFile(new FileSelection(FileSelection::Type::WRITE, this)),
_openConfigFile(new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrOpenConfigFile"))),
_reloadConfig(new QPushButton()),
_configLayout(new QHBoxLayout()),
_previewDialog(previewDialog),
_entryData(data)
{
populatePageSegModeSelection(_pageSegMode);
_reloadConfig->setMaximumWidth(22);
SetButtonIcon(_reloadConfig, GetThemeTypeName() == "Light"
? ":res/images/refresh.svg"
: "theme:Dark/refresh.svg");
_reloadConfig->setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrConfigReload"));
QWidget::connect(_colorButton,
SIGNAL(ColorVariableChanged(const ColorVariable &)),
this, SLOT(ColorChanged(const ColorVariable &)));
QWidget::connect(
_colorThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
QWidget::connect(_matchText, SIGNAL(textChanged()), this,
SLOT(MatchTextChanged()));
QWidget::connect(_regex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(RegexChanged(const RegexConfig &)));
QWidget::connect(_pageSegMode, SIGNAL(currentIndexChanged(int)), this,
SLOT(PageSegModeChanged(int)));
QWidget::connect(_tesseractBaseDir,
SIGNAL(PathChanged(const QString &)), this,
SLOT(TesseractBaseDirChanged(const QString &)));
QWidget::connect(_languageCode, SIGNAL(editingFinished()), this,
SLOT(LanguageChanged()));
QWidget::connect(_useConfig, SIGNAL(stateChanged(int)), this,
SLOT(UseConfigChanged(int)));
QWidget::connect(_configFile, SIGNAL(PathChanged(const QString &)),
this, SLOT(ConfigFileChanged(const QString &)));
QWidget::connect(_openConfigFile, &QPushButton::clicked, [this](bool) {
openFileInEditor(
_entryData->_ocrParameters.GetCustomConfigFile());
});
QWidget::connect(_reloadConfig, &QPushButton::clicked, [this](bool) {
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.EnableCustomConfig(true);
_previewDialog->OCRParametersChanged(
_entryData->_ocrParameters);
});
auto configFileHint = new QLabel();
const QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
const QIcon icon(path);
const QPixmap pixmap = icon.pixmap(QSize(16, 16));
configFileHint->setPixmap(pixmap);
configFileHint->setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrConfigHint"));
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{color}}", _colorButton},
{"{{textType}}", _pageSegMode},
{"{{tesseractBaseDir}}", _tesseractBaseDir},
{"{{languageCode}}", _languageCode},
{"{{configFile}}", _configFile},
{"{{openConfigFile}}", _openConfigFile},
{"{{reloadConfig}}", _reloadConfig},
{"{{configFileHint}}", configFileHint},
};
auto layout = new QVBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
auto textLayout = new QHBoxLayout();
textLayout->setContentsMargins(0, 0, 0, 0);
textLayout->addWidget(_matchText);
textLayout->addWidget(_regex);
layout->addLayout(textLayout);
auto pageModeSegLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrTextType"),
pageModeSegLayout, widgetPlaceholders);
layout->addLayout(pageModeSegLayout);
auto baseDirLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrBaseDir"),
baseDirLayout, widgetPlaceholders, false);
layout->addLayout(baseDirLayout);
auto languageLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrLanguage"),
languageLayout, widgetPlaceholders);
layout->addLayout(languageLayout);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrConfig"),
_configLayout, widgetPlaceholders, false);
layout->addWidget(_useConfig);
layout->addLayout(_configLayout);
auto colorPickLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrColorPick"),
colorPickLayout, widgetPlaceholders);
layout->addLayout(colorPickLayout);
layout->addWidget(_colorThreshold);
setLayout(layout);
_matchText->setPlainText(_entryData->_ocrParameters.text);
_regex->SetRegexConfig(_entryData->_ocrParameters.regex);
_colorButton->SetValue(_entryData->_ocrParameters.color);
_colorThreshold->SetDoubleValue(
_entryData->_ocrParameters.colorThreshold);
_pageSegMode->setCurrentIndex(_pageSegMode->findData(
static_cast<int>(_entryData->_ocrParameters.GetPageMode())));
_tesseractBaseDir->SetPath(
_entryData->_ocrParameters.GetTesseractBasePath());
_languageCode->setText(_entryData->_ocrParameters.GetLanguageCode());
_useConfig->setChecked(
_entryData->_ocrParameters.CustomConfigIsEnabled());
_configFile->SetPath(_entryData->_ocrParameters.GetCustomConfigFile());
SetLayoutVisible(_configLayout,
_entryData->_ocrParameters.CustomConfigIsEnabled());
_loading = false;
}
void OCREdit::ColorChanged(const ColorVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.color = value;
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::ColorThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.colorThreshold = value;
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::MatchTextChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.text =
_matchText->toPlainText().toUtf8().constData();
adjustSize();
updateGeometry();
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::RegexChanged(const RegexConfig &conf)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.regex = conf;
adjustSize();
updateGeometry();
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::PageSegModeChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->SetPageSegMode(static_cast<tesseract::PageSegMode>(
_pageSegMode->itemData(idx).toInt()));
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::TesseractBaseDirChanged(const QString &path)
{
GUARD_LOADING_AND_LOCK();
if (!_entryData->SetTesseractBaseDir(path.toStdString())) {
const QString message(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
const QDir dataDir(path);
const QString fileName(_languageCode->text() + ".traineddata");
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
// Reset to previous value
const QSignalBlocker b(this);
_tesseractBaseDir->SetPath(
_entryData->_ocrParameters.GetTesseractBasePath());
return;
}
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::LanguageChanged()
{
GUARD_LOADING_AND_LOCK();
if (!_entryData->SetLanguageCode(_languageCode->text().toStdString())) {
const QString message(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
const QDir dataDir(QString::fromStdString(
_entryData->_ocrParameters.GetTesseractBasePath()));
const QString fileName(_languageCode->text() + ".traineddata");
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
// Reset to previous value
const QSignalBlocker b(this);
_languageCode->setText(
_entryData->_ocrParameters.GetLanguageCode());
return;
}
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::UseConfigChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.EnableCustomConfig(value);
SetLayoutVisible(_configLayout, value);
adjustSize();
updateGeometry();
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::ConfigFileChanged(const QString &path)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.SetCustomConfigFile(path.toStdString());
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
ObjectDetectEdit::ObjectDetectEdit(
QWidget *parent, PreviewDialog *previewDialog,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_modelDataPath(new FileSelection()),
_objectScaleThreshold(new SliderSpinBox(
1.1, 5.,
obs_module_text(
"AdvSceneSwitcher.condition.video.objectScaleThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.objectScaleThresholdDescription"))),
_minNeighbors(new QSpinBox()),
_minNeighborsDescription(new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.video.minNeighborDescription"))),
_minSize(new SizeSelection(0, 1024)),
_maxSize(new SizeSelection(0, 4096)),
_previewDialog(previewDialog),
_entryData(data)
{
_minNeighbors->setMinimum(minMinNeighbors);
_minNeighbors->setMaximum(maxMinNeighbors);
QWidget::connect(
_objectScaleThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(ObjectScaleThresholdChanged(
const NumberVariable<double> &)));
QWidget::connect(_minNeighbors, SIGNAL(valueChanged(int)), this,
SLOT(MinNeighborsChanged(int)));
QWidget::connect(_minSize, SIGNAL(SizeChanged(Size)), this,
SLOT(MinSizeChanged(Size)));
QWidget::connect(_maxSize, SIGNAL(SizeChanged(Size)), this,
SLOT(MaxSizeChanged(Size)));
QWidget::connect(_modelDataPath, SIGNAL(PathChanged(const QString &)),
this, SLOT(ModelPathChanged(const QString &)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{minNeighbors}}", _minNeighbors},
{"{{minSize}}", _minSize},
{"{{maxSize}}", _maxSize},
{"{{modelDataPath}}", _modelDataPath},
};
auto pathLayout = new QHBoxLayout;
pathLayout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.modelPath"),
pathLayout, widgetPlaceholders);
auto neighborsLayout = new QHBoxLayout;
neighborsLayout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.minNeighbor"),
neighborsLayout, widgetPlaceholders);
auto sizeGrid = new QGridLayout;
sizeGrid->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.video.minSize")),
0, 0);
sizeGrid->addWidget(_minSize, 0, 1);
sizeGrid->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.video.maxSize")),
1, 0);
sizeGrid->addWidget(_maxSize, 1, 1);
auto sizeLayout = new QHBoxLayout;
sizeLayout->setContentsMargins(0, 0, 0, 0);
sizeLayout->addLayout(sizeGrid);
sizeLayout->addStretch();
auto layout = new QVBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addLayout(pathLayout);
layout->addLayout(neighborsLayout);
layout->addLayout(sizeLayout);
setLayout(layout);
_modelDataPath->SetPath(_entryData->_objMatchParameters.GetModelPath());
_objectScaleThreshold->SetDoubleValue(
_entryData->_objMatchParameters.scaleFactor);
_minNeighbors->setValue(_entryData->_objMatchParameters.minNeighbors);
_minSize->SetSize(_entryData->_objMatchParameters.minSize);
_maxSize->SetSize(_entryData->_objMatchParameters.maxSize);
_loading = false;
}
void ObjectDetectEdit::ObjectScaleThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_objMatchParameters.scaleFactor = value;
_previewDialog->ObjDetectParametersChanged(
_entryData->_objMatchParameters);
}
void ObjectDetectEdit::MinNeighborsChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_objMatchParameters.minNeighbors = value;
_previewDialog->ObjDetectParametersChanged(
_entryData->_objMatchParameters);
}
void ObjectDetectEdit::MinSizeChanged(advss::Size value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_objMatchParameters.minSize = value;
_previewDialog->ObjDetectParametersChanged(
_entryData->_objMatchParameters);
}
void ObjectDetectEdit::MaxSizeChanged(advss::Size value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_objMatchParameters.maxSize = value;
_previewDialog->ObjDetectParametersChanged(
_entryData->_objMatchParameters);
}
void ObjectDetectEdit::ModelPathChanged(const QString &text)
{
if (_loading || !_entryData) {
return;
}
bool dataLoaded = false;
{
auto lock = LockContext();
std::string path = text.toStdString();
dataLoaded = _entryData->_objMatchParameters.SetModelPath(path);
}
if (!dataLoaded) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.condition.video.modelLoadFail"));
}
_previewDialog->ObjDetectParametersChanged(
_entryData->_objMatchParameters);
}
ColorEdit::ColorEdit(QWidget *parent,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_matchThreshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.colorMatchThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.colorMatchThresholdDescription"),
true)),
_colorThreshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
true)),
_colorButton(new VariableColorButton(
this,
obs_module_text(
"AdvSceneSwitcher.condition.video.selectColor"))),
_entryData(data)
{
QWidget::connect(_colorButton,
SIGNAL(ColorVariableChanged(const ColorVariable &)),
this, SLOT(ColorChanged(const ColorVariable &)));
QWidget::connect(
_matchThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(MatchThresholdChanged(const NumberVariable<double> &)));
QWidget::connect(
_colorThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{color}}", _colorButton},
};
auto colorLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.condition.video.layout.color"),
colorLayout, widgetPlaceholders);
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addLayout(colorLayout);
layout->addWidget(_colorThreshold);
layout->addWidget(_matchThreshold);
setLayout(layout);
_matchThreshold->SetDoubleValue(
_entryData->_colorParameters.matchThreshold);
_colorThreshold->SetDoubleValue(
_entryData->_colorParameters.colorThreshold);
_colorButton->SetValue(_entryData->_colorParameters.color);
_loading = false;
}
void ColorEdit::ColorChanged(const ColorVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_colorParameters.color = value;
}
void ColorEdit::MatchThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_colorParameters.matchThreshold = value;
}
void ColorEdit::ColorThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_colorParameters.colorThreshold = value;
}
AreaEdit::AreaEdit(QWidget *parent, PreviewDialog *previewDialog,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_checkAreaEnable(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.condition.video.layout.checkAreaEnable"))),
_checkArea(new AreaSelection(0, 99999)),
_selectArea(new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.video.selectArea"))),
_previewDialog(previewDialog),
_entryData(data)
{
QWidget::connect(_checkAreaEnable, SIGNAL(stateChanged(int)), this,
SLOT(CheckAreaEnableChanged(int)));
QWidget::connect(_checkArea, SIGNAL(AreaChanged(Area)), this,
SLOT(CheckAreaChanged(Area)));
QWidget::connect(_selectArea, SIGNAL(clicked()), this,
SLOT(SelectAreaClicked()));
QWidget::connect(_previewDialog, SIGNAL(SelectionAreaChanged(QRect)),
this, SLOT(CheckAreaChanged(QRect)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{checkAreaEnable}}", _checkAreaEnable},
{"{{checkArea}}", _checkArea},
{"{{selectArea}}", _selectArea},
};
auto layout = new QHBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.checkArea"),
layout, widgetPlaceholders);
setLayout(layout);
_checkAreaEnable->setChecked(_entryData->_areaParameters.enable);
_checkArea->SetArea(_entryData->_areaParameters.area);
SetWidgetVisibility();
_loading = false;
}
void AreaEdit::SetWidgetVisibility()
{
_checkArea->setVisible(_entryData->_areaParameters.enable);
_selectArea->setVisible(_entryData->_areaParameters.enable);
adjustSize();
updateGeometry();
}
void AreaEdit::SelectAreaClicked()
{
_previewDialog->show();
_previewDialog->raise();
_previewDialog->activateWindow();
_previewDialog->SelectArea();
}
void AreaEdit::CheckAreaEnableChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_areaParameters.enable = value;
SetWidgetVisibility();
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
emit Resized();
}
void AreaEdit::CheckAreaChanged(Area value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_areaParameters.area = value;
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
}
void AreaEdit::CheckAreaChanged(QRect rect)
{
const QSignalBlocker b(_checkArea);
Area area{rect.topLeft().x(), rect.y(), rect.width(), rect.height()};
_checkArea->SetArea(area);
CheckAreaChanged(area);
}
static QStringList getVideoSourcesList()
{
auto sources = GetVideoSourceNames();
@ -1263,7 +613,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
_previewDialog(this),
_brightness(new BrightnessEdit(this, entryData)),
_ocr(new OCREdit(this, &_previewDialog, entryData)),
_objectDetect(new ObjectDetectEdit(this, &_previewDialog, entryData)),
_cascadeClassifierEdit(
new CascadeClassifierEdit(this, &_previewDialog, entryData)),
_color(new ColorEdit(this, entryData)),
_area(new AreaEdit(this, &_previewDialog, entryData)),
_throttleControlLayout(new QHBoxLayout),
@ -1292,8 +643,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
QSizePolicy::Preferred);
_ocr->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_objectDetect->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_cascadeClassifierEdit->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_color->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_area->setSizePolicy(QSizePolicy::MinimumExpanding,
@ -1392,7 +743,7 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
mainLayout->addLayout(_patternMatchModeLayout);
mainLayout->addWidget(_brightness);
mainLayout->addWidget(_ocr);
mainLayout->addWidget(_objectDetect);
mainLayout->addWidget(_cascadeClassifierEdit);
mainLayout->addWidget(_color);
mainLayout->addLayout(_throttleControlLayout);
mainLayout->addWidget(_area);
@ -1634,13 +985,14 @@ void MacroConditionVideoEdit::ShowMatchClicked()
static bool needsShowMatch(VideoCondition cond)
{
return cond == VideoCondition::PATTERN ||
cond == VideoCondition::OBJECT || cond == VideoCondition::OCR;
cond == VideoCondition::OBJECT_CASCADE ||
cond == VideoCondition::OCR;
}
static bool needsThrottleControls(VideoCondition cond)
{
return cond == VideoCondition::PATTERN ||
cond == VideoCondition::OBJECT ||
cond == VideoCondition::OBJECT_CASCADE ||
cond == VideoCondition::HAS_CHANGED ||
cond == VideoCondition::HAS_NOT_CHANGED;
}
@ -1681,8 +1033,8 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
VideoCondition::BRIGHTNESS);
_showMatch->setVisible(needsShowMatch(_entryData->GetCondition()));
_ocr->setVisible(_entryData->GetCondition() == VideoCondition::OCR);
_objectDetect->setVisible(_entryData->GetCondition() ==
VideoCondition::OBJECT);
_cascadeClassifierEdit->setVisible(_entryData->GetCondition() ==
VideoCondition::OBJECT_CASCADE);
_color->setVisible(_entryData->GetCondition() == VideoCondition::COLOR);
SetLayoutVisible(_throttleControlLayout,
needsThrottleControls(_entryData->GetCondition()));
@ -1725,8 +1077,8 @@ void MacroConditionVideoEdit::SetupPreviewDialogParams()
{
_previewDialog.PatternMatchParametersChanged(
_entryData->_patternMatchParameters);
_previewDialog.ObjDetectParametersChanged(
_entryData->_objMatchParameters);
_previewDialog.CascadeClassifierParametersChanged(
_entryData->_cascadeMatchParameters);
_previewDialog.OCRParametersChanged(_entryData->_ocrParameters);
_previewDialog.VideoSelectionChanged(_entryData->_video);
_previewDialog.AreaParametersChanged(_entryData->_areaParameters);

View File

@ -4,18 +4,21 @@
#include "parameter-wrappers.hpp"
#include "preview-dialog.hpp"
#include <help-icon.hpp>
#include <macro-condition-edit.hpp>
#include <file-selection.hpp>
#include <screenshot-helper.hpp>
#include <slider-spinbox.hpp>
#include <source-helpers.hpp>
#include <variable-color-button.hpp>
#include <variable-line-edit.hpp>
#include <variable-text-edit.hpp>
#include "help-icon.hpp"
#include "section.hpp"
#include "macro-condition-edit.hpp"
#include "file-selection.hpp"
#include "screenshot-helper.hpp"
#include "slider-spinbox.hpp"
#include "source-helpers.hpp"
#include "variable-color-button.hpp"
#include "variable-line-edit.hpp"
#include "variable-text-edit.hpp"
#include <QCheckBox>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QLineEdit>
#include <QDateTime>
#include <QGridLayout>
#include <QHBoxLayout>
@ -70,7 +73,7 @@ public:
bool _blockUntilScreenshotDone = true;
NumberVariable<double> _brightnessThreshold = 0.5;
PatternMatchParameters _patternMatchParameters;
ObjDetectParameters _objMatchParameters;
CascadeClassifierParameters _cascadeMatchParameters;
OCRParameters _ocrParameters;
ColorParameters _colorParameters;
AreaParameters _areaParameters;
@ -174,12 +177,12 @@ private:
bool _loading = true;
};
class ObjectDetectEdit : public QWidget {
class CascadeClassifierEdit : public QWidget {
Q_OBJECT
public:
ObjectDetectEdit(QWidget *parent, PreviewDialog *,
const std::shared_ptr<MacroConditionVideo> &);
CascadeClassifierEdit(QWidget *parent, PreviewDialog *,
const std::shared_ptr<MacroConditionVideo> &);
private slots:
void ModelPathChanged(const QString &text);
@ -320,7 +323,7 @@ private:
BrightnessEdit *_brightness;
OCREdit *_ocr;
ObjectDetectEdit *_objectDetect;
CascadeClassifierEdit *_cascadeClassifierEdit;
ColorEdit *_color;
AreaEdit *_area;

View File

@ -0,0 +1,18 @@
#pragma once
#include <opencv2/opencv.hpp>
#include <QImage>
#include <string>
#include <vector>
namespace advss {
struct ObjectDetector {
virtual bool Load(const std::string &modelPath) = 0;
virtual bool IsLoaded() const = 0;
virtual std::vector<cv::Rect> Detect(QImage &img) = 0;
virtual ~ObjectDetector() = default;
};
} // namespace advss

View File

@ -1,6 +1,5 @@
#include "opencv-helpers.hpp"
#include <log-helper.hpp>
#include "log-helper.hpp"
namespace advss {
@ -135,29 +134,6 @@ double MatchPattern(QImage &img, QImage &pattern, double threshold,
matchColor);
}
std::vector<cv::Rect> MatchObject(QImage &img, cv::CascadeClassifier &cascade,
double scaleFactor, int minNeighbors,
const cv::Size &minSize,
const cv::Size &maxSize)
{
if (img.isNull() || cascade.empty()) {
return {};
}
auto image = QImageToMat(img);
cv::Mat frameGray;
cv::cvtColor(image, frameGray, cv::COLOR_RGBA2GRAY);
cv::equalizeHist(frameGray, frameGray);
std::vector<cv::Rect> objects;
try {
cascade.detectMultiScale(frameGray, objects, scaleFactor,
minNeighbors, 0, minSize, maxSize);
} catch (const std::exception &e) {
vblog(LOG_INFO, "detectMultiScale failed: %s", e.what());
}
return objects;
}
uchar GetAvgBrightness(QImage &img)
{
if (img.isNull()) {

View File

@ -72,10 +72,6 @@ double MatchPattern(QImage &img, QImage &pattern, double threshold,
cv::Mat &result, bool useAlphaAsMask,
cv::TemplateMatchModes matchMode);
int CountPatternMatches(const cv::Mat &result, const cv::Size &patternSize);
std::vector<cv::Rect> MatchObject(QImage &img, cv::CascadeClassifier &cascade,
double scaleFactor, int minNeighbors,
const cv::Size &minSize,
const cv::Size &maxSize);
uchar GetAvgBrightness(QImage &img);
cv::Mat PreprocessForOCR(const QImage &image, const QColor &color,
double colorDiff);

View File

@ -1,8 +1,9 @@
#include "parameter-wrappers.hpp"
#include "cascade-classifier-detector.hpp"
#include "log-helper.hpp"
#include "source-helpers.hpp"
#include <QFileInfo>
#include <source-helpers.hpp>
namespace advss {
@ -48,45 +49,6 @@ bool PatternMatchParameters::Load(obs_data_t *obj)
return true;
}
static std::shared_ptr<cv::CascadeClassifier>
initObjectCascade(std::string &path)
{
auto cascade = std::make_shared<cv::CascadeClassifier>();
try {
cascade->load(path);
} catch (...) {
blog(LOG_WARNING, "failed to load model data \"%s\"",
path.c_str());
}
return cascade;
}
bool ObjDetectParameters::LoadModelData()
{
const auto path = QString::fromStdString(modelPath);
if (!QFileInfo(path).exists(path)) {
cascade.reset();
return false;
}
cascade = initObjectCascade(modelPath);
return !cascade->empty();
}
bool ObjDetectParameters::Save(obs_data_t *obj) const
{
auto data = obs_data_create();
obs_data_set_string(data, "modelPath", modelPath.c_str());
scaleFactor.Save(data, "scaleFactor");
obs_data_set_int(data, "minNeighbors", minNeighbors);
minSize.Save(data, "minSize");
maxSize.Save(data, "maxSize");
obs_data_set_obj(obj, "objectMatchData", data);
obs_data_set_int(data, "version", 2);
obs_data_release(data);
return true;
}
static bool isScaleFactorValid(double scaleFactor)
{
return scaleFactor > 1.;
@ -98,11 +60,41 @@ static bool isMinNeighborsValid(int minNeighbors)
minNeighbors <= maxMinNeighbors;
}
bool ObjDetectParameters::Load(obs_data_t *obj)
bool CascadeClassifierParameters::LoadModelData()
{
const auto path = QString::fromStdString(_modelPath);
if (!QFileInfo(path).exists(path)) {
_detector.reset();
return false;
}
auto det = std::make_unique<CascadeClassifierDetector>();
if (!det->Load(_modelPath)) {
_detector.reset();
return false;
}
_detector = std::move(det);
return true;
}
bool CascadeClassifierParameters::Save(obs_data_t *obj) const
{
auto data = obs_data_create();
obs_data_set_string(data, "modelPath", _modelPath.c_str());
scaleFactor.Save(data, "scaleFactor");
obs_data_set_int(data, "minNeighbors", minNeighbors);
minSize.Save(data, "minSize");
maxSize.Save(data, "maxSize");
obs_data_set_int(data, "version", 2);
obs_data_set_obj(obj, "objectMatchData", data);
obs_data_release(data);
return true;
}
bool CascadeClassifierParameters::Load(obs_data_t *obj)
{
// TODO: Remove this fallback in a future version
if (!obs_data_has_user_value(obj, "patternMatchData")) {
modelPath = obs_data_get_string(obj, "modelDataPath");
_modelPath = obs_data_get_string(obj, "modelDataPath");
scaleFactor = obs_data_get_double(obj, "scaleFactor");
if (!isScaleFactorValid(scaleFactor)) {
scaleFactor = 1.1;
@ -116,7 +108,7 @@ bool ObjDetectParameters::Load(obs_data_t *obj)
return true;
}
auto data = obs_data_get_obj(obj, "objectMatchData");
modelPath = obs_data_get_string(data, "modelPath");
_modelPath = obs_data_get_string(data, "modelPath");
scaleFactor.Load(data, "scaleFactor");
// TODO: Remove this fallback in a future version
if (!obs_data_has_user_value(data, "version")) {
@ -129,11 +121,11 @@ bool ObjDetectParameters::Load(obs_data_t *obj)
// which invalidates previously saved default model paths.
const std::string oldPrefix =
"../../data/obs-plugins/advanced-scene-switcher/res/cascadeClassifiers/";
if (modelPath.substr(0, oldPrefix.size()) == oldPrefix) {
modelPath = std::string(obs_get_module_data_path(
obs_current_module())) +
"/res/cascadeClassifiers/" +
modelPath.substr(oldPrefix.size());
if (_modelPath.substr(0, oldPrefix.size()) == oldPrefix) {
_modelPath = std::string(obs_get_module_data_path(
obs_current_module())) +
"/res/cascadeClassifiers/" +
_modelPath.substr(oldPrefix.size());
}
#endif
}
@ -147,27 +139,29 @@ bool ObjDetectParameters::Load(obs_data_t *obj)
minSize.Load(data, "minSize");
maxSize.Load(data, "maxSize");
obs_data_release(data);
return true;
}
bool ObjDetectParameters::SetModelPath(const std::string &path)
bool CascadeClassifierParameters::SetModelPath(const std::string &path)
{
modelPath = path;
_modelPath = path;
return LoadModelData();
}
std::shared_ptr<cv::CascadeClassifier> ObjDetectParameters::GetModel()
ObjectDetector *CascadeClassifierParameters::GetDetector()
{
if (cascade && !cascade->empty()) {
return cascade;
if (!_detector || !_detector->IsLoaded()) {
if (!LoadModelData()) {
return nullptr;
}
}
if (!LoadModelData()) {
return {};
}
return cascade;
auto *cascade =
static_cast<CascadeClassifierDetector *>(_detector.get());
cascade->scaleFactor = scaleFactor;
cascade->minNeighbors = minNeighbors;
cascade->minSize = minSize.CV();
cascade->maxSize = maxSize.CV();
return _detector.get();
}
bool AreaParameters::Save(obs_data_t *obj) const

View File

@ -1,14 +1,15 @@
#pragma once
#include "object-detector.hpp"
#include "opencv-helpers.hpp"
#include "obs-module-helper.hpp"
#include "area-selection.hpp"
#include "source-selection.hpp"
#include "scene-selection.hpp"
#include "regex-config.hpp"
#include "variable-color.hpp"
#include "variable-string.hpp"
#include "variable-number.hpp"
#include <source-selection.hpp>
#include <scene-selection.hpp>
#include <regex-config.hpp>
#include <variable-color.hpp>
#include <variable-string.hpp>
#include <variable-number.hpp>
#include <obs.hpp>
#include <obs-module.h>
@ -27,7 +28,7 @@ enum class VideoCondition {
HAS_CHANGED,
NO_IMAGE,
PATTERN,
OBJECT,
OBJECT_CASCADE,
BRIGHTNESS,
OCR,
COLOR,
@ -64,14 +65,37 @@ public:
NumberVariable<double> threshold = 0.999;
};
class ObjDetectParameters {
class CascadeClassifierParameters {
public:
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
CascadeClassifierParameters() = default;
CascadeClassifierParameters(const CascadeClassifierParameters &other)
: scaleFactor(other.scaleFactor),
minNeighbors(other.minNeighbors),
minSize(other.minSize),
maxSize(other.maxSize),
_modelPath(other._modelPath)
{
}
CascadeClassifierParameters &
operator=(const CascadeClassifierParameters &other)
{
if (this != &other) {
scaleFactor = other.scaleFactor;
minNeighbors = other.minNeighbors;
minSize = other.minSize;
maxSize = other.maxSize;
_modelPath = other._modelPath;
_detector.reset();
}
return *this;
}
bool SetModelPath(const std::string &path);
const std::string &GetModelPath() const { return modelPath; }
std::shared_ptr<cv::CascadeClassifier> GetModel();
const std::string &GetModelPath() const { return _modelPath; }
ObjectDetector *GetDetector();
NumberVariable<double> scaleFactor = defaultScaleFactor;
int minNeighbors = minMinNeighbors;
@ -81,8 +105,8 @@ public:
private:
bool LoadModelData();
std::shared_ptr<cv::CascadeClassifier> cascade;
std::string modelPath =
std::unique_ptr<ObjectDetector> _detector;
std::string _modelPath =
obs_get_module_data_path(obs_current_module()) +
std::string(
"/res/cascadeClassifiers/haarcascade_frontalface_alt.xml");

View File

@ -1,9 +1,9 @@
#include "preview-dialog.hpp"
#include "opencv-helpers.hpp"
#include "screenshot-helper.hpp"
#include "ui-helpers.hpp"
#include <QLayout>
#include <screenshot-helper.hpp>
namespace advss {
@ -131,10 +131,11 @@ void PreviewDialog::PatternMatchParametersChanged(
_patternImageData = CreatePatternData(_patternMatchParams.image);
}
void PreviewDialog::ObjDetectParametersChanged(const ObjDetectParameters &params)
void PreviewDialog::CascadeClassifierParametersChanged(
const CascadeClassifierParameters &params)
{
std::unique_lock<std::mutex> lock(_mtx);
_objDetectParams = std::make_shared<ObjDetectParameters>(params);
_cascadeParams = std::make_shared<CascadeClassifierParameters>(params);
}
void PreviewDialog::OCRParametersChanged(const OCRParameters &params)
@ -185,7 +186,7 @@ void PreviewDialog::UpdateImage(const QPixmap &image)
DrawFrame();
}
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
_objDetectParams, _ocrParams, _areaParams, _condition);
_cascadeParams, _ocrParams, _areaParams, _condition);
}
void PreviewDialog::Start()
@ -215,7 +216,7 @@ void PreviewDialog::Start()
_thread.start();
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
_objDetectParams, _ocrParams, _areaParams, _condition);
_cascadeParams, _ocrParams, _areaParams, _condition);
}
void PreviewDialog::DrawFrame()
@ -266,7 +267,7 @@ void PreviewImage::CreateImage(
const VideoInput &video, PreviewType type,
const PatternMatchParameters &patternMatchParams,
const PatternImageData &patternImageData,
std::shared_ptr<ObjDetectParameters> objDetectParams,
std::shared_ptr<CascadeClassifierParameters> cascadeParams,
std::shared_ptr<OCRParameters> ocrParams,
const AreaParameters &areaParams, VideoCondition condition)
{
@ -299,7 +300,7 @@ void PreviewImage::CreateImage(
std::unique_lock<std::mutex> lock(_mtx);
// Will emit status label update
MarkMatch(screenshot.GetImage(), patternMatchParams,
patternImageData, objDetectParams, ocrParams,
patternImageData, cascadeParams, ocrParams,
condition);
} else {
emit StatusUpdate(obs_module_text(
@ -311,14 +312,16 @@ void PreviewImage::CreateImage(
void PreviewImage::MarkMatch(
QImage &screenshot, const PatternMatchParameters &patternMatchParams,
const PatternImageData &patternImageData,
std::shared_ptr<ObjDetectParameters> objDetectParams,
std::shared_ptr<CascadeClassifierParameters> cascadeParams,
std::shared_ptr<OCRParameters> ocrParams, VideoCondition condition)
{
if (condition == VideoCondition::PATTERN) {
MarkPatternMatch(screenshot, patternMatchParams,
patternImageData);
} else if (condition == VideoCondition::OBJECT) {
MarkObjectMatch(screenshot, objDetectParams);
} else if (condition == VideoCondition::OBJECT_CASCADE) {
MarkObjectsFromDetector(
screenshot,
cascadeParams ? cascadeParams->GetDetector() : nullptr);
} else if (condition == VideoCondition::OCR) {
MarkOCRMatch(screenshot, ocrParams);
}
@ -347,26 +350,15 @@ void PreviewImage::MarkPatternMatch(
}
}
void PreviewImage::MarkObjectMatch(
QImage &screenshot,
const std::shared_ptr<ObjDetectParameters> &objDetectParams)
void PreviewImage::MarkObjectsFromDetector(QImage &screenshot,
ObjectDetector *detector)
{
if (!objDetectParams) {
if (!detector) {
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.objectMatchFail"));
return;
}
auto model = objDetectParams->GetModel();
if (!model) {
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.objectMatchFail"));
return;
}
auto objects = MatchObject(screenshot, *model,
objDetectParams->scaleFactor,
objDetectParams->minNeighbors,
objDetectParams->minSize.CV(),
objDetectParams->maxSize.CV());
auto objects = detector->Detect(screenshot);
if (objects.empty()) {
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.objectMatchFail"));

View File

@ -27,7 +27,7 @@ public slots:
void CreateImage(const VideoInput &, PreviewType,
const PatternMatchParameters &,
const PatternImageData &,
std::shared_ptr<ObjDetectParameters>,
std::shared_ptr<CascadeClassifierParameters>,
std::shared_ptr<OCRParameters>, const AreaParameters &,
VideoCondition);
signals:
@ -38,12 +38,11 @@ signals:
private:
void MarkMatch(QImage &screenshot, const PatternMatchParameters &,
const PatternImageData &,
std::shared_ptr<ObjDetectParameters>,
std::shared_ptr<CascadeClassifierParameters>,
std::shared_ptr<OCRParameters>, VideoCondition);
void MarkPatternMatch(QImage &, const PatternMatchParameters &,
const PatternImageData &);
void MarkObjectMatch(QImage &,
const std::shared_ptr<ObjDetectParameters> &);
void MarkObjectsFromDetector(QImage &, ObjectDetector *);
void MarkOCRMatch(QImage &, const std::shared_ptr<OCRParameters> &);
std::mutex &_mtx;
@ -62,7 +61,8 @@ public:
public slots:
void PatternMatchParametersChanged(const PatternMatchParameters &);
void ObjDetectParametersChanged(const ObjDetectParameters &);
void
CascadeClassifierParametersChanged(const CascadeClassifierParameters &);
void OCRParametersChanged(const OCRParameters &);
void VideoSelectionChanged(const VideoInput &);
void AreaParametersChanged(const AreaParameters &);
@ -75,7 +75,7 @@ signals:
void SelectionAreaChanged(QRect area);
void NeedImage(const VideoInput &, PreviewType,
const PatternMatchParameters &, const PatternImageData &,
std::shared_ptr<ObjDetectParameters>,
std::shared_ptr<CascadeClassifierParameters>,
std::shared_ptr<OCRParameters>, const AreaParameters &,
VideoCondition);
@ -90,7 +90,7 @@ private:
VideoInput _video;
PatternMatchParameters _patternMatchParams;
PatternImageData _patternImageData;
std::shared_ptr<ObjDetectParameters> _objDetectParams;
std::shared_ptr<CascadeClassifierParameters> _cascadeParams;
std::shared_ptr<OCRParameters> _ocrParams;
AreaParameters _areaParams;

View File

@ -1,9 +1,9 @@
#pragma once
#include "screenshot-helper.hpp"
#include "parameter-wrappers.hpp"
#include "screenshot-helper.hpp"
#include <optional>
#include <obs.h>
#include <QDialog>
#include <QDialogButtonBox>
#include <QImage>
@ -11,6 +11,8 @@
#include <QRubberBand>
#include <QScrollArea>
#include <optional>
namespace advss {
class ScreenshotDialog : public QDialog {

View File

@ -0,0 +1,89 @@
#include "macro-condition-video.hpp"
#include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include <QPushButton>
namespace advss {
AreaEdit::AreaEdit(QWidget *parent, PreviewDialog *previewDialog,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_checkAreaEnable(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.condition.video.layout.checkAreaEnable"))),
_checkArea(new AreaSelection(0, 99999)),
_selectArea(new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.video.selectArea"))),
_previewDialog(previewDialog),
_entryData(data)
{
QWidget::connect(_checkAreaEnable, SIGNAL(stateChanged(int)), this,
SLOT(CheckAreaEnableChanged(int)));
QWidget::connect(_checkArea, SIGNAL(AreaChanged(Area)), this,
SLOT(CheckAreaChanged(Area)));
QWidget::connect(_selectArea, SIGNAL(clicked()), this,
SLOT(SelectAreaClicked()));
QWidget::connect(_previewDialog, SIGNAL(SelectionAreaChanged(QRect)),
this, SLOT(CheckAreaChanged(QRect)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{checkAreaEnable}}", _checkAreaEnable},
{"{{checkArea}}", _checkArea},
{"{{selectArea}}", _selectArea},
};
auto layout = new QHBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.checkArea"),
layout, widgetPlaceholders);
setLayout(layout);
_checkAreaEnable->setChecked(_entryData->_areaParameters.enable);
_checkArea->SetArea(_entryData->_areaParameters.area);
SetWidgetVisibility();
_loading = false;
}
void AreaEdit::SetWidgetVisibility()
{
_checkArea->setVisible(_entryData->_areaParameters.enable);
_selectArea->setVisible(_entryData->_areaParameters.enable);
adjustSize();
updateGeometry();
}
void AreaEdit::SelectAreaClicked()
{
_previewDialog->show();
_previewDialog->raise();
_previewDialog->activateWindow();
_previewDialog->SelectArea();
}
void AreaEdit::CheckAreaEnableChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_areaParameters.enable = value;
SetWidgetVisibility();
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
emit Resized();
}
void AreaEdit::CheckAreaChanged(Area value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_areaParameters.area = value;
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
}
void AreaEdit::CheckAreaChanged(QRect rect)
{
const QSignalBlocker b(_checkArea);
Area area{rect.topLeft().x(), rect.y(), rect.width(), rect.height()};
_checkArea->SetArea(area);
CheckAreaChanged(area);
}
} // namespace advss

View File

@ -0,0 +1,53 @@
#include "macro-condition-video.hpp"
#include <QTimer>
#include <QVBoxLayout>
namespace advss {
BrightnessEdit::BrightnessEdit(QWidget *parent,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_threshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.brightnessThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.brightnessThresholdDescription"))),
_current(new QLabel),
_entryData(data)
{
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(_threshold);
layout->addWidget(_current);
setLayout(layout);
QWidget::connect(
_threshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(BrightnessThresholdChanged(
const NumberVariable<double> &)));
QWidget::connect(&_timer, &QTimer::timeout, this,
&BrightnessEdit::UpdateCurrentBrightness);
_timer.start(1000);
_threshold->SetDoubleValue(_entryData->_brightnessThreshold);
_loading = false;
}
void BrightnessEdit::UpdateCurrentBrightness()
{
QString text = obs_module_text(
"AdvSceneSwitcher.condition.video.currentBrightness");
_current->setText(text.arg(_entryData->GetCurrentBrightness()));
}
void BrightnessEdit::BrightnessThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_brightnessThreshold = value;
}
} // namespace advss

View File

@ -0,0 +1,88 @@
#include "macro-condition-video.hpp"
#include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include <QVBoxLayout>
namespace advss {
ColorEdit::ColorEdit(QWidget *parent,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_matchThreshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.colorMatchThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.colorMatchThresholdDescription"),
true)),
_colorThreshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
true)),
_colorButton(new VariableColorButton(
this,
obs_module_text(
"AdvSceneSwitcher.condition.video.selectColor"))),
_entryData(data)
{
QWidget::connect(_colorButton,
SIGNAL(ColorVariableChanged(const ColorVariable &)),
this, SLOT(ColorChanged(const ColorVariable &)));
QWidget::connect(
_matchThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(MatchThresholdChanged(const NumberVariable<double> &)));
QWidget::connect(
_colorThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{color}}", _colorButton},
};
auto colorLayout = new QHBoxLayout;
PlaceWidgets(obs_module_text(
"AdvSceneSwitcher.condition.video.layout.color"),
colorLayout, widgetPlaceholders);
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addLayout(colorLayout);
layout->addWidget(_colorThreshold);
layout->addWidget(_matchThreshold);
setLayout(layout);
_matchThreshold->SetDoubleValue(
_entryData->_colorParameters.matchThreshold);
_colorThreshold->SetDoubleValue(
_entryData->_colorParameters.colorThreshold);
_colorButton->SetValue(_entryData->_colorParameters.color);
_loading = false;
}
void ColorEdit::ColorChanged(const ColorVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_colorParameters.color = value;
}
void ColorEdit::MatchThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_colorParameters.matchThreshold = value;
}
void ColorEdit::ColorThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_colorParameters.colorThreshold = value;
}
} // namespace advss

View File

@ -0,0 +1,157 @@
#include "macro-condition-video.hpp"
#include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include <QSpinBox>
#include <QVBoxLayout>
namespace advss {
CascadeClassifierEdit::CascadeClassifierEdit(
QWidget *parent, PreviewDialog *previewDialog,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_modelDataPath(new FileSelection()),
_objectScaleThreshold(new SliderSpinBox(
1.1, 5.,
obs_module_text(
"AdvSceneSwitcher.condition.video.objectScaleThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.objectScaleThresholdDescription"))),
_minNeighbors(new QSpinBox()),
_minNeighborsDescription(new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.video.minNeighborDescription"))),
_minSize(new SizeSelection(0, 1024)),
_maxSize(new SizeSelection(0, 4096)),
_previewDialog(previewDialog),
_entryData(data)
{
_minNeighbors->setMinimum(minMinNeighbors);
_minNeighbors->setMaximum(maxMinNeighbors);
QWidget::connect(
_objectScaleThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(ObjectScaleThresholdChanged(
const NumberVariable<double> &)));
QWidget::connect(_minNeighbors, SIGNAL(valueChanged(int)), this,
SLOT(MinNeighborsChanged(int)));
QWidget::connect(_minSize, SIGNAL(SizeChanged(Size)), this,
SLOT(MinSizeChanged(Size)));
QWidget::connect(_maxSize, SIGNAL(SizeChanged(Size)), this,
SLOT(MaxSizeChanged(Size)));
QWidget::connect(_modelDataPath, SIGNAL(PathChanged(const QString &)),
this, SLOT(ModelPathChanged(const QString &)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{minNeighbors}}", _minNeighbors},
{"{{minSize}}", _minSize},
{"{{maxSize}}", _maxSize},
{"{{modelDataPath}}", _modelDataPath},
};
auto pathLayout = new QHBoxLayout;
pathLayout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.modelPath"),
pathLayout, widgetPlaceholders);
auto neighborsLayout = new QHBoxLayout;
neighborsLayout->setContentsMargins(0, 0, 0, 0);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.minNeighbor"),
neighborsLayout, widgetPlaceholders);
auto sizeGrid = new QGridLayout;
sizeGrid->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.video.minSize")),
0, 0);
sizeGrid->addWidget(_minSize, 0, 1);
sizeGrid->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.video.maxSize")),
1, 0);
sizeGrid->addWidget(_maxSize, 1, 1);
auto sizeLayout = new QHBoxLayout;
sizeLayout->setContentsMargins(0, 0, 0, 0);
sizeLayout->addLayout(sizeGrid);
sizeLayout->addStretch();
auto layout = new QVBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
layout->addLayout(pathLayout);
layout->addLayout(neighborsLayout);
layout->addLayout(sizeLayout);
setLayout(layout);
_modelDataPath->SetPath(
_entryData->_cascadeMatchParameters.GetModelPath());
_objectScaleThreshold->SetDoubleValue(
_entryData->_cascadeMatchParameters.scaleFactor);
_minNeighbors->setValue(
_entryData->_cascadeMatchParameters.minNeighbors);
_minSize->SetSize(_entryData->_cascadeMatchParameters.minSize);
_maxSize->SetSize(_entryData->_cascadeMatchParameters.maxSize);
_loading = false;
}
void CascadeClassifierEdit::ObjectScaleThresholdChanged(
const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_cascadeMatchParameters.scaleFactor = value;
_previewDialog->CascadeClassifierParametersChanged(
_entryData->_cascadeMatchParameters);
}
void CascadeClassifierEdit::MinNeighborsChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_cascadeMatchParameters.minNeighbors = value;
_previewDialog->CascadeClassifierParametersChanged(
_entryData->_cascadeMatchParameters);
}
void CascadeClassifierEdit::MinSizeChanged(advss::Size value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_cascadeMatchParameters.minSize = value;
_previewDialog->CascadeClassifierParametersChanged(
_entryData->_cascadeMatchParameters);
}
void CascadeClassifierEdit::MaxSizeChanged(advss::Size value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_cascadeMatchParameters.maxSize = value;
_previewDialog->CascadeClassifierParametersChanged(
_entryData->_cascadeMatchParameters);
}
void CascadeClassifierEdit::ModelPathChanged(const QString &text)
{
if (_loading || !_entryData) {
return;
}
bool dataLoaded = false;
{
auto lock = LockContext();
std::string path = text.toStdString();
dataLoaded =
_entryData->_cascadeMatchParameters.SetModelPath(path);
}
if (!dataLoaded) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.condition.video.modelLoadFail"));
}
_previewDialog->CascadeClassifierParametersChanged(
_entryData->_cascadeMatchParameters);
}
} // namespace advss

View File

@ -0,0 +1,320 @@
#include "macro-condition-video.hpp"
#include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QUrl>
#include <QVBoxLayout>
namespace advss {
const static std::map<tesseract::PageSegMode, std::string> pageSegModes = {
{tesseract::PageSegMode::PSM_SINGLE_COLUMN,
"AdvSceneSwitcher.condition.video.ocrMode.singleColumn"},
{tesseract::PageSegMode::PSM_SINGLE_BLOCK_VERT_TEXT,
"AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText"},
{tesseract::PageSegMode::PSM_SINGLE_BLOCK,
"AdvSceneSwitcher.condition.video.ocrMode.singleBlock"},
{tesseract::PageSegMode::PSM_SINGLE_LINE,
"AdvSceneSwitcher.condition.video.ocrMode.singleLine"},
{tesseract::PageSegMode::PSM_SINGLE_WORD,
"AdvSceneSwitcher.condition.video.ocrMode.singleWord"},
{tesseract::PageSegMode::PSM_CIRCLE_WORD,
"AdvSceneSwitcher.condition.video.ocrMode.circleWord"},
{tesseract::PageSegMode::PSM_SINGLE_CHAR,
"AdvSceneSwitcher.condition.video.ocrMode.singleChar"},
{tesseract::PageSegMode::PSM_SPARSE_TEXT,
"AdvSceneSwitcher.condition.video.ocrMode.sparseText"},
{tesseract::PageSegMode::PSM_SPARSE_TEXT_OSD,
"AdvSceneSwitcher.condition.video.ocrMode.sparseTextOSD"},
};
static inline void populatePageSegModeSelection(QComboBox *list)
{
for (const auto &[mode, name] : pageSegModes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(mode));
}
}
static void openFileInEditor(const std::string &filepath)
{
const auto path = QString::fromStdString(filepath);
const QFileInfo fileInfo(path);
if (!fileInfo.exists()) {
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrOpenConfig.createFailed"));
return;
}
file.close();
}
QUrl fileUrl = QUrl::fromLocalFile(path);
if (!QDesktopServices::openUrl(fileUrl)) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrOpenConfig.openFailed"));
}
}
OCREdit::OCREdit(QWidget *parent, PreviewDialog *previewDialog,
const std::shared_ptr<MacroConditionVideo> &data)
: QWidget(parent),
_matchText(new VariableTextEdit(this)),
_regex(new RegexConfigWidget(this)),
_colorButton(new VariableColorButton(
this,
obs_module_text(
"AdvSceneSwitcher.condition.video.selectColor"))),
_colorThreshold(new SliderSpinBox(
0., 1.,
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
obs_module_text(
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
true)),
_pageSegMode(new QComboBox()),
_tesseractBaseDir(new FileSelection(FileSelection::Type::FOLDER)),
_languageCode(new VariableLineEdit(this)),
_useConfig(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrUseConfigFile"))),
_configFile(new FileSelection(FileSelection::Type::WRITE, this)),
_openConfigFile(new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrOpenConfigFile"))),
_reloadConfig(new QPushButton()),
_configLayout(new QHBoxLayout()),
_previewDialog(previewDialog),
_entryData(data)
{
populatePageSegModeSelection(_pageSegMode);
_reloadConfig->setMaximumWidth(22);
SetButtonIcon(_reloadConfig, GetThemeTypeName() == "Light"
? ":res/images/refresh.svg"
: "theme:Dark/refresh.svg");
_reloadConfig->setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrConfigReload"));
QWidget::connect(_colorButton,
SIGNAL(ColorVariableChanged(const ColorVariable &)),
this, SLOT(ColorChanged(const ColorVariable &)));
QWidget::connect(
_colorThreshold,
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
this,
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
QWidget::connect(_matchText, SIGNAL(textChanged()), this,
SLOT(MatchTextChanged()));
QWidget::connect(_regex,
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
SLOT(RegexChanged(const RegexConfig &)));
QWidget::connect(_pageSegMode, SIGNAL(currentIndexChanged(int)), this,
SLOT(PageSegModeChanged(int)));
QWidget::connect(_tesseractBaseDir,
SIGNAL(PathChanged(const QString &)), this,
SLOT(TesseractBaseDirChanged(const QString &)));
QWidget::connect(_languageCode, SIGNAL(editingFinished()), this,
SLOT(LanguageChanged()));
QWidget::connect(_useConfig, SIGNAL(stateChanged(int)), this,
SLOT(UseConfigChanged(int)));
QWidget::connect(_configFile, SIGNAL(PathChanged(const QString &)),
this, SLOT(ConfigFileChanged(const QString &)));
QWidget::connect(_openConfigFile, &QPushButton::clicked, [this](bool) {
openFileInEditor(
_entryData->_ocrParameters.GetCustomConfigFile());
});
QWidget::connect(_reloadConfig, &QPushButton::clicked, [this](bool) {
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.EnableCustomConfig(true);
_previewDialog->OCRParametersChanged(
_entryData->_ocrParameters);
});
auto configFileHint = new QLabel();
const QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
const QIcon icon(path);
const QPixmap pixmap = icon.pixmap(QSize(16, 16));
configFileHint->setPixmap(pixmap);
configFileHint->setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrConfigHint"));
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{color}}", _colorButton},
{"{{textType}}", _pageSegMode},
{"{{tesseractBaseDir}}", _tesseractBaseDir},
{"{{languageCode}}", _languageCode},
{"{{configFile}}", _configFile},
{"{{openConfigFile}}", _openConfigFile},
{"{{reloadConfig}}", _reloadConfig},
{"{{configFileHint}}", configFileHint},
};
auto layout = new QVBoxLayout();
layout->setContentsMargins(0, 0, 0, 0);
auto textLayout = new QHBoxLayout();
textLayout->setContentsMargins(0, 0, 0, 0);
textLayout->addWidget(_matchText);
textLayout->addWidget(_regex);
layout->addLayout(textLayout);
auto pageModeSegLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrTextType"),
pageModeSegLayout, widgetPlaceholders);
layout->addLayout(pageModeSegLayout);
auto baseDirLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrBaseDir"),
baseDirLayout, widgetPlaceholders, false);
layout->addLayout(baseDirLayout);
auto languageLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrLanguage"),
languageLayout, widgetPlaceholders);
layout->addLayout(languageLayout);
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrConfig"),
_configLayout, widgetPlaceholders, false);
layout->addWidget(_useConfig);
layout->addLayout(_configLayout);
auto colorPickLayout = new QHBoxLayout();
PlaceWidgets(
obs_module_text(
"AdvSceneSwitcher.condition.video.layout.ocrColorPick"),
colorPickLayout, widgetPlaceholders);
layout->addLayout(colorPickLayout);
layout->addWidget(_colorThreshold);
setLayout(layout);
_matchText->setPlainText(_entryData->_ocrParameters.text);
_regex->SetRegexConfig(_entryData->_ocrParameters.regex);
_colorButton->SetValue(_entryData->_ocrParameters.color);
_colorThreshold->SetDoubleValue(
_entryData->_ocrParameters.colorThreshold);
_pageSegMode->setCurrentIndex(_pageSegMode->findData(
static_cast<int>(_entryData->_ocrParameters.GetPageMode())));
_tesseractBaseDir->SetPath(
_entryData->_ocrParameters.GetTesseractBasePath());
_languageCode->setText(_entryData->_ocrParameters.GetLanguageCode());
_useConfig->setChecked(
_entryData->_ocrParameters.CustomConfigIsEnabled());
_configFile->SetPath(_entryData->_ocrParameters.GetCustomConfigFile());
SetLayoutVisible(_configLayout,
_entryData->_ocrParameters.CustomConfigIsEnabled());
_loading = false;
}
void OCREdit::ColorChanged(const ColorVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.color = value;
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::ColorThresholdChanged(const DoubleVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.colorThreshold = value;
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::MatchTextChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.text =
_matchText->toPlainText().toUtf8().constData();
adjustSize();
updateGeometry();
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::RegexChanged(const RegexConfig &conf)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.regex = conf;
adjustSize();
updateGeometry();
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::PageSegModeChanged(int idx)
{
GUARD_LOADING_AND_LOCK();
_entryData->SetPageSegMode(static_cast<tesseract::PageSegMode>(
_pageSegMode->itemData(idx).toInt()));
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::TesseractBaseDirChanged(const QString &path)
{
GUARD_LOADING_AND_LOCK();
if (!_entryData->SetTesseractBaseDir(path.toStdString())) {
const QString message(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
const QDir dataDir(path);
const QString fileName(_languageCode->text() + ".traineddata");
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
// Reset to previous value
const QSignalBlocker b(this);
_tesseractBaseDir->SetPath(
_entryData->_ocrParameters.GetTesseractBasePath());
return;
}
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::LanguageChanged()
{
GUARD_LOADING_AND_LOCK();
if (!_entryData->SetLanguageCode(_languageCode->text().toStdString())) {
const QString message(obs_module_text(
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
const QDir dataDir(QString::fromStdString(
_entryData->_ocrParameters.GetTesseractBasePath()));
const QString fileName(_languageCode->text() + ".traineddata");
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
// Reset to previous value
const QSignalBlocker b(this);
_languageCode->setText(
_entryData->_ocrParameters.GetLanguageCode());
return;
}
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::UseConfigChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.EnableCustomConfig(value);
SetLayoutVisible(_configLayout, value);
adjustSize();
updateGeometry();
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
void OCREdit::ConfigFileChanged(const QString &path)
{
GUARD_LOADING_AND_LOCK();
_entryData->_ocrParameters.SetCustomConfigFile(path.toStdString());
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
}
} // namespace advss