#!/usr/bin/env python """ desub.py Simple program to eliminate subtitles. Reference: http://www.animemusicvideos.org/guides/subremoval/default.htm Requires ImageMagick (GraphicsMagick might also work) and Python 2.3.0 or greater. Copyright (C) 2004-2005 Marco De la Cruz (marco@reimeika.ca) It's a trivial program, but might as well GPL it, so see: http://www.gnu.org/copyleft/gpl.html for license. See my vids at http://amv.reimeika.ca ! """ # Change this if you want to use a different temp dir, # otherwise use current dir. tempdir='.' # No need to change anything below this. version = '0.0.8' convert = 'convert' composite = 'composite' mogrify = 'mogrify' identify = 'identify -format "%w %h" ' crop = '-crop' append = '-append' resize = '-resize' flip = '-flip' geometry = '-geometry' matte = '+matte' etonull = '2> /dev/null' usage = \ """ desub.py -h desub.py -V desub.py firstframe [lastframe] desub.py -c [-v|-q] [-s height] [-p offset|c] [-t] [-x] [-e ext] firstframe [lastframe] desub.py -r [-v|-q] [-s height] [-t] [-x] [-e ext] [-k keyframe] firstframe [lastframe] desub.py -o [-v|-q] -g geometry [-x] [-e ext] [-k keyframe] firstframe [lastframe] """ help = \ """ SUMMARY (ver. %s): Eliminates subtitles using either "selective cropping" (-c), via a "replacement" (-r), or using an "overlay" (-o), starting on firstframe and ending at lastframe (filenames are assumed to be in numerical order and of constant length with the same image file extension e.g. 000345.png to 002612.png). If lastframe is omitted then only firstframe will be de-subbed. Graphic formats can be anything that your ImageMagick (and perhaps GraphicsMagick) can handle, although using a lossless format is recommended. All frames are assumed to have the same geometry and to be located in the same directory (including the keyframe). If no options are given then selective cropping is used, and assumes that the subtitles occupy the lower 20%% of the frame. No horizontal panning is done, and resulting frames are named "ds_frame" (original frames are retained). Use "-h" to get help, "-v" for verbose output, and "-q" for no output. Use "-V" to show the version number and exit. If the "-c" option is chosen then selective cropping is performed. The height of the subtitles can be specified using the "-s" flag in either pixels or a percentage of the total frame height. The non-subtitled area is then enlarged and cropped to the original frame size. By default the top right corner of the frame is selected, but horizontal panning can be performed using the "-p" option. If instead of the number of pixels the letter "c" is specified then selected portion of the image will be centred. To determine how much you can pan an image whose original dimensions are "A x B" and which has non-subtitled area "A x b" you can use the following formula: maxpan = round( (A x B)/b ) - A Use "-t" if the subtitles are on top i.e. if they are surtitles. Setting "-x" will overwrite the original frame (unless "-e" is used, see below), otherwise the new frames will be named "ds_frame" If you don't overwrite you can always do it later using e.g.: rename ds_0 0 ds_0* With "-e" it is possible to set a different extension for the resulting file name. For example, using "-e new" will create, say: ds_00001000.new Alternatively, you can also use it in combination with "-x" to only change the extension. Note, however, that "-e" will not change the image format, i.e. using "-x -e jpg" on 00001000.png would generate a non-subtitled frame called: 00001000.jpg but it will still be a PNG file. If the "-r" option is chosen then replacements are performed. The height specified by the "-s" flag selects the subtitle height. Setting "-t" and "-x" will behave as above. The "keyframe" is the frame whose bottom (or top) band will replace those in firstframe to lastframe. By default keyframe = firstframe - 1. The "-o" option is similar to "-r" but allows fine control over the geometry of the subtitled area via the "-g" option (which is mandatory). Geometry must be of the form WxH+X+Y. Note that selective cropping will not work well if there are black borders hardcoded around the images. EXAMPLES: To eliminate the subtitles on frames 1 to 1000 and create ds_00000001.png to ds_00001000.png use: desub.py 00000001.png 00001000.png Suppose your frames are 352x240, and that the subtitles occupy the bottom 47 pixels. The following command eliminates the subtitles and slightly pans the images to the right by 43 pixels. The resulting images will overwrite the originals: desub.py -c -s 47 -p 43 -x 00235.png 00987.png Moving the selected region to the right by 43 pixels actually centers it in this case, so the command above is equivalent to: desub.py -c -s 47 -p c -x 00235.png 00987.png To eliminate the subtitles with a replacement you can do: desub.py -r -s 47 -x 00235.png 00987.png As above, but specifying the subtitle region both vertically and horizontally: desub.py -o -g '200x47+70+193' -x 00235.png 00987.png If the surtitles occupy the top 20%% pixels then, explicitly using keyframe 00234.png: desub.py -r -s '20%%' -t -x -k 00234.png 00235.png 00987.png De-subbing a single frame using selective cropping and giving it a ".tmp" extension can be done as follows: desub.py -c -x -e tmp 00987.png """ % version if __name__ == '__main__': import os import sys import getopt import math import tempfile import shutil try: if sys.version_info[0:3] < (2, 3, 0): raise SystemExit except: print 'You need Python 2.3.0 or greater to run me!' raise SystemExit (std_in, std_out_err) = os.popen4(convert) std_in.close() status = std_out_err.read() if not ('ImageMagick' or 'GraphicsMagick') in status.split(): print '(Image|Graphics)Magick\'s "convert" command does ' + \ 'not seem to work...' print 'I get: ' + status.strip() raise SystemExit try: (opts, args) = getopt.getopt(sys.argv[1:], \ 'qvhcs:p:txre:k:Vog:') except: print "Something's wrong. Try the '-h' flag." raise SystemExit opts = dict(opts) if '-v' in opts: verbose = True else: verbose = False if '-q' in opts: quiet = True verbose = False else: quiet = False if not opts and not args: print usage raise SystemExit if '-h' in opts: print usage + help raise SystemExit if '-V' in opts: print 'desub.py version ' + version raise SystemExit if len(args) == 0: print 'You need to specify at least the first frame!' raise SystemExit elif len(args) == 1: args.append(args[0]) if not os.path.isfile(args[0]) or \ not os.path.isfile(args[1]): print args[0] + ' and/or ' + args[1] \ + ' do not exist!' raise SystemExit framedir = os.path.dirname(args[0]) first_frame_num = os.path.basename(args[0]).split('.')[0] last_frame_num = os.path.basename(args[1]).split('.')[0] imgext = '.' + os.path.basename(args[0]).split('.')[1] numberlength = len(first_frame_num) iden = identify + args[0] geom = os.popen(iden).read().strip().split() x = int(geom[0]) y = int(geom[1]) # Defaults: tpref = 'desub_' method = 'SelCrop' pre = 'ds_' sur = '' subheight = opts.get('-s', '20%') nosub_x = x nosub_offx = 0 sub_x = x sub_offx = 0 pan = opts.get('-p', 0) newext = opts.get('-e', False) if newext: newext = '.' + newext keyframe = opts.get('-k', False) if '-c' in opts: method = 'SelCrop' elif '-r' in opts: method = 'Replace' elif '-o' in opts: method = 'Overlay' if not '-g' in opts: print 'You must specify a geometry.' raise SystemExit else: over_geom = opts.get('-g') try: # xd, yd are dummy [xd, yd] = [int(i) for i in over_geom.split('+')[1:]] [xd, yd] = [int(i) for i in over_geom.split('+')[0].split('x')] if '-' in over_geom: raise ValueError except: print 'Invalid -g geometry (must be WxH+X+Y).' raise SystemExit if '-x' in opts: pre = '' if '-t' in opts: sur = flip if method in ['Replace', 'SelCrop']: if '%' == subheight[-1]: subheight = subheight[:-1] percent = True else: percent = False try: subheight = int(subheight) except: print 'Subtitle height must be a percentage ' + \ 'or a number of pixels!' raise SystemExit if percent: nosubfactor = 1 - (subheight/100.0) nosub_y = int(math.floor(y*nosubfactor)) else: nosub_y = int(y - subheight) if nosub_y < 0 or nosub_y > y: print 'Subtitle height out of range!' print 'Frame size is %sx%s' % (x, y) raise SystemExit sub_y = y - nosub_y nosub_geom = '%sx%s+%s+%s' % \ (nosub_x, nosub_y, nosub_offx, 0) sub_geom = '%sx%s+%s+%s' % \ (sub_x, sub_y, sub_offx, nosub_y) elif method == 'Overlay': # TODO: add checks? sub_geom = over_geom if not quiet: print 'desub.py version ' + version + \ '\nWorking using "' + method + '" method...' if method == 'SelCrop': if keyframe and not quiet: print 'Ignoring keyframe (try using -r)' scaledx = int(round((x*y)/float(nosub_y))) maxpan = scaledx - x if pan == 'c': pan = int(math.floor(maxpan/2.0)) try: pan = int(pan) except: print 'Pan value must be an integer or "c"!' raise SystemExit if pan > maxpan: print 'Pan value out of range!' print 'Max pan value is %s' % maxpan raise SystemExit if method in ['Replace', 'Overlay']: if not keyframe: keyframe = \ str(int(first_frame_num) - 1).zfill(numberlength) \ + imgext fp_keyframe = os.path.join(framedir, os.path.basename(keyframe)) if not os.path.isfile(fp_keyframe): print 'Keyframe ' + fp_keyframe + ' does not exist!' raise SystemExit else: desub_strip = tempfile.mkstemp(imgext, tpref, tempdir)[1] if method == 'Replace': get_strip = ' '.join([convert, \ sur, \ crop, sub_geom, \ fp_keyframe, desub_strip]) elif method == 'Overlay': get_strip = ' '.join([convert, \ crop, sub_geom, \ fp_keyframe, desub_strip]) if verbose: print get_strip os.system(get_strip) for iframe in range(int(first_frame_num), \ int(last_frame_num) + 1): rt_fname = str(iframe).zfill(numberlength) fname = rt_fname + imgext fp_fname = os.path.join(framedir, fname) if newext: fp_prefname = tempfile.mkstemp(imgext, tpref, tempdir)[1] else: fp_prefname = os.path.join(framedir, pre + fname) if method == 'SelCrop': enlarged_geom = '%sx%s!' % (scaledx, y) newcrop_geom = '%sx%s+%s+0' % (x, y, pan) selcrop = ' '.join([convert, \ fp_fname, \ sur, \ crop, nosub_geom, \ resize, enlarged_geom, \ crop, newcrop_geom, \ sur, \ fp_prefname]) if verbose: print selcrop os.system(selcrop) elif method == 'Replace': getnosub = ' '.join([convert, \ sur, \ crop, nosub_geom, \ fp_fname, \ fp_prefname]) if verbose: print getnosub os.system(getnosub) replace = ' '.join([convert, \ append, \ fp_prefname, \ desub_strip, \ fp_prefname]) if verbose: print replace os.system(replace) if '-t' in opts: doflip = ' '.join([convert, \ sur, \ fp_prefname, \ fp_prefname]) if verbose: print doflip os.system(doflip) flatten = ' '.join([mogrify, matte, fp_prefname, etonull]) if verbose: print flatten os.system(flatten) elif method == 'Overlay': ltc = list(sub_geom) while ltc[0] not in ['+', '-']: del ltc[0] ltc = ''.join(ltc) overlay = ' '.join([composite, \ geometry, ltc, \ desub_strip, \ fp_fname, \ matte, \ fp_prefname]) if verbose: print overlay os.system(overlay) if newext: fp_prefname_e = \ os.path.join(framedir, pre + rt_fname + newext) shutil.move(fp_prefname, fp_prefname_e) if verbose: print fp_prefname + ' -> ' + fp_prefname_e if method in ['Replace', 'Overlay']: os.remove(desub_strip) if not quiet: print 'Done!' """ CHANGELOG: 26 Mar 2004 : 0.0.1 : first release. 10 Jun 2004 : 0.0.2 : made desub more LiVES-friendly: - made lastframe optional - file extension can be chosen rewrote tempfile creation added keyframe message (if no -r) no longer needs string module requires at least python 2.3 19 Jul 2004 : 0.0.3 : renamed "desub" to "desub.py" added "-V" option minor internal improvements fixed small bug with %% in comment 31 Dec 2004 : 0.0.4 : fixed "selective cropping" to work with ImageMagick 6+ 11 Feb 2005 : 0.0.5 : replacement method added opacity layer which confused encoders, now make sure the image is flattened. 07 Mar 2005 : 0.0.6 : added "overlay" technique. 21 Mar 2005 : 0.0.7 : use env python (hopefully >= 2.3) 13 Aug 2005 : 0.0.8 : check for bad geometry. """