33 Commits

Author SHA1 Message Date
Flavio Ribeiro
3494382958 Merge pull request #30 from bradleyfalzon/pil-constant
Rename Pillow constant Image.ANTIALIAS to Image.Resampling.LANCZOS
2023-08-23 13:10:06 -04:00
Bradley Falzon
214f34e9c2 Add exception message to output when an exception occurs 2023-08-23 19:19:20 +09:30
Bradley Falzon
59ef76dce6 Rename Pillow constant Image.ANTIALIAS to Image.Resampling.LANCZOS
Image.ANTIALIAS was deprecated in Pillow v2.7 in favour of Image.LANCZOS,
and removed in v10. Instead Image.LANCZOS or Image.Resampling.LANCZOS
should be used.
2023-08-23 18:47:15 +09:30
Flavio Ribeiro
f60c80139c Merge pull request #23 from phloxic/issue22
Fix off by 1 reported total frame count (#22)
2021-05-25 12:01:12 -04:00
Christian Ebert
2a95a84bdb Fix off by 1 reported total frame count (#22) 2021-05-18 21:42:22 +01:00
Flavio Ribeiro
db80e95671 Merge pull request #19 from s0lesurviv0r/master
Parallelism and PEP8 compliance
2021-05-18 10:16:31 -07:00
Jacob Zelek
1c8342440b Only display file name and not full path in stdout 2020-12-30 14:28:08 -08:00
Jacob Zelek
ebe573910a Skip over thumbnails that already exist 2020-12-26 15:27:04 -08:00
Jacob Zelek
7ca81d03bd Fix for Queue when empty 2020-12-26 15:23:13 -08:00
Jacob Zelek
81dd25829f Update doc to indicate parallelism arg is optional 2020-12-17 06:31:25 -08:00
Jacob Zelek
8d1223b646 Bug fix - worker was only taking one item and terminating after 2020-12-17 06:28:48 -08:00
Jacob Zelek
7a0cf18c94 Using processes instead of process pool map to solve mem issue 2020-12-17 03:34:00 -08:00
Jacob Zelek
2cdccec800 Bug fix - cast parallelism input to int 2020-12-14 03:00:14 -08:00
Jacob Zelek
71ca66802d Parallelism opt is now optional, bug fix for output path 2020-12-14 02:58:47 -08:00
Jacob Zelek
e3355f49e2 Documentation update 2020-11-02 22:09:21 -08:00
Jacob Zelek
4f635b68cc Parallel processing 2020-11-02 22:03:29 -08:00
Jacob Zelek
e9dd9ab799 Directory support added 2020-11-02 21:24:24 -08:00
Jacob Zelek
493bd76f0e Refactored to pass PEP8 rules 2020-11-02 20:39:41 -08:00
Flavio Ribeiro
542f0d2e36 Merge pull request #18 from Voodfy/master
Adding support to python3
2020-09-26 11:49:30 -04:00
Leandro Barbosa
1a1e047484 update to python3 2020-08-22 02:36:19 -03:00
Leandro Barbosa
2248f57ca5 update for pip3 2020-08-22 02:34:07 -03:00
Leandro Barbosa
3403f73758 fix problem with print on python3 2020-08-22 02:33:11 -03:00
Flávio Ribeiro
f1eb403c1f Merge pull request #14 from blacktrash/blacktrash-fixes
q&d fixes
2018-04-18 12:47:05 -04:00
Christian Ebert
79405353d0 Simple failover to RGB mode (#13) 2018-04-18 00:44:17 +01:00
Christian Ebert
4161b485f2 Create and clean up a directory for temporary images (#12) 2018-04-18 00:33:23 +01:00
Flávio Ribeiro
ed56b0912b update requirements (fix #10) 2017-06-17 13:08:42 -04:00
Flávio Ribeiro
2bcf475007 Merge pull request #9 from Pichok/generate_thumbnails_of_large_movies
Generate Thumbnails of large movies
2017-01-06 11:54:52 -05:00
Guilherme Pichok
1897d40707 Generate Thumbnails of large movies 2017-01-04 01:09:10 -02:00
Flávio Ribeiro
37c199c42a Update README.md 2016-11-08 10:53:57 -05:00
Flávio Ribeiro
601521c03a update README 2016-06-08 13:16:23 -04:00
Flávio Ribeiro
b77fbd8b7d requirements: add click dependency (close #8) 2016-06-08 13:14:51 -04:00
Flávio Ribeiro
19fdb4fb86 Update README.md 2015-11-21 22:51:36 -05:00
Flávio Ribeiro
28006a2f7a Update README.md 2015-11-21 22:50:48 -05:00
4 changed files with 183 additions and 65 deletions

View File

@@ -1,11 +1,11 @@
# Video thumbnail generator # Video thumbnail generator
> Generate thumbnail sprites from videos. Generate thumbnail sprites from videos.
## Why ## Why
![image](https://cloud.githubusercontent.com/assets/244265/11234416/b1a67230-8d95-11e5-97a4-c2acdcbf72f7.png) ![image](https://cloud.githubusercontent.com/assets/244265/11234416/b1a67230-8d95-11e5-97a4-c2acdcbf72f7.png)
Generate a thumbnail sprite shouldn't be hard, because almost all video players enhances user's seekbar navigation by providing a thumbnail preview of the moments where the user want to seek. This is a python script that, given a video, generate a thumbnail sprite image from it. Almost all video players enhances user's seekbar navigation by providing a thumbnail preview of the moments where the user want to seek, so generate this sprites shouldn't be hard. This is a python script that, given a video, generates a thumbnail sprite image from it.
## Build ## Build
@@ -32,7 +32,7 @@ $ ./generator --help
Video Thumbnail Generator Video Thumbnail Generator
Usage: Usage:
./generator <video> <interval> <width> <height> <columns> <output> ./generator <video> <interval> <width> <height> <columns> <output> [<parallelism>]
./generator (-h | --help) ./generator (-h | --help)
./generator --version ./generator --version
@@ -45,26 +45,36 @@ Options:
<height> Height of each thumbnail. <height> Height of each thumbnail.
<columns> Total number of thumbnails per line. <columns> Total number of thumbnails per line.
<output> Output. <output> Output.
[<parallelism>] Number of files to process in parallel
``` ```
## Example ## Example
**Single file**
```shell ```shell
$ ./generator videos/27467_1_milkbots_wg_720p.mp4 2 126 73 10 thumbnails.jpg $ ./generator samples/sample.mp4 60 300 200 2 output/sample.mp4.png
Extracting 101 frames [sample.mp4] Extracting frame 1/3
.................................................................. Frames extracted. [sample.mp4] Extracting frame 2/3
[sample.mp4] Extracting frame 3/3
[sample.mp4.png] Savedacted.
Saved! Saved!
``` ```
**Directory**
```shell
$ ./generator samples/ 60 300 200 2 output/
[sample copy.mp4] Extracting frame 1/3
[sample.mp4] Extracting frame 1/3
[sample copy.mp4] Extracting frame 2/3
[sample.mp4] Extracting frame 2/3
[sample copy.mp4] Extracting frame 3/3
[sample.mp4] Extracting frame 3/3
[sample copy.mp4.png] Saved
[sample.mp4.png] Saved
```
![image](https://cloud.githubusercontent.com/assets/244265/11234316/b42913a6-8d94-11e5-865a-128ea8d801f7.png) ![image](https://cloud.githubusercontent.com/assets/244265/11234316/b42913a6-8d94-11e5-865a-128ea8d801f7.png)
## Browser Support
![IE](https://cloud.githubusercontent.com/assets/398893/3528325/20373e76-078e-11e4-8e3a-1cb86cf506f0.png) | ![Chrome](https://cloud.githubusercontent.com/assets/398893/3528328/23bc7bc4-078e-11e4-8752-ba2809bf5cce.png) | ![Firefox](https://cloud.githubusercontent.com/assets/398893/3528329/26283ab0-078e-11e4-84d4-db2cf1009953.png) | ![Opera](https://cloud.githubusercontent.com/assets/398893/3528330/27ec9fa8-078e-11e4-95cb-709fd11dac16.png) | ![Safari](https://cloud.githubusercontent.com/assets/398893/3528331/29df8618-078e-11e4-8e3e-ed8ac738693f.png)
--- | --- | --- | --- | --- |
IE 9+ ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ |
## Contributing ## Contributing
1. Fork it! 1. Fork it!

2
build
View File

@@ -5,5 +5,5 @@ if [ "$UNAME_S" == "Darwin" ]; then
ln -s /usr/local/include/freetype2 /usr/local/include/freetype ln -s /usr/local/include/freetype2 /usr/local/include/freetype
fi fi
pip install -r requirements.txt pip3 install -r requirements.txt
chmod a+x generator chmod a+x generator

193
generator
View File

@@ -1,9 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python3
"""Video Thumbnail Generator """Video Thumbnail Generator
Usage: Usage:
./generator <video> <interval> <width> <height> <columns> <output> ./generator <video> <interval> <width> <height> <columns> <output> [<parallelism>]
./generator (-h | --help) ./generator (-h | --help)
./generator --version ./generator --version
@@ -16,76 +16,185 @@ Options:
<height> Height of each thumbnail. <height> Height of each thumbnail.
<columns> Total number of thumbnails per line. <columns> Total number of thumbnails per line.
<output> Output. <output> Output.
[<parallelism>] Number of files to process in parallel.
""" """
from docopt import docopt from docopt import docopt
from moviepy.editor import VideoFileClip from moviepy.editor import VideoFileClip
from PIL import Image from PIL import Image
from click import progressbar from click import progressbar
import glob, os, random, shutil, math, tempfile from collections import namedtuple
from multiprocessing import cpu_count, Queue, Process
import glob
import os
import random
import shutil
import math
import tempfile
import sys
TMP_FRAMES_PATH = tempfile.mkstemp()[1] TMP_FRAMES_PATH = tempfile.mkdtemp()
def generate_video_thumbnail(args):
videoFileClip = VideoFileClip(args['<video>']) def generate_video_thumbnails(args):
input_path = args['<video>']
interval = int(args['<interval>']) interval = int(args['<interval>'])
size = (int(args['<width>']), int(args['<height>'])) size = (int(args['<width>']), int(args['<height>']))
outputPrefix = get_output_prefix()
generate_frames(videoFileClip, interval, outputPrefix, size)
columns = int(args['<columns>']) columns = int(args['<columns>'])
output = args['<output>'] output_path = args['<output>']
generate_sprite_from_frames(outputPrefix, columns, size, output) parallelism = args.get('[<parallelism>]', cpu_count()*2-1)
def generate_frames(videoFileClip, interval, outputPrefix, size): work_queue = Queue()
print "Extracting", int(videoFileClip.duration / interval), "frames" work_units = 0
frameCount = 0
with progressbar(range(0, int(videoFileClip.duration), interval)) as items:
for i in items:
extract_frame(videoFileClip, i, outputPrefix, size, frameCount)
frameCount += 1
print "Frames extracted."
def extract_frame(videoFileClip, moment, outputPrefix, size, frameCount): if os.path.isdir(input_path):
output = outputPrefix + ("%05d.png" % frameCount) # Ensure output path is also directory
videoFileClip.save_frame(output, t=int(moment)) if not os.path.isdir(output_path):
print(
"If input path is directory then "
"output path must be directory"
)
sys.exit(1)
# Strip seperator so contructing output is uniform
output_path = output_path.rstrip(os.sep)
# Add all files in directory for processing
for file_name in os.listdir(input_path):
file_path = os.path.join(input_path, file_name)
if os.path.isfile(file_path):
# Construct output path for thumbnail using
# the video files filename
single_output_path = os.path.join(
output_path, os.path.basename(file_path) + ".png"
)
work_queue.put((file_path, single_output_path,
interval, size, columns,))
work_units += 1
else:
work_queue.put((input_path, output_path, interval, size, columns,))
work_units += 1
# Limit the number of parallel jobs if lower number of files
parallelism = min(int(parallelism), work_units)
# Start worker processes
processes = []
for i in range(parallelism):
p = Process(target=worker, args=(work_queue,))
p.start()
processes.append(p)
# Block until all processes complete
for p in processes:
p.join()
def worker(queue):
while True:
try:
work_unit = queue.get(False)
# If no work unit then quit
if not work_unit:
break
# Handle exception when no more items in queue
except:
break
input_file, output_file, interval, size, columns = work_unit
file_name = os.path.basename(input_file)
# Skip over any thumbnails that exist already
if os.path.exists(output_file):
print("[{file_name}] Already exists, skipping".format(
file_name=file_name))
continue
try:
video_file_clip = VideoFileClip(input_file)
output_prefix = get_output_prefix()
generate_frames(file_name, video_file_clip,
interval, output_prefix, size)
generate_sprite_from_frames(output_prefix, columns,
size, output_file)
except Exception as e:
print("[{file_name}] Error occurred with file: {error}".format(
file_name=file_name, error=e))
def generate_frames(file_name, video_file_clip, interval, output_prefix, size):
duration = video_file_clip.duration
frame_count = 0
total_frames = int(duration / interval)
for i in range(0, int(duration), interval):
frame_count += 1
print("[{file_name}] Extracting frame {current}/{total}".
format(file_name=file_name, current=frame_count,
total=total_frames))
extract_frame(video_file_clip, i, output_prefix, size, frame_count)
def extract_frame(video_file_clip, moment, output_prefix, size, frame_count):
output = output_prefix + ("%05d.png" % frame_count)
video_file_clip.save_frame(output, t=int(moment))
resize_frame(output, size) resize_frame(output, size)
def resize_frame(filename, size): def resize_frame(filename, size):
image = Image.open(filename) image = Image.open(filename)
image = image.resize(size, Image.ANTIALIAS) image = image.resize(size, Image.Resampling.LANCZOS)
image.save(filename) image.save(filename)
def generate_sprite_from_frames(framesPath, columns, size, output):
framesMap = sorted(glob.glob(framesPath + "*.png"))
images = [Image.open(filename) for filename in framesMap]
masterWidth = size[0] * columns
masterHeight = size[1] * int(math.ceil(float(len(images)) / columns))
finalImage = Image.new(mode='RGBA', size=(masterWidth, masterHeight), color=(0,0,0,0))
merge_frames(images, finalImage, columns, size, output)
def merge_frames(images, finalImage, columns, size, output): def generate_sprite_from_frames(frames_path, columns, size, output):
line, column = 0, 0 frames_map = sorted(glob.glob(frames_path + "*.png"))
for image in images:
locationX = size[0] * column master_width = size[0] * columns
locationY = size[1] * line master_height = size[1] * int(math.ceil(float(len(frames_map)) / columns))
finalImage.paste(image, (locationX, locationY))
line, column, mode = 0, 0, 'RGBA'
try:
final_image = Image.new(
mode=mode,
size=(master_width, master_height),
color=(0, 0, 0, 0)
)
final_image.save(output)
except IOError:
mode = 'RGB'
final_image = Image.new(mode=mode, size=(master_width, master_height))
for filename in frames_map:
with Image.open(filename) as image:
location_x = size[0] * column
location_y = size[1] * line
final_image.paste(image, (location_x, location_y))
column += 1 column += 1
if column == columns: if column == columns:
line += 1 line += 1
column = 0 column = 0
finalImage.save(output, transparency=0) final_image.save(output)
shutil.rmtree(TMP_FRAMES_PATH, ignore_errors=True) shutil.rmtree(frames_path, ignore_errors=True)
print "Saved!" output_file = os.path.basename(output)
print("[{output_file}] Saved".format(output_file=output_file))
def get_output_prefix(): def get_output_prefix():
if not os.path.exists(TMP_FRAMES_PATH): if not os.path.exists(TMP_FRAMES_PATH):
os.makedirs(TMP_FRAMES_PATH) os.makedirs(TMP_FRAMES_PATH)
return TMP_FRAMES_PATH + ("%032x_" % random.getrandbits(128)) return TMP_FRAMES_PATH + os.sep + ("%032x_" % random.getrandbits(128))
if __name__ == "__main__": if __name__ == "__main__":
arguments = docopt(__doc__, version='0.0.2') arguments = docopt(__doc__, version='0.0.2')
generate_video_thumbnail(arguments) generate_video_thumbnails(arguments)

View File

@@ -1,5 +1,4 @@
docopt==0.6.1 docopt==0.6.1
moviepy moviepy
--allow-external PIL Pillow
--allow-unverified PIL click
PIL