Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
CC := gcc
CFLAGS_COMMON := -Wall -Wextra -Wpedantic -I./src -I./deps
CFLAGS_DEBUG := -O0 -g
CFLAGS_RELEASE := -O2

CFLAGS_ASAN := -fsanitize=address -g -O0 -fno-omit-frame-pointer
LDFLAGS_ASAN := -fsanitize=address

CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_DEBUG)
CFLAGS_BENCH := $(CFLAGS_COMMON) $(CFLAGS_RELEASE) -DNDEBUG
LDFLAGS := -lm -fopenmp

SRC := $(shell find src -name '*.c' ! -name 'main.c' ! -name 'benchmark.c')
APP_SRC := $(SRC) src/main.c
BENCH_SRC := $(SRC) src/benchmark.c
HDR := $(shell find src -name '*.h') $(shell find tests -name '*.h' 2>/dev/null || true)

TARGET := build/convol
BENCH_TARGET := build/benchmark

TEST_SRC := $(shell find tests -name '*.c' 2>/dev/null || true)
TEST_BIN := build/tests

.PHONY: build clean fmt test bench help asan

help:
@echo "Available targets:"
@echo " build - Build main program"
@echo " test - Run tests"
@echo " bench - Run single image benchmark"
@echo " asan - Build with AddressSanitizer"
@echo " clean - Remove build directory"
@echo " fmt - Format code"

build: $(TARGET)

$(TARGET): $(APP_SRC) $(HDR)
@mkdir -p build
$(CC) $(CFLAGS) $(APP_SRC) -o $@ $(LDFLAGS)

$(BENCH_TARGET): $(BENCH_SRC) $(HDR)
@mkdir -p build
$(CC) $(CFLAGS_BENCH) $(BENCH_SRC) -o $@ $(LDFLAGS)

$(TEST_BIN): $(SRC) $(TEST_SRC) $(HDR)
@mkdir -p build
$(CC) $(CFLAGS) $(SRC) $(TEST_SRC) -o $@ $(LDFLAGS) -lcmocka

bench: $(BENCH_TARGET)
@./$(BENCH_TARGET) $(FILTER) $(IMAGE)

test: $(TEST_BIN)
@./$(TEST_BIN)

asan: clean
@mkdir -p build
$(CC) $(CFLAGS) $(CFLAGS_ASAN) $(APP_SRC) -o $(TARGET)_asan $(LDFLAGS) $(LDFLAGS_ASAN)

clean:
rm -rf build

fmt:
clang-format -i $(SRC) $(APP_SRC) $(HDR) $(TEST_SRC) $(BENCH_SRC)

# Default values for benchmark
FILTER ?= motion
IMAGE ?= ./images/test_image.jpg
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Convolution

Image convolution tool with multiple parallelization strategies.

## Features

- **Filters**: `blur`, `sharpen`, `gaussian`, `motion`, `edge`, `emboss`
- **Modes**:
- `seq` - sequential
- `row` - parallel by rows
- `column` - parallel by columns
- `pixel` - parallel by individual pixels
- `block` - parallel by blocks (64x64)
- **Formats**: `jpeg`, `jpg`, `png`, `bmp`, `tga`

## Build

```bash
make build
````

## Usage

```bash
./build/convol <input_file> --filter=<filter> --mode=<mode> [--help | -h]
````

## Examples

```bash
# Sequential blur on an image
./build/convol image.jpg --filter=blur --mode=seq

# Parallel by blocks with motion blur
./build/convol image.png --filter=motion --mode=block

# Emboss effect with row-parallel execution
./build/convol photo.jpg --filter=emboss --mode=row

# Parallel blocks with motion blur for multiple files
./build/convol image.png photo.png file.bmp --filter=motion --mode=block
```

## Tests

```
make test
```
## Benchmarks

**Single image benchmark (comparison of modes)**
```bash
# Default (filter=motion, image=./images/test_image.jpg)
make bench
# Custom filter and image
make bench FILTER=blur IMAGE=./images/photo.png
make bench FILTER=gaussian IMAGE=./images/image.jpg
```

