// Finds vertical lines in the given list of BLOBNBOXes. bleft and tright // are the bounds of the image on which the input line_bblobs were found. // The input line_bblobs list is const really. // The output vertical_x and vertical_y are the total of all the vectors. // The output list of TabVector makes no reference to the input BLOBNBOXes. void LineFinder::FindLineVectors(const ICOORD& bleft, const ICOORD& tright, BLOBNBOX_LIST* line_bblobs, int* vertical_x, int* vertical_y, TabVector_LIST* vectors) { BLOBNBOX_IT bbox_it(line_bblobs); int b_count = 0; // Put all the blobs into the grid to find the lines, and move the blobs // to the output lists. AlignedBlob blob_grid(kLineFindGridSize, bleft, tright); for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) { BLOBNBOX* bblob = bbox_it.data(); bblob->set_left_tab_type(TT_UNCONFIRMED); bblob->set_left_rule(bleft.x()); bblob->set_right_rule(tright.x()); bblob->set_left_crossing_rule(bleft.x()); bblob->set_right_crossing_rule(tright.x()); blob_grid.InsertBBox(false, true, bblob); ++b_count; } if (textord_debug_tabfind) tprintf("Inserted %d line blobs into grid\n", b_count); if (b_count == 0) return; // Search the entire grid, looking for vertical line vectors. GridSearch<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT> lsearch(&blob_grid); BLOBNBOX* bbox; TabVector_IT vector_it(vectors); *vertical_x = 0; *vertical_y = 1; lsearch.StartFullSearch(); while ((bbox = lsearch.NextFullSearch()) != NULL) { if (bbox->left_tab_type() == TT_UNCONFIRMED) { const TBOX& box = bbox->bounding_box(); if (AlignedBlob::WithinTestRegion(2, box.left(), box.bottom())) tprintf("Finding line vector starting at bbox (%d,%d)\n", box.left(), box.bottom()); AlignedBlobParams align_params(*vertical_x, *vertical_y, box.width()); TabVector* vector = blob_grid.FindVerticalAlignment(align_params, bbox, vertical_x, vertical_y); if (vector != NULL) { vector->Freeze(); vector_it.add_to_end(vector); } } } ScrollView* line_win = NULL; if (textord_tabfind_show_vlines) { line_win = blob_grid.MakeWindow(0, 50, "Vlines"); blob_grid.DisplayBoxes(line_win); line_win = blob_grid.DisplayTabs("Vlines", line_win); } }
// Tests each blob in the list to see if it is certain non-text using 2 // conditions: // 1. blob overlaps a cell with high value in noise_density_ (previously set // by ComputeNoiseDensity). // OR 2. The blob overlaps more than max_blob_overlaps in *this grid. This // condition is disabled with max_blob_overlaps == -1. // If it does, the blob is declared non-text, and is used to mark up the // nontext_mask. Such blobs are fully deleted, and non-noise blobs have their // neighbours reset, as they may now point to deleted data. // WARNING: The blobs list blobs may be in the *this grid, but they are // not removed. If any deleted blobs might be in *this, then this must be // Clear()ed immediately after MarkAndDeleteNonTextBlobs is called. // If the win is not NULL, deleted blobs are drawn on it in red, and kept // blobs are drawn on it in ok_color. void CCNonTextDetect::MarkAndDeleteNonTextBlobs(BLOBNBOX_LIST* blobs, int max_blob_overlaps, ScrollView* win, ScrollView::Color ok_color, Pix* nontext_mask) { int imageheight = tright().y() - bleft().x(); BLOBNBOX_IT blob_it(blobs); BLOBNBOX_LIST dead_blobs; BLOBNBOX_IT dead_it(&dead_blobs); for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { BLOBNBOX* blob = blob_it.data(); TBOX box = blob->bounding_box(); if (!noise_density_->RectMostlyOverThreshold(box, max_noise_count_) && (max_blob_overlaps < 0 || !BlobOverlapsTooMuch(blob, max_blob_overlaps))) { blob->ClearNeighbours(); #ifndef GRAPHICS_DISABLED if (win != NULL) blob->plot(win, ok_color, ok_color); #endif // GRAPHICS_DISABLED } else { if (noise_density_->AnyZeroInRect(box)) { // There is a danger that the bounding box may overlap real text, so // we need to render the outline. Pix* blob_pix = blob->cblob()->render_outline(); pixRasterop(nontext_mask, box.left(), imageheight - box.top(), box.width(), box.height(), PIX_SRC | PIX_DST, blob_pix, 0, 0); pixDestroy(&blob_pix); } else { if (box.area() < gridsize() * gridsize()) { // It is a really bad idea to make lots of small components in the // photo mask, so try to join it to a bigger area by expanding the // box in a way that does not touch any zero noise density cell. box = AttemptBoxExpansion(box, *noise_density_, gridsize()); } // All overlapped cells are non-zero, so just mark the rectangle. pixRasterop(nontext_mask, box.left(), imageheight - box.top(), box.width(), box.height(), PIX_SET, NULL, 0, 0); } #ifndef GRAPHICS_DISABLED if (win != NULL) blob->plot(win, ScrollView::RED, ScrollView::RED); #endif // GRAPHICS_DISABLED // It is safe to delete the cblob now, as it isn't used by the grid // or BlobOverlapsTooMuch, and the BLOBNBOXes will go away with the // dead_blobs list. // TODO(rays) delete the delete when the BLOBNBOX destructor deletes // the cblob. delete blob->cblob(); dead_it.add_to_end(blob_it.extract()); } } }
// Extend this vector to include the supplied blob if it doesn't // already have it. void TabVector::ExtendToBox(BLOBNBOX* new_blob) { TBOX new_box = new_blob->bounding_box(); BLOBNBOX_C_IT it(&boxes_); if (!it.empty()) { BLOBNBOX* blob = it.data(); TBOX box = blob->bounding_box(); while (!it.at_last() && box.top() <= new_box.top()) { if (blob == new_blob) return; // We have it already. it.forward(); blob = it.data(); box = blob->bounding_box(); } if (box.top() >= new_box.top()) { it.add_before_stay_put(new_blob); needs_refit_ = true; return; } } needs_refit_ = true; it.add_after_stay_put(new_blob); }
// Returns the box gaps between this and its neighbours_ in an array // indexed by BlobNeighbourDir. void BLOBNBOX::NeighbourGaps(int gaps[BND_COUNT]) const { for (int dir = 0; dir < BND_COUNT; ++dir) { gaps[dir] = MAX_INT16; BLOBNBOX* neighbour = neighbours_[dir]; if (neighbour != NULL) { TBOX n_box = neighbour->bounding_box(); if (dir == BND_LEFT || dir == BND_RIGHT) { gaps[dir] = box.x_gap(n_box); } else { gaps[dir] = box.y_gap(n_box); } } } }
// Moves blobs that look like they don't sit well on a textline from the // input blobs list to the output small_blobs list. // This gets them away from initial textline finding to stop diacritics // from forming incorrect textlines. (Introduced mainly to fix Thai.) void TextlineProjection::MoveNonTextlineBlobs( BLOBNBOX_LIST* blobs, BLOBNBOX_LIST* small_blobs) const { BLOBNBOX_IT it(blobs); BLOBNBOX_IT small_it(small_blobs); for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* blob = it.data(); const TBOX& box = blob->bounding_box(); bool debug = AlignedBlob::WithinTestRegion(2, box.left(), box.bottom()); if (BoxOutOfHTextline(box, NULL, debug) && !blob->UniquelyVertical()) { blob->ClearNeighbours(); small_it.add_to_end(it.extract()); } } }
TBOX box_next_pre_chopped( //get bounding box BLOBNBOX_IT *it //iterator to blobds ) { BLOBNBOX *blob; //current blob TBOX result; //total box blob = it->data (); result = blob->bounding_box (); do { it->forward (); blob = it->data (); } //until next real blob while (blob->joined_to_prev ()); return result; }
// Finds vertical line objects in the given pix. // Uses the given resolution to determine size thresholds instead of any // that may be present in the pix. // The output vertical_x and vertical_y contain a sum of the output vectors, // thereby giving the mean vertical direction. // The output vectors are owned by the list and Frozen (cannot refit) by // having no boxes, as there is no need to refit or merge separator lines. void LineFinder::FindVerticalLines(int resolution, Pix* pix, int* vertical_x, int* vertical_y, TabVector_LIST* vectors) { #ifdef HAVE_LIBLEPT Pix* line_pix; Boxa* boxes = GetVLineBoxes(resolution, pix, &line_pix); C_BLOB_LIST line_cblobs; int width = pixGetWidth(pix); int height = pixGetHeight(pix); ConvertBoxaToBlobs(width, height, &boxes, &line_cblobs); // Make the BLOBNBOXes from the C_BLOBs. BLOBNBOX_LIST line_bblobs; C_BLOB_IT blob_it(&line_cblobs); BLOBNBOX_IT bbox_it(&line_bblobs); for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { C_BLOB* cblob = blob_it.data(); BLOBNBOX* bblob = new BLOBNBOX(cblob); bbox_it.add_to_end(bblob); } ICOORD bleft(0, 0); ICOORD tright(width, height); FindLineVectors(bleft, tright, &line_bblobs, vertical_x, vertical_y, vectors); if (!vectors->empty()) { // Some lines were found, so erase the unused blobs from the line image // and then subtract the line image from the source. bbox_it.move_to_first(); for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) { BLOBNBOX* blob = bbox_it.data(); if (blob->left_tab_type() == TT_UNCONFIRMED) { const TBOX& box = blob->bounding_box(); Box* pixbox = boxCreate(box.left(), height - box.top(), box.width(), box.height()); pixClearInRect(line_pix, pixbox); boxDestroy(&pixbox); } } pixDilateBrick(line_pix, line_pix, 1, 3); pixSubtract(pix, pix, line_pix); if (textord_tabfind_show_vlines) pixWrite("vlinesclean.png", line_pix, IFF_PNG); ICOORD vertical; vertical.set_with_shrink(*vertical_x, *vertical_y); TabVector::MergeSimilarTabVectors(vertical, vectors, NULL); } pixDestroy(&line_pix); #endif }
// Display the blobs in the window colored according to textline quality. void TextlineProjection::PlotGradedBlobs(BLOBNBOX_LIST* blobs, ScrollView* win) { #ifndef GRAPHICS_DISABLED BLOBNBOX_IT it(blobs); for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* blob = it.data(); const TBOX& box = blob->bounding_box(); bool bad_box = BoxOutOfHTextline(box, NULL, false); if (blob->UniquelyVertical()) win->Pen(ScrollView::YELLOW); else win->Pen(bad_box ? ScrollView::RED : ScrollView::BLUE); win->Rectangle(box.left(), box.bottom(), box.right(), box.top()); } win->Update(); #endif // GRAPHICS_DISABLED }
// Display the tab codes of the BLOBNBOXes in this grid. ScrollView* AlignedBlob::DisplayTabs(const char* window_name, ScrollView* tab_win) { #ifndef GRAPHICS_DISABLED if (tab_win == nullptr) tab_win = MakeWindow(0, 50, window_name); // For every tab in the grid, display it. GridSearch<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT> 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(); TabType tabtype = bbox->left_tab_type(); if (tabtype != TT_NONE) { if (tabtype == TT_MAYBE_ALIGNED) tab_win->Pen(ScrollView::BLUE); else if (tabtype == TT_MAYBE_RAGGED) tab_win->Pen(ScrollView::YELLOW); else if (tabtype == TT_CONFIRMED) tab_win->Pen(ScrollView::GREEN); else tab_win->Pen(ScrollView::GREY); tab_win->Line(left_x, top_y, left_x, bottom_y); } tabtype = bbox->right_tab_type(); if (tabtype != TT_NONE) { if (tabtype == TT_MAYBE_ALIGNED) tab_win->Pen(ScrollView::MAGENTA); else if (tabtype == TT_MAYBE_RAGGED) tab_win->Pen(ScrollView::ORANGE); else if (tabtype == TT_CONFIRMED) tab_win->Pen(ScrollView::RED); else tab_win->Pen(ScrollView::GREY); tab_win->Line(right_x, top_y, right_x, bottom_y); } } tab_win->Update(); #endif return tab_win; }
// Returns true if the given blob overlaps more than max_overlaps blobs // in the current grid. bool CCNonTextDetect::BlobOverlapsTooMuch(BLOBNBOX* blob, int max_overlaps) { // Search the grid to see what intersects it. // Setup a Rectangle search for overlapping this blob. BlobGridSearch rsearch(this); TBOX box = blob->bounding_box(); rsearch.StartRectSearch(box); rsearch.SetUniqueMode(true); BLOBNBOX* neighbour; int overlap_count = 0; while (overlap_count <= max_overlaps && (neighbour = rsearch.NextRectSearch()) != NULL) { if (box.major_overlap(neighbour->bounding_box())) { ++overlap_count; if (overlap_count > max_overlaps) return true; } } return false; }
// Sets up displacement_modes_ with the top few modes of the perpendicular // distance of each blob from the given direction vector, after rounding. void BaselineRow::SetupBlobDisplacements(const FCOORD& direction) { // Set of perpendicular displacements of the blob bottoms from the required // baseline direction. GenericVector<double> perp_blob_dists; displacement_modes_.truncate(0); // Gather the skew-corrected position of every blob. double min_dist = MAX_FLOAT32; double max_dist = -MAX_FLOAT32; BLOBNBOX_IT blob_it(blobs_); bool debug = false; for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { BLOBNBOX* blob = blob_it.data(); const TBOX& box = blob->bounding_box(); #ifdef kDebugYCoord if (box.bottom() < kDebugYCoord && box.top() > kDebugYCoord) debug = true; #endif FCOORD blob_pos((box.left() + box.right()) / 2.0f, blob->baseline_position()); double offset = direction * blob_pos; perp_blob_dists.push_back(offset); if (debug) { tprintf("Displacement %g for blob at:", offset); box.print(); } UpdateRange(offset, &min_dist, &max_dist); } // Set up a histogram using disp_quant_factor_ as the bucket size. STATS dist_stats(IntCastRounded(min_dist / disp_quant_factor_), IntCastRounded(max_dist / disp_quant_factor_) + 1); for (int i = 0; i < perp_blob_dists.size(); ++i) { dist_stats.add(IntCastRounded(perp_blob_dists[i] / disp_quant_factor_), 1); } GenericVector<KDPairInc<float, int> > scaled_modes; dist_stats.top_n_modes(kMaxDisplacementsModes, &scaled_modes); if (debug) { for (int i = 0; i < scaled_modes.size(); ++i) { tprintf("Top mode = %g * %d\n", scaled_modes[i].key * disp_quant_factor_, scaled_modes[i].data); } } for (int i = 0; i < scaled_modes.size(); ++i) displacement_modes_.push_back(disp_quant_factor_ * scaled_modes[i].key); }
// Places a copy of blobs that are near a word (after applying rotation to the // blob) in the most appropriate word, unless there is doubt, in which case a // blob can end up in two words. Source blobs are not touched. void Textord::TransferDiacriticsToWords(BLOBNBOX_LIST* diacritic_blobs, const FCOORD& rotation, WordGrid* word_grid) { WordSearch ws(word_grid); BLOBNBOX_IT b_it(diacritic_blobs); // Apply rotation to each blob before finding the nearest words. The rotation // allows us to only consider above/below placement and not left/right on // vertical text, because all text is horizontal here. for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) { BLOBNBOX* blobnbox = b_it.data(); TBOX blob_box = blobnbox->bounding_box(); blob_box.rotate(rotation); ws.StartRectSearch(blob_box); // Above/below refer to word position relative to diacritic. Since some // scripts eg Kannada/Telugu habitually put diacritics below words, and // others eg Thai/Vietnamese/Latin put most diacritics above words, try // for both if there isn't much in it. WordWithBox* best_above_word = nullptr; WordWithBox* best_below_word = nullptr; int best_above_distance = 0; int best_below_distance = 0; for (WordWithBox* word = ws.NextRectSearch(); word != nullptr; word = ws.NextRectSearch()) { if (word->word()->flag(W_REP_CHAR)) continue; TBOX word_box = word->true_bounding_box(); int x_distance = blob_box.x_gap(word_box); int y_distance = blob_box.y_gap(word_box); if (x_distance > 0) { // Arbitrarily divide x-distance by 2 if there is a major y overlap, // and the word is to the left of the diacritic. If the // diacritic is a dropped broken character between two words, this will // help send all the pieces to a single word, instead of splitting them // over the 2 words. if (word_box.major_y_overlap(blob_box) && blob_box.left() > word_box.right()) { x_distance /= 2; } y_distance += x_distance; } if (word_box.y_middle() > blob_box.y_middle() && (best_above_word == nullptr || y_distance < best_above_distance)) { best_above_word = word; best_above_distance = y_distance; } if (word_box.y_middle() <= blob_box.y_middle() && (best_below_word == nullptr || y_distance < best_below_distance)) { best_below_word = word; best_below_distance = y_distance; } } bool above_good = best_above_word != nullptr && (best_below_word == nullptr || best_above_distance < best_below_distance + blob_box.height()); bool below_good = best_below_word != nullptr && best_below_word != best_above_word && (best_above_word == nullptr || best_below_distance < best_above_distance + blob_box.height()); if (below_good) { C_BLOB* copied_blob = C_BLOB::deep_copy(blobnbox->cblob()); copied_blob->rotate(rotation); // Put the blob into the word's reject blobs list. C_BLOB_IT blob_it(best_below_word->RejBlobs()); blob_it.add_to_end(copied_blob); } if (above_good) { C_BLOB* copied_blob = C_BLOB::deep_copy(blobnbox->cblob()); copied_blob->rotate(rotation); // Put the blob into the word's reject blobs list. C_BLOB_IT blob_it(best_above_word->RejBlobs()); blob_it.add_to_end(copied_blob); } } }
// (Re)Fit a line to the stored points. Returns false if the line // is degenerate. Althougth the TabVector code mostly doesn't care about the // direction of lines, XAtY would give silly results for a horizontal line. // The class is mostly aimed at use for vertical lines representing // horizontal tab stops. bool TabVector::Fit(ICOORD vertical, bool force_parallel) { needs_refit_ = false; if (boxes_.empty()) { // Don't refit something with no boxes, as that only happens // in Evaluate, and we don't want to end up with a zero vector. if (!force_parallel) return false; // If we are forcing parallel, then we just need to set the sort_key_. ICOORD midpt = startpt_; midpt += endpt_; midpt /= 2; sort_key_ = SortKey(vertical, midpt.x(), midpt.y()); return startpt_.y() != endpt_.y(); } if (!force_parallel && !IsRagged()) { // Use a fitted line as the vertical. DetLineFit linepoints; BLOBNBOX_C_IT it(&boxes_); // Fit a line to all the boxes in the list. for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* bbox = it.data(); TBOX box = bbox->bounding_box(); int x1 = IsRightTab() ? box.right() : box.left(); ICOORD boxpt(x1, box.bottom()); linepoints.Add(boxpt); if (it.at_last()) { ICOORD top_pt(x1, box.top()); linepoints.Add(top_pt); } } linepoints.Fit(&startpt_, &endpt_); if (startpt_.y() != endpt_.y()) { vertical = endpt_; vertical -= startpt_; } } int start_y = startpt_.y(); int end_y = endpt_.y(); sort_key_ = IsLeftTab() ? MAX_INT32 : -MAX_INT32; BLOBNBOX_C_IT it(&boxes_); // Choose a line parallel to the vertical such that all boxes are on the // correct side of it. mean_width_ = 0; int width_count = 0; for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* bbox = it.data(); TBOX box = bbox->bounding_box(); mean_width_ += box.width(); ++width_count; int x1 = IsRightTab() ? box.right() : box.left(); // Test both the bottom and the top, as one will be more extreme, depending // on the direction of skew. int bottom_y = box.bottom(); int top_y = box.top(); int key = SortKey(vertical, x1, bottom_y); if (IsLeftTab() == (key < sort_key_)) { sort_key_ = key; startpt_ = ICOORD(x1, bottom_y); } key = SortKey(vertical, x1, top_y); if (IsLeftTab() == (key < sort_key_)) { sort_key_ = key; startpt_ = ICOORD(x1, top_y); } if (it.at_first()) start_y = bottom_y; if (it.at_last()) end_y = top_y; } if (width_count > 0) { mean_width_ = (mean_width_ + width_count - 1) / width_count; } endpt_ = startpt_ + vertical; needs_evaluation_ = true; if (start_y != end_y) { // Set the ends of the vector to fully include the first and last blobs. startpt_.set_x(XAtY(vertical, sort_key_, start_y)); startpt_.set_y(start_y); endpt_.set_x(XAtY(vertical, sort_key_, end_y)); endpt_.set_y(end_y); return true; } return false; }
// Fits a straight baseline to the points. Returns true if it had enough // points to be reasonably sure of the fitted baseline. // If use_box_bottoms is false, baselines positions are formed by // considering the outlines of the blobs. bool BaselineRow::FitBaseline(bool use_box_bottoms) { // Deterministic fitting is used wherever possible. fitter_.Clear(); // Linear least squares is a backup if the DetLineFit produces a bad line. LLSQ llsq; BLOBNBOX_IT blob_it(blobs_); for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { BLOBNBOX* blob = blob_it.data(); if (!use_box_bottoms) blob->EstimateBaselinePosition(); const TBOX& box = blob->bounding_box(); int x_middle = (box.left() + box.right()) / 2; #ifdef kDebugYCoord if (box.bottom() < kDebugYCoord && box.top() > kDebugYCoord) { tprintf("Box bottom = %d, baseline pos=%d for box at:", box.bottom(), blob->baseline_position()); box.print(); } #endif fitter_.Add(ICOORD(x_middle, blob->baseline_position()), box.width() / 2); llsq.add(x_middle, blob->baseline_position()); } // Fit the line. ICOORD pt1, pt2; baseline_error_ = fitter_.Fit(&pt1, &pt2); baseline_pt1_ = pt1; baseline_pt2_ = pt2; if (baseline_error_ > max_baseline_error_ && fitter_.SufficientPointsForIndependentFit()) { // The fit was bad but there were plenty of points, so try skipping // the first and last few, and use the new line if it dramatically improves // the error of fit. double error = fitter_.Fit(kNumSkipPoints, kNumSkipPoints, &pt1, &pt2); if (error < baseline_error_ / 2.0) { baseline_error_ = error; baseline_pt1_ = pt1; baseline_pt2_ = pt2; } } int debug = 0; #ifdef kDebugYCoord Print(); debug = bounding_box_.bottom() < kDebugYCoord && bounding_box_.top() > kDebugYCoord ? 3 : 2; #endif // Now we obtained a direction from that fit, see if we can improve the // fit using the same direction and some other start point. FCOORD direction(pt2 - pt1); double target_offset = direction * pt1; good_baseline_ = false; FitConstrainedIfBetter(debug, direction, 0.0, target_offset); // Wild lines can be produced because DetLineFit allows vertical lines, but // vertical text has been rotated so angles over pi/4 should be disallowed. // Near vertical lines can still be produced by vertically aligned components // on very short lines. double angle = BaselineAngle(); if (fabs(angle) > M_PI * 0.25) { // Use the llsq fit as a backup. baseline_pt1_ = llsq.mean_point(); baseline_pt2_ = baseline_pt1_ + FCOORD(1.0f, llsq.m()); // TODO(rays) get rid of this when m and c are no longer used. double m = llsq.m(); double c = llsq.c(m); baseline_error_ = llsq.rms(m, c); good_baseline_ = false; } return good_baseline_; }
// Return true if this vector is the same side, overlaps, and close // enough to the other to be merged. bool TabVector::SimilarTo(const ICOORD& vertical, const TabVector& other, BlobGrid* grid) const { if ((IsRightTab() && other.IsRightTab()) || (IsLeftTab() && other.IsLeftTab())) { // If they don't overlap, at least in extensions, then there is no chance. if (ExtendedOverlap(other.extended_ymax_, other.extended_ymin_) < 0) return false; // A fast approximation to the scale factor of the sort_key_. int v_scale = abs(vertical.y()); if (v_scale == 0) v_scale = 1; // If they are close enough, then OK. if (sort_key_ + kSimilarVectorDist * v_scale >= other.sort_key_ && sort_key_ - kSimilarVectorDist * v_scale <= other.sort_key_) return true; // Ragged tabs get a bigger threshold. if (!IsRagged() || !other.IsRagged() || sort_key_ + kSimilarRaggedDist * v_scale < other.sort_key_ || sort_key_ - kSimilarRaggedDist * v_scale > other.sort_key_) return false; if (grid == NULL) { // There is nothing else to test! return true; } // If there is nothing in the rectangle between the vector that is going to // move, and the place it is moving to, then they can be merged. // Setup a vertical search for any blob. const TabVector* mover = (IsRightTab() && sort_key_ < other.sort_key_) ? this : &other; int top_y = mover->endpt_.y(); int bottom_y = mover->startpt_.y(); int left = MIN(mover->XAtY(top_y), mover->XAtY(bottom_y)); int right = MAX(mover->XAtY(top_y), mover->XAtY(bottom_y)); int shift = abs(sort_key_ - other.sort_key_) / v_scale; if (IsRightTab()) { right += shift; } else { left -= shift; } GridSearch<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT> vsearch(grid); vsearch.StartVerticalSearch(left, right, top_y); BLOBNBOX* blob; while ((blob = vsearch.NextVerticalSearch(true)) != NULL) { TBOX box = blob->bounding_box(); if (box.top() > bottom_y) return true; // Nothing found. if (box.bottom() < top_y) continue; // Doesn't overlap. int left_at_box = XAtY(box.bottom()); int right_at_box = left_at_box; if (IsRightTab()) right_at_box += shift; else left_at_box -= shift; if (MIN(right_at_box, box.right()) > MAX(left_at_box, box.left())) return false; } return true; // Nothing found. } return false; }
// Evaluate the vector in terms of coverage of its length by good-looking // box edges. A good looking box is one where its nearest neighbour on the // inside is nearer than half the distance its nearest neighbour on the // outside of the putative column. Bad boxes are removed from the line. // A second pass then further filters boxes by requiring that the gutter // width be a minimum fraction of the mean gutter along the line. void TabVector::Evaluate(const ICOORD& vertical, TabFind* finder) { bool debug = false; needs_evaluation_ = false; int length = endpt_.y() - startpt_.y(); if (length == 0 || boxes_.empty()) { percent_score_ = 0; Print("Zero length in evaluate"); return; } // Compute the mean box height. BLOBNBOX_C_IT it(&boxes_); int mean_height = 0; int height_count = 0; for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* bbox = it.data(); const TBOX& box = bbox->bounding_box(); int height = box.height(); mean_height += height; ++height_count; } if (height_count > 0) mean_height /= height_count; int max_gutter = kGutterMultiple * mean_height; if (IsRagged()) { // Ragged edges face a tougher test in that the gap must always be within // the height of the blob. max_gutter = kGutterToNeighbourRatio * mean_height; } STATS gutters(0, max_gutter + 1); // Evaluate the boxes for their goodness, calculating the coverage as we go. // Remove boxes that are not good and shorten the list to the first and // last good boxes. int num_deleted_boxes = 0; bool text_on_image = false; int good_length = 0; const TBOX* prev_good_box = NULL; for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* bbox = it.data(); const TBOX& box = bbox->bounding_box(); int mid_y = (box.top() + box.bottom()) / 2; if (TabFind::WithinTestRegion(2, XAtY(box.bottom()), box.bottom())) { if (!debug) { tprintf("After already deleting %d boxes, ", num_deleted_boxes); Print("Starting evaluation"); } debug = true; } // A good box is one where the nearest neighbour on the inside is closer // than half the distance to the nearest neighbour on the outside // (of the putative column). bool left = IsLeftTab(); int tab_x = XAtY(mid_y); int gutter_width; int neighbour_gap; finder->GutterWidthAndNeighbourGap(tab_x, mean_height, max_gutter, left, bbox, &gutter_width, &neighbour_gap); if (debug) { tprintf("Box (%d,%d)->(%d,%d) has gutter %d, ndist %d\n", box.left(), box.bottom(), box.right(), box.top(), gutter_width, neighbour_gap); } // Now we can make the test. if (neighbour_gap * kGutterToNeighbourRatio <= gutter_width) { // A good box contributes its height to the good_length. good_length += box.top() - box.bottom(); gutters.add(gutter_width, 1); // Two good boxes together contribute the gap between them // to the good_length as well, as long as the gap is not // too big. if (prev_good_box != NULL) { int vertical_gap = box.bottom() - prev_good_box->top(); double size1 = sqrt(static_cast<double>(prev_good_box->area())); double size2 = sqrt(static_cast<double>(box.area())); if (vertical_gap < kMaxFillinMultiple * MIN(size1, size2)) good_length += vertical_gap; if (debug) { tprintf("Box and prev good, gap=%d, target %g, goodlength=%d\n", vertical_gap, kMaxFillinMultiple * MIN(size1, size2), good_length); } } else { // Adjust the start to the first good box. SetYStart(box.bottom()); } prev_good_box = &box; if (bbox->flow() == BTFT_TEXT_ON_IMAGE) text_on_image = true; } else { // Get rid of boxes that are not good. if (debug) { tprintf("Bad Box (%d,%d)->(%d,%d) with gutter %d, ndist %d\n", box.left(), box.bottom(), box.right(), box.top(), gutter_width, neighbour_gap); } it.extract(); ++num_deleted_boxes; } } if (debug) { Print("Evaluating:"); } // If there are any good boxes, do it again, except this time get rid of // boxes that have a gutter that is a small fraction of the mean gutter. // This filters out ends that run into a coincidental gap in the text. int search_top = endpt_.y(); int search_bottom = startpt_.y(); int median_gutter = IntCastRounded(gutters.median()); if (gutters.get_total() > 0) { prev_good_box = NULL; for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* bbox = it.data(); const TBOX& box = bbox->bounding_box(); int mid_y = (box.top() + box.bottom()) / 2; // A good box is one where the gutter width is at least some constant // fraction of the mean gutter width. bool left = IsLeftTab(); int tab_x = XAtY(mid_y); int max_gutter = kGutterMultiple * mean_height; if (IsRagged()) { // Ragged edges face a tougher test in that the gap must always be // within the height of the blob. max_gutter = kGutterToNeighbourRatio * mean_height; } int gutter_width; int neighbour_gap; finder->GutterWidthAndNeighbourGap(tab_x, mean_height, max_gutter, left, bbox, &gutter_width, &neighbour_gap); // Now we can make the test. if (gutter_width >= median_gutter * kMinGutterFraction) { if (prev_good_box == NULL) { // Adjust the start to the first good box. SetYStart(box.bottom()); search_bottom = box.top(); } prev_good_box = &box; search_top = box.bottom(); } else { // Get rid of boxes that are not good. if (debug) { tprintf("Bad Box (%d,%d)->(%d,%d) with gutter %d, mean gutter %d\n", box.left(), box.bottom(), box.right(), box.top(), gutter_width, median_gutter); } it.extract(); ++num_deleted_boxes; } } } // If there has been a good box, adjust the end. if (prev_good_box != NULL) { SetYEnd(prev_good_box->top()); // Compute the percentage of the vector that is occupied by good boxes. int length = endpt_.y() - startpt_.y(); percent_score_ = 100 * good_length / length; if (num_deleted_boxes > 0) { needs_refit_ = true; FitAndEvaluateIfNeeded(vertical, finder); if (boxes_.empty()) return; } // Test the gutter over the whole vector, instead of just at the boxes. int required_shift; if (search_bottom > search_top) { search_bottom = startpt_.y(); search_top = endpt_.y(); } double min_gutter_width = kLineCountReciprocal / boxes_.length(); min_gutter_width += IsRagged() ? kMinRaggedGutter : kMinAlignedGutter; min_gutter_width *= mean_height; int max_gutter_width = IntCastRounded(min_gutter_width) + 1; if (median_gutter > max_gutter_width) max_gutter_width = median_gutter; int gutter_width = finder->GutterWidth(search_bottom, search_top, *this, text_on_image, max_gutter_width, &required_shift); if (gutter_width < min_gutter_width) { if (debug) { tprintf("Rejecting bad tab Vector with %d gutter vs %g min\n", gutter_width, min_gutter_width); } boxes_.shallow_clear(); percent_score_ = 0; } else if (debug) { tprintf("Final gutter %d, vs limit of %g, required shift = %d\n", gutter_width, min_gutter_width, required_shift); } } else { // There are no good boxes left, so score is 0. percent_score_ = 0; } if (debug) { Print("Evaluation complete:"); } }
void plot_word_decisions( //draw words ScrollView *win, //window tro draw in inT16 pitch, //of block TO_ROW *row //row to draw ) { ScrollView::Color colour = ScrollView::MAGENTA; //current colour ScrollView::Color rect_colour; //fuzzy colour inT32 prev_x; //end of prev blob inT16 blob_count; //blobs in word BLOBNBOX *blob; //current blob TBOX blob_box; //bounding box //iterator BLOBNBOX_IT blob_it = row->blob_list(); BLOBNBOX_IT start_it = blob_it;//word start rect_colour = ScrollView::BLACK; prev_x = -MAX_INT16; blob_count = 0; for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { blob = blob_it.data(); blob_box = blob->bounding_box(); if (!blob->joined_to_prev() && blob_box.left() - prev_x > row->max_nonspace) { if ((blob_box.left() - prev_x >= row->min_space || blob_box.left() - prev_x > row->space_threshold) && blob_count > 0) { if (pitch > 0 && textord_show_fixed_cuts) plot_fp_cells(win, colour, &start_it, pitch, blob_count, &row->projection, row->projection_left, row->projection_right, row->xheight * textord_projection_scale); blob_count = 0; start_it = blob_it; } if (colour == ScrollView::MAGENTA) colour = ScrollView::RED; else colour = (ScrollView::Color)(colour + 1); if (blob_box.left() - prev_x < row->min_space) { if (blob_box.left() - prev_x > row->space_threshold) rect_colour = ScrollView::GOLDENROD; else rect_colour = ScrollView::CORAL; //fill_color_index(win, rect_colour); win->Brush(rect_colour); win->Rectangle(prev_x, blob_box.bottom(), blob_box.left(), blob_box.top()); } } if (!blob->joined_to_prev()) prev_x = blob_box.right(); if (blob->cblob() != NULL) blob->cblob()->plot(win, colour, colour); if (!blob->joined_to_prev() && blob->cblob() != NULL) blob_count++; } if (pitch > 0 && textord_show_fixed_cuts && blob_count > 0) plot_fp_cells(win, colour, &start_it, pitch, blob_count, &row->projection, row->projection_left, row->projection_right, row->xheight * textord_projection_scale); }
// Search vertically for a blob that is aligned with the input bbox. // The search parameters are determined by AlignedBlobParams. // top_to_bottom tells whether to search down or up. // The return value is nullptr if nothing was found in the search box // or if a blob was found in the gutter. On a nullptr return, end_y // is set to the edge of the search box or the leading edge of the // gutter blob if one was found. BLOBNBOX* AlignedBlob::FindAlignedBlob(const AlignedBlobParams& p, bool top_to_bottom, BLOBNBOX* bbox, int x_start, int* end_y) { TBOX box = bbox->bounding_box(); // If there are separator lines, get the column edges. int left_column_edge = bbox->left_rule(); int right_column_edge = bbox->right_rule(); // start_y is used to guarantee that forward progress is made and the // search does not go into an infinite loop. New blobs must extend the // line beyond start_y. int start_y = top_to_bottom ? box.bottom() : box.top(); if (WithinTestRegion(2, x_start, start_y)) { tprintf("Column edges for blob at (%d,%d)->(%d,%d) are [%d, %d]\n", box.left(), box.top(), box.right(), box.bottom(), left_column_edge, right_column_edge); } // Compute skew tolerance. int skew_tolerance = p.max_v_gap / kMaxSkewFactor; // Calculate xmin and xmax of the search box so that it contains // all possibly relevant boxes up to p.max_v_gap above or below accoording // to top_to_bottom. // Start with a notion of vertical with the current estimate. int x2 = (p.max_v_gap * p.vertical.x() + p.vertical.y()/2) / p.vertical.y(); if (top_to_bottom) { x2 = x_start - x2; *end_y = start_y - p.max_v_gap; } else { x2 = x_start + x2; *end_y = start_y + p.max_v_gap; } // Expand the box by an additional skew tolerance int xmin = std::min(x_start, x2) - skew_tolerance; int xmax = std::max(x_start, x2) + skew_tolerance; // Now add direction-specific tolerances. if (p.right_tab) { xmax += p.min_gutter; xmin -= p.l_align_tolerance; } else { xmax += p.r_align_tolerance; xmin -= p.min_gutter; } // Setup a vertical search for an aligned blob. GridSearch<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT> vsearch(this); if (WithinTestRegion(2, x_start, start_y)) tprintf("Starting %s %s search at %d-%d,%d, search_size=%d, gutter=%d\n", p.ragged ? "Ragged" : "Aligned", p.right_tab ? "Right" : "Left", xmin, xmax, start_y, p.max_v_gap, p.min_gutter); vsearch.StartVerticalSearch(xmin, xmax, start_y); // result stores the best real return value. BLOBNBOX* result = nullptr; // The backup_result is not a tab candidate and can be used if no // real tab candidate result is found. BLOBNBOX* backup_result = nullptr; // neighbour is the blob that is currently being investigated. BLOBNBOX* neighbour = nullptr; while ((neighbour = vsearch.NextVerticalSearch(top_to_bottom)) != nullptr) { if (neighbour == bbox) continue; TBOX nbox = neighbour->bounding_box(); int n_y = (nbox.top() + nbox.bottom()) / 2; if ((!top_to_bottom && n_y > start_y + p.max_v_gap) || (top_to_bottom && n_y < start_y - p.max_v_gap)) { if (WithinTestRegion(2, x_start, start_y)) tprintf("Neighbour too far at (%d,%d)->(%d,%d)\n", nbox.left(), nbox.bottom(), nbox.right(), nbox.top()); break; // Gone far enough. } // It is CRITICAL to ensure that forward progress is made, (strictly // in/decreasing n_y) or the caller could loop infinitely, while // waiting for a sequence of blobs in a line to end. // NextVerticalSearch alone does not guarantee this, as there may be // more than one blob in a grid cell. See comment in AlignTabs. if ((n_y < start_y) != top_to_bottom || nbox.y_overlap(box)) continue; // Only look in the required direction. if (result != nullptr && result->bounding_box().y_gap(nbox) > gridsize()) return result; // This result is clear. if (backup_result != nullptr && p.ragged && result == nullptr && backup_result->bounding_box().y_gap(nbox) > gridsize()) return backup_result; // This result is clear. // If the neighbouring blob is the wrong side of a separator line, then it // "doesn't exist" as far as we are concerned. int x_at_n_y = x_start + (n_y - start_y) * p.vertical.x() / p.vertical.y(); if (x_at_n_y < neighbour->left_crossing_rule() || x_at_n_y > neighbour->right_crossing_rule()) continue; // Separator line in the way. int n_left = nbox.left(); int n_right = nbox.right(); int n_x = p.right_tab ? n_right : n_left; if (WithinTestRegion(2, x_start, start_y)) tprintf("neighbour at (%d,%d)->(%d,%d), n_x=%d, n_y=%d, xatn=%d\n", nbox.left(), nbox.bottom(), nbox.right(), nbox.top(), n_x, n_y, x_at_n_y); if (p.right_tab && n_left < x_at_n_y + p.min_gutter && n_right > x_at_n_y + p.r_align_tolerance && (p.ragged || n_left < x_at_n_y + p.gutter_fraction * nbox.height())) { // In the gutter so end of line. if (bbox->right_tab_type() >= TT_MAYBE_ALIGNED) bbox->set_right_tab_type(TT_DELETED); *end_y = top_to_bottom ? nbox.top() : nbox.bottom(); if (WithinTestRegion(2, x_start, start_y)) tprintf("gutter\n"); return nullptr; } if (!p.right_tab && n_left < x_at_n_y - p.l_align_tolerance && n_right > x_at_n_y - p.min_gutter && (p.ragged || n_right > x_at_n_y - p.gutter_fraction * nbox.height())) { // In the gutter so end of line. if (bbox->left_tab_type() >= TT_MAYBE_ALIGNED) bbox->set_left_tab_type(TT_DELETED); *end_y = top_to_bottom ? nbox.top() : nbox.bottom(); if (WithinTestRegion(2, x_start, start_y)) tprintf("gutter\n"); return nullptr; } if ((p.right_tab && neighbour->leader_on_right()) || (!p.right_tab && neighbour->leader_on_left())) continue; // Neighbours of leaders are not allowed to be used. if (n_x <= x_at_n_y + p.r_align_tolerance && n_x >= x_at_n_y - p.l_align_tolerance) { // Aligned so keep it. If it is a marked tab save it as result, // otherwise keep it as backup_result to return in case of later failure. if (WithinTestRegion(2, x_start, start_y)) tprintf("aligned, seeking%d, l=%d, r=%d\n", p.right_tab, neighbour->left_tab_type(), neighbour->right_tab_type()); TabType n_type = p.right_tab ? neighbour->right_tab_type() : neighbour->left_tab_type(); if (n_type != TT_NONE && (p.ragged || n_type != TT_MAYBE_RAGGED)) { if (result == nullptr) { result = neighbour; } else { // Keep the closest neighbour by Euclidean distance. // This prevents it from picking a tab blob in another column. const TBOX& old_box = result->bounding_box(); int x_diff = p.right_tab ? old_box.right() : old_box.left(); x_diff -= x_at_n_y; int y_diff = (old_box.top() + old_box.bottom()) / 2 - start_y; int old_dist = x_diff * x_diff + y_diff * y_diff; x_diff = n_x - x_at_n_y; y_diff = n_y - start_y; int new_dist = x_diff * x_diff + y_diff * y_diff; if (new_dist < old_dist) result = neighbour; } } else if (backup_result == nullptr) { if (WithinTestRegion(2, x_start, start_y)) tprintf("Backup\n"); backup_result = neighbour; } else { TBOX backup_box = backup_result->bounding_box(); if ((p.right_tab && backup_box.right() < nbox.right()) || (!p.right_tab && backup_box.left() > nbox.left())) { if (WithinTestRegion(2, x_start, start_y)) tprintf("Better backup\n"); backup_result = neighbour; } } } } return result != nullptr ? result : backup_result; }
float Textord::filter_noise_blobs( BLOBNBOX_LIST *src_list, // original list BLOBNBOX_LIST *noise_list, // noise list BLOBNBOX_LIST *small_list, // small blobs BLOBNBOX_LIST *large_list) { // large blobs int16_t height; //height of blob int16_t width; //of blob BLOBNBOX *blob; //current blob float initial_x; //first guess BLOBNBOX_IT src_it = src_list; //iterators BLOBNBOX_IT noise_it = noise_list; BLOBNBOX_IT small_it = small_list; BLOBNBOX_IT large_it = large_list; STATS size_stats (0, MAX_NEAREST_DIST); //blob heights float min_y; //size limits float max_y; float max_x; float max_height; //of good blobs for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) { blob = src_it.data(); if (blob->bounding_box().height() < textord_max_noise_size) noise_it.add_after_then_move(src_it.extract()); else if (blob->enclosed_area() >= blob->bounding_box().height() * blob->bounding_box().width() * textord_noise_area_ratio) small_it.add_after_then_move(src_it.extract()); } for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) { size_stats.add(src_it.data()->bounding_box().height(), 1); } initial_x = size_stats.ile(textord_initialx_ile); max_y = ceil(initial_x * (tesseract::CCStruct::kDescenderFraction + tesseract::CCStruct::kXHeightFraction + 2 * tesseract::CCStruct::kAscenderFraction) / tesseract::CCStruct::kXHeightFraction); min_y = floor (initial_x / 2); max_x = ceil (initial_x * textord_width_limit); small_it.move_to_first (); for (small_it.mark_cycle_pt (); !small_it.cycled_list (); small_it.forward ()) { height = small_it.data()->bounding_box().height(); if (height > max_y) large_it.add_after_then_move(small_it.extract ()); else if (height >= min_y) src_it.add_after_then_move(small_it.extract ()); } size_stats.clear (); for (src_it.mark_cycle_pt (); !src_it.cycled_list (); src_it.forward ()) { height = src_it.data ()->bounding_box ().height (); width = src_it.data ()->bounding_box ().width (); if (height < min_y) small_it.add_after_then_move (src_it.extract ()); else if (height > max_y || width > max_x) large_it.add_after_then_move (src_it.extract ()); else size_stats.add (height, 1); } max_height = size_stats.ile (textord_initialasc_ile); // tprintf("max_y=%g, min_y=%g, initial_x=%g, max_height=%g,", // max_y,min_y,initial_x,max_height); max_height *= tesseract::CCStruct::kXHeightCapRatio; if (max_height > initial_x) initial_x = max_height; // tprintf(" ret=%g\n",initial_x); return initial_x; }
int32_t row_words2( //compute space size TO_BLOCK* block, //block it came from TO_ROW* row, //row to operate on int32_t maxwidth, //max expected space size FCOORD rotation, //for drawing bool testing_on //for debug ) { bool prev_valid; //if decent size bool this_valid; //current blob big enough int32_t prev_x; //end of prev blob int32_t min_width; //min interesting width int32_t valid_count; //good gaps int32_t total_count; //total gaps int32_t cluster_count; //no of clusters int32_t prev_count; //previous cluster_count int32_t gap_index; //which cluster int32_t smooth_factor; //for smoothing stats BLOBNBOX *blob; //current blob float lower, upper; //clustering parameters ICOORD testpt; TBOX blob_box; //bounding box //iterator BLOBNBOX_IT blob_it = row->blob_list (); STATS gap_stats (0, maxwidth); //gap sizes float gaps[BLOCK_STATS_CLUSTERS]; STATS cluster_stats[BLOCK_STATS_CLUSTERS + 1]; //clusters testpt = ICOORD (textord_test_x, textord_test_y); smooth_factor = static_cast<int32_t>(block->xheight * textord_wordstats_smooth_factor + 1.5); // if (testing_on) // tprintf("Row smooth factor=%d\n",smooth_factor); prev_valid = false; prev_x = -INT16_MAX; const bool testing_row = false; //min blob size min_width = static_cast<int32_t>(block->pr_space); total_count = 0; for (blob_it.mark_cycle_pt (); !blob_it.cycled_list (); blob_it.forward ()) { blob = blob_it.data (); if (!blob->joined_to_prev ()) { blob_box = blob->bounding_box (); this_valid = blob_box.width () >= min_width; if (this_valid && prev_valid && blob_box.left () - prev_x < maxwidth) { gap_stats.add (blob_box.left () - prev_x, 1); } total_count++; //count possibles prev_x = blob_box.right (); prev_valid = this_valid; } } valid_count = gap_stats.get_total (); if (valid_count < total_count * textord_words_minlarge) { gap_stats.clear (); prev_x = -INT16_MAX; for (blob_it.mark_cycle_pt (); !blob_it.cycled_list (); blob_it.forward ()) { blob = blob_it.data (); if (!blob->joined_to_prev ()) { blob_box = blob->bounding_box (); if (blob_box.left () - prev_x < maxwidth) { gap_stats.add (blob_box.left () - prev_x, 1); } prev_x = blob_box.right (); } } } if (gap_stats.get_total () == 0) { row->min_space = 0; //no evidence row->max_nonspace = 0; return 0; } cluster_count = 0; lower = block->xheight * words_initial_lower; upper = block->xheight * words_initial_upper; gap_stats.smooth (smooth_factor); do { prev_count = cluster_count; cluster_count = gap_stats.cluster (lower, upper, textord_spacesize_ratioprop, BLOCK_STATS_CLUSTERS, cluster_stats); } while (cluster_count > prev_count && cluster_count < BLOCK_STATS_CLUSTERS); if (cluster_count < 1) { row->min_space = 0; row->max_nonspace = 0; return 0; } for (gap_index = 0; gap_index < cluster_count; gap_index++) gaps[gap_index] = cluster_stats[gap_index + 1].ile (0.5); //get medians if (testing_on) { tprintf ("cluster_count=%d:", cluster_count); for (gap_index = 0; gap_index < cluster_count; gap_index++) tprintf (" %g(%d)", gaps[gap_index], cluster_stats[gap_index + 1].get_total ()); tprintf ("\n"); } //Try to find proportional non-space and space for row. for (gap_index = 0; gap_index < cluster_count && gaps[gap_index] > block->max_nonspace; gap_index++); if (gap_index < cluster_count) lower = gaps[gap_index]; //most frequent below else { if (testing_on) tprintf ("No cluster below block threshold!, using default=%g\n", block->pr_nonsp); lower = block->pr_nonsp; } for (gap_index = 0; gap_index < cluster_count && gaps[gap_index] <= block->max_nonspace; gap_index++); if (gap_index < cluster_count) upper = gaps[gap_index]; //most frequent above else { if (testing_on) tprintf ("No cluster above block threshold!, using default=%g\n", block->pr_space); upper = block->pr_space; } row->min_space = static_cast<int32_t>(ceil (upper - (upper - lower) * textord_words_definite_spread)); row->max_nonspace = static_cast<int32_t>(floor (lower + (upper - lower) * textord_words_definite_spread)); row->space_threshold = (row->max_nonspace + row->min_space) / 2; row->space_size = upper; row->kern_size = lower; if (testing_on) { if (testing_row) { tprintf ("GAP STATS\n"); gap_stats.print(); tprintf ("SPACE stats\n"); cluster_stats[2].print_summary(); tprintf ("NONSPACE stats\n"); cluster_stats[1].print_summary(); } tprintf ("Row at %g has minspace=%d(%g), max_non=%d(%g)\n", row->intercept (), row->min_space, upper, row->max_nonspace, lower); } return 1; }
int32_t row_words( //compute space size TO_BLOCK* block, //block it came from TO_ROW* row, //row to operate on int32_t maxwidth, //max expected space size FCOORD rotation, //for drawing bool testing_on //for debug ) { bool testing_row; //contains testpt bool prev_valid; //if decent size int32_t prev_x; //end of prev blob int32_t cluster_count; //no of clusters int32_t gap_index; //which cluster int32_t smooth_factor; //for smoothing stats BLOBNBOX *blob; //current blob float lower, upper; //clustering parameters float gaps[3]; //gap clusers ICOORD testpt; TBOX blob_box; //bounding box //iterator BLOBNBOX_IT blob_it = row->blob_list (); STATS gap_stats (0, maxwidth); STATS cluster_stats[4]; //clusters testpt = ICOORD (textord_test_x, textord_test_y); smooth_factor = static_cast<int32_t>(block->xheight * textord_wordstats_smooth_factor + 1.5); // if (testing_on) // tprintf("Row smooth factor=%d\n",smooth_factor); prev_valid = false; prev_x = -INT32_MAX; testing_row = false; for (blob_it.mark_cycle_pt (); !blob_it.cycled_list (); blob_it.forward ()) { blob = blob_it.data (); blob_box = blob->bounding_box (); if (blob_box.contains (testpt)) testing_row = true; gap_stats.add (blob_box.width (), 1); } gap_stats.clear (); for (blob_it.mark_cycle_pt (); !blob_it.cycled_list (); blob_it.forward ()) { blob = blob_it.data (); if (!blob->joined_to_prev ()) { blob_box = blob->bounding_box (); if (prev_valid && blob_box.left () - prev_x < maxwidth) { gap_stats.add (blob_box.left () - prev_x, 1); } prev_valid = true; prev_x = blob_box.right (); } } if (gap_stats.get_total () == 0) { row->min_space = 0; //no evidence row->max_nonspace = 0; return 0; } gap_stats.smooth (smooth_factor); lower = row->xheight * textord_words_initial_lower; upper = row->xheight * textord_words_initial_upper; cluster_count = gap_stats.cluster (lower, upper, textord_spacesize_ratioprop, 3, cluster_stats); while (cluster_count < 2 && ceil (lower) < floor (upper)) { //shrink gap upper = (upper * 3 + lower) / 4; lower = (lower * 3 + upper) / 4; cluster_count = gap_stats.cluster (lower, upper, textord_spacesize_ratioprop, 3, cluster_stats); } if (cluster_count < 2) { row->min_space = 0; //no evidence row->max_nonspace = 0; return 0; } for (gap_index = 0; gap_index < cluster_count; gap_index++) gaps[gap_index] = cluster_stats[gap_index + 1].ile (0.5); //get medians if (cluster_count > 2) { if (testing_on && textord_show_initial_words) { tprintf ("Row at %g has 3 sizes of gap:%g,%g,%g\n", row->intercept (), cluster_stats[1].ile (0.5), cluster_stats[2].ile (0.5), cluster_stats[3].ile (0.5)); } lower = gaps[0]; if (gaps[1] > lower) { upper = gaps[1]; //prefer most frequent if (upper < block->xheight * textord_words_min_minspace && gaps[2] > gaps[1]) { upper = gaps[2]; } } else if (gaps[2] > lower && gaps[2] >= block->xheight * textord_words_min_minspace) upper = gaps[2]; else if (lower >= block->xheight * textord_words_min_minspace) { upper = lower; //not nice lower = gaps[1]; if (testing_on && textord_show_initial_words) { tprintf ("Had to switch most common from lower to upper!!\n"); gap_stats.print(); } } else { row->min_space = 0; //no evidence row->max_nonspace = 0; return 0; } } else { if (gaps[1] < gaps[0]) { if (testing_on && textord_show_initial_words) { tprintf ("Had to switch most common from lower to upper!!\n"); gap_stats.print(); } lower = gaps[1]; upper = gaps[0]; } else { upper = gaps[1]; lower = gaps[0]; } } if (upper < block->xheight * textord_words_min_minspace) { row->min_space = 0; //no evidence row->max_nonspace = 0; return 0; } if (upper * 3 < block->min_space * 2 + block->max_nonspace || lower * 3 > block->min_space * 2 + block->max_nonspace) { if (testing_on && textord_show_initial_words) { tprintf ("Disagreement between block and row at %g!!\n", row->intercept ()); tprintf ("Lower=%g, upper=%g, Stats:\n", lower, upper); gap_stats.print(); } } row->min_space = static_cast<int32_t>(ceil (upper - (upper - lower) * textord_words_definite_spread)); row->max_nonspace = static_cast<int32_t>(floor (lower + (upper - lower) * textord_words_definite_spread)); row->space_threshold = (row->max_nonspace + row->min_space) / 2; row->space_size = upper; row->kern_size = lower; if (testing_on && textord_show_initial_words) { if (testing_row) { tprintf ("GAP STATS\n"); gap_stats.print(); tprintf ("SPACE stats\n"); cluster_stats[2].print_summary(); tprintf ("NONSPACE stats\n"); cluster_stats[1].print_summary(); } tprintf ("Row at %g has minspace=%d(%g), max_non=%d(%g)\n", row->intercept (), row->min_space, upper, row->max_nonspace, lower); } return cluster_stats[2].get_total (); }