Correctly rotating an image to an arbitrary angle in OpenCV

Correctly rotating an image to an arbitrary angle in OpenCV

Introduction

Have you ever tried to rotate an image in OpenCV? It's damn simple, right?

  1. You include the OpenCV headers:
    #include <cmath>
    #include <iostream>
    #include <algorithm>
    #include <opencv2/core.hpp>
    #include <opencv2/imgproc.hpp>
    #include <opencv2/highgui.hpp>
    
  2. You load the image:
    cv::Mat image = cv::imread("sria91.png");
    
    Here's how my image looks: input.png
  3. You call cv::rotate. For example to rotate an image clockwise by 90 degrees:
    cv::Mat rotated_image = cv::rotate(image, cv::ROTATE_90_CLOCKWISE);
    
  4. And then you can display the image:
    cv::imshow("Output", output_image);
    cv::waitKey(10000);
    
    Here's how the output_image looks: cv_rotate_-90.png

But what if you wanted to rotate the image by an arbitrary angle say 23 degrees anti-clockwise? How would you go about that?

Naive approach

If you look at the OpenCV's official C++ tutorials you would be out of luck. But, if you dig deeper you might come across OpenCV's official Python tutorials and find this. Adapting the example on that page for rotation to C++ you could write the following:

cv::Mat input_image = cv::imread("sria91.png");
int rows = input_image.rows;
int cols = input_image.cols;
cv::Mat transformation_matrix = cv::getRotationMatrix2D(cv::Point2f((cols-1)/2.0, (rows-1)/2.0), 23, 1);
cv::Mat output_image;
cv::warpAffine(input_image, output_image, transformation_matrix, cv::Size(cols, rows));
cv::imshow("Output", output_image);
cv::waitKey(10000);

The result looks like this: cv_warpAffine_rotate_23.png

You would notice that something is not right. The output image is cropped from all around. How would you get an image that is rotated and also not cropped? I've got you covered. Follow along to find out.

The correct approach

This approach is based on the first principles.

  1. The first thing you have to do is to construct the rotation matrix.
    double angle = 23;  // in degrees
    double angle_in_radians = angle * M_PI / 180;
    cv::Mat rotation_matrix = cv::Mat::eye(3, 3, CV_64F);
    rotation_matrix.at<double>(0, 0) = cos(angle_in_radians);
    rotation_matrix.at<double>(0, 1) = -sin(angle_in_radians);
    rotation_matrix.at<double>(1, 0) = sin(angle_in_radians);
    rotation_matrix.at<double>(1, 1) = cos(angle_in_radians);
    
  2. Then you construct the translation matrix.
    cv::Mat translation_matrix = cv::Mat::eye(3, 3, CV_64F);
    translation_matrix.at<double>(0, 2) = - rows / 2;
    translation_matrix.at<double>(1, 2) = - cols / 2;
    
  3. Collect the coordinates for corners of the image.
    cv::Mat corners = cv::Mat::ones(3, 4, CV_64F);
    corners.at<double>(0, 0) = 0;
    corners.at<double>(1, 0) = 0;
    corners.at<double>(0, 1) = 0;
    corners.at<double>(1, 1) = cols - 1;
    corners.at<double>(0, 2) = rows - 1;
    corners.at<double>(1, 2) = cols - 1;
    corners.at<double>(0, 3) = rows - 1;
    corners.at<double>(1, 3) = 0;
    
  4. Find the coordinates of the corners of the image when its center is translated to the origin and rotated by angle.
    cv::Mat new_corners = (rotation_matrix * translation_matrix) * corners;
    
  5. Calculate the rows and columns for the output image.
    double *p_new_corners;
    p_new_corners = new_corners.row(0).ptr<double>();
    int max_rows = *max_element(p_new_corners, p_new_corners+4);
    int min_rows = *min_element(p_new_corners, p_new_corners+4);
    p_new_corners = new_corners.row(1).ptr<double>();
    int max_cols = *max_element(p_new_corners, p_new_corners+4);
    int min_cols = *min_element(p_new_corners, p_new_corners+4);
    int new_rows = max_rows - min_rows + 1;
    int new_cols = max_cols - min_cols + 1;
    
  6. Construct the new_translation_matrix to translate the image's center from the origin.
    cv::Mat new_translation_matrix = cv::Mat::eye(3, 3, CV_64F);
    new_translation_matrix.at<double>(0, 2) = new_rows / 2;
    new_translation_matrix.at<double>(1, 2) = new_cols / 2;
    
  7. Compute the inverse_transformation_matrix to map the coordinates from output_image to input_image.
    cv::Mat inverse_transformation_matrix = (new_translation_matrix * rotation_matrix * translation_matrix).inv();
    
  8. Now you allocate the memory for the output_image.
    cv::Mat output_image(new_rows, new_cols, input_image.type());
    
  9. Finally rotate and display the image.
    cv::Mat new_point = cv::Mat::ones(3, 1, CV_64F);
    for (int new_row=0; new_row<new_rows; new_row++) {
     for (int new_col=0; new_col<new_cols; new_col++) {
         new_point.at<double>(0, 0) = new_row;
         new_point.at<double>(1, 0) = new_col;
         cv::Mat point = inverse_transformation_matrix * new_point;
         int row = point.at<double>(0, 0);
         int col = point.at<double>(1, 0);
         if (row >= 0 && row < rows && col >= 0 && col < cols) {
             output_image.at<cv::Vec3b>(cv::Point(new_col, new_row)) = input_image.at<cv::Vec3b>(cv::Point(col, row));
         }
     }
    }
    cv::imshow("Output", input_image);
    cv::waitKey(10000);
    
    And the result looks like this: correct_rotate_23.png

Link to the code

Try online in a new Codespace

Did you find this article valuable?

Support Srikanth Anantharam's Blog by becoming a sponsor. Any amount is appreciated!