Skip to content

Corg-Labs/filters

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 

Image Filters in C

A command-line PPM image filter tool that applies grayscale, invert, blur, sharpen, Sobel edge detection, sepia, and brightness adjustments to P6 binary PPM files — with a live progress bar on stderr.

Written in pure C with no external dependencies. Part of the Corg-Labs collection.


Features

  • --grayscale — luminance-weighted grayscale conversion
  • --invert — colour inversion (255 - channel)
  • --blur — 3x3 box blur
  • --sharpen — unsharp-mask sharpening kernel
  • --edge — Sobel operator edge detection
  • --sepia — warm sepia tone transformation
  • --brightness N — additive brightness adjustment (-100 to +100)
  • Progress bar on stderr for every filter
  • Edge-clamping for convolution at image borders
  • Reads and writes standard P6 (binary) PPM files

Tutorial

1. The Pixel and Image Structures

Each pixel holds three unsigned char channels. The image stores its dimensions and a flat row-major data array.

typedef struct { unsigned char r, g, b; } Pixel;

typedef struct {
    int    width;
    int    height;
    int    maxval;
    Pixel *data;     /* data[y * width + x] */
} Image;

A clamp_u8 helper prevents overflow when arithmetic pushes channel values outside [0, 255]:

static inline unsigned char clamp_u8(int v) {
    if (v < 0)   return 0;
    if (v > 255) return 255;
    return (unsigned char)v;
}

2. Reading a PPM File

PPM P6 files have a plain-text header followed by raw binary pixel data. The reader skips whitespace and # comment lines between header fields, then bulk-reads all pixels with a single fread.

/* Header: "P6\n<width> <height>\n<maxval>\n" then raw bytes */
if (fread(magic, 1, 2, fp) != 2 || strncmp(magic, "P6", 2) != 0)
    return NULL;   /* not a P6 file */

fscanf(fp, "%d", &img->width);
fscanf(fp, "%d", &img->height);
fscanf(fp, "%d", &img->maxval);
fgetc(fp);   /* consume the single whitespace byte after maxval */

size_t npix = (size_t)img->width * img->height;
img->data   = malloc(npix * sizeof(Pixel));
fread(img->data, 3, npix, fp);

Writing is the mirror: fprintf the header, fwrite the pixel array.

3. Grayscale — Luminance Weighting

Human eyes are most sensitive to green and least sensitive to blue. The standard ITU-R BT.709 coefficients weight each channel accordingly.

static void filter_grayscale(Image *img) {
    int n = img->width * img->height;
    for (int i = 0; i < n; i++) {
        unsigned char g = clamp_u8((int)(
            0.2126 * img->data[i].r +
            0.7152 * img->data[i].g +
            0.0722 * img->data[i].b));
        img->data[i].r = img->data[i].g = img->data[i].b = g;
    }
}

A naive average ((r+g+b)/3) gives technically correct results but produces a flat, less perceptually accurate grey.

4. Convolution Kernels

Blur, sharpen, and edge detection all work by convolution: for each pixel, multiply each neighbour's channels by the corresponding kernel weight and sum. The result replaces the centre pixel.

The box blur kernel weights every neighbour equally (1/9 each):

static void filter_blur(Image *img) {
    Image *out = alloc_image(img->width, img->height, img->maxval);
    int w = img->width, h = img->height;
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            int sr = 0, sg = 0, sb = 0;
            for (int dy = -1; dy <= 1; dy++) {
                for (int dx = -1; dx <= 1; dx++) {
                    Pixel p = get_pixel_clamped(img, x+dx, y+dy);
                    sr += p.r; sg += p.g; sb += p.b;
                }
            }
            out->data[y*w+x].r = clamp_u8(sr / 9);
            /* ... g, b similarly ... */
        }
    }
    memcpy(img->data, out->data, (size_t)w * h * sizeof(Pixel));
    free_image(out);
}

The sharpening kernel is the unsharp-mask: emphasise the centre (weight 5), subtract the four orthogonal neighbours (weight -1):

static const int K[3][3] = {{ 0,-1, 0},
                              {-1, 5,-1},
                              { 0,-1, 0}};

5. Edge-Clamping for Border Pixels

When the convolution window extends outside the image at the edges, get_pixel_clamped clamps the coordinates to the nearest valid pixel. This avoids reading out-of-bounds memory and produces natural-looking borders.

static inline Pixel get_pixel_clamped(const Image *img, int x, int y) {
    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (x >= img->width)  x = img->width  - 1;
    if (y >= img->height) y = img->height - 1;
    return img->data[y * img->width + x];
}

6. Sobel Operator for Edge Detection

The Sobel operator uses two 3x3 kernels — Gx for horizontal gradients and Gy for vertical gradients. The edge strength at each pixel is sqrt(Gx^2 + Gy^2).

static const int Gx[3][3] = {{-1, 0, 1},
                               {-2, 0, 2},
                               {-1, 0, 1}};
static const int Gy[3][3] = {{-1,-2,-1},
                               { 0, 0, 0},
                               { 1, 2, 1}};

/* For each pixel: */
int gxr = 0, gyr = 0;   /* ... and for g, b channels */
for (int dy = -1; dy <= 1; dy++)
    for (int dx = -1; dx <= 1; dx++) {
        Pixel p = get_pixel_clamped(img, x+dx, y+dy);
        gxr += Gx[dy+1][dx+1] * p.r;
        gyr += Gy[dy+1][dx+1] * p.r;
    }
out->data[y*w+x].r = clamp_u8((int)sqrt((double)(gxr*gxr + gyr*gyr)));

High-contrast borders produce large gradient magnitudes (bright), flat regions produce near-zero values (dark).

7. Sepia and Brightness

Sepia applies a fixed 3x3 colour-matrix transform that maps RGB to a warm reddish-brown:

img->data[i].r = clamp_u8((int)(r*0.393 + g*0.769 + b*0.189));
img->data[i].g = clamp_u8((int)(r*0.349 + g*0.686 + b*0.168));
img->data[i].b = clamp_u8((int)(r*0.272 + g*0.534 + b*0.131));

Brightness is a simple additive delta (user provides -100..100, which is scaled to -255..255):

img->data[i].r = clamp_u8(img->data[i].r + delta);

Build

gcc imgfilter.c -o imgfilter -lm

Run

./imgfilter in.ppm out.ppm --grayscale
./imgfilter in.ppm out.ppm --blur
./imgfilter in.ppm out.ppm --edge
./imgfilter in.ppm out.ppm --sharpen
./imgfilter in.ppm out.ppm --sepia
./imgfilter in.ppm out.ppm --invert
./imgfilter in.ppm out.ppm --brightness 30
./imgfilter in.ppm out.ppm --brightness -50

To convert a JPEG/PNG to PPM first: convert photo.jpg photo.ppm (requires ImageMagick).


Concepts Practiced

  • PPM P6 binary file format: header parsing and bulk pixel I/O
  • Flat row-major pixel array (y * width + x indexing)
  • Convolution with arbitrary 3x3 kernels (blur, sharpen, Sobel)
  • Luminance-weighted greyscale conversion (BT.709 coefficients)
  • Edge-clamping as a simple boundary condition strategy
  • Sobel gradient magnitude for edge detection
  • clamp_u8 to prevent channel overflow in arithmetic filters
  • In-place vs. double-buffered filter application

Dependencies

Standard C libraries only: stdio.h, stdlib.h, string.h, math.h, ctype.h

About

►A command-line PPM image filter tool

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages