ffmpeg command to create thumbnail sprites - unix

To output a single frame from ffmpeg I can do:
ffmpeg -i input.flv -ss 00:00:14.435 -vframes 1 out.png
And to output an image every second, I can do:
ffmpeg -i input.flv -vf fps=1 out%d.png
Would there be a way to create a thumbnail sprite from these outputs to help with the creation of a vtt file for thumbs on seek position?

Here is an example to create jpeg files (280x180) from mp4 video, using ffmpeg, and then assembling this thumbnails into a sprite (png format) using PHP gd2 + writing a VTT file for the video player.
First create 1 image per second with ffmpeg:
ffmpeg -i sprite/MyVideoFile.mp4 -r 1 -s 280x180 -f image2 sprite/thumbs/thumb-%%d.jpg
Then create sprite file + vtt file (example with PHP):
$dirToScan = 'thumbs/';
$filePrefix = 'thumb-';
$fileSuffix = '.jpg';
$thumbWidth = 280;
$thumbHeight = 180;
$imageFiles = array();
$spriteFile = 'sprite.png';
$imageLine = 20;
$vttFile = 'sprite.vtt';
$dst_x = 0;
$dst_y = 0;
# read the directory with thumbnails, file name in array
foreach (glob($dirToScan.$filePrefix.'*'.$fileSuffix) as $filename) {
array_push($imageFiles,$filename);
}
natsort($imageFiles);
#calculate dimension for the sprite
$spriteWidth = $thumbWidth*$imageLine;
$spriteHeight = $thumbHeight*(floor(count($imageFiles)/$imageLine)+1);
# create png file for sprite
$png = imagecreatetruecolor($spriteWidth,$spriteHeight);
# open vtt file
$handle = fopen($vttFile,'wb+');
fwrite($handle,'WEBVTT'."\n");
# insert thumbs in sprite and write the vtt file
foreach($imageFiles AS $file) {
$counter = str_replace($filePrefix,'',str_replace($fileSuffix,'',str_replace($dirToScan,'',$file)));
$imageSrc = imagecreatefromjpeg($file);
imagecopyresized ($png, $imageSrc, $dst_x , $dst_y , 0, 0, $thumbWidth, $thumbHeight, $thumbWidth,$thumbHeight);
$varTCstart = gmdate("H:i:s", $counter-1).'.000';
$varTCend = gmdate("H:i:s", $counter).'.000';
$varSprite = $spriteFile.'#xywh='.$dst_x.','.$dst_y.','.$thumbWidth.','.$thumbHeight;
fwrite($handle,$counter."\n".$varTCstart.' --> '.$varTCend."\n".$varSprite."\n\n");
create new line after 20 images
if ($counter % $imageLine == 0) {
$dst_x=0;
$dst_y+=$thumbHeight;
}
else {
$dst_x+=$thumbWidth;
}
}
imagepng($png,$spriteFile);
fclose($handle);
VTT file looks like:
WEBVTT
1
00:00:00.000 --> 00:00:01.000
sprite.png#xywh=0,0,280,180
2
00:00:01.000 --> 00:00:02.000
sprite.png#xywh=280,0,280,180
3
00:00:02.000 --> 00:00:03.000
sprite.png#xywh=560,0,280,180
...

I am not completely sure what you need/mean with sprite for a vtt file, but there is a nice tool that allows you to combine single images into a big overview picture:
ImageMagick comes with a handy tool called montage
montage - create a composite image by combining several separate images. The images are tiled on the composite image optionally adorned with a border, frame, image name, and more.
You can put the thumbnails together on one or more images with the following command:
montage *.png -tile 4x4 overview.png
It will automatically generate the number of needed pictures to give you an overview.

A little improvement proposal to the answer given by MR_1204: if the final sprite is a PNG, I would avoid the quality decrease caused by JPG compression and rather save the temporary thumbnails as PNGs:
ffmpeg -i sprite/MyVideoFile.mp4 -r 1 -s 280x180 -f image2 sprite/thumbs/thumb-%%d.png
Also, saving to PNG is usually a (tiny) little bit faster than saving to JPG.
Instead, to keep the download small, it might make sense to save (only) the final sprite image as JPG, which in case of PHP and GD only requires to replace imagepng(...) with imagejpeg(...).

