main.cpp 33.1 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
 *  MPAI CAE-ARP Video Analyser.
 *
 *	Implements MPAI CAE-ARP Video Analyser Technical Specification.
Matteo's avatar
Matteo committed
7
8
 *	It identifies Irregularities on the Preservation Audio-Visual File,
 *providing:
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
9
10
11
 *	- Irregularity Files;
 *	- Irregularity Images.
 *
Matteo's avatar
Matteo committed
12
13
 * @warning Currently, this program is only compatible with the Studer A810
 *and videos recorded in PAL standard.
Matteo's avatar
update    
Matteo committed
14
 *
Matteo's avatar
Matteo committed
15
16
17
18
19
 * @todo
 *  - A resize function of the entire video should be implemented if it does not
 *conform to the PAL standard (currently taken for granted).
 *  - Progressive videos, which do not require deinterlacing, should be managed
 *(in the code there are several steps that operate considering this property).
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
20
 *
Matteo's avatar
Matteo committed
21
22
 *  @author Nadir Dalla Pozza <nadir.dallapozza@unipd.it>
 *  @author Matteo Spanio <dev2@audioinnova.com>
Matteo's avatar
Matteo committed
23
 *	@copyright 2023, Audio Innova S.r.l.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
24
25
 *	@credits Niccolò Pretto, Nadir Dalla Pozza, Sergio Canazza
 *	@license GPL v3.0
Matteo's avatar
Matteo committed
26
 *	@version 1.1.2
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
27
28
 *	@status Production
 */
29
#include <stdlib.h>
30
31
32
#include <sys/timeb.h>

#include <boost/lexical_cast.hpp>
Matteo's avatar
Matteo committed
33
34
35
36
37
38
39
40
#include <boost/program_options.hpp>
#include <boost/uuid/uuid.hpp>             // uuid class
#include <boost/uuid/uuid_generators.hpp>  // generators
#include <boost/uuid/uuid_io.hpp>          // streaming operators etc.
#include <filesystem>
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
41
42
43
44
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
Matteo's avatar
Matteo committed
45
#include <ranges>
46

Matteo's avatar
Matteo committed
47
48
49
50
51
52
#include "forAudioAnalyser.h"
#include "lib/Irregularity.h"
#include "lib/IrregularityFile.h"
#include "lib/colors.h"
#include "lib/files.h"
#include "lib/time.h"
53
#include "opencv2/calib3d.hpp"
Matteo's avatar
Matteo committed
54
55
#include "opencv2/core.hpp"
#include "opencv2/features2d.hpp"
56
57
58
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/xfeatures2d.hpp"
59
#include "utility.h"
Matteo's avatar
update    
Matteo committed
60

Matteo's avatar
Matteo committed
61
62
63
64
#define A_IRREG_FILE_1 "AudioAnalyser_IrregularityFileOutput1.json"
#define V_IRREG_FILE_1 "VideoAnalyser_IrregularityFileOutput1.json"
#define V_IRREG_FILE_2 "VideoAnalyser_IrregularityFileOutput2.json"

65
66
using namespace cv;
using namespace std;
Matteo's avatar
update    
Matteo committed
67
using utility::Frame;
68
using json = nlohmann::json;
69
70
namespace fs = std::filesystem;
namespace po = boost::program_options;
71

Matteo's avatar
Matteo committed
72
73
74
75
76
77
78
79
/**
 * @const bool useSURF
 * @brief If true, SURF is used for capstan detection, otherwise GHT is used.
 *
 * For capstan detection, there are two alternative approaches:
 * 1. Generalized Hough Transform
 * 2. SURF.
 */
80
81
bool useSURF = true;

Matteo's avatar
update    
Matteo committed
82
83
bool savingPinchRoller = false;
bool pinchRollerRect = false;
84
bool savingBrand = false;
85
bool endTapeSaved = false;
86
float mediaPrevFrame = 0;
Matteo's avatar
Matteo committed
87
88
89
90
91
92

/**
 * @var bool firstBrand
 * @brief The first frame containing brands on tape must be saved
 */
bool firstBrand = true;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
93
float firstInstant = 0;
94
string fileName, extension;
95

96
// Path variables
Matteo's avatar
Matteo committed
97
98
static fs::path outputPath{};
static fs::path irregularityImagesPath{};
99
// JSON files
Matteo's avatar
Matteo committed
100
101
102
static json configurationFile{};
static json irregularityFileOutput1{};
static json irregularityFileOutput2{};
103
// RotatedRect identifying the processing area
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
104
RotatedRect rect, rectTape, rectCapstan;
105

Matteo's avatar
Matteo committed
106
/**
Matteo's avatar
Matteo committed
107
 * @fn void pprint(string text, string color)
Matteo's avatar
Matteo committed
108
 * @brief Prints a text in a given color.
Matteo's avatar
Matteo committed
109
 *
Matteo's avatar
Matteo committed
110
 * @param text
Matteo's avatar
Matteo committed
111
 * @param color
Matteo's avatar
Matteo committed
112
 */
Matteo's avatar
Matteo committed
113
void pprint(string text, string color) { cout << color << text << END << endl; }
Matteo's avatar
Matteo committed
114

115
struct Args {
Matteo's avatar
Matteo committed
116
117
    fs::path
        workingPath;  /**< The working path where all input files are stored and where all output files will be saved */
Matteo's avatar
Matteo committed
118
    string filesName; /**< The name of the preservation files to be considered */
Matteo's avatar
Matteo committed
119
120
    bool brands;      /**< True if tape presents brands on its surface */
    float speed;      /**< The speed at which the tape was read */
Matteo's avatar
Matteo committed
121
122

