Mercurial > hgrepos > Python2 > PyMuPDF
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 1:1d09e1dec1d9 | 2:b50eed0cc0ef |
|---|---|
| 1 /********************************************************************** | |
| 2 * File: pagesegmain.cpp | |
| 3 * Description: Top-level page segmenter for Tesseract. | |
| 4 * Author: Ray Smith | |
| 5 * | |
| 6 * (C) Copyright 2008, Google Inc. | |
| 7 ** Licensed under the Apache License, Version 2.0 (the "License"); | |
| 8 ** you may not use this file except in compliance with the License. | |
| 9 ** You may obtain a copy of the License at | |
| 10 ** http://www.apache.org/licenses/LICENSE-2.0 | |
| 11 ** Unless required by applicable law or agreed to in writing, software | |
| 12 ** distributed under the License is distributed on an "AS IS" BASIS, | |
| 13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 14 ** See the License for the specific language governing permissions and | |
| 15 ** limitations under the License. | |
| 16 * | |
| 17 **********************************************************************/ | |
| 18 | |
| 19 #ifdef _WIN32 | |
| 20 # ifndef unlink | |
| 21 # include <io.h> | |
| 22 # endif | |
| 23 #else | |
| 24 # include <unistd.h> | |
| 25 #endif // _WIN32 | |
| 26 | |
| 27 // Include automatically generated configuration file if running autoconf. | |
| 28 #ifdef HAVE_CONFIG_H | |
| 29 # include "config_auto.h" | |
| 30 #endif | |
| 31 | |
| 32 #include <allheaders.h> | |
| 33 #include "blobbox.h" | |
| 34 #include "blread.h" | |
| 35 #include "colfind.h" | |
| 36 #include "debugpixa.h" | |
| 37 #ifndef DISABLED_LEGACY_ENGINE | |
| 38 # include "equationdetect.h" | |
| 39 #endif | |
| 40 #include <tesseract/osdetect.h> | |
| 41 #include "imagefind.h" | |
| 42 #include "linefind.h" | |
| 43 #include "makerow.h" | |
| 44 #include "tabvector.h" | |
| 45 #include "tesseractclass.h" | |
| 46 #include "tessvars.h" | |
| 47 #include "textord.h" | |
| 48 #include "tordmain.h" | |
| 49 #include "wordseg.h" | |
| 50 | |
| 51 namespace tesseract { | |
| 52 | |
| 53 // Max erosions to perform in removing an enclosing circle. | |
| 54 const int kMaxCircleErosions = 8; | |
| 55 | |
| 56 // Helper to remove an enclosing circle from an image. | |
| 57 // If there isn't one, then the image will most likely get badly mangled. | |
| 58 // The returned pix must be pixDestroyed after use. nullptr may be returned | |
| 59 // if the image doesn't meet the trivial conditions that it uses to determine | |
| 60 // success. | |
| 61 static Image RemoveEnclosingCircle(Image pixs) { | |
| 62 Image pixsi = pixInvert(nullptr, pixs); | |
| 63 Image pixc = pixCreateTemplate(pixs); | |
| 64 pixSetOrClearBorder(pixc, 1, 1, 1, 1, PIX_SET); | |
| 65 pixSeedfillBinary(pixc, pixc, pixsi, 4); | |
| 66 pixInvert(pixc, pixc); | |
| 67 pixsi.destroy(); | |
| 68 Image pixt = pixs & pixc; | |
| 69 l_int32 max_count; | |
| 70 pixCountConnComp(pixt, 8, &max_count); | |
| 71 // The count has to go up before we start looking for the minimum. | |
| 72 l_int32 min_count = INT32_MAX; | |
| 73 Image pixout = nullptr; | |
| 74 for (int i = 1; i < kMaxCircleErosions; i++) { | |
| 75 pixt.destroy(); | |
| 76 pixErodeBrick(pixc, pixc, 3, 3); | |
| 77 pixt = pixs & pixc; | |
| 78 l_int32 count; | |
| 79 pixCountConnComp(pixt, 8, &count); | |
| 80 if (i == 1 || count > max_count) { | |
| 81 max_count = count; | |
| 82 min_count = count; | |
| 83 } else if (count < min_count) { | |
| 84 min_count = count; | |
| 85 pixout.destroy(); | |
| 86 pixout = pixt.copy(); // Save the best. | |
| 87 } else if (count >= min_count) { | |
| 88 break; // We have passed by the best. | |
| 89 } | |
| 90 } | |
| 91 pixt.destroy(); | |
| 92 pixc.destroy(); | |
| 93 return pixout; | |
| 94 } | |
| 95 | |
| 96 /** | |
| 97 * Segment the page according to the current value of tessedit_pageseg_mode. | |
| 98 * pix_binary_ is used as the source image and should not be nullptr. | |
| 99 * On return the blocks list owns all the constructed page layout. | |
| 100 */ | |
| 101 int Tesseract::SegmentPage(const char *input_file, BLOCK_LIST *blocks, Tesseract *osd_tess, | |
| 102 OSResults *osr) { | |
| 103 ASSERT_HOST(pix_binary_ != nullptr); | |
| 104 int width = pixGetWidth(pix_binary_); | |
| 105 int height = pixGetHeight(pix_binary_); | |
| 106 // Get page segmentation mode. | |
| 107 auto pageseg_mode = static_cast<PageSegMode>(static_cast<int>(tessedit_pageseg_mode)); | |
| 108 // If a UNLV zone file can be found, use that instead of segmentation. | |
| 109 if (!PSM_COL_FIND_ENABLED(pageseg_mode) && input_file != nullptr && input_file[0] != '\0') { | |
| 110 std::string name = input_file; | |
| 111 auto lastdot = name.find_last_of('.'); | |
| 112 if (lastdot != std::string::npos) { | |
| 113 name.resize(lastdot); | |
| 114 } | |
| 115 read_unlv_file(name, width, height, blocks); | |
| 116 } | |
| 117 if (blocks->empty()) { | |
| 118 // No UNLV file present. Work according to the PageSegMode. | |
| 119 // First make a single block covering the whole image. | |
| 120 BLOCK_IT block_it(blocks); | |
| 121 auto *block = new BLOCK("", true, 0, 0, 0, 0, width, height); | |
| 122 block->set_right_to_left(right_to_left()); | |
| 123 block_it.add_to_end(block); | |
| 124 } else { | |
| 125 // UNLV file present. Use PSM_SINGLE_BLOCK. | |
| 126 pageseg_mode = PSM_SINGLE_BLOCK; | |
| 127 } | |
| 128 // The diacritic_blobs holds noise blobs that may be diacritics. They | |
| 129 // are separated out on areas of the image that seem noisy and short-circuit | |
| 130 // the layout process, going straight from the initial partition creation | |
| 131 // right through to after word segmentation, where they are added to the | |
| 132 // rej_cblobs list of the most appropriate word. From there classification | |
| 133 // will determine whether they are used. | |
| 134 BLOBNBOX_LIST diacritic_blobs; | |
| 135 int auto_page_seg_ret_val = 0; | |
| 136 TO_BLOCK_LIST to_blocks; | |
| 137 if (PSM_OSD_ENABLED(pageseg_mode) || PSM_BLOCK_FIND_ENABLED(pageseg_mode) || | |
| 138 PSM_SPARSE(pageseg_mode)) { | |
| 139 auto_page_seg_ret_val = | |
| 140 AutoPageSeg(pageseg_mode, blocks, &to_blocks, | |
| 141 enable_noise_removal ? &diacritic_blobs : nullptr, osd_tess, osr); | |
| 142 if (pageseg_mode == PSM_OSD_ONLY) { | |
| 143 return auto_page_seg_ret_val; | |
| 144 } | |
| 145 // To create blobs from the image region bounds uncomment this line: | |
| 146 // to_blocks.clear(); // Uncomment to go back to the old mode. | |
| 147 } else { | |
| 148 deskew_ = FCOORD(1.0f, 0.0f); | |
| 149 reskew_ = FCOORD(1.0f, 0.0f); | |
| 150 if (pageseg_mode == PSM_CIRCLE_WORD) { | |
| 151 Image pixcleaned = RemoveEnclosingCircle(pix_binary_); | |
| 152 if (pixcleaned != nullptr) { | |
| 153 pix_binary_.destroy(); | |
| 154 pix_binary_ = pixcleaned; | |
| 155 } | |
| 156 } | |
| 157 } | |
| 158 | |
| 159 if (auto_page_seg_ret_val < 0) { | |
| 160 return -1; | |
| 161 } | |
| 162 | |
| 163 if (blocks->empty()) { | |
| 164 if (textord_debug_tabfind) { | |
| 165 tprintf("Empty page\n"); | |
| 166 } | |
| 167 return 0; // AutoPageSeg found an empty page. | |
| 168 } | |
| 169 bool splitting = pageseg_devanagari_split_strategy != ShiroRekhaSplitter::NO_SPLIT; | |
| 170 bool cjk_mode = textord_use_cjk_fp_model; | |
| 171 | |
| 172 textord_.TextordPage(pageseg_mode, reskew_, width, height, pix_binary_, pix_thresholds_, | |
| 173 pix_grey_, splitting || cjk_mode, &diacritic_blobs, blocks, &to_blocks, &gradient_); | |
| 174 return auto_page_seg_ret_val; | |
| 175 } | |
| 176 | |
| 177 /** | |
| 178 * Auto page segmentation. Divide the page image into blocks of uniform | |
| 179 * text linespacing and images. | |
| 180 * | |
| 181 * Resolution (in ppi) is derived from the input image. | |
| 182 * | |
| 183 * The output goes in the blocks list with corresponding TO_BLOCKs in the | |
| 184 * to_blocks list. | |
| 185 * | |
| 186 * If !PSM_COL_FIND_ENABLED(pageseg_mode), then no attempt is made to divide | |
| 187 * the image into columns, but multiple blocks are still made if the text is | |
| 188 * of non-uniform linespacing. | |
| 189 * | |
| 190 * If diacritic_blobs is non-null, then diacritics/noise blobs, that would | |
| 191 * confuse layout analysis by causing textline overlap, are placed there, | |
| 192 * with the expectation that they will be reassigned to words later and | |
| 193 * noise/diacriticness determined via classification. | |
| 194 * | |
| 195 * If osd (orientation and script detection) is true then that is performed | |
| 196 * as well. If only_osd is true, then only orientation and script detection is | |
| 197 * performed. If osd is desired, (osd or only_osd) then osr_tess must be | |
| 198 * another Tesseract that was initialized especially for osd, and the results | |
| 199 * will be output into osr (orientation and script result). | |
| 200 */ | |
| 201 int Tesseract::AutoPageSeg(PageSegMode pageseg_mode, BLOCK_LIST *blocks, TO_BLOCK_LIST *to_blocks, | |
| 202 BLOBNBOX_LIST *diacritic_blobs, Tesseract *osd_tess, OSResults *osr) { | |
| 203 Image photomask_pix = nullptr; | |
| 204 Image musicmask_pix = nullptr; | |
| 205 // The blocks made by the ColumnFinder. Moved to blocks before return. | |
| 206 BLOCK_LIST found_blocks; | |
| 207 TO_BLOCK_LIST temp_blocks; | |
| 208 | |
| 209 ColumnFinder *finder = SetupPageSegAndDetectOrientation( | |
| 210 pageseg_mode, blocks, osd_tess, osr, &temp_blocks, &photomask_pix, | |
| 211 pageseg_apply_music_mask ? &musicmask_pix : nullptr); | |
| 212 int result = 0; | |
| 213 if (finder != nullptr) { | |
| 214 TO_BLOCK_IT to_block_it(&temp_blocks); | |
| 215 TO_BLOCK *to_block = to_block_it.data(); | |
| 216 if (musicmask_pix != nullptr) { | |
| 217 // TODO(rays) pass the musicmask_pix into FindBlocks and mark music | |
| 218 // blocks separately. For now combine with photomask_pix. | |
| 219 photomask_pix |= musicmask_pix; | |
| 220 } | |
| 221 #ifndef DISABLED_LEGACY_ENGINE | |
| 222 if (equ_detect_) { | |
| 223 finder->SetEquationDetect(equ_detect_); | |
| 224 } | |
| 225 #endif // ndef DISABLED_LEGACY_ENGINE | |
| 226 result = finder->FindBlocks(pageseg_mode, scaled_color_, scaled_factor_, to_block, | |
| 227 photomask_pix, pix_thresholds_, pix_grey_, &pixa_debug_, | |
| 228 &found_blocks, diacritic_blobs, to_blocks); | |
| 229 if (result >= 0) { | |
| 230 finder->GetDeskewVectors(&deskew_, &reskew_); | |
| 231 } | |
| 232 delete finder; | |
| 233 } | |
| 234 photomask_pix.destroy(); | |
| 235 musicmask_pix.destroy(); | |
| 236 if (result < 0) { | |
| 237 return result; | |
| 238 } | |
| 239 | |
| 240 blocks->clear(); | |
| 241 BLOCK_IT block_it(blocks); | |
| 242 // Move the found blocks to the input/output blocks. | |
| 243 block_it.add_list_after(&found_blocks); | |
| 244 return result; | |
| 245 } | |
| 246 | |
| 247 // Helper adds all the scripts from sid_set converted to ids from osd_set to | |
| 248 // allowed_ids. | |
| 249 static void AddAllScriptsConverted(const UNICHARSET &sid_set, const UNICHARSET &osd_set, | |
| 250 std::vector<int> *allowed_ids) { | |
| 251 for (int i = 0; i < sid_set.get_script_table_size(); ++i) { | |
| 252 if (i != sid_set.null_sid()) { | |
| 253 const char *script = sid_set.get_script_from_script_id(i); | |
| 254 allowed_ids->push_back(osd_set.get_script_id_from_name(script)); | |
| 255 } | |
| 256 } | |
| 257 } | |
| 258 | |
| 259 /** | |
| 260 * Sets up auto page segmentation, determines the orientation, and corrects it. | |
| 261 * Somewhat arbitrary chunk of functionality, factored out of AutoPageSeg to | |
| 262 * facilitate testing. | |
| 263 * photo_mask_pix is a pointer to a nullptr pointer that will be filled on | |
| 264 * return with the leptonica photo mask, which must be pixDestroyed by the | |
| 265 * caller. to_blocks is an empty list that will be filled with (usually a | |
| 266 * single) block that is used during layout analysis. This ugly API is required | |
| 267 * because of the possibility of a unlv zone file. | |
| 268 * TODO(rays) clean this up. | |
| 269 * See AutoPageSeg for other arguments. | |
| 270 * The returned ColumnFinder must be deleted after use. | |
| 271 */ | |
| 272 ColumnFinder *Tesseract::SetupPageSegAndDetectOrientation(PageSegMode pageseg_mode, | |
| 273 BLOCK_LIST *blocks, Tesseract *osd_tess, | |
| 274 OSResults *osr, TO_BLOCK_LIST *to_blocks, | |
| 275 Image *photo_mask_pix, | |
| 276 Image *music_mask_pix) { | |
| 277 int vertical_x = 0; | |
| 278 int vertical_y = 1; | |
| 279 TabVector_LIST v_lines; | |
| 280 TabVector_LIST h_lines; | |
| 281 ICOORD bleft(0, 0); | |
| 282 | |
| 283 ASSERT_HOST(pix_binary_ != nullptr); | |
| 284 if (tessedit_dump_pageseg_images) { | |
| 285 pixa_debug_.AddPix(pix_binary_, "PageSegInput"); | |
| 286 } | |
| 287 // Leptonica is used to find the rule/separator lines in the input. | |
| 288 LineFinder::FindAndRemoveLines(source_resolution_, textord_tabfind_show_vlines, pix_binary_, | |
| 289 &vertical_x, &vertical_y, music_mask_pix, &v_lines, &h_lines); | |
| 290 if (tessedit_dump_pageseg_images) { | |
| 291 pixa_debug_.AddPix(pix_binary_, "NoLines"); | |
| 292 } | |
| 293 // Leptonica is used to find a mask of the photo regions in the input. | |
| 294 *photo_mask_pix = ImageFind::FindImages(pix_binary_, &pixa_debug_); | |
| 295 if (tessedit_dump_pageseg_images) { | |
| 296 Image pix_no_image_ = nullptr; | |
| 297 if (*photo_mask_pix != nullptr) { | |
| 298 pix_no_image_ = pixSubtract(nullptr, pix_binary_, *photo_mask_pix); | |
| 299 } else { | |
| 300 pix_no_image_ = pix_binary_.clone(); | |
| 301 } | |
| 302 pixa_debug_.AddPix(pix_no_image_, "NoImages"); | |
| 303 pix_no_image_.destroy(); | |
| 304 } | |
| 305 if (!PSM_COL_FIND_ENABLED(pageseg_mode)) { | |
| 306 v_lines.clear(); | |
| 307 } | |
| 308 | |
| 309 // The rest of the algorithm uses the usual connected components. | |
| 310 textord_.find_components(pix_binary_, blocks, to_blocks); | |
| 311 | |
| 312 TO_BLOCK_IT to_block_it(to_blocks); | |
| 313 // There must be exactly one input block. | |
| 314 // TODO(rays) handle new textline finding with a UNLV zone file. | |
| 315 ASSERT_HOST(to_blocks->singleton()); | |
| 316 TO_BLOCK *to_block = to_block_it.data(); | |
| 317 TBOX blkbox = to_block->block->pdblk.bounding_box(); | |
| 318 ColumnFinder *finder = nullptr; | |
| 319 int estimated_resolution = source_resolution_; | |
| 320 if (source_resolution_ == kMinCredibleResolution) { | |
| 321 // Try to estimate resolution from typical body text size. | |
| 322 int res = IntCastRounded(to_block->line_size * kResolutionEstimationFactor); | |
| 323 if (res > estimated_resolution && res < kMaxCredibleResolution) { | |
| 324 estimated_resolution = res; | |
| 325 tprintf("Estimating resolution as %d\n", estimated_resolution); | |
| 326 } | |
| 327 } | |
| 328 | |
| 329 if (to_block->line_size >= 2) { | |
| 330 finder = new ColumnFinder(static_cast<int>(to_block->line_size), blkbox.botleft(), | |
| 331 blkbox.topright(), estimated_resolution, textord_use_cjk_fp_model, | |
| 332 textord_tabfind_aligned_gap_fraction, &v_lines, &h_lines, vertical_x, | |
| 333 vertical_y); | |
| 334 | |
| 335 finder->SetupAndFilterNoise(pageseg_mode, *photo_mask_pix, to_block); | |
| 336 | |
| 337 #ifndef DISABLED_LEGACY_ENGINE | |
| 338 if (equ_detect_) { | |
| 339 equ_detect_->LabelSpecialText(to_block); | |
| 340 } | |
| 341 #endif | |
| 342 | |
| 343 BLOBNBOX_CLIST osd_blobs; | |
| 344 // osd_orientation is the number of 90 degree rotations to make the | |
| 345 // characters upright. (See tesseract/osdetect.h for precise definition.) | |
| 346 // We want the text lines horizontal, (vertical text indicates vertical | |
| 347 // textlines) which may conflict (eg vertically written CJK). | |
| 348 int osd_orientation = 0; | |
| 349 bool vertical_text = | |
| 350 textord_tabfind_force_vertical_text || pageseg_mode == PSM_SINGLE_BLOCK_VERT_TEXT; | |
| 351 if (!vertical_text && textord_tabfind_vertical_text && PSM_ORIENTATION_ENABLED(pageseg_mode)) { | |
| 352 vertical_text = finder->IsVerticallyAlignedText(textord_tabfind_vertical_text_ratio, to_block, | |
| 353 &osd_blobs); | |
| 354 } | |
| 355 | |
| 356 #ifndef DISABLED_LEGACY_ENGINE | |
| 357 if (PSM_OSD_ENABLED(pageseg_mode) && osd_tess != nullptr && osr != nullptr) { | |
| 358 std::vector<int> osd_scripts; | |
| 359 if (osd_tess != this) { | |
| 360 // We are running osd as part of layout analysis, so constrain the | |
| 361 // scripts to those allowed by *this. | |
| 362 AddAllScriptsConverted(unicharset, osd_tess->unicharset, &osd_scripts); | |
| 363 for (auto &lang : sub_langs_) { | |
| 364 AddAllScriptsConverted(lang->unicharset, osd_tess->unicharset, &osd_scripts); | |
| 365 } | |
| 366 } | |
| 367 os_detect_blobs(&osd_scripts, &osd_blobs, osr, osd_tess); | |
| 368 if (pageseg_mode == PSM_OSD_ONLY) { | |
| 369 delete finder; | |
| 370 return nullptr; | |
| 371 } | |
| 372 osd_orientation = osr->best_result.orientation_id; | |
| 373 double osd_score = osr->orientations[osd_orientation]; | |
| 374 double osd_margin = min_orientation_margin * 2; | |
| 375 for (int i = 0; i < 4; ++i) { | |
| 376 if (i != osd_orientation && osd_score - osr->orientations[i] < osd_margin) { | |
| 377 osd_margin = osd_score - osr->orientations[i]; | |
| 378 } | |
| 379 } | |
| 380 int best_script_id = osr->best_result.script_id; | |
| 381 const char *best_script_str = osd_tess->unicharset.get_script_from_script_id(best_script_id); | |
| 382 bool cjk = best_script_id == osd_tess->unicharset.han_sid() || | |
| 383 best_script_id == osd_tess->unicharset.hiragana_sid() || | |
| 384 best_script_id == osd_tess->unicharset.katakana_sid() || | |
| 385 strcmp("Japanese", best_script_str) == 0 || | |
| 386 strcmp("Korean", best_script_str) == 0 || strcmp("Hangul", best_script_str) == 0; | |
| 387 if (cjk) { | |
| 388 finder->set_cjk_script(true); | |
| 389 } | |
| 390 if (osd_margin < min_orientation_margin) { | |
| 391 // The margin is weak. | |
| 392 if (!cjk && !vertical_text && osd_orientation == 2) { | |
| 393 // upside down latin text is improbable with such a weak margin. | |
| 394 tprintf( | |
| 395 "OSD: Weak margin (%.2f), horiz textlines, not CJK: " | |
| 396 "Don't rotate.\n", | |
| 397 osd_margin); | |
| 398 osd_orientation = 0; | |
| 399 } else { | |
| 400 tprintf( | |
| 401 "OSD: Weak margin (%.2f) for %d blob text block, " | |
| 402 "but using orientation anyway: %d\n", | |
| 403 osd_margin, osd_blobs.length(), osd_orientation); | |
| 404 } | |
| 405 } | |
| 406 } | |
| 407 #endif // ndef DISABLED_LEGACY_ENGINE | |
| 408 | |
| 409 osd_blobs.shallow_clear(); | |
| 410 finder->CorrectOrientation(to_block, vertical_text, osd_orientation); | |
| 411 } | |
| 412 | |
| 413 return finder; | |
| 414 } | |
| 415 | |
| 416 } // namespace tesseract. |
