| Panotools-Script documentation | Contained in the Panotools-Script distribution. |
Panotools::Photos - Photo sets
Query sets of photos
A collection of photos has possibilities, it could be one or more panoramas or a bracketed set. This module provides some methods for describing groups of photos based on available metadata
Create a new object like so:
my $photos = new Panotools::Photos;
Alternatively supply some filenames:
my $photos = new Panotools::Photos ('DSC_0001.JPG', 'DSC_0002.JPG');
Add to or get the list of image filenames:
$photos->Paths ('DSC_0003.JPG', 'DSC_0004.JPG');
my @paths = $photos->Paths;
Construct a stub filename from the names of the first and last images in the list.
my $stub = $photos->Stub;
e.g. DSC_0001.JPG DSC_0002.JPG DSC_0003.JPG -> DSC_0001-DSC_0003
Query to discover if this is a likely bracketed set. i.e. is the total number of photos divisible by the number of different exposures:
&do_stuff if ($photos->Bracketed);
Query to discover if this is a layered set, i.e. there is a large exposure difference in the set, but it isn't bracketed.
&do_stuff if ($photos->Layered);
By default the threshold is 4, e.g. exposures varying between 2 and 1/2 seconds indicate layers. Vary this threshold like so:
&do_stuff if ($photos->Layered (2));
Get a list of exposure times sorted with longest exposure first
@speeds = @{$photos->Speeds};
Given a set of photos, split it into a one or more sets by looking at the variation of time interval between shots. e.g. typically the interval between shots in a panorama varies by less than 15 seconds. A variation greater than that indicates the start of the next panorama:
my @sets = $photos->SplitInterval (15);
Sets with an average interval greater than 4x this variation are not considered panoramas at all and discarded.
Get the average time between shots:
$average = $photos->AverageInterval;
Get the Angle of View in degrees of the first photo:
$photos->FOV;
..or any other photo (-1 is last):
$photos->FOV (123);
Returns undef if the FOV can't be calculated.
Get an EV value for a photo, this will be guessed from partial EXIF data:
$photos->Eev ($index);
| Panotools-Script documentation | Contained in the Panotools-Script distribution. |
package Panotools::Photos;
use strict; use warnings; use File::Spec; use Image::ExifTool;
sub new { my $class = shift; $class = ref $class || $class; my $self = bless [], $class; $self->Paths (@_); return $self; }
sub Paths { my $self = shift; for my $path (@_) { push @{$self}, {path => $path, exif => Image::ExifTool::ImageInfo ($path)}; } return map ($_->{path}, @{$self}); }
sub Stub { my $self = shift; my $path_a = $self->[0]->{path}; my $path_b = $self->[-1]->{path}; # strip any suffixes $path_a =~ s/\.[[:alnum:]]+$//; $path_b =~ s/\.[[:alnum:]]+$//; # strip all but filename $path_b =~ s/.*[\/\\]//; return $path_a .'-'. $path_b; }
sub Bracketed { my $self = shift; # bracketed photos are not shot on 'Auto Exposure' for my $index (0 .. scalar @{$self} -1) { next unless defined $self->[$index]->{exif}->{ExposureMode}; return 0 if $self->[$index]->{exif}->{ExposureMode} eq 'Auto'; } my $brackets = scalar (@{$self->Speeds}); # require equal numbers of each exposure level return 0 if (scalar (@{$self}) % $brackets); # require more than one exposure level return 0 if ($brackets < 2); # require bracketing order repeats: 1/50 1/100 1/200 1/50 1/100 1/200 etc... for my $index ($brackets .. scalar @{$self} -1) { return 0 unless $self->[$index]->{exif}->{ExposureTime} eq $self->[$index - $brackets]->{exif}->{ExposureTime}; } return 1; }
sub Layered { my $self = shift; my $factor = shift || 4; return 0 if $self->Bracketed; my $longest = $self->Speeds->[0]; my $shortest = $self->Speeds->[-1]; if ($longest =~ /^1\/([0-9]+)$/) {$longest = 1 / $1}; if ($shortest =~ /^1\/([0-9]+)$/) {$shortest = 1 / $1}; return 0 unless $longest or $shortest; return 0 if $longest / $shortest < $factor; return 1; }
sub Speeds { my $self = shift; my $speeds = {}; for my $image (@{$self}) { my $et = $image->{exif}->{ExposureTime} || $image->{exif}->{ShutterSpeed} || 0; $speeds->{$et} = 'TRUE'; } return [sort {_longer ($b, $a)} keys (%{$speeds})]; } sub _longer { my ($a, $b) = @_; if ($a =~ /^1\/([0-9]+)$/) {$a = 1 / $1}; if ($b =~ /^1\/([0-9]+)$/) {$b = 1 / $1}; return 0 if ($b > $a); return 1; }
sub SplitInterval { my $self = shift; my $d_inc = shift || 15; my $max_inc = $d_inc * 4; my @groups; my $group_tmp = new Panotools::Photos; my $previous_time; my $previous_inc = 0; for my $image (@{$self}) { my $datetime = $image->{exif}->{'DateTimeOriginal'} || $image->{exif}->{'FileModifyDate'}; my $time_unix = Image::ExifTool::GetUnixTime ($datetime); $previous_time = $time_unix unless (defined $previous_time); my $inc = $time_unix - $previous_time; if (($inc - $previous_inc) > $d_inc) { push @groups, $group_tmp if ($group_tmp->AverageInterval < $max_inc); $group_tmp = new Panotools::Photos; } push @{$group_tmp}, $image; $previous_time = $time_unix; $previous_inc = $inc; } push @groups, $group_tmp if ($group_tmp->AverageInterval < $max_inc); return @groups; }
sub AverageInterval { my $self = shift; return 0 unless (scalar @{$self} > 1); my $start = $self->[0]->{exif}->{'DateTimeOriginal'} || $self->[0]->{exif}->{'FileModifyDate'}; my $end = $self->[-1]->{exif}->{'DateTimeOriginal'} || $self->[-1]->{exif}->{'FileModifyDate'}; my $totaltime = Image::ExifTool::GetUnixTime ($end) - Image::ExifTool::GetUnixTime ($start); return $totaltime / (scalar @{$self} -1); }
sub FOV { my $self = shift; my $index = 0; $index = shift if @_; my $fov = $self->[$index]->{exif}->{'FOV'}; $fov =~ s/[^0-9.]*$// if defined $fov; return $fov; } sub Rotation { my $self = shift; my $index = shift || 0; my $rotation = $self->[$index]->{exif}->{'Rotation'} || undef; return 0 unless $rotation; return 0 if $rotation =~ /Mirror/; return 0 if ($self->[$index]->{exif}->{'ImageWidth'} < $self->[$index]->{exif}->{'ImageHeight'}); return 90 if $rotation =~ /Rotate 90 CW/; return 180 if $rotation =~ /Rotate 180/; return -90 if $rotation =~ /Rotate 270 CW/; return 0; # 1 => 'Horizontal (normal)', # 3 => 'Rotate 180', # 6 => 'Rotate 90 CW', # 8 => 'Rotate 270 CW', # 2 => 'Mirror horizontal', # 4 => 'Mirror vertical', # 5 => 'Mirror horizontal and rotate 270 CW', # 7 => 'Mirror horizontal and rotate 90 CW', }
sub Eev { my $self = shift; my $index = shift || 0; my $exif = $self->[$index]->{exif}; my $aperture = $exif->{Aperture} || 1.0; my $et = $exif->{ExposureTime} || $exif->{ShutterSpeed} || 1.0; if ($et =~ /^1\/([0-9]+)$/) {$et = 1 / $1}; my $iso = $exif->{ISO} || 100; # (A light value of 0 is defined as f/1.0 at 1 second with ISO 100) return sprintf ('%.3f', (2*log ($aperture) - log($et) - log($iso/100)) / log(2)); } sub AverageRGB { my $self = shift; my $RedBalance = 0; my $GreenBalance = 0; my $BlueBalance = 0; my $count = 0; for my $image (@{$self}) { next unless ($image->{exif}->{'RedBalance'} and $image->{exif}->{'BlueBalance'}); $RedBalance += $image->{exif}->{'RedBalance'}; $GreenBalance += $image->{exif}->{'GreenBalance'} if defined $image->{exif}->{'GreenBalance'}; $BlueBalance += $image->{exif}->{'BlueBalance'}; $count++; } return (1,1,1) unless $count; return ($RedBalance / $count, $GreenBalance / $count, $BlueBalance / $count); } 1;