Panotools::Script - Panorama Tools scripting


Panotools-Script documentation Contained in the Panotools-Script distribution.

Index


Code Index:

NAME

Top

Panotools::Script - Panorama Tools scripting

SYNOPSIS

Top

Read, write and manipulate hugin script files.

DESCRIPTION

Top

Library and utilities for manipulating project files created by the hugin photo stitching software.

This file format is shared with various other tools, in particular this module is also capable of working with Panorama Tools script files.

USAGE

Top

  my $p = new Panotools::Script;

  $p->Read ('/path/to/script.txt');

  $p->Write ('/path/to/script.txt');

File paths in a Panorama Tools script file are generally relative to the directory containing the script. Modify this or otherwise prefix the filenames by supplying an optional second argument:

  $p->Write ('/path/to/script.txt', '../path/to/prefix/tofiles');

Clone a script object

 $clone = $p->Clone;

Access various sections of the scriptfile:

 $p->Mode;          # a L<Panotools::Script::Line::Mode> object
 $p->Panorama;      # a L<Panotools::Script::Line::Panorama> object
 $p->Variable;      # a L<Panotools::Script::Line::Variable> object

 $p->Image;         # an array of L<Panotools::Script::Line::Image> objects
 $p->Output;        # an array of L<Panotools::Script::Line::Output> objects
 $p->Control;       # an array of L<Panotools::Script::Line::Control> objects
 $p->ControlMorph;  # an array of L<Panotools::Script::Line::ControlMorph> objects

Rotate transform all the images in a project, angles in degrees:

  $p->Transform ($roll, $pitch, $yaw);

'o' output lines are generated by PTOptimizer and contain stitching parameters for each input image. 'i' image lines provide parameters for optimisation as well as stitching.

Update the 'image' lines based on 'output' lines and vice-versa like so:

  $p->Output2Image;
  $p->Image2Output;

Remove duplicate control points from the project, returns a list of deleted points:

  my $deleted = $p->Duplicates;

Remove all points with an error distance greater than a threshold measured in pixels, returns a list of deleted points:

  my $pruned = $p->Prune (12.345);

Extract a new object consisting of just the requested images, related control points and optimisation settings:

  my $subset = $p->Subset (1, 2, 34, 56);

Images can be requested in any order, but they will be returned in the same order as the 'parent' project.

Merge a project with another:

  $p->Merge ($newstuff);

This adds extra images from $newstuff, skipping duplicates. All control points except exact duplicates are imported regardless.

Get a summary of control point error distances in pixel units scaled to the output panorama:

  my ($total, $min, $max, $average, $sigma) = $p->Stats;

Centre input images into the final panorama:

   $p->Centre ('y');
   $p->Centre ('p');
   $p->Centre ('r');

Split the project into exposure stacks based in roll, pitch & yaw, or into exposure layers based on EV values:

  $stacks = $pto->Stacks;
  $layers = $pto->ExposureLayers;

Returns a list of image number lists.

e.g. extract the first stack as a new project:

  $pto_stack = $pto->Subset (@{$pto->Stacks->[0]});

Split a project into exposure layers, returns a list of lists of image ids:

   my $layers = $pto->ExposureLayers (1.0);

Deafults to 0.5EV difference threshold.

Get a list of unconnected groups, i.e. a list of image id lists:

  $groups = $pto->ConnectedGroups;

  warn 'just one group' if scalar @{$groups} == 1;

Count the connections between any two images:

  $points = $pto->Connections (3, 5);

Given a project with unlinked lens parameters, link them together with the same lens number if all distortion, and photometric parameters match:

   $pto->UnifyLenses;

Given a project with stacks indicated by 'j' parameters, hard-link the positions (only recognised by Hugin with layout mode code).

   $pto->LinkStacks;

Return the angular distance in degrees between two images:

  $deg = $pto->AngularDistance (3, 5);

Look at all photos and calculate an optimal pixel width for this panorama, optionally supply a scaling factor:

   $width = $pto->OptimalWidth (0.7);

