And here we go! High Level Overview This approach can be described as the sequential execution of the following steps:
I don't want to go into details because I use the source code, so feel free to test and modify it in any way. Let it begin, winter is coming:
#include <iostream> #include <vector> #include <cmath> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> cv::RNG rng(12345); float PI = std::atan(1) * 4; void isolate_object(const cv::Mat& input, cv::Mat& output) { if (input.channels() != 1) { std::cout << "isolate_object: !!! input must be grayscale" << std::endl; return; } // Store the set of points in the image before assembling the bounding box std::vector<cv::Point> points; cv::Mat_<uchar>::const_iterator it = input.begin<uchar>(); cv::Mat_<uchar>::const_iterator end = input.end<uchar>(); for (; it != end; ++it) { if (*it) points.push_back(it.pos()); } // Compute minimal bounding box cv::RotatedRect box = cv::minAreaRect(cv::Mat(points)); // Set Region of Interest to the area defined by the box cv::Rect roi; roi.x = box.center.x - (box.size.width / 2); roi.y = box.center.y - (box.size.height / 2); roi.width = box.size.width; roi.height = box.size.height; // Crop the original image to the defined ROI output = input(roi); }
For more information on the implementation of isolate_object() , please check this thread . cv::RNG used later to fill each path with a different color and PI , well ... you know PI .
int main(int argc, char* argv[]) { // Load input (colored, 3-channel, BGR) cv::Mat input = cv::imread("test.jpg"); if (input.empty()) { std::cout << "!!! Failed imread() #1" << std::endl; return -1; } // Convert colored image to grayscale cv::Mat gray; cv::cvtColor(input, gray, CV_BGR2GRAY); // Execute a threshold operation to get a binary image from the grayscale cv::Mat binary; cv::threshold(gray, binary, 128, 255, cv::THRESH_BINARY);
The binary image looks exactly the same as the input, because it only had 2 colors (B & W):

What color_contours looks like :

// Create a mask for each C found to isolate them from each other for (int i = 0; i < fill_colors.size(); i++) { // After inRange() single_color_mask stores a single C letter cv::Mat single_color_mask = cv::Mat::zeros(input.size(), CV_8UC1); cv::inRange(colored_contours, fill_colors[i], fill_colors[i], single_color_mask); //cv::imwrite("test_mask.jpg", single_color_mask);
Since this for loop runs twice, one for each color that was used to fill the paths, I want you to see all the images created at this point. Thus, the following images are those that were saved by single_color_mask (one for each iteration of the loop):


These are the ones that were kept trimmed (by the way, the smaller C looks thick because the image is rescaled by this page to have the same size as the larger C, no worries):


// Figure out the center of the image cv::Point obj_center(cropped.cols/2, cropped.rows/2); //cv::circle(cropped, obj_center, 3, cv::Scalar(128, 128, 128)); //cv::imwrite("test_cropped_center.jpg", cropped);
To better understand what obj_center is for , I drew a small gray circle for educational purposes in this place:


// Figure out the exact center location of the border std::vector<cv::Point> border_points; for (int y = 0; y < cropped.cols; y++) { if (cropped.at<uchar>(obj_center.x, y) != 0) border_points.push_back(cv::Point(obj_center.x, y)); if (border_points.size() > 0 && cropped.at<uchar>(obj_center.x, y) == 0) break; } if (border_points.size() == 0) { std::cout << "!!! Oops! No border detected." << std::endl; return 0; } // Figure out the exact center location of the border cv::Point border_center = border_points[border_points.size() / 2]; //cv::circle(cropped, border_center, 3, cv::Scalar(128, 128, 128)); //cv::imwrite("test_border_center.jpg", cropped);
The above procedure scans one vertical line at the top / in the middle of the image to find the borders of the circle in order to be able to calculate the width. Again, for educational purposes, I drew a small gray circle in the middle of the border. Here's what the cropped looks like:


// Scan the border of the circle for discontinuities int radius = obj_center.y - border_center.y; if (radius < 0) radius *= -1; std::vector<cv::Point> discontinuity_points; std::vector<int> discontinuity_angles; for (int angle = 0; angle <= 360; angle++) { int x = obj_center.x + (radius * cos((angle+90) * (PI / 180.f))); int y = obj_center.y + (radius * sin((angle+90) * (PI / 180.f))); if (cropped.at<uchar>(x, y) < 128) { discontinuity_points.push_back(cv::Point(y, x)); discontinuity_angles.push_back(angle); //cv::circle(cropped, cv::Point(y, x), 1, cv::Scalar(128, 128, 128)); } } //std::cout << "Discontinuity size: " << discontinuity_points.size() << std::endl; if (discontinuity_points.size() == 0 && discontinuity_angles.size() == 0) { std::cout << "!!! Oops! No discontinuity detected. It a perfect circle, dang!" << std::endl; return 0; }
Great, so the code above scans the middle of the circle border, looking for a gap. I am sharing a sample to illustrate what I mean. Each gray dot in the image represents a pixel that is being tested. When the pixel is black, it means that we have detected a gap:

// Figure out the approximate angle of the discontinuity: // the first angle found will suffice for this demo. int approx_angle = discontinuity_angles[0]; std::cout << "#" << i << " letter C is rotated approximately at: " << approx_angle << " degrees" << std::endl; // Figure out the central point of the discontinuity cv::Point discontinuity_center; for (int a = 0; a < discontinuity_points.size(); a++) discontinuity_center += discontinuity_points[a]; discontinuity_center.x /= discontinuity_points.size(); discontinuity_center.y /= discontinuity_points.size(); cv::circle(orig_cropped, discontinuity_center, 2, cv::Scalar(128, 128, 128)); cv::imshow("Original crop", orig_cropped); cv::waitKey(0); } return 0; }
Very good ... This last piece of code is responsible for determining the approximate angle of the gap, and also points to the center point of the gap. The following images are stored in orig_cropped . Once again, I added a gray dot to show the exact positions found as the center of the breaks:


When executed, this application displays the following information:
#0 letter C is rotated approximately at: 49 degrees #1 letter C is rotated approximately at: 0 degrees
Hope this helps.