Benchmark results are located in a [corresponding](./benchmark_res/) folder
51 changes: 51 additions & 0 deletions benchmark_res/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Benchmark Results

**Benchmark Setup**:
- Ubuntu 25.04
- AMD Ryzen 5 4600H @ 3.0 GHz (12 threads)
- 32 GB DDR4
- GCC 15.0.1

## Small Image

**Configuration:**
- Filter: motion (9x9)
- Image: 736x469, 3 channels
- Runs: 100

| Mode | Avg (sec) | Min (sec) | Median (sec) | Max (sec) |
|--------|-----------|-----------|--------------|-----------|
| seq | 0.109843 | 0.109399 | 0.109727 | 0.122655 |
| row | 0.018749 | 0.018467 | 0.018488 | 0.028643 |
| pixel | **0.018463** | **0.018279** | **0.018302** | **0.024248** |
| column | 0.018745 | 0.018511 | 0.018550 | 0.024789 |
| block | 0.021096 | 0.020226 | 0.020857 | 0.026649 |

![Image Benchmark](result_small.png)
![Image Benchmark](result_small_parallel.png)

## Large Image

**Configuration:**
- Filter: motion (9x9)
- Image: 6000x4000, 3 channels
- Runs: 50

| Mode | Avg (sec) | Min (sec) | Median (sec) | Max (sec) |
|--------|-----------|-----------|--------------|-----------|
| seq | 7.683813 | 7.657912 | 7.680730 | 7.739433 |
| row | 1.295177 | 1.274591 | 1.284864 | 1.405707 |
| pixel | **1.291997** | **1.272699** | **1.283954** | **1.356831** |
| column | 1.475161 | 1.399772 | 1.447077 | 1.684282 |
| block | 1.310514 | 1.281334 | 1.301490 | 1.436669 |

![Image Benchmark](result_large.png)
![Image Benchmark](result_large_parallel.png)

## Conclusion

We can make following conclusions:
- **Pixel mode achieves the best performance**
- **Row-parallel and Pixel-parallel are nearly identical**
- **Column mode** is the slowest due to poor cache locality
- **Block-parallel has higher overhead** on small images, but performs well on large images
57 changes: 57 additions & 0 deletions benchmark_res/plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

modes = []
data = []

filename = "result_small"

def read_benchmark_data_pandas(filename):
df = pd.read_csv(filename + ".csv", comment='#')

modes = df['mode'].tolist()
value_cols = [col for col in df.columns if col != 'mode']
data = [df[col].tolist() for col in value_cols]
data = list(zip(*data))

return modes, data

modes, data = read_benchmark_data_pandas(filename)

fig, ax = plt.subplots(figsize=(8, 6))

bp = ax.boxplot(
data,
positions=np.arange(len(data)) + 1,
widths=0.6,
patch_artist=True,
showfliers=False,
)

colors = ["#FF9999", "#99FF99", "#9999FF", "#FFCC99", "#CC99FF", "#66CCCC"]
for patch, color in zip(bp["boxes"], colors * 10):
patch.set_facecolor(color)
patch.set_alpha(0.6)

for i, d in enumerate(data, start=1):
x = np.random.normal(i, 0.04, size=len(d))
ax.plot(x, d, "o", markersize=5, alpha=0.3, color="black")

ax.set_xticks(np.arange(1, len(modes) + 1))
ax.set_xticklabels(modes, rotation=20)
ax.set_ylabel("Time (sec)")
ax.set_title("Benchmark: motion (9x9) | 736x469")

plt.grid(True, axis="y", linestyle="--", alpha=0.6)
plt.tight_layout()

plt.savefig(filename + ".png", dpi=200)
plt.show()

print("\n=== Benchmark Summary ===")
print(f"{'Mode':<10} {'Min':<10} {'Median':<10} {'Max':<10}")
print("-" * 50)
for i, mode in enumerate(modes):
vals = data[i]
print(f"{mode:<10} {np.min(vals):<10.6f} {np.median(vals):<10.6f} {np.max(vals):<10.6f}")
Binary file added benchmark_res/result_large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark_res/result_large_parallel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark_res/result_small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added benchmark_res/result_small_parallel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading