Skip to content

Commit b4fc7fd

Browse files
authoredDec 9, 2024··
Merge pull request #21 from gjbex/development
Development
2 parents 47bed4d + a36af39 commit b4fc7fd

File tree

9 files changed

+245
-89
lines changed

9 files changed

+245
-89
lines changed
 

‎python_for_hpc.pptx

-167 Bytes
Binary file not shown.

‎source-code/convolution/cpp/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ set(CMAKE_CXX_EXTENSIONS NO)
77

88
add_compile_options(-Wall -Wextra -Wpedantic)
99

10+
enable_testing()
11+
1012
add_subdirectory(src)

‎source-code/convolution/cpp/src/CMakeLists.txt

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
add_subdirectory(convolution)
22

3-
add_executable(test_convolution.exe
3+
find_package(Catch2)
4+
5+
if(Catch2_FOUND)
6+
add_executable(test_convolution.exe
47
test_convolution.cpp)
5-
target_include_directories(test_convolution.exe
6-
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/convolution)
7-
target_link_libraries(test_convolution.exe
8-
PRIVATE convolution)
8+
target_link_libraries(test_convolution.exe
9+
PRIVATE Catch2::Catch2WithMain)
10+
target_include_directories(test_convolution.exe
11+
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/convolution)
12+
target_link_libraries(test_convolution.exe
13+
PRIVATE convolution)
14+
15+
add_test(NAME convolution
16+
COMMAND test_convolution.exe)
17+
else()
18+
message(STATUS "Catch2 not found, tests will not be built")
19+
endif()
920

1021
add_executable(benchmark_convolution.exe
1122
benchmark_convolution.cpp)
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,40 @@
11
#include "convolution.h"
22
#include <exception>
33

4-
Matrix convolve(const Matrix& image, const Matrix& kernel) {
4+
Matrix convolve(const Matrix& input, const Matrix& kernel) {
55
if (kernel.rows() % 2 != 1 || kernel.cols() % 2 != 1) {
66
throw std::invalid_argument("Only odd dimensions on kernel supported");
77
}
88
/*
9-
s_mid and t_mid are number of pixels between the center pixel
9+
kernel_row_mid and kernel_col_mid are number of pixels between the center pixel
1010
and the edge, ie for a 5x5 filter they will be 2.
1111
12-
The output size is calculated by adding s_mid, t_mid to each
13-
side of the dimensions of the input image.
12+
The output size is calculated by adding kernel_row_mid, kernel_col_mid to each
13+
side of the dimensions of the input input.
1414
*/
15-
auto s_mid {kernel.rows()/2};
16-
auto t_mid {kernel.cols()/2};
17-
auto x_max {image.rows() + 2*s_mid};
18-
auto y_max {image.cols() + 2*t_mid};
19-
// Allocate result image.
20-
Matrix new_image(x_max, y_max);
21-
// Do convolution
22-
for (int x = 0; x < x_max; ++x) {
23-
for (int y = 0; y < y_max; ++y) {
24-
// Calculate pixel value for h at (x,y). Sum one component
25-
// for each pixel (s, t) of the filter kernel.
26-
auto s_from {std::max(s_mid - x, -s_mid)};
27-
auto s_to {std::min((x_max - x) - s_mid, s_mid + 1)};
28-
auto t_from {std::max(t_mid - y, -t_mid)};
29-
auto t_to {std::min((y_max - y) - t_mid, t_mid + 1)};
15+
auto kernel_row_mid {kernel.rows()/2};
16+
auto kernel_col_mid {kernel.cols()/2};
17+
// Allocate result input.
18+
Matrix output(input.rows(), input.cols());
19+
// Do convolution.
20+
for (int row = 0; row < input.rows(); ++row) {
21+
auto min_kernel_row {std::max(0, kernel_row_mid - row)};
22+
auto max_kernel_row {std::min(kernel.cols(), kernel_row_mid + input.rows() - row)};
23+
for (int col = 0; col < input.cols(); ++col) {
24+
auto min_kernel_col {std::max(0, kernel_col_mid - col)};
25+
auto max_kernel_col {std::min(kernel.cols(), kernel_col_mid + input.cols() - col)};
3026
double value {0.0};
31-
for (int s = s_from; s < s_to; ++s) {
32-
for (int t = t_from; t < t_to; ++t) {
33-
auto v {x - s_mid + s};
34-
auto w {y - t_mid + t};
35-
value += kernel(s_mid - s, t_mid - t)*image(v, w);
27+
// i and j are the kernel images.
28+
for (int i = min_kernel_row; i < max_kernel_row; ++i) {
29+
for (int j = min_kernel_col; j < max_kernel_col; ++j) {
30+
// k and l are the image indices.
31+
auto k {row - kernel_row_mid + i};
32+
auto l {col - kernel_col_mid + j};
33+
value += kernel(i, j)*input(k, l);
3634
}
3735
}
38-
new_image(x, y) = value;
36+
output(row, col) = value;
3937
}
4038
}
41-
return new_image;
39+
return output;
4240
}

‎source-code/convolution/cpp/src/convolution/matrices.cpp

+18
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Matrix& Matrix::operator=(const Matrix& other) {
1919
}
2020
return *this;
2121
}
22+
2223
Matrix::Matrix(Matrix&& other) noexcept :
2324
rows_{other.rows_}, cols_{other.cols_},
2425
data_{std::move(other.data_)} {
@@ -38,6 +39,23 @@ Matrix& Matrix::operator=(Matrix&& other) noexcept {
3839
return *this;
3940
}
4041

42+
bool Matrix::operator==(const Matrix& other) const {
43+
if (this == &other) {
44+
return true;
45+
}
46+
if (rows_ != other.rows_ || cols_ != other.cols_) {
47+
return false;
48+
}
49+
for (int i = 0; i < rows_; ++i) {
50+
for (int j = 0; j < cols_; ++j) {
51+
if ((*this)(i, j) != other(i, j)) {
52+
return false;
53+
}
54+
}
55+
}
56+
return true;
57+
}
58+
4159
std::ostream& operator<<(std::ostream& os, const Matrix& m) {
4260
for (int i = 0; i < m.rows_; ++i) {
4361
for (int j = 0; j < m.cols_; ++j) {

‎source-code/convolution/cpp/src/convolution/matrices.h

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@ struct Matrix {
1212
std::unique_ptr<double[]> data_;
1313
public:
1414
Matrix(int rows, int cols) :
15-
rows_(rows), cols_(cols), data_(new double[rows * cols]) {}
15+
rows_(rows), cols_(cols), data_(new double[rows*cols]) {}
1616
// copy constructor & assignment operator
1717
Matrix(const Matrix& other);
1818
Matrix& operator=(const Matrix& other);
1919
// move constructor & assignment operator
2020
Matrix(Matrix&& other) noexcept;
2121
Matrix& operator=(Matrix&& other) noexcept;
2222
// matrix indexing by row and column
23-
double& operator()(int i, int j) { return data_[i * cols_ + j]; }
24-
double operator()(int i, int j) const { return data_[i * cols_ + j]; }
23+
double& operator()(int i, int j) { return data_[i*cols_ + j]; }
24+
double operator()(int i, int j) const { return data_[i*cols_ + j]; }
2525
// getters for number of rows and columns
2626
int rows() const { return rows_; }
2727
int cols() const { return cols_; }
28+
bool operator==(const Matrix& other) const;
29+
bool operator!=(const Matrix& other) const { return !(*this == other); }
2830
// accessors for the data
2931
double* data() { return data_.get(); }
3032
const double* data() const { return data_.get(); }
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,134 @@
1+
#define CATCH_CONFIG_MAIN
2+
#include <catch2/catch.hpp>
13
#include <convolution.h>
2-
#include <iostream>
3-
4-
int main() {
5-
// Create a 10x10 image
6-
Matrix image(10, 10);
7-
for (int i = 0; i < image.rows(); ++i) {
8-
for (int j = 0; j < image.cols(); ++j) {
9-
image(i, j) = i*image.cols() + j;
4+
5+
TEST_CASE("3x3 kernel", "[convolution]") {
6+
const int input_size {5};
7+
const int kernel_size {3};
8+
9+
Matrix input(input_size, input_size);
10+
for (int i = 0; i < input.rows(); ++i) {
11+
for (int j = 0; j < input.cols(); ++j) {
12+
input(i, j) = 1.0;
13+
}
14+
}
15+
16+
Matrix kernel(kernel_size, kernel_size);
17+
for (int i = 0; i < kernel.rows(); ++i) {
18+
for (int j = 0; j < kernel.cols(); ++j) {
19+
kernel(i, j) = 1.0;
1020
}
1121
}
1222

13-
// Print the image
14-
std::cout << image << std::endl;
23+
Matrix target(input_size, input_size);
24+
target(0, 0) = 4.0;
25+
target(0, 1) = 6.0;
26+
target(0, 2) = 6.0;
27+
target(0, 3) = 6.0;
28+
target(0, 4) = 4.0;
29+
target(1, 0) = 6.0;
30+
target(1, 1) = 9.0;
31+
target(1, 2) = 9.0;
32+
target(1, 3) = 9.0;
33+
target(1, 4) = 6.0;
34+
target(2, 0) = 6.0;
35+
target(2, 1) = 9.0;
36+
target(2, 2) = 9.0;
37+
target(2, 3) = 9.0;
38+
target(2, 4) = 6.0;
39+
target(3, 0) = 6.0;
40+
target(3, 1) = 9.0;
41+
target(3, 2) = 9.0;
42+
target(3, 3) = 9.0;
43+
target(3, 4) = 6.0;
44+
target(4, 0) = 4.0;
45+
target(4, 1) = 6.0;
46+
target(4, 2) = 6.0;
47+
target(4, 3) = 6.0;
48+
target(4, 4) = 4.0;
49+
50+
auto result = convolve(input, kernel);
51+
52+
REQUIRE(result == target);
53+
}
54+
55+
TEST_CASE("5x5 kernel", "[convolution]") {
56+
const int input_size {6};
57+
const int kernel_size {5};
1558

16-
// Create a 3x3 kernel
17-
Matrix kernel(3, 3);
59+
Matrix input(input_size, input_size);
60+
for (int i = 0; i < input.rows(); ++i) {
61+
for (int j = 0; j < input.cols(); ++j) {
62+
input(i, j) = 1.0;
63+
}
64+
}
65+
66+
Matrix kernel(kernel_size, kernel_size);
1867
for (int i = 0; i < kernel.rows(); ++i) {
1968
for (int j = 0; j < kernel.cols(); ++j) {
20-
kernel(i, j) = 1.0/(kernel.rows()*kernel.cols());
69+
kernel(i, j) = 1.0;
2170
}
2271
}
2372

24-
// Print the kernel
25-
std::cout << kernel << std::endl;
73+
Matrix target(input_size, input_size);
74+
target(0, 0) = 9.0;
75+
target(0, 1) = 12.0;
76+
target(0, 2) = 15.0;
77+
target(0, 3) = 15.0;
78+
target(0, 4) = 12.0;
79+
target(0, 5) = 9.0;
80+
target(1, 0) = 12.0;
81+
target(1, 1) = 16.0;
82+
target(1, 2) = 20.0;
83+
target(1, 3) = 20.0;
84+
target(1, 4) = 16.0;
85+
target(1, 5) = 12.0;
86+
target(2, 0) = 15.0;
87+
target(2, 1) = 20.0;
88+
target(2, 2) = 25.0;
89+
target(2, 3) = 25.0;
90+
target(2, 4) = 20.0;
91+
target(2, 5) = 15.0;
92+
target(3, 0) = 15.0;
93+
target(3, 1) = 20.0;
94+
target(3, 2) = 25.0;
95+
target(3, 3) = 25.0;
96+
target(3, 4) = 20.0;
97+
target(3, 5) = 15.0;
98+
target(4, 0) = 12.0;
99+
target(4, 1) = 16.0;
100+
target(4, 2) = 20.0;
101+
target(4, 3) = 20.0;
102+
target(4, 4) = 16.0;
103+
target(4, 5) = 12.0;
104+
target(5, 0) = 9.0;
105+
target(5, 1) = 12.0;
106+
target(5, 2) = 15.0;
107+
target(5, 3) = 15.0;
108+
target(5, 4) = 12.0;
109+
target(5, 5) = 9.0;
110+
auto result = convolve(input, kernel);
111+
112+
REQUIRE(result == target);
113+
}
26114

27-
// Create a convolution object
28-
auto new_image = convolve(image, kernel);
115+
TEST_CASE("even-sized kernel error", "[convolution]") {
116+
const int input_size {5};
117+
const int kernel_size {4};
29118

30-
// Print the result
31-
std::cout << new_image << std::endl;
119+
Matrix input(input_size, input_size);
120+
for (int i = 0; i < input.rows(); ++i) {
121+
for (int j = 0; j < input.cols(); ++j) {
122+
input(i, j) = 1.0;
123+
}
124+
}
32125

33-
return 0;
126+
Matrix kernel(kernel_size, kernel_size);
127+
for (int i = 0; i < kernel.rows(); ++i) {
128+
for (int j = 0; j < kernel.cols(); ++j) {
129+
kernel(i, j) = 1.0;
130+
}
131+
}
132+
REQUIRE_THROWS_AS(convolve(input, kernel), std::invalid_argument);
34133
}
134+

‎source-code/convolution/python/convolution.py

+25-28
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def convolve(input, kernel):
1414
Returns
1515
-------
1616
output : numpy.ndarray
17-
output matrix, it is not cropped
17+
output matrix, shape is the same as input
1818
1919
Raises
2020
------
@@ -23,31 +23,28 @@ def convolve(input, kernel):
2323
'''
2424
if kernel.shape[0] % 2 != 1 or kernel.shape[1] % 2 != 1:
2525
raise ValueError("Only odd dimensions on filter supported")
26-
# s_mid and t_mid are number of pixels between the center pixel
27-
# and the edge, ie for a 5x5 filter they will be 2.
28-
#
29-
# The output size is calculated by adding s_mid, t_mid to each
30-
# side of the dimensions of the input image.
31-
s_mid = kernel.shape[0] // 2
32-
t_mid = kernel.shape[1] // 2
33-
x_max = input.shape[0] + 2 * s_mid
34-
y_max = input.shape[1] + 2 * t_mid
35-
# Allocate result image.
36-
output = np.zeros([x_max, y_max], dtype=input.dtype)
37-
# Do convolution
38-
for x in range(x_max):
39-
for y in range(y_max):
40-
# Calculate pixel value for h at (x,y). Sum one component
41-
# for each pixel (s, t) of the filter kernel.
42-
s_from = max(s_mid - x, -s_mid)
43-
s_to = min((x_max - x) - s_mid, s_mid + 1)
44-
t_from = max(t_mid - y, -t_mid)
45-
t_to = min((y_max - y) - t_mid, t_mid + 1)
46-
value = 0
47-
for s in range(s_from, s_to):
48-
for t in range(t_from, t_to):
49-
v = x - s_mid + s
50-
w = y - t_mid + t
51-
value += kernel[s_mid - s, t_mid - t]*input[v, w]
52-
output[x, y] = value
26+
# kernel_row_mid and kernel_col_mid are number of elements between the
27+
# center elements # and the edge, i.e., for a 5x5 filter they will be 2.
28+
kernel_row_mid = kernel.shape[0]//2
29+
kernel_col_mid = kernel.shape[1]//2
30+
# Allocate result input.
31+
output = np.empty_like(input)
32+
# Do convolution.
33+
for row in range(input.shape[0]):
34+
min_kernel_row = max(0, kernel_row_mid - row)
35+
max_kernel_row = min(kernel.shape[0],
36+
kernel_row_mid + input.shape[0] - row)
37+
for col in range(input.shape[1]):
38+
min_kernel_col = max(0, kernel_col_mid - col)
39+
max_kernel_col = min(kernel.shape[1],
40+
kernel_col_mid + input.shape[1] - col)
41+
value = 0.0
42+
# i and j are the kernel images
43+
for i in range(min_kernel_row, max_kernel_row):
44+
for j in range(min_kernel_col, max_kernel_col):
45+
# k and l are the image indices
46+
k = row - kernel_row_mid + i
47+
l = col - kernel_col_mid + j
48+
value += kernel[i, j]*input[k, l]
49+
output[row, col] = value
5350
return output

‎source-code/convolution/python/test_convolution.py

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1-
#!/usr/bin/env python
2-
31
import convolution
42
import numpy as np
3+
import pytest
4+
5+
def test_4x4_kernel_error():
6+
image_size, kernel_size = 5, 4
7+
image = np.ones((image_size, image_size))
8+
kernel = np.ones((kernel_size, kernel_size))
9+
with pytest.raises(ValueError):
10+
new_image = convolution.convolve(image, kernel)
511

12+
def test_3x3_kernel():
13+
image_size, kernel_size = 5, 3
14+
image = np.ones((image_size, image_size))
15+
kernel = np.ones((kernel_size, kernel_size))
16+
new_image = convolution.convolve(image, kernel)
17+
target = np.array([
18+
[4, 6, 6, 6, 4,],
19+
[6, 9, 9, 9, 6,],
20+
[6, 9, 9, 9, 6,],
21+
[6, 9, 9, 9, 6,],
22+
[4, 6, 6, 6, 4,],
23+
], dtype=np.float64)
24+
assert (new_image == target).all()
625

7-
image = np.arange(0.0, 100.0, 1.0).reshape(10, 10)
8-
print(f'{image}\n')
9-
kernel = np.ones((3, 3))/(3*3)
10-
print(f'{kernel}\n')
26+
def test_5x5_kernel():
27+
image_size, kernel_size = 6, 5
28+
image = np.ones((image_size, image_size))
29+
kernel = np.ones((kernel_size, kernel_size))
30+
new_image = convolution.convolve(image, kernel)
31+
target = np.array([
32+
[9, 12, 15, 15, 12, 9,],
33+
[12, 16, 20, 20, 16, 12,],
34+
[15, 20, 25, 25, 20, 15,],
35+
[15, 20, 25, 25, 20, 15,],
36+
[12, 16, 20, 20, 16, 12,],
37+
[9, 12, 15, 15, 12, 9,],
38+
], dtype=np.float64)
39+
assert (new_image == target).all()
1140

12-
print(convolution.convolve(image, kernel))

0 commit comments

Comments
 (0)
Please sign in to comment.