Anti-Spoiler Comic Reader - Parte 1

Immaginate di essere usciti in questo momento dal vostro giornalaio di fiducia o dalla vostra fumetteria preferita. Avete appena acquistato il settimo volume della nuova raccolta in italiano degli albi di The Walking Dead. Vi siete appassionati così tanto alla serie a fumetti che avete impostato tutti i possibili filtri anti-spoiler per i vostri browser. Mentre passeggiate per tornare a casa, decidete che ormai la strada la conoscete come le vostre tasche, quindi perchè non cominciare il primo dei quattro albi sulla via del ritorno? La lettura vi prende, avete già fumato una decina di pagine, quando ad un certo punto accade l'irreparabile: il volumetto vi scivola di mano. Con una smorfia di terrore stampata sul volto recuperate al volo l'albo, ma la vostra mano destra rapida agguanta proprio l'ultima pagina. Non seguendo i consigli del mitologico Perseo, gli occhi cadono su quelle ultime vignette e.. ZAC! Vi siete appena rovinati il finale di quattro albi della vostra serie a fumetti preferita.

Questa narrazione, tratta in parte da fatti realmente accaduti, mi ha dato la motivazione per provare a buttare giù un piccolo progetto basato sulla nostra libreria preferita, OpenCV, che vi presenterò in due articoli pubblicati a breve distanza fra loro (si spera). Il programma che andrò a presentarvi, date una serie di pagine di fumetti, mostra a video le singole vignette ordinate dal vertice alto a sinistra della pagina a quello in basso a destra (o dal vertice in alto a destra a quello in basso a sinistra per quanto riguarda i manga).

Oltre all'evitare spoiler dati dai rapidi sguardi alle pagine successive, il codice potrebbe essere inglobato in una applicazione mobile/tablet per leggere i fumetti senza stare ogni 5 secondi a pinchare sullo schermo per spostarsi nella pagina o zoomare su certe vignette.

La base del programma è un esempio di codice OpenCV già presente all'interno della libreria: squares.cpp, semplice applicazione per la detection di rettangoli all'interno di una serie di immagini. Partendo da questo, ho aggiunto alcune funzioni appositamente create per raggiungere lo scopo prefissato. Qui il main del codice:

#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <limits.h>
#include <list>
#include <iostream>
#include <math.h>
#include <string.h>

using namespace cv;
using namespace std;

// inserire qui le funzioni descritte in seguito negli articoli

int main(int /*argc*/, char** /*argv*/) {
	static const char* names[] = { "ratman.png", 0 };
	help();
	namedWindow(wndname, 1);
	bool isManga = false;
	bool draw = true;

	for (unsigned int i = 0; names[i] != 0; i++) {
		Mat image = imread(names[i], 1);
		vector<vector<Point> > squares;


		if (image.empty()) {
			cout << "Couldn't load " << names[i] << endl;
			continue;
		}

		findSquares(image, squares);
		squares = eraseMaxSquare(squares);
		squares = eraseNestedSquares(squares, image);
		squares = addNotReveleadSquares(squares, image);
		squares = reorderSquares(squares, image, isManga);

		if(draw){
			drawSquares(image, squares);
			imshow(wndname, image);
			int c = waitKey();
			if ((char) c == 'q')
				continue;
		}


		for (unsigned int i = 0; i < squares.size(); i++) {
			Rect roi = Rect(squares[i][0], squares[i][2]);
			Mat img = image(roi);
			imshow(wndname, img);
			int c = waitKey();
			if ((char) c == 'q')
				continue;
		}
		squares.clear();
	}

	return 0;
}

Dopo aver specificato l'elenco delle immagini, il programma le carica una a una nella variabile Mat image. La funzione findSquares era già implementata all'interno dell'esempio. Ho dovuto solo modificare alcuni dettagli per adeguarmi al task. Vi riporto qui il codice, anche se le differenze con l'originale sono minime:

