main.cpp 36.1 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
using namespace cv;
using namespace std;
Matteo's avatar
update    
Matteo committed
54
using utility::Frame;
55
using json = nlohmann::json;
56
57
namespace fs = std::filesystem;
namespace po = boost::program_options;
58

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

Matteo's avatar
update    
Matteo committed
63
64
bool savingPinchRoller = false;
bool pinchRollerRect = false;
65
bool savingBrand = false;
66
bool endTapeSaved = false;
67
68
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
69
float firstInstant = 0;
70
string fileName, extension;
71

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

Matteo's avatar
update    
Matteo committed
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 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
102
103
104
static Config config;
static SceneObject tape;
static SceneObject capstan;
Matteo's avatar
update    
Matteo committed
105

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
111
112
113
114
115
116
117
118
/**
 * @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
119
bool getArguments(int argc, char** argv) {
120
	// Read configuration file
Matteo's avatar
update    
Matteo committed
121
	ifstream iConfig(CONFIG_FILE);
122
123
124
125
	iConfig >> configurationFile;

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

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

135
136
137
138
139
	} 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
140
				"By default, the configuartion parameters are loaded from config/config.json file,\n"
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
				"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
156
157
158
159
160
161
162

			// 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>();

163
164
165
166
167
168
169
170
171
		} 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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
	capstan = {
		configurationFile["MinDistCapstan"],
		{
			configurationFile["CapstanThresholdPercentual"],
			configurationFile["AngleThreshCapstan"],
			configurationFile["ScaleThreshCapstan"],
			configurationFile["PosThreshCapstan"]
		}
	};

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

	return true;
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
196
197
198
199
200
201
/**
 * @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
202
 * @param myFrame The current frame of the video.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
203
204
205
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
Matteo's avatar
update    
Matteo committed
206
bool findProcessingAreas(Mat myFrame) {
207
208
209
210
211

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

Matteo's avatar
update    
Matteo committed
212
	// Save a grayscale version of myFrame in myFrameGrayscale and downsample it in half pixels for performance reasons
Matteo's avatar
update    
Matteo committed
213
214
215
216
217
218
	Frame gray_current_frame = Frame(myFrame)
		.convertColor(COLOR_BGR2GRAY);

	Frame halved_gray_current_frame = gray_current_frame
		.clone()
		.downsample(2);
Matteo's avatar
update    
Matteo committed
219
220

	// Get input shape in grayscale and downsample it in half pixels
Matteo's avatar
update    
Matteo committed
221
	Frame reading_head_template = Frame(cv::imread(READING_HEAD_IMG, IMREAD_GRAYSCALE)).downsample(2);
222
223

	// Process only the bottom-central portion of the input video -> best results with our videos
Matteo's avatar
update    
Matteo committed
224
225
226
227
228
229
230
	Rect readingHeadProcessingAreaRect(
		halved_gray_current_frame.cols/4,
		halved_gray_current_frame.rows/2,
		halved_gray_current_frame.cols/2,
		halved_gray_current_frame.rows/2
	);
	Mat processingImage = halved_gray_current_frame(readingHeadProcessingAreaRect);
231
232

	// Algorithm and parameters
Matteo's avatar
update    
Matteo committed
233
	// 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
234
235
236
237
	Ptr<GeneralizedHoughGuil> alg = createGeneralizedHoughGuil();

	vector<Vec4f> positionsPos, positionsNeg;
	Mat votesPos, votesNeg;
Matteo's avatar
update    
Matteo committed
238
	int oldPosThresh = tape.threshold.pos;
239
240
241
242
243
244
245
246
247
	RotatedRect rectPos, rectNeg;
	ofstream myFile;

	// 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
248
249
250
251
	alg->setMinDist(tape.minDist);
	alg->setLevels(360);
	alg->setDp(2);
	alg->setMaxBufferSize(1000);
252

Matteo's avatar
update    
Matteo committed
253
254
	alg->setAngleStep(1);
	alg->setAngleThresh(tape.threshold.angle);
255

Matteo's avatar
update    
Matteo committed
256
257
258
259
	alg->setMinScale(0.9);
	alg->setMaxScale(1.1);
	alg->setScaleStep(0.01);
	alg->setScaleThresh(tape.threshold.scale);
260

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

Matteo's avatar
update    
Matteo committed
263
264
	alg->setCannyLowThresh(150); // Old: 100
	alg->setCannyHighThresh(240); // Old: 300
265

Matteo's avatar
update    
Matteo committed
266
	alg->setTemplate(reading_head_template);
267

Matteo's avatar
update    
Matteo committed
268
	utility::detectShape(alg, reading_head_template, tape.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg, processingImage);
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285

	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
286
		rectPos = utility::drawShapes(myFrame, positionsPos[indexPos], Scalar(0, 0, 255-indexPos*64), reading_head_template.cols, reading_head_template.rows, halved_gray_current_frame.cols/4, halved_gray_current_frame.rows/2, 2);
287
	if (positionsNeg.size() > 0)
Matteo's avatar
update    
Matteo committed
288
		rectNeg = utility::drawShapes(myFrame, positionsNeg[indexNeg], Scalar(128, 128, 255-indexNeg*64), reading_head_template.cols, reading_head_template.rows, halved_gray_current_frame.cols/4, halved_gray_current_frame.rows/2, 2);
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318

	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;
	}

	/*********************************************************************************************/
	/************************************ 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
319
	rectTape = utility::drawShapes(myFrame, positionTape, Scalar(0, 255-indexPos*64, 0), rect.size.width, 50 * (rect.size.width / 200), 0, 0, 1);
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341

	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
342
	files::saveFile(fs::path("./" + fileName + ".json"), autoJSON.dump(4), false);
343
344
345
346
347
348

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

	// Read template image - it is smaller than before, therefore there is no need to downsample
Matteo's avatar
update    
Matteo committed
349
	Mat templateShape = imread(CAPSTAN_TEMPLATE_IMG, IMREAD_GRAYSCALE);
350
351
352
353
354
355
356
357
358
359
360
	// templateShape = imread("../input/capstanBERIO058.png", IMREAD_GRAYSCALE);

	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);
Matteo's avatar
update    
Matteo committed
361
		detector->detectAndCompute(gray_current_frame, noArray(), keypoints_scene, descriptors_scene);
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377

		// 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;
Matteo's avatar
update    
Matteo committed
378
		drawMatches(templateShape, keypoints_object, halved_gray_current_frame, keypoints_scene, good_matches, img_matches, Scalar::all(-1), Scalar::all(-1), vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
379
380
381
		// Localize the object
		vector<Point2f> obj;
		vector<Point2f> scene;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
382
		for (size_t i = 0; i < good_matches.size(); i++) {
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
			// 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
404
		Vec4f positionCapstan(capstanX + 10, capstanY + 45, 1, 0);
Matteo's avatar
update    
Matteo committed
405
		rectCapstan = utility::drawShapes(myFrame, positionCapstan, Scalar(255-indexPos*64, 0, 0), templateShape.cols - 20, templateShape.rows - 90, 0, 0, 1);
406
407
408
409
410
411
412
413
414

	} 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);
Matteo's avatar
update    
Matteo committed
415
		Mat capstanProcessingAreaGrayscale = gray_current_frame(capstanProcessingAreaRect);
416
417
418
		// Reset algorithm and set parameters
		alg = createGeneralizedHoughGuil();

Matteo's avatar
update    
Matteo committed
419
420
421
422
		alg->setMinDist(capstan.minDist);
		alg->setLevels(360);
		alg->setDp(2);
		alg->setMaxBufferSize(1000);
423

Matteo's avatar
update    
Matteo committed
424
425
		alg->setAngleStep(1);
		alg->setAngleThresh(capstan.threshold.angle);
426

Matteo's avatar
update    
Matteo committed
427
428
429
430
		alg->setMinScale(0.9);
		alg->setMaxScale(1.1);
		alg->setScaleStep(0.01);
		alg->setScaleThresh(capstan.threshold.scale);
431

Matteo's avatar
update    
Matteo committed
432
		alg->setPosThresh(capstan.threshold.pos);
433

Matteo's avatar
update    
Matteo committed
434
435
		alg->setCannyLowThresh(150);
		alg->setCannyHighThresh(240);
436

Matteo's avatar
update    
Matteo committed
437
		alg->setTemplate(templateShape);
438

Matteo's avatar
update    
Matteo committed
439
		oldPosThresh = capstan.threshold.pos;
440
441
442
443

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

Matteo's avatar
update    
Matteo committed
444
		utility::detectShape(alg, templateShape, capstan.threshold.pos, positionsC1Pos, votesC1Pos, positionsC1Neg, votesC1Neg, capstanProcessingAreaGrayscale);
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466

		// 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
467
			rectCapstanPos = utility::drawShapes(myFrame, positionsC1Pos[indexPos], Scalar(255-indexPos*64, 0, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
468
		if (positionsC1Neg.size() > 0)
Matteo's avatar
update    
Matteo committed
469
			rectCapstanNeg = utility::drawShapes(myFrame, positionsC1Neg[indexNeg], Scalar(255-indexNeg*64, 128, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
470
471
472
473
474
475
476
477
478
479
480
481
482
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

		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
508
509
510
511
512
513
514
515
516
517
/**
 * @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.
 */
518
519
bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd) {

520
	/*********************************************************************************************/
521
	/********************************** Capstan analysis *****************************************/
522
	/*********************************************************************************************/
523
524

	// In the last minute of the video, check for pinchRoller position for endTape event
525
	if (!endTapeSaved && msToEnd < 60000) {
526
527
528

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
531
		// Extract matrices corresponding to the processing area
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
		// 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
547
548
		cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
		cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
549
		// crop the resulting image
550
551
		cv::getRectSubPix(rotatedPrevFrame, rect_size, rectCapstan.center, croppedPrevFrame);
		cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectCapstan.center, croppedCurrentFrame);
552
553
554

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

Matteo's avatar
update    
Matteo committed
555
		cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
556
557
558
559
560
561
562
563
564
565
566
567
568
569

		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;
570
			endTapeSaved = true; // Never check again for end tape instant
571
572
573
574
			return true;
		} else {
			savingPinchRoller = false;
		}
575
576
	} 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
577
	}
578
579

	/*********************************************************************************************/
580
	/************************************ Tape analysis ******************************************/
581
	/*********************************************************************************************/
582
583
584

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
587
	// Extract matrices corresponding to the processing area
588
589
590
591
592
	// 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
593
594
	float angle = rectTape.angle;
	Size rect_size = rectTape.size;
