main.cpp 36.6 KB
Newer Older
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
1
2
3
4
5
6
7
8
9
10
11
/**
 *  MPAI CAE-ARP Video Analyser.
 *
 *	Implements MPAI CAE-ARP Video Analyser Technical Specification.
 *	It identifies Irregularities on the Preservation Audio-Visual File, providing:
 *	- Irregularity Files;
 *	- Irregularity Images.
 *
 *	WARNING:
 *	Currently, this program is only compatible with the Studer A810 and videos recorded in PAL standard.
 *
Matteo's avatar
update    
Matteo committed
12
 *  @authors Nadir Dalla Pozza <nadir.dallapozza@unipd.it>, Matteo Spanio <dev2@audioinnova.com>
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
13
14
15
 *	@copyright 2022, Audio Innova S.r.l.
 *	@credits Niccolò Pretto, Nadir Dalla Pozza, Sergio Canazza
 *	@license GPL v3.0
Matteo's avatar
update    
Matteo committed
16
 *	@version 1.1.0
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
17
18
 *	@status Production
 */
19
#include <filesystem>
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
20
#include <fstream>
21
#include <iostream>
22
#include <stdlib.h>
23
24
#include <sys/timeb.h>

25
#include <boost/program_options.hpp>
26
27
28
29
30
31
32
33
34
35
36
37
#include <boost/uuid/uuid.hpp>            // uuid class
#include <boost/uuid/uuid_generators.hpp> // generators
#include <boost/uuid/uuid_io.hpp>         // streaming operators etc.
#include <boost/lexical_cast.hpp>

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>

#include <nlohmann/json.hpp>

38
39
40
41
42
43
44
#include "opencv2/core.hpp"
#include "opencv2/calib3d.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/xfeatures2d.hpp"

45
46
47
#include "utility.h"
#include "forAudioAnalyser.h"

Matteo's avatar
update    
Matteo committed
48
#include "lib/files.h"
Matteo's avatar
update    
Matteo committed
49
50
51
#include "lib/colors.h"
#include "lib/time.h"

52
53
54
using namespace cv;
using namespace std;
using json = nlohmann::json;
55
56
namespace fs = std::filesystem;
namespace po = boost::program_options;
57

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
58
// For capstan detection, there are two alternative approaches:
59
60
61
// Generalized Hough Transform and SURF.
bool useSURF = true;

Matteo's avatar
update    
Matteo committed
62
63
bool savingPinchRoller = false;
bool pinchRollerRect = false;
64
bool savingBrand = false;
65
bool endTapeSaved = false;
66
67
float mediaPrevFrame = 0;
bool firstBrand = true;	// The first frame containing brands on tape must be saved
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
68
float firstInstant = 0;
69
string fileName, extension;
70

71
// Path variables
Matteo's avatar
update    
Matteo committed
72
73
fs::path outputPath {};
fs::path irregularityImagesPath {};
74
// JSON files
Matteo's avatar
update    
Matteo committed
75
76
77
static json configurationFile {};
static json irregularityFileOutput1 {};
static json irregularityFileOutput2 {};
78
// RotatedRect identifying the processing area
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
79
RotatedRect rect, rectTape, rectCapstan;
80

Matteo's avatar
update    
Matteo committed
81
// Structs
82

Matteo's avatar
update    
Matteo committed
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// config.json parameters
struct Config {
	fs::path workingPath;
	string filesName;
	bool brands;
	float speed;
};

struct Threshold {
	float percentual;
	int angle;
	int scale;
	int pos;
};

struct SceneObject {
	int minDist;
	Threshold threshold;
};

Matteo's avatar
update    
Matteo committed
103
104
105
static Config config;
static SceneObject tape;
static SceneObject capstan;
Matteo's avatar
update    
Matteo committed
106

Matteo's avatar
update    
Matteo committed
107
108
109
110
// Constants Paths
static const string READING_HEAD_IMG = "images/reading_head.png";
static const string CAPSTAN_TEMPLATE_IMG = "input/capstanBERIO058prova.png";
static const string CONFIG_FILE = "config/config.json";
111

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
112
113
114
115
116
117
118
119
/**
 * @brief Get operation arguments from command line or config.json file.
 *
 * @param argc Command line arguments count;
 * @param argv Command line arguments.
 * @return true if input configuration is valid;
 * @return false otherwise.
 */
