fix: fixed issue #555

* fix: fixed issue #555

* chore: refactor code

* chore: refactor code

* chore: refactor code

* chore: refactor code

* chore: refactor code
This commit is contained in:
SWHL authored and GitHub committed 2025-09-30 17:44:20 +08:00
1 parent 145342f59f
commit 32dad48ebb
17 files changed
+248 -222

No files matched your search

+2 -1
View File
@@ -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")
+2 -1
View File
@@ -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
+2 -2
View File
@@ -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]
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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):
+1 -2
View File
@@ -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,
)
)
+1 -1
View File
@@ -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),
)
@@ -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)
@@ -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)
+3 -4
View File
@@ -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:
+16 -19
View File
@@ -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)
+139 -144
View File
@@ -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
-11
View File
@@ -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
+45 -1
View File
@@ -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)
+8 -10
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+11 -4
View File
@@ -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"