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

#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;
}