main.cpp 31 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>
Matteo's avatar
Matteo committed
41
#include <opencv2/calib3d.hpp>
42
#include <opencv2/core/core.hpp>
Matteo's avatar
Matteo committed
43
44
#include <opencv2/features2d.hpp>
#include <opencv2/highgui.hpp>
45
46
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
Matteo's avatar
Matteo committed
47
#include <opencv2/xfeatures2d.hpp>
48

Matteo's avatar
Matteo committed
49
50
51
52
53
54
#include "forAudioAnalyser.h"
#include "lib/Irregularity.h"
#include "lib/IrregularityFile.h"
#include "lib/colors.h"
#include "lib/files.h"
#include "lib/time.h"
55
#include "utility.h"
Matteo's avatar
update    
Matteo committed
56

Matteo's avatar
Matteo committed
57
58
59
60
#define A_IRREG_FILE_1 "AudioAnalyser_IrregularityFileOutput1.json"
#define V_IRREG_FILE_1 "VideoAnalyser_IrregularityFileOutput1.json"
#define V_IRREG_FILE_2 "VideoAnalyser_IrregularityFileOutput2.json"

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

Matteo's avatar
Matteo committed
68
69
70
71
72
73
74
75
/**
 * @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.
 */
76
77
78
bool useSURF = true;

bool endTapeSaved = false;
79
float mediaPrevFrame = 0;
Matteo's avatar
Matteo committed
80
81
82
83
84
85

/**
 * @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
86
float firstInstant = 0;
87

88
// Path variables
Matteo's avatar
Matteo committed
89
90
static fs::path outputPath{};
static fs::path irregularityImagesPath{};
91
// JSON files
Matteo's avatar
Matteo committed
92
93
static json irregularityFileOutput1{};
static json irregularityFileOutput2{};
94
// RotatedRect identifying the processing area
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
95
RotatedRect rect, rectTape, rectCapstan;
96

Matteo's avatar
Matteo committed
97
/**
Matteo's avatar
Matteo committed
98
 * @fn void pprint(string text, string color)
Matteo's avatar
Matteo committed
99
 * @brief Prints a text in a given color.
Matteo's avatar
Matteo committed
100
 *
Matteo's avatar
Matteo committed
101
 * @param text
Matteo's avatar
Matteo committed
102
 * @param color
Matteo's avatar
Matteo committed
103
 */
Matteo's avatar
Matteo committed
104
105
106
107
108
109
void pprint(string text, string color) { std::cout << color << text << END << endl; }
void print_error_and_exit(string title, string message) {
    std::cerr << RED << BOLD << title << END << endl;
    std::cerr << RED << message << END << endl;
    exit(EXIT_FAILURE);
}
Matteo's avatar
Matteo committed
110

111
struct Args {
Matteo's avatar
Matteo committed
112
113
    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
114
    string filesName; /**< The name of the preservation files to be considered */
Matteo's avatar
Matteo committed
115
116
    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
117
118

    Args(fs::path workingPath, string filesName, bool brands, float speed) {
Matteo's avatar
Matteo committed
119
        if (speed != 7.5 && speed != 15) throw invalid_argument("Speed must be 7.5 or 15");
Matteo's avatar
Matteo committed
120
121
122
123
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
        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")) {
Matteo's avatar
Matteo committed
154
                std::cout << desc << "\n";
Matteo's avatar
Matteo committed
155
156
157
158
                std::exit(EXIT_SUCCESS);
            }
            po::notify(vm);
        } catch (po::invalid_command_line_syntax& e) {
Matteo's avatar
Matteo committed
159
            print_error_and_exit("Invalid command line syntax!", string(e.what()));
Matteo's avatar
Matteo committed
160
        } catch (po::required_option& e) {
Matteo's avatar
Matteo committed
161
            print_error_and_exit("Missing required option!", string(e.what()));
Matteo's avatar
Matteo committed
162
        } catch (nlohmann::detail::type_error e) {
Matteo's avatar
Matteo committed
163
            print_error_and_exit("config.json error!", string(e.what()));
Matteo's avatar
Matteo committed
164
165
166
167
168
        }

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

171
172
173
174
// 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";
175