    Args(fs::path workingPath, string filesName, bool brands, float speed) {
Matteo's avatar
Matteo committed
123
        if (speed != 7.5 && speed != 15) throw invalid_argument("Speed must be 7.5 or 15");
Matteo's avatar
Matteo committed
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
        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;
        try {
            po::options_description desc(
                "A tool that implements MPAI CAE-ARP Video Analyser Technical "
                "Specification.\n"
                "By default, the configuartion parameters are loaded from "
                "config/config.json file,\n"
                "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";
                std::exit(EXIT_SUCCESS);
            }
            po::notify(vm);
        } catch (po::invalid_command_line_syntax& e) {
            pprint("The command line syntax is invalid: " + string(e.what()), RED + BOLD);
            std::exit(EXIT_FAILURE);
        } catch (po::required_option& e) {
            cerr << "Error: " << e.what() << endl;
            std::exit(EXIT_FAILURE);
        } catch (nlohmann::detail::type_error e) {
            pprint("config.json error! " + string(e.what()), RED);
            std::exit(EXIT_FAILURE);
        }

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

178
179
180
181
// 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";
182

Matteo's avatar
Matteo committed
183
/**
Matteo's avatar
update    
Matteo committed
184
185
 * @brief Get the next frame object.
 *
Matteo's avatar
Matteo committed
186
187
 * Whenever we find an Irregularity, we want to skip a lenght equal to the
 * Studer reading head (3 cm = 1.18 inches).
Matteo's avatar
update    
Matteo committed
188
 *
Matteo's avatar
Matteo committed
189
190
191
192
 * Note the following considerations:
 * - since we are analysing video at 25 fps a frame occurs every 40 ms
 * - at 15 ips we cross 3 cm of tape in 79 ms (2 frames)
 * - at 7.5 ips we cross 3 cm of tape in 157 ms (4 frames)
Matteo's avatar
update    
Matteo committed
193
 *
Matteo's avatar
Matteo committed
194
195
196
 * 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
197
 *
Matteo's avatar
Matteo committed
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
 * @param cap VideoCapture object
 * @param speed tape reading speed
 * @return Frame
 */
Frame get_next_frame(VideoCapture& cap, float speed, bool skip = false) {
    if (skip) {
        int ms_to_skip = speed == 15 ? 79 : 157;
        cap.set(CAP_PROP_POS_MSEC, cap.get(CAP_PROP_POS_MSEC) + ms_to_skip);
    }

    Frame frame;
    cap >> frame;
    return frame;
}

Matteo's avatar
Matteo committed
213
double rotatedRectArea(RotatedRect rect) { return rect.size.width * rect.size.height; }
214

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
215
/**
Matteo's avatar
Matteo committed
216
217
 * @fn std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>>
 * findObject(Mat model, SceneObject object)
Matteo's avatar
Matteo committed
218
219
220
 * @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 shape, then choose the one with the
Matteo's avatar
Matteo committed
221
222
223
224
225
226
227
228
229
 * 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
230
 */
Matteo's avatar
Matteo committed
231
232
233
234
235
236
237
std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>> findObject(Mat model, SceneObject object,
                                                                              Mat processing_area) {
    // Algorithm and parameters
    // 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
    Ptr<GeneralizedHoughGuil> alg = createGeneralizedHoughGuil();
238

Matteo's avatar
Matteo committed
239
240
    vector<Vec4f> positionsPos, positionsNeg;
    Mat votesPos, votesNeg;
241

Matteo's avatar
Matteo committed
242
243
    double maxValPos = 0, maxValNeg = 0;
    int indexPos = 0, indexNeg = 0;
244

Matteo's avatar
Matteo committed
245
246
247
248
    alg->setMinDist(object.minDist);
    alg->setLevels(360);
    alg->setDp(2);
    alg->setMaxBufferSize(1000);
249

Matteo's avatar
Matteo committed
250
251
    alg->setAngleStep(1);
    alg->setAngleThresh(object.threshold.angle);
252

Matteo's avatar
Matteo committed
253
254
255
256
    alg->setMinScale(0.9);
    alg->setMaxScale(1.1);
    alg->setScaleStep(0.01);
    alg->setScaleThresh(object.threshold.scale);
257

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

Matteo's avatar
Matteo committed
260
261
    alg->setCannyLowThresh(150);   // Old: 100
    alg->setCannyHighThresh(240);  // Old: 300
262

Matteo's avatar
Matteo committed
263
    alg->setTemplate(model);
264

Matteo's avatar
Matteo committed
265
266
    utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg,
                         processing_area);
267

Matteo's avatar
Matteo committed
268
269
270
271
272
273
    for (int i = 0; i < votesPos.size().width; i++) {
        if (votesPos.at<int>(i) >= maxValPos) {
            maxValPos = votesPos.at<int>(i);
            indexPos = i;
        }
    }
274

Matteo's avatar
Matteo committed
275
276
277
278
279
280
    for (int i = 0; i < votesNeg.size().width; i++) {
        if (votesNeg.at<int>(i) >= maxValNeg) {
            maxValNeg = votesNeg.at<int>(i);
            indexNeg = i;
        }
    }
281

Matteo's avatar
Matteo committed
282
    return {indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg};
Matteo's avatar
Matteo committed
283
284
285
}

/**
Matteo's avatar
Matteo committed
286
 * @fn bool findProcessingAreas(Mat myFrame)
Matteo's avatar
Matteo committed
287
288
289
 * @brief Identifies the Regions Of Interest (ROIs) on the video,
 * which are:
 * - The reading head;
Matteo's avatar
Matteo committed
290
291
 * - The tape area under the tape head (computed on the basis of the detected
 * reading head);
Matteo's avatar
Matteo committed
292
293
294
295
296
 * - The capstan.
 * @param myFrame The current frame of the video.
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
297
bool findProcessingAreas(Mat myFrame, SceneObject tape, SceneObject capstan) {
Matteo's avatar
Matteo committed
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
    /*********************************************************************************************/
    /*********************************** 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);

    // The color is progressively darkened to emphasize that the algorithm found
    // more than one shape
    if (positionsPos.size() > 0)
        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);
    if (positionsNeg.size() > 0)
        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);

    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;
    }
Matteo's avatar
Matteo committed
347

Matteo's avatar
Matteo committed
348
    /************************************ TAPE AREA DETECTION ****************/
Matteo's avatar
Matteo committed
349
350
351
352
353
354
355

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

Matteo's avatar
Matteo committed
356
    /************************************* CAPSTAN DETECTION ******************/
Matteo's avatar
Matteo committed
357
358
359

    // Read template image - it is smaller than before, therefore there is no
    // need to downsample
Matteo's avatar
Matteo committed
360
    Mat templateShape = cv::imread(CAPSTAN_TEMPLATE_IMG, IMREAD_GRAYSCALE);
Matteo's avatar
Matteo committed
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387

    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);
        detector->detectAndCompute(gray_current_frame, noArray(), keypoints_scene, descriptors_scene);

        // 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
Matteo committed
388
        cv::drawMatches(templateShape, keypoints_object, halved_gray_current_frame, keypoints_scene, good_matches,
Matteo's avatar
update    
Matteo committed
389
390
                        img_matches, Scalar::all(-1), Scalar::all(-1), vector<char>(),
                        DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
Matteo's avatar
Matteo committed
391
392
393
394
395
396
397
398
        // Localize the object
        vector<Point2f> obj;
        vector<Point2f> scene;
        for (size_t i = 0; i < good_matches.size(); i++) {
            // 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);
        }
Matteo's avatar
Matteo committed
399
        Mat H = cv::findHomography(obj, scene, RANSAC);
Matteo's avatar
Matteo committed
400
401
402
403
404
405
406
        // 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);
Matteo's avatar
Matteo committed
407
        cv::perspectiveTransform(obj_corners, scene_corners, H);
Matteo's avatar
Matteo committed
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
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
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468

        // 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
        Vec4f positionCapstan(capstanX + 10, capstanY + 45, 1, 0);
        rectCapstan = utility::drawShapes(myFrame, positionCapstan, Scalar(255 - indexPos * 64, 0, 0),
                                          templateShape.cols - 20, templateShape.rows - 90, 0, 0, 1);

    } else {
        // Process only right portion of the image, where the capstain always
        // appears
        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);
        Mat capstanProcessingAreaGrayscale = gray_current_frame(capstanProcessingAreaRect);
        // Reset algorithm and set parameters

        auto [indexPos, indexNeg, maxValPos, maxValNeg, positionsC1Pos, positionsC1Neg] =
            findObject(templateShape, capstan, capstanProcessingAreaGrayscale);

        RotatedRect rectCapstanPos, rectCapstanNeg;
        if (positionsC1Pos.size() > 0)
            rectCapstanPos = utility::drawShapes(myFrame, positionsC1Pos[indexPos], Scalar(255 - indexPos * 64, 0, 0),
                                                 templateShape.cols - 22, templateShape.rows - 92,
                                                 capstanProcessingAreaRectX + 11, capstanProcessingAreaRectY + 46, 1);
        if (positionsC1Neg.size() > 0)
            rectCapstanNeg = utility::drawShapes(myFrame, positionsC1Neg[indexNeg], Scalar(255 - indexNeg * 64, 128, 0),
                                                 templateShape.cols - 22, templateShape.rows - 92,
                                                 capstanProcessingAreaRectX + 11, capstanProcessingAreaRectY + 46, 1);

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

Matteo's avatar
Matteo committed
471
472
473
/**
 * @fn RotatedRect check_skew(RotatedRect roi)
 * @brief Check if the region of interest is skewed and correct it
Matteo's avatar
Matteo committed
474
 *
Matteo's avatar
Matteo committed
475
476
477
 * @param roi the region of interest
 * @return RotatedRect the corrected region of interest
 */
Matteo's avatar
Matteo committed
478
RotatedRect check_skew(RotatedRect roi) {
Matteo's avatar
Matteo committed
479
480
481
482
483
484
485
486
487
    // 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);
    }
Matteo's avatar
Matteo committed
488

Matteo's avatar
Matteo committed
489
    return RotatedRect(roi.center, rect_size, angle);
