High-performance computer vision primitives in pure C99. Designed for edge deployment, deterministic memory control, and clean FFI consumption from Rust, Python, Go, and Zig.
Zero hidden costs. Every allocation goes through a caller-provided allocator. Every function returns an explicit error code. No global state, no exceptions, no RTTI.
Composable, not monolithic. CVCL provides a kernel library of CV primitives, not a framework. It sits below OpenCV, not beside it.
SIMD-ready layout. Row stride is always 64-byte aligned. This is not aesthetic, it means your AVX-512 / NEON / SVE inner loops never straddle a cache line boundary.
#include <cvcl/cvcl.h>
int main(void) {
// Load an image
cvcl_image_t img;
cvcl_io_read_ppm(&img, "photo.ppm", NULL);
// Gaussian blur
cvcl_image_t blurred;
cvcl_image_create(&blurred, img.width, img.height,
img.channels, CVCL_DEPTH_U8, NULL);
cvcl_blur_gaussian(&blurred, &img, 9, 2.0f, CVCL_BORDER_REPLICATE);
// Save
cvcl_io_write_ppm(&blurred, "out.ppm");
// Explicit cleanup
cvcl_image_free(&img, NULL);
cvcl_image_free(&blurred, NULL);
return 0;
}- CMake >= 3.16
- C99 compiler (GCC, Clang, MSVC, IAR, ARMCC)
- math library (
-lm)
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build buildcmake -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCVCL_BUILD_TESTS=ON \
-DCVCL_SANITIZE=ON
cmake --build build
ctest --test-dir build --output-on-failurecmake -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-arm-none-eabi.cmake \
-DCVCL_NO_SIMD=OFF \
-DCVCL_BUILD_TESTS=OFFtypedef struct {
uint8_t *data; // Raw pixel buffer (64-byte aligned rows)
int32_t width;
int32_t height;
int32_t channels; // 1=gray, 3=RGB, 4=RGBA
int32_t stride; // Bytes per row (>= width*channels*depth)
cvcl_depth_t depth; // U8, U16, or F32
cvcl_layout_t layout; // Interleaved or planar
} cvcl_image_t;The stride field decouples logical width from memory layout. This enables:
- Zero-copy crops and ROI views
- Camera / DMA buffer wrapping
- SIMD alignment without padding the public dimensions
// Drop-in arena allocator example
static uint8_t arena[1 << 20];
static size_t arena_pos = 0;
void *arena_alloc(size_t n, void *ctx) {
(void)ctx;
void *p = &arena[arena_pos];
arena_pos = CVCL_ALIGN_UP(arena_pos + n, 16);
return p;
}
void arena_free(void *p, void *ctx) { (void)p; (void)ctx; }
cvcl_allocator_t my_alloc = { arena_alloc, arena_free, NULL };
// Use it everywhere
cvcl_image_create(&img, 640, 480, 3, CVCL_DEPTH_U8, &my_alloc);
cvcl_image_free(&img, &my_alloc);cvcl_rect_t roi = {100, 80, 320, 240};
cvcl_image_t view;
cvcl_crop(&view, &src, roi);
// view.data points into src.data — no allocation, no copy
// DO NOT call cvcl_image_free on view| Module | Function | Status |
|---|---|---|
| Core | Image create/free/clone | Done |
| Zero-copy ROI view | Done | |
| Custom allocator interface | Done | |
| I/O | PPM/PGM read/write | Done |
| In-memory PPM encode | Done | |
| PNG/JPEG (via stb_image) | Done | |
| Transform | Nearest-neighbor resize | Done |
| Bilinear resize (U8) | Done | |
| Bicubic resize | Planned | |
| Flip H/V | Done | |
| Affine transform | Planned | |
| Rotate 90/180/270 | Planned | |
| RGB/Gray channel convert | Done | |
| Filter | Generic 2D convolution | Done |
| Separable convolution | Done | |
| Box blur | Done | |
| Gaussian blur | Done | |
| Median filter | Planned | |
| Sobel X/Y | Done | |
| Canny edges | Done | |
| Erode / Dilate / Open / Close | Done | |
| Pixel | U8 / U16 / F32 accessors | Done |
| Otsu threshold | Done | |
| Histogram equalization | Planned | |
| Draw | Lines, rects, circles | Planned |
| Bitmap text | Planned | |
| SIMD | AVX2 fast paths | Planned |
| NEON fast paths | Planned |
All functions return cvcl_result_t. Errors propagate explicitly:
cvcl_result_t rc = cvcl_blur_gaussian(&dst, &src, 9, 2.0f, CVCL_BORDER_REPLICATE);
if (rc != CVCL_OK) {
fprintf(stderr, "Error: %s\n", cvcl_strerror(rc));
}
// Or use the propagation macro inside library code:
CVCL_CHECK(cvcl_image_create(&tmp, w, h, ch, depth, alloc));| Flag | Effect |
|---|---|
CVCL_NO_SIMD |
Disable all SIMD intrinsics |
CVCL_NO_STDLIB |
Freestanding: disable stdlib includes |
CVCL_WITH_STB |
Enable PNG/JPEG I/O via stb_image |
CVCL_ASSERT_DISABLE |
Strip all internal assertions |
CVCL is designed to be a clean FFI target. The C API is stable and has no C++-only constructs.
Rust (via bindgen):
// cvcl-sys generated bindings
let mut img = cvcl_image_t { ..Default::default() };
unsafe { cvcl_image_create(&mut img, 640, 480, 3, CVCL_DEPTH_U8, std::ptr::null()); }Python (via ctypes):
lib = ctypes.CDLL("libcvcl.a")
# ... bind cvcl_image_create, cvcl_io_read_ppm, etc.cvcl/
├── include/cvcl/
│ ├── cvcl.h
│ ├── cvcl_types.h
│ ├── cvcl_image.h
│ ├── cvcl_alloc.h
│ ├── cvcl_error.h
│ ├── cvcl_io.h
│ ├── cvcl_transform.h
│ ├── cvcl_filter.h
│ ├── cvcl_draw.h
│ ├── cvcl_pixel.h
│ └── cvcl_version.h
├── src/
│ ├── core/
│ │ ├── alloc.c # Pluggable allocator (malloc/free default)
│ │ ├── error.c # Error code to string mapping
│ │ ├── image.c # Image lifecycle: create, free, clone, view
│ │ └── pixel_hist.c # Histogram, Otsu, threshold, equalization, fill
│ ├── io/
│ │ ├── io_ppm.c # Native PPM/PGM read/write (zero deps)
│ │ └── io_stb.c # PNG/JPEG via stb_image (CVCL_WITH_STB)
│ ├── transform/
│ │ └── resize.c # Resize, flip, crop, channel convert
│ └── filter/
│ ├── blur.c # Gaussian (Q15 fixed-point), box (sliding window), SIMD add/sub/blend
│ ├── convolve.c # Generic 2D convolution, separable (interior/border split)
│ ├── edge.c # Sobel (integer), Canny (no atan2f, iterative hysteresis)
│ └── morph.c # Erode/dilate/open/close (monotonic deque O(n))
├── tests/
│ ├── CMakeLists.txt
│ ├── test_image.c
│ ├── test_io.c
│ ├── test_transform.c
│ ├── test_filter.c
│ └── test_morph.c
├── examples/
│ ├── CMakeLists.txt
│ └── demo_pipeline.c # Load → grayscale → blur → Canny → save
├── bench/
│ ├── CMakeLists.txt
│ └── bench_blur.c # Throughput benchmark (Mpix/s)
├── third_party/
│ └── README.md # stb_image download instructions
├── cmake/
│ └── cvclConfig.cmake.in
└── CMakeLists.txt
| Operation | Throughput | Notes |
|---|---|---|
| Gaussian blur k=5 | 47 Mpix/s | Fixed-point Q15 separable |
| Gaussian blur k=31 | 12 Mpix/s | Grows with k (separable) |
| Box blur k=5 | 169 Mpix/s | O(1) sliding window |
| Box blur k=63 | 158 Mpix/s | Same cost as k=5 |
| Pixel-wise add | 2924 Mpix/s | SSE2 saturating adds |
| Erode/Dilate 5x5 | 157 Mpix/s | Monotonic deque O(n) |
Key design wins:
- Box blur k=63 costs the same as k=5 -- sliding window is O(w·h) regardless of kernel size
- Gaussian uses fixed-point Q15 arithmetic -- no float multiply in the inner loop
- Morphology uses monotonic deque -- O(w·h) vs O(w·h·k) separable naive
- Fork and branch from
main - Run tests:
ctest --test-dir build --output-on-failure - Ensure no new warnings with
-Wall -Wextra -Wpedantic -Werror - Add a test for any new function
- Open a PR with a clear description of what changed and why
Apache License 2.0 - see LICENSE.