This number is rounded up to the nearest multiple of 16 pixels.

COPYRIGHT

Top


Panotools-Script documentation Contained in the Panotools-Script distribution.
package Panotools::Script;

use strict;
use warnings;

use Panotools::Script::Line::Mode;
use Panotools::Script::Line::Panorama;
use Panotools::Script::Line::Option;
use Panotools::Script::Line::Image;
use Panotools::Script::Line::ImageMetadata;
use Panotools::Script::Line::Output;
use Panotools::Script::Line::Control;
use Panotools::Script::Line::ControlMorph;
use Panotools::Script::Line::Variable;
use Panotools::Script::Line::Mask;
use Digest::MD5 'md5_hex';

use File::Temp qw/ tempdir /;
use File::Spec;
use Math::Trig qw/:radial pi great_circle_distance/;

use Storable qw/ dclone /;

our $VERSION = '0.26';

our $CLEANUP = 1;
$CLEANUP = 0 if defined $ENV{DEBUG};

sub new
{
    my $class = shift;
    $class = ref $class || $class;
    my $self = bless {}, $class;
    $self->_defaults;
    return $self;
}

sub _defaults
{
    my $self = shift;
    $self->{mode} = new Panotools::Script::Line::Mode;
    $self->{panorama} = new Panotools::Script::Line::Panorama;
    $self->{option} = new Panotools::Script::Line::Option;
    $self->{variable} = new Panotools::Script::Line::Variable;
    $self->{image} = [];
    $self->{imagemetadata} = [];
    $self->{output} = [];
    $self->{control} = [];
    $self->{controlmorph} = [];
    $self->{mask} = [];
}

