Bug: TextClassifier ONNX session is eagerly loaded even when Global.use_cls is set to False
问题描述 / Problem Description
When Global.use_cls is set to False, the TextClassifier engine (and its ONNX session) is still constructed and loaded unconditionally during RapidOCR.__init__. The use_cls=False flag only skips the inference call - it does not skip model loading.
The TextClassifier module (ch_ppocr_cls) exists solely to classify text line orientation and perform 180-degree rotation correction (label_list: ["0", "180"]). This feature is not necessary for every OCR scenario (e.g. document scanning, UI screenshots, or standard upright text reading). For consumers that disable the angle classifier, the cls ONNX model is still loaded into memory on every engine init despite never being called.
Evidence
1. TextClassifier is constructed unconditionally in _initialize:
python/rapidocr/main.py:73-76
self.use_cls = cfg.Global.use_cls
cfg.Cls.engine_cfg = cfg.EngineConfig[cfg.Cls.engine_type.value]
cfg.Cls.model_root_dir = cfg.Global.model_root_dir
self.text_cls = TextClassifier(cfg.Cls) # no `if self.use_cls` guard2. Construction immediately loads the ONNX session (no lazy loading):
python/rapidocr/ch_ppocr_cls/main.py:40
self.session = get_engine(cfg.engine_type)(cfg) # -> InferenceSession(model_path, ...)3. use_cls is only consumed as an inference-call guard, never as a load guard:
python/rapidocr/main.py:137
if self.use_cls:
cls_img_list, cls_res = self.cls_and_rotate(cropped_img_list)
else:
cls_img_list = cropped_img_listself.text_cls is referenced in exactly two places across the package - the constructor (main.py:76) and cls_and_rotate (main.py:320) - and the latter is only reachable under if self.use_cls:. So when use_cls=False, the text_cls attribute is never accessed; loading the model serves no purpose.
4. Internal inconsistency - download_models does respect use_cls:
python/rapidocr/utils/download_models.py:35,54
use_cls = cfg.Global.get("use_cls", True)
...
if use_cls:
# only downloads the cls model when use_cls is Truedownload_models treats "skip cls when use_cls=False" as expected behavior, but _initialize does not - the two code paths disagree about what use_cls=False means. As a consequence, if the cls model file is absent from disk (e.g. in an offline environment after running download_models with use_cls=False), initializing RapidOCR(params={"Global.use_cls": False}) still tries to load/download it (see Impact below).
5. Upstream reference - PaddleOCR's TextSystem guards cls construction:
RapidOCR's ch_ppocr_* pipeline is ported from PaddleOCR, whose TextSystem.__init__ constructs det and rec unconditionally but guards cls:
tools/infer/predict_system.py:53-58 (PaddleOCR)
self.text_detector = predict_det.TextDetector(args)
self.text_recognizer = predict_rec.TextRecognizer(args)
self.use_angle_cls = args.use_angle_cls
...
if self.use_angle_cls:
self.text_classifier = predict_cls.TextClassifier(args)Note the guard is at construction time (in __init__), not just at the inference-call site - so PaddleOCR does not load the cls model when use_angle_cls=False. RapidOCR's ch_ppocr_cls/main.py:40 loads the ONNX session eagerly inside TextClassifier.__init__ (via InferenceSession(...)), so its unconditional construction in _initialize has the opposite effect.
Impact
- Memory / startup cost: every
use_cls=Falseconsumer still loads the cls ONNX session at init - resident memory and startup time spent on a model that is never called. This is particularly relevant for long-running / memory-constrained deployments (tray apps, embedded, mobile). - Breaks packaged builds that omit the cls model: it's reasonable for a packager to assume
use_cls=Falsemeans the cls ONNX can be omitted from the bundle. But since_initializestill constructsTextClassifier->OrtInferSession(ch_ppocr_cls/main.py:40), andDownloadFileonly skips when the file already exists and its SHA256 matches (utils/download_file.py:37), the missing file triggers a download attempt at init (inference_engine/onnxruntime/main.py:45-61). This was reproduced in a packaged desktop build: with the cls ONNX stripped from the package under the assumption above, the installed app - running from a read-only install path (C:\Program Files\WindowsApps\...) - hitPermissionError: [Errno 13] Permission deniedwhen rapidocr tried to write the downloaded file back into that path. The crash occurs inTextClassifier.__init__, so the engine singleton never gets created, and every OCR call re-attempts init and re-fails - OCR is broken entirely, not just at startup. (The same path would also fail in a genuinely offline environment, where the download itself can't complete.)
运行环境 / Runtime Environment
- RapidOCR: latest
main(095232a chore: update default_models url) - Python: 3.13
- onnxruntime: 1.20.0 (CPU)
- Engine: onnxruntime only
复现代码 / Reproduction Code
from rapidocr import RapidOCR
# cls disabled
engine = RapidOCR(params={"Global.use_cls": False})
# -> TextClassifier.__init__ still runs -> OrtInferSession still loads cls.onnx
engine(img) # cls inference is correctly skipped, but the session was already loaded可能解决方案 / Possible solutions
A guard at construction time would conflict with the per-call use_cls argument, which is the crux of the issue:
python/rapidocr/main.py:95-116
def __call__(self, img_content, ..., use_cls: Optional[bool] = None, ...):
self.update_params(use_cls=use_cls, ...)update_params (main.py:255-279) just does setattr(self, "use_cls", value) without (re)constructing self.text_cls, so an instance initialized with use_cls=False can still be called with use_cls=True later. If cls were not loaded at init, that later call would hit a missing self.text_cls. So the fix depends on whether per-call use_cls toggling on an existing instance is intended behavior.
For reference, PaddleOCR's TextSystem - the upstream that RapidOCR's ch_ppocr_* pipeline is ported from - resolves this by guarding at construction time (see Evidence 5) while still accepting a per-call cls argument. In TextSystem.__call__(self, img, cls=True, ...), the cls parameter is used only to skip cls for that call, not to load a model that wasn't constructed at init. That is, construction-time guarding and a per-call toggle coexist there, because the toggle only narrows what runs, never expands what was loaded. The same approach would make use_cls=False actually skip the load without breaking the per-call API.
Source: RapidAI/RapidOCR