| |
| """ |
| PP-OCRv6 ONNX Inference (standalone, zero Paddle dependency) |
| |
| Dependencies: |
| numpy, opencv-python, onnxruntime, pyyaml, shapely, pyclipper |
| |
| Usage: |
| from ppocrv6_onnx import PPOCRv6Onnx |
| ocr = PPOCRv6Onnx(det_onnx="det.onnx", rec_onnx="rec.onnx", char_dict="inference.yml") |
| results = ocr(img) # img is BGR numpy array |
| |
| # With direction classifier: |
| ocr = PPOCRv6Onnx(..., cls_onnx="cls.onnx", cls_label_list=["0","180"], cls_thresh=0.9) |
| """ |
|
|
| import argparse |
| import json |
| import math |
| import os |
| from typing import List, Optional, Tuple, Union |
|
|
| import cv2 |
| import numpy as np |
| import axengine as ort |
| import yaml |
|
|
| from PIL import Image, ImageDraw, ImageFont |
| from shapely.geometry import Polygon |
| import pyclipper |
| import random |
|
|
|
|
| |
| |
| |
|
|
| def _get_dim_value(dim): |
| """Extract integer value from an ONNX Runtime dimension, returning 0 for dynamic.""" |
| if dim is None: |
| return 0 |
| if isinstance(dim, str): |
| return 0 |
| if hasattr(dim, "dim_value"): |
| return int(dim.dim_value) if dim.dim_value else 0 |
| if hasattr(dim, "dim_param"): |
| return 0 |
| try: |
| return int(dim) |
| except (TypeError, ValueError): |
| return 0 |
|
|
|
|
| def _detect_fixed_dims(session: ort.InferenceSession, det_onnx: str): |
| """Detect fixed H/W from ONNX input shape. Returns (fixed_h, fixed_w).""" |
| inp = session.get_inputs()[0] |
| h, w = _get_dim_value(inp.shape[2]), _get_dim_value(inp.shape[3]) |
| if h == 0 and w == 0: |
| try: |
| import onnx |
| m = onnx.load(det_onnx) |
| dims = m.graph.input[0].type.tensor_type.shape.dim |
| h = dims[2].dim_value if len(dims) > 2 else 0 |
| w = dims[3].dim_value if len(dims) > 3 else 0 |
| except Exception: |
| pass |
| return (h if h > 0 else 0), (w if w > 0 else 0) |
|
|
|
|
| def _load_char_dict(source: Union[str, List[str]]) -> List[str]: |
| if isinstance(source, list): |
| return source |
| ext = os.path.splitext(source)[1].lower() |
| if ext in (".yml", ".yaml"): |
| with open(source, "r", encoding="utf-8") as f: |
| cfg = yaml.safe_load(f) |
| dic = cfg.get("PostProcess", {}).get("character_dict", []) |
| if not dic: |
| raise ValueError(f"No PostProcess.character_dict found in {source}") |
| return dic |
| elif ext == ".txt": |
| with open(source, "r", encoding="utf-8") as f: |
| return [line.strip("\n\r") for line in f.readlines()] |
| else: |
| raise ValueError(f"Unsupported char_dict source: {source}. Use .yml, .txt, or list.") |
|
|
|
|
| |
| |
| |
|
|
| class _DetResizeForTest: |
| def __init__(self, limit_side_len=960, limit_type="max", max_side_limit=4000): |
| self.limit_side_len = limit_side_len |
| self.limit_type = limit_type |
| self.max_side_limit = max_side_limit |
|
|
| def _image_padding(self, im, value=0): |
| h, w, c = im.shape |
| im_pad = np.zeros((max(32, h), max(32, w), c), np.uint8) + value |
| im_pad[:h, :w, :] = im |
| return im_pad |
|
|
| def _resize_image_type0(self, img): |
| h, w, _ = img.shape |
| limit_side_len = self.limit_side_len |
| if self.limit_type == "max": |
| ratio = float(limit_side_len) / max(h, w) if max(h, w) > limit_side_len else 1.0 |
| elif self.limit_type == "min": |
| ratio = float(limit_side_len) / min(h, w) if min(h, w) < limit_side_len else 1.0 |
| elif self.limit_type == "resize_long": |
| ratio = float(limit_side_len) / max(h, w) |
| else: |
| raise ValueError(f"not support limit_type: {self.limit_type}") |
|
|
| resize_h, resize_w = int(h * ratio), int(w * ratio) |
| if max(resize_h, resize_w) > self.max_side_limit: |
| ratio = float(self.max_side_limit) / max(resize_h, resize_w) |
| resize_h, resize_w = int(resize_h * ratio), int(resize_w * ratio) |
| resize_h = max(int(round(resize_h / 32) * 32), 32) |
| resize_w = max(int(round(resize_w / 32) * 32), 32) |
| if int(resize_w) <= 0 or int(resize_h) <= 0: |
| return None, (None, None) |
| img = cv2.resize(img, (int(resize_w), int(resize_h))) |
| ratio_h, ratio_w = resize_h / float(h), resize_w / float(w) |
| return img, [ratio_h, ratio_w] |
|
|
| def __call__(self, img): |
| src_h, src_w = img.shape[:2] |
| if sum([src_h, src_w]) < 64: |
| img = self._image_padding(img) |
| img, [ratio_h, ratio_w] = self._resize_image_type0(img) |
| shape = np.array([src_h, src_w, ratio_h, ratio_w]) |
| return img, shape |
|
|
|
|
| class _NormalizeImage: |
| def __init__(self, mean, std, scale=1.0 / 255.0, order="hwc"): |
| self.scale = np.float32(scale) |
| shape = (1, 1, 3) if order == "hwc" else (3, 1, 1) |
| self.mean = np.array(mean, dtype=np.float32).reshape(shape) |
| self.std = np.array(std, dtype=np.float32).reshape(shape) |
|
|
| def __call__(self, img): |
| return (img.astype("float32") * self.scale - self.mean) / self.std |
|
|
|
|
| class _ToCHWImage: |
| def __call__(self, img): |
| return img.transpose((2, 0, 1)) |
|
|
|
|
| |
| |
| |
|
|
| def _resize_norm_img(img, image_shape, max_wh_ratio=None): |
| imgC, imgH, imgW = image_shape |
| if max_wh_ratio is None: |
| max_wh_ratio = imgW * 1.0 / imgH |
| h, w = img.shape[:2] |
| max_wh_ratio = max(max_wh_ratio, w / h) |
| target_w = int(imgH * max_wh_ratio) |
| h, w = img.shape[:2] |
| ratio = w / h |
| resized_w = target_w if math.ceil(imgH * ratio) > target_w else int(math.ceil(imgH * ratio)) |
| resized_image = cv2.resize(img, (resized_w, imgH)).astype("float32") |
| resized_image = resized_image.transpose((2, 0, 1)) |
| |
| |
| |
| padding_im = np.zeros((imgC, imgH, target_w), dtype=np.float32) |
| padding_im[:, :, 0:resized_w] = resized_image |
| return padding_im |
|
|
|
|
| |
| |
| |
|
|
| class _DBPostProcess: |
| def __init__(self, thresh=0.3, box_thresh=0.7, max_candidates=1000, |
| unclip_ratio=2.0, use_dilation=False, score_mode="fast", box_type="quad"): |
| self.thresh = thresh |
| self.box_thresh = box_thresh |
| self.max_candidates = max_candidates |
| self.unclip_ratio = unclip_ratio |
| self.min_size = 3 |
| self.score_mode = score_mode |
| self.box_type = box_type |
| self.dilation_kernel = None if not use_dilation else np.array([[1, 1], [1, 1]]) |
|
|
| def _unclip(self, box, unclip_ratio): |
| poly = Polygon(box) |
| distance = poly.area * unclip_ratio / poly.length |
| offset = pyclipper.PyclipperOffset() |
| offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) |
| return offset.Execute(distance) |
|
|
| def _get_mini_boxes(self, contour): |
| bb = cv2.minAreaRect(contour) |
| points = sorted(list(cv2.boxPoints(bb)), key=lambda x: x[0]) |
| i1, i4 = (0, 1) if points[1][1] > points[0][1] else (1, 0) |
| i2, i3 = (2, 3) if points[3][1] > points[2][1] else (3, 2) |
| return [points[i1], points[i2], points[i3], points[i4]], min(bb[1]) |
|
|
| def _box_score_fast(self, bitmap, _box): |
| h, w = bitmap.shape[:2] |
| box = _box.copy() |
| xmin = np.clip(np.floor(box[:, 0].min()).astype("int32"), 0, w - 1) |
| xmax = np.clip(np.ceil(box[:, 0].max()).astype("int32"), 0, w - 1) |
| ymin = np.clip(np.floor(box[:, 1].min()).astype("int32"), 0, h - 1) |
| ymax = np.clip(np.ceil(box[:, 1].max()).astype("int32"), 0, h - 1) |
| mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) |
| box[:, 0] -= xmin |
| box[:, 1] -= ymin |
| cv2.fillPoly(mask, box.reshape(1, -1, 2).astype("int32"), 1) |
| return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] |
|
|
| def _boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height): |
| height, width = _bitmap.shape |
| outs = cv2.findContours( |
| (_bitmap * 255).astype(np.uint8), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE |
| ) |
| contours = outs[0] if len(outs) == 2 else outs[1] |
| num = min(len(contours), self.max_candidates) |
| boxes, scores = [], [] |
| for i in range(num): |
| points, sside = self._get_mini_boxes(contours[i]) |
| if sside < self.min_size: |
| continue |
| points = np.array(points) |
| score = self._box_score_fast(pred, points.reshape(-1, 2)) |
| if self.box_thresh > score: |
| continue |
| box = self._unclip(points, self.unclip_ratio) |
| if len(box) > 1: |
| continue |
| box = np.array(box).reshape(-1, 1, 2) |
| box, sside = self._get_mini_boxes(box) |
| if sside < self.min_size + 2: |
| continue |
| box = np.array(box) |
| box[:, 0] = np.clip(np.round(box[:, 0] / width * dest_width), 0, dest_width) |
| box[:, 1] = np.clip(np.round(box[:, 1] / height * dest_height), 0, dest_height) |
| boxes.append(box.astype("int32")) |
| scores.append(score) |
| return np.array(boxes, dtype="int32"), scores |
|
|
| def __call__(self, pred, shape_list): |
| pred = pred[:, 0, :, :] |
| segmentation = pred > self.thresh |
| boxes_batch = [] |
| for bi in range(pred.shape[0]): |
| src_h, src_w, ratio_h, ratio_w = shape_list[bi] |
| mask = cv2.dilate(np.array(segmentation[bi]).astype(np.uint8), |
| self.dilation_kernel) if self.dilation_kernel is not None else segmentation[bi] |
| boxes, _ = self._boxes_from_bitmap(pred[bi], mask, src_w, src_h) |
| boxes_batch.append(boxes) |
| return boxes_batch |
|
|
|
|
| |
| |
| |
|
|
| class _CTCLabelDecode: |
| def __init__(self, character_list: List[str], use_space_char=True): |
| self.character_str = list(character_list) |
| if use_space_char: |
| self.character_str.append(" ") |
| dict_character = ["blank"] + self.character_str |
| self.character = dict_character |
|
|
| def decode(self, text_index, text_prob=None, is_remove_duplicate=True): |
| result_list = [] |
| for bi in range(len(text_index)): |
| sel = np.ones(len(text_index[bi]), dtype=bool) |
| if is_remove_duplicate: |
| sel[1:] = text_index[bi][1:] != text_index[bi][:-1] |
| sel &= text_index[bi] != 0 |
| chars = [self.character[int(t)] for t in text_index[bi][sel]] |
| conf = text_prob[bi][sel] if text_prob is not None else [1] * len(sel) |
| if len(conf) == 0: |
| conf = [0] |
| result_list.append(("".join(chars), float(np.mean(conf)))) |
| return result_list |
|
|
| def __call__(self, preds): |
| return self.decode(preds.argmax(axis=2), preds.max(axis=2), is_remove_duplicate=True) |
|
|
|
|
| |
| |
| |
|
|
| def _get_rotate_crop_image(img: np.ndarray, points: np.ndarray) -> np.ndarray: |
| assert len(points) == 4 |
| cw = int(max(np.linalg.norm(points[0] - points[1]), np.linalg.norm(points[2] - points[3]))) |
| ch = int(max(np.linalg.norm(points[0] - points[3]), np.linalg.norm(points[1] - points[2]))) |
| pts_std = np.float32([[0, 0], [cw, 0], [cw, ch], [0, ch]]) |
| M = cv2.getPerspectiveTransform(points.astype(np.float32), pts_std) |
| dst = cv2.warpPerspective(img, M, (cw, ch), borderMode=cv2.BORDER_REPLICATE, flags=cv2.INTER_CUBIC) |
| if dst.shape[0] * 1.0 / dst.shape[1] >= 1.5: |
| dst = np.rot90(dst) |
| return dst |
|
|
|
|
| def _sorted_boxes(dt_boxes): |
| if len(dt_boxes) == 0: |
| return dt_boxes |
| boxes = sorted(dt_boxes, key=lambda x: (x[0][1], x[0][0])) |
| lst = list(boxes) |
| for i in range(len(lst) - 1): |
| for j in range(i, -1, -1): |
| if abs(lst[j + 1][0][1] - lst[j][0][1]) < 10 and lst[j + 1][0][0] < lst[j][0][0]: |
| lst[j], lst[j + 1] = lst[j + 1], lst[j] |
| else: |
| break |
| return lst |
|
|
|
|
| def draw_ocr_result( |
| img: np.ndarray, |
| results: List[dict], |
| font_path: str = "./fonts/simfang.ttf", |
| ) -> np.ndarray: |
| """Draw detection boxes (semi-transparent) on original image, with text list on the right side.""" |
| h, w = img.shape[:2] |
|
|
| |
| pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) |
| overlay = Image.new("RGBA", pil_img.size, (0, 0, 0, 0)) |
| draw_overlay = ImageDraw.Draw(overlay) |
|
|
| random.seed(0) |
| for res in results: |
| box = res["box"] |
| color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255), 90) |
| draw_overlay.polygon([tuple(p) for p in box], fill=color) |
|
|
| left_img = Image.alpha_composite(pil_img.convert("RGBA"), overlay).convert("RGB") |
|
|
| |
| right_w = int(w * 0.9) |
| right = Image.new("RGB", (right_w, h), (255, 255, 255)) |
| draw_right = ImageDraw.Draw(right) |
|
|
| try: |
| font = ImageFont.truetype(font_path, 14) |
| except (OSError, IOError): |
| font = ImageFont.load_default() |
|
|
| y = 5 |
| gap = 18 |
| for i, res in enumerate(results): |
| text = f"{i+1}. {res['text']} ({res['confidence']:.3f})" |
| |
| random.seed(i) |
| blk_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) |
| draw_right.rectangle([5, y + 3, 15, y + 14], fill=blk_color, outline=(0, 0, 0)) |
| draw_right.text((20, y), text, fill=(0, 0, 0), font=font) |
| y += gap |
|
|
| |
| result_img = Image.new("RGB", (w + right_w, h)) |
| result_img.paste(left_img, (0, 0)) |
| result_img.paste(right, (w, 0)) |
|
|
| return cv2.cvtColor(np.array(result_img), cv2.COLOR_RGB2BGR) |
|
|
|
|
| |
| |
| |
|
|
| class PPOCRv6Onnx: |
|
|
| def __init__( |
| self, |
| det_onnx: str, |
| rec_onnx: str, |
| char_dict: Union[str, List[str]], |
| |
| det_limit_side_len: int = 960, |
| det_db_thresh: float = 0.2, |
| det_db_box_thresh: float = 0.4, |
| det_db_unclip_ratio: float = 1.4, |
| det_max_candidates: int = 3000, |
| |
| rec_image_shape: Tuple[int, int, int] = (3, 48, 320), |
| rec_batch_num: int = 1, |
| |
| use_angle_cls: bool = False, |
| cls_onnx: Optional[str] = None, |
| cls_image_shape: Tuple[int, int, int] = (3, 48, 192), |
| cls_batch_num: int = 1, |
| cls_thresh: float = 0.9, |
| cls_label_list: Optional[List[str]] = None, |
| |
| drop_score: float = 0.5, |
| use_gpu: bool = False, |
| onnx_providers: Optional[List[str]] = None, |
| resize_mode: str = "letterbox", |
| ): |
| assert resize_mode in ("letterbox", "stretch"), f"invalid resize_mode: {resize_mode}" |
| if cls_label_list is None: |
| cls_label_list = ["0", "180"] |
|
|
| self.rec_image_shape = rec_image_shape |
| self.rec_batch_num = rec_batch_num |
| self.drop_score = drop_score |
| self.use_angle_cls = use_angle_cls |
| self.cls_thresh = cls_thresh |
| self.cls_label_list = cls_label_list |
| self.cls_batch_num = cls_batch_num |
| self.cls_image_shape = cls_image_shape |
| self._resize_mode = resize_mode |
|
|
| self.det_session = ort.InferenceSession(det_onnx) |
| self.det_input_name = self.det_session.get_inputs()[0].name |
|
|
| self.rec_session = ort.InferenceSession(rec_onnx) |
| self.rec_input_name = self.rec_session.get_inputs()[0].name |
|
|
| |
| if use_angle_cls: |
| if cls_onnx is None: |
| raise ValueError("cls_onnx is required when use_angle_cls=True") |
| self.cls_session = ort.InferenceSession(cls_onnx) |
| self.cls_input_name = self.cls_session.get_inputs()[0].name |
| |
| cls_h, cls_w = _detect_fixed_dims(self.cls_session, cls_onnx) |
| self._cls_fixed_h = cls_h if cls_h > 0 else 0 |
| self._cls_fixed_w = cls_w if cls_w > 0 else 0 |
| else: |
| self.cls_session = None |
|
|
| |
| self._det_fixed_h, self._det_fixed_w = _detect_fixed_dims(self.det_session, det_onnx) |
| det_shape = self.det_session.get_inputs()[0].shape |
| print(f"[PPOCRv6] det shape={det_shape}, fixed_h={self._det_fixed_h}, fixed_w={self._det_fixed_w}, " |
| f"cls={use_angle_cls}, resize_mode={resize_mode}") |
|
|
| |
| rec_inp = self.rec_session.get_inputs()[0] |
| rec_fw = _get_dim_value(rec_inp.shape[3]) |
| self._rec_fixed_w = rec_fw if rec_fw > 0 else 0 |
|
|
| |
| self._det_resize = _DetResizeForTest(limit_side_len=det_limit_side_len, limit_type="max") |
| |
| self._det_normalize = _NormalizeImage(mean=[0., 0., 0.], std=[1.0, 1.0, 1.0], scale=1.0) |
| self._det_to_chw = _ToCHWImage() |
| self._det_post = _DBPostProcess(thresh=det_db_thresh, box_thresh=det_db_box_thresh, |
| unclip_ratio=det_db_unclip_ratio, max_candidates=det_max_candidates, box_type="quad") |
|
|
| |
| self._rec_post = _CTCLabelDecode(_load_char_dict(char_dict), use_space_char=True) |
|
|
| |
|
|
| def _preprocess_det(self, img: np.ndarray): |
| src_h, src_w = img.shape[:2] |
| fh, fw = self._det_fixed_h, self._det_fixed_w |
| |
| if fh > 0 and fw > 0: |
| if self._resize_mode == "stretch": |
| |
| img_r = cv2.resize(img, (fw, fh)) |
| shape = np.array([src_h, src_w, float(fh) / src_h, float(fw) / src_w]) |
| else: |
| |
| ratio = min(fh / src_h, fw / src_w) |
| new_h = min(max(int(round(src_h * ratio / 32) * 32), 32), fh) |
| new_w = min(max(int(round(src_w * ratio / 32) * 32), 32), fw) |
| img_r = cv2.resize(img, (new_w, new_h)) |
| pad_h, pad_w = max(0, fh - new_h), max(0, fw - new_w) |
| if pad_h or pad_w: |
| img_r = cv2.copyMakeBorder(img_r, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value=(0, 0, 0)) |
| shape = np.array([src_h * fh / new_h, src_w * fw / new_w, |
| float(new_h) / src_h, float(new_w) / src_w]) |
| else: |
| img_r, shape = self._det_resize(img) |
|
|
| img_n = self._det_normalize(img_r) |
| img_c = self._det_to_chw(img_n) |
| return np.expand_dims(img_c.astype(np.float32), axis=0), shape |
|
|
| def _postprocess_det(self, output, shape): |
| return self._det_post(output, np.expand_dims(shape, axis=0))[0] |
|
|
| def detect(self, img): |
| tensor, shape = self._preprocess_det(img) |
| out = self.det_session.run(None, {self.det_input_name: tensor}) |
| return self._postprocess_det(out[0], shape) |
|
|
| |
|
|
| def _preprocess_cls(self, img_list): |
| num = len(img_list) |
| width_list = [im.shape[1] / float(im.shape[0]) for im in img_list] |
| indices = np.argsort(np.array(width_list)) |
| batches, idx_maps = [], [] |
| for beg in range(0, num, self.cls_batch_num): |
| end = min(num, beg + self.cls_batch_num) |
| imgC, imgH, imgW = self.cls_image_shape |
|
|
| |
| if self._cls_fixed_h > 0: |
| imgH = self._cls_fixed_h |
| if self._cls_fixed_w > 0: |
| imgW = self._cls_fixed_w |
|
|
| max_wh_ratio = imgW / imgH |
| for ino in range(beg, end): |
| h, w = img_list[indices[ino]].shape[:2] |
| max_wh_ratio = max(max_wh_ratio, w / h) |
| if self._cls_fixed_w > 0: |
| max_wh_ratio = self._cls_fixed_w / imgH |
|
|
| shape = (imgC, imgH, imgW) |
| norm_list, idx_list = [], [] |
| for ino in range(beg, end): |
| orig_idx = indices[ino] |
| norm = _resize_norm_img(img_list[orig_idx], shape, max_wh_ratio=max_wh_ratio) |
| norm_list.append(np.expand_dims(norm, axis=0)) |
| idx_list.append(orig_idx) |
| if norm_list: |
| batches.append(np.concatenate(norm_list, axis=0).astype(np.float32)) |
| idx_maps.append(idx_list) |
| return batches, idx_maps |
|
|
| def _postprocess_cls(self, batch_outputs, idx_maps, total_num, img_list): |
| results = [("0", 1.0)] * total_num |
| for preds_batch, idx_list in zip(batch_outputs, idx_maps): |
| pred_ids = preds_batch.argmax(axis=1) |
| for i, orig_idx in enumerate(idx_list): |
| label = self.cls_label_list[int(pred_ids[i])] |
| score = float(preds_batch[i, int(pred_ids[i])]) |
| results[orig_idx] = (label, score) |
| if "180" in str(label) and score > self.cls_thresh: |
| img_list[orig_idx] = cv2.rotate(img_list[orig_idx], cv2.ROTATE_180) |
| return results |
|
|
| def classify(self, img_list): |
| if not img_list or not self.use_angle_cls: |
| return img_list, [], 0 |
| img_list = [im.copy() for im in img_list] |
| batches, idx_maps = self._preprocess_cls(img_list) |
| outputs = [] |
| for batch in batches: |
| out = self.cls_session.run(None, {self.cls_input_name: batch}) |
| outputs.append(out[0]) |
| cls_res = self._postprocess_cls(outputs, idx_maps, len(img_list), img_list) |
| return img_list, cls_res, 0 |
|
|
| |
|
|
| def _preprocess_rec(self, img_crop_list): |
| num = len(img_crop_list) |
| width_list = [im.shape[1] / float(im.shape[0]) for im in img_crop_list] |
| indices = np.argsort(np.array(width_list)) |
| batches, idx_maps = [], [] |
| for beg in range(0, num, self.rec_batch_num): |
| end = min(num, beg + self.rec_batch_num) |
| imgC, imgH, imgW = self.rec_image_shape |
| max_wh_ratio = imgW / imgH |
| for ino in range(beg, end): |
| h, w = img_crop_list[indices[ino]].shape[:2] |
| max_wh_ratio = max(max_wh_ratio, w / h) |
| |
| if self._rec_fixed_w > 0: |
| max_wh_ratio = self._rec_fixed_w / imgH |
| norm_list, idx_list = [], [] |
| for ino in range(beg, end): |
| orig_idx = indices[ino] |
| norm = _resize_norm_img(img_crop_list[orig_idx], self.rec_image_shape, max_wh_ratio=max_wh_ratio) |
| norm_list.append(np.expand_dims(norm, axis=0)) |
| idx_list.append(orig_idx) |
| if norm_list: |
| batches.append(np.concatenate(norm_list, axis=0).astype(np.float32)) |
| idx_maps.append(idx_list) |
| return batches, idx_maps |
|
|
| def _postprocess_rec(self, batch_outputs, idx_maps, total_num): |
| results = [("", 0.0)] * total_num |
| |
| for preds_batch, idx_list in zip(batch_outputs, idx_maps): |
| texts = self._rec_post(preds_batch) |
| for i, orig_idx in enumerate(idx_list): |
| results[orig_idx] = texts[i] |
| return results |
|
|
| def recognize(self, img_crop_list): |
| if not img_crop_list: |
| return [] |
| batches, idx_maps = self._preprocess_rec(img_crop_list) |
| outputs = [] |
| for batch in batches: |
| out = self.rec_session.run(None, {self.rec_input_name: batch}) |
| outputs.append(out[0]) |
| return self._postprocess_rec(outputs, idx_maps, len(img_crop_list)) |
|
|
| |
|
|
| def predict_image(self, image_path, visualize=False): |
| img = cv2.imread(image_path) |
| if img is None: |
| raise FileNotFoundError(f"Cannot read image: {image_path}") |
| return self(img, visualize=visualize) |
|
|
| def __call__(self, img: np.ndarray, visualize=False, use_cls=None): |
| ori_im = img.copy() |
|
|
| boxes = self.detect(img) |
| |
| if len(boxes) == 0: |
| return [] if not visualize else ori_im |
|
|
| boxes = _sorted_boxes(boxes) |
| |
|
|
| img_crop_list = [] |
| for i, box in enumerate(boxes): |
| crop = _get_rotate_crop_image(ori_im, np.array(box, dtype=np.float32)) |
| img_crop_list.append(crop) |
|
|
| |
| do_cls = self.use_angle_cls if use_cls is None else use_cls |
| if do_cls and self.cls_session is not None: |
| img_crop_list, cls_res, _ = self.classify(img_crop_list) |
|
|
| rec_res = self.recognize(img_crop_list) |
|
|
| results = [] |
| for box, (text, conf) in zip(boxes, rec_res): |
| if conf >= self.drop_score: |
| results.append({"text": text, "confidence": round(conf, 4), "box": box.tolist()}) |
|
|
| return (results, draw_ocr_result(ori_im, results)) if visualize else (results, None) |
|
|
|
|
| |
| |
| |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="PP-OCRv6 ONNX Inference (standalone, no Paddle dependency)") |
| parser.add_argument("--det_onnx", type=str, default="axmodel/ax650/det_npu1.axmodel") |
| parser.add_argument("--rec_onnx", type=str, default="axmodel/ax650/rec_npu1.axmodel") |
| parser.add_argument("--char_dict", type=str, default="onnx/rec_inference.yml") |
| parser.add_argument("--image", required=True, help="Input image path") |
| parser.add_argument("--use_gpu", action="store_true") |
| parser.add_argument("--drop_score", type=float, default=0.5) |
| parser.add_argument("--det_limit_side_len", type=int, default=960) |
| parser.add_argument("--det_db_thresh", type=float, default=0.2) |
| parser.add_argument("--det_db_box_thresh", type=float, default=0.45) |
| parser.add_argument("--det_db_unclip_ratio", type=float, default=1.4) |
| parser.add_argument("--rec_batch_num", type=int, default=1) |
| parser.add_argument("--resize_mode", type=str, default="letterbox", choices=["letterbox", "stretch"]) |
| |
| parser.add_argument("--use_angle_cls", action="store_true", help="Enable direction classifier") |
| parser.add_argument("--cls_onnx", type=str, default="axmodel/ax650/cls_npu1.axmodel", help="Classifer ONNX model path") |
| parser.add_argument("--cls_thresh", type=float, default=0.9, help="Angie classifier confidence threshold") |
| parser.add_argument("--cls_batch_num", type=int, default=1) |
| |
| parser.add_argument("--visualize", action="store_true") |
| parser.add_argument("--output", type=str, default=None) |
| parser.add_argument("--json", type=str, default=None) |
|
|
| args = parser.parse_args() |
|
|
| char_dict_src = args.char_dict |
| if not os.path.exists(char_dict_src) and ("," in char_dict_src or char_dict_src.startswith("[")): |
| char_dict = [c.strip() for c in char_dict_src.strip("[]").split(",") if c.strip()] |
| else: |
| char_dict = char_dict_src |
|
|
| ocr = PPOCRv6Onnx( |
| det_onnx=args.det_onnx, |
| rec_onnx=args.rec_onnx, |
| char_dict=char_dict, |
| det_limit_side_len=args.det_limit_side_len, |
| det_db_thresh=args.det_db_thresh, |
| det_db_box_thresh=args.det_db_box_thresh, |
| det_db_unclip_ratio=args.det_db_unclip_ratio, |
| rec_batch_num=args.rec_batch_num, |
| use_angle_cls=args.use_angle_cls, |
| cls_onnx=args.cls_onnx, |
| cls_thresh=args.cls_thresh, |
| cls_batch_num=args.cls_batch_num, |
| drop_score=args.drop_score, |
| use_gpu=args.use_gpu, |
| resize_mode=args.resize_mode, |
| ) |
|
|
| do_viz = args.visualize or args.output is not None |
| img = cv2.imread(args.image) |
| if img is None: |
| raise FileNotFoundError(f"Cannot read image: {args.image}") |
|
|
| if do_viz: |
| results, vis = ocr(img, visualize=True) |
| out_path = args.output or "res-ax.jpg" |
| cv2.imwrite(out_path, vis) |
| print(f"Annotated image saved to: {out_path}") |
| else: |
| results, vis = ocr(img) |
|
|
| if args.json: |
| with open(args.json, "w", encoding="utf-8") as f: |
| json.dump(results, f, ensure_ascii=False, indent=2) |
| print(f"Results saved to: {args.json}") |
| else: |
| for i, res in enumerate(results): |
| print(f"{i+1}. {res['text']} ({res['confidence']:.3f})") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|