genetic-images/src/methods.cc

418 lines
16 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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 inputs
* 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 views origin (top left)
* \param[in] t_y Y value of the views 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 objects 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);
}