gluing point tracking tool

This commit is contained in:
q
2025-06-10 15:44:19 +03:00
parent 69491a64b6
commit 0777e5561d
4 changed files with 181 additions and 18 deletions

View File

@@ -23,7 +23,7 @@ classifiers = [
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
] ]
dependencies = ["opencv-python>=4.5.0"] dependencies = ["opencv-python>=4.5.0","scipy"]
[project.scripts] [project.scripts]
tsmark = "tsmark:main" tsmark = "tsmark:main"

View File

@@ -1,5 +1,5 @@
from distutils.core import setup
import os import os
from distutils.core import setup
def version_reader(path): def version_reader(path):
@@ -14,13 +14,13 @@ setup(
packages=["tsmark"], packages=["tsmark"],
version=version, version=version,
description="Video timestamp marking.", description="Video timestamp marking.",
author="Ville Rantanen", author="Q",
author_email="ville.q.rantanen@gmail.com", author_email="q@six9.net",
keywords=["video"], keywords=["video"],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"tsmark=tsmark:main", "tsmark=tsmark:main",
], ],
}, },
install_requires=["opencv-python>=4.5.0"], install_requires=["opencv-python>=4.5.0", "scipy"],
) )

View File

@@ -2,7 +2,7 @@ import argparse
from tsmark.video_annotator import Marker from tsmark.video_annotator import Marker
VERSION = "0.6.1" VERSION = "0.7.0"
def get_options(): def get_options():

View File

