diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index d99cd82c..22aaf580 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -204,6 +204,7 @@ AdvSceneSwitcher.condition.video.condition.pattern="matches pattern" AdvSceneSwitcher.condition.video.condition.object="contains object" AdvSceneSwitcher.condition.video.condition.brightness="brightness" AdvSceneSwitcher.condition.video.condition.ocr="contains text" +AdvSceneSwitcher.condition.video.condition.color="matches color" AdvSceneSwitcher.condition.video.askFileAction="Do you want to use an existing file or create a screenshot of the currently selected source?" AdvSceneSwitcher.condition.video.askFileAction.file="Use existing file" AdvSceneSwitcher.condition.video.askFileAction.screenshot="Create screenshot" @@ -245,6 +246,10 @@ AdvSceneSwitcher.condition.video.ocrMode.circleWord="Single word in a circle" AdvSceneSwitcher.condition.video.ocrMode.singleChar="Single character" AdvSceneSwitcher.condition.video.ocrMode.sparseText="Find text in no particular order" AdvSceneSwitcher.condition.video.ocrMode.sparseTextOSD="Find text in no particular order (OSD)" +AdvSceneSwitcher.condition.video.colorMatchThreshold="Match threshold:" +AdvSceneSwitcher.condition.video.colorMatchThresholdDescription="How much of the image has to match the given color?\nA value of 1 requires every pixel of the input image to match the given color." +AdvSceneSwitcher.condition.video.colorDeviationThreshold="Color deviation:" +AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription="How similar can the clolor be to the provided input color for it to still count as a match?\nA value of 0 requires a perfect color match, while a higher value includes similar colors also." AdvSceneSwitcher.condition.video.type.main="OBS's main output" AdvSceneSwitcher.condition.video.type.source="Source" AdvSceneSwitcher.condition.video.type.scene="Scene" @@ -256,6 +261,7 @@ AdvSceneSwitcher.condition.video.entry.checkAreaEnable="Perform check only in ar AdvSceneSwitcher.condition.video.entry.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}" AdvSceneSwitcher.condition.video.entry.orcColorPick="Check for text color:{{textColor}}{{selectColor}}" AdvSceneSwitcher.condition.video.entry.orcTextType="Check for text type:{{textType}}" +AdvSceneSwitcher.condition.video.entry.color="Check for color:{{color}}{{selectColor}}" AdvSceneSwitcher.condition.video.minSize="Minimum size:" AdvSceneSwitcher.condition.video.maxSize="Maximum size:" AdvSceneSwitcher.condition.video.selectArea="Select area" diff --git a/src/macro-external/video/macro-condition-video.cpp b/src/macro-external/video/macro-condition-video.cpp index bd661e87..efb7df73 100644 --- a/src/macro-external/video/macro-condition-video.cpp +++ b/src/macro-external/video/macro-condition-video.cpp @@ -40,6 +40,8 @@ const static std::map conditionTypes = { #ifdef OCR_SUPPORT {VideoCondition::OCR, "AdvSceneSwitcher.condition.video.condition.ocr"}, #endif + {VideoCondition::COLOR, + "AdvSceneSwitcher.condition.video.condition.color"}, }; const static std::map videoInputTypes = { @@ -162,6 +164,7 @@ bool MacroConditionVideo::Save(obs_data_t *obj) const _patternMatchParameters.Save(obj); _objMatchParameters.Save(obj); _ocrParameters.Save(obj); + _colorParameters.Save(obj); obs_data_set_bool(obj, "throttleEnabled", _throttleEnabled); obs_data_set_int(obj, "throttleCount", _throttleCount); _areaParameters.Save(obj); @@ -186,6 +189,7 @@ bool MacroConditionVideo::Load(obs_data_t *obj) _patternMatchParameters.Load(obj); _objMatchParameters.Load(obj); _ocrParameters.Load(obj); + _colorParameters.Load(obj); _throttleEnabled = obs_data_get_bool(obj, "throttleEnabled"); _throttleCount = obs_data_get_int(obj, "throttleCount"); _areaParameters.Load(obj); @@ -314,6 +318,14 @@ bool MacroConditionVideo::CheckOCR() return text == std::string(_ocrParameters.text); } +bool MacroConditionVideo::CheckColor() +{ + return ContainsPixelsInColorRange(_screenshotData.image, + _colorParameters.color, + _colorParameters.colorThreshold, + _colorParameters.matchThreshold); +} + bool MacroConditionVideo::Compare() { if (_areaParameters.enable && _condition != VideoCondition::NO_IMAGE) { @@ -346,6 +358,8 @@ bool MacroConditionVideo::Compare() return CheckBrightnessThreshold(); case VideoCondition::OCR: return CheckOCR(); + case VideoCondition::COLOR: + return CheckColor(); default: break; } @@ -714,6 +728,111 @@ void ObjectDetectEdit::ModelPathChanged(const QString &text) _previewDialog->ObjDetectParametersChanged(_data->_objMatchParameters); } +ColorEdit::ColorEdit(QWidget *parent, + const std::shared_ptr &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)), + _color(new QLabel), + _selectColor(new QPushButton(obs_module_text( + "AdvSceneSwitcher.condition.video.selectColor"))), + _data(data) +{ + QWidget::connect(_selectColor, SIGNAL(clicked()), this, + SLOT(SelectColorClicked())); + QWidget::connect( + _matchThreshold, + SIGNAL(DoubleValueChanged(const NumberVariable &)), + this, + SLOT(MatchThresholdChanged(const NumberVariable &))); + QWidget::connect( + _colorThreshold, + SIGNAL(DoubleValueChanged(const NumberVariable &)), + this, + SLOT(ColorThresholdChanged(const NumberVariable &))); + + std::unordered_map widgetPlaceholders = { + {"{{color}}", _color}, + {"{{selectColor}}", _selectColor}, + }; + + auto colorLayout = new QHBoxLayout; + PlaceWidgets( + obs_module_text("AdvSceneSwitcher.condition.video.entry.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(_data->_colorParameters.matchThreshold); + _colorThreshold->SetDoubleValue(_data->_colorParameters.colorThreshold); + SetupColorLabel(_data->_colorParameters.color); + _loading = false; +} + +void ColorEdit::SetupColorLabel(const QColor &color) +{ + _color->setText(color.name()); + _color->setPalette(QPalette(color)); + _color->setAutoFillBackground(true); +} + +void ColorEdit::MatchThresholdChanged(const DoubleVariable &value) +{ + if (_loading || !_data) { + return; + } + + auto lock = LockContext(); + _data->_colorParameters.matchThreshold = value; +} + +void ColorEdit::ColorThresholdChanged(const DoubleVariable &value) +{ + if (_loading || !_data) { + return; + } + + auto lock = LockContext(); + _data->_colorParameters.colorThreshold = value; +} + +void ColorEdit::SelectColorClicked() +{ + if (_loading || !_data) { + return; + } + + const QColor color = QColorDialog::getColor( + _data->_colorParameters.color, this, + obs_module_text("AdvSceneSwitcher.condition.video.selectColor"), + QColorDialog::ColorDialogOption()); + + if (!color.isValid()) { + return; + } + + SetupColorLabel(color); + auto lock = LockContext(); + _data->_colorParameters.color = color; +} + AreaEdit::AreaEdit(QWidget *parent, PreviewDialog *previewDialog, const std::shared_ptr &data) : QWidget(parent), @@ -831,6 +950,7 @@ MacroConditionVideoEdit::MacroConditionVideoEdit( _brightness(new BrightnessEdit(this, entryData)), _ocr(new OCREdit(this, &_previewDialog, entryData)), _objectDetect(new ObjectDetectEdit(this, &_previewDialog, entryData)), + _color(new ColorEdit(this, entryData)), _area(new AreaEdit(this, &_previewDialog, entryData)), _throttleControlLayout(new QHBoxLayout), _throttleEnable(new QCheckBox()), @@ -855,6 +975,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit( QSizePolicy::Preferred); _objectDetect->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + _color->setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::Preferred); _area->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); @@ -943,6 +1065,7 @@ MacroConditionVideoEdit::MacroConditionVideoEdit( mainLayout->addWidget(_brightness); mainLayout->addWidget(_ocr); mainLayout->addWidget(_objectDetect); + mainLayout->addWidget(_color); mainLayout->addLayout(_throttleControlLayout); mainLayout->addWidget(_area); mainLayout->addWidget(_reduceLatency); @@ -1299,6 +1422,7 @@ void MacroConditionVideoEdit::SetWidgetVisibility() _ocr->setVisible(_entryData->_condition == VideoCondition::OCR); _objectDetect->setVisible(_entryData->_condition == VideoCondition::OBJECT); + _color->setVisible(_entryData->_condition == VideoCondition::COLOR); SetLayoutVisible(_throttleControlLayout, needsThrottleControls(_entryData->_condition)); _area->setVisible(needsAreaControls(_entryData->_condition)); diff --git a/src/macro-external/video/macro-condition-video.hpp b/src/macro-external/video/macro-condition-video.hpp index b7023ebb..3f65d296 100644 --- a/src/macro-external/video/macro-condition-video.hpp +++ b/src/macro-external/video/macro-condition-video.hpp @@ -57,6 +57,7 @@ public: PatternMatchParameters _patternMatchParameters; ObjDetectParameters _objMatchParameters; OCRParameters _ocrParameters; + ColorParameters _colorParameters; AreaParameters _areaParameters; bool _throttleEnabled = false; int _throttleCount = 3; @@ -67,6 +68,7 @@ private: bool ScreenshotContainsObject(); bool CheckBrightnessThreshold(); bool CheckOCR(); + bool CheckColor(); bool Compare(); bool CheckShouldBeSkipped(); @@ -160,6 +162,30 @@ private: bool _loading = true; }; +class ColorEdit : public QWidget { + Q_OBJECT + +public: + ColorEdit(QWidget *parent, + const std::shared_ptr &); + +private slots: + void SelectColorClicked(); + void MatchThresholdChanged(const NumberVariable &); + void ColorThresholdChanged(const NumberVariable &); + +private: + void SetupColorLabel(const QColor &); + + SliderSpinBox *_matchThreshold; + SliderSpinBox *_colorThreshold; + QLabel *_color; + QPushButton *_selectColor; + + std::shared_ptr _data; + bool _loading = true; +}; + class AreaEdit : public QWidget { Q_OBJECT @@ -258,6 +284,7 @@ private: BrightnessEdit *_brightness; OCREdit *_ocr; ObjectDetectEdit *_objectDetect; + ColorEdit *_color; AreaEdit *_area; QHBoxLayout *_throttleControlLayout; diff --git a/src/macro-external/video/opencv-helpers.cpp b/src/macro-external/video/opencv-helpers.cpp index 14e3b1be..a8416080 100644 --- a/src/macro-external/video/opencv-helpers.cpp +++ b/src/macro-external/video/opencv-helpers.cpp @@ -174,6 +174,37 @@ std::string RunOCR(tesseract::TessBaseAPI *ocr, const QImage &image, #endif } +bool ContainsPixelsInColorRange(const QImage &image, const QColor &color, + double colorDeviationThreshold, + double totalPixelMatchThreshold) +{ + int totalPixels = image.width() * image.height(); + int matchingPixels = 0; + int maxColorDiff = static_cast(colorDeviationThreshold * 255.0); + + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + const auto pixelColor = image.pixelColor(x, y); + const int diffRed = + std::abs(pixelColor.red() - color.red()); + const int diffGreen = + std::abs(pixelColor.green() - color.green()); + const int diffBlue = + std::abs(pixelColor.blue() - color.blue()); + + if (diffRed <= maxColorDiff && + diffGreen <= maxColorDiff && + diffBlue <= maxColorDiff) { + matchingPixels++; + } + } + } + + double matchPercentage = + static_cast(matchingPixels) / totalPixels; + return matchPercentage >= totalPixelMatchThreshold; +} + // Assumption is that QImage uses Format_RGBA8888. // Conversion from: https://github.com/dbzhang800/QtOpenCV cv::Mat QImageToMat(const QImage &img) diff --git a/src/macro-external/video/opencv-helpers.hpp b/src/macro-external/video/opencv-helpers.hpp index 568904a9..615fd88c 100644 --- a/src/macro-external/video/opencv-helpers.hpp +++ b/src/macro-external/video/opencv-helpers.hpp @@ -61,6 +61,9 @@ std::vector MatchObject(QImage &img, cv::CascadeClassifier &cascade, uchar GetAvgBrightness(QImage &img); cv::Mat PreprocessForOCR(const QImage &image, const QColor &color); std::string RunOCR(tesseract::TessBaseAPI *, const QImage &, const QColor &); +bool ContainsPixelsInColorRange(const QImage &image, const QColor &color, + double colorDeviationThreshold, + double totalPixelMatchThreshold); cv::Mat QImageToMat(const QImage &img); QImage MatToQImage(const cv::Mat &mat); diff --git a/src/macro-external/video/paramerter-wrappers.cpp b/src/macro-external/video/paramerter-wrappers.cpp index c8401478..b68451e3 100644 --- a/src/macro-external/video/paramerter-wrappers.cpp +++ b/src/macro-external/video/paramerter-wrappers.cpp @@ -315,4 +315,25 @@ void OCRParameters::Setup() initDone = true; } +bool ColorParameters::Save(obs_data_t *obj) const +{ + auto data = obs_data_create(); + SaveColor(data, "color", color); + colorThreshold.Save(data, "colorThreshold"); + matchThreshold.Save(data, "matchThreshold"); + obs_data_set_obj(obj, "colorData", data); + obs_data_release(data); + return true; +} + +bool ColorParameters::Load(obs_data_t *obj) +{ + auto data = obs_data_get_obj(obj, "colorData"); + color = LoadColor(data, "color"); + colorThreshold.Load(data, "colorThreshold"); + matchThreshold.Load(data, "matchThreshold"); + obs_data_release(data); + return true; +} + } // namespace advss diff --git a/src/macro-external/video/paramerter-wrappers.hpp b/src/macro-external/video/paramerter-wrappers.hpp index 4be3fbd2..4894c3e6 100644 --- a/src/macro-external/video/paramerter-wrappers.hpp +++ b/src/macro-external/video/paramerter-wrappers.hpp @@ -28,6 +28,7 @@ enum class VideoCondition { OBJECT, BRIGHTNESS, OCR, + COLOR, }; class VideoInput { @@ -104,6 +105,16 @@ private: bool initDone = false; }; +class ColorParameters { +public: + bool Save(obs_data_t *obj) const; + bool Load(obs_data_t *obj); + + QColor color = Qt::black; + DoubleVariable colorThreshold = 0.1; + DoubleVariable matchThreshold = 0.8; +}; + class AreaParameters { public: bool Save(obs_data_t *obj) const;