4 Commits

Author SHA1 Message Date
AndreaRigoni
876b8f4592 algorithm chain for ram-vram 2026-03-28 08:22:14 +00:00
AndreaRigoni
ec2027e980 check if algorithm could be run on cuda 2026-03-27 16:42:04 +00:00
AndreaRigoni
69b47623f8 algorithm on filters 2026-03-27 02:45:40 +00:00
AndreaRigoni
f5c1e317e8 algorithm def 2026-03-27 02:29:56 +00:00
25 changed files with 1780 additions and 240 deletions

View File

@@ -0,0 +1,7 @@
---
trigger: always_on
---
build in build directory using always micromamba "mutom" env.
build with make flag -j$(nproc).

View File

@@ -21,7 +21,7 @@ endif()
project(uLib)
# CUDA Toolkit seems to be missing locally. Toggle ON if nvcc is made available.
option(USE_CUDA "Enable CUDA support" ON)
option(USE_CUDA "Enable CUDA support" OFF)
if(USE_CUDA)
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -allow-unsupported-compiler")
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
@@ -115,7 +115,7 @@ set(Boost_USE_MULTITHREADED ON)
set(Boost_USE_STATIC_RUNTIME OFF)
message(STATUS "CMAKE_PREFIX_PATH is ${CMAKE_PREFIX_PATH}")
find_package(HDF5 REQUIRED CONFIG)
find_package(HDF5 REQUIRED)
find_package(Boost 1.45.0 COMPONENTS program_options serialization unit_test_framework REQUIRED)
include_directories(${Boost_INCLUDE_DIRS})

View File

@@ -7,6 +7,7 @@
#include "Vtk/uLibVtkInterface.h"
#include "Math/Units.h"
#include "Math/Dense.h"
#include "Settings.h"
namespace uLib {
namespace Qt {
@@ -15,8 +16,21 @@ PropertyWidgetBase::PropertyWidgetBase(PropertyBase* prop, QWidget* parent)
: QWidget(parent), m_BaseProperty(prop) {
m_Layout = new QHBoxLayout(this);
m_Layout->setContentsMargins(4, 2, 4, 2);
m_Label = new QLabel(QString::fromStdString(prop->GetName()), this);
m_Label->setMinimumWidth(100);
std::string unit = prop->GetUnits();
QString labelText = QString::fromStdString(prop->GetName());
if (!unit.empty()) {
auto dim = Settings::Instance().IdentifyDimension(unit);
std::string pref = Settings::Instance().GetPreferredUnit(dim);
if (!pref.empty()) {
labelText += " [" + QString::fromStdString(pref) + "]";
} else {
labelText += " [" + QString::fromStdString(unit) + "]";
}
}
m_Label = new QLabel(labelText, this);
m_Label->setMinimumWidth(120);
m_Layout->addWidget(m_Label);
}
PropertyWidgetBase::~PropertyWidgetBase() {
@@ -115,9 +129,6 @@ void UnitLineEdit::updateText() {
s += ".0";
}
}
if (!m_Suffix.isEmpty()) {
s += " " + m_Suffix;
}
setText(s);
}
@@ -129,11 +140,12 @@ void UnitLineEdit::setIntegerOnly(bool integerOnly) {
DoublePropertyWidget::DoublePropertyWidget(Property<double>* prop, QWidget* parent)
: PropertyWidgetBase(prop, parent), m_Prop(prop) {
m_Edit = new UnitLineEdit(this);
QString units = QString::fromStdString(prop->GetUnits());
if (!units.isEmpty()) {
double factor = 1.0;
parseWithUnits("1 " + units, &factor);
m_Edit->setUnits(units, factor);
std::string unit = prop->GetUnits();
if (!unit.empty()) {
auto dim = Settings::Instance().IdentifyDimension(unit);
std::string pref = Settings::Instance().GetPreferredUnit(dim);
double factor = Settings::Instance().GetUnitFactor(pref);
m_Edit->setUnits(QString::fromStdString(pref), factor);
}
m_Edit->setValue(prop->Get());
m_Layout->addWidget(m_Edit, 1);
@@ -146,11 +158,12 @@ DoublePropertyWidget::DoublePropertyWidget(Property<double>* prop, QWidget* pare
FloatPropertyWidget::FloatPropertyWidget(Property<float>* prop, QWidget* parent)
: PropertyWidgetBase(prop, parent), m_Prop(prop) {
m_Edit = new UnitLineEdit(this);
QString units = QString::fromStdString(prop->GetUnits());
if (!units.isEmpty()) {
double factor = 1.0;
parseWithUnits("1 " + units, &factor);
m_Edit->setUnits(units, factor);
std::string unit = prop->GetUnits();
if (!unit.empty()) {
auto dim = Settings::Instance().IdentifyDimension(unit);
std::string pref = Settings::Instance().GetPreferredUnit(dim);
double factor = Settings::Instance().GetUnitFactor(pref);
m_Edit->setUnits(QString::fromStdString(pref), factor);
}
m_Edit->setValue(prop->Get());
m_Layout->addWidget(m_Edit, 1);
@@ -164,11 +177,12 @@ IntPropertyWidget::IntPropertyWidget(Property<int>* prop, QWidget* parent)
: PropertyWidgetBase(prop, parent), m_Prop(prop) {
m_Edit = new UnitLineEdit(this);
m_Edit->setIntegerOnly(true);
QString units = QString::fromStdString(prop->GetUnits());
if (!units.isEmpty()) {
double factor = 1.0;
parseWithUnits("1 " + units, &factor);
m_Edit->setUnits(units, factor);
std::string unit = prop->GetUnits();
if (!unit.empty()) {
auto dim = Settings::Instance().IdentifyDimension(unit);
std::string pref = Settings::Instance().GetPreferredUnit(dim);
double factor = Settings::Instance().GetUnitFactor(pref);
m_Edit->setUnits(QString::fromStdString(pref), factor);
}
m_Edit->setValue(prop->Get());
m_Layout->addWidget(m_Edit, 1);
@@ -211,6 +225,26 @@ StringPropertyWidget::StringPropertyWidget(Property<std::string>* prop, QWidget*
}
StringPropertyWidget::~StringPropertyWidget() {}
class GroupHeaderWidget : public QWidget {
public:
GroupHeaderWidget(const QString& name, QWidget* parent = nullptr) : QWidget(parent) {
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 8, 0, 4);
auto* line = new QFrame(this);
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
line->setStyleSheet("color: #555;");
layout->addWidget(line);
auto* label = new QLabel(name, this);
QFont font = label->font();
font.setBold(true);
font.setPointSize(font.pointSize() + 1);
label->setFont(font);
label->setStyleSheet("color: #aaa; text-transform: uppercase;");
layout->addWidget(label);
}
};
class EnumPropertyWidget : public PropertyWidgetBase {
PropertyBase* m_Prop;
QComboBox* m_Combo;
@@ -305,26 +339,51 @@ void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) {
}
}
// Group properties by their group string
std::map<std::string, std::vector<::uLib::PropertyBase*>> groupedProps;
std::vector<std::string> groupOrder;
for (auto* prop : *props) {
// Priority 1: Check if it provides enum labels
if (!prop->GetEnumLabels().empty()) {
m_ContainerLayout->addWidget(new EnumPropertyWidget(prop, m_Container));
continue;
std::string group = prop->GetGroup();
if (groupedProps.find(group) == groupedProps.end()) {
groupOrder.push_back(group);
}
groupedProps[group].push_back(prop);
}
for (const auto& groupName : groupOrder) {
if (!groupName.empty()) {
m_ContainerLayout->addWidget(new GroupHeaderWidget(QString::fromStdString(groupName), m_Container));
}
// Priority 2: Standard factory lookup
auto it = m_Factories.find(prop->GetTypeIndex());
if (it != m_Factories.end()) {
QWidget* widget = it->second(prop, m_Container);
m_ContainerLayout->addWidget(widget);
} else {
// Debug info for unknown types
std::cout << "PropertyEditor: No factory for " << prop->GetName()
<< " (Type: " << prop->GetTypeName() << ")" << std::endl;
for (auto* prop : groupedProps[groupName]) {
QWidget* widget = nullptr;
// Priority 1: Check if it provides enum labels
if (!prop->GetEnumLabels().empty()) {
widget = new EnumPropertyWidget(prop, m_Container);
} else {
// Priority 2: Standard factory lookup
auto it = m_Factories.find(prop->GetTypeIndex());
if (it != m_Factories.end()) {
widget = it->second(prop, m_Container);
} else {
// Debug info for unknown types
std::cout << "PropertyEditor: No factory for " << prop->GetQualifiedName()
<< " (Type: " << prop->GetTypeName() << ")" << std::endl;
QWidget* fallback = new PropertyWidgetBase(prop, m_Container);
fallback->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")"));
m_ContainerLayout->addWidget(fallback);
widget = new PropertyWidgetBase(prop, m_Container);
widget->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")"));
}
}
if (widget) {
if (!groupName.empty()) {
// Indent grouped properties
widget->setContentsMargins(16, 0, 0, 0);
}
m_ContainerLayout->addWidget(widget);
}
}
}
m_ContainerLayout->addStretch(1);

View File

@@ -16,6 +16,7 @@
#include "Core/Object.h"
#include "Core/Signal.h"
#include "Math/Dense.h"
#include "Settings.h"
namespace uLib {
namespace Qt {
@@ -93,18 +94,24 @@ class VectorPropertyWidget : public PropertyWidgetBase {
public:
VectorPropertyWidget(Property<VecT>* prop, QWidget* parent = nullptr)
: PropertyWidgetBase(prop, parent), m_Prop(prop) {
QString units = QString::fromStdString(prop->GetUnits());
std::string unit = prop->GetUnits();
double factor = 1.0;
if (!units.isEmpty()) {
parseWithUnits("1 " + units, &factor);
QString prefSuffix;
if (!unit.empty()) {
auto dim = Settings::Instance().IdentifyDimension(unit);
std::string pref = Settings::Instance().GetPreferredUnit(dim);
factor = Settings::Instance().GetUnitFactor(pref);
prefSuffix = QString::fromStdString(pref);
}
for (int i = 0; i < Size; ++i) {
m_Edits[i] = new UnitLineEdit(this);
if (std::is_integral<typename VecT::Scalar>::value) {
m_Edits[i]->setIntegerOnly(true);
}
if (!units.isEmpty()) {
m_Edits[i]->setUnits(units, factor);
if (!prefSuffix.isEmpty()) {
m_Edits[i]->setUnits(prefSuffix, factor);
}
m_Layout->addWidget(m_Edits[i], 1);

View File

@@ -0,0 +1,75 @@
#ifndef GCOMPOSE_SETTINGS_H
#define GCOMPOSE_SETTINGS_H
#include <string>
#include <map>
#include "Math/Units.h"
namespace uLib {
namespace Qt {
class Settings {
public:
static Settings& Instance() {
static Settings instance;
return instance;
}
enum Dimension {
Length,
Angle,
Energy,
Time,
Dimensionless
};
void SetPreferredUnit(Dimension dim, const std::string& unit) {
m_PreferredUnits[dim] = unit;
}
std::string GetPreferredUnit(Dimension dim) const {
auto it = m_PreferredUnits.find(dim);
if (it != m_PreferredUnits.end()) return it->second;
switch(dim) {
case Length: return "mm";
case Angle: return "deg";
case Energy: return "MeV";
case Time: return "ns";
default: return "";
}
}
double GetUnitFactor(const std::string& unit) const {
if (unit == "m") return CLHEP::meter;
if (unit == "cm") return CLHEP::centimeter;
if (unit == "mm") return CLHEP::millimeter;
if (unit == "um") return CLHEP::micrometer;
if (unit == "deg") return CLHEP::degree;
if (unit == "rad") return CLHEP::radian;
if (unit == "ns") return CLHEP::nanosecond;
if (unit == "s") return CLHEP::second;
if (unit == "ms") return CLHEP::millisecond;
if (unit == "MeV") return CLHEP::megaelectronvolt;
if (unit == "GeV") return CLHEP::gigaelectronvolt;
if (unit == "eV") return CLHEP::electronvolt;
return 1.0;
}
Dimension IdentifyDimension(const std::string& unit) const {
if (unit == "m" || unit == "cm" || unit == "mm" || unit == "um" || unit == "nm") return Length;
if (unit == "deg" || unit == "rad") return Angle;
if (unit == "MeV" || unit == "GeV" || unit == "eV" || unit == "keV" || unit == "TeV") return Energy;
if (unit == "ns" || unit == "s" || unit == "ms" || unit == "us") return Time;
return Dimensionless;
}
private:
Settings() {}
std::map<Dimension, std::string> m_PreferredUnits;
};
} // namespace Qt
} // namespace uLib
#endif

View File

@@ -46,23 +46,25 @@ ViewportPane::ViewportPane(QWidget* parent) : QWidget(parent), m_viewport(nullpt
m_layout->addWidget(m_titleBar);
// Main horizontal container for viewport and display panel
QWidget* mainArea = new QWidget(this);
QHBoxLayout* hLayout = new QHBoxLayout(mainArea);
hLayout->setContentsMargins(0, 0, 0, 0);
hLayout->setSpacing(0);
m_layout->addWidget(mainArea);
// Main area with splitter for viewport and display panel
m_areaSplitter = new QSplitter(Qt::Horizontal, this);
m_areaSplitter->setObjectName("ViewportAreaSplitter");
m_layout->addWidget(m_areaSplitter, 1);
// Viewport will be added here via setViewport
m_viewport = new uLib::Vtk::QViewport(mainArea);
hLayout->addWidget(m_viewport);
m_viewport = new uLib::Vtk::QViewport(m_areaSplitter);
m_areaSplitter->addWidget(m_viewport);
// Display Panel (Overlay/Slide-out)
m_displayPanel = new QFrame(mainArea);
m_displayPanel = new QFrame(m_areaSplitter);
m_displayPanel->setObjectName("DisplayPropertiesPanel");
m_displayPanel->setFixedWidth(250);
m_displayPanel->setMinimumWidth(150);
m_displayPanel->hide();
m_areaSplitter->addWidget(m_displayPanel);
m_areaSplitter->setStretchFactor(0, 1);
m_areaSplitter->setStretchFactor(1, 0);
QVBoxLayout* panelLayout = new QVBoxLayout(m_displayPanel);
panelLayout->setContentsMargins(5, 5, 5, 5);
@@ -72,8 +74,6 @@ ViewportPane::ViewportPane(QWidget* parent) : QWidget(parent), m_viewport(nullpt
m_displayEditor = new uLib::Qt::PropertyEditor(m_displayPanel);
panelLayout->addWidget(m_displayEditor);
hLayout->addWidget(m_displayPanel);
connect(m_toggleBtn, &QPushButton::toggled, this, &ViewportPane::toggleDisplayPanel);
connect(m_titleBar, &QWidget::customContextMenuRequested, this, &ViewportPane::showContextMenu);
@@ -85,7 +85,15 @@ ViewportPane::ViewportPane(QWidget* parent) : QWidget(parent), m_viewport(nullpt
ViewportPane::~ViewportPane() {}
void ViewportPane::toggleDisplayPanel() {
m_displayPanel->setVisible(m_toggleBtn->isChecked());
bool visible = m_toggleBtn->isChecked();
m_displayPanel->setVisible(visible);
if (visible && m_areaSplitter->sizes().value(1, 0) == 0) {
QList<int> sizes = m_areaSplitter->sizes();
int total = sizes[0] + sizes[1];
sizes[1] = 250;
sizes[0] = total - 250;
m_areaSplitter->setSizes(sizes);
}
}
void ViewportPane::setObject(uLib::Object* obj) {
@@ -107,15 +115,14 @@ void ViewportPane::setObject(uLib::Object* obj) {
void ViewportPane::setViewport(QWidget* viewport, const QString& title) {
if (m_viewport) {
m_viewport->parentWidget()->layout()->removeWidget(m_viewport);
delete m_viewport;
}
m_viewport = viewport;
m_titleLabel->setText(title);
m_viewport->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
auto* mainAreaLayout = static_cast<QHBoxLayout*>(m_displayPanel->parentWidget()->layout());
mainAreaLayout->insertWidget(0, m_viewport);
m_areaSplitter->insertWidget(0, m_viewport);
m_areaSplitter->setStretchFactor(0, 1);
}
void ViewportPane::addVtkViewport() {

View File

@@ -10,6 +10,7 @@ namespace uLib {
namespace Qt { class PropertyEditor; }
}
class QSplitter;
class QVBoxLayout;
class QLabel;
@@ -39,6 +40,7 @@ private:
QVBoxLayout* m_layout;
QWidget* m_titleBar;
QLabel* m_titleLabel;
QSplitter* m_areaSplitter;
QWidget* m_viewport;
// Display Properties Overlay

338
docs/algorithms/algoritm.md Normal file
View File

@@ -0,0 +1,338 @@
# Algorithm Infrastructure
## Overview
An algorithm in the uLib infrastructure is a class for containing a functional that can be dynamically loaded into memory as a plug-in.
It derives from the base `Object` class (`Core/Object.h`) and therefore can contain properties that define the serialization of operating parameters or the implementation of widgets for interactive parameter manipulation.
The algorithm class is designed to be inserted into an `AlgorithmTask`, a class for managing the execution of scheduled operations. A task contains `Run` and `Stop` methods to start and stop execution. A task can be configured to work in two modes:
- **Cyclic mode**: the algorithm is executed periodically with a configurable cycle time.
- **Asynchronous mode**: the task waits for a trigger before each execution. Triggers can come from the uLib signal-slot system (`Object::connect`) or from a condition variable as defined in the monitor pattern (`Core/Monitor.h`).
The algorithm is defined as a template class on two types `T_enc` and `T_dec`. The encoder is a type for data input or another algorithm that is chained with this one and outputs data in a compatible format. The decoder is the type of data output or a downstream algorithm compatible with it.
## Class Hierarchy
```
Object (Core/Object.h)
|
+-- Algorithm<T_enc, T_dec> (Core/Algorithm.h)
| |
| +-- VoxImageFilter<VoxelT, CrtpImplT> (Math/VoxImageFilter.h)
| |
| +-- VoxFilterAlgorithmLinear (Math/VoxImageFilterLinear.hpp)
| +-- VoxFilterAlgorithmMedian (Math/VoxImageFilterMedian.hpp)
| +-- VoxFilterAlgorithmAbtrim (Math/VoxImageFilterABTrim.hpp)
| +-- VoxFilterAlgorithmSPR (Math/VoxImageFilterABTrim.hpp)
| +-- VoxFilterAlgorithmThreshold (Math/VoxImageFilterThreshold.hpp)
| +-- VoxFilterAlgorithmBilateral (Math/VoxImageFilterBilateral.hpp)
| +-- VoxFilterAlgorithmBilateralTrim(Math/VoxImageFilterBilateral.hpp)
| +-- VoxFilterAlgorithm2ndStat (Math/VoxImageFilter2ndStat.hpp)
| +-- VoxFilterAlgorithmCustom (Math/VoxImageFilterCustom.hpp)
|
+-- Thread (Core/Threads.h)
|
+-- AlgorithmTask<T_enc, T_dec> (Core/Algorithm.h)
```
## Algorithm (`Core/Algorithm.h`)
### Template Parameters
```cpp
template <typename T_enc, typename T_dec>
class Algorithm : public Object;
```
- **`T_enc`** (Encoder): the input data type. Can be a raw data type or a pointer to a data structure. When chaining algorithms, the upstream algorithm's `T_dec` must be compatible with this algorithm's `T_enc`.
- **`T_dec`** (Decoder): the output data type. Produced by `Process()` and consumed by the next algorithm in the chain.
### Core Interface
| Method | Description |
|--------|-------------|
| `virtual T_dec Process(const T_enc& input) = 0` | Pure virtual. Implement the algorithm logic here. |
| `T_dec operator()(const T_enc& input)` | Calls `Process()`. Enables functional syntax: `result = alg(data)`. |
### Algorithm Chaining
Algorithms can be linked in processing pipelines via encoder/decoder pointers:
```cpp
Algorithm* upstream; // SetEncoder() / GetEncoder()
Algorithm* downstream; // SetDecoder() / GetDecoder()
```
This allows building chains like:
```
[RawData] --> AlgorithmA --> AlgorithmB --> [Result]
encoder decoder
```
### Signals
| Signal | Emitted when |
|--------|-------------|
| `Started()` | The algorithm begins processing (caller responsibility). |
| `Finished()` | The algorithm completes processing (caller responsibility). |
### Device Preference (CUDA)
Algorithms report their preferred execution device via `GetPreferredDevice()`:
| Method | Description |
|--------|-------------|
| `virtual MemoryDevice GetPreferredDevice() const` | Returns `RAM` or `VRAM`. Subclasses override. |
| `void SetPreferredDevice(MemoryDevice dev)` | Manually set the device preference. |
| `bool IsGPU() const` | Shorthand for `GetPreferredDevice() == VRAM`. |
GPU-based algorithms are responsible for calling `cudaDeviceSynchronize()` inside their `Process()` implementation before returning, so that results are available to the caller or downstream algorithm.
### Example: Defining a Custom Algorithm
```cpp
class MyFilter : public Algorithm<VoxImage<Voxel>*, VoxImage<Voxel>*> {
public:
const char* GetClassName() const override { return "MyFilter"; }
VoxImage<Voxel>* Process(VoxImage<Voxel>* const& image) override {
// ... filter the image in-place ...
return image;
}
};
```
## AlgorithmTask (`Core/Algorithm.h`)
`AlgorithmTask` manages the execution of an `Algorithm` within a scheduled, threaded context. It inherits from `Thread` (`Core/Threads.h`) and uses `Mutex` (`Core/Monitor.h`) for synchronization.
### Template Parameters
```cpp
template <typename T_enc, typename T_dec>
class AlgorithmTask : public Thread;
```
Must match the `Algorithm<T_enc, T_dec>` it manages.
### Configuration
| Method | Description |
|--------|-------------|
| `void SetAlgorithm(AlgorithmType* alg)` | Set the algorithm to execute. |
| `void SetMode(Mode mode)` | `Cyclic` or `Async`. |
| `void SetCycleTime(int ms)` | Period for cyclic mode (milliseconds). |
### Execution Modes
#### Cyclic Mode
The algorithm's `Process()` is called periodically. The cycle waits on a `condition_variable_any` with timeout, so `Stop()` can interrupt immediately without waiting for the full cycle.
```cpp
AlgorithmTask<int, int> task;
task.SetAlgorithm(&myAlgorithm);
task.SetMode(AlgorithmTask<int, int>::Cyclic);
task.SetCycleTime(100); // every 100ms
task.Run(inputData);
// ... later ...
task.Stop();
```
#### Asynchronous Mode
The task thread blocks on a condition variable until `Notify()` is called. Each notification triggers exactly one `Process()` invocation.
```cpp
task.SetMode(AlgorithmTask<int, int>::Async);
task.Run(inputData);
// Trigger manually:
task.Notify();
// Or connect to a signal:
task.ConnectTrigger(sender, &SenderClass::DataReady);
// Now each emission of DataReady() triggers one Process() call.
```
### Lifecycle
| Method | Description |
|--------|-------------|
| `void Run(const T_enc& input)` | Starts the background thread with the given input. |
| `void Stop()` | Requests stop and joins the thread. |
| `bool IsRunning()` | Inherited from `Thread`. |
### Signals
| Signal | Emitted when |
|--------|-------------|
| `Stopped()` | The task thread has completed (after last `Process()` and before thread exit). |
### Signal-Slot Triggering
`ConnectTrigger()` connects any uLib `Object` signal to the task's `Notify()` method:
```cpp
task.ConnectTrigger(detector, &Detector::EventReady);
```
This uses the uLib signal system (`Core/Signal.h`), not Qt signals. The connection is type-safe and works with the `Object::connect` infrastructure.
## VoxImageFilter (`Math/VoxImageFilter.h`)
`VoxImageFilter` specializes `Algorithm` for kernel-based volumetric image filtering. It uses CRTP (Curiously Recurring Template Pattern) so that concrete filters provide their `Evaluate()` method without virtual dispatch overhead in the inner loop.
### Template Parameters
```cpp
template <typename VoxelT, typename CrtpImplT>
class VoxImageFilter : public Abstract::VoxImageFilter,
public Algorithm<VoxImage<VoxelT>*, VoxImage<VoxelT>*>;
```
- **`VoxelT`**: the voxel data type (must satisfy `Interface::Voxel` — requires `.Value` and `.Count` fields).
- **`CrtpImplT`**: the concrete filter subclass. Must implement:
```cpp
float Evaluate(const VoxImage<VoxelT>& buffer, int index);
```
### How It Works
1. `Process(image)` creates a read-only buffer copy of the input image.
2. For each voxel in parallel (OpenMP), it calls `CrtpImplT::Evaluate(buffer, index)`.
3. `Evaluate()` reads from the buffer using the kernel offsets and writes the result.
4. The filtered image is returned (in-place modification).
```
Process(image)
|
+-- buffer = copy of image (read-only snapshot)
|
+-- #pragma omp parallel for
| for each voxel i:
| image[i].Value = CrtpImplT::Evaluate(buffer, i)
|
+-- return image
```
### Kernel System
The `Kernel<VoxelT>` class stores convolution weights and precomputed index offsets:
| Method | Description |
|--------|-------------|
| `SetKernelNumericXZY(values)` | Set kernel weights from a flat vector (XZY order). |
| `SetKernelSpherical(shape)` | Set weights via a radial function `f(distance^2)`. |
| `SetKernelWeightFunction(shape)` | Set weights via a 3D position function `f(Vector3f)`. |
### CUDA Support
Concrete filters can override `Process()` with a CUDA implementation:
```cpp
#if defined(USE_CUDA) && defined(__CUDACC__)
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
if (this->GetPreferredDevice() == MemoryDevice::VRAM) {
// Launch CUDA kernel, synchronize, return
} else {
return BaseClass::Process(image); // CPU fallback
}
}
#endif
```
The base class `GetPreferredDevice()` automatically returns `VRAM` when the image or kernel data resides on the GPU, enabling transparent device dispatch.
Filters with CUDA implementations: `VoxFilterAlgorithmLinear`, `VoxFilterAlgorithmAbtrim`, `VoxFilterAlgorithmSPR`.
### Concrete Filters
| Filter | File | Description |
|--------|------|-------------|
| `VoxFilterAlgorithmLinear` | `VoxImageFilterLinear.hpp` | Weighted linear convolution (FIR filter). CUDA-enabled. |
| `VoxFilterAlgorithmMedian` | `VoxImageFilterMedian.hpp` | Median filter with kernel-weighted sorting. |
| `VoxFilterAlgorithmAbtrim` | `VoxImageFilterABTrim.hpp` | Alpha-beta trimmed mean filter. CUDA-enabled. |
| `VoxFilterAlgorithmSPR` | `VoxImageFilterABTrim.hpp` | Robespierre filter: trimmed mean applied only to outlier voxels. CUDA-enabled. |
| `VoxFilterAlgorithmThreshold` | `VoxImageFilterThreshold.hpp` | Binary threshold filter. |
| `VoxFilterAlgorithmBilateral` | `VoxImageFilterBilateral.hpp` | Edge-preserving bilateral filter (intensity-weighted Gaussian). |
| `VoxFilterAlgorithmBilateralTrim` | `VoxImageFilterBilateral.hpp` | Bilateral filter with alpha-beta trimming. |
| `VoxFilterAlgorithm2ndStat` | `VoxImageFilter2ndStat.hpp` | Local variance (second-order statistic). |
| `VoxFilterAlgorithmCustom` | `VoxImageFilterCustom.hpp` | User-supplied evaluation function via function pointer. |
### Example: Using a Filter with AlgorithmTask
```cpp
// Create filter and configure kernel
VoxFilterAlgorithmLinear<Voxel> filter(Vector3i(3, 3, 3));
std::vector<float> weights(27, 1.0f); // uniform 3x3x3
filter.SetKernelNumericXZY(weights);
// Direct use
filter.SetImage(&image);
filter.Run();
// Or via Algorithm interface
VoxImage<Voxel>* result = filter.Process(&image);
// Or scheduled in a task
AlgorithmTask<VoxImage<Voxel>*, VoxImage<Voxel>*> task;
task.SetAlgorithm(&filter);
task.SetMode(AlgorithmTask<VoxImage<Voxel>*, VoxImage<Voxel>*>::Cyclic);
task.SetCycleTime(500);
task.Run(&image);
```
## Structural Benefits
### 1. Uniform Processing Interface
Every algorithm — from a simple threshold to a GPU-accelerated convolution — exposes the same `Process(input) -> output` interface. Client code does not need to know the concrete type:
```cpp
Algorithm<VoxImage<Voxel>*, VoxImage<Voxel>*>* alg = &anyFilter;
alg->Process(&image);
```
### 2. Pipeline Composition
The encoder/decoder chaining allows building data processing pipelines where each stage transforms data and passes it to the next. Type safety is enforced at compile time through template parameters.
### 3. Scheduled and Event-Driven Execution
`AlgorithmTask` decouples the algorithm from its execution schedule. The same algorithm can be:
- Called directly (`Process()`)
- Run periodically (Cyclic mode for monitoring/acquisition)
- Triggered by events (Async mode for reactive processing)
### 4. Transparent CPU/GPU Dispatch
The `MemoryDevice` preference and `GetPreferredDevice()` virtual allow the same algorithm interface to dispatch to CPU or GPU implementations. The `DataAllocator` transparently manages RAM/VRAM transfers, and concrete filters override `Process()` with CUDA kernels when data is on the GPU.
### 5. Integration with the Object System
Since `Algorithm` inherits from `Object`, algorithms gain:
- **Properties**: serializable parameters via the `Property<T>` system, enabling persistent configuration and GUI widget generation.
- **Signals**: `Started`/`Finished` notifications for connecting to monitoring or logging.
- **Serialization**: save/load algorithm configuration via Boost archives.
- **Instance naming**: `SetInstanceName()` for runtime identification in contexts.
### 6. CRTP Performance for Inner Loops
`VoxImageFilter` uses CRTP to dispatch to `Evaluate()` without virtual function overhead. The per-voxel evaluation runs at full speed inside OpenMP parallel loops, while the outer `Process()` method remains virtual for polymorphic use through the Algorithm interface.
## Dependencies
```
Core/Object.h — base class, properties, signals, serialization
Core/Signal.h — signal-slot connection infrastructure
Core/Monitor.h — Mutex, condition variables, ULIB_MUTEX_LOCK
Core/Threads.h — Thread base class for AlgorithmTask
Core/DataAllocator.h — MemoryDevice enum, RAM/VRAM data management
Math/VoxImage.h — volumetric image container
Math/VoxImageFilter.h — kernel-based filter framework
```

263
src/Core/Algorithm.h Normal file
View File

@@ -0,0 +1,263 @@
/*//////////////////////////////////////////////////////////////////////////////
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
All rights reserved
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
------------------------------------------------------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library.
//////////////////////////////////////////////////////////////////////////////*/
#ifndef U_CORE_ALGORITHM_H
#define U_CORE_ALGORITHM_H
#include <atomic>
#include <chrono>
#include <condition_variable>
#include "Core/Object.h"
#include "Core/Monitor.h"
#include "Core/Threads.h"
#include "Core/DataAllocator.h"
namespace uLib {
////////////////////////////////////////////////////////////////////////////////
//// ALGORITHM /////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* @brief Algorithm is a template class for containing a functional that can be
* dynamically loaded as a plug-in. It derives from Object and supports
* properties for serialization and interactive parameter widgets.
*
* Algorithms are responsible for their own GPU synchronization: if Process()
* launches CUDA kernels, it must call cudaDeviceSynchronize() before returning
* so that the result is available to the caller or downstream algorithm.
*
* @tparam T_enc Encoder type: the input data type, or a chained algorithm
* whose output is compatible with this algorithm's input.
* @tparam T_dec Decoder type: the output data type, or a chained algorithm
* whose input is compatible with this algorithm's output.
*/
template <typename T_enc, typename T_dec>
class Algorithm : public Object {
public:
using EncoderType = T_enc;
using DecoderType = T_dec;
Algorithm()
: Object()
, m_Encoder(nullptr)
, m_Decoder(nullptr)
, m_PreferredDevice(MemoryDevice::RAM)
{}
virtual ~Algorithm() = default;
virtual const char* GetClassName() const override { return "Algorithm"; }
// Processing ///////////////////////////////////////////////////////////////
/**
* @brief Process input data and produce output.
* Override this in subclasses to implement the algorithm logic.
* GPU-based implementations must synchronize before returning.
*/
virtual T_dec Process(const T_enc& input) = 0;
/** @brief Operator form of Process for functional chaining. */
T_dec operator()(const T_enc& input) { return Process(input); }
// Chaining /////////////////////////////////////////////////////////////////
void SetEncoder(Algorithm* enc) { m_Encoder = enc; }
Algorithm* GetEncoder() const { return m_Encoder; }
void SetDecoder(Algorithm* dec) { m_Decoder = dec; }
Algorithm* GetDecoder() const { return m_Decoder; }
// Device preference ////////////////////////////////////////////////////////
/**
* @brief Returns the preferred memory device for this algorithm.
* CUDA-capable algorithms should override to return VRAM when their
* data resides on the GPU.
*/
virtual MemoryDevice GetPreferredDevice() const { return m_PreferredDevice; }
void SetPreferredDevice(MemoryDevice dev) { m_PreferredDevice = dev; }
/** @brief Returns true if this algorithm prefers GPU execution. */
bool IsGPU() const { return GetPreferredDevice() == MemoryDevice::VRAM; }
// Signals //////////////////////////////////////////////////////////////////
signals:
virtual void Started() { ULIB_SIGNAL_EMIT(Algorithm::Started); }
virtual void Finished() { ULIB_SIGNAL_EMIT(Algorithm::Finished); }
protected:
Algorithm* m_Encoder;
Algorithm* m_Decoder;
MemoryDevice m_PreferredDevice;
};
////////////////////////////////////////////////////////////////////////////////
//// ALGORITHM TASK ////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* @brief AlgorithmTask manages the execution of an Algorithm within a
* scheduled context. Uses uLib::Thread for execution and uLib::Mutex for
* synchronization.
*
* Two execution modes:
* - Cyclic: executes Process() periodically with configurable cycle time.
* - Async: waits for Notify() or a connected signal before each execution.
*
* GPU synchronization is the algorithm's responsibility (see Algorithm::Process).
*/
template <typename T_enc, typename T_dec>
class AlgorithmTask : public Thread {
public:
using AlgorithmType = Algorithm<T_enc, T_dec>;
enum Mode { Cyclic, Async };
AlgorithmTask()
: Thread()
, m_Algorithm(nullptr)
, m_Mode(Cyclic)
, m_CycleTime_ms(1000)
, m_StopRequested(false)
, m_Triggered(false)
{}
virtual ~AlgorithmTask() { Stop(); }
virtual const char* GetClassName() const override { return "AlgorithmTask"; }
// Configuration ////////////////////////////////////////////////////////////
void SetAlgorithm(AlgorithmType* alg) { m_Algorithm = alg; }
AlgorithmType* GetAlgorithm() const { return m_Algorithm; }
void SetMode(Mode mode) { m_Mode = mode; }
Mode GetMode() const { return m_Mode; }
void SetCycleTime(int milliseconds) { m_CycleTime_ms = milliseconds; }
int GetCycleTime() const { return m_CycleTime_ms; }
// Lifecycle ////////////////////////////////////////////////////////////////
/**
* @brief Start the task execution in a separate thread (via Thread::Start).
* In Cyclic mode, the algorithm is executed periodically.
* In Async mode, call Notify() or connect a signal to trigger execution.
*/
void Run(const T_enc& input) {
if (IsRunning()) return;
m_StopRequested.store(false);
m_Triggered.store(false);
m_Input = input;
Start();
}
/** @brief Stop the task execution and join the thread. */
void Stop() {
m_StopRequested.store(true);
ULIB_MUTEX_LOCK(m_WaitMutex, -1) {
m_Condition.notify_all();
}
if (IsJoinable()) Join();
}
// Async triggering /////////////////////////////////////////////////////////
/**
* @brief Notify the task to execute one iteration (Async mode).
* Can be called from a signal-slot connection or externally.
*/
void Notify() {
m_Triggered.store(true);
ULIB_MUTEX_LOCK(m_WaitMutex, -1) {
m_Condition.notify_one();
}
}
/**
* @brief Connect an Object signal to trigger async execution.
* Usage: task.ConnectTrigger(sender, &SenderClass::SomeSignal);
*/
template <typename Func1>
Connection ConnectTrigger(typename FunctionPointer<Func1>::Object* sender, Func1 sigf) {
return Object::connect(sender, sigf, [this]() { Notify(); });
}
// Signals //////////////////////////////////////////////////////////////////
signals:
virtual void Stopped() { ULIB_SIGNAL_EMIT(AlgorithmTask::Stopped); }
protected:
/** @brief Thread entry point — dispatches to cyclic or async loop. */
void Run() override {
if (m_Mode == Cyclic)
RunCyclic();
else
RunAsync();
Stopped();
}
private:
void RunCyclic() {
while (!m_StopRequested.load()) {
if (m_Algorithm) m_Algorithm->Process(m_Input);
std::unique_lock<std::timed_mutex> lock(m_WaitMutex.GetNative());
m_Condition.wait_for(lock,
std::chrono::milliseconds(m_CycleTime_ms),
[this]() { return m_StopRequested.load(); });
}
}
void RunAsync() {
while (!m_StopRequested.load()) {
std::unique_lock<std::timed_mutex> lock(m_WaitMutex.GetNative());
m_Condition.wait(lock, [this]() {
return m_StopRequested.load() || m_Triggered.load();
});
if (m_StopRequested.load()) break;
m_Triggered.store(false);
if (m_Algorithm) m_Algorithm->Process(m_Input);
}
}
AlgorithmType* m_Algorithm;
Mode m_Mode;
int m_CycleTime_ms;
T_enc m_Input;
std::atomic<bool> m_StopRequested;
std::atomic<bool> m_Triggered;
Mutex m_WaitMutex;
std::condition_variable_any m_Condition;
};
} // namespace uLib
#endif // U_CORE_ALGORITHM_H

View File

@@ -1,7 +1,8 @@
set(HEADERS
Archives.h
Array.h
set(HEADERS
Algorithm.h
Archives.h
Array.h
Collection.h
DataAllocator.h
Debug.h

View File

@@ -90,10 +90,10 @@ const std::vector<PropertyBase*>& Object::GetProperties() const {
PropertyBase* Object::GetProperty(const std::string& name) const {
for (auto* p : d->m_Properties) {
if (p->GetName() == name) return p;
if (p->GetName() == name || p->GetQualifiedName() == name) return p;
}
for (auto* p : d->m_DynamicProperties) {
if (p->GetName() == name) return p;
if (p->GetName() == name || p->GetQualifiedName() == name) return p;
}
return nullptr;
}

View File

@@ -2,11 +2,16 @@
#define U_CORE_PROPERTY_H
#include <string>
#include <vector>
#include <sstream>
#include <typeinfo>
#include <typeindex> // Added
#include <boost/serialization/nvp.hpp>
#include <boost/lexical_cast.hpp>
#include <vector>
#include <boost/type_traits/is_class.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/serialization/serialization.hpp>
#include "Core/Archives.h"
#include "Core/Signal.h"
#include "Core/Object.h"
@@ -29,6 +34,12 @@ public:
static std::vector<std::string> empty;
return empty;
}
virtual const std::string& GetGroup() const = 0;
virtual void SetGroup(const std::string& group) = 0;
std::string GetQualifiedName() const {
if (GetGroup().empty()) return GetName();
return GetGroup() + "." + GetName();
}
// Signal support
signals:
@@ -51,16 +62,16 @@ template <typename T>
class Property : public PropertyBase {
public:
// PROXY: Use an existing variable as back-end storage
Property(Object* owner, const std::string& name, T* valuePtr, const std::string& units = "")
: m_owner(owner), m_name(name), m_units(units), m_value(valuePtr), m_own(false) {
Property(Object* owner, const std::string& name, T* valuePtr, const std::string& units = "", const std::string& group = "")
: m_owner(owner), m_name(name), m_units(units), m_group(group), m_value(valuePtr), m_own(false) {
if (m_owner) {
m_owner->RegisterProperty(this);
}
}
// MANAGED: Create and own internal storage
Property(Object* owner, const std::string& name, const T& defaultValue = T(), const std::string& units = "")
: m_owner(owner), m_name(name), m_units(units), m_value(new T(defaultValue)), m_own(true) {
Property(Object* owner, const std::string& name, const T& defaultValue = T(), const std::string& units = "", const std::string& group = "")
: m_owner(owner), m_name(name), m_units(units), m_group(group), m_value(new T(defaultValue)), m_own(true) {
if (m_owner) {
m_owner->RegisterProperty(this);
}
@@ -76,6 +87,8 @@ public:
virtual std::type_index GetTypeIndex() const override { return std::type_index(typeid(T)); }
virtual const std::string& GetUnits() const override { return m_units; }
virtual void SetUnits(const std::string& units) override { m_units = units; }
virtual const std::string& GetGroup() const override { return m_group; }
virtual void SetGroup(const std::string& group) override { m_group = group; }
std::string GetValueAsString() const override {
@@ -127,6 +140,7 @@ public:
private:
std::string m_name;
std::string m_units;
std::string m_group;
T* m_value;
bool m_own;
Object* m_owner;
@@ -149,8 +163,8 @@ typedef Property<Bool_t> BoolProperty;
*/
class EnumProperty : public Property<int> {
public:
EnumProperty(Object* owner, const std::string& name, int* valuePtr, const std::vector<std::string>& labels, const std::string& units = "")
: Property<int>(owner, name, valuePtr, units), m_Labels(labels) {}
EnumProperty(Object* owner, const std::string& name, int* valuePtr, const std::vector<std::string>& labels, const std::string& units = "", const std::string& group = "")
: Property<int>(owner, name, valuePtr, units, group), m_Labels(labels) {}
const std::vector<std::string>& GetEnumLabels() const override { return m_Labels; }
const char* GetTypeName() const override { return "Enum"; }
@@ -209,11 +223,20 @@ public:
boost::archive::detail::common_oarchive<property_register_archive>(boost::archive::no_header),
m_Object(obj) {}
std::string GetCurrentGroup() const {
std::string group;
for (const auto& g : m_GroupStack) {
if (!group.empty()) group += ".";
group += g;
}
return group;
}
// Core logic: encounter HRP -> Create Dynamic Property
template<class T>
void save_override(const boost::serialization::hrp<T> &t) {
if (m_Object) {
Property<T>* p = new Property<T>(m_Object, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value(), t.units() ? t.units() : "");
Property<T>* p = new Property<T>(m_Object, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value(), t.units() ? t.units() : "", GetCurrentGroup());
m_Object->RegisterDynamicProperty(p);
}
}
@@ -221,7 +244,7 @@ public:
template<class T>
void save_override(const boost::serialization::hrp_enum<T> &t) {
if (m_Object) {
EnumProperty* p = new EnumProperty(m_Object, t.name(), (int*)&const_cast<boost::serialization::hrp_enum<T>&>(t).value(), t.labels(), t.units() ? t.units() : "");
EnumProperty* p = new EnumProperty(m_Object, t.name(), (int*)&const_cast<boost::serialization::hrp_enum<T>&>(t).value(), t.labels(), t.units() ? t.units() : "", GetCurrentGroup());
m_Object->RegisterDynamicProperty(p);
}
}
@@ -229,11 +252,24 @@ public:
// Handle standard NVPs by recursing (important for base classes)
template<class T>
void save_override(const boost::serialization::nvp<T> &t) {
boost::archive::detail::common_oarchive<property_register_archive>::save_override(t.const_value());
if (t.name()) m_GroupStack.push_back(t.name());
this->save_helper(t.const_value(), typename boost::is_class<T>::type());
if (t.name()) m_GroupStack.pop_back();
}
// Ignore everything else
template<class T> void save_override(const T &t) {}
// Recursion for nested classes, ignore primitives
template<class T>
void save_override(const T &t) {
this->save_helper(t, typename boost::is_class<T>::type());
}
template<class T>
void save_helper(const T &t, boost::mpl::true_) {
boost::serialization::serialize_adl(*this, const_cast<T&>(t), 0);
}
template<class T>
void save_helper(const T &t, boost::mpl::false_) {}
// Required attribute overrides for common_oarchive
void save_override(const boost::archive::object_id_type & t) {}
@@ -244,6 +280,9 @@ public:
void save_override(const boost::archive::class_id_reference_type & t) {}
void save_override(const boost::archive::class_name_type & t) {}
void save_override(const boost::archive::tracking_type & t) {}
private:
std::vector<std::string> m_GroupStack;
};
/**

View File

@@ -0,0 +1,206 @@
#include "Core/Algorithm.h"
#include <iostream>
#include <atomic>
#include <cassert>
using namespace uLib;
////////////////////////////////////////////////////////////////////////////////
// Test algorithms
class DoubleAlgorithm : public Algorithm<int, int> {
public:
const char* GetClassName() const override { return "DoubleAlgorithm"; }
int Process(const int& input) override {
m_CallCount++;
return input * 2;
}
std::atomic<int> m_CallCount{0};
};
class StringifyAlgorithm : public Algorithm<int, std::string> {
public:
const char* GetClassName() const override { return "StringifyAlgorithm"; }
std::string Process(const int& input) override {
return std::to_string(input);
}
};
// Signal source to test ConnectTrigger
class TriggerSource : public Object {
public:
const char* GetClassName() const override { return "TriggerSource"; }
signals:
virtual void DataReady() { ULIB_SIGNAL_EMIT(TriggerSource::DataReady); }
};
////////////////////////////////////////////////////////////////////////////////
// Tests
void TestBasicProcess() {
std::cout << "Testing basic Algorithm::Process..." << std::endl;
DoubleAlgorithm alg;
assert(alg.Process(5) == 10);
assert(alg.Process(-3) == -6);
assert(alg.Process(0) == 0);
std::cout << " Passed." << std::endl;
}
void TestOperatorCall() {
std::cout << "Testing Algorithm::operator()..." << std::endl;
DoubleAlgorithm alg;
assert(alg(7) == 14);
assert(alg(0) == 0);
std::cout << " Passed." << std::endl;
}
void TestEncoderDecoderChain() {
std::cout << "Testing encoder/decoder chain pointers..." << std::endl;
DoubleAlgorithm a, b;
a.SetDecoder(&b);
b.SetEncoder(&a);
assert(a.GetDecoder() == &b);
assert(b.GetEncoder() == &a);
assert(a.GetEncoder() == nullptr);
assert(b.GetDecoder() == nullptr);
std::cout << " Passed." << std::endl;
}
void TestAlgorithmSignals() {
std::cout << "Testing Algorithm signals..." << std::endl;
DoubleAlgorithm alg;
bool started = false;
bool finished = false;
Object::connect(&alg, &DoubleAlgorithm::Started, [&]() { started = true; });
Object::connect(&alg, &DoubleAlgorithm::Finished, [&]() { finished = true; });
alg.Started();
alg.Finished();
assert(started);
assert(finished);
std::cout << " Passed." << std::endl;
}
void TestCyclicTask() {
std::cout << "Testing AlgorithmTask cyclic mode (Thread-based)..." << std::endl;
DoubleAlgorithm alg;
AlgorithmTask<int, int> task;
task.SetAlgorithm(&alg);
task.SetMode(AlgorithmTask<int, int>::Cyclic);
task.SetCycleTime(50);
assert(!task.IsRunning());
task.Run(5);
// Let it run for ~200ms -> expect ~4 cycles
Thread::Sleep(220);
task.Stop();
assert(!task.IsRunning());
int count = alg.m_CallCount.load();
std::cout << " Cyclic iterations: " << count << std::endl;
assert(count >= 3 && count <= 6);
std::cout << " Passed." << std::endl;
}
void TestAsyncTask() {
std::cout << "Testing AlgorithmTask async mode (Mutex + condition_variable)..." << std::endl;
DoubleAlgorithm alg;
AlgorithmTask<int, int> task;
task.SetAlgorithm(&alg);
task.SetMode(AlgorithmTask<int, int>::Async);
task.Run(42);
Thread::Sleep(50); // let the thread start and wait
// Trigger 3 notifications
for (int i = 0; i < 3; ++i) {
task.Notify();
Thread::Sleep(30);
}
task.Stop();
int count = alg.m_CallCount.load();
std::cout << " Async invocations: " << count << std::endl;
assert(count == 3);
std::cout << " Passed." << std::endl;
}
void TestConnectTrigger() {
std::cout << "Testing AlgorithmTask::ConnectTrigger (signal-slot async)..." << std::endl;
DoubleAlgorithm alg;
AlgorithmTask<int, int> task;
task.SetAlgorithm(&alg);
task.SetMode(AlgorithmTask<int, int>::Async);
TriggerSource source;
task.ConnectTrigger(&source, &TriggerSource::DataReady);
task.Run(10);
Thread::Sleep(50);
// Emit signal 3 times
for (int i = 0; i < 3; ++i) {
source.DataReady();
Thread::Sleep(30);
}
task.Stop();
int count = alg.m_CallCount.load();
std::cout << " Signal-triggered invocations: " << count << std::endl;
assert(count == 3);
std::cout << " Passed." << std::endl;
}
void TestTaskStoppedSignal() {
std::cout << "Testing AlgorithmTask Stopped signal..." << std::endl;
DoubleAlgorithm alg;
AlgorithmTask<int, int> task;
task.SetAlgorithm(&alg);
task.SetMode(AlgorithmTask<int, int>::Cyclic);
task.SetCycleTime(20);
std::atomic<bool> stopped{false};
Object::connect(&task, &AlgorithmTask<int, int>::Stopped,
[&]() { stopped.store(true); });
task.Run(1);
Thread::Sleep(50);
task.Stop();
Thread::Sleep(50);
assert(stopped.load());
std::cout << " Passed." << std::endl;
}
void TestClassName() {
std::cout << "Testing GetClassName..." << std::endl;
DoubleAlgorithm alg;
AlgorithmTask<int, int> task;
assert(std::string(alg.GetClassName()) == "DoubleAlgorithm");
assert(std::string(task.GetClassName()) == "AlgorithmTask");
std::cout << " Passed." << std::endl;
}
void TestDifferentTypes() {
std::cout << "Testing Algorithm with different enc/dec types..." << std::endl;
StringifyAlgorithm alg;
assert(alg.Process(42) == "42");
assert(alg.Process(-1) == "-1");
assert(alg(100) == "100");
std::cout << " Passed." << std::endl;
}
int main() {
TestBasicProcess();
TestOperatorCall();
TestEncoderDecoderChain();
TestAlgorithmSignals();
TestDifferentTypes();
TestCyclicTask();
TestAsyncTask();
TestConnectTrigger();
TestTaskStoppedSignal();
TestClassName();
std::cout << "All Algorithm tests passed!" << std::endl;
return 0;
}

View File

@@ -23,11 +23,13 @@ set( TESTS
VectorMetaAllocatorTest
PropertyTypesTest
HRPTest
PropertyGroupingTest
MutexTest
ThreadsTest
OpenMPTest
TeamTest
AffinityTest
AlgorithmTest
)
set(LIBRARIES

View File

@@ -0,0 +1,78 @@
#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#include "Core/Object.h"
#include "Core/Property.h"
using namespace uLib;
struct Nested {
float x = 1.0f;
float y = 2.0f;
ULIB_SERIALIZE_ACCESS
template<class Archive>
void serialize(Archive & ar, const unsigned int version) {
ar & HRP(x);
ar & HRP(y);
}
};
class GroupObject : public Object {
uLibTypeMacro(GroupObject, Object)
public:
Nested position;
Nested orientation;
float weight = 50.0f;
ULIB_SERIALIZE_ACCESS
template<class Archive>
void serialize(Archive & ar, const unsigned int version) {
ar & boost::serialization::make_nvp("Position", position);
ar & boost::serialization::make_nvp("Orientation", orientation);
ar & HRP(weight);
}
};
int main() {
std::cout << "Testing Property Grouping..." << std::endl;
GroupObject obj;
ULIB_ACTIVATE_PROPERTIES(obj);
auto props = obj.GetProperties();
std::cout << "Registered " << props.size() << " properties." << std::endl;
for (auto* p : props) {
std::cout << "Prop: " << p->GetName()
<< " Group: " << p->GetGroup()
<< " Qualified: " << p->GetQualifiedName() << std::endl;
}
// Check if nested properties are registered
PropertyBase* p1 = obj.GetProperty("Position.x");
PropertyBase* p2 = obj.GetProperty("Position.y");
PropertyBase* p3 = obj.GetProperty("Orientation.x");
PropertyBase* p4 = obj.GetProperty("Orientation.y");
PropertyBase* p5 = obj.GetProperty("weight");
assert(p1 != nullptr && "Position.x not found");
assert(p2 != nullptr && "Position.y not found");
assert(p3 != nullptr && "Orientation.x not found");
assert(p4 != nullptr && "Orientation.y not found");
assert(p5 != nullptr && "weight not found");
assert(p1->GetGroup() == "Position");
assert(p2->GetGroup() == "Position");
assert(p3->GetGroup() == "Orientation");
assert(p4->GetGroup() == "Orientation");
assert(p5->GetGroup() == "");
assert(p1->GetQualifiedName() == "Position.x");
assert(p5->GetQualifiedName() == "weight");
std::cout << "Property Grouping Tests PASSED!" << std::endl;
return 0;
}

View File

@@ -27,12 +27,16 @@
#define VOXIMAGEFILTER_H
#include "Core/StaticInterface.h"
#include "Core/Algorithm.h"
#include "Math/Dense.h"
#include "Math/VoxImage.h"
namespace uLib {
////////////////////////////////////////////////////////////////////////////////
// Kernel shape interface (static check for operator()(float) and operator()(Vector3f))
namespace Interface {
struct VoxImageFilterShape {
template <class Self> void check_structural() {
@@ -42,63 +46,95 @@ struct VoxImageFilterShape {
};
} // namespace Interface
////////////////////////////////////////////////////////////////////////////////
// Forward declaration
template <typename VoxelT> class Kernel;
////////////////////////////////////////////////////////////////////////////////
// Abstract interface (type-erased, used by python bindings)
namespace Abstract {
class VoxImageFilter {
public:
virtual void Run() = 0;
virtual void SetImage(Abstract::VoxImage *image) = 0;
protected:
virtual ~VoxImageFilter() {}
};
} // namespace Abstract
template <typename VoxelT, typename AlgorithmT>
class VoxImageFilter : public Abstract::VoxImageFilter, public Object {
////////////////////////////////////////////////////////////////////////////////
// VoxImageFilter — kernel-based voxel filter using CRTP + Algorithm
//
// Template parameters:
// VoxelT — voxel data type (must satisfy Interface::Voxel)
// CrtpImplT — concrete filter subclass (CRTP), must provide:
// float Evaluate(const VoxImage<VoxelT>& buffer, int index)
//
// Inherits Algorithm<VoxImage<VoxelT>*, VoxImage<VoxelT>*> so that filters
// can be used with AlgorithmTask for scheduled/async execution, and chained
// via encoder/decoder.
template <typename VoxelT, typename CrtpImplT>
class VoxImageFilter : public Abstract::VoxImageFilter,
public Algorithm<VoxImage<VoxelT>*, VoxImage<VoxelT>*> {
public:
virtual const char * GetClassName() const { return "VoxImageFilter"; }
virtual const char* GetClassName() const { return "VoxImageFilter"; }
VoxImageFilter(const Vector3i &size);
// Algorithm interface ////////////////////////////////////////////////////////
/**
* @brief Process implements Algorithm::Process.
* Applies the filter in-place on the input image and returns it.
*/
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override;
/**
* @brief Run implements Abstract::VoxImageFilter::Run.
* Calls Process on the current image.
*/
void Run();
// Device awareness ///////////////////////////////////////////////////////////
/** @brief Returns VRAM if image or kernel data is on GPU, RAM otherwise. */
MemoryDevice GetPreferredDevice() const override {
if (m_Image && m_Image->Data().GetDevice() == MemoryDevice::VRAM)
return MemoryDevice::VRAM;
if (m_KernelData.ConstData().GetDevice() == MemoryDevice::VRAM)
return MemoryDevice::VRAM;
return MemoryDevice::RAM;
}
// Kernel setup ///////////////////////////////////////////////////////////////
void SetKernelNumericXZY(const std::vector<float> &numeric);
void SetKernelSpherical(float (*shape)(float));
template <class ShapeT> void SetKernelSpherical(ShapeT shape);
void SetKernelWeightFunction(float (*shape)(const Vector3f &));
template <class ShapeT> void SetKernelWeightFunction(ShapeT shape);
inline const Kernel<VoxelT> &GetKernelData() const {
return this->m_KernelData;
}
inline Kernel<VoxelT> &GetKernelData() { return this->m_KernelData; }
// Accessors //////////////////////////////////////////////////////////////////
inline VoxImage<VoxelT> *GetImage() const { return this->m_Image; }
const Kernel<VoxelT> &GetKernelData() const { return m_KernelData; }
Kernel<VoxelT> &GetKernelData() { return m_KernelData; }
VoxImage<VoxelT> *GetImage() const { return m_Image; }
void SetImage(Abstract::VoxImage *image);
protected:
float Convolve(const VoxImage<VoxelT> &buffer, int index); // remove //
void SetKernelOffset();
float Distance2(const Vector3i &v);
// protected members for algorithm access //
Kernel<VoxelT> m_KernelData;
VoxImage<VoxelT> *m_Image;
private:
AlgorithmT *t_Algoritm;
CrtpImplT *m_CrtpImpl;
};
} // namespace uLib

View File

@@ -33,7 +33,9 @@
namespace uLib {
// KERNEL //////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//// KERNEL ////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
template <typename T> class Kernel : public StructuredData {
typedef StructuredData BaseClass;
@@ -41,13 +43,12 @@ template <typename T> class Kernel : public StructuredData {
public:
Kernel(const Vector3i &size);
inline T &operator[](const Vector3i &id) { return m_Data[Map(id)]; }
inline T &operator[](const int &id) { return m_Data[id]; }
inline int GetCenterData() const;
T &operator[](const Vector3i &id) { return m_Data[Map(id)]; }
T &operator[](const int &id) { return m_Data[id]; }
int GetCenterData() const;
inline DataAllocator<T> &Data() { return this->m_Data; }
inline const DataAllocator<T> &ConstData() const { return this->m_Data; }
DataAllocator<T> &Data() { return m_Data; }
const DataAllocator<T> &ConstData() const { return m_Data; }
void PrintSelf(std::ostream &o) const;
@@ -60,12 +61,14 @@ Kernel<T>::Kernel(const Vector3i &size) : BaseClass(size), m_Data(size.prod()) {
Interface::IsA<T, Interface::Voxel>();
}
template <typename T> inline int Kernel<T>::GetCenterData() const {
template <typename T>
int Kernel<T>::GetCenterData() const {
static int center = Map(this->GetDims() / 2);
return center;
}
template <typename T> void Kernel<T>::PrintSelf(std::ostream &o) const {
template <typename T>
void Kernel<T>::PrintSelf(std::ostream &o) const {
o << " Filter Kernel Dump [XZ_Y]: \n";
Vector3i index;
o << "\n Value: \n\n"
@@ -96,26 +99,42 @@ template <typename T> void Kernel<T>::PrintSelf(std::ostream &o) const {
}
}
////////////////////////////////////////////////////////////////////////////////
//// VOXIMAGEFILTER IMPLEMENTATION /////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
#define _TPL_ template <typename VoxelT, typename AlgorithmT>
#define _TPLT_ VoxelT, AlgorithmT
template <typename VoxelT, typename CrtpImplT>
VoxImageFilter<VoxelT, CrtpImplT>::VoxImageFilter(const Vector3i &size)
: m_KernelData(size)
, m_Image(nullptr)
, m_CrtpImpl(static_cast<CrtpImplT *>(this))
{}
_TPL_
VoxImageFilter<_TPLT_>::VoxImageFilter(const Vector3i &size)
: m_KernelData(size), t_Algoritm(static_cast<AlgorithmT *>(this)) {}
_TPL_
void VoxImageFilter<_TPLT_>::Run() {
template <typename VoxelT, typename CrtpImplT>
VoxImage<VoxelT>* VoxImageFilter<VoxelT, CrtpImplT>::Process(
VoxImage<VoxelT>* const& image) {
if (m_Image != image) SetImage(image);
VoxImage<VoxelT> buffer = *m_Image;
#pragma omp parallel for
for (int i = 0; i < m_Image->Data().size(); ++i)
m_Image->operator[](i).Value = this->t_Algoritm->Evaluate(buffer, i);
m_Image->operator[](i).Value = m_CrtpImpl->Evaluate(buffer, i);
#pragma omp barrier
return m_Image;
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelOffset() {
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::Run() {
Process(m_Image);
}
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetImage(Abstract::VoxImage *image) {
m_Image = reinterpret_cast<VoxImage<VoxelT> *>(image);
SetKernelOffset();
}
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelOffset() {
Vector3i id(0, 0, 0);
for (int z = 0; z < m_KernelData.GetDims()(2); ++z) {
for (int x = 0; x < m_KernelData.GetDims()(0); ++x) {
@@ -127,10 +146,10 @@ void VoxImageFilter<_TPLT_>::SetKernelOffset() {
}
}
_TPL_
float VoxImageFilter<_TPLT_>::Distance2(const Vector3i &v) {
template <typename VoxelT, typename CrtpImplT>
float VoxImageFilter<VoxelT, CrtpImplT>::Distance2(const Vector3i &v) {
Vector3i tmp = v;
const Vector3i &dim = this->m_KernelData.GetDims();
const Vector3i &dim = m_KernelData.GetDims();
Vector3i center = dim / 2;
tmp = tmp - center;
center = center.cwiseProduct(center);
@@ -140,12 +159,9 @@ float VoxImageFilter<_TPLT_>::Distance2(const Vector3i &v) {
0.25 * (3 - (dim(0) % 2) - (dim(1) % 2) - (dim(2) % 2)));
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelNumericXZY(
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelNumericXZY(
const std::vector<float> &numeric) {
// set data order //
StructuredData::Order order = m_KernelData.GetDataOrder();
// m_KernelData.SetDataOrder(StructuredData::XZY);
Vector3i id;
int index = 0;
for (int y = 0; y < m_KernelData.GetDims()(1); ++y) {
@@ -156,38 +172,39 @@ void VoxImageFilter<_TPLT_>::SetKernelNumericXZY(
}
}
}
// m_KernelData.SetDataOrder(order);
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelSpherical(float (*shape)(float)) {
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelSpherical(
float (*shape)(float)) {
Vector3i id;
for (int y = 0; y < m_KernelData.GetDims()(1); ++y) {
for (int z = 0; z < m_KernelData.GetDims()(2); ++z) {
for (int x = 0; x < m_KernelData.GetDims()(0); ++x) {
id << x, y, z;
m_KernelData[id].Value = shape(this->Distance2(id));
m_KernelData[id].Value = shape(Distance2(id));
}
}
}
}
_TPL_ template <class ShapeT>
void VoxImageFilter<_TPLT_>::SetKernelSpherical(ShapeT shape) {
template <typename VoxelT, typename CrtpImplT>
template <class ShapeT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelSpherical(ShapeT shape) {
Interface::IsA<ShapeT, Interface::VoxImageFilterShape>();
Vector3i id;
for (int y = 0; y < m_KernelData.GetDims()(1); ++y) {
for (int z = 0; z < m_KernelData.GetDims()(2); ++z) {
for (int x = 0; x < m_KernelData.GetDims()(0); ++x) {
id << x, y, z;
m_KernelData[id].Value = shape(this->Distance2(id));
m_KernelData[id].Value = shape(Distance2(id));
}
}
}
}
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelWeightFunction(
float (*shape)(const Vector3f &)) {
const Vector3i &dim = m_KernelData.GetDims();
Vector3i id;
@@ -195,20 +212,19 @@ void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(
for (int y = 0; y < dim(1); ++y) {
for (int z = 0; z < dim(2); ++z) {
for (int x = 0; x < dim(0); ++x) {
// get voxels centroid coords from kernel center //
id << x, y, z;
pt << id(0) - dim(0) / 2 + 0.5 * !(dim(0) % 2),
id(1) - dim(1) / 2 + 0.5 * !(dim(1) % 2),
id(2) - dim(2) / 2 + 0.5 * !(dim(2) % 2);
// compute function using given shape //
m_KernelData[id].Value = shape(pt);
}
}
}
}
_TPL_ template <class ShapeT>
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(ShapeT shape) {
template <typename VoxelT, typename CrtpImplT>
template <class ShapeT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelWeightFunction(ShapeT shape) {
Interface::IsA<ShapeT, Interface::VoxImageFilterShape>();
const Vector3i &dim = m_KernelData.GetDims();
Vector3i id;
@@ -216,45 +232,16 @@ void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(ShapeT shape) {
for (int y = 0; y < dim(1); ++y) {
for (int z = 0; z < dim(2); ++z) {
for (int x = 0; x < dim(0); ++x) {
// get voxels centroid coords from kernel center //
id << x, y, z;
pt << id(0) - dim(0) / 2 + 0.5 * !(dim(0) % 2),
id(1) - dim(1) / 2 + 0.5 * !(dim(1) % 2),
id(2) - dim(2) / 2 + 0.5 * !(dim(2) % 2);
// compute function using given shape //
m_KernelData[id].Value = shape(pt);
}
}
}
}
_TPL_
void VoxImageFilter<_TPLT_>::SetImage(Abstract::VoxImage *image) {
this->m_Image = reinterpret_cast<VoxImage<VoxelT> *>(image);
this->SetKernelOffset();
}
_TPL_
float VoxImageFilter<_TPLT_>::Convolve(const VoxImage<VoxelT> &buffer,
int index) {
const DataAllocator<VoxelT> &vbuf = buffer.ConstData();
const DataAllocator<VoxelT> &vker = m_KernelData.ConstData();
int vox_size = vbuf.size();
int ker_size = vker.size();
int pos;
float conv = 0, ksum = 0;
for (int ik = 0; ik < ker_size; ++ik) {
pos = index + vker[ik].Count - vker[m_KernelData.GetCenterData()].Count;
pos = (pos + vox_size) % vox_size;
conv += vbuf[pos].Value * vker[ik].Value;
ksum += vker[ik].Value;
}
return conv / ksum;
}
#undef _TPLT_
#undef _TPL_
} // namespace uLib
#endif // VOXIMAGEFILTER_HPP

View File

@@ -109,7 +109,8 @@ public:
}
#if defined(USE_CUDA) && defined(__CUDACC__)
void Run() {
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
if (this->m_Image != image) this->SetImage(image);
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
@@ -136,8 +137,9 @@ public:
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count,
mAtrim, mBtrim);
cudaDeviceSynchronize();
return this->m_Image;
} else {
BaseClass::Run();
return BaseClass::Process(image);
}
}
#endif
@@ -207,7 +209,8 @@ public:
}
#if defined(USE_CUDA) && defined(__CUDACC__)
void Run() {
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
if (this->m_Image != image) this->SetImage(image);
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
@@ -234,8 +237,9 @@ public:
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count,
mAtrim, mBtrim);
cudaDeviceSynchronize();
return this->m_Image;
} else {
BaseClass::Run();
return BaseClass::Process(image);
}
}
#endif

View File

@@ -30,8 +30,6 @@
#include "VoxImageFilter.h"
#include <Math/Dense.h>
#define likely(expr) __builtin_expect(!!(expr), 1)
////////////////////////////////////////////////////////////////////////////////
///// VOXIMAGE FILTER CUSTOM /////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
@@ -50,7 +48,7 @@ public:
: BaseClass(size), m_CustomEvaluate(NULL) {}
float Evaluate(const VoxImage<VoxelT> &buffer, int index) {
if (likely(m_CustomEvaluate)) {
if (m_CustomEvaluate) {
const DataAllocator<VoxelT> &vbuf = buffer.ConstData();
const DataAllocator<VoxelT> &vker = this->m_KernelData.ConstData();
int vox_size = vbuf.size();

View File

@@ -67,7 +67,8 @@ public:
VoxFilterAlgorithmLinear(const Vector3i &size) : BaseClass(size) {}
#if defined(USE_CUDA) && defined(__CUDACC__)
void Run() {
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
if (this->m_Image != image) this->SetImage(image);
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
@@ -92,8 +93,9 @@ public:
LinearFilterKernel<<<blocksPerGrid, threadsPerBlock>>>(
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count);
cudaDeviceSynchronize();
return this->m_Image;
} else {
BaseClass::Run();
return BaseClass::Process(image);
}
}
#endif

View File

@@ -23,8 +23,6 @@
//////////////////////////////////////////////////////////////////////////////*/
#ifndef VOXIMAGEFILTERTHRESHOLD_HPP
#define VOXIMAGEFILTERTHRESHOLD_HPP
@@ -39,40 +37,24 @@
namespace uLib {
template <typename VoxelT>
class VoxFilterAlgorithmThreshold :
public VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT> > {
class VoxFilterAlgorithmThreshold
: public VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT>> {
typedef VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT> > BaseClass;
// ULIB_OBJECT_PARAMETERS(BaseClass) {
// float threshold;
// };
typedef VoxImageFilter<VoxelT, VoxFilterAlgorithmThreshold<VoxelT>> BaseClass;
float m_threshold;
float m_threshold;
public:
VoxFilterAlgorithmThreshold(const Vector3i &size) : BaseClass(size)
{
// init_parameters();
m_threshold = 0;
}
VoxFilterAlgorithmThreshold(const Vector3i &size)
: BaseClass(size), m_threshold(0) {}
inline void SetThreshold(float th) { m_threshold = th; }
float Evaluate(const VoxImage<VoxelT> &buffer, int index)
{
return static_cast<float>(buffer.ConstData().at(index).Value >=
// parameters().threshold);
m_threshold );
}
void SetThreshold(float th) { m_threshold = th; }
float Evaluate(const VoxImage<VoxelT> &buffer, int index) {
return static_cast<float>(buffer.ConstData().at(index).Value >= m_threshold);
}
};
//template <typename VoxelT>
//inline void VoxFilterAlgorithmThreshold<VoxelT>::init_parameters()
//{
// parameters().threshold = 0;
//}
}
} // namespace uLib
#endif // VOXIMAGEFILTERTHRESHOLD_HPP

View File

@@ -0,0 +1,408 @@
/*//////////////////////////////////////////////////////////////////////////////
// CMT Cosmic Muon Tomography project //////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
Copyright (c) 2014, Universita' degli Studi di Padova, INFN sez. di Padova
All rights reserved
Authors: Andrea Rigoni Garola < andrea.rigoni@pd.infn.it >
------------------------------------------------------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library.
//////////////////////////////////////////////////////////////////////////////*/
#include "testing-prototype.h"
#include "Core/Algorithm.h"
#include "Math/VoxImage.h"
#include "Math/VoxImageFilter.h"
#include <iostream>
#include <thread>
#include <chrono>
using namespace uLib;
struct TestVoxel {
Scalarf Value;
unsigned int Count;
};
int main() {
BEGIN_TESTING(AlgorithmCudaChain);
////////////////////////////////////////////////////////////////////////////
// TEST 1: Single filter — GetPreferredDevice reflects data location
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 1: GetPreferredDevice reflects data location ---\n";
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
image[Vector3i(5, 5, 5)].Value = 1;
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
std::vector<float> weights(27, 1.0f);
filter.SetImage(&image);
filter.SetKernelNumericXZY(weights);
// Before VRAM move: should prefer RAM
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
TEST1(!filter.IsGPU());
std::cout << " RAM mode: PreferredDevice=RAM, IsGPU=false OK\n";
// Move image data to VRAM
image.Data().MoveToVRAM();
// After VRAM move: should prefer VRAM
TEST1(filter.GetPreferredDevice() == MemoryDevice::VRAM);
TEST1(filter.IsGPU());
std::cout << " VRAM mode: PreferredDevice=VRAM, IsGPU=true OK\n";
// Move back to RAM
image.Data().MoveToRAM();
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
std::cout << " Back to RAM: PreferredDevice=RAM OK\n";
}
////////////////////////////////////////////////////////////////////////////
// TEST 2: Kernel data on VRAM also triggers GPU preference
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 2: Kernel on VRAM triggers GPU preference ---\n";
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
std::vector<float> weights(27, 1.0f);
filter.SetImage(&image);
filter.SetKernelNumericXZY(weights);
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
// Only kernel on VRAM
filter.GetKernelData().Data().MoveToVRAM();
TEST1(filter.GetPreferredDevice() == MemoryDevice::VRAM);
std::cout << " Kernel on VRAM: PreferredDevice=VRAM OK\n";
filter.GetKernelData().Data().MoveToRAM();
TEST1(filter.GetPreferredDevice() == MemoryDevice::RAM);
}
////////////////////////////////////////////////////////////////////////////
// TEST 3: Algorithm interface — Process through base pointer
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 3: Process through Algorithm base pointer ---\n";
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
image[Vector3i(5, 5, 5)].Value = 10;
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
std::vector<float> weights(27, 1.0f);
filter.SetImage(&image);
filter.SetKernelNumericXZY(weights);
// Use through Algorithm base class pointer
Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>* alg = &filter;
VoxImage<TestVoxel>* result = alg->Process(&image);
TEST1(result == &image);
std::cout << " Process through base pointer returned correct image OK\n";
// Verify filter actually ran (center voxel should be averaged)
// With uniform 3x3x3 kernel and single non-zero voxel at center,
// the center value should be 10/27 ≈ 0.37
TEST1(image[Vector3i(5, 5, 5)].Value < 10.0f);
std::cout << " Filter modified voxel values OK\n";
}
////////////////////////////////////////////////////////////////////////////
// TEST 4: Encoder/decoder chain — two filters linked
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 4: Encoder/decoder chain ---\n";
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
image[Vector3i(5, 5, 5)].Value = 100;
// First filter: linear smoothing
VoxFilterAlgorithmLinear<TestVoxel> filter1(Vector3i(3, 3, 3));
std::vector<float> weights1(27, 1.0f);
filter1.SetImage(&image);
filter1.SetKernelNumericXZY(weights1);
// Second filter: threshold
VoxFilterAlgorithmThreshold<TestVoxel> filter2(Vector3i(1, 1, 1));
filter2.SetThreshold(0.5f);
filter2.SetImage(&image);
// 1x1x1 kernel with value 1
std::vector<float> weights2(1, 1.0f);
filter2.SetKernelNumericXZY(weights2);
// Chain: filter1 → filter2
filter1.SetDecoder(&filter2);
filter2.SetEncoder(&filter1);
TEST1(filter1.GetDecoder() == &filter2);
TEST1(filter2.GetEncoder() == &filter1);
std::cout << " Chain linked: filter1 -> filter2 OK\n";
// Execute chain manually (encoder first, then decoder)
filter1.Process(&image);
float smoothed_center = image[Vector3i(5, 5, 5)].Value;
std::cout << " After linear: center = " << smoothed_center << "\n";
filter2.Process(&image);
float thresholded_center = image[Vector3i(5, 5, 5)].Value;
std::cout << " After threshold: center = " << thresholded_center << "\n";
// After threshold, values should be 0 or 1
TEST1(thresholded_center == 0.0f || thresholded_center == 1.0f);
std::cout << " Chain execution produced valid results OK\n";
}
////////////////////////////////////////////////////////////////////////////
// TEST 5: CUDA chain — VRAM data through chained filters
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 5: VRAM data through chained filters ---\n";
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
image[Vector3i(5, 5, 5)].Value = 50;
VoxFilterAlgorithmLinear<TestVoxel> filter1(Vector3i(3, 3, 3));
std::vector<float> weights1(27, 1.0f);
filter1.SetImage(&image);
filter1.SetKernelNumericXZY(weights1);
VoxFilterAlgorithmAbtrim<TestVoxel> filter2(Vector3i(3, 3, 3));
std::vector<float> weights2(27, 1.0f);
filter2.SetImage(&image);
filter2.SetKernelNumericXZY(weights2);
filter2.SetABTrim(1, 1);
// Chain
filter1.SetDecoder(&filter2);
filter2.SetEncoder(&filter1);
// Move data to VRAM
image.Data().MoveToVRAM();
filter1.GetKernelData().Data().MoveToVRAM();
filter2.GetKernelData().Data().MoveToVRAM();
// Both filters should report VRAM preference
TEST1(filter1.GetPreferredDevice() == MemoryDevice::VRAM);
TEST1(filter2.GetPreferredDevice() == MemoryDevice::VRAM);
TEST1(filter1.IsGPU());
TEST1(filter2.IsGPU());
std::cout << " Both filters detect VRAM preference OK\n";
// Verify the chain's device consistency
auto* encoder = filter2.GetEncoder();
TEST1(encoder != nullptr);
TEST1(encoder->IsGPU());
std::cout << " Encoder in chain also reports GPU OK\n";
#ifdef USE_CUDA
// With CUDA: filters execute on GPU via Process()
image.Data().MoveToRAM(); // reset for clean test
image[Vector3i(5, 5, 5)].Value = 50;
image.Data().MoveToVRAM();
filter1.Process(&image);
TEST1(image.Data().GetDevice() == MemoryDevice::VRAM);
std::cout << " CUDA: data stays in VRAM after filter1 OK\n";
filter2.Process(&image);
TEST1(image.Data().GetDevice() == MemoryDevice::VRAM);
std::cout << " CUDA: data stays in VRAM after filter2 OK\n";
#else
// Without CUDA: verify Process still works via CPU fallback
image.Data().MoveToRAM();
image[Vector3i(5, 5, 5)].Value = 50;
filter1.GetKernelData().Data().MoveToRAM();
filter2.GetKernelData().Data().MoveToRAM();
filter1.Process(&image);
filter2.Process(&image);
std::cout << " No CUDA: CPU fallback executed correctly OK\n";
#endif
}
////////////////////////////////////////////////////////////////////////////
// TEST 6: AlgorithmTask with VRAM-aware filter
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 6: AlgorithmTask with VRAM-aware filter ---\n";
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
image[Vector3i(4, 4, 4)].Value = 20;
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
std::vector<float> weights(27, 1.0f);
filter.SetImage(&image);
filter.SetKernelNumericXZY(weights);
// Set up task
AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*> task;
task.SetAlgorithm(&filter);
task.SetMode(AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>::Cyclic);
task.SetCycleTime(50);
// Run task for a few cycles
task.Run(&image);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
task.Stop();
// After cyclic execution, the filter should have smoothed values
TEST1(image[Vector3i(4, 4, 4)].Value < 20.0f);
std::cout << " Task cyclic execution modified image OK\n";
std::cout << " Center value after smoothing: "
<< image[Vector3i(4, 4, 4)].Value << "\n";
}
////////////////////////////////////////////////////////////////////////////
// TEST 7: AlgorithmTask async with chained filters
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 7: AlgorithmTask async with filter ---\n";
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
image[Vector3i(4, 4, 4)].Value = 30;
VoxFilterAlgorithmLinear<TestVoxel> filter(Vector3i(3, 3, 3));
std::vector<float> weights(27, 1.0f);
filter.SetImage(&image);
filter.SetKernelNumericXZY(weights);
AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*> task;
task.SetAlgorithm(&filter);
task.SetMode(AlgorithmTask<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>::Async);
float before = image[Vector3i(4, 4, 4)].Value;
task.Run(&image);
// Trigger one execution
task.Notify();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
task.Stop();
float after = image[Vector3i(4, 4, 4)].Value;
TEST1(after < before);
std::cout << " Async trigger: value " << before << " -> " << after << " OK\n";
}
////////////////////////////////////////////////////////////////////////////
// TEST 8: Device preference propagation in chain
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 8: Device preference propagation check ---\n";
VoxImage<TestVoxel> image(Vector3i(8, 8, 8));
image[Vector3i(4, 4, 4)].Value = 10;
VoxFilterAlgorithmLinear<TestVoxel> filterA(Vector3i(3, 3, 3));
VoxFilterAlgorithmAbtrim<TestVoxel> filterB(Vector3i(3, 3, 3));
VoxFilterAlgorithmThreshold<TestVoxel> filterC(Vector3i(1, 1, 1));
std::vector<float> w27(27, 1.0f);
std::vector<float> w1(1, 1.0f);
filterA.SetImage(&image);
filterA.SetKernelNumericXZY(w27);
filterB.SetImage(&image);
filterB.SetKernelNumericXZY(w27);
filterB.SetABTrim(1, 1);
filterC.SetImage(&image);
filterC.SetKernelNumericXZY(w1);
filterC.SetThreshold(0.1f);
// Chain: A → B → C
filterA.SetDecoder(&filterB);
filterB.SetEncoder(&filterA);
filterB.SetDecoder(&filterC);
filterC.SetEncoder(&filterB);
// All on RAM
TEST1(!filterA.IsGPU());
TEST1(!filterB.IsGPU());
TEST1(!filterC.IsGPU());
std::cout << " All filters on RAM OK\n";
// Move image to VRAM — filters A and B should detect it
image.Data().MoveToVRAM();
TEST1(filterA.IsGPU());
TEST1(filterB.IsGPU());
// filterC with 1x1x1 kernel doesn't have CUDA override, but still detects VRAM
TEST1(filterC.IsGPU());
std::cout << " Image on VRAM: all filters report GPU OK\n";
// Can walk the chain and check device consistency
auto* step = static_cast<Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>*>(&filterA);
bool all_gpu = true;
while (step) {
if (!step->IsGPU()) all_gpu = false;
step = static_cast<Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>*>(step->GetDecoder());
}
TEST1(all_gpu);
std::cout << " Chain walk: all steps report GPU OK\n";
image.Data().MoveToRAM();
}
////////////////////////////////////////////////////////////////////////////
// TEST 9: Process through chain with Algorithm interface
////////////////////////////////////////////////////////////////////////////
{
std::cout << "\n--- Test 9: Sequential chain processing via Algorithm interface ---\n";
VoxImage<TestVoxel> image(Vector3i(10, 10, 10));
// Set a pattern: single bright voxel
image[Vector3i(5, 5, 5)].Value = 100;
VoxFilterAlgorithmLinear<TestVoxel> filterA(Vector3i(3, 3, 3));
std::vector<float> w(27, 1.0f);
filterA.SetImage(&image);
filterA.SetKernelNumericXZY(w);
VoxFilterAlgorithmLinear<TestVoxel> filterB(Vector3i(3, 3, 3));
filterB.SetImage(&image);
filterB.SetKernelNumericXZY(w);
// Chain
filterA.SetDecoder(&filterB);
filterB.SetEncoder(&filterA);
// Process chain through base pointer
using AlgType = Algorithm<VoxImage<TestVoxel>*, VoxImage<TestVoxel>*>;
AlgType* chain = &filterA;
// Walk and process
AlgType* current = chain;
while (current) {
current->Process(&image);
current = static_cast<AlgType*>(current->GetDecoder());
}
// After two rounds of smoothing, the peak should be smaller than original
float final_val = image[Vector3i(5, 5, 5)].Value;
TEST1(final_val < 100.0f);
std::cout << " Two-stage smoothing: peak = " << final_val << " OK\n";
}
END_TESTING;
}

View File

@@ -16,6 +16,7 @@ set(TESTS
QuadMeshTest
BitCodeTest
UnitsTest
AlgorithmCudaChainTest
)
set(LIBRARIES
@@ -28,6 +29,6 @@ set(LIBRARIES
uLib_add_tests(Math)
if(USE_CUDA)
set_source_files_properties(VoxImageTest.cpp VoxImageCopyTest.cpp VoxImageFilterTest.cpp VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp PROPERTIES LANGUAGE CUDA)
set_source_files_properties(VoxImageTest.cpp VoxImageCopyTest.cpp VoxImageFilterTest.cpp VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp AlgorithmCudaChainTest.cpp PROPERTIES LANGUAGE CUDA)
set_source_files_properties(VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp PROPERTIES CXX_STANDARD 17 CUDA_STANDARD 17)
endif()

View File

@@ -61,6 +61,7 @@
#include "uLibVtkInterface.h"
#include "vtkHandlerWidget.h"
#include "Math/Dense.h"
#include "Core/Property.h"
@@ -96,9 +97,9 @@ public:
m_Dragable(true)
{
m_Color[0] = m_Color[1] = m_Color[2] = -1.0;
m_Position[0] = m_Position[1] = m_Position[2] = 0.0;
m_Orientation[0] = m_Orientation[1] = m_Orientation[2] = 0.0;
m_Scale[0] = m_Scale[1] = m_Scale[2] = 1.0;
m_Position = Vector3d::Zero();
m_Orientation = Vector3d::Zero();
m_Scale = Vector3d::Ones();
}
~PuppetData() {
@@ -124,9 +125,9 @@ public:
bool m_Selected;
bool m_Visibility;
bool m_Dragable;
double m_Position[3];
double m_Orientation[3];
double m_Scale[3];
Vector3d m_Position;
Vector3d m_Orientation;
Vector3d m_Scale;
void ApplyAppearance(vtkProp *p) {
p->SetVisibility(m_Visibility);
@@ -156,9 +157,9 @@ public:
// Handle transformation if it's a Prop3D
if (auto* p3d = vtkProp3D::SafeDownCast(p)) {
// NOTE: Usually managed by Puppet::Update from model, but here for direct prop manipulation
// p3d->SetPosition(m_Position);
// p3d->SetOrientation(m_Orientation);
// p3d->SetScale(m_Scale);
// p3d->SetPosition(m_Position.data());
// p3d->SetOrientation(m_Orientation.data());
// p3d->SetScale(m_Scale.data());
}
}
@@ -499,9 +500,9 @@ void Puppet::Update()
// Apply transformation if it's a Prop3D
if (auto* p3d = vtkProp3D::SafeDownCast(root)) {
p3d->SetPosition(pd->m_Position);
p3d->SetOrientation(pd->m_Orientation);
p3d->SetScale(pd->m_Scale);
p3d->SetPosition(pd->m_Position.data());
p3d->SetOrientation(pd->m_Orientation.data());
p3d->SetScale(pd->m_Scale.data());
}
}
@@ -549,9 +550,9 @@ void Puppet::SyncFromVtk()
// Update properties
for (int i=0; i<3; ++i) {
pd->m_Position[i] = pos[i];
pd->m_Orientation[i] = ori[i];
pd->m_Scale[i] = scale[i];
pd->m_Position(i) = pos[i];
pd->m_Orientation(i) = ori[i];
pd->m_Scale(i) = scale[i];
}
// Get the properties from the object
@@ -567,26 +568,37 @@ void Puppet::ConnectInteractor(vtkRenderWindowInteractor *interactor)
{
}
struct TransformProxy {
PuppetData* pd;
template<class Archive>
void serialize(Archive & ar, const unsigned int version) {
ar & boost::serialization::make_hrp("Position", pd->m_Position, "mm");
ar & boost::serialization::make_hrp("Orientation", pd->m_Orientation, "deg");
ar & boost::serialization::make_hrp("Scale", pd->m_Scale, "");
}
};
struct AppearanceProxy {
PuppetData* pd;
template<class Archive>
void serialize(Archive & ar, const unsigned int version) {
ar & boost::serialization::make_hrp("ColorR", pd->m_Color[0]);
ar & boost::serialization::make_hrp("ColorG", pd->m_Color[1]);
ar & boost::serialization::make_hrp("ColorB", pd->m_Color[2]);
ar & boost::serialization::make_hrp("Opacity", pd->m_Opacity);
ar & boost::serialization::make_hrp_enum("Representation", pd->m_Representation, {"Points", "Wireframe", "Surface", "SurfaceWithEdges", "Volume", "Outline", "Slice"});
ar & boost::serialization::make_hrp("Visibility", pd->m_Visibility);
ar & boost::serialization::make_hrp("Pickable", pd->m_Selectable);
ar & boost::serialization::make_hrp("Dragable", pd->m_Dragable);
}
};
void Puppet::serialize_display(Archive::display_properties_archive & ar, const unsigned int version) {
ar & boost::serialization::make_hrp("ColorR", pd->m_Color[0]);
ar & boost::serialization::make_hrp("ColorG", pd->m_Color[1]);
ar & boost::serialization::make_hrp("ColorB", pd->m_Color[2]);
ar & boost::serialization::make_hrp("Opacity", pd->m_Opacity);
ar & boost::serialization::make_hrp_enum("Representation", pd->m_Representation, {"Points", "Wireframe", "Surface", "SurfaceWithEdges", "Volume", "Outline", "Slice"});
ar & boost::serialization::make_hrp("Visibility", pd->m_Visibility);
ar & boost::serialization::make_hrp("Pickable", pd->m_Selectable);
ar & boost::serialization::make_hrp("Dragable", pd->m_Dragable);
AppearanceProxy appearance{pd};
ar & boost::serialization::make_nvp("Appearance", appearance);
// Geometry knobs (caution: these might be overridden by internal matrices)
ar & boost::serialization::make_hrp("PosX", pd->m_Position[0], "mm");
ar & boost::serialization::make_hrp("PosY", pd->m_Position[1], "mm");
ar & boost::serialization::make_hrp("PosZ", pd->m_Position[2], "mm");
ar & boost::serialization::make_hrp("OriX", pd->m_Orientation[0], "deg");
ar & boost::serialization::make_hrp("OriY", pd->m_Orientation[1], "deg");
ar & boost::serialization::make_hrp("OriZ", pd->m_Orientation[2], "deg");
ar & boost::serialization::make_hrp("ScaleX", pd->m_Scale[0]);
ar & boost::serialization::make_hrp("ScaleY", pd->m_Scale[1]);
ar & boost::serialization::make_hrp("ScaleZ", pd->m_Scale[2]);
TransformProxy transform{pd};
ar & boost::serialization::make_nvp("Transform", transform);
}
void Puppet::serialize(Archive::xml_oarchive & ar, const unsigned int v) { }

View File

@@ -29,6 +29,9 @@
#include <iomanip>
#include <ostream>
#include <vector>
#include <boost/type_traits/is_class.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/serialization/serialization.hpp>
#include "Core/Object.h"
#include "Core/Property.h"
#include "Core/Monitor.h"
@@ -157,10 +160,19 @@ public:
boost::archive::detail::common_oarchive<display_properties_archive>(boost::archive::no_header),
m_Puppet(puppet) {}
std::string GetCurrentGroup() const {
std::string group;
for (const auto& g : m_GroupStack) {
if (!group.empty()) group += ".";
group += g;
}
return group;
}
template<class T>
void save_override(const boost::serialization::hrp<T> &t) {
if (m_Puppet) {
uLib::Property<T>* p = new uLib::Property<T>(m_Puppet, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value(), t.units() ? t.units() : "");
uLib::Property<T>* p = new uLib::Property<T>(m_Puppet, t.name(), &const_cast<boost::serialization::hrp<T>&>(t).value(), t.units() ? t.units() : "", GetCurrentGroup());
m_Puppet->RegisterDisplayProperty(p);
Vtk::Puppet* puppet = m_Puppet;
uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); });
@@ -170,7 +182,7 @@ public:
template<class T>
void save_override(const boost::serialization::hrp_enum<T> &t) {
if (m_Puppet) {
uLib::EnumProperty* p = new uLib::EnumProperty(m_Puppet, t.name(), (int*)&const_cast<boost::serialization::hrp_enum<T>&>(t).value(), t.labels(), t.units() ? t.units() : "");
uLib::EnumProperty* p = new uLib::EnumProperty(m_Puppet, t.name(), (int*)&const_cast<boost::serialization::hrp_enum<T>&>(t).value(), t.labels(), t.units() ? t.units() : "", GetCurrentGroup());
m_Puppet->RegisterDisplayProperty(p);
Vtk::Puppet* puppet = m_Puppet;
uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); });
@@ -178,10 +190,23 @@ public:
}
template<class T> void save_override(const boost::serialization::nvp<T> &t) {
boost::archive::detail::common_oarchive<display_properties_archive>::save_override(t.const_value());
if (t.name()) m_GroupStack.push_back(t.name());
this->save_helper(t.const_value(), typename boost::is_class<T>::type());
if (t.name()) m_GroupStack.pop_back();
}
template<class T> void save_override(const T &t) {}
// Recursion for nested classes, ignore primitives
template<class T> void save_override(const T &t) {
this->save_helper(t, typename boost::is_class<T>::type());
}
template<class T>
void save_helper(const T &t, boost::mpl::true_) {
boost::serialization::serialize_adl(*this, const_cast<T&>(t), 0);
}
template<class T>
void save_helper(const T &t, boost::mpl::false_) {}
void save_override(const boost::archive::object_id_type & t) {}
void save_override(const boost::archive::object_reference_type & t) {}
@@ -194,6 +219,7 @@ public:
private:
Vtk::Puppet* m_Puppet;
std::vector<std::string> m_GroupStack;
};
} // namespace Archive