main.cpp 35.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
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
/**
Matteo's avatar
Matteo committed
197
198
199
200
201
202
203
204
 * @brief Find the model in the scene using the Generalized Hough Transform. It returns the best matches.
 * 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
 * 
 * @param model the template image to be searched with the Generalized Hough Transform
 * @param object the sceneObject struct containing the parameters for the Generalized Hough Transform
 * @return std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> a tuple containing the best matches for positive and negative angles
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
205
 */
Matteo's avatar
Matteo committed
206
std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> findObject(Mat model, SceneObject object, Mat processing_area) {
207
208

	// Algorithm and parameters
Matteo's avatar
Matteo committed
209
	// for informations about the Generalized Hough Guild usage see the tutorial at https://docs.opencv.org/4.7.0/da/ddc/tutorial_generalized_hough_ballard_guil.html
210
211
212
213
214
215
216
217
	Ptr<GeneralizedHoughGuil> alg = createGeneralizedHoughGuil();

	vector<Vec4f> positionsPos, positionsNeg;
	Mat votesPos, votesNeg;

	double maxValPos = 0, maxValNeg = 0;
	int indexPos = 0, indexNeg = 0;

Matteo's avatar
Matteo committed
218
	alg->setMinDist(object.minDist);
Matteo's avatar
update    
Matteo committed
219
220
221
	alg->setLevels(360);
	alg->setDp(2);
	alg->setMaxBufferSize(1000);
222

Matteo's avatar
update    
Matteo committed
223
	alg->setAngleStep(1);
Matteo's avatar
Matteo committed
224
	alg->setAngleThresh(object.threshold.angle);
225

Matteo's avatar
update    
Matteo committed
226
227
228
	alg->setMinScale(0.9);
	alg->setMaxScale(1.1);
	alg->setScaleStep(0.01);
Matteo's avatar
Matteo committed
229
	alg->setScaleThresh(object.threshold.scale);
230

Matteo's avatar
Matteo committed
231
	alg->setPosThresh(object.threshold.pos);
232

Matteo's avatar
update    
Matteo committed
233
234
	alg->setCannyLowThresh(150); // Old: 100
	alg->setCannyHighThresh(240); // Old: 300
235

Matteo's avatar
Matteo committed
236
	alg->setTemplate(model);
237

Matteo's avatar
Matteo committed
238
	utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg, processing_area);
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253

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

Matteo's avatar
Matteo committed
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
	return { indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg };
}

/**
 * @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.
 * @param myFrame The current frame of the video.
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
bool findProcessingAreas(Mat myFrame) {

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

	// Save a grayscale version of myFrame in myFrameGrayscale and downsample it in half pixels for performance reasons
	Frame gray_current_frame = Frame(myFrame)
		.convertColor(COLOR_BGR2GRAY);

	Frame halved_gray_current_frame = gray_current_frame
		.clone()
		.downsample(2);

	// Get input shape in grayscale and downsample it in half pixels
	Frame reading_head_template = Frame(cv::imread(READING_HEAD_IMG, IMREAD_GRAYSCALE)).downsample(2);

	// Process only the bottom-central portion of the input video -> best results with our videos
	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);

	ofstream myFile;
	RotatedRect rectPos, rectNeg;
	auto [indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg] = findObject(reading_head_template, tape, processingImage);

297
298
	// 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
299
		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);
300
	if (positionsNeg.size() > 0)
Matteo's avatar
update    
Matteo committed
301
		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);
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
330
331

	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
332
	rectTape = utility::drawShapes(myFrame, positionTape, Scalar(0, 255-indexPos*64, 0), rect.size.width, 50 * (rect.size.width / 200), 0, 0, 1);
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354

	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
355
	files::saveFile(fs::path("./" + fileName + ".json"), autoJSON.dump(4), false);
356
357
358
359
360
361

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

	// Read template image - it is smaller than before, therefore there is no need to downsample
Matteo's avatar
update    
Matteo committed
362
	Mat templateShape = imread(CAPSTAN_TEMPLATE_IMG, IMREAD_GRAYSCALE);
363
364
365
366
367
368
369
370
371
372
373
	// 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
374
		detector->detectAndCompute(gray_current_frame, noArray(), keypoints_scene, descriptors_scene);
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390

		// 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
391
		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);
392
393
394
		// 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

	} else {

Matteo's avatar
Matteo committed
422
		// Process only right portion of the image, where the capstain always appears
423
424
425
426
427
		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
428
		Mat capstanProcessingAreaGrayscale = gray_current_frame(capstanProcessingAreaRect);
429
430
		// Reset algorithm and set parameters

Matteo's avatar
Matteo committed
431
		auto [indexPos, indexNeg, maxValPos, maxValNeg, positionsC1Pos, positionsC1Neg] = findObject(templateShape, capstan, capstanProcessingAreaGrayscale);
432
433
434

		RotatedRect rectCapstanPos, rectCapstanNeg;
		if (positionsC1Pos.size() > 0)
Matteo's avatar
update    
Matteo committed
435
			rectCapstanPos = utility::drawShapes(myFrame, positionsC1Pos[indexPos], Scalar(255-indexPos*64, 0, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
436
		if (positionsC1Neg.size() > 0)
Matteo's avatar
update    
Matteo committed
437
			rectCapstanNeg = utility::drawShapes(myFrame, positionsC1Neg[indexNeg], Scalar(255-indexNeg*64, 128, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475

		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
476
477
478
479
480
481
482
483
484
485
/**
 * @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.
 */
