1 ///////////////////////////////////////////////////////////////////////
2 // File:        alignedblob.cpp
3 // Description: Subclass of BBGrid to find vertically aligned blobs.
4 // Author:      Ray Smith
5 //
6 // (C) Copyright 2008, Google Inc.
7 // Licensed under the Apache License, Version 2.0 (the "License");
8 // you may not use this file except in compliance with the License.
9 // You may obtain a copy of the License at
10 // http://www.apache.org/licenses/LICENSE-2.0
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
16 //
17 ///////////////////////////////////////////////////////////////////////
18 
19 #ifdef HAVE_CONFIG_H
20 #  include "config_auto.h"
21 #endif
22 
23 #include "alignedblob.h"
24 
25 #include <algorithm>
26 
27 namespace tesseract {
28 
29 INT_VAR(textord_debug_tabfind, 0, "Debug tab finding");
30 INT_VAR(textord_debug_bugs, 0, "Turn on output related to bugs in tab finding");
31 static INT_VAR(textord_testregion_left, -1,
32                "Left edge of debug reporting rectangle in Leptonica coords "
33                "(bottom=0/top=height), with horizontal lines x/y-flipped");
34 static INT_VAR(textord_testregion_top, INT32_MAX,
35                "Top edge of debug reporting rectangle in Leptonica coords "
36                "(bottom=0/top=height), with horizontal lines x/y-flipped");
37 static INT_VAR(textord_testregion_right, INT32_MAX,
38                "Right edge of debug rectangle in Leptonica coords "
39                "(bottom=0/top=height), with horizontal lines x/y-flipped");
40 static INT_VAR(textord_testregion_bottom, -1,
41                "Bottom edge of debug rectangle in Leptonica coords "
42                "(bottom=0/top=height), with horizontal lines x/y-flipped");
43 BOOL_VAR(textord_debug_printable, false, "Make debug windows printable");
44 
45 // Fraction of resolution used as alignment tolerance for aligned tabs.
46 const double kAlignedFraction = 0.03125;
47 // Fraction of resolution used as alignment tolerance for ragged tabs.
48 const double kRaggedFraction = 2.5;
49 // Fraction of height used as a minimum gutter gap for aligned blobs.
50 const double kAlignedGapFraction = 0.75;
51 // Fraction of height used as a minimum gutter gap for ragged tabs.
52 const double kRaggedGapFraction = 1.0;
53 // Constant number of pixels used as alignment tolerance for line finding.
54 const int kVLineAlignment = 3;
55 // Constant number of pixels used as gutter gap tolerance for line finding.
56 const int kVLineGutter = 1;
57 // Constant number of pixels used as the search size for line finding.
58 const int kVLineSearchSize = 150;
59 // Min number of points to accept for a ragged tab stop.
60 const int kMinRaggedTabs = 5;
61 // Min number of points to accept for an aligned tab stop.
62 const int kMinAlignedTabs = 4;
63 // Constant number of pixels minimum height of a vertical line.
64 const int kVLineMinLength = 300;
65 // Minimum gradient for a vertical tab vector. Used to prune away junk
66 // tab vectors with what would be a ridiculously large skew angle.
67 // Value corresponds to tan(90 - max allowed skew angle)
68 const double kMinTabGradient = 4.0;
69 // Tolerance to skew on top of current estimate of skew. Divide x or y length
70 // by kMaxSkewFactor to get the y or x skew distance.
71 // If the angle is small, the angle in degrees is roughly 60/kMaxSkewFactor.
72 const int kMaxSkewFactor = 15;
73 
74 // Constructor to set the parameters for finding aligned and ragged tabs.
75 // Vertical_x and vertical_y are the current estimates of the true vertical
76 // direction (up) in the image. Height is the height of the starter blob.
77 // v_gap_multiple is the multiple of height that will be used as a limit
78 // on vertical gap before giving up and calling the line ended.
79 // resolution is the original image resolution, and align0 indicates the
80 // type of tab stop to be found.
AlignedBlobParams(int vertical_x,int vertical_y,int height,int v_gap_multiple,int min_gutter_width,int resolution,TabAlignment align0)81 AlignedBlobParams::AlignedBlobParams(int vertical_x, int vertical_y, int height, int v_gap_multiple,
82                                      int min_gutter_width, int resolution, TabAlignment align0)
83     : right_tab(align0 == TA_RIGHT_RAGGED || align0 == TA_RIGHT_ALIGNED)
84     , ragged(align0 == TA_LEFT_RAGGED || align0 == TA_RIGHT_RAGGED)
85     , alignment(align0)
86     , confirmed_type(TT_CONFIRMED)
87     , min_length(0) {
88   // Set the tolerances according to the type of line sought.
89   // For tab search, these are based on the image resolution for most, or
90   // the height of the starting blob for the maximum vertical gap.
91   max_v_gap = height * v_gap_multiple;
92   if (ragged) {
93     // In the case of a ragged edge, we are much more generous with the
94     // inside alignment fraction, but also require a much bigger gutter.
95     gutter_fraction = kRaggedGapFraction;
96     if (alignment == TA_RIGHT_RAGGED) {
97       l_align_tolerance = static_cast<int>(resolution * kRaggedFraction + 0.5);
98       r_align_tolerance = static_cast<int>(resolution * kAlignedFraction + 0.5);
99     } else {
100       l_align_tolerance = static_cast<int>(resolution * kAlignedFraction + 0.5);
101       r_align_tolerance = static_cast<int>(resolution * kRaggedFraction + 0.5);
102     }
103     min_points = kMinRaggedTabs;
104   } else {
105     gutter_fraction = kAlignedGapFraction;
106     l_align_tolerance = static_cast<int>(resolution * kAlignedFraction + 0.5);
107     r_align_tolerance = static_cast<int>(resolution * kAlignedFraction + 0.5);
108     min_points = kMinAlignedTabs;
109   }
110   min_gutter = static_cast<int>(height * gutter_fraction + 0.5);
111   if (min_gutter < min_gutter_width) {
112     min_gutter = min_gutter_width;
113   }
114   // Fit the vertical vector into an ICOORD, which is 16 bit.
115   set_vertical(vertical_x, vertical_y);
116 }
117 
118 // Constructor to set the parameters for finding vertical lines.
119 // Vertical_x and vertical_y are the current estimates of the true vertical
120 // direction (up) in the image. Width is the width of the starter blob.
AlignedBlobParams(int vertical_x,int vertical_y,int width)121 AlignedBlobParams::AlignedBlobParams(int vertical_x, int vertical_y, int width)
122     : gutter_fraction(0.0)
123     , right_tab(false)
124     , ragged(false)
125     , alignment(TA_SEPARATOR)
126     , confirmed_type(TT_VLINE)
127     , max_v_gap(kVLineSearchSize)
128     , min_gutter(kVLineGutter)
129     , min_points(1)
130     , min_length(kVLineMinLength) {
131   // Compute threshold for left and right alignment.
132   l_align_tolerance = std::max(kVLineAlignment, width);
133   r_align_tolerance = std::max(kVLineAlignment, width);
134 
135   // Fit the vertical vector into an ICOORD, which is 16 bit.
136   set_vertical(vertical_x, vertical_y);
137 }
138 
139 // Fit the vertical vector into an ICOORD, which is 16 bit.
set_vertical(int vertical_x,int vertical_y)140 void AlignedBlobParams::set_vertical(int vertical_x, int vertical_y) {
141   int factor = 1;
142   if (vertical_y > INT16_MAX) {
143     factor = vertical_y / INT16_MAX + 1;
144   }
145   vertical.set_x(vertical_x / factor);
146   vertical.set_y(vertical_y / factor);
147 }
148 
AlignedBlob(int gridsize,const ICOORD & bleft,const ICOORD & tright)149 AlignedBlob::AlignedBlob(int gridsize, const ICOORD &bleft, const ICOORD &tright)
150     : BlobGrid(gridsize, bleft, tright) {}
151 
152 // Return true if the given coordinates are within the test rectangle
153 // and the debug level is at least the given detail level.
WithinTestRegion(int detail_level,int x,int y)154 bool AlignedBlob::WithinTestRegion(int detail_level, int x, int y) {
155   if (textord_debug_tabfind < detail_level) {
156     return false;
157   }
158   return x >= textord_testregion_left && x <= textord_testregion_right &&
159          y <= textord_testregion_top && y >= textord_testregion_bottom;
160 }
161 
162 #ifndef GRAPHICS_DISABLED
163 
164 // Display the tab codes of the BLOBNBOXes in this grid.
DisplayTabs(const char * window_name,ScrollView * tab_win)165 ScrollView *AlignedBlob::DisplayTabs(const char *window_name, ScrollView *tab_win) {
166   if (tab_win == nullptr) {
167     tab_win = MakeWindow(0, 50, window_name);
168   }
169   // For every tab in the grid, display it.
170   GridSearch<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT> gsearch(this);
171   gsearch.StartFullSearch();
172   BLOBNBOX *bbox;
173   while ((bbox = gsearch.NextFullSearch()) != nullptr) {
174     const TBOX &box = bbox->bounding_box();
175     int left_x = box.left();
176     int right_x = box.right();
177     int top_y = box.top();
178     int bottom_y = box.bottom();
179     TabType tabtype = bbox->left_tab_type();
180     if (tabtype != TT_NONE) {
181       if (tabtype == TT_MAYBE_ALIGNED) {
182         tab_win->Pen(ScrollView::BLUE);
183       } else if (tabtype == TT_MAYBE_RAGGED) {
184         tab_win->Pen(ScrollView::YELLOW);
185       } else if (tabtype == TT_CONFIRMED) {
186         tab_win->Pen(ScrollView::GREEN);
187       } else {
188         tab_win->Pen(ScrollView::GREY);
189       }
190       tab_win->Line(left_x, top_y, left_x, bottom_y);
191     }
192     tabtype = bbox->right_tab_type();
193     if (tabtype != TT_NONE) {
194       if (tabtype == TT_MAYBE_ALIGNED) {
195         tab_win->Pen(ScrollView::MAGENTA);
196       } else if (tabtype == TT_MAYBE_RAGGED) {
197         tab_win->Pen(ScrollView::ORANGE);
198       } else if (tabtype == TT_CONFIRMED) {
199         tab_win->Pen(ScrollView::RED);
200       } else {
201         tab_win->Pen(ScrollView::GREY);
202       }
203       tab_win->Line(right_x, top_y, right_x, bottom_y);
204     }
205   }
206   tab_win->Update();
207   return tab_win;
208 }
209 
210 #endif // !GRAPHICS_DISABLED
211 
212 // Helper returns true if the total number of line_crossings of all the blobs
213 // in the list is at least 2.
AtLeast2LineCrossings(BLOBNBOX_CLIST * blobs)214 static bool AtLeast2LineCrossings(BLOBNBOX_CLIST *blobs) {
215   BLOBNBOX_C_IT it(blobs);
216   int total_crossings = 0;
217   for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
218     total_crossings += it.data()->line_crossings();
219   }
220   return total_crossings >= 2;
221 }
222 
223 // Destructor.
224 // It is defined here, so the compiler can create a single vtable
225 // instead of weak vtables in every compilation unit.
226 AlignedBlob::~AlignedBlob() = default;
227 
228 // Finds a vector corresponding to a set of vertically aligned blob edges
229 // running through the given box. The type of vector returned and the
230 // search parameters are determined by the AlignedBlobParams.
231 // vertical_x and y are updated with an estimate of the real
232 // vertical direction. (skew finding.)
233 // Returns nullptr if no decent vector can be found.
FindVerticalAlignment(AlignedBlobParams align_params,BLOBNBOX * bbox,int * vertical_x,int * vertical_y)234 TabVector *AlignedBlob::FindVerticalAlignment(AlignedBlobParams align_params, BLOBNBOX *bbox,
235                                               int *vertical_x, int *vertical_y) {
236   int ext_start_y, ext_end_y;
237   BLOBNBOX_CLIST good_points;
238   // Search up and then down from the starting bbox.
239   TBOX box = bbox->bounding_box();
240   bool debug = WithinTestRegion(2, box.left(), box.bottom());
241   int pt_count = AlignTabs(align_params, false, bbox, &good_points, &ext_end_y);
242   pt_count += AlignTabs(align_params, true, bbox, &good_points, &ext_start_y);
243   BLOBNBOX_C_IT it(&good_points);
244   it.move_to_last();
245   box = it.data()->bounding_box();
246   int end_y = box.top();
247   int end_x = align_params.right_tab ? box.right() : box.left();
248   it.move_to_first();
249   box = it.data()->bounding_box();
250   int start_x = align_params.right_tab ? box.right() : box.left();
251   int start_y = box.bottom();
252   // Acceptable tab vectors must have a minimum number of points,
253   // have a minimum acceptable length, and have a minimum gradient.
254   // The gradient corresponds to the skew angle.
255   // Ragged tabs don't need to satisfy the gradient condition, as they
256   // will always end up parallel to the vertical direction.
257   bool at_least_2_crossings = AtLeast2LineCrossings(&good_points);
258   if ((pt_count >= align_params.min_points && end_y - start_y >= align_params.min_length &&
259        (align_params.ragged || end_y - start_y >= abs(end_x - start_x) * kMinTabGradient)) ||
260       at_least_2_crossings) {
261     int confirmed_points = 0;
262     // Count existing confirmed points to see if vector is acceptable.
263     for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
264       bbox = it.data();
265       if (align_params.right_tab) {
266         if (bbox->right_tab_type() == align_params.confirmed_type) {
267           ++confirmed_points;
268         }
269       } else {
270         if (bbox->left_tab_type() == align_params.confirmed_type) {
271           ++confirmed_points;
272         }
273       }
274     }
275     // Ragged vectors are not allowed to use too many already used points.
276     if (!align_params.ragged || confirmed_points + confirmed_points < pt_count) {
277       const TBOX &box = bbox->bounding_box();
278       if (debug) {
279         tprintf("Confirming tab vector of %d pts starting at %d,%d\n", pt_count, box.left(),
280                 box.bottom());
281       }
282       // Flag all the aligned neighbours as confirmed .
283       for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
284         bbox = it.data();
285         if (align_params.right_tab) {
286           bbox->set_right_tab_type(align_params.confirmed_type);
287         } else {
288           bbox->set_left_tab_type(align_params.confirmed_type);
289         }
290         if (debug) {
291           bbox->bounding_box().print();
292         }
293       }
294       // Now make the vector and return it.
295       TabVector *result =
296           TabVector::FitVector(align_params.alignment, align_params.vertical, ext_start_y,
297                                ext_end_y, &good_points, vertical_x, vertical_y);
298       result->set_intersects_other_lines(at_least_2_crossings);
299       if (debug) {
300         tprintf("Box was %d, %d\n", box.left(), box.bottom());
301         result->Print("After fitting");
302       }
303       return result;
304     } else if (debug) {
305       tprintf("Ragged tab used too many used points: %d out of %d\n", confirmed_points, pt_count);
306     }
307   } else if (debug) {
308     tprintf(
309         "Tab vector failed basic tests: pt count %d vs min %d, "
310         "length %d vs min %d, min grad %g\n",
311         pt_count, align_params.min_points, end_y - start_y, align_params.min_length,
312         abs(end_x - start_x) * kMinTabGradient);
313   }
314   return nullptr;
315 }
316 
317 // Find a set of blobs that are aligned in the given vertical
318 // direction with the given blob. Returns a list of aligned
319 // blobs and the number in the list.
320 // For other parameters see FindAlignedBlob below.
AlignTabs(const AlignedBlobParams & params,bool top_to_bottom,BLOBNBOX * bbox,BLOBNBOX_CLIST * good_points,int * end_y)321 int AlignedBlob::AlignTabs(const AlignedBlobParams &params, bool top_to_bottom, BLOBNBOX *bbox,
322                            BLOBNBOX_CLIST *good_points, int *end_y) {
323   int ptcount = 0;
324   BLOBNBOX_C_IT it(good_points);
325 
326   TBOX box = bbox->bounding_box();
327   bool debug = WithinTestRegion(2, box.left(), box.bottom());
328   if (debug) {
329     tprintf("Starting alignment run at blob:");
330     box.print();
331   }
332   int x_start = params.right_tab ? box.right() : box.left();
333   while (bbox != nullptr) {
334     // Add the blob to the list if the appropriate side is a tab candidate,
335     // or if we are working on a ragged tab.
336     TabType type = params.right_tab ? bbox->right_tab_type() : bbox->left_tab_type();
337     if (((type != TT_NONE && type != TT_MAYBE_RAGGED) || params.ragged) &&
338         (it.empty() || it.data() != bbox)) {
339       if (top_to_bottom) {
340         it.add_before_then_move(bbox);
341       } else {
342         it.add_after_then_move(bbox);
343       }
344       ++ptcount;
345     }
346     // Find the next blob that is aligned with the current one.
347     // FindAlignedBlob guarantees that forward progress will be made in the
348     // top_to_bottom direction, and therefore eventually it will return nullptr,
349     // making this while (bbox != nullptr) loop safe.
350     bbox = FindAlignedBlob(params, top_to_bottom, bbox, x_start, end_y);
351     if (bbox != nullptr) {
352       box = bbox->bounding_box();
353       if (!params.ragged) {
354         x_start = params.right_tab ? box.right() : box.left();
355       }
356     }
357   }
358   if (debug) {
359     tprintf("Alignment run ended with %d pts at blob:", ptcount);
360     box.print();
361   }
362   return ptcount;
363 }
364 
365 // Search vertically for a blob that is aligned with the input bbox.
366 // The search parameters are determined by AlignedBlobParams.
367 // top_to_bottom tells whether to search down or up.
368 // The return value is nullptr if nothing was found in the search box
369 // or if a blob was found in the gutter. On a nullptr return, end_y
370 // is set to the edge of the search box or the leading edge of the
371 // gutter blob if one was found.
FindAlignedBlob(const AlignedBlobParams & p,bool top_to_bottom,BLOBNBOX * bbox,int x_start,int * end_y)372 BLOBNBOX *AlignedBlob::FindAlignedBlob(const AlignedBlobParams &p, bool top_to_bottom,
373                                        BLOBNBOX *bbox, int x_start, int *end_y) {
374   TBOX box = bbox->bounding_box();
375   // If there are separator lines, get the column edges.
376   int left_column_edge = bbox->left_rule();
377   int right_column_edge = bbox->right_rule();
378   // start_y is used to guarantee that forward progress is made and the
379   // search does not go into an infinite loop. New blobs must extend the
380   // line beyond start_y.
381   int start_y = top_to_bottom ? box.bottom() : box.top();
382   if (WithinTestRegion(2, x_start, start_y)) {
383     tprintf("Column edges for blob at (%d,%d)->(%d,%d) are [%d, %d]\n", box.left(), box.top(),
384             box.right(), box.bottom(), left_column_edge, right_column_edge);
385   }
386   // Compute skew tolerance.
387   int skew_tolerance = p.max_v_gap / kMaxSkewFactor;
388   // Calculate xmin and xmax of the search box so that it contains
389   // all possibly relevant boxes up to p.max_v_gap above or below according
390   // to top_to_bottom.
391   // Start with a notion of vertical with the current estimate.
392   int x2 = (p.max_v_gap * p.vertical.x() + p.vertical.y() / 2) / p.vertical.y();
393   if (top_to_bottom) {
394     x2 = x_start - x2;
395     *end_y = start_y - p.max_v_gap;
396   } else {
397     x2 = x_start + x2;
398     *end_y = start_y + p.max_v_gap;
399   }
400   // Expand the box by an additional skew tolerance
401   int xmin = std::min(x_start, x2) - skew_tolerance;
402   int xmax = std::max(x_start, x2) + skew_tolerance;
403   // Now add direction-specific tolerances.
404   if (p.right_tab) {
405     xmax += p.min_gutter;
406     xmin -= p.l_align_tolerance;
407   } else {
408     xmax += p.r_align_tolerance;
409     xmin -= p.min_gutter;
410   }
411   // Setup a vertical search for an aligned blob.
412   GridSearch<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT> vsearch(this);
413   if (WithinTestRegion(2, x_start, start_y)) {
414     tprintf("Starting %s %s search at %d-%d,%d, search_size=%d, gutter=%d\n",
415             p.ragged ? "Ragged" : "Aligned", p.right_tab ? "Right" : "Left", xmin, xmax, start_y,
416             p.max_v_gap, p.min_gutter);
417   }
418   vsearch.StartVerticalSearch(xmin, xmax, start_y);
419   // result stores the best real return value.
420   BLOBNBOX *result = nullptr;
421   // The backup_result is not a tab candidate and can be used if no
422   // real tab candidate result is found.
423   BLOBNBOX *backup_result = nullptr;
424   // neighbour is the blob that is currently being investigated.
425   BLOBNBOX *neighbour = nullptr;
426   while ((neighbour = vsearch.NextVerticalSearch(top_to_bottom)) != nullptr) {
427     if (neighbour == bbox) {
428       continue;
429     }
430     TBOX nbox = neighbour->bounding_box();
431     int n_y = (nbox.top() + nbox.bottom()) / 2;
432     if ((!top_to_bottom && n_y > start_y + p.max_v_gap) ||
433         (top_to_bottom && n_y < start_y - p.max_v_gap)) {
434       if (WithinTestRegion(2, x_start, start_y)) {
435         tprintf("Neighbour too far at (%d,%d)->(%d,%d)\n", nbox.left(), nbox.bottom(), nbox.right(),
436                 nbox.top());
437       }
438       break; // Gone far enough.
439     }
440     // It is CRITICAL to ensure that forward progress is made, (strictly
441     // in/decreasing n_y) or the caller could loop infinitely, while
442     // waiting for a sequence of blobs in a line to end.
443     // NextVerticalSearch alone does not guarantee this, as there may be
444     // more than one blob in a grid cell. See comment in AlignTabs.
445     if ((n_y < start_y) != top_to_bottom || nbox.y_overlap(box)) {
446       continue; // Only look in the required direction.
447     }
448     if (result != nullptr && result->bounding_box().y_gap(nbox) > gridsize()) {
449       return result; // This result is clear.
450     }
451     if (backup_result != nullptr && p.ragged && result == nullptr &&
452         backup_result->bounding_box().y_gap(nbox) > gridsize()) {
453       return backup_result; // This result is clear.
454     }
455 
456     // If the neighbouring blob is the wrong side of a separator line, then it
457     // "doesn't exist" as far as we are concerned.
458     int x_at_n_y = x_start + (n_y - start_y) * p.vertical.x() / p.vertical.y();
459     if (x_at_n_y < neighbour->left_crossing_rule() || x_at_n_y > neighbour->right_crossing_rule()) {
460       continue; // Separator line in the way.
461     }
462     int n_left = nbox.left();
463     int n_right = nbox.right();
464     int n_x = p.right_tab ? n_right : n_left;
465     if (WithinTestRegion(2, x_start, start_y)) {
466       tprintf("neighbour at (%d,%d)->(%d,%d), n_x=%d, n_y=%d, xatn=%d\n", nbox.left(),
467               nbox.bottom(), nbox.right(), nbox.top(), n_x, n_y, x_at_n_y);
468     }
469     if (p.right_tab && n_left < x_at_n_y + p.min_gutter &&
470         n_right > x_at_n_y + p.r_align_tolerance &&
471         (p.ragged || n_left < x_at_n_y + p.gutter_fraction * nbox.height())) {
472       // In the gutter so end of line.
473       if (bbox->right_tab_type() >= TT_MAYBE_ALIGNED) {
474         bbox->set_right_tab_type(TT_DELETED);
475       }
476       *end_y = top_to_bottom ? nbox.top() : nbox.bottom();
477       if (WithinTestRegion(2, x_start, start_y)) {
478         tprintf("gutter\n");
479       }
480       return nullptr;
481     }
482     if (!p.right_tab && n_left < x_at_n_y - p.l_align_tolerance &&
483         n_right > x_at_n_y - p.min_gutter &&
484         (p.ragged || n_right > x_at_n_y - p.gutter_fraction * nbox.height())) {
485       // In the gutter so end of line.
486       if (bbox->left_tab_type() >= TT_MAYBE_ALIGNED) {
487         bbox->set_left_tab_type(TT_DELETED);
488       }
489       *end_y = top_to_bottom ? nbox.top() : nbox.bottom();
490       if (WithinTestRegion(2, x_start, start_y)) {
491         tprintf("gutter\n");
492       }
493       return nullptr;
494     }
495     if ((p.right_tab && neighbour->leader_on_right()) ||
496         (!p.right_tab && neighbour->leader_on_left())) {
497       continue; // Neighbours of leaders are not allowed to be used.
498     }
499     if (n_x <= x_at_n_y + p.r_align_tolerance && n_x >= x_at_n_y - p.l_align_tolerance) {
500       // Aligned so keep it. If it is a marked tab save it as result,
501       // otherwise keep it as backup_result to return in case of later failure.
502       if (WithinTestRegion(2, x_start, start_y)) {
503         tprintf("aligned, seeking%d, l=%d, r=%d\n", p.right_tab, neighbour->left_tab_type(),
504                 neighbour->right_tab_type());
505       }
506       TabType n_type = p.right_tab ? neighbour->right_tab_type() : neighbour->left_tab_type();
507       if (n_type != TT_NONE && (p.ragged || n_type != TT_MAYBE_RAGGED)) {
508         if (result == nullptr) {
509           result = neighbour;
510         } else {
511           // Keep the closest neighbour by Euclidean distance.
512           // This prevents it from picking a tab blob in another column.
513           const TBOX &old_box = result->bounding_box();
514           int x_diff = p.right_tab ? old_box.right() : old_box.left();
515           x_diff -= x_at_n_y;
516           int y_diff = (old_box.top() + old_box.bottom()) / 2 - start_y;
517           int old_dist = x_diff * x_diff + y_diff * y_diff;
518           x_diff = n_x - x_at_n_y;
519           y_diff = n_y - start_y;
520           int new_dist = x_diff * x_diff + y_diff * y_diff;
521           if (new_dist < old_dist) {
522             result = neighbour;
523           }
524         }
525       } else if (backup_result == nullptr) {
526         if (WithinTestRegion(2, x_start, start_y)) {
527           tprintf("Backup\n");
528         }
529         backup_result = neighbour;
530       } else {
531         TBOX backup_box = backup_result->bounding_box();
532         if ((p.right_tab && backup_box.right() < nbox.right()) ||
533             (!p.right_tab && backup_box.left() > nbox.left())) {
534           if (WithinTestRegion(2, x_start, start_y)) {
535             tprintf("Better backup\n");
536           }
537           backup_result = neighbour;
538         }
539       }
540     }
541   }
542   return result != nullptr ? result : backup_result;
543 }
544 
545 } // namespace tesseract.
546