main.cpp 35.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
 *  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.1
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

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
bool useSURF = true;

Matteo's avatar
update    
Matteo committed
78
79
bool savingPinchRoller = false;
bool pinchRollerRect = false;
80
bool savingBrand = false;
81
bool endTapeSaved = false;
82
float mediaPrevFrame = 0;
Matteo's avatar
Matteo committed
83
84
85
86
87
88

/**
 * @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
89
float firstInstant = 0;
90
string fileName, extension;
91

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

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

111
struct Args {
Matteo's avatar
Matteo committed
112
113
114
115
116
117
118
119
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
fs::path workingPath; /**< The working path where all input files are stored and where all output files will be saved */
    string filesName; /**< The name of the preservation files to be considered */
    bool brands; /**< True if tape presents brands on its surface */
    float speed; /**< The speed at which the tape was read */

    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;
        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>());
    }
170
};
171

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

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
179
/**
Matteo's avatar
Matteo committed
180
181
182
183
184
185
186
187
188
189
190
191
192
193
 * @fn std::tuple<int, int, double, double, vector<Vec4f>, vector<Vec4f>>
 * findObject(Mat model, SceneObject object)
 * @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
194
 */
Matteo's avatar
Matteo committed
195
196
197
198
199
200
201
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();
202

Matteo's avatar
Matteo committed
203
204
    vector<Vec4f> positionsPos, positionsNeg;
    Mat votesPos, votesNeg;
205

Matteo's avatar
Matteo committed
206
207
    double maxValPos = 0, maxValNeg = 0;
    int indexPos = 0, indexNeg = 0;
208

Matteo's avatar
Matteo committed
209
210
211
212
    alg->setMinDist(object.minDist);
    alg->setLevels(360);
    alg->setDp(2);
    alg->setMaxBufferSize(1000);
213

Matteo's avatar
Matteo committed
214
215
    alg->setAngleStep(1);
    alg->setAngleThresh(object.threshold.angle);
216

Matteo's avatar
Matteo committed
217
218
219
220
    alg->setMinScale(0.9);
    alg->setMaxScale(1.1);
    alg->setScaleStep(0.01);
    alg->setScaleThresh(object.threshold.scale);
221

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

Matteo's avatar
Matteo committed
224
225
    alg->setCannyLowThresh(150);   // Old: 100
    alg->setCannyHighThresh(240);  // Old: 300
226

Matteo's avatar
Matteo committed
227
    alg->setTemplate(model);
228

Matteo's avatar
Matteo committed
229
230
    utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg,
                         processing_area);
231

Matteo's avatar
Matteo committed
232
233
234
235
236
237
    for (int i = 0; i < votesPos.size().width; i++) {
        if (votesPos.at<int>(i) >= maxValPos) {
            maxValPos = votesPos.at<int>(i);
            indexPos = i;
        }
    }
238

Matteo's avatar
Matteo committed
239
240
241
242
243
244
    for (int i = 0; i < votesNeg.size().width; i++) {
        if (votesNeg.at<int>(i) >= maxValNeg) {
            maxValNeg = votesNeg.at<int>(i);
            indexNeg = i;
        }
    }
245

Matteo's avatar
Matteo committed
246
    return {indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg};
Matteo's avatar
Matteo committed
247
248
249
}

