一個會回傳可驗證定界框的 OCR API
多數 OCR API 都會回傳定界框,但座標系統各不相同,而一個框只告訴你值的來源。這是一份開發者導向的指南:data.cells[path] 裡正規化為 0–1000 的 box、跟著傾斜走的 quad,以及點名哪些值值得再看一眼的 verified / review 約定。
定界框是你驗證 OCR 的依據。一個光禿禿的字串只告訴你模型認為它讀到了什麼;而一個框會告訴你它是在頁面上的哪個位置讀到的,這樣你(或你的審核者,或你的程式碼)就能拿這個值去對照原件,而不必盲目相信。如果你要把 OCR 整合進任何會被稽核的場景——發票、費用、KYC、紀錄管理——「模型回傳了 total: 2,045」是不夠的;你得能指出 2,045 到底是從哪些像素來的。
好消息是,多數主流 OCR API 確實會回傳定界框。麻煩在於,一旦你開始動手做,它們有三個會帶來影響的差別——座標系統、你是不是也拿得到結構化欄位(而不只是純文字),以及框旁邊那個逐值訊號究竟是什麼。本指南會把這三點逐一走一遍,並示範當座標帶著一份明確的複核約定——逐值的判定,加上一份值得再看一眼的路徑清單——一起回來時,一個 OCR API 長什麼樣子。
多數 OCR API 都會回傳框——以下是它們的差別
Google Cloud Vision、Tesseract、Amazon Textract 和 Azure AI Document Intelligence 都會在回傳文字時一併附上幾何資訊。它們的分歧在於座標系統、你拿到的是結構化欄位還是只有純文字加版面,以及那個逐值數字到底回報了什麼。下表整理的是 2026 年 8 月各家的公開文件——這些產品一直在變,估算整合工作量之前,請以最新文件再核對一次。
| API | 座標系統 | 結構化欄位 | 逐值訊號 |
|---|---|---|---|
| Google Cloud Vision | boundingPoly 頂點,以來源影像的像素為單位(部分功能回傳的是 normalizedVertices) | 只有文字+幾何(結構化的鍵值對屬於 Google Document AI,是另一個獨立產品) | 每個字/符號的辨識信心(0–1) |
| Tesseract | hOCR/TSV 框,以像素為單位(本機函式庫,不是託管 API) | 無——只有純文字+版面 | 每個字的辨識信心(0–100) |
| Amazon Textract | BoundingBox,以頁面寬高正規化為 0–1(+同樣是 0–1 的 Polygon) | 透過 AnalyzeDocument 處理表單/表格;收據透過 AnalyzeExpense | 每個區塊的辨識信心(%) |
| Azure Document Intelligence | 定界多邊形,以像素(影像)或英吋(PDF)為單位 | 預建/自訂模型 | 每個字的辨識信心 |
| space-ocr | 以你宣告的欄位路徑為鍵,正規化為 0–1000 的 box,外加跟著傾斜走的 quad | 由你用 fields 宣告(明細項目用 children),或交給 autoFields | verified 判定+review.flagged 待確認清單(依據在 evidence) |
有兩件事值得注意。第一,座標單位沒辦法直接拿去沿用——像素框綁死在實際被讀取的那張影像上,而正規化的框則撐得過縮放。第二,逐值那一欄量的未必是同一件事:辨識信心回答的是「引擎對自己的讀法有多確定」,這跟「回傳的這個值究竟有沒有在頁面上被找到」是兩個問題。
框是怎麼推導出來的,跟它的格式一樣重要。 在 space-ocr 中,語言模型回傳的只是每個欄位的文字,以及它用到了哪些 word token 的提示,框本身從來不由它給出。引擎接著拿這段文字,去跟視覺 OCR 在頁面上實際偵測到的符號做字元層級的比對,於是框就落在這些字元真正被找到的像素上。凡是跑過這道比對的值,其儲存格的 evidence 會帶一個 match_ratio,代表它被定位到的程度;要是根本沒有可比對的對象,這個鍵就不會出現。那些 token 提示可能帶雜訊(有時會在重複的列之間張冠李戴),所以系統用欄一致性與列一致性檢查來驗證它們,而不是盲目相信。這就是「模型宣稱的座標」和「被拿回頁面上重新核對過的座標」之間的差別。
space-ocr 為每個值回傳什麼
業務資料留在 data.values,形狀就是你請求的那套 schema。而這個值從哪裡來、有沒有通過檢查,則集中在另一張以相同路徑為鍵的對應表 data.cells——例如 total,明細項目則是 items[0].price。每個儲存格都帶著:
box——一個軸對齊的矩形{ xmin, ymin, xmax, ymax },由整數組成,建立在 0–1000 正規化的網格上(0,0 = 左上,1000,1000 = 右下),與影像的像素尺寸無關。quad——四個有序的點(左上、右上、右下、左下),組成一個跟著文件傾斜角度走的有方向定界框,所以歪斜的手機照片照樣框得乾淨。它一定和box一起回傳。verified——判定,也是review的鏡像:只要有任何東西被標記就是false;什麼都沒標記、而且比對確實跑過,就是true;什麼都沒標記但根本沒有可比對的對象(例如整列的聯集框)則是null。review——不是null,就是{ reasons }:理由依排序排列,第一個是主因。代碼包括text_mismatch、low_ratio、nobox和missing等。evidence——判定背後的原始訊號:text_match(字元比對本身)、source(vision_symbol_match、token_id)、match_ratio,以及printed_text——OCR 在那組座標處讀到的字形。
同一批路徑也會出現在 data.review.flagged 裡,那就是待辦清單:每個需要看一眼的值一筆,附上它的理由。data.image 給的是實際被讀取的那一頁的寬高,所有座標都以它為基準。(若你宣告了純量 type,或替 string 欄位加上 pattern、enum,還會多出一層 data.normalized;下面這個只有 string 的例子不會產生它。)
{
"data": {
"values": { "total": "2,045" },
"cells": {
"total": {
"box": { "xmin": 381, "ymin": 803, "xmax": 500, "ymax": 825 },
"quad": [
{ "x": 380, "y": 804 }, { "x": 500, "y": 801 },
{ "x": 500, "y": 823 }, { "x": 381, "y": 826 }
],
"verified": true,
"review": null,
"evidence": {
"text_match": true,
"source": "vision_symbol_match",
"match_ratio": 1.0,
"printed_text": "2,045"
}
}
},
"image": { "width": 1654, "height": 2339 }
}
}像素還是正規化?轉換一次,縮放就不再出包
OCR 整合常見的一個 bug,是像素座標綁死在你上傳的那張影像上——為了存檔把它縮放或重新壓縮、或漏掉一個 EXIF 旋轉旗標,疊上去的框就會飄移、被裁切,或落在錯的文字上。正規化座標能避開這一整類 bug:一個 0–1000 的框能對應到同一頁面的任何一種呈現方式。
先要弄清一件事:基準面是 data.image,而不是你送出的那個檔案。它是實際被讀取的那一頁——EXIF 方向已經烘進像素裡,大張照片在讀取前還會先縮小——所以它的 width 和 height 有可能跟你上傳的正好對調(送 4000×3000,拿回 3000×4000)。以 data.image 為基準來換算,算式才會對得上。
要在顯示出來的影像上畫一個框,只要轉換一次:
- SVG 疊圖——給 SVG 設
viewBox="0 0 1000 1000",然後原封不動地畫出box或quad。 - 絕對定位的 div——
leftPct = xmin / 1000 * 100、topPct = ymin / 1000 * 100、widthPct = (xmax - xmin) / 1000 * 100、heightPct = (ymax - ymin) / 1000 * 100。 - 轉回像素——
pixel_x = box.xmin / 1000 * data.image.width、pixel_y = box.ymin / 1000 * data.image.height。
由於 EXIF 方向已經套用在那一頁上,一張旋轉過的手機照片(方向 6/8)也不必你這邊再做一次校正。但頁面不會被擺正(deskew):拍歪的照片依舊是歪的,這正是為什麼 quad 跟著傾斜走,而 box 在它周圍維持軸對齊。
curl -s https://api.space-ocr.com/ocr/fields \
-H "Authorization: Bearer $SPACE_OCR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://example.com/receipt.jpg",
"imageType": "url",
"fields": [
{ "name": "vendor", "type": "string" },
{ "name": "total", "type": "string" }
]
}'信心分數、比對命中率,以及判定
這個區別值得你記牢。多數 OCR API 記載的是辨識信心——一個反映引擎對自己讀法有多確定的數字,根據的是字型清晰度、影像品質之類的因素。它有用,但說到底是模型在替自己的作業打分數。而比對命中率量的是外部的事實:模型回傳的值裡那些字元,有多少真的在頁面層級 OCR 偵測到的符號之中被找到。一個值可能帶著相當高的辨識信心被回傳出來,卻仍然跟頁面上的任何東西對不上。
不過 API 並不會把這個比率丟給你、讓你自己挑一個門檻。match_ratio 作為判定的依據放在 evidence 裡;門檻由引擎自己施加——達到 0.85 以上才算高信心的字元命中——覆蓋不足時,那個儲存格會帶著 review 裡的 low_ratio 回來。所以你程式碼裡的閘門是 review != null,更好的做法是直接走訪 data.review.flagged,它還能觸及命中率看不到的那幾類:宣告為 required 卻根本沒回來的值(missing)、沒有座標的值(nobox)、違反了你所宣告的 pattern 或範圍的值。
有一種組合常讓人意外,其實不必:verified: false 和 evidence.text_match: true 同時出現。這代表這個值的字元跟頁面對得上,是被你宣告的規則攔下來的。兩者都值得複核,只是理由不同——而且哪一種都不能反過來替對方背書,因為兩個引擎也可能在同一個誤讀上取得一致。
先驗證,再查詢——不必重跑 OCR
資料留得住,座標才最有用。用 POST /upload 把影像推進一張表格,再用 GET /view 在伺服器端查詢它——where、sort、select、limit、offset——好把比方說每一列 total >= 40000 的資料抓出來,不必重跑 OCR,也不另外收費。篩選的對象是這張表格自己的欄位(外加 name、ocrStatus、createdAt),而每一列都會連同完整的 cells 對應表一起回來,所以 box 和 quad 都還在;想要更輕的回應,就加上 boxes=0 把它們丟掉。想深入了解這套驗證流程,請看 用定界框驗證 OCR 和 OCR 稽核軌跡。
如何從這個 API 拿到可驗證的定界框
- 請求欄位把影像 POST 到 /ocr/fields,imageType 用「url」或「base64」,再附上你自己的 fields 陣列,或把 autoFields 設為 true。引擎讀取的是點陣影像。
- 讀取座標用欄位路徑查 data.cells。每個儲存格都帶著 0–1000 網格上的 box { xmin, ymin, xmax, ymax }、四點 quad、判定 verified、review 以及 evidence。
- 疊圖或轉換用一個 viewBox「0 0 1000 1000」的 SVG 畫出框,或用 pixel_x = box.xmin / 1000 * data.image.width 轉成像素。data.image 就是實際被讀取的那一頁,EXIF 旋轉已經套用過。
- 處理待確認清單別自己對分數設門檻,直接走訪 data.review.flagged:每一筆都把路徑和理由配成一組,依據則在 cells[path].evidence 裡,也包含 match_ratio。
- 儲存並查詢用 /upload 把影像推進一張表格,再用 GET /view(where、sort、select)查詢它——每一列都保留帶有 box 和 quad 的 cells 對應表,不必重跑 OCR,也不另外收費。