From 68cdbdd2ec4e21f667a49ae1dbbc2cebd833051c Mon Sep 17 00:00:00 2001 From: Samuel Debionne Date: Wed, 20 Jan 2021 23:09:58 +0100 Subject: [PATCH 01/51] Add inverse function for matrix3x2 (#527) --- .../boost/gil/extension/numeric/affine.hpp | 19 ++++++++++ test/extension/numeric/matrix3x2.cpp | 37 +++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/include/boost/gil/extension/numeric/affine.hpp b/include/boost/gil/extension/numeric/affine.hpp index ebef93afca..26054149a9 100644 --- a/include/boost/gil/extension/numeric/affine.hpp +++ b/include/boost/gil/extension/numeric/affine.hpp @@ -89,6 +89,25 @@ point transform(matrix3x2 const& mat, point const& src) return src * mat; } +/// Returns the inverse of the given affine transformation matrix +/// +/// \warning Floating point arithmetic, use Boost.Rational if precision maters +template +boost::gil::matrix3x2 inverse(boost::gil::matrix3x2 m) +{ + T const determinant = m.a * m.d - m.b * m.c; + + boost::gil::matrix3x2 res; + res.a = m.d / determinant; + res.b = -m.b / determinant; + res.c = -m.c / determinant; + res.d = m.a / determinant; + res.e = (m.c * m.f - m.d * m.e) / determinant; + res.f = (m.b * m.e - m.a * m.f) / determinant; + + return res; +} + }} // namespace boost::gil #endif diff --git a/test/extension/numeric/matrix3x2.cpp b/test/extension/numeric/matrix3x2.cpp index f1c19cd046..d3c34f4e48 100644 --- a/test/extension/numeric/matrix3x2.cpp +++ b/test/extension/numeric/matrix3x2.cpp @@ -16,8 +16,19 @@ namespace gil = boost::gil; -// FIXME: Remove when https://github.com/boostorg/core/issues/38 happens -#define BOOST_GIL_TEST_IS_CLOSE(a, b, epsilon) BOOST_TEST_LT(std::fabs((a) - (b)), (epsilon)) +// Tolerance predicate for floating point comparison to use with BOOST_TEST_WITH +template +struct with_tolerance +{ + with_tolerance(T tolerance) : tolerance(tolerance) {} + bool operator()(T lhs, T rhs) + { + return (std::abs(lhs - rhs) <= tolerance); + } + +private: + T tolerance; +}; namespace { constexpr double HALF_PI = 1.57079632679489661923; @@ -123,10 +134,10 @@ void test_matrix3x2_vector_multiplication() void test_matrix3x2_get_rotate() { auto m1 = gil::matrix3x2::get_rotate(HALF_PI); - BOOST_GIL_TEST_IS_CLOSE(m1.a, std::cos(HALF_PI), 0.03); + BOOST_TEST_WITH(m1.a, std::cos(HALF_PI), with_tolerance(0.03)); BOOST_TEST_EQ(m1.b, 1); BOOST_TEST_EQ(m1.c, -1); - BOOST_GIL_TEST_IS_CLOSE(m1.d, std::cos(HALF_PI), 0.03); + BOOST_TEST_WITH(m1.d, std::cos(HALF_PI), with_tolerance(0.03)); BOOST_TEST_EQ(m1.e, 0); BOOST_TEST_EQ(m1.f, 0); } @@ -173,6 +184,23 @@ void test_matrix3x2_transform() BOOST_TEST_EQ(v2.y, 4); } +void test_matrix3x2_inverse() +{ + using matrix_t = gil::matrix3x2; + using point_t = gil::point; + + matrix_t mo = matrix_t::get_translate(0, 16); + matrix_t mb = matrix_t::get_rotate(HALF_PI); + auto m = mo * mb; + + point_t p(10, 10); + point_t q = gil::transform(inverse(m), p); + point_t p2 = gil::transform(m, q); + + BOOST_TEST_WITH(p.x, p2.x, with_tolerance(1e-9)); + BOOST_TEST_WITH(p.y, p2.y, with_tolerance(1e-9)); +} + int main() { test_matrix3x2_default_constructor(); @@ -186,6 +214,7 @@ int main() test_matrix3x2_get_scale(); test_matrix3x2_get_translate(); test_matrix3x2_transform(); + test_matrix3x2_inverse(); return ::boost::report_errors(); } From 853bc1266b3d287bafbb571e12d00e01df6a8734 Mon Sep 17 00:00:00 2001 From: Giovanni Mascellani Date: Wed, 20 Jan 2021 23:11:26 +0100 Subject: [PATCH 02/51] Fix typo in copyright headers (#524) --- test/extension/toolbox/Jamfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extension/toolbox/Jamfile b/test/extension/toolbox/Jamfile index a68af63f38..a2a78f7bed 100644 --- a/test/extension/toolbox/Jamfile +++ b/test/extension/toolbox/Jamfile @@ -1,7 +1,7 @@ # Boost.GIL (Generic Image Library) - Toolbox tests # # Copyright (c) 2012 Christian Henning -# Copyright (c) 2012-202 Mateusz Loskot +# Copyright (c) 2012-2020 Mateusz Loskot # # Distributed under the Boost Software License, Version 1.0. # (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) From eef475e27a91df8c912fbf82f330d1af955743ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Thu, 21 Jan 2021 17:19:39 +0100 Subject: [PATCH 03/51] ci: Temporarily set B2_VERBOSE=0 for CircleCI Helping to investigate https://github.com/boostorg/build/issues/704 --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5779c9a34c..b56aaf123e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,6 +87,7 @@ env_gcc_cpp11_dbg: &env_gcc_cpp11_dbg - OPTIMIZ: "" - CXXSTD: "11" - LNKFLAG: "" + - B2_VERBOSE: "0" env_gcc_cpp11_opt_speed: &env_gcc_cpp11_opt_speed environment: @@ -95,6 +96,7 @@ env_gcc_cpp11_opt_speed: &env_gcc_cpp11_opt_speed - OPTIMIZ: "optimization=speed" - CXXSTD: "11" - LNKFLAG: "" + - B2_VERBOSE: "0" env_clang_cpp11_dbg: &env_clang_cpp11_dbg environment: @@ -103,6 +105,7 @@ env_clang_cpp11_dbg: &env_clang_cpp11_dbg - OPTIMIZ: "" - CXXSTD: "11" - LNKFLAG: "" + - B2_VERBOSE: "0" env_clang_cpp11_opt_speed: &env_clang_cpp11_opt_speed environment: @@ -111,6 +114,7 @@ env_clang_cpp11_opt_speed: &env_clang_cpp11_opt_speed - OPTIMIZ: "optimization=speed" - CXXSTD: "11" - LNKFLAG: "" + - B2_VERBOSE: "0" ############################################################################## # Build configurations From 470923be362881f5ea9bd72414a95690f8cceb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Fri, 22 Jan 2021 18:01:54 +0100 Subject: [PATCH 04/51] Fix default ctor of homogeneous_color_base for reference pixel elements (#542) If `Element` is a reference, then Element v{} is ill-formed. Refines #273 which aimed to correctly value-initialize channel and pixel value members --- include/boost/gil/color_base.hpp | 77 +++++++++++++++++++++++--------- test/core/pixel/test_fixture.hpp | 2 +- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/include/boost/gil/color_base.hpp b/include/boost/gil/color_base.hpp index 16609429a0..a05b409de8 100644 --- a/include/boost/gil/color_base.hpp +++ b/include/boost/gil/color_base.hpp @@ -79,8 +79,14 @@ struct homogeneous_color_base { using layout_t = Layout; - homogeneous_color_base() = default; - homogeneous_color_base(Element v) : v0_(v) {} + template + < + typename U = Element, + typename = typename std::enable_if::value>::type + > + homogeneous_color_base() : v0_{} {}; + + explicit homogeneous_color_base(Element v) : v0_(v) {} template homogeneous_color_base(homogeneous_color_base const& c) @@ -100,7 +106,7 @@ struct homogeneous_color_base operator Element() const { return v0_; } private: - Element v0_{}; + Element v0_; }; /// \brief A homogeneous color base holding two color elements @@ -111,8 +117,15 @@ struct homogeneous_color_base { using layout_t = Layout; - homogeneous_color_base() = default; + template + < + typename U = Element, + typename = typename std::enable_if::value>::type + > + homogeneous_color_base() : v0_{}, v1_{} {} + explicit homogeneous_color_base(Element v) : v0_(v), v1_(v) {} + homogeneous_color_base(Element v0, Element v1) : v0_(v0), v1_(v1) {} template @@ -174,8 +187,8 @@ struct homogeneous_color_base } private: - Element v0_{}; - Element v1_{}; + Element v0_; + Element v1_; }; /// \brief A homogeneous color base holding three color elements. @@ -186,8 +199,15 @@ struct homogeneous_color_base { using layout_t = Layout; - homogeneous_color_base() = default; + template + < + typename U = Element, + typename = typename std::enable_if::value>::type + > + homogeneous_color_base() : v0_{}, v1_{}, v2_{} {} + explicit homogeneous_color_base(Element v) : v0_(v), v1_(v), v2_(v) {} + homogeneous_color_base(Element v0, Element v1, Element v2) : v0_(v0), v1_(v1), v2_(v2) {} @@ -268,9 +288,9 @@ struct homogeneous_color_base } private: - Element v0_{}; - Element v1_{}; - Element v2_{}; + Element v0_; + Element v1_; + Element v2_; }; /// \brief A homogeneous color base holding four color elements. @@ -281,8 +301,15 @@ struct homogeneous_color_base { using layout_t = Layout; - homogeneous_color_base() = default; + template + < + typename U = Element, + typename = typename std::enable_if::value>::type + > + homogeneous_color_base() : v0_{}, v1_{}, v2_{}, v3_{} {} + explicit homogeneous_color_base(Element v) : v0_(v), v1_(v), v2_(v), v3_(v) {} + homogeneous_color_base(Element v0, Element v1, Element v2, Element v3) : v0_(v0), v1_(v1), v2_(v2), v3_(v3) {} @@ -377,10 +404,10 @@ struct homogeneous_color_base } private: - Element v0_{}; - Element v1_{}; - Element v2_{}; - Element v3_{}; + Element v0_; + Element v1_; + Element v2_; + Element v3_; }; /// \brief A homogeneous color base holding five color elements. @@ -391,7 +418,15 @@ struct homogeneous_color_base { using layout_t = Layout; - homogeneous_color_base() = default; + template + < + typename U = Element, + typename = typename std::enable_if::value>::type + > + homogeneous_color_base() + : v0_{}, v1_{}, v2_{}, v3_{}, v4_{} + {} + explicit homogeneous_color_base(Element v) : v0_(v), v1_(v), v2_(v), v3_(v), v4_(v) {} @@ -505,11 +540,11 @@ struct homogeneous_color_base } private: - Element v0_{}; - Element v1_{}; - Element v2_{}; - Element v3_{}; - Element v4_{}; + Element v0_; + Element v1_; + Element v2_; + Element v3_; + Element v4_; }; #if BOOST_WORKAROUND(BOOST_MSVC, >= 1400) diff --git a/test/core/pixel/test_fixture.hpp b/test/core/pixel/test_fixture.hpp index bf4e1ed8dc..c9ef03f32a 100644 --- a/test/core/pixel/test_fixture.hpp +++ b/test/core/pixel/test_fixture.hpp @@ -69,7 +69,7 @@ class pixel_value public: using type = Pixel; using pixel_t = type; - type pixel_{}; + type pixel_; pixel_value() = default; explicit pixel_value(pixel_t const& pixel) From cb5bc9d8c233374736118290f45ac08172fa835c Mon Sep 17 00:00:00 2001 From: Olzhas Zhumabek Date: Sat, 23 Jan 2021 04:55:17 +0600 Subject: [PATCH 05/51] =?UTF-8?q?Add=20Perona=E2=80=93Malik=20anisotropic?= =?UTF-8?q?=20diffusion=20algorithm=20(#500)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The output type must be floating point, thus a check was added to make sure it is the case. Though I had to add specific cases for float32_t as std::is_floating_point does not consider it a floating point type The accumulate part was wrong, it multiplied by delta_t on every sum, which is wrong Use 8 way nabla compute. This is just different discretization of Laplace operator https://en.wikipedia.org/wiki/Discrete_Laplace_operator Laplace stencils are now the same as in mathematical notation The new function will provide a uniform way to generate stencils by making sure directions are indexed properly Add only required stencil points: The 5 points Laplace stencil is now adding only required points and not assuming that others are zero --- example/Jamfile | 1 + example/anisotropic_diffusion.cpp | 128 ++++++ .../boost/gil/image_processing/diffusion.hpp | 421 ++++++++++++++++++ test/core/image_processing/CMakeLists.txt | 3 +- test/core/image_processing/Jamfile | 1 + .../anisotropic_diffusion.cpp | 304 +++++++++++++ test/core/test_fixture.hpp | 27 +- 7 files changed, 876 insertions(+), 9 deletions(-) create mode 100644 example/anisotropic_diffusion.cpp create mode 100644 include/boost/gil/image_processing/diffusion.hpp create mode 100644 test/core/image_processing/anisotropic_diffusion.cpp diff --git a/example/Jamfile b/example/Jamfile index fc33ce319c..02bb33163c 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -21,6 +21,7 @@ project local sources = adaptive_threshold.cpp affine.cpp + anisotropic_diffusion.cpp convolution.cpp convolve2d.cpp dynamic_image.cpp diff --git a/example/anisotropic_diffusion.cpp b/example/anisotropic_diffusion.cpp new file mode 100644 index 0000000000..03defb7566 --- /dev/null +++ b/example/anisotropic_diffusion.cpp @@ -0,0 +1,128 @@ +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace gil = boost::gil; + +void gray_version(std::string const& input_path, std::string const& output_path, + unsigned int iteration_count, float kappa) +{ + gil::gray8_image_t input; + gil::read_image(input_path, input, gil::png_tag{}); + auto input_view = gil::view(input); + + gil::gray32f_image_t gray(input.dimensions()); + auto gray_view = gil::view(gray); + + gil::transform_pixels(input_view, gray_view, [](const gil::gray8_pixel_t& p) { return p[0]; }); + double sum_before = 0; + gil::for_each_pixel(gray_view, [&sum_before](gil::gray32f_pixel_t p) { sum_before += p[0]; }); + gil::gray32f_image_t output(gray.dimensions()); + auto output_view = gil::view(output); + + // gil::anisotropic_diffusion(gray_view, output_view, iteration_count, {kappa, delta_t}); + gil::default_anisotropic_diffusion(gray_view, output_view, iteration_count, kappa); + double sum_after = 0; + gil::for_each_pixel(output_view, [&sum_after](gil::gray32f_pixel_t p) { sum_after += p[0]; }); + + gil::gray8_image_t true_output(output.dimensions()); + gil::transform_pixels(output_view, gil::view(true_output), + [](gil::gray32f_pixel_t p) { return static_cast(p[0]); }); + + gil::write_view(output_path, gil::view(true_output), gil::png_tag{}); + + std::cout << "sum of intensity before diffusion: " << sum_before << '\n' + << "sum of intensity after diffusion: " << sum_after << '\n' + << "difference: " << sum_after - sum_before << '\n'; +} + +void rgb_version(const std::string& input_path, const std::string& output_path, + unsigned int iteration_count, float kappa) +{ + gil::rgb8_image_t input; + gil::read_image(input_path, input, gil::png_tag{}); + auto input_view = gil::view(input); + + gil::rgb32f_image_t gray(input.dimensions()); + auto gray_view = gil::view(gray); + + gil::transform_pixels(input_view, gray_view, [](const gil::rgb8_pixel_t& p) { + return gil::rgb32f_pixel_t(p[0], p[1], p[2]); + }); + double sum_before[3] = {}; + gil::for_each_pixel(gray_view, [&sum_before](gil::rgb32f_pixel_t p) { + sum_before[0] += p[0]; + sum_before[1] += p[1]; + sum_before[2] += p[2]; + }); + gil::rgb32f_image_t output(gray.dimensions()); + auto output_view = gil::view(output); + + // gil::anisotropic_diffusion(gray_view, output_view, iteration_count, {kappa, delta_t}); + gil::default_anisotropic_diffusion(gray_view, output_view, iteration_count, kappa); + double sum_after[3] = {}; + gil::for_each_pixel(output_view, [&sum_after](gil::rgb32f_pixel_t p) { + sum_after[0] += p[0]; + sum_after[1] += p[1]; + sum_after[2] += p[2]; + }); + + gil::rgb8_image_t true_output(output.dimensions()); + gil::transform_pixels(output_view, gil::view(true_output), [](gil::rgb32f_pixel_t p) { + return gil::rgb8_pixel_t(static_cast(p[0]), static_cast(p[1]), + static_cast(p[2])); + }); + + gil::write_view(output_path, gil::view(true_output), gil::png_tag{}); + std::cout << "sum of intensity before diffusion: (" << sum_before[0] << ", " << sum_before[1] + << ", " << sum_before[2] << ")\n" + << "sum of intensity after diffusion: (" << sum_after[0] << ", " << sum_after[1] + << ", " << sum_after[2] << ")\n" + << "difference: (" << sum_after[0] - sum_before[0] << ", " + << sum_after[1] - sum_before[1] << ", " << sum_after[2] - sum_before[2] << ")\n"; +} + +int main(int argc, char* argv[]) +{ + if (argc != 6) + { + std::cerr << "usage: " << argv[0] + << " " + " \n"; + return -1; + } + std::string input_path = argv[1]; + std::string output_path = argv[2]; + std::string colorspace = argv[3]; + + unsigned int iteration_count = static_cast(std::stoul(argv[4])); + float kappa = std::stof(argv[5]); + if (colorspace == "gray") + { + gray_version(input_path, output_path, iteration_count, kappa); + } + else if (colorspace == "rgb") + { + rgb_version(input_path, output_path, iteration_count, kappa); + } + else + { + std::cerr << "unknown colorspace option passed (did you type gray with A?)\n"; + } +} diff --git a/include/boost/gil/image_processing/diffusion.hpp b/include/boost/gil/image_processing/diffusion.hpp new file mode 100644 index 0000000000..72f55a2ae4 --- /dev/null +++ b/include/boost/gil/image_processing/diffusion.hpp @@ -0,0 +1,421 @@ +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include "boost/gil/detail/math.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { namespace gil { +namespace conductivity { +struct perona_malik_conductivity +{ + double kappa; + template + Pixel operator()(Pixel input) + { + using channel_type = typename channel_type::type; + // C++11 doesn't seem to capture members + static_transform(input, input, [this](channel_type value) { + value /= kappa; + return std::exp(-std::abs(value)); + }); + + return input; + } +}; + +struct gaussian_conductivity +{ + double kappa; + template + Pixel operator()(Pixel input) + { + using channel_type = typename channel_type::type; + // C++11 doesn't seem to capture members + static_transform(input, input, [this](channel_type value) { + value /= kappa; + return std::exp(-value * value); + }); + + return input; + } +}; + +struct wide_regions_conductivity +{ + double kappa; + template + Pixel operator()(Pixel input) + { + using channel_type = typename channel_type::type; + // C++11 doesn't seem to capture members + static_transform(input, input, [this](channel_type value) { + value /= kappa; + return 1.0 / (1.0 + value * value); + }); + + return input; + } +}; + +struct more_wide_regions_conductivity +{ + double kappa; + template + Pixel operator()(Pixel input) + { + using channel_type = typename channel_type::type; + // C++11 doesn't seem to capture members + static_transform(input, input, [this](channel_type value) { + value /= kappa; + return 1.0 / std::sqrt((1.0 + value * value)); + }); + + return input; + } +}; +} // namespace diffusion + +/** + \brief contains discrete approximations of 2D Laplacian operator +*/ +namespace laplace_function { +// The functions assume clockwise enumeration of stencil points, as such +// NW North NE 0 1 2 (-1, -1) (0, -1) (+1, -1) +// West East ===> 7 3 ===> (-1, 0) (+1, 0) +// SW South SE 6 5 4 (-1, +1) (0, +1) (+1, +1) + +/** + \brief This function makes sure all Laplace functions enumerate + values in the same order and direction. + + The first element is difference North West direction, second in North, + and so on in clockwise manner. Leave element as zero if it is not + to be computed. +*/ +std::array get_directed_offsets() +{ + return {point_t{-1, -1}, point_t{0, -1}, point_t{+1, -1}, point_t{+1, 0}, + point_t{+1, +1}, point_t{0, +1}, point_t{-1, +1}, point_t{-1, 0}}; +} + +template +using stencil_type = std::array; + +/** + \brief 5 point stencil approximation of Laplacian + + Only main 4 directions are non-zero, the rest are zero +*/ +struct stencil_5points +{ + double delta_t = 0.25; + + template + stencil_type compute_laplace(SubImageView view, + point_t origin) + { + auto current = view(origin); + stencil_type stencil; + using channel_type = typename channel_type::type; + std::array offsets(get_directed_offsets()); + typename SubImageView::value_type zero_pixel; + static_fill(zero_pixel, 0); + for (std::size_t index = 0; index < offsets.size(); ++index) + { + if (index % 2 != 0) + { + static_transform(view(origin.x + offsets[index].x, origin.y + offsets[index].y), + current, stencil[index], std::minus{}); + } + else + { + stencil[index] = zero_pixel; + } + } + return stencil; + } + + template + Pixel reduce(const stencil_type& stencil) + { + auto first = stencil.begin(); + auto last = stencil.end(); + using channel_type = typename channel_type::type; + auto result = []() { + Pixel zero_pixel; + static_fill(zero_pixel, channel_type(0)); + return zero_pixel; + }(); + + for (std::size_t index : {1u, 3u, 5u, 7u}) + { + static_transform(result, stencil[index], result, std::plus{}); + } + Pixel delta_t_pixel; + static_fill(delta_t_pixel, delta_t); + static_transform(result, delta_t_pixel, result, std::multiplies{}); + + return result; + } +}; + +/** + \brief 9 point stencil approximation of Laplacian + + This is full 8 way approximation, though diagonal + elements are halved during reduction. +*/ +struct stencil_9points_standard +{ + double delta_t = 0.125; + + template + stencil_type compute_laplace(SubImageView view, + point_t origin) + { + stencil_type stencil; + auto out = stencil.begin(); + auto current = view(origin); + using channel_type = typename channel_type::type; + std::array offsets(get_directed_offsets()); + for (auto offset : offsets) + { + static_transform(view(origin.x + offset.x, origin.y + offset.y), current, *out++, + std::minus{}); + } + + return stencil; + } + + template + Pixel reduce(const stencil_type& stencil) + { + using channel_type = typename channel_type::type; + auto result = []() { + Pixel zero_pixel; + static_fill(zero_pixel, channel_type(0)); + return zero_pixel; + }(); + for (std::size_t index : {1u, 3u, 5u, 7u}) + { + static_transform(result, stencil[index], result, std::plus{}); + } + + for (std::size_t index : {0u, 2u, 4u, 6u}) + { + Pixel half_pixel; + static_fill(half_pixel, channel_type(1 / 2.0)); + static_transform(stencil[index], half_pixel, half_pixel, + std::multiplies{}); + static_transform(result, half_pixel, result, std::plus{}); + } + + Pixel delta_t_pixel; + static_fill(delta_t_pixel, delta_t); + static_transform(result, delta_t_pixel, result, std::multiplies{}); + + return result; + } +}; +} // namespace laplace_function + +namespace brightness_function { +using laplace_function::stencil_type; +struct identity +{ + template + stencil_type operator()(const stencil_type& stencil) + { + return stencil; + } +}; + +// TODO: Figure out how to implement color gradient brightness, as it +// seems to need dx and dy using sobel or scharr kernels + +struct rgb_luminance +{ + using pixel_type = rgb32f_pixel_t; + stencil_type operator()(const stencil_type& stencil) + { + stencil_type output; + std::transform(stencil.begin(), stencil.end(), output.begin(), [](const pixel_type& pixel) { + float32_t luminance = 0.2126f * pixel[0] + 0.7152f * pixel[1] + 0.0722f * pixel[2]; + pixel_type result_pixel; + static_fill(result_pixel, luminance); + return result_pixel; + }); + return output; + } +}; + +} // namespace brightness_function + +enum class matlab_connectivity +{ + minimal, + maximal +}; + +enum class matlab_conduction_method +{ + exponential, + quadratic +}; + +template +void classic_anisotropic_diffusion(const InputView& input, const OutputView& output, + unsigned int num_iter, double kappa) +{ + anisotropic_diffusion(input, output, num_iter, laplace_function::stencil_5points{}, + brightness_function::identity{}, + conductivity::perona_malik_conductivity{kappa}); +} + +template +void matlab_anisotropic_diffusion(const InputView& input, const OutputView& output, + unsigned int num_iter, double kappa, + matlab_connectivity connectivity, + matlab_conduction_method conduction_method) +{ + if (connectivity == matlab_connectivity::minimal) + { + if (conduction_method == matlab_conduction_method::exponential) + { + anisotropic_diffusion(input, output, num_iter, laplace_function::stencil_5points{}, + brightness_function::identity{}, + conductivity::gaussian_conductivity{kappa}); + } + else if (conduction_method == matlab_conduction_method::quadratic) + { + anisotropic_diffusion(input, output, num_iter, laplace_function::stencil_5points{}, + brightness_function::identity{}, + conductivity::gaussian_conductivity{kappa}); + } + else + { + throw std::logic_error("unhandled conduction method found"); + } + } + else if (connectivity == matlab_connectivity::maximal) + { + if (conduction_method == matlab_conduction_method::exponential) + { + anisotropic_diffusion(input, output, num_iter, laplace_function::stencil_5points{}, + brightness_function::identity{}, + conductivity::gaussian_conductivity{kappa}); + } + else if (conduction_method == matlab_conduction_method::quadratic) + { + anisotropic_diffusion(input, output, num_iter, laplace_function::stencil_5points{}, + brightness_function::identity{}, + conductivity::gaussian_conductivity{kappa}); + } + else + { + throw std::logic_error("unhandled conduction method found"); + } + } + else + { + throw std::logic_error("unhandled connectivity found"); + } +} + +template +void default_anisotropic_diffusion(const InputView& input, const OutputView& output, + unsigned int num_iter, double kappa) +{ + anisotropic_diffusion(input, output, num_iter, laplace_function::stencil_9points_standard{}, + brightness_function::identity{}, conductivity::gaussian_conductivity{kappa}); +} + +/// \brief Performs diffusion according to Perona-Malik equation +/// +/// WARNING: Output channel type must be floating point, +/// otherwise there will be loss in accuracy which most +/// probably will lead to incorrect results (input will be unchanged). +/// Anisotropic diffusion is a smoothing algorithm that respects +/// edge boundaries and can work as an edge detector if suitable +/// iteration count is set and grayscale image view is used +/// as an input +template +void anisotropic_diffusion(const InputView& input, const OutputView& output, unsigned int num_iter, + LaplaceStrategy laplace, BrightnessFunction brightness, + DiffusivityFunction diffusivity) +{ + using input_pixel_type = typename InputView::value_type; + using pixel_type = typename OutputView::value_type; + using channel_type = typename channel_type::type; + using computation_image = image; + const auto width = input.width(); + const auto height = input.height(); + const point_t dims(width, height); + const auto zero_pixel = []() { + pixel_type pixel; + static_fill(pixel, static_cast(0)); + + return pixel; + }(); + computation_image result_image(width + 2, height + 2, zero_pixel); + auto result = view(result_image); + computation_image scratch_result_image(width + 2, height + 2, zero_pixel); + auto scratch_result = view(scratch_result_image); + transform_pixels(input, subimage_view(result, 1, 1, width, height), + [](const input_pixel_type& pixel) { + pixel_type converted; + for (std::size_t i = 0; i < num_channels{}; ++i) + { + converted[i] = pixel[i]; + } + return converted; + }); + + for (unsigned int iteration = 0; iteration < num_iter; ++iteration) + { + for (std::ptrdiff_t relative_y = 0; relative_y < height; ++relative_y) + { + for (std::ptrdiff_t relative_x = 0; relative_x < width; ++relative_x) + { + auto x = relative_x + 1; + auto y = relative_y + 1; + auto stencil = laplace.compute_laplace(result, point_t(x, y)); + auto brightness_stencil = brightness(stencil); + laplace_function::stencil_type diffusivity_stencil; + std::transform(brightness_stencil.begin(), brightness_stencil.end(), + diffusivity_stencil.begin(), diffusivity); + laplace_function::stencil_type product_stencil; + std::transform(stencil.begin(), stencil.end(), diffusivity_stencil.begin(), + product_stencil.begin(), [](pixel_type lhs, pixel_type rhs) { + static_transform(lhs, rhs, lhs, std::multiplies{}); + return lhs; + }); + static_transform(result(x, y), laplace.reduce(product_stencil), + scratch_result(x, y), std::plus{}); + } + } + using std::swap; + swap(result, scratch_result); + } + + copy_pixels(subimage_view(result, 1, 1, width, height), output); +} + +}} // namespace boost::gil diff --git a/test/core/image_processing/CMakeLists.txt b/test/core/image_processing/CMakeLists.txt index 5ecd590629..2ad32f4f28 100644 --- a/test/core/image_processing/CMakeLists.txt +++ b/test/core/image_processing/CMakeLists.txt @@ -34,7 +34,8 @@ foreach(_name hessian box_filter median_filter - sobel_scharr) + sobel_scharr + anisotropic_diffusion) set(_test t_core_image_processing_${_name}) set(_target test_core_image_processing_${_name}) diff --git a/test/core/image_processing/Jamfile b/test/core/image_processing/Jamfile index d9593f0793..be4c755c02 100644 --- a/test/core/image_processing/Jamfile +++ b/test/core/image_processing/Jamfile @@ -19,3 +19,4 @@ run hessian.cpp ; run sobel_scharr.cpp ; run box_filter.cpp ; run median_filter.cpp ; +run anisotropic_diffusion.cpp ; diff --git a/test/core/image_processing/anisotropic_diffusion.cpp b/test/core/image_processing/anisotropic_diffusion.cpp new file mode 100644 index 0000000000..58a5c9d6ce --- /dev/null +++ b/test/core/image_processing/anisotropic_diffusion.cpp @@ -0,0 +1,304 @@ +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include "../test_fixture.hpp" +#include "boost/gil/algorithm.hpp" +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace gil = boost::gil; + +template +void diffusion_function_check(DiffusionFunction diffusion) +{ + using limits = std::numeric_limits; + using channel_type = typename gil::channel_type::type; + for (channel_type value = limits::min(); value <= limits::max(); ++value) + { + PixelType pixel; + gil::static_fill(pixel, value); + auto diffusivity_value = diffusion(pixel); + gil::static_for_each(diffusivity_value, [](channel_type channel_value) { + BOOST_TEST(0 <= channel_value && channel_value <= 1.0); + }); + } +} + +void brightness_function_test() +{ + std::vector rgb_pixels{ + gil::rgb32f_pixel_t(0, 11, 14), gil::rgb32f_pixel_t(2, 117, 200), + gil::rgb32f_pixel_t(223, 2, 180), gil::rgb32f_pixel_t(250, 254, 100), + gil::rgb32f_pixel_t(255, 255, 255), + }; + for (const auto& pixel : rgb_pixels) + { + boost::gil::laplace_function::stencil_type stencil; + std::fill(stencil.begin(), stencil.end(), pixel); + auto brightness_stencil = boost::gil::brightness_function::identity{}(stencil); + for (const auto& brightness_pixel : brightness_stencil) + { + BOOST_TEST(pixel == brightness_pixel); + } + } + + std::vector corresponding_luminance_values{8.878f, 98.5436f, 61.8362f, 242.0308f, 255}; + std::size_t index = 0; + for (const auto& pixel : rgb_pixels) + { + boost::gil::laplace_function::stencil_type stencil; + std::fill(stencil.begin(), stencil.end(), pixel); + auto brightness_stencil = boost::gil::brightness_function::rgb_luminance{}(stencil); + for (const auto& brightness_pixel : brightness_stencil) + { + gil::static_for_each(brightness_pixel, [&corresponding_luminance_values, + index](boost::gil::float32_t value) { + BOOST_TEST(std::abs(value - corresponding_luminance_values[index]) < 1.0f); + }); + } + ++index; + } + + std::vector gray_pixels{ + gil::gray32f_pixel_t(11), gil::gray32f_pixel_t(0), gil::gray32f_pixel_t(255), + gil::gray32f_pixel_t(123), gil::gray32f_pixel_t(17), + }; + for (const auto& pixel : gray_pixels) + { + boost::gil::laplace_function::stencil_type stencil; + std::fill(stencil.begin(), stencil.end(), pixel); + auto brightness_stencil = boost::gil::brightness_function::identity{}(stencil); + for (const auto& brightness_pixel : brightness_stencil) + { + BOOST_TEST(pixel == brightness_pixel); + } + } +} + +template +void heat_conservation_test(std::uint32_t seed) +{ + gil::test::fixture::random_value dist(seed, 0, + std::numeric_limits::max()); + + ImageType image(32, 32); + auto view = gil::view(image); + constexpr std::ptrdiff_t num_channels = gil::num_channels::value; + double before_diffusion[num_channels] = {0}; + for (auto& pixel : view) + { + for (std::size_t channel_index = 0; channel_index < num_channels; ++channel_index) + { + pixel[channel_index] = static_cast(dist()); + before_diffusion[channel_index] += pixel[channel_index]; + } + } + + OutputImageType output(32, 32); + auto output_view = gil::view(output); + gil::default_anisotropic_diffusion(view, output_view, 10, 5); + double after_diffusion[num_channels] = {0}; + for (const auto& pixel : output_view) + { + for (std::size_t channel_index = 0; channel_index < num_channels; ++channel_index) + { + after_diffusion[channel_index] += pixel[channel_index]; + } + } + + for (std::ptrdiff_t channel_index = 0; channel_index < num_channels; ++channel_index) + { + const auto percentage = + before_diffusion[channel_index] != 0 + ? std::abs(after_diffusion[channel_index] - before_diffusion[channel_index]) / + after_diffusion[channel_index] * 100.0 + : std::abs(after_diffusion[channel_index] - before_diffusion[channel_index]) / + after_diffusion[channel_index] * 100.0; +#ifdef BOOST_GIL_TEST_DEBUG + std::cout << percentage << ' '; +#endif + BOOST_TEST(percentage < 1); + } +#ifdef BOOST_GIL_TEST_DEBUG + std::cout << '\n'; +#endif +} + +template +void test_stencil_pixels(const gil::laplace_function::stencil_type& stencil, + const PixelType& reference) +{ + for (const auto& stencil_entry : stencil) + { + BOOST_TEST(stencil_entry == reference); + } +} + +template +void test_stencil(const gil::laplace_function::stencil_type& stencil, + const std::array& expected_values) +{ + for (std::size_t i = 0; i < 8; ++i) + { + PixelType expected_pixel; + gil::static_fill(expected_pixel, expected_values[i]); + BOOST_TEST(stencil[i] == expected_pixel); + } +} + +void laplace_functions_test() +{ + // sanity checks + auto zero_rgb_pixel = []() { + gil::rgb32f_pixel_t zero_pixel; + gil::static_fill(zero_pixel, 0); + return zero_pixel; + }(); + auto zero_gray_pixel = []() { + gil::gray32f_pixel_t zero_pixel; + gil::static_fill(zero_pixel, 0); + return zero_pixel; + }(); + + auto image_size = gil::point_t(16, 16); + gil::rgb32f_image_t rgb_image(image_size, zero_rgb_pixel); + gil::gray32f_image_t gray_image(image_size, zero_gray_pixel); + auto rgb = gil::view(rgb_image); + auto gray = gil::view(gray_image); + + auto s4way = gil::laplace_function::stencil_5points{}; + auto s8way = gil::laplace_function::stencil_9points_standard{}; + for (std::ptrdiff_t y = 1; y < image_size.y - 1; ++y) + { + for (std::ptrdiff_t x = 1; x < image_size.x - 1; ++x) + { + auto rgb_4way_stencil = s4way.compute_laplace(rgb, {x, y}); + auto gray_4way_stencil = s4way.compute_laplace(gray, {x, y}); + + auto rgb_8way_stencil = s8way.compute_laplace(rgb, {x, y}); + auto gray_8way_stencil = s8way.compute_laplace(gray, {x, y}); + + test_stencil_pixels(rgb_4way_stencil, zero_rgb_pixel); + test_stencil_pixels(rgb_8way_stencil, zero_rgb_pixel); + test_stencil_pixels(gray_4way_stencil, zero_gray_pixel); + test_stencil_pixels(gray_8way_stencil, zero_gray_pixel); + } + } + + // a predefined case + // 5 11 2 + // 17 25 58 + // 90 31 40 + + using rgb_pixel = gil::rgb32f_pixel_t; + rgb(1, 1) = rgb_pixel(5, 5, 5); + rgb(2, 1) = rgb_pixel(11, 11, 11); + rgb(3, 1) = rgb_pixel(2, 2, 2); + rgb(1, 2) = rgb_pixel(17, 17, 17); + rgb(2, 2) = rgb_pixel(25, 25, 25); + rgb(3, 2) = rgb_pixel(58, 58, 58); + rgb(1, 3) = rgb_pixel(90, 90, 90); + rgb(2, 3) = rgb_pixel(31, 31, 31); + rgb(3, 3) = rgb_pixel(40, 40, 40); + + using gray_pixel = gil::gray32f_pixel_t; + gray(1, 1) = gray_pixel(5); + gray(2, 1) = gray_pixel(11); + gray(3, 1) = gray_pixel(2); + gray(1, 2) = gray_pixel(17); + gray(2, 2) = gray_pixel(25); + gray(3, 2) = gray_pixel(58); + gray(1, 3) = gray_pixel(90); + gray(2, 3) = gray_pixel(31); + gray(3, 3) = gray_pixel(40); + + // 4 way stencil should be + // 0 -14 0 + // -8 33 + // 0 6 0 + + auto test_point = gil::point_t(2, 2); + std::array _4way_expected{0, -14, 0, 33, 0, 6, 0, -8}; + auto rgb_4way = s4way.compute_laplace(rgb, test_point); + test_stencil(rgb_4way, _4way_expected); + auto gray_4way = s4way.compute_laplace(gray, test_point); + test_stencil(gray_4way, _4way_expected); + + // 8 way stencil should be + // -20 -14 -23 + // -8 33 + // 65 6 15 + + std::array _8way_expected{-20, -14, -23, 33, 15, 6, 65, -8}; + auto rgb_8way = s8way.compute_laplace(rgb, test_point); + test_stencil(rgb_8way, _8way_expected); + auto gray_8way = s8way.compute_laplace(gray, test_point); + test_stencil(gray_8way, _8way_expected); + + // reduce result for 4 way should be (-14 - 8 + 6 + 33) * 0.25 = 4.25 + auto rgb_reduced_4way = s4way.reduce(rgb_4way); + gil::static_for_each(rgb_reduced_4way, + [](gil::float32_t value) { BOOST_TEST(value == 4.25f); }); + auto gray_reduced_4way = s4way.reduce(gray_4way); + gil::static_for_each(gray_reduced_4way, + [](gil::float32_t value) { BOOST_TEST(value == 4.25f); }); + + // reduce result for 8 way should be ((-20-23+15+65)*0.5+(-14+33+6-8)) * 0.125 = (18.5+17) * + // 0.125 = 4.4375 + auto rgb_reduced_8way = s8way.reduce(rgb_8way); + gil::static_for_each(rgb_reduced_8way, + [](gil::float32_t value) { BOOST_TEST(value == 4.4375); }); + auto gray_reduced_8way = s8way.reduce(gray_8way); + gil::static_for_each(gray_reduced_8way, + [](gil::float32_t value) { BOOST_TEST(value == 4.4375); }); +} + +int main() +{ + for (std::uint32_t seed = 0; seed < 100; ++seed) + { + heat_conservation_test(seed); + heat_conservation_test(seed); + } + + for (double kappa = 5; kappa <= 70; ++kappa) + { + diffusion_function_check( + gil::conductivity::perona_malik_conductivity{kappa}); + diffusion_function_check( + gil::conductivity::gaussian_conductivity{kappa}); + diffusion_function_check( + gil::conductivity::wide_regions_conductivity{kappa}); + diffusion_function_check( + gil::conductivity::more_wide_regions_conductivity{kappa}); + + diffusion_function_check( + gil::conductivity::perona_malik_conductivity{kappa}); + diffusion_function_check( + gil::conductivity::gaussian_conductivity{kappa}); + diffusion_function_check( + gil::conductivity::wide_regions_conductivity{kappa}); + diffusion_function_check( + gil::conductivity::more_wide_regions_conductivity{kappa}); + } + + brightness_function_test(); + + laplace_functions_test(); + + return boost::report_errors(); +} diff --git a/test/core/test_fixture.hpp b/test/core/test_fixture.hpp index 819b2cac22..d7a35abe9a 100644 --- a/test/core/test_fixture.hpp +++ b/test/core/test_fixture.hpp @@ -8,8 +8,8 @@ #ifndef BOOST_GIL_TEST_CORE_TEST_FIXTURE_HPP #define BOOST_GIL_TEST_CORE_TEST_FIXTURE_HPP -#include #include +#include #include #include @@ -60,19 +60,30 @@ template struct random_value { static_assert(std::is_integral::value, "T must be integral type"); - static constexpr auto range_min = std::numeric_limits::min(); - static constexpr auto range_max = std::numeric_limits::max(); - random_value() : rng_(rd_()), uid_(range_min, range_max) {} + random_value(T range_min = std::numeric_limits::min(), + T range_max = std::numeric_limits::max()) + : uid_(range_min, range_max) + {} + + random_value(std::uint32_t seed, T minimum, T maximum) : rng_(seed), uid_(minimum, maximum) + {} T operator()() { - auto value = uid_(rng_); - BOOST_ASSERT(range_min <= value && value <= range_max); - return static_cast(value); + return uid_(rng_); + } + + T range_min() const noexcept + { + return uid_.a(); + } + + T range_max() const noexcept + { + return uid_.b(); } - std::random_device rd_; std::mt19937 rng_; std::uniform_int_distribution::type> uid_; }; From b23c5e75b2999cf363a7f6af6b806c771de221b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sat, 23 Jan 2021 14:13:07 +0100 Subject: [PATCH 06/51] doc: Display GIL is header-only in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 055555f8aa..0c7367c59a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Documentation | AppVeyor | Azure Pipelines | Travis CI | CircleCI Boost.GIL is a part of the [Boost C++ Libraries](http://github.com/boostorg). -The Boost Generic Image Library (GIL) is a **C++11** library that abstracts image +The Boost Generic Image Library (GIL) is a **C++11** header-only library that abstracts image representations from algorithms and allows writing code that can work on a variety of images with performance similar to hand-writing for a specific image type. From 0e372a10bebd72cccbd76bb6ee2fb636c83210fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sat, 23 Jan 2021 14:16:23 +0100 Subject: [PATCH 07/51] docs: Display GIL is header-only in README --- doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 737cdaaba0..2a52bd6fa1 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,7 +1,7 @@ Boost Generic Image Library =========================== -The Generic Image Library (GIL) is a C++11 library that abstracts image +The Generic Image Library (GIL) is a C++11 header-only library that abstracts image representations from algorithms and allows writing code that can work on a variety of images with performance similar to hand-writing for a specific image type. From 3e729e5dae4c0ba6f407f6885bf6a18d525e8489 Mon Sep 17 00:00:00 2001 From: Debabrata Mandal <32168969+codejaeger@users.noreply.github.com> Date: Sun, 24 Jan 2021 04:32:51 +0530 Subject: [PATCH 08/51] Add histogram class and related functionality (#499) A new histogram class proposed with close suport for gil image constructs. Shift the stl support implmentation to extension to serve as example for overloading fill_histogram. Add cumulative histogram and histogram normalization. Co-authored-by: debabrata1 --- example/Jamfile | 1 + example/histogram.cpp | 71 +- example/tutorial_histogram.cpp | 49 ++ include/boost/gil.hpp | 1 + include/boost/gil/extension/histogram/std.hpp | 172 +++++ include/boost/gil/histogram.hpp | 717 ++++++++++++++++++ test/core/CMakeLists.txt | 1 + test/core/Jamfile | 1 + test/core/histogram/CMakeLists.txt | 37 + test/core/histogram/Jamfile | 21 + test/core/histogram/access.cpp | 35 + test/core/histogram/constructor.cpp | 31 + test/core/histogram/cumulative.cpp | 54 ++ test/core/histogram/dimension.cpp | 31 + test/core/histogram/fill.cpp | 282 +++++++ test/core/histogram/hash_tuple.cpp | 35 + test/core/histogram/helpers.cpp | 113 +++ test/core/histogram/is_compatible.cpp | 72 ++ test/core/histogram/key.cpp | 63 ++ test/core/histogram/sub_histogram.cpp | 58 ++ test/core/histogram/utilities.cpp | 214 ++++++ test/extension/CMakeLists.txt | 4 + test/extension/Jamfile | 1 + test/extension/histogram/CMakeLists.txt | 27 + test/extension/histogram/Jamfile | 11 + test/extension/histogram/histogram.cpp | 176 +++++ 26 files changed, 2244 insertions(+), 34 deletions(-) create mode 100644 example/tutorial_histogram.cpp create mode 100644 include/boost/gil/extension/histogram/std.hpp create mode 100644 include/boost/gil/histogram.hpp create mode 100644 test/core/histogram/CMakeLists.txt create mode 100644 test/core/histogram/Jamfile create mode 100644 test/core/histogram/access.cpp create mode 100644 test/core/histogram/constructor.cpp create mode 100644 test/core/histogram/cumulative.cpp create mode 100644 test/core/histogram/dimension.cpp create mode 100644 test/core/histogram/fill.cpp create mode 100644 test/core/histogram/hash_tuple.cpp create mode 100644 test/core/histogram/helpers.cpp create mode 100644 test/core/histogram/is_compatible.cpp create mode 100644 test/core/histogram/key.cpp create mode 100644 test/core/histogram/sub_histogram.cpp create mode 100644 test/core/histogram/utilities.cpp create mode 100644 test/extension/histogram/CMakeLists.txt create mode 100644 test/extension/histogram/Jamfile create mode 100644 test/extension/histogram/histogram.cpp diff --git a/example/Jamfile b/example/Jamfile index 02bb33163c..48b9e51aef 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -34,6 +34,7 @@ local sources = resize.cpp sobel_scharr.cpp threshold.cpp + tutorial_histogram.cpp x_gradient.cpp ; diff --git a/example/histogram.cpp b/example/histogram.cpp index b1b4896429..229b8d9b69 100644 --- a/example/histogram.cpp +++ b/example/histogram.cpp @@ -1,49 +1,52 @@ // -// Copyright 2005-2007 Adobe Systems Incorporated +// Copyright 2020 Debabrata Mandal // // Distributed under the Boost Software License, Version 1.0 // See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt // -#include -#include -#include -#include +#include +#include +#include -// Example file to demonstrate a way to compute histogram +#include using namespace boost::gil; -template -void gray_image_hist(GrayView const& img_view, R& hist) -{ - for (auto it = img_view.begin(); it != img_view.end(); ++it) - ++hist[*it]; - - // Alternatively, prefer the algorithm with lambda - // for_each_pixel(img_view, [&hist](gray8_pixel_t const& pixel) { - // ++hist[pixel]; - // }); -} - -template -void get_hist(const V& img_view, R& hist) { - gray_image_hist(color_converted_view(img_view), hist); -} - -int main() { - rgb8_image_t img; - read_image("test.jpg", img, jpeg_tag()); +/* +This file explains how to use the histogram class and some of its features +that can be applied for a variety of tasks. +*/ - int histogram[256]; - std::fill(histogram,histogram + 256, 0); - get_hist(const_view(img), histogram); - - std::fstream histo_file("out-histogram.txt", std::ios::out); - for(std::size_t ii = 0; ii < 256; ++ii) - histo_file << histogram[ii] << std::endl; - histo_file.close(); +int main() +{ + // Create a histogram class. Use uint or unsigned short as the default axes type in most cases. + histogram h; + + // Fill histogram with GIL images (of any color space) + gray8_image_t g; + read_image("test_adaptive.png", g, png_tag{}); + + fill_histogram + ( + view(g), // Input image view + h, // Histogram to be filled + 1, // Histogram bin widths + false, // Specify whether to accumulate over the values already present in h (default = false) + true, // Specify whether to have a sparse or continuous histogram (default = true) + false, // Specify if image mask is to be specified + {{}}, // Mask as a 2D vector. Used only if prev argument specified + {0}, // Lower limit on the values in histogram (default numeric_limit::min() on axes) + {255}, // Upper limit on the values in histogram (default numeric_limit::max() on axes) + true // Use specified limits if this is true (default is false) + ); + + // Normalize the histogram + h.normalize(); + + // Get a cumulative histogram from the histogram + auto h2 = cumulative_histogram(h); return 0; } diff --git a/example/tutorial_histogram.cpp b/example/tutorial_histogram.cpp new file mode 100644 index 0000000000..b1b4896429 --- /dev/null +++ b/example/tutorial_histogram.cpp @@ -0,0 +1,49 @@ +// +// Copyright 2005-2007 Adobe Systems Incorporated +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// +#include +#include + +#include +#include + +// Example file to demonstrate a way to compute histogram + +using namespace boost::gil; + +template +void gray_image_hist(GrayView const& img_view, R& hist) +{ + for (auto it = img_view.begin(); it != img_view.end(); ++it) + ++hist[*it]; + + // Alternatively, prefer the algorithm with lambda + // for_each_pixel(img_view, [&hist](gray8_pixel_t const& pixel) { + // ++hist[pixel]; + // }); +} + +template +void get_hist(const V& img_view, R& hist) { + gray_image_hist(color_converted_view(img_view), hist); +} + +int main() { + rgb8_image_t img; + read_image("test.jpg", img, jpeg_tag()); + + int histogram[256]; + std::fill(histogram,histogram + 256, 0); + get_hist(const_view(img), histogram); + + std::fstream histo_file("out-histogram.txt", std::ios::out); + for(std::size_t ii = 0; ii < 256; ++ii) + histo_file << histogram[ii] << std::endl; + histo_file.close(); + + return 0; +} diff --git a/include/boost/gil.hpp b/include/boost/gil.hpp index 5fc7d15819..b5db505d9b 100644 --- a/include/boost/gil.hpp +++ b/include/boost/gil.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/gil/extension/histogram/std.hpp b/include/boost/gil/extension/histogram/std.hpp new file mode 100644 index 0000000000..58dcae80a9 --- /dev/null +++ b/include/boost/gil/extension/histogram/std.hpp @@ -0,0 +1,172 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#ifndef BOOST_GIL_EXTENSION_HISTOGRAM_STL_HISTOGRAM_HPP +#define BOOST_GIL_EXTENSION_HISTOGRAM_STL_HISTOGRAM_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace boost { namespace gil { + +////////////////////////////////////////////////////////// +/// Histogram extension for STL container +////////////////////////////////////////////////////////// +/// \defgroup Histogram - STL Containers +/// \brief Collection of functions to provide histogram support in GIL using Standard +/// Template Library Containers +/// The conversion from Boost.GIL images to compatible histograms are provided. The supported +/// container types would be std::vector, std::array, std::map. +/// +/// Some general constraints on STL extension:- +/// 1. Supports only 1D histogram. +/// 2. Cannot use signed images with compatible random access containers. +/// 3. Automatic resize of std::array in case of shortage of bins, to ensure +/// correctness comes before performance. +/// 4. Container key type (if exists) has to be one of std::integral types to be +/// GIL compatible. +/// 5. Container value type has to be of std::arithmetic types. +/// + +/// +/// \ingroup Histogram - STL Containers +/// \brief Overload for std::vector of fill_histogram +/// +template +void fill_histogram(SrcView const& srcview, std::vector& histogram, bool accumulate = false) +{ + gil_function_requires>(); + static_assert(std::is_arithmetic::value, "Improper container type for images."); + static_assert( + std::is_unsigned::type>::value, + "Improper container type for signed images."); + + using channel_t = typename channel_type::type; + using pixel_t = pixel; + + if (!accumulate) + histogram.clear(); + histogram.resize(std::numeric_limits::max() + 1); + + for_each_pixel(color_converted_view(srcview), [&](pixel_t const& p) { + ++histogram[static_cast(p)]; + }); +} + +/// \ingroup Histogram - STL Containers +/// \brief Overload for std::array of fill_histogram +/// +template +void fill_histogram(SrcView const& srcview, std::array& histogram, bool accumulate = false) +{ + gil_function_requires>(); + static_assert(std::is_arithmetic::value && N > 0, "Improper container type for images."); + static_assert( + std::is_unsigned::type>::value, + "Improper container type for signed images."); + + using channel_t = typename channel_type::type; + using pixel_t = pixel; + + const size_t pixel_max = std::numeric_limits::max(); + const float scale = (histogram.size() - 1.0f) / pixel_max; + + if (!accumulate) + std::fill(std::begin(histogram), std::end(histogram), 0); + + for_each_pixel(color_converted_view(srcview), [&](pixel_t const& p) { + ++histogram[static_cast(p * scale)]; + }); +} + +/// \ingroup Histogram - STL Containers +/// \brief Overload for std::map of fill_histogram +/// +template +void fill_histogram(SrcView const& srcview, std::map& histogram, bool accumulate = false) +{ + gil_function_requires>(); + static_assert( + std::is_arithmetic::value && std::is_integral::value, + "Improper container type for images."); + + using channel_t = typename channel_type::type; + using pixel_t = pixel; + + if (!accumulate) + histogram.clear(); + + for_each_pixel(color_converted_view(srcview), [&](pixel_t const& p) { + ++histogram[static_cast(p)]; + }); +} + +/// \ingroup Histogram - STL Containers +/// \brief Overload for std::vector of cumulative_histogram +/// +template +std::vector cumulative_histogram(std::vector& hist) +{ + std::vector cumulative_hist(hist.size()); + static_assert(std::is_arithmetic::value, "Improper container type for images."); + T cumulative_counter = 0; + for (std::size_t i = 0; i < hist.size(); i++) + { + cumulative_counter += hist[i]; + cumulative_hist[i] = cumulative_counter; + } + return cumulative_hist; +} + +/// \ingroup Histogram - STL Containers +/// \brief Overload for std::array of cumulative_histogram +/// +template +std::array cumulative_histogram(std::array& histogram) +{ + std::array cumulative_hist; + static_assert(std::is_arithmetic::value && N > 0, "Improper container type for images."); + T cumulative_counter = 0; + for (std::size_t i = 0; i < N; i++) + { + cumulative_counter += histogram[i]; + cumulative_hist[i] = cumulative_counter; + } + return cumulative_hist; +} + +/// \ingroup Histogram - STL Containers +/// \brief Overload for std::map of cumulative_histogram +/// +template +std::map cumulative_histogram(std::map& histogram) +{ + std::map cumulative_hist; + static_assert( + std::is_arithmetic::value && std::is_integral::value, + "Improper container type for images."); + T2 cumulative_counter = 0; + for (auto const& it : histogram) + { + cumulative_counter += it.second; + cumulative_hist[it.first] = cumulative_counter; + } + return cumulative_hist; +} + +}} // namespace boost::gil + +#endif diff --git a/include/boost/gil/histogram.hpp b/include/boost/gil/histogram.hpp new file mode 100644 index 0000000000..bdbf9a6d7b --- /dev/null +++ b/include/boost/gil/histogram.hpp @@ -0,0 +1,717 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#ifndef BOOST_GIL_HISTOGRAM_HPP +#define BOOST_GIL_HISTOGRAM_HPP + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace boost { namespace gil { + +////////////////////////////////////////////////////////// +/// Histogram +////////////////////////////////////////////////////////// +/// \defgroup Histogram Histogram +/// \brief Contains description of the boost.gil.histogram class, extensions provided in place +/// of the default class, algorithms over the histogram class (both extensions and the +/// default class) +/// + +namespace detail { + +/// \defgroup Histogram-Helpers Histogram-Helpers +/// \brief Helper implementations supporting the histogram class. + +/// \ingroup Histogram-Helpers +/// +template +inline typename std::enable_if::type + hash_tuple_impl(std::size_t&, std::tuple const&) +{ +} + +/// \ingroup Histogram-Helpers +/// +template +inline typename std::enable_if::type + hash_tuple_impl(std::size_t& seed, std::tuple const& t) +{ + boost::hash_combine(seed, std::get(t)); + hash_tuple_impl(seed, t); +} + +/// \ingroup Histogram-Helpers +/// \brief Functor provided for the hashing of tuples. +/// The following approach makes use hash_combine from +/// boost::container_hash. Although there is a direct hashing +/// available for tuples, this approach will ease adopting in +/// future to a std::hash_combine. In case std::hash extends +/// support to tuples this functor as well as the helper +/// implementation hash_tuple_impl can be removed. +/// +template +struct hash_tuple +{ + std::size_t operator()(std::tuple const& t) const + { + std::size_t seed = 0; + hash_tuple_impl<0>(seed, t); + return seed; + } +}; + +/// \ingroup Histogram-Helpers +/// \todo With C++14 and using auto we don't need the decltype anymore +/// +template +auto pixel_to_tuple(Pixel const& p, boost::mp11::index_sequence) + -> decltype(std::make_tuple(p[I]...)) +{ + return std::make_tuple(p[I]...); +} + +/// \ingroup Histogram-Helpers +/// \todo With C++14 and using auto we don't need the decltype anymore +/// +template +auto tuple_to_tuple(Tuple const& t, boost::mp11::index_sequence) + -> decltype(std::make_tuple(std::get(t)...)) +{ + return std::make_tuple(std::get(t)...); +} + +/// \ingroup Histogram-Helpers +/// +template +bool tuple_compare(Tuple const& t1, Tuple const& t2, boost::mp11::index_sequence) +{ + std::array::value> comp_list; + comp_list = {std::get(t1) <= std::get(t2)...}; + bool comp = true; + for (std::size_t i = 0; i < comp_list.size(); i++) + { + comp = comp & comp_list[i]; + } + return comp; +} + +/// \ingroup Histogram-Helpers +/// \brief Compares 2 tuples and outputs t1 <= t2 +/// Comparison is not in a lexicographic manner but on every element of the tuple hence +/// (2, 2) > (1, 3) evaluates to false +/// +template +bool tuple_compare(Tuple const& t1, Tuple const& t2) +{ + std::size_t const tuple_size = std::tuple_size::value; + auto index_list = boost::mp11::make_index_sequence{}; + return tuple_compare(t1, t2, index_list); +} + +/// \ingroup Histogram-Helpers +/// \brief Provides equivalent of std::numeric_limits for type std::tuple +/// tuple_limit gets called with only tuples having integral elements +/// +template +struct tuple_limit +{ + static constexpr Tuple min() + { + return min_impl(boost::mp11::make_index_sequence::value>{}); + } + static constexpr Tuple max() + { + return max_impl(boost::mp11::make_index_sequence::value>{}); + } + +private: + template + static constexpr Tuple min_impl(boost::mp11::index_sequence) + { + return std::make_tuple( + std::numeric_limits::type>::min()...); + } + + template + static constexpr Tuple max_impl(boost::mp11::index_sequence) + { + return std::make_tuple( + std::numeric_limits::type>::max()...); + } +}; + +/// \ingroup Histogram-Helpers +/// \brief Filler is used to fill the histogram class with all values between a specified range +/// This functor is used when sparsefill is false, since all the keys need to be present +/// in that case. +/// Currently on 1D implementation is available, extend by adding specialization for 2D +/// and higher dimensional cases. +/// +template +struct filler +{ + template + void operator()(Container&, Tuple&, Tuple&, std::size_t) + { + } +}; + +/// \ingroup Histogram-Helpers +/// \brief Specialisation for 1D histogram. +template <> +struct filler<1> +{ + template + void operator()(Container& hist, Tuple& lower, Tuple& upper, std::size_t bin_width = 1) + { + for (auto i = std::get<0>(lower); std::get<0>(upper) - i >= bin_width; i += bin_width) + { + hist(i / bin_width) = 0; + } + hist(std::get<0>(upper) / bin_width) = 0; + } +}; + +} //namespace detail + +/// +/// \class boost::gil::histogram +/// \ingroup Histogram +/// \brief Default histogram class provided by boost::gil. +/// +/// The class inherits over the std::unordered_map provided by STL. A complete example/tutorial +/// of how to use the class resides in the docs. +/// Simple calling syntax for a 3D dimensional histogram : +/// \code +/// histogram h; +/// h(1, 1, 1) = 0; +/// \endcode +/// This is just a starter to what all can be achieved with it, refer to the docs for the +/// full demo. +/// +template +class histogram : public std::unordered_map, double, detail::hash_tuple> +{ + using base_t = std::unordered_map, double, detail::hash_tuple>; + using bin_t = boost::mp11::mp_list; + using key_t = typename base_t::key_type; + using mapped_t = typename base_t::mapped_type; + using value_t = typename base_t::value_type; + +public: + histogram() = default; + + /// \brief Returns the number of dimensions(axes) the class supports. + static constexpr std::size_t dimension() + { + return std::tuple_size::value; + } + + /// \brief Returns bin value corresponding to specified tuple + mapped_t& operator()(T... indices) + { + auto key = std::make_tuple(indices...); + std::size_t const index_dimension = std::tuple_size>::value; + std::size_t const histogram_dimension = dimension(); + static_assert(histogram_dimension == index_dimension, "Dimensions do not match."); + + return base_t::operator[](key); + } + + /// \brief Checks if 2 histograms are equal. Ignores type, and checks if + /// the keys (after type casting) match. + template + bool equals(OtherType const& otherhist) const + { + bool check = (dimension() == otherhist.dimension()); + + using other_value_t = typename OtherType::value_type; + std::for_each(otherhist.begin(), otherhist.end(), [&](other_value_t const& v) { + key_t key = key_from_tuple(v.first); + if (base_t::find(key) != base_t::end()) + { + check = check & (base_t::at(key) == otherhist.at(v.first)); + } + else + { + check = false; + } + }); + return check; + } + + /// \brief Checks if the histogram class is compatible to be used with + /// a GIL image type + static constexpr bool is_pixel_compatible() + { + using bin_types = boost::mp11::mp_list; + return boost::mp11::mp_all_of::value; + } + + /// \brief Checks if the histogram class is compatible to be used with + /// the specified tuple type + template + bool is_tuple_compatible(Tuple const&) + { + std::size_t const tuple_size = std::tuple_size::value; + std::size_t const histogram_size = dimension(); + // TODO : Explore consequence of using if-constexpr + using sequence_type = typename std::conditional + < + tuple_size >= histogram_size, + boost::mp11::make_index_sequence, + boost::mp11::make_index_sequence + >::type; + + if (is_tuple_size_compatible()) + return is_tuple_type_compatible(sequence_type{}); + else + return false; + } + + /// \brief Returns a key compatible to be used as the histogram key + /// from the input tuple + template + key_t key_from_tuple(Tuple const& t) const + { + using index_list = boost::mp11::mp_list_c; + std::size_t const index_list_size = boost::mp11::mp_size::value; + std::size_t const tuple_size = std::tuple_size::value; + std::size_t const histogram_dimension = dimension(); + + static_assert( + ((index_list_size != 0 && index_list_size == histogram_dimension) || + (tuple_size == histogram_dimension)), + "Tuple and histogram key of different sizes"); + + using new_index_list = typename std::conditional + < + index_list_size == 0, + boost::mp11::mp_list_c, + index_list + >::type; + + std::size_t const min = + boost::mp11::mp_min_element::value; + + std::size_t const max = + boost::mp11::mp_max_element::value; + + static_assert((0 <= min && max < tuple_size) || index_list_size == 0, "Index out of Range"); + + using seq1 = boost::mp11::make_index_sequence; + using seq2 = boost::mp11::index_sequence; + // TODO : Explore consequence of using if-constexpr + using sequence_type = typename std::conditional::type; + + auto key = detail::tuple_to_tuple(t, sequence_type{}); + static_assert( + is_tuple_type_compatible(seq1{}), + "Tuple type and histogram type not compatible."); + + return make_histogram_key(key, seq1{}); + } + + /// \brief Returns a histogram compatible key from the input pixel which + /// can be directly used + template + key_t key_from_pixel(Pixel const& p) const + { + using index_list = boost::mp11::mp_list_c; + std::size_t const index_list_size = boost::mp11::mp_size::value; + std::size_t const pixel_dimension = num_channels::value; + std::size_t const histogram_dimension = dimension(); + + static_assert( + ((index_list_size != 0 && index_list_size == histogram_dimension) || + (index_list_size == 0 && pixel_dimension == histogram_dimension)) && + is_pixel_compatible(), + "Pixels and histogram key are not compatible."); + + using new_index_list = typename std::conditional + < + index_list_size == 0, + boost::mp11::mp_list_c, + index_list + >::type; + + std::size_t const min = + boost::mp11::mp_min_element::value; + + std::size_t const max = + boost::mp11::mp_max_element::value; + + static_assert( + (0 <= min && max < pixel_dimension) || index_list_size == 0, "Index out of Range"); + + using seq1 = boost::mp11::make_index_sequence; + using seq2 = boost::mp11::index_sequence; + using sequence_type = typename std::conditional::type; + + auto key = detail::pixel_to_tuple(p, sequence_type{}); + return make_histogram_key(key, seq1{}); + } + + /// \brief Return nearest smaller key to specified histogram key + key_t nearest_key(key_t const& k) const + { + using check_list = boost::mp11::mp_list...>; + static_assert( + boost::mp11::mp_all_of::value, + "Keys are not comparable."); + auto nearest_k = k; + if (base_t::find(k) != base_t::end()) + { + return nearest_k; + } + else + { + bool once = true; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + if (v.first <= k) + { + if (once) + { + once = !once; + nearest_k = v.first; + } + else if (nearest_k < v.first) + nearest_k = v.first; + } + }); + return nearest_k; + } + } + + /// \brief Fills the histogram with the input image view + template + void fill( + SrcView const& srcview, + std::size_t bin_width = 1, + bool applymask = false, + std::vector> mask = {}, + key_t lower = key_t(), + key_t upper = key_t(), + bool setlimits = false) + { + gil_function_requires>(); + using channel_t = typename channel_type::type; + + for (std::ptrdiff_t src_y = 0; src_y < srcview.height(); ++src_y) + { + auto src_it = srcview.row_begin(src_y); + for (std::ptrdiff_t src_x = 0; src_x < srcview.width(); ++src_x) + { + if (applymask && !mask[src_y][src_x]) + continue; + auto scaled_px = src_it[src_x]; + static_for_each(scaled_px, [&](channel_t& ch) { + ch = ch / bin_width; + }); + auto key = key_from_pixel(scaled_px); + if (!setlimits || + (detail::tuple_compare(lower, key) && detail::tuple_compare(key, upper))) + base_t::operator[](key)++; + } + } + } + + /// \brief Can return a subset or a mask over the current histogram + template + histogram sub_histogram(Tuple const& t1, Tuple const& t2) + { + using index_list = boost::mp11::mp_list_c; + std::size_t const index_list_size = boost::mp11::mp_size::value; + std::size_t const histogram_dimension = dimension(); + + std::size_t const min = + boost::mp11::mp_min_element::value; + + std::size_t const max = + boost::mp11::mp_max_element::value; + + static_assert( + (0 <= min && max < histogram_dimension) && index_list_size < histogram_dimension, + "Index out of Range"); + + using seq1 = boost::mp11::make_index_sequence; + using seq2 = boost::mp11::index_sequence; + + static_assert( + is_tuple_type_compatible(seq1{}), + "Tuple type and histogram type not compatible."); + + auto low = make_histogram_key(t1, seq1{}); + auto low_key = detail::tuple_to_tuple(low, seq2{}); + auto high = make_histogram_key(t2, seq1{}); + auto high_key = detail::tuple_to_tuple(high, seq2{}); + + histogram sub_h; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& k) { + auto tmp_key = detail::tuple_to_tuple(k.first, seq2{}); + if (low_key <= tmp_key && tmp_key <= high_key) + sub_h[k.first] += base_t::operator[](k.first); + }); + return sub_h; + } + + /// \brief Returns a sub-histogram over specified axes + template + histogram>...> sub_histogram() + { + using index_list = boost::mp11::mp_list_c; + std::size_t const index_list_size = boost::mp11::mp_size::value; + std::size_t const histogram_dimension = dimension(); + + std::size_t const min = + boost::mp11::mp_min_element::value; + + std::size_t const max = + boost::mp11::mp_max_element::value; + + static_assert( + (0 <= min && max < histogram_dimension) && index_list_size < histogram_dimension, + "Index out of Range"); + + histogram>...> sub_h; + + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + auto sub_key = + detail::tuple_to_tuple(v.first, boost::mp11::index_sequence{}); + sub_h[sub_key] += base_t::operator[](v.first); + }); + return sub_h; + } + + /// \brief Normalize this histogram class + void normalize() + { + double sum = 0.0; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + sum += v.second; + }); + // std::cout<<(long int)sum<<"asfe"; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + base_t::operator[](v.first) = v.second / sum; + }); + } + + /// \brief Return the sum count of all bins + double sum() const + { + double sum = 0.0; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + sum += v.second; + }); + return sum; + } + + /// \brief Return the minimum key in histogram + key_t min_key() const + { + key_t min_key = base_t::begin()->first; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + if (v.first < min_key) + min_key = v.first; + }); + return min_key; + } + + /// \brief Return the maximum key in histogram + key_t max_key() const + { + key_t max_key = base_t::begin()->first; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + if (v.first > max_key) + max_key = v.first; + }); + return max_key; + } + + /// \brief Return sorted keys in a vector + std::vector sorted_keys() const + { + std::vector sorted_keys; + std::for_each(base_t::begin(), base_t::end(), [&](value_t const& v) { + sorted_keys.push_back(v.first); + }); + std::sort(sorted_keys.begin(), sorted_keys.end()); + return sorted_keys; + } + +private: + template + key_t make_histogram_key(Tuple const& t, boost::mp11::index_sequence) const + { + return std::make_tuple( + static_cast>>( + std::get(t))...); + } + + template + static constexpr bool is_tuple_type_compatible(boost::mp11::index_sequence) + { + using tp = boost::mp11::mp_list + < + typename std::is_convertible + < + boost::mp11::mp_at>, + typename std::tuple_element::type + >::type... + >; + return boost::mp11::mp_all_of::value; + } + + template + static constexpr bool is_tuple_size_compatible() + { + return (std::tuple_size::value == dimension()); + } +}; + +/// +/// \fn void fill_histogram +/// \ingroup Histogram Algorithms +/// \tparam SrcView Input image view +/// \tparam Container Input histogram container +/// \brief Overload this function to provide support for boost::gil::histogram or +/// any other external histogram +/// +/// Example : +/// \code +/// histogram h; +/// fill_histogram(view(img), h); +/// \endcode +/// +template +void fill_histogram(SrcView const&, Container&); + +/// +/// \fn void fill_histogram +/// \ingroup Histogram Algorithms +/// @param srcview Input Input image view +/// @param hist Output Histogram to be filled +/// @param bin_width Input Specify the bin widths for the histogram. +/// @param accumulate Input Specify whether to accumulate over the values already present in h (default = false) +/// @param sparsaefill Input Specify whether to have a sparse or continuous histogram (default = true) +/// @param applymask Input Specify if image mask is to be specified +/// @param mask Input Mask as a 2D vector. Used only if prev argument specified +/// @param lower Input Lower limit on the values in histogram (default numeric_limit::min() on axes) +/// @param upper Input Upper limit on the values in histogram (default numeric_limit::max() on axes) +/// @param setlimits Input Use specified limits if this is true (default is false) +/// \brief Overload version of fill_histogram +/// +/// Takes a third argument to determine whether to clear container before filling. +/// For eg, when there is a need to accumulate the histograms do +/// \code +/// fill_histogram(view(img), hist, true); +/// \endcode +/// +template +void fill_histogram( + SrcView const& srcview, + histogram& hist, + std::size_t bin_width = 1, + bool accumulate = false, + bool sparsefill = true, + bool applymask = false, + std::vector> mask = {}, + typename histogram::key_type lower = + detail::tuple_limit::key_type>::min(), + typename histogram::key_type upper = + detail::tuple_limit::key_type>::max(), + bool setlimits = false) +{ + if (!accumulate) + hist.clear(); + + detail::filler::dimension()> f; + if (!sparsefill) + f(hist, lower, upper, bin_width); + + hist.template fill(srcview, bin_width, applymask, mask, lower, upper, setlimits); +} + +/// +/// \fn void cumulative_histogram(Container&) +/// \ingroup Histogram Algorithms +/// \tparam Container Input histogram container +/// \brief Optionally overload this function with any external histogram class +/// +/// Cumulative histogram is calculated over any arbitrary dimensional +/// histogram. The only tradeoff could be the runtime complexity which in +/// the worst case would be max( #pixel_values , #bins ) * #dimensions. +/// For single dimensional histograms the complexity has been brought down to +/// #bins * log( #bins ) by sorting the keys and then calculating the cumulative version. +/// +template +Container cumulative_histogram(Container const&); + +template +histogram cumulative_histogram(histogram const& hist) +{ + using check_list = boost::mp11::mp_list...>; + static_assert( + boost::mp11::mp_all_of::value, + "Cumulative histogram not possible of this type"); + + using histogram_t = histogram; + using pair_t = std::pair; + using value_t = typename histogram_t::value_type; + + histogram_t cumulative_hist; + std::size_t const dims = histogram_t::dimension(); + if (dims == 1) + { + std::vector sorted_keys(hist.size()); + std::size_t counter = 0; + std::for_each(hist.begin(), hist.end(), [&](value_t const& v) { + sorted_keys[counter++] = std::make_pair(v.first, v.second); + }); + std::sort(sorted_keys.begin(), sorted_keys.end()); + auto cumulative_counter = static_cast(0); + for (std::size_t i = 0; i < sorted_keys.size(); ++i) + { + cumulative_counter += sorted_keys[i].second; + cumulative_hist[(sorted_keys[i].first)] = cumulative_counter; + } + } + else + { + std::for_each(hist.begin(), hist.end(), [&](value_t const& v1) { + auto cumulative_counter = static_cast(0); + std::for_each(hist.begin(), hist.end(), [&](value_t const& v2) { + bool comp = detail::tuple_compare( + v2.first, v1.first, + boost::mp11::make_index_sequence{}); + if (comp) + cumulative_counter += hist.at(v2.first); + }); + cumulative_hist[v1.first] = cumulative_counter; + }); + } + return cumulative_hist; +} + +}} //namespace boost::gil + +#endif diff --git a/test/core/CMakeLists.txt b/test/core/CMakeLists.txt index ed3b36f497..6abbfdee88 100644 --- a/test/core/CMakeLists.txt +++ b/test/core/CMakeLists.txt @@ -39,3 +39,4 @@ add_subdirectory(image) add_subdirectory(image_view) add_subdirectory(algorithm) add_subdirectory(image_processing) +add_subdirectory(histogram) diff --git a/test/core/Jamfile b/test/core/Jamfile index 7ced838338..45992e30ee 100644 --- a/test/core/Jamfile +++ b/test/core/Jamfile @@ -32,3 +32,4 @@ build-project image ; build-project image_view ; build-project algorithm ; build-project image_processing ; +build-project histogram ; diff --git a/test/core/histogram/CMakeLists.txt b/test/core/histogram/CMakeLists.txt new file mode 100644 index 0000000000..42a4931cfd --- /dev/null +++ b/test/core/histogram/CMakeLists.txt @@ -0,0 +1,37 @@ +# +# Copyright 2020 Debabrata Mandal +# +# Distributed under the Boost Software License, Version 1.0 +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt +# + +foreach(_name + access + constructor + cumulative + dimension + fill + hash_tuple + helpers + is_compatible + key + sub_histogram + utilities) + set(_test t_core_histogram_${_name}) + set(_target test_core_histogram_${_name}) + + add_executable(${_target} "") + target_sources(${_target} PRIVATE ${_name}.cpp) + target_link_libraries(${_target} + PRIVATE + gil_compile_options + gil_include_directories + gil_dependencies) + target_compile_definitions(${_target} PRIVATE BOOST_GIL_USE_CONCEPT_CHECK) + add_test(NAME ${_test} COMMAND ${_target}) + + unset(_name) + unset(_target) + unset(_test) +endforeach() diff --git a/test/core/histogram/Jamfile b/test/core/histogram/Jamfile new file mode 100644 index 0000000000..883fba7096 --- /dev/null +++ b/test/core/histogram/Jamfile @@ -0,0 +1,21 @@ +# +# Copyright 2020 Debabrata Mandal +# +# Distributed under the Boost Software License, Version 1.0 +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt +# + +import testing ; + +compile constructor.cpp ; +compile dimension.cpp ; +run access.cpp ; +run cumulative.cpp ; +run fill.cpp ; +run hash_tuple.cpp ; +run helpers.cpp ; +run is_compatible.cpp ; +run key.cpp ; +run sub_histogram.cpp ; +run utilities.cpp ; diff --git a/test/core/histogram/access.cpp b/test/core/histogram/access.cpp new file mode 100644 index 0000000000..0c1847e042 --- /dev/null +++ b/test/core/histogram/access.cpp @@ -0,0 +1,35 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include + +#include + +#include + +namespace gil = boost::gil; + +void check_indexing_operator() +{ + gil::histogram h1; + h1(1) = 3; + BOOST_TEST(h1(1) == 3); + BOOST_TEST(h1(3) == 0); + + gil::histogram h2; + h2(1, 'a', "A") = 4; + BOOST_TEST(h2(1, 'a', "A") == 4); + BOOST_TEST(h2(1, 'a', "B") == 0); +} + +int main() { + + check_indexing_operator(); + + return boost::report_errors(); +} diff --git a/test/core/histogram/constructor.cpp b/test/core/histogram/constructor.cpp new file mode 100644 index 0000000000..92817b8f9a --- /dev/null +++ b/test/core/histogram/constructor.cpp @@ -0,0 +1,31 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include + +#include + +namespace gil = boost::gil; +namespace mp11 = boost::mp11; + +void check_histogram_constructors() +{ + gil::histogram h1; + gil::histogram h2 = h1; + gil::histogram h3; + gil::histogram h4(h3); + + gil::histogram d1, d2 = d1, d3(d2); +} + +int main() { + + check_histogram_constructors(); + return 0; + +} diff --git a/test/core/histogram/cumulative.cpp b/test/core/histogram/cumulative.cpp new file mode 100644 index 0000000000..c06f710890 --- /dev/null +++ b/test/core/histogram/cumulative.cpp @@ -0,0 +1,54 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include + +#include + +namespace gil = boost::gil; + +void check_cumulative() +{ + gil::histogram h1; + for (int i = 0; i < 8; i++) + { + h1(i) = 1; + } + auto h2 = cumulative_histogram(h1); + bool check1 = true; + for (int i = 0; i < 8; i++) + { + if(h2(i) != i+1) + check1 = false; + } + BOOST_TEST(check1); + + gil::histogram h3; + h3(1, 3) = 1; + h3(1, 4) = 2; + h3(2, 1) = 3; + h3(2, 2) = 1; + h3(2, 5) = 2; + h3(3, 2) = 3; + h3(3, 9) = 1; + auto h4 = cumulative_histogram(h3); + BOOST_TEST(h4(1, 3) == 1); + BOOST_TEST(h4(1, 4) == 3); + BOOST_TEST(h4(2, 1) == 3); + BOOST_TEST(h4(2, 2) == 4); + BOOST_TEST(h4(2, 5) == 9); + BOOST_TEST(h4(3, 2) == 7); + BOOST_TEST(h4(3, 9) == 13); +} + +int main() { + + check_cumulative(); + + return boost::report_errors(); +} diff --git a/test/core/histogram/dimension.cpp b/test/core/histogram/dimension.cpp new file mode 100644 index 0000000000..912f15d27f --- /dev/null +++ b/test/core/histogram/dimension.cpp @@ -0,0 +1,31 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include + +namespace gil = boost::gil; +namespace mp11 = boost::mp11; + +void check_histogram_constructors() +{ + gil::histogram h1; + gil::histogram h2 = h1; + gil::histogram h3; + gil::histogram h4(h3); + + static_assert(h1.dimension() == h2.dimension(),"Dimension mismatch"); + static_assert(h1.dimension() != h3.dimension(),"Dimension mismatch"); + static_assert(h3.dimension() == h4.dimension(),"Dimension mismatch"); +} + +int main() { + + check_histogram_constructors(); + return 0; + +} diff --git a/test/core/histogram/fill.cpp b/test/core/histogram/fill.cpp new file mode 100644 index 0000000000..ac43dc1f5a --- /dev/null +++ b/test/core/histogram/fill.cpp @@ -0,0 +1,282 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include +#include +#include +#include + +#include +#include + +namespace gil = boost::gil; +namespace mp11 = boost::mp11; + +gil::gray8_image_t img1(4, 4, gil::gray8_pixel_t(1)); +gil::gray8_view_t v1 = view(img1); + +gil::rgb8_image_t img2(4, 4, gil::rgb8_pixel_t(1)); +gil::rgb8_view_t v2 = view(img2); + +std::uint8_t sparse_matrix[] = +{ + 1, 1, 1, 1, + 3, 3, 3, 3, + 5, 5, 5, 5, + 7, 7, 7, 7 +}; + +std::uint8_t big_matrix[] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 3, 4, 5, 6, 7, 8, + 3, 4, 3, 4, 3, 4, 3, 4, + 1, 2, 3, 4, 5, 6, 7, 8, + 5, 6, 5, 6, 5, 6, 5, 6, + 1, 2, 3, 4, 5, 6, 7, 8, + 7, 8, 7, 8, 7, 8, 7, 8 +}; + +std::uint8_t big_rgb_matrix[] = +{ + 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6, 5, 6, 7, 6, 7, 8, 7, 8, 9, 8, 9, 10, + 1, 2, 3, 2, 3, 4, 1, 2, 3, 2, 3, 4, 1, 2, 3, 2, 3, 4, 1, 2, 3, 2, 3, 4, + 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6, 5, 6, 7, 6, 7, 8, 7, 8, 9, 8, 9, 10, + 3, 4, 5, 4, 5, 6, 3, 4, 5, 4, 5, 6, 3, 4, 5, 4, 5, 6, 3, 4, 5, 4, 5, 6, + 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6, 5, 6, 7, 6, 7, 8, 7, 8, 9, 8, 9, 10, + 5, 6, 7, 6, 7, 8, 5, 6, 7, 6, 7, 8, 5, 6, 7, 6, 7, 8, 5, 6, 7, 6, 7, 8, + 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6, 5, 6, 7, 6, 7, 8, 7, 8, 9, 8, 9, 10, + 7, 8, 9, 8, 9, 10, 7, 8, 9, 8, 9, 10, 7, 8, 9, 8, 9, 10, 7, 8, 9, 8, 9, 10, +}; + +std::vector> mask = +{ + {1, 0, 0, 1}, + {0, 0, 1, 1}, + {0, 1, 0, 1}, + {1, 1, 0, 0}, +}; + +gil::gray8c_view_t sparse_gray_view = gil::interleaved_view(4, 4, reinterpret_cast(sparse_matrix), 4); + +gil::gray8c_view_t big_gray_view = gil::interleaved_view(8, 8, reinterpret_cast(big_matrix), 8); + +gil::rgb8c_view_t big_rgb_view = gil::interleaved_view(8, 8, reinterpret_cast(big_rgb_matrix), 24); + +void check_histogram_fill_test1() +{ + gil::histogram h1; + + h1.fill(big_gray_view); + + bool check_gray_fill = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(h1(i) != 8) + { + check_gray_fill = false; + } + } + BOOST_TEST(check_gray_fill); +} + +void check_histogram_fill_test2() +{ + gil::histogram h3; + h3.fill(big_rgb_view); + + bool check_rgb_fill = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(h3(i, i+1, i+2) != 8) + { + check_rgb_fill = false; + } + } + BOOST_TEST(check_rgb_fill); +} + +void check_histogram_fill_test3() +{ + gil::histogram h2; + h2.fill<1>(big_rgb_view); + bool check_gray_fill2 = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(h2(i+1) != 8) + { + check_gray_fill2 = false; + } + } + BOOST_TEST(check_gray_fill2); +} + +void check_histogram_fill_test4() +{ + gil::histogram h1; + // Check with limits + std::tuple lower{2}, higher{6}; + h1.clear(); + h1.fill(big_gray_view, 1, false, {{}}, lower, higher, true); + bool check_gray_fill = true; + check_gray_fill = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(i<2 || i>6) + { + check_gray_fill = check_gray_fill & (h1(i)==0);continue; + } + if(h1(i) != 8) + { + check_gray_fill = false; + } + } + BOOST_TEST(check_gray_fill); +} + +void check_histogram_fill_test5() +{ + gil::histogram h3; + std::tuple lower1{2,2,2}, higher1{6,6,6}; + h3.clear(); + h3.fill(big_rgb_view, 1, false, {{}}, lower1, higher1, true); + + bool check_rgb_fill = true; + check_rgb_fill = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(!(i >= 2 && (i+2) <= 6)) + { + check_rgb_fill = check_rgb_fill & (h3(i, i+1, i+2)==0);continue; + } + if(h3(i, i+1, i+2) != 8) + { + check_rgb_fill = false; + } + } + BOOST_TEST(check_rgb_fill); +} + +void check_histogram_fill_test6() +{ + gil::histogram h2; + h2.clear(); + std::tuple lower{2}, higher{6}; + h2.fill<1>(big_rgb_view, 1, false, {{}}, lower, higher, true); + bool check_gray_fill2 = true; + check_gray_fill2 = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(i+1 < 2 || i+1 > 6) + { + check_gray_fill2 = check_gray_fill2 & (h2(i+1)==0);continue; + } + if(h2(i+1) != 8) + { + check_gray_fill2 = false; + } + } + BOOST_TEST(check_gray_fill2); +} + +void check_histogram_fill_test7() +{ + //Check masking + gil::histogram h4; + std::tuple low{1}, high{8}; + gil::fill_histogram(sparse_gray_view, h4, 1, false, false, true, mask, low, high, true); + + bool check_1d = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(i%2==1) + { + check_1d = check_1d & (h4(i)==2); + } + } + BOOST_TEST(check_1d); +} + +void check_histogram_fill_algorithm() +{ + gil::histogram h1; + + gil::fill_histogram<1>(big_rgb_view, h1); + + bool check_1d = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(h1(i+1) != 8) + { + check_1d = false; + } + } + BOOST_TEST(check_1d); + + gil::histogram h2; + + gil::fill_histogram<2, 1>(big_rgb_view, h2); + + bool check_2d = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(h2(i+2, i+1) != 8) + { + check_2d = false; + } + } + BOOST_TEST(check_2d); + + gil::histogram h3; + + std::tuple low(1), high(8); + gil::fill_histogram(sparse_gray_view, h3, 1, false, false, false, {{}}, low, high, true); + + check_1d = true; + for (std::size_t i = 1; i <= 8; ++i) + { + if(h3.find(std::tuple(i)) == h3.end()) + { + check_1d = false; + } + else + { + check_1d = check_1d & (i % 2 == 1 ? (h3(i) == 4) : (h3(i) == 0)); + } + } + BOOST_TEST(check_1d); +} + +void check_fill_bin_width() +{ + gil::histogram h1; + gil::fill_histogram(big_gray_view, h1, 2); + bool check1 = true; + for(std::size_t i = 1; i <= 3; ++i) + { + check1 = check1 & (h1(i) == 16); + } + check1 = check1 & (h1(0) == 8) & (h1(4) == 8); + BOOST_TEST(check1); +} + +int main() { + + check_histogram_fill_test1(); + check_histogram_fill_test2(); + check_histogram_fill_test3(); + check_histogram_fill_test4(); + check_histogram_fill_test5(); + check_histogram_fill_test6(); + check_histogram_fill_test7(); + check_histogram_fill_algorithm(); + check_fill_bin_width(); + + return boost::report_errors(); +} \ No newline at end of file diff --git a/test/core/histogram/hash_tuple.cpp b/test/core/histogram/hash_tuple.cpp new file mode 100644 index 0000000000..69d47d16a2 --- /dev/null +++ b/test/core/histogram/hash_tuple.cpp @@ -0,0 +1,35 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include + +#include +#include + +#include + +namespace gil = boost::gil; +namespace mp11 = boost::mp11; + +void check_detail_hash_tuple () +{ + std::tuple t(1, 1); + std::size_t seed1 = 0; + boost::hash_combine(seed1, std::get<0>(t)); + boost::hash_combine(seed1, std::get<1>(t)); + + gil::detail::hash_tuple g; + std::size_t seed2 = g(t); + BOOST_TEST(seed1 == seed2); +} + +int main() +{ + check_detail_hash_tuple(); + return boost::report_errors(); +} diff --git a/test/core/histogram/helpers.cpp b/test/core/histogram/helpers.cpp new file mode 100644 index 0000000000..38fafd9f2a --- /dev/null +++ b/test/core/histogram/helpers.cpp @@ -0,0 +1,113 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include +#include +#include + +#include +#include + +#include + +namespace gil = boost::gil; +namespace mp11 = boost::mp11; + +void check_helper_fn_pixel_to_tuple() +{ + gil::gray8_pixel_t g1(2); + auto g2 = gil::detail::pixel_to_tuple(g1, mp11::make_index_sequence<1>{}); + + bool const same_gray_type = std::is_same, decltype(g2)>::value; + BOOST_TEST(same_gray_type); + BOOST_TEST(g1[0] == std::get<0>(g2)); + + gil::rgb8_pixel_t r1(1,2,3); + auto r2 = gil::detail::pixel_to_tuple(r1, mp11::index_sequence<0, 1, 2>{}); + + bool const same_rgb_type = std::is_same, + decltype(r2)>::value; + BOOST_TEST(same_rgb_type); + BOOST_TEST(r1[0] == std::get<0>(r2) && r1[1] == std::get<1>(r2) && r1[2] == std::get<2>(r2)); + + auto r3 = gil::detail::pixel_to_tuple(r1, mp11::index_sequence<1, 2, 0>{}); + BOOST_TEST(r1[0] == std::get<2>(r3) && r1[1] == std::get<0>(r3) && r1[2] == std::get<1>(r3)); +} + +void check_helper_fn_tuple_to_tuple() +{ + std::tuple t1(1); + auto t2 = gil::detail::tuple_to_tuple(t1, mp11::make_index_sequence<1>{}); + + bool const same_gray_type = std::is_same, decltype(t2)>::value; + BOOST_TEST(same_gray_type); + BOOST_TEST(std::get<0>(t1) == std::get<0>(t2)); + + std::tuple r1(1, 2, "A"); + auto r2 = gil::detail::tuple_to_tuple(r1, mp11::index_sequence<0, 1, 2>{}); + + bool const same_rgb_type = std::is_same, + decltype(r2)>::value; + BOOST_TEST(same_rgb_type); + BOOST_TEST( std::get<0>(r1) == std::get<0>(r2) && + std::get<1>(r1) == std::get<1>(r2) && + std::get<2>(r1) == std::get<2>(r2)); + + auto r3 = gil::detail::tuple_to_tuple(r1, mp11::index_sequence<1, 2, 0>{}); + BOOST_TEST( std::get<0>(r1) == std::get<2>(r3) && + std::get<1>(r1) == std::get<0>(r3) && + std::get<2>(r1) == std::get<1>(r3)); +} + +void check_helper_tuple_limit() +{ + using type1 = std::tuple; + using type2 = std::tuple; + type1 t1_min(std::numeric_limits::min(), std::numeric_limits::min()); + type1 t1_max(std::numeric_limits::max(), std::numeric_limits::max()); + type2 t2_min(std::numeric_limits::min(), std::numeric_limits::min()); + type2 t2_max(std::numeric_limits::max(), std::numeric_limits::max()); + + BOOST_TEST(t1_min == gil::detail::tuple_limit::min()); + BOOST_TEST(t1_max == gil::detail::tuple_limit::max()); + BOOST_TEST(t2_min == gil::detail::tuple_limit::min()); + BOOST_TEST(t2_max == gil::detail::tuple_limit::max()); + +} + +void check_filler() +{ + boost::gil::histogram h; + boost::gil::detail::filler<1> f; + std::tuple l1{4}, h1{13}; + f(h, l1, h1); + + std::tuple l2{20}, h2{33}; + f(h, l2, h2); + + bool check = true; + for(int i = 0; i < 100; i++) + { + if((i >= 4 && i <= 13) || (i >= 20 && i <= 33)) + { + if(h.find(std::tuple(i))==h.end()) + check = false; + } + } + BOOST_TEST(check); +} + +int main() { + + check_helper_fn_pixel_to_tuple(); + check_helper_fn_tuple_to_tuple(); + check_helper_tuple_limit(); + check_filler(); + + return boost::report_errors(); +} diff --git a/test/core/histogram/is_compatible.cpp b/test/core/histogram/is_compatible.cpp new file mode 100644 index 0000000000..bb150943a9 --- /dev/null +++ b/test/core/histogram/is_compatible.cpp @@ -0,0 +1,72 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include + +#include + +#include + +namespace gil = boost::gil; +namespace mp11 = boost::mp11; + + +void check_is_pixel_compatible() +{ + gil::histogram h1; + gil::histogram h2; + gil::histogram h3; + gil::histogram h4; + + BOOST_TEST(h1.is_pixel_compatible()); + BOOST_TEST(h2.is_pixel_compatible()); + BOOST_TEST(h3.is_pixel_compatible()); + BOOST_TEST(!h4.is_pixel_compatible()); +} + +void check_is_tuple_compatible() +{ + gil::histogram h1; + gil::histogram h2; + gil::histogram h3; + gil::histogram h4; + gil::histogram h5; + + std::tuple t1; + std::tuple t2; + std::tuple t3; + std::tuple t4; + std::tuple t5; + + BOOST_TEST(h1.is_tuple_compatible(t1)); + BOOST_TEST(h1.is_tuple_compatible(t2)); + BOOST_TEST(!h1.is_tuple_compatible(t3)); + BOOST_TEST(!h1.is_tuple_compatible(t5)); + + BOOST_TEST(h2.is_tuple_compatible(t1)); + BOOST_TEST(h2.is_tuple_compatible(t2)); + BOOST_TEST(!h2.is_tuple_compatible(t3)); + + BOOST_TEST(!h3.is_tuple_compatible(t1)); + BOOST_TEST(h3.is_tuple_compatible(t3)); + BOOST_TEST(h3.is_tuple_compatible(t4)); + BOOST_TEST(!h3.is_tuple_compatible(t5)); + + BOOST_TEST(!h4.is_tuple_compatible(t1)); + BOOST_TEST(h4.is_tuple_compatible(t3)); + BOOST_TEST(h4.is_tuple_compatible(t4)); + BOOST_TEST(!h4.is_tuple_compatible(t5)); +} + +int main() { + + check_is_pixel_compatible(); + check_is_tuple_compatible(); + + return boost::report_errors(); +} diff --git a/test/core/histogram/key.cpp b/test/core/histogram/key.cpp new file mode 100644 index 0000000000..e4e2418aef --- /dev/null +++ b/test/core/histogram/key.cpp @@ -0,0 +1,63 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include +#include +#include + +#include + +#include + +namespace gil = boost::gil; + +void check_histogram_key_from_tuple() +{ + gil::histogram h1; + std::tuple t1(1, 2); + auto t2 = h1.key_from_tuple(t1); + const bool same_type = std::is_same, decltype(t2)>::value; + + BOOST_TEST(same_type); + BOOST_TEST(std::get<0>(t2) == 1 && std::get<1>(t2) == 2); + + std::tuple t3(1, 2, 4, 2); + auto t4 = h1.key_from_tuple<0, 2>(t3); + const bool same_type1 = std::is_same, decltype(t4)>::value; + + BOOST_TEST(same_type1); + BOOST_TEST(std::get<0>(t4) == 1 && std::get<1>(t4) == 4); +} + +void check_histogram_key_from_pixel() +{ + gil::histogram h1; + gil::gray8_pixel_t g1(1); + auto t1 = h1.key_from_pixel(g1); + const bool same_type = std::is_same, decltype(t1)>::value; + + BOOST_TEST(same_type); + BOOST_TEST(std::get<0>(t1) == 1); + + gil::histogram h2; + gil::rgb8_pixel_t r1(1, 0, 3); + auto t2 = h2.key_from_pixel<0, 2>(r1); + const bool same_type1 = std::is_same, decltype(t2)>::value; + + BOOST_TEST(same_type1); + BOOST_TEST(std::get<0>(t2) == 1 && std::get<1>(t2) == 3); +} + +int main() { + + check_histogram_key_from_tuple(); + check_histogram_key_from_pixel(); + + return boost::report_errors(); +} + \ No newline at end of file diff --git a/test/core/histogram/sub_histogram.cpp b/test/core/histogram/sub_histogram.cpp new file mode 100644 index 0000000000..af618b2334 --- /dev/null +++ b/test/core/histogram/sub_histogram.cpp @@ -0,0 +1,58 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include + +#include + +#include + +namespace gil = boost::gil; + +void check_sub_histogram_without_tuple() +{ + gil::histogram h; + h(1, 1, "A", 1) = 1; + h(1, 2, "B", 1) = 1; + h(2, 1, "C", 1) = 1; + h(2, 1, "D", 1) = 1; + h(2, 3, "E", 4) = 1; + auto h1 = h.sub_histogram<0,3>(); + BOOST_TEST(h1(1, 1) == 2); + BOOST_TEST(h1(2, 1) == 2); + BOOST_TEST(h1(2, 4) == 1); + BOOST_TEST(h1(5, 5) == 0); +} + +void check_sub_histogram_with_tuple() +{ + gil::histogram h; + h(1, 1, "A", 1) = 3; + h(1, 2, "C", 1) = 1; + h(2, 1, "C", 1) = 1; + h(2, 1, "A", 1) = 1; + h(2, 3, "E", 4) = 1; + h(1, 3, "A", 1) = 2; + std::tuple t(1.0, 1000, "A", 1); + // This means 1st dimension is useless for matching. + auto h1 = h.sub_histogram<0,2,3>(t, t); + BOOST_TEST(h1(1, 1, "A", 1) == 3); + BOOST_TEST(h1(1, 2, "C", 1) == 0); + BOOST_TEST(h1(2, 1, "C", 1) == 0); + BOOST_TEST(h1(2, 1, "A", 1) == 0); + BOOST_TEST(h1(2, 1, "A", 1) == 0); + BOOST_TEST(h1(1, 3, "A", 1) == 2); +} + +int main() { + + check_sub_histogram_without_tuple(); + check_sub_histogram_with_tuple(); + + return boost::report_errors(); +} diff --git a/test/core/histogram/utilities.cpp b/test/core/histogram/utilities.cpp new file mode 100644 index 0000000000..4f26a05b41 --- /dev/null +++ b/test/core/histogram/utilities.cpp @@ -0,0 +1,214 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include +#include +#include +#include + +#include +#include +#include + +namespace gil = boost::gil; +namespace mp11 = boost::mp11; + +std::uint8_t big_matrix[] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, + 1, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 3, 4, 5, 6, 7, 8, + 3, 4, 3, 4, 3, 4, 3, 4, + 1, 2, 3, 4, 5, 6, 7, 8, + 5, 6, 5, 6, 5, 6, 5, 6, + 1, 2, 3, 4, 5, 6, 7, 8, + 7, 8, 7, 8, 7, 8, 7, 8 +}; + +void check_normalize() +{ + auto epsilon = 1e-6; + // 1D histogram + double expected[64]; + gil::histogram h1; + int sum = 0; + for (std::size_t i = 0; i < 64; i++) + { + h1(i) = big_matrix[i]; + sum += big_matrix[i]; + } + for (std::size_t i = 0; i < 64; i++) + { + expected[i] = double(big_matrix[i]) / sum; + } + h1.normalize(); + + bool check = true; + for (std::size_t i = 0; i < 64; i++) + { + check = check & (abs(expected[i] - h1(i)) < epsilon); + } + BOOST_TEST(check); + + // 2D histogram + double expected2[8][8]; + gil::histogram h2; + int sum2 = 0; + for (std::size_t i = 0; i < 64; i++) + { + h2(i/8, i%8) = big_matrix[i]; + sum2 += big_matrix[i]; + } + for (std::size_t i = 0; i < 64; i++) + { + expected2[i/8][i%8] = double(big_matrix[i]) / sum2; + } + h2.normalize(); + + bool check2 = true; + for (std::size_t i = 0; i < 64; i++) + { + check2 = check2 & (abs(expected2[i/8][i%8] - h2(i/8,i%8)) < epsilon); + } + BOOST_TEST(check2); +} + +void check_nearest_key() +{ + { + gil::histogram h1; + h1(1) = 1; + h1(3) = 4; + h1(4) = 4; + h1(6) = 1; + std::tuple k1{2}, k2{3}, k3{5}; + std::tuple k1_expected{1}, k2_expected{3}, k3_expected{4}; + BOOST_TEST(k1_expected == h1.nearest_key(k1)); + BOOST_TEST(k2_expected == h1.nearest_key(k2)); + BOOST_TEST(k3_expected == h1.nearest_key(k3)); + } + + { + gil::histogram h2; + h2(1, 1) = 1; + h2(1, 4) = 1; + h2(2, 4) = 1; + h2(4, 4) = 1; + std::tuple k1(1, 1), k2(1, 3), k3(2, 1), k4(2, 7), k5(4, 4); + std::tuple k1_exp(1, 1), k2_exp(1, 1), k3_exp(1, 4), k4_exp(2, 4), k5_exp(4, 4); + BOOST_TEST(k1_exp == h2.nearest_key(k1)); + BOOST_TEST(k2_exp == h2.nearest_key(k2)); + BOOST_TEST(k3_exp == h2.nearest_key(k3)); + BOOST_TEST(k4_exp == h2.nearest_key(k4)); + BOOST_TEST(k5_exp == h2.nearest_key(k5)); + } + +} + +void check_equals() +{ + gil::histogram h, h2; + h(1) = 3; + h(4) = 1; + h(2) = 6; + h(7) = 3; + h(9) = 7; + h2 = h; + BOOST_TEST(h2.equals(h)); + + gil::histogram h3; + h3(1) = 3; + h3(4) = 1; + h3(2) = 6; + h3(7) = 3; + h3(9) = 7; + BOOST_TEST(h3.equals(h)); +} + +void check_sum() +{ + gil::histogram h; + h(1) = 3; + h(4) = 1; + h(2) = 6; + h(7) = 3; + h(9) = 7; + auto sm = h.sum(); + BOOST_TEST(sm == 20); +} + +void check_max_key() +{ + gil::histogram h; + h(1) = 3; + h(4) = 1; + h(2) = 6; + h(7) = 3; + h(9) = 7; + BOOST_TEST(std::get<0>(h.max_key()) == 9); + + gil::histogram h2; + h2(1, 4) = 3; + h2(4, 2) = 1; + h2(2, 5) = 6; + h2(7, 4) = 3; + h2(9, 1) = 7; + h2(9, 3) = 7; + BOOST_TEST(std::get<0>(h2.max_key()) == 9 && std::get<1>(h2.max_key()) == 3); +} + +void check_min_key() +{ + gil::histogram h; + h(1) = 3; + h(4) = 1; + h(2) = 6; + h(7) = 3; + h(9) = 7; + BOOST_TEST(std::get<0>(h.min_key()) == 1); + + gil::histogram h2; + h2(1, 4) = 3; + h2(4, 2) = 1; + h2(2, 5) = 6; + h2(7, 4) = 3; + h2(9, 1) = 7; + h2(9, 3) = 7; + BOOST_TEST(std::get<0>(h2.min_key()) == 1 && std::get<1>(h2.min_key()) == 4); +} + +void check_sorted_keys() +{ + gil::histogram h; + h(1) = 3; + h(4) = 1; + h(2) = 6; + h(7) = 3; + h(9) = 7; + + std::vector> v; + v.push_back(std::tuple(1)); + v.push_back(std::tuple(2)); + v.push_back(std::tuple(4)); + v.push_back(std::tuple(7)); + v.push_back(std::tuple(9)); + BOOST_TEST(v == h.sorted_keys()); +} + +int main() { + + check_normalize(); + check_nearest_key(); + check_equals(); + check_max_key(); + check_min_key(); + check_sum(); + check_sorted_keys(); + + return boost::report_errors(); +} diff --git a/test/extension/CMakeLists.txt b/test/extension/CMakeLists.txt index b642d2b296..9f7883066f 100644 --- a/test/extension/CMakeLists.txt +++ b/test/extension/CMakeLists.txt @@ -9,6 +9,10 @@ if(BOOST_GIL_ENABLE_EXT_DYNAMIC_IMAGE) add_subdirectory(dynamic_image) endif() +if(BOOST_GIL_ENABLE_EXT_HISTOGRAM) + add_subdirectory(histogram) +endif() + if(BOOST_GIL_ENABLE_EXT_NUMERIC) add_subdirectory(numeric) endif() diff --git a/test/extension/Jamfile b/test/extension/Jamfile index cf9ef40896..5754513279 100644 --- a/test/extension/Jamfile +++ b/test/extension/Jamfile @@ -7,6 +7,7 @@ # copy at http://www.boost.org/LICENSE_1_0.txt) build-project dynamic_image ; +build-project histogram ; build-project numeric ; build-project toolbox ; build-project io ; diff --git a/test/extension/histogram/CMakeLists.txt b/test/extension/histogram/CMakeLists.txt new file mode 100644 index 0000000000..562d4d916c --- /dev/null +++ b/test/extension/histogram/CMakeLists.txt @@ -0,0 +1,27 @@ +# +# Copyright 2020 Debabrata Mandal +# +# Distributed under the Boost Software License, Version 1.0 +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt +# + +foreach(_name + histogram) + set(_test t_core_histogram_${_name}) + set(_target test_core_histogram_${_name}) + + add_executable(${_target} "") + target_sources(${_target} PRIVATE ${_name}.cpp) + target_link_libraries(${_target} + PRIVATE + gil_compile_options + gil_include_directories + gil_dependencies) + target_compile_definitions(${_target} PRIVATE BOOST_GIL_USE_CONCEPT_CHECK) + add_test(NAME ${_test} COMMAND ${_target}) + + unset(_name) + unset(_target) + unset(_test) +endforeach() diff --git a/test/extension/histogram/Jamfile b/test/extension/histogram/Jamfile new file mode 100644 index 0000000000..d7c7edf238 --- /dev/null +++ b/test/extension/histogram/Jamfile @@ -0,0 +1,11 @@ +# +# Copyright 2020 Debabrata Mandal +# +# Distributed under the Boost Software License, Version 1.0 +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt +# + +import testing ; + +run histogram.cpp ; diff --git a/test/extension/histogram/histogram.cpp b/test/extension/histogram/histogram.cpp new file mode 100644 index 0000000000..6b4b23951e --- /dev/null +++ b/test/extension/histogram/histogram.cpp @@ -0,0 +1,176 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// + +#include +#include +#include +#include + +// Supported Container types for histogram +#include +#include +#include +#include + +// Basic tests to make sure compatible container types produce +// expected output histogram. + +namespace gil = boost::gil; + +gil::gray8_image_t img1(4, 4, gil::gray8_pixel_t(1)); +gil::gray8_view_t v1 = view(img1); + +gil::rgb8_image_t img2(4, 4, gil::rgb8_pixel_t(1)); +gil::rgb8_view_t v2 = view(img2); + +gil::gray16_image_t img3(4, 4, gil::gray16_pixel_t(1)); +gil::gray16_view_t v3 = view(img3); + +template +bool check_equal(T &cont1, T &cont2, std::size_t size) +{ + bool ch = true; + for(std::size_t i = 0; i < size; ++i) + { + ch = ch & (cont1[i] == cont2[i]); + } + return ch; +} + +template +bool check_equal(T &cont1, T &cont2) +{ + bool ch = true; + for(auto &it : cont1) + { + ch = ch & (cont1[it.first] == cont2[it.first]); + } + return ch; +} + +void check_fill_histogram_vector() +{ + std::vector c1, c1_expected(256,0); + c1_expected[1] = 16; + gil::fill_histogram(v1, c1); + BOOST_TEST(check_equal(c1, c1_expected, c1_expected.size())); + + c1_expected[1] = 32; + gil::fill_histogram(v1, c1, true); + BOOST_TEST(check_equal(c1, c1_expected, c1_expected.size())); + + std::vector c2, c2_expected; + gil::fill_histogram(v2, c2); + gil::fill_histogram(gil::color_converted_view(v2), c2_expected); + BOOST_TEST(check_equal(c2, c2_expected, c2_expected.size())); +} + + +void check_fill_histogram_array() +{ + std::array c1{0}, c1_expected{0}; + c1_expected[1] = 16; + gil::fill_histogram(v1, c1); + BOOST_TEST(check_equal(c1, c1_expected, c1_expected.size())); + + c1_expected[1] = 32; + gil::fill_histogram(v1, c1, true); + BOOST_TEST(check_equal(c1, c1_expected, c1_expected.size())); + + std::array c2{0}, c2_expected{0}; + gil::fill_histogram(v1, c2); + gil::fill_histogram(gil::color_converted_view(v2), c2_expected); + BOOST_TEST(check_equal(c2, c2_expected, c2_expected.size())); + + // Check binning + std::array c3{0}, c3_expected{0}; + c3_expected[0] = 16; + gil::fill_histogram(v3, c3); + BOOST_TEST(check_equal(c3, c3_expected, c3_expected.size())); +} + +void check_fill_histogram_map() +{ + std::map c1, c1_expected; + c1_expected[1] = 16; + gil::fill_histogram(v1, c1); + BOOST_TEST(check_equal(c1, c1_expected)); + + c1_expected[1] = 32; + gil::fill_histogram(v1, c1, true); + BOOST_TEST(check_equal(c1, c1_expected)); + + std::map c2, c2_expected; + gil::fill_histogram(v2, c2); + gil::fill_histogram(gil::color_converted_view(v2), c2_expected); + BOOST_TEST(check_equal(c2, c2_expected)); +} + +void check_cumulative_histogram_vector() +{ + std::vector v(8); + for (std::size_t i = 0; i < v.size(); i++) + { + v[i] = 1; + } + auto v1 = gil::cumulative_histogram(v); + bool check = true; + for (std::size_t i = 0; i < v.size(); i++) + { + if(v1[i] != int(i) + 1) + check = false; + } + BOOST_TEST(check); +} + +void check_cumulative_histogram_array() +{ + std::array arr; + for (std::size_t i = 0; i < arr.size(); i++) + { + arr[i] = 1; + } + auto arr1 = gil::cumulative_histogram(arr); + bool check = true; + for (std::size_t i = 0; i < arr.size(); i++) + { + if(arr1[i] != int(i) + 1) + check = false; + } + BOOST_TEST(check); +} + +void check_cumulative_histogram_map() +{ + std::map mp; + for (std::size_t i = 0; i < 8; i++) + { + mp[i] = 1; + } + auto mp1 = gil::cumulative_histogram(mp); + bool check = true; + for (std::size_t i = 0; i < mp.size(); i++) + { + if(mp1[i] != int(i) + 1) + check = false; + } + BOOST_TEST(check); +} + +int main() +{ + check_fill_histogram_vector(); + check_fill_histogram_array(); + check_fill_histogram_map(); + + check_cumulative_histogram_vector(); + check_cumulative_histogram_array(); + check_cumulative_histogram_map(); + + return boost::report_errors(); +} From fb7512c29f5c3ac3f8b44ce5925757855242d489 Mon Sep 17 00:00:00 2001 From: Debabrata Mandal <32168969+codejaeger@users.noreply.github.com> Date: Mon, 25 Jan 2021 02:02:39 +0530 Subject: [PATCH 09/51] Add histogram equalization feature (#514) Co-authored-by: codejaeger --- .../contrast_enhancement/barbara.jpg | Bin 0 -> 24897 bytes .../contrast_enhancement/church.jpg | Bin 0 -> 34771 bytes .../contrast_enhancement/he_chart.png | Bin 0 -> 14693 bytes .../histogram_equalization.rst | 100 +++++++ example/histogram_equalization.cpp | 20 ++ .../histogram_equalization.hpp | 150 ++++++++++ .../histogram_equalization.cpp | 280 ++++++++++++++++++ 7 files changed, 550 insertions(+) create mode 100644 doc/image_processing/contrast_enhancement/barbara.jpg create mode 100644 doc/image_processing/contrast_enhancement/church.jpg create mode 100644 doc/image_processing/contrast_enhancement/he_chart.png create mode 100644 doc/image_processing/contrast_enhancement/histogram_equalization.rst create mode 100644 example/histogram_equalization.cpp create mode 100644 include/boost/gil/image_processing/histogram_equalization.hpp create mode 100644 test/core/image_processing/histogram_equalization.cpp diff --git a/doc/image_processing/contrast_enhancement/barbara.jpg b/doc/image_processing/contrast_enhancement/barbara.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3dad19747e21279788ebcd70df7c3207482715eb GIT binary patch literal 24897 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 literal 0 HcmV?d00001 diff --git a/doc/image_processing/contrast_enhancement/church.jpg b/doc/image_processing/contrast_enhancement/church.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0beb7c651a51c87fa91d265cf94b41bd2cb72fe GIT binary patch literal 34771 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%0Dn3>Q_=du7oL8mhQphw zNQd|9RE7*+5*B7NiFZ3%!ykOOR!ySo20c;C~d(dG8>dOU=7i0Gy~NaO-9hxYcvxp29w`vnHu_kYLH6 zQ^<*AC&d^@mF-&>++Y)(cM1Cv;rBMzev-;3=?o+)tgetPZHM8pZo$^XM>vSB)tTxc zPH7*Ld39QJBO6}2E|3;~=xM2~3auW2UrX@g0y~Eopfq!d?87e`^#i=Qc-~=wihl%n z2V1!Oz(ivYP*z;mZy7l!JY|7Q6YA%!E~9zwE&(5}r{>2M2Vk^(17Cv*uy+Evt{<53 zA!%5y1KvLIPO@s~2zI8lA&teo>iM+#z1N5=8SX!Oboe_CIKP?{K#N?+rKvQIuLOYEzrqq&Bs;SVir< ziLG`i5wZ6kv1zGU+ET=fy=qsCnyvjY`{et3p68Eua^;mPxsofdoYy(`x$pb*u?VV! z+{}q*QFA<91MKT<0QcrAyK~P@U!oC@HkdCs=c^?$B7wPb3J)9!Qlvz;S=2b!Y+V@$ zB)uo-?%t(xD{T4*t2FVKWGem#Xw3A0PlliBaxs0$S^AzG!V=4JzZp6HT|jp>+FAQ6 z-n3aNj?6+HbURo>&4^|K^G0Zt_RE{ZWT8iF2#_T17gLmj-r$S5*#bKmUZN4ne}EQC z;lz}mAO;ziR*wO+zn=_x>|iQ7q;sjIzwK?WrNKb*szOV6YyA|C2kQ;}ybvyLtW1{8 z?4{g8txwXBuUJ`AG;O32gCx8C9C5?2H*A`rVq>=f+qk1yEQrt;{k|^xUo0)G4T|9I z%g%?0FCLiSQZ!!k^xIGAT|aw~h#TWG{%RA(Ub1?u{IgTM{K<2ijS#4zne{|hDARPc z=Y*NJo#mkm*P+jsqx&?v6SQYc+cl1HMfi`C3!Aa!tnjH!ABQXB<+b zSh>eON-!~9fWkLGCE$C#?B`mULDm=7ERZ{%SK*uujxYt~VE)zB!eLW?brQ6fG?bI< zIe}F*xcaktkuz}3!|ZvTb0gajMcEh3BpjRSqz~aN*kX#*8r^#Hn$f?yj8e`3LX_vy z;qQ%a_MhIu>XO!?zm!$Xjal&oFiFdGIqI2S(xM@(=E2#n+veaNl$)M~B4w`0ioswY zaY_j{2-Y#K?yd<7(`eZ40Hw&drJXFhL@aJQv9E|Yu5j-LLeB@yjs@6SPkRZ2FfnW0 zy8YQAx*x5An{%Z*&DbaYR!ah@MQWT$1-s#M@q(pCpE@6FQ80oud~8)YI=!B|dpRn> z#PfpjQg{sEWDUxw%pvji(sH*Dx=-Pj>KlgI+?-LzgBaGZ;p(iHG+HPug5 zDvN;0_lS_G7>)Az%#J6p9R4m zlE=IEGgglgIm2eC0ctumtFqLK8eU5I3U+|HByLunE8l44N^{#hxj0!Bm4v-m&CWN; zc9@4jAO(SKF!-Zbck3hW-mL|tkFEy8A>)Sw$&jKdn#PdLji-Elob+@%RkRYJ`RaA< zkPaLZPzP&G$GukBQT7L^sMPqef-f}6T&Dunh-MQPYpmzGXu*B{_ zQwSW7z&X;+K6a7;c{NDkrZ2(;Rz5=!Ekd+-1|NA5A#`pLbxJD;XIyy#nlP8hbMQZa zBofb#`GIQY6eE{f`>lU5Kna_p29&~o#nr^}Fj@#TGa$q`xFOY&*lSF@ewF(V@NKDeyFl$HNd(W1G>y+M^Z&sZ4{>gO zbZbvEA>3-;ANS};F!j&=1Gp)Meap;xtUvFsdpJsvYJTj5&1$oxCOF#aLe@n=IM?h(KSph2)Q5Y>Jmg9d@V?B^1M6X^tg{D zha00^gsH7h?nWnk4E-V2ZIodxI}82Aqx(%8QZ}Jy&vUH8v2qv*H+YC6FwCxfN(AcKrQw$Y2L_&-!x7((qZ z$=aQpWzdYbxee?ep#{#1CD+Y^)f}m;9eh44E2{D?Yzbo+PT~RzZAW1fS z@pPfh>E;$kwEv5{=0W+vr09b|jV@<8bMOAGXjSm;oh#UoeT%>@#T#S>{zBVP)VJZL zNgP4i&;No_^n>4_3eh*PffQD<^l`1VadU~_ad5;3f8s*%hb6b*pJu<(z|Uxn@y^+9 zE)@k}Q+foPC^L52_c?MiHdxDy=sN>uMNGaGIR8%fx>vd*9w?FL2AgMXKa zu@KK#72C(=j~A7iyv#&TwhC~AX{*M!4_DKR{3 z+7%DCLHq+$x!Rv8i8|Rvj;MOZu-oQ4E;^1%J?Y8vyu;i;U<}!z2_vJY_mZO5ehz1@ z2}hTy&l!4&bY-e--W-^jL<`RhuPpv(qcHEEusV}oIETd$@ZGGu_F>l~X##9>Gwpekz{?w9o`-ld zuRwR&Cnw=rCc5P~RzI#=guG@)lORIW9zkn)9`tpWJ;2_N<-0H z)O#%m-mJ7!tw_@%x8YWC{@S+;1D~Y|im)aMAN8HoGKLPRpJmww0ZcM1B=VtrF)3r_ z7a!$l(liE*2#$wtr#fz?ojBl38fW9B`-|bO4h|r2?Y%a`V_vGl4}1ag556&S$+CunZ$&m9crCM@cDpIe9gCXR2pmnB87Gt*ti*ECW4;NQhXf}1b0JwD zb~b}xDMTK|kb@rDy_vNxu1z+Lr;gOz;E~_wJuMrVsDFSTp%u@DULlBgfz6q{OrcF% zh7yd}Myt1nbywOuPD(W+90q7i8sQ+)WTE`7zPRq&@8h=E=Q**!8cy8GM+cTKBQ_Zz z`mw({yHYp9n|6splSjT%SkdhnuhHMW?f!j*%iOdqLH3=~HIk(_>1wU%uyWh`0thMY92E0i0to{3KtP#*G1 zS(F`454^Lc%omsbzHF_-R)J`mCN)7@x%7sx7k~E_NIJu?Qw=Eq|AqBr3WZN!z z36@UP(Rv~*f(B*ZzyhX!&Fa0P926sl>$rMdJLSQ`*$MKEDVP@IG;AXuNmh}T#r8-S z7|8d_1?24{^?~vfeF^Y|7#8B*X?od}Et_e2u~DsN88a*vWrpk0kW0mT=>Gz3JK8?N z#x2Zw)5C|fSGaO!t&-%;-2#npVL{nHP1I%Sttp+xhx;EAt(b|~IUl|h|E-rX2t^zG zUQrt=XW{{-)a|_)7415G#<^5+`+9LOBbcUQn-keT8~W%{JzY-t!7tH8${x;m?+OWI zW3>L0XV~fgIOh?&T{;BKZj?&?sdF4R+84A&HDl*1w_Oze-e@XBVYuD1^WaGH@~oXv z$%%er3eEE7*&V;XKynTHO#g$vvFh^hme2YE^~&B2@>_ZLa;^xY1W3E8cz3${*T->L zQ!FwzYjnhE=BD}IF|{cVm->?nhd&Rjjnwp(T@)DTRJ+i=dJTn%P7*AR)~<*ukAsXJ zMr-QFip1^m6XG&Z|gwP&PVp8F>>doFo;Dpu;b zwD0!YEXzp<4Wz;KX zXQkh*+x!Zp7D!+0dGVp0=ut1V==jb$NmsbdbpDI-TK3!@Lys))d1?lfO;Nr(fvkfn z^dTl1UQgW&?H!E<`3QoU%y!?UlqR#ouP*DJtf*5F$36%2y}bImXPEp-*&r=(HsSj` zLe1i@>p6J>#3hg!5pJiiehH;$W1uNI7;sy1dCR{ z@U;E;tQ(1DaX6N+Z542*sS(7LI`S+ov`t(^z^i5Tqw&yJqXjcyRq7q}DWbS&Tk7;to&CJ`#4B%Qy^hgJ5-YILl_Qda3 z^Kq|jp9K|(zfcNfaC=3SwRBjTxoRxE(Waunz^guN&o6?fv%$3ES`UZY36hZIs;J;# z?Qia^1DRoHPIcW4}Rp)9>2}j-wzxxkFdywp^HbV%jCB(27 zLtu&GR>EePPa690VB|FP=>^58)DjM#9nG(5m?8qg%|dq^7yY*44AJ9WAw5gw`z!^T^7;LWVztHts_KwuqN`w`d{z)W9! z_UBEXuQ^>6&p|XH!_A4F*3U@jnX)u7E3v@t2TEB>kVK8Rk zd6V`8RY1ivM!}%U6r(1^0=kvI18|9(Ep^G2)S7fJTUGh6+Oo!JMbD94yK;h8V!5`O zG=^z9P?N}-A4GARo6m!fN}y*J!%easoOkzb%Le)|t+OSMm5~$v*?=$9J@0lLgi07I zyAQo3kHHQ6;m4c1rk#{S>s^_tdPP(~1illK>uD3UOvOFvh~9SmkAk*WIC-97u8{d0 zh=8|b{bQ9Yq|wS^mGBk`X@OqMHWh&96uKbVC|pt0a=e}dEZs?Y(tI@7fhIs&ix-6& zI(U~n`1NbkhpI^qSG~$YkqT*Z5qO6>rUQt7r*y4HN=~gjpJX#F)cPX1d)Fro zfNg)7;2!V9pwea;!e|F?AQSKNa-S98tX;Hh=8@07vB^`PZlX2w5(hjiHT9bf>ENfv zkLv&B*6(=_x}TW8ReG{iyVn|dKJ-x5l4#|eATZdT7$wW#(*lovMGlGE?9?V6cxo2g zTHQhm@kC}OtR!`sFk*HNj^0u?teEK4H!<{F0I{G$+ti^`?aJB5M7r2%E7PB!KQ1n@ zF)Pe|^H#s{Nk(;#Z0@KR=C0;64YO|*L(Ldj7CyG3wRgJ#08A5RW0W4YsHlBG8;$h`e@MQPNvB1njmky~! zyBLippGaItoZVS^cfnM^n3;v^X;k;!3Rwu0YG@aUGpk$B$en&<0;X%~C9*UT^( zEyn|J9>+nm=xcPBpvAu2PstOg%5w|*9H|NGh`6Lk`krr)U!r7o7 z`1&D(k z>N#f-_THI8UoP21IoQ8P(F20q7i3yIXnNSw z%o*H@TCQ6TU3A1}Yy2JAJ7a8c)mP#ef-h>DH9iS270}~TH>$A76M~9--rN*^FZvrl z*#als#JV}jVNA{bK(tfe4%yOLZDR;>M%Z()65&b$^#*ch*&&a%$o>!G$2m+RWuKxj zMMk-}9Y#y?7`%eX6k#Rjr<;-$9z)Lp$P|4FjD9k|VG;T!e#9Kn>@ynz$&Nf2T$RBG z6D)uNx47QodyGA8vvKqkRLtUqhx%8dG?vQAXA0<6mxyt#KI*B0WRmQOh3C(Pq7sbz zokg-NX3R>>2GAn9<~jKNHJyPBwKP$K4esN~>@S`i*&*C`wObJ$vb1C)=#(C&kU|!d z)O60s;ML!*?#ltc-!80K#)+%FcK7vGaRaxg(Oyy66ph?P@VqHEePg(_4ZZq$qEj@@ z)gz_p&@{X^K;JxdMQWcosluw4z&Hn7V4r^P1NIHK4BCZNG1Ln4!jncwMO0HNvCQCI z+B*?-iGV0%r{~V9XUZUty%TdxG&kQ7#U!<|J7Yzgw$dd#2nSkh)+CU~M;@FEtaW-G zzghGpIKiLMSXWZeD}yu#nM%6=_%wtSKD-q@S~;>ir}9s>e0-}8pAok}5a`m7nMWC%fW4aV8)|Ksu@kxKt zUoD04PE_W0=;?rf%X{YEMCLdjrqhPH28)enN* zA&sPJdqdR4Q0}BMtS{Zf^hf&?>;}B6Au+Pqik@f{nh~Qv&9O6%XRY&h!WIkeh*29} z4hasQq7b^e8gRNQ!Xi4{K--KZi#q#Op-d_lMwul|=33+CJ@Xp5%$E(Ig!^M|rn_su zpzE4|WAP*p{&;SD`fsIFe#rcqOQ!9uvax6tf2y4B^|CeS3Y6!7?ps!+tQ5+|vy%~4 zX8+rLrSV&k4Z{G9LqGZ9saFSo&y1N&)t~(q!|HE7lOF5QceYcalG2eS9}!6jcOp?e z6$7`XdOa9Q;m~^a*j9D9DMzJl6Pfx%v}gJcxj&2@$zhyLRc`g z^4;H)Dj6!Hte7CRE#KS7>(4R9*||A!0Sp;oX0Y+wr<+0a^uEq@+Z+>}BXnG%hWW8n z`ttZxwlIJck(2=jMp$C~)vfezQor$+SoF$Kn zdG6zJX?*xT|2K0+V&H;YlDAprDjHQKIy187oC1%#?T`UYH~gouJG4-)=>53{ejk!N zOElUMb|u`j3r)Y7(`eZZ8lsj&q4Aofn_JSrPIg(2VD_77O0?i#^dB{oorCET`}e2L zoI4Xpu@B?xncN$B@-N->i*PP0{wR@DGtQD&+SElCe|m^vQJ0G6#5RsD>Oi)~D7kTN+?pgE{1@6FA*|8NqnTE7L)eq{aMQPa zJJ&&&8H5`%g=s*gH5a>2o{@}f%dEkoz#1lyWV5)4N8~DbzTmNJ*>7nHdOaiG;9ePJ zX3-4^rVu6Ne79li_dn8_8~+}(q!@kh4mC~-n(5c5^Ynky|Dx{|9Cu}FD=K|vz9|FO z2a!=w=QA#+LHTK^vkMc?(sq(n#{cU{|9CSif`C@ZJ`9?-o11`#z}F28^a*`64CU3> zhehRV{Zau;e5yT~`CEBKK|}geHV%0viTw!16JjjVG4fbg-%gA48m)4gN*+Q#1t5kN7MtR2T{;{?kMgYj~=V>^w88Ol#FBryTCIB zmq%b_EX^=cEi+qk!7^-6Gb`{bYLIXS&RSUUI{a+@}PN&Ae{3C6UJps{OKNFbDTn-Eq zY@<%|bd-7E@boB+p4i7-;7Z`agUmV`dn(LvI?&#an$IUKKaqT}fvs`gQfFo!DWkD% zJBT$rX(q8hd9Cq7Q69X!XQ1@N!Ew|&bhF4#Nua6o+T(R!bbmU76b{5Fx}~uUJE5dO4vXQIp`U0^X_7h^=bRp=Qzh8@emHoDboX_cB@=7%Vw5 zIGWYCe@x&_A^mM?g0pld^Y2tO2oZd7UTdKELyDOw^%;6XWv42EL{EhVUQi_4DzKDf zr3hgqaO&L-z%VXW5#VFpdHp}gH7G1ntmk3> z9%Ot{9SjoFn4VSU?>wWrmjB2!ke=7d2P@58M$maEr>_ zC|J03g7GLPEN5X*5iIx@ktzL*LR{0Xs4`J%WUP=c?p3y@?RtwH^oh)Wn?*lU>_)Nl zGIyVAL#RmH+eSl=L!Y(8PAzAoCbuxo>D9)Ga$$LOW9ww5;d>%b78QWXjwx0nZJ6Yl zNCnPMyP+*lw$9qi>ENs;Fnd~LT1XzkYR!TsrwP(RJux~YQ)~Nm2NJ?&yKek>wydc$ z!5BQ(#GxX|Ao3?q`9w7P=}l%^KQVickT9ta)~)5h;$+6d8Ybp^_k(JyX+Nk;O3F+8 zT8m4uIdcSo12{1`oKdqlTxmbiSU#9fRiM`?5#N0mg>`C$!oy|fh@}zg4$B?8+tNfx zHwVwO`Y9CkRBjN~iXk<3kz#QB7`ig~tLrVW=(6J=*ADT=O#&SSzlG(}+r99X8LAeD z+ly1^RY=1J(>%fXqrR>g-B$7UOQhmif&XTfDJBr1Dn%Q_aLOYV9?T%qWUy^+Eu-F3 z#al_<8r%90ppx$A8^8=_R5&tuW;t6gYwp&`rYDYgkC~*rt1VGBdDLQm?_!_qwaOIr z+f9r!wE56wAHBP0W;7E$Bm6~^MY_otBfCL2;F7zIE0jjYeoig6IyT`%Pcl~X-5ZqB zi8U|Nk5}vYjk95N{Gznjd-+`(d)tPtWyst~@U}T$qs|If&y6v|R*^-TfG?2l(~EWF zsVb42(m%jLDYF;EQC)rJIH1N?lMA?xU3!x(ktUEaKP_CxN`f0KJ)N+V_v4qxvYRJJ zDxNLM)K}4L;fTa{`7-r_wn6`KUc?gLRhEi4%~G@?fv#n4LnvBt+A!@Wr%7rI$xE3! zgz8eCjL1+!a31~c0C~JNw9%prQ8q`MTwRj{hM}KEC)IM0n&+9zB8zv*XRZEK)x9B^-T5=7O5KH8{)2Z z)HG;8)OOf1)5MifV5WyJ4Z@&L9r0=6$9v;~gLf`VhQr++d=a7JK6jeMd)oX0njsmY z!z6*w#xOFC>Eg%0zp}<4>YNB@eX|qbn0`LSp*$rb7^=>%j*{qad?MN7porc zV)c}Kh->nEcF*1Wz*0Q@vHs*fOS%IK)f|}(#fkqV_gxOr*DiYPgl`%)rL38*MLw9W zOl6q&fB6N&@3P2eqLk{&z-J{DB~$XT#yq_lDXNOxKPyRF9)!~6C(EK;BqiCtz8uj3 zAHWn!q7q7)7@Rjqip^AdE#NbNwb_mSS$|ftY)LeLThuYddLtPLh+gcy+XEcV(4?q} z9>NX8NtXa2DS+Vt0R?)4ms&RlTjY75S>fFA2s+_h+Jn35q1ATWl(xxNdMv)LcHkea zRe!0T*tNL;o!DRm0wywK3sj3ki#T}G*+jv6+A09MuR>%)R|5d}IVW-TCxNDXL@UL21qFU{ zzH(3ZCxrB589us!%%mX3RUX1MZoFu5Up-s}D=So%dU-rPltUvM0Q3FuA4T@F52Xuqg+Se5A1t~3^6?1 z&mSTQh*XRj9+yLvb~?BPFW^zrNbfd=eXag{Oh>tQn#(E-zK+LB|8K6(2T0kb6G|aS zHaW*Hb#H2ML45DJnEDnTWI?h)astc9{~Gnv)V*?UFk9SePM(wDx=Q1+nTw)Wkg^j% z%ehwe<;$%oSoh_aO$t3mdJ~yv6cv~hvqO9qIz`RPBIWu@1Yo2l`5J(a5<1d)>1w~G zpBpFfs+5_0CXY2klf>6slT_neddBj%s&^_yBttj-ZZWC=vg`cZqB?XK2%x2d{H`l{ zY#gojZ9TZ_#dAjhebFo1%8rhw`<)JY1D`vbN@|I#myzeqg?-~h&v+-J=4-g$P#i(E z?c?ovTI}ETliy79{vaErti;6Hc4Nuy4tN)R)+dOc73!E^Gp5bwpTsdVvMqTb`_IfC zcV)1r);PQ!Munbfscz7pSGS6)ItL=eQ*U6(HlJP22Q)p0T0Jz2-Z>0W)PR3W%@(;u zB3~Wv8OeWm4gq{r(3prTn)v1OrL$l27Wdy$CMfBmk`61FLUt&h%ZM zQS^b6EZ+Zw-=@#kp^TpV)ggnJ^Ec|6ldq|=H;?Sm@~AOy7J6>82+=HF_E;7t&*ug; z2ncx@#5D#%}e zop;3;w*Ir)iUYlE|6JkcWSF)AB`37-l|t{L8ttWR#E(L|m&G2$PoDV-O5R2MsF4}_ zZkPR}ZF5VwsN~6GWjE0pTQ;q_niuMzlLxE;?iC&k55}&aA`#C= z?B%ktoVoILM9`5#>Z;-=Jg4t+_fY-&91G@*6>K$J$GA3YrPr1%I(AZBSYciJ^Wb4R z=ANf0*SFSNZAj?Brs*z8CbkLs?Byfk{}C-JalAZ866w00-Xi0KuBX)mII415zpe`R zJlNLf(%;p}kE>`Ke9w8iRsj=t0#?h&W{Pi;LJ_W(aDzaa`P0w#L!B2yyh6y-|!_yRCH+>*-=v=Olma(`2fJae4dY&UC^hYyw?`8^nO+KYT%MG}Q*$&Zv=RT7tRA^icqPGXiUMW~5JN0Fgvw+x6Bb*V@qPVAOqj#Vw$R8A;ze<0w?D7L{dtGsHfh zD7mY~5!uuHk5avRs$&v&12LO)j+@SbXaVTMET+=sAHjrFwCN`a29lo=Up%z(E7|KV zVwkLUAx34U`pW#c&$b7+m*i=G%W!`sYfz`dy-P*{%@R*PSv1km&{)iPM}=M-z?uQa zlDH+ADrblbp+HfeeW4s`n!B2esu*=c`vD+3(G2r_mMx{vQg_HHub5D`Q>B~gSgbBF z*FQiMw&<>rL9^95^QD#}Fy71zCG>Wp*(W-uA2XjfU%LvIFCj%-q(k(hXso~0a>k&=7G&D=tbKTJ4*jwy>iaC2SI%_T zxALv$UJU(?`)_74%y#cN+uh^Wz{E%J3WuKF^#VAZbt+}5$J5P0M2L0hOhx8h z+^QGN+o~q)D+Qi9)IBof8b22Q2Y4jgDylr~l%|kryY=LG>2D!e^beKO1j1TDK@i4R zcGC1{NrHUF8|{R492^a$ZYAT{5j2peHEh%<@SixZ@!W;&FeflGbsIm=I776Q3CPWn zgn>i>IDM=R$$->}nbT53vtST&Tk{~jP;Zw&$rews1=gEDy>Z?jWP3QYd_QBrLm$^; zAY~1y$R5=m+pm?%%QWJC`?zWiQG(f}#D`C@usW$&4HWA#?VgS2;HHR)*l zuJB6_WU_`|HV^BWQZ{5Yxc)2h;@Ij%+`5`#{~=PIJ>>TC#>l=Q29137*H` zL>lAS+tz&`j?^{tGK|YCM8A~&{B$v3BWh4IUGo9<55Q)}yenCIqS^6Rp=$it#&1cS ze0T^=Y7+9^cE11k2el5+AyTTdN-bYqsK72}!fe7VdW8B^DEg7N9AifF+N)pJebB2Y zw9^P0wWeXLSg)dyqIz2W)Y_NIQ;fPVKYOh_llFWwpWufGi*1R0q>p*^Ud{OSA4;)4 zw&)K#N<9p;Sl3##RkpQH88k{le@H4_q8qx#5Bwyj+IMMpp?U4wgWT) zE}7MI@nxs{5`b~BGArZL+j7a@&qPsz{XLg=zpK3RXOn-x_B^2OEa^6}Y1;+jXp6zL^IIn~` zIp-rg#j!@jy=Vyz8180GZxt>uTy*3yK=btGjsfVQFRPMIMXSQ(;R>0JVUR`KQlL zyvlxNyC2;Bu+Y%gto?)k+REZ*!vZO9Ol0jGr00}tcuQOwdTXj$C3k5mH*=z+l&JuI zdsu-5;1P%wp5jX;sxMCvGKF2WKQBjOmwkgdMFYw|mC)=VEI?>zbB!C%IVCHAFjC{L}gtSchl7-wBTY6lr#Sj8q=FiXuIX2_^=oY4?QkSjc5 z2%xeKM2lvQ2RMnQsYdFoTVv&NaPp$ZoQ%c>nJD!%-_RNE@!n*>1_@NVQi(eJ?|?I( z2u{E5;H{VjkS5p6AOM)&Uemxa>#?|n9Y6tn5EGCPpxoxo5g6WF(vMj*9Ch>O37^%z$Yx?$vQs5UFu6q%Vu=-0Fy^DeC&U9)ao4Q=K=&OCPyIb9k4JDNnIvmXn!62xZ60iTnRhDazKv10LmsfHA-k~J z_^RpFWZ6fobRoKn*V91Zq9NbZ*csR|LQJYM?PHqhD0Lg3!a5Xy0GwCO4H-CpS#lj^ zld|NJivI^Np0j_$tK8J6NpHu>QUCquIXu0qX81t;9_bH%By>*Isd)IE%UrEO`|uaF zD%i5~$jEQmzpHSxyL6U-Kv2XeZ%3W_J=~%V%!w89f28zGf)%?`dHA&X?pI9Ml7W& zKd~_-xY)&3fnon>#2~(gyg`Pk73&-qelL}k3IhYSz z+c2nE7^XSY|GLDSH}F~!Tdks`4^rV0Ni@Q#jijV;XIug+4|SGps8OXgZCE0|zaNwvsa|5XaPM4FRem{h)zO)Wun66Y-v-SVmPjghQ= zG!T2*E!6*PNVJb1t0?eLB$cE4UByc~Z1HRR^HTVciJPyKiAi8piz{*eUeT8E>nWUr zqtlDVj*%UH(;*=`8#rvz57<%t#x;S*qhi^ZHar8}(v#@H@mRw+oeGDMt7Yo4AK4SIXF;j9(yO7zZ|8G>Uqp5o>#D!cwmU~4w0C@2-T(Y8u+$3le7iWtI zpvAraQxvX)yUT2ZMZQg+&<3G!H>1251trq|gM6~6Q-1rjq#*vg2xQQf?C$fuk8UPx z=Ph;9eR1)%_4fqP^}|!SZP>}h4tB2Pa9PXJh*oN={Dgc9e>yiizKl@jaCvI%*b3!R zwq1nZgHbh0PM}37KWC;m<}5tp8XqcRXD6k*t%&+_WmsI&@H9@vEB8!3>dG3c_8p)v z_5JX32OHTO3-~$YdQApuK!1ef%)DDUX_?HxdKTzYcHsZ)Fx43{5I8F^vYH06W(2`{ z$$%RF2PQ6>3#vExV-ueQX@ya88-~AZ4y65SE*+$;cPcbZd$;8@_+kTW6O{_PdKu?l zUcL(+%o+UM+L5*Y{d6qcNVum2V@)5TUuCnJ!}Rm-?jN;?4N(k|0O*{4LOsn+j4%v0 zdS#9f{EifGSmYZwli_Z+DdCe_W375in`ce2a=55IdUf+VfM_g1huMOw(smzYcg&Ifxh94yOm|sbF)v4qiu*U5+ZoY1yX6*>#+>} zx1w#0IChlPSJdhYOb@jO@LT%kD70n;G$qup)^`sijX7y-NWnrqjDn#=b6UgwTUJ^< z>;Vk1uibULg|1B?eA1M$dwWJC3_+H2E@l4U%Vyr9Zph=(i#y)nU%hNDgYRYuNGsr& zrNWfAIh8*LeY8%Cr`s6G$Dza6Hx-ust^uZG7+X!+Q;*p*!Y(Fr~X-zMlxJ7qNn)d6)+^n_%?-KQ(>=%)(#_ zTM1j}YyR)nAWDa-N1l7bfJpz3d<@ll$hYokrak$`Y)k1>caHg3SI-i>0S@lPiV|B} zxWP0=6p&L+bstyoB(Q|=MBXfi>%7V~^GCm;a46)x{jFhi0oimSq7P5@{30PV&s(x8 zIuvGtV+67>h{Z66XVMkSoF!}BPUFhUMoz{&{-d9!3;Oy>Y>;+v^1K2+;~>=lwVtH- z^{?)>!AF~-n1X3q;UaHgvT&Y}^rRJXBNb8{*~Ni3%G&FJaA=Id2p`KkKy^h?!F*Tze3ffsMj^Vr+!#6 zn>+2VI$n&G%fyaN>WKFAU}ni?$q=88pr)U%)%X;Xk)8zuXwSH zf`2$^hw!5QtYNL2Y}oUp=QsH%gp%EjIE?p_QEiP!ZI8ct2k+99w=y_ zB6L5$cR{+&M5QOs&c09)DQdM(Ay-qVp|L|sQ1v8J9oo|el<-b^PtoZk(+0XX5o zA^_*ueKBXM?Bm&STfVZ1G^Glltx?SLt(HNA=v&jJkoeQjCO&-y2&!R|2Qy|0>PZ4f zXM5~@Jb_=|sQnctyT%TZ+MTP5S!agtwqHN^^BnUD!JTM0u_&U_Xt_7Ks?m@$zXDF;2j@7`ThX3%0TYNFeGRV{taH)hO<$fSKHPOH3G zxly_rZ_8!mG_q+V_r|$<0|l?0JDvoyRVjvXZ_3JitsOCM8m#gP!A@c&)xAGkLyoyn z^(#I90t*?O0ALo4qX(F1EFQ#It5Nd6zty`;+{zyLld}h|t7Jf#{hot~g$60_aU9D` z>BC0D*IAL!uG1Q0DhvB!RE8jDG!80)C}2~yy&4%H#WDhW#jhqKhkV_PyVy%}kO~W$ zxE76*4xL(;YYEN}>1g2V$g@3zN*{gFg1l74 z4O?VZ!jqydg*v@Qj{t*zQhwBx=wZ1Ff7a?zra0*-!2>qpW-nC+vI*nYO7p49>6+vN zd2Zmipo2GmxvoipB)c^9M!z!<@ghZ!8r97(9m#G1suS!wIlb5F6IWxr0`GW~{cX+NfXCbx0waM!UCabCzE_JqPS?iNJB4&?Lw{SG{xk9B zJjHIF1*0<$nmZh^%m`Y&aNnPY+$f!w3QM;I7Ky_wYK$0a{X~SKh*JrW=I+3F|LGf= z3)v5_3P?ff#QayuuJR&HgP4S-$a2P`2P#y2QN`{zo?8HcT!Z{b#V!J*V8&rTrKlDK zS>NY*szQrl%nW~RWDMTAJa%m|fTk_dQ^G0+Z1Y*z;Kx_hZ;73FQP{7*nzXp}kZH64 z!aLhrYSGH4hWVsP7>1jrrYdojv=r|7-2KK;=Bki3xElNlc$F=;yOzR5dr5U)5{?`7_~=G|7Q3~giVE=o zWZ8&Wl6WUzvYB`RsZ6c%R&{03GuiVdn{{_>@*Vq12~b>+qLxuJj`jyBPb%HwDY`rkhkNaDpCUH zK%OF6=9Ih7i{{?-1?P_3YKeWe}yheEn z)tNz!`w_EW^;|@d3A?qE=mmS_rVe)UA;;T(Tpgjsx%da0GcHimkrv{`8SdG5@IQbw zq|A0`>$1jfC!NiQ*FB9f3ZK-DJR9QeKz95=EcXhdK zUBOkIXIR?|yqet|Ob3>swAcpa`0%tu`2PuI1)BPj$-^qK+xndTwKgb{W=p~r5#~8k zcB>AazvO1I?xA4K7tZI6< zx#WQ(48#vK*HBjb1_n*g!=PxP*SYm+3~S}4da zRNxVud-bj1DI+$M)aRwKC+Wa4A8kU7pOGBMV!%Bl!mpy#>kRpyORus>SSGAux84@y=%;{yVkPc=dh!jgmQ zONsz(6tt9pN-1e51EbQ1{7YgdIOKk{byR>js@gUVp8z0pp5FC!orunR6I&^0WcLBM z5=#5v(|{XKBc9aN`H3f-3RhfYj>A1Ft|8;61HC^WpG=M_KI8q|)Wrh09Gs@(lS<${ za7y4Do}c}1@u#TVRdJtw$NvDW?@6>S(b<TgS9!7VopDeNAjuW9g1v>7F659qKy0VL&YI!NJbdL z4B&sBm11_==AZ)%Ipf-%(XZMaPavoq(&h^k#k+=Kx1rv@*Zlj_#i%4;E^)at`ihbw zOmRq9l2;h*)BgaiQJx`eak2RP{{Z^*oS5u|SW|!koQ#5V$n>dI02l{`;AfnB8jU_? zbnSpgvFD%CfZrlF{Pf|6;Zt#vj41hu!0VphU-73Y=kGBis66BA>-DAu3m);Dh69t- zXT3IW-F&IQ10$Z4hD<`S0FraiXZ+{+(%=q&;GgsUcr_GXyox7p`Dyon>C>$wj#JFQ z1gXYP@ehAm0GyRx00NgP4?qvF#W0WGEMSldk`Hk~z(8;h&x{fJ&@xbRIPcRmZs*e# z5`OXBSe^kO{{Wt};&BSejGW^i=cO?d2Ivk)(9on1aDPq(CeS2g91LcZ099O$NyPv^ zJY$3DN>8Zi-_n_n-iZ4VnppmHF2j{L6oyDK?kRUL#(B*%8)Q@mh6C`WLf^by#|w%E ziGe(hgXvE~NyY&lp!BHYP{^!$pMQFk3`ke7sbwNzxfgcq4{r5p112_rxz9p6)i}V~ zTb>xMJ!-@;g2)RTHZnf7jI}8m7N>NI8*X#X)4}8NtMPe?bHXVc4(HrrvtV84<~bvO zC$AkU%uuX$!sQDGAn~~4k80+7k7G&;sTqs83_utmhwD?va~!YpgV+1HIr@sn6S=#N z202$5&q6+!tJVx|8ss<1U~DO0^NL9@ElVsW28 z*}>%V%|c`>{{S+n;E|ryFLJsS=e6E<81ex7%~^LgDoJEy$D)#JTH;9~R**R)Z3A!w z4Et6+_1uY*g<=;uVV?M{pyY(?(Vq6Mr2g|Cg;tV1zdr2x07YAt>5C!+MFb4vIXU{8 zV!<5cfg5(7Ht}6?mCWT7P<3n&tmllD&u>iCS6qDin{P~-y>g2W0B>A<)+CCrEGgw~ z$5IA63eiZ-#H30PPd_(Ltwwg_<8jAa^NM`#6KX0b-g!9Yppf}wDu4%k4z;3WG&Jxq zsLVguAJ(HIlZ5;aK~5{?DPm8`Z~&!54hHP;?^KwI>>CU~A6k!{xdQ<8KDBAk88(tg z&w6QzHw=vXns-1|A+gEYzVyNY`IzI=HF4ujyr?|)s3akJD)jWFpb!N^)MWe7QU=KA LKPoBc7eD{m?VOo` literal 0 HcmV?d00001 diff --git a/doc/image_processing/contrast_enhancement/he_chart.png b/doc/image_processing/contrast_enhancement/he_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..217c89ef1565ebdae0e03f2044aa931d954a35ed GIT binary patch literal 14693 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 literal 0 HcmV?d00001 diff --git a/doc/image_processing/contrast_enhancement/histogram_equalization.rst b/doc/image_processing/contrast_enhancement/histogram_equalization.rst new file mode 100644 index 0000000000..0874c1c4be --- /dev/null +++ b/doc/image_processing/contrast_enhancement/histogram_equalization.rst @@ -0,0 +1,100 @@ +.. _he: + +###################### +Histogram Equalization +###################### + +Description +----------- + +Histogram equalization also known as histogram flattening, is a non-linear image enhancement +algorithm that follows the idea that not only should an image cover the entire grayscale space +but also be uniformly distributed over that range. + +An ideal image would be the one having a flat histogram. + +Although care should be taken before applying a non-linear transformation on the image +histogram, there are good mathematical reasons why a flat histogram is the desired goal. + +A simple scenario would be an image with pixels concentrated in an interval, in which case +histogram equalization transforms pixels to achieve a flat histogram image. Thus enhancing +the image contrast. + +.. figure:: he_chart.png + :width: 200px + :align: center + :height: 100px + :alt: Could not load image. + :figclass: align-center + + Pixels concentrated in an interval spread out. + +Algorithm +--------- + +#. First calculate the histogram corresponding to input image. +#. If it is a multi channeled image (e.g. RGB), convert it to a independent color space + (like YCbCr, HSV etc.). +#. Then calculate the cumulative histogram over the input image. +#. Normalize the histogram to bring bin values between 0-1. For multi-channeled images + normalize each channel independently (by the number of pixels in image). +#. If the histogram of image is H(p\ :sub:`x`\) p\ :sub:`x`\ in [0, 255], then apply + the transformation p\ :sub:`x'`\ = H(p\ :sub:`x`\), p\ :sub:`x'`\ is pixel in output + image. + +**Explanation** + +Since we will be transforming the image to match a flat histogram, we match +the cumulative histogram of the image to the cumulative histogram of a flat histogram. + +Cumulative histogram of flat image is H(p\ :sub:`x'`\) = p\ :sub:`x'` . + +Hence, + + => H(p\ :sub:`x'`\) = H(p\ :sub:`x`\) + + => p\ :sub:`x'`\ = H(p\ :sub:`x`\) + +Results +------- +The algorithm is applied on a few standard images. One of the transformations in shown below: + +**Grayscale Image** + +.. figure:: barbara.jpg + :width: 512px + :align: center + :height: 256px + :alt: Could not load image. + :figclass: align-center + +**RGB** + +.. figure:: church.jpg + :width: 900px + :align: center + :height: 300px + :alt: Could not load image. + :figclass: align-center + + +Demo +---- + +Usage Syntax: + + .. code-block:: cpp + + gray8_image_t inp_img; + read_image("your_image.png", inp_img, png_tag{}); + gray8_image_t dst_img(inp_img.dimensions()); + histogram_equalization(view(inp_img), view(dst_img)); + + // To specify mask over input image + + vector> mask(inp_img.height(), vector(inp_img.width(), true)); + histogram_equalization(view(inp_img), view(dst_img), true, mask); + + .. tip:: Convert an RGB image to a channel independent color space + before trying the histogram equalization algorithm. + diff --git a/example/histogram_equalization.cpp b/example/histogram_equalization.cpp new file mode 100644 index 0000000000..e076f9f747 --- /dev/null +++ b/example/histogram_equalization.cpp @@ -0,0 +1,20 @@ +#include +#include +#include + +using namespace boost::gil; + +int main() +{ + gray8_image_t img; + + read_image("test_adaptive.png", img, png_tag{}); + gray8_image_t img_out(img.dimensions()); + + // Consider changing image to independent color space, e.g. cmyk + boost::gil::histogram_equalization(view(img),view(img_out)); + + write_view("histogram_gray_equalized.png", view(img_out), png_tag{}); + + return 0; +} diff --git a/include/boost/gil/image_processing/histogram_equalization.hpp b/include/boost/gil/image_processing/histogram_equalization.hpp new file mode 100644 index 0000000000..d33c6f4663 --- /dev/null +++ b/include/boost/gil/image_processing/histogram_equalization.hpp @@ -0,0 +1,150 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#ifndef BOOST_GIL_IMAGE_PROCESSING_HISTOGRAM_EQUALIZATION_HPP +#define BOOST_GIL_IMAGE_PROCESSING_HISTOGRAM_EQUALIZATION_HPP + +#include +#include + +#include +#include +#include + +namespace boost { namespace gil { + + +///////////////////////////////////////// +/// Histogram Equalization(HE) +///////////////////////////////////////// +/// \defgroup HE HE +/// \brief Contains implementation and description of the algorithm used to compute +/// global histogram equalization of input images. +/// +/// Algorithm :- +/// 1. If histogram A is to be equalized compute the cumulative histogram of A. +/// 2. Let CFD(A) refer to the cumulative histogram of A +/// 3. For a uniform histogram A', CDF(A') = A' +/// 4. We need to transfrom A to A' such that +/// 5. CDF(A') = CDF(A) => A' = CDF(A) +/// 6. Hence the pixel transform , px => histogram_of_ith_channel[px]. +/// + +/// \fn histogram_equalization +/// \ingroup HE +/// \tparam SrcKeyType Key Type of input histogram +/// @param src_hist INPUT Input source histogram +/// \brief Overload for histogram equalization algorithm, takes in a single source histogram +/// and returns the color map used for histogram equalization. +/// +template +std::map histogram_equalization(histogram const& src_hist) +{ + histogram dst_hist; + return histogram_equalization(src_hist, dst_hist); +} + +/// \overload histogram_equalization +/// \ingroup HE +/// \tparam SrcKeyType Key Type of input histogram +/// \tparam DstKeyType Key Type of output histogram +/// @param src_hist INPUT source histogram +/// @param dst_hist OUTPUT Output histogram +/// \brief Overload for histogram equalization algorithm, takes in both source histogram & +/// destination histogram and returns the color map used for histogram equalization +/// as well as transforming the destination histogram. +/// +template +std::map + histogram_equalization(histogram const& src_hist, histogram& dst_hist) +{ + static_assert( + std::is_integral::value && + std::is_integral::value, + "Source and destination histogram types are not appropriate"); + + using value_t = typename histogram::value_type; + dst_hist.clear(); + double sum = src_hist.sum(); + SrcKeyType min_key = std::numeric_limits::min(); + SrcKeyType max_key = std::numeric_limits::max(); + auto cumltv_srchist = cumulative_histogram(src_hist); + std::map color_map; + std::for_each(cumltv_srchist.begin(), cumltv_srchist.end(), [&](value_t const& v) { + DstKeyType trnsfrmd_key = + static_cast((v.second * (max_key - min_key)) / sum + min_key); + color_map[std::get<0>(v.first)] = trnsfrmd_key; + }); + std::for_each(src_hist.begin(), src_hist.end(), [&](value_t const& v) { + dst_hist[color_map[std::get<0>(v.first)]] += v.second; + }); + return color_map; +} + +/// \overload histogram_equalization +/// \ingroup HE +/// @param src_view INPUT source image view +/// @param dst_view OUTPUT Output image view +/// @param bin_width INPUT Histogram bin width +/// @param mask INPUT Specify is mask is to be used +/// @param src_mask INPUT Mask vector over input image +/// \brief Overload for histogram equalization algorithm, takes in both source & destination +/// image views and histogram equalizes the input image. +/// +template +void histogram_equalization( + SrcView const& src_view, + DstView const& dst_view, + std::size_t bin_width = 1, + bool mask = false, + std::vector> src_mask = {}) +{ + gil_function_requires>(); + gil_function_requires>(); + + static_assert( + color_spaces_are_compatible< + typename color_space_type::type, + typename color_space_type::type>::value, + "Source and destination views must have same color space"); + + // Defining channel type + using source_channel_t = typename channel_type::type; + using dst_channel_t = typename channel_type::type; + using coord_t = typename SrcView::x_coord_t; + + std::size_t const channels = num_channels::value; + coord_t const width = src_view.width(); + coord_t const height = src_view.height(); + std::size_t pixel_max = std::numeric_limits::max(); + std::size_t pixel_min = std::numeric_limits::min(); + + for (std::size_t i = 0; i < channels; i++) + { + histogram h; + fill_histogram(nth_channel_view(src_view, i), h, bin_width, false, false, mask, src_mask); + h.normalize(); + auto h2 = cumulative_histogram(h); + for (std::ptrdiff_t src_y = 0; src_y < height; ++src_y) + { + auto src_it = nth_channel_view(src_view, i).row_begin(src_y); + auto dst_it = nth_channel_view(dst_view, i).row_begin(src_y); + for (std::ptrdiff_t src_x = 0; src_x < width; ++src_x) + { + if (mask && !src_mask[src_y][src_x]) + dst_it[src_x][0] = channel_convert(src_it[src_x][0]); + else + dst_it[src_x][0] = static_cast( + h2[src_it[src_x][0]] * (pixel_max - pixel_min) + pixel_min); + } + } + } +} + +}} //namespace boost::gil + +#endif diff --git a/test/core/image_processing/histogram_equalization.cpp b/test/core/image_processing/histogram_equalization.cpp new file mode 100644 index 0000000000..8aab634a03 --- /dev/null +++ b/test/core/image_processing/histogram_equalization.cpp @@ -0,0 +1,280 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include + +#include + +const int a = 5; +const double epsilon = 0.005; // Decided by the value 1/255 i.e. an error of 1 px in 255 px +boost::gil::gray8_image_t original(a, a); +boost::gil::gray8_image_t processed_1(a, a), processed_2(a, a), expected(a, a); +std::vector > test1_random{ + { 1, 10, 10, 10, 10}, + { 20, 25, 25, 55, 20}, + { 0, 55, 55, 55, 20}, + { 20, 255, 255, 255, 0}, + { 100, 100, 100, 10, 0}}; +std::vector > expected_test1{ + { 40, 91, 91, 91, 91}, + { 132, 153, 153, 193, 132}, + { 30, 193, 193, 193, 132}, + { 132, 255, 255, 255, 30}, + { 224, 224, 224, 91, 30}}; + +std::vector > all_white{ + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}}; + +std::vector > expected_all_white{ + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}}; + +std::vector > binary_img{ + {0 , 0 , 0 , 0 , 0 }, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}}; + +std::vector > expected_binary_img{ + {51 , 51 , 51 , 51 , 51 }, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255}}; + +std::vector > test2_uniform{ + { 0, 10, 20, 30, 40}, + { 50, 60, 70, 80, 90}, + { 100, 110, 120, 130, 140}, + { 150, 160, 170, 180, 190}, + { 200, 210, 220, 230, 240}}; +std::vector > expected_test2{ + { 10, 20, 30, 40, 51}, + { 61, 71, 81, 91, 102}, + { 112, 122, 132, 142, 153}, + { 163, 173, 183, 193, 204}, + { 214, 224, 234, 244, 255}}; + +std::vector > test3_2peaks{ + { 0, 0, 0, 0, 10}, + { 40, 43, 44, 46, 50}, + { 55, 56, 44, 46, 44}, + { 200, 201, 202, 203, 200}, + { 201, 202, 201, 201, 22}}; +std::vector > expected_test3{ + { 40, 40, 40, 40, 51}, + { 71, 81, 112, 132, 142}, + { 153, 163, 112, 132, 112}, + { 183, 224, 244, 255, 183}, + { 224, 244, 224, 224, 61}}; + +std::vector > test_mask{ + {1, 10, 10, 10, 10}, + {20, 25, 25, 25, 20}, + {0, 25, 25, 25, 20}, + {20, 25, 25, 25, 0}, + {100, 100, 100, 10, 0}}; +std::vector > expected_test_mask{ + {1, 10, 10, 10, 10}, + {20, 255, 255, 255, 20}, + {0, 255, 255, 255, 20}, + {20, 255, 255, 255, 0}, + {100, 100, 100, 10, 0}}; + +std::vector > mask{ + {0, 0, 0, 0, 0}, + {0, 1, 1, 1, 0}, + {0, 1, 1, 1, 0}, + {0, 1, 1, 1, 0}, + {0, 0, 0, 0, 0}}; + +void vector_to_gray_image(boost::gil::gray8_image_t& img, + std::vector >& grid) +{ + for(std::ptrdiff_t y=0; y +bool equal_pixels(SrcView const& v1, SrcView const& v2, double threshold) +{ + double sum=0.0; + using pixel_t = typename boost::gil::get_pixel_type::type; + using channel_t = typename boost::gil::channel_type::type; + channel_t max_p = std::numeric_limits::max(); + channel_t min_p = std::numeric_limits::min(); + long int num_pixels = v1.width() * v1.height(); + std::size_t num_channels = boost::gil::num_channels::value; + for (std::ptrdiff_t y = 0; y < v1.height(); ++y) + { + auto it1 = v1.row_begin(y); + auto it2 = v2.row_begin(y); + for (std::ptrdiff_t x = 0; x < v2.width(); ++x) + { + for(std::ptrdiff_t c = 0; c < num_channels; ++c) + { + sum += abs(it1[x][c]-it2[x][c]); + } + } + } + return ( abs(sum) / (num_pixels * num_channels * (max_p - min_p)) < threshold ); +} + +void test_random_image() +{ + vector_to_gray_image(original,test1_random); + vector_to_gray_image(expected,expected_test1); + + histogram_equalization(boost::gil::const_view(original),boost::gil::view(processed_1)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(expected), epsilon)); + + // Process image again to look for differences + histogram_equalization(boost::gil::const_view(processed_1),boost::gil::view(processed_2)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(processed_2), epsilon)); + + // Test overloaded version + boost::gil::histogram hist, process_1, process_2; + fill_histogram(boost::gil::const_view(original), hist, 1, false, false); + histogram_equalization(hist, process_1); + + histogram_equalization(process_1, process_2); + BOOST_TEST(process_1.equals(process_2)); +} + +void test_random_image_with_mask() +{ + vector_to_gray_image(original,test_mask); + vector_to_gray_image(expected,expected_test_mask); + histogram_equalization(boost::gil::const_view(original),boost::gil::view(processed_1), 1, true, mask); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(expected), epsilon)); + + // Process image again to look for differences + histogram_equalization(boost::gil::const_view(processed_1),boost::gil::view(processed_2), 1, true, mask); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(processed_2), epsilon)); + + // Test overloaded version + boost::gil::histogram hist, process_1, process_2; + fill_histogram(boost::gil::const_view(original), hist, 1, false, false); + histogram_equalization(hist, process_1); + + histogram_equalization(process_1, process_2); + BOOST_TEST(process_1.equals(process_2)); +} + +void test_uniform_image() +{ + vector_to_gray_image(original,test2_uniform); + vector_to_gray_image(expected,expected_test2); + histogram_equalization(boost::gil::const_view(original),boost::gil::view(processed_1)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(expected), epsilon)); + + // Process image again to look for differences + histogram_equalization(boost::gil::const_view(processed_1),boost::gil::view(processed_2)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(processed_2), epsilon)); + + // Test overloaded version + boost::gil::histogram hist, process_1, process_2; + fill_histogram(boost::gil::const_view(original), hist, 1, false, false); + histogram_equalization(hist, process_1); + + histogram_equalization(process_1, process_2); + BOOST_TEST(process_1.equals(process_2)); +} + +void test_all_white_image() +{ + vector_to_gray_image(original,all_white); + vector_to_gray_image(expected,expected_all_white); + histogram_equalization(boost::gil::const_view(original),boost::gil::view(processed_1)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(expected), epsilon)); + + // Process image again to look for differences + histogram_equalization(boost::gil::const_view(processed_1),boost::gil::view(processed_2)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(processed_2), epsilon)); + + // Test overloaded version + boost::gil::histogram hist, process_1, process_2; + fill_histogram(boost::gil::const_view(original), hist, 1, false, false); + histogram_equalization(hist, process_1); + + histogram_equalization(process_1, process_2); + BOOST_TEST(process_1.equals(process_2)); +} + +void test_binary_image() +{ + vector_to_gray_image(original,binary_img); + vector_to_gray_image(expected,expected_binary_img); + histogram_equalization(boost::gil::const_view(original),boost::gil::view(processed_1)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(expected), epsilon)); + + // Process image again to look for differences + histogram_equalization(boost::gil::const_view(processed_1),boost::gil::view(processed_2)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(processed_2), epsilon)); + + // Test overloaded version + boost::gil::histogram hist, process_1, process_2; + fill_histogram(boost::gil::const_view(original), hist, 1, false, false); + histogram_equalization(hist, process_1); + + histogram_equalization(process_1, process_2); + BOOST_TEST(process_1.equals(process_2)); +} + +void test_double_peaked_image() +{ + vector_to_gray_image(original,test3_2peaks); + vector_to_gray_image(expected,expected_test3); + histogram_equalization(boost::gil::const_view(original),boost::gil::view(processed_1)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(expected), epsilon)); + + // Process image again to look for differences + histogram_equalization(boost::gil::const_view(processed_1),boost::gil::view(processed_2)); + BOOST_TEST(equal_pixels(boost::gil::view(processed_1), boost::gil::view(processed_2), epsilon)); + + // Test overloaded version + boost::gil::histogram hist, process_1, process_2; + fill_histogram(boost::gil::const_view(original), hist, 1, false, false); + histogram_equalization(hist, process_1); + + histogram_equalization(process_1, process_2); + BOOST_TEST(process_1.equals(process_2)); +} + +int main() +{ + //Basic tests for grayscale histogram_equalization + test_random_image(); + test_random_image_with_mask(); + test_all_white_image(); + test_binary_image(); + test_uniform_image(); + test_double_peaked_image(); + + return boost::report_errors(); +} From 77255e9e61b517ebfceb4f9439a074e61b4ba1c5 Mon Sep 17 00:00:00 2001 From: Debabrata Mandal <32168969+codejaeger@users.noreply.github.com> Date: Mon, 25 Jan 2021 02:03:38 +0530 Subject: [PATCH 10/51] Add histogram matching algorithm (#515) --- .../histogram_matching.rst | 87 ++++++++ .../contrast_enhancement/matching.jpg | Bin 0 -> 14950 bytes .../contrast_enhancement/matching_out.jpg | Bin 0 -> 7607 bytes example/histogram_matching.cpp | 45 ++++ .../image_processing/histogram_matching.hpp | 206 ++++++++++++++++++ .../image_processing/histogram_matching.cpp | 137 ++++++++++++ 6 files changed, 475 insertions(+) create mode 100644 doc/image_processing/contrast_enhancement/histogram_matching.rst create mode 100644 doc/image_processing/contrast_enhancement/matching.jpg create mode 100644 doc/image_processing/contrast_enhancement/matching_out.jpg create mode 100644 example/histogram_matching.cpp create mode 100644 include/boost/gil/image_processing/histogram_matching.hpp create mode 100644 test/core/image_processing/histogram_matching.cpp diff --git a/doc/image_processing/contrast_enhancement/histogram_matching.rst b/doc/image_processing/contrast_enhancement/histogram_matching.rst new file mode 100644 index 0000000000..be7883a0c9 --- /dev/null +++ b/doc/image_processing/contrast_enhancement/histogram_matching.rst @@ -0,0 +1,87 @@ +.. _hm: + +################## +Histogram Matching +################## + +Description +----------- + +Histogram Matching is a technique to match the histograms of two images. + +One use case of this would be when two images of the same location have been taken +under the same local illumination but with different sensors, bringing out different +features in either image. + +The famous histogram equalization is a special case of this algorithm when the reference image +is expected to have a uniform histogram. + + +Algorithm +--------- + +#. Calculate the histogram corresponding to input image and reference image. +#. If it is a multi channeled image (e.g. RGB), convert both to an independent color space + (like YCbCr, HSV etc.). +#. Then calculate the cumulative histogram over the input image and reference image. +#. Normalize both the histogram to bring bin values between 0-1. For multi-channeled images + normalize each channel independently (by the number of pixels in image). +#. If the cumulative histogram of input image is H(p\ :sub:`x`\) and of reference image is R(p\ :sub:`x'`\) + p\ :sub:`x`\ & p\ :sub:`x'`\ in [0, 255], then apply the transformation + p\ :sub:`x'`\ = R\ :sup:`-1`\ (H(p\ :sub:`x`\ )) + +**Explanation** + +Since we will be transforming the image to match a reference image, we match +the cumulative histogram of the image to the cumulative histogram of the reference histogram. + +Hence, + + => R(p\ :sub:`x'`\) = H(p\ :sub:`x`\ ) + + => p\ :sub:`x'`\ = R\ :sup:`-1`\ (H(p\ :sub:`x`\ )) + +Results +------- +The algorithm is applied on a few standard images. One of the transformations in shown below: + +**Original Image(left) & Reference Image(right)** + +.. figure:: matching.jpg + :width: 600px + :align: center + :height: 300px + :alt: Could not load image. + :figclass: align-center + +**Histogram matched Image** + +.. figure:: matching_out.jpg + :width: 300px + :align: center + :height: 300px + :alt: Could not load image. + :figclass: align-center + + +Demo +---- + +Usage Syntax: + + .. code-block:: cpp + + gray8_image_t inp_img, ref_img; + read_image("your_image.png", inp_img, png_tag{}); + read_image("your_ref_image.png", ref_img, png_tag{}); + gray8_image_t dst_img(inp_img.dimensions()); + histogram_matching(view(inp_img), view(ref_image), view(dst_img)); + + // To specify mask over input image + + vector> mask(inp_img.height(), vector(inp_img.width(), true)); + histogram_matching(view(inp_img), view(ref_image), view(dst_img), true, mask); + + .. tip:: Convert an RGB image to a channel independent color space + before trying the histogram matching algorithm. + diff --git a/doc/image_processing/contrast_enhancement/matching.jpg b/doc/image_processing/contrast_enhancement/matching.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d03e07a0404975ff535f89ccfc1757869ae83e56 GIT binary patch literal 14950 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 literal 0 HcmV?d00001 diff --git a/doc/image_processing/contrast_enhancement/matching_out.jpg b/doc/image_processing/contrast_enhancement/matching_out.jpg new file mode 100644 index 0000000000000000000000000000000000000000..869043accf89bd69a4a07fd75c81be676121295f GIT binary patch literal 7607 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 x#J|?b-aYJtUE!DC$9t6wf0rp-PDf7a-bm&q< +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include + +#include + +using namespace boost::gil; + +std::vector> get_mask(gray8_view_t const& mask) +{ + std::vector> mask_vec(mask.height(), std::vector(mask.width(), 0)); + for (std::size_t i = 0; i < mask.height(); i++) + { + for (std::size_t j = 0; j < mask.width(); j++) + { + mask_vec[i][j] = mask(j, i); + } + } + return mask_vec; +} + +int main() +{ + gray8_image_t img; + read_image("test_adaptive.png", img, png_tag{}); + + gray8_image_t ref_img; + read_image("test_adaptive.png", ref_img, png_tag{}); + + gray8_image_t img_out(img.dimensions()); + + boost::gil::histogram_matching(view(img), view(ref_img), view(img_out)); + + write_view("histogram_gray_matching.png", view(img_out), png_tag{}); + + return 0; +} diff --git a/include/boost/gil/image_processing/histogram_matching.hpp b/include/boost/gil/image_processing/histogram_matching.hpp new file mode 100644 index 0000000000..ea49a5c153 --- /dev/null +++ b/include/boost/gil/image_processing/histogram_matching.hpp @@ -0,0 +1,206 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_GIL_IMAGE_PROCESSING_HISTOGRAM_MATCHING_HPP +#define BOOST_GIL_IMAGE_PROCESSING_HISTOGRAM_MATCHING_HPP + +#include +#include +#include + +#include +#include +#include +#include + +namespace boost { namespace gil { + +///////////////////////////////////////// +/// Histogram Matching(HM) +///////////////////////////////////////// +/// \defgroup HM HM +/// \brief Contains implementation and description of the algorithm used to compute +/// global histogram matching of input images. +/// +/// Algorithm :- +/// 1. Calculate histogram A(pixel) of input image and G(pixel) of reference image. +/// 2. Compute the normalized cumulative(CDF) histograms of A and G. +/// 3. Match the histograms using transofrmation => CDF(A(px)) = CDF(G(px')) +/// => px' = Inv-CDF (CDF(px)) +/// + +/// \fn histogram_matching +/// \ingroup HM +/// \tparam SrcKeyType Key Type of input histogram +/// @param src_hist INPUT Input source histogram +/// @param ref_hist INPUT Input reference histogram +/// \brief Overload for histogram matching algorithm, takes in a single source histogram & +/// reference histogram and returns the color map used for histogram matching. +/// +template +std::map + histogram_matching(histogram const& src_hist, histogram const& ref_hist) +{ + histogram dst_hist; + return histogram_matching(src_hist, ref_hist, dst_hist); +} + +/// \overload histogram_matching +/// \ingroup HM +/// \tparam SrcKeyType Key Type of input histogram +/// \tparam RefKeyType Key Type of reference histogram +/// \tparam DstKeyType Key Type of output histogram +/// @param src_hist INPUT source histogram +/// @param ref_hist INPUT reference histogram +/// @param dst_hist OUTPUT Output histogram +/// \brief Overload for histogram matching algorithm, takes in source histogram, reference +/// histogram & destination histogram and returns the color map used for histogram +/// matching as well as transforming the destination histogram. +/// +template +std::map histogram_matching( + histogram const& src_hist, + histogram const& ref_hist, + histogram& dst_hist) +{ + static_assert( + std::is_integral::value && + std::is_integral::value && + std::is_integral::value, + "Source, Refernce or Destination histogram type is not appropriate."); + + using value_t = typename histogram::value_type; + dst_hist.clear(); + double src_sum = src_hist.sum(); + double ref_sum = ref_hist.sum(); + auto cumltv_srchist = cumulative_histogram(src_hist); + auto cumltv_refhist = cumulative_histogram(ref_hist); + std::map inverse_mapping; + + std::vector::key_type> src_keys, ref_keys; + src_keys = src_hist.sorted_keys(); + ref_keys = ref_hist.sorted_keys(); + std::ptrdiff_t start = ref_keys.size() - 1; + RefKeyType ref_max; + if (start >= 0) + ref_max = std::get<0>(ref_keys[start]); + + for (std::ptrdiff_t j = src_keys.size() - 1; j >= 0; --j) + { + double src_val = (cumltv_srchist[src_keys[j]] * ref_sum) / src_sum; + while (cumltv_refhist[ref_keys[start]] > src_val && start > 0) + { + start--; + } + if (abs(cumltv_refhist[ref_keys[start]] - src_val) > + abs(cumltv_refhist(std::min(ref_max, std::get<0>(ref_keys[start + 1]))) - + src_val)) + { + inverse_mapping[std::get<0>(src_keys[j])] = + std::min(ref_max, std::get<0>(ref_keys[start + 1])); + } + else + { + inverse_mapping[std::get<0>(src_keys[j])] = std::get<0>(ref_keys[start]); + } + if (j == 0) + break; + } + std::for_each(src_hist.begin(), src_hist.end(), [&](value_t const& v) { + dst_hist[inverse_mapping[std::get<0>(v.first)]] += v.second; + }); + return inverse_mapping; +} + +/// \overload histogram_matching +/// \ingroup HM +/// @param src_view INPUT source image view +/// @param ref_view INPUT Reference image view +/// @param dst_view OUTPUT Output image view +/// @param bin_width INPUT Histogram bin width +/// @param mask INPUT Specify is mask is to be used +/// @param src_mask INPUT Mask vector over input image +/// @param ref_mask INPUT Mask vector over reference image +/// \brief Overload for histogram matching algorithm, takes in both source, reference & +/// destination image views and histogram matches the input image using the +/// reference image. +/// +template +void histogram_matching( + SrcView const& src_view, + ReferenceView const& ref_view, + DstView const& dst_view, + std::size_t bin_width = 1, + bool mask = false, + std::vector> src_mask = {}, + std::vector> ref_mask = {}) +{ + gil_function_requires>(); + gil_function_requires>(); + gil_function_requires>(); + + static_assert( + color_spaces_are_compatible< + typename color_space_type::type, + typename color_space_type::type>::value, + "Source and reference view must have same color space"); + + static_assert( + color_spaces_are_compatible< + typename color_space_type::type, + typename color_space_type::type>::value, + "Source and destination view must have same color space"); + + // Defining channel type + using source_channel_t = typename channel_type::type; + using ref_channel_t = typename channel_type::type; + using dst_channel_t = typename channel_type::type; + using coord_t = typename SrcView::x_coord_t; + + std::size_t const channels = num_channels::value; + coord_t const width = src_view.width(); + coord_t const height = src_view.height(); + source_channel_t src_pixel_min = std::numeric_limits::min(); + source_channel_t src_pixel_max = std::numeric_limits::max(); + ref_channel_t ref_pixel_min = std::numeric_limits::min(); + ref_channel_t ref_pixel_max = std::numeric_limits::max(); + dst_channel_t dst_pixel_min = std::numeric_limits::min(); + dst_channel_t dst_pixel_max = std::numeric_limits::max(); + + for (std::size_t i = 0; i < channels; i++) + { + histogram src_histogram; + histogram ref_histogram; + fill_histogram( + nth_channel_view(src_view, i), src_histogram, bin_width, false, false, mask, src_mask, + std::tuple(src_pixel_min), + std::tuple(src_pixel_max), true); + fill_histogram( + nth_channel_view(ref_view, i), ref_histogram, bin_width, false, false, mask, ref_mask, + std::tuple(ref_pixel_min), std::tuple(ref_pixel_max), + true); + auto inverse_mapping = histogram_matching(src_histogram, ref_histogram); + for (std::ptrdiff_t src_y = 0; src_y < height; ++src_y) + { + auto src_it = nth_channel_view(src_view, i).row_begin(src_y); + auto dst_it = nth_channel_view(dst_view, i).row_begin(src_y); + for (std::ptrdiff_t src_x = 0; src_x < width; ++src_x) + { + if (mask && !src_mask[src_y][src_x]) + dst_it[src_x][0] = src_it[src_x][0]; + else + dst_it[src_x][0] = + static_cast(inverse_mapping[src_it[src_x][0]]); + } + } + } +} + +}} //namespace boost::gil + +#endif diff --git a/test/core/image_processing/histogram_matching.cpp b/test/core/image_processing/histogram_matching.cpp new file mode 100644 index 0000000000..56672e0494 --- /dev/null +++ b/test/core/image_processing/histogram_matching.cpp @@ -0,0 +1,137 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include +#include +#include + +#include +#include + +const int a = 5; +const double epsilon = 0.000001; // Decided by the value 5/255 i.e. an error of 5 px in 255 px +boost::gil::gray8_image_t original(a, a), reference(a, a); +boost::gil::gray8_image_t processed(a, a), processed2(a, a); +std::vector > test1_random{ + { 1, 10, 10, 10, 10}, + { 20, 25, 25, 55, 20}, + { 0, 55, 55, 55, 20}, + { 20, 255, 255, 255, 0}, + { 100, 100, 100, 10, 0}}; +std::vector > test1_reference{ + { 0, 10, 20, 30, 40}, + { 50, 60, 70, 80, 90}, + { 100, 110, 120, 130, 140}, + { 150, 160, 170, 180, 190}, + { 200, 210, 220, 230, 240}}; + +std::vector > test2_uniform{ + { 0, 10, 20, 30, 40}, + { 50, 60, 70, 80, 90}, + { 100, 110, 120, 130, 140}, + { 150, 160, 170, 180, 190}, + { 200, 210, 220, 230, 240}}; +std::vector > test2_reference{ + { 10, 20, 30, 40, 51}, + { 61, 71, 81, 91, 102}, + { 112, 122, 132, 142, 153}, + { 163, 173, 183, 193, 204}, + { 214, 224, 234, 244, 255}}; + +std::vector > test3_equal_image{ + { 0, 10, 20, 30, 40}, + { 50, 60, 70, 80, 90}, + { 100, 110, 120, 130, 140}, + { 150, 160, 170, 180, 190}, + { 200, 210, 220, 230, 240}}; + +std::vector > test3_reference{ + { 0, 10, 20, 30, 40}, + { 50, 60, 70, 80, 90}, + { 100, 110, 120, 130, 140}, + { 150, 160, 170, 180, 190}, + { 200, 210, 220, 230, 240}}; + +void vector_to_gray_image(boost::gil::gray8_image_t& img, + std::vector >& grid) +{ + for(std::ptrdiff_t y=0; y +bool equal_histograms(SrcView const& v1, SrcView const& v2, double threshold = epsilon) +{ + double sum=0.0; + using channel_t = typename boost::gil::channel_type::type; + boost::gil::histogram h1, h2; + using value_t = typename boost::gil::histogram::value_type; + channel_t max_p = std::numeric_limits::max(); + channel_t min_p = std::numeric_limits::min(); + long int num_pixels = v1.width() * v1.height(); + + boost::gil::fill_histogram(v1, h1, 1, false, false); + boost::gil::fill_histogram(v2, h2, 1, false, false); + auto ch1 = boost::gil::cumulative_histogram(h1); + auto ch2 = boost::gil::cumulative_histogram(h2); + std::for_each(ch1.begin(), ch1.end(), [&](value_t const& v) { + sum+=abs(v.second-ch1[v.first]); + }); + return ( abs(sum) / (ch1.size() * (max_p - min_p)) < threshold ); +} + +void test_random_image() +{ + vector_to_gray_image(original,test1_random); + vector_to_gray_image(reference,test1_reference); + histogram_matching(boost::gil::const_view(original),boost::gil::const_view(reference),boost::gil::view(processed)); + BOOST_TEST(equal_histograms(boost::gil::view(processed), boost::gil::view(reference))); + + histogram_matching(boost::gil::const_view(processed),boost::gil::const_view(reference),boost::gil::view(processed2)); + BOOST_TEST(equal_histograms(boost::gil::view(processed), boost::gil::view(processed2))); +} + +void test_uniform_image() +{ + vector_to_gray_image(original,test2_uniform); + vector_to_gray_image(reference,test2_reference); + histogram_matching(boost::gil::const_view(original),boost::gil::const_view(reference),boost::gil::view(processed)); + BOOST_TEST(equal_histograms(boost::gil::view(processed), boost::gil::view(reference))); + + histogram_matching(boost::gil::const_view(processed),boost::gil::const_view(reference),boost::gil::view(processed2)); + BOOST_TEST(equal_histograms(boost::gil::view(processed), boost::gil::view(processed2))); +} + +void test_equal_image() +{ + vector_to_gray_image(original,test3_equal_image); + vector_to_gray_image(reference,test3_reference); + histogram_matching(boost::gil::const_view(original),boost::gil::const_view(reference),boost::gil::view(processed)); + BOOST_TEST(equal_histograms(boost::gil::view(processed), boost::gil::view(reference))); + + histogram_matching(boost::gil::const_view(processed),boost::gil::const_view(reference),boost::gil::view(processed2)); + BOOST_TEST(equal_histograms(boost::gil::view(processed), boost::gil::view(processed2))); +} + +int main() +{ + //Basic tests for grayscale histogram_equalization + test_random_image(); + test_uniform_image(); + test_equal_image(); + + return boost::report_errors(); +} From b82aed8f8e12602e1a3537ebbdbeb6a0b357d481 Mon Sep 17 00:00:00 2001 From: Olzhas Zhumabek Date: Mon, 25 Jan 2021 02:43:06 +0600 Subject: [PATCH 11/51] Implement hstack and vstack (#506) Allow non-equal dims along stack dim Width can be different in hstack, height can be different in vstack --- example/harris.cpp | 148 ++++++++++++++++++++------------------------ example/hvstack.hpp | 116 ++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 80 deletions(-) create mode 100644 example/hvstack.hpp diff --git a/example/harris.cpp b/example/harris.cpp index f860105d2f..81233fe38a 100644 --- a/example/harris.cpp +++ b/example/harris.cpp @@ -5,17 +5,21 @@ // Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) // -#include -#include #include -#include -#include #include -#include +#include +#include +#include +#include +#include + +#include "hvstack.hpp" + +#include #include -#include #include -#include +#include +#include namespace gil = boost::gil; @@ -29,24 +33,28 @@ gil::gray8_image_t to_grayscale(gil::rgb8_view_t original) gil::gray8_image_t output_image(original.dimensions()); auto output = gil::view(output_image); constexpr double max_channel_intensity = (std::numeric_limits::max)(); - for (long int y = 0; y < original.height(); ++y) { - for (long int x = 0; x < original.width(); ++x) { + for (long int y = 0; y < original.height(); ++y) + { + for (long int x = 0; x < original.width(); ++x) + { // scale the values into range [0, 1] and calculate linear intensity - double red_intensity = original(x, y).at(std::integral_constant{}) - / max_channel_intensity; - double green_intensity = original(x, y).at(std::integral_constant{}) - / max_channel_intensity; - double blue_intensity = original(x, y).at(std::integral_constant{}) - / max_channel_intensity; - auto linear_luminosity = 0.2126 * red_intensity - + 0.7152 * green_intensity - + 0.0722 * blue_intensity; + double red_intensity = + original(x, y).at(std::integral_constant{}) / max_channel_intensity; + double green_intensity = + original(x, y).at(std::integral_constant{}) / max_channel_intensity; + double blue_intensity = + original(x, y).at(std::integral_constant{}) / max_channel_intensity; + auto linear_luminosity = + 0.2126 * red_intensity + 0.7152 * green_intensity + 0.0722 * blue_intensity; // perform gamma adjustment double gamma_compressed_luminosity = 0; - if (linear_luminosity < 0.0031308) { + if (linear_luminosity < 0.0031308) + { gamma_compressed_luminosity = linear_luminosity * 12.92; - } else { + } + else + { gamma_compressed_luminosity = 1.055 * std::pow(linear_luminosity, 1 / 2.4) - 0.055; } @@ -62,45 +70,43 @@ void apply_gaussian_blur(gil::gray8_view_t input_view, gil::gray8_view_t output_ { constexpr static auto filterHeight = 5ull; constexpr static auto filterWidth = 5ull; - constexpr static double filter[filterHeight][filterWidth] = - { - 2, 4, 6, 4, 2, - 4, 9, 12, 9, 4, - 5, 12, 15, 12, 5, - 4, 9, 12, 9, 4, - 2, 4, 5, 4, 2, + constexpr static double filter[filterHeight][filterWidth] = { + 2, 4, 6, 4, 2, 4, 9, 12, 9, 4, 5, 12, 15, 12, 5, 4, 9, 12, 9, 4, 2, 4, 5, 4, 2, }; constexpr double factor = 1.0 / 159; constexpr double bias = 0.0; const auto height = input_view.height(); const auto width = input_view.width(); - for (long x = 0; x < width; ++x) { - for (long y = 0; y < height; ++y) { + for (long x = 0; x < width; ++x) + { + for (long y = 0; y < height; ++y) + { double intensity = 0.0; - for (size_t filter_y = 0; filter_y < filterHeight; ++filter_y) { - for (size_t filter_x = 0; filter_x < filterWidth; ++filter_x) { + for (size_t filter_y = 0; filter_y < filterHeight; ++filter_y) + { + for (size_t filter_x = 0; filter_x < filterWidth; ++filter_x) + { int image_x = x - filterWidth / 2 + filter_x; int image_y = y - filterHeight / 2 + filter_y; - if (image_x >= input_view.width() || image_x < 0 - || image_y >= input_view.height() || image_y < 0) { + if (image_x >= input_view.width() || image_x < 0 || + image_y >= input_view.height() || image_y < 0) + { continue; } auto& pixel = input_view(image_x, image_y); - intensity += pixel.at(std::integral_constant{}) - * filter[filter_y][filter_x]; + intensity += + pixel.at(std::integral_constant{}) * filter[filter_y][filter_x]; } } auto& pixel = output_view(gil::point_t(x, y)); pixel = (std::min)((std::max)(int(factor * intensity + bias), 0), 255); } - } } -std::vector suppress( - gil::gray32f_view_t harris_response, - double harris_response_threshold) +std::vector suppress(gil::gray32f_view_t harris_response, + double harris_response_threshold) { std::vector corner_points; for (gil::gray32f_view_t::coord_t y = 1; y < harris_response.height() - 1; ++y) @@ -111,29 +117,17 @@ std::vector suppress( return pixel.at(std::integral_constant{}); }; double values[9] = { - value(harris_response(x - 1, y - 1)), - value(harris_response(x, y - 1)), - value(harris_response(x + 1, y - 1)), - value(harris_response(x - 1, y)), - value(harris_response(x, y)), - value(harris_response(x + 1, y)), - value(harris_response(x - 1, y + 1)), - value(harris_response(x, y + 1)), - value(harris_response(x + 1, y + 1)) - }; + value(harris_response(x - 1, y - 1)), value(harris_response(x, y - 1)), + value(harris_response(x + 1, y - 1)), value(harris_response(x - 1, y)), + value(harris_response(x, y)), value(harris_response(x + 1, y)), + value(harris_response(x - 1, y + 1)), value(harris_response(x, y + 1)), + value(harris_response(x + 1, y + 1))}; - auto maxima = *std::max_element( - values, - values + 9, - [](double lhs, double rhs) - { - return lhs < rhs; - } - ); + auto maxima = *std::max_element(values, values + 9, + [](double lhs, double rhs) { return lhs < rhs; }); - if (maxima == value(harris_response(x, y)) - && std::count(values, values + 9, maxima) == 1 - && maxima >= harris_response_threshold) + if (maxima == value(harris_response(x, y)) && + std::count(values, values + 9, maxima) == 1 && maxima >= harris_response_threshold) { corner_points.emplace_back(x, y); } @@ -147,8 +141,9 @@ int main(int argc, char* argv[]) { if (argc != 6) { - std::cout << "usage: " << argv[0] << " " - " \n"; + std::cout << "usage: " << argv[0] + << " " + " \n"; return -1; } @@ -159,7 +154,8 @@ int main(int argc, char* argv[]) gil::rgb8_image_t input_image; gil::read_image(argv[1], input_image, gil::png_tag{}); - + auto original_image = input_image; + auto original_view = gil::view(original_image); auto input_view = gil::view(input_image); auto grayscaled = to_grayscale(input_view); gil::gray8_image_t smoothed_image(grayscaled.dimensions()); @@ -178,30 +174,22 @@ int main(int argc, char* argv[]) gil::gray32f_image_t m11(x_gradient.dimensions()); gil::gray32f_image_t m12_21(x_gradient.dimensions()); gil::gray32f_image_t m22(x_gradient.dimensions()); - gil::compute_tensor_entries( - x_gradient, - y_gradient, - gil::view(m11), - gil::view(m12_21), - gil::view(m22) - ); + gil::compute_tensor_entries(x_gradient, y_gradient, gil::view(m11), gil::view(m12_21), + gil::view(m22)); gil::gray32f_image_t harris_response(x_gradient.dimensions()); auto gaussian_kernel = gil::generate_gaussian_kernel(window_size, 0.84089642); - gil::compute_harris_responses( - gil::view(m11), - gil::view(m12_21), - gil::view(m22), - gaussian_kernel, - discrimnation_constant, - gil::view(harris_response) - ); + gil::compute_harris_responses(gil::view(m11), gil::view(m12_21), gil::view(m22), + gaussian_kernel, discrimnation_constant, + gil::view(harris_response)); auto corner_points = suppress(gil::view(harris_response), harris_response_threshold); - for (auto point: corner_points) + for (auto point : corner_points) { input_view(point) = gil::rgb8_pixel_t(0, 0, 0); input_view(point).at(std::integral_constant{}) = 255; } - gil::write_view(argv[5], input_view, gil::png_tag{}); + auto stacked = gil::hstack(std::vector{original_view, input_view}); + + gil::write_view(argv[5], gil::view(stacked), gil::png_tag{}); } diff --git a/example/hvstack.hpp b/example/hvstack.hpp new file mode 100644 index 0000000000..1b4daaaf13 --- /dev/null +++ b/example/hvstack.hpp @@ -0,0 +1,116 @@ +#include "boost/gil/image_view_factory.hpp" +#include +#include +#include +#include +#include + +namespace boost { namespace gil { +template +void hstack(const std::vector& views, const View& output_view) +{ + if (views.size() == 0) + { + throw std::invalid_argument("empty views vector is passed - cannot create stacked image"); + } + + auto height = views.front().height(); + auto width = views.front().width(); + for (const auto& view : views) + { + if (view.height() != height) + { + throw std::invalid_argument("one or many views are not of the same height"); + } + } + + std::ptrdiff_t full_width = + std::accumulate(views.begin(), views.end(), 0, + [](std::ptrdiff_t old, const View& view) { return old + view.width(); }); + if (output_view.width() != full_width || output_view.height() != height) + { + throw std::invalid_argument("the destination view is not of the right dimensions"); + } + + std::ptrdiff_t current_x = 0; + for (std::size_t i = 0; i < views.size(); ++i) + { + auto subview = + subimage_view(output_view, current_x, 0, views[i].width(), views[i].height()); + copy_pixels(views[i], subview); + current_x += views[i].width(); + } +} + +template +image hstack(const std::vector& views) +{ + if (views.size() == 0) + { + throw std::invalid_argument("empty views vector is passed - cannot create stacked image"); + } + + auto dimensions = views.front().dimensions(); + std::ptrdiff_t full_width = + std::accumulate(views.begin(), views.end(), 0, + [](std::ptrdiff_t old, const View& view) { return old + view.width(); }); + std::ptrdiff_t height = views.front().height(); + image result_image(full_width, height); + hstack(views, view(result_image)); + return result_image; +} + +template +void vstack(const std::vector& views, const View& output_view) +{ + if (views.size() == 0) + { + throw std::invalid_argument("empty views vector is passed - cannot create stacked image"); + } + + auto full_height = + std::accumulate(views.begin(), views.end(), 0, + [](std::ptrdiff_t old, const View& view) { return old + view.height(); }); + std::ptrdiff_t width = views.front().height(); + + for (const auto& view : views) + { + if (view.width() != width) + { + throw std::invalid_argument("one or many views are not of the same width"); + } + } + + if (output_view != full_height || output_view.width() != width) + { + throw std::invalid_argument("the destination view is not of the right dimensions"); + } + + std::ptrdiff_t current_y = 0; + for (std::size_t i = 0; i < views.size(); ++i) + { + auto subview = + subimage_view(output_view, 0, current_y, views[i].width(), views[i].height()); + copy_pixels(views[i], subview); + current_y += views[i].height(); + } +} + +template +image vstack(const std::vector& views) +{ + if (views.size() == 0) + { + throw std::invalid_argument("empty views vector is passed - cannot create stacked image"); + } + + auto full_height = + std::accumulate(views.begin(), views.end(), 0, + [](std::ptrdiff_t old, const View& view) { return old + view.height(); }); + std::ptrdiff_t width = views.front().height(); + + image result_image(width, full_height); + hstack(views, view(result_image)); + return result_image; +} +}} // namespace boost::gil From a68a95d5f457038d4851c900e57d430dfb09eca8 Mon Sep 17 00:00:00 2001 From: Debabrata Mandal <32168969+codejaeger@users.noreply.github.com> Date: Mon, 25 Jan 2021 03:52:48 +0530 Subject: [PATCH 12/51] Add code for ahe algorithm (#516) --- example/adaptive_he.cpp | 25 ++ .../adaptive_histogram_equalization.hpp | 307 ++++++++++++++++++ test/core/image_processing/adaptive_he.cpp | 113 +++++++ 3 files changed, 445 insertions(+) create mode 100644 example/adaptive_he.cpp create mode 100644 include/boost/gil/image_processing/adaptive_histogram_equalization.hpp create mode 100644 test/core/image_processing/adaptive_he.cpp diff --git a/example/adaptive_he.cpp b/example/adaptive_he.cpp new file mode 100644 index 0000000000..0610932c98 --- /dev/null +++ b/example/adaptive_he.cpp @@ -0,0 +1,25 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +using namespace boost::gil; + +int main() +{ + gray8_image_t img; + read_image("test_adaptive.png", img, png_tag{}); + gray8_image_t img_out(img.dimensions()); + + boost::gil::non_overlapping_interpolated_clahe(view(img), view(img_out)); + write_view("out-adaptive.png", view(img_out), png_tag{}); + return 0; +} diff --git a/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp b/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp new file mode 100644 index 0000000000..9dcbfba928 --- /dev/null +++ b/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp @@ -0,0 +1,307 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_GIL_IMAGE_PROCESSING_ADAPTIVE_HISTOGRAM_EQUALIZATION_HPP +#define BOOST_GIL_IMAGE_PROCESSING_ADAPTIVE_HISTOGRAM_EQUALIZATION_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost { namespace gil { + +///////////////////////////////////////// +/// Adaptive Histogram Equalization(AHE) +///////////////////////////////////////// +/// \defgroup AHE AHE +/// \brief Contains implementation and description of the algorithm used to compute +/// adaptive histogram equalization of input images. Naming for the AHE functions +/// are done in the following way +/// __.._ahe +/// For example, for AHE done using local (non-overlapping) tiles/blocks and +/// final output interpolated among tiles , it is called +/// non_overlapping_interpolated_clahe +/// + +namespace detail { + +/// \defgroup AHE-helpers AHE-helpers +/// \brief AHE helper functions + +/// \fn double actual_clip_limit +/// \ingroup AHE-helpers +/// \brief Computes the actual clip limit given a clip limit value using binary search. +/// Reference - Adaptive Histogram Equalization and Its Variations +/// (http://www.cs.unc.edu/techreports/86-013.pdf, Pg - 15) +/// +template +double actual_clip_limit(SrcHist const& src_hist, double cliplimit = 0.03) +{ + double epsilon = 1.0; + using value_t = typename SrcHist::value_type; + double sum = src_hist.sum(); + std::size_t num_bins = src_hist.size(); + + cliplimit = sum * cliplimit; + long low = 0, high = cliplimit, middle = low; + while (high - low >= 1) + { + middle = (low + high + 1) >> 1; + long excess = 0; + std::for_each(src_hist.begin(), src_hist.end(), [&](value_t const& v) { + if (v.second > middle) + excess += v.second - middle; + }); + if (abs(excess - (cliplimit - middle) * num_bins) < epsilon) + break; + else if (excess > (cliplimit - middle) * num_bins) + high = middle - 1; + else + low = middle + 1; + } + return middle / sum; +} + +/// \fn void clip_and_redistribute +/// \ingroup AHE-helpers +/// \brief Clips and redistributes excess pixels based on the actual clip limit value +/// obtained from the other helper function actual_clip_limit +/// Reference - Graphic Gems 4, Pg. 474 +/// (http://cas.xav.free.fr/Graphics%20Gems%204%20-%20Paul%20S.%20Heckbert.pdf) +/// +template +void clip_and_redistribute(SrcHist const& src_hist, DstHist& dst_hist, double clip_limit = 0.03) +{ + using value_t = typename SrcHist::value_type; + double sum = src_hist.sum(); + double actual_clip_value = detail::actual_clip_limit(src_hist, clip_limit); + // double actual_clip_value = clip_limit; + long actual_clip_limit = actual_clip_value * sum; + double excess = 0; + std::for_each(src_hist.begin(), src_hist.end(), [&](value_t const& v) { + if (v.second > actual_clip_limit) + excess += v.second - actual_clip_limit; + }); + std::for_each(src_hist.begin(), src_hist.end(), [&](value_t const& v) { + if (v.second >= actual_clip_limit) + dst_hist[dst_hist.key_from_tuple(v.first)] = clip_limit * sum; + else + dst_hist[dst_hist.key_from_tuple(v.first)] = v.second + excess / src_hist.size(); + }); + long rem = long(excess) % src_hist.size(); + if (rem == 0) + return; + long period = round(src_hist.size() / rem); + std::size_t index = 0; + while (rem) + { + if (dst_hist(index) >= clip_limit * sum) + { + index = (index + 1) % src_hist.size(); + } + dst_hist(index)++; + rem--; + index = (index + period) % src_hist.size(); + } +} + +} // namespace detail + + +/// \fn void non_overlapping_interpolated_clahe +/// \ingroup AHE +/// @param src_view Input Source image view +/// @param dst_view Output Output image view +/// @param tile_width_x Input Tile width along x-axis to apply HE +/// @param tile_width_y Input Tile width along x-axis to apply HE +/// @param clip_limit Input Clipping limit to be applied +/// @param bin_width Input Bin widths for histogram +/// @param mask Input Specify if mask is to be used +/// @param src_mask Input Mask on input image to ignore specified pixels +/// \brief Performs local histogram equalization on tiles of size (tile_width_x, tile_width_y) +/// Then uses the clip limit to redistribute excess pixels above the limit uniformly to +/// other bins. The clip limit is specified as a fraction i.e. a bin's value is clipped +/// if bin_value >= clip_limit * (Total number of pixels in the tile) +/// +template +void non_overlapping_interpolated_clahe( + SrcView const& src_view, + DstView const& dst_view, + std::size_t tile_width_x = 20, + std::size_t tile_width_y = 20, + double clip_limit = 0.03, + std::size_t bin_width = 1.0, + bool mask = false, + std::vector> src_mask = {}) +{ + gil_function_requires>(); + gil_function_requires>(); + + static_assert( + color_spaces_are_compatible< + typename color_space_type::type, + typename color_space_type::type>::value, + "Source and destination views must have same color space"); + + using source_channel_t = typename channel_type::type; + using dst_channel_t = typename channel_type::type; + using coord_t = typename SrcView::x_coord_t; + + std::size_t const channels = num_channels::value; + coord_t const width = src_view.width(); + coord_t const height = src_view.height(); + std::size_t pixel_max = std::numeric_limits::max(); + std::size_t pixel_min = std::numeric_limits::min(); + + // Find control points + + std::vector sample_x; + coord_t sample_x1 = tile_width_x / 2; + coord_t sample_x2 = (tile_width_x + 1) / 2; + coord_t sample_y1 = tile_width_y / 2; + coord_t sample_y2 = (tile_width_y + 1) / 2; + + auto extend_left = tile_width_x; + auto extend_top = tile_width_y; + auto extend_right = (tile_width_x - width % tile_width_x) % tile_width_x + tile_width_x; + auto extend_bottom = (tile_width_y - height % tile_width_y) % tile_width_y + tile_width_y; + + auto new_width = width + extend_left + extend_right; + auto new_height = height + extend_top + extend_bottom; + + image padded_img(new_width, new_height); + + auto top_left_x = tile_width_x; + auto top_left_y = tile_width_y; + auto bottom_right_x = tile_width_x + width; + auto bottom_right_y = tile_width_y + height; + + copy_pixels(src_view, subimage_view(view(padded_img), top_left_x, top_left_y, width, height)); + + for (std::size_t k = 0; k < channels; k++) + { + std::vector> prev_row(new_width / tile_width_x), + next_row((new_width / tile_width_x)); + std::vector> prev_map( + new_width / tile_width_x), + next_map((new_width / tile_width_x)); + + coord_t prev = 0, next = 1; + auto channel_view = nth_channel_view(view(padded_img), k); + + for (std::ptrdiff_t i = top_left_y; i < bottom_right_y; ++i) + { + if ((i - sample_y1) / tile_width_y >= next || i == top_left_y) + { + if (i != top_left_y) + { + prev = next; + next++; + } + prev_row = next_row; + prev_map = next_map; + for (std::ptrdiff_t j = sample_x1; j < new_width; j += tile_width_x) + { + auto img_view = subimage_view( + channel_view, j - sample_x1, next * tile_width_y, + std::max( + std::min(tile_width_x + j - sample_x1, bottom_right_x) - + (j - sample_x1), + 0), + std::max( + std::min((next + 1) * tile_width_y, bottom_right_y) - + next * tile_width_y, + 0)); + + fill_histogram( + img_view, next_row[(j - sample_x1) / tile_width_x], bin_width, false, + false); + + detail::clip_and_redistribute( + next_row[(j - sample_x1) / tile_width_x], + next_row[(j - sample_x1) / tile_width_x], clip_limit); + + next_map[(j - sample_x1) / tile_width_x] = + histogram_equalization(next_row[(j - sample_x1) / tile_width_x]); + } + } + bool prev_row_mask = 1, next_row_mask = 1; + if (prev == 0) + prev_row_mask = false; + else if (next + 1 == new_height / tile_width_y) + next_row_mask = false; + for (std::ptrdiff_t j = top_left_x; j < bottom_right_x; ++j) + { + bool prev_col_mask = true, next_col_mask = true; + if ((j - sample_x1) / tile_width_x == 0) + prev_col_mask = false; + else if ((j - sample_x1) / tile_width_x + 1 == new_width / tile_width_x - 1) + next_col_mask = false; + + // Bilinear interpolation + point_t top_left( + (j - sample_x1) / tile_width_x * tile_width_x + sample_x1, + prev * tile_width_y + sample_y1); + point_t top_right(top_left.x + tile_width_x, top_left.y); + point_t bottom_left(top_left.x, top_left.y + tile_width_y); + point_t bottom_right(top_left.x + tile_width_x, top_left.y + tile_width_y); + + long double x_diff = top_right.x - top_left.x; + long double y_diff = bottom_left.y - top_left.y; + + long double x1 = (j - top_left.x) / x_diff; + long double x2 = (top_right.x - j) / x_diff; + long double y1 = (i - top_left.y) / y_diff; + long double y2 = (bottom_left.y - i) / y_diff; + + if (prev_row_mask == 0) + y1 = 1; + else if (next_row_mask == 0) + y2 = 1; + if (prev_col_mask == 0) + x1 = 1; + else if (next_col_mask == 0) + x2 = 1; + + long double numerator = + ((prev_row_mask & prev_col_mask) * x2 * + prev_map[(top_left.x - sample_x1) / tile_width_x][channel_view(j, i)] + + (prev_row_mask & next_col_mask) * x1 * + prev_map[(top_right.x - sample_x1) / tile_width_x][channel_view(j, i)]) * + y2 + + ((next_row_mask & prev_col_mask) * x2 * + next_map[(bottom_left.x - sample_x1) / tile_width_x][channel_view(j, i)] + + (next_row_mask & next_col_mask) * x1 * + next_map[(bottom_right.x - sample_x1) / tile_width_x][channel_view(j, i)]) * + y1; + + if (mask && !src_mask[i - top_left_y][j - top_left_x]) + { + dst_view(j - top_left_x, i - top_left_y) = + channel_convert( + static_cast(channel_view(i, j))); + } + else + { + dst_view(j - top_left_x, i - top_left_y) = + channel_convert(static_cast(numerator)); + } + } + } + } +} + +}} //namespace boost::gil + +#endif diff --git a/test/core/image_processing/adaptive_he.cpp b/test/core/image_processing/adaptive_he.cpp new file mode 100644 index 0000000000..df7068243f --- /dev/null +++ b/test/core/image_processing/adaptive_he.cpp @@ -0,0 +1,113 @@ +// +// Copyright 2020 Debabrata Mandal +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include + +#include + +namespace gil = boost::gil; + +double epsilon = 1.0; + +std::uint8_t image_matrix[] = +{ + 1, 1, 1, 1, + 3, 3, 3, 3, + 5, 5, 5, 5, + 7, 7, 7, 7 +}; +gil::gray8c_view_t gray_view = gil::interleaved_view(4, 4, reinterpret_cast(image_matrix), 4); + +void check_actual_clip_limit() +{ + gil::histogram h; + for(std::size_t i = 0; i < 100; i++) + { + if (i % 40 == 0) + { + h(i) = 60; + } + else + { + h(i) = 5; + } + } + double limit = 0.01; + double value = gil::detail::actual_clip_limit(h, limit); + + long actual_limit = round(value * h.sum()), max_bin_val = 0; + double excess = 0; + for(std::size_t i = 0; i < 100; i++) + { + if (h(i) > actual_limit) + excess += actual_limit - h(i); + max_bin_val = std::max(max_bin_val, h(i)); + } + BOOST_TEST((abs(excess / h.size() + actual_limit) - limit * h.sum()) < epsilon); +} + +void check_clip_and_redistribute() +{ + gil::histogram h, h2; + for(std::size_t i = 0; i < 100; i++) + { + if (i % 50 == 0) + { + h(i) = 60; + } + else + { + h(i) = 5; + } + } + bool check = true; + double limit = 0.001; + gil::detail::clip_and_redistribute(h, h2, limit); + for(std::size_t i = 0; i < 100; i++) + { + check = check & (abs(limit * h.sum() - h2(i)) < epsilon); + } + BOOST_TEST(check); +} + +void check_non_overlapping_interpolated_clahe() +{ + { + gil::gray8_image_t img1(4, 4), img2(4, 4), img3(4, 4); + gil::histogram_equalization(gray_view, view(img2)); + gil::non_overlapping_interpolated_clahe(gray_view, view(img3), 8, 8, 1.0); + BOOST_TEST(gil::equal_pixels(view(img2), view(img3))); + } + { + gil::gray8_image_t img1(8, 8), img2(8, 8), img3(4, 4); + gil::copy_pixels(gray_view, gil::subimage_view(view(img1), 0, 0, 4, 4)); + gil::copy_pixels(gray_view, gil::subimage_view(view(img1), 0, 4, 4, 4)); + gil::copy_pixels(gray_view, gil::subimage_view(view(img1), 4, 0, 4, 4)); + gil::copy_pixels(gray_view, gil::subimage_view(view(img1), 4, 4, 4, 4)); + gil::histogram_equalization(gray_view, view(img3)); + gil::non_overlapping_interpolated_clahe(view(img1), view(img2), 8, 8, 1.0); + BOOST_TEST(gil::equal_pixels(gil::subimage_view(view(img2), 0, 0, 4, 4), view(img3))); + BOOST_TEST(gil::equal_pixels(gil::subimage_view(view(img2), 0, 4, 4, 4), view(img3))); + BOOST_TEST(gil::equal_pixels(gil::subimage_view(view(img2), 4, 0, 4, 4), view(img3))); + BOOST_TEST(gil::equal_pixels(gil::subimage_view(view(img2), 4, 4, 4, 4), view(img3))); + } +} + +int main() +{ + check_actual_clip_limit(); + check_clip_and_redistribute(); + check_non_overlapping_interpolated_clahe(); + + return boost::report_errors(); +} From 81b4dc08bd9177bf9361194a66b0e70cfd6438e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Mon, 25 Jan 2021 02:34:04 +0100 Subject: [PATCH 13/51] ci: Add configuration for GitHub Actions (#544) Add basic GitHub Actions configuration based on mp11 Remove Actions jobs using GCC 4.7 and 4.8 - unsupported compilers Run b2 with --abbreviate-paths on Windows The -std=c++1z is broken for clang-4.0 but no need to test it Add -mbig-obj to GCC on Windows - That is to avoid string table overflow and file too big Define _GLIBCXX_USE_CXX11_ABI=0 for clang 3.5, 3.6, 3.7 - Should help avoid linker error: `undefined reference to std::ios_base::failure::failure(char const*, std::error_code const&)` Disable certain check in algorithm_channel_relation test for clang<3.8 --- .github/workflows/ci.yml | 195 ++++++++++++++++++ .../channel/algorithm_channel_relation.cpp | 6 + 2 files changed, 201 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..880ca948ed --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,195 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + - develop + - feature/** + +env: + LIBRARY: gil + UBSAN_OPTIONS: print_stacktrace=1 + +jobs: + posix: + strategy: + fail-fast: false + matrix: + include: + - toolset: gcc-4.9 + cxxstd: "11" + os: ubuntu-16.04 + install: g++-4.9 + - toolset: gcc-5 + cxxstd: "11,14,1z" + os: ubuntu-16.04 + - toolset: gcc-6 + cxxstd: "11,14,1z" + os: ubuntu-16.04 + install: g++-6 + - toolset: gcc-7 + cxxstd: "11,14,17" + os: ubuntu-18.04 + - toolset: gcc-8 + cxxstd: "11,14,17,2a" + os: ubuntu-18.04 + - toolset: gcc-9 + cxxstd: "11,14,17,2a" + os: ubuntu-18.04 + - toolset: gcc-10 + cxxstd: "11,14,17,2a" + os: ubuntu-18.04 + - toolset: clang + compiler: clang++-3.5 + cxxstd: "11,14" + define: "_GLIBCXX_USE_CXX11_ABI=0" + os: ubuntu-16.04 + install: clang-3.5 + - toolset: clang + compiler: clang++-3.6 + cxxstd: "11,14" + define: "_GLIBCXX_USE_CXX11_ABI=0" + os: ubuntu-16.04 + install: clang-3.6 + - toolset: clang + compiler: clang++-3.7 + cxxstd: "11,14" + define: "_GLIBCXX_USE_CXX11_ABI=0" + os: ubuntu-16.04 + install: clang-3.7 + - toolset: clang + compiler: clang++-3.8 + cxxstd: "11,14" + os: ubuntu-16.04 + install: clang-3.8 + - toolset: clang + compiler: clang++-3.9 + cxxstd: "11,14" + os: ubuntu-16.04 + install: clang-3.9 + - toolset: clang + compiler: clang++-4.0 + cxxstd: "11,14" + os: ubuntu-16.04 + install: clang-4.0 + - toolset: clang + compiler: clang++-5.0 + cxxstd: "11,14,1z" + os: ubuntu-16.04 + install: clang-5.0 + - toolset: clang + compiler: clang++-6.0 + cxxstd: "11,14,17" + os: ubuntu-18.04 + - toolset: clang + compiler: clang++-7 + cxxstd: "11,14,17" + os: ubuntu-18.04 + install: clang-7 + - toolset: clang + compiler: clang++-8 + cxxstd: "11,14,17,2a" + os: ubuntu-20.04 + - toolset: clang + compiler: clang++-9 + cxxstd: "11,14,17,2a" + os: ubuntu-20.04 + - toolset: clang + compiler: clang++-10 + cxxstd: "11,14,17,2a" + os: ubuntu-20.04 + - toolset: clang + cxxstd: "11,14,17,2a" + os: macos-10.15 + + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v2 + + - name: Install packages + if: matrix.install + run: sudo apt install ${{matrix.install}} + + - name: Setup Boost + run: | + REF=${GITHUB_BASE_REF:-$GITHUB_REF} + BOOST_BRANCH=develop && [ "$REF" == "master" ] && BOOST_BRANCH=master || true + cd .. + git clone -b $BOOST_BRANCH --depth 1 https://github.com/boostorg/boost.git boost-root + cd boost-root + cp -r $GITHUB_WORKSPACE/* libs/$LIBRARY + git submodule update --init tools/boostdep + python tools/boostdep/depinst/depinst.py --git_args "--jobs 3" $LIBRARY + ./bootstrap.sh + ./b2 -d0 headers + + - name: Create user-config.jam + if: matrix.compiler + run: | + echo "using ${{matrix.toolset}} : : ${{matrix.compiler}} ;" > ~/user-config.jam + + - name: Run tests + if: "!matrix.define" + run: | + cd ../boost-root + ./b2 -j3 libs/$LIBRARY/test toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} variant=debug,release + + - name: Run tests + if: matrix.define + run: | + cd ../boost-root + ./b2 -j3 libs/$LIBRARY/test toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} define=${{matrix.define}} variant=debug,release + + windows: + strategy: + fail-fast: false + matrix: + include: + - toolset: msvc-14.1 + cxxstd: "14,17,latest" + addrmd: 32,64 + os: windows-2016 + - toolset: msvc-14.2 + cxxstd: "14,17,latest" + addrmd: 32,64 + os: windows-2019 + - toolset: gcc + cxxstd: "11,14,17,2a" + addrmd: 64 + os: windows-2019 + + runs-on: ${{matrix.os}} + + steps: + - uses: actions/checkout@v2 + + - name: Setup Boost + shell: cmd + run: | + if "%GITHUB_BASE_REF%" == "" set GITHUB_BASE_REF=%GITHUB_REF% + set BOOST_BRANCH=develop + if "%GITHUB_BASE_REF%" == "master" set BOOST_BRANCH=master + cd .. + git clone -b %BOOST_BRANCH% --depth 1 https://github.com/boostorg/boost.git boost-root + cd boost-root + xcopy /s /e /q %GITHUB_WORKSPACE% libs\%LIBRARY%\ + git submodule update --init tools/boostdep + python tools/boostdep/depinst/depinst.py --git_args "--jobs 3" %LIBRARY% + cmd /c bootstrap + b2 -d0 headers + + - name: Run tests + if: startsWith(matrix.toolset, 'msvc') + shell: cmd + run: | + cd ../boost-root + b2 -j3 --abbreviate-paths libs/%LIBRARY%/test toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} address-model=${{matrix.addrmd}} variant=debug,release + - name: Run tests + if: startsWith(matrix.toolset, 'gcc') + shell: cmd + run: | + cd ../boost-root + b2 -j3 --abbreviate-paths libs/%LIBRARY%/test toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} address-model=${{matrix.addrmd}} cxxflags=-mbig-obj variant=debug,release diff --git a/test/core/channel/algorithm_channel_relation.cpp b/test/core/channel/algorithm_channel_relation.cpp index bad3a1d4bc..e7c301b7fe 100644 --- a/test/core/channel/algorithm_channel_relation.cpp +++ b/test/core/channel/algorithm_channel_relation.cpp @@ -31,7 +31,13 @@ void test_channel_relation() BOOST_TEST_GT(f.max_v_, f.min_v_); BOOST_TEST_NE(f.max_v_, f.min_v_); BOOST_TEST_EQ(f.min_v_, f.min_v_); +#if !defined(BOOST_CLANG) || (__clang_major__ == 3 && __clang_minor__ >= 8) + // This particular test fails with optimised build using clang 3.5 or 3.6 + // for unknown reasons. Volunteers are welcome to debug and confirm it is + // either the compiler bug or the library bug: + // b2 toolset=clang variant=release cxxstd=11 define=_GLIBCXX_USE_CXX11_ABI=0 libs/gil/test/core/channel//algorithm_channel_relation BOOST_TEST_NE(f.min_v_, one); // comparable to integral +#endif } struct test_channel_value From a37f12b3e9e90c968e2e416d33c339fa26e77226 Mon Sep 17 00:00:00 2001 From: Olzhas Zhumabek Date: Tue, 26 Jan 2021 04:31:39 +0600 Subject: [PATCH 14/51] Add implementation of Hough transforms (#512) Support construction from step_size, step_count, and a function for angles Implement angle and radious version of Hough line transform and adds a demo with static line that goes over secondary diagonal. Implement incremental line raster Implement naive line raster Implement Bresenham line raster Leave only Bresenham line rasterization Naive and incremental algorithms were removed because they are supposed to produce the same results anyway. The reason for diverging results is inaccuracy of floating point numbers Add circle rendering through trigonometric functions, using arctan(1 / (radius + 1)) as minimal angle step. Trigonometric circle rasterizer does not follow circle equation, but still produces very round shapes. A new testing methodology needs to be devised for this rasterizer. The new version accepts start and points inclusively and tries to use canonic representation during computations. Slope decided to be is (diff_y + 1) / (diff_x + 1). --- example/hough_transform_circle.cpp | 55 +++++++ example/hough_transform_line.cpp | 71 +++++++++ example/rasterizer_circle.cpp | 33 ++++ example/rasterizer_line.cpp | 42 +++++ include/boost/gil.hpp | 8 +- .../gil/image_processing/hough_parameter.hpp | 112 ++++++++++++++ .../gil/image_processing/hough_transform.hpp | 138 +++++++++++++++++ include/boost/gil/rasterization/circle.hpp | 126 +++++++++++++++ include/boost/gil/rasterization/line.hpp | 97 ++++++++++++ test/core/CMakeLists.txt | 1 + test/core/Jamfile | 1 + test/core/image_processing/CMakeLists.txt | 5 +- test/core/image_processing/Jamfile | 2 + .../hough_circle_transform.cpp | 83 ++++++++++ .../image_processing/hough_line_transform.cpp | 99 ++++++++++++ .../core/image_processing/hough_parameter.cpp | 78 ++++++++++ test/core/rasterization/CMakeLists.txt | 27 ++++ test/core/rasterization/Jamfile | 12 ++ test/core/rasterization/circle.cpp | 76 +++++++++ test/core/rasterization/line.cpp | 144 ++++++++++++++++++ 20 files changed, 1207 insertions(+), 3 deletions(-) create mode 100644 example/hough_transform_circle.cpp create mode 100644 example/hough_transform_line.cpp create mode 100644 example/rasterizer_circle.cpp create mode 100644 example/rasterizer_line.cpp create mode 100644 include/boost/gil/image_processing/hough_parameter.hpp create mode 100644 include/boost/gil/image_processing/hough_transform.hpp create mode 100644 include/boost/gil/rasterization/circle.hpp create mode 100644 include/boost/gil/rasterization/line.hpp create mode 100644 test/core/image_processing/hough_circle_transform.cpp create mode 100644 test/core/image_processing/hough_line_transform.cpp create mode 100644 test/core/image_processing/hough_parameter.cpp create mode 100644 test/core/rasterization/CMakeLists.txt create mode 100644 test/core/rasterization/Jamfile create mode 100644 test/core/rasterization/circle.cpp create mode 100644 test/core/rasterization/line.cpp diff --git a/example/hough_transform_circle.cpp b/example/hough_transform_circle.cpp new file mode 100644 index 0000000000..9f83da9d0d --- /dev/null +++ b/example/hough_transform_circle.cpp @@ -0,0 +1,55 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include + +#include +#include +#include + +namespace gil = boost::gil; + +int main() +{ + const std::size_t size = 128; + gil::gray8_image_t input_image(size, size); + auto input = gil::view(input_image); + + const std::ptrdiff_t circle_radius = 16; + const gil::point_t circle_center = {64, 64}; + const auto rasterizer = gil::midpoint_circle_rasterizer{}; + std::vector circle_points(rasterizer.point_count(circle_radius)); + rasterizer(circle_radius, circle_center, circle_points.begin()); + for (const auto& point : circle_points) + { + input(point) = std::numeric_limits::max(); + } + + const auto radius_parameter = + gil::hough_parameter::from_step_count(circle_radius, 3, 3); + const auto x_parameter = + gil::hough_parameter::from_step_count(circle_center.x, 3, 3); + const auto y_parameter = + gil::hough_parameter::from_step_count(circle_center.x, 3, 3); + + std::vector parameter_space_images( + radius_parameter.step_count, + gil::gray16_image_t(x_parameter.step_count, y_parameter.step_count)); + std::vector parameter_space_views(parameter_space_images.size()); + std::transform(parameter_space_images.begin(), parameter_space_images.end(), + parameter_space_views.begin(), + [](gil::gray16_image_t& img) + { + return gil::view(img); + }); + + gil::hough_circle_transform_brute(input, radius_parameter, x_parameter, y_parameter, + parameter_space_views.begin(), rasterizer); + std::cout << parameter_space_views[3](3, 3) << '\n'; +} diff --git a/example/hough_transform_line.cpp b/example/hough_transform_line.cpp new file mode 100644 index 0000000000..29c8fc6def --- /dev/null +++ b/example/hough_transform_line.cpp @@ -0,0 +1,71 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include +#include +#include +#include + +namespace gil = boost::gil; + +int main() +{ + std::ptrdiff_t size = 32; + gil::gray16_image_t input_image(size, size); + auto input_view = gil::view(input_image); + + // fill secondary diagonal with ones + // do note that origin is located at upper left, + // not bottom left as in usual plots + for (std::ptrdiff_t i = 0; i < size; ++i) + { + input_view(i, size - i - 1) = 1; + } + + // print vertically flipped for better understanding of origin location + for (std::ptrdiff_t y = size - 1; y >= 0; --y) + { + for (std::ptrdiff_t x = 0; x < size; ++x) + { + std::cout << input_view(x, y)[0] << ' '; + } + std::cout << '\n'; + } + + double minimum_theta_step = std::atan(1.0 / size); + // this is the expected theta + double _45_degrees = gil::detail::pi / 4; + double _5_degrees = gil::detail::pi / 36; + std::size_t step_count = 5; + auto theta_parameter = + gil::make_theta_parameter(_45_degrees, _5_degrees, input_view.dimensions()); + auto expected_radius = static_cast(std::round(std::cos(_45_degrees) * size)); + auto radius_parameter = + gil::hough_parameter::from_step_size(expected_radius, 7, 1); + gil::gray32_image_t accumulator_array_image(theta_parameter.step_count, + radius_parameter.step_count); + auto accumulator_array = gil::view(accumulator_array_image); + gil::hough_line_transform(input_view, accumulator_array, theta_parameter, radius_parameter); + std::cout << "expecting maximum at theta=" << _45_degrees << " and radius=" << expected_radius + << '\n'; + for (std::size_t theta_index = 0; theta_index < theta_parameter.step_count; ++theta_index) + { + for (std::size_t radius_index = 0; radius_index < radius_parameter.step_count; + ++radius_index) + { + double current_theta = + theta_parameter.start_point + theta_index * theta_parameter.step_size; + std::ptrdiff_t current_radius = + radius_parameter.start_point + radius_parameter.step_size * radius_index; + std::cout << "theta: " << current_theta << " radius: " << current_radius + << " accumulated value: " << accumulator_array(theta_index, radius_index)[0] + << '\n'; + } + } +} diff --git a/example/rasterizer_circle.cpp b/example/rasterizer_circle.cpp new file mode 100644 index 0000000000..4d2997afb0 --- /dev/null +++ b/example/rasterizer_circle.cpp @@ -0,0 +1,33 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include +#include +#include +#include + +namespace gil = boost::gil; + +int main() +{ + const std::ptrdiff_t size = 256; + gil::gray8_image_t buffer_image(size, size); + auto buffer = gil::view(buffer_image); + + const std::ptrdiff_t radius = 64; + const auto rasterizer = gil::trigonometric_circle_rasterizer{}; + std::vector circle_points(rasterizer.point_count(radius)); + rasterizer(radius, {128, 128}, circle_points.begin()); + for (const auto& point : circle_points) + { + buffer(point) = std::numeric_limits::max(); + } + + gil::write_view("circle.png", buffer, gil::png_tag{}); +} diff --git a/example/rasterizer_line.cpp b/example/rasterizer_line.cpp new file mode 100644 index 0000000000..9f02a347a4 --- /dev/null +++ b/example/rasterizer_line.cpp @@ -0,0 +1,42 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include + +#include +#include + +namespace gil = boost::gil; + +const std::ptrdiff_t size = 256; + +void line_bresenham(std::ptrdiff_t width, std::ptrdiff_t height, const std::string& output_name) +{ + const auto rasterizer = gil::bresenham_line_rasterizer{}; + std::vector line_points(rasterizer.point_count(width, height)); + + gil::gray8_image_t image(size, size); + auto view = gil::view(image); + + rasterizer({0, 0}, {width - 1, height - 1}, line_points.begin()); + for (const auto& point : line_points) + { + view(point) = std::numeric_limits::max(); + } + + gil::write_view(output_name, view, gil::png_tag{}); +} + +int main() +{ + line_bresenham(256, 256, "line-bresenham-256-256.png"); + line_bresenham(256, 128, "line-bresenham-256-128.png"); + line_bresenham(256, 1, "line-bresenham-256-1.png"); + line_bresenham(1, 256, "line-bresenham-1-256.png"); +} diff --git a/include/boost/gil.hpp b/include/boost/gil.hpp index b5db505d9b..797b1e6006 100644 --- a/include/boost/gil.hpp +++ b/include/boost/gil.hpp @@ -24,6 +24,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -38,13 +42,13 @@ #include #include #include +#include +#include #include #include #include #include #include #include -#include -#include #endif diff --git a/include/boost/gil/image_processing/hough_parameter.hpp b/include/boost/gil/image_processing/hough_parameter.hpp new file mode 100644 index 0000000000..97fcd8cde5 --- /dev/null +++ b/include/boost/gil/image_processing/hough_parameter.hpp @@ -0,0 +1,112 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#ifndef BOOST_GIL_IMAGE_PROCESSING_HOUGH_PARAMETER_HPP +#define BOOST_GIL_IMAGE_PROCESSING_HOUGH_PARAMETER_HPP + +#include +#include +#include + +namespace boost +{ +namespace gil +{ +/// \ingroup HoughTransform +/// \brief A type to encapsulate Hough transform parameter range +/// +/// This type provides a way to express value range for a parameter +/// as well as some factory functions to simplify initialization +template +struct hough_parameter +{ + T start_point; + T step_size; + std::size_t step_count; + + /// \ingroup HoughTransform + /// \brief Create Hough parameter from value neighborhood and step count + /// + /// This function will take start_point as middle point, and in both + /// directions will try to walk half_step_count times until distance of + /// neighborhood is reached + static hough_parameter from_step_count(T start_point, T neighborhood, + std::size_t half_step_count) + { + T step_size = neighborhood / half_step_count; + std::size_t step_count = half_step_count * 2 + 1; + // explicitly fill out members, as aggregate init will error out with narrowing + hough_parameter parameter; + parameter.start_point = start_point - neighborhood; + parameter.step_size = step_size; + parameter.step_count = step_count; + return parameter; + } + + /// \ingroup HoughTransform + /// \brief Create Hough parameter from value neighborhood and step size + /// + /// This function will take start_point as middle point, and in both + /// directions will try to walk step_size at a time until distance of + /// neighborhood is reached + static hough_parameter from_step_size(T start_point, T neighborhood, T step_size) + { + std::size_t step_count = + 2 * static_cast(std::floor(neighborhood / step_size)) + 1; + // do not use step_size - neighborhood, as step_size might not allow + // landing exactly on that value when starting from start_point + // also use parentheses on step_count / 2 because flooring is exactly + // what we want + + // explicitly fill out members, as aggregate init will error out with narrowing + hough_parameter parameter; + parameter.start_point = start_point - step_size * (step_count / 2); + parameter.step_size = step_size; + parameter.step_count = step_count; + return parameter; + } +}; + +/// \ingroup HoughTransform +/// \brief Calculate minimum angle which would be observable if walked on a circle +/// +/// When drawing a circle or moving around a point in circular motion, it is +/// important to not do too many steps, but also to not have disconnected +/// trajectory. This function will calculate the minimum angle that is observable +/// when walking on a circle or tilting a line. +/// WARNING: do keep in mind IEEE 754 quirks, e.g. no-associativity, +/// no-commutativity and precision. Do not expect expressions that are +/// mathematically the same to produce the same values +inline double minimum_angle_step(point_t dimensions) +{ + auto longer_dimension = dimensions.x > dimensions.y ? dimensions.x : dimensions.y; + return std::atan2(1, longer_dimension); +} + +/// \ingroup HoughTransform +/// \brief Create a Hough transform parameter with optimal angle step +/// +/// Due to computational intensity and noise sensitivity of Hough transform, +/// having any candidates missed or computed again is problematic. This function +/// will properly encapsulate optimal value range around approx_angle with amplitude of +/// neighborhood in each direction. +/// WARNING: do keep in mind IEEE 754 quirks, e.g. no-associativity, +/// no-commutativity and precision. Do not expect expressions that are +/// mathematically the same to produce the same values +inline hough_parameter make_theta_parameter(double approx_angle, double neighborhood, + point_t dimensions) +{ + auto angle_step = minimum_angle_step(dimensions); + + // std::size_t step_count = + // 2 * static_cast(std::floor(neighborhood / angle_step)) + 1; + // return {approx_angle - angle_step * (step_count / 2), angle_step, step_count}; + return hough_parameter::from_step_size(approx_angle, neighborhood, angle_step); +} +}} // namespace boost::gil +#endif diff --git a/include/boost/gil/image_processing/hough_transform.hpp b/include/boost/gil/image_processing/hough_transform.hpp new file mode 100644 index 0000000000..6fd7148886 --- /dev/null +++ b/include/boost/gil/image_processing/hough_transform.hpp @@ -0,0 +1,138 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#ifndef BOOST_GIL_IMAGE_PROCESSING_HOUGH_TRANSFORM_HPP +#define BOOST_GIL_IMAGE_PROCESSING_HOUGH_TRANSFORM_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace boost { namespace gil { +/// \defgroup HoughTransform +/// \brief A family of shape detectors that are specified by equation +/// +/// Hough transform is a method of mapping (voting) an object which can be described by +/// equation to single point in accumulator array (also called parameter space). +/// Each set pixel in edge map votes for every shape it can be part of. +/// Circle and ellipse transforms are very costly to brute force, while +/// non-brute-forcing algorithms tend to gamble on probabilities. + +/// \ingroup HoughTransform +/// \brief Vote for best fit of a line in parameter space +/// +/// The input must be an edge map with grayscale pixels. Be aware of overflow inside +/// accumulator array. The theta parameter is best computed through factory function +/// provided in hough_parameter.hpp +template +void hough_line_transform(const InputView& input_view, const OutputView& accumulator_array, + const hough_parameter& theta, + const hough_parameter& radius) +{ + std::ptrdiff_t r_lower_bound = radius.start_point; + std::ptrdiff_t r_upper_bound = r_lower_bound + radius.step_size * (radius.step_count - 1); + + for (std::ptrdiff_t y = 0; y < input_view.height(); ++y) + { + for (std::ptrdiff_t x = 0; x < input_view.width(); ++x) + { + if (!input_view(x, y)[0]) + { + continue; + } + + for (std::size_t theta_index = 0; theta_index < theta.step_count; ++theta_index) + { + double theta_current = + theta.start_point + theta.step_size * static_cast(theta_index); + std::ptrdiff_t current_r = + std::llround(static_cast(x) * std::cos(theta_current) + + static_cast(y) * std::sin(theta_current)); + if (current_r < r_lower_bound || current_r > r_upper_bound) + { + continue; + } + std::size_t r_index = static_cast( + std::llround((current_r - radius.start_point) / radius.step_size)); + // one more safety guard to not get out of bounds + if (r_index < radius.step_count) + { + accumulator_array(theta_index, r_index)[0] += 1; + } + } + } + } +} + +/// \ingroup HoughTransform +/// \brief Vote for best fit of a circle in parameter space according to rasterizer +/// +/// The input must be an edge map with grayscale pixels. Be aware of overflow inside +/// accumulator array. Rasterizer is used to rasterize a circle for voting. The circle +/// then is translated for every origin (x, y) in x y parameter space. For available +/// circle rasterizers, please look at rasterization/circle.hpp +template +void hough_circle_transform_brute(const ImageView& input, + const hough_parameter radius_parameter, + const hough_parameter x_parameter, + const hough_parameter& y_parameter, + ForwardIterator d_first, Rasterizer rasterizer) +{ + const auto width = input.width(); + const auto height = input.height(); + for (std::size_t radius_index = 0; radius_index < radius_parameter.step_count; ++radius_index) + { + const auto radius = radius_parameter.start_point + + radius_parameter.step_size * static_cast(radius_index); + std::vector circle_points(rasterizer.point_count(radius)); + rasterizer(radius, {0, 0}, circle_points.begin()); + // sort by scanline to improve cache coherence for row major images + std::sort(circle_points.begin(), circle_points.end(), + [](const point_t& lhs, const point_t& rhs) { return lhs.y < rhs.y; }); + const auto translate = [](std::vector& points, point_t offset) { + std::transform(points.begin(), points.end(), points.begin(), [offset](point_t point) { + return point_t(point.x + offset.x, point.y + offset.y); + }); + }; + + // in case somebody passes iterator to likes of std::vector + typename std::iterator_traits::reference current_image = *d_first; + + // the algorithm has to traverse over parameter space and look at input, instead + // of vice versa, as otherwise it will call translate too many times, as input + // is usually bigger than the coordinate portion of parameter space. + // This might cause extensive cache misses + for (std::size_t x_index = 0; x_index < x_parameter.step_count; ++x_index) + { + for (std::size_t y_index = 0; y_index < y_parameter.step_count; ++y_index) + { + const std::ptrdiff_t x = x_parameter.start_point + x_index * x_parameter.step_size; + const std::ptrdiff_t y = y_parameter.start_point + y_index * y_parameter.step_size; + + auto translated_circle = circle_points; + translate(translated_circle, {x, y}); + for (const auto& point : translated_circle) + { + if (input(point)) + { + ++current_image(x_index, y_index)[0]; + } + } + } + } + ++d_first; + } +} + +}} // namespace boost::gil + +#endif diff --git a/include/boost/gil/rasterization/circle.hpp b/include/boost/gil/rasterization/circle.hpp new file mode 100644 index 0000000000..31c3cf6caf --- /dev/null +++ b/include/boost/gil/rasterization/circle.hpp @@ -0,0 +1,126 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#ifndef BOOST_GIL_RASTERIZATION_CIRCLE_HPP +#define BOOST_GIL_RASTERIZATION_CIRCLE_HPP + +#include +#include +#include +#include + +namespace boost { namespace gil { +/// \defgroup CircleRasterization +/// \ingroup Rasterization +/// \brief Circle rasterization algorithms +/// +/// The main problems are connectivity and equation following. Circle can be easily moved +/// to new offset, and rotation has no effect on it (not recommended to do rotation). + +/// \ingroup CircleRasterization +/// \brief Rasterize trigonometric circle according to radius by sine and radius by cosine +/// +/// This rasterizer is the one used that is used in standard Hough circle transform in +/// the books. It is also quite expensive to compute. +/// WARNING: the product of this rasterizer does not follow circle equation, even though it +/// produces quite round like shapes. +struct trigonometric_circle_rasterizer +{ + /// \brief Calculates minimum angle step that is distinguishable when walking on circle + /// + /// It is important to not have disconnected circle and to not compute unnecessarily, + /// thus the result of this function is used when rendering. + double minimum_angle_step(std::ptrdiff_t radius) const noexcept + { + const auto diameter = radius * 2 - 1; + return std::atan2(1.0, diameter); + } + + /// \brief Calculate the amount of points that rasterizer will output + std::ptrdiff_t point_count(std::ptrdiff_t radius) const noexcept + { + return 8 * static_cast( + std::round(detail::pi / 4 / minimum_angle_step(radius)) + 1); + } + + /// \brief perform rasterization and output into d_first + template + void operator()(std::ptrdiff_t radius, point_t offset, RandomAccessIterator d_first) const + { + const double minimum_angle_step = std::atan2(1.0, radius); + auto translate_mirror_points = [&d_first, offset](point_t p) { + *d_first++ = point_t{offset.x + p.x, offset.y + p.y}; + *d_first++ = point_t{offset.x + p.x, offset.y - p.y}; + *d_first++ = point_t{offset.x - p.x, offset.y + p.y}; + *d_first++ = point_t{offset.x - p.x, offset.y - p.y}; + *d_first++ = point_t{offset.x + p.y, offset.y + p.x}; + *d_first++ = point_t{offset.x + p.y, offset.y - p.x}; + *d_first++ = point_t{offset.x - p.y, offset.y + p.x}; + *d_first++ = point_t{offset.x - p.y, offset.y - p.x}; + }; + const std::ptrdiff_t iteration_count = point_count(radius) / 8; + double angle = 0; + // do note that + 1 was done inside count estimation, thus <= is not needed, only < + for (std::ptrdiff_t i = 0; i < iteration_count; ++i, angle += minimum_angle_step) + { + std::ptrdiff_t x = static_cast(std::round(radius * std::cos(angle))); + std::ptrdiff_t y = static_cast(std::round(radius * std::sin(angle))); + translate_mirror_points({x, y}); + } + } +}; + +/// \ingroup CircleRasterization +/// \brief Perform circle rasterization according to Midpoint algorithm +/// +/// This algorithm givess reasonable output and is cheap to compute. +/// reference: +/// https://en.wikipedia.org/wiki/Midpoint_circle_algorithm +struct midpoint_circle_rasterizer +{ + /// \brief Calculate the amount of points that rasterizer will output + std::ptrdiff_t point_count(std::ptrdiff_t radius) const noexcept + { + // the reason for pulling 8 out is so that when the expression radius * cos(45 degrees) + // is used, it would yield the same result as here + // + 1 at the end is because the point at radius itself is computed as well + return 8 * static_cast( + std::round(radius * std::cos(boost::gil::detail::pi / 4)) + 1); + } + + /// \brief perform rasterization and output into d_first + template + void operator()(std::ptrdiff_t radius, point_t offset, RAIterator d_first) const + { + auto translate_mirror_points = [&d_first, offset](point_t p) { + *d_first++ = point_t{offset.x + p.x, offset.y + p.y}; + *d_first++ = point_t{offset.x + p.x, offset.y - p.y}; + *d_first++ = point_t{offset.x - p.x, offset.y + p.y}; + *d_first++ = point_t{offset.x - p.x, offset.y - p.y}; + *d_first++ = point_t{offset.x + p.y, offset.y + p.x}; + *d_first++ = point_t{offset.x + p.y, offset.y - p.x}; + *d_first++ = point_t{offset.x - p.y, offset.y + p.x}; + *d_first++ = point_t{offset.x - p.y, offset.y - p.x}; + }; + std::ptrdiff_t iteration_distance = point_count(radius) / 8; + std::ptrdiff_t y_current = radius; + std::ptrdiff_t r_squared = radius * radius; + translate_mirror_points({0, y_current}); + for (std::ptrdiff_t x = 1; x < iteration_distance; ++x) + { + std::ptrdiff_t midpoint = x * x + y_current * y_current - y_current - r_squared; + if (midpoint > 0) + { + --y_current; + } + translate_mirror_points({x, y_current}); + } + } +}; +}} // namespace boost::gil +#endif diff --git a/include/boost/gil/rasterization/line.hpp b/include/boost/gil/rasterization/line.hpp new file mode 100644 index 0000000000..1ff91b6a35 --- /dev/null +++ b/include/boost/gil/rasterization/line.hpp @@ -0,0 +1,97 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include + +namespace boost +{ +namespace gil +{ +/// \defgroup Rasterization +/// \brief A set of functions to rasterize shapes +/// +/// Due to images being discrete, most shapes require specialized algorithms to +/// handle rasterization efficiently and solve problem of connectivity and being +/// close to the original shape. + +/// \defgroup LineRasterization +/// \ingroup Rasterization +/// \brief A set of rasterizers for lines +/// +/// The main problem with line rasterization is to do it efficiently, e.g. less +/// floating point operations. There are multiple algorithms that on paper +/// should reach the same result, but due to quirks of IEEE-754 they don't. +/// Please select one and stick to it if possible. At the moment only Bresenham +/// rasterizer is implemented. + +/// \ingroup LineRasterization +/// \brief Rasterize a line according to Bresenham algorithm +/// +/// Do note that if either width or height is 1, slope is set to zero. +/// reference: +/// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm#:~:text=Bresenham's%20line%20algorithm%20is%20a,straight%20line%20between%20two%20points. +struct bresenham_line_rasterizer +{ + constexpr std::ptrdiff_t point_count(std::ptrdiff_t width, std::ptrdiff_t height) const noexcept + { + return width > height ? width : height; + } + + std::ptrdiff_t point_count(point_t start, point_t end) const noexcept + { + const auto abs_width = std::abs(end.x - start.x) + 1; + const auto abs_height = std::abs(end.y - start.y) + 1; + return point_count(abs_width, abs_height); + } + + template + void operator()(point_t start, point_t end, RandomAccessIterator d_first) const + { + if (start == end) + { + // put the point and immediately exit, as later on division by zero will + // occur + *d_first = start; + return; + } + + auto width = std::abs(end.x - start.x) + 1; + auto height = std::abs(end.y - start.y) + 1; + bool const needs_flip = width < height; + if (needs_flip) + { + // transpose the coordinate system if uncomfortable angle detected + std::swap(width, height); + std::swap(start.x, start.y); + std::swap(end.x, end.y); + } + std::ptrdiff_t const x_increment = end.x >= start.x ? 1 : -1; + std::ptrdiff_t const y_increment = end.y >= start.y ? 1 : -1; + double const slope = + height == 1 ? 0 : static_cast(height) / static_cast(width); + std::ptrdiff_t y = start.y; + double error_term = 0; + for (std::ptrdiff_t x = start.x; x != end.x; x += x_increment) + { + // transpose coordinate system back to proper form if needed + *d_first++ = needs_flip ? point_t{y, x} : point_t{x, y}; + error_term += slope; + if (error_term >= 0.5) + { + --error_term; + y += y_increment; + } + } + *d_first++ = needs_flip ? point_t{end.y, end.x} : end; + } +}; + +}} // namespace boost::gil diff --git a/test/core/CMakeLists.txt b/test/core/CMakeLists.txt index 6abbfdee88..2c812d014b 100644 --- a/test/core/CMakeLists.txt +++ b/test/core/CMakeLists.txt @@ -40,3 +40,4 @@ add_subdirectory(image_view) add_subdirectory(algorithm) add_subdirectory(image_processing) add_subdirectory(histogram) +add_subdirectory(rasterization) diff --git a/test/core/Jamfile b/test/core/Jamfile index 45992e30ee..c7c03ee376 100644 --- a/test/core/Jamfile +++ b/test/core/Jamfile @@ -33,3 +33,4 @@ build-project image_view ; build-project algorithm ; build-project image_processing ; build-project histogram ; +build-project rasterization ; diff --git a/test/core/image_processing/CMakeLists.txt b/test/core/image_processing/CMakeLists.txt index 2ad32f4f28..2fc51c1d4a 100644 --- a/test/core/image_processing/CMakeLists.txt +++ b/test/core/image_processing/CMakeLists.txt @@ -35,7 +35,10 @@ foreach(_name box_filter median_filter sobel_scharr - anisotropic_diffusion) + anisotropic_diffusion + hough_parameter + hough_line_transform + hough_circle_transform) set(_test t_core_image_processing_${_name}) set(_target test_core_image_processing_${_name}) diff --git a/test/core/image_processing/Jamfile b/test/core/image_processing/Jamfile index be4c755c02..a67c4d9f21 100644 --- a/test/core/image_processing/Jamfile +++ b/test/core/image_processing/Jamfile @@ -20,3 +20,5 @@ run sobel_scharr.cpp ; run box_filter.cpp ; run median_filter.cpp ; run anisotropic_diffusion.cpp ; +run hough_line_transform.cpp ; +run hough_circle_transform.cpp ; diff --git a/test/core/image_processing/hough_circle_transform.cpp b/test/core/image_processing/hough_circle_transform.cpp new file mode 100644 index 0000000000..9b0a3563da --- /dev/null +++ b/test/core/image_processing/hough_circle_transform.cpp @@ -0,0 +1,83 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace gil = boost::gil; + +template +void exact_fit_test(std::ptrdiff_t radius, gil::point_t offset, Rasterizer rasterizer) +{ + std::vector circle_points(rasterizer.point_count(radius)); + rasterizer(radius, offset, circle_points.begin()); + // const std::ptrdiff_t diameter = radius * 2 - 1; + const std::ptrdiff_t width = offset.x + radius + 1; + const std::ptrdiff_t height = offset.y + radius + 1; + gil::gray8_image_t image(width, height); + auto input = gil::view(image); + + for (const auto& point : circle_points) + { + input(point) = std::numeric_limits::max(); + } + + using param_t = gil::hough_parameter; + const auto radius_parameter = param_t{radius, 0, 1}; + // const auto x_parameter = param_t::from_step_count(offset.x, neighborhood, half_step_count); + // const auto y_parameter = param_t::from_step_count(offset.y, neighborhood, half_step_count); + const auto x_parameter = param_t{offset.x, 0, 1}; + const auto y_parameter = param_t{offset.y, 0, 1}; + + std::vector output_images( + radius_parameter.step_count, + gil::gray16_image_t(x_parameter.step_count, y_parameter.step_count)); + std::vector output_views(radius_parameter.step_count); + std::transform(output_images.begin(), output_images.end(), output_views.begin(), + [](gil::gray16_image_t& img) + { + return gil::view(img); + }); + gil::hough_circle_transform_brute(input, radius_parameter, x_parameter, y_parameter, + output_views.begin(), rasterizer); + if (output_views[0](0, 0) != rasterizer.point_count(radius)) + { + std::cout << "accumulated value: " << static_cast(output_views[0](0, 0)) + << " expected value: " << rasterizer.point_count(radius) << "\n\n"; + } + BOOST_TEST(output_views[0](0, 0) == rasterizer.point_count(radius)); +} + +int main() +{ + const int test_dim_length = 20; + for (std::ptrdiff_t radius = 5; radius < test_dim_length; ++radius) + { + for (std::ptrdiff_t x_offset = radius; x_offset < radius + test_dim_length; ++x_offset) + { + for (std::ptrdiff_t y_offset = radius; y_offset < radius + test_dim_length; ++y_offset) + { + + exact_fit_test(radius, {x_offset, y_offset}, gil::midpoint_circle_rasterizer{}); + exact_fit_test(radius, {x_offset, y_offset}, + gil::trigonometric_circle_rasterizer{}); + } + } + } + + return boost::report_errors(); +} diff --git a/test/core/image_processing/hough_line_transform.cpp b/test/core/image_processing/hough_line_transform.cpp new file mode 100644 index 0000000000..f0ed992fa3 --- /dev/null +++ b/test/core/image_processing/hough_line_transform.cpp @@ -0,0 +1,99 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gil = boost::gil; + +const std::ptrdiff_t width = 64; + +void translate(std::vector& points, std::ptrdiff_t intercept) +{ + std::transform(points.begin(), points.end(), points.begin(), + [intercept](gil::point_t point) + { + return gil::point_t{point.x, point.y + intercept}; + }); +} + +void hough_line_test(std::ptrdiff_t height, std::ptrdiff_t intercept) +{ + const auto rasterizer = gil::bresenham_line_rasterizer{}; + gil::gray8_image_t image(width, width, gil::gray8_pixel_t(0)); + auto input = gil::view(image); + std::vector line_points(rasterizer.point_count(width, height)); + rasterizer({0, 0}, {width - 1, height - 1}, line_points.begin()); + translate(line_points, intercept); + for (const auto& p : line_points) + { + input(p) = 255; + } + + double alpha = std::atan2(height, width); + const double theta = alpha + gil::detail::pi / 2; + const auto minimum_angle_step = gil::minimum_angle_step({width, height}); + const double expected_alpha = std::round(alpha / minimum_angle_step) * minimum_angle_step; + const double expected_radius = std::round(intercept * std::cos(expected_alpha)); + const double expected_theta = std::round(theta / minimum_angle_step) * minimum_angle_step; + + const std::size_t half_step_count = 3; + const std::ptrdiff_t expected_index = 3; + const std::size_t accumulator_array_dimensions = half_step_count * 3 + 1; + auto radius_param = + gil::hough_parameter::from_step_count(expected_radius, 3, half_step_count); + auto theta_param = gil::make_theta_parameter( + expected_theta, minimum_angle_step * half_step_count, {width, height}); + gil::gray32_image_t accumulator_array_image( + accumulator_array_dimensions, accumulator_array_dimensions, gil::gray32_pixel_t(0)); + auto accumulator_array = gil::view(accumulator_array_image); + gil::hough_line_transform(input, accumulator_array, theta_param, radius_param); + + auto max_element_iterator = + std::max_element(accumulator_array.begin(), accumulator_array.end()); + gil::point_t candidates[] = { + {expected_index - 1, expected_index - 1}, {expected_index, expected_index - 1}, + {expected_index + 1, expected_index - 1}, {expected_index - 1, expected_index}, + {expected_index, expected_index}, {expected_index + 1, expected_index}, + {expected_index - 1, expected_index + 1}, {expected_index, expected_index + 1}, + {expected_index + 1, expected_index + 1}}; + bool match_found = false; + for (std::size_t i = 0; i < 9; ++i) + { + if (*max_element_iterator == accumulator_array(candidates[i])) + { + match_found = true; + break; + } + } + BOOST_TEST(match_found); +} + +int main() +{ + for (std::ptrdiff_t height = 1; height < width; ++height) + { + for (std::ptrdiff_t intercept = 1; intercept < width - height; ++intercept) + { + hough_line_test(height, intercept); + } + } + return boost::report_errors(); +} diff --git a/test/core/image_processing/hough_parameter.cpp b/test/core/image_processing/hough_parameter.cpp new file mode 100644 index 0000000000..8d1c7b4452 --- /dev/null +++ b/test/core/image_processing/hough_parameter.cpp @@ -0,0 +1,78 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include + +namespace gil = boost::gil; + +void from_step_count_test() +{ + const double middle_point = 0.5; + const std::size_t step_count = 5; + const double neighborhood = 1.0; + auto param = + gil::hough_parameter::from_step_count(middle_point, neighborhood, step_count); + BOOST_TEST(param.start_point == middle_point - neighborhood); + BOOST_TEST(param.step_count == step_count * 2 + 1); + BOOST_TEST(param.step_size == neighborhood / step_count); + + bool middle_point_occured = false; + for (std::size_t i = 0; i < param.step_count; ++i) + { + auto current = param.start_point + param.step_size * i; + if (current == middle_point) + { + middle_point_occured = true; + break; + } + } + BOOST_TEST(middle_point_occured); +} + +void from_step_size_test(const double middle_point, const double step_size, + const double neighborhood) +{ + const std::size_t expected_step_count = + static_cast(neighborhood / step_size) * 2 + 1; + auto param = + gil::hough_parameter::from_step_size(middle_point, neighborhood, step_size); + BOOST_TEST(param.start_point == middle_point - step_size * std::floor(expected_step_count / 2)); + BOOST_TEST(param.step_count == expected_step_count); + BOOST_TEST(param.step_size == step_size); + + bool middle_point_occured = false; + for (std::size_t i = 0; i < param.step_count; ++i) + { + auto current = param.start_point + param.step_size * i; + if (current == middle_point) + { + middle_point_occured = true; + break; + } + } + BOOST_TEST(middle_point_occured); +} + +void minimum_step_angle_test(const std::ptrdiff_t width, const std::ptrdiff_t height) +{ + const auto bigger_dim = width > height ? width : height; + const double expected_angle = std::atan2(1.0, bigger_dim); + BOOST_TEST(expected_angle == gil::minimum_angle_step({width, height})); +} + +int main() +{ + from_step_count_test(); + // ideal case + from_step_size_test(2.0, 0.25, 1.0); + from_step_size_test(5.0, 2, 5.0); + minimum_step_angle_test(1200, 800); + minimum_step_angle_test(800, 1200); + return boost::report_errors(); +} diff --git a/test/core/rasterization/CMakeLists.txt b/test/core/rasterization/CMakeLists.txt new file mode 100644 index 0000000000..2ab6dd2b43 --- /dev/null +++ b/test/core/rasterization/CMakeLists.txt @@ -0,0 +1,27 @@ +# Boost.GIL (Generic Image Library) - tests +# +# Copyright 2020 Olzhas Zhumabek +# +# Use, modification and distribution are subject to the Boost Software License, +# Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +foreach(_name + line + circle) + set(_test t_core_rasterization_${_name}) + set(_target test_core_rasterization_${_name}) + + add_executable(${_target} "") + target_sources(${_target} PRIVATE ${_name}.cpp) + target_link_libraries(${_target} + PRIVATE + gil_compile_options + gil_include_directories + gil_dependencies) + target_compile_definitions(${_target} PRIVATE BOOST_GIL_USE_CONCEPT_CHECK) + add_test(NAME ${_test} COMMAND ${_target}) + + unset(_name) + unset(_target) +endforeach() diff --git a/test/core/rasterization/Jamfile b/test/core/rasterization/Jamfile new file mode 100644 index 0000000000..750eb46697 --- /dev/null +++ b/test/core/rasterization/Jamfile @@ -0,0 +1,12 @@ +# Boost.GIL (Generic Image Library) - tests +# +# Copyright 2020 Olzhas Zhumabek +# +# Use, modification and distribution are subject to the Boost Software License, +# Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +import testing ; + +run line.cpp ; +run circle.cpp ; diff --git a/test/core/rasterization/circle.cpp b/test/core/rasterization/circle.cpp new file mode 100644 index 0000000000..68a91f4fd3 --- /dev/null +++ b/test/core/rasterization/circle.cpp @@ -0,0 +1,76 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +namespace gil = boost::gil; + +template +void test_rasterizer_follows_equation(std::ptrdiff_t radius, Rasterizer rasterizer) +{ + + std::vector circle_points(rasterizer.point_count(radius)); + std::ptrdiff_t r_squared = radius * radius; + rasterizer(radius, {0, 0}, circle_points.begin()); + std::vector first_octant(rasterizer.point_count(radius) / 8); + + for (std::size_t i = 0, octant_index = 0; i < circle_points.size(); i += 8, ++octant_index) + { + first_octant[octant_index] = circle_points[i]; + } + + for (const auto& point : first_octant) + { + double y_exact = std::sqrt(radius * radius - point.x * point.x); + std::ptrdiff_t lower_result = static_cast(std::floor(y_exact)); + std::ptrdiff_t upper_result = static_cast(std::ceil(y_exact)); + BOOST_TEST(point.y >= lower_result && point.y <= upper_result); + } +} + +template +void test_connectivity(std::ptrdiff_t radius, Rasterizer rasterizer) +{ + std::vector circle_points(rasterizer.point_count(radius)); + rasterizer(radius, {radius, radius}, circle_points.begin()); + for (std::size_t i = 0; i < 8; ++i) + { + std::vector octant(circle_points.size() / 8); + for (std::size_t octant_index = i, index = 0; octant_index < circle_points.size(); + octant_index += 8, ++index) + { + octant[index] = circle_points[octant_index]; + } + + for (std::size_t index = 1; index < octant.size(); ++index) + { + const auto diff_x = std::abs(octant[index].x - octant[index - 1].x); + const auto diff_y = std::abs(octant[index].y - octant[index - 1].y); + BOOST_TEST_LE(diff_x, 1); + BOOST_TEST_LE(diff_y, 1); + } + } +} + +int main() +{ + for (std::ptrdiff_t radius = 5; radius <= 512; ++radius) + { + test_rasterizer_follows_equation(radius, gil::midpoint_circle_rasterizer{}); + // TODO: find out a new testing procedure for trigonometric rasterizer + // test_equation_following(radius, gil::trigonometric_circle_rasterizer{}); + test_connectivity(radius, gil::midpoint_circle_rasterizer{}); + test_connectivity(radius, gil::trigonometric_circle_rasterizer{}); + } + + return boost::report_errors(); +} diff --git a/test/core/rasterization/line.cpp b/test/core/rasterization/line.cpp new file mode 100644 index 0000000000..42e3bf7469 --- /dev/null +++ b/test/core/rasterization/line.cpp @@ -0,0 +1,144 @@ +// Boost.GIL (Generic Image Library) - tests +// +// Copyright 2020 Olzhas Zhumabek +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace gil = boost::gil; + +namespace boost +{ +namespace gil +{ +std::ostream& operator<<(std::ostream& os, const point_t p) +{ + os << "{x=" << p.x << ", y=" << p.y << "}"; + return os; +} +}} // namespace boost::gil + +using line_type = std::vector; + +struct endpoints +{ + gil::point_t start; + gil::point_t end; +}; + +endpoints create_endpoints(std::mt19937& twister, + std::uniform_int_distribution& distr) +{ + gil::point_t start{distr(twister), distr(twister)}; + gil::point_t end{distr(twister), distr(twister)}; + return {start, end}; +} + +line_type create_line(endpoints points) +{ + gil::bresenham_line_rasterizer rasterizer; + line_type forward_line(rasterizer.point_count(points.start, points.end)); + rasterizer(points.start, points.end, forward_line.begin()); + return forward_line; +} + +void test_start_end(const line_type& line_points, endpoints points) +{ + BOOST_TEST_EQ(line_points.front(), points.start); + BOOST_TEST_EQ(line_points.back(), points.end); +} + +// Look at TODO below +// void test_two_way_equivalence(const line_type& forward, line_type backward) +// { +// std::reverse(backward.begin(), backward.end()); +// BOOST_TEST_ALL_EQ(forward.begin(), forward.end(), backward.begin(), backward.end()); +// } + +void test_connectivity(line_type const& line_points) +{ + for (std::size_t i = 1; i < line_points.size(); ++i) + { + const auto x_diff = std::abs(line_points[i].x - line_points[i - 1].x); + const auto y_diff = std::abs(line_points[i].y - line_points[i - 1].y); + BOOST_TEST_LE(x_diff, 1); + BOOST_TEST_LE(y_diff, 1); + } +} + +void test_bresenham_rasterizer_follows_equation(line_type line_points) +{ + auto start = line_points.front(); + auto end = line_points.back(); + + auto width = std::abs(end.x - start.x) + 1; + auto height = std::abs(end.y - start.y) + 1; + if (width < height) + { + std::swap(width, height); + std::transform(line_points.begin(), line_points.end(), line_points.begin(), + [](gil::point_t p) + { + return gil::point_t{p.y, p.x}; + }); + // update start and end + start = line_points.front(); + end = line_points.back(); + } + const double sign = [start, end]() + { + auto const width_sign = end.x < start.x; + auto const height_sign = end.y < start.y; + auto const slope_sign = width_sign != height_sign; + return slope_sign ? -1 : 1; + }(); + const double slope = static_cast(height) / static_cast(width); + const double intercept = + static_cast(start.y) - sign * slope * static_cast(start.x); + for (const auto& point : line_points) + { + double const expected_y = sign * slope * static_cast(point.x) + intercept; + auto const difference = + std::abs(point.y - static_cast(std::round(expected_y))); + BOOST_TEST_LE(difference, static_cast(slope + 1)); + } +} + +int main() +{ + const std::ptrdiff_t size = 256; + for (std::size_t seed = 0; seed <= 100; ++seed) + { + std::mt19937 twister(seed); + std::uniform_int_distribution distr(0, size - 1); + const std::size_t sample_count = 100; + for (std::size_t sample_index = 0; sample_index < sample_count; ++sample_index) + { + auto endpoints = create_endpoints(twister, distr); + auto forward_line = create_line(endpoints); + test_start_end(forward_line, endpoints); + // TODO: figure out if forward/backward equivalence is possible to provide + // auto backward_line = create_line({endpoints.end, endpoints.start}); + // test_two_way_equivalence(forward_line, backward_line); + test_connectivity(forward_line); + // test_connectivity(backward_line); + test_bresenham_rasterizer_follows_equation(forward_line); + // test_bresenham_rasterizer_follows_equation(backward_line); + } + } + + return boost::report_errors(); +} From 434e78f76b5179369738f25a553b74cbaf886218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Tue, 26 Jan 2021 01:05:03 +0100 Subject: [PATCH 15/51] ci: R.I.P. Travis CI - Long live GitHub Actions Follows: - https://github.com/boostorg/gil/pull/544 Outstanding tasks: - https://github.com/boostorg/gil/issues/549 - https://github.com/boostorg/gil/issues/548 --- .ci/upload_docs.sh | 53 --------- .travis.yml | 283 --------------------------------------------- CMakeLists.txt | 2 +- CONTRIBUTING.md | 10 +- README.md | 8 +- test/Jamfile | 2 +- 6 files changed, 11 insertions(+), 347 deletions(-) delete mode 100755 .ci/upload_docs.sh delete mode 100644 .travis.yml diff --git a/.ci/upload_docs.sh b/.ci/upload_docs.sh deleted file mode 100755 index f8b98b5f55..0000000000 --- a/.ci/upload_docs.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -set -e # Exit with nonzero exit code if anything fails - -SOURCE_BRANCH="master" -TARGET_BRANCH="gh-pages" - -# Pull requests and commits to other branches shouldn't try to deploy, just build to verify -if [ "$TRAVIS_PULL_REQUEST" != "false" ] || \ - [ "$TRAVIS_BRANCH" != master -a \ - "$TRAVIS_BRANCH" != develop -a \ - "$TRAVIS_BRANCH" != doc -a \ - "$TRAVIS_BRANCH" != ci ]; then - echo "No docs to upload." - exit 0 -fi - -if [ -z "$GH_TOKEN" ]; then - echo "Error: GH_TOKEN is undefined" - exit 1 -fi - -# Save some useful information -REPO=`git config remote.origin.url` -SHA=`git rev-parse --verify HEAD` - -# doc happens to contain the "html" tree that we want to push -# into the gh-pages branch. So we step into that directory, create a new repo, -# set the remote appropriately, then commit and push. -cd doc -git init -git config user.name "Travis CI" -git config user.email "travis-ci" - -# Make sure 'GH_TOKEN' is set (as 'secure' variable) in .travis.yml -git remote add upstream "https://$GH_TOKEN@github.com/boostorg/gil.git" -git fetch upstream -git reset upstream/gh-pages - -# Prepare version. -if [ "$TRAVIS_BRANCH" = develop -o "$TRAVIS_BRANCH" = doc ]; then - mkdir -p develop/doc/ - cp ../index.html develop/ - cp ../doc/index.html develop/doc/ - cp -a html develop/doc/ - git add -A develop -else - git add html index.html -fi -# Commit the new version. -git commit -m "Deploy to GitHub Pages: ${SHA}" - -# Now that we're all set up, we can push. -git push -q upstream HEAD:gh-pages diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5c18d49687..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,283 +0,0 @@ -# Copyright 2016 Peter Dimov -# Distributed under the Boost Software License, Version 1.0. -# (See accompanying file LICENSE_1_0.txt or copy at http://boost.org/LICENSE_1_0.txt) -dist: xenial - -language: cpp - -sudo: false - -os: - - linux - - osx - -env: - matrix: - - BOGUS_JOB=true - -matrix: - fast_finish: true - exclude: - - env: BOGUS_JOB=true - include: - - os: linux - env: COMPILER=g++-6 VARIANT=debug TOOLSET=gcc CXXSTD=11 B2_OPTIONS="coverage=on" - addons: - apt: - packages: - - g++-6 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - - lcov - sources: - - ubuntu-toolchain-r-test - after_success: - - bash libs/gil/.ci/coverage.sh - - - os: linux - env: COMPILER=g++-5 VARIANT=debug TOOLSET=gcc CXXSTD=11 TEST_HEADERS=1 - addons: - apt: - packages: - - g++-5 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=g++-5 VARIANT=release TOOLSET=gcc CXXSTD=14 - addons: - apt: - packages: - - g++-5 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=g++-6 VARIANT=debug TOOLSET=gcc CXXSTD=14 - addons: - apt: - packages: - - g++-6 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=g++-6 VARIANT=release TOOLSET=gcc CXXSTD=11 - addons: - apt: - packages: - - g++-6 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=g++-7 VARIANT=debug TOOLSET=gcc CXXSTD=17 - addons: - apt: - packages: - - g++-7 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=g++-7 VARIANT=release TOOLSET=gcc CXXSTD=17 - addons: - apt: - packages: - - g++-7 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=g++-9 VARIANT=debug TOOLSET=gcc CXXSTD=2a - addons: - apt: - packages: - - g++-9 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=g++-9 VARIANT=release TOOLSET=gcc CXXSTD=2a - addons: - apt: - packages: - - g++-9 - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=clang++-3.9 VARIANT=debug TOOLSET=clang CXXSTD=11 - addons: - apt: - packages: - - clang-3.9 - - libstdc++-6-dev - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-precise-3.9 - - - os: linux - env: COMPILER=clang++-3.9 VARIANT=release TOOLSET=clang CXXSTD=11 - addons: - apt: - packages: - - clang-3.9 - - libstdc++-6-dev - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-precise-3.9 - - - os: linux - env: COMPILER=clang++-6.0 VARIANT=gil_ubsan_integer TOOLSET=clang CXXSTD=11 B2_OPTIONS="visibility=global" UBSAN_OPTIONS='print_stacktrace=1' - addons: - apt: - packages: - - clang-6.0 - - libstdc++-7-dev - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=clang++-6.0 VARIANT=gil_ubsan_nullability TOOLSET=clang CXXSTD=11 B2_OPTIONS="visibility=global" UBSAN_OPTIONS='print_stacktrace=1' - addons: - apt: - packages: - - clang-6.0 - - libstdc++-7-dev - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: linux - env: COMPILER=clang++-6.0 VARIANT=gil_ubsan_undefined TOOLSET=clang CXXSTD=11 B2_OPTIONS="visibility=global" UBSAN_OPTIONS='print_stacktrace=1' - addons: - apt: - packages: - - clang-6.0 - - libstdc++-7-dev - - libpng-dev - - libjpeg-dev - - libtiff5-dev - - libraw-dev - sources: - - ubuntu-toolchain-r-test - - - os: osx - env: COMPILER=clang++ VARIANT=debug TOOLSET=clang CXXSTD=11 - - - os: osx - env: COMPILER=clang++ VARIANT=release TOOLSET=clang CXXSTD=11 - - - env: - - DOC=1 - - secure: "UHit2f6Hq2pkHvx8rfrQvFacYiQKVO3vrCbNuDi/VSAIzQjRnqCqE06y4vpXLMsXf62TvBeCBStIuI8g+HP8B+f39oGb/9Om+yIgN/yes47R4sLFKFbRiOS6sfCIefJp7Kx7GSFf81xWxStpIU4QaSsk8Dlt5xyurTWXFSde+lQ=" - addons: - apt: - packages: - - doxygen - - python3 - - python3-pip - - python3-setuptools - -install: - - |- - if [ "$DOC" ]; then - pip3 --version - pip3 install --upgrade pip - pip3 install --user sphinx>=2.1 - sphinx-build --version - fi - - cd .. - - $TRAVIS_BUILD_DIR/.ci/get-boost.sh $TRAVIS_BRANCH $TRAVIS_BUILD_DIR - - cd boost-root - - export BOOST_ROOT=$(pwd) - - ./bootstrap.sh - -script: - - |- - if [ "$DOC" ]; then - echo "using doxygen ;" > ~/user-config.jam - ./b2 libs/gil/doc - else - echo "using $TOOLSET : : $COMPILER : ;" > ~/user-config.jam - ./b2 -j 2 $B2_OPTIONS toolset=$TOOLSET variant=$VARIANT cxxstd=$CXXSTD libs/gil/test/core - ./b2 -j 2 $B2_OPTIONS toolset=$TOOLSET variant=$VARIANT cxxstd=$CXXSTD libs/gil/test/legacy - ./b2 -j 2 $B2_OPTIONS toolset=$TOOLSET variant=$VARIANT cxxstd=$CXXSTD libs/gil/test/extension/dynamic_image - ./b2 -j 2 $B2_OPTIONS toolset=$TOOLSET variant=$VARIANT cxxstd=$CXXSTD libs/gil/test/extension/numeric - ./b2 -j 2 $B2_OPTIONS toolset=$TOOLSET variant=$VARIANT cxxstd=$CXXSTD libs/gil/test/extension/toolbox - ./b2 -j 2 $B2_OPTIONS toolset=$TOOLSET variant=$VARIANT cxxstd=$CXXSTD libs/gil/test/extension/io//simple - fi - -after_success: -# Upload docs only when building upstream. - - |- - if [ "$DOC" -a \ - "$TRAVIS_REPO_SLUG" = "boostorg/gil" -a \ - "$TRAVIS_PULL_REQUEST" = "false" ]; then - export GH_TOKEN - cd libs/gil - .ci/upload_docs.sh - fi - -notifications: - email: - on_success: always - webhooks: - urls: - - https://webhooks.gitter.im/e/f59b626f2ed08f3d30ab - # options: [always|never|change] - on_success: change # default: always - on_failure: always # default: always - on_start: change # default: never - on_cancel: always # default: always - on_error: always # default: always diff --git a/CMakeLists.txt b/CMakeLists.txt index 52327b933a..fc5e60123b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,7 @@ message(STATUS "Boost.GIL: Require C++${CMAKE_CXX_STANDARD}") set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Avoid warnings flood on Travis CI, AppVeyor, CircleCI, Azure Pipelines +# Avoid warnings flood from CI builds if(DEFINED ENV{CI} OR DEFINED ENV{AGENT_JOBSTATUS} OR DEFINED ENV{GITHUB_ACTIONS}) set(BOOST_GIL_BUILD_CI ON) message(STATUS "Boost.GIL: Turning off detailed compiler warnings for CI build short log") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aae35c138f..058cb45ebd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -233,10 +233,10 @@ git push feature/foo Finally, sign in to your GitHub account and [create a pull request](https://help.github.com/articles/creating-a-pull-request/). -Your pull request will be automatically built and tests will run on Travis CI -and AppVeyor (see [README](README.md) for builds status). Please, keep an eye -on those CI builds and correct any problems detected in your contribution -by updating your pull request. +Your pull request will be automatically built and tests will run on GitHub ACtions, +Azure Pipelines, AppVeyor and Circle CI (see [README](README.md) for builds status). +Please, keep an eye on those CI builds and correct any problems detected in your +contribution by updating your pull request. ### 5. Update your pull request @@ -300,7 +300,7 @@ request from reviewer, just add new commits: cd libs/gil git checkout feature/foo git add -A -git commit -m "Fix build Travis CI failures" +git commit -m "Fix CI build failure" git push feature/foo ``` diff --git a/README.md b/README.md index 0c7367c59a..62895dae58 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ [![Conan](https://img.shields.io/badge/on-conan-blue.svg)](https://bintray.com/bincrafters/public-conan/boost_gil%3Abincrafters) [![Vcpkg](https://img.shields.io/badge/on-vcpkg-blue.svg)](https://github.com/Microsoft/vcpkg/tree/master/ports/boost-gil) -Documentation | AppVeyor | Azure Pipelines | Travis CI | CircleCI | Regression ---------------|-----------------|-----------------|-----------------|-----------------|------------ -[![develop](https://img.shields.io/badge/doc-develop-blue.svg)](https://boostorg.github.io/gil/develop/) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h/branch/develop?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/develop) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=develop)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=develop) | [![Travis](https://travis-ci.org/boostorg/gil.svg?branch=develop)](https://travis-ci.org/boostorg/gil) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/develop.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/develop) | [![gil](https://img.shields.io/badge/gil-develop-blue.svg)](http://www.boost.org/development/tests/develop/developer/gil.html) -[![master](https://img.shields.io/badge/doc-master-blue.svg)](https://boostorg.github.io/gil/) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/master) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=master)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=master) | [![Travis](https://travis-ci.org/boostorg/gil.svg?branch=master)](https://travis-ci.org/boostorg/gil) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/master.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/master) | [![gil](https://img.shields.io/badge/gil-master-blue.svg)](http://www.boost.org/development/tests/master/developer/gil.html) +Documentation | GitHub Actions | Azure Pipelines | CircleCI | Regression +--------------|----------------|-----------------|-----------------|------------ +[![develop](https://img.shields.io/badge/doc-develop-blue.svg)](https://boostorg.github.io/gil/develop/) | [![GitHub Actions](https://github.com/boostorg/gil/workflows/CI/badge.svg?branch=develop)](https://github.com/boostorg/gil/actions?query=branch:develop) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h/branch/develop?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/develop) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=develop)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=develop) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/develop.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/develop) | [![gil](https://img.shields.io/badge/gil-develop-blue.svg)](http://www.boost.org/development/tests/develop/developer/gil.html) +[![master](https://img.shields.io/badge/doc-master-blue.svg)](https://boostorg.github.io/gil/) | [![GitHub Actions](https://github.com/boostorg/gil/workflows/CI/badge.svg?branch=master)](https://github.com/boostorg/gil/actions?query=branch:master) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/master) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=master)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=master) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/master.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/master) | [![gil](https://img.shields.io/badge/gil-master-blue.svg)](http://www.boost.org/development/tests/master/developer/gil.html) # Boost.GIL diff --git a/test/Jamfile b/test/Jamfile index cfca2f4436..f787b72a86 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -16,7 +16,7 @@ import regex ; import sequence ; import testing ; -# Avoid warnings flood on Travis CI, AppVeyor, CircleCI, Azure Pipelines, GitHub Actions +# Avoid warnings flood from CI builds if ! [ os.environ CI ] && ! [ os.environ AGENT_JOBSTATUS ] && ! [ os.environ GITHUB_ACTIONS ] { DEVELOPMENT_EXTRA_WARNINGS = From 4e0f815f5e27edb315093a3f632f611bb7ffad16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Wed, 27 Jan 2021 07:07:51 +0100 Subject: [PATCH 16/51] build: Remove most of uses of cxxflags from test/Jamfile (#550) Most uses of cxxflags are wrong. Use b2 warnings instead, e.g. warnings=pedantic --- test/Jamfile | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/test/Jamfile b/test/Jamfile index f787b72a86..99fed7abd5 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -16,32 +16,12 @@ import regex ; import sequence ; import testing ; -# Avoid warnings flood from CI builds -if ! [ os.environ CI ] && ! [ os.environ AGENT_JOBSTATUS ] && ! [ os.environ GITHUB_ACTIONS ] -{ - DEVELOPMENT_EXTRA_WARNINGS = - msvc:"-W4" - gcc:"-pedantic -Wextra -Wcast-align -Wconversion -Wfloat-equal -Wshadow -Wsign-promo -Wstrict-aliasing -Wunused-parameter" - clang,debug:"-pedantic -Wextra -Wcast-align -Wconversion -Wfloat-equal -Wshadow -Wsign-promo -Wstrict-aliasing -Wunused-parameter -Wsign-conversion" - clang,release:"-pedantic -Wextra -Wcast-align -Wconversion -Wfloat-equal -Wshadow -Wsign-promo -Wstrict-aliasing -Wunused-parameter -Wsign-conversion" - darwin:"-pedantic -Wextra -Wcast-align -Wconversion -Wfloat-equal -Wshadow -Wsign-promo -Wstrict-aliasing -Wunused-parameter" - ; -} -else -{ - DEVELOPMENT_EXTRA_WARNINGS = - msvc:"-W1" - ; -} - project : requirements . # TODO: Enable concepts check for all, not just test/core #BOOST_GIL_USE_CONCEPT_CHECK=1 - msvc:"-bigobj" - msvc:on msvc:_SCL_SECURE_NO_DEPRECATE msvc:_CRT_SECURE_NO_WARNINGS msvc:_CRT_NONSTDC_NO_DEPRECATE From 6f0a061363ffa1f1ed0b2bcd509d3c0ce2a058a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Thu, 28 Jan 2021 09:21:29 +0100 Subject: [PATCH 17/51] docs: Clarify PR update procedure in CONTRIBUTING.md [ci skip] --- CONTRIBUTING.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 058cb45ebd..5492bd7e52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,12 +61,13 @@ please follow the workflow explained in this document. - **DO** ensure each commit successfully builds. The entire PR must pass all tests in the Continuous Integration (CI) system before it'll be merged. - **DO** ensure any new features or changes to existing behaviours are covered with test cases. -- **DO** address PR feedback in an additional commit(s) rather than amending the existing - commits, and only rebase/squash them when necessary. This makes it easier for reviewers - to track changes. +- **DO** address PR feedback in an additional commit(s) rather than amending the existing commits. + This makes it easier for reviewers to track changes. +- **DO** sync your PR branch with the upstream `develop` branch frequently resolving any conflicts if necessary. + You can either `git merge upstream/develop` or `git rebase upstream/develop` with `git push --force` for the latter. + The merge may make it easier for reviewers to track changes though. - **DO** assume that the [Squash and Merge] will be used to merge your commit unless you request otherwise in the PR. -- **DO** NOT fix merge conflicts using a merge commit. Prefer git rebase. - **DO** NOT submit changes to the original legacy tests, see [test/legacy/README.md](test/legacy/README.md). From 5d63cf6798c3985cd02aa703274b2c9a85ff24f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Thu, 28 Jan 2021 22:15:50 +0100 Subject: [PATCH 18/51] Fix more warnings in examples Completing https://github.com/boostorg/gil/pull/545 --- example/harris.cpp | 6 +++++- example/hessian.cpp | 10 +++++----- example/hvstack.hpp | 3 +-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/example/harris.cpp b/example/harris.cpp index 81233fe38a..fd9bede286 100644 --- a/example/harris.cpp +++ b/example/harris.cpp @@ -71,7 +71,11 @@ void apply_gaussian_blur(gil::gray8_view_t input_view, gil::gray8_view_t output_ constexpr static auto filterHeight = 5ull; constexpr static auto filterWidth = 5ull; constexpr static double filter[filterHeight][filterWidth] = { - 2, 4, 6, 4, 2, 4, 9, 12, 9, 4, 5, 12, 15, 12, 5, 4, 9, 12, 9, 4, 2, 4, 5, 4, 2, + { 2, 4, 6, 4, 2 }, + { 4, 9, 12, 9, 4 }, + { 5, 12, 15, 12, 5}, + { 4, 9, 12, 9, 4 }, + { 2, 4, 5, 4, 2 } }; constexpr double factor = 1.0 / 159; constexpr double bias = 0.0; diff --git a/example/hessian.cpp b/example/hessian.cpp index d12f51ddb0..e27199f5f4 100644 --- a/example/hessian.cpp +++ b/example/hessian.cpp @@ -64,11 +64,11 @@ void apply_gaussian_blur(gil::gray8_view_t input_view, gil::gray8_view_t output_ constexpr static auto filter_width = 5ull; constexpr static double filter[filter_height][filter_width] = { - 2, 4, 6, 4, 2, - 4, 9, 12, 9, 4, - 5, 12, 15, 12, 5, - 4, 9, 12, 9, 4, - 2, 4, 5, 4, 2, + { 2, 4, 6, 4, 2 }, + { 4, 9, 12, 9, 4 }, + { 5, 12, 15, 12, 5 }, + { 4, 9, 12, 9, 4 }, + { 2, 4, 5, 4, 2 } }; constexpr double factor = 1.0 / 159; constexpr double bias = 0.0; diff --git a/example/hvstack.hpp b/example/hvstack.hpp index 1b4daaaf13..8f120a10b6 100644 --- a/example/hvstack.hpp +++ b/example/hvstack.hpp @@ -1,6 +1,7 @@ #include "boost/gil/image_view_factory.hpp" #include #include +#include #include #include #include @@ -15,7 +16,6 @@ void hstack(const std::vector& views, const View& output_view) } auto height = views.front().height(); - auto width = views.front().width(); for (const auto& view : views) { if (view.height() != height) @@ -50,7 +50,6 @@ image hstack(const std::vector& views) throw std::invalid_argument("empty views vector is passed - cannot create stacked image"); } - auto dimensions = views.front().dimensions(); std::ptrdiff_t full_width = std::accumulate(views.begin(), views.end(), 0, [](std::ptrdiff_t old, const View& view) { return old + view.width(); }); From 063385398fe7690e8b6e6611d0d6e1b851e636d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Thu, 28 Jan 2021 22:32:43 +0100 Subject: [PATCH 19/51] Fix warnings about abs called without std qualification --- .../gil/image_processing/adaptive_histogram_equalization.hpp | 2 +- include/boost/gil/image_processing/histogram_matching.hpp | 4 ++-- test/core/histogram/utilities.cpp | 5 +++-- test/core/image_processing/adaptive_he.cpp | 5 +++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp b/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp index 9dcbfba928..d56746094a 100644 --- a/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp +++ b/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp @@ -63,7 +63,7 @@ double actual_clip_limit(SrcHist const& src_hist, double cliplimit = 0.03) if (v.second > middle) excess += v.second - middle; }); - if (abs(excess - (cliplimit - middle) * num_bins) < epsilon) + if (std::abs(excess - (cliplimit - middle) * num_bins) < epsilon) break; else if (excess > (cliplimit - middle) * num_bins) high = middle - 1; diff --git a/include/boost/gil/image_processing/histogram_matching.hpp b/include/boost/gil/image_processing/histogram_matching.hpp index ea49a5c153..7019e278c6 100644 --- a/include/boost/gil/image_processing/histogram_matching.hpp +++ b/include/boost/gil/image_processing/histogram_matching.hpp @@ -97,8 +97,8 @@ std::map histogram_matching( { start--; } - if (abs(cumltv_refhist[ref_keys[start]] - src_val) > - abs(cumltv_refhist(std::min(ref_max, std::get<0>(ref_keys[start + 1]))) - + if (std::abs(cumltv_refhist[ref_keys[start]] - src_val) > + std::abs(cumltv_refhist(std::min(ref_max, std::get<0>(ref_keys[start + 1]))) - src_val)) { inverse_mapping[std::get<0>(src_keys[j])] = diff --git a/test/core/histogram/utilities.cpp b/test/core/histogram/utilities.cpp index 4f26a05b41..72b40e9477 100644 --- a/test/core/histogram/utilities.cpp +++ b/test/core/histogram/utilities.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -51,7 +52,7 @@ void check_normalize() bool check = true; for (std::size_t i = 0; i < 64; i++) { - check = check & (abs(expected[i] - h1(i)) < epsilon); + check = check & (std::abs(expected[i] - h1(i)) < epsilon); } BOOST_TEST(check); @@ -73,7 +74,7 @@ void check_normalize() bool check2 = true; for (std::size_t i = 0; i < 64; i++) { - check2 = check2 & (abs(expected2[i/8][i%8] - h2(i/8,i%8)) < epsilon); + check2 = check2 & (std::abs(expected2[i/8][i%8] - h2(i/8,i%8)) < epsilon); } BOOST_TEST(check2); } diff --git a/test/core/image_processing/adaptive_he.cpp b/test/core/image_processing/adaptive_he.cpp index df7068243f..b625d0b6b5 100644 --- a/test/core/image_processing/adaptive_he.cpp +++ b/test/core/image_processing/adaptive_he.cpp @@ -13,6 +13,7 @@ #include +#include #include namespace gil = boost::gil; @@ -53,7 +54,7 @@ void check_actual_clip_limit() excess += actual_limit - h(i); max_bin_val = std::max(max_bin_val, h(i)); } - BOOST_TEST((abs(excess / h.size() + actual_limit) - limit * h.sum()) < epsilon); + BOOST_TEST((std::abs(excess / h.size() + actual_limit) - limit * h.sum()) < epsilon); } void check_clip_and_redistribute() @@ -75,7 +76,7 @@ void check_clip_and_redistribute() gil::detail::clip_and_redistribute(h, h2, limit); for(std::size_t i = 0; i < 100; i++) { - check = check & (abs(limit * h.sum() - h2(i)) < epsilon); + check = check & (std::abs(limit * h.sum() - h2(i)) < epsilon); } BOOST_TEST(check); } From 6007d74667f9079ebed4475de48906f3eef573ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Thu, 28 Jan 2021 22:43:31 +0100 Subject: [PATCH 20/51] Fix some clang -Wunused-variable warnings --- .../adaptive_histogram_equalization.hpp | 2 -- test/core/channel/algorithm_channel_relation.cpp | 5 +++-- test/legacy/pixel_iterator.cpp | 10 ++++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp b/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp index d56746094a..b1c189d948 100644 --- a/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp +++ b/include/boost/gil/image_processing/adaptive_histogram_equalization.hpp @@ -161,8 +161,6 @@ void non_overlapping_interpolated_clahe( std::size_t const channels = num_channels::value; coord_t const width = src_view.width(); coord_t const height = src_view.height(); - std::size_t pixel_max = std::numeric_limits::max(); - std::size_t pixel_min = std::numeric_limits::min(); // Find control points diff --git a/test/core/channel/algorithm_channel_relation.cpp b/test/core/channel/algorithm_channel_relation.cpp index e7c301b7fe..28d155f94f 100644 --- a/test/core/channel/algorithm_channel_relation.cpp +++ b/test/core/channel/algorithm_channel_relation.cpp @@ -21,8 +21,6 @@ template void test_channel_relation() { using fixture_t = fixture::channel; - using channel_value_t = typename fixture_t::channel_value_t; - channel_value_t const one = 1; fixture_t f; BOOST_TEST_LE(f.min_v_, f.max_v_); @@ -31,11 +29,14 @@ void test_channel_relation() BOOST_TEST_GT(f.max_v_, f.min_v_); BOOST_TEST_NE(f.max_v_, f.min_v_); BOOST_TEST_EQ(f.min_v_, f.min_v_); + #if !defined(BOOST_CLANG) || (__clang_major__ == 3 && __clang_minor__ >= 8) // This particular test fails with optimised build using clang 3.5 or 3.6 // for unknown reasons. Volunteers are welcome to debug and confirm it is // either the compiler bug or the library bug: // b2 toolset=clang variant=release cxxstd=11 define=_GLIBCXX_USE_CXX11_ABI=0 libs/gil/test/core/channel//algorithm_channel_relation + using channel_value_t = typename fixture_t::channel_value_t; + channel_value_t const one = 1; BOOST_TEST_NE(f.min_v_, one); // comparable to integral #endif } diff --git a/test/legacy/pixel_iterator.cpp b/test/legacy/pixel_iterator.cpp index 1b4a6d2a4a..419f7e649d 100644 --- a/test/legacy/pixel_iterator.cpp +++ b/test/legacy/pixel_iterator.cpp @@ -19,6 +19,11 @@ #include #include +#if defined(BOOST_CLANG) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-variable" +#endif + using namespace boost::gil; using namespace std; @@ -353,3 +358,8 @@ int main() return EXIT_FAILURE; } } + +#if defined(BOOST_CLANG) +#pragma clang diagnostic pop +#endif + From 422ca82fe58d5aed90f86a2b043a402a2dc48a53 Mon Sep 17 00:00:00 2001 From: Cypre55 <57396709+Cypre55@users.noreply.github.com> Date: Fri, 29 Jan 2021 03:12:30 +0530 Subject: [PATCH 21/51] Removed two instaces of boost.mpl (#551) Removed "#include " at include/boost/gil/detail/mp11/hpp and test/core/pixel/test_fixture.hpp --- include/boost/gil/detail/mp11.hpp | 1 - test/core/pixel/test_fixture.hpp | 1 - 2 files changed, 2 deletions(-) diff --git a/include/boost/gil/detail/mp11.hpp b/include/boost/gil/detail/mp11.hpp index 2778978faf..8c4eec0305 100644 --- a/include/boost/gil/detail/mp11.hpp +++ b/include/boost/gil/detail/mp11.hpp @@ -10,7 +10,6 @@ #define BOOST_GIL_DETAIL_MP11_HPP #include -#include // required by dynamic_image and boost::variant (?) namespace boost { namespace gil { namespace detail { diff --git a/test/core/pixel/test_fixture.hpp b/test/core/pixel/test_fixture.hpp index c9ef03f32a..d2633356b7 100644 --- a/test/core/pixel/test_fixture.hpp +++ b/test/core/pixel/test_fixture.hpp @@ -21,7 +21,6 @@ #include #include -#include // for compatibility with Boost.Test #include #include From 2e2764225f81fff3eb90e56b55635880d954f3b0 Mon Sep 17 00:00:00 2001 From: theroyn Date: Sat, 30 Jan 2021 22:11:51 +0200 Subject: [PATCH 22/51] Support constructing a planar image from interleaved image (#552) Fixes #478 --- include/boost/gil/algorithm.hpp | 61 +++++++++++++++++++++++++++++---- test/core/image/image.cpp | 27 ++++++++++----- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/include/boost/gil/algorithm.hpp b/include/boost/gil/algorithm.hpp index b67d54a35c..cbd1716868 100644 --- a/include/boost/gil/algorithm.hpp +++ b/include/boost/gil/algorithm.hpp @@ -734,10 +734,33 @@ void default_construct_pixels(View const& view) namespace detail { +enum class copy_planarity_condition +{ + planar_to_planar, + interleaved_to_planar, + mixed_to_interleaved +}; + +using planar_to_planar_type = + std::integral_constant + < + copy_planarity_condition, copy_planarity_condition::planar_to_planar + >; +using interleaved_to_planar_type = + std::integral_constant + < + copy_planarity_condition, copy_planarity_condition::interleaved_to_planar + >; +using mixed_to_interleaved_type = + std::integral_constant + < + copy_planarity_condition, copy_planarity_condition::mixed_to_interleaved + >; + /// std::uninitialized_copy for pairs of planar iterators template BOOST_FORCEINLINE -void uninitialized_copy_aux(It1 first1, It1 last1, It2 first2, std::true_type) +void uninitialized_copy_aux(It1 first1, It1 last1, It2 first2, It2 last2, planar_to_planar_type) { std::size_t channel=0; try { @@ -761,13 +784,25 @@ void uninitialized_copy_aux(It1 first1, It1 last1, It2 first2, std::true_type) } } -/// std::uninitialized_copy for interleaved or mixed iterators +/// std::uninitialized_copy for interleaved or mixed(planar into interleaved) iterators template BOOST_FORCEINLINE -void uninitialized_copy_aux(It1 first1, It1 last1, It2 first2, std::false_type) +void uninitialized_copy_aux(It1 first1, It1 last1, It2 first2, It2 last2, mixed_to_interleaved_type) { std::uninitialized_copy(first1, last1, first2); } + +/// std::uninitialized_copy for interleaved to planar iterators +template +BOOST_FORCEINLINE +void uninitialized_copy_aux(It1 first1, It1 last1, It2 first2, It2 last2, +interleaved_to_planar_type) +{ + default_construct_aux(first2, last2, std::true_type()); + + typename It2::difference_type n = last2 - first2; + copier_n()(first1, n, first2); +} } // namespace detail /// \ingroup ImageViewSTLAlgorithmsUninitializedCopyPixels @@ -777,13 +812,24 @@ void uninitialized_copy_aux(It1 first1, It1 last1, It2 first2, std::false_type) template void uninitialized_copy_pixels(View1 const& view1, View2 const& view2) { - using is_planar = std::integral_constant::value && is_planar::value>; + using copy_planarity_condition = detail::copy_planarity_condition; + using copy_planarity_condition_type = + std::integral_constant + < + copy_planarity_condition, + !is_planar::value + ? copy_planarity_condition::mixed_to_interleaved + : (is_planar::value + ? copy_planarity_condition::planar_to_planar + : copy_planarity_condition::interleaved_to_planar) + >; BOOST_ASSERT(view1.dimensions() == view2.dimensions()); if (view1.is_1d_traversable() && view2.is_1d_traversable()) { detail::uninitialized_copy_aux( - view1.begin().x(), view1.end().x(), view2.begin().x(), is_planar()); + view1.begin().x(), view1.end().x(), view2.begin().x(), view2.end().x(), + copy_planarity_condition_type()); } else { @@ -792,12 +838,13 @@ void uninitialized_copy_pixels(View1 const& view1, View2 const& view2) { for (y = 0; y < view1.height(); ++y) detail::uninitialized_copy_aux( - view1.row_begin(y), view1.row_end(y), view2.row_begin(y), is_planar()); + view1.row_begin(y), view1.row_end(y), view2.row_begin(y), view2.row_end(y), + copy_planarity_condition_type()); } catch(...) { for (typename View1::y_coord_t y0 = 0; y0 < y; ++y0) - detail::destruct_aux(view2.row_begin(y0), view2.row_end(y0), is_planar()); + detail::destruct_aux(view2.row_begin(y0), view2.row_end(y0), is_planar()); throw; } } diff --git a/test/core/image/image.cpp b/test/core/image/image.cpp index 52d7bd7034..df61a3df92 100644 --- a/test/core/image/image.cpp +++ b/test/core/image/image.cpp @@ -56,15 +56,15 @@ struct test_constructor_from_other_image auto v2 = gil::const_view(image2); BOOST_TEST_ALL_EQ(v1.begin(), v1.end(), v2.begin(), v2.end()); } - // { - // //constructor planar from interleaved - // image_t image1(dimensions, rnd_pixel); - // gil::image image2(image1); - // BOOST_TEST_EQ(image2.dimensions(), dimensions); - // auto v1 = gil::const_view(image1); - // auto v2 = gil::const_view(image2); - // BOOST_TEST_ALL_EQ(v1.begin(), v1.end(), v2.begin(), v2.end()); - // } + { + // constructor planar from interleaved + image_t image1(dimensions, rnd_pixel); + gil::image image2(image1); + BOOST_TEST_EQ(image2.dimensions(), dimensions); + auto v1 = gil::const_view(image1); + auto v2 = gil::const_view(image2); + BOOST_TEST_ALL_EQ(v1.begin(), v1.end(), v2.begin(), v2.end()); + } } static void run() { @@ -90,6 +90,15 @@ struct test_constructor_from_view auto v2 = gil::const_view(image2); BOOST_TEST_ALL_EQ(v1.begin(), v1.end(), v2.begin(), v2.end()); } + { + //constructor planar from interleaved + image_t image1(dimensions, rnd_pixel); + auto v1 = gil::transposed_view(gil::const_view(image1)); + gil::image image2(gil::transposed_view(v1)); + BOOST_TEST_EQ(image2.dimensions(), dimensions); + auto v2 = gil::const_view(image2); + BOOST_TEST_ALL_EQ(v1.begin(), v1.end(), v2.begin(), v2.end()); + } } static void run() { From 90f7952b054de873b14196466c35a646b83b2575 Mon Sep 17 00:00:00 2001 From: Scramjet911 <36035352+Scramjet911@users.noreply.github.com> Date: Tue, 2 Feb 2021 23:23:16 +0530 Subject: [PATCH 23/51] Fixed most of warnings in examples and some in core library (#545) --- example/hessian.cpp | 4 ++-- example/mandelbrot.cpp | 2 +- include/boost/gil/channel_algorithm.hpp | 2 +- include/boost/gil/color_base.hpp | 2 +- include/boost/gil/color_convert.hpp | 8 ++++---- include/boost/gil/image_processing/harris.hpp | 2 +- include/boost/gil/image_processing/hessian.hpp | 4 ++-- include/boost/gil/image_processing/numeric.hpp | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/example/hessian.cpp b/example/hessian.cpp index e27199f5f4..cf05050bcc 100644 --- a/example/hessian.cpp +++ b/example/hessian.cpp @@ -60,8 +60,8 @@ gil::gray8_image_t to_grayscale(gil::rgb8_view_t original) void apply_gaussian_blur(gil::gray8_view_t input_view, gil::gray8_view_t output_view) { - constexpr static auto filter_height = 5ull; - constexpr static auto filter_width = 5ull; + constexpr static std::ptrdiff_t filter_height = 5ull; + constexpr static std::ptrdiff_t filter_width = 5ull; constexpr static double filter[filter_height][filter_width] = { { 2, 4, 6, 4, 2 }, diff --git a/example/mandelbrot.cpp b/example/mandelbrot.cpp index 0bcfc2ea14..1aa5a90897 100644 --- a/example/mandelbrot.cpp +++ b/example/mandelbrot.cpp @@ -40,7 +40,7 @@ struct mandelbrot_fn t=pow(t,0.2); value_type ret; - for (int k=0; k::value; ++k) + for (std::size_t k=0; k::value; ++k) ret[k]=(typename channel_type

::type)(_in_color[k]*t + _out_color[k]*(1-t)); return ret; } diff --git a/include/boost/gil/channel_algorithm.hpp b/include/boost/gil/channel_algorithm.hpp index 12ecd01b0a..848c9652ad 100644 --- a/include/boost/gil/channel_algorithm.hpp +++ b/include/boost/gil/channel_algorithm.hpp @@ -79,7 +79,7 @@ struct unsigned_integral_max_value> template struct unsigned_integral_num_bits - : std::integral_constant + : std::integral_constant(sizeof(UnsignedIntegralChannel) * 8)> {}; template diff --git a/include/boost/gil/color_base.hpp b/include/boost/gil/color_base.hpp index a05b409de8..b638aacbba 100644 --- a/include/boost/gil/color_base.hpp +++ b/include/boost/gil/color_base.hpp @@ -84,7 +84,7 @@ struct homogeneous_color_base typename U = Element, typename = typename std::enable_if::value>::type > - homogeneous_color_base() : v0_{} {}; + homogeneous_color_base() : v0_{} {} explicit homogeneous_color_base(Element v) : v0_(v) {} diff --git a/include/boost/gil/color_convert.hpp b/include/boost/gil/color_convert.hpp index 080d758ae9..a4b6427023 100644 --- a/include/boost/gil/color_convert.hpp +++ b/include/boost/gil/color_convert.hpp @@ -170,13 +170,13 @@ struct default_color_converter_impl // Apply color correction, strengthening, reducing non-zero components by // s = 1 / (1 - k) for k < 1, where 1 denotes dst_t max, otherwise s = 1 (literal). uint_t const dst_max = channel_traits::max_value(); - uint_t const s_div = dst_max - k; + uint_t const s_div = static_cast(dst_max - k); if (s_div != 0) { double const s = dst_max / static_cast(s_div); - c = (c - k) * s; - m = (m - k) * s; - y = (y - k) * s; + c = static_cast((c - k) * s); + m = static_cast((m - k) * s); + y = static_cast((y - k) * s); } else { diff --git a/include/boost/gil/image_processing/harris.hpp b/include/boost/gil/image_processing/harris.hpp index c5deedda99..18185afd81 100644 --- a/include/boost/gil/image_processing/harris.hpp +++ b/include/boost/gil/image_processing/harris.hpp @@ -44,7 +44,7 @@ void compute_harris_responses( " tensor from the same image"); } - auto const window_length = weights.size(); + std::ptrdiff_t const window_length = weights.size(); auto const width = m11.width(); auto const height = m11.height(); auto const half_length = window_length / 2; diff --git a/include/boost/gil/image_processing/hessian.hpp b/include/boost/gil/image_processing/hessian.hpp index 2c8c3adeb1..f88c153607 100644 --- a/include/boost/gil/image_processing/hessian.hpp +++ b/include/boost/gil/image_processing/hessian.hpp @@ -50,9 +50,9 @@ inline void compute_hessian_responses( auto ddxx_i = channel_t(); auto ddyy_i = channel_t(); auto dxdy_i = channel_t(); - for (typename OutputView::coord_t w_y = 0; w_y < weights.size(); ++w_y) + for (typename OutputView::coord_t w_y = 0; w_y < static_cast(weights.size()); ++w_y) { - for (typename OutputView::coord_t w_x = 0; w_x < weights.size(); ++w_x) + for (typename OutputView::coord_t w_x = 0; w_x < static_cast(weights.size()); ++w_x) { ddxx_i += ddxx(x + w_x - center, y + w_y - center) .at(std::integral_constant{}) * weights.at(w_x, w_y); diff --git a/include/boost/gil/image_processing/numeric.hpp b/include/boost/gil/image_processing/numeric.hpp index 2109c555f4..b601b17dd0 100644 --- a/include/boost/gil/image_processing/numeric.hpp +++ b/include/boost/gil/image_processing/numeric.hpp @@ -48,7 +48,7 @@ inline double lanczos(double x, std::ptrdiff_t a) if (0 <= x && x <= 0) return 1; - if (-a < x && x < a) + if (static_cast(-a) < x && x < static_cast(a)) return normalized_sinc(x) / normalized_sinc(x / static_cast(a)); return 0; From d923a8f37e9b2fa05b57f476076be451acb6ec9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Tue, 2 Feb 2021 19:58:29 +0100 Subject: [PATCH 24/51] Declare laplacian_function::get_directed_offsets as inline Fixes linker error due to multiple definitions. The issue was missed during review of #500 as some public headers are still missing from boost/gil.hpp. --- include/boost/gil/image_processing/diffusion.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/gil/image_processing/diffusion.hpp b/include/boost/gil/image_processing/diffusion.hpp index 72f55a2ae4..0c1a3dce75 100644 --- a/include/boost/gil/image_processing/diffusion.hpp +++ b/include/boost/gil/image_processing/diffusion.hpp @@ -106,7 +106,7 @@ namespace laplace_function { and so on in clockwise manner. Leave element as zero if it is not to be computed. */ -std::array get_directed_offsets() +inline std::array get_directed_offsets() { return {point_t{-1, -1}, point_t{0, -1}, point_t{+1, -1}, point_t{+1, 0}, point_t{+1, +1}, point_t{0, +1}, point_t{-1, +1}, point_t{-1, 0}}; From c7847f4d640e0a3ab4bea4dad53af48dd9ad57de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Tue, 2 Feb 2021 23:50:58 +0100 Subject: [PATCH 25/51] Fix warning: returning reference to local temporary object (#556) Fixes #475 --- .../extension/toolbox/image_types/subchroma_image.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/boost/gil/extension/toolbox/image_types/subchroma_image.hpp b/include/boost/gil/extension/toolbox/image_types/subchroma_image.hpp index f49d69e286..cd7de24b29 100644 --- a/include/boost/gil/extension/toolbox/image_types/subchroma_image.hpp +++ b/include/boost/gil/extension/toolbox/image_types/subchroma_image.hpp @@ -245,8 +245,8 @@ class subchroma_image_view : public image_view : image_view< locator >( v ) {} - const point_t& v_ssfactors() const { return point_t( get_deref_fn().vx_ssfactor(), get_deref_fn().vx_ssfactor() ); } - const point_t& u_ssfactors() const { return point_t( get_deref_fn().ux_ssfactor(), get_deref_fn().ux_ssfactor() ); } + point_t v_ssfactors() const { return point_t( get_deref_fn().vx_ssfactor(), get_deref_fn().vx_ssfactor() ); } + point_t u_ssfactors() const { return point_t( get_deref_fn().ux_ssfactor(), get_deref_fn().ux_ssfactor() ); } const point_t& y_dimension() const { return _y_dimensions; } const point_t& v_dimension() const { return _v_dimensions; } @@ -256,9 +256,9 @@ class subchroma_image_view : public image_view const plane_locator_t& v_plane() const { return get_deref_fn().v_locator(); } const plane_locator_t& u_plane() const { return get_deref_fn().u_locator(); } - const plane_view_t y_plane_view() const { return plane_view_t( _y_dimensions, y_plane() ); } - const plane_view_t v_plane_view() const { return plane_view_t( _v_dimensions, v_plane() ); } - const plane_view_t u_plane_view() const { return plane_view_t( _u_dimensions, u_plane() ); } + plane_view_t y_plane_view() const { return plane_view_t( _y_dimensions, y_plane() ); } + plane_view_t v_plane_view() const { return plane_view_t( _v_dimensions, v_plane() ); } + plane_view_t u_plane_view() const { return plane_view_t( _u_dimensions, u_plane() ); } private: From 04daae7160474e9d1dbd795e2e10f761d9cfec2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Wed, 3 Feb 2021 00:26:14 +0100 Subject: [PATCH 26/51] Fix warning: comparison of integer expressions of different signedness --- include/boost/gil/histogram.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/boost/gil/histogram.hpp b/include/boost/gil/histogram.hpp index bdbf9a6d7b..a74b590033 100644 --- a/include/boost/gil/histogram.hpp +++ b/include/boost/gil/histogram.hpp @@ -183,7 +183,7 @@ struct filler<1> template void operator()(Container& hist, Tuple& lower, Tuple& upper, std::size_t bin_width = 1) { - for (auto i = std::get<0>(lower); std::get<0>(upper) - i >= bin_width; i += bin_width) + for (auto i = std::get<0>(lower); static_cast(std::get<0>(upper) - i) >= bin_width; i += bin_width) { hist(i / bin_width) = 0; } From 58804ecb241063947a0ec34aed972a018f2f425a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Wed, 3 Feb 2021 00:47:17 +0100 Subject: [PATCH 27/51] Remove and other cruft from Jamfile (#557) The warnings, debug-symbols and other build properties can (and should) be controlled via b2 command line options. By the way, most of uses in Jamfile-s are usually incorrect. Instead, better option is to: use warnings=pedantic to control the warnings instead of cxxflags options directly either in requirements or in default-build, depending on whether you want to override it from the command line or not --- test/Jamfile | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/Jamfile b/test/Jamfile index 99fed7abd5..341ac443dc 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -22,18 +22,6 @@ project . # TODO: Enable concepts check for all, not just test/core #BOOST_GIL_USE_CONCEPT_CHECK=1 - msvc:_SCL_SECURE_NO_DEPRECATE - msvc:_CRT_SECURE_NO_WARNINGS - msvc:_CRT_NONSTDC_NO_DEPRECATE - msvc:NOMINMAX - intel:off - gcc:"-fstrict-aliasing" - darwin:"-fstrict-aliasing" - # variant filter for clang is necessary to allow ubsan_* - # custom variants declare distinct set of - clang,debug:"-fstrict-aliasing" - clang,release:"-fstrict-aliasing" - $(DEVELOPMENT_EXTRA_WARNINGS) [ requires cxx11_constexpr cxx11_defaulted_functions From f58f3a23a1c009535fb8f88c36d71dee59e7d13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Wed, 3 Feb 2021 00:52:57 +0100 Subject: [PATCH 28/51] Disable warning: overlapping comparisons always evaluate to false [-Wtautological-overlap-compare] --- test/legacy/channel.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/legacy/channel.cpp b/test/legacy/channel.cpp index b4225d20e9..f53d170e4a 100644 --- a/test/legacy/channel.cpp +++ b/test/legacy/channel.cpp @@ -17,6 +17,9 @@ #if defined(BOOST_CLANG) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wfloat-equal" +#if (__clang_major__ >= 10) +#pragma clang diagnostic ignored "-Wtautological-overlap-compare" +#endif #elif BOOST_GCC >= 40700 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wfloat-equal" From 8337f47aeb72e81141f3ea63834bdb1672965bb1 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Thu, 4 Feb 2021 22:31:32 +0530 Subject: [PATCH 29/51] Review and update includes in boost/gil.hpp (#559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #555 Co-authored-by: Mateusz Łoskot --- include/boost/gil.hpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/include/boost/gil.hpp b/include/boost/gil.hpp index 797b1e6006..a2e2831114 100644 --- a/include/boost/gil.hpp +++ b/include/boost/gil.hpp @@ -21,13 +21,10 @@ #include #include #include +#include #include #include #include -#include -#include -#include -#include #include #include #include @@ -42,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -50,5 +48,17 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #endif From c3b3d35aaabee47df448e95cdeedc8b08f272c87 Mon Sep 17 00:00:00 2001 From: meshtag <63031630+meshtag@users.noreply.github.com> Date: Fri, 5 Feb 2021 16:57:11 +0530 Subject: [PATCH 30/51] Fix sphinx installation link in docs readme (#560) --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 48ac0b32a1..de7700be63 100644 --- a/doc/README.md +++ b/doc/README.md @@ -5,7 +5,7 @@ A simple guide about writing and building documentation for Boost.GIL. ## Prerequisites - Python 3 -- [Install Sphinx](#install-sphinx) (see `requirements.txt`) +- Install [Sphinx](https://www.sphinx-doc.org/en/master/index.html) (see `requirements.txt`) - Install [Doxygen](http://www.doxygen.org) From 2676d31801c1232c2824c93e617f5c49a638e594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Tue, 9 Feb 2021 23:49:02 +0100 Subject: [PATCH 31/51] ci: Disable AppVeyor job using CMake to avoid timeouts --- .appveyor.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 2608d1e6eb..4ad917bd3f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -29,17 +29,6 @@ environment: CXXSTD: 11 TEST_HEADERS: 1 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - - TOOLSET: msvc-14.1 - ARCH: x86_64 - VARIANT: debug - CXXSTD: 17 - TEST_HEADERS: 1 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - - TOOLSET: msvc-14.1 - ARCH: x86_64 - VARIANT: release - CXXSTD: 17 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - TOOLSET: msvc-14.1 ARCH: x86_64 VARIANT: debug @@ -64,10 +53,21 @@ environment: - TOOLSET: msvc-14.1 ARCH: x86_64 VARIANT: debug - CXXSTD: 14 - GENERATOR: "Visual Studio 15 2017 Win64" - CMAKE_CONFIG: Debug + CXXSTD: 17 + TEST_HEADERS: 1 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + - TOOLSET: msvc-14.1 + ARCH: x86_64 + VARIANT: release + CXXSTD: 17 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + # - TOOLSET: msvc-14.1 + # ARCH: x86_64 + # VARIANT: debug + # CXXSTD: 14 + # GENERATOR: "Visual Studio 15 2017 Win64" + # CMAKE_CONFIG: Debug + # APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 matrix: fast_finish: true From 1e8526797e3327fd1bed7c2ace092fad5fd5ee74 Mon Sep 17 00:00:00 2001 From: meshtag <63031630+meshtag@users.noreply.github.com> Date: Sat, 13 Feb 2021 21:31:22 +0530 Subject: [PATCH 32/51] Added all standard morphological transformations (#541) * Added all standard morphological transformations * Improved comments and some other things * Applied adviced changes * Applied adviced changes * Should handle grayscale dilation/erosion * Checking * Added test cases and improved code structure * Added command line control * Added command line control * Rectified some things * Rectified some more things * Improved comments * Improved comments * Improved doxygen comments and added more test cases * Improved compatibility for builds and rectifying whitespace use * Minor improvement in comments * Did clang formatting * pushed enum class inside namespace 'detail' and some other things * Should handle multichannel images * Clang formatting attempt * got rid of if/else comparators for target_element * Adds morphology.hpp declaration in boost/gil.hpp * Fix newline * (std::max)(a, b) instead of std::max(a, b) * Improved Formatting --- example/Jamfile | 1 + example/morphology.cpp | 139 ++++++++ example/morphology_original.png | Bin 0 -> 2082 bytes include/boost/gil.hpp | 1 + .../boost/gil/image_processing/morphology.hpp | 300 ++++++++++++++++++ test/core/image_processing/CMakeLists.txt | 3 +- test/core/image_processing/Jamfile | 1 + test/core/image_processing/morphology.cpp | 136 ++++++++ 8 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 example/morphology.cpp create mode 100644 example/morphology_original.png create mode 100644 include/boost/gil/image_processing/morphology.hpp create mode 100644 test/core/image_processing/morphology.cpp diff --git a/example/Jamfile b/example/Jamfile index 48b9e51aef..df17d57c42 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -30,6 +30,7 @@ local sources = histogram.cpp interleaved_ptr.cpp mandelbrot.cpp + morphology.cpp packed_pixel.cpp resize.cpp sobel_scharr.cpp diff --git a/example/morphology.cpp b/example/morphology.cpp new file mode 100644 index 0000000000..e54bbfef6a --- /dev/null +++ b/example/morphology.cpp @@ -0,0 +1,139 @@ +// +// Copyright 2021 Prathamesh Tagore +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include +#include +#include +#include +#include + +// Default structuring element is SE = [1,1,1] +// |1,1,1| +// [1,1,1] +// SE(1,1)(center pixel) is the one which coincides with the currently +// considered pixel of the image to be convolved. The structuring element can be +// easily changed by the user. +namespace gil = boost::gil; +int main(int argc, char** argv) +{ + std::map operations; + if (argc < 4 || argc > 11) + { + throw std::invalid_argument( + "Wrong format of command line arguments.\n" + "Correct format is " + " " + "" + " \n"); + // User has to enter atleast one operation and they can enter maximum 8 + // operations considering binary conversion to be an + // operation.Output_image_template argument is the common component which + // will be added in names of all output images followed by a hyphen and + // the operation name. + // Example : + // ./example_morphology morphology_original.png out black_hat top_hat + // morphological_gradient dilation erosion opening closing binary + // Order of arguments entered will not matter with the exception of binary + // operation used for binary morphological operations.If binary is entered + // through the command line, it will always be the first operation to be + // applied. + return -1; + } + else + { + for (int i = 3; i < argc; ++i) + operations[argv[i]] = true; + } + gil::gray8_image_t img; + gil::read_image(argv[1], img, gil::png_tag{}); + + // Image can be converted to a binary format with high value as 255 and low + // value as 0 by using the threshold operator . This can be used for binary + // morphological operations . Convenient threshold for binary conversion may + // be chosen by the user. + if (operations["binary"]) + { + threshold_binary(view(img), view(img), 170, 255); + std::string name = argv[2]; + name += "-binary.png"; + gil::write_view(name, view(img), gil::png_tag{}); + } + + std::vector ker_vec(9, 1.0f); // Structuring element + gil::detail::kernel_2d ker_mat(ker_vec.begin(), ker_vec.size(), 1, 1); + gil::gray8_image_t img_out_dilation(img.dimensions()), img_out_erosion(img.dimensions()), + img_out_opening(img.dimensions()); + gil::gray8_image_t img_out_closing(img.dimensions()), img_out_mg(img.dimensions()), + img_out_top_hat(img.dimensions()); + gil::gray8_image_t img_out_black_hat(img.dimensions()); + + // Do not pass empty input image views in functions defined below for + // morphological operations to avoid errors. + if (operations["dilation"]) + { + // dilate(input_image_view,output_image_view,structuring_element,iterations) + dilate(view(img), view(img_out_dilation), ker_mat, 1); + std::string name = argv[2]; + name += "-dilation.png"; + gil::write_view(name, view(img_out_dilation), gil::png_tag{}); + } + + if (operations["erosion"]) + { + // erode(input_image_view,output_image_view,structuring_element,iterations) + erode(view(img), view(img_out_erosion), ker_mat, 1); + std::string name = argv[2]; + name += "-erosion.png"; + gil::write_view(name, view(img_out_erosion), gil::png_tag{}); + } + + if (operations["opening"]) + { + // opening(input_image_view,output_image_view,structuring_element) + opening(view(img), view(img_out_opening), ker_mat); + std::string name = argv[2]; + name += "-opening.png"; + gil::write_view(name, view(img_out_opening), gil::png_tag{}); + } + + if (operations["closing"]) + { + // closing(input_image_view,output_image_view,structuring_element) + closing(view(img), view(img_out_closing), ker_mat); + std::string name = argv[2]; + name += "-closing.png"; + gil::write_view(name, view(img_out_closing), gil::png_tag{}); + } + + if (operations["morphological_gradient"]) + { + // morphological_gradient(input_image_view,output_image_view,structuring_element) + morphological_gradient(view(img), view(img_out_mg), ker_mat); + std::string name = argv[2]; + name += "-morphological_gradient.png"; + gil::write_view(name, view(img_out_mg), gil::png_tag{}); + } + + if (operations["top_hat"]) + { + // top_hat(input_image_view,output_image_view,structuring_element) + top_hat(view(img), view(img_out_top_hat), ker_mat); + std::string name = argv[2]; + name += "-top_hat.png"; + gil::write_view(name, view(img_out_top_hat), gil::png_tag{}); + } + + if (operations["black_hat"]) + { + // black_hat(input_image_view,output_image_view,structuring_element) + black_hat(view(img), view(img_out_black_hat), ker_mat); + std::string name = argv[2]; + name += "-black_hat.png"; + gil::write_view(name, view(img_out_black_hat), gil::png_tag{}); + } +} diff --git a/example/morphology_original.png b/example/morphology_original.png new file mode 100644 index 0000000000000000000000000000000000000000..b63e99ed9da7b769fd4fe45826f1f5118fb24297 GIT binary patch literal 2082 zcmZ8iZB!Fi8U~6e6lgL-Es3HqNe0s5Za%OUTm@l-02eGGm}5C53=p(+p&(Y24~YWK zK(s~zJ5gHBi8NB>s42CDEv14Z@)2P@Srsd~k^)M*T0KZn5ZybR-Tv4gbMJkhdGEdN zecmtn{lpkA521&Xlap7hir5V2d+@7Y0tY9@y4#~pPKzgFi49wdobPnWkNh0%QaSqS zjxOi@^TQ>-X)lhwvGd>?_lTT(es>3(tFzub(NvAy zIzL_N=%N<~u4pLwB>>1_qlr^ZWhdM<)3v6ru8s!PTca3@j-!I7^6GIpP-Cc@kD( zks#cOtGc|>&2*QNl z-@N~ND`WIcdN{QDW0^FG2q~KzompwR+{E4haX$RR>1DIiIl0YWB(C z_K7Nv)~o{Z13a*r3WW%+5Lo54{A?l}lG1*4jQtiQ8u_K%O}9bf&VeyPaBVBRWQ}Qs zRcaUYQyZ?^ztSFH-i`lH+1sWat0O6k$XzgUB>fM9Ywvt#c=&Nxe;Q zW`6;35cEMMAkh`){i+D%Gf-Lt5(%7#o<5a)H5d=XP>OUcwFe3z| z#s^FUXoj)3*|Mn+_*c{U+VTgCs-?@5=ZDgyW1TXt3HUFSpCtNS%?;aAHk~@2XY(~E zb?Rr~%I3JEtJ`P@dCAD=2_RC*sNSX!^z0zGmYLX2K*BgpbTszJ5|9vZQ2JO3I_5mA9KC5A zgw#;RNyoMLQ;NB zuIpv0R2`-J=4=?w6WDnJ^bvQWhrlk(&5n0-5K6yE)k7$R{kyFWDywzyWnvKDGT;jB zA{Uvnq2IQV|9mw4yu10A$t}u@1SflOIQ&177yja<=p+}nuK}^pB1sJhvjm(4G2q$w zsr3EYEf>OXddj@=v%^R~EF+qha?~~oB&pbDZN+efAm9dEFvC>>JXNI7Qp42(?1rdd zVx84*>I=1@ZT9A?m-CCaS~l(MdrGgS&xF5vazET1ruZ=TN?jkdn?AFDekOeXKVV0F u^w54g2gVt>+wmdSAYM%We=~M;&9AKA^Y#TxX!BmK`NwWdB+f=^4*UmVeU2^w literal 0 HcmV?d00001 diff --git a/include/boost/gil.hpp b/include/boost/gil.hpp index a2e2831114..b98af56e6a 100644 --- a/include/boost/gil.hpp +++ b/include/boost/gil.hpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/gil/image_processing/morphology.hpp b/include/boost/gil/image_processing/morphology.hpp new file mode 100644 index 0000000000..1149b2c165 --- /dev/null +++ b/include/boost/gil/image_processing/morphology.hpp @@ -0,0 +1,300 @@ +// +// Copyright 2021 Prathamesh Tagore +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef BOOST_GIL_IMAGE_PROCESSING_MORPHOLOGY_HPP +#define BOOST_GIL_IMAGE_PROCESSING_MORPHOLOGY_HPP +#include +#include +#include + +namespace boost +{ +namespace gil +{ +namespace detail +{ +enum class morphological_operation +{ + dilation, + erosion, +}; +/// \addtogroup ImageProcessing +/// @{ + +/// \brief Implements morphological operations at pixel level.This function +/// compares neighbouring pixel values according to the kernel and choose +/// minimum/mamximum neighbouring pixel value and assigns it to the pixel under +/// consideration. +/// \param src_view - Source/Input image view. +/// \param dst_view - View which stores the final result of operations performed by this function. +/// \param kernel - Kernel matrix/structuring element containing 0's and 1's +/// which will be used for applying the required morphological operation. +/// \param identifier - Indicates the type of morphological operation to be applied. +/// \tparam SrcView type of source image. +/// \tparam DstView type of output image. +/// \tparam Kernel type of structuring element. +template +void morph_impl(SrcView const& src_view, DstView const& dst_view, Kernel const& kernel, + morphological_operation identifier) +{ + std::ptrdiff_t flip_ker_row, flip_ker_col, row_boundary, col_boundary; + typename channel_type::type target_element; + for (std::ptrdiff_t view_row = 0; view_row < src_view.height(); ++view_row) + { + for (std::ptrdiff_t view_col = 0; view_col < src_view.width(); ++view_col) + { + target_element = src_view(view_col, view_row); + for (std::size_t kernel_row = 0; kernel_row < kernel.size(); ++kernel_row) + { + flip_ker_row = kernel.size() - 1 - kernel_row; // row index of flipped kernel + + for (std::size_t kernel_col = 0; kernel_col < kernel.size(); ++kernel_col) + { + flip_ker_col = kernel.size() - 1 - kernel_col; // column index of flipped kernel + + // We ensure that we consider only those pixels which are overlapped + // on a non-zero kernel_element as + if (kernel.at(flip_ker_row, flip_ker_col) == 0) + { + continue; + } + // index of input signal, used for checking boundary + row_boundary = view_row + (kernel.center_y() - flip_ker_row); + col_boundary = view_col + (kernel.center_x() - flip_ker_col); + + // ignore input samples which are out of bound + if (row_boundary >= 0 && row_boundary < src_view.height() && + col_boundary >= 0 && col_boundary < src_view.width()) + { + + if (identifier == morphological_operation::dilation) + { + target_element = + (std::max)(src_view(col_boundary, row_boundary)[0], target_element); + } + else if (identifier == morphological_operation::erosion) + { + target_element = + (std::min)(src_view(col_boundary, row_boundary)[0], target_element); + } + } + } + } + dst_view(view_col, view_row) = target_element; + } + } +} + +/// \brief Checks feasibility of the desired operation and passes parameter +/// values to the function morph_impl alongwith individual channel views of the +/// input image. +/// \param src_view - Source/Input image view. +/// \param dst_view - View which stores the final result of operations performed by this function. +/// \param kernel - Kernel matrix/structuring element containing 0's and 1's +/// which will be used for applying the required morphological operation. +/// \param identifier - Indicates the type of morphological operation to be applied. +/// \tparam SrcView type of source image. +/// \tparam DstView type of output image. +/// \tparam Kernel type of structuring element. +template +void morph(SrcView const& src_view, DstView const& dst_view, Kernel const& ker_mat, + morphological_operation identifier) +{ + BOOST_ASSERT(ker_mat.size() != 0 && src_view.dimensions() == dst_view.dimensions()); + gil_function_requires>(); + gil_function_requires>(); + + gil_function_requires::type, + typename color_space_type::type>>(); + + gil::image intermediate_img(src_view.dimensions()); + + for (std::size_t i = 0; i < src_view.num_channels(); i++) + { + morph_impl(nth_channel_view(src_view, i), nth_channel_view(view(intermediate_img), i), + ker_mat, identifier); + } + copy_pixels(view(intermediate_img), dst_view); +} + +/// \brief Calculates the difference between pixel values of first image_view +/// and second image_view. +/// \param src_view1 - First parameter for subtraction of views. +/// \param src_view2 - Second parameter for subtraction of views. +/// \param diff_view - View containing result of the subtraction of second view from +/// the first view. +/// \tparam SrcView type of source/Input images used for subtraction. +/// \tparam DiffView type of image view containing the result of subtraction. +template +void difference_impl(SrcView const& src_view1, SrcView const& src_view2, DiffView const& diff_view) +{ + for (std::ptrdiff_t view_row = 0; view_row < src_view1.height(); ++view_row) + for (std::ptrdiff_t view_col = 0; view_col < src_view1.width(); ++view_col) + diff_view(view_col, view_row) = + src_view1(view_col, view_row) - src_view2(view_col, view_row); +} + +/// \brief Passes parameter values to the function 'difference_impl' alongwith +/// individual channel views of input images. +/// \param src_view1 - First parameter for subtraction of views. +/// \param src_view2 - Second parameter for subtraction of views. +/// \param diff_view - View containing result of the subtraction of second view from the first view. +/// \tparam SrcView type of source/Input images used for subtraction. +/// \tparam DiffView type of image view containing the result of subtraction. +template +void difference(SrcView const& src_view1, SrcView const& src_view2, DiffView const& diff_view) +{ + gil_function_requires>(); + gil_function_requires>(); + + gil_function_requires::type, typename color_space_type::type>>(); + + for (std::size_t i = 0; i < src_view1.num_channels(); i++) + { + difference_impl(nth_channel_view(src_view1, i), nth_channel_view(src_view2, i), + nth_channel_view(diff_view, i)); + } +} +} // namespace detail + +/// \brief Applies morphological dilation on the input image view using given +/// structuring element. It gives the maximum overlapped value to the pixel +/// overlapping with the center element of structuring element. \param src_view +/// - Source/input image view. +/// \param int_op_view - view for writing output and performing intermediate operations. +/// \param ker_mat - Kernel matrix/structuring element containing 0's and 1's which will be used for +/// applying dilation. +/// \param iterations - Specifies the number of times dilation is to be applied on the input image +/// view. +/// \tparam SrcView type of source image, models gil::ImageViewConcept. +/// \tparam IntOpView type of output image, models gil::MutableImageViewConcept. +/// \tparam Kernel type of structuring element. +template +void dilate(SrcView const& src_view, IntOpView const& int_op_view, Kernel const& ker_mat, + int iterations) +{ + copy_pixels(src_view, int_op_view); + for (int i = 0; i < iterations; ++i) + morph(int_op_view, int_op_view, ker_mat, detail::morphological_operation::dilation); +} + +/// \brief Applies morphological erosion on the input image view using given +/// structuring element. It gives the minimum overlapped value to the pixel +/// overlapping with the center element of structuring element. +/// \param src_view - Source/input image view. +/// \param int_op_view - view for writing output and performing intermediate operations. +/// \param ker_mat - Kernel matrix/structuring element containing 0's and 1's which will be used for +/// applying erosion. +/// \param iterations - Specifies the number of times erosion is to be applied on the input +/// image view. +/// \tparam SrcView type of source image, models gil::ImageViewConcept. +/// \tparam IntOpView type of output image, models gil::MutableImageViewConcept. +/// \tparam Kernel type of structuring element. +template +void erode(SrcView const& src_view, IntOpView const& int_op_view, Kernel const& ker_mat, + int iterations) +{ + copy_pixels(src_view, int_op_view); + for (int i = 0; i < iterations; ++i) + morph(int_op_view, int_op_view, ker_mat, detail::morphological_operation::erosion); +} + +/// \brief Performs erosion and then dilation on the input image view . This +/// operation is utilized for removing noise from images. +/// \param src_view - Source/input image view. +/// \param int_op_view - view for writing output and performing intermediate operations. +/// \param ker_mat - Kernel matrix/structuring element containing 0's and 1's which will be used for +/// applying the opening operation. +/// \tparam SrcView type of source image, models gil::ImageViewConcept. +/// \tparam IntOpView type of output image, models gil::MutableImageViewConcept. +/// \tparam Kernel type of structuring element. +template +void opening(SrcView const& src_view, IntOpView const& int_op_view, Kernel const& ker_mat) +{ + erode(src_view, int_op_view, ker_mat, 1); + dilate(int_op_view, int_op_view, ker_mat, 1); +} + +/// \brief Performs dilation and then erosion on the input image view which is +/// exactly opposite to the opening operation . Closing operation can be +/// utilized for closing small holes inside foreground objects. +/// \param src_view - Source/input image view. +/// \param int_op_view - view for writing output and performing intermediate operations. +/// \param ker_mat - Kernel matrix/structuring element containing 0's and 1's which will be used for +/// applying the closing operation. +/// \tparam SrcView type of source image, models gil::ImageViewConcept. +/// \tparam IntOpView type of output image, models gil::MutableImageViewConcept. +/// \tparam Kernel type of structuring element. +template +void closing(SrcView const& src_view, IntOpView const& int_op_view, Kernel const& ker_mat) +{ + dilate(src_view, int_op_view, ker_mat, 1); + erode(int_op_view, int_op_view, ker_mat, 1); +} + +/// \brief Calculates the difference between image views generated after +/// applying dilation dilation and erosion on an image . The resultant image +/// will look like the outline of the object(s) present in the image. +/// \param src_view - Source/input image view. +/// \param dst_view - Destination view which will store the final result of morphological +/// gradient operation. +/// \param ker_mat - Kernel matrix/structuring element containing 0's and 1's which +/// will be used for applying the morphological gradient operation. +/// \tparam SrcView type of source image, models gil::ImageViewConcept. +/// \tparam DstView type of output image, models gil::MutableImageViewConcept. +/// \tparam Kernel type of structuring element. +template +void morphological_gradient(SrcView const& src_view, DstView const& dst_view, Kernel const& ker_mat) +{ + using namespace boost::gil; + gil::image int_dilate(src_view.dimensions()), + int_erode(src_view.dimensions()); + dilate(src_view, view(int_dilate), ker_mat, 1); + erode(src_view, view(int_erode), ker_mat, 1); + difference(view(int_dilate), view(int_erode), dst_view); +} + +/// \brief Calculates the difference between input image view and the view +/// generated by opening operation on the input image view. +/// \param src_view - Source/input image view. +/// \param dst_view - Destination view which will store the final result of top hat operation. +/// \param ker_mat - Kernel matrix/structuring element containing 0's and 1's which will be used for +/// applying the top hat operation. +/// \tparam SrcView type of source image, models gil::ImageViewConcept. +/// \tparam DstView type of output image, models gil::MutableImageViewConcept. +/// \tparam Kernel type of structuring element. +template +void top_hat(SrcView const& src_view, DstView const& dst_view, Kernel const& ker_mat) +{ + using namespace boost::gil; + gil::image int_opening(src_view.dimensions()); + opening(src_view, view(int_opening), ker_mat); + difference(src_view, view(int_opening), dst_view); +} + +/// \brief Calculates the difference between closing of the input image and +/// input image. +/// \param src_view - Source/input image view. +/// \param dst_view - Destination view which will store the final result of black hat operation. +/// \param ker_mat - Kernel matrix/structuring element containing 0's and 1's +/// which will be used for applying the black hat operation. +/// \tparam SrcView type of source image, models gil::ImageViewConcept. +/// \tparam DstView type of output image, models gil::MutableImageViewConcept. +/// \tparam Kernel type of structuring element. +template +void black_hat(SrcView const& src_view, DstView const& dst_view, Kernel const& ker_mat) +{ + using namespace boost::gil; + gil::image int_closing(src_view.dimensions()); + closing(src_view, view(int_closing), ker_mat); + difference(view(int_closing), src_view, dst_view); +} +/// @} +}} // namespace boost::gil +#endif // BOOST_GIL_IMAGE_PROCESSING_MORPHOLOGY_HPP diff --git a/test/core/image_processing/CMakeLists.txt b/test/core/image_processing/CMakeLists.txt index 2fc51c1d4a..1581fd3075 100644 --- a/test/core/image_processing/CMakeLists.txt +++ b/test/core/image_processing/CMakeLists.txt @@ -9,7 +9,8 @@ foreach(_name threshold_binary threshold_truncate - threshold_otsu) + threshold_otsu + morphology) set(_test t_core_image_processing_${_name}) set(_target test_core_image_processing_${_name}) diff --git a/test/core/image_processing/Jamfile b/test/core/image_processing/Jamfile index a67c4d9f21..acd74d08f2 100644 --- a/test/core/image_processing/Jamfile +++ b/test/core/image_processing/Jamfile @@ -22,3 +22,4 @@ run median_filter.cpp ; run anisotropic_diffusion.cpp ; run hough_line_transform.cpp ; run hough_circle_transform.cpp ; +run morphology.cpp ; diff --git a/test/core/image_processing/morphology.cpp b/test/core/image_processing/morphology.cpp new file mode 100644 index 0000000000..b57f605723 --- /dev/null +++ b/test/core/image_processing/morphology.cpp @@ -0,0 +1,136 @@ +// +// Copyright 2021 Prathamesh Tagore +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// + +#include +#include +#include + +namespace gil = boost::gil; + +// This function helps us fill pixels of a view given as 2nd argument with +// elements of the vector given as 1st argument. +void pixel_fill(std::vector>& original_binary_vector, + boost::gil::gray8_image_t& original_img) +{ + for (std::ptrdiff_t view_row = 0; view_row < view(original_img).height(); ++view_row) + { + for (std::ptrdiff_t view_col = 0; view_col < view(original_img).width(); ++view_col) + { + view(original_img)(view_col, view_row) = + gil::gray8_pixel_t(original_binary_vector[view_row][view_col]); + } + } +} + +int main() +{ + std::vector> original_binary_vector{ + {0, 0, 0, 0, 0, 0}, {0, 0, 127, 144, 143, 0}, {0, 0, 128, 0, 142, 0}, + {0, 0, 129, 0, 141, 0}, {0, 0, 130, 140, 139, 0}, {0, 0, 131, 0, 0, 0}, + {0, 0, 132, 137, 136, 138}, {0, 0, 133, 134, 135, 0}}; + std::vector> orig_dil_imp{ + {255, 100, 100, 100}, {100, 100, 100, 100}, {100, 100, 100, 100}}; + // All vectors defined below will be used for creating expected image views + // which are supposed to match the views obtained after applying morphological + // operations. + std::vector> exp_dil{ + {0, 127, 144, 144, 144, 143}, {0, 128, 144, 144, 144, 143}, {0, 129, 144, 144, 144, 143}, + {0, 130, 140, 142, 142, 142}, {0, 131, 140, 141, 141, 141}, {0, 132, 140, 140, 140, 139}, + {0, 133, 137, 137, 138, 138}, {0, 133, 137, 137, 138, 138}}; + // Following vector intends to check result of dilation operation when it is + // applied 2 times on the original image. + std::vector> exp_dil_iter2{ + {128, 144, 144, 144, 144, 144}, {129, 144, 144, 144, 144, 144}, + {130, 144, 144, 144, 144, 144}, {131, 144, 144, 144, 144, 144}, + {132, 140, 142, 142, 142, 142}, {133, 140, 141, 141, 141, 141}, + {133, 140, 140, 140, 140, 140}, {133, 137, 137, 138, 138, 138}}; + std::vector> exp_er{ + {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 132, 0, 0}}; + // Following vector intends to check result of erosion operation when it is + // applied 2 times on the original image. + std::vector> exp_er_iter2{ + {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}}; + std::vector> exp_opening{ + {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 132, 132, 132, 0}, {0, 0, 132, 132, 132, 0}}; + std::vector> exp_closing{ + {0, 0, 127, 144, 143, 143}, {0, 0, 127, 144, 143, 143}, {0, 0, 128, 140, 142, 142}, + {0, 0, 129, 140, 141, 141}, {0, 0, 130, 140, 139, 139}, {0, 0, 131, 137, 137, 138}, + {0, 0, 132, 137, 137, 138}, {0, 0, 133, 137, 137, 138}}; + std::vector> exp_mg{{0, 127, 144, 144, 144, 143}, {0, 128, 144, 144, 144, 143}, + {0, 129, 144, 144, 144, 143}, {0, 130, 140, 142, 142, 142}, + {0, 131, 140, 141, 141, 141}, {0, 132, 140, 140, 140, 139}, + {0, 133, 137, 137, 138, 138}, {0, 133, 137, 5, 138, 138}}; + std::vector> exp_top_hat{{0, 0, 0, 0, 0, 0}, {0, 0, 127, 144, 143, 0}, + {0, 0, 128, 0, 142, 0}, {0, 0, 129, 0, 141, 0}, + {0, 0, 130, 140, 139, 0}, {0, 0, 131, 0, 0, 0}, + {0, 0, 0, 5, 4, 138}, {0, 0, 1, 2, 3, 0}}; + std::vector> exp_black_hat{ + {0, 0, 127, 144, 143, 143}, {0, 0, 0, 0, 0, 143}, {0, 0, 0, 140, 0, 142}, + {0, 0, 0, 140, 0, 141}, {0, 0, 0, 0, 0, 139}, {0, 0, 0, 137, 137, 138}, + {0, 0, 0, 0, 1, 0}, {0, 0, 0, 3, 2, 138}}; + std::vector> exp_dil_imp{ + {255, 255, 100, 100}, {255, 255, 100, 100}, {100, 100, 100, 100}}; + + gil::gray8_image_t original_img(6, 8), obtained_dilation(6, 8), expected_dilation(6, 8); + gil::gray8_image_t obtained_erosion(6, 8), expected_erosion(6, 8); + gil::gray8_image_t obtained_opening(6, 8), expected_opening(6, 8); + gil::gray8_image_t obtained_closing(6, 8), expected_closing(6, 8); + gil::gray8_image_t obtained_mg(6, 8), expected_mg(6, 8); + gil::gray8_image_t obtained_top_hat(6, 8), expected_top_hat(6, 8); + gil::gray8_image_t obtained_black_hat(6, 8), expected_black_hat(6, 8); + gil::gray8_image_t obtained_dil_iter2(6, 8), expected_dil_iter2(6, 8); + gil::gray8_image_t obtained_er_iter2(6, 8), expected_er_iter2(6, 8); + gil::gray8_image_t obtained_imp_dil(4, 3), expected_imp_dil(4, 3), original_imp_dil(4, 3); + + std::vector ker_vec(9, 1.0f); // Structuring element + gil::detail::kernel_2d ker_mat(ker_vec.begin(), ker_vec.size(), 1, 1); + + pixel_fill(original_binary_vector, original_img); + pixel_fill(exp_dil, expected_dilation); + pixel_fill(exp_er, expected_erosion); + pixel_fill(exp_opening, expected_opening); + pixel_fill(exp_closing, expected_closing); + pixel_fill(exp_mg, expected_mg); + pixel_fill(exp_top_hat, expected_top_hat); + pixel_fill(exp_black_hat, expected_black_hat); + pixel_fill(exp_dil_iter2, expected_dil_iter2); + pixel_fill(orig_dil_imp, original_imp_dil); + pixel_fill(exp_dil_imp, expected_imp_dil); + pixel_fill(exp_er_iter2, expected_er_iter2); + + // Different morphological operations are applied on the same initial image to + // obtain results of our implementation which are then compared with expected + // results. + gil::dilate(view(original_img), view(obtained_dilation), ker_mat, 1); + gil::erode(view(original_img), view(obtained_erosion), ker_mat, 1); + gil::opening(view(original_img), view(obtained_opening), ker_mat); + gil::closing(view(original_img), view(obtained_closing), ker_mat); + gil::morphological_gradient(view(original_img), view(obtained_mg), ker_mat); + gil::top_hat(view(original_img), view(obtained_top_hat), ker_mat); + gil::black_hat(view(original_img), view(obtained_black_hat), ker_mat); + gil::dilate(view(original_imp_dil), view(obtained_imp_dil), ker_mat, 1); + gil::dilate(view(original_img), view(obtained_dil_iter2), ker_mat, 2); + gil::erode(view(original_img), view(obtained_er_iter2), ker_mat, 2); + + // Testing obtained results with expected results. + BOOST_TEST(gil::equal_pixels(gil::view(obtained_dilation), gil::view(expected_dilation))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_erosion), gil::view(expected_erosion))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_opening), gil::view(expected_opening))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_closing), gil::view(expected_closing))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_mg), gil::view(expected_mg))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_top_hat), gil::view(expected_top_hat))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_black_hat), gil::view(expected_black_hat))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_imp_dil), gil::view(expected_imp_dil))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_dil_iter2), gil::view(expected_dil_iter2))); + BOOST_TEST(gil::equal_pixels(gil::view(obtained_er_iter2), gil::view(expected_er_iter2))); + + return boost::report_errors(); +} From a8cb364492667b9a6441a6538b458aace64513c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Fri, 19 Feb 2021 22:13:43 +0100 Subject: [PATCH 33/51] doc: Add Compiling section with C++ version and compilers support Clarification suggested in https://github.com/boostorg/website/pull/562 following discussion about https://github.com/boostorg/gil/pull/526 --- doc/installation.rst | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index 175d1d4007..d810645159 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -1,9 +1,9 @@ Installation ============ -The latest version of GIL can be downloaded from https://github.com/boostorg/gil. +The latest version of Boost.GIL can be downloaded from https://github.com/boostorg/gil. -The GIL is a header-only library. Meaning, it consists of header files only, +The Boost.GIL is a header-only library. Meaning, it consists of header files only, it does not require Boost to be built and it does not require any libraries to link against. @@ -13,5 +13,22 @@ to link against. which requires client libraries implementing popular image formats like libpng, libjpeg, etc. -In order to use GIL, including ``boost/gil.hpp`` and telling your compiler +In order to use Boost.GIL, including ``boost/gil.hpp`` and telling your compiler where to find Boost and GIL headers should be sufficient for most projects. + +Compiling +--------- + +The Boost.GIL library source code should successfully compile with any +compiler with complete C++11 support. + +.. note:: + + We are planning to drop support for require C++14 support in Boost 1.76 or later, + or selectively drop support for GCC 5 due to its issues with inheriting constructors, + see `discussion for PR #526 `_. + +For the actual list of currently tested compilers, check results of the library CI +builds linked from the `README.md `_ +or inspect the CI services configuration files in the `develop branch `_ +of the library repository. From a82af6d25de304bd7482fdfece3b6f775f2832b9 Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Sun, 28 Feb 2021 14:24:56 +0530 Subject: [PATCH 34/51] ci: Add Codecov to GitHub Actions (#564) Delete coverage.sh as unused Closes #565 --- .ci/coverage.sh | 7 ----- .github/workflows/coverage.yml | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) delete mode 100644 .ci/coverage.sh create mode 100644 .github/workflows/coverage.yml diff --git a/.ci/coverage.sh b/.ci/coverage.sh deleted file mode 100644 index 42ff3e703f..0000000000 --- a/.ci/coverage.sh +++ /dev/null @@ -1,7 +0,0 @@ -#! /bin/bash -set -e -lcov --directory bin.v2 --capture --no-external --directory $(pwd) --output-file coverage.info > /dev/null 2>&1 -lcov --extract coverage.info $(pwd)'/boost/gil/*' --output-file coverage.info > /dev/null -lcov --list coverage.info -cd libs/gil -bash <(curl -s https://codecov.io/bash) -f ../../coverage.info || echo "Codecov did not collect coverage reports" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000000..c538be936d --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,55 @@ +name: Code Coverage + +on: + pull_request: + push: + branches: + - master + - develop + +env: + LIBRARY: gil + UBSAN_OPTIONS: print_stacktrace=1 + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: install packages + run: | + sudo apt update + sudo apt install g++ libpng-dev libjpeg-dev libtiff5-dev libraw-dev lcov python -y + + - name: Setup Boost + run: | + REF=${GITHUB_BASE_REF:-$GITHUB_REF} + BOOST_BRANCH=develop && [ "$REF" == "master" ] && BOOST_BRANCH=master || true + cd .. + git clone -b $BOOST_BRANCH --depth 1 https://github.com/boostorg/boost.git boost-root + cd boost-root + cp -r $GITHUB_WORKSPACE/* libs/$LIBRARY + git submodule update --init tools/boostdep + python tools/boostdep/depinst/depinst.py --git_args "--jobs 3" $LIBRARY + ./bootstrap.sh + ./b2 -d0 headers + + - name: Create user-config.jam + run: | + echo "using gcc : : g++ ;" > ~/user-config.jam + + - name: Run tests + run: | + cd ../boost-root + ./b2 -j3 libs/$LIBRARY/test coverage=on toolset=gcc cxxstd=11 variant=debug + lcov --directory bin.v2 --capture --no-external --directory $(pwd) --output-file coverage.info > /dev/null 2>&1 + lcov --extract coverage.info $(pwd)'/boost/gil/*' --output-file coverage.info > /dev/null + lcov --list coverage.info + + - name: Upload to Codecov + uses: codecov/codecov-action@v1.2.1 + with: + files: ../boost-root/coverage.info + \ No newline at end of file From 33ab3cfc1034b0a67526966b3c126faa912624d2 Mon Sep 17 00:00:00 2001 From: Pranam Lashkari Date: Mon, 1 Mar 2021 04:46:59 +0530 Subject: [PATCH 35/51] added missing header guards (#568) added missing header guards in --- include/boost/gil/image_processing/diffusion.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/boost/gil/image_processing/diffusion.hpp b/include/boost/gil/image_processing/diffusion.hpp index 0c1a3dce75..eedd50ebb8 100644 --- a/include/boost/gil/image_processing/diffusion.hpp +++ b/include/boost/gil/image_processing/diffusion.hpp @@ -1,10 +1,15 @@ // // Copyright 2020 Olzhas Zhumabek +// Copyright 2021 Pranam Lashkari // // Use, modification and distribution are subject to the Boost Software License, // Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) // + +#ifndef BOOST_GIL_IMAGE_PROCESSING_DIFFUSION_HPP +#define BOOST_GIL_IMAGE_PROCESSING_DIFFUSION_HPP + #include "boost/gil/detail/math.hpp" #include #include @@ -419,3 +424,5 @@ void anisotropic_diffusion(const InputView& input, const OutputView& output, uns } }} // namespace boost::gil + +#endif From ec25950aff8b953ce359cba95f711a90e3708112 Mon Sep 17 00:00:00 2001 From: Pranam Lashkari Date: Mon, 1 Mar 2021 14:42:33 +0530 Subject: [PATCH 36/51] added missing copyright info (#569) added missing copyright info --- include/boost/gil/image_processing/hessian.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/boost/gil/image_processing/hessian.hpp b/include/boost/gil/image_processing/hessian.hpp index f88c153607..f37c0ffdd5 100644 --- a/include/boost/gil/image_processing/hessian.hpp +++ b/include/boost/gil/image_processing/hessian.hpp @@ -1,3 +1,10 @@ +// +// Copyright 2019 Olzhas Zhumabek +// Copyright 2021 Scramjet911 <36035352+Scramjet911@users.noreply.github.com> +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + #ifndef BOOST_GIL_IMAGE_PROCESSING_HESSIAN_HPP #define BOOST_GIL_IMAGE_PROCESSING_HESSIAN_HPP From ad007ab92127daab26596524402e89ffbf386ccd Mon Sep 17 00:00:00 2001 From: Avinal Kumar <185067@nith.ac.in> Date: Mon, 1 Mar 2021 16:16:27 +0530 Subject: [PATCH 37/51] ci: Add Codecov badge and codecov.yml file (#567) --- README.md | 8 ++++---- codecov.yml | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 codecov.yml diff --git a/README.md b/README.md index 62895dae58..50a0cbe349 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ [![Conan](https://img.shields.io/badge/on-conan-blue.svg)](https://bintray.com/bincrafters/public-conan/boost_gil%3Abincrafters) [![Vcpkg](https://img.shields.io/badge/on-vcpkg-blue.svg)](https://github.com/Microsoft/vcpkg/tree/master/ports/boost-gil) -Documentation | GitHub Actions | Azure Pipelines | CircleCI | Regression ---------------|----------------|-----------------|-----------------|------------ -[![develop](https://img.shields.io/badge/doc-develop-blue.svg)](https://boostorg.github.io/gil/develop/) | [![GitHub Actions](https://github.com/boostorg/gil/workflows/CI/badge.svg?branch=develop)](https://github.com/boostorg/gil/actions?query=branch:develop) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h/branch/develop?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/develop) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=develop)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=develop) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/develop.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/develop) | [![gil](https://img.shields.io/badge/gil-develop-blue.svg)](http://www.boost.org/development/tests/develop/developer/gil.html) -[![master](https://img.shields.io/badge/doc-master-blue.svg)](https://boostorg.github.io/gil/) | [![GitHub Actions](https://github.com/boostorg/gil/workflows/CI/badge.svg?branch=master)](https://github.com/boostorg/gil/actions?query=branch:master) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/master) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=master)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=master) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/master.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/master) | [![gil](https://img.shields.io/badge/gil-master-blue.svg)](http://www.boost.org/development/tests/master/developer/gil.html) +Documentation | GitHub Actions | AppVeyor | Azure Pipelines | CircleCI | Regression | Codecov +--------------|----------------|------------|-----------------|-----------------|------------|---------- +[![develop](https://img.shields.io/badge/doc-develop-blue.svg)](https://boostorg.github.io/gil/develop/) | [![GitHub Actions](https://github.com/boostorg/gil/workflows/CI/badge.svg?branch=develop)](https://github.com/boostorg/gil/actions?query=branch:develop) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h/branch/develop?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/develop) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=develop)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=develop) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/develop.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/develop) | [![gil](https://img.shields.io/badge/gil-develop-blue.svg)](http://www.boost.org/development/tests/develop/developer/gil.html) | [![codecov](https://codecov.io/gh/boostorg/gil/branch/develop/graphs/badge.svg)](https://app.codecov.io/gh/boostorg/gil/branch/develop) +[![master](https://img.shields.io/badge/doc-master-blue.svg)](https://boostorg.github.io/gil/) | [![GitHub Actions](https://github.com/boostorg/gil/workflows/CI/badge.svg?branch=master)](https://github.com/boostorg/gil/actions?query=branch:master) | [![AppVeyor](https://ci.appveyor.com/api/projects/status/w4k19d8io2af168h?svg=true)](https://ci.appveyor.com/project/stefanseefeld/gil/branch/master) | [![Azure](https://dev.azure.com/boostorg/gil/_apis/build/status/boostorg.gil?branchName=master)](https://dev.azure.com/boostorg/gil/_build/latest?definitionId=7&branchName=master) | [![CircleCI](https://circleci.com/gh/boostorg/gil/tree/master.svg?style=shield)](https://circleci.com/gh/boostorg/workflows/gil/tree/master) | [![gil](https://img.shields.io/badge/gil-master-blue.svg)](http://www.boost.org/development/tests/master/developer/gil.html) | [![codecov](https://codecov.io/gh/boostorg/gil/branch/master/graphs/badge.svg)](https://app.codecov.io/gh/boostorg/gil/branch/master) # Boost.GIL diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..7359abc758 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +comment: + layout: "diff" + behavior: default + branches: + - master + - develop + require_changes: true From 093117d5650cb4f8f277dd73a00888c488447f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ferdinand=20Rivera=20Morell?= Date: Mon, 1 Mar 2021 14:13:36 -0600 Subject: [PATCH 38/51] Repoint B2 refs to new non-boostorg home. (#570) Also, relabel Boost.Build to the current B2 name. --- CONTRIBUTING.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5492bd7e52..91613c08c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ please follow the workflow explained in this document. - [5. Update your pull request](#5-update-your-pull-request) - [Development](#development) - [Install dependencies](#install-dependencies) - - [Using Boost.Build](#using-boostbuild) + - [Using B2](#using-boostbuild) - [Using CMake](#using-cmake) - [Running clang-tidy](#running-clang-tidy) - [Guidelines](#guidelines) @@ -35,7 +35,7 @@ please follow the workflow explained in this document. it may be a good idea to skim through the [Boost Getting Started](https://www.boost.org/more/getting_started/index.html) chapters, especially if you are going to use - [Boost.Build](https://boostorg.github.io/build/) for the first time. + [B2](https://www.bfgroup.xyz/b2/) for the first time. ## Pull Requests @@ -124,7 +124,7 @@ The preparation involves the following steps: git submodule update --init --recursive --jobs 8 ``` -3. Build the `b2` driver program for Boost.Build engine. +3. Build the `b2` driver program for B2 engine. ```shell ./bootstrap.sh @@ -311,15 +311,15 @@ Boost.GIL is a [header-only library](https://en.wikipedia.org/wiki/Header-only) which does not require sources compilation. Only test runners and [example](example/README.md) programs have to be compiled. -By default, Boost.GIL uses Boost.Build to build all the executables. +By default, Boost.GIL uses B2 to build all the executables. We also provide configuration for two alternative build systems: - [CMake](https://cmake.org) **NOTE:** The CMake is optional and the corresponding build configurations -for Boost.GIL do not offer equivalents for all Boost.Build features. -Most important difference to recognise is that Boost.Build will automatically +for Boost.GIL do not offer equivalents for all B2 features. +Most important difference to recognise is that B2 will automatically build any other Boost libraries required by Boost.GIL as dependencies. ### Install dependencies @@ -333,9 +333,9 @@ sudo apt-get install libjpeg-dev libpng-dev libtiff5-dev libraw-dev **TIP:** On Windows, use vcpkg with `user-config.jam` configuration provided in [example/b2/user-config-windows-vcpkg.jam](example/b2/). -### Using Boost.Build +### Using B2 -The [b2 invocation](https://boostorg.github.io/build/manual/develop/index.html#bbv2.overview.invocation) +The [b2 invocation](https://www.bfgroup.xyz/b2/manual/release/index.html#bbv2.overview.invocation) explains available options like `toolset`, `variant` and others. Simply, just execute `b2` to run all tests built using default From f18445f90ba0ae05877d659c9fb369ffb55afa95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ferdinand=20Rivera=20Morell?= Date: Mon, 1 Mar 2021 14:15:02 -0600 Subject: [PATCH 39/51] Repoint B2 refs to new non-boostorg home. (#571) --- example/b2/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/b2/README.md b/example/b2/README.md index e4f56aaf8a..c847a26015 100644 --- a/example/b2/README.md +++ b/example/b2/README.md @@ -8,7 +8,7 @@ users who wish to run complete build of Boost.GIL tests and examples. Copy any of the provided user configuration files to `%HOME%\user-config.jam` on Windows or `$HOME/user-config.jam` on Linux. -Refer to [Boost.Build User Manual](https://boostorg.github.io/build/) +Refer to [B2 User Manual](https://www.bfgroup.xyz/b2/manual/release/index.html) for more details about use of configuration files. ## Examples From 0778069b8e97fed6138fbd5dab6e5bb5e9a2ef37 Mon Sep 17 00:00:00 2001 From: Gopi Krishna Menon Date: Tue, 2 Mar 2021 15:32:07 +0530 Subject: [PATCH 40/51] Switch docs deployment from Travis CI to GitHub Actions (#563) --- .github/actions/docs-prerequisites/action.yml | 7 ++++ .../actions/generate-publish-doc/action.yml | 17 ++++++++ .../generate-publish-doc/upload-script.sh | 40 +++++++++++++++++++ .github/actions/setup-boost/action.yml | 17 ++++++++ .github/workflows/docs.yaml | 17 ++++++++ 5 files changed, 98 insertions(+) create mode 100644 .github/actions/docs-prerequisites/action.yml create mode 100644 .github/actions/generate-publish-doc/action.yml create mode 100644 .github/actions/generate-publish-doc/upload-script.sh create mode 100644 .github/actions/setup-boost/action.yml create mode 100644 .github/workflows/docs.yaml diff --git a/.github/actions/docs-prerequisites/action.yml b/.github/actions/docs-prerequisites/action.yml new file mode 100644 index 0000000000..bc5a6a426c --- /dev/null +++ b/.github/actions/docs-prerequisites/action.yml @@ -0,0 +1,7 @@ +name: 'Get Docs Prerequisites' +description: 'Downloads all the necessary packages for building documentation' +runs: + using: composite + steps: + - run: sudo apt-get install doxygen python3 python3-pip python3-setuptools python3-sphinx + shell: bash diff --git a/.github/actions/generate-publish-doc/action.yml b/.github/actions/generate-publish-doc/action.yml new file mode 100644 index 0000000000..d794f43e59 --- /dev/null +++ b/.github/actions/generate-publish-doc/action.yml @@ -0,0 +1,17 @@ +name: 'Generate Doc' +description: 'Runs b2 on lib/gil/doc to generate documentation' +inputs: + github_token: + description: 'Github Token for Access' + required: true +runs: + using: composite + steps: + - run: | + echo "using doxygen ;" > ~/user-config.jam + cd ../boost-root/libs + ../b2 gil/doc + cd gil + chmod +x $GITHUB_WORKSPACE/.github/actions/generate-publish-doc/upload-script.sh + $GITHUB_WORKSPACE/.github/actions/generate-publish-doc/upload-script.sh ${{inputs.github_token}} + shell: bash diff --git a/.github/actions/generate-publish-doc/upload-script.sh b/.github/actions/generate-publish-doc/upload-script.sh new file mode 100644 index 0000000000..6883c198fc --- /dev/null +++ b/.github/actions/generate-publish-doc/upload-script.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -e # Exit with Non Zero exit Code + +# Save some useful information +SHA=`git rev-parse --verify HEAD` + +mkdir temp-doc +cd temp-doc +git init + +git config user.name "Github Action Upload Docs" +git config user.email "Github Action Upload Docs" + +git remote add upstream "https://$1@github.com/boostorg/gil.git" + +git fetch upstream +git switch gh-pages + + +if [ "${GITHUB_REF##*/}" = develop ]; then + rm -r develop + mkdir -p develop/doc + cp ../index.html develop/ + cp ../doc/index.html develop/doc + cp -a ../doc/html develop/doc/ +else + rm index.html + rm -r html + cp ../doc/index.html . + cp -r ../doc/html . +fi + + +git add . + +# Commit the new version. +git commit -m "Deploy to GitHub Pages: ${SHA}" + +# Now that we're all set up, we can push. +git push -q upstream HEAD:gh-pages diff --git a/.github/actions/setup-boost/action.yml b/.github/actions/setup-boost/action.yml new file mode 100644 index 0000000000..89ba925f37 --- /dev/null +++ b/.github/actions/setup-boost/action.yml @@ -0,0 +1,17 @@ +name: 'Setup-Boost' +description: 'Downloads and sets up the necessary dependencies of Boost' +runs: + using: composite + steps: + - run: | + REF=${GITHUB_BASE_REF:-$GITHUB_REF} + BOOST_BRANCH=develop && [ "$REF" == "master" ] && BOOST_BRANCH=master || true + cd .. + git clone -b $BOOST_BRANCH --depth 1 https://github.com/boostorg/boost.git boost-root + cd boost-root + cp -r $GITHUB_WORKSPACE/* libs/gil + git submodule update --init tools/boostdep + python tools/boostdep/depinst/depinst.py --git_args "--jobs 3" gil + ./bootstrap.sh + ./b2 -d0 headers + shell: bash diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000000..376c42360b --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,17 @@ +name: Docs-Build +on: + push: + branches: + - master + - develop + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/docs-prerequisites + - uses: ./.github/actions/setup-boost + - uses: ./.github/actions/generate-publish-doc + with: + github_token: ${{ secrets.GIL_TEST_TOKEN }} From 8bd2413127237e6fdb3a903ea662bd5e0dfc1b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Tue, 2 Mar 2021 19:25:11 +0100 Subject: [PATCH 41/51] ci: Remove GCC 5 on GitHub Actions and Azure Pipelines (#572) Required by #526 See also https://github.com/boostorg/website/pull/562 Disable system.debug on AzP --- .azure-pipelines.yml | 11 +++++++++-- .github/workflows/ci.yml | 7 ------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 524365e51f..bf35a61860 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -5,7 +5,7 @@ # (See accompanying file LICENSE_1_0.txt or copy at http://boost.org/LICENSE_1_0.txt) # variables: - system.debug: true + #system.debug: true configuration: release trigger: @@ -15,10 +15,13 @@ trigger: - ml/* jobs: - - job: 'ubuntu1604_gcc5_cxx11_cmake' + - job: 'ubuntu1604_gcc6_cxx14_cmake' pool: vmImage: 'ubuntu-16.04' steps: + - template: .ci/azure-pipelines/steps-install-gcc.yml + parameters: + major_version: '6' - script: which g++ && g++ --version displayName: 'Check GCC' - template: .ci/azure-pipelines/steps-check-cmake.yml @@ -28,12 +31,16 @@ jobs: displayName: 'Install dependencies' - template: .ci/azure-pipelines/steps-install-boost.yml - template: .ci/azure-pipelines/steps-cmake-build-and-test.yml + parameters: + cxxver: '14' - job: 'ubuntu1604_gcc8_cxx14_cmake' pool: vmImage: 'ubuntu-16.04' steps: - template: .ci/azure-pipelines/steps-install-gcc.yml + parameters: + major_version: '8' - script: which g++ && g++ --version displayName: 'Check GCC' - template: .ci/azure-pipelines/steps-check-cmake.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 880ca948ed..3c3f063b6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,13 +18,6 @@ jobs: fail-fast: false matrix: include: - - toolset: gcc-4.9 - cxxstd: "11" - os: ubuntu-16.04 - install: g++-4.9 - - toolset: gcc-5 - cxxstd: "11,14,1z" - os: ubuntu-16.04 - toolset: gcc-6 cxxstd: "11,14,1z" os: ubuntu-16.04 From 745d033ef2f382dea338851cfa6b2668f1e52905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Fri, 5 Mar 2021 20:57:59 +0100 Subject: [PATCH 42/51] ci: Use GitHub Actions automatic GITHUB_TOKEN secret in docs workflow --- .github/workflows/docs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 376c42360b..171f44e7f5 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: ./.github/actions/docs-prerequisites - - uses: ./.github/actions/setup-boost + - uses: ./.github/actions/setup-boost - uses: ./.github/actions/generate-publish-doc with: - github_token: ${{ secrets.GIL_TEST_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} From 392ac4996d49d7ea958426438775d291b689af07 Mon Sep 17 00:00:00 2001 From: Gaurav kumar <45493322+gkumar28@users.noreply.github.com> Date: Tue, 9 Mar 2021 10:26:21 +0530 Subject: [PATCH 43/51] Rotation of image by any arbitrary angle from its center (#565) * basic rotation function for theta between +/- 180 deg * added scaling matrix for same dimensions and added bound checks for theta --- .../boost/gil/extension/numeric/affine.hpp | 62 ++++++++++++++++++- test/extension/numeric/matrix3x2.cpp | 16 +++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/include/boost/gil/extension/numeric/affine.hpp b/include/boost/gil/extension/numeric/affine.hpp index 26054149a9..7b590e64a6 100644 --- a/include/boost/gil/extension/numeric/affine.hpp +++ b/include/boost/gil/extension/numeric/affine.hpp @@ -108,6 +108,66 @@ boost::gil::matrix3x2 inverse(boost::gil::matrix3x2 m) return res; } +/// \fn gil::matrix3x2 center_rotate +/// \tparam T Data type for source image dimensions +/// \tparam F Data type for angle through which image is to be rotated +/// @param dims dimensions of source image +/// @param rads angle through which image is to be rotated +/// @return A transformation matrix for rotating the source image about its center +/// \brief rotates an image from its center point +/// using consecutive affine transformations. +template +boost::gil::matrix3x2 center_rotate(boost::gil::point dims,F rads) +{ + const F PI = F(3.141592653589793238); + const F c_theta = std::abs(std::cos(rads)); + const F s_theta = std::abs(std::sin(rads)); + + // Bound checks for angle rads + while(rads + PI < 0) + { + rads = rads + PI; + } + + while(rads > PI) + { + rads = rads - PI; + } + + // Basic Rotation Matrix + boost::gil::matrix3x2 rotate = boost::gil::matrix3x2::get_rotate(rads); + + // Find distance for translating the image into view + boost::gil::matrix3x2 translation(0,0,0,0,0,0); + if(rads > 0) + { + translation.b = s_theta; + } + else + { + translation.c = s_theta; + } + + if(std::abs(rads) > PI/2) + { + translation.a = c_theta; + translation.d = c_theta; + } + + // To bring the complete image into view + boost::gil::matrix3x2 translate = + boost::gil::matrix3x2::get_translate(-1 * dims * translation); + + // To fit inside the source dimensions + boost::gil::matrix3x2 scale = + boost::gil::matrix3x2::get_scale( + s_theta * dims.y / dims.x + c_theta , + s_theta * dims.x / dims.y + c_theta + ); + + return scale * translate * rotate; +} + }} // namespace boost::gil -#endif +#endif \ No newline at end of file diff --git a/test/extension/numeric/matrix3x2.cpp b/test/extension/numeric/matrix3x2.cpp index d3c34f4e48..1f0ae8f67d 100644 --- a/test/extension/numeric/matrix3x2.cpp +++ b/test/extension/numeric/matrix3x2.cpp @@ -201,6 +201,21 @@ void test_matrix3x2_inverse() BOOST_TEST_WITH(p.y, p2.y, with_tolerance(1e-9)); } +void test_matrix3x2_center_rotate() +{ + gil::point dimension(100.0,100.0); + gil::matrix3x2 m1; + + m1 = gil::center_rotate(dimension, HALF_PI); + + BOOST_TEST_WITH(m1.a , std::cos(HALF_PI) , with_tolerance(1e-9)); + BOOST_TEST_EQ (m1.b , 1); + BOOST_TEST_EQ (m1.c , -1); + BOOST_TEST_WITH(m1.d , std::cos(HALF_PI) , with_tolerance(1e-9)); + BOOST_TEST_EQ (m1.e , 100); + BOOST_TEST_WITH(m1.f , std::cos(HALF_PI) , with_tolerance(1e-9)); +} + int main() { test_matrix3x2_default_constructor(); @@ -215,6 +230,7 @@ int main() test_matrix3x2_get_translate(); test_matrix3x2_transform(); test_matrix3x2_inverse(); + test_matrix3x2_center_rotate(); return ::boost::report_errors(); } From 6da59cc3351e5657275d3a536e0b6e7a1b6ac738 Mon Sep 17 00:00:00 2001 From: Gopi Krishna Menon Date: Wed, 10 Mar 2021 13:46:10 +0530 Subject: [PATCH 44/51] docs: Updated GA workflow to publish with peaceiris/action-gh-pages (#574) --- .github/actions/generate-doc/action.yml | 13 ++++++++++++ .../docs-config.sh} | 20 ++++++------------- .../actions/generate-publish-doc/action.yml | 17 ---------------- .github/workflows/docs.yaml | 7 ++++++- 4 files changed, 25 insertions(+), 32 deletions(-) create mode 100644 .github/actions/generate-doc/action.yml rename .github/actions/{generate-publish-doc/upload-script.sh => generate-doc/docs-config.sh} (51%) delete mode 100644 .github/actions/generate-publish-doc/action.yml diff --git a/.github/actions/generate-doc/action.yml b/.github/actions/generate-doc/action.yml new file mode 100644 index 0000000000..3ab8995f32 --- /dev/null +++ b/.github/actions/generate-doc/action.yml @@ -0,0 +1,13 @@ +name: 'Generate Doc' +description: 'Runs b2 on lib/gil/doc to generate documentation' +runs: + using: composite + steps: + - run: | + echo "using doxygen ;" > ~/user-config.jam + cd ../boost-root/libs + ../b2 gil/doc + cd gil + chmod +x $GITHUB_WORKSPACE/.github/actions/generate-doc/docs-config.sh + $GITHUB_WORKSPACE/.github/actions/generate-doc/docs-config.sh + shell: bash diff --git a/.github/actions/generate-publish-doc/upload-script.sh b/.github/actions/generate-doc/docs-config.sh similarity index 51% rename from .github/actions/generate-publish-doc/upload-script.sh rename to .github/actions/generate-doc/docs-config.sh index 6883c198fc..695d6d0eb0 100644 --- a/.github/actions/generate-publish-doc/upload-script.sh +++ b/.github/actions/generate-doc/docs-config.sh @@ -1,40 +1,32 @@ #!/bin/bash set -e # Exit with Non Zero exit Code -# Save some useful information -SHA=`git rev-parse --verify HEAD` - mkdir temp-doc cd temp-doc + git init -git config user.name "Github Action Upload Docs" -git config user.email "Github Action Upload Docs" -git remote add upstream "https://$1@github.com/boostorg/gil.git" +git remote add upstream "https://github.com/boostorg/gil.git" git fetch upstream git switch gh-pages if [ "${GITHUB_REF##*/}" = develop ]; then + # Only updates develop directory and keeps others intact rm -r develop mkdir -p develop/doc cp ../index.html develop/ cp ../doc/index.html develop/doc cp -a ../doc/html develop/doc/ else + # main branch rm index.html rm -r html cp ../doc/index.html . cp -r ../doc/html . fi - -git add . - -# Commit the new version. -git commit -m "Deploy to GitHub Pages: ${SHA}" - -# Now that we're all set up, we can push. -git push -q upstream HEAD:gh-pages +# Remove version control +rm -rf .git \ No newline at end of file diff --git a/.github/actions/generate-publish-doc/action.yml b/.github/actions/generate-publish-doc/action.yml deleted file mode 100644 index d794f43e59..0000000000 --- a/.github/actions/generate-publish-doc/action.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: 'Generate Doc' -description: 'Runs b2 on lib/gil/doc to generate documentation' -inputs: - github_token: - description: 'Github Token for Access' - required: true -runs: - using: composite - steps: - - run: | - echo "using doxygen ;" > ~/user-config.jam - cd ../boost-root/libs - ../b2 gil/doc - cd gil - chmod +x $GITHUB_WORKSPACE/.github/actions/generate-publish-doc/upload-script.sh - $GITHUB_WORKSPACE/.github/actions/generate-publish-doc/upload-script.sh ${{inputs.github_token}} - shell: bash diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 171f44e7f5..9a20e7c55f 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -12,6 +12,11 @@ jobs: - uses: actions/checkout@v2 - uses: ./.github/actions/docs-prerequisites - uses: ./.github/actions/setup-boost - - uses: ./.github/actions/generate-publish-doc + - uses: ./.github/actions/generate-doc + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ../boost-root/libs/gil/temp-doc + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' From b8564e256c067275287792a1678a74cf81d4ae08 Mon Sep 17 00:00:00 2001 From: Samuel Debionne Date: Thu, 18 Mar 2021 10:53:53 +0100 Subject: [PATCH 45/51] Fix any_image_view::const_t (#526) Use inherited constructors in any_image as well --- .../gil/extension/dynamic_image/any_image.hpp | 18 +-- .../dynamic_image/any_image_view.hpp | 16 +-- test/extension/dynamic_image/CMakeLists.txt | 1 + test/extension/dynamic_image/Jamfile | 1 + .../dynamic_image/any_image_view.cpp | 128 ++++++++++++++++++ 5 files changed, 136 insertions(+), 28 deletions(-) create mode 100644 test/extension/dynamic_image/any_image_view.cpp diff --git a/include/boost/gil/extension/dynamic_image/any_image.hpp b/include/boost/gil/extension/dynamic_image/any_image.hpp index 701fece697..69b674b1f9 100644 --- a/include/boost/gil/extension/dynamic_image/any_image.hpp +++ b/include/boost/gil/extension/dynamic_image/any_image.hpp @@ -89,29 +89,15 @@ template class any_image : public variant2::variant { using parent_t = variant2::variant; + public: using view_t = mp11::mp_rename, any_image_view>; using const_view_t = mp11::mp_rename, any_image_view>; using x_coord_t = std::ptrdiff_t; using y_coord_t = std::ptrdiff_t; using point_t = point; - - any_image() = default; - any_image(any_image const& img) : parent_t((parent_t const&)img) {} - - template - explicit any_image(Image const& img) : parent_t(img) {} - template - any_image(Image&& img) : parent_t(std::move(img)) {} - - template - explicit any_image(Image& img, bool do_swap) : parent_t(img, do_swap) {} - - template - any_image(any_image const& img) - : parent_t((variant2::variant const&)img) - {} + using parent_t::parent_t; any_image& operator=(any_image const& img) { diff --git a/include/boost/gil/extension/dynamic_image/any_image_view.hpp b/include/boost/gil/extension/dynamic_image/any_image_view.hpp index 901fb6fdbe..5d71b588b0 100644 --- a/include/boost/gil/extension/dynamic_image/any_image_view.hpp +++ b/include/boost/gil/extension/dynamic_image/any_image_view.hpp @@ -1,5 +1,6 @@ // // Copyright 2005-2007 Adobe Systems Incorporated +// Copyright 2020 Samuel Debionne // // Distributed under the Boost Software License, Version 1.0 // See accompanying file LICENSE_1_0.txt or copy at @@ -24,10 +25,10 @@ struct dynamic_xy_step_transposed_type; namespace detail { template -struct get_const_t { using type = typename View::const_t; }; +using get_const_t = typename View::const_t; template -struct views_get_const_t : mp11::mp_transform {}; +using views_get_const_t = mp11::mp_transform; // works for both image_view and image struct any_type_get_num_channels @@ -82,16 +83,7 @@ class any_image_view : public variant2::variant using point_t = point; using size_type = std::size_t; - any_image_view() = default; - any_image_view(any_image_view const& view) : parent_t((parent_t const&)view) {} - - template - explicit any_image_view(View const& view) : parent_t(view) {} - - template - any_image_view(any_image_view const& view) - : parent_t((variant2::variant const&)view) - {} + using parent_t::parent_t; any_image_view& operator=(any_image_view const& view) { diff --git a/test/extension/dynamic_image/CMakeLists.txt b/test/extension/dynamic_image/CMakeLists.txt index d2a37bf10c..0e720b5bab 100644 --- a/test/extension/dynamic_image/CMakeLists.txt +++ b/test/extension/dynamic_image/CMakeLists.txt @@ -8,6 +8,7 @@ message(STATUS "Boost.GIL: Configuring tests in test/extension/dynamic_image") foreach(_name any_image + any_image_view subimage_view) set(_test t_ext_dynamic_image_${_name}) set(_target test_ext_dynamic_image_${_name}) diff --git a/test/extension/dynamic_image/Jamfile b/test/extension/dynamic_image/Jamfile index 8cd2d5184d..63189265e4 100644 --- a/test/extension/dynamic_image/Jamfile +++ b/test/extension/dynamic_image/Jamfile @@ -11,4 +11,5 @@ import testing ; alias headers : [ generate_self_contained_headers extension/dynamic_image ] ; run any_image.cpp ; +run any_image_view.cpp ; run subimage_view.cpp ; diff --git a/test/extension/dynamic_image/any_image_view.cpp b/test/extension/dynamic_image/any_image_view.cpp new file mode 100644 index 0000000000..176e7f25b2 --- /dev/null +++ b/test/extension/dynamic_image/any_image_view.cpp @@ -0,0 +1,128 @@ +// +// Copyright 2020 Samuel Debionne +// +// Distributed under the Boost Software License, Version 1.0 +// See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt +// +#include +#include + +#include +#include + +#include + +#include "test_fixture.hpp" +#include "core/image/test_fixture.hpp" + +namespace gil = boost::gil; +namespace fixture = boost::gil::test::fixture; + +void test_any_image_view_nested_types() +{ + BOOST_TEST_TRAIT_SAME(gil::any_image_view::const_t, gil::any_image_view); +} + + +struct test_any_image_view_init_ctor +{ + template + void operator()(Image const&) + { + using image_t = Image; + using view_t = typename Image::view_t; + using any_view_t = gil::any_image_view; + using any_const_view_t = typename any_view_t::const_t; + Image i0(fixture::create_image(4, 4, 128)); + + view_t v0 = gil::view(i0); + any_view_t v1 = v0; + + BOOST_TEST_EQ(v1.dimensions().x, 4); + BOOST_TEST_EQ(v1.dimensions().y, 4); + + any_const_view_t v2 = v0; + + BOOST_TEST_EQ(v2.dimensions().x, 4); + BOOST_TEST_EQ(v2.dimensions().y, 4); + + //any_const_view_t v3 = v1; + } + static void run() + { + boost::mp11::mp_for_each(test_any_image_view_init_ctor{}); + } +}; + +struct test_any_image_view_copy_ctor +{ + template + void operator()(Image const&) + { + using image_t = Image; + using view_t = typename Image::view_t; + using any_view_t = gil::any_image_view; + using any_const_view_t = typename any_view_t::const_t; + Image i0(fixture::create_image(4, 4, 128)); + + view_t v0 = gil::view(i0); + any_view_t v1 = v0; + + BOOST_TEST_EQ(v1.dimensions().x, 4); + BOOST_TEST_EQ(v1.dimensions().y, 4); + + any_const_view_t v2 = v0; + + BOOST_TEST_EQ(v2.dimensions().x, 4); + BOOST_TEST_EQ(v2.dimensions().y, 4); + + //any_const_view_t v3 = v1; + } + static void run() + { + boost::mp11::mp_for_each(test_any_image_view_copy_ctor{}); + } +}; + +struct test_any_image_view_assign_operator +{ + template + void operator()(Image const&) + { + using image_t = Image; + using view_t = typename Image::view_t; + using any_view_t = gil::any_image_view; + using any_const_view_t = typename any_view_t::const_t; + Image i0(fixture::create_image(4, 4, 128)); + + view_t v0 = gil::view(i0); + any_view_t v1; + any_const_view_t v2; + + v1 = v0; + + BOOST_TEST_EQ(v1.dimensions().x, 4); + BOOST_TEST_EQ(v1.dimensions().y, 4); + + v2 = v0; + + BOOST_TEST_EQ(v2.dimensions().x, 4); + BOOST_TEST_EQ(v2.dimensions().y, 4); + + //v2 = v1; + } + static void run() + { + boost::mp11::mp_for_each(test_any_image_view_assign_operator{}); + } +}; + +int main() +{ + test_any_image_view_init_ctor::run(); + test_any_image_view_copy_ctor::run(); + test_any_image_view_assign_operator::run(); + + return ::boost::report_errors(); +} From dc9ba74cb3e7801f7025902796a76172d75517bf Mon Sep 17 00:00:00 2001 From: Debabrata Mandal <32168969+codejaeger@users.noreply.github.com> Date: Thu, 25 Mar 2021 01:49:06 +0530 Subject: [PATCH 46/51] Add docs for histogram (#503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add docs for histogram * Add docs and make changes: - Made changes suggested in first review - Add docs for relevant files * Rename docs file and correct typos * Change doc for cumulative histogram * Add docs for histogram equalization * Make changes suggested in review * Add docs * Remove docs for algorithms * Move images to test_images Co-authored-by: Mateusz Łoskot --- doc/histogram/create.rst | 42 +++++++ doc/histogram/cumulative.rst | 29 +++++ doc/histogram/extend.rst | 68 ++++++++++++ doc/histogram/extension/index.rst | 12 ++ doc/histogram/extension/overview.rst | 32 ++++++ doc/histogram/extension/std.rst | 45 ++++++++ doc/histogram/fill.rst | 103 ++++++++++++++++++ doc/histogram/index.rst | 20 ++++ doc/histogram/limitations.rst | 6 + doc/histogram/overview.rst | 28 +++++ doc/histogram/stl_compatibility.rst | 6 + doc/histogram/subhistogram.rst | 66 +++++++++++ doc/histogram/utilities.rst | 6 + .../contrast_enhancement/index.rst | 11 ++ .../contrast_enhancement/overview.rst | 15 +++ doc/image_processing/index.rst | 2 + doc/index.rst | 2 + 17 files changed, 493 insertions(+) create mode 100644 doc/histogram/create.rst create mode 100644 doc/histogram/cumulative.rst create mode 100644 doc/histogram/extend.rst create mode 100644 doc/histogram/extension/index.rst create mode 100644 doc/histogram/extension/overview.rst create mode 100644 doc/histogram/extension/std.rst create mode 100644 doc/histogram/fill.rst create mode 100644 doc/histogram/index.rst create mode 100644 doc/histogram/limitations.rst create mode 100644 doc/histogram/overview.rst create mode 100644 doc/histogram/stl_compatibility.rst create mode 100644 doc/histogram/subhistogram.rst create mode 100644 doc/histogram/utilities.rst create mode 100644 doc/image_processing/contrast_enhancement/index.rst create mode 100644 doc/image_processing/contrast_enhancement/overview.rst diff --git a/doc/histogram/create.rst b/doc/histogram/create.rst new file mode 100644 index 0000000000..7df551c63e --- /dev/null +++ b/doc/histogram/create.rst @@ -0,0 +1,42 @@ +.. _create_histogram: + +Create a histogram +================== + +**Method 1** - Using the histogram constructor + +Syntax:: + + histogram + +``Type1`` .. ``TypeN`` correspond to the axis type of the N axes in the histogram + +Example: If we want a 3D histogram of Axis1 of type ``int``, Axis2 of type ``float`` and Axis3 of type ``std::string`` +we would do it this way:: + + histogram h; + +And done. + + +**Method 2** (TODO) - Using make_histogram() + +There is an alternative to create the histogram directly from +a GIL image view. + +This should be the preferred over method-1 when creating +histogram with GIL images, since it creates a histogram with axes configured +to match the GIL image. + +Also it is easier than method-1. + +Syntax:: + + auto h = make_histogram(view(image)); + +where ``image`` could be a ``gray8_image_t``/``rgb8_image_t`` object read from source. + + + + + diff --git a/doc/histogram/cumulative.rst b/doc/histogram/cumulative.rst new file mode 100644 index 0000000000..a769f9ddde --- /dev/null +++ b/doc/histogram/cumulative.rst @@ -0,0 +1,29 @@ +.. _cumulative_histogram: + +Making a cumulative histogram +============================= + +Overview +-------- + +A cumulative histogram is a histogram in which each bin stores the count / frequency of itself +as well as all the bins with keys 'smaller' than the particular bin. +As such, a notion of ordering among its keys should be existant in the histogram. + +The GIL histogram class has the ability to convert itself into its cumulative version. + +Since the container needs to first get an ordering +over the keys a key sorting takes place before calculating the cumulative histogram. + +Example: + + .. code-block:: cpp + + histogram h; + /* + Fill histogram ... + */ + auto h1 = cumulative_histogram(h); + +Tip: *In case you need to store the cumulative histogram elsewhere, consider creating a copy of the histogram +and then call the function*. \ No newline at end of file diff --git a/doc/histogram/extend.rst b/doc/histogram/extend.rst new file mode 100644 index 0000000000..3197af7841 --- /dev/null +++ b/doc/histogram/extend.rst @@ -0,0 +1,68 @@ +.. _extend_support: + +Extending the class +=================== + +.. contents:: + :local: + :depth: 1 + +User defined Axes +----------------- + +In case you need a histogram with an axes of an arbitrary type that is not identified by +the C++ Standard Library, you need to provide a overload for the hashing function that is +used in the histogram class. + +GIL's histogram class uses ``boost::hash_combine`` in a sub routine to generate a hash from +the key. + +So we need to provide an overload of ``boost::hash_combine`` for the purpose. + +For example, let's consider you need a histogram with an axis over class Test. + +.. code-block:: cpp + + // File : ./test.hpp + #include + #include + + struct Test + { + int a{0}; + Test() = default; + Test(int c) : a(c) {} + bool operator==(Test const& other) const + { + return (a == other.a); + } + }; + + namespace boost { + std::size_t hash_value(Test const& t) + { + // Replace with your hashing code + std::hash hasher; + return hasher(t.a); + } + } + +Now lets get to the usage example. + +.. code-block:: cpp + + #include + #include + #include + // Mind the order of include i.e. test.hpp before boost/gil.hpp + + using namespace boost::gil; + + int main() + { + boost::gil::histogram h; + Test t(1); + h(t) = 1; + std::cout< v; + gil::gray8_image_t img; + /* + Fill image ... + */ + gil::fill_histogram(view(img), v, false); + +#. **cumulative_histogram()** + + .. code-block:: cpp + + // Demo for std::vector + std::vector v; + /* + Fill vector... + */ + gil::cumulative_histogram(v); + + + + + + diff --git a/doc/histogram/fill.rst b/doc/histogram/fill.rst new file mode 100644 index 0000000000..cf3f913c6a --- /dev/null +++ b/doc/histogram/fill.rst @@ -0,0 +1,103 @@ +.. _fill_it: + +Fill histogram +============== + +.. contents:: + :local: + :depth: 1 + +Overview +-------- + +We will demonstrate the available options for filling an instance of the `histogram` class with +values that cater from the most simplest to the complex needs that might arise. + +Basic +----- + +#. Use operator() + + **Task** - Add value to a particular cell / key / bin in histogram + + .. code-block:: cpp + + histogram h; + h(1, 2) = 1; + +#. Use operator[] + + This requires to input the indices in a format the histogram internally stores its keys, + which is of ``std::tuple`` due to its simple interface. + + **Task** - Output value of a bin + + .. code-block:: cpp + + histogram h; + h(1, 2) = 1; + h[{1, 2}] += 1; // Note the curly braces reqd. to construct a tuple + std::cout< A; + /* + Fill value in A + */ + histogram B(A), C; + C = A; + +#. Use a GIL image view + + You can also use GIL images to directly fill histograms. + + **Task** - Fill histogram using GIL image view + + .. code-block:: cpp + + gil::gray8_image_t img; + /* + Fill img ... + */ + histogram h; + h.fill(view(img)); + // OR + gil::fill_histogram(view(img), h, false); // false if histogram needs to be cleared before filling + + +Advanced +-------- + +#. Fill histogram using only a few dimensions of image + + **Task** - Make an histogram over Red and Blue channel of an rgb image + + .. code-block:: cpp + + gil::rgb8_image_t img; + /* + Fill img ... + */ + histogram h; + fill_histogram<0, 2>(view(img), h, false); // 0 - red, 1 - green, 2 - blue + +#. Fill histogram using GIL pixel + + **Task** - Fill histogram bin using pixel construct in GIL + + .. code-block:: cpp + + gil::gray8_image_t img; + /* + Fill img ... + */ + histogram h; + gil::for_each_pixel(view(img), [](gil::gray8_pixel_t const& p){ + ++h[h.key_from_pixel(p)]; + }); + diff --git a/doc/histogram/index.rst b/doc/histogram/index.rst new file mode 100644 index 0000000000..ff8cc16bba --- /dev/null +++ b/doc/histogram/index.rst @@ -0,0 +1,20 @@ +Histogram +========= + +The GIL documentation sections listed below are dedicated to describe the +histogram class and functions used in many image processing algorithms. + +.. toctree:: + :maxdepth: 1 + :caption: Table of Contents + + overview + create + fill + subhistogram + cumulative + stl_compatibility + utilities + extend + limitations + extension/index diff --git a/doc/histogram/limitations.rst b/doc/histogram/limitations.rst new file mode 100644 index 0000000000..819d308f6e --- /dev/null +++ b/doc/histogram/limitations.rst @@ -0,0 +1,6 @@ +.. _limitations: + +Limitations +=========== + +*TODO* \ No newline at end of file diff --git a/doc/histogram/overview.rst b/doc/histogram/overview.rst new file mode 100644 index 0000000000..8b222659e8 --- /dev/null +++ b/doc/histogram/overview.rst @@ -0,0 +1,28 @@ +Overview +======== + +.. contents:: + :local: + :depth: 1 + +Description +----------- + +The histogram class is built on top of std::unordered_map to keep it compatible with other +STL algorithms. It can support any number of axes (known at compile time i.e. during class +instantiation). Suitable conversion routines from GIL image constructs to the histogram bin +key are shipped with the class itself. + + +Tutorials +--------- +The following flow is recommended: + #. :ref:`create_histogram` + #. :ref:`fill_it` + #. :ref:`sub_histogram` + #. :ref:`cumulative_histogram` + #. :ref:`stl_compatibility` + #. :ref:`extend_support` + #. :ref:`limitations` + +.. note:: To try out these tutorials you need to get a clone of the repository, since it is not yet released. diff --git a/doc/histogram/stl_compatibility.rst b/doc/histogram/stl_compatibility.rst new file mode 100644 index 0000000000..9b932a2cef --- /dev/null +++ b/doc/histogram/stl_compatibility.rst @@ -0,0 +1,6 @@ +.. _stl_compatibility: + +STL compatibility +================= + +*TODO* \ No newline at end of file diff --git a/doc/histogram/subhistogram.rst b/doc/histogram/subhistogram.rst new file mode 100644 index 0000000000..4e046a45a2 --- /dev/null +++ b/doc/histogram/subhistogram.rst @@ -0,0 +1,66 @@ +.. _sub_histogram: + +Making a sub-histogram +====================== + +Overview +-------- + +Sub-histogram is a subset of a histogram or one that is formed by masking out a +few axis of the parent histogram. + +GIL class histogram provides these functions as members and returns an instance of +the desired sub-class. + +#. Histogram over fewer axes + + **Task** - Get a 2D histogram from a 3D RGB histogram over red and green axes + + .. code-block:: cpp + + histogram h; + gil::rgb8_image_t img; + /* + Fill img ... + */ + fill_histogram(view(img), h, false); + auto sub_h = h.sub_histogram<0, 1>(); // sub_h is a 2D histogram + + Demo output: + + .. code-block:: cpp + + h is {{1, 2, 3} : 1, + {1, 4, 3} : 2, + {1, 2, 5} : 3} + sub_h would be {{1, 2} : 4, {1, 4} : 2} + +#. Histogram using a particular key range + + **Task** - Get a 2D histogram from a 3D RGB histogram for bins whose red color lie between 10 - 20 + and blue color lie between 2 - 10 + + .. code-block:: cpp + + histogram h; + gil::rgb8_image_t img; + /* + Fill img ... + */ + fill_histogram(view(img), h, false); + std::tuple low, high; + low = {10, 0, 2} // Since no check over blue channel pass any dummy value + high = {20, 0, 10} // Since no check over blue channel pass any dummy value + auto sub_h = h.sub_histogram<0, 2>(low, high); // In angle brackets pass the relevant dimensions in order + + Demo Output: + + .. code-block:: cpp + + h is {{11, 2, 3 } : 1, + {1 , 4, 11} : 2, + {1 , 2, 1 } : 3, + {11, 3, 3 } : 4} + sub_h would be {{11, 2, 3} : 1, {11, 3, 3} : 4} + + diff --git a/doc/histogram/utilities.rst b/doc/histogram/utilities.rst new file mode 100644 index 0000000000..5c4bf77554 --- /dev/null +++ b/doc/histogram/utilities.rst @@ -0,0 +1,6 @@ +.. _utilities: + +Utilities +========= + +*TODO* \ No newline at end of file diff --git a/doc/image_processing/contrast_enhancement/index.rst b/doc/image_processing/contrast_enhancement/index.rst new file mode 100644 index 0000000000..60c60cd468 --- /dev/null +++ b/doc/image_processing/contrast_enhancement/index.rst @@ -0,0 +1,11 @@ +Contrast Enhancement +==================== + +The GIL documentation sections listed below are dedicated to describe image +processing algorithms used for contrast enhancement. + +.. toctree:: + :maxdepth: 1 + :caption: Table of Contents + + overview \ No newline at end of file diff --git a/doc/image_processing/contrast_enhancement/overview.rst b/doc/image_processing/contrast_enhancement/overview.rst new file mode 100644 index 0000000000..4677055112 --- /dev/null +++ b/doc/image_processing/contrast_enhancement/overview.rst @@ -0,0 +1,15 @@ +Overview +======== + +Contrast Enhancement is a significant part of image processing algorithms. Too dark or too +light images don't look good to the human eyes. Hence while the primary focus of these +algorithms is to enhance visual beauty, some applications of this in medical research is also +prevalent. + +We have a few contrast enhancement algorithms in the GIL image processing suite as well. +These include : + #. :ref:`he` + #. :ref:`hm` + #. :ref:`ahe` + #. Linear and Non-Linear Contrast stretching + diff --git a/doc/image_processing/index.rst b/doc/image_processing/index.rst index 71c01c742f..c5bc61cfad 100644 --- a/doc/image_processing/index.rst +++ b/doc/image_processing/index.rst @@ -11,3 +11,5 @@ features, structures and algorithms, for image processing and analysis. overview basics affine-region-detectors + contrast_enhancement/index + diff --git a/doc/index.rst b/doc/index.rst index 2a52bd6fa1..71bdcf1200 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -26,6 +26,7 @@ Core Library Documentation design/index image_processing/index + histogram/index API Reference <./reference/index.html#://> Extensions Documentation @@ -37,6 +38,7 @@ Extensions Documentation io toolbox numeric + histogram/extension/index Examples -------- From 6e91e4bf5c1bc9b5def5ef60e3e11bc475cbf11d Mon Sep 17 00:00:00 2001 From: Debabrata Mandal <32168969+codejaeger@users.noreply.github.com> Date: Thu, 25 Mar 2021 01:50:12 +0530 Subject: [PATCH 47/51] Update image url links for docs for histogram (#586) --- .../contrast_enhancement/barbara.jpg | Bin 24897 -> 0 bytes .../contrast_enhancement/church.jpg | Bin 34771 -> 0 bytes .../contrast_enhancement/he_chart.png | Bin 14693 -> 0 bytes .../histogram_equalization.rst | 4 ++-- .../contrast_enhancement/histogram_matching.rst | 4 ++-- .../contrast_enhancement/matching.jpg | Bin 14950 -> 0 bytes .../contrast_enhancement/matching_out.jpg | Bin 7607 -> 0 bytes 7 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 doc/image_processing/contrast_enhancement/barbara.jpg delete mode 100644 doc/image_processing/contrast_enhancement/church.jpg delete mode 100644 doc/image_processing/contrast_enhancement/he_chart.png delete mode 100644 doc/image_processing/contrast_enhancement/matching.jpg delete mode 100644 doc/image_processing/contrast_enhancement/matching_out.jpg diff --git a/doc/image_processing/contrast_enhancement/barbara.jpg b/doc/image_processing/contrast_enhancement/barbara.jpg deleted file mode 100644 index 3dad19747e21279788ebcd70df7c3207482715eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24897 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 diff --git a/doc/image_processing/contrast_enhancement/church.jpg b/doc/image_processing/contrast_enhancement/church.jpg deleted file mode 100644 index c0beb7c651a51c87fa91d265cf94b41bd2cb72fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34771 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%0Dn3>Q_=du7oL8mhQphw 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 LKPoBc7eD{m?VOo` diff --git a/doc/image_processing/contrast_enhancement/he_chart.png b/doc/image_processing/contrast_enhancement/he_chart.png deleted file mode 100644 index 217c89ef1565ebdae0e03f2044aa931d954a35ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14693 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 diff --git a/doc/image_processing/contrast_enhancement/histogram_equalization.rst b/doc/image_processing/contrast_enhancement/histogram_equalization.rst index 0874c1c4be..2a52118c66 100644 --- a/doc/image_processing/contrast_enhancement/histogram_equalization.rst +++ b/doc/image_processing/contrast_enhancement/histogram_equalization.rst @@ -61,7 +61,7 @@ The algorithm is applied on a few standard images. One of the transformations in **Grayscale Image** -.. figure:: barbara.jpg +.. figure:: https://github.com/boost-gil/test-images/blob/master/jpeg/suite/barbara.jpg :width: 512px :align: center :height: 256px @@ -70,7 +70,7 @@ The algorithm is applied on a few standard images. One of the transformations in **RGB** -.. figure:: church.jpg +.. figure:: https://github.com/boost-gil/test-images/blob/master/jpeg/suite/church.jpg :width: 900px :align: center :height: 300px diff --git a/doc/image_processing/contrast_enhancement/histogram_matching.rst b/doc/image_processing/contrast_enhancement/histogram_matching.rst index be7883a0c9..0f89a9420c 100644 --- a/doc/image_processing/contrast_enhancement/histogram_matching.rst +++ b/doc/image_processing/contrast_enhancement/histogram_matching.rst @@ -47,7 +47,7 @@ The algorithm is applied on a few standard images. One of the transformations in **Original Image(left) & Reference Image(right)** -.. figure:: matching.jpg +.. figure:: https://github.com/boost-gil/test-images/blob/master/jpeg/suite/matching.jpg :width: 600px :align: center :height: 300px @@ -56,7 +56,7 @@ The algorithm is applied on a few standard images. One of the transformations in **Histogram matched Image** -.. figure:: matching_out.jpg +.. figure:: https://github.com/boost-gil/test-images/blob/master/jpeg/suite/matching_out.jpg :width: 300px :align: center :height: 300px diff --git a/doc/image_processing/contrast_enhancement/matching.jpg b/doc/image_processing/contrast_enhancement/matching.jpg deleted file mode 100644 index d03e07a0404975ff535f89ccfc1757869ae83e56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14950 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 diff --git a/doc/image_processing/contrast_enhancement/matching_out.jpg b/doc/image_processing/contrast_enhancement/matching_out.jpg deleted file mode 100644 index 869043accf89bd69a4a07fd75c81be676121295f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7607 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 x#J|?b-aYJtUE!DC$9t6wf0rp-PDf7a-bm&q< Date: Mon, 29 Mar 2021 00:30:12 +0530 Subject: [PATCH 48/51] Add rasterizer support for ellipse (#585) * Added all standard morphological transformations * Should handle grayscale dilation/erosion * Added test cases and improved code structure * Should handle multichannel images --- example/Jamfile | 1 + example/rasterizer_ellipse.cpp | 44 +++++ include/boost/gil.hpp | 1 + include/boost/gil/rasterization/ellipse.hpp | 192 ++++++++++++++++++++ test/core/rasterization/CMakeLists.txt | 3 +- test/core/rasterization/Jamfile | 1 + test/core/rasterization/ellipse.cpp | 83 +++++++++ 7 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 example/rasterizer_ellipse.cpp create mode 100644 include/boost/gil/rasterization/ellipse.hpp create mode 100644 test/core/rasterization/ellipse.cpp diff --git a/example/Jamfile b/example/Jamfile index df17d57c42..3ce548cff5 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -32,6 +32,7 @@ local sources = mandelbrot.cpp morphology.cpp packed_pixel.cpp + rasterizer_ellipse.cpp resize.cpp sobel_scharr.cpp threshold.cpp diff --git a/example/rasterizer_ellipse.cpp b/example/rasterizer_ellipse.cpp new file mode 100644 index 0000000000..461103ac9e --- /dev/null +++ b/example/rasterizer_ellipse.cpp @@ -0,0 +1,44 @@ +// +// Copyright 2021 Prathamesh Tagore +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include + +namespace gil = boost::gil; + +int main() +{ + // Syntax for usage :- + // auto rasterizer = gil::midpoint_elliptical_rasterizer{}; + // rasterizer(img_view, colour, center, semi-axes_length); + // Where + // img_view : gil view of the image on which ellipse is to be drawn. + // colour : Vector containing channel intensity values for img_view. Number of colours + // provided must be equal to the number of channels present in img_view. + // center : Array containing positive integer x co-ordinate and y co-ordinate of the center + // respectively. + // semi-axes_length : Array containing positive integer lengths of horizontal semi-axis + // and vertical semi-axis respectively. + + gil::gray8_image_t gray_buffer_image(256, 256); + auto gray_elliptical_rasterizer = gil::midpoint_elliptical_rasterizer{}; + gray_elliptical_rasterizer(view(gray_buffer_image), {128}, {128, 128}, {100, 50}); + + gil::rgb8_image_t rgb_buffer_image(256, 256); + auto rgb_elliptical_rasterizer = gil::midpoint_elliptical_rasterizer{}; + rgb_elliptical_rasterizer(view(rgb_buffer_image), {0, 0, 255}, {128, 128}, {50, 100}); + + gil::rgb8_image_t rgb_buffer_image_out_of_bound(256, 256); + auto rgb_elliptical_rasterizer_out_of_bound = gil::midpoint_elliptical_rasterizer{}; + rgb_elliptical_rasterizer_out_of_bound(view(rgb_buffer_image_out_of_bound), {255, 0, 0}, + {100, 100}, {160, 160}); + + gil::write_view("rasterized_ellipse_gray.jpg", view(gray_buffer_image), gil::jpeg_tag{}); + gil::write_view("rasterized_ellipse_rgb.jpg", view(rgb_buffer_image), gil::jpeg_tag{}); + gil::write_view("rasterized_ellipse_rgb_out_of_bound.jpg", view(rgb_buffer_image_out_of_bound), + gil::jpeg_tag{}); +} diff --git a/include/boost/gil.hpp b/include/boost/gil.hpp index b98af56e6a..0df6b59fb5 100644 --- a/include/boost/gil.hpp +++ b/include/boost/gil.hpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/gil/rasterization/ellipse.hpp b/include/boost/gil/rasterization/ellipse.hpp new file mode 100644 index 0000000000..43e51e8147 --- /dev/null +++ b/include/boost/gil/rasterization/ellipse.hpp @@ -0,0 +1,192 @@ +// +// Copyright 2021 Prathamesh Tagore +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#ifndef BOOST_GIL_RASTERIZATION_ELLIPSE_HPP +#define BOOST_GIL_RASTERIZATION_ELLIPSE_HPP + +#include +#include +#include + +namespace boost { namespace gil { + +/// \defgroup EllipseRasterization +/// \ingroup Rasterization +/// \brief Ellipse rasterization algorithms. + +/// \ingroup EllipseRasterization +/// \brief Performs ellipse rasterization using midpoint algorithm. Initially, program considers +/// origin as center of ellipse and obtains first quadrant trajectory points. After that, +/// it shifts origin to provided co-ordinates of center and then draws the curve. +struct midpoint_elliptical_rasterizer +{ + /// \brief Returns a vector containing co-ordinates of first quadrant points which lie on + /// rasterizer trajectory of the ellipse. + /// \param semi_axes - Array containing half of lengths of horizontal and vertical axis + /// respectively. + auto obtain_trajectory(std::array const semi_axes) + -> std::vector> + { + // Citation : J. Van Aken, "An Efficient Ellipse-Drawing Algorithm" in IEEE Computer + // Graphics and Applications, vol. 4, no. 09, pp. 24-35, 1984. + // doi: 10.1109/MCG.1984.275994 + // keywords: {null} + // url: https://doi.ieeecomputersociety.org/10.1109/MCG.1984.275994 + std::vector> trajectory_points; + std::ptrdiff_t x = semi_axes[0], y = 0; + + // Variables declared on following lines are temporary variables used for improving + // performance since they help in converting all multiplicative operations inside the while + // loop into additive/subtractive operations. + long long int const t1 = semi_axes[0] * semi_axes[0]; + long long int const t4 = semi_axes[1] * semi_axes[1]; + long long int t2, t3, t5, t6, t8, t9; + t2 = 2 * t1, t3 = 2 * t2; + t5 = 2 * t4, t6 = 2 * t5; + long long int const t7 = semi_axes[0] * t5; + t8 = 2 * t7, t9 = 0; + + // Following variables serve as decision parameters and help in choosing the right point + // to be included in rasterizer trajectory. + long long int d1, d2; + d1 = t2 - t7 + t4 / 2, d2 = t1 / 2 - t8 + t5; + + while (d2 < 0) + { + trajectory_points.push_back({x, y}); + y += 1; + t9 += t3; + if (d1 < 0) + { + d1 += t9 + t2; + d2 += t9; + } + else + { + x -= 1; + t8 -= t6; + d1 += t9 + t2 - t8; + d2 += t5 + t9 - t8; + } + } + while (x >= 0) + { + trajectory_points.push_back({x, y}); + x -= 1; + t8 -= t6; + if (d2 < 0) + { + y += 1; + t9 += t3; + d2 += t5 + t9 - t8; + } + else + { + d2 += t5 - t8; + } + } + return trajectory_points; + } + + /// \brief Fills pixels returned by function 'obtain_trajectory' as well as pixels + /// obtained from their reflection along major axis, minor axis and line passing through + /// center with slope -1 using colours provided by user. + /// \param view - Gil view of image on which the elliptical curve is to be drawn. + /// \param colour - Constant vector specifying colour intensity values for all channels present + /// in 'view'. + /// \param center - Constant array specifying co-ordinates of center of ellipse to be drawn. + /// \param trajectory_points - Constant vector specifying pixel co-ordinates of points lying + /// on rasterizer trajectory. + /// \tparam View - Type of input image view. + template + void draw_curve(View view, std::vector const colour, + std::array const center, + std::vector> const trajectory_points) + { + for (int i = 0, colour_index = 0; i < static_cast(view.num_channels()); + ++i, ++colour_index) + { + for (std::array point : trajectory_points) + { + std::array co_ords = {center[0] + point[0], + center[0] - point[0], center[1] + point[1], center[1] - point[1] + }; + bool validity[4] = {0}; + if (co_ords[0] < view.width()) + { + validity[0] = 1; + } + if (co_ords[1] >= 0 && co_ords[1] < view.width()) + { + validity[1] = 1; + } + if (co_ords[2] < view.height()) + { + validity[2] = 1; + } + if (co_ords[3] >= 0 && co_ords[3] < view.height()) + { + validity[3] = 1; + } + if (validity[0] && validity[2]) + { + nth_channel_view(view, i)(co_ords[0], co_ords[2])[0] = colour[colour_index]; + } + if (validity[1] && validity[2]) + { + nth_channel_view(view, i)(co_ords[1], co_ords[2])[0] = colour[colour_index]; + } + if (validity[1] && validity[3]) + { + nth_channel_view(view, i)(co_ords[1], co_ords[3])[0] = colour[colour_index]; + } + if (validity[0] && validity[3]) + { + nth_channel_view(view, i)(co_ords[0], co_ords[3])[0] = colour[colour_index]; + } + } + } + } + + /// \brief Calls the function 'obtain_trajectory' and then passes obtained trajectory points + /// in the function 'draw_curve' for drawing the desired ellipse. + /// \param view - Gil view of image on which the elliptical curve is to be drawn. + /// \param colour - Constant vector specifying colour intensity values for all channels present + /// in 'view'. + /// \param center - Array containing positive integer x co-ordinate and y co-ordinate of the + /// center respectively. + /// \param semi_axes - Array containing positive integer lengths of horizontal semi-axis + /// and vertical semi-axis respectively. + /// \tparam View - Type of input image view. + template + void operator()(View view, std::vector const colour, + std::array center, std::array const semi_axes) + { + --center[0], --center[1]; // For converting center co-ordinate values to zero based indexing. + if (colour.size() != view.num_channels()) + { + throw std::length_error("Number of channels in given image is not equal to the " + "number of colours provided."); + } + if (center[0] + semi_axes[0] >= view.width() || center[1] + semi_axes[1] >= view.height() + || static_cast(center[0] - semi_axes[0]) < 0 + || static_cast(center[0] - semi_axes[0]) >= view.width() + || static_cast(center[1] - semi_axes[1]) < 0 + || static_cast(center[1] - semi_axes[1]) >= view.height()) + { + std::cout << "Image can't contain whole curve.\n" + "However, it will contain those parts of curve which can fit inside it.\n" + "Note : Image width = " << view.width() << " and Image height = " << + view.height() << "\n"; + } + std::vector> trajectory_points = + obtain_trajectory(semi_axes); + draw_curve(view, colour, center, trajectory_points); + } +}; // midpoint elliptical rasterizer +}} // namespace boost::gil +#endif diff --git a/test/core/rasterization/CMakeLists.txt b/test/core/rasterization/CMakeLists.txt index 2ab6dd2b43..522e743ddf 100644 --- a/test/core/rasterization/CMakeLists.txt +++ b/test/core/rasterization/CMakeLists.txt @@ -8,7 +8,8 @@ # foreach(_name line - circle) + circle + ellipse) set(_test t_core_rasterization_${_name}) set(_target test_core_rasterization_${_name}) diff --git a/test/core/rasterization/Jamfile b/test/core/rasterization/Jamfile index 750eb46697..6be4cd9705 100644 --- a/test/core/rasterization/Jamfile +++ b/test/core/rasterization/Jamfile @@ -10,3 +10,4 @@ import testing ; run line.cpp ; run circle.cpp ; +run ellipse.cpp ; diff --git a/test/core/rasterization/ellipse.cpp b/test/core/rasterization/ellipse.cpp new file mode 100644 index 0000000000..1b9e813136 --- /dev/null +++ b/test/core/rasterization/ellipse.cpp @@ -0,0 +1,83 @@ +// +// Copyright 2021 Prathamesh Tagore +// +// Use, modification and distribution are subject to the Boost Software License, +// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +#include +#include +#include +#include +#include + +namespace gil = boost::gil; + +// This function utilizes the fact that sum of distances of a point on an ellipse from its foci +// is equal to the length of major axis of the ellipse. +// Parameters b and a represent half of lengths of vertical and horizontal axis respectively. +void test_rasterizer_follows_equation( + std::vector> trajectory_points, float a, float b) +{ + float focus_x, focus_y; + if (a > b) // For horizontal ellipse + { + focus_x = a * std::sqrt(1 - b * b / (a * a)); + focus_y = 0; + } + else // For vertical ellipse + { + focus_x = 0; + focus_y = b * std::sqrt(1 - a * a / (b * b)); + } + + for (auto trajectory_point : trajectory_points) + { + // To suppress conversion warnings from compiler + std::array point { + static_cast(trajectory_point[0]), static_cast(trajectory_point[1])}; + + double dist_sum = std::sqrt(std::pow(focus_x - point[0], 2) + + std::pow(focus_y - point[1], 2)) + std::sqrt(std::pow( - focus_x - point[0], 2) + + std::pow( - focus_y - point[1], 2)); + if (a > b) + { + BOOST_TEST(std::abs(dist_sum - 2 * a) < 1); + } + else + { + BOOST_TEST(std::abs(dist_sum - 2 * b) < 1); + } + } +} + +// This function verifies that the difference between x co-ordinates and y co-ordinates for two +// successive trajectory points is less than or equal to 1. This ensures that the curve is connected. +void test_connectivity(std::vector> points) +{ + for (std::size_t i = 1; i < points.size(); ++i) + { + std::ptrdiff_t diff_x = points[i][0] - points[i - 1][0]; + std::ptrdiff_t diff_y = points[i][1] - points[i - 1][1]; + BOOST_TEST_LE(diff_x, 1); + BOOST_TEST_LE(diff_y, 1); + } +} + +// We verify all test cases for the portion of ellipse in first quadrant, since all other portions +// can be constructed with simple reflection, they tend to be correct if first quadrant is verified. +int main() +{ + for (float a = 1; a < 101; ++a) + { + for (float b = 1; b < 101; ++b) + { + auto rasterizer = gil::midpoint_elliptical_rasterizer{}; + std::vector> points = rasterizer.obtain_trajectory( + {static_cast(a), static_cast(b)}); + test_rasterizer_follows_equation(points, a, b); + test_connectivity(points); + } + } + return boost::report_errors(); +} From cea6ef2752cbfc4c8d1991d1d96b9fc3f78d79f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Fri, 2 Apr 2021 15:53:03 +0200 Subject: [PATCH 49/51] ci: Fix availability of toolsets in updated Ubuntu images of GitHub Actions (#589) The `compiler` property should trigger creation of `user-config.jam` with the expected GCC 8 defined. Something has changed on the GitHub Actions images or Boost.Build and GCC 8 build job started failing: /home/runner/work/gil/boost-root/tools/build/src/tools/gcc.jam:203: in gcc.init from module gcc error: toolset gcc initialization: error: version '8' requested but 'g++-8' not found and version '7.5.0' of default 'g++' does not match e.g. https://github.com/boostorg/gil/pull/562/checks?check_run_id=2246393722 The compilers should be `install`-ed explicitly to avoid such issues in future. --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c3f063b6a..0d77b4dc6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,11 @@ jobs: - toolset: gcc-7 cxxstd: "11,14,17" os: ubuntu-18.04 - - toolset: gcc-8 + - toolset: gcc + compiler: g++-8 cxxstd: "11,14,17,2a" os: ubuntu-18.04 + install: g++-8 - toolset: gcc-9 cxxstd: "11,14,17,2a" os: ubuntu-18.04 @@ -76,6 +78,7 @@ jobs: compiler: clang++-6.0 cxxstd: "11,14,17" os: ubuntu-18.04 + install: clang-6.0 - toolset: clang compiler: clang++-7 cxxstd: "11,14,17" @@ -85,6 +88,7 @@ jobs: compiler: clang++-8 cxxstd: "11,14,17,2a" os: ubuntu-20.04 + install: clang-8 - toolset: clang compiler: clang++-9 cxxstd: "11,14,17,2a" @@ -123,6 +127,7 @@ jobs: if: matrix.compiler run: | echo "using ${{matrix.toolset}} : : ${{matrix.compiler}} ;" > ~/user-config.jam + cat ~/user-config.jam - name: Run tests if: "!matrix.define" From 2ad274cc81f1613a0dc723a6c87a4c48dea9e35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Fri, 2 Apr 2021 20:35:17 +0200 Subject: [PATCH 50/51] ci: Remove cxxstd=14 from clang 3.5 job on GitHub Actions (#590) Despite https://clang.llvm.org/cxx_status.html saying: Clang 3.4 and later implement all of the ISO C++ 2014 standard. The clang 3.5 suffers from the bug in C++14 mode causing failure of Boost.Filesystem build with: error: debug information for auto is not yet supported error: debug information for auto is not yet supported https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=800483 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d77b4dc6b..1e6c356e4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: os: ubuntu-18.04 - toolset: clang compiler: clang++-3.5 - cxxstd: "11,14" + cxxstd: "11" define: "_GLIBCXX_USE_CXX11_ABI=0" os: ubuntu-16.04 install: clang-3.5 From bbdce36a1f655ebf8332b775790673b197c8f1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Schr=C3=B6der?= Date: Sat, 3 Apr 2021 00:46:05 +0200 Subject: [PATCH 51/51] docs: Correct typos in documentation (#592) --- doc/design/color_space.rst | 4 ++-- doc/design/concepts.rst | 2 +- doc/design/point.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/design/color_space.rst b/doc/design/color_space.rst index 413f09c703..e469c4fa02 100644 --- a/doc/design/color_space.rst +++ b/doc/design/color_space.rst @@ -45,7 +45,7 @@ Besides the standard layouts, it also provides: - ``abgr_layout_t`` - ``argb_layout_t`` -As an example, here is how GIL defines the RGBA color space:: +As an example, here is how GIL defines the RGBA color space: .. code-block:: cpp @@ -58,7 +58,7 @@ As an example, here is how GIL defines the RGBA color space:: The ordering of the channels in the color space definition specifies their semantic order. For example, ``red_t`` is the first semantic channel of ``rgba_t``. While there is a unique semantic ordering of the channels in a -color space, channels may vary in their physical ordering in memory +color space, channels may vary in their physical ordering in memory. The mapping of channels is specified by ``ChannelMappingConcept``, which is an MPL random access sequence of integral types. diff --git a/doc/design/concepts.rst b/doc/design/concepts.rst index 47faa7e5d5..7aac361380 100644 --- a/doc/design/concepts.rst +++ b/doc/design/concepts.rst @@ -8,7 +8,7 @@ algorithmic guarantees. For example, GIL class ``pixel`` is a model of GIL ``PixelConcept``. The user may substitute the pixel class with one of their own, and, as long as it satisfies the requirements of ``PixelConcept``, all other GIL classes and algorithms can be used with it. -See more about concepts is avaialble at +See more about concepts at `Generic Programming in ConceptC++ `_ In this document we will use a syntax for defining concepts that is described diff --git a/doc/design/point.rst b/doc/design/point.rst index 035771b50b..e32bca936e 100644 --- a/doc/design/point.rst +++ b/doc/design/point.rst @@ -39,7 +39,7 @@ in which both dimensions are of the same type: typename value_type = axis<0>::type; const value_type& operator[](const T&, size_t i); - value_type& operator[]( T&, size_t i); + value_type& operator[]( T&, size_t i); value_type x,y; };