Browse Source

code cleanup, started report, new verbose option

master
Phuntsok Drak-pa 1 year ago
parent
commit
eb1046603d
15 changed files with 2670 additions and 62 deletions
  1. +1
    -0
      CMakeLists.txt
  2. +11
    -2
      README.org
  3. +5
    -0
      doc/.gitignore
  4. +2494
    -0
      doc/Doxyfile
  5. BIN
      img/mahakala-monochrome.jpg
  6. +2
    -5
      include/genimg/common.hh
  7. +3
    -3
      include/genimg/methods.hh
  8. +1
    -2
      include/genimg/parseargs.hh
  9. +3
    -0
      report/.gitignore
  10. +91
    -0
      report/report.org
  11. BIN
      report/report.pdf
  12. +5
    -10
      src/common.cc
  13. +9
    -7
      src/main.cc
  14. +40
    -30
      src/methods.cc
  15. +5
    -3
      src/parseargs.cc

+ 1
- 0
CMakeLists.txt View File

@@ -17,6 +17,7 @@ conan_basic_setup()
enable_cxx_compiler_flag_if_supported("-Wall")
enable_cxx_compiler_flag_if_supported("-pedantic")
enable_cxx_compiler_flag_if_supported("-O3")
enable_cxx_compiler_flag_if_supported("-flto")

# include_directories(<PUBLIC HEADER DIRECTORIES>)
set(TGT genetic-image)


+ 11
- 2
README.org View File

@@ -53,7 +53,7 @@ use it, first install clang-7 and lldb 7, then run this:
conan profile new default --detect
conan profile update settings.compiler=clang default
conan profile update settings.compiler.version=7.0 default
conan profile update settings.compiler.libcxx=libstdc++ default
conan profile update settings.compiler.libcxx=libstdc++11 default
conan profile update env.CC=/bin/clang default
conan profile update env.CXX=/bin/clang++ default
#+end_src
@@ -65,7 +65,7 @@ Then, To build and run the program, go to the root of the project and run this:
#+begin_src shell
mkdir build && cd build
conan install .. --build missing
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .. -G Ninja
cmake -DCMAKE_CXX_COMPILER=clang++ .. -G Ninja
cmake --build .
#+end_src
If you want to use another profile than your default one, you should run the
@@ -74,6 +74,15 @@ following line instead of the second line:
conan install .. --build missing --profile <your_profile>
#+end_src

If you do not wish to build your project with Ninja but with another generator,
such as Unix Makefiles, simply replace ~Ninja~ in the second to last ~cmake~
command with the name of your generator. For instance:
#+begin_src shell
cmake -DCMAKE_CXX_COMPILER=clang++ .. -G "Unix Makefiles"
#+end_src
You can still build your project by running ~cmake --build .~ or by running
~make~ manually.

This project was built and tested using clang-7, lldb and gdb on Void Linux
(kernel 4.19) and Arch Linux (kernel 5.0).



+ 5
- 0
doc/.gitignore View File

@@ -0,0 +1,5 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
!Doxyfile

+ 2494
- 0
doc/Doxyfile
File diff suppressed because it is too large
View File


BIN
img/mahakala-monochrome.jpg View File

Before After
Width: 275  |  Height: 275  |  Size: 68 KiB

+ 2
- 5
include/genimg/common.hh View File

@@ -5,13 +5,10 @@
#include <opencv2/highgui/highgui.hpp>
#include <spdlog/spdlog.h>
#include <string>
#include <tuple>
#include <random>
#include <utility>

std::tuple<cv::Mat, cv::Mat> init_image(std::string const &);
std::pair<cv::Mat, cv::Mat> init_image(std::string const &);

double euclidian_distance(cv::Mat const &, cv::Mat const &);

cv::Scalar random_color(std::mt19937 &);

#endif /* GENETIC_IMAGE_INCLUDE_GENIMG_COMMON_HH_ */

include/genimg/method1.hh → include/genimg/methods.hh View File