Matteo's avatar
Matteo committed
490
491
492
}

/**
Matteo's avatar
Matteo committed
493
494
495
496
497
 * @fn Frame get_difference_for_roi(Frame previous, Frame current, RotatedRect
 * roi)
 * @brief Look for differences in two consecutive frames in a specific region of
 * interest
 *
Matteo's avatar
Matteo committed
498
499
500
501
502
503
 * @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) {
Matteo's avatar
Matteo committed
504
505
506
507
508
    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));
Matteo's avatar
Matteo committed
509
}
510

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
511
/**
Matteo's avatar
update    
Matteo committed
512
 * @fn bool is_frame_different(cv::Mat prevFrame, cv::Mat currentFrame, int
Matteo's avatar
Matteo committed
513
514
515
516
 * msToEnd)
 * @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.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
517
518
519
 *
 * @param prevFrame the frame before the current one;
 * @param currentFrame the current frame;
Matteo's avatar
Matteo committed
520
521
 * @param msToEnd the number of milliseconds left before the end of the video.
 * Useful for capstan analysis.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
522
523
524
 * @return true if a potential Irregularity has been found;
 * @return false otherwise.
 */
Matteo's avatar
update    
Matteo committed
525
bool is_frame_different(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd, SceneObject capstan, SceneObject tape,
Matteo's avatar
Matteo committed
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
                     Args args) {
    bool result = false;

    /*********************** Capstan analysis ************************/

    // In the last minute of the video, check for pinchRoller position for
    // endTape event
    if (!endTapeSaved && msToEnd < 60000) {
        // Capstan area
        int capstanAreaPixels = rectCapstan.size.width * rectCapstan.size.height;
        float capstanDifferentPixelsThreshold = capstanAreaPixels * capstan.threshold.percentual / 100;

        RotatedRect corrected_capstan_roi = check_skew(rectCapstan);
        Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_capstan_roi);

        int blackPixelsCapstan = 0;

        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) {
                    // There is a black pixel, then there is a difference
                    // between previous and current frames
                    blackPixelsCapstan++;
                }
            }
        }

        if (blackPixelsCapstan > capstanDifferentPixelsThreshold) {
            savingPinchRoller = true;
            endTapeSaved = true;  // Never check again for end tape instant
            return true;
        }
    }
    // savingPinchRoller 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
    savingPinchRoller = false;
563

Matteo's avatar
Matteo committed
564
    /********************* Tape analysis *********************/
565

Matteo's avatar
Matteo committed
566
567
568
    // Tape area
    int tapeAreaPixels = rotatedRectArea(rectTape);
    float tapeDifferentPixelsThreshold = tapeAreaPixels * tape.threshold.percentual / 100;
569

Matteo's avatar
Matteo committed
570
    RotatedRect corrected_tape_roi = check_skew(rectTape);
571

Matteo's avatar
Matteo committed
572
573
574
    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);
575

Matteo's avatar
Matteo committed
576
    Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_tape_roi);
577

Matteo's avatar
Matteo committed
578
579
580
581
    int decEnd = (msToEnd % 1000) / 100;
    int secEnd = (msToEnd - (msToEnd % 1000)) / 1000;
    int minEnd = secEnd / 60;
    secEnd = secEnd % 60;
582

Matteo's avatar
Matteo committed
583
    /********************** Segment analysis ************************/
584

Matteo's avatar
Matteo committed
585
586
587
    int blackPixels = 0;
    float mediaCurrFrame;
    int totColoreCF = 0;
Matteo's avatar
Matteo committed
588

