emelryan commited on
Commit
6f08375
·
1 Parent(s): 4c3050e

Accept torch.Tensor inputs, remove non-release scripts

Browse files

- Add torch.Tensor support (CPU/CUDA, CHW/HWC, uint8/float)
- Add pathlib.Path support for file paths

nemotron-ocr/src/nemotron_ocr/inference/pipeline_v2.py CHANGED
@@ -12,6 +12,7 @@ Extends :class:`NemotronOCR` with:
12
  """
13
 
14
  import logging
 
15
  import time
16
 
17
  import numpy as np
@@ -237,10 +238,30 @@ class NemotronOCRV2(NemotronOCR):
237
 
238
  @staticmethod
239
  def _load_image_to_tensor_uint8(image):
240
- """Load image and return a uint8 CHW tensor (no dtype conversion).
241
 
242
- Defers the expensive uint8→float16 conversion to the GPU.
 
243
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  if isinstance(image, np.ndarray):
245
  if image.ndim == 2:
246
  image = np.stack([image] * 3, axis=-1)
@@ -251,7 +272,7 @@ class NemotronOCRV2(NemotronOCR):
251
  return torch.from_numpy(image).permute(2, 0, 1)
252
 
253
  from torchvision.io import read_image, decode_image
254
- if isinstance(image, str):
255
  return read_image(str(image), mode="RGB")
256
  if isinstance(image, bytes):
257
  import base64
 
12
  """
13
 
14
  import logging
15
+ import os
16
  import time
17
 
18
  import numpy as np
 
238
 
239
  @staticmethod
240
  def _load_image_to_tensor_uint8(image):
241
+ """Load image and return a uint8 CHW tensor on CPU.
242
 
