πŸ‡―πŸ‡΅ ζ—₯本θͺž | πŸ‡ΊπŸ‡Έ English
Advertisement

Image Processing in C

Roll your own PPM/PGM I/O, grayscale conversion, convolution filters, and edge detection β€” with minimal external libraries.

Representing images

A digital image is a 2D array of pixel intensities:
See the multi-dim array deep dive β€” C's row-major layout works out nicely for image buffers.

Read/write PPM & PGM (no library needed)

PGM (Portable Graymap) and PPM (Portable Pixmap) have tiny headers followed by raw pixel data β€” perfect for learning.

PGM header format

P5 # magic number (P5 binary, P2 ASCII)
4 3 # width height
255 # maximum value
<width Γ— height pixel bytes>

Write

#include <stdio.h>

int save_pgm(const char *path, int w, int h, const unsigned char *data) {
    FILE *fp = fopen(path, "wb");
    if (!fp) return -1;
    fprintf(fp, "P5\n%d %d\n255\n", w, h);
    fwrite(data, 1, (size_t)w * h, fp);
    fclose(fp);
    return 0;
}

Read

unsigned char *load_pgm(const char *path, int *w, int *h) {
    FILE *fp = fopen(path, "rb");
    if (!fp) return NULL;
    char magic[3];
    int maxval;
    if (fscanf(fp, "%2s %d %d %d", magic, w, h, &maxval) != 4) {
        fclose(fp); return NULL;
    }
    fgetc(fp);   // skip the single newline after the header
    size_t n = (size_t)(*w) * (*h);
    unsigned char *data = malloc(n);
    fread(data, 1, n, fp);
    fclose(fp);
    return data;
}

PPM (color, 3 bytes per pixel)

fprintf(fp, "P6\n%d %d\n255\n", w, h);
fwrite(rgb_data, 3, (size_t)w * h, fp);   // RGB order
How to get test images: on macOS/Linux use ImageMagick (convert input.png output.pgm), or export as PGM/PPM from GIMP or Preview.app.

Grayscale / invert / threshold

RGB β†’ grayscale

Perceptual luminance: Y = 0.299 R + 0.587 G + 0.114 B (ITU-R BT.601).
void to_gray(int w, int h,
             const unsigned char *rgb,
             unsigned char *gray) {
    for (int i = 0; i < w * h; i++) {
        int r = rgb[3*i + 0];
        int g = rgb[3*i + 1];
        int b = rgb[3*i + 2];
        gray[i] = (unsigned char)(0.299 * r + 0.587 * g + 0.114 * b);
    }
}

Invert

void invert(int w, int h, unsigned char *img) {
    for (int i = 0; i < w * h; i++)
        img[i] = 255 - img[i];
}

Threshold (binarize)

void binarize(int w, int h, unsigned char *img, int threshold) {
    for (int i = 0; i < w * h; i++)
        img[i] = (img[i] >= threshold) ? 255 : 0;
}
Picking the threshold: a fixed 128 is often fine, but Otsu's method chooses it automatically using the histogram.

Convolution (blur / sharpen)

A convolution multiplies each pixel by a kernel (weight grid) together with its neighbors and sums them up. This is the foundation of image filters.

Box blur (3Γ—3 mean)

1/9
1/9
1/9
1/9
1/9
1/9
1/9
1/9
1/9

Sharpen

0
-1
0
-1
5
-1
0
-1
0

Generic 3Γ—3 convolution

void convolve3(int w, int h,
               const unsigned char *src,
               unsigned char *dst,
               const double k[3][3]) {
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            double sum = 0;
            for (int dy = -1; dy <= 1; dy++) {
                for (int dx = -1; dx <= 1; dx++) {
                    int ny = y + dy, nx = x + dx;
                    if (ny < 0) ny = 0;
                    if (ny >= h) ny = h - 1;
                    if (nx < 0) nx = 0;
                    if (nx >= w) nx = w - 1;
                    sum += src[ny * w + nx] * k[dy + 1][dx + 1];
                }
            }
            if (sum < 0) sum = 0;
            if (sum > 255) sum = 255;
            dst[y * w + x] = (unsigned char)sum;
        }
    }
}

