main.cpp 20.7 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
44
45
46
47
48
49
#include <variant>

#include "lib/Irregularity.hpp"
#include "lib/IrregularityFile.hpp"
#include "lib/colors.hpp"
#include "lib/core.hpp"
#include "lib/detection.hpp"
#include "lib/files.hpp"
Matteo's avatar
Matteo committed
50
#include "forAudioAnalyser.h"
Matteo's avatar
Matteo committed
51
52
53
54
#include "lib/io.hpp"
#include "lib/time.hpp"
#include "utility.hpp"

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

Matteo's avatar
Matteo committed
65
bool g_end_tape_saved = false;
Matteo's avatar
Matteo committed
66
bool g_first_brand = true;  // The first frame containing brands on tape must be saved
Matteo's avatar
Matteo committed
67
68
69
70
71
72
73
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{};
74
// RotatedRect identifying the processing area
Matteo's avatar
Matteo committed
75
RotatedRect g_rect_tape, g_rect_capstan;
Matteo's avatar
Matteo committed
76

77
struct Args {
Matteo's avatar
Matteo committed
78
    fs::path
Matteo's avatar
Matteo committed
79
80
81
82
        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
83

Matteo's avatar
Matteo committed
84
    Args(fs::path working_path, string files_name, bool brands, float speed) {
Matteo's avatar
Matteo committed
85
        if (speed != 7.5 && speed != 15) throw invalid_argument("Speed must be 7.5 or 15");
Matteo's avatar
Matteo committed
86
87
        this->working_path = working_path;
        this->files_name = files_name;
Matteo's avatar
Matteo committed
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
119
        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
120
                std::cout << desc << "\n";
Matteo's avatar
Matteo committed
121
122
123
124
                std::exit(EXIT_SUCCESS);
            }
            po::notify(vm);
        } catch (po::invalid_command_line_syntax& e) {
Matteo's avatar
Matteo committed
125
            print_error_and_exit("Invalid command line syntax: " + string(e.what()));
Matteo's avatar
Matteo committed
126
        } catch (po::required_option& e) {
Matteo's avatar
Matteo committed
127
            print_error_and_exit("Missing required option: " + string(e.what()));
Matteo's avatar
Matteo committed
128
        } catch (nlohmann::detail::type_error e) {
Matteo's avatar
Matteo committed
129
            print_error_and_exit("config.json error: " + string(e.what()));
Matteo's avatar
Matteo committed
130
131
132
133
134
        }

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

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

Matteo's avatar
Matteo committed
168
/**
Matteo's avatar
Matteo committed
169
 * @fn bool find_processing_areas(Mat my_frame)
Matteo's avatar
Matteo committed
170
171
172
 * @brief Identifies the Regions Of Interest (ROIs) on the video,
 * which are:
 * - The reading head;
Matteo's avatar
Matteo committed
173
174
 * - The tape area under the tape head (computed on the basis of the detected
 * reading head);
Matteo's avatar
Matteo committed
175
 * - The capstan.
Matteo's avatar
Matteo committed
176
 * @param my_frame The current frame of the video.
Matteo's avatar
Matteo committed
177
178
179
 * @return true if some areas have been detected;
 * @return false otherwise.
 */
Matteo's avatar
Matteo committed
180
181
182
183
184
185
186
187
188
bool find_processing_areas(Frame frame, SceneObject tape, SceneObject capstan) {
    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);
    if (std::holds_alternative<va::Error>(tape_roi_result)) {
        pprint(std::get<va::Error>(tape_roi_result), RED);
Matteo's avatar
Matteo committed
189
190
        return false;
    }
Matteo's avatar
Matteo committed
191
    g_rect_tape = std::get<va::detection::Roi>(tape_roi_result);
Matteo's avatar
Matteo committed
192

Matteo's avatar
Matteo committed
193
194
195
196
197
198
199
200
201
202
    cv::rectangle(frame, g_rect_tape.boundingRect(), cv::Scalar(0, 255, 0), 2);

    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);
    if (std::holds_alternative<va::Error>(capstan_roi_result)) {
        pprint(std::get<va::Error>(capstan_roi_result), RED);
        return false;
Matteo's avatar
Matteo committed
203
    }
Matteo's avatar
Matteo committed
204
    g_rect_capstan = std::get<va::detection::Roi>(capstan_roi_result);
Matteo's avatar
Matteo committed
205

Matteo's avatar
Matteo committed
206
207
    cv::rectangle(frame, g_rect_capstan.boundingRect(), cv::Scalar(255, 0, 0), 2);
    cv::imwrite(g_output_path.string() + "/my_tape_areas.jpg", frame);
Matteo's avatar
Matteo committed
208
209

    return true;
210
211
}

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

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

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

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
250
/**
Matteo's avatar
Matteo committed
251
252
 * @fn bool is_frame_different(cv::Mat prev_frame, cv::Mat current_frame, int
 * ms_to_end)
Matteo's avatar
Matteo committed
253
254
255
 * @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
256
 *
Matteo's avatar
Matteo committed
257
258
259
 * @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
260
 * Useful for capstan analysis.
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
261
262
263
 * @return true if a potential Irregularity has been found;
 * @return false otherwise.
 */
Matteo's avatar
Matteo committed
264
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
265
                        Args args) {
Matteo's avatar
Matteo committed
266
    bool result = false;
Matteo's avatar
Matteo committed
267
    int num_different_pixels = 0;
Matteo's avatar
Matteo committed
268
269
270
271
272

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

    // In the last minute of the video, check for pinchRoller position for
    // endTape event
Matteo's avatar
Matteo committed
273
274
275
    if (!g_end_tape_saved && ms_to_end < 60000) {
        RotatedRect corrected_capstan_roi = check_skew(g_rect_capstan);
        Frame difference_frame = get_difference_for_roi(Frame(prev_frame), Frame(current_frame), corrected_capstan_roi);
Matteo's avatar
Matteo committed
276
277
278
279
280
281

        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
Matteo's avatar
Matteo committed
282
                    num_different_pixels++;
Matteo's avatar
Matteo committed
283
284
285
286
                }
            }
        }

Matteo's avatar
Matteo committed
287
288
289
        float capstan_pixel_threshold = rotated_rect_area(g_rect_capstan) * capstan.threshold.percentual / 100;
        if (num_different_pixels > capstan_pixel_threshold) {
            g_end_tape_saved = true;  // Never check again for end tape instant
Matteo's avatar
Matteo committed
290
291
292
            return true;
        }
    }
293

Matteo's avatar
Matteo committed
294
    /********************* Tape analysis *********************/
295

Matteo's avatar
Matteo committed
296
297
298
    RotatedRect corrected_tape_roi = check_skew(g_rect_tape);
    Frame cropped_current_frame =
        Frame(current_frame)
Matteo's avatar
Matteo committed
299
            .warp(cv::getRotationMatrix2D(corrected_tape_roi.center, corrected_tape_roi.angle, 1.0))
Matteo's avatar
Matteo committed
300
301
            .crop(corrected_tape_roi.size, corrected_tape_roi.center);
    Frame difference_frame = get_difference_for_roi(Frame(prev_frame), Frame(current_frame), corrected_tape_roi);
302

Matteo's avatar
Matteo committed
303
    /********************** Segment analysis ************************/
304

Matteo's avatar
Matteo committed
305
306
307
    num_different_pixels = 0;
    float mean_current_frame_color;
    int current_frame_color_sum = 0;
Matteo's avatar
Matteo committed
308

Matteo's avatar
Matteo committed
309
310
311
312
313
    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
314
            if (difference_frame.at<cv::Vec3b>(i, j)[0] == 0) {
Matteo's avatar
Matteo committed
315
                num_different_pixels++;
Matteo's avatar
Matteo committed
316
317
318
            }
        }
    }
Matteo's avatar
Matteo committed
319
320
    float tape_area_pixels_sq = rotated_rect_area(g_rect_tape);
    mean_current_frame_color = current_frame_color_sum / tape_area_pixels_sq;
Matteo's avatar
Matteo committed
321
322
323

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

Matteo's avatar
Matteo committed
324
325
    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
326
327

        /***** AVERAGE_COLOR-BASED DECISION *****/
Matteo's avatar
Matteo committed
328
329
        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
330
331
332
333
            result = true;
        }

        /***** BRANDS MANAGEMENT *****/
Matteo's avatar
Matteo committed
334
335
336
337
        // 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
338
339
        if (args.brands && g_first_brand && g_first_instant - ms_to_end > 5000) {
            g_first_brand = false;
Matteo's avatar
Matteo committed
340
            result = true;
Matteo's avatar
Matteo committed
341
342
        }
    }
343

Matteo's avatar
Matteo committed
344
    g_mean_prev_frame_color = mean_current_frame_color;
345

Matteo's avatar
Matteo committed
346
347
    return result;
}
348

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
349
/**
Matteo's avatar
Matteo committed
350
 * @fn void processing(cv::VideoCapture video_capture, SceneObject capstan,
Matteo's avatar
Matteo committed
351
 * SceneObject tape, Args args)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
352
 * @brief video processing phase, where each frame is analysed.
Matteo's avatar
Matteo committed
353
354
355
 * It saves the IrregularityImages and updates the IrregularityFiles if an
 * Irregularity is found
 *
Matteo's avatar
update    
Matteo committed
356
 * @note To be able to work with the "old" neural network (by Ilenya),
Matteo's avatar
Matteo committed
357
358
359
360
361
362
 * 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
363
 *
Matteo's avatar
Matteo committed
364
 * @param video_capture the input Preservation Audio-Visual File;
Matteo's avatar
update    
Matteo committed
365
366
367
 * @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
368
 */
Matteo's avatar
Matteo committed
369
370
371
372
373
void processing(cv::VideoCapture video_capture, SceneObject capstan, SceneObject tape, Args args) {
    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
374
    bool irregularity_found = false;
Matteo's avatar
Matteo committed
375
376

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

Matteo's avatar
Matteo committed
380
381
382
    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
383
384

        if (frame.empty()) {
Matteo's avatar
Matteo committed
385
            std::cout << endl << "Empty frame!" << endl;
Matteo's avatar
Matteo committed
386
            video_capture.release();
Matteo's avatar
Matteo committed
387
388
389
            return;
        }

Matteo's avatar
Matteo committed
390
        int ms_to_end = video_length_ms - video_current_ms;
Matteo's avatar
Matteo committed
391
392
393
394
395
        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
396
397
398
        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
399
400
        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
401

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

Matteo's avatar
Matteo committed
405
        irregularity_found = is_frame_different(prev_frame, frame, ms_to_end, capstan, tape, args);
Matteo's avatar
update    
Matteo committed
406
        if (irregularity_found) {
Matteo's avatar
Matteo committed
407
            auto [odd_frame, _] = frame.deinterlace();
Matteo's avatar
Matteo committed
408

Matteo's avatar
update    
Matteo committed
409
            string irregularityImageFilename =
Matteo's avatar
Matteo committed
410
411
                to_string(num_saved_frames) + "_" + getTimeLabel(video_current_ms, "-") + ".jpg";
            cv::imwrite(g_irregularity_images_path / irregularityImageFilename, odd_frame);
Matteo's avatar
Matteo committed
412
413

            // Append Irregularity information to JSON
Matteo's avatar
update    
Matteo committed
414
            Irregularity irreg = Irregularity(Source::Video, getTimeLabel(video_current_ms, ":"));
Matteo's avatar
Matteo committed
415
416
417
            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
418

Matteo's avatar
Matteo committed
419
            num_saved_frames++;
Matteo's avatar
Matteo committed
420
        }
Matteo's avatar
Matteo committed
421
        prev_frame = frame;
Matteo's avatar
Matteo committed
422
    }
423
424
}

Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
425
/**
Matteo's avatar
Matteo committed
426
 * @fn int main(int argc, char** argv)
Nadir Dalla Pozza's avatar
Nadir Dalla Pozza committed
427
428
429
430
431
432
433
434
 * @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
435
436
437
438
439
 * @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
440
441
442
443
 * @param argc Command line arguments count;
 * @param argv Command line arguments.
 * @return int program status.
 */
