Web controlled camera with pan-tilt capability

Controlling a web camera and its motion over the internet.

Arduino,Processing IDE,PHP October '14

Introduction

This project aims at using a conventional web camera and adding pan tilt functionality using two analog servo motors. The web camera can be controlled using a webpage as the user interface which consists of the live webcam feed alongside a button pad used to move the camera.

Demonstration Video


Step 1
Physical Construction

An ordinary USB web camera was modified by making a crude camera mount out of the two servos, servo arms, 3mm gauge steel wire, some zip ties and a pair of crafty hands.

Although there is no standard method of construction you may hack the camera to build the mount as per the degree of precision required which in my case was not more than a degree.

Alternatively you could source a cheap camera mount available on RadioShack which is compatible with most of the analog servos in case you are planning to use this assembly for advanced applications such as face tracking.

Here are some pictures of my webcam mount.




Step 2
Controlling servo movement with an Arduino

Controlling servo movement with an Arduino The servo can be controlled primarily using servo.write () function under Servo.h As we know a servo has three pins GND(black/brown),POWER(red/orange) and SIGNAL(yellow).

The servo control is achieved by using the PWM (Pulse width modulation) output pins on the Arduino, the PWM output is provided to the signal input of the servo. In my case I used pin 10, 11 for the PWM output.

Here is the code (Refer to inline comments for explanation)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <Servo.h> //include this header file for servo control functions
 
Servo myservo1;  // create servo object to control a servo objects for the two servos.
Servo myservo2;  
 
String IncomingData = "";
String Temp = "";
char var;//the character buffer which stores a single character that is read.

String span="";
String stilt="";

int pan;//new incoming value of pan.
int tilt;//new incoming value of tilt.

int c_pan;//current value of pan angle
int c_tilt;//current value of tilt angle

int pos;

void setup() 
{ 
  c_pan=90;
  c_tilt=90;
  Serial.begin(9600);
  myservo1.attach(10);// attaches the servo on pin 10 to the servo object 
  myservo2.attach(11);// attaches the servo on pin 11 to the servo object 
  myservo1.write(c_pan);
  myservo1.write(c_tilt);//setting the servos to initial positions i.e (90,90) 
  
} 
 
 
void loop() 
{ 
  while(Serial.available() > 0) {
    delay(10); //introducing a delay makes reading smoother.
    if (Serial.available() >0) {
        var = Serial.read();  //gets one byte from serial buffer
        IncomingData+=String(var);//building the incoming string  
    }
    }
    if(IncomingData.length()>0)
    {
      int temp=IncomingData.indexOf(';');//find the index of ';'
      
      
  Serial.print("Command String :"+IncomingData+"\n"); 
  span=IncomingData.substring(0,temp);//split the string into pan and tilt strings
  stilt=IncomingData.substring(temp+1);//An example could be 03;67 to "03" and "67"
  Serial.print("Raw pan:"+span+"\n");//we send single digits prepended by a zero note:this is not necessary.
  Serial.print("Raw tilt:"+stilt+"\n");
  
  if(span[0]=='0')
  {
    char c=span[1];//if first char is 0 we take only the second digit.
    pan = c - '0';//used to convert char to int implicitly.
    Serial.print("pan:"+String(pan)+"\n");
  }
  else
  {
    pan=span.toInt();//use the two-digit value as is.
    Serial.print("pan:"+String(pan)+"\n");
  }
  
  if(stilt[0]=='0')
  {
    char c=stilt[1];//same operations as explained above.
    tilt = c - '0';
    Serial.print("tilt:"+String(tilt)+"\n");
  }
  else
  {
    tilt=stilt.toInt();
    Serial.print("tilt:"+String(tilt)+"\n");
  }
  
  if(c_pan<pan)
  {
  //if current pan angle is smaller than new pan angle increase angle from current pan angle to new pan angle.
  for(pos = c_pan; pos < pan; pos += 1)  // goes from c_pan degrees to pan degrees 
  {                                  // incrementing in steps of 1 degree 
    myservo1.write(pos);              // tell servo to go to position in variable 'pos' 
    delay(15);                       // waits 15ms for the servo to reach the position 
  }
  }
  else
  {
  //if current pan angle is larger than new pan angle decrease angle to new pan angle.
    for(pos = c_pan; pos >pan; pos -= 1)  // goes from c_pan degrees to pan degrees 
  {                                  // decrementing in steps of 1 degree 
    myservo1.write(pos);              // tell servo to go to position in variable 'pos' 
    delay(15);                       // waits 15ms for the servo to reach the position 
  }
  }
  c_pan=pan;//set the new pan value to current pan value after turning to new pan value
  
   if(c_tilt<tilt)
  {
  //same operations performed for tilt as for pan in the section above.
  for(pos = c_tilt; pos < tilt; pos += 1) 
  {                                  
    myservo2.write(pos);              
    delay(15);                        
  }
  }
  else
  {
    for(pos = c_tilt; pos >tilt; pos -= 1)  
  {                                   
    myservo2.write(pos);              
    delay(15);                        
  }
  }
  c_tilt=tilt;//set the new tilt value to current tilt value after turning to new tilt value
  
 IncomingData = "";
 //clear the incoming string buffer
    }
} 

