418 lines
16 KiB
C++
418 lines
16 KiB
C++
#include "methods.hh"
|
||
|
||
#include <algorithm>
|
||
#include <future>
|
||
#include <optional>
|
||
#include <thread>
|
||
|
||
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<float>(remaining_iter_)
|
||
/ static_cast<float>(total_iterations_);
|
||
int const min_size
|
||
= static_cast<int>((static_cast<float>(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<double>(color[0]),
|
||
static_cast<double>(color[1]),
|
||
static_cast<double>(color[2])});
|
||
const auto new_diff = cv::norm(reference_, temp_img);
|
||
return (new_diff < diff_)
|
||
? std::optional<std::pair<cv::Mat, double>>{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<vector<ImageManipulator>>)
|
||
*/
|
||
[[nodiscard]] auto ImageManipulator::generate_tiles(int const t_cols,
|
||
int const t_rows) const
|
||
{
|
||
std::vector<std::vector<ImageManipulator>> 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<ImageManipulator> 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<std::thread> 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<uchar, 3> temp
|
||
= {reference_.at<uchar>(t_h, w), reference_.at<uchar>(t_h, w + 1),
|
||
reference_.at<uchar>(t_h, w + 2)};
|
||
auto pos = std::find(std::begin(colors_), std::end(colors_), temp);
|
||
if (pos == std::end(colors_)) {
|
||
std::lock_guard<std::mutex> 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<std::vector<ImageManipulator>> const &t_tiles)
|
||
{
|
||
std::vector<cv::Mat> columns{};
|
||
std::for_each(t_tiles.begin(), t_tiles.end(), [&columns](auto const &col) {
|
||
std::vector<cv::Mat> 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<std::future<std::optional<std::pair<cv::Mat, double>>>>
|
||
results{};
|
||
std::vector<std::pair<cv::Mat, double>> 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<std::thread> 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);
|
||
}
|