main.cpp 21.8 KB
Newer Older
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
1
/**
Matteo's avatar
Matteo committed
2
 * @mainpage MPAI CAE-ARP Video Analyser
Matteo's avatar
Matteo committed
3
 * @file main.cpp
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
4
5
6
 *  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.2
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
27
28
 *	@status Production
 */
29
#include <stdlib.h>
30
31
#include <sys/timeb.h>

Matteo's avatar
Matteo committed
32
#include <boost/program_options.hpp>
Matteo's avatar
Matteo committed
33
34
35
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
Matteo's avatar
Matteo committed
36
37
38
39
#include <filesystem>
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
40
#include <opencv2/core/core.hpp>
Matteo's avatar
Matteo committed
41
#include <opencv2/highgui.hpp>
Matteo's avatar
Matteo committed
42
43
#include <variant>

Matteo's avatar
Matteo committed
44
#include "forAudioAnalyser.h"
Matteo's avatar
Matteo committed
45
46
47
48
49
50
51
52
53
#include "lib/Irregularity.hpp"
#include "lib/IrregularityFile.hpp"
#include "lib/colors.hpp"
#include "lib/core.hpp"
#include "lib/detection.hpp"
#include "lib/files.hpp"
#include "lib/io.hpp"
#include "lib/time.hpp"
#include "utility.hpp"
Matteo's avatar
Matteo committed
54
#define BLACK_PIXEL 0
Matteo's avatar
Matteo committed
55

56
using namespace std;
Matteo's avatar
Matteo committed
57
using namespace colors;
58
using json = nlohmann::json;
Matteo's avatar
Matteo committed
59
60
61
using videoanalyser::core::Frame;
using videoanalyser::io::pprint;
using videoanalyser::io::print_error_and_exit;
62
63
namespace fs = std::filesystem;
namespace po = boost::program_options;
Matteo's avatar
Matteo committed
64
namespace va = videoanalyser;
65

Matteo's avatar
Matteo committed
66
bool g_end_tape_saved = false;
Matteo's avatar
Matteo committed
67
bool g_first_brand = true;  // The first frame containing brands on tape must be saved
Matteo's avatar
Matteo committed
68
69
70
71
72
73
74
float g_first_instant = 0;
float g_mean_prev_frame_color = 0;  // Average frame color

static fs::path g_output_path{};
static fs::path g_irregularity_images_path{};
static json g_irregularity_file_1{};
static json g_irregularity_file_2{};
Matteo's avatar
Matteo committed
75

76
struct Args {
Matteo's avatar
Matteo committed
77
    fs::path
Matteo's avatar
Matteo committed
78
79
80
81
        working_path; /**< The working path where all input files are stored and where all output files will be saved */
    string files_name; /**< 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 */
Matteo's avatar
Matteo committed
82

Matteo's avatar
Matteo committed
83
    Args(fs::path working_path, string files_name, bool brands, float speed) {
Matteo's avatar
Matteo committed
84
        if (speed != 7.5 && speed != 15) throw invalid_argument("Speed must be 7.5 or 15");
Matteo's avatar
Matteo committed
85
86
        this->working_path = working_path;
        this->files_name = files_name;
Matteo's avatar
Matteo committed
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
        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
119
                std::cout << desc << "\n";
Matteo's avatar
Matteo committed
120
121
122
123
                std::exit(EXIT_SUCCESS);
            }
            po::notify(vm);
        } catch (po::invalid_command_line_syntax& e) {
Matteo's avatar
Matteo committed
124
            print_error_and_exit("Invalid command line syntax: " + string(e.what()));
Matteo's avatar
Matteo committed
125
        } catch (po::required_option& e) {
Matteo's avatar
Matteo committed
126
            print_error_and_exit("Missing required option: " + string(e.what()));
Matteo's avatar
Matteo committed
127
        } catch (nlohmann::detail::type_error e) {
Matteo's avatar
Matteo committed
128
            print_error_and_exit("config.json error: " + string(e.what()));
Matteo's avatar
Matteo committed
129
130
131
132
133
        }

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

