身份证识别 API(免费 · 离线 · Docker · 极限低配可用)
上传身份证图片 → 返回结构化 JSON。OCR 用 RapidOCR(PaddleOCR 的 ONNX 版,Apache-2.0 可商用),服务用 FastAPI,全程不需要 GPU、不需要联网、不需要任何云服务密钥。
目标硬件:1 核 CPU / 2G 内存配额即可运行。速度高度依赖 CPU 单核性能:现代 x86 NAS(J4125 及以上)单张约 0.5–1.5s;老旧低端机型(如 Celeron 1007U @1.5GHz)约 2–6s,属于正常范围。
能力
- 正面:姓名、性别、民族、出生日期、住址、公民身份号码
- 反面:签发机关、有效期限
- 自动判正反面(也可用
side=front/back强制指定) - 身份证号 ISO 7064 MOD 11-2 校验位验证,识别错了会被标记
id_number_valid=false - 返回每字段平均置信度与耗时
- 可选脱敏返回(
mask=true)
1 核 2G 极限低配适配(重点)
2G 内存的真正风险不是模型本身(det/cls/rec 三个 onnx 权重合计约 15MB),而是三件事:
- onnxruntime 的线程风暴——Docker 的
cpus: 1.0限额不会改变容器内os.cpu_count(),onnxruntime / OpenBLAS 仍会按宿主机核数建线程池。64 核宿主机上容器能起出几十个线程,1 核机器上直接 OOM 或被 cgroup 杀。必须显式钉死。 - 大图的解码缓冲 + det 中间张量——内存随图片面积平方增长。
- 多 worker——每个 worker 独立加载一份模型,直接翻倍。
本项目已做六层控制:
| 层 | 手段 | 效果 |
|---|---|---|
| 进程 | --workers 1,引擎全局单例 |
不会加载多份模型;每多一个 worker 多 400–600MB |
| 线程 | OMP/OPENBLAS/MKL_NUM_THREADS=1 + cv2.setNumThreads(0) |
三重钉死,杜绝线程风暴 |
| 模型 | 关闭方向分类器 use_angle_cls=False |
身份证基本正向,少一个模型、少一次推理 |
| 输入 | 图片最长边 960px + det_limit_side_len=736(低端机型下调过,通用默认 1200/960) |
解码缓冲和 det 张量双降 |
| 推理 | rec_batch_num=1 |
batch=6 在单核下只涨峰值内存,不省时间 |
| 容器 | cpus: 1.0、mem_limit: 1024m |
留 1G 给系统 / Docker 本身 / 其他服务 |
| 入口 | content-length 预检 + 5MB 上限 + 6000px 尺寸硬上限 | 三层拦超大图,防解压炸弹打爆内存 |
内存预算(估算,建议你本地 docker stats 实测校准)
| 阶段 | RSS |
|---|---|
| Python 3.11 + FastAPI + uvicorn | ~150–200 MB |
| numpy + opencv-headless + pillow | ~120–150 MB |
| onnxruntime 运行时 | ~80–100 MB |
| 三个 onnx 模型常驻 + 图优化副本 | +150–250 MB |
| 单张 960px 图推理峰值 | +80–150 MB |
| 稳态 / 峰值 | 约 500–650 MB / 800MB–1.0GB |
容器限 1024m 缓冲不多,峰值贴着上限时先加 swap,再考虑降 MAX_SIDE=736。低端机型(4G 单条)强烈建议提前加 2G swap,这是最便宜的保险:
快速开始
cd idcard-api
docker compose up -d --build # 首次构建会预热模型,稍慢
docker stats idcard-api # 看内存是否在预期区间
curl http://127.0.0.1:8000/api/v1/idcard/health # 直接看 mem_mb 字段
接口文档:http://127.0.0.1:8000/docs
💡 根路径
/返回服务信息与端点清单,不是 404;/api/v1/idcard/health返回状态和内存。 若/docs打开是白屏,说明页面依赖的外网 CDN(jsdelivr)被墙或超时——不是服务故障,直接用 curl / Postman / Apifox 调接口即可,请求格式见下方「调用」。
发布到飞牛 fnOS(Docker 应用)
适合把服务放到 NAS 上、给局域网其他应用调用。主推 SSH 路径(多文件项目必须整体上传,Compose 界面只能贴单个 yaml,上下文文件传不上去)。
第 0 步:三项前置检查(SSH 进 fnOS 后执行)
uname -m # 必须是 x86_64;若是 aarch64 见下方说明
free -h # 看可用内存,关键!见下方警告
ss -tlnp | grep 8000 # 确认 8000 端口没被别的容器占用
⚠️ 内存警告:
mem_limit: 1024m是给容器的配额,fnOS 系统本身要占 1G+。当前默认值已针对 Celeron 1007U / 4GB 单条内存这类低端机型调好;NAS 总内存 ≥8G 可调回1280m并把MAX_SIDE调回1200。4G 机型务必加 2G swap 兜底。
⏱️ 速度预期(Celeron 1007U @1.5GHz):单张身份证约 2–6 秒。这块 ULV 单核比常见 NAS 的 J4125 弱 3 倍以上,
MAX_SIDE=960就是为速度做的妥协(文字仍有约 25px 高,清晰印刷体识别率影响很小)。慢是正常的,不是没调好。
第 1 步:把整个项目目录传到 NAS
# fnOS 上建目录(存储池编号按实际改,/volume1 或 /vol1 都见过)
mkdir -p /volume1/docker/idcard-api
用 WinSCP / FileZilla 把本地的 idcard-api 目录整个拖上去(app/、Dockerfile、docker-compose.yml、requirements.txt、client_example.py、.env.example),保持目录结构。或者用 scp:
scp -r idcard-api root@飞牛IP:/volume1/docker/
第 2 步:配置 .env
cd /volume1/docker/idcard-api
cp .env.example .env
vi .env # 配 API_TOKENS(每方一把 token),见「鉴权」一节
第 3 步:构建并启动(最容易卡的是模型下载)
docker compose config # 先检查 yaml 语法,没问题再往下
docker compose up -d --build
docker compose logs -f # 看到 "engine ready" 且无 Traceback 即成功
构建卡住/失败的两种原因和对策:
- pip 慢:Docker Hub 基础镜像可在 fnOS「设置 → Docker → 镜像仓库」配轩辕等加速源;pip 慢就在 Dockerfile 的
pip install后加-i https://pypi.tuna.tsinghua.edu.cn/simple。 - onnx 模型下载失败(构建到预热那步报错,国内访问 GitHub 不稳):在能上网的 Windows 上先执行
把生成模型目录(一般在
pip install rapidocr-onnxruntime python -c "from rapidocr_onnxruntime import RapidOCR; RapidOCR()"C:\Users\<你>\.rapidocr_model)传到 NAS/volume1/docker/idcard-api/models,compose 已挂载/app/models,重跑docker compose up -d --build即可。
第 4 步:验证
curl http://127.0.0.1:8000/api/v1/idcard/health # 看 status / mem_mb
curl -X POST http://127.0.0.1:8000/api/v1/idcard/recognize \
-F "file=@/volume1/docker/idcard-api/test.jpg" -F "side=auto" \
-H "X-API-Key: 你的密钥"
docker stats idcard-api # 看内存峰值是否贴着 1280m
建议先拿一张真实身份证正反面各测一次,确认字段和 id_number_valid=true 再交给调用方。
第 5 步:接入调用方
- 同 NAS 上的其他 Docker 容器:把调用方容器加入
idcard-api_idcard-api网络(或在调用方 compose 里加external_links/ 同一自定义网络),地址写http://idcard-api:8000;图省事直接用http://飞牛IP:8000。 - 局域网其他设备:
http://飞牛IP:8000,带Authorization: Bearer <token>头。 - 要域名 + HTTPS:用 fnOS 自带的反向代理/远程访问把域名指到
127.0.0.1:8000,不要在路由器上映射公网端口直连。 - ARM 机型(aarch64):python:3.11-slim 和 opencv-headless 有 ARM 版,但 onnxruntime 需 ≥1.19 才有完善的 ARM wheel,先在 NAS上
docker compose build试;若 onnxruntime 装不上,只能换 x86 机型或用云侧部署。
第 6 步:以后更新代码怎么发(重要)
fnOS 的「重新部署 / 重建容器」只会 recreate 容器,不会 rebuild 镜像——environment 变了会把新变量注进新容器,但代码改动完全不会进去。所以改完代码必须显式 build。
cd /vol1/1000/Docker/idcard-api # 换成你的实际目录
sudo docker compose -p icd up -d --build
-p icd不能省:镜像名icd-idcard-api说明 compose 项目名是icd,不带这个参数会建成另一个项目,跟现有容器对不上。宿主机用户如果不在 docker 组,命令前都要sudo(或执行一次sudo usermod -aG docker $USER && newgrp docker)。
| 你改了什么 | 命令 | 耗时 |
|---|---|---|
app/*.py |
up -d --build |
10–30 秒 |
requirements.txt |
up -d --build(pip 层失效重跑) |
1–3 分钟 |
.env / docker-compose.yml |
up -d(自动 recreate,无需 build) |
秒级 |
| 怀疑缓存脏 / 要刷基础镜像 | build --no-cache + up -d --force-recreate |
3–5 分钟 |
--no-cache 不是日常操作:Docker 的层缓存按内容哈希失效,COPY app ./app 依赖代码内容,代码一变这一层和后续层必然重跑,前面的 apt/pip 层照常命中缓存。
也可以直接用 deploy.sh(已随项目提供),它会增量构建、等就绪、校验镜像内代码真的更新了,失败自动降级全量重建:
bash /vol1/1000/Docker/idcard-api/deploy.sh
如果脚本报
\r: command not found,是 Windows 换行符问题,执行sed -i 's/\r$//' deploy.sh修一下。
调用
curl -X POST http://127.0.0.1:8000/api/v1/idcard/recognize \
-F "file=@/path/idcard.jpg" -F "side=auto"
base64 方式:
curl -X POST http://127.0.0.1:8000/api/v1/idcard/recognize/base64 \
-H "Content-Type: application/json" \
-d '{"image_base64": "<base64>", "side": "auto", "mask": false}'
返回示例
{
"code": 0,
"data": {
"side": "front",
"name": "张三",
"gender": "男",
"nation": "汉",
"birth": "19900101",
"address": "北京市朝阳区某某路1号",
"id_number": "110101199001011237",
"id_number_valid": true,
"issuer": null,
"validity": null,
"field_count": 6,
"confidence": 0.96,
"elapsed_ms": 980.5,
"raw_text": "姓名张三 | 性别男 | ..."
}
}
Python 示例见 client_example.py。
C# / .NET 调用示例(server-to-server)
要点三个:必须带鉴权头、必须检查 HTTP 状态码(否则 401 会被当成"未识别到"静默吞掉)、超时要给足(低端 NAS 首张要加载模型 10–20 秒)。
// 配置里加一项 IdCardOcrToken,与服务端 .env 的 API_TOKENS 对应
string token = ServiceSettingsPolicy.GetIdCardOcrToken(service);
using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
if (!string.IsNullOrWhiteSpace(token))
{
// 新版服务(推荐):标准 Bearer 头
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// 或旧版服务(仅设置了 API_KEY):改用下面这行
// request.Headers.TryAddWithoutValidation("X-API-Key", token);
}
request.Content = form;
using var response = client.SendAsync(request).GetAwaiter().GetResult();
// 关键:先判状态码,别让 401/413/422 掉进 JSON 解析分支变成"静默失败"
if (!response.IsSuccessStatusCode)
{
string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Console.WriteLine($"[UploadIdCardFile] OCR HTTP {(int)response.StatusCode}: {body}");
return data; // 前端提示"未识别到信息",后端日志里有真实原因
}
HttpClient 建议用 IHttpClientFactory 或静态单例,不要每次 new——高频调用会耗尽 socket。
鉴权(Token 验证)
支持三种方式,全部用标准库实现(HMAC-SHA256),不引入 PyJWT 等依赖,对 1C/2G 无额外开销。
方式一:多 token(推荐)
.env 里按 名称:token 配置,多个用逗号分隔:
API_TOKENS=frontend:abc123def456,win-service:xyz789uvw012
好处是每个调用方一把,能单独吊销(删掉对应那段重启即可),日志里也会打出调用方名称,出问题能定位是谁在调。
调用时带标准 Bearer 头:
curl -X POST http://10.1.1.163:8000/api/v1/idcard/recognize \
-H "Authorization: Bearer xyz789uvw012" \
-F "file=@idcard.jpg"
旧的 X-API-Key 头仍然有效(兼容不改代码的老调用方);API_KEY 单密钥配置也继续支持。
方式二:短期 token(前端场景推荐)
设置 CLIENT_SECRET 后,多出一个换 token 的接口:
# ① 用 client_secret 换 token(默认 1 小时有效)
curl -X POST http://10.1.1.163:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"client_id":"frontend","client_secret":"你的密钥"}'
# 返回
{"token":"eyJzdWIiOiJmcm9udGVuZCIsImlhdCI6...9f2a","expires_in":3600,"token_type":"Bearer"}
# ② 后续请求带这个 token
curl -X POST http://10.1.1.163:8000/api/v1/idcard/recognize \
-H "Authorization: Bearer eyJzdWIi..." \
-F "file=@idcard.jpg"
token 是 base64(payload) + "." + HMAC签名 的结构(类似 JWT 但没有第三段),服务端校验签名和 exp 过期时间。TOKEN_TTL 可调有效期。
适合前端的原因:页面里不硬编码长期密钥,泄露了也就一小时的窗口。
方式三:不配置(本地调试)
API_TOKENS、API_KEY、CLIENT_SECRET 全为空时,接口完全开放。只适合本机调试——/api/v1/idcard/health 会返回 auth_enabled: false,启动日志也会警告。
几个实现细节
/api/v1/idcard/health不鉴权,方便探活和监控;它不返回任何身份证数据。- 401 响应带
WWW-Authenticate: Bearer头,符合 HTTP 标准,前端库能自动识别。 - 签名比较用
hmac.compare_digest(常数时间),避免时序侧信道攻击。 /docs不做鉴权,方便调试;要锁起来就在 Nginx 反代层加 basic auth。- 日志只记调用方名称和耗时,不记姓名/身份证号。
- 静态 token 以明文存在容器环境变量里,能读
docker inspect的人就能看到。安全要求更高的话,用短期 token 模式 + 只把CLIENT_SECRET挂在服务端,客户端只拿短时 token。
跨域(CORS)
先分清调用方:
| 调用方 | 有跨域限制吗 | 需要做什么 |
|---|---|---|
| 后端服务(curl、Python requests、其他 Docker 容器、Java/Go 等) | ❌ 没有 | 什么都不用配 |
| 浏览器前端(Vue / React / 原生 JS 页面) | ✅ 有 | 服务端开 CORS + 请求头带 Authorization: Bearer <token> |
CORS 是浏览器的同源策略,只在浏览器环境生效。所以「别的应用调用」如果是后端服务,完全不受影响;如果是网页前端,才会撞到。
服务已内置 CORSMiddleware,由环境变量 CORS_ORIGINS 控制:
# .env 里配置,逗号分隔;* 表示允许所有来源(开发方便)
CORS_ORIGINS=http://10.1.1.163:5173,http://10.1.1.163:8080
几个要点:
- 预检请求:前端带
X-API-Key自定义头会触发OPTIONS预检,中间件已自动处理(返回 204 + 允许头),无需额外路由。 - 鉴权头必须在
allow_headers里:已设allow_headers=["*"],X-API-Key可通过。 allow_credentials=False是刻意的:本项目用请求头鉴权、不用 Cookie,而allow_origins=["*"]与 credentials=True 是非法组合,浏览器会直接拒绝。- 安全提醒:身份证是敏感数据。
CORS_ORIGINS=*且未配置任何 token 时,局域网内任何一个网页的 JS 都能悄悄调用你的接口读数据。生产必须二选一:配API_TOKENS,或把CORS_ORIGINS收紧到具体前端地址。 - 生产更推荐同源:用 Nginx / fnOS 反向代理把前端和 API 放在同一域名下(
/api转发到 8000),浏览器视为同源,连 CORS 都不需要,也少一层预检开销。
如果内存还是吃紧(降级路径)
按代价从低到高:
MAX_SIDE=960+DET_LIMIT_SIDE_LEN=736:身份证文字在 960px 下仍有 ~25px 高,识别基本无损,通常再省 100–150MB- 换 PP-OCR mobile 版识别模型(
ch_PP-OCRv4_mobile_rec),体积和内存都小于 server 版,身份证这种清晰印刷体精度损失很小 - 加 swap(2–4G)作为兜底——会拖慢速度,但能防 OOM 被杀。这是 2G 机器强烈建议做的:
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile sudo mkswap /swapfile && sudo swapon /swapfile - 极端情况换 Tesseract +
chi_sim:内存可压到 200MB 内,但中文证件识别率明显下降,不推荐
并发上不去时:低配下不要加 worker,改成前面挂 Nginx 限流排队。1 核的现实吞吐大约 1–2 QPS,超过就会堆积。要扛量就横向多开几个容器 + 前面 LB,而不是在单机上加进程。
准确率与调优
- 清晰扫描件 / 规整拍照:字段级 95%+;模糊、反光、强倾斜、复印件会明显下降。
- 掉得最多的通常是住址(长文本)和名字里的生僻字。建议对
id_number_valid=false的结果走人工复核,不要直接采信。 - 拍照场景强烈建议前端先做一次方向校正 + 边缘裁剪,比在服务端补救有效得多。
- 想加"先检测身份证区域再 OCR"(防复杂背景),可接 YOLOv8n 做前置裁剪,挂在
_load_image之后即可——1C/2G 下不建议加,会再吃 100MB 内存和一大截 CPU。
合规与安全(重要)
身份证号属于敏感个人信息,受《个人信息保护法》《数据安全法》约束:
- 这个方案的价值就在数据不出你自己的机器。别为了省事换成公有云 API,那等于把 PII 交给第三方。
- 服务不落盘、不缓存图片,
raw_text含明文,生产环境建议要么关掉、要么mask=true。 - 日志里不要打印身份证号和姓名。当前实现只打加载信息。
- 对外必须 HTTPS + 鉴权 + 限流;已做两层大小防护(content-length 预检 + 惰性尺寸检查),
MAX_IMAGE_BYTES=5MB。 - 收集前应有用户授权与隐私政策,明确保留期限——最安全的做法是识别完即丢弃。
目录
idcard-api/
├── app/
│ ├── main.py # FastAPI 服务、鉴权、图片校验、低配参数
│ └── parser.py # 关键词锚点解析 + 身份证校验位
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── client_example.py
常见问题
构建时报 401 Unauthorized(拉不下 python:3.11-slim 元数据):Docker 镜像源问题,与项目无关。注意 fnOS 的 Docker 应用会用自己的界面配置覆盖 /etc/docker/daemon.json,手工改文件常常不生效(报错 URL 仍是 docker.fnnas.com 就是没生效)。本项目已改走第三条路——在 Dockerfile 里直接写国内镜像仓库全路径,不依赖 daemon 配置:
ARG BASE_IMAGE=docker.m.daocloud.io/library/python:3.11-slim
FROM ${BASE_IMAGE}
切换源只需在 .env 里设 BASE_IMAGE,构建前先探活:
for m in docker.m.daocloud.io docker.1ms.run mirror.ccs.tencentyun.com; do
echo -n "$m -> "; curl -s -o /dev/null -w "%{http_code}\n" --max-time 8 \
"https://$m/v2/library/python/manifests/3.11-slim"
done
返回 200 可用;401 且带 WWW-Authenticate 头属于 Registry 正常认证流程,Docker 能自动处理;连接超时/5xx 则换下一个。全部不通就走离线导入兜底:
# 1) 在能联网且有 Docker 的机器上(你的 Windows 本机装了 Docker Desktop 即可)
docker pull python:3.11-slim
docker save python:3.11-slim -o python311slim.tar
# 2) 传到 NAS 后导入
docker load -i python311slim.tar
# 3) 构建时指向本地 tag
# 在 .env 里设 BASE_IMAGE=python:3.11-slim,然后 docker compose up -d --build
pip 装包慢/断:与镜像源无关,是另一条网络路径。Dockerfile 已默认清华源(PIP_INDEX_URL),需要时构建覆盖。
构建时模型下载失败:国内访问模型源不稳。先在能上网的机器上 python -c "from rapidocr_onnxruntime import RapidOCR; RapidOCR()" 生成模型目录,再拷进 /app/models 卷;pip 可加 -i https://pypi.tuna.tsinghua.edu.cn/simple。
容器起不来 / 报 OOM:先 docker stats 看峰值,或 curl .../health 看 mem_mb。若峰值贴着 mem_limit,按上面降级路径降 MAX_SIDE;反复被杀就先加 swap。
RapidOCR rejected {...} 警告:正常,代码在逐级降级尝试构造参数,最终会用可用组合启动,不会失败。锁死 requirements.txt 版本后这条警告会消失。
CPU 跑满但吞吐很低:典型症状是线程在抖。检查容器内 echo $OMP_NUM_THREADS 必须是 1;如果宿主机是多核而 compose 里只写了 cpus: 1.0,环境变量没生效就会这样。
返回 422 no text detected:图片不是身份证、或分辨率过低/文字太小。
性别/民族识别成空:正面右侧那一行字小,常见于低清图。提高图片质量,或换 mobile/server 识别模型。
numpy 相关报错:requirements.txt 里锁了 numpy<2,不要手动升。onnxruntime 与 numpy 2 存在兼容问题。