Skip to content
C++
Domain Track
Difficulty 3/5

Image Processing in C++

C++ image processing — OpenCV basics, pixel manipulation, filters, deep learning inference with ONNX Runtime, video pipelines, SIMD intrinsics, and performance patterns.

TL;DR

Use OpenCV for full computer vision pipelines (read, transform, detect, encode). Use stb_image for lightweight load/save without dependencies. Use SIMD or OpenCV's parallel operations for throughput-critical pixel work.


OpenCV Basics

cpp
#include <opencv2/opencv.hpp>

// Load image
cv::Mat img = cv::imread("photo.jpg");
if (img.empty()) throw std::runtime_error("failed to load image");

std::println("size: {}x{}, channels: {}", img.cols, img.rows, img.channels());

// Resize
cv::Mat thumb;
cv::resize(img, thumb, {256, 256});

// Color conversion
cv::Mat gray;
cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);

// Save
cv::imwrite("output.png", thumb);

// Show (desktop)
cv::imshow("preview", img);
cv::waitKey(0);

Pixel-Level Access

cpp
// Access individual pixels (slow — use for prototyping)
for (int y = 0; y < img.rows; ++y) {
    for (int x = 0; x < img.cols; ++x) {
        cv::Vec3b& pixel = img.at<cv::Vec3b>(y, x);
        pixel[0] = 255 - pixel[0];  // invert blue channel
        pixel[1] = 255 - pixel[1];  // invert green
        pixel[2] = 255 - pixel[2];  // invert red
    }
}

// Fast row-pointer access (preferred)
for (int y = 0; y < img.rows; ++y) {
    uint8_t* row = img.ptr<uint8_t>(y);
    for (int x = 0; x < img.cols * img.channels(); ++x) {
        row[x] = 255 - row[x];
    }
}

Common Filters

cpp
// Gaussian blur
cv::Mat blurred;
cv::GaussianBlur(img, blurred, {5, 5}, 1.5);

// Edge detection
cv::Mat edges;
cv::Canny(gray, edges, 50, 150);

// Threshold
cv::Mat binary;
cv::threshold(gray, binary, 128, 255, cv::THRESH_BINARY);
// Adaptive threshold
cv::adaptiveThreshold(gray, binary, 255,
    cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 11, 2);

// Morphological ops
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, {3, 3});
cv::Mat dilated, eroded;
cv::dilate(binary, dilated, kernel);
cv::erode(binary, eroded, kernel);

SIMD Pixel Processing (Without OpenCV)

cpp
#include <immintrin.h>  // AVX2

// Invert 32 bytes (32 pixels of grayscale) per iteration
void invert_gray(uint8_t* data, size_t n) {
    const __m256i all_ones = _mm256_set1_epi8(static_cast<char>(255));

    size_t i = 0;
    for (; i + 32 <= n; i += 32) {
        __m256i pixels = _mm256_loadu_si256((__m256i*)(data + i));
        __m256i result = _mm256_xor_si256(pixels, all_ones);
        _mm256_storeu_si256((__m256i*)(data + i), result);
    }
    for (; i < n; ++i) data[i] ^= 255;  // tail
}

stb_image (Header-Only, No Dependencies)

cpp
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"

// Load
int w, h, channels;
uint8_t* data = stbi_load("photo.jpg", &w, &h, &channels, 0);
if (!data) throw std::runtime_error(stbi_failure_reason());

// Process
// data is w * h * channels bytes, row-major

// Save
stbi_write_png("output.png", w, h, channels, data, w * channels);

stbi_image_free(data);

Parallel Processing with OpenCV

cpp
// OpenCV uses TBB/OpenMP internally for many operations
// Force parallel execution:
cv::parallel_for_(cv::Range(0, img.rows), [&](const cv::Range& range) {
    for (int y = range.start; y < range.end; ++y) {
        uint8_t* row = img.ptr<uint8_t>(y);
        for (int x = 0; x < img.cols * 3; ++x)
            row[x] = 255 - row[x];
    }
});

// Set thread count
cv::setNumThreads(4);

Video Processing

cpp
#include <opencv2/opencv.hpp>

// Capture from file or camera (index 0 = default webcam)
cv::VideoCapture cap("video.mp4");
if (!cap.isOpened()) throw std::runtime_error("cannot open video");

cv::Mat frame, gray, edges;
while (cap.read(frame)) {
    cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
    cv::Canny(gray, edges, 50, 150);
    cv::imshow("edges", edges);
    if (cv::waitKey(30) == 'q') break;
}

// Write processed video
cv::VideoWriter writer(
    "output.mp4",
    cv::VideoWriter::fourcc('m','p','4','v'),
    cap.get(cv::CAP_PROP_FPS),
    {(int)cap.get(cv::CAP_PROP_FRAME_WIDTH),
     (int)cap.get(cv::CAP_PROP_FRAME_HEIGHT)}
);
// writer.write(processed_frame);

Deep Learning Inference — ONNX Runtime

ONNX Runtime runs models trained in PyTorch/TensorFlow without Python, at near-native speed. One API covers CPU, CUDA, TensorRT, and CoreML.

cpp
#include <onnxruntime/core/providers/cpu/cpu_provider_factory.h>
#include <onnxruntime_cxx_api.h>
#include <opencv2/opencv.hpp>

// Load model once (expensive)
Ort::Env env{ORT_LOGGING_LEVEL_WARNING, "image_classifier"};
Ort::SessionOptions opts;
opts.SetIntraOpNumThreads(4);
opts.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);

Ort::Session session{env, L"resnet50.onnx", opts};

// Preprocess: resize + normalize + NHWC → NCHW
cv::Mat img = cv::imread("cat.jpg");
cv::resize(img, img, {224, 224});
img.convertTo(img, CV_32F, 1.0 / 255.0);

// Subtract ImageNet mean, divide by std
cv::subtract(img, cv::Scalar(0.485, 0.456, 0.406), img);
cv::divide(img, cv::Scalar(0.229, 0.224, 0.225), img);

// HWC → CHW (OpenCV stores as HWC, ONNX expects CHW)
cv::Mat blob = cv::dnn::blobFromImage(img);  // returns NCHW float32

// Run inference
std::array<int64_t, 4> shape{1, 3, 224, 224};
Ort::MemoryInfo mem = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
    mem, (float*)blob.data, 3*224*224, shape.data(), 4);

const char* input_name  = "input";
const char* output_name = "output";
auto outputs = session.Run(Ort::RunOptions{}, &input_name, &input_tensor, 1,
                           &output_name, 1);

// Get top-1 class
float* scores = outputs[0].GetTensorMutableData<float>();
int top1 = std::max_element(scores, scores + 1000) - scores;

OpenCV alternative: cv::dnn::readNetFromONNX for simpler inference without the ONNX Runtime dependency — but less control over backends.


Feature Detection and Matching

cpp
#include <opencv2/features2d.hpp>

cv::Mat img1 = cv::imread("scene.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat img2 = cv::imread("object.jpg", cv::IMREAD_GRAYSCALE);

// ORB — fast, patent-free keypoint detector + descriptor
auto orb = cv::ORB::create(500);

std::vector<cv::KeyPoint> kp1, kp2;
cv::Mat desc1, desc2;
orb->detectAndCompute(img1, cv::noArray(), kp1, desc1);
orb->detectAndCompute(img2, cv::noArray(), kp2, desc2);

// BFMatcher — brute force matching with Hamming distance (for binary descriptors)
cv::BFMatcher matcher(cv::NORM_HAMMING, /*crossCheck=*/true);
std::vector<cv::DMatch> matches;
matcher.match(desc1, desc2, matches);

// Filter by distance
std::sort(matches.begin(), matches.end());
std::vector<cv::DMatch> good(matches.begin(),
    matches.begin() + std::min<int>(50, matches.size()));

// Draw matches
cv::Mat result;
cv::drawMatches(img1, kp1, img2, kp2, good, result);
cv::imwrite("matches.jpg", result);

Performance Patterns

ROI (Region of Interest) — process a crop without copying:

cpp
cv::Rect roi{100, 100, 200, 200};
cv::Mat crop = img(roi);   // no copy — shares pixels with img
cv::GaussianBlur(crop, crop, {5, 5}, 1.5);   // modifies img in-place

Avoid repeated allocation in loops:

cpp
cv::Mat gray, blurred;  // declared OUTSIDE loop — reuse buffers
while (cap.read(frame)) {
    cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);   // reuses gray
    cv::GaussianBlur(gray, blurred, {5, 5}, 1.5);   // reuses blurred
}

UMat for GPU (transparent OpenCL acceleration):

cpp
cv::UMat uimg;
img.copyTo(uimg);              // upload to GPU once
cv::GaussianBlur(uimg, uimg, {5, 5}, 1.5);  // runs on GPU if available
cv::UMat result;
cv::Canny(uimg, result, 50, 150);
result.copyTo(output);         // download result

CMake Setup

cmake
find_package(OpenCV REQUIRED)
target_link_libraries(myapp PRIVATE ${OpenCV_LIBS})
target_include_directories(myapp PRIVATE ${OpenCV_INCLUDE_DIRS})

# stb_image (header-only)
target_include_directories(myapp PRIVATE third_party/stb)

# ONNX Runtime (if used)
find_package(onnxruntime REQUIRED)
target_link_libraries(myapp PRIVATE onnxruntime::onnxruntime)

SIMD-Optimized Pixel Operations — AVX2

AVX2 processes 32 bytes (32 grayscale pixels, or 8 RGBA pixels) in a single instruction, giving 8–16x throughput over scalar loops on modern x86 CPUs. The key discipline is aligned allocation, handling the tail, and keeping the hot loop free of branches.

cpp
#include <immintrin.h>
#include <cstdint>
#include <cstddef>

// Brighten a grayscale image by adding a constant, saturating at 255
// Processes 32 pixels per iteration with AVX2
void brighten_gray_avx2(uint8_t* __restrict__ dst,
                         const uint8_t* __restrict__ src,
                         size_t n, uint8_t amount)
{
    const __m256i delta = _mm256_set1_epi8(static_cast<char>(amount));

    size_t i = 0;
    for (; i + 32 <= n; i += 32) {
        __m256i pixels = _mm256_loadu_si256((const __m256i*)(src + i));
        // _mm256_adds_epu8 saturates at 255 — no overflow
        __m256i bright  = _mm256_adds_epu8(pixels, delta);
        _mm256_storeu_si256((__m256i*)(dst + i), bright);
    }
    // Scalar tail for remaining pixels
    for (; i < n; ++i)
        dst[i] = static_cast<uint8_t>(std::min<int>(src[i] + amount, 255));
}

// Alpha-blend two RGBA images: out = src*alpha + dst*(1-alpha)
// Processes 8 RGBA pixels (32 bytes) per iteration
void alpha_blend_avx2(uint8_t* dst, const uint8_t* src,
                       size_t pixel_count, uint8_t alpha)
{
    const __m256i a  = _mm256_set1_epi16(alpha);
    const __m256i ia = _mm256_set1_epi16(255 - alpha);

    size_t i = 0;
    for (; i + 8 <= pixel_count; i += 8) {
        // Unpack 8-bit to 16-bit for multiply
        __m128i s8 = _mm_loadu_si128((const __m128i*)(src + i * 4));
        __m128i d8 = _mm_loadu_si128((const __m128i*)(dst + i * 4));
        __m256i s16 = _mm256_cvtepu8_epi16(s8);
        __m256i d16 = _mm256_cvtepu8_epi16(d8);

        __m256i blended = _mm256_srli_epi16(
            _mm256_add_epi16(
                _mm256_mullo_epi16(s16, a),
                _mm256_mullo_epi16(d16, ia)), 8);

        __m128i result = _mm256_cvtepi16_epi8(blended);   // narrow back to 8-bit
        _mm_storeu_si128((__m128i*)(dst + i * 4), result);
    }
}

Check CPU support at runtime with __builtin_cpu_supports("avx2") and fall back to a scalar path for portability.


Color Space Conversions

Correct color math requires working in linear light; sRGB is gamma-encoded and must be decoded before blending or filtering, then re-encoded for display. YCbCr is widely used in JPEG/H.264 because the luma channel (Y) compresses better than RGB.

cpp
#include <cmath>
#include <array>
#include <cstdint>

// --- sRGB ↔ Linear (via LUT for speed) ---

// Build a 256-entry LUT: sRGB byte → linear float
std::array<float, 256> build_srgb_to_linear_lut() {
    std::array<float, 256> lut;
    for (int i = 0; i < 256; ++i) {
        float c = i / 255.0f;
        lut[i] = (c <= 0.04045f)
                 ? c / 12.92f
                 : std::pow((c + 0.055f) / 1.055f, 2.4f);
    }
    return lut;
}