Summary

The code reads a string from the serial port which is in the format “;” and processes the string by extracting the pan and tilt values as integers. Once these values are obtained the servos are positioned accordingly.

We show the user that he is panning or tilting between -90 to +90 degrees, however the servos have a range from 0 degree to 180 degree.

Hence we add +90 degree to the angle that the user selects using the webpage.

Step 3
Setting up the live camera feed

This step is pretty straightforward and involves installing a third party software to stream the video obtained from a webcam online.

In my case I used Yawcam which does the job flawlessly,it is a free utility and can be downloaded online. Setup your device by selecting it from the list of devices/cameras under Yawcam.

Once selected Yawcam will show a window with the video feed from the selected device. After this enable HTTP and Stream options from the main list of options.

Step 4
Building the user interface

This step is pretty straightforward and involves installing a third party software to stream the video obtained from a webcam online.

A PHP based server is needed to host the user interface files. I used XAMPP ,you may install your preferred PHP server.

The user interface is a web page which consists of an 'iframe' element with its source set to Yawcam’s video feed URL.(obtaining the URL is explained in the steps that follow)

It also consists of a button pad with four buttons for:
1) Panning left
2) Panning right
3) Tilting up
4) Tilting down

Clicking on any of the buttons sends the current values of pan and tilt to a PHP script via AJAX which writes these to a text file named values.txt. The code for the user interface(index.php) and PHP file writing script(control.php) is as below:

<!DOCTYPE html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script>
var timer_up;
var timer_down;
var timer_left;
var timer_right;
var pan=0;
var tilt=0;
function up_move()
{
if(parseFloat($('#tilt').css('top'),10)>60)
{
$('#tilt').css('top',(parseFloat($('#tilt').css('top'), 10)-2.68333333333333333)+"px");
tilt=parseInt(Number(((301.5-parseFloat($('#tilt').css('top'),10))/2.68333333333333333).toFixed(2)));
document.getElementById('tilt_disp').innerHTML=tilt+"&#176";
senddata();
timer_up=setTimeout(up_move,150);
}
}
function up_move_stop()
{
clearTimeout(timer_up);
}

function left_move()
{
if(parseFloat($('#pan').css('left'),10)>109)
{
$('#pan').css('left',(parseFloat($('#pan').css('left'), 10)-3.51111111111111111)+"px");
pan=parseInt(Number(((parseFloat($('#pan').css('left'),10)-430)/3.5666666666666667).toFixed(2)));
document.getElementById('pan_disp').innerHTML=pan+"&#176";
senddata();
timer_left=setTimeout(left_move,150);
}
}
function left_move_stop()
{
clearTimeout(timer_left);
}
function down_move()
{
if(parseFloat($('#tilt').css('top'),10)<523)
{
$('#tilt').css('top',(parseFloat($('#tilt').css('top'), 10)+2.68333333333333333)+"px");
tilt=parseInt(Number(((301.5-20-parseFloat($('#tilt').css('top'),10))/2.68333333333333333).toFixed(2)));
document.getElementById('tilt_disp').innerHTML=tilt+"&#176";
senddata();
timer_down=setTimeout(down_move,150);
}
}
function down_move_stop()
{
clearTimeout(timer_down);
}

function right_move()
{
if(parseFloat($('#pan').css('left'),10)<731)
{
$('#pan').css('left',(parseFloat($('#pan').css('left'), 10)+3.51111111111111111)+"px");
pan=parseInt(Number(((parseFloat($('#pan').css('left'),10)-430+20)/3.5666666666666667).toFixed(2)));
document.getElementById('pan_disp').innerHTML=pan+"&#176";
senddata();
timer_right=setTimeout(right_move,150);
}
}
function right_move_stop()
{
clearTimeout(timer_right);
}