Matteo's avatar
update    
Matteo committed
120
bool getArguments(int argc, const char** argv) {
121
	// Read configuration file
Matteo's avatar
update    
Matteo committed
122
	ifstream iConfig(CONFIG_FILE);
123
124
125
126
	iConfig >> configurationFile;

	if (argc == 1) {
		// Read from JSON file
Matteo's avatar
update    
Matteo committed
127
		string working_path = configurationFile["WorkingPath"];
Matteo's avatar
update    
Matteo committed
128
129

		config = {
Matteo's avatar
update    
Matteo committed
130
			fs::path(working_path),
Matteo's avatar
update    
Matteo committed
131
132
133
134
135
			configurationFile["FilesName"],
			configurationFile["Brands"],
			configurationFile["Speed"]
		};

136
137
138
139
140
	} else {
		// Get from command line
		try {
			po::options_description desc(
				"A tool that implements MPAI CAE-ARP Video Analyser Technical Specification.\n"
Matteo's avatar
update    
Matteo committed
141
				"By default, the configuartion parameters are loaded from config/config.json file,\n"
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
				"but, alternately, you can pass command line arguments to replace them"
			);
			desc.add_options()
				("help,h", "Display this help message")
				("working-path,w", po::value<string>()->required(), "Specify the Working Path, where all input files are stored")
				("files-name,f", po::value<string>()->required(), "Specify the name of the Preservation files (without extension)")
				("brands,b", po::value<bool>()->required(), "Specify if the tape presents brands on its surface")
				("speed,s", po::value<float>()->required(), "Specify the speed at which the tape was read");
			po::variables_map vm;
			po::store(po::command_line_parser(argc, argv).options(desc).run(), vm);
			if (vm.count("help")) {
				cout << desc << "\n";
				return false;
			}
			po::notify(vm);
Matteo's avatar
update    
Matteo committed
157
158
159
160
161
162
163

			// Access the stored options
			config.workingPath = fs::path(vm["working-path"].as<string>());
			config.filesName = vm["files-name"].as<string>();
			config.brands = vm["brands"].as<bool>();
			config.speed = vm["speed"].as<float>();

164
165
166
167
168
169
170
171
172
		} catch (po::invalid_command_line_syntax& e) {
			cerr << RED << BOLD << "The command line syntax is invalid: " << END << RED << e.what() << END << endl;
			return false;
		} catch (po::required_option& e) {
			cerr << "Error: " << e.what() << endl;
			return false;
		}
	}

Matteo's avatar
update    
Matteo committed
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
	capstan = {
		configurationFile["MinDistCapstan"],
		{
			configurationFile["CapstanThresholdPercentual"],
			configurationFile["AngleThreshCapstan"],
			configurationFile["ScaleThreshCapstan"],
			configurationFile["PosThreshCapstan"]
		}
	};

	tape = {
		configurationFile["MinDist"],
		{
			configurationFile["TapeThresholdPercentual"],
			configurationFile["AngleThresh"],
			configurationFile["ScaleThresh"],
			configurationFile["PosThresh"]
		}
	};
192
193
194
195
196

	return true;
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
197
198
199
200
201
202
/**
 * @brief Identifies the Regions Of Interest (ROIs) on the video,
 * which are:
 * - The reading head;
 * - The tape area under the tape head (computed on the basis of the detected reading head);
 * - The capstan.
Matteo's avatar
update    
Matteo committed
203
 * @param myFrame The current frame of the video.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
204
205
206
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
Matteo's avatar
update    
Matteo committed
207
bool findProcessingAreas(Mat myFrame) {
208
209
210
211
212

	/*********************************************************************************************/
	/*********************************** READING HEAD DETECTION **********************************/
	/*********************************************************************************************/

Matteo's avatar
update    
Matteo committed
213
214
215
216
217
218
219
220
221
222
	utility::Frame myFrameUtility(myFrame);
	myFrameUtility
		.convertColor(COLOR_BGR2GRAY)
		.downsample(2);

	utility::Frame templateImageUtility(cv::imread(READING_HEAD_IMG, IMREAD_GRAYSCALE));
	templateImageUtility.downsample(2);

	// Save a grayscale version of myFrame in myFrameGrayscale and downsample it in half pixels for performance reasons
	Mat myFrameGrayscale, myFrameGrayscaleHalf;
223
224
	cvtColor(myFrame, myFrameGrayscale, COLOR_BGR2GRAY);
	pyrDown(myFrameGrayscale, myFrameGrayscaleHalf, Size(myFrame.cols/2, myFrame.rows/2));
Matteo's avatar
update    
Matteo committed
225
226
227

	// Get input shape in grayscale and downsample it in half pixels
	Mat templateImage = cv::imread(READING_HEAD_IMG, IMREAD_GRAYSCALE);
228
229
230
231
232
233
234
235
236
237
	Mat templateImageHalf;
	pyrDown(templateImage, templateImageHalf, Size(templateImage.cols/2, templateImage.rows/2));

	// Process only the bottom-central portion of the input video -> best results with our videos
	Rect readingHeadProcessingAreaRect(myFrameGrayscaleHalf.cols/4, myFrameGrayscaleHalf.rows/2, myFrameGrayscaleHalf.cols/2, myFrameGrayscaleHalf.rows/2);
	Mat processingImage = myFrameGrayscaleHalf(readingHeadProcessingAreaRect);
	// Select the template to be detected
	Mat templateShape = templateImageHalf;

	// Algorithm and parameters
Matteo's avatar
update    
Matteo committed
238
	// for informations about the Generalized Hough Guild interface see the tutorial at https://docs.opencv.org/4.7.0/da/ddc/tutorial_generalized_hough_ballard_guil.html
239
240
241
242
	Ptr<GeneralizedHoughGuil> alg = createGeneralizedHoughGuil();

	vector<Vec4f> positionsPos, positionsNeg;
	Mat votesPos, votesNeg;
Matteo's avatar
update    
Matteo committed
243
	int oldPosThresh = tape.threshold.pos;
244
245
246
247
248
249
250
251
252
253
	RotatedRect rectPos, rectNeg;
	ofstream myFile;
	Point2f pts[4];

	// Find the best matches for positive and negative angles
	// If there are more than one shapes, then choose the one with the highest score
	// If there are more than one with the same highest score, then arbitrarily choose the latest
	double maxValPos = 0, maxValNeg = 0;
	int indexPos = 0, indexNeg = 0;

Matteo's avatar
update    
Matteo committed
254
255
256
257
	alg->setMinDist(tape.minDist);
	alg->setLevels(360);
	alg->setDp(2);
	alg->setMaxBufferSize(1000);
258

Matteo's avatar
update    
Matteo committed
259
260
	alg->setAngleStep(1);
	alg->setAngleThresh(tape.threshold.angle);
261

Matteo's avatar
update    
Matteo committed
262
263
264
265
	alg->setMinScale(0.9);
	alg->setMaxScale(1.1);
	alg->setScaleStep(0.01);
	alg->setScaleThresh(tape.threshold.scale);
266

Matteo's avatar
update    
Matteo committed
267
	alg->setPosThresh(tape.threshold.pos);
268

Matteo's avatar
update    
Matteo committed
269
270
	alg->setCannyLowThresh(150); // Old: 100
	alg->setCannyHighThresh(240); // Old: 300
271

Matteo's avatar
update    
Matteo committed
272
	alg->setTemplate(templateShape);
273
274

	cout << DARK_CYAN << "Reading head" << END << endl;
Matteo's avatar
update    
Matteo committed
275
	
Matteo's avatar
update    
Matteo committed
276
	utility::detectShape(alg, templateShape, tape.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg, processingImage);
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293

	for (int i = 0; i < votesPos.size().width; i++) {
		if (votesPos.at<int>(i) >= maxValPos) {
			maxValPos = votesPos.at<int>(i);
			indexPos = i;
		}
	}

	for (int i = 0; i < votesNeg.size().width; i++) {
		if (votesNeg.at<int>(i) >= maxValNeg) {
			maxValNeg = votesNeg.at<int>(i);
			indexNeg = i;
		}
	}

	// The color is progressively darkened to emphasize that the algorithm found more than one shape
	if (positionsPos.size() > 0)
Matteo's avatar
update    
Matteo committed
294
		rectPos = utility::drawShapes(myFrame, positionsPos[indexPos], Scalar(0, 0, 255-indexPos*64), templateImageHalf.cols, templateImageHalf.rows, myFrameGrayscaleHalf.cols/4, myFrameGrayscaleHalf.rows/2, 2);
295
	if (positionsNeg.size() > 0)
Matteo's avatar
update    
Matteo committed
296
		rectNeg = utility::drawShapes(myFrame, positionsNeg[indexNeg], Scalar(128, 128, 255-indexNeg*64), templateImageHalf.cols, templateImageHalf.rows, myFrameGrayscaleHalf.cols/4, myFrameGrayscaleHalf.rows/2, 2);
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329

	myFile.open("log.txt", ios::app);

	if (maxValPos > 0)
		if (maxValNeg > 0)
			if (maxValPos > maxValNeg) {
				myFile << "READING HEAD: Positive angle is best, match number: " << indexPos << endl;
				rect = rectPos;
			} else {
				myFile << "READING HEAD: Negative angle is best, match number: " << indexNeg << endl;
				rect = rectNeg;
			}
		else {
			myFile << "READING HEAD: Positive angle is the only choice, match number: " << indexPos << endl;
			rect = rectPos;
		}
	else if (maxValNeg > 0) {
		myFile << "READING HEAD: Negative angle is the only choice, match number: " << indexNeg << endl;
		rect = rectNeg;
	} else {
		myFile.close();
		return false;
	}
	cout << endl;

	rect.points(pts);

	/*********************************************************************************************/
	/************************************ TAPE AREA DETECTION ************************************/
	/*********************************************************************************************/

	// Compute area basing on reading head detection
	Vec4f positionTape( rect.center.x, rect.center.y + rect.size.height / 2 + 20 * (rect.size.width / 200), 1, rect.angle );
Matteo's avatar
update    
Matteo committed
330
	rectTape = utility::drawShapes(myFrame, positionTape, Scalar(0, 255-indexPos*64, 0), rect.size.width, 50 * (rect.size.width / 200), 0, 0, 1);
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352

	myFile << "Tape area:" << endl;
	myFile << "  Center (x, y): (" << rectTape.center.x << ", " << rectTape.center.y << ")" << endl;
	myFile << "  Size (w, h): (" << rectTape.size.width << ", " << rectTape.size.height << ")" << endl;
	myFile << "  Angle (deg): (" << rectTape.angle << ")" << endl;

	json autoJSON;
	autoJSON["PreservationAudioVisualFile"] = fileName;
	autoJSON["RotatedRect"] = {
		{
			"CenterX", rectTape.center.x
		}, {
			"CenterY", rectTape.center.y
		}, {
			"Width", rectTape.size.width
		}, {
			"Height", rectTape.size.height
		}, {
			"Angle", rectTape.angle
		}
	};

Matteo's avatar
update    
Matteo committed
353
	files::saveFile(fs::path("./" + fileName + ".json"), autoJSON.dump(4), false);
354
355
356
357
358
359

	/*********************************************************************************************/
	/************************************* CAPSTAN DETECTION *************************************/
	/*********************************************************************************************/

	// Read template image - it is smaller than before, therefore there is no need to downsample
Matteo's avatar
update    
Matteo committed
360
	templateShape = imread(CAPSTAN_TEMPLATE_IMG, IMREAD_GRAYSCALE);
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
	// templateShape = imread("../input/capstanBERIO058.png", IMREAD_GRAYSCALE);

	cout << DARK_CYAN << "Capstan" << END << endl;

	if (useSURF) {

		// Step 1: Detect the keypoints using SURF Detector, compute the descriptors
		int minHessian = 100;
		Ptr<xfeatures2d::SURF> detector = xfeatures2d::SURF::create(minHessian);
		vector<KeyPoint> keypoints_object, keypoints_scene;
		Mat descriptors_object, descriptors_scene;

		detector->detectAndCompute(templateShape, noArray(), keypoints_object, descriptors_object);
		detector->detectAndCompute(myFrameGrayscale, noArray(), keypoints_scene, descriptors_scene);

		// Step 2: Matching descriptor vectors with a FLANN based matcher
		// Since SURF is a floating-point descriptor NORM_L2 is used
		Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create(DescriptorMatcher::FLANNBASED);
		vector<vector<DMatch>> knn_matches;
		matcher->knnMatch(descriptors_object, descriptors_scene, knn_matches, 2);
		//-- Filter matches using the Lowe's ratio test
		const float ratio_thresh = 0.75f;
		vector<DMatch> good_matches;
		for (size_t i = 0; i < knn_matches.size(); i++) {
			if (knn_matches[i][0].distance < ratio_thresh * knn_matches[i][1].distance) {
				good_matches.push_back(knn_matches[i][0]);
			}
		}
		// Draw matches
		Mat img_matches;
		drawMatches(templateShape, keypoints_object, myFrameGrayscale, keypoints_scene, good_matches, img_matches, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
		// Localize the object
		vector<Point2f> obj;
		vector<Point2f> scene;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
395
		for (size_t i = 0; i < good_matches.size(); i++) {
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
			// Get the keypoints from the good matches
			obj.push_back(keypoints_object[good_matches[i].queryIdx].pt);
			scene.push_back(keypoints_scene[good_matches[i].trainIdx].pt);
		}
		Mat H = findHomography(obj, scene, RANSAC);
		// Get the corners from the image_1 ( the object to be "detected" )
		vector<Point2f> obj_corners(4);
		obj_corners[0] = Point2f(0, 0);
		obj_corners[1] = Point2f((float)templateShape.cols, 0);
		obj_corners[2] = Point2f((float)templateShape.cols, (float)templateShape.rows);
		obj_corners[3] = Point2f(0, (float)templateShape.rows);
		vector<Point2f> scene_corners(4);
		perspectiveTransform( obj_corners, scene_corners, H);

		// Find average
		float capstanX = (scene_corners[0].x + scene_corners[1].x + scene_corners[2].x + scene_corners[3].x) / 4;
		float capstanY = (scene_corners[0].y + scene_corners[1].y + scene_corners[2].y + scene_corners[3].y) / 4;

		// In the following there are two alterations to cut the first 20 horizontal pixels and the first 90 vertical pixels from the found rectangle:
		// +10 in X for centering and -20 in width
		// +45 in Y for centering and -90 in height
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
417
		Vec4f positionCapstan(capstanX + 10, capstanY + 45, 1, 0);
Matteo's avatar
update    
Matteo committed
418
		rectCapstan = utility::drawShapes(myFrame, positionCapstan, Scalar(255-indexPos*64, 0, 0), templateShape.cols - 20, templateShape.rows - 90, 0, 0, 1);
419
420
421
422
423
424
425
426
427
428
429
430
431

	} else {

		// Process only right portion of the image, wherw the capstain always appears
		int capstanProcessingAreaRectX = myFrame.cols*3/4;
		int capstanProcessingAreaRectY = myFrame.rows/2;
		int capstanProcessingAreaRectWidth = myFrame.cols/4;
		int capstanProcessingAreaRectHeight = myFrame.rows/2;
		Rect capstanProcessingAreaRect(capstanProcessingAreaRectX, capstanProcessingAreaRectY, capstanProcessingAreaRectWidth, capstanProcessingAreaRectHeight);
		Mat capstanProcessingAreaGrayscale = myFrameGrayscale(capstanProcessingAreaRect);
		// Reset algorithm and set parameters
		alg = createGeneralizedHoughGuil();

Matteo's avatar
update    
Matteo committed
432
		alg -> setMinDist(capstan.minDist);
433
434
435
436
437
		alg -> setLevels(360);
		alg -> setDp(2);
		alg -> setMaxBufferSize(1000);

		alg -> setAngleStep(1);
Matteo's avatar
update    
Matteo committed
438
		alg -> setAngleThresh(capstan.threshold.angle);
439
440
441
442

		alg -> setMinScale(0.9);
		alg -> setMaxScale(1.1);
		alg -> setScaleStep(0.01);
Matteo's avatar
update    
Matteo committed
443
		alg -> setScaleThresh(capstan.threshold.scale);
444

Matteo's avatar
update    
Matteo committed
445
		alg -> setPosThresh(capstan.threshold.pos);
446
447
448
449
450
451

		alg -> setCannyLowThresh(150);
		alg -> setCannyHighThresh(240);

		alg -> setTemplate(templateShape);

Matteo's avatar
update    
Matteo committed
452
		oldPosThresh = capstan.threshold.pos;
453
454
455
456

		vector<Vec4f> positionsC1Pos, positionsC1Neg;
		Mat votesC1Pos, votesC1Neg;

Matteo's avatar
update    
Matteo committed
457
		utility::detectShape(alg, templateShape, capstan.threshold.pos, positionsC1Pos, votesC1Pos, positionsC1Neg, votesC1Neg, capstanProcessingAreaGrayscale);
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479

		// Find the best matches for positive and negative angles
		// If there are more than one shapes, then choose the one with the highest score
		// If there are more than one with the same highest score, then choose the latest
		maxValPos = 0, maxValNeg = 0, indexPos = 0, indexNeg = 0;

		for (int i = 0; i < votesC1Pos.size().width; i++) {
			if (votesC1Pos.at<int>(i) >= maxValPos) {
				maxValPos = votesC1Pos.at<int>(i);
				indexPos = i;
			}
		}

		for (int i = 0; i < votesC1Neg.size().width; i++) {
			if (votesC1Neg.at<int>(i) >= maxValNeg) {
				maxValNeg = votesC1Neg.at<int>(i);
				indexNeg = i;
			}
		}

		RotatedRect rectCapstanPos, rectCapstanNeg;
		if (positionsC1Pos.size() > 0)
Matteo's avatar
update    
Matteo committed
480
			rectCapstanPos = utility::drawShapes(myFrame, positionsC1Pos[indexPos], Scalar(255-indexPos*64, 0, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
481
		if (positionsC1Neg.size() > 0)
Matteo's avatar
update    
Matteo committed
482
			rectCapstanNeg = utility::drawShapes(myFrame, positionsC1Neg[indexNeg], Scalar(255-indexNeg*64, 128, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520

		if (maxValPos > 0)
			if (maxValNeg > 0)
				if (maxValPos > maxValNeg) {
					myFile << "CAPSTAN: Positive is best, match number: " << indexPos << endl;
					rectCapstan = rectCapstanPos;
				} else {
					myFile << "CAPSTAN: Negative is best, match number: " << indexNeg << endl;
					rectCapstan = rectCapstanNeg;
				}
			else {
				myFile << "CAPSTAN: Positive is the only choice, match number: " << indexPos << endl;
				rectCapstan = rectCapstanPos;
			}
		else if (maxValNeg > 0) {
			myFile << "CAPSTAN: Negative is the only choice, match number: " << indexNeg << endl;
			rectCapstan = rectCapstanNeg;
		} else {
			myFile.close();
			return false;
		}

	}

	myFile << "Capstan ROI:" << endl;
	myFile << "  Center (x, y): (" << rectCapstan.center.x << ", " << rectCapstan.center.y << ")" << endl;
	myFile << "  Size (w, h): (" << rectCapstan.size.width << ", " << rectCapstan.size.height << ")" << endl;
	myFile << "  Angle (deg): (" << rectCapstan.angle << ")" << endl;
	myFile.close();
	cout << endl;

	// Save the image containing the detected areas
	cv::imwrite(outputPath.string() + "/tapeAreas.jpg", myFrame);

	return true;
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
521
522
523
524
525
526
527
528
529
530
/**
 * @brief Compares two consecutive video frames and establish if there potentially is an Irregularity.
 * The comparison is pixel-wise and based on threshold values set on config.json file.
 *
 * @param prevFrame the frame before the current one;
 * @param currentFrame the current frame;
 * @param msToEnd the number of milliseconds left before the end of the video. Useful for capstan analysis.
 * @return true if a potential Irregularity has been found;
 * @return false otherwise.
 */
531
532
bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd) {

533
	/*********************************************************************************************/
534
	/********************************** Capstan analysis *****************************************/
535
	/*********************************************************************************************/
536
537

	// In the last minute of the video, check for pinchRoller position for endTape event
538
	if (!endTapeSaved && msToEnd < 60000) {
539
540
541

		// Capstan area
		int capstanAreaPixels = rectCapstan.size.width * rectCapstan.size.height;
Matteo's avatar
update    
Matteo committed
542
		float capstanDifferentPixelsThreshold = capstanAreaPixels * capstan.threshold.percentual / 100;
543

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
544
		// Extract matrices corresponding to the processing area
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
		// CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/

		// matrices we'll use
		Mat M, rotatedPrevFrame, croppedPrevFrame, rotatedCurrentFrame, croppedCurrentFrame;
		// get angle and size from the bounding box
		float angle = rectCapstan.angle;
		Size rect_size = rectCapstan.size;
		// thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/
		if (rectCapstan.angle < -45.) {
			angle += 90.0;
			swap(rect_size.width, rect_size.height);
		}
		// get the rotation matrix
		M = getRotationMatrix2D(rectCapstan.center, angle, 1.0);
		// perform the affine transformation
560
561
		cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
		cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
562
		// crop the resulting image
563
564
		cv::getRectSubPix(rotatedPrevFrame, rect_size, rectCapstan.center, croppedPrevFrame);
		cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectCapstan.center, croppedCurrentFrame);
565
566
567

		// END CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/

Matteo's avatar
update    
Matteo committed
568
		cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
569
570
571
572
573
574
575
576
577
578
579
580
581
582

		int blackPixelsCapstan = 0;

		for (int i = 0; i < croppedCurrentFrame.rows; i++) {
			for (int j = 0; j < croppedCurrentFrame.cols; j++) {
				if (differenceFrame.at<cv::Vec3b>(i, j)[0] == 0) {
					// There is a black pixel, then there is a difference between previous and current frames
					blackPixelsCapstan++;
				}
			}
		}

		if (blackPixelsCapstan > capstanDifferentPixelsThreshold) {
			savingPinchRoller = true;
583
			endTapeSaved = true; // Never check again for end tape instant
584
585
586
587
			return true;
		} else {
			savingPinchRoller = false;
		}
588
589
	} else {
		savingPinchRoller = false; // It will already be false before the last minute of the video. After having saved the capstan, the next time reset the variable to not save again
590
	}
591
592

	/*********************************************************************************************/
593
	/************************************ Tape analysis ******************************************/
594
	/*********************************************************************************************/
595
596
597

	// Tape area
    int tapeAreaPixels = rectTape.size.width * rectTape.size.height;
Matteo's avatar
update    
Matteo committed
598
	float tapeDifferentPixelsThreshold = tapeAreaPixels * tape.threshold.percentual / 100;
599

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
600
	// Extract matrices corresponding to the processing area
601
602
603
604
605
	// CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/

	// matrices we'll use
	Mat M, rotatedPrevFrame, croppedPrevFrame, rotatedCurrentFrame, croppedCurrentFrame;
	// get angle and size from the bounding box
606
607
	float angle = rectTape.angle;
	Size rect_size = rectTape.size;
608
	// thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/
609
	if (rectTape.angle < -45.) {
610
611
612
613
		angle += 90.0;
		swap(rect_size.width, rect_size.height);
	}
	// get the rotation matrix
614
	M = getRotationMatrix2D(rectTape.center, angle, 1.0);
615
	// perform the affine transformation
616
617
	cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
	cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
618
	// crop the resulting image
619
620
	cv::getRectSubPix(rotatedPrevFrame, rect_size, rectTape.center, croppedPrevFrame);
	cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectTape.center, croppedCurrentFrame);
621
622
623

	// END CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/

Matteo's avatar
update    
Matteo committed
624
	cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
625
626
627
628
629
630
631

	int decEnd = (msToEnd % 1000) / 100;
	int secEnd = (msToEnd - (msToEnd % 1000)) / 1000;
	int minEnd = secEnd / 60;
	secEnd = secEnd % 60;


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
632
	/************************************* Segment analysis **************************************/
633

634
635
636
637
638
639
640
641
642
643
644
645
  	int blackPixels = 0;
	float mediaCurrFrame;
	int totColoreCF = 0;

	for (int i = 0; i < croppedCurrentFrame.rows; i++) {
		for (int j = 0; j < croppedCurrentFrame.cols; j++) {
			totColoreCF += croppedCurrentFrame.at<cv::Vec3b>(i, j)[0] + croppedCurrentFrame.at<cv::Vec3b>(i, j)[1] + croppedCurrentFrame.at<cv::Vec3b>(i, j)[2];
			if (differenceFrame.at<cv::Vec3b>(i, j)[0] == 0) {
				blackPixels++;
			}
		}
	}
646
	mediaCurrFrame = totColoreCF/tapeAreaPixels;
647

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
648
649
	/************************************* Decision stage ****************************************/

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
650
	bool isIrregularity = false;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
651

652
	if (blackPixels > tapeDifferentPixelsThreshold) { // The threshold must be passed
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
653

654
655
		/***** AVERAGE_COLOR-BASED DECISION *****/
		if (mediaPrevFrame > (mediaCurrFrame + 7) || mediaPrevFrame < (mediaCurrFrame - 7)) { // They are not similar for color average
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
656
			isIrregularity = true;
657
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
658

659
		/***** BRANDS MANAGEMENT *****/
Matteo's avatar
update    
Matteo committed
660
		if (config.brands) {
661
			// At the beginning of the video, wait at least 5 seconds before the next Irregularity to consider it as a brand.
662
			// It is not guaranteed that it will be the first brand, but it is generally a safe approach to have a correct image
663
664
665
666
			if (firstBrand) {
				if (firstInstant - msToEnd > 5000) {
					firstBrand = false;
					savingBrand = true;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
667
					isIrregularity = true;
668
				}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
669
			// In the following iterations reset savingBrand, since we are no longer interested in brands.
670
			} else
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
671
				savingBrand = false;
672
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
673

674
	}
675
676
677
678

	// Update mediaPrevFrame
	mediaPrevFrame = mediaCurrFrame;

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
679
	return isIrregularity;
680
681
682
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
683
684
685
686
687
688
689
/**
 * @brief video processing phase, where each frame is analysed.
 * It saves the IrregularityImages and updates the IrregularityFiles if an Irregularity is found
 *
 * @param videoCapture the input Preservation Audio-Visual File;
 */
void processing(cv::VideoCapture videoCapture) {
690
691
692
693
694
695

	// Video duration
	int frameNumbers_v = videoCapture.get(CAP_PROP_FRAME_COUNT);
	float fps_v = videoCapture.get(CAP_PROP_FPS); // FPS can be non-integers!!!
	float videoLength = (float) frameNumbers_v / fps_v; // [s]
	int videoLength_ms = videoLength * 1000;
696

697
698
    int savedFrames = 0, unsavedFrames = 0;
	float lastSaved = -160;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
699
	// Whenever we find an Irregularity, we want to skip a lenght equal to the Studer reading head (3 cm = 1.18 inches).
700
701
	int savingRate = 79; // [ms]. Time taken to cross 3 cm at 15 ips, or 1.5 cm at 7.5 ips. The considered lengths are the widths of the tape areas.
	// The following condition constitutes a valid approach if the tape areas have widths always equal to the reading head
Matteo's avatar
update    
Matteo committed
702
	if (config.speed == 7.5)
703
704
705
706
707
		savingRate = 157; // Time taken to cross 3 cm at 7.5 ips

	// The first frame of the video won't be processed
    cv::Mat prevFrame;
	videoCapture >> prevFrame;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
708
	firstInstant = videoLength_ms - videoCapture.get(CAP_PROP_POS_MSEC);
709
710
711
712
713
714
715
716
717
718
719
720

    while (videoCapture.isOpened()) {

		cv::Mat frame;
        videoCapture >> frame;

        if (!frame.empty()) {

			int ms = videoCapture.get(CAP_PROP_POS_MSEC);
			int msToEnd = videoLength_ms - ms;
			if (ms == 0) // With OpenCV library, this happens at the last few frames of the video before realising that "frame" is empty.
				break;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
721
722

			// Variables to display program status
723
724
725
726
			int secToEnd = msToEnd / 1000;
			int minToEnd = (secToEnd / 60) % 60;
			secToEnd = secToEnd % 60;

727
			string secStrToEnd = to_string(secToEnd), minStrToEnd = to_string(minToEnd);
728
729
730
731
732
			if (minToEnd < 10)
				minStrToEnd = "0" + minStrToEnd;
			if (secToEnd < 10)
				secStrToEnd = "0" + secStrToEnd;

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
733
			// Display program status
734
735
			cout << "\rIrregularities: " << savedFrames << ".   ";
			cout << "Remaining video time [mm:ss]: " << minStrToEnd << ":" << secStrToEnd << flush;
736
737

			if ((ms - lastSaved > savingRate) && frameDifference(prevFrame, frame, msToEnd)) {
738

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
739
				// An Irregularity has been found!
740
741
742
743

				// De-interlacing frame
				cv::Mat oddFrame(frame.rows/2, frame.cols, CV_8UC3);
				cv::Mat evenFrame(frame.rows/2, frame.cols, CV_8UC3);
Matteo's avatar
update    
Matteo committed
744
				utility::separateFrame(frame, oddFrame, evenFrame);
745

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
746
				// Extract the image corresponding to the ROIs
747
				Point2f pts[4];
748
749
750
751
752
				if (savingPinchRoller)
					rectCapstan.points(pts);
				else
					rectTape.points(pts);
				cv::Mat subImage(frame, cv::Rect(100, min(pts[1].y, pts[2].y), frame.cols - 100, static_cast<int>(rectTape.size.height)));
753

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
754
				// De-interlacing
755
756
757
				cv::Mat oddSubImage(subImage.rows/2, subImage.cols, CV_8UC3);
				int evenSubImageRows = subImage.rows/2;
				if (subImage.rows % 2 != 0) // If the found rectangle is of odd height, we must increase evenSubImage height by 1, otherwise we have segmentation_fault!!!
758
					evenSubImageRows += 1;
759
				cv::Mat evenSubImage(evenSubImageRows, subImage.cols, CV_8UC3);
Matteo's avatar
update    
Matteo committed
760
				utility::separateFrame(subImage, oddSubImage, evenSubImage);
761

Matteo's avatar
update    
Matteo committed
762
763
				string timeLabel = getTimeLabel(ms, ":");
				string safeTimeLabel = getTimeLabel(ms, "-");
764

765
766
				string irregularityImageFilename = to_string(savedFrames) + "_" + safeTimeLabel + ".jpg";
				cv::imwrite(irregularityImagesPath / irregularityImageFilename, oddFrame);
767
768
769

				// Append Irregularity information to JSON
				boost::uuids::uuid uuid = boost::uuids::random_generator()();
770
771
772
				irregularityFileOutput1["Irregularities"] += {
					{
						"IrregularityID", boost::lexical_cast<string>(uuid)
773
774
775
776
777
778
					}, {
						"Source", "v"
					}, {
						"TimeLabel", timeLabel
					}
				};
779
780
781
				irregularityFileOutput2["Irregularities"] += {
					{
						"IrregularityID", boost::lexical_cast<string>(uuid)
782
783
784
785
786
					}, {
						"Source", "v"
					}, {
						"TimeLabel", timeLabel
					}, {
787
						"ImageURI", irregularityImagesPath.string() + "/" + irregularityImageFilename
788
789
790
791
792
793
794
795
796
797
798
799
800
					}
				};

				lastSaved = ms;
				savedFrames++;

			} else {
				unsavedFrames++;
			}

			prevFrame = frame;

	    } else {
801
			cout << endl << "Empty frame!" << endl;
802
803
804
805
806
807
808
	    	videoCapture.release();
	    	break;
	    }
	}

	ofstream myFile;
	myFile.open("log.txt", ios::app);
809
	myFile << "Saved frames are: " << savedFrames << endl;
810
811
812
813
814
	myFile.close();

}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
/*************************************************************************************************/
/********************************************* MAIN **********************************************/
/*************************************************************************************************/

/**
 * @brief main program, organised as:
 * - Get input from command line or config.json file;
 * - Check input parameters;
 * - Creation of output directories;
 * - Regions Of Interest (ROIs) detection;
 * - Irregularities detection;
 * - Saving of output IrregularityFiles.
 *
 * @param argc Command line arguments count;
 * @param argv Command line arguments.
 * @return int program status.
 */
832
833
int main(int argc, char** argv) {

Matteo's avatar
update    
Matteo committed
834
835
	json irregularityFileInput;
	fs::path irregularityFileInputPath;
Matteo's avatar
update    
Matteo committed
836
837
	cv::Mat myFrame;

838
839
840
	/*********************************************************************************************/
	/*************************************** CONFIGURATION ***************************************/
	/*********************************************************************************************/
841

842
	// Get the input from config.json or command line
843
	try {
844
845
846
		bool continueExecution = getArguments(argc, argv);
		if (!continueExecution) {
			return 0;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
847
		}
848
	} catch (nlohmann::detail::type_error e) {
849
		cerr << RED << "config.json error!" << endl << e.what() << END << endl;
850
851
		return -1;
	}
852

Matteo's avatar
update    
Matteo committed
853
854
855
856
	fs::path VIDEO_PATH = config.workingPath / "PreservationAudioVisualFile" / config.filesName;

    if (files::findFileName(VIDEO_PATH, fileName, extension) == -1) {
        cerr << RED << BOLD << "config.json error!" << END << endl << RED << VIDEO_PATH.string() << " cannot be found or opened." << END << endl;
857
858
859
        return -1;
    }

Matteo's avatar
update    
Matteo committed
860
861
	irregularityFileInputPath = config.workingPath / "temp" / fileName / "AudioAnalyser_IrregularityFileOutput1.json";

862

863
	// Input JSON check
864
	ifstream iJSON(irregularityFileInputPath);
865
	if (iJSON.fail()) {
866
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
867
868
		return -1;
	}
Matteo's avatar
update    
Matteo committed
869
	if (config.speed != 7.5 && config.speed != 15) {
870
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
871
872
		return -1;
	}
Matteo's avatar
update    
Matteo committed
873
	if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
874
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
875
876
		return -1;
	}
Matteo's avatar
update    
Matteo committed
877
	if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
878
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl;
879
880
881
		return -1;
	}

882
	// Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
update    
Matteo committed
883
884
885
	if (config.brands) {
		if (config.speed == 15)
			tape.threshold.percentual += 6;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
886
	} else
Matteo's avatar
update    
Matteo committed
887
888
		if (config.speed == 15)
			tape.threshold.percentual += 20;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
889
		else
Matteo's avatar
update    
Matteo committed
890
			tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
891

892
893
    cout << endl;
	cout << "Parameters:" << endl;
Matteo's avatar
update    
Matteo committed
894
895
896
897
	cout << "    Brands: " << config.brands << endl;
	cout << "    Speed: " << config.speed << endl;
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
	cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
898
	cout << endl;
899
900
901
902

	// Read input JSON
	iJSON >> irregularityFileInput;

903
904
905
	/*********************************************************************************************/
	/*********************************** MAKE OUTPUT DIRECTORY ***********************************/
	/*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
906
907

	// Make directory with fileName name
Matteo's avatar
update    
Matteo committed
908
	outputPath = config.workingPath / "temp" / fileName;
909
	int outputFileNameDirectory = create_directory(outputPath);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
910
	// Get now time
911
912
913
	time_t t = chrono::system_clock::to_time_t(chrono::system_clock::now());
    string ts = ctime(&t);
	// Write useful info to log file
Matteo's avatar
update    
Matteo committed
914
915
916
	fs::path logPath = outputPath / "log.txt";

	string logInfo = fileName + '\n' + "tsh: " + to_string(tape.threshold.percentual) + "   tshp: " + to_string(capstan.threshold.percentual) + '\n' + ts;
Matteo's avatar
update    
Matteo committed
917
	files::saveFile(logPath, logInfo, true);
Matteo's avatar
update    
Matteo committed
918

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
919

920
921
922
	/*********************************************************************************************/
	/************************************** AREAS DETECTION **************************************/
	/*********************************************************************************************/
923

Matteo's avatar
update    
Matteo committed
924
	cv::VideoCapture videoCapture(VIDEO_PATH);
925
    if (!videoCapture.isOpened()) {
926
        cerr << RED << BOLD << "Video unreadable." << END << endl;
927
928
929
930
931
932
933
        return -1;
    }

	// Get total number of frames
	int totalFrames = videoCapture.get(CAP_PROP_FRAME_COUNT);
	// Set frame position to half video length
	videoCapture.set(CAP_PROP_POS_FRAMES, totalFrames/2);
934
	// Get frame
935
	videoCapture >> myFrame;
936
937
938

	cout << "Video resolution: " << myFrame.cols << "x" << myFrame.rows << endl << endl;

939
	// Find the processing area corresponding to the tape area over the reading head
Matteo's avatar
update    
Matteo committed
940
	bool found = findProcessingAreas(myFrame);
941
942
943
944

	// Reset frame position
	videoCapture.set(CAP_PROP_POS_FRAMES, 0);

945
	// Write useful information to log file
Matteo's avatar
update    
Matteo committed
946
	string message;
947
	if (found) {
Matteo's avatar
update    
Matteo committed
948
949
		message = "Processing areas found!\n";
		cout << message;
Matteo's avatar
update    
Matteo committed
950
		files::saveFile(logPath, message, true);
951
	} else {
Matteo's avatar
update    
Matteo committed
952
953
		message = "Processing area not found. Try changing JSON parameters.\n";
		cout << message;
Matteo's avatar
update    
Matteo committed
954
		files::saveFile(logPath, message, true);
955
		return -1; // Program terminated early
956
957
	}

958
959
960
	/*********************************************************************************************/
	/***************************** MAKE ADDITIONAL OUTPUT DIRECTORIES ****************************/
	/*********************************************************************************************/
961

962
963
964
965
	irregularityImagesPath = outputPath / "IrregularityImages";
	int fullFrameDirectory = fs::create_directory(irregularityImagesPath);

	/*********************************************************************************************/
966
	/**************************************** PROCESSING *****************************************/
967
	/*********************************************************************************************/
968

Matteo's avatar
update    
Matteo committed
969
	cout << '\n' << CYAN << "Starting processing..." << END << '\n';
970
971
972
973
974

	// Processing timer
	time_t startTimer, endTimer;
	startTimer = time(NULL);

975
	processing(videoCapture);
976
977
978
979
980

	endTimer = time(NULL);
	float min = (endTimer - startTimer) / 60;
	float sec = (endTimer - startTimer) % 60;

981
982
	string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
	cout << endl << result << endl;
983

Matteo's avatar
update    
Matteo committed
984
	files::saveFile("log.txt", result + '\n', true);
985

986
987
988
	/*********************************************************************************************/
	/************************************* IRREGULARITY FILES ************************************/
	/*********************************************************************************************/
989

990
	fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
Matteo's avatar
update    
Matteo committed
991
	files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
992
993

	// Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier
Matteo's avatar
update    
Matteo committed
994
	extractIrregularityImagesForAudio(outputPath, VIDEO_PATH, irregularityFileInput, irregularityFileOutput2);
995

996
	fs::path outputFile2Name = outputPath / "VideoAnalyser_IrregularityFileOutput2.json";
Matteo's avatar
update    
Matteo committed
997
	files::saveFile(outputFile2Name, irregularityFileOutput2.dump(4), false);
998

999
1000
    return 0;
}