// Build inverse LUT: linear float → sRGB byte (1024-entry, 0..1 range)
uint8_t linear_to_srgb(float c) {
    c = std::clamp(c, 0.0f, 1.0f);
    float encoded = (c <= 0.0031308f)
                    ? 12.92f * c
                    : 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f;
    return static_cast<uint8_t>(std::round(encoded * 255.0f));
}

// --- BT.601 RGB ↔ YCbCr (JPEG range: Y=[0,255], Cb/Cr=[0,255]) ---
struct YCbCr { uint8_t Y, Cb, Cr; };

YCbCr rgb_to_ycbcr(uint8_t R, uint8_t G, uint8_t B) {
    float r = R, g = G, b = B;
    return {
        static_cast<uint8_t>(std::clamp(  0.299f*r + 0.587f*g + 0.114f*b,         0.f, 255.f)),
        static_cast<uint8_t>(std::clamp(128.f - 0.168736f*r - 0.331264f*g + 0.5f*b, 0.f, 255.f)),
        static_cast<uint8_t>(std::clamp(128.f + 0.5f*r - 0.418688f*g - 0.081312f*b, 0.f, 255.f)),
    };
}

// OpenCV color conversion (preferred in practice)
cv::Mat bgr, ycrcb;
cv::cvtColor(bgr, ycrcb, cv::COLOR_BGR2YCrCb);   // split luma/chroma
cv::cvtColor(ycrcb, bgr, cv::COLOR_YCrCb2BGR);   // back to display
ConversionWhen to use
sRGB → linearBefore blending, filtering, or any math
linear → sRGBBefore storing to PNG/JPG or sending to display
RGB → YCbCrJPEG encoding, chroma subsampling, luma-based algorithms
RGB → HSVHue-based segmentation, color pickers

Image Codecs — libjpeg-turbo, libpng, libwebp, libtiff

Each codec is optimised for a different trade-off between compression ratio, lossiness, metadata richness, and decoding speed. libjpeg-turbo uses SIMD to decode JPEG 2–6x faster than the reference IJG implementation.

cpp
// libjpeg-turbo — fast JPEG encode/decode
#include <turbojpeg.h>

std::vector<uint8_t> encode_jpeg(const uint8_t* rgb, int w, int h, int quality) {
    tjhandle compressor = tjInitCompress();
    unsigned char* buf  = nullptr;
    unsigned long  size = 0;
    tjCompress2(compressor, rgb, w, 0, h, TJPF_RGB,
                &buf, &size, TJSAMP_420, quality, TJFLAG_FASTDCT);
    std::vector<uint8_t> result(buf, buf + size);
    tjFree(buf);
    tjDestroy(compressor);
    return result;
}

// libpng — lossless with metadata/alpha
#include <png.h>
void write_png(const char* filename, const uint8_t* rgba, int w, int h) {
    FILE* fp = fopen(filename, "wb");
    png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
                                              nullptr, nullptr, nullptr);
    png_infop info  = png_create_info_struct(png);
    png_init_io(png, fp);
    png_set_IHDR(png, info, w, h, 8, PNG_COLOR_TYPE_RGBA,
                 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
                 PNG_FILTER_TYPE_DEFAULT);
    png_write_info(png, info);
    for (int y = 0; y < h; ++y)
        png_write_row(png, rgba + y * w * 4);
    png_write_end(png, nullptr);
    png_destroy_write_struct(&png, &info);
    fclose(fp);
}
CodecLossyAlphaHDR/16-bitBest for
libjpeg-turboYesNoNoPhotos, thumbnails
libpngNoYesYes (16-bit)Screenshots, sprites
libwebpBothYesNoWeb images, small size
libtiffBothYesYes (32-bit float)Scientific, medical, print

Histogram Equalization and CLAHE

Histogram equalization redistributes pixel intensities so contrast is spread across the full range; CLAHE (Contrast-Limited Adaptive Histogram Equalization) does this locally per tile to avoid over-amplifying noise in uniform regions. CLAHE is the standard pre-processing step before OCR and medical imaging algorithms.

cpp
#include <opencv2/opencv.hpp>

// Global histogram equalization (fast, but amplifies noise in bright/dark regions)
cv::Mat gray = cv::imread("low_contrast.jpg", cv::IMREAD_GRAYSCALE);
cv::Mat equalized;
cv::equalizeHist(gray, equalized);