Matteo's avatar
Matteo committed
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
    for (int i = 0; i < croppedCurrentFrame.rows; i++) {
        for (int j = 0; j < croppedCurrentFrame.cols; j++) {
            totColoreCF += croppedCurrentFrame.at<cv::Vec3b>(i, j)[0] + croppedCurrentFrame.at<cv::Vec3b>(i, j)[1] +
                           croppedCurrentFrame.at<cv::Vec3b>(i, j)[2];
            if (difference_frame.at<cv::Vec3b>(i, j)[0] == 0) {
                blackPixels++;
            }
        }
    }
    mediaCurrFrame = totColoreCF / tapeAreaPixels;

    /*********************** Decision stage ************************/

    if (blackPixels > tapeDifferentPixelsThreshold) {  // The threshold must be passed

        /***** AVERAGE_COLOR-BASED DECISION *****/
        if (mediaPrevFrame > (mediaCurrFrame + 7) ||
            mediaPrevFrame < (mediaCurrFrame - 7)) {  // They are not similar for color average
            result = true;
        }

        /***** BRANDS MANAGEMENT *****/
        if (args.brands) {
            // At the beginning of the video, wait at least 5 seconds before the
            // next Irregularity to consider it as a brand. It is not guaranteed
            // that it will be the first brand, but it is generally a safe
            // approach to have a correct image
            if (firstBrand) {
                if (firstInstant - msToEnd > 5000) {
                    firstBrand = false;
                    savingBrand = true;
                    result = true;
                }
                // In the following iterations reset savingBrand, since we are
                // no longer interested in brands.
            } else
                savingBrand = false;
        }
    }
628

Matteo's avatar
Matteo committed
629
630
    // Update mediaPrevFrame
    mediaPrevFrame = mediaCurrFrame;
631

Matteo's avatar
Matteo committed
632
633
    return result;
}
634

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
635
/**
Matteo's avatar
Matteo committed
636
637
 * @fn void processing(cv::VideoCapture videoCapture, SceneObject capstan,
 * SceneObject tape, Args args)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
638
 * @brief video processing phase, where each frame is analysed.
Matteo's avatar
Matteo committed
639
640
641
 * It saves the IrregularityImages and updates the IrregularityFiles if an
 * Irregularity is found
 *
Matteo's avatar
update    
Matteo committed
642
 * @note To be able to work with the "old" neural network (by Ilenya),
Matteo's avatar
Matteo committed
643
644
645
646
647
648
 * the output images should correspond to the old "whole tape" where, from the
 * frame judged as interesting, an area corresponding to the height of the tape
 * was extracted (so about the height of the current rectangle) and as wide as
 * the original frame (so 720px). This area will then have to be resized to
 * 224x224 as in the past. If instead you decide to use the new neural network,
 * no changes are needed.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
649
650
 *
 * @param videoCapture the input Preservation Audio-Visual File;
Matteo's avatar
update    
Matteo committed
651
652
653
 * @param capstan the capstan SceneObject;
 * @param tape the tape SceneObject;
 * @param args the command line arguments.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
654
 */
