main.cpp 30.4 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
Matteo committed
88
/**
Matteo's avatar
Matteo committed
89
 * @fn void pprint(string text, string color)
Matteo's avatar
Matteo committed
90
91
92
93
94
95
96
97
98
 * @brief Prints a text in a given color.
 * 
 * @param text
 * @param color 
 */
void pprint(string text, string color) {
	cout << color << text << END << endl;
}

99
100
101
102
103
struct Args {
	fs::path workingPath;
	string filesName;
	bool brands;
	float speed;
Matteo's avatar
update    
Matteo committed
104

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
	Args(fs::path workingPath, string filesName, bool brands, float speed) {
		this->workingPath = workingPath;
		this->filesName = filesName;
		this->brands = brands;
		this->speed = speed;
	}
	~Args() {}

	static Args from_file(fs::path path) {
		ifstream iConfig(path);
		json j;
		iConfig >> j;
		return Args(
			fs::path(string(j["WorkingPath"])),
			j["FilesName"],
			j["Brands"],
			j["Speed"]
		);
	}

	static Args from_cli(int argc, char** argv) {
		po::variables_map vm;
127
128
129
		try {
			po::options_description desc(
				"A tool that implements MPAI CAE-ARP Video Analyser Technical Specification.\n"
Matteo's avatar
update    
Matteo committed
130
				"By default, the configuartion parameters are loaded from config/config.json file,\n"
131
132
133
134
135
136
137
138
139
140
141
				"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::store(po::command_line_parser(argc, argv).options(desc).run(), vm);
			if (vm.count("help")) {
				cout << desc << "\n";
142
				std::exit(EXIT_SUCCESS);
143
144
145
			}
			po::notify(vm);
		} catch (po::invalid_command_line_syntax& e) {
146
147
			pprint("The command line syntax is invalid: " + string(e.what()), RED + BOLD);
			std::exit(EXIT_FAILURE);
148
149
		} catch (po::required_option& e) {
			cerr << "Error: " << e.what() << endl;
150
151
152
153
			std::exit(EXIT_FAILURE);
		} catch (nlohmann::detail::type_error e) {
			pprint("config.json error! " + string(e.what()), RED);
			std::exit(EXIT_FAILURE);
154
		}
155
156
157
158
159
160
161

		return Args(
			fs::path(vm["working-path"].as<string>()),
			vm["files-name"].as<string>(),
			vm["brands"].as<bool>(),
			vm["speed"].as<float>()
		);
162
	}
163
};
164

165
166
167
168
// Constants Paths
static const string READING_HEAD_IMG = "input/readingHead.png";
static const string CAPSTAN_TEMPLATE_IMG = "input/capstanBERIO058prova.png";
static const string CONFIG_FILE = "config/config.json";
169

170
171
double rotatedRectArea(RotatedRect rect) {
	return rect.size.width * rect.size.height;
172
173
}

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
174
/**
Matteo's avatar
Matteo committed
175
 * @fn std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> findObject(Mat model, SceneObject object)
Matteo's avatar
Matteo committed
176
177
178
179
180
181
182
183
 * @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
184
 */
Matteo's avatar
Matteo committed
185
std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> findObject(Mat model, SceneObject object, Mat processing_area) {
186
187

	// Algorithm and parameters
Matteo's avatar
Matteo committed
188
	// 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
189
190
191
192
193
194
195
196
	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
197
	alg->setMinDist(object.minDist);
Matteo's avatar
update    
Matteo committed
198
199
200
	alg->setLevels(360);
	alg->setDp(2);
	alg->setMaxBufferSize(1000);
201

Matteo's avatar
update    
Matteo committed
202
	alg->setAngleStep(1);
Matteo's avatar
Matteo committed
203
	alg->setAngleThresh(object.threshold.angle);
204

Matteo's avatar
update    
Matteo committed
205
206
207
	alg->setMinScale(0.9);
	alg->setMaxScale(1.1);
	alg->setScaleStep(0.01);
Matteo's avatar
Matteo committed
208
	alg->setScaleThresh(object.threshold.scale);
209

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

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

Matteo's avatar
Matteo committed
215
	alg->setTemplate(model);
216

Matteo's avatar
Matteo committed
217
	utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg, processing_area);
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

	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
233
234
235
236
	return { indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg };
}

