diff --git a/include/genimg/methods.hh b/include/genimg/methods.hh index d7d5509..48f35bf 100644 --- a/include/genimg/methods.hh +++ b/include/genimg/methods.hh @@ -1,29 +1,28 @@ #pragma once -#include -#include #include #include #include #include -#include #include #include class ImageManipulator { public: ImageManipulator() = delete; - ImageManipulator(const ImageManipulator& other); - ImageManipulator(ImageManipulator&& other) noexcept; - [[nodiscard]] auto operator=(const ImageManipulator& other) - -> ImageManipulator; - [[nodiscard]] auto operator=(ImageManipulator&& other) noexcept - -> ImageManipulator; - // Load image from input, and prepare for output - ImageManipulator(std::filesystem::path const t_input_path, - std::filesystem::path const t_output_path, + /// \brief Copy contructor + ImageManipulator(const ImageManipulator& other); + + /// \brief Move constructor + ImageManipulator(ImageManipulator&& other) noexcept; + + /// \brief Load image from input, and prepare for output + ImageManipulator(std::string const t_input_path, + std::string const t_output_path, int const iterations); + + /// \brief Basically makes views from image ImageManipulator(cv::Mat const& t_origin_image, int const iterations, int const t_x, @@ -31,57 +30,96 @@ class ImageManipulator { int const t_width, int const t_height); + /// \brief Copy assignment operator + [[nodiscard]] auto operator=(const ImageManipulator& other) + -> ImageManipulator; + + /// \brief Move assignment operator + [[nodiscard]] auto operator=(ImageManipulator&& other) noexcept + -> ImageManipulator; + + /// \brief Execute the nth method on the current object void exec_method(int const t_nb_method, bool const t_controlled_size, int const t_cols, int const t_rows, int const t_submethod); + + /// \brief Write the generated image to the output path void write_file() const; + + /// \brief Returns a reference to the generated image [[nodiscard]] auto const& get_generated_image() const noexcept { return generated_image_; } - //! Destructor + /// \brief Destructor virtual ~ImageManipulator() noexcept = default; protected: private: + /// \brief Calculates the euclidian distance between two images [[nodiscard]] auto euclidian_distance(cv::Mat const& t_img) const noexcept -> double; + /// \brief Creates and returns a random color [[nodiscard]] auto random_color() const noexcept; + /// \brief Generates random square coordinates [[nodiscard]] auto get_square_values() const noexcept; + /// \brief Generates controlled random square coordinates [[nodiscard]] auto get_controlled_square_values() const noexcept; + /// \brief Generates a candidate for image generation improvement [[nodiscard]] auto create_candidate(bool const t_controlled_size) const; + [[nodiscard]] auto generate_tiles(int const t_cols, int const t_rows) const; + + /// \brief Gets all colors from the reference image void get_color_set(); + /// \brief Threaded helper for \ref get_color_set void threaded_get_color(int t_h); - void adjust_size(cv::Point& t_top_left, int const size) noexcept; + /// \brief Draw a square on an image void draw_square(cv::Mat& t_img, cv::Point const& t_top_left, int const t_size, cv::Scalar const& t_color) const; + /// \brief Update this object’s generated image void update_gen_image(cv::Mat const& t_img, double const t_diff); + + void merge_tiles(std::vector> t_tiles); + + /// \brief First method as described in the + /// [report](https://labs.phundrak.fr/phundrak/genetic-images/blob/master/report/report.pdf) void method1(); + /// \brief Second method as described in the + /// [report](https://labs.phundrak.fr/phundrak/genetic-images/blob/master/report/report.pdf) void method2(); + /// \brief Third method as described in the + /// [report](https://labs.phundrak.fr/phundrak/genetic-images/blob/master/report/report.pdf) void method3(); + /// \brief Fourth method as described in the + /// [report](https://labs.phundrak.fr/phundrak/genetic-images/blob/master/report/report.pdf) void method4(bool const t_controlled_size); + /// \brief Fifth method as described in the + /// [report](https://labs.phundrak.fr/phundrak/genetic-images/blob/master/report/report.pdf) void method5(bool const t_controlled_size, - int cols, + int const cols, int const rows, int const submethod); std::vector> colors_ - = std::vector>{}; - cv::Mat const reference_; + = std::vector>{}; /*!< Color set from reference */ + cv::Mat const reference_; /*!< Reference image */ cv::Mat generated_image_ = cv::Mat{reference_.size().height, reference_.size().width, CV_8UC3, - cv::Scalar(0, 0, 0)}; - mutable std::mutex colors_mutex_ = std::mutex{}; - std::string const output_path_{""}; - double diff_ = 0.0; - int const total_iterations_ = 0; - int remaining_iter_ = total_iterations_; - int const width_ = reference_.size().width; - int const height_ = reference_.size().height; + cv::Scalar(0, 0, 0)}; /*!< Working, generated image */ + mutable std::mutex colors_mutex_ + = std::mutex{}; /*!< Thread mutex for color set generation */ + std::string const output_path_{""}; /*!< Write path for the generated image */ + double diff_ = euclidian_distance(generated_image_); /*!< Euclidian difference + between \ref reference_ and \ref generated_image_ */ + int const total_iterations_ = 0; /*!< Number of iterations to perform */ + int remaining_iter_ + = total_iterations_; /*!< Remaining iterations to perform */ + int const width_ = reference_.size().width; /*!< Width of the image */ + int const height_ = reference_.size().height; /*!< Height of the image */ }; diff --git a/include/genimg/parseargs.hh b/include/genimg/parseargs.hh index 3535547..873ae46 100644 --- a/include/genimg/parseargs.hh +++ b/include/genimg/parseargs.hh @@ -3,6 +3,7 @@ #include #include +/// \brief Parses the arguments passed to the program [[nodiscard]] auto parse_args(int, char**) -> std::tuple #include #include +#include #include -#include #include +#include #include auto const thread_nbr = std::thread::hardware_concurrency(); -/////////////////////////////////////////////////////////////////////////////// -// class implementation // -/////////////////////////////////////////////////////////////////////////////// - // constructors /////////////////////////////////////////////////////////////// -ImageManipulator::ImageManipulator(std::filesystem::path const t_input_path, - std::filesystem::path const t_output_path, - int const t_iterations) - : reference_{cv::imread(t_input_path.native(), cv::IMREAD_COLOR)}, - output_path_{t_output_path.native()}, - diff_{euclidian_distance(generated_image_)}, - total_iterations_{t_iterations} -{ - if (!reference_.data) { - spdlog::critical("Could not open or find image!\n"); - exit(-1); - } - spdlog::debug("Image loaded!"); - spdlog::debug("Width:\t{}", reference_.size().width); - spdlog::debug("Height:\t{}", reference_.size().height); -} - -ImageManipulator::ImageManipulator(cv::Mat const& t_origin_image, - int const t_iterations, - int const t_x, - int const t_y, - int const t_width, - int const t_height) - : reference_{t_origin_image( - cv::Range{t_y, std::min(t_y + t_height, t_origin_image.rows)}, - cv::Range{t_x, std::min(t_x + t_width, t_origin_image.cols)})}, - diff_{euclidian_distance(generated_image_)}, - total_iterations_{t_iterations} -{ - if (!reference_.data) { - spdlog::critical("Could not open or find image!\n"); - exit(-1); - } - spdlog::debug("Image loaded!"); - spdlog::debug("Width:\t{}", reference_.size().width); - spdlog::debug("Height:\t{}", reference_.size().height); -} - +/** + * Copy constructor of \ref ImageManipulator, will copy all of its members + * except for its mutex. + * + * \param[in] other Element to copy + */ ImageManipulator::ImageManipulator(const ImageManipulator& other) : colors_{other.colors_}, reference_{other.reference_}, @@ -66,6 +31,12 @@ ImageManipulator::ImageManipulator(const ImageManipulator& other) { } +/** + * Move constructor of \ref ImageManipulator, will move all of the input’s + * members except for its mutex, a new one will be made. + * + * \param[in] other Element to move + */ ImageManipulator::ImageManipulator(ImageManipulator&& other) noexcept : colors_{std::move(other.colors_)}, reference_{std::move(other.reference_)}, @@ -79,13 +50,78 @@ ImageManipulator::ImageManipulator(ImageManipulator&& other) noexcept { } +/** + * Creates an instance of \ref ImageManipulator based on an input path and an + * output path. It will load the input image from its first argument, and will + * write an output image when asked at the path passed as its second argument. + * + * \param[in] t_input_path Path for the input, reference image + * \param[in] t_output_path Path to the output image to write + */ +ImageManipulator::ImageManipulator(std::string const t_input_path, + std::string const t_output_path, + int const t_iterations) + : reference_{cv::imread(t_input_path, cv::IMREAD_COLOR)}, + output_path_{t_output_path}, + total_iterations_{t_iterations} +{ + if (!reference_.data) { + spdlog::critical("Could not open or find image!\n"); + exit(-1); + } +} + +/** + * Creates a view of the input image, and will generate an image based only on + * that view. + * + * \param[in] t_origin_image Image to create a view from + * \param[in] t_iterations Number of iterations to perform on this view + * \param[in] t_x X value of the view’s origin (top left) + * \param[in] t_y Y value of the view’s origin (top left) + * \param[in] t_width Width of the view from its origin + * \param[in] t_height Height of the view from its origin + */ +ImageManipulator::ImageManipulator(cv::Mat const& t_origin_image, + int const t_iterations, + int const t_x, + int const t_y, + int const t_width, + int const t_height) + : reference_{t_origin_image( + cv::Range{t_y, std::min(t_y + t_height, t_origin_image.rows)}, + cv::Range{t_x, std::min(t_x + t_width, t_origin_image.cols)})}, + total_iterations_{t_iterations} +{ + if (!reference_.data) { + spdlog::critical("Could not open or find image!\n"); + exit(-1); + } +} + + // operators ////////////////////////////////////////////////////////////////// + +/** + * Copy assignment operator, will copy all of the input’s members except for + * its mutex, a new one will be generated. + * + * \param[in] other Element to copy + * \return ImageManipulator + */ [[nodiscard]] auto ImageManipulator::operator=(const ImageManipulator& other) -> ImageManipulator { return ImageManipulator(other); } +/** + * Move assignment operator, will move all of the input’s members except for + * its mutex, a new one will be generated. + * + * \param[in] other Element to move + * \return ImageManipulator + */ [[nodiscard]] auto ImageManipulator::operator=( ImageManipulator&& other) noexcept -> ImageManipulator { @@ -93,11 +129,25 @@ ImageManipulator::ImageManipulator(ImageManipulator&& other) noexcept } // public methods ///////////////////////////////////////////////////////////// + +/** + * Execute one of the methods as described in the report. If a non-valid + * method is called, the program will be terminated. The argument + * `t_controlled_size` allows the program to have some control over the random + * size of the squares that will be generated. The arguments `t_cols`, `t_rows` + * and `submethod` are relevant to the fifth method. + * + * \param[in] t_nb_method Method identifier + * \param[in] t_controlled_size Control over the squares’ size + * \param[in] t_cols Number of columns the reference should be divided into + * \param[in] t_rows Number of rows the reference should be divided into + * \param[in] t_submethod + */ void ImageManipulator::exec_method(int const t_nb_method, bool const t_controlled_size = false, - int const cols = 1, - int const rows = 0, - int const submethod = 1) + int const t_cols = 1, + int const t_rows = 0, + int const t_submethod = 1) { switch (t_nb_method) { case 1: { @@ -117,7 +167,7 @@ void ImageManipulator::exec_method(int const t_nb_method, break; } case 5: { - method5(t_controlled_size, cols, rows, submethod); + method5(t_controlled_size, t_cols, t_rows, t_submethod); break; } default: @@ -126,6 +176,10 @@ void ImageManipulator::exec_method(int const t_nb_method, } } +/** + * Write the generated image as a file to the specified path stored in the + * object + */ void ImageManipulator::write_file() const { cv::imwrite(output_path_, generated_image_); @@ -133,6 +187,13 @@ void ImageManipulator::write_file() const // private methods //////////////////////////////////////////////////////////// +/** + * Calculates the euclidian distance between the reference image and the image + * passed as an argument + * + * \param t_img Image with which the distance is computed + * \return double + */ [[nodiscard]] auto ImageManipulator::euclidian_distance( cv::Mat const& t_img) const noexcept -> double { @@ -145,11 +206,19 @@ void ImageManipulator::write_file() const return std::sqrt(euclidian); } +/** + * \return cv::Scalar + */ [[nodiscard]] auto ImageManipulator::random_color() const noexcept { return cv::Scalar(rand() % 255, rand() % 255, rand() % 255); } +/** + * Generates random x/y coordinates for a square and the size of said square. + * + * \return Tuple of three ints + */ [[nodiscard]] auto ImageManipulator::get_square_values() const noexcept { int rand_x = rand() % reference_.size().width; @@ -160,6 +229,13 @@ void ImageManipulator::write_file() const return std::tuple(rand_x, rand_y, size); } +/** + * Generates random x/y coordinates for a square’s origin (top left), and a + * random size which’s max and minimal value are controled as mentionned in the + * [report](https://labs.phundrak.fr/phundrak/genetic-images/blob/master/report/report.pdf). + * + * \return Tuple of three ints + */ [[nodiscard]] auto ImageManipulator::get_controlled_square_values() const noexcept { @@ -177,6 +253,15 @@ void ImageManipulator::write_file() const return std::tuple(rand_x, rand_y, size); } +/** + * Creates a temporary image on which a random square is drawn. If its + * euclidian distance with the reference image proves to be an improvement from + * the latest improvement before, then both the image and the distance are + * returned. Otherwise, nothing is returned. + * + * \param[in] t_controlled_size Enables controlled square size + * \return Optional pair of cv::Mat and double + */ [[nodiscard]] auto ImageManipulator::create_candidate( bool const t_controlled_size = false) const { @@ -196,6 +281,33 @@ void ImageManipulator::write_file() const : std::nullopt; } +[[nodiscard]] auto ImageManipulator::generate_tiles(int const t_cols, + int const t_rows) const +{ + std::vector> tiles{}; + int const tile_width = reference_.cols / t_cols; + int const tile_height = reference_.rows / t_rows; + for (int index_x = 0; index_x < t_cols; ++index_x) { + std::vector tile_col{}; + for (int index_y = 0; index_y < t_rows; ++index_y) { + int const width = (index_x != t_cols - 1) + ? tile_width + : tile_width + reference_.cols % tile_width; + int const height = (index_y != t_rows - 1) + ? tile_height + : tile_height + reference_.rows % tile_height; + tile_col.emplace_back(reference_, total_iterations_, index_x * tile_width, + index_y * tile_height, width, height); + } + tiles.push_back(tile_col); + } + return tiles; +} + +/** + * Will analyse the reference image and will store each color found in member + * variable \ref colors_. Works on multithreading. + */ void ImageManipulator::get_color_set() { for (int h = 0; h < reference_.size().height; h += thread_nbr) { @@ -211,6 +323,12 @@ void ImageManipulator::get_color_set() colors_.shrink_to_fit(); } +/** + * Will search for every color found in its designated column. If a new color + * is found, pauses all its other similar threads, adds the new color in \ref + * colors_, then resumes the other threads. Helper function for \ref + * get_color_set + */ void ImageManipulator::threaded_get_color(int t_h) { if (t_h > reference_.size().height) { @@ -229,19 +347,15 @@ void ImageManipulator::threaded_get_color(int t_h) } } -void ImageManipulator::adjust_size(cv::Point& t_top_left, - int const size) noexcept -{ - int const shape_total_width = t_top_left.x + size; - int const shape_total_height = t_top_left.y + size; - if (int const diff = shape_total_height + height_; diff > 0) { - t_top_left.x += diff + 1; - } - if (int const diff = shape_total_width + width_; diff > 0) { - t_top_left.x += diff + 1; - } -} - +/** + * Draw a square on the image passed as its argument, following its passed + * coordinates and with the passed color. + * + * \param[out] t_img Image to draw the square to + * \param[in] t_top_left Origin of the square + * \param[in] t_size Size of the square + * \param[in] t_color Color of the square + */ void ImageManipulator::draw_square(cv::Mat& t_img, cv::Point const& t_top_left, int const t_size, @@ -255,6 +369,16 @@ void ImageManipulator::draw_square(cv::Mat& t_img, fillConvexPoly(t_img, points.get(), 4, t_color); } +/** + * Updates the object’s current generated image and difference with its + * reference by replacing them with the arguments passed in this function. This + * function should only be called if the passed elements are improving the + * generated image and reduce the euclidian distance between said image and its + * reference. + * + * \param[in] t_img Image to replace \ref generated_image_ + * \param[in] t_diff New euclidian distance + */ void ImageManipulator::update_gen_image(cv::Mat const& t_img, double const t_diff) { @@ -264,6 +388,22 @@ void ImageManipulator::update_gen_image(cv::Mat const& t_img, spdlog::debug("remaining iter: {}\tdiff: {}", remaining_iter_, diff_); } +void ImageManipulator::merge_tiles( + std::vector> t_tiles) +{ + std::vector columns{}; + for (auto const& col : t_tiles) { + std::vector column_arr{}; + cv::Mat column_img{}; + for (auto const& tile : col) { + column_arr.push_back(tile.get_generated_image()); + } + vconcat(column_arr, column_img); + columns.push_back(std::move(column_img)); + } + hconcat(columns, generated_image_); +} + void ImageManipulator::method1() { spdlog::debug("Beginning method1, initial difference: {}", diff_); @@ -311,6 +451,9 @@ void ImageManipulator::method3() } } +/** + * \param[in] t_controlled_size Enables control over the random squares’ size + */ void ImageManipulator::method4(bool const t_controlled_size) { spdlog::debug("Beginning method4, initial difference: {}", diff_); @@ -334,45 +477,50 @@ void ImageManipulator::method4(bool const t_controlled_size) } } if (values.size() > 0) { - size_t best = 0; - for (size_t i = 0; i < values.size(); ++i) { - if (values[i].second < values[best].second) { - best = i; - } - } - update_gen_image(values[best].first, values[best].second); + auto const pos + = std::min_element(std::begin(values), std::end(values), + [](const auto& elem1, const auto& elem2) { + return elem1.second < elem2.second; + }); + update_gen_image(pos->first, pos->second); + // size_t best = 0; + // for (size_t i = 0; i < values.size(); ++i) { + // if (values[i].second < values[best].second) { + // best = i; + // } + // } + // update_gen_image(values[best].first, values[best].second); } } } +/** + * \param[in] t_controlled_size Enables control over the random squares’ size + * \param[in] t_cols Number of colomns the reference should be divided into + * \param[in] t_rows Number of rows the reference should be divided into + * \param[in] t_submethod Method to be used on each tile + */ void ImageManipulator::method5(bool const t_controlled_size, - int cols, - int const rows, - int const submethod) + int const t_cols, + int const t_rows, + int const t_submethod) { spdlog::debug("Beginning method5, initial difference: {}", diff_); spdlog::debug("nb_total_iter: {}, nb_remaining_iter: {}", total_iterations_, remaining_iter_); spdlog::debug("Running on {} threads", thread_nbr); - if (cols == 0) { - cols = rows; - } - std::vector> tiles{}; - int const tile_width = reference_.cols / cols; - int const tile_height = reference_.rows / rows; - spdlog::debug("tile: width:{}\theight:{}", tile_width, tile_height); - spdlog::debug("image: width:{}\theight:{}", reference_.cols, reference_.rows); - for (int i = 0; i < rows; ++i) { - std::vector tile_row{}; - for (int j = 0; j < cols; ++j) { - tile_row.emplace_back( - reference_, total_iterations_, i * tile_height, j * tile_width, - (i != rows - 1) ? tile_height - : tile_height + reference_.cols % tile_height, - (j != cols - 1) ? tile_width - : tile_width + reference_.cols % tile_width); - } - tiles.push_back(tile_row); - } + auto tiles = generate_tiles((t_cols != 0) ? t_cols : t_rows, t_rows); spdlog::debug("{} tiles", tiles.size()); + + std::vector thread_list{}; + for (auto& row : tiles) { + for (auto& tile : row) { + thread_list.emplace_back( + [&]() { tile.exec_method(t_submethod, t_controlled_size); }); + } + } + for (auto& th : thread_list) { + th.join(); + } + merge_tiles(tiles); } diff --git a/src/parseargs.cc b/src/parseargs.cc index 109659e..8ac44e6 100644 --- a/src/parseargs.cc +++ b/src/parseargs.cc @@ -1,6 +1,5 @@ #include "parseargs.hh" #include -#include #include constexpr int DEFAULT_ITERATIONS = 2000; @@ -8,11 +7,23 @@ constexpr int DEFAULT_ITERATIONS = 2000; using path = std::filesystem::path; namespace po = boost::program_options; -void processFilenames(po::variables_map const& vm, +/** + * \brief Ensures correct output path + * + * Checks if an output file exists, and if yes if it has an extension. In case + * it doesn’t exist, `output_` is appended at the beginning of the input + * filename. If the output path does not have an extension, the type `.png` is + * appended at the end of the path. + * + * \param[in] t_vm Arguments passed to the program + * \param[out] t_input Input path + * \param[out] t_output Output path + */ +void processFilenames(po::variables_map const& t_vm, path const& t_input, path& t_output) { - if (!vm.count("output")) { + if (!t_vm.count("output")) { t_output.replace_filename("output_" + std::string{t_input.filename().string()}); } @@ -21,6 +32,16 @@ void processFilenames(po::variables_map const& vm, } } +/** + * Parses the arguments given to the program, formats them and returns them as + * a tuple. If `-h` or `--help` or a malformed argument is passed, then the + * list of arguments and their comment will be displayed, and the program will + * exit. + * + * \param[in] t_ac Number of arguments passed to the program + * \param[in] t_av Arguments passed to the program + * \return Tuple of path, path, int, int, int, int, int, bool and bool + */ [[nodiscard]] auto parse_args(int t_ac, char** t_av) -> std::tuple {