8 #include <libgnomecanvas/libgnomecanvas.h>
11 #include "xo-shapes.h"
14 typedef struct Inertia {
15 double mass, sx, sy, sxx, sxy, syy;
18 typedef struct RecoSegment {
21 double xcenter, ycenter, angle, radius;
22 double x1, y1, x2, y2;
26 struct RecoSegment recognizer_queue[MAX_POLYGON_SIDES+1];
27 int recognizer_queue_length;
28 struct UndoItem *last_item_checker; // check if queue is stale
30 void reset_recognizer(void)
32 recognizer_queue_length = 0;
33 last_item_checker = NULL;
36 /* compute mass and moments of a stroke */
38 void incr_inertia(double *pt, struct Inertia *s, int coef)
41 dm = coef*hypot(pt[2]-pt[0], pt[3]-pt[1]);
45 s->sxx += dm*pt[0]*pt[0];
46 s->syy += dm*pt[1]*pt[1];
47 s->sxy += dm*pt[0]*pt[1];
50 void calc_inertia(double *pt, int start, int end, struct Inertia *s)
54 s->mass = s->sx = s->sy = s->sxx = s->sxy = s->syy = 0.;
55 for (i=start, pt+=2*start; i<end; i++, pt+=2) incr_inertia(pt, s, 1);
58 /* compute normalized quantities */
60 inline double center_x(struct Inertia s)
65 inline double center_y(struct Inertia s)
70 inline double I_xx(struct Inertia s)
72 if (s.mass <= 0.) return 0.;
73 return (s.sxx - s.sx*s.sx/s.mass)/s.mass;
76 inline double I_xy(struct Inertia s)
78 if (s.mass <= 0.) return 0.;
79 return (s.sxy - s.sx*s.sy/s.mass)/s.mass;
82 inline double I_yy(struct Inertia s)
84 if (s.mass <= 0.) return 0.;
85 return (s.syy - s.sy*s.sy/s.mass)/s.mass;
88 inline double I_rad(struct Inertia s)
90 double ixx = I_xx(s), iyy = I_yy(s);
91 if (ixx+iyy <= 0.) return 0.;
95 inline double I_det(struct Inertia s)
97 double ixx = I_xx(s), iyy = I_yy(s), ixy = I_xy(s);
98 if (s.mass <= 0.) return 0.;
99 if (ixx+iyy <= 0.) return 0.;
100 return 4*(ixx*iyy-ixy*ixy)/(ixx+iyy)/(ixx+iyy);
103 /* check if something is a polygonal line with at most nsides sides */
105 int find_polygonal(double *pt, int start, int end, int nsides, int *breaks, struct Inertia *ss)
107 struct Inertia s, s1, s2;
108 int k, i1, i2, n1, n2;
111 if (end == start) return 0; // no way
112 if (nsides <= 0) return 0;
113 if (end-start<5) nsides = 1; // too small for a polygon
115 // look for a linear piece that's big enough
116 for (k=0; k<nsides; k++) {
117 i1 = start + (k*(end-start))/nsides;
118 i2 = start + ((k+1)*(end-start))/nsides;
119 calc_inertia(pt, i1, i2, &s);
120 if (I_det(s) < LINE_MAX_DET) break;
122 if (k==nsides) return 0; // failed!
124 // grow the linear piece we found
128 incr_inertia(pt+2*(i1-1), &s1, 1);
134 incr_inertia(pt+2*i2, &s2, 1);
138 if (det1<det2 && det1<LINE_MAX_DET) { i1--; s=s1; }
139 else if (det2<det1 && det2<LINE_MAX_DET) { i2++; s=s2; }
144 n1 = find_polygonal(pt, start, i1, (i2==end)?(nsides-1):(nsides-2), breaks, ss);
145 if (n1 == 0) return 0; // it doesn't work
154 n2 = find_polygonal(pt, i2, end, nsides-n1-1, breaks+n1+1, ss+n1+1);
155 if (n2 == 0) return 0;
162 /* improve on the polygon found by find_polygonal() */
164 void optimize_polygonal(double *pt, int nsides, int *breaks, struct Inertia *ss)
167 double cost, newcost;
168 struct Inertia s1, s2;
171 for (i=1; i<nsides; i++) {
172 // optimize break between sides i and i+1
173 cost = I_det(ss[i-1])*I_det(ss[i-1])+I_det(ss[i])*I_det(ss[i]);
174 s1 = ss[i-1]; s2 = ss[i];
176 while (breaks[i]>breaks[i-1]+1) {
177 // try moving the break to the left
178 incr_inertia(pt+2*(breaks[i]-1), &s1, -1);
179 incr_inertia(pt+2*(breaks[i]-1), &s2, 1);
180 newcost = I_det(s1)*I_det(s1)+I_det(s2)*I_det(s2);
181 if (newcost >= cost) break;
188 if (improved) continue;
189 s1 = ss[i-1]; s2 = ss[i];
190 while (breaks[i]<breaks[i+1]-1) {
191 // try moving the break to the right
192 incr_inertia(pt+2*breaks[i], &s1, 1);
193 incr_inertia(pt+2*breaks[i], &s2, -1);
194 newcost = I_det(s1)*I_det(s1)+I_det(s2)*I_det(s2);
195 if (newcost >= cost) break;
204 /* find the geometry of a recognized segment */
206 void get_segment_geometry(double *pt, int start, int end, struct Inertia *s, struct RecoSegment *r)
208 double a, b, c, lmin, lmax, l;
211 r->xcenter = center_x(*s);
212 r->ycenter = center_y(*s);
213 a = I_xx(*s); b = I_xy(*s); c = I_yy(*s);
214 /* max angle for inertia quadratic form solves: tan(2t) = 2b/(a-c) */
215 r->angle = atan2(2*b, a-c)/2;
216 r->radius = sqrt(3*(a+c));
219 for (i=start, pt+=2*start; i<=end; i++, pt+=2) {
220 l = (pt[0]-r->xcenter)*cos(r->angle)+(pt[1]-r->ycenter)*sin(r->angle);
221 if (l<lmin) lmin = l;
222 if (l>lmax) lmax = l;
224 r->x1 = r->xcenter + lmin*cos(r->angle);
225 r->y1 = r->ycenter + lmin*sin(r->angle);
226 r->x2 = r->xcenter + lmax*cos(r->angle);
227 r->y2 = r->ycenter + lmax*sin(r->angle);
230 /* test if we have a circle; inertia has been precomputed by caller */
232 double score_circle(double *pt, int start, int end, struct Inertia *s)
234 double sum, x0, y0, r0, dm, deltar;
237 if (s->mass == 0.) return 0;
239 x0 = center_x(*s); y0 = center_y(*s); r0 = I_rad(*s);
240 for (i=start, pt+=2*start; i<end; i++, pt+=2) {
241 dm = hypot(pt[2]-pt[0], pt[3]-pt[1]);
242 deltar = hypot(pt[0]-x0, pt[1]-y0) - r0;
243 sum += dm * fabs(deltar);
245 return sum/(s->mass*r0);
248 /* replace strokes by various shapes */
250 void make_circle_shape(double x0, double y0, double r)
254 struct UndoErasureData *erasure;
257 if (npts<12) npts = 12; // min. number of points
258 realloc_cur_path(npts+1);
259 ui.cur_path.num_points = npts+1;
260 for (i=0;i<=npts; i++) {
261 ui.cur_path.coords[2*i] = x0 + r*cos((2*M_PI*i)/npts);
262 ui.cur_path.coords[2*i+1] = y0 + r*sin((2*M_PI*i)/npts);
266 void calc_edge_isect(struct RecoSegment *r1, struct RecoSegment *r2, double *pt)
269 t = (r2->xcenter - r1->xcenter) * sin(r2->angle) -
270 (r2->ycenter - r1->ycenter) * cos(r2->angle);
271 t /= sin(r2->angle-r1->angle);
272 pt[0] = r1->xcenter + t*cos(r1->angle);
273 pt[1] = r1->ycenter + t*sin(r1->angle);
276 void remove_recognized_strokes(struct RecoSegment *rs, int num_old_items)
278 struct Item *old_item;
280 struct UndoErasureData *erasure;
284 undo->type = ITEM_RECOGNIZER;
285 undo->layer = ui.cur_layer;
286 undo->erasurelist = NULL;
289 for (i=0; i<num_old_items; i++) {
290 if (rs[i].item == old_item) continue; // already done
291 old_item = rs[i].item;
292 erasure = g_new(struct UndoErasureData, 1);
293 erasure->item = old_item;
294 erasure->npos = g_list_index(ui.cur_layer->items, old_item) + (shift++);
296 erasure->replacement_items = NULL;
297 undo->erasurelist = g_list_append(undo->erasurelist, erasure);
298 if (old_item->canvas_item != NULL)
299 gtk_object_destroy(GTK_OBJECT(old_item->canvas_item));
300 ui.cur_layer->items = g_list_remove(ui.cur_layer->items, old_item);
301 ui.cur_layer->nitems--;
305 struct Item *insert_recognized_curpath(void)
309 struct UndoErasureData *erasure;
311 erasure = (struct UndoErasureData *)(undo->erasurelist->data);
312 item = g_new(struct Item, 1);
313 item->type = ITEM_STROKE;
314 g_memmove(&(item->brush), &(erasure->item->brush), sizeof(struct Brush));
315 item->brush.variable_width = FALSE;
316 subdivide_cur_path();
317 item->path = gnome_canvas_points_new(ui.cur_path.num_points);
318 g_memmove(item->path->coords, ui.cur_path.coords, 2*ui.cur_path.num_points*sizeof(double));
320 update_item_bbox(item);
321 ui.cur_path.num_points = 0;
324 erasure->replacement_items = g_list_append(erasure->replacement_items, item);
325 ui.cur_layer->items = g_list_append(ui.cur_layer->items, item);
326 ui.cur_layer->nitems++;
327 make_canvas_item_one(ui.cur_layer->group, item);
332 /* test if segments form standard shapes */
334 gboolean try_rectangle(void)
336 struct RecoSegment *rs, *r1, *r2;
338 double dist, avg_angle;
340 // first, we need whole strokes to combine to 4 segments...
341 if (recognizer_queue_length<4) return FALSE;
342 rs = recognizer_queue + recognizer_queue_length - 4;
343 if (rs->startpt!=0) return FALSE;
345 // check edges make angles ~= Pi/2 and vertices roughly match
347 for (i=0; i<=3; i++) {
348 r1 = rs+i; r2 = rs+(i+1)%4;
349 if (fabs(fabs(r1->angle-r2->angle)-M_PI/2) > RECTANGLE_ANGLE_TOLERANCE)
351 avg_angle += r1->angle;
352 if (r2->angle > r1->angle) avg_angle += (i+1)*M_PI/2;
353 else avg_angle -= (i+1)*M_PI/2;
354 // test if r1 points away from r2 rather than towards it
355 r1->reversed = ((r1->x2-r1->x1)*(r2->xcenter-r1->xcenter)+
356 (r1->y2-r1->y1)*(r2->ycenter-r1->ycenter)) < 0;
358 for (i=0; i<=3; i++) {
359 r1 = rs+i; r2 = rs+(i+1)%4;
360 dist = hypot((r1->reversed?r1->x1:r1->x2) - (r2->reversed?r2->x2:r2->x1),
361 (r1->reversed?r1->y1:r1->y2) - (r2->reversed?r2->y2:r2->y1));
362 if (dist > RECTANGLE_LINEAR_TOLERANCE*(r1->radius+r2->radius)) return FALSE;
365 // make a rectangle of the correct size and slope
366 avg_angle = avg_angle/4;
367 if (fabs(avg_angle)<SLANT_TOLERANCE) avg_angle = 0.;
368 if (fabs(avg_angle)>M_PI/2-SLANT_TOLERANCE) avg_angle = M_PI/2;
370 ui.cur_path.num_points = 5;
371 for (i=0; i<=3; i++) rs[i].angle = avg_angle+i*M_PI/2;
372 for (i=0; i<=3; i++) calc_edge_isect(rs+i, rs+(i+1)%4, ui.cur_path.coords+2*i+2);
373 ui.cur_path.coords[0] = ui.cur_path.coords[8];
374 ui.cur_path.coords[1] = ui.cur_path.coords[9];
376 remove_recognized_strokes(rs, 4);
377 insert_recognized_curpath();
381 gboolean try_arrow(void)
383 struct RecoSegment *rs;
385 double alpha[3], dist, pt[2], tmp, delta;
386 double x1, y1, x2, y2, angle;
389 // first, we need whole strokes to combine to nsides segments...
390 if (recognizer_queue_length<3) return FALSE;
391 rs = recognizer_queue + recognizer_queue_length - 3;
392 if (rs->startpt!=0) return FALSE;
394 // check arrow head not too big, and orient main segment
395 for (i=1; i<=2; i++) {
396 if (rs[i].radius > ARROW_MAXSIZE*rs[0].radius) return FALSE;
397 rev[i] = (hypot(rs[i].xcenter-rs->x1, rs[i].ycenter-rs->y1) <
398 hypot(rs[i].xcenter-rs->x2, rs[i].ycenter-rs->y2));
400 if (rev[1]!=rev[2]) return FALSE;
402 x1 = rs->x2; y1 = rs->y2; x2 = rs->x1; y2 = rs->y1;
403 angle = rs->angle + M_PI;
406 x1 = rs->x1; y1 = rs->y1; x2 = rs->x2; y2 = rs->y2;
410 // check arrow head not too big, and angles roughly ok
411 for (i=1; i<=2; i++) {
412 rs[i].reversed = FALSE;
413 alpha[i] = rs[i].angle - angle;
414 while (alpha[i]<-M_PI/2) { alpha[i]+=M_PI; rs[i].reversed = !rs[i].reversed; }
415 while (alpha[i]>M_PI/2) { alpha[i]-=M_PI; rs[i].reversed = !rs[i].reversed; }
416 #ifdef RECOGNIZER_DEBUG
417 printf("DEBUG: arrow: alpha[%d] = %.1f degrees\n", i, alpha[i]*180/M_PI);
419 if (fabs(alpha[i])<ARROW_ANGLE_MIN || fabs(alpha[i])>ARROW_ANGLE_MAX) return FALSE;
422 // check arrow head segments are roughly symmetric
423 if (alpha[1]*alpha[2]>0 || fabs(alpha[1]+alpha[2]) > ARROW_ASYMMETRY_MAX_ANGLE) return FALSE;
424 if (rs[1].radius/rs[2].radius > 1+ARROW_ASYMMETRY_MAX_LINEAR) return FALSE;
425 if (rs[2].radius/rs[1].radius > 1+ARROW_ASYMMETRY_MAX_LINEAR) return FALSE;
427 // check vertices roughly match
428 calc_edge_isect(rs+1, rs+2, pt);
429 for (j=1; j<=2; j++) {
430 dist = hypot(pt[0]-(rs[j].reversed?rs[j].x1:rs[j].x2),
431 pt[1]-(rs[j].reversed?rs[j].y1:rs[j].y2));
432 #ifdef RECOGNIZER_DEBUG
433 printf("DEBUG: linear tolerance: tip[%d] = %.2f\n", j, dist/rs[j].radius);
435 if (dist>ARROW_TIP_LINEAR_TOLERANCE*rs[j].radius) return FALSE;
437 dist = (pt[0]-x2)*sin(angle)-(pt[1]-y2)*cos(angle);
438 dist /= rs[1].radius + rs[2].radius;
439 #ifdef RECOGNIZER_DEBUG
440 printf("DEBUG: sideways gap tolerance = %.2f\n", dist);
442 if (fabs(dist)>ARROW_SIDEWAYS_GAP_TOLERANCE) return FALSE;
443 dist = (pt[0]-x2)*cos(angle)+(pt[1]-y2)*sin(angle);
444 dist /= rs[1].radius + rs[2].radius;
445 #ifdef RECOGNIZER_DEBUG
446 printf("DEBUG: main linear gap = %.2f\n", dist);
448 if (dist<ARROW_MAIN_LINEAR_GAP_MIN || dist>ARROW_MAIN_LINEAR_GAP_MAX) return FALSE;
450 // make an arrow of the correct size and slope
451 if (fabs(rs->angle)<SLANT_TOLERANCE) { // nearly horizontal
452 angle = angle - rs->angle;
453 y1 = y2 = rs->ycenter;
455 if (rs->angle>M_PI/2-SLANT_TOLERANCE) { // nearly vertical
456 angle = angle - (rs->angle-M_PI/2);
457 x1 = x2 = rs->xcenter;
459 if (rs->angle<-M_PI/2+SLANT_TOLERANCE) { // nearly vertical
460 angle = angle - (rs->angle+M_PI/2);
461 x1 = x2 = rs->xcenter;
463 delta = fabs(alpha[1]-alpha[2])/2;
464 dist = (hypot(rs[1].x1-rs[1].x2, rs[1].y1-rs[1].y2) +
465 hypot(rs[2].x1-rs[2].x2, rs[2].y1-rs[2].y2))/2;
468 ui.cur_path.num_points = 2;
469 ui.cur_path.coords[0] = x1; ui.cur_path.coords[1] = y1;
470 ui.cur_path.coords[2] = x2; ui.cur_path.coords[3] = y2;
471 remove_recognized_strokes(rs, 3);
472 insert_recognized_curpath();
475 ui.cur_path.num_points = 3;
476 ui.cur_path.coords[0] = x2 - dist*cos(angle+delta);
477 ui.cur_path.coords[1] = y2 - dist*sin(angle+delta);
478 ui.cur_path.coords[2] = x2;
479 ui.cur_path.coords[3] = y2;
480 ui.cur_path.coords[4] = x2 - dist*cos(angle-delta);
481 ui.cur_path.coords[5] = y2 - dist*sin(angle-delta);
482 insert_recognized_curpath();
487 gboolean try_closed_polygon(int nsides)
489 struct RecoSegment *rs, *r1, *r2;
493 // first, we need whole strokes to combine to nsides segments...
494 if (recognizer_queue_length<nsides) return FALSE;
495 rs = recognizer_queue + recognizer_queue_length - nsides;
496 if (rs->startpt!=0) return FALSE;
498 // check vertices roughly match
499 for (i=0; i<nsides; i++) {
500 r1 = rs+i; r2 = rs+(i+1)%nsides;
501 // test if r1 points away from r2 rather than towards it
502 calc_edge_isect(r1, r2, pt);
503 r1->reversed = (hypot(pt[0]-r1->x1,pt[1]-r1->y1) < hypot(pt[0]-r1->x2,pt[1]-r1->y2));
505 for (i=0; i<nsides; i++) {
506 r1 = rs+i; r2 = rs+(i+1)%nsides;
507 calc_edge_isect(r1, r2, pt);
508 dist = hypot((r1->reversed?r1->x1:r1->x2)-pt[0],(r1->reversed?r1->y1:r1->y2)-pt[1])
509 + hypot((r2->reversed?r2->x2:r2->x1)-pt[0],(r2->reversed?r2->y2:r2->y1)-pt[1]);
510 if (dist > POLYGON_LINEAR_TOLERANCE*(r1->radius+r2->radius)) return FALSE;
513 // make a polygon of the correct size and slope
514 realloc_cur_path(nsides+1);
515 ui.cur_path.num_points = nsides+1;
516 for (i=0; i<nsides; i++)
517 calc_edge_isect(rs+i, rs+(i+1)%nsides, ui.cur_path.coords+2*i+2);
518 ui.cur_path.coords[0] = ui.cur_path.coords[2*nsides];
519 ui.cur_path.coords[1] = ui.cur_path.coords[2*nsides+1];
521 remove_recognized_strokes(rs, nsides);
522 insert_recognized_curpath();
526 /* the main pattern recognition function, called after finalize_stroke() */
527 void recognize_patterns(void)
530 struct Inertia s, ss[4];
531 struct RecoSegment *rs;
536 if (!undo || undo->type!=ITEM_STROKE) return;
537 if (undo->next != last_item_checker) reset_recognizer(); // reset queue
538 if (last_item_checker!=NULL && ui.cur_layer != last_item_checker->layer) reset_recognizer();
541 calc_inertia(it->path->coords, 0, it->path->num_points-1, &s);
542 #ifdef RECOGNIZER_DEBUG
543 printf("DEBUG: Mass=%.0f, Center=(%.1f,%.1f), I=(%.0f,%.0f, %.0f), "
544 "Rad=%.2f, Det=%.4f \n",
545 s.mass, center_x(s), center_y(s), I_xx(s), I_yy(s), I_xy(s), I_rad(s), I_det(s));
548 // first see if it's a polygon
549 n = find_polygonal(it->path->coords, 0, it->path->num_points-1, MAX_POLYGON_SIDES, brk, ss);
551 optimize_polygonal(it->path->coords, n, brk, ss);
552 #ifdef RECOGNIZER_DEBUG
553 printf("DEBUG: Polygon, %d edges: ", n);
555 printf("DEBUG: %d-%d (M=%.0f, det=%.4f) ", brk[i], brk[i+1], ss[i].mass, I_det(ss[i]));
558 /* update recognizer segment queue (most recent at end) */
559 while (n+recognizer_queue_length > MAX_POLYGON_SIDES) {
560 // remove oldest polygonal stroke
562 while (i<recognizer_queue_length && recognizer_queue[i].startpt!=0) i++;
563 recognizer_queue_length-=i;
564 g_memmove(recognizer_queue, recognizer_queue+i,
565 recognizer_queue_length * sizeof(struct RecoSegment));
567 #ifdef RECOGNIZER_DEBUG
568 printf("DEBUG: Queue now has %d + %d edges\n", recognizer_queue_length, n);
570 rs = recognizer_queue + recognizer_queue_length;
571 recognizer_queue_length += n;
572 for (i=0; i<n; i++) {
574 rs[i].startpt = brk[i];
575 rs[i].endpt = brk[i+1];
576 get_segment_geometry(it->path->coords, brk[i], brk[i+1], ss+i, rs+i);
578 if (try_rectangle()) { reset_recognizer(); return; }
579 if (try_arrow()) { reset_recognizer(); return; }
580 if (try_closed_polygon(3)) { reset_recognizer(); return; }
581 if (try_closed_polygon(4)) { reset_recognizer(); return; }
582 if (n==1) { // current stroke is a line
583 if (fabs(rs->angle)<SLANT_TOLERANCE) { // nearly horizontal
585 rs->y1 = rs->y2 = rs->ycenter;
587 if (fabs(rs->angle)>M_PI/2-SLANT_TOLERANCE) { // nearly vertical
588 rs->angle = (rs->angle>0)?(M_PI/2):(-M_PI/2);
589 rs->x1 = rs->x2 = rs->xcenter;
592 ui.cur_path.num_points = 2;
593 ui.cur_path.coords[0] = rs->x1;
594 ui.cur_path.coords[1] = rs->y1;
595 ui.cur_path.coords[2] = rs->x2;
596 ui.cur_path.coords[3] = rs->y2;
597 remove_recognized_strokes(rs, 1);
598 rs->item = insert_recognized_curpath();
600 last_item_checker = undo;
604 // not a polygon: maybe a circle ?
606 if (I_det(s)>CIRCLE_MIN_DET) {
607 score = score_circle(it->path->coords, 0, it->path->num_points-1, &s);
608 #ifdef RECOGNIZER_DEBUG
609 printf("DEBUG: Circle score: %.2f\n", score);
611 if (score < CIRCLE_MAX_SCORE) {
612 make_circle_shape(center_x(s), center_y(s), I_rad(s));
613 recognizer_queue[0].item = it;
614 remove_recognized_strokes(recognizer_queue, 1);
615 insert_recognized_curpath();