function senddata()
{
            var span=(-1*pan)+90;
            var stilt=(-1*tilt)+90;
            var xmlhttp;
            if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari
                xmlhttp = new XMLHttpRequest();
            }
            else {// code for IE6, IE5
                xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
            }
            xmlhttp.onreadystatechange = function () {
                if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
                  // alert(xmlhttp.responseText);
                }
            }
            xmlhttp.open("POST", "control.php", true);
            xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            if(span<10 && stilt<10)
            {
            xmlhttp.send("pan=0" + span + "&tilt=0" + stilt);
            }
            if(span<10 && stilt>10)
            {
            xmlhttp.send("pan=0" + span + "&tilt=" + stilt);
            }
            if(span>10 && stilt<10)
            {
            xmlhttp.send("pan=" + span + "&tilt=0" + stilt);
            }
            if(span>10 && stilt>10)
            {
            xmlhttp.send("pan=" + span + "&tilt=" + stilt);
            }
            
}
function init()
{
pan=0;tilt=0;
senddata();
}
</script>
<title>gimbalcam|Home</title>
<style>
.bctrl
{
opacity:0.7;
}
.bctrl:hover
{
opacity:1;
}
#pan
{
margin-top:-2.5px;
position:absolute;
height:54px;
left:420px;
width:20px;
background-color:#546353;
z-index:3;
}
#tilt
{
position:absolute;
width:55px;
top:291.5px;
left:752.5px;
height:20px;
background-color:#546353;
z-index:3;
}
#control
{
padding:10px;
position:absolute;
width:180px;
height:180px;
top:60px;
left:810px;
background-color:#546353;
border-radius:20px;
}
</style>
</head>
<body onload=init()>
<div id="pan"></div>
<div id="tilt"></div>
<div style="position:absolute;left:109px;width:642px;height:50px;z-index:2;background-color:#373733;"></div>
<div id="pan_disp" style="position:absolute;left:5px;width:60px;height:50px;z-index:2;font-size:30px;color:green;">0&#176</div>
<div style="position:absolute;left:755px;top:60px;width:50px;height:483px;z-index:2;background-color:#373733;text-align:center;font-size:40px;color:white;"></div>
<div id="tilt_disp" style="position:absolute;left:850px;top:490px;width:60px;height:50px;z-index:2;font-size:40px;color:red">0&#176</div>
<div style="position:absolute;top:100px;left:100px;" ><iframe height="560px" width="660px" src="http://182.57.167.147:8081/" scrolling="no" frameborder="none" style="margin-top:-100px" ></iframe>
</div>
<table cellspacing="0" border="0" cellpadding="0" id="control">
<tr>
<td>
<td><img class="bctrl" id="up" src="up.png" height="60px" width="60px" onmousedown="up_move()" onmouseup="up_move_stop()"></img>  
<td>
</tr>
<td><img class="bctrl" id="left" src="left.png" height="60px" width="60px" onmousedown="left_move()" onmouseup="left_move_stop()"></img>  
<td>
<td><img class="bctrl" id="right" src="right.png" height="60px" width="60px" onmousedown="right_move()" onmouseup="right_move_stop()"></img>  
<tr>
<td>
<td><img class="bctrl" id="down" src="down.png" height="60px" width="60px" onmousedown="down_move()" onmouseup="down_move_stop()"></img>  
<td>
</tr>
<tr>
</tr>
</table>
</body>
<html>

control.php

1
2
3
4
5
6
7
8
<?php
$pan=$_POST["pan"];
$tilt=$_POST["tilt"];
$myfile = fopen("values.txt", "w") or die("Unable to open file!");
$txt = $pan.";".$tilt;
fwrite($myfile, $txt);
fclose($myfile);
?>

Step 5
Making your camera visible to the world

This step basically sets up your computer as your own web server on the internet. It involves forwarding your ports via your router (Note: this can work only if your ISP allows forwarding ports) Port forwarding setup may vary from router to router, please refer to portforward.com for detailed device specific guides to forwarding port.

In my case I forwarded the following ports:
1)80-XAMPP runs the Apache server on this port.
2)8081-Yawcam video streaming runs on this port.

Note that while forwarding these ports associate the local IP address of the computer that you will be using for the above services.

Step 6
Reading values.txt using Processing and writing its contents on the serial port.

The following simple code in Processing (Java) reads the text file and then writes the string on the serial port on which Arduino is looking for the same data string. This operation is repeated every 900ms, although this a large delay it is much needed for the Arduino to initialize its serial communication.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 import processing.serial.*;
 Serial port;
 
 void setup()  
{
    port = new Serial(this,"COM42", 9600);  //Open the port that the Arduino board is connected to, at 9600 baud
 
}
 void draw() {
  String val[] = loadStrings("C:/xampp/htdocs/values.txt"); // Insert the location of your .txt file
  print( val[0]);
  port.write(val[0]);
  delay(900);
 }