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:

  • Module base class – Found in base/include/Module.h, this provides the pipeline plumbing including pin management, threading, and property-change queuing. Your transform inherits protected helpers like makeFrame() and fillProps().

  • Detail pattern – Heavy implementation details (OpenCV networks, CUDA contexts) hide inside a private Detail struct accessed via boost::shared_ptr. This keeps headers clean and copy-semantics safe, as seen in production transforms like FaceDetectorXform.

  • 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 in process().

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 Module with TRANSFORM nature to create processing elements that modify frames.
  • Implement virtual hooks including validateInputPins(), addInputPin(), and process() to control validation, pin creation, and frame processing.
  • Use the Detail pattern to hide heavy resources (OpenCV nets, CUDA contexts) from the public header while maintaining safe copy semantics.
  • Register in ModuleRegistrations.cpp with selfManagedOutputPins() 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →