/src/tesseract/src/textord/tordmain.cpp
Line | Count | Source |
1 | | /********************************************************************** |
2 | | * File: tordmain.cpp (Formerly textordp.c) |
3 | | * Description: C++ top level textord code. |
4 | | * Author: Ray Smith |
5 | | * |
6 | | * (C) Copyright 1992, Hewlett-Packard Ltd. |
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 | | #define _USE_MATH_DEFINES // for M_PI |
20 | | |
21 | | #ifdef HAVE_CONFIG_H |
22 | | # include "config_auto.h" |
23 | | #endif |
24 | | |
25 | | #include "tordmain.h" |
26 | | |
27 | | #include "blobbox.h" // for BLOBNBOX_IT, BLOBNBOX, TO_BLOCK, TO_B... |
28 | | #include "ccstruct.h" // for CCStruct, CCStruct::kXHeightFraction |
29 | | #include "clst.h" // for CLISTIZE |
30 | | #include "coutln.h" // for C_OUTLINE_IT, C_OUTLINE_LIST, C_OUTLINE |
31 | | #include "drawtord.h" // for plot_box_list, to_win, create_to_win |
32 | | #include "edgblob.h" // for extract_edges |
33 | | #include "errcode.h" // for ASSERT_HOST, ... |
34 | | #include "image.h" // for Image, Leptonica (pixDestroy, pixGetHeight, ...) |
35 | | #include "makerow.h" // for textord_test_x, textord_test_y, texto... |
36 | | #include "ocrblock.h" // for BLOCK_IT, BLOCK, BLOCK_LIST (ptr only) |
37 | | #include "ocrrow.h" // for ROW, ROW_IT, ROW_LIST, tweak_row_base... |
38 | | #include "params.h" // for DoubleParam, BoolParam, IntParam |
39 | | #include "pdblock.h" // for PDBLK |
40 | | #include "points.h" // for FCOORD, ICOORD |
41 | | #include "polyblk.h" // for POLY_BLOCK |
42 | | #include "quadratc.h" // for QUAD_COEFFS |
43 | | #include "quspline.h" // for QSPLINE, tweak_row_baseline |
44 | | #include "rect.h" // for TBOX |
45 | | #include "scrollview.h" // for ScrollView, ScrollView::WHITE |
46 | | #include "statistc.h" // for STATS |
47 | | #include "stepblob.h" // for C_BLOB_IT, C_BLOB, C_BLOB_LIST |
48 | | #include "textord.h" // for Textord, WordWithBox, WordGrid, WordS... |
49 | | #include "tprintf.h" // for tprintf |
50 | | #include "werd.h" // for WERD_IT, WERD, WERD_LIST, W_DONT_CHOP |
51 | | |
52 | | #include <cfloat> // for FLT_MAX |
53 | | #include <cmath> // for ceil, floor, M_PI |
54 | | #include <cstdint> // for INT16_MAX, uint32_t, int32_t, int16_t |
55 | | #include <memory> |
56 | | |
57 | | namespace tesseract { |
58 | | |
59 | 15.4k | #define MAX_NEAREST_DIST 600 // for block skew stats |
60 | | |
61 | | /********************************************************************** |
62 | | * SetBlobStrokeWidth |
63 | | * |
64 | | * Set the horizontal and vertical stroke widths in the blob. |
65 | | **********************************************************************/ |
66 | 2.65M | void SetBlobStrokeWidth(Image pix, BLOBNBOX *blob) { |
67 | | // Cut the blob rectangle into a Pix. |
68 | 2.65M | int pix_height = pixGetHeight(pix); |
69 | 2.65M | const TBOX &box = blob->bounding_box(); |
70 | 2.65M | int width = box.width(); |
71 | 2.65M | int height = box.height(); |
72 | 2.65M | Box *blob_pix_box = boxCreate(box.left(), pix_height - box.top(), width, height); |
73 | 2.65M | Image pix_blob = pixClipRectangle(pix, blob_pix_box, nullptr); |
74 | 2.65M | boxDestroy(&blob_pix_box); |
75 | 2.65M | Image dist_pix = pixDistanceFunction(pix_blob, 4, 8, L_BOUNDARY_BG); |
76 | 2.65M | pix_blob.destroy(); |
77 | | // Compute the stroke widths. |
78 | 2.65M | uint32_t *data = pixGetData(dist_pix); |
79 | 2.65M | int wpl = pixGetWpl(dist_pix); |
80 | | // Horizontal width of stroke. |
81 | 2.65M | STATS h_stats(0, width); |
82 | 19.4M | for (int y = 0; y < height; ++y) { |
83 | 16.7M | uint32_t *pixels = data + y * wpl; |
84 | 16.7M | int prev_pixel = 0; |
85 | 16.7M | int pixel = Image::getDataByte(pixels, 0); |
86 | 131M | for (int x = 1; x < width; ++x) { |
87 | 115M | int next_pixel = Image::getDataByte(pixels, x); |
88 | | // We are looking for a pixel that is equal to its vertical neighbours, |
89 | | // yet greater than its left neighbour. |
90 | 115M | if (prev_pixel < pixel && (y == 0 || pixel == Image::getDataByte(pixels - wpl, x - 1)) && |
91 | 17.6M | (y == height - 1 || pixel == Image::getDataByte(pixels + wpl, x - 1))) { |
92 | 11.7M | if (pixel > next_pixel) { |
93 | | // Single local max, so an odd width. |
94 | 6.41M | h_stats.add(pixel * 2 - 1, 1); |
95 | 6.41M | } else if (pixel == next_pixel && x + 1 < width && pixel > Image::getDataByte(pixels, x + 1)) { |
96 | | // Double local max, so an even width. |
97 | 2.72M | h_stats.add(pixel * 2, 1); |
98 | 2.72M | } |
99 | 11.7M | } |
100 | 115M | prev_pixel = pixel; |
101 | 115M | pixel = next_pixel; |
102 | 115M | } |
103 | 16.7M | } |
104 | | // Vertical width of stroke. |
105 | 2.65M | STATS v_stats(0, height); |
106 | 14.2M | for (int x = 0; x < width; ++x) { |
107 | 11.5M | int prev_pixel = 0; |
108 | 11.5M | int pixel = Image::getDataByte(data, x); |
109 | 131M | for (int y = 1; y < height; ++y) { |
110 | 120M | uint32_t *pixels = data + y * wpl; |
111 | 120M | int next_pixel = Image::getDataByte(pixels, x); |
112 | | // We are looking for a pixel that is equal to its horizontal neighbours, |
113 | | // yet greater than its upper neighbour. |
114 | 120M | if (prev_pixel < pixel && (x == 0 || pixel == Image::getDataByte(pixels - wpl, x - 1)) && |
115 | 16.9M | (x == width - 1 || pixel == Image::getDataByte(pixels - wpl, x + 1))) { |
116 | 11.4M | if (pixel > next_pixel) { |
117 | | // Single local max, so an odd width. |
118 | 5.42M | v_stats.add(pixel * 2 - 1, 1); |
119 | 6.05M | } else if (pixel == next_pixel && y + 1 < height && |
120 | 3.78M | pixel > Image::getDataByte(pixels + wpl, x)) { |
121 | | // Double local max, so an even width. |
122 | 2.20M | v_stats.add(pixel * 2, 1); |
123 | 2.20M | } |
124 | 11.4M | } |
125 | 120M | prev_pixel = pixel; |
126 | 120M | pixel = next_pixel; |
127 | 120M | } |
128 | 11.5M | } |
129 | 2.65M | dist_pix.destroy(); |
130 | | // Store the horizontal and vertical width in the blob, keeping both |
131 | | // widths if there is enough information, otherwise only the one with |
132 | | // the most samples. |
133 | | // If there are insufficient samples, store zero, rather than using |
134 | | // 2*area/perimeter, as the numbers that gives do not match the numbers |
135 | | // from the distance method. |
136 | 2.65M | if (h_stats.get_total() >= (width + height) / 4) { |
137 | 648k | blob->set_horz_stroke_width(h_stats.ile(0.5f)); |
138 | 648k | if (v_stats.get_total() >= (width + height) / 4) { |
139 | 168k | blob->set_vert_stroke_width(v_stats.ile(0.5f)); |
140 | 479k | } else { |
141 | 479k | blob->set_vert_stroke_width(0.0f); |
142 | 479k | } |
143 | 2.01M | } else { |
144 | 2.01M | if (v_stats.get_total() >= (width + height) / 4 || v_stats.get_total() > h_stats.get_total()) { |
145 | 305k | blob->set_horz_stroke_width(0.0f); |
146 | 305k | blob->set_vert_stroke_width(v_stats.ile(0.5f)); |
147 | 1.70M | } else { |
148 | 1.70M | blob->set_horz_stroke_width(h_stats.get_total() > 2 ? h_stats.ile(0.5f) : 0.0f); |
149 | 1.70M | blob->set_vert_stroke_width(0.0f); |
150 | 1.70M | } |
151 | 2.01M | } |
152 | 2.65M | } |
153 | | |
154 | | /********************************************************************** |
155 | | * assign_blobs_to_blocks2 |
156 | | * |
157 | | * Make a list of TO_BLOCKs for portrait and landscape orientation. |
158 | | **********************************************************************/ |
159 | | |
160 | | void assign_blobs_to_blocks2(Image pix, |
161 | | BLOCK_LIST *blocks, // blocks to process |
162 | 15.4k | TO_BLOCK_LIST *port_blocks) { // output list |
163 | 15.4k | BLOCK_IT block_it = blocks; |
164 | 15.4k | C_BLOB_IT blob_it; // iterator |
165 | 15.4k | BLOBNBOX_IT port_box_it; // iterator |
166 | | // destination iterator |
167 | 15.4k | TO_BLOCK_IT port_block_it = port_blocks; |
168 | | |
169 | 30.8k | for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) { |
170 | 15.4k | auto block = block_it.data(); |
171 | 15.4k | auto port_block = new TO_BLOCK(block); |
172 | | |
173 | | // Convert the good outlines to block->blob_list |
174 | 15.4k | port_box_it.set_to_list(&port_block->blobs); |
175 | 15.4k | blob_it.set_to_list(block->blob_list()); |
176 | 2.67M | for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { |
177 | 2.65M | auto blob = blob_it.extract(); |
178 | 2.65M | auto newblob = new BLOBNBOX(blob); // Convert blob to BLOBNBOX. |
179 | 2.65M | newblob->set_owns_cblob(true); |
180 | 2.65M | SetBlobStrokeWidth(pix, newblob); |
181 | 2.65M | port_box_it.add_after_then_move(newblob); |
182 | 2.65M | } |
183 | | |
184 | | // Put the rejected outlines in block->noise_blobs, which allows them to |
185 | | // be reconsidered and sorted back into rows and recover outlines mistakenly |
186 | | // rejected. |
187 | 15.4k | port_box_it.set_to_list(&port_block->noise_blobs); |
188 | 15.4k | blob_it.set_to_list(block->reject_blobs()); |
189 | 18.6k | for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { |
190 | 3.29k | auto blob = blob_it.extract(); |
191 | 3.29k | auto newblob = new BLOBNBOX(blob); // Convert blob to BLOBNBOX. |
192 | 3.29k | newblob->set_owns_cblob(true); |
193 | 3.29k | SetBlobStrokeWidth(pix, newblob); |
194 | 3.29k | port_box_it.add_after_then_move(newblob); |
195 | 3.29k | } |
196 | | |
197 | 15.4k | port_block_it.add_after_then_move(port_block); |
198 | 15.4k | } |
199 | 15.4k | } |
200 | | |
201 | | /********************************************************************** |
202 | | * find_components |
203 | | * |
204 | | * Find the C_OUTLINEs of the connected components in each block, put them |
205 | | * in C_BLOBs, and filter them by size, putting the different size |
206 | | * grades on different lists in the matching TO_BLOCK in to_blocks. |
207 | | **********************************************************************/ |
208 | | |
209 | 15.4k | void Textord::find_components(Image pix, BLOCK_LIST *blocks, TO_BLOCK_LIST *to_blocks) { |
210 | 15.4k | int width = pixGetWidth(pix); |
211 | 15.4k | int height = pixGetHeight(pix); |
212 | 15.4k | if (width > INT16_MAX || height > INT16_MAX) { |
213 | 0 | tprintf("Input image too large! (%d, %d)\n", width, height); |
214 | 0 | return; // Can't handle it. |
215 | 0 | } |
216 | | |
217 | 15.4k | BLOCK_IT block_it(blocks); // iterator |
218 | 30.8k | for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) { |
219 | 15.4k | BLOCK *block = block_it.data(); |
220 | 15.4k | if (block->pdblk.poly_block() == nullptr || block->pdblk.poly_block()->IsText()) { |
221 | 15.4k | extract_edges(pix, block); |
222 | 15.4k | } |
223 | 15.4k | } |
224 | | |
225 | 15.4k | assign_blobs_to_blocks2(pix, blocks, to_blocks); |
226 | 15.4k | ICOORD page_tr(width, height); |
227 | 15.4k | filter_blobs(page_tr, to_blocks, !textord_test_landscape); |
228 | 15.4k | } |
229 | | |
230 | | /********************************************************************** |
231 | | * filter_blobs |
232 | | * |
233 | | * Sort the blobs into sizes in all the blocks for later work. |
234 | | **********************************************************************/ |
235 | | |
236 | | void Textord::filter_blobs(ICOORD page_tr, // top right |
237 | | TO_BLOCK_LIST *blocks, // output list |
238 | 15.4k | bool testing_on) { // for plotting |
239 | 15.4k | TO_BLOCK_IT block_it = blocks; // destination iterator |
240 | 15.4k | TO_BLOCK *block; // created block |
241 | | |
242 | | #ifndef GRAPHICS_DISABLED |
243 | | if (to_win != nullptr) { |
244 | | to_win->Clear(); |
245 | | } |
246 | | #endif // !GRAPHICS_DISABLED |
247 | | |
248 | 30.8k | for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) { |
249 | 15.4k | block = block_it.data(); |
250 | 15.4k | block->line_size = filter_noise_blobs(&block->blobs, &block->noise_blobs, &block->small_blobs, |
251 | 15.4k | &block->large_blobs); |
252 | 15.4k | if (block->line_size == 0) { |
253 | 425 | block->line_size = 1; |
254 | 425 | } |
255 | 15.4k | block->line_spacing = |
256 | 15.4k | block->line_size * |
257 | 15.4k | (tesseract::CCStruct::kDescenderFraction + tesseract::CCStruct::kXHeightFraction + |
258 | 15.4k | 2 * tesseract::CCStruct::kAscenderFraction) / |
259 | 15.4k | tesseract::CCStruct::kXHeightFraction; |
260 | 15.4k | block->line_size *= textord_min_linesize; |
261 | 15.4k | block->max_blob_size = block->line_size * textord_excess_blobsize; |
262 | | |
263 | | #ifndef GRAPHICS_DISABLED |
264 | | if (textord_show_blobs && testing_on) { |
265 | | if (to_win == nullptr) { |
266 | | create_to_win(page_tr); |
267 | | } |
268 | | block->plot_graded_blobs(to_win); |
269 | | } |
270 | | if (textord_show_boxes && testing_on) { |
271 | | if (to_win == nullptr) { |
272 | | create_to_win(page_tr); |
273 | | } |
274 | | plot_box_list(to_win, &block->noise_blobs, ScrollView::WHITE); |
275 | | plot_box_list(to_win, &block->small_blobs, ScrollView::WHITE); |
276 | | plot_box_list(to_win, &block->large_blobs, ScrollView::WHITE); |
277 | | plot_box_list(to_win, &block->blobs, ScrollView::WHITE); |
278 | | } |
279 | | #endif // !GRAPHICS_DISABLED |
280 | 15.4k | } |
281 | 15.4k | } |
282 | | |
283 | | /********************************************************************** |
284 | | * filter_noise_blobs |
285 | | * |
286 | | * Move small blobs to a separate list. |
287 | | **********************************************************************/ |
288 | | |
289 | | float Textord::filter_noise_blobs(BLOBNBOX_LIST *src_list, // original list |
290 | | BLOBNBOX_LIST *noise_list, // noise list |
291 | | BLOBNBOX_LIST *small_list, // small blobs |
292 | 15.4k | BLOBNBOX_LIST *large_list) { // large blobs |
293 | 15.4k | int16_t height; // height of blob |
294 | 15.4k | int16_t width; // of blob |
295 | 15.4k | BLOBNBOX *blob; // current blob |
296 | 15.4k | float initial_x; // first guess |
297 | 15.4k | BLOBNBOX_IT src_it = src_list; // iterators |
298 | 15.4k | BLOBNBOX_IT noise_it = noise_list; |
299 | 15.4k | BLOBNBOX_IT small_it = small_list; |
300 | 15.4k | BLOBNBOX_IT large_it = large_list; |
301 | 15.4k | STATS size_stats(0, MAX_NEAREST_DIST - 1); |
302 | | // blob heights |
303 | 15.4k | float min_y; // size limits |
304 | 15.4k | float max_y; |
305 | 15.4k | float max_x; |
306 | 15.4k | float max_height; // of good blobs |
307 | | |
308 | 2.67M | for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) { |
309 | 2.65M | blob = src_it.data(); |
310 | 2.65M | if (blob->bounding_box().height() < textord_max_noise_size) { |
311 | 1.57M | noise_it.add_after_then_move(src_it.extract()); |
312 | 1.57M | } else if (blob->enclosed_area() >= blob->bounding_box().height() * |
313 | 1.08M | blob->bounding_box().width() * |
314 | 1.08M | textord_noise_area_ratio) { |
315 | 520k | small_it.add_after_then_move(src_it.extract()); |
316 | 520k | } |
317 | 2.65M | } |
318 | 576k | for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) { |
319 | 561k | size_stats.add(src_it.data()->bounding_box().height(), 1); |
320 | 561k | } |
321 | 15.4k | initial_x = size_stats.ile(textord_initialx_ile); |
322 | 15.4k | max_y = ceil(initial_x * |
323 | 15.4k | (tesseract::CCStruct::kDescenderFraction + tesseract::CCStruct::kXHeightFraction + |
324 | 15.4k | 2 * tesseract::CCStruct::kAscenderFraction) / |
325 | 15.4k | tesseract::CCStruct::kXHeightFraction); |
326 | 15.4k | min_y = std::floor(initial_x / 2); |
327 | 15.4k | max_x = ceil(initial_x * textord_width_limit); |
328 | 15.4k | small_it.move_to_first(); |
329 | 536k | for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) { |
330 | 520k | height = small_it.data()->bounding_box().height(); |
331 | 520k | if (height > max_y) { |
332 | 3.16k | large_it.add_after_then_move(small_it.extract()); |
333 | 517k | } else if (height >= min_y) { |
334 | 496k | src_it.add_after_then_move(small_it.extract()); |
335 | 496k | } |
336 | 520k | } |
337 | 15.4k | size_stats.clear(); |
338 | 1.07M | for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) { |
339 | 1.05M | height = src_it.data()->bounding_box().height(); |
340 | 1.05M | width = src_it.data()->bounding_box().width(); |
341 | 1.05M | if (height < min_y) { |
342 | 21.3k | small_it.add_after_then_move(src_it.extract()); |
343 | 1.03M | } else if (height > max_y || width > max_x) { |
344 | 14.3k | large_it.add_after_then_move(src_it.extract()); |
345 | 1.02M | } else { |
346 | 1.02M | size_stats.add(height, 1); |
347 | 1.02M | } |
348 | 1.05M | } |
349 | 15.4k | max_height = size_stats.ile(textord_initialasc_ile); |
350 | | // tprintf("max_y=%g, min_y=%g, initial_x=%g, max_height=%g,", |
351 | | // max_y,min_y,initial_x,max_height); |
352 | 15.4k | max_height *= tesseract::CCStruct::kXHeightCapRatio; |
353 | 15.4k | if (max_height > initial_x) { |
354 | 2.17k | initial_x = max_height; |
355 | 2.17k | } |
356 | | // tprintf(" ret=%g\n",initial_x); |
357 | 15.4k | return initial_x; |
358 | 15.4k | } |
359 | | |
360 | | // Fixes the block so it obeys all the rules: |
361 | | // Must have at least one ROW. |
362 | | // Must have at least one WERD. |
363 | | // WERDs contain a fake blob. |
364 | 0 | void Textord::cleanup_nontext_block(BLOCK *block) { |
365 | | // Non-text blocks must contain at least one row. |
366 | 0 | ROW_IT row_it(block->row_list()); |
367 | 0 | if (row_it.empty()) { |
368 | 0 | const TBOX &box = block->pdblk.bounding_box(); |
369 | 0 | float height = box.height(); |
370 | 0 | int32_t xstarts[2] = {box.left(), box.right()}; |
371 | 0 | double coeffs[3] = {0.0, 0.0, static_cast<double>(box.bottom())}; |
372 | 0 | ROW *row = new ROW(1, xstarts, coeffs, height / 2.0f, height / 4.0f, height / 4.0f, 0, 1); |
373 | 0 | row_it.add_after_then_move(row); |
374 | 0 | } |
375 | | // Each row must contain at least one word. |
376 | 0 | for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { |
377 | 0 | ROW *row = row_it.data(); |
378 | 0 | WERD_IT w_it(row->word_list()); |
379 | 0 | if (w_it.empty()) { |
380 | | // Make a fake blob to put in the word. |
381 | 0 | TBOX box = block->row_list()->singleton() ? block->pdblk.bounding_box() : row->bounding_box(); |
382 | 0 | C_BLOB *blob = C_BLOB::FakeBlob(box); |
383 | 0 | C_BLOB_LIST blobs; |
384 | 0 | C_BLOB_IT blob_it(&blobs); |
385 | 0 | blob_it.add_after_then_move(blob); |
386 | 0 | WERD *word = new WERD(&blobs, 0, nullptr); |
387 | 0 | w_it.add_after_then_move(word); |
388 | 0 | } |
389 | | // Each word must contain a fake blob. |
390 | 0 | for (w_it.mark_cycle_pt(); !w_it.cycled_list(); w_it.forward()) { |
391 | 0 | WERD *word = w_it.data(); |
392 | | // Just assert that this is true, as it would be useful to find |
393 | | // out why it isn't. |
394 | 0 | ASSERT_HOST(!word->cblob_list()->empty()); |
395 | 0 | } |
396 | 0 | row->recalc_bounding_box(); |
397 | 0 | } |
398 | 0 | } |
399 | | |
400 | | /********************************************************************** |
401 | | * cleanup_blocks |
402 | | * |
403 | | * Delete empty blocks, rows from the page. |
404 | | **********************************************************************/ |
405 | | |
406 | 15.4k | void Textord::cleanup_blocks(bool clean_noise, BLOCK_LIST *blocks) { |
407 | 15.4k | BLOCK_IT block_it = blocks; // iterator |
408 | 15.4k | ROW_IT row_it; // row iterator |
409 | | |
410 | 15.4k | int num_rows = 0; |
411 | 15.4k | int num_rows_all = 0; |
412 | 15.4k | int num_blocks = 0; |
413 | 15.4k | int num_blocks_all = 0; |
414 | 30.8k | for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) { |
415 | 15.4k | BLOCK *block = block_it.data(); |
416 | 15.4k | if (block->pdblk.poly_block() != nullptr && !block->pdblk.poly_block()->IsText()) { |
417 | 0 | cleanup_nontext_block(block); |
418 | 0 | continue; |
419 | 0 | } |
420 | 15.4k | num_rows = 0; |
421 | 15.4k | num_rows_all = 0; |
422 | 15.4k | if (clean_noise) { |
423 | 15.4k | row_it.set_to_list(block->row_list()); |
424 | 188k | for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { |
425 | 173k | ROW *row = row_it.data(); |
426 | 173k | ++num_rows_all; |
427 | 173k | clean_small_noise_from_words(row); |
428 | 173k | if ((textord_noise_rejrows && !row->word_list()->empty() && clean_noise_from_row(row)) || |
429 | 141k | row->word_list()->empty()) { |
430 | 36.2k | delete row_it.extract(); // lose empty row. |
431 | 136k | } else { |
432 | 136k | if (textord_noise_rejwords) { |
433 | 136k | clean_noise_from_words(row_it.data()); |
434 | 136k | } |
435 | 136k | if (textord_blshift_maxshift >= 0) { |
436 | 136k | tweak_row_baseline(row, textord_blshift_maxshift, textord_blshift_xfraction); |
437 | 136k | } |
438 | 136k | ++num_rows; |
439 | 136k | } |
440 | 173k | } |
441 | 15.4k | } |
442 | 15.4k | if (block->row_list()->empty()) { |
443 | 1.60k | delete block_it.extract(); // Lose empty text blocks. |
444 | 13.8k | } else { |
445 | 13.8k | ++num_blocks; |
446 | 13.8k | } |
447 | 15.4k | ++num_blocks_all; |
448 | 15.4k | if (textord_noise_debug) { |
449 | 0 | tprintf("cleanup_blocks: # rows = %d / %d\n", num_rows, num_rows_all); |
450 | 0 | } |
451 | 15.4k | } |
452 | 15.4k | if (textord_noise_debug) { |
453 | 0 | tprintf("cleanup_blocks: # blocks = %d / %d\n", num_blocks, num_blocks_all); |
454 | 0 | } |
455 | 15.4k | } |
456 | | |
457 | | /********************************************************************** |
458 | | * clean_noise_from_row |
459 | | * |
460 | | * Move blobs of words from rows of garbage into the reject blobs list. |
461 | | **********************************************************************/ |
462 | | |
463 | | bool Textord::clean_noise_from_row( // remove empties |
464 | | ROW *row // row to clean |
465 | 167k | ) { |
466 | 167k | bool testing_on; |
467 | 167k | TBOX blob_box; // bounding box |
468 | 167k | C_BLOB *blob; // current blob |
469 | 167k | C_OUTLINE *outline; // current outline |
470 | 167k | WERD *word; // current word |
471 | 167k | int32_t blob_size; // biggest size |
472 | 167k | int32_t trans_count = 0; // no of transitions |
473 | 167k | int32_t trans_threshold; // noise tolerance |
474 | 167k | int32_t dot_count; // small objects |
475 | 167k | int32_t norm_count; // normal objects |
476 | 167k | int32_t super_norm_count; // real char-like |
477 | | // words of row |
478 | 167k | WERD_IT word_it = row->word_list(); |
479 | 167k | C_BLOB_IT blob_it; // blob iterator |
480 | 167k | C_OUTLINE_IT out_it; // outline iterator |
481 | | |
482 | 167k | testing_on = textord_test_y > row->base_line(textord_test_x) && textord_show_blobs && |
483 | 0 | textord_test_y < row->base_line(textord_test_x) + row->x_height(); |
484 | 167k | dot_count = 0; |
485 | 167k | norm_count = 0; |
486 | 167k | super_norm_count = 0; |
487 | 438k | for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) { |
488 | 270k | word = word_it.data(); // current word |
489 | | // blobs in word |
490 | 270k | blob_it.set_to_list(word->cblob_list()); |
491 | 1.31M | for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { |
492 | 1.04M | blob = blob_it.data(); |
493 | 1.04M | if (!word->flag(W_DONT_CHOP)) { |
494 | | // get outlines |
495 | 1.01M | out_it.set_to_list(blob->out_list()); |
496 | 2.88M | for (out_it.mark_cycle_pt(); !out_it.cycled_list(); out_it.forward()) { |
497 | 1.86M | outline = out_it.data(); |
498 | 1.86M | blob_box = outline->bounding_box(); |
499 | 1.86M | blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height(); |
500 | 1.86M | if (blob_size < textord_noise_sizelimit * row->x_height()) { |
501 | 1.14M | dot_count++; // count small outlines |
502 | 1.14M | } |
503 | 1.86M | if (!outline->child()->empty() && |
504 | 62.0k | blob_box.height() < (1 + textord_noise_syfract) * row->x_height() && |
505 | 53.3k | blob_box.height() > (1 - textord_noise_syfract) * row->x_height() && |
506 | 15.3k | blob_box.width() < (1 + textord_noise_sxfract) * row->x_height() && |
507 | 12.5k | blob_box.width() > (1 - textord_noise_sxfract) * row->x_height()) { |
508 | 4.06k | super_norm_count++; // count small outlines |
509 | 4.06k | } |
510 | 1.86M | } |
511 | 1.01M | } else { |
512 | 35.3k | super_norm_count++; |
513 | 35.3k | } |
514 | 1.04M | blob_box = blob->bounding_box(); |
515 | 1.04M | blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height(); |
516 | 1.04M | if (blob_size >= textord_noise_sizelimit * row->x_height() && |
517 | 746k | blob_size < row->x_height() * 2) { |
518 | 705k | trans_threshold = blob_size / textord_noise_sizefraction; |
519 | 705k | trans_count = blob->count_transitions(trans_threshold); |
520 | 705k | if (trans_count < textord_noise_translimit) { |
521 | 635k | norm_count++; |
522 | 635k | } |
523 | 705k | } else if (blob_box.height() > row->x_height() * 2 && |
524 | 19.8k | (!word_it.at_first() || !blob_it.at_first())) { |
525 | 13.3k | dot_count += 2; |
526 | 13.3k | } |
527 | 1.04M | if (testing_on) { |
528 | 0 | tprintf("Blob at (%d,%d) -> (%d,%d), ols=%d, tc=%d, bldiff=%g\n", blob_box.left(), |
529 | 0 | blob_box.bottom(), blob_box.right(), blob_box.top(), blob->out_list()->length(), |
530 | 0 | trans_count, blob_box.bottom() - row->base_line(blob_box.left())); |
531 | 0 | } |
532 | 1.04M | } |
533 | 270k | } |
534 | | // TODO: check whether `&& super_norm_count < textord_noise_sncount`should always be added here. |
535 | 167k | bool rejected = dot_count > norm_count * textord_noise_normratio && |
536 | 42.7k | dot_count > 2; |
537 | 167k | if (textord_noise_debug) { |
538 | 0 | tprintf("Row ending at (%d,%g):", blob_box.right(), row->base_line(blob_box.right())); |
539 | 0 | tprintf(" R=%g, dc=%d, nc=%d, %s\n", |
540 | 0 | norm_count > 0 ? static_cast<float>(dot_count) / norm_count : 9999, dot_count, |
541 | 0 | norm_count, |
542 | 0 | rejected? "REJECTED": "ACCEPTED"); |
543 | 0 | } |
544 | 167k | return super_norm_count < textord_noise_sncount && rejected; |
545 | 167k | } |
546 | | |
547 | | /********************************************************************** |
548 | | * clean_noise_from_words |
549 | | * |
550 | | * Move blobs of words from rows of garbage into the reject blobs list. |
551 | | **********************************************************************/ |
552 | | |
553 | | void Textord::clean_noise_from_words( // remove empties |
554 | | ROW *row // row to clean |
555 | 136k | ) { |
556 | 136k | TBOX blob_box; // bounding box |
557 | 136k | C_BLOB *blob; // current blob |
558 | 136k | C_OUTLINE *outline; // current outline |
559 | 136k | WERD *word; // current word |
560 | 136k | int32_t blob_size; // biggest size |
561 | 136k | int32_t trans_count; // no of transitions |
562 | 136k | int32_t trans_threshold; // noise tolerance |
563 | 136k | int32_t dot_count; // small objects |
564 | 136k | int32_t norm_count; // normal objects |
565 | 136k | int32_t dud_words; // number discarded |
566 | 136k | int32_t ok_words; // number remaining |
567 | 136k | int32_t word_index; // current word |
568 | | // words of row |
569 | 136k | WERD_IT word_it = row->word_list(); |
570 | 136k | C_BLOB_IT blob_it; // blob iterator |
571 | 136k | C_OUTLINE_IT out_it; // outline iterator |
572 | | |
573 | 136k | ok_words = word_it.length(); |
574 | 136k | if (ok_words == 0 || textord_no_rejects) { |
575 | 0 | return; |
576 | 0 | } |
577 | | // was it chucked |
578 | 136k | std::vector<int8_t> word_dud(ok_words); |
579 | 136k | dud_words = 0; |
580 | 136k | ok_words = 0; |
581 | 136k | word_index = 0; |
582 | 333k | for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) { |
583 | 196k | word = word_it.data(); // current word |
584 | 196k | dot_count = 0; |
585 | 196k | norm_count = 0; |
586 | | // blobs in word |
587 | 196k | blob_it.set_to_list(word->cblob_list()); |
588 | 857k | for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { |
589 | 661k | blob = blob_it.data(); |
590 | 661k | if (!word->flag(W_DONT_CHOP)) { |
591 | | // get outlines |
592 | 626k | out_it.set_to_list(blob->out_list()); |
593 | 1.56M | for (out_it.mark_cycle_pt(); !out_it.cycled_list(); out_it.forward()) { |
594 | 935k | outline = out_it.data(); |
595 | 935k | blob_box = outline->bounding_box(); |
596 | 935k | blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height(); |
597 | 935k | if (blob_size < textord_noise_sizelimit * row->x_height()) { |
598 | 364k | dot_count++; // count small outlines |
599 | 364k | } |
600 | 935k | if (!outline->child()->empty() && |
601 | 38.7k | blob_box.height() < (1 + textord_noise_syfract) * row->x_height() && |
602 | 33.9k | blob_box.height() > (1 - textord_noise_syfract) * row->x_height() && |
603 | 10.9k | blob_box.width() < (1 + textord_noise_sxfract) * row->x_height() && |
604 | 9.50k | blob_box.width() > (1 - textord_noise_sxfract) * row->x_height()) { |
605 | 4.06k | norm_count++; // count small outlines |
606 | 4.06k | } |
607 | 935k | } |
608 | 626k | } else { |
609 | 35.3k | norm_count++; |
610 | 35.3k | } |
611 | 661k | blob_box = blob->bounding_box(); |
612 | 661k | blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height(); |
613 | 661k | if (blob_size >= textord_noise_sizelimit * row->x_height() && |
614 | 558k | blob_size < row->x_height() * 2) { |
615 | 535k | trans_threshold = blob_size / textord_noise_sizefraction; |
616 | 535k | trans_count = blob->count_transitions(trans_threshold); |
617 | 535k | if (trans_count < textord_noise_translimit) { |
618 | 502k | norm_count++; |
619 | 502k | } |
620 | 535k | } else if (blob_box.height() > row->x_height() * 2 && |
621 | 10.9k | (!word_it.at_first() || !blob_it.at_first())) { |
622 | 6.07k | dot_count += 2; |
623 | 6.07k | } |
624 | 661k | } |
625 | 196k | if (dot_count > 2 && !word->flag(W_REP_CHAR)) { |
626 | 27.8k | if (dot_count > norm_count * textord_noise_normratio * 2) { |
627 | 3.74k | word_dud[word_index] = 2; |
628 | 24.1k | } else if (dot_count > norm_count * textord_noise_normratio) { |
629 | 3.23k | word_dud[word_index] = 1; |
630 | 20.9k | } else { |
631 | 20.9k | word_dud[word_index] = 0; |
632 | 20.9k | } |
633 | 168k | } else { |
634 | 168k | word_dud[word_index] = 0; |
635 | 168k | } |
636 | 196k | if (word_dud[word_index] == 2) { |
637 | 3.74k | dud_words++; |
638 | 192k | } else { |
639 | 192k | ok_words++; |
640 | 192k | } |
641 | 196k | word_index++; |
642 | 196k | } |
643 | | |
644 | 136k | word_index = 0; |
645 | 333k | for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) { |
646 | 196k | if (word_dud[word_index] == 2 || (word_dud[word_index] == 1 && dud_words > ok_words)) { |
647 | 3.95k | word = word_it.data(); // Current word. |
648 | | // Previously we threw away the entire word. |
649 | | // Now just aggressively throw all small blobs into the reject list, where |
650 | | // the classifier can decide whether they are actually needed. |
651 | 3.95k | word->CleanNoise(textord_noise_sizelimit * row->x_height()); |
652 | 3.95k | } |
653 | 196k | word_index++; |
654 | 196k | } |
655 | 136k | } |
656 | | |
657 | | // Remove outlines that are a tiny fraction in either width or height |
658 | | // of the word height. |
659 | 173k | void Textord::clean_small_noise_from_words(ROW *row) { |
660 | 173k | WERD_IT word_it(row->word_list()); |
661 | 449k | for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) { |
662 | 276k | WERD *word = word_it.data(); |
663 | 276k | int min_size = static_cast<int>(textord_noise_hfract * word->bounding_box().height() + 0.5); |
664 | 276k | C_BLOB_IT blob_it(word->cblob_list()); |
665 | 1.48M | for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { |
666 | 1.20M | C_BLOB *blob = blob_it.data(); |
667 | 1.20M | C_OUTLINE_IT out_it(blob->out_list()); |
668 | 3.50M | for (out_it.mark_cycle_pt(); !out_it.cycled_list(); out_it.forward()) { |
669 | 2.29M | C_OUTLINE *outline = out_it.data(); |
670 | 2.29M | outline->RemoveSmallRecursive(min_size, &out_it); |
671 | 2.29M | } |
672 | 1.20M | if (blob->out_list()->empty()) { |
673 | 155k | delete blob_it.extract(); |
674 | 155k | } |
675 | 1.20M | } |
676 | 276k | if (word->cblob_list()->empty()) { |
677 | 6.56k | if (!word_it.at_last()) { |
678 | | // The next word is no longer a fuzzy non space if it was before, |
679 | | // since the word before is about to be deleted. |
680 | 870 | WERD *next_word = word_it.data_relative(1); |
681 | 870 | if (next_word->flag(W_FUZZY_NON)) { |
682 | 254 | next_word->set_flag(W_FUZZY_NON, false); |
683 | 254 | } |
684 | 870 | } |
685 | 6.56k | delete word_it.extract(); |
686 | 6.56k | } |
687 | 276k | } |
688 | 173k | } |
689 | | |
690 | | // Local struct to hold a group of blocks. |
691 | | struct BlockGroup { |
692 | 0 | BlockGroup() : rotation(1.0f, 0.0f), angle(0.0f), min_xheight(1.0f) {} |
693 | | explicit BlockGroup(BLOCK *block) |
694 | 13.8k | : bounding_box(block->pdblk.bounding_box()) |
695 | 13.8k | , rotation(block->re_rotation()) |
696 | 13.8k | , angle(block->re_rotation().angle()) |
697 | 13.8k | , min_xheight(block->x_height()) { |
698 | 13.8k | blocks.push_back(block); |
699 | 13.8k | } |
700 | | // Union of block bounding boxes. |
701 | | TBOX bounding_box; |
702 | | // Common rotation of the blocks. |
703 | | FCOORD rotation; |
704 | | // Angle of rotation. |
705 | | float angle; |
706 | | // Min xheight of the blocks. |
707 | | float min_xheight; |
708 | | // Collection of borrowed pointers to the blocks in the group. |
709 | | std::vector<BLOCK *> blocks; |
710 | | }; |
711 | | |
712 | | // Groups blocks by rotation, then, for each group, makes a WordGrid and calls |
713 | | // TransferDiacriticsToWords to copy the diacritic blobs to the most |
714 | | // appropriate words in the group of blocks. Source blobs are not touched. |
715 | 15.4k | void Textord::TransferDiacriticsToBlockGroups(BLOBNBOX_LIST *diacritic_blobs, BLOCK_LIST *blocks) { |
716 | | // Angle difference larger than this is too much to consider equal. |
717 | | // They should only be in multiples of M_PI/2 anyway. |
718 | 15.4k | const double kMaxAngleDiff = 0.01; // About 0.6 degrees. |
719 | 15.4k | std::vector<std::unique_ptr<BlockGroup>> groups; |
720 | 15.4k | BLOCK_IT bk_it(blocks); |
721 | 29.2k | for (bk_it.mark_cycle_pt(); !bk_it.cycled_list(); bk_it.forward()) { |
722 | 13.8k | BLOCK *block = bk_it.data(); |
723 | 13.8k | if (block->pdblk.poly_block() != nullptr && !block->pdblk.poly_block()->IsText()) { |
724 | 0 | continue; |
725 | 0 | } |
726 | | // Linear search of the groups to find a matching rotation. |
727 | 13.8k | float block_angle = block->re_rotation().angle(); |
728 | 13.8k | int best_g = 0; |
729 | 13.8k | float best_angle_diff = FLT_MAX; |
730 | 13.8k | for (const auto &group : groups) { |
731 | 0 | double angle_diff = std::fabs(block_angle - group->angle); |
732 | 0 | if (angle_diff > M_PI) { |
733 | 0 | angle_diff = fabs(angle_diff - 2.0 * M_PI); |
734 | 0 | } |
735 | 0 | if (angle_diff < best_angle_diff) { |
736 | 0 | best_angle_diff = angle_diff; |
737 | 0 | best_g = &group - &groups[0]; |
738 | 0 | } |
739 | 0 | } |
740 | 13.8k | if (best_angle_diff > kMaxAngleDiff) { |
741 | 13.8k | groups.push_back(std::make_unique<BlockGroup>(block)); |
742 | 13.8k | } else { |
743 | 0 | groups[best_g]->blocks.push_back(block); |
744 | 0 | groups[best_g]->bounding_box += block->pdblk.bounding_box(); |
745 | 0 | float x_height = block->x_height(); |
746 | 0 | if (x_height < groups[best_g]->min_xheight) { |
747 | 0 | groups[best_g]->min_xheight = x_height; |
748 | 0 | } |
749 | 0 | } |
750 | 13.8k | } |
751 | | // Now process each group of blocks. |
752 | 15.4k | std::vector<std::unique_ptr<WordWithBox>> word_ptrs; |
753 | 15.4k | for (const auto &group : groups) { |
754 | 13.8k | if (group->bounding_box.null_box()) { |
755 | 0 | continue; |
756 | 0 | } |
757 | 13.8k | WordGrid word_grid(group->min_xheight, group->bounding_box.botleft(), |
758 | 13.8k | group->bounding_box.topright()); |
759 | 13.8k | for (auto b : group->blocks) { |
760 | 13.8k | ROW_IT row_it(b->row_list()); |
761 | 150k | for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { |
762 | 136k | ROW *row = row_it.data(); |
763 | | // Put the words of the row into the grid. |
764 | 136k | WERD_IT w_it(row->word_list()); |
765 | 333k | for (w_it.mark_cycle_pt(); !w_it.cycled_list(); w_it.forward()) { |
766 | 196k | WERD *word = w_it.data(); |
767 | 196k | auto box_word = std::make_unique<WordWithBox>(word); |
768 | 196k | word_grid.InsertBBox(true, true, box_word.get()); |
769 | | // Save the pointer where it will be auto-deleted. |
770 | 196k | word_ptrs.emplace_back(std::move(box_word)); |
771 | 196k | } |
772 | 136k | } |
773 | 13.8k | } |
774 | 13.8k | FCOORD rotation = group->rotation; |
775 | | // Make it a forward rotation that will transform blob coords to block. |
776 | 13.8k | rotation.set_y(-rotation.y()); |
777 | 13.8k | TransferDiacriticsToWords(diacritic_blobs, rotation, &word_grid); |
778 | 13.8k | } |
779 | 15.4k | } |
780 | | |
781 | | // Places a copy of blobs that are near a word (after applying rotation to the |
782 | | // blob) in the most appropriate word, unless there is doubt, in which case a |
783 | | // blob can end up in two words. Source blobs are not touched. |
784 | | void Textord::TransferDiacriticsToWords(BLOBNBOX_LIST *diacritic_blobs, const FCOORD &rotation, |
785 | 13.8k | WordGrid *word_grid) { |
786 | 13.8k | WordSearch ws(word_grid); |
787 | 13.8k | BLOBNBOX_IT b_it(diacritic_blobs); |
788 | | // Apply rotation to each blob before finding the nearest words. The rotation |
789 | | // allows us to only consider above/below placement and not left/right on |
790 | | // vertical text, because all text is horizontal here. |
791 | 13.8k | for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) { |
792 | 0 | BLOBNBOX *blobnbox = b_it.data(); |
793 | 0 | TBOX blob_box = blobnbox->bounding_box(); |
794 | 0 | blob_box.rotate(rotation); |
795 | 0 | ws.StartRectSearch(blob_box); |
796 | | // Above/below refer to word position relative to diacritic. Since some |
797 | | // scripts eg Kannada/Telugu habitually put diacritics below words, and |
798 | | // others eg Thai/Vietnamese/Latin put most diacritics above words, try |
799 | | // for both if there isn't much in it. |
800 | 0 | WordWithBox *best_above_word = nullptr; |
801 | 0 | WordWithBox *best_below_word = nullptr; |
802 | 0 | int best_above_distance = 0; |
803 | 0 | int best_below_distance = 0; |
804 | 0 | for (WordWithBox *word = ws.NextRectSearch(); word != nullptr; word = ws.NextRectSearch()) { |
805 | 0 | if (word->word()->flag(W_REP_CHAR)) { |
806 | 0 | continue; |
807 | 0 | } |
808 | 0 | TBOX word_box = word->true_bounding_box(); |
809 | 0 | int x_distance = blob_box.x_gap(word_box); |
810 | 0 | int y_distance = blob_box.y_gap(word_box); |
811 | 0 | if (x_distance > 0) { |
812 | | // Arbitrarily divide x-distance by 2 if there is a major y overlap, |
813 | | // and the word is to the left of the diacritic. If the |
814 | | // diacritic is a dropped broken character between two words, this will |
815 | | // help send all the pieces to a single word, instead of splitting them |
816 | | // over the 2 words. |
817 | 0 | if (word_box.major_y_overlap(blob_box) && blob_box.left() > word_box.right()) { |
818 | 0 | x_distance /= 2; |
819 | 0 | } |
820 | 0 | y_distance += x_distance; |
821 | 0 | } |
822 | 0 | if (word_box.y_middle() > blob_box.y_middle() && |
823 | 0 | (best_above_word == nullptr || y_distance < best_above_distance)) { |
824 | 0 | best_above_word = word; |
825 | 0 | best_above_distance = y_distance; |
826 | 0 | } |
827 | 0 | if (word_box.y_middle() <= blob_box.y_middle() && |
828 | 0 | (best_below_word == nullptr || y_distance < best_below_distance)) { |
829 | 0 | best_below_word = word; |
830 | 0 | best_below_distance = y_distance; |
831 | 0 | } |
832 | 0 | } |
833 | 0 | bool above_good = best_above_word != nullptr && |
834 | 0 | (best_below_word == nullptr || |
835 | 0 | best_above_distance < best_below_distance + blob_box.height()); |
836 | 0 | bool below_good = best_below_word != nullptr && best_below_word != best_above_word && |
837 | 0 | (best_above_word == nullptr || |
838 | 0 | best_below_distance < best_above_distance + blob_box.height()); |
839 | 0 | if (below_good) { |
840 | 0 | C_BLOB *copied_blob = C_BLOB::deep_copy(blobnbox->cblob()); |
841 | 0 | copied_blob->rotate(rotation); |
842 | | // Put the blob into the word's reject blobs list. |
843 | 0 | C_BLOB_IT blob_it(best_below_word->RejBlobs()); |
844 | 0 | blob_it.add_to_end(copied_blob); |
845 | 0 | } |
846 | 0 | if (above_good) { |
847 | 0 | C_BLOB *copied_blob = C_BLOB::deep_copy(blobnbox->cblob()); |
848 | 0 | copied_blob->rotate(rotation); |
849 | | // Put the blob into the word's reject blobs list. |
850 | 0 | C_BLOB_IT blob_it(best_above_word->RejBlobs()); |
851 | 0 | blob_it.add_to_end(copied_blob); |
852 | 0 | } |
853 | 0 | } |
854 | 13.8k | } |
855 | | |
856 | | /********************************************************************** |
857 | | * tweak_row_baseline |
858 | | * |
859 | | * Shift baseline to fit the blobs more accurately where they are |
860 | | * close enough. |
861 | | **********************************************************************/ |
862 | | |
863 | 136k | void tweak_row_baseline(ROW *row, double blshift_maxshift, double blshift_xfraction) { |
864 | 136k | TBOX blob_box; // bounding box |
865 | 136k | C_BLOB *blob; // current blob |
866 | 136k | WERD *word; // current word |
867 | 136k | int32_t blob_count; // no of blobs |
868 | 136k | int32_t src_index; // source segment |
869 | 136k | int32_t dest_index; // destination segment |
870 | 136k | float ydiff; // baseline error |
871 | 136k | float x_centre; // centre of blob |
872 | | // words of row |
873 | 136k | WERD_IT word_it = row->word_list(); |
874 | 136k | C_BLOB_IT blob_it; // blob iterator |
875 | | |
876 | 136k | blob_count = 0; |
877 | 333k | for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) { |
878 | 196k | word = word_it.data(); // current word |
879 | | // get total blobs |
880 | 196k | blob_count += word->cblob_list()->length(); |
881 | 196k | } |
882 | 136k | if (blob_count == 0) { |
883 | 0 | return; |
884 | 0 | } |
885 | | // spline segments |
886 | 136k | std::vector<int32_t> xstarts(blob_count + row->baseline.segments + 1); |
887 | | // spline coeffs |
888 | 136k | std::vector<double> coeffs((blob_count + row->baseline.segments) * 3); |
889 | | |
890 | 136k | src_index = 0; |
891 | 136k | dest_index = 0; |
892 | 136k | xstarts[0] = row->baseline.xcoords[0]; |
893 | 333k | for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) { |
894 | 196k | word = word_it.data(); // current word |
895 | | // blobs in word |
896 | 196k | blob_it.set_to_list(word->cblob_list()); |
897 | 843k | for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { |
898 | 647k | blob = blob_it.data(); |
899 | 647k | blob_box = blob->bounding_box(); |
900 | 647k | x_centre = (blob_box.left() + blob_box.right()) / 2.0; |
901 | 647k | ydiff = blob_box.bottom() - row->base_line(x_centre); |
902 | 647k | if (ydiff < 0) { |
903 | 223k | ydiff = -ydiff / row->x_height(); |
904 | 424k | } else { |
905 | 424k | ydiff = ydiff / row->x_height(); |
906 | 424k | } |
907 | 647k | if (ydiff < blshift_maxshift && blob_box.height() / row->x_height() > blshift_xfraction) { |
908 | 0 | if (xstarts[dest_index] >= x_centre) { |
909 | 0 | xstarts[dest_index] = blob_box.left(); |
910 | 0 | } |
911 | 0 | coeffs[dest_index * 3] = 0; |
912 | 0 | coeffs[dest_index * 3 + 1] = 0; |
913 | 0 | coeffs[dest_index * 3 + 2] = blob_box.bottom(); |
914 | | // shift it |
915 | 0 | dest_index++; |
916 | 0 | xstarts[dest_index] = blob_box.right() + 1; |
917 | 647k | } else { |
918 | 647k | if (xstarts[dest_index] <= x_centre) { |
919 | 381k | while (row->baseline.xcoords[src_index + 1] <= x_centre && |
920 | 153k | src_index < row->baseline.segments - 1) { |
921 | 153k | if (row->baseline.xcoords[src_index + 1] > xstarts[dest_index]) { |
922 | 61.9k | coeffs[dest_index * 3] = row->baseline.quadratics[src_index].a; |
923 | 61.9k | coeffs[dest_index * 3 + 1] = row->baseline.quadratics[src_index].b; |
924 | 61.9k | coeffs[dest_index * 3 + 2] = row->baseline.quadratics[src_index].c; |
925 | 61.9k | dest_index++; |
926 | 61.9k | xstarts[dest_index] = row->baseline.xcoords[src_index + 1]; |
927 | 61.9k | } |
928 | 153k | src_index++; |
929 | 153k | } |
930 | 228k | coeffs[dest_index * 3] = row->baseline.quadratics[src_index].a; |
931 | 228k | coeffs[dest_index * 3 + 1] = row->baseline.quadratics[src_index].b; |
932 | 228k | coeffs[dest_index * 3 + 2] = row->baseline.quadratics[src_index].c; |
933 | 228k | dest_index++; |
934 | 228k | xstarts[dest_index] = row->baseline.xcoords[src_index + 1]; |
935 | 228k | } |
936 | 647k | } |
937 | 647k | } |
938 | 196k | } |
939 | 273k | while (src_index < row->baseline.segments && |
940 | 248k | row->baseline.xcoords[src_index + 1] <= xstarts[dest_index]) { |
941 | 136k | src_index++; |
942 | 136k | } |
943 | 251k | while (src_index < row->baseline.segments) { |
944 | 114k | coeffs[dest_index * 3] = row->baseline.quadratics[src_index].a; |
945 | 114k | coeffs[dest_index * 3 + 1] = row->baseline.quadratics[src_index].b; |
946 | 114k | coeffs[dest_index * 3 + 2] = row->baseline.quadratics[src_index].c; |
947 | 114k | dest_index++; |
948 | 114k | src_index++; |
949 | 114k | xstarts[dest_index] = row->baseline.xcoords[src_index]; |
950 | 114k | } |
951 | | // turn to spline |
952 | 136k | row->baseline = QSPLINE(dest_index, &xstarts[0], &coeffs[0]); |
953 | 136k | } |
954 | | |
955 | | } // namespace tesseract |