Machine Vision
Goal
The goal of this exercise is to learn how to use vision to assist an industrial robot by detecting known objects and unknown obstacles, and then completing a pick-and-place task with a robot arm and a two-finger gripper.
Two depth cameras are available (one fixed to the world and another mounted on the robot end effector). The shape, size and colour of the objects are known, but their poses and the surrounding obstacles must be perceived using the cameras. The exercise runs on Gazebo Harmonic with ROS 2 and MoveIt 2.
Frequency API
Python
import Frequency- to import the Frequency library class. This class contains the tick function to regulate the execution rate.Frequency.tick(ideal_rate)- regulates the execution rate to the number of Hz specified. Defaults to 50 Hz.
C++
#include "Frequency.hpp"- to import the Frequency library class. This class contains the tick function to regulate the execution rate.Frequency freq = Frequency();- to instanciate the Frequency class.freq.tick(ideal_rate);- regulates the execution rate to the number of Hz specified. Defaults to 50 Hz.
Robot API
This exercise supports a ROS 2-direct implementation in addition to the original HAL-based approach. Below you’ll find the details for both options, in Python and C++.
HAL-based Implementation
Python
import HAL- to import the HAL (Hardware Abstraction Layer) library class. This class contains the functions that send and receive information to and from the Hardware (Gazebo).
Robot information
HAL.get_TCP_pose()→(xyz, ypr)- Returns the current TCP pose.xyz = [x, y, z]in metres andypr = [yaw, pitch, roll]in degrees.HAL.get_Joint_states()→[j1..j6]- Returns the current robot joint positions in degrees.
Direct Kinematics
HAL.MoveAbsJ(absolute_joints, speed, wait_time)- Moves the robot to an absolute joint-space configuration (6 joint angles in degrees), at a given relative speed in the range [0-1], adding a final delay in seconds.HAL.MoveSingleJ(joint_number, relative_angle, speed, wait_time)- Moves a single joint [1 to 6] by a relative angular increment (in degrees).
Inverse Kinematics
HAL.MoveJoint(abs_xyz, abs_ypr, speed, wait_time)- Point-to-Point (PTP) movement to an absolute Cartesian pose.abs_xyz = [x, y, z]in metres andabs_ypr = [yaw, pitch, roll]in degrees.HAL.MoveLinear(abs_xyz, abs_ypr, speed, wait_time)- Moves the TCP to an absolute Cartesian pose following a linear trajectory. Useful for pick-and-place approach motions.HAL.MoveRelLinear(relative_xyz, speed, wait_time)- Moves the TCP by a relative Cartesian displacement (in metres), keeping the orientation unchanged.HAL.MoveRelReor(relative_ypr, speed, wait_time)- Reorients the TCP by relative angular increments (Yaw,Pitch,Roll in degrees), keeping the position fixed.
Gripper
The gripper grasps and releases objects automatically through a contact-based attachment system, so no manual attach/detach calls are required.
HAL.GripperSet(percentage_closure, wait_time)- Closes (100) or opens (0) the two-finger gripper to the given closing percentage, adding a final delay in seconds. When it starts closing (> 5) automatic attachment is enabled; when it opens (<= 5) any attached object is automatically detached.
Perception
HAL.start_color_filter(color, rmax, rmin, gmax, gmin, bmax, bmin)- Starts RGB colour filtering. Supported colours:"red","green","blue","purple". RGB values in [0-255].HAL.stop_color_filter(color)- Stops a running colour filter.HAL.start_shape_filter(color, shape, radius)- Starts shape detection over the colour-filtered cloud. Supported shapes:"sphere","cylinder".radiusin metres.HAL.stop_shape_filter(color, shape)- Stops the shape filter.
Object and target queries
HAL.get_object_position(object_name)→[x, y, z]orNone- Returns the Cartesian position of a known object, orNoneif it is not found.HAL.get_object_info(object_name)→(height, width, length, shape, color)- Returns the metadata of a known object.HAL.get_target_position(target_name)→geometry_msgs/Point- Returns the position of a target (access it as.x,.y,.z).
Cameras
HAL.getImage(camera="hand")- Returns an RGB image (numpy array) from the selected camera:"hand"(wrist-mounted) or"base"(fixed).
Workspace and scanning
HAL.scan_workspace()- Moves the robot to a scan pose, triggers environment scanning, returns home, and returns the detected object list.HAL.buildmap()- Runs a full environment mapping procedure that updates the MoveIt 2 planning scene with the detected obstacles.HAL.custom_scan_sequence(scan_positions)- Runs a custom multi-pose scanning sequence.scan_positionsis a list of 6-joint configurations. Returns the merged detected objects.HAL.set_home_position(joint_angles_deg)/HAL.get_home_position()- Set or get the robot home joint configuration.HAL.back_to_home()- Moves the robot to the home position and opens the gripper.HAL.move_joint_arm(j0, j1, j2, j3, j4, j5)- Convenience wrapper for absolute joint-space motion.
C++
#include "HAL.hpp"- to import the HAL (Hardware Abstraction Layer) library class. This class contains the functions that send and receive information to and from the Hardware (Gazebo).#include "WebGUI.hpp"- to import the WebGUI library. Used to display camera images in the browser.
Robot information
HAL::get_TCP_position();- Returns the current TCP position asstd::array<double, 3>[x, y, z] in metres.HAL::get_TCP_orientation();- Returns the current TCP orientation asstd::array<double, 3>[yaw, pitch, roll] in degrees.HAL::get_Joint_states();- Returns the current joint positions asstd::array<double, 6>in degrees.
Direct Kinematics
HAL::MoveAbsJ(joints, speed, wait_time);- Moves the robot to the given angular position for each joint.jointsisstd::array<double, 6>in degrees,speedin [0,1],wait_timein seconds.HAL::MoveSingleJ(joint_number, relative_angle, speed, wait_time);- Moves a single joint by a relative angular increment.joint_numberin [1,6], angle in degrees.
Inverse Kinematics
HAL::MoveJoint(xyz, ypr, speed, wait_time);- Moves the TCP to an absolute Cartesian pose.xyzisstd::array<double, 3>in metres,yprin degrees.HAL::MoveLinear(xyz, ypr, speed, wait_time);- Moves the TCP in a linear trajectory to an absolute Cartesian pose.xyzin metres,yprin degrees.HAL::MoveRelLinear(xyz, speed, wait_time);- Moves the TCP by a relative Cartesian increment.xyzisstd::array<double, 3>in metres.HAL::MoveRelReor(ypr, speed, wait_time);- Reorients the TCP by relative angular increments.yprisstd::array<double, 3>in degrees.
Gripper
HAL::GripperSet(relative_closure, wait_time);- Controls the gripper.relative_closurein [0,100] (0 = fully open, 100 = fully closed). When closing (> 5) contact-based automatic attachment is enabled; when opening (<= 5) the attached object is automatically released.
Cameras
HAL::getImage(camera);- Returns a camera frame ascv::Mat.camerais"hand"(wrist-mounted) or"base"(fixed). Default:"hand".WebGUI::showImage(image);- Sends acv::Matframe to the browser image panel.
Perception filters
HAL::start_color_filter(color, rmax, rmin, gmax, gmin, bmax, bmin);- Starts an RGB colour filter. Supported colours:"red","green","blue","purple". RGB values in [0,255].HAL::stop_color_filter(color);- Stops the colour filter for the given colour.HAL::start_shape_filter(color, shape, radius);- Starts shape detection on the colour-filtered cloud. Supported shapes:"sphere","cylinder".radiusin metres.HAL::stop_shape_filter(color, shape);- Stops the shape filter.
Object and target queries
HAL::get_object_position(object_name);- Returns the position of a known object asstd::array<double, 3>[x, y, z] in metres. Returns{0, 0, 0}if not found.HAL::get_object_info(object_name);- Returns anObjectInfostruct with fields:position,height,width,length,shape,color. Checkinfo.shape.empty()to detect a not-found object.HAL::get_target_position(target_name);- Returns the position of a target zone asstd::array<double, 3>in metres.
Workspace
HAL::scan_workspace();- Moves to a scan pose, triggers environment scanning, and returns home.HAL::buildmap();- Runs a full environment mapping procedure.HAL::gripper_percentage_for(diameter, max_open_m);- Converts an object diameter (m) to gripper closure percentage.max_open_mdefaults to0.085m.
In order to use the HAL-based controls you must include the following lines:
#include "HAL.hpp"
#include "WebGUI.hpp"
#include "Frequency.hpp"
void exercise() {
Frequency freq = Frequency();
// Enter sequential code!
while (true)
{
// Enter iterative code!
freq.tick();
}
}
ROS 2-direct Implementation
Instead of the HAL, you can write your own ROS 2 node (importing only WebGUI) and talk straight to the simulation through standard ROS 2 topics, services and actions. The arm is driven with the standard MoveIt 2 and ros2_control interfaces; the gripper, perception and camera come from the simulator, and WebGUI exposes the debug image topic used by the browser.
Unless noted otherwise, the interfaces below use the default QoS (reliable, keep-last, depth 10); publish to /webgui_image with this default profile so it stays compatible with the GUI subscriber.
Cameras (sensor_msgs/msg/Image, BGR8)
/hand_camera/image- Subscribe to receive the wrist-mounted camera image./base_camera/image- Subscribe to receive the fixed base camera image.
Image debugging
/webgui_image- Publish asensor_msgs/msg/Imagehere to display it in the browser image panel (the equivalent ofWebGUI.showImage).
Arm motion (MoveIt 2 + ros2_control)
The arm is commanded with the standard MoveIt / ros2_control interfaces:
/compute_ik-moveit_msgs/srv/GetPositionIKservice. Solves inverse kinematics for a Cartesian goal. Fillik_request.group_name = "ur5_manipulator",ik_request.ik_link_name = "tool0"and the targetPoseStamped; anerror_code.val == 1in the response means success and the joint solution comes insolution.joint_state./joint_trajectory_controller/follow_joint_trajectory-control_msgs/action/FollowJointTrajectoryaction. Executes a joint trajectory for the six arm joints (shoulder_pan_joint,shoulder_lift_joint,elbow_joint,wrist_1_joint,wrist_2_joint,wrist_3_joint), positions in radians.
So a Cartesian move (the equivalent of MoveJoint) is two steps: call /compute_ik to turn the pose into joint angles, then send those angles to /joint_trajectory_controller. A joint-space move (the equivalent of MoveAbsJ) sends the joints straight to the controller, without IK. Note that /compute_ik only solves IK — it does not plan a Cartesian path, so a straight-line MoveLinear approach falls back to a point-to-point motion.
/current_target- Publish the Cartesian goal as ageometry_msgs/msg/PoseStamped(latched, QoS TransientLocal) to visualise it in RViz.
Gripper
/gripper_controller/follow_joint_trajectory-control_msgs/action/FollowJointTrajectoryaction controlling therobotiq_85_left_knuckle_joint(0.0open ..1.0closed)./gripper_auto_attach- Publishstd_msgs/msg/Bool(truewhile closing,falsewhile opening) to enable/disable the contact-based attachment./graspable_objects- Publish a comma-separatedstd_msgs/msg/Stringwith the objects that may be attached.
Perception filters
/start_color_filter- Publishpcl_filter_msgs/msg/ColorFilterto start/stop RGB colour filtering (colorid, RGBrmin/rmax/gmin/gmax/bmin/bmax,status)./start_shape_filter- Publishpcl_filter_msgs/msg/ShapeFilterto start/stop shape detection (colorid,shapeid,radius,status).
The numeric ids used by the filter messages are red=1, green=2, blue=3, purple=4 and sphere=1, cylinder=2.
Robot feedback
/joint_states-sensor_msgs/msg/JointState, the current joint positions (radians).- The current TCP pose is available from TF (
world→tool0).
Object and target positions
In the HAL-based version, get_object_position() and get_target_position() read these values from the exercise configuration file. A ROS 2-direct node does not have access to that file (and the C++ template does not parse it), so use the known scene positions directly. The robot base is at the origin, so all values below are base-relative, in metres. The object Z is the value get_object_position() returns: the object centre plus its radius (spheres) or half its height (cylinders).
Objects [x, y, z]:
| Object | Position |
|---|---|
red_sphere |
[0.45, -0.25, 0.31] |
green_sphere |
[0.45, 0.09, 0.31] |
blue_sphere |
[0.45, 0.25, 0.31] |
purple_sphere |
[0.45, -0.09, 0.31] |
red_cylinder |
[0.65, 0.09, 0.31] |
green_cylinder |
[0.65, -0.09, 0.305] |
blue_cylinder |
[0.65, -0.25, 0.315] |
purple_cylinder |
[0.65, 0.25, 0.31] |
Targets [x, y, z] (all at z = 0.25):
| Target | Position | Target | Position |
|---|---|---|---|
target1 |
[-0.68, -0.18, 0.25] |
target9 |
[-0.44, -0.18, 0.25] |
target2 |
[-0.68, -0.06, 0.25] |
target10 |
[-0.44, -0.06, 0.25] |
target3 |
[-0.68, 0.06, 0.25] |
target11 |
[-0.44, 0.06, 0.25] |
target4 |
[-0.68, 0.18, 0.25] |
target12 |
[-0.44, 0.18, 0.25] |
target5 |
[-0.56, -0.18, 0.25] |
target13 |
[-0.32, -0.18, 0.25] |
target6 |
[-0.56, -0.06, 0.25] |
target14 |
[-0.32, -0.06, 0.25] |
target7 |
[-0.56, 0.06, 0.25] |
target15 |
[-0.32, 0.06, 0.25] |
target8 |
[-0.56, 0.18, 0.25] |
target16 |
[-0.32, 0.18, 0.25] |
Python
Note: Ensure this import is included in your script to access the Web GUI functionalities.
import WebGUI - to enable the browser GUI and its /webgui_image bridge.
To have frequency control you need to use standard ROS 2 mechanisms to manage loop timing:
rclpy.spin()- Event-driven execution using callbacks.rclpy.spin_once()- Single-step processing, often with custom timers.rclpy.Rate()- Loop-based frequency control.
Note
WebGUI already initializes rclpy internally, so guard your own initialization with if not rclpy.ok(): rclpy.init().
C++
In order to use direct ROS controls you must include the following lines:
#ifndef USER_NODE
#define USER_NODE
#include "rclcpp/rclcpp.hpp"
class UserNode : public rclcpp::Node {
// Your class
};
#endif
You must define USER_NODE and a UserNode node class. In this mode only WebGUI is initialized (the HAL is not), so the arm, gripper and filters are commanded through the topics, services and actions listed above.
Argument examples
Home and intermediate poses (joint space, in degrees)
home = [0.0, -90.0, 70.0, -70.0, -90.0, 0.0]
pre_pick = [0.0, -90.0, 90.0, -90.0, -90.0, 0.0]
pre_place = [180.0, -90.0, 90.0, -90.0, -90.0, 0.0]
Absolute XYZ poses for MoveJoint and MoveLinear (in metres)
above_object = [object_pos[0], object_pos[1], object_pos[2] + 0.15]
at_object = [object_pos[0], object_pos[1], object_pos[2] - 0.025]
TCP orientation (YPR, in degrees)
down = [180.0, 0.0, -90.0]
Colour filter presets (rmax, rmin, gmax, gmin, bmax, bmin)
red - 255, 100, 40, 0, 40, 0
green - 40, 0, 255, 100, 40, 0
blue - 40, 0, 40, 0, 255, 100
purple - 255, 100, 120, 0, 255, 50
Theory
Object detection
Object detectors are implemented on top of PCL. First, a colour filter removes the points outside the chosen RGB range, isolating only the objects of that colour:
Then a shape segmentation detects the most likely sphere or cylinder for that colour (given an approximate radius), producing a TF frame and debug topics that let you compute approach and grasp poses:
Obstacle detection and avoidance
Point clouds captured by the camera mounted on the robot are used to update the MoveIt 2 planning scene (octomap). Build a map by moving the robot to observe the workspace; obstacles are then considered during planning to avoid collisions.
Hints
Simple hints to help you solve the Machine Vision exercise.
Where to insert and run the code
In the launched web page, type your code in the text editor and run it by pressing the play button:
import HAL
# Enter sequential code here!
while True:
# Enter iterative code here!
How should I solve the exercise?
Implement the high-level flow using HAL. A typical sequence is:
# 1) Build an obstacle map
HAL.buildmap()
# 2) Return to a clean start state
HAL.back_to_home()
# 3) Detect one object (example: green cylinder)
HAL.start_color_filter("green", 40, 0, 255, 100, 40, 0)
HAL.start_shape_filter("green", "cylinder", 0.03)
pos = HAL.get_object_position("green_cylinder")
HAL.stop_shape_filter("green", "cylinder")
HAL.stop_color_filter("green")
if pos is not None:
# 4) Approach, grasp and lift
tcp_ypr = [180, 0, -90]
HAL.MoveLinear([pos[0], pos[1], pos[2] + 0.10], tcp_ypr, 0.2, 0.5)
HAL.MoveLinear([pos[0], pos[1], pos[2]], tcp_ypr, 0.1, 0.0)
HAL.GripperSet(45, 0.3) # close (auto-attach)
HAL.MoveRelLinear([0, 0, 0.10], 0.2, 0.3)
# 5) Place at a target
target = HAL.get_target_position("target6")
HAL.MoveLinear([target.x, target.y, target.z + 0.10], tcp_ypr, 0.2, 0.0)
HAL.MoveLinear([target.x, target.y, target.z], tcp_ypr, 0.1, 0.0)
HAL.GripperSet(0, 0.3) # open (auto-detach)
HAL.MoveRelLinear([0, 0, 0.10], 0.2, 0.2)
If planning fails (Fail: ABORTED: No motion plan found. No execution attempted.), the pose is likely unreachable. Adjust the Z-clearance or the orientation and retry.
How to write buildmap()
Move the robot through several viewpoints so the wrist camera can observe the surroundings. The point cloud is fused into an octomap that MoveIt 2 uses as collision geometry.
How to use get_object_position()
- Pick a colour, a shape and an approximate radius.
- Start the colour filter and tune the RGB limits; verify with the GUI image feeds.
- Start the shape filter and verify with the debug topics; adjust the radius if needed.
- Read the position with
HAL.get_object_position(object_name)and approach with a safe Z-clearance. - Stop the filters when done to avoid noisy results.
How to check filter results
Two image panes are provided in the GUI. Select the topics to view the colour-filtered and shape-filtered images (like the ones shown in the Theory section). For 3D inspection, open RViz and switch the PointCloud topic to the filter output. If nothing is detected, the image remains black or the cloud does not update.
Limitations of the gripper simulation
Grasp simulation can be imperfect and objects may slip. Consider a run successful if the map is built and the desired object is picked reliably.
Ignorable ERROR and WARNING messages
No p gain specified for pid.- Other transient TF/initialization warnings at startup (safe to ignore if they stop repeating once the system settles).
Object and target lists
Object list (four spheres and four cylinders, in red, green, blue and purple):
red_sphere,green_sphere,blue_sphere,purple_spherered_cylinder,green_cylinder,blue_cylinder,purple_cylinder
Target list: target1 .. target16.
Videos
Contributors
- Contributors: Diego Martín, José María Cañas and Javier Izquierdo.
References
- IFRA-Cranfield (2023). ROS 2 Sim-to-Real Robot Control. https://github.com/IFRA-Cranfield/ros2_SimRealRobotControl
- https://moveit.ros.org/
- https://pointclouds.org/