/**
Matteo's avatar
Matteo committed
250
 * @fn bool findProcessingAreas(Mat myFrame)
Matteo's avatar
Matteo committed
251
252
253
 * @brief Identifies the Regions Of Interest (ROIs) on the video,
 * which are:
 * - The reading head;
Matteo's avatar
Matteo committed
254
255
 * - The tape area under the tape head (computed on the basis of the detected
 * reading head);
Matteo's avatar
Matteo committed
256
257
258
259
260
 * - The capstan.
 * @param myFrame The current frame of the video.
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
261
bool findProcessingAreas(Mat myFrame, SceneObject tape, SceneObject capstan) {
Matteo's avatar
Matteo committed
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
    /*********************************************************************************************/
    /*********************************** 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
311

Matteo's avatar
Matteo committed
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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
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
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
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
    /*********************************************************************************************/
    /************************************ 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);
    rectTape = utility::drawShapes(myFrame, positionTape, Scalar(0, 255 - indexPos * 64, 0), rect.size.width,
                                   50 * (rect.size.width / 200), 0, 0, 1);

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

    // Read template image - it is smaller than before, therefore there is no
    // need to downsample
    Mat templateShape = imread(CAPSTAN_TEMPLATE_IMG, IMREAD_GRAYSCALE);

    if (useSURF) {
        // Step 1: Detect the keypoints using SURF Detector, compute the
        // descriptors
        int minHessian = 100;
        Ptr<xfeatures2d::SURF> detector = xfeatures2d::SURF::create(minHessian);
        vector<KeyPoint> keypoints_object, keypoints_scene;
        Mat descriptors_object, descriptors_scene;

        detector->detectAndCompute(templateShape, noArray(), keypoints_object, descriptors_object);
        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;
        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);
        // 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);
        }
        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
        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;
437
438
}

Matteo's avatar
Matteo committed
439
440
441
/**
 * @fn RotatedRect check_skew(RotatedRect roi)
 * @brief Check if the region of interest is skewed and correct it
Matteo's avatar
Matteo committed
442
 *
Matteo's avatar
Matteo committed
443
444
445
 * @param roi the region of interest
 * @return RotatedRect the corrected region of interest
 */
Matteo's avatar
Matteo committed
446
RotatedRect check_skew(RotatedRect roi) {
Matteo's avatar
Matteo committed
447
448
449
450
451
452
453
454
455
    // 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
456

Matteo's avatar
Matteo committed
457
    return RotatedRect(roi.center, rect_size, angle);
Matteo's avatar
Matteo committed
458
459
460
}

/**
Matteo's avatar
Matteo committed
461
462
463
464
465
 * @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
466
467
468
469
470
471
 * @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
472
473
474
475
476
    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
477
}
478

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
479
/**
Matteo's avatar
Matteo committed
480
481
482
483
484
 * @fn bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int
 * 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
485
486
487
 *
 * @param prevFrame the frame before the current one;
 * @param currentFrame the current frame;
Matteo's avatar
Matteo committed
488
489
 * @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
490
491
492
 * @return true if a potential Irregularity has been found;
 * @return false otherwise.
 */
Matteo's avatar
Matteo committed
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd, SceneObject capstan, SceneObject tape,
                     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;
531

Matteo's avatar
Matteo committed
532
    /********************* Tape analysis *********************/
533

Matteo's avatar
Matteo committed
534
535
536
    // Tape area
    int tapeAreaPixels = rotatedRectArea(rectTape);
    float tapeDifferentPixelsThreshold = tapeAreaPixels * tape.threshold.percentual / 100;
537

Matteo's avatar
Matteo committed
538
    RotatedRect corrected_tape_roi = check_skew(rectTape);
539

Matteo's avatar
Matteo committed
540
541
542
    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);
543

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

Matteo's avatar
Matteo committed
546
547
548
549
    int decEnd = (msToEnd % 1000) / 100;
    int secEnd = (msToEnd - (msToEnd % 1000)) / 1000;
    int minEnd = secEnd / 60;
    secEnd = secEnd % 60;
550

Matteo's avatar
Matteo committed
551
    /********************** Segment analysis ************************/
552

Matteo's avatar
Matteo committed
553
554
555
    int blackPixels = 0;
    float mediaCurrFrame;
    int totColoreCF = 0;
Matteo's avatar
Matteo committed
556

Matteo's avatar
Matteo committed
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
    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;
        }
    }
596

Matteo's avatar
Matteo committed
597
598
    // Update mediaPrevFrame
    mediaPrevFrame = mediaCurrFrame;
599

Matteo's avatar
Matteo committed
600
601
    return result;
}
602

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
603
/**
Matteo's avatar
Matteo committed
604
605
 * @fn void processing(cv::VideoCapture videoCapture, SceneObject capstan,
 * SceneObject tape, Args args)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
606
 * @brief video processing phase, where each frame is analysed.
Matteo's avatar
Matteo committed
607
608
609
 * It saves the IrregularityImages and updates the IrregularityFiles if an
 * Irregularity is found
 *
Matteo's avatar
update    
Matteo committed
610
 * @note To be able to work with the "old" neural network (by Ilenya),
Matteo's avatar
Matteo committed
611
612
613
614
615
616
 * 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
617
618
 *
 * @param videoCapture the input Preservation Audio-Visual File;
Matteo's avatar
update    
Matteo committed
619
620
621
 * @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
622
 */
