From 9e58d5cc26d77ee82533a7b84d9d0960c75fdbe3 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Wed, 29 May 2024 16:45:39 +0300 Subject: [PATCH 1/6] List additional information for connected Magewell USB capture devices, WiP #94 --- .../capturelib/include/reprostim/CaptureLib.h | 2 + Capture/capturelib/src/CaptureApp.cpp | 2 +- Capture/capturelib/src/CaptureLib.cpp | 59 +++++++++++++++++++ Capture/version.txt | 2 +- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Capture/capturelib/include/reprostim/CaptureLib.h b/Capture/capturelib/include/reprostim/CaptureLib.h index 800a871..0623cc3 100644 --- a/Capture/capturelib/include/reprostim/CaptureLib.h +++ b/Capture/capturelib/include/reprostim/CaptureLib.h @@ -154,6 +154,8 @@ namespace reprostim { void listAudioDevices(); + void listVideoDevices(); + std::string mwcSdkVersion(); AudioVolume parseAudioVolume(const std::string text); diff --git a/Capture/capturelib/src/CaptureApp.cpp b/Capture/capturelib/src/CaptureApp.cpp index 5b5f181..c916bbc 100644 --- a/Capture/capturelib/src/CaptureApp.cpp +++ b/Capture/capturelib/src/CaptureApp.cpp @@ -95,7 +95,7 @@ namespace reprostim { printVersion(); _INFO(" "); _INFO("[List of available Video devices]:"); - _INFO(" N/A in this version."); + listVideoDevices(); _INFO(" "); if( audioEnabled ) { _INFO("[List of available Audio devices]:"); diff --git a/Capture/capturelib/src/CaptureLib.cpp b/Capture/capturelib/src/CaptureLib.cpp index d6c1ea2..8c51f78 100644 --- a/Capture/capturelib/src/CaptureLib.cpp +++ b/Capture/capturelib/src/CaptureLib.cpp @@ -618,6 +618,65 @@ namespace reprostim { } while (snd_card_next(&card) >= 0 && card >= 0); } + void listVideoDevices() { + _VERBOSE("listVideoDevices()") + const int MAX_CAPTURE = 16; + MW_RESULT mr = MW_SUCCEEDED; + + BOOL fInit = MWCaptureInitInstance(); + if( !fInit ) + _ERROR("Failed MWCaptureInitInstance"); + + for(int k = 0; k<1; k++) { + SLEEP_MS(1); + mr = MWRefreshDevice(); + if (mr != MW_SUCCEEDED) + _ERROR("Failed MWRefreshDevice: " << mr); + + int n = MWGetChannelCount(); + MWCAP_CHANNEL_INFO mci; + MWCAP_VIDEO_SIGNAL_STATUS vss; + + for (int i = 0; i < n; i++) { + mr = MWGetChannelInfoByIndex(i, &mci); + if (mr != MW_SUCCEEDED) { + _ERROR("Failed MWGetChannelInfoByIndex[" << std::to_string(i) << "]: " << mr); + continue; + } + _INFO("Channel [" << std::to_string(i) << "]: " << mci); + + char wPath[256] = {0}; + if (MWGetDevicePath(i, wPath) == MW_SUCCEEDED) { + _INFO(" Device instance path: " << wPath); + } else { + _ERROR("Failed MWGetDevicePath[" << std::to_string(i) << "]"); + continue; + } + + HCHANNEL hChannel = MWOpenChannelByPath(wPath); + if (hChannel == NULL) { + _ERROR("Failed MWOpenChannelByPath[" << std::to_string(i) << "]"); + continue; + } + + MWGetChannelInfo(hChannel, &mci); + _INFO("Channel [" << std::to_string(i) << "]: " << mci); + + MWGetVideoSignalStatus(hChannel, &vss); + _INFO(" Video Signal Status: " << vss); + + safeMWCloseChannel(hChannel); + + if (i >= MAX_CAPTURE) { + _ERROR("Max capture limit reached: " << std::to_string(MAX_CAPTURE)); + break; + } + } + } + + MWCaptureExitInstance(); + } + std::string mwcSdkVersion() { BYTE bMajor = 0; BYTE bMinor = 0; diff --git a/Capture/version.txt b/Capture/version.txt index 35f6398..7f25cb7 100644 --- a/Capture/version.txt +++ b/Capture/version.txt @@ -1 +1 @@ -1.8.0.225 \ No newline at end of file +1.9.0.226 \ No newline at end of file From da5a63b0e323195210984704613c32614e835af0 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Thu, 30 May 2024 10:31:14 +0300 Subject: [PATCH 2/6] List additional information for connected Magewell USB capture devices, WiP #94 --- .../capturelib/include/reprostim/CaptureApp.h | 2 +- Capture/capturelib/src/CaptureApp.cpp | 24 +++++++++++++------ Capture/screencapture/src/ScreenCapture.cpp | 23 ++++++++++++++---- Capture/version.txt | 2 +- Capture/videocapture/src/VideoCapture.cpp | 23 ++++++++++++++---- 5 files changed, 55 insertions(+), 19 deletions(-) diff --git a/Capture/capturelib/include/reprostim/CaptureApp.h b/Capture/capturelib/include/reprostim/CaptureApp.h index 5d27a70..c5193a2 100644 --- a/Capture/capturelib/include/reprostim/CaptureApp.h +++ b/Capture/capturelib/include/reprostim/CaptureApp.h @@ -111,7 +111,7 @@ namespace reprostim { std::string createOutPath(const std::optional &ts = std::nullopt, bool fCreateDir = true); SessionLogger_ptr createSessionLogger(const std::string& name, const std::string& filePath); - void listDevices(); + void listDevices(const std::string& devices); virtual bool loadConfig(AppConfig& cfg, const std::string& pathConfig); virtual void onCaptureStart(); virtual void onCaptureStop(const std::string& message); diff --git a/Capture/capturelib/src/CaptureApp.cpp b/Capture/capturelib/src/CaptureApp.cpp index c916bbc..abf72f8 100644 --- a/Capture/capturelib/src/CaptureApp.cpp +++ b/Capture/capturelib/src/CaptureApp.cpp @@ -91,15 +91,25 @@ namespace reprostim { return nullptr; } - void CaptureApp::listDevices() { + void CaptureApp::listDevices(const std::string& devices) { printVersion(); - _INFO(" "); - _INFO("[List of available Video devices]:"); - listVideoDevices(); - _INFO(" "); - if( audioEnabled ) { + if( !(devices == "all" || devices == "audio" || devices == "video") ) { + _ERROR("Invalid device type: " << devices << ", must be 'all', 'audio' or 'video'"); + return; + } + if( devices=="all" || devices=="video" ) { + _INFO(" "); + _INFO("[List of available Video devices]:"); + listVideoDevices(); + } + if( devices=="all" || devices=="audio" ) { + _INFO(" "); _INFO("[List of available Audio devices]:"); - listAudioDevices(); + if (audioEnabled) { + listAudioDevices(); + } else { + _INFO("Audio capture is disabled in " << appName); + } } } diff --git a/Capture/screencapture/src/ScreenCapture.cpp b/Capture/screencapture/src/ScreenCapture.cpp index 1e1b698..3120945 100644 --- a/Capture/screencapture/src/ScreenCapture.cpp +++ b/Capture/screencapture/src/ScreenCapture.cpp @@ -86,8 +86,13 @@ int ScreenCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { "\t \tDefaults to console output\n" "\t-v, --verbose\n" "\t \tVerbose, provides detailed information to stdout\n" - "\t-l, --list-devices\n" - "\t \tList devices, only audio is supported\n" + "\t-l, --list-devices \n" + "\t \tList connected capture devices information.\n" + "\t \tSupported values:\n" + "\t \t all : list all available information\n" + "\t \t audio : list only audio devices information\n" + "\t \t video : list only video devices information\n" + "\t \tDefault value is \"all\"\n" "\t-V\n" "\t \tPrint version number only\n" "\t--version\n" @@ -106,7 +111,7 @@ int ScreenCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { {"help", no_argument, nullptr, 'h'}, {"verbose", no_argument, nullptr, 'v'}, {"version", no_argument, nullptr, 1000}, - {"list-devices", no_argument, nullptr, 'l'}, + {"list-devices", optional_argument, nullptr, 'l'}, {"file-log", required_argument, nullptr, 'f'}, {nullptr, 0, nullptr, 0} }; @@ -125,8 +130,16 @@ int ScreenCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { case 'h': _INFO(HELP_STR); return 1; - case 'l': - listDevices(); + case 'l': { + std::string devices = "all"; + if (optarg) { + devices = std::string(optarg); + } else if (optind < argc && argv[optind][0] != '-') { + devices = std::string(argv[optind]); + optind++; + } + listDevices(devices); + } return 1; case 'v': opts.verbose = true; diff --git a/Capture/version.txt b/Capture/version.txt index 7f25cb7..bcce4cc 100644 --- a/Capture/version.txt +++ b/Capture/version.txt @@ -1 +1 @@ -1.9.0.226 \ No newline at end of file +1.9.0.230 \ No newline at end of file diff --git a/Capture/videocapture/src/VideoCapture.cpp b/Capture/videocapture/src/VideoCapture.cpp index f502c2e..b1c7592 100644 --- a/Capture/videocapture/src/VideoCapture.cpp +++ b/Capture/videocapture/src/VideoCapture.cpp @@ -211,8 +211,13 @@ int VideoCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { "\t \tPrint version number only\n" "\t--version\n" "\t \tPrint expanded version information\n" - "\t-l, --list-devices\n" - "\t \tList devices, only audio is supported\n" + "\t-l, --list-devices \n" + "\t \tList connected capture devices information.\n" + "\t \tSupported values:\n" + "\t \t all : list all available information\n" + "\t \t audio : list only audio devices information\n" + "\t \t video : list only video devices information\n" + "\t \tDefault value is \"all\"\n" "\t-h, --help\n" "\t \tPrint this help string\n"; @@ -227,7 +232,7 @@ int VideoCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { {"help", no_argument, nullptr, 'h'}, {"verbose", no_argument, nullptr, 'v'}, {"version", no_argument, nullptr, 1000}, - {"list-devices", no_argument, nullptr, 'l'}, + {"list-devices", optional_argument, nullptr, 'l'}, {"file-log", required_argument, nullptr, 'f'}, {nullptr, 0, nullptr, 0} }; @@ -246,8 +251,16 @@ int VideoCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { case 'h': _INFO(HELP_STR); return 1; - case 'l': - listDevices(); + case 'l': { + std::string devices = "all"; + if (optarg) { + devices = std::string(optarg); + } else if (optind < argc && argv[optind][0] != '-') { + devices = std::string(argv[optind]); + optind++; + } + listDevices(devices); + } return 1; case 'v': opts.verbose = true; From aef40ab48dbe6e33ea15968693c7d70f5e1b09bc Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 3 Jun 2024 13:39:39 +0300 Subject: [PATCH 3/6] nosignal-batch file sample to analize directory with video mkv files, WiP --- Capture/nosignal/docs/install.txt | 2 +- Capture/nosignal/nosignal-batch.sh | 61 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100755 Capture/nosignal/nosignal-batch.sh diff --git a/Capture/nosignal/docs/install.txt b/Capture/nosignal/docs/install.txt index dee6e41..2a1a7ab 100644 --- a/Capture/nosignal/docs/install.txt +++ b/Capture/nosignal/docs/install.txt @@ -6,7 +6,7 @@ >> Poetry (version 1.8.2) # set in project venv in poetry -./venv/bin/poetry config virtualenvs.create false +./venv/bin/poetry config virtualenvs.create true ./venv/bin/poetry config virtualenvs.in-project true # install poetry and run test diff --git a/Capture/nosignal/nosignal-batch.sh b/Capture/nosignal/nosignal-batch.sh new file mode 100755 index 0000000..5688020 --- /dev/null +++ b/Capture/nosignal/nosignal-batch.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# chmod +x ns_05.sh + +# Define external variables +NOSIGNAL_ARGS="--number-of-checks 5 --truncated check5" +#NOSIGNAL_ARGS="--number-of-checks 5 --truncated fixup" + + +# log all to file +LOG_TIMESTAMP=$(date '+%Y%m%d_%H%M%S') +LOG_FILE="ns_$LOG_TIMESTAMP.log" +# Redirect stdout and stderr to the log file +exec > >(tee -a "$LOG_FILE") 2>&1 + + +DIRECTORY="/data/repronim/reprostim-reproiner/Videos/2024/05" + +TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') +echo "[$TIMESTAMP] Processing nosignal batch:" +echo " - DIRECTORY : $DIRECTORY" +echo " - NOSIGNAL_ARGS : $NOSIGNAL_ARGS" +echo " " + +FILES=("$DIRECTORY"/*.mkv) +TOTAL_FILES=${#FILES[@]} + + +COUNTER=1 +NOSIGNAL_COUNTER=0 +NOSIGNAL_FILES=() +for FILE in "${FILES[@]}" +do + echo " " + echo "----------------------------------------------" + TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$TIMESTAMP] Processing ($COUNTER of $TOTAL_FILES) $FILE ..." + ./reprostim/nosignal $NOSIGNAL_ARGS "$FILE" + EXIT_CODE=$? + echo "EXIT_CODE=$EXIT_CODE" + if [ $EXIT_CODE -eq 1 ]; then + echo "[$TIMESTAMP] NOSIGNAL_FILE: $FILE" + ((NOSIGNAL_COUNTER++)) + NOSIGNAL_FILES+=("$FILE") + fi + ((COUNTER++)) +done + +echo " " +echo "----------------------------------------------" +echo "DIRECTORY : $DIRECTORY" +echo "NOSIGNAL_ARGS : $NOSIGNAL_ARGS" +echo " " +echo "TOTAL_FILES : $TOTAL_FILES" +echo "NOSIGNAL_COUNT : $NOSIGNAL_COUNTER" +for NOSIGNAL_FILE in "${NOSIGNAL_FILES[@]}" +do + echo "$NOSIGNAL_FILE" +done +echo " " + + From ee276f777a7189c53febaf3097e8e7b025bd35bf Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Thu, 6 Jun 2024 13:43:53 +0300 Subject: [PATCH 4/6] Improoved "fixup" option with no audio, and --invalid-timing options, #82 --- Capture/nosignal/data/nosignal.png | Bin 0 -> 46766 bytes Capture/nosignal/reprostim/nosignal | 71 ++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 Capture/nosignal/data/nosignal.png diff --git a/Capture/nosignal/data/nosignal.png b/Capture/nosignal/data/nosignal.png new file mode 100644 index 0000000000000000000000000000000000000000..57a19c63b8624a88f7cc40a758a02e6a71195bcc GIT binary patch literal 46766 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∨MkIBvcmj9!6?|OP**q_Lj$=5+W`|dCn%4^MXC!nsdKz ztYic|8Db4dU2ouK>RC0(x|zN!y!oclqoFQlDQT;r%IlZJ_FBcwfu?DBH2vT;i3V=0DPbq!M?F8msQxAw{#T7V_hS8fwf5#!ZhvI-p-R|?uM7d zYHHi~vfL6H4(8J)RP;*|x-90IBi|!@&HdWXobfF6px)29&E%F;W2#qm!qi6AVy!lN zN279mV^9Hu_~+P$q#IsDg=Di7fkrjNGgRvkw>9X}lfGFxa6p$EYZfbbde&zz`IN-!>DHqC7wq^!0~!r2M%OriWd>C{cb^QX1tO5s1sD<>ljID ziS*-Up5=E02RibYxjKH(S({TLWt;82;`P8Ur5hG5=|P9U+gGmrk+FlUnxv*Zoog#< zJ~dMF;z{Em6Uk4RmSIC;YnPB!bk?;%+4L)1MRWX}-{zmHhMP>uyBObT352gov)wI} zgwxM{`>TOB_v((z3DLD7qorY3tRheNcn&hWc%D&A46#}UQXYZ+foQ~q-+rec$NbAd z3MC|o3iGR@x(O;fIpyuhNkW~-M)h!7qk8iw8D&b=&&}*4-v-XXD(bJP&kpZwG;;Dr z(Uq>{@h!*P6M%S(Q73IcjaR(Q{(YhGsqHRE*V&wr5Pgpb?3L>tkfU`T`g#P9f@&7W zNUvSb5bXDZ>>O}pVD9Jgzarap*M>ux*(ER^wcM;_HtpzEYs&{)IM|AXEk4*{ z!YwA;V!|yZ++xBlCfs7e|GP}6){V2Tf<-AL{rJWZd*s6Y%{FL VideoInfo: vi: VideoInfo = VideoInfo(fps=0, width=0, height=0, + is_invalid_timing=False, is_truncated=False, frames_count=0, nosignal_count=0, nosignal_rate=0.0, scanned_count=0) @@ -127,6 +129,8 @@ def find_no_signal(video_path: str, step: int = 1, fps = cap.get(cv2.CAP_PROP_FPS) frame_width: int = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) frame_height: int = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + duration_sec: float = frame_count / fps if fps > 0 else -1.0 + logger.debug(f"duration_sec={duration_sec}") vi.width = frame_width vi.height = frame_height vi.fps = round(fps, 2) @@ -155,6 +159,12 @@ def find_no_signal(video_path: str, step: int = 1, vi.error = f"Invalid frame range: {pos_first_frame} - {pos_last_frame}" return vi + if duration_sec < 0 or duration_sec > 2*24*60*60: + vi.is_invalid_timing = True + if check_first_frames == 0: + vi.error = f"Invalid video duration: {duration_sec} seconds" + return vi + pos_cur_frame: int = pos_first_frame pos_next_frame: int = pos_cur_frame nosignal_counter: int = 0 @@ -240,12 +250,23 @@ def find_no_signal(video_path: str, step: int = 1, ' [exit2] - exit with error code 2, default value.\n' ' [fixup] - use ffmpeg to fix the video by copying\n' ' it into a temporary location with\n' - ' "ffmpeg -i -c copy "\n' + ' "ffmpeg -i -an -c copy "\n' ' command.\n' ' [check5] - check for nosignal first 5 frames only in\n' ' truncated video.\n \n' 'To check if video is truncated, use:\n' ' mediainfo -i | grep "IsTruncated"') +@click.option('--invalid-timing', type=click.Choice(['exit3', 'fixup', 'check5'], + case_sensitive=False), + default='exit3', + help='\b\nSpecify behavior on video with invalid duration:\n \n' + ' [exit3] - exit with error code 3, default value.\n' + ' [fixup] - use ffmpeg to fix the video by copying\n' + ' it into a temporary location with\n' + ' "ffmpeg -i -an -c copy "\n' + ' command.\n' + ' [check5] - check for nosignal first 5 frames only in\n' + ' truncated video.\n \n') @click.option('--threshold', default=0.01, type=float, help='Specify the threshold for nosignal frames, default is ' '0.01 which means 1% of the totally checked frames.') @@ -254,6 +275,7 @@ def main(ctx, path: str, log_level, step: int, number_of_checks: int, show_progress: float, truncated: str, + invalid_timing: str, threshold: float): logger.setLevel(log_level) logger.debug("nosignal.py tool") @@ -263,25 +285,44 @@ def main(ctx, path: str, log_level, step: int, temp_path: str = None res = find_no_signal(path, step, number_of_checks, show_progress, 0) + + if res.error is not None: + logger.error(f"ERROR : {res.error}") + + # process truncated video if any if res.is_truncated: logger.error(f"ERROR : Truncated video detected.") - if res.is_truncated and truncated == "exit2": - return main_exit(2) + if truncated == "exit2": + return main_exit(2) - if res.is_truncated and truncated == "fixup": - logger.info("TRUNCATED VIDEO: Attempting to fix the truncated video.") - temp_path = tempfile.mktemp(suffix="_nosignal_fixed.mkv") - logger.info(f"Copying video to temporary file: {temp_path}") - auto_fix_video(path, temp_path) - res = find_no_signal(temp_path, step, number_of_checks, show_progress, 0) + if truncated == "fixup": + logger.info("TRUNCATED VIDEO: Attempting to fix the truncated video.") + temp_path = tempfile.mktemp(suffix="_nosignal_fixed.mkv") + logger.info(f"Copying video to temporary file: {temp_path}") + auto_fix_video(path, temp_path) + res = find_no_signal(temp_path, step, number_of_checks, show_progress, 0) - if res.is_truncated and truncated == "check5": - logger.info("TRUNCATED VIDEO: Checking first 5 frames only.") - res = find_no_signal(path, step, number_of_checks, show_progress, 5) + if truncated == "check5": + logger.info("TRUNCATED VIDEO: Checking first 5 frames only.") + res = find_no_signal(path, step, number_of_checks, show_progress, 5) - if res.error is not None: - logger.error(f"ERROR : {res.error}") + elif res.is_invalid_timing: + logger.error(f"ERROR : Invaild video timing/duration detected.") + + if invalid_timing == "exit3": + return main_exit(3) + + if invalid_timing == "fixup": + logger.info("INVALID TIMING : Attempting to fix the video duration.") + temp_path = tempfile.mktemp(suffix="_nosignal_fixed.mkv") + logger.info(f"Copying video to temporary file: {temp_path}") + auto_fix_video(path, temp_path) + res = find_no_signal(temp_path, step, number_of_checks, show_progress, 0) + + if invalid_timing == "check5": + logger.info("INVALID TIMING : Checking first 5 frames only.") + res = find_no_signal(path, step, number_of_checks, show_progress, 5) # delete temp_path if exists if temp_path is not None and os.path.exists(temp_path): From 980ad4ca874ea715b01821b49c9cb8bd928df1b3 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Thu, 6 Jun 2024 14:07:41 +0300 Subject: [PATCH 5/6] Nosignal batch script extended with latest options and processing directory as argument, #82 --- Capture/nosignal/nosignal-batch.sh | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Capture/nosignal/nosignal-batch.sh b/Capture/nosignal/nosignal-batch.sh index 5688020..e87eedc 100755 --- a/Capture/nosignal/nosignal-batch.sh +++ b/Capture/nosignal/nosignal-batch.sh @@ -2,8 +2,24 @@ # chmod +x ns_05.sh # Define external variables -NOSIGNAL_ARGS="--number-of-checks 5 --truncated check5" -#NOSIGNAL_ARGS="--number-of-checks 5 --truncated fixup" +#NOSIGNAL_ARGS="--number-of-checks 5 --truncated check5" +NOSIGNAL_ARGS="--number-of-checks 100 --truncated fixup --invalid-timing fixup --threshold 0.5" + +print_help() { + echo "Usage: $0 " + echo + echo "Arguments:" + echo " The directory containing the .mkv files to process." + echo + echo "Example:" + echo " $0 /data/repronim/reprostim-reproiner/Videos/2024/05" +} + +# Check if the DIRECTORY parameter is provided +if [ -z "$1" ]; then + print_help + exit 1 +fi # log all to file @@ -13,7 +29,8 @@ LOG_FILE="ns_$LOG_TIMESTAMP.log" exec > >(tee -a "$LOG_FILE") 2>&1 -DIRECTORY="/data/repronim/reprostim-reproiner/Videos/2024/05" +#DIRECTORY="/data/repronim/reprostim-reproiner/Videos/2024/05" +DIRECTORY="$1" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') echo "[$TIMESTAMP] Processing nosignal batch:" From 5d57529f0caab3d865cd377a60505fc5cda486c7 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Tue, 11 Jun 2024 17:48:46 +0300 Subject: [PATCH 6/6] Added new algorithm to check nosignal/rainbow frames, based on reference data/nosignal.png sample. Also fixed some errors in processing video causing eternal cycle, #82 --- Capture/nosignal/reprostim/nosignal | 85 ++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/Capture/nosignal/reprostim/nosignal b/Capture/nosignal/reprostim/nosignal index a0a5c1b..f054b05 100755 --- a/Capture/nosignal/reprostim/nosignal +++ b/Capture/nosignal/reprostim/nosignal @@ -50,6 +50,11 @@ ts = time.time() lower_rainbow = np.array([0, 50, 50]) upper_rainbow = np.array([30, 255, 255]) +# Specify nosignal grid size (8 bands) for custom algorithm +grid_rows: int = 6 +grid_cols: int = 8 +grid_colors = [[None for _ in range(grid_cols)] for _ in range(grid_rows)] + def auto_fix_video(video_path: str, temp_path: str): logger.info(f"Run mediainfo to get video information: mediainfo -i {video_path}") @@ -69,6 +74,13 @@ def auto_fix_video(video_path: str, temp_path: str): logger.info("Video fixup completed.") +# Function to calculate opencv2 color difference +def calc_color_diff(color1, color2): + b1, g1, r1 = color1.astype(np.int32) + b2, g2, r2 = color2.astype(np.int32) + return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) + + def has_rainbow(frame): # Convert frame to HSV color space hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) @@ -81,6 +93,61 @@ def has_rainbow(frame): return pixel_count > 10000 # Adjust threshold as needed +# match nosignal grid colors using custom algorithm +# based on reference image sample +def has_rainbow2(frame): + height, width, _ = frame.shape + cy = height // grid_rows + cx = width // grid_cols + n = grid_rows * grid_cols + diff = 0 + for i in range(grid_rows): + for j in range(grid_cols): + x = int(j * cx + cx // 2) + y = int(i * cy + cy // 2) + clr1 = grid_colors[i][j] + clr2 = frame[y, x] + diff = diff + calc_color_diff(clr1, clr2) + + diff = diff / n + logger.debug(f"diff={diff}") + # NOTE: tune the threshold value as needed + return diff < 35 + + +def init_grid_colors(ref_image_path: str): + # Load reference image + image = cv2.imread(ref_image_path) + + # Get the dimensions of the image + height, width, _ = image.shape + + # Calculate the height and width of grid cell + cell_height = height // grid_rows + cell_width = width // grid_cols + + # Loop over the grid + for i in range(grid_rows): + for j in range(grid_cols): + # Calculate the center of each region + x = int(j * cell_width + cell_width // 2) + y = int(i * cell_height + cell_height // 2) + + # Get the pixel color at the center + # opencv use (row, column) coordinates + clr = image[y, x] + + # Store the color in the 2D list + grid_colors[i][j] = clr + + # dump the grid colors + logger.debug("Nosignal reference grid colors:") + for i, row in enumerate(grid_colors): + for j, color in enumerate(row): + b, g, r = color.astype(np.int32) # OpenCV stores colors as BGR + logger.debug(f" grid_colors[{i+1}, {j+1}] : RGB({r}, {g}, {b})") + + def main_exit(code: int) -> int: # print debug message exec_time = time.time() - ts @@ -184,6 +251,19 @@ def find_no_signal(video_path: str, step: int = 1, if ret is False: logger.debug(f"Failed reading frame {pos_cur_frame}") break + + # for some rare videos, opencv continues to read frames even after the end of video + # e.g. Videos/2024/03/2024.03.18.14.39.38.336_2024.03.18.14.44.02.541.mkv + # to fix this cases, just break the loop + if pos_cur_frame > pos_last_frame: + logger.debug(f"Failed reading frame sequencer, pos_cur_frame={pos_cur_frame} > pos_last_frame={pos_last_frame}") + break + + # also double check number_of_checks for similar cases + if 0 < number_of_checks <= scan_counter: + logger.debug(f"Failed reading frame, number_of_checks={number_of_checks} limit reached") + break + scan_counter += 1 # break if check_first_frames is set and reached @@ -197,7 +277,7 @@ def find_no_signal(video_path: str, step: int = 1, else: pos_next_frame = pos_cur_frame + step - if has_rainbow(frame): + if has_rainbow2(frame): logger.debug("rainbow-yes") nosignal_counter = nosignal_counter + 1 nosignal_frames.append(pos_cur_frame) @@ -284,6 +364,9 @@ def main(ctx, path: str, log_level, step: int, temp_path: str = None + logger.debug("Initializing grid colors...") + init_grid_colors("data/nosignal.png") + res = find_no_signal(path, step, number_of_checks, show_progress, 0) if res.error is not None: