Feb 27 2004

Perl bringing Pia to the web

In Big brother watching over little sister I mentioned a primitive motion detection application that grabs frames from the wireless network camera in Pia’s nursery and publishes them through (a secret page on) KahunaBurger. I’ve got a number of requests for details about this application. Here they are …

In Pia’s nursery we have a Veo Observer Wireless Network Camera. Great camera, but it has two major flaws: The supplied software only allows you to view the camera from a PC (using an Active-X control) and only one person at a time is allowed to connect to the camera. These restrictions make the camera essentially unusable for the kind of task I’m using it for.

After downloading the Camera SDK from Veo’s web site I created a minimal application that would connect to the camera on fixed interverals and store a JPG frame on the harddisk. This happens on one of my Windows PCs, because the SDK is only available for Windows (I’m still looking for a way to decode the XJPG frames). The JPG file is then shipped to my FreeBSD box. The FreeBSD system also happens to be the web server for kahunaburger.com.

Again on a fixed interval, the FreeBSD system will look at the new file and compare the file to the “lastest” camera grab. If it detects a certain amount of difference between the two frames, it will start the process of saving the current frame. “Saving the current frame” involves:

  • copying the “latest” image to the “last” image
  • copying the “current” image to the “latest” image
  • generating a thumbnail for the “latest” image
  • maintaining a list of up to 80 saved frames
  • maintaining a list of up to 80 thumbnails
  • regenerating a HTML page that shows those thumbnails and saved frames

Here is a small section from the HTML-page generated by the scaript. This section shows the table that holds the thumbnails for all saved frames. As you hover over each thumbnail, the big picture is automatically updated, showing you a full-size picture of the frame the mouse is currently over.

Screenshot of Pia's page

The code uses pretty much only standard stuff besides the Imager and the HTML::Template modules. Both of them are available on CPAN and/or through ActiveState’s ppm.

I’m not supplying the index.template file mentioned in the script, because mine is tuned for the kahunaburger.com environment and will not work for you anyway. Just make sure you have a template variable content in the body and a variable js in the head.

Let's me know, if you have questions.

#!/usr/bin/perl -w

# we are always strict - aren't we?
use strict;

use File::Spec;
use File::stat;
use File::Copy;

use Imager;
use HTML::Template;

# use the FORCE to generate the HTML page even if no change was detected
use constant FORCE => 0;

