This project is a minimal C library and CLI for learning and experimenting with image processing. It works with the PPM (Portable Pixmap) format, making it easy to manipulate raw RGB pixel data and build your own filters.
This project works with the PPM (Portable Pixmap) image format, which is simple and human-readable. There are two main variants:
- P3: ASCII (plain text) format. Each pixel's RGB values are written as numbers in text. Easy to debug, but large and slow to load.
- P6: Binary format. Each pixel's RGB values are stored as raw bytes. Much smaller and faster.
PPM File Structure:
P6
<width> <height>
<max_val>
<binary RGB data>
- The header contains the magic number (
P3
orP6
), image dimensions, and the maximum color value (usually 255 or 65535). - For P3, pixel data is ASCII numbers; for P6, it's binary bytes.
Converting JPEG/PNG to PPM using ImageMagick:
# For P3 (ASCII, uncompressed):
magick cat.jpeg -compress none cat.ppm
# For P6 (binary, default):
magick cat.jpeg cat.ppm
P3 is useful for debugging, but P6 is recommended for performance and file size.
This chapter introduces the core image-handling code used to load, manipulate, and save P6 PPM images. The code is designed for clarity and learning, not for speed or advanced features.
typedef struct {
uint16_t r, g, b;
} pixel_t;
typedef struct {
unsigned int width;
unsigned int height;
unsigned int max_val;
pixel_t **pixels;
} image_t;
pixel_t
: Represents a single RGB pixel, with up to 16 bits per channel.image_t
: Represents the entire image, including dimensions, max value, and a 2D array of pixels.
load_image()
: Opens a binary PPM file (P6
), parses the header, and reads pixel data into a heap-allocated 2D array. Usesmmap()
for efficient file access. Supports both 8-bit and 16-bit per channel images (max_val > 255
⇒ 2 bytes per channel).copy_image()
: Creates a deep copy of the image structure and pixel data.save_image()
: Writes the image to a file in PPM P6 format, handling both 8-bit and 16-bit output.free_image()
: Frees all dynamically allocated memory associated with the image.
image_t *img = load_image("cat.ppm");
// ... manipulate pixels ...
save_image("cat_out.ppm", img);
free_image(img);
The max_val
field in the PPM header determines the pixel value range:
≤ 255
: Each color component is 1 byte.> 255
: Each component is stored in 2 bytes (high bit-depth).
These files provide color conversion utilities and helpers for color-based effects.
typedef struct {
uint16_t r, g, b;
} rgb_t;
typedef struct {
float h, s, v;
} hsv_t;
hsv_to_rgb(hsv_t hsv)
: Converts HSV to RGB.rgb_to_hsv(uint16_t r, uint16_t g, uint16_t b)
: Converts RGB to HSV.rgb_to_luminance(uint16_t r, uint16_t g, uint16_t b)
: Computes perceived brightness (used for grayscale).
These are used for filters like grayscale, hue shift, and more.
This module implements a variety of pixel-wise image filters. All operate directly on the pixel data, modifying the image in-place.
- Brightness/Contrast: Adjusts overall lightness or contrast.
- Inversion: Inverts all colors.
- Grayscale: Converts to grayscale using luminance.
- Hue Shift: Rotates hue in HSV space.
- Sepia: Applies a warm, old-photo effect.
- Vintage: Blends original and grayscale, tweaks color balance.
- Vignette: Darkens corners for a spotlight effect.
- Posterize: Reduces the number of color levels.
brightness(img, 1.2f); // Increase brightness by 20%
contrast(img, 0.8f); // Reduce contrast
inversion(img); // Invert colors
This module implements classic convolution-based effects using 3x3 kernels. These filters consider each pixel and its neighbors, enabling effects like blurring and edge detection.
- Blur: Softens the image by averaging neighboring pixels (repeated for stronger effect).
- Smooth: Weighted blur for gentle smoothing.
- Sharpen: Enhances edges and details.
- Emboss: Creates a 3D relief effect.
- Edge Detection: Uses the Sobel operator to highlight edges (converts to grayscale first).
Each filter defines a 3x3 kernel (matrix of weights). The kernel is applied to each pixel and its neighbors, computing a weighted sum for each color channel. Results are clamped to valid pixel values.
- GCC or Clang (C compiler)
make
- ImageMagick (for converting images to PPM)
make # Build the project
./main # Run the CLI (see main.c for usage)
- Input images must be in PPM format (see Chapter 0).
- See
main.c
for CLI usage and filter examples.
Thanks to this YouTube video for teaching the basics of the PPM format and parsing in C.
Project: omniflare/imagick