main.cpp 31.9 KB
Newer Older
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
1
/**
Matteo's avatar
Matteo committed
2
 * @file main.cpp
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
3
4
5
6
7
8
9
10
11
12
 *  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
13
 *  @authors Nadir Dalla Pozza <nadir.dallapozza@unipd.it>, Matteo Spanio <dev2@audioinnova.com>
Matteo's avatar
Matteo committed
14
 *	@copyright 2023, Audio Innova S.r.l.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
15
16
 *	@credits Niccolò Pretto, Nadir Dalla Pozza, Sergio Canazza
 *	@license GPL v3.0
Matteo's avatar
Matteo committed
17
 *	@version 1.1.1
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
18
19
 *	@status Production
 */
20
#include <filesystem>
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
21
#include <fstream>
22
#include <iostream>
23
#include <stdlib.h>
24
#include <sys/timeb.h>
Matteo's avatar
update    
Matteo committed
25
#include <ranges>
26

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

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

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

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

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

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

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

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

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

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

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

Matteo's avatar
Matteo committed
117
/**
Matteo's avatar
Matteo committed
118
 * @fn void pprint(string text, string color)
Matteo's avatar
Matteo committed
119
120
121
122
123
124
125
126
127
 * @brief Prints a text in a given color.
 * 
 * @param text
 * @param color 
 */
void pprint(string text, string color) {
	cout << color << text << END << endl;
}

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
128
/**
Matteo's avatar
Matteo committed
129
 * @fn bool getArguments(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
130
131
132
133
134
135
136
 * @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
137
bool getArguments(int argc, char** argv) {
138
	// Read configuration file
Matteo's avatar
update    
Matteo committed
139
	ifstream iConfig(CONFIG_FILE);
140
141
142
143
	iConfig >> configurationFile;

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

		config = {
Matteo's avatar
update    
Matteo committed
147
			fs::path(working_path),
Matteo's avatar
update    
Matteo committed
148
149
150
151
152
			configurationFile["FilesName"],
			configurationFile["Brands"],
			configurationFile["Speed"]
		};

153
154
155
156
157
	} 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
158
				"By default, the configuartion parameters are loaded from config/config.json file,\n"
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
				"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
174
175
176
177
178
179
180

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

181
182
183
184
185
186
187
188
189
		} 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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
	capstan = {
		configurationFile["MinDistCapstan"],
		{
			configurationFile["CapstanThresholdPercentual"],
			configurationFile["AngleThreshCapstan"],
			configurationFile["ScaleThreshCapstan"],
			configurationFile["PosThreshCapstan"]
		}
	};

	tape = {
		configurationFile["MinDist"],
		{
			configurationFile["TapeThresholdPercentual"],
			configurationFile["AngleThresh"],
			configurationFile["ScaleThresh"],
			configurationFile["PosThresh"]
		}
	};
209
210
211
212
213

	return true;
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
214
/**
Matteo's avatar
Matteo committed
215
 * @fn std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> findObject(Mat model, SceneObject object)
Matteo's avatar
Matteo committed
216
217
218
219
220
221
222
223
 * @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
224
 */
Matteo's avatar
Matteo committed
225
std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> findObject(Mat model, SceneObject object, Mat processing_area) {
226
227

	// Algorithm and parameters
Matteo's avatar
Matteo committed
228
	// 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
229
230
231
232
233
234
235
236
	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
237
	alg->setMinDist(object.minDist);
Matteo's avatar
update    
Matteo committed
238
239
240
	alg->setLevels(360);
	alg->setDp(2);
	alg->setMaxBufferSize(1000);
241

Matteo's avatar
update    
Matteo committed
242
	alg->setAngleStep(1);
Matteo's avatar
Matteo committed
243
	alg->setAngleThresh(object.threshold.angle);
244

Matteo's avatar
update    
Matteo committed
245
246
247
	alg->setMinScale(0.9);
	alg->setMaxScale(1.1);
	alg->setScaleStep(0.01);
Matteo's avatar
Matteo committed
248
	alg->setScaleThresh(object.threshold.scale);
249

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

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

Matteo's avatar
Matteo committed
255
	alg->setTemplate(model);
256

Matteo's avatar
Matteo committed
257
	utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg, processing_area);
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272

	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
273
274
275
276
	return { indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg };
}

/**
Matteo's avatar
Matteo committed
277
 * @fn bool findProcessingAreas(Mat myFrame)
Matteo's avatar
Matteo committed
278
279
280
281
282
283
284
285
286
287
288
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
 * @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);

316
317
	// 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
318
		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);
319
	if (positionsNeg.size() > 0)
Matteo's avatar
update    
Matteo committed
320
		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);
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343

	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
344
	rectTape = utility::drawShapes(myFrame, positionTape, Scalar(0, 255-indexPos*64, 0), rect.size.width, 50 * (rect.size.width / 200), 0, 0, 1);
345
346
347
348
349
350

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

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

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

	} else {

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

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

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

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

Matteo's avatar
Matteo committed
453
454
455
456
457
458
459
/**
 * @fn RotatedRect check_skew(RotatedRect roi)
 * @brief Check if the region of interest is skewed and correct it
 * 
 * @param roi the region of interest
 * @return RotatedRect the corrected region of interest
 */
Matteo's avatar
Matteo committed
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
RotatedRect check_skew(RotatedRect roi) {
	// get angle and size from the bounding box
	// thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/
	cv::Size rect_size = roi.size;
	float angle = roi.angle;

	if (roi.angle < -45.) {
		angle += 90.0;
		swap(rect_size.width, rect_size.height);
	}

	return RotatedRect(roi.center, rect_size, angle);
}

/**
Matteo's avatar
Matteo committed
475
 * @fn Frame get_difference_for_roi(Frame previous, Frame current, RotatedRect roi)
Matteo's avatar
Matteo committed
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
 * @brief Look for differences in two consecutive frames in a specific region of interest
 * 
 * @param previous the reference frame
 * @param current the frame to compare with the reference
 * @param roi the region of interest
 * @return Frame the difference matrix between the two frames
 */
Frame get_difference_for_roi(Frame previous, Frame current, RotatedRect roi) {
	cv::Mat rotation_matrix = getRotationMatrix2D(roi.center, roi.angle, 1.0);

	return previous
		.warp(rotation_matrix)
		.crop(roi.size, roi.center)
		.difference(current
			.warp(rotation_matrix)
			.crop(roi.size, roi.center));
}
493

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
494
/**
Matteo's avatar
Matteo committed
495
 * @fn bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
496
497
498
499
500
501
502
503
504
 * @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.
 */
505
bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd) {
Matteo's avatar
Matteo committed
506
	bool result = false;
507

508
509
510
	/********************************** Capstan analysis *****************************************/

	// In the last minute of the video, check for pinchRoller position for endTape event
511
	if (!endTapeSaved && msToEnd < 60000) {
512
513
514

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

Matteo's avatar
Matteo committed
517
518
		RotatedRect corrected_capstan_roi = check_skew(rectCapstan);
		Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_capstan_roi);
519
520
521

		int blackPixelsCapstan = 0;

Matteo's avatar
Matteo committed
522
523
524
		for (int i = 0; i < difference_frame.rows; i++) {
			for (int j = 0; j < difference_frame.cols; j++) {
				if (difference_frame.at<cv::Vec3b>(i, j)[0] == 0) {
525
526
527
528
529
530
531
532
					// There is a black pixel, then there is a difference between previous and current frames
					blackPixelsCapstan++;
				}
			}
		}

		if (blackPixelsCapstan > capstanDifferentPixelsThreshold) {
			savingPinchRoller = true;
533
			endTapeSaved = true; // Never check again for end tape instant
534
			return true;
Matteo's avatar
Matteo committed
535
		} 
536
	}
537

Matteo's avatar
Matteo committed
538
539
	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

540
541
542
	/************************************ Tape analysis ******************************************/

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

Matteo's avatar
Matteo committed
546
	RotatedRect corrected_tape_roi = check_skew(rectTape);
547

Matteo's avatar
Matteo committed
548
549
550
	Frame croppedCurrentFrame = Frame(currentFrame)
		.warp(getRotationMatrix2D(corrected_tape_roi.center, corrected_tape_roi.angle, 1.0))
		.crop(corrected_tape_roi.size, corrected_tape_roi.center);
551

Matteo's avatar
Matteo committed
552
	Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_tape_roi);
553
554
555
556
557
558

	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
559
	/************************************* Segment analysis **************************************/
560

561
562
563
564
565
566
567
  	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];
Matteo's avatar
Matteo committed
568
			if (difference_frame.at<cv::Vec3b>(i, j)[0] == 0) {
569
570
571
572
				blackPixels++;
			}
		}
	}
573
	mediaCurrFrame = totColoreCF/tapeAreaPixels;
574

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
575
576
	/************************************* Decision stage ****************************************/

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

579
580
		/***** AVERAGE_COLOR-BASED DECISION *****/
		if (mediaPrevFrame > (mediaCurrFrame + 7) || mediaPrevFrame < (mediaCurrFrame - 7)) { // They are not similar for color average
Matteo's avatar
Matteo committed
581
			result = true;
582
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
583

584
		/***** BRANDS MANAGEMENT *****/
Matteo's avatar
update    
Matteo committed
585
		if (config.brands) {
586
			// At the beginning of the video, wait at least 5 seconds before the next Irregularity to consider it as a brand.
587
			// It is not guaranteed that it will be the first brand, but it is generally a safe approach to have a correct image
588
589
590
591
			if (firstBrand) {
				if (firstInstant - msToEnd > 5000) {
					firstBrand = false;
					savingBrand = true;
Matteo's avatar
Matteo committed
592
					result = true;
593
				}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
594
			// In the following iterations reset savingBrand, since we are no longer interested in brands.
595
			} else
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
596
				savingBrand = false;
597
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
598

599
	}
600
601
602
603

	// Update mediaPrevFrame
	mediaPrevFrame = mediaCurrFrame;

Matteo's avatar
Matteo committed
604
	return result;
605
606
607
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
608
/**
Matteo's avatar
Matteo committed
609
 * @fn void processing(cv::VideoCapture videoCapture)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
610
611
612
613
614
615
 * @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) {
616

Matteo's avatar
update    
Matteo committed
617
618
619
620
621
	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;
622
	float lastSaved = -160;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
623
	// Whenever we find an Irregularity, we want to skip a lenght equal to the Studer reading head (3 cm = 1.18 inches).
624
625
	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
626
	if (config.speed == 7.5)
627
628
629
630
631
		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
632
	firstInstant = video_length_ms - video_current_ms;
633
634
635
636

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

Matteo's avatar
update    
Matteo committed
639
		if (frame.empty()) {
640
			cout << endl << "Empty frame!" << endl;
641
642
	    	videoCapture.release();
	    	break;
Matteo's avatar
update    
Matteo committed
643
644
645
646
647
648
649
650
651
652
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
		}

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

Matteo's avatar
update    
Matteo committed
716
717
			lastSaved = video_current_ms;
			savedFrames++;
718

Matteo's avatar
update    
Matteo committed
719
720
721
722
723
		} else {
			unsavedFrames++;
		}
		prevFrame = frame;
	}
724
725
726
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
727
/**
Matteo's avatar
Matteo committed
728
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
729
730
731
732
733
734
735
736
737
738
739
740
 * @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.
 */
741
742
int main(int argc, char** argv) {

Matteo's avatar
update    
Matteo committed
743
744
	json irregularityFileInput;
	fs::path irregularityFileInputPath;
Matteo's avatar
update    
Matteo committed
745
746
	cv::Mat myFrame;

747
748
749
	/*********************************************************************************************/
	/*************************************** CONFIGURATION ***************************************/
	/*********************************************************************************************/
750

751
	// Get the input from config.json or command line
752
	try {
753
754
755
		bool continueExecution = getArguments(argc, argv);
		if (!continueExecution) {
			return 0;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
756
		}
757
	} catch (nlohmann::detail::type_error e) {
758
		cerr << RED << "config.json error!" << endl << e.what() << END << endl;
759
760
		return -1;
	}
761

Matteo's avatar
update    
Matteo committed
762
	const fs::path VIDEO_PATH = config.workingPath / "PreservationAudioVisualFile" / config.filesName;
Matteo's avatar
update    
Matteo committed
763
764
765

    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;
766
767
768
        return -1;
    }

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

771

772
	// Input JSON check
773
	ifstream iJSON(irregularityFileInputPath);
774
	if (iJSON.fail()) {
775
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
776
777
		return -1;
	}
Matteo's avatar
update    
Matteo committed
778
	if (config.speed != 7.5 && config.speed != 15) {
779
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
780
781
		return -1;
	}
Matteo's avatar
update    
Matteo committed
782
	if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
783
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
784
785
		return -1;
	}
