cropping for videos

This commit is contained in:
q
2024-12-12 20:15:37 +02:00
parent 6ffcaa257e
commit c5947d0d60
2 changed files with 125 additions and 4 deletions

View File

@@ -2,7 +2,7 @@ import argparse
from tsmark.video_annotator import Marker from tsmark.video_annotator import Marker
VERSION = "0.4.7" VERSION = "0.5"
def get_options(): def get_options():
@@ -33,6 +33,15 @@ def get_options():
type=float, type=float,
help="Force FPS to play video", 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("--version", action="version", version=VERSION)
parser.add_argument(action="store", dest="video") parser.add_argument(action="store", dest="video")
return parser.parse_args() return parser.parse_args()

View File

@@ -20,6 +20,8 @@ class Marker:
self.frame_visu = [] self.frame_visu = []
self.max_res = (1280, 720) self.max_res = (1280, 720)
self.min_res = (512, None) self.min_res = (512, None)
self.crop = [(None, None), (None, None), None]
self.crop_click = 0
self.forced_fps = opts.fps self.forced_fps = opts.fps
try: 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_WIDTH)),
int(self.video_reader.get(cv2.CAP_PROP_FRAME_HEIGHT)), 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] video_aspect = self.video_res[0] / self.video_res[1]
if self.video_res[0] > self.max_res[0]: if self.video_res[0] > self.max_res[0]:
self.video_res[0] = int(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[1] = int(self.video_res[0] / video_aspect)
self.video_res = tuple(self.video_res) 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_start = int(self.video_res[0] * 0.05)
self.bar_end = int(self.video_res[0] * 0.95) self.bar_end = int(self.video_res[0] * 0.95)
self.bar_top = int(self.video_res[1] * 0.90) self.bar_top = int(self.video_res[1] * 0.90)
@@ -147,6 +167,49 @@ class Marker:
(255, 255, 255), (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): def draw_help(self, frame):
bottom = 80 bottom = 80
@@ -195,6 +258,7 @@ class Marker:
mark frame mark frame
space or click video space or click video
pause pause
a and s modify crop offset or size
f toggle 0.5x 1x or 2x FPS f toggle 0.5x 1x or 2x FPS
v toggle HUD v toggle HUD
h toggle help h toggle help
@@ -211,6 +275,17 @@ class Marker:
y > self.bar_top, 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 event == cv2.EVENT_LBUTTONDOWN:
if in_bar: if in_bar:
click_relative = (x - self.bar_start) / (self.bar_end - self.bar_start) click_relative = (x - self.bar_start) / (self.bar_end - self.bar_start)
@@ -271,17 +346,48 @@ class Marker:
print(self.get_help()) print(self.get_help())
def print_timestamps(self): 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() self.stamps.sort()
print("# Timestamps:") print("# Timestamps:")
for i, ts in enumerate(self.stamps): for i, ts in enumerate(self.stamps):
print("# {}: {} / {}".format(i + 1, self.format_time(ts), ts)) 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: if len(self.stamps) > 0:
print( 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[0]),
self.format_time(self.stamps[-1]), 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('"', '\\"'),
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: if (not self.paused) or self.read_next:
self.read_next = False self.read_next = False
frame_visu = cv2.resize(frame.copy(), self.video_res) frame_visu = cv2.resize(frame.copy(), self.video_res)
self.draw_crop(frame_visu)
nr_time = self.nr / self.fps nr_time = self.nr / self.fps
if self.show_info: if self.show_info:
self.draw_time(frame_visu) self.draw_time(frame_visu)
@@ -436,7 +543,12 @@ class Marker:
elif k & 0xFF == ord("f"): # modify FPS elif k & 0xFF == ord("f"): # modify FPS
FPS_modifier = (FPS_modifier + 1) % len(FPS_modifiers) 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 elif k & 0xFF == ord("x"): # toggle ts
self.toggle_stamp() self.toggle_stamp()