243
+ Accepts file paths, numpy arrays, torch tensors, base64 bytes,
244
+ or BytesIO objects.
245
  """
246
+ if isinstance(image, torch.Tensor):
247
+ t = image.detach().cpu()
248
+ if t.ndim == 2:
249
+ t = t.unsqueeze(0).expand(3, -1, -1)
250
+ if t.ndim == 3 and t.shape[0] in (1, 3, 4):
251
+ if t.shape[0] == 4:
252
+ t = t[:3]
253
+ if t.dtype != torch.uint8:
254
+ t = (t * 255).clamp(0, 255).to(torch.uint8) if t.is_floating_point() else t.to(torch.uint8)
255
+ return t
256
+ if t.ndim == 3 and t.shape[2] in (1, 3, 4):
257
+ t = t.permute(2, 0, 1)
258
+ if t.shape[0] == 4:
259
+ t = t[:3]
260
+ if t.dtype != torch.uint8:
261
+ t = (t * 255).clamp(0, 255).to(torch.uint8) if t.is_floating_point() else t.to(torch.uint8)
262
+ return t
263
+ raise ValueError(f"Unsupported tensor shape: {image.shape}")
264
+
265
  if isinstance(image, np.ndarray):
266
  if image.ndim == 2:
267
  image = np.stack([image] * 3, axis=-1)
 
272
  return torch.from_numpy(image).permute(2, 0, 1)
273
 
274
  from torchvision.io import read_image, decode_image
275
+ if isinstance(image, (str, os.PathLike)):
276
  return read_image(str(image), mode="RGB")
277
  if isinstance(image, bytes):
278
  import base64
run_ocr_inference.py DELETED
@@ -1,251 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Run OCR inference over a directory of images and write:
4
- - images/ : JPG copies of processed input images
5
- - visualizations/ : Annotated images with bounding boxes + text
6
- - results.json : OCR outputs for all images
7
- """
8
-
9
- import argparse
10
- import json
11
- import logging
12
- import os
13
- import random
14
- from pathlib import Path
15
-
16
- from PIL import Image, ImageDraw, ImageFont
17
- from tqdm import tqdm
18
-
19
- from nemotron_ocr.inference.pipeline import NemotronOCR
20
- from nemotron_ocr.inference.pipeline_v2 import NemotronOCRV2
21
-
22
- SCRIPT_DIR = Path(__file__).parent.resolve()
23
- FONT_PATH = SCRIPT_DIR / "NotoSansCJKsc-Regular.otf"
24
-
25
- logging.getLogger("nemotron_ocr.inference.pipeline_v2").setLevel(logging.WARNING)
26
- logging.getLogger("nemotron_ocr.inference.models.relational").setLevel(logging.WARNING)
27
-
28
-
29
- def get_font(size: int = 14):
30
- try:
31
- return ImageFont.truetype(str(FONT_PATH), size)
32
- except Exception:
33
- return ImageFont.load_default()
34
-
35
-
36
- def find_images(images_dir, extensions=(".png", ".jpg", ".jpeg", ".tiff", ".bmp")):
37
- images_dir = Path(images_dir)
38
- image_files = []
39
- for ext in extensions:
40
- image_files.extend(images_dir.glob(f"**/*{ext}"))
41
- image_files.extend(images_dir.glob(f"**/*{ext.upper()}"))
42
- return sorted(set(image_files))
43
-
44
-
45
- def save_visualization(image_path: str, predictions: list, output_path: str):
46
- pil_image = Image.open(image_path).convert("RGB")
47
- draw = ImageDraw.Draw(pil_image)
48
- font = get_font(14)
49
-
50
- img_width, img_height = pil_image.size
51
- colors = [
52
- (255, 0, 0),
53
- (0, 255, 0),
54
- (0, 0, 255),
55
- (255, 165, 0),
56
- (128, 0, 128),
57
- (0, 128, 128),
58
- (255, 192, 203),
59
- (165, 42, 42),
60
- ]
61
-
62
- for i, pred in enumerate(predictions):
63
- if isinstance(pred.get("left"), str) and pred["left"] == "nan":
64
- continue
65
-
66
- color = colors[i % len(colors)]
67
- left = int(pred["left"] * img_width)
68
- right = int(pred["right"] * img_width)
69
- upper = int(pred.get("upper", pred.get("bottom", 0)) * img_height)
70
- lower = int(pred.get("lower", pred.get("top", 0)) * img_height)
71
- top, bottom = min(upper, lower), max(upper, lower)
72
-
73
- draw.rectangle([left, top, right, bottom], outline=color, width=2)
74
-
75
- display_text = pred["text"][:50] + "..." if len(pred["text"]) > 50 else pred["text"]
76
- text_y = max(0, top - 20)
77
- try:
78
- text_bbox = draw.textbbox((left, text_y), display_text, font=font)
79
- draw.rectangle(
80
- [text_bbox[0] - 2, text_bbox[1] - 2, text_bbox[2] + 2, text_bbox[3] + 2],
81
- fill=(255, 255, 255, 200),
82
- outline=color,
83
- )
84
- draw.text((left, text_y), display_text, fill=color, font=font)
85
- except Exception:
86
- draw.text((left, text_y), display_text, fill=color, font=font)
87
-
88
- os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else ".", exist_ok=True)
89
- pil_image.save(output_path, "JPEG", quality=90)
90
-
91
-
92
- def save_image_as_jpg(image_path: str, output_path: str):
93
- pil_image = Image.open(image_path).convert("RGB")
94
- pil_image.save(output_path, "JPEG", quality=90)
95
-
96
-
97
- def _store_result(image_path: Path, predictions: list, images_output_dir: Path, viz_output_dir: Path):
98
- result_key = f"{image_path.stem}.jpg"
99
- row = {
100
- "original_path": str(image_path),
101
- "predictions": predictions,
102
- "num_detections": len(predictions),
103
- "status": "success",
104
- }
105
-
106
- dest_image_path = images_output_dir / f"{image_path.stem}.jpg"
107
- if not dest_image_path.exists():
108
- save_image_as_jpg(str(image_path), str(dest_image_path))
109
- if predictions:
110
- vis_path = viz_output_dir / f"{image_path.stem}_viz.jpg"
111
- save_visualization(str(image_path), predictions, str(vis_path))
112
- return result_key, row
113
-
114
-
115
- def main():
116
- parser = argparse.ArgumentParser(description="Run OCR inference on image directories")
117
- parser.add_argument("--model_dir", type=str, required=True, help="Path to model checkpoint directory")
118
- parser.add_argument("--images_dir", type=str, required=True, help="Path to directory with input images")
119
- parser.add_argument("--output_dir", type=str, required=True, help="Output directory")
120
- parser.add_argument("--num_samples", type=int, default=0, help="Number of images to process (0 for all)")
121
- parser.add_argument(
122
- "--merge_level",
123
- type=str,
124
- default="sentence",
125
- choices=["word", "sentence", "paragraph"],
126
- help="Text merging granularity",
127
- )
128
- parser.add_argument("--seed", type=int, default=42, help="Random seed for sampling")
129
- parser.add_argument("--include_invalid", action="store_true", help="Include regions marked invalid")
130
- parser.add_argument("--infer_length", type=int, default=1024, help="Detector resolution")
131
- parser.add_argument("--pipeline", type=str, default="v2", choices=("v1", "v2"), help="OCR pipeline version")
132
- parser.add_argument("--batch_size", type=int, default=8, help="Batch size for v2 pipeline")
133
- parser.add_argument("--verbose", action="store_true", help="Enable verbose OCR logs")
134
- args = parser.parse_args()
135
-
136
- if args.verbose:
137
- logging.getLogger("nemotron_ocr.inference.pipeline_v2").setLevel(logging.INFO)
138
-
139
- output_dir = Path(args.output_dir)
140
- images_output_dir = output_dir / "images"
141
- viz_output_dir = output_dir / "visualizations"
142
- output_dir.mkdir(parents=True, exist_ok=True)
143
- images_output_dir.mkdir(exist_ok=True)
144
- viz_output_dir.mkdir(exist_ok=True)
145
-
146
- image_files = [Path(p) for p in find_images(args.images_dir)]
147
- if not image_files:
148
- print("No images found.")
149
- return
150
-
151
- if 0 < args.num_samples < len(image_files):
152
- random.seed(args.seed)
153
- image_files = random.sample(image_files, args.num_samples)
154
-
155
- if args.pipeline == "v2":
156
- ocr = NemotronOCRV2(
157
- model_dir=args.model_dir,
158
- infer_length=args.infer_length,
159
- detector_max_batch_size=max(1, args.batch_size),
160
- )
161
- else:
162
- ocr = NemotronOCR(args.model_dir, infer_length=args.infer_length)
163
-
164
- results = {}
165
-
166
- if args.pipeline == "v2":
167
- bs = max(1, args.batch_size)
168
- for start in tqdm(range(0, len(image_files), bs), desc="Batches"):
169
- batch_paths = image_files[start:start + bs]
170
- batch_strs = [str(p) for p in batch_paths]
171
- batch_preds = None
172
- try:
173
- batch_preds = ocr(
174
- batch_strs,
175
- merge_level=args.merge_level,
176
- include_invalid=args.include_invalid,
177
- )
178
- except Exception:
179
- batch_preds = None
180
-
181
- if batch_preds is None:
182
- for image_path in batch_paths:
183
- try:
184
- predictions = ocr(
185
- str(image_path),
186
- merge_level=args.merge_level,
187
- include_invalid=args.include_invalid,
188
- )
189
- key, row = _store_result(image_path, predictions, images_output_dir, viz_output_dir)
190
- results[key] = row
191
- except Exception as e:
192
- import traceback
193
-
194
- results[f"{image_path.stem}.jpg"] = {
195
- "original_path": str(image_path),
196
- "predictions": [],
197
- "num_detections": 0,
198
- "status": "error",
199
- "error": str(e),
200
- "traceback": traceback.format_exc(),
201
- }
202
- continue
203
-
204
- for image_path, predictions in zip(batch_paths, batch_preds):
205
- try:
206
- key, row = _store_result(image_path, predictions, images_output_dir, viz_output_dir)
207
- results[key] = row
208
- except Exception as e:
209
- import traceback
210
-
211
- results[f"{image_path.stem}.jpg"] = {
212
- "original_path": str(image_path),
213
- "predictions": [],
214
- "num_detections": 0,
215
- "status": "error",
216
- "error": str(e),
217
- "traceback": traceback.format_exc(),
218
- }
219
- else:
220
- for image_path in tqdm(image_files, desc="Processing"):
221
- try:
222
- predictions = ocr(
223
- str(image_path),
224
- merge_level=args.merge_level,
225
- include_invalid=args.include_invalid,
226
- )
227
- key, row = _store_result(image_path, predictions, images_output_dir, viz_output_dir)
228
- results[key] = row
229
- except Exception as e:
230
- import traceback
231
-
232
- results[f"{image_path.stem}.jpg"] = {
233
- "original_path": str(image_path),
234
- "predictions": [],
235
- "num_detections": 0,
236
- "status": "error",
237
- "error": str(e),
238
- "traceback": traceback.format_exc(),
239
- }
240
-
241
- results_path = output_dir / "results.json"
242
- with open(results_path, "w", encoding="utf-8") as f:
243
- json.dump(results, f, ensure_ascii=False, indent=2)
244
-
245
- success_count = sum(1 for r in results.values() if r["status"] == "success")
246
- total_detections = sum(r["num_detections"] for r in results.values())
247
- print(f"Done: {success_count}/{len(results)} images, {total_detections} detections -> {output_dir}")
248
-
249
-
250
- if __name__ == "__main__":
251
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
submit_inference_viz_gpu.sh DELETED
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Submit OCR + visualization (NemotronOCRV2) on a GPU node from scratch clone.
3
- #
4
- # Usage:
5
- # ./submit_inference_viz_gpu.sh /path/to/images [OUTPUT_DIR]
6
- #
7
- # Environment (optional):
8
- # MODEL — checkpoint directory (default: $BASE/v2_multilingual)
9
- # BATCH_SIZE — v2 detector batch size (default: 16)
10
- # MERGE_LEVEL — word|sentence|paragraph (default: sentence)
11
- # SLURM_TIME — wall time in minutes (default: 120)
12
- # JOB_NAME — sbatch job name (default: infer_viz_scratch)
13
-
14
- set -euo pipefail
15
-
16
- BASE="/lustre/fsw/portfolios/datascience/users/rchesler/scratch/nemotron-ocr-v2"
17
- VENV="$BASE/nemotron-ocr/.venv"
18
- ACCOUNT="datascience_nemo_retriever"
19
- PARTITIONS="batch_block1,batch_block3,batch_block4"
20
-
21
- IMAGES="${1:?usage: $0 IMAGES_DIR [OUTPUT_DIR]}"
22
- TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
23
- OUT="${2:-$BASE/inference_output/viz_v2_${TIMESTAMP}}"
24
- MODEL="${MODEL:-$BASE/v2_multilingual}"
25
- BATCH_SIZE="${BATCH_SIZE:-16}"
26
- MERGE_LEVEL="${MERGE_LEVEL:-sentence}"
27
- SLURM_TIME="${SLURM_TIME:-120}"
28
- JOB_NAME="${JOB_NAME:-infer_viz_scratch}"
29
-
30
- LOGS="$OUT/logs"
31
- mkdir -p "$LOGS"
32
-
33
- PREAMBLE='source /etc/profile.d/modules.sh 2>/dev/null || true
34
- if command -v module >/dev/null 2>&1; then
35
- module load cuda12.2/toolkit/12.2.2 || true
36
- fi
37
- export PATH="$HOME/.local/bin:$PATH"
38
- source '"$VENV"'/bin/activate'
39
-
40
- JOB="$OUT/.job_inference_${TIMESTAMP}.sh"
41
- cat > "$JOB" << ENDSCRIPT
42
- #!/bin/bash
43
- #SBATCH --job-name=${JOB_NAME}
44
- #SBATCH --account=${ACCOUNT}
45
- #SBATCH --partition=${PARTITIONS}
46
- #SBATCH --nodes=1
47
- #SBATCH --gpus-per-node=1
48
- #SBATCH --time=${SLURM_TIME}
49
- #SBATCH --output=${LOGS}/infer_%j.out
50
- #SBATCH --error=${LOGS}/infer_%j.err
51
-
52
- set -euo pipefail
53
- ${PREAMBLE}
54
- cd ${BASE}
55
-
56
- python run_ocr_inference.py \\
57
- --model_dir "${MODEL}" \\
58
- --images_dir "${IMAGES}" \\
59
- --output_dir "${OUT}" \\
60
- --pipeline v2 \\
61
- --batch_size ${BATCH_SIZE} \\
62
- --num_samples 0 \\
63
- --merge_level ${MERGE_LEVEL}
64
-
65
- echo "Done: \$(date)"
66
- ENDSCRIPT
67
- chmod +x "$JOB"
68
-
69
- echo "============================================================"
70
- echo " Inference + viz (v2 pipeline) — GPU job (scratch clone)"
71
- echo "============================================================"
72
- echo " images: $IMAGES"
73
- echo " model: $MODEL"
74
- echo " merge: $MERGE_LEVEL"
75
- echo " output: $OUT"
76
- echo " logs: $LOGS"
77
- echo " job file: $JOB"
78
- echo "============================================================"
79
-
80
- JID=$(sbatch "$JOB" 2>&1 | sed -n 's/.* \([0-9][0-9]*\)$/\1/p' || true)
81
- if [[ -n "${JID:-}" ]]; then
82
- echo " submitted job ID: $JID"
83
- echo " tail: tail -f $LOGS/infer_${JID}.out"
84
- else
85
- echo " sbatch failed or did not return a job id" >&2
86
- exit 1
87
- fi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
submit_install_gpu.sh DELETED
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Submit a GPU-node install/build job for scratch nemotron-ocr-v2.
3
- #
4
- # Usage:
5
- # ./submit_install_gpu.sh
6
- #
7
- # Environment (optional):
8
- # SLURM_TIME — wall time in minutes (default: 45)
9
- # JOB_NAME — sbatch job name (default: install_ocr_v2)
10
-
11
- set -euo pipefail
12
-
13
- BASE="/lustre/fsw/portfolios/datascience/users/rchesler/scratch/nemotron-ocr-v2"
14
- PKG="$BASE/nemotron-ocr"
15
- ACCOUNT="datascience_nemo_retriever"
16
- PARTITIONS="batch_block1,batch_block3,batch_block4"
17
- SLURM_TIME="${SLURM_TIME:-45}"
18
- JOB_NAME="${JOB_NAME:-install_ocr_v2}"
19
-
20
- TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
21
- LOGS="$BASE/inference_output/install_logs"
22
- mkdir -p "$LOGS"
23
-
24
- JOB="$BASE/.job_install_${TIMESTAMP}.sh"
25
- cat > "$JOB" << ENDSCRIPT
26
- #!/bin/bash
27
- #SBATCH --job-name=${JOB_NAME}
28
- #SBATCH --account=${ACCOUNT}
29
- #SBATCH --partition=${PARTITIONS}
30
- #SBATCH --nodes=1
31
- #SBATCH --gpus-per-node=1
32
- #SBATCH --time=${SLURM_TIME}
33
- #SBATCH --output=${LOGS}/install_%j.out
34
- #SBATCH --error=${LOGS}/install_%j.err
35
-
36
- set -euo pipefail
37
- source /etc/profile.d/modules.sh 2>/dev/null || true
38
- if command -v module >/dev/null 2>&1; then
39
- module load cuda12.2/toolkit/12.2.2 || true
40
- fi
41
- export PATH="\$HOME/.local/bin:\$PATH"
42
-
43
- cd "${PKG}"
44
- uv venv
45
- source .venv/bin/activate
46
-
47
- uv pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128
48
- rm -f src/nemotron_ocr_cpp/_nemotron_ocr_cpp*.so
49
- BUILD_CPP_FORCE=1 uv pip install -e . -v
50
-
51
- python -c "
52
- import torch
53
- print(f'torch={torch.__version__} cuda_available={torch.cuda.is_available()}')
54
- if torch.cuda.is_available():
55
- print(f'gpu={torch.cuda.get_device_name(0)}')
56
- import nemotron_ocr, nemotron_ocr_cpp
57
- from nemotron_ocr.inference.pipeline_v2 import NemotronOCRV2
58
- print('nemotron_ocr import OK; nemotron_ocr_cpp OK; NemotronOCRV2 OK')
59
- "
60
- ENDSCRIPT
61
-
62
- chmod +x "$JOB"
63
-
64
- echo "============================================================"
65
- echo " GPU install job (scratch clone)"
66
- echo "============================================================"
67
- echo " package: $PKG"
68
- echo " logs: $LOGS"
69
- echo " job file: $JOB"
70
- echo "============================================================"
71
-
72
- JID=$(sbatch "$JOB" 2>&1 | sed -n 's/.* \([0-9][0-9]*\)$/\1/p' || true)
73
- if [[ -n "${JID:-}" ]]; then
74
- echo " submitted job ID: $JID"
75
- echo " tail: tail -f $LOGS/install_${JID}.out"
76
- else
77
- echo " sbatch failed or did not return a job id" >&2
78
- exit 1
79
- fi