File Coverage

blib/lib/Video/FrameGrab.pm
Criterion Covered Total %
statement 34 224 15.1
branch 3 66 4.5
condition 1 27 3.7
subroutine 10 26 38.4
pod 8 17 47.0
total 56 360 15.5


line stmt bran cond sub pod time code
1             ###########################################
2             package Video::FrameGrab;
3             ###########################################
4              
5 3     3   482781 use strict;
  3         7  
  3         125  
6 3     3   16 use warnings;
  3         6  
  3         123  
7 3     3   16 use Sysadm::Install qw(bin_find tap slurp blurt);
  3         17  
  3         102  
8 3     3   310 use File::Temp qw(tempdir);
  3         5  
  3         148  
9 3     3   5786 use DateTime;
  3         631415  
  3         121  
10 3     3   38 use DateTime::Duration;
  3         7  
  3         69  
11 3     3   3555 use DateTime::Format::Duration;
  3         26223  
  3         194  
12 3     3   4366 use Imager;
  3         151768  
  3         60  
13              
14 3     3   236 use Log::Log4perl qw(:easy);
  3         9  
  3         35  
15              
16             our $VERSION = "0.07";
17              
18             ###########################################
19             sub new {
20             ###########################################
21 3     3 0 3027 my($class, %options) = @_;
22              
23 3         20 my $self = {
24             mplayer => undef,
25             tmpdir => tempdir(CLEANUP => 1),
26             meta => undef,
27             video => undef,
28             aspects => ['16:9', '4:3'],
29             test_dont_snap => 0,
30             %options,
31             };
32              
33 3 50       1459 if(! defined $self->{video}) {
34 0         0 LOGDIE "Parameter missing: video";
35             }
36              
37 3 50       14 if(! defined $self->{mplayer}) {
38 3         59 $self->{mplayer} = bin_find("mplayer"),
39             }
40              
41 3 50 33     1934 if(!defined $self->{mplayer} or ! -x $self->{mplayer}) {
42 3         22 LOGDIE "Fatal error: Can't find mplayer";
43             }
44              
45 0           bless $self, $class;
46             }
47              
48             ###########################################
49             sub snap {
50             ###########################################
51 0     0 1   goto &frame_grab;
52             }
53              
54             ###########################################
55             sub frame_grab {
56             ###########################################
57 0     0 0   my($self, $time) = @_;
58              
59 0 0         if($self->{test_dont_snap}) {
60 0           INFO "Test mode, no snap";
61 0           return $self->{jpeg_data};
62             }
63              
64 0           my $tmpdir = $self->{tmpdir};
65              
66 0           for (<$tmpdir/*>) {
67 0           unlink $_;
68             }
69              
70 0           my($stdout, $stderr, $rc) =
71             tap $self->{mplayer}, qw(-frames 1 -ss), $time,
72             "-vo", "jpeg:maxfiles=1:outdir=$self->{tmpdir}",
73             "-ao", "null",
74             $self->{video};
75              
76 0 0         if($rc != 0) {
77 0           ERROR "$stderr";
78 0           return undef;
79             }
80              
81 0           my $frame = "$self->{tmpdir}/00000001.jpg";
82              
83 0 0         if(! -f $frame) {
84 0           ERROR "$stderr";
85 0           return undef;
86             }
87              
88 0           $self->{jpeg} = slurp("$self->{tmpdir}/00000001.jpg");
89 0           return $self->{jpeg}
90             }
91              
92             ###########################################
93             sub cropdetect {
94             ###########################################
95 0     0 1   my($self, $time, $opts) = @_;
96              
97 0 0         if(!defined $time) {
98 0           LOGDIE "Missing parameter: time";
99             }
100              
101 0 0         $opts = {} unless defined $opts;
102 0 0         $opts->{algorithm} = "schilli" unless exists $opts->{algorithm};
103              
104 0           my $algo = $opts->{algorithm};
105              
106 0           my $method = "cropdetect_$algo";
107              
108 0           return $self->$method( $time, $opts );
109             }
110              
111             ###########################################
112             sub cropdetect_schilli {
113             ###########################################
114 0     0 0   my($self, $time, $opts) = @_;
115              
116 0 0         $opts = {} unless defined $opts;
117              
118 0 0         $opts->{min_intensity_average} = 20 unless
119             exists $opts->{min_intensity_average};
120              
121 0 0         $opts->{gaussian_blur_radius} = 3 unless
122             exists $opts->{gaussian_blur_radius};
123              
124 0           my $img;
125              
126 0 0         if(exists $opts->{image}) {
127 0           $img = $opts->{image};
128             } else {
129 0           my $data = $self->snap( $time );
130 0           $img = Imager->new();
131 0           my $rc = $img->read( data => $data );
132 0 0         die $img->errstr() unless $rc;
133             }
134              
135 0           $img->filter( type => "gaussian",
136             stddev => $opts->{gaussian_blur_radius} );
137              
138 0           my $width = $img->getwidth();
139 0           my $height = $img->getheight();
140              
141 0           my $borders = { left => 0, right => 0, lower => 0, upper => 0 };
142              
143 0           for my $traverse (
144             ["upper", 0, 0, 1, 0, 0, 1],
145             ["left", 0, 0, 0, 1, 1, 0],
146             ["right", $width-1, 0, 0, 1, -1, 0],
147             ["lower", 0, $height-1, 1, 0, 0, -1],
148             ) {
149              
150 0           my($trav_name, $x, $y, $dx, $dy, $mdx, $mdy) = @$traverse;
151 0           my $border_width = 0;
152              
153 0   0       while($x < $width and $x >= 0 and $y >= 0 and $y < $height) {
      0        
      0        
154 0           my $avg = $self->intensity_average(
155             $img, $width, $height, $x, $y, $dx, $dy );
156              
157 0           TRACE "Intensity[$trav_name,$x,$y]: $avg";
158              
159 0 0         if($avg < $opts->{min_intensity_average}) {
160 0           $border_width++;
161 0           $x += $mdx;
162 0           $y += $mdy;
163 0           next;
164             }
165              
166 0           last;
167             }
168              
169 0           DEBUG "Border[$trav_name]: $border_width";
170 0           $borders->{$trav_name} = $border_width;
171             }
172              
173 0           my $cw = $width - $borders->{left} - $borders->{right};
174 0           my $ch = $height - $borders->{upper} - $borders->{lower};
175 0           my $cx = $borders->{left};
176 0           my $cy = $borders->{upper};
177              
178 0           DEBUG "Crop detect: $cw, $ch, $cx, $cy";
179 0           return ($cw, $ch, $cx, $cy);
180             }
181              
182             ###########################################
183             sub intensity_average {
184             ###########################################
185 0     0 0   my($self, $img, $width, $height, $x, $y, $dx, $dy) = @_;
186              
187 0           DEBUG "intensity_average: $width, $height, $x, $y, $dx, $dy";
188              
189 0           my $intensity = 0;
190 0           my $data_points = 0;
191              
192 0   0       while($x < $width and $x >= 0 and $y >= 0 and $y < $height) {
      0        
      0        
193 0           my $color = $img->getpixel( x => $x, y => $y );
194              
195 0 0         if(! defined $color) {
196 0           LOGDIE "Failed to obtain pixel $x/$y";
197             }
198              
199 0           my @comps = $color->rgba();
200              
201 0           $intensity += ($comps[0] + $comps[1] + $comps[2]) / 3.0;
202 0           $data_points++;
203              
204 0           $x += $dx;
205 0           $y += $dy;
206             }
207              
208 0 0         return 0 if $data_points == 0;
209              
210 0           return int(1.0*$intensity/$data_points);
211             }
212              
213             ###########################################
214             sub cropdetect_mplayer {
215             ###########################################
216 0     0 0   my($self, $time) = @_;
217              
218 0           my($stdout, $stderr, $rc) =
219             tap $self->{mplayer}, qw(-vf cropdetect -ss), $time,
220             "-frames", 10,
221             "-vo", "null",
222             "-ao", "null",
223             $self->{video};
224              
225 0 0 0       if(defined $stdout and
226             $stdout =~ /-vf crop=(\d+):(\d+):(\d+):(\d+)/) {
227 0           DEBUG "Suggested crop: $1, $2, $3, $4";
228 0           return ($1, $2, $3, $4);
229             }
230              
231 0           ERROR "$stderr";
232              
233 0           return undef;
234             }
235              
236             ###########################################
237             sub cropdetect_average {
238             ###########################################
239 0     0 1   my($self, $nof_probes, $opts) = @_;
240              
241 0 0         $opts = {} unless defined $opts;
242              
243 0           $self->result_clear();
244              
245 0           my @images = ();
246              
247 0 0         if(exists $opts->{images}) {
248 0           for my $img (@{ $opts->{images} }) {
  0            
249 0           push @images, $img;
250             }
251             } else {
252 0           for my $probe (
253             $self->equidistant_snap_times( $nof_probes,
254             $opts ) ) {
255              
256 0           my $data = $self->snap( $probe );
257 0           my $img = Imager->new();
258 0           my $rc = $img->read( data => $data );
259 0 0         if(! $rc) {
260 0           LOGWARN "Reading snapshop at time $probe failed ($!)";
261 0           next;
262             }
263 0           push @images, $img;
264             }
265             }
266              
267             # average all snapshots to obtain a single overlay image
268 0           my $overlay;
269              
270 0           my $i = 1;
271              
272 0           for my $img (@images) {
273 0 0         $img->filter(type=>"gaussian", stddev=>10)
274             or die $overlay->errstr;
275              
276 0 0         if(! defined $overlay) {
277 0           $overlay = $img;
278 0           next;
279             }
280 0           $overlay->compose( src => $img, combine => 'add' );
281 0 0         $overlay->filter(type=>"postlevels", levels=>3) or
282             die $overlay->errstr;
283              
284 0 0         if(get_logger()->is_trace()) {
285 0           $overlay->write(file => "i-$i.jpg");
286             }
287 0           $i++;
288             }
289              
290 0           my @params = $self->cropdetect( 0, { image => $overlay } );
291              
292 0           return @params;
293              
294             # my @params = $self->cropdetect( $probe, $opts );
295             # if(! defined $params[0] ) {
296             # ERROR "cropdetect returned an error";
297             # next;
298             # }
299             # DEBUG "Cropdetect at $probe yielded (@params)";
300             # $self->result_push( @params );
301             # }
302             #
303             # my @result = $self->result_majority_decision();
304             # DEBUG "Majority decision: (@result)";
305             # return @result;
306              
307             }
308              
309             ###########################################
310             sub result_clear {
311             ###########################################
312 0     0 0   my($self) = @_;
313              
314 0           $self->{result} = [];
315             }
316              
317             ###########################################
318             sub result_push {
319             ###########################################
320 0     0 0   my($self, @result) = @_;
321              
322 0           for(0..$#result) {
323 0           $self->{result}->[$_]->{ $result[$_] }++;
324             }
325             }
326              
327             ###########################################
328             sub result_majority_decision {
329             ###########################################
330 0     0 0   my($self) = @_;
331              
332 0           my @result = ();
333              
334 0           for my $sample (@{ $self->{result} }) {
  0            
335 0           my($majority) = sort { $sample->{$b} <=> $sample->{$a} } keys %$sample;
  0            
336 0           push @result, $majority;
337             }
338              
339 0           return @result;
340             }
341              
342             ###########################################
343             sub jpeg_data {
344             ###########################################
345 0     0 0   my($self) = @_;
346 0           return $self->{jpeg};
347             }
348              
349             ###########################################
350             sub jpeg_save {
351             ###########################################
352 0     0 1   my($self, $file) = @_;
353              
354 0           blurt $self->{jpeg}, $file;
355             }
356              
357             ###########################################
358             sub meta_data {
359             ###########################################
360 0     0 1   my($self) = @_;
361              
362 0           my($stdout, $stderr, $rc) =
363             tap $self->{mplayer},
364             qw(-vo null -ao null -frames 0 -identify),
365             $self->{video};
366              
367 0 0         if($rc != 0) {
368 0           ERROR "$stderr";
369 0           return undef;
370             }
371              
372 0           $self->{meta} = {};
373              
374 0           while($stdout =~ /^ID_(.*?)=(.*)/mg) {
375 0           $self->{meta}->{ lc($1) } = $2;
376             }
377              
378 0           return $self->{meta};
379             }
380              
381             ###########################################
382             sub equidistant_snap_times {
383             ###########################################
384 0     0 1   my($self, $nof_snaps, $opts) = @_;
385              
386 0 0         $opts = {} unless defined $opts;
387              
388 0 0         if(! defined $nof_snaps) {
389 0           LOGDIE "Parameter missing: nof_snaps";
390             }
391              
392 0           my @stamps = ();
393              
394 0 0         if(!defined $self->{meta}) {
395 0           $self->meta_data();
396             }
397              
398 0           my $length = $self->{meta}->{length};
399 0 0         $length = $opts->{movie_length} if defined $opts->{movie_length};
400              
401 0           my $interval = $length / ($nof_snaps + 1.0);
402 0           my $interval_seconds = int( $interval );
403              
404 0           my $dur = DateTime::Duration->new(seconds => $interval_seconds);
405 0           my $point = DateTime::Duration->new(seconds => 0);
406              
407 0           my $format = DateTime::Format::Duration->new(pattern => "%r");
408 0           $format->set_normalizing( "ISO" );
409              
410 0           for my $snap_no (1 .. $nof_snaps) {
411 0           $point->add_duration( $dur );
412              
413 0           my $stamp = $format->format_duration( $point );
414 0           push @stamps, $stamp;
415             }
416              
417 0           return @stamps;
418             }
419              
420             ###########################################
421             sub dimensions {
422             ###########################################
423 0     0 1   my($self) = @_;
424              
425 0 0         if(defined $self->{width}) {
426 0           return ($self->{width}, $self->{height});
427             }
428              
429 0           my $time = "00:00:01";
430              
431 0           my $data = $self->frame_grab( $time );
432 0           my $img = Imager->new();
433 0           my $rc = $img->read( data => $data );
434              
435 0           my $width = $img->getwidth();
436 0           my $height = $img->getheight();
437              
438 0           $self->{width} = $width;
439 0           $self->{height} = $height;
440              
441 0           return($width, $height);
442             }
443              
444             ###########################################
445             sub aspect_ratio_guess {
446             ###########################################
447 0     0 1   my($self, $formats) = @_;
448              
449 0 0         if(! defined $formats) {
450 0           $formats = $self->{aspects};
451             }
452              
453 0           my($width, $height) = $self->dimensions();
454              
455 0 0 0       if(!$width or !$height) {
456 0           ERROR "Can't get image dimensions data for width/height";
457 0           return undef;
458             }
459              
460 0           my %matches = ();
461              
462 0           for my $format (@$formats) {
463 0           my($fw, $fh) = split /:/, $format;
464 0           my $factor = 1.0*$width/$fw;
465 0           my $fhguess = 1.0*$height/$factor;
466              
467 0           my $deviate = abs($fh-$fhguess)/$fh*100.0;
468 0           INFO "$format deviates from $width:$height ",
469             sprintf("%.2f", $deviate), "%";
470            
471 0           $matches{ $format } = $deviate;
472             }
473              
474 0           return (sort { $matches{$a} <=> $matches{$b} } keys %matches)[0];
  0            
475             }
476              
477             1;
478              
479             __END__