1 | initial version |
I would suggest to do better preprocessing because of non uniform background and to avoid blur because thin arcs can disappear. When you have a good mask for your draw you can use massive morphology operation to isolate nodes and arcs Finally you have to design a logic to build the chart connections.
Here is my implementation
int Main_DiagramAnalyzer()
{
Mat src, bw;
src = imread("../img/diagram-x5.jpg");
==PREPROCESSING==
Option1: Remove non uniform background/illumination. You can achieve this using the wonderful quadratic background removal by @LBerger. Than get a mask of the draw using a simple threshold
#if 1
Mat srcGray, dstGray;
cvtColor(src, srcGray, CV_BGR2GRAY);
BrightnessAndContrast::BackgroundRemoveQuadratic(srcGray, dstGray);
threshold(dstGray, bw, 100, 255, THRESH_OTSU);
Quadratic result threshold
OR Option2: Get the HSV planes and threshold high values in S (>=95 percentile) to get a mask of the draw. You can achieve this using clipHistPercent=5
with BrightnessAndContrastAuto than get a binary mask looking at very high values in saturation
#else
Mat src_hsv, saturation;
vector<cv::Mat> hsv_planes;
cvtColor(src, src_hsv, CV_BGR2HSV);
cv::split(src_hsv, hsv_planes);
saturation = hsv_planes[1];
bw = saturation > 70;
BrightnessAndContrastAuto(saturation, saturation, 5);
bw = saturation >= 254;
#endif
Saturation result (look draw values) threshold
fill circular structures using small circle
int nodeSize = 15; //avg node diameter in px
Morph(bw, bw, MORPH_CLOSE, MORPH_ELLIPSE, cvRound(nodeSize / 5));
imshow("Preprocessing", bw);
GET MASK OF NODES: remove boundary structures smaller than avgNodeSize. Should remove the arcs
cv::Mat nodesMask;
Morph(bw, nodesMask, MORPH_OPEN, MORPH_ELLIPSE, cvRound(nodeSize / 5));
imshow("Nodes-Mask", nodesMask);
Node Mask
GET MASK OF ARCS: dilate nodes a bit (ensure node vs arcs separation) than subtract them from the global mask
cv::Mat arcs, nodesLarger;
Morph(nodesMask, nodesLarger, MORPH_DILATE, MORPH_ELLIPSE, 1);
arcs = bw - nodesLarger;
imshow("Arcs-Mask", arcs);
Arcs Mask
GET INTERSECTIONS: ensure node vs arcs intersection doubling size of nodes than get a mask of common pixels
cv::Mat intersections;
Morph(nodesLarger, nodesLarger, MORPH_DILATE, MORPH_ELLIPSE, nodeSize);
intersections = nodesLarger & arcs;
GET CONTOURS OF ELEMENTS
vector<vector<Point> > nodesContours, arcsContours, intesectsContours;
findContours(nodesLarger, nodesContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
findContours(arcs, arcsContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
findContours(intersections, intesectsContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector<Point> nodesCenter(nodesContours.size(), Point(-1, -1));
vector<double> nodesArea(nodesContours.size(), -1);
vector<double> arcsLenght(arcsContours.size(), -1);
DRAW ENUMERATED CONTOURS and get center of nodes
Moments m;
Point center;
Scalar cl = Scalar(0, 0, 255);
double val,minvalue = nodeSize/3;
for (size_t i = 0; i < nodesContours.size(); i++)
{
val = contourArea(nodesContours[i]);
if (val < minvalue) continue; //skip if it's small
nodesArea[i] = val;
m = moments(nodesContours[i], true);
//if (m.m00 == 0) continue;
center = Point(m.m10 / m.m00, m.m01 / m.m00);
nodesCenter[i] = center;
drawMarker(src, center, cl, MARKER_TILTED_CROSS, 5);
putText(src, "N" + to_string(i), center, FONT_HERSHEY_PLAIN, 1, cl);
}
cl = Scalar(0, 255, 0);
minvalue = nodeSize/5;
for (size_t i = 0; i < arcsContours.size(); i++)
{
val = arcLength(arcsContours[i], false);
if (val < minvalue) continue; //skip if it's small
arcsLenght[i] = val;
center = (arcsContours[i][0] + arcsContours[i][arcsContours[i].size() - 1]) / 2;
drawContours(src, arcsContours, i, cl);
putText(src, "A" + to_string(i), center, FONT_HERSHEY_PLAIN, 1, cl);
}
BUILD THE GRAPH: Use of common pixels to detect arc vs node connection. For each common pixels search related arc and node and build a vectors of connected nodes. THIS IS NOT OPTIMAL TECHNIQUE BUT IT WORKS FOR EXAMPLE PURPOSE.
vector<vector<int>> connections(arcsContours.size());
for (size_t c = 0; c < intesectsContours.size(); c++)
{
int nodeIdx = -1;
int archIdx = -1;
for (size_t p = 0; p < intesectsContours[c].size(); c++)
{
Point pt = intesectsContours[c][p];
for (size_t n = 0; n < nodesContours.size() && nodeIdx <0; n++)
{
if (nodesArea[n] < 0) continue; //skip if it's small
if (pointPolygonTest(nodesContours[n], pt, false) >= 0)
nodeIdx = n;
} //for nodesContours
for (size_t a = 0; a < arcsContours.size() && archIdx < 0; a++)
{
if (pointPolygonTest(arcsContours[a], pt, false) >= 0)
archIdx = a;
} //for arcsContours
if ((nodeIdx >= 0) && (archIdx >= 0))
{
connections[archIdx].push_back(nodeIdx);
break;
} // if
} // for intesectsContours[c]
} // for intesectsContours
PRINT OUT THE RESULT as Graphviz chart
cout << "graph G{ " << endl;
for (size_t i = 0; i < nodesContours.size(); i++)
{
if (nodesArea[i] < 0 ) continue; //skip if it's small
cout << "N"<<i << " ";
}
cout << endl;
for (size_t i = 0; i < connections.size(); i++)
{
if (connections[i].size()>1)
{
//cout << "\nArc " << i << " connect nodes :";
for (size_t j = 0; j < connections[i].size() - 1; j++)
{
int from = connections[i][j];
int to = connections[i][j + 1];
if (nodesCenter[from].y > nodesCenter[to].y)
{
int tmp = from;
from = to;
to = tmp;
}
cout << "\tN"<<from<<" -- "<< "N"<<to << endl;
}
}
}
cout << "}";
imshow("SRC", src);
cv::waitKey(0);
return 0;
}
OUTPUT
graph G{
N0 N1 N2 N3 N4
N1 -- N0
N2 -- N0
N3 -- N2
N3 -- N1
N4 -- N3
}
Enumerating: Layout by Graphviz/dot
2 | No.2 Revision |
I would suggest to do better preprocessing because of non uniform background and to avoid blur because thin arcs can disappear.
When you have a good mask for your draw drawing you can use massive morphology operation to isolate nodes and arcs
Finally you have to design a logic to build the chart connections.
edit: small changes in descriptions
Below is my DiagramAnalyzer
to convert your image into a Graphviz/dot chart. Here is my implementationthe result
graph G{
N0 N1 N2 N3 N4
N1 -- N0
N2 -- N0
N3 -- N2
N3 -- N1
N4 -- N3
}
Enumerating: Layout by Graphviz/dot
int Main_DiagramAnalyzer()
DiagramAnalyzer()
{
Mat src, bw;
src = imread("../img/diagram-x5.jpg");
==PREPROCESSING==
Option1: Remove non uniform background/illumination. You can achieve this using the wonderful quadratic background removal by @LBerger. Than get a mask of the draw drawing using a simple threshold
#if 1
Mat srcGray, dstGray;
cvtColor(src, srcGray, CV_BGR2GRAY);
BrightnessAndContrast::BackgroundRemoveQuadratic(srcGray, dstGray);
threshold(dstGray, bw, 100, 255, THRESH_OTSU);
Quadratic result threshold
OR Option2: Get the HSV planes and threshold high values in S (>=95 percentile) to get a mask of the draw. drawing. You can achieve this using clipHistPercent=5
with BrightnessAndContrastAuto than get a binary mask looking at very high values in saturation
#else
Mat src_hsv, saturation;
vector<cv::Mat> hsv_planes;
cvtColor(src, src_hsv, CV_BGR2HSV);
cv::split(src_hsv, hsv_planes);
saturation = hsv_planes[1];
bw = saturation > 70;
BrightnessAndContrastAuto(saturation, saturation, 5);
bw = saturation >= 254;
#endif
Saturation result (look draw values) at values of drawing) threshold
fill circular structures using small circle
int nodeSize = 15; //avg node diameter in px
Morph(bw, bw, MORPH_CLOSE, MORPH_ELLIPSE, cvRound(nodeSize / 5));
imshow("Preprocessing", bw);
Preprocessing (result of quadratic)
GET MASK OF NODES: remove boundary structures smaller than avgNodeSize. nodeSize
. Should remove the arcs
cv::Mat nodesMask;
Morph(bw, nodesMask, MORPH_OPEN, MORPH_ELLIPSE, cvRound(nodeSize / 5));
imshow("Nodes-Mask", nodesMask);
Node Mask
GET MASK OF ARCS: dilate nodes a bit (ensure node vs arcs separation) than subtract them from the global mask
cv::Mat arcs, nodesLarger;
Morph(nodesMask, nodesLarger, MORPH_DILATE, MORPH_ELLIPSE, 1);
arcs = bw - nodesLarger;
imshow("Arcs-Mask", arcs);
Arcs Mask
GET INTERSECTIONS: ensure node vs arcs intersection doubling size of nodes than get a mask of common shared pixels
cv::Mat intersections;
Morph(nodesLarger, nodesLarger, MORPH_DILATE, MORPH_ELLIPSE, nodeSize);
intersections = nodesLarger & arcs;
GET CONTOURS OF ELEMENTS
vector<vector<Point> > nodesContours, arcsContours, intesectsContours;
findContours(nodesLarger, nodesContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
findContours(arcs, arcsContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
findContours(intersections, intesectsContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector<Point> nodesCenter(nodesContours.size(), Point(-1, -1));
vector<double> nodesArea(nodesContours.size(), -1);
vector<double> arcsLenght(arcsContours.size(), -1);
DRAW ENUMERATED CONTOURS and get center of nodes
Moments m;
Point center;
Scalar cl = Scalar(0, 0, 255);
double val,minvalue = nodeSize/3;
for (size_t i = 0; i < nodesContours.size(); i++)
{
val = contourArea(nodesContours[i]);
if (val < minvalue) continue; //skip if it's small
nodesArea[i] = val;
m = moments(nodesContours[i], true);
//if (m.m00 == 0) continue;
center = Point(m.m10 / m.m00, m.m01 / m.m00);
nodesCenter[i] = center;
drawMarker(src, center, cl, MARKER_TILTED_CROSS, 5);
putText(src, "N" + to_string(i), center, FONT_HERSHEY_PLAIN, 1, cl);
}
cl = Scalar(0, 255, 0);
minvalue = nodeSize/5;
for (size_t i = 0; i < arcsContours.size(); i++)
{
val = arcLength(arcsContours[i], false);
if (val < minvalue) continue; //skip if it's small
arcsLenght[i] = val;
center = (arcsContours[i][0] + arcsContours[i][arcsContours[i].size() - 1]) / 2;
drawContours(src, arcsContours, i, cl);
putText(src, "A" + to_string(i), center, FONT_HERSHEY_PLAIN, 1, cl);
}
BUILD THE GRAPH: Use of common shared pixels to detect arc vs node connection. For each common shared pixels search related arc and node and build a vectors of connected nodes. THIS IS NOT OPTIMAL TECHNIQUE BUT IT WORKS FOR EXAMPLE PURPOSE.
vector<vector<int>> connections(arcsContours.size());
for (size_t c = 0; c < intesectsContours.size(); c++)
{
int nodeIdx = -1;
int archIdx = -1;
for (size_t p = 0; p < intesectsContours[c].size(); c++)
{
Point pt = intesectsContours[c][p];
for (size_t n = 0; n < nodesContours.size() && nodeIdx <0; n++)
{
if (nodesArea[n] < 0) continue; //skip if it's small
if (pointPolygonTest(nodesContours[n], pt, false) >= 0)
nodeIdx = n;
} //for nodesContours
for (size_t a = 0; a < arcsContours.size() && archIdx < 0; a++)
{
if (pointPolygonTest(arcsContours[a], pt, false) >= 0)
archIdx = a;
} //for arcsContours
if ((nodeIdx >= 0) && (archIdx >= 0))
{
connections[archIdx].push_back(nodeIdx);
break;
} // if
} // for intesectsContours[c]
} // for intesectsContours
PRINT OUT THE RESULT as Graphviz chart
cout << "graph G{ " << endl;
for (size_t i = 0; i < nodesContours.size(); i++)
{
if (nodesArea[i] < 0 ) continue; //skip if it's small
cout << "N"<<i << " ";
}
cout << endl;
for (size_t i = 0; i < connections.size(); i++)
{
if (connections[i].size()>1)
{
//cout << "\nArc " << i << " connect nodes :";
for (size_t j = 0; j < connections[i].size() - 1; j++)
{
int from = connections[i][j];
int to = connections[i][j + 1];
if (nodesCenter[from].y > nodesCenter[to].y)
{
int tmp = from;
from = to;
to = tmp;
}
cout << "\tN"<<from<<" -- "<< "N"<<to << endl;
}
}
}
cout << "}";
imshow("SRC", src);
cv::waitKey(0);
return 0;
}
OUTPUT
graph G{
N0 N1 N2 N3 N4
N1 -- N0
N2 -- N0
N3 -- N2
N3 -- N1
N4 -- N3
void Morph(const cv::Mat &src, cv::Mat &dst, int operation = cv::MORPH_OPEN, int kernel_type = cv::MORPH_RECT, int size = 1)
{
cv::Point anchor = cv::Point(size, size);
cv::Mat element = getStructuringElement(kernel_type, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(src, dst, operation, element, anchor);
}
Enumerating: Layout by Graphviz/dot
3 | No.3 Revision |
I would suggest to do better preprocessing because of non uniform background and to avoid blur because thin arcs can disappear. When you have a good mask for your drawing you can use massive morphology operation to isolate nodes and arcs Finally you have to design a logic to build the chart connections.
edit: small changes in descriptions
Below is my DiagramAnalyzer
to convert your image into a Graphviz/dot chart. Here is the result
graph G{
N0 N1 N2 N3 N4
N1 -- N0
N2 -- N0
N3 -- N2
N3 -- N1
N4 -- N3
}
Enumerating: Layout by Graphviz/dot
int DiagramAnalyzer()
{
Mat src, bw;
src = imread("../img/diagram-x5.jpg");
==PREPROCESSING==
Option1: Remove non uniform background/illumination. You can achieve this using the wonderful quadratic background removal by @LBerger. Than get a mask of the drawing using a simple threshold
#if 1
Mat srcGray, dstGray;
cvtColor(src, srcGray, CV_BGR2GRAY);
BrightnessAndContrast::BackgroundRemoveQuadratic(srcGray, BackgroundRemoveQuadratic(srcGray, dstGray);
threshold(dstGray, bw, 100, 255, THRESH_OTSU);
Quadratic result threshold
OR Option2: Get the HSV planes and threshold high values in S (>=95 percentile) to get a mask of the drawing. You can achieve this using clipHistPercent=5
with BrightnessAndContrastAuto than get a binary mask looking at very high values in saturation
#else
Mat src_hsv, saturation;
vector<cv::Mat> hsv_planes;
cvtColor(src, src_hsv, CV_BGR2HSV);
cv::split(src_hsv, hsv_planes);
saturation = hsv_planes[1];
BrightnessAndContrastAuto(saturation, saturation, 5);
bw = saturation >= 254;
#endif
Saturation result (look at values of drawing) threshold
fill circular structures using small circle
int nodeSize = 15; //avg node diameter in px
Morph(bw, bw, MORPH_CLOSE, MORPH_ELLIPSE, cvRound(nodeSize / 5));
imshow("Preprocessing", bw);
Preprocessing (result of quadratic)
GET MASK OF NODES: remove boundary structures smaller than nodeSize
. Should remove the arcs
cv::Mat nodesMask;
Morph(bw, nodesMask, MORPH_OPEN, MORPH_ELLIPSE, cvRound(nodeSize / 5));
imshow("Nodes-Mask", nodesMask);
Node Mask
GET MASK OF ARCS: dilate nodes a bit (ensure node vs arcs separation) than subtract them from the global mask
cv::Mat arcs, nodesLarger;
Morph(nodesMask, nodesLarger, MORPH_DILATE, MORPH_ELLIPSE, 1);
arcs = bw - nodesLarger;
imshow("Arcs-Mask", arcs);
Arcs Mask
GET INTERSECTIONS: ensure node vs arcs intersection doubling size of nodes than get a mask of shared pixels
cv::Mat intersections;
Morph(nodesLarger, nodesLarger, MORPH_DILATE, MORPH_ELLIPSE, nodeSize);
intersections = nodesLarger & arcs;
GET CONTOURS OF ELEMENTS
vector<vector<Point> > nodesContours, arcsContours, intesectsContours;
findContours(nodesLarger, nodesContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
findContours(arcs, arcsContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
findContours(intersections, intesectsContours, CV_RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
vector<Point> nodesCenter(nodesContours.size(), Point(-1, -1));
vector<double> nodesArea(nodesContours.size(), -1);
vector<double> arcsLenght(arcsContours.size(), -1);
DRAW ENUMERATED CONTOURS and get center of nodes
Moments m;
Point center;
Scalar cl = Scalar(0, 0, 255);
double val,minvalue = nodeSize/3;
for (size_t i = 0; i < nodesContours.size(); i++)
{
val = contourArea(nodesContours[i]);
if (val < minvalue) continue; //skip if it's small
nodesArea[i] = val;
m = moments(nodesContours[i], true);
//if (m.m00 == 0) continue;
center = Point(m.m10 / m.m00, m.m01 / m.m00);
nodesCenter[i] = center;
drawMarker(src, center, cl, MARKER_TILTED_CROSS, 5);
putText(src, "N" + to_string(i), center, FONT_HERSHEY_PLAIN, 1, cl);
}
cl = Scalar(0, 255, 0);
minvalue = nodeSize/5;
for (size_t i = 0; i < arcsContours.size(); i++)
{
val = arcLength(arcsContours[i], false);
if (val < minvalue) continue; //skip if it's small
arcsLenght[i] = val;
center = (arcsContours[i][0] + arcsContours[i][arcsContours[i].size() - 1]) / 2;
drawContours(src, arcsContours, i, cl);
putText(src, "A" + to_string(i), center, FONT_HERSHEY_PLAIN, 1, cl);
}
BUILD THE GRAPH: Use shared pixels to detect arc vs node connection. For each shared pixels search related arc and node and build a vectors of connected nodes. THIS IS NOT OPTIMAL TECHNIQUE BUT IT WORKS FOR EXAMPLE PURPOSE.
vector<vector<int>> connections(arcsContours.size());
for (size_t c = 0; c < intesectsContours.size(); c++)
{
int nodeIdx = -1;
int archIdx = -1;
for (size_t p = 0; p < intesectsContours[c].size(); c++)
{
Point pt = intesectsContours[c][p];
for (size_t n = 0; n < nodesContours.size() && nodeIdx <0; n++)
{
if (nodesArea[n] < 0) continue; //skip if it's small
if (pointPolygonTest(nodesContours[n], pt, false) >= 0)
nodeIdx = n;
} //for nodesContours
for (size_t a = 0; a < arcsContours.size() && archIdx < 0; a++)
{
if (pointPolygonTest(arcsContours[a], pt, false) >= 0)
archIdx = a;
} //for arcsContours
if ((nodeIdx >= 0) && (archIdx >= 0))
{
connections[archIdx].push_back(nodeIdx);
break;
} // if
} // for intesectsContours[c]
} // for intesectsContours
PRINT OUT THE RESULT as Graphviz chart
cout << "graph G{ " << endl;
for (size_t i = 0; i < nodesContours.size(); i++)
{
if (nodesArea[i] < 0 ) continue; //skip if it's small
cout << "N"<<i << " ";
}
cout << endl;
for (size_t i = 0; i < connections.size(); i++)
{
if (connections[i].size()>1)
{
//cout << "\nArc " << i << " connect nodes :";
for (size_t j = 0; j < connections[i].size() - 1; j++)
{
int from = connections[i][j];
int to = connections[i][j + 1];
if (nodesCenter[from].y > nodesCenter[to].y)
{
int tmp = from;
from = to;
to = tmp;
}
cout << "\tN"<<from<<" -- "<< "N"<<to << endl;
}
}
}
cout << "}";
imshow("SRC", src);
cv::waitKey(0);
return 0;
}
void Morph(const cv::Mat &src, cv::Mat &dst, int operation = cv::MORPH_OPEN, int kernel_type = cv::MORPH_RECT, int size = 1)
{
cv::Point anchor = cv::Point(size, size);
cv::Mat element = getStructuringElement(kernel_type, cv::Size(2 * size + 1, 2 * size + 1), anchor);
morphologyEx(src, dst, operation, element, anchor);
}