double blur[3][3] = {
    {1/9.0, 1/9.0, 1/9.0},
    {1/9.0, 1/9.0, 1/9.0},
    {1/9.0, 1/9.0, 1/9.0}
};
convolve3(w, h, src, dst, blur);
Don't do it in-place: writing to the same buffer you read from corrupts the result. Keep src and dst separate.

Sobel edge detection

Compute horizontal and vertical gradients with two kernels and take their magnitude.

Sobel Gx / Gy kernels

Gx:
-1
0
1
-2
0
2
-1
0
1
Gy:
-1
-2
-1
0
0
0
1
2
1
#include <math.h>

void sobel(int w, int h,
           const unsigned char *src,
           unsigned char *dst) {
    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 (int y = 1; y < h - 1; y++) {
        for (int x = 1; x < w - 1; x++) {
            int sx = 0, sy = 0;
            for (int dy = -1; dy <= 1; dy++) {
                for (int dx = -1; dx <= 1; dx++) {
                    int v = src[(y + dy) * w + (x + dx)];
                    sx += v * Gx[dy + 1][dx + 1];
                    sy += v * Gy[dy + 1][dx + 1];
                }
            }
            double mag = sqrt((double)sx * sx + (double)sy * sy);
            if (mag > 255) mag = 255;
            dst[y * w + x] = (unsigned char)mag;
        }
    }
}
Compile: gcc -O2 sobel.c -o sobel -lm. Run on a PGM and open the output in any image viewer to see only the edges light up.

Histogram & contrast stretch

How many pixels take each intensity. Useful for contrast, thresholding, and analysis.
void histogram(int w, int h,
               const unsigned char *img,
               int hist[256]) {
    for (int i = 0; i < 256; i++) hist[i] = 0;
    for (int i = 0; i < w * h; i++) hist[img[i]]++;
}

void print_histogram(const int hist[256]) {
    int maxv = 0;
    for (int i = 0; i < 256; i++) if (hist[i] > maxv) maxv = hist[i];
    for (int i = 0; i < 256; i += 4) {
        int sum = 0;
        for (int k = 0; k < 4; k++) sum += hist[i + k];
        int bars = maxv ? sum * 40 / maxv : 0;
        printf("%3d |", i);
        for (int b = 0; b < bars; b++) putchar('#');
        putchar('\n');
    }
}

Linear contrast stretch

void stretch_contrast(int w, int h, unsigned char *img) {
    int lo = 255, hi = 0;
    for (int i = 0; i < w * h; i++) {
        if (img[i] < lo) lo = img[i];
        if (img[i] > hi) hi = img[i];
    }
    if (hi == lo) return;
    for (int i = 0; i < w * h; i++) {
        img[i] = (unsigned char)((img[i] - lo) * 255 / (hi - lo));
    }
}

stb_image for PNG/JPG

The easiest way to read PNG/JPG/BMP/GIF in C is stb_image β€” a single-file, public-domain, header-only library.

Download

$ curl -O https://raw.githubusercontent.com/nothings/stb/master/stb_image.h $ curl -O https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h

Minimal example: PNG β†’ PGM

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    int w, h, ch;
    unsigned char *data = stbi_load(argv[1], &w, &h, &ch, 1);
    // last arg = 1: force single-channel grayscale
    if (!data) { fprintf(stderr, "load failed\n"); return 1; }

    FILE *fp = fopen("out.pgm", "wb");
    fprintf(fp, "P5\n%d %d\n255\n", w, h);
    fwrite(data, 1, (size_t)w * h, fp);
    fclose(fp);

    stbi_image_free(data);
    return 0;
}
Writing PNG: include stb_image_write.h and call stbi_write_png("out.png", w, h, 1, data, w);.

Challenges

Challenge 1: Generate a checkerboard PGM
Produce a 400Γ—400 black-and-white checkerboard image (50-pixel squares) and save it as PGM.
Challenge 2: Grayscale CLI tool
Build ./togray input.ppm output.pgm: read P6 PPM, convert via luminance formula, write PGM.
Challenge 3: Gaussian blur
Define a 5Γ—5 Gaussian kernel and implement a smoother blur than box blur (weights decay from center).
Challenge 4: Mosaic
Average each NΓ—N block and paint every pixel in that block with the average. Compare N = 1, 4, 16, 32.
Challenge 5: Otsu's method
Compute the optimal binarization threshold automatically by maximizing between-class variance on the histogram.

See also