How to Implement a Transform Module in ApraPipes to Process Incoming Frames
To implement a transform module in ApraPipes, subclass Module with TRANSFORM nature, define a Props class inheriting from ModuleProps, and override virtual hooks including validateInputPins(), addInputPin(), and process() to handle frame processing logic.
The apra-labs/aprapipes framework treats every processing element as a Module, and implementing a custom transform module allows you to modify or process incoming frames within its pipeline architecture. This guide walks through the exact pattern used in the codebase, referencing real implementation files like base/include/Module.h and base/src/FaceDetectorXform.cpp to ensure your module integrates seamlessly with the declarative pipeline system.
Understanding the Transform Module Architecture
ApraPipes organizes processing logic into modules with specific natures. A transform module differs from sources or sinks by its TRANSFORM nature, indicating it accepts frames on input pins, processes them, and pushes results to output pins.
The architecture relies on three core components:
-
Modulebase class – Found inbase/include/Module.h, this provides the pipeline plumbing including pin management, threading, and property-change queuing. Your transform inherits protected helpers likemakeFrame()andfillProps(). -
Detailpattern – Heavy implementation details (OpenCV networks, CUDA contexts) hide inside a privateDetailstruct accessed viaboost::shared_ptr. This keeps headers clean and copy-semantics safe, as seen in production transforms likeFaceDetectorXform. -
Self-managed pins – Unlike fixed-pin modules, transforms often create output pins dynamically inside
addInputPin()after inspecting input metadata, storing the returned ID for later use inprocess().
Step-by-Step Implementation Guide
1. Define a Props Class Inheriting from ModuleProps
Create a header file in base/include/ (e.g., ExampleTransformXform.h). The Props class handles configurable parameters, serialization, and dynamic property updates.
// base/include/ExampleTransformXform.h
#pragma once
#include "Module.h"
#include "declarative/PropertyMacros.h"
class ExampleTransformXformProps : public ModuleProps {
public:
ExampleTransformXformProps(double _gain = 1.0) : gain(_gain) {}
double gain;
size_t getSerializeSize() {
return ModuleProps::getSerializeSize() + sizeof(gain);
}
template<typename PropsT>
static void applyProperties(
PropsT& props,
const std::map<std::string, apra::ScalarPropertyValue>& values,
std::vector<std::string>& missingRequired) {
apra::applyProp(props.gain, "gain", values, false, missingRequired);
}
apra::ScalarPropertyValue getProperty(const std::string& name) const {
if (name == "gain") return gain;
throw std::runtime_error("Unknown property");
}
bool setProperty(const std::string& name,
const apra::ScalarPropertyValue& v) {
if (name == "gain") return apra::applyFromVariant(gain, v);
throw std::runtime_error("Unknown property");
}
};
The applyProperties() static method enables runtime modification through the declarative system, as implemented in FaceDetectorXformProps.
2. Declare the Module Class with TRANSFORM Nature
In the same header, declare your module class inheriting from Module and forwarding the constructor to Module(TRANSFORM, "YourTransformXform", _props).
class ExampleTransformXform : public Module {
public:
explicit ExampleTransformXform(ExampleTransformXformProps _props);
virtual ~ExampleTransformXform() {}
bool init() override;
bool term() override;
void setProps(ExampleTransformXformProps& props);
ExampleTransformXformProps getProps();
protected:
bool validateInputPins() override;
bool validateOutputPins() override;
void addInputPin(framemetadata_sp& metadata, string& pinId) override;
bool process(frame_container& frames) override;
bool handlePropsChange(frame_sp& frame) override;
private:
class Detail;
boost::shared_ptr<Detail> mDetail;
};
The Detail inner class holds private state, following the pattern established in base/src/FaceDetectorXform.cpp.
3. Implement Core Virtual Hooks
Create the implementation file in base/src/ (e.g., ExampleTransformXform.cpp). First, define the Detail struct and constructor:
// base/src/ExampleTransformXform.cpp
#include "ExampleTransformXform.h"
#include "FrameMetadata.h"
#include "RawImageMetadata.h"
#include "FrameMetadataFactory.h"
#include "Frame.h"
#include "Utils.h"
#include <opencv2/opencv.hpp>
class ExampleTransformXform::Detail {
public:
explicit Detail(ExampleTransformXformProps& p) : props(p) {}
void setProps(const ExampleTransformXformProps& p) { props = p; }
ExampleTransformXformProps props;
string outPinId;
cv::Mat inputImg;
};
ExampleTransformXform::ExampleTransformXform(ExampleTransformXformProps _props)
: Module(TRANSFORM, "ExampleTransformXform", _props) {
mDetail.reset(new Detail(_props));
}
Validation methods ensure correct pipeline topology:
bool ExampleTransformXform::validateInputPins() {
if (getNumberOfInputPins() != 1) return false;
auto md = getFirstInputMetadata();
return md->getFrameType() == FrameMetadata::RAW_IMAGE;
}
bool ExampleTransformXform::validateOutputPins() {
return getNumberOfOutputPins() == 1;
}
Pin management creates output pins after input metadata is known:
void ExampleTransformXform::addInputPin(framemetadata_sp& metadata, string& pinId) {
Module::addInputPin(metadata, pinId);
// Create output pin after we know the image size
mDetail->outPinId = addOutputPin(metadata);
}
The process method contains your transformation logic:
bool ExampleTransformXform::process(frame_container& frames) {
// 1. Grab incoming frame
auto inFrame = frames.cbegin()->second;
mDetail->inputImg.data = static_cast<uint8_t*>(inFrame->data());
// 2. Apply transformation (example: gain)
cv::Mat outImg = mDetail->inputImg * mDetail->props.gain;
// 3. Allocate output frame
auto outFrame = makeFrame(outImg.total() * outImg.elemSize());
std::memcpy(outFrame->data(), outImg.data, outImg.total() * outImg.elemSize());
// 4. Insert and forward
frames.insert({mDetail->outPinId, outFrame});
send(frames);
return true;
}
Resource management handles heavy initialization:
bool ExampleTransformXform::init() {
return Module::init(); // Add CUDA/OpenCV init here if needed
}
bool ExampleTransformXform::term() {
return Module::term();
}
Dynamic property updates refresh internal state:
bool ExampleTransformXform::handlePropsChange(frame_sp& frame) {
ExampleTransformXformProps newProps;
auto ok = Module::handlePropsChange(frame, newProps);
mDetail->setProps(newProps);
return ok;
}
4. Register for Declarative Pipelines
Expose your module in base/src/declarative/ModuleRegistrations.cpp using the fluent builder API:
#include "ExampleTransformXform.h"
if (!registry.hasModule("ExampleTransformXform")) {
registerModule<ExampleTransformXform, ExampleTransformXformProps>()
.category(ModuleCategory::Transform)
.description("Applies a simple gain to raw image frames")
.tags("transform", "example")
.input("input", "RawImage")
.output("output", "RawImage")
.dynamicProp("gain", "float", "Multiplicative gain", false, "1.0")
.selfManagedOutputPins();
}
The selfManagedOutputPins() call informs the registry that your module creates pins inside addInputPin(), matching the behavior of FaceDetectorXform.
Integrating with Declarative Pipelines
Once registered, reference your transform module in JSON pipeline definitions:
{
"modules": [
{ "name": "cam", "type": "WebCamSource", "props": { "cameraId": 0 } },
{ "name": "gain", "type": "ExampleTransformXform", "props": { "gain": 1.5 } },
{ "name": "sink", "type": "FileWriterModule" }
],
"connections": [
["cam", "output", "gain", "input"],
["gain", "output", "sink", "input"]
]
}
Summary
- Inherit from
ModulewithTRANSFORMnature to create processing elements that modify frames. - Implement virtual hooks including
validateInputPins(),addInputPin(), andprocess()to control validation, pin creation, and frame processing. - Use the
Detailpattern to hide heavy resources (OpenCV nets, CUDA contexts) from the public header while maintaining safe copy semantics. - Register in
ModuleRegistrations.cppwithselfManagedOutputPins()if you create output pins dynamically based on input metadata. - Support dynamic properties by implementing
applyProperties()in your Props class and exposing them via.dynamicProp()in registration.
Frequently Asked Questions
What is the difference between a transform module and a source module in ApraPipes?
A transform module accepts input pins and processes incoming frames, while a source module generates frames and has no inputs. According to base/include/Module.h, you specify this distinction by passing TRANSFORM versus SOURCE to the Module constructor. Transforms typically override process() to manipulate data, whereas sources override produce().
How do I handle dynamic property changes in a transform module?
Implement the static applyProperties() method in your Props class (as shown in FaceDetectorXformProps) to deserialize incoming values. Then override handlePropsChange() in your module to forward these values to your Detail class. Finally, expose the property in ModuleRegistrations.cpp using the .dynamicProp() fluent API method.
Why does my transform module need self-managed output pins?
Many transforms cannot define output pins until runtime because output metadata depends on input characteristics (e.g., image dimensions). By calling addOutputPin() inside addInputPin() and storing the returned ID, you create pins dynamically. You must add .selfManagedOutputPins() in the registration block to indicate this behavior to the declarative engine.
Where should I allocate heavy resources like CUDA contexts or deep learning models?
Allocate heavy resources in the init() method and release them in term(). Store these resources inside the private Detail inner class accessed via boost::shared_ptr to keep your public header lightweight. This pattern appears in base/src/FaceDetectorXform.cpp, where OpenCV DNN networks initialize in init() and persist in the Detail struct.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →