A small, self-contained C++17 project implementing basic grayscale image-processing operations without relying on an image-processing library for the core algorithms.
The project was built as focused evidence of C++, standard-library containers, numerical programming, testing, and modular software-design fundamentals.
- ASCII PGM (
P2) input and output - Pixel ownership using
std::vector<unsigned char> - Matrix-style pixel access through an
Imageclass - Binary thresholding
- 3×3 box-blur convolution
- Sobel edge detection using explicit X and Y kernels
- Validation of PGM headers, dimensions, and pixel values
- Focused automated tests for the image-processing operations
- CMake-based build and CTest integration
The executable reads examples/input_16x16.pgm and applies the operations in this order:
P2 PGM input
-> binary threshold (threshold = 128)
-> 3×3 box blur
-> Sobel edge detection
It writes:
examples/output_threshold.pgmexamples/output_blur.pgmexamples/output_edges.pgm
Each operation also has an independent function interface, so it can be tested or reused separately from the command-line pipeline.
.
├── CMakeLists.txt
├── include/
│ ├── Image.hpp
│ ├── ImageOperations.hpp
│ └── PgmIO.hpp
├── src/
│ ├── Image.cpp
│ ├── ImageOperations.cpp
│ ├── PgmIO.cpp
│ └── main.cpp
├── tests/
│ └── image_operations_tests.cpp
└── examples/
├── input_4x3.pgm
├── input_16x16.pgm
├── output_threshold.pgm
├── output_blur.pgm
└── output_edges.pgm
- A C++17-compatible compiler
- CMake 3.20 or later
The project has been configured with warning flags for AppleClang, Clang, and GCC:
-Wall -Wextra -Wpedantic
Run from the repository root:
cmake -S . -B build
cmake --build buildRun the executable from the repository root so its relative examples/ paths resolve correctly:
./build/image_toolThe program reports the processed image dimensions and the paths of the three generated outputs.
On a multi-configuration CMake generator, the executable may instead be located under a configuration directory such as build/Debug/.
Build the project, then run:
ctest \
--test-dir build \
--output-on-failureThe current test executable checks:
- threshold behaviour immediately below, at, and above the threshold
- the centre value of a 3×3 box blur on a known matrix
- preservation of blur border pixels
- zero interior Sobel response for a constant image
- a positive Sobel response at a synthetic vertical edge
The test suite currently focuses on the numerical image operations. Automated PGM parser tests are a planned improvement.
The current prototype reads ASCII PGM (P2) files with:
- positive width and height
- a declared maximum grayscale value from 1 to 255
- whitespace-separated integer pixel values
#comments in the header or pixel data- pixel values within the file's declared range
The writer produces ASCII PGM (P2) files with a maximum grayscale value of 255.
For inputs whose declared maximum value is below 255, the current reader preserves the numeric pixel values rather than rescaling them. Consequently, exact intensity-preserving round trips are currently intended for inputs whose declared maximum value is 255.
Binary PGM (P5), PNG, and JPEG are not supported.
Image owns its grayscale pixels in a one-dimensional std::vector<unsigned char>. The at(x, y) accessors centralise the conversion from two-dimensional coordinates to a vector index and provide const and non-const access.
PGM parsing and writing are isolated from the numerical operations. The reader:
- skips PGM comments
- validates integer tokens
- rejects unsupported magic numbers
- rejects non-positive dimensions
- validates the declared maximum value
- validates every pixel against the declared range
- reports truncated input
The algorithms accept their source as const Image& and return a new Image. This keeps the input unchanged and makes each operation straightforward to test independently.
Pixels below the selected threshold are set to black (0). Pixels equal to or above the threshold are set to white (255).
For the executable's threshold of 128:
pixel < 128 -> 0
pixel >= 128 -> 255
Each interior output pixel is the integer average of its 3×3 neighbourhood:
1/9 × [1 1 1
1 1 1
1 1 1]
The blur starts with a copy of the input, so its outer one-pixel border remains unchanged. Images smaller than 3×3 are returned unchanged.
The implementation applies explicit Sobel X and Y kernels:
Gx = [-1 0 1] Gy = [ 1 2 1]
[-2 0 2] [ 0 0 0]
[-1 0 1] [-1 -2 -1]
Gradient magnitude is calculated using:
sqrt(gx² + gy²)
The result is clamped to the grayscale range 0-255. Sobel is evaluated only for interior pixels; because the output image is initially zero-filled, its outer one-pixel border is black.
The two neighbourhood-based operations currently use different explicit border policies:
- 3×3 box blur: copy the input border unchanged
- Sobel edge detection: leave the output border at zero
Both policies avoid sampling outside the image. Production implementations might instead use zero padding, coordinate clamping, reflection, or another policy selected for the application.
- Supports only ASCII PGM (
P2) - Uses fixed processing parameters in the executable
- Uses a fixed 3×3 box-blur kernel
- Uses integer division for the box-blur average
- Clamps Sobel magnitude rather than normalising it across the image
- Does not rescale PGM inputs whose declared maximum value is below 255
- Processes images sequentially on the CPU
- Does not include performance benchmarks
- PGM error paths are not yet covered by automated tests
- Intended as a focused learning prototype, not production or medical-device software
- representing a two-dimensional image in a one-dimensional
std::vector - designing a small C++ class with const and non-const pixel access
- separating image representation, file I/O, algorithms, and application flow
- parsing and validating a simple external file format
- implementing thresholding and convolution with explicit loops
- combining Sobel X and Y gradients into an edge magnitude
- testing numerical operations with small matrices whose results are known
- building and testing a multi-file C++17 project with CMake and CTest
- add automated tests for valid and malformed PGM files
- either restrict input to a maximum value of 255 or rescale other valid PGM ranges
- accept input/output paths and operation parameters as command-line arguments
- make border handling configurable and consistent across operations
- support binary PGM (
P5) - benchmark larger images and investigate performance improvements
- C++17 project and compiler-warning configuration
- Grayscale pixels stored in
std::vector - Matrix-style pixel indexing through
Image - P2 PGM input and output
- PGM input validation
- Binary thresholding
- 3×3 box blur
- Sobel edge detection
- Focused image-operation tests
- CMake build and CTest integration
- Automated PGM parser tests
- Command-line input and output paths