Simple image processing and computer vision library in pure C.
"Simple" means:
- Color images are a flat array of 8-bit per channel RGBA row-major top-to-bottom
- Grayscale images are a flat array of 8-bit GRAY row-major top-to-bottom
- All operations are done on the raw sRGB pixel values
- Minimal usage of macros and preprocessor
- Available as an amalgamation where all code is combined into one file.
(Each release
includes a flatcv.h
and flatcv.c
file.)
- No fusing of image transformations
- No multithreading
You're more likely to process one file per core
than one file over multiple cores anyways.
Yet, it's still often faster than GraphicsMagick with multiple threads.
(See benchmark below.)
- No GPU acceleration
- Only uses functions from the C standard library.
You can find the code on GitHub
All examples in this documentation are executed during testing.
So you can be confident that they work as described!
FlatCV can either be used via its CLI or as a C library.
The CLI supports edit pipelines which sequentially apply all transformations.
flatcv <input> <comma-separated-edit-pipeline> <output>
As commas are not special characters in shells,
you can write the edit pipeline without quotes.
Both variants yield the same result:
flatcv i.jpg 'grayscale, blur 9' o.jpg
flatcv i.jpg grayscale, blur 9 o.jpg
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg grayscale, blur 9 imgs/parrot_grayscale_blur.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 2 operations:
Applying operation: grayscale
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: blur with parameter: 9.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_grayscale_blur.jpeg'
$ git diff --quiet imgs/parrot_grayscale_blur.jpeg
#include "flatcv.h"
// Resize an image to half size
unsigned char const * half_size = resize(
input_width, input_height,
0.5, 0.5,
&out_width, &out_height,
input_data
);
// Do something with the resized image
// Free the allocated memory
free(half_size);
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg flip_x imgs/parrot_flip_x.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: flip_x
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_flip_x.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg flip_y imgs/parrot_flip_y.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: flip_y
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_flip_y.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg flip_x, flip_y imgs/parrot_flip_both.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 2 operations:
Applying operation: flip_x
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: flip_y
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_flip_both.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg crop 200x150+100+100 imgs/parrot_crop.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: crop with parameter: 100.00 100.00 200.00 150.00
→ Completed in \d+.\d+ ms \(output: 200x150\) (regex)
Final output dimensions: 200x150
Successfully saved processed image to 'imgs/parrot_crop.jpeg'
FlatCV uses area-based sampling for shrinking images
and bilinear interpolation for enlarging images.
This ensures a good balance between performance and quality.
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg resize 50% imgs/parrot_resize_50_percent.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: resize with parameter: 50%
→ Completed in \d+.\d+ ms \(output: 256x192\) (regex)
Final output dimensions: 256x192
Successfully saved processed image to 'imgs/parrot_resize_50_percent.jpeg'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg resize 50%x150% imgs/parrot_resize_50x150_percent.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: resize with parameter: 50%x150%
→ Completed in \d+.\d+ ms \(output: 256x576\) (regex)
Final output dimensions: 256x576
Successfully saved processed image to 'imgs/parrot_resize_50x150_percent.jpeg'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg resize 800x400 imgs/parrot_resize_800x400.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: resize with parameter: 800x400
→ Completed in \d+.\d+ ms \(output: 800x400\) (regex)
Final output dimensions: 800x400
Successfully saved processed image to 'imgs/parrot_resize_800x400.jpeg'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg grayscale, resize 50%, blur 2 imgs/parrot_gray_resize_blur.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 3 operations:
Applying operation: grayscale
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: resize with parameter: 50%
→ Completed in \d+.\d+ ms \(output: 256x192\) (regex)
Applying operation: blur with parameter: 2.00
→ Completed in \d+.\d+ ms \(output: 256x192\) (regex)
Final output dimensions: 256x192
Successfully saved processed image to 'imgs/parrot_gray_resize_blur.jpeg'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg blur 9 imgs/parrot_blur.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: blur with parameter: 9.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_blur.jpeg'
$ git diff --quiet imgs/parrot_blur.jpeg
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg circle FF0000 50 200x150 imgs/parrot_circle_red.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: circle with parameter: FF0000 200.00 150.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_circle_red.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg circle 0000FF 30 100x100 imgs/parrot_circle_blue.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: circle with parameter: 0000FF 100.00 100.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_circle_blue.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg circle 00FF00 25 400x200, circle FFFF00 35 100x200 imgs/parrot_circles_multi.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 2 operations:
Applying operation: circle with parameter: 00FF00 400.00 200.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: circle with parameter: FFFF00 100.00 200.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_circles_multi.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg grayscale, circle FFFFFF 40 250x250 imgs/parrot_gray_circle.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 2 operations:
Applying operation: grayscale
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: circle with parameter: FFFFFF 250.00 250.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_gray_circle.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg circle 00FFFF 80 10x10 imgs/parrot_circle_boundary.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: circle with parameter: 00FFFF 10.00 10.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_circle_boundary.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg disk FF0000 50 200x150 imgs/parrot_disk_red.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: disk with parameter: FF0000 200.00 150.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_disk_red.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg disk 0000FF 30 100x100 imgs/parrot_disk_blue.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: disk with parameter: 0000FF 100.00 100.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_disk_blue.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg disk 00FF00 25 400x200, disk FFFF00 35 100x200 imgs/parrot_disks_multi.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 2 operations:
Applying operation: disk with parameter: 00FF00 400.00 200.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: disk with parameter: FFFF00 100.00 200.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_disks_multi.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg grayscale, disk FFFFFF 40 250x250 imgs/parrot_gray_disk.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 2 operations:
Applying operation: grayscale
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: disk with parameter: FFFFFF 250.00 250.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_gray_disk.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg disk 00FFFF 80 10x10 imgs/parrot_disk_boundary.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: disk with parameter: 00FFFF 10.00 10.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_disk_boundary.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg disk FF0000 60 250x250, circle FFFFFF 65 250x250 imgs/parrot_circle_disk_combo.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 2 operations:
Applying operation: disk with parameter: FF0000 250.00 250.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Applying operation: circle with parameter: FFFFFF 250.00 250.00
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_circle_disk_combo.png'
Input | Output |
 |  |