@@ -5,6 +5,8 @@ import sys
import time import time
import cv2 import cv2
import numpy as np
from scipy.interpolate import PchipInterpolator
AUDIO_COMPRESS = "-c:a libmp3lame -ar 44100 -b:a 192k".split(" ") AUDIO_COMPRESS = "-c:a libmp3lame -ar 44100 -b:a 192k".split(" ")
AUDIO_COPY = "-c:a copy".split(" ") AUDIO_COPY = "-c:a copy".split(" ")
@@ -31,6 +33,11 @@ class Marker:
self.min_res = (512, None) self.min_res = (512, None)
self.crop = [(None, None), (None, None), None] self.crop = [(None, None), (None, None), None]
self.crop_click = 0 self.crop_click = 0
self.point_click = 0
self.points = {}
self.points_interpolated = {}
self.point_index = None
self.forced_fps = opts.fps self.forced_fps = opts.fps
try: try:
@@ -219,6 +226,118 @@ class Marker:
(0, 192, 192), (0, 192, 192),
) )
def draw_points(self, frame):
if self.point_click == 0:
return
x, y = [self.video_res[0] - 120, 50]
self.shadow_text(
frame,
"Points: " + str(self.point_index),
(x, y),
0.5,
1,
(0, 192, 192),
)
try:
current = self.points_interpolated[self.point_index][self.nr]
color = (0, 192, 192)
if current[3] == 2:
color = (60, 205, 60)
if current[3] == 1:
color = (192, 0, 192)
cv2.circle(frame, (current[1], current[2]), 10, color, 1)
history = list(range(max(1, int(self.nr - self.viewer_fps)), self.nr + 1))
for p in history:
self.points_interpolated[self.point_index][p - 1][1:2]
cv2.line(
frame,
tuple(self.points_interpolated[self.point_index][p - 1][1:3]),
tuple(self.points_interpolated[self.point_index][p][1:3]),
(192, 0, 192),
1,
)
except KeyError:
pass
except IndexError:
print(current, self.nr)
pass
try:
# ~ point_keys = list(sorted(self.points[self.point_index].keys()))
current = self.points[self.point_index][self.nr]
color = (60, 205, 60)
cv2.circle(frame, (current[0], current[1]), 13, color, 2)
except KeyError:
pass
except IndexError:
print(self.points[self.point_index])
print(self.nr)
pass
def scan_point(self, direction):
try:
if direction == "next":
for ts in sorted(list(self.points[self.point_index].keys())):
if ts > self.nr:
self.nr = ts - 1
self.read_next = True
return
if direction == "previous":
for ts in reversed(sorted(list(self.points[self.point_index].keys()))):
if ts < self.nr - 1:
self.nr = ts - 1
self.read_next = True
return
except Exception:
pass
def del_point(self, ts):
try:
del self.points[self.point_index][ts]
self.interpolate_points()
except Exception:
pass
def interpolate_points(self):
if not self.point_index in self.points_interpolated:
self.points_interpolated[self.point_index] = {key: [] for key in range(self.frames)}
if len(self.points[self.point_index]) == 1:
key = list(self.points[self.point_index].keys())[0]
x, y = self.points[self.point_index][key]
for key in range(self.frames):
self.points_interpolated[self.point_index][key] = [False, int(x), int(y), 0]
self.points_interpolated[self.point_index][self.nr][3] = 2
else: # more points
point_keys = list(sorted(list(self.points[self.point_index].keys())))
point_values = [self.points[self.point_index][k] for k in point_keys]
xy = np.array(point_values).T
spline = PchipInterpolator(point_keys, xy, axis=1)
start_key = min(point_keys)
end_key = max(point_keys) + 1
t2 = np.arange(start_key, end_key)
for key in range(0, start_key):
self.points_interpolated[self.point_index][key] = [
False,
self.points[self.point_index][start_key][0],
self.points[self.point_index][start_key][1],
0,
]
# interpolated points
for row in np.vstack((t2, spline(t2))).T:
self.points_interpolated[self.point_index][row[0]] = [True, int(row[1]), int(row[2]), 1]
for key in range(end_key, self.frames + 1):
self.points_interpolated[self.point_index][key] = [False, int(row[1]), int(row[2]), 0]
# clicked points (not necessary, could determine at draw time!)
for key in point_keys:
self.points_interpolated[self.point_index][key][3] = 2
def draw_help(self, frame): def draw_help(self, frame):
bottom = 80 bottom = 80
@@ -296,6 +415,14 @@ class Marker:
return return
if event == cv2.EVENT_LBUTTONDOWN: if event == cv2.EVENT_LBUTTONDOWN:
if self.point_click == 1:
if not self.point_index in self.points:
self.points[self.point_index] = {}
self.points[self.point_index][self.nr] = (int(x), int(y))
self.interpolate_points()
return
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)
self.nr = int(click_relative * self.frames) self.nr = int(click_relative * self.frames)
@@ -304,6 +431,13 @@ class Marker:
self.paused = not self.paused self.paused = not self.paused
if event == cv2.EVENT_LBUTTONDBLCLK: if event == cv2.EVENT_LBUTTONDBLCLK:
if self.point_click == 1:
if not self.point_index in self.points:
return
if self.nr in self.points[self.point_index]:
del self.points[self.point_index][self.nr]
return
if not in_bar: if not in_bar:
self.toggle_stamp() self.toggle_stamp()
# doubleclick (toggle?) # doubleclick (toggle?)
@@ -485,6 +619,7 @@ class Marker:
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) self.draw_crop(frame_visu)
self.draw_points(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)
@@ -550,17 +685,23 @@ class Marker:
self.read_next = True self.read_next = True
elif k & 0xFF == ord("z"): # move to previous ts elif k & 0xFF == ord("z"): # move to previous ts
for ts in reversed(sorted(self.stamps)): if self.point_click == 1:
if ts < self.nr - 1: self.scan_point("previous")
self.nr = ts - 1 else:
self.read_next = True for ts in reversed(sorted(self.stamps)):
break if ts < self.nr - 1:
elif k & 0xFF == ord("c"): # move to previous ts self.nr = ts - 1
for ts in sorted(self.stamps): self.read_next = True
if ts > self.nr: break
self.nr = ts - 1 elif k & 0xFF == ord("c"): # move to next ts
self.read_next = True if self.point_click == 1:
break self.scan_point("next")
else:
for ts in sorted(self.stamps):
if ts > self.nr:
self.nr = ts - 1
self.read_next = True
break
# Move by number # Move by number
elif k & 0xFF in digits_ords: elif k & 0xFF in digits_ords:
@@ -577,8 +718,30 @@ class Marker:
elif k & 0xFF == ord("s"): # toggle crop size elif k & 0xFF == ord("s"): # toggle crop size
self.crop_click = 0 if self.crop_click == 2 else 2 self.crop_click = 0 if self.crop_click == 2 else 2
self.crop[2] = True self.crop[2] = True
elif k & 0xFF == ord("p"): # toggle points
self.point_click = 0 if self.point_click == 1 else 1
if self.point_click == 1:
self.shadow_text(
frame_visu,
"Enter point index",
(self.video_res[0] - 200, 50),
0.5,
1,
(0, 192, 192),
)
cv2.imshow("tsmark", frame_visu)
k2 = cv2.waitKey(0)
if k2 & 0xFF == ord("q") or k2 & 0xFF == 27:
self.point_click = 0
else:
self.point_index = chr(k2)
elif k & 0xFF == ord("x"): # toggle ts elif k & 0xFF == ord("x"): # toggle ts
self.toggle_stamp() if self.point_click == 1:
self.del_point(self.nr)
else:
self.toggle_stamp()
elif k & 0xFF == ord("v"): elif k & 0xFF == ord("v"):
self.show_info = not self.show_info self.show_info = not self.show_info