Python solution of MR_1204's solution
import ffmpeg
import logging
from pathlib import Path
from config import temp
import os
from PIL import Image
from datetime import datetime, timedelta
import math
logger = logging.getLogger(__name__)
class ScreenshotExtractor:
def __init__(self):
self.screenshot_folder = f"{temp}ss"
self.gap = 10
self.size = "177x100"
self.col = 5
self.ss_width = 177
self.ss_height = 100
def create_sprite_sheet(self, name):
path, dirs, files = next(os.walk(self.screenshot_folder))
file_count = len(files)
img_width = self.ss_width * self.col
img_height = self.ss_height * math.ceil(file_count/self.col)
file_name = f"{name}.jpg"
out_image = Image.new('RGB', (img_width, img_height))
webvtt = "WEBVTT\n\n"
for count, img_file in enumerate(files):
img = Image.open(f"{self.screenshot_folder}/{img_file}")
st_time = timedelta(seconds=count * self.gap)
end_time = timedelta(seconds=(count + 1) * self.gap)
# Adding img to out_file
x_mod = int(count / self.col)
x = 0 + (count - (self.col * x_mod)) * self.ss_width
y = 0 + x_mod * self.ss_height
out_image.paste(img, (x, y))
sprite = f"{file_name}#xywh={x},{y},{self.ss_width},{self.ss_height}"
webvtt += f"{count + 1}\n0{str(st_time)}.000 --> 0{str(end_time)}.000\n{sprite}\n\n"
out_image.save(f"{temp}{file_name}", quality=90)
f = open(f"{temp}{name}.vtt", "w")
f.write(webvtt)
f.close()
return True
def extract_screenshots(self, file_uri, name):
try:
Path(self.screenshot_folder).mkdir(parents=True, exist_ok=True)
vod = ffmpeg.input(file_uri)
vod.output(f"{self.screenshot_folder}/{name}_%04d.png", r=1/self.gap, s=self.size).run()
return True
except Exception as e:
logger.warning(e)
return False
def run(self, file_uri, name):
# TODO - Actually do logic here
self.extract_screenshots(file_uri, name)
self.create_sprite_sheet(name)
Hope this helps somebody

Related

Display file size in a directory

I am hoping to return and print a dictionary of the files and their file size, what I have written is this;
file_size = {}
for fn in glob.glob('*'):
with os.stat(fn) as f:
file_size[fn] = f.st_size
print (file_size)
But I am getting the AtributeError: enter
To use with statement you need to have the methods __enter__() and __exit__() in the object methods.
That is not the case for os.stat(). Remove the with statement and your problem will be fixed:
import glob, os
file_size = {}
for fn in glob.glob('*'):
f = os.stat(fn)
file_size[fn] = f.st_size
print (file_size)

How to save bi-level image to file in julia?

Objective: To save image in bi-level format as demonstrated at https://memorynotfound.com/convert-image-black-white-java/
Code:
using Images, ImageView;
function save_as_binary_image(img_path::String, threshold::Float16)
img_binary = load(img_path);
img_binary = (Gray.(img_binary) .> threshold);
imshow(img_binary);
typeof(img_binary);#=>BitArray{2}
save("/path/to/dest/image.png", img_binary);
img_saved = load("/path/to/dest/image.png");
imshow(img_saved);
typeof(img_saved);#=>Array{Gray{Normed{UInt8,8}},2}
end
save_as_binary_image("/path/to/image/file", convert(Float16, 0.5));
It saves as image of depth 8 but not of depth 1.
Please guide me in saving bi-level image to file!
I'm not an Images.jl user yet (soon perhaps) but here's something that works:
using Images, ImageView
function save_binary_image(img_path, threshold)
img_binary = load(img_path)
#info size(img_binary)
tib = Gray.(Gray.(img_binary) .> threshold)
save("$(img_path)-$(threshold).png", tib)
end
save_binary_image("/tmp/mandrill.png", 0.1)
Perhaps you can slowly modify this to do what you want...
It can be useful to work at the REPL, so that you can see the errors immediately.

xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 0

I'm trying to parse a directory with a collection of xml files from RSS feeds.
I have a similar code for another directory working fine, so I can't figure out the problem. I want to return the items so I can write them to a CSV file. The error I'm getting is:
xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 0
Here is the site I've collected RSS feeds from: https://www.ba.no/service/rss
It worked fine for: https://www.nrk.no/toppsaker.rss and https://www.vg.no/rss/feed/?limit=10&format=rss&categories=&keywords=
Here is the function for this RSS:
import os
import xml.etree.ElementTree as ET
import csv
def baitem():
basepath = "../data_copy/bergens_avisen"
table = []
for fname in os.listdir(basepath):
if fname != "last_feed.xml":
files = ET.parse(os.path.join(basepath, fname))
root = files.getroot()
items = root.find("channel").findall("item")
#print(items)
for item in items:
date = item.find("pubDate").text
title = item.find("title").text
description = item.find("description").text
link = item.find("link").text
table.append((date, title, description, link))
return table
I tested with print(items) and it returns all the objects.
Can it be how the XML files are written?
Asked a friend and said to test with a try except statement. Found a .DS_Store file, which only applies to Mac computers. I'm providing the solution for those who might experience the same problem in the future.
def baitem():
basepath = "../data_copy/bergens_avisen"
table = []
for fname in os.listdir(basepath):
try:
if fname != "last_feed.xml" and fname != ".DS_Store":
files = ET.parse(os.path.join(basepath, fname))
root = files.getroot()
items = root.find("channel").findall("item")
for item in items:
date = item.find("pubDate").text
title = item.find("title").text
description = item.find("description").text
link = item.find("link").text
table.append((date, title, description, link))
except Exception as e:
print(fname, e)
return table

