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

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

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

49
50
51
#include "utility.h"
#include "forAudioAnalyser.h"

Matteo's avatar
update    
Matteo committed
52
#include "lib/files.h"
Matteo's avatar
update    
Matteo committed
53
54
#include "lib/colors.h"
#include "lib/time.h"
Matteo's avatar
Matteo committed
55
56
#include "lib/Irregularity.h"
#include "lib/IrregularityFile.h"
Matteo's avatar
update    
Matteo committed
57

58
59
using namespace cv;
using namespace std;
Matteo's avatar
update    
Matteo committed
60
using utility::Frame;
61
using json = nlohmann::json;
62
63
namespace fs = std::filesystem;
namespace po = boost::program_options;
64

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
65
// For capstan detection, there are two alternative approaches:
66
67
68
// Generalized Hough Transform and SURF.
bool useSURF = true;

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

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

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

Matteo's avatar
update    
Matteo committed
112
// Constants Paths
Matteo's avatar
update    
Matteo committed
113
static const string READING_HEAD_IMG = "input/readingHead.png";
Matteo's avatar
update    
Matteo committed
114
115
static const string CAPSTAN_TEMPLATE_IMG = "input/capstanBERIO058prova.png";
static const string CONFIG_FILE = "config/config.json";
116

Matteo's avatar
update    
Matteo committed
117
118
119
120
double rotatedRectArea(RotatedRect rect) {
	return rect.size.width * rect.size.height;
}

Matteo's avatar
Matteo committed
121
/**
Matteo's avatar
Matteo committed
122
 * @fn void pprint(string text, string color)
Matteo's avatar
Matteo committed
123
124
125
126
127
128
129
130
131
 * @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
132
/**
Matteo's avatar
Matteo committed
133
 * @fn bool getArguments(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
134
135
136
137
138
139
140
 * @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
141
bool getArguments(int argc, char** argv) {
142
	// Read configuration file
Matteo's avatar
update    
Matteo committed
143
	ifstream iConfig(CONFIG_FILE);
144
145
146
147
	iConfig >> configurationFile;

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

		config = {
Matteo's avatar
update    
Matteo committed
151
			fs::path(working_path),
Matteo's avatar
update    
Matteo committed
152
153
154
155
156
			configurationFile["FilesName"],
			configurationFile["Brands"],
			configurationFile["Speed"]
		};

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

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

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

	tape = {
		configurationFile["MinDist"],
		{
			configurationFile["TapeThresholdPercentual"],
			configurationFile["AngleThresh"],
			configurationFile["ScaleThresh"],
			configurationFile["PosThresh"]
		}
	};
213
214
215
216
217

	return true;
}


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

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

Matteo's avatar
update    
Matteo committed
246
	alg->setAngleStep(1);
Matteo's avatar
Matteo committed
247
	alg->setAngleThresh(object.threshold.angle);
248

Matteo's avatar
update    
Matteo committed
249
250
251
	alg->setMinScale(0.9);
	alg->setMaxScale(1.1);
	alg->setScaleStep(0.01);
Matteo's avatar
Matteo committed
252
	alg->setScaleThresh(object.threshold.scale);
253

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

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

Matteo's avatar
Matteo committed
259
	alg->setTemplate(model);
260

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

	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
277
278
279
280
	return { indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg };
}

/**
Matteo's avatar
Matteo committed
281
 * @fn bool findProcessingAreas(Mat myFrame)
Matteo's avatar
Matteo committed
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
316
317
318
319
 * @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);

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

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

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

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

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

	} else {

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

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

		RotatedRect rectCapstanPos, rectCapstanNeg;
		if (positionsC1Pos.size() > 0)
Matteo's avatar
update    
Matteo committed
428
			rectCapstanPos = utility::drawShapes(myFrame, positionsC1Pos[indexPos], Scalar(255-indexPos*64, 0, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
429
		if (positionsC1Neg.size() > 0)
Matteo's avatar
update    
Matteo committed
430
			rectCapstanNeg = utility::drawShapes(myFrame, positionsC1Neg[indexNeg], Scalar(255-indexNeg*64, 128, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
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;
}

Matteo's avatar
Matteo committed
457
458
459
460
461
462
463
/**
 * @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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
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
479
 * @fn Frame get_difference_for_roi(Frame previous, Frame current, RotatedRect roi)
Matteo's avatar
Matteo committed
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
 * @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));
}
497

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

512
513
514
	/********************************** Capstan analysis *****************************************/

	// In the last minute of the video, check for pinchRoller position for endTape event
515
	if (!endTapeSaved && msToEnd < 60000) {
516
517
518

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

Matteo's avatar
Matteo committed
521
522
		RotatedRect corrected_capstan_roi = check_skew(rectCapstan);
		Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_capstan_roi);
523
524
525

		int blackPixelsCapstan = 0;

Matteo's avatar
Matteo committed
526
527
528
		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) {
529
530
531
532
533
534
535
536
					// There is a black pixel, then there is a difference between previous and current frames
					blackPixelsCapstan++;
				}
			}
		}

		if (blackPixelsCapstan > capstanDifferentPixelsThreshold) {
			savingPinchRoller = true;
537
			endTapeSaved = true; // Never check again for end tape instant
538
			return true;
Matteo's avatar
Matteo committed
539
		} 
540
	}
541

Matteo's avatar
Matteo committed
542
543
	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

544
545
546
	/************************************ Tape analysis ******************************************/

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

Matteo's avatar
Matteo committed
550
	RotatedRect corrected_tape_roi = check_skew(rectTape);
551

Matteo's avatar
Matteo committed
552
553
554
	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);
555

Matteo's avatar
Matteo committed
556
	Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_tape_roi);
557
558
559
560
561
562

	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
563
	/************************************* Segment analysis **************************************/
564

565
566
567
568
569
570
571
  	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
572
			if (difference_frame.at<cv::Vec3b>(i, j)[0] == 0) {
573
574
575
576
				blackPixels++;
			}
		}
	}
577
	mediaCurrFrame = totColoreCF/tapeAreaPixels;
578

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
579
580
	/************************************* Decision stage ****************************************/

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

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

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

603
	}
604
605
606
607

	// Update mediaPrevFrame
	mediaPrevFrame = mediaCurrFrame;

Matteo's avatar
Matteo committed
608
	return result;
609
610
611
}


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

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

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

Matteo's avatar
update    
Matteo committed
643
		if (frame.empty()) {
644
			cout << endl << "Empty frame!" << endl;
645
	    	videoCapture.release();
Matteo's avatar
Matteo committed
646
	    	return;
Matteo's avatar
update    
Matteo committed
647
648
649
650
		}

		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.
Matteo's avatar
Matteo committed
651
			return;
Matteo's avatar
update    
Matteo committed
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

		// 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
Matteo's avatar
Matteo committed
698
699
700
			Irregularity irreg = Irregularity(Source::Video, timeLabel);
			irregularityFileOutput1["Irregularities"] += irreg.to_JSON();
			irregularityFileOutput2["Irregularities"] += irreg.set_image_URI(irregularityImagesPath.string() + "/" + irregularityImageFilename).to_JSON();
701

Matteo's avatar
update    
Matteo committed
702
703
			lastSaved = video_current_ms;
			savedFrames++;
704

Matteo's avatar
update    
Matteo committed
705
706
707
708
709
		} else {
			unsavedFrames++;
		}
		prevFrame = frame;
	}
710
711
712
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
713
/**
Matteo's avatar
Matteo committed
714
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
715
716
717
718
719
720
721
722
723
724
725
726
 * @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.
 */
727
728
int main(int argc, char** argv) {

Matteo's avatar
update    
Matteo committed
729
730
	json irregularityFileInput;
	fs::path irregularityFileInputPath;
Matteo's avatar
update    
Matteo committed
731
732
	cv::Mat myFrame;

733
734
735
	/*********************************************************************************************/
	/*************************************** CONFIGURATION ***************************************/
	/*********************************************************************************************/
736

737
	// Get the input from config.json or command line
738
	try {
739
740
741
		bool continueExecution = getArguments(argc, argv);
		if (!continueExecution) {
			return 0;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
742
		}
743
	} catch (nlohmann::detail::type_error e) {
744
		cerr << RED << "config.json error!" << endl << e.what() << END << endl;
745
746
		return -1;
	}
747

Matteo's avatar
update    
Matteo committed
748
	const fs::path VIDEO_PATH = config.workingPath / "PreservationAudioVisualFile" / config.filesName;
Matteo's avatar
update    
Matteo committed
749
750
751

    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;
752
753
754
        return -1;
    }

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

757

758
	// Input JSON check
759
	ifstream iJSON(irregularityFileInputPath);
760
	if (iJSON.fail()) {
761
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
762
763
		return -1;
	}
Matteo's avatar
update    
Matteo committed
764
	if (config.speed != 7.5 && config.speed != 15) {
765
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
766
767
		return -1;
	}
Matteo's avatar
update    
Matteo committed
768
	if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
769
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
770
771
		return -1;
	}
Matteo's avatar
update    
Matteo committed
772
	if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
773
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl;
774
775
776
		return -1;
	}

777
	// Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
update    
Matteo committed
778
779
780
	if (config.brands) {
		if (config.speed == 15)
			tape.threshold.percentual += 6;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
781
	} else
Matteo's avatar
update    
Matteo committed
782
783
		if (config.speed == 15)
			tape.threshold.percentual += 20;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
784
		else
Matteo's avatar
update    
Matteo committed
785
			tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
786

787
788
    cout << endl;
	cout << "Parameters:" << endl;
Matteo's avatar
update    
Matteo committed
789
790
791
792
	cout << "    Brands: " << config.brands << endl;
	cout << "    Speed: " << config.speed << endl;
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
	cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
793
	cout << endl;
794
795
796
797

	// Read input JSON
	iJSON >> irregularityFileInput;

798
799
800
	/*********************************************************************************************/
	/*********************************** MAKE OUTPUT DIRECTORY ***********************************/
	/*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
801
802

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

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

809
810
811
	/*********************************************************************************************/
	/************************************** AREAS DETECTION **************************************/
	/*********************************************************************************************/
812

Matteo's avatar
update    
Matteo committed
813
	cv::VideoCapture videoCapture(VIDEO_PATH);
814
    if (!videoCapture.isOpened()) {
815
        cerr << RED << BOLD << "Video unreadable." << END << endl;
816
817
818
        return -1;
    }

Matteo's avatar
update    
Matteo committed
819
	int frames_number = videoCapture.get(CAP_PROP_FRAME_COUNT);
820
	// Set frame position to half video length
Matteo's avatar
update    
Matteo committed
821
	videoCapture.set(CAP_PROP_POS_FRAMES, frames_number/2);
822
	// Get frame
823
	videoCapture >> myFrame;
824

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

Matteo's avatar
update    
Matteo committed
827
	bool found = findProcessingAreas(myFrame);
828
829
830
831

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

Matteo's avatar
update    
Matteo committed
832
	if (!found) {
Matteo's avatar
Matteo committed
833
		pprint("Processing area not found. Try changing JSON parameters.", RED);
834
		return -1; // Program terminated early
835
836
	}

837
	/*********************************************************************************************/
838
	/**************************************** PROCESSING *****************************************/
839
	/*********************************************************************************************/
840

Matteo's avatar
Matteo committed
841
	pprint("\nProcessing...", CYAN);
842
843
844
845
846

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

847
	processing(videoCapture);
848
849
850
851
852

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

853
854
	string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
	cout << endl << result << endl;
855

856
857
858
	/*********************************************************************************************/
	/************************************* IRREGULARITY FILES ************************************/
	/*********************************************************************************************/
859

860
	fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
Matteo's avatar
update    
Matteo committed
861
	files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
862
863

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

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

869
870
    return 0;
}