/* Questo script esegue l'analisi di un video fornito per rilevare le discontinuità che vengono trovate. Tutte le informazioni necessarie all'agoritmo si possono individuare nei file XML all'interno della cartella config. @author Nadir Dalla Pozza @version 3.0 @date 18-02-2023 */ #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" using namespace cv; using namespace std; using json = nlohmann::json; namespace fs = std::filesystem; namespace po = boost::program_options; /* ------------------------------------------------------------------------------ VARIABLES ------------------------------------------------------------------------------ */ // There are two alternative approaches: // Generalized Hough Transform and SURF. bool useSURF = true; bool savingPinchRoller = false, pinchRollerRect = false; bool savingBrand = false; bool endTapeSaved = false; cv::Mat myFrame; float mediaPrevFrame = 0; bool firstBrand = true; // The first frame containing brands on tape must be saved float firstInstant = 0; string fileName, extension; // config.json parameters fs::path workingPath; string filesName; bool brands; float speed, tapeThresholdPercentual, capstanThresholdPercentual; int minDist, angleThresh, scaleThresh, posThresh, minDistTape, angleThreshTape, scaleThreshTape, posThreshTape, minDistCapstan, angleThreshCapstan, scaleThreshCapstan, posThreshCapstan; // Path variables fs::path outputPath; fs::path irregularityImagesPath; fs::path videoPath; fs::path irregularityFileInputPath; // JSON files json configurationFile; json irregularityFileInput; json irregularityFileOutput1; json irregularityFileOutput2; // RotatedRect identifying the processing area RotatedRect rect, rectTape, rectCapstan; string PURPLE = "\033[95m"; string CYAN = "\033[96m"; string DARK_CYAN = "\033[36m"; string BLUE = "\033[94m"; string GREEN = "\033[92m"; string YELLOW = "\033[93m"; string RED = "\033[91m"; string BOLD = "\033[1m"; string UNDERLINE = "\033[4m"; string END = "\033[0m"; bool getArguments(int argc, char** argv) { // Read configuration file ifstream iConfig("../config/config.json"); iConfig >> configurationFile; if (argc == 1) { // Read from JSON file string wp = configurationFile["WorkingPath"]; workingPath = fs::path(wp); filesName = configurationFile["FilesName"]; brands = configurationFile["Brands"]; speed = configurationFile["Speed"]; } else { // Get from command line 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.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::variables_map vm; po::store(po::command_line_parser(argc, argv).options(desc).run(), vm); if (vm.count("help")) { cout << desc << "\n"; return false; } po::notify(vm); } catch (po::invalid_command_line_syntax& e) { cerr << RED << BOLD << "The command line syntax is invalid: " << END << RED << e.what() << END << endl; return false; } catch (po::required_option& e) { cerr << "Error: " << e.what() << endl; return false; } } tapeThresholdPercentual = configurationFile["TapeThresholdPercentual"]; capstanThresholdPercentual = configurationFile["CapstanThresholdPercentual"]; minDist = configurationFile["MinDist"]; angleThresh = configurationFile["AngleThresh"]; scaleThresh = configurationFile["ScaleThresh"]; posThresh = configurationFile["PosThresh"]; minDistTape = configurationFile["MinDistTape"]; angleThreshTape = configurationFile["AngleThreshTape"]; scaleThreshTape = configurationFile["ScaleThreshTape"]; posThreshTape = configurationFile["PosThreshTape"]; minDistCapstan = configurationFile["MinDistCapstan"]; angleThreshCapstan = configurationFile["AngleThreshCapstan"]; scaleThreshCapstan = configurationFile["ScaleThreshCapstan"]; posThreshCapstan = configurationFile["PosThreshCapstan"]; return true; } bool findProcessingAreas(json configurationFile) { /*********************************************************************************************/ /*********************************** READING HEAD DETECTION **********************************/ /*********************************************************************************************/ // Obtain grayscale version of myFrame Mat myFrameGrayscale; cvtColor(myFrame, myFrameGrayscale, COLOR_BGR2GRAY); // Get input shape in grayscale Mat templateImage = imread("../input/readingHead.png", IMREAD_GRAYSCALE); // Downsample myFrameGrayscale in half pixels for performance reasons Mat myFrameGrayscaleHalf; pyrDown(myFrameGrayscale, myFrameGrayscaleHalf, Size(myFrame.cols/2, myFrame.rows/2)); // Downsample tapeShape in half pixels Mat templateImageHalf; pyrDown(templateImage, templateImageHalf, Size(templateImage.cols/2, templateImage.rows/2)); // Process only the bottom-central portion of the input video -> best results with our videos Rect readingHeadProcessingAreaRect(myFrameGrayscaleHalf.cols/4, myFrameGrayscaleHalf.rows/2, myFrameGrayscaleHalf.cols/2, myFrameGrayscaleHalf.rows/2); Mat processingImage = myFrameGrayscaleHalf(readingHeadProcessingAreaRect); // Select the template to be detected Mat templateShape = templateImageHalf; // Algorithm and parameters Ptr alg = createGeneralizedHoughGuil(); vector positionsPos, positionsNeg; Mat votesPos, votesNeg; TickMeter tm; int oldPosThresh = posThresh; RotatedRect rectPos, rectNeg; ofstream myFile; Point2f pts[4]; // 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 double maxValPos = 0, maxValNeg = 0; int indexPos = 0, indexNeg = 0; alg -> setMinDist(minDist); alg -> setLevels(360); alg -> setDp(2); alg -> setMaxBufferSize(1000); alg -> setAngleStep(1); alg -> setAngleThresh(angleThresh); alg -> setMinScale(0.9); alg -> setMaxScale(1.1); alg -> setScaleStep(0.01); alg -> setScaleThresh(scaleThresh); alg -> setPosThresh(posThresh); alg -> setCannyLowThresh(150); // Old: 100 alg -> setCannyHighThresh(240); // Old: 300 alg -> setTemplate(templateShape); cout << DARK_CYAN << "Reading head" << END << endl; tm.start(); // Invoke utility.h function detectShape(alg, templateShape, posThresh, positionsPos, votesPos, positionsNeg, votesNeg, processingImage); tm.stop(); cout << "Reading head detection time: " << tm.getTimeMilli() << " ms" << endl; 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; } } // The color is progressively darkened to emphasize that the algorithm found more than one shape if (positionsPos.size() > 0) rectPos = drawShapes(myFrame, positionsPos[indexPos], Scalar(0, 0, 255-indexPos*64), templateImageHalf.cols, templateImageHalf.rows, myFrameGrayscaleHalf.cols/4, myFrameGrayscaleHalf.rows/2, 2); if (positionsNeg.size() > 0) rectNeg = drawShapes(myFrame, positionsNeg[indexNeg], Scalar(128, 128, 255-indexNeg*64), templateImageHalf.cols, templateImageHalf.rows, myFrameGrayscaleHalf.cols/4, myFrameGrayscaleHalf.rows/2, 2); myFile.open("log.txt", ios::app); if (maxValPos > 0) if (maxValNeg > 0) if (maxValPos > maxValNeg) { myFile << "READING HEAD: Positive angle is best, match number: " << indexPos << endl; rect = rectPos; } else { myFile << "READING HEAD: Negative angle is best, match number: " << indexNeg << endl; rect = rectNeg; } else { myFile << "READING HEAD: Positive angle is the only choice, match number: " << indexPos << endl; rect = rectPos; } else if (maxValNeg > 0) { myFile << "READING HEAD: Negative angle is the only choice, match number: " << indexNeg << endl; rect = rectNeg; } else { myFile.close(); return false; } cout << endl; rect.points(pts); /*********************************************************************************************/ /************************************ 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 = drawShapes(myFrame, positionTape, Scalar(0, 255-indexPos*64, 0), rect.size.width, 50 * (rect.size.width / 200), 0, 0, 1); myFile << "Tape area:" << endl; myFile << " Center (x, y): (" << rectTape.center.x << ", " << rectTape.center.y << ")" << endl; myFile << " Size (w, h): (" << rectTape.size.width << ", " << rectTape.size.height << ")" << endl; myFile << " Angle (deg): (" << rectTape.angle << ")" << endl; json autoJSON; autoJSON["PreservationAudioVisualFile"] = fileName; autoJSON["RotatedRect"] = { { "CenterX", rectTape.center.x }, { "CenterY", rectTape.center.y }, { "Width", rectTape.size.width }, { "Height", rectTape.size.height }, { "Angle", rectTape.angle } }; ofstream outputFile; string outputFileName = "/Users/nadir/Documents/MPAI-CAE/AreaJSONs/Auto/" + fileName + ".json"; outputFile.open(outputFileName); outputFile << autoJSON << endl; outputFile.close(); /*********************************************************************************************/ /************************************* CAPSTAN DETECTION *************************************/ /*********************************************************************************************/ // Read template image - it is smaller than before, therefore there is no need to downsample templateShape = imread("../input/capstanBERIO058prova.png", IMREAD_GRAYSCALE); // WORKING // templateShape = imread("../input/capstanBERIO058.png", IMREAD_GRAYSCALE); cout << DARK_CYAN << "Capstan" << END << endl; 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; tm.reset(); tm.start(); detector->detectAndCompute(templateShape, noArray(), keypoints_object, descriptors_object); detector->detectAndCompute(myFrameGrayscale, noArray(), keypoints_scene, descriptors_scene); tm.stop(); cout << "Capstan detection time: " << tm.getTimeMilli() << " ms" << endl; // 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, myFrameGrayscale, 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 = 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, wherw 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 = myFrameGrayscale(capstanProcessingAreaRect); // Reset algorithm and set parameters alg = createGeneralizedHoughGuil(); alg -> setMinDist(minDistCapstan); alg -> setLevels(360); alg -> setDp(2); alg -> setMaxBufferSize(1000); alg -> setAngleStep(1); alg -> setAngleThresh(angleThreshCapstan); alg -> setMinScale(0.9); alg -> setMaxScale(1.1); alg -> setScaleStep(0.01); alg -> setScaleThresh(scaleThreshCapstan); alg -> setPosThresh(posThreshCapstan); alg -> setCannyLowThresh(150); alg -> setCannyHighThresh(240); alg -> setTemplate(templateShape); oldPosThresh = posThreshCapstan; vector positionsC1Pos, positionsC1Neg; Mat votesC1Pos, votesC1Neg; tm.reset(); tm.start(); detectShape(alg, templateShape, posThreshCapstan, positionsC1Pos, votesC1Pos, positionsC1Neg, votesC1Neg, capstanProcessingAreaGrayscale); tm.stop(); cout << "Capstan detection time: " << tm.getTimeMilli() << " ms" << endl; // 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 choose the latest maxValPos = 0, maxValNeg = 0, indexPos = 0, indexNeg = 0; for (int i = 0; i < votesC1Pos.size().width; i++) { if (votesC1Pos.at(i) >= maxValPos) { maxValPos = votesC1Pos.at(i); indexPos = i; } } for (int i = 0; i < votesC1Neg.size().width; i++) { if (votesC1Neg.at(i) >= maxValNeg) { maxValNeg = votesC1Neg.at(i); indexNeg = i; } } RotatedRect rectCapstanPos, rectCapstanNeg; if (positionsC1Pos.size() > 0) rectCapstanPos = 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 = 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) { myFile << "CAPSTAN: Positive is best, match number: " << indexPos << endl; rectCapstan = rectCapstanPos; } else { myFile << "CAPSTAN: Negative is best, match number: " << indexNeg << endl; rectCapstan = rectCapstanNeg; } else { myFile << "CAPSTAN: Positive is the only choice, match number: " << indexPos << endl; rectCapstan = rectCapstanPos; } else if (maxValNeg > 0) { myFile << "CAPSTAN: Negative is the only choice, match number: " << indexNeg << endl; rectCapstan = rectCapstanNeg; } else { myFile.close(); return false; } } myFile << "Capstan ROI:" << endl; myFile << " Center (x, y): (" << rectCapstan.center.x << ", " << rectCapstan.center.y << ")" << endl; myFile << " Size (w, h): (" << rectCapstan.size.width << ", " << rectCapstan.size.height << ")" << endl; myFile << " Angle (deg): (" << rectCapstan.angle << ")" << endl; myFile.close(); cout << endl; // Save the image containing the detected areas cv::imwrite(outputPath.string() + "/tapeAreas.jpg", myFrame); return true; } bool frameDifference(cv::Mat prevFrame, cv::Mat currentFrame, int msToEnd) { /*********************************************************************************************/ /********************************** 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 * capstanThresholdPercentual / 100; // CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/ // matrices we'll use Mat M, rotatedPrevFrame, croppedPrevFrame, rotatedCurrentFrame, croppedCurrentFrame; // get angle and size from the bounding box float angle = rectCapstan.angle; Size rect_size = rectCapstan.size; // thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/ if (rectCapstan.angle < -45.) { angle += 90.0; swap(rect_size.width, rect_size.height); } // get the rotation matrix M = getRotationMatrix2D(rectCapstan.center, angle, 1.0); // perform the affine transformation cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC); cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC); // crop the resulting image cv::getRectSubPix(rotatedPrevFrame, rect_size, rectCapstan.center, croppedPrevFrame); cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectCapstan.center, croppedCurrentFrame); // imshow("Current frame", currentFrame); // imshow("Cropped Current Frame", croppedCurrentFrame); // waitKey(); // END CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/ cv::Mat differenceFrame = difference(croppedPrevFrame, croppedCurrentFrame); int blackPixelsCapstan = 0; for (int i = 0; i < croppedCurrentFrame.rows; i++) { for (int j = 0; j < croppedCurrentFrame.cols; j++) { if (differenceFrame.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; } else { savingPinchRoller = false; } } else { 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 = rectTape.size.width * rectTape.size.height; float tapeDifferentPixelsThreshold = tapeAreaPixels * tapeThresholdPercentual / 100; /***************** Extract matrices corresponding to the processing area *********************/ // Tape area // CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/ // matrices we'll use Mat M, rotatedPrevFrame, croppedPrevFrame, rotatedCurrentFrame, croppedCurrentFrame; // get angle and size from the bounding box float angle = rectTape.angle; Size rect_size = rectTape.size; // thanks to http://felix.abecassis.me/2011/10/opencv-rotation-deskewing/ if (rectTape.angle < -45.) { angle += 90.0; swap(rect_size.width, rect_size.height); } // get the rotation matrix M = getRotationMatrix2D(rectTape.center, angle, 1.0); // perform the affine transformation cv::warpAffine(prevFrame, rotatedPrevFrame, M, prevFrame.size(), INTER_CUBIC); cv::warpAffine(currentFrame, rotatedCurrentFrame, M, currentFrame.size(), INTER_CUBIC); // crop the resulting image cv::getRectSubPix(rotatedPrevFrame, rect_size, rectTape.center, croppedPrevFrame); cv::getRectSubPix(rotatedCurrentFrame, rect_size, rectTape.center, croppedCurrentFrame); // imshow("Current frame", currentFrame); // imshow("Cropped Current Frame", croppedCurrentFrame); // waitKey(); // END CODE FROM https://answers.opencv.org/question/497/extract-a-rotatedrect-area/ cv::Mat differenceFrame = difference(croppedPrevFrame, croppedCurrentFrame); 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 (differenceFrame.at(i, j)[0] == 0) { blackPixels++; } } } mediaCurrFrame = totColoreCF/tapeAreaPixels; /************************************* Decision stage ****************************************/ bool irregularity = false; 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 irregularity = true; } /***** BRANDS MANAGEMENT *****/ if (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; irregularity = true; } // In the following iterations reset savingBrand, since we are no longer interested in brands. } else savingBrand = false; } } // Update mediaPrevFrame mediaPrevFrame = mediaCurrFrame; return irregularity; } int processing(cv::VideoCapture videoCapture) { // Video duration int frameNumbers_v = videoCapture.get(CAP_PROP_FRAME_COUNT); float fps_v = videoCapture.get(CAP_PROP_FPS); // FPS can be non-integers!!! float videoLength = (float) frameNumbers_v / fps_v; // [s] int videoLength_ms = videoLength * 1000; int savedFrames = 0, unsavedFrames = 0; float lastSaved = -160; // Whenever we find an Irregularity, we want to skip a lenght equal to the 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 (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 = videoLength_ms - videoCapture.get(CAP_PROP_POS_MSEC); while (videoCapture.isOpened()) { cv::Mat frame; videoCapture >> frame; if (!frame.empty()) { int ms = videoCapture.get(CAP_PROP_POS_MSEC); int msToEnd = videoLength_ms - ms; if (ms == 0) // With OpenCV library, this happens at the last few frames of the video before realising that "frame" is empty. break; int secToEnd = msToEnd / 1000; int minToEnd = (secToEnd / 60) % 60; secToEnd = secToEnd % 60; string secStrToEnd = to_string(secToEnd), minStrToEnd = to_string(minToEnd); if (minToEnd < 10) minStrToEnd = "0" + minStrToEnd; if (secToEnd < 10) secStrToEnd = "0" + secStrToEnd; cout << "\rIrregularities: " << savedFrames << ". "; cout << "Remaining video time [mm:ss]: " << minStrToEnd << ":" << secStrToEnd << flush; if ((ms - lastSaved > savingRate) && frameDifference(prevFrame, frame, msToEnd)) { // An Irregularity is found! // De-interlacing frame cv::Mat oddFrame(frame.rows/2, frame.cols, CV_8UC3); cv::Mat evenFrame(frame.rows/2, frame.cols, CV_8UC3); separateFrame(frame, oddFrame, evenFrame); // Finding an image containing the whole tape Point2f pts[4]; if (savingPinchRoller) rectCapstan.points(pts); else rectTape.points(pts); cv::Mat subImage(frame, cv::Rect(100, min(pts[1].y, pts[2].y), frame.cols - 100, static_cast(rectTape.size.height))); // De-interlacing the image with the whole tape cv::Mat oddSubImage(subImage.rows/2, subImage.cols, CV_8UC3); int evenSubImageRows = subImage.rows/2; if (subImage.rows % 2 != 0) // If the found rectangle is of odd height, we must increase evenSubImage height by 1, otherwise we have segmentation_fault!!! evenSubImageRows += 1; cv::Mat evenSubImage(evenSubImageRows, subImage.cols, CV_8UC3); separateFrame(subImage, oddSubImage, evenSubImage); string timeLabel = getTimeLabel(ms); string safeTimeLabel = getSafeTimeLabel(ms); string irregularityImageFilename = to_string(savedFrames) + "_" + safeTimeLabel + ".jpg"; cv::imwrite(irregularityImagesPath / irregularityImageFilename, oddFrame); // Append Irregularity information to JSON boost::uuids::uuid uuid = boost::uuids::random_generator()(); irregularityFileOutput1["Irregularities"] += { { "IrregularityID", boost::lexical_cast(uuid) }, { "Source", "v" }, { "TimeLabel", timeLabel } }; irregularityFileOutput2["Irregularities"] += { { "IrregularityID", boost::lexical_cast(uuid) }, { "Source", "v" }, { "TimeLabel", timeLabel }, { "ImageURI", irregularityImagesPath.string() + "/" + irregularityImageFilename } }; lastSaved = ms; savedFrames++; } else { unsavedFrames++; } prevFrame = frame; } else { cout << endl << "Empty frame!" << endl; videoCapture.release(); break; } } ofstream myFile; myFile.open("log.txt", ios::app); myFile << "Saved frames are: " << savedFrames << endl; myFile.close(); return 0; } int main(int argc, char** argv) { /*********************************************************************************************/ /*************************************** CONFIGURATION ***************************************/ /*********************************************************************************************/ // Get the input from config.json or command line try { bool continueExecution = getArguments(argc, argv); if (!continueExecution) { return 0; } } catch (nlohmann::detail::type_error e) { cerr << RED << "config.json error!" << endl << e.what() << END << endl; return -1; } videoPath = workingPath / "PreservationAudioVisualFile" / filesName; if (findFileName(videoPath, fileName, extension) == -1) { cerr << RED << BOLD << "config.json error!" << END << endl << RED << videoPath.string() << " cannot be found or opened." << END << endl; return -1; } irregularityFileInputPath = 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; return -1; } if (speed != 7.5 && speed != 15) { cerr << RED << BOLD << "config.json error!" << END << endl << RED << "Speed parameter must be 7.5 or 15 ips." << END << endl; return -1; } if (tapeThresholdPercentual < 0 || tapeThresholdPercentual > 100) { cerr << RED << BOLD << "config.json error!" << END << endl << RED << "TapeThresholdPercentual parameter must be a percentage value." << END << endl; return -1; } if (capstanThresholdPercentual < 0 || capstanThresholdPercentual > 100) { cerr << RED << BOLD << "config.json error!" << END << endl << RED << "CapstanThresholdPercentual parameter must be a percentage value." << END << endl; return -1; } // Adjust input paramenters (considering given ones as pertinent to a speed reference = 7.5) if (brands) { if (speed == 15) tapeThresholdPercentual += 6; } else if (speed == 15) tapeThresholdPercentual += 20; else tapeThresholdPercentual += 21; cout << endl; cout << "Parameters:" << endl; cout << " Brands: " << brands << endl; cout << " Speed: " << speed << endl; cout << " ThresholdPercentual: " << tapeThresholdPercentual << endl; cout << " ThresholdPercentualCapstan: " << capstanThresholdPercentual << endl; cout << endl; // Read input JSON iJSON >> irregularityFileInput; /*********************************************************************************************/ /*********************************** MAKE OUTPUT DIRECTORY ***********************************/ /*********************************************************************************************/ // Make directory with fileName name outputPath = workingPath / "temp" / fileName; int outputFileNameDirectory = create_directory(outputPath); // Get now time time_t t = chrono::system_clock::to_time_t(chrono::system_clock::now()); string ts = ctime(&t); // Write useful info to log file ofstream myFile; myFile.open(outputPath / "log.txt", ios::app); myFile << endl << fileName << endl; myFile << "tsh: " << tapeThresholdPercentual << " tshp: " << capstanThresholdPercentual << endl; myFile << ts; // No endline character for avoiding middle blank line. myFile.close(); /*********************************************************************************************/ /************************************** AREAS DETECTION **************************************/ /*********************************************************************************************/ cv::VideoCapture videoCapture(videoPath); if (!videoCapture.isOpened()) { cerr << RED << BOLD << "Video unreadable." << END << endl; return -1; } // Get total number of frames int totalFrames = videoCapture.get(CAP_PROP_FRAME_COUNT); // Set frame position to half video length videoCapture.set(CAP_PROP_POS_FRAMES, totalFrames/2); // Get frame videoCapture >> myFrame; cout << "Video resolution: " << myFrame.cols << "x" << myFrame.rows << endl << endl; // Find the processing area corresponding to the tape area over the reading head bool found = findProcessingAreas(configurationFile); // Reset frame position videoCapture.set(CAP_PROP_POS_FRAMES, 0); // Write useful information to log file myFile.open("log.txt", ios::app); if (found) { cout << "Processing areas found!" << endl; myFile << "Processing areas found!" << endl; myFile.close(); } else { cout << "Processing area not found. Try changing JSON parameters." << endl; myFile << "Processing area not found." << endl; myFile.close(); return -1; // Program terminated early } /*********************************************************************************************/ /***************************** MAKE ADDITIONAL OUTPUT DIRECTORIES ****************************/ /*********************************************************************************************/ irregularityImagesPath = outputPath / "IrregularityImages"; int fullFrameDirectory = fs::create_directory(irregularityImagesPath); /*********************************************************************************************/ /**************************************** PROCESSING *****************************************/ /*********************************************************************************************/ cout << endl << CYAN << "Starting processing..." << END << endl; // Processing timer time_t startTimer, endTimer; startTimer = time(NULL); processing(videoCapture); 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; myFile.open("log.txt", ios::app); myFile << result << endl << endl; myFile.close(); /*********************************************************************************************/ /************************************* IRREGULARITY FILES ************************************/ /*********************************************************************************************/ ofstream outputFile1; fs::path outputFile1Name = outputPath / "VideoAnalyser_IrregularityFileOutput1.json"; outputFile1.open(outputFile1Name); outputFile1 << irregularityFileOutput1 << endl; // Irregularities to extract for the AudioAnalyser and to the TapeIrregularityClassifier extractIrregularityImagesForAudio(outputPath, videoPath, irregularityFileInput, irregularityFileOutput2); ofstream outputFile2; fs::path outputFile2Name = outputPath / "VideoAnalyser_IrregularityFileOutput2.json"; outputFile2.open(outputFile2Name); outputFile2 << irregularityFileOutput2 << endl; return 0; }