Skip to content

Commit 4f5c268

Browse files
committed
feat: optional deps handling for ONNX runtime
This makes it possible to load docling with ONNX without needing torch/transformer deps: - Moved the module-level torch and transformers imports down into the load methods. - Fixed device selection to gracefully fall back to CPU when torch isn't around - Wrapped engine registration in a guarded loader. If an optional backend (like TableFormer v2) is missing, we just skip it now instead of crashing the factory setup. - Added a numpy image processor fallback for ONNX. This handles the RT-DETR layout presets without needing torch. Output is identical to the transformers version. Signed-off-by: Amin Ya <aminyahyaabadi74@gmail.com>
1 parent 0bdc78d commit 4f5c268

4 files changed

Lines changed: 258 additions & 56 deletions

File tree

docling/models/inference_engines/common/hf_vision_base.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from __future__ import annotations
44

5+
import json
56
import logging
67
from numbers import Integral, Real
78
from pathlib import Path
8-
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
9+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
910

1011
import numpy as np
12+
from PIL import Image
1113

1214
from docling.datamodel.accelerator_options import AcceleratorOptions
1315
from docling.models.inference_engines.vlm._utils import resolve_model_artifacts_path
@@ -21,6 +23,82 @@
2123
_log = logging.getLogger(__name__)
2224

2325

26+
class NumpyImageProcessor:
27+
"""Dependency-light, torch-free stand-in for a transformers image processor.
28+
29+
Reproduces the resize/rescale/normalize preprocessing described by a
30+
``preprocessor_config.json`` using only numpy + Pillow and returns
31+
``{"pixel_values": np.ndarray}`` shaped ``(N, C, H, W)``. It exists so the ONNX
32+
Runtime object-detection engine can preprocess inputs in environments without
33+
torch/torchvision: transformers >= 5 gates its image-processor classes behind
34+
those backends, so ``AutoImageProcessor.from_pretrained`` is unavailable there.
35+
36+
For the default docling layout presets (RT-DETR: resize to a fixed square, no
37+
rescale/normalize) the output is byte-identical to the transformers slow image
38+
processor.
39+
"""
40+
41+
# PIL resampling filter for the integer codes stored by transformers/PIL.
42+
_RESAMPLE = {
43+
0: Image.Resampling.NEAREST,
44+
1: Image.Resampling.LANCZOS,
45+
2: Image.Resampling.BILINEAR,
46+
3: Image.Resampling.BICUBIC,
47+
4: Image.Resampling.BOX,
48+
5: Image.Resampling.HAMMING,
49+
}
50+
51+
def __init__(self, config: Dict[str, Any]) -> None:
52+
self.size: Optional[Dict[str, int]] = config.get("size")
53+
self.do_resize: bool = bool(config.get("do_resize", False))
54+
self.do_rescale: bool = bool(config.get("do_rescale", False))
55+
self.do_normalize: bool = bool(config.get("do_normalize", False))
56+
self.rescale_factor: float = float(config.get("rescale_factor", 1.0 / 255.0))
57+
self.image_mean: Optional[List[float]] = config.get("image_mean")
58+
self.image_std: Optional[List[float]] = config.get("image_std")
59+
self.resample = self._RESAMPLE.get(
60+
int(config.get("resample", 2)), Image.Resampling.BILINEAR
61+
)
62+
63+
@classmethod
64+
def from_config_file(cls, path: Path) -> "NumpyImageProcessor":
65+
with path.open(encoding="utf-8") as handle:
66+
return cls(json.load(handle))
67+
68+
def _target_hw(self) -> Optional[Tuple[int, int]]:
69+
size = self.size or {}
70+
if "height" in size and "width" in size:
71+
return int(size["height"]), int(size["width"])
72+
edge = size.get("shortest_edge") or size.get("longest_edge")
73+
if edge is not None:
74+
return int(edge), int(edge)
75+
return None
76+
77+
def __call__(
78+
self, images: Any, return_tensors: str = "np", **_: Any
79+
) -> Dict[str, np.ndarray]:
80+
if not isinstance(images, (list, tuple)):
81+
images = [images]
82+
target = self._target_hw()
83+
frames: List[np.ndarray] = []
84+
for image in images:
85+
pil = image.convert("RGB")
86+
if self.do_resize and target is not None:
87+
pil = pil.resize((target[1], target[0]), resample=self.resample)
88+
frames.append(np.asarray(pil))
89+
batch = np.stack(frames, axis=0) # (N, H, W, C), uint8
90+
if self.do_rescale or self.do_normalize:
91+
batch = batch.astype(np.float32)
92+
if self.do_rescale:
93+
batch = batch * self.rescale_factor
94+
if self.do_normalize and self.image_mean is not None and self.image_std is not None:
95+
mean = np.asarray(self.image_mean, dtype=np.float32)
96+
std = np.asarray(self.image_std, dtype=np.float32)
97+
batch = (batch - mean) / std
98+
pixel_values = np.ascontiguousarray(batch.transpose(0, 3, 1, 2))
99+
return {"pixel_values": pixel_values}
100+
101+
24102
class HfVisionModelMixin(HuggingFaceModelDownloadMixin):
25103
"""Shared utility mixin for HF vision model loading and label conversion."""
26104

@@ -85,6 +163,17 @@ def _load_preprocessor(self, model_folder: Path) -> BaseImageProcessor:
85163

86164
_log.debug("Loading image processor from %s", model_folder)
87165
return AutoImageProcessor.from_pretrained(str(model_folder))
166+
except ImportError:
167+
# transformers >= 5 gates its image-processor classes behind the
168+
# torch/torchvision backends. In a torch-free environment the ONNX
169+
# object-detection engine still needs preprocessing, so fall back to a
170+
# dependency-light numpy processor built from preprocessor_config.json.
171+
_log.info(
172+
"transformers image processor unavailable (torch-free environment); "
173+
"using numpy preprocessing fallback for %s",
174+
model_folder,
175+
)
176+
return NumpyImageProcessor.from_config_file(preprocessor_config)
88177
except Exception as exc:
89178
raise RuntimeError(
90179
f"Failed to load image processor from {model_folder}: {exc}"

docling/models/plugins/defaults.py

Lines changed: 152 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1-
def ocr_engines():
1+
from __future__ import annotations
2+
3+
from contextlib import suppress
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
# Experimental table crops layout models
8+
from docling.experimental.models.table_crops_layout_model import (
9+
TableCropsLayoutModel,
10+
)
11+
12+
# Layout models
13+
from docling.models.stages.layout.layout_model import LayoutModel
14+
from docling.models.stages.layout.layout_object_detection_model import (
15+
LayoutObjectDetectionModel,
16+
)
17+
18+
# OCR models
219
from docling.models.stages.ocr.auto_ocr_model import OcrAutoModel
320
from docling.models.stages.ocr.easyocr_model import EasyOcrModel
421
from docling.models.stages.ocr.kserve_v2_ocr_model import KserveV2OcrModel
@@ -7,60 +24,19 @@ def ocr_engines():
724
from docling.models.stages.ocr.rapid_ocr_model import RapidOcrModel
825
from docling.models.stages.ocr.tesseract_ocr_cli_model import TesseractOcrCliModel
926
from docling.models.stages.ocr.tesseract_ocr_model import TesseractOcrModel
10-
11-
return {
12-
"ocr_engines": [
13-
OcrAutoModel,
14-
EasyOcrModel,
15-
KserveV2OcrModel,
16-
NemotronOcrModel,
17-
OcrMacModel,
18-
RapidOcrModel,
19-
TesseractOcrModel,
20-
TesseractOcrCliModel,
21-
]
22-
}
23-
24-
25-
def picture_description():
2627
from docling.models.stages.picture_description.picture_description_api_model import (
2728
PictureDescriptionApiModel,
2829
)
30+
31+
# Picture description models
2932
from docling.models.stages.picture_description.picture_description_vlm_engine_model import (
3033
PictureDescriptionVlmEngineModel,
3134
)
3235
from docling.models.stages.picture_description.picture_description_vlm_model import (
3336
PictureDescriptionVlmModel,
3437
)
3538

36-
return {
37-
"picture_description": [
38-
PictureDescriptionVlmEngineModel, # New engine-based (preferred)
39-
PictureDescriptionVlmModel, # Legacy direct transformers
40-
PictureDescriptionApiModel, # API-based
41-
]
42-
}
43-
44-
45-
def layout_engines():
46-
from docling.experimental.models.table_crops_layout_model import (
47-
TableCropsLayoutModel,
48-
)
49-
from docling.models.stages.layout.layout_model import LayoutModel
50-
from docling.models.stages.layout.layout_object_detection_model import (
51-
LayoutObjectDetectionModel,
52-
)
53-
54-
return {
55-
"layout_engines": [
56-
LayoutObjectDetectionModel,
57-
LayoutModel,
58-
TableCropsLayoutModel,
59-
]
60-
}
61-
62-
63-
def table_structure_engines():
39+
# Table structure models
6440
from docling.models.stages.table_structure.table_structure_model import (
6541
TableStructureModel,
6642
)
@@ -71,10 +47,137 @@ def table_structure_engines():
7147
TableStructureModelV2,
7248
)
7349

