Mercurial > hgrepos > Python2 > PyMuPDF
diff mupdf-source/thirdparty/tesseract/src/ccmain/pagesegmain.cpp @ 2:b50eed0cc0ef upstream
ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4.
The directory name has changed: no version number in the expanded directory now.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 11:43:07 +0200 |
| parents | |
| children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mupdf-source/thirdparty/tesseract/src/ccmain/pagesegmain.cpp Mon Sep 15 11:43:07 2025 +0200 @@ -0,0 +1,416 @@ +/********************************************************************** + * File: pagesegmain.cpp + * Description: Top-level page segmenter for Tesseract. + * Author: Ray Smith + * + * (C) Copyright 2008, Google Inc. + ** Licensed under the Apache License, Version 2.0 (the "License"); + ** you may not use this file except in compliance with the License. + ** You may obtain a copy of the License at + ** http://www.apache.org/licenses/LICENSE-2.0 + ** Unless required by applicable law or agreed to in writing, software + ** distributed under the License is distributed on an "AS IS" BASIS, + ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ** See the License for the specific language governing permissions and + ** limitations under the License. + * + **********************************************************************/ + +#ifdef _WIN32 +# ifndef unlink +# include <io.h> +# endif +#else +# include <unistd.h> +#endif // _WIN32 + +// Include automatically generated configuration file if running autoconf. +#ifdef HAVE_CONFIG_H +# include "config_auto.h" +#endif + +#include <allheaders.h> +#include "blobbox.h" +#include "blread.h" +#include "colfind.h" +#include "debugpixa.h" +#ifndef DISABLED_LEGACY_ENGINE +# include "equationdetect.h" +#endif +#include <tesseract/osdetect.h> +#include "imagefind.h" +#include "linefind.h" +#include "makerow.h" +#include "tabvector.h" +#include "tesseractclass.h" +#include "tessvars.h" +#include "textord.h" +#include "tordmain.h" +#include "wordseg.h" + +namespace tesseract { + +// Max erosions to perform in removing an enclosing circle. +const int kMaxCircleErosions = 8; + +// Helper to remove an enclosing circle from an image. +// If there isn't one, then the image will most likely get badly mangled. +// The returned pix must be pixDestroyed after use. nullptr may be returned +// if the image doesn't meet the trivial conditions that it uses to determine +// success. +static Image RemoveEnclosingCircle(Image pixs) { + Image pixsi = pixInvert(nullptr, pixs); + Image pixc = pixCreateTemplate(pixs); + pixSetOrClearBorder(pixc, 1, 1, 1, 1, PIX_SET); + pixSeedfillBinary(pixc, pixc, pixsi, 4); + pixInvert(pixc, pixc); + pixsi.destroy(); + Image pixt = pixs & pixc; + l_int32 max_count; + pixCountConnComp(pixt, 8, &max_count); + // The count has to go up before we start looking for the minimum. + l_int32 min_count = INT32_MAX; + Image pixout = nullptr; + for (int i = 1; i < kMaxCircleErosions; i++) { + pixt.destroy(); + pixErodeBrick(pixc, pixc, 3, 3); + pixt = pixs & pixc; + l_int32 count; + pixCountConnComp(pixt, 8, &count); + if (i == 1 || count > max_count) { + max_count = count; + min_count = count; + } else if (count < min_count) { + min_count = count; + pixout.destroy(); + pixout = pixt.copy(); // Save the best. + } else if (count >= min_count) { + break; // We have passed by the best. + } + } + pixt.destroy(); + pixc.destroy(); + return pixout; +} + +/** + * Segment the page according to the current value of tessedit_pageseg_mode. + * pix_binary_ is used as the source image and should not be nullptr. + * On return the blocks list owns all the constructed page layout. + */ +int Tesseract::SegmentPage(const char *input_file, BLOCK_LIST *blocks, Tesseract *osd_tess, + OSResults *osr) { + ASSERT_HOST(pix_binary_ != nullptr); + int width = pixGetWidth(pix_binary_); + int height = pixGetHeight(pix_binary_); + // Get page segmentation mode. + auto pageseg_mode = static_cast<PageSegMode>(static_cast<int>(tessedit_pageseg_mode)); + // If a UNLV zone file can be found, use that instead of segmentation. + if (!PSM_COL_FIND_ENABLED(pageseg_mode) && input_file != nullptr && input_file[0] != '\0') { + std::string name = input_file; + auto lastdot = name.find_last_of('.'); + if (lastdot != std::string::npos) { + name.resize(lastdot); + } + read_unlv_file(name, width, height, blocks); + } + if (blocks->empty()) { + // No UNLV file present. Work according to the PageSegMode. + // First make a single block covering the whole image. + BLOCK_IT block_it(blocks); + auto *block = new BLOCK("", true, 0, 0, 0, 0, width, height); + block->set_right_to_left(right_to_left()); + block_it.add_to_end(block); + } else { + // UNLV file present. Use PSM_SINGLE_BLOCK. + pageseg_mode = PSM_SINGLE_BLOCK; + } + // The diacritic_blobs holds noise blobs that may be diacritics. They + // are separated out on areas of the image that seem noisy and short-circuit + // the layout process, going straight from the initial partition creation + // right through to after word segmentation, where they are added to the + // rej_cblobs list of the most appropriate word. From there classification + // will determine whether they are used. + BLOBNBOX_LIST diacritic_blobs; + int auto_page_seg_ret_val = 0; + TO_BLOCK_LIST to_blocks; + if (PSM_OSD_ENABLED(pageseg_mode) || PSM_BLOCK_FIND_ENABLED(pageseg_mode) || + PSM_SPARSE(pageseg_mode)) { + auto_page_seg_ret_val = + AutoPageSeg(pageseg_mode, blocks, &to_blocks, + enable_noise_removal ? &diacritic_blobs : nullptr, osd_tess, osr); + if (pageseg_mode == PSM_OSD_ONLY) { + return auto_page_seg_ret_val; + } + // To create blobs from the image region bounds uncomment this line: + // to_blocks.clear(); // Uncomment to go back to the old mode. + } else { + deskew_ = FCOORD(1.0f, 0.0f); + reskew_ = FCOORD(1.0f, 0.0f); + if (pageseg_mode == PSM_CIRCLE_WORD) { + Image pixcleaned = RemoveEnclosingCircle(pix_binary_); + if (pixcleaned != nullptr) { + pix_binary_.destroy(); + pix_binary_ = pixcleaned; + } + } + } + + if (auto_page_seg_ret_val < 0) { + return -1; + } + + if (blocks->empty()) { + if (textord_debug_tabfind) { + tprintf("Empty page\n"); + } + return 0; // AutoPageSeg found an empty page. + } + bool splitting = pageseg_devanagari_split_strategy != ShiroRekhaSplitter::NO_SPLIT; + bool cjk_mode = textord_use_cjk_fp_model; + + textord_.TextordPage(pageseg_mode, reskew_, width, height, pix_binary_, pix_thresholds_, + pix_grey_, splitting || cjk_mode, &diacritic_blobs, blocks, &to_blocks, &gradient_); + return auto_page_seg_ret_val; +} + +/** + * Auto page segmentation. Divide the page image into blocks of uniform + * text linespacing and images. + * + * Resolution (in ppi) is derived from the input image. + * + * The output goes in the blocks list with corresponding TO_BLOCKs in the + * to_blocks list. + * + * If !PSM_COL_FIND_ENABLED(pageseg_mode), then no attempt is made to divide + * the image into columns, but multiple blocks are still made if the text is + * of non-uniform linespacing. + * + * If diacritic_blobs is non-null, then diacritics/noise blobs, that would + * confuse layout analysis by causing textline overlap, are placed there, + * with the expectation that they will be reassigned to words later and + * noise/diacriticness determined via classification. + * + * If osd (orientation and script detection) is true then that is performed + * as well. If only_osd is true, then only orientation and script detection is + * performed. If osd is desired, (osd or only_osd) then osr_tess must be + * another Tesseract that was initialized especially for osd, and the results + * will be output into osr (orientation and script result). + */ +int Tesseract::AutoPageSeg(PageSegMode pageseg_mode, BLOCK_LIST *blocks, TO_BLOCK_LIST *to_blocks, + BLOBNBOX_LIST *diacritic_blobs, Tesseract *osd_tess, OSResults *osr) { + Image photomask_pix = nullptr; + Image musicmask_pix = nullptr; + // The blocks made by the ColumnFinder. Moved to blocks before return. + BLOCK_LIST found_blocks; + TO_BLOCK_LIST temp_blocks; + + ColumnFinder *finder = SetupPageSegAndDetectOrientation( + pageseg_mode, blocks, osd_tess, osr, &temp_blocks, &photomask_pix, + pageseg_apply_music_mask ? &musicmask_pix : nullptr); + int result = 0; + if (finder != nullptr) { + TO_BLOCK_IT to_block_it(&temp_blocks); + TO_BLOCK *to_block = to_block_it.data(); + if (musicmask_pix != nullptr) { + // TODO(rays) pass the musicmask_pix into FindBlocks and mark music + // blocks separately. For now combine with photomask_pix. + photomask_pix |= musicmask_pix; + } +#ifndef DISABLED_LEGACY_ENGINE + if (equ_detect_) { + finder->SetEquationDetect(equ_detect_); + } +#endif // ndef DISABLED_LEGACY_ENGINE + result = finder->FindBlocks(pageseg_mode, scaled_color_, scaled_factor_, to_block, + photomask_pix, pix_thresholds_, pix_grey_, &pixa_debug_, + &found_blocks, diacritic_blobs, to_blocks); + if (result >= 0) { + finder->GetDeskewVectors(&deskew_, &reskew_); + } + delete finder; + } + photomask_pix.destroy(); + musicmask_pix.destroy(); + if (result < 0) { + return result; + } + + blocks->clear(); + BLOCK_IT block_it(blocks); + // Move the found blocks to the input/output blocks. + block_it.add_list_after(&found_blocks); + return result; +} + +// Helper adds all the scripts from sid_set converted to ids from osd_set to +// allowed_ids. +static void AddAllScriptsConverted(const UNICHARSET &sid_set, const UNICHARSET &osd_set, + std::vector<int> *allowed_ids) { + for (int i = 0; i < sid_set.get_script_table_size(); ++i) { + if (i != sid_set.null_sid()) { + const char *script = sid_set.get_script_from_script_id(i); + allowed_ids->push_back(osd_set.get_script_id_from_name(script)); + } + } +} + +/** + * Sets up auto page segmentation, determines the orientation, and corrects it. + * Somewhat arbitrary chunk of functionality, factored out of AutoPageSeg to + * facilitate testing. + * photo_mask_pix is a pointer to a nullptr pointer that will be filled on + * return with the leptonica photo mask, which must be pixDestroyed by the + * caller. to_blocks is an empty list that will be filled with (usually a + * single) block that is used during layout analysis. This ugly API is required + * because of the possibility of a unlv zone file. + * TODO(rays) clean this up. + * See AutoPageSeg for other arguments. + * The returned ColumnFinder must be deleted after use. + */ +ColumnFinder *Tesseract::SetupPageSegAndDetectOrientation(PageSegMode pageseg_mode, + BLOCK_LIST *blocks, Tesseract *osd_tess, + OSResults *osr, TO_BLOCK_LIST *to_blocks, + Image *photo_mask_pix, + Image *music_mask_pix) { + int vertical_x = 0; + int vertical_y = 1; + TabVector_LIST v_lines; + TabVector_LIST h_lines; + ICOORD bleft(0, 0); + + ASSERT_HOST(pix_binary_ != nullptr); + if (tessedit_dump_pageseg_images) { + pixa_debug_.AddPix(pix_binary_, "PageSegInput"); + } + // Leptonica is used to find the rule/separator lines in the input. + LineFinder::FindAndRemoveLines(source_resolution_, textord_tabfind_show_vlines, pix_binary_, + &vertical_x, &vertical_y, music_mask_pix, &v_lines, &h_lines); + if (tessedit_dump_pageseg_images) { + pixa_debug_.AddPix(pix_binary_, "NoLines"); + } + // Leptonica is used to find a mask of the photo regions in the input. + *photo_mask_pix = ImageFind::FindImages(pix_binary_, &pixa_debug_); + if (tessedit_dump_pageseg_images) { + Image pix_no_image_ = nullptr; + if (*photo_mask_pix != nullptr) { + pix_no_image_ = pixSubtract(nullptr, pix_binary_, *photo_mask_pix); + } else { + pix_no_image_ = pix_binary_.clone(); + } + pixa_debug_.AddPix(pix_no_image_, "NoImages"); + pix_no_image_.destroy(); + } + if (!PSM_COL_FIND_ENABLED(pageseg_mode)) { + v_lines.clear(); + } + + // The rest of the algorithm uses the usual connected components. + textord_.find_components(pix_binary_, blocks, to_blocks); + + TO_BLOCK_IT to_block_it(to_blocks); + // There must be exactly one input block. + // TODO(rays) handle new textline finding with a UNLV zone file. + ASSERT_HOST(to_blocks->singleton()); + TO_BLOCK *to_block = to_block_it.data(); + TBOX blkbox = to_block->block->pdblk.bounding_box(); + ColumnFinder *finder = nullptr; + int estimated_resolution = source_resolution_; + if (source_resolution_ == kMinCredibleResolution) { + // Try to estimate resolution from typical body text size. + int res = IntCastRounded(to_block->line_size * kResolutionEstimationFactor); + if (res > estimated_resolution && res < kMaxCredibleResolution) { + estimated_resolution = res; + tprintf("Estimating resolution as %d\n", estimated_resolution); + } + } + + if (to_block->line_size >= 2) { + finder = new ColumnFinder(static_cast<int>(to_block->line_size), blkbox.botleft(), + blkbox.topright(), estimated_resolution, textord_use_cjk_fp_model, + textord_tabfind_aligned_gap_fraction, &v_lines, &h_lines, vertical_x, + vertical_y); + + finder->SetupAndFilterNoise(pageseg_mode, *photo_mask_pix, to_block); + + #ifndef DISABLED_LEGACY_ENGINE + if (equ_detect_) { + equ_detect_->LabelSpecialText(to_block); + } + #endif + + BLOBNBOX_CLIST osd_blobs; + // osd_orientation is the number of 90 degree rotations to make the + // characters upright. (See tesseract/osdetect.h for precise definition.) + // We want the text lines horizontal, (vertical text indicates vertical + // textlines) which may conflict (eg vertically written CJK). + int osd_orientation = 0; + bool vertical_text = + textord_tabfind_force_vertical_text || pageseg_mode == PSM_SINGLE_BLOCK_VERT_TEXT; + if (!vertical_text && textord_tabfind_vertical_text && PSM_ORIENTATION_ENABLED(pageseg_mode)) { + vertical_text = finder->IsVerticallyAlignedText(textord_tabfind_vertical_text_ratio, to_block, + &osd_blobs); + } + + #ifndef DISABLED_LEGACY_ENGINE + if (PSM_OSD_ENABLED(pageseg_mode) && osd_tess != nullptr && osr != nullptr) { + std::vector<int> osd_scripts; + if (osd_tess != this) { + // We are running osd as part of layout analysis, so constrain the + // scripts to those allowed by *this. + AddAllScriptsConverted(unicharset, osd_tess->unicharset, &osd_scripts); + for (auto &lang : sub_langs_) { + AddAllScriptsConverted(lang->unicharset, osd_tess->unicharset, &osd_scripts); + } + } + os_detect_blobs(&osd_scripts, &osd_blobs, osr, osd_tess); + if (pageseg_mode == PSM_OSD_ONLY) { + delete finder; + return nullptr; + } + osd_orientation = osr->best_result.orientation_id; + double osd_score = osr->orientations[osd_orientation]; + double osd_margin = min_orientation_margin * 2; + for (int i = 0; i < 4; ++i) { + if (i != osd_orientation && osd_score - osr->orientations[i] < osd_margin) { + osd_margin = osd_score - osr->orientations[i]; + } + } + int best_script_id = osr->best_result.script_id; + const char *best_script_str = osd_tess->unicharset.get_script_from_script_id(best_script_id); + bool cjk = best_script_id == osd_tess->unicharset.han_sid() || + best_script_id == osd_tess->unicharset.hiragana_sid() || + best_script_id == osd_tess->unicharset.katakana_sid() || + strcmp("Japanese", best_script_str) == 0 || + strcmp("Korean", best_script_str) == 0 || strcmp("Hangul", best_script_str) == 0; + if (cjk) { + finder->set_cjk_script(true); + } + if (osd_margin < min_orientation_margin) { + // The margin is weak. + if (!cjk && !vertical_text && osd_orientation == 2) { + // upside down latin text is improbable with such a weak margin. + tprintf( + "OSD: Weak margin (%.2f), horiz textlines, not CJK: " + "Don't rotate.\n", + osd_margin); + osd_orientation = 0; + } else { + tprintf( + "OSD: Weak margin (%.2f) for %d blob text block, " + "but using orientation anyway: %d\n", + osd_margin, osd_blobs.length(), osd_orientation); + } + } + } + #endif // ndef DISABLED_LEGACY_ENGINE + + osd_blobs.shallow_clear(); + finder->CorrectOrientation(to_block, vertical_text, osd_orientation); + } + + return finder; +} + +} // namespace tesseract.
