Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/perl -w
- # Taken from:
- # http://u88.n24.queensu.ca/pub/facetest.pl
- # http://u88.n24.queensu.ca/exiftool/forum/index.php?topic=3156.0
- #------------------------------------------------------------------------------
- # File: facetest.pl
- #
- # Syntax: facetest.pl [MAKE|DIR|FILE]...
- #
- # Description: test face detection/recognition decoding
- #
- # Revisions: 2011/02/26 - P. Harvey created
- # 2011/03/27 - PH Restructured to provide FaceList() function
- # and added support for more makes
- #
- # Face Colors: Red/Blue - basic face area from newer/older format metadata
- # Green - primary face(s)?
- # Orange - recognized faces
- #
- # Notes: - requires imagemagick 'convert' to be installed
- # - draws rectangles around faces and writes resized output images
- # - output images are written to 'tmp' directory
- # - defaults to input directory of '../testpics/facedetect/Olympus'
- # - processes only JPG images
- #------------------------------------------------------------------------------
- use strict;
- BEGIN { unshift @INC, 'lib' }
- use Image::ExifTool;
- sub FaceList($$); # prototype for the subroutine that does all of the hard work
- # configurable parameters
- my $resize = 640; # maximum width of rescaled images
- my $dstdir = 'tmp'; # destination directory for output images
- my @files = @ARGV;
- @files or push @files, '../testpics/facedetect/Olympus';
- my $exifTool = new Image::ExifTool;
- mkdir $dstdir; # make sure destination directory exists
- my $file;
- foreach $file (@files) {
- unless (-e $file) {
- my $f = "../testpics/facedetect/$file";
- unless (-e $f) {
- warn("Can't open $file\n");
- next;
- }
- $file = $f;
- }
- if (-d $file) {
- # read all files in directory (recursively)
- opendir DIR, $file or warn("Error opening $file\n"), next;
- my @f = readdir DIR;
- closedir DIR;
- foreach (@f) {
- next if /^\./;
- push @files, "$file/$_";
- }
- next;
- }
- next unless $file =~ /\.jpe?g$/i; # process only JPEG images
- print "==== $file ";
- my $info = $exifTool->ImageInfo($file);
- # get the list of face information hashes
- my $faceList = FaceList($exifTool, $info);
- unless (ref $faceList) {
- print "[no face information]\n";
- warn "$faceList\n" if $faceList;
- next;
- }
- print "[$$info{Make}]\n";
- my ($width, $height) = @$info{'ImageWidth','ImageHeight'};
- my $aspect = $height / $width;
- # calculate dimensions for a resized image with a max width of $resize pixels
- my ($rw, $rh);
- if ($aspect < 1) {
- ($rw, $rh) = ($resize, int($resize * $aspect + 0.5));
- } else {
- ($rw, $rh) = (int($resize / $aspect + 0.5), $resize);
- }
- my @s = ($rw,$rh,$rw,$rh); # scaling factors for face coordinates
- # initialize 'convert' command with default drawing parameters
- my $cmd = "convert '$file' -fill none -strokewidth 2 -resize ${rw}x$rh";
- my $index = -1;
- foreach my $faceInfo (@$faceList) {
- # set the rectangle colour
- my $idx = $$faceInfo{Type} || 0;
- unless ($idx == $index) {
- $index = $idx;
- $idx = 2 if $idx > 3;
- $cmd .= ' -stroke ' . ['red','green','blue','orange']->[$idx];
- }
- # scale the face rectangle to the coordinates of the resized image
- my @p;
- for (my $i=0; $i<4; ++$i) {
- $p[$i] = int($$faceInfo{Position}[$i] * $s[$i] + 0.5);
- }
- # draw the face rectangle
- $cmd .= " -draw 'rectangle $p[0],$p[1] $p[2],$p[3]'";
- if (defined $$faceInfo{Rotation}) {
- # draw the face orientation
- my $ang = $$faceInfo{Rotation} * 3.14159 / 180; # convert to radians
- my ($cx, $cy) = (($p[0]+$p[2])/2, ($p[1]+$p[3])/2);
- my ($w, $h) = (abs($p[2]-$p[0]), abs($p[3]-$p[1]));
- my ($x, $y) = (int(($cx+$w/2*sin($ang))*10)/10, int(($cy-$h/2*cos($ang))*10)/10);
- $cmd .= " -draw \"path 'M $cx,$cy L $x,$y'\"";
- }
- }
- $index > -1 or print("No face\n"), next;
- # add destination filename to command
- my $name = $file;
- $name =~ s/.*\///; # remove directory name
- $cmd .= " '$dstdir/$name'";
- # resize the image and draw the face positions
- print "$cmd\n";
- system $cmd;
- }
- #------------------------------------------------------------------------------
- # check to see if all specified tags were found
- # Inputs: 0) tag info hash, 1) message to return, 2-N) tag names
- # Returns: true if specified tags exist
- sub Found($$@)
- {
- local $_;
- my $info = $_[0];
- foreach (@_[2..$#_]) {
- next if defined $$info{$_};
- $_[1] = "Missing $_";
- return 0;
- }
- return 1;
- }
- #------------------------------------------------------------------------------
- # Get margines and size of cropped image area in face detect frame
- # Inputs: 0/1) face detect frame width/height
- # 2) aspect ratio of original image (must be < 1)
- # Returns: 0/1) X/Y cropped border size
- # 2/3) width/height of cropped image
- sub GetCropArea($$$)
- {
- my ($fw, $fh, $aspect) = @_;
- my $crop_w = $fw;
- my $crop_h = $fw * $aspect;
- if ($crop_h > $fh) {
- $crop_w = $fh / $aspect;
- $crop_h = $fh;
- }
- my ($crop_x, $crop_y) = (($fw - $crop_w) / 2, ($fh - $crop_h) / 2);
- return ($crop_x, $crop_y, $crop_w, $crop_h);
- }
- #------------------------------------------------------------------------------
- # Return normalized face information from tags extracted by ExifTool
- # Inputs: 0) ExifTool object reference
- # 1) image information hash reference (from call to ImageInfo)
- # Returns: undef if there is no face-detect information, or
- # error string if there there were no faces or there was an error, or
- # reference to array of face information hashes on success
- # Face information hash elements:
- # Position - left,top,right,bottom coordinates of face as a fraction of image size
- # Rotation - [optional] CW rotation angle of face in degrees
- # Type - [optional] type of face information:
- # 0=normal, 1=primary, 2=old models, 3=recognized
- # Name - [optional] face name if recognized (FujiFilm, Panasonic)
- # Age - [optional] age of person (FujiFilm, Panasonic)
- # Category - [optional] camera category for this face (FujiFilm)
- sub FaceList($$)
- {
- local $_;
- my ($exifTool, $info) = @_;
- my ($tag, $make, @faceList, $wasRotated, $msg, $i);
- return undef unless $$info{ImageWidth} and $$info{ImageHeight};
- my ($width, $height) = @$info{'ImageWidth','ImageHeight'};
- # all face detect coordinates are in unrotated image
- if ($height > $width) {
- # image was probably rotated, but face detect coordinates are always
- # given for the unrotated image, so assume that width is the long dimension
- my $tmp = $width;
- $width = $height;
- $height = $tmp;
- $wasRotated = 1;
- }
- my $aspect = $height / $width;
- my ($fw, $fh); # face detect frame width/height
- my ($sx, $sy); # x/y scaling factors
- # get the ExifTool manufacturer group name
- foreach $tag ('FacesDetected', 'ValidAFPoints') {
- next unless defined $$info{$tag};
- $make = $exifTool->GetGroup($tag, 1);
- last;
- }
- my $model = $$info{Model} || '';
- $make or return 'Unrecognized face information!';
- #
- # unfortunately each manufacturer has its own way of storing face detection
- # information, so we must handle them all differently...
- #
- if ($make eq 'Sony') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'Face1Position');
- # calculate scaling factors for face detect area coordinates
- ($sx, $sy) = (1/$width, 1/$height);
- for ($i=1; ; ++$i) {
- my $tag = "Face${i}Position";
- last unless $$info{$tag};
- my @a = split ' ', $$info{$tag};
- last unless @a >= 4;
- my ($x1, $y1) = ($a[1]*$sx, $a[0]*$sy);
- my ($x2, $y2) = ($x1+$a[3]*$sx, $y1+$a[2]*$sy);
- push @faceList, { Position => [$x1, $y1, $x2, $y2] };
- }
- } elsif ($make eq 'FujiFilm') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'FacePositions');
- my $n = $$info{FacesDetected} or return 'No faces';
- my @a = split ' ', $$info{FacePositions};
- ($sx, $sy) = (1/$width, 1/$height);
- for ($i=0; $i<$n; ++$i) {
- my ($x1, $y1) = ($a[$i*4]*$sx, $a[$i*4+1]*$sy);
- my ($x2, $y2) = ($a[$i*4+2]*$sx, $a[$i*4+3]*$sy);
- my $faceInfo = { Position => [$x1, $y1, $x2, $y2] };
- push @faceList, $faceInfo;
- my $name = $$info{"Face${i}Name"};
- next unless defined $name;
- $$faceInfo{Type} = 3; # recognized face
- $$faceInfo{Name} = $name;
- $$faceInfo{Category} = $$info{"Face${i}Category"};
- # calculate age
- my $bday = $$info{"Face${i}Birthday"} or next;
- my $date = $$info{DateTimeOriginal} || $$info{CreateDate} or next;
- my @t1 = $bday =~ /\d+/g;
- my @t2 = $date =~ /\d+/g;
- push @t1, 0 while @t1 < 6; # pad with zeros
- push @t2, 0 while @t1 < 6;
- my $borrow = 0;
- my ($j, @age);
- for ($j=5; $j>=0; --$j) {
- $age[$j] = $t2[$j] - $t1[$j] - $borrow;
- $age[$j] >= 0 and $borrow = 0, next;
- last unless $j;
- # handle borrow in subtraction
- $borrow = 1;
- my $add = [0, 12, 0, 24, 60, 60]->[$j];
- $add and $age[$j] += $add, next;
- # borrow days from the month before
- my ($m, $y) = ($t2[1]-1, $t2[0]);
- $age[$j] += [31,31,28,31,30,31,30,31,31,30,31,30]->[$m];
- # handle leap years if month is February
- $age[$j] += 1 if $m==2 and $y % 4 and ($y % 100 or not $y % 400);
- }
- $$faceInfo{Age} = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2d', @age);
- }
- } elsif ($make eq 'Nikon') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'FaceDetectFrameSize', 'Face1Position');
- ($fw,$fh) = split ' ', $$info{FaceDetectFrameSize};
- # (note: have seen crazy $fh for S550, so scale Y by same factor as X)
- ($sx,$sy) = (1/$fw, 1/$fh);
- for ($i=1; ; ++$i) {
- my $val = $$info{"Face${i}Position"} or last;
- my @a = split ' ', $val;
- # (have seen high bit set in a S550 sample, so reset it just in case)
- my ($x1,$y1) = (($a[0]&0x7fff)*$sx, ($a[1]&0x7fff)*$sy);
- my ($x2,$y2) = ($x1+$a[2]*$sx, $y1+$a[3]*$sy);
- push @faceList, { Position => [$x1, $y1, $x2, $y2] };
- }
- } elsif ($make eq 'Panasonic') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'Face1Position');
- # face detect frame is 320 pixels wide unless aspect ratio is less than 3/4
- if ($aspect <= 3/4) {
- ($fw, $fh) = (320, 320 * $aspect);
- } else {
- ($fw, $fh) = (240 / $aspect, 240);
- }
- ($sx, $sy) = (1/$fw, 1/$fh);
- my $type;
- for ($type=0; $type<2; ++$type) {
- my $pre = $type ? 'Recognized' : '';
- for ($i=1; ; ++$i) {
- my $val = $$info{"${pre}Face${i}Position"} or last;
- my @a = split ' ', $val;
- my ($x1,$y1) = (($a[0]-$a[2]/2)*$sx, ($a[1]-$a[3]/2)*$sy);
- my ($x2,$y2) = ($x1+$a[2]*$sx, $y1+$a[3]*$sy);
- my $faceInfo = { Position => [$x1, $y1, $x2, $y2] };
- push @faceList, $faceInfo;
- next unless $type;
- $$faceInfo{Type} = 3; # use index 3 for recognized faces
- $$faceInfo{Name} = $$info{"RecognizedFace${i}Name"};
- $$faceInfo{Age} = $$info{"RecognizedFace${i}Age"};
- }
- }
- } elsif ($make eq 'Pentax') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'Face1Position');
- my $n = $$info{FacesDetected};
- $n or return 'No faces';
- ($sx,$sy) = (1/$width, 1/$height);
- for ($i=1; $i<=$n; ++$i) {
- my $val = $$info{"Face${i}Position"} or last;
- my ($x,$y) = split ' ', $val;
- $val = $$info{"Face${i}Size"} or last;
- my ($w,$h) = split ' ', $val;
- my ($x1,$y1) = (($x-$w/2)*$sx, ($y-$h/2)*$sy);
- my ($x2,$y2) = ($x1+$w*$sx, $y1+$h*$sy);
- push @faceList, { Position => [$x1, $y1, $x2, $y2] };
- }
- if ($$info{'FacePosition'}) {
- my ($x,$y) = split ' ', $$info{FacePosition};
- my $w = 100; # (just pull a number for the face size out of thin air)
- my ($x1,$y1) = ($x/100-$w/2, $y/100-$w/2);
- my ($x2,$y2) = ($x1+$w, $y1+$w);
- # set the Type for the primary face
- push @faceList, { Position => [$x1, $y1, $x2, $y2], Type => 1 };
- }
- } elsif ($make eq 'Sanyo') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'FacePosition');
- # face detect frame is 640 pixels wide
- ($fw, $fh) = (640, 640 * $aspect);
- ($sx, $sy) = (1/$fw, 1/$fh);
- my $val = $$info{"FacePosition"} or last;
- my @a = split ' ', $val;
- my ($x1,$y1) = ($a[0]*$sx, $a[1]*$sy);
- my ($x2,$y2) = ($a[2]*$sx, $a[3]*$sy);
- push @faceList, { Position => [$x1, $y1, $x2, $y2] };
- } elsif ($make eq 'Casio') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'FaceDetectFrameSize', 'Face1Position');
- # extract face orientation if available
- my $rot;
- $rot = $$info{FaceOrientation} =~ /(\d+)/ ? $1 : 0 if $$info{FaceOrientation};
- ($fw, $fh) = split ' ', $$info{FaceDetectFrameSize};
- my ($crop_x, $crop_y, $crop_w, $crop_h) = GetCropArea($fw, $fh, $aspect);
- ($sx, $sy) = (1/$crop_w, 1/$crop_h);
- for ($i=1; ; ++$i) {
- my $val = $$info{"Face${i}Position"} or last;
- my @a = split ' ', $val;
- my ($x1,$y1) = (($a[0]-$crop_x)*$sx, ($a[1]-$crop_y)*$sy);
- my ($x2,$y2) = (($a[2]-$crop_x)*$sx, ($a[3]-$crop_y)*$sy);
- my $faceInfo = { Position => [$x1, $y1, $x2, $y2] };
- $$faceInfo{Rotation} = $rot if defined $rot;
- push @faceList, $faceInfo;
- }
- } elsif ($make eq 'Ricoh') {
- return $msg unless Found($info, $msg, 'FacesDetected', 'FaceDetectFrameSize', 'Face1Position');
- ($fw, $fh) = split ' ', $$info{FaceDetectFrameSize};
- my ($sx, $sy) = (1/$fw, 1/$fh);
- for ($i=1; ; ++$i) {
- my $val = $$info{"Face${i}Position"} or last;
- my ($x,$y,$w,$h) = split ' ', $val;
- my ($x1,$y1) = ($x/$fw, $y*$sy);
- my ($x2,$y2) = (($x+$w)*$sx, ($y+$h)*$sy);
- push @faceList, { Position => [$x1, $y1, $x2, $y2] };
- }
- } elsif ($make eq 'Canon') {
- # older models store face detect information
- if ($$info{FacesDetected} and $$info{FaceDetectFrameSize}) {
- return 'No faces' unless $$info{FacesDetected};
- ($fw, $fh) = split ' ', $$info{FaceDetectFrameSize};
- $fw or ($fw,$fh) = (320,240);
- ($sx,$sy) = (1/$fw, 1/$fh);
- my $facewid = $$info{FaceWidth} || 35;
- for ($i=1; ; ++$i) {
- my $val = $$info{"Face${i}Position"} or last;
- my @a = split ' ', $val;
- my ($x1,$y1) = (($a[0]+$fw/2-$facewid)*$sx, ($a[1]+$fh/2-$facewid)*$sy);
- my ($x2,$y2) = ($x1+$facewid*2*$sx, $y1+$facewid*2*$sy);
- # set Type to 2 for older Canon face-detect information
- push @faceList, { Position => [$x1, $y1, $x2, $y2], Type => 2 };
- }
- } else { # newer models use AF points
- return $msg unless Found($info, $msg, 'ValidAFPoints', 'AFImageWidth', 'AFImageHeight',
- 'AFAreaXPositions', 'AFAreaYPositions', 'PrimaryAFPoint');
- # test for face detect mode
- unless ($$info{AFAreaMode} and $$info{AFAreaMode} =~ /Face/) {
- return 'Face detect off';
- }
- my ($width, $height) = @$info{'AFImageWidth', 'AFImageHeight'};
- my @x = split ' ', $$info{AFAreaXPositions};
- my @y = split ' ', $$info{AFAreaYPositions};
- # sometimes widths are stored separately for each AF area
- my (@w, @h);
- if ($$info{AFAreaWidths}) {
- @w = split ' ', $$info{AFAreaWidths};
- @h = split ' ', $$info{AFAreaHeights};
- } elsif ($$info{AFAreaWidth}) {
- @w = ($$info{AFAreaWidth}) x (scalar @x);
- @h = ($$info{AFAreaHeight}) x (scalar @x);
- } else {
- return 'No AF area size';
- }
- # convert to positive coordinates
- $_ += $width/2 foreach @x;
- $_ += $height/2 foreach @y;
- # EOS models have Y flipped
- if ($model =~ /EOS/) {
- $_ = $height - $_ foreach @y;
- }
- # calculate scaling factors for AF area coordinates
- ($sx,$sy) = (1/$width, 1/$height);
- for ($i=0; $i<$$info{ValidAFPoints}; ++$i) {
- my ($x1,$y1) = (($x[$i]-$w[$i]/2)*$sx, ($y[$i]-$h[$i]/2)*$sy);
- my ($x2,$y2) = ($x1+$w[$i]*$sx, $y1+$h[$i]*$sy);
- push @faceList, { Position => [$x1, $y1, $x2, $y2] };
- }
- }
- } elsif ($make eq 'Olympus') {
- # Olympus stores 1 or 2 (or maybe more in the future) sets of face-detect data.
- # I'm not sure why, but the 2nd set (Type 1) seems to be more accurate.
- return $msg unless Found($info, $msg, 'FacesDetected', 'FaceDetectArea');
- my @f = split ' ', $$info{FacesDetected};
- return 'No faces' unless $f[0] or $f[1];
- my @a = split ' ', ${$$info{FaceDetectArea}};
- my (@m, $type, $index);
- if ($$info{MaxFaces}) {
- @m = split ' ', $$info{MaxFaces};
- $type = 'new';
- $index = 0;
- } else {
- push @m, 0;
- $index = 2; # information from older models
- }
- # get face detect frame size
- if ($$info{FaceDetectFrameSize} and
- $$info{FaceDetectFrameSize} =~ /^(\d+) (\d+)/)
- {
- ($fw, $fh) = ($1, $2);
- } else {
- ($fw, $fh) = (240, 180); # empirically determined for older models
- }
- # get width/height of largest resized image that fits into face detect frame
- my ($crop_w, $crop_h, $crop_x, $crop_y);
- if ($$info{FaceDetectFrameCrop} and
- $$info{FaceDetectFrameCrop} =~ /^(\d+) (\d+) (\d+) (\d+)/)
- {
- ($crop_x, $crop_y, $crop_w, $crop_h) = ($1, $2, $3, $4);
- } else {
- ($crop_x, $crop_y, $crop_w, $crop_h) = GetCropArea($fw, $fh, $aspect);
- }
- ($sx, $sy) = (1/$crop_w, 1/$crop_h);
- my $rot = 90; # default rotation angle to landscape
- my ($orient, $max);
- $orient = $1 if $$info{Orientation} and $$info{Orientation} =~ /(\d+)/;
- my $pos = 0;
- foreach $max (@m) {
- my $faces = shift @f;
- my $face;
- for ($face=0; $face<$faces; ++$face) {
- last if $pos + 4 > scalar @a;
- my $n = $pos + $face * ($type ? 4 : 8);
- my ($x1,$y1,$x2,$y2,$xc,$yc,$rotation);
- if ($type) {
- my ($x,$y,$w,$r) = @a[$n..($n+3)];
- if (defined $orient) {
- # the angle depends on orientation for some models (doh!)
- $r -= $orient if $model eq 'u-7050';
- } else {
- # rotate by 270 if any face orientation is 270 degrees
- $rot = 270 if $r == 270;
- # again, angle depends on orientation for some models
- $r -= $rot if $wasRotated and $model =~ /^FE4030/;
- }
- $rotation = $r;
- $rotation += 360 if $rotation < 0;
- my $hx = $w * $sx / 2;
- my $hy = $w * $sy / 2;
- # adjust coordinates for cropped border and normalize
- $xc = ($x - $crop_x) * $sx;
- $yc = ($y - $crop_y) * $sy;
- ($x1, $y1) = ($xc - $hx, $yc - $hy);
- ($x2, $y2) = ($x1 + $w * $sx, $y1 + $w * $sy);
- } else {
- # adjust coordinates for cropped image
- for ($i=$n; $i<$n+8; $i+=2) {
- $a[$i] = ($a[$i] - $crop_x) * $sx;
- $a[$i+1] = ($a[$i+1] - $crop_y) * $sy;
- }
- my ($x3,$y3,$x4,$y4);
- ($x1,$y1,$x2,$y2,$x3,$y3,$x4,$y4) = @a[$n..($n+7)];
- if ($y1 == $y2) {
- $rotation = $x1 < $x2 ? 0 : 180;
- } else {
- $rotation = $y1 < $y2 ? 90 : 270;
- }
- $x2 = $x3 if $x2 == $x1;
- $y2 = $y3 if $y2 == $y1;
- }
- # draw the face rectangle and line pointing to the top of the face
- push @faceList, {
- Position => [$x1, $y1, $x2, $y2],
- Rotation => $rotation,
- Type => $index,
- };
- }
- $pos += $max * 4;
- ++$index;
- }
- } else {
- return "Sorry, $make images not yet supported";
- }
- # finally, rotate face coordinates if image was rotated
- if ($wasRotated) {
- my $rot = 270;
- my $faceInfo;
- foreach $faceInfo (@faceList) {
- next unless defined $$faceInfo{Rotation};
- $rot = $$faceInfo{Rotation} < 180 ? 90 : 270;
- last;
- }
- # rotate face coordinates
- foreach $faceInfo (@faceList) {
- my $p = $$faceInfo{Position};
- if ($rot == 90) {
- @$p = ($$p[1], 1-$$p[0], $$p[3], 1-$$p[2]);
- } else {
- @$p = (1-$$p[1], $$p[0], 1-$$p[3], $$p[2]);
- }
- next unless defined $$faceInfo{Rotation};
- $$faceInfo{Rotation} -= $rot;
- $$faceInfo{Rotation} += 360 if $$faceInfo{Rotation} < 0;
- }
- }
- return \@faceList;
- }
- # end
Add Comment
Please, Sign In to add comment