623
void processing(cv::VideoCapture videoCapture, SceneObject capstan, SceneObject tape, Args args) {
Matteo's avatar
Matteo committed
624
625
626
    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
627
    int savedFrames = 0;
Matteo's avatar
Matteo committed
628
629
630
631
632
633
634
635
636
637
638
    int unsavedFrames = 0;
    float lastSaved = -160;
    // Whenever we find an Irregularity, we want to skip a lenght equal to the
    // Studer reading head (3 cm = 1.18 inches).
    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
    if (args.speed == 7.5) savingRate = 157;  // Time taken to cross 3 cm at 7.5 ips

    // The first frame of the video won't be processed
639
    cv::Mat prevFrame;
Matteo's avatar
Matteo committed
640
641
    videoCapture >> prevFrame;
    firstInstant = video_length_ms - video_current_ms;
642
643

    while (videoCapture.isOpened()) {
Matteo's avatar
Matteo committed
644
        Frame frame;
645
        videoCapture >> frame;
Matteo's avatar
Matteo committed
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
        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;

        if ((video_current_ms - lastSaved > savingRate) &&
            frameDifference(prevFrame, frame, msToEnd, capstan, tape, args)) {
            // An Irregularity has been found!
            auto [odd_frame, even_frame] = frame.deinterlace();

            // Extract the image corresponding to the ROIs
            Point2f pts[4];
            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();

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

            string irregularityImageFilename = to_string(savedFrames) + "_" + safeTimeLabel + ".jpg";
            cv::imwrite(irregularityImagesPath / irregularityImageFilename, odd_frame);  

            // Append Irregularity information to JSON
            Irregularity irreg = Irregularity(Source::Video, timeLabel);
            irregularityFileOutput1["Irregularities"] += irreg.to_JSON();
            irregularityFileOutput2["Irregularities"] +=
                irreg.set_image_URI(irregularityImagesPath.string() + "/" + irregularityImageFilename).to_JSON();

            lastSaved = video_current_ms;
            savedFrames++;

        } else {
            unsavedFrames++;
        }
        prevFrame = frame;
    }
706
707
}

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
708
/**
Matteo's avatar
Matteo committed
709
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
710
711
712
713
714
715
716
717
 * @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
718
719
720
721
722
 * @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
723
724
725
726
 * @param argc Command line arguments count;
 * @param argv Command line arguments.
 * @return int program status.
 */
727
int main(int argc, char** argv) {
Matteo's avatar
Matteo committed
728
729
730
    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);
731

Matteo's avatar
Matteo committed
732
733
734
    json irregularityFileInput;
    fs::path irregularityFileInputPath;
    cv::Mat myFrame;
Matteo's avatar
update    
Matteo committed
735

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

    if (files::findFileName(VIDEO_PATH, fileName, extension) == -1) {
Matteo's avatar
Matteo committed
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
        cerr << RED << BOLD << "Input error!" << END << endl
             << RED << VIDEO_PATH.string() << " cannot be found or opened." << END << endl;
        std::exit(EXIT_FAILURE);
    }

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

    // 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);
    }
    if (args.speed != 7.5 && args.speed != 15) {
        cerr << RED << BOLD << "config.json error!" << END << endl
             << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl;
        std::exit(EXIT_FAILURE);
    }
    if (tape.threshold.percentual < 0 || tape.threshold.percentual > 100) {
        cerr << RED << BOLD << "config.json error!" << END << endl
             << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl;
        std::exit(EXIT_FAILURE);
    }
    if (capstan.threshold.percentual < 0 || capstan.threshold.percentual > 100) {
        cerr << RED << BOLD << "config.json error!" << END << endl
             << RED
             << "CapstanThresholdPercentual parameter must be a percentage "
                "value."
             << END << endl;
        std::exit(EXIT_FAILURE);
770
771
    }

