mirror of
https://github.com/RPCS3/rpcs3.git
synced 2026-11-07 09:26:04 -10:00
psmove: get rid of opencv
This commit is contained in:
1 parent
6940b12ebc
commit
71d5cd17c3
30 files changed
+2073
-365
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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=$?;
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
@@ -2,7 +2,6 @@ ports:
|
||||
- name: abseil
|
||||
- name: ccache
|
||||
- name: gtest
|
||||
- name: opencv4
|
||||
- name: sdl3
|
||||
- name: vulkan-headers
|
||||
- name: vulkan-loader
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
-4
@@ -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)
|
||||
Vendored
-21
@@ -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()
|
||||
Vendored
-1
Submodule 3rdparty/opencv/opencv deleted from d4dde8afe3.
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 `
|
||||
|
||||
Reference in new issue
Block a user