486
487
bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd) {

488
	/*********************************************************************************************/
489
	/********************************** Capstan analysis *****************************************/
490
	/*********************************************************************************************/
491
492

	// In the last minute of the video, check for pinchRoller position for endTape event
493
	if (!endTapeSaved && msToEnd < 60000) {
494
495
496

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
499
		// Extract matrices corresponding to the processing area
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
		// 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
515
516
		cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
		cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
517
		// crop the resulting image
518
519
		cv::getRectSubPix(rotatedPrevFrame, rect_size, rectCapstan.center, croppedPrevFrame);
		cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectCapstan.center, croppedCurrentFrame);
520
521
522

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

Matteo's avatar
update    
Matteo committed
523
		cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
524
525
526
527
528
529
530
531
532
533
534
535
536
537

		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;
538
			endTapeSaved = true; // Never check again for end tape instant
539
540
541
542
			return true;
		} else {
			savingPinchRoller = false;
		}
543
544
	} 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
545
	}
546
547

	/*********************************************************************************************/
548
	/************************************ Tape analysis ******************************************/
549
	/*********************************************************************************************/
550
551
552

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
555
	// Extract matrices corresponding to the processing area
556
557
558
559
560
	// 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
561
562
	float angle = rectTape.angle;
	Size rect_size = rectTape.size;
563
	// thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/
564
	if (rectTape.angle < -45.) {
565
566
567
568
		angle += 90.0;
		swap(rect_size.width, rect_size.height);
	}
	// get the rotation matrix
569
	M = getRotationMatrix2D(rectTape.center, angle, 1.0);
570
	// perform the affine transformation
571
572
	cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
	cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
573
	// crop the resulting image
574
575
	cv::getRectSubPix(rotatedPrevFrame, rect_size, rectTape.center, croppedPrevFrame);
	cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectTape.center, croppedCurrentFrame);
576
577
578

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

Matteo's avatar
update    
Matteo committed
579
	cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
580
581
582
583
584
585
586

	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
587
	/************************************* Segment analysis **************************************/
588

589
590
591
592
593
594
595
596
597
598
599
600
  	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++;
			}
		}
	}
601
	mediaCurrFrame = totColoreCF/tapeAreaPixels;
602

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
603
604
	/************************************* Decision stage ****************************************/

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
605
	bool isIrregularity = false;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
606

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

609
610
		/***** 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
611
			isIrregularity = true;
612
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
613

614
		/***** BRANDS MANAGEMENT *****/
Matteo's avatar
update    
Matteo committed
615
		if (config.brands) {
616
			// At the beginning of the video, wait at least 5 seconds before the next Irregularity to consider it as a brand.
617
			// It is not guaranteed that it will be the first brand, but it is generally a safe approach to have a correct image
618
619
620
621
			if (firstBrand) {
				if (firstInstant - msToEnd > 5000) {
					firstBrand = false;
					savingBrand = true;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
622
					isIrregularity = true;
623
				}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
624
			// In the following iterations reset savingBrand, since we are no longer interested in brands.
625
			} else
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
626
				savingBrand = false;
627
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
628

629
	}
630
631
632
633

	// Update mediaPrevFrame
	mediaPrevFrame = mediaCurrFrame;

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
634
	return isIrregularity;
