Download runtime/engine.py from failed09/bashkir-trocr-base: direct link, hf CLI and curl.
- Browser
- Download file 7.81 kB
-
https://huggingface.co/failed09/bashkir-trocr-base/resolve/main/runtime/engine.py
- Command line
-
hf download hf://failed09/bashkir-trocr-base/runtime/engine.py
-
curl -L -o engine.py https://huggingface.co/failed09/bashkir-trocr-base/resolve/main/runtime/engine.py
7.81 kB
| """Local ONNX OCR with shared detection and line assembly for CPU/GPU.""" | |
| import base64 | |
| import io | |
| import json | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import cv2 | |
| from PIL import Image, ImageOps | |
| from tokenizers import Tokenizer | |
| from .dbnet_utils import DBPostProcess, DetResizeForTest | |
| from .line_merger import merge_line_boxes | |
| def pixels(image, config): | |
| size=config['size'] | |
| image=image.convert('RGB').resize((size['width'],size['height']),Image.Resampling(config['resample'])) | |
| x=np.asarray(image,dtype=np.float32)*np.float32(config['rescale_factor']) | |
| x=(x-np.asarray(config['image_mean'],dtype=np.float32))/np.asarray(config['image_std'],dtype=np.float32) | |
| return np.ascontiguousarray(x.transpose(2,0,1)[None]) | |
| def split_line(image, ratio=20): | |
| w,h=image.size | |
| if w/max(h,1)<=ratio:return [image] | |
| ink=(np.asarray(image.convert('L'))<210).sum(axis=0) | |
| runs=[];start=None | |
| for x,value in enumerate(ink): | |
| if value==0 and start is None:start=x | |
| elif value!=0 and start is not None:runs.append((start,x));start=None | |
| if start is not None:runs.append((start,w)) | |
| gaps=[(a,b) for a,b in runs if a>40 and b<w-40 and b-a>=8] | |
| if gaps: | |
| a,b=min(gaps,key=lambda r:abs((r[0]+r[1])/2-w/2));cut=(a+b)//2 | |
| else: | |
| left,right=int(w*.35),int(w*.65) | |
| cut=left+int(np.argmin(ink[left:right])) if right>left else w//2 | |
| cut=max(1,min(w-1,cut)) | |
| return split_line(image.crop((0,0,cut,h)),ratio)+split_line(image.crop((cut,0,w,h)),ratio) | |
| def preview(image): | |
| image=image.copy();image.thumbnail((1100,180)) | |
| out=io.BytesIO();image.save(out,format='PNG') | |
| return 'data:image/png;base64,'+base64.b64encode(out.getvalue()).decode('ascii') | |
| class OCR: | |
| def __init__(self, root=None, backend='cpu', threads=4): | |
| import onnxruntime as ort | |
| ort.disable_telemetry_events() | |
| self.root=Path(root or Path(__file__).resolve().parents[1]) | |
| self.config=json.loads((self.root/'config.json').read_text(encoding='utf-8')) | |
| self.preprocess=json.loads((self.root/'preprocessor_config.json').read_text(encoding='utf-8')) | |
| self.tokenizer=Tokenizer.from_file(str(self.root/'tokenizer.json')) | |
| self.tokenizer.no_padding();self.tokenizer.no_truncation() | |
| requested_backend=backend | |
| if backend not in ('cpu','gpu','auto'):raise ValueError('backend must be cpu, gpu or auto') | |
| if backend in ('gpu','auto') and hasattr(ort,'preload_dlls'): | |
| ort.preload_dlls() | |
| if backend=='auto':backend='gpu' if 'CUDAExecutionProvider' in ort.get_available_providers() else 'cpu' | |
| if backend=='gpu' and 'CUDAExecutionProvider' not in ort.get_available_providers(): | |
| raise RuntimeError('CUDA недоступна. Установите GPU-зависимости или выберите CPU.') | |
| options=ort.SessionOptions();options.intra_op_num_threads=threads;options.inter_op_num_threads=1 | |
| if backend=='gpu': | |
| variant=self.config['variants']['fp16'] | |
| providers=['CUDAExecutionProvider','CPUExecutionProvider'] | |
| try: | |
| self.encoder=ort.InferenceSession(str(self.root/variant['encoder']),sess_options=options,providers=providers) | |
| self.decoder=ort.InferenceSession(str(self.root/variant['decoder']),sess_options=options,providers=providers) | |
| if any('CUDAExecutionProvider' not in s.get_providers() for s in (self.encoder,self.decoder)): | |
| raise RuntimeError('Модель не загрузилась на CUDA. Выберите CPU; скрытая смена GPU на CPU запрещена.') | |
| self.encoder.disable_fallback() | |
| self.decoder.disable_fallback() | |
| except Exception: | |
| if requested_backend!='auto':raise | |
| backend='cpu' | |
| if backend=='cpu': | |
| variant=self.config['variants']['int8'] | |
| providers=['CPUExecutionProvider'] | |
| self.encoder=ort.InferenceSession(str(self.root/variant['encoder']),sess_options=options,providers=providers) | |
| self.decoder=ort.InferenceSession(str(self.root/variant['decoder']),sess_options=options,providers=providers) | |
| self.backend=backend | |
| self.detector=ort.InferenceSession(str(self.root/'models/dbnet.onnx'),sess_options=options,providers=['CPUExecutionProvider']) | |
| self.resize=DetResizeForTest(limit_side_len=736,limit_type='min') | |
| self.post=DBPostProcess(thresh=.2,box_thresh=.2,max_candidates=1000,unclip_ratio=1.3,use_dilation=True,score_mode='fast') | |
| def recognize_line(self,image): | |
| hidden=self.encoder.run(None,{'pixel_values':pixels(image,self.preprocess)})[0] | |
| ids=np.array([[self.config['decoder_start_token_id']]],dtype=np.int64) | |
| truncated=True | |
| for _ in range(self.config['max_new_tokens']): | |
| logits=self.decoder.run(None,{'input_ids':ids,'encoder_hidden_states':hidden})[0] | |
| if not np.isfinite(logits).all():raise RuntimeError('Non-finite decoder output') | |
| token=int(np.argmax(logits[0,-1])) | |
| ids=np.concatenate((ids,np.array([[token]],dtype=np.int64)),axis=1) | |
| if token==self.config['eos_token_id']:truncated=False;break | |
| return self.tokenizer.decode(ids[0].tolist(),skip_special_tokens=True).strip(),truncated | |
| def detect(self,image): | |
| # The original detector upsizes the short side. Bound unusually narrow | |
| # images so a one-line crop cannot allocate a gigantic detector tensor. | |
| scale=max(1.,736/min(image.size)) | |
| if max(image.size)*scale>4096: | |
| scale=4096/max(image.size) | |
| width=max(32,round(image.width*scale/32)*32) | |
| height=max(32,round(image.height*scale/32)*32) | |
| data=dict(image=cv2.resize(np.asarray(image),(width,height)), | |
| shape=np.array([image.height,image.width,height/image.height,width/image.width])) | |
| else:data=self.resize({'image':np.asarray(image)}) | |
| x=np.asarray(data['image'],dtype=np.float32)*np.float32(1/255) | |
| x=(x-np.array([.485,.456,.406],dtype=np.float32))/np.array([.229,.224,.225],dtype=np.float32) | |
| x=np.ascontiguousarray(x.transpose(2,0,1)[None]) | |
| output=self.detector.run(None,{self.detector.get_inputs()[0].name:x})[0] | |
| boxes=self.post(output,np.asarray([data['shape']]))[0]['points'] | |
| valid=[] | |
| for box in boxes: | |
| box=np.asarray(box,dtype=np.float32) | |
| box[:,0]=np.clip(box[:,0],0,image.width-1);box[:,1]=np.clip(box[:,1],0,image.height-1) | |
| if np.linalg.norm(box[0]-box[1])>3 and np.linalg.norm(box[0]-box[3])>3:valid.append(box) | |
| return merge_line_boxes(valid,x_margin_ratio=3.5,y_overlap_thresh=.45) | |
| def recognize(self,image,boxes=None,include_images=True): | |
| t=time.perf_counter() | |
| image=ImageOps.exif_transpose(image).convert('RGB') | |
| if np.asarray(image.convert('L')).mean()<125:image=ImageOps.invert(image) | |
| boxes=self.detect(image) if boxes is None else boxes | |
| lines=[] | |
| for box in boxes: | |
| a,b,c,d=[int(v) for v in box] | |
| if c-a<4 or d-b<4:continue | |
| crop=image.crop((max(0,a-4),max(0,b-2),min(image.width,c+4),min(image.height,d+2))) | |
| parts=[self.recognize_line(ch) for ch in split_line(crop)] | |
| text=' '.join(s for s,_ in parts if s).strip() | |
| line=dict(bbox=[a,b,c,d],text=text,truncated=any(end for _,end in parts),chunks=len(parts)) | |
| if include_images:line['image']=preview(crop) | |
| lines.append(line) | |
| return dict(backend=self.backend,lines=lines,full_text='\n'.join(row['text'] for row in lines), | |
| seconds=time.perf_counter()-t,no_text_detected=not lines) | |