Compare commits
3 commits
af09508a87
...
11915abcf5
| Author | SHA1 | Date | |
|---|---|---|---|
| 11915abcf5 | |||
| 6deaca8161 | |||
| 4e5a84e96a |
5 changed files with 131 additions and 26 deletions
89
pandora/archive/cutdetection.py
Normal file
89
pandora/archive/cutdetection.py
Normal 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
|
||||
|
||||
|
|
@ -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
25
pandora/archive/utils.py
Normal 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)
|
||||
|
||||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue