From 71d5cd17c32c846fc29bdaa2a03c30f539b198b4 Mon Sep 17 00:00:00 2001 From: Megamouse Date: Tue, 20 Oct 2026 12:32:37 +0200 Subject: [PATCH] psmove: get rid of opencv --- .ci/build-freebsd.sh | 3 +- .ci/build-linux-aarch64.sh | 3 +- .ci/build-linux.sh | 3 +- .ci/build-mac.sh | 4 +- .ci/build-windows-clang.sh | 3 +- .ci/install-freebsd.sh | 2 +- .github/parameters/setup-macports.yaml | 1 - .github/workflows/rpcs3.yml | 1 - .gitmodules | 4 - 3rdparty/CMakeLists.txt | 4 - 3rdparty/opencv/CMakeLists.txt | 21 - 3rdparty/opencv/opencv | 1 - CMakeLists.txt | 1 - CMakePresets.json | 3 - buildfiles/msvc/rpcs3_default.props | 2 - rpcs3/CMakeLists.txt | 2 +- rpcs3/Emu/Cell/Modules/cellGem.cpp | 4 +- rpcs3/Input/blob_detector.cpp | 941 +++++++++++++++++++++++ rpcs3/Input/blob_detector.h | 131 ++++ rpcs3/Input/ps_move_tracker.cpp | 351 +++++---- rpcs3/Input/ps_move_tracker.h | 19 +- rpcs3/rpcs3.vcxproj | 20 +- rpcs3/rpcs3.vcxproj.filters | 6 + rpcs3/rpcs3qt/CMakeLists.txt | 2 +- rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp | 12 +- rpcs3/tests/rpcs3_test.vcxproj | 11 +- rpcs3/tests/test_blob_detector.cpp | 663 ++++++++++++++++ rpcs3/tests/test_iso_extent.cpp | 116 +-- rpcs3/tests/test_iso_open.cpp | 103 +-- scripts/win_build.ps1 | 1 - 30 files changed, 2073 insertions(+), 365 deletions(-) delete mode 100644 3rdparty/opencv/CMakeLists.txt delete mode 160000 3rdparty/opencv/opencv create mode 100644 rpcs3/Input/blob_detector.cpp create mode 100644 rpcs3/Input/blob_detector.h create mode 100644 rpcs3/tests/test_blob_detector.cpp diff --git a/.ci/build-freebsd.sh b/.ci/build-freebsd.sh index 7e10691203..439d54890b 100755 --- a/.ci/build-freebsd.sh +++ b/.ci/build-freebsd.sh @@ -4,7 +4,7 @@ # Note: Tried to use git submodule status, but it takes over 20 seconds # shellcheck disable=SC2046 git config --global --add safe.directory . -git submodule -q update --init --depth 1 $(awk '/path/ && !/llvm/ && !/opencv/ && !/libpng/ && !/libsdl-org/ && !/curl/ && !/zlib/ && !/libusb/ && !/feralinteractive/ { print $3 }' .gitmodules) +git submodule -q update --init --depth 1 $(awk '/path/ && !/llvm/ && !/libpng/ && !/libsdl-org/ && !/curl/ && !/zlib/ && !/libusb/ && !/feralinteractive/ { print $3 }' .gitmodules) CONFIGURE_ARGS=" -DWITH_LLVM=ON @@ -15,7 +15,6 @@ CONFIGURE_ARGS=" -DUSE_SYSTEM_CURL=ON -DUSE_SYSTEM_LIBPNG=ON -DUSE_SYSTEM_LIBUSB=ON - -DUSE_SYSTEM_OPENCV=ON " # base Clang workaround (missing clang-scan-deps) diff --git a/.ci/build-linux-aarch64.sh b/.ci/build-linux-aarch64.sh index 26fc1dc9d9..914d45333f 100755 --- a/.ci/build-linux-aarch64.sh +++ b/.ci/build-linux-aarch64.sh @@ -8,7 +8,7 @@ git config --global --add safe.directory '*' # Pull all the submodules except some # shellcheck disable=SC2046 -git submodule -q update --init $(awk '/path/ && !/llvm/ && !/opencv/ && !/libsdl-org/ && !/curl/ && !/zlib/ { print $3 }' .gitmodules) +git submodule -q update --init $(awk '/path/ && !/llvm/ && !/libsdl-org/ && !/curl/ && !/zlib/ { print $3 }' .gitmodules) mkdir build && cd build || exit 1 @@ -36,7 +36,6 @@ cmake .. \ -DUSE_SDL=ON \ -DUSE_SYSTEM_SDL=ON \ -DUSE_SYSTEM_FFMPEG=ON \ - -DUSE_SYSTEM_OPENCV=ON \ -DUSE_DISCORD_RPC=ON \ -DOpenGL_GL_PREFERENCE=LEGACY \ -DLLVM_DIR=/opt/llvm/lib/cmake/llvm \ diff --git a/.ci/build-linux.sh b/.ci/build-linux.sh index d9f66f6a04..e9ee3230f1 100755 --- a/.ci/build-linux.sh +++ b/.ci/build-linux.sh @@ -9,7 +9,7 @@ git config --global --add safe.directory '*' # Pull all the submodules except some # Note: Tried to use git submodule status, but it takes over 20 seconds # shellcheck disable=SC2046 -git submodule -q update --init $(awk '/path/ && !/llvm/ && !/opencv/ && !/libsdl-org/ && !/curl/ && !/zlib/ { print $3 }' .gitmodules) +git submodule -q update --init $(awk '/path/ && !/llvm/ && !/libsdl-org/ && !/curl/ && !/zlib/ { print $3 }' .gitmodules) mkdir build && cd build || exit 1 @@ -47,7 +47,6 @@ cmake .. \ -DUSE_SDL=ON \ -DUSE_SYSTEM_SDL=ON \ -DUSE_SYSTEM_FFMPEG=ON \ - -DUSE_SYSTEM_OPENCV=ON \ -DUSE_DISCORD_RPC=ON \ -DOpenGL_GL_PREFERENCE=LEGACY \ -DLLVM_DIR=/opt/llvm/lib/cmake/llvm \ diff --git a/.ci/build-mac.sh b/.ci/build-mac.sh index 5b39b1ad2a..ed8788cce5 100755 --- a/.ci/build-mac.sh +++ b/.ci/build-mac.sh @@ -43,7 +43,6 @@ ditto "/tmp/Qt/$QT_VER" "qt-downloader/$QT_VER" export Qt6_DIR="$WORKDIR/qt-downloader/$QT_VER/clang_64/lib/cmake/Qt$QT_VER_MAIN" export SDL3_DIR="/opt/local/lib/cmake/SDL3" -export OpenCV_DIR="/opt/local/libexec/opencv4" export PATH="/opt/local/libexec/llvm-$LLVM_COMPILER_VER/bin:/$WORKDIR/qt-downloader/$QT_VER/clang_64/bin:/opt/local/bin:/opt/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Library/Apple/usr/bin:$PATH" export LDFLAGS="-L/opt/local/lib -Wl,-rpath,/opt/local/lib" export VULKAN_SDK="/opt/local" @@ -51,7 +50,7 @@ export LLVM_DIR="/opt/local/libexec/llvm-$LLVM_COMPILER_VER" # Pull all the submodules except some # shellcheck disable=SC2046 -git submodule -q update --init --depth=1 --jobs=8 $(awk '/path/ && !/llvm/ && !/opencv/ && !/libsdl-org/ && !/feralinteractive/ && !/curl/ && !/zlib/ { print $3 }' .gitmodules) +git submodule -q update --init --depth=1 --jobs=8 $(awk '/path/ && !/llvm/ && !/libsdl-org/ && !/feralinteractive/ && !/curl/ && !/zlib/ { print $3 }' .gitmodules) mkdir build && cd build || exit 1 # The below should be uncommented once bugs with Qt 6 QListWidgets when using the OS 26 visual style are resolved. @@ -73,7 +72,6 @@ cmake .. \ -DUSE_PRECOMPILED_HEADERS=OFF \ -DUSE_SYSTEM_MVK=ON \ -DUSE_SYSTEM_SDL=ON \ - -DUSE_SYSTEM_OPENCV=ON \ -G Ninja ninja; build_status=$?; diff --git a/.ci/build-windows-clang.sh b/.ci/build-windows-clang.sh index 4f07688ee5..ac52f2f471 100644 --- a/.ci/build-windows-clang.sh +++ b/.ci/build-windows-clang.sh @@ -8,7 +8,7 @@ MSYS2="${2:-clang64}" # Pull all the submodules except some # Note: Tried to use git submodule status, but it takes over 20 seconds # shellcheck disable=SC2046 -git submodule -q update --init $(awk '/path/ && !/llvm/ && !/opencv/ && !/ffmpeg/ && !/curl/ && !/FAudio/ && !/zlib/ { print $3 }' .gitmodules) +git submodule -q update --init $(awk '/path/ && !/llvm/ && !/ffmpeg/ && !/curl/ && !/FAudio/ && !/zlib/ { print $3 }' .gitmodules) mkdir build && cd build || exit 1 @@ -42,7 +42,6 @@ cmake .. \ -DUSE_SDL=ON \ -DUSE_SYSTEM_SDL=OFF \ -DUSE_SYSTEM_FFMPEG=ON \ - -DUSE_SYSTEM_OPENCV=ON \ -DUSE_SYSTEM_OPENAL=OFF \ -DUSE_DISCORD_RPC=ON \ -DOpenGL_GL_PREFERENCE=LEGACY \ diff --git a/.ci/install-freebsd.sh b/.ci/install-freebsd.sh index 836ff2f4d9..193ea3d979 100755 --- a/.ci/install-freebsd.sh +++ b/.ci/install-freebsd.sh @@ -15,4 +15,4 @@ pkg install "llvm$LLVM_COMPILER_VER" pkg install git ccache cmake ninja "qt$QT_VER_MAIN-multimedia" "qt$QT_VER_MAIN-svg" glew openal-soft ffmpeg pcre2 # Optional dependencies (libevdev is pulled by qtX-base) -pkg install pkgconf alsa-lib pulseaudio sdl3 evdev-proto vulkan-headers vulkan-loader opencv +pkg install pkgconf alsa-lib pulseaudio sdl3 evdev-proto vulkan-headers vulkan-loader diff --git a/.github/parameters/setup-macports.yaml b/.github/parameters/setup-macports.yaml index 1da4c67d44..fba26efc02 100644 --- a/.github/parameters/setup-macports.yaml +++ b/.github/parameters/setup-macports.yaml @@ -2,7 +2,6 @@ ports: - name: abseil - name: ccache - name: gtest - - name: opencv4 - name: sdl3 - name: vulkan-headers - name: vulkan-loader diff --git a/.github/workflows/rpcs3.yml b/.github/workflows/rpcs3.yml index 0303fd1128..c637ea6f62 100644 --- a/.github/workflows/rpcs3.yml +++ b/.github/workflows/rpcs3.yml @@ -534,7 +534,6 @@ jobs: mingw-w64-clang-${{ matrix.arch }}-ninja mingw-w64-clang-${{ matrix.arch }}-llvm mingw-w64-clang-${{ matrix.arch }}-ffmpeg - mingw-w64-clang-${{ matrix.arch }}-opencv mingw-w64-clang-${{ matrix.arch }}-iconv mingw-w64-clang-${{ matrix.arch }}-glew mingw-w64-clang-${{ matrix.arch }}-vulkan diff --git a/.gitmodules b/.gitmodules index 4c1c0f0f12..868c64158a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -88,10 +88,6 @@ path = 3rdparty/stblib/stb url = ../../nothings/stb.git ignore = dirty -[submodule "3rdparty/opencv/opencv"] - path = 3rdparty/opencv/opencv - url = ../../Megamouse/opencv_minimal.git - ignore = dirty [submodule "3rdparty/fusion/fusion"] path = 3rdparty/fusion/fusion url = ../../xioTechnologies/Fusion.git diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index b25afb8834..4d1fc687ef 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -344,9 +344,6 @@ add_subdirectory(miniupnp EXCLUDE_FROM_ALL) # RTMIDI add_subdirectory(rtmidi EXCLUDE_FROM_ALL) -# OPENCV -add_subdirectory(opencv EXCLUDE_FROM_ALL) - # FUSION add_subdirectory(fusion EXCLUDE_FROM_ALL) @@ -383,6 +380,5 @@ add_library(3rdparty::soundtouch ALIAS soundtouch) add_library(3rdparty::sdl3 ALIAS ${SDL3_TARGET}) add_library(3rdparty::miniupnpc ALIAS 3rdparty_miniupnpc) add_library(3rdparty::rtmidi ALIAS rtmidi) -add_library(3rdparty::opencv ALIAS ${OPENCV_TARGET}) add_library(3rdparty::fusion ALIAS Fusion) add_library(3rdparty::feralinteractive ALIAS 3rdparty_feralinteractive) diff --git a/3rdparty/opencv/CMakeLists.txt b/3rdparty/opencv/CMakeLists.txt deleted file mode 100644 index 93441d6546..0000000000 --- a/3rdparty/opencv/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -# OpenCV - -set(OPENCV_TARGET 3rdparty_dummy_lib PARENT_SCOPE) - -if (USE_SYSTEM_OPENCV) - message(STATUS "RPCS3: using system OpenCV") - find_package(OpenCV COMPONENTS core imgproc) - - if(OPENCV_FOUND) - message(STATUS "RPCS3: found system OpenCV") - include_directories(${OpenCV_INCLUDE_DIRS}) - add_library(3rdparty_opencv INTERFACE) - target_link_libraries(3rdparty_opencv INTERFACE ${OpenCV_LIBS}) - target_compile_definitions(3rdparty_opencv INTERFACE -DHAVE_OPENCV) - set(OPENCV_TARGET 3rdparty_opencv PARENT_SCOPE) - else() - message(WARNING "RPCS3: OpenCV not found. Building without OpenCV support") - endif() -else() - message(WARNING "RPCS3: Building without OpenCV support") -endif() diff --git a/3rdparty/opencv/opencv b/3rdparty/opencv/opencv deleted file mode 160000 index d4dde8afe3..0000000000 --- a/3rdparty/opencv/opencv +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d4dde8afe3098f33f253278e25d45d6554afb10c diff --git a/CMakeLists.txt b/CMakeLists.txt index 179e835b3e..3b3c9a0968 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,7 +51,6 @@ option(USE_SYSTEM_LIBUSB "Prefer system libusb instead of the builtin one" OFF) option(USE_SYSTEM_MINIUPNPC "Prefer system MiniUPnPc instead of the builtin one" OFF) option(USE_SYSTEM_MVK "Prefer system MoltenVK instead of the builtin one" OFF) option(USE_SYSTEM_OPENAL "Prefer system OpenAL instead of the prebuild one" ${USE_SYSTEM_OPENAL_DEFAULT}) -option(USE_SYSTEM_OPENCV "Prefer system OpenCV instead of the builtin one" ON) option(USE_SYSTEM_PUGIXML "Prefer system pugixml instead of the builtin one" OFF) option(USE_SYSTEM_RTMIDI "Prefer system RtMidi instead of the builtin one" OFF) option(USE_SYSTEM_SDL "Prefer system SDL instead of the builtin one" ON) diff --git a/CMakePresets.json b/CMakePresets.json index a81db6227d..f2ad04077c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -13,7 +13,6 @@ "USE_SYSTEM_CURL": "OFF", "USE_SYSTEM_ZLIB": "OFF", "USE_SYSTEM_LIBPNG": "OFF", - "USE_SYSTEM_OPENCV": "ON", "BUILD_LLVM": "OFF", "STATIC_LINK_LLVM": "ON" } @@ -30,7 +29,6 @@ "USE_SYSTEM_CURL": "OFF", "USE_SYSTEM_ZLIB": "OFF", "USE_SYSTEM_LIBPNG": "OFF", - "USE_SYSTEM_OPENCV": "ON", "LLVM_ENABLE_LIBCXX": "ON", "BUILD_LLVM": "OFF", "STATIC_LINK_LLVM": "ON" @@ -58,7 +56,6 @@ "USE_SYSTEM_CURL": "OFF", "USE_SYSTEM_ZLIB": "OFF", "USE_SYSTEM_OPENAL": "OFF", - "USE_SYSTEM_OPENCV": "OFF", "BUILD_LLVM": "ON", "STATIC_LINK_LLVM": "ON" }, diff --git a/buildfiles/msvc/rpcs3_default.props b/buildfiles/msvc/rpcs3_default.props index a7d830c5df..6199b446f4 100644 --- a/buildfiles/msvc/rpcs3_default.props +++ b/buildfiles/msvc/rpcs3_default.props @@ -9,8 +9,6 @@ $(SolutionDir)build\tmp\$(ProjectName)-$(Configuration)-$(Platform)\ $(SolutionDir)packages\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.1.8.1.8\build\native\Microsoft.googletest.v140.windesktop.msvcstl.static.rt-static.targets true - $(SolutionDir)3rdparty\opencv\opencv\opencv414\build - opencv_world4140 diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index 852bf4fdf3..5073d6c649 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -67,7 +67,6 @@ if (NOT ANDROID) 3rdparty::wolfssl 3rdparty::libcurl 3rdparty::zlib - 3rdparty::opencv 3rdparty::fusion) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") @@ -184,6 +183,7 @@ if(BUILD_RPCS3_TESTS) tests/test.cpp tests/test_address_range.cpp tests/test_bit_set.cpp + tests/test_blob_detector.cpp tests/test_cctype.cpp tests/test_dmux_pamf.cpp tests/test_fmt.cpp diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index d32bbcc4c5..70da44545e 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -1707,8 +1707,8 @@ public: m_tracker.set_active(gem_num, controller.enabled_tracking && controller.status == CELL_GEM_STATUS_READY); m_tracker.set_hue(gem_num, g_cfg.io.allow_move_hue_set_by_game ? controller.hue : config->hue); - m_tracker.set_hue_threshold(gem_num, config->hue_threshold); - m_tracker.set_saturation_threshold(gem_num, config->saturation_threshold); + m_tracker.set_hue_threshold(gem_num, static_cast(config->hue_threshold.get())); + m_tracker.set_saturation_threshold(gem_num, static_cast(config->saturation_threshold.get())); } m_tracker.set_min_radius(static_cast(g_cfg_move.min_radius) / 100.0f); diff --git a/rpcs3/Input/blob_detector.cpp b/rpcs3/Input/blob_detector.cpp new file mode 100644 index 0000000000..0bd49bdbc3 --- /dev/null +++ b/rpcs3/Input/blob_detector.cpp @@ -0,0 +1,941 @@ +#include "stdafx.h" +#include "blob_detector.h" + +#include +#include +#include +#include +#include + +f32 blob_detector::blob::fill_ratio() const +{ + if (!area) + { + return 0.0f; + } + + // The radius goes through the centers of the boundary pixels, so we add half a pixel to cover the whole pixels. + const f32 r = radius + 0.5f; + return area / (std::numbers::pi_v * r * r); +} + +f32 blob_detector::blob::ellipse_fill_ratio() const +{ + if (!area || major_axis <= 0.0f || minor_axis <= 0.0f) + { + return 0.0f; + } + + return area / (std::numbers::pi_v * major_axis * minor_axis); +} + +u32 blob_detector::find_root(u32 label) +{ + while (m_parents[label] != label) + { + // Path halving + m_parents[label] = m_parents[m_parents[label]]; + label = m_parents[label]; + } + + return label; +} + +void blob_detector::unite(u32 a, u32 b) +{ + const u32 root_a = find_root(a); + const u32 root_b = find_root(b); + + if (root_a == root_b) + { + return; + } + + // Keep the smaller label as root + if (root_a < root_b) + { + m_parents[root_b] = root_a; + } + else + { + m_parents[root_a] = root_b; + } +} + +void blob_detector::fill_holes() +{ + // Background regions are 4-connected (the complement of the 8-connected foreground). + constexpr u8 outside = 2; // Background that is connected to the image border + constexpr u8 visited = 3; // Background that is part of a hole + + const u32 width = m_width; + const u32 height = m_height; + const u32 size = width * height; + + // Flood fill the background from the image border + m_stack.clear(); + + const auto push_outside = [this](u32 index) + { + if (m_mask[index] == 0) + { + m_mask[index] = outside; + m_stack.push_back(index); + } + }; + + for (u32 x = 0; x < width; x++) + { + push_outside(x); + push_outside((height - 1) * width + x); + } + + for (u32 y = 0; y < height; y++) + { + push_outside(y * width); + push_outside(y * width + width - 1); + } + + while (!m_stack.empty()) + { + const u32 index = m_stack.back(); + m_stack.pop_back(); + + const u32 x = index % width; + const u32 y = index / width; + + if (x > 0) push_outside(index - 1); + if (x + 1 < width) push_outside(index + 1); + if (y > 0) push_outside(index - width); + if (y + 1 < height) push_outside(index + width); + } + + // All remaining background pixels are part of a hole. + // Only fill holes that are enclosed by a single blob. A hole that touches multiple blobs is the gap between an outer blob and the blobs inside of it. + // Also skip holes that are larger than their blob. Those are usually large areas that are enclosed by a thin outline and not a ring. + for (u32 start = 0; start < size; start++) + { + if (m_mask[start] != 0) + { + continue; + } + + u32 hole_label = 0; + bool single_blob = true; + + m_region.clear(); + m_stack.clear(); + m_mask[start] = visited; + m_stack.push_back(start); + + while (!m_stack.empty()) + { + const u32 index = m_stack.back(); + m_stack.pop_back(); + m_region.push_back(index); + + const auto visit = [&](u32 neighbor) + { + const u8 value = m_mask[neighbor]; + + if (value == 0) + { + m_mask[neighbor] = visited; + m_stack.push_back(neighbor); + } + else if (value == 1) + { + const u32 label = m_labels[neighbor]; + + if (!hole_label) + { + hole_label = label; + } + else if (label != hole_label) + { + single_blob = false; + } + } + }; + + // Holes can't touch the image border, so all neighbors exist + visit(index - 1); + visit(index + 1); + visit(index - width); + visit(index + width); + } + + if (single_blob && hole_label && m_region.size() <= m_areas[hole_label - 1]) + { + for (u32 index : m_region) + { + m_labels[index] = hole_label; + } + } + } + + // Filled holes are foreground now + for (u32 i = 0; i < size; i++) + { + m_mask[i] = m_labels[i] ? 1 : 0; + } +} + +// Calculates the squared euclidean distance transform of one row or column in place (Felzenszwalb and Huttenlocher). +// v needs n entries, z and d need n + 1 entries. +static void distance_transform_1d(f32* f, u32 n, s32* v, f32* z, f32* d) +{ + u32 k = 0; + v[0] = 0; + z[0] = -std::numeric_limits::infinity(); + z[1] = std::numeric_limits::infinity(); + + // Intersection of the parabolas at q and p + const auto intersection = [f](s32 q, s32 p) + { + return ((f[q] + static_cast(q * q)) - (f[p] + static_cast(p * p))) / static_cast(2 * (q - p)); + }; + + for (s32 q = 1; q < static_cast(n); q++) + { + f32 s = intersection(q, v[k]); + + while (s <= z[k]) + { + k--; + s = intersection(q, v[k]); + } + + k++; + v[k] = q; + z[k] = s; + z[k + 1] = std::numeric_limits::infinity(); + } + + k = 0; + + for (s32 q = 0; q < static_cast(n); q++) + { + while (z[k + 1] < q) + { + k++; + } + + const f32 dq = static_cast(q - v[k]); + d[q] = dq * dq + f[v[k]]; + } + + std::copy(d, d + n, f); +} + +blob_detector::inscribed_circle blob_detector::find_inscribed_circle(const blob& b) +{ + inscribed_circle result {}; + + if (!b.label || b.label > m_blobs.size() || b.min_x > b.max_x || b.min_y > b.max_y || b.max_x >= m_width || b.max_y >= m_height) + { + return result; + } + + // The center of the largest inscribed circle is the pixel with the largest distance to the background. + // Calculate the exact distance transform of the bounding box with a border of 1 pixel. + // Every pixel that doesn't belong to the blob counts as background, so other blobs in the bounding box are ignored. + const u32 w = b.max_x - b.min_x + 3; + const u32 h = b.max_y - b.min_y + 3; + + // Larger than any real squared distance, but small enough to keep the calculations precise + const f32 far = static_cast(u64{w + h} * (w + h)); + + m_distances.resize(usz{w} * h); + + for (u32 y = 0; y < h; y++) + { + for (u32 x = 0; x < w; x++) + { + const bool inside = x > 0 && y > 0 && x + 1 < w && y + 1 < h && + m_labels[(b.min_y + y - 1) * m_width + (b.min_x + x - 1)] == b.label; + + m_distances[y * w + x] = inside ? far : 0.0f; + } + } + + const u32 n = std::max(w, h); + m_line.resize(n * 2 + 1); + m_parabolas.resize(n); + m_bounds.resize(n + 1); + + f32* line = m_line.data(); + f32* temp = line + n; + + // Columns + for (u32 x = 0; x < w; x++) + { + for (u32 y = 0; y < h; y++) line[y] = m_distances[y * w + x]; + distance_transform_1d(line, h, m_parabolas.data(), m_bounds.data(), temp); + for (u32 y = 0; y < h; y++) m_distances[y * w + x] = line[y]; + } + + // Rows + f32 max_distance = 0.0f; + u32 max_x = 0; + u32 max_y = 0; + + for (u32 y = 0; y < h; y++) + { + f32* row = &m_distances[y * w]; + distance_transform_1d(row, w, m_parabolas.data(), m_bounds.data(), temp); + + for (u32 x = 0; x < w; x++) + { + if (row[x] > max_distance) + { + max_distance = row[x]; + max_x = x; + max_y = y; + } + } + } + + if (max_distance <= 0.0f) + { + return result; + } + + result.center_x = static_cast(b.min_x + max_x - 1); + result.center_y = static_cast(b.min_y + max_y - 1); + + // The distance goes to the center of the closest background pixel. Subtract half a pixel to get a radius through the boundary pixel centers. + result.radius = std::max(0.0f, std::sqrt(max_distance) - 0.5f); + + // Find the largest distance outside of the inscribed circle. This is small if the rest of the blob is thin compared to the circle. + const f32 outer_radius = std::sqrt(max_distance) + 1.0f; + const f32 outer_radius_squared = outer_radius * outer_radius; + f32 max_remainder_distance = 0.0f; + + for (u32 y = 0; y < h; y++) + { + const f32* row = &m_distances[y * w]; + const f32 dy = static_cast(y) - static_cast(max_y); + + for (u32 x = 0; x < w; x++) + { + const f32 dx = static_cast(x) - static_cast(max_x); + + if (row[x] > max_remainder_distance && dx * dx + dy * dy > outer_radius_squared) + { + max_remainder_distance = row[x]; + } + } + } + + result.remainder_radius = max_remainder_distance > 0.0f ? std::max(0.0f, std::sqrt(max_remainder_distance) - 0.5f) : 0.0f; + + return result; +} + +f32 blob_detector::free_outline_ratio(const blob& b, f32 center_x, f32 center_y, f32 radius) const +{ + constexpr u32 sample_count = 64; + const f32 r = radius + 2.0f; + const s32 width = static_cast(m_width); + const s32 height = static_cast(m_height); + + u32 free_count = 0; + + for (u32 i = 0; i < sample_count; i++) + { + const f32 angle = i * (2.0f * std::numbers::pi_v / sample_count); + const s32 x = static_cast(std::lround(center_x + r * std::cos(angle))); + const s32 y = static_cast(std::lround(center_y + r * std::sin(angle))); + + if (x < 0 || y < 0 || x >= width || y >= height || m_labels[y * width + x] != b.label) + { + free_count++; + } + } + + return free_count / static_cast(sample_count); +} + +bool blob_detector::is_round_bulge(const blob& b, const inscribed_circle& circle) const +{ + if (circle.radius <= 0.0f) + { + return false; + } + + return circle.remainder_radius < circle.radius * 0.6f && + free_outline_ratio(b, circle.center_x, circle.center_y, circle.radius) >= 0.6f; +} + +bool blob_detector::fit_capsule(const blob& b, const inscribed_circle& circle, capsule& result) +{ + const f32 radius = circle.radius; + + if (b.minor_axis <= 0.0f || radius <= 0.0f) + { + return false; + } + + // Clearly elongated, but not a line + const f32 elongation = b.major_axis / b.minor_axis; + if (elongation < 1.2f || elongation > 5.0f) + { + return false; + } + + // Capsules have about the same area as their moment ellipse (0.97 to 1.0). Irregular shapes have less. + if (b.ellipse_fill_ratio() < 0.85f) + { + return false; + } + + // The minor axis of a capsule is 1.0 to 1.15 times its half width + if (b.minor_axis < radius * 0.9f || b.minor_axis > radius * 1.25f) + { + return false; + } + + result.center_x = b.centroid_x; + result.center_y = b.centroid_y; + result.radius = radius; + result.half_length = std::max(0.0f, b.radius - radius); // The enclosing radius of a capsule is half its length plus its radius + result.dir_x = std::cos(b.angle); + result.dir_y = std::sin(b.angle); + return true; +} + +// 3x3 erosion (min) or dilation (max) in two separable passes. +// Pixels outside of the image are ignored, so blobs that touch the image border don't shrink. +template +static void morphology_3x3(u8* image, u8* temp, u32 width, u32 height) +{ + const auto op = [](u8 a, u8 b) -> u8 + { + if constexpr (Erode) return a < b ? a : b; + else return a > b ? a : b; + }; + + // Horizontal pass + for (u32 y = 0; y < height; y++) + { + const u8* src = image + y * width; + u8* dst = temp + y * width; + + if (width == 1) + { + dst[0] = src[0]; + continue; + } + + dst[0] = op(src[0], src[1]); + + for (u32 x = 1; x + 1 < width; x++) + { + dst[x] = op(op(src[x - 1], src[x]), src[x + 1]); + } + + dst[width - 1] = op(src[width - 2], src[width - 1]); + } + + // Vertical pass. Missing rows at the border are replaced by the current row. + for (u32 y = 0; y < height; y++) + { + const u8* above = temp + (y > 0 ? y - 1 : y) * width; + const u8* center = temp + y * width; + const u8* below = temp + (y + 1 < height ? y + 1 : y) * width; + u8* dst = image + y * width; + + for (u32 x = 0; x < width; x++) + { + dst[x] = op(op(above[x], center[x]), below[x]); + } + } +} + +void blob_detector::open(std::vector& binary, u32 width, u32 height) +{ + const usz size = usz{width} * height; + + if (binary.size() != size || !size || size > u32{umax}) + { + return; + } + + m_temp.resize(size); + + // Normalize the foreground to 255, so we can use min and max + for (u8& value : binary) + { + value = value ? 255 : 0; + } + + morphology_3x3(binary.data(), m_temp.data(), width, height); + morphology_3x3(binary.data(), m_temp.data(), width, height); +} + +void blob_detector::detect(const std::vector& binary, u32 width, u32 height, bool fill_holes, u32 min_circle_area) +{ + m_blobs.clear(); + m_width = 0; + m_height = 0; + + const usz size = usz{width} * height; + + if (binary.size() != size || !size || size > u32{umax}) + { + m_mask.clear(); + m_labels.clear(); + return; + } + + m_width = width; + m_height = height; + + m_mask.resize(size); + m_labels.assign(size, 0); + + for (usz i = 0; i < size; i++) + { + m_mask[i] = binary[i] ? 1 : 0; + } + + // First pass: assign provisional labels and record which ones are connected (8-connectivity) + m_parents.clear(); + m_parents.push_back(0); // Label 0 is the background + + for (u32 y = 0; y < height; y++) + { + for (u32 x = 0; x < width; x++) + { + const u32 index = y * width + x; + + if (!m_mask[index]) + { + continue; + } + + u32 label = 0; + + const auto check_neighbor = [&](u32 neighbor) + { + const u32 neighbor_label = m_labels[neighbor]; + + if (!neighbor_label) + { + return; + } + + if (!label) + { + label = neighbor_label; + } + else if (neighbor_label != label) + { + unite(label, neighbor_label); + } + }; + + // Check the neighbors that were already visited + if (x > 0) + { + check_neighbor(index - 1); + } + + if (y > 0) + { + const u32 above = index - width; + + if (x > 0) check_neighbor(above - 1); + check_neighbor(above); + if (x + 1 < width) check_neighbor(above + 1); + } + + if (!label) + { + label = ::size32(m_parents); + m_parents.push_back(label); + } + + m_labels[index] = label; + } + } + + // Second pass: resolve the final labels in raster order + m_compact_labels.assign(m_parents.size(), 0); + u32 blob_count = 0; + m_areas.clear(); + + for (u32& label : m_labels) + { + if (!label) + { + continue; + } + + u32& final_label = m_compact_labels[find_root(label)]; + + if (!final_label) + { + final_label = ++blob_count; + m_areas.push_back(0); + } + + label = final_label; + m_areas[final_label - 1]++; + } + + if (fill_holes) + { + this->fill_holes(); + } + + // Third pass: collect the blob data + struct sums + { + u64 x = 0; + u64 y = 0; + u64 xx = 0; + u64 yy = 0; + u64 xy = 0; + }; + std::vector blob_sums(blob_count); + + m_blobs.resize(blob_count); + + if (m_boundaries.size() < blob_count) + { + m_boundaries.resize(blob_count); + } + + for (u32 i = 0; i < blob_count; i++) + { + m_blobs[i].label = i + 1; + m_boundaries[i].clear(); + } + + for (u32 y = 0; y < height; y++) + { + for (u32 x = 0; x < width; x++) + { + const u32 index = y * width + x; + const u32 label = m_labels[index]; + + if (!label) + { + continue; + } + + blob& b = m_blobs[label - 1]; + + if (!b.area) + { + b.min_x = x; + b.min_y = y; + b.max_x = x; + } + + b.area++; + b.min_x = std::min(b.min_x, x); + b.max_x = std::max(b.max_x, x); + b.max_y = y; + + sums& s = blob_sums[label - 1]; + s.x += x; + s.y += y; + s.xx += u64{x} * x; + s.yy += u64{y} * y; + s.xy += u64{x} * y; + + // A pixel is part of the boundary if it touches the image border or the background (4-connectivity) + const bool is_boundary = + x == 0 || y == 0 || x + 1 == width || y + 1 == height || + !m_mask[index - 1] || !m_mask[index + 1] || + !m_mask[index - width] || !m_mask[index + width]; + + if (is_boundary) + { + m_boundaries[label - 1].push_back(point{ static_cast(x), static_cast(y) }); + } + } + } + + for (usz i = 0; i < m_blobs.size(); i++) + { + blob& b = m_blobs[i]; + const sums& s = blob_sums[i]; + std::vector& boundary = m_boundaries[i]; + + const f64 area = b.area; + const f64 centroid_x = s.x / area; + const f64 centroid_y = s.y / area; + + b.centroid_x = static_cast(centroid_x); + b.centroid_y = static_cast(centroid_y); + b.perimeter = ::size32(boundary); + + // Fit an ellipse with the same second order central moments. The variance along an axis of a filled ellipse is (semi-axis)^2 / 4. + const f64 mu20 = s.xx / area - centroid_x * centroid_x; + const f64 mu02 = s.yy / area - centroid_y * centroid_y; + const f64 mu11 = s.xy / area - centroid_x * centroid_y; + const f64 mean = (mu20 + mu02) / 2.0; + const f64 half_diff = (mu20 - mu02) / 2.0; + const f64 delta = std::sqrt(half_diff * half_diff + mu11 * mu11); + + b.major_axis = static_cast(2.0 * std::sqrt(std::max(0.0, mean + delta))); + b.minor_axis = static_cast(2.0 * std::sqrt(std::max(0.0, mean - delta))); + b.angle = static_cast(0.5 * std::atan2(2.0 * mu11, mu20 - mu02)); + + if (b.area < min_circle_area) + { + // Skip the expensive part for small blobs that will be ignored anyway + b.center_x = b.centroid_x; + b.center_y = b.centroid_y; + b.radius = 0.0f; + continue; + } + + min_enclosing_circle(boundary, b.center_x, b.center_y, b.radius); + } +} + +void blob_detector::fill_blob(std::vector& binary, const blob& b, u8 value) const +{ + const usz size = usz{m_width} * m_height; + + if (binary.size() != size || !b.label || b.max_x >= m_width || b.max_y >= m_height) + { + return; + } + + for (u32 y = b.min_y; y <= b.max_y; y++) + { + for (u32 x = b.min_x; x <= b.max_x; x++) + { + const u32 index = y * m_width + x; + + if (m_labels[index] == b.label) + { + binary[index] = value; + } + } + } +} + +void blob_detector::fill_blob_rgba(std::vector& rgba, const blob& b, const std::array& color) const +{ + const usz size = usz{m_width} * m_height * 4u; + + if (rgba.size() != size || !b.label || b.max_x >= m_width || b.max_y >= m_height) + { + return; + } + + for (u32 y = b.min_y; y <= b.max_y; y++) + { + for (u32 x = b.min_x; x <= b.max_x; x++) + { + const u32 index = y * m_width + x; + + if (m_labels[index] == b.label) + { + std::copy(color.begin(), color.end(), &rgba[index * 4]); + } + } + } +} + +void blob_detector::min_enclosing_circle(std::vector& points, f32& center_x, f32& center_y, f32& radius) +{ + center_x = 0.0f; + center_y = 0.0f; + radius = 0.0f; + + if (points.empty()) + { + return; + } + + struct circle + { + f64 x = 0.0; + f64 y = 0.0; + f64 r2 = 0.0; // Squared radius + }; + + const auto contains = [](const circle& c, const point& p) + { + const f64 dx = p.x - c.x; + const f64 dy = p.y - c.y; + return dx * dx + dy * dy <= c.r2 * (1.0 + 1e-10) + 1e-7; + }; + + const auto circle_from_2 = [](const point& a, const point& b) + { + const f64 x = (a.x + b.x) / 2.0; + const f64 y = (a.y + b.y) / 2.0; + const f64 dx = a.x - x; + const f64 dy = a.y - y; + return circle{ x, y, dx * dx + dy * dy }; + }; + + const auto circle_from_3 = [&](const point& a, const point& b, const point& c) + { + const f64 ax = a.x, ay = a.y; + const f64 bx = b.x, by = b.y; + const f64 cx = c.x, cy = c.y; + const f64 d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + + if (std::abs(d) < 1e-12) + { + // Collinear points: use the two points that are furthest apart + const circle ab = circle_from_2(a, b); + const circle ac = circle_from_2(a, c); + const circle bc = circle_from_2(b, c); + return std::max({ ab, ac, bc }, [](const circle& l, const circle& r) { return l.r2 < r.r2; }); + } + + const f64 a2 = ax * ax + ay * ay; + const f64 b2 = bx * bx + by * by; + const f64 c2 = cx * cx + cy * cy; + const f64 x = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d; + const f64 y = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d; + const f64 dx = ax - x; + const f64 dy = ay - y; + return circle{ x, y, dx * dx + dy * dy }; + }; + + // Randomize the order to get the expected linear runtime. Use a fixed seed to get reproducible results. + std::mt19937 rng(static_cast(points.size())); + std::shuffle(points.begin(), points.end(), rng); + + circle c{ static_cast(points[0].x), static_cast(points[0].y), 0.0 }; + + for (usz i = 1; i < points.size(); i++) + { + if (contains(c, points[i])) + { + continue; + } + + // points[i] has to be on the boundary + c = circle{ static_cast(points[i].x), static_cast(points[i].y), 0.0 }; + + for (usz j = 0; j < i; j++) + { + if (contains(c, points[j])) + { + continue; + } + + // points[i] and points[j] have to be on the boundary + c = circle_from_2(points[i], points[j]); + + for (usz k = 0; k < j; k++) + { + if (!contains(c, points[k])) + { + c = circle_from_3(points[i], points[j], points[k]); + } + } + } + } + + center_x = static_cast(c.x); + center_y = static_cast(c.y); + radius = static_cast(std::sqrt(c.r2)); +} + +namespace rgba_draw +{ + static inline void set_pixel(std::vector& rgba, u32 width, u32 height, s32 x, s32 y, const std::array& color) + { + if (x < 0 || y < 0 || static_cast(x) >= width || static_cast(y) >= height) + { + return; + } + + std::copy(color.begin(), color.end(), &rgba[(static_cast(y) * width + x) * 4]); + } + + void circle(std::vector& rgba, u32 width, u32 height, f32 center_x, f32 center_y, s32 radius, const std::array& color, bool filled) + { + const usz size = usz{width} * height * 4u; + + if (rgba.empty() || rgba.size() != size || radius < 0) + { + return; + } + + const s32 cx = static_cast(std::lround(center_x)); + const s32 cy = static_cast(std::lround(center_y)); + + // Outline: all pixels within half a pixel of the radius + const f32 outer = radius + 0.5f; + const f32 inner = std::max(0.0f, radius - 0.5f); + const f32 outer2 = outer * outer; + const f32 inner2 = inner * inner; + const s32 r2 = radius * radius; + + const s32 y_begin = std::max(0, cy - radius - 1); + const s32 y_end = std::min(static_cast(height) - 1, cy + radius + 1); + const s32 x_begin = std::max(0, cx - radius - 1); + const s32 x_end = std::min(static_cast(width) - 1, cx + radius + 1); + + for (s32 y = y_begin; y <= y_end; y++) + { + for (s32 x = x_begin; x <= x_end; x++) + { + const s32 dx = x - cx; + const s32 dy = y - cy; + const s32 d2 = dx * dx + dy * dy; + + if (filled ? (d2 <= r2) : (d2 >= inner2 && d2 < outer2)) + { + set_pixel(rgba, width, height, x, y, color); + } + } + } + } + + void line(std::vector& rgba, u32 width, u32 height, s32 x0, s32 y0, s32 x1, s32 y1, const std::array& color) + { + const usz size = usz{width} * height * 4u; + + if (rgba.empty() || rgba.size() != size) + { + return; + } + + // Bresenham + const s32 dx = std::abs(x1 - x0); + const s32 dy = -std::abs(y1 - y0); + const s32 sx = x0 < x1 ? 1 : -1; + const s32 sy = y0 < y1 ? 1 : -1; + s32 err = dx + dy; + + while (true) + { + set_pixel(rgba, width, height, x0, y0, color); + + if (x0 == x1 && y0 == y1) + { + break; + } + + const s32 e2 = 2 * err; + + if (e2 >= dy) + { + err += dy; + x0 += sx; + } + + if (e2 <= dx) + { + err += dx; + y0 += sy; + } + } + } +} diff --git a/rpcs3/Input/blob_detector.h b/rpcs3/Input/blob_detector.h new file mode 100644 index 0000000000..07d448e685 --- /dev/null +++ b/rpcs3/Input/blob_detector.h @@ -0,0 +1,131 @@ +#pragma once + +#include "util/types.hpp" + +#include +#include + +// Finds connected blobs in binary images. +class blob_detector +{ +public: + struct point + { + s32 x = 0; + s32 y = 0; + }; + + struct blob + { + u32 label = 0; // Label of the blob in labels() (1-based) + u32 area = 0; // Number of pixels (including filled holes) + u32 perimeter = 0; // Number of boundary pixels + u32 min_x = 0; // Bounding box + u32 min_y = 0; + u32 max_x = 0; + u32 max_y = 0; + f32 centroid_x = 0.0f; // Center of mass + f32 centroid_y = 0.0f; + f32 center_x = 0.0f; // Center of the minimum enclosing circle + f32 center_y = 0.0f; + f32 radius = 0.0f; // Radius of the minimum enclosing circle (through the boundary pixel centers) + f32 major_axis = 0.0f; // Semi-axes of the ellipse with the same second order moments as the blob + f32 minor_axis = 0.0f; + f32 angle = 0.0f; // Angle of the major axis in radians (-pi/2 to pi/2, y points down) + + // Ratio between the area of the blob and the area of its enclosing circle. + // About 1.0 for a disk, 0.64 for a square and 0.5 for a half disk or a 2:1 ellipse. + f32 fill_ratio() const; + + // Ratio between the area of the blob and the area of its moment ellipse. + // About 1.0 for ellipses and capsules, 0.95 for rectangles and lower for irregular shapes. + f32 ellipse_fill_ratio() const; + }; + + struct inscribed_circle + { + f32 center_x = 0.0f; // Center of the largest circle that fits inside the blob + f32 center_y = 0.0f; + f32 radius = 0.0f; // Radius of the largest inscribed circle (through the boundary pixel centers) + f32 remainder_radius = 0.0f; // Radius of the largest inscribed circle whose center lies outside of the largest inscribed circle + }; + + struct capsule + { + f32 center_x = 0.0f; // Center of the capsule (centroid of the blob) + f32 center_y = 0.0f; + f32 radius = 0.0f; // Half the width of the capsule + f32 half_length = 0.0f; // Half the length of the straight part + f32 dir_x = 1.0f; // Direction of the long axis + f32 dir_y = 0.0f; + }; + + // Finds all 8-connected blobs in a binary image (0 = background, anything else = foreground). + // If fill_holes is true, background regions that are completely enclosed by a single blob are added to that blob, + // unless the hole is larger than the blob itself (e.g. a large area that is only enclosed by a thin outline). + // Holes that touch more than one blob (e.g. the gap between a frame and a blob inside of it) are not filled. + // The enclosing circle is only calculated for blobs with at least min_circle_area pixels. Smaller blobs get radius 0. + void detect(const std::vector& binary, u32 width, u32 height, bool fill_holes, u32 min_circle_area = 0); + + const std::vector& blobs() const { return m_blobs; } + const std::vector& labels() const { return m_labels; } // One label per pixel (0 = background) + u32 width() const { return m_width; } + u32 height() const { return m_height; } + + // Sets all pixels of the blob to value. The image must have the same size as the one passed to detect(). + void fill_blob(std::vector& binary, const blob& b, u8 value) const; + + // Sets all pixels of the blob to color. The image must have the same size as the one passed to detect(). + void fill_blob_rgba(std::vector& rgba, const blob& b, const std::array& color) const; + + // Calculates the largest circle that fits inside the blob. The blob has to be one of the blobs found by the last call to detect(). + // This is relatively expensive for large blobs, so it is only done on demand. + inscribed_circle find_inscribed_circle(const blob& b); + + // Returns the fraction of a circle outline (slightly outside of the given circle) that doesn't belong to the blob. + f32 free_outline_ratio(const blob& b, f32 center_x, f32 center_y, f32 radius) const; + + // Checks if the inscribed circle is a round bulge of the blob, e.g. a disk that touches a thinner object. + // The rest of the blob has to be clearly thinner than the circle and most of the circle outline has to be free. + bool is_round_bulge(const blob& b, const inscribed_circle& circle) const; + + // Checks if the blob looks like a capsule (an elongated, smooth shape), e.g. a disk that is blurred by movement. + // circle has to be the inscribed circle of the blob. Rectangles can't be distinguished from capsules. + static bool fit_capsule(const blob& b, const inscribed_circle& circle, capsule& result); + + // Morphological opening with a 3x3 square (erode, then dilate). Removes noise and thin lines and splits blobs that are connected by thin bridges. + void open(std::vector& binary, u32 width, u32 height); + + // Calculates the smallest circle that contains all points (Welzl's algorithm). The points will be reordered. + static void min_enclosing_circle(std::vector& points, f32& center_x, f32& center_y, f32& radius); + +private: + void fill_holes(); + u32 find_root(u32 label); + void unite(u32 a, u32 b); + + u32 m_width = 0; + u32 m_height = 0; + + std::vector m_mask; // 1 = foreground + std::vector m_labels; // Label per pixel + std::vector m_parents; // Union-find parents of the provisional labels + std::vector m_compact_labels; // Provisional root label -> final label + std::vector m_stack; // Pixel indices for the flood fill + std::vector m_region; // Pixel indices of the current hole + std::vector m_areas; // Area of each blob before the holes are filled + std::vector m_temp; // Temporary image for the morphology + std::vector m_distances; // Squared distances for the distance transform + std::vector m_line; // One row or column of the distance transform + std::vector m_parabolas; // Parabola positions for the distance transform + std::vector m_bounds; // Parabola boundaries for the distance transform + std::vector> m_boundaries; // Boundary pixels of each blob + std::vector m_blobs; +}; + +// Simple drawing functions for RGBA images (4 bytes per pixel, no row padding) +namespace rgba_draw +{ + void circle(std::vector& rgba, u32 width, u32 height, f32 center_x, f32 center_y, s32 radius, const std::array& color, bool filled); + void line(std::vector& rgba, u32 width, u32 height, s32 x0, s32 y0, s32 x1, s32 y1, const std::array& color); +} diff --git a/rpcs3/Input/ps_move_tracker.cpp b/rpcs3/Input/ps_move_tracker.cpp index e933151568..26bd2665bd 100644 --- a/rpcs3/Input/ps_move_tracker.cpp +++ b/rpcs3/Input/ps_move_tracker.cpp @@ -4,14 +4,7 @@ #include "ps_move_tracker.h" #include - -#ifdef HAVE_OPENCV -#include -#include -#if CV_VERSION_MAJOR >= 5 -#include -#endif -#endif +#include LOG_CHANNEL(ps_move); @@ -112,7 +105,7 @@ void ps_move_tracker::set_hue(u32 index, u16 hue) } template -void ps_move_tracker::set_hue_threshold(u32 index, u16 threshold) +void ps_move_tracker::set_hue_threshold(u32 index, u8 threshold) { ps_move_config& config = ::at32(m_config, index); config.hue_threshold = threshold; @@ -120,11 +113,10 @@ void ps_move_tracker::set_hue_threshold(u32 index, u16 thres } template -void ps_move_tracker::set_saturation_threshold(u32 index, u16 threshold) +void ps_move_tracker::set_saturation_threshold(u32 index, u8 threshold) { ps_move_config& config = ::at32(m_config, index); config.saturation_threshold_u8 = threshold; - config.calculate_values(); } template @@ -309,24 +301,42 @@ void ps_move_tracker::process_hues() } } -#ifdef HAVE_OPENCV -static bool is_circular_contour(const std::vector& contour, f32& area) +static constexpr u32 min_blob_area = 30; +static constexpr f32 min_sphere_radius = 5.0f; + +static bool is_circular_blob(const blob_detector::blob& b) { - std::vector approx; - cv::approxPolyDP(contour, approx, 0.01 * cv::arcLength(contour, true), true); - if (approx.size() < 8ULL) return false; + // A disk has a fill ratio of about 1. Squares (0.64), half disks and 2:1 ellipses (0.5) are rejected. + constexpr f32 min_fill_ratio = 0.7f; - area = static_cast(cv::contourArea(contour)); - if (area < 30.0f) return false; - - cv::Point2f center; - f32 radius; - cv::minEnclosingCircle(contour, center, radius); - if (radius < 5.0f) return false; + if (b.area < min_blob_area) return false; + if (b.radius < min_sphere_radius) return false; + if (b.fill_ratio() < min_fill_ratio) return false; return true; } +// Checks if the last position is close to the line of movement of a sphere that was blurred into a capsule. +// The sphere usually moves further between two frames than during the exposure, so the last position may be behind the end of the capsule. +static bool is_on_movement_path(const blob_detector::capsule& capsule, f32 last_x, f32 last_y, f32 last_radius) +{ + const f32 dx = last_x - capsule.center_x; + const f32 dy = last_y - capsule.center_y; + const f32 axis_distance = std::abs(dx * capsule.dir_x + dy * capsule.dir_y); + const f32 side_distance = std::abs(dx * capsule.dir_y - dy * capsule.dir_x); + + return side_distance <= last_radius * 2.0f && axis_distance <= capsule.half_length * 4.0f + last_radius * 2.0f; +} + +struct sphere_candidate +{ + const blob_detector::blob* blob = nullptr; + f32 x = 0.0f; + f32 y = 0.0f; + f32 radius = 0.0f; + f32 score = 0.0f; +}; + template void ps_move_tracker::draw_sphere_size_range(f32 result_radius) { @@ -336,25 +346,67 @@ void ps_move_tracker::draw_sphere_size_range(f32 result_radi // Protect the destination image from race conditions std::lock_guard lock(m_mutex); - // Map memory - cv::Mat rgba(cv::Size(m_width, m_height), CV_8UC4, m_image_rgba_contours.data(), 0); - // Draw result, min and max radius const f32 min_radius = m_min_radius * m_width; const f32 max_radius = m_max_radius * m_width; const f32 min_radius_clamped = std::max(0.0f, std::min(min_radius, max_radius)); - const cv::Point2f center = cv::Point2f(m_width - 1 - max_radius, max_radius); + const f32 center_x = m_width - 1 - max_radius; + const f32 center_y = max_radius; if (result_radius > 0.0f) { - cv::circle(rgba, center, static_cast(result_radius), cv::Scalar(255, 0, 0, 255), cv::FILLED); + rgba_draw::circle(m_image_rgba_contours, m_width, m_height, center_x, center_y, static_cast(result_radius), { 255, 0, 0, 255 }, true); } if (min_radius_clamped > 0.0f && min_radius_clamped <= max_radius) { - cv::circle(rgba, center, static_cast(min_radius_clamped), cv::Scalar(0, 0, 0, 255), cv::FILLED); + rgba_draw::circle(m_image_rgba_contours, m_width, m_height, center_x, center_y, static_cast(min_radius_clamped), { 0, 0, 0, 255 }, true); } if (max_radius > min_radius_clamped) { - cv::circle(rgba, center, static_cast(max_radius), cv::Scalar(0, 0, 0, 255), 1); + rgba_draw::circle(m_image_rgba_contours, m_width, m_height, center_x, center_y, static_cast(max_radius), { 0, 0, 0, 255 }, false); + } +} + +template +void ps_move_tracker::filter_image(u32 index) +{ + const u32 width = m_width; + const u32 height = m_height; + + if (width == 0 || height == 0) + { + return; + } + + const ps_move_config& config = ::at32(m_config, index); + const std::vector& image_hsv = m_image_hsv; + std::vector& image_binary = ::at32(m_image_binary, index); + + const bool wrapped_hue = config.min_hue > config.max_hue; // e.g. min=355, max=5 (red) + const bool has_hue_threshold = config.hue_threshold < 180; // with hue threshold 180 min and max are the same value + + for (u32 y = 0; y < height; y++) + { + const u8* src = &image_hsv[y * width * 3]; + u8* dst = &image_binary[y * width]; + + for (u32 x = 0; x < width; x++, src += 3) + { + const u16 hue = src[0] * 2; + const u8 saturation = src[1]; + const u8 value = src[2]; + + // Simply drop dark and colorless pixels as well as pixels that don't match our hue + if ((has_hue_threshold && (wrapped_hue ? (hue < config.min_hue && hue > config.max_hue) : (hue < config.min_hue || hue > config.max_hue))) || + saturation < config.saturation_threshold_u8 || + value < 150) + { + dst[x] = 0; + } + else + { + dst[x] = 255; + } + } } } @@ -370,134 +422,115 @@ void ps_move_tracker::process_contours(ps_move_info& info, u return; } - const ps_move_config& config = ::at32(m_config, index); - const std::vector& image_hsv = m_image_hsv; - std::vector& image_binary = ::at32(m_image_binary, index); - - const bool wrapped_hue = config.min_hue > config.max_hue; // e.g. min=355, max=5 (red) - const bool has_hue_threshold = config.hue_threshold < 180; // with hue threshold 180 min and max are the same value - - // Map memory - cv::Mat binary(cv::Size(width, height), CV_8UC1, image_binary.data(), 0); - // Filter image - for (u32 y = 0; y < height; y++) - { - const u8* src = &image_hsv[y * width * 3]; - u8* dst = &image_binary[y * width]; + filter_image(index); - for (u32 x = 0; x < width; x++, src += 3) - { - const u16 hue = src[0] * 2; - const u8 saturation = src[1]; - const u8 value = src[2]; + std::vector& image_binary = ::at32(m_image_binary, index); + blob_detector& detector = ::at32(m_blob_detectors, index); - // Simply drop dark and colorless pixels as well as pixels that don't match our hue - if ((has_hue_threshold && (wrapped_hue ? (hue < config.min_hue && hue > config.max_hue) : (hue < config.min_hue || hue > config.max_hue))) || - saturation < config.saturation_threshold_u8 || saturation > 200 || - value < 150) - { - dst[x] = 0; - } - else - { - dst[x] = 255; - } - } - } + // Remove noise and thin lines. This also separates the sphere from background objects that only touch it with a thin bridge. + detector.open(image_binary, width, height); - // Remove all small outer contours - if (m_filter_small_contours) - { - std::vector> contours; - cv::findContours(binary, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE); - for (auto it = contours.begin(); it != contours.end();) - { - f32 area; - if (is_circular_contour(*it, area)) - { - it = contours.erase(it); - continue; - } - it++; - } - if (!contours.empty()) - { - cv::drawContours(binary, contours, -1, 0, cv::FILLED); - } - } - - // Find best contour - std::vector> all_contours; - cv::findContours(binary, all_contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); - - if (all_contours.empty()) - { - set_valid(info, index, false); - - if constexpr (DiagnosticsEnabled) - { - draw_sphere_size_range(0.0f); - } - return; - } - - std::vector> contours; - std::vector centers; - std::vector radii; - - if constexpr (DiagnosticsEnabled) - { - contours.reserve(all_contours.size()); - centers.reserve(all_contours.size()); - radii.reserve(all_contours.size()); - } + // Find all blobs. Holes are filled, so overexposed sphere centers don't split the sphere into a ring. + detector.detect(image_binary, width, height, true, min_blob_area); const f32 min_radius = m_min_radius * width; const f32 max_radius = m_max_radius * width; - usz best_index = umax; - f32 best_area = 0.0f; - f32 best_radius = 0.0f; - cv::Point2f best_center {}; + // Prefer candidates near the last known position + const bool has_last_position = info.valid && info.radius > 0.0f; + const f32 last_x = info.x_pos * width; + const f32 last_y = info.y_pos * height; + const f32 search_radius = info.radius * 2.0f; - for (usz i = 0; i < all_contours.size(); i++) + // The inscribed circle has to be similar to the last sphere + const f32 min_inscribed_radius = std::max(min_sphere_radius, info.radius * 0.5f); + const f32 max_inscribed_radius = info.radius * 1.5f; + + std::vector candidates; + sphere_candidate best_candidate {}; + + for (const blob_detector::blob& b : detector.blobs()) { - const std::vector& contour = all_contours[i]; - f32 area; - if (!is_circular_contour(contour, area)) - continue; + sphere_candidate candidate { .blob = &b }; + f32 weight = 1.0f; - // Get center and radius - cv::Point2f center; - f32 radius; - cv::minEnclosingCircle(contour, center, radius); + if (is_circular_blob(b)) + { + candidate.x = b.center_x; + candidate.y = b.center_y; + candidate.radius = b.radius; + } + else if (has_last_position && + b.area >= min_blob_area && + b.max_x - b.min_x + 1 >= 2.0f * min_inscribed_radius && + b.max_y - b.min_y + 1 >= 2.0f * min_inscribed_radius && + b.area >= std::numbers::pi_v * min_inscribed_radius * min_inscribed_radius) + { + // The blob is not round. This is only used to keep tracking a sphere of similar size. A new sphere has to be found as a real circle. + // The cheap checks above make sure that we only calculate the inscribed circle if the blob is large enough to contain it. + const blob_detector::inscribed_circle circle = detector.find_inscribed_circle(b); + const bool similar_size = circle.radius >= min_inscribed_radius && circle.radius <= max_inscribed_radius; + blob_detector::capsule capsule {}; + + if (similar_size && blob_detector::fit_capsule(b, circle, capsule) && is_on_movement_path(capsule, last_x, last_y, info.radius)) + { + // The sphere is blurred by fast movement. The center of the capsule is the average position during the exposure. + candidate.x = capsule.center_x; + candidate.y = capsule.center_y; + candidate.radius = capsule.radius; + weight = 0.75f; + } + else if (similar_size && detector.is_round_bulge(b, circle)) + { + // The blob contains a round bulge. This happens if the sphere touches a background object with a similar color. + // Use the largest circle that fits inside the blob, but prefer real circles. + candidate.x = circle.center_x; + candidate.y = circle.center_y; + candidate.radius = circle.radius; + weight = 0.5f; + } + } + + if (candidate.radius <= 0.0f) + { + // Remove all non-circular blobs from the binary image + if constexpr (DiagnosticsEnabled) + { + if (m_filter_small_contours) + { + detector.fill_blob(image_binary, b, 0); + } + } + continue; + } // Filter radius - if (radius < min_radius || radius > max_radius) + if (candidate.radius < min_radius || candidate.radius > max_radius) continue; + // Larger spheres are more likely to be the real one, but the distance to the last position is more important + candidate.score = weight * candidate.radius * candidate.radius; + + if (has_last_position) + { + const f32 dx = (candidate.x - last_x) / search_radius; + const f32 dy = (candidate.y - last_y) / search_radius; + candidate.score /= 1.0f + dx * dx + dy * dy; + } + if constexpr (DiagnosticsEnabled) { - contours.push_back(std::move(all_contours[i])); - centers.push_back(center); - radii.push_back(radius); + candidates.push_back(candidate); } - if (area > best_area) + if (!best_candidate.blob || candidate.score > best_candidate.score) { - best_area = area; - best_center = center; - best_radius = radius; - - if constexpr (DiagnosticsEnabled) - { - best_index = contours.size() - 1; - } + best_candidate = candidate; } } - if (best_area <= 0.0f) + if (!best_candidate.blob) { set_valid(info, index, false); @@ -509,15 +542,15 @@ void ps_move_tracker::process_contours(ps_move_info& info, u } // Calculate distance from sphere to camera - const f32 sphere_radius_pixels = best_radius; + const f32 sphere_radius_pixels = best_candidate.radius; constexpr f32 focal_length_mm = 3.5f; // Based on common webcam specs constexpr f32 sensor_width_mm = 3.6f; // Based on common webcam specs const f32 image_width_pixels = static_cast(width); const f32 focal_length_pixels = (focal_length_mm * image_width_pixels) / sensor_width_mm; const f32 distance_mm = (focal_length_pixels * CELL_GEM_SPHERE_RADIUS_MM) / sphere_radius_pixels; - const u32 x_pos = std::clamp(static_cast(best_center.x), 0u, width); - const u32 y_pos = std::clamp(static_cast(best_center.y), 0u, height); + const u32 x_pos = std::clamp(static_cast(best_candidate.x), 0u, width); + const u32 y_pos = std::clamp(static_cast(best_candidate.y), 0u, height); // Only set new values if the new shape and position are relatively similar to the old ones. const auto distance_squared = [](int x1, int y1, int x2, int y2) @@ -555,52 +588,38 @@ void ps_move_tracker::process_contours(ps_move_info& info, u // Protect the destination image from race conditions std::lock_guard lock(m_mutex); - // Map memory - cv::Mat rgba(cv::Size(width, height), CV_8UC4, m_image_rgba_contours.data(), 0); - if (!m_show_all_contours) [[likely]] { - std::vector contour = std::move(contours[best_index]); - contours = { std::move(contour) }; - centers = { best_center }; - radii = { best_radius }; + candidates = { best_candidate }; } - static const cv::Scalar contour_color(255, 0, 0, 255); - static const cv::Scalar circle_color(0, 255, 0, 255); - static const cv::Scalar center_color(0, 0, 255, 255); + static constexpr std::array contour_color = { 255, 0, 0, 255 }; + static constexpr std::array circle_color = { 0, 255, 0, 255 }; + static constexpr std::array center_color = { 0, 0, 255, 255 }; // Draw contours if (m_draw_contours) { - cv::drawContours(rgba, contours, -1, contour_color, cv::FILLED); + for (const sphere_candidate& candidate : candidates) + { + detector.fill_blob_rgba(m_image_rgba_contours, *candidate.blob, contour_color); + } } // Draw overlays if (m_draw_overlays) { - for (usz i = 0; i < centers.size(); i++) + for (const sphere_candidate& candidate : candidates) { - const cv::Point2f& center = centers[i]; - const f32 radius = radii[i]; + const s32 x = static_cast(std::lround(candidate.x)); + const s32 y = static_cast(std::lround(candidate.y)); - cv::circle(rgba, center, static_cast(radius), circle_color, 1); - cv::line(rgba, center + cv::Point2f(-2.0f, 0.0f), center + cv::Point2f(2.0f, 0.0f), center_color, 1); - cv::line(rgba, center + cv::Point2f(0.0f, -2.0f), center + cv::Point2f(0.0f, 2.0f), center_color, 1); + rgba_draw::circle(m_image_rgba_contours, width, height, candidate.x, candidate.y, static_cast(candidate.radius), circle_color, false); + rgba_draw::line(m_image_rgba_contours, width, height, x - 2, y, x + 2, y, center_color); + rgba_draw::line(m_image_rgba_contours, width, height, x, y - 2, x, y + 2, center_color); } } } -#else -template -void ps_move_tracker::process_contours(ps_move_info& info, u32 index) -{ - ensure(index < m_config.size()); - - info.valid = false; - - ps_move.error("The tracker is not implemented for this operating system."); -} -#endif template std::tuple ps_move_tracker::hsv_to_rgb(u16 hue, f32 saturation, f32 value) diff --git a/rpcs3/Input/ps_move_tracker.h b/rpcs3/Input/ps_move_tracker.h index e4b3078843..b0595d1049 100644 --- a/rpcs3/Input/ps_move_tracker.h +++ b/rpcs3/Input/ps_move_tracker.h @@ -1,12 +1,7 @@ #pragma once #include "Emu/Cell/Modules/cellGem.h" - -#ifdef HAVE_OPENCV - constexpr bool g_ps_move_tracking_supported = true; -#else - constexpr bool g_ps_move_tracking_supported = false; -#endif +#include "blob_detector.h" struct ps_move_info { @@ -35,8 +30,8 @@ public: void set_active(u32 index, bool active); void set_hue(u32 index, u16 hue); - void set_hue_threshold(u32 index, u16 threshold); - void set_saturation_threshold(u32 index, u16 threshold); + void set_hue_threshold(u32 index, u8 threshold); + void set_saturation_threshold(u32 index, u8 threshold); void set_min_radius(f32 radius) { m_min_radius = radius; } void set_max_radius(f32 radius) { m_max_radius = radius; } @@ -62,8 +57,8 @@ private: bool active = false; u16 hue = 0; - u16 hue_threshold = 0; - u16 saturation_threshold_u8 = 0; + u8 hue_threshold = 0; + u8 saturation_threshold_u8 = 0; s16 min_hue = 0; s16 max_hue = 0; @@ -73,6 +68,9 @@ private: void set_valid(ps_move_info& info, u32 index, bool valid); + // Creates the binary image of all pixels that match the hue, saturation and value of the given device + void filter_image(u32 index); + void draw_sphere_size_range(f32 result_radius); CellGemVideoConvertAttribute m_vc_attr {}; @@ -104,6 +102,7 @@ private: std::array m_hues{}; std::array m_info{}; std::array m_config{}; + std::array m_blob_detectors{}; std::array>>, CELL_GEM_MAX_NUM> m_workers{}; std::array, CELL_GEM_MAX_NUM> m_wake_up_workers{}; diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 707644d406..0bc5955ee0 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -62,7 +62,7 @@ - $(OpenCvBuildDir)\include;$(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\release;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories) + $(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\release;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories) /Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) release\ false @@ -70,7 +70,7 @@ 4577;4467;4281;%(DisableSpecificWarnings) $(IntDir) MaxSpeed - _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;HAVE_OPENCV;CV_IGNORE_DEBUG_BUILD_GUARD;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;WITH_DISCORD_RPC;QT_NO_DEBUG;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;NDEBUG;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;CURL_STATICLIB;%(PreprocessorDefinitions) + _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;WITH_DISCORD_RPC;QT_NO_DEBUG;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;NDEBUG;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;CURL_STATICLIB;%(PreprocessorDefinitions) false $(IntDir)vc$(PlatformToolsetVersion).pdb true @@ -81,8 +81,8 @@ true - $(OpenCvWorld).lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslang.lib;OSDependent.lib;SPIRV.lib;MachineIndependent.lib;GenericCodeGen.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Core.lib;Qt6Gui.lib;Qt6Widgets.lib;Qt6Concurrent.lib;Qt6Multimedia.lib;Qt6MultimediaWidgets.lib;Qt6Svg.lib;Qt6SvgWidgets.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) - $(OpenCvBuildDir)\x64\lib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib_ext\$(CONFIGURATION)-$(PLATFORM);$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\lib;$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) + DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslang.lib;OSDependent.lib;SPIRV.lib;MachineIndependent.lib;GenericCodeGen.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Core.lib;Qt6Gui.lib;Qt6Widgets.lib;Qt6Concurrent.lib;Qt6Multimedia.lib;Qt6MultimediaWidgets.lib;Qt6Svg.lib;Qt6SvgWidgets.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) + $(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib_ext\$(CONFIGURATION)-$(PLATFORM);$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\lib;$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /d2:-cgthreads8 %(AdditionalOptions) true Debug @@ -108,7 +108,6 @@ $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --translationdir "$(TargetDir)qt6\translations" --release "$(TargetPath)" - xcopy /y /d "$(OpenCvBuildDir)\x64\bin\$(OpenCvWorld).dll" "$(OutDir)" @@ -123,7 +122,7 @@ - $(OpenCvBuildDir)\include;$(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\debug;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories) + $(SolutionDir)3rdparty\SoundTouch\soundtouch\include;$(SolutionDir)3rdparty\cubeb\extra;$(SolutionDir)3rdparty\cubeb\cubeb\include\;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\include;$(SolutionDir)3rdparty\wolfssl\wolfssl;$(SolutionDir)3rdparty\wolfssl\extra\win32;$(SolutionDir)3rdparty\curl\curl\include;$(SolutionDir)3rdparty\rtmidi\rtmidi;$(SolutionDir)3rdparty\libusb\libusb\libusb;$(VULKAN_SDK)\Include;$(SolutionDir)3rdparty\libsdl-org\SDL\include;$(QTDIR)\include;$(QTDIR)\include\QtCore;$(QTDIR)\include\QtConcurrent;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtSvgWidgets;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtMultimedia;$(QTDIR)\mkspecs\win32-msvc;.\debug;.\QTGeneratedFiles\$(ConfigurationName);.\QTGeneratedFiles;%(AdditionalIncludeDirectories) /Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions) debug\ false @@ -131,7 +130,7 @@ 4577;4467;4281;%(DisableSpecificWarnings) $(IntDir) Disabled - _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;HAVE_OPENCV;CV_IGNORE_DEBUG_BUILD_GUARD;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;CURL_STATICLIB;%(PreprocessorDefinitions) + _WINDOWS;UNICODE;WIN32;WIN64;WIN32_LEAN_AND_MEAN;HAVE_VULKAN;MINIUPNP_STATICLIB;ZLIB_CONST;AL_LIBTYPE_STATIC;WOLFSSL_USER_SETTINGS;HAVE_SDL3;QT_WIDGETS_LIB;QT_GUI_LIB;QT_CORE_LIB;QT_CONCURRENT_LIB;QT_MULTIMEDIA_LIB;QT_MULTIMEDIAWIDGETS_LIB;QT_SVG_LIB;CURL_STATICLIB;%(PreprocessorDefinitions) false true true @@ -141,8 +140,8 @@ true - $(OpenCvWorld).lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl-d.lib;Wldap32.lib;glslangd.lib;OSDependentd.lib;SPIRVd.lib;MachineIndependentd.lib;GenericCodeGend.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Cored.lib;Qt6Guid.lib;Qt6Widgetsd.lib;Qt6Concurrentd.lib;Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) - $(OpenCvBuildDir)\x64\lib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib\$(CONFIGURATION)-$(PLATFORM);$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\lib;$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) + DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl-d.lib;Wldap32.lib;glslangd.lib;OSDependentd.lib;SPIRVd.lib;MachineIndependentd.lib;GenericCodeGend.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Cored.lib;Qt6Guid.lib;Qt6Widgetsd.lib;Qt6Concurrentd.lib;Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) + $(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib\$(CONFIGURATION)-$(PLATFORM);$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\lib;$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /VERBOSE /d2:-cgthreads8 %(AdditionalOptions) true Debug @@ -167,7 +166,6 @@ $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --translationdir "$(TargetDir)qt6\translations" --debug "$(TargetPath)" - xcopy /y /d "$(OpenCvBuildDir)\x64\bin\$(OpenCvWorld).dll" "$(OutDir)" @@ -191,6 +189,7 @@ + @@ -1128,6 +1127,7 @@ + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index f63cca55b7..28a36d4583 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1266,6 +1266,9 @@ Io\Move + + Io\Move + Io\Move @@ -1562,6 +1565,9 @@ Io\Move + + Io\Move + Io\Move diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index cfe2e0d9af..0220757ab0 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -161,6 +161,7 @@ add_library(rpcs3_ui STATIC ../Input/basic_keyboard_handler.cpp ../Input/basic_mouse_handler.cpp + ../Input/blob_detector.cpp ../Input/camera_video_sink.cpp ../Input/ds3_pad_handler.cpp ../Input/ds4_pad_handler.cpp @@ -229,7 +230,6 @@ target_link_libraries(rpcs3_ui 3rdparty::libusb 3rdparty::wolfssl 3rdparty::libcurl - 3rdparty::opencv 3rdparty::fusion PRIVATE diff --git a/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp b/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp index 7ee3c3bdcb..7de9da462a 100644 --- a/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp +++ b/rpcs3/rpcs3qt/ps_move_tracker_dialog.cpp @@ -266,14 +266,6 @@ ps_move_tracker_dialog::ps_move_tracker_dialog(QWidget* parent) update_min_radius(true); update_max_radius(true); - if constexpr (!g_ps_move_tracking_supported) - { - QTimer::singleShot(1000, this, [this]() - { - QMessageBox::warning(this, QObject::tr("Tracking not supported!"), QObject::tr("The PS Move tracking is not yet supported on this operating system.")); - }); - } - ui->imageLabel->installEventFilter(this); } @@ -562,8 +554,8 @@ void ps_move_tracker_dialog::process_camera_frame() m_ps_move_tracker->set_active(i, index == i); m_ps_move_tracker->set_hue(i, config->hue); - m_ps_move_tracker->set_hue_threshold(i, config->hue_threshold); - m_ps_move_tracker->set_saturation_threshold(i, config->saturation_threshold); + m_ps_move_tracker->set_hue_threshold(i, static_cast(config->hue_threshold.get())); + m_ps_move_tracker->set_saturation_threshold(i, static_cast(config->saturation_threshold.get())); } m_ps_move_tracker->set_image_data(m_frame_frozen ? m_image_data_frozen.data() : m_image_data.data(), m_image_data.size(), width, height, m_camera_handler->format()); diff --git a/rpcs3/tests/rpcs3_test.vcxproj b/rpcs3/tests/rpcs3_test.vcxproj index 8488c1b9b1..9d3bd337a6 100644 --- a/rpcs3/tests/rpcs3_test.vcxproj +++ b/rpcs3/tests/rpcs3_test.vcxproj @@ -47,8 +47,8 @@ true - rpcs3.lib;$(OpenCvWorld).lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslang.lib;OSDependent.lib;SPIRV.lib;MachineIndependent.lib;GenericCodeGen.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Core.lib;Qt6Gui.lib;Qt6Widgets.lib;Qt6Concurrent.lib;Qt6Multimedia.lib;Qt6MultimediaWidgets.lib;Qt6Svg.lib;Qt6SvgWidgets.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) - $(OpenCvBuildDir)\x64\lib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib_ext\$(CONFIGURATION)-$(PLATFORM);$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\lib;$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) + rpcs3.lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl.lib;Wldap32.lib;glslang.lib;OSDependent.lib;SPIRV.lib;MachineIndependent.lib;GenericCodeGen.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Core.lib;Qt6Gui.lib;Qt6Widgets.lib;Qt6Concurrent.lib;Qt6Multimedia.lib;Qt6MultimediaWidgets.lib;Qt6Svg.lib;Qt6SvgWidgets.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) + $(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib_ext\$(CONFIGURATION)-$(PLATFORM);$(SolutionDir)build\lib\$(Configuration)-$(Platform)\protobuf_build\lib;$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) /d2:-cgthreads8 %(AdditionalOptions) Console true @@ -56,7 +56,6 @@ $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-translations --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-ffmpeg --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --release "$(TargetPath)" - xcopy /y /d "$(OpenCvBuildDir)\x64\bin\$(OpenCvWorld).dll" "$(OutDir)" @@ -72,8 +71,8 @@ true - rpcs3d.lib;$(OpenCvWorld).lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl-d.lib;Wldap32.lib;glslangd.lib;OSDependentd.lib;SPIRVd.lib;MachineIndependentd.lib;GenericCodeGend.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Cored.lib;Qt6Guid.lib;Qt6Widgetsd.lib;Qt6Concurrentd.lib;Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) - $(OpenCvBuildDir)\x64\lib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) + rpcs3d.lib;DbgHelp.lib;Ole32.lib;gdi32.lib;hidapi.lib;libusb-1.0.lib;winmm.lib;miniupnpc_static.lib;rtmidi.lib;imm32.lib;ksuser.lib;version.lib;OpenAL32.lib;XAudio.lib;GLGSRender.lib;shlwapi.lib;VKGSRender.lib;vulkan-1.lib;wolfssl.lib;libcurl-d.lib;Wldap32.lib;glslangd.lib;OSDependentd.lib;SPIRVd.lib;MachineIndependentd.lib;GenericCodeGend.lib;Advapi32.lib;user32.lib;zlib.lib;zstd.lib;libpng16.lib;asmjit.lib;yaml-cpp.lib;discord-rpc.lib;emucore.lib;dxgi.lib;shell32.lib;Qt6Cored.lib;Qt6Guid.lib;Qt6Widgetsd.lib;Qt6Concurrentd.lib;Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;7zip.lib;libcubeb.lib;cubeb.lib;soundtouch.lib;Avrt.lib;SDL.lib;fusion.lib;%(AdditionalDependencies) + $(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories) /d2:-cgthreads8 %(AdditionalOptions) Console true @@ -81,7 +80,6 @@ $(QTDIR)\bin\windeployqt6 --no-compiler-runtime --no-opengl-sw --no-patchqt --no-translations --no-quick --no-system-d3d-compiler --no-system-dxc-compiler --no-ffmpeg --no-quick-import --plugindir "$(TargetDir)qt6\plugins" --release "$(TargetPath)" - xcopy /y /d "$(OpenCvBuildDir)\x64\bin\$(OpenCvWorld).dll" "$(OutDir)" @@ -96,6 +94,7 @@ + true diff --git a/rpcs3/tests/test_blob_detector.cpp b/rpcs3/tests/test_blob_detector.cpp new file mode 100644 index 0000000000..b37278bca4 --- /dev/null +++ b/rpcs3/tests/test_blob_detector.cpp @@ -0,0 +1,663 @@ +#include + +#include "Input/blob_detector.h" + +#include +#include +#include + +namespace blob +{ + struct test_image + { + u32 width = 0; + u32 height = 0; + std::vector pixels; + + test_image(u32 w, u32 h) + : width(w), height(h), pixels(usz{w} * h, 0) + { + } + + void disk(f32 center_x, f32 center_y, f32 radius, u8 value = 255) + { + for (u32 y = 0; y < height; y++) + { + for (u32 x = 0; x < width; x++) + { + const f32 dx = x - center_x; + const f32 dy = y - center_y; + + if (dx * dx + dy * dy <= radius * radius) + { + pixels[y * width + x] = value; + } + } + } + } + + void rect(u32 x0, u32 y0, u32 x1, u32 y1, u8 value = 255) + { + for (u32 y = y0; y <= y1; y++) + { + for (u32 x = x0; x <= x1; x++) + { + pixels[y * width + x] = value; + } + } + } + + void detect(blob_detector& detector, bool fill_holes, u32 min_circle_area = 0) const + { + detector.detect(pixels, width, height, fill_holes, min_circle_area); + } + }; + + // Returns the blob that contains the given pixel + const blob_detector::blob* blob_at(const blob_detector& detector, u32 x, u32 y) + { + const u32 label = detector.labels().at(y * detector.width() + x); + return label ? &detector.blobs().at(label - 1) : nullptr; + } + + TEST(BlobDetector, EmptyImage) + { + blob_detector detector; + test_image image(64, 48); + + image.detect(detector, true); + EXPECT_TRUE(detector.blobs().empty()); + EXPECT_EQ(detector.labels().size(), 64u * 48u); + } + + TEST(BlobDetector, InvalidInput) + { + blob_detector detector; + + detector.detect({}, 64, 48, true); + EXPECT_TRUE(detector.blobs().empty()); + + const std::vector pixel { 255 }; + detector.detect(pixel, 0, 0, true); + EXPECT_TRUE(detector.blobs().empty()); + EXPECT_TRUE(detector.labels().empty()); + } + + TEST(BlobDetector, SinglePixel) + { + blob_detector detector; + test_image image(10, 10); + image.pixels[4 * 10 + 5] = 1; + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + + const blob_detector::blob& b = detector.blobs()[0]; + EXPECT_EQ(b.area, 1u); + EXPECT_EQ(b.perimeter, 1u); + EXPECT_EQ(b.min_x, 5u); + EXPECT_EQ(b.max_x, 5u); + EXPECT_EQ(b.min_y, 4u); + EXPECT_EQ(b.max_y, 4u); + EXPECT_FLOAT_EQ(b.center_x, 5.0f); + EXPECT_FLOAT_EQ(b.center_y, 4.0f); + EXPECT_FLOAT_EQ(b.radius, 0.0f); + } + + TEST(BlobDetector, Disk) + { + blob_detector detector; + test_image image(640, 480); + image.disk(300.0f, 200.0f, 20.0f); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + + const blob_detector::blob& b = detector.blobs()[0]; + EXPECT_NEAR(b.radius, 20.0f, 0.6f); + EXPECT_NEAR(b.center_x, 300.0f, 0.6f); + EXPECT_NEAR(b.center_y, 200.0f, 0.6f); + EXPECT_NEAR(b.centroid_x, 300.0f, 0.1f); + EXPECT_NEAR(b.centroid_y, 200.0f, 0.1f); + EXPECT_GT(b.fill_ratio(), 0.9f); + EXPECT_LT(b.fill_ratio(), 1.1f); + EXPECT_EQ(b.min_x, 280u); + EXPECT_EQ(b.max_x, 320u); + EXPECT_EQ(b.min_y, 180u); + EXPECT_EQ(b.max_y, 220u); + } + + TEST(BlobDetector, RingWithAndWithoutHoleFilling) + { + blob_detector detector; + test_image image(200, 200); + image.disk(100.0f, 100.0f, 30.0f); + image.disk(100.0f, 100.0f, 15.0f, 0); + + image.detect(detector, false); + ASSERT_EQ(detector.blobs().size(), 1u); + const u32 ring_area = detector.blobs()[0].area; + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + const blob_detector::blob& b = detector.blobs()[0]; + + // The hole is part of the blob now + EXPECT_GT(b.area, ring_area); + EXPECT_GT(b.fill_ratio(), 0.9f); + EXPECT_NEAR(b.radius, 30.0f, 0.6f); + + // Only the outer boundary is left + EXPECT_LT(b.perimeter, 2 * 3.1416f * 30.0f * 1.5f); + } + + TEST(BlobDetector, NestedBlobStaysSeparateWhenFillingHoles) + { + blob_detector detector; + test_image image(100, 100); + image.disk(50.0f, 50.0f, 30.0f); + image.disk(50.0f, 50.0f, 20.0f, 0); + image.disk(50.0f, 50.0f, 5.0f); + + image.detect(detector, false); + ASSERT_EQ(detector.blobs().size(), 2u); + const u32 ring_area = blob_at(detector, 50, 20)->area; + + // The gap touches the ring and the inner disk, so it must not be filled + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 2u); + EXPECT_EQ(detector.labels()[50 * 100 + 35], 0u); + + const blob_detector::blob* ring = blob_at(detector, 50, 20); + const blob_detector::blob* inner = blob_at(detector, 50, 50); + ASSERT_NE(ring, nullptr); + ASSERT_NE(inner, nullptr); + EXPECT_NE(ring, inner); + EXPECT_EQ(ring->area, ring_area); + EXPECT_NEAR(inner->radius, 5.0f, 0.6f); + } + + TEST(BlobDetector, OnlyHolesOfSingleBlobsAreFilled) + { + blob_detector detector; + test_image image(100, 100); + + // Outer ring + image.disk(50.0f, 50.0f, 40.0f); + image.disk(50.0f, 50.0f, 35.0f, 0); + + // Inner ring with a hole that should be filled + image.disk(50.0f, 50.0f, 20.0f); + image.disk(50.0f, 50.0f, 10.0f, 0); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 2u); + + // The inner hole was filled, the gap between the rings was not + const blob_detector::blob* inner = blob_at(detector, 50, 50); + ASSERT_NE(inner, nullptr); + EXPECT_EQ(detector.labels()[50 * 100 + 75], 0u); + EXPECT_GT(inner->fill_ratio(), 0.9f); + EXPECT_NEAR(inner->radius, 20.0f, 0.6f); + } + + TEST(BlobDetector, LargeHoleIsNotFilled) + { + blob_detector detector; + test_image image(100, 100); + + // A thin outline around a large area + image.rect(10, 10, 89, 89); + image.rect(13, 13, 86, 86, 0); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + EXPECT_EQ(detector.blobs()[0].area, 80u * 80u - 74u * 74u); + EXPECT_EQ(detector.labels()[50 * 100 + 50], 0u); + + // A hole that is smaller than its blob is still filled + image.rect(10, 10, 89, 89); + image.rect(40, 40, 59, 59, 0); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + EXPECT_EQ(detector.blobs()[0].area, 80u * 80u); + } + + TEST(BlobDetector, MomentEllipse) + { + blob_detector detector; + test_image image(300, 200); + + // Disk + image.disk(50.0f, 50.0f, 20.0f); + + // Horizontal capsule with radius 20 and a straight part of 40 pixels + image.disk(120.0f, 150.0f, 20.0f); + image.disk(160.0f, 150.0f, 20.0f); + image.rect(120, 130, 160, 170); + + // Vertical 2:1 rectangle + image.rect(240, 20, 259, 59); + + image.detect(detector, true, 30); + ASSERT_EQ(detector.blobs().size(), 3u); + + const blob_detector::blob* disk = blob_at(detector, 50, 50); + const blob_detector::blob* capsule = blob_at(detector, 140, 150); + const blob_detector::blob* rect = blob_at(detector, 250, 40); + ASSERT_NE(disk, nullptr); + ASSERT_NE(capsule, nullptr); + ASSERT_NE(rect, nullptr); + + EXPECT_NEAR(disk->major_axis, 20.0f, 0.5f); + EXPECT_NEAR(disk->minor_axis, 20.0f, 0.5f); + EXPECT_NEAR(disk->ellipse_fill_ratio(), 1.0f, 0.02f); + + EXPECT_NEAR(capsule->major_axis / capsule->minor_axis, 1.94f, 0.05f); + EXPECT_NEAR(capsule->minor_axis, 20.0f * 1.09f, 0.5f); + EXPECT_NEAR(capsule->angle, 0.0f, 0.01f); + EXPECT_GT(capsule->ellipse_fill_ratio(), 0.97f); + EXPECT_NEAR(capsule->centroid_x, 140.0f, 0.1f); + EXPECT_NEAR(capsule->centroid_y, 150.0f, 0.1f); + + // The capsule is as wide as the sphere + EXPECT_NEAR(detector.find_inscribed_circle(*capsule).radius, 20.0f, 1.0f); + + EXPECT_NEAR(rect->major_axis / rect->minor_axis, 2.0f, 0.05f); + EXPECT_NEAR(std::abs(rect->angle), std::numbers::pi_v / 2.0f, 0.01f); + EXPECT_NEAR(rect->ellipse_fill_ratio(), 0.95f, 0.02f); + + // A single pixel has no extent + image.pixels.assign(image.pixels.size(), 0); + image.pixels[10 * 300 + 10] = 255; + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + EXPECT_FLOAT_EQ(detector.blobs()[0].major_axis, 0.0f); + EXPECT_FLOAT_EQ(detector.blobs()[0].ellipse_fill_ratio(), 0.0f); + } + + TEST(BlobDetector, RoundBulgeAndCapsule) + { + blob_detector detector; + test_image image(400, 300); + + // Disk + image.disk(50.0f, 50.0f, 20.0f); + + // Disk that touches a thinner bar + image.disk(200.0f, 50.0f, 25.0f); + image.rect(220, 40, 399, 60); + + // Two disks of the same size that touch each other + image.disk(50.0f, 150.0f, 20.0f); + image.disk(85.0f, 150.0f, 20.0f); + + // Diagonal capsule with radius 15 + for (int i = 0; i <= 40; i++) + { + image.disk(200.0f + i, 150.0f + i, 15.0f); + } + + // L shape + image.rect(300, 200, 389, 219); + image.rect(300, 200, 319, 289); + + image.detect(detector, true, 30); + ASSERT_EQ(detector.blobs().size(), 5u); + + const blob_detector::blob* disk = blob_at(detector, 50, 50); + const blob_detector::blob* bulge = blob_at(detector, 200, 50); + const blob_detector::blob* pair = blob_at(detector, 50, 150); + const blob_detector::blob* capsule = blob_at(detector, 220, 170); + const blob_detector::blob* corner = blob_at(detector, 305, 205); + ASSERT_NE(disk, nullptr); + ASSERT_NE(bulge, nullptr); + ASSERT_NE(pair, nullptr); + ASSERT_NE(capsule, nullptr); + ASSERT_NE(corner, nullptr); + + // Round bulge + EXPECT_TRUE(detector.is_round_bulge(*disk, detector.find_inscribed_circle(*disk))); + EXPECT_TRUE(detector.is_round_bulge(*bulge, detector.find_inscribed_circle(*bulge))); + EXPECT_FALSE(detector.is_round_bulge(*pair, detector.find_inscribed_circle(*pair))); + EXPECT_FALSE(detector.is_round_bulge(*disk, blob_detector::inscribed_circle{})); + + // The outline around a lone disk is free, the bar covers a small part of the bulge outline + const blob_detector::inscribed_circle bulge_circle = detector.find_inscribed_circle(*bulge); + EXPECT_FLOAT_EQ(detector.free_outline_ratio(*disk, 50.0f, 50.0f, 20.0f), 1.0f); + EXPECT_GT(detector.free_outline_ratio(*bulge, bulge_circle.center_x, bulge_circle.center_y, bulge_circle.radius), 0.8f); + EXPECT_LT(detector.free_outline_ratio(*bulge, bulge_circle.center_x, bulge_circle.center_y, bulge_circle.radius), 1.0f); + + // Capsule + blob_detector::capsule result {}; + ASSERT_TRUE(blob_detector::fit_capsule(*capsule, detector.find_inscribed_circle(*capsule), result)); + EXPECT_NEAR(result.center_x, 220.0f, 0.5f); + EXPECT_NEAR(result.center_y, 170.0f, 0.5f); + EXPECT_NEAR(result.radius, 15.0f, 1.0f); + EXPECT_NEAR(result.half_length, 20.0f * std::numbers::sqrt2_v, 1.5f); + EXPECT_NEAR(std::abs(result.dir_x), std::numbers::sqrt2_v / 2.0f, 0.01f); + EXPECT_NEAR(result.dir_x, result.dir_y, 0.01f); + + // Disks are not elongated and L shapes are not smooth + EXPECT_FALSE(blob_detector::fit_capsule(*disk, detector.find_inscribed_circle(*disk), result)); + EXPECT_FALSE(blob_detector::fit_capsule(*corner, detector.find_inscribed_circle(*corner), result)); + } + + TEST(BlobDetector, InscribedCircle) + { + blob_detector detector; + test_image image(200, 200); + image.disk(100.0f, 100.0f, 20.0f); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + + const blob_detector::blob& b = detector.blobs()[0]; + const blob_detector::inscribed_circle circle = detector.find_inscribed_circle(b); + EXPECT_NEAR(circle.center_x, 100.0f, 1.0f); + EXPECT_NEAR(circle.center_y, 100.0f, 1.0f); + EXPECT_NEAR(circle.radius, b.radius, 1.0f); + + // There is nothing left outside of the inscribed circle + EXPECT_LT(circle.remainder_radius, 2.0f); + + // Invalid blobs are ignored + EXPECT_FLOAT_EQ(detector.find_inscribed_circle(blob_detector::blob{}).radius, 0.0f); + } + + TEST(BlobDetector, InscribedCircleOfMergedBlob) + { + blob_detector detector; + test_image image(300, 200); + + // A disk that touches a bar with the same color + image.disk(100.0f, 100.0f, 25.0f); + image.rect(120, 85, 299, 115); + + image.detect(detector, true, 30); + ASSERT_EQ(detector.blobs().size(), 1u); + + // The merged blob is not round, but the inscribed circle still finds the disk + const blob_detector::blob& b = detector.blobs()[0]; + EXPECT_LT(b.fill_ratio(), 0.5f); + const blob_detector::inscribed_circle circle = detector.find_inscribed_circle(b); + EXPECT_NEAR(circle.center_x, 100.0f, 1.5f); + EXPECT_NEAR(circle.center_y, 100.0f, 1.5f); + EXPECT_NEAR(circle.radius, 25.0f, 1.0f); + + // The bar is 31 pixels wide + EXPECT_NEAR(circle.remainder_radius, 15.0f, 1.0f); + } + + TEST(BlobDetector, InscribedCircleIgnoresOtherBlobs) + { + blob_detector detector; + test_image image(200, 200); + + // A frame around a small disk. The disk is inside the bounding box of the frame, but it doesn't belong to it. + image.rect(10, 10, 189, 189); + image.rect(20, 20, 179, 179, 0); + image.disk(100.0f, 100.0f, 10.0f); + + image.detect(detector, true, 30); + ASSERT_EQ(detector.blobs().size(), 2u); + + const blob_detector::blob* frame = blob_at(detector, 10, 10); + const blob_detector::blob* disk = blob_at(detector, 100, 100); + ASSERT_NE(frame, nullptr); + ASSERT_NE(disk, nullptr); + + // The frame is 10 pixels wide + EXPECT_NEAR(detector.find_inscribed_circle(*frame).radius, 5.0f, 1.0f); + EXPECT_NEAR(detector.find_inscribed_circle(*disk).radius, 10.0f, 1.0f); + } + + TEST(BlobDetector, OpeningRemovesNoiseAndBridges) + { + blob_detector detector; + test_image image(200, 100); + + // Two disks that are connected by a thin line + image.disk(50.0f, 50.0f, 20.0f); + image.disk(150.0f, 50.0f, 20.0f); + image.rect(50, 50, 150, 51); + + // Noise + image.pixels[10 * 200 + 10] = 255; + image.pixels[90 * 200 + 100] = 255; + + // A blob at the image border + image.rect(0, 80, 9, 99); + + image.detect(detector, true); + EXPECT_EQ(detector.blobs().size(), 4u); + + std::vector pixels = image.pixels; + detector.open(pixels, image.width, image.height); + detector.detect(pixels, image.width, image.height, true); + ASSERT_EQ(detector.blobs().size(), 3u); + + const blob_detector::blob* left = blob_at(detector, 50, 50); + const blob_detector::blob* right = blob_at(detector, 150, 50); + const blob_detector::blob* border = blob_at(detector, 0, 99); + ASSERT_NE(left, nullptr); + ASSERT_NE(right, nullptr); + ASSERT_NE(border, nullptr); + EXPECT_NE(left, right); + + // The disks keep their shape and the border blob doesn't shrink + EXPECT_NEAR(left->radius, 20.0f, 0.6f); + EXPECT_NEAR(right->radius, 20.0f, 0.6f); + EXPECT_EQ(border->area, 10u * 20u); + + // Invalid input is ignored + std::vector invalid(10, 255); + detector.open(invalid, 100, 100); + EXPECT_EQ(invalid, std::vector(10, 255)); + } + + TEST(BlobDetector, FillRatioRejectsNonCircularShapes) + { + blob_detector detector; + test_image image(200, 200); + image.rect(20, 20, 59, 59); // Square + image.rect(100, 20, 179, 59); // 2:1 rectangle + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 2u); + + EXPECT_NEAR(detector.blobs()[0].fill_ratio(), 0.64f, 0.05f); + EXPECT_NEAR(detector.blobs()[1].fill_ratio(), 0.51f, 0.05f); + } + + TEST(BlobDetector, DiagonalPixelsAreConnected) + { + blob_detector detector; + test_image image(10, 10); + image.pixels[1 * 10 + 1] = 255; + image.pixels[2 * 10 + 2] = 255; + image.pixels[3 * 10 + 3] = 255; + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + EXPECT_EQ(detector.blobs()[0].area, 3u); + } + + TEST(BlobDetector, SeparateBlobs) + { + blob_detector detector; + test_image image(100, 50); + image.disk(20.0f, 25.0f, 10.0f); + image.disk(70.0f, 25.0f, 15.0f); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 2u); + + // Labels are 1-based and assigned in raster order (the larger disk starts in an earlier row) + EXPECT_EQ(detector.blobs()[0].label, 1u); + EXPECT_EQ(detector.blobs()[1].label, 2u); + EXPECT_EQ(detector.labels()[25 * 100 + 70], 1u); + EXPECT_EQ(detector.labels()[25 * 100 + 20], 2u); + EXPECT_EQ(detector.labels()[0], 0u); + + const blob_detector::blob* small = blob_at(detector, 20, 25); + const blob_detector::blob* large = blob_at(detector, 70, 25); + ASSERT_NE(small, nullptr); + ASSERT_NE(large, nullptr); + EXPECT_NEAR(small->radius, 10.0f, 0.6f); + EXPECT_NEAR(large->radius, 15.0f, 0.6f); + } + + TEST(BlobDetector, LateMergeAndBorderContact) + { + blob_detector detector; + test_image image(20, 20); + + // U shape: the two arms are only connected at the bottom, and the shape touches the image border + image.rect(0, 0, 2, 15); + image.rect(10, 0, 12, 15); + image.rect(0, 13, 12, 15); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 1u); + EXPECT_EQ(detector.blobs()[0].area, 3u * 16u + 3u * 16u + 7u * 3u); + + // The open U is not a hole, so nothing should have been filled + EXPECT_EQ(detector.labels()[5 * 20 + 6], 0u); + } + + TEST(BlobDetector, FillBlob) + { + blob_detector detector; + test_image image(100, 50); + image.disk(20.0f, 25.0f, 10.0f); + image.disk(70.0f, 25.0f, 15.0f); + + image.detect(detector, true); + ASSERT_EQ(detector.blobs().size(), 2u); + + const blob_detector::blob* small = blob_at(detector, 20, 25); + const blob_detector::blob* large = blob_at(detector, 70, 25); + ASSERT_NE(small, nullptr); + ASSERT_NE(large, nullptr); + + std::vector pixels = image.pixels; + detector.fill_blob(pixels, *small, 0); + + // The small blob is gone, the large one is untouched + EXPECT_EQ(pixels[25 * 100 + 20], 0); + EXPECT_EQ(pixels[25 * 100 + 70], 255); + + std::vector rgba(pixels.size() * 4, 0); + detector.fill_blob_rgba(rgba, *large, { 1, 2, 3, 4 }); + EXPECT_EQ(rgba[(25 * 100 + 70) * 4 + 0], 1); + EXPECT_EQ(rgba[(25 * 100 + 70) * 4 + 3], 4); + EXPECT_EQ(rgba[(25 * 100 + 20) * 4 + 0], 0); + } + + TEST(BlobDetector, MinCircleArea) + { + blob_detector detector; + test_image image(100, 50); + image.disk(20.0f, 25.0f, 2.0f); // Small + image.disk(70.0f, 25.0f, 15.0f); // Large + + image.detect(detector, true, 30); + ASSERT_EQ(detector.blobs().size(), 2u); + + const blob_detector::blob* small = blob_at(detector, 20, 25); + const blob_detector::blob* large = blob_at(detector, 70, 25); + ASSERT_NE(small, nullptr); + ASSERT_NE(large, nullptr); + + // The small blob is still reported, but without an enclosing circle + EXPECT_LT(small->area, 30u); + EXPECT_FLOAT_EQ(small->radius, 0.0f); + EXPECT_FLOAT_EQ(small->center_x, small->centroid_x); + EXPECT_NEAR(large->radius, 15.0f, 0.6f); + } + + TEST(BlobDetector, MinEnclosingCircle) + { + std::mt19937 rng(42); + + for (int iteration = 0; iteration < 200; iteration++) + { + std::vector points; + const u32 count = 1 + rng() % 40; + + for (u32 i = 0; i < count; i++) + { + points.push_back({ static_cast(rng() % 100), static_cast(rng() % 100) }); + } + + std::vector shuffled = points; + f32 center_x, center_y, radius; + blob_detector::min_enclosing_circle(shuffled, center_x, center_y, radius); + + f32 max_distance = 0.0f; + f32 max_half_distance = 0.0f; + + for (const blob_detector::point& a : points) + { + max_distance = std::max(max_distance, std::hypot(a.x - center_x, a.y - center_y)); + + for (const blob_detector::point& b : points) + { + max_half_distance = std::max(max_half_distance, std::hypot(static_cast(a.x - b.x), static_cast(a.y - b.y)) / 2.0f); + } + } + + // All points are inside + EXPECT_LE(max_distance, radius + 1e-3f); + + // The circle is at least as large as half the largest distance, and at most diameter / sqrt(3) + EXPECT_GE(radius, max_half_distance - 1e-3f); + EXPECT_LE(radius, max_half_distance * 1.1548f + 1e-3f); + } + } + + TEST(BlobDetector, MinEnclosingCircleCollinear) + { + std::vector points = { { 0, 0 }, { 5, 0 }, { 10, 0 } }; + f32 center_x, center_y, radius; + blob_detector::min_enclosing_circle(points, center_x, center_y, radius); + + EXPECT_NEAR(center_x, 5.0f, 1e-4f); + EXPECT_NEAR(center_y, 0.0f, 1e-4f); + EXPECT_NEAR(radius, 5.0f, 1e-4f); + } + + TEST(BlobDetector, DrawingStaysInBounds) + { + constexpr u32 width = 32; + constexpr u32 height = 16; + std::vector rgba(width * height * 4, 0); + + // Partially or completely outside of the image + rgba_draw::circle(rgba, width, height, 30.0f, 2.0f, 10, { 1, 2, 3, 4 }, true); + rgba_draw::circle(rgba, width, height, -5.0f, 8.0f, 3, { 1, 2, 3, 4 }, false); + rgba_draw::circle(rgba, width, height, 1000.0f, 1000.0f, 3, { 1, 2, 3, 4 }, true); + rgba_draw::line(rgba, width, height, -10, -10, 50, 40, { 1, 2, 3, 4 }); + + EXPECT_EQ(rgba[(2 * width + 30) * 4], 1); + } + + TEST(BlobDetector, DrawCircleOutline) + { + constexpr u32 width = 21; + constexpr u32 height = 21; + std::vector rgba(width * height * 4, 0); + + rgba_draw::circle(rgba, width, height, 10.0f, 10.0f, 5, { 255, 0, 0, 255 }, false); + + // On the outline + EXPECT_EQ(rgba[(10 * width + 15) * 4], 255); + EXPECT_EQ(rgba[(5 * width + 10) * 4], 255); + + // Inside and outside + EXPECT_EQ(rgba[(10 * width + 10) * 4], 0); + EXPECT_EQ(rgba[(10 * width + 18) * 4], 0); + } +} diff --git a/rpcs3/tests/test_iso_extent.cpp b/rpcs3/tests/test_iso_extent.cpp index ece67d1798..456c8f8aa6 100644 --- a/rpcs3/tests/test_iso_extent.cpp +++ b/rpcs3/tests/test_iso_extent.cpp @@ -1,6 +1,6 @@ #include "iso_test_fixture.h" -namespace +namespace iso { class counted_file final : public fs::file_base { @@ -57,70 +57,70 @@ namespace this->m_file = fs::file(std::make_unique(std::move(this->m_file), reads)); } }; -} -TEST_F(iso_test, ExtentEofDoesNotReadBackingFile) -{ - for (bool encrypted_path : {false, true}) + TEST_F(iso_test, ExtentEofDoesNotReadBackingFile) { - for (bool multiple_extents : {false, true}) + for (bool encrypted_path : {false, true}) { - SCOPED_TRACE(testing::Message() << "encrypted path=" << encrypted_path << ", multiple extents=" << multiple_extents); - iso_fs_node node; - node.metadata.extents = {{20, 3}}; - if (multiple_extents) + for (bool multiple_extents : {false, true}) { - node.metadata.extents.insert(node.metadata.extents.begin(), {22, ISO_SECTOR_SIZE}); - } - u64 reads = 0; - std::unique_ptr member; - if (encrypted_path) - { - member = std::make_unique>(reads, m_path, fs::read, node, std::make_shared()); - } - else - { - member = std::make_unique>(reads, m_path, fs::read, node); - } - for (u64 offset : {member->size(), member->size() + 1, member->size() + ISO_SECTOR_SIZE, u64{umax}}) - { - SCOPED_TRACE(offset); - std::array buffer; - buffer.fill(0x5a); - const auto expected = buffer; - const u64 before = reads; - EXPECT_EQ(member->read_at(offset, buffer.data(), buffer.size()), 0); - EXPECT_EQ(reads, before); - EXPECT_EQ(buffer, expected); + SCOPED_TRACE(testing::Message() << "encrypted path=" << encrypted_path << ", multiple extents=" << multiple_extents); + iso_fs_node node; + node.metadata.extents = {{20, 3}}; + if (multiple_extents) + { + node.metadata.extents.insert(node.metadata.extents.begin(), {22, ISO_SECTOR_SIZE}); + } + u64 reads = 0; + std::unique_ptr member; + if (encrypted_path) + { + member = std::make_unique>(reads, m_path, fs::read, node, std::make_shared()); + } + else + { + member = std::make_unique>(reads, m_path, fs::read, node); + } + for (u64 offset : {member->size(), member->size() + 1, member->size() + ISO_SECTOR_SIZE, u64{umax}}) + { + SCOPED_TRACE(offset); + std::array buffer; + buffer.fill(0x5a); + const auto expected = buffer; + const u64 before = reads; + EXPECT_EQ(member->read_at(offset, buffer.data(), buffer.size()), 0); + EXPECT_EQ(reads, before); + EXPECT_EQ(buffer, expected); + } } } } -} -TEST_F(iso_test, ReadsAcrossExtentsStopAtLogicalEof) -{ - iso_fs_node node; - node.metadata.extents = {{22, ISO_SECTOR_SIZE}, {20, 3}}; - iso_file member(m_path, fs::read, node); - std::array buffer{}; - EXPECT_EQ(member.read_at(ISO_SECTOR_SIZE - 1, buffer.data(), buffer.size()), 4); - EXPECT_EQ(buffer[0], '\0'); - EXPECT_EQ(std::string(buffer.data() + 1, 3), "abc"); - EXPECT_EQ(buffer[4], '\0'); - EXPECT_EQ(member.seek(member.size() + 1, fs::seek_set), member.size() + 1); - EXPECT_EQ(member.read(buffer.data(), buffer.size()), 0); -} + TEST_F(iso_test, ReadsAcrossExtentsStopAtLogicalEof) + { + iso_fs_node node; + node.metadata.extents = {{22, ISO_SECTOR_SIZE}, {20, 3}}; + iso_file member(m_path, fs::read, node); + std::array buffer{}; + EXPECT_EQ(member.read_at(ISO_SECTOR_SIZE - 1, buffer.data(), buffer.size()), 4); + EXPECT_EQ(buffer[0], '\0'); + EXPECT_EQ(std::string(buffer.data() + 1, 3), "abc"); + EXPECT_EQ(buffer[4], '\0'); + EXPECT_EQ(member.seek(member.size() + 1, fs::seek_set), member.size() + 1); + EXPECT_EQ(member.read(buffer.data(), buffer.size()), 0); + } -TEST_F(iso_test, EmptyMemberDoesNotReadBackingFile) -{ - iso_fs_node node; - node.metadata.extents = {{20, 0}}; - u64 reads = 0; - observed_iso_file member(reads, m_path, fs::read, node); - char byte = 'x'; - EXPECT_EQ(member.read_at(0, &byte, 1), 0); - EXPECT_EQ(member.read_at(1, &byte, 1), 0); - EXPECT_EQ(member.read_at(0, &byte, 0), 0); - EXPECT_EQ(reads, 0); - EXPECT_EQ(byte, 'x'); + TEST_F(iso_test, EmptyMemberDoesNotReadBackingFile) + { + iso_fs_node node; + node.metadata.extents = {{20, 0}}; + u64 reads = 0; + observed_iso_file member(reads, m_path, fs::read, node); + char byte = 'x'; + EXPECT_EQ(member.read_at(0, &byte, 1), 0); + EXPECT_EQ(member.read_at(1, &byte, 1), 0); + EXPECT_EQ(member.read_at(0, &byte, 0), 0); + EXPECT_EQ(reads, 0); + EXPECT_EQ(byte, 'x'); + } } diff --git a/rpcs3/tests/test_iso_open.cpp b/rpcs3/tests/test_iso_open.cpp index 6d31bde2d6..74a0b20f92 100644 --- a/rpcs3/tests/test_iso_open.cpp +++ b/rpcs3/tests/test_iso_open.cpp @@ -1,64 +1,67 @@ #include "iso_test_fixture.h" -TEST_F(iso_test, MissingMemberReturnsEmptyHandle) +namespace iso { - iso_archive archive(m_path); - ASSERT_TRUE(archive.is_valid()); - EXPECT_FALSE(archive.open("MISSING.BIN")); - EXPECT_EQ(fs::g_tls_error, fs::error::noent); - EXPECT_FALSE(archive.open("TEST.BIN/MISSING")); - EXPECT_FALSE(archive.open("")); - - fs::file member(archive.open("TEST.BIN")); - ASSERT_TRUE(member); - std::array data{}; - EXPECT_EQ(member.read(data.data(), data.size()), data.size()); - EXPECT_EQ(std::string(data.data(), data.size()), "abc"); -} - -TEST_F(iso_test, InvalidArchiveReturnsEmptyHandle) -{ - iso_archive archive(m_path + ".missing"); - ASSERT_FALSE(archive.is_valid()); - EXPECT_FALSE(archive.open("TEST.BIN")); -} - -TEST_F(iso_test, TruncatedMetadataRejectsArchive) -{ - for (usz length : {16 * ISO_SECTOR_SIZE + 157, 18 * ISO_SECTOR_SIZE, 18 * ISO_SECTOR_SIZE + 10, 18 * ISO_SECTOR_SIZE + 33}) + TEST_F(iso_test, MissingMemberReturnsEmptyHandle) { - SCOPED_TRACE(length); - { - std::ofstream out(m_path, std::ios::binary); - out.write(reinterpret_cast(m_image.data()), length); - ASSERT_TRUE(out.good()); - } iso_archive archive(m_path); - EXPECT_FALSE(archive.is_valid()); + ASSERT_TRUE(archive.is_valid()); + EXPECT_FALSE(archive.open("MISSING.BIN")); + EXPECT_EQ(fs::g_tls_error, fs::error::noent); + EXPECT_FALSE(archive.open("TEST.BIN/MISSING")); + EXPECT_FALSE(archive.open("")); + + fs::file member(archive.open("TEST.BIN")); + ASSERT_TRUE(member); + std::array data{}; + EXPECT_EQ(member.read(data.data(), data.size()), data.size()); + EXPECT_EQ(std::string(data.data(), data.size()), "abc"); } -} -TEST_F(iso_test, InvalidDirectoryRecordRejectsArchive) -{ - const auto original = m_image; - for (bool invalid_length : {false, true}) + TEST_F(iso_test, InvalidArchiveReturnsEmptyHandle) { - SCOPED_TRACE(invalid_length); - m_image = original; - if (invalid_length) - { - m_image[18 * ISO_SECTOR_SIZE] = 1; - } - else + iso_archive archive(m_path + ".missing"); + ASSERT_FALSE(archive.is_valid()); + EXPECT_FALSE(archive.open("TEST.BIN")); + } + + TEST_F(iso_test, TruncatedMetadataRejectsArchive) + { + for (usz length : {16 * ISO_SECTOR_SIZE + 157, 18 * ISO_SECTOR_SIZE, 18 * ISO_SECTOR_SIZE + 10, 18 * ISO_SECTOR_SIZE + 33}) { - m_image[18 * ISO_SECTOR_SIZE + 32] = 255; + SCOPED_TRACE(length); + { + std::ofstream out(m_path, std::ios::binary); + out.write(reinterpret_cast(m_image.data()), length); + ASSERT_TRUE(out.good()); + } + iso_archive archive(m_path); + EXPECT_FALSE(archive.is_valid()); } + } + + TEST_F(iso_test, InvalidDirectoryRecordRejectsArchive) + { + const auto original = m_image; + for (bool invalid_length : {false, true}) { - std::ofstream out(m_path, std::ios::binary); - out.write(reinterpret_cast(m_image.data()), m_image.size()); - ASSERT_TRUE(out.good()); + SCOPED_TRACE(invalid_length); + m_image = original; + if (invalid_length) + { + m_image[18 * ISO_SECTOR_SIZE] = 1; + } + else + { + m_image[18 * ISO_SECTOR_SIZE + 32] = 255; + } + { + std::ofstream out(m_path, std::ios::binary); + out.write(reinterpret_cast(m_image.data()), m_image.size()); + ASSERT_TRUE(out.good()); + } + iso_archive archive(m_path); + EXPECT_FALSE(archive.is_valid()); } - iso_archive archive(m_path); - EXPECT_FALSE(archive.is_valid()); } } diff --git a/scripts/win_build.ps1 b/scripts/win_build.ps1 index a5e42b31d2..cdb33a9ead 100644 --- a/scripts/win_build.ps1 +++ b/scripts/win_build.ps1 @@ -147,7 +147,6 @@ if (-not (Test-Path $CacheFile)) { & cmake $SourceDir -G 'Ninja Multi-Config' ` -DBUILD_LLVM=ON ` - -DUSE_SYSTEM_OPENCV=OFF ` -DUSE_SYSTEM_CURL=OFF ` -DUSE_SYSTEM_ZLIB=OFF ` -DUSE_SYSTEM_LIBPNG=OFF `