// CLAHE — preferred for natural images
// clipLimit controls noise amplification; tileGridSize sets tile dimensions
auto clahe = cv::createCLAHE(/*clipLimit=*/2.0, /*tileGridSize=*/{8, 8});
cv::Mat clahe_result;
clahe->apply(gray, clahe_result);

// Apply CLAHE to color images via the L channel of LAB
cv::Mat lab;
cv::cvtColor(cv::imread("photo.jpg"), lab, cv::COLOR_BGR2Lab);

std::vector<cv::Mat> channels;
cv::split(lab, channels);           // channels[0] = L (lightness)
clahe->apply(channels[0], channels[0]);
cv::merge(channels, lab);
cv::Mat result;
cv::cvtColor(lab, result, cv::COLOR_Lab2BGR);

// Manual histogram compute (useful outside OpenCV)
std::array<int, 256> hist{};
for (int y = 0; y < gray.rows; ++y) {
    const uint8_t* row = gray.ptr<uint8_t>(y);
    for (int x = 0; x < gray.cols; ++x)
        ++hist[row[x]];
}
// Build CDF for equalization mapping
std::array<int, 256> cdf{};
cdf[0] = hist[0];
for (int i = 1; i < 256; ++i) cdf[i] = cdf[i-1] + hist[i];
int total = gray.rows * gray.cols;
std::array<uint8_t, 256> lut;
for (int i = 0; i < 256; ++i)
    lut[i] = static_cast<uint8_t>(255.0 * cdf[i] / total);

Morphological Operations

Morphological operations process binary or grayscale images based on a structuring element (kernel shape): erosion shrinks bright regions, dilation expands them, opening removes noise, closing fills holes. They are the standard tool for blob cleanup after thresholding.

cpp
#include <opencv2/opencv.hpp>

cv::Mat binary;  // result of thresholding
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, {5, 5});

// Erosion — shrinks white blobs, removes thin protrusions
cv::Mat eroded;
cv::erode(binary, eroded, kernel, {-1,-1}, /*iterations=*/2);

// Dilation — grows white blobs, fills small holes
cv::Mat dilated;
cv::dilate(binary, dilated, kernel);

// Opening = erode then dilate — removes small white noise
cv::Mat opened;
cv::morphologyEx(binary, opened, cv::MORPH_OPEN, kernel);

// Closing = dilate then erode — fills small holes inside white regions
cv::Mat closed;
cv::morphologyEx(binary, closed, cv::MORPH_CLOSE, kernel);

// Morphological gradient — highlights edges (dilate - erode)
cv::Mat gradient;
cv::morphologyEx(binary, gradient, cv::MORPH_GRADIENT, kernel);

// Top-hat transform — extracts small bright regions from uneven background
cv::Mat tophat;
cv::morphologyEx(binary, tophat, cv::MORPH_TOPHAT, kernel);

Connected Components Labeling and Blob Analysis

Connected components labeling assigns a unique integer label to each isolated white region in a binary image, enabling per-blob measurements like area, bounding box, and centroid. This is the core step for counting objects, OCR character segmentation, and particle analysis.

cpp
#include <opencv2/opencv.hpp>

cv::Mat binary;  // threshold result, CV_8U

// Two-pass labeling via OpenCV (returns label image + statistics per component)
cv::Mat labels, stats, centroids;
int n_labels = cv::connectedComponentsWithStats(
    binary, labels, stats, centroids,
    /*connectivity=*/8, CV_32S);

// stats rows: [label][CC_STAT_LEFT, TOP, WIDTH, HEIGHT, AREA]
for (int i = 1; i < n_labels; ++i) {  // skip 0 = background
    int  area   = stats.at<int>(i, cv::CC_STAT_AREA);
    int  left   = stats.at<int>(i, cv::CC_STAT_LEFT);
    int  top    = stats.at<int>(i, cv::CC_STAT_TOP);
    int  width  = stats.at<int>(i, cv::CC_STAT_WIDTH);
    int  height = stats.at<int>(i, cv::CC_STAT_HEIGHT);
    double cx   = centroids.at<double>(i, 0);
    double cy   = centroids.at<double>(i, 1);

    // Filter small blobs by area
    if (area < 50) continue;

    std::println("blob {}: area={} bbox={}x{}@({},{})", i, area, width, height, left, top);
    cv::rectangle(binary, {left, top, width, height}, 128, 2);
}

