This is a follow-up question for conv2 Template Function Implementation for Image in C++ and An Updated Multi-dimensional Image Data Structure with Variadic Template Functions in C++. For performing the convolution in double
format, both im2double
and im2uint8
functions (just like Matlab's im2double
and im2uint8
) are implemented in this post.
The experimental implementation
im2double
function implementation (in fileimage_operations.h
)namespace TinyDIP { // im2double function implementation constexpr static auto im2double(Image<RGB> input) { auto image_data = input.getImageData(); std::vector<RGB_DOUBLE> new_data; for (size_t index = 0; index < input.count(); ++index) { RGB_DOUBLE rgb_double { static_cast<double>(image_data[index].channels[0]), static_cast<double>(image_data[index].channels[1]), static_cast<double>(image_data[index].channels[2])}; new_data.emplace_back(rgb_double); } Image<RGB_DOUBLE> output(new_data, input.getSize()); return output; } }
im2uint8
function implementation (in fileimage_operations.h
)namespace TinyDIP { // im2uint8 function implementation constexpr static auto im2uint8(Image<RGB_DOUBLE> input) { auto image_data = input.getImageData(); std::vector<RGB> new_data; for (size_t index = 0; index < input.count(); ++index) { RGB rgb { static_cast<std::uint8_t>(image_data[index].channels[0]), static_cast<std::uint8_t>(image_data[index].channels[1]), static_cast<std::uint8_t>(image_data[index].channels[2])}; new_data.emplace_back(rgb); } Image<RGB> output(new_data, input.getSize()); return output; } }
RGB_DOUBLE
struct (in filebase_types.h
)struct RGB_DOUBLE { double channels[3]; };
The updated
conv2
template function implementation (in fileimage_operations.h
)namespace TinyDIP { // conv2 template function implementation template<typename ElementT> requires(std::floating_point<ElementT> || std::integral<ElementT> || is_complex<ElementT>::value) constexpr auto conv2(const Image<ElementT>& x, const Image<ElementT>& y, bool is_size_same = false) { auto output = Image<ElementT>(x.getWidth() + y.getWidth() - 1, x.getHeight() + y.getHeight() - 1); for (std::size_t y1 = 0; y1 < x.getHeight(); ++y1) { auto* x_row = &(x.at(0, y1)); for (std::size_t y2 = 0; y2 < y.getHeight(); ++y2) { auto* y_row = &(y.at(0, y2)); auto* out_row = &(output.at(0, y1 + y2)); for (std::size_t x1 = 0; x1 < x.getWidth(); ++x1) { for (std::size_t x2 = 0; x2 < y.getWidth(); ++x2) { out_row[x1 + x2] += x_row[x1] * y_row[x2]; } } } } if(is_size_same) { output = subimage(output, x.getWidth(), x.getHeight(), static_cast<double>(output.getWidth()) / 2.0, static_cast<double>(output.getHeight()) / 2.0); } return output; } // conv2 template function implementation template<typename ElementT, typename ElementT2> requires (((std::same_as<ElementT, RGB>) || (std::same_as<ElementT, RGB_DOUBLE>) || (std::same_as<ElementT, HSV>)) && (std::floating_point<ElementT2> || std::integral<ElementT2> || is_complex<ElementT2>::value)) constexpr static auto conv2(const Image<ElementT>& input1, const Image<ElementT2>& input2, bool is_size_same = false) { return apply_each(input1, [&](auto&& planes) { return conv2(planes, input2, is_size_same); }); } }
is_complex
struct implementation (in filebasic_functions.h
)namespace TinyDIP { // Reference: https://stackoverflow.com/a/64287611/6667035 template <typename T> struct is_complex : std::false_type {}; template <typename T> struct is_complex<std::complex<T>> : std::true_type {}; }
apply_each
template function implementation (in fileimage_operations.h
)namespace TinyDIP { // apply_each template function implementation template<class F, class... Args> constexpr static auto apply_each(Image<RGB> input, F operation, Args&&... args) { return constructRGB(operation(getRplane(input), args...), operation(getGplane(input), args...), operation(getBplane(input), args...)); } // apply_each template function implementation template<class F, class... Args> constexpr static auto apply_each(Image<RGB_DOUBLE> input, F operation, Args&&... args) { return constructRGBDOUBLE(operation(getRplane(input), args...), operation(getGplane(input), args...), operation(getBplane(input), args...)); } // apply_each template function implementation template<class F, class... Args> constexpr static auto apply_each(Image<HSV> input, F operation, Args&&... args) { return constructHSV(operation(getHplane(input), args...), operation(getSplane(input), args...), operation(getVplane(input), args...)); } }
constructRGB
template function implementation (in fileimage_operations.h
)namespace TinyDIP { // constructRGB template function implementation template<typename OutputT = RGB> constexpr static auto constructRGB(Image<GrayScale> r, Image<GrayScale> g, Image<GrayScale> b) { check_size_same(r, g); check_size_same(g, b); auto image_data_r = r.getImageData(); auto image_data_g = g.getImageData(); auto image_data_b = b.getImageData(); std::vector<OutputT> new_data; for (size_t index = 0; index < r.count(); ++index) { OutputT rgb { image_data_r[index], image_data_g[index], image_data_b[index]}; new_data.emplace_back(rgb); } Image<OutputT> output(new_data, r.getSize()); return output; } }
constructRGBDOUBLE
template function implementation (in fileimage_operations.h
)namespace TinyDIP { // constructRGBDOUBLE template function implementation template<typename OutputT = RGB_DOUBLE> constexpr static auto constructRGBDOUBLE(Image<double> r, Image<double> g, Image<double> b) { check_size_same(r, g); check_size_same(g, b); auto image_data_r = r.getImageData(); auto image_data_g = g.getImageData(); auto image_data_b = b.getImageData(); std::vector<OutputT> new_data; for (size_t index = 0; index < r.count(); ++index) { OutputT rgb_double { image_data_r[index], image_data_g[index], image_data_b[index]}; new_data.emplace_back(rgb_double); } Image<OutputT> output(new_data, r.getSize()); return output; } }
constructHSV
template function implementation (in fileimage_operations.h
)namespace TinyDIP { template<typename OutputT = HSV> constexpr static auto constructHSV(Image<double> h, Image<double> s, Image<double> v) { check_size_same(h, s); check_size_same(s, v); auto image_data_h = h.getImageData(); auto image_data_s = s.getImageData(); auto image_data_v = v.getImageData(); std::vector<OutputT> new_data; for (size_t index = 0; index < h.count(); ++index) { OutputT hsv { image_data_h[index], image_data_s[index], image_data_v[index]}; new_data.emplace_back(hsv); } Image<OutputT> output(new_data, h.getSize()); return output; } }
Image
class implementation (in fileimage.h
)namespace TinyDIP { template <typename ElementT> class Image { public: Image() = default; template<std::same_as<std::size_t>... Sizes> Image(Sizes... sizes): size{sizes...}, image_data((1 * ... * sizes)) {} template<std::same_as<int>... Sizes> Image(Sizes... sizes) { size.reserve(sizeof...(sizes)); (size.push_back(sizes), ...); image_data.resize( std::reduce( std::ranges::cbegin(size), std::ranges::cend(size), std::size_t{1}, std::multiplies<>() ) ); } template<std::ranges::input_range Range, std::same_as<std::size_t>... Sizes> Image(const Range& input, Sizes... sizes): size{sizes...}, image_data(begin(input), end(input)) { if (image_data.size() != (1 * ... * sizes)) { throw std::runtime_error("Image data input and the given size are mismatched!"); } } template<std::same_as<std::size_t>... Sizes> Image(std::vector<ElementT>&& input, Sizes... sizes): size{sizes...}, image_data(begin(input), end(input)) { if (image_data.size() != (1 * ... * sizes)) { throw std::runtime_error("Image data input and the given size are mismatched!"); } } Image(std::vector<ElementT>& input, std::vector<std::size_t> sizes): size{sizes}, image_data(begin(input), end(input)) { } Image(std::vector<ElementT>& input, std::size_t newWidth, std::size_t newHeight) { size.reserve(2); size.emplace_back(newWidth); size.emplace_back(newHeight); if (input.size() != newWidth * newHeight) { throw std::runtime_error("Image data input and the given size are mismatched!"); } image_data = std::move(input); // Reference: https://stackoverflow.com/a/51706522/6667035 } Image(const std::vector<std::vector<ElementT>>& input) { size.reserve(2); size.emplace_back(input[0].size()); size.emplace_back(input.size()); for (auto& rows : input) { image_data.insert(image_data.end(), std::ranges::begin(input), std::ranges::end(input)); // flatten } return; } // at template function implementation template<typename... Args> constexpr ElementT& at(const Args... indexInput) { return const_cast<ElementT&>(static_cast<const Image &>(*this).at(indexInput...)); } // at template function implementation // Reference: https://codereview.stackexchange.com/a/288736/231235 template<typename... Args> constexpr ElementT const& at(const Args... indexInput) const { checkBoundary(indexInput...); constexpr std::size_t n = sizeof...(Args); if(n != size.size()) { throw std::runtime_error("Dimensionality mismatched!"); } std::size_t i = 0; std::size_t stride = 1; std::size_t position = 0; auto update_position = [&](auto index) { position += index * stride; stride *= size[i++]; }; (update_position(indexInput), ...); return image_data[position]; } // at_without_boundary_check template function implementation template<typename... Args> constexpr ElementT& at_without_boundary_check(const Args... indexInput) { return const_cast<ElementT&>(static_cast<const Image &>(*this).at_without_boundary_check(indexInput...)); } template<typename... Args> constexpr ElementT const& at_without_boundary_check(const Args... indexInput) const { std::size_t i = 0; std::size_t stride = 1; std::size_t position = 0; auto update_position = [&](auto index) { position += index * stride; stride *= size[i++]; }; (update_position(indexInput), ...); return image_data[position]; } // cast template function implementation template<typename TargetT> constexpr Image<TargetT> cast() { std::vector<TargetT> output_data; output_data.resize(image_data.size()); std::transform( std::ranges::cbegin(image_data), std::ranges::cend(image_data), std::ranges::begin(output_data), [](auto& input){ return static_cast<TargetT>(input); } ); Image<TargetT> output(output_data, size); return output; } constexpr std::size_t count() const noexcept { return std::reduce(std::ranges::cbegin(size), std::ranges::cend(size), 1, std::multiplies()); } constexpr std::size_t getDimensionality() const noexcept { return size.size(); } constexpr std::size_t getWidth() const noexcept { return size[0]; } constexpr std::size_t getHeight() const noexcept { return size[1]; } // getSize function implementation constexpr auto getSize() const noexcept { return size; } std::vector<ElementT> const& getImageData() const noexcept { return image_data; } // expose the internal data void print(std::string separator = "\t", std::ostream& os = std::cout) const { if(size.size() == 1) { for(std::size_t x = 0; x < size[0]; ++x) { // Ref: https://isocpp.org/wiki/faq/input-output#print-char-or-ptr-as-number os << +at(x) << separator; } os << "\n"; } else if(size.size() == 2) { for (std::size_t y = 0; y < size[1]; ++y) { for (std::size_t x = 0; x < size[0]; ++x) { // Ref: https://isocpp.org/wiki/faq/input-output#print-char-or-ptr-as-number os << +at(x, y) << separator; } os << "\n"; } os << "\n"; } else if (size.size() == 3) { for(std::size_t z = 0; z < size[2]; ++z) { for (std::size_t y = 0; y < size[1]; ++y) { for (std::size_t x = 0; x < size[0]; ++x) { // Ref: https://isocpp.org/wiki/faq/input-output#print-char-or-ptr-as-number os << +at(x, y, z) << separator; } os << "\n"; } os << "\n"; } os << "\n"; } } // Enable this function if ElementT = RGB void print(std::string separator = "\t", std::ostream& os = std::cout) const requires(std::same_as<ElementT, RGB>) { for (std::size_t y = 0; y < size[1]; ++y) { for (std::size_t x = 0; x < size[0]; ++x) { os << "( "; for (std::size_t channel_index = 0; channel_index < 3; ++channel_index) { // Ref: https://isocpp.org/wiki/faq/input-output#print-char-or-ptr-as-number os << +at(x, y).channels[channel_index] << separator; } os << ")" << separator; } os << "\n"; } os << "\n"; return; } Image<ElementT>& setAllValue(const ElementT input) { std::fill(std::ranges::begin(image_data), std::ranges::end(image_data), input); return *this; } friend std::ostream& operator<<(std::ostream& os, const Image<ElementT>& rhs) { const std::string separator = "\t"; rhs.print(separator, os); return os; } Image<ElementT>& operator+=(const Image<ElementT>& rhs) { check_size_same(rhs, *this); std::transform(std::ranges::cbegin(image_data), std::ranges::cend(image_data), std::ranges::cbegin(rhs.image_data), std::ranges::begin(image_data), std::plus<>{}); return *this; } Image<ElementT>& operator-=(const Image<ElementT>& rhs) { check_size_same(rhs, *this); std::transform(std::ranges::cbegin(image_data), std::ranges::cend(image_data), std::ranges::cbegin(rhs.image_data), std::ranges::begin(image_data), std::minus<>{}); return *this; } Image<ElementT>& operator*=(const Image<ElementT>& rhs) { check_size_same(rhs, *this); std::transform(std::ranges::cbegin(image_data), std::ranges::cend(image_data), std::ranges::cbegin(rhs.image_data), std::ranges::begin(image_data), std::multiplies<>{}); return *this; } Image<ElementT>& operator/=(const Image<ElementT>& rhs) { check_size_same(rhs, *this); std::transform(std::ranges::cbegin(image_data), std::ranges::cend(image_data), std::ranges::cbegin(rhs.image_data), std::ranges::begin(image_data), std::divides<>{}); return *this; } friend bool operator==(Image<ElementT> const&, Image<ElementT> const&) = default; friend bool operator!=(Image<ElementT> const&, Image<ElementT> const&) = default; friend Image<ElementT> operator+(Image<ElementT> input1, const Image<ElementT>& input2) { return input1 += input2; } friend Image<ElementT> operator-(Image<ElementT> input1, const Image<ElementT>& input2) { return input1 -= input2; } friend Image<ElementT> operator*(Image<ElementT> input1, ElementT input2) { return multiplies(input1, input2); } friend Image<ElementT> operator*(ElementT input1, Image<ElementT> input2) { return multiplies(input2, input1); } #ifdef USE_BOOST_SERIALIZATION void Save(std::string filename) { const std::string filename_with_extension = filename + ".dat"; // Reference: https://stackoverflow.com/questions/523872/how-do-you-serialize-an-object-in-c std::ofstream ofs(filename_with_extension, std::ios::binary); boost::archive::binary_oarchive ArchiveOut(ofs); // write class instance to archive ArchiveOut << *this; // archive and stream closed when destructors are called ofs.close(); } #endif private: std::vector<std::size_t> size; std::vector<ElementT> image_data; template<typename... Args> void checkBoundary(const Args... indexInput) const { constexpr std::size_t n = sizeof...(Args); if(n != size.size()) { throw std::runtime_error("Dimensionality mismatched!"); } std::size_t parameter_pack_index = 0; auto function = [&](auto index) { if (index >= size[parameter_pack_index]) throw std::out_of_range("Given index out of range!"); parameter_pack_index = parameter_pack_index + 1; }; (function(indexInput), ...); } #ifdef USE_BOOST_SERIALIZATION friend class boost::serialization::access; template<class Archive> void serialize(Archive& ar, const unsigned int version) { ar& size; ar& image_data; } #endif }; }
The usage of im2double
, im2uint8
and conv2
functions:
int main()
{
auto start = std::chrono::system_clock::now();
auto image1 = TinyDIP::bmp_read("InputImages/1", false);
auto double_image = TinyDIP::im2double(image1);
std::size_t mask_size = 5;
std::vector<double> mask_data;
for (std::size_t i = 0; i < mask_size * mask_size; ++i)
{
mask_data.emplace_back(1.0 / (static_cast<double>(mask_size) * static_cast<double>(mask_size)));
}
auto mask = TinyDIP::Image<double>(mask_data, mask_size, mask_size);
auto output_image = TinyDIP::conv2(TinyDIP::im2double(image1), mask, true);
TinyDIP::bmp_write("OutputImages/1", TinyDIP::im2uint8(output_image));
auto end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
std::time_t end_time = std::chrono::system_clock::to_time_t(end);
std::cout << "Computation finished at " << std::ctime(&end_time) << "elapsed time: " << elapsed_seconds.count() << '\n';
return EXIT_SUCCESS;
}
All suggestions are welcome.
The summary information:
Which question it is a follow-up to?
conv2 Template Function Implementation for Image in C++ and An Updated Multi-dimensional Image Data Structure with Variadic Template Functions in C++
What changes has been made in the code since last question?
For performing the convolution in
double
format, bothim2double
andim2uint8
functions (just like Matlab'sim2double
andim2uint8
) are implemented in this post.Why a new review is being asked for?
Please review the implementation of
im2double
andim2uint8
functions and those tests.