Robotics Engineer | ROS2 Specialist | AI Enthusiast
Masters in Robotics graduate with extensive experience in ROS2, computer vision, and autonomous systems. Passionate about developing robust robotics solutions and sharing knowledge with the community.
Currently working on cutting-edge robotics projects involving ROS2, sensor fusion, and machine learning for autonomous navigation and manipulation.
#!/bin/bash
# Name: Sai Jagadeesh Muralikrishnan
# Location: College Park, MD
# Education:
# - M.Eng in Robotics @ University of Maryland (2023-2025)
# - B.Eng in Mechatronics @ REC Chennai (2018-2022)
# Experience:
# - Robotics Intern @ Kick Robotics
# - Embedded Engineer @ TuTr Hyperloop
# - Research Assistant @ Maryland Robotics Center
# Skills:
# - Robotics: ROS2, SLAM, Sensor Fusion
# - Programming: Python, C++, CUDA
# - ML/CV: PyTorch, OpenCV, YOLOv8
# - Simulation: Gazebo, Isaac Sim
# Contact:
# - Email: saijagadeesh.muralikrishnan@gmail.com
# - LinkedIn: linkedin.com/in/sai-jagadeesh-m
# - Website: saijagadeesh.com
Quick setup guide for TurtleBot3 with ROS2 Humble on Ubuntu 22.04:
# Install TurtleBot3 packages sudo apt install ros-humble-turtlebot3-* # Set TurtleBot3 model (burger, waffle, waffle_pi) echo "export TURTLEBOT3_MODEL=waffle_pi" >> ~/.bashrc source ~/.bashrc # Launch simulation ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py # Launch teleop ros2 run turtlebot3_teleop teleop_keyboard
Create and activate a Python 3.10 virtual environment for ROS2 development:
# Install Python 3.10 if not present sudo apt install python3.10 python3.10-venv # Create virtual environment python3.10 -m venv ~/ros2_venv # Activate environment source ~/ros2_venv/bin/activate # Install ROS2 packages (after sourcing ROS2) pip install -U colcon-common-extensions # Install other dependencies pip install numpy opencv-python pyyaml
Add this to your ~/.bashrc to open Terminator with 4 panes automatically:
# Function to launch terminator with 4 panes
quadterminator() {
terminator --layout ros2quad &
sleep 0.5
xdotool key Super+Up
}
# Create the layout file if it doesn't exist
if [ ! -f ~/.config/terminator/config ]; then
mkdir -p ~/.config/terminator
cat > ~/.config/terminator/config << 'EOL'
[global_config]
title_transmit_fg_color = "#2e3436"
title_transmit_bg_color = "#3465a4"
title_inactive_fg_color = "#ffffff"
title_inactive_bg_color = "#555753"
[keybindings]
[profiles]
[[default]]
cursor_color = "#aaa"
foreground_color = "#ffffff"
background_color = "#000000"
palette = "#2e3436:#cc0000:#4e9a06:#c4a000:#3465a4:#75507b:#06989a:#d3d7cf:#555753:#ef2929:#8ae234:#fce94f:#729fcf:#ad7fa8:#34e2e2:#eeeeec"
[layouts]
[[ros2quad]]
[[[child0]]]
type = Window
parent = ""
[[[child1]]]
type = HPaned
parent = child0
[[[child2]]]
type = VPaned
parent = child1
[[[child3]]]
type = VPaned
parent = child1
position = 50
[[[terminal4]]]
type = Terminal
parent = child2
profile = default
command = "echo 'TOP LEFT - ROS COMMANDS'"
[[[terminal5]]]
type = Terminal
parent = child2
profile = default
command = "echo 'BOTTOM LEFT - BUILD/LOGS'"
[[[terminal6]]]
type = Terminal
parent = child3
profile = default
command = "echo 'TOP RIGHT - EDITOR'"
[[[terminal7]]]
type = Terminal
parent = child3
profile = default
command = "echo 'BOTTOM RIGHT - MONITORING'"
[plugins]
EOL
fi
Run quadterminator in your terminal to launch the pre-configured layout.
Setup script for integrating VOXL2 with ROS2 and PX4 Autopilot:
#!/bin/bash
# Install required dependencies
sudo apt-get update
sudo apt-get install -y ros-humble-mavros ros-humble-mavros-extras \
ros-humble-mavros-msgs ros-humble-geographic-msgs \
ros-humble-tf2-eigen ros-humble-tf2-ros
# Download PX4 messages
git clone https://github.com/PX4/px4_msgs.git ~/px4_ros2/src/px4_msgs
# Build the workspace
cd ~/px4_ros2
colcon build --symlink-install
# Configure VOXL2 for ROS2
sudo echo "export MAVLINK_DIALECT=common" >> /etc/profile.d/voxl.sh
sudo echo "export ROS_DOMAIN_ID=42" >> /etc/profile.d/voxl.sh
sudo echo "source /opt/ros/humble/setup.bash" >> /etc/profile.d/voxl.sh
sudo echo "source ~/px4_ros2/install/setup.bash" >> /etc/profile.d/voxl.sh
# Setup PX4 communication
cat > ~/px4_ros2/src/run_px4.sh << 'EOL'
#!/bin/bash
micrortps_agent -t UDP -n voxl2 &
ros2 run px4_ros_com mavlink_router -e px4_sender -t UDP -d 14550 -r 14540 &
EOL
chmod +x ~/px4_ros2/src/run_px4.sh
echo "VOXL2 ROS2 setup complete. Reboot to apply changes."
Built ROS2 validation pipelines in Python/C++ using PyTest and GoogleTest covering motion, sensor integrity, and firmware stability tests.
# Example test for motion validation
import pytest
import rclpy
from std_msgs.msg import Float32
class TestMotionNodes:
@classmethod
def setup_class(cls):
rclpy.init()
cls.node = rclpy.create_node('test_motion')
@classmethod
def teardown_class(cls):
cls.node.destroy_node()
rclpy.shutdown()
def test_velocity_publisher(self):
from example_interfaces.msg import Float32MultiArray
pub = self.node.create_publisher(Float32MultiArray, '/cmd_vel', 10)
# Test publishing
msg = Float32MultiArray(data=[0.1, 0.0]) # Linear x, angular z
pub.publish(msg)
# Give some time for message to be processed
rclpy.spin_once(self.node, timeout_sec=0.1)
# Add your assertions here
assert True # Replace with actual assertions
Implemented multi-view image registration in ROS2 using OpenCV with feature matching and homography estimation.
import cv2
import numpy as np
import rclpy
from cv_bridge import CvBridge
from sensor_msgs.msg import Image
class ImageRegistrationNode:
def __init__(self):
self.node = rclpy.create_node('image_registration')
self.bridge = CvBridge()
# Subscribers
self.image1_sub = self.node.create_subscription(
Image, '/camera1/image_raw', self.image1_callback, 10)
self.image2_sub = self.node.create_subscription(
Image, '/camera2/image_raw', self.image2_callback, 10)
# Feature detector
self.orb = cv2.ORB_create()
self.bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
self.last_image1 = None
self.last_image2 = None
def image1_callback(self, msg):
self.last_image1 = self.bridge.imgmsg_to_cv2(msg, "bgr8")
self.process_images()
def image2_callback(self, msg):
self.last_image2 = self.bridge.imgmsg_to_cv2(msg, "bgr8")
self.process_images()
def process_images(self):
if self.last_image1 is None or self.last_image2 is None:
return
# Find keypoints and descriptors
kp1, des1 = self.orb.detectAndCompute(self.last_image1, None)
kp2, des2 = self.orb.detectAndCompute(self.last_image2, None)
if des1 is None or des2 is None:
return
# Match features
matches = self.bf.match(des1, des2)
matches = sorted(matches, key=lambda x: x.distance)
# Extract matched points
src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1,1,2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1,1,2)
# Find homography
h_matrix, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
# You can now use h_matrix to transform images or points between views
Step-by-step guide to calibrating cameras in ROS2 using OpenCV's checkerboard method.
mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src ros2 pkg create --build-type ament_python camera_calibration cd camera_calibration
sudo apt install ros-${ROS_DISTRO}-cv-bridge ros-${ROS_DISTRO}-image-transport
pip install opencv-python numpy
Create a file camera_calibration/calibration_node.py:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
import numpy as np
import os
from datetime import datetime
class CameraCalibrationNode(Node):
def __init__(self):
super().__init__('camera_calibration_node')
# Parameters
self.declare_parameter('chessboard_width', 9)
self.declare_parameter('chessboard_height', 6)
self.declare_parameter('square_size', 0.024) # meters
self.declare_parameter('image_topic', '/camera/image_raw')
# Initialize variables
self.chessboard_size = (
self.get_parameter('chessboard_width').value,
self.get_parameter('chessboard_height').value)
self.square_size = self.get_parameter('square_size').value
self.bridge = CvBridge()
self.image_sub = self.create_subscription(
Image,
self.get_parameter('image_topic').value,
self.image_callback,
10)
# Calibration variables
self.criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
self.objp = np.zeros((self.chessboard_size[0]*self.chessboard_size[1],3), np.float32)
self.objp[:,:2] = np.mgrid[0:self.chessboard_size[0],0:self.chessboard_size[1]].T.reshape(-1,2) * self.square_size
self.objpoints = [] # 3D points in real world space
self.imgpoints = [] # 2D points in image plane
self.calibration_done = False
def image_callback(self, msg):
if self.calibration_done:
return
try:
cv_image = self.bridge.imgmsg_to_cv2(msg, 'bgr8')
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
# Find chessboard corners
ret, corners = cv2.findChessboardCorners(
gray, self.chessboard_size,
cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE)
if ret:
self.get_logger().info('Chessboard detected!')
# Refine corner locations
corners2 = cv2.cornerSubPix(
gray, corners, (11,11), (-1,-1), self.criteria)
# Draw and display the corners
cv2.drawChessboardCorners(cv_image, self.chessboard_size, corners2, ret)
cv2.imshow('Calibration', cv_image)
cv2.waitKey(500) # Display for 500ms
# Add to calibration data
self.objpoints.append(self.objp)
self.imgpoints.append(corners2)
if len(self.objpoints) >= 15: # Use at least 15 images
self.calibrate_camera(gray.shape[::-1])
except Exception as e:
self.get_logger().error(f'Error processing image: {str(e)}')
def calibrate_camera(self, image_size):
self.get_logger().info('Starting calibration...')
try:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
self.objpoints, self.imgpoints, image_size, None, None)
if ret:
self.get_logger().info('Calibration successful!')
self.save_calibration(mtx, dist)
self.calibration_done = True
except Exception as e:
self.get_logger().error(f'Calibration failed: {str(e)}')
def save_calibration(self, camera_matrix, distortion_coeffs):
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'camera_calibration_{timestamp}.npz'
np.savez(filename,
camera_matrix=camera_matrix,
distortion_coeffs=distortion_coeffs)
self.get_logger().info(f'Saved calibration to {filename}')
def main(args=None):
rclpy.init(args=args)
node = CameraCalibrationNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Create launch/calibration.launch.py:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='camera_calibration',
executable='calibration_node',
name='camera_calibration',
parameters=[{
'chessboard_width': 9,
'chessboard_height': 6,
'square_size': 0.024,
'image_topic': '/camera/image_raw'
}]
)
])
# Add this to setup.py if not present
entry_points={
'console_scripts': [
'calibration_node = camera_calibration.calibration_node:main',
],
},
# Build and run
cd ~/ros2_ws
colcon build --packages-select camera_calibration
source install/setup.bash
ros2 launch camera_calibration calibration.launch.py
Setup Gazebo Fortress with ROS2 for PX4 simulation with cameras and LiDAR.
sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list' wget https://packages.osrfoundation.org/gazebo.key -O - | sudo apt-key add - sudo apt-get update sudo apt-get install gazebo-fortress gazebo-fortress-common
sudo apt install ros-${ROS_DISTRO}-ros-gz-sim ros-${ROS_DISTRO}-ros-gz-bridge
# Launch Gazebo with ROS2 bridge
ros2 launch ros_gz_sim gz_sim.launch.py gz_args:="-v fortress -r empty.sdf"
# In another terminal, spawn a model
ros2 service call /world/empty/create ros_gz_interfaces/srv/SpawnEntity \
'{
entity_factory: {
name: "drone",
allow_renaming: false,
pose: {
position: {x: 0.0, y: 0.0, z: 1.0},
orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}
},
sdf: """
<sdf version="1.7">
<model name="drone">
<include>
<uri>model://iris</uri>
</include>
<plugin filename="libignition-gazebo-multicopter-motor-model-system.so" name="ignition::gazebo::systems::MulticopterMotorModel">
<robotNamespace>iris</robotNamespace>
<jointName>iris::rotor_0_joint</jointName>
<linkName>iris::rotor_0</linkName>
<turningDirection>ccw</turningDirection>
<timeConstantUp>0.0125</timeConstantUp>
<timeConstantDown>0.025</timeConstantDown>
<maxRotVelocity>1000</maxRotVelocity>
<motorConstant>8.54858e-06</motorConstant>
<momentConstant>0.06</momentConstant>
<commandSubTopic>command/motor_speed</commandSubTopic>
<motorNumber>0</motorNumber>
<rotorDragCoefficient>8.06428e-05</rotorDragCoefficient>
<rollingMomentCoefficient>1e-06</rollingMomentCoefficient>
<motorSpeedPubTopic>motor_speed/0</motorSpeedPubTopic>
<rotorVelocitySlowdownSim>10</rotorVelocitySlowdownSim>
</plugin>
</model>
</sdf>
"""
}
}'
Modify the SDF to include sensors:
# In the model SDF, add this before closing the model tag
<link name="camera_link">
<pose>0.1 0 0.1 0 1.57 0</pose>
<visual name="camera_visual">
<geometry>
<box>
<size>0.05 0.05 0.05</size>
</box>
</geometry>
</visual>
<sensor name="camera" type="camera">
<camera>
<horizontal_fov>1.047</horizontal_fov>
<image>
<width>640</width>
<height>480</height>
</image>
<clip>
<near>0.1</near>
<far>100</far>
</clip>
</camera>
<always_on>1</always_on>
<update_rate>30</update_rate>
<visualize>true</visualize>
<topic>camera</topic>
<plugin filename="libignition-gazebo-ros-camera-system.so" name="ignition::gazebo::systems::Camera">
<ros>
<namespace>/model/iris</namespace>
</ros>
<camera_name>front_camera</camera_name>
<frame_name>camera_link</frame_name>
</plugin>
</sensor>
</link>
This script automatically builds your ROS2 workspace with different build types for development and release.
#!/bin/bash
# Exit on error
set -e
# Define build type (dev or release)
if [ "$1" == "--release" ]; then
BUILD_TYPE="Release"
BUILD_FLAGS="--cmake-args -DCMAKE_BUILD_TYPE=Release -DNDEBUG=ON"
echo "Building in Release mode"
else
BUILD_TYPE="Debug"
BUILD_FLAGS="--cmake-args -DCMAKE_BUILD_TYPE=Debug"
echo "Building in Debug mode"
fi
# Clear previous build
if [ "$2" == "--clean" ]; then
echo "Cleaning previous build..."
rm -rf build install log
fi
# Source ROS setup
if [ -z "$ROS_DISTRO" ]; then
echo "ROS environment not set, sourcing..."
if [ -f "/opt/ros/humble/setup.bash" ]; then
source /opt/ros/humble/setup.bash
else
echo "ERROR: ROS setup file not found. Update the path or install ROS."
exit 1
fi
fi
# Find the package directory
PKG_DIR=$(pwd)
while [ "$PKG_DIR" != "/" ] && [ ! -f "$PKG_DIR/COLCON_IGNORE" ] && [ ! -f "$PKG_DIR/package.xml" ]; do
PKG_DIR=$(dirname "$PKG_DIR")
done
if [ -f "$PKG_DIR/package.xml" ]; then
echo "Building ${PKG_DIR##*/} package in $BUILD_TYPE mode"
cd "$PKG_DIR/../.." || exit # Move to workspace root
else
echo "No package.xml found in any parent directory."
echo "Please run this script from within your ROS2 package directory."
exit 1
fi
# Build commands
colcon build \
$BUILD_FLAGS \
--symlink-install \
--event-handlers console_direct+ \
--packages-select "${PKG_DIR##*/}" \
--parallel-workers $(nproc)
echo "Build complete. Source the workspace with:"
echo "source ./install/setup.bash"
Usage: ./build_ros2.sh [--release] [--clean]
Convert ROS2 bag files containing image topics to mp4 videos with timestamps.
#!/bin/bash
if [ "$#" -ne 4 ]; then
echo "Usage: $0 <input_bag> <image_topic> <output_video> <fps>"
exit 1
fi
INPUT_BAG=$1
IMAGE_TOPIC=$2
OUTPUT_VIDEO=$3
FPS=$4
# Create temp directory
TEMP_DIR=$(mktemp -d)
echo "Created temp directory: $TEMP_DIR"
# Extract images from bag file
echo "Extracting images from bag file..."
ros2 run image_view extract_images _sec_per_frame:=0 _filename_format:="$TEMP_DIR/frame_%04d.jpg" __name:=extract_images &
EXTRACT_PID=$!
ros2 bag play "$INPUT_BAG" --rate 1.0 --topics "$IMAGE_TOPIC" --remap "$IMAGE_TOPIC:=/extract_images/image"
kill $EXTRACT_PID
# Convert images to video
echo "Converting images to video..."
ffmpeg -y -r "$FPS" -i "$TEMP_DIR/frame_%04d.jpg" -c:v libx264 -vf "fps=$FPS,format=yuv420p" "$OUTPUT_VIDEO"
# Clean up
rm -r "$TEMP_DIR"
echo "Done. Output video saved to $OUTPUT_VIDEO"
Usage: ./bag_to_video.sh input.bag /camera/image_raw output.mp4 30
A simple Bash script to monitor the status of ROS2 nodes with color coding.
#!/bin/bash
# ROS2 Node Status Monitor
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Check ROS environment
if [ -z "$ROS_DISTRO" ]; then
echo -e "${RED}ERROR: ROS2 environment not sourced!${NC}"
exit 1
fi
# Main loop
while true; do
clear
echo -e "${BLUE}ROS2 Node Status Monitor - $(date)${NC}\n"
# Get node list
NODE_LIST=$(ros2 node list 2>/dev/null)
if [ -z "$NODE_LIST" ]; then
echo -e "${YELLOW}No active ROS2 nodes detected.${NC}"
else
echo -e "${GREEN}Active ROS2 Nodes:${NC}"
while read -r node; do
# Check node info
NODE_INFO=$(ros2 node info "$node" 2>/dev/null)
if [ $? -eq 0 ]; then
# Count publishers and subscribers
PUB_COUNT=$(echo "$NODE_INFO" | grep -c "Publishers:")
SUB_COUNT=$(echo "$NODE_INFO" | grep -c "Subscribers:")
SRV_COUNT=$(echo "$NODE_INFO" | grep -c "Service Servers:")
CLI_COUNT=$(echo "$NODE_INFO" | grep -c "Service Clients:")
# Get node namespace
NS=$(echo "$node" | awk -F '/' '{OFS="/"; $NF=""; print $0}')
NODE_NAME=$(echo "$node" | awk -F '/' '{print $NF}')
# Print node info
echo -e "\n${BLUE}Node: ${NC}${NODE_NAME} ${YELLOW}(ns: ${NS:-/})${NC}"
echo -e " ${GREEN}Publishers:${NC} $PUB_COUNT ${GREEN}Subscribers:${NC} $SUB_COUNT"
echo -e " ${GREEN}Services:${NC} $SRV_COUNT ${GREEN}Clients:${NC} $CLI_COUNT"
else
echo -e "\n${RED}Error getting info for node: $node${NC}"
fi
done <<< "$NODE_LIST"
fi
# Check for parameter server
PARAM_LIST=$(ros2 param list 2>/dev/null)
if [ ! -z "$PARAM_LIST" ]; then
echo -e "\n${BLUE}Global Parameters:${NC}"
echo "$PARAM_LIST" | while read -r param; do
echo -e " - $param"
done
fi
echo -e "\n${YELLOW}Press Ctrl+C to exit...${NC}"
sleep 2
done
Run this script to continuously monitor your ROS2 system status.
I'm always open to discussing robotics projects, collaborations, or just chatting about the latest in ROS2 and autonomous systems.