diff --git a/CMakeLists.txt b/CMakeLists.txt index b9d359c..f9527fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ project( include(GNUInstallDirs) include(CTest) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -20,15 +20,15 @@ if(PIXEL_SUM_ENABLE_SANITIZERS AND CMAKE_BUILD_TYPE STREQUAL "Debug") endif() set(PIXEL_SUM_PUBLIC_HEADERS - ${CMAKE_SOURCE_DIR}/include/pixel_sum/PixelSum.h) + ${CMAKE_SOURCE_DIR}/include/pixel_sum/pixel_sum.hpp) set(PIXEL_SUM_TEST_SUPPORT_HEADERS - ${CMAKE_SOURCE_DIR}/tests/support/Common.h - ${CMAKE_SOURCE_DIR}/tests/support/TestUtility.h - ${CMAKE_SOURCE_DIR}/tests/support/TimeUtility.h) + ${CMAKE_SOURCE_DIR}/tests/support/common.hpp + ${CMAKE_SOURCE_DIR}/tests/support/test_utility.hpp + ${CMAKE_SOURCE_DIR}/tests/support/time_utility.hpp) add_library(PixelSumLib - src/PixelSum.cpp + src/pixel_sum.cpp ${PIXEL_SUM_PUBLIC_HEADERS}) target_include_directories(PixelSumLib @@ -41,7 +41,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") endif() add_executable(PixelSumTest - tests/PixelSumTest.cpp + tests/pixel_sum_test.cpp ${PIXEL_SUM_TEST_SUPPORT_HEADERS}) target_link_libraries(PixelSumTest PRIVATE PixelSumLib) @@ -58,8 +58,8 @@ find_program(CPPCHECK cppcheck) set(PIXEL_SUM_ALL_CPP ${PIXEL_SUM_PUBLIC_HEADERS} - ${CMAKE_SOURCE_DIR}/src/PixelSum.cpp - ${CMAKE_SOURCE_DIR}/tests/PixelSumTest.cpp + ${CMAKE_SOURCE_DIR}/src/pixel_sum.cpp + ${CMAKE_SOURCE_DIR}/tests/pixel_sum_test.cpp ${PIXEL_SUM_TEST_SUPPORT_HEADERS}) if(CLANG_FORMAT) @@ -78,7 +78,7 @@ endif() if(CLANG_TIDY) add_custom_target(tidy - COMMAND ${CLANG_TIDY} -p ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src/PixelSum.cpp ${CMAKE_SOURCE_DIR}/tests/PixelSumTest.cpp + COMMAND ${CLANG_TIDY} -p ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src/pixel_sum.cpp ${CMAKE_SOURCE_DIR}/tests/pixel_sum_test.cpp WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Running clang-tidy" VERBATIM) @@ -87,7 +87,7 @@ endif() if(CPPCHECK) set(PIXEL_SUM_CPPCHECK_ARGS --enable=warning,style,performance,portability - --std=c++17 + --std=c++20 --language=c++ --inline-suppr) diff --git a/README.md b/README.md index 1d62dd8..a2f132e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PixelSum -A tiny C++17 library that builds summed-area tables (integral images) so rectangular queries over pixel buffers are O(1). It supports 8- and 16-bit pixels via a templated `PixelSum` that stores both the raw pixels and two integral images: one for sums and one for non-zero counts. +A tiny C++20 library that builds summed-area tables (integral images) so rectangular queries over pixel buffers are O(1). It supports 8- and 16-bit pixels via a templated `PixelSum` that stores both the raw pixels and two integral images: one for sums and one for non-zero counts. ## Features - Constant-time sum, average, non-zero count, and non-zero average for any axis-aligned window @@ -25,7 +25,7 @@ cmake --build build --parallel ## Usage ```cpp -#include +#include std::vector pixels(width * height, 128); PixelSumU8 ps(pixels.data(), width, height); @@ -65,4 +65,4 @@ GitHub Actions (`.github/workflows/ci.yml`) runs: ## Notes - Dimensions are limited to 4096 x 4096 to keep intermediate sums within `uint32_t` for 8-bit pixels; switch to `PixelSumU16` for larger ranges or higher bit depth. -- The minimal test harness in `tests/PixelSumTest.cpp` exercises edge cases and can be extended with additional `TEST` blocks. +- The minimal test harness in `tests/pixel_sum_test.cpp` exercises edge cases and can be extended with additional `TEST` blocks. diff --git a/include/pixel_sum/PixelSum.h b/include/pixel_sum/pixel_sum.hpp similarity index 52% rename from include/pixel_sum/PixelSum.h rename to include/pixel_sum/pixel_sum.hpp index cdd4b62..bc500f0 100644 --- a/include/pixel_sum/PixelSum.h +++ b/include/pixel_sum/pixel_sum.hpp @@ -1,16 +1,19 @@ #pragma once +#include #include -#include +#include #include template class PixelSum { - static constexpr int MAX_WIDTH = 4096; - static constexpr int MAX_HEIGHT = 4096; + static constexpr int kMaxWidth = 4096; + static constexpr int kMaxHeight = 4096; public: explicit PixelSum(const T* buffer, int width, int height); + explicit PixelSum(std::span buffer, int width, int height); + ~PixelSum() = default; PixelSum(const PixelSum&) = default; PixelSum(PixelSum&&) noexcept = default; @@ -25,31 +28,18 @@ class PixelSum { explicit operator bool() const noexcept; private: - int width_ { 0 }; - int height_ { 0 }; - - // pixel data - std::vector pixel_data_ {}; + int width_{0}; + int height_{0}; - // sparse matrix for existent flags - std::vector nonzero_data_ {}; + std::vector pixel_data_{}; + std::vector nonzero_data_{}; + std::vector summed_data_{}; - // each pixel represents the cumulative sum of corresponding input pixel with - // all pixels above and to the left of input pixel. - std::vector summed_data_ {}; - - void swap(int& x0, int& y0, int& x1, int& y1) const; - bool clampBound(int& x0, int& y0, int& x1, int& y1) const; - [[nodiscard]] S getSummedArea(const std::vector& data, int x0, int y0, int x1, int y1) const; + static void normalizeBounds(int& x0, int& y0, int& x1, int& y1); + [[nodiscard]] bool clampBounds(int& x0, int& y0, int& x1, int& y1) const; + [[nodiscard]] static std::size_t indexOf(int x, int y, int width) noexcept; + [[nodiscard]] S getSummedArea(std::span data, int x0, int y0, int x1, int y1) const; }; using PixelSumU8 = PixelSum; using PixelSumU16 = PixelSum; - -template -inline void swap_if_a_greater_than_b(T& a, T& b) -{ - if (a > b) { - std::swap(a, b); - } -} diff --git a/src/PixelSum.cpp b/src/PixelSum.cpp deleted file mode 100644 index 1cafee5..0000000 --- a/src/PixelSum.cpp +++ /dev/null @@ -1,179 +0,0 @@ -#include "pixel_sum/PixelSum.h" - -#include -#include -#include - -// In case of huge data is given, then it should take a fairly long time. -template -void IntegralImage(const std::vector& source, - int width, - int height, - std::vector& target, - Transformer transformer) -{ - auto source_iter = source.cbegin(); - auto target_iter = target.begin(); - - // The summed matrix is calculated from top left corner - // I(x, y) = i(x, y) + I(x, y - 1) + I(x - 1, y) - I(x - 1, y - 1) - - // fill first row - *target_iter++ = transformer(*source_iter++); - for (int x = 1; x < width; x++) { - *target_iter++ = *(target_iter - 1) + transformer(*source_iter++); - } - - // fill rest of row - for (int y = 1; y < height; y++) { - *target_iter++ = transformer(*source_iter++) + *(target_iter - width); - for (int x = 1; x < width; x++) { - *target_iter++ = transformer(*source_iter++) + *(target_iter - width) + *(target_iter - 1) - *(target_iter - width - 1); - } - } -} - -template -void IntegralImage(const std::vector& source, int width, int height, std::vector& target) -{ - IntegralImage(source, - width, - height, - target, - [](const T& val) -> S { return static_cast(val); }); -} - -template -PixelSum::PixelSum(const T* buffer, int width, int height) - : width_(width) - , height_(height) -{ - if (width_ <= 0 || width_ > MAX_WIDTH || height_ <= 0 || height_ > MAX_HEIGHT) { - throw std::runtime_error("Dimension is out of bound"); - } - - const auto dimension = static_cast(width_) * static_cast(height_); - - // copy buffer to pixel data - pixel_data_ = std::vector(buffer, buffer + dimension); - - // non-zero count for sparce matrix - nonzero_data_ = std::vector(dimension); - IntegralImage( - pixel_data_, width_, height_, nonzero_data_, [](const T& val) -> T { - return val > 0; - }); - - // cumulative matrix - summed_data_ = std::vector(dimension); - IntegralImage(pixel_data_, width_, height_, summed_data_); -} - -template -S PixelSum::getPixelSum(int x0, int y0, int x1, int y1) const -{ - swap(x0, y0, x1, y1); - if (!clampBound(x0, y0, x1, y1)) { - return 0; - } - - return getSummedArea(summed_data_, x0, y0, x1, y1); -} - -template -double PixelSum::getPixelAverage(int x0, int y0, int x1, int y1) const -{ - swap(x0, y0, x1, y1); - int count = (x1 - x0 + 1) * (y1 - y0 + 1); - - if (!clampBound(x0, y0, x1, y1)) { - return 0.0; - } - - double sum = static_cast(getPixelSum(x0, y0, x1, y1)); - - return sum / count; -} - -template -S PixelSum::getNonZeroCount(int x0, int y0, int x1, int y1) const -{ - swap(x0, y0, x1, y1); - if (!clampBound(x0, y0, x1, y1)) { - return 0; - } - - return getSummedArea(nonzero_data_, x0, y0, x1, y1); -} - -template -double PixelSum::getNonZeroAverage(int x0, int y0, int x1, int y1) const -{ - double sum = static_cast(getPixelSum(x0, y0, x1, y1)); - auto count = getNonZeroCount(x0, y0, x1, y1); - - return count > 0 ? (sum / count) : 0; -} - -template -PixelSum::operator bool() const noexcept -{ - return !pixel_data_.empty() && !nonzero_data_.empty() && !summed_data_.empty(); -} - -template -void PixelSum::swap(int& x0, int& y0, int& x1, int& y1) const -{ - swap_if_a_greater_than_b(x0, x1); - swap_if_a_greater_than_b(y0, y1); -} - -template -bool PixelSum::clampBound(int& x0, int& y0, int& x1, int& y1) const -{ - if ((x0 < 0 && x1 < 0) || (x0 >= width_ && x1 >= width_) || (y0 < 0 && y1 < 0) || (y0 >= height_ && y1 >= height_)) { - return false; - } - - x0 = std::max(x0, 0); - y0 = std::max(y0, 0); - x1 = std::min(x1, width_ - 1); - y1 = std::min(y1, height_ - 1); - - return true; -} - -template -S PixelSum::getSummedArea(const std::vector& data, - int x0, - int y0, - int x1, - int y1) const -{ - /* - * (x0, y0) (x1, y0) - * A---------------B - * | | - * | D - B - C + A | - * | | - * C---------------D - * (x0, y1) (x1, y1) - */ - - S A = 0, B = 0, C = 0, D = 0; - D = data[y1 * width_ + x1]; - if (x0 >= 1 && y0 >= 1) { - A = data[(y0 - 1) * width_ + (x0 - 1)]; - C = data[y1 * width_ + (x0 - 1)]; - B = data[(y0 - 1) * width_ + x1]; - } else if (x0 >= 1) { - C = data[y1 * width_ + (x0 - 1)]; - } else if (y0 >= 1) { - B = data[(y0 - 1) * width_ + x1]; - } - - return D - B - C + A; -} - -template class PixelSum; -template class PixelSum; diff --git a/src/pixel_sum.cpp b/src/pixel_sum.cpp new file mode 100644 index 0000000..8ea8561 --- /dev/null +++ b/src/pixel_sum.cpp @@ -0,0 +1,167 @@ +#include "pixel_sum/pixel_sum.hpp" + +#include +#include + +namespace { +template +void buildIntegralImage(std::span source, + int width, + int height, + std::span target, + Transformer transformer) +{ + const auto at = [width](int x, int y) -> std::size_t { + return static_cast(y) * static_cast(width) + static_cast(x); + }; + + target[at(0, 0)] = transformer(source[at(0, 0)]); + + for (int x = 1; x < width; ++x) { + target[at(x, 0)] = target[at(x - 1, 0)] + transformer(source[at(x, 0)]); + } + + for (int y = 1; y < height; ++y) { + target[at(0, y)] = target[at(0, y - 1)] + transformer(source[at(0, y)]); + for (int x = 1; x < width; ++x) { + target[at(x, y)] = transformer(source[at(x, y)]) + target[at(x, y - 1)] + target[at(x - 1, y)] - + target[at(x - 1, y - 1)]; + } + } +} + +template +void buildIntegralImage(std::span source, int width, int height, std::span target) +{ + buildIntegralImage(source, width, height, target, [](const T value) -> S { return static_cast(value); }); +} +} // namespace + +template +PixelSum::PixelSum(const T* buffer, int width, int height) + : PixelSum(std::span(buffer, static_cast(width) * static_cast(height)), width, height) +{ +} + +template +PixelSum::PixelSum(std::span buffer, int width, int height) + : width_(width) + , height_(height) +{ + if (width_ <= 0 || width_ > kMaxWidth || height_ <= 0 || height_ > kMaxHeight) { + throw std::runtime_error("Dimension is out of bound"); + } + + const auto dimension = static_cast(width_) * static_cast(height_); + if (buffer.size() < dimension) { + throw std::runtime_error("Buffer size is smaller than width*height"); + } + + pixel_data_.assign(buffer.begin(), buffer.begin() + static_cast(dimension)); + + nonzero_data_.assign(dimension, S{}); + buildIntegralImage(std::span(pixel_data_), + width_, + height_, + std::span(nonzero_data_), + [](const T value) -> S { return value > T{} ? S{1} : S{0}; }); + + summed_data_.assign(dimension, S{}); + buildIntegralImage(std::span(pixel_data_), width_, height_, std::span(summed_data_)); +} + +template +S PixelSum::getPixelSum(int x0, int y0, int x1, int y1) const +{ + normalizeBounds(x0, y0, x1, y1); + if (!clampBounds(x0, y0, x1, y1)) { + return S{}; + } + + return getSummedArea(std::span(summed_data_), x0, y0, x1, y1); +} + +template +double PixelSum::getPixelAverage(int x0, int y0, int x1, int y1) const +{ + normalizeBounds(x0, y0, x1, y1); + if (!clampBounds(x0, y0, x1, y1)) { + return 0.0; + } + + const auto count = static_cast((x1 - x0 + 1) * (y1 - y0 + 1)); + const auto sum = static_cast(getSummedArea(std::span(summed_data_), x0, y0, x1, y1)); + return count > 0.0 ? (sum / count) : 0.0; +} + +template +S PixelSum::getNonZeroCount(int x0, int y0, int x1, int y1) const +{ + normalizeBounds(x0, y0, x1, y1); + if (!clampBounds(x0, y0, x1, y1)) { + return S{}; + } + + return getSummedArea(std::span(nonzero_data_), x0, y0, x1, y1); +} + +template +double PixelSum::getNonZeroAverage(int x0, int y0, int x1, int y1) const +{ + const auto sum = static_cast(getPixelSum(x0, y0, x1, y1)); + const auto count = getNonZeroCount(x0, y0, x1, y1); + return count > S{} ? (sum / static_cast(count)) : 0.0; +} + +template +PixelSum::operator bool() const noexcept +{ + return !pixel_data_.empty() && !nonzero_data_.empty() && !summed_data_.empty(); +} + +template +void PixelSum::normalizeBounds(int& x0, int& y0, int& x1, int& y1) +{ + if (x0 > x1) { + std::swap(x0, x1); + } + if (y0 > y1) { + std::swap(y0, y1); + } +} + +template +bool PixelSum::clampBounds(int& x0, int& y0, int& x1, int& y1) const +{ + if ((x0 < 0 && x1 < 0) || (x0 >= width_ && x1 >= width_) || (y0 < 0 && y1 < 0) || (y0 >= height_ && y1 >= height_)) { + return false; + } + + x0 = std::clamp(x0, 0, width_ - 1); + y0 = std::clamp(y0, 0, height_ - 1); + x1 = std::clamp(x1, 0, width_ - 1); + y1 = std::clamp(y1, 0, height_ - 1); + return true; +} + +template +std::size_t PixelSum::indexOf(int x, int y, int width) noexcept +{ + return static_cast(y) * static_cast(width) + static_cast(x); +} + +template +S PixelSum::getSummedArea(std::span data, int x0, int y0, int x1, int y1) const +{ + const auto idx = [this](int x, int y) -> std::size_t { return indexOf(x, y, width_); }; + + const S d = data[idx(x1, y1)]; + const S b = y0 > 0 ? data[idx(x1, y0 - 1)] : S{}; + const S c = x0 > 0 ? data[idx(x0 - 1, y1)] : S{}; + const S a = (x0 > 0 && y0 > 0) ? data[idx(x0 - 1, y0 - 1)] : S{}; + + return d - b - c + a; +} + +template class PixelSum; +template class PixelSum; diff --git a/tests/PixelSumTest.cpp b/tests/pixel_sum_test.cpp similarity index 98% rename from tests/PixelSumTest.cpp rename to tests/pixel_sum_test.cpp index 8aed3fc..8a50a5b 100644 --- a/tests/PixelSumTest.cpp +++ b/tests/pixel_sum_test.cpp @@ -1,6 +1,6 @@ -#include "pixel_sum/PixelSum.h" -#include "support/TestUtility.h" -#include "support/TimeUtility.h" +#include "pixel_sum/pixel_sum.hpp" +#include "support/test_utility.hpp" +#include "support/time_utility.hpp" TEST(PixelSum_CaseInProblemDescription_Test, GivenPixelsInsideWindow_WhenGetPixelAverage_ThenExpectedAverageIsReturned) { diff --git a/tests/support/Common.h b/tests/support/common.hpp similarity index 100% rename from tests/support/Common.h rename to tests/support/common.hpp diff --git a/tests/support/TestUtility.h b/tests/support/test_utility.hpp similarity index 99% rename from tests/support/TestUtility.h rename to tests/support/test_utility.hpp index d00468a..a44b2e6 100644 --- a/tests/support/TestUtility.h +++ b/tests/support/test_utility.hpp @@ -1,6 +1,6 @@ #pragma once -#include "support/Common.h" +#include "support/common.hpp" #include #include diff --git a/tests/support/TimeUtility.h b/tests/support/time_utility.hpp similarity index 95% rename from tests/support/TimeUtility.h rename to tests/support/time_utility.hpp index 096f77e..31bcaca 100644 --- a/tests/support/TimeUtility.h +++ b/tests/support/time_utility.hpp @@ -1,6 +1,6 @@ #pragma once -#include "support/Common.h" +#include "support/common.hpp" #include #include