74-
return {
75-
"table_structure_engines": [
50+
51+
def ocr_engines():
52+
engines: list[
53+
type[OcrAutoModel]
54+
| type[EasyOcrModel]
55+
| type[KserveV2OcrModel]
56+
| type[NemotronOcrModel]
57+
| type[OcrMacModel]
58+
| type[RapidOcrModel]
59+
| type[TesseractOcrModel]
60+
| type[TesseractOcrCliModel]
61+
] = []
62+
63+
with suppress(ImportError):
64+
from docling.models.stages.ocr.auto_ocr_model import OcrAutoModel
65+
66+
engines.append(OcrAutoModel)
67+
with suppress(ImportError):
68+
from docling.models.stages.ocr.easyocr_model import EasyOcrModel
69+
70+
engines.append(EasyOcrModel)
71+
with suppress(ImportError):
72+
from docling.models.stages.ocr.kserve_v2_ocr_model import KserveV2OcrModel
73+
74+
engines.append(KserveV2OcrModel)
75+
with suppress(ImportError):
76+
from docling.models.stages.ocr.nemotron_ocr_model import NemotronOcrModel
77+
78+
engines.append(NemotronOcrModel)
79+
with suppress(ImportError):
80+
from docling.models.stages.ocr.ocr_mac_model import OcrMacModel
81+
82+
engines.append(OcrMacModel)
83+
with suppress(ImportError):
84+
from docling.models.stages.ocr.rapid_ocr_model import RapidOcrModel
85+
86+
engines.append(RapidOcrModel)
87+
with suppress(ImportError):
88+
from docling.models.stages.ocr.tesseract_ocr_model import TesseractOcrModel
89+
90+
engines.append(TesseractOcrModel)
91+
with suppress(ImportError):
92+
from docling.models.stages.ocr.tesseract_ocr_cli_model import (
93+
TesseractOcrCliModel,
94+
)
95+
96+
engines.append(TesseractOcrCliModel)
97+
98+
return {"ocr_engines": engines}
99+
100+
101+
def picture_description():
102+
engines: list[
103+
type[PictureDescriptionVlmEngineModel]
104+
| type[PictureDescriptionVlmModel]
105+
| type[PictureDescriptionApiModel]
106+
] = []
107+
108+
with suppress(ImportError):
109+
from docling.models.stages.picture_description.picture_description_vlm_engine_model import (
110+
PictureDescriptionVlmEngineModel,
111+
)
112+
113+
engines.append(PictureDescriptionVlmEngineModel) # New engine-based (preferred)
114+
with suppress(ImportError):
115+
from docling.models.stages.picture_description.picture_description_vlm_model import (
116+
PictureDescriptionVlmModel,
117+
)
118+
119+
engines.append(PictureDescriptionVlmModel) # Legacy direct transformers
120+
with suppress(ImportError):
121+
from docling.models.stages.picture_description.picture_description_api_model import (
122+
PictureDescriptionApiModel,
123+
)
124+
125+
engines.append(PictureDescriptionApiModel) # API-based
126+
127+
return {"picture_description": engines}
128+
129+
130+
def layout_engines():
131+
engines: list[
132+
type[LayoutObjectDetectionModel]
133+
| type[LayoutModel]
134+
| type[TableCropsLayoutModel]
135+
] = []
136+
137+
with suppress(ImportError):
138+
from docling.models.stages.layout.layout_object_detection_model import (
139+
LayoutObjectDetectionModel,
140+
)
141+
142+
engines.append(LayoutObjectDetectionModel)
143+
with suppress(ImportError):
144+
from docling.models.stages.layout.layout_model import LayoutModel
145+
146+
engines.append(LayoutModel)
147+
with suppress(ImportError):
148+
from docling.experimental.models.table_crops_layout_model import (
149+
TableCropsLayoutModel,
150+
)
151+
152+
engines.append(TableCropsLayoutModel)
153+
154+
return {"layout_engines": engines}
155+
156+
157+
def table_structure_engines():
158+
engines: list[
159+
type[TableStructureModel]
160+
| type[TableStructureModelV2]
161+
| type[GraniteVisionTableStructureModel]
162+
] = []
163+
164+
with suppress(ImportError):
165+
from docling.models.stages.table_structure.table_structure_model import (
76166
TableStructureModel,
167+
)
168+
169+
engines.append(TableStructureModel)
170+
with suppress(ImportError):
171+
from docling.models.stages.table_structure.table_structure_model_v2 import (
77172
TableStructureModelV2,
173+
)
174+
175+
engines.append(TableStructureModelV2)
176+
with suppress(ImportError):
177+
from docling.models.stages.table_structure.table_structure_model_granite_vision import (
78178
GraniteVisionTableStructureModel,
79-
]
80-
}
179+
)
180+
181+
engines.append(GraniteVisionTableStructureModel)
182+
183+
return {"table_structure_engines": engines}

