diff --git a/CMakeLists.txt b/CMakeLists.txt
index b8d4c461a938bb420398e9cd5fd304643287774a..865504715433f0b8567ff652b77d4b5652f54bed 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -156,7 +156,7 @@ if(pigpiod)
     ## Declare a C++ executable
     ## With catkin_make all packages are built within a single CMake context
     ## The recommended prefix ensures that target names across packages don't collide
-    add_executable(${PROJECT_NAME}_rpi_node src/main_rpi.cpp src/ros_node.cpp src/rpi_driver.cpp src/driver.cpp)
+    add_executable(${PROJECT_NAME}_rpi_node src/main_rpi.cpp src/ros_node.cpp src/calibration.cpp src/rpi_driver.cpp src/driver.cpp)
     ## Rename C++ executable without prefix
     ## The above recommended prefix causes long target names, the following renames the
     ## target back to the shorter version for ease of user use
diff --git a/src/calibration.cpp b/src/calibration.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e1be3341dc844b663da2351ca348c2e341c2daee
--- /dev/null
+++ b/src/calibration.cpp
@@ -0,0 +1,59 @@
+#include "calibration.h"
+
+#include <ros/names.h>
+
+// CONSTRUCTORS
+calibration::calibration()
+{
+    // Initialize matrices/vectors.
+    calibration::m_calibration.setIdentity();
+    calibration::m_u.setZero();
+    calibration::m_u(3) = 1.0;
+    calibration::m_c.setZero();
+}
+
+// CONFIGURATION
+void calibration::load(ros::NodeHandle& node_handle, std::string param_name)
+{
+    // Try reading the calibration parameter.
+    std::vector<double> components;
+    if(!node_handle.getParam(param_name, components))
+    {
+        // Param not found, quit.
+        return;
+    }
+
+    // Check validity of parameter.
+    if(components.size() != 16)
+    {
+        ROS_ERROR_STREAM("invalid parameter for 4x4 calibration matrix: " << param_name);
+        return;
+    }
+
+    // Read in row-step order.
+    uint32_t k = 0;
+    for(uint32_t i = 0; i < calibration::m_calibration.rows(); ++i)
+    {
+        for(uint32_t j = 0; j < calibration::m_calibration.cols(); ++j)
+        {
+            calibration::m_calibration(i,j) = components.at(k++);
+        }
+    }
+}
+
+// CALIBRATION
+void calibration::calibrate(double& x, double& y, double& z)
+{
+    // Store point into vector.
+    calibration::m_u(0) = x;
+    calibration::m_u(1) = y;
+    calibration::m_u(2) = z;
+
+    // Calibrate point.
+    calibration::m_c.noalias() = calibration::m_calibration * calibration::m_u;
+
+    // Output calibrated point.
+    x = calibration::m_c(0);
+    y = calibration::m_c(1);
+    z = calibration::m_c(2);
+}
\ No newline at end of file
diff --git a/src/calibration.h b/src/calibration.h
new file mode 100644
index 0000000000000000000000000000000000000000..357e02cddde8611d51ed87c8b528dd429f23e66a
--- /dev/null
+++ b/src/calibration.h
@@ -0,0 +1,43 @@
+/// \file calibration.h
+/// \brief Defines the calibration class.
+#ifndef MPU9250_CALIBRATION_H
+#define MPU9250_CALIBRATION_H
+
+#include <ros/node_handle.h>
+#include <vector>
+#include <eigen3/Eigen/Dense>
+
+/// \brief Performs calibration on 3D points.
+class calibration
+{
+public:
+    // CONSTRUCTORS
+    /// \brief Creates a new calibration instance with a unity calibration matrix.
+    calibration();
+
+    /// \brief Loads a calibration matrix from a ROS parameter.
+    /// \param node_handle The node handle to read the parameter from.
+    /// \param param_name The name of the ROS parameter to read from.
+    /// \note If the parameter doesn't exist or is invalid, the calibration remains unchanged.
+    void load(ros::NodeHandle& node_handle, std::string param_name);
+
+    // CALIBRATION
+    /// \brief Performs an in-place calibration on a 3D point.
+    /// \param x The x component to calibrate.
+    /// \param y The y component to calibrate.
+    /// \param z The z component to calibrate.
+    void calibrate(double& x, double& y, double& z);
+
+private:
+    // CALIBRATION
+    /// \brief Stores the calibration as a homogeneous transformation matrix.
+    Eigen::Matrix4d m_calibration;
+
+    // PREALLOCATIONS
+    /// \brief An uncalibrated point.
+    Eigen::Vector4d m_u;
+    /// \brief A calibrated point.
+    Eigen::Vector4d m_c;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/ros_node.cpp b/src/ros_node.cpp
index 4aa670a9699cffae70f36f38737e64c236e599f3..9b2431e96cbfec24ab07412c21a155d48a77e145 100644
--- a/src/ros_node.cpp
+++ b/src/ros_node.cpp
@@ -29,6 +29,9 @@ ros_node::ros_node(driver *driver, int argc, char **argv)
     int param_accel_fsr = private_node.param<int>("accel_fsr", 0);
     float param_max_data_rate = private_node.param<float>("max_data_rate", 8000.0F);
 
+    // Read calibrations.
+    ros_node::m_calibration_accelerometer.load(private_node, "calibration_accelerometer");
+    ros_node::m_calibration_magnetometer.load(private_node, "calibration_magnetometer");
 
     // Set up publishers.
     ros_node::m_publisher_accelerometer = ros_node::m_node->advertise<sensor_msgs_ext::acceleration>("imu/accelerometer", 1);
@@ -97,6 +100,8 @@ void ros_node::data_callback(driver::data data)
     message_accel.x = static_cast<double>(data.accel_x) * 9.80665;
     message_accel.y = static_cast<double>(data.accel_y) * 9.80665;
     message_accel.z = static_cast<double>(data.accel_z) * 9.80665;
+    // Apply calibration.
+    ros_node::m_calibration_accelerometer.calibrate(message_accel.x, message_accel.y, message_accel.z);
     // Publish message.
     ros_node::m_publisher_accelerometer.publish(message_accel);
 
@@ -106,6 +111,8 @@ void ros_node::data_callback(driver::data data)
     message_gyro.x = static_cast<double>(data.gyro_x) * M_PI / 180.0;
     message_gyro.y = static_cast<double>(data.gyro_y) * M_PI / 180.0;
     message_gyro.z = static_cast<double>(data.gyro_z) * M_PI / 180.0;
+    // Apply calibration.
+    ros_node::m_calibration_gyroscope.calibrate(message_gyro.x, message_gyro.y, message_gyro.z);
     // Publish message.
     ros_node::m_publisher_gyroscope.publish(message_gyro);
 
@@ -118,6 +125,8 @@ void ros_node::data_callback(driver::data data)
         message_mag.x = static_cast<double>(data.magneto_x) * 0.000001;
         message_mag.y = static_cast<double>(data.magneto_y) * 0.000001;
         message_mag.z = static_cast<double>(data.magneto_z) * 0.000001;
+        // Apply calibration.
+        ros_node::m_calibration_magnetometer.calibrate(message_mag.x, message_mag.y, message_mag.z);
         // Publish message.
         ros_node::m_publisher_magnetometer.publish(message_mag);
     }
