From 047cce58e398c685c3c8c432cfc1680885f40738 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Thu, 20 Jun 2024 19:51:21 +0300 Subject: [PATCH 01/20] Add timestamp to the main log, normalize log level and thread info in session and file logs, WiP #100 --- .../capturelib/include/reprostim/CaptureLog.h | 33 ++++++++++--- Capture/capturelib/src/CaptureApp.cpp | 1 + Capture/capturelib/src/CaptureLog.cpp | 46 +++++++++++++++++++ Capture/screencapture/src/ScreenCapture.cpp | 1 + Capture/version.txt | 2 +- Capture/videocapture/src/VideoCapture.cpp | 1 + 6 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Capture/capturelib/include/reprostim/CaptureLog.h b/Capture/capturelib/include/reprostim/CaptureLog.h index 6e1fb90..b469776 100644 --- a/Capture/capturelib/include/reprostim/CaptureLog.h +++ b/Capture/capturelib/include/reprostim/CaptureLog.h @@ -4,12 +4,16 @@ //////////////////////////////////////////////////////////////////////////////// // Macros +#ifndef _LOG_EXPR +#define _LOG_EXPR(expr, level) buildLogPrefix(getLogPattern(), level) << expr +#endif + #ifndef _ERROR -#define _ERROR(expr) std::cerr << expr << std::endl; _SESSION_LOG_ERROR(expr) +#define _ERROR(expr) std::cerr << _LOG_EXPR(expr, LogLevel::ERROR) << std::endl; _SESSION_LOG_ERROR(expr) #endif #ifndef _INFO -#define _INFO(expr) std::cout << expr << std::endl; _SESSION_LOG_INFO(expr) +#define _INFO(expr) std::cout << _LOG_EXPR(expr, LogLevel::INFO) << std::endl; _SESSION_LOG_INFO(expr) #endif #ifndef _INFO_RAW @@ -17,7 +21,7 @@ #endif #ifndef _VERBOSE -#define _VERBOSE(expr) if( isVerbose() ) { std::cout << expr << std::endl; _SESSION_LOG_DEBUG(expr); } +#define _VERBOSE(expr) if( isVerbose() ) { std::cout << _LOG_EXPR(expr, LogLevel::DEBUG) << std::endl; _SESSION_LOG_DEBUG(expr); } #endif // Session logger related macros @@ -83,12 +87,18 @@ namespace reprostim { ERROR = 4 }; + enum LogPattern:int { + SIMPLE = 0, // just simple log line + FULL = 1 // detailed log line with timestamp, thread info, log level etc + }; + //////////////////////////////////////////////////////////////////////////////// // Functions - LogLevel parseLogLevel(const std::string &level); - void registerFileLogger(const std::string &name, const std::string &filePath, int level = LogLevel::DEBUG); - void unregisterFileLogger(const std::string &name); + std::string buildLogPrefix(LogPattern pattern, LogLevel level); + LogLevel parseLogLevel(const std::string &level); + void registerFileLogger(const std::string &name, const std::string &filePath, int level = LogLevel::DEBUG); + void unregisterFileLogger(const std::string &name); //////////////////////////////////////////////////////////////////////////////// // Classes @@ -173,7 +183,8 @@ namespace reprostim { ////////////////////////////////////////////////////////////////////////// // Global variables - extern volatile int g_verbose; + extern volatile LogPattern g_logPattern; + extern volatile int g_verbose; // global TLS variable to hold local session logger extern thread_local SessionLogger_ptr tl_pSessionLogger; @@ -181,10 +192,18 @@ namespace reprostim { ////////////////////////////////////////////////////////////////////////// // Inline functions + inline LogPattern getLogPattern() { + return g_logPattern; + } + inline bool isVerbose() { return g_verbose>0; } + inline void setLogPattern(LogPattern pattern) { + g_logPattern = pattern; + } + inline void setVerbose(bool verbose) { g_verbose = verbose ? 1 : 0; } diff --git a/Capture/capturelib/src/CaptureApp.cpp b/Capture/capturelib/src/CaptureApp.cpp index abf72f8..298b107 100644 --- a/Capture/capturelib/src/CaptureApp.cpp +++ b/Capture/capturelib/src/CaptureApp.cpp @@ -39,6 +39,7 @@ namespace reprostim { } pRepromonQueue = nullptr; unregisterFileLogger(_FILE_LOGGER_NAME); + setLogPattern(LogPattern::SIMPLE); } std::string CaptureApp::createOutPath(const std::optional &ts, bool fCreateDir) { diff --git a/Capture/capturelib/src/CaptureLog.cpp b/Capture/capturelib/src/CaptureLog.cpp index e8c83d6..5bd3c3a 100644 --- a/Capture/capturelib/src/CaptureLog.cpp +++ b/Capture/capturelib/src/CaptureLog.cpp @@ -1,6 +1,10 @@ #include #include + +// make log level to be upper case, can be also placed under include/spdlog/tweakme.h +#define SPDLOG_LEVEL_NAMES { "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL", "OFF" }; + #include #include #include "reprostim/CaptureLib.h" @@ -9,10 +13,52 @@ namespace fs = std::filesystem; namespace reprostim { + volatile LogPattern g_logPattern = LogPattern::SIMPLE; // set default log pattern volatile int g_verbose = 0; thread_local SessionLogger_ptr tl_pSessionLogger = nullptr; std::shared_ptr g_pGlobalLogger = nullptr; + std::string buildLogPrefix(LogPattern pattern, LogLevel level) { + if( pattern != LogPattern::FULL ) { + return ""; + } + + std::stringstream ss; + const Timestamp &ts = CURRENT_TIMESTAMP(); + + ss << getTimeFormatStr(ts, "%Y-%m-%d %H:%M:%S"); + + // put also ms precision + auto nowMs = std::chrono::duration_cast(ts.time_since_epoch()) % 1000; + ss << '.' << std::setw(3) << std::setfill('0') << nowMs.count(); + + // add log level + switch( level ) { + case LogLevel::DEBUG: + ss << " [DEBUG]"; + break; + case LogLevel::INFO: + ss << " [INFO]"; + break; + case LogLevel::WARN: + ss << " [WARN]"; + break; + case LogLevel::ERROR: + ss << " [ERROR]"; + break; + default: + ss << " [UNKNOWN]"; + break; + } + + // add thread id + //std::thread::id thread_id = std::this_thread::get_id(); + ss << " [" << spdlog::details::os::thread_id() << "]"; + + ss << " "; + return ss.str(); + } + LogLevel parseLogLevel(const std::string &level) { if( level == "DEBUG" ) { return LogLevel::DEBUG; diff --git a/Capture/screencapture/src/ScreenCapture.cpp b/Capture/screencapture/src/ScreenCapture.cpp index 3120945..6ac953b 100644 --- a/Capture/screencapture/src/ScreenCapture.cpp +++ b/Capture/screencapture/src/ScreenCapture.cpp @@ -152,6 +152,7 @@ int ScreenCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { return 1; case 'f': registerFileLogger(_FILE_LOGGER_NAME, optarg); + setLogPattern(LogPattern::FULL); break; } } diff --git a/Capture/version.txt b/Capture/version.txt index bcce4cc..5cbbe9f 100644 --- a/Capture/version.txt +++ b/Capture/version.txt @@ -1 +1 @@ -1.9.0.230 \ No newline at end of file +1.10.0.247 \ No newline at end of file diff --git a/Capture/videocapture/src/VideoCapture.cpp b/Capture/videocapture/src/VideoCapture.cpp index b1c7592..53e9c44 100644 --- a/Capture/videocapture/src/VideoCapture.cpp +++ b/Capture/videocapture/src/VideoCapture.cpp @@ -273,6 +273,7 @@ int VideoCaptureApp::parseOpts(AppOpts& opts, int argc, char* argv[]) { return 1; case 'f': registerFileLogger(_FILE_LOGGER_NAME, optarg); + setLogPattern(LogPattern::FULL); break; } } From 2f9267ac530171e764367a5cfee8a51d2f6b2e9a Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Fri, 21 Jun 2024 14:44:34 +0300 Subject: [PATCH 02/20] Add iso format timestamps to the .mkv.log header, #99 --- Capture/version.txt | 2 +- Capture/videocapture/src/VideoCapture.cpp | 21 ++++++++++++++++----- Capture/videocapture/src/VideoCapture.h | 1 + 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Capture/version.txt b/Capture/version.txt index 5cbbe9f..00c64bb 100644 --- a/Capture/version.txt +++ b/Capture/version.txt @@ -1 +1 @@ -1.10.0.247 \ No newline at end of file +1.10.0.254 \ No newline at end of file diff --git a/Capture/videocapture/src/VideoCapture.cpp b/Capture/videocapture/src/VideoCapture.cpp index 53e9c44..d772eec 100644 --- a/Capture/videocapture/src/VideoCapture.cpp +++ b/Capture/videocapture/src/VideoCapture.cpp @@ -135,11 +135,14 @@ void FfmpegThread ::run() { // terminate session logs _VERBOSE("FfmpegThread leave [" << tid << "]: " << getParams().cmd); + Timestamp ts = CURRENT_TIMESTAMP(); json jm = { {"type", "session_end"}, - {"ts", getTimeStr()}, + {"ts", getTimeStr(ts)}, + {"ts_iso", getTimeIsoStr(ts)}, {"message", "ffmpeg thread terminated"}, - {"start_ts", getParams().start_ts} + {"start_ts", getParams().start_ts}, + {"start_ts_iso", getTimeIsoStr(getParams().tsStart)} }; _METADATA_LOG(jm); _SESSION_LOG_END_CLOSE_RENAME(outVideoFile2 + ".log"); @@ -181,12 +184,16 @@ void VideoCaptureApp::onCaptureStop(const std::string& message) { Timestamp tsStop = CURRENT_TIMESTAMP(); std::string stop_ts = getTimeStr(tsStop); + Timestamp ts = CURRENT_TIMESTAMP(); json jm = { {"type", "capture_stop"}, - {"ts", getTimeStr()}, + {"ts", getTimeStr(ts)}, + {"ts_iso", getTimeIsoStr(ts)}, {"message", message}, {"start_ts", start_ts}, - {"stop_ts", stop_ts} + {"start_ts_iso", getTimeIsoStr(tsStart)}, + {"stop_ts", stop_ts}, + {"stop_ts_iso", getTimeIsoStr(tsStop)} }; _METADATA_LOG(jm); @@ -332,15 +339,18 @@ void VideoCaptureApp::startRecording(int cx, int cy, const std::string& frameRat SessionLogger_ptr pLogger = createSessionLogger("session_logger_" + start_ts, outVideoFile + ".log"); _SESSION_LOG_BEGIN(pLogger); + Timestamp ts = CURRENT_TIMESTAMP(); json jm = { {"type", "session_begin"}, - {"ts", getTimeStr()}, + {"ts", getTimeStr(ts)}, + {"ts_iso", getTimeIsoStr(ts)}, {"version", CAPTURE_VERSION_STRING}, {"appName", appName}, {"serial", targetVideoDev.serial}, {"vDev", targetVideoDev.name}, {"aDev", targetAudioInDev.alsaDeviceName}, {"start_ts", start_ts}, + {"start_ts_iso", getTimeIsoStr(tsStart)}, {"cx", cx}, {"cy", cy}, {"frameRate", frameRate} @@ -367,6 +377,7 @@ void VideoCaptureApp::startRecording(int cx, int cy, const std::string& frameRat outPath, outVideoFile, start_ts, + tsStart, pLogger, fRepromonEnabled, pRepromonQueue.get() // NOTE: unsafe ownership diff --git a/Capture/videocapture/src/VideoCapture.h b/Capture/videocapture/src/VideoCapture.h index 98aee68..9b0f66c 100644 --- a/Capture/videocapture/src/VideoCapture.h +++ b/Capture/videocapture/src/VideoCapture.h @@ -17,6 +17,7 @@ struct FfmpegParams { const std::string outPath; const std::string outVideoFile; const std::string start_ts; + const Timestamp tsStart; const SessionLogger_ptr pLogger; const bool fRepromonEnabled; RepromonQueue* pRepromonQueue; From eeb509e546ab12d4febaeaf08522d7141d67661f Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Fri, 21 Jun 2024 17:05:28 +0300 Subject: [PATCH 03/20] Specify parse_wQR.py requirements, #96 --- Parsing/requirements.txt | 6 +++++- README.md | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Parsing/requirements.txt b/Parsing/requirements.txt index 4c3b56a..f6fd30c 100644 --- a/Parsing/requirements.txt +++ b/Parsing/requirements.txt @@ -1 +1,5 @@ -pyzbar +pyzbar>=0.1.9 +opencv-python>=4.9.0.80 +numpy>=1.26.4 +click>=8.1.7 + diff --git a/README.md b/README.md index 56bc72c..51e8bca 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,11 @@ On Debian apt-get install -y ffmpeg libudev-dev libasound-dev libv4l-dev libyaml-cpp-dev libspdlog-dev catch2 v4l-utils libopencv-dev libcurl4-openssl-dev nlohmann-json3-dev cmake g++ +"Parsing/parse_wQR.py" script requires in zbar to be installed as well: + + apt-get install -y libzbar0 + + ## Build cd Capture From 5c4dd877b263a1ee385e873677860f9a50039e3c Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 24 Jun 2024 15:44:21 +0300 Subject: [PATCH 04/20] Tune up extend parse_wQR.py, added click skeleton, WiP, #96 --- Parsing/parse_wQR.py | 159 +++++++++++++++++++++++++++---------------- 1 file changed, 100 insertions(+), 59 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index ce49f93..6259f96 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -1,77 +1,118 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import json +import logging +import os + from pyzbar.pyzbar import decode, ZBarSymbol import cv2 import sys import numpy as np import time +import click -starttime = time.time() -cap = cv2.VideoCapture(sys.argv[1]) +# initialize the logger +logger = logging.getLogger(__name__) +logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) +logger.setLevel(logging.INFO) +logger.debug(f"name={__name__}") -# Check if the video opened successfully -if not cap.isOpened(): - print("Error: Could not open video.", file=sys.stderr) - sys.exit(1) -clips = [] -qrData = {} +def finalize_record(iframe): + global record + record['frame_end'] = iframe + record['time_end'] = 'TODO' + print(json.dumps(record), flush=True) + record = None -inRun = False -clip_start = None -acqNum = None -runNum = None +def do_parse(path_video: str): + starttime = time.time() + cap = cv2.VideoCapture(path_video) + # Check if the video opened successfully + if not cap.isOpened(): + print("Error: Could not open video.", file=sys.stderr) + sys.exit(1) -#for f in vid.iter_frames(with_times=True): + clips = [] + qrData = {} -# TODO: just use tqdm for progress indication -iframe = 0 -record = None + inRun = False -def finalize_record(): - global record - record['frame_end'] = iframe - record['time_end'] = 'TODO' - print(json.dumps(record), flush=True) + clip_start = None + acqNum = None + runNum = None + + + #for f in vid.iter_frames(with_times=True): + + # TODO: just use tqdm for progress indication + iframe = 0 record = None -while True: - iframe += 1 - ret, frame = cap.read() - if not ret: - break - - f = np.mean(frame, axis=2) # poor man greyscale from RGB - - if np.mod(iframe,50) == 0: - print(f"iframe={iframe} {np.std(f)}", file=sys.stderr) - -# if np.std(f) > 10: -# cv2.imwrite('grayscale_image.png', f) -# import pdb; pdb.set_trace() - - cod = decode(f, symbols=[ZBarSymbol.QRCODE]) - if len(cod) > 0: - assert len(cod) == 1, f"Expecting only one, got {len(cod)}" - data = eval(eval(str(cod[0].data)).decode('utf-8')) - if record is not None: - if data == record['data']: - # we are still in the same QR code record - continue - # It is a different QR code! we need to finalize current one - finalize_record() - # We just got beginning of the QR code! - record = { - 'frame_start': iframe, - 'time_start': "TODO-figureout", - 'data' : data - } - else: - if record: - finalize_record() - -if record: - finalize_record() + while True: + iframe += 1 + ret, frame = cap.read() + if not ret: + break + + f = np.mean(frame, axis=2) # poor man greyscale from RGB + + if np.mod(iframe,50) == 0: + print(f"iframe={iframe} {np.std(f)}", file=sys.stderr) + + # if np.std(f) > 10: + # cv2.imwrite('grayscale_image.png', f) + # import pdb; pdb.set_trace() + + cod = decode(f, symbols=[ZBarSymbol.QRCODE]) + if len(cod) > 0: + assert len(cod) == 1, f"Expecting only one, got {len(cod)}" + data = eval(eval(str(cod[0].data)).decode('utf-8')) + if record is not None: + if data == record['data']: + # we are still in the same QR code record + continue + # It is a different QR code! we need to finalize current one + finalize_record(iframe) + # We just got beginning of the QR code! + record = { + 'frame_start': iframe, + 'time_start': "TODO-figureout", + 'data' : data + } + else: + if record: + finalize_record(iframe) + + if record: + finalize_record(iframe) + + +@click.command(help='Utility to parse video and locate integrated ' + 'QR time codes.') +@click.argument('path', type=click.Path(exists=True)) +@click.option('--log-level', default='INFO', + type=click.Choice(['DEBUG', 'INFO', + 'WARNING', 'ERROR', + 'CRITICAL']), + help='Set the logging level') +@click.pass_context +def main(ctx, path: str, log_level): + logger.setLevel(log_level) + logger.debug("parse_wQR.py tool") + logger.debug(f"current dir: {os.getcwd()}") + logger.debug(f"path={path}") + + if not os.path.exists(path): + logger.error(f"Path does not exist: {path}") + return 1 + + do_parse(path) + return 0 + + +if __name__ == "__main__": + code = main() + From d71fd2fafb2ba94a197fac529b564515e6aa9ba8 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 24 Jun 2024 16:09:09 +0300 Subject: [PATCH 05/20] Tune up extend parse_wQR.py, added click skeleton, WiP, #96 --- Parsing/parse_wQR.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index 6259f96..215ba15 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -17,6 +17,7 @@ logger.setLevel(logging.INFO) logger.debug(f"name={__name__}") +record = None def finalize_record(iframe): global record @@ -26,14 +27,14 @@ def finalize_record(iframe): record = None -def do_parse(path_video: str): +def do_parse(path_video: str) -> int: starttime = time.time() cap = cv2.VideoCapture(path_video) # Check if the video opened successfully if not cap.isOpened(): - print("Error: Could not open video.", file=sys.stderr) - sys.exit(1) + logger.error("Error: Could not open video.") + return 1 clips = [] qrData = {} @@ -49,6 +50,7 @@ def do_parse(path_video: str): # TODO: just use tqdm for progress indication iframe = 0 + global record record = None while True: @@ -115,4 +117,5 @@ def main(ctx, path: str, log_level): if __name__ == "__main__": code = main() + sys.exit(code) From 2490db05a45ccb7e452ed98c902f0f668184c707 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Tue, 25 Jun 2024 16:36:45 +0300 Subject: [PATCH 06/20] Provide additional video info in parse_wQR.py logs, WiP #96 --- Parsing/parse_wQR.py | 46 +++++++++++++++++++++++++++++++--------- Parsing/requirements.txt | 1 + 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index 215ba15..5c71ff9 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -3,7 +3,7 @@ import json import logging import os - +from pydantic import BaseModel, Field from pyzbar.pyzbar import decode, ZBarSymbol import cv2 import sys @@ -17,13 +17,29 @@ logger.setLevel(logging.INFO) logger.debug(f"name={__name__}") +# Define the data model for the QR record +class QrRecord(BaseModel): + frame_start: int = Field(..., description="Frame number where QR code starts") + frame_end: int = Field(None, description="Frame number where QR code ends") + time_start: str = Field(..., description="Time where QR code starts") + time_end: str = Field(None, description="Time where QR code ends") + data: dict = Field(..., description="QR code data") + + def __str__(self): + return (f"QrRecord( frames=[{self.frame_start}...{self.frame_end}], " + f"time=[{self.time_start}..{self.time_end}], " + f"data={self.data})" + ) + + record = None + def finalize_record(iframe): global record record['frame_end'] = iframe record['time_end'] = 'TODO' - print(json.dumps(record), flush=True) + logger.info(f"QR: {json.dumps(record)}") record = None @@ -36,6 +52,15 @@ def do_parse(path_video: str) -> int: logger.error("Error: Could not open video.") return 1 + # dump video metadata + frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + 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.info(f"Video infomrmation: resolution={frame_width}x{frame_height}, fps={str(fps)}, " + f"frames count={str(frame_count)}, duration={str(duration_sec)} sec") + clips = [] qrData = {} @@ -45,7 +70,6 @@ def do_parse(path_video: str) -> int: acqNum = None runNum = None - #for f in vid.iter_frames(with_times=True): # TODO: just use tqdm for progress indication @@ -61,28 +85,31 @@ def do_parse(path_video: str) -> int: f = np.mean(frame, axis=2) # poor man greyscale from RGB - if np.mod(iframe,50) == 0: - print(f"iframe={iframe} {np.std(f)}", file=sys.stderr) + if np.mod(iframe, 50) == 0: + logger.info(f"iframe={iframe} {np.std(f)}") - # if np.std(f) > 10: - # cv2.imwrite('grayscale_image.png', f) - # import pdb; pdb.set_trace() + # if np.std(f) > 10: + # cv2.imwrite('grayscale_image.png', f) + # import pdb; pdb.set_trace() cod = decode(f, symbols=[ZBarSymbol.QRCODE]) if len(cod) > 0: + logger.debug("Found QR code: " + str(cod)); assert len(cod) == 1, f"Expecting only one, got {len(cod)}" data = eval(eval(str(cod[0].data)).decode('utf-8')) if record is not None: if data == record['data']: # we are still in the same QR code record + logger.debug(f"Same QR code: continue") continue # It is a different QR code! we need to finalize current one finalize_record(iframe) # We just got beginning of the QR code! + logger.debug("New QR code: " + str(data)) record = { 'frame_start': iframe, 'time_start': "TODO-figureout", - 'data' : data + 'data': data } else: if record: @@ -118,4 +145,3 @@ def main(ctx, path: str, log_level): if __name__ == "__main__": code = main() sys.exit(code) - diff --git a/Parsing/requirements.txt b/Parsing/requirements.txt index f6fd30c..442e9d2 100644 --- a/Parsing/requirements.txt +++ b/Parsing/requirements.txt @@ -2,4 +2,5 @@ pyzbar>=0.1.9 opencv-python>=4.9.0.80 numpy>=1.26.4 click>=8.1.7 +pydantic>=2.7.1 From 56ad2a61f719e04604b7232180ac362dcc8bbece Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Tue, 25 Jun 2024 16:49:58 +0300 Subject: [PATCH 07/20] Make QR record model more strict with pydantic based class, WiP #96 --- Parsing/parse_wQR.py | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index 5c71ff9..c669173 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -17,6 +17,7 @@ logger.setLevel(logging.INFO) logger.debug(f"name={__name__}") + # Define the data model for the QR record class QrRecord(BaseModel): frame_start: int = Field(..., description="Frame number where QR code starts") @@ -26,21 +27,17 @@ class QrRecord(BaseModel): data: dict = Field(..., description="QR code data") def __str__(self): - return (f"QrRecord( frames=[{self.frame_start}...{self.frame_end}], " + return (f"QrRecord(frames=[{self.frame_start}...{self.frame_end}], " f"time=[{self.time_start}..{self.time_end}], " f"data={self.data})" ) -record = None - - -def finalize_record(iframe): - global record - record['frame_end'] = iframe - record['time_end'] = 'TODO' - logger.info(f"QR: {json.dumps(record)}") - record = None +def finalize_record(record: QrRecord, iframe: int) -> QrRecord: + record.frame_end = iframe + record.time_end = 'TODO' + logger.info(f"QR: {str(record)}") + return None def do_parse(path_video: str) -> int: @@ -58,7 +55,7 @@ def do_parse(path_video: str) -> int: 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.info(f"Video infomrmation: resolution={frame_width}x{frame_height}, fps={str(fps)}, " + logger.info(f"Video information: resolution={frame_width}x{frame_height}, fps={str(fps)}, " f"frames count={str(frame_count)}, duration={str(duration_sec)} sec") clips = [] @@ -73,9 +70,8 @@ def do_parse(path_video: str) -> int: #for f in vid.iter_frames(with_times=True): # TODO: just use tqdm for progress indication - iframe = 0 - global record - record = None + iframe: int = 0 + record: QrRecord = None while True: iframe += 1 @@ -98,25 +94,24 @@ def do_parse(path_video: str) -> int: assert len(cod) == 1, f"Expecting only one, got {len(cod)}" data = eval(eval(str(cod[0].data)).decode('utf-8')) if record is not None: - if data == record['data']: + if data == record.data: # we are still in the same QR code record logger.debug(f"Same QR code: continue") continue # It is a different QR code! we need to finalize current one - finalize_record(iframe) + record = finalize_record(record, iframe) # We just got beginning of the QR code! logger.debug("New QR code: " + str(data)) - record = { - 'frame_start': iframe, - 'time_start': "TODO-figureout", - 'data': data - } + record = QrRecord(frame_start=iframe, frame_end=-1, + time_start="TODO-figureout", + time_end="", + data=data) else: if record: - finalize_record(iframe) + record = finalize_record(record, iframe) if record: - finalize_record(iframe) + record = finalize_record(record, iframe) @click.command(help='Utility to parse video and locate integrated ' From aa07fdadcb6076aae95cb49c57f620d14a8e699b Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Tue, 25 Jun 2024 17:03:19 +0300 Subject: [PATCH 08/20] Extract video time info based on reprostim capture filename format, WiP #96 --- Parsing/parse_wQR.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index c669173..fc02cfa 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -3,6 +3,8 @@ import json import logging import os +from datetime import datetime + from pydantic import BaseModel, Field from pyzbar.pyzbar import decode, ZBarSymbol import cv2 @@ -18,6 +20,12 @@ logger.debug(f"name={__name__}") +# Define class for video time info +class VideoTimeInfo(BaseModel): + start_time: datetime = Field(..., description="Start time of the video") + end_time: datetime = Field(..., description="End time of the video") + + # Define the data model for the QR record class QrRecord(BaseModel): frame_start: int = Field(..., description="Frame number where QR code starts") From 0782d8c7f71e80ac64445a00db9ce9569d760e28 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Thu, 27 Jun 2024 13:09:08 +0300 Subject: [PATCH 09/20] Parse expected video duration based on captured *.mkv file name, WiP #96 --- Parsing/parse_wQR.py | 78 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index fc02cfa..d85243f 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -3,7 +3,9 @@ import json import logging import os +import re from datetime import datetime +from typing import Optional from pydantic import BaseModel, Field from pyzbar.pyzbar import decode, ZBarSymbol @@ -16,14 +18,19 @@ # initialize the logger logger = logging.getLogger(__name__) logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) -logger.setLevel(logging.INFO) +stm_error = logging.StreamHandler(sys.stderr) +stm_error.setLevel(logging.ERROR) +logging.getLogger().addHandler(stm_error) logger.debug(f"name={__name__}") # Define class for video time info class VideoTimeInfo(BaseModel): - start_time: datetime = Field(..., description="Start time of the video") - end_time: datetime = Field(..., description="End time of the video") + success: bool = Field(..., description="Success flag") + error: Optional[str] = Field(None, description="Error message if any") + start_time: Optional[datetime] = Field(None, description="Start time of the video") + end_time: Optional[datetime] = Field(None, description="End time of the video") + duration_sec: Optional[float] = Field(None, description="Duration of the video in seconds") # Define the data model for the QR record @@ -41,6 +48,47 @@ def __str__(self): ) +def get_video_time_info(path_video: str) -> VideoTimeInfo: + res: VideoTimeInfo = VideoTimeInfo(success=False, error=None, + start_time=None, end_time=None) + # Define the regex pattern for the timestamp and file extension (either .mkv or .mp4) + pattern = (r'^(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})' + r'_(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})\.(mkv|mp4)$') + + file_name: str = os.path.basename(path_video) + logger.debug(f"Video file name : {file_name}") + + match = re.match(pattern, file_name) + if not match: + res.error = "Filename does not match the required pattern." + return res + + start_ts, end_ts, extension = match.groups() + + # Define the format for datetime parsing + ts_format = "%Y.%m.%d.%H.%M.%S.%f" + + try: + # Parse the timestamps + res.start_time = datetime.strptime(start_ts, ts_format) + res.end_time = datetime.strptime(end_ts, ts_format) + except ValueError as e: + res.error = f"Timestamp parsing error: {e}" + return res + + # Validate the chronological order + if res.start_time >= res.end_time: + res.error = "Start timestamp is not earlier than end timestamp." + return res + + # calculate the duration in seconds + dt: float = (res.end_time - res.start_time).total_seconds() + res.duration_sec = dt + + res.success = True + return res + + def finalize_record(record: QrRecord, iframe: int) -> QrRecord: record.frame_end = iframe record.time_end = 'TODO' @@ -49,6 +97,15 @@ def finalize_record(record: QrRecord, iframe: int) -> QrRecord: def do_parse(path_video: str) -> int: + vti: VideoTimeInfo = get_video_time_info(path_video) + if not vti.success: + logger.error(f"Failed parse file name time patter, error: {vti.error}") + return 1 + + logger.debug(f"Video start time : {vti.start_time}") + logger.debug(f"Video end time : {vti.end_time}") + logger.debug(f"Video duration : {vti.duration_sec} sec") + starttime = time.time() cap = cv2.VideoCapture(path_video) @@ -63,8 +120,15 @@ def do_parse(path_video: str) -> int: 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.info(f"Video information: resolution={frame_width}x{frame_height}, fps={str(fps)}, " - f"frames count={str(frame_count)}, duration={str(duration_sec)} sec") + logger.info(f"Video media info : ") + logger.info(f" - resolution : {frame_width}x{frame_height}") + logger.info(f" - frame rate : {str(fps)} FPS") + logger.info(f" - duration : {str(duration_sec)} sec") + logger.info(f" - frame count: {str(frame_count)}") + + if abs(duration_sec - vti.duration_sec) > 120.0: + logger.error(f"Video duration significant mismatch (real/file name):" + f" {duration_sec} sec vs {vti.duration_sec} sec") clips = [] qrData = {} @@ -134,8 +198,8 @@ def do_parse(path_video: str) -> int: def main(ctx, path: str, log_level): logger.setLevel(log_level) logger.debug("parse_wQR.py tool") - logger.debug(f"current dir: {os.getcwd()}") - logger.debug(f"path={path}") + logger.debug(f"Working dir : {os.getcwd()}") + logger.debug(f"Video full path : {path}") if not os.path.exists(path): logger.error(f"Path does not exist: {path}") From cddb6de884d3368c81bdff20cd77d274dd628ac7 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Thu, 27 Jun 2024 13:58:23 +0300 Subject: [PATCH 10/20] Calculate QR code time based on start time from file name and code position in video, WiP #96 --- Parsing/parse_wQR.py | 84 +++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index d85243f..fcd8655 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -4,7 +4,7 @@ import logging import os import re -from datetime import datetime +from datetime import datetime, timedelta from typing import Optional from pydantic import BaseModel, Field @@ -30,33 +30,50 @@ class VideoTimeInfo(BaseModel): error: Optional[str] = Field(None, description="Error message if any") start_time: Optional[datetime] = Field(None, description="Start time of the video") end_time: Optional[datetime] = Field(None, description="End time of the video") - duration_sec: Optional[float] = Field(None, description="Duration of the video in seconds") + duration_sec: Optional[float] = Field(None, description="Duration of the video " + "in seconds") # Define the data model for the QR record class QrRecord(BaseModel): - frame_start: int = Field(..., description="Frame number where QR code starts") + frame_start: int = Field(None, description="Frame number where QR code starts") frame_end: int = Field(None, description="Frame number where QR code ends") - time_start: str = Field(..., description="Time where QR code starts") - time_end: str = Field(None, description="Time where QR code ends") - data: dict = Field(..., description="QR code data") + start_time: Optional[str] = Field(None, description="Time where QR code starts") + end_time: Optional[str] = Field(None, description="Time where QR code ends") + start_pos_sec: Optional[float] = Field(None, description="Position in seconds " + "where QR code starts") + end_pos_sec: Optional[float] = Field(None, description="Position in seconds " + "where QR code ends") + data: Optional[dict] = Field(None, description="QR code data") def __str__(self): - return (f"QrRecord(frames=[{self.frame_start}...{self.frame_end}], " - f"time=[{self.time_start}..{self.time_end}], " + return (f"QrRecord(frames=[{self.frame_start}, {self.frame_end}], " + f"pos=[{self.start_pos_sec}, {self.end_pos_sec} sec], " + f"start_time={self.start_time}, end_time={self.end_time}], " f"data={self.data})" ) +def calc_time(ts: datetime, pos_sec: float) -> datetime: + return ts + timedelta(seconds=pos_sec) + + +def get_iso_time(ts: str) -> datetime: + dt: datetime = datetime.fromisoformat(ts) + dt = dt.replace(tzinfo=None) + return dt + + def get_video_time_info(path_video: str) -> VideoTimeInfo: res: VideoTimeInfo = VideoTimeInfo(success=False, error=None, start_time=None, end_time=None) - # Define the regex pattern for the timestamp and file extension (either .mkv or .mp4) + # Define the regex pattern for the timestamp and file extension + # (either .mkv or .mp4) pattern = (r'^(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})' r'_(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})\.(mkv|mp4)$') file_name: str = os.path.basename(path_video) - logger.debug(f"Video file name : {file_name}") + logger.info(f"Video file name : {file_name}") match = re.match(pattern, file_name) if not match: @@ -89,10 +106,24 @@ def get_video_time_info(path_video: str) -> VideoTimeInfo: return res -def finalize_record(record: QrRecord, iframe: int) -> QrRecord: +def finalize_record(vti: VideoTimeInfo, + record: QrRecord, iframe: int, + pos_sec: float) -> QrRecord: record.frame_end = iframe - record.time_end = 'TODO' + # Note: unclear should we also use last frame duration or not + record.end_time = calc_time(vti.start_time, pos_sec) + record.end_pos_sec = pos_sec logger.info(f"QR: {str(record)}") + # dump times + event_time = get_iso_time(record.data['time_formatted']) + keys_time = get_iso_time(record.data['keys_time_str']) + logger.info(f" - QR code time : {record.start_time}") + logger.info(f" - Event time : " + f"{event_time} / " + f"dt={(event_time - record.start_time).total_seconds()} sec") + logger.info(f" - Keys time : " + f"{keys_time} / " + f"dt={(keys_time - record.start_time).total_seconds()} sec") return None @@ -102,9 +133,9 @@ def do_parse(path_video: str) -> int: logger.error(f"Failed parse file name time patter, error: {vti.error}") return 1 - logger.debug(f"Video start time : {vti.start_time}") - logger.debug(f"Video end time : {vti.end_time}") - logger.debug(f"Video duration : {vti.duration_sec} sec") + logger.info(f"Video start time : {vti.start_time}") + logger.info(f"Video end time : {vti.end_time}") + logger.info(f"Video duration : {vti.duration_sec} sec") starttime = time.time() cap = cv2.VideoCapture(path_video) @@ -143,10 +174,13 @@ def do_parse(path_video: str) -> int: # TODO: just use tqdm for progress indication iframe: int = 0 + pos_sec: float = 0.0 record: QrRecord = None while True: iframe += 1 + # pos time in ms + pos_sec = round((iframe-1) / fps, 3) ret, frame = cap.read() if not ret: break @@ -154,7 +188,7 @@ def do_parse(path_video: str) -> int: f = np.mean(frame, axis=2) # poor man greyscale from RGB if np.mod(iframe, 50) == 0: - logger.info(f"iframe={iframe} {np.std(f)}") + logger.debug(f"iframe={iframe} {np.std(f)}") # if np.std(f) > 10: # cv2.imwrite('grayscale_image.png', f) @@ -171,19 +205,21 @@ def do_parse(path_video: str) -> int: logger.debug(f"Same QR code: continue") continue # It is a different QR code! we need to finalize current one - record = finalize_record(record, iframe) + record = finalize_record(vti, record, iframe, pos_sec) # We just got beginning of the QR code! logger.debug("New QR code: " + str(data)) - record = QrRecord(frame_start=iframe, frame_end=-1, - time_start="TODO-figureout", - time_end="", - data=data) + record = QrRecord() + record.frame_start = iframe + record.start_time = calc_time(vti.start_time, pos_sec) + record.end_time = "" + record.start_pos_sec = pos_sec + record.data = data else: if record: - record = finalize_record(record, iframe) + record = finalize_record(vti, record, iframe, pos_sec) if record: - record = finalize_record(record, iframe) + record = finalize_record(vti, record, iframe, pos_sec) @click.command(help='Utility to parse video and locate integrated ' @@ -199,7 +235,7 @@ def main(ctx, path: str, log_level): logger.setLevel(log_level) logger.debug("parse_wQR.py tool") logger.debug(f"Working dir : {os.getcwd()}") - logger.debug(f"Video full path : {path}") + logger.info(f"Video full path : {path}") if not os.path.exists(path): logger.error(f"Path does not exist: {path}") From 6b2b58fc51900a9e9c2a1b041f32708319b3d00e Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 15 Jul 2024 14:32:28 +0300 Subject: [PATCH 11/20] Adjust time format for filenames?? #98 --- Capture/capturelib/src/CaptureLib.cpp | 2 +- Capture/version.txt | 2 +- Capture/videocapture/src/VideoCapture.cpp | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Capture/capturelib/src/CaptureLib.cpp b/Capture/capturelib/src/CaptureLib.cpp index 8c51f78..d5d4cc5 100644 --- a/Capture/capturelib/src/CaptureLib.cpp +++ b/Capture/capturelib/src/CaptureLib.cpp @@ -425,7 +425,7 @@ namespace reprostim { std::stringstream ss; ss << 1900 + ltm->tm_year << '.' << std::setw(2) << std::setfill('0') << std::to_string(1 + ltm->tm_mon) << '.' - << std::setw(2) << std::setfill('0') << std::to_string(ltm->tm_mday) << '.' + << std::setw(2) << std::setfill('0') << std::to_string(ltm->tm_mday) << '-' << std::setw(2) << std::setfill('0') << std::to_string(ltm->tm_hour) << '.' << std::setw(2) << std::setfill('0') << std::to_string(ltm->tm_min) << '.' << std::setw(2) << std::setfill('0') << std::to_string(ltm->tm_sec) << '.' diff --git a/Capture/version.txt b/Capture/version.txt index 00c64bb..f08a237 100644 --- a/Capture/version.txt +++ b/Capture/version.txt @@ -1 +1 @@ -1.10.0.254 \ No newline at end of file +1.11.0.257 \ No newline at end of file diff --git a/Capture/videocapture/src/VideoCapture.cpp b/Capture/videocapture/src/VideoCapture.cpp index d772eec..6eba457 100644 --- a/Capture/videocapture/src/VideoCapture.cpp +++ b/Capture/videocapture/src/VideoCapture.cpp @@ -93,7 +93,7 @@ std::string renameVideoFile( const std::string& out_fmt, const std::string& message) { std::string stop_ts = getTimeStr(); - std::string outVideoFile2 = buildVideoFile(outPath, start_ts + "_" + stop_ts, out_fmt); + std::string outVideoFile2 = buildVideoFile(outPath, start_ts + "--" + stop_ts, out_fmt); if( std::filesystem::exists(outVideoFile) ) { _INFO(message << " Saving video " << outVideoFile2); rename(outVideoFile.c_str(), outVideoFile2.c_str()); @@ -315,7 +315,7 @@ void VideoCaptureApp::startRecording(int cx, int cy, const std::string& frameRat const FfmpegOpts& opts = cfg.ffm_opts; std::string a_dev2 = a_dev; if( a_dev2.find("-i ")!=0 ) a_dev2 = "-i " + a_dev2; - std::string outVideoFile = buildVideoFile(outPath, start_ts + "_", opts.out_fmt); + std::string outVideoFile = buildVideoFile(outPath, start_ts + "--", opts.out_fmt); sprintf( ffmpg, "ffmpeg %s %s %s %s %s -framerate %s -video_size %ix%i %s -i %s " @@ -390,7 +390,7 @@ void VideoCaptureApp::stopRecording(const std::string& start_ts, const std::string& vpath, const std::string& message) { std::string out_fmt = cfg.ffm_opts.out_fmt; - std::string oldname = buildVideoFile(vpath, start_ts + "_", out_fmt); + std::string oldname = buildVideoFile(vpath, start_ts + "--", out_fmt); _INFO("stop record says: " << "terminating ffmpeg with SIGINT"); if( !killProc("ffmpeg", SIGINT, 5, false) ) { From a8a7c7adb805e3a5afd173f2da70ff4ea5ed217b Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 15 Jul 2024 14:38:23 +0300 Subject: [PATCH 12/20] Adjust time format for filenames?? #98 --- Capture/capturelib/test/TestCaptureLib.cpp | 2 +- Capture/version.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Capture/capturelib/test/TestCaptureLib.cpp b/Capture/capturelib/test/TestCaptureLib.cpp index e988b2e..2ce8bb0 100644 --- a/Capture/capturelib/test/TestCaptureLib.cpp +++ b/Capture/capturelib/test/TestCaptureLib.cpp @@ -45,7 +45,7 @@ TEST_CASE("TestCaptureLib_getTimeStr", INFO("ts: " << ts); REQUIRE(ts.length() == 23); - std::regex pattern(R"(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})"); + std::regex pattern(R"(\d{4}\.\d{2}\.\d{2}\-\d{2}\.\d{2}\.\d{2}\.\d{3})"); std::smatch match; ts = getTimeStr(); diff --git a/Capture/version.txt b/Capture/version.txt index f08a237..72fd1c7 100644 --- a/Capture/version.txt +++ b/Capture/version.txt @@ -1 +1 @@ -1.11.0.257 \ No newline at end of file +1.11.0.258 \ No newline at end of file From 8d19d5e6be85422c4bb9252b0f829c8700df68b1 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 15 Jul 2024 14:50:40 +0300 Subject: [PATCH 13/20] Adjust time format for filenames??, #98 --- Parsing/parse_wQR.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index fcd8655..704b146 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -5,6 +5,7 @@ import os import re from datetime import datetime, timedelta +from re import match from typing import Optional from pydantic import BaseModel, Field @@ -69,21 +70,31 @@ def get_video_time_info(path_video: str) -> VideoTimeInfo: start_time=None, end_time=None) # Define the regex pattern for the timestamp and file extension # (either .mkv or .mp4) - pattern = (r'^(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})' + pattern1 = (r'^(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})' r'_(\d{4}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{2}\.\d{3})\.(mkv|mp4)$') + # add support for new file format + pattern2 = (r'^(\d{4}\.\d{2}\.\d{2}\-\d{2}\.\d{2}\.\d{2}\.\d{3})' + r'\-\-(\d{4}\.\d{2}\.\d{2}\-\d{2}\.\d{2}\.\d{2}\.\d{3})\.(mkv|mp4)$') + file_name: str = os.path.basename(path_video) logger.info(f"Video file name : {file_name}") - match = re.match(pattern, file_name) - if not match: - res.error = "Filename does not match the required pattern." - return res - - start_ts, end_ts, extension = match.groups() + ts_format = "" + match1: str = re.match(pattern1, file_name) + if match1: + # Define the format for datetime parsing + ts_format = "%Y.%m.%d.%H.%M.%S.%f" + else: + match1 = re.match(pattern2, file_name) + if match1: + # Define the format for datetime parsing + ts_format = "%Y.%m.%d-%H.%M.%S.%f" + else: + res.error = "Filename does not match the required pattern." + return res - # Define the format for datetime parsing - ts_format = "%Y.%m.%d.%H.%M.%S.%f" + start_ts, end_ts, extension = match1.groups() try: # Parse the timestamps From 900b99f399649318a23b99e75b6602667b2b5464 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 15 Jul 2024 15:17:29 +0300 Subject: [PATCH 14/20] Uniform names to end with _start/_end and to have common types like isotime, time ts etc, #102 --- Parsing/parse_wQR.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index 704b146..74d5b2f 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -39,18 +39,23 @@ class VideoTimeInfo(BaseModel): class QrRecord(BaseModel): frame_start: int = Field(None, description="Frame number where QR code starts") frame_end: int = Field(None, description="Frame number where QR code ends") - start_time: Optional[str] = Field(None, description="Time where QR code starts") - end_time: Optional[str] = Field(None, description="Time where QR code ends") - start_pos_sec: Optional[float] = Field(None, description="Position in seconds " + isotime_start: Optional[str] = Field(None, description="ISO datetime where QR " + "code starts") + isotime_end: Optional[str] = Field(None, description="ISO datetime where QR " + "code ends") + time_start: Optional[float] = Field(None, description="Position in seconds " "where QR code starts") - end_pos_sec: Optional[float] = Field(None, description="Position in seconds " + time_end: Optional[float] = Field(None, description="Position in seconds " "where QR code ends") + duration: Optional[float] = Field(None, description="Duration of the QR code " + "in seconds") data: Optional[dict] = Field(None, description="QR code data") def __str__(self): return (f"QrRecord(frames=[{self.frame_start}, {self.frame_end}], " - f"pos=[{self.start_pos_sec}, {self.end_pos_sec} sec], " - f"start_time={self.start_time}, end_time={self.end_time}], " + f"times=[{self.time_start}, {self.time_end} sec], " + f"duration={self.duration} sec, " + f"isotimes=[{self.isotime_start}, {self.isotime_end}], " f"data={self.data})" ) @@ -122,19 +127,20 @@ def finalize_record(vti: VideoTimeInfo, pos_sec: float) -> QrRecord: record.frame_end = iframe # Note: unclear should we also use last frame duration or not - record.end_time = calc_time(vti.start_time, pos_sec) - record.end_pos_sec = pos_sec + record.isotime_end = calc_time(vti.start_time, pos_sec) + record.time_end = pos_sec + record.duration = record.time_end - record.time_start logger.info(f"QR: {str(record)}") # dump times event_time = get_iso_time(record.data['time_formatted']) keys_time = get_iso_time(record.data['keys_time_str']) - logger.info(f" - QR code time : {record.start_time}") - logger.info(f" - Event time : " + logger.info(f" - QR code isotime : {record.isotime_start}") + logger.info(f" - Event isotime : " f"{event_time} / " - f"dt={(event_time - record.start_time).total_seconds()} sec") - logger.info(f" - Keys time : " + f"dt={(event_time - record.isotime_start).total_seconds()} sec") + logger.info(f" - Keys isotime : " f"{keys_time} / " - f"dt={(keys_time - record.start_time).total_seconds()} sec") + f"dt={(keys_time - record.isotime_start).total_seconds()} sec") return None @@ -221,9 +227,9 @@ def do_parse(path_video: str) -> int: logger.debug("New QR code: " + str(data)) record = QrRecord() record.frame_start = iframe - record.start_time = calc_time(vti.start_time, pos_sec) - record.end_time = "" - record.start_pos_sec = pos_sec + record.isotime_start = calc_time(vti.start_time, pos_sec) + record.isotime_end = "" + record.time_start = pos_sec record.data = data else: if record: From cc8b033ad947e5b35515cc4513d468d290a21063 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 15 Jul 2024 15:41:34 +0300 Subject: [PATCH 15/20] Restore utility behaviour when QR codes json goes to stdout, but other logs and additional info to stderr, #96 --- Parsing/parse_wQR.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index 74d5b2f..c227c92 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -17,11 +17,9 @@ import click # initialize the logger +# Note: all logs goes to stderr logger = logging.getLogger(__name__) -logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) -stm_error = logging.StreamHandler(sys.stderr) -stm_error.setLevel(logging.ERROR) -logging.getLogger().addHandler(stm_error) +logging.getLogger().addHandler(logging.StreamHandler(sys.stderr)) logger.debug(f"name={__name__}") @@ -39,9 +37,9 @@ class VideoTimeInfo(BaseModel): class QrRecord(BaseModel): frame_start: int = Field(None, description="Frame number where QR code starts") frame_end: int = Field(None, description="Frame number where QR code ends") - isotime_start: Optional[str] = Field(None, description="ISO datetime where QR " + isotime_start: Optional[datetime] = Field(None, description="ISO datetime where QR " "code starts") - isotime_end: Optional[str] = Field(None, description="ISO datetime where QR " + isotime_end: Optional[datetime] = Field(None, description="ISO datetime where QR " "code ends") time_start: Optional[float] = Field(None, description="Position in seconds " "where QR code starts") @@ -141,6 +139,7 @@ def finalize_record(vti: VideoTimeInfo, logger.info(f" - Keys isotime : " f"{keys_time} / " f"dt={(keys_time - record.isotime_start).total_seconds()} sec") + print(record.json()) return None @@ -228,7 +227,7 @@ def do_parse(path_video: str) -> int: record = QrRecord() record.frame_start = iframe record.isotime_start = calc_time(vti.start_time, pos_sec) - record.isotime_end = "" + record.isotime_end = None record.time_start = pos_sec record.data = data else: From 7dc132190b4cc2306d8267282a34a635ec7bc1bb Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 15 Jul 2024 16:12:02 +0300 Subject: [PATCH 16/20] Added JSON record at the end of parsing with summary info like duration, video info etc, #96 --- Parsing/parse_wQR.py | 53 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index c227c92..0fb57c0 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -32,6 +32,32 @@ class VideoTimeInfo(BaseModel): duration_sec: Optional[float] = Field(None, description="Duration of the video " "in seconds") +# Define model for parsing summary info +class ParseSummary(BaseModel): + qr_count: Optional[int] = Field(0, description="Number of QR codes found") + parsing_duration: Optional[float] = Field(0.0, description="Duration of the " + "parsing in seconds") + # exit code + exit_code: Optional[int] = Field(-1, description="Number of QR codes found") + video_full_path: Optional[str] = Field(None, description="Full path " + "to the video file") + video_file_name: Optional[str] = Field(None, description="Name of the " + "video file") + video_isotime_start: Optional[datetime] = Field(None, description="ISO datetime " + "video started") + video_isotime_end: Optional[datetime] = Field(None, description="ISO datetime " + "video ended") + video_duration: Optional[float] = Field(None, description="Duration of the video " + "in seconds") + video_frame_width: Optional[int] = Field(None, description="Width of the " + "video frame in px") + video_frame_height: Optional[int] = Field(None, description="Height of the " + "video frame in px") + video_frame_rate: Optional[float] = Field(None, description="Frame rate of the " + "video in FPS") + video_frame_count: Optional[int] = Field(None, description="Number of frames " + "in video file") + # Define the data model for the QR record class QrRecord(BaseModel): @@ -120,7 +146,8 @@ def get_video_time_info(path_video: str) -> VideoTimeInfo: return res -def finalize_record(vti: VideoTimeInfo, +def finalize_record(ps: ParseSummary, + vti: VideoTimeInfo, record: QrRecord, iframe: int, pos_sec: float) -> QrRecord: record.frame_end = iframe @@ -140,10 +167,13 @@ def finalize_record(vti: VideoTimeInfo, f"{keys_time} / " f"dt={(keys_time - record.isotime_start).total_seconds()} sec") print(record.json()) + ps.qr_count += 1 return None def do_parse(path_video: str) -> int: + ps: ParseSummary = ParseSummary() + vti: VideoTimeInfo = get_video_time_info(path_video) if not vti.success: logger.error(f"Failed parse file name time patter, error: {vti.error}") @@ -153,7 +183,7 @@ def do_parse(path_video: str) -> int: logger.info(f"Video end time : {vti.end_time}") logger.info(f"Video duration : {vti.duration_sec} sec") - starttime = time.time() + dt = time.time() cap = cv2.VideoCapture(path_video) # Check if the video opened successfully @@ -172,6 +202,15 @@ def do_parse(path_video: str) -> int: logger.info(f" - frame rate : {str(fps)} FPS") logger.info(f" - duration : {str(duration_sec)} sec") logger.info(f" - frame count: {str(frame_count)}") + ps.video_frame_rate = fps + ps.video_frame_width = frame_width + ps.video_frame_height = frame_height + ps.video_frame_count = frame_count + ps.video_duration = duration_sec + ps.video_isotime_start = vti.start_time + ps.video_isotime_end = vti.end_time + ps.video_full_path = path_video + ps.video_file_name = os.path.basename(path_video) if abs(duration_sec - vti.duration_sec) > 120.0: logger.error(f"Video duration significant mismatch (real/file name):" @@ -221,7 +260,7 @@ def do_parse(path_video: str) -> int: logger.debug(f"Same QR code: continue") continue # It is a different QR code! we need to finalize current one - record = finalize_record(vti, record, iframe, pos_sec) + record = finalize_record(ps, vti, record, iframe, pos_sec) # We just got beginning of the QR code! logger.debug("New QR code: " + str(data)) record = QrRecord() @@ -232,10 +271,14 @@ def do_parse(path_video: str) -> int: record.data = data else: if record: - record = finalize_record(vti, record, iframe, pos_sec) + record = finalize_record(ps, vti, record, iframe, pos_sec) if record: - record = finalize_record(vti, record, iframe, pos_sec) + record = finalize_record(ps, vti, record, iframe, pos_sec) + + ps.exit_code = 0 + ps.parsing_duration = round(time.time() - dt, 1) + print(ps.json()) @click.command(help='Utility to parse video and locate integrated ' From 67b55d3ac986b102b62cf3c95e7450117bd3ba8b Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Mon, 15 Jul 2024 16:27:41 +0300 Subject: [PATCH 17/20] Make "do_parse" as generator functions, so it can be used to work with QR codes realtime later by external scripts, #96 --- Parsing/parse_wQR.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index 0fb57c0..f1a9449 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -166,18 +166,18 @@ def finalize_record(ps: ParseSummary, logger.info(f" - Keys isotime : " f"{keys_time} / " f"dt={(keys_time - record.isotime_start).total_seconds()} sec") - print(record.json()) + #print(record.json()) ps.qr_count += 1 - return None + return record -def do_parse(path_video: str) -> int: +def do_parse(path_video: str): ps: ParseSummary = ParseSummary() vti: VideoTimeInfo = get_video_time_info(path_video) if not vti.success: logger.error(f"Failed parse file name time patter, error: {vti.error}") - return 1 + return logger.info(f"Video start time : {vti.start_time}") logger.info(f"Video end time : {vti.end_time}") @@ -189,7 +189,7 @@ def do_parse(path_video: str) -> int: # Check if the video opened successfully if not cap.isOpened(): logger.error("Error: Could not open video.") - return 1 + return # dump video metadata frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -260,7 +260,8 @@ def do_parse(path_video: str) -> int: logger.debug(f"Same QR code: continue") continue # It is a different QR code! we need to finalize current one - record = finalize_record(ps, vti, record, iframe, pos_sec) + yield finalize_record(ps, vti, record, iframe, pos_sec) + record = None # We just got beginning of the QR code! logger.debug("New QR code: " + str(data)) record = QrRecord() @@ -271,14 +272,17 @@ def do_parse(path_video: str) -> int: record.data = data else: if record: - record = finalize_record(ps, vti, record, iframe, pos_sec) + yield finalize_record(ps, vti, record, iframe, pos_sec) + record = None if record: - record = finalize_record(ps, vti, record, iframe, pos_sec) + yield finalize_record(ps, vti, record, iframe, pos_sec) + record = None ps.exit_code = 0 ps.parsing_duration = round(time.time() - dt, 1) - print(ps.json()) + yield ps + #print(ps.json()) @click.command(help='Utility to parse video and locate integrated ' @@ -300,7 +304,8 @@ def main(ctx, path: str, log_level): logger.error(f"Path does not exist: {path}") return 1 - do_parse(path) + for item in do_parse(path): + print(item.json()) return 0 From 5dc7425d448145bada4dfa4ad923fdcf315b0c55 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Tue, 16 Jul 2024 16:31:49 +0300 Subject: [PATCH 18/20] Normalize videocapture metadata JSON logs according to this suggestion: https://github.com/ReproNim/reprostim/issues/99#issuecomment-2221090283, #99 --- Capture/version.txt | 2 +- Capture/videocapture/src/VideoCapture.cpp | 28 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Capture/version.txt b/Capture/version.txt index 72fd1c7..bff3477 100644 --- a/Capture/version.txt +++ b/Capture/version.txt @@ -1 +1 @@ -1.11.0.258 \ No newline at end of file +1.11.0.261 \ No newline at end of file diff --git a/Capture/videocapture/src/VideoCapture.cpp b/Capture/videocapture/src/VideoCapture.cpp index 6eba457..766c1b0 100644 --- a/Capture/videocapture/src/VideoCapture.cpp +++ b/Capture/videocapture/src/VideoCapture.cpp @@ -138,11 +138,11 @@ void FfmpegThread ::run() { Timestamp ts = CURRENT_TIMESTAMP(); json jm = { {"type", "session_end"}, - {"ts", getTimeStr(ts)}, - {"ts_iso", getTimeIsoStr(ts)}, + {"json_ts", getTimeStr(ts)}, + {"json_isotime", getTimeIsoStr(ts)}, {"message", "ffmpeg thread terminated"}, - {"start_ts", getParams().start_ts}, - {"start_ts_iso", getTimeIsoStr(getParams().tsStart)} + {"cap_ts_start", getParams().start_ts}, + {"cap_isotime_start", getTimeIsoStr(getParams().tsStart)} }; _METADATA_LOG(jm); _SESSION_LOG_END_CLOSE_RENAME(outVideoFile2 + ".log"); @@ -187,13 +187,13 @@ void VideoCaptureApp::onCaptureStop(const std::string& message) { Timestamp ts = CURRENT_TIMESTAMP(); json jm = { {"type", "capture_stop"}, - {"ts", getTimeStr(ts)}, - {"ts_iso", getTimeIsoStr(ts)}, + {"json_ts", getTimeStr(ts)}, + {"json_isotime", getTimeIsoStr(ts)}, {"message", message}, - {"start_ts", start_ts}, - {"start_ts_iso", getTimeIsoStr(tsStart)}, - {"stop_ts", stop_ts}, - {"stop_ts_iso", getTimeIsoStr(tsStop)} + {"cap_ts_start", start_ts}, + {"cap_isotime_start", getTimeIsoStr(tsStart)}, + {"cap_ts_stop", stop_ts}, + {"cap_isotime_stop", getTimeIsoStr(tsStop)} }; _METADATA_LOG(jm); @@ -342,15 +342,15 @@ void VideoCaptureApp::startRecording(int cx, int cy, const std::string& frameRat Timestamp ts = CURRENT_TIMESTAMP(); json jm = { {"type", "session_begin"}, - {"ts", getTimeStr(ts)}, - {"ts_iso", getTimeIsoStr(ts)}, + {"json_ts", getTimeStr(ts)}, + {"json_isotime", getTimeIsoStr(ts)}, {"version", CAPTURE_VERSION_STRING}, {"appName", appName}, {"serial", targetVideoDev.serial}, {"vDev", targetVideoDev.name}, {"aDev", targetAudioInDev.alsaDeviceName}, - {"start_ts", start_ts}, - {"start_ts_iso", getTimeIsoStr(tsStart)}, + {"cap_ts_start", start_ts}, + {"cap_isotime_start", getTimeIsoStr(tsStart)}, {"cx", cx}, {"cy", cy}, {"frameRate", frameRate} From b889e3d054d5f228078025f62884acc3b4eab14d Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Tue, 16 Jul 2024 16:41:08 +0300 Subject: [PATCH 19/20] Provide additional information like kind/index in utility JSON output log, #96 --- Parsing/parse_wQR.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index f1a9449..670fe82 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -34,6 +34,7 @@ class VideoTimeInfo(BaseModel): # Define model for parsing summary info class ParseSummary(BaseModel): + kind: Optional[str] = Field("ParseSummary", description="JSON record kind/class") qr_count: Optional[int] = Field(0, description="Number of QR codes found") parsing_duration: Optional[float] = Field(0.0, description="Duration of the " "parsing in seconds") @@ -61,8 +62,10 @@ class ParseSummary(BaseModel): # Define the data model for the QR record class QrRecord(BaseModel): - frame_start: int = Field(None, description="Frame number where QR code starts") - frame_end: int = Field(None, description="Frame number where QR code ends") + kind: Optional[str] = Field("QrRecord", description="JSON record kind/class") + index: Optional[int] = Field(None, description="Zero-based i ndex of the QR code") + frame_start: Optional[int] = Field(None, description="Frame number where QR code starts") + frame_end: Optional[int] = Field(None, description="Frame number where QR code ends") isotime_start: Optional[datetime] = Field(None, description="ISO datetime where QR " "code starts") isotime_end: Optional[datetime] = Field(None, description="ISO datetime where QR " @@ -155,6 +158,7 @@ def finalize_record(ps: ParseSummary, record.isotime_end = calc_time(vti.start_time, pos_sec) record.time_end = pos_sec record.duration = record.time_end - record.time_start + record.index = ps.qr_count logger.info(f"QR: {str(record)}") # dump times event_time = get_iso_time(record.data['time_formatted']) From 36397ab58999bb2e0d2d5960c94572b89b231be4 Mon Sep 17 00:00:00 2001 From: Vadim Melnik Date: Tue, 16 Jul 2024 16:50:27 +0300 Subject: [PATCH 20/20] Provide additional information like type/index in utility JSON output log, #96 --- Parsing/parse_wQR.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Parsing/parse_wQR.py b/Parsing/parse_wQR.py index 670fe82..11ea614 100755 --- a/Parsing/parse_wQR.py +++ b/Parsing/parse_wQR.py @@ -34,7 +34,7 @@ class VideoTimeInfo(BaseModel): # Define model for parsing summary info class ParseSummary(BaseModel): - kind: Optional[str] = Field("ParseSummary", description="JSON record kind/class") + type: Optional[str] = Field("ParseSummary", description="JSON record type/class") qr_count: Optional[int] = Field(0, description="Number of QR codes found") parsing_duration: Optional[float] = Field(0.0, description="Duration of the " "parsing in seconds") @@ -62,7 +62,7 @@ class ParseSummary(BaseModel): # Define the data model for the QR record class QrRecord(BaseModel): - kind: Optional[str] = Field("QrRecord", description="JSON record kind/class") + type: Optional[str] = Field("QrRecord", description="JSON record type/class") index: Optional[int] = Field(None, description="Zero-based i ndex of the QR code") frame_start: Optional[int] = Field(None, description="Frame number where QR code starts") frame_end: Optional[int] = Field(None, description="Frame number where QR code ends")