655
void processing(cv::VideoCapture videoCapture, SceneObject capstan, SceneObject tape, Args args) {
Matteo's avatar
Matteo committed
656
657
658
    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
Matteo's avatar
update    
Matteo committed
659
    int savedFrames = 0;
Matteo's avatar
update    
Matteo committed
660
    bool irregularity_found = false;
Matteo's avatar
Matteo committed
661
662

    // The first frame of the video won't be processed
Matteo's avatar
update    
Matteo committed
663
    cv::Mat prevFrame = get_next_frame(videoCapture, args.speed, irregularity_found);
Matteo's avatar
Matteo committed
664
    firstInstant = video_length_ms - video_current_ms;
665
666

    while (videoCapture.isOpened()) {
Matteo's avatar
update    
Matteo committed
667
        Frame frame = get_next_frame(videoCapture, args.speed, irregularity_found);
Matteo's avatar
Matteo committed
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
        video_current_ms = videoCapture.get(CAP_PROP_POS_MSEC);

        if (frame.empty()) {
            cout << endl << "Empty frame!" << endl;
            videoCapture.release();
            return;
        }

        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.
            return;

        // Display program status
        int secToEnd = msToEnd / 1000;
        int minToEnd = (secToEnd / 60) % 60;
        secToEnd = secToEnd % 60;
        string secStrToEnd = secToEnd < 10 ? "0" + to_string(secToEnd) : to_string(secToEnd);
        string minStrToEnd = minToEnd < 10 ? "0" + to_string(minToEnd) : to_string(minToEnd);

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

Matteo's avatar
update    
Matteo committed
691
692
        irregularity_found = is_frame_different(prevFrame, frame, msToEnd, capstan, tape, args);
        if (irregularity_found) {
Matteo's avatar
Matteo committed
693
694
            auto [odd_frame, even_frame] = frame.deinterlace();

Matteo's avatar
update    
Matteo committed
695
696
            string irregularityImageFilename =
                to_string(savedFrames) + "_" + getTimeLabel(video_current_ms, "-") + ".jpg";
Matteo's avatar
Matteo committed
697
            cv::imwrite(irregularityImagesPath / irregularityImageFilename, odd_frame);
Matteo's avatar
Matteo committed
698
699

            // Append Irregularity information to JSON
Matteo's avatar
update    
Matteo committed
700
            Irregularity irreg = Irregularity(Source::Video, getTimeLabel(video_current_ms, ":"));
Matteo's avatar
Matteo committed
701
702
703
704
705
706
            irregularityFileOutput1["Irregularities"] += irreg.to_JSON();
            irregularityFileOutput2["Irregularities"] +=
                irreg.set_image_URI(irregularityImagesPath.string() + "/" + irregularityImageFilename).to_JSON();

            savedFrames++;
        }
Matteo's avatar
Matteo committed
707

Matteo's avatar
Matteo committed
708
709
        prevFrame = frame;
    }
710
711
}

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
712
/**
Matteo's avatar
Matteo committed
713
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
714
715
716
717
718
719
720
721
 * @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.
 *
Matteo's avatar
Matteo committed
722
723
724
725
726
 * @todo The main function should be splitted into 2 steps, and each step should be callable from the command line, so
 * that the user can choose to run only the first step, only the second step, or both:
 * - First step: generate irregularity file output 1;
 * - Second step: generate irregularity file output 2.
 *
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
727
728
729
730
 * @param argc Command line arguments count;
 * @param argv Command line arguments.
 * @return int program status.
 */
