/* 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(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(y,x)[0] = couls[c][2]; img_coul.at(y,x)[1] = couls[c][1]; img_coul.at(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(y,x)[0] = 255 - img.at(y,x)[0]; img.at(y,x)[1] = 255 - img.at(y,x)[1]; img.at(y,x)[2] = 255 - img.at(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(y,x)[0] = 64; dest.at(y,x)[1] = 64; dest.at(y,x)[2] = 64; continue; } dest.at(y,x)[0] = src.at(y0,x0)[0]; dest.at(y,x)[1] = src.at(y0,x0)[1]; dest.at(y,x)[2] = src.at(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(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(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(y,x)[2], cV = src.at(y,x)[1], cB = src.at(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