I've been teaching myself C++, and I recently created a simple Image
class that makes use of the XTensor library. With my Image
class, users can easily read and write PNG or JPG images and convert an RGB image to a grayscale image.
If you're interested, you can check out the code for yourself in this repository. Please forgive the extensive comments in the code - I'm still in the process of learning C++.
Declaration of Image
Class
To make things easier to read and review here, I've removed some of the extensive comments from my code.
- The declaration of Image class:
#ifndef IMAGE_XTENSOR_HPP
#define IMAGE_XTENSOR_HPP
#include <memory>
#include <string>
#include <xtensor/xarray.hpp>
#include <xtensor/xio.hpp>
#include <xtensor/xview.hpp>
namespace mypackage::image {
struct ImageXTensor {
ImageXTensor();
explicit ImageXTensor(std::string file_path);
explicit ImageXTensor(int c, int h, int w);
explicit ImageXTensor(const xt::xtensor<double, 3> &input_matrix);
explicit ImageXTensor(const ImageXTensor &other);
ImageXTensor(ImageXTensor &&other);
ImageXTensor &operator=(const ImageXTensor &other);
ImageXTensor &operator=(ImageXTensor &&other);
~ImageXTensor();
bool operator==(const ImageXTensor &other) const;
int channels; // aka comp in std_image
int height;
int width;
int size;
std::unique_ptr<xt::xtensor<double, 3>> pixels;
bool save(std::string file_path);
void swap(ImageXTensor &other);
};
ImageXTensor rgb_to_grayscale_xtensor(const ImageXTensor &img);
xt::xarray<double> rgb_to_grayscale_xtensor(const xt::xarray<double> &pixels);
} // namespace mypackage::image
#endif
I hope you find that the declaration of the Image
class is easy to understand. However, I have a question about the move constructor ImageXTensor(ImageXTensor &&other);
. If I attach explicit
keyword to the move constructor like explicit ImageXTensor(ImageXTensor &&other);
, it will lead to a compilation error like below:
/home/lai/cmake_template/src/mypackage/image/image_xtensor.cpp: In function ‘mypackage::image::ImageXTensor mypackage::image::rgb_to_grayscale_xtensor(const mypackage::image::ImageXTensor&)’:
/home/lai/cmake_template/src/mypackage/image/image_xtensor.cpp:309:10: error: no matching function for call to ‘mypackage::image::ImageXTensor::ImageXTensor(mypackage::image::ImageXTensor&)’
309 | return gray;
| ^~~~
/home/lai/cmake_template/src/mypackage/image/image_xtensor.cpp:18:1: note: candidate: ‘mypackage::image::ImageXTensor::ImageXTensor()’
18 | ImageXTensor::ImageXTensor()
| ^~~~~~~~~~~~
/home/lai/cmake_template/src/mypackage/image/image_xtensor.cpp:18:1: note: candidate expects 0 arguments, 1 provided
make[2]: *** [src/mypackage/image/CMakeFiles/image_xtensor.dir/build.make:76: src/mypackage/image/CMakeFiles/image_xtensor.dir/image_xtensor.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:506: src/mypackage/image/CMakeFiles/image_xtensor.dir/all] Error 2
make: *** [Makefile:146: all] Error 2
I tried to understand this error from this SO question, but I'm still not quite sure why this is happening. I understand explicit
is used to prevent compilers do implicit conversion, and as a believer of explicit is always better than implicit, I think using explicit everywhere should be always preferred.
Thus, I would appreciate any insights or suggestions you might have.
Definition of Image class
Here is the complete definition of the Image class.
#include <cassert>
#include <cmath>
#include <filesystem>
#include <iostream>
#include <mypackage/image/image_xtensor.hpp>
#include <utility>
#include <xtensor/xadapt.hpp>
#include <xtensor/xarray.hpp>
#include <xtensor/xio.hpp>
#include <xtensor/xtensor.hpp>
#include <xtensor/xview.hpp>
#include <stb/stb_image.h>
#include <stb/stb_image_write.h>
namespace mypackage::image {
ImageXTensor::ImageXTensor()
: channels{0}, height{0}, width{0}, size{0}, pixels{nullptr} {
std::clog << "The default constructor takes no paramters.\n";
}
ImageXTensor::ImageXTensor(std::string file_path) {
std::clog << "The constructor takes a file path.\n";
unsigned char *img_data =
stbi_load(file_path.c_str(), &width, &height, &channels, 0);
if (img_data == nullptr) {
const char *error_msg = stbi_failure_reason();
std::cerr << "Failed to load image: " << file_path.c_str() << "\n";
std::cerr << "Error msg (stb_image): " << error_msg << "\n";
std::exit(1);
}
pixels = std::make_unique<xt::xtensor<double, 3>>(
xt::zeros<double>({channels, height, width}));
size = pixels->size();
std::clog << "The image shape: " << channels << " x " << height << " x "
<< width << '\n';
assert(size == channels * height * width);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
for (int c = 0; c < channels; c++) {
// PNG's pixels order is mysterious for me.
std::size_t src_idx = y * width * channels + x * channels + c;
// Rescale uint8 to float 0-1.
(*pixels)(c, y, x) = img_data[src_idx] / 255.;
}
}
}
if (channels == 4)
channels = 3; // ignore alpha channel
stbi_image_free(img_data);
}
ImageXTensor::ImageXTensor(int c, int h, int w)
: channels{c}, height{h}, width{w}, size{c * h * w},
pixels{std::make_unique<xt::xtensor<double, 3>>(
xt::zeros<double>({c, h, w}))} {}
ImageXTensor::ImageXTensor(const xt::xtensor<double, 3> &input_matrix) {
channels = input_matrix.shape(0);
height = input_matrix.shape(1);
width = input_matrix.shape(2);
size = input_matrix.size();
pixels = std::make_unique<xt::xtensor<double, 3>>(input_matrix);
}
ImageXTensor::ImageXTensor(const ImageXTensor &other)
: channels{other.channels}, height{other.height}, width{other.width},
size{other.size}, pixels{std::make_unique<xt::xtensor<double, 3>>(
xt::zeros<double>(
{other.channels, other.height, other.width}))} {
std::clog << "Copy Constructor\n";
*pixels = *other.pixels;
}
ImageXTensor &ImageXTensor::operator=(const ImageXTensor &other) {
std::clog << "Copy Assignment Operator\n";
if (this != &other) {
channels = other.channels;
height = other.height;
width = other.width;
size = other.size;
pixels = std::make_unique<xt::xtensor<double, 3>>(
xt::zeros<double>({other.channels, other.height, other.width}));
*pixels = *other.pixels;
}
return *this;
}
ImageXTensor::ImageXTensor(ImageXTensor &&other)
: channels{other.channels}, height{other.height}, width{other.width},
size{other.size}, pixels{std::move(other.pixels)} {
std::clog << "Move Constructor\n";
swap(other);
}
ImageXTensor &ImageXTensor::operator=(ImageXTensor &&other) {
std::clog << "Move Assignment Operator\n";
swap(other);
return *this;
}
ImageXTensor::~ImageXTensor() { std::clog << "Destruct Image.\n"; }
bool ImageXTensor::operator==(const ImageXTensor &other) const {
return (width == other.width) && (height == other.height) &&
(channels == other.channels) && (size == other.size) &&
(*pixels == *other.pixels);
}
bool ImageXTensor::save(std::string file_path) {
auto file_extension = std::filesystem::path(file_path).extension();
unsigned char *out_data = new unsigned char[width * height * channels];
/** NOTE: There seems to be no easy way to unfold a 3D array into a 1D array
* with the desired order.
*/
for (auto x = 0; x < width; x++) {
for (auto y = 0; y < height; y++) {
for (auto c = 0; c < channels; c++) {
int dst_idx = y * width * channels + x * channels + c;
// Fill out_data with uint8 values range 0-255.
out_data[dst_idx] = std::roundf((*pixels)(c, y, x) * 255.);
}
}
}
bool success{false};
if (file_extension == std::string(".jpg") ||
file_extension == std::string(".JPG")) {
auto quality = 100;
success = stbi_write_jpg(file_path.c_str(), width, height, channels,
out_data, quality);
} else if (file_extension == std::string(".png") ||
file_extension == std::string(".png")) {
auto stride_in_bytes = width * channels;
success = stbi_write_png(file_path.c_str(), width, height, channels,
out_data, stride_in_bytes);
} else {
std::cerr << "Unsupported file format: " << file_extension << "\n";
}
if (!success)
std::cerr << "Failed to save image: " << file_path << "\n";
delete[] out_data;
return true;
}
void ImageXTensor::swap(ImageXTensor &other) {
std::swap(channels, other.channels);
std::swap(height, other.height);
std::swap(width, other.width);
std::swap(size, other.size);
std::swap(pixels, other.pixels);
}
ImageXTensor rgb_to_grayscale_xtensor(const ImageXTensor &img) {
assert(img.channels >= 3);
ImageXTensor gray(1, img.height, img.width);
xt::xarray<double> red = xt::view(*img.pixels, 0, xt::all(), xt::all());
xt::xarray<double> green = xt::view(*img.pixels, 1, xt::all(), xt::all());
xt::xarray<double> blue = xt::view(*img.pixels, 2, xt::all(), xt::all());
xt::view(*gray.pixels, 0, xt::all(), xt::all()) =
0.299 * red + 0.587 * green + 0.114 * blue;
return gray;
}
xt::xarray<double> rgb_to_grayscale_xtensor(const xt::xarray<double> &pixels) {
assert(pixels.shape(0) >= 3);
auto height = pixels.shape(1);
auto width = pixels.shape(2);
xt::xarray<double>::shape_type shape = {1, height, width};
xt::xarray<double> gray(shape);
xt::xarray<double> red = xt::view(pixels, 0, xt::all(), xt::all());
xt::xarray<double> green = xt::view(pixels, 1, xt::all(), xt::all());
xt::xarray<double> blue = xt::view(pixels, 2, xt::all(), xt::all());
xt::view(gray, 0, xt::all(), xt::all()) =
0.299 * red + 0.587 * green + 0.114 * blue;
return gray;
}
} // namespace mypackage::image
I hope you find the implementation of the Image
class to be straightforward.
Usage of Image
class (You can find the code here)
- Pass Image object to
rgb_to_grayscale_xtensor
:
bool rgb2gray_image_xtensor(std::string input, std::string output) {
mypackage::image::ImageXTensor in_img{input};
auto out_img = mypackage::image::rgb_to_grayscale_xtensor(in_img);
out_img.save(output);
return 0;
}
- Pass
xt::xarray<double>
torgb_to_grayscale_xtensor
:
bool rgb2gray_image_xtensor_PassByTensor(std::string input,
std::string output) {
mypackage::image::ImageXTensor in_img{input};
mypackage::image::ImageXTensor out_img{in_img.channels, in_img.height,
in_img.width};
*out_img.pixels = mypackage::image::rgb_to_grayscale_xtensor(*in_img.pixels);
out_img.save(output);
return 0;
}
Unit test: (You can find the code here)
- Test constructor and assignment operator:
#include <gtest/gtest.h>
#include <iostream>
#include <mypackage/image/image_xtensor.hpp>
#include <vector>
#include <xtensor/xadapt.hpp>
TEST(ImageXTensor, ClassAssertion) {
mypackage::image::ImageXTensor test_img1(2, 3, 4);
int counter = 0;
for (int x = 0; x < test_img1.width; x++) {
for (int y = 0; y < test_img1.height; y++) {
for (int c = 0; c < test_img1.channels; c++) {
(*test_img1.pixels)(c, y, x) = counter;
++counter;
}
}
}
std::clog << "test_img1:\n" << (*test_img1.pixels) << '\n';
std::clog << "Test Copy Constructor.\n";
mypackage::image::ImageXTensor test_img2{test_img1};
std::clog << "test_img2:\n" << (*test_img2.pixels) << '\n';
EXPECT_EQ(test_img2, test_img1);
std::clog << "Test Copy Assignment Operator.\n";
mypackage::image::ImageXTensor test_img3;
test_img3 = test_img1;
std::clog << "test_img3\n" << (*test_img3.pixels) << '\n';
EXPECT_EQ(test_img3, test_img1);
std::clog << "Test Move Assignment Operator.\n";
mypackage::image::ImageXTensor test_img4;
test_img4 = std::move(test_img1);
// test_img1 becomes unspecified after it's moved from.
EXPECT_EQ(test_img1.pixels, nullptr);
EXPECT_EQ(test_img4, test_img2);
}
As I'm still learning C++, I'm trying to focus on modern C++ and adhere to the best coding practices. If it's not too much trouble, I would appreciate it if you could review the code with modern C++ (especially those from C++17 and beyond) in mind. Additionally, any tips on following C++ best coding practices would be greatly appreciated. Thank you.
explicit
is usually not applied to copy constructors? Thanks for your feedback. \$\endgroup\$explicit
add? It means every time you want to doX x = std:move(other_x)
you need to call the constructor explicitly:X x = X(std:move(other_x))
. This makes no sense! Second, as explained in the SO answer you linked, when you doreturn x
, either the move constructor or the copy constructor (or move is not possible) is implicitly called, not having the implicit constructor means you can’t return objects of this class (this is the reason you’re getting the error). \$\endgroup\$explicit
is useful when casting from one type to another. If you have a constructorX(Y y)
, then you can doX x = y
. If you make that constructor explicit, then you need to explicitly cast. This is useful to avoid unintended casts. You don’t want to make operators explicit if the cast they provide is normal, expected, or convenient if implicit. \$\endgroup\$