You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
667 lines
13 KiB
667 lines
13 KiB
#include "ShapeRecognizer.h" |
|
|
|
#include "CircleRecognizer.h" |
|
#include "Inertia.h" |
|
#include "ShapeRecognizerResult.h" |
|
|
|
#include "model/Stroke.h" |
|
|
|
#include <config-debug.h> |
|
|
|
#include <math.h> |
|
#include <string.h> |
|
|
|
ShapeRecognizer::ShapeRecognizer() |
|
{ |
|
XOJ_INIT_TYPE(ShapeRecognizer); |
|
|
|
resetRecognizer(); |
|
this->stroke = NULL; |
|
this->queueLength = 0; |
|
} |
|
|
|
ShapeRecognizer::~ShapeRecognizer() |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
resetRecognizer(); |
|
|
|
XOJ_RELEASE_TYPE(ShapeRecognizer); |
|
} |
|
|
|
void ShapeRecognizer::resetRecognizer() |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
RDEBUG("reset"); |
|
|
|
for (int i = 0; i < MAX_POLYGON_SIDES + 1; i++) |
|
{ |
|
this->queue[i].stroke = NULL; |
|
} |
|
|
|
this->queueLength = 0; |
|
} |
|
|
|
/** |
|
* Test if segments form standard shapes |
|
*/ |
|
Stroke* ShapeRecognizer::tryRectangle() |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
// first, we need whole strokes to combine to 4 segments... |
|
if (this->queueLength < 4) |
|
{ |
|
return NULL; |
|
} |
|
|
|
RecoSegment* rs = &this->queue[this->queueLength - 4]; |
|
if (rs->startpt != 0) |
|
{ |
|
return NULL; |
|
} |
|
|
|
// check edges make angles ~= Pi/2 and vertices roughly match |
|
double avgAngle = 0.; |
|
for (int i = 0; i <= 3; i++) |
|
{ |
|
RecoSegment* r1 = &rs[i]; |
|
RecoSegment* r2 = &rs[(i + 1) % 4]; |
|
if (fabs(fabs(r1->angle - r2->angle) - M_PI / 2) > RECTANGLE_ANGLE_TOLERANCE) |
|
{ |
|
return FALSE; |
|
} |
|
avgAngle += r1->angle; |
|
if (r2->angle > r1->angle) |
|
{ |
|
avgAngle += (i + 1) * M_PI / 2; |
|
} |
|
else |
|
{ |
|
avgAngle -= (i + 1) * M_PI / 2; |
|
} |
|
|
|
// test if r1 points away from r2 rather than towards it |
|
r1->reversed = ((r1->x2 - r1->x1) * (r2->xcenter - r1->xcenter) + (r1->y2 - r1->y1) * (r2->ycenter - r1->ycenter)) < 0; |
|
} |
|
for (int i = 0; i <= 3; i++) |
|
{ |
|
RecoSegment* r1 = &rs[i]; |
|
RecoSegment* r2 = &rs[(i + 1) % 4]; |
|
double dist = hypot((r1->reversed ? r1->x1 : r1->x2) - (r2->reversed ? r2->x2 : r2->x1), |
|
(r1->reversed ? r1->y1 : r1->y2) - (r2->reversed ? r2->y2 : r2->y1)); |
|
if (dist > RECTANGLE_LINEAR_TOLERANCE * (r1->radius + r2->radius)) |
|
{ |
|
return NULL; |
|
} |
|
} |
|
|
|
// make a rectangle of the correct size and slope |
|
avgAngle = avgAngle / 4; |
|
if (fabs(avgAngle) < SLANT_TOLERANCE) |
|
{ |
|
avgAngle = 0.; |
|
} |
|
|
|
if (fabs(avgAngle) > M_PI / 2 - SLANT_TOLERANCE) |
|
{ |
|
avgAngle = M_PI / 2; |
|
} |
|
|
|
Stroke* s = new Stroke(); |
|
s->applyStyleFrom(this->stroke); |
|
|
|
for (int i = 0; i <= 3; i++) |
|
{ |
|
rs[i].angle = avgAngle + i * M_PI / 2; |
|
} |
|
|
|
for (int i = 0; i <= 3; i++) |
|
{ |
|
Point p = rs[i].calcEdgeIsect(&rs[(i + 1) % 4]); |
|
s->addPoint(p); |
|
} |
|
|
|
s->addPoint(s->getPoint(0)); |
|
|
|
return s; |
|
} |
|
|
|
Stroke* ShapeRecognizer::tryArrow() |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
bool rev[3]; |
|
|
|
// first, we need whole strokes to combine to nsides segments... |
|
if (queueLength < 3) |
|
{ |
|
return NULL; |
|
} |
|
|
|
RecoSegment* rs = &this->queue[queueLength - 3]; |
|
if (rs->startpt != 0) |
|
{ |
|
return NULL; |
|
} |
|
|
|
// check arrow head not too big, and orient main segment |
|
for (int i = 1; i <= 2; i++) |
|
{ |
|
if (rs[i].radius > ARROW_MAXSIZE * rs[0].radius) |
|
{ |
|
return NULL; |
|
} |
|
|
|
rev[i] = hypot(rs[i].xcenter - rs->x1, rs[i].ycenter - rs->y1) < hypot(rs[i].xcenter - rs->x2, rs[i].ycenter - rs->y2); |
|
} |
|
|
|
if (rev[1] != rev[2]) |
|
{ |
|
return NULL; |
|
} |
|
|
|
double x1; |
|
double y1; |
|
double x2; |
|
double y2; |
|
double angle; |
|
|
|
if (rev[1]) |
|
{ |
|
x1 = rs->x2; |
|
y1 = rs->y2; |
|
x2 = rs->x1; |
|
y2 = rs->y1; |
|
angle = rs->angle + M_PI; |
|
} |
|
else |
|
{ |
|
x1 = rs->x1; |
|
y1 = rs->y1; |
|
x2 = rs->x2; |
|
y2 = rs->y2; |
|
angle = rs->angle; |
|
} |
|
|
|
double alpha[3]; |
|
// check arrow head not too big, and angles roughly ok |
|
for (int i = 1; i <= 2; i++) |
|
{ |
|
rs[i].reversed = FALSE; |
|
alpha[i] = rs[i].angle - angle; |
|
while (alpha[i] < -M_PI / 2) |
|
{ |
|
alpha[i] += M_PI; |
|
rs[i].reversed = !rs[i].reversed; |
|
} |
|
while (alpha[i] > M_PI / 2) |
|
{ |
|
alpha[i] -= M_PI; |
|
rs[i].reversed = !rs[i].reversed; |
|
} |
|
RDEBUG("arrow: alpha[%d] = %.1f degrees", i, (alpha[i] * 180 / M_PI)); |
|
if (fabs(alpha[i]) < ARROW_ANGLE_MIN || fabs(alpha[i]) > ARROW_ANGLE_MAX) |
|
{ |
|
return NULL; |
|
} |
|
} |
|
|
|
// check arrow head segments are roughly symmetric |
|
if (alpha[1] * alpha[2] > 0 || fabs(alpha[1] + alpha[2]) > ARROW_ASYMMETRY_MAX_ANGLE) |
|
{ |
|
return NULL; |
|
} |
|
|
|
if (rs[1].radius / rs[2].radius > 1 + ARROW_ASYMMETRY_MAX_LINEAR) |
|
{ |
|
return NULL; |
|
} |
|
|
|
if (rs[2].radius / rs[1].radius > 1 + ARROW_ASYMMETRY_MAX_LINEAR) |
|
{ |
|
return NULL; |
|
} |
|
|
|
// check vertices roughly match |
|
Point pt = rs[1].calcEdgeIsect(&rs[2]); |
|
for (int j = 1; j <= 2; j++) |
|
{ |
|
double dist = hypot(pt.x - (rs[j].reversed ? rs[j].x1 : rs[j].x2), |
|
pt.y - (rs[j].reversed ? rs[j].y1 : rs[j].y2)); |
|
RDEBUG("linear tolerance: tip[%d] = %.2f", j, (dist / rs[j].radius)); |
|
if (dist > ARROW_TIP_LINEAR_TOLERANCE * rs[j].radius) |
|
{ |
|
return NULL; |
|
} |
|
} |
|
|
|
double dist = (pt.x - x2) * sin(angle) - (pt.y - y2) * cos(angle); |
|
dist /= rs[1].radius + rs[2].radius; |
|
|
|
RDEBUG("sideways gap tolerance = %.2f", dist); |
|
|
|
if (fabs(dist) > ARROW_SIDEWAYS_GAP_TOLERANCE) |
|
{ |
|
return NULL; |
|
} |
|
|
|
dist = (pt.x - x2) * cos(angle) + (pt.y - y2) * sin(angle); |
|
dist /= rs[1].radius + rs[2].radius; |
|
|
|
RDEBUG("main linear gap = %.2f", dist); |
|
|
|
if (dist < ARROW_MAIN_LINEAR_GAP_MIN || dist > ARROW_MAIN_LINEAR_GAP_MAX) |
|
{ |
|
return NULL; |
|
} |
|
|
|
// make an arrow of the correct size and slope |
|
if (fabs(rs->angle) < SLANT_TOLERANCE) // nearly horizontal |
|
{ |
|
angle = angle - rs->angle; |
|
y1 = y2 = rs->ycenter; |
|
} |
|
|
|
if (rs->angle > M_PI / 2 - SLANT_TOLERANCE) // nearly vertical |
|
{ |
|
angle = angle - (rs->angle - M_PI / 2); |
|
x1 = x2 = rs->xcenter; |
|
} |
|
|
|
if (rs->angle < -M_PI / 2 + SLANT_TOLERANCE) // nearly vertical |
|
{ |
|
angle = angle - (rs->angle + M_PI / 2); |
|
x1 = x2 = rs->xcenter; |
|
} |
|
|
|
double delta = fabs(alpha[1] - alpha[2]) / 2; |
|
dist = (hypot(rs[1].x1 - rs[1].x2, rs[1].y1 - rs[1].y2) + hypot(rs[2].x1 - rs[2].x2, rs[2].y1 - rs[2].y2)) / 2; |
|
|
|
Stroke* s = new Stroke(); |
|
s->applyStyleFrom(this->stroke); |
|
|
|
s->addPoint(Point(x1, y1)); |
|
s->addPoint(Point(x2, y2)); |
|
|
|
s->addPoint(Point(x2 - dist * cos(angle + delta), y2 - dist * sin(angle + delta))); |
|
s->addPoint(Point(x2, y2)); |
|
|
|
s->addPoint(Point(x2 - dist * cos(angle - delta), y2 - dist * sin(angle - delta))); |
|
|
|
return s; |
|
} |
|
|
|
/* |
|
* check if something is a polygonal line with at most nsides sides |
|
*/ |
|
int ShapeRecognizer::findPolygonal(const Point* pt, int start, int end, int nsides, int* breaks, Inertia* ss) |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
Inertia s; |
|
int i1, i2, n1, n2; |
|
|
|
if (end == start) |
|
{ |
|
return 0; // no way |
|
} |
|
|
|
if (nsides <= 0) |
|
{ |
|
return 0; |
|
} |
|
|
|
if (end - start < 5) |
|
{ |
|
nsides = 1; // too small for a polygon |
|
} |
|
|
|
// look for a linear piece that's big enough |
|
int k = 0; |
|
for (; k < nsides; k++) |
|
{ |
|
i1 = start + (k * (end - start)) / nsides; |
|
i2 = start + ((k + 1) * (end - start)) / nsides; |
|
s.calc(pt, i1, i2); |
|
if (s.det() < LINE_MAX_DET) |
|
{ |
|
break; |
|
} |
|
} |
|
if (k == nsides) |
|
{ |
|
return 0; // failed! |
|
} |
|
|
|
double det1; |
|
double det2; |
|
Inertia s1; |
|
Inertia s2; |
|
|
|
// grow the linear piece we found |
|
while (true) |
|
{ |
|
if (i1 > start) |
|
{ |
|
s1 = s; |
|
s1.increase(pt[i1 - 1], pt[i1], 1); |
|
det1 = s1.det(); |
|
} |
|
else |
|
{ |
|
det1 = 1.0; |
|
} |
|
|
|
if (i2 < end) |
|
{ |
|
s2 = s; |
|
s2.increase(pt[i2], pt[i2 + 1], 1); |
|
det2 = s2.det(); |
|
} |
|
else |
|
{ |
|
det2 = 1.0; |
|
} |
|
|
|
if (det1 < det2 && det1 < LINE_MAX_DET) |
|
{ |
|
i1--; |
|
s = s1; |
|
} |
|
else if (det2 < det1 && det2 < LINE_MAX_DET) |
|
{ |
|
i2++; |
|
s = s2; |
|
} |
|
else |
|
{ |
|
break; |
|
} |
|
} |
|
|
|
if (i1 > start) |
|
{ |
|
n1 = findPolygonal(pt, start, i1, (i2 == end) ? (nsides - 1) : (nsides - 2), breaks, ss); |
|
if (n1 == 0) |
|
{ |
|
return 0; // it doesn't work |
|
} |
|
} |
|
else |
|
{ |
|
n1 = 0; |
|
} |
|
|
|
breaks[n1] = i1; |
|
breaks[n1 + 1] = i2; |
|
ss[n1] = s; |
|
|
|
if (i2 < end) |
|
{ |
|
n2 = findPolygonal(pt, i2, end, nsides - n1 - 1, breaks + n1 + 1, ss + n1 + 1); |
|
if (n2 == 0) |
|
{ |
|
return 0; |
|
} |
|
} |
|
else |
|
{ |
|
n2 = 0; |
|
} |
|
|
|
return n1 + n2 + 1; |
|
} |
|
|
|
/** |
|
* Improve on the polygon found by find_polygonal() |
|
*/ |
|
void ShapeRecognizer::optimizePolygonal(const Point* pt, int nsides, int* breaks, Inertia* ss) |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
for (int i = 1; i < nsides; i++) |
|
{ |
|
// optimize break between sides i and i+1 |
|
double cost = ss[i - 1].det() * ss[i - 1].det() + ss[i].det() * ss[i].det(); |
|
Inertia s1 = ss[i - 1]; |
|
Inertia s2 = ss[i]; |
|
bool improved = false; |
|
while (breaks[i] > breaks[i - 1] + 1) |
|
{ |
|
// try moving the break to the left |
|
s1.increase(pt[breaks[i] - 1], pt[breaks[i] - 2], -1); |
|
s2.increase(pt[breaks[i] - 1], pt[breaks[i] - 2], 1); |
|
double newcost = s1.det() * s1.det() + s2.det() * s2.det(); |
|
|
|
if (newcost >= cost) |
|
{ |
|
break; |
|
} |
|
|
|
improved = true; |
|
cost = newcost; |
|
breaks[i]--; |
|
ss[i - 1] = s1; |
|
ss[i] = s2; |
|
} |
|
|
|
if (improved) |
|
{ |
|
continue; |
|
} |
|
|
|
s1 = ss[i - 1]; |
|
s2 = ss[i]; |
|
while (breaks[i] < breaks[i + 1] - 1) |
|
{ |
|
// try moving the break to the right |
|
s1.increase(pt[breaks[i]], pt[breaks[i] + 1], 1); |
|
s2.increase(pt[breaks[i]], pt[breaks[i] + 1], -1); |
|
|
|
double newcost = s1.det() * s1.det() + s2.det() * s2.det(); |
|
if (newcost >= cost) |
|
{ |
|
break; |
|
} |
|
|
|
cost = newcost; |
|
breaks[i]++; |
|
ss[i - 1] = s1; |
|
ss[i] = s2; |
|
} |
|
} |
|
} |
|
|
|
Stroke* ShapeRecognizer::tryClosedPolygon(int nsides) |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
//to eliminate bug #52, remove this until it's perfected |
|
return NULL; |
|
|
|
/* |
|
RecoSegment* r1 = NULL; |
|
RecoSegment* r2 = NULL; |
|
|
|
// first, we need whole strokes to combine to nsides segments... |
|
if (this->queueLength < nsides) |
|
{ |
|
return NULL; |
|
} |
|
|
|
RecoSegment* rs = &this->queue[this->queueLength - nsides]; |
|
if (rs->startpt != 0) |
|
{ |
|
return NULL; |
|
} |
|
|
|
// check vertices roughly match |
|
for (int i = 0; i < nsides; i++) |
|
{ |
|
r1 = rs + i; |
|
r2 = rs + (i + 1) % nsides; |
|
// test if r1 points away from r2 rather than towards it |
|
Point pt = r1->calcEdgeIsect(r2); |
|
r1->reversed = (hypot(pt.x - r1->x1, pt.y - r1->y1) < hypot(pt.x - r1->x2, pt.y - r1->y2)); |
|
} |
|
|
|
for (int i = 0; i < nsides; i++) |
|
{ |
|
r1 = rs + i; |
|
r2 = rs + (i + 1) % nsides; |
|
Point pt = r1->calcEdgeIsect(r2); |
|
double dist = hypot((r1->reversed ? r1->x1 : r1->x2) - pt.x, |
|
(r1->reversed ? r1->y1 : r1->y2) - pt.y) |
|
+ hypot((r2->reversed ? r2->x2 : r2->x1) - pt.x, |
|
(r2->reversed ? r2->y2 : r2->y1) - pt.y); |
|
if (dist > POLYGON_LINEAR_TOLERANCE * (r1->radius + r2->radius)) |
|
{ |
|
return NULL; |
|
} |
|
} |
|
|
|
Stroke* s = new Stroke(); |
|
s->applyStyleFrom(this->stroke); |
|
|
|
for (int i = 0; i < nsides; i++) |
|
{ |
|
Point p = rs[i].calcEdgeIsect(&rs[(i + 1) % nsides]); |
|
s->addPoint(p); |
|
} |
|
|
|
s->addPoint(s->getPoint(0)); |
|
|
|
return s; |
|
*/ |
|
} |
|
|
|
/** |
|
* The main pattern recognition function |
|
*/ |
|
ShapeRecognizerResult* ShapeRecognizer::recognizePatterns(Stroke* stroke) |
|
{ |
|
XOJ_CHECK_TYPE(ShapeRecognizer); |
|
|
|
this->stroke = stroke; |
|
|
|
if (stroke->getPointCount() < 3) |
|
{ |
|
return NULL; |
|
} |
|
|
|
Inertia ss[4]; |
|
int brk[5] = {0}; |
|
|
|
// first see if it's a polygon |
|
int n = findPolygonal(stroke->getPoints(), 0, stroke->getPointCount() - 1, MAX_POLYGON_SIDES, brk, ss); |
|
if (n > 0) |
|
{ |
|
optimizePolygonal(stroke->getPoints(), n, brk, ss); |
|
#ifdef DEBUG_RECOGNIZER |
|
g_message("--"); |
|
g_message("ShapeReco:: Polygon, %d edges:", n); |
|
for (int i = 0; i < n; i++) |
|
{ |
|
g_message("ShapeReco:: %d-%d (M=%.0f, det=%.4f)", brk[i], brk[i + 1], ss[i].getMass(), ss[i].det()); |
|
} |
|
g_message("--"); |
|
#endif |
|
// update recognizer segment queue (most recent at end) |
|
while (n + queueLength > MAX_POLYGON_SIDES) |
|
{ |
|
// remove oldest polygonal stroke |
|
int i = 1; |
|
while (i < queueLength && queue[i].startpt != 0) |
|
{ |
|
i++; |
|
} |
|
queueLength -= i; |
|
g_memmove(queue, queue + i, queueLength * sizeof(RecoSegment)); |
|
} |
|
|
|
RDEBUG("Queue now has %i + %i edges", this->queueLength, n); |
|
|
|
RecoSegment* rs = &this->queue[this->queueLength]; |
|
this->queueLength += n; |
|
|
|
for (int i = 0; i < n; i++) |
|
{ |
|
rs[i].startpt = brk[i]; |
|
rs[i].endpt = brk[i + 1]; |
|
rs[i].calcSegmentGeometry(stroke->getPoints(), brk[i], brk[i + 1], ss + i); |
|
} |
|
|
|
Stroke* tmp = NULL; |
|
|
|
if ((tmp = tryRectangle()) != NULL) |
|
{ |
|
ShapeRecognizerResult* result = new ShapeRecognizerResult(tmp, this); |
|
resetRecognizer(); |
|
RDEBUG("return tryRectangle()"); |
|
return result; |
|
} |
|
|
|
// if ((tmp = tryArrow()) != NULL) |
|
// { |
|
// ShapeRecognizerResult* result = new ShapeRecognizerResult(tmp, this); |
|
// resetRecognizer(); |
|
// RDEBUG("return tryArrow()"); |
|
// return result; |
|
// } |
|
// |
|
// if ((tmp = tryClosedPolygon(3)) != NULL) |
|
// { |
|
// ShapeRecognizerResult* result = new ShapeRecognizerResult(tmp, this); |
|
// RDEBUG("return tryClosedPolygon(3)"); |
|
// resetRecognizer(); |
|
// return result; |
|
// } |
|
// |
|
// if ((tmp = tryClosedPolygon(4)) != NULL) |
|
// { |
|
// ShapeRecognizerResult* result = new ShapeRecognizerResult(tmp, this); |
|
// RDEBUG("return tryClosedPolygon(4)"); |
|
// resetRecognizer(); |
|
// return result; |
|
// } |
|
|
|
|
|
// Removed complicated recognition |
|
|
|
if (n == 1) // current stroke is a line |
|
{ |
|
if (fabs(rs->angle) < SLANT_TOLERANCE) // nearly horizontal |
|
{ |
|
rs->angle = 0.0; |
|
rs->y1 = rs->y2 = rs->ycenter; |
|
} |
|
if (fabs(rs->angle) > M_PI / 2 - SLANT_TOLERANCE) // nearly vertical |
|
{ |
|
rs->angle = (rs->angle > 0) ? (M_PI / 2) : (-M_PI / 2); |
|
rs->x1 = rs->x2 = rs->xcenter; |
|
} |
|
|
|
Stroke* s = new Stroke(); |
|
s->applyStyleFrom(this->stroke); |
|
|
|
s->addPoint(Point(rs->x1, rs->y1)); |
|
s->addPoint(Point(rs->x2, rs->y2)); |
|
rs->stroke = s; |
|
ShapeRecognizerResult* result = new ShapeRecognizerResult(s); |
|
RDEBUG("return line"); |
|
return result; |
|
} |
|
} |
|
|
|
// not a polygon: maybe a circle ? |
|
Stroke* s = CircleRecognizer::recognize(stroke); |
|
if (s) |
|
{ |
|
RDEBUG("return circle"); |
|
return new ShapeRecognizerResult(s); |
|
} |
|
|
|
return NULL; |
|
}
|
|
|