diff --git a/src/ros_node.h b/src/ros_node.h
index 82cb1fd5fc109c6f7b899f9dc90bc9f8e7be101e..841e90395ac7eca3a5b7cbfa70439feb3b474fca 100644
--- a/src/ros_node.h
+++ b/src/ros_node.h
@@ -4,8 +4,10 @@
 #define ROS_NODE_H
 
 #include "driver.h"
+#include "calibration.h"
 
 #include <ros/ros.h>
+#include <std_srvs/Trigger.h>
 
 /// \brief Implements the driver's ROS node functionality.
 class ros_node
@@ -24,11 +26,23 @@ public:
     void spin();
 
 private:
-    // VARIABLES
+    // COMPONENTS
     /// \brief m_driver The driver instance.
     driver* m_driver;
+
+    // CALIBRATIONS
+    /// \brief The accelerometer's calibration.
+    calibration m_calibration_accelerometer;
+    /// \brief The gyroscope's calibration.
+    calibration m_calibration_gyroscope;
+    /// \brief The magnetometer's calibration.
+    calibration m_calibration_magnetometer;
+
+    // ROS
     /// \brief m_node The node's handle.
     ros::NodeHandle* m_node;
+
+    // PUBLISHERS
     /// \brief Publisher for accelerometer data.
     ros::Publisher m_publisher_accelerometer;
     /// \brief Publisher for gyroscope data.