$ ./flatcv imgs/parrot.jpeg grayscale imgs/parrot_grayscale.jpeg
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: grayscale
→ Completed in \d+.\d+ ms \(output: 512x384\) (regex)
Final output dimensions: 512x384
Successfully saved processed image to 'imgs/parrot_grayscale.jpeg'
$ git diff --quiet imgs/parrot_grayscale.jpeg
Convert images to black and white.
Process:
- Convert the image to grayscale.
- Remove low frequencies (e.g. shadows)
- Apply threshold with OTSU's method
Input | Output |
 |  |
$ ./flatcv imgs/page.png bw_smart imgs/page_bw_smart.png
Loaded image: 384x256 with 1 channels
Executing pipeline with 1 operations:
Applying operation: bw_smart
→ Completed in \d+.\d+ ms \(output: 384x256\) (regex)
Final output dimensions: 384x256
Successfully saved processed image to 'imgs/page_bw_smart.png'
$ git diff --quiet imgs/page_bw_smart.png
Input | Output |
 |  |
$ ./flatcv imgs/page.png bw_smooth imgs/page_bw_smooth.png
Loaded image: 384x256 with 1 channels
Executing pipeline with 1 operations:
Applying operation: bw_smooth
→ Completed in \d+.\d+ ms \(output: 384x256\) (regex)
Final output dimensions: 384x256
Successfully saved processed image to 'imgs/page_bw_smooth.png'
$ git diff --quiet imgs/page_bw_smooth.png
$ ./flatcv imgs/receipt.jpeg detect_corners
Loaded image: 1024x1024 with 3 channels
Executing pipeline with 1 operations:
Applying operation: detect_corners
{
"corners": {
"top_left": [332, 68],
"top_right": [692, 76],
"bottom_right": [720, 956],
"bottom_left": [352, 960]
}
}
→ Completed in \d+.\d+ ms \(output: 1024x1024\) (regex)
Input | Output |
 |  |
$ ./flatcv imgs/receipt.jpeg draw_corners imgs/receipt_corners.png
Loaded image: 1024x1024 with 3 channels
Executing pipeline with 1 operations:
Applying operation: draw_corners
Detected corners:
Top-left: (332, 68)
Top-right: (692, 76)
Bottom-right: (720, 956)
Bottom-left: (352, 960)
→ Completed in \d+.\d+ ms \(output: 1024x1024\) (regex)
Final output dimensions: 1024x1024
Successfully saved processed image to 'imgs/receipt_corners.png'
Input | Output |
 |  |
The image is a white ring in the center of a black background.
Running the following watershed segmentation command segments the image into 2 regions:
- Disk in the center with a radius that goes to the middle of the rings edge
- The rest of the image
$ ./flatcv imgs/elevation_2_basins_1_ring.png watershed 0x0 150x100 imgs/elevation_2_basins_1_ring_watershed.png
Loaded image: 300x200 with 1 channels
Executing pipeline with 1 operations:
Applying operation: watershed with parameter: 0x0 150x100
→ Completed in \d+.\d+ ms \(output: 300x200\) (regex)
Final output dimensions: 300x200
Successfully saved processed image to 'imgs/elevation_2_basins_1_ring_watershed.png'
Input | Output |
 |  |
The image contains 2 white rings in the center of a black background.
Running the following watershed segmentation command segments the image into 2 regions:
- Disk in the center with a radius that goes to the middle of the black gap between the rings
- The rest of the image
$ ./flatcv imgs/elevation_2_basins_2_rings.png watershed 0x0 150x100 imgs/elevation_2_basins_2_rings_watershed.png
Loaded image: 300x200 with 1 channels
Executing pipeline with 1 operations:
Applying operation: watershed with parameter: 0x0 150x100
→ Completed in \d+.\d+ ms \(output: 300x200\) (regex)
Final output dimensions: 300x200
Successfully saved processed image to 'imgs/elevation_2_basins_2_rings_watershed.png'
Input | Output |
 |  |
