3
\$\begingroup\$

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 file image_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 file image_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 file base_types.h)

    struct RGB_DOUBLE
    {
        double channels[3];
    };
    
  • The updated conv2 template function implementation (in file image_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 file basic_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 file image_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 file image_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 file image_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 file image_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 file image.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;
}

TinyDIP on GitHub

All suggestions are welcome.

The summary information:

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

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.

\$\endgroup\$
2
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.