mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 12:45:28 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b74e9d2e2a | ||
|
|
7cb95cb3f6 | ||
|
|
160d19d6c1 | ||
|
|
a0550ca141 | ||
|
|
ac68fc016d | ||
|
|
5170307f72 | ||
|
|
3af0e51bd2 | ||
|
|
f34d1fe081 | ||
|
|
83c72ebba5 | ||
|
|
6ca8e6b3ea | ||
|
|
ba24fc2fab | ||
|
|
7f28b22599 | ||
|
|
4345db0d2e | ||
|
|
eddc10c3e2 | ||
|
|
46eaada0c4 | ||
|
|
f8b2dea270 | ||
|
|
90ebf0e0f8 | ||
|
|
1a819c807e | ||
|
|
3220809c04 | ||
|
|
72d5255c9e | ||
|
|
5995563a80 | ||
|
|
347743d93a | ||
|
|
3c067ce723 |
81
.github/scripts/.build.zsh
vendored
81
.github/scripts/.build.zsh
vendored
@@ -173,23 +173,29 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
read -r product_name product_version <<< \
|
||||
"$(jq -r '. | {name, version} | join(" ")' ${buildspec_file})"
|
||||
|
||||
local opencv_dir="${project_root}/deps/opencv"
|
||||
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
|
||||
|
||||
|
||||
|
||||
case ${host_os} {
|
||||
macos)
|
||||
sed -i '' \
|
||||
"s/project(\(.*\) VERSION \(.*\))/project(${product_name} VERSION ${product_version})/" \
|
||||
"${project_root}/CMakeLists.txt"
|
||||
|
||||
# Note: IPP must be set to OFF to avoid build failures on ARM architectures
|
||||
local opencv_dir="${project_root}/deps/opencv"
|
||||
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
|
||||
|
||||
local -a opencv_cmake_args=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DBUILD_LIST=core,imgproc,objdetect
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DWITH_IPP=OFF
|
||||
)
|
||||
|
||||
if [ "${target}" != "macos-x86_64" ]; then
|
||||
opencv_cmake_args+=(-DWITH_IPP=OFF)
|
||||
fi
|
||||
|
||||
pushd ${opencv_dir}
|
||||
log_info "Configure OpenCV ..."
|
||||
cmake -S . -B ${opencv_build_dir} -G ${generator} ${opencv_cmake_args}
|
||||
@@ -198,9 +204,73 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
cmake --build ${opencv_build_dir} --config Release
|
||||
|
||||
log_info "Installing OpenCV..."
|
||||
cmake --install ${opencv_build_dir} --config Release --prefix ${opencv_build_dir}
|
||||
cmake --install ${opencv_build_dir} --config Release || true
|
||||
popd
|
||||
|
||||
local leptonica_dir="${project_root}/deps/leptonica"
|
||||
local leptonica_build_dir="${leptonica_dir}/build_${target##*-}"
|
||||
|
||||
local -a leptonica_cmake_args=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DSW_BUILD=OFF
|
||||
-DOPENJPEG_SUPPORT=OFF
|
||||
-DLIBWEBP_SUPPORT=OFF
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=TRUE
|
||||
)
|
||||
|
||||
pushd ${leptonica_dir}
|
||||
log_info "Configure Leptonica ..."
|
||||
cmake -S . -B ${leptonica_build_dir} -G ${generator} ${leptonica_cmake_args}
|
||||
|
||||
log_info "Building Leptonica ..."
|
||||
cmake --build ${leptonica_build_dir} --config Release
|
||||
|
||||
log_info "Installing Leptonica..."
|
||||
# Workaround for "unknown file attribute: H" errors when running install
|
||||
cmake --install ${leptonica_build_dir} --config Release || :
|
||||
popd
|
||||
|
||||
local tesseract_dir="${project_root}/deps/tesseract"
|
||||
local tesseract_build_dir="${tesseract_dir}/build_${target##*-}"
|
||||
|
||||
local -a tesseract_cmake_args=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DSW_BUILD=OFF
|
||||
-DBUILD_TRAINING_TOOLS=OFF
|
||||
)
|
||||
|
||||
if [ "${target}" != "macos-x86_64" ]; then
|
||||
tesseract_cmake_args+=(
|
||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64
|
||||
-DHAVE_AVX=FALSE
|
||||
-DHAVE_AVX2=FALSE
|
||||
-DHAVE_AVX512F=FALSE
|
||||
-DHAVE_FMA=FALSE
|
||||
-DHAVE_SSE4_1=FALSE
|
||||
-DHAVE_NEON=TRUE
|
||||
)
|
||||
sed -i'.original' 's/HAVE_NEON FALSE/HAVE_NEON TRUE/g' "${tesseract_dir}/CMakeLists.txt"
|
||||
fi
|
||||
|
||||
pushd ${tesseract_dir}
|
||||
log_info "Configure Tesseract ..."
|
||||
cmake -S . -B ${tesseract_build_dir} -G ${generator} ${tesseract_cmake_args}
|
||||
|
||||
log_info "Building Tesseract ..."
|
||||
#cmake --build ${tesseract_build_dir} --config Release --target libtesseract
|
||||
cmake --build ${tesseract_build_dir} --config Release
|
||||
|
||||
log_info "Installing Tesseract..."
|
||||
#cmake --install ${tesseract_build_dir} --config Release --component libtesseract
|
||||
cmake --install ${tesseract_build_dir} --config Release
|
||||
popd
|
||||
;;
|
||||
linux)
|
||||
sed -i'' \
|
||||
@@ -242,7 +312,6 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DOBS_CODESIGN_LINKER=ON
|
||||
-DOBS_BUNDLE_CODESIGN_IDENTITY="${CODESIGN_IDENT:--}"
|
||||
-DOpenCV_DIR="${opencv_build_dir}"
|
||||
)
|
||||
|
||||
;;
|
||||
|
||||
101
.github/scripts/Build-Windows.ps1
vendored
101
.github/scripts/Build-Windows.ps1
vendored
@@ -37,7 +37,7 @@ function Build {
|
||||
|
||||
$UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse
|
||||
|
||||
foreach($Utility in $UtilityFunctions) {
|
||||
foreach ($Utility in $UtilityFunctions) {
|
||||
Write-Debug "Loading $($Utility.FullName)"
|
||||
. $Utility.FullName
|
||||
}
|
||||
@@ -57,18 +57,22 @@ function Build {
|
||||
$CmakeGenerator = $script:VisualStudioVersion
|
||||
}
|
||||
|
||||
$DepsPath = "plugin-deps-${script:DepsVersion}-qt${script:QtVersion}-${script:Target}"
|
||||
$DepInstallPath = "$(Resolve-Path -Path ${ProjectRoot}/../obs-build-dependencies/${DepsPath})"
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
|
||||
Push-Location -Stack BuildOpenCVTemp
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
$OpenCVCmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=$(Resolve-Path -Path ${ProjectRoot}/../obs-build-dependencies/${DepsPath})"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DBUILD_LIST=core,imgproc,objdetect"
|
||||
)
|
||||
|
||||
@@ -80,18 +84,92 @@ function Build {
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$OpenCVCmakeArgs+=('--verbose')
|
||||
$OpenCVCmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building OpenCV..."
|
||||
Invoke-External cmake --build "${OpenCVBuildPath}" @OpenCVCmakeArgs
|
||||
}
|
||||
Log-Information "Install OpenCV}..."
|
||||
Invoke-External cmake --install "${OpenCVBuildPath}" --prefix "${OpenCVBuildPath}" @OpenCVCmakeArgs
|
||||
Invoke-External cmake --install "${OpenCVBuildPath}" --prefix "${DepInstallPath}" @OpenCVCmakeArgs
|
||||
|
||||
$LeptonicaPath = "${ProjectRoot}/deps/leptonica"
|
||||
$LeptonicaBuildPath = "${LeptonicaPath}/build"
|
||||
|
||||
Push-Location -Stack BuildLeptonicaTemp
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$LeptonicaCmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DCMAKE_INSTALL_PREFIX:PATH=${DepInstallPath}"
|
||||
"-DSW_BUILD=OFF"
|
||||
"-DLIBWEBP_SUPPORT=OFF"
|
||||
)
|
||||
|
||||
Log-Information "Configuring leptonica..."
|
||||
Invoke-External cmake -S ${LeptonicaPath} -B ${LeptonicaBuildPath} @LeptonicaCmakeArgs
|
||||
|
||||
$LeptonicaCmakeArgs = @(
|
||||
'--config', "Release"
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$LeptonicaCmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building leptonica..."
|
||||
Invoke-External cmake --build "${LeptonicaBuildPath}" @LeptonicaCmakeArgs
|
||||
}
|
||||
Log-Information "Install leptonica..."
|
||||
Invoke-External cmake --install "${LeptonicaBuildPath}" --prefix "${DepInstallPath}" @LeptonicaCmakeArgs
|
||||
|
||||
Push-Location -Stack BuildTesseractTemp
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$TesseractPath = "${ProjectRoot}/deps/tesseract"
|
||||
$TesseractBuildPath = "${TesseractPath}/build"
|
||||
|
||||
# Explicitly disable PkgConfig and tiff as it will lead build errors
|
||||
$TesseractCmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DCMAKE_INSTALL_PREFIX:PATH=${DepInstallPath}"
|
||||
"-DSW_BUILD=OFF"
|
||||
"-DDISABLE_CURL=ON"
|
||||
"-DBUILD_TRAINING_TOOLS=OFF"
|
||||
"-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE"
|
||||
"-DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=TRUE"
|
||||
)
|
||||
|
||||
Log-Information "Configuring tesseract..."
|
||||
Invoke-External cmake -S ${TesseractPath} -B ${TesseractBuildPath} @TesseractCmakeArgs
|
||||
|
||||
$TesseractCmakeArgs = @(
|
||||
'--config', "Release"
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$TesseractCmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building tesseract..."
|
||||
Invoke-External cmake --build "${TesseractBuildPath}" @TesseractCmakeArgs
|
||||
}
|
||||
Log-Information "Install tesseract..."
|
||||
Invoke-External cmake --install "${TesseractBuildPath}" --prefix "${DepInstallPath}" @TesseractCmakeArgs
|
||||
|
||||
(Get-Content -Path ${ProjectRoot}/CMakeLists.txt -Raw) `
|
||||
-replace "project\((.*) VERSION (.*)\)", "project(${ProductName} VERSION ${ProductVersion})" `
|
||||
| Out-File -Path ${ProjectRoot}/CMakeLists.txt
|
||||
| Out-File -Path ${ProjectRoot}/CMakeLists.txt
|
||||
|
||||
Setup-Obs
|
||||
|
||||
@@ -99,18 +177,13 @@ function Build {
|
||||
if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) {
|
||||
Ensure-Location $ProjectRoot
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
|
||||
$DepsPath = "plugin-deps-${script:DepsVersion}-qt${script:QtVersion}-${script:Target}"
|
||||
$CmakeArgs = @(
|
||||
'-G', $CmakeGenerator
|
||||
"-DCMAKE_SYSTEM_VERSION=${script:PlatformSDK}"
|
||||
"-DCMAKE_GENERATOR_PLATFORM=$(if (${script:Target} -eq "x86") { "Win32" } else { "x64" })"
|
||||
"-DCMAKE_BUILD_TYPE=${Configuration}"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=$(Resolve-Path -Path "${ProjectRoot}/../obs-build-dependencies/${DepsPath}")"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${DepInstallPath}"
|
||||
"-DQT_VERSION=${script:QtVersion}"
|
||||
"-DOpenCV_DIR=${OpenCVBuildPath}"
|
||||
)
|
||||
|
||||
Log-Debug "Attempting to configure OBS with CMake arguments: $($CmakeArgs | Out-String)"
|
||||
@@ -122,7 +195,7 @@ function Build {
|
||||
)
|
||||
|
||||
if ( $VerbosePreference -eq 'Continue' ) {
|
||||
$CmakeArgs+=('--verbose')
|
||||
$CmakeArgs += ('--verbose')
|
||||
}
|
||||
|
||||
Log-Information "Building ${ProductName}..."
|
||||
|
||||
1
.github/scripts/utils.zsh/setup_linux
vendored
1
.github/scripts/utils.zsh/setup_linux
vendored
@@ -38,6 +38,7 @@ if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
|
||||
libxtst-dev \
|
||||
libxss-dev \
|
||||
libopencv-dev \
|
||||
libtesseract-dev \
|
||||
libprocps-dev
|
||||
|
||||
local -a _qt_packages=()
|
||||
|
||||
24
.github/workflows/main.yml
vendored
24
.github/workflows/main.yml
vendored
@@ -113,7 +113,11 @@ jobs:
|
||||
id: ccache-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
${{ github.workspace }}/plugin/deps/opencv/build_*
|
||||
${{ github.workspace }}/plugin/deps/leptonica/build_*
|
||||
${{ github.workspace }}/plugin/deps/tesseract/build*
|
||||
key: macos-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
|
||||
restore-keys: |
|
||||
macos-${{ matrix.arch }}-ccache-plugin-
|
||||
@@ -221,7 +225,8 @@ jobs:
|
||||
id: ccache-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
key: linux-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
|
||||
restore-keys: |
|
||||
linux-${{ matrix.arch }}-ccache-plugin-
|
||||
@@ -301,9 +306,24 @@ jobs:
|
||||
id: setup
|
||||
run: |
|
||||
## SETUP ENVIRONMENT SCRIPT
|
||||
$CcacheDate = (Get-Date -Format "yyyy-M-d")
|
||||
"ccacheDate=${CcacheDate}" >> $env:GITHUB_OUTPUT
|
||||
$CommitHash = (git rev-parse HEAD)[0..8] -join ''
|
||||
"commitHash=${CommitHash}" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Restore Compilation Cache
|
||||
id: ccache-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/.ccache
|
||||
${{ github.workspace }}/plugin/deps/opencv/build_*
|
||||
${{ github.workspace }}/plugin/deps/leptonica/build_*
|
||||
${{ github.workspace }}/plugin/deps/tesseract/build*
|
||||
key: windows-${{ matrix.arch }}-ccache-plugin-${{ steps.setup.outputs.ccacheDate }}
|
||||
restore-keys: |
|
||||
windows-${{ matrix.arch }}-ccache-plugin-
|
||||
|
||||
- name: Check for GitHub Labels
|
||||
id: seekingTesters
|
||||
working-directory: ${{ github.workspace }}/plugin
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -10,3 +10,9 @@
|
||||
[submodule "deps/openvr"]
|
||||
path = deps/openvr
|
||||
url = https://github.com/ValveSoftware/openvr.git
|
||||
[submodule "deps/leptonica"]
|
||||
path = deps/leptonica
|
||||
url = https://github.com/DanBloomberg/leptonica.git
|
||||
[submodule "deps/tesseract"]
|
||||
path = deps/tesseract
|
||||
url = https://github.com/tesseract-ocr/tesseract.git
|
||||
|
||||
@@ -113,6 +113,8 @@ target_sources(
|
||||
src/macro-core/macro-action-plugin-state.hpp
|
||||
src/macro-core/macro-action-profile.cpp
|
||||
src/macro-core/macro-action-profile.hpp
|
||||
src/macro-core/macro-action-projector.cpp
|
||||
src/macro-core/macro-action-projector.hpp
|
||||
src/macro-core/macro-action-random.cpp
|
||||
src/macro-core/macro-action-random.hpp
|
||||
src/macro-core/macro-action-recording.cpp
|
||||
|
||||
@@ -22,5 +22,9 @@
|
||||
</array>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>Advanced scene switcher needs additional permissions to be able to run AppleScript applications</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -82,9 +82,13 @@ AdvSceneSwitcher.macroTab.defaultname="Macro %1"
|
||||
AdvSceneSwitcher.macroTab.defaultGroupName="Group %1"
|
||||
AdvSceneSwitcher.macroTab.exists="Macro name exists already"
|
||||
AdvSceneSwitcher.macroTab.groupDeleteConfirm="Are you sure you want to delete \"%1\" and all its elements?"
|
||||
AdvSceneSwitcher.macroTab.copy="Create copy of current macro"
|
||||
AdvSceneSwitcher.macroTab.group="Group selected items"
|
||||
AdvSceneSwitcher.macroTab.ungroup="Ungroup selected groups"
|
||||
AdvSceneSwitcher.macroTab.deleteMultipleMacrosConfirmation="Are you sure you want to delete %1 macros?"
|
||||
AdvSceneSwitcher.macroTab.contextMenuAdd="Add"
|
||||
AdvSceneSwitcher.macroTab.copy="Duplicate Macro"
|
||||
AdvSceneSwitcher.macroTab.group="Group Selected Macros"
|
||||
AdvSceneSwitcher.macroTab.ungroup="Ungroup Selected Groups"
|
||||
AdvSceneSwitcher.macroTab.rename="Rename"
|
||||
AdvSceneSwitcher.macroTab.remove="Remove"
|
||||
AdvSceneSwitcher.macroTab.expandAll="Expand all"
|
||||
AdvSceneSwitcher.macroTab.collapseAll="Collapse all"
|
||||
AdvSceneSwitcher.macroTab.maximize="Maximize"
|
||||
@@ -174,6 +178,7 @@ AdvSceneSwitcher.condition.video.condition.noImage="has no output"
|
||||
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.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"
|
||||
@@ -184,6 +189,11 @@ AdvSceneSwitcher.condition.video.usePatternForChangedCheck.tooltip="This will al
|
||||
AdvSceneSwitcher.condition.video.patternThreshold="Threshold: "
|
||||
AdvSceneSwitcher.condition.video.patternThresholdDescription="A higher threshold value means that the pattern needs to match the video source more closely."
|
||||
AdvSceneSwitcher.condition.video.patternThresholdUseAlphaAsMask="Use alpha channel as mask for pattern."
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode="Use pattern matching mode {{patternMatchingModes}}"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.crossCorrelation="Cross correlation"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.correlationCoefficient="Correlation coefficient"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.squaredDifference="Squared difference"
|
||||
AdvSceneSwitcher.condition.video.patternMatchMode.tip="The best method to use depends on the specific problem and the characteristics of the images involved.\nIn practice, it's often necessary to experiment with different methods and parameters to find the best match for a particular use case."
|
||||
AdvSceneSwitcher.condition.video.brightnessThreshold="Average brightness is above:"
|
||||
AdvSceneSwitcher.condition.video.brightnessThresholdDescription="A high value is indicating a bright image and a low one a darker one."
|
||||
AdvSceneSwitcher.condition.video.currentBrightness="Current average brightness: %1"
|
||||
@@ -198,7 +208,18 @@ AdvSceneSwitcher.condition.video.patternMatchFail="Pattern was not found!"
|
||||
AdvSceneSwitcher.condition.video.patternMatchSuccess="Pattern is highlighted in red"
|
||||
AdvSceneSwitcher.condition.video.objectMatchFail="Object was not found!"
|
||||
AdvSceneSwitcher.condition.video.objectMatchSuccess="Object is highlighted in red"
|
||||
AdvSceneSwitcher.condition.video.ocrMatchSuccess="Detected text:\n\n%1"
|
||||
AdvSceneSwitcher.condition.video.modelLoadFail="Model data could not be loaded!"
|
||||
AdvSceneSwitcher.condition.video.selectColor="Select Color"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleColumn="Single column of text of variable sizes"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText="Single uniform block of vertically aligned text"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleBlock="Single uniform block of text"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleLine="Single text line"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleWord="Single word"
|
||||
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.type.main="OBS's main output"
|
||||
AdvSceneSwitcher.condition.video.type.source="Source"
|
||||
AdvSceneSwitcher.condition.video.type.scene="Scene"
|
||||
@@ -208,6 +229,8 @@ AdvSceneSwitcher.condition.video.entry.minNeighbor="Minimum neighbors: {{minNeig
|
||||
AdvSceneSwitcher.condition.video.entry.throttle="{{throttleEnable}}Reduce CPU load by performing check only every {{throttleCount}} milliseconds"
|
||||
AdvSceneSwitcher.condition.video.entry.checkAreaEnable="Perform check only in area"
|
||||
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.minSize="Minimum size:"
|
||||
AdvSceneSwitcher.condition.video.maxSize="Maximum size:"
|
||||
AdvSceneSwitcher.condition.video.selectArea="Select area"
|
||||
@@ -594,6 +617,17 @@ AdvSceneSwitcher.action.variable.currentSegmentValue="Current value:"
|
||||
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}"
|
||||
AdvSceneSwitcher.action.variable.entry.substringIndex="Substring start:{{subStringStart}} Substring size:{{subStringSize}}"
|
||||
AdvSceneSwitcher.action.variable.entry.substringRegex="Assign value of{{regexMatchIdx}}match using regular expression:"
|
||||
AdvSceneSwitcher.action.projector="Projector"
|
||||
AdvSceneSwitcher.action.projector.type.source="Source"
|
||||
AdvSceneSwitcher.action.projector.type.scene="Scene"
|
||||
AdvSceneSwitcher.action.projector.type.preview="Preview"
|
||||
AdvSceneSwitcher.action.projector.type.program="Program"
|
||||
AdvSceneSwitcher.action.projector.type.multiview="Multiview"
|
||||
AdvSceneSwitcher.action.projector.display="Display"
|
||||
AdvSceneSwitcher.action.projector.windowed="Windowed"
|
||||
AdvSceneSwitcher.action.projector.fullscreen="Fullscreen"
|
||||
AdvSceneSwitcher.action.projector.entry="Open{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}"
|
||||
AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}"
|
||||
|
||||
|
||||
; Transition Tab
|
||||
|
||||
BIN
data/res/ocr/eng.traineddata
Normal file
BIN
data/res/ocr/eng.traineddata
Normal file
Binary file not shown.
1
deps/leptonica
vendored
Submodule
1
deps/leptonica
vendored
Submodule
Submodule deps/leptonica added at b667978e86
1
deps/tesseract
vendored
Submodule
1
deps/tesseract
vendored
Submodule
Submodule deps/tesseract added at 080da83cc5
@@ -46,7 +46,10 @@ public:
|
||||
|
||||
bool addNewMacro(std::shared_ptr<Macro> &res, std::string &name,
|
||||
std::string format = "");
|
||||
void RemoveMacro(std::shared_ptr<Macro> &);
|
||||
void RenameMacro(std::shared_ptr<Macro> &, const QString &name);
|
||||
std::shared_ptr<Macro> getSelectedMacro();
|
||||
std::vector<std::shared_ptr<Macro>> getSelectedMacros();
|
||||
void SetEditMacro(Macro &m);
|
||||
void SetMacroEditAreaDisabled(bool);
|
||||
void HighlightAction(int idx, QColor color = QColor(Qt::green));
|
||||
@@ -145,6 +148,7 @@ public slots:
|
||||
void ShowMacroActionsContextMenu(const QPoint &);
|
||||
void ShowMacroConditionsContextMenu(const QPoint &);
|
||||
void CopyMacro();
|
||||
void RenameCurrentMacro();
|
||||
void ExpandAllActions();
|
||||
void ExpandAllConditions();
|
||||
void CollapseAllActions();
|
||||
|
||||
@@ -653,7 +653,7 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj)
|
||||
|
||||
obs_data_set_int(obj, "threadPriority", threadPriority);
|
||||
|
||||
obs_data_set_bool(obj, "tansitionOverrideOverride",
|
||||
obs_data_set_bool(obj, "transitionOverrideOverride",
|
||||
transitionOverrideOverride);
|
||||
obs_data_set_bool(obj, "adjustActiveTransitionType",
|
||||
adjustActiveTransitionType);
|
||||
@@ -735,8 +735,14 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
|
||||
QThread::NormalPriority);
|
||||
threadPriority = obs_data_get_int(obj, "threadPriority");
|
||||
|
||||
transitionOverrideOverride =
|
||||
obs_data_get_bool(obj, "tansitionOverrideOverride");
|
||||
// TODO: Remove this fallback in future version
|
||||
if (obs_data_has_user_value(obj, "tansitionOverrideOverride")) {
|
||||
transitionOverrideOverride =
|
||||
obs_data_get_bool(obj, "tansitionOverrideOverride");
|
||||
} else {
|
||||
transitionOverrideOverride =
|
||||
obs_data_get_bool(obj, "transitionOverrideOverride");
|
||||
}
|
||||
adjustActiveTransitionType =
|
||||
obs_data_get_bool(obj, "adjustActiveTransitionType");
|
||||
|
||||
|
||||
290
src/macro-core/macro-action-projector.cpp
Normal file
290
src/macro-core/macro-action-projector.cpp
Normal file
@@ -0,0 +1,290 @@
|
||||
#include "macro-action-projector.hpp"
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <QScreen>
|
||||
|
||||
const std::string MacroActionProjector::id = "projector";
|
||||
|
||||
bool MacroActionProjector::_registered = MacroActionFactory::Register(
|
||||
MacroActionProjector::id,
|
||||
{MacroActionProjector::Create, MacroActionProjectorEdit::Create,
|
||||
"AdvSceneSwitcher.action.projector"});
|
||||
|
||||
const static std::map<MacroActionProjector::Type, std::string> selectionTypes = {
|
||||
{MacroActionProjector::Type::SOURCE,
|
||||
"AdvSceneSwitcher.action.projector.type.source"},
|
||||
{MacroActionProjector::Type::SCENE,
|
||||
"AdvSceneSwitcher.action.projector.type.scene"},
|
||||
{MacroActionProjector::Type::PREVIEW,
|
||||
"AdvSceneSwitcher.action.projector.type.preview"},
|
||||
{MacroActionProjector::Type::PROGRAM,
|
||||
"AdvSceneSwitcher.action.projector.type.program"},
|
||||
{MacroActionProjector::Type::MULTIVIEW,
|
||||
"AdvSceneSwitcher.action.projector.type.multiview"},
|
||||
};
|
||||
|
||||
bool MacroActionProjector::PerformAction()
|
||||
{
|
||||
std::string name = "";
|
||||
const char *type = "";
|
||||
|
||||
switch (_type) {
|
||||
case MacroActionProjector::Type::SOURCE:
|
||||
type = "Source";
|
||||
name = GetWeakSourceName(_source.GetSource());
|
||||
if (name.empty()) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MacroActionProjector::Type::SCENE:
|
||||
type = "Scene";
|
||||
name = GetWeakSourceName(_scene.GetScene());
|
||||
if (name.empty()) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MacroActionProjector::Type::PREVIEW:
|
||||
type = "Preview";
|
||||
break;
|
||||
case MacroActionProjector::Type::PROGRAM:
|
||||
type = "StudioProgram";
|
||||
break;
|
||||
case MacroActionProjector::Type::MULTIVIEW:
|
||||
type = "Multiview";
|
||||
break;
|
||||
}
|
||||
|
||||
obs_frontend_open_projector(type, _fullscreen ? _monitor : -1, "",
|
||||
name.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionProjector::LogAction() const
|
||||
{
|
||||
auto it = selectionTypes.find(_type);
|
||||
if (it != selectionTypes.end()) {
|
||||
vblog(LOG_INFO,
|
||||
"performed projector action \"%s\" with"
|
||||
"source \"%s\","
|
||||
"scene \"%s\","
|
||||
"monitor %d",
|
||||
it->second.c_str(), _source.ToString(true).c_str(),
|
||||
_scene.ToString(true).c_str(), _monitor);
|
||||
} else {
|
||||
blog(LOG_WARNING, "ignored unknown projector action %d",
|
||||
static_cast<int>(_type));
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionProjector::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
obs_data_set_int(obj, "type", static_cast<int>(_type));
|
||||
obs_data_set_int(obj, "monitor", _monitor);
|
||||
obs_data_set_bool(obj, "fullscreen", _fullscreen);
|
||||
_scene.Save(obj);
|
||||
_source.Save(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionProjector::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_type = static_cast<Type>(obs_data_get_int(obj, "type"));
|
||||
_monitor = obs_data_get_int(obj, "monitor");
|
||||
_fullscreen = obs_data_get_bool(obj, "fullscreen");
|
||||
_scene.Load(obj);
|
||||
_source.Load(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void populateSelectionTypes(QComboBox *list)
|
||||
{
|
||||
for (auto entry : selectionTypes) {
|
||||
list->addItem(obs_module_text(entry.second.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void populateWindowTypes(QComboBox *list)
|
||||
{
|
||||
list->addItem(
|
||||
obs_module_text("AdvSceneSwitcher.action.projector.windowed"));
|
||||
list->addItem(obs_module_text(
|
||||
"AdvSceneSwitcher.action.projector.fullscreen"));
|
||||
}
|
||||
|
||||
static QStringList getMonitorNames()
|
||||
{
|
||||
QStringList monitorNames;
|
||||
QList<QScreen *> screens = QGuiApplication::screens();
|
||||
for (int i = 0; i < screens.size(); i++) {
|
||||
QScreen *screen = screens[i];
|
||||
QRect screenGeometry = screen->geometry();
|
||||
qreal ratio = screen->devicePixelRatio();
|
||||
QString name = "";
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
name = screen->name();
|
||||
#else
|
||||
name = screen->model().simplified();
|
||||
if (name.length() > 1 && name.endsWith("-")) {
|
||||
name.chop(1);
|
||||
}
|
||||
#endif
|
||||
name = name.simplified();
|
||||
|
||||
if (name.length() == 0) {
|
||||
name = QString("%1 %2")
|
||||
.arg(obs_module_text(
|
||||
"AdvSceneSwitcher.action.projector.display"))
|
||||
.arg(QString::number(i + 1));
|
||||
}
|
||||
QString str =
|
||||
QString("%1: %2x%3 @ %4,%5")
|
||||
.arg(name,
|
||||
QString::number(screenGeometry.width() *
|
||||
ratio),
|
||||
QString::number(screenGeometry.height() *
|
||||
ratio),
|
||||
QString::number(screenGeometry.x()),
|
||||
QString::number(screenGeometry.y()));
|
||||
|
||||
monitorNames << str;
|
||||
}
|
||||
return monitorNames;
|
||||
}
|
||||
|
||||
MacroActionProjectorEdit::MacroActionProjectorEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionProjector> entryData)
|
||||
: QWidget(parent),
|
||||
_windowTypes(new QComboBox()),
|
||||
_types(new QComboBox()),
|
||||
_scenes(new SceneSelectionWidget(window(), true, false, true, true,
|
||||
true)),
|
||||
_sources(new SourceSelectionWidget(window(), QStringList(), true)),
|
||||
_monitorSelection(new QHBoxLayout()),
|
||||
_monitors(new QComboBox())
|
||||
{
|
||||
populateWindowTypes(_windowTypes);
|
||||
populateSelectionTypes(_types);
|
||||
auto sources = GetSourceNames();
|
||||
sources.sort();
|
||||
_sources->SetSourceNameList(sources);
|
||||
_monitors->addItems(getMonitorNames());
|
||||
|
||||
QWidget::connect(_windowTypes, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(WindowTypeChanged(int)));
|
||||
QWidget::connect(_types, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(TypeChanged(int)));
|
||||
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
|
||||
this, SLOT(SceneChanged(const SceneSelection &)));
|
||||
QWidget::connect(_sources,
|
||||
SIGNAL(SourceChanged(const SourceSelection &)), this,
|
||||
SLOT(SourceChanged(const SourceSelection &)));
|
||||
QWidget::connect(_monitors, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(MonitorChanged(int)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{windowTypes}}", _windowTypes}, {"{{types}}", _types},
|
||||
{"{{scenes}}", _scenes}, {"{{sources}}", _sources},
|
||||
{"{{monitors}}", _monitors},
|
||||
};
|
||||
|
||||
placeWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.action.projector.entry.monitor"),
|
||||
_monitorSelection, widgetPlaceholders);
|
||||
|
||||
QHBoxLayout *mainLayout = new QHBoxLayout;
|
||||
placeWidgets(obs_module_text("AdvSceneSwitcher.action.projector.entry"),
|
||||
mainLayout, widgetPlaceholders);
|
||||
mainLayout->insertLayout(mainLayout->count() - 1, _monitorSelection);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
_windowTypes->setCurrentIndex(_entryData->_fullscreen ? 1 : 0);
|
||||
_types->setCurrentIndex(static_cast<int>(_entryData->_type));
|
||||
_scenes->SetScene(_entryData->_scene);
|
||||
_sources->SetSource(_entryData->_source);
|
||||
_monitors->setCurrentIndex(_entryData->_monitor);
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::SceneChanged(const SceneSelection &s)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_scene = s;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::SourceChanged(const SourceSelection &source)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_source = source;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::MonitorChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_monitor = value;
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::WindowTypeChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_fullscreen =
|
||||
_windowTypes->currentText() ==
|
||||
obs_module_text("AdvSceneSwitcher.action.projector.fullscreen");
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::TypeChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
_entryData->_type = static_cast<MacroActionProjector::Type>(value);
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionProjectorEdit::SetWidgetVisibility()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_scenes->setVisible(_entryData->_type ==
|
||||
MacroActionProjector::Type::SCENE);
|
||||
_sources->setVisible(_entryData->_type ==
|
||||
MacroActionProjector::Type::SOURCE);
|
||||
setLayoutVisible(_monitorSelection, _entryData->_fullscreen);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
73
src/macro-core/macro-action-projector.hpp
Normal file
73
src/macro-core/macro-action-projector.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "scene-selection.hpp"
|
||||
#include "source-selection.hpp"
|
||||
|
||||
class MacroActionProjector : public MacroAction {
|
||||
public:
|
||||
MacroActionProjector(Macro *m) : MacroAction(m) {}
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
std::string GetId() const { return id; };
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionProjector>(m);
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
SOURCE,
|
||||
SCENE,
|
||||
PREVIEW,
|
||||
PROGRAM,
|
||||
MULTIVIEW,
|
||||
};
|
||||
|
||||
Type _type = Type::SCENE;
|
||||
SourceSelection _source;
|
||||
SceneSelection _scene;
|
||||
int _monitor = 0;
|
||||
bool _fullscreen = true;
|
||||
|
||||
private:
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroActionProjectorEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroActionProjectorEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionProjector> entryData = nullptr);
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
return new MacroActionProjectorEdit(
|
||||
parent, std::dynamic_pointer_cast<MacroActionProjector>(
|
||||
action));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void WindowTypeChanged(int value);
|
||||
void TypeChanged(int value);
|
||||
void SceneChanged(const SceneSelection &);
|
||||
void SourceChanged(const SourceSelection &);
|
||||
void MonitorChanged(int value);
|
||||
|
||||
protected:
|
||||
QComboBox *_windowTypes;
|
||||
QComboBox *_types;
|
||||
SceneSelectionWidget *_scenes;
|
||||
SourceSelectionWidget *_sources;
|
||||
QHBoxLayout *_monitorSelection;
|
||||
QComboBox *_monitors;
|
||||
std::shared_ptr<MacroActionProjector> _entryData;
|
||||
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
bool _loading = true;
|
||||
};
|
||||
@@ -375,7 +375,7 @@ void AdvSceneSwitcher::AddMacroCondition(int idx)
|
||||
if (idx >= 1) {
|
||||
id = macro->Conditions().at(idx - 1)->GetId();
|
||||
if (idx == 1) {
|
||||
logic = LogicType::NONE;
|
||||
logic = LogicType::OR;
|
||||
} else {
|
||||
logic = macro->Conditions().at(idx - 1)->GetLogicType();
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ void MacroSegmentList::SetHelpMsgVisible(bool visible)
|
||||
_helpMsg->setVisible(visible);
|
||||
}
|
||||
|
||||
void MacroSegmentList::Insert(int idx, QWidget *widget)
|
||||
void MacroSegmentList::Insert(int idx, MacroSegmentEdit *widget)
|
||||
{
|
||||
widget->installEventFilter(this);
|
||||
_contentLayout->insertWidget(idx, widget);
|
||||
@@ -135,8 +135,6 @@ void MacroSegmentList::SetSelection(int idx)
|
||||
|
||||
bool MacroSegmentList::eventFilter(QObject *object, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonPress) {
|
||||
}
|
||||
switch (event->type()) {
|
||||
case QEvent::MouseButtonPress:
|
||||
mousePressEvent(static_cast<QMouseEvent *>(event));
|
||||
|
||||
@@ -16,7 +16,7 @@ public:
|
||||
void SetHelpMsg(const QString &msg);
|
||||
void SetHelpMsgVisible(bool visible);
|
||||
MacroSegmentEdit *WidgetAt(int idx);
|
||||
void Insert(int idx, QWidget *widget);
|
||||
void Insert(int idx, MacroSegmentEdit *widget);
|
||||
void Add(QWidget *widget);
|
||||
void Remove(int idx);
|
||||
void Clear(int idx = 0); // Clear all elements >= idx
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QMouseEvent>
|
||||
#include <QLabel>
|
||||
#include <QScrollBar>
|
||||
#include <QApplication>
|
||||
|
||||
MacroSegment::MacroSegment(Macro *m, bool supportsVariableValue)
|
||||
: _macro(m), _supportsVariableValue(supportsVariableValue)
|
||||
@@ -182,11 +183,37 @@ MacroSegmentEdit::MacroSegmentEdit(bool highlight, QWidget *parent)
|
||||
SetSelected(false);
|
||||
ShowDropLine(DropLineState::NONE);
|
||||
|
||||
// Enable dragging while clicking on the header text
|
||||
_headerInfo->installEventFilter(this);
|
||||
|
||||
_timer.setInterval(1500);
|
||||
connect(&_timer, SIGNAL(timeout()), this, SLOT(Highlight()));
|
||||
_timer.start();
|
||||
}
|
||||
|
||||
bool MacroSegmentEdit::eventFilter(QObject *obj, QEvent *ev)
|
||||
{
|
||||
if (obj == _headerInfo && ev->type() == QEvent::MouseMove) {
|
||||
if (!parentWidget()) {
|
||||
return QWidget::eventFilter(obj, ev);
|
||||
}
|
||||
// Generate a mouse move event for the MacroSegmentList
|
||||
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(ev);
|
||||
QMouseEvent *newEvent = new QMouseEvent(
|
||||
mouseEvent->type(),
|
||||
_headerInfo->mapTo(this, mouseEvent->pos()),
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
mouseEvent->globalPos(),
|
||||
#else
|
||||
mouseEvent->globalPosition(),
|
||||
#endif
|
||||
mouseEvent->button(), mouseEvent->buttons(),
|
||||
mouseEvent->modifiers());
|
||||
QApplication::sendEvent(parentWidget(), newEvent);
|
||||
}
|
||||
return QWidget::eventFilter(obj, ev);
|
||||
}
|
||||
|
||||
void MacroSegmentEdit::HeaderInfoChanged(const QString &text)
|
||||
{
|
||||
_headerInfo->setVisible(!text.isEmpty());
|
||||
|
||||
@@ -75,6 +75,8 @@ signals:
|
||||
void SceneGroupRenamed(const QString &oldName, const QString newName);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *ev) override;
|
||||
|
||||
Section *_section;
|
||||
QLabel *_headerInfo;
|
||||
QWidget *_frame;
|
||||
|
||||
@@ -83,14 +83,14 @@ void AdvSceneSwitcher::on_macroAdd_clicked()
|
||||
emit MacroAdded(QString::fromStdString(name));
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroRemove_clicked()
|
||||
void AdvSceneSwitcher::RemoveMacro(std::shared_ptr<Macro> ¯o)
|
||||
{
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = QString::fromStdString(macro->Name());
|
||||
if (macro->IsGroup()) {
|
||||
if (macro->IsGroup() && macro->GroupSize() > 0) {
|
||||
QString deleteWarning = obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.groupDeleteConfirm");
|
||||
if (!DisplayMessage(deleteWarning.arg(name), true)) {
|
||||
@@ -106,6 +106,40 @@ void AdvSceneSwitcher::on_macroRemove_clicked()
|
||||
emit MacroRemoved(name);
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::RenameMacro(std::shared_ptr<Macro> ¯o,
|
||||
const QString &name)
|
||||
{
|
||||
auto oldName = QString::fromStdString(macro->Name());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
macro->SetName(name.toStdString());
|
||||
}
|
||||
emit MacroRenamed(oldName, name);
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroRemove_clicked()
|
||||
{
|
||||
auto macros = getSelectedMacros();
|
||||
if (macros.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (macros.size() == 1) {
|
||||
RemoveMacro(macros.at(0));
|
||||
return;
|
||||
}
|
||||
|
||||
QString deleteWarning = obs_module_text(
|
||||
"AdvSceneSwitcher.macroTab.deleteMultipleMacrosConfirmation");
|
||||
if (!DisplayMessage(deleteWarning.arg(macros.size()), true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto ¯o : macros) {
|
||||
RemoveMacro(macro);
|
||||
}
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroUp_clicked()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
@@ -126,10 +160,37 @@ void AdvSceneSwitcher::on_macroDown_clicked()
|
||||
ui->macros->Down(macro);
|
||||
}
|
||||
|
||||
static bool newMacroNameValid(const std::string &name)
|
||||
{
|
||||
if (!macroNameExists(name)) {
|
||||
return true;
|
||||
}
|
||||
DisplayMessage(obs_module_text("AdvSceneSwitcher.macroTab.exists"));
|
||||
return false;
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::RenameCurrentMacro()
|
||||
{
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
}
|
||||
std::string oldName = macro->Name();
|
||||
std::string name;
|
||||
if (!AdvSSNameDialog::AskForName(
|
||||
this, obs_module_text("AdvSceneSwitcher.windowTitle"),
|
||||
obs_module_text("AdvSceneSwitcher.item.newName"), name,
|
||||
QString::fromStdString(oldName))) {
|
||||
return;
|
||||
}
|
||||
if (name.empty() || name == oldName || !newMacroNameValid(name)) {
|
||||
return;
|
||||
}
|
||||
RenameMacro(macro, QString::fromStdString(name));
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_macroName_editingFinished()
|
||||
{
|
||||
bool nameValid = true;
|
||||
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
@@ -138,33 +199,12 @@ void AdvSceneSwitcher::on_macroName_editingFinished()
|
||||
QString newName = ui->macroName->text();
|
||||
QString oldName = QString::fromStdString(macro->Name());
|
||||
|
||||
if (newName.isEmpty() || newName == oldName) {
|
||||
nameValid = false;
|
||||
}
|
||||
|
||||
if (nameValid && macroNameExists(newName.toUtf8().constData())) {
|
||||
DisplayMessage(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.exists"));
|
||||
nameValid = false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
if (nameValid) {
|
||||
macro->SetName(newName.toUtf8().constData());
|
||||
auto macro = getSelectedMacro();
|
||||
if (!macro) {
|
||||
return;
|
||||
}
|
||||
macro->SetName(newName.toStdString());
|
||||
} else {
|
||||
ui->macroName->setText(oldName);
|
||||
}
|
||||
}
|
||||
|
||||
if (nameValid) {
|
||||
emit MacroRenamed(oldName, newName);
|
||||
if (newName.isEmpty() || newName == oldName ||
|
||||
!newMacroNameValid(newName.toStdString())) {
|
||||
ui->macroName->setText(oldName);
|
||||
return;
|
||||
}
|
||||
RenameMacro(macro, newName);
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::on_runMacro_clicked()
|
||||
@@ -314,6 +354,11 @@ std::shared_ptr<Macro> AdvSceneSwitcher::getSelectedMacro()
|
||||
return ui->macros->GetCurrentMacro();
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Macro>> AdvSceneSwitcher::getSelectedMacros()
|
||||
{
|
||||
return ui->macros->GetCurrentMacros();
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::MacroSelectionChanged(const QItemSelection &,
|
||||
const QItemSelection &)
|
||||
{
|
||||
@@ -335,7 +380,8 @@ void AdvSceneSwitcher::MacroSelectionChanged(const QItemSelection &,
|
||||
|
||||
void AdvSceneSwitcher::HighlightOnChange()
|
||||
{
|
||||
if (!switcher->macroProperties._highlightExecuted) {
|
||||
if (!switcher->macroProperties._highlightActions &&
|
||||
!switcher->macroProperties._highlightExecuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -466,15 +512,20 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||
{
|
||||
QPoint globalPos = ui->macros->mapToGlobal(pos);
|
||||
QMenu menu;
|
||||
|
||||
auto add = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.contextMenuAdd"),
|
||||
this, &AdvSceneSwitcher::on_macroAdd_clicked);
|
||||
|
||||
auto copy = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.copy"), this,
|
||||
&AdvSceneSwitcher::CopyMacro);
|
||||
copy->setDisabled(ui->macros->GroupsSelected());
|
||||
copy->setEnabled(ui->macros->SingleItemSelected());
|
||||
menu.addSeparator();
|
||||
|
||||
auto group = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.group"), ui->macros,
|
||||
&MacroTree::GroupSelectedItems);
|
||||
// Nested groups are not supported
|
||||
group->setDisabled(ui->macros->GroupedItemsSelected() ||
|
||||
ui->macros->GroupsSelected() ||
|
||||
ui->macros->SelectionEmpty());
|
||||
@@ -483,6 +534,17 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.ungroup"),
|
||||
ui->macros, &MacroTree::UngroupSelectedGroups);
|
||||
ungroup->setEnabled(ui->macros->GroupsSelected());
|
||||
menu.addSeparator();
|
||||
|
||||
auto rename = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.rename"), this,
|
||||
&AdvSceneSwitcher::RenameCurrentMacro);
|
||||
rename->setEnabled(ui->macros->SingleItemSelected());
|
||||
|
||||
auto remove = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.remove"), this,
|
||||
&AdvSceneSwitcher::on_macroRemove_clicked);
|
||||
remove->setDisabled(ui->macros->SelectionEmpty());
|
||||
|
||||
menu.exec(globalPos);
|
||||
}
|
||||
|
||||
@@ -375,24 +375,26 @@ void MacroTreeModel::Add(std::shared_ptr<Macro> item)
|
||||
|
||||
void MacroTreeModel::Remove(std::shared_ptr<Macro> item)
|
||||
{
|
||||
auto startIdx = GetItemModelIndex(item);
|
||||
if (startIdx == -1) {
|
||||
auto uiStartIdx = GetItemModelIndex(item);
|
||||
if (uiStartIdx == -1) {
|
||||
return;
|
||||
}
|
||||
auto macroStartIdx = ModelIndexToMacroIndex(startIdx, _macros);
|
||||
auto macroStartIdx = ModelIndexToMacroIndex(uiStartIdx, _macros);
|
||||
|
||||
auto endIdx = startIdx;
|
||||
auto uiEndIdx = uiStartIdx;
|
||||
auto macroEndIdx = macroStartIdx;
|
||||
|
||||
bool isGroup = item->IsGroup();
|
||||
if (isGroup) {
|
||||
macroEndIdx += item->GroupSize();
|
||||
if (!item->IsCollapsed()) {
|
||||
endIdx = item->GroupSize();
|
||||
uiEndIdx += item->GroupSize();
|
||||
}
|
||||
} else if (item->IsSubitem()) {
|
||||
Macro::PrepareMoveToGroup(nullptr, item);
|
||||
}
|
||||
|
||||
beginRemoveRows(QModelIndex(), startIdx, endIdx);
|
||||
beginRemoveRows(QModelIndex(), uiStartIdx, uiEndIdx);
|
||||
_macros.erase(std::next(_macros.begin(), macroStartIdx),
|
||||
std::next(_macros.begin(), macroEndIdx + 1));
|
||||
endRemoveRows();
|
||||
@@ -472,6 +474,21 @@ std::shared_ptr<Macro> MacroTreeModel::GetCurrentMacro() const
|
||||
return _macros[ModelIndexToMacroIndex(idx, _macros)];
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Macro>>
|
||||
MacroTreeModel::GetCurrentMacros(const QModelIndexList &selection) const
|
||||
{
|
||||
std::vector<std::shared_ptr<Macro>> result;
|
||||
result.reserve(selection.size());
|
||||
for (const auto &sel : selection) {
|
||||
try {
|
||||
result.emplace_back(_macros.at(
|
||||
ModelIndexToMacroIndex(sel.row(), _macros)));
|
||||
} catch (const std::out_of_range &) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
MacroTreeModel::MacroTreeModel(MacroTree *st_,
|
||||
std::deque<std::shared_ptr<Macro>> ¯os)
|
||||
: QAbstractListModel(st_), _mt(st_), _macros(macros)
|
||||
@@ -566,37 +583,42 @@ void MacroTreeModel::GroupSelectedItems(QModelIndexList &indices)
|
||||
|
||||
std::lock_guard<std::mutex> lock(switcher->m);
|
||||
QString name = GetNewGroupName();
|
||||
std::vector<std::shared_ptr<Macro>> items;
|
||||
items.reserve(indices.size());
|
||||
std::vector<std::shared_ptr<Macro>> itemsToGroup;
|
||||
itemsToGroup.reserve(indices.size());
|
||||
int insertGroupAt = indices[0].row();
|
||||
for (int i = 0; i < indices.count(); i++) {
|
||||
std::shared_ptr<Macro> item = _macros[ModelIndexToMacroIndex(
|
||||
indices[i].row(), _macros)];
|
||||
items.emplace_back(item);
|
||||
int selectedIdx = indices[i].row();
|
||||
if (selectedIdx < insertGroupAt) {
|
||||
insertGroupAt = selectedIdx;
|
||||
}
|
||||
|
||||
std::shared_ptr<Macro> item =
|
||||
_macros[ModelIndexToMacroIndex(selectedIdx, _macros)];
|
||||
itemsToGroup.emplace_back(item);
|
||||
}
|
||||
|
||||
std::shared_ptr<Macro> item =
|
||||
Macro::CreateGroup(name.toStdString(), items);
|
||||
Macro::CreateGroup(name.toStdString(), itemsToGroup);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A new list entry for group
|
||||
Add(item);
|
||||
_macros.insert(_macros.begin() + insertGroupAt, item);
|
||||
|
||||
// Move all selected items after new group entry
|
||||
for (const auto &item : items) {
|
||||
int offset = 1;
|
||||
for (const auto &item : itemsToGroup) {
|
||||
auto it = _macros.begin() + GetItemMacroIndex(item);
|
||||
std::rotate(it, std::next(it, 1), _macros.end());
|
||||
_macros.erase(it);
|
||||
_macros.insert(_macros.begin() + insertGroupAt + offset, item);
|
||||
offset++;
|
||||
}
|
||||
|
||||
_hasGroups = true;
|
||||
_mt->selectionModel()->clear();
|
||||
|
||||
for (auto &m : _macros) {
|
||||
m->ResolveMacroRef();
|
||||
}
|
||||
|
||||
Reset(_macros);
|
||||
|
||||
for (auto &m : _macros) {
|
||||
m->ResolveMacroRef();
|
||||
}
|
||||
@@ -703,6 +725,12 @@ std::shared_ptr<Macro> MacroTree::GetCurrentMacro() const
|
||||
return GetModel()->GetCurrentMacro();
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<Macro>> MacroTree::GetCurrentMacros() const
|
||||
{
|
||||
QModelIndexList indices = selectedIndexes();
|
||||
return GetModel()->GetCurrentMacros(indices);
|
||||
}
|
||||
|
||||
MacroTree::MacroTree(QWidget *parent_) : QListView(parent_)
|
||||
{
|
||||
setStyleSheet(QString(
|
||||
@@ -1029,14 +1057,12 @@ void MacroTree::dropEvent(QDropEvent *event)
|
||||
|
||||
bool MacroTree::GroupsSelected() const
|
||||
{
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
QModelIndexList selectedIndices = selectedIndexes();
|
||||
|
||||
if (SelectionEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &idx : selectedIndices) {
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
for (auto &idx : selectedIndexes()) {
|
||||
std::shared_ptr<Macro> item =
|
||||
mtm->_macros[ModelIndexToMacroIndex(idx.row(),
|
||||
mtm->_macros)];
|
||||
@@ -1049,14 +1075,12 @@ bool MacroTree::GroupsSelected() const
|
||||
|
||||
bool MacroTree::GroupedItemsSelected() const
|
||||
{
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
QModelIndexList selectedIndices = selectedIndexes();
|
||||
|
||||
if (SelectionEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto &idx : selectedIndices) {
|
||||
auto *mtm = GetModel();
|
||||
for (auto &idx : selectedIndexes()) {
|
||||
std::shared_ptr<Macro> item =
|
||||
mtm->_macros[ModelIndexToMacroIndex(idx.row(),
|
||||
mtm->_macros)];
|
||||
@@ -1068,6 +1092,11 @@ bool MacroTree::GroupedItemsSelected() const
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MacroTree::SingleItemSelected() const
|
||||
{
|
||||
return selectedIndexes().size() == 1;
|
||||
}
|
||||
|
||||
bool MacroTree::SelectionEmpty() const
|
||||
{
|
||||
return selectedIndexes().empty();
|
||||
|
||||
@@ -92,6 +92,8 @@ private:
|
||||
std::shared_ptr<Macro> FindEndOfGroup(const std::shared_ptr<Macro> &m,
|
||||
bool above) const;
|
||||
std::shared_ptr<Macro> GetCurrentMacro() const;
|
||||
std::vector<std::shared_ptr<Macro>>
|
||||
GetCurrentMacros(const QModelIndexList &) const;
|
||||
QString GetNewGroupName();
|
||||
void GroupSelectedItems(QModelIndexList &indices);
|
||||
void UngroupSelectedGroups(QModelIndexList &indices);
|
||||
@@ -121,8 +123,10 @@ public:
|
||||
void Up(std::shared_ptr<Macro> item) const;
|
||||
void Down(std::shared_ptr<Macro> item) const;
|
||||
std::shared_ptr<Macro> GetCurrentMacro() const;
|
||||
std::vector<std::shared_ptr<Macro>> GetCurrentMacros() const;
|
||||
bool GroupsSelected() const;
|
||||
bool GroupedItemsSelected() const;
|
||||
bool SingleItemSelected() const;
|
||||
bool SelectionEmpty() const;
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -17,5 +17,5 @@ install_advss_plugin_dependency(...)
|
||||
... to install the plugin and its dependencies.
|
||||
#]]
|
||||
|
||||
add_subdirectory(opencv)
|
||||
add_subdirectory(openvr)
|
||||
add_subdirectory(video)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-opencv)
|
||||
|
||||
# --- Check OpenCV requirements ---
|
||||
|
||||
find_package(OpenCV)
|
||||
if(NOT OpenCV_FOUND)
|
||||
message(
|
||||
WARNING
|
||||
"OpenCV not found! Video condition will be disabled!\nOpenCV sources are available under: ${CMAKE_CURRENT_SOURCE_DIR}/../../../deps/opencv"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(advanced-scene-switcher-opencv MODULE)
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE area-selection.cpp
|
||||
area-selection.hpp
|
||||
macro-condition-video.cpp
|
||||
macro-condition-video.hpp
|
||||
opencv-helpers.cpp
|
||||
opencv-helpers.hpp
|
||||
paramerter-wrappers.cpp
|
||||
paramerter-wrappers.hpp
|
||||
preview-dialog.cpp
|
||||
preview-dialog.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
# --- OpenCV build settings ---
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${OpenCV_INCLUDE_DIRS}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OpenCV_LIBRARIES})
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(NOT OS_LINUX)
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
${OpenCV_LIBS})
|
||||
endif()
|
||||
@@ -1,113 +0,0 @@
|
||||
#include "opencv-helpers.hpp"
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern)
|
||||
{
|
||||
PatternImageData data{};
|
||||
if (pattern.isNull()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data.rgbaPattern = QImageToMat(pattern);
|
||||
std::vector<cv::Mat1b> rgbaChannelsPattern;
|
||||
cv::split(data.rgbaPattern, rgbaChannelsPattern);
|
||||
std::vector<cv::Mat1b> rgbChanlesPattern(
|
||||
rgbaChannelsPattern.begin(), rgbaChannelsPattern.begin() + 3);
|
||||
cv::merge(rgbChanlesPattern, data.rgbPattern);
|
||||
cv::threshold(rgbaChannelsPattern[3], data.mask, 0, 255,
|
||||
cv::THRESH_BINARY);
|
||||
return data;
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, PatternImageData &patternData, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask)
|
||||
{
|
||||
if (img.isNull() || patternData.rgbaPattern.empty()) {
|
||||
return;
|
||||
}
|
||||
if (img.height() < patternData.rgbaPattern.rows ||
|
||||
img.width() < patternData.rgbaPattern.cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto i = QImageToMat(img);
|
||||
|
||||
if (useAlphaAsMask) {
|
||||
std::vector<cv::Mat1b> rgbaChannelsImage;
|
||||
cv::split(i, rgbaChannelsImage);
|
||||
std::vector<cv::Mat1b> rgbChanlesImage(
|
||||
rgbaChannelsImage.begin(),
|
||||
rgbaChannelsImage.begin() + 3);
|
||||
|
||||
cv::Mat3b rgbImage;
|
||||
cv::merge(rgbChanlesImage, rgbImage);
|
||||
|
||||
cv::matchTemplate(rgbImage, patternData.rgbPattern, result,
|
||||
cv::TM_CCORR_NORMED, patternData.mask);
|
||||
cv::threshold(result, result, threshold, 0, cv::THRESH_TOZERO);
|
||||
|
||||
} else {
|
||||
cv::matchTemplate(i, patternData.rgbaPattern, result,
|
||||
cv::TM_CCOEFF_NORMED);
|
||||
cv::threshold(result, result, threshold, 0, cv::THRESH_TOZERO);
|
||||
}
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask)
|
||||
{
|
||||
auto data = createPatternData(pattern);
|
||||
matchPattern(img, data, threshold, result, useAlphaAsMask);
|
||||
}
|
||||
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize)
|
||||
{
|
||||
if (img.isNull() || cascade.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat frameGray;
|
||||
cv::cvtColor(i, frameGray, cv::COLOR_BGR2GRAY);
|
||||
equalizeHist(frameGray, frameGray);
|
||||
std::vector<cv::Rect> objects;
|
||||
cascade.detectMultiScale(frameGray, objects, scaleFactor, minNeighbors,
|
||||
0, minSize, maxSize);
|
||||
return objects;
|
||||
}
|
||||
|
||||
uchar getAvgBrightness(QImage &img)
|
||||
{
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat hsvImage, rgbImage;
|
||||
cv::cvtColor(i, rgbImage, cv::COLOR_RGBA2RGB);
|
||||
cv::cvtColor(rgbImage, hsvImage, cv::COLOR_RGB2HSV);
|
||||
long long brightnessSum = 0;
|
||||
for (int i = 0; i < hsvImage.rows; ++i) {
|
||||
for (int j = 0; j < hsvImage.cols; ++j) {
|
||||
brightnessSum += hsvImage.at<cv::Vec3b>(i, j)[2];
|
||||
}
|
||||
}
|
||||
return brightnessSum / (hsvImage.rows * hsvImage.cols);
|
||||
}
|
||||
|
||||
// Assumption is that QImage uses Format_RGBA8888.
|
||||
// Conversion from: https://github.com/dbzhang800/QtOpenCV
|
||||
cv::Mat QImageToMat(const QImage &img)
|
||||
{
|
||||
if (img.isNull()) {
|
||||
return cv::Mat();
|
||||
}
|
||||
return cv::Mat(img.height(), img.width(), CV_8UC(img.depth() / 8),
|
||||
(uchar *)img.bits(), img.bytesPerLine());
|
||||
}
|
||||
|
||||
QImage MatToQImage(const cv::Mat &mat)
|
||||
{
|
||||
if (mat.empty()) {
|
||||
return QImage();
|
||||
}
|
||||
return QImage(mat.data, mat.cols, mat.rows,
|
||||
QImage::Format::Format_RGBA8888);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
#include <QImage>
|
||||
#undef NO // MacOS macro that can conflict with OpenCV
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
constexpr int minMinNeighbors = 3;
|
||||
constexpr int maxMinNeighbors = 6;
|
||||
constexpr double defaultScaleFactor = 1.1;
|
||||
|
||||
struct PatternImageData {
|
||||
cv::Mat4b rgbaPattern;
|
||||
cv::Mat3b rgbPattern;
|
||||
cv::Mat1b mask;
|
||||
};
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern);
|
||||
void matchPattern(QImage &img, PatternImageData &patternData, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask = true);
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask);
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize);
|
||||
uchar getAvgBrightness(QImage &img);
|
||||
cv::Mat QImageToMat(const QImage &img);
|
||||
QImage MatToQImage(const cv::Mat &mat);
|
||||
63
src/macro-external/video/CMakeLists.txt
Normal file
63
src/macro-external/video/CMakeLists.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-opencv)
|
||||
|
||||
# --- Check OpenCV requirements ---
|
||||
|
||||
find_package(OpenCV)
|
||||
if(NOT OpenCV_FOUND)
|
||||
message(
|
||||
WARNING
|
||||
"OpenCV not found!\n"
|
||||
"Video condition will be disabled!\n\n"
|
||||
"OpenCV sources are available under: ${CMAKE_CURRENT_SOURCE_DIR}/../../../deps/opencv"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# --- Check optional OCR dependencies ---
|
||||
|
||||
find_package(Leptonica)
|
||||
find_package(Tesseract)
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(${PROJECT_NAME} MODULE)
|
||||
|
||||
if(Leptonica_FOUND AND Tesseract_FOUND)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE OCR_SUPPORT)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Tesseract::libtesseract
|
||||
${Leptonica_LIBRARIES})
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${Tesseract_INCLUDE_DIRS}
|
||||
${Leptonica_INCLUDE_DIRS})
|
||||
else()
|
||||
message(
|
||||
WARNING
|
||||
"OCR capabilities of video condition disabled!\n"
|
||||
"Leptonica or Tesseract dependencies were not found!\n\n"
|
||||
"Sources are available under: ${CMAKE_CURRENT_SOURCE_DIR}/../../../deps/")
|
||||
endif()
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE area-selection.cpp
|
||||
area-selection.hpp
|
||||
macro-condition-video.cpp
|
||||
macro-condition-video.hpp
|
||||
opencv-helpers.cpp
|
||||
opencv-helpers.hpp
|
||||
paramerter-wrappers.cpp
|
||||
paramerter-wrappers.hpp
|
||||
preview-dialog.cpp
|
||||
preview-dialog.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${OpenCV_INCLUDE_DIRS})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OpenCV_LIBRARIES})
|
||||
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(NOT OS_LINUX)
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
${OpenCV_LIBS})
|
||||
endif()
|
||||
@@ -61,7 +61,6 @@ SizeSelection::SizeSelection(int min, int max, QWidget *parent)
|
||||
auto layout = new QHBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(_x);
|
||||
layout->addWidget(new QLabel(" x "));
|
||||
layout->addWidget(_y);
|
||||
setLayout(layout);
|
||||
}
|
||||
@@ -92,12 +91,14 @@ AreaSelection::AreaSelection(int min, int max, QWidget *parent)
|
||||
_x(new SizeSelection(min, max)),
|
||||
_y(new SizeSelection(min, max))
|
||||
{
|
||||
_x->_x->setToolTip("X");
|
||||
_x->_y->setToolTip("Y");
|
||||
_y->_x->setToolTip(
|
||||
obs_module_text("AdvSceneSwitcher.condition.video.width"));
|
||||
_y->_y->setToolTip(
|
||||
obs_module_text("AdvSceneSwitcher.condition.video.height"));
|
||||
_x->_x->setPrefix("X:");
|
||||
_x->_y->setPrefix("Y:");
|
||||
_y->_x->setPrefix(QString(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.width")) +
|
||||
":");
|
||||
_y->_y->setPrefix(QString(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.height")) +
|
||||
":");
|
||||
|
||||
connect(_x, SIGNAL(SizeChanged(advss::Size)), this,
|
||||
SLOT(XSizeChanged(advss::Size)));
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QToolTip>
|
||||
#include <QMessageBox>
|
||||
#include <QtGlobal>
|
||||
#include <QColorDialog>
|
||||
|
||||
const std::string MacroConditionVideo::id = "video";
|
||||
|
||||
@@ -35,6 +36,9 @@ const static std::map<VideoCondition, std::string> conditionTypes = {
|
||||
"AdvSceneSwitcher.condition.video.condition.object"},
|
||||
{VideoCondition::BRIGHTNESS,
|
||||
"AdvSceneSwitcher.condition.video.condition.brightness"},
|
||||
#ifdef OCR_SUPPORT
|
||||
{VideoCondition::OCR, "AdvSceneSwitcher.condition.video.condition.ocr"},
|
||||
#endif
|
||||
};
|
||||
|
||||
const static std::map<VideoInput::Type, std::string> videoInputTypes = {
|
||||
@@ -46,6 +50,36 @@ const static std::map<VideoInput::Type, std::string> videoInputTypes = {
|
||||
"AdvSceneSwitcher.condition.video.type.scene"},
|
||||
};
|
||||
|
||||
const static std::map<cv::TemplateMatchModes, std::string> patternMatchModes = {
|
||||
{cv::TemplateMatchModes::TM_CCOEFF_NORMED,
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.correlationCoefficient"},
|
||||
{cv::TemplateMatchModes::TM_CCORR_NORMED,
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.crossCorrelation"},
|
||||
{cv::TemplateMatchModes::TM_SQDIFF_NORMED,
|
||||
"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"},
|
||||
};
|
||||
|
||||
cv::CascadeClassifier initObjectCascade(std::string &path)
|
||||
{
|
||||
cv::CascadeClassifier cascade;
|
||||
@@ -58,7 +92,7 @@ cv::CascadeClassifier initObjectCascade(std::string &path)
|
||||
return cascade;
|
||||
}
|
||||
|
||||
bool requiresFileInput(VideoCondition t)
|
||||
static bool requiresFileInput(VideoCondition t)
|
||||
{
|
||||
return t == VideoCondition::MATCH || t == VideoCondition::DIFFER ||
|
||||
t == VideoCondition::PATTERN;
|
||||
@@ -126,6 +160,7 @@ bool MacroConditionVideo::Save(obs_data_t *obj) const
|
||||
obs_data_set_double(obj, "brightness", _brightnessThreshold);
|
||||
_patternMatchParameters.Save(obj);
|
||||
_objMatchParameters.Save(obj);
|
||||
_ocrParamters.Save(obj);
|
||||
obs_data_set_bool(obj, "throttleEnabled", _throttleEnabled);
|
||||
obs_data_set_int(obj, "throttleCount", _throttleCount);
|
||||
_areaParameters.Save(obj);
|
||||
@@ -144,6 +179,7 @@ bool MacroConditionVideo::Load(obs_data_t *obj)
|
||||
_brightnessThreshold = obs_data_get_double(obj, "brightness");
|
||||
_patternMatchParameters.Load(obj);
|
||||
_objMatchParameters.Load(obj);
|
||||
_ocrParamters.Load(obj);
|
||||
_throttleEnabled = obs_data_get_bool(obj, "throttleEnabled");
|
||||
_throttleCount = obs_data_get_int(obj, "throttleCount");
|
||||
_areaParameters.Load(obj);
|
||||
@@ -203,12 +239,18 @@ std::string MacroConditionVideo::GetModelDataPath() const
|
||||
return _objMatchParameters.modelPath;
|
||||
}
|
||||
|
||||
void MacroConditionVideo::SetPageSegMode(tesseract::PageSegMode mode)
|
||||
{
|
||||
_ocrParamters.SetPageMode(mode);
|
||||
}
|
||||
|
||||
bool MacroConditionVideo::ScreenshotContainsPattern()
|
||||
{
|
||||
cv::Mat result;
|
||||
matchPattern(_screenshotData.image, _patternImageData,
|
||||
_patternMatchParameters.threshold, result,
|
||||
_patternMatchParameters.useAlphaAsMask);
|
||||
_patternMatchParameters.useAlphaAsMask,
|
||||
_patternMatchParameters.matchMode);
|
||||
return countNonZero(result) > 0;
|
||||
}
|
||||
|
||||
@@ -219,7 +261,8 @@ bool MacroConditionVideo::OutputChanged()
|
||||
_patternImageData = createPatternData(_matchImage);
|
||||
matchPattern(_screenshotData.image, _patternImageData,
|
||||
_patternMatchParameters.threshold, result,
|
||||
_patternMatchParameters.useAlphaAsMask);
|
||||
_patternMatchParameters.useAlphaAsMask,
|
||||
_patternMatchParameters.matchMode);
|
||||
return countNonZero(result) == 0;
|
||||
}
|
||||
return _screenshotData.image != _matchImage;
|
||||
@@ -238,8 +281,31 @@ bool MacroConditionVideo::ScreenshotContainsObject()
|
||||
|
||||
bool MacroConditionVideo::CheckBrightnessThreshold()
|
||||
{
|
||||
_currentBrigthness = getAvgBrightness(_screenshotData.image) / 255.;
|
||||
return _currentBrigthness > _brightnessThreshold;
|
||||
_currentBrightness = getAvgBrightness(_screenshotData.image) / 255.;
|
||||
return _currentBrightness > _brightnessThreshold;
|
||||
}
|
||||
|
||||
bool MacroConditionVideo::CheckOCR()
|
||||
{
|
||||
if (!_ocrParamters.Initialized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto text = runOCR(_ocrParamters.GetOCR(), _screenshotData.image,
|
||||
_ocrParamters.color);
|
||||
|
||||
if (_ocrParamters.regex.Enabled()) {
|
||||
auto expr = _ocrParamters.regex.GetRegularExpression(
|
||||
_ocrParamters.text);
|
||||
if (!expr.isValid()) {
|
||||
return false;
|
||||
}
|
||||
auto match = expr.match(QString::fromStdString(text));
|
||||
return match.hasMatch();
|
||||
}
|
||||
|
||||
SetVariableValue(text);
|
||||
return text == std::string(_ocrParamters.text);
|
||||
}
|
||||
|
||||
bool MacroConditionVideo::Compare()
|
||||
@@ -251,6 +317,10 @@ bool MacroConditionVideo::Compare()
|
||||
_areaParameters.area.height);
|
||||
}
|
||||
|
||||
if (_condition != VideoCondition::OCR) {
|
||||
SetVariableValue("");
|
||||
}
|
||||
|
||||
switch (_condition) {
|
||||
case VideoCondition::MATCH:
|
||||
return _screenshotData.image == _matchImage;
|
||||
@@ -268,6 +338,8 @@ bool MacroConditionVideo::Compare()
|
||||
return ScreenshotContainsObject();
|
||||
case VideoCondition::BRIGHTNESS:
|
||||
return CheckBrightnessThreshold();
|
||||
case VideoCondition::OCR:
|
||||
return CheckOCR();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -288,6 +360,22 @@ static inline void populateConditionSelection(QComboBox *list)
|
||||
}
|
||||
}
|
||||
|
||||
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 inline void populatePatternMatchModeSelection(QComboBox *list)
|
||||
{
|
||||
for (const auto &[mode, name] : patternMatchModes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(mode));
|
||||
}
|
||||
}
|
||||
|
||||
MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionVideo> entryData)
|
||||
: QWidget(parent),
|
||||
@@ -309,6 +397,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
"AdvSceneSwitcher.condition.video.patternThresholdDescription"))),
|
||||
_useAlphaAsMask(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternThresholdUseAlphaAsMask"))),
|
||||
_patternMatchModeLayout(new QHBoxLayout()),
|
||||
_patternMatchMode(new QComboBox()),
|
||||
_brightnessThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
@@ -316,6 +406,13 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.brightnessThresholdDescription"))),
|
||||
_currentBrightness(new QLabel),
|
||||
_ocrLayout(new QVBoxLayout),
|
||||
_matchText(new VariableTextEdit(this)),
|
||||
_regex(new RegexConfigWidget(this)),
|
||||
_textColor(new QLabel),
|
||||
_selectColor(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectColor"))),
|
||||
_pageSegMode(new QComboBox()),
|
||||
_modelDataPath(new FileSelection()),
|
||||
_modelPathLayout(new QHBoxLayout),
|
||||
_objectScaleThreshold(new SliderSpinBox(
|
||||
@@ -342,15 +439,19 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
_throttleCount(new QSpinBox()),
|
||||
_showMatch(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.showMatch"))),
|
||||
_previewDialog(this, GetSwitcher()->interval)
|
||||
_previewDialog(this)
|
||||
{
|
||||
_reduceLatency->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.reduceLatency.tooltip"));
|
||||
_imagePath->Button()->disconnect();
|
||||
_usePatternForChangedCheck->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.usePatternForChangedCheck.tooltip"));
|
||||
_patternMatchMode->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.tip"));
|
||||
populatePatternMatchModeSelection(_patternMatchMode);
|
||||
_minNeighbors->setMinimum(minMinNeighbors);
|
||||
_minNeighbors->setMaximum(maxMinNeighbors);
|
||||
populatePageSegModeSelection(_pageSegMode);
|
||||
_throttleCount->setMinimum(1 * GetSwitcher()->interval);
|
||||
_throttleCount->setMaximum(10 * GetSwitcher()->interval);
|
||||
_throttleCount->setSingleStep(GetSwitcher()->interval);
|
||||
@@ -380,6 +481,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
this, SLOT(PatternThresholdChanged(double)));
|
||||
QWidget::connect(_useAlphaAsMask, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseAlphaAsMaskChanged(int)));
|
||||
QWidget::connect(_patternMatchMode, SIGNAL(currentIndexChanged(int)),
|
||||
this, SLOT(PatternMatchModeChanged(int)));
|
||||
QWidget::connect(_brightnessThreshold,
|
||||
SIGNAL(DoubleValueChanged(double)), this,
|
||||
SLOT(BrightnessThresholdChanged(double)));
|
||||
@@ -414,10 +517,26 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
&_previewDialog, SLOT(ConditionChanged(int)));
|
||||
QWidget::connect(_selectArea, SIGNAL(clicked()), this,
|
||||
SLOT(SelectAreaClicked()));
|
||||
QWidget::connect(_selectColor, SIGNAL(clicked()), this,
|
||||
SLOT(SelectColorClicked()));
|
||||
QWidget::connect(_matchText, SIGNAL(textChanged()), this,
|
||||
SLOT(MatchTextChanged()));
|
||||
QWidget::connect(_regex, SIGNAL(RegexConfigChanged(RegexConfig)), this,
|
||||
SLOT(RegexChanged(RegexConfig)));
|
||||
QWidget::connect(_pageSegMode, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(PageSegModeChanged(int)));
|
||||
|
||||
populateVideoInputSelection(_videoInputTypes);
|
||||
populateConditionSelection(_condition);
|
||||
|
||||
_patternMatchModeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_ocrLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_modelPathLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_neighborsControlLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_sizeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_checkAreaControlLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_throttleControlLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QHBoxLayout *entryLine1Layout = new QHBoxLayout;
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{videoInputTypes}}", _videoInputTypes},
|
||||
@@ -435,9 +554,17 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
{"{{checkAreaEnable}}", _checkAreaEnable},
|
||||
{"{{checkArea}}", _checkArea},
|
||||
{"{{selectArea}}", _selectArea},
|
||||
{"{{textColor}}", _textColor},
|
||||
{"{{selectColor}}", _selectColor},
|
||||
{"{{textType}}", _pageSegMode},
|
||||
{"{{patternMatchingModes}}", _patternMatchMode},
|
||||
};
|
||||
placeWidgets(obs_module_text("AdvSceneSwitcher.condition.video.entry"),
|
||||
entryLine1Layout, widgetPlaceholders);
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode"),
|
||||
_patternMatchModeLayout, widgetPlaceholders);
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.entry.modelPath"),
|
||||
@@ -454,6 +581,24 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
"AdvSceneSwitcher.condition.video.entry.checkArea"),
|
||||
_checkAreaControlLayout, widgetPlaceholders);
|
||||
|
||||
_ocrLayout->addWidget(_matchText);
|
||||
auto regexLayout = new QHBoxLayout;
|
||||
regexLayout->addWidget(_regex);
|
||||
regexLayout->addStretch();
|
||||
_ocrLayout->addLayout(regexLayout);
|
||||
auto pageModeSegLayout = new QHBoxLayout();
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.entry.orcTextType"),
|
||||
pageModeSegLayout, widgetPlaceholders);
|
||||
_ocrLayout->addLayout(pageModeSegLayout);
|
||||
auto colorPickLayout = new QHBoxLayout();
|
||||
placeWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.entry.orcColorPick"),
|
||||
colorPickLayout, widgetPlaceholders);
|
||||
_ocrLayout->addLayout(colorPickLayout);
|
||||
|
||||
QGridLayout *sizeGrid = new QGridLayout;
|
||||
sizeGrid->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
@@ -467,7 +612,6 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
sizeGrid->addWidget(_maxSize, 1, 1);
|
||||
_sizeLayout->addLayout(sizeGrid);
|
||||
_sizeLayout->addStretch();
|
||||
_sizeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
QHBoxLayout *showMatchLayout = new QHBoxLayout;
|
||||
showMatchLayout->addWidget(_showMatch);
|
||||
@@ -477,8 +621,10 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
mainLayout->addWidget(_usePatternForChangedCheck);
|
||||
mainLayout->addWidget(_patternThreshold);
|
||||
mainLayout->addWidget(_useAlphaAsMask);
|
||||
mainLayout->addLayout(_patternMatchModeLayout);
|
||||
mainLayout->addWidget(_brightnessThreshold);
|
||||
mainLayout->addWidget(_currentBrightness);
|
||||
mainLayout->addLayout(_ocrLayout);
|
||||
mainLayout->addLayout(_modelPathLayout);
|
||||
mainLayout->addWidget(_objectScaleThreshold);
|
||||
mainLayout->addLayout(_neighborsControlLayout);
|
||||
@@ -586,6 +732,8 @@ void MacroConditionVideoEdit::ConditionChanged(int cond)
|
||||
_entryData->_objMatchParameters.cascade =
|
||||
initObjectCascade(path);
|
||||
}
|
||||
|
||||
SetupPreviewDialogParams();
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::ImagePathChanged(const QString &text)
|
||||
@@ -703,8 +851,7 @@ void MacroConditionVideoEdit::UsePatternForChangedCheckChanged(int value)
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_patternMatchParameters.useForChangedCheck = value;
|
||||
_patternThreshold->setVisible(value);
|
||||
adjustSize();
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::PatternThresholdChanged(double value)
|
||||
@@ -742,6 +889,20 @@ void MacroConditionVideoEdit::UseAlphaAsMaskChanged(int value)
|
||||
_entryData->_patternMatchParameters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::PatternMatchModeChanged(int idx)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_patternMatchParameters.matchMode =
|
||||
static_cast<cv::TemplateMatchModes>(
|
||||
_patternMatchMode->itemData(idx).toInt());
|
||||
_previewDialog.PatternMatchParamtersChanged(
|
||||
_entryData->_patternMatchParameters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::BrightnessThresholdChanged(double value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
@@ -808,6 +969,78 @@ void MacroConditionVideoEdit::MaxSizeChanged(advss::Size value)
|
||||
_entryData->_objMatchParameters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::SetupColorLabel(const QColor &color)
|
||||
{
|
||||
_textColor->setText(color.name());
|
||||
_textColor->setPalette(QPalette(color));
|
||||
_textColor->setAutoFillBackground(true);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::SelectColorClicked()
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QColor color = QColorDialog::getColor(
|
||||
_entryData->_ocrParamters.color, this,
|
||||
obs_module_text("AdvSceneSwitcher.condition.video.selectColor"),
|
||||
QColorDialog::ColorDialogOption());
|
||||
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetupColorLabel(color);
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_ocrParamters.color = color;
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::MatchTextChanged()
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_ocrParamters.text =
|
||||
_matchText->toPlainText().toUtf8().constData();
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::RegexChanged(RegexConfig conf)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->_ocrParamters.regex = conf;
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::PageSegModeChanged(int idx)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(GetSwitcher()->m);
|
||||
_entryData->SetPageSegMode(static_cast<tesseract::PageSegMode>(
|
||||
_pageSegMode->itemData(idx).toInt()));
|
||||
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::CheckAreaEnableChanged(int value)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
@@ -905,37 +1138,37 @@ void MacroConditionVideoEdit::ModelPathChanged(const QString &text)
|
||||
}
|
||||
}
|
||||
|
||||
bool needsShowMatch(VideoCondition cond)
|
||||
static bool needsShowMatch(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::OBJECT;
|
||||
cond == VideoCondition::OBJECT || cond == VideoCondition::OCR;
|
||||
}
|
||||
|
||||
bool needsObjectControls(VideoCondition cond)
|
||||
static bool needsObjectControls(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::OBJECT;
|
||||
}
|
||||
|
||||
bool needsThrottleControls(VideoCondition cond)
|
||||
static bool needsThrottleControls(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::OBJECT;
|
||||
}
|
||||
|
||||
bool needsThreshold(VideoCondition cond)
|
||||
static bool needsThreshold(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::HAS_CHANGED ||
|
||||
cond == VideoCondition::HAS_NOT_CHANGED;
|
||||
}
|
||||
|
||||
bool patternControlIsOptional(VideoCondition cond)
|
||||
static bool patternControlIsOptional(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::HAS_CHANGED ||
|
||||
cond == VideoCondition::HAS_NOT_CHANGED;
|
||||
}
|
||||
|
||||
bool needsAreaControls(VideoCondition cond)
|
||||
static bool needsAreaControls(VideoCondition cond)
|
||||
{
|
||||
return cond != VideoCondition::NO_IMAGE;
|
||||
}
|
||||
@@ -951,6 +1184,8 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
|
||||
_patternThreshold->setVisible(needsThreshold(_entryData->_condition));
|
||||
_useAlphaAsMask->setVisible(_entryData->_condition ==
|
||||
VideoCondition::PATTERN);
|
||||
setLayoutVisible(_patternMatchModeLayout,
|
||||
_entryData->_condition == VideoCondition::PATTERN);
|
||||
_brightnessThreshold->setVisible(_entryData->_condition ==
|
||||
VideoCondition::BRIGHTNESS);
|
||||
_currentBrightness->setVisible(_entryData->_condition ==
|
||||
@@ -962,6 +1197,8 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
|
||||
needsObjectControls(_entryData->_condition));
|
||||
_minNeighborsDescription->setVisible(
|
||||
needsObjectControls(_entryData->_condition));
|
||||
setLayoutVisible(_ocrLayout,
|
||||
_entryData->_condition == VideoCondition::OCR);
|
||||
setLayoutVisible(_sizeLayout,
|
||||
needsObjectControls(_entryData->_condition));
|
||||
setLayoutVisible(_modelPathLayout,
|
||||
@@ -977,11 +1214,27 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
|
||||
_entryData->_condition == VideoCondition::HAS_NOT_CHANGED) {
|
||||
_patternThreshold->setVisible(
|
||||
_entryData->_patternMatchParameters.useForChangedCheck);
|
||||
setLayoutVisible(
|
||||
_patternMatchModeLayout,
|
||||
_entryData->_patternMatchParameters.useForChangedCheck);
|
||||
}
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::SetupPreviewDialogParams()
|
||||
{
|
||||
_previewDialog.PatternMatchParamtersChanged(
|
||||
_entryData->_patternMatchParameters);
|
||||
_previewDialog.ObjDetectParamtersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
_previewDialog.OCRParamtersChanged(_entryData->_ocrParamters);
|
||||
_previewDialog.VideoSelectionChanged(_entryData->_video);
|
||||
_previewDialog.AreaParamtersChanged(_entryData->_areaParameters);
|
||||
_previewDialog.ConditionChanged(
|
||||
static_cast<int>(_entryData->_condition));
|
||||
}
|
||||
|
||||
void MacroConditionVideoEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
@@ -1001,10 +1254,17 @@ void MacroConditionVideoEdit::UpdateEntryData()
|
||||
_entryData->_patternMatchParameters.threshold);
|
||||
_useAlphaAsMask->setChecked(
|
||||
_entryData->_patternMatchParameters.useAlphaAsMask);
|
||||
_patternMatchMode->setCurrentIndex(_patternMatchMode->findData(
|
||||
_entryData->_patternMatchParameters.matchMode));
|
||||
_brightnessThreshold->SetDoubleValue(_entryData->_brightnessThreshold);
|
||||
_modelDataPath->SetPath(_entryData->GetModelDataPath().c_str());
|
||||
_objectScaleThreshold->SetDoubleValue(
|
||||
_entryData->_objMatchParameters.scaleFactor);
|
||||
_matchText->setPlainText(_entryData->_ocrParamters.text);
|
||||
_regex->SetRegexConfig(_entryData->_ocrParamters.regex);
|
||||
SetupColorLabel(_entryData->_ocrParamters.color);
|
||||
_pageSegMode->setCurrentIndex(_pageSegMode->findData(
|
||||
static_cast<int>(_entryData->_ocrParamters.GetPageMode())));
|
||||
_minNeighbors->setValue(_entryData->_objMatchParameters.minNeighbors);
|
||||
_minSize->SetSize(_entryData->_objMatchParameters.minSize);
|
||||
_maxSize->SetSize(_entryData->_objMatchParameters.maxSize);
|
||||
@@ -1014,15 +1274,6 @@ void MacroConditionVideoEdit::UpdateEntryData()
|
||||
_checkAreaEnable->setChecked(_entryData->_areaParameters.enable);
|
||||
_checkArea->SetArea(_entryData->_areaParameters.area);
|
||||
UpdatePreviewTooltip();
|
||||
SetupPreviewDialogParams();
|
||||
SetWidgetVisibility();
|
||||
|
||||
// Prepare previewDialog
|
||||
_previewDialog.PatternMatchParamtersChanged(
|
||||
_entryData->_patternMatchParameters);
|
||||
_previewDialog.ObjDetectParamtersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
_previewDialog.VideoSelectionChanged(_entryData->_video);
|
||||
_previewDialog.AreaParamtersChanged(_entryData->_areaParameters);
|
||||
_previewDialog.ConditionChanged(
|
||||
static_cast<int>(_entryData->_condition));
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <file-selection.hpp>
|
||||
#include <screenshot-helper.hpp>
|
||||
#include <slider-spinbox.hpp>
|
||||
#include <variable-text-edit.hpp>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QComboBox>
|
||||
@@ -21,7 +22,7 @@ class PreviewDialog;
|
||||
|
||||
class MacroConditionVideo : public MacroCondition {
|
||||
public:
|
||||
MacroConditionVideo(Macro *m) : MacroCondition(m) {}
|
||||
MacroConditionVideo(Macro *m) : MacroCondition(m, true){};
|
||||
bool CheckCondition();
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
@@ -37,7 +38,8 @@ public:
|
||||
bool LoadModelData(std::string &path);
|
||||
std::string GetModelDataPath() const;
|
||||
void ResetLastMatch() { _lastMatchResult = false; }
|
||||
double GetCurrentBrightness() const { return _currentBrigthness; }
|
||||
double GetCurrentBrightness() const { return _currentBrightness; }
|
||||
void SetPageSegMode(tesseract::PageSegMode);
|
||||
|
||||
VideoInput _video;
|
||||
VideoCondition _condition = VideoCondition::MATCH;
|
||||
@@ -51,7 +53,8 @@ public:
|
||||
bool _blockUntilScreenshotDone = false;
|
||||
double _brightnessThreshold = 0.5;
|
||||
PatternMatchParameters _patternMatchParameters;
|
||||
ObjDetectParamerts _objMatchParameters;
|
||||
ObjDetectParameters _objMatchParameters;
|
||||
OCRParameters _ocrParamters;
|
||||
AreaParamters _areaParameters;
|
||||
bool _throttleEnabled = false;
|
||||
int _throttleCount = 3;
|
||||
@@ -61,6 +64,7 @@ private:
|
||||
bool ScreenshotContainsPattern();
|
||||
bool ScreenshotContainsObject();
|
||||
bool CheckBrightnessThreshold();
|
||||
bool CheckOCR();
|
||||
bool Compare();
|
||||
bool CheckShouldBeSkipped();
|
||||
|
||||
@@ -71,7 +75,8 @@ private:
|
||||
|
||||
bool _lastMatchResult = false;
|
||||
int _runCount = 0;
|
||||
double _currentBrigthness = 0.;
|
||||
|
||||
double _currentBrightness = 0.;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
@@ -108,6 +113,7 @@ private slots:
|
||||
void UsePatternForChangedCheckChanged(int value);
|
||||
void PatternThresholdChanged(double);
|
||||
void UseAlphaAsMaskChanged(int value);
|
||||
void PatternMatchModeChanged(int value);
|
||||
|
||||
void BrightnessThresholdChanged(double);
|
||||
|
||||
@@ -117,6 +123,11 @@ private slots:
|
||||
void MinSizeChanged(advss::Size value);
|
||||
void MaxSizeChanged(advss::Size value);
|
||||
|
||||
void SelectColorClicked();
|
||||
void MatchTextChanged();
|
||||
void RegexChanged(RegexConfig conf);
|
||||
void PageSegModeChanged(int);
|
||||
|
||||
void CheckAreaEnableChanged(int value);
|
||||
void CheckAreaChanged(advss::Area);
|
||||
void CheckAreaChanged(QRect area);
|
||||
@@ -134,6 +145,8 @@ signals:
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
void HandleVideoInputUpdate();
|
||||
void SetupPreviewDialogParams();
|
||||
void SetupColorLabel(const QColor &);
|
||||
|
||||
QComboBox *_videoInputTypes;
|
||||
SceneSelectionWidget *_scenes;
|
||||
@@ -147,10 +160,19 @@ private:
|
||||
|
||||
SliderSpinBox *_patternThreshold;
|
||||
QCheckBox *_useAlphaAsMask;
|
||||
QHBoxLayout *_patternMatchModeLayout;
|
||||
QComboBox *_patternMatchMode;
|
||||
|
||||
SliderSpinBox *_brightnessThreshold;
|
||||
QLabel *_currentBrightness;
|
||||
|
||||
QVBoxLayout *_ocrLayout;
|
||||
VariableTextEdit *_matchText;
|
||||
RegexConfigWidget *_regex;
|
||||
QLabel *_textColor;
|
||||
QPushButton *_selectColor;
|
||||
QComboBox *_pageSegMode;
|
||||
|
||||
FileSelection *_modelDataPath;
|
||||
QHBoxLayout *_modelPathLayout;
|
||||
SliderSpinBox *_objectScaleThreshold;
|
||||
184
src/macro-external/video/opencv-helpers.cpp
Normal file
184
src/macro-external/video/opencv-helpers.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "opencv-helpers.hpp"
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern)
|
||||
{
|
||||
PatternImageData data{};
|
||||
if (pattern.isNull()) {
|
||||
return data;
|
||||
}
|
||||
|
||||
data.rgbaPattern = QImageToMat(pattern);
|
||||
std::vector<cv::Mat1b> rgbaChannelsPattern;
|
||||
cv::split(data.rgbaPattern, rgbaChannelsPattern);
|
||||
std::vector<cv::Mat1b> rgbChanlesPattern(
|
||||
rgbaChannelsPattern.begin(), rgbaChannelsPattern.begin() + 3);
|
||||
cv::merge(rgbChanlesPattern, data.rgbPattern);
|
||||
cv::threshold(rgbaChannelsPattern[3], data.mask, 0, 255,
|
||||
cv::THRESH_BINARY);
|
||||
return data;
|
||||
}
|
||||
|
||||
static void invertPatternMatchResult(cv::Mat &mat)
|
||||
{
|
||||
for (int r = 0; r < mat.rows; r++) {
|
||||
for (int c = 0; c < mat.cols; c++) {
|
||||
float value = mat.at<float>(r, c) =
|
||||
1.0 - mat.at<float>(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, const PatternImageData &patternData,
|
||||
double threshold, cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchMode)
|
||||
{
|
||||
if (img.isNull() || patternData.rgbaPattern.empty()) {
|
||||
return;
|
||||
}
|
||||
if (img.height() < patternData.rgbaPattern.rows ||
|
||||
img.width() < patternData.rgbaPattern.cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto input = QImageToMat(img);
|
||||
|
||||
if (useAlphaAsMask) {
|
||||
// Remove alpha channel of input image as the alpha channel
|
||||
// information is used as a stencil for the pattern instead and
|
||||
// thus should not be used while matching the pattern as well
|
||||
//
|
||||
// Input format is Format_RGBA8888 so discard the 4th channel
|
||||
std::vector<cv::Mat1b> inputChannels;
|
||||
cv::split(input, inputChannels);
|
||||
std::vector<cv::Mat1b> rgbChanlesImage(
|
||||
inputChannels.begin(), inputChannels.begin() + 3);
|
||||
cv::Mat3b rgbInput;
|
||||
cv::merge(rgbChanlesImage, rgbInput);
|
||||
|
||||
cv::matchTemplate(rgbInput, patternData.rgbPattern, result,
|
||||
matchMode, patternData.mask);
|
||||
} else {
|
||||
cv::matchTemplate(input, patternData.rgbaPattern, result,
|
||||
matchMode);
|
||||
}
|
||||
|
||||
// A perfect match is represented as "0" for TM_SQDIFF_NORMED
|
||||
//
|
||||
// For TM_CCOEFF_NORMED and TM_CCORR_NORMED a perfect match is
|
||||
// represented as "1"
|
||||
if (matchMode == cv::TM_SQDIFF_NORMED) {
|
||||
invertPatternMatchResult(result);
|
||||
}
|
||||
cv::threshold(result, result, threshold, 0.0, cv::THRESH_TOZERO);
|
||||
}
|
||||
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchColor)
|
||||
{
|
||||
auto data = createPatternData(pattern);
|
||||
matchPattern(img, data, threshold, result, useAlphaAsMask, matchColor);
|
||||
}
|
||||
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize)
|
||||
{
|
||||
if (img.isNull() || cascade.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat frameGray;
|
||||
cv::cvtColor(i, frameGray, cv::COLOR_RGBA2GRAY);
|
||||
equalizeHist(frameGray, frameGray);
|
||||
std::vector<cv::Rect> objects;
|
||||
cascade.detectMultiScale(frameGray, objects, scaleFactor, minNeighbors,
|
||||
0, minSize, maxSize);
|
||||
return objects;
|
||||
}
|
||||
|
||||
uchar getAvgBrightness(QImage &img)
|
||||
{
|
||||
auto i = QImageToMat(img);
|
||||
cv::Mat hsvImage, rgbImage;
|
||||
cv::cvtColor(i, rgbImage, cv::COLOR_RGBA2RGB);
|
||||
cv::cvtColor(rgbImage, hsvImage, cv::COLOR_RGB2HSV);
|
||||
long long brightnessSum = 0;
|
||||
for (int i = 0; i < hsvImage.rows; ++i) {
|
||||
for (int j = 0; j < hsvImage.cols; ++j) {
|
||||
brightnessSum += hsvImage.at<cv::Vec3b>(i, j)[2];
|
||||
}
|
||||
}
|
||||
return brightnessSum / (hsvImage.rows * hsvImage.cols);
|
||||
}
|
||||
|
||||
cv::Mat preprocessForOCR(const QImage &image, const QColor &color)
|
||||
{
|
||||
auto mat = QImageToMat(image);
|
||||
|
||||
// Only keep the desired color
|
||||
cv::cvtColor(mat, mat, cv::COLOR_RGBA2RGB);
|
||||
cv::cvtColor(mat, mat, cv::COLOR_RGB2HSV);
|
||||
cv::inRange(mat, cv::Scalar(0, 0, 0),
|
||||
cv::Scalar(color.red(), color.green(), color.blue()), mat);
|
||||
|
||||
// Invert to improve ORC detection
|
||||
cv::bitwise_not(mat, mat);
|
||||
|
||||
// Scale image up if selected area is too small
|
||||
// Results will probably still be unsatisfying
|
||||
if (mat.rows <= 300 || mat.cols <= 300) {
|
||||
double scale = 0.;
|
||||
if (mat.rows < mat.cols) {
|
||||
scale = 300. / mat.rows;
|
||||
} else {
|
||||
scale = 300. / mat.cols;
|
||||
}
|
||||
|
||||
cv::resize(mat, mat,
|
||||
cv::Size(mat.cols * scale, mat.rows * scale),
|
||||
cv::INTER_CUBIC);
|
||||
}
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
std::string runOCR(tesseract::TessBaseAPI *ocr, const QImage &image,
|
||||
const QColor &color)
|
||||
{
|
||||
#ifdef OCR_SUPPORT
|
||||
auto mat = preprocessForOCR(image, color);
|
||||
ocr->SetImage(mat.data, mat.cols, mat.rows, 1, mat.step);
|
||||
ocr->Recognize(0);
|
||||
std::unique_ptr<char[]> detectedText(ocr->GetUTF8Text());
|
||||
|
||||
if (!detectedText) {
|
||||
return "";
|
||||
}
|
||||
return detectedText.get();
|
||||
|
||||
#else
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
|
||||
// Assumption is that QImage uses Format_RGBA8888.
|
||||
// Conversion from: https://github.com/dbzhang800/QtOpenCV
|
||||
cv::Mat QImageToMat(const QImage &img)
|
||||
{
|
||||
if (img.isNull()) {
|
||||
return cv::Mat();
|
||||
}
|
||||
return cv::Mat(img.height(), img.width(), CV_8UC(img.depth() / 8),
|
||||
(uchar *)img.bits(), img.bytesPerLine());
|
||||
}
|
||||
|
||||
QImage MatToQImage(const cv::Mat &mat)
|
||||
{
|
||||
if (mat.empty()) {
|
||||
return QImage();
|
||||
}
|
||||
return QImage(mat.data, mat.cols, mat.rows,
|
||||
QImage::Format::Format_RGBA8888);
|
||||
}
|
||||
62
src/macro-external/video/opencv-helpers.hpp
Normal file
62
src/macro-external/video/opencv-helpers.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
#include <QImage>
|
||||
#undef NO // MacOS macro that can conflict with OpenCV
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
#ifdef OCR_SUPPORT
|
||||
#include <tesseract/baseapi.h>
|
||||
#else
|
||||
namespace tesseract {
|
||||
enum PageSegMode {
|
||||
PSM_OSD_ONLY = 0,
|
||||
PSM_AUTO_OSD = 1,
|
||||
PSM_AUTO_ONLY = 2,
|
||||
PSM_AUTO = 3,
|
||||
PSM_SINGLE_COLUMN = 4,
|
||||
PSM_SINGLE_BLOCK_VERT_TEXT = 5,
|
||||
PSM_SINGLE_BLOCK = 6,
|
||||
PSM_SINGLE_LINE = 7,
|
||||
PSM_SINGLE_WORD = 8,
|
||||
PSM_CIRCLE_WORD = 9,
|
||||
PSM_SINGLE_CHAR = 10,
|
||||
PSM_SPARSE_TEXT = 11,
|
||||
PSM_SPARSE_TEXT_OSD = 12,
|
||||
PSM_RAW_LINE = 13,
|
||||
|
||||
PSM_COUNT
|
||||
};
|
||||
|
||||
class TessBaseAPI {
|
||||
public:
|
||||
void SetPageSegMode(PageSegMode) {}
|
||||
int Init(const char *, const char *) { return 0; }
|
||||
void End() {}
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr int minMinNeighbors = 3;
|
||||
constexpr int maxMinNeighbors = 6;
|
||||
constexpr double defaultScaleFactor = 1.1;
|
||||
|
||||
struct PatternImageData {
|
||||
cv::Mat4b rgbaPattern;
|
||||
cv::Mat3b rgbPattern;
|
||||
cv::Mat1b mask;
|
||||
};
|
||||
|
||||
PatternImageData createPatternData(QImage &pattern);
|
||||
void matchPattern(QImage &img, const PatternImageData &patternData,
|
||||
double threshold, cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchMode);
|
||||
void matchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchMode);
|
||||
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
cv::Size minSize, cv::Size maxSize);
|
||||
uchar getAvgBrightness(QImage &img);
|
||||
cv::Mat preprocessForOCR(const QImage &image, const QColor &color);
|
||||
std::string runOCR(tesseract::TessBaseAPI *, const QImage &, const QColor &);
|
||||
cv::Mat QImageToMat(const QImage &img);
|
||||
QImage MatToQImage(const cv::Mat &mat);
|
||||
@@ -6,6 +6,7 @@ bool PatternMatchParameters::Save(obs_data_t *obj) const
|
||||
obs_data_set_bool(data, "useForChangedCheck", useForChangedCheck);
|
||||
obs_data_set_double(data, "threshold", threshold);
|
||||
obs_data_set_bool(data, "useAlphaAsMask", useAlphaAsMask);
|
||||
obs_data_set_int(data, "matchMode", matchMode);
|
||||
obs_data_set_obj(obj, "patternMatchData", data);
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
@@ -25,11 +26,18 @@ bool PatternMatchParameters::Load(obs_data_t *obj)
|
||||
useForChangedCheck = obs_data_get_bool(data, "useForChangedCheck");
|
||||
threshold = obs_data_get_double(data, "threshold");
|
||||
useAlphaAsMask = obs_data_get_bool(data, "useAlphaAsMask");
|
||||
// TODO: Remove this fallback in a future version
|
||||
if (!obs_data_has_user_value(obj, "matchMode")) {
|
||||
matchMode = cv::TM_CCORR_NORMED;
|
||||
} else {
|
||||
matchMode = static_cast<cv::TemplateMatchModes>(
|
||||
obs_data_get_int(data, "matchMode"));
|
||||
}
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ObjDetectParamerts::Save(obs_data_t *obj) const
|
||||
bool ObjDetectParameters::Save(obs_data_t *obj) const
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
obs_data_set_string(data, "modelPath", modelPath.c_str());
|
||||
@@ -53,7 +61,7 @@ bool isMinNeighborsValid(int minNeighbors)
|
||||
minNeighbors <= maxMinNeighbors;
|
||||
}
|
||||
|
||||
bool ObjDetectParamerts::Load(obs_data_t *obj)
|
||||
bool ObjDetectParameters::Load(obs_data_t *obj)
|
||||
{
|
||||
// TODO: Remove this fallback in a future version
|
||||
if (!obs_data_has_user_value(obj, "patternMatchData")) {
|
||||
@@ -188,3 +196,109 @@ OBSWeakSource VideoInput::GetVideo() const
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void SaveColor(obs_data_t *obj, const char *name, const QColor &color)
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
obs_data_set_int(data, "red", color.red());
|
||||
obs_data_set_int(data, "green", color.green());
|
||||
obs_data_set_int(data, "blue", color.blue());
|
||||
obs_data_set_obj(obj, name, data);
|
||||
obs_data_release(data);
|
||||
}
|
||||
|
||||
OCRParameters::OCRParameters()
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
OCRParameters::~OCRParameters()
|
||||
{
|
||||
if (!initDone) {
|
||||
return;
|
||||
}
|
||||
ocr->End();
|
||||
}
|
||||
|
||||
OCRParameters::OCRParameters(const OCRParameters &other)
|
||||
: text(other.text),
|
||||
regex(other.regex),
|
||||
color(other.color),
|
||||
pageSegMode(other.pageSegMode)
|
||||
{
|
||||
Setup();
|
||||
if (initDone) {
|
||||
ocr->SetPageSegMode(pageSegMode);
|
||||
}
|
||||
}
|
||||
|
||||
OCRParameters &OCRParameters::operator=(const OCRParameters &other)
|
||||
{
|
||||
text = other.text;
|
||||
regex = other.regex;
|
||||
color = other.color;
|
||||
pageSegMode = other.pageSegMode;
|
||||
ocr->SetPageSegMode(pageSegMode);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool OCRParameters::Save(obs_data_t *obj) const
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
text.Save(data, "pattern");
|
||||
regex.Save(data);
|
||||
SaveColor(data, "textColor", color);
|
||||
obs_data_set_int(data, "pageSegMode", static_cast<int>(pageSegMode));
|
||||
obs_data_set_obj(obj, "ocrData", data);
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static QColor LoadColor(obs_data_t *obj, const char *name)
|
||||
{
|
||||
QColor color = Qt::black;
|
||||
auto data = obs_data_get_obj(obj, name);
|
||||
color.setRed(obs_data_get_int(data, "red"));
|
||||
color.setGreen(obs_data_get_int(data, "green"));
|
||||
color.setBlue(obs_data_get_int(data, "blue"));
|
||||
obs_data_release(data);
|
||||
return color;
|
||||
}
|
||||
|
||||
bool OCRParameters::Load(obs_data_t *obj)
|
||||
{
|
||||
auto data = obs_data_get_obj(obj, "ocrData");
|
||||
text.Load(data, "pattern");
|
||||
regex.Load(data);
|
||||
color = LoadColor(data, "textColor");
|
||||
pageSegMode = static_cast<tesseract::PageSegMode>(
|
||||
obs_data_get_int(data, "pageSegMode"));
|
||||
obs_data_release(data);
|
||||
|
||||
if (initDone) {
|
||||
ocr->SetPageSegMode(pageSegMode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OCRParameters::SetPageMode(tesseract::PageSegMode mode)
|
||||
{
|
||||
pageSegMode = mode;
|
||||
ocr->SetPageSegMode(mode);
|
||||
}
|
||||
|
||||
void OCRParameters::Setup()
|
||||
{
|
||||
ocr = std::make_unique<tesseract::TessBaseAPI>();
|
||||
if (!ocr) {
|
||||
initDone = false;
|
||||
return;
|
||||
}
|
||||
std::string dataPath = obs_get_module_data_path(obs_current_module()) +
|
||||
std::string("/res/ocr");
|
||||
if (ocr->Init(dataPath.c_str(), "eng") != 0) {
|
||||
initDone = false;
|
||||
return;
|
||||
}
|
||||
initDone = true;
|
||||
}
|
||||
@@ -4,9 +4,17 @@
|
||||
|
||||
#include <source-selection.hpp>
|
||||
#include <scene-selection.hpp>
|
||||
#include <regex-config.hpp>
|
||||
#include <variable.hpp>
|
||||
#include <obs.hpp>
|
||||
#include <obs-module.h>
|
||||
|
||||
#include <QMetaType>
|
||||
|
||||
#ifdef OCR_SUPPORT
|
||||
#include <tesseract/baseapi.h>
|
||||
#endif
|
||||
|
||||
enum class VideoCondition {
|
||||
MATCH,
|
||||
DIFFER,
|
||||
@@ -16,6 +24,7 @@ enum class VideoCondition {
|
||||
PATTERN,
|
||||
OBJECT,
|
||||
BRIGHTNESS,
|
||||
OCR,
|
||||
};
|
||||
|
||||
class VideoInput {
|
||||
@@ -45,10 +54,11 @@ public:
|
||||
QImage image;
|
||||
bool useForChangedCheck = false;
|
||||
bool useAlphaAsMask = false;
|
||||
cv::TemplateMatchModes matchMode = cv::TM_CCORR_NORMED;
|
||||
double threshold = 0.8;
|
||||
};
|
||||
|
||||
class ObjDetectParamerts {
|
||||
class ObjDetectParameters {
|
||||
public:
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
@@ -64,6 +74,36 @@ public:
|
||||
advss::Size maxSize{0, 0};
|
||||
};
|
||||
|
||||
class OCRParameters {
|
||||
public:
|
||||
OCRParameters();
|
||||
~OCRParameters();
|
||||
OCRParameters(const OCRParameters &other);
|
||||
OCRParameters &operator=(const OCRParameters &);
|
||||
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
|
||||
bool Initialized() const { return initDone; }
|
||||
void SetPageMode(tesseract::PageSegMode);
|
||||
tesseract::PageSegMode GetPageMode() const { return pageSegMode; }
|
||||
tesseract::TessBaseAPI *GetOCR() const { return ocr.get(); }
|
||||
|
||||
VariableResolvingString text =
|
||||
obs_module_text("AdvSceneSwitcher.enterText");
|
||||
RegexConfig regex = RegexConfig::PartialMatchRegexConfig();
|
||||
QColor color = Qt::black;
|
||||
|
||||
private:
|
||||
void Setup();
|
||||
|
||||
tesseract::PageSegMode pageSegMode = tesseract::PSM_SINGLE_BLOCK;
|
||||
std::unique_ptr<tesseract::TessBaseAPI> ocr;
|
||||
bool initDone = false;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(OCRParameters)
|
||||
|
||||
class AreaParamters {
|
||||
public:
|
||||
bool Save(obs_data_t *obj) const;
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
#include <screenshot-helper.hpp>
|
||||
|
||||
PreviewDialog::PreviewDialog(QWidget *parent, int delay)
|
||||
PreviewDialog::PreviewDialog(QWidget *parent)
|
||||
: QDialog(parent),
|
||||
_scrollArea(new QScrollArea),
|
||||
_imageLabel(new QLabel(this)),
|
||||
_rubberBand(new QRubberBand(QRubberBand::Rectangle, this)),
|
||||
_delay(delay)
|
||||
_rubberBand(new QRubberBand(QRubberBand::Rectangle, this))
|
||||
{
|
||||
setWindowTitle("Advanced Scene Switcher");
|
||||
setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint |
|
||||
@@ -39,22 +38,12 @@ PreviewDialog::PreviewDialog(QWidget *parent, int delay)
|
||||
layout->addWidget(_statusLabel);
|
||||
layout->addWidget(_scrollArea);
|
||||
setLayout(layout);
|
||||
|
||||
// This is a workaround to handle random segfaults triggered when using:
|
||||
// QMetaObject::invokeMethod(this, "RedrawImage", Qt::QueuedConnection,
|
||||
// Q_ARG(QImage, image));
|
||||
// from within CheckForMatchLoop().
|
||||
// Even using BlockingQueuedConnection causes deadlocks
|
||||
_timer.setInterval(300);
|
||||
QWidget::connect(&_timer, &QTimer::timeout, this,
|
||||
&PreviewDialog::ResizeImageLabel);
|
||||
_timer.start();
|
||||
}
|
||||
|
||||
void PreviewDialog::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
_selectingArea = true;
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_type == PreviewType::SHOW_MATCH) {
|
||||
return;
|
||||
}
|
||||
_origin = event->pos();
|
||||
@@ -64,7 +53,7 @@ void PreviewDialog::mousePressEvent(QMouseEvent *event)
|
||||
|
||||
void PreviewDialog::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_type == PreviewType::SHOW_MATCH) {
|
||||
return;
|
||||
}
|
||||
_rubberBand->setGeometry(QRect(_origin, event->pos()).normalized());
|
||||
@@ -72,7 +61,7 @@ void PreviewDialog::mouseMoveEvent(QMouseEvent *event)
|
||||
|
||||
void PreviewDialog::mouseReleaseEvent(QMouseEvent *)
|
||||
{
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_type == PreviewType::SHOW_MATCH) {
|
||||
return;
|
||||
}
|
||||
auto selectionStart =
|
||||
@@ -100,16 +89,16 @@ PreviewDialog::~PreviewDialog()
|
||||
|
||||
void PreviewDialog::ShowMatch()
|
||||
{
|
||||
Start();
|
||||
_type = PreviewType::SHOW_MATCH;
|
||||
_rubberBand->hide();
|
||||
_type = Type::SHOW_MATCH;
|
||||
Start();
|
||||
}
|
||||
|
||||
void PreviewDialog::SelectArea()
|
||||
{
|
||||
_selectingArea = false;
|
||||
_type = PreviewType::SELECT_AREA;
|
||||
Start();
|
||||
_type = Type::SELECT_AREA;
|
||||
DrawFrame();
|
||||
_statusLabel->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectArea.status"));
|
||||
@@ -117,11 +106,8 @@ void PreviewDialog::SelectArea()
|
||||
|
||||
void PreviewDialog::Stop()
|
||||
{
|
||||
_stop = true;
|
||||
_cv.notify_all();
|
||||
if (_thread.joinable()) {
|
||||
_thread.join();
|
||||
}
|
||||
_thread.quit();
|
||||
_thread.wait();
|
||||
}
|
||||
|
||||
void PreviewDialog::closeEvent(QCloseEvent *event)
|
||||
@@ -137,12 +123,18 @@ void PreviewDialog::PatternMatchParamtersChanged(
|
||||
_patternImageData = createPatternData(_patternMatchParams.image);
|
||||
}
|
||||
|
||||
void PreviewDialog::ObjDetectParamtersChanged(const ObjDetectParamerts ¶ms)
|
||||
void PreviewDialog::ObjDetectParamtersChanged(const ObjDetectParameters ¶ms)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
_objDetectParams = params;
|
||||
}
|
||||
|
||||
void PreviewDialog::OCRParamtersChanged(const OCRParameters ¶ms)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
_ocrParams = params;
|
||||
}
|
||||
|
||||
void PreviewDialog::VideoSelectionChanged(const VideoInput &video)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
@@ -164,79 +156,66 @@ void PreviewDialog::ConditionChanged(int cond)
|
||||
_condition = static_cast<VideoCondition>(cond);
|
||||
}
|
||||
|
||||
void PreviewDialog::ResizeImageLabel()
|
||||
void PreviewDialog::UpdateStatus(const QString &status)
|
||||
{
|
||||
_statusLabel->setText(status);
|
||||
}
|
||||
|
||||
void PreviewDialog::UpdateImage(const QPixmap &image)
|
||||
{
|
||||
_imageLabel->setPixmap(image);
|
||||
_imageLabel->adjustSize();
|
||||
if (_type == Type::SELECT_AREA && !_selectingArea) {
|
||||
if (_type == PreviewType::SELECT_AREA && !_selectingArea) {
|
||||
DrawFrame();
|
||||
}
|
||||
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
|
||||
_objDetectParams, _ocrParams, _areaParams, _condition);
|
||||
}
|
||||
|
||||
void PreviewDialog::Start()
|
||||
{
|
||||
if (_thread.joinable()) {
|
||||
return;
|
||||
}
|
||||
if (!_video.ValidSelection()) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotFail"));
|
||||
close();
|
||||
return;
|
||||
}
|
||||
_stop = false;
|
||||
_thread = std::thread(&PreviewDialog::CheckForMatchLoop, this);
|
||||
}
|
||||
|
||||
void PreviewDialog::CheckForMatchLoop()
|
||||
{
|
||||
while (!_stop) {
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
auto source = obs_weak_source_get_source(_video.GetVideo());
|
||||
ScreenshotHelper screenshot(source);
|
||||
obs_source_release(source);
|
||||
_cv.wait_for(lock, std::chrono::milliseconds(_delay));
|
||||
if (_stop || isHidden()) {
|
||||
return;
|
||||
}
|
||||
if (!_video.ValidSelection()) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotFail"));
|
||||
_imageLabel->setPixmap(QPixmap());
|
||||
continue;
|
||||
}
|
||||
if (!screenshot.done) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotFail"));
|
||||
_imageLabel->setPixmap(QPixmap());
|
||||
if (_delay < 1000) {
|
||||
_delay += 50;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (screenshot.image.width() == 0 ||
|
||||
screenshot.image.height() == 0) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotEmpty"));
|
||||
_imageLabel->setPixmap(QPixmap());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_type == Type::SHOW_MATCH) {
|
||||
if (_areaParams.enable) {
|
||||
screenshot.image = screenshot.image.copy(
|
||||
_areaParams.area.x, _areaParams.area.y,
|
||||
_areaParams.area.width,
|
||||
_areaParams.area.height);
|
||||
}
|
||||
MarkMatch(screenshot.image);
|
||||
} else {
|
||||
_statusLabel->setText("");
|
||||
}
|
||||
_imageLabel->setPixmap(QPixmap::fromImage(screenshot.image));
|
||||
if (_thread.isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewImage *worker = new PreviewImage();
|
||||
worker->moveToThread(&_thread);
|
||||
connect(&_thread, &QThread::finished, worker, &QObject::deleteLater);
|
||||
connect(worker, &PreviewImage::ImageReady, this,
|
||||
&PreviewDialog::UpdateImage);
|
||||
connect(worker, &PreviewImage::StatusUpdate, this,
|
||||
&PreviewDialog::UpdateStatus);
|
||||
connect(this, &PreviewDialog::NeedImage, worker,
|
||||
&PreviewImage::CreateImage);
|
||||
_thread.start();
|
||||
|
||||
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
|
||||
_objDetectParams, _ocrParams, _areaParams, _condition);
|
||||
}
|
||||
|
||||
void markPatterns(cv::Mat &matchResult, QImage &image, cv::Mat &pattern)
|
||||
void PreviewDialog::DrawFrame()
|
||||
{
|
||||
if (!_video.ValidSelection()) {
|
||||
return;
|
||||
}
|
||||
auto imageStart =
|
||||
_imageLabel->mapToGlobal(_imageLabel->rect().topLeft());
|
||||
auto windowStart = mapToGlobal(rect().topLeft());
|
||||
_rubberBand->resize(_areaParams.area.width, _areaParams.area.height);
|
||||
_rubberBand->move(
|
||||
_areaParams.area.x + (imageStart.x() - windowStart.x()),
|
||||
_areaParams.area.y + (imageStart.y() - windowStart.y()));
|
||||
_rubberBand->show();
|
||||
}
|
||||
|
||||
void markPatterns(cv::Mat &matchResult, QImage &image, const cv::Mat &pattern)
|
||||
{
|
||||
auto matchImg = QImageToMat(image);
|
||||
for (int row = 0; row < matchResult.rows - 1; row++) {
|
||||
@@ -262,50 +241,91 @@ void markObjects(QImage &image, std::vector<cv::Rect> &objects)
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewDialog::MarkMatch(QImage &screenshot)
|
||||
void PreviewImage::CreateImage(const VideoInput &video, PreviewType type,
|
||||
const PatternMatchParameters &patternMatchParams,
|
||||
const PatternImageData &patternImageData,
|
||||
ObjDetectParameters objDetectParams,
|
||||
OCRParameters ocrParams,
|
||||
const AreaParamters &areaParams,
|
||||
VideoCondition condition)
|
||||
{
|
||||
if (_condition == VideoCondition::PATTERN) {
|
||||
auto source = obs_weak_source_get_source(video.GetVideo());
|
||||
ScreenshotHelper screenshot(source, true);
|
||||
obs_source_release(source);
|
||||
|
||||
if (!video.ValidSelection() || !screenshot.done) {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotFail"));
|
||||
emit ImageReady(QPixmap());
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenshot.image.width() == 0 || screenshot.image.height() == 0) {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.screenshotEmpty"));
|
||||
emit ImageReady(QPixmap());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == PreviewType::SHOW_MATCH) {
|
||||
if (areaParams.enable) {
|
||||
screenshot.image = screenshot.image.copy(
|
||||
areaParams.area.x, areaParams.area.y,
|
||||
areaParams.area.width, areaParams.area.height);
|
||||
}
|
||||
// Will emit status label update
|
||||
MarkMatch(screenshot.image, patternMatchParams,
|
||||
patternImageData, objDetectParams, ocrParams,
|
||||
condition);
|
||||
} else {
|
||||
emit StatusUpdate("");
|
||||
}
|
||||
emit ImageReady(QPixmap::fromImage(screenshot.image));
|
||||
}
|
||||
|
||||
void PreviewImage::MarkMatch(QImage &screenshot,
|
||||
const PatternMatchParameters &patternMatchParams,
|
||||
const PatternImageData &patternImageData,
|
||||
ObjDetectParameters &objDetectParams,
|
||||
const OCRParameters &ocrParams,
|
||||
VideoCondition condition)
|
||||
{
|
||||
if (condition == VideoCondition::PATTERN) {
|
||||
cv::Mat result;
|
||||
matchPattern(screenshot, _patternImageData,
|
||||
_patternMatchParams.threshold, result,
|
||||
_patternMatchParams.useAlphaAsMask);
|
||||
matchPattern(screenshot, patternImageData,
|
||||
patternMatchParams.threshold, result,
|
||||
patternMatchParams.useAlphaAsMask,
|
||||
patternMatchParams.matchMode);
|
||||
if (countNonZero(result) == 0) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchFail"));
|
||||
} else {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.patternMatchSuccess"));
|
||||
markPatterns(result, screenshot,
|
||||
_patternImageData.rgbaPattern);
|
||||
patternImageData.rgbaPattern);
|
||||
}
|
||||
} else if (_condition == VideoCondition::OBJECT) {
|
||||
auto objects = matchObject(screenshot, _objDetectParams.cascade,
|
||||
_objDetectParams.scaleFactor,
|
||||
_objDetectParams.minNeighbors,
|
||||
_objDetectParams.minSize.CV(),
|
||||
_objDetectParams.maxSize.CV());
|
||||
} else if (condition == VideoCondition::OBJECT) {
|
||||
auto objects = matchObject(screenshot, objDetectParams.cascade,
|
||||
objDetectParams.scaleFactor,
|
||||
objDetectParams.minNeighbors,
|
||||
objDetectParams.minSize.CV(),
|
||||
objDetectParams.maxSize.CV());
|
||||
if (objects.empty()) {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectMatchFail"));
|
||||
} else {
|
||||
_statusLabel->setText(obs_module_text(
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectMatchSuccess"));
|
||||
markObjects(screenshot, objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (condition == VideoCondition::OCR) {
|
||||
auto text =
|
||||
runOCR(ocrParams.GetOCR(), screenshot, ocrParams.color);
|
||||
QString status(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrMatchSuccess"));
|
||||
emit StatusUpdate(status.arg(QString::fromStdString(text)));
|
||||
|
||||
void PreviewDialog::DrawFrame()
|
||||
{
|
||||
if (!_video.ValidSelection()) {
|
||||
return;
|
||||
// TODO: show preprocessed image
|
||||
}
|
||||
auto imageStart =
|
||||
_imageLabel->mapToGlobal(_imageLabel->rect().topLeft());
|
||||
auto windowStart = mapToGlobal(rect().topLeft());
|
||||
_rubberBand->resize(_areaParams.area.width, _areaParams.area.height);
|
||||
_rubberBand->move(
|
||||
_areaParams.area.x + (imageStart.x() - windowStart.x()),
|
||||
_areaParams.area.y + (imageStart.y() - windowStart.y()));
|
||||
_rubberBand->show();
|
||||
}
|
||||
@@ -4,21 +4,40 @@
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
|
||||
#include <QThread>
|
||||
#include <QMouseEvent>
|
||||
#include <QRubberBand>
|
||||
#include <QPoint>
|
||||
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
enum class PreviewType {
|
||||
SHOW_MATCH,
|
||||
SELECT_AREA,
|
||||
};
|
||||
|
||||
class PreviewImage : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public slots:
|
||||
void CreateImage(const VideoInput &, PreviewType,
|
||||
const PatternMatchParameters &,
|
||||
const PatternImageData &, ObjDetectParameters,
|
||||
OCRParameters, const AreaParamters &, VideoCondition);
|
||||
signals:
|
||||
void ImageReady(const QPixmap &);
|
||||
void StatusUpdate(const QString &);
|
||||
|
||||
private:
|
||||
void MarkMatch(QImage &screenshot, const PatternMatchParameters &,
|
||||
const PatternImageData &, ObjDetectParameters &,
|
||||
const OCRParameters &, VideoCondition);
|
||||
};
|
||||
|
||||
class PreviewDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PreviewDialog(QWidget *parent, int delay);
|
||||
PreviewDialog(QWidget *parent);
|
||||
virtual ~PreviewDialog();
|
||||
void ShowMatch();
|
||||
void SelectArea();
|
||||
@@ -27,19 +46,23 @@ public:
|
||||
|
||||
public slots:
|
||||
void PatternMatchParamtersChanged(const PatternMatchParameters &);
|
||||
void ObjDetectParamtersChanged(const ObjDetectParamerts &);
|
||||
void ObjDetectParamtersChanged(const ObjDetectParameters &);
|
||||
void OCRParamtersChanged(const OCRParameters &);
|
||||
void VideoSelectionChanged(const VideoInput &);
|
||||
void AreaParamtersChanged(const AreaParamters &);
|
||||
void ConditionChanged(int cond);
|
||||
private slots:
|
||||
void ResizeImageLabel();
|
||||
void UpdateImage(const QPixmap &);
|
||||
void UpdateStatus(const QString &);
|
||||
signals:
|
||||
void SelectionAreaChanged(QRect area);
|
||||
void NeedImage(const VideoInput &, PreviewType,
|
||||
const PatternMatchParameters &, const PatternImageData &,
|
||||
ObjDetectParameters, OCRParameters,
|
||||
const AreaParamters &, VideoCondition);
|
||||
|
||||
private:
|
||||
void Start();
|
||||
void CheckForMatchLoop();
|
||||
void MarkMatch(QImage &screenshot);
|
||||
void DrawFrame();
|
||||
|
||||
void mousePressEvent(QMouseEvent *event);
|
||||
@@ -49,28 +72,21 @@ private:
|
||||
VideoInput _video;
|
||||
PatternMatchParameters _patternMatchParams;
|
||||
PatternImageData _patternImageData;
|
||||
ObjDetectParamerts _objDetectParams;
|
||||
ObjDetectParameters _objDetectParams;
|
||||
OCRParameters _ocrParams;
|
||||
AreaParamters _areaParams;
|
||||
|
||||
VideoCondition _condition = VideoCondition::PATTERN;
|
||||
QScrollArea *_scrollArea;
|
||||
QLabel *_statusLabel;
|
||||
QLabel *_imageLabel;
|
||||
QTimer _timer;
|
||||
|
||||
QPoint _origin;
|
||||
QRubberBand *_rubberBand = nullptr;
|
||||
std::atomic_bool _selectingArea = {false};
|
||||
|
||||
std::mutex _mtx;
|
||||
std::condition_variable _cv;
|
||||
std::thread _thread;
|
||||
std::atomic_bool _stop = {true};
|
||||
PreviewType _type = PreviewType::SHOW_MATCH;
|
||||
|
||||
enum class Type {
|
||||
SHOW_MATCH,
|
||||
SELECT_AREA,
|
||||
};
|
||||
Type _type = Type::SHOW_MATCH;
|
||||
int _delay = 300;
|
||||
std::mutex _mtx;
|
||||
QThread _thread;
|
||||
};
|
||||
Reference in New Issue
Block a user