From 32dad48ebbfa76e51b54693fab6dfa686e04a66d Mon Sep 17 00:00:00 2001 From: SWHL Date: Tue, 30 Sep 2025 17:44:20 +0800 Subject: [PATCH] fix: fixed issue #555 * fix: fixed issue #555 * chore: refactor code * chore: refactor code * chore: refactor code * chore: refactor code * chore: refactor code --- python/demo.py | 3 +- python/rapidocr/__init__.py | 3 +- python/rapidocr/cal_rec_boxes/main.py | 4 +- python/rapidocr/ch_ppocr_det/main.py | 6 +- python/rapidocr/ch_ppocr_det/utils.py | 2 +- python/rapidocr/ch_ppocr_rec/main.py | 3 +- python/rapidocr/ch_ppocr_rec/typings.py | 2 +- .../inference_engine/onnxruntime/main.py | 10 +- .../onnxruntime/provider_config.py | 22 +- python/rapidocr/inference_engine/openvino.py | 7 +- python/rapidocr/inference_engine/paddle.py | 35 +-- python/rapidocr/main.py | 283 +++++++++--------- python/rapidocr/utils/__init__.py | 11 - python/rapidocr/utils/process_img.py | 46 ++- python/rapidocr/utils/vis_res.py | 18 +- .../test_files/check_return_word_len.jpeg | Bin 0 -> 37225 bytes python/tests/test_return_word.py | 15 +- 17 files changed, 248 insertions(+), 222 deletions(-) create mode 100644 python/tests/test_files/check_return_word_len.jpeg diff --git a/python/demo.py b/python/demo.py index 6a0e4e1..200c2ed 100644 --- a/python/demo.py +++ b/python/demo.py @@ -6,7 +6,8 @@ from rapidocr import RapidOCR engine = RapidOCR() img_url = "https://img1.baidu.com/it/u=3619974146,1266987475&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=516" -result = engine(img_url) +img_url = "tests/test_files/return_word_debug.jpg" +result = engine(img_url, return_word_box=True) print(result) result.vis("vis_result.jpg") diff --git a/python/rapidocr/__init__.py b/python/rapidocr/__init__.py index a81fcfd..15e216c 100644 --- a/python/rapidocr/__init__.py +++ b/python/rapidocr/__init__.py @@ -2,5 +2,6 @@ # @Author: SWHL # @Contact: liekkaskono@163.com from .main import RapidOCR -from .utils import LoadImageError, VisRes +from .utils.load_image import LoadImageError from .utils.typings import EngineType, LangCls, LangDet, LangRec, ModelType, OCRVersion +from .utils.vis_res import VisRes diff --git a/python/rapidocr/cal_rec_boxes/main.py b/python/rapidocr/cal_rec_boxes/main.py index 928312c..2b8728f 100644 --- a/python/rapidocr/cal_rec_boxes/main.py +++ b/python/rapidocr/cal_rec_boxes/main.py @@ -25,13 +25,13 @@ class CalRecBoxes: def __call__( self, imgs: List[np.ndarray], - dt_boxes: List[np.ndarray], + dt_boxes: np.ndarray, rec_res: TextRecOutput, return_single_char_box: bool = False, ) -> TextRecOutput: word_results = [] for idx, (img, box) in enumerate(zip(imgs, dt_boxes)): - if rec_res.txts is None: + if rec_res.txts is None or img.size == 0 or rec_res.word_results is None: continue h, w = img.shape[:2] diff --git a/python/rapidocr/ch_ppocr_det/main.py b/python/rapidocr/ch_ppocr_det/main.py index 146f3d5..9c2736f 100644 --- a/python/rapidocr/ch_ppocr_det/main.py +++ b/python/rapidocr/ch_ppocr_det/main.py @@ -15,7 +15,7 @@ # @Author: SWHL # @Contact: liekkaskono@163.com import time -from typing import Any, Dict, List +from typing import Any, Dict import numpy as np @@ -77,7 +77,7 @@ class TextDetector: return DetPreProcess(limit_side_len, self.limit_type, self.mean, self.std) @staticmethod - def sorted_boxes(dt_boxes: np.ndarray) -> List[np.ndarray]: + def sorted_boxes(dt_boxes: np.ndarray) -> np.ndarray: """ Sort text boxes in order from top to bottom, left to right args: @@ -100,4 +100,4 @@ class TextDetector: _boxes[j + 1] = tmp else: break - return _boxes + return np.array(_boxes) diff --git a/python/rapidocr/ch_ppocr_det/utils.py b/python/rapidocr/ch_ppocr_det/utils.py index 1311f32..8449d81 100644 --- a/python/rapidocr/ch_ppocr_det/utils.py +++ b/python/rapidocr/ch_ppocr_det/utils.py @@ -18,7 +18,7 @@ from ..utils.vis_res import VisRes class TextDetOutput: img: Optional[np.ndarray] = None boxes: Optional[np.ndarray] = None - scores: Optional[Tuple[float]] = None + scores: Optional[List[float]] = None elapse: float = 0.0 def __len__(self): diff --git a/python/rapidocr/ch_ppocr_rec/main.py b/python/rapidocr/ch_ppocr_rec/main.py index 430174a..43789f5 100644 --- a/python/rapidocr/ch_ppocr_rec/main.py +++ b/python/rapidocr/ch_ppocr_rec/main.py @@ -35,7 +35,6 @@ DEFAULT_MODEL_PATH = Path(__file__).parent.parent / "models" class TextRecognizer: def __init__(self, cfg: Dict[str, Any]): self.session = get_engine(cfg.engine_type)(cfg) - self.logger = logger # onnx has inner character, other engine get or download character_dict_path character, character_dict_path = self.get_character_dict(cfg) @@ -77,7 +76,7 @@ class TextRecognizer: file_url=dict_download_url, sha256=None, save_path=dict_path, - logger=self.logger, + logger=logger, ) ) diff --git a/python/rapidocr/ch_ppocr_rec/typings.py b/python/rapidocr/ch_ppocr_rec/typings.py index 0239d9f..a61952a 100644 --- a/python/rapidocr/ch_ppocr_rec/typings.py +++ b/python/rapidocr/ch_ppocr_rec/typings.py @@ -36,7 +36,7 @@ class TextRecInput: class TextRecOutput: imgs: Optional[List[np.ndarray]] = None txts: Optional[Tuple[str]] = None - scores: Tuple[float] = (1.0,) + scores: List[float] = field(default_factory=lambda: [1.0]) word_results: Tuple[Tuple[str, float, Optional[List[List[int]]]]] = ( ("", 1.0, None), ) diff --git a/python/rapidocr/inference_engine/onnxruntime/main.py b/python/rapidocr/inference_engine/onnxruntime/main.py index 411eacf..7ed03b7 100644 --- a/python/rapidocr/inference_engine/onnxruntime/main.py +++ b/python/rapidocr/inference_engine/onnxruntime/main.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List import numpy as np from onnxruntime import GraphOptimizationLevel, InferenceSession, SessionOptions -from ...utils import DownloadFile, DownloadFileInput +from ...utils.download_file import DownloadFile, DownloadFileInput from ...utils.log import logger from ..base import FileInfo, InferSession from .provider_config import ProviderConfig @@ -17,8 +17,6 @@ from .provider_config import ProviderConfig class OrtInferSession(InferSession): def __init__(self, cfg: Dict[str, Any]): - self.logger = logger - # support custom session (PR #451) session = cfg.get("session", None) if session is not None: @@ -27,7 +25,7 @@ class OrtInferSession(InferSession): f"Expected session to be an InferenceSession, got {type(session)}" ) - self.logger.debug("Using the provided InferenceSession for inference.") + logger.debug("Using the provided InferenceSession for inference.") self.session = session return @@ -48,11 +46,11 @@ class OrtInferSession(InferSession): file_url=model_info["model_dir"], sha256=model_info["SHA256"], save_path=model_path, - logger=self.logger, + logger=logger, ) DownloadFile.run(download_params) - self.logger.info(f"Using {model_path}") + logger.info(f"Using {model_path}") model_path = Path(model_path) self._verify_model(model_path) diff --git a/python/rapidocr/inference_engine/onnxruntime/provider_config.py b/python/rapidocr/inference_engine/onnxruntime/provider_config.py index 8b47144..deff27a 100644 --- a/python/rapidocr/inference_engine/onnxruntime/provider_config.py +++ b/python/rapidocr/inference_engine/onnxruntime/provider_config.py @@ -19,8 +19,6 @@ class EP(Enum): class ProviderConfig: def __init__(self, engine_cfg: Dict[str, Any]): - self.logger = logger - self.had_providers: List[str] = get_available_providers() self.default_provider = self.had_providers[0] @@ -37,13 +35,13 @@ class ProviderConfig: results.insert(0, (EP.CUDA_EP.value, self.cuda_ep_cfg())) if self.is_dml_available(): - self.logger.info( + logger.info( "Windows 10 or above detected, try to use DirectML as primary provider" ) results.insert(0, (EP.DIRECTML_EP.value, self.dml_ep_cfg())) if self.is_cann_available(): - self.logger.info("Try to use CANNExecutionProvider to infer") + logger.info("Try to use CANNExecutionProvider to infer") results.insert(0, (EP.CANN_EP.value, self.cann_ep_cfg())) return results @@ -79,10 +77,10 @@ class ProviderConfig: for ep, check_func in providers_to_check.items(): if check_func() and first_provider != ep.value: - self.logger.warning( + logger.warning( f"{ep.value} is available, but the inference part is automatically shifted to be executed under {first_provider}. " ) - self.logger.warning(f"The available lists are {session_providers}") + logger.warning(f"The available lists are {session_providers}") def is_cuda_available(self) -> bool: if not self.cfg_use_cuda: @@ -92,7 +90,7 @@ class ProviderConfig: if get_device() == "GPU" and CUDA_EP in self.had_providers: return True - self.logger.warning( + logger.warning( f"{CUDA_EP} is not in available providers ({self.had_providers}). Use {self.default_provider} inference by default." ) install_instructions = [ @@ -113,7 +111,7 @@ class ProviderConfig: cur_os = platform.system() if cur_os != "Windows": - self.logger.warning( + logger.warning( f"DirectML is only supported in Windows OS. The current OS is {cur_os}. Use {self.default_provider} inference by default.", ) return False @@ -123,7 +121,7 @@ class ProviderConfig: int(window_build_number_str) if window_build_number_str.isdigit() else 0 ) if window_build_number < 18362: - self.logger.warning( + logger.warning( f"DirectML is only supported in Windows 10 Build 18362 and above OS. The current Windows Build is {window_build_number}. Use {self.default_provider} inference by default.", ) return False @@ -132,7 +130,7 @@ class ProviderConfig: if DML_EP in self.had_providers: return True - self.logger.warning( + logger.warning( f"{DML_EP} is not in available providers ({self.had_providers}). Use {self.default_provider} inference by default." ) install_instructions = [ @@ -152,7 +150,7 @@ class ProviderConfig: if CANN_EP in self.had_providers: return True - self.logger.warning( + logger.warning( f"{CANN_EP} is not in available providers ({self.had_providers}). Use {self.default_provider} inference by default." ) install_instructions = [ @@ -167,4 +165,4 @@ class ProviderConfig: def print_log(self, log_list: List[str]): for log_info in log_list: - self.logger.info(log_info) + logger.info(log_info) diff --git a/python/rapidocr/inference_engine/openvino.py b/python/rapidocr/inference_engine/openvino.py index a9f0484..11b905c 100644 --- a/python/rapidocr/inference_engine/openvino.py +++ b/python/rapidocr/inference_engine/openvino.py @@ -18,7 +18,6 @@ from .base import FileInfo, InferSession class OpenVINOInferSession(InferSession): def __init__(self, cfg: DictConfig): super().__init__(cfg) - self.logger = logger core = Core() @@ -38,11 +37,11 @@ class OpenVINOInferSession(InferSession): file_url=model_info["model_dir"], sha256=model_info["SHA256"], save_path=model_path, - logger=self.logger, + logger=logger, ) DownloadFile.run(download_params) - self.logger.info(f"Using {model_path}") + logger.info(f"Using {model_path}") model_path = Path(model_path) self._verify_model(model_path) @@ -85,7 +84,7 @@ class OpenVINOInferSession(InferSession): if scheduling_core_type is not None: config["SCHEDULING_CORE_TYPE"] = str(scheduling_core_type) - self.logger.info(f"Using OpenVINO config: {config}") + logger.info(f"Using OpenVINO config: {config}") return config def __call__(self, input_content: np.ndarray) -> np.ndarray: diff --git a/python/rapidocr/inference_engine/paddle.py b/python/rapidocr/inference_engine/paddle.py index c8a5679..5ae6008 100644 --- a/python/rapidocr/inference_engine/paddle.py +++ b/python/rapidocr/inference_engine/paddle.py @@ -19,7 +19,6 @@ from .base import FileInfo, InferSession class PaddleInferSession(InferSession): def __init__(self, cfg, mode: Optional[str] = None) -> None: - self.logger = logger self.mode = mode # 设置设备特定的环境变量 @@ -37,7 +36,7 @@ class PaddleInferSession(InferSession): def _set_envs(envs): for key, val in envs.items(): os.environ[key] = val - self.logger.debug(f"{key} has been set to {val}.") + logger.debug(f"{key} has been set to {val}.") if engine_cfg.get("use_cuda", False): envs = {} @@ -97,8 +96,8 @@ class PaddleInferSession(InferSession): model_info, default_model_dir, pdiparams_name ) - self.logger.info(f"Using {pdmodel_path}") - self.logger.info(f"Using {pdiparams_path}") + logger.info(f"Using {pdmodel_path}") + logger.info(f"Using {pdiparams_path}") return pdmodel_path, pdiparams_path model_dir = Path(model_dir) @@ -109,8 +108,8 @@ class PaddleInferSession(InferSession): self._verify_model(pdmodel_path) self._verify_model(pdiparams_path) - self.logger.info(f"Using {pdmodel_path}") - self.logger.info(f"Using {pdiparams_path}") + logger.info(f"Using {pdmodel_path}") + logger.info(f"Using {pdiparams_path}") return pdmodel_path, pdiparams_path def download_model( @@ -125,7 +124,7 @@ class PaddleInferSession(InferSession): file_url=model_file_url, sha256=model_info[model_file_name], save_path=model_file_path, - logger=self.logger, + logger=logger, ) ) return model_file_path @@ -136,27 +135,27 @@ class PaddleInferSession(InferSession): if cfg.engine_cfg.use_cuda: gpu_id = self.get_infer_gpuid() if gpu_id is None: - self.logger.warning( + logger.warning( "GPU is not found in current device by nvidia-smi. Please check your device or ignore it if run on jetson." ) device_id = cfg.engine_cfg.gpu_id infer_opts.enable_use_gpu(cfg.engine_cfg.gpu_mem, device_id) - self.logger.info(f"Using GPU device with ID: {device_id}") + logger.info(f"Using GPU device with ID: {device_id}") elif cfg.engine_cfg.use_npu: npu_id = cfg.engine_cfg.get("npu_id", 0) infer_opts.enable_custom_device("npu", npu_id) - self.logger.info(f"Using NPU device with ID: {npu_id}") + logger.info(f"Using NPU device with ID: {npu_id}") else: # CPU infer_opts.disable_gpu() - self.logger.info("Using CPU device") + logger.info("Using CPU device") cpu_nums = os.cpu_count() infer_num_threads = cfg.engine_cfg.get("cpu_math_library_num_threads", -1) if infer_num_threads != -1 and 1 <= infer_num_threads <= cpu_nums: infer_opts.set_cpu_math_library_num_threads(infer_num_threads) - self.logger.info(f"Set CPU math library threads to: {infer_num_threads}") + logger.info(f"Set CPU math library threads to: {infer_num_threads}") # enable memory optim infer_opts.enable_memory_optim() @@ -174,31 +173,29 @@ class PaddleInferSession(InferSession): if cfg.engine_cfg.use_cuda: gpu_id = self.get_infer_gpuid() if gpu_id is None: - self.logger.warning( + logger.warning( "GPU is not found in current device by nvidia-smi. Please check your device or ignore it if run on jetson." ) device_id = cfg.engine_cfg.gpu_id infer_opts.enable_use_gpu(cfg.engine_cfg.gpu_mem, device_id) - self.logger.info(f"Using GPU device with ID: {device_id}") + logger.info(f"Using GPU device with ID: {device_id}") elif cfg.engine_cfg.use_npu: npu_id = cfg.engine_cfg.get("npu_id", 0) infer_opts.enable_custom_device("npu", npu_id) - self.logger.info(f"Using NPU device with ID: {npu_id}") + logger.info(f"Using NPU device with ID: {npu_id}") else: # CPU infer_opts.disable_gpu() if hasattr(infer_opts, "disable_mkldnn"): infer_opts.disable_mkldnn() - self.logger.info("Using CPU device") + logger.info("Using CPU device") cpu_nums = os.cpu_count() infer_num_threads = cfg.engine_cfg.get("cpu_math_library_num_threads", -1) if infer_num_threads != -1 and 1 <= infer_num_threads <= cpu_nums: infer_opts.set_cpu_math_library_num_threads(infer_num_threads) - self.logger.info( - f"Set CPU math library threads to: {infer_num_threads}" - ) + logger.info(f"Set CPU math library threads to: {infer_num_threads}") if hasattr(infer_opts, "enable_new_ir"): infer_opts.enable_new_ir(True) diff --git a/python/rapidocr/main.py b/python/rapidocr/main.py index b880368..544678f 100644 --- a/python/rapidocr/main.py +++ b/python/rapidocr/main.py @@ -15,18 +15,18 @@ from .ch_ppocr_cls import TextClassifier, TextClsOutput from .ch_ppocr_det import TextDetector, TextDetOutput from .ch_ppocr_rec import TextRecInput, TextRecognizer, TextRecOutput from .cli import check_install, generate_cfg -from .utils import ( - LoadImage, - RapidOCROutput, - VisRes, - add_round_letterbox, - get_padding_h, +from .utils.load_image import LoadImage +from .utils.log import logger +from .utils.output import RapidOCROutput +from .utils.parse_parameters import ParseParams +from .utils.process_img import ( + apply_vertical_padding, get_rotate_crop_image, + map_boxes_to_original, resize_image_within_bounds, ) -from .utils.log import logger -from .utils.parse_parameters import ParseParams from .utils.typings import LangRec +from .utils.vis_res import VisRes root_dir = Path(__file__).resolve().parent DEFAULT_CFG_PATH = root_dir / "config.yaml" @@ -39,7 +39,6 @@ class RapidOCR: cfg = self._load_config(config_path, params) logger.setLevel(cfg.Global.log_level.upper()) - self.logger = logger self._initialize(cfg) @@ -109,64 +108,134 @@ class RapidOCR: ori_img = self.load_img(img_content) img, op_record = self.preprocess_img(ori_img) + det_res, cls_res, rec_res, cropped_img_list = self.run_ocr_steps(img, op_record) + return self.build_final_output( + ori_img, det_res, cls_res, rec_res, cropped_img_list, op_record + ) + def run_ocr_steps(self, img: np.ndarray, op_record: Dict[str, Any]): det_res, cls_res, rec_res = TextDetOutput(), TextClsOutput(), TextRecOutput() + if self.use_det: try: - img, det_res = self.get_det_res(img, op_record) + cropped_img_list, det_res = self.detect_and_crop(img, op_record) except RapidOCRError as e: - self.logger.warning(e) - return RapidOCROutput() + logger.warning(e) + return TextDetOutput(), TextClsOutput(), TextRecOutput(), [] + else: + cropped_img_list = [img] if self.use_cls: try: - img, cls_res = self.get_cls_res(img) + cls_img_list, cls_res = self.cls_and_rotate(cropped_img_list) except RapidOCRError as e: - self.logger.warning(e) - return RapidOCROutput() + logger.warning(e) + return det_res, TextClsOutput(), TextRecOutput(), [] + else: + cls_img_list = cropped_img_list if self.use_rec: - rec_res = self.get_rec_res(img) + try: + rec_res = self.recognize_txt(cls_img_list) + except RapidOCRError as e: + logger.warning(e) + return det_res, cls_res, TextRecOutput(), [] - return self.finalize_results(ori_img, det_res, cls_res, rec_res, img, op_record) + return det_res, cls_res, rec_res, cropped_img_list - def finalize_results( + def build_final_output( self, ori_img: np.ndarray, det_res: TextDetOutput, cls_res: TextClsOutput, rec_res: TextRecOutput, - img: List[np.ndarray], + cropped_img_list: List[np.ndarray], op_record: Dict[str, Any], ) -> Union[TextDetOutput, TextClsOutput, TextRecOutput, RapidOCROutput]: - raw_h, raw_w = ori_img.shape[:2] + ori_h, ori_w = ori_img.shape[:2] + + if det_res.boxes is not None: + det_res.boxes = map_boxes_to_original( + det_res.boxes, op_record, ori_h, ori_w + ) + + # 过滤识别结果为空的值 + if ( + rec_res.txts is not None + and det_res.boxes is not None + and det_res.scores is not None + ): + empty_ids = {i for i, v in enumerate(rec_res.txts) if not v.strip()} + + det_res.boxes = np.array( + [v for i, v in enumerate(det_res.boxes) if i not in empty_ids] + ) + det_res.scores = [ + v for i, v in enumerate(det_res.scores) if i not in empty_ids + ] + + rec_res.txts = [v for i, v in enumerate(rec_res.txts) if i not in empty_ids] + rec_res.word_results = [ + v for i, v in enumerate(rec_res.word_results) if i not in empty_ids + ] + + # 仅分类结果 + if ( + det_res.boxes is None + and rec_res.txts is None + and cls_res.cls_res is not None + ): + return cls_res + + # 无有效输出 + if det_res.boxes is None and rec_res.txts is None: + return RapidOCROutput() + + # 仅识别结果(无检测) + if det_res.boxes is None and rec_res.txts is not None: + return rec_res + + # 仅检测结果(无识别) + if det_res.boxes is not None and rec_res.txts is None: + return det_res if ( self.return_word_box and det_res.boxes is not None - and all(v for v in rec_res.word_results) + and all(rec_res.word_results) ): rec_res.word_results = self.calc_word_boxes( - img, det_res, rec_res, op_record, raw_h, raw_w + cropped_img_list, det_res.boxes, rec_res, op_record, ori_h, ori_w ) - if det_res.boxes is not None: - det_res.boxes = self._get_origin_points( - det_res.boxes, op_record, raw_h, raw_w - ) - return self.get_final_res(ori_img, det_res, cls_res, rec_res) + ocr_res = RapidOCROutput( + img=ori_img, + boxes=det_res.boxes, + txts=rec_res.txts, + scores=rec_res.scores, + word_results=rec_res.word_results, + elapse_list=[det_res.elapse, cls_res.elapse, rec_res.elapse], + viser=VisRes( + text_score=self.cfg.Global.text_score, + lang_type=self.cfg.Rec.lang_type, + font_path=self.cfg.Global.font_path, + ), + ) + + ocr_res = self.filter_by_text_score(ocr_res) + return ocr_res if len(ocr_res) > 0 else RapidOCROutput() def calc_word_boxes( self, img: List[np.ndarray], - det_res: TextDetOutput, + dt_boxes: np.ndarray, rec_res: TextRecOutput, op_record: Dict[str, Any], raw_h: int, raw_w: int, ) -> Any: rec_res = self.cal_rec_boxes( - img, det_res.boxes, rec_res, self.return_single_char_box + img, dt_boxes, rec_res, self.return_single_char_box ) origin_words = [] @@ -176,8 +245,8 @@ class RapidOCR: if bbox is None: continue - origin_words_points = self._get_origin_points( - [bbox], op_record, raw_h, raw_w + origin_words_points = map_boxes_to_original( + np.array([bbox]).astype(np.float64), op_record, raw_h, raw_w ) origin_words_points = origin_words_points.astype(np.int32).tolist()[0] origin_words_item.append((txt, score, origin_words_points)) @@ -188,14 +257,14 @@ class RapidOCR: def update_params( self, - use_det, - use_cls, - use_rec, - return_word_box, - return_single_char_box, - text_score, - box_thresh, - unclip_ratio, + use_det: Optional[bool] = None, + use_cls: Optional[bool] = None, + use_rec: Optional[bool] = None, + return_word_box: bool = False, + return_single_char_box: bool = False, + text_score: float = 0.5, + box_thresh: float = 0.5, + unclip_ratio: float = 1.6, ): self.use_det = self.use_det if use_det is None else use_det self.use_cls = self.use_cls if use_cls is None else use_cls @@ -215,28 +284,30 @@ class RapidOCR: op_record["preprocess"] = {"ratio_h": ratio_h, "ratio_w": ratio_w} return img, op_record - def get_det_res( + def detect_and_crop( self, img: np.ndarray, op_record: Dict[str, Any] ) -> Tuple[List[np.ndarray], TextDetOutput]: - img, op_record = self._add_letterbox(img, op_record) + img, op_record = apply_vertical_padding( + img, op_record, self.width_height_ratio, self.min_height + ) det_res = self.text_det(img) + if det_res.boxes is None: raise RapidOCRError("The text detection result is empty") - img_list = self.get_crop_img_list(img, det_res) - return img_list, det_res + img_crop_list = self.crop_text_regions(img, det_res.boxes) + return img_crop_list, det_res - def get_crop_img_list( - self, img: np.ndarray, det_res: TextDetOutput + def crop_text_regions( + self, img: np.ndarray, det_boxes: np.ndarray ) -> List[np.ndarray]: img_crop_list = [] - for box in det_res.boxes: - tmp_box = copy.deepcopy(box) - img_crop = get_rotate_crop_image(img, tmp_box) + for box in det_boxes: + img_crop = get_rotate_crop_image(img, copy.deepcopy(box)) img_crop_list.append(img_crop) return img_crop_list - def get_cls_res( + def cls_and_rotate( self, img: List[np.ndarray] ) -> Tuple[List[np.ndarray], TextClsOutput]: cls_res = self.text_cls(img) @@ -244,112 +315,36 @@ class RapidOCR: raise RapidOCRError("The text classifier is empty") return cls_res.img_list, cls_res - def get_rec_res(self, img: List[np.ndarray]) -> TextRecOutput: + def recognize_txt(self, img: List[np.ndarray]) -> TextRecOutput: rec_input = TextRecInput(img=img, return_word_box=self.return_word_box) - return self.text_rec(rec_input) - def get_final_res( - self, - ori_img: np.ndarray, - det_res: TextDetOutput, - cls_res: TextClsOutput, - rec_res: TextRecOutput, - ) -> Union[TextDetOutput, TextClsOutput, TextRecOutput, RapidOCROutput]: - dt_boxes = det_res.boxes - txt_res = rec_res.txts + rec_res = self.text_rec(rec_input) + if rec_res.txts is None: + raise RapidOCRError("The text recognize result is empty") - if dt_boxes is None and txt_res is None and cls_res.cls_res is not None: - return cls_res - - if dt_boxes is None and txt_res is None: - return RapidOCROutput() - - if dt_boxes is None and txt_res is not None: - return rec_res - - if dt_boxes is not None and txt_res is None: - return det_res - - ocr_res = RapidOCROutput( - img=ori_img, - boxes=det_res.boxes, - txts=rec_res.txts, - scores=rec_res.scores, - word_results=rec_res.word_results, - elapse_list=[det_res.elapse, cls_res.elapse, rec_res.elapse], - viser=VisRes( - text_score=self.cfg.Global.text_score, - lang_type=self.cfg.Rec.lang_type, - font_path=self.cfg.Global.font_path, - ), - ) - ocr_res = self.filter_by_text_score(ocr_res) - if len(ocr_res) <= 0: - return RapidOCROutput() - - return ocr_res + return rec_res def filter_by_text_score(self, ocr_res: RapidOCROutput) -> RapidOCROutput: - filter_boxes, filter_txts, filter_scores = [], [], [] - for box, txt, score in zip(ocr_res.boxes, ocr_res.txts, ocr_res.scores): - if float(score) >= self.text_score: - filter_boxes.append(box) - filter_txts.append(txt) - filter_scores.append(score) + filter_boxes, filter_txts, filter_scores, filter_words = [], [], [], [] + for i, (box, txt, score) in enumerate( + zip(ocr_res.boxes, ocr_res.txts, ocr_res.scores) + ): + if score < self.text_score: + continue + + if ocr_res.word_results[i]: + filter_words.append(ocr_res.word_results[i]) + + filter_boxes.append(box) + filter_txts.append(txt) + filter_scores.append(score) ocr_res.boxes = np.array(filter_boxes) ocr_res.txts = tuple(filter_txts) ocr_res.scores = tuple(filter_scores) + ocr_res.word_results = tuple(filter_words) return ocr_res - def _add_letterbox( - self, img: np.ndarray, op_record: Dict[str, Any] - ) -> Tuple[np.ndarray, Dict[str, Any]]: - h, w = img.shape[:2] - - if self.width_height_ratio == -1: - use_limit_ratio = False - else: - use_limit_ratio = w / h > self.width_height_ratio - - if h <= self.min_height or use_limit_ratio: - padding_h = get_padding_h(h, w, self.width_height_ratio, self.min_height) - block_img = add_round_letterbox(img, (padding_h, padding_h, 0, 0)) - op_record["padding_1"] = {"top": padding_h, "left": 0} - return block_img, op_record - - op_record["padding_1"] = {"top": 0, "left": 0} - return img, op_record - - def _get_origin_points( - self, - dt_boxes: List[np.ndarray], - op_record: Dict[str, Any], - raw_h: int, - raw_w: int, - ) -> np.ndarray: - dt_boxes_array = np.array(dt_boxes).astype(np.float32) - for op in reversed(list(op_record.keys())): - v = op_record[op] - if "padding" in op: - top, left = v.get("top"), v.get("left") - dt_boxes_array[:, :, 0] -= left - dt_boxes_array[:, :, 1] -= top - elif "preprocess" in op: - ratio_h = v.get("ratio_h") - ratio_w = v.get("ratio_w") - dt_boxes_array[:, :, 0] *= ratio_w - dt_boxes_array[:, :, 1] *= ratio_h - - dt_boxes_array = np.where(dt_boxes_array < 0, 0, dt_boxes_array) - dt_boxes_array[..., 0] = np.where( - dt_boxes_array[..., 0] > raw_w, raw_w, dt_boxes_array[..., 0] - ) - dt_boxes_array[..., 1] = np.where( - dt_boxes_array[..., 1] > raw_h, raw_h, dt_boxes_array[..., 1] - ) - return dt_boxes_array - class RapidOCRError(Exception): pass diff --git a/python/rapidocr/utils/__init__.py b/python/rapidocr/utils/__init__.py index ff42bbd..0ecdd4f 100644 --- a/python/rapidocr/utils/__init__.py +++ b/python/rapidocr/utils/__init__.py @@ -1,14 +1,3 @@ # -*- encoding: utf-8 -*- # @Author: SWHL # @Contact: liekkaskono@163.com -from .download_file import DownloadFile, DownloadFileException, DownloadFileInput -from .load_image import LoadImage, LoadImageError -from .output import RapidOCROutput -from .parse_parameters import ParseParams -from .process_img import ( - add_round_letterbox, - get_padding_h, - get_rotate_crop_image, - resize_image_within_bounds, -) -from .vis_res import VisRes diff --git a/python/rapidocr/utils/process_img.py b/python/rapidocr/utils/process_img.py index 4d71249..46e8a16 100644 --- a/python/rapidocr/utils/process_img.py +++ b/python/rapidocr/utils/process_img.py @@ -1,12 +1,56 @@ # -*- encoding: utf-8 -*- # @Author: SWHL # @Contact: liekkaskono@163.com -from typing import Tuple +from typing import Any, Dict, Tuple import cv2 import numpy as np +def map_boxes_to_original( + dt_boxes: np.ndarray, op_record: Dict[str, Any], ori_h: int, ori_w: int +) -> np.ndarray: + for op in reversed(list(op_record.keys())): + v = op_record[op] + if "padding" in op: + top, left = v.get("top"), v.get("left") + dt_boxes[:, :, 0] -= left + dt_boxes[:, :, 1] -= top + elif "preprocess" in op: + ratio_h = v.get("ratio_h") + ratio_w = v.get("ratio_w") + dt_boxes[:, :, 0] *= ratio_w + dt_boxes[:, :, 1] *= ratio_h + + dt_boxes = np.where(dt_boxes < 0, 0, dt_boxes) + dt_boxes[..., 0] = np.where(dt_boxes[..., 0] > ori_w, ori_w, dt_boxes[..., 0]) + dt_boxes[..., 1] = np.where(dt_boxes[..., 1] > ori_h, ori_h, dt_boxes[..., 1]) + return dt_boxes + + +def apply_vertical_padding( + img: np.ndarray, + op_record: Dict[str, Any], + width_height_ratio: float, + min_height: float, +) -> Tuple[np.ndarray, Dict[str, Any]]: + h, w = img.shape[:2] + + if width_height_ratio == -1: + use_limit_ratio = False + else: + use_limit_ratio = w / h > width_height_ratio + + if h <= min_height or use_limit_ratio: + padding_h = get_padding_h(h, w, width_height_ratio, min_height) + block_img = add_round_letterbox(img, (padding_h, padding_h, 0, 0)) + op_record["padding_1"] = {"top": padding_h, "left": 0} + return block_img, op_record + + op_record["padding_1"] = {"top": 0, "left": 0} + return img, op_record + + def get_padding_h(h: int, w: int, width_height_ratio: float, min_height: float) -> int: new_h = max(int(w / width_height_ratio), min_height) * 2 padding_h = int(abs(new_h - h) / 2) diff --git a/python/rapidocr/utils/vis_res.py b/python/rapidocr/utils/vis_res.py index f6f3c22..f228897 100644 --- a/python/rapidocr/utils/vis_res.py +++ b/python/rapidocr/utils/vis_res.py @@ -31,8 +31,6 @@ class VisRes: lang_type: Optional[LangRec] = None, font_path: Optional[str] = None, ): - self.logger = logger - self.text_score = text_score self.load_img = LoadImage() @@ -44,9 +42,9 @@ class VisRes: img_content: InputType, dt_boxes: np.ndarray, txts: Optional[Union[List[str], Tuple[str]]] = None, - scores: Optional[Tuple[float]] = None, + scores: Optional[List[float]] = None, ) -> np.ndarray: - self.logger.info(f"Using {self.font_path} to visualize results.") + logger.info(f"Using {self.font_path} to visualize results.") if txts is None: return self.draw_dt_boxes(img_content, dt_boxes, scores) @@ -56,7 +54,7 @@ class VisRes: self, img_content: InputType, dt_boxes: np.ndarray, - scores: Optional[Tuple[float]] = None, + scores: Optional[List[float]] = None, ) -> np.ndarray: img = self.load_img(img_content) if scores is None: @@ -90,7 +88,7 @@ class VisRes: file_url=default_info["path"], sha256=default_info["SHA256"], save_path=DEFAULT_FONT_PATH, - logger=self.logger, + logger=logger, ) if lang_type is None: @@ -106,7 +104,7 @@ class VisRes: font_url, font_sha256 = font_info["path"], font_info["SHA256"] if font_url is None: - self.logger.warning( + logger.warning( "Font file for %s is not found in the supported font list. Default font file will be used.", lang_type, ) @@ -119,7 +117,7 @@ class VisRes: file_url=font_url, sha256=font_sha256, save_path=save_font_path, - logger=self.logger, + logger=logger, verbose=False, ) DownloadFile.run(input_param) @@ -131,7 +129,7 @@ class VisRes: self, imgs: Sequence[InputType], txts: Union[List[str], Tuple[str]], - scores: Tuple[float], + scores: List[float], ) -> np.ndarray: result_imgs = [] for img, txt, score in zip(imgs, txts, scores): @@ -178,7 +176,7 @@ class VisRes: img_content: InputType, dt_boxes: np.ndarray, txts: Union[List[str], Tuple[str]], - scores: Optional[Tuple[float]] = None, + scores: Optional[List[float]] = None, ) -> np.ndarray: image = Image.fromarray(self.load_img(img_content)) h, w = image.height, image.width diff --git a/python/tests/test_files/check_return_word_len.jpeg b/python/tests/test_files/check_return_word_len.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..583553cc34f087606b9112735fce0bfa6523d2fb GIT binary patch literal 37225 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%0AjRY%0scyt_s@Vv`E|J;DBOI< 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 Os_(H0<*O@FzyI0VM%o7e literal 0 HcmV?d00001 diff --git a/python/tests/test_return_word.py b/python/tests/test_return_word.py index 0814c5e..1c8cc84 100644 --- a/python/tests/test_return_word.py +++ b/python/tests/test_return_word.py @@ -13,7 +13,7 @@ sys.path.append(str(root_dir)) from rapidocr import RapidOCR -tests_dir = root_dir / "tests" / "test_files" +test_dir = root_dir / "tests" / "test_files" @pytest.fixture() @@ -22,6 +22,13 @@ def engine(): return engine +def test_txts_equal_words(engine): + img_path = test_dir / "check_return_word_len.jpeg" + result = engine(img_path, return_word_box=True) + + assert len(result.txts) == len(result.word_results) + + @mark.parametrize( "img_name,words", [ @@ -36,7 +43,7 @@ def engine(): ], ) def test_cn_word_ocr(engine, img_name: str, words: List[str]): - img_path = tests_dir / img_name + img_path = test_dir / img_name result = engine(img_path, return_word_box=True) txts, _, _ = list(zip(*result.word_results[0])) assert txts == words @@ -47,14 +54,14 @@ def test_cn_word_ocr(engine, img_name: str, words: List[str]): [("issue_170.png", "TEST"), ("return_word_debug.jpg", "3F1")], ) def test_en_word_ocr(engine, img_name: str, words: str): - img_path = tests_dir / img_name + img_path = test_dir / img_name result = engine(img_path, return_word_box=True) txts, _, _ = list(zip(*result.word_results[0])) assert txts[0] == words def test_en_return_single_char_box(engine): - img_path = tests_dir / "en.jpg" + img_path = test_dir / "en.jpg" result = engine(img_path, return_word_box=True, return_single_char_box=True) txts, _, _ = list(zip(*result.word_results[0])) assert txts[0] == "3"