m2b-gd-tp8/astico2d.cpp

692 lines
21 KiB
C++

/*
Module d'interface utilisateur pour Astico2D :
Atelier Simple de Transformations d'Images au Clavier avec OpenCV en 2D
CC BY-SA Edouard.Thiel@univ-amu.fr - 04/09/2021 - v1.0
*/
#include "astico2d.hpp"
//---------------------------- C O U L E U R S --------------------------------
void representer_en_couleurs_vga (cv::Mat &img_niv, cv::Mat &img_coul)
{
CHECK_MAT_TYPE(img_niv, CV_32SC1);
CHECK_MAT_TYPE(img_coul, CV_8UC3);
unsigned char couls[16][3] = { // R, G, B
{ 0, 0, 0 }, // 0 black -> 0 uniquement
{ 30, 30, 230 }, // 1 blue -> 1, 15, 29, ...
{ 30, 200, 30 }, // 2 green -> 2, 16, 30, ...
{ 30, 200, 200 }, // 3 cyan -> 3, 17, 31, ...
{ 200, 30, 30 }, // 4 red -> 4, 18, 32, ...
{ 200, 30, 200 }, // 5 magenta -> 5, 19, 33, ...
{ 200, 130, 50 }, // 6 brown -> 6, 20, 34, ...
{ 200, 200, 200 }, // 7 light gray -> 7, 21, 35, ...
{ 110, 110, 140 }, // 8 dark gray -> 8, 22, 36, ...
{ 84, 130, 252 }, // 9 light blue -> 9, 23, 37, ...
{ 84, 252, 84 }, // 10 light green -> 10, 24, 38, ...
{ 84, 252, 252 }, // 11 light cyan -> 11, 25, 39, ...
{ 252, 84, 84 }, // 12 light red -> 12, 26, 40, ...
{ 252, 84, 252 }, // 13 light magenta -> 13, 27, 41, ...
{ 252, 252, 84 }, // 14 yellow -> 14, 28, 42, ...
{ 252, 252, 252 }, // 15 white -> 255 uniquement
};
for (int y = 0; y < img_niv.rows; y++)
for (int x = 0; x < img_niv.cols; x++)
{
int g = img_niv.at<int>(y,x), c = 0;
if (g == 255) c = 15; // seul 255 est blanc
else if (g != 0) c = 1 + abs(g-1) % 14; // seul 0 est noir
// Attention img_coul est en B, G, R -> inverser les canaux
img_coul.at<cv::Vec3b>(y,x)[0] = couls[c][2];
img_coul.at<cv::Vec3b>(y,x)[1] = couls[c][1];
img_coul.at<cv::Vec3b>(y,x)[2] = couls[c][0];
}
}
void inverser_couleurs (cv::Mat &img)
{
CHECK_MAT_TYPE(img, CV_8UC3);
for (int y = 0; y < img.rows; y++)
for (int x = 0; x < img.cols; x++)
{
img.at<cv::Vec3b>(y,x)[0] = 255 - img.at<cv::Vec3b>(y,x)[0];
img.at<cv::Vec3b>(y,x)[1] = 255 - img.at<cv::Vec3b>(y,x)[1];
img.at<cv::Vec3b>(y,x)[2] = 255 - img.at<cv::Vec3b>(y,x)[2];
}
}
//--------------------------------- L O U P E ---------------------------------
void Loupe::reborner (cv::Mat &res1, cv::Mat &res2)
{
int bon_zoom = zoom >= 1 ? zoom : 1;
int h = res2.rows / bon_zoom;
int w = res2.cols / bon_zoom;
if (zoom_x0 < 0) zoom_x0 = 0;
zoom_x1 = zoom_x0 + w;
if (zoom_x1 > res1.cols) {
zoom_x1 = res1.cols;
zoom_x0 = zoom_x1 - w;
if (zoom_x0 < 0) zoom_x0 = 0;
}
if (zoom_y0 < 0) zoom_y0 = 0;
zoom_y1 = zoom_y0 + h;
if (zoom_y1 > res1.rows) {
zoom_y1 = res1.rows;
zoom_y0 = zoom_y1 - h;
if (zoom_y0 < 0) zoom_y0 = 0;
}
}
void Loupe::deplacer (cv::Mat &res1, cv::Mat &res2, int dx, int dy)
{
zoom_x0 += dx; zoom_y0 += dy;
zoom_x1 += dx; zoom_y1 += dy;
reborner (res1, res2);
}
int Loupe::calculer_zoom_optimal (cv::Mat &src)
{
// On autorise l'image à dépasser de 10%
int zh = opt_h * 110 / (src.rows * 100),
zw = opt_w * 110 / (src.cols * 100);
int z = zh < zw ? zh : zw;
if (z < 1) z = 1; else if (z > zoom_max) z = zoom_max;
return z;
}
void Loupe::calculer_portion_optimale (cv::Mat &src, int &ph, int &pw)
{
ph = ((src.rows * zoom - 1) / vue_inc +1) * vue_inc;
pw = ((src.cols * zoom - 1) / vue_inc +1) * vue_inc;
if (ph < vue_min) ph = vue_min; else if (ph > opt_h) ph = opt_h;
if (pw < vue_min) pw = vue_min; else if (pw > opt_w) pw = opt_w;
}
void Loupe::dessiner_rect (cv::Mat &src, cv::Mat &dest)
{
dest = src.clone();
if (zoom == 0) return;
cv::Point p0 = cv::Point(zoom_x0, zoom_y0),
p1 = cv::Point(zoom_x1, zoom_y1);
cv::rectangle(dest, p0, p1, cv::Scalar (255, 255, 255), 3, 4);
cv::rectangle(dest, p0, p1, cv::Scalar ( 0, 0, 255), 1, 4);
}
void Loupe::dessiner_portion (cv::Mat &src, cv::Mat &dest)
{
CHECK_MAT_TYPE(src, CV_8UC3);
int bon_zoom = zoom >= 1 ? zoom : 1;
for (int y = 0; y < dest.rows; y++)
for (int x = 0; x < dest.cols; x++)
{
int x0 = zoom_x0 + x / bon_zoom;
int y0 = zoom_y0 + y / bon_zoom;
if (x0 < 0 || x0 >= src.cols || y0 < 0 || y0 >= src.rows) {
dest.at<cv::Vec3b>(y,x)[0] = 64;
dest.at<cv::Vec3b>(y,x)[1] = 64;
dest.at<cv::Vec3b>(y,x)[2] = 64;
continue;
}
dest.at<cv::Vec3b>(y,x)[0] = src.at<cv::Vec3b>(y0,x0)[0];
dest.at<cv::Vec3b>(y,x)[1] = src.at<cv::Vec3b>(y0,x0)[1];
dest.at<cv::Vec3b>(y,x)[2] = src.at<cv::Vec3b>(y0,x0)[2];
}
}
void Loupe::afficher_tableau_niveaux (cv::Mat &src, int ex, int ey, int rx, int ry)
{
CHECK_MAT_TYPE(src, CV_32SC1);
int bon_zoom = zoom >= 1 ? zoom : 1;
int cx = zoom_x0 + ex / bon_zoom;
int cy = zoom_y0 + ey / bon_zoom;
int x1 = cx-rx, x2 = cx+rx, y1 = cy-ry, y2 = cy+ry;
int vmin = 0, vmax = 0;
for (int y = y1; y <= y2; y++) {
for (int x = x1; x <= x2; x++) {
if (x < 0 || x >= src.cols || y < 0 || y >= src.rows)
continue;
int v = src.at<int>(y,x);
if (v < vmin) vmin = v;
else if (v > vmax) vmax = v;
}
}
int n1 = ceil(log10(abs(vmin)+1)), n2 = ceil(log10(abs(vmax)+1));
if (vmin < 0) n1++; // signe
if (vmax < 0) n2++;
int n = std::max(n1,n2); // nb de chiffres
std::cout << "Valeurs autour de " << cx << "," << cy << " :" << std::endl;
for (int y = y1; y <= y2; y++) {
for (int x = x1; x <= x2; x++) {
if (x < 0 || x >= src.cols || y < 0 || y >= src.rows)
std::cout << std::setw(n+1) << "* " ; // hors de l'image
else std::cout << std::setw(n) << src.at<int>(y,x) << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
}
void Loupe::afficher_tableau_couleurs (cv::Mat &src, int ex, int ey, int rx, int ry)
{
CHECK_MAT_TYPE(src, CV_8UC3);
int bon_zoom = zoom >= 1 ? zoom : 1;
int cx = zoom_x0 + ex / bon_zoom;
int cy = zoom_y0 + ey / bon_zoom;
int x1 = cx-rx, x2 = cx+rx, y1 = cy-ry, y2 = cy+ry;
int n = 3; // nb de chiffres
std::cout << "Valeurs autour de " << cx << "," << cy << " :" << std::endl;
for (int y = y1; y <= y2; y++) {
for (int x = x1; x <= x2; x++) {
if (x < 0 || x >= src.cols || y < 0 || y >= src.rows)
// hors de l'image
std::cout << "("
<< std::setw(n) << "* " << ","
<< std::setw(n) << "* " << ","
<< std::setw(n) << "* " << ") ";
else {
int cR = src.at<cv::Vec3b>(y,x)[2],
cV = src.at<cv::Vec3b>(y,x)[1],
cB = src.at<cv::Vec3b>(y,x)[0];
std::cout << "("
<< std::setw(n) << cR << ","
<< std::setw(n) << cV << ","
<< std::setw(n) << cB << ") ";
}
}
std::cout << std::endl;
}
std::cout << std::endl;
}
//----------------------------- A S T I C O 2 D -------------------------------
Astico2D::Astico2D (int &argc, char **&argv) : temps()
{
if (analyser_arguments (argc, argv) != 0) { init_ok = false ; return; }
if (lire_image_entree () != 0) { init_ok = false ; return; }
creer_images_resultat ();
creer_fenetres ();
}
int Astico2D::run ()
{
if (! init_ok) return 1;
afficher_touches_clavier ();
traiter_evenements ();
return (enregistrer_image_resultat () == 0) ? 0 : 1;
}
void Astico2D::afficher_usage (const char *nom_prog) {
std::cout << "Usage: " << nom_prog
<< " [-mag w h] [-thr seuil] [-o image_res] image_src"
<< " [autres options]"
<< std::endl;
}
int Astico2D::analyser_arguments (int &argc, char **&argv)
{
nom_out2 = NULL;
char *nom_prog = argv[0];
argc -= 1; argv += 1;
while (argc > 0) {
if (!strcmp(argv[0], "-mag")) {
if (argc-1 < 3) { afficher_usage(nom_prog); return 1; }
zoom_w = atoi(argv[1]);
zoom_h = atoi(argv[2]);
argc -= 3; argv += 3;
} else if (!strcmp(argv[0], "-thr")) {
if (argc-1 < 2) { afficher_usage(nom_prog); return 1; }
seuil_niv = atoi(argv[1]);
argc -= 2; argv += 2;
} else if (!strcmp(argv[0], "-o")) {
if (argc-1 < 2) { afficher_usage(nom_prog); return 1; }
nom_out2 = argv[1];
argc -= 2; argv += 2;
} else break;
}
if (argc < 1) { afficher_usage(nom_prog); return 1; }
nom_in1 = argv[0];
argc -= 1; argv += 1;
// Les arguments supplémentaires restent dans argc,argv et peuvent être
// analysés dans le constructeur d'une classe fille après l'appel du
// constructeur de la classe mère.
return 0;
}
int Astico2D::lire_image_entree ()
{
img_src = cv::imread (nom_in1, cv::IMREAD_COLOR); // produit du 8UC3
if (img_src.empty()) {
std::cout << "Erreur de lecture de \"" << nom_in1 << "\","
<< " ou format incompatible de l'image." << std::endl;
return -1;
}
return 0;
}
int Astico2D::enregistrer_image_resultat ()
{
if (!nom_out2) return 0;
int res = cv::imwrite (nom_out2, img_coul);
if (res) std::cout << "Enregistrement reussi";
else std::cout << "Erreur d'enregistrement";
std::cout << " dans \"" << nom_out2 << "\"." << std::endl;
return res ? 0 : -1;
}
void Astico2D::creer_images_resultat ()
{
img_res1 = cv::Mat(img_src.rows, img_src.cols, CV_8UC3);
img_res2 = cv::Mat(zoom_h, zoom_w, CV_8UC3);
img_niv = cv::Mat(img_src.rows, img_src.cols, CV_32SC1);
img_coul = cv::Mat(img_src.rows, img_src.cols, CV_8UC3);
loupe.reborner(img_res1, img_res2);
}
void Astico2D::creer_slider (const char* nom_slider, int *valeur_courante,
int valeur_max)
{
cv::createTrackbar (nom_slider, "ImageSrc", valeur_courante, valeur_max,
onDefaultTrackbarSlide, this);
}
void Astico2D::modifier_slider (const char* nom_slider, int nouvelle_valeur)
{
cv::setTrackbarPos (nom_slider, "ImageSrc", nouvelle_valeur);
}
void Astico2D::diminuer_slider (const char* nom_slider, int valeur_min)
{
int nouv = cv::getTrackbarPos (nom_slider, "ImageSrc") - 1;
if (nouv >= valeur_min) modifier_slider (nom_slider, nouv);
}
void Astico2D::augmenter_slider (const char* nom_slider, int valeur_max)
{
int nouv = cv::getTrackbarPos (nom_slider, "ImageSrc") + 1;
if (nouv <= valeur_max) modifier_slider (nom_slider, nouv);
}
void Astico2D::creer_fenetres ()
{
cv::namedWindow ("ImageSrc", cv::WINDOW_AUTOSIZE);
cv::createTrackbar ("Zoom", "ImageSrc", &loupe.zoom, loupe.zoom_max,
onZoomSlide, this);
creer_slider ("Seuil", &seuil_niv, seuil_max);
cv::setMouseCallback ("ImageSrc", onMouseEventSrc, this);
cv::namedWindow ("Loupe", cv::WINDOW_AUTOSIZE);
cv::setMouseCallback ("Loupe", onMouseEventLoupe, this);
}
void Astico2D::traiter_evenements ()
{
for (;;) {
if (recalc == R_TOUTES)
{
if (mode_transfo == M_NIVEAUX) {
//std::cout << "Calcul img_niv" << std::endl;
cv::Mat img_gry;
cv::cvtColor (img_src, img_gry, cv::COLOR_BGR2GRAY);
cv::threshold (img_gry, img_gry, seuil_niv, 255, cv::THRESH_BINARY);
img_gry.convertTo (img_niv, CV_32SC1,1., 0.);
} else {
img_coul = img_src.clone();
}
if (mode_transfo != M_AUCUN && touche_transfo != 0) {
//std::cout << "Calcul tranformations pour '"
// << char(touche_transfo) << "'" << std::endl;
double t1 = temps.mesurer ();
// RQ : cet appel doit produire une image couleur dans img_coul.
effectuer_transformations ();
double t2 = temps.mesurer ();
if (MESURER_TEMPS) {
std::cout << "Duree transformations : "
<< std::to_string(t2-t1) << " s" << std::endl;
}
} else if (mode_transfo == M_NIVEAUX) {
representer_en_couleurs_vga (img_niv, img_coul);
}
}
if (recalc == R_TOUTES || recalc == R_LOUPE) {
//std::cout << "Calcul images loupe" << std::endl;
loupe.dessiner_rect (img_coul, img_res1);
loupe.dessiner_portion (img_coul, img_res2);
cv::imshow ("ImageSrc", img_res1);
cv::imshow ("Loupe" , img_res2);
}
recalc = R_RIEN;
// Attente du prochain événement sur toutes les fenêtres, avec un
// timeout de 15ms pour détecter les changements de flags
int key = cv::waitKey (15);
// Gestion des événements clavier avec une callback "maison" que l'on
// appelle nous-même. Les Callbacks souris et slider sont directement
// appelées par waitKey lors de l'attente.
if (onKeyPressEvent (key, this) < 0) break;
}
}
// Callbacks des sliders
void Astico2D::onZoomSlide (int pos, void *data)
{
Astico2D *ao = (Astico2D*) data;
ao->loupe.reborner (ao->img_res1, ao->img_res2);
ao->recalc = R_LOUPE;
}
void Astico2D::onDefaultTrackbarSlide (int pos, void *data)
{
Astico2D *ao = (Astico2D*) data;
ao->recalc = R_TOUTES;
}
// Callback pour la souris
void Astico2D::onMouseEventSrc (int event, int x, int y, int flags, void *data)
{
Astico2D *ao = (Astico2D*) data;
switch (event) {
case cv::EVENT_LBUTTONDOWN :
ao->clic_x = x;
ao->clic_y = y;
ao->clic_n = 1;
break;
case cv::EVENT_MOUSEMOVE :
// std::cout << "mouse move " << x << "," << y << std::endl;
if (ao->clic_n == 1) {
ao->loupe.deplacer (ao->img_res1, ao->img_res2,
x - ao->clic_x, y - ao->clic_y);
ao->clic_x = x;
ao->clic_y = y;
ao->recalc = R_LOUPE;
}
break;
case cv::EVENT_LBUTTONUP :
ao->clic_n = 0;
break;
}
}
void Astico2D::onMouseEventLoupe (int event, int x, int y, int flags, void *data)
{
Astico2D *ao = (Astico2D*) data;
switch (event) {
case cv::EVENT_LBUTTONDOWN :
if (ao->mode_transfo == M_NIVEAUX)
ao->loupe.afficher_tableau_niveaux (ao->img_niv, x, y, 5, 4);
else ao->loupe.afficher_tableau_couleurs (ao->img_coul, x, y, 2, 3);
break;
}
}
// Gestion du clavier
void Astico2D::afficher_touches_clavier ()
{
std::cout <<
"Touches du clavier :\n"
" a affiche cette aide\n"
" Esc quitte (et enregistre image_res)\n"
" hHlL change la hauteur ou largeur de la loupe\n"
" jJkK deplace la loupe\n"
" zZ change le zoom\n"
" g agrandit la loupe et adapte le zoom\n"
" i inverse les couleurs de l'image d'origine\n"
" os affiche l'image d'origine ou seuillee\n"
" eE change le seuil\n";
}
int Astico2D::onKeyPressEvent (int key, void *data)
{
Astico2D *ao = (Astico2D*) data;
if (key < 0) return 0; // aucune touche pressée
key &= 255; // pour comparer avec un char
switch (key) {
case 'a' :
ao->afficher_touches_clavier();
break;
case 27 : // ESC pour quitter
return -1;
case 'h' :
case 'H' :
case 'l' :
case 'L' : {
int h = ao->img_res2.rows, w = ao->img_res2.cols,
a = ao->loupe.vue_min, b = ao->loupe.vue_max,
i = ao->loupe.vue_inc;
if (key == 'h') h = (h >= a+i) ? h-i : a;
else if (key == 'H') h = (h <= b-i) ? h+i : b;
else if (key == 'l') w = (w >= a+i) ? w-i : a;
else if (key == 'L') w = (w <= b-i) ? w+i : b;
std::cout << "Taille loupe " << w << "x" << h << std::endl;
ao->img_res2 = cv::Mat(h, w, CV_8UC3);
ao->loupe.reborner(ao->img_res1, ao->img_res2);
ao->recalc = R_LOUPE;
break;
}
case 'j' :
ao->loupe.deplacer (ao->img_res1, ao->img_res2, 0, -1);
ao->recalc = R_LOUPE;
break;
case 'J' :
ao->loupe.deplacer (ao->img_res1, ao->img_res2, 0, 1);
ao->recalc = R_LOUPE;
break;
case 'k' :
ao->loupe.deplacer (ao->img_res1, ao->img_res2, -1, 0);
ao->recalc = R_LOUPE;
break;
case 'K' :
ao->loupe.deplacer (ao->img_res1, ao->img_res2, 1, 0);
ao->recalc = R_LOUPE;
break;
case 'z' :
ao->diminuer_slider ("Zoom", 1);
break;
case 'Z' :
ao->augmenter_slider ("Zoom", ao->loupe.zoom_max);
break;
case 'g' : {
int z = ao->loupe.calculer_zoom_optimal (ao->img_src);
ao->modifier_slider ("Zoom", z);
int ph, pw;
ao->loupe.calculer_portion_optimale (ao->img_src, ph, pw);
std::cout << "Taille loupe " << pw << "x" << ph
<<" zoom " << z << std::endl;
ao->img_res2 = cv::Mat(ph, pw, CV_8UC3);
ao->loupe.reborner(ao->img_res1, ao->img_res2);
ao->recalc = R_LOUPE;
break;
}
case 'i' :
std::cout << "Couleurs inversees" << std::endl;
inverser_couleurs(ao->img_src);
ao->recalc = R_TOUTES;
break;
case 'o' :
std::cout << "Image originale" << std::endl;
ao->recalc = R_TOUTES;
ao->mode_transfo = M_AUCUN;
break;
case 's' :
std::cout << "Image seuillee" << std::endl;
ao->recalc = R_TOUTES;
ao->mode_transfo = M_NIVEAUX;
ao->touche_transfo = 0;
break;
case 'e' :
ao->diminuer_slider ("Seuil", 0);
break;
case 'E' :
ao->augmenter_slider ("Seuil", ao->seuil_max);
break;
default :
if (ao->traiter_touche_clavier (key)) {
ao->recalc = R_TOUTES;
}
break;
}
return 1;
}
//------------------------ M E S U R E T E M P S ----------------------------
// Mesure du temps.
// Code extrait de EZ-Draw version 1.2 (LGPL), Edouard.Thiel@univ-amu.fr
#if MESURER_TEMPS
MesureTemps::MesureTemps ()
{
# ifdef _WIN32
_init_timeofday ();
# endif
}
// Return the elapsed time since the Epoch in seconds,microseconds
double MesureTemps::mesurer ()
{
struct timeval tp;
_gettimeofday (&tp);
return (double) tp.tv_sec + (double) tp.tv_usec / (double) 1E6;
}
# ifdef _WIN32
// Initialize time computation.
void MesureTemps::_init_timeofday (void)
{
LARGE_INTEGER freq;
_perf_freq = 0;
_get_systemtime (&_start_time);
QueryPerformanceCounter (&_start_count);
if (QueryPerformanceFrequency (&freq) && freq.QuadPart > 0)
_perf_freq = 1.0 / freq.QuadPart;
}
// Emulate gettimeofday() with GetSystemTimeAsFileTime().
// GetSystemTimeAsFileTime() has an accuracy of 15ms (10ms on XP).
// GetSystemTimePreciseAsFileTime() has an accuracy of 1ms but needs
// windows >= 8, and seems rather expensive.
void MesureTemps::_get_systemtime (struct timeval *t)
{
ULARGE_INTEGER ul;
FILETIME ft;
double dt;
if (t == NULL) return;
GetSystemTimeAsFileTime (&ft);
ul.LowPart = ft.dwLowDateTime;
ul.HighPart = ft.dwHighDateTime;
dt = ul.QuadPart / 10000000.0 - 11644473600.0;
t->tv_sec = dt;
t->tv_usec = (dt - t->tv_sec) * 1000000.0;
}
// Emulate gettimeofday() with QueryPerformanceCounter().
// This method seems to be more accurate than _get_systemtime()
// but leads to a drift.
void MesureTemps::_get_countertime (struct timeval *t)
{
LARGE_INTEGER now;
double dt;
QueryPerformanceCounter (&now);
dt = (now.QuadPart - _start_count.QuadPart) * _perf_freq;
*t = _start_time;
t->tv_sec += dt;
t->tv_usec += (dt - (int)dt) * 1000000;
t->tv_sec += t->tv_usec / 1000000;
t->tv_usec %= 1000000;
}
# endif // _WIN32
// Compute elapsed time since the Epoch.
void MesureTemps::_gettimeofday (struct timeval *t)
{
if (t == NULL) return;
#ifdef _WIN32
if (_perf_freq == 0)
_get_systemtime (t);
else _get_countertime (t);
#else
gettimeofday (t, NULL);
#endif
}
#endif // MESURER_TEMPS