@@ -1,5 +1,5 @@
#ifndef GENETIC_IMAGE_INCLUDE_GENIMG_METHOD1_METHOD1_HH_
#define GENETIC_IMAGE_INCLUDE_GENIMG_METHOD1_METHOD1_HH_
#ifndef GENETIC_IMAGE_INCLUDE_GENIMG_METHODS_HH_
#define GENETIC_IMAGE_INCLUDE_GENIMG_METHODS_HH_

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
@@ -12,4 +12,4 @@ void method1(cv::Mat &t_reference, cv::Mat &t_output, int t_iterations,
void method2(cv::Mat &t_reference, cv::Mat &t_output, int t_iterations,
std::mt19937 &t_gen);

#endif /* GENETIC_IMAGE_INCLUDE_GENIMG_METHOD1_METHOD1_HH_ */
#endif /* GENETIC_IMAGE_INCLUDE_GENIMG_METHODS_HH_ */

+ 1
- 2
include/genimg/parseargs.hh View File

@@ -2,10 +2,9 @@
#define GENETIC_IMAGE_INCLUDE_GENIMG_PARSEARGS_HH_

#include <filesystem>
#include <string>
#include <tuple>

std::tuple<std::filesystem::path, std::filesystem::path, bool, int, int>
std::tuple<std::filesystem::path, std::filesystem::path, bool, int, int, bool>
parse_args(int, char **);

#endif /* GENETIC_IMAGE_INCLUDE_GENIMG_PARSEARGS_HH_ */

+ 3
- 0
report/.gitignore View File

@@ -0,0 +1,3 @@
auto/
_minted*/
*.tex

+ 91
- 0
report/report.org View File

@@ -0,0 +1,91 @@
#+TITLE: Création d’images par algorithme génétique avec référence
#+SUBTITLE: Rapport de projet
#+AUTHOR: Lucien Cartier-Tilet
#+EMAIL: [email protected]
#+CREATOR: Lucien Cartier-Tilet
#+LANGUAGE: fr
#+LATEX_CLASS: article
#+LaTeX_CLASS_OPTIONS: [a4paper,twoside]
#+LATEX_HEADER: \usepackage{xltxtra,fontspec,xunicode}\usepackage[total={6.5in,9.5in}]{geometry}\setromanfont[Numbers=Lowercase]{Charis SIL}
#+LATEX_HEADER: \usepackage{xcolor} \usepackage{hyperref}
#+LATEX_HEADER: \hypersetup{colorlinks=true,linkbordercolor=red,linkcolor=blue,pdfborderstyle={/S/U/W 1}}
#+STARTUP: latexpreview

* Sujet

Le sujet de ce projet est la création d’un logiciel pouvant recréer une image
fournie grâce à des générations aléatoires et successives de formes aux,
positions, couleurs et taille aléatoires. L’algorithme commence par créer une
image vide aux dimensions identiques à l’image de référence, puis applique une
de ces formes aléatoires. Si la ressemblance de l’image ainsi générée augmente
par rapport à sa version précédente par rapport à l’image de référence, alors
cette modification est conservée, sinon elle est annulée. Répéter jusqu’à
satisfaction.

* Les méthodes utilisées

Plusieurs approches au problème sont possibles, allant de la simple
implémentation naïve du problème à des moyen pouvant au moins décupler la
vitesse de génération de l’image. Sauf indication contraire, j’ai utilisé dans
l’implémentation de chaque méthode des carrés comme forme d’éléments appliqués
aléatoirement à l’image.

Pour évaluer la ressemblance entre deux image, j’évalue une distance euclidienne
entre le vecteur de leurs pixels qui peut se résumer à ceci :
#+begin_export latex
$$\sqrt{\sum_{i=0}^{n} V_{i}^{2}+W_{i}^{2}}$$
#+end_export
~V~ étant le vecteur de pixels de l’image de référence, ~W~ étant le vecteur de
pixels de l’image générée, et ~n~ la taille de ces deux vecteurs.

Les tests de temps sont réalisés sur un Thinkpad x220, disposant d’un processeur
Intel® Core™ i5-2540M à 2.6GHz, composé de deux cœurs supportant chacun deux
threads, et de 4Go de RAM. Le programme est compilé avec les options
d’optimisation ~-O3~ et ~-flto~.

Voici également ci-dessous la liste des options et arguments possibles
concernant l’exécution du logiciel.
#+begin_src text
$ ./bin/genetic-image -h
Allowed options:
-h [ --help ] Display this help message
-i [ --input ] arg Input image
-o [ --output ] arg Image or video output path (default: input path +
"_output")
-m [ --method ] arg Method number to be used (default: 1)
-n [ --iterations ] arg Number of iterations (default: 5000)
-v [ --video ] Enable video output
#+end_src

** Méthode naïve

J’ai tout d’abord implémenté la méthode naïve afin d’avoir une référence en
matière de temps. Cette dernière est implémentée dans ~src/methods.cc~ avec la
fonction ~method1()~. Comme ce à quoi je m’attendais, cette méthode de
génération d’images est très lente, principalement dû au fait que l’algorithme
en l’état essaiera d’appliquer des couleurs n’existant pas dans l’image de
référence, voire complètement à l’opposées de la palette de couleurs de l’image
de référence.

Voici la ligne de commande utilisée depuis le répertoire ~build~ afin de pouvoir
obtenir un temps d’exécution :
#+begin_src shell
perf stat -r nombreDExécutions -B ./bin/genetic-image \
-i ../img/mahakala-monochrome.jpg -o output.png -n 200 -m 1
#+end_src

| / | < | < |
| nombre d’itérations réussies | nombre d’exécutions | temps d’exécution moyen |
|------------------------------+---------------------+-------------------------|
| 10 | 100 | 0.09447s (±0.02%) |
| 50 | 100 | 1.1331s (±2.85%) |
| 100 | 50 | |
| 200 | 20 | |
| 500 | 10 | |
| 1000 | 5 | |

Naturellement, la variation en temps d’exécution croît en même temps que le
nombre d’améliorations nécessaires à apporter à l’image à améliorer, dû à la
nature aléatoire de l’algorithme. Cependant, on constate également une
croissance importante du temps d’exécution suivant également ce nombre
d’itérations réussies.

BIN
report/report.pdf View File


+ 5
- 10
src/common.cc View File

@@ -3,18 +3,18 @@
#include <cmath>
#include <cstdlib>

std::tuple<cv::Mat, cv::Mat> init_image(std::string const &t_input_file) {
std::pair<cv::Mat, cv::Mat> init_image(std::string const &t_input_file) {
cv::Mat input_image = cv::imread(t_input_file, cv::IMREAD_COLOR);
if (!input_image.data) {
spdlog::critical("Could not open or find image!\n");
exit(-1);
}
spdlog::info("Image loaded!");
spdlog::info("Width:\t\t{}", input_image.size().width);
spdlog::info("Height:\t{}", input_image.size().height);
spdlog::debug("Image loaded!");
spdlog::debug("Width:\t\t{}", input_image.size().width);
spdlog::debug("Height:\t{}", input_image.size().height);
cv::Mat process_image(input_image.size().height, input_image.size().width,
CV_8UC3, cv::Scalar(0, 0, 0));
return std::make_tuple(std::move(input_image), process_image);
return std::make_pair(std::move(input_image), process_image);
}

double euclidian_distance(cv::Mat const &t_img1, cv::Mat const &t_img2) {
@@ -28,8 +28,3 @@ double euclidian_distance(cv::Mat const &t_img1, cv::Mat const &t_img2) {
euclidian = std::sqrt(euclidian);
return euclidian;
}

cv::Scalar random_color(std::mt19937 &t_gen) {
static std::uniform_int_distribution<> dis(0, 255);
return cv::Scalar(dis(t_gen), dis(t_gen), dis(t_gen));
}

+ 9
- 7
src/main.cc View File

@@ -1,14 +1,16 @@
#include "common.hh"
#include "method1.hh"
#include "methods.hh"
#include "parseargs.hh"
#include <iostream>

int main(int ac, char **av) {
auto const [input_file, output_file, video_output, iterations, method] =
parse_args(ac, av);
spdlog::info("Input file:\t{}", input_file.native());
spdlog::info("Output file:\t{}", output_file.native());
spdlog::info("Video output:\t{}", video_output);
spdlog::info("Iterations:\t{}", iterations);
auto const [input_file, output_file, video_output, iterations, method,
verbose] = parse_args(ac, av);
spdlog::set_level(verbose ? spdlog::level::debug : spdlog::level::info);
spdlog::debug("Input file:\t{}", input_file.native());
spdlog::debug("Output file:\t{}", output_file.native());
spdlog::debug("Video output:\t{}", video_output);
spdlog::debug("Iterations:\t{}", iterations);
auto [input_image, process_image] = init_image(input_file.native());
std::random_device rd;
std::mt19937 gen(rd());


src/method1.cc → src/methods.cc View File

@@ -1,4 +1,4 @@
#include "method1.hh"
#include "methods.hh"
#include "common.hh"
#include "drawing.hh"
#include <algorithm>
@@ -13,34 +13,21 @@ using randint = std::uniform_int_distribution<>;
using Color = std::array<uchar, 3>;
using ColorSet = std::vector<Color>;

namespace methods_private {

cv::Scalar randomColor(std::mt19937 &t_gen) {
static std::uniform_int_distribution<> dis(0, 255);
return cv::Scalar(dis(t_gen), dis(t_gen), dis(t_gen));
}

void newSquare1(cv::Mat &t_process_img, std::mt19937 &t_gen,
randint &t_rand_pos) {
const int square_size = t_rand_pos(t_gen);
auto square_top_left = cv::Point{t_rand_pos(t_gen), t_rand_pos(t_gen)};
draw_shape(t_process_img, square_top_left, square_size, random_color(t_gen),
draw_shape(t_process_img, square_top_left, square_size, randomColor(t_gen),
Shapes::Square);
}

void method1(cv::Mat &t_reference, cv::Mat &t_output, int t_iterations,
std::mt19937 &t_gen) {
auto diff = euclidian_distance(t_reference, t_output);
auto const max_size =
std::max(t_reference.size().width, t_reference.size().height);
randint dist(0, max_size);
spdlog::info("Beginning method1, initial difference: {}", diff);
while (t_iterations > 0) {
auto temp_image = t_output.clone();
newSquare1(temp_image, t_gen, dist);
if (auto new_diff = euclidian_distance(t_reference, temp_image);
new_diff < diff) {
diff = new_diff;
temp_image.copyTo(t_output);
--t_iterations;
spdlog::info("Iteration {}: diff {}", t_iterations, diff);
}
}
}

void threadedGetColor(cv::Mat &t_reference, ColorSet &t_colors, int t_h) {
if (t_h > t_reference.size().height)
return;
@@ -62,8 +49,9 @@ ColorSet getColorSet(cv::Mat &t_reference) {
for (int h = 0; h < t_reference.size().height; h += thread_nbr) {
std::vector<std::thread> thread_list{};
for (int i = 0; i < thread_nbr; ++i) {
thread_list.push_back(std::thread(threadedGetColor, std::ref(t_reference),
std::ref(res), h + i));
thread_list.push_back(std::thread(methods_private::threadedGetColor,
std::ref(t_reference), std::ref(res),
h + i));
}
for (auto &th : thread_list)
th.join();
@@ -85,26 +73,48 @@ void newSquare2(cv::Mat &t_process_img, std::mt19937 &t_gen,
Shapes::Square);
}

} // namespace methods_private

void method1(cv::Mat &t_reference, cv::Mat &t_output, int t_iterations,
std::mt19937 &t_gen) {
auto diff = euclidian_distance(t_reference, t_output);
auto const max_size =
std::max(t_reference.size().width, t_reference.size().height);
randint dist(0, max_size);
spdlog::debug("Beginning method1, initial difference: {}", diff);
while (t_iterations > 0) {
auto temp_image = t_output.clone();
methods_private::newSquare1(temp_image, t_gen, dist);
if (auto new_diff = euclidian_distance(t_reference, temp_image);
new_diff < diff) {
diff = new_diff;
temp_image.copyTo(t_output);
--t_iterations;
spdlog::debug("Iteration {}: diff {}", t_iterations, diff);
}
}
}

void method2(cv::Mat &t_reference, cv::Mat &t_output, int t_iterations,
std::mt19937 &t_gen) {
auto diff = euclidian_distance(t_reference, t_output);
auto const max_size =
std::max(t_reference.size().width, t_reference.size().height);
randint dist(0, max_size);
spdlog::info("Beginning method2, initial difference: {}", diff);
auto const colors = getColorSet(t_reference);
spdlog::info("Running {} threads.", thread_nbr);
spdlog::info("{} colors detected.", colors.size());
spdlog::debug("Beginning method2, initial difference: {}", diff);
auto const colors = methods_private::getColorSet(t_reference);
spdlog::debug("Running {} threads.", thread_nbr);
spdlog::debug("{} colors detected.", colors.size());
randint rand_color(0, colors.size());
while (t_iterations > 0) {
auto temp_image = t_output.clone();
newSquare2(temp_image, t_gen, colors, dist, rand_color);
methods_private::newSquare2(temp_image, t_gen, colors, dist, rand_color);
if (auto new_diff = euclidian_distance(t_reference, temp_image);
new_diff < diff) {
diff = new_diff;
temp_image.copyTo(t_output);
--t_iterations;
spdlog::info("Iteration {}: diff {}", t_iterations, diff);
spdlog::debug("Iteration {}: diff {}", t_iterations, diff);
}
}
}

+ 5
- 3
src/parseargs.cc View File

@@ -21,7 +21,7 @@ void processFilenames(po::variables_map const &vm, path const &t_input,
}
}

std::tuple<path, path, bool, int, int> parse_args(int t_ac, char **t_av) {
std::tuple<path, path, bool, int, int, bool> parse_args(int t_ac, char **t_av) {
po::options_description desc("Allowed options");
desc.add_options()
("help,h", "Display this help message")
@@ -30,7 +30,8 @@ std::tuple<path, path, bool, int, int> parse_args(int t_ac, char **t_av) {
"Image or video output path (default: input path + \"_output\")")
("method,m", po::value<int>(), "Method number to be used (default: 1)")
("iterations,n", po::value<int>(), "Number of iterations (default: 5000)")
("video,v", "Enable video output");
("video,V", "Enable video output")
("verbose,v", "Enables verbosity");
po::variables_map vm;
po::store(po::parse_command_line(t_ac, t_av, desc), vm);
po::notify(vm);
@@ -49,5 +50,6 @@ std::tuple<path, path, bool, int, int> parse_args(int t_ac, char **t_av) {
output_path,
vm.count("video") ? true : false,
vm.count("iterations") ? vm["iterations"].as<int>() : DEFAULT_ITERATIONS,
vm.count("method") ? vm["method"].as<int>() : 1);
vm.count("method") ? vm["method"].as<int>() : 1,
vm.count("verbose") ? true : false);
}

Loading…
Cancel
Save