635
636
637
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
638
639
640
641
642
643
644
/**
 * @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) {
645
646
647
648
649
650

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

652
653
    int savedFrames = 0, unsavedFrames = 0;
	float lastSaved = -160;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
654
	// Whenever we find an Irregularity, we want to skip a lenght equal to the Studer reading head (3 cm = 1.18 inches).
655
656
	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
657
	if (config.speed == 7.5)
658
659
660
661
662
		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
663
	firstInstant = videoLength_ms - videoCapture.get(CAP_PROP_POS_MSEC);
664
665
666
667
668
669
670
671
672
673
674
675

    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
676
677

			// Variables to display program status
678
679
680
681
			int secToEnd = msToEnd / 1000;
			int minToEnd = (secToEnd / 60) % 60;
			secToEnd = secToEnd % 60;

682
			string secStrToEnd = to_string(secToEnd), minStrToEnd = to_string(minToEnd);
683
684
685
686
687
			if (minToEnd < 10)
				minStrToEnd = "0" + minStrToEnd;
			if (secToEnd < 10)
				secStrToEnd = "0" + secStrToEnd;

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
688
			// Display program status
689
690
			cout << "\rIrregularities: " << savedFrames << ".   ";
			cout << "Remaining video time [mm:ss]: " << minStrToEnd << ":" << secStrToEnd << flush;
691
692

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
694
				// An Irregularity has been found!
695
696
697
698

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
701
				// Extract the image corresponding to the ROIs
702
				Point2f pts[4];
703
704
705
706
707
				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)));
708

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
709
				// De-interlacing
710
711
712
				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!!!
713
					evenSubImageRows += 1;
714
				cv::Mat evenSubImage(evenSubImageRows, subImage.cols, CV_8UC3);
Matteo's avatar
update    
Matteo committed
715
				utility::separateFrame(subImage, oddSubImage, evenSubImage);
716

Matteo's avatar
update    
Matteo committed
717
718
				string timeLabel = getTimeLabel(ms, ":");
				string safeTimeLabel = getTimeLabel(ms, "-");
719

720
721
				string irregularityImageFilename = to_string(savedFrames) + "_" + safeTimeLabel + ".jpg";
				cv::imwrite(irregularityImagesPath / irregularityImageFilename, oddFrame);
722
723
724

				// Append Irregularity information to JSON
				boost::uuids::uuid uuid = boost::uuids::random_generator()();
725
726
727
				irregularityFileOutput1["Irregularities"] += {
					{
						"IrregularityID", boost::lexical_cast<string>(uuid)
728
729
730
731
732
733
					}, {
						"Source", "v"
					}, {
						"TimeLabel", timeLabel
					}
				};
734
735
736
				irregularityFileOutput2["Irregularities"] += {
					{
						"IrregularityID", boost::lexical_cast<string>(uuid)
737
738
739
740
741
					}, {
						"Source", "v"
					}, {
						"TimeLabel", timeLabel
					}, {
742
						"ImageURI", irregularityImagesPath.string() + "/" + irregularityImageFilename
743
744
745
746
747
748
749
750
751
752
753
754
755
					}
				};

				lastSaved = ms;
				savedFrames++;

			} else {
				unsavedFrames++;
			}

			prevFrame = frame;

	    } else {
756
			cout << endl << "Empty frame!" << endl;
757
758
759
760
761
762
763
	    	videoCapture.release();
	    	break;
	    }
	}

	ofstream myFile;
	myFile.open("log.txt", ios::app);
764
	myFile << "Saved frames are: " << savedFrames << endl;
765
766
767
768
769
	myFile.close();

}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
/*************************************************************************************************/
/********************************************* 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.
 */
787
788
int main(int argc, char** argv) {

Matteo's avatar
update    
Matteo committed
789
790
	json irregularityFileInput;
	fs::path irregularityFileInputPath;
Matteo's avatar
update    
Matteo committed
791
792
	cv::Mat myFrame;

793
794
795
	/*********************************************************************************************/
	/*************************************** CONFIGURATION ***************************************/
	/*********************************************************************************************/
796

797
	// Get the input from config.json or command line
798
	try {
799
800
801
		bool continueExecution = getArguments(argc, argv);
		if (!continueExecution) {
			return 0;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
802
		}
803
	} catch (nlohmann::detail::type_error e) {
804
		cerr << RED << "config.json error!" << endl << e.what() << END << endl;
805
806
		return -1;
	}
807

Matteo's avatar
update    
Matteo committed
808
809
810
811
	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;
812
813
814
        return -1;
    }

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

817

818
	// Input JSON check
819
	ifstream iJSON(irregularityFileInputPath);
820
	if (iJSON.fail()) {
821
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
822
823
		return -1;
	}
Matteo's avatar
update    
Matteo committed
824
	if (config.speed != 7.5 && config.speed != 15) {
825
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
826
827
		return -1;
	}
Matteo's avatar
update    
Matteo committed
828
	if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
829
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
830
831
		return -1;
	}
Matteo's avatar
update    
Matteo committed
832
	if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
833
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl;
834
835
836
		return -1;
	}

837
	// Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
update    
Matteo committed
838
839
840
	if (config.brands) {
		if (config.speed == 15)
			tape.threshold.percentual += 6;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
841
	} else
Matteo's avatar
update    
Matteo committed
842
843
		if (config.speed == 15)
			tape.threshold.percentual += 20;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
844
		else
Matteo's avatar
update    
Matteo committed
845
			tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
846

847
848
    cout << endl;
	cout << "Parameters:" << endl;
Matteo's avatar
update    
Matteo committed
849
850
851
852
	cout << "    Brands: " << config.brands << endl;
	cout << "    Speed: " << config.speed << endl;
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
	cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
853
	cout << endl;
854
855
856
857

	// Read input JSON
	iJSON >> irregularityFileInput;

858
859
860
	/*********************************************************************************************/
	/*********************************** MAKE OUTPUT DIRECTORY ***********************************/
	/*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
861
862

	// Make directory with fileName name
Matteo's avatar
update    
Matteo committed
863
	outputPath = config.workingPath / "temp" / fileName;
864
	int outputFileNameDirectory = create_directory(outputPath);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
865
	// Get now time
866
867
868
	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
869
870
871
	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
872
	files::saveFile(logPath, logInfo, true);
Matteo's avatar
update    
Matteo committed
873

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

875
876
877
	/*********************************************************************************************/
	/************************************** AREAS DETECTION **************************************/
	/*********************************************************************************************/
878

Matteo's avatar
update    
Matteo committed
879
	cv::VideoCapture videoCapture(VIDEO_PATH);
880
    if (!videoCapture.isOpened()) {
881
        cerr << RED << BOLD << "Video unreadable." << END << endl;
882
883
884
885
886
887
888
        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);
889
	// Get frame
890
	videoCapture >> myFrame;
891
892
893

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

894
	// Find the processing area corresponding to the tape area over the reading head
Matteo's avatar
update    
Matteo committed
895
	bool found = findProcessingAreas(myFrame);
896
897
898
899

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

900
	// Write useful information to log file
Matteo's avatar
update    
Matteo committed
901
	string message;
902
	if (found) {
Matteo's avatar
update    
Matteo committed
903
904
		message = "Processing areas found!\n";
		cout << message;
Matteo's avatar
update    
Matteo committed
905
		files::saveFile(logPath, message, true);
906
	} else {
Matteo's avatar
update    
Matteo committed
907
908
		message = "Processing area not found. Try changing JSON parameters.\n";
		cout << message;
Matteo's avatar
update    
Matteo committed
909
		files::saveFile(logPath, message, true);
910
		return -1; // Program terminated early
911
912
	}

913
914
915
	/*********************************************************************************************/
	/***************************** MAKE ADDITIONAL OUTPUT DIRECTORIES ****************************/
	/*********************************************************************************************/
916

917
918
919
920
	irregularityImagesPath = outputPath / "IrregularityImages";
	int fullFrameDirectory = fs::create_directory(irregularityImagesPath);

	/*********************************************************************************************/
921
	/**************************************** PROCESSING *****************************************/
922
	/*********************************************************************************************/
923

Matteo's avatar
update    
Matteo committed
924
	cout << '\n' << CYAN << "Starting processing..." << END << '\n';
925
926
927
928
929

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

930
	processing(videoCapture);
931
932
933
934
935

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

936
937
	string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
	cout << endl << result << endl;
938

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

941
942
943
	/*********************************************************************************************/
	/************************************* IRREGULARITY FILES ************************************/
	/*********************************************************************************************/
944

945
	fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
Matteo's avatar
update    
Matteo committed
946
	files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
947
948

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

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

954
955
    return 0;
}