Matteo's avatar
Matteo committed
176
/**
Matteo's avatar
update    
Matteo committed
177
178
 * @brief Get the next frame object.
 *
Matteo's avatar
Matteo committed
179
180
 * 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
181
 *
Matteo's avatar
Matteo committed
182
183
184
185
 * 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
186
 *
Matteo's avatar
Matteo committed
187
188
189
 * 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
190
 *
Matteo's avatar
Matteo committed
191
192
193
194
195
196
197
198
199
200
201
202
203
204
 * @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
205
double rotatedRectArea(RotatedRect rect) { return rect.size.width * rect.size.height; }
206

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
207
/**
Matteo's avatar
Matteo committed
208
209
 * @fn std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>>
 * findObject(Mat model, SceneObject object)
Matteo's avatar
Matteo committed
210
211
212
 * @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
213
214
215
216
217
218
219
220
221
 * 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
222
 */
Matteo's avatar
Matteo committed
223
224
225
226
227
228
229
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();
230

Matteo's avatar
Matteo committed
231
232
    vector<Vec4f> positionsPos, positionsNeg;
    Mat votesPos, votesNeg;
233

Matteo's avatar
Matteo committed
234
235
    double maxValPos = 0, maxValNeg = 0;
    int indexPos = 0, indexNeg = 0;
236

Matteo's avatar
Matteo committed
237
238
239
240
    alg->setMinDist(object.minDist);
    alg->setLevels(360);
    alg->setDp(2);
    alg->setMaxBufferSize(1000);
241

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

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

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

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

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

Matteo's avatar
Matteo committed
257
258
    utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg,
                         processing_area);
259

Matteo's avatar
Matteo committed
260
261
262
263
264
265
    for (int i = 0; i < votesPos.size().width; i++) {
        if (votesPos.at<int>(i) >= maxValPos) {
            maxValPos = votesPos.at<int>(i);
            indexPos = i;
        }
    }
266

Matteo's avatar
Matteo committed
267
268
269
270
271
272
    for (int i = 0; i < votesNeg.size().width; i++) {
        if (votesNeg.at<int>(i) >= maxValNeg) {
            maxValNeg = votesNeg.at<int>(i);
            indexNeg = i;
        }
    }
273

Matteo's avatar
Matteo committed
274
    return {indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg};
Matteo's avatar
Matteo committed
275
276
277
}

/**
Matteo's avatar
Matteo committed
278
 * @fn bool findProcessingAreas(Mat myFrame)
Matteo's avatar
Matteo committed
279
280
281
 * @brief Identifies the Regions Of Interest (ROIs) on the video,
 * which are:
 * - The reading head;
Matteo's avatar
Matteo committed
282
283
 * - The tape area under the tape head (computed on the basis of the detected
 * reading head);
Matteo's avatar
Matteo committed
284
285
286
287
288
 * - The capstan.
 * @param myFrame The current frame of the video.
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
289
bool findProcessingAreas(Mat myFrame, SceneObject tape, SceneObject capstan) {
Matteo's avatar
Matteo committed
290
    /************************** READING HEAD DETECTION ***********************/
Matteo's avatar
Matteo committed
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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336

    // 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
337

Matteo's avatar
Matteo committed
338
    /************************************ TAPE AREA DETECTION ****************/
Matteo's avatar
Matteo committed
339
340
341
342
343
344
345

    // 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
346
    /************************************* CAPSTAN DETECTION ******************/
Matteo's avatar
Matteo committed
347
348
349

    // Read template image - it is smaller than before, therefore there is no
    // need to downsample
Matteo's avatar
Matteo committed
350
    Mat capstan_template = cv::imread(CAPSTAN_TEMPLATE_IMG, IMREAD_GRAYSCALE);
Matteo's avatar
Matteo committed
351
352
353
354
355
356
357
358
359

    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;

Matteo's avatar
Matteo committed
360
        detector->detectAndCompute(capstan_template, noArray(), keypoints_object, descriptors_object);
Matteo's avatar
Matteo committed
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
        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
