Skip to content

C++ API#

The C++ API is the native interface to libvl. The detailed Doxygen reference is available here. Sample applications can be found in the vl-samples repository.

The library is distributed as a .tar.gz from the project's public package registry — see Installation.

Core Concepts#

The API is built around two main interfaces:

  • ICamera — camera control and configuration
  • IFrameObserver — callback interface for receiving frames

The recommended usage pattern is streaming mode: configure the camera, attach one or more IFrameObserver instances, then call start(). Frames are delivered asynchronously to each observer.

Minimal Example#

#include <iostream>
#include <vl/camera.h>

class FrameObserver : public vl::IFrameObserver
{
public:
    void onFrameReceived(const vl::Frame &frame) override
    {
        if (!frame.valid()) return;

        std::cout << "Frame received"
                  << "  timestamp: " << frame.timestamp() << " µs"
                  << "  size: " << frame.image().width() << "x" << frame.image().height()
                  << std::endl;
    }
};

int main()
{
    // Create camera (auto-detects hardware)
    auto cam = vl::makeCamera();

    // Configure image output
    auto settings = std::make_shared<vl::ImageSettings>();
    settings->format = vl::ImageFormat::RGB888;

    // Attach observer and start streaming
    auto observer = std::make_shared<FrameObserver>();
    cam->addStream(observer, settings);
    cam->start();

    // Wait for user input, then stop
    std::string input;
    std::cin >> input;
    cam->stop();

    return 0;
}

Setup and Run#

Log into the camera and run:

git clone https://git.3dvisionlabs.com/3dvisionlabs/software/ecm/vl-samples.git
cd vl-samples/cpp/streaming
./run.sh

Camera Creation#

Use makeCamera(). It detects the connected hardware and creates the matching camera instance, so the same binary runs on every C7 generation regardless of which sensor module is fitted:

auto cam = vl::makeCamera();

The backend-specific factories exist for diagnostics and for the rare case where a device must be forced onto a particular backend:

auto cam = vl::makeArgusCamera();   // Nvidia Argus (Jetson CSI)
auto cam = vl::makeAlviumCamera();  // Allied Vision Alvium (VimbaX)
auto cam = vl::makeVcCamera();      // Vision Components

Warning

Hard-coding a factory ties the application to one camera generation — it will fail on any device with a different module. Prefer makeCamera() and query capabilities at runtime with the *Available() methods instead of branching on the camera type.

ImageSettings#

ImageSettings controls post-processing applied to each frame before delivery:

auto settings = std::make_shared<vl::ImageSettings>();
settings->format   = vl::ImageFormat::RGB888;   // RGB888, Gray8, NV12, YUV420
settings->scale    = 0.5;                        // Resize factor (1.0 = original)
settings->memory   = vl::MemoryType::Cuda;       // Host, Cuda, CudaManaged
settings->encoding = vl::EncodingSettings(vl::ImageEncoding::Jpg, 90); // JPEG @ quality 90

Exposure and Gain#

// Manual exposure
cam->setAutoExposureMode(vl::AutoControlMode::Off);
cam->setExposure(10000);  // 10 ms in µs

// Auto exposure with limits
cam->setAutoExposureMode(vl::AutoControlMode::Continuous);
cam->setExposureRange(vl::Range<double>(1000, 30000));  // 1-30 ms
cam->setAutoControlTargetBrightness(0.5);

// Gain
cam->setGain(12.0);  // dB
auto limits = cam->gainLimits();  // Query hardware limits

Trigger and Flash#

// Software trigger
cam->setTriggerMode(vl::TriggerMode::Software);
cam->start();
cam->trigger();  // Fire one exposure

// Hardware trigger
cam->setTriggerMode(vl::TriggerMode::HardwareRising);
cam->setTriggerDelay(100);  // µs delay after trigger edge

// Flash sync
cam->setFlashOutputMode(vl::FlashOutputMode::ExposureActive);

Available trigger modes depend on the camera hardware. Query with:

auto modes = cam->triggerModesAvailable();

Region of Interest#

if (cam->roiAvailable()) {
    auto maxSize = cam->imageSizeMax();
    cam->setRoi(vl::Rect<int>(100, 100, 640, 480));
}

Image Encoding#

// Encode via ImageSettings (automatic, per-frame)
settings->encoding = vl::EncodingSettings(vl::ImageEncoding::Jpg, 85);

// Or encode manually
#include <vl/jpeg.h>
#include <vl/png.h>

vl::EncodedImage jpg = vl::encodeJpeg(frame.image(), 90);
vl::EncodedImage png = vl::encodePng(frame.image());

