Improve video thumbnailer by skipping early black frames.
This commit is contained in:
parent
19a0322901
commit
ec8644dded
1 changed files with 32 additions and 18 deletions
|
@ -3,7 +3,9 @@ This file provides functions which are used in various places throughout the
|
||||||
codebase but don't deserve to be methods of any class.
|
codebase but don't deserve to be methods of any class.
|
||||||
'''
|
'''
|
||||||
import bs4
|
import bs4
|
||||||
|
import io
|
||||||
import datetime
|
import datetime
|
||||||
|
import kkroening_ffmpeg
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
|
@ -18,6 +20,9 @@ from voussoirkit import imagetools
|
||||||
from voussoirkit import pathclass
|
from voussoirkit import pathclass
|
||||||
from voussoirkit import stringtools
|
from voussoirkit import stringtools
|
||||||
from voussoirkit import timetools
|
from voussoirkit import timetools
|
||||||
|
from voussoirkit import vlogging
|
||||||
|
|
||||||
|
log = vlogging.get_logger(__name__)
|
||||||
|
|
||||||
from . import constants
|
from . import constants
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
|
@ -255,13 +260,23 @@ def generate_image_thumbnail(*args, trusted_file=False, **kwargs) -> PIL.Image:
|
||||||
finally:
|
finally:
|
||||||
PIL.Image.MAX_IMAGE_PIXELS = _max_pixels
|
PIL.Image.MAX_IMAGE_PIXELS = _max_pixels
|
||||||
|
|
||||||
|
def image_is_mostly_black(image):
|
||||||
|
tiny = image.copy()
|
||||||
|
tiny.thumbnail((64, 64))
|
||||||
|
pixels = list(tiny.getdata())
|
||||||
|
black_count = 0
|
||||||
|
if tiny.mode == 'RGB':
|
||||||
|
black_count = sum(1 for pixel in pixels if sum(pixel) <= 24)
|
||||||
|
|
||||||
|
return (black_count / len(pixels)) > 0.5
|
||||||
|
|
||||||
def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
|
def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
|
||||||
if not os.path.isfile(filepath):
|
file = pathclass.Path(filepath)
|
||||||
raise FileNotFoundError(filepath)
|
file.assert_is_file()
|
||||||
probe = constants.ffmpeg.probe(filepath)
|
probe = constants.ffmpeg.probe(filepath)
|
||||||
|
|
||||||
if not probe or not probe.video:
|
if not probe or not probe.video:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
size = imagetools.fit_into_bounds(
|
size = imagetools.fit_into_bounds(
|
||||||
image_width=probe.video.video_width,
|
image_width=probe.video.video_width,
|
||||||
|
@ -269,26 +284,25 @@ def generate_video_thumbnail(filepath, width, height, **special) -> PIL.Image:
|
||||||
frame_width=width,
|
frame_width=width,
|
||||||
frame_height=height,
|
frame_height=height,
|
||||||
)
|
)
|
||||||
size = '%dx%d' % size
|
|
||||||
duration = probe.video.duration
|
duration = probe.video.duration
|
||||||
|
|
||||||
if 'timestamp' in special:
|
if 'timestamp' in special:
|
||||||
timestamp = special['timestamp']
|
timestamp_choices = [special['timestamp']]
|
||||||
elif duration < 3:
|
|
||||||
timestamp = 0
|
|
||||||
else:
|
else:
|
||||||
timestamp = 2
|
timestamp_choices = list(range(0, int(duration), 3))
|
||||||
|
|
||||||
|
image = None
|
||||||
|
for this_time in timestamp_choices:
|
||||||
|
log.debug('Attempting video thumbnail at t=%d', this_time)
|
||||||
|
command = kkroening_ffmpeg.input(file.absolute_path, ss=this_time)
|
||||||
|
command = command.filter('scale', size[0], size[1])
|
||||||
|
command = command.output('pipe:', vcodec='bmp', format='image2pipe', vframes=1)
|
||||||
|
(out, trash) = command.run(capture_stdout=True, capture_stderr=True)
|
||||||
|
bio = io.BytesIO(out)
|
||||||
|
image = PIL.Image.open(bio)
|
||||||
|
if not image_is_mostly_black(image):
|
||||||
|
break
|
||||||
|
|
||||||
outfile = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False)
|
|
||||||
constants.ffmpeg.thumbnail(
|
|
||||||
filepath,
|
|
||||||
outfile=outfile.name,
|
|
||||||
quality=2,
|
|
||||||
size=size,
|
|
||||||
time=timestamp,
|
|
||||||
)
|
|
||||||
outfile.close()
|
|
||||||
image = PIL.Image.open(outfile.name)
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
def get_mimetype(extension) -> typing.Optional[str]:
|
def get_mimetype(extension) -> typing.Optional[str]:
|
||||||
|
|
Loading…
Reference in a new issue