Getting a full resolution image after compiling a montage

I'm using MATLAB to montage several high-resolution images together, register the overlay coordinates into a text file, then reading the text file and loading the montaged image. However, once I have the montage, the individual images making up the montage lose resolution. Is there a way to to display the montage with the full resolution of each individual image still intact?
Here is the code.
file = 'ImageFile.txt';
info = importdata(file);
ImageNames = info.textdata(:,1);
xoffset = info.data(:,1);
yoffset = info.data(:,2);
for i=1:length(ImageNames)
diffx(i) = xoffset(length(ImageNames),1) - xoffset(i,1);
end
diffx = (diffx)';
for j=1:length(ImageNames)
diffy(j) = yoffset(length(ImageNames),1) - yoffset(j,1);
end
diffy = (diffy)';
colormap(gray(256));
for k=1:length(ImageNames)
imshow(ImageNames{k,1}, 'XData', [diffx(k,1) (size(ImageNames{1},2) + diffx(k,1))], 'YData',[diffy(k,1) (size(ImageNames{1}, 1) + diffy(k,1))]), hold on
end
This is the method that I used to implement montage. You might not feel the same.
I would assume that you have the co-ordinates of every image location in final montage.
Say I have to create a montage of 9 images and they are named as 1.jpg, 2.jpg, 3.jpg,... 9.jpg.
for i=1:9
filename = sprintf('%d.jpg',i);
a{i} = imread(filename);
end
montage = [a{1} a{2} a{3}; a{4} a{5} a{6}; a{7} a{8} a{9}];
imshow(montage);
imwrite(montage, 'montage.jpg');

It is possible to compile less file to less with compression and without comments?

i have much more less files that are imported in a main.less file. Now i wanna to make a main.min.less file with imported files and compressed and without any comments. what command i used is:
lessc main.less > main.min.less -x
This command compress the file but can't remove the restricted comments(/*! comments */).
Keep in mind i wanna to make another .less file and not .css file. Any idea?
Because of Less code is very similar to CSS code, you should be able to (re)use many methods of for instance clean-css.
Create a file called clean-less and write down the following content into it:
#!/usr/bin/env node
var path = require('path'),
CommentsProcessor = require('clean-css/lib/text/comments'),
fs = require('fs');
var args = process.argv.slice(1);
var input = args[1];
if (input && input != '-') {
input = path.resolve(process.cwd(), input);
}
var parseFile = function (e, data) {
var lineBreak = process.platform == 'win32' ? '\r\n' : '\n';
var commentsProcessor = new CommentsProcessor(0,false,lineBreak);
//single line comments
var regex = /\/\/ .*/;
data = data.replace(/\/{2,}.*/g,"");
/*
methods from clean css, see https://github.com/jakubpawlowicz/clean-css/
*/
var replace = function() {
if (typeof arguments[0] == 'function')
arguments[0]();
else
data = data.replace.apply(data, arguments);
};
//multi line comments
replace(function escapeComments() {
data = commentsProcessor.escape(data);
});
// whitespace inside attribute selectors brackets
replace(/\[([^\]]+)\]/g, function(match) {
return match.replace(/\s/g, '');
});
// line breaks
replace(/[\r]?\n/g, ' ');
// multiple whitespace
replace(/[\t ]+/g, ' ');
// multiple semicolons (with optional whitespace)
replace(/;[ ]?;+/g, ';');
// multiple line breaks to one
replace(/ (?:\r\n|\n)/g, lineBreak);
replace(/(?:\r\n|\n)+/g, lineBreak);
// remove spaces around selectors
replace(/ ([+~>]) /g, '$1');
// remove extra spaces inside content
replace(/([!\(\{\}:;=,\n]) /g, '$1');
replace(/ ([!\)\{\};=,\n])/g, '$1');
replace(/(?:\r\n|\n)\}/g, '}');
replace(/([\{;,])(?:\r\n|\n)/g, '$1');
replace(/ :([^\{\};]+)([;}])/g, ':$1$2');
process.stdout.write(data);
}
if (input != '-') {
fs.readFile(input, 'utf8', parseFile);
}
Than make the clean-less file executable on your system (chmod +x clean-css).
You should probably also run npm install path and npm install css. After that you should be able to run:
./cleanless input.less > input-clean.less
The input-clean.less file will be some kind of minimized and not contain comments any more.
In the comments #seven-phases-max wonders if minifying reduces client side compile time. Well the client side compiler loads the Less files over a XMLHttpRequest. Reduce the number of bytes will give faster loading. I can not say that is a bottleneck or not.
When i try the above code with the navbar.less file from Bootstrap i found:
8.0K navbar-clean.less
16K navbar.less

Resources