Image Transformations Using Pure C++

Working with images often requires just a few simple transformations, such as cropping and rotating. While libraries like opencv provides these features, sometimes a lightweight solution may be needed. This post demonstrates how to implement these operations in pure C++, without any external dependencies.

All functions in this post assume your image data is stored as a contiguous array of uint8_t values in row-major order

Cropping

To crop out a portion of an image, we have to extract the specified region to a new buffer.

std::vector<uint8_t> cropImage(
    const uint8_t* image,
    uint32_t width,
    uint32_t height,
    uint8_t channels,
    uint32_t x1,
    uint32_t y1,
    uint32_t x2,
    uint32_t y2
) {
    assert(x1 < x2 && y1 < y2);
    assert(x1 < width && x2 <= width);
    assert(y1 < height && y2 <= height);

    uint32_t new_width = x2 - x1;
    uint32_t new_height = y2 - y1;
    
    std::vector<uint8_t> result(new_width * new_height * channels);

    // copy all rows from y1 to y2
    for (uint32_t y = 0; y < new_height; y++) {
        memcpy(
            result.data() + y * new_width * channels,
            image + (y1 + y) * width * channels + x1 * channels,
            new_width * channels
        );
    }

    return result;
}

This function goes through all rows in the range [y1y2][y_1 \ldots y_2] in the source image and copies new_width pixels to the destination.

Flipping

Vertically

Vertical flipping mirrors the image across its horizontal axis by reversing the row order:

std::vector<uint8_t> flipImageVertically(
    const uint8_t* image,
    uint32_t width,
    uint32_t height,
    uint8_t channels
) {
    std::vector<uint8_t> result(width * height * channels);

    for (uint32_t y = 0; y < height; y++) {
        memcpy(
            result.data() + y * width * channels,
            image + (height - y - 1) * width * channels,
            width * channels
        );
    }

    return result;
}

Each row y in the destination corresponds to row (height - y - 1) in the source image. Since rows are stored contiguously, we can copy entire rows at once using memcpy.

Horizontally

Horizontal flipping mirrors the image along its vertical axis by reversing the column order:

std::vector<uint8_t> flipImageHorizontally(
    const uint8_t* image,
    uint32_t width,
    uint32_t height,
    uint8_t channels
) {
    std::vector<uint8_t> result(width * height * channels);

    for (uint32_t y = 0; y < height; y++) {
        for (uint32_t x = 0; x < width; x++) {
            const uint8_t* src = image + (y * width + (width - x - 1)) * channels;
            uint8_t* dst = result.data() + (y * width + x) * channels;
            
            for (uint8_t c = 0; c < channels; c++) {
                dst[c] = src[c];
            }
        }
    }

    return result;
}

Unlike with vertical flipping, we have to process all pixels individually as the columns are not stored contiguously in memory. Every column x in the destination image is mapped to column (x - height - 1) in the source.

Rotation (in 90° increments)

The coordinate transformations for rotation are as follows:

  • 90°: (x,y)(y,widthx1)(x, y) \rightarrow (y, \text{width} - x - 1)
  • 180°: (x,y)(widthx1,heighty1)(x, y) \rightarrow (\text{width} - x - 1, \text{height} - y - 1)
  • 270°: (x,y)(heighty1,x)(x, y) \rightarrow (\text{height} - y - 1, x)

Rotation by 180 degrees is the same as a vertical + horizontal flip. Rotation by 90 and 270 degrees swaps the width and height of the resulting image.

enum class Rotation {
    R90_CCW,
    R180,
    R270_CCW,

    R90_CW = R270_CCW,
    R270_CW = R90_CCW
};

std::vector<uint8_t> rotateImage(
    const uint8_t* image,
    uint32_t width,
    uint32_t height,
    uint8_t channels,
    Rotation rotation
) {
    std::vector<uint8_t> result(width * height * channels);
    
    switch (rotation) {
        case Rotation::R90_CCW: {
            for (uint32_t y = 0; y < height; y++) {
                for (uint32_t x = 0; x < width; x++) {
                    uint32_t srcIndex = (y * width + x) * channels;
                    // Destination pixel at (y, width - 1 - x)
                    uint32_t dstIndex = (y * height + (width - 1 - x)) * channels;
                    
                    for (uint8_t c = 0; c < channels; c++) {
                        result[dstIndex + c] = image[srcIndex + c];
                    }
                }
            }
            break;
        }
        
        case Rotation::R180: {
            for (uint32_t y = 0; y < height; y++) {
                for (uint32_t x = 0; x < width; x++) {
                    uint32_t srcIndex = (y * width + x) * channels;
                    // Destination pixel at (width - 1 - x, height - 1 - y)
                    uint32_t dstIndex = ((height - 1 - y) * width + (width - 1 - x)) * channels;
                    
                    for (uint8_t c = 0; c < channels; c++) {
                        result[dstIndex + c] = image[srcIndex + c];
                    }
                }
            }
            break;
        }
        
        case Rotation::R270_CCW: {
            for (uint32_t y = 0; y < height; y++) {
                for (uint32_t x = 0; x < width; x++) {
                    uint32_t srcIndex = (y * width + x) * channels;
                    // Destination pixel at (height - 1 - y, x)
                    uint32_t dstIndex = ((height - 1 - y) * height + x) * channels;
                    
                    for (uint8_t c = 0; c < channels; c++) {
                        result[dstIndex + c] = image[srcIndex + c];
                    }
                }
            }
            break;
        }
    }
    
    return result;
}

Performance Considerations

These operations can be easily parallelized as the calculations for each pixel is independent of others. For processing many, or large images, consider moving these operations to the GPU or enabling SIMD instructions:

  • -march=x86-64-v3 - enables all instructions sets for the x86-64-v3 architecture, which is most modern PCs (recommended).
  • -mavx2/-maxvx512 - enables (AVX2/AVX512) only (not recommended).
  • -march=native - enables all instruction sets available on your machine. Best for performance but not portable.

Image Transformations Using Pure C++

Author

incend1um

Publish Date

07 - 12 - 2025

Avatar
incend1um

Professional procrastinator

Categories