Matteo's avatar
Matteo committed
772
773
774
775
776
777
778
    // 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
779

780
    cout << endl;
Matteo's avatar
Matteo committed
781
782
783
    cout << "Parameters:" << endl;
    cout << "    Brands: " << args.brands << endl;
    cout << "    Speed: " << args.speed << endl;
Matteo's avatar
update    
Matteo committed
784
    cout << "    ThresholdPercentual: " << tape.threshold.percentual << endl;
Matteo's avatar
Matteo committed
785
786
    cout << "    ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl;
    cout << endl;
787

Matteo's avatar
Matteo committed
788
789
    // Read input JSON
    iJSON >> irregularityFileInput;
790

Matteo's avatar
Matteo committed
791
792
793
    /*********************************************************************************************/
    /*********************************** MAKE OUTPUT DIRECTORY
    /*********************************************************************************************/
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
794

Matteo's avatar
Matteo committed
795
796
797
    // Make directory with fileName name
    outputPath = args.workingPath / "temp" / fileName;
    int outputFileNameDirectory = create_directory(outputPath);
Matteo's avatar
update    
Matteo committed
798

Matteo's avatar
Matteo committed
799
800
    irregularityImagesPath = outputPath / "IrregularityImages";
    int fullFrameDirectory = fs::create_directory(irregularityImagesPath);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
801

Matteo's avatar
Matteo committed
802
803
804
    /*********************************************************************************************/
    /************************************** AREAS DETECTION
    /*********************************************************************************************/
805

Matteo's avatar
Matteo committed
806
    cv::VideoCapture videoCapture(VIDEO_PATH);
807
    if (!videoCapture.isOpened()) {
Matteo's avatar
Matteo committed
808
809
        pprint("Video unreadable.", RED + BOLD);
        std::exit(EXIT_FAILURE);
810
811
    }

Matteo's avatar
Matteo committed
812
813
814
815
816
    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);
    // Get frame
    videoCapture >> myFrame;
817

Matteo's avatar
Matteo committed
818
    cout << "Video resolution: " << myFrame.cols << "x" << myFrame.rows << endl;
819

Matteo's avatar
Matteo committed
820
    bool found = findProcessingAreas(myFrame, tape, capstan);
821

Matteo's avatar
Matteo committed
822
823
    // Reset frame position
    videoCapture.set(CAP_PROP_POS_FRAMES, 0);
824

Matteo's avatar
Matteo committed
825
826
827
828
    if (!found) {
        pprint("Processing area not found. Try changing JSON parameters.", RED);
        std::exit(EXIT_FAILURE);
    }
829

Matteo's avatar
Matteo committed
830
831
832
    /*********************************************************************************************/
    /**************************************** PROCESSING
    /*********************************************************************************************/
833

Matteo's avatar
Matteo committed
834
    pprint("\nProcessing...", CYAN);
835

Matteo's avatar
Matteo committed
836
837
838
    // Processing timer
    time_t startTimer, endTimer;
    startTimer = time(NULL);
839

Matteo's avatar
Matteo committed
840
    processing(videoCapture, capstan, tape, args);
841

Matteo's avatar
Matteo committed
842
843
844
    endTimer = time(NULL);
    float min = (endTimer - startTimer) / 60;
    float sec = (endTimer - startTimer) % 60;
845

Matteo's avatar
Matteo committed
846
847
    string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec));
    cout << endl << result << endl;
848

Matteo's avatar
Matteo committed
849
850
851
    /*********************************************************************************************/
    /************************************* IRREGULARITY FILES
    /*********************************************************************************************/
852

Matteo's avatar
Matteo committed
853
854
    fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json";
    files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false);
855

Matteo's avatar
Matteo committed
856
857
    // Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier
    extractIrregularityImagesForAudio(outputPath, VIDEO_PATH, irregularityFileInput, irregularityFileOutput2);
858

Matteo's avatar
Matteo committed
859
860
    fs::path outputFile2Name = outputPath / "VideoAnalyser_IrregularityFileOutput2.json";
    files::saveFile(outputFile2Name, irregularityFileOutput2.dump(4), false);
861

862
863
    return 0;
}