sub Read
{
    my $self = shift;
    $self->_defaults;
    my $path = shift || return 0;
    if ($path eq '-')
    {
        open FILE, '<-';
    }
    else
    {
        open FILE, "<", $path or die "cannot read-open $path";
    }
    my @raw = <FILE>;
    close FILE;

    $self->{md5} = md5_hex (join '', @raw);

    my ($volume, $directories, $file) = File::Spec->splitpath ($path);

    for my $line (@raw)
    {
        $line =~ s/(\r|\n)//g;
        $self->Mode->Parse ($line) if ($line =~ /^m /);
        $self->Panorama->Parse ($line) if ($line =~ /^p /);
        $self->Option->Parse ($line) if ($line =~ /^#hugin_/);
        $self->Variable->Parse ($line) if ($line =~ /^v /);
        if ($line =~ /^i /)
        {
            my $image = new Panotools::Script::Line::Image;
            $image->Parse ($line);
            push @{$self->Image}, $image;
        }
        if ($line =~ /^o /)
        {
            my $output = new Panotools::Script::Line::Output;
            $output->Parse ($line);
            push @{$self->Output}, $output;
        }
        if ($line =~ /^c /)
        {
            my $control = new Panotools::Script::Line::Control;
            $control->Parse ($line);
            push @{$self->Control}, $control;
        }
        if ($line =~ /^C /)
        {
            my $controlmorph = new Panotools::Script::Line::ControlMorph;
            $controlmorph->Parse ($line);
            push @{$self->ControlMorph}, $controlmorph;
        }
        if ($line =~ /^#-hugin /)
        {
            # per-image metadata
            my $imagemeta = new Panotools::Script::Line::ImageMetadata;
            $imagemeta->Parse ($line);
            push @{$self->ImageMetadata}, $imagemeta;
        }
        if ($line =~ /^k /)
        {
            my $mask = new Panotools::Script::Line::Mask;
            $mask->Parse ($line);
            push @{$self->Mask}, $mask;
        }
    }
    $self->Output2Image;
    return 1;
}

sub Write
{
    my $self = shift;
    $self->Image2Output if scalar @{$self->Output};
    my $path = shift || return 0;
    my $vector = shift || '';
    if ($path eq '-')
    {
        open FILE, '>-';
    }
    else
    {
        open FILE, ">", $path or die "cannot write-open $path";
    }
    print FILE "# Created by ". (ref $self) ." $VERSION\n\n";
    print FILE $self->Panorama->Assemble;
    print FILE $self->Mode->Assemble;
    print FILE "\n# Image lines\n";
    for my $index (0 .. (scalar (@{$self->Image}) - 1))
    {
        print FILE $self->ImageMetadata->[$index]->Assemble if defined ($self->ImageMetadata->[$index]);
        print FILE $self->Image->[$index]->Assemble if defined ($self->Image->[$index]);
    }
    print FILE "\n# Variable lines\n";
    print FILE $self->Variable->Assemble;
    print FILE "\n# Control point lines\n";
    for my $control (@{$self->Control})
    {
         print FILE $control->Assemble;
    }
    for my $controlmorph (@{$self->ControlMorph})
    {
         print FILE $controlmorph->Assemble;
    }
    print FILE "\n# Mask lines\n";
    for my $mask (@{$self->Mask})
    {
         print FILE $mask->Assemble;
    }
    print FILE "\n# option lines\n";
    print FILE $self->Option->Assemble;
    print FILE "\n*\n";
    print FILE "\n# Output image lines\n";
    for my $output (@{$self->Output})
    {
         print FILE $output->Assemble ($vector);
    }
    close FILE;
}

sub Clone
{
    my $self = shift;
    dclone ($self);
}

sub Mode
{
    my $self = shift;
    $self->{mode};
}

sub Panorama
{
    my $self = shift;
    $self->{panorama};
}

sub Option
{
    my $self = shift;
    $self->{option};
}

sub Variable
{
    my $self = shift;
    $self->{variable};
}

sub Image
{
    my $self = shift;
    $self->{image};
}

sub ImageMetadata
{
    my $self = shift;
    $self->{imagemetadata};
}

sub Output
{
    my $self = shift;
    $self->{output};
}

sub Control
{
    my $self = shift;
    $self->{control} = shift if scalar @_;
    $self->{control};
}

sub ControlMorph
{
    my $self = shift;
    $self->{controlmorph};
}

sub Mask
{
    my $self = shift;
    $self->{mask};
}

sub Transform
{
    my $self = shift;
    my ($roll, $pitch, $yaw) = @_;
    for my $image (@{$self->Image})
    {
        $image->Transform ($roll, $pitch, $yaw);
    }
    $self->Image2Output;
}

sub Output2Image
{
    my $self = shift;
    for my $index (0 .. (@{$self->Output} - 1))
    {
        for my $entry (keys %{$self->Output->[$index]})
        {
            $self->Image->[$index] = new Panotools::Script::Line::Image unless (defined $self->Image->[$index]);
            $self->Image->[$index]->{$entry} = $self->Output->[$index]->{$entry}
                unless (defined $self->Image->[$index]->{$entry} and $self->Image->[$index]->{$entry} =~ /=/);
        }
    }
}

sub Image2Output
{
    my $self = shift;
    for my $index (0 .. (@{$self->Image} - 1))
    {
        for my $entry (keys %{$self->Image->[$index]})
        {
            $self->Output->[$index] = new Panotools::Script::Line::Output unless (defined $self->Output->[$index]);
            unless ($self->Image->[$index]->{$entry} =~ /=/)
            {
                $self->Output->[$index]->{$entry} = $self->Image->[$index]->{$entry};
            }
            else
            {
                my $base = $self->Image->[$index]->{$entry};
                $base =~ s/=//;
                $self->Output->[$index]->{$entry} = $self->Image->[$base]->{$entry};
            }
        }
    }
}

sub Duplicates
{
    my $self = shift;
    my $packed_seen = {};
    my $points_uniq = [];
    my $points_deleted = [];
    for my $point (@{$self->Control})
    {
        my $packed = $point->Packed;
        if (defined $packed_seen->{$packed})
        {   
            push @{$points_deleted}, $point;
        }
        else
        {   
            push @{$points_uniq}, $point;
        }
        $packed_seen->{$packed} = 'TRUE';
    }

    $self->Control ($points_uniq);
    return $points_deleted;
}

sub Prune
{
    my $self = shift;
    my $threshold = shift;
    return [] unless $threshold > 0;
    my $points_new = [];
    my $points_pruned = [];

    for my $point (@{$self->Control})
    {
        if ($point->{t} > 0 or $point->Distance ($self) < $threshold)
        {
            push @{$points_new}, $point;
        }
        else
        {
            push @{$points_pruned}, $point;
        }
    }

    $self->{control} = $points_new;
    return $points_pruned;
}

sub Subset
{
    my $self = shift;
    my @selection = sort {$a <=> $b} @_;

    my $mapping;
    for my $index (0 .. scalar @selection -1)
    {
        return 0 unless $selection[$index] =~ /^[0-9]+$/;
        return 0 if $selection[$index] >= scalar @{$self->{image}};
        $mapping->{$selection[$index]} = $index;
    }
    return 0 unless scalar keys %{$mapping} == scalar @selection;

    my $pto_out = $self->Clone;

    # only use selected images
    $pto_out->{image} = [];
    $pto_out->{imagemetadata} = [];
    $pto_out->{variable} = new Panotools::Script::Line::Variable;

    for my $index (0 .. scalar @{$self->{image}} -1)
    {
        next unless defined $mapping->{$index};

        # copy metadata for selected image
        $pto_out->{imagemetadata}->[$mapping->{$index}]
            = $self->{imagemetadata}->[$index]->Clone
            if defined $self->{imagemetadata}->[$index];

        # copy selected image but resolve '=0' style references
        my $image = $self->{image}->[$index]->Clone;
        for my $key (keys %{$image})
        {
            # resolve references as anchor image may be gone
            if ($image->{$key} =~ /^=([0-9]+)$/)
            {
                $image->{$key} = $self->{image}->[$1]->{$key};
            }
            # rereference to image 0 if possible
            if (scalar @{$pto_out->{image}} > 0
                  and $image->{$key} eq $pto_out->{image}->[0]->{$key}
                  and $key =~ /^([abcdev]|R[abcde]|V[abcdxy])$/)
            {
                $image->{$key} = '=0';
            }
        }
        $pto_out->{image}->[$mapping->{$index}] = $image;

        # copy only optimisation parameters for selected image
        $pto_out->{variable}->{$mapping->{$index}}
            = {%{$self->{variable}->{$index}}}
            if defined $self->{variable}->{$index};
    }

    # copy only control points related to selected images
    $pto_out->{control} = [];
    for my $control (@{$self->{control}})
    {
        next unless defined $mapping->{$control->{n}};
        next unless defined $mapping->{$control->{N}};
        my $clone = $control->Clone;
        $clone->{n} = $mapping->{$control->{n}};
        $clone->{N} = $mapping->{$control->{N}};
        push @{$pto_out->{control}}, $clone;
    }

    # copy masks for selected images
    $pto_out->{mask} = [];
    for my $mask (@{$self->{mask}})
    {
        next unless defined $mapping->{$mask->{i}};
        my $clone = $mask->Clone;
        $clone->{i} = $mapping->{$mask->{i}};
        push @{$pto_out->{mask}}, $clone;
    }

    return $pto_out;
}

sub Merge
{
    my $self = shift;
    my $b = shift || return 0;

    # create lookup table relating filenames to index in final project
    my $mapping = {};

    for my $index (0 .. scalar @{$self->Image} -1)
    {
        $mapping->{$self->Image->[$index]->{n}} = $index;
    }

    my $index = scalar keys %{$mapping};
    for my $image (@{$b->Image})
    {
        unless (defined $mapping->{$image->{n}})
        {
            $mapping->{$image->{n}} = $index;
            $index++;
        }
    }

    # insert metadata, image and variable info if a new filename
    for my $index (0 .. scalar @{$b->Image} -1)
    {
        my $filename = $b->Image->[$index]->{n};
        next if defined $self->Image->[$mapping->{$filename}];

        $self->ImageMetadata->[$mapping->{$filename}] = $b->ImageMetadata->[$index]->Clone
            if defined $b->ImageMetadata->[$index];

        my $image = $b->Image->[$index]->Clone;
        for my $key (keys %{$image})
        {
            # update references
            if ($image->{$key} =~ /^=([0-9]+)$/)
            {
                my $index_new = $mapping->{$b->Image->[$1]->{n}};
                $image->{$key} = "=$index_new";
            }
        }
        $self->Image->[$mapping->{$filename}] = $image;

        my $variable = $b->Variable->Clone;
        $self->Variable->{$mapping->{$filename}} = $variable->{$index};
    }

    # append control points
    for my $control (@{$b->Control})
    {
        my $clone = $control->Clone;
        $clone->{n} = $mapping->{$b->Image->[$clone->{n}]->{n}};
        $clone->{N} = $mapping->{$b->Image->[$clone->{N}]->{n}};
        push @{$self->Control}, $clone;
    }

    # add masks
    for my $mask (@{$b->Mask})
    {
        my $jump;
        for my $self_mask (@{$self->Mask})
        {
            $jump = 1 if ($self_mask->{i} eq $mask->{i} and $self_mask->{p} eq $mask->{p});
        }
        next if $jump;
        my $clone = $mask->Clone;
        $clone->{i} = $mapping->{$b->Image->[$clone->{i}]->{n}};
        push @{$self->Mask}, $clone;
    }

    $self->Duplicates;

    for my $option (keys %{$b->Option})
    {
        $self->Option->{$option} = $b->Option->{$option} unless defined $self->Option->{$option};
    }

    return 1;
}

sub Stats
{
    my $self = shift;

    # get a list of all the distances
    my @distances;
    for my $point (@{$self->Control})
    {
        next unless $point->{t} == 0;
        push @distances, $point->Distance ($self);
    }

    my $total = scalar (@distances);

    return (0,0,0,0,0) unless $total;

    # calculate maximum and average distance
    my $max = undef;
    my $min = undef;
    my $sum = 0;
    for my $distance (@distances)
    {
        $min = $distance unless defined $min;
        $min = $distance if ($min > $distance);
        $max = $distance unless defined $max;
        $max = $distance if ($max < $distance);
        $sum += $distance;
    }
    my $average = $sum / $total;

    # calculate variation and standard deviation (sigma)
    $sum = 0;
    for my $distance (@distances)
    {
        my $variation = $distance - $average;
        $sum += $variation * $variation;
    }
    my $variance = $sum / $total;
    my $sigma = sqrt ($variance);

    return ($total, $min, $max, $average, $sigma);
}

sub Centre
{
    my $self = shift;
    my $param = shift;

    for my $image (@{$self->Image})
    {
        my $sigma_old = $self->Sigma ($param);
        $image->{$param} += 360;
        next if $self->Sigma ($param) < $sigma_old;
        $image->{$param} -= 720;
        next if $self->Sigma ($param) < $sigma_old;
        $image->{$param} += 360;
    }

    my $average_r = $self->_average ('r');
    my $average_p = $self->_average ('p');
    my $average_y = $self->_average ('y');

    if ($param eq 'r')
    {
        $self->Transform (0, 0, 0 - $average_y);
          $self->Transform (0, 0 - $average_p, 0);
            $self->Transform (0 - $average_r, 0, 0);
          $self->Transform (0, $average_p, 0);
        $self->Transform (0, 0, $average_y);
    }
    if ($param eq 'p')
    {
        $self->Transform (0, 0, 0 - $average_y);
          $self->Transform (0, 0 - $average_p, 0);
        $self->Transform (0, 0, $average_y);
    }
    if ($param eq 'y')
    {
        $self->Transform (0, 0, 0 - $average_y);
    }
}

sub _average
{
    my $self = shift;
    my $param = shift;
    my $sum = 0;
    for my $image (@{$self->Image})
    {
        $sum += $image->{$param}
    }
    return $sum / scalar @{$self->Image};
}

sub Sigma
{
    my $self = shift;
    my $param = shift;
    my $sum = 0;
    my $average = $self->_average ($param);
    for my $image (@{$self->Image})
    {
        my $variation = $image->{$param} - $average;
        $sum += $variation * $variation;
    }
    my $variance = $sum / scalar @{$self->Image};
    return sqrt ($variance);
}

sub Stacks
{
    my $self = shift->Clone;
    my $stacks = [];
    my $maxShift = $self->Image->[0]->{v} / 10.0;
    my @images = (0 .. scalar @{$self->Image} -1);
    while (@images)
    {
        my $base_image = shift @images;
        my $stack = [$base_image];
        my @images_remaining = @images;
        for my $image (@images)
        {
            if (_samestack ($self->{image}->[$base_image], $self->{image}->[$image], $maxShift))
            {   
                push @{$stack}, $image;
                @images_remaining = grep !/^$image$/, @images_remaining;
            }
        }
        @images = @images_remaining;
        push @{$stacks}, $stack;
    }
    return $stacks;
} 

sub _samestack
{
    my ($image0, $image1, $maxShift) = @_;
    my $minShift = 360.0 - $maxShift;
    return 1
        if ( (abs ($image0->y - $image1->y) < $maxShift || abs ($image0->y - $image1->y) > $minShift)
                && abs ($image0->p - $image1->p) < $maxShift );
    return 0;
}

sub ExposureLayers
{
    my $self = shift->Clone;
    my $layers = [];
    my $maxEVDiff = shift || 0.5;
    my @images = (0 .. scalar @{$self->Image} -1);
    while (@images)
    {
        my $base_image = shift @images;
        my $layer = [$base_image];
        my @images_remaining = @images;
        for my $image (@images)
        {
            if (_samelayer ($self->{image}->[$base_image], $self->{image}->[$image], $maxEVDiff))
            {   
                push @{$layer}, $image;
                @images_remaining = grep !/^$image$/, @images_remaining;
            }
        }
        @images = @images_remaining;
        push @{$layers}, $layer;
    }
    return $layers;
} 

sub _samelayer
{
    my ($image0, $image1, $maxEVDiff) = @_;
    return 1 if (abs ($image0->{Eev} - $image1->{Eev}) < $maxEVDiff );
    return 0;
}

sub ConnectedGroups
{
    my $self = shift;
    return [[]] unless scalar @{$self->Image};
    my $groups = [[0]];
    my $group_id = 0;

    my @images = (1 .. scalar @{$self->Image} -1);
    while (@images)
    {
        my $match = 0;
        for my $image (@images)
        {
            next if $match;
            next if grep /^$image$/, @{$groups->[$group_id]};
            for my $base_image (@{$groups->[$group_id]})
            {
                next if $match;
                if (scalar $self->Connections ($base_image, $image))
                {
                    push @{$groups->[$group_id]}, $image;
                    $match = 1;
                    @images = grep !/^$image$/, @images;
                }
            }
        }
        unless ($match)
        {
            $group_id++;
            $groups->[$group_id]->[0] = shift @images;
        }
    }
    return $groups;
}

sub Connections
{
    my $self = shift;
    my ($a, $b) = @_;

    my $results = 0;
    for my $control (@{$self->Control})
    {
        my $N = $control->{N};
        my $n = $control->{n};
        $results++ if (($n == $a and $N == $b) or ($n == $b and $N == $a));
    }
    return $results;
}

sub UnifyLenses
{
    my $self = shift;
    for my $id (1 .. scalar @{$self->Image} -1)
    {
        my $img = $self->Image->[$id];
        for my $base_id (0 .. $id -1)
        {
            my $base_img = $self->Image->[$base_id];
            if ($img->v ($self) eq $base_img->{v}
            and $img->a ($self) eq $base_img->{a}
            and $img->b ($self) eq $base_img->{b}
            and $img->c ($self) eq $base_img->{c}
            and $img->d ($self) eq $base_img->{d}
            and $img->e ($self) eq $base_img->{e}
            and $img->Ra ($self) eq $base_img->{Ra}
            and $img->Rb ($self) eq $base_img->{Rb}
            and $img->Rc ($self) eq $base_img->{Rc}
            and $img->Rd ($self) eq $base_img->{Rd}
            and $img->Re ($self) eq $base_img->{Re}
            and $img->Va ($self) eq $base_img->{Va}
            and $img->Vb ($self) eq $base_img->{Vb}
            and $img->Vc ($self) eq $base_img->{Vc}
            and $img->Vd ($self) eq $base_img->{Vd}
            and $img->Vx ($self) eq $base_img->{Vx}
            and $img->Vy ($self) eq $base_img->{Vy}
            )
            {
                $img->{v} = "=$base_id";
                $img->{a} = "=$base_id";
                $img->{b} = "=$base_id";
                $img->{c} = "=$base_id";
                $img->{d} = "=$base_id";
                $img->{e} = "=$base_id";
                $img->{Ra} = "=$base_id";
                $img->{Rb} = "=$base_id";
                $img->{Rc} = "=$base_id";
                $img->{Rd} = "=$base_id";
                $img->{Re} = "=$base_id";
                $img->{Va} = "=$base_id";
                $img->{Vb} = "=$base_id";
                $img->{Vc} = "=$base_id";
                $img->{Vd} = "=$base_id";
                $img->{Vx} = "=$base_id";
                $img->{Vy} = "=$base_id";
                next;
            }
        }
    }
}

sub LinkStacks
{
    my $self = shift;
    for my $id (1 .. scalar @{$self->Image} -1)
    {
        my $img = $self->Image->[$id];
        my $found;
        for my $base_id (0 .. $id -1)
        {
            next if $found;
            my $base_img = $self->Image->[$base_id];
            next unless defined $img->{j};
            if ($img->{j} eq $base_img->{j})
            {
                $img->{r} = "=$base_id";
                $img->{p} = "=$base_id";
                $img->{y} = "=$base_id";
                $found = 1;
                next;
            }
        }
    }
}

sub AngularDistance
{
    my $self = shift;
    return undef unless ($_[0] =~ /^[0-9]+$/ and $_[1] =~ /^[0-9]+$/);
    my $yaw_a = $self->Image->[$_[0]]->y ($self);
    my $pitch_a = $self->Image->[$_[0]]->p ($self);
    my $yaw_b = $self->Image->[$_[1]]->y ($self);
    my $pitch_b = $self->Image->[$_[1]]->p ($self);
    my $distance = great_circle_distance ($yaw_a * pi/180, pi/2 - ($pitch_a * pi/180),
                                              $yaw_b * pi/180, pi/2 - ($pitch_b * pi/180));
    return $distance * 180/pi;
}

sub OptimalWidth
{
    my $self = shift;
    my $factor = shift || 1;
    my $pix_radius_max = 1;
    for (@{$self->Image})
    {
        my $pix_radius = $_->Radius ($self);
        $pix_radius_max = $pix_radius if $pix_radius > $pix_radius_max;
    }
    my $pix_width;
    my $rad_fov = Math::Trig::deg2rad ($self->Panorama->{v});
    $rad_fov = 2 * Math::Trig::pi() if $rad_fov == 0;
    if ($self->Panorama->{f} == 0)
    {
        return $self->Panorama->{w} if $self->Panorama->{v} >= 180;
        $pix_width = 2 * $pix_radius_max * Math::Trig::tan ($rad_fov/2); 
    }
    else
    {
        $pix_width = $pix_radius_max * $rad_fov;
    }
    return int (($pix_width * $factor / 16) +1) * 16;
}


1;