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

Image processing and computer vision library in pure C.

Check out the sidebar on the left for all available features.

Characteristics

  • 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
  • Available as an amalgamation where all code is combined into one file.
    (Each release includes a flatcv.h and flatcv.c file.)
  • Simple code
    • Only uses functions from the C standard library.
    • Minimal usage of macros and preprocessor
    • No multithreading
      Yet, it's still plenty fast and, for many operations, faster than ImageMagick. (See benchmark below.) For batch processing, make sure to process one file per core.
    • No GPU acceleration
    • No fusing of image transformations

Playground

You can try out FlatCV in your browser using the FlatCV Playground. It uses WebAssembly to run the image processing code in your browser.

Code

You can find the ISC licensed source code on GitHub.

Examples

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'

Image Trimming

The trim operation removes border pixels that have the same color, repeatedly until no more uniform borders can be removed.

InputOutput

Basic Trim Operation

For this test, we'll create a simple image with a uniform border and demonstrate the trim functionality:

$ ./flatcv imgs/blob_with_border.png trim imgs/blob_with_border_trim.png
Loaded image: 256x256 with 3 channels
Executing pipeline with 1 operations:
Applying operation: trim
  → Completed in \d+.\d+ ms \(output: \d+x\d+\) (regex)
Final output dimensions: \d+x\d+ (regex)
Successfully saved processed image to 'imgs/blob_with_border_trim.png'

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

Histogram Visualization

Generate a brightness histogram from an image where the x-axis represents brightness values (0-255) and the y-axis shows the number of pixels with that brightness level.

For grayscale images, displays a white histogram on black background. For RGB(A) images, displays overlapping red, green, and blue histograms.

InputOutput
$ ./flatcv imgs/parrot.jpeg histogram imgs/parrot_histogram.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: histogram
  → Completed in \d+.\d+ ms \(output: 256x200\) (regex)
Final output dimensions: 256x200
Successfully saved processed image to 'imgs/parrot_histogram.png'
$ git diff --quiet imgs/parrot_histogram.png

Grayscale Histogram

InputOutput
$ ./flatcv imgs/parrot_grayscale.jpeg histogram imgs/parrot_histogram_grayscale.png
Loaded image: 512x384 with 3 channels
Executing pipeline with 1 operations:
Applying operation: histogram
  → Completed in \d+.\d+ ms \(output: 256x200\) (regex)
Final output dimensions: 256x200
Successfully saved processed image to 'imgs/parrot_histogram_grayscale.png'
$ git diff --quiet imgs/parrot_histogram_grayscale.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
  → Completed in \d+.\d+ ms \(output: 1024x1024\) (regex)
  {
    "corners": {
      "top_left": [332, 68],
      "top_right": [692, 76],
      "bottom_right": [720, 956],
      "bottom_left": [352, 960]
    }
  }

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
  → Completed in \d+.\d+ ms \(output: 1024x1024\) (regex)
Final output dimensions: 1024x1024
Successfully saved processed image to 'imgs/receipt_corners.png'
  Detected corners:
    Top-left:     (332, 68)
    Top-right:    (692, 76)
    Bottom-right: (720, 956)
    Bottom-left:  (352, 960)

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'
InputOutput
$ ./flatcv imgs/receipt2.jpeg extract_document imgs/receipt2_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/receipt2_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

Use the benchmark script to run the benchmark yourself.

===== ▶️ Rotate =====
vips rot imgs/parrot_hq.jpeg tmp/rotate_vips.jpeg d90 ran
1.12 ± 0.08 times faster than gm convert imgs/parrot_hq.jpeg -rotate 90 tmp/rotate_gm.jpeg
2.30 ± 0.15 times faster than magick convert imgs/parrot_hq.jpeg -rotate 90 tmp/rotate_magick.jpeg

===== ▶️ Resize =====
vips thumbnail imgs/parrot_hq.jpeg tmp/resize_vips.jpeg 256 --height 256 --size force ran
1.27 ± 0.10 times faster than ./flatcv imgs/parrot_hq.jpeg resize 256x256 tmp/resize_flatcv.jpeg
1.50 ± 0.12 times faster than gm convert imgs/parrot_hq.jpeg -resize 256x256! tmp/resize_gm.jpeg
2.72 ± 0.21 times faster than magick convert imgs/parrot_hq.jpeg -resize 256x256! tmp/resize_magick.jpeg

===== ▶️ Resize 50% =====
vips resize imgs/parrot_hq.jpeg tmp/resize_50_vips.jpeg 0.5 ran
1.39 ± 0.05 times faster than gm convert imgs/parrot_hq.jpeg -resize 50% tmp/resize_50_gm.jpeg
1.60 ± 0.05 times faster than ./flatcv imgs/parrot_hq.jpeg resize 50% tmp/resize_50_flatcv.jpeg
2.71 ± 0.10 times faster than magick convert imgs/parrot_hq.jpeg -resize 50% tmp/resize_50_magick.jpeg

===== ▶️ Crop =====
vips crop imgs/parrot_hq.jpeg tmp/crop_vips.jpeg 1200 1200 500 500 ran
1.10 ± 0.17 times faster than gm convert imgs/parrot_hq.jpeg -crop 500x500+1200+1200 tmp/crop_gm.jpeg
1.40 ± 0.09 times faster than magick convert imgs/parrot_hq.jpeg -crop 500x500+1200+1200 tmp/crop_magick.jpeg
1.43 ± 0.09 times faster than ./flatcv imgs/parrot_hq.jpeg crop 500x500+1200+1200 tmp/crop_flatcv.jpeg

===== ▶️ Grayscale =====
vips colourspace imgs/village.jpeg tmp/gray_vips.jpeg b-w ran
2.07 ± 0.11 times faster than gm convert imgs/village.jpeg -colorspace Gray tmp/gray_gm.jpeg
2.79 ± 0.25 times faster than magick convert imgs/village.jpeg -colorspace Gray tmp/gray_magick.jpeg
4.31 ± 0.28 times faster than ./flatcv imgs/village.jpeg grayscale tmp/gray_flatcv.jpeg

===== ▶️ Blur =====
vips gaussblur imgs/page_hq.png tmp/blur_vips.jpeg 7 ran
1.51 ± 0.04 times faster than gm convert imgs/page_hq.png -blur 21x7 tmp/blur_gm.jpeg
1.96 ± 0.07 times faster than ./flatcv imgs/page_hq.png blur 21 tmp/blur_flatcv.jpeg
3.61 ± 0.09 times faster than magick convert imgs/page_hq.png -blur 21x7 tmp/blur_magick.jpeg

===== ▶️ Sobel =====
./flatcv imgs/page_hq.png sobel tmp/sobel_flatcv.jpeg ran
1.02 ± 0.07 times faster than vips sobel imgs/page_hq.png tmp/sobel_vips.jpeg
3.99 ± 0.29 times faster than magick convert imgs/page_hq.png -define convolve:scale="!" \
        -define morphology:compose=Lighten \
        -morphology Convolve "Sobel:>" tmp/sobel_magick.jpeg

===== ▶️ Histogram RGB =====
./flatcv imgs/parrot_hq.jpeg histogram tmp/histogram_rgb_flatcv.jpeg ran
1.13 ± 0.03 times faster than magick convert imgs/parrot_hq.jpeg histogram:tmp/histogram_rgb_magick.jpeg

===== ▶️ Histogram Gray =====
./flatcv imgs/parrot_grayscale.jpeg histogram tmp/histogram_gray_flatcv.jpeg ran
14.88 ± 2.93 times faster than magick convert imgs/parrot_grayscale.jpeg histogram:tmp/histogram_gray_magick.jpeg

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.

Changelog

2025-08-23 - 0.2.0

  • Add new image manipulation features
    • Resize
    • Crop
    • Trim
    • Flip x and flip y
    • Draw circles and disks
    • Sobel edge detection
    • Foerstner corner detection
      • Draw corners
    • Watershed segmentation
      • Extract document
  • Host documentation site at flatcv.ad-si.com
    • Includes an interactive playground page with wasm build of FlatCV
  • Add a benchmark comparing it with GM and ImageMagick
  • Rename all functions to start with fcv_
  • Print warning if binarized image is saved as JPEG
  • Use ISC license
  • Log run time for each step in image processing pipeline
  • Fix blur function
FlatCV Playground
<main>
  <h1>FlatCV Playground</h1>

  <hr />

  <div class="image-container">
    <div class="image-box">
      <h3>Original Image</h3>
      <canvas id="originalCanvas"></canvas>
      <button id="loadImageBtn">
        Load New Image<div class="spinner" id="loadSpinner"></div>
      </button>
    </div>
    <div class="image-box">
      <h3>Processed Image</h3>
      <canvas id="processedCanvas"></canvas>
    </div>
  </div>

  <div class="controls">
    <input type="file" id="fileInput" accept="image/*">
    <button class="filter-btn" id="grayscaleBtn" disabled>
      Grayscale
      <div class="spinner" id="grayscaleSpinner"></div>
    </button>
    <button class="filter-btn" id="blurBtn" disabled>
      Blur
      <div class="spinner" id="blurSpinner"></div>
    </button>
    <button class="filter-btn" id="sobelBtn" disabled>
      Edge Detection
      <div class="spinner" id="sobelSpinner"></div>
    </button>
    <button class="filter-btn" id="binaryBtn" disabled>
      Binary Threshold
      <div class="spinner" id="binarySpinner"></div>
    </button>
  </div>
</main>