378
        cv::drawMatches(capstan_template, keypoints_object, halved_gray_current_frame, keypoints_scene, good_matches,
Matteo's avatar
update    
Matteo committed
379
380
                        img_matches, Scalar::all(-1), Scalar::all(-1), vector<char>(),
                        DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
Matteo's avatar
Matteo committed
381
382
383
384
385
386
387
388
        // 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
389
        Mat H = cv::findHomography(obj, scene, RANSAC);
Matteo's avatar
Matteo committed
390
391
392
        // Get the corners from the image_1 ( the object to be "detected" )
        vector<Point2f> obj_corners(4);
        obj_corners[0] = Point2f(0, 0);
Matteo's avatar
Matteo committed
393
394
395
        obj_corners[1] = Point2f((float)capstan_template.cols, 0);
        obj_corners[2] = Point2f((float)capstan_template.cols, (float)capstan_template.rows);
        obj_corners[3] = Point2f(0, (float)capstan_template.rows);
Matteo's avatar
Matteo committed
396
        vector<Point2f> scene_corners(4);
Matteo's avatar
Matteo committed
397
        cv::perspectiveTransform(obj_corners, scene_corners, H);
Matteo's avatar
Matteo committed
398
399
400
401
402
403
404
405
406
407
408

        // 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),
Matteo's avatar
Matteo committed
409
                                          capstan_template.cols - 20, capstan_template.rows - 90, 0, 0, 1);
Matteo's avatar
Matteo committed
410
411
412
413
414
415
416
417
418
419
420
421
422
423

    } 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] =
Matteo's avatar
Matteo committed
424
            findObject(capstan_template, capstan, capstanProcessingAreaGrayscale);
Matteo's avatar
Matteo committed
425
426
427
428

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

Matteo's avatar
Matteo committed
453
    // Save the image containing the ROIs
Matteo's avatar
Matteo committed
454
455
456
    cv::imwrite(outputPath.string() + "/tapeAreas.jpg", myFrame);

    return true;
457
458
}

Matteo's avatar
Matteo committed
459
460
461
/**
 * @fn RotatedRect check_skew(RotatedRect roi)
 * @brief Check if the region of interest is skewed and correct it
Matteo's avatar
Matteo committed
462
 *
Matteo's avatar
Matteo committed
463
464
465
 * @param roi the region of interest
 * @return RotatedRect the corrected region of interest
 */
Matteo's avatar
Matteo committed
466
RotatedRect check_skew(RotatedRect roi) {
Matteo's avatar
Matteo committed
467
468
469
470
471
472
473
474
475
    // 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
476
477
478
}

/**
Matteo's avatar
Matteo committed
479
480
481
482
483
 * @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
484
485
486
487
488
489
 * @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
490
491
492
493
494
    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
495
}
496

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
497
/**
Matteo's avatar
update    
Matteo committed
498
 * @fn bool is_frame_different(cv::Mat prevFrame, cv::Mat currentFrame, int
Matteo's avatar
Matteo committed
499
500
501
502
 * 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
503
504
505
 *
 * @param prevFrame the frame before the current one;
 * @param currentFrame the current frame;
Matteo's avatar
Matteo committed
506
507
 * @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
508
509
510
 * @return true if a potential Irregularity has been found;
 * @return false otherwise.
 */
Matteo's avatar
update    
Matteo committed
511
bool is_frame_different(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd, SceneObject capstan, SceneObject tape,
Matteo's avatar
Matteo committed
512
                        Args args) {
Matteo's avatar
Matteo committed
513
514
515
516
517
518
519
    bool result = false;

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

    // In the last minute of the video, check for pinchRoller position for
    // endTape event
    if (!endTapeSaved && msToEnd < 60000) {
Matteo's avatar
Matteo committed
520
521
        float capstanDifferentPixelsThreshold =
            (rectCapstan.size.width * rectCapstan.size.height) * capstan.threshold.percentual / 100;
Matteo's avatar
Matteo committed
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542

        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) {
            endTapeSaved = true;  // Never check again for end tape instant
            return true;
        }
    }
543

Matteo's avatar
Matteo committed
544
    /********************* Tape analysis *********************/
545

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

Matteo's avatar
Matteo committed
550
551
552
553
554
    RotatedRect corrected_tape_roi = check_skew(rectTape);
    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);
    Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_tape_roi);
555

Matteo's avatar
Matteo committed
556
    /********************** Segment analysis ************************/
557

Matteo's avatar
Matteo committed
558
559
560
    int blackPixels = 0;
    float mediaCurrFrame;
    int totColoreCF = 0;
Matteo's avatar
Matteo committed
561

Matteo's avatar
Matteo committed
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
    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 *****/
Matteo's avatar
Matteo committed
584
585
586
587
588
589
590
        // 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 (args.brands && firstBrand && firstInstant - msToEnd > 5000) {
            firstBrand = false;
            result = true;
Matteo's avatar
Matteo committed
591
592
        }
    }
593

