#include "methods.hh" #include #include #include #include static auto const thread_nbr = std::thread::hardware_concurrency(); /////////////////////////////////////////////////////////////////////////////// // Public // /////////////////////////////////////////////////////////////////////////////// // constructors /////////////////////////////////////////////////////////////// /** * 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_}, generated_image_{other.generated_image_}, shape_{other.shape_}, output_path_{other.output_path_}, diff_{other.diff_}, total_iterations_{other.total_iterations_}, remaining_iter_{other.remaining_iter_}, width_{other.width_}, height_{other.height_} { } /** * 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_)}, generated_image_{std::move(other.generated_image_)}, shape_{std::move( other.shape_)}, output_path_{std::move(other.output_path_)}, diff_{std::move(other.diff_)}, total_iterations_{other.total_iterations_}, remaining_iter_{other.remaining_iter_}, width_{other.width_}, height_{other.height_} { } /** * 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, Shape::ShapeType const t_shape) : reference_{cv::imread(t_input_path, cv::IMREAD_COLOR)}, shape_{Shape{ t_shape}}, 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, Shape::ShapeType const t_shape, 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)})}, shape_{Shape{t_shape}}, total_iterations_{t_iterations} { if (!reference_.data) { spdlog::critical("Could not open or find image!\n"); exit(-1); } } // 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 t_cols = 1, int const t_rows = 0, int const t_submethod = 1) { switch (t_nb_method) { case 1: method1(); break; case 2: method2(); break; case 3: method3(); break; case 4: method4(t_controlled_size); break; case 5: method5(t_controlled_size, t_cols, t_rows, t_submethod); break; default: spdlog::error("Requested method {} is not implemented.", t_nb_method); std::exit(-1); } } /////////////////////////////////////////////////////////////////////////////// // Private // /////////////////////////////////////////////////////////////////////////////// // methods //////////////////////////////////////////////////////////////////// /** * \return cv::Scalar */ [[nodiscard]] auto ImageManipulator::random_color() const noexcept { return cv::Scalar(rand() % 255, rand() % 255, rand() % 255); } void ImageManipulator::create_shape() noexcept { shape_.update(cv::Point{reference_.size().width, reference_.size().height}, std::min(reference_.size().width, reference_.size().height)); } void ImageManipulator::create_controlled_shape() noexcept { float const coef = static_cast(remaining_iter_) / static_cast(total_iterations_); int const min_size = static_cast((static_cast(std::min(reference_.size().width, reference_.size().height)) / 2.0f) * coef); int const max_size = min_size * 2 + 1; shape_.update(cv::Point{reference_.size().width, reference_.size().height}, max_size, min_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) { auto temp_img = generated_image_.clone(); auto const &color = colors_[rand() % colors_.size()]; if (t_controlled_size) { create_controlled_shape(); } else { create_shape(); } draw_shape(temp_img, cv::Scalar{static_cast(color[0]), static_cast(color[1]), static_cast(color[2])}); const auto new_diff = cv::norm(reference_, temp_img); return (new_diff < diff_) ? std::optional>{std::make_pair( std::move(temp_img), new_diff)} : std::nullopt; } /** * Generates views and stores them in a double vector so the tiles (views) are * stored by column top to bottom, and within the columns left to right. * * \param t_cols Number of columns the reference image should be divided into * \param t_rows Number of rows the reference image should be divided into * \return Collection of tiles (vector>) */ [[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_, shape_.get_type(), 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) { std::vector thread_list{}; for (auto i = 0u; i < thread_nbr; ++i) { thread_list.push_back( std::thread(&ImageManipulator::threaded_get_color, this, h + i)); } std::for_each(thread_list.begin(), thread_list.end(), [](auto &th) { th.join(); }); } 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 const t_h) { if (t_h > reference_.size().height) { return; } for (int w = 0; w < reference_.size().width; w += 3) { std::array temp = {reference_.at(t_h, w), reference_.at(t_h, w + 1), reference_.at(t_h, w + 2)}; auto pos = std::find(std::begin(colors_), std::end(colors_), temp); if (pos == std::end(colors_)) { std::lock_guard lock(colors_mutex_); colors_.push_back(std::move(temp)); } } } void ImageManipulator::draw_shape(cv::Mat &t_img, cv::Scalar &&t_color) { fillConvexPoly(t_img, shape_.get_points().data(), shape_.get_nb_points(), 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) { diff_ = t_diff; t_img.copyTo(generated_image_); --remaining_iter_; spdlog::debug("remaining iter: {}\tdiff: {}", remaining_iter_, diff_); } /** * Merges the tiles generated by \ref method5 into a single image. The tiles * are organized by column top to bottom, within each they are stored in order, * left to right. They will be merged in \ref generated_image_. * * \param t_tiles Collection of tiles to be merged together */ void ImageManipulator::merge_tiles( std::vector> const &t_tiles) { std::vector columns{}; std::for_each(t_tiles.begin(), t_tiles.end(), [&columns](auto const &col) { std::vector column_arr{}; cv::Mat column_img{}; std::for_each(col.begin(), col.end(), [&column_arr](auto const &tile) { 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_); while (remaining_iter_ > 0 && diff_ > 0.0) { auto temp_image = generated_image_.clone(); create_shape(); draw_shape(temp_image, random_color()); if (auto const new_diff = cv::norm(reference_, temp_image); new_diff < diff_) { update_gen_image(temp_image, new_diff); } } } void ImageManipulator::method2() { spdlog::debug("Beginning method2, initial difference: {}", diff_); spdlog::debug("Running on {} threads", thread_nbr); get_color_set(); spdlog::debug("{} colors detected", colors_.size()); while (remaining_iter_ > 0 && diff_ > 0.0) { if (auto result = create_candidate(); result.has_value()) { update_gen_image(result->first, result->second); } } } void ImageManipulator::method3() { spdlog::debug("Beginning method3, initial difference: {}", diff_); spdlog::debug("Running on {} threads", thread_nbr); get_color_set(); spdlog::debug("{} colors detected", colors_.size()); while (remaining_iter_ > 0 && diff_ > 0.0) { auto temp_image = generated_image_.clone(); if (auto result = create_candidate(true); result.has_value()) { update_gen_image(result->first, result->second); } } } /** * \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_); spdlog::debug("Running on {} threads", thread_nbr); get_color_set(); spdlog::debug("{} colors detected", colors_.size()); while (remaining_iter_ > 0 && diff_ > 0.0) { std::vector>>> results{}; std::vector> values{}; // launch asynchronously candidate image generation for (size_t i = 0; i < thread_nbr; ++i) { results.push_back(std::async(std::launch::async, &ImageManipulator::create_candidate, this, t_controlled_size)); } // if candidate is a success, store it std::for_each(results.begin(), results.end(), [&values, this](auto &elem) { if (auto res = elem.get(); res.has_value() && res->second < this->diff_) { values.push_back(*res); } }); // apply best candidate if (values.size() > 0) { 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); } } } /** * \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 const t_cols, int const t_rows, int const t_submethod) { spdlog::debug("Beginning method5, initial difference: {}", diff_); spdlog::debug("Running on {} threads", thread_nbr); auto tiles = generate_tiles((t_cols != 0) ? t_cols : t_rows, t_rows); spdlog::debug("{} tiles", tiles.size()); std::vector thread_list{}; std::for_each(tiles.begin(), tiles.end(), [&](auto &row) { std::for_each(row.begin(), row.end(), [&](auto &tile) { thread_list.emplace_back( [&]() { tile.exec_method(t_submethod, t_controlled_size); }); }); }); std::for_each(thread_list.begin(), thread_list.end(), [](auto &th) { th.join(); }); merge_tiles(tiles); }