/**
Matteo's avatar
Matteo committed
237
 * @fn bool findProcessingAreas(Mat myFrame)
Matteo's avatar
Matteo committed
238
239
240
241
242
243
244
245
246
 * @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.
 */
247
bool findProcessingAreas(Mat myFrame, SceneObject tape, SceneObject capstan) {
Matteo's avatar
Matteo committed
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275

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

276
277
	// 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
278
		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);
279
	if (positionsNeg.size() > 0)
Matteo's avatar
update    
Matteo committed
280
		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);
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303

	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
304
	rectTape = utility::drawShapes(myFrame, positionTape, Scalar(0, 255-indexPos*64, 0), rect.size.width, 50 * (rect.size.width / 200), 0, 0, 1);
305
306
307
308
309
310

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

	// Read template image - it is smaller than before, therefore there is no need to downsample
Matteo's avatar
update    
Matteo committed
311
	Mat templateShape = imread(CAPSTAN_TEMPLATE_IMG, IMREAD_GRAYSCALE);
312
313
314
315
316
317
318
319
320
321

	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
322
		detector->detectAndCompute(gray_current_frame, noArray(), keypoints_scene, descriptors_scene);
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338

		// 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
339
		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);
340
341
342
		// Localize the object
		vector<Point2f> obj;
		vector<Point2f> scene;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
343
		for (size_t i = 0; i < good_matches.size(); i++) {
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
			// 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
365
		Vec4f positionCapstan(capstanX + 10, capstanY + 45, 1, 0);
Matteo's avatar
update    
Matteo committed
366
		rectCapstan = utility::drawShapes(myFrame, positionCapstan, Scalar(255-indexPos*64, 0, 0), templateShape.cols - 20, templateShape.rows - 90, 0, 0, 1);
367
368
369

	} else {

Matteo's avatar
Matteo committed
370
		// Process only right portion of the image, where the capstain always appears
371
372
373
374
375
		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
376
		Mat capstanProcessingAreaGrayscale = gray_current_frame(capstanProcessingAreaRect);
377
378
		// Reset algorithm and set parameters

Matteo's avatar
Matteo committed
379
		auto [indexPos, indexNeg, maxValPos, maxValNeg, positionsC1Pos, positionsC1Neg] = findObject(templateShape, capstan, capstanProcessingAreaGrayscale);
380
381
382

		RotatedRect rectCapstanPos, rectCapstanNeg;
		if (positionsC1Pos.size() > 0)
Matteo's avatar
update    
Matteo committed
383
			rectCapstanPos = utility::drawShapes(myFrame, positionsC1Pos[indexPos], Scalar(255-indexPos*64, 0, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
384
		if (positionsC1Neg.size() > 0)
Matteo's avatar
update    
Matteo committed
385
			rectCapstanNeg = utility::drawShapes(myFrame, positionsC1Neg[indexNeg], Scalar(255-indexNeg*64, 128, 0), templateShape.cols-22, templateShape.rows-92, capstanProcessingAreaRectX+11, capstanProcessingAreaRectY+46, 1);
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411

		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
412
413
414
415
416
417
418
/**
 * @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
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
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
434
 * @fn Frame get_difference_for_roi(Frame previous, Frame current, RotatedRect roi)
Matteo's avatar
Matteo committed
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
 * @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));
}
452

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
453
/**
Matteo's avatar
Matteo committed
454
 * @fn bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
455
456
457
458
459
460
461
462
463
 * @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.
 */
464
bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd, SceneObject capstan, SceneObject tape, Args args) {
Matteo's avatar
Matteo committed
465
	bool result = false;
466

467
468
469
	/********************************** Capstan analysis *****************************************/

	// In the last minute of the video, check for pinchRoller position for endTape event
470
	if (!endTapeSaved && msToEnd < 60000) {
471
472
473

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

Matteo's avatar
Matteo committed
476
477
		RotatedRect corrected_capstan_roi = check_skew(rectCapstan);
		Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_capstan_roi);
478
479
480

		int blackPixelsCapstan = 0;

Matteo's avatar
Matteo committed
481
482
483
		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) {
484
485
486
487
488
489
490
491
					// There is a black pixel, then there is a difference between previous and current frames
					blackPixelsCapstan++;
				}
			}
		}

		if (blackPixelsCapstan > capstanDifferentPixelsThreshold) {
			savingPinchRoller = true;
492
			endTapeSaved = true; // Never check again for end tape instant
493
			return true;
Matteo's avatar
Matteo committed
494
		} 
