117 lines
5.4 KiB
Python
117 lines
5.4 KiB
Python
"""导图图片 OCR:PowerShell WinRT Windows.Media.Ocr(zh-Hans-CN),坐标还原阅读顺序"""
|
||
import re
|
||
import subprocess
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
from .config import OCR_DIR
|
||
|
||
# WinRT StorageFile 无法处理含中文的路径 → 中间文件一律用纯 ASCII 临时目录
|
||
ASCII_TMP = Path(tempfile.gettempdir()) / "yawen_ocr"
|
||
|
||
PS_TEMPLATE = r"""$ErrorActionPreference = 'Stop'
|
||
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
||
$null = [Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]
|
||
$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
||
$null = [Windows.Foundation.IAsyncOperation`1, Windows.Foundation, ContentType = WindowsRuntime]
|
||
$null = [Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime]
|
||
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
|
||
function Await($WinRtTask, $ResultType) {
|
||
$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
|
||
$netTask = $asTask.Invoke($null, @($WinRtTask))
|
||
try { $netTask.GetAwaiter().GetResult() }
|
||
catch {
|
||
$ex = $_.Exception
|
||
while ($ex.InnerException) { $ex = $ex.InnerException }
|
||
Write-Output "AWAIT-ERR: $($ex.GetType().FullName): $($ex.Message)"
|
||
throw
|
||
}
|
||
}
|
||
$imgPath = '{{IMG}}'
|
||
$outFile = '{{OUT}}'
|
||
$file = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($imgPath)) ([Windows.Storage.StorageFile])
|
||
$stream = Await ($file.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream])
|
||
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
||
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
||
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
||
if ($null -eq $engine) {
|
||
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage([Windows.Globalization.Language]::new('zh-Hans-CN'))
|
||
}
|
||
$result = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
|
||
$sb = New-Object System.Text.StringBuilder
|
||
foreach ($line in $result.Lines) {
|
||
$firstWord = $line.Words | Select-Object -First 1
|
||
$x = [int]$firstWord.BoundingRect.X
|
||
$y = [int]$firstWord.BoundingRect.Y
|
||
$text = $line.Text
|
||
$null = $sb.AppendLine("[$x,$y] $text")
|
||
}
|
||
[System.IO.File]::WriteAllText($outFile, $sb.ToString(), [System.Text.Encoding]::UTF8)
|
||
Write-Host "DONE lines=$($result.Lines.Count)"
|
||
"""
|
||
|
||
def _reorder(lines: list[tuple[int, int, str]]) -> str:
|
||
"""按坐标还原导图阅读顺序:行优先(y 容差 60px),行内按 x"""
|
||
if not lines:
|
||
return ""
|
||
lines.sort(key=lambda t: t[1])
|
||
rows: list[list[tuple[int, int, str]]] = []
|
||
cur_y, cur_row = None, []
|
||
for x, y, text in lines:
|
||
if cur_y is None or abs(y - cur_y) <= 60:
|
||
cur_row.append((x, y, text))
|
||
cur_y = y if cur_y is None else min(cur_y, y)
|
||
else:
|
||
rows.append(sorted(cur_row, key=lambda t: t[0]))
|
||
cur_row, cur_y = [(x, y, text)], y
|
||
if cur_row:
|
||
rows.append(sorted(cur_row, key=lambda t: t[0]))
|
||
return "\n".join(" ".join(t[2] for t in row) for row in rows)
|
||
|
||
|
||
def _prepare(img_path: str) -> str:
|
||
"""预处理:EXIF 转正 + 缩放到 OCR 引擎可处理尺寸(≤2000px),存纯 ASCII 路径"""
|
||
try:
|
||
from PIL import Image, ImageOps
|
||
im = Image.open(img_path)
|
||
im = ImageOps.exif_transpose(im)
|
||
w, h = im.size
|
||
if max(w, h) > 2000:
|
||
r = 2000 / max(w, h)
|
||
im = im.resize((int(w * r), int(h * r)), Image.LANCZOS)
|
||
ASCII_TMP.mkdir(parents=True, exist_ok=True)
|
||
small = ASCII_TMP / f"{Path(img_path).stem}_small.jpg"
|
||
im.convert("RGB").save(small, quality=92)
|
||
return str(small)
|
||
except Exception:
|
||
return img_path # PIL 失败就用原图
|
||
|
||
def ocr_image(img_path: str) -> dict:
|
||
"""OCR 一张图片,返回排序后的文本"""
|
||
target = _prepare(img_path)
|
||
img = Path(target)
|
||
OCR_DIR.mkdir(parents=True, exist_ok=True)
|
||
out = OCR_DIR / f"{img.stem}_ocr.txt"
|
||
ps = OCR_DIR / f"{img.stem}_ocr.ps1"
|
||
ps.write_text(PS_TEMPLATE.replace("{{IMG}}", str(img))
|
||
.replace("{{OUT}}", str(out)),
|
||
encoding="utf-8-sig") # BOM:PowerShell 5.1 按 UTF-8 解析脚本
|
||
try:
|
||
r = subprocess.run(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
|
||
"-File", str(ps)], capture_output=True, text=True,
|
||
encoding="gbk", errors="replace", timeout=180)
|
||
except Exception as e:
|
||
return {"ok": False, "error": f"OCR 进程失败: {e}"}
|
||
if not out.exists():
|
||
return {"ok": False, "error": (r.stderr or r.stdout or "OCR 无输出")[:300]}
|
||
raw = out.read_text(encoding="utf-8", errors="ignore")
|
||
lines = []
|
||
for ln in raw.splitlines():
|
||
m = re.match(r"\[(\d+),(\d+)\] (.*)", ln)
|
||
if m:
|
||
lines.append((int(m.group(1)), int(m.group(2)), m.group(3).strip()))
|
||
text = _reorder(lines)
|
||
if not text.strip():
|
||
return {"ok": False, "error": "OCR 未识别到文字(图片可能不清晰)"}
|
||
return {"ok": True, "text": text, "lines": len(lines)}
|