forked from CordySmith/cc_decoder
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcc_decoder.py
More file actions
208 lines (172 loc) · 9.31 KB
/
Copy pathcc_decoder.py
File metadata and controls
208 lines (172 loc) · 9.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env python
# coding=utf-8
"""
ccDecoder is a Python Closed Caption Decoder/Extractor
Presented by Max Smith and notonbluray.com
Python 2.7+ and Python 3 compatible
Public domain / Unlicense per license section below
But attribution is always appreciated where possible.
Usage
-----
cc_decoder.py somevideofile.mpg >> somevideofile.srt
A Few Notes
-----------
There are two scripts that make up this tool: A command line interface
(cc_decoder) and a library file (lib.cc_decode).
The library makes the assumption that a stream of images are passed to
it that have closed caption data embedded in the top. It also assumes
that the images are wrapped in a object which will provide standardized
access to it. The library has minimal dependencies, and may be useful
for embedded projects.
The command line interface has many dependencies including PIL (Pillow)
and FFmpeg. Excellent builds of FFMpeg are available at
http://ffmpeg.zeranoe.com/builds/
If you use the command line tool - it's worth providing it with access
to a fast temporary area for writing data to, by default it will use
the system default temporary area, which may share space with your OS.
My primary goal of this project was to extract subtitles from my
Laserdisc collection (mostly simple pop-on mode). I've tried to cover
as much of the Closed Caption spec as possible, but I am limited by a
shortage of test-cases. I would love to get it working on some more
exotic captions (i.e. roll-up, XDS, ITV) but really need some sample
media to work with. If you have media that has these more exotic
captions - please drop me a line via my website (notonbluray.com)
"""
__author__ = "Max Smith"
__copyright__ = "Copyright 2014 Max Smith"
__credits__ = ["Max Smith"]
__license__ = """
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
"""
from PIL import Image # Note using Pillow rather than PIL
import atexit
import os
import argparse
import shutil
import subprocess
import sys
import tempfile
import time
from lib.cc_decode import decode_image_list_to_srt, decode_captions_raw, decode_captions_to_scc, decode_captions_debug
from lib.cc_decode import FileImageWrapper, decode_xds_packets
FFMPEG_LOC = {
'win32': os.path.join(os.environ["ProgramFiles"], 'ffmpeg', 'ffmpeg.exe'),
'cygwin': os.path.join(os.environ["ProgramFiles"], 'ffmpeg', 'ffmpeg.exe'),
'linux2': os.path.join(os.path.sep + 'usr', 'local', 'bin', 'ffmpeg'),
'darwin': os.path.join(os.path.sep + 'usr', 'local', 'bin', 'ffmpeg'),
}
class PilImageWrapper(FileImageWrapper):
""" Since we might want to hook the caption decoder up to live streams, etc, decouple the image object from the
decoding function """
def __init__(self, filename):
super(PilImageWrapper, self).__init__(filename)
img = Image.open(self.file_name) # .transpose(Image.FLIP_TOP_BOTTOM)
self.width, self.height = img.size
if img.size[0] != 720:
img.resize((720, img.size[1])) # If the image is not 720px wide, resize
self.image = img.convert('RGB').load()
def get_pixel_luma(self, x, y):
""" Return a pixels luma value normalized to the range 0 (black) to 255 (white) """
r, g, b, = self.image[int(x), int(y)]
return (r + g + b) / 3
class ClosedCaptionFileDecoder(object):
DECODERS = {'srt': decode_image_list_to_srt,
'scc': decode_captions_to_scc,
'raw': decode_captions_raw,
'debug': decode_captions_debug,
'xds': decode_xds_packets}
def __init__(self, ffmpeg_path=None, temp_path=None, ccformat=None, start_line=0, lines=10, fixed_line=None):
self.ffmpeg_path = ffmpeg_path or FFMPEG_LOC.get(sys.platform)
self.temp_dir_path = temp_path or tempfile.gettempdir()
self.format = ccformat or 'srt'
self.lines = lines
self.fixed_line = fixed_line
self.fpid = None
self.start_line = start_line
self.workingdir = ''
def _cleanup(self):
""" If we terminate unexpectedly, make sure we stop ffmpeg generating files """
if self.fpid and self.fpid.poll() is None: # Still running
self.fpid.kill()
if self.workingdir:
shutil.rmtree(self.workingdir)
def stream_decode_file_list(self, input_file, start_line=0, lines=5, image_wrapper=None):
""" Returns a generator of image objects based on ffmpeg decoding the top 10 lines of the passed input_file.
Run ffmpeg in a subprocess generating tiffs of the video frame until ffmpeg finishes and we run out of
frames.
input_file - input video file. Anything that ffmpeg understands
tempdir - where to write the tiffs to, ideally somewhere that can sustain high throughput, and has space
start_line - the line number to start capturing (default 0)
lines - the number of lines to write to the tiff, counting from the start line (default 5)
image_wrapper - the class to wrap the image file name with, default is PilImageWrapper """
if not os.path.exists(self.ffmpeg_path):
raise RuntimeError('Could not find ffmpeg at %s' % self.ffmpeg_path)
image_wrapper = image_wrapper or PilImageWrapper
self.workingdir = tempfile.mkdtemp(dir=self.temp_dir_path)
tempfile_name_structure = 'ccdecode%07d.tif'
ffmpeg_cmd = '%s -i "%s" -vf "scale=720:ih, crop=iw:%d:0:%d" -pix_fmt rgb24 -f image2 "%s"' % \
(self.ffmpeg_path, input_file, start_line + lines, start_line,
os.path.join(self.workingdir, tempfile_name_structure))
def next_file_name(file_num):
return os.path.join(self.workingdir, (tempfile_name_structure % file_num))
with open(os.devnull, 'wb') as devnull:
atexit.register(self._cleanup)
self.fpid = subprocess.Popen(ffmpeg_cmd, stderr=devnull)
file_number = 1
while self.fpid.poll() is None: # While ffmpeg is running
if os.path.exists(next_file_name(file_number)) and os.path.exists(next_file_name(file_number + 1)):
# Latch on the existence of the n+1 file, which wouldn't exist until the n file is fully written
yield image_wrapper(next_file_name(file_number))
file_number += 1
else:
time.sleep(0.25) # Take a nap for a moment, since we must have caught up with FFMpeg
# FFMpeg must have exited - process all remaining files
self.fpid = None
while os.path.exists(next_file_name(file_number)):
yield image_wrapper(next_file_name(file_number))
file_number += 1
os.rmdir(self.workingdir)
self.workingdir = ''
def decode(self, filename):
imagewrapper_generator = self.stream_decode_file_list(
filename, lines=self.lines, start_line=self.start_line)
if self.format in self.DECODERS:
self.DECODERS.get(self.format)(imagewrapper_generator)
else:
raise RuntimeError('Unknown output format %s, try one of %s' % (format, self.DECODERS.keys()))
def main():
p = argparse.ArgumentParser(description='Extract visible closed captions in a video file')
ffmpeg = FFMPEG_LOC.get(sys.platform, '')
tempdir = tempfile.gettempdir()
p.add_argument('videofile', help='Input video file name')
p.add_argument('--ffmpeg', default=ffmpeg, help='Path to a copy of the ffmpeg binary (default %s)' % ffmpeg)
p.add_argument('--temp', default=tempdir, help='Path to temporary working area (default %s)' % tempdir)
p.add_argument('--ccformat', default='srt', help='Output format xds, srt, scc or debug (default srt)')
p.add_argument('--lines', default=3, type=int,
help='Number of lines to search for CC in the video, starting at the start line (default 3)')
p.add_argument('--start_line', default=0, type=int, help='Start at a particular line 0=topmost line')
args = p.parse_args()
if args.videofile:
decoder = ClosedCaptionFileDecoder(ffmpeg_path=args.ffmpeg, temp_path=args.temp, ccformat=args.ccformat,
lines=args.lines, start_line=args.start_line)
decoder.decode(args.videofile)
main()