Matteo's avatar
Matteo committed
136
/**
Matteo's avatar
update    
Matteo committed
137
138
 * @brief Get the next frame object.
 *
Matteo's avatar
Matteo committed
139
140
 * 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
141
 *
Matteo's avatar
Matteo committed
142
143
144
145
 * 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
146
 *
Matteo's avatar
Matteo committed
147
148
149
 * 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
150
 *
Matteo's avatar
Matteo committed
151
152
153
154
155
156
157
158
159
160
161
162
163
164
 * @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
165
float rotated_rect_area(RotatedRect rect) { return rect.size.width * rect.size.height; }
166

Matteo's avatar
Matteo committed
167
/**
Matteo's avatar
Matteo committed
168
 * @fn bool find_processing_areas(Frame frame, SceneObject tape, SceneObject capstan)
Matteo's avatar
Matteo committed
169
170
171
 * @brief Identifies the Regions Of Interest (ROIs) on the video,
 * which are:
 * - The reading head;
Matteo's avatar
Matteo committed
172
173
 * - The tape area under the tape head (computed on the basis of the detected
 * reading head);
Matteo's avatar
Matteo committed
174
 * - The capstan.
Matteo's avatar
Matteo committed
175
176
177
 *
 *
 *
Matteo's avatar
Matteo committed
178
179
180
 * @param frame the frame to be analysed
 * @param tape the tape object
 * @param capstan the capstan object
Matteo's avatar
Matteo committed
181
182
183
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
Matteo's avatar
Matteo committed
184
185
va::Result<pair<va::detection::Roi, va::detection::Roi>> find_processing_areas(Frame frame, SceneObject tape,
                                                                               SceneObject capstan) {
Matteo's avatar
Matteo committed
186
187
188
189
190
191
    va::detection::SceneElement tape_element{
        va::detection::ElementType::TAPE,
        tape.minDist,
        {tape.threshold.percentual, tape.threshold.angle, tape.threshold.scale, tape.threshold.pos}};

    auto tape_roi_result = va::detection::find_roi(frame, va::detection::Algorithm::GHT, tape_element);
Matteo's avatar
Matteo committed
192
193
194
    if (std::holds_alternative<va::Error>(tape_roi_result))
        return va::Error("Error while finding tape roi: " + std::get<va::Error>(tape_roi_result));
    auto rect_tape = std::get<va::detection::Roi>(tape_roi_result);
Matteo's avatar
Matteo committed
195
196
197
198
199
200

    va::detection::SceneElement capstan_element{
        va::detection::ElementType::CAPSTAN,
        capstan.minDist,
        {capstan.threshold.percentual, capstan.threshold.angle, capstan.threshold.scale, capstan.threshold.pos}};
    auto capstan_roi_result = va::detection::find_roi(frame, va::detection::Algorithm::SURF, capstan_element);
Matteo's avatar
Matteo committed
201
202
203
    if (std::holds_alternative<va::Error>(capstan_roi_result))
        return va::Error("Error while finding capstan roi: " + std::get<va::Error>(capstan_roi_result));
    auto rect_capstan = std::get<va::detection::Roi>(capstan_roi_result);
Matteo's avatar
Matteo committed
204

Matteo's avatar
Matteo committed
205
    // save detected areas to file
Matteo's avatar
Matteo committed
206
207
    cv::rectangle(frame, rect_tape.boundingRect(), cv::Scalar(0, 255, 0), 2);
    cv::rectangle(frame, rect_capstan.boundingRect(), cv::Scalar(255, 0, 0), 2);
Matteo's avatar
Matteo committed
208
    cv::imwrite(g_output_path.string() + "/my_tape_areas.jpg", frame);
Matteo's avatar
Matteo committed
209

Matteo's avatar
Matteo committed
210
    return make_pair(rect_tape, rect_capstan);
211
212
}

Matteo's avatar
Matteo committed
213
214
215
/**
 * @fn RotatedRect check_skew(RotatedRect roi)
 * @brief Check if the region of interest is skewed and correct it
Matteo's avatar
Matteo committed
216
 *
Matteo's avatar
Matteo committed
217
218
219
 * @param roi the region of interest
 * @return RotatedRect the corrected region of interest
 */
Matteo's avatar
Matteo committed
220
RotatedRect check_skew(RotatedRect roi) {
Matteo's avatar
Matteo committed
221
222
223
224
225
226
    // 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;
Matteo's avatar
Matteo committed
227
        std::swap(rect_size.width, rect_size.height);
Matteo's avatar
Matteo committed
228
229
    }
    return RotatedRect(roi.center, rect_size, angle);
Matteo's avatar
Matteo committed
230
231
232
}