595
	// thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/
596
	if (rectTape.angle < -45.) {
597
598
599
600
		angle += 90.0;
		swap(rect_size.width, rect_size.height);
	}
	// get the rotation matrix
601
	M = getRotationMatrix2D(rectTape.center, angle, 1.0);
602
	// perform the affine transformation
603
604
	cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
	cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
605
	// crop the resulting image
606
607
	cv::getRectSubPix(rotatedPrevFrame, rect_size, rectTape.center, croppedPrevFrame);
	cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectTape.center, croppedCurrentFrame);
608
609
610

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

Matteo's avatar
update    
Matteo committed
611
	cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
612
613
614
615
616
617
618

	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
619
	/************************************* Segment analysis **************************************/
620

621
622
623
624
625
626
627
628
629
630
631
632
  	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++;
			}
		}
	}
633
	mediaCurrFrame = totColoreCF/tapeAreaPixels;
634

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
635
636
	/************************************* Decision stage ****************************************/

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
637
	bool isIrregularity = false;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
638

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

641
642
		/***** 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
643
			isIrregularity = true;
644
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
645

646
		/***** BRANDS MANAGEMENT *****/
Matteo's avatar
update    
Matteo committed
647
		if (config.brands) {
648
			// At the beginning of the video, wait at least 5 seconds before the next Irregularity to consider it as a brand.
649
			// It is not guaranteed that it will be the first brand, but it is generally a safe approach to have a correct image
650
651
652
653
			if (firstBrand) {
				if (firstInstant - msToEnd > 5000) {
					firstBrand = false;
					savingBrand = true;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
654
					isIrregularity = true;
655
				}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
656
			// In the following iterations reset savingBrand, since we are no longer interested in brands.
657
			} else
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
658
				savingBrand = false;
659
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
660

661
	}
662
663
664
665

	// Update mediaPrevFrame
	mediaPrevFrame = mediaCurrFrame;

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
666
	return isIrregularity;
