diff --git a/example/sharpening.cpp b/example/sharpening.cpp new file mode 100644 index 0000000000..e98ce9d1c1 --- /dev/null +++ b/example/sharpening.cpp @@ -0,0 +1,27 @@ +// +// Copyright 2021 Harsit Pant +// +// 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; +using namespace std; +int main() +{ + gray8_image_t img_in; + read_image("test_adaptive.png", img_in, png_tag{}); + gray8_image_t img_out(img_in.dimensions()); + + sharpen(view(img_in), view(img_out), 1, 3); + write_view("sharpened image.png", view(img_out), png_tag{}); + + cout << "done"; + cin.get(); + return 0; +} diff --git a/include/boost/gil/image_processing/sharpening.hpp b/include/boost/gil/image_processing/sharpening.hpp new file mode 100644 index 0000000000..b244caf8a6 --- /dev/null +++ b/include/boost/gil/image_processing/sharpening.hpp @@ -0,0 +1,189 @@ +// +// Copyright 2021 Harsit Pant +// +// 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_SHARPENING_HPP +#define BOOST_GIL_IMAGE_PROCESSING_SHARPENING_HPP + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace boost { namespace gil { +namespace detail{ + +/// \breif USM algorithm might sharpen noise (perceiving noise as edges), to control this +/// threshold is specified, thus sharpening only high frequency regions (strong edges). +/// For each pixel I(u,v), if (local_contrast(u, v) < minimum local contrast), put I(u, v) = 0. +template +void threshold_mask(View const& highpass_filtered_image, double threshold) +{ + float32_t max_absolute_contrast = 0.0; + for_each_pixel(nth_channel_view(highpass_filtered_image, 0), + [&max_absolute_contrast](auto& pixel) { + if (max_absolute_contrast < std::abs(pixel[0])) + max_absolute_contrast = std::abs(pixel[0]); }); + + float32_t const minimum_local_contrat = max_absolute_contrast * static_cast(threshold); + + for_each_pixel(nth_channel_view(highpass_filtered_image, 0), + [&minimum_local_contrat](auto& pixel) { + if (std::abs(pixel[0]) < minimum_local_contrat) + pixel[0] = 0; }); +} + +/// \breif clamps lab colors out of gamut for RGB color space. +template +inline void clip_lab_color(SrcView& float32_output_image) +{ + rgb32f_pixel_t rgb32f_temp_pixel; + + for_each_pixel(float32_output_image, [&rgb32f_temp_pixel](auto& input_pixel) { + + color_convert(input_pixel, rgb32f_temp_pixel); + + if (rgb32f_temp_pixel[0] > 1) rgb32f_temp_pixel[0] = 1; + if (rgb32f_temp_pixel[0] < 0) rgb32f_temp_pixel[0] = 0; + if (rgb32f_temp_pixel[1] > 1) rgb32f_temp_pixel[1] = 1; + if (rgb32f_temp_pixel[1] < 0) rgb32f_temp_pixel[1] = 0; + if (rgb32f_temp_pixel[2] > 1) rgb32f_temp_pixel[2] = 1; + if (rgb32f_temp_pixel[2] < 0) rgb32f_temp_pixel[2] = 0; + + color_convert(rgb32f_temp_pixel, input_pixel); }); +} + +/// TODO - Replace use of this function with convolve_2d when, +/// it starts supporting boundary_option::extend_constant. +template +void convolve_2d_extend_constant(SrcView const& src_view, DstView const& dst_view, + Kernel const& kernel) +{ + auto padded_image = extend_boundary(src_view, kernel.size() / 2, + boundary_option::extend_constant); + + decltype(padded_image) convolved_image(padded_image.dimensions()); + + detail::convolve_2d(view(padded_image), kernel, view(convolved_image)); + + copy_pixels(subimage_view(view(convolved_image), + point_t(kernel.size() / 2, kernel.size() / 2), + src_view.dimensions()), dst_view); +} +} // namespace detail + +/// \addtogroup ImageProcessing +/// @{ +/// \breif Sharpens grayscale/RGB images using unsharp masking technique. +/// +/// For each pixel I(u, v) - +/// if(local_contrast(u, v) > minimum local constrast) +/// I(u, v) = I(u, v) + amount * (I(u, v) - I'(u, v)) +/// else +/// I(u, v) = I (u, v) +/// where, minimum local contrast is specified using the parameter threshold. +/// I' is a smoother version of I, made by gaussian bluring. +/// +/// RGB color images are converted to lab color space, USM algorithm is applied to L* channel +/// (a*and b* channels remain unchanged), then lab image is converted back to RGB color space. +/// +/// Reference - Principles of Digital Image Processing - Fundamental Techniques +/// by Wilhelm Burger, Mark J.Burge. +/// +/// \param src_view - Source image view. +/// \param dst_view - Destination image view. +/// \param sigma - Standard deviation for generating gaussian kernel. +/// \param amount - Sharpening amount or weight. +/// \param threshold - Minimum local contrast for each pixel, +/// specified in the range 0 to 1, to apply sharpening. +/// \tparam SrcView - Source image view type. +/// \tparam DstView - Destination image view type. +template +void sharpen(SrcView const& src_view, DstView const& dst_view, double sigma = 1, + double amount = 1, double threshold = 0.0) +{ + gil_function_requires>(); + gil_function_requires>(); + + using SrcColorSpace = typename color_space_type::type; + using DstColorSpace = typename color_space_type::type; + + static_assert(color_spaces_are_compatible::value, + "Source and destination views must have same color space."); + + static_assert(is_same::value || + is_same::value, + "Incompatible Color space used."); + + BOOST_ASSERT(src_view.dimensions() == dst_view.dimensions()); + + int const kernel_size = static_cast(std::ceil(sigma) * 6 + 1); + detail::kernel_2d highpass_kernel = generate_gaussian_kernel + (kernel_size, static_cast(sigma)); + + //highpass kernel = impulse kernel - lowpass kernel. Produces the mask. + for_each(highpass_kernel.begin(), highpass_kernel.end(), [](float& x) { x *= -1; }); + highpass_kernel[kernel_size / 2 * (kernel_size + 1)] += 1; + + typename std::conditional< + is_same::value, + lab32f_image_t, gray32f_image_t>::type float32_input_image(src_view.dimensions()), + float32_output_image(src_view.dimensions()); + + copy_and_convert_pixels(src_view, view(float32_input_image)); + copy_and_convert_pixels(src_view, view(float32_output_image)); + + detail::convolve_2d_extend_constant(nth_channel_view(view(float32_input_image), 0), + nth_channel_view(view(float32_output_image), 0), + highpass_kernel); + + if (threshold > 0.0f) + { + detail::threshold_mask(view(float32_output_image), threshold); + } + + transform_pixels(view(float32_input_image), + view(float32_output_image), + view(float32_output_image), [&amount](auto orignal, auto mask) { + decltype(orignal) temp(orignal); + temp[0] += mask[0] * + static_cast(amount); + return temp; }); + + float32_t channel_max = (is_same::value) ? 100 : 1; + float32_t channel_min = 0; + for_each_pixel(nth_channel_view(view(float32_output_image), 0), + [&channel_max, &channel_min](auto& pixel) { + if (pixel[0] > channel_max) + pixel[0] = channel_max; + if (pixel[0] < channel_min) + pixel[0] = channel_min; }); + + if (is_same::value) + { + clip_lab_color(view(float32_output_image)); + } + + copy_and_convert_pixels(view(float32_output_image), dst_view); +} +/// @} + +}} //namespace boost::gil +#endif //BOOST_GIL_IMAGE_PROCESSING_SHARPENING_HPP diff --git a/test/core/image_processing/CMakeLists.txt b/test/core/image_processing/CMakeLists.txt index 1581fd3075..3f4154aaa9 100644 --- a/test/core/image_processing/CMakeLists.txt +++ b/test/core/image_processing/CMakeLists.txt @@ -10,7 +10,8 @@ foreach(_name threshold_binary threshold_truncate threshold_otsu - morphology) + morphology + sharpening) 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 acd74d08f2..2476377c51 100644 --- a/test/core/image_processing/Jamfile +++ b/test/core/image_processing/Jamfile @@ -23,3 +23,4 @@ run anisotropic_diffusion.cpp ; run hough_line_transform.cpp ; run hough_circle_transform.cpp ; run morphology.cpp ; +run sharpening.cpp ; diff --git a/test/core/image_processing/sharpening.cpp b/test/core/image_processing/sharpening.cpp new file mode 100644 index 0000000000..f5f149c05b --- /dev/null +++ b/test/core/image_processing/sharpening.cpp @@ -0,0 +1,93 @@ +// +// Copyright 2021 Harsit Pant +// +// 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; +using namespace gil; +using namespace std; + +int const width = 9; +int const height = 9; + +gray8_image_t gray_img_in(width, height); +gray8_image_t gray_img_out(width, height); +gray8_image_t gray_img_expected(width, height); + +rgb8_image_t rgb_img_in(width, height); +rgb8_image_t rgb_img_out(width, height); +rgb8_image_t rgb_img_expected(width, height); + +void no_edges() +{ + //all pixels same, no edge detected, thus nothing to sharpen. + fill_pixels(view(gray_img_in), gray8_pixel_t(50)); + fill_pixels(view(gray_img_expected), gray8_pixel_t(50)); + + sharpen(const_view(gray_img_in), view(gray_img_out), 2, 1, 0.5); + + BOOST_TEST(equal_pixels(const_view(gray_img_out), const_view(gray_img_expected))); +} + +void no_edges_rgb() +{ + //all pixels same, no edge detected, thus nothing to sharpen. + fill_pixels(view(rgb_img_in), rgb8_pixel_t(50, 50, 50)); + fill_pixels(view(rgb_img_expected), rgb8_pixel_t(50, 50, 50)); + + sharpen(const_view(rgb_img_in), view(rgb_img_out), 1, 3, 0.1); + + BOOST_TEST(equal_pixels(const_view(rgb_img_out), const_view(rgb_img_expected))); +} + +void amount_zero() +{ + // image remains unchanged if specified ammount is zero. + uint8_t in[] = + { + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53 + }; + + uint8_t out[] = + { + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53, + 12, 32, 224, 53, 255, 55, 32, 224, 53 + }; + + gray8c_view_t in_view = interleaved_view(9, 9, reinterpret_cast(in), 9); + + gray8c_view_t exp_view = interleaved_view(9, 9, reinterpret_cast(out), 9); + + sharpen(in_view, view(gray_img_out), 1, 0); + BOOST_TEST(equal_pixels(const_view(gray_img_out), exp_view)); +} +int main() +{ + no_edges(); + no_edges_rgb(); + amount_zero(); + return ::boost::report_errors(); +}