Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3494382958 | ||
|
|
214f34e9c2 | ||
|
|
59ef76dce6 | ||
|
|
f60c80139c | ||
|
|
2a95a84bdb | ||
|
|
db80e95671 | ||
|
|
1c8342440b | ||
|
|
ebe573910a | ||
|
|
7ca81d03bd | ||
|
|
81dd25829f | ||
|
|
8d1223b646 | ||
|
|
7a0cf18c94 | ||
|
|
2cdccec800 | ||
|
|
71ca66802d | ||
|
|
e3355f49e2 | ||
|
|
4f635b68cc | ||
|
|
e9dd9ab799 | ||
|
|
493bd76f0e | ||
|
|
542f0d2e36 | ||
|
|
1a1e047484 | ||
|
|
2248f57ca5 | ||
|
|
3403f73758 | ||
|
|
f1eb403c1f | ||
|
|
79405353d0 | ||
|
|
4161b485f2 | ||
|
|
ed56b0912b | ||
|
|
2bcf475007 | ||
|
|
1897d40707 | ||
|
|
37c199c42a | ||
|
|
601521c03a | ||
|
|
b77fbd8b7d | ||
|
|
19fdb4fb86 | ||
|
|
28006a2f7a | ||
|
|
a49846cf0a | ||
|
|
abb32c50e0 | ||
|
|
6640769595 | ||
|
|
337dbbc11f | ||
|
|
681bcb37e4 | ||
|
|
6c5aaeb20b | ||
|
|
fae5db9f75 | ||
|
|
2d4c88da9e | ||
|
|
b08c30cd69 |
65
README.md
65
README.md
@@ -1,25 +1,38 @@
|
|||||||
# video-thumbnail-generator
|
# Video thumbnail generator
|
||||||
Generate thumbnail sprites from videos.
|
Generate thumbnail sprites from videos.
|
||||||
|
|
||||||
### Why?
|
## Why
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Almost all video players enhances user's seekbar navigation by providing a thumbnail preview of the moments where the user can seek to. 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
|
||||||
|
|
||||||
Clone this repository. On project folder:
|
1. Clone it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ git clone git@github.com:flavioribeiro/video-thumbnail-generator.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Then go to the project's folder:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ cd video-thumbnail-generator
|
||||||
|
```
|
||||||
|
|
||||||
|
3. And finally run:
|
||||||
```shell
|
```shell
|
||||||
$ chmod a+x build && ./build
|
$ chmod a+x build && ./build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run
|
## Run
|
||||||
```shell
|
```shell
|
||||||
$ chmod a+x generator && ./generator --help
|
$ ./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
|
||||||
|
|
||||||
@@ -32,18 +45,44 @@ 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 27467_1_milkbots_wg_with_script.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
|
||||||
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### License
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork it!
|
||||||
|
2. Create your feature branch: `git checkout -b my-awesome-new-feature`
|
||||||
|
3. Commit your changes: `git commit -m 'Add some awesome feature'`
|
||||||
|
4. Push to the branch: `git push origin my-awesome-new-feature`
|
||||||
|
5. Submit a pull request :]
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
This code is under [Apache License](https://github.com/flavioribeiro/video-thumbnail-generator/blob/master/LICENSE).
|
This code is under [Apache License](https://github.com/flavioribeiro/video-thumbnail-generator/blob/master/LICENSE).
|
||||||
|
|||||||
3
build
3
build
@@ -5,4 +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
|
||||||
|
|||||||
213
generator
213
generator
@@ -1,91 +1,200 @@
|
|||||||
#!/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
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h --help Show this screen.
|
-h --help Show this screen.
|
||||||
--version Show version.
|
--version Show version.
|
||||||
<video> Video filepath.
|
<video> Video filepath.
|
||||||
<interval> Interval em seconds between frames.
|
<interval> Interval em seconds between frames.
|
||||||
<width> Width of each thumbnail.
|
<width> Width of each thumbnail.
|
||||||
<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
|
||||||
import glob, os, random, sys, shutil, math
|
from click import progressbar
|
||||||
|
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="/tmp/frames/"
|
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_images(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
|
|
||||||
for i in range(0, int(videoFileClip.duration), interval):
|
|
||||||
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):
|
||||||
sys.stdout.write(".")
|
# Ensure output path is also directory
|
||||||
sys.stdout.flush()
|
if not os.path.isdir(output_path):
|
||||||
output = outputPrefix + ("%05d.png" % frameCount)
|
print(
|
||||||
videoFileClip.save_frame(output, t=int(moment))
|
"If input path is directory then "
|
||||||
resize_image(output, size)
|
"output path must be directory"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def resize_image(filename, size):
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
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_images(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))
|
|
||||||
column += 1
|
line, column, mode = 0, 0, 'RGBA'
|
||||||
if column == columns:
|
|
||||||
line += 1
|
try:
|
||||||
column = 0
|
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
|
||||||
|
|
||||||
|
if column == columns:
|
||||||
|
line += 1
|
||||||
|
column = 0
|
||||||
|
|
||||||
|
final_image.save(output)
|
||||||
|
shutil.rmtree(frames_path, ignore_errors=True)
|
||||||
|
output_file = os.path.basename(output)
|
||||||
|
print("[{output_file}] Saved".format(output_file=output_file))
|
||||||
|
|
||||||
finalImage.save(output, transparency=0)
|
|
||||||
shutil.rmtree(TMP_FRAMES_PATH, ignore_errors=True)
|
|
||||||
print "Saved!"
|
|
||||||
|
|
||||||
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.1')
|
arguments = docopt(__doc__, version='0.0.2')
|
||||||
generate_video_thumbnail(arguments)
|
generate_video_thumbnails(arguments)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
docopt==0.6.1
|
docopt==0.6.1
|
||||||
moviepy
|
moviepy
|
||||||
--allow-external PIL
|
Pillow
|
||||||
--allow-unverified PIL
|
click
|
||||||
PIL
|
|
||||||
|
|||||||
Reference in New Issue
Block a user