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
#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
// 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
// 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)
#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)
#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
// 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
#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.
#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
#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:
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-placeAvoid repeated allocation in loops:
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):
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 resultCMake Setup
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.
#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.
#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| Conversion | When to use |
|---|---|
| sRGB → linear | Before blending, filtering, or any math |
| linear → sRGB | Before storing to PNG/JPG or sending to display |
| RGB → YCbCr | JPEG encoding, chroma subsampling, luma-based algorithms |
| RGB → HSV | Hue-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.
// 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);
}| Codec | Lossy | Alpha | HDR/16-bit | Best for |
|---|---|---|---|---|
| libjpeg-turbo | Yes | No | No | Photos, thumbnails |
| libpng | No | Yes | Yes (16-bit) | Screenshots, sprites |
| libwebp | Both | Yes | No | Web images, small size |
| libtiff | Both | Yes | Yes (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.
#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.
#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.
#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.
#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.
// --- 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);// 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));
}| Backend | Vendor | API complexity | Use case |
|---|---|---|---|
| CUDA NPP | NVIDIA | Low (mirrors OpenCV) | Production CV pipelines |
| OpenCV CUDA module | NVIDIA | Very low | OpenCV users adding GPU |
| OpenCL / cv::UMat | All | Medium | Vendor-neutral |
| Vulkan compute | All | High | Custom kernels, mobile |