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 configurationIFrameObserver— 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.