# How to Implement a Transform Module in ApraPipes to Process Incoming Frames

> Learn to implement a transform module in ApraPipes to process incoming frames. Subclass Module, define Props, and override hooks like validateInputPins, addInputPin, and process for custom frame manipulation.

- Repository: [Apra Labs/aprapipes](https://github.com/apra-labs/aprapipes)
- Tags: how-to-guide
- Published: 2026-02-25

---

**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`](https://github.com/apra-labs/aprapipes/blob/main/base/include/Module.h) and [`base/src/FaceDetectorXform.cpp`](https://github.com/apra-labs/aprapipes/blob/main/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`](https://github.com/apra-labs/aprapipes/blob/main/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`](https://github.com/apra-labs/aprapipes/blob/main/ExampleTransformXform.h)). The **Props class** handles configurable parameters, serialization, and dynamic property updates.

```cpp
// 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)`.

```cpp
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`](https://github.com/apra-labs/aprapipes/blob/main/base/src/FaceDetectorXform.cpp).

### 3. Implement Core Virtual Hooks

Create the implementation file in `base/src/` (e.g., [`ExampleTransformXform.cpp`](https://github.com/apra-labs/aprapipes/blob/main/ExampleTransformXform.cpp)). First, define the `Detail` struct and constructor:

```cpp
// 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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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`](https://github.com/apra-labs/aprapipes/blob/main/base/src/declarative/ModuleRegistrations.cpp) using the fluent builder API:

```cpp
#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:

```json
{
  "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`](https://github.com/apra-labs/aprapipes/blob/main/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`](https://github.com/apra-labs/aprapipes/blob/main/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`](https://github.com/apra-labs/aprapipes/blob/main/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`](https://github.com/apra-labs/aprapipes/blob/main/base/src/FaceDetectorXform.cpp), where OpenCV DNN networks initialize in `init()` and persist in the `Detail` struct.