diff --git a/pillar_detection.py b/pillar_detection.py index a501109..f076c15 100644 --- a/pillar_detection.py +++ b/pillar_detection.py @@ -17,11 +17,10 @@ parser.add_argument("-d", "--debug", default=False, action="store_true", help="f # 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("-manual", "--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") 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 @@ -51,7 +50,6 @@ def main(): for vid_index, fpath in enumerate(input_list): vid_name = os.path.basename(fpath)[:-4] vid_num = len(input_list) - isSkipped = False 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) @@ -75,29 +73,38 @@ def main(): else : cropPos = autoDetectCropArea(fpath) # Crop Area validation check (width, height must be even number) - cropPos = adjustCropArea(cropPos) + if cropPos == []: + cropPos = [crop_cfg.frame_width, crop_cfg.frame_height, 0, 0] + + cropPos = adjustCropArea(cropPos, crop_cfg.frame_width, crop_cfg.frame_height) + print(cropPos) cv2.namedWindow(f'{vid_name}', cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # Display crop size control UI while True: - retCode, cropPos = video_croppping(crop_cfg, cropPos) + retCode, cropPos = videoCropping(crop_cfg, cropPos) if retCode == 1: break elif retCode == 2: crop_cfg.getVidInfo(fpath) continue + elif retCode == 3: + print("") + return print("") crop_cfg.vidRelease() cv2.destroyAllWindows() - if not isSkipped: + if not crop_cfg.isSkipped: writeLog(logs_dir, log_path, cropPos) # Set ffmpeg commands, actual ffmpeg encoding - if not (args.only_logfile or isSkipped): - ffmpeg_encoding(args, vid_index, vid_num, cropPos, vid_name, fpath) + if not (args.only_logfile or crop_cfg.isSkipped): + retCode = ffmpegEncoding(args, vid_index, vid_num, cropPos, vid_name, fpath, crop_cfg.ffmpeg_process) + if retCode < 0 : + return retCode if __name__ == "__main__": main() - print("[INFO]\t Program End.") + print(f"\r\n Program End. end time : {getTime('string')}") diff --git a/pillar_detection_core.py b/pillar_detection_core.py index 23159c6..68c2950 100644 --- a/pillar_detection_core.py +++ b/pillar_detection_core.py @@ -1,7 +1,9 @@ from pillar_detection_utils import * +import signal class VideoCropCfg(): def __init__(self): + self.ffmpeg_process = [] self.vid_name = '' self.frame_pos = 0 @@ -20,19 +22,20 @@ class VideoCropCfg(): 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.frame_width = int(self.captured_vid.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.frame_height = int(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): +def videoCropping(crop_cfg, cropPos): ''' retCode value mean 0 : keep playing 1 : done with cropping (to next vid) 2 : replay + 3 : program exit ''' retCode = 0 @@ -44,8 +47,8 @@ def video_croppping(crop_cfg, cropPos): # Close button(X button) event if cv2.getWindowProperty(f'{crop_cfg.vid_name}', cv2.WND_PROP_VISIBLE) < 1 : - retCode = 1 - return retCode, cropPos + retCode = 3 + return retCode, [] if key > 0 : # 13 == Enter key ASCII Code @@ -76,9 +79,11 @@ def video_croppping(crop_cfg, cropPos): print("[INFO]\t ############################################################################################################################################################################ ") # Skip this Vid elif key == ord('n') or key == ord('N'): + print("") print(f"[INFO]\t Skip this video") crop_cfg.isSkipped = True retCode = 1 + return retCode, [] # Preview this crop area on new window elif key == ord('v') or key == ord('V'): _x = cropPos[2] @@ -113,7 +118,6 @@ def video_croppping(crop_cfg, cropPos): _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) @@ -124,31 +128,32 @@ def video_croppping(crop_cfg, cropPos): 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) - + # 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): +def ffmpegEncoding(args, vid_index, vid_num, cropPos, vid_name, fpath, ffmpeg_process): + retCode = -1 + uhd_output = args.uhd_output debug = args.debug add_letterbox = args.add_letterbox @@ -217,26 +222,27 @@ def ffmpeg_encoding(args, vid_index, vid_num, cropPos, vid_name, fpath): ffmpeg arguments -stats : print only encoding progress status (ignore loglevel argument) -loglevel : make ffmpeg stdout more verbose or not + quiet : no logs warning : only warning, error - error : only errors - fatal : only fatal (crash) errors - info : defualt + error : only errors + fatal : only fatal (crash) errors + info : defualt ''' - loglevel = 'fatal' + loglevel = 'quiet' 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}\"" - duration_list = getVideDuration(fpath) - vid_hour = duration_list[0] - vid_min = duration_list[1] - vid_sec = duration_list[2] + vid_duration = getVideDuration(fpath) + vid_hour = vid_duration[0] + vid_min = vid_duration[1] + vid_sec = vid_duration[2] # print("====================================================================") print("[INFO]\t Encoding start \"%s\" ..." % vid_name) - print("[INFO]\t Video CRF : %d" % int(crf)) + print("[INFO]\t CRF : %d" % int(crf)) print("[INFO]\t Video Duration : %02d:%02d:%02d" % (vid_hour, vid_min, vid_sec)) print("[INFO]\t Pixel format : YUV420p") - print("[INFO]\t Video Codec : libx264") - print("[INFO]\t Encoding Preset : medium") + print("[INFO]\t Encoding Codec : libx264") + print("[INFO]\t Ffmpeg Preset : medium") print(f"[INFO]\t Cropping Info : [W : {cropPos[0]} H : {cropPos[1]} X : {cropPos[2]} Y : {cropPos[3]}]") print("[INFO]\t Crop area ratio : %.5f" %current_crop_ratio) if add_letterbox : @@ -252,39 +258,44 @@ def ffmpeg_encoding(args, vid_index, vid_num, cropPos, vid_name, fpath): # 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 : - print(f"[INFO]\t Video Duration : {vid_hour:02d}:{vid_min:02d}:{vid_sec:02d}\ - Current Time : {getTime()}") - 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: - # 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()}") - 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): - # 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()}") - subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() - else : - subprocess.Popen(ffmpeg_command, shell=False) + start_time = getTime() + try: + if manual_mode: + # Manual Mode (always cropping one by one) + # Hold process when encoding last video + if vid_index == vid_num : + print(f"[INFO]\t Video Duration : {vid_hour:02d}:{vid_min:02d}:{vid_sec:02d}\ + Start Time : {getTime('string')}") + ffmpeg_process.append(subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait()) + else : + ffmpeg_process.append(subprocess.Popen(ffmpeg_command, shell=False)) + else: + # Log file mode (encoding simultaneously n videos) + if args.multi_encoding == 0: + subprocess.Popen(ffmpeg_command + ' -stats', shell=False).wait() + + elif (vid_index % args.multi_encoding == 0) or (vid_index == vid_num): + print(f"[INFO]\t Video Duration : {vid_hour:02d}:{vid_min:02d}:{vid_sec:02d}\ + Start Time : {getTime('string')}") + p = subprocess.Popen(ffmpeg_command + ' -stats', shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + ffmpeg_process.append(p.pid) + while p.poll() is None: + displayRemainTime(vid_duration, p.stderr, start_time) + else : + p = subprocess.Popen(ffmpeg_command, shell=False) + ffmpeg_process.append(p.pid) + + retCode = 0 + except Exception as e : + print('[!!! ERROR !!!]\t', e) + # print(ffmpeg_process) + # for p in ffmpeg_process : + # print('terminate') + # os.killpg(os.getpgid(p, signal.SIGTERM)) + # # p.terminate() + # # p.kill() + + retCode = -1 + finally : + return retCode \ No newline at end of file diff --git a/pillar_detection_utils.py b/pillar_detection_utils.py index 973a007..b25bb6c 100644 --- a/pillar_detection_utils.py +++ b/pillar_detection_utils.py @@ -9,6 +9,53 @@ import copy import datetime import time +def getTime(tformat = None): + dt_now = datetime.datetime.now() + dt_str = dt_now.strftime('[%Y-%m-%d %H:%M:%S]') + if tformat == 'string' : + out = dt_str + else: + out = dt_now + # print(dt_now + datetime.timedelta(hours=1)) + + return out + +def timeToSecond(time_stamp): + return (time_stamp[0] * 3600) + (time_stamp[1] * 60) + (time_stamp[2]) + +def displayRemainTime(vid_duration, ffmpeg_stream, start_time): + #TODO : now encoding... [percentage] [ETA : yyyy-mm-dd hh:mm:ss] + # infos (ex)= "frame= 228 fps= 37 q=26.0 size= 3840kB time=00:00:08.08 bitrate=3890.7kbits/s speed=1.31x" + infos = ffmpeg_stream.readline() + # parsing time stamp + time_stamp = re.findall("time=\S+", infos) + if time_stamp != []: + ''' + parsing process + 1. List type to String type + 2. lid of "time=" string + 3. get each time arguments (h:m:s) + 4. String type to Int type + ''' + time_stamp = time_stamp[0] + time_stamp = time_stamp[5:] + time_stamp = time_stamp.split(':') + time_stamp = [int(float(x)) for x in time_stamp] + # current_percentage + current_progress_sec = timeToSecond(time_stamp) + total_progress_sec = timeToSecond(vid_duration) + current_percentage = (current_progress_sec / total_progress_sec) * 100 + + # eta_time + passed_time = getTime() - start_time + passed_time = passed_time.total_seconds() + encoding_speed = current_percentage / passed_time + eta = (100 - current_percentage) / encoding_speed + eta_time = datetime.datetime.now() + datetime.timedelta(seconds=eta) + eta_time = eta_time.strftime('[ETA : %Y-%m-%d %H:%M:%S]') + print(f"[INFO]\t now encoding ... {eta_time} [{current_percentage:.2f}%] [Speed : {encoding_speed:.2f} %/s]", end='\r') + + def resizeCropInfo(key, cropPos, frame_width, frame_height): # Adjust crop info in display layout. def _IsOutOfBound(_cropPos): @@ -60,13 +107,6 @@ def resizeCropInfo(key, cropPos, frame_width, frame_height): 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) @@ -164,15 +204,15 @@ def autoDetectCropArea(fpath): 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') + "-vframes", "3000", "-f", "null", "out.null"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + infos = p.stderr.read() 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})") + print(f"[!!! ERROR !!!]\t Can`t auto detect crop area \t ({fpath})") return cropPos # Delete "crop=" in list "crop" @@ -183,7 +223,7 @@ def autoDetectCropArea(fpath): return cropPos -def adjustCropArea(cropPos): +def adjustCropArea(cropPos, frame_width, frame_height): # Crop Area even num check if (cropPos[0] % 2) != 0 : if (cropPos[0] + 1) >= frame_width: @@ -191,7 +231,7 @@ def adjustCropArea(cropPos): else: cropPos[0] += 1 if (cropPos[1] % 2) != 0 : - if (cropPos[1] + 1) >= frame_width: + if (cropPos[1] + 1) >= frame_height: cropPos[1] -= 1 else: cropPos[1] += 1 @@ -203,7 +243,7 @@ def adjustCropArea(cropPos): else: cropPos[0] += 2 if (cropPos[1] % 4) != 0 : - if (cropPos[1] + 2) >= frame_width: + if (cropPos[1] + 2) >= frame_height: cropPos[1] -= 2 else: cropPos[1] += 2