psmove: get rid of opencv

This commit is contained in:
Megamouse committed 2026-10-21 10:29:52 +02:00
1 parent 6940b12ebc
commit 71d5cd17c3
30 files changed
+2073 -365

No files matched your search

+1 -2
View File
@@ -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)
+1 -2
View File
@@ -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 \
+1 -2
View File
@@ -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 \
+1 -3
View File
@@ -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=$?;
+1 -2
View File
@@ -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 \
+1 -1
View File
@@ -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
-1
View File
@@ -2,7 +2,6 @@ ports:
- name: abseil
- name: ccache
- name: gtest
- name: opencv4
- name: sdl3
- name: vulkan-headers
- name: vulkan-loader
-1
View File
@@ -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
-4
View File
@@ -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
-4
View File
@@ -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)
-21
View File
@@ -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()
Submodule 3rdparty/opencv/opencv deleted from d4dde8afe3.
-1
View File
@@ -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)
-3
View File
@@ -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"
},
-2
View File
@@ -9,8 +9,6 @@
<IntDir>$(SolutionDir)build\tmp\$(ProjectName)-$(Configuration)-$(Platform)\</IntDir>
<GTestPath>$(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</GTestPath>
<GTestInstalled Condition="Exists('$(GTestPath)') AND '$(SKIP_RPCS3_TEST)' == ''">true</GTestInstalled>
<OpenCvBuildDir>$(SolutionDir)3rdparty\opencv\opencv\opencv414\build</OpenCvBuildDir>
<OpenCvWorld>opencv_world4140</OpenCvWorld>
</PropertyGroup>
<ItemDefinitionGroup>
<Lib>
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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<u8>(config->hue_threshold.get()));
m_tracker.set_saturation_threshold(gem_num, static_cast<u8>(config->saturation_threshold.get()));
}
m_tracker.set_min_radius(static_cast<f32>(g_cfg_move.min_radius) / 100.0f);
+941
View File
@@ -0,0 +1,941 @@
#include "stdafx.h"
#include "blob_detector.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numbers>
#include <random>
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<f32> * 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<f32> * 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<f32>::infinity();
z[1] = std::numeric_limits<f32>::infinity();
// Intersection of the parabolas at q and p
const auto intersection = [f](s32 q, s32 p)
{
return ((f[q] + static_cast<f32>(q * q)) - (f[p] + static_cast<f32>(p * p))) / static_cast<f32>(2 * (q - p));
};
for (s32 q = 1; q < static_cast<s32>(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<f32>::infinity();
}
k = 0;
for (s32 q = 0; q < static_cast<s32>(n); q++)
{
while (z[k + 1] < q)
{
k++;
}
const f32 dq = static_cast<f32>(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<f32>(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<f32>(b.min_x + max_x - 1);
result.center_y = static_cast<f32>(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<f32>(y) - static_cast<f32>(max_y);
for (u32 x = 0; x < w; x++)
{
const f32 dx = static_cast<f32>(x) - static_cast<f32>(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<s32>(m_width);
const s32 height = static_cast<s32>(m_height);
u32 free_count = 0;
for (u32 i = 0; i < sample_count; i++)
{
const f32 angle = i * (2.0f * std::numbers::pi_v<f32> / sample_count);
const s32 x = static_cast<s32>(std::lround(center_x + r * std::cos(angle)));
const s32 y = static_cast<s32>(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<f32>(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 <bool Erode>
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<u8>& 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<true>(binary.data(), m_temp.data(), width, height);
morphology_3x3<false>(binary.data(), m_temp.data(), width, height);
}
void blob_detector::detect(const std::vector<u8>& 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<sums> 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<s32>(x), static_cast<s32>(y) });
}
}
}
for (usz i = 0; i < m_blobs.size(); i++)
{
blob& b = m_blobs[i];
const sums& s = blob_sums[i];
std::vector<point>& 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<f32>(centroid_x);
b.centroid_y = static_cast<f32>(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<f32>(2.0 * std::sqrt(std::max(0.0, mean + delta)));
b.minor_axis = static_cast<f32>(2.0 * std::sqrt(std::max(0.0, mean - delta)));
b.angle = static_cast<f32>(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<u8>& 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<u8>& rgba, const blob& b, const std::array<u8, 4>& 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<point>& 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<u32>(points.size()));
std::shuffle(points.begin(), points.end(), rng);
circle c{ static_cast<f64>(points[0].x), static_cast<f64>(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<f64>(points[i].x), static_cast<f64>(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<f32>(c.x);
center_y = static_cast<f32>(c.y);
radius = static_cast<f32>(std::sqrt(c.r2));
}
namespace rgba_draw
{
static inline void set_pixel(std::vector<u8>& rgba, u32 width, u32 height, s32 x, s32 y, const std::array<u8, 4>& color)
{
if (x < 0 || y < 0 || static_cast<u32>(x) >= width || static_cast<u32>(y) >= height)
{
return;
}
std::copy(color.begin(), color.end(), &rgba[(static_cast<usz>(y) * width + x) * 4]);
}
void circle(std::vector<u8>& rgba, u32 width, u32 height, f32 center_x, f32 center_y, s32 radius, const std::array<u8, 4>& color, bool filled)
{
const usz size = usz{width} * height * 4u;
if (rgba.empty() || rgba.size() != size || radius < 0)
{
return;
}
const s32 cx = static_cast<s32>(std::lround(center_x));
const s32 cy = static_cast<s32>(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<s32>(height) - 1, cy + radius + 1);
const s32 x_begin = std::max(0, cx - radius - 1);
const s32 x_end = std::min(static_cast<s32>(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<u8>& rgba, u32 width, u32 height, s32 x0, s32 y0, s32 x1, s32 y1, const std::array<u8, 4>& 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;
}
}
}
}
+131
View File
@@ -0,0 +1,131 @@
#pragma once
#include "util/types.hpp"
#include <array>
#include <vector>
// 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<u8>& binary, u32 width, u32 height, bool fill_holes, u32 min_circle_area = 0);
const std::vector<blob>& blobs() const { return m_blobs; }
const std::vector<u32>& 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<u8>& 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<u8>& rgba, const blob& b, const std::array<u8, 4>& 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<u8>& 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<point>& 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<u8> m_mask; // 1 = foreground
std::vector<u32> m_labels; // Label per pixel
std::vector<u32> m_parents; // Union-find parents of the provisional labels
std::vector<u32> m_compact_labels; // Provisional root label -> final label
std::vector<u32> m_stack; // Pixel indices for the flood fill
std::vector<u32> m_region; // Pixel indices of the current hole
std::vector<u32> m_areas; // Area of each blob before the holes are filled
std::vector<u8> m_temp; // Temporary image for the morphology
std::vector<f32> m_distances; // Squared distances for the distance transform
std::vector<f32> m_line; // One row or column of the distance transform
std::vector<s32> m_parabolas; // Parabola positions for the distance transform
std::vector<f32> m_bounds; // Parabola boundaries for the distance transform
std::vector<std::vector<point>> m_boundaries; // Boundary pixels of each blob
std::vector<blob> m_blobs;
};
// Simple drawing functions for RGBA images (4 bytes per pixel, no row padding)
namespace rgba_draw
{
void circle(std::vector<u8>& rgba, u32 width, u32 height, f32 center_x, f32 center_y, s32 radius, const std::array<u8, 4>& color, bool filled);
void line(std::vector<u8>& rgba, u32 width, u32 height, s32 x0, s32 y0, s32 x1, s32 y1, const std::array<u8, 4>& color);
}
+185 -166
View File
@@ -4,14 +4,7 @@
#include "ps_move_tracker.h"
#include <cmath>
#ifdef HAVE_OPENCV
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#if CV_VERSION_MAJOR >= 5
#include <opencv2/geometry.hpp>
#endif
#endif
#include <numbers>
LOG_CHANNEL(ps_move);
@@ -112,7 +105,7 @@ void ps_move_tracker<DiagnosticsEnabled>::set_hue(u32 index, u16 hue)
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::set_hue_threshold(u32 index, u16 threshold)
void ps_move_tracker<DiagnosticsEnabled>::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<DiagnosticsEnabled>::set_hue_threshold(u32 index, u16 thres
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::set_saturation_threshold(u32 index, u16 threshold)
void ps_move_tracker<DiagnosticsEnabled>::set_saturation_threshold(u32 index, u8 threshold)
{
ps_move_config& config = ::at32(m_config, index);
config.saturation_threshold_u8 = threshold;
config.calculate_values();
}
template <bool DiagnosticsEnabled>
@@ -309,24 +301,42 @@ void ps_move_tracker<DiagnosticsEnabled>::process_hues()
}
}
#ifdef HAVE_OPENCV
static bool is_circular_contour(const std::vector<cv::Point>& 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<cv::Point> 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<f32>(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 <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::draw_sphere_size_range(f32 result_radius)
{
@@ -336,25 +346,67 @@ void ps_move_tracker<DiagnosticsEnabled>::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<int>(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<s32>(result_radius), { 255, 0, 0, 255 }, true);
}
if (min_radius_clamped > 0.0f && min_radius_clamped <= max_radius)
{
cv::circle(rgba, center, static_cast<int>(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<s32>(min_radius_clamped), { 0, 0, 0, 255 }, true);
}
if (max_radius > min_radius_clamped)
{
cv::circle(rgba, center, static_cast<int>(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<s32>(max_radius), { 0, 0, 0, 255 }, false);
}
}
template <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::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<u8>& image_hsv = m_image_hsv;
std::vector<u8>& 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<DiagnosticsEnabled>::process_contours(ps_move_info& info, u
return;
}
const ps_move_config& config = ::at32(m_config, index);
const std::vector<u8>& image_hsv = m_image_hsv;
std::vector<u8>& 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<u8>& 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<std::vector<cv::Point>> 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<std::vector<cv::Point>> 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<std::vector<cv::Point>> contours;
std::vector<cv::Point2f> centers;
std::vector<f32> 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<sphere_candidate> candidates;
sphere_candidate best_candidate {};
for (const blob_detector::blob& b : detector.blobs())
{
const std::vector<cv::Point>& 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<f32> * 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<DiagnosticsEnabled>::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<f32>(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<u32>(best_center.x), 0u, width);
const u32 y_pos = std::clamp(static_cast<u32>(best_center.y), 0u, height);
const u32 x_pos = std::clamp(static_cast<u32>(best_candidate.x), 0u, width);
const u32 y_pos = std::clamp(static_cast<u32>(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<DiagnosticsEnabled>::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<cv::Point> 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<u8, 4> contour_color = { 255, 0, 0, 255 };
static constexpr std::array<u8, 4> circle_color = { 0, 255, 0, 255 };
static constexpr std::array<u8, 4> 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<s32>(std::lround(candidate.x));
const s32 y = static_cast<s32>(std::lround(candidate.y));
cv::circle(rgba, center, static_cast<int>(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<s32>(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 <bool DiagnosticsEnabled>
void ps_move_tracker<DiagnosticsEnabled>::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 <bool DiagnosticsEnabled>
std::tuple<u8, u8, u8> ps_move_tracker<DiagnosticsEnabled>::hsv_to_rgb(u16 hue, f32 saturation, f32 value)
+9 -10
View File
@@ -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<u32, 360> m_hues{};
std::array<ps_move_info, CELL_GEM_MAX_NUM> m_info{};
std::array<ps_move_config, CELL_GEM_MAX_NUM> m_config{};
std::array<blob_detector, CELL_GEM_MAX_NUM> m_blob_detectors{};
std::array<std::unique_ptr<named_thread<std::function<void()>>>, CELL_GEM_MAX_NUM> m_workers{};
std::array<atomic_t<u32>, CELL_GEM_MAX_NUM> m_wake_up_workers{};
+10 -10
View File
@@ -62,7 +62,7 @@
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(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)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<AdditionalOptions>/Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
<AssemblerListingLocation>release\</AssemblerListingLocation>
<BrowseInformation>false</BrowseInformation>
@@ -70,7 +70,7 @@
<DisableSpecificWarnings>4577;4467;4281;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<ObjectFileName>$(IntDir)</ObjectFileName>
<Optimization>MaxSpeed</Optimization>
<PreprocessorDefinitions>_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)</PreprocessorDefinitions>
<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)</PreprocessorDefinitions>
<PreprocessToFile>false</PreprocessToFile>
<ProgramDataBaseFileName>$(IntDir)vc$(PlatformToolsetVersion).pdb</ProgramDataBaseFileName>
<RuntimeTypeInfo>true</RuntimeTypeInfo>
@@ -81,8 +81,8 @@
</ClCompile>
<Link>
<ExcludedFromBuild Condition="'$(SKIP_RPCS3_EXE)' != ''">true</ExcludedFromBuild>
<AdditionalDependencies>$(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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(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)</AdditionalLibraryDirectories>
<AdditionalDependencies>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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(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)</AdditionalLibraryDirectories>
<AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /d2:-cgthreads8 %(AdditionalOptions)</AdditionalOptions>
<DataExecutionPrevention>true</DataExecutionPrevention>
<GenerateDebugInformation>Debug</GenerateDebugInformation>
@@ -108,7 +108,6 @@
<PostBuildEvent Condition="'$(SKIP_RPCS3_EXE)' == ''">
<Command>
$(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)"
</Command>
</PostBuildEvent>
<PreLinkEvent Condition="'$(SKIP_RPCS3_EXE)' == ''">
@@ -123,7 +122,7 @@
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(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)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<AdditionalOptions>/Zc:__cplusplus -Zc:strictStrings -Zc:throwingNew- -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions>
<AssemblerListingLocation>debug\</AssemblerListingLocation>
<BrowseInformation>false</BrowseInformation>
@@ -131,7 +130,7 @@
<DisableSpecificWarnings>4577;4467;4281;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<ObjectFileName>$(IntDir)</ObjectFileName>
<Optimization>Disabled</Optimization>
<PreprocessorDefinitions>_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)</PreprocessorDefinitions>
<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)</PreprocessorDefinitions>
<PreprocessToFile>false</PreprocessToFile>
<RuntimeTypeInfo>true</RuntimeTypeInfo>
<SuppressStartupBanner>true</SuppressStartupBanner>
@@ -141,8 +140,8 @@
</ClCompile>
<Link>
<ExcludedFromBuild Condition="'$(SKIP_RPCS3_EXE)' != ''">true</ExcludedFromBuild>
<AdditionalDependencies>$(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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(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)</AdditionalLibraryDirectories>
<AdditionalDependencies>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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(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)</AdditionalLibraryDirectories>
<AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /VERBOSE /d2:-cgthreads8 %(AdditionalOptions)</AdditionalOptions>
<DataExecutionPrevention>true</DataExecutionPrevention>
<GenerateDebugInformation>Debug</GenerateDebugInformation>
@@ -167,7 +166,6 @@
<PostBuildEvent Condition="'$(SKIP_RPCS3_EXE)' == ''">
<Command>
$(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)"
</Command>
</PostBuildEvent>
<PreLinkEvent Condition="'$(SKIP_RPCS3_EXE)' == ''">
@@ -191,6 +189,7 @@
<ClCompile Include="Input\mouse_gyro_handler.cpp" />
<ClCompile Include="Input\ps_move_calibration.cpp" />
<ClCompile Include="Input\ps_move_config.cpp" />
<ClCompile Include="Input\blob_detector.cpp" />
<ClCompile Include="Input\ps_move_tracker.cpp" />
<ClCompile Include="Input\raw_mouse_config.cpp" />
<ClCompile Include="Input\raw_mouse_handler.cpp" />
@@ -1128,6 +1127,7 @@
<ClInclude Include="Input\mouse_gyro_handler.h" />
<ClInclude Include="Input\ps_move_calibration.h" />
<ClInclude Include="Input\ps_move_config.h" />
<ClInclude Include="Input\blob_detector.h" />
<ClInclude Include="Input\ps_move_tracker.h" />
<ClInclude Include="Input\raw_mouse_config.h" />
<ClInclude Include="Input\raw_mouse_handler.h" />
+6
View File
@@ -1266,6 +1266,9 @@
<ClCompile Include="Input\ps_move_tracker.cpp">
<Filter>Io\Move</Filter>
</ClCompile>
<ClCompile Include="Input\blob_detector.cpp">
<Filter>Io\Move</Filter>
</ClCompile>
<ClCompile Include="Input\ps_move_config.cpp">
<Filter>Io\Move</Filter>
</ClCompile>
@@ -1562,6 +1565,9 @@
<ClInclude Include="Input\ps_move_tracker.h">
<Filter>Io\Move</Filter>
</ClInclude>
<ClInclude Include="Input\blob_detector.h">
<Filter>Io\Move</Filter>
</ClInclude>
<ClInclude Include="Input\ps_move_config.h">
<Filter>Io\Move</Filter>
</ClInclude>
+1 -1
View File
@@ -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
+2 -10
View File
@@ -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<u8>(config->hue_threshold.get()));
m_ps_move_tracker->set_saturation_threshold(i, static_cast<u8>(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());
+5 -6
View File
@@ -47,8 +47,8 @@
</ClCompile>
<Link>
<ExcludedFromBuild Condition="'$(SKIP_RPCS3_TEST)' != ''">true</ExcludedFromBuild>
<AdditionalDependencies>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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(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)</AdditionalLibraryDirectories>
<AdditionalDependencies>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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(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)</AdditionalLibraryDirectories>
<AdditionalOptions>/d2:-cgthreads8 %(AdditionalOptions)</AdditionalOptions>
<SubSystem>Console</SubSystem>
<RandomizedBaseAddress>true</RandomizedBaseAddress>
@@ -56,7 +56,6 @@
<PostBuildEvent Condition="'$(SKIP_RPCS3_TEST)' == ''">
<Command>
$(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)"
</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
@@ -72,8 +71,8 @@
</ClCompile>
<Link>
<ExcludedFromBuild Condition="'$(SKIP_RPCS3_TEST)' != ''">true</ExcludedFromBuild>
<AdditionalDependencies>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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(OpenCvBuildDir)\x64\lib;$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>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)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(SolutionDir)build\lib\$(Configuration)-$(Platform)\glslang;$(SolutionDir)build\lib\$(CONFIGURATION)-$(PLATFORM);$(QTDIR)\lib;$(VULKAN_SDK)\Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalOptions>/d2:-cgthreads8 %(AdditionalOptions)</AdditionalOptions>
<SubSystem>Console</SubSystem>
<RandomizedBaseAddress>true</RandomizedBaseAddress>
@@ -81,7 +80,6 @@
<PostBuildEvent Condition="'$(SKIP_RPCS3_TEST)' == ''">
<Command>
$(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)"
</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
@@ -96,6 +94,7 @@
<ItemGroup>
<ClCompile Include="test.cpp" />
<ClCompile Include="test_bit_set.cpp" />
<ClCompile Include="test_blob_detector.cpp" />
<ClCompile Include="test_cctype.cpp" />
<ClCompile Include="test_dmux_pamf.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
+663
View File
@@ -0,0 +1,663 @@
#include <gtest/gtest.h>
#include "Input/blob_detector.h"
#include <cmath>
#include <numbers>
#include <random>
namespace blob
{
struct test_image
{
u32 width = 0;
u32 height = 0;
std::vector<u8> 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<u8> 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<f32> / 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<f32>, 1.5f);
EXPECT_NEAR(std::abs(result.dir_x), std::numbers::sqrt2_v<f32> / 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<u8> 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<u8> invalid(10, 255);
detector.open(invalid, 100, 100);
EXPECT_EQ(invalid, std::vector<u8>(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<u8> 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<u8> 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<blob_detector::point> points;
const u32 count = 1 + rng() % 40;
for (u32 i = 0; i < count; i++)
{
points.push_back({ static_cast<s32>(rng() % 100), static_cast<s32>(rng() % 100) });
}
std::vector<blob_detector::point> 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<f32>(a.x - b.x), static_cast<f32>(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<blob_detector::point> 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<u8> 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<u8> 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);
}
}
+58 -58
View File
@@ -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<counted_file>(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<iso_file> member;
if (encrypted_path)
{
member = std::make_unique<observed_iso_file<iso_file_encrypted>>(reads, m_path, fs::read, node, std::make_shared<iso_file_decryption>());
}
else
{
member = std::make_unique<observed_iso_file<iso_file>>(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<u8, 8> 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<iso_file> member;
if (encrypted_path)
{
member = std::make_unique<observed_iso_file<iso_file_encrypted>>(reads, m_path, fs::read, node, std::make_shared<iso_file_decryption>());
}
else
{
member = std::make_unique<observed_iso_file<iso_file>>(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<u8, 8> 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<char, 8> 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<char, 8> 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<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<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');
}
}
+53 -50
View File
@@ -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<char, 3> 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<const char*>(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<char, 3> 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<const char*>(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<const char*>(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<const char*>(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());
}
}
-1
View File
@@ -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 `