This is a follow-up question for Tests for the operators of image template class in C++. As G. Sliepen's answer mentioned, I am attempting to use Boost.Test and several test cases are created with BOOST_AUTO_TEST_CASE_TEMPLATE
structure.
The experimental implementation
Image
template class implementation (image.h
):/* Developed by Jimmy Hu */ #ifndef Image_H #define Image_H #include <algorithm> #include <array> #include <cassert> #include <chrono> #include <complex> #include <concepts> #include <fstream> #include <functional> #include <iostream> #include <iterator> #include <list> #include <numeric> #include <string> #include <type_traits> #include <variant> #include <vector> #include "image_operations.h" #ifdef USE_BOOST_SERIALIZATION #include <boost/archive/binary_oarchive.hpp> #include <boost/archive/binary_iarchive.hpp> #include <boost/archive/text_iarchive.hpp> #include <boost/serialization/array.hpp> #include <boost/serialization/base_object.hpp> #include <boost/serialization/export.hpp> #include <boost/serialization/list.hpp> #include <boost/serialization/nvp.hpp> #include <boost/serialization/serialization.hpp> #include <boost/serialization/split_free.hpp> #include <boost/serialization/unique_ptr.hpp> #include <boost/serialization/vector.hpp> #endif namespace TinyDIP { template <typename ElementT> class Image { public: Image() = default; Image(const std::size_t width, const std::size_t height): width(width), height(height), image_data(width * height) { } Image(const std::size_t width, const std::size_t height, const ElementT initVal): width(width), height(height), image_data(width * height, initVal) {} Image(const std::vector<ElementT>& input, std::size_t newWidth, std::size_t newHeight): width(newWidth), height(newHeight) { assert(input.size() == newWidth * newHeight); this->image_data = input; // Deep copy } Image(const std::vector<std::vector<ElementT>>& input) { this->height = input.size(); this->width = input[0].size(); for (auto& rows : input) { this->image_data.insert(this->image_data.end(), std::begin(input), std::end(input)); // flatten } return; } constexpr ElementT& at(const unsigned int x, const unsigned int y) { checkBoundary(x, y); return this->image_data[y * width + x]; } constexpr ElementT const& at(const unsigned int x, const unsigned int y) const { checkBoundary(x, y); return this->image_data[y * width + x]; } constexpr std::size_t getWidth() const { return this->width; } constexpr std::size_t getHeight() const { return this->height; } constexpr auto getSize() { return std::make_tuple(this->width, this->height); } std::vector<ElementT> const& getImageData() const { return this->image_data; } // expose the internal data void print(std::string separator = "\t", std::ostream& os = std::cout) { for (std::size_t y = 0; y < this->height; ++y) { for (std::size_t x = 0; x < this->width; ++x) { // Ref: https://isocpp.org/wiki/faq/input-output#print-char-or-ptr-as-number os << +this->at(x, y) << separator; } os << "\n"; } os << "\n"; return; } // Enable this function if ElementT = RGB void print(std::string separator = "\t", std::ostream& os = std::cout) requires(std::same_as<ElementT, RGB>) { for (std::size_t y = 0; y < this->height; ++y) { for (std::size_t x = 0; x < this->width; ++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 << +this->at(x, y).channels[channel_index] << separator; } os << ")" << separator; } os << "\n"; } os << "\n"; return; } friend std::ostream& operator<<(std::ostream& os, const Image<ElementT>& rhs) { const std::string separator = "\t"; for (std::size_t y = 0; y < rhs.height; ++y) { for (std::size_t x = 0; x < rhs.width; ++x) { // Ref: https://isocpp.org/wiki/faq/input-output#print-char-or-ptr-as-number os << +rhs.at(x, y) << separator; } os << "\n"; } os << "\n"; return os; } Image<ElementT>& operator+=(const Image<ElementT>& rhs) { assert(rhs.width == this->width); assert(rhs.height == this->height); std::transform(image_data.cbegin(), image_data.cend(), rhs.image_data.cbegin(), image_data.begin(), std::plus<>{}); return *this; } Image<ElementT>& operator-=(const Image<ElementT>& rhs) { assert(rhs.width == this->width); assert(rhs.height == this->height); std::transform(image_data.cbegin(), image_data.cend(), rhs.image_data.cbegin(), image_data.begin(), std::minus<>{}); return *this; } Image<ElementT>& operator*=(const Image<ElementT>& rhs) { assert(rhs.width == this->width); assert(rhs.height == this->height); std::transform(image_data.cbegin(), image_data.cend(), rhs.image_data.cbegin(), image_data.begin(), std::multiplies<>{}); return *this; } Image<ElementT>& operator/=(const Image<ElementT>& rhs) { assert(rhs.width == this->width); assert(rhs.height == this->height); std::transform(image_data.cbegin(), image_data.cend(), rhs.image_data.cbegin(), image_data.begin(), std::divides<>{}); return *this; } bool operator==(const Image<ElementT>& rhs) const { /* do actual comparison */ if (rhs.width != this->width || rhs.height != this->height) { return false; } return rhs.image_data == this->image_data; } bool operator!=(const Image<ElementT>& rhs) const { return !(this == rhs); } Image<ElementT>& operator=(Image<ElementT> const& input) = default; // Copy Assign Image<ElementT>& operator=(Image<ElementT>&& other) = default; // Move Assign Image(const Image<ElementT> &input) = default; // Copy Constructor Image(Image<ElementT> &&input) = default; // Move Constructor #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::size_t width; std::size_t height; std::vector<ElementT> image_data; void checkBoundary(const size_t x, const size_t y) const { assert(x < width); assert(y < height); } #ifdef USE_BOOST_SERIALIZATION friend class boost::serialization::access; template<class Archive> void serialize(Archive& ar, const unsigned int version) { ar& width; ar& height; ar& image_data; } #endif }; } #endif
image_operations.h
:/* Developed by Jimmy Hu */ #ifndef ImageOperations_H #define ImageOperations_H #include <numbers> #include <string> #include "base_types.h" #include "image.h" #define is_size_same(x, y) {assert(x.getWidth() == y.getWidth()); assert(x.getHeight() == y.getHeight());} namespace TinyDIP { // Forward Declaration class Image template <typename ElementT> class Image; template<class T = GrayScale> requires (std::same_as<T, GrayScale>) constexpr static auto constructRGB(Image<T> r, Image<T> g, Image<T> b) { is_size_same(r, g); is_size_same(g, b); is_size_same(r, b); return; } template<typename T> T normalDistribution1D(const T x, const T standard_deviation) { return std::exp(-x * x / (2 * standard_deviation * standard_deviation)); } template<typename T> T normalDistribution2D(const T xlocation, const T ylocation, const T standard_deviation) { return std::exp(-(xlocation * xlocation + ylocation * ylocation) / (2 * standard_deviation * standard_deviation)) / (2 * std::numbers::pi * standard_deviation * standard_deviation); } template<class InputT1, class InputT2> constexpr static auto cubicPolate(const InputT1 v0, const InputT1 v1, const InputT1 v2, const InputT1 v3, const InputT2 frac) { auto A = (v3-v2)-(v0-v1); auto B = (v0-v1)-A; auto C = v2-v0; auto D = v1; return D + frac * (C + frac * (B + frac * A)); } template<class InputT = float, class ElementT> constexpr static auto bicubicPolate(const ElementT* const ndata, const InputT fracx, const InputT fracy) { auto x1 = cubicPolate( ndata[0], ndata[1], ndata[2], ndata[3], fracx ); auto x2 = cubicPolate( ndata[4], ndata[5], ndata[6], ndata[7], fracx ); auto x3 = cubicPolate( ndata[8], ndata[9], ndata[10], ndata[11], fracx ); auto x4 = cubicPolate( ndata[12], ndata[13], ndata[14], ndata[15], fracx ); return std::clamp( cubicPolate( x1, x2, x3, x4, fracy ), static_cast<InputT>(std::numeric_limits<ElementT>::min()), static_cast<InputT>(std::numeric_limits<ElementT>::max())); } template<class FloatingType = float, class ElementT> Image<ElementT> copyResizeBicubic(Image<ElementT>& image, size_t width, size_t height) { auto output = Image<ElementT>(width, height); // get used to the C++ way of casting auto ratiox = static_cast<FloatingType>(image.getWidth()) / static_cast<FloatingType>(width); auto ratioy = static_cast<FloatingType>(image.getHeight()) / static_cast<FloatingType>(height); for (size_t y = 0; y < height; ++y) { for (size_t x = 0; x < width; ++x) { FloatingType xMappingToOrigin = static_cast<FloatingType>(x) * ratiox; FloatingType yMappingToOrigin = static_cast<FloatingType>(y) * ratioy; FloatingType xMappingToOriginFloor = std::floor(xMappingToOrigin); FloatingType yMappingToOriginFloor = std::floor(yMappingToOrigin); FloatingType xMappingToOriginFrac = xMappingToOrigin - xMappingToOriginFloor; FloatingType yMappingToOriginFrac = yMappingToOrigin - yMappingToOriginFloor; ElementT ndata[4 * 4]; for (int ndatay = -1; ndatay <= 2; ++ndatay) { for (int ndatax = -1; ndatax <= 2; ++ndatax) { ndata[(ndatay + 1) * 4 + (ndatax + 1)] = image.at( std::clamp(xMappingToOriginFloor + ndatax, static_cast<FloatingType>(0), image.getWidth() - static_cast<FloatingType>(1)), std::clamp(yMappingToOriginFloor + ndatay, static_cast<FloatingType>(0), image.getHeight() - static_cast<FloatingType>(1))); } } output.at(x, y) = bicubicPolate(ndata, xMappingToOriginFrac, yMappingToOriginFrac); } } return output; } // multiple standard deviations template<class InputT> constexpr static Image<InputT> gaussianFigure2D( const size_t xsize, const size_t ysize, const size_t centerx, const size_t centery, const InputT standard_deviation_x, const InputT standard_deviation_y) { auto output = TinyDIP::Image<InputT>(xsize, ysize); auto row_vector_x = TinyDIP::Image<InputT>(xsize, 1); for (size_t x = 0; x < xsize; ++x) { row_vector_x.at(x, 0) = normalDistribution1D(static_cast<InputT>(x) - static_cast<InputT>(centerx), standard_deviation_x); } auto row_vector_y = TinyDIP::Image<InputT>(ysize, 1); for (size_t y = 0; y < ysize; ++y) { row_vector_y.at(y, 0) = normalDistribution1D(static_cast<InputT>(y) - static_cast<InputT>(centery), standard_deviation_y); } for (size_t y = 0; y < ysize; ++y) { for (size_t x = 0; x < xsize; ++x) { output.at(x, y) = row_vector_x.at(x, 0) * row_vector_y.at(y, 0); } } return output; } // single standard deviation template<class InputT> constexpr static Image<InputT> gaussianFigure2D( const size_t xsize, const size_t ysize, const size_t centerx, const size_t centery, const InputT standard_deviation) { return gaussianFigure2D(xsize, ysize, centerx, centery, standard_deviation, standard_deviation); } template<typename Op, class InputT, class... Args> constexpr static Image<InputT> pixelwiseOperation(Op op, const Image<InputT>& input1, const Args&... inputs) { Image<InputT> output( recursive_transform<1>( [&](auto&& element1, auto&&... elements) { auto result = op(element1, elements...); return static_cast<InputT>(std::clamp( result, static_cast<decltype(result)>(std::numeric_limits<InputT>::min()), static_cast<decltype(result)>(std::numeric_limits<InputT>::max()))); }, (input1.getImageData()), (inputs.getImageData())...), input1.getWidth(), input1.getHeight()); return output; } template<class InputT> constexpr static Image<InputT> plus(const Image<InputT>& input1) { return input1; } template<class InputT, class... Args> constexpr static Image<InputT> plus(const Image<InputT>& input1, const Args&... inputs) { return TinyDIP::pixelwiseOperation(std::plus<>{}, input1, plus(inputs...)); } template<class InputT> constexpr static Image<InputT> subtract(const Image<InputT>& input1, const Image<InputT>& input2) { is_size_same(input1, input2); return TinyDIP::pixelwiseOperation(std::minus<>{}, input1, input2); } template<class InputT = RGB> requires (std::same_as<InputT, RGB>) constexpr static Image<InputT> subtract(Image<InputT>& input1, Image<InputT>& input2) { is_size_same(input1, input2); Image<InputT> output(input1.getWidth(), input1.getHeight()); for (std::size_t y = 0; y < input1.getHeight(); ++y) { for (std::size_t x = 0; x < input1.getWidth(); ++x) { for(std::size_t channel_index = 0; channel_index < 3; ++channel_index) { output.at(x, y).channels[channel_index] = std::clamp( input1.at(x, y).channels[channel_index] - input2.at(x, y).channels[channel_index], 0, 255); } } } return output; } template<class InputT> constexpr static Image<InputT> multiplies(const Image<InputT>& input1, const Image<InputT>& input2) { return TinyDIP::pixelwiseOperation(std::multiplies<>{}, input1, input2); } template<class InputT, class... Args> constexpr static Image<InputT> multiplies(const Image<InputT>& input1, const Args&... inputs) { return TinyDIP::pixelwiseOperation(std::multiplies<>{}, input1, multiplies(inputs...)); } template<class InputT> constexpr static Image<InputT> divides(const Image<InputT>& input1, const Image<InputT>& input2) { return TinyDIP::pixelwiseOperation(std::divides<>{}, input1, input2); } template<class InputT> constexpr static Image<InputT> modulus(const Image<InputT>& input1, const Image<InputT>& input2) { return TinyDIP::pixelwiseOperation(std::modulus<>{}, input1, input2); } template<class InputT> constexpr static Image<InputT> negate(const Image<InputT>& input1, const Image<InputT>& input2) { return TinyDIP::pixelwiseOperation(std::negate<>{}, input1); } } #endif
base_types.h
: The base types/* Developed by Jimmy Hu */ #ifndef BASE_H #define BASE_H #include <filesystem> #include <math.h> #include <stdio.h> #include <stdlib.h> #include <string> #include <utility> using BYTE = unsigned char; struct RGB { BYTE channels[3]; }; using GrayScale = BYTE; struct HSV { double channels[3]; // Range: 0 <= H < 360, 0 <= S <= 1, 0 <= V <= 255 }; struct BMPIMAGE { std::filesystem::path FILENAME; unsigned int XSIZE; unsigned int YSIZE; BYTE FILLINGBYTE; BYTE *IMAGE_DATA; }; #endif
Unit Tests with Boost.Test
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MODULE image_elementwise_tests
#ifdef BOOST_TEST_MODULE
#include <boost/test/included/unit_test.hpp>
#ifdef BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>
#else
#include <boost/test/included/unit_test.hpp>
#endif // BOOST_TEST_DYN_LINK
#include <boost/mpl/list.hpp>
#include <boost/mpl/vector.hpp>
#include <tao/tuple/tuple.hpp>
typedef boost::mpl::list<
byte, char, int, short, long, long long int,
unsigned int, unsigned short int, unsigned long int, unsigned long long int,
float, double, long double> test_types;
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_add_test, T, test_types)
{
std::size_t size_x = 10;
std::size_t size_y = 10;
T initVal = 10;
T increment = 1;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test += TinyDIP::Image<T>(size_x, size_y, increment);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal + increment));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_add_test_zero_dimensions, T, test_types)
{
std::size_t size_x = 0; // Test images with both of the dimensions having size zero.
std::size_t size_y = 0; // Test images with both of the dimensions having size zero.
T initVal = 10;
T increment = 1;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test += TinyDIP::Image<T>(size_x, size_y, increment);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal + increment));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_add_test_large_dimensions, T, test_types)
{
std::size_t size_x = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
std::size_t size_y = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
T initVal = 10;
T increment = 1;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test += TinyDIP::Image<T>(size_x, size_y, increment);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal + increment));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_minus_test, T, test_types)
{
std::size_t size_x = 10;
std::size_t size_y = 10;
T initVal = 10;
T difference = 1;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test -= TinyDIP::Image<T>(size_x, size_y, difference);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal - difference));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_minus_test_zero_dimensions, T, test_types)
{
std::size_t size_x = 0; // Test images with both of the dimensions having size zero.
std::size_t size_y = 0; // Test images with both of the dimensions having size zero.
T initVal = 10;
T difference = 1;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test -= TinyDIP::Image<T>(size_x, size_y, difference);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal - difference));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_minus_test_large_dimensions, T, test_types)
{
std::size_t size_x = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
std::size_t size_y = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
T initVal = 10;
T difference = 1;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test -= TinyDIP::Image<T>(size_x, size_y, difference);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal - difference));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_multiplies_test, T, test_types)
{
std::size_t size_x = 10;
std::size_t size_y = 10;
T initVal = 10;
T multiplier = 2;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test *= TinyDIP::Image<T>(size_x, size_y, multiplier);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal * multiplier));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_multiplies_test_zero_dimensions, T, test_types)
{
std::size_t size_x = 0; // Test images with both of the dimensions having size zero.
std::size_t size_y = 0; // Test images with both of the dimensions having size zero.
T initVal = 10;
T multiplier = 2;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test *= TinyDIP::Image<T>(size_x, size_y, multiplier);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal * multiplier));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_multiplies_test_large_dimensions, T, test_types)
{
std::size_t size_x = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
std::size_t size_y = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
T initVal = 10;
T multiplier = 2;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test *= TinyDIP::Image<T>(size_x, size_y, multiplier);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal * multiplier));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_divides_test, T, test_types)
{
std::size_t size_x = 10;
std::size_t size_y = 10;
T initVal = 10;
T divider = 2;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test /= TinyDIP::Image<T>(size_x, size_y, divider);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal / divider));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_divides_test_zero_dimensions, T, test_types)
{
std::size_t size_x = 0; // Test images with both of the dimensions having size zero.
std::size_t size_y = 0; // Test images with both of the dimensions having size zero.
T initVal = 10;
T divider = 2;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test /= TinyDIP::Image<T>(size_x, size_y, divider);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal / divider));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_divides_test_large_dimensions, T, test_types)
{
std::size_t size_x = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
std::size_t size_y = 18446744073709551615; // Test images with very large dimensions (std::numeric_limits<std::size_t>::max()).
T initVal = 10;
T divider = 2;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test /= TinyDIP::Image<T>(size_x, size_y, divider);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal / divider));
}
BOOST_AUTO_TEST_CASE_TEMPLATE(image_elementwise_divides_zero_test, T, test_types)
{
std::size_t size_x = 10;
std::size_t size_y = 10;
T initVal = 10;
T divider = 0;
auto test = TinyDIP::Image<T>(size_x, size_y, initVal);
test /= TinyDIP::Image<T>(size_x, size_y, divider);
BOOST_TEST(test == TinyDIP::Image<T>(size_x, size_y, initVal / divider)); // dividing by zero test
}
#endif
All suggestions are welcome.
The summary information:
Which question it is a follow-up to?
What changes has been made in the code since last question?
I am attempting to create some test cases with Boost.Test framework in this post.
Why a new review is being asked for?
If there is any possible improvement, please let me know.