Files

174 lines
7.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
本地 OCR 工具
- OCR引擎: macOS默认Apple Vision(pyobjc-framework-Vision);其他平台rapidocr-onnxruntime
- 输出兼容text/lines/details;bbox为输入图左上原点四角像素,enhance后自动缩回
- macOS截图走macljqCtrl(需要录屏权限);图片文件OCR不需要录屏权限
- 坑(rapid): result[i][2] conf 是 str 不是 float
- 坑(rapid): 无文字时 result 返回 None 而非空列表
- 坑: enhance 放大+高对比度处理,对清晰文字有害,默认关闭
- 坑(远程桌面): ImageGrab/mss 在 RDP 断开后截图全黑,用 ocr_window(hwnd) 代替
"""
import re
import sys
import os
from io import BytesIO
from PIL import ImageGrab, Image, ImageEnhance, ImageOps
_LANG = 'zh-Hans-CN'
_rapid_engine = None
def _get_rapid():
global _rapid_engine
if _rapid_engine is None:
from rapidocr_onnxruntime import RapidOCR
_rapid_engine = RapidOCR()
return _rapid_engine
def _preprocess(img, scale=3, contrast=3.0):
img = ImageEnhance.Contrast(img).enhance(contrast)
img = img.resize((img.width * scale, img.height * scale))
return img
def _strip_cjk_spaces(t):
return re.sub(r'(?<=[\u4e00-\u9fff])\s+(?=[\u4e00-\u9fff])', '', t)
def _ocr_rapid(img):
import numpy as np
engine = _get_rapid()
arr = np.array(img)
result, elapse = engine(arr)
if not result:
return {'text': '', 'lines': [], 'details': []}
lines = [r[1] for r in result]
details = [{'bbox': r[0], 'text': r[1], 'conf': float(r[2])} for r in result]
text = _strip_cjk_spaces('\n'.join(lines))
return {'text': text, 'lines': [_strip_cjk_spaces(l) for l in lines], 'details': details}
def _ocr_apple(img, lang):
import objc
import Vision
import Quartz
from Foundation import NSData
aliases = {'zh-Hans-CN': 'zh-Hans', 'zh-CN': 'zh-Hans', 'en': 'en-US'}
languages = list(lang) if isinstance(lang, (list, tuple)) else [lang or 'zh-Hans']
languages = [aliases.get(l, l) for l in languages]
if 'en-US' not in languages:
languages.append('en-US')
with objc.autorelease_pool():
buf = BytesIO()
img.save(buf, format='PNG')
raw = buf.getvalue()
data = NSData.dataWithBytes_length_(raw, len(raw))
source = Quartz.CGImageSourceCreateWithData(data, None)
cg = Quartz.CGImageSourceCreateImageAtIndex(source, 0, None)
request = Vision.VNRecognizeTextRequest.alloc().init()
request.setRecognitionLevel_(Vision.VNRequestTextRecognitionLevelAccurate)
request.setRecognitionLanguages_(languages)
request.setUsesLanguageCorrection_(True)
handler = Vision.VNImageRequestHandler.alloc().initWithCGImage_options_(cg, {})
ok, error = handler.performRequests_error_([request], None)
if not ok:
raise RuntimeError(f'Apple Vision OCR failed: {error}')
details = []
w, h = img.size
for observation in request.results() or []:
candidates = observation.topCandidates_(1)
if not candidates:
continue
candidate = candidates[0]
box = observation.boundingBox()
# Apple: normalized bottom-left -> image pixel top-left, rapid-compatible quad.
x1 = max(0, min(w, box.origin.x * w))
y1 = max(0, min(h, (1 - box.origin.y - box.size.height) * h))
x2 = max(0, min(w, (box.origin.x + box.size.width) * w))
y2 = max(0, min(h, (1 - box.origin.y) * h))
details.append({'bbox': [[x1, y1], [x2, y1], [x2, y2], [x1, y2]],
'text': _strip_cjk_spaces(str(candidate.string())),
'conf': float(candidate.confidence())})
lines = [d['text'] for d in details]
return {'text': '\n'.join(lines), 'lines': lines, 'details': details}
def ocr_image(image_input, lang=_LANG, enhance=False, engine=None):
"""PIL/path -> {text, lines, details}; bbox为输入图左上原点像素四角。
engine=None: macOS Apple Vision,其他平台rapid;可指定apple/vision/rapid。
lang支持语言字符串或列表(Apple后端);enhance默认关闭。
"""
if isinstance(image_input, (str, os.PathLike)):
with Image.open(image_input) as source:
image_input = ImageOps.exif_transpose(source).convert('RGB')
elif isinstance(image_input, Image.Image):
image_input = ImageOps.exif_transpose(image_input).convert('RGB')
else:
raise TypeError('image_input must be a path or PIL Image')
original_size = image_input.size
if enhance:
image_input = _preprocess(image_input)
engine = engine or ('apple' if sys.platform == 'darwin' else 'rapid')
if engine in ('apple', 'vision'):
if sys.platform != 'darwin':
raise RuntimeError('Apple Vision requires macOS')
result = _ocr_apple(image_input, lang)
elif engine == 'rapid':
result = _ocr_rapid(image_input)
else:
raise ValueError('engine must be apple, vision, rapid or None')
if enhance:
sx, sy = original_size[0] / image_input.width, original_size[1] / image_input.height
for detail in result['details']:
detail['bbox'] = [[float(x) * sx, float(y) * sy] for x, y in detail['bbox']]
return result
def ocr_screen(bbox=None, lang=_LANG, enhance=False, engine=None):
"""
截取屏幕区域并 OCR
:param bbox: 必填(x1, y1, x2, y2)物理像素坐标,不允许全屏
:return: dict,bbox相对截图左上角
"""
if bbox is None or len(bbox) != 4 or bbox[2] <= bbox[0] or bbox[3] <= bbox[1]:
raise ValueError('Provide a valid region bbox=(left, top, right, bottom)')
if sys.platform == 'darwin':
import macljqCtrl as ctrl
img = ctrl.GrabScreen(bbox)
else:
# Preserve the existing non-macOS region-capture implementation.
img = ImageGrab.grab(bbox=bbox)
return ocr_image(img, lang, enhance, engine)
def ocr_window(hwnd, lang=_LANG, enhance=False, engine=None):
"""
截取窗口并 OCR (macOS: macljqCtrl;Windows: PrintWindow支持远程桌面断开)
:param hwnd: macOS窗口ID或Windows窗口句柄(int)
:return: dict,bbox相对窗口截图左上角
"""
if sys.platform == 'darwin':
import macljqCtrl as ctrl
return ocr_image(ctrl.GrabWindow(hwnd), lang, enhance, engine)
import win32gui, win32ui
from ctypes import windll
l, t, r, b = win32gui.GetWindowRect(hwnd)
w, h = r - l, b - t
hwndDC = win32gui.GetWindowDC(hwnd)
mfcDC = win32ui.CreateDCFromHandle(hwndDC)
saveDC = mfcDC.CreateCompatibleDC()
saveBitMap = win32ui.CreateBitmap()
saveBitMap.CreateCompatibleBitmap(mfcDC, w, h)
saveDC.SelectObject(saveBitMap)
windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 3)
bmpinfo = saveBitMap.GetInfo()
bmpstr = saveBitMap.GetBitmapBits(True)
img = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']), bmpstr, 'raw', 'BGRX', 0, 1)
win32gui.DeleteObject(saveBitMap.GetHandle())
saveDC.DeleteDC()
mfcDC.DeleteDC()
win32gui.ReleaseDC(hwnd, hwndDC)
return ocr_image(img, lang, enhance, engine)
if __name__ == "__main__":
r = ocr_screen((0, 0, 400, 100))
print(f"识别结果: {r['text']}")
for line in r['lines']:
print(f" 行: {line}")
if 'details' in r:
for d in r['details']:
print(f" [{d['conf']:.3f}] {d['text']}")