-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathvideo.py
1361 lines (1125 loc) · 47.6 KB
/
video.py
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Generating or processing video, often using ffmpeg"""
from __future__ import print_function
from __future__ import division
from builtins import zip
from builtins import str
from builtins import map
from builtins import input
from builtins import object
from past.utils import old_div
import numpy as np
import subprocess
import re
import datetime
import os
import matplotlib.pyplot as plt
import my.plot
try:
import ffmpeg
except ImportError:
pass
class OutOfFrames(BaseException):
"""Exception raised when more frames cannot be extracted from a video"""
pass
def ffmpeg_frame_string(filename, frame_time=None, frame_number=None):
"""Given a frame time or number, create a string for ffmpeg -ss.
This attempts to reverse engineer the way that ffmpeg converts frame
times to frame numbers, so that we can specify an exact frame number
and get that exact frame.
As far as I can tell, if you request time T,
ffmpeg rounds T to the nearest millisecond,
and then gives you frame N,
where N is ceil(T * frame_rate).
So -.001 gives you the first frame, and .001 gives you the second frame.
It's hard to predict what will happen within one millisecond of a
frame time, so try to avoid that if exactness is important.
filename : video file. Used to get frame rate.
frame_time : This one takes precedence if both are provided.
We simply subtract half of the frame interval, and then round to
the nearest millisecond to account for ffmpeg's rounding up.
frame_number : This one is used if frame_time is not specified.
We convert to a frame time using
((frame_number / frame_rate) - 1 ms)
rounded down to the nearest millisecond.
This should give accurate results as long as frame rate is not
>500 fps or so.
frametime, frame_number : which frame to get
if you request time T, ffmpeg gives you frame N, where N is
ceil(time * frame_rate). So -.001 gives you the first frame, and
.001 gives you the second frame. It's hard to predict what will
happen with one ms of the exact frame time due to rounding errors.
Returns : string, suitable for -ss
"""
if frame_number is not None:
# If specified by number, convert to time
frame_rate = get_video_params(filename)[2]
use_frame_time = (old_div(frame_number, float(frame_rate))) - .001
use_frame_time = old_div(np.floor(use_frame_time * 1000), 1000.)
elif frame_time is not None:
frame_rate = get_video_params(filename)[2]
use_frame_time = frame_time - (old_div(1., (2 * frame_rate)))
else:
raise ValueError("must specify frame by time or number")
use_frame_string = '%0.3f' % use_frame_time
return use_frame_string
def get_frame(filename, frametime=None, frame_number=None, frame_string=None,
pix_fmt='gray', bufsize=10**9, path_to_ffmpeg='ffmpeg', vsync='drop',
n_frames=1):
"""Returns a single frame from a video as an array.
This creates an ffmpeg process and extracts data from it with a pipe.
This syntax is used to seek with ffmpeg:
ffmpeg -ss %frametime% -i %filename% ...
This is supposed to be relatively fast while still accurate.
Parameters
----------
filename : video filename
frame_string : to pass to -ss
frametime, frame_number:
If frame_string is None, then these are passed to
ffmpeg_frame_string to generate a frame string.
pix_fmt : the "output" format of ffmpeg.
currently only gray and rgb24 are accepted, because I need to
know how to reshape the result.
n_frames : int
How many frames to get
Returns
-------
tuple: (frame_data, stdout, stderr)
frame : numpy array
Generally the shape is (n_frames, height, width, n_channels)
Dimensions of size 1 are squeezed out
stdout : typically blank
stderr : ffmpeg's text output
"""
v_width, v_height = get_video_aspect(filename)
if pix_fmt == 'gray':
bytes_per_pixel = 1
reshape_size = (n_frames, v_height, v_width)
elif pix_fmt == 'rgb24':
bytes_per_pixel = 3
reshape_size = (n_frames, v_height, v_width, 3)
else:
raise ValueError("can't handle pix_fmt:", pix_fmt)
# Generate a frame string if we need it
if frame_string is None:
frame_string = ffmpeg_frame_string(filename,
frame_time=frametime, frame_number=frame_number)
# Create the command
command = [path_to_ffmpeg,
'-ss', frame_string,
'-i', filename,
'-vsync', vsync,
'-vframes', str(n_frames),
'-f', 'image2pipe',
'-pix_fmt', pix_fmt,
'-vcodec', 'rawvideo', '-']
# To store result
res_l = []
frames_read = 0
# Init the pipe
# We set stderr to PIPE to keep it from writing to screen
# Do this outside the try, because errors here won't init the pipe anyway
pipe = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
bufsize=bufsize)
try:
# Read
read_size = bytes_per_pixel * v_width * v_height * n_frames
raw_image = pipe.stdout.read(read_size)
# Raise if not enough data
if len(raw_image) < read_size:
raise OutOfFrames
# Convert to numpy
flattened_im = np.fromstring(raw_image, dtype='uint8')
# Reshape
frame_data = flattened_im.reshape(reshape_size)
# Squeeze if n_frames == 1
if n_frames == 1:
frame_data = frame_data[0]
except OutOfFrames:
print("warning: cannot get frame")
frame_data = None
finally:
# Restore stdout
pipe.terminate()
# Keep the leftover data and the error signal (ffmpeg output)
stdout, stderr = pipe.communicate()
# Convert to string
if stdout is not None:
stdout = stdout.decode('utf-8')
if stderr is not None:
stderr = stderr.decode('utf-8')
return frame_data, stdout, stderr
def frame_dump(filename, frametime, output_filename='out.png',
meth='ffmpeg fast', subseek_cushion=20., verbose=False, dry_run=False,
very_verbose=False):
"""Dump the frame in the specified file.
Probably better to use get_frame instead.
If the subprocess fails, CalledProcessError is raised.
Special case: if seek is beyond the end of the file, nothing is done
and no error is raised
(because ffmpeg does not report any problem in this case).
Values for meth:
'ffmpeg best' : Seek quickly, then accurately
ffmpeg -y -ss :coarse: -i :filename: -ss :fine: -vframes 1 \
:output_filename:
'ffmpeg fast' : Seek quickly
ffmpeg -y -ss :frametime: -i :filename: -vframes 1 :output_filename:
'ffmpeg accurate' : Seek accurately, but takes forever
ffmpeg -y -i :filename: -ss frametime -vframes 1 :output_filename:
'mplayer' : This takes forever and also dumps two frames, the first
and the desired. Not currently working but something like this:
mplayer -nosound -benchmark -vf framestep=:framenum: \
-frames 2 -vo png :filename:
Note that output files are always overwritten without asking.
With recent, non-avconv versions of ffmpeg, it appears that 'ffmpeg fast'
is just as accurate as 'ffmpeg best', and is now the preferred method.
Use scipy.misc.imread to read them back in.
Source
https://trac.ffmpeg.org/wiki/Seeking%20with%20FFmpeg
"""
if meth == 'mplayer':
raise ValueError("mplayer not supported")
elif meth == 'ffmpeg best':
# Break the seek into a coarse and a fine
coarse = np.max([0, frametime - subseek_cushion])
fine = frametime - coarse
syscall = 'ffmpeg -y -ss %r -i %s -ss %r -vframes 1 %s' % (
coarse, filename, fine, output_filename)
elif meth == 'ffmpeg accurate':
syscall = 'ffmpeg -y -i %s -ss %r -vframes 1 %s' % (
filename, frametime, output_filename)
elif meth == 'ffmpeg fast':
syscall = 'ffmpeg -y -ss %r -i %s -vframes 1 %s' % (
frametime, filename, output_filename)
if verbose:
print(syscall)
if not dry_run:
#os.system(syscall)
syscall_l = syscall.split(' ')
syscall_result = subprocess.check_output(syscall_l,
stderr=subprocess.STDOUT)
if very_verbose:
print(syscall_result)
def process_chunks_of_video(filename, n_frames, func='mean', verbose=False,
frame_chunk_sz=1000, bufsize=10**9,
image_w=None, image_h=None, pix_fmt='gray',
finalize='concatenate', path_to_ffmpeg='ffmpeg', vsync='drop'):
"""Read frames from video, apply function, return result
Uses a pipe to ffmpeg to load chunks of frame_chunk_sz frames, applies
func, then stores just the result of func to save memory.
If n_frames > # available, returns just the available frames with a
warning.
filename : file to read
n_frames : number of frames to process
if None or np.inf, will continue until video is exhausted
func : function to apply to each frame
If 'mean', then func = lambda frame: frame.mean()
If 'keep', then func = lambda frame: frame
'keep' will return every frame, which will obviously require a lot
of memory.
verbose : If True, prints out frame number for every chunk
frame_chunk_sz : number of frames to load at once from ffmpeg
bufsize : sent to subprocess.Popen
image_w, image_h : width and height of video in pxels
pix_fmt : Sent to ffmpeg
"""
if n_frames is None:
n_frames = np.inf
# Default function is mean luminance
if func == 'mean':
func = lambda frame: frame.mean()
elif func == 'keep':
func = lambda frame: frame
elif func is None:
raise ValueError("must specify frame function")
# Get aspect
if image_w is None:
image_w, image_h = get_video_aspect(filename)
# Set up pix_fmt
if pix_fmt == 'gray':
bytes_per_pixel = 1
reshape_size = (image_h, image_w)
elif pix_fmt == 'rgb24':
bytes_per_pixel = 3
reshape_size = (image_h, image_w, 3)
else:
raise ValueError("can't handle pix_fmt:", pix_fmt)
read_size_per_frame = bytes_per_pixel * image_w * image_h
# Create the command
command = [path_to_ffmpeg,
'-i', filename,
'-vsync', vsync,
'-f', 'image2pipe',
'-pix_fmt', pix_fmt,
'-vcodec', 'rawvideo', '-']
# To store result
res_l = []
frames_read = 0
# Init the pipe
# We set stderr to PIPE to keep it from writing to screen
# Do this outside the try, because errors here won't init the pipe anyway
# Actually, stderr will fill up and the process will hang
# http://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python/4896288#4896288
pipe = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'),
bufsize=bufsize)
# Catch any IO errors and restore stdout
try:
# Read in chunks
out_of_frames = False
while frames_read < n_frames and not out_of_frames:
if verbose:
print(frames_read)
# Figure out how much to acquire
if frames_read + frame_chunk_sz > n_frames:
this_chunk = n_frames - frames_read
else:
this_chunk = frame_chunk_sz
# Read this_chunk, or as much as we can
raw_image = pipe.stdout.read(read_size_per_frame * this_chunk)
# check if we ran out of frames
if len(raw_image) < read_size_per_frame * this_chunk:
#print("warning: ran out of frames")
out_of_frames = True
this_chunk = old_div(len(raw_image), read_size_per_frame)
assert this_chunk * read_size_per_frame == len(raw_image)
# Process
flattened_im = np.fromstring(raw_image, dtype='uint8')
if bytes_per_pixel == 1:
video = flattened_im.reshape(
(this_chunk, image_h, image_w))
else:
video = flattened_im.reshape(
(this_chunk, image_h, image_w, bytes_per_pixel))
# Store as list to avoid dtype and shape problems later
#chunk_res = np.asarray(map(func, video))
chunk_res = list(map(func, video))
# Store
res_l.append(chunk_res)
# Update
frames_read += this_chunk
except:
raise
finally:
# Restore stdout
pipe.terminate()
# Keep the leftover data and the error signal (ffmpeg output)
stdout, stderr = pipe.communicate()
# Convert to string
if stderr is not None:
stderr = stderr.decode('utf-8')
if not np.isinf(n_frames) and frames_read != n_frames:
# This usually happens when there's some rounding error in the frame
# times
# But it can also happen if more frames are requested than length
# of video
# So just warn, not error
print("warning: requested {} frames but only read {}".format(
n_frames, frames_read))
# Stick chunks together
if len(res_l) == 0:
print("warning: no data found")
res = np.array([])
elif finalize == 'concatenate':
res = np.concatenate(res_l)
elif finalize == 'listcomp':
res = np.array([item for sublist in res_l for item in sublist])
elif finalize == 'list':
res = res_l
else:
print("warning: unknown finalize %r" % finalize)
res = res_l
return res
def get_video_aspect(video_filename):
"""Returns width, height of video using ffmpeg-python"""
if not os.path.exists(video_filename):
raise ValueError("%s does not exist" % video_filename)
probe = ffmpeg.probe(video_filename)
assert len(probe['streams']) == 1
width = probe['streams'][0]['width']
height = probe['streams'][0]['height']
return width, height
def get_video_frame_rate(video_filename):
"""Returns frame rate of video using ffmpeg-python
https://video.stackexchange.com/questions/20789/ffmpeg-default-output-frame-rate
"""
if not os.path.exists(video_filename):
raise ValueError("%s does not exist" % video_filename)
probe = ffmpeg.probe(video_filename)
assert len(probe['streams']) == 1
# Seems to be two ways of coding, not sure which is better
avg_frame_rate = probe['streams'][0]['avg_frame_rate']
r_frame_rate = probe['streams'][0]['r_frame_rate']
assert avg_frame_rate == r_frame_rate
# Convert fraction to number
num, den = avg_frame_rate.split('/')
frame_rate = float(num) / float(den)
return frame_rate
def get_video_params(video_filename):
"""Returns width, height, frame_rate of video using ffmpeg-python"""
width, height = get_video_aspect(video_filename)
frame_rate = get_video_frame_rate(video_filename)
return width, height, frame_rate
def get_video_duration(video_filename):
"""Returns duration of a video file
Uses ffmpeg.probe to probe the file, and extracts the duration of the
container. Checks that the video file contains only a single stream,
whose duration matches the container's.
Returns : float
The duration in seconds
This seems to be exact, so there will be int(np.rint(duration * rate))
frames in the video. You can request one less than this number using
my.video.get_frame to get the last frame (Pythonic). If you request
this number or more, you will get an error.
"""
## Check
# Check it exists
if not os.path.exists(video_filename):
raise ValueError("%s does not exist" % video_filename)
# Probe it
probe = ffmpeg.probe(video_filename)
# Check that it contains only one stream
assert len(probe['streams']) == 1
## Container duration
# This is the easiest one to extract, but in theory the stream duration
# could differ
container_duration = float(probe['format']['duration'])
## Stream duration
if 'DURATION' in probe['streams'][0]['tags']:
# This tends to be the right way for most ffmpeg-encoded videos
stream_duration_s = probe['streams'][0]['tags']['DURATION']
# For some reason this is in nanoseconds, convert to microseconds
stream_duration_s = stream_duration_s[:-3]
# Match
video_duration_temp = datetime.datetime.strptime(
stream_duration_s, '%H:%M:%S.%f')
stream_duration_dt = datetime.timedelta(
hours=video_duration_temp.hour,
minutes=video_duration_temp.minute,
seconds=video_duration_temp.second,
microseconds=video_duration_temp.microsecond)
# Convert to seconds
stream_duration = stream_duration_dt.total_seconds()
else:
# This works for mjpeg videos from white matter
stream_duration_s = probe['streams'][0]['duration']
# Convert to seconds
stream_duration = float(stream_duration_s)
## Check that container and stream duration are the same
assert stream_duration == container_duration
# Return the single duration
return stream_duration
def choose_rectangular_ROI(vfile, n_frames=4, interactive=False, check=True,
hints=None):
"""Displays a subset of frames from video so the user can specify an ROI.
If interactive is False, the frames are simply displayed in a figure.
If interactive is True, a simple text-based UI allows the user to input
the x- and y- coordinates of the ROI. These are drawn and the user has
the opportunity to confirm them.
If check is True, then the values are swapped as necessary such that
x0 < x1 and y0 < y1.
Finally the results are returned as a dict with keys x0, x1, y0, y1.
hints : dict, or None
If it has key x0, x1, y0, or y1, the corresponding values will
be displayed as a hint to the user while selecting.
"""
import matplotlib.pyplot as plt
import my.plot
# Not sure why this doesn't work if it's lower down in the function
if interactive:
plt.ion()
# Get frames
duration = get_video_duration(vfile)
frametimes = np.linspace(duration * .1, duration * .9, n_frames)
frames = []
for frametime in frametimes:
frame, stdout, stderr = get_frame(vfile, frametime)
frames.append(frame)
# Plot them
f, axa = plt.subplots(1, 4, figsize=(11, 2.5))
f.subplots_adjust(left=.05, right=.975, bottom=.05, top=.975)
for frame, ax in zip(frames, axa.flatten()):
my.plot.imshow(frame, ax=ax, axis_call='image', cmap=plt.cm.gray)
my.plot.harmonize_clim_in_subplots(fig=f, clim=(0, 255))
# Get interactive results
res = {}
if interactive:
params_l = ['x0', 'x1', 'y0', 'y1']
lines = []
try:
while True:
for line in lines:
line.set_visible(False)
plt.draw()
# Get entries for each params
for param in params_l:
# Form request string, using hint if available
hint = None
if hints is not None and param in hints:
hint = hints[param]
if hint is None:
request_s = 'Enter %s: ' % param
else:
request_s = 'Enter %s [hint = %d]: ' % (param, hint)
# Keep getting input till it is valid
while True:
try:
val = input(request_s)
break
except ValueError:
print("invalid entry")
res[param] = int(val)
# Check ordering
if check:
if res['x0'] > res['x1']:
res['x0'], res['x1'] = res['x1'], res['x0']
if res['y0'] > res['y1']:
res['y0'], res['y1'] = res['y1'], res['y0']
# Draw results
for ax in axa:
lines.append(ax.plot(
ax.get_xlim(), [res['y0'], res['y0']], 'r-')[0])
lines.append(ax.plot(
ax.get_xlim(), [res['y1'], res['y1']], 'r-')[0])
lines.append(ax.plot(
[res['x0'], res['x0']], ax.get_ylim(), 'r-')[0])
lines.append(ax.plot(
[res['x1'], res['x1']], ax.get_ylim(), 'r-')[0])
plt.draw()
# Get confirmation
choice = input("Confirm [y/n/q]: ")
if choice == 'q':
res = {}
print("cancelled")
break
elif choice == 'y':
break
else:
pass
except KeyboardInterrupt:
res = {}
print("cancelled")
finally:
plt.ioff()
plt.close(f)
return res
def crop(input_file, output_file, crop_x0, crop_x1,
crop_y0, crop_y1, crop_stop_sec=None, vcodec='mpeg4', quality=2,
overwrite=True, verbose=False, very_verbose=False):
"""Crops the input file into the output file"""
# Overwrite avoid
if os.path.exists(output_file) and not overwrite:
raise ValueError("%s already exists" % output_file)
# Set up width, height and origin of crop zone
if crop_x0 > crop_x1:
crop_x0, crop_x1 = crop_x1, crop_x0
if crop_y0 > crop_y1:
crop_y0, crop_y1 = crop_y1, crop_y0
width = crop_x1 - crop_x0
height = crop_y1 - crop_y0
# Form the syscall
crop_string = '"crop=%d:%d:%d:%d"' % (width, height, crop_x0, crop_y0)
syscall_l = ['ffmpeg', '-i', input_file, '-y',
'-vcodec', vcodec,
'-q', str(quality),
'-vf', crop_string]
if crop_stop_sec is not None:
syscall_l += ['-t', str(crop_stop_sec)]
syscall_l.append(output_file)
# Call, redirecting to standard output so that we can catch it
if verbose:
print(' '.join(syscall_l))
# I think when -t parameter is set, it raises CalledProcessError
#~ syscall_result = subprocess.check_output(syscall_l,
#~ stderr=subprocess.STDOUT)
#~ if very_verbose:
#~ print syscall_result
os.system(' '.join(syscall_l))
def split():
# ffmpeg -i 150401_CR1_cropped.mp4 -f segment -vcodec copy -reset_timestamps 1 -map 0 -segment_time 1000 OUTPUT%d.mp4
pass
class WebcamController(object):
def __init__(self, device='/dev/video0', output_filename='/dev/null',
width=320, height=240, framerate=30,
window_title='webcam', image_controls=None,
):
"""Init a new webcam controller for a certain webcam.
image_controls : dict containing controls like gain, exposure
They will be set to reasonable defaults if not specified.
"""
# Store params
self.device = device
self.output_filename = output_filename
self.width = width
self.height = height
self.framerate = framerate
self.window_title = window_title
if self.output_filename is None:
self.output_filename = '/dev/null'
# Image controls
self.image_controls = {
'gain': 3,
'exposure': 20,
'brightness': 40,
'contrast': 50,
'saturation': 69,
'hue': 0,
'white_balance_automatic': 0,
'gain_automatic': 0,
'auto_exposure': 1, # flipped
}
# Above are for the PS3 Eye
# This is for C270
self.image_controls = {
'gain': 3,
'exposure_absolute': 1000,
'brightness': 40,
'contrast': 30,
'saturation': 69,
'white_balance_temperature_auto': 0,
'exposure_auto': 1,
}
if image_controls is not None:
self.image_controls.update(image_controls)
self.read_stderr = None
self.ffplay_stderr = None
self.ffplay_stdout = None
self.ffplay_proc = None
self.read_proc = None
self.tee_proc = None
def start(self, print_ffplay_proc_stderr=False, print_read_proc_stderr=False):
"""Start displaying and encoding
To stop, call the stop method, or close the ffplay window.
In the latter case, it will keep reading from the webcam until
you call cleanup or delete the object.
print_ffplay_proc_stderr : If True, prints the status messages to
the terminal from the the process that plays video to the screen.
If False, writes to /dev/null.
print_read_proc_stderr : Same, but for the process that reads from
the webcam.
"""
# Set the image controls
self.set_controls()
# Create a process to read from the webcam
# stdin should be pipe so it doesn't suck up keypresses (??)
# stderr should be null, so pipe doesn't fill up and block
# stdout will go to downstream process
if print_read_proc_stderr:
read_proc_stderr = None
else:
read_proc_stderr = open(os.devnull, 'w')
read_proc_cmd_l = ['ffmpeg',
'-f', 'video4linux2',
'-i', self.device,
'-vcodec', 'libx264',
'-qp', '0',
'-vf', 'format=gray',
'-preset', 'ultrafast',
'-f', 'rawvideo', '-',
]
self.read_proc = subprocess.Popen(read_proc_cmd_l, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=read_proc_stderr)
# Sometimes the read_proc fails because the device is busy or "Input/ouput error"
# but the returncode isn't set or anything so I don't know how to
# detect this.
# Tee the compressed output to a file
self.tee_proc = subprocess.Popen(['tee', self.output_filename],
stdin=self.read_proc.stdout,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Play the output
if print_ffplay_proc_stderr:
ffplay_proc_stderr = None
else:
ffplay_proc_stderr = open(os.devnull, 'w')
self.ffplay_proc = subprocess.Popen([
'ffplay',
#~ '-fflags', 'nobuffer', # not compatible with analyzeduration or probesize?
'-analyzeduration', '500000', # 500 ms delay in starting
'-window_title', self.window_title,
'-',
],
stdin=self.tee_proc.stdout,
stdout=subprocess.PIPE, stderr=ffplay_proc_stderr)
# This is supposed to allow SIGPIPE
# https://docs.python.org/2/library/subprocess.html#replacing-shell-pipeline
self.read_proc.stdout.close()
self.tee_proc.stdout.close()
def set_controls(self):
"""Use v4l2-ctl to set the controls"""
# Form the param list
cmd_list = ['v4l2-ctl',
'-d', self.device,
'--set-fmt-video=width=%d,height=%d' % (self.width, self.height),
'--set-parm=%d' % self.framerate,
]
for k, v in list(self.image_controls.items()):
cmd_list += ['-c', '%s=%d' % (k, v)]
# Create a process to set the parameters and run it
self.set_proc = subprocess.Popen(cmd_list,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.set_stdout, self.set_stderr = self.set_proc.communicate()
if self.set_proc.returncode != 0:
print("failed to set parameters")
print(self.set_stdout)
print(self.set_stderr)
raise IOError("failed to set parameters")
def stop(self):
if self.ffplay_proc is not None:
self.ffplay_proc.terminate()
self.cleanup()
def update(self):
pass
def cleanup(self):
self.__del__()
def __del__(self):
if self.ffplay_proc is not None:
if self.ffplay_proc.returncode is None:
self.ffplay_stdout, self.ffplay_stderr = \
self.ffplay_proc.communicate()
if self.read_proc is not None:
if self.read_proc.returncode is None:
self.read_proc.terminate()
self.read_proc.wait()
if self.tee_proc is not None:
self.tee_proc.wait()
class WebcamControllerFFplay(WebcamController):
"""Simpler version that just plays with ffplay"""
def start(self):
self.set_controls()
self.ffplay_proc = subprocess.Popen([
'ffplay',
'-f', 'video4linux2',
'-window_title', self.window_title,
'-i', self.device,
],
stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'),
bufsize=1000000)
self.stdout_l = []
self.stderr_l = []
def stop(self):
self.ffplay_proc.terminate()
self.cleanup()
def update(self):
"""This is supposed to read the stuff on stderr but I can't
get it to not block"""
return
#~ self.stdout_l.append(self.ffplay_proc.stdout.read())
print("update")
data = self.ffplay_proc.stderr.read(1000000)
print("got data")
print(len(data))
while len(data) == 1000000:
self.stderr_l.append(data)
data = self.ffplay_proc.stderr.read(1000000)
print("done")
def __del__(self):
try:
if self.ffplay_proc.returncode is None:
self.ffplay_stdout, self.ffplay_stderr = (
self.ffplay_proc.communicate())
except AttributeError:
pass
## These were copied in from WhiskiWrap, use these from now on
class FFmpegReader(object):
"""Reads frames from a video file using ffmpeg process"""
def __init__(self, input_filename, pix_fmt='gray', bufsize=10**9,
duration=None, start_frame_time=None, start_frame_number=None,
write_stderr_to_screen=False, vsync='drop'):
"""Initialize a new reader
input_filename : name of file
pix_fmt : used to format the raw data coming from ffmpeg into
a numpy array
bufsize : probably not necessary because we read one frame at a time
duration : duration of video to read (-t parameter)
start_frame_time, start_frame_number : -ss parameter
Parsed using my.video.ffmpeg_frame_string
write_stderr_to_screen : if True, writes to screen, otherwise to
/dev/null
"""
self.input_filename = input_filename
# Get params
self.frame_width, self.frame_height, self.frame_rate = \
get_video_params(input_filename)
# Set up pix_fmt
if pix_fmt == 'gray':
self.bytes_per_pixel = 1
elif pix_fmt == 'rgb24':
self.bytes_per_pixel = 3
else:
raise ValueError("can't handle pix_fmt:", pix_fmt)
self.read_size_per_frame = self.bytes_per_pixel * \
self.frame_width * self.frame_height
# Create the command
command = ['ffmpeg']
# Add ss string
if start_frame_time is not None or start_frame_number is not None:
ss_string = ffmpeg_frame_string(input_filename,
frame_time=start_frame_time, frame_number=start_frame_number)
command += [
'-ss', ss_string]
command += [
'-i', input_filename,
'-vsync', vsync,
'-f', 'image2pipe',
'-pix_fmt', pix_fmt]
# Add duration string
if duration is not None:
command += [
'-t', str(duration),]
# Add vcodec for pipe
command += [
'-vcodec', 'rawvideo', '-']
# To store result
self.n_frames_read = 0
# stderr
if write_stderr_to_screen:
stderr = None
else:
stderr = open(os.devnull, 'w')
# Init the pipe
# We set stderr to null so it doesn't fill up screen or buffers
# And we set stdin to PIPE to keep it from breaking our STDIN
self.ffmpeg_proc = subprocess.Popen(command,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=stderr,
bufsize=bufsize)
def iter_frames(self):
"""Yields one frame at a time
When done: terminates ffmpeg process, and stores any remaining
results in self.leftover_bytes and self.stdout and self.stderr
It might be worth writing this as a chunked reader if this is too
slow. Also we need to be able to seek through the file.
"""
# Read this_chunk, or as much as we can
while(True):
raw_image = self.ffmpeg_proc.stdout.read(self.read_size_per_frame)
# check if we ran out of frames
if len(raw_image) != self.read_size_per_frame:
self.leftover_bytes = raw_image
self.close()
return
# Convert to array
flattened_im = np.fromstring(raw_image, dtype='uint8')
if self.bytes_per_pixel == 1:
frame = flattened_im.reshape(
(self.frame_height, self.frame_width))
else:
frame = flattened_im.reshape(
(self.frame_height, self.frame_width, self.bytes_per_pixel))
# Update
self.n_frames_read = self.n_frames_read + 1
# Yield
yield frame
def close(self):
"""Closes the process"""
# Need to terminate in case there is more data but we don't
# care about it
# But if it's already terminated, don't try to terminate again
if self.ffmpeg_proc.returncode is None:
self.ffmpeg_proc.terminate()
# Extract the leftover bits
self.stdout, self.stderr = self.ffmpeg_proc.communicate()