The image contains the elevation map of a photo of a receipt.
Running the following watershed segmentation command segments the image into 2 regions:
- The receipt itself
- The background
$ ./flatcv imgs/elevation_2_basins_receipt.png watershed 0x0 128x128 imgs/elevation_2_basins_receipt_watershed.png
Loaded image: 256x256 with 1 channels
Executing pipeline with 1 operations:
Applying operation: watershed with parameter: 0x0 128x128
→ Completed in \d+.\d+ ms \(output: 256x256\) (regex)
Final output dimensions: 256x256
Successfully saved processed image to 'imgs/elevation_2_basins_receipt_watershed.png'
Input | Output |
 |  |
The image is a horizontal gradient from white to black and back to white.
There is also a white disk with black edge in the center.
Running the following watershed segmentation command segments the image into three regions:
- Left side
- Center
- Right side
$ ./flatcv imgs/elevation_3_basins_gradient.png watershed 0x0 150x100 299x0 imgs/elevation_3_basins_gradient_watershed.png
Loaded image: 300x200 with 1 channels
Executing pipeline with 1 operations:
Applying operation: watershed with parameter: 0x0 150x100 299x0
→ Completed in \d+.\d+ ms \(output: 300x200\) (regex)
Final output dimensions: 300x200
Successfully saved processed image to 'imgs/elevation_3_basins_gradient_watershed.png'
Input | Output |
 |  |
$ ./flatcv imgs/receipt.jpeg extract_document imgs/receipt_extracted_auto.jpeg
Loaded image: 1024x1024 with 3 channels
Executing pipeline with 1 operations:
Applying operation: extract_document
→ Completed in \d+.\d+ ms \(output: \d+x\d+\) (regex)
Final output dimensions: \d+x\d+ (regex)
Successfully saved processed image to 'imgs/receipt_extracted_auto.jpeg'
Input | Output |
 |  |
$ ./flatcv imgs/receipt.jpeg extract_document_to 200x400 imgs/receipt_extracted.jpeg
Loaded image: 1024x1024 with 3 channels
Executing pipeline with 1 operations:
Applying operation: extract_document_to with parameter: 200.00 400.00
→ Completed in \d+.\d+ ms \(output: 200x400\) (regex)
Final output dimensions: 200x400
Successfully saved processed image to 'imgs/receipt_extracted.jpeg'
===== ▶️ Resize =====
./flatcv imgs/parrot_hq.jpeg resize 256x256 tmp/resize_flatcv.png ran
1.42 ± 0.01 times faster than gm convert imgs/parrot_hq.jpeg -resize 256x256! tmp/resize_gm.png
2.39 ± 0.05 times faster than magick convert imgs/parrot_hq.jpeg -resize 256x256! tmp/resize_magick.png
===== ▶️ Crop =====
gm convert imgs/parrot_hq.jpeg -crop 100x100+200+200 tmp/crop_gm.png ran
1.30 ± 0.04 times faster than magick convert imgs/parrot_hq.jpeg -crop 100x100+200+200 tmp/crop_magick.png
1.30 ± 0.04 times faster than ./flatcv imgs/parrot_hq.jpeg crop 100 100 100 100 tmp/crop_flatcv.png
===== ▶️ Grayscale =====
gm convert imgs/village.jpeg -colorspace Gray tmp/gray_gm.png ran
1.76 ± 0.02 times faster than ./flatcv imgs/village.jpeg grayscale tmp/gray_flatcv.png
3.83 ± 0.04 times faster than magick convert imgs/village.jpeg -colorspace Gray tmp/gray_magick.png
===== ▶️ Blur =====
./flatcv imgs/page_hq.png blur 21 tmp/blur_flatcv.png ran
1.66 ± 0.01 times faster than gm convert imgs/page_hq.png -blur 21x7 tmp/blur_gm.png
2.20 ± 0.01 times faster than magick convert imgs/page_hq.png -blur 21x7 tmp/blur_magick.png
===== ▶️ Sobel =====
./flatcv imgs/page_hq.png sobel tmp/sobel_flatcv.png ran
3.07 ± 0.05 times faster than magick convert imgs/page_hq.png -define convolve:scale="!" \
-define morphology:compose=Lighten \
-morphology Convolve "Sobel:>" tmp/sobel_magick.png
-
Most portable language
Almost every other language has support to integrate C code
in one way or another.
Especially since it's available a single file
that can be vendored into your project.
-
Lot of existing code
C and C++ are the most widely used languages for image processing.
So there is a lot of existing code that can be reused and adapted.
-
Great for AI powered development
As there is so much training data available,
LLMs are especially good at writing C code.
I am open to rewrite this in Rust
or Zig in the future,
once the advantages of C become less relevant.