/**
Matteo's avatar
Matteo committed
233
234
235
236
237
 * @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
238
239
240
241
242
243
 * @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
244
    cv::Mat rotation_matrix = cv::getRotationMatrix2D(roi.center, roi.angle, 1.0);
Matteo's avatar
Matteo committed
245
246
247
248

    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
249
}
250

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
251
/**
Matteo's avatar
Matteo committed
252
 * @fn bool is_frame_different(cv::Mat prev_frame, cv::Mat current_frame, int
Matteo's avatar
Matteo committed
253
254
 * ms_to_end, SceneObject capstan, SceneObject tape, Args args, va::detection::Roi rect_tape, va::detection::Roi
 * rect_capstan)
Matteo's avatar
Matteo committed
255
256
257
 * @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
258
 *
Matteo's avatar
Matteo committed
259
260
261
 * @param prev_frame the frame before the current one;
 * @param current_frame the current frame;
 * @param ms_to_end the number of milliseconds left before the end of the video.
Matteo's avatar
Matteo committed
262
 * Useful for capstan analysis.
Matteo's avatar
Matteo committed
263
264
265
266
267
 * @param capstan the capstan object;
 * @param tape the tape object;
 * @param args the command line arguments;
 * @param rect_tape the tape area under the tape head;
 * @param rect_capstan the capstan area.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
268
269
270
 * @return true if a potential Irregularity has been found;
 * @return false otherwise.
 */