docling/models/stages/chart_extraction/granite_vision.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from typing import Any, List, Literal, Optional, cast
99

1010
import pandas as pd
11-
import torch
1211
from docling_core.types.doc import (
1312
CodeLanguageLabel,
1413
DescriptionMetaField,
@@ -24,7 +23,6 @@
2423
from docling_core.types.doc.document import CodeMetaField
2524
from PIL import Image
2625
from pydantic import BaseModel
27-
from transformers import AutoModelForImageTextToText, AutoProcessor
2826

2927
from docling.datamodel.accelerator_options import AcceleratorDevice, AcceleratorOptions
3028
from docling.datamodel.base_models import ItemAndImageEnrichmentElement
@@ -200,6 +198,8 @@ class ChartExtractionModelGraniteVision(_BaseChartExtractionModelGraniteVision):
200198
_model_repo_revision = "6e1fbaae4604ecc85f4f371416d82154ca49ad67"
201199

202200
def _load_model(self, artifacts_path: Path) -> None:
201+
from transformers import AutoModelForImageTextToText, AutoProcessor
202+
203203
self._processor = AutoProcessor.from_pretrained(
204204
artifacts_path,
205205
trust_remote_code=True,
@@ -346,6 +346,9 @@ class ChartExtractionModelGraniteVisionV4(_BaseChartExtractionModelGraniteVision
346346
_model_repo_revision = "dd48e97503de471803850df70843cf9eb5da8712"
347347

348348
def _load_model(self, artifacts_path: Path) -> None:
349+
import torch
350+
from transformers import AutoModelForImageTextToText, AutoProcessor
351+
349352
with warnings.catch_warnings():
350353
warnings.filterwarnings(
351354
"ignore",

0 commit comments

Comments
 (0)