Settings Persistence#

Camera settings can be saved to and loaded from JSON files:

cam->saveSettings("/path/to/config.json");
cam->loadSettings("/path/to/config.json");
cam->resetToDefaults();

Additional Controls#

Method Description
setBinningMode(BinningMode) Pixel binning (Off, 2x2, 4x4)
setAwbMode(AwbMode) White balance (Off, Auto, Daylight, ...)
setGamma(double) Gamma correction
setFrameRate(double) Frame rate in Hz
setRotationMode(RotationMode) Rotation in 90° steps
setFlipX(bool) / setFlipY(bool) Mirror image

All setters have corresponding getters and *Available() / *Limits() query methods to check hardware support at runtime.

Error Handling#

Every frame delivered to your observer carries a CaptureStatus. Always check before processing:

void onFrameReceived(const vl::Frame &frame) override
{
    if (!frame.valid()) {
        // Inspect the reason
        switch (frame.status()) {
            case vl::CaptureStatus::Timeout:
                // No frame within expected time (e.g. hardware trigger not fired)
                break;
            case vl::CaptureStatus::TimestampError:
                // Frame arrived but timestamp was invalid
                break;
            case vl::CaptureStatus::PipelineError:
                // Internal camera pipeline error — may recover on next frame
                break;
            default:
                break;
        }
        return;
    }

    // Frame is valid — safe to access image data
    auto &img = frame.image();
}
CaptureStatus Meaning
Success Frame is valid, image data can be used
Timeout No frame received in time — check trigger source or cable
TimestampError Frame received but timestamp is unreliable
PipelineError Internal error in the camera pipeline — usually transient
Uninitialized Frame object was not properly initialized
UnknownError Unexpected error

Tip

In streaming mode, transient errors (Timeout, PipelineError) are common during startup or trigger configuration changes. Your observer should handle them gracefully rather than aborting.

Memory Types#

The MemoryType in ImageSettings controls where the image data is allocated. This has significant performance implications on Jetson platforms:

MemoryType Location Use Case
Host CPU RAM Safe default. Use when processing on CPU only (OpenCV, file I/O)
Cuda GPU VRAM Use when passing images directly to CUDA kernels or TensorRT
CudaManaged Unified memory Recommended on Jetson. Accessible from both CPU and GPU without explicit copies (zero-copy)
Unspecified Library decides Lets the library pick the best option for the platform
auto settings = std::make_shared<vl::ImageSettings>();

// Zero-copy on Jetson (recommended)
settings->memory = vl::MemoryType::CudaManaged;

// CPU-only processing
settings->memory = vl::MemoryType::Host;

// Direct GPU processing
settings->memory = vl::MemoryType::Cuda;

Note

The Python API's vlImageToNPArray() currently requires Host memory. When using CudaManaged or Cuda memory, image data must be on the host before conversion to NumPy arrays.

Accessing CudaManaged Memory#

CudaManaged buffers must not be touched directly. On the Tegra platforms libvl targets, managed pages are attached to a CUDA stream while the GPU pipeline is running; reading or writing them from the CPU at that moment is undefined behaviour and typically faults or yields stale data.

Every access — CPU or GPU — therefore has to be wrapped in vl::cudaMemoryAccess(), declared in <vl/memory.h>. It takes the buffer's "is managed" flag and a callable, acquires the managed-memory lock, runs the callable, synchronises the device and releases the lock again. The callable's return value is passed through:

#include <vl/memory.h>

void onFrameReceived(const vl::Frame &frame) override
{
    if (!frame.valid()) return;

    const auto &img = frame.image();
    const bool isManaged = img.memoryResource()->memoryType() == vl::MemoryType::CudaManaged;

    // Wrong — direct access to a managed buffer
    // std::memcpy(dst, img.data(), img.size());

    // Correct — access is serialised against the GPU pipeline
    vl::cudaMemoryAccess(isManaged, [&]() {
        std::memcpy(dst, img.data(), img.size());
    });
}

Passing false for isManaged calls the lambda directly with no locking, so the same code path works unchanged for Host and Cuda buffers — always pass the actual memory type rather than branching yourself. If a buffer is known to be managed, vl::cudaManagedMemoryAccess(func, ...) skips the flag.

Warning

Keep the wrapped block short. It holds a global lock and ends in a full cudaDeviceSynchronize(), so long-running work inside it stalls the capture pipeline. Copy the data out, then process it outside the block.

cudaMemoryAccess() is a C++ function template and is excluded from the SWIG bindings. In Python and C#, use MemoryType::Host for buffers you intend to read from the application.