main.cpp 33.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
#include <sys/timeb.h>
Matteo's avatar
update    
Matteo committed
24
#include <ranges>
25

26
#include <boost/program_options.hpp>
27
28
29
30
31
32
33
34
35
36
37
38
#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>

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

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

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

53
54
using namespace cv;
using namespace std;
Matteo's avatar
update    
Matteo committed
55
using utility::Frame;
56
using json = nlohmann::json;
57
58
namespace fs = std::filesystem;
namespace po = boost::program_options;
59

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

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

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

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

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

struct SceneObject {
	int minDist;
	Threshold threshold;
};

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

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

Matteo's avatar
update    
Matteo committed
112
113
114
115
double rotatedRectArea(RotatedRect rect) {
	return rect.size.width * rect.size.height;
}

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

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

		config = {
Matteo's avatar
update    
Matteo committed
134
			fs::path(working_path),
Matteo's avatar
update    
Matteo committed
135
136
137
138
139
			configurationFile["FilesName"],
			configurationFile["Brands"],
			configurationFile["Speed"]
		};

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

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

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

	tape = {
		configurationFile["MinDist"],
		{
			configurationFile["TapeThresholdPercentual"],
			configurationFile["AngleThresh"],
			configurationFile["ScaleThresh"],
			configurationFile["PosThresh"]
		}
	};
196
197
198
199
200

	return true;
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
201
/**
Matteo's avatar
Matteo committed
202
203
204
205
206
207
208
209
 * @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
210
 */
Matteo's avatar
Matteo committed
211
std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> findObject(Mat model, SceneObject object, Mat processing_area) {
212
213

	// Algorithm and parameters
Matteo's avatar
Matteo committed
214
	// 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
215
216
217
218
219
220
221
222
	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
223
	alg->setMinDist(object.minDist);
Matteo's avatar
update    
Matteo committed
224
225
226
	alg->setLevels(360);
	alg->setDp(2);
	alg->setMaxBufferSize(1000);
227

Matteo's avatar
update    
Matteo committed
228
	alg->setAngleStep(1);
Matteo's avatar
Matteo committed
229
	alg->setAngleThresh(object.threshold.angle);
230

Matteo's avatar
update    
Matteo committed
231
232
233
	alg->setMinScale(0.9);
	alg->setMaxScale(1.1);
	alg->setScaleStep(0.01);
Matteo's avatar
Matteo committed
234
	alg->setScaleThresh(object.threshold.scale);
235

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

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

Matteo's avatar
Matteo committed
241
	alg->setTemplate(model);
242

Matteo's avatar
Matteo committed
243
	utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg, processing_area);
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258

	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
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
297
298
299
300
	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);

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

301
302
	// 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
303
		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);
304
	if (positionsNeg.size() > 0)