444
int main(int argc, char** argv) {
Matteo's avatar
Matteo committed
445
446
447
448
    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
449
450
    SceneObject capstan = SceneObject::from_file(CONFIG_FILE, ROI::CAPSTAN);
    SceneObject tape = SceneObject::from_file(CONFIG_FILE, ROI::TAPE);
Matteo's avatar
Matteo committed
451
    Args args = argc > 1 ? Args::from_cli(argc, argv) : Args::from_file(CONFIG_FILE);
Matteo's avatar
Matteo committed
452
    const fs::path VIDEO_PATH = args.working_path / "PreservationAudioVisualFile" / args.files_name;
Matteo's avatar
Matteo committed
453
    const auto [FILE_NAME, FILE_FORMAT] = files::get_filename_and_extension(VIDEO_PATH);
Matteo's avatar
Matteo committed
454
    const fs::path AUDIO_IRR_FILE_PATH = args.working_path / "temp" / FILE_NAME / A_IRREG_FILE_1;
Matteo's avatar
Matteo committed
455

Matteo's avatar
Matteo committed
456
    std::cout << "Video to be analysed: " << endl;
Matteo's avatar
Matteo committed
457
458
    std::cout << "\tFile name:  " << FILE_NAME << endl;
    std::cout << "\tExtension:  " << FILE_FORMAT << endl;
Matteo's avatar
Matteo committed
459

Matteo's avatar
Matteo committed
460
    if (FILE_FORMAT.compare("avi") != 0 && FILE_FORMAT.compare("mp4") != 0 && FILE_FORMAT.compare("mov") != 0)
Matteo's avatar
Matteo committed
461
        print_error_and_exit("Input error: The input file must be an AVI, MP4 or MOV file.");
Matteo's avatar
Matteo committed
462
    ifstream iJSON(AUDIO_IRR_FILE_PATH);
Matteo's avatar
Matteo committed
463
    if (iJSON.fail())
Matteo's avatar
Matteo committed
464
        print_error_and_exit("config.json error" + AUDIO_IRR_FILE_PATH.string() + " cannot be found or opened.");
Matteo's avatar
Matteo committed
465

Matteo's avatar
Matteo committed
466
467
    json audio_irr_file;
    iJSON >> audio_irr_file;
468

Matteo's avatar
Matteo committed
469
    // Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5)
Matteo's avatar
Matteo committed
470
471
472
    if (args.speed == 15) {
        tape.threshold.percentual += args.brands ? 6 : 20;
    } else if (!args.brands)
Matteo's avatar
Matteo committed
473
        tape.threshold.percentual += 21;
Nadir Dalla Pozza's avatar
Update.    
Nadir Dalla Pozza committed
474

Matteo's avatar
Matteo committed
475
    g_output_path = args.working_path / "temp" / FILE_NAME;
Matteo's avatar
Matteo committed
476
    fs::create_directory(g_output_path);
Matteo's avatar
update    
Matteo committed
477

Matteo's avatar
Matteo committed
478
479
    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
480

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

Matteo's avatar
Matteo committed
484
485
486
487
    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
488

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

Matteo's avatar
Matteo committed
491
    bool found = find_processing_areas(middle_frame, tape, capstan);
Matteo's avatar
Matteo committed
492
    if (!found) print_error_and_exit("Processing area not found: Try changing JSON parameters.");
493

Matteo's avatar
Matteo committed
494
    pprint("Processing...", CYAN);
Matteo's avatar
Matteo committed
495
    processing(video_capture, capstan, tape, args);
496

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

Matteo's avatar
Matteo committed
499
    // Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier
Matteo's avatar
Matteo committed
500
    extract_irregularity_images_for_audio(g_output_path, VIDEO_PATH, audio_irr_file, g_irregularity_file_2);
Matteo's avatar
Matteo committed
501
    files::save_file(g_output_path / V_IRREG_FILE_2, g_irregularity_file_2.dump(4));
502

Matteo's avatar
Matteo committed
503
    return EXIT_SUCCESS;
504
}