// Alternative: cv::findContours for shape analysis
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
for (auto& c : contours) {
    double area      = cv::contourArea(c);
    cv::Rect bb      = cv::boundingRect(c);
    double perimeter = cv::arcLength(c, true);
    double circularity = 4 * CV_PI * area / (perimeter * perimeter);
    if (circularity > 0.8) std::println("circular blob, area={}", area);
}

Affine and Perspective Transforms

Affine transforms (rotation, scale, shear, translation) preserve parallelism; perspective transforms model 3D-to-2D projection and are used for document rectification, lane detection, and homography-based stitching. Both are represented as matrices applied via cv::warpAffine or cv::warpPerspective.

cpp
#include <opencv2/opencv.hpp>

// --- Affine transform: rotate 45° around image center ---
cv::Mat img = cv::imread("input.jpg");
cv::Point2f center{img.cols / 2.0f, img.rows / 2.0f};
cv::Mat M = cv::getRotationMatrix2D(center, /*angle=*/45.0, /*scale=*/1.0);
cv::Mat rotated;
cv::warpAffine(img, rotated, M, img.size(),
               cv::INTER_LINEAR, cv::BORDER_REFLECT);

// --- Perspective transform: rectify a document ---
// Four corners of the document in the original image
std::vector<cv::Point2f> src_pts = {
    {72, 39}, {619, 50}, {631, 863}, {61, 862}
};
// Target rectangle in the output
std::vector<cv::Point2f> dst_pts = {
    {0, 0}, {600, 0}, {600, 800}, {0, 800}
};
cv::Mat H = cv::getPerspectiveTransform(src_pts, dst_pts);
cv::Mat rectified;
cv::warpPerspective(img, rectified, H, {600, 800});

// --- Estimate homography from matched keypoints (RANSAC) ---
// kp1, kp2: matched cv::Point2f from feature matching
std::vector<cv::Point2f> kp1, kp2;  // filled by feature matching
cv::Mat H_ransac = cv::findHomography(kp1, kp2, cv::RANSAC, /*reproj_thresh=*/3.0);
if (!H_ransac.empty()) {
    cv::Mat warped;
    cv::warpPerspective(img, warped, H_ransac, img.size());
}

GPU Image Processing — CUDA NPP and Vulkan Compute

For real-time video or batch processing of thousands of images, GPU pipelines reduce latency 10–100x over CPU paths. NVIDIA Performance Primitives (NPP) wrap CUDA with image-processing primitives that mirror OpenCV's API; Vulkan compute shaders offer vendor-neutral GPU access.

cpp
// --- CUDA NPP (NVIDIA only) ---
// Requires CUDA toolkit and libnppi
#include <nppi.h>
#include <cuda_runtime.h>

int w = 1920, h = 1080, step;
Npp8u* d_src = nppiMalloc_8u_C3(w, h, &step);   // device memory
Npp8u* d_dst = nppiMalloc_8u_C3(w, h, &step);

// Upload
cudaMemcpy2D(d_src, step, host_rgb, w*3, w*3, h, cudaMemcpyHostToDevice);

// Gaussian blur on GPU
NppiSize roi{w, h};
NppiSize mask{5, 5};
NppiPoint anchor{2, 2};
nppiFilterGauss_8u_C3R(d_src, step, d_dst, step, roi, NPP_MASK_SIZE_5_X_5);

// Download result
cudaMemcpy2D(host_dst, w*3, d_dst, step, w*3, h, cudaMemcpyDeviceToHost);

nppiFree(d_src);
nppiFree(d_dst);
glsl
// Vulkan compute shader — invert grayscale (GLSL 450)
#version 450
layout (local_size_x = 16, local_size_y = 16) in;
layout (binding = 0, r8ui) uniform readonly  uimage2D input_img;
layout (binding = 1, r8ui) uniform writeonly uimage2D output_img;

void main() {
    ivec2 coord = ivec2(gl_GlobalInvocationID.xy);
    uint pixel  = imageLoad(input_img, coord).r;
    imageStore(output_img, coord, uvec4(255u - pixel));
}
BackendVendorAPI complexityUse case
CUDA NPPNVIDIALow (mirrors OpenCV)Production CV pipelines
OpenCV CUDA moduleNVIDIAVery lowOpenCV users adding GPU
OpenCL / cv::UMatAllMediumVendor-neutral
Vulkan computeAllHighCustom kernels, mobile