667
668
669
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
670
671
672
673
674
675
676
/**
 * @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) {
677
678
679
680
681
682

	// 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;
683

684
685
    int savedFrames = 0, unsavedFrames = 0;
	float lastSaved = -160;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
686
	// Whenever we find an Irregularity, we want to skip a lenght equal to the Studer reading head (3 cm = 1.18 inches).
687
688
	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
689
	if (config.speed == 7.5)
690
691
692
693
694
		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
695
	firstInstant = videoLength_ms - videoCapture.get(CAP_PROP_POS_MSEC);
696
697
698
699
700
701
702
703
704
705
706
707

    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
708
709

			// Variables to display program status
710
711
712
713
			int secToEnd = msToEnd / 1000;
			int minToEnd = (secToEnd / 60) % 60;
			secToEnd = secToEnd % 60;

714
			string secStrToEnd = to_string(secToEnd), minStrToEnd = to_string(minToEnd);
715
716
717
718
719
			if (minToEnd < 10)
				minStrToEnd = "0" + minStrToEnd;
			if (secToEnd < 10)
				secStrToEnd = "0" + secStrToEnd;

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
720
			// Display program status
721
722
			cout << "\rIrregularities: " << savedFrames << ".   ";
			cout << "Remaining video time [mm:ss]: " << minStrToEnd << ":" << secStrToEnd << flush;
723
724

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
726
				// An Irregularity has been found!
727
728
729
730

				// 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
731
				utility::separateFrame(frame, oddFrame, evenFrame);
732

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
733
				// Extract the image corresponding to the ROIs
734
				Point2f pts[4];
735
736
737
738
739
				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)));
740

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
741
				// De-interlacing
742
743
744
				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!!!
745
					evenSubImageRows += 1;
746
				cv::Mat evenSubImage(evenSubImageRows, subImage.cols, CV_8UC3);
Matteo's avatar
update    
Matteo committed
747
				utility::separateFrame(subImage, oddSubImage, evenSubImage);
748

Matteo's avatar
update    
Matteo committed
749
750
				string timeLabel = getTimeLabel(ms, ":");
				string safeTimeLabel = getTimeLabel(ms, "-");
751

752
753
				string irregularityImageFilename = to_string(savedFrames) + "_" + safeTimeLabel + ".jpg";
				cv::imwrite(irregularityImagesPath / irregularityImageFilename, oddFrame);
754
755
756

				// Append Irregularity information to JSON
				boost::uuids::uuid uuid = boost::uuids::random_generator()();
757
758
759
				irregularityFileOutput1["Irregularities"] += {
					{
						"IrregularityID", boost::lexical_cast<string>(uuid)
760
761
762
763
764
765
					}, {
						"Source", "v"
					}, {
						"TimeLabel", timeLabel
					}
				};
766
767
768
				irregularityFileOutput2["Irregularities"] += {
					{
						"IrregularityID", boost::lexical_cast<string>(uuid)
769
770
771
772
773
					}, {
						"Source", "v"
					}, {
						"TimeLabel", timeLabel
					}, {
774
						"ImageURI", irregularityImagesPath.string() + "/" + irregularityImageFilename
775
776
777
778
779
780
781
782
783
784
785
786
787
					}
				};

				lastSaved = ms;
				savedFrames++;

			} else {
				unsavedFrames++;
			}

			prevFrame = frame;

	    } else {
788
			cout << endl << "Empty frame!" << endl;
789
790
791
792
793
794
795
	    	videoCapture.release();
	    	break;
	    }
	}

	ofstream myFile;
	myFile.open("log.txt", ios::app);
796
	myFile << "Saved frames are: " << savedFrames << endl;
797
798
799
800
801
	myFile.close();

}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
/*************************************************************************************************/
/********************************************* 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.
 */
819
820
int main(int argc, char** argv) {

Matteo's avatar
update    
Matteo committed
821
822
	json irregularityFileInput;
	fs::path irregularityFileInputPath;
Matteo's avatar
update    
Matteo committed
823
824
	cv::Mat myFrame;

825
826
827
	/*********************************************************************************************/
	/*************************************** CONFIGURATION ***************************************/
	/*********************************************************************************************/
828

829
	// Get the input from config.json or command line
830
	try {
831
832
833
		bool continueExecution = getArguments(argc, argv);
		if (!continueExecution) {
			return 0;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
834
		}
835
	} catch (nlohmann::detail::type_error e) {
836
		cerr << RED << "config.json error!" << endl << e.what() << END << endl;
837
838
		return -1;
	}
839

Matteo's avatar
update    
Matteo committed
840
841
842
843
	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;
844
845
846
        return -1;
    }

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

