diff --git a/tsmark/__init__.py b/tsmark/__init__.py index 535af02..0b2d644 100644 --- a/tsmark/__init__.py +++ b/tsmark/__init__.py @@ -2,7 +2,7 @@ import argparse from tsmark.video_annotator import Marker -VERSION = "0.4.7" +VERSION = "0.5" def get_options(): @@ -33,6 +33,15 @@ def get_options(): type=float, help="Force FPS to play video", ) + parser.add_argument( + "--crop", + action="store", + dest="crop", + default=None, + required=False, + type=str, + help="predefined crop. Syntax: 'w:h:x:y' example: 1280:720:30:20", + ) parser.add_argument("--version", action="version", version=VERSION) parser.add_argument(action="store", dest="video") return parser.parse_args() diff --git a/tsmark/video_annotator.py b/tsmark/video_annotator.py index 3c1ed54..adff941 100755 --- a/tsmark/video_annotator.py +++ b/tsmark/video_annotator.py @@ -20,6 +20,8 @@ class Marker: self.frame_visu = [] self.max_res = (1280, 720) self.min_res = (512, None) + self.crop = [(None, None), (None, None), None] + self.crop_click = 0 self.forced_fps = opts.fps try: @@ -49,6 +51,10 @@ class Marker: int(self.video_reader.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self.video_reader.get(cv2.CAP_PROP_FRAME_HEIGHT)), ] + self.video_res_original = [ + int(self.video_reader.get(cv2.CAP_PROP_FRAME_WIDTH)), + int(self.video_reader.get(cv2.CAP_PROP_FRAME_HEIGHT)), + ] video_aspect = self.video_res[0] / self.video_res[1] if self.video_res[0] > self.max_res[0]: self.video_res[0] = int(self.max_res[0]) @@ -63,6 +69,20 @@ class Marker: self.video_res[1] = int(self.video_res[0] / video_aspect) self.video_res = tuple(self.video_res) + self.crop = [(0, 0), tuple(self.video_res), None] + if self.opts.crop: + w, h, x, y = [int(c) for c in self.opts.crop.split(":")] + self.crop = [ + ( + int(self.video_res[0] * x / self.video_res_original[0]), + int(self.video_res[1] * y / self.video_res_original[1]), + ), + ( + int(self.video_res[0] * w / self.video_res_original[0]), + int(self.video_res[1] * h / self.video_res_original[1]), + ), + True, + ] self.bar_start = int(self.video_res[0] * 0.05) self.bar_end = int(self.video_res[0] * 0.95) self.bar_top = int(self.video_res[1] * 0.90) @@ -147,6 +167,49 @@ class Marker: (255, 255, 255), ) + def draw_crop(self, frame): + + if self.crop[2] is None: + return + p2 = (self.crop[0][0] + self.crop[1][0], self.crop[0][1] + self.crop[1][1]) + cv2.rectangle( + frame, + self.crop[0], + p2, + (0, 192, 192), + 1, + ) + if self.crop_click == 1: + x, y = [ + int(self.video_res_original[0] * self.crop[0][0] / self.video_res[0]), + int(self.video_res_original[1] * self.crop[0][1] / self.video_res[1]), + ] + self.shadow_text( + frame, + f"{x},{y}", + self.crop[0], + 0.5, + 1, + (0, 192, 192), + ) + if self.crop_click == 2: + x, y = [ + int(self.video_res_original[0] * self.crop[0][0] / self.video_res[0]), + int(self.video_res_original[1] * self.crop[0][1] / self.video_res[1]), + ] + w, h = [ + abs(int(self.video_res_original[0] * self.crop[1][0] / self.video_res[0])), + abs(int(self.video_res_original[1] * self.crop[1][1] / self.video_res[1])), + ] + self.shadow_text( + frame, + f"{w}x{h}", + self.crop[0], + 0.5, + 1, + (0, 192, 192), + ) + def draw_help(self, frame): bottom = 80 @@ -195,6 +258,7 @@ class Marker: mark frame space or click video pause + a and s modify crop offset or size f toggle 0.5x 1x or 2x FPS v toggle HUD h toggle help @@ -211,6 +275,17 @@ class Marker: y > self.bar_top, ) ) + if self.crop_click == 1: + self.crop[0] = (x, y) + if event == cv2.EVENT_LBUTTONDOWN: + self.crop_click = 0 + return + if self.crop_click == 2: + self.crop[1] = (x - self.crop[0][0], y - self.crop[0][1]) + if event == cv2.EVENT_LBUTTONDOWN: + self.crop_click = 0 + return + if event == cv2.EVENT_LBUTTONDOWN: if in_bar: click_relative = (x - self.bar_start) / (self.bar_end - self.bar_start) @@ -271,17 +346,48 @@ class Marker: print(self.get_help()) def print_timestamps(self): + + if self.crop[2] is None: + cropstr = "" + else: + x, y = [ + int(self.video_res_original[0] * self.crop[0][0] / self.video_res[0]), + int(self.video_res_original[1] * self.crop[0][1] / self.video_res[1]), + ] + w, h = [ + int(self.video_res_original[0] * self.crop[1][0] / self.video_res[0]), + int(self.video_res_original[1] * self.crop[1][1] / self.video_res[1]), + ] + if w < 0: + x = x + w + w = -w + if h < 0: + y = y + h + h = -h + cropstr = f"-filter:v crop={w}:{h}:{x}:{y} " self.stamps.sort() print("# Timestamps:") for i, ts in enumerate(self.stamps): print("# {}: {} / {}".format(i + 1, self.format_time(ts), ts)) + if len(self.stamps) == 0: + self.stamps.append(0) + self.stamps.append(self.frames) if len(self.stamps) > 0: print( - 'ffmpeg -ss {} -to {} -i "{}" -c copy "{}.trimmed.mp4"'.format( + 'ffmpeg -i "{}" {}-c:v mpeg2video -q:v 3 -g 1 -a copy -ss {} -to {} "{}.trimmed.mp4"'.format( + self.opts.video.replace('"', '\\"'), + cropstr, self.format_time(self.stamps[0]), self.format_time(self.stamps[-1]), + os.path.splitext(self.opts.video)[0].replace('"', '\\"'), + ) + ) + elif self.crop[0][0] is not None: + print( + 'ffmpeg -i "{}" {}-c:v mpeg2video -q:v 3 -g 1 "{}.trimmed.mp4"'.format( self.opts.video.replace('"', '\\"'), - self.opts.video.replace('"', '\\"'), + cropstr, + os.path.splitext(self.opts.video)[0].replace('"', '\\"'), ) ) @@ -350,6 +456,7 @@ class Marker: if (not self.paused) or self.read_next: self.read_next = False frame_visu = cv2.resize(frame.copy(), self.video_res) + self.draw_crop(frame_visu) nr_time = self.nr / self.fps if self.show_info: self.draw_time(frame_visu) @@ -436,7 +543,12 @@ class Marker: elif k & 0xFF == ord("f"): # modify FPS FPS_modifier = (FPS_modifier + 1) % len(FPS_modifiers) - + elif k & 0xFF == ord("a"): # toggle crop offset + self.crop_click = 0 if self.crop_click == 1 else 1 + self.crop[2] = True + elif k & 0xFF == ord("s"): # toggle crop size + self.crop_click = 0 if self.crop_click == 2 else 2 + self.crop[2] = True elif k & 0xFF == ord("x"): # toggle ts self.toggle_stamp()