Overhead mapFrom Brown University RoboticsThe overhead_map webpage allows one to display images, lines, points, and arrows on a top-down rendering of a real-world surface.
InstallationThe overhead visualization system requires a current version of brown-ros-pkg, which includes packages for rosjs (ROS JavaScript interface), om_msgs (ROS overhead map messages), and [1] (overhead_map JavaScript Applet). Assuming brown-ros-pkg is installed, it can be updated by: > roscd irobot_create_2_1/../.. # (ie, the root directory of brown-ros-pkg) > svn update The om_msgs package needs to be "compiled" so that the message types Overhead_Map_Obj and Overhead_Map_Objs are available: > rosmake om_msgs You will also need a websockets compatible web browser* (check), such as Chrome 4+, Firefox 4+, or Safari 5+
Introduction: Running omclock example on a single machineTo test that we can get everything up and working, we will use omclock, which draws a clock face with the current time on a soccer field. Once this is up and running, one can easily swap omclock with any other node that publishes Overhead_Map_Objs messages to allow for custom visualizations. omclock has a built-in launch file, which will start omclock and rosjs: > roslaunch omclock omclock.launch Next, point your web browser to: file://[PATH-TO-brown-ros-pkg]/experimental/overhead_map/overhead_map.html where the "PATH-TO-brown-ros-pkg is the local path to brown-ros-pkg. For example, in Brian's test, he visits the page: file:///home/brian/ros/stacks/brown-ros-pkg/experimental/overhead_map/overhead_map.html If successful, you should see a rendering of the CS148 field with a clock.
Running overhead_map with custom codeIf all overhead_map could do was run clock applications, it would be pretty useless! The good news, though, is that it is a very straightforward process to move away from the demo provided and completely swap it with your code. Simply put, to publish messages to overhead_map, one needs to publish messages of type Overhead_Map_Objs (defined in om_msgs/msg) to the topic "overhead_map_objs". (In fact, this is exactly what omclock does.) When a web browser is directed to the overhead_map page, overhead_map communicates to rosjs via WebSockets to ask rosjs to subscribe to said topic; rosjs then listens for the topic in ROS and forwards the messages to overhead_map. overhead_map then renders the messages on-screen as they arrive. In each rendering, only the last sent message is rendered. Details about the publishing format for om_msgs can be found in om_msgs/msg/Overhead_Map_Obj.msg and om_msgs/msg/Overhead_Map_Objs.msg. Below, we will walk step-by-step through omclock's source code to show how to publish this message type. Using overhead_map on multiple machinesOne of the perks of the overhead_map system is its modularity; the computer serving webpages, the computer publishing overhead_map information, and the computer viewing the information published can be three completely separate entities! This section will explain the exact division of labor and how to connect the divided laborers together over a local network. The three tasks at hand are:
Any single machine may take on one or multiple roles, which interact in the following manner:
Installation
http://[server]/[overhead_map-path]/overhead_map.html?address=[robot] Alternatively, if server and laptop are the same computer, we can look up the website without an http server, in which case when we refer to "the overhead_map website" below, we will be referring to: file://[path-to-brown-ros-pkg]/experimental/overhead_map/overhead_map.html?address=[robot] If the robot and laptop are the same computer, the "?address=[robot]" querystring may be left off. (This value defaults to 127.0.0.1, ie the local computer.)
Multi-machine introduction: Running omclock example on multiple machines machineThis example will closely follow the single-machine example for running omclock, but it is adapted to use three (or two (or even one)) machines. Like in the single-machine example, after omclock is up and running, it is simple to swap out the omclock node for a custom node on your robot.
> roslaunch omclock omclock.launch
If successful, on the laptop you should see a rendering of the CS148 field with a clock. Customizing overhead_mapThe overhead_map package was designed to be very flexible with regard to the images, measurements, etc. used. The icons (such as the "irobot-create" picture), backgrounds (such as the soccer field), and logos (such as the center Brown logo and the RLAB logo in the bottom-right) are all customizable without ever touching the Javascript code. Adding/changing backgroundsWe will demonstrate how to add a background by means of example. Let's say you wanted to make a court for playing robot tennis, and let's suppose
Navigate to the overhead_map base directory and then to the "backgrounds" subdirectory. Let's say you want to Let's ... Adding/changing logosLorem ipsum... Adding/changing iconsLorem ipsum... Walkthrough of omclock.pyomclock handles each of the 4 basic types of messages passable by om_msgs, so it will be used to demonstrate how each of these is used. omclock.py can be found by: > roscd omclock/bin > gedit omclock.py # or your favorite editor Breakdown of omclock.launchIn the om_clock example, the launch file starts ROS nodes for rosjs and omclock (to drive the clock behavior): <launch> <node name="rosjs" pkg="rosjs" type="rosjs.py" /> <node name="omclock" pkg="omclock" type="omclock.py" /> </launch> Breakdown of overhead_map.js and usage in htmlThe example webpage for the om_clock example contains a overhead_map JavaScript object, such as the one contained in brown-ros-pkg. overhead_map.js uses rosjs to listen for messages related to the visualizing overhead localization topics and a canvas element to render these objects in the web browser. Note: This page depends on jQuery, the jQuery query string object plugin, and ros.min.js which are available either locally (as provided in overhead_map/js) or accessible online.
<html>
<head>
<title>Overhead Map</title>
<script type="text/javascript" src="js/jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="js/jquery.query.js"></script>
<script type="text/javascript" src="js/ros.min.js"></script>
<script type="text/javascript" src="overhead_map.js"></script>
</head>
<body>
<table>
<tr><td>
<!-- canvas width/height controls the max. size of the board -->
<canvas id="board" width="800" height="600" ></canvas>
</td></tr>
<tr><td>
Log:
</td></tr>
<tr><td>
<textarea id="console" rows="10" cols="80" readonly="readonly"></textarea>
</td></tr>
</table>
</body>
</html>
Message typesFirst, let's look at how our messages are specified. For each desired update, we will pass a single message of type Overhead_Map_Objs -- essentially, a list with elements of type Overhead_Map_Obj -- to rosjs. rosjs will then use websockets to send this information to your web browser, resulting in the clock appearing on your screen. The message types the overhead_map system (including but not limited to overhead_map, omclock, and omdc) uses are in the ROS node om_msgs: > roscd om_msgs/msg There are two message types specified, namely Overhead_Map_Obj.msg and Overhead_Map_Objs.msg Overhead_Map_Obj.msg: # For each type of name, there is an associated tuple: # * "point": [x y] draws a point at (x, y) # * "line": [x0 y0 x1 y1] draws a line from (x0, y0) to (x1, y1) # * "arrow": [x0 y0 x1 y1] draws an arrow (x0, y0) --> (x1, y1) # * "imname": [x y th] draws an image named "imname" centered # at (x,y), rotated th radians. # location of image is # "$OVERHEAD_MAP_ROOT/icons/imname.png". string name float32[] tuple Overhead_Map_Objs.msg: Header header Overhead_Map_Obj[] objs ScaffoldingLet's start making omclock.py. There's a bit of ROS scaffolding to do first: #!/usr/bin/env python
import roslib; roslib.load_manifest('omclock')
import rospy, time
from math import cos,sin,pi
from om_msgs.msg import Overhead_Map_Obj, Overhead_Map_Objs
def go():
pub = rospy.Publisher('overhead_map_objs',Overhead_Map_Objs)
rospy.init_node('omclock')
# main control loop
r = rospy.Rate(10) # hz
while not rospy.is_shutdown():
#
#
# Make the clock!
#
#
# wait for next loop
r.sleep()
# Input: propTime in [0,1]
# propTime is, eg, hours/12 or minutes/60
# Returns the angle (in radians) of the clock pointer at
def clockAngle(propTime):
return (2*pi*(1-propTime)) + pi/2
def dtrace(s,x): # for debugging
rospy.loginfo(s+" == %s"%x)
return x;
if __name__ == '__main__':
go()
Basically, we are setting Python up to publish messages of type Overhead_Map_Objs on topic "overhead_map_objs" and to publish the information at about 10 frames per second. (This will let the robot, which represents the second hand, look like it is driving around the clock face.) We also have some helper functions tangential to this walkthrough, which won't be investigated further. Making the clockWe will now create the clock face by using "Overhead_Map_Obj"s by filling the "Make the clock!" comment. Before we can do that, however, we need to know how the coordinate system is set up. All the drawing in overhead_map is performed relative to an "ideal" coordinate frame, which is similar to the one presented in math classes. In our frame, the bottom-left corner of the background is at point (0,0), x points rightward, and y points upward. Theta is measured in radians, is equal to zero when pointing towards +x, and increases as one rotates counterclockwise. The unit is declared arbitrarily, and is meant to be some sort of real-world measure. For example, the "soccer field" in real life is about 4 meters long and 2.5 meters high, so the soccer map is configured to represent the area surrounded by coordinates (0,0), (4,0), (4,2.5), and (0,2.5), and one draws objects by specifying them in ideal coordinates. For instance, if one sees an iRobot Create centered at the real world at coordinates (3,1), one can simply instruct overhead_map to draw a picture of the iRobot Create at (3,1); all the graphics processing and rendering is handled behind-the-scenes by overhead_map.js! Let's start making the clock by setting up some constants and reading in the time: # Get time. Make an analog clock face with center (centerX,centerY)
# and largest radius "radius"
# This one's set up for the soccer field's center
centerX = 2
centerY = 1.25
radius = 1
curTime = time.localtime();
hours = curTime.tm_hour
minutes = curTime.tm_min
seconds = curTime.tm_sec + (time.time()%1) # for partial seconds
ArrowNow, let's draw the hour hand using an arrow: # Hour hand is an arrow
hourAngle = clockAngle((hours+minutes*1./60+seconds*1./3600)*1./12)
hourHand = Overhead_Map_Obj()
hourHand.name = "arrow"
hourHand.tuple = [centerX,
centerY,
centerX+0.5*radius*cos(hourAngle),
centerY+0.5*radius*sin(hourAngle)]
There is a bit of computation happening to compute the angle the hour hand needs to face, but let's not worry about the details. The important bits are: hourHand = Overhead_Map_Obj() which makes the object, hourHand.name = "arrow" which tells overhead_map to draw the object as an arrow, and hourHand.tuple = [A,B,C,D] which tells overhead_map to draw an arrow from (A,B) to (pointing toward) (C,D) as specified in Overhead_Map_Obj.msg: # * "arrow": [x0 y0 x1 y1] draws an arrow (x0, y0) --> (x1, y1) The name and tuple together form a complete specification of an Overhead_Map_Obj. LineNext, we'll draw the minute hand using a line: # Minute hand is a line
minuteAngle = clockAngle((minutes+seconds*1./60)*1./60)
minuteHand = Overhead_Map_Obj()
minuteHand.name = "line"
minuteHand.tuple = [centerX,
centerY,
centerX+0.75*radius*cos(minuteAngle),
centerY+0.75*radius*sin(minuteAngle)]
Note the only difference between it and the hour hand is that the name is "line" instead of "arrow"; otherwise, the code takes the same form, as specified in Overhead_Map_Obj.msg: # * "line": [x0 y0 x1 y1] draws a line from (x0, y0) to (x1, y1) ImageNext, we'll draw the second hand by using an image of the iRobot Create: # Second "hand" is a robot going around the outside
secondAngle = clockAngle(seconds*1./60)
secondHand = Overhead_Map_Obj()
secondHand.name = "irobot-create"
secondHand.tuple = [centerX+radius*cos(secondAngle),
centerY+radius*sin(secondAngle),
secondAngle-pi/2]
Creating the blank Overhead_Map_Obj is the same as usual. The name of this object is "irobot-create", which is an image stored in overhead_map's icon subdirectory; recall from Overhead_Map_Obj.msg: # * "imname": [x y th] draws an image named "imname" centered # at (x,y), rotated th radians. # location of image is # "$OVERHEAD_MAP_ROOT/icons/imname.png". The tuple we created reflects the x, y, and theta coordinates desired. PointFinally, let's put a dot in the center of the clock for looks, using a point: # Center "dot"
centerDot = Overhead_Map_Obj()
centerDot.name = "point"
centerDot.tuple = [centerX, centerY]
Note the name "point" and the tuple specified in Overhead_Map_Obj.msg: # * "point": [x y] draws a point at (x, y) Merging "Overhead_Map_Obj"s into one "Overhead_Map_Objs", and publishingTo merge all the objects we want drawn, we simply create an instance of Overhead_Map_Objs and put all of our objects in that instance's ".objs" field: # Put it all together (front drawn first)
drawing = Overhead_Map_Objs()
drawing.objs = [secondHand, minuteHand, hourHand, centerDot]
Finally, we need to publish our instance of Overhead_Map_Objs to rosjs: pub.publish(drawing) ...and our clock is drawn! Full Source CodeHere's the entirety of omclock.py: #!/usr/bin/env python
import roslib; roslib.load_manifest('omclock')
import rospy, time
from math import cos,sin,pi
from om_msgs.msg import Overhead_Map_Obj, Overhead_Map_Objs
def go():
pub = rospy.Publisher('overhead_map_objs',Overhead_Map_Objs)
rospy.init_node('omclock')
# main control loop
r = rospy.Rate(10) # hz
while not rospy.is_shutdown():
# Get time. Make an analog clock face with center (centerX,centerY)
# and largest radius "radius"
# This one's set up for the soccer field's center
centerX = 2
centerY = 1.25
radius = 1
curTime = time.localtime();
hours = curTime.tm_hour
minutes = curTime.tm_min
seconds = curTime.tm_sec + (time.time()%1) # for partial seconds
# Hour hand is an arrow
hourAngle = clockAngle((hours+minutes*1./60+seconds*1./3600)*1./12)
hourHand = Overhead_Map_Obj()
hourHand.name = "arrow"
hourHand.tuple = [centerX,
centerY,
centerX+0.5*radius*cos(hourAngle),
centerY+0.5*radius*sin(hourAngle)]
# Minute hand is a line
minuteAngle = clockAngle((minutes+seconds*1./60)*1./60)
minuteHand = Overhead_Map_Obj()
minuteHand.name = "line"
minuteHand.tuple = [centerX,
centerY,
centerX+0.75*radius*cos(minuteAngle),
centerY+0.75*radius*sin(minuteAngle)]
# Second "hand" is a robot going around the outside
secondAngle = clockAngle(seconds*1./60)
secondHand = Overhead_Map_Obj()
secondHand.name = "irobot-create"
secondHand.tuple = [centerX+radius*cos(secondAngle),
centerY+radius*sin(secondAngle),
secondAngle-pi/2]
# Center "dot"
centerDot = Overhead_Map_Obj()
centerDot.name = "point"
centerDot.tuple = [centerX, centerY]
# Put it all together (front drawn first)
drawing = Overhead_Map_Objs()
drawing.objs = [secondHand, minuteHand, hourHand, centerDot]
pub.publish(drawing)
# wait for next loop
r.sleep()
# Input: propTime in [0,1]
# propTime is, eg, hours/12 or minutes/60
# Returns the angle (in radians) of the clock pointer at
def clockAngle(propTime):
return (2*pi*(1-propTime)) + pi/2
def dtrace(s,x):
rospy.loginfo(s+" == %s"%x)
return x;
if __name__ == '__main__':
go()
Example CodeSome code was written to test the functionality of overhead_map and may provide a good starting ground for hacking or additional insight.
> roslaunch omclock omclock.launch
|