From b517e88e117555d6ee625e16d42e0ab9a914d68f Mon Sep 17 00:00:00 2001
From: Paul Coignac <61156794+DayUx@users.noreply.github.com>
Date: Tue, 11 Nov 2025 20:55:48 +0100
Subject: [PATCH 1/4] Features/latest release if not pre release (#6)
* make `make_latest` dynamic based on pre-release label
* make `prerelease` dynamic based on pre-release label
* restructure workflows: add test job and set dependencies for build tasks
* Restrict release
---
.github/workflows/build-subvision-core.yml | 25 ++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/build-subvision-core.yml b/.github/workflows/build-subvision-core.yml
index 6999284..5b9d7da 100644
--- a/.github/workflows/build-subvision-core.yml
+++ b/.github/workflows/build-subvision-core.yml
@@ -12,10 +12,8 @@ on:
required: false
jobs:
- build-wasm:
+ test:
runs-on: ubuntu-latest
- permissions:
- contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
@@ -33,7 +31,16 @@ jobs:
make -j$(nproc)
cd test
./subvision_tests
-
+
+ build-wasm:
+ needs: test
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
- name: Build subvision.js & subvision.mjs
run: make all
@@ -46,6 +53,7 @@ jobs:
build_wasm/subvision.mjs
build-dotnet:
+ needs: test
runs-on: windows-latest
permissions:
contents: write
@@ -109,10 +117,19 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
+ if: |
+ github.event_name == 'push' &&
+ (
+ startsWith(github.ref, 'refs/heads/master') ||
+ startsWith(github.ref, 'refs/heads/main') ||
+ startsWith(github.ref, 'refs/heads/develop')
+ )
with:
tag_name: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }}
body: ${{ inputs.release_notes }}
+ prerelease : ${{ steps.version.outputs.pre-release-label != '' }}
+ make_latest: ${{ steps.version.outputs.pre-release-label == '' }}
files: |
wasm-artifacts/subvision.js
wasm-artifacts/subvision.mjs
From a64a005ef73fccb6a6cd3ce7a8f30af23fb64c25 Mon Sep 17 00:00:00 2001
From: Paul Coignac <61156794+DayUx@users.noreply.github.com>
Date: Sat, 22 Nov 2025 17:20:19 +0100
Subject: [PATCH 2/4] Features/custom logging system (#7)
* Feature: Implement custom logging system and integrate into existing code
* Feature: Add logging source file to Makefile for custom logging system
* Feature: Include logging header in cli_wrapper.cpp for custom logging integration
---
.idea/editor.xml | 1 +
CMakeLists.txt | 79 ++++++++++++++++++-----------------
Makefile | 3 +-
cli_wrapper.cpp | 7 ++++
emscripten_binding.cpp | 11 +++--
include/logging.h | 11 +++++
src/image_processing.cpp | 19 +++++----
src/impact_detection.cpp | 3 +-
src/logging.cpp | 26 ++++++++++++
src/sheet_detection.cpp | 12 +++---
src/target_detection.cpp | 3 +-
src/utils.cpp | 3 +-
test/EllipseDetectionTest.cpp | 14 +++----
test/ImpactDetectionTest.cpp | 13 +++---
14 files changed, 128 insertions(+), 77 deletions(-)
create mode 100644 include/logging.h
create mode 100644 src/logging.cpp
diff --git a/.idea/editor.xml b/.idea/editor.xml
index 25c6c37..198c798 100644
--- a/.idea/editor.xml
+++ b/.idea/editor.xml
@@ -117,6 +117,7 @@
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 98a0146..236f71a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,28 +5,29 @@ project(subvision_core)
set(CMAKE_CXX_STANDARD 23)
# Vérifier si on compile avec Emscripten
-if(EMSCRIPTEN)
+if (EMSCRIPTEN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap']")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s EXPORT_NAME='Subvision'")
set(CMAKE_EXECUTABLE_SUFFIX ".js")
# OpenCV doit être configuré pour Emscripten
set(OpenCV_DIR "${CMAKE_SOURCE_DIR}/opencv_emscripten_build")
-else()
+else ()
set(ENV{OPENCV_DIR} "C:\\tools\\opencv\\build")
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
-endif()
+endif ()
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/include)
# Définir les fichiers sources de la bibliothèque
-set(LIB_SOURCES
- src/utils.cpp
- src/image_processing.cpp
- src/target_detection.cpp
- src/impact_detection.cpp
- src/sheet_detection.cpp
+set(LIB_SOURCES
+ src/utils.cpp
+ src/image_processing.cpp
+ src/target_detection.cpp
+ src/impact_detection.cpp
+ src/sheet_detection.cpp
+ src/logging.cpp
)
# Créer une bibliothèque statique
@@ -34,14 +35,14 @@ add_library(subvision_lib STATIC ${LIB_SOURCES})
target_include_directories(subvision_lib PUBLIC ${CMAKE_SOURCE_DIR}/include)
set(OpenCV_LIBS opencv_core
- opencv_imgproc
- opencv_highgui
- opencv_imgcodecs
- opencv_videoio
- opencv_features2d
- opencv_calib3d
- opencv_flann
- opencv_dnn)
+ opencv_imgproc
+ opencv_highgui
+ opencv_imgcodecs
+ opencv_videoio
+ opencv_features2d
+ opencv_calib3d
+ opencv_flann
+ opencv_dnn)
# Création d'un répertoire resources si nécessaire
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/resources)
@@ -51,14 +52,14 @@ option(BUILD_TESTS "Build tests" ON)
option(BUILD_CLI_WRAPPER "Build C++/CLI .NET wrapper" OFF)
# Don't build tests when building CLI wrapper (they conflict with /clr)
-if(BUILD_TESTS AND NOT EMSCRIPTEN AND NOT BUILD_CLI_WRAPPER)
+if (BUILD_TESTS AND NOT EMSCRIPTEN AND NOT BUILD_CLI_WRAPPER)
add_subdirectory(test)
-endif()
+endif ()
# Configuration C++/CLI pour .NET
-if(BUILD_CLI_WRAPPER AND NOT EMSCRIPTEN)
+if (BUILD_CLI_WRAPPER AND NOT EMSCRIPTEN)
# C++/CLI requires MSVC
- if(MSVC)
+ if (MSVC)
# Create the C++/CLI wrapper library (only compile the wrapper with /clr)
add_library(Subvision SHARED cli_wrapper.cpp)
target_include_directories(Subvision PUBLIC ${CMAKE_SOURCE_DIR}/include)
@@ -68,45 +69,45 @@ if(BUILD_CLI_WRAPPER AND NOT EMSCRIPTEN)
# Disable C++ modules and standards that conflict with /clr
set_target_properties(Subvision PROPERTIES
- CXX_STANDARD 17
- CXX_SCAN_FOR_MODULES OFF
- COMMON_LANGUAGE_RUNTIME ""
- VS_DOTNET_TARGET_FRAMEWORK_VERSION "v4.7.2"
+ CXX_STANDARD 17
+ CXX_SCAN_FOR_MODULES OFF
+ COMMON_LANGUAGE_RUNTIME ""
+ VS_DOTNET_TARGET_FRAMEWORK_VERSION "v4.7.2"
)
# Enable C++/CLI only for the wrapper file
target_compile_options(Subvision PRIVATE
- /clr
- /EHa # Exception handling for C++/CLI
- /std:c++17 # C++/CLI works best with C++17
+ /clr
+ /EHa # Exception handling for C++/CLI
+ /std:c++17 # C++/CLI works best with C++17
)
# Set output name with architecture suffix
- if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+ if (CMAKE_SIZEOF_VOID_P EQUAL 8)
set(ARCH_SUFFIX "x64")
- else()
+ else ()
set(ARCH_SUFFIX "x86")
- endif()
+ endif ()
set_target_properties(Subvision PROPERTIES
- OUTPUT_NAME "subvision-${ARCH_SUFFIX}"
- RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${ARCH_SUFFIX}"
+ OUTPUT_NAME "subvision-${ARCH_SUFFIX}"
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${ARCH_SUFFIX}"
)
- else()
+ else ()
message(WARNING "C++/CLI wrapper requires MSVC compiler")
- endif()
-endif()
+ endif ()
+endif ()
# Configuration WebAssembly pour Emscripten
-if(EMSCRIPTEN)
+if (EMSCRIPTEN)
# Création de la cible WebAssembly
add_executable(subvision_wasm emscripten_binding.cpp)
target_link_libraries(subvision_wasm subvision_lib ${OpenCV_LIBS})
# Options spécifiques pour la cible WebAssembly
set_target_properties(subvision_wasm PROPERTIES
- LINK_FLAGS "-s EXPORT_ES6=1 -s MODULARIZE=1 -s ENVIRONMENT=web,worker -s USE_ES6_IMPORT_META=0 -s EXPORTED_FUNCTIONS=['_malloc','_free'] -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','stringToUTF8','UTF8ToString']")
+ LINK_FLAGS "-s EXPORT_ES6=1 -s MODULARIZE=1 -s ENVIRONMENT=web,worker -s USE_ES6_IMPORT_META=0 -s EXPORTED_FUNCTIONS=['_malloc','_free'] -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','stringToUTF8','UTF8ToString']")
# Générer un fichier HTML de test
configure_file(${CMAKE_SOURCE_DIR}/web/index.html ${CMAKE_BINARY_DIR}/index.html COPYONLY)
-endif()
\ No newline at end of file
+endif ()
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 0995b3f..6398046 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,8 @@ LIB_SOURCES = src/utils.cpp \
src/image_processing.cpp \
src/target_detection.cpp \
src/impact_detection.cpp \
- src/sheet_detection.cpp
+ src/sheet_detection.cpp \
+ src/logging.cpp
# Options de compilation emscripten
EMCC_FLAGS = -O3 -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 \
diff --git a/cli_wrapper.cpp b/cli_wrapper.cpp
index 45763f6..2945d81 100644
--- a/cli_wrapper.cpp
+++ b/cli_wrapper.cpp
@@ -1,6 +1,7 @@
#include "include/types.h"
#include "include/impact_detection.h"
#include "include/sheet_detection.h"
+#include "include/logging.h"
#include
using namespace System;
@@ -131,5 +132,11 @@ namespace SubvisionNET {
return managedPoints;
}
+
+ // Enable or disable logging
+ // enabled: true to enable logging, false to disable
+ static void SetLoggingEnabled(bool enabled) {
+ subvision::setLoggingEnabled(enabled);
+ }
};
}
diff --git a/emscripten_binding.cpp b/emscripten_binding.cpp
index cd01dd1..184e7f8 100644
--- a/emscripten_binding.cpp
+++ b/emscripten_binding.cpp
@@ -1,8 +1,10 @@
#include
#include
+#include
#include "include/types.h"
#include "include/impact_detection.h"
#include "include/sheet_detection.h"
+#include "include/logging.h"
using namespace emscripten;
@@ -34,13 +36,13 @@ struct JSImpactResults {
template
val getSheetCoordinates(int width, int height, const val &typedArray) {
- std::cout << "Start processing getSheetCoordinates with width: " << width << ", height: " << height << std::endl;
+ subvision::log("Start processing getSheetCoordinates with width: " + std::to_string(width) + ", height: " + std::to_string(height));
std::vector vec = convertJSArrayToNumberVector(typedArray);
- std::cout << "Vector size: " << vec.size() << std::endl;
+ subvision::log("Vector size: " + std::to_string(vec.size()));
cv::Mat mat(height, width, CV_8UC4, vec.data());
cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR);
- std::cout << "Processing getSheetCoordinates with width: " << width << ", height: " << height << std::endl;
+ subvision::log("Processing getSheetCoordinates with width: " + std::to_string(width) + ", height: " + std::to_string(height));
auto points = subvision::getSheetCoordinates(mat);
val jsArray = val::array();
@@ -64,7 +66,7 @@ JSImpactResults processTargetImage(int width, int height, const val &typedArray)
std::vector vec = convertJSArrayToNumberVector(typedArray);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour vecFromJSArray: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour vecFromJSArray: " + std::to_string(elapsed.count()) + " secondes");
cv::Mat mat(height, width, CV_8UC4, vec.data());
cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR);
@@ -122,4 +124,5 @@ EMSCRIPTEN_BINDINGS (subvision_module) {
function("processTargetImage", &processTargetImage);
function("getSheetCoordinates", &getSheetCoordinates);
+ function("setLoggingEnabled", &subvision::setLoggingEnabled);
}
diff --git a/include/logging.h b/include/logging.h
new file mode 100644
index 0000000..b3b6183
--- /dev/null
+++ b/include/logging.h
@@ -0,0 +1,11 @@
+#pragma once
+#include
+
+namespace subvision {
+
+ extern bool g_loggingEnabled;
+
+ void setLoggingEnabled(bool enabled);
+ void log(const std::string &msg);
+
+}
diff --git a/src/image_processing.cpp b/src/image_processing.cpp
index 71ceefa..b008c60 100644
--- a/src/image_processing.cpp
+++ b/src/image_processing.cpp
@@ -1,10 +1,12 @@
#include "../include/image_processing.h"
+
+#include "../include/logging.h"
#include "../include/constants.h"
#include "../include/utils.h"
namespace subvision {
std::vector getBiggestValidContour(const std::vector > &contours) {
- std::cout << "Start processing getBiggestValidContour with " << contours.size() << " contours" << std::endl;
+ subvision::log("Start processing getBiggestValidContour with " + std::to_string(contours.size()) + " contours");
std::vector biggestContour;
double biggestArea = 0;
constexpr double totalArea = PICTURE_WIDTH_SHEET_DETECTION * PICTURE_HEIGHT_SHEET_DETECTION;
@@ -18,7 +20,7 @@ namespace subvision {
approx.reserve(4);
for (const auto &contour: contours) {
- std::cout << "Processing contour with size: " << contour.size() << std::endl;
+ subvision::log("Processing contour with size: " + std::to_string(contour.size()));
if (contour.size() < 4)
continue;
@@ -74,8 +76,7 @@ namespace subvision {
biggestArea = area;
}
- std::cout << "Biggest contour found with size: " << biggestContour.size() << std::endl;
-
+ subvision::log( "Biggest contour found with size: " + std::to_string(biggestContour.size()));
return biggestContour;
}
@@ -121,7 +122,7 @@ namespace subvision {
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour getImpactsMask: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour getImpactsMask: " + std::to_string(elapsed.count()) + " secondes");
return result;
}
@@ -145,7 +146,7 @@ namespace subvision {
}
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour getImpactsCoordinates: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour getImpactsCoordinates: " + std::to_string(elapsed.count()) + " secondes");
return centers;
}
@@ -194,7 +195,7 @@ namespace subvision {
const cv::RotatedRect rotatedRect = fitEllipse(biggestContour);
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour retrieveEllipse: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour retrieveEllipse: " + std::to_string(elapsed.count()) + " secondes");
return std::make_tuple(rotatedRect.center, rotatedRect.size, rotatedRect.angle);
}
@@ -208,13 +209,13 @@ namespace subvision {
const cv::RotatedRect rotatedRect = fitEllipse(ptsEdges);
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour retrieveEllipse: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour retrieveEllipse: " + std::to_string(elapsed.count()) + " secondes");
return std::make_tuple(rotatedRect.center, rotatedRect.size, rotatedRect.angle);
}
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour retrieveEllipse: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour retrieveEllipse: " + std::to_string(elapsed.count()) + " secondes");
return emptyEllipse;
}
}
diff --git a/src/impact_detection.cpp b/src/impact_detection.cpp
index 545468d..4031bba 100644
--- a/src/impact_detection.cpp
+++ b/src/impact_detection.cpp
@@ -1,5 +1,6 @@
#include "../include/impact_detection.h"
+#include "../include/logging.h"
#include "sheet_detection.h"
#include "../include/constants.h"
#include "../include/utils.h"
@@ -71,7 +72,7 @@ namespace subvision {
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour drawAndGetImpactsPoints: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour drawAndGetImpactsPoints: " + std::to_string(elapsed.count()) + " secondes");
return points;
}
diff --git a/src/logging.cpp b/src/logging.cpp
new file mode 100644
index 0000000..413bdd1
--- /dev/null
+++ b/src/logging.cpp
@@ -0,0 +1,26 @@
+#include "../include/logging.h"
+#include
+
+bool subvision::g_loggingEnabled = false; // default: enabled
+
+#if defined(__EMSCRIPTEN__)
+#include
+#endif
+
+void subvision::setLoggingEnabled(bool enabled) {
+ g_loggingEnabled = enabled;
+}
+
+void subvision::log(const std::string &msg) {
+ if (!g_loggingEnabled) return; // ZERO COST WHEN DISABLED
+
+#if defined(__EMSCRIPTEN__)
+ emscripten_log(EM_LOG_CONSOLE, "%s", msg.c_str());
+
+#elif defined(_MANAGED) || defined(__CLR_VER)
+ System::Console::WriteLine(gcnew System::String(msg.c_str()));
+
+#else
+ std::cout << msg << std::endl;
+#endif
+}
diff --git a/src/sheet_detection.cpp b/src/sheet_detection.cpp
index bc472ac..b553e8a 100644
--- a/src/sheet_detection.cpp
+++ b/src/sheet_detection.cpp
@@ -9,8 +9,8 @@
#include "constants.h"
#include "image_processing.h"
#include "utils.h"
+#include "../include/logging.h"
using namespace cv;
-using namespace std;
namespace subvision {
@@ -37,22 +37,22 @@ namespace subvision {
Mat mask;
inRange(light, cv::Scalar(minVal), cv::Scalar(maxVal), mask);
- std::cout << "Start find contours" << std::endl;
+ subvision::log("Start find contours");
std::vector> contours;
findContours(mask, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
- std::cout << "End find contours" << std::endl;
+ subvision::log("End find contours");
const auto biggest = getBiggestValidContour(contours);
- std::cout << "Biggest contour size: " << biggest.size() << std::endl;
+ subvision::log("Biggest contour size: " + std::to_string(biggest.size()));
if (biggest.empty()) {
- cout << "No valid contour found" << endl;
+ subvision::log("No valid contour found");
throw std::runtime_error("No valid contour found");
}
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour getSheetCoordinates: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour getSheetCoordinates: " + std::to_string(elapsed.count()) + " secondes");
return coordinatesToPercentage(biggest, PICTURE_WIDTH_SHEET_DETECTION, PICTURE_HEIGHT_SHEET_DETECTION);
}
diff --git a/src/target_detection.cpp b/src/target_detection.cpp
index c8af585..913810f 100644
--- a/src/target_detection.cpp
+++ b/src/target_detection.cpp
@@ -3,6 +3,7 @@
#include
#include "../include/constants.h"
+#include "../include/logging.h"
#include "../include/utils.h"
#include "../include/image_processing.h"
@@ -98,7 +99,7 @@ namespace subvision {
const auto end = std::chrono::high_resolution_clock::now();
const std::chrono::duration elapsed = end - start;
- std::cout << "Temps écoulé pour getTargetEllipse: " << elapsed.count() << " secondes" << std::endl;
+ subvision::log("Temps écoulé pour getTargetEllipse: " + std::to_string(elapsed.count()) + " secondes");
return ellipse;
}
diff --git a/src/utils.cpp b/src/utils.cpp
index 7dac96b..b5e627c 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -1,5 +1,6 @@
#include "../include/utils.h"
#include "../include/constants.h"
+#include "../include/logging.h"
namespace subvision {
@@ -132,7 +133,7 @@ namespace subvision {
static_cast(coordinate.y) * invHeight
);
}
- std::cout << "Converted " << percentageCoordinates.size() << " coordinates to percentage." << std::endl;
+ subvision::log("Converted " + std::to_string(percentageCoordinates.size()) + " coordinates to percentage.");
return percentageCoordinates;
}
diff --git a/test/EllipseDetectionTest.cpp b/test/EllipseDetectionTest.cpp
index 6e40024..8b55de2 100644
--- a/test/EllipseDetectionTest.cpp
+++ b/test/EllipseDetectionTest.cpp
@@ -1,12 +1,10 @@
#include
-#include
#include
-#include
#include
#include
#include "../include/constants.h"
-#include "../include/image_processing.h"
#include "../include/target_detection.h"
+#include "../include/logging.h"
namespace fs = std::filesystem;
@@ -50,11 +48,11 @@ class EllipseDetectionTests : public ::testing::Test {
};
TEST_F(EllipseDetectionTests, TestEllipsesDetection) {
- std::cout << "Looking for resources in: " << TESTS_RESOURCES_PATH << std::endl;
- std::cout << "Current path: " << fs::current_path() << std::endl;
-
+ subvision::log("Looking for resources in: " + TESTS_RESOURCES_PATH);
+ subvision::log("Current path: " + fs::current_path().string());
+
if (!fs::exists(TESTS_RESOURCES_PATH)) {
- std::cout << "Resources directory does not exist!" << std::endl;
+ subvision::log("Resources directory does not exist!");
}
int pictureCount = 0;
@@ -68,5 +66,5 @@ TEST_F(EllipseDetectionTests, TestEllipsesDetection) {
}
}
}
- std::cout << "Ellipse Detection: Tested " << pictureCount << " pictures" << std::endl;
+ subvision::log("Ellipse Detection: Tested " + std::to_string(pictureCount) + " pictures");
}
diff --git a/test/ImpactDetectionTest.cpp b/test/ImpactDetectionTest.cpp
index b253ca2..0d620b6 100644
--- a/test/ImpactDetectionTest.cpp
+++ b/test/ImpactDetectionTest.cpp
@@ -1,5 +1,4 @@
#include
-#include
#include
#include
#include
@@ -7,8 +6,8 @@
#include
#include "../include/constants.h"
#include "../include/image_processing.h"
-#include "../include/target_detection.h"
#include "../include/utils.h"
+#include "../include/logging.h"
namespace fs = std::filesystem;
@@ -68,11 +67,11 @@ class ImpactDetectionTests : public ::testing::Test {
};
TEST_F(ImpactDetectionTests, TestImpactsDetection) {
- std::cout << "Looking for resources in: " << TESTS_RESOURCES_PATH << std::endl;
- std::cout << "Current path: " << fs::current_path() << std::endl;
-
+ subvision::log("Looking for resources in: " + TESTS_RESOURCES_PATH);
+ subvision::log("Current path: " + fs::current_path().string());
+
if (!fs::exists(TESTS_RESOURCES_PATH)) {
- std::cout << "Resources directory does not exist!" << std::endl;
+ subvision::log("Resources directory does not exist!");
}
int pictureCount = 0;
@@ -86,5 +85,5 @@ TEST_F(ImpactDetectionTests, TestImpactsDetection) {
}
}
}
- std::cout << "Impact Detection: Tested " << pictureCount << " pictures" << std::endl;
+ subvision::log("Impact Detection: Tested " + std::to_string(pictureCount) + " pictures");
}
From 5f14f96be71fb837fd20eeabb73394620831f7b7 Mon Sep 17 00:00:00 2001
From: Paul Coignac <61156794+DayUx@users.noreply.github.com>
Date: Sun, 5 Apr 2026 22:04:28 +0200
Subject: [PATCH 3/4] Features/Manual Crop & Docs & x64 build improvements (#8)
* Feature: Implement custom logging system and integrate into existing code
* Feature: Add logging source file to Makefile for custom logging system
* Feature: Include logging header in cli_wrapper.cpp for custom logging integration
* Feature: Add manual cropping functionality for sheet detection with coordinates
* Feature: Rename cli_wrapper.cpp to cli_wrapper.cs for C++/CLI compatibility
* Feature: Update CMakeLists and Makefile for C++/CLI wrapper build and add build dir to .gitignore
* Feature: Enhance ProcessTargetImage method with input validation and improved color conversion
* Feature: Refactor ProcessTargetImage method to improve image data handling and color conversion
* Feature: Improve ProcessTargetImage method with input validation and dynamic channel handling
* Add a validation test and improve impacts detection
* test
* Feature: Refactor workflow configuration and enhance image processing tests
* Fix getPoint on ellipse
* feat: Generate comprehensive C++ documentation, and establish CI build.
* feat: add multi-architecture C++/CLI wrapper support and NuGet packaging pipeline
* refactor: update CMake configuration to improve C++/CLI build compatibility and dynamic OpenCV DLL discovery
* fix github action
* fix github action build
* fix github action and remove build directory from repo
* fix github action
* fix wasm build
* fix github action wasm
* fix github action
* fix nuget packaging
* fix tests
* chore: include README.md in NuGet package metadata
* fix merge issue
---
.github/workflows/build-subvision-core.yml | 178 ++++++--
.gitignore | 9 +-
.idea/editor.xml | 3 +-
.idea/subvision-cv.iml | 2 +-
CMakeLists.txt | 76 +++-
Makefile | 81 +++-
README.md | 22 +-
Subvision.nuspec | 35 ++
Subvision.targets | 29 ++
cli_wrapper.cpp | 502 ++++++++++++++++-----
docs/Doxyfile | 106 +++++
docs/docfx.json | 51 +++
docs/index.md | 84 ++++
docs/manual/architecture.md | 206 +++++++++
docs/manual/getting-started.md | 181 ++++++++
docs/manual/index.md | 44 ++
docs/manual/javascript.md | 271 +++++++++++
docs/toc.yml | 17 +
emscripten_binding.cpp | 368 ++++++++++-----
include/constants.h | 68 ++-
include/image_processing.h | 98 +++-
include/impact_detection.h | 88 +++-
include/logging.h | 48 +-
include/sheet_detection.h | 71 ++-
include/subvision_cv.h | 30 +-
include/target_detection.h | 107 ++++-
include/types.h | 68 ++-
include/utils.h | 248 ++++++++--
resources/10/cropped_sheet.jpg | Bin 0 -> 1171683 bytes
resources/10/expected_impacts.jpg | Bin 0 -> 86161 bytes
resources/10/expected_sheet.jpg | Bin 0 -> 368752 bytes
resources/10/expected_visuals.jpg | Bin 0 -> 125287 bytes
resources/10/image.jpg | Bin 0 -> 12036474 bytes
resources/9/cropped_sheet.jpg | Bin 0 -> 640944 bytes
resources/9/expected_impacts.jpg | Bin 0 -> 29090 bytes
resources/9/expected_sheet.jpg | Bin 0 -> 117232 bytes
resources/9/expected_visuals.jpg | Bin 0 -> 51335 bytes
resources/9/image.jpg | Bin 0 -> 4046116 bytes
resources/README.md | 1 +
scripts/build-docs.sh | 99 ++++
src/image_processing.cpp | 475 +++++++++++--------
src/impact_detection.cpp | 57 ++-
src/sheet_detection.cpp | 19 +-
src/target_detection.cpp | 43 +-
src/utils.cpp | 10 +-
test/EllipseDetectionTest.cpp | 84 +++-
test/ImpactDetectionTest.cpp | 42 +-
web/index.html | 208 ++++-----
48 files changed, 3346 insertions(+), 783 deletions(-)
create mode 100644 Subvision.nuspec
create mode 100644 Subvision.targets
create mode 100644 docs/Doxyfile
create mode 100644 docs/docfx.json
create mode 100644 docs/index.md
create mode 100644 docs/manual/architecture.md
create mode 100644 docs/manual/getting-started.md
create mode 100644 docs/manual/index.md
create mode 100644 docs/manual/javascript.md
create mode 100644 docs/toc.yml
create mode 100644 resources/10/cropped_sheet.jpg
create mode 100644 resources/10/expected_impacts.jpg
create mode 100644 resources/10/expected_sheet.jpg
create mode 100644 resources/10/expected_visuals.jpg
create mode 100644 resources/10/image.jpg
create mode 100644 resources/9/cropped_sheet.jpg
create mode 100644 resources/9/expected_impacts.jpg
create mode 100644 resources/9/expected_sheet.jpg
create mode 100644 resources/9/expected_visuals.jpg
create mode 100644 resources/9/image.jpg
create mode 100644 scripts/build-docs.sh
diff --git a/.github/workflows/build-subvision-core.yml b/.github/workflows/build-subvision-core.yml
index 5b9d7da..457a009 100644
--- a/.github/workflows/build-subvision-core.yml
+++ b/.github/workflows/build-subvision-core.yml
@@ -2,9 +2,9 @@ name: Build and Release Subvision Core
on:
push:
- branches: []
+ branches: [ ]
pull_request:
- branches: []
+ branches: [ ]
workflow_dispatch:
inputs:
release_notes:
@@ -12,6 +12,23 @@ on:
required: false
jobs:
+ get-version:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ pre-release-label: ${{ steps.version.outputs.pre-release-label }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Get version
+ uses: reecetech/version-increment@2023.10.2
+ id: version
+ with:
+ scheme: calver
+ increment: patch
+
test:
runs-on: ubuntu-latest
steps:
@@ -33,7 +50,7 @@ jobs:
./subvision_tests
build-wasm:
- needs: test
+ # needs: test
runs-on: ubuntu-latest
permissions:
contents: write
@@ -53,10 +70,19 @@ jobs:
build_wasm/subvision.mjs
build-dotnet:
- needs: test
+ # needs: test
runs-on: windows-latest
permissions:
contents: write
+ strategy:
+ fail-fast: false
+ matrix:
+ arch: [x64]
+ include:
+ - arch: x64
+ cmake_arch: x64
+ rid: win-x64
+ suffix: x64
steps:
- name: Checkout repository
uses: actions/checkout@v3
@@ -69,39 +95,75 @@ jobs:
choco install opencv -y
echo "OpenCV_DIR=C:\tools\opencv\build" >> $env:GITHUB_ENV
- - name: Build C++/CLI .NET Library (x64)
+ - name: Build C++/CLI .NET Library (${{ matrix.arch }})
run: |
- mkdir build-dotnet-x64
- cd build-dotnet-x64
- cmake -G "Visual Studio 17 2022" -A x64 -DBUILD_CLI_WRAPPER=ON ..
+ if (Test-Path build-dotnet-${{ matrix.suffix }}) { Remove-Item -Recurse -Force build-dotnet-${{ matrix.suffix }} }
+ New-Item -ItemType Directory -Force -Path build-dotnet-${{ matrix.suffix }}
+ cd build-dotnet-${{ matrix.suffix }}
+ cmake -G "Visual Studio 17 2022" -A ${{ matrix.cmake_arch }} -DBUILD_CLI_WRAPPER=ON ..
cmake --build . --config Release
- - name: Upload .NET artifacts
+ - name: Upload .NET artifacts (${{ matrix.arch }})
uses: actions/upload-artifact@v4
with:
- name: dotnet-artifacts-x64
+ name: dotnet-artifacts-${{ matrix.suffix }}
path: |
- build-dotnet-x64/bin/**/*.dll
- build-dotnet-x64/bin/**/*.pdb
+ build-dotnet-${{ matrix.suffix }}/bin/**/*.dll
+ build-dotnet-${{ matrix.suffix }}/bin/**/*.pdb
- create-release:
- needs: [build-wasm, build-dotnet]
- runs-on: ubuntu-latest
+ package-nuget:
+ needs: [build-dotnet, get-version]
+ runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- - name: Get next version
- uses: reecetech/version-increment@2023.10.2
- id: version
+ - name: Download x64 artifacts
+ uses: actions/download-artifact@v4
with:
- scheme: calver
- increment: patch
+ name: dotnet-artifacts-x64
+ path: ./nupkg/runtimes/win-x64/native
+
+ - name: Flatten artifact directories
+ shell: pwsh
+ run: |
+ # Flatten nested bin//Release/*.dll to runtimes//native/
+ foreach ($rid in @("win-x64")) {
+ $nativeDir = "./nupkg/runtimes/$rid/native"
+ Get-ChildItem -Path $nativeDir -Recurse -Filter "*.dll" | ForEach-Object {
+ if ($_.DirectoryName -ne (Resolve-Path $nativeDir).Path) {
+ Move-Item $_.FullName -Destination $nativeDir -Force
+ }
+ }
+ # Clean up empty subdirectories
+ Get-ChildItem -Path $nativeDir -Directory -Recurse | Sort-Object FullName -Descending | Remove-Item -Force -ErrorAction SilentlyContinue
+ }
+
+ - name: Build NuGet package
+ shell: pwsh
+ run: |
+ Copy-Item Subvision.targets ./nupkg/
+ nuget pack Subvision.nuspec -OutputDirectory ./nupkg -Version ${{ needs.get-version.outputs.version }} -BasePath ./nupkg
+
+ - name: Upload NuGet package
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-package
+ path: ./nupkg/*.nupkg
+
+ create-release:
+ needs: [ build-wasm, build-dotnet, package-nuget, get-version ]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
- name: Print version
- run: echo "Version is ${{ steps.version.outputs.version }}"
+ run: echo "Version is ${{ needs.get-version.outputs.version }}"
- name: Download WASM artifacts
uses: actions/download-artifact@v4
@@ -113,7 +175,13 @@ jobs:
uses: actions/download-artifact@v4
with:
name: dotnet-artifacts-x64
- path: ./dotnet-artifacts
+ path: ./dotnet-artifacts-x64
+
+ - name: Download NuGet package
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-package
+ path: ./nuget-package
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
@@ -125,14 +193,68 @@ jobs:
startsWith(github.ref, 'refs/heads/develop')
)
with:
- tag_name: ${{ steps.version.outputs.version }}
- name: ${{ steps.version.outputs.version }}
+ tag_name: ${{ needs.get-version.outputs.version }}
+ name: ${{ needs.get-version.outputs.version }}
body: ${{ inputs.release_notes }}
- prerelease : ${{ steps.version.outputs.pre-release-label != '' }}
- make_latest: ${{ steps.version.outputs.pre-release-label == '' }}
+ prerelease: ${{ needs.get-version.outputs.pre-release-label != '' }}
+ make_latest: ${{ needs.get-version.outputs.pre-release-label == '' }}
files: |
wasm-artifacts/subvision.js
wasm-artifacts/subvision.mjs
- dotnet-artifacts/**/*.dll
+ dotnet-artifacts-x64/**/*.dll
+ nuget-package/*.nupkg
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ build-docs:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Install Doxygen
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y doxygen
+
+ - name: Create output directories
+ run: mkdir -p docs/cpp/html
+
+ - name: Generate C++ API docs (Doxygen)
+ run: |
+ cd docs
+ doxygen Doxyfile
+
+ - name: Install DocFX
+ run: |
+ dotnet tool install -g docfx || true
+ echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
+
+ - name: Build documentation site (DocFX)
+ run: |
+ cd docs
+ docfx build docfx.json || echo "DocFX build completed (metadata step skipped — no .NET project present)"
+
+ - name: Upload docs artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: documentation
+ path: docs/_site
+
+ - name: Setup GitHub Pages
+ if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
+ uses: actions/configure-pages@v4
+
+ - name: Upload to GitHub Pages
+ if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: docs/_site
+
+ - name: Deploy to GitHub Pages
+ if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
+ uses: actions/deploy-pages@v4
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3bde07b..c7574b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,11 @@
/build_wasm/
/build/
.vscode/
-.idea
\ No newline at end of file
+.idea
+/build-dotnet-x64
+/build-dotnet-arm64
+
+# Generated documentation output
+docs/cpp/html/
+docs/_site/
+docs/api/
\ No newline at end of file
diff --git a/.idea/editor.xml b/.idea/editor.xml
index 198c798..933d1dd 100644
--- a/.idea/editor.xml
+++ b/.idea/editor.xml
@@ -17,7 +17,7 @@
-
+
@@ -56,6 +56,7 @@
+
diff --git a/.idea/subvision-cv.iml b/.idea/subvision-cv.iml
index f08604b..4c94235 100644
--- a/.idea/subvision-cv.iml
+++ b/.idea/subvision-cv.iml
@@ -1,2 +1,2 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 236f71a..938532d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.30.5)
+cmake_minimum_required(VERSION 3.27.7)
project(subvision_core)
@@ -60,6 +60,24 @@ endif ()
if (BUILD_CLI_WRAPPER AND NOT EMSCRIPTEN)
# C++/CLI requires MSVC
if (MSVC)
+ # --- Architecture detection (x64, ARM64, x86) ---
+ if (CMAKE_GENERATOR_PLATFORM)
+ string(TOUPPER "${CMAKE_GENERATOR_PLATFORM}" _PLAT)
+ elseif (CMAKE_VS_PLATFORM_NAME)
+ string(TOUPPER "${CMAKE_VS_PLATFORM_NAME}" _PLAT)
+ else ()
+ string(TOUPPER "${CMAKE_SYSTEM_PROCESSOR}" _PLAT)
+ endif ()
+
+ if (_PLAT MATCHES "ARM64")
+ set(ARCH_SUFFIX "arm64")
+ elseif (_PLAT MATCHES "X64" OR _PLAT MATCHES "AMD64" OR CMAKE_SIZEOF_VOID_P EQUAL 8)
+ set(ARCH_SUFFIX "x64")
+ else ()
+ set(ARCH_SUFFIX "x86")
+ endif ()
+ message(STATUS "Building C++/CLI wrapper for architecture: ${ARCH_SUFFIX}")
+
# Create the C++/CLI wrapper library (only compile the wrapper with /clr)
add_library(Subvision SHARED cli_wrapper.cpp)
target_include_directories(Subvision PUBLIC ${CMAKE_SOURCE_DIR}/include)
@@ -77,22 +95,60 @@ if (BUILD_CLI_WRAPPER AND NOT EMSCRIPTEN)
# Enable C++/CLI only for the wrapper file
target_compile_options(Subvision PRIVATE
- /clr
- /EHa # Exception handling for C++/CLI
- /std:c++17 # C++/CLI works best with C++17
+ /EHa # Exception handling for C++/CLI (clr added via COMMON_LANGUAGE_RUNTIME)
)
- # Set output name with architecture suffix
- if (CMAKE_SIZEOF_VOID_P EQUAL 8)
- set(ARCH_SUFFIX "x64")
- else ()
- set(ARCH_SUFFIX "x86")
- endif ()
+ # NOTE: C++/CLI (/clr) requires dynamic CRT (/MD). Static CRT (/MT) is NOT
+ # compatible. The VC++ Redistributable is required on target machines, but it
+ # is already installed on the vast majority of Windows machines.
+ # Set output name with architecture suffix
set_target_properties(Subvision PROPERTIES
OUTPUT_NAME "subvision-${ARCH_SUFFIX}"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${ARCH_SUFFIX}"
)
+
+ # --- Post-build: copy OpenCV DLLs next to the wrapper for distribution ---
+ # OpenCV_DIR is set by find_package(OpenCV).
+ set(_OPENCV_BIN_DIR "")
+
+ # 1. Try OpenCV_DIR / ARCH / RUNTIME / bin (typical for OpenCV root)
+ if (DEFINED OpenCV_ARCH AND DEFINED OpenCV_RUNTIME)
+ set(_TEST_DIR "${OpenCV_DIR}/${OpenCV_ARCH}/${OpenCV_RUNTIME}/bin")
+ if (EXISTS "${_TEST_DIR}")
+ set(_OPENCV_BIN_DIR "${_TEST_DIR}")
+ endif ()
+ endif ()
+
+ # 2. Try OpenCV_DIR / ../bin (typical if OpenCV_DIR is in the lib folder)
+ if (NOT _OPENCV_BIN_DIR OR NOT EXISTS "${_OPENCV_BIN_DIR}")
+ get_filename_component(_TEST_DIR "${OpenCV_DIR}/../bin" ABSOLUTE)
+ if (EXISTS "${_TEST_DIR}")
+ set(_OPENCV_BIN_DIR "${_TEST_DIR}")
+ endif ()
+ endif ()
+
+ # 3. Fallback to standard choco path if everything else fails
+ if (NOT _OPENCV_BIN_DIR OR NOT EXISTS "${_OPENCV_BIN_DIR}")
+ set(_OPENCV_BIN_DIR "C:/tools/opencv/build/x64/vc16/bin")
+ endif ()
+
+ file(GLOB OPENCV_DLLS "${_OPENCV_BIN_DIR}/opencv_*.dll")
+ if (OPENCV_DLLS)
+ message(STATUS "Found OpenCV DLLs in: ${_OPENCV_BIN_DIR}")
+ add_custom_command(TARGET Subvision POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E echo "Copying OpenCV DLLs to output directory..."
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ ${OPENCV_DLLS}
+ "$/"
+ COMMAND ${CMAKE_COMMAND} -E echo "OpenCV DLLs copied successfully."
+ COMMENT "Bundling OpenCV DLLs with Subvision wrapper"
+ VERBATIM
+ )
+ else ()
+ message(WARNING "Could not find OpenCV DLLs (opencv_*.dll) in ${_OPENCV_BIN_DIR}. "
+ "You will need to manually place the OpenCV binaries next to subvision-${ARCH_SUFFIX}.dll")
+ endif ()
else ()
message(WARNING "C++/CLI wrapper requires MSVC compiler")
endif ()
diff --git a/Makefile b/Makefile
index 6398046..074847c 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ LIB_SOURCES = src/utils.cpp \
src/logging.cpp
# Options de compilation emscripten
-EMCC_FLAGS = -O3 -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 \
+EMCC_FLAGS = -O3 -std=c++20 -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s USE_ZLIB=1 \
-s MODULARIZE=1 -s ENVIRONMENT=web,worker \
-s DISABLE_EXCEPTION_CATCHING=0 -s SINGLE_FILE \
-s USE_ES6_IMPORT_META=0 -s NO_EXIT_RUNTIME=1 \
@@ -33,7 +33,7 @@ $(OUTPUT_DIR):
# Compilation de la bibliothèque et du binding WebAssembly
$(OUTPUT_DIR)/subvision.js: $(OUTPUT_DIR)
- docker run --rm -v $${PWD}:/src -w /src $(DOCKER_IMAGE) bash -c "emcc $(LIB_SOURCES) emscripten_binding.cpp \
+ docker run --rm -v "$(CURDIR):/src" -w /src $(DOCKER_IMAGE) bash -c "emcc $(LIB_SOURCES) emscripten_binding.cpp \
-I./include \
\`pkg-config --cflags --libs opencv4\` \
-o $(OUTPUT_DIR)/subvision.js \
@@ -47,7 +47,7 @@ $(OUTPUT_DIR)/index.html: $(OUTPUT_DIR) web/index.html
# Compilation de l'application complète Subvision
subvision: $(OUTPUT_DIR)
@echo "Compilation de Subvision..."
- docker run --rm -v $${PWD}:/src -w /src $(DOCKER_IMAGE) bash -c "emcc $(LIB_SOURCES) emscripten_binding.cpp \
+ docker run --rm -v "$(CURDIR):/src" -w /src $(DOCKER_IMAGE) bash -c "emcc $(LIB_SOURCES) emscripten_binding.cpp \
-I./include \
\`pkg-config --cflags --libs opencv4\` \
-o $(OUTPUT_DIR)/subvision.js \
@@ -59,7 +59,7 @@ subvision: $(OUTPUT_DIR)
# Compilation de l'application complète Subvision
subvision_es6: $(OUTPUT_DIR)
@echo "Compilation de Subvision en mode ES6..."
- docker run --rm -v $${PWD}:/src -w /src $(DOCKER_IMAGE) bash -c "emcc $(LIB_SOURCES) emscripten_binding.cpp \
+ docker run --rm -v "$(CURDIR):/src" -w /src $(DOCKER_IMAGE) bash -c "emcc $(LIB_SOURCES) emscripten_binding.cpp \
-I./include \
\`pkg-config --cflags --libs opencv4\` \
-o $(OUTPUT_DIR)/subvision.mjs \
@@ -67,13 +67,78 @@ subvision_es6: $(OUTPUT_DIR)
--bind"
cp web/index.html $(OUTPUT_DIR)/
@echo "Subvision compilé avec succès. Les fichiers sont dans $(OUTPUT_DIR)/"
+
+# --- .NET wrapper builds ---
+
+# Build x64 wrapper
+subvision_dotnet: subvision_dotnet_x64
+
+subvision_dotnet_x64:
+ @echo "Building .NET wrapper (x64)..."
+ifeq ($(OS),Windows_NT)
+ @if not exist build-dotnet-x64 mkdir build-dotnet-x64
+ @cd build-dotnet-x64 && cmake -G "Visual Studio 17 2022" -A x64 -DBUILD_CLI_WRAPPER=ON ..
+ @cmake --build build-dotnet-x64 --config Release
+else
+ @echo "C++/CLI wrapper requires Windows with MSVC"
+ @exit 1
+endif
+ @echo "x64 artifacts in build-dotnet-x64/bin/x64/Release/"
+
+# Build ARM64 wrapper
+subvision_dotnet_arm64:
+ @echo "Building .NET wrapper (ARM64)..."
+ifeq ($(OS),Windows_NT)
+ @if not exist build-dotnet-arm64 mkdir build-dotnet-arm64
+ @cd build-dotnet-arm64 && cmake -G "Visual Studio 17 2022" -A ARM64 -DBUILD_CLI_WRAPPER=ON ..
+ @cmake --build build-dotnet-arm64 --config Release
+else
+ @echo "C++/CLI wrapper requires Windows with MSVC"
+ @exit 1
+endif
+ @echo "ARM64 artifacts in build-dotnet-arm64/bin/arm64/Release/"
+
+# Build all architectures
+subvision_dotnet_all: subvision_dotnet_x64 subvision_dotnet_arm64
+ @echo "All .NET wrapper architectures built successfully."
+
+# Package into NuGet
+subvision_nuget: subvision_dotnet_all
+ @echo "Creating NuGet package..."
+ifeq ($(OS),Windows_NT)
+ @if not exist nupkg mkdir nupkg
+ @if not exist nupkg\runtimes\win-x64\native mkdir nupkg\runtimes\win-x64\native
+ @copy build-dotnet-x64\bin\x64\Release\subvision-x64.dll nupkg\runtimes\win-x64\native\
+ @copy build-dotnet-x64\bin\x64\Release\opencv_*.dll nupkg\runtimes\win-x64\native\
+ @copy Subvision.nuspec nupkg\
+ @copy Subvision.targets nupkg\
+ nuget pack nupkg\Subvision.nuspec -OutputDirectory nupkg
+endif
+ @echo "NuGet package created in nupkg/"
+
+# Clean dotnet builds
+clean_dotnet:
+ @echo "Cleaning .NET build directories..."
+ifeq ($(OS),Windows_NT)
+ @if exist build-dotnet-x64 rmdir /s /q build-dotnet-x64
+ @if exist build-dotnet-arm64 rmdir /s /q build-dotnet-arm64
+ @if exist nupkg rmdir /s /q nupkg
+endif
+ @echo "Clean complete."
# Aide
help:
@echo "Makefile pour compiler Subvision avec Emscripten via Docker"
@echo ""
@echo "Cibles disponibles:"
- @echo " all : Compile le projet complet (Subvision et Subvision ES6)"
- @echo " subvision : Compile l'application Subvision complète"
- @echo " subvision_es6 : Compile l'application Subvision en mode ES6"
- @echo " help : Affiche cette aide"
+ @echo " all : Compile le projet complet (Subvision et Subvision ES6)"
+ @echo " subvision : Compile l'application Subvision complète"
+ @echo " subvision_es6 : Compile l'application Subvision en mode ES6"
+ @echo " subvision_dotnet : Build .NET wrapper (x64, alias for subvision_dotnet_x64)"
+ @echo " subvision_dotnet_x64 : Build .NET wrapper for x64"
+ @echo " subvision_dotnet_arm64 : Build .NET wrapper for ARM64"
+ @echo " subvision_dotnet_all : Build .NET wrapper for all architectures"
+ @echo " subvision_nuget : Build all + create NuGet package"
+ @echo " clean_dotnet : Clean all .NET build directories"
+ @echo " help : Affiche cette aide"
+
diff --git a/README.md b/README.md
index b359abe..a1d737a 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ It aims to be validated by the **FFESSM** (French Underwater Federation) for off
---
## 📁 Repository Structure
+
```
├── include/ # Header files
├── src/ # C++ core source files
@@ -41,10 +42,12 @@ It aims to be validated by the **FFESSM** (French Underwater Federation) for off
## ⚙️ Prerequisites
### For WebAssembly Build:
+
- [Docker](https://www.docker.com/)
- Internet access to pull the image `ghcr.io/subvision-soft/subvision-emscripten:2025.6.1`
### For .NET Build:
+
- Windows with Visual Studio 2022
- CMake 3.30.5+
- OpenCV 4.x (installed via chocolatey: `choco install opencv`)
@@ -102,6 +105,7 @@ cmake --build . --config Release
```
The output will be:
+
- `build-dotnet-x64\bin\x64\SubvisionNET-x64.dll` (64-bit)
- `build-dotnet-x86\bin\x86\SubvisionNET-x86.dll` (32-bit)
@@ -133,10 +137,10 @@ cd test
### .NET
-| File | Description |
-|------------------------|------------------------------------------|
-| SubvisionNET-x64.dll | .NET assembly for 64-bit applications |
-| SubvisionNET-x86.dll | .NET assembly for 32-bit applications |
+| File | Description |
+|----------------------|---------------------------------------|
+| SubvisionNET-x64.dll | .NET assembly for 64-bit applications |
+| SubvisionNET-x86.dll | .NET assembly for 32-bit applications |
---
@@ -192,11 +196,11 @@ var coords = SubvisionCore.GetSheetCoordinates(imageData, width, height);
### CMake Options
-| Option | Description | Default |
-|---------------------|---------------------------------|---------|
-| BUILD_TESTS | Build unit tests | ON |
-| BUILD_CLI_WRAPPER | Build C++/CLI .NET wrapper | OFF |
-| EMSCRIPTEN | Build for WebAssembly | OFF |
+| Option | Description | Default |
+|-------------------|----------------------------|---------|
+| BUILD_TESTS | Build unit tests | ON |
+| BUILD_CLI_WRAPPER | Build C++/CLI .NET wrapper | OFF |
+| EMSCRIPTEN | Build for WebAssembly | OFF |
## 📄 License
diff --git a/Subvision.nuspec b/Subvision.nuspec
new file mode 100644
index 0000000..a0e18ef
--- /dev/null
+++ b/Subvision.nuspec
@@ -0,0 +1,35 @@
+
+
+
+ SubvisionNET
+ $version$
+ Subvision CV .NET Wrapper
+ Subvision
+
+ .NET wrapper for the Subvision CV native library.
+ Provides impact detection and sheet coordinate extraction for
+ underwater target shooting sheet analysis.
+ Includes native binaries for Windows x64 and ARM64.
+
+ opencv computer-vision image-processing native interop
+ LICENSE
+ docs\README.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Subvision.targets b/Subvision.targets
new file mode 100644
index 0000000..d77300b
--- /dev/null
+++ b/Subvision.targets
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ <_SubvisionRID Condition="'$(PlatformTarget)' == 'ARM64' OR '$(RuntimeIdentifier)' == 'win-arm64'">win-arm64
+ <_SubvisionRID Condition="'$(_SubvisionRID)' == '' AND ('$(PlatformTarget)' == 'x64' OR '$(RuntimeIdentifier)' == 'win-x64')">win-x64
+
+ <_SubvisionRID Condition="'$(_SubvisionRID)' == ''">win-x64
+
+
+
+
+
+
+
+
diff --git a/cli_wrapper.cpp b/cli_wrapper.cpp
index 2945d81..688d9dd 100644
--- a/cli_wrapper.cpp
+++ b/cli_wrapper.cpp
@@ -1,142 +1,396 @@
-#include "include/types.h"
+/**
+ * @file cli_wrapper.cpp
+ * @brief C++/CLI wrapper exposing the Subvision CV library to .NET
+ * applications.
+ *
+ * Provides managed (.NET) classes that wrap the native C++ API, enabling
+ * C# and other .NET languages to use Subvision CV for impact detection
+ * and sheet coordinate extraction.
+ *
+ * ## Architecture
+ *
+ * The wrapper performs the following marshalling:
+ * - **Input**: Managed `byte[]` arrays (RGBA pixel data) → native `cv::Mat`
+ * (BGR)
+ * - **Output**: Native `subvision::ImpactResults` → managed `ImpactResults^`
+ * with copied image data and managed `Impact^` objects
+ * - **Coordinates**: Managed `List` ↔ native
+ * `std::vector`
+ *
+ * ## Memory Management
+ *
+ * - Input arrays are pinned (via `pin_ptr`) during processing to prevent GC
+ * relocation
+ * - Output image data is copied from native to managed heap via `Marshal::Copy`
+ * - Native cv::Mat objects are automatically freed when they go out of scope
+ *
+ * ## .NET Usage Example
+ *
+ * ```csharp
+ * using SubvisionNET;
+ *
+ * // Load an image as RGBA byte array
+ * byte[] imageData = LoadImageAsRGBA("target_sheet.jpg", out int width, out int
+ * height);
+ *
+ * // Detect impacts
+ * var results = SubvisionCore.ProcessTargetImage(imageData, width, height,
+ * null); foreach (var impact in results.Impacts)
+ * {
+ * Console.WriteLine($"Score: {impact.Score}, Distance:
+ * {impact.Distance}mm");
+ * }
+ *
+ * // Get sheet coordinates
+ * var coords = SubvisionCore.GetSheetCoordinates(imageData, width, height);
+ * foreach (var pt in coords)
+ * {
+ * Console.WriteLine($"Corner: ({pt.X}, {pt.Y})");
+ * }
+ * ```
+ */
+
#include "include/impact_detection.h"
-#include "include/sheet_detection.h"
#include "include/logging.h"
+#include "include/sheet_detection.h"
+#include "include/types.h"
#include
+
using namespace System;
using namespace System::Collections::Generic;
using namespace System::Runtime::InteropServices;
+///
+/// Root namespace for the Subvision .NET wrapper library.
+/// Contains managed classes that expose the native C++ computer vision
+/// API for underwater target shooting sheet analysis.
+///
namespace SubvisionNET {
- // .NET representation of Impact
- public ref class Impact {
- public:
- property int Distance;
- property int Score;
- property int Zone;
- property float Angle;
- property int Count;
-
- Impact(int distance, int score, int zone, float angle, int count) {
- Distance = distance;
- Score = score;
- Zone = zone;
- Angle = angle;
- Count = count;
- }
- };
+///
+/// Represents a single detected impact (shot) on a target.
+///
+///
+/// Maps to the native subvision::Impact struct.
+/// Each impact contains scoring information computed from its
+/// position relative to the target center.
+///
+public
+ref class Impact {
+public:
+ /// Distance from the target center in millimeters.
+ property int Distance;
- // .NET representation of Point2f
- public ref class Point2f {
- public:
- property float X;
- property float Y;
+ /// Computed score on the 0–570 federation scale.
+ property int Score;
- Point2f(float x, float y) {
- X = x;
- Y = y;
- }
- };
-
- // .NET representation of Impact Results
- public ref class ImpactResults {
- public:
- property array^ AnnotatedImageData;
- property int Width;
- property int Height;
- property int Channels;
- property List^ Impacts;
-
- ImpactResults() {
- Impacts = gcnew List();
+ /// Target zone identifier (0–4, or -1 for undefined).
+ property int Zone;
+
+ /// Angular position in degrees relative to target center.
+ property float Angle;
+
+ /// Number of impacts at this location (typically 1).
+ property int Count;
+
+ ///
+ /// Constructs an Impact with all scoring properties.
+ ///
+ /// Distance from center in millimeters.
+ /// Computed score value.
+ /// Target zone identifier.
+ /// Angular position in degrees.
+ /// Number of impacts.
+ Impact(int distance, int score, int zone, float angle, int count) {
+ Distance = distance;
+ Score = score;
+ Zone = zone;
+ Angle = angle;
+ Count = count;
+ }
+};
+
+///
+/// Represents a 2D point with floating-point coordinates.
+///
+///
+/// Maps to the native cv::Point2f. Used for sheet corner
+/// coordinates returned as normalised percentages in [0, 1] range.
+///
+public
+ref class Point2f {
+public:
+ /// X coordinate (normalised to [0, 1] for percentage
+ /// coordinates).
+ property float X;
+
+ /// Y coordinate (normalised to [0, 1] for percentage
+ /// coordinates).
+ property float Y;
+
+ ///
+ /// Constructs a Point2f with the given coordinates.
+ ///
+ /// X coordinate.
+ /// Y coordinate.
+ Point2f(float x, float y) {
+ X = x;
+ Y = y;
+ }
+};
+
+///
+/// Contains the results of impact detection processing.
+///
+///
+/// Maps to the native subvision::ImpactResults.
+/// The annotated image data is returned as an RGBA byte array
+/// that can be directly used to create a bitmap in .NET.
+/// Memory for the image data is managed by the .NET garbage collector.
+///
+public
+ref class ImpactResults {
+public:
+ /// RGBA pixel data of the annotated image.
+ ///
+ /// Array length = Width × Height × Channels.
+ /// The image has targets and impacts drawn on it.
+ ///
+ property array ^ AnnotatedImageData;
+
+ /// Width of the annotated image in pixels.
+ property int Width;
+
+ /// Height of the annotated image in pixels.
+ property int Height;
+
+ /// Number of colour channels (always 4 for RGBA).
+ property int Channels;
+
+ /// List of detected impacts with their scores.
+ property List ^ Impacts;
+
+ ///
+ /// Constructs an empty ImpactResults with an initialised Impacts list.
+ ///
+ ImpactResults() { Impacts = gcnew List(); }
+};
+
+///
+/// Main wrapper class exposing the Subvision CV native API to .NET.
+///
+///
+///
+/// All methods are static and thread-safe for independent calls.
+/// Image data must be provided as RGBA byte arrays.
+///
+///
+/// Native interop: Input arrays are pinned during processing.
+/// Output data is fully copied to managed memory — no native pointers
+/// are retained after the call returns.
+///
+///
+/// Mapping to C++ API:
+///
+/// - ProcessTargetImage → subvision::retrieveImpacts()
+/// - GetSheetCoordinates →
+/// subvision::getSheetCoordinates()
+/// - SetLoggingEnabled →
+/// subvision::setLoggingEnabled()
+///
+///
+///
+///
+///
+/// var results = SubvisionCore.ProcessTargetImage(imageData, width, height,
+/// null); foreach (var impact in results.Impacts)
+/// Console.WriteLine($"Score={impact.Score}");
+///
+///
+public
+ref class SubvisionCore {
+public:
+ ///
+ /// Process a target image to detect and score all impacts.
+ ///
+ ///
+ ///
+ /// This is the main entry point for impact detection from .NET.
+ /// The method performs the full Subvision pipeline: sheet detection,
+ /// perspective correction, target localisation, impact detection,
+ /// and scoring.
+ ///
+ ///
+ /// Colour conversion: The input RGBA data is converted to BGR
+ /// internally (OpenCV convention). Supports 1, 3, or 4 channel inputs.
+ ///
+ ///
+ /// Memory: The input array is pinned (not copied) during processing.
+ /// The output annotated image is fully copied to managed memory.
+ ///
+ ///
+ /// RGBA pixel data as a byte array.
+ /// Image width in pixels.
+ /// Image height in pixels.
+ /// Optional pre-computed sheet corner coordinates.
+ /// Pass null for automatic sheet detection.
+ /// ImpactResults with annotated image and impact list,
+ /// or null if input is invalid.
+ static ImpactResults ^
+ ProcessTargetImage(array ^ imageData, int width,
+ int height, List ^ coordinates) {
+ if (imageData == nullptr || width <= 0 || height <= 0)
+ return nullptr;
+
+ int length = imageData->Length;
+ if (length == 0)
+ return nullptr;
+
+ // Calcul du pas (bytes par ligne) et du nombre de canaux
+ int step = length / height;
+ if (step <= 0)
+ return nullptr;
+ int channels = step / width;
+ if (channels <= 0)
+ return nullptr;
+
+ int type;
+ if (channels == 1)
+ type = CV_8UC1;
+ else if (channels == 3)
+ type = CV_8UC3;
+ else if (channels == 4)
+ type = CV_8UC4;
+ else
+ return nullptr; // format non supporté
+
+ // log type
+ subvision::log("Image type detected: " + std::to_string(type) +
+ " with " + std::to_string(channels) +
+ " channels and step " + std::to_string(step));
+
+ // Pinner le tableau managé et construire une cv::Mat qui utilise ces
+ // données (avec step correct)
+ pin_ptr pinned = &imageData[0];
+ unsigned char *dataPtr = pinned;
+ cv::Mat mat(height, width, type, dataPtr, step);
+
+ // Convertir en BGR attendu par le pipeline natif
+ cv::Mat bgrMat;
+ if (channels == 4) {
+ cv::cvtColor(mat, bgrMat,
+ cv::COLOR_RGBA2BGR); // ajuster si vos données sont BGRA
+ } else if (channels == 3) {
+ cv::cvtColor(
+ mat, bgrMat,
+ cv::COLOR_RGB2BGR); // ajuster si vos données sont déjà BGR
+ } else // 1 canal
+ {
+ cv::cvtColor(mat, bgrMat, cv::COLOR_GRAY2BGR);
}
- };
-
- // Main wrapper class
- public ref class SubvisionCore {
- public:
- // Process target image and detect impacts
- // imageData: RGBA image data as byte array
- // width: image width
- // height: image height
- static ImpactResults^ ProcessTargetImage(array^ imageData, int width, int height) {
- // Convert managed array to native vector
- std::vector nativeData(imageData->Length);
- Marshal::Copy((array^)imageData, 0, IntPtr(nativeData.data()), imageData->Length);
-
- // Create OpenCV Mat from the data (RGBA format)
- cv::Mat mat(height, width, CV_8UC4, nativeData.data());
- cv::Mat bgrMat;
- cv::cvtColor(mat, bgrMat, cv::COLOR_RGBA2BGR);
-
- // Call native function
- subvision::ImpactResults nativeResults;
- bool success = subvision::retrieveImpacts(bgrMat, nativeResults);
-
- // Convert results to managed types
- ImpactResults^ managedResults = gcnew ImpactResults();
-
- if (success) {
- // Convert annotated image back to RGBA
- cv::Mat annotatedRGBA;
- cv::cvtColor(nativeResults.annotatedImage, annotatedRGBA, cv::COLOR_BGR2RGBA);
-
- // Copy image data to managed array
- int dataSize = annotatedRGBA.total() * annotatedRGBA.elemSize();
- managedResults->AnnotatedImageData = gcnew array(dataSize);
- Marshal::Copy(IntPtr(annotatedRGBA.data), managedResults->AnnotatedImageData, 0, dataSize);
- managedResults->Width = annotatedRGBA.cols;
- managedResults->Height = annotatedRGBA.rows;
- managedResults->Channels = annotatedRGBA.channels();
-
- // Convert impacts
- for (const auto& impact : nativeResults.impacts) {
- Impact^ managedImpact = gcnew Impact(
- impact.distance,
- impact.score,
- impact.zone,
- impact.angle,
- impact.count
- );
- managedResults->Impacts->Add(managedImpact);
- }
- }
-
- return managedResults;
+
+ // Convertir coordonnées managées -> natives
+ std::vector nativeCoords;
+ if (coordinates != nullptr && coordinates->Count > 0) {
+ nativeCoords.reserve(coordinates->Count);
+ for each (Point2f ^ p in coordinates) {
+ nativeCoords.emplace_back(p->X, p->Y);
+ }
}
- // Get sheet coordinates from image
- // imageData: RGBA image data as byte array
- // width: image width
- // height: image height
- static List^ GetSheetCoordinates(array^ imageData, int width, int height) {
- // Convert managed array to native vector
- std::vector nativeData(imageData->Length);
- Marshal::Copy((array^)imageData, 0, IntPtr(nativeData.data()), imageData->Length);
-
- // Create OpenCV Mat from the data (RGBA format)
- cv::Mat mat(height, width, CV_8UC4, nativeData.data());
- cv::Mat bgrMat;
- cv::cvtColor(mat, bgrMat, cv::COLOR_RGBA2BGR);
-
- // Call native function
- std::vector nativePoints = subvision::getSheetCoordinates(bgrMat);
-
- // Convert to managed list
- List^ managedPoints = gcnew List();
- for (const auto& pt : nativePoints) {
- managedPoints->Add(gcnew Point2f(pt.x, pt.y));
- }
-
- return managedPoints;
+ // Appel à la fonction native
+ subvision::ImpactResults nativeResults;
+ bool success =
+ subvision::retrieveImpacts(bgrMat, nativeResults, nativeCoords);
+
+ // Convertir résultats -> types managés
+ ImpactResults ^ managedResults = gcnew ImpactResults();
+ if (success) {
+ cv::Mat annotatedRGBA;
+ cv::cvtColor(nativeResults.annotatedImage, annotatedRGBA,
+ cv::COLOR_BGR2RGBA);
+
+ int dataSize = static_cast(annotatedRGBA.total() *
+ annotatedRGBA.elemSize());
+ managedResults->AnnotatedImageData =
+ gcnew array(dataSize);
+ Marshal::Copy(IntPtr(annotatedRGBA.data),
+ managedResults->AnnotatedImageData, 0, dataSize);
+ managedResults->Width = annotatedRGBA.cols;
+ managedResults->Height = annotatedRGBA.rows;
+ managedResults->Channels = annotatedRGBA.channels();
+
+ for (const auto &impact : nativeResults.impacts) {
+ Impact ^ managedImpact =
+ gcnew Impact(impact.distance, impact.score, impact.zone,
+ impact.angle, impact.count);
+ managedResults->Impacts->Add(managedImpact);
+ }
}
- // Enable or disable logging
- // enabled: true to enable logging, false to disable
- static void SetLoggingEnabled(bool enabled) {
- subvision::setLoggingEnabled(enabled);
+ return managedResults;
+ }
+
+ ///
+ /// Detect the four corner coordinates of the shooting sheet.
+ ///
+ ///
+ ///
+ /// Detects the white shooting sheet in the image and returns its
+ /// four corners as normalised percentage coordinates in [0, 1] range.
+ /// These coordinates can be stored and reused with
+ /// to skip automatic detection.
+ ///
+ ///
+ /// Native mapping: Calls subvision::getSheetCoordinates().
+ ///
+ ///
+ /// RGBA pixel data as a byte array.
+ /// Image width in pixels.
+ /// Image height in pixels.
+ /// List of 4 Point2f objects representing sheet corners
+ /// in normalised coordinates.
+ static List ^
+ GetSheetCoordinates(array ^ imageData, int width,
+ int height) {
+ // Convert managed array to native vector
+ std::vector nativeData(imageData->Length);
+ Marshal::Copy((array ^) imageData, 0,
+ IntPtr(nativeData.data()), imageData->Length);
+
+ // Create OpenCV Mat from the data (RGBA format)
+ cv::Mat mat(height, width, CV_8UC4, nativeData.data());
+ cv::Mat bgrMat;
+ cv::cvtColor(mat, bgrMat, cv::COLOR_RGBA2BGR);
+
+ // Call native function
+ std::vector nativePoints =
+ subvision::getSheetCoordinates(bgrMat);
+
+ // Convert to managed list
+ List ^ managedPoints = gcnew List();
+ for (const auto &pt : nativePoints) {
+ managedPoints->Add(gcnew Point2f(pt.x, pt.y));
}
- };
-}
+
+ return managedPoints;
+ }
+
+ ///
+ /// Enable or disable runtime logging.
+ ///
+ ///
+ /// When enabled, log messages from the native C++ pipeline are written
+ /// to System.Console. Logging is disabled by default.
+ ///
+ /// Native mapping: Calls subvision::setLoggingEnabled().
+ ///
+ ///
+ /// True to enable logging, false to
+ /// disable.
+ static void SetLoggingEnabled(bool enabled) {
+ subvision::setLoggingEnabled(enabled);
+ }
+};
+} // namespace SubvisionNET
\ No newline at end of file
diff --git a/docs/Doxyfile b/docs/Doxyfile
new file mode 100644
index 0000000..3e21acd
--- /dev/null
+++ b/docs/Doxyfile
@@ -0,0 +1,106 @@
+# Doxyfile for Subvision CV
+# Generated documentation configuration
+
+#---------------------------------------------------------------------------
+# Project related configuration
+#---------------------------------------------------------------------------
+
+PROJECT_NAME = "Subvision CV"
+PROJECT_NUMBER = "1.0"
+PROJECT_BRIEF = "Cross-platform computer vision library for underwater target shooting scoring"
+PROJECT_LOGO =
+OUTPUT_DIRECTORY =
+CREATE_SUBDIRS = NO
+
+#---------------------------------------------------------------------------
+# Build related configuration
+#---------------------------------------------------------------------------
+
+EXTRACT_ALL = YES
+EXTRACT_PRIVATE = NO
+EXTRACT_STATIC = YES
+EXTRACT_LOCAL_CLASSES = YES
+
+#---------------------------------------------------------------------------
+# Input configuration
+#---------------------------------------------------------------------------
+
+INPUT = ../include \
+ ../src \
+ ../emscripten_binding.cpp \
+ ../cli_wrapper.cpp
+INPUT_ENCODING = UTF-8
+FILE_PATTERNS = *.h *.hpp *.cpp *.c
+RECURSIVE = YES
+EXCLUDE_PATTERNS = */cmake-build-* */build-dotnet-* */test/*
+
+#---------------------------------------------------------------------------
+# Source browsing
+#---------------------------------------------------------------------------
+
+SOURCE_BROWSER = YES
+INLINE_SOURCES = NO
+STRIP_CODE_COMMENTS = NO
+REFERENCED_BY_RELATION = YES
+REFERENCES_RELATION = YES
+
+#---------------------------------------------------------------------------
+# Output configuration
+#---------------------------------------------------------------------------
+
+GENERATE_HTML = YES
+HTML_OUTPUT = cpp/html
+HTML_FILE_EXTENSION = .html
+GENERATE_TREEVIEW = YES
+TREEVIEW_WIDTH = 300
+
+GENERATE_LATEX = NO
+GENERATE_MAN = NO
+GENERATE_RTF = NO
+GENERATE_XML = NO
+
+#---------------------------------------------------------------------------
+# Preprocessor configuration
+#---------------------------------------------------------------------------
+
+ENABLE_PREPROCESSING = YES
+MACRO_EXPANSION = YES
+EXPAND_ONLY_PREDEF = NO
+PREDEFINED = __EMSCRIPTEN__ \
+ _MANAGED
+
+#---------------------------------------------------------------------------
+# Dot / Graph configuration
+#---------------------------------------------------------------------------
+
+HAVE_DOT = NO
+CLASS_DIAGRAMS = YES
+COLLABORATION_GRAPH = NO
+INCLUDE_GRAPH = YES
+INCLUDED_BY_GRAPH = YES
+
+#---------------------------------------------------------------------------
+# Warning configuration
+#---------------------------------------------------------------------------
+
+QUIET = NO
+WARNINGS = YES
+WARN_IF_UNDOCUMENTED = YES
+WARN_IF_DOC_ERROR = YES
+
+#---------------------------------------------------------------------------
+# Additional settings
+#---------------------------------------------------------------------------
+
+JAVADOC_AUTOBRIEF = YES
+MARKDOWN_SUPPORT = YES
+AUTOLINK_SUPPORT = YES
+BUILTIN_STL_SUPPORT = YES
+SORT_MEMBER_DOCS = YES
+SORT_BRIEF_DOCS = YES
+SHOW_NAMESPACES = YES
+SHOW_FILES = YES
+SHOW_INCLUDE_FILES = YES
+FULL_PATH_NAMES = NO
+TAB_SIZE = 4
+OPTIMIZE_OUTPUT_FOR_C = NO
diff --git a/docs/docfx.json b/docs/docfx.json
new file mode 100644
index 0000000..dbf0656
--- /dev/null
+++ b/docs/docfx.json
@@ -0,0 +1,51 @@
+{
+ "metadata": [
+ {
+ "src": [
+ {
+ "files": ["**/*.csproj"],
+ "src": "../dotnet"
+ }
+ ],
+ "dest": "api",
+ "disableGitFeatures": false,
+ "disableDefaultFilter": false
+ }
+ ],
+ "build": {
+ "content": [
+ {
+ "files": ["api/**.yml", "api/index.md"]
+ },
+ {
+ "files": [
+ "manual/index.md",
+ "manual/getting-started.md",
+ "manual/javascript.md",
+ "manual/architecture.md",
+ "toc.yml",
+ "index.md"
+ ]
+ }
+ ],
+ "resource": [
+ {
+ "files": ["cpp/html/**"]
+ }
+ ],
+ "overwrite": [],
+ "dest": "_site",
+ "globalMetadataFiles": [],
+ "fileMetadataFiles": [],
+ "template": ["default", "modern"],
+ "globalMetadata": {
+ "_appTitle": "Subvision CV Documentation",
+ "_appFooter": "Subvision CV — Cross-platform computer vision for underwater target shooting",
+ "_enableSearch": true,
+ "_disableContribution": true,
+ "_appLogoPath": "",
+ "_appFaviconPath": ""
+ },
+ "markdownEngineName": "markdig"
+ }
+}
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..aabf085
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,84 @@
+---
+uid: index
+title: Subvision CV Documentation
+---
+
+# Subvision CV Documentation
+
+Welcome to the **Subvision CV** documentation — the complete reference for the cross-platform computer vision library powering the [Subvision](https://github.com/subvision-soft) underwater target shooting scoring system.
+
+## What is Subvision CV?
+
+Subvision CV is a C++ library that detects, locates, and scores impacts on underwater target shooting sheets. It is used by the Subvision mobile and web application, which aims to be validated by the **FFESSM** (French Underwater Federation) for official competition use.
+
+## Platform Support
+
+Subvision CV runs on three platforms from a single C++ codebase:
+
+| Platform | Technology | Output |
+|----------|-----------|--------|
+| **Native C++** | OpenCV + CMake | Static library (`subvision_lib`) |
+| **WebAssembly** | Emscripten + embind | ES6 module (`subvision_core_es6.js`) |
+| **.NET** | C++/CLI wrapper | Managed DLL (`subvision-x64.dll`) |
+
+## Documentation Sections
+
+- **[Getting Started](manual/getting-started.md)** — Prerequisites, build instructions, and quick start guide
+- **[JavaScript / WebAssembly Guide](manual/javascript.md)** — Complete guide to using Subvision CV in the browser
+- **[Architecture](manual/architecture.md)** — System design, module overview, and data flow
+- **[C++ API Reference](cpp/html/index.html)** — Full Doxygen-generated C++ API documentation
+- **[.NET API Reference](api/)** — DocFX-generated .NET API documentation
+
+## Quick Examples
+
+### C++
+
+```cpp
+#include "subvision_cv.h"
+
+cv::Mat image = cv::imread("target_sheet.jpg");
+subvision::ImpactResults results;
+
+if (subvision::retrieveImpacts(image, results)) {
+ for (const auto& impact : results.impacts) {
+ std::cout << "Score: " << impact.score
+ << ", Distance: " << impact.distance << "mm"
+ << std::endl;
+ }
+}
+```
+
+### JavaScript (WebAssembly)
+
+```javascript
+import SubvisionCV from './subvision_core_es6.js';
+
+const module = await SubvisionCV();
+
+const canvas = document.getElementById('targetCanvas');
+const ctx = canvas.getContext('2d');
+const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+const results = module.processTargetImage(
+ canvas.width, canvas.height, imageData.data
+);
+
+for (let i = 0; i < results.impacts.size(); i++) {
+ const impact = results.impacts.get(i);
+ console.log(`Score: ${impact.score}, Distance: ${impact.distance}mm`);
+}
+```
+
+### C# (.NET)
+
+```csharp
+using SubvisionNET;
+
+byte[] imageData = LoadImageAsRGBA("target_sheet.jpg", out int w, out int h);
+var results = SubvisionCore.ProcessTargetImage(imageData, w, h, null);
+
+foreach (var impact in results.Impacts)
+{
+ Console.WriteLine($"Score: {impact.Score}, Distance: {impact.Distance}mm");
+}
+```
diff --git a/docs/manual/architecture.md b/docs/manual/architecture.md
new file mode 100644
index 0000000..43c3351
--- /dev/null
+++ b/docs/manual/architecture.md
@@ -0,0 +1,206 @@
+# Architecture
+
+This document describes the architecture of Subvision CV, including the module structure, processing pipeline, and cross-platform design.
+
+## System Overview
+
+Subvision CV is a C++ computer vision library built on OpenCV. It processes photographs of underwater target shooting sheets to detect impacts and compute scores.
+
+```
+┌─────────────────────────────────────────────────────┐
+│ Application Layer │
+│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ C++ App │ │ Browser │ │ .NET App │ │
+│ │ (native) │ │ (WASM/JS) │ │ (C#/VB) │ │
+│ └────┬─────┘ └──────┬───────┘ └──────┬───────┘ │
+├────────┼────────────────┼────────────────┼───────────┤
+│ │ Platform Binding Layer │ │
+│ ┌────┴─────┐ ┌──────┴───────┐ ┌─────┴──────┐ │
+│ │ Direct │ │ Emscripten │ │ C++/CLI │ │
+│ │ C++ Link │ │ embind │ │ Wrapper │ │
+│ └────┬─────┘ └──────┬───────┘ └─────┬──────┘ │
+├────────┼────────────────┼────────────────┼───────────┤
+│ └────────────────┼────────────────┘ │
+│ │ │
+│ ┌──────────┴──────────┐ │
+│ │ Subvision CV Core │ │
+│ │ (C++ / OpenCV) │ │
+│ └─────────────────────┘ │
+└─────────────────────────────────────────────────────┘
+```
+
+## Module Structure
+
+### Core Modules
+
+| Module | Header | Responsibility |
+|--------|--------|---------------|
+| **Types** | `types.h` | Core data structures: `Impact`, `ImpactResults`, `Ellipse` |
+| **Constants** | `constants.h` | Zone IDs, processing dimensions, kernels |
+| **Utils** | `utils.h` | Math, geometry, coordinate transforms, scoring |
+| **Image Processing** | `image_processing.h` | Contour analysis, colour masking, ellipse fitting |
+| **Sheet Detection** | `sheet_detection.h` | Sheet boundary detection, perspective correction |
+| **Target Detection** | `target_detection.h` | Target ring localisation, multi-zone detection |
+| **Impact Detection** | `impact_detection.h` | Impact localisation, scoring, annotation |
+| **Logging** | `logging.h` | Cross-platform logging (console, emscripten, .NET) |
+
+### Platform Bindings
+
+| File | Platform | Technology |
+|------|----------|-----------|
+| `emscripten_binding.cpp` | WebAssembly | Emscripten embind |
+| `cli_wrapper.cpp` | .NET | C++/CLI (MSVC) |
+
+### Dependency Graph
+
+```
+impact_detection
+├── sheet_detection
+│ ├── image_processing
+│ │ ├── constants
+│ │ ├── utils
+│ │ └── logging
+│ └── utils
+├── target_detection
+│ ├── image_processing
+│ ├── constants
+│ ├── utils
+│ └── logging
+├── image_processing
+├── utils
+└── logging
+```
+
+## Processing Pipeline
+
+### Main Pipeline (`retrieveImpacts`)
+
+```
+Input Image (BGR)
+ │
+ ▼
+┌──────────────────┐
+│ Sheet Detection │ getSheetPicture() or getSheetPictureManually()
+│ - HLS lightness │
+│ - Contour find │
+│ - Quadrilateral │
+│ validation │
+└────────┬─────────┘
+ │
+ ▼
+┌──────────────────┐
+│ Perspective │ getPerspectiveTransform + warpPerspective
+│ Correction │ → 2000×2000 flat image
+└────────┬─────────┘
+ │
+ ▼
+┌──────────────────┐
+│ Target Detection │ getTargetsEllipse() × 5 zones
+│ - XYZ colour │ (top-left, top-right, bottom-left,
+│ - Threshold │ bottom-right, center)
+│ - Ellipse fit │
+└────────┬─────────┘
+ │
+ ▼
+┌──────────────────┐
+│ Impact Detection │ getImpactsCoordinates()
+│ - CLAHE normalise│
+│ - HSV red mask │
+│ - Morphology │
+│ - Ellipse fit │
+└────────┬─────────┘
+ │
+ ▼
+┌──────────────────┐
+│ Score Computation│ drawAndGetImpactsPoints()
+│ - Zone matching │
+│ - Distance calc │
+│ - Score lookup │
+│ - Annotation │
+└────────┬─────────┘
+ │
+ ▼
+Output: ImpactResults
+ - annotatedImage (BGR)
+ - impacts[] (distance, score, zone, angle)
+```
+
+### Target Zone Layout
+
+A standard underwater shooting sheet has five targets arranged as follows:
+
+```
+┌───────────────────────────────────────┐
+│ │
+│ ┌─────────┐ ┌─────────┐ │
+│ │ Zone 0 │ │ Zone 1 │ │
+│ │ TOP LEFT │ │TOP RIGHT │ │
+│ └─────────┘ └─────────┘ │
+│ │
+│ ┌─────────┐ │
+│ │ Zone 4 │ │
+│ │ CENTER │ │
+│ └─────────┘ │
+│ │
+│ ┌─────────┐ ┌─────────┐ │
+│ │ Zone 2 │ │ Zone 3 │ │
+│ │ BOT LEFT │ │BOT RIGHT │ │
+│ └─────────┘ └─────────┘ │
+│ │
+└───────────────────────────────────────┘
+```
+
+### Scoring System
+
+The scoring follows FFESSM underwater target shooting rules:
+
+| Distance from Center | Score Formula | Example |
+|---------------------|---------------|---------|
+| > 48 mm | 0 | — |
+| ≤ 0 mm (bullseye) | 570 | 570 |
+| 1–5 mm | 570 − (distance × 6) | 3mm → 552 |
+| > 5 mm | 540 − ((distance − 5) × 3) | 20mm → 495 |
+
+The real-world distance is calculated by mapping the pixel distance between the target center and the impact to the known 45mm target radius.
+
+## Cross-Platform Design
+
+### Colour Space Handling
+
+Each platform provides images in different colour formats:
+
+| Platform | Input Format | Internal Format |
+|----------|-------------|----------------|
+| Native C++ | BGR (OpenCV default) | BGR |
+| WebAssembly | RGBA (browser Canvas) | BGR (converted) |
+| .NET | RGBA (managed bitmap) | BGR (converted) |
+
+All platform bindings convert to BGR before calling the core pipeline. Output images are converted back to RGBA for browser and .NET consumers.
+
+### Memory Management
+
+| Platform | Strategy |
+|----------|----------|
+| Native C++ | Automatic (stack/RAII) — cv::Mat uses reference counting |
+| WebAssembly | Emscripten manages memory; `embind` handles marshalling |
+| .NET | `pin_ptr` for input; `Marshal::Copy` for output; GC handles managed objects |
+
+### Logging
+
+The logging module (`logging.h` / `logging.cpp`) uses compile-time preprocessor directives to select the output mechanism:
+
+| Build Target | Preprocessor | Output |
+|-------------|-------------|--------|
+| Emscripten | `__EMSCRIPTEN__` | `emscripten_log()` → browser console |
+| C++/CLI | `_MANAGED` | `System::Console::WriteLine()` |
+| Native C++ | (default) | `std::cout` |
+
+## Build System
+
+The project uses CMake with platform-specific configurations:
+
+- **Native**: Standard CMake build with `subvision_lib` static library
+- **Emscripten**: Docker-based build with Makefile wrapper
+- **C++/CLI**: MSVC-only build with `/clr` and `/EHa` flags
+
+The `CMakeLists.txt` uses conditional logic (`if(EMSCRIPTEN)`, `if(BUILD_CLI_WRAPPER)`) to configure each target independently.
diff --git a/docs/manual/getting-started.md b/docs/manual/getting-started.md
new file mode 100644
index 0000000..b726e4f
--- /dev/null
+++ b/docs/manual/getting-started.md
@@ -0,0 +1,181 @@
+# Getting Started
+
+This guide covers prerequisites, building Subvision CV for each platform, and writing your first application.
+
+## Prerequisites
+
+### Common Requirements
+
+- **CMake** 3.27+ ([cmake.org](https://cmake.org/download/))
+- **OpenCV** 4.x with core, imgproc, imgcodecs, features2d, calib3d, dnn modules
+
+### For Native C++ Build
+
+- A C++23-compatible compiler (GCC 13+, Clang 16+, or MSVC 2022)
+- OpenCV 4.x installed and discoverable by CMake
+
+### For WebAssembly Build
+
+- **Docker** ([docker.com](https://www.docker.com/))
+- The build uses the `ghcr.io/subvision-soft/subvision-emscripten:2025.6.1` image which includes Emscripten SDK + OpenCV
+
+### For .NET Build
+
+- **Windows** with Visual Studio 2022
+- **OpenCV 4.x** (`choco install opencv`)
+- **.NET Framework 4.7.2+** or .NET 6+
+
+---
+
+## Building
+
+### Native C++ Library
+
+```bash
+mkdir build && cd build
+cmake ..
+cmake --build . --config Release
+
+# Run tests
+cd test
+./subvision_tests
+```
+
+This produces the static library `subvision_lib` which can be linked into any C++ application.
+
+### WebAssembly (via Docker)
+
+```bash
+# Build both standard and ES6 module versions
+make all
+
+# Or build individually
+make subvision # Standard version
+make subvision_es6 # ES6 module version
+```
+
+Output files are placed in `build_wasm/`:
+- `subvision_core.js` — Standard WebAssembly wrapper
+- `subvision_core_es6.js` — ES6 module wrapper
+- `subvision_core.wasm` — WebAssembly binary
+
+### .NET Wrapper (Windows)
+
+```bash
+# x64 build
+mkdir build-dotnet-x64 && cd build-dotnet-x64
+cmake -G "Visual Studio 17 2022" -A x64 -DBUILD_CLI_WRAPPER=ON ..
+cmake --build . --config Release
+
+# x86 build
+cd ..
+mkdir build-dotnet-x86 && cd build-dotnet-x86
+cmake -G "Visual Studio 17 2022" -A Win32 -DBUILD_CLI_WRAPPER=ON ..
+cmake --build . --config Release
+```
+
+Output:
+- `build-dotnet-x64/bin/x64/subvision-x64.dll`
+- `build-dotnet-x86/bin/x86/subvision-x86.dll`
+
+---
+
+## Quick Start
+
+### C++ — Detect Impacts
+
+```cpp
+#include "subvision_cv.h"
+#include
+
+int main() {
+ // Load an image of a shooting sheet
+ cv::Mat image = cv::imread("target_sheet.jpg");
+ if (image.empty()) {
+ std::cerr << "Failed to load image" << std::endl;
+ return 1;
+ }
+
+ // Enable logging for debugging
+ subvision::setLoggingEnabled(true);
+
+ // Run the full detection pipeline
+ subvision::ImpactResults results;
+ bool success = subvision::retrieveImpacts(image, results);
+
+ if (success) {
+ std::cout << "Detected " << results.impacts.size() << " impacts:" << std::endl;
+ for (const auto& impact : results.impacts) {
+ std::cout << " Zone: " << impact.zone
+ << ", Score: " << impact.score
+ << ", Distance: " << impact.distance << "mm"
+ << ", Angle: " << impact.angle << "°"
+ << std::endl;
+ }
+
+ // Save the annotated image
+ cv::imwrite("annotated_result.jpg", results.annotatedImage);
+ } else {
+ std::cerr << "Detection failed" << std::endl;
+ }
+
+ return 0;
+}
+```
+
+### C++ — Two-Step Detection (Manual Sheet Coordinates)
+
+```cpp
+#include "subvision_cv.h"
+
+// Step 1: Detect sheet coordinates (can be stored for reuse)
+cv::Mat image = cv::imread("target_sheet.jpg");
+auto coords = subvision::getSheetCoordinates(image);
+// coords are normalised percentages — resolution-independent
+
+// Step 2: Process with known coordinates (faster, no auto-detection)
+subvision::ImpactResults results;
+subvision::retrieveImpacts(image, results, coords);
+```
+
+### C# — .NET Usage
+
+```csharp
+using SubvisionNET;
+
+// Load image as RGBA byte array (from your imaging library)
+byte[] imageData = LoadImageAsRGBA("target_sheet.jpg", out int width, out int height);
+
+// Optional: enable logging
+SubvisionCore.SetLoggingEnabled(true);
+
+// Detect impacts
+var results = SubvisionCore.ProcessTargetImage(imageData, width, height, null);
+
+if (results != null)
+{
+ Console.WriteLine($"Detected {results.Impacts.Count} impacts:");
+ foreach (var impact in results.Impacts)
+ {
+ Console.WriteLine($" Zone: {impact.Zone}, Score: {impact.Score}, " +
+ $"Distance: {impact.Distance}mm");
+ }
+
+ // The annotated image is available as RGBA byte array
+ // results.AnnotatedImageData (Width × Height × 4 bytes)
+}
+```
+
+---
+
+## CMake Options
+
+| Option | Description | Default |
+|--------|-------------|---------|
+| `BUILD_TESTS` | Build unit tests | `ON` |
+| `BUILD_CLI_WRAPPER` | Build C++/CLI .NET wrapper | `OFF` |
+
+## Next Steps
+
+- **[JavaScript / WebAssembly Guide](javascript.md)** — Detailed browser integration
+- **[Architecture](architecture.md)** — Understand the system design
diff --git a/docs/manual/index.md b/docs/manual/index.md
new file mode 100644
index 0000000..1c6c322
--- /dev/null
+++ b/docs/manual/index.md
@@ -0,0 +1,44 @@
+# Subvision CV Documentation
+
+Welcome to the Subvision CV manual. This documentation covers the complete API and usage guides for the cross-platform computer vision library.
+
+## Overview
+
+**Subvision CV** is a C++ computer vision library for detecting, locating, and scoring impacts on underwater target shooting sheets. It powers the [Subvision](https://github.com/subvision-soft) application used for FFESSM competition scoring.
+
+### Core Capabilities
+
+| Feature | Description |
+|---------|-------------|
+| **Sheet Detection** | Automatically locate the shooting sheet in a photograph |
+| **Target Detection** | Identify the five concentric ring targets on the sheet |
+| **Impact Localisation** | Detect red impact marks (shots) on the targets |
+| **Score Computation** | Calculate scores using FFESSM federation rules (0–570 scale) |
+| **Image Annotation** | Generate annotated images with targets, impacts, and scores drawn |
+
+### Platform Support
+
+The library is deployable on three platforms from a single C++ codebase:
+
+- **Native C++** — Static library linked via CMake
+- **WebAssembly** — ES6 module built with Emscripten for browser usage
+- **.NET** — C++/CLI managed wrapper for Windows desktop apps
+
+### Processing Pipeline
+
+The library follows a sequential pipeline:
+
+1. **Input** — Receive a photograph of a shooting sheet (RGBA or BGR)
+2. **Sheet Detection** — Detect the white sheet boundary and extract corner coordinates
+3. **Perspective Correction** — Warp to a flat 2000×2000 pixel image
+4. **Target Detection** — Locate the elliptical target rings in each of the 5 zones
+5. **Impact Detection** — Find red impact marks via colour analysis
+6. **Scoring** — Compute distance and score for each impact
+7. **Annotation** — Draw targets, impacts, lines, and scores on the image
+8. **Output** — Return the annotated image and list of scored impacts
+
+## Next Steps
+
+- **[Getting Started](getting-started.md)** — Installation and first use
+- **[JavaScript / WebAssembly](javascript.md)** — Browser integration guide
+- **[Architecture](architecture.md)** — Detailed system design
diff --git a/docs/manual/javascript.md b/docs/manual/javascript.md
new file mode 100644
index 0000000..6c99969
--- /dev/null
+++ b/docs/manual/javascript.md
@@ -0,0 +1,271 @@
+# JavaScript / WebAssembly Usage Guide
+
+This guide covers how to use Subvision CV in web applications via the Emscripten WebAssembly module.
+
+## Overview
+
+Subvision CV is compiled to WebAssembly using Emscripten and exposed to JavaScript through `embind`. The module provides two main functions:
+
+| Function | Purpose |
+|----------|---------|
+| `processTargetImage(width, height, data)` | Detect and score all impacts on a shooting sheet |
+| `getSheetCoordinates(width, height, data)` | Detect the sheet corner coordinates |
+| `setLoggingEnabled(bool)` | Enable/disable console logging |
+
+## Loading the Module
+
+### ES6 Module (Recommended)
+
+```javascript
+import SubvisionCV from './subvision_core_es6.js';
+
+// Initialize the module (loads the WASM binary)
+const module = await SubvisionCV();
+console.log('Subvision CV loaded successfully');
+```
+
+### Standard Script
+
+```html
+
+
+```
+
+## Core API
+
+### Processing a Target Image
+
+The `processTargetImage` function takes raw pixel data from a canvas and returns impact detection results.
+
+```javascript
+async function processImage(imageElement) {
+ const module = await SubvisionCV();
+
+ // Draw image to a canvas to get pixel data
+ const canvas = document.createElement('canvas');
+ canvas.width = imageElement.naturalWidth;
+ canvas.height = imageElement.naturalHeight;
+ const ctx = canvas.getContext('2d');
+ ctx.drawImage(imageElement, 0, 0);
+
+ // Get RGBA pixel data
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+ // Process the image
+ const results = module.processTargetImage(
+ canvas.width, canvas.height, imageData.data
+ );
+
+ // Read results
+ console.log('Annotated image dimensions:',
+ results.annotatedImage.rows, 'x', results.annotatedImage.columns);
+
+ const impactCount = results.impacts.size();
+ console.log(`Detected ${impactCount} impacts:`);
+
+ for (let i = 0; i < impactCount; i++) {
+ const impact = results.impacts.get(i);
+ console.log(` Impact ${i + 1}:`,
+ `score=${impact.score}`,
+ `distance=${impact.distance}mm`,
+ `zone=${impact.zone}`,
+ `angle=${impact.angle}°`
+ );
+ }
+
+ return results;
+}
+```
+
+### Displaying the Annotated Image
+
+```javascript
+function displayAnnotatedImage(results, targetCanvas) {
+ const mat = results.annotatedImage;
+ const width = mat.columns;
+ const height = mat.rows;
+
+ targetCanvas.width = width;
+ targetCanvas.height = height;
+ const ctx = targetCanvas.getContext('2d');
+
+ // Get pixel data from the Mat (RGBA format)
+ const data = new Uint8ClampedArray(mat.data);
+ const imageData = new ImageData(data, width, height);
+
+ ctx.putImageData(imageData, 0, 0);
+}
+```
+
+### Detecting Sheet Coordinates
+
+Use `getSheetCoordinates` to find the sheet corners. These can be cached and reused.
+
+```javascript
+async function detectSheet(imageElement) {
+ const module = await SubvisionCV();
+
+ const canvas = document.createElement('canvas');
+ canvas.width = imageElement.naturalWidth;
+ canvas.height = imageElement.naturalHeight;
+ const ctx = canvas.getContext('2d');
+ ctx.drawImage(imageElement, 0, 0);
+
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+ // Returns an array of {x, y} points (normalised 0-1 coordinates)
+ const coords = module.getSheetCoordinates(
+ canvas.width, canvas.height, imageData.data
+ );
+
+ console.log('Sheet corners:');
+ for (let i = 0; i < coords.length; i++) {
+ console.log(` Corner ${i}: (${coords[i].x}, ${coords[i].y})`);
+ }
+
+ return coords;
+}
+```
+
+## Data Types
+
+### Impact Object
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `distance` | `number` | Distance from target center (mm) |
+| `score` | `number` | Score on 0–570 FFESSM scale |
+| `zone` | `number` | Target zone ID (0–4, or -1) |
+| `angle` | `number` | Angular position in degrees |
+| `count` | `number` | Impact count (usually 1) |
+
+### ImpactResults Object
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `annotatedImage` | `Mat` | Annotated image with drawn targets and scores |
+| `impacts` | `ImpactVector` | Vector of Impact objects |
+
+### Mat Object
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `rows` | `number` | Image height in pixels |
+| `columns` | `number` | Image width in pixels |
+| `data` | `Uint8Array` | Raw RGBA pixel data |
+
+### ImpactVector Methods
+
+| Method | Returns | Description |
+|--------|---------|-------------|
+| `.size()` | `number` | Number of impacts |
+| `.get(index)` | `Impact` | Impact at the given index |
+
+## Performance Considerations
+
+- **Image size**: Larger images take longer. The library internally resizes to 2000×2000 pixels, so there is no benefit to providing images larger than this.
+- **First call**: The first call may be slower due to WASM compilation. Subsequent calls are faster.
+- **Memory**: Use `Module._malloc` and `Module._free` carefully if working with raw memory. The embind API handles memory automatically for the exported functions.
+- **Web Workers**: For non-blocking UI, run the processing in a Web Worker.
+
+## Error Handling
+
+```javascript
+try {
+ const results = module.processTargetImage(width, height, data);
+ if (results.impacts.size() === 0) {
+ console.warn('No impacts detected — the sheet may not be visible');
+ }
+} catch (error) {
+ console.error('Processing failed:', error.message);
+ // Common causes:
+ // - Image does not contain a visible shooting sheet
+ // - Image is too dark or overexposed
+ // - Sheet is partially occluded
+}
+```
+
+## Debugging
+
+Enable logging to see the internal processing steps in the browser console:
+
+```javascript
+module.setLoggingEnabled(true);
+
+// Process image — detailed logs will appear in the console
+const results = module.processTargetImage(width, height, data);
+
+// Disable logging when done
+module.setLoggingEnabled(false);
+```
+
+## Complete Working Example
+
+```html
+
+
+
+ Subvision CV Demo
+
+
+
+
+
+
+
+
+
+```
diff --git a/docs/toc.yml b/docs/toc.yml
new file mode 100644
index 0000000..11d6862
--- /dev/null
+++ b/docs/toc.yml
@@ -0,0 +1,17 @@
+- name: Home
+ href: index.md
+- name: Manual
+ href: manual/
+ items:
+ - name: Overview
+ href: manual/index.md
+ - name: Getting Started
+ href: manual/getting-started.md
+ - name: JavaScript / WebAssembly
+ href: manual/javascript.md
+ - name: Architecture
+ href: manual/architecture.md
+- name: C++ API Reference
+ href: cpp/html/index.html
+- name: .NET API Reference
+ href: api/
diff --git a/emscripten_binding.cpp b/emscripten_binding.cpp
index 184e7f8..5ec8b9e 100644
--- a/emscripten_binding.cpp
+++ b/emscripten_binding.cpp
@@ -1,128 +1,290 @@
+/**
+ * @file emscripten_binding.cpp
+ * @brief WebAssembly bindings for the Subvision CV library via Emscripten.
+ *
+ * Exposes the core Subvision functionality to JavaScript through Emscripten's
+ * embind system. This file handles:
+ * - RGBA → BGR colour space conversion (browser images are RGBA)
+ * - Typed array marshalling between JavaScript and C++
+ * - Result packaging as JavaScript-friendly objects
+ *
+ * ## Exported JavaScript API
+ *
+ * | C++ Function | JavaScript Function |
+ * Description |
+ * |-------------------------|----------------------------------------|---------------------------------------|
+ * | `processTargetImage` | `Module.processTargetImage(w, h, arr)` | Detect
+ * and score all impacts | | `getSheetCoordinates`|
+ * `Module.getSheetCoordinates(w, h, arr)`| Detect sheet corner coordinates | |
+ * `setLoggingEnabled` | `Module.setLoggingEnabled(bool)` |
+ * Enable/disable console logging |
+ *
+ * ## JavaScript Usage Example
+ *
+ * @code{.js}
+ * import SubvisionCV from './subvision_core_es6.js';
+ *
+ * const module = await SubvisionCV();
+ *
+ * // Get image data from a canvas
+ * const canvas = document.getElementById('myCanvas');
+ * const ctx = canvas.getContext('2d');
+ * const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ *
+ * // Process the target image
+ * const results = module.processTargetImage(canvas.width, canvas.height,
+ * imageData.data); console.log('Number of impacts:', results.impacts.size());
+ *
+ * for (let i = 0; i < results.impacts.size(); i++) {
+ * const impact = results.impacts.get(i);
+ * console.log(`Impact: score=${impact.score},
+ * distance=${impact.distance}`);
+ * }
+ *
+ * // Get sheet coordinates
+ * const coords = module.getSheetCoordinates(canvas.width, canvas.height,
+ * imageData.data); for (let i = 0; i < coords.size(); i++) { const pt =
+ * coords.get(i); console.log(`Corner: (${pt.x}, ${pt.y})`);
+ * }
+ * @endcode
+ */
+
+#include "include/impact_detection.h"
+#include "include/logging.h"
+#include "include/sheet_detection.h"
+#include "include/types.h"
#include
#include
#include
-#include "include/types.h"
-#include "include/impact_detection.h"
-#include "include/sheet_detection.h"
-#include "include/logging.h"
+
using namespace emscripten;
-// Structure pour représenter un impact en JavaScript
+/**
+ * @brief JavaScript-friendly representation of an impact result.
+ *
+ * Mirrors the native subvision::Impact struct but is designed for
+ * use with Emscripten's value_object binding. Exported to JavaScript
+ * as the `Impact` type.
+ *
+ * @note In JavaScript, access fields directly: `impact.distance`,
+ * `impact.score`, etc.
+ */
struct JSImpact {
- int distance;
- int score;
- int zone;
- float angle;
- int count;
-
- // Conversion d'un Impact C++ vers JSImpact
- static JSImpact fromImpact(const subvision::Impact &impact) {
- JSImpact jsImpact;
- jsImpact.distance = impact.distance;
- jsImpact.score = impact.score;
- jsImpact.zone = impact.zone;
- jsImpact.angle = impact.angle;
- jsImpact.count = impact.count;
- return jsImpact;
- }
+ int distance; ///< Distance from center in millimeters.
+ int score; ///< Computed score (0–570).
+ int zone; ///< Target zone identifier.
+ float angle; ///< Angular position in degrees.
+ int count; ///< Number of impacts at this location.
+
+ /**
+ * @brief Convert a native C++ Impact to a JSImpact.
+ *
+ * @param impact The native Impact to convert.
+ * @return A JSImpact with identical field values.
+ */
+ static JSImpact fromImpact(const subvision::Impact &impact) {
+ JSImpact jsImpact;
+ jsImpact.distance = impact.distance;
+ jsImpact.score = impact.score;
+ jsImpact.zone = impact.zone;
+ jsImpact.angle = impact.angle;
+ jsImpact.count = impact.count;
+ return jsImpact;
+ }
};
-// Structure pour les résultats retournés à JavaScript
+/**
+ * @brief Container for impact processing results returned to JavaScript.
+ *
+ * Exported as the `ImpactResults` type in JavaScript. Contains the
+ * annotated image as a cv::Mat and an array of Impact objects.
+ *
+ * JavaScript usage:
+ * @code{.js}
+ * const results = module.processTargetImage(width, height, data);
+ * const mat = results.annotatedImage;
+ * const imageBytes = mat.data; // Uint8Array of RGBA pixels
+ * const impacts = results.impacts; // ImpactVector
+ * @endcode
+ */
struct JSImpactResults {
- cv::Mat annotatedImage;
- val impacts = val::array();
+ cv::Mat annotatedImage; ///< Annotated image with targets and impacts drawn
+ ///< (RGBA format).
+ val impacts = val::array(); ///< JavaScript array of JSImpact objects.
};
-template
+/**
+ * @brief Detect sheet corner coordinates from a JavaScript image buffer.
+ *
+ * Converts the incoming RGBA typed array to a BGR cv::Mat, calls the
+ * native getSheetCoordinates(), and returns the result as a JavaScript
+ * array of {x, y} point objects.
+ *
+ * @tparam T Pixel data type (typically `unsigned char`).
+ * @param width Image width in pixels.
+ * @param height Image height in pixels.
+ * @param typedArray JavaScript Uint8Array containing RGBA pixel data.
+ * @return JavaScript array of point objects with `x` and `y` properties
+ * (normalised percentage coordinates in [0, 1]).
+ *
+ * JavaScript usage:
+ * @code{.js}
+ * const coords = Module.getSheetCoordinates(width, height, imageData.data);
+ * @endcode
+ */
+template
val getSheetCoordinates(int width, int height, const val &typedArray) {
- subvision::log("Start processing getSheetCoordinates with width: " + std::to_string(width) + ", height: " + std::to_string(height));
- std::vector vec = convertJSArrayToNumberVector(typedArray);
- subvision::log("Vector size: " + std::to_string(vec.size()));
- cv::Mat mat(height, width, CV_8UC4, vec.data());
- cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR);
-
- subvision::log("Processing getSheetCoordinates with width: " + std::to_string(width) + ", height: " + std::to_string(height));
- auto points = subvision::getSheetCoordinates(mat);
-
- val jsArray = val::array();
- for (const auto &pt: points) {
- val jsPoint = val::object();
- jsPoint.set("x", pt.x);
- jsPoint.set("y", pt.y);
- jsArray.call("push", jsPoint);
- }
+ subvision::log("Start processing getSheetCoordinates with width: " +
+ std::to_string(width) + ", height: " + std::to_string(height));
+ std::vector vec = convertJSArrayToNumberVector(typedArray);
+ subvision::log("Vector size: " + std::to_string(vec.size()));
+ cv::Mat mat(height, width, CV_8UC4, vec.data());
+ cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR);
- return jsArray;
+ subvision::log("Processing getSheetCoordinates with width: " +
+ std::to_string(width) + ", height: " + std::to_string(height));
+ auto points = subvision::getSheetCoordinates(mat);
+
+ val jsArray = val::array();
+ for (const auto &pt : points) {
+ val jsPoint = val::object();
+ jsPoint.set("x", pt.x);
+ jsPoint.set("y", pt.y);
+ jsArray.call("push", jsPoint);
+ }
+
+ return jsArray;
}
+/**
+ * @brief Process a target image from JavaScript and return impact results.
+ *
+ * Main entry point for WebAssembly impact detection. Converts the incoming
+ * RGBA typed array to BGR, runs the full detection pipeline, and packages
+ * results as JavaScript-friendly objects.
+ *
+ * @tparam T Pixel data type (typically `unsigned char`).
+ * @param width Image width in pixels.
+ * @param height Image height in pixels.
+ * @param typedArray JavaScript Uint8Array containing RGBA pixel data.
+ * @return JSImpactResults containing the annotated image (RGBA) and impact
+ * array.
+ *
+ * @note The annotated image is converted back to RGBA before returning
+ * so it can be directly drawn to a canvas.
+ *
+ * JavaScript usage:
+ * @code{.js}
+ * const results = Module.processTargetImage(canvas.width, canvas.height,
+ * imageData.data);
+ * @endcode
+ */
+template
+JSImpactResults processTargetImage(int width, int height,
+ const val &typedArray) {
+ subvision::ImpactResults results;
+ auto start = std::chrono::high_resolution_clock::now();
-// Fonction wrapper pour retrieveImpacts
-template
-JSImpactResults processTargetImage(int width, int height, const val &typedArray) {
- subvision::ImpactResults results;
- auto start = std::chrono::high_resolution_clock::now();
-
- std::vector vec = convertJSArrayToNumberVector(typedArray);
- auto end = std::chrono::high_resolution_clock::now();
- std::chrono::duration elapsed = end - start;
- subvision::log("Temps écoulé pour vecFromJSArray: " + std::to_string(elapsed.count()) + " secondes");
- cv::Mat mat(height, width, CV_8UC4, vec.data());
- cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR);
-
- bool success = subvision::retrieveImpacts(mat, results);
-
- JSImpactResults jsResults;
- if (success) {
- auto annotated_image = results.annotatedImage;
- cv::cvtColor(annotated_image, annotated_image, cv::COLOR_BGR2RGBA);
- jsResults.annotatedImage = annotated_image;
-
- val impactArray = val::array();
- for (const auto& impact : results.impacts) {
- impactArray.call("push", JSImpact::fromImpact(impact));
- }
- jsResults.impacts = impactArray;
- }
+ std::vector vec = convertJSArrayToNumberVector(typedArray);
+ auto end = std::chrono::high_resolution_clock::now();
+ std::chrono::duration elapsed = end - start;
+ subvision::log("Temps écoulé pour vecFromJSArray: " +
+ std::to_string(elapsed.count()) + " secondes");
+ cv::Mat mat(height, width, CV_8UC4, vec.data());
+ cv::cvtColor(mat, mat, cv::COLOR_RGBA2BGR);
+
+ bool success = subvision::retrieveImpacts(mat, results);
+ JSImpactResults jsResults;
+ if (success) {
+ auto annotated_image = results.annotatedImage;
+ cv::cvtColor(annotated_image, annotated_image, cv::COLOR_BGR2RGBA);
+ jsResults.annotatedImage = annotated_image;
- return jsResults;
+ val impactArray = val::array();
+ for (const auto &impact : results.impacts) {
+ impactArray.call("push", JSImpact::fromImpact(impact));
+ }
+ jsResults.impacts = impactArray;
+ }
+
+ return jsResults;
}
-template
-val matData(const cv::Mat &mat) {
- return val(memory_view((mat.total() * mat.elemSize()) / sizeof(T),
- (T *) mat.data));
+/**
+ * @brief Extract raw pixel data from a cv::Mat as a JavaScript typed array
+ * view.
+ *
+ * Creates a memory_view over the Mat's data buffer, allowing JavaScript
+ * to read the pixel data directly without copying.
+ *
+ * @tparam T Element type for the memory view (typically `unsigned char`).
+ * @param mat The cv::Mat to expose.
+ * @return An Emscripten val wrapping a typed array view of the Mat's data.
+ *
+ * @warning The returned view becomes invalid if the Mat is deallocated or
+ * reallocated.
+ */
+template val matData(const cv::Mat &mat) {
+ return val(memory_view((mat.total() * mat.elemSize()) / sizeof(T),
+ (T *)mat.data));
}
+/**
+ * @defgroup emscripten_bindings Emscripten WASM Bindings
+ * @brief JavaScript API bindings for the Subvision WebAssembly module.
+ *
+ * These bindings export the following types and functions to JavaScript:
+ *
+ * **Types:**
+ * - `Point2f` — 2D point with `x`, `y` float properties
+ * - `Mat` — OpenCV matrix with `rows`, `columns`, `data` properties
+ * - `Impact` — Impact result with `distance`, `score`, `zone`, `angle`, `count`
+ * - `ImpactResults` — Contains `annotatedImage` (Mat) and `impacts` (array)
+ *
+ * **Functions:**
+ * - `processTargetImage(width, height, typedArray)` — Full impact detection
+ * - `getSheetCoordinates(width, height, typedArray)` — Sheet corner detection
+ * - `setLoggingEnabled(bool)` — Toggle console logging
+ *
+ * **Vectors:**
+ * - `vector_uchar` — std::vector
+ * - `vector_point2f` — std::vector
+ * - `ImpactVector` — std::vector
+ * @{
+ */
+
// Définition des liaisons Emscripten
-EMSCRIPTEN_BINDINGS (subvision_module) {
- register_vector("vector_uchar");
- register_vector("vector_point2f");
- class_("Point2f")
- .constructor()
- .property("x", &cv::Point2f::x)
- .property("y", &cv::Point2f::y);
-
- class_("Mat")
- .property("rows", &cv::Mat::rows)
- .property("columns", &cv::Mat::cols)
- .property("data", &matData);
-
- value_object("Impact")
- .field("distance", &JSImpact::distance)
- .field("score", &JSImpact::score)
- .field("zone", &JSImpact::zone)
- .field("angle", &JSImpact::angle)
- .field("count", &JSImpact::count);
-
- register_vector("ImpactVector");
-
- value_object("ImpactResults")
- .field("annotatedImage", &JSImpactResults::annotatedImage)
- .field("impacts", &JSImpactResults::impacts);
-
- function("processTargetImage", &processTargetImage);
- function("getSheetCoordinates", &getSheetCoordinates);
- function("setLoggingEnabled", &subvision::setLoggingEnabled);
+EMSCRIPTEN_BINDINGS(subvision_module) {
+ register_vector("vector_uchar");
+ register_vector("vector_point2f");
+ class_("Point2f")
+ .constructor()
+ .property("x", &cv::Point2f::x)
+ .property("y", &cv::Point2f::y);
+
+ class_("Mat")
+ .property("rows", &cv::Mat::rows)
+ .property("columns", &cv::Mat::cols)
+ .property("data", &matData);
+
+ value_object("Impact")
+ .field("distance", &JSImpact::distance)
+ .field("score", &JSImpact::score)
+ .field("zone", &JSImpact::zone)
+ .field("angle", &JSImpact::angle)
+ .field("count", &JSImpact::count);
+
+ register_vector("ImpactVector");
+
+ value_object("ImpactResults")
+ .field("annotatedImage", &JSImpactResults::annotatedImage)
+ .field("impacts", &JSImpactResults::impacts);
+
+ function("processTargetImage", &processTargetImage);
+ function("getSheetCoordinates", &getSheetCoordinates);
+ function("setLoggingEnabled", &subvision::setLoggingEnabled);
}
+
+/** @} */ // end of emscripten_bindings group
diff --git a/include/constants.h b/include/constants.h
index 70315f6..2b366a1 100644
--- a/include/constants.h
+++ b/include/constants.h
@@ -1,20 +1,60 @@
+/**
+ * @file constants.h
+ * @brief Global constants for the Subvision CV library.
+ *
+ * Defines target zone identifiers, standard image processing dimensions,
+ * and morphological kernels used throughout the detection pipeline.
+ */
+
#ifndef SUBVISION_CORE_CONSTANTS_H
#define SUBVISION_CORE_CONSTANTS_H
#include
namespace subvision {
- const int SUBVISION_ZONE_TOP_LEFT = 0;
- const int SUBVISION_ZONE_TOP_RIGHT = 1;
- const int SUBVISION_ZONE_BOTTOM_LEFT = 2;
- const int SUBVISION_ZONE_BOTTOM_RIGHT = 3;
- const int SUBVISION_ZONE_CENTER = 4;
- const int SUBVISION_ZONE_UNDEFINED = -1;
-
- const int PICTURE_WIDTH_SHEET_DETECTION = 2000;
- const int PICTURE_HEIGHT_SHEET_DETECTION = 2000;
- const cv::Size KERNEL_SIZE(PICTURE_WIDTH_SHEET_DETECTION / 200, PICTURE_WIDTH_SHEET_DETECTION / 200);
- const cv::Mat ROUND_KERNEL = cv::getStructuringElement(cv::MORPH_ELLIPSE, KERNEL_SIZE);
-}
-
-#endif //SUBVISION_CORE_CONSTANTS_H
+
+/** @name Target Zone Identifiers
+ * Constants identifying the five target zones on a standard
+ * underwater shooting sheet (four corners + center).
+ * @{
+ */
+const int SUBVISION_ZONE_TOP_LEFT = 0; ///< Top-left target zone.
+const int SUBVISION_ZONE_TOP_RIGHT = 1; ///< Top-right target zone.
+const int SUBVISION_ZONE_BOTTOM_LEFT = 2; ///< Bottom-left target zone.
+const int SUBVISION_ZONE_BOTTOM_RIGHT = 3; ///< Bottom-right target zone.
+const int SUBVISION_ZONE_CENTER = 4; ///< Center target zone.
+const int SUBVISION_ZONE_UNDEFINED = -1; ///< Undefined or unresolved zone.
+/** @} */
+
+/** @name Image Processing Dimensions
+ * Standard dimensions to which sheet images are resized
+ * before detection processing. All coordinate calculations
+ * are performed relative to these dimensions.
+ * @{
+ */
+const int PICTURE_WIDTH_SHEET_DETECTION =
+ 2000; ///< Standard processing width in pixels.
+const int PICTURE_HEIGHT_SHEET_DETECTION =
+ 2000; ///< Standard processing height in pixels.
+/** @} */
+
+/**
+ * @brief Kernel size for morphological operations, derived from processing
+ * dimensions.
+ *
+ * Set to 1/200th of the standard processing width (10×10 pixels at 2000px).
+ */
+const cv::Size KERNEL_SIZE(PICTURE_WIDTH_SHEET_DETECTION / 200,
+ PICTURE_WIDTH_SHEET_DETECTION / 200);
+
+/**
+ * @brief Pre-built elliptical structuring element for morphological operations.
+ *
+ * Used in contour filtering, mask cleanup, and noise removal stages
+ * of the image processing pipeline.
+ */
+const cv::Mat ROUND_KERNEL =
+ cv::getStructuringElement(cv::MORPH_ELLIPSE, KERNEL_SIZE);
+} // namespace subvision
+
+#endif // SUBVISION_CORE_CONSTANTS_H
diff --git a/include/image_processing.h b/include/image_processing.h
index aec5ed5..b130fa7 100644
--- a/include/image_processing.h
+++ b/include/image_processing.h
@@ -1,25 +1,97 @@
+/**
+ * @file image_processing.h
+ * @brief Low-level image processing functions for the Subvision CV pipeline.
+ *
+ * Provides contour analysis, impact mask generation, impact coordinate
+ * extraction, colour masking, and ellipse fitting routines used by
+ * higher-level detection modules.
+ */
+
#ifndef SUBVISION_CORE_IMAGE_PROCESSING_H
#define SUBVISION_CORE_IMAGE_PROCESSING_H
+#include "types.h"
#include
#include
-#include "types.h"
+
namespace subvision {
- // Obtenir le plus grand contour valide
- std::vector getBiggestValidContour(const std::vector> &contours);
- // Obtenir le masque des impacts
- cv::Mat getImpactsMask(const cv::Mat &image);
+/**
+ * @brief Find the largest valid quadrilateral contour from a list of contours.
+ *
+ * Iterates through all contours, approximates each to a polygon,
+ * and selects the largest 4-sided polygon whose area ratio is between
+ * 10% and 90% of the total image area, and whose corner angles are
+ * all within 70°–110° (approximately rectangular).
+ *
+ * Used primarily to detect the shooting sheet boundary.
+ *
+ * @param contours Vector of contour point vectors to search.
+ * @return The 4-point approximation of the biggest valid contour,
+ * or an empty vector if none qualifies.
+ *
+ * @note All area/angle calculations use the standard processing
+ * dimensions defined in constants.h.
+ */
+std::vector
+getBiggestValidContour(const std::vector> &contours);
+
+/**
+ * @brief Generate a binary mask highlighting detected impact locations.
+ *
+ * Processing pipeline:
+ * 1. Convert to Lab and apply CLAHE on the L channel for normalisation
+ * 2. Convert to HSV and threshold for red hue ranges (impacts are red)
+ * 3. Apply morphological open/close to remove noise
+ * 4. Filter contours by area (0.005%–1% of image) and circularity (>0.6)
+ *
+ * @param image Input BGR image (typically a cropped sheet image).
+ * @return Binary mask (CV_8UC1) where white pixels indicate impact regions.
+ *
+ * @warning Input must be in BGR colour space.
+ */
+cv::Mat getImpactsMask(const cv::Mat &image);
- // Obtenir les coordonnées des impacts
- std::vector getImpactsCoordinates(const cv::Mat &image);
+/**
+ * @brief Detect impact center coordinates from an image.
+ *
+ * Generates the impact mask via getImpactsMask(), finds external contours,
+ * fits ellipses to each contour (minimum 5 points), and returns the
+ * centers of all valid ellipses.
+ *
+ * @param image Input BGR image of the sheet.
+ * @return Vector of 2D center points of detected impacts.
+ *
+ * @see getImpactsMask
+ */
+std::vector getImpactsCoordinates(const cv::Mat &image);
- // Obtenir un masque de couleur
- cv::Mat getColorMask(const cv::Mat &mat, const cv::Scalar &color);
+/**
+ * @brief Create a binary mask for a specific colour in the image.
+ *
+ * Converts the target colour to HSV, constructs a narrow hue range
+ * (±10), and thresholds the input image in HSV space. The resulting
+ * mask is cleaned with erosion and dilation.
+ *
+ * @param mat Input BGR image.
+ * @param color Target colour as an RGB cv::Scalar.
+ * @return Binary mask (CV_8UC1) of matching regions.
+ */
+cv::Mat getColorMask(const cv::Mat &mat, const cv::Scalar &color);
- // Extraire une ellipse d'une image
- Ellipse retrieveEllipse(const cv::Mat &image);
-}
+/**
+ * @brief Extract the best-fit ellipse from a binary image.
+ *
+ * Finds external contours, selects the one with the largest area,
+ * and fits an ellipse using `cv::fitEllipse()`. Falls back to
+ * using non-zero pixel points if the contour has fewer than 5 points.
+ *
+ * @param image Binary input image (CV_8UC1).
+ * @return The detected Ellipse (center, size, angle), or a zero-sized
+ * ellipse at origin if no valid contour is found.
+ */
+Ellipse retrieveEllipse(const cv::Mat &image);
+} // namespace subvision
-#endif //SUBVISION_CORE_IMAGE_PROCESSING_H
+#endif // SUBVISION_CORE_IMAGE_PROCESSING_H
diff --git a/include/impact_detection.h b/include/impact_detection.h
index 9eef2ec..a75911e 100644
--- a/include/impact_detection.h
+++ b/include/impact_detection.h
@@ -1,17 +1,89 @@
+/**
+ * @file impact_detection.h
+ * @brief High-level impact detection and scoring pipeline.
+ *
+ * Provides the main entry point for processing a target shooting sheet image:
+ * detecting sheet boundaries, locating targets, finding impacts, and computing
+ * scores.
+ */
+
#ifndef SUBVISION_CORE_IMPACT_DETECTION_H
#define SUBVISION_CORE_IMPACT_DETECTION_H
+#include "types.h"
#include
#include
-#include "types.h"
+
namespace subvision {
- // Dessiner les impacts sur l'image et obtenir les points d'impact
- std::vector drawAndGetImpactsPoints(const std::vector &impacts, cv::Mat &sheetMat,
- const std::map &targetsEllipsis);
- // Traiter une image pour détecter les impacts
- bool retrieveImpacts(const cv::Mat &imageToProcess, ImpactResults &results);
-}
+/**
+ * @brief Annotate impacts on the sheet image and compute their scores.
+ *
+ * For each detected impact point:
+ * 1. Determine the closest target zone
+ * 2. Calculate the real-world distance from the target center
+ * 3. Compute the score using the federation scoring table
+ * 4. Draw visual annotations (lines, circles, score text) on the image
+ *
+ * @param impacts Vector of detected impact center coordinates
+ * (sheet-global).
+ * @param sheetMat The sheet image to annotate (modified in place).
+ * @param targetsEllipsis Map of zone ID → target Ellipse in sheet-global
+ * coordinates.
+ * @return Vector of Impact objects with distance, score, zone, and angle.
+ *
+ * @see retrieveImpacts for the full processing pipeline
+ *
+ * @code
+ * auto impacts = subvision::getImpactsCoordinates(sheetMat);
+ * auto targets = subvision::getTargetsEllipse(sheetMat);
+ * targets = subvision::targetCoordinatesToSheetCoordinates(targets);
+ * auto results = subvision::drawAndGetImpactsPoints(impacts, sheetMat,
+ * targets);
+ * @endcode
+ */
+std::vector
+drawAndGetImpactsPoints(const std::vector &impacts,
+ cv::Mat &sheetMat,
+ const std::map &targetsEllipsis);
+
+/**
+ * @brief Process a raw image to detect and score all impacts.
+ *
+ * This is the main entry point of the Subvision CV pipeline. It performs:
+ * 1. Sheet detection (automatic or manual via provided coordinates)
+ * 2. Perspective correction to standard dimensions (2000×2000)
+ * 3. Target ellipse detection for all five zones
+ * 4. Impact localisation via red-colour analysis
+ * 5. Score computation and image annotation
+ *
+ * @param imageToProcess The input image in BGR format.
+ * @param results Output structure receiving the annotated image and
+ * impact list.
+ * @param coordinates Optional pre-computed sheet corner coordinates
+ * (percentage-based). If empty, automatic sheet detection is performed.
+ * @return `true` if processing succeeded, `false` on failure (e.g., sheet not
+ * found).
+ *
+ * @note In WebAssembly, input images arrive as RGBA and are converted to BGR
+ * before calling this function.
+ * @warning The input image is cloned internally; the original is not modified.
+ *
+ * Example (C++):
+ * @code
+ * cv::Mat image = cv::imread("target_sheet.jpg");
+ * subvision::ImpactResults results;
+ * bool ok = subvision::retrieveImpacts(image, results);
+ * if (ok) {
+ * for (const auto& impact : results.impacts) {
+ * std::cout << "Score: " << impact.score << std::endl;
+ * }
+ * }
+ * @endcode
+ */
+bool retrieveImpacts(const cv::Mat &imageToProcess, ImpactResults &results,
+ const std::vector &coordinates = {});
+} // namespace subvision
-#endif //SUBVISION_CORE_IMPACT_DETECTION_H
+#endif // SUBVISION_CORE_IMPACT_DETECTION_H
diff --git a/include/logging.h b/include/logging.h
index b3b6183..c83ba9d 100644
--- a/include/logging.h
+++ b/include/logging.h
@@ -1,11 +1,51 @@
+/**
+ * @file logging.h
+ * @brief Cross-platform logging utilities for the Subvision CV library.
+ *
+ * Provides a simple logging mechanism that adapts to the build target:
+ * - **Emscripten**: uses `emscripten_log()` to write to the browser console
+ * - **C++/CLI (.NET)**: uses `System::Console::WriteLine()`
+ * - **Native C++**: uses `std::cout`
+ *
+ * Logging is disabled by default and can be enabled at runtime
+ * via setLoggingEnabled(). When disabled, log calls have zero overhead.
+ */
+
#pragma once
#include
namespace subvision {
- extern bool g_loggingEnabled;
+/**
+ * @brief Global flag controlling whether log messages are emitted.
+ *
+ * Defaults to `false`. Set via setLoggingEnabled().
+ */
+extern bool g_loggingEnabled;
+
+/**
+ * @brief Enable or disable runtime logging.
+ *
+ * @param enabled `true` to enable log output, `false` to suppress.
+ *
+ * @note In WebAssembly builds, this function is exported to JavaScript
+ * via `Module.setLoggingEnabled(true)`.
+ *
+ * @code
+ * subvision::setLoggingEnabled(true); // C++
+ * Module.setLoggingEnabled(true); // JavaScript
+ * SubvisionCore.SetLoggingEnabled(true); // C#
+ * @endcode
+ */
+void setLoggingEnabled(bool enabled);
- void setLoggingEnabled(bool enabled);
- void log(const std::string &msg);
+/**
+ * @brief Write a log message to the platform-appropriate output.
+ *
+ * Does nothing if logging is disabled (g_loggingEnabled == false).
+ *
+ * @param msg The message string to log.
+ */
+void log(const std::string &msg);
-}
+} // namespace subvision
diff --git a/include/sheet_detection.h b/include/sheet_detection.h
index d6ac8b5..6c805ee 100644
--- a/include/sheet_detection.h
+++ b/include/sheet_detection.h
@@ -1,14 +1,71 @@
-//
-// Created by Paul on 15/06/2025.
-//
+/**
+ * @file sheet_detection.h
+ * @brief Shooting sheet detection and perspective correction.
+ *
+ * Provides functions to automatically detect the shooting sheet
+ * (plastron) in a photograph, extract its corner coordinates, and
+ * apply a perspective transform to produce a standardised flat image.
+ */
#ifndef SHEET_DETECTION_H
#define SHEET_DETECTION_H
#include
namespace subvision {
- cv::Mat getSheetPicture(const cv::Mat& image) ;
- std::vector getSheetCoordinates(const cv::Mat& sheet_mat) ;
-}
-#endif //SHEET_DETECTION_H
+/**
+ * @brief Automatically detect and extract the shooting sheet from an image.
+ *
+ * Combines getSheetCoordinates() and getSheetPictureManually() to perform
+ * full automatic sheet extraction in a single call.
+ *
+ * @param image Input BGR image containing the shooting sheet.
+ * @return Perspective-corrected sheet image at standard processing dimensions
+ * (PICTURE_WIDTH_SHEET_DETECTION × PICTURE_HEIGHT_SHEET_DETECTION).
+ *
+ * @throws std::runtime_error If no valid sheet contour is found.
+ *
+ * @see getSheetCoordinates, getSheetPictureManually
+ */
+cv::Mat getSheetPicture(const cv::Mat &image);
+
+/**
+ * @brief Extract the shooting sheet using pre-computed corner coordinates.
+ *
+ * Applies a perspective transform using the provided corner points
+ * (in normalised percentage format) to produce a flat, standardised
+ * sheet image.
+ *
+ * @param image Input BGR image containing the shooting sheet.
+ * @param coordinates Four corner points in normalised [0, 1] coordinates.
+ * Must contain exactly 4 points in the order:
+ * top-left, top-right, bottom-right, bottom-left.
+ * @return Perspective-corrected sheet image at standard dimensions.
+ *
+ * @throws std::runtime_error If coordinates are empty or not exactly 4 points.
+ */
+cv::Mat getSheetPictureManually(const cv::Mat &image,
+ const std::vector coordinates);
+
+/**
+ * @brief Detect the four corner coordinates of the shooting sheet.
+ *
+ * Processing pipeline:
+ * 1. Resize to standard dimensions
+ * 2. Convert to HLS and extract the lightness channel
+ * 3. Threshold to find bright regions (the white sheet)
+ * 4. Find contours and select the biggest valid quadrilateral
+ * 5. Return corners as normalised percentage coordinates
+ *
+ * @param sheet_mat Input BGR image containing the shooting sheet.
+ * @return Vector of 4 corner points in normalised [0, 1] coordinates.
+ *
+ * @throws std::runtime_error If no valid quadrilateral contour is found.
+ *
+ * @note The returned coordinates are resolution-independent percentages,
+ * suitable for storage and later reuse with getSheetPictureManually().
+ */
+std::vector getSheetCoordinates(const cv::Mat &sheet_mat);
+} // namespace subvision
+
+#endif // SHEET_DETECTION_H
diff --git a/include/subvision_cv.h b/include/subvision_cv.h
index 87c3f26..cac98b7 100644
--- a/include/subvision_cv.h
+++ b/include/subvision_cv.h
@@ -1,12 +1,34 @@
+/**
+ * @file subvision_cv.h
+ * @brief Umbrella header for the Subvision CV library.
+ *
+ * Include this single header to access the entire Subvision CV public API.
+ * It aggregates all module headers: constants, types, utilities, image
+ * processing, target detection, impact detection, and sheet detection.
+ *
+ * @code
+ * #include "subvision_cv.h"
+ *
+ * cv::Mat image = cv::imread("sheet.jpg");
+ * subvision::ImpactResults results;
+ * if (subvision::retrieveImpacts(image, results)) {
+ * for (const auto& impact : results.impacts) {
+ * std::cout << "Score: " << impact.score << std::endl;
+ * }
+ * }
+ * @endcode
+ */
+
#ifndef SUBVISION_CORE_H
#define SUBVISION_CORE_H
#include "constants.h"
-#include "types.h"
-#include "utils.h"
#include "image_processing.h"
-#include "target_detection.h"
#include "impact_detection.h"
#include "sheet_detection.h"
+#include "target_detection.h"
+#include "types.h"
+#include "utils.h"
+
-#endif //SUBVISION_CORE_H
+#endif // SUBVISION_CORE_H
diff --git a/include/target_detection.h b/include/target_detection.h
index dedb428..0865361 100644
--- a/include/target_detection.h
+++ b/include/target_detection.h
@@ -1,27 +1,104 @@
+/**
+ * @file target_detection.h
+ * @brief Target ellipse detection and visualisation functions.
+ *
+ * Provides functions to detect the concentric ring targets on an
+ * underwater shooting sheet, convert coordinates between target-local
+ * and sheet-global frames, and draw target overlays on annotated images.
+ */
+
#ifndef SUBVISION_CORE_TARGET_DETECTION_H
#define SUBVISION_CORE_TARGET_DETECTION_H
-#include
-#include