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