diff --git a/README b/README new file mode 100644 index 0000000..5ad2b58 --- /dev/null +++ b/README @@ -0,0 +1,26 @@ +oxtimelines - create timeline from video + +This program takes one or more video files as input and outputs timeline images. +If a cuts path is given, it also outputs a json file containing cuts. If in and +out points are given, only that part of the video(s) will be rendered. + +The timeline modes can be any combination of 'antialias' (average color), +'slitscan' (center pixel), 'keyframes' (one or more frames per cut), 'audio' +(waveform), 'cuts' (antialias with cut detection overlay, for debugging) and +'data' (each frame resized to 8x8 px). + +One or two timeline heights can be specified, larger height first. The timeline +widths will be 1 px per frame for the first one, and 1 px per second for the +second (smaller) one. If the wide option is set, large 'keyframeswide' tiles +will be rendered. They can be used at a later point to render small 'keyframes' +tiles without having to decode the video again. + +depends on + gstreamer 0.10.30 or newer + python-imaging + gst-python + python-ox + +on ubuntu 10.04 you need + sudo add-apt-repository ppa:gstreamer-developers/ppa + diff --git a/bin/oxtimelines b/bin/oxtimelines old mode 100644 new mode 100755 index 6da5987..6523c06 --- a/bin/oxtimelines +++ b/bin/oxtimelines @@ -16,10 +16,12 @@ if os.path.exists(os.path.join(root, 'oxtimelines')): import ox import oxtimelines -from oxtimelines import video -''' -This program takes one or more video files as input and outputs timeline images. +# fixme: -w option should be 'keyframeswide' mode + +if __name__ == '__main__': + usage = ''' +%prog takes one or more video files as input and outputs timeline images. If a cuts path is given, it also outputs a json file containing cuts. If in and out points are given, only that part of the video(s) will be rendered. @@ -33,35 +35,26 @@ widths will be 1 px per frame for the first one, and 1 px per second for the second (smaller) one. If the wide option is set, large 'keyframeswide' tiles will be rendered. They can be used at a later point to render small 'keyframes' tiles without having to decode the video again. -''' -# fixme: -w option should be 'keyframeswide' mode - -if __name__ == '__main__': - parser = OptionParser() - parser.add_option('-v', '--videos', dest='videos', help='video file(s)') - parser.add_option('-t', '--tiles', dest='tiles', help='path for combined timeline tiles') +usage: %prog [options] video1 video2''' + parser = OptionParser(usage=usage) + parser.add_option('-o', '--output', dest='tiles', help='path for combined timeline tiles') parser.add_option('-c', '--cuts', dest='cuts', help='path for combined cuts json file') parser.add_option('-p', '--points', dest='points', help='inpoint,outpoint (optional)') parser.add_option('-m', '--modes', dest='modes', help='timeline mode(s) (antialias, slitscan, keyframes, audio, cuts, data)') - parser.add_option('-s', '--sizes', dest='sizes', help='timeline size(s) (large or large,small)') + parser.add_option('-s', '--sizes', dest='sizes', help='timeline size(s) (64 or 64,16)') parser.add_option('-w', '--wide', dest='wide', default=False, action='store_true', help='keep wide frames tiles') parser.add_option('-l', '--log', dest='log', default=False, action='store_true', help='log performance') (opts, args) = parser.parse_args() - if None in (opts.videos, opts.modes, opts.sizes): + if None in (opts.modes, opts.sizes, opts.tiles) or not args: parser.print_help() sys.exit() - opts.videos = map(lambda x: os.path.abspath(x), opts.videos.split(',')) + opts.videos = map(os.path.abspath, args) if opts.points: opts.points = map(float, opts.points.split(',')) - opts.modes = opts.modes.split(',') + opts.modes = [m.strip() for m in opts.modes.split(',')] opts.sizes = map(int, opts.sizes.split(',')) - ''' - for f in glob('%s*.png' % opts.tiles): - os.unlink(f) - ''' - - video.Timelines(opts.videos, opts.tiles, opts.cuts, opts.points, opts.modes, opts.sizes, opts.wide, opts.log).render() + oxtimelines.Timelines(opts.videos, opts.tiles, opts.cuts, opts.points, opts.modes, opts.sizes, opts.wide, opts.log).render() diff --git a/oxtimelines/__init__.py b/oxtimelines/__init__.py index 9612b38..95dc6d1 100644 --- a/oxtimelines/__init__.py +++ b/oxtimelines/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # vi:si:et:sw=4:sts=4:ts=4 # GPL 2008-2010 +__version__ = 'bzr' import gobject gobject.threads_init() @@ -15,5 +16,5 @@ pygst.require("0.10") import gst import Image -import video -#import audio \ No newline at end of file +import timeline +from timeline import Timelines diff --git a/oxtimelines/video.py b/oxtimelines/timeline.py similarity index 95% rename from oxtimelines/video.py rename to oxtimelines/timeline.py index 4d9ff1c..dbba494 100644 --- a/oxtimelines/video.py +++ b/oxtimelines/timeline.py @@ -4,9 +4,9 @@ from __future__ import division +from glob import glob import Image import math -import numpy import os from time import time, strftime @@ -14,7 +14,7 @@ import gobject import gst from imagesink import ImageSink -from ox import avinfo +import ox FPS = 25 @@ -44,11 +44,11 @@ class Video(gst.Pipeline): self.add(self.src, self.sbin) if self.video: - info = avinfo(uri) + info = ox.avinfo(uri) ratio = info['video'][0]['width'] / info['video'][0]['height'] self.width = int(round(self.height * ratio)) if self.width % 4: - self.width += 4 - self.width % 4 + self.width += 4 - self.width % 4 self.vqueue = gst.element_factory_make('queue') self.scale = gst.element_factory_make('videoscale') self.rate = gst.element_factory_make('videorate') @@ -234,11 +234,12 @@ class Timelines(): self.large_tile_w = 1500 self.large_tile_h = sizes[0] self.large_tile_image = {} + self.render_small_tiles = False if len(sizes) == 2: self.small_tile_w = 3600 self.small_tile_h = sizes[1] self.small_tile_image = {} - self.render_small_tiles = True + self.render_small_tiles = True self.render_wide_tiles = render_wide_tiles @@ -250,6 +251,8 @@ class Timelines(): self.profiler = Profiler() self.profiler.set_task('gst') + ox.makedirs(self.tile_path) + def render(self): if self.points: @@ -297,7 +300,7 @@ class Timelines(): self.frame_ratio = frame_size[0] / frame_size[1] self.frame_center = int(frame_size[0] / 2) - self.large_tile_n = int(math.ceil(self.frame_n / self.large_tile_w)) + self.large_tile_n = int(math.ceil(self.frame_n / self.large_tile_w)) self.large_tile_last_w = self.frame_n % self.large_tile_w if self.render_small_tiles: self.small_tile_n = int(math.ceil(self.duration / self.small_tile_w)) @@ -315,6 +318,17 @@ class Timelines(): self.frame_offset = 0 self.videos[0].decode(self.file_points[0]) + #remove tiles that might exist from previous run + if not self.points: + for mode in self.modes: + tiles = glob('%s/timeline%s*%d*.jpg' % (self.tile_path, mode, self.large_tile_h)) + for f in ox.sorted_strings(tiles)[self.large_tile_i+2:]: + os.unlink(f) + if self.render_small_tiles: + tiles = glob('%s/timeline%s*%d*.jpg' % (self.tile_path, mode, self.small_tile_h)) + for f in ox.sorted_strings(tiles)[self.small_tile_i+2:]: + os.unlink(f) + def _video_callback(self, frame_image, timestamp): self.log and self.profiler.set_task('_video_callback()') @@ -351,7 +365,7 @@ class Timelines(): if self.render_slitscan: crop = (self.frame_center, 0, self.frame_center + 1, self.large_tile_h) self.large_tile_image['slitscan'].paste(frame_image.crop(crop), paste) - self.log and self.profiler.unset_task() + self.log and self.profiler.unset_task() # render data tile if self.render_data or self.detect_cuts: @@ -426,7 +440,7 @@ class Timelines(): large_keyframes_tile_i = self.large_keyframes_tile_i for image_w in image_widths: frame_image = self.cut_frames[image_i - self.cuts[-2]] - frame_image.save('deleteme.jpg') + #frame_image.save('deleteme.jpg') if mode == 'keyframeswide': resize = (self.wide_frame_w, self.large_tile_h) self.log and self.profiler.set_task('i,resize((w, h)) # keyframeswide timelines') @@ -468,7 +482,7 @@ class Timelines(): if large_tile_x == 0: large_tile_i = int(self.frame_i / self.large_tile_w) if large_tile_i < self.large_tile_n - 1: - w = self.large_tile_w + w = self.large_tile_w else: w = self.large_tile_last_w self.large_tile_image['audio'] = Image.new('L', (w, self.large_tile_h)) @@ -535,14 +549,15 @@ class Timelines(): self.full_tile_image.save(tile_file) if self.log: print tile_file - resize = (self.full_tile_w, self.small_tile_h) - self.full_tile_image = self.full_tile_image.resize(resize, Image.ANTIALIAS) - tile_file = '%stimelineantialias%dp.jpg' % ( - self.tile_path, self.small_tile_h - ) - self.full_tile_image.save(tile_file) - if self.log: - print tile_file + if self.render_small_tiles: + resize = (self.full_tile_w, self.small_tile_h) + self.full_tile_image = self.full_tile_image.resize(resize, Image.ANTIALIAS) + tile_file = '%stimelineantialias%dp.jpg' % ( + self.tile_path, self.small_tile_h + ) + self.full_tile_image.save(tile_file) + if self.log: + print tile_file self.log and self.profiler.unset_task() def _save_tile(self, mode, index): diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6da7dc6 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +# setup.py +# -*- coding: UTF-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +try: + from setuptools import setup +except: + from distutils.core import setup + +def get_bzr_version(): + import os + info = os.path.join(os.path.dirname(__file__), '.bzr/branch/last-revision') + if os.path.exists(info): + f = open(info) + rev = int(f.read().split()[0]) + f.close() + if rev: + return u'%s' % rev + return u'unknown' + +setup(name='oxtimelines', + version='0.%s' % get_bzr_version() , + scripts=[ + 'bin/oxtimelines', + ], + packages=[ + 'oxtimelines', + ], + author='0x2620', + author_email='0x2620@0x2620.org', + description='extract timelines from videos', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Utilities' + ], +) +