Matteo's avatar
Matteo committed
594
595
    // Update mediaPrevFrame
    mediaPrevFrame = mediaCurrFrame;
596

Matteo's avatar
Matteo committed
597
598
    return result;
}
599

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
600
/**
Matteo's avatar
Matteo committed
601
602
 * @fn void processing(cv::VideoCapture videoCapture, SceneObject capstan,
 * SceneObject tape, Args args)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
603
 * @brief video processing phase, where each frame is analysed.
Matteo's avatar
Matteo committed
604
605
606
 * It saves the IrregularityImages and updates the IrregularityFiles if an
 * Irregularity is found
 *
Matteo's avatar
update    
Matteo committed
607
 * @note To be able to work with the "old" neural network (by Ilenya),
Matteo's avatar
Matteo committed
608
609
610
611
612
613
 * 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
614
615
 *
 * @param videoCapture the input Preservation Audio-Visual File;
Matteo's avatar
update    
Matteo committed
616
617
618
 * @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
619
 */
620
void processing(cv::VideoCapture videoCapture, SceneObject capstan, SceneObject tape, Args args) {
Matteo's avatar
Matteo committed
621
622
623
    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
624
    int savedFrames = 0;
Matteo's avatar
update    
Matteo committed
625
    bool irregularity_found = false;
Matteo's avatar
Matteo committed
626
627

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

    while (videoCapture.isOpened()) {
Matteo's avatar
update    
Matteo committed
632
        Frame frame = get_next_frame(videoCapture, args.speed, irregularity_found);
Matteo's avatar
Matteo committed
633
634
635
        video_current_ms = videoCapture.get(CAP_PROP_POS_MSEC);

        if (frame.empty()) {
Matteo's avatar
Matteo committed
636
            std::cout << endl << "Empty frame!" << endl;
Matteo's avatar
Matteo committed
637
638
639
640
641
642
643
644
645
646
647
648
649
            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;
Matteo's avatar
Matteo committed
650
651
        string secStrToEnd = (secToEnd < 10 ? "0" : "") + to_string(secToEnd);
        string minStrToEnd = (minToEnd < 10 ? "0" : "") + to_string(minToEnd);
Matteo's avatar
Matteo committed
652

Matteo's avatar
Matteo committed
653
654
        std::cout << "\rIrregularities: " << savedFrames << ".   ";
        std::cout << "Remaining video time [mm:ss]: " << minStrToEnd << ":" << secStrToEnd << flush;
Matteo's avatar
Matteo committed
655

Matteo's avatar
update    
Matteo committed
656
657
        irregularity_found = is_frame_different(prevFrame, frame, msToEnd, capstan, tape, args);
        if (irregularity_found) {
Matteo's avatar
Matteo committed
658
            auto [odd_frame, _] = frame.deinterlace();
Matteo's avatar
Matteo committed
659

Matteo's avatar
update    
Matteo committed
660
661
            string irregularityImageFilename =
                to_string(savedFrames) + "_" + getTimeLabel(video_current_ms, "-") + ".jpg";
Matteo's avatar
Matteo committed
662
            cv::imwrite(irregularityImagesPath / irregularityImageFilename, odd_frame);
Matteo's avatar
Matteo committed
663
664

            // Append Irregularity information to JSON
Matteo's avatar
update    
Matteo committed
665
            Irregularity irreg = Irregularity(Source::Video, getTimeLabel(video_current_ms, ":"));
Matteo's avatar
Matteo committed
666
667
668
669
670
671
672
673
            irregularityFileOutput1["Irregularities"] += irreg.to_JSON();
            irregularityFileOutput2["Irregularities"] +=
                irreg.set_image_URI(irregularityImagesPath.string() + "/" + irregularityImageFilename).to_JSON();

            savedFrames++;
        }
        prevFrame = frame;
    }
674
675
}

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
676
/**
Matteo's avatar
Matteo committed
677
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
678
679
680
681
682
683
684
685
 * @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
686
687
688
689
690
 * @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
691
692
693
694
 * @param argc Command line arguments count;
 * @param argv Command line arguments.
 * @return int program status.
 */
695
int main(int argc, char** argv) {
Matteo's avatar
Matteo committed
696
697
    SceneObject capstan = SceneObject::from_file(CONFIG_FILE, ROI::CAPSTAN);
    SceneObject tape = SceneObject::from_file(CONFIG_FILE, ROI::TAPE);
Matteo's avatar
Matteo committed
698
    const Args args = argc > 1 ? Args::from_cli(argc, argv) : Args::from_file(CONFIG_FILE);
Matteo's avatar
Matteo committed
699
    const fs::path VIDEO_PATH = args.workingPath / "PreservationAudioVisualFile" / args.filesName;
Matteo's avatar
Matteo committed
700
    const auto [fileName, extension] = files::get_filename_and_extension(VIDEO_PATH);
Matteo's avatar
Matteo committed
701
    const fs::path irregularityFileInputPath = args.workingPath / "temp" / fileName / A_IRREG_FILE_1;
Matteo's avatar
Matteo committed
702

Matteo's avatar
Matteo committed
703
704
705
706
707
708
    std::cout << "Video to be analysed: " << endl;
    std::cout << "\tFile name:  " << fileName << endl;
    std::cout << "\tExtension:  " << extension << endl;

    if (extension.compare("avi") != 0 && extension.compare("mp4") != 0 && extension.compare("mov") != 0)
        print_error_and_exit("Input error", "The input file must be an AVI, MP4 or MOV file.");
Matteo's avatar
Matteo committed
709
    ifstream iJSON(irregularityFileInputPath);
Matteo's avatar
Matteo committed
710
711
    if (iJSON.fail())
        print_error_and_exit("config.json error", irregularityFileInputPath.string() + " cannot be found or opened.");
Matteo's avatar
Matteo committed
712
713

    // Read input JSON
Matteo's avatar
Matteo committed
714
    json irregularityFileInput;
Matteo's avatar
Matteo committed
715
    iJSON >> irregularityFileInput;
716

Matteo's avatar
Matteo committed
717
    // Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
Matteo committed
718
719
720
    if (args.speed == 15) {
        tape.threshold.percentual += args.brands ? 6 : 20;
    } else if (!args.brands)
Matteo's avatar
Matteo committed
721
        tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
722

Matteo's avatar
Matteo committed
723
724
    // Make directory with fileName name
    outputPath = args.workingPath / "temp" / fileName;
Matteo's avatar
Matteo committed
725
    fs::create_directory(outputPath);
Matteo's avatar
update    
Matteo committed
726

Matteo's avatar
Matteo committed
727
    irregularityImagesPath = outputPath / "IrregularityImages";
Matteo's avatar
Matteo committed
728
    fs::create_directory(irregularityImagesPath);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
729

Matteo's avatar
Matteo committed
730
731
    cv::VideoCapture videoCapture(VIDEO_PATH);  // Open video file
    if (!videoCapture.isOpened()) print_error_and_exit("Video error", "Video file cannot be opened.");
732

Matteo's avatar
Matteo committed
733
734
    videoCapture.set(CAP_PROP_POS_FRAMES,
                     videoCapture.get(CAP_PROP_FRAME_COUNT) / 2);  // Set frame position to half video length
Matteo's avatar
update    
Matteo committed
735
    cv::Mat middle_frame = get_next_frame(videoCapture, args.speed);
Matteo's avatar
Matteo committed
736
    videoCapture.set(CAP_PROP_POS_FRAMES, 0);  // Reset frame position
737

Matteo's avatar
Matteo committed
738
    std::cout << "\tResolution: " << middle_frame.cols << "x" << middle_frame.rows << "\n\n";
739

Matteo's avatar
update    
Matteo committed
740
    bool found = findProcessingAreas(middle_frame, tape, capstan);
Matteo's avatar
Matteo committed
741
    if (!found) print_error_and_exit("Processing area not found", "Try changing JSON parameters.");
742

Matteo's avatar
Matteo committed
743
    pprint("Processing...", CYAN);
Matteo's avatar
Matteo committed
744
    processing(videoCapture, capstan, tape, args);
745

Matteo's avatar
Matteo committed
746
    files::save_file(outputPath / V_IRREG_FILE_1, irregularityFileOutput1.dump(4));
747

Matteo's avatar
Matteo committed
748
749
    // Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier
    extractIrregularityImagesForAudio(outputPath, VIDEO_PATH, irregularityFileInput, irregularityFileOutput2);
Matteo's avatar
Matteo committed
750
    files::save_file(outputPath / V_IRREG_FILE_2, irregularityFileOutput2.dump(4));
751

Matteo's avatar
Matteo committed
752
    return EXIT_SUCCESS;
753
}