To know which reference shapes compose your images, you can
- localize the central dot which is present in all your shapes
- knowing where the dot is, find the correct shape.
For the scope of this answer I use these images which are already preprocessed. The first image is simply thresholded, for the second I used this snippet.
Find the central dots is pretty easy on the preprocessed images. You can use cv::connectedComponentsWithStats
to retrieve all black components, and then remove the ones that are too big. You can find the code in the function getCenterPoints
below.
Then you can easily get the outlines (needed later) with a simple combination of this image and the original one:
Now we are able to find the dots, but we need also a way to say which shape compose the final image.
We can use the geometry of the shape to build a simple descriptor for each shape: we save in a Mat
4 values representing the distance of the center from the outline in vertical and horizontal direction:
This uniquely identifies all your reference shapes.
Then we normalize this 4 element vector so it becomes scale-invariant. Using this descriptor allow us to avoid tedious "multiscale template matching" like stuff, and is also much faster and extendible. You can find the code for this in the function computeShapeDescriptor
below.
In order to compute the shape descriptor we need also the correct position of the shape center, which is simply the centroid of the blob we found earlier. We basically use again cv::connectedComponentWithStats
. See getCentroids
below.
Now we know how to find the dots to localize all shapes, and know how to describe them. To find the corresponding reference shape in the image simply compare the descriptors. The one most similar would be the correct one!
Full code for reference:
#include <opencv2opencv.hpp>
#include <vector>
void computeShapeDescriptor(const cv::Mat1b shape_outline, cv::Point center, cv::Mat1d& desc)
{
desc = cv::Mat1d(1, 4, 0.0);
// Go up until I find a outline pixel
for (int i = center.y; i >= 0; --i) {
if (shape_outline(i, center.x) > 0) {
desc(0) = std::abs(i - center.y);
break;
}
}
// Go right until I find a outline pixel
for (int i = center.x; i < shape_outline.cols; ++i) {
if (shape_outline(center.y, i) > 0) {
desc(1) = std::abs(i - center.x);
break;
}
}
// Go down until I find a outline pixel
for (int i = center.y; i < shape_outline.rows; ++i) {
if (shape_outline(i, center.x) > 0) {
desc(2) = std::abs(i - center.y);
break;
}
}
// Go left until I find a outline pixel
for (int i = center.x; i >= 0; --i) {
if (shape_outline(center.y, i) > 0) {
desc(3) = std::abs(i - center.x);
break;
}
}
desc /= cv::norm(desc, cv::NORM_L1);
}
void getCenterPoints(const cv::Mat1b& src, cv::Mat1b& dst)
{
dst = cv::Mat1b(src.rows, src.cols, uchar(0));
cv::Mat1i labels;
cv::Mat1i stats;
cv::Mat1d centroids;
int n_labels = cv::connectedComponentsWithStats(~src, labels, stats, centroids);
for (int i = 1; i < n_labels; ++i) {
if (stats(i, cv::CC_STAT_AREA) < 100)
{
dst.setTo(255, labels == i);
}
}
}
void getCentroids(const cv::Mat1b& src, cv::Mat1d& centroids)
{
// Find the central pixel
cv::Mat1i labels;
cv::Mat1i stats;
cv::connectedComponentsWithStats(src, labels, stats, centroids);
// 'centroids' contains in each row x,y coordinates of the centroid
}
int main()
{
// Load the reference shapes
cv::Mat1b reference = cv::imread("path_to_reference_shapes", cv::IMREAD_GRAYSCALE);
// -------------------------
// Compute descriptor for each reference shape
// -------------------------
// Get the centers
cv::Mat1b reference_centers;
getCenterPoints(reference, reference_centers);
// Get the centroids
cv::Mat1d shape_centroids;
getCentroids(reference_centers, shape_centroids);
// Find the outline
cv::Mat1b reference_outline = ~(reference | reference_centers);
// Prepare output image
cv::Mat3b reference_output;
cv::cvtColor(reference, reference_output, cv::COLOR_GRAY2BGR);
// Compute the descriptor for each shape
std::vector<cv::Mat1f> shape_descriptors;
for (int i = 1; i < shape_centroids.rows; ++i)
{
cv::Point center;
center.x = std::round(shape_centroids(i, 0));
center.y = std::round(shape_centroids(i, 1));
cv::Mat1d desc;
computeShapeDescriptor(reference_outline, center, desc);
shape_descriptors.push_back(desc.clone());
// Draw the ID of the shape
cv::putText(reference_output, cv::String(std::to_string(i)), center, cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255));
}
// -------------------------
// Find shapes in image
// -------------------------
cv::Mat1b img = cv::imread("path_to_image", cv::IMREAD_GRAYSCALE);
// Get the centers
cv::Mat1b img_centers;
getCenterPoints(img, img_centers);
// Get the centroids
cv::Mat1d img_centroids;
getCentroids(img_centers, img_centroids);
// Find the outline
cv::Mat1b img_outline = ~(img | img_centers);
// Prepare output image
cv::Mat3b img_output;
cv::cvtColor(img, img_output, cv::COLOR_GRAY2BGR);
// Compute the descriptor for each found shape, and assign to nearest descriptor among reference shapes
for (int i = 1; i < img_centroids.rows; ++i)
{
cv::Point center;
center.x = std::round(img_centroids(i, 0));
center.y = std::round(img_centroids(i, 1));
cv::Mat1d desc;
computeShapeDescriptor(img_outline, center, desc);
// Compute the distance with all reference descriptors
double minDist = 1e10;
int minIdx = 0;
for (size_t j = 0; j < shape_descriptors.size(); ++j)
{
// Actual distance computation
double dist = 0.0;
for (int c = 0; c < desc.cols; ++c) {
dist += std::abs(desc(c) - shape_descriptors[j](c));
}
if (minDist > dist) {
minDist = dist;
minIdx = j;
}
}
// Draw the ID of the shape
cv::putText(img_output, cv::String(std::to_string(minIdx + 1)), center, cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255, 255));
}
return 0;
}