Matteo's avatar
update    
Matteo committed
305
		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);
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328

	if (maxValPos > 0)
		if (maxValNeg > 0)
			if (maxValPos > maxValNeg) {
				rect = rectPos;
			} else {
				rect = rectNeg;
			}
		else {
			rect = rectPos;
		}
	else if (maxValNeg > 0) {
		rect = rectNeg;
	} else {
		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
329
	rectTape = utility::drawShapes(myFrame, positionTape, Scalar(0, 255-indexPos*64, 0), rect.size.width, 50 * (rect.size.width / 200), 0, 0, 1);
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346

	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
347
	files::saveFile(fs::path("./" + fileName + ".json"), autoJSON.dump(4), false);
348
349
350
351
352
353

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

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

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

	} else {

Matteo's avatar
Matteo committed
414
		// Process only right portion of the image, where the capstain always appears
415
416
417
418
419
		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
420
		Mat capstanProcessingAreaGrayscale = gray_current_frame(capstanProcessingAreaRect);
421
422
		// Reset algorithm and set parameters

Matteo's avatar
Matteo committed
423
		auto [indexPos, indexNeg, maxValPos, maxValNeg, positionsC1Pos, positionsC1Neg] = findObject(templateShape, capstan, capstanProcessingAreaGrayscale);
424
425
426

		RotatedRect rectCapstanPos, rectCapstanNeg;
		if (positionsC1Pos.size() > 0)
Matteo's avatar
update    
Matteo committed
427
			rectCapstanPos = utility::drawShapes(myFrame, positionsC1Pos[indexPos], Scalar(255-indexPos*64, 0, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
428
		if (positionsC1Neg.size() > 0)
Matteo's avatar
update    
Matteo committed
429
			rectCapstanNeg = utility::drawShapes(myFrame, positionsC1Neg[indexNeg], Scalar(255-indexNeg*64, 128, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456

		if (maxValPos > 0)
			if (maxValNeg > 0)
				if (maxValPos > maxValNeg) {
					rectCapstan = rectCapstanPos;
				} else {
					rectCapstan = rectCapstanNeg;
				}
			else {
				rectCapstan = rectCapstanPos;
			}
		else if (maxValNeg > 0) {
			rectCapstan = rectCapstanNeg;
		} else {
			return false;
		}
	}

	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
457
458
459
460
461
462
463
464
465
466
/**
 * @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.
 */
467
468
bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd) {

469
	/*********************************************************************************************/
470
	/********************************** Capstan analysis *****************************************/
471
	/*********************************************************************************************/
472
473

	// In the last minute of the video, check for pinchRoller position for endTape event
474
	if (!endTapeSaved && msToEnd < 60000) {
475
476
477

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
480
		// Extract matrices corresponding to the processing area
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
		// 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
496
497
		cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
		cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
498
		// crop the resulting image
499
500
		cv::getRectSubPix(rotatedPrevFrame, rect_size, rectCapstan.center, croppedPrevFrame);
		cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectCapstan.center, croppedCurrentFrame);
501
502
503

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

Matteo's avatar
update    
Matteo committed
504
		cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
505
506
507
508
509
510
511
512
513
514
515
516
517
518

		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;
519
			endTapeSaved = true; // Never check again for end tape instant
520
521
522
523
			return true;
		} else {
			savingPinchRoller = false;
		}
524
525
	} 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
526
	}
527
528

	/*********************************************************************************************/
529
	/************************************ Tape analysis ******************************************/
530
	/*********************************************************************************************/
531
532

	// Tape area
Matteo's avatar
update    
Matteo committed
533
    int tapeAreaPixels = rotatedRectArea(rectTape);
Matteo's avatar
update    
Matteo committed
534
	float tapeDifferentPixelsThreshold = tapeAreaPixels * tape.threshold.percentual / 100;
535

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
536
	// Extract matrices corresponding to the processing area
537
538
539
540
541
	// 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
542
543
	float angle = rectTape.angle;
	Size rect_size = rectTape.size;
544
	// thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/
545
	if (rectTape.angle < -45.) {
546
547
548
549
		angle += 90.0;
		swap(rect_size.width, rect_size.height);
	}
	// get the rotation matrix
550
	M = getRotationMatrix2D(rectTape.center, angle, 1.0);
551
	// perform the affine transformation
552
553
	cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC);
	cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC);
554
	// crop the resulting image
555
556
	cv::getRectSubPix(rotatedPrevFrame, rect_size, rectTape.center, croppedPrevFrame);
	cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectTape.center, croppedCurrentFrame);
557
558
559

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

Matteo's avatar
update    
Matteo committed
560
	cv::Mat differenceFrame = utility::difference(croppedPrevFrame, croppedCurrentFrame);
561
562
563
564
565
566
567

	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
568
	/************************************* Segment analysis **************************************/
569

570
571
572
573
574
575
576
577
578
579
580
581
  	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++;
			}
		}
	}
582
	mediaCurrFrame = totColoreCF/tapeAreaPixels;
583

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
584
585
	/************************************* Decision stage ****************************************/

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
586
	bool isIrregularity = false;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
587

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

590
591
		/***** 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
592
			isIrregularity = true;
593
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
594

595
		/***** BRANDS MANAGEMENT *****/
Matteo's avatar
update    
Matteo committed
596
		if (config.brands) {
597
			// At the beginning of the video, wait at least 5 seconds before the next Irregularity to consider it as a brand.
598
			// It is not guaranteed that it will be the first brand, but it is generally a safe approach to have a correct image
599
600
601
602
			if (firstBrand) {
				if (firstInstant - msToEnd > 5000) {
					firstBrand = false;
					savingBrand = true;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
603
					isIrregularity = true;
604
				}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
605
			// In the following iterations reset savingBrand, since we are no longer interested in brands.
606
			} else
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
607
				savingBrand = false;
608
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
609

610
	}
611
612
613
614

	// Update mediaPrevFrame
	mediaPrevFrame = mediaCurrFrame;

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
615
	return isIrregularity;
616
617
618
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
619
620
621
622
623
624
625
/**
 * @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) {
626

Matteo's avatar
update    
Matteo committed
627
628
629
630
631
	const int video_length_ms = ((float) videoCapture.get(CAP_PROP_FRAME_COUNT) / videoCapture.get(CAP_PROP_FPS)) * 1000;
	int video_current_ms = videoCapture.get(CAP_PROP_POS_MSEC);
	// counters
    int savedFrames = 0;
	int unsavedFrames = 0;
632
	float lastSaved = -160;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
633
	// Whenever we find an Irregularity, we want to skip a lenght equal to the Studer reading head (3 cm = 1.18 inches).
634
635
	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
636
	if (config.speed == 7.5)
637
638
639
640
641
		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;
Matteo's avatar
update    
Matteo committed
642
	firstInstant = video_length_ms - video_current_ms;
643
644
645
646

    while (videoCapture.isOpened()) {
		cv::Mat frame;
        videoCapture >> frame;
Matteo's avatar
update    
Matteo committed
647
		video_current_ms = videoCapture.get(CAP_PROP_POS_MSEC);
648

Matteo's avatar
update    
Matteo committed
649
		if (frame.empty()) {
650
			cout << endl << "Empty frame!" << endl;
651
652
	    	videoCapture.release();
	    	break;
Matteo's avatar
update    
Matteo committed
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
		}

		int msToEnd = video_length_ms - video_current_ms;
		if (video_current_ms == 0) // With OpenCV library, this happens at the last few frames of the video before realising that "frame" is empty.
			break;

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

		string secStrToEnd = to_string(secToEnd), minStrToEnd = to_string(minToEnd);
		if (minToEnd < 10)
			minStrToEnd = "0" + minStrToEnd;
		if (secToEnd < 10)
			secStrToEnd = "0" + secStrToEnd;

		// Display program status
		cout << "\rIrregularities: " << savedFrames << ".   ";
		cout << "Remaining video time [mm:ss]: " << minStrToEnd << ":" << secStrToEnd << flush;

		if ((video_current_ms - lastSaved > savingRate) && frameDifference(prevFrame, frame, msToEnd)) {
			// An Irregularity has been found!
			// De-interlacing frame
			cv::Mat oddFrame(frame.rows/2, frame.cols, CV_8UC3);
			cv::Mat evenFrame(frame.rows/2, frame.cols, CV_8UC3);
			utility::separateFrame(frame, oddFrame, evenFrame);

			// Extract the image corresponding to the ROIs
			Point2f pts[4];
			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)));

			// De-interlacing
			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!!!
				evenSubImageRows += 1;
			cv::Mat evenSubImage(evenSubImageRows, subImage.cols, CV_8UC3);
			utility::separateFrame(subImage, oddSubImage, evenSubImage);

			string timeLabel = getTimeLabel(video_current_ms, ":");
			string safeTimeLabel = getTimeLabel(video_current_ms, "-");

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

			// Append Irregularity information to JSON
			boost::uuids::uuid uuid = boost::uuids::random_generator()();
			irregularityFileOutput1["Irregularities"] += {
				{
					"IrregularityID", boost::lexical_cast<string>(uuid)
				}, {
					"Source", "v"
				}, {
					"TimeLabel", timeLabel
				}
			};
			irregularityFileOutput2["Irregularities"] += {
				{
					"IrregularityID", boost::lexical_cast<string>(uuid)
				}, {
					"Source", "v"
				}, {
					"TimeLabel", timeLabel
				}, {
					"ImageURI", irregularityImagesPath.string() + "/" + irregularityImageFilename
				}
			};
725

Matteo's avatar
update    
Matteo committed
726
727
			lastSaved = video_current_ms;
			savedFrames++;
728

Matteo's avatar
update    
Matteo committed
729
730
731
732
733
734
735
		} else {
			unsavedFrames++;
		}

		prevFrame = frame;

	}
736
737
738
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
/*************************************************************************************************/
/********************************************* 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.
 */
756
757
int main(int argc, char** argv) {

Matteo's avatar
update    
Matteo committed
758
759
	json irregularityFileInput;
	fs::path irregularityFileInputPath;
Matteo's avatar
update    
Matteo committed
760
761
	cv::Mat myFrame;

762
763
764
	/*********************************************************************************************/
	/*************************************** CONFIGURATION ***************************************/
	/*********************************************************************************************/
765

766
	// Get the input from config.json or command line
767
	try {
768
769
770
		bool continueExecution = getArguments(argc, argv);
		if (!continueExecution) {
			return 0;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
771
		}
772
	} catch (nlohmann::detail::type_error e) {
773
		cerr << RED << "config.json error!" << endl << e.what() << END << endl;
774
775
		return -1;
	}
776

Matteo's avatar
update    
Matteo committed
777
	const fs::path VIDEO_PATH = config.workingPath / "PreservationAudioVisualFile" / config.filesName;
Matteo's avatar
update    
Matteo committed
778
779
780

    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;
781
782
783
        return -1;
    }

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

786

787
	// Input JSON check
788
	ifstream iJSON(irregularityFileInputPath);
789
	if (iJSON.fail()) {
790
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
791
792
		return -1;
	}
Matteo's avatar
update    
Matteo committed
793
	if (config.speed != 7.5 && config.speed != 15) {
794
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
795
796
		return -1;
	}
Matteo's avatar
update    
Matteo committed
797
	if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
798
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
799
800
		return -1;
	}
Matteo's avatar
update    
Matteo committed
801
	if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
802
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl;
803
804
805
		return -1;
	}

806
	// Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
update    
Matteo committed
807
808
809
	if (config.brands) {
		if (config.speed == 15)
			tape.threshold.percentual += 6;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
810
	} else
Matteo's avatar
update    
Matteo committed
811
812
		if (config.speed == 15)
			tape.threshold.percentual += 20;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
813
		else
Matteo's avatar
update    
Matteo committed
814
			tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
815

816
817
    cout << endl;
	cout << "Parameters:" << endl;
Matteo's avatar
update    
Matteo committed
818
819
820
821
	cout << "    Brands: " << config.brands << endl;
	cout << "    Speed: " << config.speed << endl;
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
	cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
822
	cout << endl;
823
824
825
826

	// Read input JSON
	iJSON >> irregularityFileInput;

827
828
829
	/*********************************************************************************************/
	/*********************************** MAKE OUTPUT DIRECTORY ***********************************/
	/*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
830
831

	// Make directory with fileName name
Matteo's avatar
update    
Matteo committed
832
	outputPath = config.workingPath / "temp" / fileName;
833
	int outputFileNameDirectory = create_directory(outputPath);
Matteo's avatar
update    
Matteo committed
834

Matteo's avatar
update    
Matteo committed
835
836
	irregularityImagesPath = outputPath / "IrregularityImages";
	int fullFrameDirectory = fs::create_directory(irregularityImagesPath);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
837

838
839
840
	/*********************************************************************************************/
	/************************************** AREAS DETECTION **************************************/
	/*********************************************************************************************/
841

Matteo's avatar
update    
Matteo committed
842
	cv::VideoCapture videoCapture(VIDEO_PATH);
843
    if (!videoCapture.isOpened()) {
844
        cerr << RED << BOLD << "Video unreadable." << END << endl;
845
846
847
        return -1;
    }

Matteo's avatar
update    
Matteo committed
848
	int frames_number = videoCapture.get(CAP_PROP_FRAME_COUNT);
849
	// Set frame position to half video length
Matteo's avatar
update    
Matteo committed
850
	videoCapture.set(CAP_PROP_POS_FRAMES, frames_number/2);
851
	// Get frame
852
	videoCapture >> myFrame;
853

Matteo's avatar
update    
Matteo committed
854
	cout << "Video resolution: " << myFrame.cols << "x" << myFrame.rows << endl;
855

Matteo's avatar
update    
Matteo committed
856
	bool found = findProcessingAreas(myFrame);
857
858
859
860

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

Matteo's avatar
update    
Matteo committed
861
862
	if (!found) {
		cout << RED << "Processing area not found. Try changing JSON parameters." << END << endl;
863
		return -1; // Program terminated early
864
865
	}

866
	/*********************************************************************************************/
867
	/**************************************** PROCESSING *****************************************/
868
	/*********************************************************************************************/
869

Matteo's avatar
update    
Matteo committed
870
	cout << '\n' << CYAN << "Starting processing..." << END << '\n';
871
872
873
874
875

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

876
	processing(videoCapture);
877
878
879
880
881

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

882
883
	string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
	cout << endl << result << endl;
884

885
886
887
	/*********************************************************************************************/
	/************************************* IRREGULARITY FILES ************************************/
	/*********************************************************************************************/
888

889
	fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
Matteo's avatar
update    
Matteo committed
890
	files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
891
892

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

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

898
899
    return 0;
}