# what's the URL for the page?
use constant THISURL => qq{http://www.yourserver.com/motion/};
# what's the filesystem path for the page?
use constant PATH => qq{/mnt/apache/www.yourserver.com/htdocs/motion};

# constants that determine when we keep an image
use constant DARKNESS => 0.190;
use constant DIFFERENCE => 1.000;
use constant GRAYNOISE => 40;
use constant DIFFWIDTH => 160;

# where are the "latest" and "last" files kept?
use constant IMAGEDIR => qq{pia};
use constant SOURCE => qq{webcam.jpg};
use constant LATEST => qq{latest.jpg};
use constant LAST => qq{last.jpg};
use constant TIMESTAMP => qq{.timestamp};

# how many frames/thumbnails do we keep around?
use constant SAVEFRAMES => 80;
use constant SAVEDIR => qq{save};
use constant SAVEPREFIX => qq{saved-};

use constant THUMBDIR => qq{thumb};
# how wide are our thumbnails?
use constant THUMBWIDTH => 80;

# what's the filename for the templat file?
use constant HTMLTEMPLATE => qq{index.template};
# and where does the final html file go?
use constant HTMLFINAL => qq{index.html};

# go to "our" directory
unless(chdir(PATH)) {
    print STDERR "unable to change directory to ",PATH,"\n";
    exit(1);
}

# check if camera source exists
if(!-f SOURCE || -z _) {
    print STDERR "no source or empty source file\n";
    exit(1);
}
# check modification time of old source against
# camera source
my ($lastcheck)=0;
my $newsource=File::Spec->catfile(IMAGEDIR,SOURCE);
my $last=File::Spec->catfile(IMAGEDIR,LAST);
my $latest=File::Spec->catfile(IMAGEDIR,LATEST);
if(-f $latest) {
    my $tstat=stat($latest);
    $lastcheck=$tstat->mtime;
}
my $sstat=stat(SOURCE);
if($lastcheck >= $sstat->mtime) {
    print STDERR "no new data\n";
    exit(1);
}
# copy current source to latest and verify
unless(copy(SOURCE,$newsource) && -f $newsource && -s _ == $sstat->size) {
    print STDERR "invalid new source after copy\n";
    exit(1);
}
# copy access/modtime
utime($sstat->atime,$sstat->mtime,$newsource);
# we are good to go ... remove last (ignore errors)
unlink($last);
# move latest to last (ignore errors)
move($latest,$last);
# move newsource to latest (ignore errors)
move($newsource,$latest);
# open the image files
my $latestImage=Imager->new();
unless($latestImage->open(file=>$latest)) {
    print STDERR "cannot open $latest - ",$latestImage->errstr(),"\n";
    exit(1);
}
my $lastImage=Imager->new();
unless($lastImage->open(file=>$last)) {
    print STDERR "cannot open $last - ",$lastImage->errstr(),"\n";
    exit(1);
}

my(@savedFiles);
unless(opendir(DIR,SAVEDIR)) {
    print STDERR "cannot open save directory\n";
}
while(my $entry=readdir(DIR)) {
    my $filename=File::Spec->catfile(SAVEDIR,$entry);
    next unless (-f $filename);
    push(@savedFiles,[$entry,stat($filename)->mtime]);
}
close(DIR);
# sort files by modification time (newest first)
@savedFiles=sort { $b->[1] <=> $a->[1] } @savedFiles;

# now check if this image is worth saving
if(FORCE || compareImages($lastImage,$latestImage)) {
    # looks like latest fits the criteria, let's save it to the
    # motion directory

    # trim number of saved files to SAVEFRAMES-1 (we are going to add one)
    if(scalar(@savedFiles) >= SAVEFRAMES - 1) {
        my(@oldFiles)=splice(@savedFiles,SAVEFRAMES - 1);
        foreach (@oldFiles) {
            # remove saved frame
            unlink(File::Spec->catfile(SAVEDIR,$_->[0]));
            # remove thumbnail
            unlink(File::Spec->catfile(THUMBDIR,$_->[0]));
        }
    }
    # generate a new filename
    my $savedFile=SAVEPREFIX.time.qq{.jpg};
    # add to list of saved files
    unshift(@savedFiles,[$savedFile,time]);
    $latestImage->write(file=>File::Spec->catfile(SAVEDIR,$savedFile));
    # generate thumbnail
    $latestImage=$latestImage->scale(xpixels=>THUMBWIDTH);
    $latestImage->write(file=>File::Spec->catfile(THUMBDIR,$savedFile));
}
# synchronize the save/thumb folders to make sure we are not wasting space
# in the thumb dir
if(opendir(DIR,THUMBDIR)) {
    my(@thumbsToRemove);
    my(@filesInSaved)=(map { $_->[0] } @savedFiles);
    while(my $entry=readdir(DIR)) {
        my $filename=File::Spec->catfile(THUMBDIR,$entry);
        next unless (-f $filename);
        # is the thumb also in the save directory?
        next if(grep(/^$entry$/i,@filesInSaved));
        push(@thumbsToRemove,$filename);
    }
    close(DIR);
    foreach(@thumbsToRemove) {
        unlink($_);
    }
}

# generate the table for the images
my $t=qq{<table border="1">};
my($rows)=0;
my($saved)=SAVEFRAMES;
while($saved) {
    if(!($saved % 8)) {
        if($saved != SAVEFRAMES) {
            $t.=qq{</tr>\n};
            if($rows++ == 4) {
                $t.=qq{<tr><td colspan="8"><center><img name="latest" src="$latest"><div id="label">}.
                  scalar(localtime()).qq{</div></center></td></tr>\n};
            }
        }
        $t.=qq{<tr>};
    }
    if(scalar(@savedFiles)) {
        my $entry=shift(@savedFiles);
        my $imgname=THUMBDIR.qq{/}.$entry->[0];
        my $simgname=SAVEDIR.qq{/}.$entry->[0];
        my $datestamp=scalar(localtime($entry->[1]));
        $t.=qq{<td><center><a href="#" onmouseover="update('latest','$simgname','$datestamp')">}.
          qq{<img border="0" src="$imgname" alt="$datestamp"></a></center></td>};
    }else{
        $t.=qq{<td>&nbsp;</td>};
    }
    $saved--;
}
$t.=qq{</tr></table>\n};
# we are ready to generate the HTML file
my $template = HTML::Template->new(
                                   filename => HTMLTEMPLATE,
                                   die_on_bad_params => 0,
                                  );
my $htmlfile=HTMLFINAL.$$;
unless(open(OUT,">$htmlfile")) {
    print STDERR "cannot write to $htmlfile - $!";
    exit(1);
}
# use this and a meta variable in your template to automatically refresh the page (300=5 mins)
$template->param('meta' => qq{<META HTTP-EQUIV="refresh" content="300;URL=}.THISURL.qq{">});
$template->param('title' => "yourserver.com - motion cam - last update ".scalar(localtime()));
$template->param('content' => $t);
$template->param('js' => qq{
<SCRIPT language="JavaScript">
<!-- Begin
    function browserdetect(){
      //returns 1 for IE, 2 for N6,3 for N4 and others
      if(navigator.appName=="Microsoft Internet Explorer"){return 1;}
      else if(navigator.appName=="Netscape" && parseInt(navigator.appVersion)==5){return 2;}
      else{return 3;}
    }

    function update(imgname,source,date_stamp) {
      var browsertype; browsertype=browserdetect();
      if(browsertype==2){ //if N6
        document.getElementById(imgname).src=source;
      }else{
        document.all[imgname].src=source;
      }
      el = document.all ? document.all('label') :
             document.getElementById ? document.getElementById('label') : null;
      if (el) el.innerHTML = date_stamp;
    }
// End -->
</SCRIPT>});
print OUT $template->output;
close(OUT);
# remove current file
unlink(HTMLFINAL);
# and bring current one into place
move($htmlfile,HTMLFINAL);
exit(0);

sub compareImages {
    my($img1,$img2)=@_;
    # scale down for comparison
    $img1=$img1->scale(xpixels=>DIFFWIDTH);
    $img2=$img2->scale(xpixels=>DIFFWIDTH);
    # convert to grayscale
    $img1=$img1->convert(preset=>'grey');
    $img2=$img2->convert(preset=>'grey');
    return diffImages($img2, $img1, DARKNESS, DIFFERENCE);
}

sub diffImages {
    my($older,$newer,$darkness,$difference)=@_;
    my($white)=$older->getwidth()*$older->getheight()*255;
    my($d2,$s2)=("",0);
    $newer->write(data=>\$d2,type=>'raw');
    # check darkness of frame
    map { $s2+=ord($_) } split(//,$d2);
    # do we have a dark frame?
    if($s2/$white <= $darkness) {
        return 0;
    }
    my($d1,$s1)=("",0);
    $older->write(data=>\$d1,type=>'raw');
    my($i)=0;
    map { my $x=abs(ord($_)-vec($d2,$i++,8));
          $s1+=($x*$x) if($x>=GRAYNOISE);
      } split(//,$d1);
    my $d=$s1/$white;
    if($d < $difference) {
        return 0;
    }
    return 1;
}

3 Responses to “Perl bringing Pia to the web”

  • Dana Says:

    Tobi, This kind of image production reminds me very much of the work of some artists. Have you considered using these images, namely the series if images as they relate to the time shot, for works of art? See movie, Smoke, for inspiration.

  • Barry Marshall Says:

    Dear Kahunaburger team,
    These are very nice baby photos on your site which I found while looking for Veo camera support. The question I had was, if I get a java error from my veo in explorer, is there a download somewhere I can use to fix it? The only help I found so far was a “windows xp reinstall/fix” solution. There must be an easier way (the support from Veo sites was unhelpful).
    My son has a new baby coming today but my suggestion to use a scanner as a change table has not been taken up yet. We have a similar system running at my lab to post diagnostic tests on the web: http://www.hpylori.com.au/clocam/scgh1-clocam.html
    Barry Marshall

  • amtc Says:

    Q: Do you know the direct path to the jpg file on the veo cam?

    I am trying to do something similar where Im using software called webcam xp, and it is asking for the path to the jpg file on the cam i want to access.

    Do you know what it is?
    (obviously ill be using my ip:port etc)

Leave a Reply