3 Commits

Author SHA1 Message Date
AndreaRigoni
93e5602562 transform properties 2026-03-27 02:43:30 +00:00
AndreaRigoni
09859e872c fix build 2026-03-27 01:49:27 +00:00
AndreaRigoni
2a6dcf02bd add properties groups 2026-03-26 23:13:43 +00:00
25 changed files with 430 additions and 1472 deletions

View File

@@ -1,338 +0,0 @@
# 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
```

View File

@@ -1,263 +0,0 @@
/*//////////////////////////////////////////////////////////////////////////////
// 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,8 +1,7 @@
set(HEADERS
Algorithm.h
Archives.h
Array.h
set(HEADERS
Archives.h
Array.h
Collection.h
DataAllocator.h
Debug.h

View File

@@ -9,7 +9,7 @@ namespace uLib {
/**
* @brief ObjectsContext represents a collection of Object instances.
*/
class ObjectsContext : public Object {
class ObjectsContext : virtual public Object {
public:
ObjectsContext();
virtual ~ObjectsContext();

View File

@@ -137,6 +137,11 @@ public:
void serialize(Archive::hrt_iarchive & ar, const unsigned int v) override { serialize_impl(ar, v); }
void serialize(Archive::log_archive & ar, const unsigned int v) override { serialize_impl(ar, v); }
virtual void Updated() override {
PropertyBase::Updated();
this->PropertyChanged();
}
private:
std::string m_name;
std::string m_units;

View File

@@ -1,206 +0,0 @@
#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

@@ -29,7 +29,6 @@ set( TESTS
OpenMPTest
TeamTest
AffinityTest
AlgorithmTest
)
set(LIBRARIES

View File

@@ -23,7 +23,7 @@ class G4Event;
namespace uLib {
namespace Geant {
class EmitterPrimary : public G4VUserPrimaryGeneratorAction, public Object, public AffineTransform
class EmitterPrimary : public G4VUserPrimaryGeneratorAction, public AffineTransform
{
public:

View File

@@ -45,6 +45,7 @@ namespace uLib {
*/
class Assembly : public ObjectsContext, public AffineTransform {
public:
uLibTypeMacro(Assembly, ObjectsContext, AffineTransform)
virtual const char *GetClassName() const override { return "Assembly"; }
Assembly();

View File

@@ -44,16 +44,17 @@ namespace uLib {
* that defines the box's specific origin and size relative to its own
* coordinate system.
*/
class ContainerBox : public AffineTransform, public Object {
typedef AffineTransform BaseClass;
class ContainerBox : public AffineTransform {
public:
uLibTypeMacro(ContainerBox, AffineTransform)
virtual const char * GetClassName() const override { return "ContainerBox"; }
////////////////////////////////////////////////////////////////////////////
// PROPERTIES //
Property<Vector3f> p_Size;
Property<Vector3f> p_Origin;
virtual const char * GetClassName() const { return "ContainerBox"; }
Vector3f Size;
Vector3f Origin;
/**
* @brief Default constructor.
@@ -61,10 +62,10 @@ public:
*/
ContainerBox()
: m_LocalT(this), // BaseClass is Parent of m_LocalTransform
p_Size(this, "Size", Vector3f(1.0f, 1.0f, 1.0f)),
p_Origin(this, "Origin", Vector3f(0.0f, 0.0f, 0.0f)) {
Object::connect(&p_Size, &Property<Vector3f>::PropertyChanged, this, &ContainerBox::SyncSize);
Object::connect(&p_Origin, &Property<Vector3f>::PropertyChanged, this, &ContainerBox::SyncOrigin);
Size(1.0f, 1.0f, 1.0f),
Origin(0.0f, 0.0f, 0.0f) {
ULIB_ACTIVATE_PROPERTIES(*this);
this->Sync();
}
/**
@@ -73,11 +74,10 @@ public:
*/
ContainerBox(const Vector3f &size)
: m_LocalT(this),
p_Size(this, "Size", size),
p_Origin(this, "Origin", Vector3f(0.0f, 0.0f, 0.0f)) {
Object::connect(&p_Size, &Property<Vector3f>::PropertyChanged, this, &ContainerBox::SyncSize);
Object::connect(&p_Origin, &Property<Vector3f>::PropertyChanged, this, &ContainerBox::SyncOrigin);
this->SetSize(size);
Size(size),
Origin(0.0f, 0.0f, 0.0f) {
ULIB_ACTIVATE_PROPERTIES(*this);
this->Sync();
}
/**
@@ -85,13 +85,21 @@ public:
* @param copy The ContainerBox instance to copy from.
*/
ContainerBox(const ContainerBox &copy)
: m_LocalT(copy.m_LocalT), // Copy local transform state
: m_LocalT(this), // Reset parent to the new object
AffineTransform(copy),
p_Size(this, "Size", copy.p_Size),
p_Origin(this, "Origin", copy.p_Origin) {
m_LocalT.SetParent(this); // Reset parent to the new object
Object::connect(&p_Size, &Property<Vector3f>::PropertyChanged, this, &ContainerBox::SyncSize);
Object::connect(&p_Origin, &Property<Vector3f>::PropertyChanged, this, &ContainerBox::SyncOrigin);
Size(copy.Size),
Origin(copy.Origin) {
ULIB_ACTIVATE_PROPERTIES(*this);
this->Sync();
}
/**
* @brief Serialization template for property registration and persistence.
*/
template <class ArchiveT>
void serialize(ArchiveT & ar, const unsigned int version) {
ar & HRP(Size);
ar & HRP(Origin);
}
/**
@@ -99,7 +107,7 @@ public:
* @param v The origin position vector.
*/
void SetOrigin(const Vector3f &v) {
p_Origin = v;
Origin = v;
m_LocalT.SetPosition(v);
}
@@ -115,7 +123,7 @@ public:
* @param v The size vector (width, height, depth).
*/
void SetSize(const Vector3f &v) {
p_Size = v;
Size = v;
Vector3f pos = this->GetOrigin();
m_LocalT = AffineTransform(this); // regenerate local transform
m_LocalT.Scale(v);
@@ -194,26 +202,27 @@ public:
}
/** Translate using transformation chain */
using BaseClass::Translate;
using AffineTransform::Translate;
/** Rotate using transformation chain */
using BaseClass::Rotate;
using AffineTransform::Rotate;
/** Scale using transformation chain */
using BaseClass::Scale;
using AffineTransform::Scale;
signals:
// signal to emit when the box is updated //
virtual void Updated() override { ULIB_SIGNAL_EMIT(ContainerBox::Updated); }
private slots:
void SyncSize() {
this->SetSize(p_Size);
/** Signal emitted when properties change */
virtual void Updated() override {
this->Sync();
ULIB_SIGNAL_EMIT(ContainerBox::Updated);
}
void SyncOrigin() {
this->SetOrigin(p_Origin);
private:
/** Synchronizes internal transformation with properties */
void Sync() {
this->SetOrigin(Origin);
this->SetSize(Size);
}

View File

@@ -39,10 +39,17 @@ namespace uLib {
* The cylinder orientation is defined by the Axis property (0=X, 1=Y, 2=Z).
* By default, it is aligned with the Y axis (Axis=1).
*/
class Cylinder : public AffineTransform, public Object {
class Cylinder : public AffineTransform {
public:
uLibTypeMacro(Cylinder, Object)
uLibTypeMacro(Cylinder, AffineTransform)
/**
* @brief PROPERTIES
*/
float Radius;
float Height;
int Axis;
virtual const char * GetClassName() const override { return "Cylinder"; }
@@ -51,7 +58,7 @@ public:
*/
Cylinder() : m_LocalT(this), Radius(1.0), Height(1.0), Axis(1) {
ULIB_ACTIVATE_PROPERTIES(*this);
UpdateLocalMatrix();
this->Sync();
}
/**
@@ -60,7 +67,7 @@ public:
Cylinder(float radius, float height, int axis = 1)
: m_LocalT(this), Radius(radius), Height(height), Axis(axis) {
ULIB_ACTIVATE_PROPERTIES(*this);
UpdateLocalMatrix();
this->Sync();
}
/**
@@ -69,7 +76,7 @@ public:
Cylinder(const Cylinder &copy)
: m_LocalT(this), AffineTransform(copy), Radius(copy.Radius), Height(copy.Height), Axis(copy.Axis) {
ULIB_ACTIVATE_PROPERTIES(*this);
this->UpdateLocalMatrix();
this->Sync();
}
/**
@@ -85,7 +92,7 @@ public:
/** Sets the radius of the cylinder */
inline void SetRadius(float r) {
Radius = r;
UpdateLocalMatrix();
this->Sync();
}
/** Gets the radius of the cylinder */
@@ -94,7 +101,7 @@ public:
/** Sets the height of the cylinder */
inline void SetHeight(float h) {
Height = h;
UpdateLocalMatrix();
this->Sync();
}
/** Gets the height of the cylinder */
@@ -103,7 +110,7 @@ public:
/** Sets the main axis (0=X, 1=Y, 2=Z) */
inline void SetAxis(int axis) {
Axis = axis;
UpdateLocalMatrix();
this->Sync();
}
/** Gets the main axis */
@@ -157,25 +164,33 @@ public:
return Vector3f(r, theta, h);
}
/** Translate using transformation chain */
using AffineTransform::Translate;
/** Rotate using transformation chain */
using AffineTransform::Rotate;
/** Scale using transformation chain */
using AffineTransform::Scale;
signals:
/** Signal emitted when properties change */
virtual void Updated() override {
this->UpdateLocalMatrix();
this->Sync();
ULIB_SIGNAL_EMIT(Cylinder::Updated);
}
private:
/** Recalculates the internal local matrix based on dimensions and axis */
void UpdateLocalMatrix() {
m_LocalT = AffineTransform(this);
if (Axis == 0) m_LocalT.Scale(Vector3f(Height, Radius, Radius));
else if (Axis == 1) m_LocalT.Scale(Vector3f(Radius, Height, Radius));
else m_LocalT.Scale(Vector3f(Radius, Radius, Height));
}
/** Synchronizes internal transformation with properties */
void Sync() {
m_LocalT = AffineTransform(this);
if (Axis == 0) m_LocalT.Scale(Vector3f(Height, Radius, Radius));
else if (Axis == 1) m_LocalT.Scale(Vector3f(Radius, Height, Radius));
else m_LocalT.Scale(Vector3f(Radius, Radius, Height));
}
float Radius;
float Height;
int Axis;
private:
AffineTransform m_LocalT;
};

View File

@@ -35,10 +35,11 @@
namespace uLib {
class Geometry : public AffineTransform, public Object {
class Geometry : public AffineTransform {
public:
uLibTypeMacro(Geometry, AffineTransform)
virtual const char * GetClassName() const { return "Geometry"; }
virtual const char * GetClassName() const override { return "Geometry"; }
virtual Vector3f ToLinear(const Vector3f& curved_space) const {
return curved_space;
@@ -70,6 +71,7 @@ public:
class CylindricalGeometry : public Geometry {
public:
uLibTypeMacro(CylindricalGeometry, Geometry)
CylindricalGeometry() {}
Vector3f ToLinear(const Vector3f& cylindrical) const {
@@ -88,9 +90,10 @@ public:
class SphericalGeometry : public Geometry {
public:
uLibTypeMacro(SphericalGeometry, Geometry)
SphericalGeometry() {}
virtual const char * GetClassName() const { return "SphericalGeometry"; }
virtual const char * GetClassName() const override { return "SphericalGeometry"; }
Vector3f ToLinear(const Vector3f& spherical) const {
float r = spherical.x();
@@ -112,9 +115,10 @@ public:
class ToroidalGeometry : public Geometry {
public:
uLibTypeMacro(ToroidalGeometry, Geometry)
ToroidalGeometry(float Rtor) : m_Rtor(Rtor) {}
virtual const char * GetClassName() const { return "ToroidalGeometry"; }
virtual const char * GetClassName() const override { return "ToroidalGeometry"; }
Vector3f ToLinear(const Vector3f& toroidal) const {
float r = toroidal.x();

View File

@@ -34,11 +34,12 @@
namespace uLib {
class QuadMesh : public AffineTransform, public Object
class QuadMesh : public AffineTransform
{
public:
uLibTypeMacro(QuadMesh, AffineTransform)
virtual const char * GetClassName() const { return "QuadMesh"; }
virtual const char * GetClassName() const override { return "QuadMesh"; }
void PrintSelf(std::ostream &o);

View File

@@ -50,6 +50,7 @@
#define U_TRANSFORM_H
#include <Eigen/Geometry>
#include "Math/Units.h"
#include "Math/Dense.h"
@@ -59,27 +60,65 @@ namespace uLib {
///////// AFFINE TRANSFORM WRAPPER //////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
class AffineTransform {
class AffineTransform : virtual public Object {
public:
uLibTypeMacro(AffineTransform, Object)
/**
* @brief Grouped transformation parameters for property-based control.
*/
struct {
Vector3f Position = Vector3f::Zero();
Vector3f Orientation = Vector3f::Zero();
Vector3f Scale = Vector3f::Ones();
template <class ArchiveT>
void serialize(ArchiveT & ar, const unsigned int version) {
ar & HRPU(Position, "mm");
ar & HRPU(Orientation, "deg");
ar & HRP(Scale);
}
} Transform;
protected:
Eigen::Affine3f m_T;
AffineTransform *m_Parent;
public:
AffineTransform() :
m_T(Matrix4f::Identity()),
m_Parent(NULL)
{}
{
ULIB_ACTIVATE_PROPERTIES(*this);
this->Sync();
}
virtual ~AffineTransform() {}
AffineTransform(AffineTransform *parent) :
m_T(Matrix4f::Identity()),
m_Parent(parent)
{}
{
ULIB_ACTIVATE_PROPERTIES(*this);
this->Sync();
}
AffineTransform(const AffineTransform &copy) :
m_T(copy.m_T),
m_Parent(copy.m_Parent)
{}
m_Parent(copy.m_Parent),
Transform(copy.Transform)
{
ULIB_ACTIVATE_PROPERTIES(*this);
this->Sync();
}
/**
* @brief Registration of properties in groups.
*/
template <class ArchiveT>
void serialize(ArchiveT & ar, const unsigned int version) {
ar & boost::serialization::make_nvp("Transform", Transform);
}
Eigen::Affine3f& GetTransform() { return m_T; }
@@ -87,7 +126,11 @@ public:
void SetParent(AffineTransform *name) { this->m_Parent = name; }
void SetMatrix (Matrix4f mat) { m_T.matrix() = mat; }
void SetMatrix (Matrix4f mat) {
m_T.matrix() = mat;
this->UpdatePropertiesFromMatrix();
}
Matrix4f GetMatrix() const { return m_T.matrix(); }
Matrix4f GetWorldMatrix() const
@@ -96,32 +139,51 @@ public:
else return m_Parent->GetWorldMatrix() * m_T.matrix(); // T = B * A //
}
void SetPosition(const Vector3f v) { this->m_T.translation() = v; }
void SetPosition(const Vector3f v) {
this->Transform.Position = v;
this->Sync();
}
Vector3f GetPosition() const { return this->m_T.translation(); }
Vector3f GetPosition() const { return this->Transform.Position; }
void SetRotation(const Matrix3f m) { this->m_T.linear() = m; }
void SetRotation(const Matrix3f m) {
this->m_T.linear() = m;
this->UpdatePropertiesFromMatrix();
}
Matrix3f GetRotation() const { return this->m_T.rotation(); }
void Translate(const Vector3f v) { this->m_T.translate(v); }
void Scale(const Vector3f v) { this->m_T.scale(v); }
Vector3f GetScale() const {
return Vector3f(m_T.linear().col(0).norm(),
m_T.linear().col(1).norm(),
m_T.linear().col(2).norm());
void Translate(const Vector3f v) {
this->Transform.Position += v;
this->Sync();
}
void Scale(const Vector3f v) {
this->Transform.Scale = this->Transform.Scale.cwiseProduct(v);
this->Sync();
}
void Rotate(const Matrix3f m) { this->m_T.rotate(m); }
Vector3f GetScale() const { return this->Transform.Scale; }
void SetOrientation(const Vector3f v) {
this->Transform.Orientation = v;
this->Sync();
}
Vector3f GetOrientation() const { return this->Transform.Orientation; }
void Rotate(const Matrix3f m) {
this->m_T.rotate(m);
this->UpdatePropertiesFromMatrix();
}
void Rotate(const float angle, Vector3f axis)
{
axis.normalize(); // prehaps not necessary ( see eigens )
axis.normalize();
Eigen::AngleAxisf ax(angle,axis);
this->m_T.rotate(Eigen::Quaternion<float>(ax));
this->UpdatePropertiesFromMatrix();
}
void Rotate(const Vector3f euler_axis) {
@@ -129,17 +191,14 @@ public:
Rotate(angle,euler_axis);
}
void PreRotate(const Matrix3f m) { this->m_T.prerotate(m); }
void PreRotate(const Matrix3f m) { this->m_T.prerotate(m); this->UpdatePropertiesFromMatrix(); }
void QuaternionRotate(const Vector4f q)
{ this->m_T.rotate(Eigen::Quaternion<float>(q)); }
{ this->m_T.rotate(Eigen::Quaternion<float>(q)); this->UpdatePropertiesFromMatrix(); }
void EulerYZYRotate(const Vector3f e) {
Matrix3f mat;
mat = Eigen::AngleAxisf(e.x(), Vector3f::UnitY())
* Eigen::AngleAxisf(e.y(), Vector3f::UnitZ())
* Eigen::AngleAxisf(e.z(), Vector3f::UnitY());
m_T.rotate(mat);
this->Transform.Orientation = e;
this->Sync();
}
void FlipAxes(int first, int second)
@@ -147,7 +206,61 @@ public:
Matrix3f mat = Matrix3f::Identity();
mat.col(first).swap(mat.col(second));
m_T.rotate(mat);
this->UpdatePropertiesFromMatrix();
}
/**
* @brief Decomposes the internal matrix m_T back into Position, Orientation, and Scale properties.
*/
void UpdatePropertiesFromMatrix() {
// 1. Position
Transform.Position = m_T.translation();
// 2. Scale
Matrix3f linear = m_T.linear();
Transform.Scale(0) = linear.col(0).norm();
Transform.Scale(1) = linear.col(1).norm();
Transform.Scale(2) = linear.col(2).norm();
// 3. Rotation (Normalization removes scale)
Matrix3f rotation = linear;
if (Transform.Scale(0) > 1e-6) rotation.col(0) /= Transform.Scale(0);
if (Transform.Scale(1) > 1e-6) rotation.col(1) /= Transform.Scale(1);
if (Transform.Scale(2) > 1e-6) rotation.col(2) /= Transform.Scale(2);
// Euler YZY (indices 1, 2, 1)
Vector3f euler = rotation.eulerAngles(1, 2, 1);
Transform.Orientation = euler / CLHEP::degree;
// Notify properties
PropertyBase* p;
if ((p = this->GetProperty("Transform.Position"))) p->Updated();
if ((p = this->GetProperty("Transform.Orientation"))) p->Updated();
if ((p = this->GetProperty("Transform.Scale"))) p->Updated();
}
signals:
/** Signal emitted when properties change */
virtual void Updated() override {
this->Sync();
ULIB_SIGNAL_EMIT(AffineTransform::Updated);
}
private:
/** Synchronizes m_T with properties */
void Sync() {
m_T = Eigen::Affine3f::Identity();
m_T.translate(Transform.Position);
// Orientation (using YZY order as implied by EulerYZYRotate)
Matrix3f mat;
mat = Eigen::AngleAxisf(Transform.Orientation.x() * CLHEP::degree, Vector3f::UnitY())
* Eigen::AngleAxisf(Transform.Orientation.y() * CLHEP::degree, Vector3f::UnitZ())
* Eigen::AngleAxisf(Transform.Orientation.z() * CLHEP::degree, Vector3f::UnitY());
m_T.rotate(mat);
m_T.scale(Transform.Scale);
}
};

View File

@@ -37,11 +37,12 @@
namespace uLib {
class TriangleMesh : public AffineTransform, public Object
class TriangleMesh : public AffineTransform
{
public:
uLibTypeMacro(TriangleMesh, AffineTransform)
virtual const char * GetClassName() const { return "TriangleMesh"; }
virtual const char * GetClassName() const override { return "TriangleMesh"; }
void PrintSelf(std::ostream &o);

View File

@@ -27,16 +27,12 @@
#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() {
@@ -46,95 +42,63 @@ 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
////////////////////////////////////////////////////////////////////////////////
// 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 AlgorithmT>
class VoxImageFilter : public Abstract::VoxImageFilter, public Object {
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);
// Accessors //////////////////////////////////////////////////////////////////
inline const Kernel<VoxelT> &GetKernelData() const {
return this->m_KernelData;
}
inline Kernel<VoxelT> &GetKernelData() { return this->m_KernelData; }
const Kernel<VoxelT> &GetKernelData() const { return m_KernelData; }
Kernel<VoxelT> &GetKernelData() { return m_KernelData; }
inline VoxImage<VoxelT> *GetImage() const { return this->m_Image; }
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:
CrtpImplT *m_CrtpImpl;
AlgorithmT *t_Algoritm;
};
} // namespace uLib

View File

@@ -33,9 +33,7 @@
namespace uLib {
////////////////////////////////////////////////////////////////////////////////
//// KERNEL ////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// KERNEL //////////////////////////////////////////////////////////////////////
template <typename T> class Kernel : public StructuredData {
typedef StructuredData BaseClass;
@@ -43,12 +41,13 @@ template <typename T> class Kernel : public StructuredData {
public:
Kernel(const Vector3i &size);
T &operator[](const Vector3i &id) { return m_Data[Map(id)]; }
T &operator[](const int &id) { return m_Data[id]; }
int GetCenterData() const;
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;
DataAllocator<T> &Data() { return m_Data; }
const DataAllocator<T> &ConstData() const { return m_Data; }
inline DataAllocator<T> &Data() { return this->m_Data; }
inline const DataAllocator<T> &ConstData() const { return this->m_Data; }
void PrintSelf(std::ostream &o) const;
@@ -61,14 +60,12 @@ Kernel<T>::Kernel(const Vector3i &size) : BaseClass(size), m_Data(size.prod()) {
Interface::IsA<T, Interface::Voxel>();
}
template <typename T>
int Kernel<T>::GetCenterData() const {
template <typename T> inline 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"
@@ -99,42 +96,26 @@ void Kernel<T>::PrintSelf(std::ostream &o) const {
}
}
////////////////////////////////////////////////////////////////////////////////
//// VOXIMAGEFILTER IMPLEMENTATION /////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
template <typename VoxelT, typename CrtpImplT>
VoxImageFilter<VoxelT, CrtpImplT>::VoxImageFilter(const Vector3i &size)
: m_KernelData(size)
, m_Image(nullptr)
, m_CrtpImpl(static_cast<CrtpImplT *>(this))
{}
#define _TPL_ template <typename VoxelT, typename AlgorithmT>
#define _TPLT_ VoxelT, AlgorithmT
template <typename VoxelT, typename CrtpImplT>
VoxImage<VoxelT>* VoxImageFilter<VoxelT, CrtpImplT>::Process(
VoxImage<VoxelT>* const& image) {
if (m_Image != image) SetImage(image);
_TPL_
VoxImageFilter<_TPLT_>::VoxImageFilter(const Vector3i &size)
: m_KernelData(size), t_Algoritm(static_cast<AlgorithmT *>(this)) {}
_TPL_
void VoxImageFilter<_TPLT_>::Run() {
VoxImage<VoxelT> buffer = *m_Image;
#pragma omp parallel for
for (int i = 0; i < m_Image->Data().size(); ++i)
m_Image->operator[](i).Value = m_CrtpImpl->Evaluate(buffer, i);
m_Image->operator[](i).Value = this->t_Algoritm->Evaluate(buffer, i);
#pragma omp barrier
return m_Image;
}
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() {
_TPL_
void VoxImageFilter<_TPLT_>::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) {
@@ -146,10 +127,10 @@ void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelOffset() {
}
}
template <typename VoxelT, typename CrtpImplT>
float VoxImageFilter<VoxelT, CrtpImplT>::Distance2(const Vector3i &v) {
_TPL_
float VoxImageFilter<_TPLT_>::Distance2(const Vector3i &v) {
Vector3i tmp = v;
const Vector3i &dim = m_KernelData.GetDims();
const Vector3i &dim = this->m_KernelData.GetDims();
Vector3i center = dim / 2;
tmp = tmp - center;
center = center.cwiseProduct(center);
@@ -159,9 +140,12 @@ float VoxImageFilter<VoxelT, CrtpImplT>::Distance2(const Vector3i &v) {
0.25 * (3 - (dim(0) % 2) - (dim(1) % 2) - (dim(2) % 2)));
}
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelNumericXZY(
_TPL_
void VoxImageFilter<_TPLT_>::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) {
@@ -172,39 +156,38 @@ void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelNumericXZY(
}
}
}
// m_KernelData.SetDataOrder(order);
}
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelSpherical(
float (*shape)(float)) {
_TPL_
void VoxImageFilter<_TPLT_>::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(Distance2(id));
m_KernelData[id].Value = shape(this->Distance2(id));
}
}
}
}
template <typename VoxelT, typename CrtpImplT>
template <class ShapeT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelSpherical(ShapeT shape) {
_TPL_ template <class ShapeT>
void VoxImageFilter<_TPLT_>::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(Distance2(id));
m_KernelData[id].Value = shape(this->Distance2(id));
}
}
}
}
template <typename VoxelT, typename CrtpImplT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelWeightFunction(
_TPL_
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(
float (*shape)(const Vector3f &)) {
const Vector3i &dim = m_KernelData.GetDims();
Vector3i id;
@@ -212,19 +195,20 @@ void VoxImageFilter<VoxelT, CrtpImplT>::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);
}
}
}
}
template <typename VoxelT, typename CrtpImplT>
template <class ShapeT>
void VoxImageFilter<VoxelT, CrtpImplT>::SetKernelWeightFunction(ShapeT shape) {
_TPL_ template <class ShapeT>
void VoxImageFilter<_TPLT_>::SetKernelWeightFunction(ShapeT shape) {
Interface::IsA<ShapeT, Interface::VoxImageFilterShape>();
const Vector3i &dim = m_KernelData.GetDims();
Vector3i id;
@@ -232,16 +216,45 @@ void VoxImageFilter<VoxelT, CrtpImplT>::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,8 +109,7 @@ public:
}
#if defined(USE_CUDA) && defined(__CUDACC__)
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
if (this->m_Image != image) this->SetImage(image);
void Run() {
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
@@ -137,9 +136,8 @@ public:
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count,
mAtrim, mBtrim);
cudaDeviceSynchronize();
return this->m_Image;
} else {
return BaseClass::Process(image);
BaseClass::Run();
}
}
#endif
@@ -209,8 +207,7 @@ public:
}
#if defined(USE_CUDA) && defined(__CUDACC__)
VoxImage<VoxelT>* Process(VoxImage<VoxelT>* const& image) override {
if (this->m_Image != image) this->SetImage(image);
void Run() {
if (this->m_Image->Data().GetDevice() == MemoryDevice::VRAM ||
this->m_KernelData.Data().GetDevice() == MemoryDevice::VRAM) {
@@ -237,9 +234,8 @@ public:
d_img_in, d_img_out, d_kernel, vox_size, ker_size, center_count,
mAtrim, mBtrim);
cudaDeviceSynchronize();
return this->m_Image;
} else {
return BaseClass::Process(image);
BaseClass::Run();
}
}
#endif

View File

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

View File

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

View File

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

View File

@@ -1,408 +0,0 @@
/*//////////////////////////////////////////////////////////////////////////////
// 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,7 +16,6 @@ set(TESTS
QuadMeshTest
BitCodeTest
UnitsTest
AlgorithmCudaChainTest
)
set(LIBRARIES
@@ -29,6 +28,6 @@ set(LIBRARIES
uLib_add_tests(Math)
if(USE_CUDA)
set_source_files_properties(VoxImageTest.cpp VoxImageCopyTest.cpp VoxImageFilterTest.cpp VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp AlgorithmCudaChainTest.cpp PROPERTIES LANGUAGE CUDA)
set_source_files_properties(VoxImageTest.cpp VoxImageCopyTest.cpp VoxImageFilterTest.cpp VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp PROPERTIES LANGUAGE CUDA)
set_source_files_properties(VoxRaytracerTest.cpp VoxRaytracerTestExtended.cpp PROPERTIES CXX_STANDARD 17 CUDA_STANDARD 17)
endif()

View File

@@ -41,6 +41,7 @@
#include <vtkVersion.h>
#include "vtkViewport.h"
#include "uLibVtkInterface.h"
#include "Math/Transform.h"
#include <vtkActor.h>
#include <vtkPolyDataMapper.h>
#include <vtkProperty.h>
@@ -484,7 +485,7 @@ void Puppet::SetSelected(bool selected)
if (!pd->m_Selectable) return;
if (pd->m_Selected == selected) return;
pd->m_Selected = selected;
pd->UpdateHighlight();
pd->UpdateHighlight();0
}
bool Puppet::IsSelected() const
@@ -498,8 +499,25 @@ void Puppet::Update()
if (root) {
pd->ApplyAppearance(root);
// Apply transformation if it's a Prop3D
if (auto* p3d = vtkProp3D::SafeDownCast(root)) {
// Handle transformation synchronization from content
if (auto* content = dynamic_cast<uLib::AffineTransform*>(GetContent())) {
pd->m_Position = content->GetPosition().cast<double>();
pd->m_Orientation = content->GetOrientation().cast<double>();
pd->m_Scale = content->GetScale().cast<double>();
if (auto* p3d = vtkProp3D::SafeDownCast(root)) {
vtkNew<vtkMatrix4x4> vmat;
const Matrix4f& emat = content->GetMatrix();
for(int i=0; i<4; ++i) for(int j=0; j<4; ++j) vmat->SetElement(i, j, emat(i,j));
p3d->SetUserMatrix(vmat);
// Clear base transform to avoid double-application
p3d->SetPosition(0,0,0);
p3d->SetOrientation(0,0,0);
p3d->SetScale(1,1,1);
}
}
else if (auto* p3d = vtkProp3D::SafeDownCast(root)) {
p3d->SetPosition(pd->m_Position.data());
p3d->SetOrientation(pd->m_Orientation.data());
p3d->SetScale(pd->m_Scale.data());
@@ -539,23 +557,42 @@ void Puppet::Update()
}
}
void Puppet::SyncFromVtk()
{
vtkProp* root = this->GetProp();
if (auto* p3d = vtkProp3D::SafeDownCast(root)) {
double pos[3], ori[3], scale[3];
p3d->GetPosition(pos);
p3d->GetOrientation(ori);
p3d->GetScale(scale);
// 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];
// Handle content synchronization if it's an AffineTransform
if (auto* content = dynamic_cast<uLib::AffineTransform*>(GetContent())) {
vtkMatrix4x4* vmat = p3d->GetUserMatrix();
if (vmat) {
Matrix4f emat;
for (int i=0; i<4; ++i)
for (int j=0; j<4; ++j)
emat(i, j) = vmat->GetElement(i, j);
content->SetMatrix(emat);
// Re-sync internal puppet properties from the now-updated content
pd->m_Position = content->GetPosition().cast<double>();
pd->m_Orientation = content->GetOrientation().cast<double>();
pd->m_Scale = content->GetScale().cast<double>();
}
}
else {
// Update internal puppet properties directly from base components
// only if no content exists (old behavior)
double pos[3], ori[3], scale[3];
p3d->GetPosition(pos);
p3d->GetOrientation(ori);
p3d->GetScale(scale);
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];
}
}
// Get the properties from the object
// Notify puppet properties updated
if (auto* propPos = this->GetProperty("Position")) propPos->Updated();
if (auto* propOri = this->GetProperty("Orientation")) propOri->Updated();
if (auto* propScale = this->GetProperty("Scale")) propScale->Updated();

View File

@@ -110,17 +110,16 @@ void vtkObjectsContext::Update() {
Puppet* vtkObjectsContext::CreatePuppet(uLib::Object* obj) {
if (!obj) return nullptr;
const char* className = obj->GetClassName();
if (std::strcmp(className, "ContainerBox") == 0) {
return new vtkContainerBox(static_cast<uLib::ContainerBox*>(obj));
} else if (std::strcmp(className, "DetectorChamber") == 0) {
return new vtkDetectorChamber(static_cast<uLib::DetectorChamber*>(obj));
} else if (std::strcmp(className, "Cylinder") == 0) {
return new vtkCylinder(static_cast<uLib::Cylinder*>(obj));
} else if (std::strcmp(className, "VoxImage") == 0) {
return new vtkVoxImage(*static_cast<uLib::Abstract::VoxImage*>(obj));
} else if (std::strcmp(className, "Assembly") == 0) {
return new Assembly(static_cast<uLib::Assembly*>(obj));
if (auto* box = dynamic_cast<uLib::ContainerBox*>(obj)) {
return new vtkContainerBox(box);
} else if (auto* chamber = dynamic_cast<uLib::DetectorChamber*>(obj)) {
return new vtkDetectorChamber(chamber);
} else if (auto* cylinder = dynamic_cast<uLib::Cylinder*>(obj)) {
return new vtkCylinder(cylinder);
} else if (auto* vox = dynamic_cast<uLib::Abstract::VoxImage*>(obj)) {
return new vtkVoxImage(*vox);
} else if (auto* assembly = dynamic_cast<uLib::Assembly*>(obj)) {
return new Assembly(assembly);
}
// Fallback if we don't know the exact class but it might be a context itself