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.
2 Answers 2
Write a generic conversion routine
What if you add float
images, or uint16_t
ones? The number of conversion functions you would need then would grow quadratic with the number of image times. To avoid this, write a template that can convert any image type to any other one:
template<typename DstT, typename SrcT>
constexpr static auto convert_image(Image<Src> input) {
auto image_data = input.getImageData();
std::vector<DstT> new_data;
using channel_type = decltype(DstT{}.channels[0]);
for (size_t index = 0; index < input.count(); ++index)
{
new_data.emplace_back(DstT{
static_cast<channel_type>(image_data[index].channels[0]),
...
});
...
}
}
Then you can use it like so:
Image<RGB> foo;
...
auto bar = convert_image<RGB_DOUBLE>(foo);
The code above assumes you have three color channels, but you can make it even more generic and handle conversions to/from/between grayscale images as well.
When you create an empty vector, and repeatedly emplace_back()
pixels, you do a lot of reallocations that you can avoid by reserve()
ing the right number of pixels before the loop.
Next, you put this vector into your image constructor, which copies the data over. Thus, you are copying the data twice. Consider std::move()
ing the vector, and have a constructor that doesn’t copy the vector but moves it.
One more comment: MATLAB’s im2double
divides the pixel values by 255, and im2uint8
multiplies them by 255. I’m not saying you should do this, I think it’s wasted compute cycles. But saying "like in MATLAB" could mislead some users.
Edit: I just noticed that you have three Image
constructors that take a std::vector
with the pixel values as input:
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
}
The first one takes the vector by r-value reference, the other two by l-value reference. So the first one has to be invoked by Image(std::move(new_data), 0)
, the second one by Image(new_data, input.getSize())
(it's the one you call), and the third one by Image(new_data, 0, 0)
.
Note that the second one is the only one that doesn't check for the given image sizes to match the array size. The first two allow for images of any dimensionality, and the third one is explicitly 2D.
But the worst thing here is what you do with the input vectors: In the first one, where you get a vector that the caller cannot use any more (it's an r-value reference, a reference to a temporary, which means that the user either moved from it, or it was the output of a function that was't stored in a local variable), and you're free to steal the data, but you copy the values from the vector. In the second one you also copy the values. In the third one, you steal the data from the input l-value reference, which the user will not at all expect. If you call this constructor as Image(new_data, 0, 0)
, you don't expect new_data
to no longer be useful after this.
Without regard for how the size is specified, you need to rewrite all of these constructors to take the vector by value, like so:
Image(std::vector<ElementT> input, std::vector<std::size_t> sizes):
size{std::move(sizes)}, image_data(std::move(input)) {}
Why? What does this do? When you call this function with an r-value (a temporary), or by moving a vector, then input
is this same vector. If you call the function with an l-value, input
will be a copy, and the caller's vector will not be unexpectedly broken.
So, Image(new_data, input.getSize())
will copy the vector new_data
, but Image(std::move(new_data), input.getSize())
will steal the data from new_data
. The user gets to decide!
Read the post you linked in your code again: https://stackoverflow.com/a/51706522/6667035 We're doing (1) here. Alternatively do (2) (one constructor takes a const&
, the other a &&
). On that page, no constructor takes a &
like yours.
Explore related questions
See similar questions with these tags.