1 /*
2 Scan Tailor - Interactive post-processing tool for scanned pages.
3 Copyright (C) Joseph Artsimovich <joseph.artsimovich@gmail.com>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "ContentBoxFinder.h"
20 #include <QDebug>
21 #include <QPainter>
22 #include <QPainterPath>
23 #include <cmath>
24 #include <queue>
25 #include "DebugImages.h"
26 #include "Despeckle.h"
27 #include "FilterData.h"
28 #include "TaskStatus.h"
29 #include "imageproc/Binarize.h"
30 #include "imageproc/BinaryImage.h"
31 #include "imageproc/ConnComp.h"
32 #include "imageproc/ConnCompEraserExt.h"
33 #include "imageproc/Connectivity.h"
34 #include "imageproc/ConnectivityMap.h"
35 #include "imageproc/GrayRasterOp.h"
36 #include "imageproc/InfluenceMap.h"
37 #include "imageproc/MaxWhitespaceFinder.h"
38 #include "imageproc/Morphology.h"
39 #include "imageproc/PolygonRasterizer.h"
40 #include "imageproc/RasterOp.h"
41 #include "imageproc/SEDM.h"
42 #include "imageproc/SeedFill.h"
43 #include "imageproc/SlicedHistogram.h"
44 #include "imageproc/Transform.h"
45
46 namespace select_content {
47 using namespace imageproc;
48
49 class ContentBoxFinder::Garbage {
50 public:
51 enum Type { HOR, VERT };
52
53 Garbage(Type type, const BinaryImage& garbage);
54
55 void add(const BinaryImage& garbage, const QRect& rect);
56
image() const57 const BinaryImage& image() const { return m_garbage; }
58
59 const SEDM& sedm();
60
61 private:
62 imageproc::BinaryImage m_garbage;
63 SEDM m_sedm;
64 SEDM::Borders m_sedmBorders;
65 bool m_sedmUpdatePending;
66 };
67
68
69 namespace {
70 struct PreferHorizontal {
operator ()select_content::__anon7b870ffa0111::PreferHorizontal71 bool operator()(const QRect& lhs, const QRect& rhs) const {
72 return lhs.width() * lhs.width() * lhs.height() < rhs.width() * rhs.width() * rhs.height();
73 }
74 };
75
76 struct PreferVertical {
operator ()select_content::__anon7b870ffa0111::PreferVertical77 bool operator()(const QRect& lhs, const QRect& rhs) const {
78 return lhs.width() * lhs.height() * lhs.height() < rhs.width() * rhs.height() * rhs.height();
79 }
80 };
81 } // namespace
82
findContentBox(const TaskStatus & status,const FilterData & data,const QRectF & page_rect,DebugImages * dbg)83 QRectF ContentBoxFinder::findContentBox(const TaskStatus& status,
84 const FilterData& data,
85 const QRectF& page_rect,
86 DebugImages* dbg) {
87 ImageTransformation xform_150dpi(data.xform());
88 xform_150dpi.preScaleToDpi(Dpi(150, 150));
89
90 if (xform_150dpi.resultingRect().toRect().isEmpty()) {
91 return QRectF();
92 }
93
94 const GrayImage dataGrayImage = data.isBlackOnWhite() ? data.grayImage() : data.grayImage().inverted();
95 const uint8_t darkest_gray_level = darkestGrayLevel(dataGrayImage);
96 const QColor outside_color(darkest_gray_level, darkest_gray_level, darkest_gray_level);
97
98 QImage gray150(transformToGray(dataGrayImage, xform_150dpi.transform(), xform_150dpi.resultingRect().toRect(),
99 OutsidePixels::assumeColor(outside_color)));
100 // Note that we fill new areas that appear as a result of
101 // rotation with black, not white. Filling them with white
102 // may be bad for detecting the shadow around the page.
103 if (dbg) {
104 dbg->add(gray150, "gray150");
105 }
106
107 BinaryImage bw150(binarizeWolf(gray150, QSize(51, 51), 50));
108 if (dbg) {
109 dbg->add(bw150, "bw150");
110 }
111
112 const double xscale = 150.0 / data.xform().origDpi().horizontal();
113 const double yscale = 150.0 / data.xform().origDpi().vertical();
114 QRectF page_rect150(page_rect.left() * xscale, page_rect.top() * yscale, page_rect.right() * xscale,
115 page_rect.bottom() * yscale);
116 PolygonRasterizer::fillExcept(bw150, BLACK, page_rect150, Qt::WindingFill);
117
118 PolygonRasterizer::fillExcept(bw150, BLACK, xform_150dpi.resultingPreCropArea(), Qt::WindingFill);
119 if (dbg) {
120 dbg->add(bw150, "page_mask_applied");
121 }
122
123 BinaryImage hor_shadows_seed(openBrick(bw150, QSize(200, 14), BLACK));
124 if (dbg) {
125 dbg->add(hor_shadows_seed, "hor_shadows_seed");
126 }
127
128 status.throwIfCancelled();
129
130 BinaryImage ver_shadows_seed(openBrick(bw150, QSize(14, 300), BLACK));
131 if (dbg) {
132 dbg->add(ver_shadows_seed, "ver_shadows_seed");
133 }
134
135 status.throwIfCancelled();
136
137 BinaryImage shadows_seed(hor_shadows_seed.release());
138 rasterOp<RopOr<RopSrc, RopDst>>(shadows_seed, ver_shadows_seed);
139 ver_shadows_seed.release();
140 if (dbg) {
141 dbg->add(shadows_seed, "shadows_seed");
142 }
143
144 status.throwIfCancelled();
145
146 BinaryImage dilated(dilateBrick(bw150, QSize(3, 3)));
147 if (dbg) {
148 dbg->add(dilated, "dilated");
149 }
150
151 status.throwIfCancelled();
152
153 BinaryImage shadows_dilated(seedFill(shadows_seed, dilated, CONN8));
154 dilated.release();
155 if (dbg) {
156 dbg->add(shadows_dilated, "shadows_dilated");
157 }
158
159 status.throwIfCancelled();
160
161 rasterOp<RopAnd<RopSrc, RopDst>>(shadows_dilated, bw150);
162 BinaryImage garbage(shadows_dilated.release());
163 if (dbg) {
164 dbg->add(garbage, "shadows");
165 }
166
167 status.throwIfCancelled();
168
169 filterShadows(status, garbage, dbg);
170 if (dbg) {
171 dbg->add(garbage, "filtered_shadows");
172 }
173
174 status.throwIfCancelled();
175
176 BinaryImage content(bw150.release());
177 rasterOp<RopSubtract<RopDst, RopSrc>>(content, garbage);
178 if (dbg) {
179 dbg->add(content, "content");
180 }
181
182 status.throwIfCancelled();
183
184 Despeckle::Level despeckleLevel = Despeckle::NORMAL;
185
186 BinaryImage despeckled(Despeckle::despeckle(content, Dpi(150, 150), despeckleLevel, status, dbg));
187 if (dbg) {
188 dbg->add(despeckled, "despeckled");
189 }
190
191 status.throwIfCancelled();
192
193 BinaryImage content_blocks(content.size(), BLACK);
194 const int area_threshold = std::min(content.width(), content.height());
195
196 {
197 MaxWhitespaceFinder hor_ws_finder(PreferHorizontal(), despeckled);
198
199 for (int i = 0; i < 80; ++i) {
200 QRect ws(hor_ws_finder.next(hor_ws_finder.MANUAL_OBSTACLES));
201 if (ws.isNull()) {
202 break;
203 }
204 if (ws.width() * ws.height() < area_threshold) {
205 break;
206 }
207 content_blocks.fill(ws, WHITE);
208 const int height_fraction = ws.height() / 5;
209 ws.setTop(ws.top() + height_fraction);
210 ws.setBottom(ws.bottom() - height_fraction);
211 hor_ws_finder.addObstacle(ws);
212 }
213 }
214
215 {
216 MaxWhitespaceFinder vert_ws_finder(PreferVertical(), despeckled);
217
218 for (int i = 0; i < 40; ++i) {
219 QRect ws(vert_ws_finder.next(vert_ws_finder.MANUAL_OBSTACLES));
220 if (ws.isNull()) {
221 break;
222 }
223 if (ws.width() * ws.height() < area_threshold) {
224 break;
225 }
226 content_blocks.fill(ws, WHITE);
227 const int width_fraction = ws.width() / 5;
228 ws.setLeft(ws.left() + width_fraction);
229 ws.setRight(ws.right() - width_fraction);
230 vert_ws_finder.addObstacle(ws);
231 }
232 }
233
234 if (dbg) {
235 dbg->add(content_blocks, "content_blocks");
236 }
237
238 trimContentBlocksInPlace(despeckled, content_blocks);
239 if (dbg) {
240 dbg->add(content_blocks, "initial_trimming");
241 }
242
243 // Do some more whitespace finding. This should help us separate
244 // blocks that don't belong together.
245 {
246 BinaryImage tmp(content);
247 rasterOp<RopOr<RopNot<RopSrc>, RopDst>>(tmp, content_blocks);
248 MaxWhitespaceFinder ws_finder(tmp.release(), QSize(4, 4));
249
250 for (int i = 0; i < 10; ++i) {
251 QRect ws(ws_finder.next());
252 if (ws.isNull()) {
253 break;
254 }
255 if (ws.width() * ws.height() < area_threshold) {
256 break;
257 }
258 content_blocks.fill(ws, WHITE);
259 }
260 }
261 if (dbg) {
262 dbg->add(content_blocks, "more_whitespace");
263 }
264
265 trimContentBlocksInPlace(despeckled, content_blocks);
266 if (dbg) {
267 dbg->add(content_blocks, "more_trimming");
268 }
269
270 despeckled.release();
271
272 inPlaceRemoveAreasTouchingBorders(content_blocks, dbg);
273 if (dbg) {
274 dbg->add(content_blocks, "except_bordering");
275 }
276
277 BinaryImage text_mask(estimateTextMask(content, content_blocks, dbg));
278 if (dbg) {
279 QImage text_mask_visualized(content.size(), QImage::Format_ARGB32_Premultiplied);
280 text_mask_visualized.fill(0xffffffff); // Opaque white.
281 QPainter painter(&text_mask_visualized);
282
283 QImage tmp(content.size(), QImage::Format_ARGB32_Premultiplied);
284 tmp.fill(0xff64dd62); // Opaque light green.
285 tmp.setAlphaChannel(text_mask.inverted().toQImage());
286 painter.drawImage(QPoint(0, 0), tmp);
287
288 tmp.fill(0xe0000000); // Mostly transparent black.
289 tmp.setAlphaChannel(content.inverted().toQImage());
290 painter.drawImage(QPoint(0, 0), tmp);
291
292 painter.end();
293
294 dbg->add(text_mask_visualized, "text_mask");
295 }
296
297 // Make text_mask strore the actual content pixels that are text.
298 rasterOp<RopAnd<RopSrc, RopDst>>(text_mask, content);
299
300 QRect content_rect(content_blocks.contentBoundingBox());
301 // Temporarily reuse hor_shadows_seed and ver_shadows_seed.
302 // It's OK they are null.
303 segmentGarbage(garbage, hor_shadows_seed, ver_shadows_seed, dbg);
304 garbage.release();
305
306 if (dbg) {
307 dbg->add(hor_shadows_seed, "initial_hor_garbage");
308 dbg->add(ver_shadows_seed, "initial_vert_garbage");
309 }
310
311 Garbage hor_garbage(Garbage::HOR, hor_shadows_seed.release());
312 Garbage vert_garbage(Garbage::VERT, ver_shadows_seed.release());
313
314 enum Side { LEFT = 1, RIGHT = 2, TOP = 4, BOTTOM = 8 };
315
316 int side_mask = LEFT | RIGHT | TOP | BOTTOM;
317
318 while (side_mask && !content_rect.isEmpty()) {
319 QRect old_content_rect;
320
321 if (side_mask & LEFT) {
322 side_mask &= ~LEFT;
323 old_content_rect = content_rect;
324 content_rect = trimLeft(content, content_blocks, text_mask, content_rect, vert_garbage, dbg);
325
326 status.throwIfCancelled();
327
328 if (content_rect.isEmpty()) {
329 break;
330 }
331 if (old_content_rect != content_rect) {
332 side_mask |= LEFT | TOP | BOTTOM;
333 }
334 }
335
336 if (side_mask & RIGHT) {
337 side_mask &= ~RIGHT;
338 old_content_rect = content_rect;
339 content_rect = trimRight(content, content_blocks, text_mask, content_rect, vert_garbage, dbg);
340
341 status.throwIfCancelled();
342
343 if (content_rect.isEmpty()) {
344 break;
345 }
346 if (old_content_rect != content_rect) {
347 side_mask |= RIGHT | TOP | BOTTOM;
348 }
349 }
350
351 if (side_mask & TOP) {
352 side_mask &= ~TOP;
353 old_content_rect = content_rect;
354 content_rect = trimTop(content, content_blocks, text_mask, content_rect, hor_garbage, dbg);
355
356 status.throwIfCancelled();
357
358 if (content_rect.isEmpty()) {
359 break;
360 }
361 if (old_content_rect != content_rect) {
362 side_mask |= TOP | LEFT | RIGHT;
363 }
364 }
365
366 if (side_mask & BOTTOM) {
367 side_mask &= ~BOTTOM;
368 old_content_rect = content_rect;
369 content_rect = trimBottom(content, content_blocks, text_mask, content_rect, hor_garbage, dbg);
370
371 status.throwIfCancelled();
372
373 if (content_rect.isEmpty()) {
374 break;
375 }
376 if (old_content_rect != content_rect) {
377 side_mask |= BOTTOM | LEFT | RIGHT;
378 }
379 }
380
381 if ((content_rect.width() < 8) || (content_rect.height() < 8)) {
382 content_rect = QRect();
383 break;
384 } else if ((content_rect.width() < 30) && (content_rect.height() > content_rect.width() * 20)) {
385 content_rect = QRect();
386 break;
387 }
388 }
389
390 // Increase the content rect due to cutting off the content at edges because of rescaling made.
391 if (!content_rect.isEmpty()) {
392 content_rect.adjust(-1, -1, 1, 1);
393 }
394
395 // Transform back from 150dpi.
396 QTransform combined_xform(xform_150dpi.transform().inverted());
397 combined_xform *= data.xform().transform();
398
399 return combined_xform.map(QRectF(content_rect)).boundingRect().intersected(data.xform().resultingRect());
400 } // ContentBoxFinder::findContentBox
401
402 namespace {
403 struct Bounds {
404 // All are inclusive.
405 int left;
406 int right;
407 int top;
408 int bottom;
409
Boundsselect_content::__anon7b870ffa0211::Bounds410 Bounds() : left(INT_MAX), right(INT_MIN), top(INT_MAX), bottom(INT_MIN) {}
411
isInsideselect_content::__anon7b870ffa0211::Bounds412 bool isInside(int x, int y) const {
413 if (x < left) {
414 return false;
415 } else if (x > right) {
416 return false;
417 } else if (y < top) {
418 return false;
419 } else if (y > bottom) {
420 return false;
421 } else {
422 return true;
423 }
424 }
425
forceInsideselect_content::__anon7b870ffa0211::Bounds426 void forceInside(int x, int y) {
427 if (x < left) {
428 left = x;
429 }
430 if (x > right) {
431 right = x;
432 }
433 if (y < top) {
434 top = y;
435 }
436 if (y > bottom) {
437 bottom = y;
438 }
439 }
440 };
441 } // namespace
442
trimContentBlocksInPlace(const imageproc::BinaryImage & content,imageproc::BinaryImage & content_blocks)443 void ContentBoxFinder::trimContentBlocksInPlace(const imageproc::BinaryImage& content,
444 imageproc::BinaryImage& content_blocks) {
445 const ConnectivityMap cmap(content_blocks, CONN4);
446 std::vector<Bounds> bounds(cmap.maxLabel() + 1);
447
448 int width = content.width();
449 int height = content.height();
450 const uint32_t msb = uint32_t(1) << 31;
451
452 const uint32_t* content_line = content.data();
453 const int content_stride = content.wordsPerLine();
454 const uint32_t* cmap_line = cmap.data();
455 const int cmap_stride = cmap.stride();
456 for (int y = 0; y < height; ++y) {
457 for (int x = 0; x < width; ++x) {
458 const uint32_t label = cmap_line[x];
459 if (label == 0) {
460 continue;
461 }
462 if (content_line[x >> 5] & (msb >> (x & 31))) {
463 bounds[label].forceInside(x, y);
464 }
465 }
466 cmap_line += cmap_stride;
467 content_line += content_stride;
468 }
469
470 uint32_t* cb_line = content_blocks.data();
471 const int cb_stride = content_blocks.wordsPerLine();
472 cmap_line = cmap.data();
473 for (int y = 0; y < height; ++y) {
474 for (int x = 0; x < width; ++x) {
475 const uint32_t label = cmap_line[x];
476 if (label == 0) {
477 continue;
478 }
479 if (!bounds[label].isInside(x, y)) {
480 cb_line[x >> 5] &= ~(msb >> (x & 31));
481 }
482 }
483 cmap_line += cmap_stride;
484 cb_line += cb_stride;
485 }
486 } // ContentBoxFinder::trimContentBlocksInPlace
487
inPlaceRemoveAreasTouchingBorders(imageproc::BinaryImage & content_blocks,DebugImages * dbg)488 void ContentBoxFinder::inPlaceRemoveAreasTouchingBorders(imageproc::BinaryImage& content_blocks, DebugImages* dbg) {
489 // We could just do a seed fill from borders, but that
490 // has the potential to remove too much. Instead, we
491 // do something similar to a seed fill, but with a limited
492 // spread distance.
493
494
495 const int width = content_blocks.width();
496 const int height = content_blocks.height();
497
498 const auto max_spread_dist = static_cast<const uint16_t>(std::min(width, height) / 4);
499
500 std::vector<uint16_t> map((width + 2) * (height + 2), ~uint16_t(0));
501
502 uint32_t* cb_line = content_blocks.data();
503 const int cb_stride = content_blocks.wordsPerLine();
504 uint16_t* map_line = &map[0] + width + 3;
505 const int map_stride = width + 2;
506 for (int y = 0; y < height; ++y) {
507 for (int x = 0; x < width; ++x) {
508 uint32_t mask = cb_line[x >> 5] >> (31 - (x & 31));
509 mask &= uint32_t(1);
510 --mask;
511
512 // WHITE -> max, BLACK -> 0
513 map_line[x] = static_cast<uint16_t>(mask);
514 }
515 map_line += map_stride;
516 cb_line += cb_stride;
517 }
518
519 std::queue<uint16_t*> queue;
520 // Initialize border seeds.
521 map_line = &map[0] + width + 3;
522 for (int x = 0; x < width; ++x) {
523 if (map_line[x] == 0) {
524 map_line[x] = max_spread_dist;
525 queue.push(&map_line[x]);
526 }
527 }
528 for (int y = 1; y < height - 1; ++y) {
529 if (map_line[0] == 0) {
530 map_line[0] = max_spread_dist;
531 queue.push(&map_line[0]);
532 }
533 if (map_line[width - 1] == 0) {
534 map_line[width - 1] = max_spread_dist;
535 queue.push(&map_line[width - 1]);
536 }
537 map_line += map_stride;
538 }
539 for (int x = 0; x < width; ++x) {
540 if (map_line[x] == 0) {
541 map_line[x] = max_spread_dist;
542 queue.push(&map_line[x]);
543 }
544 }
545
546 if (queue.empty()) {
547 // Common case optimization.
548 return;
549 }
550
551 while (!queue.empty()) {
552 uint16_t* cell = queue.front();
553 queue.pop();
554
555 assert(*cell != 0);
556 const auto new_dist = static_cast<const uint16_t>(*cell - 1);
557
558 uint16_t* nbh = cell - map_stride;
559 if (new_dist > *nbh) {
560 *nbh = new_dist;
561 queue.push(nbh);
562 }
563
564 nbh = cell - 1;
565 if (new_dist > *nbh) {
566 *nbh = new_dist;
567 queue.push(nbh);
568 }
569
570 nbh = cell + 1;
571 if (new_dist > *nbh) {
572 *nbh = new_dist;
573 queue.push(nbh);
574 }
575
576 nbh = cell + map_stride;
577 if (new_dist > *nbh) {
578 *nbh = new_dist;
579 queue.push(nbh);
580 }
581 }
582
583 cb_line = content_blocks.data();
584 map_line = &map[0] + width + 3;
585 const uint32_t msb = uint32_t(1) << 31;
586 for (int y = 0; y < height; ++y) {
587 for (int x = 0; x < width; ++x) {
588 if (map_line[x] + 1 > 1) { // If not 0 or ~uint16_t(0)
589 cb_line[x >> 5] &= ~(msb >> (x & 31));
590 }
591 }
592 map_line += map_stride;
593 cb_line += cb_stride;
594 }
595 } // ContentBoxFinder::inPlaceRemoveAreasTouchingBorders
596
segmentGarbage(const imageproc::BinaryImage & garbage,imageproc::BinaryImage & hor_garbage,imageproc::BinaryImage & vert_garbage,DebugImages * dbg)597 void ContentBoxFinder::segmentGarbage(const imageproc::BinaryImage& garbage,
598 imageproc::BinaryImage& hor_garbage,
599 imageproc::BinaryImage& vert_garbage,
600 DebugImages* dbg) {
601 hor_garbage = openBrick(garbage, QSize(200, 1), WHITE);
602
603 QRect rect(garbage.rect());
604 rect.setHeight(1);
605 rasterOp<RopOr<RopSrc, RopDst>>(hor_garbage, rect, garbage, rect.topLeft());
606 rect.moveBottom(garbage.rect().bottom());
607 rasterOp<RopOr<RopSrc, RopDst>>(hor_garbage, rect, garbage, rect.topLeft());
608
609 vert_garbage = openBrick(garbage, QSize(1, 200), WHITE);
610
611 rect = garbage.rect();
612 rect.setWidth(1);
613 rasterOp<RopOr<RopSrc, RopDst>>(vert_garbage, rect, garbage, rect.topLeft());
614 rect.moveRight(garbage.rect().right());
615 rasterOp<RopOr<RopSrc, RopDst>>(vert_garbage, rect, garbage, rect.topLeft());
616
617 ConnectivityMap cmap(garbage.size());
618
619 cmap.addComponent(vert_garbage);
620 vert_garbage.fill(WHITE);
621 cmap.addComponent(hor_garbage);
622 hor_garbage.fill(WHITE);
623
624 InfluenceMap imap(cmap, garbage);
625 cmap = ConnectivityMap();
626
627 const int width = garbage.width();
628 const int height = garbage.height();
629
630 InfluenceMap::Cell* imap_line = imap.data();
631 const int imap_stride = imap.stride();
632
633 uint32_t* vg_line = vert_garbage.data();
634 const int vg_stride = vert_garbage.wordsPerLine();
635 uint32_t* hg_line = hor_garbage.data();
636 const int hg_stride = hor_garbage.wordsPerLine();
637
638 const uint32_t msb = uint32_t(1) << 31;
639
640 for (int y = 0; y < height; ++y) {
641 for (int x = 0; x < width; ++x) {
642 switch (imap_line[x].label) {
643 case 1:
644 vg_line[x >> 5] |= msb >> (x & 31);
645 break;
646 case 2:
647 hg_line[x >> 5] |= msb >> (x & 31);
648 break;
649 default:
650 break;
651 }
652 }
653 imap_line += imap_stride;
654 vg_line += vg_stride;
655 hg_line += hg_stride;
656 }
657
658 BinaryImage unconnected_garbage(garbage);
659 rasterOp<RopSubtract<RopDst, RopSrc>>(unconnected_garbage, hor_garbage);
660 rasterOp<RopSubtract<RopDst, RopSrc>>(unconnected_garbage, vert_garbage);
661
662 rasterOp<RopOr<RopSrc, RopDst>>(hor_garbage, unconnected_garbage);
663 rasterOp<RopOr<RopSrc, RopDst>>(vert_garbage, unconnected_garbage);
664 } // ContentBoxFinder::segmentGarbage
665
estimateTextMask(const imageproc::BinaryImage & content,const imageproc::BinaryImage & content_blocks,DebugImages * dbg)666 imageproc::BinaryImage ContentBoxFinder::estimateTextMask(const imageproc::BinaryImage& content,
667 const imageproc::BinaryImage& content_blocks,
668 DebugImages* dbg) {
669 // We differentiate between a text line and a slightly skewed straight
670 // line (which may have a fill factor similar to that of text) by the
671 // presence of ultimate eroded points.
672
673 const BinaryImage ueps(SEDM(content, SEDM::DIST_TO_BLACK, SEDM::DIST_TO_NO_BORDERS).findPeaksDestructive());
674 if (dbg) {
675 QImage canvas(content_blocks.toQImage().convertToFormat(QImage::Format_ARGB32_Premultiplied));
676 QPainter painter;
677 painter.begin(&canvas);
678 QImage overlay(canvas.size(), canvas.format());
679 overlay.fill(0xff0000ff); // opaque blue
680 overlay.setAlphaChannel(content.inverted().toQImage());
681 painter.drawImage(QPoint(0, 0), overlay);
682
683 BinaryImage ueps_on_content_blocks(content_blocks);
684 rasterOp<RopAnd<RopSrc, RopDst>>(ueps_on_content_blocks, ueps);
685
686 overlay.fill(0xffffff00); // opaque yellow
687 overlay.setAlphaChannel(ueps_on_content_blocks.inverted().toQImage());
688 painter.drawImage(QPoint(0, 0), overlay);
689
690 painter.end();
691 dbg->add(canvas, "ueps");
692 }
693
694 BinaryImage text_mask(content.size(), WHITE);
695
696 const int min_text_height = 6;
697
698 ConnCompEraserExt eraser(content_blocks, CONN4);
699 while (true) {
700 const ConnComp cc(eraser.nextConnComp());
701 if (cc.isNull()) {
702 break;
703 }
704
705 BinaryImage cc_img(eraser.computeConnCompImage());
706 BinaryImage content_img(cc_img.size());
707 rasterOp<RopSrc>(content_img, content_img.rect(), content, cc.rect().topLeft());
708
709 // Note that some content may actually be not masked
710 // by content_blocks, because we build content_blocks
711 // based on despeckled content image.
712 rasterOp<RopAnd<RopSrc, RopDst>>(content_img, cc_img);
713
714 const SlicedHistogram hist(content_img, SlicedHistogram::ROWS);
715 const SlicedHistogram block_hist(cc_img, SlicedHistogram::ROWS);
716
717 assert(hist.size() != 0);
718
719 typedef std::pair<const int*, const int*> Range;
720 std::vector<Range> ranges;
721 std::vector<Range> splittable_ranges;
722 splittable_ranges.emplace_back(&hist[0], &hist[hist.size() - 1]);
723
724 std::vector<int> max_forward(hist.size());
725 std::vector<int> max_backwards(hist.size());
726
727 // Try splitting text lines.
728 while (!splittable_ranges.empty()) {
729 const int* const first = splittable_ranges.back().first;
730 const int* const last = splittable_ranges.back().second;
731 splittable_ranges.pop_back();
732
733 if (last - first < min_text_height - 1) {
734 // Just ignore such a small segment.
735 continue;
736 }
737 // Fill max_forward and max_backwards.
738 {
739 int prev = *first;
740 for (int i = 0; i <= last - first; ++i) {
741 prev = std::max(prev, first[i]);
742 max_forward[i] = prev;
743 }
744 prev = *last;
745 for (int i = 0; i <= last - first; ++i) {
746 prev = std::max(prev, last[-i]);
747 max_backwards[i] = prev;
748 }
749 }
750
751 int best_magnitude = std::numeric_limits<int>::min();
752 const int* best_split_pos = nullptr;
753 assert(first != last);
754 for (const int* p = first + 1; p != last; ++p) {
755 const int peak1 = max_forward[p - (first + 1)];
756 const int peak2 = max_backwards[(last - 1) - p];
757 if (*p * 3.5 > 0.5 * (peak1 + peak2)) {
758 continue;
759 }
760 const int shoulder1 = peak1 - *p;
761 const int shoulder2 = peak2 - *p;
762 if ((shoulder1 <= 0) || (shoulder2 <= 0)) {
763 continue;
764 }
765 if (std::min(shoulder1, shoulder2) * 20 < std::max(shoulder1, shoulder2)) {
766 continue;
767 }
768
769 const int magnitude = shoulder1 + shoulder2;
770 if (magnitude > best_magnitude) {
771 best_magnitude = magnitude;
772 best_split_pos = p;
773 }
774 }
775
776 if (best_split_pos) {
777 splittable_ranges.emplace_back(first, best_split_pos - 1);
778 splittable_ranges.emplace_back(best_split_pos + 1, last);
779 } else {
780 ranges.emplace_back(first, last);
781 }
782 }
783
784 for (const Range range : ranges) {
785 const auto first = static_cast<const int>(range.first - &hist[0]);
786 const auto last = static_cast<const int>(range.second - &hist[0]);
787 if (last - first < min_text_height - 1) {
788 continue;
789 }
790
791 int64_t weighted_y = 0;
792 int total_weight = 0;
793 for (int i = first; i <= last; ++i) {
794 const int val = hist[i];
795 weighted_y += val * i;
796 total_weight += val;
797 }
798
799 if (total_weight == 0) {
800 // qDebug() << "no black pixels at all";
801 continue;
802 }
803
804 const double min_fill_factor = 0.22;
805 const double max_fill_factor = 0.65;
806
807 const auto center_y = static_cast<const int>((weighted_y + total_weight / 2) / total_weight);
808 int top = center_y - min_text_height / 2;
809 int bottom = top + min_text_height - 1;
810 int num_black = 0;
811 int num_total = 0;
812 int max_width = 0;
813 if ((top < first) || (bottom > last)) {
814 continue;
815 }
816 for (int i = top; i <= bottom; ++i) {
817 num_black += hist[i];
818 num_total += block_hist[i];
819 max_width = std::max(max_width, block_hist[i]);
820 }
821 if (num_black < num_total * min_fill_factor) {
822 // qDebug() << "initial fill factor too low";
823 continue;
824 }
825 if (num_black > num_total * max_fill_factor) {
826 // qDebug() << "initial fill factor too high";
827 continue;
828 }
829
830 // Extend the top and bottom of the text line.
831 while ((top > first || bottom < last) && std::abs((center_y - top) - (bottom - center_y)) <= 1) {
832 const int new_top = (top > first) ? top - 1 : top;
833 const int new_bottom = (bottom < last) ? bottom + 1 : bottom;
834 num_black += hist[new_top] + hist[new_bottom];
835 num_total += block_hist[new_top] + block_hist[new_bottom];
836 if (num_black < num_total * min_fill_factor) {
837 break;
838 }
839 max_width = std::max(max_width, block_hist[new_top]);
840 max_width = std::max(max_width, block_hist[new_bottom]);
841 top = new_top;
842 bottom = new_bottom;
843 }
844
845 if (num_black > num_total * max_fill_factor) {
846 // qDebug() << "final fill factor too high";
847 continue;
848 }
849
850 if (max_width < (bottom - top + 1) * 0.6) {
851 // qDebug() << "aspect ratio too low";
852 continue;
853 }
854
855 QRect line_rect(cc.rect());
856 line_rect.setTop(cc.rect().top() + top);
857 line_rect.setBottom(cc.rect().top() + bottom);
858
859 // Check if there are enough ultimate eroded points on the line.
860 auto ueps_todo = int(0.4 * line_rect.width() / line_rect.height());
861 if (ueps_todo) {
862 BinaryImage line_ueps(line_rect.size());
863 rasterOp<RopSrc>(line_ueps, line_ueps.rect(), content_blocks, line_rect.topLeft());
864 rasterOp<RopAnd<RopSrc, RopDst>>(line_ueps, line_ueps.rect(), ueps, line_rect.topLeft());
865 ConnCompEraser ueps_eraser(line_ueps, CONN4);
866 ConnComp cc;
867 for (; ueps_todo && !(cc = ueps_eraser.nextConnComp()).isNull(); --ueps_todo) {
868 // Erase components until ueps_todo reaches zero or there are no more components.
869 }
870 if (ueps_todo) {
871 // Not enough ueps were found.
872 // qDebug() << "Not enough UEPs.";
873 continue;
874 }
875 }
876 // Write this block to the text mask.
877 rasterOp<RopOr<RopSrc, RopDst>>(text_mask, line_rect, cc_img, QPoint(0, top));
878 }
879 }
880
881 return text_mask;
882 } // ContentBoxFinder::estimateTextMask
883
trimLeft(const imageproc::BinaryImage & content,const imageproc::BinaryImage & content_blocks,const imageproc::BinaryImage & text,const QRect & area,Garbage & garbage,DebugImages * const dbg)884 QRect ContentBoxFinder::trimLeft(const imageproc::BinaryImage& content,
885 const imageproc::BinaryImage& content_blocks,
886 const imageproc::BinaryImage& text,
887 const QRect& area,
888 Garbage& garbage,
889 DebugImages* const dbg) {
890 const SlicedHistogram hist(content_blocks, area, SlicedHistogram::COLS);
891
892 size_t start = 0;
893 while (start < hist.size()) {
894 size_t first_ws = start;
895 for (; first_ws < hist.size() && hist[first_ws] != 0; ++first_ws) {
896 // Skip non-empty columns.
897 }
898 size_t first_non_ws = first_ws;
899 for (; first_non_ws < hist.size() && hist[first_non_ws] == 0; ++first_non_ws) {
900 // Skip empty columns.
901 }
902
903 first_ws += area.left();
904 first_non_ws += area.left();
905
906 QRect new_area(area);
907 new_area.setLeft(static_cast<int>(first_non_ws));
908 if (new_area.isEmpty()) {
909 return area;
910 }
911
912 QRect removed_area(area);
913 removed_area.setRight(static_cast<int>(first_ws - 1));
914 if (removed_area.isEmpty()) {
915 return new_area;
916 }
917
918 bool can_retry_grouped = false;
919 const QRect res
920 = trim(content, content_blocks, text, area, new_area, removed_area, garbage, can_retry_grouped, dbg);
921 if (can_retry_grouped) {
922 start = first_non_ws - area.left();
923 } else {
924 return res;
925 }
926 }
927
928 return area;
929 } // ContentBoxFinder::trimLeft
930
trimRight(const imageproc::BinaryImage & content,const imageproc::BinaryImage & content_blocks,const imageproc::BinaryImage & text,const QRect & area,Garbage & garbage,DebugImages * const dbg)931 QRect ContentBoxFinder::trimRight(const imageproc::BinaryImage& content,
932 const imageproc::BinaryImage& content_blocks,
933 const imageproc::BinaryImage& text,
934 const QRect& area,
935 Garbage& garbage,
936 DebugImages* const dbg) {
937 const SlicedHistogram hist(content_blocks, area, SlicedHistogram::COLS);
938
939 auto start = static_cast<int>(hist.size() - 1);
940 while (start >= 0) {
941 int first_ws = start;
942 for (; first_ws >= 0 && hist[first_ws] != 0; --first_ws) {
943 // Skip non-empty columns.
944 }
945 int first_non_ws = first_ws;
946 for (; first_non_ws >= 0 && hist[first_non_ws] == 0; --first_non_ws) {
947 // Skip empty columns.
948 }
949
950 first_ws += area.left();
951 first_non_ws += area.left();
952
953 QRect new_area(area);
954 new_area.setRight(first_non_ws);
955 if (new_area.isEmpty()) {
956 return area;
957 }
958
959 QRect removed_area(area);
960 removed_area.setLeft(first_ws + 1);
961 if (removed_area.isEmpty()) {
962 return new_area;
963 }
964
965 bool can_retry_grouped = false;
966 const QRect res
967 = trim(content, content_blocks, text, area, new_area, removed_area, garbage, can_retry_grouped, dbg);
968 if (can_retry_grouped) {
969 start = first_non_ws - area.left();
970 } else {
971 return res;
972 }
973 }
974
975 return area;
976 } // ContentBoxFinder::trimRight
977
trimTop(const imageproc::BinaryImage & content,const imageproc::BinaryImage & content_blocks,const imageproc::BinaryImage & text,const QRect & area,Garbage & garbage,DebugImages * const dbg)978 QRect ContentBoxFinder::trimTop(const imageproc::BinaryImage& content,
979 const imageproc::BinaryImage& content_blocks,
980 const imageproc::BinaryImage& text,
981 const QRect& area,
982 Garbage& garbage,
983 DebugImages* const dbg) {
984 const SlicedHistogram hist(content_blocks, area, SlicedHistogram::ROWS);
985
986 size_t start = 0;
987 while (start < hist.size()) {
988 size_t first_ws = start;
989 for (; first_ws < hist.size() && hist[first_ws] != 0; ++first_ws) {
990 // Skip non-empty columns.
991 }
992 size_t first_non_ws = first_ws;
993 for (; first_non_ws < hist.size() && hist[first_non_ws] == 0; ++first_non_ws) {
994 // Skip empty columns.
995 }
996
997 first_ws += area.top();
998 first_non_ws += area.top();
999
1000 QRect new_area(area);
1001 new_area.setTop(static_cast<int>(first_non_ws));
1002 if (new_area.isEmpty()) {
1003 return area;
1004 }
1005
1006 QRect removed_area(area);
1007 removed_area.setBottom(static_cast<int>(first_ws - 1));
1008 if (removed_area.isEmpty()) {
1009 return new_area;
1010 }
1011
1012 bool can_retry_grouped = false;
1013 const QRect res
1014 = trim(content, content_blocks, text, area, new_area, removed_area, garbage, can_retry_grouped, dbg);
1015 if (can_retry_grouped) {
1016 start = first_non_ws - area.top();
1017 } else {
1018 return res;
1019 }
1020 }
1021
1022 return area;
1023 } // ContentBoxFinder::trimTop
1024
trimBottom(const imageproc::BinaryImage & content,const imageproc::BinaryImage & content_blocks,const imageproc::BinaryImage & text,const QRect & area,Garbage & garbage,DebugImages * const dbg)1025 QRect ContentBoxFinder::trimBottom(const imageproc::BinaryImage& content,
1026 const imageproc::BinaryImage& content_blocks,
1027 const imageproc::BinaryImage& text,
1028 const QRect& area,
1029 Garbage& garbage,
1030 DebugImages* const dbg) {
1031 const SlicedHistogram hist(content_blocks, area, SlicedHistogram::ROWS);
1032
1033 auto start = static_cast<int>(hist.size() - 1);
1034 while (start >= 0) {
1035 int first_ws = start;
1036 for (; first_ws >= 0 && hist[first_ws] != 0; --first_ws) {
1037 // Skip non-empty columns.
1038 }
1039 int first_non_ws = first_ws;
1040 for (; first_non_ws >= 0 && hist[first_non_ws] == 0; --first_non_ws) {
1041 // Skip empty columns.
1042 }
1043
1044 first_ws += area.top();
1045 first_non_ws += area.top();
1046
1047 QRect new_area(area);
1048 new_area.setBottom(first_non_ws);
1049 if (new_area.isEmpty()) {
1050 return area;
1051 }
1052
1053 QRect removed_area(area);
1054 removed_area.setTop(first_ws + 1);
1055 if (removed_area.isEmpty()) {
1056 return new_area;
1057 }
1058
1059 bool can_retry_grouped = false;
1060 const QRect res
1061 = trim(content, content_blocks, text, area, new_area, removed_area, garbage, can_retry_grouped, dbg);
1062 if (can_retry_grouped) {
1063 start = first_non_ws - area.top();
1064 } else {
1065 return res;
1066 }
1067 }
1068
1069 return area;
1070 } // ContentBoxFinder::trimBottom
1071
trim(const imageproc::BinaryImage & content,const imageproc::BinaryImage & content_blocks,const imageproc::BinaryImage & text,const QRect & area,const QRect & new_area,const QRect & removed_area,Garbage & garbage,bool & can_retry_grouped,DebugImages * const dbg)1072 QRect ContentBoxFinder::trim(const imageproc::BinaryImage& content,
1073 const imageproc::BinaryImage& content_blocks,
1074 const imageproc::BinaryImage& text,
1075 const QRect& area,
1076 const QRect& new_area,
1077 const QRect& removed_area,
1078 Garbage& garbage,
1079 bool& can_retry_grouped,
1080 DebugImages* const dbg) {
1081 can_retry_grouped = false;
1082
1083 QImage visualized;
1084
1085 if (dbg) {
1086 visualized = QImage(content_blocks.size(), QImage::Format_ARGB32_Premultiplied);
1087 QPainter painter(&visualized);
1088 painter.drawImage(QPoint(0, 0), content_blocks.toQImage());
1089
1090 QPainterPath outer_path;
1091 outer_path.addRect(visualized.rect());
1092 QPainterPath inner_path;
1093 inner_path.addRect(area);
1094
1095 // Fill already rejected area with translucent gray.
1096 painter.setPen(Qt::NoPen);
1097 painter.setBrush(QColor(0x00, 0x00, 0x00, 50));
1098 painter.drawPath(outer_path.subtracted(inner_path));
1099 }
1100
1101 // Don't trim too much.
1102 while (removed_area.width() * removed_area.height() > 0.3 * (new_area.width() * new_area.height())) {
1103 // It's a loop just to be able to break from it.
1104
1105 // There is a special case when there is nothing but
1106 // garbage on the page. Let's try to handle it here.
1107 if ((removed_area.width() < 6) || (removed_area.height() < 6)) {
1108 break;
1109 }
1110
1111 if (dbg) {
1112 QPainter painter(&visualized);
1113 painter.setPen(Qt::NoPen);
1114 painter.setBrush(QColor(0x5f, 0xdf, 0x57, 50));
1115 painter.drawRect(removed_area);
1116 painter.drawRect(new_area);
1117 painter.end();
1118 dbg->add(visualized, "trim_too_much");
1119 }
1120
1121 return area;
1122 }
1123
1124 const int content_pixels = content.countBlackPixels(removed_area);
1125
1126 const bool vertical_cut = (new_area.top() == area.top() && new_area.bottom() == area.bottom());
1127 // qDebug() << "vertical cut: " << vertical_cut;
1128 // Ranged from 0.0 to 1.0. When it's less than 0.5, objects
1129 // are more likely to be considered as garbage. When it's
1130 // more than 0.5, objects are less likely to be considered
1131 // as garbage.
1132 double proximity_bias = vertical_cut ? 0.5 : 0.65;
1133
1134 const int num_text_pixels = text.countBlackPixels(removed_area);
1135 if (num_text_pixels == 0) {
1136 proximity_bias = vertical_cut ? 0.4 : 0.5;
1137 } else {
1138 int total_pixels = content_pixels;
1139 total_pixels += garbage.image().countBlackPixels(removed_area);
1140
1141 // qDebug() << "num_text_pixels = " << num_text_pixels;
1142 // qDebug() << "total_pixels = " << total_pixels;
1143 ++total_pixels; // just in case
1144 const double min_text_influence = 0.2;
1145 const double max_text_influence = 1.0;
1146 const int upper_threshold = 5000;
1147 double text_influence = max_text_influence;
1148 if (num_text_pixels < upper_threshold) {
1149 text_influence = min_text_influence
1150 + (max_text_influence - min_text_influence) * std::log((double) num_text_pixels)
1151 / std::log((double) upper_threshold);
1152 }
1153 // qDebug() << "text_influence = " << text_influence;
1154
1155 proximity_bias += (1.0 - proximity_bias) * text_influence * num_text_pixels / total_pixels;
1156 proximity_bias = qBound(0.0, proximity_bias, 1.0);
1157 }
1158
1159 BinaryImage remaining_content(content_blocks.size(), WHITE);
1160 rasterOp<RopSrc>(remaining_content, new_area, content, new_area.topLeft());
1161 rasterOp<RopAnd<RopSrc, RopDst>>(remaining_content, new_area, content_blocks, new_area.topLeft());
1162
1163 const SEDM dm_to_others(remaining_content, SEDM::DIST_TO_BLACK, SEDM::DIST_TO_NO_BORDERS);
1164 remaining_content.release();
1165
1166 double sum_dist_to_garbage = 0;
1167 double sum_dist_to_others = 0;
1168
1169 const uint32_t* cb_line = content_blocks.data();
1170 const int cb_stride = content_blocks.wordsPerLine();
1171 const uint32_t msb = uint32_t(1) << 31;
1172
1173 const uint32_t* dm_garbage_line = garbage.sedm().data();
1174 const uint32_t* dm_others_line = dm_to_others.data();
1175 const int dm_stride = dm_to_others.stride();
1176
1177 int count = 0;
1178 cb_line += cb_stride * removed_area.top();
1179 dm_garbage_line += dm_stride * removed_area.top();
1180 dm_others_line += dm_stride * removed_area.top();
1181 for (int y = removed_area.top(); y <= removed_area.bottom(); ++y) {
1182 for (int x = removed_area.left(); x <= removed_area.right(); ++x) {
1183 if (cb_line[x >> 5] & (msb >> (x & 31))) {
1184 sum_dist_to_garbage += std::sqrt((double) dm_garbage_line[x]);
1185 sum_dist_to_others += std::sqrt((double) dm_others_line[x]);
1186 ++count;
1187 }
1188 }
1189 cb_line += cb_stride;
1190 dm_garbage_line += dm_stride;
1191 dm_others_line += dm_stride;
1192 }
1193
1194 // qDebug() << "proximity_bias = " << proximity_bias;
1195 // qDebug() << "sum_dist_to_garbage = " << sum_dist_to_garbage;
1196 // qDebug() << "sum_dist_to_others = " << sum_dist_to_others;
1197 // qDebug() << "count = " << count;
1198
1199 sum_dist_to_garbage *= proximity_bias;
1200 sum_dist_to_others *= 1.0 - proximity_bias;
1201
1202 if (sum_dist_to_garbage < sum_dist_to_others) {
1203 garbage.add(content, removed_area);
1204 if (dbg) {
1205 QPainter painter(&visualized);
1206 painter.setPen(Qt::NoPen);
1207 painter.setBrush(QColor(0x5f, 0xdf, 0x57, 50));
1208 painter.drawRect(new_area);
1209 painter.setBrush(QColor(0xff, 0x20, 0x1e, 50));
1210 painter.drawRect(removed_area);
1211 painter.end();
1212 dbg->add(visualized, "trimmed");
1213 }
1214
1215 return new_area;
1216 } else {
1217 if (dbg) {
1218 QPainter painter(&visualized);
1219 painter.setPen(Qt::NoPen);
1220 painter.setBrush(QColor(0x5f, 0xdf, 0x57, 50));
1221 painter.drawRect(removed_area);
1222 painter.drawRect(new_area);
1223 painter.end();
1224 dbg->add(visualized, "not_trimmed");
1225 }
1226 can_retry_grouped = (proximity_bias < 0.85);
1227
1228 return area;
1229 }
1230 } // ContentBoxFinder::trim
1231
filterShadows(const TaskStatus & status,imageproc::BinaryImage & shadows,DebugImages * const dbg)1232 void ContentBoxFinder::filterShadows(const TaskStatus& status,
1233 imageproc::BinaryImage& shadows,
1234 DebugImages* const dbg) {
1235 // The input image should only contain shadows from the edges
1236 // of a page, but in practice it may also contain things like
1237 // a black table header which white letters on it. Here we
1238 // try to filter them out.
1239
1240 #if 1
1241 // Shadows that touch borders are genuine and should not be removed.
1242 BinaryImage borders(shadows.size(), WHITE);
1243 borders.fillExcept(borders.rect().adjusted(1, 1, -1, -1), BLACK);
1244
1245 BinaryImage touching_shadows(seedFill(borders, shadows, CONN8));
1246 rasterOp<RopXor<RopSrc, RopDst>>(shadows, touching_shadows);
1247 if (dbg) {
1248 dbg->add(shadows, "non_border_shadows");
1249 }
1250
1251 if (shadows.countBlackPixels()) {
1252 BinaryImage inv_shadows(shadows.inverted());
1253 BinaryImage mask(seedFill(borders, inv_shadows, CONN8));
1254 borders.release();
1255 rasterOp<RopOr<RopNot<RopDst>, RopSrc>>(mask, shadows);
1256 if (dbg) {
1257 dbg->add(mask, "shadows_no_holes");
1258 }
1259
1260 BinaryImage text_mask(estimateTextMask(inv_shadows, mask, dbg));
1261 inv_shadows.release();
1262 mask.release();
1263 text_mask = seedFill(text_mask, shadows, CONN8);
1264 if (dbg) {
1265 dbg->add(text_mask, "misclassified_shadows");
1266 }
1267 rasterOp<RopXor<RopSrc, RopDst>>(shadows, text_mask);
1268 }
1269
1270 rasterOp<RopOr<RopSrc, RopDst>>(shadows, touching_shadows);
1271 #else // if 1
1272 // White dots on black background may be a problem for us.
1273 // They may be misclassified as parts of white letters.
1274 BinaryImage reduced_dithering(closeBrick(shadows, QSize(1, 2), BLACK));
1275 reduced_dithering = closeBrick(reduced_dithering, QSize(2, 1), BLACK);
1276 if (dbg) {
1277 dbg->add(reduced_dithering, "reduced_dithering");
1278 }
1279
1280 status.throwIfCancelled();
1281
1282 // Long white vertical lines are definately not spaces between letters.
1283 BinaryImage vert_whitespace(closeBrick(reduced_dithering, QSize(1, 150), BLACK));
1284 if (dbg) {
1285 dbg->add(vert_whitespace, "vert_whitespace");
1286 }
1287
1288 status.throwIfCancelled();
1289
1290 // Join neighboring white letters.
1291 BinaryImage opened(openBrick(reduced_dithering, QSize(10, 4), BLACK));
1292 reduced_dithering.release();
1293 if (dbg) {
1294 dbg->add(opened, "opened");
1295 }
1296
1297 status.throwIfCancelled();
1298
1299 // Extract areas that became white as a result of the last operation.
1300 rasterOp<RopSubtract<RopNot<RopDst>, RopNot<RopSrc>>>(opened, shadows);
1301 if (dbg) {
1302 dbg->add(opened, "became white");
1303 }
1304
1305 status.throwIfCancelled();
1306 // Join the spacings between words together.
1307 BinaryImage closed(closeBrick(opened, QSize(20, 1), WHITE));
1308 opened.release();
1309 rasterOp<RopAnd<RopSrc, RopDst>>(closed, vert_whitespace);
1310 vert_whitespace.release();
1311 if (dbg) {
1312 dbg->add(closed, "closed");
1313 }
1314
1315 status.throwIfCancelled();
1316 // If we've got long enough and tall enough blocks, we assume they
1317 // are the text lines.
1318 opened = openBrick(closed, QSize(50, 10), WHITE);
1319 closed.release();
1320 if (dbg) {
1321 dbg->add(opened, "reopened");
1322 }
1323
1324 status.throwIfCancelled();
1325
1326 BinaryImage non_shadows(seedFill(opened, shadows, CONN8));
1327 opened.release();
1328 if (dbg) {
1329 dbg->add(non_shadows, "non_shadows");
1330 }
1331
1332 status.throwIfCancelled();
1333
1334 rasterOp<RopSubtract<RopDst, RopSrc>>(shadows, non_shadows);
1335 #endif // if 1
1336 } // ContentBoxFinder::filterShadows
1337
1338 /*====================== ContentBoxFinder::Garbage =====================*/
1339
Garbage(const Type type,const BinaryImage & garbage)1340 ContentBoxFinder::Garbage::Garbage(const Type type, const BinaryImage& garbage)
1341 : m_garbage(garbage),
1342 m_sedmBorders(type == VERT ? SEDM::DIST_TO_VERT_BORDERS : SEDM::DIST_TO_HOR_BORDERS),
1343 m_sedmUpdatePending(true) {}
1344
add(const BinaryImage & garbage,const QRect & rect)1345 void ContentBoxFinder::Garbage::add(const BinaryImage& garbage, const QRect& rect) {
1346 rasterOp<RopOr<RopSrc, RopDst>>(m_garbage, rect, garbage, rect.topLeft());
1347 m_sedmUpdatePending = true;
1348 }
1349
sedm()1350 const SEDM& ContentBoxFinder::Garbage::sedm() {
1351 if (m_sedmUpdatePending) {
1352 m_sedm = SEDM(m_garbage, SEDM::DIST_TO_BLACK, m_sedmBorders);
1353 }
1354
1355 return m_sedm;
1356 }
1357 } // namespace select_content
1358