#ifdef HAVE_CONFIG_H
# include "config_auto.h"
#endif
#include "strokewidth.h"
#include <algorithm>
#include <cmath>
#include "blobbox.h"
#include "colpartition.h"
#include "colpartitiongrid.h"
#include "helpers.h"
#include "imagefind.h"
#include "linlsq.h"
#include "statistc.h"
#include "tabfind.h"
#include "textlineprojection.h"
#include "tordmain.h"
namespace tesseract {
#ifndef GRAPHICS_DISABLED
static INT_VAR(textord_tabfind_show_strokewidths, 0, "Show stroke widths (ScrollView)");
#else
static INT_VAR(textord_tabfind_show_strokewidths, 0, "Show stroke widths");
#endif
static BOOL_VAR(textord_tabfind_only_strokewidths, false, "Only run stroke widths");
const double kStrokeWidthFractionTolerance = 0.125;
* Allowed constant change in stroke width to be the same font.
* Really 1.5 pixels.
*/
const double kStrokeWidthTolerance = 1.5;
const double kStrokeWidthFractionCJK = 0.25;
const double kStrokeWidthCJK = 2.0;
const int kCJKRadius = 2;
const double kCJKBrokenDistanceFraction = 0.25;
const int kCJKMaxComponents = 8;
const double kCJKAspectRatio = 1.25;
const double kCJKAspectRatioIncrease = 1.0625;
const int kMaxCJKSizeRatio = 5;
const double kBrokenCJKIterationFraction = 0.125;
const double kDiacriticXPadRatio = 7.0;
const double kDiacriticYPadRatio = 1.75;
const double kMinDiacriticSizeRatio = 1.0625;
const double kMaxDiacriticDistanceRatio = 1.25;
const double kMaxDiacriticGapToBaseCharHeight = 1.0;
const int kLineTrapLongest = 4;
const int kLineTrapShortest = 2;
const int kMostlyOneDirRatio = 3;
const double kLineResidueAspectRatio = 8.0;
const int kLineResiduePadRatio = 3;
const double kLineResidueSizeRatio = 1.75;
const float kSizeRatioToReject = 2.0;
const double kNeighbourSearchFactor = 2.5;
const double kNoiseOverlapGrowthFactor = 4.0;
const double kNoiseOverlapAreaFactor = 1.0 / 512;
StrokeWidth::StrokeWidth(int gridsize, const ICOORD &bleft, const ICOORD &tright)
: BlobGrid(gridsize, bleft, tright)
, nontext_map_(nullptr)
, projection_(nullptr)
, denorm_(nullptr)
, grid_box_(bleft, tright)
, rerotation_(1.0f, 0.0f) {
}
StrokeWidth::~StrokeWidth() {
#ifndef GRAPHICS_DISABLED
if (widths_win_ != nullptr) {
widths_win_->AwaitEvent(SVET_DESTROY);
if (textord_tabfind_only_strokewidths) {
exit(0);
}
delete widths_win_;
}
delete leaders_win_;
delete initial_widths_win_;
delete chains_win_;
delete textlines_win_;
delete smoothed_win_;
delete diacritics_win_;
#endif
}
void StrokeWidth::SetNeighboursOnMediumBlobs(TO_BLOCK *block) {
InsertBlobList(&block->blobs);
BLOBNBOX_IT blob_it(&block->blobs);
for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
SetNeighbours(false, false, blob_it.data());
}
Clear();
}
void StrokeWidth::FindTextlineDirectionAndFixBrokenCJK(PageSegMode pageseg_mode, bool cjk_merge,
TO_BLOCK *input_block) {
InsertBlobs(input_block);
while (cjk_merge && FixBrokenCJK(input_block)) {
}
FindTextlineFlowDirection(pageseg_mode, false);
Clear();
}
static void CollectHorizVertBlobs(BLOBNBOX_LIST *input_blobs, int *num_vertical_blobs,
int *num_horizontal_blobs, BLOBNBOX_CLIST *vertical_blobs,
BLOBNBOX_CLIST *horizontal_blobs,
BLOBNBOX_CLIST *nondescript_blobs) {
BLOBNBOX_C_IT v_it(vertical_blobs);
BLOBNBOX_C_IT h_it(horizontal_blobs);
BLOBNBOX_C_IT n_it(nondescript_blobs);
BLOBNBOX_IT blob_it(input_blobs);
for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
BLOBNBOX *blob = blob_it.data();
const TBOX &box = blob->bounding_box();
float y_x = static_cast<float>(box.height()) / box.width();
float x_y = 1.0f / y_x;
float ratio = x_y > y_x ? x_y : y_x;
bool ok_blob = ratio <= kSizeRatioToReject;
if (blob->UniquelyVertical()) {
++*num_vertical_blobs;
if (ok_blob) {
v_it.add_after_then_move(blob);
}
} else if (blob->UniquelyHorizontal()) {
++*num_horizontal_blobs;
if (ok_blob) {
h_it.add_after_then_move(blob);
}
} else if (ok_blob) {
n_it.add_after_then_move(blob);
}
}
}
bool StrokeWidth::TestVerticalTextDirection(double find_vertical_text_ratio, TO_BLOCK *block,
BLOBNBOX_CLIST *osd_blobs) {
int vertical_boxes = 0;
int horizontal_boxes = 0;
BLOBNBOX_CLIST vertical_blobs;
BLOBNBOX_CLIST horizontal_blobs;
BLOBNBOX_CLIST nondescript_blobs;
CollectHorizVertBlobs(&block->blobs, &vertical_boxes, &horizontal_boxes, &vertical_blobs,
&horizontal_blobs, &nondescript_blobs);
CollectHorizVertBlobs(&block->large_blobs, &vertical_boxes, &horizontal_boxes, &vertical_blobs,
&horizontal_blobs, &nondescript_blobs);
if (textord_debug_tabfind) {
tprintf("TextDir hbox=%d vs vbox=%d, %dH, %dV, %dN osd blobs\n", horizontal_boxes,
vertical_boxes, horizontal_blobs.length(), vertical_blobs.length(),
nondescript_blobs.length());
}
if (osd_blobs != nullptr && vertical_boxes == 0 && horizontal_boxes == 0) {
BLOBNBOX_C_IT osd_it(osd_blobs);
osd_it.add_list_after(&nondescript_blobs);
return false;
}
int min_vert_boxes =
static_cast<int>((vertical_boxes + horizontal_boxes) * find_vertical_text_ratio);
if (vertical_boxes >= min_vert_boxes) {
if (osd_blobs != nullptr) {
BLOBNBOX_C_IT osd_it(osd_blobs);
osd_it.add_list_after(&vertical_blobs);
}
return true;
} else {
if (osd_blobs != nullptr) {
BLOBNBOX_C_IT osd_it(osd_blobs);
osd_it.add_list_after(&horizontal_blobs);
}
return false;
}
}
void StrokeWidth::CorrectForRotation(const FCOORD &rotation, ColPartitionGrid *part_grid) {
Init(part_grid->gridsize(), part_grid->bleft(), part_grid->tright());
grid_box_ = TBOX(bleft(), tright());
rerotation_.set_x(rotation.x());
rerotation_.set_y(-rotation.y());
}
void StrokeWidth::FindLeaderPartitions(TO_BLOCK *block, ColPartitionGrid *part_grid) {
Clear();
ColPartition_LIST leader_parts;
FindLeadersAndMarkNoise(block, &leader_parts);
InsertBlobList(&block->blobs);
for (ColPartition_IT it(&leader_parts); !it.empty(); it.forward()) {
ColPartition *part = it.extract();
part->ClaimBoxes();
MarkLeaderNeighbours(part, LR_LEFT);
MarkLeaderNeighbours(part, LR_RIGHT);
part_grid->InsertBBox(true, true, part);
}
}
void StrokeWidth::RemoveLineResidue(ColPartition_LIST *big_part_list) {
BlobGridSearch gsearch(this);
BLOBNBOX *bbox;
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
TBOX box = bbox->bounding_box();
if (box.height() < box.width() * kLineResidueAspectRatio) {
continue;
}
int padding = box.height() * kLineResiduePadRatio;
TBOX search_box = box;
search_box.pad(padding, padding);
bool debug = AlignedBlob::WithinTestRegion(2, box.left(), box.bottom());
BlobGridSearch rsearch(this);
int max_height = 0;
BLOBNBOX *n;
rsearch.StartRectSearch(search_box);
while ((n = rsearch.NextRectSearch()) != nullptr) {
if (n == bbox) {
continue;
}
TBOX nbox = n->bounding_box();
if (nbox.height() > max_height) {
max_height = nbox.height();
}
}
if (debug) {
tprintf("Max neighbour size=%d for candidate line box at:", max_height);
box.print();
}
if (max_height * kLineResidueSizeRatio < box.height()) {
#ifndef GRAPHICS_DISABLED
if (leaders_win_ != nullptr) {
leaders_win_->Pen(ScrollView::PINK);
leaders_win_->Rectangle(box.left(), box.bottom(), box.right(), box.top());
}
#endif
ColPartition::MakeBigPartition(bbox, big_part_list);
}
}
}
void StrokeWidth::GradeBlobsIntoPartitions(PageSegMode pageseg_mode, const FCOORD &rerotation,
TO_BLOCK *block, Image nontext_pix, const DENORM *denorm,
bool cjk_script, TextlineProjection *projection,
BLOBNBOX_LIST *diacritic_blobs,
ColPartitionGrid *part_grid,
ColPartition_LIST *big_parts) {
nontext_map_ = nontext_pix;
projection_ = projection;
denorm_ = denorm;
Clear();
InsertBlobs(block);
if (cjk_script) {
FixBrokenCJK(block);
}
FindTextlineFlowDirection(pageseg_mode, false);
projection_->ConstructProjection(block, rerotation, nontext_map_);
#ifndef GRAPHICS_DISABLED
if (textord_tabfind_show_strokewidths) {
ScrollView *line_blobs_win = MakeWindow(0, 0, "Initial textline Blobs");
projection_->PlotGradedBlobs(&block->blobs, line_blobs_win);
projection_->PlotGradedBlobs(&block->small_blobs, line_blobs_win);
}
#endif
projection_->MoveNonTextlineBlobs(&block->blobs, &block->noise_blobs);
projection_->MoveNonTextlineBlobs(&block->small_blobs, &block->noise_blobs);
Clear();
InsertBlobs(block);
FCOORD skew;
FindTextlineFlowDirection(pageseg_mode, true);
PartitionFindResult r = FindInitialPartitions(pageseg_mode, rerotation, true, block,
diacritic_blobs, part_grid, big_parts, &skew);
if (r == PFR_NOISE) {
tprintf("Detected %d diacritics\n", diacritic_blobs->length());
Clear();
InsertBlobs(block);
FindTextlineFlowDirection(pageseg_mode, true);
r = FindInitialPartitions(pageseg_mode, rerotation, false, block, diacritic_blobs, part_grid,
big_parts, &skew);
}
nontext_map_ = nullptr;
projection_ = nullptr;
denorm_ = nullptr;
}
static void PrintBoxWidths(BLOBNBOX *neighbour) {
const TBOX &nbox = neighbour->bounding_box();
tprintf("Box (%d,%d)->(%d,%d): h-width=%.1f, v-width=%.1f p-width=%1.f\n", nbox.left(),
nbox.bottom(), nbox.right(), nbox.top(), neighbour->horz_stroke_width(),
neighbour->vert_stroke_width(),
2.0 * neighbour->cblob()->area() / neighbour->cblob()->perimeter());
}
void StrokeWidth::HandleClick(int x, int y) {
BBGrid<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT>::HandleClick(x, y);
BlobGridSearch radsearch(this);
radsearch.StartRadSearch(x, y, 1);
BLOBNBOX *neighbour;
FCOORD click(static_cast<float>(x), static_cast<float>(y));
while ((neighbour = radsearch.NextRadSearch()) != nullptr) {
TBOX nbox = neighbour->bounding_box();
if (nbox.contains(click) && neighbour->cblob() != nullptr) {
PrintBoxWidths(neighbour);
if (neighbour->neighbour(BND_LEFT) != nullptr) {
PrintBoxWidths(neighbour->neighbour(BND_LEFT));
}
if (neighbour->neighbour(BND_RIGHT) != nullptr) {
PrintBoxWidths(neighbour->neighbour(BND_RIGHT));
}
if (neighbour->neighbour(BND_ABOVE) != nullptr) {
PrintBoxWidths(neighbour->neighbour(BND_ABOVE));
}
if (neighbour->neighbour(BND_BELOW) != nullptr) {
PrintBoxWidths(neighbour->neighbour(BND_BELOW));
}
int gaps[BND_COUNT];
neighbour->NeighbourGaps(gaps);
tprintf(
"Left gap=%d, right=%d, above=%d, below=%d, horz=%d, vert=%d\n"
"Good= %d %d %d %d\n",
gaps[BND_LEFT], gaps[BND_RIGHT], gaps[BND_ABOVE], gaps[BND_BELOW],
neighbour->horz_possible(), neighbour->vert_possible(),
neighbour->good_stroke_neighbour(BND_LEFT), neighbour->good_stroke_neighbour(BND_RIGHT),
neighbour->good_stroke_neighbour(BND_ABOVE), neighbour->good_stroke_neighbour(BND_BELOW));
break;
}
}
}
void StrokeWidth::FindLeadersAndMarkNoise(TO_BLOCK *block, ColPartition_LIST *leader_parts) {
InsertBlobList(&block->small_blobs);
InsertBlobList(&block->noise_blobs);
BlobGridSearch gsearch(this);
BLOBNBOX *bbox;
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
SetNeighbours(true, false, bbox);
}
ColPartition_IT part_it(leader_parts);
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
if (bbox->flow() == BTFT_NONE) {
if (bbox->neighbour(BND_RIGHT) == nullptr && bbox->neighbour(BND_LEFT) == nullptr) {
continue;
}
auto *part = new ColPartition(BRT_UNKNOWN, ICOORD(0, 1));
BLOBNBOX *blob;
for (blob = bbox; blob != nullptr && blob->flow() == BTFT_NONE;
blob = blob->neighbour(BND_RIGHT)) {
part->AddBox(blob);
}
for (blob = bbox->neighbour(BND_LEFT); blob != nullptr && blob->flow() == BTFT_NONE;
blob = blob->neighbour(BND_LEFT)) {
part->AddBox(blob);
}
if (part->MarkAsLeaderIfMonospaced()) {
part_it.add_after_then_move(part);
} else {
delete part;
}
}
}
#ifndef GRAPHICS_DISABLED
if (textord_tabfind_show_strokewidths) {
leaders_win_ = DisplayGoodBlobs("LeaderNeighbours", 0, 0);
}
#endif
BLOBNBOX_IT blob_it(&block->blobs);
BLOBNBOX_IT small_it(&block->small_blobs);
for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) {
BLOBNBOX *blob = small_it.data();
if (blob->flow() != BTFT_LEADER) {
if (blob->flow() == BTFT_NEIGHBOURS) {
blob->set_flow(BTFT_NONE);
}
blob->ClearNeighbours();
blob_it.add_to_end(small_it.extract());
}
}
BLOBNBOX_IT noise_it(&block->noise_blobs);
for (noise_it.mark_cycle_pt(); !noise_it.cycled_list(); noise_it.forward()) {
BLOBNBOX *blob = noise_it.data();
if (blob->flow() == BTFT_LEADER || blob->joined_to_prev()) {
small_it.add_to_end(noise_it.extract());
} else if (blob->flow() == BTFT_NEIGHBOURS) {
blob->set_flow(BTFT_NONE);
blob->ClearNeighbours();
}
}
Clear();
}
* Blobs remain owned by the block. */
void StrokeWidth::InsertBlobs(TO_BLOCK *block) {
InsertBlobList(&block->blobs);
InsertBlobList(&block->large_blobs);
}
void StrokeWidth::MarkLeaderNeighbours(const ColPartition *part, LeftOrRight side) {
const TBOX &part_box = part->bounding_box();
BlobGridSearch blobsearch(this);
BLOBNBOX *best_blob = nullptr;
int best_gap = 0;
blobsearch.StartSideSearch(side == LR_LEFT ? part_box.left() : part_box.right(),
part_box.bottom(), part_box.top());
BLOBNBOX *blob;
while ((blob = blobsearch.NextSideSearch(side == LR_LEFT)) != nullptr) {
const TBOX &blob_box = blob->bounding_box();
if (!blob_box.y_overlap(part_box)) {
continue;
}
int x_gap = blob_box.x_gap(part_box);
if (x_gap > 2 * gridsize()) {
break;
} else if (best_blob == nullptr || x_gap < best_gap) {
best_blob = blob;
best_gap = x_gap;
}
}
if (best_blob != nullptr) {
if (side == LR_LEFT) {
best_blob->set_leader_on_right(true);
} else {
best_blob->set_leader_on_left(true);
}
#ifndef GRAPHICS_DISABLED
if (leaders_win_ != nullptr) {
leaders_win_->Pen(side == LR_LEFT ? ScrollView::RED : ScrollView::GREEN);
const TBOX &blob_box = best_blob->bounding_box();
leaders_win_->Rectangle(blob_box.left(), blob_box.bottom(), blob_box.right(), blob_box.top());
}
#endif
}
}
static int UpperQuartileCJKSize(int gridsize, BLOBNBOX_LIST *blobs) {
STATS sizes(0, gridsize * kMaxCJKSizeRatio - 1);
BLOBNBOX_IT it(blobs);
for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
BLOBNBOX *blob = it.data();
int width = blob->bounding_box().width();
int height = blob->bounding_box().height();
if (width <= height * kCJKAspectRatio && height < width * kCJKAspectRatio) {
sizes.add(height, 1);
}
}
return static_cast<int>(sizes.ile(0.75f) + 0.5);
}
bool StrokeWidth::FixBrokenCJK(TO_BLOCK *block) {
BLOBNBOX_LIST *blobs = &block->blobs;
int median_height = UpperQuartileCJKSize(gridsize(), blobs);
int max_dist = static_cast<int>(median_height * kCJKBrokenDistanceFraction);
int max_height = static_cast<int>(median_height * kCJKAspectRatio);
int num_fixed = 0;
BLOBNBOX_IT blob_it(blobs);
for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
BLOBNBOX *blob = blob_it.data();
if (blob->cblob() == nullptr || blob->cblob()->out_list()->empty()) {
continue;
}
TBOX bbox = blob->bounding_box();
bool debug = AlignedBlob::WithinTestRegion(3, bbox.left(), bbox.bottom());
if (debug) {
tprintf("Checking for Broken CJK (max size=%d):", max_height);
bbox.print();
}
BLOBNBOX_CLIST overlapped_blobs;
AccumulateOverlaps(blob, debug, max_height, max_dist, &bbox, &overlapped_blobs);
if (!overlapped_blobs.empty()) {
if (bbox.width() > bbox.height() * kCJKAspectRatio ||
bbox.height() > bbox.width() * kCJKAspectRatio) {
if (debug) {
tprintf("Bad final aspectratio:");
bbox.print();
}
continue;
}
if (overlapped_blobs.length() >= kCJKMaxComponents) {
if (debug) {
tprintf("Too many neighbours: %d\n", overlapped_blobs.length());
}
continue;
}
BLOBNBOX_C_IT n_it(&overlapped_blobs);
for (n_it.mark_cycle_pt(); !n_it.cycled_list(); n_it.forward()) {
BLOBNBOX *neighbour = nullptr;
neighbour = n_it.data();
if (!blob->MatchingStrokeWidth(*neighbour, kStrokeWidthFractionCJK, kStrokeWidthCJK)) {
break;
}
}
if (!n_it.cycled_list()) {
if (debug) {
tprintf("Bad stroke widths:");
PrintBoxWidths(blob);
}
continue;
}
RemoveBBox(blob);
for (n_it.mark_cycle_pt(); !n_it.cycled_list(); n_it.forward()) {
BLOBNBOX *neighbour = n_it.data();
RemoveBBox(neighbour);
neighbour->set_region_type(BRT_NOISE);
blob->really_merge(neighbour);
if (rerotation_.x() != 1.0f || rerotation_.y() != 0.0f) {
blob->rotate_box(rerotation_);
}
}
InsertBBox(true, true, blob);
++num_fixed;
if (debug) {
tprintf("Done! Final box:");
bbox.print();
}
}
}
int num_remaining = 0;
for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
BLOBNBOX *blob = blob_it.data();
if (blob->cblob() != nullptr && !blob->cblob()->out_list()->empty()) {
++num_remaining;
}
}
block->DeleteUnownedNoise();
return num_fixed > num_remaining * kBrokenCJKIterationFraction;
}
static bool AcceptableCJKMerge(const TBOX &bbox, const TBOX &nbox, bool debug, int max_size,
int max_dist, int *x_gap, int *y_gap) {
*x_gap = bbox.x_gap(nbox);
*y_gap = bbox.y_gap(nbox);
TBOX merged(nbox);
merged += bbox;
if (debug) {
tprintf("gaps = %d, %d, merged_box:", *x_gap, *y_gap);
merged.print();
}
if (*x_gap <= max_dist && *y_gap <= max_dist && merged.width() <= max_size &&
merged.height() <= max_size) {
double old_ratio = static_cast<double>(bbox.width()) / bbox.height();
if (old_ratio < 1.0) {
old_ratio = 1.0 / old_ratio;
}
double new_ratio = static_cast<double>(merged.width()) / merged.height();
if (new_ratio < 1.0) {
new_ratio = 1.0 / new_ratio;
}
if (new_ratio <= old_ratio * kCJKAspectRatioIncrease) {
return true;
}
}
return false;
}
void StrokeWidth::AccumulateOverlaps(const BLOBNBOX *not_this, bool debug, int max_size,
int max_dist, TBOX *bbox, BLOBNBOX_CLIST *blobs) {
BLOBNBOX *nearests[BND_COUNT];
for (auto &nearest : nearests) {
nearest = nullptr;
}
int x = (bbox->left() + bbox->right()) / 2;
int y = (bbox->bottom() + bbox->top()) / 2;
BlobGridSearch radsearch(this);
radsearch.StartRadSearch(x, y, kCJKRadius);
BLOBNBOX *neighbour;
while ((neighbour = radsearch.NextRadSearch()) != nullptr) {
if (neighbour == not_this) {
continue;
}
TBOX nbox = neighbour->bounding_box();
int x_gap, y_gap;
if (AcceptableCJKMerge(*bbox, nbox, debug, max_size, max_dist, &x_gap, &y_gap)) {
*bbox += nbox;
blobs->add_sorted(SortByBoxLeft<BLOBNBOX>, true, neighbour);
if (debug) {
tprintf("Added:");
nbox.print();
}
for (int dir = 0; dir < BND_COUNT; ++dir) {
if (nearests[dir] == nullptr) {
continue;
}
nbox = nearests[dir]->bounding_box();
if (AcceptableCJKMerge(*bbox, nbox, debug, max_size, max_dist, &x_gap, &y_gap)) {
*bbox += nbox;
blobs->add_sorted(SortByBoxLeft<BLOBNBOX>, true, nearests[dir]);
if (debug) {
tprintf("Added:");
nbox.print();
}
nearests[dir] = nullptr;
dir = -1;
}
}
} else if (x_gap < 0 && x_gap <= y_gap) {
BlobNeighbourDir dir = nbox.top() > bbox->top() ? BND_ABOVE : BND_BELOW;
if (nearests[dir] == nullptr || y_gap < bbox->y_gap(nearests[dir]->bounding_box())) {
nearests[dir] = neighbour;
}
} else if (y_gap < 0 && y_gap <= x_gap) {
BlobNeighbourDir dir = nbox.left() > bbox->left() ? BND_RIGHT : BND_LEFT;
if (nearests[dir] == nullptr || x_gap < bbox->x_gap(nearests[dir]->bounding_box())) {
nearests[dir] = neighbour;
}
}
if (nearests[BND_LEFT] && nearests[BND_RIGHT] && nearests[BND_ABOVE] && nearests[BND_BELOW]) {
break;
}
}
for (auto &nearest : nearests) {
if (nearest == nullptr) {
continue;
}
const TBOX &nbox = nearest->bounding_box();
if (debug) {
tprintf("Testing for overlap with:");
nbox.print();
}
if (bbox->overlap(nbox)) {
blobs->shallow_clear();
if (debug) {
tprintf("Final box overlaps nearest\n");
}
return;
}
}
}
void StrokeWidth::FindTextlineFlowDirection(PageSegMode pageseg_mode, bool display_if_debugging) {
BlobGridSearch gsearch(this);
BLOBNBOX *bbox;
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
SetNeighbours(false, display_if_debugging, bbox);
}
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
SimplifyObviousNeighbours(bbox);
}
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
if (FindingVerticalOnly(pageseg_mode)) {
bbox->set_vert_possible(true);
bbox->set_horz_possible(false);
} else if (FindingHorizontalOnly(pageseg_mode)) {
bbox->set_vert_possible(false);
bbox->set_horz_possible(true);
} else {
SetNeighbourFlows(bbox);
}
}
#ifndef GRAPHICS_DISABLED
if ((textord_tabfind_show_strokewidths && display_if_debugging) ||
textord_tabfind_show_strokewidths > 1) {
initial_widths_win_ = DisplayGoodBlobs("InitialStrokewidths", 400, 0);
}
#endif
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
SmoothNeighbourTypes(pageseg_mode, false, bbox);
}
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
SmoothNeighbourTypes(pageseg_mode, true, bbox);
}
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
SmoothNeighbourTypes(pageseg_mode, true, bbox);
}
#ifndef GRAPHICS_DISABLED
if ((textord_tabfind_show_strokewidths && display_if_debugging) ||
textord_tabfind_show_strokewidths > 1) {
widths_win_ = DisplayGoodBlobs("ImprovedStrokewidths", 800, 0);
}
#endif
}
void StrokeWidth::SetNeighbours(bool leaders, bool activate_line_trap, BLOBNBOX *blob) {
int line_trap_count = 0;
for (int dir = 0; dir < BND_COUNT; ++dir) {
auto bnd = static_cast<BlobNeighbourDir>(dir);
line_trap_count += FindGoodNeighbour(bnd, leaders, blob);
}
if (line_trap_count > 0 && activate_line_trap) {
blob->ClearNeighbours();
const TBOX &box = blob->bounding_box();
blob->set_region_type(box.width() > box.height() ? BRT_HLINE : BRT_VLINE);
}
}
int StrokeWidth::FindGoodNeighbour(BlobNeighbourDir dir, bool leaders, BLOBNBOX *blob) {
TBOX blob_box = blob->bounding_box();
bool debug = AlignedBlob::WithinTestRegion(2, blob_box.left(), blob_box.bottom());
if (debug) {
tprintf("FGN in dir %d for blob:", dir);
blob_box.print();
}
int top = blob_box.top();
int bottom = blob_box.bottom();
int left = blob_box.left();
int right = blob_box.right();
int width = right - left;
int height = top - bottom;
int line_trap_max = std::max(width, height) / kLineTrapLongest;
int line_trap_min = std::min(width, height) * kLineTrapShortest;
int line_trap_count = 0;
int min_good_overlap = (dir == BND_LEFT || dir == BND_RIGHT) ? height / 2 : width / 2;
int min_decent_overlap = (dir == BND_LEFT || dir == BND_RIGHT) ? height / 3 : width / 3;
if (leaders) {
min_good_overlap = min_decent_overlap = 1;
}
int search_pad =
static_cast<int>(sqrt(static_cast<double>(width * height)) * kNeighbourSearchFactor);
if (gridsize() > search_pad) {
search_pad = gridsize();
}
TBOX search_box = blob_box;
switch (dir) {
case BND_LEFT:
search_box.set_left(search_box.left() - search_pad);
break;
case BND_RIGHT:
search_box.set_right(search_box.right() + search_pad);
break;
case BND_BELOW:
search_box.set_bottom(search_box.bottom() - search_pad);
break;
case BND_ABOVE:
search_box.set_top(search_box.top() + search_pad);
break;
case BND_COUNT:
return 0;
}
BlobGridSearch rectsearch(this);
rectsearch.StartRectSearch(search_box);
BLOBNBOX *best_neighbour = nullptr;
double best_goodness = 0.0;
bool best_is_good = false;
BLOBNBOX *neighbour;
while ((neighbour = rectsearch.NextRectSearch()) != nullptr) {
TBOX nbox = neighbour->bounding_box();
if (neighbour == blob) {
continue;
}
int mid_x = (nbox.left() + nbox.right()) / 2;
if (mid_x < blob->left_rule() || mid_x > blob->right_rule()) {
continue;
}
if (debug) {
tprintf("Neighbour at:");
nbox.print();
}
int n_width = nbox.width();
int n_height = nbox.height();
if (std::min(n_width, n_height) > line_trap_min &&
std::max(n_width, n_height) < line_trap_max) {
++line_trap_count;
}
if (TabFind::VeryDifferentSizes(std::max(n_width, n_height), std::max(width, height)) &&
(((dir == BND_LEFT || dir == BND_RIGHT) && TabFind::DifferentSizes(n_height, height)) ||
((dir == BND_BELOW || dir == BND_ABOVE) && TabFind::DifferentSizes(n_width, width)))) {
if (debug) {
tprintf("Bad size\n");
}
continue;
}
int overlap;
int perp_overlap;
int gap;
if (dir == BND_LEFT || dir == BND_RIGHT) {
overlap = std::min(static_cast<int>(nbox.top()), top) -
std::max(static_cast<int>(nbox.bottom()), bottom);
if (overlap == nbox.height() && nbox.width() > nbox.height()) {
perp_overlap = nbox.width();
} else {
perp_overlap = overlap;
}
gap = dir == BND_LEFT ? left - nbox.left() : nbox.right() - right;
if (gap <= 0) {
if (debug) {
tprintf("On wrong side\n");
}
continue;
}
gap -= n_width;
} else {
overlap = std::min(static_cast<int>(nbox.right()), right) -
std::max(static_cast<int>(nbox.left()), left);
if (overlap == nbox.width() && nbox.height() > nbox.width()) {
perp_overlap = nbox.height();
} else {
perp_overlap = overlap;
}
gap = dir == BND_BELOW ? bottom - nbox.bottom() : nbox.top() - top;
if (gap <= 0) {
if (debug) {
tprintf("On wrong side\n");
}
continue;
}
gap -= n_height;
}
if (-gap > overlap) {
if (debug) {
tprintf("Overlaps wrong way\n");
}
continue;
}
if (perp_overlap < min_decent_overlap) {
if (debug) {
tprintf("Doesn't overlap enough\n");
}
continue;
}
bool bad_sizes =
TabFind::DifferentSizes(height, n_height) && TabFind::DifferentSizes(width, n_width);
bool is_good =
overlap >= min_good_overlap && !bad_sizes &&
blob->MatchingStrokeWidth(*neighbour, kStrokeWidthFractionTolerance, kStrokeWidthTolerance);
if (gap < 1) {
gap = 1;
}
double goodness = (1.0 + is_good) * overlap / gap;
if (debug) {
tprintf("goodness = %g vs best of %g, good=%d, overlap=%d, gap=%d\n", goodness, best_goodness,
is_good, overlap, gap);
}
if (goodness > best_goodness) {
best_neighbour = neighbour;
best_goodness = goodness;
best_is_good = is_good;
}
}
blob->set_neighbour(dir, best_neighbour, best_is_good);
return line_trap_count;
}
static void ListNeighbours(const BLOBNBOX *blob, BLOBNBOX_CLIST *neighbours) {
for (int dir = 0; dir < BND_COUNT; ++dir) {
auto bnd = static_cast<BlobNeighbourDir>(dir);
BLOBNBOX *neighbour = blob->neighbour(bnd);
if (neighbour != nullptr) {
neighbours->add_sorted(SortByBoxLeft<BLOBNBOX>, true, neighbour);
}
}
}
static void List2ndNeighbours(const BLOBNBOX *blob, BLOBNBOX_CLIST *neighbours) {
ListNeighbours(blob, neighbours);
for (int dir = 0; dir < BND_COUNT; ++dir) {
auto bnd = static_cast<BlobNeighbourDir>(dir);
BLOBNBOX *neighbour = blob->neighbour(bnd);
if (neighbour != nullptr) {
ListNeighbours(neighbour, neighbours);
}
}
}
static void List3rdNeighbours(const BLOBNBOX *blob, BLOBNBOX_CLIST *neighbours) {
List2ndNeighbours(blob, neighbours);
for (int dir = 0; dir < BND_COUNT; ++dir) {
auto bnd = static_cast<BlobNeighbourDir>(dir);
BLOBNBOX *neighbour = blob->neighbour(bnd);
if (neighbour != nullptr) {
List2ndNeighbours(neighbour, neighbours);
}
}
}
static void CountNeighbourGaps(bool debug, BLOBNBOX_CLIST *neighbours, int *pure_h_count,
int *pure_v_count) {
if (neighbours->length() <= kMostlyOneDirRatio) {
return;
}
BLOBNBOX_C_IT it(neighbours);
for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
BLOBNBOX *blob = it.data();
int h_min, h_max, v_min, v_max;
blob->MinMaxGapsClipped(&h_min, &h_max, &v_min, &v_max);
if (debug) {
tprintf("Hgaps [%d,%d], vgaps [%d,%d]:", h_min, h_max, v_min, v_max);
}
if (h_max < v_min || blob->leader_on_left() || blob->leader_on_right()) {
++*pure_h_count;
if (debug) {
tprintf("Horz at:");
}
} else if (v_max < h_min) {
++*pure_v_count;
if (debug) {
tprintf("Vert at:");
}
} else {
if (debug) {
tprintf("Neither at:");
}
}
if (debug) {
blob->bounding_box().print();
}
}
}
void StrokeWidth::SetNeighbourFlows(BLOBNBOX *blob) {
if (blob->DefiniteIndividualFlow()) {
return;
}
bool debug =
AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(), blob->bounding_box().bottom());
if (debug) {
tprintf("SetNeighbourFlows (current flow=%d, type=%d) on:", blob->flow(), blob->region_type());
blob->bounding_box().print();
}
BLOBNBOX_CLIST neighbours;
List3rdNeighbours(blob, &neighbours);
int pure_h_count = 0;
int pure_v_count = 0;
CountNeighbourGaps(debug, &neighbours, &pure_h_count, &pure_v_count);
if (debug) {
HandleClick(blob->bounding_box().left() + 1, blob->bounding_box().bottom() + 1);
tprintf("SetFlows: h_count=%d, v_count=%d\n", pure_h_count, pure_v_count);
}
if (!neighbours.empty()) {
blob->set_vert_possible(true);
blob->set_horz_possible(true);
if (pure_h_count > 2 * pure_v_count) {
blob->set_vert_possible(false);
} else if (pure_v_count > 2 * pure_h_count) {
blob->set_horz_possible(false);
}
} else {
blob->set_vert_possible(false);
blob->set_horz_possible(false);
}
}
static void CountNeighbourTypes(BLOBNBOX_CLIST *neighbours, int *pure_h_count, int *pure_v_count) {
BLOBNBOX_C_IT it(neighbours);
for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
BLOBNBOX *blob = it.data();
if (blob->UniquelyHorizontal()) {
++*pure_h_count;
}
if (blob->UniquelyVertical()) {
++*pure_v_count;
}
}
}
void StrokeWidth::SimplifyObviousNeighbours(BLOBNBOX *blob) {
if ((blob->bounding_box().width() > 3 * blob->area_stroke_width() &&
blob->bounding_box().height() > 3 * blob->area_stroke_width())) {
if (blob->bounding_box().width() > 4 * blob->bounding_box().height()) {
blob->set_neighbour(BND_ABOVE, nullptr, false);
blob->set_neighbour(BND_BELOW, nullptr, false);
return;
}
if (blob->bounding_box().height() > 4 * blob->bounding_box().width()) {
blob->set_neighbour(BND_LEFT, nullptr, false);
blob->set_neighbour(BND_RIGHT, nullptr, false);
return;
}
}
int margin = gridsize() / 2;
int h_min, h_max, v_min, v_max;
blob->MinMaxGapsClipped(&h_min, &h_max, &v_min, &v_max);
if ((h_max + margin < v_min && h_max < margin / 2) || blob->leader_on_left() ||
blob->leader_on_right()) {
blob->set_neighbour(BND_ABOVE, nullptr, false);
blob->set_neighbour(BND_BELOW, nullptr, false);
} else if (v_max + margin < h_min && v_max < margin / 2) {
blob->set_neighbour(BND_LEFT, nullptr, false);
blob->set_neighbour(BND_RIGHT, nullptr, false);
}
}
void StrokeWidth::SmoothNeighbourTypes(PageSegMode pageseg_mode, bool reset_all, BLOBNBOX *blob) {
if ((blob->vert_possible() && blob->horz_possible()) || reset_all) {
BLOBNBOX_CLIST neighbours;
List2ndNeighbours(blob, &neighbours);
int pure_h_count = 0;
int pure_v_count = 0;
CountNeighbourTypes(&neighbours, &pure_h_count, &pure_v_count);
if (AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(),
blob->bounding_box().bottom())) {
HandleClick(blob->bounding_box().left() + 1, blob->bounding_box().bottom() + 1);
tprintf("pure_h=%d, pure_v=%d\n", pure_h_count, pure_v_count);
}
if (pure_h_count > pure_v_count && !FindingVerticalOnly(pageseg_mode)) {
blob->set_vert_possible(false);
blob->set_horz_possible(true);
} else if (pure_v_count > pure_h_count && !FindingHorizontalOnly(pageseg_mode)) {
blob->set_horz_possible(false);
blob->set_vert_possible(true);
}
} else if (AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(),
blob->bounding_box().bottom())) {
HandleClick(blob->bounding_box().left() + 1, blob->bounding_box().bottom() + 1);
tprintf("Clean on pass 3!\n");
}
}
PartitionFindResult StrokeWidth::FindInitialPartitions(
PageSegMode pageseg_mode, const FCOORD &rerotation, bool find_problems, TO_BLOCK *block,
BLOBNBOX_LIST *diacritic_blobs, ColPartitionGrid *part_grid, ColPartition_LIST *big_parts,
FCOORD *skew_angle) {
if (!FindingHorizontalOnly(pageseg_mode)) {
FindVerticalTextChains(part_grid);
}
if (!FindingVerticalOnly(pageseg_mode)) {
FindHorizontalTextChains(part_grid);
}
#ifndef GRAPHICS_DISABLED
if (textord_tabfind_show_strokewidths) {
chains_win_ = MakeWindow(0, 400, "Initial text chains");
part_grid->DisplayBoxes(chains_win_);
projection_->DisplayProjection();
}
#endif
if (find_problems) {
}
part_grid->SplitOverlappingPartitions(big_parts);
EasyMerges(part_grid);
RemoveLargeUnusedBlobs(block, part_grid, big_parts);
TBOX grid_box(bleft(), tright());
while (part_grid->GridSmoothNeighbours(BTFT_CHAIN, nontext_map_, grid_box, rerotation)) {
;
}
while (part_grid->GridSmoothNeighbours(BTFT_NEIGHBOURS, nontext_map_, grid_box, rerotation)) {
;
}
int pre_overlap = part_grid->ComputeTotalOverlap(nullptr);
TestDiacritics(part_grid, block);
MergeDiacritics(block, part_grid);
if (find_problems && diacritic_blobs != nullptr &&
DetectAndRemoveNoise(pre_overlap, grid_box, block, part_grid, diacritic_blobs)) {
return PFR_NOISE;
}
#ifndef GRAPHICS_DISABLED
if (textord_tabfind_show_strokewidths) {
textlines_win_ = MakeWindow(400, 400, "GoodTextline blobs");
part_grid->DisplayBoxes(textlines_win_);
diacritics_win_ = DisplayDiacritics("Diacritics", 0, 0, block);
}
#endif
PartitionRemainingBlobs(pageseg_mode, part_grid);
part_grid->SplitOverlappingPartitions(big_parts);
EasyMerges(part_grid);
while (part_grid->GridSmoothNeighbours(BTFT_CHAIN, nontext_map_, grid_box, rerotation)) {
;
}
while (part_grid->GridSmoothNeighbours(BTFT_NEIGHBOURS, nontext_map_, grid_box, rerotation)) {
;
}
while (part_grid->GridSmoothNeighbours(BTFT_STRONG_CHAIN, nontext_map_, grid_box, rerotation)) {
;
}
#ifndef GRAPHICS_DISABLED
if (textord_tabfind_show_strokewidths) {
smoothed_win_ = MakeWindow(800, 400, "Smoothed blobs");
part_grid->DisplayBoxes(smoothed_win_);
}
#endif
return PFR_OK;
}
bool StrokeWidth::DetectAndRemoveNoise(int pre_overlap, const TBOX &grid_box, TO_BLOCK *block,
ColPartitionGrid *part_grid,
BLOBNBOX_LIST *diacritic_blobs) {
ColPartitionGrid *noise_grid = nullptr;
int post_overlap = part_grid->ComputeTotalOverlap(&noise_grid);
if (pre_overlap == 0) {
pre_overlap = 1;
}
BLOBNBOX_IT diacritic_it(diacritic_blobs);
if (noise_grid != nullptr) {
if (post_overlap > pre_overlap * kNoiseOverlapGrowthFactor &&
post_overlap > grid_box.area() * kNoiseOverlapAreaFactor) {
#ifndef GRAPHICS_DISABLED
if (textord_tabfind_show_strokewidths) {
ScrollView *noise_win = MakeWindow(1000, 500, "Noise Areas");
noise_grid->DisplayBoxes(noise_win);
}
#endif
part_grid->DeleteNonLeaderParts();
BLOBNBOX_IT blob_it(&block->noise_blobs);
ColPartitionGridSearch rsearch(noise_grid);
for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
BLOBNBOX *blob = blob_it.data();
blob->ClearNeighbours();
if (!blob->IsDiacritic() || blob->owner() != nullptr) {
continue;
}
TBOX search_box(blob->bounding_box());
search_box.pad(gridsize(), gridsize());
rsearch.StartRectSearch(search_box);
ColPartition *part = rsearch.NextRectSearch();
if (part != nullptr) {
blob->set_owns_cblob(true);
blob->compute_bounding_box();
diacritic_it.add_after_then_move(blob_it.extract());
}
}
noise_grid->DeleteParts();
delete noise_grid;
return true;
}
noise_grid->DeleteParts();
delete noise_grid;
}
return false;
}
static BLOBNBOX *MutualUnusedVNeighbour(const BLOBNBOX *blob, BlobNeighbourDir dir) {
BLOBNBOX *next_blob = blob->neighbour(dir);
if (next_blob == nullptr || next_blob->owner() != nullptr || next_blob->UniquelyHorizontal()) {
return nullptr;
}
if (next_blob->neighbour(DirOtherWay(dir)) == blob) {
return next_blob;
}
return nullptr;
}
void StrokeWidth::FindVerticalTextChains(ColPartitionGrid *part_grid) {
PageSegMode pageseg_mode =
rerotation_.y() == 0.0f ? PSM_SINGLE_BLOCK_VERT_TEXT : PSM_SINGLE_COLUMN;
BlobGridSearch gsearch(this);
BLOBNBOX *bbox;
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
BLOBNBOX *blob;
if (bbox->owner() == nullptr && bbox->UniquelyVertical() &&
(blob = MutualUnusedVNeighbour(bbox, BND_ABOVE)) != nullptr) {
auto *part = new ColPartition(BRT_VERT_TEXT, ICOORD(0, 1));
part->AddBox(bbox);
while (blob != nullptr) {
part->AddBox(blob);
blob = MutualUnusedVNeighbour(blob, BND_ABOVE);
}
blob = MutualUnusedVNeighbour(bbox, BND_BELOW);
while (blob != nullptr) {
part->AddBox(blob);
blob = MutualUnusedVNeighbour(blob, BND_BELOW);
}
CompletePartition(pageseg_mode, part, part_grid);
}
}
}
static BLOBNBOX *MutualUnusedHNeighbour(const BLOBNBOX *blob, BlobNeighbourDir dir) {
BLOBNBOX *next_blob = blob->neighbour(dir);
if (next_blob == nullptr || next_blob->owner() != nullptr || next_blob->UniquelyVertical()) {
return nullptr;
}
if (next_blob->neighbour(DirOtherWay(dir)) == blob) {
return next_blob;
}
return nullptr;
}
void StrokeWidth::FindHorizontalTextChains(ColPartitionGrid *part_grid) {
PageSegMode pageseg_mode =
rerotation_.y() == 0.0f ? PSM_SINGLE_COLUMN : PSM_SINGLE_BLOCK_VERT_TEXT;
BlobGridSearch gsearch(this);
BLOBNBOX *bbox;
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
BLOBNBOX *blob;
if (bbox->owner() == nullptr && bbox->UniquelyHorizontal() &&
(blob = MutualUnusedHNeighbour(bbox, BND_RIGHT)) != nullptr) {
auto *part = new ColPartition(BRT_TEXT, ICOORD(0, 1));
part->AddBox(bbox);
while (blob != nullptr) {
part->AddBox(blob);
blob = MutualUnusedHNeighbour(blob, BND_RIGHT);
}
blob = MutualUnusedHNeighbour(bbox, BND_LEFT);
while (blob != nullptr) {
part->AddBox(blob);
blob = MutualUnusedVNeighbour(blob, BND_LEFT);
}
CompletePartition(pageseg_mode, part, part_grid);
}
}
}
void StrokeWidth::TestDiacritics(ColPartitionGrid *part_grid, TO_BLOCK *block) {
BlobGrid small_grid(gridsize(), bleft(), tright());
small_grid.InsertBlobList(&block->noise_blobs);
small_grid.InsertBlobList(&block->blobs);
int medium_diacritics = 0;
int small_diacritics = 0;
BLOBNBOX_IT small_it(&block->noise_blobs);
for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) {
BLOBNBOX *blob = small_it.data();
if (blob->owner() == nullptr && !blob->IsDiacritic() && DiacriticBlob(&small_grid, blob)) {
++small_diacritics;
}
}
BLOBNBOX_IT blob_it(&block->blobs);
for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
BLOBNBOX *blob = blob_it.data();
if (blob->IsDiacritic()) {
small_it.add_to_end(blob_it.extract());
continue;
}
ColPartition *part = blob->owner();
if (part == nullptr && DiacriticBlob(&small_grid, blob)) {
++medium_diacritics;
RemoveBBox(blob);
small_it.add_to_end(blob_it.extract());
} else if (part != nullptr && !part->block_owned() && part->boxes_count() < 3) {
BLOBNBOX_C_IT box_it(part->boxes());
for (box_it.mark_cycle_pt();
!box_it.cycled_list() && DiacriticBlob(&small_grid, box_it.data()); box_it.forward()) {
;
}
if (box_it.cycled_list()) {
while (!box_it.empty()) {
BLOBNBOX *box = box_it.extract();
box->set_owner(nullptr);
box_it.forward();
++medium_diacritics;
RemoveBBox(box);
}
small_it.add_to_end(blob_it.extract());
part_grid->RemoveBBox(part);
delete part;
}
} else if (AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(),
blob->bounding_box().bottom())) {
tprintf("Blob not available to be a diacritic at:");
blob->bounding_box().print();
}
}
if (textord_tabfind_show_strokewidths) {
tprintf("Found %d small diacritics, %d medium\n", small_diacritics, medium_diacritics);
}
}
bool StrokeWidth::DiacriticBlob(BlobGrid *small_grid, BLOBNBOX *blob) {
if (BLOBNBOX::UnMergeableType(blob->region_type()) || blob->region_type() == BRT_VERT_TEXT) {
return false;
}
TBOX small_box(blob->bounding_box());
bool debug = AlignedBlob::WithinTestRegion(2, small_box.left(), small_box.bottom());
if (debug) {
tprintf("Testing blob for diacriticness at:");
small_box.print();
}
int x = (small_box.left() + small_box.right()) / 2;
int y = (small_box.bottom() + small_box.top()) / 2;
int grid_x, grid_y;
GridCoords(x, y, &grid_x, &grid_y);
int height = small_box.height();
BLOBNBOX *best_x_overlap = nullptr;
BLOBNBOX *best_y_overlap = nullptr;
int best_total_dist = 0;
int best_y_gap = 0;
TBOX best_xbox;
TBOX search_box(small_box);
int x_pad = IntCastRounded(gridsize() * kDiacriticXPadRatio);
int y_pad = IntCastRounded(gridsize() * kDiacriticYPadRatio);
search_box.pad(x_pad, y_pad);
BlobGridSearch rsearch(this);
rsearch.SetUniqueMode(true);
int min_height = height * kMinDiacriticSizeRatio;
rsearch.StartRectSearch(search_box);
BLOBNBOX *neighbour;
while ((neighbour = rsearch.NextRectSearch()) != nullptr) {
if (BLOBNBOX::UnMergeableType(neighbour->region_type()) || neighbour == blob ||
neighbour->owner() == blob->owner()) {
continue;
}
TBOX nbox = neighbour->bounding_box();
if (neighbour->owner() == nullptr || neighbour->owner()->IsVerticalType() ||
(neighbour->flow() != BTFT_CHAIN && neighbour->flow() != BTFT_STRONG_CHAIN)) {
if (debug) {
tprintf("Neighbour not strong enough:");
nbox.print();
}
continue;
}
if (nbox.height() < min_height) {
if (debug) {
tprintf("Neighbour not big enough:");
nbox.print();
}
continue;
}
int x_gap = small_box.x_gap(nbox);
int y_gap = small_box.y_gap(nbox);
int total_distance = projection_->DistanceOfBoxFromBox(small_box, nbox, true, denorm_, debug);
if (debug) {
tprintf("xgap=%d, y=%d, total dist=%d\n", x_gap, y_gap, total_distance);
}
if (total_distance > neighbour->owner()->median_height() * kMaxDiacriticDistanceRatio) {
if (debug) {
tprintf("Neighbour with median size %d too far away:", neighbour->owner()->median_height());
neighbour->bounding_box().print();
}
continue;
}
if (x_gap <= 0) {
if (debug) {
tprintf("Computing reduced box for :");
nbox.print();
}
int left = small_box.left() - small_box.width();
int right = small_box.right() + small_box.width();
nbox = neighbour->BoundsWithinLimits(left, right);
y_gap = small_box.y_gap(nbox);
if (best_x_overlap == nullptr || y_gap < best_y_gap) {
best_x_overlap = neighbour;
best_xbox = nbox;
best_y_gap = y_gap;
if (debug) {
tprintf("New best:");
nbox.print();
}
} else if (debug) {
tprintf("Shrunken box doesn't win:");
nbox.print();
}
} else if (blob->ConfirmNoTabViolation(*neighbour)) {
if (best_y_overlap == nullptr || total_distance < best_total_dist) {
if (debug) {
tprintf("New best y overlap:");
nbox.print();
}
best_y_overlap = neighbour;
best_total_dist = total_distance;
} else if (debug) {
tprintf("New y overlap box doesn't win:");
nbox.print();
}
} else if (debug) {
tprintf("Neighbour wrong side of a tab:");
nbox.print();
}
}
if (best_x_overlap != nullptr &&
(best_y_overlap == nullptr || best_xbox.major_y_overlap(best_y_overlap->bounding_box()))) {
blob->set_diacritic_box(best_xbox);
blob->set_base_char_blob(best_x_overlap);
if (debug) {
tprintf("DiacriticBlob OK! (x-overlap:");
small_box.print();
best_xbox.print();
}
return true;
}
if (best_y_overlap != nullptr &&
DiacriticXGapFilled(small_grid, small_box, best_y_overlap->bounding_box()) &&
NoNoiseInBetween(small_box, best_y_overlap->bounding_box())) {
blob->set_diacritic_box(best_y_overlap->bounding_box());
blob->set_base_char_blob(best_y_overlap);
if (debug) {
tprintf("DiacriticBlob OK! (y-overlap:");
small_box.print();
best_y_overlap->bounding_box().print();
}
return true;
}
if (debug) {
tprintf("DiacriticBlob fails:");
small_box.print();
tprintf("Best x+y gap = %d, y = %d\n", best_total_dist, best_y_gap);
if (best_y_overlap != nullptr) {
tprintf("XGapFilled=%d, NoiseBetween=%d\n",
DiacriticXGapFilled(small_grid, small_box, best_y_overlap->bounding_box()),
NoNoiseInBetween(small_box, best_y_overlap->bounding_box()));
}
}
return false;
}
bool StrokeWidth::DiacriticXGapFilled(BlobGrid *grid, const TBOX &diacritic_box,
const TBOX &base_box) {
int max_gap = IntCastRounded(base_box.height() * kMaxDiacriticGapToBaseCharHeight);
TBOX occupied_box(base_box);
int diacritic_gap;
while ((diacritic_gap = diacritic_box.x_gap(occupied_box)) > max_gap) {
TBOX search_box(occupied_box);
if (diacritic_box.left() > search_box.right()) {
search_box.set_left(search_box.right());
search_box.set_right(search_box.left() + max_gap);
} else {
search_box.set_right(search_box.left());
search_box.set_left(search_box.left() - max_gap);
}
BlobGridSearch rsearch(grid);
rsearch.StartRectSearch(search_box);
BLOBNBOX *neighbour;
while ((neighbour = rsearch.NextRectSearch()) != nullptr) {
const TBOX &nbox = neighbour->bounding_box();
if (nbox.x_gap(diacritic_box) < diacritic_gap) {
if (nbox.left() < occupied_box.left()) {
occupied_box.set_left(nbox.left());
}
if (nbox.right() > occupied_box.right()) {
occupied_box.set_right(nbox.right());
}
break;
}
}
if (neighbour == nullptr) {
return false;
}
}
return true;
}
void StrokeWidth::MergeDiacritics(TO_BLOCK *block, ColPartitionGrid *part_grid) {
BLOBNBOX_IT small_it(&block->noise_blobs);
for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) {
BLOBNBOX *blob = small_it.data();
if (blob->base_char_blob() != nullptr) {
ColPartition *part = blob->base_char_blob()->owner();
if (part != nullptr && !part->block_owned() && blob->owner() == nullptr &&
blob->IsDiacritic()) {
part_grid->RemoveBBox(part);
part->AddBox(blob);
blob->set_region_type(part->blob_type());
blob->set_flow(part->flow());
blob->set_owner(part);
part_grid->InsertBBox(true, true, part);
}
blob->set_base_char_blob(nullptr);
}
}
}
void StrokeWidth::RemoveLargeUnusedBlobs(TO_BLOCK *block, ColPartitionGrid *part_grid,
ColPartition_LIST *big_parts) {
BLOBNBOX_IT large_it(&block->large_blobs);
for (large_it.mark_cycle_pt(); !large_it.cycled_list(); large_it.forward()) {
BLOBNBOX *blob = large_it.data();
ColPartition *big_part = blob->owner();
if (big_part == nullptr) {
ColPartition::MakeBigPartition(blob, big_parts);
}
}
}
void StrokeWidth::PartitionRemainingBlobs(PageSegMode pageseg_mode, ColPartitionGrid *part_grid) {
BlobGridSearch gsearch(this);
BLOBNBOX *bbox;
int prev_grid_x = -1;
int prev_grid_y = -1;
BLOBNBOX_CLIST cell_list;
BLOBNBOX_C_IT cell_it(&cell_list);
bool cell_all_noise = true;
gsearch.StartFullSearch();
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
int grid_x = gsearch.GridX();
int grid_y = gsearch.GridY();
if (grid_x != prev_grid_x || grid_y != prev_grid_y) {
MakePartitionsFromCellList(pageseg_mode, cell_all_noise, part_grid, &cell_list);
cell_it.set_to_list(&cell_list);
prev_grid_x = grid_x;
prev_grid_y = grid_y;
cell_all_noise = true;
}
if (bbox->owner() == nullptr) {
cell_it.add_to_end(bbox);
if (bbox->flow() != BTFT_NONTEXT) {
cell_all_noise = false;
}
} else {
cell_all_noise = false;
}
}
MakePartitionsFromCellList(pageseg_mode, cell_all_noise, part_grid, &cell_list);
}
void StrokeWidth::MakePartitionsFromCellList(PageSegMode pageseg_mode, bool combine,
ColPartitionGrid *part_grid,
BLOBNBOX_CLIST *cell_list) {
if (cell_list->empty()) {
return;
}
BLOBNBOX_C_IT cell_it(cell_list);
if (combine) {
BLOBNBOX *bbox = cell_it.extract();
auto *part = new ColPartition(bbox->region_type(), ICOORD(0, 1));
part->AddBox(bbox);
part->set_flow(bbox->flow());
for (cell_it.forward(); !cell_it.empty(); cell_it.forward()) {
part->AddBox(cell_it.extract());
}
CompletePartition(pageseg_mode, part, part_grid);
} else {
for (; !cell_it.empty(); cell_it.forward()) {
BLOBNBOX *bbox = cell_it.extract();
auto *part = new ColPartition(bbox->region_type(), ICOORD(0, 1));
part->set_flow(bbox->flow());
part->AddBox(bbox);
CompletePartition(pageseg_mode, part, part_grid);
}
}
}
void StrokeWidth::CompletePartition(PageSegMode pageseg_mode, ColPartition *part,
ColPartitionGrid *part_grid) {
part->ComputeLimits();
TBOX box = part->bounding_box();
bool debug = AlignedBlob::WithinTestRegion(2, box.left(), box.bottom());
int value = projection_->EvaluateColPartition(*part, denorm_, debug);
if (value > 0 && FindingVerticalOnly(pageseg_mode)) {
value = part->boxes_count() == 1 ? 0 : -2;
} else if (value < 0 && FindingHorizontalOnly(pageseg_mode)) {
value = part->boxes_count() == 1 ? 0 : 2;
}
part->SetRegionAndFlowTypesFromProjectionValue(value);
part->ClaimBoxes();
part_grid->InsertBBox(true, true, part);
}
void StrokeWidth::EasyMerges(ColPartitionGrid *part_grid) {
using namespace std::placeholders;
part_grid->Merges(std::bind(&StrokeWidth::OrientationSearchBox, this, _1, _2),
std::bind(&StrokeWidth::ConfirmEasyMerge, this, _1, _2));
}
bool StrokeWidth::OrientationSearchBox(ColPartition *part, TBOX *box) {
if (part->IsVerticalType()) {
box->set_top(box->top() + box->width());
box->set_bottom(box->bottom() - box->width());
} else {
box->set_left(box->left() - box->height());
box->set_right(box->right() + box->height());
}
return true;
}
bool StrokeWidth::ConfirmEasyMerge(const ColPartition *p1, const ColPartition *p2) {
ASSERT_HOST(p1 != nullptr && p2 != nullptr);
ASSERT_HOST(!p1->IsEmpty() && !p2->IsEmpty());
if ((p1->flow() == BTFT_NONTEXT && p2->flow() >= BTFT_CHAIN) ||
(p1->flow() >= BTFT_CHAIN && p2->flow() == BTFT_NONTEXT)) {
return false;
}
if ((p1->IsVerticalType() || p2->IsVerticalType()) && p1->HCoreOverlap(*p2) <= 0 &&
((!p1->IsSingleton() && !p2->IsSingleton()) ||
!p1->bounding_box().major_overlap(p2->bounding_box()))) {
return false;
}
if ((p1->IsHorizontalType() || p2->IsHorizontalType()) && p1->VCoreOverlap(*p2) <= 0 &&
((!p1->IsSingleton() && !p2->IsSingleton()) ||
(!p1->bounding_box().major_overlap(p2->bounding_box()) &&
!p1->OKDiacriticMerge(*p2, false) && !p2->OKDiacriticMerge(*p1, false)))) {
return false;
}
if (!p1->ConfirmNoTabViolation(*p2)) {
return false;
}
if (p1->flow() <= BTFT_NONTEXT && p2->flow() <= BTFT_NONTEXT) {
return true;
}
return NoNoiseInBetween(p1->bounding_box(), p2->bounding_box());
}
bool StrokeWidth::NoNoiseInBetween(const TBOX &box1, const TBOX &box2) const {
return ImageFind::BlankImageInBetween(box1, box2, grid_box_, rerotation_, nontext_map_);
}
#ifndef GRAPHICS_DISABLED
* and the vertical/horizontal flow.
*/
ScrollView *StrokeWidth::DisplayGoodBlobs(const char *window_name, int x, int y) {
auto window = MakeWindow(x, y, window_name);
window->Brush(ScrollView::NONE);
BlobGridSearch gsearch(this);
gsearch.StartFullSearch();
BLOBNBOX *bbox;
while ((bbox = gsearch.NextFullSearch()) != nullptr) {
const TBOX &box = bbox->bounding_box();
int left_x = box.left();
int right_x = box.right();
int top_y = box.top();
int bottom_y = box.bottom();
int goodness = bbox->GoodTextBlob();
BlobRegionType blob_type = bbox->region_type();
if (bbox->UniquelyVertical()) {
blob_type = BRT_VERT_TEXT;
}
if (bbox->UniquelyHorizontal()) {
blob_type = BRT_TEXT;
}
BlobTextFlowType flow = bbox->flow();
if (flow == BTFT_NONE) {
if (goodness == 0) {
flow = BTFT_NEIGHBOURS;
} else if (goodness == 1) {
flow = BTFT_CHAIN;
} else {
flow = BTFT_STRONG_CHAIN;
}
}
window->Pen(BLOBNBOX::TextlineColor(blob_type, flow));
window->Rectangle(left_x, bottom_y, right_x, top_y);
}
window->Update();
return window;
}
static void DrawDiacriticJoiner(const BLOBNBOX *blob, ScrollView *window) {
const TBOX &blob_box(blob->bounding_box());
int top = std::max(static_cast<int>(blob_box.top()), blob->base_char_top());
int bottom = std::min(static_cast<int>(blob_box.bottom()), blob->base_char_bottom());
int x = (blob_box.left() + blob_box.right()) / 2;
window->Line(x, top, x, bottom);
}
ScrollView *StrokeWidth::DisplayDiacritics(const char *window_name, int x, int y, TO_BLOCK *block) {
auto window = MakeWindow(x, y, window_name);
window->Brush(ScrollView::NONE);
BLOBNBOX_IT it(&block->blobs);
for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
BLOBNBOX *blob = it.data();
if (blob->IsDiacritic()) {
window->Pen(ScrollView::GREEN);
DrawDiacriticJoiner(blob, window);
} else {
window->Pen(blob->BoxColor());
}
const TBOX &box = blob->bounding_box();
window->Rectangle(box.left(), box.bottom(), box.right(), box.top());
}
it.set_to_list(&block->noise_blobs);
for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
BLOBNBOX *blob = it.data();
if (blob->IsDiacritic()) {
window->Pen(ScrollView::GREEN);
DrawDiacriticJoiner(blob, window);
} else {
window->Pen(ScrollView::WHITE);
}
const TBOX &box = blob->bounding_box();
window->Rectangle(box.left(), box.bottom(), box.right(), box.top());
}
window->Update();
return window;
}
#endif
}