Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p><strong>And here we go!</strong> A <strong>high level overview</strong> of this approach can be described as the sequential execution of the following steps:</p> <ul> <li><a href="https://stackoverflow.com/a/16109992/176769">Load the input image</a>;</li> <li><a href="https://stackoverflow.com/a/8408961/176769">Convert it to grayscale</a>;</li> <li><a href="https://stackoverflow.com/a/9471551/176769">Threshold</a> it to generate a binary image;</li> <li>Use the binary image to <a href="https://stackoverflow.com/a/9334267/176769">find contours</a>;</li> <li><a href="https://stackoverflow.com/questions/19966423/filling-in-a-single-colour-background-in-opencv/19989729#19989729">Fill each area of contours</a> with a different color (so we can extract each letter separately);</li> <li><a href="https://stackoverflow.com/a/20288616/176769">Create a mask for each letter found to isolate them in separate images</a>;</li> <li><a href="https://stackoverflow.com/a/10317919/176769">Crop the images to the smallest possible size</a>;</li> <li>Figure out the center of the image;</li> <li>Figure out the width of the letter's border to identify the exact center of the border;</li> <li>Scan along the border (in a circular fashion) for discontinuity; </li> <li>Figure out an approximate angle for the discontinuity, thus identifying the amount of rotation of the letter.</li> </ul> <p>I don't want to get into too much detail since I'm sharing the source code, so feel free to test and change it in any way you like. Let's start, <em>Winter Is Coming</em>:</p> <pre><code>#include &lt;iostream&gt; #include &lt;vector&gt; #include &lt;cmath&gt; #include &lt;opencv2/highgui/highgui.hpp&gt; #include &lt;opencv2/imgproc/imgproc.hpp&gt; cv::RNG rng(12345); float PI = std::atan(1) * 4; void isolate_object(const cv::Mat&amp; input, cv::Mat&amp; output) { if (input.channels() != 1) { std::cout &lt;&lt; "isolate_object: !!! input must be grayscale" &lt;&lt; std::endl; return; } // Store the set of points in the image before assembling the bounding box std::vector&lt;cv::Point&gt; points; cv::Mat_&lt;uchar&gt;::const_iterator it = input.begin&lt;uchar&gt;(); cv::Mat_&lt;uchar&gt;::const_iterator end = input.end&lt;uchar&gt;(); 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); } </code></pre> <p>For more details on the implementation of <code>isolate_object()</code> please <a href="https://stackoverflow.com/a/10317919/176769">check this thread</a>. <code>cv::RNG</code> is used later on to <a href="http://docs.opencv.org/doc/tutorials/imgproc/shapedescriptors/find_contours/find_contours.html" rel="nofollow noreferrer">fill each contour with a different color</a>, and <code>PI</code>, well... you know <strong>PI</strong>.</p> <pre><code>int main(int argc, char* argv[]) { // Load input (colored, 3-channel, BGR) cv::Mat input = cv::imread("test.jpg"); if (input.empty()) { std::cout &lt;&lt; "!!! Failed imread() #1" &lt;&lt; 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); </code></pre> <p>The <strong>binary</strong> image looks exactly like the input because it only had 2 colors (B&amp;W):</p> <p><img src="https://i.stack.imgur.com/kLosJ.png" width="300" height="200"></p> <pre><code> // Find the contours of the C's in the thresholded image std::vector&lt;std::vector&lt;cv::Point&gt; &gt; contours; cv::findContours(binary, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE); // Fill the contours found with unique colors to isolate them later cv::Mat colored_contours = input.clone(); std::vector&lt;cv::Scalar&gt; fill_colors; for (size_t i = 0; i &lt; contours.size(); i++) { std::vector&lt;cv::Point&gt; cnt = contours[i]; double area = cv::contourArea(cv::Mat(cnt)); //std::cout &lt;&lt; "* Area: " &lt;&lt; area &lt;&lt; std::endl; // Fill each C found with a different color. // If the area is larger than 100k it's probably the white background, so we ignore it. if (area &gt; 10000 &amp;&amp; area &lt; 100000) { cv::Scalar color = cv::Scalar(rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255)); cv::drawContours(colored_contours, contours, i, color, CV_FILLED, 8, std::vector&lt;cv::Vec4i&gt;(), 0, cv::Point()); fill_colors.push_back(color); //cv::imwrite("test_contours.jpg", colored_contours); } } </code></pre> <p>What <strong>colored_contours</strong> looks like:</p> <p><img src="https://i.stack.imgur.com/76Jqm.png" width="300" height="200"></p> <pre><code> // Create a mask for each C found to isolate them from each other for (int i = 0; i &lt; 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); </code></pre> <p>Since this <code>for</code> loop is executed twice, one for each color that was used to fill the contours, I want you to see all images that were generated by this stage. So the following images are the ones that were stored by <strong>single_color_mask</strong> (one for each iteration of the loop):</p> <p><img src="https://i.stack.imgur.com/VPmxs.png" width="300" height="200"> <img src="https://i.stack.imgur.com/YW1nr.png" width="300" height="200"></p> <pre><code> // Crop image to the area of the object cv::Mat cropped; isolate_object(single_color_mask, cropped); //cv::imwrite("test_cropped.jpg", cropped); cv::Mat orig_cropped = cropped.clone(); </code></pre> <p>These are the ones that were stored by <strong>cropped</strong> (by the way, the smaller C looks fat because the image is rescaled by this page to have the same size of the larger C, don't worry):</p> <p><img src="https://i.stack.imgur.com/rvYTM.png" width="200" height="200"> <img src="https://i.stack.imgur.com/p0alB.png" width="200" height="200"></p> <pre><code> // 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); </code></pre> <p>To make it clearer to understand for what <strong>obj_center</strong> is for, I painted a little gray circle for educational purposes on that location:</p> <p><img src="https://i.stack.imgur.com/WUgld.png" width="200" height="200"> <img src="https://i.stack.imgur.com/Qy765.png" width="200" height="200"> </p> <pre><code> // Figure out the exact center location of the border std::vector&lt;cv::Point&gt; border_points; for (int y = 0; y &lt; cropped.cols; y++) { if (cropped.at&lt;uchar&gt;(obj_center.x, y) != 0) border_points.push_back(cv::Point(obj_center.x, y)); if (border_points.size() &gt; 0 &amp;&amp; cropped.at&lt;uchar&gt;(obj_center.x, y) == 0) break; } if (border_points.size() == 0) { std::cout &lt;&lt; "!!! Oops! No border detected." &lt;&lt; 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); </code></pre> <p>The procedure above <em>scans a single vertical line</em> from top/middle of the image to find the borders of the circle to be able to calculate it's width. Again, for education purposes I painted a small gray circle in the middle of the border. This is what <strong>cropped</strong> looks like:</p> <p><img src="https://i.stack.imgur.com/c5IMQ.png" width="200" height="200"> <img src="https://i.stack.imgur.com/ZHz8N.png" width="200" height="200"> </p> <pre><code> // Scan the border of the circle for discontinuities int radius = obj_center.y - border_center.y; if (radius &lt; 0) radius *= -1; std::vector&lt;cv::Point&gt; discontinuity_points; std::vector&lt;int&gt; discontinuity_angles; for (int angle = 0; angle &lt;= 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&lt;uchar&gt;(x, y) &lt; 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 &lt;&lt; "Discontinuity size: " &lt;&lt; discontinuity_points.size() &lt;&lt; std::endl; if (discontinuity_points.size() == 0 &amp;&amp; discontinuity_angles.size() == 0) { std::cout &lt;&lt; "!!! Oops! No discontinuity detected. It's a perfect circle, dang!" &lt;&lt; std::endl; return 0; } </code></pre> <p>Great, so the piece of code above scans along the middle of the circle's border looking for discontinuity. I'm sharing a sample image to illustrate what I mean. Every gray dot on the image represents a pixel that is tested. When the pixel is black it means we found a discontinuity:</p> <p><img src="https://i.stack.imgur.com/a9tSp.jpg" alt="enter image description here"></p> <pre><code> // 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 &lt;&lt; "#" &lt;&lt; i &lt;&lt; " letter C is rotated approximately at: " &lt;&lt; approx_angle &lt;&lt; " degrees" &lt;&lt; std::endl; // Figure out the central point of the discontinuity cv::Point discontinuity_center; for (int a = 0; a &lt; 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; } </code></pre> <p>Very well... This last piece of code is responsible for figuring out the approximate angle of the discontinuity as well as indicate the central point of discontinuity. The following images are stored by <strong>orig_cropped</strong>. Once again I added a gray dot to show the exact positions detected as the center of the gaps:</p> <p><img src="https://i.stack.imgur.com/hthex.png" width="280" height="260"> <img src="https://i.stack.imgur.com/KLtg0.png" width="280" height="260"> </p> <p>When executed, this application prints the following information to the screen:</p> <pre><code>#0 letter C is rotated approximately at: 49 degrees #1 letter C is rotated approximately at: 0 degrees </code></pre> <p>I hope it helps.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload