space ocr
指南文章价格文档
developer

一个返回可验证边界框的 OCR API

大多数 OCR API 都会返回边界框,但坐标系各不相同,而一个框只告诉你值的出处。这是一份面向开发者的指南:data.cells[path] 里归一化到 0–1000 的 box、跟随倾斜的 quad,以及点名哪些值值得复核的 verified / review 约定。

8 分钟阅读· 2026-08-31

边界框是你验证 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 VisionboundingPoly 顶点,单位为源图像的像素(部分能力返回的是 normalizedVertices仅文本 + 几何信息(结构化键值对属于 Google Document AI,是单独的产品)每个词/符号的识别置信度(0–1)
TesseracthOCR / TSV 框,单位为像素(本地库,不是托管 API)无——仅原始文本 + 版式每个词的识别置信度(0–100)
Amazon TextractBoundingBox,按页面宽高归一化到 0–1(外加同样是 0–1 的 Polygon表单/表格用 AnalyzeDocument;票据用 AnalyzeExpense每个 block 的识别置信度(%)
Azure Document Intelligence边界多边形,单位为像素(图像)或英寸(PDF)预构建/自定义模型每个词的识别置信度
space-ocr以你声明的字段路径为键,归一化到 0–1000box,外加跟随倾斜的 quad由你用 fields 声明(明细行用 children),或交给 autoFieldsverified 判定 + review.flagged 待复核清单(依据在 evidence

有两点值得留意。第一,坐标单位没法直接拿去复用——像素框绑死在实际被读取的那张图像上,而归一化的框在缩放后依然有效。第二,逐值那一列量的未必是同一件事识别置信度回答的是"引擎对自己的读取有多确定",这跟"返回的这个值究竟有没有在页面上被找到"是两个问题。

✓ Verified

框是怎么推导出来的,和它的格式同样要紧。 在 space-ocr 中,语言模型返回的只是每个字段的文本,以及它用到了哪些词元(word token)的提示,框本身从来不由它给出。引擎随后拿这段文本,去跟视觉 OCR 在页面上实际检测到的符号做逐字符匹配,于是框就落在这些字符被找到的真实像素上。凡是跑过这道比对的值,其单元格的 evidence 里会带一个 match_ratio,表示它被定位到的比例;要是压根没有可比对的对象,这个键就不会出现。这些词元提示可能带噪声(有时会在重复出现的行之间张冠李戴),所以系统用列一致性和行一致性检查来验证它们,而不是盲目相信。这正是"模型断言的坐标"和"回过头来跟页面核对过的坐标"之间的区别。

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_mismatchlow_rationoboxmissing 等。
  • evidence——判定背后的原始信号:text_match(逐字符比对本身)、sourcevision_symbol_matchtoken_id)、match_ratio,以及 printed_text——OCR 在那组坐标处读到的字形。

同一批路径还会出现在 data.review.flagged 里,那就是待办清单:每个需要看一眼的值一条,附上它的理由。data.image 给出的是实际被读取的那一页的宽高,所有坐标都以它为基准。(如果你声明了标量 type,或给 string 字段加了 patternenum,还会多出一层 data.normalized;下面这个只有 string 的例子不会产生它。)

一个值:values 与 cells
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "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 方向已经烘进像素里,大图在读取前还会被缩小——所以它的 widthheight 有可能跟你上传的正好对调(发 4000×3000,拿回 3000×4000)。以 data.image 为基准换算,算式就不会错。

要在显示出来的图像上画一个框,只需换算一次:

  • SVG 叠加层——给 SVG 设 viewBox="0 0 1000 1000",然后原样画出 boxquad
  • 绝对定位的 div——leftPct = xmin / 1000 * 100topPct = ymin / 1000 * 100widthPct = (xmax - xmin) / 1000 * 100heightPct = (ymax - ymin) / 1000 * 100
  • 换回像素——pixel_x = box.xmin / 1000 * data.image.widthpixel_y = box.ymin / 1000 * data.image.height

由于 EXIF 方向已经应用在那一页上,一张旋转过的手机照片(方向 6/8)也不用你这边再做一遍纠正。但页面不会被摆正(deskew):拍歪的照片依旧是歪的,这正是为什么 quad 跟着倾斜走,而 box 在它周围保持轴对齐。

请求字段并拿回坐标
1
2
3
4
5
6
7
8
9
10
11
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: falseevidence.text_match: true 同时出现。这说明这个值的字符跟页面对得上,是被你声明的规则拦下的。两者都值得复核,只是理由不同——而且哪一种都不能反过来给对方背书,因为两个引擎也可能在同一个误读上达成一致。

先验证,再查询——无需重跑 OCR

数据留得下来,坐标才最有用。用 POST /upload 把图像推入一张表,再用 GET /view 在服务端查询它——wheresortselectlimitoffset——比如把所有 total >= 40000 的行拉出来,既不重跑 OCR,也不再付一次费。筛选的对象是这张表自己的列(外加 nameocrStatuscreatedAt),每一行都会连同完整的 cells 映射一起返回,所以 boxquad 都还在;想要更轻量的响应体,就加上 boxes=0 把它们去掉。关于验证工作流的深入讲解,参见 用边界框验证 OCROCR 审计记录

点击任意一个值,它的源区域就会在原件上高亮——正是 API 返回的那组坐标,做成了可交互的样子。

如何从 API 拿到可验证的边界框

  1. 请求字段
    把图像 POST 到 /ocr/fields,imageType 设为 'url' 或 'base64',再附上你自己的 fields 数组,或者把 autoFields 设为 true。引擎读取的是栅格图像。
  2. 读取坐标
    按字段路径查 data.cells。每个单元格都带着 0–1000 网格上的 box { xmin, ymin, xmax, ymax }、四点 quad、判定 verified、review 和 evidence。
  3. 叠加或换算
    用 viewBox 为 '0 0 1000 1000' 的 SVG 画框,或用 pixel_x = box.xmin / 1000 * data.image.width 换算成像素。data.image 就是实际被读取的那一页,EXIF 旋转已经应用过。
  4. 处理复核清单
    别自己对分数设阈值,直接遍历 data.review.flagged:每一条都把路径和理由配成一对,依据则在 cells[path].evidence 里,包括 match_ratio。
  5. 存储并查询
    用 /upload 把图像推入一张表,再用 GET /view(where、sort、select)查询它——每一行都保留带 box 和 quad 的 cells 映射,既不重跑 OCR,也不再付一次费。
哪些 OCR API 会返回边界框?
Google Cloud Vision、Tesseract、Amazon Textract 和 Azure AI Document Intelligence 都会随文本一起返回几何信息,space-ocr 也一样。按 2026 年 8 月各家的公开文档,它们的区别在于坐标系(Vision 和 Tesseract 用像素;Azure 图像用像素、PDF 用英寸;Textract 归一化到 0–1;space-ocr 用 0–1000 网格)、在于返回的是结构化字段还是只有原始文本加版式,以及那个逐值数字究竟报告了什么。
边界框坐标是像素还是归一化的?
space-ocr 返回的 box 归一化到一个 0–1000 网格上,与图像的像素尺寸无关,另外还有一个跟随页面倾斜的四点 quad。要换算成像素,用 pixel_x = box.xmin / 1000 * data.image.width(y 同理)即可;或者直接用 viewBox 为 '0 0 1000 1000' 的 SVG 叠上去。基准面是 data.image,也就是经过 EXIF 方向处理和必要缩小之后、实际被读取的那一页,所以它的宽高可能和你发过去的文件不同。
OCR 置信度分数和匹配比例有什么区别?
识别置信度反映的是引擎对自己读取结果有多确定。而 match_ratio 度量的是:返回值的文本有多少真的能在页面级 OCR 检测到的符号中被找到——这是一种来自外部的核对,而不是自我报告。它作为依据放在 cells[path].evidence 里:达到 0.85 及以上才算可信的字符匹配,覆盖不足时引擎会在该单元格的 review 理由里立起 low_ratio,所以你的代码是对 review 设闸门,而不是对这个数字。
对于拍歪或旋转的照片,我能拿到带方向的边界框吗?
可以。每个单元格都会在轴对齐的 box 之外返回一个由四个有序顶点(左上、右上、右下、左下)组成的 quad,而 quad 会跟随单据的倾斜角度。页面不会被摆正(deskew),而坐标所基于的那一页(data.image)已经应用过 EXIF 方向,所以一张方向为 6 或 8 的手机照片,也不用你这边再做一遍纠正。
这种带边界框的 OCR 支持日文、韩文和中文吗?
支持。同一个引擎处理中日韩文字和拉丁文字,自动检测语言——无需设置任何语言参数——不管是哪种文字,包括全角字符,每个值都按同一套约定返回:以你声明的字段路径为键,data.cells 里的 box、quad、verified、review 和 evidence。
相关文章