Matteo's avatar
Matteo committed
271
bool is_frame_different(cv::Mat prev_frame, cv::Mat current_frame, int ms_to_end, SceneObject capstan, SceneObject tape,
Matteo's avatar
Matteo committed
272
                        Args args, va::detection::Roi rect_tape, va::detection::Roi rect_capstan) {
Matteo's avatar
Matteo committed
273
    bool result = false;
Matteo's avatar
Matteo committed
274
    int num_different_pixels = 0;
Matteo's avatar
Matteo committed
275
276
277
278
279

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

    // In the last minute of the video, check for pinchRoller position for
    // endTape event
Matteo's avatar
Matteo committed
280
    if (!g_end_tape_saved && ms_to_end < 60000) {
Matteo's avatar
Matteo committed
281
        RotatedRect corrected_capstan_roi = check_skew(rect_capstan);
Matteo's avatar
Matteo committed
282
        Frame difference_frame = get_difference_for_roi(Frame(prev_frame), Frame(current_frame), corrected_capstan_roi);
Matteo's avatar
Matteo committed
283
284
285

        for (int i = 0; i < difference_frame.rows; i++) {
            for (int j = 0; j < difference_frame.cols; j++) {
Matteo's avatar
Matteo committed
286
                if (difference_frame.at<cv::Vec3b>(i, j)[0] == BLACK_PIXEL) {
Matteo's avatar
Matteo committed
287
288
                    // There is a black pixel, then there is a difference
                    // between previous and current frames
Matteo's avatar
Matteo committed
289
                    num_different_pixels++;
Matteo's avatar
Matteo committed
290
291
292
293
                }
            }
        }

Matteo's avatar
Matteo committed
294
        float capstan_pixel_threshold = rotated_rect_area(rect_capstan) * capstan.threshold.percentual / 100;
Matteo's avatar
Matteo committed
295
296
        if (num_different_pixels > capstan_pixel_threshold) {
            g_end_tape_saved = true;  // Never check again for end tape instant
Matteo's avatar
Matteo committed
297
298
299
            return true;
        }
    }
300

Matteo's avatar
Matteo committed
301
    /********************* Tape analysis *********************/
302

Matteo's avatar
Matteo committed
303
    RotatedRect corrected_tape_roi = check_skew(rect_tape);
Matteo's avatar
Matteo committed
304
    Frame difference_frame = get_difference_for_roi(Frame(prev_frame), Frame(current_frame), corrected_tape_roi);
305

Matteo's avatar
Matteo committed
306
    /********************** Segment analysis ************************/
307

Matteo's avatar
Matteo committed
308
309
310
311
    Frame cropped_current_frame =
        Frame(current_frame)
            .warp(cv::getRotationMatrix2D(corrected_tape_roi.center, corrected_tape_roi.angle, 1.0))
            .crop(corrected_tape_roi.size, corrected_tape_roi.center);
Matteo's avatar
Matteo committed
312
313
314
    num_different_pixels = 0;
    float mean_current_frame_color;
    int current_frame_color_sum = 0;
Matteo's avatar
Matteo committed
315

Matteo's avatar
Matteo committed
316
317
318
319
320
    for (int i = 0; i < cropped_current_frame.rows; i++) {
        for (int j = 0; j < cropped_current_frame.cols; j++) {
            current_frame_color_sum += cropped_current_frame.at<cv::Vec3b>(i, j)[0] +
                                       cropped_current_frame.at<cv::Vec3b>(i, j)[1] +
                                       cropped_current_frame.at<cv::Vec3b>(i, j)[2];
Matteo's avatar
Matteo committed
321
            if (difference_frame.at<cv::Vec3b>(i, j)[0] == BLACK_PIXEL) {
Matteo's avatar
Matteo committed
322
                num_different_pixels++;
Matteo's avatar
Matteo committed
323
324
325
            }
        }
    }
Matteo's avatar
Matteo committed
326
    float tape_area_pixels_sq = rotated_rect_area(rect_tape);
Matteo's avatar
Matteo committed
327
    mean_current_frame_color = current_frame_color_sum / tape_area_pixels_sq;
Matteo's avatar
Matteo committed
328
329
330

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

Matteo's avatar
Matteo committed
331
332
    float tape_pixel_threshold = tape_area_pixels_sq * tape.threshold.percentual / 100;
    if (num_different_pixels > tape_pixel_threshold) {  // The threshold must be passed
Matteo's avatar
Matteo committed
333
334

        /***** AVERAGE_COLOR-BASED DECISION *****/
Matteo's avatar
Matteo committed
335
336
        if (g_mean_prev_frame_color > (mean_current_frame_color + 7) ||
            g_mean_prev_frame_color < (mean_current_frame_color - 7)) {  // They are not similar for color average
Matteo's avatar
Matteo committed
337
338
339
340
            result = true;
        }

        /***** BRANDS MANAGEMENT *****/
Matteo's avatar
Matteo committed
341
342
343
344
        // 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
Matteo's avatar
Matteo committed
345
346
        if (args.brands && g_first_brand && g_first_instant - ms_to_end > 5000) {
            g_first_brand = false;
Matteo's avatar
Matteo committed
347
            result = true;
Matteo's avatar
Matteo committed
348
349
        }
    }
350

Matteo's avatar
Matteo committed
351
    g_mean_prev_frame_color = mean_current_frame_color;
352

Matteo's avatar
Matteo committed
353
354
    return result;
}
355

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
356
/**
Matteo's avatar
Matteo committed
357
 * @fn void processing(cv::VideoCapture video_capture, SceneObject capstan,
Matteo's avatar
Matteo committed
358
 * SceneObject tape, Args args, va::detection::Roi rect_tape, va::detection::Roi rect_capstan)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
359
 * @brief video processing phase, where each frame is analysed.
Matteo's avatar
Matteo committed
360
361
362
 * It saves the IrregularityImages and updates the IrregularityFiles if an
 * Irregularity is found
 *
Matteo's avatar
update    
Matteo committed
363
 * @note To be able to work with the "old" neural network (by Ilenya),
Matteo's avatar
Matteo committed
364
365
366
367
368
369
 * 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
370
 *
Matteo's avatar
Matteo committed
371
 * @param video_capture the input Preservation Audio-Visual File;
Matteo's avatar
update    
Matteo committed
372
373
374
 * @param capstan the capstan SceneObject;
 * @param tape the tape SceneObject;
 * @param args the command line arguments.
Matteo's avatar
Matteo committed
375
376
 * @param rect_tape the tape Roi;
 * @param rect_capstan the capstan Roi.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
377
 */