731
int main(int argc, char** argv) {
Matteo's avatar
Matteo committed
732
    const Args args = argc > 1 ? Args::from_cli(argc, argv) : Args::from_file(CONFIG_FILE);
Matteo's avatar
Matteo committed
733
734
    SceneObject capstan = SceneObject::from_file(CONFIG_FILE, Object::CAPSTAN);
    SceneObject tape = SceneObject::from_file(CONFIG_FILE, Object::TAPE);
735

Matteo's avatar
Matteo committed
736
    json irregularityFileInput;
Matteo's avatar
update    
Matteo committed
737

Matteo's avatar
Matteo committed
738
    const fs::path VIDEO_PATH = args.workingPath / "PreservationAudioVisualFile" / args.filesName;
Matteo's avatar
update    
Matteo committed
739
740

    if (files::findFileName(VIDEO_PATH, fileName, extension) == -1) {
Matteo's avatar
Matteo committed
741
742
743
744
745
        cerr << RED << BOLD << "Input error!" << END << endl
             << RED << VIDEO_PATH.string() << " cannot be found or opened." << END << endl;
        std::exit(EXIT_FAILURE);
    }

Matteo's avatar
Matteo committed
746
    const fs::path irregularityFileInputPath = args.workingPath / "temp" / fileName / A_IRREG_FILE_1;
Matteo's avatar
Matteo committed
747
748
749
750
751
752
753
754

    // Input JSON check
    ifstream iJSON(irregularityFileInputPath);
    if (iJSON.fail()) {
        cerr << RED << BOLD << "config.json error!" << END << endl
             << RED << irregularityFileInputPath.string() << " cannot be found or opened." << END << endl;
        std::exit(EXIT_FAILURE);
    }
Matteo's avatar
Matteo committed
755
756
757

    // Read input JSON
    iJSON >> irregularityFileInput;
758

Matteo's avatar
Matteo committed
759
760
761
762
763
764
765
    // Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
    if (args.brands) {
        if (args.speed == 15) tape.threshold.percentual += 6;
    } else if (args.speed == 15)
        tape.threshold.percentual += 20;
    else
        tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
766

767
    cout << endl;
Matteo's avatar
Matteo committed
768
769
770
    cout << "Parameters:" << endl;
    cout << "    Brands: " << args.brands << endl;
    cout << "    Speed: " << args.speed << endl;
Matteo's avatar
update    
Matteo committed
771
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
Matteo's avatar
Matteo committed
772
773
    cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
    cout << endl;
774

Matteo's avatar
Matteo committed
775
776
    // Make directory with fileName name
    outputPath = args.workingPath / "temp" / fileName;
Matteo's avatar
Matteo committed
777
    fs::create_directory(outputPath);
Matteo's avatar
update    
Matteo committed
778

Matteo's avatar
Matteo committed
779
    irregularityImagesPath = outputPath / "IrregularityImages";
Matteo's avatar
Matteo committed
780
    fs::create_directory(irregularityImagesPath);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
781

Matteo's avatar
Matteo committed
782
    /************************************** AREAS DETECTION *********************************/
783

Matteo's avatar
Matteo committed
784
    cv::VideoCapture videoCapture(VIDEO_PATH);
785
    if (!videoCapture.isOpened()) {
Matteo's avatar
Matteo committed
786
787
        pprint("Video unreadable.", RED + BOLD);
        std::exit(EXIT_FAILURE);
788
789
    }

Matteo's avatar
Matteo committed
790
791
792
    int frames_number = videoCapture.get(CAP_PROP_FRAME_COUNT);
    // Set frame position to half video length
    videoCapture.set(CAP_PROP_POS_FRAMES, frames_number / 2);
Matteo's avatar
update    
Matteo committed
793
    cv::Mat middle_frame = get_next_frame(videoCapture, args.speed);
794

Matteo's avatar
update    
Matteo committed
795
    cout << "Video resolution: " << middle_frame.cols << "x" << middle_frame.rows << endl;
796

Matteo's avatar
update    
Matteo committed
797
    bool found = findProcessingAreas(middle_frame, tape, capstan);
798

Matteo's avatar
Matteo committed
799
800
    // Reset frame position
    videoCapture.set(CAP_PROP_POS_FRAMES, 0);
801

Matteo's avatar
Matteo committed
802
803
804
805
    if (!found) {
        pprint("Processing area not found. Try changing JSON parameters.", RED);
        std::exit(EXIT_FAILURE);
    }
806

Matteo's avatar
Matteo committed
807
    /**************************************** PROCESSING **************************/
808

Matteo's avatar
Matteo committed
809
    pprint("Processing...", CYAN);
810

Matteo's avatar
Matteo committed
811
812
813
    // Processing timer
    time_t startTimer, endTimer;
    startTimer = time(NULL);
814

Matteo's avatar
Matteo committed
815
    processing(videoCapture, capstan, tape, args);
816

Matteo's avatar
Matteo committed
817
818
819
    endTimer = time(NULL);
    float min = (endTimer - startTimer) / 60;
    float sec = (endTimer - startTimer) % 60;
820

Matteo's avatar
Matteo committed
821
822
    string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
    cout << endl << result << endl;
823

Matteo's avatar
Matteo committed
824
    /************************************* IRREGULARITY FILES *****************************/
825

Matteo's avatar
Matteo committed
826
    files::saveFile(outputPath / V_IRREG_FILE_1, irregularityFileOutput1.dump(4), false);
827

Matteo's avatar
Matteo committed
828
829
    // Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier
    extractIrregularityImagesForAudio(outputPath, VIDEO_PATH, irregularityFileInput, irregularityFileOutput2);
830

Matteo's avatar
Matteo committed
831
    files::saveFile(outputPath / V_IRREG_FILE_2, irregularityFileOutput2.dump(4), false);
832

Matteo's avatar
Matteo committed
833
    return EXIT_SUCCESS;
834
}