849

850
	// Input JSON check
851
	ifstream iJSON(irregularityFileInputPath);
852
	if (iJSON.fail()) {
853
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
854
855
		return -1;
	}
Matteo's avatar
update    
Matteo committed
856
	if (config.speed != 7.5 && config.speed != 15) {
857
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
858
859
		return -1;
	}
Matteo's avatar
update    
Matteo committed
860
	if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
861
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
862
863
		return -1;
	}
Matteo's avatar
update    
Matteo committed
864
	if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
865
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl;
866
867
868
		return -1;
	}

869
	// Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
update    
Matteo committed
870
871
872
	if (config.brands) {
		if (config.speed == 15)
			tape.threshold.percentual += 6;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
873
	} else
Matteo's avatar
update    
Matteo committed
874
875
		if (config.speed == 15)
			tape.threshold.percentual += 20;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
876
		else
Matteo's avatar
update    
Matteo committed
877
			tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
878

879
880
    cout << endl;
	cout << "Parameters:" << endl;
Matteo's avatar
update    
Matteo committed
881
882
883
884
	cout << "    Brands: " << config.brands << endl;
	cout << "    Speed: " << config.speed << endl;
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
	cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
885
	cout << endl;
886
887
888
889

	// Read input JSON
	iJSON >> irregularityFileInput;

890
891
892
	/*********************************************************************************************/
	/*********************************** MAKE OUTPUT DIRECTORY ***********************************/
	/*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
893
894

	// Make directory with fileName name
Matteo's avatar
update    
Matteo committed
895
	outputPath = config.workingPath / "temp" / fileName;
896
	int outputFileNameDirectory = create_directory(outputPath);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
897
	// Get now time
898
899
900
	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
901
902
903
	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
904
	files::saveFile(logPath, logInfo, true);
Matteo's avatar
update    
Matteo committed
905

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

907
908
909
	/*********************************************************************************************/
	/************************************** AREAS DETECTION **************************************/
	/*********************************************************************************************/
910

Matteo's avatar
update    
Matteo committed
911
	cv::VideoCapture videoCapture(VIDEO_PATH);
912
    if (!videoCapture.isOpened()) {
913
        cerr << RED << BOLD << "Video unreadable." << END << endl;
914
915
916
917
918
919
920
        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);
921
	// Get frame
922
	videoCapture >> myFrame;
923
924
925

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

926
	// Find the processing area corresponding to the tape area over the reading head
Matteo's avatar
update    
Matteo committed
927
	bool found = findProcessingAreas(myFrame);
928
929
930
931

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

932
	// Write useful information to log file
Matteo's avatar
update    
Matteo committed
933
	string message;
934
	if (found) {
Matteo's avatar
update    
Matteo committed
935
936
		message = "Processing areas found!\n";
		cout << message;
Matteo's avatar
update    
Matteo committed
937
		files::saveFile(logPath, message, true);
938
	} else {
Matteo's avatar
update    
Matteo committed
939
940
		message = "Processing area not found. Try changing JSON parameters.\n";
		cout << message;
Matteo's avatar
update    
Matteo committed
941
		files::saveFile(logPath, message, true);
942
		return -1; // Program terminated early
943
944
	}

945
946
947
	/*********************************************************************************************/
	/***************************** MAKE ADDITIONAL OUTPUT DIRECTORIES ****************************/
	/*********************************************************************************************/
948

949
950
951
952
	irregularityImagesPath = outputPath / "IrregularityImages";
	int fullFrameDirectory = fs::create_directory(irregularityImagesPath);

	/*********************************************************************************************/
953
	/**************************************** PROCESSING *****************************************/
954
	/*********************************************************************************************/
955

Matteo's avatar
update    
Matteo committed
956
	cout << '\n' << CYAN << "Starting processing..." << END << '\n';
957
958
959
960
961

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

962
	processing(videoCapture);
963
964
965
966
967

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

968
969
	string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
	cout << endl << result << endl;
970

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

973
974
975
	/*********************************************************************************************/
	/************************************* IRREGULARITY FILES ************************************/
	/*********************************************************************************************/
976

977
	fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
Matteo's avatar
update    
Matteo committed
978
	files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
979
980

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

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

986
987
    return 0;
}