From 4e5a84e96a835689e10f68d88085340c74e4d32b Mon Sep 17 00:00:00 2001 From: j Date: Wed, 14 Jan 2026 01:46:55 +0000 Subject: [PATCH] cut detection based on fps of source material --- pandora/archive/cutdetection.py | 89 +++++++++++++++++++++++++++++++++ pandora/archive/extract.py | 23 +-------- pandora/archive/utils.py | 25 +++++++++ 3 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 pandora/archive/cutdetection.py create mode 100644 pandora/archive/utils.py diff --git a/pandora/archive/cutdetection.py b/pandora/archive/cutdetection.py new file mode 100644 index 00000000..fb78a75f --- /dev/null +++ b/pandora/archive/cutdetection.py @@ -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 + diff --git a/pandora/archive/extract.py b/pandora/archive/extract.py index 2b9209a6..8515192f 100644 --- a/pandora/archive/extract.py +++ b/pandora/archive/extract.py @@ -23,6 +23,7 @@ import pillow_avif from pillow_heif import register_heif_opener from .chop import Chop, make_keyframe_index +from .utils import AspectRatio register_heif_opener() @@ -33,28 +34,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 diff --git a/pandora/archive/utils.py b/pandora/archive/utils.py new file mode 100644 index 00000000..b5659afa --- /dev/null +++ b/pandora/archive/utils.py @@ -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) +