/** * @mainpage MPAI CAE-ARP Video Analyser * @file main.cpp * MPAI CAE-ARP Video Analyser. * * Implements MPAI CAE-ARP Video Analyser Technical Specification. * It identifies Irregularities on the Preservation Audio-Visual File, providing: * - Irregularity Files; * - Irregularity Images. * * WARNING: * Currently, this program is only compatible with the Studer A810 and videos recorded in PAL standard. * * @author Nadir Dalla Pozza * @author Matteo Spanio * @copyright 2023, Audio Innova S.r.l. * @credits Niccolò Pretto, Nadir Dalla Pozza, Sergio Canazza * @license GPL v3.0 * @version 1.1.1 * @status Production */ #include #include #include #include #include #include #include #include // uuid class #include // generators #include // streaming operators etc. #include #include #include #include #include #include #include "opencv2/core.hpp" #include "opencv2/calib3d.hpp" #include "opencv2/highgui.hpp" #include "opencv2/imgproc.hpp" #include "opencv2/features2d.hpp" #include "opencv2/xfeatures2d.hpp" #include "utility.h" #include "forAudioAnalyser.h" #include "lib/files.h" #include "lib/colors.h" #include "lib/time.h" #include "lib/Irregularity.h" #include "lib/IrregularityFile.h" using namespace cv; using namespace std; using utility::Frame; using json = nlohmann::json; namespace fs = std::filesystem; namespace po = boost::program_options; // For capstan detection, there are two alternative approaches: // Generalized Hough Transform and SURF. bool useSURF = true; bool savingPinchRoller = false; bool pinchRollerRect = false; bool savingBrand = false; bool endTapeSaved = false; float mediaPrevFrame = 0; bool firstBrand = true; // The first frame containing brands on tape must be saved float firstInstant = 0; string fileName, extension; // Path variables static fs::path outputPath {}; static fs::path irregularityImagesPath {}; // JSON files static json configurationFile {}; static json irregularityFileOutput1 {}; static json irregularityFileOutput2 {}; // RotatedRect identifying the processing area RotatedRect rect, rectTape, rectCapstan; /** * @fn void pprint(string text, string color) * @brief Prints a text in a given color. * * @param text * @param color */ void pprint(string text, string color) { cout << color << text << END << endl; } struct Args { fs::path workingPath; string filesName; bool brands; float speed; 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()->required(), "Specify the Working Path, where all input files are stored") ("files-name,f", po::value()->required(), "Specify the name of the Preservation files (without extension)") ("brands,b", po::value()->required(), "Specify if the tape presents brands on its surface") ("speed,s", po::value()->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()), vm["files-name"].as(), vm["brands"].as(), vm["speed"].as() ); } }; // 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"; double rotatedRectArea(RotatedRect rect) { return rect.size.width * rect.size.height; } /** * @fn std::tuple, vector> 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, vector> a tuple containing the best matches for positive and negative angles */ std::tuple, vector> 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 alg = createGeneralizedHoughGuil(); vector positionsPos, positionsNeg; Mat votesPos, votesNeg; double maxValPos = 0, maxValNeg = 0; int indexPos = 0, indexNeg = 0; alg->setMinDist(object.minDist); alg->setLevels(360); alg->setDp(2); alg->setMaxBufferSize(1000); alg->setAngleStep(1); alg->setAngleThresh(object.threshold.angle); alg->setMinScale(0.9); alg->setMaxScale(1.1); alg->setScaleStep(0.01); alg->setScaleThresh(object.threshold.scale); alg->setPosThresh(object.threshold.pos); alg->setCannyLowThresh(150); // Old: 100 alg->setCannyHighThresh(240); // Old: 300 alg->setTemplate(model); utility::detectShape(alg, model, object.threshold.pos, positionsPos, votesPos, positionsNeg, votesNeg, processing_area); for (int i = 0; i < votesPos.size().width; i++) { if (votesPos.at(i) >= maxValPos) { maxValPos = votesPos.at(i); indexPos = i; } } for (int i = 0; i < votesNeg.size().width; i++) { if (votesNeg.at(i) >= maxValNeg) { maxValNeg = votesNeg.at(i); indexNeg = i; } } return { indexPos, indexNeg, maxValPos, maxValNeg, positionsPos, positionsNeg }; } /** * @fn bool findProcessingAreas(Mat myFrame) * @brief Identifies the Regions Of Interest (ROIs) on the video, * which are: * - The reading head; * - The tape area under the tape head (computed on the basis of the detected reading head); * - The capstan. * @param myFrame The current frame of the video. * @return true if some areas have been detected; * @return false otherwise. */ bool findProcessingAreas(Mat myFrame, SceneObject tape, SceneObject capstan) { /*********************************************************************************************/ /*********************************** 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; } /*********************************************************************************************/ /************************************ 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 detector = xfeatures2d::SURF::create(minHessian); vector 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 matcher = DescriptorMatcher::create(DescriptorMatcher::FLANNBASED); vector> 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 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(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS); // Localize the object vector obj; vector 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 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 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; } /** * @fn RotatedRect check_skew(RotatedRect roi) * @brief Check if the region of interest is skewed and correct it * * @param roi the region of interest * @return RotatedRect the corrected region of interest */ RotatedRect check_skew(RotatedRect roi) { // get angle and size from the bounding box // thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/ cv::Size rect_size = roi.size; float angle = roi.angle; if (roi.angle < -45.) { angle += 90.0; swap(rect_size.width, rect_size.height); } return RotatedRect(roi.center, rect_size, angle); } /** * @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 * * @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) { 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)); } /** * @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. * * @param prevFrame the frame before the current one; * @param currentFrame the current frame; * @param msToEnd the number of milliseconds left before the end of the video. Useful for capstan analysis. * @return true if a potential Irregularity has been found; * @return false otherwise. */ 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(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 = false; // It 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 /************************************ Tape analysis ******************************************/ // Tape area int tapeAreaPixels = rotatedRectArea(rectTape); float tapeDifferentPixelsThreshold = tapeAreaPixels * tape.threshold.percentual / 100; RotatedRect corrected_tape_roi = check_skew(rectTape); Frame croppedCurrentFrame = Frame(currentFrame) .warp(getRotationMatrix2D(corrected_tape_roi.center, corrected_tape_roi.angle, 1.0)) .crop(corrected_tape_roi.size, corrected_tape_roi.center); Frame difference_frame = get_difference_for_roi(Frame(prevFrame), Frame(currentFrame), corrected_tape_roi); int decEnd = (msToEnd % 1000) / 100; int secEnd = (msToEnd - (msToEnd % 1000)) / 1000; int minEnd = secEnd / 60; secEnd = secEnd % 60; /************************************* Segment analysis **************************************/ int blackPixels = 0; float mediaCurrFrame; int totColoreCF = 0; for (int i = 0; i < croppedCurrentFrame.rows; i++) { for (int j = 0; j < croppedCurrentFrame.cols; j++) { totColoreCF += croppedCurrentFrame.at(i, j)[0] + croppedCurrentFrame.at(i, j)[1] + croppedCurrentFrame.at(i, j)[2]; if (difference_frame.at(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; } } // Update mediaPrevFrame mediaPrevFrame = mediaCurrFrame; return result; } /** * @fn void processing(cv::VideoCapture videoCapture) * @brief video processing phase, where each frame is analysed. * It saves the IrregularityImages and updates the IrregularityFiles if an Irregularity is found * * @param videoCapture the input Preservation Audio-Visual File; */ void processing(cv::VideoCapture videoCapture, SceneObject capstan, SceneObject tape, Args args) { 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 int savedFrames = 0; 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 cv::Mat prevFrame; videoCapture >> prevFrame; firstInstant = video_length_ms - video_current_ms; while (videoCapture.isOpened()) { Frame frame; videoCapture >> frame; 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(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); // FIXME: should it be frame? or maybe is only odd for the classification step? // 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; } } /** * @fn int main(int argc, char** argv) * @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. * * @param argc Command line arguments count; * @param argv Command line arguments. * @return int program status. */ int main(int argc, char** argv) { 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); json irregularityFileInput; fs::path irregularityFileInputPath; cv::Mat myFrame; const fs::path VIDEO_PATH = args.workingPath / "PreservationAudioVisualFile" / args.filesName; if (files::findFileName(VIDEO_PATH, fileName, extension) == -1) { 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); } // 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; cout << endl; cout << "Parameters:" << endl; cout << " Brands: " << args.brands << endl; cout << " Speed: " << args.speed << endl; cout << " ThresholdPercentual: " << tape.threshold.percentual << endl; cout << " ThresholdPercentualCapstan: " << capstan.threshold.percentual << endl; cout << endl; // Read input JSON iJSON >> irregularityFileInput; /*********************************************************************************************/ /*********************************** MAKE OUTPUT DIRECTORY ***********************************/ /*********************************************************************************************/ // Make directory with fileName name outputPath = args.workingPath / "temp" / fileName; int outputFileNameDirectory = create_directory(outputPath); irregularityImagesPath = outputPath / "IrregularityImages"; int fullFrameDirectory = fs::create_directory(irregularityImagesPath); /*********************************************************************************************/ /************************************** AREAS DETECTION **************************************/ /*********************************************************************************************/ cv::VideoCapture videoCapture(VIDEO_PATH); if (!videoCapture.isOpened()) { pprint("Video unreadable.", RED + BOLD); std::exit(EXIT_FAILURE); } 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; cout << "Video resolution: " << myFrame.cols << "x" << myFrame.rows << endl; bool found = findProcessingAreas(myFrame, tape, capstan); // Reset frame position videoCapture.set(CAP_PROP_POS_FRAMES, 0); if (!found) { pprint("Processing area not found. Try changing JSON parameters.", RED); std::exit(EXIT_FAILURE); } /*********************************************************************************************/ /**************************************** PROCESSING *****************************************/ /*********************************************************************************************/ pprint("\nProcessing...", CYAN); // Processing timer time_t startTimer, endTimer; startTimer = time(NULL); processing(videoCapture, capstan, tape, args); endTimer = time(NULL); float min = (endTimer - startTimer) / 60; float sec = (endTimer - startTimer) % 60; string result("Processing elapsed time: " + to_string((int)min) + ":" + to_string((int)sec)); cout << endl << result << endl; /*********************************************************************************************/ /************************************* IRREGULARITY FILES ************************************/ /*********************************************************************************************/ fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json"; files::saveFile(outputFile1Name, irregularityFileOutput1.dump(4), false); // Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier extractIrregularityImagesForAudio(outputPath, VIDEO_PATH, irregularityFileInput, irregularityFileOutput2); fs::path outputFile2Name = outputPath / "VideoAnalyser_IrregularityFileOutput2.json"; files::saveFile(outputFile2Name, irregularityFileOutput2.dump(4), false); return 0; }