Matteo's avatar
Matteo committed
378
379
void processing(cv::VideoCapture video_capture, SceneObject capstan, SceneObject tape, Args args,
                va::detection::Roi rect_tape, va::detection::Roi rect_capstan) {
Matteo's avatar
Matteo committed
380
381
382
383
    const int video_length_ms =
        ((float)video_capture.get(CAP_PROP_FRAME_COUNT) / video_capture.get(CAP_PROP_FPS)) * 1000;
    int video_current_ms = video_capture.get(CAP_PROP_POS_MSEC);
    int num_saved_frames = 0;
Matteo's avatar
update    
Matteo committed
384
    bool irregularity_found = false;
Matteo's avatar
Matteo committed
385
386

    // The first frame of the video won't be processed
Matteo's avatar
Matteo committed
387
388
    cv::Mat prev_frame = get_next_frame(video_capture, args.speed, irregularity_found);
    g_first_instant = video_length_ms - video_current_ms;
389

Matteo's avatar
Matteo committed
390
391
392
    while (video_capture.isOpened()) {
        Frame frame = get_next_frame(video_capture, args.speed, irregularity_found);
        video_current_ms = video_capture.get(CAP_PROP_POS_MSEC);
Matteo's avatar
Matteo committed
393
394

        if (frame.empty()) {
Matteo's avatar
Matteo committed
395
            std::cout << endl << "Empty frame!" << endl;
Matteo's avatar
Matteo committed
396
            video_capture.release();
Matteo's avatar
Matteo committed
397
398
399
            return;
        }

Matteo's avatar
Matteo committed
400
        int ms_to_end = video_length_ms - video_current_ms;
Matteo's avatar
Matteo committed
401
402
403
404
405
        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
Matteo's avatar
Matteo committed
406
407
408
        int sec_to_end = ms_to_end / 1000;
        int min_to_end = (sec_to_end / 60) % 60;
        sec_to_end = sec_to_end % 60;
Matteo's avatar
Matteo committed
409
410
        string sec_str_to_end = (sec_to_end < 10 ? "0" : "") + to_string(sec_to_end);
        string min_str_to_end = (min_to_end < 10 ? "0" : "") + to_string(min_to_end);
Matteo's avatar
Matteo committed
411

Matteo's avatar
Matteo committed
412
        std::cout << "\rIrregularities: " << num_saved_frames << ".   ";
Matteo's avatar
Matteo committed
413
        std::cout << "Remaining video time [mm:ss]: " << min_str_to_end << ":" << sec_str_to_end << flush;
Matteo's avatar
Matteo committed
414

Matteo's avatar
Matteo committed
415
416
        irregularity_found =
            is_frame_different(prev_frame, frame, ms_to_end, capstan, tape, args, rect_tape, rect_capstan);
Matteo's avatar
update    
Matteo committed
417
        if (irregularity_found) {
Matteo's avatar
Matteo committed
418
            auto [odd_frame, _] = frame.deinterlace();
Matteo's avatar
Matteo committed
419

Matteo's avatar
update    
Matteo committed
420
            string irregularityImageFilename =
Matteo's avatar
Matteo committed
421
422
                to_string(num_saved_frames) + "_" + getTimeLabel(video_current_ms, "-") + ".jpg";
            cv::imwrite(g_irregularity_images_path / irregularityImageFilename, odd_frame);
Matteo's avatar
Matteo committed
423
424

            // Append Irregularity information to JSON
Matteo's avatar
update    
Matteo committed
425
            Irregularity irreg = Irregularity(Source::Video, getTimeLabel(video_current_ms, ":"));
Matteo's avatar
Matteo committed
426
427
428
            g_irregularity_file_1["Irregularities"] += irreg.to_JSON();
            g_irregularity_file_2["Irregularities"] +=
                irreg.set_image_URI(g_irregularity_images_path.string() + "/" + irregularityImageFilename).to_JSON();
Matteo's avatar
Matteo committed
429

Matteo's avatar
Matteo committed
430
            num_saved_frames++;
Matteo's avatar
Matteo committed
431
        }
Matteo's avatar
Matteo committed
432
        prev_frame = frame;
Matteo's avatar
Matteo committed
433
    }
434
435
}

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
436
/**
Matteo's avatar
Matteo committed
437
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
438
439
440
441
442
443
444
445
 * @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
446
447
448
449
450
 * @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
451
452
453
454
 * @param argc Command line arguments count;
 * @param argv Command line arguments.
 * @return int program status.
 */
