/src/tesseract/src/textord/colpartitiongrid.cpp
Line  | Count  | Source (jump to first uncovered line)  | 
1  |  | ///////////////////////////////////////////////////////////////////////  | 
2  |  | // File:        colpartitiongrid.cpp  | 
3  |  | // Description: Class collecting code that acts on a BBGrid of ColPartitions.  | 
4  |  | // Author:      Ray Smith  | 
5  |  | //  | 
6  |  | // (C) Copyright 2009, 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 "colpartitiongrid.h"  | 
24  |  | #include "colpartitionset.h"  | 
25  |  | #include "imagefind.h"  | 
26  |  |  | 
27  |  | #include <algorithm>  | 
28  |  | #include <utility>  | 
29  |  |  | 
30  |  | namespace tesseract { | 
31  |  |  | 
32  |  | // Max pad factor used to search the neighbourhood of a partition to smooth  | 
33  |  | // partition types.  | 
34  |  | const int kMaxPadFactor = 6;  | 
35  |  | // Max multiple of size (min(height, width)) for the distance of the nearest  | 
36  |  | // neighbour for the change of type to be used.  | 
37  |  | const int kMaxNeighbourDistFactor = 4;  | 
38  |  | // Maximum number of lines in a credible figure caption.  | 
39  |  | const int kMaxCaptionLines = 7;  | 
40  |  | // Min ratio between biggest and smallest gap to bound a caption.  | 
41  |  | const double kMinCaptionGapRatio = 2.0;  | 
42  |  | // Min ratio between biggest gap and mean line height to bound a caption.  | 
43  |  | const double kMinCaptionGapHeightRatio = 0.5;  | 
44  |  | // Min fraction of ColPartition height to be overlapping for margin purposes.  | 
45  |  | const double kMarginOverlapFraction = 0.25;  | 
46  |  | // Size ratio required to consider an unmerged overlapping partition to be big.  | 
47  |  | const double kBigPartSizeRatio = 1.75;  | 
48  |  | // Fraction of gridsize to allow arbitrary overlap between partitions.  | 
49  |  | const double kTinyEnoughTextlineOverlapFraction = 0.25;  | 
50  |  | // Max vertical distance of neighbouring ColPartition as a multiple of  | 
51  |  | // partition height for it to be a partner.  | 
52  |  | // TODO(rays) fix the problem that causes a larger number to not work well.  | 
53  |  | // The value needs to be larger as sparse text blocks in a page that gets  | 
54  |  | // marked as single column will not find adjacent lines as partners, and  | 
55  |  | // will merge horizontally distant, but aligned lines. See rep.4B3 p5.  | 
56  |  | // The value needs to be small because double-spaced legal docs written  | 
57  |  | // in a single column, but justified courier have widely spaced lines  | 
58  |  | // that need to get merged before they partner-up with the lines above  | 
59  |  | // and below. See legal.3B5 p13/17. Neither of these should depend on  | 
60  |  | // the value of kMaxPartitionSpacing to be successful, and ColPartition  | 
61  |  | // merging needs attention to fix this problem.  | 
62  |  | const double kMaxPartitionSpacing = 1.75;  | 
63  |  | // Margin by which text has to beat image or vice-versa to make a firm  | 
64  |  | // decision in GridSmoothNeighbour.  | 
65  |  | const int kSmoothDecisionMargin = 4;  | 
66  |  |  | 
67  |  | ColPartitionGrid::ColPartitionGrid(int gridsize, const ICOORD &bleft,  | 
68  |  |                                    const ICOORD &tright)  | 
69  | 0  |     : BBGrid<ColPartition, ColPartition_CLIST, ColPartition_C_IT>(  | 
70  | 0  |           gridsize, bleft, tright) {} | 
71  |  |  | 
72  |  | // Handles a click event in a display window.  | 
73  | 0  | void ColPartitionGrid::HandleClick(int x, int y) { | 
74  | 0  |   BBGrid<ColPartition, ColPartition_CLIST, ColPartition_C_IT>::HandleClick(x,  | 
75  | 0  |                                                                            y);  | 
76  |  |   // Run a radial search for partitions that overlap.  | 
77  | 0  |   ColPartitionGridSearch radsearch(this);  | 
78  | 0  |   radsearch.SetUniqueMode(true);  | 
79  | 0  |   radsearch.StartRadSearch(x, y, 1);  | 
80  | 0  |   ColPartition *neighbour;  | 
81  | 0  |   FCOORD click(x, y);  | 
82  | 0  |   while ((neighbour = radsearch.NextRadSearch()) != nullptr) { | 
83  | 0  |     const TBOX &nbox = neighbour->bounding_box();  | 
84  | 0  |     if (nbox.contains(click)) { | 
85  | 0  |       tprintf("Block box:"); | 
86  | 0  |       neighbour->bounding_box().print();  | 
87  | 0  |       neighbour->Print();  | 
88  | 0  |     }  | 
89  | 0  |   }  | 
90  | 0  | }  | 
91  |  |  | 
92  |  | // Merges ColPartitions in the grid that look like they belong in the same  | 
93  |  | // textline.  | 
94  |  | // For all partitions in the grid, calls the box_cb permanent callback  | 
95  |  | // to compute the search box, searches the box, and if a candidate is found,  | 
96  |  | // calls the confirm_cb to check any more rules. If the confirm_cb returns  | 
97  |  | // true, then the partitions are merged.  | 
98  |  | // Both callbacks are deleted before returning.  | 
99  |  | void ColPartitionGrid::Merges(  | 
100  |  |     const std::function<bool(ColPartition *, TBOX *)> &box_cb,  | 
101  |  |     const std::function<bool(const ColPartition *, const ColPartition *)>  | 
102  | 0  |         &confirm_cb) { | 
103  |  |   // Iterate the ColPartitions in the grid.  | 
104  | 0  |   ColPartitionGridSearch gsearch(this);  | 
105  | 0  |   gsearch.StartFullSearch();  | 
106  | 0  |   ColPartition *part;  | 
107  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
108  | 0  |     if (MergePart(box_cb, confirm_cb, part)) { | 
109  | 0  |       gsearch.RepositionIterator();  | 
110  | 0  |     }  | 
111  | 0  |   }  | 
112  | 0  | }  | 
113  |  |  | 
114  |  | // For the given partition, calls the box_cb permanent callback  | 
115  |  | // to compute the search box, searches the box, and if a candidate is found,  | 
116  |  | // calls the confirm_cb to check any more rules. If the confirm_cb returns  | 
117  |  | // true, then the partitions are merged.  | 
118  |  | // Returns true if the partition is consumed by one or more merges.  | 
119  |  | bool ColPartitionGrid::MergePart(  | 
120  |  |     const std::function<bool(ColPartition *, TBOX *)> &box_cb,  | 
121  |  |     const std::function<bool(const ColPartition *, const ColPartition *)>  | 
122  |  |         &confirm_cb,  | 
123  | 0  |     ColPartition *part) { | 
124  | 0  |   if (part->IsUnMergeableType()) { | 
125  | 0  |     return false;  | 
126  | 0  |   }  | 
127  | 0  |   bool any_done = false;  | 
128  |  |   // Repeatedly merge part while we find a best merge candidate that works.  | 
129  | 0  |   bool merge_done = false;  | 
130  | 0  |   do { | 
131  | 0  |     merge_done = false;  | 
132  | 0  |     TBOX box = part->bounding_box();  | 
133  | 0  |     bool debug = AlignedBlob::WithinTestRegion(2, box.left(), box.bottom());  | 
134  | 0  |     if (debug) { | 
135  | 0  |       tprintf("Merge candidate:"); | 
136  | 0  |       box.print();  | 
137  | 0  |     }  | 
138  |  |     // Set up a rectangle search bounded by the part.  | 
139  | 0  |     if (!box_cb(part, &box)) { | 
140  | 0  |       continue;  | 
141  | 0  |     }  | 
142  |  |     // Create a list of merge candidates.  | 
143  | 0  |     ColPartition_CLIST merge_candidates;  | 
144  | 0  |     FindMergeCandidates(part, box, debug, &merge_candidates);  | 
145  |  |     // Find the best merge candidate based on minimal overlap increase.  | 
146  | 0  |     int overlap_increase;  | 
147  | 0  |     ColPartition *neighbour = BestMergeCandidate(part, &merge_candidates, debug,  | 
148  | 0  |                                                  confirm_cb, &overlap_increase);  | 
149  | 0  |     if (neighbour != nullptr && overlap_increase <= 0) { | 
150  | 0  |       if (debug) { | 
151  | 0  |         tprintf("Merging:hoverlap=%d, voverlap=%d, OLI=%d\n", | 
152  | 0  |                 part->HCoreOverlap(*neighbour), part->VCoreOverlap(*neighbour),  | 
153  | 0  |                 overlap_increase);  | 
154  | 0  |       }  | 
155  |  |       // Looks like a good candidate so merge it.  | 
156  | 0  |       RemoveBBox(neighbour);  | 
157  |  |       // We will modify the box of part, so remove it from the grid, merge  | 
158  |  |       // it and then re-insert it into the grid.  | 
159  | 0  |       RemoveBBox(part);  | 
160  | 0  |       part->Absorb(neighbour, nullptr);  | 
161  | 0  |       InsertBBox(true, true, part);  | 
162  | 0  |       merge_done = true;  | 
163  | 0  |       any_done = true;  | 
164  | 0  |     } else if (neighbour != nullptr) { | 
165  | 0  |       if (debug) { | 
166  | 0  |         tprintf("Overlapped when merged with increase %d: ", overlap_increase); | 
167  | 0  |         neighbour->bounding_box().print();  | 
168  | 0  |       }  | 
169  | 0  |     } else if (debug) { | 
170  | 0  |       tprintf("No candidate neighbour returned\n"); | 
171  | 0  |     }  | 
172  | 0  |   } while (merge_done);  | 
173  | 0  |   return any_done;  | 
174  | 0  | }  | 
175  |  |  | 
176  |  | // Returns true if the given part and merge candidate might believably  | 
177  |  | // be part of a single text line according to the default rules.  | 
178  |  | // In general we only want to merge partitions that look like they  | 
179  |  | // are on the same text line, ie their median limits overlap, but we have  | 
180  |  | // to make exceptions for diacritics and stray punctuation.  | 
181  |  | static bool OKMergeCandidate(const ColPartition *part,  | 
182  | 0  |                              const ColPartition *candidate, bool debug) { | 
183  | 0  |   const TBOX &part_box = part->bounding_box();  | 
184  | 0  |   if (candidate == part) { | 
185  | 0  |     return false; // Ignore itself.  | 
186  | 0  |   }  | 
187  | 0  |   if (!part->TypesMatch(*candidate) || candidate->IsUnMergeableType()) { | 
188  | 0  |     return false; // Don't mix inappropriate types.  | 
189  | 0  |   }  | 
190  |  |  | 
191  | 0  |   const TBOX &c_box = candidate->bounding_box();  | 
192  | 0  |   if (debug) { | 
193  | 0  |     tprintf("Examining merge candidate:"); | 
194  | 0  |     c_box.print();  | 
195  | 0  |   }  | 
196  |  |   // Candidates must be within a reasonable distance.  | 
197  | 0  |   if (candidate->IsVerticalType() || part->IsVerticalType()) { | 
198  | 0  |     int h_dist = -part->HCoreOverlap(*candidate);  | 
199  | 0  |     if (h_dist >= std::max(part_box.width(), c_box.width()) / 2) { | 
200  | 0  |       if (debug) { | 
201  | 0  |         tprintf("Too far away: h_dist = %d\n", h_dist); | 
202  | 0  |       }  | 
203  | 0  |       return false;  | 
204  | 0  |     }  | 
205  | 0  |   } else { | 
206  |  |     // Coarse filter by vertical distance between partitions.  | 
207  | 0  |     int v_dist = -part->VCoreOverlap(*candidate);  | 
208  | 0  |     if (v_dist >= std::max(part_box.height(), c_box.height()) / 2) { | 
209  | 0  |       if (debug) { | 
210  | 0  |         tprintf("Too far away: v_dist = %d\n", v_dist); | 
211  | 0  |       }  | 
212  | 0  |       return false;  | 
213  | 0  |     }  | 
214  |  |     // Candidates must either overlap in median y,  | 
215  |  |     // or part or candidate must be an acceptable diacritic.  | 
216  | 0  |     if (!part->VSignificantCoreOverlap(*candidate) &&  | 
217  | 0  |         !part->OKDiacriticMerge(*candidate, debug) &&  | 
218  | 0  |         !candidate->OKDiacriticMerge(*part, debug)) { | 
219  | 0  |       if (debug) { | 
220  | 0  |         tprintf("Candidate fails overlap and diacritic tests!\n"); | 
221  | 0  |       }  | 
222  | 0  |       return false;  | 
223  | 0  |     }  | 
224  | 0  |   }  | 
225  | 0  |   return true;  | 
226  | 0  | }  | 
227  |  |  | 
228  |  | // Helper function to compute the increase in overlap of the parts list of  | 
229  |  | // Colpartitions with the combination of merge1 and merge2, compared to  | 
230  |  | // the overlap with them uncombined.  | 
231  |  | // An overlap is not counted if passes the OKMergeOverlap test with ok_overlap  | 
232  |  | // as the pixel overlap limit. merge1 and merge2 must both be non-nullptr.  | 
233  |  | static int IncreaseInOverlap(const ColPartition *merge1,  | 
234  |  |                              const ColPartition *merge2, int ok_overlap,  | 
235  | 0  |                              ColPartition_CLIST *parts) { | 
236  | 0  |   ASSERT_HOST(merge1 != nullptr && merge2 != nullptr);  | 
237  | 0  |   int total_area = 0;  | 
238  | 0  |   ColPartition_C_IT it(parts);  | 
239  | 0  |   TBOX merged_box(merge1->bounding_box());  | 
240  | 0  |   merged_box += merge2->bounding_box();  | 
241  | 0  |   for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { | 
242  | 0  |     ColPartition *part = it.data();  | 
243  | 0  |     if (part == merge1 || part == merge2) { | 
244  | 0  |       continue;  | 
245  | 0  |     }  | 
246  | 0  |     TBOX part_box = part->bounding_box();  | 
247  |  |     // Compute the overlap of the merged box with part.  | 
248  | 0  |     int overlap_area = part_box.intersection(merged_box).area();  | 
249  | 0  |     if (overlap_area > 0 &&  | 
250  | 0  |         !part->OKMergeOverlap(*merge1, *merge2, ok_overlap, false)) { | 
251  | 0  |       total_area += overlap_area;  | 
252  |  |       // Subtract the overlap of merge1 and merge2 individually.  | 
253  | 0  |       overlap_area = part_box.intersection(merge1->bounding_box()).area();  | 
254  | 0  |       if (overlap_area > 0) { | 
255  | 0  |         total_area -= overlap_area;  | 
256  | 0  |       }  | 
257  | 0  |       TBOX intersection_box = part_box.intersection(merge2->bounding_box());  | 
258  | 0  |       overlap_area = intersection_box.area();  | 
259  | 0  |       if (overlap_area > 0) { | 
260  | 0  |         total_area -= overlap_area;  | 
261  |  |         // Add back the 3-way area.  | 
262  | 0  |         intersection_box &= merge1->bounding_box(); // In-place intersection.  | 
263  | 0  |         overlap_area = intersection_box.area();  | 
264  | 0  |         if (overlap_area > 0) { | 
265  | 0  |           total_area += overlap_area;  | 
266  | 0  |         }  | 
267  | 0  |       }  | 
268  | 0  |     }  | 
269  | 0  |   }  | 
270  | 0  |   return total_area;  | 
271  | 0  | }  | 
272  |  |  | 
273  |  | // Helper function to test that each partition in candidates is either a  | 
274  |  | // good diacritic merge with part or an OK merge candidate with all others  | 
275  |  | // in the candidates list.  | 
276  |  | // ASCII Art Scenario:  | 
277  |  | // We sometimes get text such as "join-this" where the - is actually a long  | 
278  |  | // dash culled from a standard set of extra characters that don't match the  | 
279  |  | // font of the text. This makes its strokewidth not match and forms a broken  | 
280  |  | // set of 3 partitions for "join", "-" and "this" and the dash may slightly  | 
281  |  | // overlap BOTH words.  | 
282  |  | // -------  -------  | 
283  |  | // |     ====     |  | 
284  |  | // -------  -------  | 
285  |  | // The standard merge rule: "you can merge 2 partitions as long as there is  | 
286  |  | // no increase in overlap elsewhere" fails miserably here. Merge any pair  | 
287  |  | // of partitions and the combined box overlaps more with the third than  | 
288  |  | // before. To allow the merge, we need to consider whether it is safe to  | 
289  |  | // merge everything, without merging separate text lines. For that we need  | 
290  |  | // everything to be an OKMergeCandidate (which is supposed to prevent  | 
291  |  | // separate text lines merging), but this is hard for diacritics to satisfy,  | 
292  |  | // so an alternative to being OKMergeCandidate with everything is to be an  | 
293  |  | // OKDiacriticMerge with part as the base character.  | 
294  |  | static bool TestCompatibleCandidates(const ColPartition &part, bool debug,  | 
295  | 0  |                                      ColPartition_CLIST *candidates) { | 
296  | 0  |   ColPartition_C_IT it(candidates);  | 
297  | 0  |   for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { | 
298  | 0  |     ColPartition *candidate = it.data();  | 
299  | 0  |     if (!candidate->OKDiacriticMerge(part, false)) { | 
300  | 0  |       ColPartition_C_IT it2(it);  | 
301  | 0  |       for (it2.mark_cycle_pt(); !it2.cycled_list(); it2.forward()) { | 
302  | 0  |         ColPartition *candidate2 = it2.data();  | 
303  | 0  |         if (candidate2 != candidate &&  | 
304  | 0  |             !OKMergeCandidate(candidate, candidate2, false)) { | 
305  | 0  |           if (debug) { | 
306  | 0  |             tprintf("NC overlap failed:Candidate:"); | 
307  | 0  |             candidate2->bounding_box().print();  | 
308  | 0  |             tprintf("fails to be a good merge with:"); | 
309  | 0  |             candidate->bounding_box().print();  | 
310  | 0  |           }  | 
311  | 0  |           return false;  | 
312  | 0  |         }  | 
313  | 0  |       }  | 
314  | 0  |     }  | 
315  | 0  |   }  | 
316  | 0  |   return true;  | 
317  | 0  | }  | 
318  |  |  | 
319  |  | // Computes and returns the total overlap of all partitions in the grid.  | 
320  |  | // If overlap_grid is non-null, it is filled with a grid that holds empty  | 
321  |  | // partitions representing the union of all overlapped partitions.  | 
322  | 0  | int ColPartitionGrid::ComputeTotalOverlap(ColPartitionGrid **overlap_grid) { | 
323  | 0  |   int total_overlap = 0;  | 
324  |  |   // Iterate the ColPartitions in the grid.  | 
325  | 0  |   ColPartitionGridSearch gsearch(this);  | 
326  | 0  |   gsearch.StartFullSearch();  | 
327  | 0  |   ColPartition *part;  | 
328  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
329  | 0  |     ColPartition_CLIST neighbors;  | 
330  | 0  |     const TBOX &part_box = part->bounding_box();  | 
331  | 0  |     FindOverlappingPartitions(part_box, part, &neighbors);  | 
332  | 0  |     ColPartition_C_IT n_it(&neighbors);  | 
333  | 0  |     bool any_part_overlap = false;  | 
334  | 0  |     for (n_it.mark_cycle_pt(); !n_it.cycled_list(); n_it.forward()) { | 
335  | 0  |       const TBOX &n_box = n_it.data()->bounding_box();  | 
336  | 0  |       int overlap = n_box.intersection(part_box).area();  | 
337  | 0  |       if (overlap > 0 && overlap_grid != nullptr) { | 
338  | 0  |         if (*overlap_grid == nullptr) { | 
339  | 0  |           *overlap_grid = new ColPartitionGrid(gridsize(), bleft(), tright());  | 
340  | 0  |         }  | 
341  | 0  |         (*overlap_grid)->InsertBBox(true, true, n_it.data()->ShallowCopy());  | 
342  | 0  |         if (!any_part_overlap) { | 
343  | 0  |           (*overlap_grid)->InsertBBox(true, true, part->ShallowCopy());  | 
344  | 0  |         }  | 
345  | 0  |       }  | 
346  | 0  |       any_part_overlap = true;  | 
347  | 0  |       total_overlap += overlap;  | 
348  | 0  |     }  | 
349  | 0  |   }  | 
350  | 0  |   return total_overlap;  | 
351  | 0  | }  | 
352  |  |  | 
353  |  | // Finds all the ColPartitions in the grid that overlap with the given  | 
354  |  | // box and returns them SortByBoxLeft(ed) and uniqued in the given list.  | 
355  |  | // Any partition equal to not_this (may be nullptr) is excluded.  | 
356  |  | void ColPartitionGrid::FindOverlappingPartitions(const TBOX &box,  | 
357  |  |                                                  const ColPartition *not_this,  | 
358  | 0  |                                                  ColPartition_CLIST *parts) { | 
359  | 0  |   ColPartitionGridSearch rsearch(this);  | 
360  | 0  |   rsearch.StartRectSearch(box);  | 
361  | 0  |   ColPartition *part;  | 
362  | 0  |   while ((part = rsearch.NextRectSearch()) != nullptr) { | 
363  | 0  |     if (part != not_this) { | 
364  | 0  |       parts->add_sorted(SortByBoxLeft<ColPartition>, true, part);  | 
365  | 0  |     }  | 
366  | 0  |   }  | 
367  | 0  | }  | 
368  |  |  | 
369  |  | // Finds and returns the best candidate ColPartition to merge with part,  | 
370  |  | // selected from the candidates list, based on the minimum increase in  | 
371  |  | // pairwise overlap among all the partitions overlapped by the combined box.  | 
372  |  | // If overlap_increase is not nullptr then it returns the increase in overlap  | 
373  |  | // that would result from the merge.  | 
374  |  | // confirm_cb is a permanent callback that (if non-null) will be used to  | 
375  |  | // confirm the validity of a proposed merge candidate before selecting it.  | 
376  |  | //  | 
377  |  | // ======HOW MERGING WORKS======  | 
378  |  | // The problem:  | 
379  |  | // We want to merge all the parts of a textline together, but avoid merging  | 
380  |  | // separate textlines. Diacritics, i dots, punctuation, and broken characters  | 
381  |  | // are examples of small bits that need merging with the main textline.  | 
382  |  | // Drop-caps and descenders in one line that touch ascenders in the one below  | 
383  |  | // are examples of cases where we don't want to merge.  | 
384  |  | //  | 
385  |  | // The solution:  | 
386  |  | // Merges that increase overlap among other partitions are generally bad.  | 
387  |  | // Those that don't increase overlap (much) and minimize the total area  | 
388  |  | // seem to be good.  | 
389  |  | //  | 
390  |  | // Ascii art example:  | 
391  |  | // The text:  | 
392  |  | // groggy descenders  | 
393  |  | // minimum ascenders  | 
394  |  | // The boxes: The === represents a small box near or overlapping the lower box.  | 
395  |  | // -----------------  | 
396  |  | // |               |  | 
397  |  | // -----------------  | 
398  |  | // -===-------------  | 
399  |  | // |               |  | 
400  |  | // -----------------  | 
401  |  | // In considering what to do with the small === box, we find the 2 larger  | 
402  |  | // boxes as neighbours and possible merge candidates, but merging with the  | 
403  |  | // upper box increases overlap with the lower box, whereas merging with the  | 
404  |  | // lower box does not increase overlap.  | 
405  |  | // If the small === box didn't overlap either to start with, total area  | 
406  |  | // would be minimized by merging with the nearer (lower) box.  | 
407  |  | //  | 
408  |  | // This is a simple example. In reality, we have to allow some increase  | 
409  |  | // in overlap, or tightly spaced text would end up in bits.  | 
410  |  | ColPartition *ColPartitionGrid::BestMergeCandidate(  | 
411  |  |     const ColPartition *part, ColPartition_CLIST *candidates, bool debug,  | 
412  |  |     const std::function<bool(const ColPartition *, const ColPartition *)>  | 
413  |  |         &confirm_cb,  | 
414  | 0  |     int *overlap_increase) { | 
415  | 0  |   if (overlap_increase != nullptr) { | 
416  | 0  |     *overlap_increase = 0;  | 
417  | 0  |   }  | 
418  | 0  |   if (candidates->empty()) { | 
419  | 0  |     return nullptr;  | 
420  | 0  |   }  | 
421  | 0  |   int ok_overlap =  | 
422  | 0  |       static_cast<int>(kTinyEnoughTextlineOverlapFraction * gridsize() + 0.5);  | 
423  |  |   // The best neighbour to merge with is the one that causes least  | 
424  |  |   // total pairwise overlap among all the neighbours.  | 
425  |  |   // If more than one offers the same total overlap, choose the one  | 
426  |  |   // with the least total area.  | 
427  | 0  |   const TBOX &part_box = part->bounding_box();  | 
428  | 0  |   ColPartition_C_IT it(candidates);  | 
429  | 0  |   ColPartition *best_candidate = nullptr;  | 
430  |  |   // Find the total combined box of all candidates and the original.  | 
431  | 0  |   TBOX full_box(part_box);  | 
432  | 0  |   for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { | 
433  | 0  |     ColPartition *candidate = it.data();  | 
434  | 0  |     full_box += candidate->bounding_box();  | 
435  | 0  |   }  | 
436  |  |   // Keep valid neighbours in a list.  | 
437  | 0  |   ColPartition_CLIST neighbours;  | 
438  |  |   // Now run a rect search of the merged box for overlapping neighbours, as  | 
439  |  |   // we need anything that might be overlapped by the merged box.  | 
440  | 0  |   FindOverlappingPartitions(full_box, part, &neighbours);  | 
441  | 0  |   if (debug) { | 
442  | 0  |     tprintf("Finding best merge candidate from %d, %d neighbours for box:", | 
443  | 0  |             candidates->length(), neighbours.length());  | 
444  | 0  |     part_box.print();  | 
445  | 0  |   }  | 
446  |  |   // If the best increase in overlap is positive, then we also check the  | 
447  |  |   // worst non-candidate overlap. This catches the case of multiple good  | 
448  |  |   // candidates that overlap each other when merged. If the worst  | 
449  |  |   // non-candidate overlap is better than the best overlap, then return  | 
450  |  |   // the worst non-candidate overlap instead.  | 
451  | 0  |   ColPartition_CLIST non_candidate_neighbours;  | 
452  | 0  |   non_candidate_neighbours.set_subtract(SortByBoxLeft<ColPartition>, true,  | 
453  | 0  |                                         &neighbours, candidates);  | 
454  | 0  |   int worst_nc_increase = 0;  | 
455  | 0  |   int best_increase = INT32_MAX;  | 
456  | 0  |   int best_area = 0;  | 
457  | 0  |   for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { | 
458  | 0  |     ColPartition *candidate = it.data();  | 
459  | 0  |     if (confirm_cb != nullptr && !confirm_cb(part, candidate)) { | 
460  | 0  |       if (debug) { | 
461  | 0  |         tprintf("Candidate not confirmed:"); | 
462  | 0  |         candidate->bounding_box().print();  | 
463  | 0  |       }  | 
464  | 0  |       continue;  | 
465  | 0  |     }  | 
466  | 0  |     int increase = IncreaseInOverlap(part, candidate, ok_overlap, &neighbours);  | 
467  | 0  |     const TBOX &cand_box = candidate->bounding_box();  | 
468  | 0  |     if (best_candidate == nullptr || increase < best_increase) { | 
469  | 0  |       best_candidate = candidate;  | 
470  | 0  |       best_increase = increase;  | 
471  | 0  |       best_area = cand_box.bounding_union(part_box).area() - cand_box.area();  | 
472  | 0  |       if (debug) { | 
473  | 0  |         tprintf("New best merge candidate has increase %d, area %d, over box:", | 
474  | 0  |                 increase, best_area);  | 
475  | 0  |         full_box.print();  | 
476  | 0  |         candidate->Print();  | 
477  | 0  |       }  | 
478  | 0  |     } else if (increase == best_increase) { | 
479  | 0  |       int area = cand_box.bounding_union(part_box).area() - cand_box.area();  | 
480  | 0  |       if (area < best_area) { | 
481  | 0  |         best_area = area;  | 
482  | 0  |         best_candidate = candidate;  | 
483  | 0  |       }  | 
484  | 0  |     }  | 
485  | 0  |     increase = IncreaseInOverlap(part, candidate, ok_overlap,  | 
486  | 0  |                                  &non_candidate_neighbours);  | 
487  | 0  |     if (increase > worst_nc_increase) { | 
488  | 0  |       worst_nc_increase = increase;  | 
489  | 0  |     }  | 
490  | 0  |   }  | 
491  | 0  |   if (best_increase > 0) { | 
492  |  |     // If the worst non-candidate increase is less than the best increase  | 
493  |  |     // including the candidates, then all the candidates can merge together  | 
494  |  |     // and the increase in outside overlap would be less, so use that result,  | 
495  |  |     // but only if each candidate is either a good diacritic merge with part,  | 
496  |  |     // or an ok merge candidate with all the others.  | 
497  |  |     // See TestCompatibleCandidates for more explanation and a picture.  | 
498  | 0  |     if (worst_nc_increase < best_increase &&  | 
499  | 0  |         TestCompatibleCandidates(*part, debug, candidates)) { | 
500  | 0  |       best_increase = worst_nc_increase;  | 
501  | 0  |     }  | 
502  | 0  |   }  | 
503  | 0  |   if (overlap_increase != nullptr) { | 
504  | 0  |     *overlap_increase = best_increase;  | 
505  | 0  |   }  | 
506  | 0  |   return best_candidate;  | 
507  | 0  | }  | 
508  |  |  | 
509  |  | // Helper to remove the given box from the given partition, put it in its  | 
510  |  | // own partition, and add to the partition list.  | 
511  |  | static void RemoveBadBox(BLOBNBOX *box, ColPartition *part,  | 
512  | 0  |                          ColPartition_LIST *part_list) { | 
513  | 0  |   part->RemoveBox(box);  | 
514  | 0  |   ColPartition::MakeBigPartition(box, part_list);  | 
515  | 0  | }  | 
516  |  |  | 
517  |  | // Split partitions where it reduces overlap between their bounding boxes.  | 
518  |  | // ColPartitions are after all supposed to be a partitioning of the blobs  | 
519  |  | // AND of the space on the page!  | 
520  |  | // Blobs that cause overlaps get removed, put in individual partitions  | 
521  |  | // and added to the big_parts list. They are most likely characters on  | 
522  |  | // 2 textlines that touch, or something big like a dropcap.  | 
523  |  | void ColPartitionGrid::SplitOverlappingPartitions(  | 
524  | 0  |     ColPartition_LIST *big_parts) { | 
525  | 0  |   int ok_overlap =  | 
526  | 0  |       static_cast<int>(kTinyEnoughTextlineOverlapFraction * gridsize() + 0.5);  | 
527  |  |   // Iterate the ColPartitions in the grid.  | 
528  | 0  |   ColPartitionGridSearch gsearch(this);  | 
529  | 0  |   gsearch.StartFullSearch();  | 
530  | 0  |   ColPartition *part;  | 
531  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
532  |  |     // Set up a rectangle search bounded by the part.  | 
533  | 0  |     const TBOX &box = part->bounding_box();  | 
534  | 0  |     ColPartitionGridSearch rsearch(this);  | 
535  | 0  |     rsearch.SetUniqueMode(true);  | 
536  | 0  |     rsearch.StartRectSearch(box);  | 
537  | 0  |     int unresolved_overlaps = 0;  | 
538  |  | 
  | 
539  | 0  |     ColPartition *neighbour;  | 
540  | 0  |     while ((neighbour = rsearch.NextRectSearch()) != nullptr) { | 
541  | 0  |       if (neighbour == part) { | 
542  | 0  |         continue;  | 
543  | 0  |       }  | 
544  | 0  |       const TBOX &neighbour_box = neighbour->bounding_box();  | 
545  | 0  |       if (neighbour->OKMergeOverlap(*part, *part, ok_overlap, false) &&  | 
546  | 0  |           part->OKMergeOverlap(*neighbour, *neighbour, ok_overlap, false)) { | 
547  | 0  |         continue; // The overlap is OK both ways.  | 
548  | 0  |       }  | 
549  |  |  | 
550  |  |       // If removal of the biggest box from either partition eliminates the  | 
551  |  |       // overlap, and it is much bigger than the box left behind, then  | 
552  |  |       // it is either a drop-cap, an inter-line join, or some junk that  | 
553  |  |       // we don't want anyway, so put it in the big_parts list.  | 
554  | 0  |       if (!part->IsSingleton()) { | 
555  | 0  |         BLOBNBOX *excluded = part->BiggestBox();  | 
556  | 0  |         TBOX shrunken = part->BoundsWithoutBox(excluded);  | 
557  | 0  |         if (!shrunken.overlap(neighbour_box) &&  | 
558  | 0  |             excluded->bounding_box().height() >  | 
559  | 0  |                 kBigPartSizeRatio * shrunken.height()) { | 
560  |  |           // Removing the biggest box fixes the overlap, so do it!  | 
561  | 0  |           gsearch.RemoveBBox();  | 
562  | 0  |           RemoveBadBox(excluded, part, big_parts);  | 
563  | 0  |           InsertBBox(true, true, part);  | 
564  | 0  |           gsearch.RepositionIterator();  | 
565  | 0  |           break;  | 
566  | 0  |         }  | 
567  | 0  |       } else if (box.contains(neighbour_box)) { | 
568  | 0  |         ++unresolved_overlaps;  | 
569  | 0  |         continue; // No amount of splitting will fix it.  | 
570  | 0  |       }  | 
571  | 0  |       if (!neighbour->IsSingleton()) { | 
572  | 0  |         BLOBNBOX *excluded = neighbour->BiggestBox();  | 
573  | 0  |         TBOX shrunken = neighbour->BoundsWithoutBox(excluded);  | 
574  | 0  |         if (!shrunken.overlap(box) &&  | 
575  | 0  |             excluded->bounding_box().height() >  | 
576  | 0  |                 kBigPartSizeRatio * shrunken.height()) { | 
577  |  |           // Removing the biggest box fixes the overlap, so do it!  | 
578  | 0  |           rsearch.RemoveBBox();  | 
579  | 0  |           RemoveBadBox(excluded, neighbour, big_parts);  | 
580  | 0  |           InsertBBox(true, true, neighbour);  | 
581  | 0  |           gsearch.RepositionIterator();  | 
582  | 0  |           break;  | 
583  | 0  |         }  | 
584  | 0  |       }  | 
585  | 0  |       int part_overlap_count = part->CountOverlappingBoxes(neighbour_box);  | 
586  | 0  |       int neighbour_overlap_count = neighbour->CountOverlappingBoxes(box);  | 
587  | 0  |       ColPartition *right_part = nullptr;  | 
588  | 0  |       if (neighbour_overlap_count <= part_overlap_count ||  | 
589  | 0  |           part->IsSingleton()) { | 
590  |  |         // Try to split the neighbour to reduce overlap.  | 
591  | 0  |         BLOBNBOX *split_blob = neighbour->OverlapSplitBlob(box);  | 
592  | 0  |         if (split_blob != nullptr) { | 
593  | 0  |           rsearch.RemoveBBox();  | 
594  | 0  |           right_part = neighbour->SplitAtBlob(split_blob);  | 
595  | 0  |           InsertBBox(true, true, neighbour);  | 
596  | 0  |           ASSERT_HOST(right_part != nullptr);  | 
597  | 0  |         }  | 
598  | 0  |       } else { | 
599  |  |         // Try to split part to reduce overlap.  | 
600  | 0  |         BLOBNBOX *split_blob = part->OverlapSplitBlob(neighbour_box);  | 
601  | 0  |         if (split_blob != nullptr) { | 
602  | 0  |           gsearch.RemoveBBox();  | 
603  | 0  |           right_part = part->SplitAtBlob(split_blob);  | 
604  | 0  |           InsertBBox(true, true, part);  | 
605  | 0  |           ASSERT_HOST(right_part != nullptr);  | 
606  | 0  |         }  | 
607  | 0  |       }  | 
608  | 0  |       if (right_part != nullptr) { | 
609  | 0  |         InsertBBox(true, true, right_part);  | 
610  | 0  |         gsearch.RepositionIterator();  | 
611  | 0  |         rsearch.RepositionIterator();  | 
612  | 0  |         break;  | 
613  | 0  |       }  | 
614  | 0  |     }  | 
615  | 0  |     if (unresolved_overlaps > 2 && part->IsSingleton()) { | 
616  |  |       // This part is no good so just add to big_parts.  | 
617  | 0  |       RemoveBBox(part);  | 
618  | 0  |       ColPartition_IT big_it(big_parts);  | 
619  | 0  |       part->set_block_owned(true);  | 
620  | 0  |       big_it.add_to_end(part);  | 
621  | 0  |       gsearch.RepositionIterator();  | 
622  | 0  |     }  | 
623  | 0  |   }  | 
624  | 0  | }  | 
625  |  |  | 
626  |  | // Filters partitions of source_type by looking at local neighbours.  | 
627  |  | // Where a majority of neighbours have a text type, the partitions are  | 
628  |  | // changed to text, where the neighbours have image type, they are changed  | 
629  |  | // to image, and partitions that have no definite neighbourhood type are  | 
630  |  | // left unchanged.  | 
631  |  | // im_box and rerotation are used to map blob coordinates onto the  | 
632  |  | // nontext_map, which is used to prevent the spread of text neighbourhoods  | 
633  |  | // into images.  | 
634  |  | // Returns true if anything was changed.  | 
635  |  | bool ColPartitionGrid::GridSmoothNeighbours(BlobTextFlowType source_type,  | 
636  |  |                                             Image nontext_map,  | 
637  |  |                                             const TBOX &im_box,  | 
638  | 0  |                                             const FCOORD &rotation) { | 
639  |  |   // Iterate the ColPartitions in the grid.  | 
640  | 0  |   ColPartitionGridSearch gsearch(this);  | 
641  | 0  |   gsearch.StartFullSearch();  | 
642  | 0  |   ColPartition *part;  | 
643  | 0  |   bool any_changed = false;  | 
644  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
645  | 0  |     if (part->flow() != source_type ||  | 
646  | 0  |         BLOBNBOX::IsLineType(part->blob_type())) { | 
647  | 0  |       continue;  | 
648  | 0  |     }  | 
649  | 0  |     const TBOX &box = part->bounding_box();  | 
650  | 0  |     bool debug = AlignedBlob::WithinTestRegion(2, box.left(), box.bottom());  | 
651  | 0  |     if (SmoothRegionType(nontext_map, im_box, rotation, debug, part)) { | 
652  | 0  |       any_changed = true;  | 
653  | 0  |     }  | 
654  | 0  |   }  | 
655  | 0  |   return any_changed;  | 
656  | 0  | }  | 
657  |  |  | 
658  |  | // Reflects the grid and its colpartitions in the y-axis, assuming that  | 
659  |  | // all blob boxes have already been done.  | 
660  | 0  | void ColPartitionGrid::ReflectInYAxis() { | 
661  | 0  |   ColPartition_LIST parts;  | 
662  | 0  |   ColPartition_IT part_it(&parts);  | 
663  |  |   // Iterate the ColPartitions in the grid to extract them.  | 
664  | 0  |   ColPartitionGridSearch gsearch(this);  | 
665  | 0  |   gsearch.StartFullSearch();  | 
666  | 0  |   ColPartition *part;  | 
667  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
668  | 0  |     part_it.add_after_then_move(part);  | 
669  | 0  |   }  | 
670  | 0  |   ICOORD bot_left(-tright().x(), bleft().y());  | 
671  | 0  |   ICOORD top_right(-bleft().x(), tright().y());  | 
672  |  |   // Reinitializing the grid with reflected coords also clears all the  | 
673  |  |   // pointers, so parts will now own the ColPartitions. (Briefly).  | 
674  | 0  |   Init(gridsize(), bot_left, top_right);  | 
675  | 0  |   for (part_it.move_to_first(); !part_it.empty(); part_it.forward()) { | 
676  | 0  |     part = part_it.extract();  | 
677  | 0  |     part->ReflectInYAxis();  | 
678  | 0  |     InsertBBox(true, true, part);  | 
679  | 0  |   }  | 
680  | 0  | }  | 
681  |  |  | 
682  |  | // Transforms the grid of partitions to the output blocks, putting each  | 
683  |  | // partition into a separate block. We don't really care about the order,  | 
684  |  | // as we just want to get as much text as possible without trying to organize  | 
685  |  | // it into proper blocks or columns.  | 
686  |  | // TODO(rays) some kind of sort function would be useful and probably better  | 
687  |  | // than the default here, which is to sort by order of the grid search.  | 
688  |  | void ColPartitionGrid::ExtractPartitionsAsBlocks(BLOCK_LIST *blocks,  | 
689  | 0  |                                                  TO_BLOCK_LIST *to_blocks) { | 
690  | 0  |   TO_BLOCK_IT to_block_it(to_blocks);  | 
691  | 0  |   BLOCK_IT block_it(blocks);  | 
692  |  |   // All partitions will be put on this list and deleted on return.  | 
693  | 0  |   ColPartition_LIST parts;  | 
694  | 0  |   ColPartition_IT part_it(&parts);  | 
695  |  |   // Iterate the ColPartitions in the grid to extract them.  | 
696  | 0  |   ColPartitionGridSearch gsearch(this);  | 
697  | 0  |   gsearch.StartFullSearch();  | 
698  | 0  |   ColPartition *part;  | 
699  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
700  | 0  |     part_it.add_after_then_move(part);  | 
701  |  |     // The partition has to be at least vaguely like text.  | 
702  | 0  |     BlobRegionType blob_type = part->blob_type();  | 
703  | 0  |     if (BLOBNBOX::IsTextType(blob_type) ||  | 
704  | 0  |         (blob_type == BRT_UNKNOWN && part->boxes_count() > 1)) { | 
705  | 0  |       PolyBlockType type =  | 
706  | 0  |           blob_type == BRT_VERT_TEXT ? PT_VERTICAL_TEXT : PT_FLOWING_TEXT;  | 
707  |  |       // Get metrics from the row that will be used for the block.  | 
708  | 0  |       TBOX box = part->bounding_box();  | 
709  | 0  |       int median_width = part->median_width();  | 
710  | 0  |       int median_height = part->median_height();  | 
711  |  |       // Turn the partition into a TO_ROW.  | 
712  | 0  |       TO_ROW *row = part->MakeToRow();  | 
713  | 0  |       if (row == nullptr) { | 
714  |  |         // This partition is dead.  | 
715  | 0  |         part->DeleteBoxes();  | 
716  | 0  |         continue;  | 
717  | 0  |       }  | 
718  | 0  |       auto *block = new BLOCK("", true, 0, 0, box.left(), box.bottom(), | 
719  | 0  |                               box.right(), box.top());  | 
720  | 0  |       block->pdblk.set_poly_block(new POLY_BLOCK(box, type));  | 
721  | 0  |       auto *to_block = new TO_BLOCK(block);  | 
722  | 0  |       TO_ROW_IT row_it(to_block->get_rows());  | 
723  | 0  |       row_it.add_after_then_move(row);  | 
724  |  |       // We haven't differentially rotated vertical and horizontal text at  | 
725  |  |       // this point, so use width or height as appropriate.  | 
726  | 0  |       if (blob_type == BRT_VERT_TEXT) { | 
727  | 0  |         to_block->line_size = static_cast<float>(median_width);  | 
728  | 0  |         to_block->line_spacing = static_cast<float>(box.width());  | 
729  | 0  |         to_block->max_blob_size = static_cast<float>(box.width() + 1);  | 
730  | 0  |       } else { | 
731  | 0  |         to_block->line_size = static_cast<float>(median_height);  | 
732  | 0  |         to_block->line_spacing = static_cast<float>(box.height());  | 
733  | 0  |         to_block->max_blob_size = static_cast<float>(box.height() + 1);  | 
734  | 0  |       }  | 
735  | 0  |       if (to_block->line_size == 0) { | 
736  | 0  |         to_block->line_size = 1;  | 
737  | 0  |       }  | 
738  | 0  |       block_it.add_to_end(block);  | 
739  | 0  |       to_block_it.add_to_end(to_block);  | 
740  | 0  |     } else { | 
741  |  |       // This partition is dead.  | 
742  | 0  |       part->DeleteBoxes();  | 
743  | 0  |     }  | 
744  | 0  |   }  | 
745  | 0  |   Clear();  | 
746  |  |   // Now it is safe to delete the ColPartitions as parts goes out of scope.  | 
747  | 0  | }  | 
748  |  |  | 
749  |  | // Rotates the grid and its colpartitions by the given angle, assuming that  | 
750  |  | // all blob boxes have already been done.  | 
751  | 0  | void ColPartitionGrid::Deskew(const FCOORD &deskew) { | 
752  | 0  |   ColPartition_LIST parts;  | 
753  | 0  |   ColPartition_IT part_it(&parts);  | 
754  |  |   // Iterate the ColPartitions in the grid to extract them.  | 
755  | 0  |   ColPartitionGridSearch gsearch(this);  | 
756  | 0  |   gsearch.StartFullSearch();  | 
757  | 0  |   ColPartition *part;  | 
758  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
759  | 0  |     part_it.add_after_then_move(part);  | 
760  | 0  |   }  | 
761  |  |   // Rebuild the grid to the new size.  | 
762  | 0  |   TBOX grid_box(bleft_, tright_);  | 
763  | 0  |   grid_box.rotate_large(deskew);  | 
764  | 0  |   Init(gridsize(), grid_box.botleft(), grid_box.topright());  | 
765  |  |   // Reinitializing the grid with rotated coords also clears all the  | 
766  |  |   // pointers, so parts will now own the ColPartitions. (Briefly).  | 
767  | 0  |   for (part_it.move_to_first(); !part_it.empty(); part_it.forward()) { | 
768  | 0  |     part = part_it.extract();  | 
769  | 0  |     part->ComputeLimits();  | 
770  | 0  |     InsertBBox(true, true, part);  | 
771  | 0  |   }  | 
772  | 0  | }  | 
773  |  |  | 
774  |  | // Sets the left and right tabs of the partitions in the grid.  | 
775  | 0  | void ColPartitionGrid::SetTabStops(TabFind *tabgrid) { | 
776  |  |   // Iterate the ColPartitions in the grid.  | 
777  | 0  |   ColPartitionGridSearch gsearch(this);  | 
778  | 0  |   gsearch.StartFullSearch();  | 
779  | 0  |   ColPartition *part;  | 
780  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
781  | 0  |     const TBOX &part_box = part->bounding_box();  | 
782  | 0  |     TabVector *left_line = tabgrid->LeftTabForBox(part_box, true, false);  | 
783  |  |     // If the overlapping line is not a left tab, try for non-overlapping.  | 
784  | 0  |     if (left_line != nullptr && !left_line->IsLeftTab()) { | 
785  | 0  |       left_line = tabgrid->LeftTabForBox(part_box, false, false);  | 
786  | 0  |     }  | 
787  | 0  |     if (left_line != nullptr && left_line->IsLeftTab()) { | 
788  | 0  |       part->SetLeftTab(left_line);  | 
789  | 0  |     }  | 
790  | 0  |     TabVector *right_line = tabgrid->RightTabForBox(part_box, true, false);  | 
791  | 0  |     if (right_line != nullptr && !right_line->IsRightTab()) { | 
792  | 0  |       right_line = tabgrid->RightTabForBox(part_box, false, false);  | 
793  | 0  |     }  | 
794  | 0  |     if (right_line != nullptr && right_line->IsRightTab()) { | 
795  | 0  |       part->SetRightTab(right_line);  | 
796  | 0  |     }  | 
797  | 0  |     part->SetColumnGoodness(tabgrid->WidthCB());  | 
798  | 0  |   }  | 
799  | 0  | }  | 
800  |  |  | 
801  |  | // Makes the ColPartSets and puts them in the PartSetVector ready  | 
802  |  | // for finding column bounds. Returns false if no partitions were found.  | 
803  | 0  | bool ColPartitionGrid::MakeColPartSets(PartSetVector *part_sets) { | 
804  | 0  |   auto *part_lists = new ColPartition_LIST[gridheight()];  | 
805  | 0  |   part_sets->reserve(gridheight());  | 
806  |  |   // Iterate the ColPartitions in the grid to get parts onto lists for the  | 
807  |  |   // y bottom of each.  | 
808  | 0  |   ColPartitionGridSearch gsearch(this);  | 
809  | 0  |   gsearch.StartFullSearch();  | 
810  | 0  |   ColPartition *part;  | 
811  | 0  |   bool any_parts_found = false;  | 
812  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
813  | 0  |     BlobRegionType blob_type = part->blob_type();  | 
814  | 0  |     if (blob_type != BRT_NOISE &&  | 
815  | 0  |         (blob_type != BRT_UNKNOWN || !part->boxes()->singleton())) { | 
816  | 0  |       int grid_x, grid_y;  | 
817  | 0  |       const TBOX &part_box = part->bounding_box();  | 
818  | 0  |       GridCoords(part_box.left(), part_box.bottom(), &grid_x, &grid_y);  | 
819  | 0  |       ColPartition_IT part_it(&part_lists[grid_y]);  | 
820  | 0  |       part_it.add_to_end(part);  | 
821  | 0  |       any_parts_found = true;  | 
822  | 0  |     }  | 
823  | 0  |   }  | 
824  | 0  |   if (any_parts_found) { | 
825  | 0  |     for (int grid_y = 0; grid_y < gridheight(); ++grid_y) { | 
826  | 0  |       ColPartitionSet *line_set = nullptr;  | 
827  | 0  |       if (!part_lists[grid_y].empty()) { | 
828  | 0  |         line_set = new ColPartitionSet(&part_lists[grid_y]);  | 
829  | 0  |       }  | 
830  | 0  |       part_sets->push_back(line_set);  | 
831  | 0  |     }  | 
832  | 0  |   }  | 
833  | 0  |   delete[] part_lists;  | 
834  | 0  |   return any_parts_found;  | 
835  | 0  | }  | 
836  |  |  | 
837  |  | // Makes a single ColPartitionSet consisting of a single ColPartition that  | 
838  |  | // represents the total horizontal extent of the significant content on the  | 
839  |  | // page. Used for the single column setting in place of automatic detection.  | 
840  |  | // Returns nullptr if the page is empty of significant content.  | 
841  | 0  | ColPartitionSet *ColPartitionGrid::MakeSingleColumnSet(WidthCallback cb) { | 
842  | 0  |   ColPartition *single_column_part = nullptr;  | 
843  |  |   // Iterate the ColPartitions in the grid to get parts onto lists for the  | 
844  |  |   // y bottom of each.  | 
845  | 0  |   ColPartitionGridSearch gsearch(this);  | 
846  | 0  |   gsearch.StartFullSearch();  | 
847  | 0  |   ColPartition *part;  | 
848  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
849  | 0  |     BlobRegionType blob_type = part->blob_type();  | 
850  | 0  |     if (blob_type != BRT_NOISE &&  | 
851  | 0  |         (blob_type != BRT_UNKNOWN || !part->boxes()->singleton())) { | 
852  |  |       // Consider for single column.  | 
853  | 0  |       BlobTextFlowType flow = part->flow();  | 
854  | 0  |       if ((blob_type == BRT_TEXT &&  | 
855  | 0  |            (flow == BTFT_STRONG_CHAIN || flow == BTFT_CHAIN ||  | 
856  | 0  |             flow == BTFT_LEADER || flow == BTFT_TEXT_ON_IMAGE)) ||  | 
857  | 0  |           blob_type == BRT_RECTIMAGE || blob_type == BRT_POLYIMAGE) { | 
858  | 0  |         if (single_column_part == nullptr) { | 
859  | 0  |           single_column_part = part->ShallowCopy();  | 
860  | 0  |           single_column_part->set_blob_type(BRT_TEXT);  | 
861  |  |           // Copy the tabs from itself to properly setup the margins.  | 
862  | 0  |           single_column_part->CopyLeftTab(*single_column_part, false);  | 
863  | 0  |           single_column_part->CopyRightTab(*single_column_part, false);  | 
864  | 0  |         } else { | 
865  | 0  |           if (part->left_key() < single_column_part->left_key()) { | 
866  | 0  |             single_column_part->CopyLeftTab(*part, false);  | 
867  | 0  |           }  | 
868  | 0  |           if (part->right_key() > single_column_part->right_key()) { | 
869  | 0  |             single_column_part->CopyRightTab(*part, false);  | 
870  | 0  |           }  | 
871  | 0  |         }  | 
872  | 0  |       }  | 
873  | 0  |     }  | 
874  | 0  |   }  | 
875  | 0  |   if (single_column_part != nullptr) { | 
876  |  |     // Make a ColPartitionSet out of the single_column_part as a candidate  | 
877  |  |     // for the single column case.  | 
878  | 0  |     single_column_part->SetColumnGoodness(cb);  | 
879  | 0  |     return new ColPartitionSet(single_column_part);  | 
880  | 0  |   }  | 
881  | 0  |   return nullptr;  | 
882  | 0  | }  | 
883  |  |  | 
884  |  | // Mark the BLOBNBOXes in each partition as being owned by that partition.  | 
885  | 0  | void ColPartitionGrid::ClaimBoxes() { | 
886  |  |   // Iterate the ColPartitions in the grid.  | 
887  | 0  |   ColPartitionGridSearch gsearch(this);  | 
888  | 0  |   gsearch.StartFullSearch();  | 
889  | 0  |   ColPartition *part;  | 
890  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
891  | 0  |     part->ClaimBoxes();  | 
892  | 0  |   }  | 
893  | 0  | }  | 
894  |  |  | 
895  |  | // Retypes all the blobs referenced by the partitions in the grid.  | 
896  |  | // Image blobs are found and returned in the im_blobs list, as they are not  | 
897  |  | // owned by the block.  | 
898  | 0  | void ColPartitionGrid::ReTypeBlobs(BLOBNBOX_LIST *im_blobs) { | 
899  | 0  |   BLOBNBOX_IT im_blob_it(im_blobs);  | 
900  | 0  |   ColPartition_LIST dead_parts;  | 
901  | 0  |   ColPartition_IT dead_part_it(&dead_parts);  | 
902  |  |   // Iterate the ColPartitions in the grid.  | 
903  | 0  |   ColPartitionGridSearch gsearch(this);  | 
904  | 0  |   gsearch.StartFullSearch();  | 
905  | 0  |   ColPartition *part;  | 
906  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
907  | 0  |     BlobRegionType blob_type = part->blob_type();  | 
908  | 0  |     BlobTextFlowType flow = part->flow();  | 
909  | 0  |     bool any_blobs_moved = false;  | 
910  | 0  |     if (blob_type == BRT_POLYIMAGE || blob_type == BRT_RECTIMAGE) { | 
911  | 0  |       BLOBNBOX_C_IT blob_it(part->boxes());  | 
912  | 0  |       for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { | 
913  | 0  |         BLOBNBOX *blob = blob_it.data();  | 
914  | 0  |         im_blob_it.add_after_then_move(blob);  | 
915  | 0  |       }  | 
916  | 0  |     } else if (blob_type != BRT_NOISE) { | 
917  |  |       // Make sure the blobs are marked with the correct type and flow.  | 
918  | 0  |       BLOBNBOX_C_IT blob_it(part->boxes());  | 
919  | 0  |       for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { | 
920  | 0  |         BLOBNBOX *blob = blob_it.data();  | 
921  | 0  |         if (blob->region_type() == BRT_NOISE) { | 
922  |  |           // TODO(rays) Deprecated. Change this section to an assert to verify  | 
923  |  |           // and then delete.  | 
924  | 0  |           ASSERT_HOST(blob->cblob()->area() != 0);  | 
925  | 0  |           blob->set_owner(nullptr);  | 
926  | 0  |           blob_it.extract();  | 
927  | 0  |           any_blobs_moved = true;  | 
928  | 0  |         } else { | 
929  | 0  |           blob->set_region_type(blob_type);  | 
930  | 0  |           if (blob->flow() != BTFT_LEADER) { | 
931  | 0  |             blob->set_flow(flow);  | 
932  | 0  |           }  | 
933  | 0  |         }  | 
934  | 0  |       }  | 
935  | 0  |     }  | 
936  | 0  |     if (blob_type == BRT_NOISE || part->boxes()->empty()) { | 
937  | 0  |       BLOBNBOX_C_IT blob_it(part->boxes());  | 
938  | 0  |       part->DisownBoxes();  | 
939  | 0  |       dead_part_it.add_to_end(part);  | 
940  | 0  |       gsearch.RemoveBBox();  | 
941  | 0  |       for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { | 
942  | 0  |         BLOBNBOX *blob = blob_it.data();  | 
943  | 0  |         if (blob->cblob()->area() == 0) { | 
944  |  |           // Any blob with zero area is a fake image blob and should be deleted.  | 
945  | 0  |           delete blob->cblob();  | 
946  | 0  |           delete blob;  | 
947  | 0  |         }  | 
948  | 0  |       }  | 
949  | 0  |     } else if (any_blobs_moved) { | 
950  | 0  |       gsearch.RemoveBBox();  | 
951  | 0  |       part->ComputeLimits();  | 
952  | 0  |       InsertBBox(true, true, part);  | 
953  | 0  |       gsearch.RepositionIterator();  | 
954  | 0  |     }  | 
955  | 0  |   }  | 
956  | 0  | }  | 
957  |  |  | 
958  |  | // The boxes within the partitions have changed (by deskew) so recompute  | 
959  |  | // the bounds of all the partitions and reinsert them into the grid.  | 
960  |  | void ColPartitionGrid::RecomputeBounds(int gridsize, const ICOORD &bleft,  | 
961  |  |                                        const ICOORD &tright,  | 
962  | 0  |                                        const ICOORD &vertical) { | 
963  | 0  |   ColPartition_LIST saved_parts;  | 
964  | 0  |   ColPartition_IT part_it(&saved_parts);  | 
965  |  |   // Iterate the ColPartitions in the grid to get parts onto a list.  | 
966  | 0  |   ColPartitionGridSearch gsearch(this);  | 
967  | 0  |   gsearch.StartFullSearch();  | 
968  | 0  |   ColPartition *part;  | 
969  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
970  | 0  |     part_it.add_to_end(part);  | 
971  | 0  |   }  | 
972  |  |   // Reinitialize grid to the new size.  | 
973  | 0  |   Init(gridsize, bleft, tright);  | 
974  |  |   // Recompute the bounds of the parts and put them back in the new grid.  | 
975  | 0  |   for (part_it.move_to_first(); !part_it.empty(); part_it.forward()) { | 
976  | 0  |     part = part_it.extract();  | 
977  | 0  |     part->set_vertical(vertical);  | 
978  | 0  |     part->ComputeLimits();  | 
979  | 0  |     InsertBBox(true, true, part);  | 
980  | 0  |   }  | 
981  | 0  | }  | 
982  |  |  | 
983  |  | // Improves the margins of the ColPartitions in the grid by calling  | 
984  |  | // FindPartitionMargins on each.  | 
985  |  | // best_columns, which may be nullptr, is an array of pointers indicating the  | 
986  |  | // column set at each y-coordinate in the grid.  | 
987  |  | // best_columns is usually the best_columns_ member of ColumnFinder.  | 
988  | 0  | void ColPartitionGrid::GridFindMargins(ColPartitionSet **best_columns) { | 
989  |  |   // Iterate the ColPartitions in the grid.  | 
990  | 0  |   ColPartitionGridSearch gsearch(this);  | 
991  | 0  |   gsearch.StartFullSearch();  | 
992  | 0  |   ColPartition *part;  | 
993  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
994  |  |     // Set up a rectangle search x-bounded by the column and y by the part.  | 
995  | 0  |     ColPartitionSet *columns =  | 
996  | 0  |         best_columns != nullptr ? best_columns[gsearch.GridY()] : nullptr;  | 
997  | 0  |     FindPartitionMargins(columns, part);  | 
998  | 0  |     const TBOX &box = part->bounding_box();  | 
999  | 0  |     if (AlignedBlob::WithinTestRegion(2, box.left(), box.bottom())) { | 
1000  | 0  |       tprintf("Computed margins for part:"); | 
1001  | 0  |       part->Print();  | 
1002  | 0  |     }  | 
1003  | 0  |   }  | 
1004  | 0  | }  | 
1005  |  |  | 
1006  |  | // Improves the margins of the ColPartitions in the list by calling  | 
1007  |  | // FindPartitionMargins on each.  | 
1008  |  | // best_columns, which may be nullptr, is an array of pointers indicating the  | 
1009  |  | // column set at each y-coordinate in the grid.  | 
1010  |  | // best_columns is usually the best_columns_ member of ColumnFinder.  | 
1011  |  | void ColPartitionGrid::ListFindMargins(ColPartitionSet **best_columns,  | 
1012  | 0  |                                        ColPartition_LIST *parts) { | 
1013  | 0  |   ColPartition_IT part_it(parts);  | 
1014  | 0  |   for (part_it.mark_cycle_pt(); !part_it.cycled_list(); part_it.forward()) { | 
1015  | 0  |     ColPartition *part = part_it.data();  | 
1016  | 0  |     ColPartitionSet *columns = nullptr;  | 
1017  | 0  |     if (best_columns != nullptr) { | 
1018  | 0  |       const TBOX &part_box = part->bounding_box();  | 
1019  |  |       // Get the columns from the y grid coord.  | 
1020  | 0  |       int grid_x, grid_y;  | 
1021  | 0  |       GridCoords(part_box.left(), part_box.bottom(), &grid_x, &grid_y);  | 
1022  | 0  |       columns = best_columns[grid_y];  | 
1023  | 0  |     }  | 
1024  | 0  |     FindPartitionMargins(columns, part);  | 
1025  | 0  |   }  | 
1026  | 0  | }  | 
1027  |  |  | 
1028  |  | // Deletes all the partitions in the grid after disowning all the blobs.  | 
1029  | 0  | void ColPartitionGrid::DeleteParts() { | 
1030  | 0  |   ColPartition_LIST dead_parts;  | 
1031  | 0  |   ColPartition_IT dead_it(&dead_parts);  | 
1032  | 0  |   ColPartitionGridSearch gsearch(this);  | 
1033  | 0  |   gsearch.StartFullSearch();  | 
1034  | 0  |   ColPartition *part;  | 
1035  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
1036  | 0  |     part->DisownBoxes();  | 
1037  | 0  |     dead_it.add_to_end(part); // Parts will be deleted on return.  | 
1038  | 0  |   }  | 
1039  | 0  |   Clear();  | 
1040  | 0  | }  | 
1041  |  |  | 
1042  |  | // Deletes all the partitions in the grid that are of type BRT_UNKNOWN and  | 
1043  |  | // all the blobs in them.  | 
1044  | 0  | void ColPartitionGrid::DeleteUnknownParts(TO_BLOCK *block) { | 
1045  | 0  |   ColPartitionGridSearch gsearch(this);  | 
1046  | 0  |   gsearch.StartFullSearch();  | 
1047  | 0  |   ColPartition *part;  | 
1048  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
1049  | 0  |     if (part->blob_type() == BRT_UNKNOWN) { | 
1050  | 0  |       gsearch.RemoveBBox();  | 
1051  |  |       // Once marked, the blobs will be swept up by DeleteUnownedNoise.  | 
1052  | 0  |       part->set_flow(BTFT_NONTEXT);  | 
1053  | 0  |       part->set_blob_type(BRT_NOISE);  | 
1054  | 0  |       part->SetBlobTypes();  | 
1055  | 0  |       part->DisownBoxes();  | 
1056  | 0  |       delete part;  | 
1057  | 0  |     }  | 
1058  | 0  |   }  | 
1059  | 0  |   block->DeleteUnownedNoise();  | 
1060  | 0  | }  | 
1061  |  |  | 
1062  |  | // Deletes all the partitions in the grid that are NOT of flow type BTFT_LEADER.  | 
1063  | 0  | void ColPartitionGrid::DeleteNonLeaderParts() { | 
1064  | 0  |   ColPartitionGridSearch gsearch(this);  | 
1065  | 0  |   gsearch.StartFullSearch();  | 
1066  | 0  |   ColPartition *part;  | 
1067  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
1068  | 0  |     if (part->flow() != BTFT_LEADER) { | 
1069  | 0  |       gsearch.RemoveBBox();  | 
1070  | 0  |       if (part->ReleaseNonLeaderBoxes()) { | 
1071  | 0  |         InsertBBox(true, true, part);  | 
1072  | 0  |         gsearch.RepositionIterator();  | 
1073  | 0  |       } else { | 
1074  | 0  |         delete part;  | 
1075  | 0  |       }  | 
1076  | 0  |     }  | 
1077  | 0  |   }  | 
1078  | 0  | }  | 
1079  |  |  | 
1080  |  | // Finds and marks text partitions that represent figure captions.  | 
1081  | 0  | void ColPartitionGrid::FindFigureCaptions() { | 
1082  |  |   // For each image region find its best candidate text caption region,  | 
1083  |  |   // if any and mark it as such.  | 
1084  | 0  |   ColPartitionGridSearch gsearch(this);  | 
1085  | 0  |   gsearch.StartFullSearch();  | 
1086  | 0  |   ColPartition *part;  | 
1087  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
1088  | 0  |     if (part->IsImageType()) { | 
1089  | 0  |       const TBOX &part_box = part->bounding_box();  | 
1090  | 0  |       bool debug =  | 
1091  | 0  |           AlignedBlob::WithinTestRegion(2, part_box.left(), part_box.bottom());  | 
1092  | 0  |       ColPartition *best_caption = nullptr;  | 
1093  | 0  |       int best_dist = 0;  // Distance to best_caption.  | 
1094  | 0  |       int best_upper = 0; // Direction of best_caption.  | 
1095  |  |       // Handle both lower and upper directions.  | 
1096  | 0  |       for (int upper = 0; upper < 2; ++upper) { | 
1097  | 0  |         ColPartition_C_IT partner_it(upper ? part->upper_partners()  | 
1098  | 0  |                                            : part->lower_partners());  | 
1099  |  |         // If there are no image partners, then this direction is ok.  | 
1100  | 0  |         for (partner_it.mark_cycle_pt(); !partner_it.cycled_list();  | 
1101  | 0  |              partner_it.forward()) { | 
1102  | 0  |           ColPartition *partner = partner_it.data();  | 
1103  | 0  |           if (partner->IsImageType()) { | 
1104  | 0  |             break;  | 
1105  | 0  |           }  | 
1106  | 0  |         }  | 
1107  | 0  |         if (!partner_it.cycled_list()) { | 
1108  | 0  |           continue;  | 
1109  | 0  |         }  | 
1110  |  |         // Find the nearest totally overlapping text partner.  | 
1111  | 0  |         for (partner_it.mark_cycle_pt(); !partner_it.cycled_list();  | 
1112  | 0  |              partner_it.forward()) { | 
1113  | 0  |           ColPartition *partner = partner_it.data();  | 
1114  | 0  |           if (!partner->IsTextType() || partner->type() == PT_TABLE) { | 
1115  | 0  |             continue;  | 
1116  | 0  |           }  | 
1117  | 0  |           const TBOX &partner_box = partner->bounding_box();  | 
1118  | 0  |           if (debug) { | 
1119  | 0  |             tprintf("Finding figure captions for image part:"); | 
1120  | 0  |             part_box.print();  | 
1121  | 0  |             tprintf("Considering partner:"); | 
1122  | 0  |             partner_box.print();  | 
1123  | 0  |           }  | 
1124  | 0  |           if (partner_box.left() >= part_box.left() &&  | 
1125  | 0  |               partner_box.right() <= part_box.right()) { | 
1126  | 0  |             int dist = partner_box.y_gap(part_box);  | 
1127  | 0  |             if (best_caption == nullptr || dist < best_dist) { | 
1128  | 0  |               best_dist = dist;  | 
1129  | 0  |               best_caption = partner;  | 
1130  | 0  |               best_upper = upper;  | 
1131  | 0  |             }  | 
1132  | 0  |           }  | 
1133  | 0  |         }  | 
1134  | 0  |       }  | 
1135  | 0  |       if (best_caption != nullptr) { | 
1136  | 0  |         if (debug) { | 
1137  | 0  |           tprintf("Best caption candidate:"); | 
1138  | 0  |           best_caption->bounding_box().print();  | 
1139  | 0  |         }  | 
1140  |  |         // We have a candidate caption. Qualify it as being separable from  | 
1141  |  |         // any body text. We are looking for either a small number of lines  | 
1142  |  |         // or a big gap that indicates a separation from the body text.  | 
1143  | 0  |         int line_count = 0;  | 
1144  | 0  |         int biggest_gap = 0;  | 
1145  | 0  |         int smallest_gap = INT16_MAX;  | 
1146  | 0  |         int total_height = 0;  | 
1147  | 0  |         int mean_height = 0;  | 
1148  | 0  |         ColPartition *end_partner = nullptr;  | 
1149  | 0  |         ColPartition *next_partner = nullptr;  | 
1150  | 0  |         for (ColPartition *partner = best_caption;  | 
1151  | 0  |              partner != nullptr && line_count <= kMaxCaptionLines;  | 
1152  | 0  |              partner = next_partner) { | 
1153  | 0  |           if (!partner->IsTextType()) { | 
1154  | 0  |             end_partner = partner;  | 
1155  | 0  |             break;  | 
1156  | 0  |           }  | 
1157  | 0  |           ++line_count;  | 
1158  | 0  |           total_height += partner->bounding_box().height();  | 
1159  | 0  |           next_partner = partner->SingletonPartner(best_upper);  | 
1160  | 0  |           if (next_partner != nullptr) { | 
1161  | 0  |             int gap =  | 
1162  | 0  |                 partner->bounding_box().y_gap(next_partner->bounding_box());  | 
1163  | 0  |             if (gap > biggest_gap) { | 
1164  | 0  |               biggest_gap = gap;  | 
1165  | 0  |               end_partner = next_partner;  | 
1166  | 0  |               mean_height = total_height / line_count;  | 
1167  | 0  |             } else if (gap < smallest_gap) { | 
1168  | 0  |               smallest_gap = gap;  | 
1169  | 0  |             }  | 
1170  |  |             // If the gap looks big compared to the text size and the smallest  | 
1171  |  |             // gap seen so far, then we can stop.  | 
1172  | 0  |             if (biggest_gap > mean_height * kMinCaptionGapHeightRatio &&  | 
1173  | 0  |                 biggest_gap > smallest_gap * kMinCaptionGapRatio) { | 
1174  | 0  |               break;  | 
1175  | 0  |             }  | 
1176  | 0  |           }  | 
1177  | 0  |         }  | 
1178  | 0  |         if (debug) { | 
1179  | 0  |           tprintf("Line count=%d, biggest gap %d, smallest%d, mean height %d\n", | 
1180  | 0  |                   line_count, biggest_gap, smallest_gap, mean_height);  | 
1181  | 0  |           if (end_partner != nullptr) { | 
1182  | 0  |             tprintf("End partner:"); | 
1183  | 0  |             end_partner->bounding_box().print();  | 
1184  | 0  |           }  | 
1185  | 0  |         }  | 
1186  | 0  |         if (next_partner == nullptr && line_count <= kMaxCaptionLines) { | 
1187  | 0  |           end_partner = nullptr; // No gap, but line count is small.  | 
1188  | 0  |         }  | 
1189  | 0  |         if (line_count <= kMaxCaptionLines) { | 
1190  |  |           // This is a qualified caption. Mark the text as caption.  | 
1191  | 0  |           for (ColPartition *partner = best_caption;  | 
1192  | 0  |                partner != nullptr && partner != end_partner;  | 
1193  | 0  |                partner = next_partner) { | 
1194  | 0  |             partner->set_type(PT_CAPTION_TEXT);  | 
1195  | 0  |             partner->SetBlobTypes();  | 
1196  | 0  |             if (debug) { | 
1197  | 0  |               tprintf("Set caption type for partition:"); | 
1198  | 0  |               partner->bounding_box().print();  | 
1199  | 0  |             }  | 
1200  | 0  |             next_partner = partner->SingletonPartner(best_upper);  | 
1201  | 0  |           }  | 
1202  | 0  |         }  | 
1203  | 0  |       }  | 
1204  | 0  |     }  | 
1205  | 0  |   }  | 
1206  | 0  | }  | 
1207  |  |  | 
1208  |  | //////// Functions that manipulate ColPartitions in the part_grid_ /////  | 
1209  |  | //////// to find chains of partner partitions of the same type.  ///////  | 
1210  |  |  | 
1211  |  | // For every ColPartition in the grid, finds its upper and lower neighbours.  | 
1212  | 0  | void ColPartitionGrid::FindPartitionPartners() { | 
1213  | 0  |   ColPartitionGridSearch gsearch(this);  | 
1214  | 0  |   gsearch.StartFullSearch();  | 
1215  | 0  |   ColPartition *part;  | 
1216  | 0  |   while ((part = gsearch.NextFullSearch()) != nullptr) { | 
1217  | 0  |     if (part->IsVerticalType()) { | 
1218  | 0  |       FindVPartitionPartners(true, part);  | 
1219  | 0  |       FindVPartitionPartners(false, part);  | 
1220  | 0  |     } else { | 
1221  | 0  |       FindPartitionPartners(true, part);  | 
1222  | 0  |       FindPartitionPartners(false, part);  | 
1223  | 0  |     }  | 
1224  | 0  |   }  | 
1225  | 0  | }  | 
1226  |  |  | 
1227  |  | // Finds the best partner in the given direction for the given partition.  | 
1228  |  | // Stores the result with AddPartner.  | 
1229  | 0  | void ColPartitionGrid::FindPartitionPartners(bool upper, ColPartition *part) { | 
1230  | 0  |   if (part->type() == PT_NOISE) { | 
1231  | 0  |     return; // Noise is not allowed to partner anything.  | 
1232  | 0  |   }  | 
1233  | 0  |   const TBOX &box = part->bounding_box();  | 
1234  | 0  |   int top = part->median_top();  | 
1235  | 0  |   int bottom = part->median_bottom();  | 
1236  | 0  |   int height = top - bottom;  | 
1237  | 0  |   int mid_y = (bottom + top) / 2;  | 
1238  | 0  |   ColPartitionGridSearch vsearch(this);  | 
1239  |  |   // Search down for neighbour below  | 
1240  | 0  |   vsearch.StartVerticalSearch(box.left(), box.right(), part->MidY());  | 
1241  | 0  |   ColPartition *neighbour;  | 
1242  | 0  |   ColPartition *best_neighbour = nullptr;  | 
1243  | 0  |   int best_dist = INT32_MAX;  | 
1244  | 0  |   while ((neighbour = vsearch.NextVerticalSearch(!upper)) != nullptr) { | 
1245  | 0  |     if (neighbour == part || neighbour->type() == PT_NOISE) { | 
1246  | 0  |       continue; // Noise is not allowed to partner anything.  | 
1247  | 0  |     }  | 
1248  | 0  |     int neighbour_bottom = neighbour->median_bottom();  | 
1249  | 0  |     int neighbour_top = neighbour->median_top();  | 
1250  | 0  |     int neighbour_y = (neighbour_bottom + neighbour_top) / 2;  | 
1251  | 0  |     if (upper != (neighbour_y > mid_y)) { | 
1252  | 0  |       continue;  | 
1253  | 0  |     }  | 
1254  | 0  |     if (!part->HOverlaps(*neighbour) && !part->WithinSameMargins(*neighbour)) { | 
1255  | 0  |       continue;  | 
1256  | 0  |     }  | 
1257  | 0  |     if (!part->TypesMatch(*neighbour)) { | 
1258  | 0  |       if (best_neighbour == nullptr) { | 
1259  | 0  |         best_neighbour = neighbour;  | 
1260  | 0  |       }  | 
1261  | 0  |       continue;  | 
1262  | 0  |     }  | 
1263  | 0  |     int dist = upper ? neighbour_bottom - top : bottom - neighbour_top;  | 
1264  | 0  |     if (dist <= kMaxPartitionSpacing * height) { | 
1265  | 0  |       if (dist < best_dist) { | 
1266  | 0  |         best_dist = dist;  | 
1267  | 0  |         best_neighbour = neighbour;  | 
1268  | 0  |       }  | 
1269  | 0  |     } else { | 
1270  | 0  |       break;  | 
1271  | 0  |     }  | 
1272  | 0  |   }  | 
1273  | 0  |   if (best_neighbour != nullptr) { | 
1274  | 0  |     part->AddPartner(upper, best_neighbour);  | 
1275  | 0  |   }  | 
1276  | 0  | }  | 
1277  |  |  | 
1278  |  | // Finds the best partner in the given direction for the given partition.  | 
1279  |  | // Stores the result with AddPartner.  | 
1280  |  | void ColPartitionGrid::FindVPartitionPartners(bool to_the_left,  | 
1281  | 0  |                                               ColPartition *part) { | 
1282  | 0  |   if (part->type() == PT_NOISE) { | 
1283  | 0  |     return; // Noise is not allowed to partner anything.  | 
1284  | 0  |   }  | 
1285  | 0  |   const TBOX &box = part->bounding_box();  | 
1286  | 0  |   int left = part->median_left();  | 
1287  | 0  |   int right = part->median_right();  | 
1288  | 0  |   int width = right >= left ? right - left : -1;  | 
1289  | 0  |   int mid_x = (left + right) / 2;  | 
1290  | 0  |   ColPartitionGridSearch hsearch(this);  | 
1291  |  |   // Search left for neighbour to_the_left  | 
1292  | 0  |   hsearch.StartSideSearch(mid_x, box.bottom(), box.top());  | 
1293  | 0  |   ColPartition *neighbour;  | 
1294  | 0  |   ColPartition *best_neighbour = nullptr;  | 
1295  | 0  |   int best_dist = INT32_MAX;  | 
1296  | 0  |   while ((neighbour = hsearch.NextSideSearch(to_the_left)) != nullptr) { | 
1297  | 0  |     if (neighbour == part || neighbour->type() == PT_NOISE) { | 
1298  | 0  |       continue; // Noise is not allowed to partner anything.  | 
1299  | 0  |     }  | 
1300  | 0  |     int neighbour_left = neighbour->median_left();  | 
1301  | 0  |     int neighbour_right = neighbour->median_right();  | 
1302  | 0  |     int neighbour_x = (neighbour_left + neighbour_right) / 2;  | 
1303  | 0  |     if (to_the_left != (neighbour_x < mid_x)) { | 
1304  | 0  |       continue;  | 
1305  | 0  |     }  | 
1306  | 0  |     if (!part->VOverlaps(*neighbour)) { | 
1307  | 0  |       continue;  | 
1308  | 0  |     }  | 
1309  | 0  |     if (!part->TypesMatch(*neighbour)) { | 
1310  | 0  |       continue; // Only match to other vertical text.  | 
1311  | 0  |     }  | 
1312  | 0  |     int dist = to_the_left ? left - neighbour_right : neighbour_left - right;  | 
1313  | 0  |     if (dist <= kMaxPartitionSpacing * width) { | 
1314  | 0  |       if (dist < best_dist || best_neighbour == nullptr) { | 
1315  | 0  |         best_dist = dist;  | 
1316  | 0  |         best_neighbour = neighbour;  | 
1317  | 0  |       }  | 
1318  | 0  |     } else { | 
1319  | 0  |       break;  | 
1320  | 0  |     }  | 
1321  | 0  |   }  | 
1322  |  |   // For vertical partitions, the upper partner is to the left, and lower is  | 
1323  |  |   // to the right.  | 
1324  | 0  |   if (best_neighbour != nullptr) { | 
1325  | 0  |     part->AddPartner(to_the_left, best_neighbour);  | 
1326  | 0  |   }  | 
1327  | 0  | }  | 
1328  |  |  | 
1329  |  | // For every ColPartition with multiple partners in the grid, reduces the  | 
1330  |  | // number of partners to 0 or 1. If get_desperate is true, goes to more  | 
1331  |  | // desperate merge methods to merge flowing text before breaking partnerships.  | 
1332  | 0  | void ColPartitionGrid::RefinePartitionPartners(bool get_desperate) { | 
1333  | 0  |   ColPartitionGridSearch gsearch(this);  | 
1334  |  |   // Refine in type order so that chasing multiple partners can be done  | 
1335  |  |   // before eliminating type mis-matching partners.  | 
1336  | 0  |   for (int type = PT_UNKNOWN + 1; type <= PT_COUNT; type++) { | 
1337  |  |     // Iterate the ColPartitions in the grid.  | 
1338  | 0  |     gsearch.StartFullSearch();  | 
1339  | 0  |     ColPartition *part;  | 
1340  | 0  |     while ((part = gsearch.NextFullSearch()) != nullptr) { | 
1341  | 0  |       part->RefinePartners(static_cast<PolyBlockType>(type), get_desperate,  | 
1342  | 0  |                            this);  | 
1343  |  |       // Iterator may have been messed up by a merge.  | 
1344  | 0  |       gsearch.RepositionIterator();  | 
1345  | 0  |     }  | 
1346  | 0  |   }  | 
1347  | 0  | }  | 
1348  |  |  | 
1349  |  | // ========================== PRIVATE CODE ========================  | 
1350  |  |  | 
1351  |  | // Finds and returns a list of candidate ColPartitions to merge with part.  | 
1352  |  | // The candidates must overlap search_box, and when merged must not  | 
1353  |  | // overlap any other partitions that are not overlapped by each individually.  | 
1354  |  | void ColPartitionGrid::FindMergeCandidates(const ColPartition *part,  | 
1355  |  |                                            const TBOX &search_box, bool debug,  | 
1356  | 0  |                                            ColPartition_CLIST *candidates) { | 
1357  | 0  |   int ok_overlap =  | 
1358  | 0  |       static_cast<int>(kTinyEnoughTextlineOverlapFraction * gridsize() + 0.5);  | 
1359  | 0  |   const TBOX &part_box = part->bounding_box();  | 
1360  |  |   // Now run the rect search.  | 
1361  | 0  |   ColPartitionGridSearch rsearch(this);  | 
1362  | 0  |   rsearch.SetUniqueMode(true);  | 
1363  | 0  |   rsearch.StartRectSearch(search_box);  | 
1364  | 0  |   ColPartition *candidate;  | 
1365  | 0  |   while ((candidate = rsearch.NextRectSearch()) != nullptr) { | 
1366  | 0  |     if (!OKMergeCandidate(part, candidate, debug)) { | 
1367  | 0  |       continue;  | 
1368  | 0  |     }  | 
1369  | 0  |     const TBOX &c_box = candidate->bounding_box();  | 
1370  |  |     // Candidate seems to be a potential merge with part. If one contains  | 
1371  |  |     // the other, then the merge is a no-brainer. Otherwise, search the  | 
1372  |  |     // combined box to see if anything else is inappropriately overlapped.  | 
1373  | 0  |     if (!part_box.contains(c_box) && !c_box.contains(part_box)) { | 
1374  |  |       // Search the combined rectangle to see if anything new is overlapped.  | 
1375  |  |       // This is a preliminary test designed to quickly weed-out poor  | 
1376  |  |       // merge candidates that would create a big list of overlapped objects  | 
1377  |  |       // for the squared-order overlap analysis. Eg. vertical and horizontal  | 
1378  |  |       // line-like objects that overlap real text when merged:  | 
1379  |  |       // || ==========================  | 
1380  |  |       // ||  | 
1381  |  |       // ||  r e a l  t e x t  | 
1382  |  |       // ||  | 
1383  |  |       // ||  | 
1384  | 0  |       TBOX merged_box(part_box);  | 
1385  | 0  |       merged_box += c_box;  | 
1386  | 0  |       ColPartitionGridSearch msearch(this);  | 
1387  | 0  |       msearch.SetUniqueMode(true);  | 
1388  | 0  |       msearch.StartRectSearch(merged_box);  | 
1389  | 0  |       ColPartition *neighbour;  | 
1390  | 0  |       while ((neighbour = msearch.NextRectSearch()) != nullptr) { | 
1391  | 0  |         if (neighbour == part || neighbour == candidate) { | 
1392  | 0  |           continue; // Ignore itself.  | 
1393  | 0  |         }  | 
1394  | 0  |         if (neighbour->OKMergeOverlap(*part, *candidate, ok_overlap, false)) { | 
1395  | 0  |           continue; // This kind of merge overlap is OK.  | 
1396  | 0  |         }  | 
1397  | 0  |         TBOX n_box = neighbour->bounding_box();  | 
1398  |  |         // The overlap is OK if:  | 
1399  |  |         // * the n_box already overlapped the part or the candidate OR  | 
1400  |  |         // * the n_box is a suitable merge with either part or candidate  | 
1401  | 0  |         if (!n_box.overlap(part_box) && !n_box.overlap(c_box) &&  | 
1402  | 0  |             !OKMergeCandidate(part, neighbour, false) &&  | 
1403  | 0  |             !OKMergeCandidate(candidate, neighbour, false)) { | 
1404  | 0  |           break;  | 
1405  | 0  |         }  | 
1406  | 0  |       }  | 
1407  | 0  |       if (neighbour != nullptr) { | 
1408  | 0  |         if (debug) { | 
1409  | 0  |           tprintf(  | 
1410  | 0  |               "Combined box overlaps another that is not OK despite"  | 
1411  | 0  |               " allowance of %d:",  | 
1412  | 0  |               ok_overlap);  | 
1413  | 0  |           neighbour->bounding_box().print();  | 
1414  | 0  |           tprintf("Reason:"); | 
1415  | 0  |           OKMergeCandidate(part, neighbour, true);  | 
1416  | 0  |           tprintf("...and:"); | 
1417  | 0  |           OKMergeCandidate(candidate, neighbour, true);  | 
1418  | 0  |           tprintf("Overlap:"); | 
1419  | 0  |           neighbour->OKMergeOverlap(*part, *candidate, ok_overlap, true);  | 
1420  | 0  |         }  | 
1421  | 0  |         continue;  | 
1422  | 0  |       }  | 
1423  | 0  |     }  | 
1424  | 0  |     if (debug) { | 
1425  | 0  |       tprintf("Adding candidate:"); | 
1426  | 0  |       candidate->bounding_box().print();  | 
1427  | 0  |     }  | 
1428  |  |     // Unique elements as they arrive.  | 
1429  | 0  |     candidates->add_sorted(SortByBoxLeft<ColPartition>, true, candidate);  | 
1430  | 0  |   }  | 
1431  | 0  | }  | 
1432  |  |  | 
1433  |  | // Smoothes the region type/flow type of the given part by looking at local  | 
1434  |  | // neighbours and the given image mask. Searches a padded rectangle with the  | 
1435  |  | // padding truncated on one size of the part's box in turn for each side,  | 
1436  |  | // using the result (if any) that has the least distance to all neighbours  | 
1437  |  | // that contribute to the decision. This biases in favor of rectangular  | 
1438  |  | // regions without completely enforcing them.  | 
1439  |  | // If a good decision cannot be reached, the part is left unchanged.  | 
1440  |  | // im_box and rerotation are used to map blob coordinates onto the  | 
1441  |  | // nontext_map, which is used to prevent the spread of text neighbourhoods  | 
1442  |  | // into images.  | 
1443  |  | // Returns true if the partition was changed.  | 
1444  |  | bool ColPartitionGrid::SmoothRegionType(Image nontext_map, const TBOX &im_box,  | 
1445  |  |                                         const FCOORD &rerotation, bool debug,  | 
1446  | 0  |                                         ColPartition *part) { | 
1447  | 0  |   const TBOX &part_box = part->bounding_box();  | 
1448  | 0  |   if (debug) { | 
1449  | 0  |     tprintf("Smooothing part at:"); | 
1450  | 0  |     part_box.print();  | 
1451  | 0  |   }  | 
1452  | 0  |   BlobRegionType best_type = BRT_UNKNOWN;  | 
1453  | 0  |   int best_dist = INT32_MAX;  | 
1454  | 0  |   int max_dist = std::min(part_box.width(), part_box.height());  | 
1455  | 0  |   max_dist = std::max(max_dist * kMaxNeighbourDistFactor, gridsize() * 2);  | 
1456  |  |   // Search with the pad truncated on each side of the box in turn.  | 
1457  | 0  |   bool any_image = false;  | 
1458  | 0  |   bool all_image = true;  | 
1459  | 0  |   for (int d = 0; d < BND_COUNT; ++d) { | 
1460  | 0  |     int dist;  | 
1461  | 0  |     auto dir = static_cast<BlobNeighbourDir>(d);  | 
1462  | 0  |     BlobRegionType type = SmoothInOneDirection(dir, nontext_map, im_box,  | 
1463  | 0  |                                                rerotation, debug, *part, &dist);  | 
1464  | 0  |     if (debug) { | 
1465  | 0  |       tprintf("Result in dir %d = %d at dist %d\n", dir, type, dist); | 
1466  | 0  |     }  | 
1467  | 0  |     if (type != BRT_UNKNOWN && dist < best_dist) { | 
1468  | 0  |       best_dist = dist;  | 
1469  | 0  |       best_type = type;  | 
1470  | 0  |     }  | 
1471  | 0  |     if (type == BRT_POLYIMAGE) { | 
1472  | 0  |       any_image = true;  | 
1473  | 0  |     } else { | 
1474  | 0  |       all_image = false;  | 
1475  | 0  |     }  | 
1476  | 0  |   }  | 
1477  | 0  |   if (best_dist > max_dist) { | 
1478  | 0  |     return false; // Too far away to set the type with it.  | 
1479  | 0  |   }  | 
1480  | 0  |   if (part->flow() == BTFT_STRONG_CHAIN && !all_image) { | 
1481  | 0  |     return false; // We are not modifying it.  | 
1482  | 0  |   }  | 
1483  | 0  |   BlobRegionType new_type = part->blob_type();  | 
1484  | 0  |   BlobTextFlowType new_flow = part->flow();  | 
1485  | 0  |   if (best_type == BRT_TEXT && !any_image) { | 
1486  | 0  |     new_flow = BTFT_STRONG_CHAIN;  | 
1487  | 0  |     new_type = BRT_TEXT;  | 
1488  | 0  |   } else if (best_type == BRT_VERT_TEXT && !any_image) { | 
1489  | 0  |     new_flow = BTFT_STRONG_CHAIN;  | 
1490  | 0  |     new_type = BRT_VERT_TEXT;  | 
1491  | 0  |   } else if (best_type == BRT_POLYIMAGE) { | 
1492  | 0  |     new_flow = BTFT_NONTEXT;  | 
1493  | 0  |     new_type = BRT_UNKNOWN;  | 
1494  | 0  |   }  | 
1495  | 0  |   if (new_type != part->blob_type() || new_flow != part->flow()) { | 
1496  | 0  |     part->set_flow(new_flow);  | 
1497  | 0  |     part->set_blob_type(new_type);  | 
1498  | 0  |     part->SetBlobTypes();  | 
1499  | 0  |     if (debug) { | 
1500  | 0  |       tprintf("Modified part:"); | 
1501  | 0  |       part->Print();  | 
1502  | 0  |     }  | 
1503  | 0  |     return true;  | 
1504  | 0  |   } else { | 
1505  | 0  |     return false;  | 
1506  | 0  |   }  | 
1507  | 0  | }  | 
1508  |  |  | 
1509  |  | // Sets up a search box based on the part_box, padded in all directions  | 
1510  |  | // except direction. Also setup dist_scaling to weight x,y distances according  | 
1511  |  | // to the given direction.  | 
1512  |  | static void ComputeSearchBoxAndScaling(BlobNeighbourDir direction,  | 
1513  |  |                                        const TBOX &part_box, int min_padding,  | 
1514  | 0  |                                        TBOX *search_box, ICOORD *dist_scaling) { | 
1515  | 0  |   *search_box = part_box;  | 
1516  |  |   // Generate a pad value based on the min dimension of part_box, but at least  | 
1517  |  |   // min_padding and then scaled by kMaxPadFactor.  | 
1518  | 0  |   int padding = std::min(part_box.height(), part_box.width());  | 
1519  | 0  |   padding = std::max(padding, min_padding);  | 
1520  | 0  |   padding *= kMaxPadFactor;  | 
1521  | 0  |   search_box->pad(padding, padding);  | 
1522  |  |   // Truncate the box in the appropriate direction and make the distance  | 
1523  |  |   // metric slightly biased in the truncated direction.  | 
1524  | 0  |   switch (direction) { | 
1525  | 0  |     case BND_LEFT:  | 
1526  | 0  |       search_box->set_left(part_box.left());  | 
1527  | 0  |       *dist_scaling = ICOORD(2, 1);  | 
1528  | 0  |       break;  | 
1529  | 0  |     case BND_BELOW:  | 
1530  | 0  |       search_box->set_bottom(part_box.bottom());  | 
1531  | 0  |       *dist_scaling = ICOORD(1, 2);  | 
1532  | 0  |       break;  | 
1533  | 0  |     case BND_RIGHT:  | 
1534  | 0  |       search_box->set_right(part_box.right());  | 
1535  | 0  |       *dist_scaling = ICOORD(2, 1);  | 
1536  | 0  |       break;  | 
1537  | 0  |     case BND_ABOVE:  | 
1538  | 0  |       search_box->set_top(part_box.top());  | 
1539  | 0  |       *dist_scaling = ICOORD(1, 2);  | 
1540  | 0  |       break;  | 
1541  | 0  |     default:  | 
1542  | 0  |       ASSERT_HOST(false);  | 
1543  | 0  |   }  | 
1544  | 0  | }  | 
1545  |  |  | 
1546  |  | // Local enum used by SmoothInOneDirection and AccumulatePartDistances  | 
1547  |  | // for the different types of partition neighbour.  | 
1548  |  | enum NeighbourPartitionType { | 
1549  |  |   NPT_HTEXT,      // Definite horizontal text.  | 
1550  |  |   NPT_VTEXT,      // Definite vertical text.  | 
1551  |  |   NPT_WEAK_HTEXT, // Weakly horizontal text. Counts as HTEXT for HTEXT, but  | 
1552  |  |                   // image for image and VTEXT.  | 
1553  |  |   NPT_WEAK_VTEXT, // Weakly vertical text. Counts as VTEXT for VTEXT, but  | 
1554  |  |                   // image for image and HTEXT.  | 
1555  |  |   NPT_IMAGE,      // Defininte non-text.  | 
1556  |  |   NPT_COUNT       // Number of array elements.  | 
1557  |  | };  | 
1558  |  |  | 
1559  |  | // Executes the search for SmoothRegionType in a single direction.  | 
1560  |  | // Creates a bounding box that is padded in all directions except direction,  | 
1561  |  | // and searches it for other partitions. Finds the nearest collection of  | 
1562  |  | // partitions that makes a decisive result (if any) and returns the type  | 
1563  |  | // and the distance of the collection. If there are any pixels in the  | 
1564  |  | // nontext_map, then the decision is biased towards image.  | 
1565  |  | BlobRegionType ColPartitionGrid::SmoothInOneDirection(  | 
1566  |  |     BlobNeighbourDir direction, Image nontext_map, const TBOX &im_box,  | 
1567  |  |     const FCOORD &rerotation, bool debug, const ColPartition &part,  | 
1568  | 0  |     int *best_distance) { | 
1569  |  |   // Set up a rectangle search bounded by the part.  | 
1570  | 0  |   const TBOX &part_box = part.bounding_box();  | 
1571  | 0  |   TBOX search_box;  | 
1572  | 0  |   ICOORD dist_scaling;  | 
1573  | 0  |   ComputeSearchBoxAndScaling(direction, part_box, gridsize(), &search_box,  | 
1574  | 0  |                              &dist_scaling);  | 
1575  | 0  |   bool image_region = ImageFind::CountPixelsInRotatedBox(  | 
1576  | 0  |                           search_box, im_box, rerotation, nontext_map) > 0;  | 
1577  | 0  |   std::vector<int> dists[NPT_COUNT];  | 
1578  | 0  |   AccumulatePartDistances(part, dist_scaling, search_box, nontext_map, im_box,  | 
1579  | 0  |                           rerotation, debug, dists);  | 
1580  |  |   // By iteratively including the next smallest distance across the vectors,  | 
1581  |  |   // (as in a merge sort) we can use the vector indices as counts of each type  | 
1582  |  |   // and find the nearest set of objects that give us a definite decision.  | 
1583  | 0  |   unsigned counts[NPT_COUNT];  | 
1584  | 0  |   memset(counts, 0, sizeof(counts));  | 
1585  |  |   // If there is image in the search box, tip the balance in image's favor.  | 
1586  | 0  |   int image_bias = image_region ? kSmoothDecisionMargin / 2 : 0;  | 
1587  | 0  |   BlobRegionType text_dir = part.blob_type();  | 
1588  | 0  |   BlobTextFlowType flow_type = part.flow();  | 
1589  | 0  |   int min_dist = 0;  | 
1590  | 0  |   do { | 
1591  |  |     // Find the minimum new entry across the vectors  | 
1592  | 0  |     min_dist = INT32_MAX;  | 
1593  | 0  |     for (int i = 0; i < NPT_COUNT; ++i) { | 
1594  | 0  |       if (counts[i] < dists[i].size() && dists[i][counts[i]] < min_dist) { | 
1595  | 0  |         min_dist = dists[i][counts[i]];  | 
1596  | 0  |       }  | 
1597  | 0  |     }  | 
1598  |  |     // Step all the indices/counts forward to include min_dist.  | 
1599  | 0  |     for (int i = 0; i < NPT_COUNT; ++i) { | 
1600  | 0  |       while (counts[i] < dists[i].size() && dists[i][counts[i]] <= min_dist) { | 
1601  | 0  |         ++counts[i];  | 
1602  | 0  |       }  | 
1603  | 0  |     }  | 
1604  | 0  |     *best_distance = min_dist;  | 
1605  | 0  |     if (debug) { | 
1606  | 0  |       tprintf("Totals: htext=%u+%u, vtext=%u+%u, image=%u+%u, at dist=%d\n", | 
1607  | 0  |               counts[NPT_HTEXT], counts[NPT_WEAK_HTEXT], counts[NPT_VTEXT],  | 
1608  | 0  |               counts[NPT_WEAK_VTEXT], counts[NPT_IMAGE], image_bias, min_dist);  | 
1609  | 0  |     }  | 
1610  |  |     // See if we have a decision yet.  | 
1611  | 0  |     auto image_count = counts[NPT_IMAGE];  | 
1612  | 0  |     int htext_score = counts[NPT_HTEXT] + counts[NPT_WEAK_HTEXT] -  | 
1613  | 0  |                       (image_count + counts[NPT_WEAK_VTEXT]);  | 
1614  | 0  |     int vtext_score = counts[NPT_VTEXT] + counts[NPT_WEAK_VTEXT] -  | 
1615  | 0  |                       (image_count + counts[NPT_WEAK_HTEXT]);  | 
1616  | 0  |     if (image_count > 0 && image_bias - htext_score >= kSmoothDecisionMargin &&  | 
1617  | 0  |         image_bias - vtext_score >= kSmoothDecisionMargin) { | 
1618  | 0  |       *best_distance = dists[NPT_IMAGE][0];  | 
1619  | 0  |       if (!dists[NPT_WEAK_VTEXT].empty() &&  | 
1620  | 0  |           *best_distance > dists[NPT_WEAK_VTEXT][0]) { | 
1621  | 0  |         *best_distance = dists[NPT_WEAK_VTEXT][0];  | 
1622  | 0  |       }  | 
1623  | 0  |       if (!dists[NPT_WEAK_HTEXT].empty() &&  | 
1624  | 0  |           *best_distance > dists[NPT_WEAK_HTEXT][0]) { | 
1625  | 0  |         *best_distance = dists[NPT_WEAK_HTEXT][0];  | 
1626  | 0  |       }  | 
1627  | 0  |       return BRT_POLYIMAGE;  | 
1628  | 0  |     }  | 
1629  | 0  |     if ((text_dir != BRT_VERT_TEXT || flow_type != BTFT_CHAIN) &&  | 
1630  | 0  |         counts[NPT_HTEXT] > 0 && htext_score >= kSmoothDecisionMargin) { | 
1631  | 0  |       *best_distance = dists[NPT_HTEXT][0];  | 
1632  | 0  |       return BRT_TEXT;  | 
1633  | 0  |     } else if ((text_dir != BRT_TEXT || flow_type != BTFT_CHAIN) &&  | 
1634  | 0  |                counts[NPT_VTEXT] > 0 && vtext_score >= kSmoothDecisionMargin) { | 
1635  | 0  |       *best_distance = dists[NPT_VTEXT][0];  | 
1636  | 0  |       return BRT_VERT_TEXT;  | 
1637  | 0  |     }  | 
1638  | 0  |   } while (min_dist < INT32_MAX);  | 
1639  | 0  |   return BRT_UNKNOWN;  | 
1640  | 0  | }  | 
1641  |  |  | 
1642  |  | // Counts the partitions in the given search_box by appending the gap  | 
1643  |  | // distance (scaled by dist_scaling) of the part from the base_part to the  | 
1644  |  | // vector of the appropriate type for the partition. Prior to return, the  | 
1645  |  | // vectors in the dists array are sorted in increasing order.  | 
1646  |  | // The nontext_map (+im_box, rerotation) is used to make text invisible if  | 
1647  |  | // there is non-text in between.  | 
1648  |  | // dists must be an array of vectors of size NPT_COUNT.  | 
1649  |  | void ColPartitionGrid::AccumulatePartDistances(  | 
1650  |  |     const ColPartition &base_part, const ICOORD &dist_scaling,  | 
1651  |  |     const TBOX &search_box, Image nontext_map, const TBOX &im_box,  | 
1652  | 0  |     const FCOORD &rerotation, bool debug, std::vector<int> *dists) { | 
1653  | 0  |   const TBOX &part_box = base_part.bounding_box();  | 
1654  | 0  |   ColPartitionGridSearch rsearch(this);  | 
1655  | 0  |   rsearch.SetUniqueMode(true);  | 
1656  | 0  |   rsearch.StartRectSearch(search_box);  | 
1657  | 0  |   ColPartition *neighbour;  | 
1658  |  |   // Search for compatible neighbours with a similar strokewidth, but not  | 
1659  |  |   // on the other side of a tab vector.  | 
1660  | 0  |   while ((neighbour = rsearch.NextRectSearch()) != nullptr) { | 
1661  | 0  |     if (neighbour->IsUnMergeableType() ||  | 
1662  | 0  |         !base_part.ConfirmNoTabViolation(*neighbour) ||  | 
1663  | 0  |         neighbour == &base_part) { | 
1664  | 0  |       continue;  | 
1665  | 0  |     }  | 
1666  | 0  |     TBOX nbox = neighbour->bounding_box();  | 
1667  | 0  |     BlobRegionType n_type = neighbour->blob_type();  | 
1668  | 0  |     if ((n_type == BRT_TEXT || n_type == BRT_VERT_TEXT) &&  | 
1669  | 0  |         !ImageFind::BlankImageInBetween(part_box, nbox, im_box, rerotation,  | 
1670  | 0  |                                         nontext_map)) { | 
1671  | 0  |       continue; // Text not visible the other side of image.  | 
1672  | 0  |     }  | 
1673  | 0  |     if (BLOBNBOX::IsLineType(n_type)) { | 
1674  | 0  |       continue; // Don't use horizontal lines as neighbours.  | 
1675  | 0  |     }  | 
1676  | 0  |     int x_gap = std::max(part_box.x_gap(nbox), 0);  | 
1677  | 0  |     int y_gap = std::max(part_box.y_gap(nbox), 0);  | 
1678  | 0  |     int n_dist = x_gap * dist_scaling.x() + y_gap * dist_scaling.y();  | 
1679  | 0  |     if (debug) { | 
1680  | 0  |       tprintf("Part has x-gap=%d, y=%d, dist=%d at:", x_gap, y_gap, n_dist); | 
1681  | 0  |       nbox.print();  | 
1682  | 0  |     }  | 
1683  |  |     // Truncate the number of boxes, so text doesn't get too much advantage.  | 
1684  | 0  |     int n_boxes = std::min(neighbour->boxes_count(), kSmoothDecisionMargin);  | 
1685  | 0  |     BlobTextFlowType n_flow = neighbour->flow();  | 
1686  | 0  |     std::vector<int> *count_vector = nullptr;  | 
1687  | 0  |     if (n_flow == BTFT_STRONG_CHAIN) { | 
1688  | 0  |       if (n_type == BRT_TEXT) { | 
1689  | 0  |         count_vector = &dists[NPT_HTEXT];  | 
1690  | 0  |       } else { | 
1691  | 0  |         count_vector = &dists[NPT_VTEXT];  | 
1692  | 0  |       }  | 
1693  | 0  |       if (debug) { | 
1694  | 0  |         tprintf("%s %d\n", n_type == BRT_TEXT ? "Htext" : "Vtext", n_boxes); | 
1695  | 0  |       }  | 
1696  | 0  |     } else if ((n_type == BRT_TEXT || n_type == BRT_VERT_TEXT) &&  | 
1697  | 0  |                (n_flow == BTFT_CHAIN || n_flow == BTFT_NEIGHBOURS)) { | 
1698  |  |       // Medium text counts as weak, and all else counts as image.  | 
1699  | 0  |       if (n_type == BRT_TEXT) { | 
1700  | 0  |         count_vector = &dists[NPT_WEAK_HTEXT];  | 
1701  | 0  |       } else { | 
1702  | 0  |         count_vector = &dists[NPT_WEAK_VTEXT];  | 
1703  | 0  |       }  | 
1704  | 0  |       if (debug) { | 
1705  | 0  |         tprintf("Weak %d\n", n_boxes); | 
1706  | 0  |       }  | 
1707  | 0  |     } else { | 
1708  | 0  |       count_vector = &dists[NPT_IMAGE];  | 
1709  | 0  |       if (debug) { | 
1710  | 0  |         tprintf("Image %d\n", n_boxes); | 
1711  | 0  |       }  | 
1712  | 0  |     }  | 
1713  | 0  |     if (count_vector != nullptr) { | 
1714  | 0  |       for (int i = 0; i < n_boxes; ++i) { | 
1715  | 0  |         count_vector->push_back(n_dist);  | 
1716  | 0  |       }  | 
1717  | 0  |     }  | 
1718  | 0  |     if (debug) { | 
1719  | 0  |       neighbour->Print();  | 
1720  | 0  |     }  | 
1721  | 0  |   }  | 
1722  | 0  |   for (int i = 0; i < NPT_COUNT; ++i) { | 
1723  | 0  |     std::sort(dists[i].begin(), dists[i].end());  | 
1724  | 0  |   }  | 
1725  | 0  | }  | 
1726  |  |  | 
1727  |  | // Improves the margins of the part ColPartition by searching for  | 
1728  |  | // neighbours that vertically overlap significantly.  | 
1729  |  | // columns may be nullptr, and indicates the assigned column structure this  | 
1730  |  | // is applicable to part.  | 
1731  |  | void ColPartitionGrid::FindPartitionMargins(ColPartitionSet *columns,  | 
1732  | 0  |                                             ColPartition *part) { | 
1733  |  |   // Set up a rectangle search x-bounded by the column and y by the part.  | 
1734  | 0  |   TBOX box = part->bounding_box();  | 
1735  | 0  |   int y = part->MidY();  | 
1736  |  |   // Initial left margin is based on the column, if there is one.  | 
1737  | 0  |   int left_margin = bleft().x();  | 
1738  | 0  |   int right_margin = tright().x();  | 
1739  | 0  |   if (columns != nullptr) { | 
1740  | 0  |     ColPartition *column = columns->ColumnContaining(box.left(), y);  | 
1741  | 0  |     if (column != nullptr) { | 
1742  | 0  |       left_margin = column->LeftAtY(y);  | 
1743  | 0  |     }  | 
1744  | 0  |     column = columns->ColumnContaining(box.right(), y);  | 
1745  | 0  |     if (column != nullptr) { | 
1746  | 0  |       right_margin = column->RightAtY(y);  | 
1747  | 0  |     }  | 
1748  | 0  |   }  | 
1749  | 0  |   left_margin -= kColumnWidthFactor;  | 
1750  | 0  |   right_margin += kColumnWidthFactor;  | 
1751  |  |   // Search for ColPartitions that reduce the margin.  | 
1752  | 0  |   left_margin = FindMargin(box.left() + box.height(), true, left_margin,  | 
1753  | 0  |                            box.bottom(), box.top(), part);  | 
1754  | 0  |   part->set_left_margin(left_margin);  | 
1755  |  |   // Search for ColPartitions that reduce the margin.  | 
1756  | 0  |   right_margin = FindMargin(box.right() - box.height(), false, right_margin,  | 
1757  | 0  |                             box.bottom(), box.top(), part);  | 
1758  | 0  |   part->set_right_margin(right_margin);  | 
1759  | 0  | }  | 
1760  |  |  | 
1761  |  | // Starting at x, and going in the specified direction, up to x_limit, finds  | 
1762  |  | // the margin for the given y range by searching sideways,  | 
1763  |  | // and ignoring not_this.  | 
1764  |  | int ColPartitionGrid::FindMargin(int x, bool right_to_left, int x_limit,  | 
1765  |  |                                  int y_bottom, int y_top,  | 
1766  | 0  |                                  const ColPartition *not_this) { | 
1767  | 0  |   int height = y_top - y_bottom;  | 
1768  |  |   // Iterate the ColPartitions in the grid.  | 
1769  | 0  |   ColPartitionGridSearch side_search(this);  | 
1770  | 0  |   side_search.SetUniqueMode(true);  | 
1771  | 0  |   side_search.StartSideSearch(x, y_bottom, y_top);  | 
1772  | 0  |   ColPartition *part;  | 
1773  | 0  |   while ((part = side_search.NextSideSearch(right_to_left)) != nullptr) { | 
1774  |  |     // Ignore itself.  | 
1775  | 0  |     if (part == not_this) { // || part->IsLineType()) | 
1776  | 0  |       continue;  | 
1777  | 0  |     }  | 
1778  |  |     // Must overlap by enough, based on the min of the heights, so  | 
1779  |  |     // large partitions can't smash through small ones.  | 
1780  | 0  |     TBOX box = part->bounding_box();  | 
1781  | 0  |     int min_overlap = std::min(height, static_cast<int>(box.height()));  | 
1782  | 0  |     min_overlap = static_cast<int>(min_overlap * kMarginOverlapFraction + 0.5);  | 
1783  | 0  |     int y_overlap = std::min(y_top, static_cast<int>(box.top())) -  | 
1784  | 0  |                     std::max(y_bottom, static_cast<int>(box.bottom()));  | 
1785  | 0  |     if (y_overlap < min_overlap) { | 
1786  | 0  |       continue;  | 
1787  | 0  |     }  | 
1788  |  |     // Must be going the right way.  | 
1789  | 0  |     int x_edge = right_to_left ? box.right() : box.left();  | 
1790  | 0  |     if ((x_edge < x) != right_to_left) { | 
1791  | 0  |       continue;  | 
1792  | 0  |     }  | 
1793  |  |     // If we have gone past x_limit, then x_limit will do.  | 
1794  | 0  |     if ((x_edge < x_limit) == right_to_left) { | 
1795  | 0  |       break;  | 
1796  | 0  |     }  | 
1797  |  |     // It reduces x limit, so save the new one.  | 
1798  | 0  |     x_limit = x_edge;  | 
1799  | 0  |   }  | 
1800  | 0  |   return x_limit;  | 
1801  | 0  | }  | 
1802  |  |  | 
1803  |  | } // namespace tesseract.  |