Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FlatCV

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

Documentation

All examples in this documentation are executed during testing. So you can be confident that they work as described!

Usage

FlatCV can either be used via its CLI or as a C library.

CLI

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
InputOutput
$ ./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

Library

#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);

Image Flipping

Horizontal Flip (flip_x)

InputOutput
$ ./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'

Vertical Flip (flip_y)

InputOutput
$ ./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'

Both Flips Combined (180° Rotation)

InputOutput
$ ./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'

Crop

InputOutput
$ ./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'

Resize

FlatCV uses area-based sampling for shrinking images and bilinear interpolation for enlarging images. This ensures a good balance between performance and quality.

Percentage Resize (Half Size)

InputOutput
$ ./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'

Mixed Percentage Resize

InputOutput
$ ./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'

Absolute Size Resize

InputOutput
$ ./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'

Resize Combined with Other Operations

InputOutput
$ ./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'

Gaussian Blur

InputOutput
$ ./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

Draw

Circle

Single Red Circle

InputOutput
$ ./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'

Single Blue Circle

InputOutput
$ ./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'

Multiple Colored Circles

InputOutput
$ ./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'

Circle with Grayscale Conversion

InputOutput
$ ./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'

Circle at Image Boundary

InputOutput
$ ./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'

Disk

Single Red Disk

InputOutput
$ ./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'

Single Blue Disk

InputOutput
$ ./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'

Multiple Colored Disks

InputOutput
$ ./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'

Disk with Grayscale Conversion

InputOutput
$ ./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'

Disk at Image Boundary

InputOutput
$ ./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'

Circle and Disk Combined

InputOutput
$ ./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'

Grayscale Conversion

InputOutput
$ ./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

Binarize

Convert images to black and white.

Process:

  1. Convert the image to grayscale.
  2. Remove low frequencies (e.g. shadows)
  3. Apply threshold with OTSU's method

Smart Black & White

InputOutput
$ ./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

Smart and Smooth (Anti-Aliased) Black & White

InputOutput
$ ./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

Corner Detection

Detect Corners

$ ./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)

Draw Corners

InputOutput
$ ./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'

Segmentation

Watershed

Watershed with 2 Markers for 1 White Ring

InputOutput

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'

Watershed with 2 Markers for 2 White Rings

InputOutput

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'

Watershed with 2 Markers for a Receipt

InputOutput

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'

Watershed with 3 Markers

InputOutput

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'

Document Extraction

Extract Document with Auto-Sizing

InputOutput
$ ./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'

Extract Document to Specific Dimensions

InputOutput
$ ./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'

Benchmark

===== ▶️ 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

FAQ

Why is this written in C?

  • 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.

Will you rewrite this in another language?

I am open to rewrite this in Rust or Zig in the future, once the advantages of C become less relevant.