From 554eff9b55703f04fcbe5e4729e5051efa66624d Mon Sep 17 00:00:00 2001 From: AndreaRigoni Date: Thu, 5 Mar 2026 15:03:19 +0000 Subject: [PATCH] add filters in python bindings --- src/Math/VoxImage.h | 3 +- src/Math/VoxImageFilterABTrim.hpp | 6 +- src/Math/VoxImageFilterLinear.hpp | 4 +- src/Python/CMakeLists.txt | 8 +- src/Python/math_bindings.cpp | 3 +- src/Python/math_filters_bindings.cpp | 100 ++++++++++++++++ src/Python/module.cpp | 2 + src/Python/testing/math_filters_test.py | 151 ++++++++++++++++++++++++ 8 files changed, 269 insertions(+), 8 deletions(-) create mode 100644 src/Python/math_filters_bindings.cpp create mode 100644 src/Python/testing/math_filters_test.py diff --git a/src/Math/VoxImage.h b/src/Math/VoxImage.h index eba6125..66b276a 100644 --- a/src/Math/VoxImage.h +++ b/src/Math/VoxImage.h @@ -90,7 +90,8 @@ struct Voxel { } // namespace Interface struct Voxel { - Scalarf Value; + Scalarf Value = 0.0f; + Scalari Count = 0; }; //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Math/VoxImageFilterABTrim.hpp b/src/Math/VoxImageFilterABTrim.hpp index de3c117..621c00d 100644 --- a/src/Math/VoxImageFilterABTrim.hpp +++ b/src/Math/VoxImageFilterABTrim.hpp @@ -36,7 +36,7 @@ namespace uLib { -#ifdef USE_CUDA +#if defined(USE_CUDA) && defined(__CUDACC__) template __global__ void ABTrimFilterKernel(const VoxelT *in, VoxelT *out, const VoxelT *kernel, int vox_size, @@ -108,7 +108,7 @@ public: mBtrim = 0; } -#ifdef USE_CUDA +#if defined(USE_CUDA) && defined(__CUDACC__) void Run() { if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM || this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) { @@ -206,7 +206,7 @@ public: mBtrim = 0; } -#ifdef USE_CUDA +#if defined(USE_CUDA) && defined(__CUDACC__) void Run() { if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM || this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) { diff --git a/src/Math/VoxImageFilterLinear.hpp b/src/Math/VoxImageFilterLinear.hpp index 8079d61..420254a 100644 --- a/src/Math/VoxImageFilterLinear.hpp +++ b/src/Math/VoxImageFilterLinear.hpp @@ -36,7 +36,7 @@ namespace uLib { -#ifdef USE_CUDA +#if defined(USE_CUDA) && defined(__CUDACC__) template __global__ void LinearFilterKernel(const VoxelT *in, VoxelT *out, const VoxelT *kernel, int vox_size, @@ -66,7 +66,7 @@ public: typedef VoxImageFilter> BaseClass; VoxFilterAlgorithmLinear(const Vector3i &size) : BaseClass(size) {} -#ifdef USE_CUDA +#if defined(USE_CUDA) && defined(__CUDACC__) void Run() { if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM || this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) { diff --git a/src/Python/CMakeLists.txt b/src/Python/CMakeLists.txt index b165b98..5ca99bf 100644 --- a/src/Python/CMakeLists.txt +++ b/src/Python/CMakeLists.txt @@ -4,10 +4,11 @@ set(SOURCES module.cpp core_bindings.cpp math_bindings.cpp + math_filters_bindings.cpp ) # Use pybind11 to add the python module -pybind11_add_module(uLib_python module.cpp core_bindings.cpp math_bindings.cpp) +pybind11_add_module(uLib_python module.cpp core_bindings.cpp math_bindings.cpp math_filters_bindings.cpp) # Link against our C++ libraries target_link_libraries(uLib_python PRIVATE @@ -49,4 +50,9 @@ if(BUILD_TESTING) COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/math_pybind_test.py) set_tests_properties(pybind_math PROPERTIES ENVIRONMENT "PYTHONPATH=$:${PROJECT_SOURCE_DIR}/src/Python") + + add_test(NAME pybind_math_filters + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/testing/math_filters_test.py) + set_tests_properties(pybind_math_filters PROPERTIES + ENVIRONMENT "PYTHONPATH=$:${PROJECT_SOURCE_DIR}/src/Python") endif() diff --git a/src/Python/math_bindings.cpp b/src/Python/math_bindings.cpp index 2c60f88..cdf2eed 100644 --- a/src/Python/math_bindings.cpp +++ b/src/Python/math_bindings.cpp @@ -327,7 +327,8 @@ void init_math(py::module_ &m) { // 6. High-level Structures py::class_(m, "Voxel") .def(py::init<>()) - .def_readwrite("Value", &Voxel::Value); + .def_readwrite("Value", &Voxel::Value) + .def_readwrite("Count", &Voxel::Count); py::class_(m, "AbstractVoxImage") .def("GetValue", py::overload_cast(&Abstract::VoxImage::GetValue, py::const_)) diff --git a/src/Python/math_filters_bindings.cpp b/src/Python/math_filters_bindings.cpp new file mode 100644 index 0000000..109ffcf --- /dev/null +++ b/src/Python/math_filters_bindings.cpp @@ -0,0 +1,100 @@ +#include +#include +#include + +#include "Math/VoxImage.h" +#include "Math/VoxImageFilter.h" +#include "Math/VoxImageFilterLinear.hpp" +#include "Math/VoxImageFilterABTrim.hpp" +#include "Math/VoxImageFilterBilateral.hpp" +#include "Math/VoxImageFilterThreshold.hpp" +#include "Math/VoxImageFilterMedian.hpp" +#include "Math/VoxImageFilter2ndStat.hpp" +#include "Math/VoxImageFilterCustom.hpp" + +namespace py = pybind11; +using namespace uLib; + +template +void bind_common_filter(py::class_ &cls) { + cls.def(py::init()) + .def("Run", &Algorithm::Run) + .def("SetKernelNumericXZY", &Algorithm::SetKernelNumericXZY) + .def("GetImage", &Algorithm::GetImage, py::return_value_policy::reference_internal) + .def("SetImage", &Algorithm::SetImage); +} + +void init_math_filters(py::module_ &m) { + + // Abstract::VoxImageFilter + py::class_>(m, "AbstractVoxImageFilter") + .def("Run", &Abstract::VoxImageFilter::Run) + .def("SetImage", &Abstract::VoxImageFilter::SetImage); + + // Helper macro to define standard bindings for a filter +#define BIND_FILTER(ClassName) \ + { \ + auto cls = py::class_, Abstract::VoxImageFilter>(m, #ClassName); \ + bind_common_filter(cls); \ + } + + // VoxFilterAlgorithmLinear + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmLinear"); + bind_common_filter(cls); + } + + // VoxFilterAlgorithmAbtrim + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmAbtrim"); + bind_common_filter(cls); + cls.def("SetABTrim", &VoxFilterAlgorithmAbtrim::SetABTrim); + } + + // VoxFilterAlgorithmSPR + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmSPR"); + bind_common_filter(cls); + cls.def("SetABTrim", &VoxFilterAlgorithmSPR::SetABTrim); + } + + // VoxFilterAlgorithmBilateral + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmBilateral"); + bind_common_filter(cls); + cls.def("SetIntensitySigma", &VoxFilterAlgorithmBilateral::SetIntensitySigma); + } + + // VoxFilterAlgorithmBilateralTrim + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmBilateralTrim"); + bind_common_filter(cls); + cls.def("SetIntensitySigma", &VoxFilterAlgorithmBilateralTrim::SetIntensitySigma); + cls.def("SetABTrim", &VoxFilterAlgorithmBilateralTrim::SetABTrim); + } + + // VoxFilterAlgorithmThreshold + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmThreshold"); + bind_common_filter(cls); + cls.def("SetThreshold", &VoxFilterAlgorithmThreshold::SetThreshold); + } + + // VoxFilterAlgorithmMedian + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmMedian"); + bind_common_filter(cls); + } + + // VoxFilterAlgorithm2ndStat + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithm2ndStat"); + bind_common_filter(cls); + } + + // VoxFilterAlgorithmCustom (Omit CustomEvaluate since it uses static function ptrs) + { + auto cls = py::class_, Abstract::VoxImageFilter>(m, "VoxFilterAlgorithmCustom"); + bind_common_filter(cls); + } +} diff --git a/src/Python/module.cpp b/src/Python/module.cpp index 0cef30d..0cf5d57 100644 --- a/src/Python/module.cpp +++ b/src/Python/module.cpp @@ -4,6 +4,7 @@ namespace py = pybind11; void init_core(py::module_ &m); void init_math(py::module_ &m); +void init_math_filters(py::module_ &m); PYBIND11_MODULE(uLib_python, m) { m.doc() = "Python bindings for uLib Core and Math libraries"; @@ -15,4 +16,5 @@ PYBIND11_MODULE(uLib_python, m) { // Math submodule py::module_ math = m.def_submodule("Math", "Math library bindings"); init_math(math); + init_math_filters(math); } diff --git a/src/Python/testing/math_filters_test.py b/src/Python/testing/math_filters_test.py new file mode 100644 index 0000000..fd8cd8b --- /dev/null +++ b/src/Python/testing/math_filters_test.py @@ -0,0 +1,151 @@ +import unittest +import numpy as np +import os +import sys + +# Ensure PYTHONPATH is correct if run from root +sys.path.append(os.path.join(os.getcwd(), 'src', 'Python')) + +import uLib + +class TestMathFilters(unittest.TestCase): + def test_filter_creation(self): + # 1. Linear Filter + dims = [10, 10, 10] + v_dims = uLib.Math.Vector3i(dims) + + linear_filter = uLib.Math.VoxFilterAlgorithmLinear(v_dims) + self.assertIsNotNone(linear_filter) + + # 2. ABTrim Filter + abtrim_filter = uLib.Math.VoxFilterAlgorithmAbtrim(v_dims) + self.assertIsNotNone(abtrim_filter) + abtrim_filter.SetABTrim(1, 1) + + # 3. Bilateral Filter + bilat_filter = uLib.Math.VoxFilterAlgorithmBilateral(v_dims) + self.assertIsNotNone(bilat_filter) + bilat_filter.SetIntensitySigma(0.5) + + # 4. Threshold Filter + threshold_filter = uLib.Math.VoxFilterAlgorithmThreshold(v_dims) + self.assertIsNotNone(threshold_filter) + threshold_filter.SetThreshold(0.5) + + # 5. Median Filter + median_filter = uLib.Math.VoxFilterAlgorithmMedian(v_dims) + self.assertIsNotNone(median_filter) + + def test_filter_run(self): + # Create image + dims = [10, 10, 10] + vox_img = uLib.Math.VoxImage(dims) + for i in range(10*10*10): + vox_img.SetValue(i, 1.0) + + # Linear filter + linear_filter = uLib.Math.VoxFilterAlgorithmLinear([3, 3, 3]) + linear_filter.SetImage(vox_img) + + # Set kernel (simple 3x3x3 all ones) + # Weights are usually normalized in linear filter logic? + # Let's just test it runs. + linear_filter.SetKernelNumericXZY([1.0] * 27) + + # Run filter + linear_filter.Run() + + # Value should be 1.0 (mean of all 1.0 is 1.0) + self.assertAlmostEqual(vox_img.GetValue(0), 1.0) + + def test_filter_run_abtrim(self): + # Create image + dims = [10, 10, 10] + vox_img = uLib.Math.VoxImage(dims) + for i in range(10*10*10): + vox_img.SetValue(i, 1.0) + + # ABTrim filter + abtrim_filter = uLib.Math.VoxFilterAlgorithmAbtrim([3, 3, 3]) + abtrim_filter.SetImage(vox_img) + + # Set kernel (simple 3x3x3 all ones) + # Weights are usually normalized in linear filter logic? + # Let's just test it runs. + abtrim_filter.SetKernelNumericXZY([1.0] * 27) + + # Run filter + abtrim_filter.Run() + + # Value should be 1.0 (mean of all 1.0 is 1.0) + self.assertAlmostEqual(vox_img.GetValue(0), 1.0) + + def test_filter_run_bilateral(self): + # Create image + dims = [10, 10, 10] + vox_img = uLib.Math.VoxImage(dims) + for i in range(10*10*10): + vox_img.SetValue(i, 1.0) + + # Bilateral filter + bilat_filter = uLib.Math.VoxFilterAlgorithmBilateral([3, 3, 3]) + bilat_filter.SetImage(vox_img) + + # Set kernel (simple 3x3x3 all ones) + # Weights are usually normalized in linear filter logic? + # Let's just test it runs. + bilat_filter.SetKernelNumericXZY([1.0] * 27) + + # Run filter + bilat_filter.Run() + + # Value should be 1.0 (mean of all 1.0 is 1.0) + self.assertAlmostEqual(vox_img.GetValue(0), 1.0) + + def test_filter_run_threshold(self): + # Create image + dims = [10, 10, 10] + vox_img = uLib.Math.VoxImage(dims) + for i in range(10*10*10): + vox_img.SetValue(i, 1.0) + + # Threshold filter + threshold_filter = uLib.Math.VoxFilterAlgorithmThreshold([3, 3, 3]) + threshold_filter.SetImage(vox_img) + + # Set kernel (simple 3x3x3 all ones) + # Weights are usually normalized in linear filter logic? + # Let's just test it runs. + threshold_filter.SetKernelNumericXZY([1.0] * 27) + + # Run filter + threshold_filter.Run() + + # Value should be 1.0 (mean of all 1.0 is 1.0) + self.assertAlmostEqual(vox_img.GetValue(0), 1.0) + + def test_filter_run_median(self): + # Create image + dims = [10, 10, 10] + vox_img = uLib.Math.VoxImage(dims) + for i in range(10*10*10): + vox_img.SetValue(i, 1.0) + + # Median filter + median_filter = uLib.Math.VoxFilterAlgorithmMedian([3, 3, 3]) + median_filter.SetImage(vox_img) + + # Set kernel (simple 3x3x3 all ones) + # Weights are usually normalized in linear filter logic? + # Let's just test it runs. + median_filter.SetKernelNumericXZY([1.0] * 27) + + # Run filter + median_filter.Run() + + # Value should be 1.0 (mean of all 1.0 is 1.0) + self.assertAlmostEqual(vox_img.GetValue(0), 1.0) + + +if __name__ == '__main__': + unittest.main()