Compare commits

...

3 commits

Author SHA1 Message Date
j
11915abcf5 settings flag to use new cut detection 2026-01-14 02:05:34 +00:00
j
6deaca8161 use .3 for timecodes 2026-01-14 02:04:15 +00:00
j
4e5a84e96a cut detection based on fps of source material 2026-01-14 01:46:55 +00:00
5 changed files with 131 additions and 26 deletions

View file

@ -0,0 +1,89 @@
import subprocess
import numpy as np
import ox
from django.config import settings
from .utils import AspectRatio
def _get_distance(data0, data1):
diff = data0.astype(np.float32) - data1.astype(np.float32)
per_pixel_distance = np.linalg.norm(diff, axis=2)
total_distance = per_pixel_distance.sum()
num_pixels = data0.shape[0] * data0.shape[1]
max_distance = num_pixels * np.sqrt(3 * (255 ** 2))
return total_distance / max_distance
def detect_cuts(path, seconds=True):
depth = 3
info = ox.avinfo(path)
dar = AspectRatio(info['video'][0]['display_aspect_ratio'])
fps = AspectRatio(info['video'][0]['framerate'])
height = 96
width = int(dar * height)
width += width % 2
nbytes = depth * width * height
bufsize = nbytes + 100
cmd = [
settings.FFMPEG,
'-hide_banner',
'-loglevel', 'error',
'-i', path,
'-threads', '4',
'-f', 'rawvideo',
'-pix_fmt', 'rgb24',
'-vcodec', 'rawvideo',
'-vf', 'scale=%d:%d' % (width, height),
'-aspect', '%d:%d' % (width, height),
'-'
]
#print(' '.join(cmd))
p = subprocess.Popen(cmd,
bufsize=bufsize,
stdout=subprocess.PIPE,
close_fds=True)
first = True
previous_frame = None
cuts = []
detect_cuts = True
short_cut = None
cut_frames = []
frame_i = 0
previous_distance = 0
while True:
data = p.stdout.read(nbytes)
if len(data) != nbytes:
if first:
raise IOError("ERROR: could not open file %s" % path)
else:
break
else:
first = False
frame = np.frombuffer(data, dtype=np.uint8).reshape((height, width, depth))
frame_data = np.asarray(frame)
if frame_i == 0:
is_cut = False
previous_distance = 0
else:
if short_cut and frame_i - short_cut > 2:
cuts.append(short_cut)
short_cut = None
distance = _get_distance(previous_frame_data, frame_data)
is_cut = distance > 0.1 or (distance > 0.2 and abs(distance - previous_distance) > 0.2)
if is_cut:
if frame_i - (0 if not cuts else short_cut or cuts[-1]) < 3:
is_cut = False
short_cut = frame_i
else:
cuts.append(frame_i)
previous_distance = distance
previous_frame_data = frame_data
frame_i += 1
if seconds:
return [float('%0.3f' % float(c/fps)) for c in cuts]
else:
return cuts

View file

@ -23,6 +23,8 @@ import pillow_avif
from pillow_heif import register_heif_opener
from .chop import Chop, make_keyframe_index
from .cutdetection import detect_cuts
from .utils import AspectRatio
register_heif_opener()
@ -33,28 +35,6 @@ img_extension = 'jpg'
MAX_DISTANCE = math.sqrt(3 * pow(255, 2))
class AspectRatio(fractions.Fraction):
def __new__(cls, numerator, denominator=None):
if not denominator:
ratio = list(map(int, numerator.split(':')))
if len(ratio) == 1:
ratio.append(1)
numerator = ratio[0]
denominator = ratio[1]
# if its close enough to the common aspect ratios rather use that
if abs(numerator/denominator - 4/3) < 0.03:
numerator = 4
denominator = 3
elif abs(numerator/denominator - 16/9) < 0.02:
numerator = 16
denominator = 9
return super(AspectRatio, cls).__new__(cls, numerator, denominator)
@property
def ratio(self):
return "%d:%d" % (self.numerator, self.denominator)
def supported_formats():
if not find_executable(settings.FFMPEG):
return None
@ -490,17 +470,26 @@ def timeline(video, prefix, modes=None, size=None):
size = [64, 16]
if isinstance(video, str):
video = [video]
if settings.CUT_DETECTION == 'oxtimelines':
cuts = [
'-c', os.path.join(prefix, 'cuts.json'),
]
else:
cuts = []
cmd = [os.path.normpath(os.path.join(settings.BASE_DIR, '../bin/oxtimelines')),
'-s', ','.join(map(str, reversed(sorted(size)))),
'-m', ','.join(modes),
'-o', prefix,
'-c', os.path.join(prefix, 'cuts.json'),
] + video
] + cuts + video
# print(cmd)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
close_fds=True)
p.wait()
if settings.CUT_DETECTION != 'oxtimelines':
cuts = detect_cuts(video)
with open(os.path.join(prefix, 'cuts.json'), "w") as fd:
json.dump(cuts, fd)
def average_color(prefix, start=0, end=0, mode='antialias'):
height = 64

25
pandora/archive/utils.py Normal file
View file

@ -0,0 +1,25 @@
import fractions
import math
class AspectRatio(fractions.Fraction):
def __new__(cls, numerator, denominator=None):
if not denominator:
ratio = list(map(int, numerator.split(':')))
if len(ratio) == 1:
ratio.append(1)
numerator = ratio[0]
denominator = ratio[1]
# if its close enough to the common aspect ratios rather use that
if abs(numerator/denominator - 4/3) < 0.03:
numerator = 4
denominator = 3
elif abs(numerator/denominator - 16/9) < 0.02:
numerator = 16
denominator = 9
return super(AspectRatio, cls).__new__(cls, numerator, denominator)
@property
def ratio(self):
return "%d:%d" % (self.numerator, self.denominator)

View file

@ -217,7 +217,7 @@ def join_tiles(source_paths, durations, target_path):
offset += durations[i]
with open(os.path.join(target_path, 'cuts.json'), 'w') as f:
# avoid float rounding artefacts
f.write('[' + ', '.join(map(lambda x: '%.2f' % x, cuts)) + ']')
f.write('[' + ', '.join(map(lambda x: '%.3f' % x, cuts)) + ']')
def split_tiles(path, paths, durations):

View file

@ -222,6 +222,8 @@ USE_VP9 = True
FFMPEG_SUPPORTS_VP9 = True
FFMPEG_DEBUG = False
CUT_DETECTION = "oxtimelines"
#=========================================================================
#Pan.do/ra related settings settings
#to customize, create local_settings.py and overwrite keys