Matteo's avatar
update    
Matteo committed
786
	if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
787
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl;
788
789
790
		return -1;
	}

791
	// Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
update    
Matteo committed
792
793
794
	if (config.brands) {
		if (config.speed == 15)
			tape.threshold.percentual += 6;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
795
	} else
Matteo's avatar
update    
Matteo committed
796
797
		if (config.speed == 15)
			tape.threshold.percentual += 20;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
798
		else
Matteo's avatar
update    
Matteo committed
799
			tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
800

801
802
    cout << endl;
	cout << "Parameters:" << endl;
Matteo's avatar
update    
Matteo committed
803
804
805
806
	cout << "    Brands: " << config.brands << endl;
	cout << "    Speed: " << config.speed << endl;
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
	cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
807
	cout << endl;
808
809
810
811

	// Read input JSON
	iJSON >> irregularityFileInput;

812
813
814
	/*********************************************************************************************/
	/*********************************** MAKE OUTPUT DIRECTORY ***********************************/
	/*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
815
816

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

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

823
824
825
	/*********************************************************************************************/
	/************************************** AREAS DETECTION **************************************/
	/*********************************************************************************************/
826

Matteo's avatar
update    
Matteo committed
827
	cv::VideoCapture videoCapture(VIDEO_PATH);
828
    if (!videoCapture.isOpened()) {
829
        cerr << RED << BOLD << "Video unreadable." << END << endl;
830
831
832
        return -1;
    }

Matteo's avatar
update    
Matteo committed
833
	int frames_number = videoCapture.get(CAP_PROP_FRAME_COUNT);
834
	// Set frame position to half video length
Matteo's avatar
update    
Matteo committed
835
	videoCapture.set(CAP_PROP_POS_FRAMES, frames_number/2);
836
	// Get frame
837
	videoCapture >> myFrame;
838

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

Matteo's avatar
update    
Matteo committed
841
	bool found = findProcessingAreas(myFrame);
842
843
844
845

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

Matteo's avatar
update    
Matteo committed
846
	if (!found) {
Matteo's avatar
Matteo committed
847
		pprint("Processing area not found. Try changing JSON parameters.", RED);
848
		return -1; // Program terminated early
849
850
	}

851
	/*********************************************************************************************/
852
	/**************************************** PROCESSING *****************************************/
853
	/*********************************************************************************************/
854

Matteo's avatar
Matteo committed
855
	pprint("\nProcessing...", CYAN);
856
857
858
859
860

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

861
	processing(videoCapture);
862
863
864
865
866

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

867
868
	string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
	cout << endl << result << endl;
869

870
871
872
	/*********************************************************************************************/
	/************************************* IRREGULARITY FILES ************************************/
	/*********************************************************************************************/
873

874
	fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
Matteo's avatar
update    
Matteo committed
875
	files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
876
877

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

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

883
884
    return 0;
}