Profile

Sai Jagadeesh Muralikrishnan

Robotics Engineer | ROS2 Specialist | AI Enthusiast

About Me

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.

Technical Skills

ROS2 Python C++ OpenCV PyTorch Gazebo SLAM Docker Bash Computer Vision Embedded Systems PX4 Autopilot
bash ~/profile
#!/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
                        

ROS2 Tips & Tricks

TurtleBot3 ROS2 Easy Setup

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

Python 3.10 Virtual Environment

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

Automated Terminator with 4 Panes

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.

VOXL2 + ROS2 Setup with PX4

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."

ROS2 Projects

ROS2 Validation Pipelines

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

Image Registration in ROS2

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

Tutorials

ROS2 Camera Calibration with OpenCV

Step-by-step guide to calibrating cameras in ROS2 using OpenCV's checkerboard method.

1. Create a new ROS2 package

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python camera_calibration
cd camera_calibration

2. Install dependencies

sudo apt install ros-${ROS_DISTRO}-cv-bridge ros-${ROS_DISTRO}-image-transport
pip install opencv-python numpy

3. Create calibration node

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()

4. Create launch file

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'
            }]
        )
    ])

5. Install and run

# 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

Gazebo Fortress Simulation with ROS2

Setup Gazebo Fortress with ROS2 for PX4 simulation with cameras and LiDAR.

1. Install Gazebo Fortress

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

2. ROS2 Bridge for Gazebo

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>
      """
    }
  }'

3. Add cameras and sensors

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>

Useful Scripts

Automated Colcon Build Script

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]

ROS2 Bag to Video Converter

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

ROS2 Node Status Monitor

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.

Get In Touch

I'm always open to discussing robotics projects, collaborations, or just chatting about the latest in ROS2 and autonomous systems.