From 67fdffbe1070bb0fe96487fc389b7fb590d49dd2 Mon Sep 17 00:00:00 2001 From: JunHo Kim Date: Fri, 10 Dec 2021 19:05:16 +0900 Subject: [PATCH] module python files --- .gitignore | 5 + README.md | 35 +-- pillar_detection.py | 500 ++++---------------------------------- pillar_detection_core.py | 269 ++++++++++++++++++++ pillar_detection_utils.py | 212 ++++++++++++++++ 5 files changed, 531 insertions(+), 490 deletions(-) create mode 100644 pillar_detection_core.py create mode 100644 pillar_detection_utils.py diff --git a/.gitignore b/.gitignore index 72110cb..0979e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ +__pycache__/* .vscode/* backups/* +input/* +logs/* +out/* +test_video/* \ No newline at end of file diff --git a/README.md b/README.md index c8e3f2d..34d4733 100644 --- a/README.md +++ b/README.md @@ -2,41 +2,12 @@ Convenient Vid cropping tool ## Info -- Window 용 Script +- Window 용 python Script - 재생은 x3 배속으로 고정 -## ivtc Code -< 하나의 파일을 변환하는 코드 > - -"ffmpeg -i in.mp4 -vf fieldmatch=mode=pc_n:combmatch=full, bwdif=0:-1:1, decimate -c:v libx264 -crf 0 -preset ultrafast out.mp4" - -< 폴더 내에 있는 모든 파일을 변환하는 코드 > - -"for %a in ("*.*") do ffmpeg -i "%a" -vf fieldmatch=mode=pc_n:combmatch=full, bwdif=0:-1:1, decimate -c:v libx264 -crf 0 -preset ultrafast ".\out\%~na_ivtc.mp4"" - ## ToDo -- **Concealing bitstream error** 해결 -- ~~타이틀 창 등에 현재 재생 퍼센트 / 프레임 수 Display~~ -- 타이틀 한글 깨지는 것 -- 처음에 목록 보여주고 시작? (필요한지?) -- Log Ouput 필요 - ### 포함 해야 할 내용 - - 날짜, 시간 정보 - - 영상의 정보 (영상 파일명, 해상도, 길이, fps, 포맷) - - ~~Crop 정보 (W, H, X, Y)~~ - - 사용자가 Confirm 한 영상 TimeStamp (어디까지 보고 확인했는지에 대한 정보) - - 소요된 Encoding 시간 - - ~~Error 로그~~ - - ~~File Missing~~ - ### 로그 파일 포맷 - - Txt 파일 포맷 - - 영상 파일 명.txt - - 같은 파일에 매 편집 시 누적되는 식 - - 각 편집 로그의 구별은 편집 시간정보별로. +- ffmpeg ### 구조적 문제 - Subprocess 기반으로 FFMPEG 동작 - 현재 Encoding 중인 영상이 어느정도 진행 되었는 지 알 수 없음 - - 전체 프로그램이 종료되기 전까지는 파일이 생성되지 않음 (subprocess가 계속 돌고있게 됨) - - 엔터를 누를 때 마다 다음 영상으로 넘어가기 때문에, 이전 영상의 진행도를 알기 어려울 듯 하다. - - _생성되는 각 프로세스를 다른 변수명에 Mapping 하여 Log 관리를 각각 해야 하나?_ - - Encoding이 종료되면 프로세스를 종료시키는 것 같은 방법이 필요할듯.. \ No newline at end of file + - 전체 프로그램이 종료되기 전까지는 파일이 생성되지 않음 (subprocess가 계속 돌고있게 됨) \ No newline at end of file diff --git a/pillar_detection.py b/pillar_detection.py index 23aec8a..fe3c782 100644 --- a/pillar_detection.py +++ b/pillar_detection.py @@ -1,514 +1,98 @@ -import os -from collections import Counter -import cv2 -import glob -import re -import subprocess -import argparse -import copy -import datetime -import time +from pillar_detection_utils import * +from pillar_detection_core import * parser = argparse.ArgumentParser() +# file path options parser.add_argument("-i", "--input_dir", default="./input", help="Input source vid directory") parser.add_argument("-o", "--output_dir", default="./out", help="Output vid directory") + +# ffmpeg encoding option parser.add_argument("-c", "--crf", default=20, type=int, help="set crf level") -parser.add_argument("-sp", "--add_sharpening", default=False, action="store_true", help="add ffmpeg sharpening filter") -parser.add_argument("-lb", "--add_letterbox", default=False, action="store_true", help="add letterbox to make 16:9 ratio") parser.add_argument("-uhd", "--uhd_output", default=False, action="store_true", help="set output resolution to 3840:2160") +parser.add_argument("-lb", "--add_letterbox", default=False, action="store_true", help="add letterbox to make 16:9 ratio") +parser.add_argument("-sp", "--add_sharpening", default=False, action="store_true", help="add ffmpeg sharpening filter") parser.add_argument("-mul", "--multi_encoding", default=2, type=int, help="when log file exist, encoding mutiple videos by this args") +parser.add_argument("-d", "--debug", default=False, action="store_true", help="for debugging. (short length video output)") + +# logfile option parser.add_argument("-s", "--cropsize_scale", default=1, type=int, help="before use crop size from logfile, scale up the crop size (for reuse when vid rescaled)") parser.add_argument("-log", "--only_logfile", default=False, action="store_true", help="large vid mode. (only write log file.)") parser.add_argument("-man", "--manual_mode", default=False, action="store_true", help="manual cropping without using log file (Ignore log files)") parser.add_argument("-edit", "--edit_mode", default=False, action="store_true", help="get first crop area from log file") -parser.add_argument("-d", "--debug", default=False, action="store_true", help="for debugging. (short length video output)") + args = parser.parse_args() # option when only write log file (no encoding) : -log # options when only edit log file that already exists (no encoding) : -log -edit -# Adjust crop info in display layout. -def resizeCropInfo(key, cropPos, frame_width, frame_height): - def _IsOutOfBound(_cropPos): - w, h, x, y = _cropPos - if (w + x) > frame_width or (h + y) > frame_height: - return True - if (x < 0) or (y < 0): - return True - if (w <= 0) or (h <= 0): - return True - - w, h, x, y = cropPos - # Left (Expand width) - if key == ord('j') or key == ord('J'): - x -= 2 - w += 4 - # Right (Shrink width) - elif key == ord('l') or key == ord('L'): - x += 2 - w -= 4 - # Up (Expand height) - elif key == ord('i') or key == ord('I'): - y -= 2 - h += 4 - # Down (Shrink height) - elif key == ord('k') or key == ord('K'): - y += 2 - h -= 4 - # Num 4 (Move left) - elif key == ord('a') or key == ord('A'): - # w -= 1 - x -= 1 - # Num 6 (Move right) - elif key == ord('d') or key == ord('D'): - # w += 1 - x += 1 - # Num 8 (Move upward) - elif key == ord('w') or key == ord('W'): - # h -= 1 - y -= 1 - # Num 2 (Move downward) - elif key == ord('s') or key == ord('S'): - # h += 1 - y += 1 - _cropPos = w, h, x, y - - if not _IsOutOfBound(_cropPos): - cropPos = _cropPos - - return cropPos - -def getTime(): - dt_now = datetime.datetime.now() - dt_str = dt_now.strftime('[%Y-%m-%d %H:%M:%S]') - # print(dt_str) - - return dt_str - -def getVideDuration(file_path): - ffprobe_command = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"{file_path}\"" - p = subprocess.run(ffprobe_command, shell=False, stdout=subprocess.PIPE) - _hour = 0 - _min = 0 - _sec = 0 - - _sec = int(float(p.stdout.decode('utf-8'))) - # print(_sec) - - if _sec > 60 : - _min = int(_sec // 60) - _sec = _sec - (60 * _min) - # print(_min, _sec) - if _sec > 3600 : - _hour = int(_sec // 60) - _sec = _sec - (60 * _min) - _min = int(_sec // 60) - _sec = _sec - (60 * _min) - # print(_hour, _min, _sec) - - return [_hour, _min, _sec] - -# for unicode free filename -def imwrite(filename, img, params=None): - try: - ext = os.path.splitext(filename)[1] - result, n = cv2.imencode(ext, img, params) - if result: - with open(filename, mode='w+b') as f: - n.tofile(f) - return True - else: - return False - except Exception as e: - print(e) - return False - def main(): vid_types = ('*.avi', '*.mov', '*.mp4', '*.mxf') - - # video encoding opts - input_dir = args.input_dir - crf = args.crf - add_letterbox = args.add_letterbox - add_sharpening = args.add_sharpening - uhd_output = args.uhd_output - - # program modes - multi_how_many = args.multi_encoding - 1 - scale = int(args.cropsize_scale) - only_logfile = args.only_logfile manual_mode = args.manual_mode - debug = args.debug edit_mode = args.edit_mode + input_dir = args.input_dir + logs_dir = input_dir - if scale < 1 : - print(f"[!!! ERROR !!!]\t Invalid scale option [scale : {scale}]") + if args.cropsize_scale < 1 : + print(f"[!!! ERROR !!!]\t Invalid scale option [scale : {args.cropsize_scale}]") return - elif multi_how_many < 0 : - print(f"[!!! WARRNING !!!]\t \"multi_encoding\" option should be more then 1 [multi_encoding : {multi_how_many}]") + elif args.multi_encoding < 1 : + print(f"[!!! WARRNING !!!]\t \"multi_encoding\" option should be more then 0 [multi_encoding : {args.multi_encoding}]") return - uhd_output_args = "" - if uhd_output : - uhd_output_args = "-s 3840x2160" - - debug_shortOutput = "" - if debug : - debug_shortOutput = "-t 15 -ss 30" - - output_dir = args.output_dir input_list = [] for types in vid_types : input_list += glob.glob(os.path.join(input_dir, types)) input_list = sorted(input_list) - print("[INFO]\t Input dir :", input_dir) - - print(str(input_list)) for vid_index, fpath in enumerate(input_list): vid_name = os.path.basename(fpath)[:-4] vid_num = len(input_list) isSkipped = False - logs_dir = input_dir - logs = '' - + log_path = f"{logs_dir}/{vid_name}.txt" + print(f"[INFO]\t [Video num : {vid_index + 1} / {vid_num}]") + print("[INFO]\t File path: ", fpath) print("[INFO]\t Log dir :", logs_dir) + + # init crop configuration class + crop_cfg = VideoCropCfg() + crop_cfg.vid_name = vid_name + # Read log file if exist if not manual_mode: - if not os.path.exists(f"{logs_dir}/{vid_name}.txt"): - print(f"[INFO]\t Can`t find log file. [{logs_dir}/{vid_name}.txt]") - edit_mode = False - else : - print(f"[INFO]\t Loading log file... [{logs_dir}/{vid_name}.txt]") - f = open(f"{logs_dir}/{vid_name}.txt", "r") - logs = f.read() + cropPos, edit_mode, manual_mode = parseLog(log_path, edit_mode, manual_mode, args.cropsize_scale) - if logs != '': - try : - # Delete "crop=" in list "crop" - allCrops = re.findall("crop=\S+", logs) - cropPos = allCrops[-1].split(":") - cropPos[0] = cropPos[0][5:] - print(f"[INFO]\t Crop area from log file: [w : {cropPos[0]}, h : {cropPos[1]}, x : {cropPos[2]}, y : {cropPos[3]}]") - cropPos = [int(x * scale) for x in cropPos] - if scale > 1 : - print(f"[INFO]\t Scale up crop area: [w : {cropPos[0]}, h : {cropPos[1]}, x : {cropPos[2]}, y : {cropPos[3]}]") - - except: - print(f"[!!! WARNNIG !!!]\t Can`t read log file. [{logs_dir}/{vid_name}.txt]") - - else : - print(f"[!!! WARNNIG !!!]\t Empty log file. [{logs_dir}/{vid_name}.txt]") - - if (logs == '') or manual_mode or edit_mode: - # TODO : Korean title text bug fix - # vid_name = vid_name.encode('utf-8').decode('cp949') - - print(f"[INFO]\t [Video num : {vid_index + 1} / {vid_num}]") - print("[INFO]\t File to detect crop area: ", fpath) + if manual_mode or edit_mode: + # Capture vid, Get Vid info + crop_cfg.getVidInfo(fpath) + if edit_mode: print("[INFO]\t (Edit mode) Read crop area from log file...") print(f"[INFO]\t Crop area from log file: [w : {cropPos[0]}, h : {cropPos[1]}, x : {cropPos[2]}, y : {cropPos[3]}]") else : - print("[INFO]\t Detecting crop area...") - p = subprocess.Popen(["ffmpeg", "-ss", "10", "-i", fpath, "-vf", "cropdetect=limit=38", - "-vframes", "3000", "-f", "null", "out.null"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - infos = p.stderr.read().decode('utf-8') - allCrops = re.findall("crop=\S+", infos) - mostCommonCrop = Counter(allCrops).most_common(1) - - if len(mostCommonCrop) > 0 : - crop = mostCommonCrop[0][0] - else : - print(f"[!!! ERROR !!!]\t Can`t read input vid. Skip to next vid \t ({fpath})") - continue - - # Delete "crop=" in list "crop" - cropPos = crop.split(":") - cropPos[0] = cropPos[0][5:] - print(f"[INFO]\t Most common detected crop area: [w : {cropPos[0]}, h : {cropPos[1]}, x : {cropPos[2]}, y : {cropPos[3]}]") - cropPos = [int(x) for x in cropPos] - - # Capture vid, Get Vid info - capture = cv2.VideoCapture(fpath) - frame_width = capture.get(cv2.CAP_PROP_FRAME_WIDTH) - frame_height = capture.get(cv2.CAP_PROP_FRAME_HEIGHT) - fps = capture.get(cv2.CAP_PROP_FPS) - frame_pos = 0 - frame_count = 0 - - # Crop Area even num check - if (cropPos[0] % 2) != 0 : - if (cropPos[0] + 1) >= frame_width: - cropPos[0] -= 1 - else: - cropPos[0] += 1 - if (cropPos[1] % 2) != 0 : - if (cropPos[1] + 1) >= frame_width: - cropPos[1] -= 1 - else: - cropPos[1] += 1 - - # Crop Area Multiple of 4 check - if (cropPos[0] % 4) != 0 : - if (cropPos[0] + 2) >= frame_width: - cropPos[0] -= 2 - else: - cropPos[0] += 2 - if (cropPos[1] % 4) != 0 : - if (cropPos[1] + 2) >= frame_width: - cropPos[1] -= 2 - else: - cropPos[1] += 2 - - key = None - isPaused = False - cleanFrame = None - _boxedFrame = None - frame = None + cropPos = autoDetectCropArea(fpath) + # Crop Area validation check (width, height must be even number) + cropPos = adjustCropArea(cropPos) cv2.namedWindow(f'{vid_name}', cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) - # Display crop size control UI while True: - key = cv2.waitKeyEx(10) - - # Close button(X button) event - if cv2.getWindowProperty(f'{vid_name}', cv2.WND_PROP_VISIBLE) < 1 : - return - - if key > 0 : - # 13 == Enter key ASCII Code - if key == 13: - break - # Pause - elif key == ord('p') or key == ord('P'): - if isPaused : - isPaused = False - else : - isPaused = True - # frame = copy.deepcopy(cleanFrame) - # Replay - elif key == ord('r') or key == ord('R'): - if not isPaused: - capture = cv2.VideoCapture(fpath) - continue - # Move Forward (5s) - elif key == ord('e') or key == ord('E'): - if not isPaused: - capture.set(cv2.CAP_PROP_POS_FRAMES, frame_pos + (fps * 5)) - # Move Backward (5s) - elif key == ord('q') or key == ord('Q'): - if not isPaused: - capture.set(cv2.CAP_PROP_POS_FRAMES, frame_pos - (fps * 5)) - # Display key guide - elif key == ord('h') or key == ord('H'): - print("[INFO]\t ### KEY GUIDE ##############################################################################################################################################################") - print("[INFO]\t # (MOVE : wasd), (RESIZE : ijkl), (FOR / BACKWARD : q, e), (RESET : r), (PAUSE : p), (SKIP : n), (Preview : v), (Save current frame : b)(CONFIRM : enter) #") - print("[INFO]\t ############################################################################################################################################################################ ") - # Skip this Vid - elif key == ord('n') or key == ord('N'): - print(f"[INFO]\t Skip this video") - isSkipped = True - break - # Preview this crop area on new window - elif key == ord('v') or key == ord('V'): - _x = cropPos[2] - _y = cropPos[3] - _w = cropPos[0] - _h = cropPos[1] - _frame = copy.deepcopy(cleanFrame) - cv2.namedWindow(f'{vid_name}_current_area', cv2.WINDOW_KEEPRATIO) - cv2.waitKey(10) - cv2.imshow(f'{vid_name}_current_area', _frame[_y: (_y + _h), _x: (_x + _w)]) - # Capture, download this crop area in png file - elif key == ord('b') or key == ord('B'): - _x = cropPos[2] - _y = cropPos[3] - _w = cropPos[0] - _h = cropPos[1] - _frame = copy.deepcopy(cleanFrame) - - _vid_name = vid_name.replace(" ","_") - image_dir = "./capture_image" - path = os.path.join(image_dir, _vid_name) - if not os.path.exists(path) : - os.makedirs(path) - print(f'[INFO]\t {path} created') - - image_name = f'{_vid_name}_({frame_pos}-{frame_count})_before.png' - _path = os.path.join(path, image_name) - imwrite(_path, _boxedFrame) - print(f"\n[INFO]\t Saved current frame : {_path}") - - image_name = f'{_vid_name}_({frame_pos}-{frame_count})_after.png' - _path = os.path.join(path, image_name) - imwrite(_path, _frame[_y: (_y + _h), _x: (_x + _w)]) - print(f"[INFO]\t Saved current frame in crop area : {_path}") - - else: - # Crop Area control - cropPos = resizeCropInfo(key, cropPos, frame_width, frame_height) - - if not isPaused: - frame_pos = int(capture.get(cv2.CAP_PROP_POS_FRAMES)) - frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) - - if(frame_pos >= frame_count): + retCode, cropPos = video_croppping(crop_cfg, cropPos) + if retCode == 1: break - - # When paused, hold current frame untill unpause. - if isPaused: - # deep copy current frame, use that frame for draw rectangle. - # cleanFrame = copy.deepcopy(frame) - _boxedFrame = copy.deepcopy(cleanFrame) - cv2.rectangle(_boxedFrame, (cropPos[2], cropPos[3]), (cropPos[2] + cropPos[0], - cropPos[3] + cropPos[1]), (0, 0, 255), thickness=2, lineType=cv2.LINE_8) - cv2.imshow(f'{vid_name}', _boxedFrame) - else : - ret, frame = capture.read() - # deep copy current frame for clean preview. - cleanFrame = copy.deepcopy(frame) - cv2.rectangle(frame, (cropPos[2], cropPos[3]), (cropPos[2] + cropPos[0], - cropPos[3] + cropPos[1]), (0, 0, 255), thickness=2, lineType=cv2.LINE_8) - cv2.imshow(f'{vid_name}', frame) - _boxedFrame = copy.deepcopy(frame) - + elif retCode == 2: + crop_cfg.getVidInfo(fpath) + continue - print("[INFO]\t Current crop area : ", end="") - print(cropPos, end="") - print(f"\t Frame Count ({frame_pos}/{frame_count})\t[PRESS \"h\" TO VIEW KEY GUIDE]", end="\r") - - print("") - capture.release() + crop_cfg.vidRelease() cv2.destroyAllWindows() - # Debugging - # cropPos[0] = 678 - # cropPos[1] = 402 - # cropPos[2] = 20 - # cropPos[3] = 38 - # pad_arg = "" - - dt_str = getTime() - if not isSkipped: - if not os.path.exists(f"{logs_dir}"): - print(f"[INFO]\t Make log directory [{logs_dir}/{vid_name}.txt]") - os.makedirs(f"{logs_dir}") - - if not os.path.exists(f"{logs_dir}/{vid_name}.txt"): - print(f"[INFO]\t Save log file in [{logs_dir}/{vid_name}.txt]") - f = open(f"{logs_dir}/{vid_name}.txt", 'w') - else : - print(f"[INFO]\t Add line in log file [{logs_dir}/{vid_name}.txt]") - f = open(f"{logs_dir}/{vid_name}.txt", 'a') - - f.write(f"{dt_str}\tcrop={cropPos[0]}:{cropPos[1]}:{cropPos[2]}:{cropPos[3]}\n") - f.close + writeLog(logs_dir, log_path, cropPos) # Set ffmpeg commands, actual ffmpeg encoding - if not (only_logfile or isSkipped): - crop_arg = f"crop={cropPos[0]}:{cropPos[1]}:{cropPos[2]}:{cropPos[3]}" - print(f"[INFO]\t [CropInfo] (W : {cropPos[0]}), (H : {cropPos[1]}), (X : {cropPos[2]}), (Y : {cropPos[3]})") - - crop_arg = f"crop={cropPos[0]}:{cropPos[1]}:{cropPos[2]}:{cropPos[3]}" - current_crop_ratio = cropPos[0] / cropPos[1] - scale_arg = "" - pad_arg = "" - - """ - Set padding(letter box) argument - variable "offset" for 16:9 output display ratio - - pad_arg syntax: - pad= {width after padding}:{height after padding}:{x offset of pad}:{y offset of pad} - """ - if add_letterbox : - # Always target 16:9 display ratio - target_ratio = 16 / 9 - print("[INFO]\t Target ratio : %.7f" %target_ratio) - - # when current ratio is more flat - if target_ratio < current_crop_ratio: - # offset : simple equation (widht / (height + offset) = 16/9) - offset = (9 * cropPos[0] - 16 * cropPos[1]) / 16 - tmp_height = cropPos[1] + offset - tmp_height = int(tmp_height) - if (tmp_height % 4) != 0: - tmp_height -= (tmp_height % 4) - pad_arg = f",pad={cropPos[0]}:{tmp_height}:(ow-iw)/2:(ih-oh)/2" - scale_arg = f",scale=w={cropPos[0]}:h={tmp_height}" - print(f"[INFO]\t [PadInfo] (W : {cropPos[0]}), (H : {tmp_height})") - else: - offset = (16 * cropPos[1] - 9 * cropPos[0]) / 9 - tmp_width = cropPos[0] + offset - tmp_width = int(tmp_width) - if (tmp_width % 4) != 0: - tmp_width -= (tmp_width % 4) - pad_arg = f",pad={tmp_width}:{cropPos[1]}:(ow-iw)/2:(ih-oh)/2" - scale_arg = f",scale=w={tmp_width}:h={cropPos[1]}" - print(f"[INFO]\t [PadInfo] (W : {tmp_width}), (H : {cropPos[1]})") - - if not os.path.exists(output_dir) : - os.mkdir(output_dir) - print(f'[INFO]\t folder created : {output_dir}') - - output_path = os.path.join(output_dir, f"{vid_name}_box.mov") - - ## ffmpeg command setting - if add_sharpening : - sharpen_arg = ',unsharp=7:7:1.5:7:7:1.5' - else : - sharpen_arg = '' - ''' - ffmpeg arguments - -stats : print only encoding progress status (ignore loglevel argument) - -loglevel : make ffmpeg stdout more verbose or not - warning : only warning, error - error : only errors - info : defualt - ''' - loglevel = 'error' - ffmpeg_command = f"ffmpeg {debug_shortOutput} -i \"{fpath}\" -vf \"{crop_arg}{scale_arg}{pad_arg}{sharpen_arg}\"\ - -y -pix_fmt yuv420p -c:v libx264 -crf {crf} -c:a copy -preset medium -loglevel {loglevel}\ - {uhd_output_args} \"{output_path}\"" - print(ffmpeg_command) - print("[INFO]\t Encoding \"%s\" ..." % vid_name) - print("[INFO]\t Video CRF = %d" % int(crf)) - - # duration_list[0] : hour, [1] : min, [2] : sec - duration_list = getVideDuration(fpath) - print(f"[INFO]\t Video Duration : {duration_list[0]:02d}:{duration_list[1]:02d}:{duration_list[2]:02d}\ - Current Time : {getTime()}") - st_time = time.time() - - if (logs == '') or manual_mode: - # Manual Mode (always encoding one by one) - # Hold process when encoding last video - if (vid_index + 1) == vid_num : - subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() - else : - subprocess.Popen(ffmpeg_command, shell=False) - else: - # Log file mode (encoding simultaneously n videos) - if multi_how_many == 0: - p = subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() - - # [21.12.08] progress status coding.. - # p = subprocess.Popen(ffmpeg_command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # while p.poll() is None: - # # print('now encoding..', end='\r') - # infos = p.stderr.read().decode('utf-8') - # print('h',infos) - # infos = p.stdout.read().decode('utf-8') - # print('t',infos) - # continue - elif ((vid_index % multi_how_many == 0) and (vid_index != 0)) or ((vid_index + 1) == vid_num): - subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() - else : - subprocess.Popen(ffmpeg_command, shell=False) + if not (args.only_logfile or isSkipped): + ffmpeg_encoding(args, vid_index, vid_num, cropPos, vid_name, fpath) if __name__ == "__main__": main() diff --git a/pillar_detection_core.py b/pillar_detection_core.py new file mode 100644 index 0000000..72acd04 --- /dev/null +++ b/pillar_detection_core.py @@ -0,0 +1,269 @@ +from pillar_detection_utils import * + +class VideoCropCfg(): + def __init__(self): + self.vid_name = '' + + self.frame_pos = 0 + self.frame_count = 0 + + self.captured_vid = None + self.isPaused = False + self.isSkipped = False + self.cleanFrame = None + self.boxedFrame = None + self.frame = None + + self.frame_width = 0 + self.frame_height = 0 + self.fps = 0 + + def getVidInfo(self, fpath): + self.captured_vid = cv2.VideoCapture(fpath) + self.frame_width = self.captured_vid.get(cv2.CAP_PROP_FRAME_WIDTH) + self.frame_height = self.captured_vid.get(cv2.CAP_PROP_FRAME_HEIGHT) + self.fps = self.captured_vid.get(cv2.CAP_PROP_FPS) + + def vidRelease(self): + self.captured_vid.release() + +def video_croppping(crop_cfg, cropPos): + ''' + retCode value mean + 0 : keep playing + 1 : done with cropping (to next vid) + 2 : replay + ''' + retCode = 0 + + # search_distance unit : second + search_distance = 5 + # play_speed unit : millisecond + play_speed = 10 + key = cv2.waitKeyEx(play_speed) + + # Close button(X button) event + if cv2.getWindowProperty(f'{crop_cfg.vid_name}', cv2.WND_PROP_VISIBLE) < 1 : + retCode = 1 + return retCode, cropPos + + if key > 0 : + # 13 == Enter key ASCII Code + if key == 13: + retCode = 1 + # Pause + elif key == ord('p') or key == ord('P'): + if crop_cfg.isPaused : + crop_cfg.isPaused = False + else : + crop_cfg.isPaused = True + # Replay + elif key == ord('r') or key == ord('R'): + if not crop_cfg.isPaused: + retCode = 2 + # Move Forward + elif key == ord('e') or key == ord('E'): + if not crop_cfg.isPaused: + crop_cfg.captured_vid.set(cv2.CAP_PROP_POS_FRAMES, crop_cfg.frame_pos + (crop_cfg.fps * search_distance)) + # Move Backward + elif key == ord('q') or key == ord('Q'): + if not crop_cfg.isPaused: + crop_cfg.captured_vid.set(cv2.CAP_PROP_POS_FRAMES, crop_cfg.frame_pos - (crop_cfg.fps * search_distance)) + # Display key guide + elif key == ord('h') or key == ord('H'): + print("[INFO]\t ### KEY GUIDE ##############################################################################################################################################################") + print("[INFO]\t # (MOVE : wasd), (RESIZE : ijkl), (FOR / BACKWARD : q, e), (RESET : r), (PAUSE : p), (SKIP : n), (Preview : v), (Save current frame : b)(CONFIRM : enter) #") + print("[INFO]\t ############################################################################################################################################################################ ") + # Skip this Vid + elif key == ord('n') or key == ord('N'): + print(f"[INFO]\t Skip this video") + crop_cfg.isSkipped = True + retCode = 1 + # Preview this crop area on new window + elif key == ord('v') or key == ord('V'): + _x = cropPos[2] + _y = cropPos[3] + _w = cropPos[0] + _h = cropPos[1] + _frame = copy.deepcopy(crop_cfg.cleanFrame) + cv2.namedWindow(f'{crop_cfg.vid_name}_current_area', cv2.WINDOW_KEEPRATIO) + cv2.waitKey(10) + cv2.imshow(f'{crop_cfg.vid_name}_current_area', _frame[_y: (_y + _h), _x: (_x + _w)]) + # captured_vid, download this crop area in png file + elif key == ord('b') or key == ord('B'): + _x = cropPos[2] + _y = cropPos[3] + _w = cropPos[0] + _h = cropPos[1] + _frame = copy.deepcopy(crop_cfg.cleanFrame) + + _vid_name = crop_cfg.vid_name.replace(" ","_") + image_dir = "./capture_image" + path = os.path.join(image_dir, _vid_name) + if not os.path.exists(path) : + os.makedirs(path) + print(f'[INFO]\t {path} created') + + image_name = f'{_vid_name}_({crop_cfg.frame_pos}-{crop_cfg.frame_count})_before.png' + _path = os.path.join(path, image_name) + imwrite(_path, crop_cfg.boxedFrame) + print(f"\n[INFO]\t Saved current frame : {_path}") + + image_name = f'{_vid_name}_({crop_cfg.frame_pos}-{crop_cfg.frame_count})_after.png' + _path = os.path.join(path, image_name) + imwrite(_path, _frame[_y: (_y + _h), _x: (_x + _w)]) + print(f"[INFO]\t Saved current frame in crop area : {_path}") + + else: + # Crop Area control + cropPos = resizeCropInfo(key, cropPos, crop_cfg.frame_width, crop_cfg.frame_height) + + if not crop_cfg.isPaused: + crop_cfg.frame_pos = int(crop_cfg.captured_vid.get(cv2.CAP_PROP_POS_FRAMES)) + crop_cfg.frame_count = int(crop_cfg.captured_vid.get(cv2.CAP_PROP_FRAME_COUNT)) + + if(crop_cfg.frame_pos >= crop_cfg.frame_count): + retCode = 1 + + # When paused, hold current frame untill unpause. + if crop_cfg.isPaused: + # deep copy current frame, use that frame for draw rectangle. + crop_cfg.boxedFrame = copy.deepcopy(crop_cfg.cleanFrame) + cv2.rectangle(crop_cfg.boxedFrame, (cropPos[2], cropPos[3]), (cropPos[2] + cropPos[0], + cropPos[3] + cropPos[1]), (0, 0, 255), thickness=2, lineType=cv2.LINE_8) + cv2.imshow(f'{crop_cfg.vid_name}', crop_cfg.boxedFrame) + else : + _, crop_cfg.frame = crop_cfg.captured_vid.read() + # deep copy current frame for clean preview. + crop_cfg.cleanFrame = copy.deepcopy(crop_cfg.frame) + cv2.rectangle(crop_cfg.frame, (cropPos[2], cropPos[3]), (cropPos[2] + cropPos[0], + cropPos[3] + cropPos[1]), (0, 0, 255), thickness=2, lineType=cv2.LINE_8) + cv2.imshow(f'{crop_cfg.vid_name}', crop_cfg.frame) + crop_cfg.boxedFrame = copy.deepcopy(crop_cfg.frame) + + + print("[INFO]\t Current crop area : ", end="") + print(cropPos, end="") + print(f"\t Frame Count ({crop_cfg.frame_pos}/{crop_cfg.frame_count})\t[PRESS \"h\" TO VIEW KEY GUIDE]", end="\r") + + return retCode, cropPos + +def ffmpeg_encoding(args, vid_index, vid_num, cropPos, vid_name, fpath): + uhd_output = args.uhd_output + debug = args.debug + add_letterbox = args.add_letterbox + output_dir = args.output_dir + add_sharpening = args.add_sharpening + manual_mode = args.manual_mode + crf = args.crf + + uhd_output_args = "" + if uhd_output : + uhd_output_args = "-s 3840x2160" + + debug_shortOutput = "" + if debug : + debug_shortOutput = "-t 15 -ss 30" + + crop_arg = f"crop={cropPos[0]}:{cropPos[1]}:{cropPos[2]}:{cropPos[3]}" + print(f"[INFO]\t [CropInfo] (W : {cropPos[0]}), (H : {cropPos[1]}), (X : {cropPos[2]}), (Y : {cropPos[3]})") + + crop_arg = f"crop={cropPos[0]}:{cropPos[1]}:{cropPos[2]}:{cropPos[3]}" + current_crop_ratio = cropPos[0] / cropPos[1] + scale_arg = "" + pad_arg = "" + + """ + Set padding(letter box) argument + variable "offset" for 16:9 output display ratio + + pad_arg syntax: + pad= {width after padding}:{height after padding}:{x offset of pad}:{y offset of pad} + """ + if add_letterbox : + # Always target 16:9 display ratio + target_ratio = 16 / 9 + print("[INFO]\t Target ratio : %.7f" %target_ratio) + + # when current ratio is more flat + if target_ratio < current_crop_ratio: + # offset : simple equation (widht / (height + offset) = 16/9) + offset = (9 * cropPos[0] - 16 * cropPos[1]) / 16 + tmp_height = cropPos[1] + offset + tmp_height = int(tmp_height) + if (tmp_height % 4) != 0: + tmp_height -= (tmp_height % 4) + pad_arg = f",pad={cropPos[0]}:{tmp_height}:(ow-iw)/2:(ih-oh)/2" + scale_arg = f",scale=w={cropPos[0]}:h={tmp_height}" + print(f"[INFO]\t [PadInfo] (W : {cropPos[0]}), (H : {tmp_height})") + else: + offset = (16 * cropPos[1] - 9 * cropPos[0]) / 9 + tmp_width = cropPos[0] + offset + tmp_width = int(tmp_width) + if (tmp_width % 4) != 0: + tmp_width -= (tmp_width % 4) + pad_arg = f",pad={tmp_width}:{cropPos[1]}:(ow-iw)/2:(ih-oh)/2" + scale_arg = f",scale=w={tmp_width}:h={cropPos[1]}" + print(f"[INFO]\t [PadInfo] (W : {tmp_width}), (H : {cropPos[1]})") + + if not os.path.exists(output_dir) : + os.mkdir(output_dir) + print(f'[INFO]\t folder created : {output_dir}') + + output_path = os.path.join(output_dir, f"{vid_name}_box.mov") + + ## ffmpeg command setting + if add_sharpening : + sharpen_arg = ',unsharp=7:7:1.5:7:7:1.5' + else : + sharpen_arg = '' + ''' + ffmpeg arguments + -stats : print only encoding progress status (ignore loglevel argument) + -loglevel : make ffmpeg stdout more verbose or not + warning : only warning, error + error : only errors + fatal : only fatal (crash) errors + info : defualt + ''' + loglevel = 'fatal' + ffmpeg_command = f"ffmpeg {debug_shortOutput} -i \"{fpath}\" -vf \"{crop_arg}{scale_arg}{pad_arg}{sharpen_arg}\"\ + -y -pix_fmt yuv420p -c:v libx264 -crf {crf} -c:a copy -preset medium -loglevel {loglevel}\ + {uhd_output_args} \"{output_path}\"" + print(ffmpeg_command) + print("[INFO]\t Encoding \"%s\" ..." % vid_name) + print("[INFO]\t Video CRF = %d" % int(crf)) + + # duration_list[0] : hour, [1] : min, [2] : sec + duration_list = getVideDuration(fpath) + print(f"[INFO]\t Video Duration : {duration_list[0]:02d}:{duration_list[1]:02d}:{duration_list[2]:02d}\ + Current Time : {getTime()}") + # st_time = time.time() + + vid_index += 1 + if manual_mode: + # Manual Mode (always encoding one by one) + # Hold process when encoding last video + if vid_index == vid_num : + subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() + else : + subprocess.Popen(ffmpeg_command, shell=False) + else: + # Log file mode (encoding simultaneously n videos) + if args.multi_encoding == 0: + p = subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() + + # [21.12.08] progress status coding.. + # p = subprocess.Popen(ffmpeg_command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + # while p.poll() is None: + # # print('now encoding..', end='\r') + # infos = p.stderr.read().decode('utf-8') + # print('h',infos) + # infos = p.stdout.read().decode('utf-8') + # print('t',infos) + # continue + elif (vid_index % args.multi_encoding == 0) or (vid_index == vid_num): + subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() + else : + subprocess.Popen(ffmpeg_command, shell=False) + \ No newline at end of file diff --git a/pillar_detection_utils.py b/pillar_detection_utils.py new file mode 100644 index 0000000..973a007 --- /dev/null +++ b/pillar_detection_utils.py @@ -0,0 +1,212 @@ +import os +from collections import Counter +import cv2 +import glob +import re +import subprocess +import argparse +import copy +import datetime +import time + +def resizeCropInfo(key, cropPos, frame_width, frame_height): +# Adjust crop info in display layout. + def _IsOutOfBound(_cropPos): + w, h, x, y = _cropPos + if (w + x) > frame_width or (h + y) > frame_height: + return True + if (x < 0) or (y < 0): + return True + if (w <= 0) or (h <= 0): + return True + + w, h, x, y = cropPos + # Left (Expand width) + if key == ord('j') or key == ord('J'): + x -= 2 + w += 4 + # Right (Shrink width) + elif key == ord('l') or key == ord('L'): + x += 2 + w -= 4 + # Up (Expand height) + elif key == ord('i') or key == ord('I'): + y -= 2 + h += 4 + # Down (Shrink height) + elif key == ord('k') or key == ord('K'): + y += 2 + h -= 4 + # Num 4 (Move left) + elif key == ord('a') or key == ord('A'): + # w -= 1 + x -= 1 + # Num 6 (Move right) + elif key == ord('d') or key == ord('D'): + # w += 1 + x += 1 + # Num 8 (Move upward) + elif key == ord('w') or key == ord('W'): + # h -= 1 + y -= 1 + # Num 2 (Move downward) + elif key == ord('s') or key == ord('S'): + # h += 1 + y += 1 + _cropPos = w, h, x, y + + if not _IsOutOfBound(_cropPos): + cropPos = _cropPos + + return cropPos + +def getTime(): + dt_now = datetime.datetime.now() + dt_str = dt_now.strftime('[%Y-%m-%d %H:%M:%S]') + # print(dt_str) + + return dt_str + +def getVideDuration(file_path): + ffprobe_command = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"{file_path}\"" + p = subprocess.run(ffprobe_command, shell=False, stdout=subprocess.PIPE) + _hour = 0 + _min = 0 + _sec = 0 + + _sec = int(float(p.stdout.decode('utf-8'))) + # print(_sec) + + if _sec > 60 : + _min = int(_sec // 60) + _sec = _sec - (60 * _min) + # print(_min, _sec) + if _sec > 3600 : + _hour = int(_sec // 60) + _sec = _sec - (60 * _min) + _min = int(_sec // 60) + _sec = _sec - (60 * _min) + # print(_hour, _min, _sec) + + return [_hour, _min, _sec] + +# for unicode free filename +def imwrite(filename, img, params=None): + try: + ext = os.path.splitext(filename)[1] + result, n = cv2.imencode(ext, img, params) + if result: + with open(filename, mode='w+b') as f: + n.tofile(f) + return True + else: + return False + except Exception as e: + print(e) + return False + +def parseLog(log_path, edit_mode, manual_mode, scale): + cropPos = [] + + if not os.path.exists(log_path): + print(f"[INFO]\t Can`t find log file. [{log_path}]") + manual_mode = True + edit_mode = False + else : + print(f"[INFO]\t Loading log file... [{log_path}]") + try : + f = open(log_path, "r") + logs = f.read() + + # if log file is not empty + if logs != '': + # Delete "crop=" in list "crop" + allCrops = re.findall("crop=\S+", logs) + cropPos = allCrops[-1].split(":") + cropPos[0] = cropPos[0][5:] + print(f"[INFO]\t Crop area from log file: [w : {cropPos[0]}, h : {cropPos[1]}, x : {cropPos[2]}, y : {cropPos[3]}]") + cropPos = [int(x * scale) for x in cropPos] + if scale > 1 : + print(f"[INFO]\t Scale up crop area: [w : {cropPos[0]}, h : {cropPos[1]}, x : {cropPos[2]}, y : {cropPos[3]}]") + + else : + print(f"[!!! WARNNIG !!!]\t Empty log file. [{log_path}]") + manual_mode = True + edit_mode = False + except Exception as e: + print(f"[!!! WARNNIG !!!]\t Can`t read log file. [{log_path}]") + print(e) + manual_mode = True + edit_mode = False + + + return cropPos, edit_mode, manual_mode + +def writeLog(logs_dir, log_path, cropPos): + if not os.path.exists(f"{logs_dir}"): + print(f"[INFO]\t Make log directory [{log_path}]") + os.makedirs(f"{logs_dir}") + + if not os.path.exists(f"{log_path}"): + print(f"[INFO]\t Save log file in [{log_path}]") + f = open(f"{log_path}", 'w') + else : + print(f"[INFO]\t Add line in log file [{log_path}]") + f = open(f"{log_path}", 'a') + + dt_str = getTime() + + f.write(f"{dt_str}\tcrop={cropPos[0]}:{cropPos[1]}:{cropPos[2]}:{cropPos[3]}\n") + f.close + +def autoDetectCropArea(fpath): + cropPos = [] + + print("[INFO]\t Detecting crop area...") + p = subprocess.Popen(["ffmpeg", "-ss", "10", "-i", fpath, "-vf", "cropdetect=limit=38", + "-vframes", "3000", "-f", "null", "out.null"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + infos = p.stderr.read().decode('utf-8') + allCrops = re.findall("crop=\S+", infos) + mostCommonCrop = Counter(allCrops).most_common(1) + + if len(mostCommonCrop) > 0 : + crop = mostCommonCrop[0][0] + else : + print(f"[!!! ERROR !!!]\t Can`t read input vid. Skip to next vid \t ({fpath})") + return cropPos + + # Delete "crop=" in list "crop" + cropPos = crop.split(":") + cropPos[0] = cropPos[0][5:] + print(f"[INFO]\t Most common detected crop area: [w : {cropPos[0]}, h : {cropPos[1]}, x : {cropPos[2]}, y : {cropPos[3]}]") + cropPos = [int(x) for x in cropPos] + + return cropPos + +def adjustCropArea(cropPos): + # Crop Area even num check + if (cropPos[0] % 2) != 0 : + if (cropPos[0] + 1) >= frame_width: + cropPos[0] -= 1 + else: + cropPos[0] += 1 + if (cropPos[1] % 2) != 0 : + if (cropPos[1] + 1) >= frame_width: + cropPos[1] -= 1 + else: + cropPos[1] += 1 + + # Crop Area Multiple of 4 check + if (cropPos[0] % 4) != 0 : + if (cropPos[0] + 2) >= frame_width: + cropPos[0] -= 2 + else: + cropPos[0] += 2 + if (cropPos[1] % 4) != 0 : + if (cropPos[1] + 2) >= frame_width: + cropPos[1] -= 2 + else: + cropPos[1] += 2 + + return cropPos +