495
	}
496

Matteo's avatar
Matteo committed
497
498
	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

499
500
501
	/************************************ Tape analysis ******************************************/

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

Matteo's avatar
Matteo committed
505
	RotatedRect corrected_tape_roi = check_skew(rectTape);
506

Matteo's avatar
Matteo committed
507
508
509
	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);
510

Matteo's avatar
Matteo committed
511
	Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_tape_roi);
512
513
514
515
516
517

	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
518
	/************************************* Segment analysis **************************************/
519

520
521
522
523
524
525
526
  	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
527
			if (difference_frame.at<cv::Vec3b>(i, j)[0] == 0) {
528
529
530
531
				blackPixels++;
			}
		}
	}
532
	mediaCurrFrame = totColoreCF/tapeAreaPixels;
533

Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
534
535
	/************************************* Decision stage ****************************************/

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

538
539
		/***** AVERAGE_COLOR-BASED DECISION *****/
		if (mediaPrevFrame > (mediaCurrFrame + 7) || mediaPrevFrame < (mediaCurrFrame - 7)) { // They are not similar for color average
Matteo's avatar
Matteo committed
540
			result = true;
541
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
542

543
		/***** BRANDS MANAGEMENT *****/
544
		if (args.brands) {
545
			// At the beginning of the video, wait at least 5 seconds before the next Irregularity to consider it as a brand.
546
			// It is not guaranteed that it will be the first brand, but it is generally a safe approach to have a correct image
547
548
549
550
			if (firstBrand) {
				if (firstInstant - msToEnd > 5000) {
					firstBrand = false;
					savingBrand = true;
Matteo's avatar
Matteo committed
551
					result = true;
552
				}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
553
			// In the following iterations reset savingBrand, since we are no longer interested in brands.
554
			} else
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
555
				savingBrand = false;
556
		}
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
557

558
	}
559
560
561
562

	// Update mediaPrevFrame
	mediaPrevFrame = mediaCurrFrame;

Matteo's avatar
Matteo committed
563
	return result;
564
565
566
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
567
/**
Matteo's avatar
Matteo committed
568
 * @fn void processing(cv::VideoCapture videoCapture)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
569
570
571
572
573
 * @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;
 */
574
void processing(cv::VideoCapture videoCapture, SceneObject capstan, SceneObject tape, Args args) {
575

Matteo's avatar
update    
Matteo committed
576
577
578
579
580
	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;
581
	float lastSaved = -160;
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
582
	// Whenever we find an Irregularity, we want to skip a lenght equal to the Studer reading head (3 cm = 1.18 inches).
583
584
	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
585
	if (args.speed == 7.5)
586
587
588
589
590
		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
591
	firstInstant = video_length_ms - video_current_ms;
592
593

    while (videoCapture.isOpened()) {
594
		Frame frame;
595
        videoCapture >> frame;
Matteo's avatar
update    
Matteo committed
596
		video_current_ms = videoCapture.get(CAP_PROP_POS_MSEC);
597

Matteo's avatar
update    
Matteo committed
598
		if (frame.empty()) {
599
			cout << endl << "Empty frame!" << endl;
600
	    	videoCapture.release();
Matteo's avatar
Matteo committed
601
	    	return;
Matteo's avatar
update    
Matteo committed
602
603
604
605
		}

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

608
		// Display program status
Matteo's avatar
update    
Matteo committed
609
610
611
		int secToEnd = msToEnd / 1000;
		int minToEnd = (secToEnd / 60) % 60;
		secToEnd = secToEnd % 60;
612
613
		string secStrToEnd = secToEnd < 10 ? "0" + to_string(secToEnd) : to_string(secToEnd);
		string minStrToEnd = minToEnd < 10 ? "0" + to_string(minToEnd) : to_string(minToEnd);
Matteo's avatar
update    
Matteo committed
614
615
616
617

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

618
		if ((video_current_ms - lastSaved > savingRate) && frameDifference(prevFrame, frame, msToEnd, capstan, tape, args)) {
Matteo's avatar
update    
Matteo committed
619
			// An Irregularity has been found!
620
			auto [odd_frame, even_frame] = frame.deinterlace();
Matteo's avatar
update    
Matteo committed
621
622
623

			// Extract the image corresponding to the ROIs
			Point2f pts[4];
624
625
626
627
628
629
			savingPinchRoller ? rectCapstan.points(pts) : rectTape.points(pts);

			Frame subImage(cv::Mat(frame, cv::Rect(100, min(pts[1].y, pts[2].y), frame.cols - 100, static_cast<int>(rectTape.size.height))));
			// the following comment was in the previous code, if some errors occur, add this correction in the deinterlace method
			// If the found rectangle is of odd height, we must increase evenSubImage height by 1, otherwise we have segmentation_fault!!!
			auto [oddSubImage, evenSubImage] = subImage.deinterlace();
Matteo's avatar
update    
Matteo committed
630
631
632
633
634

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

			string irregularityImageFilename = to_string(savedFrames) + "_" + safeTimeLabel + ".jpg";
635
			cv::imwrite(irregularityImagesPath / irregularityImageFilename, odd_frame); // FIXME: should it be frame? or maybe is only odd for the classification step?
Matteo's avatar
update    
Matteo committed
636
637

			// Append Irregularity information to JSON
Matteo's avatar
Matteo committed
638
639
640
			Irregularity irreg = Irregularity(Source::Video, timeLabel);
			irregularityFileOutput1["Irregularities"] += irreg.to_JSON();
			irregularityFileOutput2["Irregularities"] += irreg.set_image_URI(irregularityImagesPath.string() + "/" + irregularityImageFilename).to_JSON();
641

Matteo's avatar
update    
Matteo committed
642
643
			lastSaved = video_current_ms;
			savedFrames++;
644

Matteo's avatar
update    
Matteo committed
645
646
647
648
649
		} else {
			unsavedFrames++;
		}
		prevFrame = frame;
	}
650
651
652
}


Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
653
/**
Matteo's avatar
Matteo committed
654
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
655
656
657
658
659
660
661
662
663
664
665
666
 * @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.
 */
667
668
int main(int argc, char** argv) {

669
670
671
672
	Args args = argc > 1 ? Args::from_cli(argc, argv) : Args::from_file(CONFIG_FILE);
	SceneObject capstan = SceneObject::from_file(CONFIG_FILE, Object::CAPSTAN);
	SceneObject tape = SceneObject::from_file(CONFIG_FILE, Object::TAPE);

Matteo's avatar
update    
Matteo committed
673
674
	json irregularityFileInput;
	fs::path irregularityFileInputPath;
Matteo's avatar
update    
Matteo committed
675
676
	cv::Mat myFrame;

677
	const fs::path VIDEO_PATH = args.workingPath / "PreservationAudioVisualFile" / args.filesName;
Matteo's avatar
update    
Matteo committed
678
679

    if (files::findFileName(VIDEO_PATH, fileName, extension) == -1) {
680
681
        cerr << RED << BOLD << "Input error!" << END << endl << RED << VIDEO_PATH.string() << " cannot be found or opened." << END << endl;
		std::exit(EXIT_FAILURE);
682
683
    }

684
	irregularityFileInputPath = args.workingPath / "temp" / fileName / "AudioAnalyser_IrregularityFileOutput1.json";
685

686
	// Input JSON check
687
	ifstream iJSON(irregularityFileInputPath);
688
	if (iJSON.fail()) {
689
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
690
		std::exit(EXIT_FAILURE);
691
	}
692
	if (args.speed != 7.5 && args.speed != 15) {
693
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
694
		std::exit(EXIT_FAILURE);
695
	}
Matteo's avatar
update    
Matteo committed
696
	if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
697
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
698
		std::exit(EXIT_FAILURE);
699
	}
Matteo's avatar
update    
Matteo committed
700
	if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
701
		cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl;
702
		std::exit(EXIT_FAILURE);
703
704
	}

705
	// Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
706
707
	if (args.brands) {
		if (args.speed == 15)
Matteo's avatar
update    
Matteo committed
708
			tape.threshold.percentual += 6;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
709
	} else
710
		if (args.speed == 15)
Matteo's avatar
update    
Matteo committed
711
			tape.threshold.percentual += 20;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
712
		else
Matteo's avatar
update    
Matteo committed
713
			tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
714

715
716
    cout << endl;
	cout << "Parameters:" << endl;
717
718
	cout << "    Brands: " << args.brands << endl;
	cout << "    Speed: " << args.speed << endl;
Matteo's avatar
update    
Matteo committed
719
720
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
	cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
721
	cout << endl;
722
723
724
725

	// Read input JSON
	iJSON >> irregularityFileInput;

726
727
728
	/*********************************************************************************************/
	/*********************************** MAKE OUTPUT DIRECTORY ***********************************/
	/*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
729
730

	// Make directory with fileName name
731
	outputPath = args.workingPath / "temp" / fileName;
732
	int outputFileNameDirectory = create_directory(outputPath);
Matteo's avatar
update    
Matteo committed
733

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

737
738
739
	/*********************************************************************************************/
	/************************************** AREAS DETECTION **************************************/
	/*********************************************************************************************/
740

Matteo's avatar
update    
Matteo committed
741
	cv::VideoCapture videoCapture(VIDEO_PATH);
742
    if (!videoCapture.isOpened()) {
743
744
		pprint("Video unreadable.", RED + BOLD);
		std::exit(EXIT_FAILURE);
745
746
    }

Matteo's avatar
update    
Matteo committed
747
	int frames_number = videoCapture.get(CAP_PROP_FRAME_COUNT);
748
	// Set frame position to half video length
Matteo's avatar
update    
Matteo committed
749
	videoCapture.set(CAP_PROP_POS_FRAMES, frames_number/2);
750
	// Get frame
751
	videoCapture >> myFrame;
752

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

755
	bool found = findProcessingAreas(myFrame, tape, capstan);
756
757
758
759

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

Matteo's avatar
update    
Matteo committed
760
	if (!found) {
Matteo's avatar
Matteo committed
761
		pprint("Processing area not found. Try changing JSON parameters.", RED);
762
		std::exit(EXIT_FAILURE);
763
764
	}

765
	/*********************************************************************************************/
766
	/**************************************** PROCESSING *****************************************/
767
	/*********************************************************************************************/
768

Matteo's avatar
Matteo committed
769
	pprint("\nProcessing...", CYAN);
770
771
772
773
774

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

775
	processing(videoCapture, capstan, tape, args);
776
777
778
779
780

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

781
782
	string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
	cout << endl << result << endl;
783

784
785
786
	/*********************************************************************************************/
	/************************************* IRREGULARITY FILES ************************************/
	/*********************************************************************************************/
787

788
	fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
Matteo's avatar
update    
Matteo committed
789
	files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
790
791

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

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

797
798
    return 0;
}