// returns sequence of squares detected on the image.
// the sequence is stored in the specified memory storage
static void findSquares(const Mat& image, vector<vector<Point> >& squares) {
	squares.clear();

	Mat imgThreshold = Mat(image.cols, image.rows, IPL_DEPTH_8U, 1);
	cvtColor(image, imgThreshold, CV_RGB2GRAY);
	adaptiveThreshold(imgThreshold, imgThreshold, 255,
			CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, 75, 25);

	Mat pyr, timg, gray0(imgThreshold.size(), CV_8U), gray;

	// down-scale and upscale the image to filter out the noise
	pyrDown(imgThreshold, pyr,
			Size(imgThreshold.cols / 2, imgThreshold.rows / 2));
	pyrUp(pyr, timg, imgThreshold.size());
	vector<vector<Point> > contours;

	// find squares in every color plane of the image
	for (unsigned int c = 0; c < 1; c++) {
		int ch[] = { c, 0 };
		mixChannels(&timg, 1, &gray0, 1, ch, 1);

		// try several threshold levels
		for (unsigned int l = 0; l < N; l++) {
			// hack: use Canny instead of zero threshold level.
			// Canny helps to catch squares with gradient shading
			if (l == 0) {
				// apply Canny. Take the upper threshold from slider
				// and set the lower to 0 (which forces edges merging)
				Canny(gray0, gray, 0, thresh, 5);
				// dilate canny output to remove potential
				// holes between edge segments
				dilate(gray, gray, Mat(), Point(-1, -1));
			}

			// find contours and store them all as a list
			findContours(gray, contours, CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE);

			vector<Point> approx;

			// test each contour
			for (size_t i = 0; i < contours.size(); i++) {
				// approximate contour with accuracy proportional
				// to the contour perimeter
				approxPolyDP(Mat(contours[i]), approx,
						arcLength(Mat(contours[i]), true) * 0.02, true);

				// square contours should have 4 vertices after approximation
				// relatively large area (to filter out noisy contours)
				// and be convex.
				// Note: absolute value of an area is used because
				// area may be positive or negative - in accordance with the
				// contour orientation
				if (approx.size() == 4 && fabs(contourArea(Mat(approx))) > 1000
						&& isContourConvex(Mat(approx))) {
					double maxCosine = 0;

					for (unsigned int j = 2; j < 5; j++) {
						// find the maximum cosine of the angle between joint edges
						double cosine = fabs(
								angle(approx[j % 4], approx[j - 2],
										approx[j - 1]));
						maxCosine = MAX(maxCosine, cosine);
					}

					// if cosines of all angles are small
					// (all angles are ~90 degree) then write quandrange
					// vertices to resultant sequence
					if (maxCosine < 0.3)
						squares.push_back(approx);
				}
			}
		}
	}

}

La funzione contiene diversi argomenti già trattati in precedenti tutorial come l'algoritmo di Canny e l'adaptive thresholding.
Questa funzione però non basta da sola per trovare solo le vignette: nel vettore squares saranno presenti tanti, troppi, falsi positivi. A questo scopo sono state implementate le quattro funzioni che seguono e che ad ogni chiamata modificano appropriatamente il vettore di rettangoli, fino ad arrivare alle sole vignette contenute nelle pagine.

Le funzioni (dai nomi piuttosto evocativi) sono:
- eraseMaxSquare
- eraseNestedSquares
- addNotReveleadSquares
- reorderSquares

La prima semplicemente si occupa di ricercare l'elemento di squares con l'area più grande e ad eliminarlo dal vettore. Questo perchè findSquares aggiunge nel vettore anche la pagina completa, essendo anch'essa un rettangolo. Ma dopotutto mostrare l'intera pagina è esattamente quello che vogliamo evitare con questo codice.

La seconda ricerca i rettangoli che sono totalmente inclusi dentro altri e li elimina. Questo perchè per l'elaborato la funzione findSquares genera molti falsi positivi tra i quadrilateri contenuti nelle scene. Esempi pratici sono le narrazioni esterne, racchiuse spesso in rettangoli fluttuanti nella scena.

Può accadere però che alcune vignette non siano inserite nel vettore squares. Questo perchè per esempio non hanno tutti i contorni ben definiti. Utilizzando una griglia più o meno densa, questa funzione aggiunge le vignette non trovate al vettore.

Infine, la funzione reorderSquares riarrangia squares (che arrivati a questo punto conterrà tutte e sole le vignette) per mostrarle a video in ordine a seconda che il fumetto sia occidentale o manga.

Siccome a noi ingegneri piacciono tanto i diagrammi a blocchi, ricapitolo tutto quanto nello schema seguente in cui ho utilizzato come esempio una pagina fotografata da un mio albo di RatMan. Cliccate sull'immagine per ingrandire.

Nel prossimo articolo vedremo nel dettaglio il codice delle quattro funzioni. Come sempre, per domande e dubbi, scrivete un commento all'articolo!

Alessio Antonielli

Ingegnere informatico, appassionato di cinema, musica, videogiochi e serie tv. Insomma, le solite cose che vi aspettereste da un ex studente di Ingegneria Informatica, giusto per rafforzare lo stereotipo…

Anti-Spoiler Comic Reader - Parte 1 ultima modifica: 2013-09-01T20:14:09+01:00 da Alessio Antonielli


Advertisment ad adsense adlogger