455
int main(int argc, char** argv) {
Matteo's avatar
Matteo committed
456
457
458
459
    const string CONFIG_FILE = "config/config.json";
    const string A_IRREG_FILE_1 = "AudioAnalyser_IrregularityFileOutput1.json";
    const string V_IRREG_FILE_1 = "VideoAnalyser_IrregularityFileOutput1.json";
    const string V_IRREG_FILE_2 = "VideoAnalyser_IrregularityFileOutput2.json";
Matteo's avatar
Matteo committed
460
461

    Args args = argc > 1 ? Args::from_cli(argc, argv) : Args::from_file(CONFIG_FILE);
Matteo's avatar
Matteo committed
462
463
    SceneObject capstan = SceneObject::from_file(CONFIG_FILE, ROI::CAPSTAN);
    SceneObject tape = SceneObject::from_file(CONFIG_FILE, ROI::TAPE);
Matteo's avatar
Matteo committed
464

Matteo's avatar
Matteo committed
465
    const fs::path VIDEO_PATH = args.working_path / "PreservationAudioVisualFile" / args.files_name;
Matteo's avatar
Matteo committed
466
    const auto [FILE_NAME, FILE_FORMAT] = files::get_filename_and_extension(VIDEO_PATH);
Matteo's avatar
Matteo committed
467
    const fs::path AUDIO_IRR_FILE_PATH = args.working_path / "temp" / FILE_NAME / A_IRREG_FILE_1;
Matteo's avatar
Matteo committed
468

Matteo's avatar
Matteo committed
469
    std::cout << "Video to be analysed: " << endl;
Matteo's avatar
Matteo committed
470
471
    std::cout << "\tFile name:  " << FILE_NAME << endl;
    std::cout << "\tExtension:  " << FILE_FORMAT << endl;
Matteo's avatar
Matteo committed
472

Matteo's avatar
Matteo committed
473
    if (FILE_FORMAT.compare("avi") != 0 && FILE_FORMAT.compare("mp4") != 0 && FILE_FORMAT.compare("mov") != 0)
Matteo's avatar
Matteo committed
474
        print_error_and_exit("Input error: The input file must be an AVI, MP4 or MOV file.");
Matteo's avatar
Matteo committed
475
    ifstream iJSON(AUDIO_IRR_FILE_PATH);
Matteo's avatar
Matteo committed
476
    if (iJSON.fail())
Matteo's avatar
Matteo committed
477
        print_error_and_exit("config.json error" + AUDIO_IRR_FILE_PATH.string() + " cannot be found or opened.");
Matteo's avatar
Matteo committed
478

Matteo's avatar
Matteo committed
479
480
    json audio_irr_file;
    iJSON >> audio_irr_file;
481

Matteo's avatar
Matteo committed
482
    // Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
Matteo committed
483
484
485
    if (args.speed == 15) {
        tape.threshold.percentual += args.brands ? 6 : 20;
    } else if (!args.brands)
Matteo's avatar
Matteo committed
486
        tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
487

Matteo's avatar
Matteo committed
488
    g_output_path = args.working_path / "temp" / FILE_NAME;
Matteo's avatar
Matteo committed
489
    fs::create_directory(g_output_path);
Matteo's avatar
update    
Matteo committed
490

Matteo's avatar
Matteo committed
491
492
    g_irregularity_images_path = g_output_path / "IrregularityImages";
    fs::create_directory(g_irregularity_images_path);
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
493

Matteo's avatar
Matteo committed
494
    cv::VideoCapture video_capture(VIDEO_PATH);  // Open video file
Matteo's avatar
Matteo committed
495
    if (!video_capture.isOpened()) print_error_and_exit("Video error: Video file cannot be opened.");
496

Matteo's avatar
Matteo committed
497
498
499
500
    video_capture.set(CAP_PROP_POS_FRAMES,
                      video_capture.get(CAP_PROP_FRAME_COUNT) / 2);  // Set frame position to half video length
    cv::Mat middle_frame = get_next_frame(video_capture, args.speed);
    video_capture.set(CAP_PROP_POS_FRAMES, 0);  // Reset frame position
501

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

Matteo's avatar
Matteo committed
504
505
506
507
    auto processing_areas = find_processing_areas(middle_frame, tape, capstan);
    if (std::holds_alternative<va::Error>(processing_areas))
        print_error_and_exit("Processing area not found: Try changing JSON parameters.");
    auto [rect_tape, rect_capstan] = std::get<pair<va::detection::Roi, va::detection::Roi>>(processing_areas);
508

Matteo's avatar
Matteo committed
509
    pprint("Processing...", CYAN);
Matteo's avatar
Matteo committed
510
    processing(video_capture, capstan, tape, args, rect_tape, rect_capstan);
511

Matteo's avatar
Matteo committed
512
    files::save_file(g_output_path / V_IRREG_FILE_1, g_irregularity_file_1.dump(4));
513

Matteo's avatar
Matteo committed
514
    // Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier
Matteo's avatar
Matteo committed
515
    extract_irregularity_images_for_audio(g_output_path, VIDEO_PATH, audio_irr_file, g_irregularity_file_2);
Matteo's avatar
Matteo committed
516
    files::save_file(g_output_path / V_IRREG_FILE_2, g_irregularity_file_2.dump(4));
517

Matteo's avatar
Matteo committed
518
    return EXIT_SUCCESS;
519
}