b3bd842e04
This patch used -Wmissing-declarations warning to show functions and symbols that had no declarations, and either: - Make static - Add to header This helps avoid possability that declarations and functions get out of sync. And ensures all source files reference headers correctly. It also makes sure functions defined with extern "C", have this defined in the header. An error found in calligraph.h while writing this patch. This has been applied to toonzlib, to avoid making very large global changes. If accepted, -Wmissing-declarations warning could be added to CMake.
978 lines
32 KiB
C++
978 lines
32 KiB
C++
|
|
|
|
#include "tcenterlinevectP.h"
|
|
|
|
//==========================================================================
|
|
|
|
//*********************************
|
|
// Skeleton re-organization
|
|
//*********************************
|
|
|
|
//==========================================================================
|
|
|
|
//----------------------------------------
|
|
// Skeleton re-organization Globals
|
|
//----------------------------------------
|
|
|
|
namespace {
|
|
VectorizerCoreGlobals *globals;
|
|
std::vector<unsigned int> contourFamilyOfOrganized;
|
|
JointSequenceGraph *currJSGraph;
|
|
ContourFamily *currContourFamily;
|
|
};
|
|
|
|
//==========================================================================
|
|
|
|
//--------------------------------------
|
|
// Skeleton re-organization
|
|
//--------------------------------------
|
|
|
|
// EXPLANATION: The raw skeleton data obtained from StraightSkeletonizer
|
|
// class need to be grouped in joints and sequences before proceding with
|
|
// conversion in quadratics - which works on single sequences.
|
|
|
|
// NOTE: Due to maxHeight, we have to assume that a single SkeletonGraph can
|
|
// hold
|
|
// more connected graphs at once.
|
|
|
|
// a) Isolate graph-like part of skeleton
|
|
// b) Retrieve remaining single sequences.
|
|
|
|
typedef std::map<UINT, UINT, std::less<UINT>> uintMap;
|
|
|
|
// void organizeGraphs(SkeletonList* skeleton)
|
|
void organizeGraphs(SkeletonList *skeleton, VectorizerCoreGlobals &g) {
|
|
globals = &g;
|
|
|
|
SkeletonList::iterator currGraphPtr;
|
|
Sequence currSequence;
|
|
uintMap jointsMap;
|
|
UINT i, j;
|
|
|
|
UINT counter = 0; // We also count current graph number, to associate
|
|
// organized graphs to their contour family
|
|
|
|
contourFamilyOfOrganized.clear();
|
|
|
|
for (currGraphPtr = skeleton->begin(); currGraphPtr != skeleton->end();
|
|
++currGraphPtr) {
|
|
SkeletonGraph &currGraph = **currGraphPtr;
|
|
currSequence.m_graphHolder = &currGraph;
|
|
|
|
// Separate single Points - can happen only when a single node gets stored
|
|
// in a SkeletonGraph.
|
|
if (currGraph.getNodesCount() == 1) {
|
|
globals->singlePoints.push_back(*currGraph.getNode(0));
|
|
++counter;
|
|
continue;
|
|
}
|
|
|
|
// Discriminate between graphs, two-endpoint single sequences, and circular
|
|
// ones
|
|
bool has1DegreePoint = 0;
|
|
for (i = 0; i < currGraph.getNodesCount(); ++i)
|
|
if (currGraph.getNode(i).degree() != 2)
|
|
if (currGraph.getNode(i).degree() == 1)
|
|
has1DegreePoint = 1;
|
|
else
|
|
goto _graph;
|
|
|
|
if (has1DegreePoint)
|
|
goto _two_endpoint;
|
|
else
|
|
goto _circulars;
|
|
|
|
_two_endpoint : {
|
|
// Find head
|
|
for (i = 0; currGraph.getNode(i).degree() != 1; ++i)
|
|
;
|
|
|
|
currSequence.m_head = i;
|
|
currSequence.m_headLink = 0;
|
|
|
|
// Find tail
|
|
for (++i;
|
|
i < currGraph.getNodesCount() && currGraph.getNode(i).degree() == 2;
|
|
++i)
|
|
;
|
|
currSequence.m_tail = i;
|
|
currSequence.m_tailLink = 0;
|
|
|
|
globals->singleSequences.push_back(currSequence);
|
|
|
|
++counter;
|
|
continue;
|
|
}
|
|
|
|
_graph : {
|
|
// Organize Graph-like part
|
|
globals->organizedGraphs.push_back(JointSequenceGraph());
|
|
JointSequenceGraph &JSGraph = globals->organizedGraphs.back();
|
|
contourFamilyOfOrganized.push_back(counter);
|
|
|
|
jointsMap.clear();
|
|
|
|
// Gather all sequence extremities
|
|
for (i = 0; i < currGraph.getNodesCount(); ++i) {
|
|
if (currGraph.getNode(i).degree() != 2) {
|
|
j = JSGraph.newNode(i);
|
|
// Using a map to keep one-to-one relation between j and i
|
|
jointsMap.insert(uintMap::value_type(i, j));
|
|
}
|
|
}
|
|
|
|
// Extract Sequences
|
|
for (i = 0; i < JSGraph.getNodesCount(); ++i) {
|
|
UINT joint = *JSGraph.getNode(i);
|
|
for (j = 0; j < currGraph.getNode(joint).getLinksCount(); ++j) {
|
|
currSequence.m_head = joint;
|
|
currSequence.m_headLink = j;
|
|
|
|
// Seek tail
|
|
UINT oldNode = joint,
|
|
thisNode = currGraph.getNode(joint).getLink(j).getNext();
|
|
while (currGraph.getNode(thisNode).degree() == 2) {
|
|
currGraph.node(thisNode).setAttribute(
|
|
ORGANIZEGRAPHS_SIGN); // Sign thisNode as part of a JSGraph
|
|
currSequence.advance(oldNode, thisNode);
|
|
}
|
|
|
|
currSequence.m_tail = thisNode;
|
|
currSequence.m_tailLink =
|
|
currGraph.getNode(thisNode).linkOfNode(oldNode);
|
|
|
|
JSGraph.newLink(i, jointsMap.find(thisNode)->second, currSequence);
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: The following may seem uncommon - you must observe that, *WHEN
|
|
// maxThickness<INF*,
|
|
// more isolated sequence groups may arise in the SAME SkeletonGraph; so,
|
|
// an organized
|
|
// graph may contain different unconnected basic graph-structures.
|
|
// Further, remaining circular sequences may still exist. Therefore:
|
|
|
|
// Proceed with remaining circulars extraction
|
|
|
|
_circulars : {
|
|
// Extract all circular sequences
|
|
// Find a sequence point
|
|
for (i = 0; i < currGraph.getNodesCount(); ++i) {
|
|
if (!currGraph.getNode(i).hasAttribute(ORGANIZEGRAPHS_SIGN) &&
|
|
currGraph.getNode(i).degree() == 2) {
|
|
unsigned int curr = i, currLink = 0;
|
|
currSequence.next(curr, currLink);
|
|
|
|
for (; curr != i; currSequence.next(curr, currLink))
|
|
currGraph.node(curr).setAttribute(ORGANIZEGRAPHS_SIGN);
|
|
|
|
// Add sequence
|
|
currSequence.m_head = currSequence.m_tail = i;
|
|
currSequence.m_headLink = 0;
|
|
currSequence.m_tailLink = 1;
|
|
|
|
globals->singleSequences.push_back(currSequence);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//==========================================================================
|
|
|
|
//******************************
|
|
// Junction Recovery
|
|
//******************************
|
|
|
|
// EXPLANATION: Junction recovery attempts to reshape skeleton junctions when
|
|
// possible. The original polygons shape must not be exceedingly broken, and
|
|
// no visible shape alteration must found.
|
|
// WARNING: Currently not working together with
|
|
// globals->currConfig->m_maxThickness<INF.
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//------------------
|
|
// Globals
|
|
//------------------
|
|
|
|
namespace {
|
|
const double roadsMaxSlope = 0.3;
|
|
const double shapeDistMul = 1;
|
|
const double hDiffMul = 0.3;
|
|
const double lineDistMul = 1;
|
|
const double pullBackMul = 0.2;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//-------------------------
|
|
// Roads Extraction
|
|
//-------------------------
|
|
|
|
// SMALL ISSUE: The following is currently done for both side
|
|
// of a sequence...
|
|
|
|
inline void findRoads(const Sequence &s) {
|
|
unsigned int curr, currLink, next;
|
|
unsigned int roadStart, roadStartLink;
|
|
double d, hMax, length;
|
|
bool lowSlope, roadOn = 0;
|
|
|
|
curr = s.m_head;
|
|
currLink = s.m_headLink;
|
|
for (; curr != s.m_tail; s.next(curr, currLink)) {
|
|
next = s.m_graphHolder->getNode(curr).getLink(currLink).getNext();
|
|
|
|
d = planeDistance(*s.m_graphHolder->getNode(curr),
|
|
*s.m_graphHolder->getNode(next));
|
|
lowSlope =
|
|
fabs(s.m_graphHolder->getNode(curr).getLink(currLink)->getSlope()) <
|
|
roadsMaxSlope
|
|
? 1
|
|
: 0;
|
|
|
|
// Entering event in a *possibile* road axis
|
|
if (!roadOn && lowSlope) {
|
|
length = 0;
|
|
hMax = s.m_graphHolder->getNode(curr)->z;
|
|
roadStart = curr;
|
|
roadStartLink = currLink;
|
|
}
|
|
|
|
// Exit event from a """
|
|
else if (roadOn && !lowSlope && (length > hMax))
|
|
// Then sign ROAD the sequence from roadStart to curr.
|
|
for (; roadStart != curr; s.next(roadStart, roadStartLink))
|
|
s.m_graphHolder->node(roadStart)
|
|
.link(roadStartLink)
|
|
->setAttribute(SkeletonArc::ROAD);
|
|
|
|
// Now update vars
|
|
if (lowSlope) {
|
|
length += d;
|
|
if (hMax < s.m_graphHolder->getNode(next)->z)
|
|
hMax = s.m_graphHolder->getNode(next)->z;
|
|
}
|
|
|
|
roadOn = lowSlope;
|
|
}
|
|
|
|
// At sequence end, force an exit event
|
|
if (roadOn && (length > hMax))
|
|
for (; roadStart != curr; s.next(roadStart, roadStartLink))
|
|
s.m_graphHolder->node(roadStart)
|
|
.link(roadStartLink)
|
|
->setAttribute(SkeletonArc::ROAD);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
// Find the 'roads' of the current Graph.
|
|
static void findRoads(const JointSequenceGraph &JSGraph) {
|
|
unsigned int i, j;
|
|
|
|
// For all Sequence of currGraph, extract roads
|
|
for (i = 0; i < JSGraph.getNodesCount(); ++i) {
|
|
for (j = 0; j < JSGraph.getNode(i).getLinksCount(); ++j)
|
|
// if(JSGraph.getNode(i).getLink(j)->isForward())
|
|
findRoads(*JSGraph.getNode(i).getLink(j));
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//----------------------------------
|
|
// Junction Recovery Classes
|
|
//----------------------------------
|
|
|
|
// Entering point of a Sequence inside a Junction Area
|
|
class EnteringSequence final : public Sequence {
|
|
public:
|
|
TPointD m_direction; // In this case, we keep
|
|
double m_height; // separated (x,y) and z coords.
|
|
|
|
// Also store indices toward the next (outer) joint
|
|
unsigned int m_initialJoint;
|
|
unsigned int m_outerLink;
|
|
|
|
EnteringSequence() {}
|
|
EnteringSequence(const Sequence &s) : Sequence(s) {}
|
|
};
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
class JunctionArea {
|
|
public:
|
|
std::vector<EnteringSequence> m_enteringSequences;
|
|
std::vector<unsigned int> m_jointsAbsorbed;
|
|
TPointD m_newJointPosition;
|
|
|
|
JunctionArea() {}
|
|
|
|
// Initialize Junction Area
|
|
void expandArea(unsigned int initial);
|
|
|
|
// Calculate and evaluate area
|
|
bool checkShape();
|
|
bool solveJunctionPosition();
|
|
bool makeHeights();
|
|
bool calculateReconstruction();
|
|
|
|
// Substitute area over old configuration
|
|
bool sequencesPullBack();
|
|
void apply();
|
|
};
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//----------------------------
|
|
// Expansion Procedure
|
|
//----------------------------
|
|
|
|
// Junction Area expansion procedure
|
|
inline void JunctionArea::expandArea(unsigned int initial) {
|
|
unsigned int a;
|
|
unsigned int curr, currLink;
|
|
unsigned int i, iLink, iNext;
|
|
|
|
m_jointsAbsorbed.push_back(initial);
|
|
currJSGraph->node(initial).setAttribute(
|
|
JointSequenceGraph::REACHED); // Nodes absorbed gets signed
|
|
|
|
for (a = 0; a < m_jointsAbsorbed.size(); ++a) {
|
|
// Extract a joint from vector
|
|
curr = m_jointsAbsorbed[a];
|
|
|
|
for (currLink = 0; currLink < currJSGraph->getNode(curr).getLinksCount();
|
|
++currLink) {
|
|
Sequence &s = *currJSGraph->node(curr).link(currLink);
|
|
if (s.m_graphHolder->getNode(s.m_head)
|
|
.getLink(s.m_headLink)
|
|
.getAccess()) {
|
|
// Expand into all nearby sequences, until a ROAD is found
|
|
i = s.m_head;
|
|
iLink = s.m_headLink;
|
|
for (; i != s.m_tail &&
|
|
!s.m_graphHolder->getNode(i).getLink(iLink)->hasAttribute(
|
|
SkeletonArc::ROAD);
|
|
s.next(i, iLink))
|
|
;
|
|
|
|
// If the sequence has been completely run, include next joint in the
|
|
// expansion procedure AND sign it as 'REACHED'
|
|
if (i == s.m_tail) {
|
|
iNext = currJSGraph->getNode(curr).getLink(currLink).getNext();
|
|
if (!currJSGraph->node(iNext).hasAttribute(
|
|
JointSequenceGraph::REACHED)) {
|
|
currJSGraph->node(iNext).setAttribute(JointSequenceGraph::REACHED);
|
|
m_jointsAbsorbed.push_back(iNext);
|
|
}
|
|
// Negate access to this sequence
|
|
s.m_graphHolder->node(s.m_tail).link(s.m_tailLink).setAccess(0);
|
|
s.m_graphHolder->node(s.m_head).link(s.m_headLink).setAccess(0);
|
|
} else {
|
|
// Initialize and copy the entering sequence found
|
|
m_enteringSequences.push_back(EnteringSequence(s));
|
|
m_enteringSequences.back().m_head = i;
|
|
m_enteringSequences.back().m_headLink = iLink;
|
|
|
|
// Initialize entering directions
|
|
iNext = s.m_graphHolder->getNode(i).getLink(iLink).getNext();
|
|
m_enteringSequences.back().m_direction = planeProjection(
|
|
*s.m_graphHolder->getNode(i) - *s.m_graphHolder->getNode(iNext));
|
|
m_enteringSequences.back().m_direction =
|
|
m_enteringSequences.back().m_direction *
|
|
(1 / norm(m_enteringSequences.back().m_direction));
|
|
|
|
// Initialize entering height / slope
|
|
m_enteringSequences.back().m_height = s.m_graphHolder->getNode(i)->z;
|
|
|
|
// Also store pointer to link toward the joint at the end of sequence
|
|
m_enteringSequences.back().m_initialJoint = curr;
|
|
m_enteringSequences.back().m_outerLink = currLink;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//-----------------------
|
|
// Area Shape Test
|
|
//-----------------------
|
|
|
|
inline bool JunctionArea::checkShape() {
|
|
std::vector<EnteringSequence>::iterator a, b;
|
|
unsigned int node, contour, first, last, n;
|
|
TPointD A, A1, B, B1, P, P1;
|
|
bool result = 1;
|
|
|
|
// First, sign all outgoing arcs' m_leftGeneratingNode as end of
|
|
// control procedure
|
|
for (a = m_enteringSequences.begin(); a != m_enteringSequences.end(); ++a) {
|
|
node = a->m_graphHolder->getNode(a->m_head)
|
|
.getLink(a->m_headLink)
|
|
->getLeftGenerator();
|
|
contour = a->m_graphHolder->getNode(a->m_head)
|
|
.getLink(a->m_headLink)
|
|
->getLeftContour();
|
|
|
|
(*currContourFamily)[contour][node].setAttribute(ContourNode::JR_RESERVED);
|
|
}
|
|
|
|
// Now, check shape
|
|
for (a = m_enteringSequences.begin(), b = m_enteringSequences.end() - 1;
|
|
a != m_enteringSequences.end(); b = a, ++a) {
|
|
// Initialize contour check
|
|
first = a->m_graphHolder->getNode(a->m_head)
|
|
.getLink(a->m_headLink)
|
|
->getRightGenerator();
|
|
// last= b->m_graphHolder->getNode(b->m_head).getLink(b->m_headLink)
|
|
// ->getLeftGenerator();
|
|
contour = a->m_graphHolder->getNode(a->m_head)
|
|
.getLink(a->m_headLink)
|
|
->getRightContour();
|
|
n = (*currContourFamily)[contour].size();
|
|
|
|
// Better this effective way to find the last
|
|
unsigned int numChecked = 0;
|
|
for (last = first; !(*currContourFamily)[contour][last].hasAttribute(
|
|
ContourNode::JR_RESERVED) &&
|
|
numChecked < n;
|
|
last = (last + 1) % n, ++numChecked)
|
|
;
|
|
|
|
// Security check
|
|
if (numChecked == n) return 0;
|
|
|
|
A = planeProjection((*currContourFamily)[contour][first].m_position);
|
|
A1 = planeProjection(
|
|
(*currContourFamily)[contour][(first + 1) % n].m_position);
|
|
B = planeProjection((*currContourFamily)[contour][last].m_position);
|
|
B1 = planeProjection(
|
|
(*currContourFamily)[contour][(last + 1) % n].m_position);
|
|
|
|
for (node = first; !(*currContourFamily)[contour][node].hasAttribute(
|
|
ContourNode::JR_RESERVED);
|
|
node = (node + 1) % n) {
|
|
P = planeProjection((*currContourFamily)[contour][node].m_position);
|
|
P1 = planeProjection(
|
|
(*currContourFamily)[contour][(node + 1) % n].m_position);
|
|
|
|
// EXPLANATION:
|
|
// Segment P-P1 must be included in fat lines passing for A-A1 or B-B1
|
|
result &=
|
|
(fabs(cross(P - A, normalize(A1 - A))) < a->m_height * shapeDistMul &&
|
|
fabs(cross(P1 - A, normalize(A1 - A))) < a->m_height * shapeDistMul)
|
|
|
|
||
|
|
|
|
(fabs(cross(P - B, normalize(B1 - B))) < b->m_height * shapeDistMul &&
|
|
fabs(cross(P1 - B, normalize(B1 - B))) < b->m_height * shapeDistMul);
|
|
}
|
|
}
|
|
|
|
// Finally, restore nodes attributes
|
|
for (a = m_enteringSequences.begin(); a != m_enteringSequences.end(); ++a) {
|
|
node = a->m_graphHolder->getNode(a->m_head)
|
|
.getLink(a->m_headLink)
|
|
->getLeftGenerator();
|
|
contour = a->m_graphHolder->getNode(a->m_head)
|
|
.getLink(a->m_headLink)
|
|
->getLeftContour();
|
|
|
|
(*currContourFamily)[contour][node].clearAttribute(
|
|
ContourNode::JR_RESERVED);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//--------------------------------------------
|
|
// Solve new junction position problem
|
|
//--------------------------------------------
|
|
|
|
inline bool JunctionArea::solveJunctionPosition() {
|
|
std::vector<EnteringSequence>::iterator a;
|
|
double Sx2 = 0, Sy2 = 0, Sxy = 0;
|
|
TPointD P, v, b;
|
|
double h;
|
|
|
|
// Build preliminary sums for the linear system
|
|
for (a = m_enteringSequences.begin(); a != m_enteringSequences.end(); ++a) {
|
|
h = a->m_height;
|
|
v = a->m_direction;
|
|
P = planeProjection(*a->m_graphHolder->getNode(a->m_head));
|
|
|
|
// Height-weighted problem
|
|
Sx2 += sq(v.x) * h;
|
|
Sy2 += sq(v.y) * h;
|
|
Sxy += v.x * v.y * h;
|
|
b.x += h * (sq(v.y) * P.x - (v.x * v.y * P.y));
|
|
b.y += h * (sq(v.x) * P.y - (v.x * v.y * P.x));
|
|
}
|
|
|
|
// First check problem determinant
|
|
double det = Sx2 * Sy2 - sq(Sxy);
|
|
if (fabs(det) < 0.1) return 0;
|
|
|
|
// Now construct linear system
|
|
TAffine M(Sx2 / det, Sxy / det, 0, Sxy / det, Sy2 / det, 0);
|
|
|
|
m_newJointPosition = M * b;
|
|
|
|
// Finally, check if J is too far from the line extensions of the entering
|
|
// sequences
|
|
for (a = m_enteringSequences.begin(); a != m_enteringSequences.end(); ++a) {
|
|
P = planeProjection(*a->m_graphHolder->getNode(a->m_head));
|
|
if (tdistance(m_newJointPosition, a->m_direction, P) >
|
|
a->m_height * lineDistMul)
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//---------------------------------------
|
|
// Calculate optimal joint heights
|
|
//---------------------------------------
|
|
|
|
// Globals
|
|
|
|
namespace {
|
|
const std::vector<EnteringSequence> *currEnterings;
|
|
const std::vector<unsigned int> *heightIndicesPtr;
|
|
|
|
std::vector<double> *optHeights;
|
|
double optMeanError;
|
|
double hMax;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
inline bool checkCircles(std::vector<double> &heights) {
|
|
unsigned int i, j;
|
|
double cos, sin, frac;
|
|
TPointD vi, vj;
|
|
|
|
// Execute test on angle-consecutive EnteringSequences couples
|
|
for (j = 0, i = currEnterings->size() - 1; j < currEnterings->size();
|
|
i = j, ++j) {
|
|
vi = (*currEnterings)[i].m_direction;
|
|
vj = (*currEnterings)[j].m_direction;
|
|
|
|
sin = cross(vi, vj);
|
|
|
|
if (heights[i] == heights[j]) goto test_against_max_height;
|
|
|
|
frac = heights[i] / heights[j];
|
|
if (sin < 0) return 0;
|
|
cos = vi * vj;
|
|
|
|
if (cos < 0 && (frac < -cos || frac > (-1 / cos))) return 0;
|
|
|
|
test_against_max_height:
|
|
// Reusing cos
|
|
cos = (sin < 0.1)
|
|
? std::max(heights[i], heights[j])
|
|
: norm((vi * (heights[j] / sin)) + (vj * (heights[i] / sin)));
|
|
if (cos < hMax) return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
inline void tryConfiguration(const std::vector<unsigned int> &bounds) {
|
|
std::vector<double> currHeights(currEnterings->size());
|
|
double mean, currMeanError = 0;
|
|
unsigned int i, j, first, end;
|
|
|
|
for (i = 0, first = 0; i <= bounds.size(); first = end, ++i) {
|
|
end = i < bounds.size() ? end = bounds[i] + 1 : currEnterings->size();
|
|
|
|
// Calculate mean from first (included) to end (not included)
|
|
for (j = first, mean = 0; j < end; ++j)
|
|
mean += (*currEnterings)[(*heightIndicesPtr)[j]].m_height;
|
|
|
|
mean /= end - first;
|
|
|
|
// Check if the distance from extremities to mean is tolerable
|
|
if (std::max((*currEnterings)[(*heightIndicesPtr)[end - 1]].m_height - mean,
|
|
mean - (*currEnterings)[(*heightIndicesPtr)[first]].m_height) >
|
|
hDiffMul * mean)
|
|
return;
|
|
|
|
// Calculate squared error to mean
|
|
for (j = first; j < end; ++j)
|
|
currMeanError +=
|
|
sq((*currEnterings)[(*heightIndicesPtr)[j]].m_height - mean);
|
|
|
|
// Set calculated currHeights
|
|
for (j = first; j < end; ++j) currHeights[(*heightIndicesPtr)[j]] = mean;
|
|
}
|
|
|
|
// Update current maximum height
|
|
hMax = mean; // Global
|
|
|
|
// If this configuration could be better than current, launch circle test
|
|
if (optHeights->empty() || currMeanError < optMeanError) {
|
|
if (checkCircles(currHeights)) {
|
|
(*optHeights) = currHeights;
|
|
optMeanError = currMeanError;
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
class hLess {
|
|
public:
|
|
std::vector<EnteringSequence> &m_entSequences;
|
|
|
|
hLess(std::vector<EnteringSequence> &v) : m_entSequences(v) {}
|
|
|
|
inline bool operator()(unsigned int a, unsigned int b) {
|
|
return m_entSequences[a].m_height < m_entSequences[b].m_height;
|
|
}
|
|
};
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
// EXPLANATION: We build intervals on which height means are done.
|
|
// Their right Interval Bounds are supposed INCLUDED.
|
|
|
|
// Example: heights[]= {1, 2, 3, 4}; rightIntervalBounds[]={0, 2};
|
|
// => do height means on: {1}, {2,3}, {4}. (*)
|
|
|
|
// After means are calculated, a test on the obtained configuration is
|
|
// performed. Among those configurations which pass the test, the one
|
|
// with rightIntervalBounds.size()->min and, on same sizes,
|
|
// currMeanError->min is the 'best' configuration possible.
|
|
// If no height configuration pass the test, reconstruction fails.
|
|
|
|
//(*) NOTE: The right Interval Bounds will never include index n-1, which
|
|
// interferes with push_backs.
|
|
|
|
inline bool JunctionArea::makeHeights() {
|
|
std::vector<unsigned int> heightOrderedIndices;
|
|
std::vector<unsigned int> rightIntervalBounds;
|
|
std::vector<double> optimalHeights;
|
|
|
|
unsigned int i, n, m;
|
|
|
|
// Sort entering sequences' indices for increasing height/thickness
|
|
heightOrderedIndices.resize(m_enteringSequences.size());
|
|
for (i = 0; i < m_enteringSequences.size(); ++i) heightOrderedIndices[i] = i;
|
|
sort(heightOrderedIndices.begin(), heightOrderedIndices.end(),
|
|
hLess(m_enteringSequences));
|
|
|
|
// Initialize globals/references
|
|
currEnterings = &m_enteringSequences;
|
|
heightIndicesPtr = &heightOrderedIndices;
|
|
|
|
optMeanError = 0;
|
|
optHeights = &optimalHeights;
|
|
|
|
// Now build height-mean configurations and launch their tests
|
|
n = m_enteringSequences.size();
|
|
|
|
// The m=0 case is done first, apart
|
|
rightIntervalBounds.resize(0);
|
|
tryConfiguration(rightIntervalBounds);
|
|
for (m = 1; m < n && optimalHeights.empty(); ++m) {
|
|
// Initialize bounds
|
|
rightIntervalBounds.resize(1);
|
|
rightIntervalBounds[0] = 0;
|
|
|
|
while (!rightIntervalBounds.empty()) {
|
|
// Fill bounds if necessary
|
|
while (rightIntervalBounds.size() < m)
|
|
rightIntervalBounds.push_back(rightIntervalBounds.back() + 1);
|
|
|
|
tryConfiguration(rightIntervalBounds);
|
|
|
|
//'Advance' configuration: increment last index and pop those
|
|
// exceeding valid size. If bounds gets empty, done all configs
|
|
// with m+1 intervals.
|
|
while ((
|
|
++rightIntervalBounds.back(),
|
|
rightIntervalBounds.back() < n - 1 - (m - rightIntervalBounds.size())
|
|
? 0
|
|
: (rightIntervalBounds.pop_back(), !rightIntervalBounds.empty())))
|
|
;
|
|
}
|
|
}
|
|
|
|
if (!optimalHeights.empty()) {
|
|
for (i = 0; i < n; ++i) m_enteringSequences[i].m_height = optimalHeights[i];
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//==========================================================================
|
|
|
|
//------------------------------
|
|
// Area Calculation Main
|
|
//------------------------------
|
|
|
|
class EntSequenceLess {
|
|
public:
|
|
EntSequenceLess() {}
|
|
|
|
inline bool operator()(const EnteringSequence &a, const EnteringSequence &b) {
|
|
// m_direction is normalized, therefore:
|
|
return a.m_direction.y >= 0
|
|
? b.m_direction.y >= 0 ? a.m_direction.x > b.m_direction.x : 1
|
|
: b.m_direction.y < 0 ? a.m_direction.x < b.m_direction.x : 0;
|
|
}
|
|
};
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
bool JunctionArea::calculateReconstruction() {
|
|
unsigned int i;
|
|
|
|
if (m_enteringSequences.size() == 0) return 0;
|
|
|
|
// Apply preliminary tests for this Junction Area
|
|
|
|
// a) Check if endpoints got absorbed by the expansion process
|
|
for (i = 0; i < m_jointsAbsorbed.size(); ++i)
|
|
if (currJSGraph->getNode(m_jointsAbsorbed[i]).degree() == 1) return 0;
|
|
|
|
// b) Check if the contours shape resembles that of a crossing
|
|
|
|
sort(m_enteringSequences.begin(), m_enteringSequences.end(),
|
|
EntSequenceLess());
|
|
|
|
if (!checkShape()) return 0;
|
|
|
|
// c) Build the new junction Point plane position
|
|
if (!solveJunctionPosition()) return 0;
|
|
|
|
// d) Build joint optimal heights (each for entering sequence...)
|
|
if (!makeHeights()) return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
//==========================================================================
|
|
|
|
//---------------------------
|
|
// Sequences Pull Back
|
|
//---------------------------
|
|
|
|
// EXPLANATION: We have to insure that connecting entering sequences to the
|
|
// new junction point happens *smoothly*. In order to do this, wh withdraw
|
|
// entering sequences along the enterin road, until the angle given by the
|
|
// connecting line and the entering direction is small.
|
|
// However, sequence pull back can be done only under some constraints:
|
|
// * we have to remain inside a road axis
|
|
// * ........................ a given fat line around the entering direction
|
|
|
|
inline bool JunctionArea::sequencesPullBack() {
|
|
std::vector<EnteringSequence>::iterator a;
|
|
double alongLinePosition, distanceFromLine;
|
|
unsigned int i, iLink, tail;
|
|
TPointD P;
|
|
|
|
for (a = m_enteringSequences.begin(); a != m_enteringSequences.end(); ++a) {
|
|
i = a->m_head;
|
|
iLink = a->m_headLink;
|
|
// NOTE: updated tails are stored this way, *DONT* look in a->m_tail
|
|
// because these typically store old infos
|
|
tail =
|
|
currJSGraph->getNode(a->m_initialJoint).getLink(a->m_outerLink)->m_tail;
|
|
P = planeProjection(*a->m_graphHolder->getNode(a->m_head));
|
|
|
|
while (i != tail) {
|
|
alongLinePosition = a->m_direction * (m_newJointPosition - P);
|
|
distanceFromLine = tdistance(m_newJointPosition, a->m_direction, P);
|
|
|
|
if (alongLinePosition >= 0 &&
|
|
(distanceFromLine / alongLinePosition) <= 0.5)
|
|
goto found_pull_back;
|
|
|
|
// We then take the next arc and check it
|
|
|
|
if (!a->m_graphHolder->getNode(i).getLink(iLink)->hasAttribute(
|
|
SkeletonArc::ROAD))
|
|
return 0; // Pull back failed
|
|
|
|
a->next(i, iLink);
|
|
|
|
P = planeProjection(*a->m_graphHolder->getNode(i));
|
|
if (tdistance(P, a->m_direction, m_newJointPosition) >
|
|
std::max(pullBackMul * a->m_height, 1.0))
|
|
return 0; // Pull back failed
|
|
}
|
|
|
|
// Now checking opposite sequence extremity
|
|
if (alongLinePosition < 0 || (distanceFromLine / alongLinePosition) > 0.5)
|
|
return 0;
|
|
|
|
found_pull_back:
|
|
|
|
a->m_head = i;
|
|
a->m_headLink = iLink;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//----------------------------------------------
|
|
// Substitute new junction configuration
|
|
//----------------------------------------------
|
|
|
|
void JunctionArea::apply() {
|
|
std::vector<EnteringSequence>::iterator a;
|
|
unsigned int newJoint, newSkeletonNode;
|
|
unsigned int head, headLink, tail, tailLink;
|
|
unsigned int outerJoint, outerLink;
|
|
unsigned int i, next, nextLink;
|
|
|
|
// First, check if Entering Sequences pullback is possible
|
|
if (!sequencesPullBack()) return;
|
|
|
|
// Then, we can substitute the old configuration
|
|
// First, sign as 'ELIMINATED' all absorbed joints
|
|
for (i = 0; i < m_jointsAbsorbed.size(); ++i)
|
|
currJSGraph->node(m_jointsAbsorbed[i])
|
|
.setAttribute(JointSequenceGraph::ELIMINATED);
|
|
|
|
newJoint = currJSGraph->newNode();
|
|
|
|
for (a = m_enteringSequences.begin(); a != m_enteringSequences.end(); ++a) {
|
|
// Initialize infos
|
|
newSkeletonNode =
|
|
a->m_graphHolder->newNode(T3DPointD(m_newJointPosition, a->m_height));
|
|
|
|
// Retrieve sequence infos to substitute
|
|
// NOTE: We update *tail* infos in currJSGraph sequences after each "apply"
|
|
|
|
const JointSequenceGraph::Link &tempLink =
|
|
currJSGraph->getNode(a->m_initialJoint).getLink(a->m_outerLink);
|
|
|
|
head = tempLink->m_head;
|
|
headLink = tempLink->m_headLink;
|
|
tail = tempLink->m_tail;
|
|
tailLink = tempLink->m_tailLink;
|
|
|
|
outerJoint = tempLink.getNext();
|
|
|
|
// Find outerLink - from outerJoint to a->m_initialJoint
|
|
for (i = 0;
|
|
(currJSGraph->getNode(outerJoint).getLink(i)->m_tail != head) ||
|
|
(currJSGraph->getNode(outerJoint).getLink(i)->m_tailLink != headLink);
|
|
++i)
|
|
;
|
|
outerLink = i;
|
|
|
|
// Now, provide skeleton linkages
|
|
// NOTE: Discriminate between the following cases
|
|
// a) m_head->degree == 2
|
|
// b) m_head == m_tail
|
|
// c) m_head == original m_head - or, (m_head!=m_tail && m_head->deg>2)
|
|
|
|
if (a->m_graphHolder->getNode(a->m_head).degree() == 2) {
|
|
a->m_graphHolder->newLink(newSkeletonNode, a->m_head);
|
|
|
|
a->m_graphHolder->node(a->m_head)
|
|
.link(!a->m_headLink)
|
|
.setNext(newSkeletonNode);
|
|
a->m_graphHolder->node(a->m_head)
|
|
.link(!a->m_headLink)
|
|
->setAttribute(SkeletonArc::ROAD);
|
|
// Better clear road attribute or set it?
|
|
} else if (a->m_head == tail) {
|
|
a->m_graphHolder->newLink(newSkeletonNode, tail);
|
|
|
|
a->m_graphHolder->node(tail).link(tailLink).setNext(newSkeletonNode);
|
|
a->m_graphHolder->node(tail).link(tailLink)->setAttribute(
|
|
SkeletonArc::ROAD);
|
|
} else //(a->m_head == head)
|
|
{
|
|
// In this case, better introduce further a new substitute of head
|
|
unsigned int newHead = a->m_graphHolder->newNode(
|
|
T3DPointD(*a->m_graphHolder->getNode(a->m_head)));
|
|
|
|
a->m_graphHolder->newLink(newSkeletonNode, newHead);
|
|
a->m_graphHolder->newLink(newHead, newSkeletonNode);
|
|
|
|
// Link newHead on the other side
|
|
next = a->m_graphHolder->getNode(head).getLink(headLink).getNext();
|
|
nextLink = a->m_graphHolder->getNode(next).linkOfNode(head);
|
|
|
|
a->m_graphHolder->newLink(newHead, next);
|
|
a->m_graphHolder->node(next).link(nextLink).setNext(newHead);
|
|
}
|
|
|
|
// Finally, update joint linkings and sequence tails.
|
|
Sequence newSequence;
|
|
|
|
newSequence.m_graphHolder = a->m_graphHolder;
|
|
newSequence.m_head = newSkeletonNode;
|
|
newSequence.m_headLink = 0;
|
|
newSequence.m_tail = tail;
|
|
newSequence.m_tailLink = tailLink;
|
|
|
|
currJSGraph->node(outerJoint).link(outerLink).setNext(newJoint);
|
|
currJSGraph->node(outerJoint).link(outerLink)->m_tail = newSkeletonNode;
|
|
currJSGraph->node(outerJoint).link(outerLink)->m_tailLink = 0;
|
|
|
|
currJSGraph->newLink(newJoint, outerJoint, newSequence);
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
//-------------------------------
|
|
// Junction Recovery Main
|
|
//-------------------------------
|
|
|
|
// EXPLANATION: Junction Recovery attempts reconstruction of badly-behaved
|
|
// crossings in the raw skeleton of the image.
|
|
|
|
// void inline junctionRecovery(Contours* polygons)
|
|
void junctionRecovery(Contours *polygons, VectorizerCoreGlobals &g) {
|
|
globals = &g;
|
|
|
|
unsigned int i, j;
|
|
std::vector<JunctionArea> junctionAreasList;
|
|
|
|
// For all joints not processed by the Recoverer, launch a new junction
|
|
// area reconstruction
|
|
for (i = 0; i < globals->organizedGraphs.size(); ++i) {
|
|
currJSGraph = &globals->organizedGraphs[i];
|
|
currContourFamily = &(*polygons)[contourFamilyOfOrganized[i]];
|
|
junctionAreasList.clear();
|
|
|
|
// First, graph roads are found and signed on the skeleton
|
|
findRoads(*currJSGraph);
|
|
|
|
// Then, junction areas are identified and reconstructions are calculated
|
|
for (j = 0; j < currJSGraph->getNodesCount(); ++j)
|
|
if (!currJSGraph->getNode(j).hasAttribute(JointSequenceGraph::REACHED)) {
|
|
junctionAreasList.push_back(JunctionArea());
|
|
junctionAreasList.back().expandArea(j);
|
|
if (!junctionAreasList.back().calculateReconstruction())
|
|
junctionAreasList.pop_back();
|
|
}
|
|
|
|
// Finally, reconstructions are substituted inside the skeleton
|
|
for (j = 0; j < junctionAreasList.size(); ++j) junctionAreasList[j].apply();
|
|
}
|
|
}
|