Files
Docker/idcard-api/README.md
T
2026-10-18 07:40:42 +08:00

352 lines
18 KiB
Markdown
Raw 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.
# 身份证识别 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),而是三件事:
1. **onnxruntime 的线程风暴**——Docker 的 `cpus: 1.0` 限额**不会**改变容器内 `os.cpu_count()`,onnxruntime / OpenBLAS 仍会按宿主机核数建线程池。64 核宿主机上容器能起出几十个线程,1 核机器上直接 OOM 或被 cgroup 杀。必须显式钉死。
2. **大图的解码缓冲 + det 中间张量**——内存随图片面积平方增长。
3. **多 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,这是最便宜的保险:
## 快速开始
```bash
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 后执行)
```bash
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
```bash
# 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:
```bash
scp -r idcard-api root@飞牛IP:/volume1/docker/
```
### 第 2 步:配置 .env
```bash
cd /volume1/docker/idcard-api
cp .env.example .env
vi .env # 配 API_TOKENS(每方一把 token),见「鉴权」一节
```
### 第 3 步:构建并启动(最容易卡的是模型下载)
```bash
docker compose config # 先检查 yaml 语法,没问题再往下
docker compose up -d --build
docker compose logs -f # 看到 "engine ready" 且无 Traceback 即成功
```
**构建卡住/失败的两种原因和对策**:
1. **pip 慢**:Docker Hub 基础镜像可在 fnOS「设置 → Docker → 镜像仓库」配轩辕等加速源;pip 慢就在 Dockerfile 的 `pip install` 后加 `-i https://pypi.tuna.tsinghua.edu.cn/simple`。
2. **onnx 模型下载失败**(构建到预热那步报错,国内访问 GitHub 不稳):在能上网的 Windows 上先执行
```powershell
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 步:验证
```bash
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`,带 `X-API-Key` 头。
- **要域名 + 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 机型或用云侧部署。
## 调用
```bash
curl -X POST http://127.0.0.1:8000/api/v1/idcard/recognize \
-F "file=@/path/idcard.jpg" -F "side=auto"
```
base64 方式:
```bash
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}'
```
**返回示例**
```json
{
"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`。
## 鉴权(Token 验证)
支持三种方式,全部用**标准库实现**(HMAC-SHA256),不引入 PyJWT 等依赖,对 1C/2G 无额外开销。
### 方式一:多 token(推荐)
`.env` 里按 `名称:token` 配置,多个用逗号分隔:
```bash
API_TOKENS=frontend:abc123def456,win-service:xyz789uvw012
```
好处是**每个调用方一把,能单独吊销**(删掉对应那段重启即可),日志里也会打出调用方名称,出问题能定位是谁在调。
调用时带标准 Bearer 头:
```bash
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 的接口:
```bash
# ① 用 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` 控制:
```bash
# .env 里配置,逗号分隔;* 表示允许所有来源(开发方便)
CORS_ORIGINS=http://10.1.1.163:5173,http://10.1.1.163:8080
```
几个要点:
1. **预检请求**:前端带 `X-API-Key` 自定义头会触发 `OPTIONS` 预检,中间件已自动处理(返回 204 + 允许头),无需额外路由。
2. **鉴权头必须在 `allow_headers` 里**:已设 `allow_headers=["*"]`,`X-API-Key` 可通过。
3. **`allow_credentials=False` 是刻意的**:本项目用请求头鉴权、不用 Cookie,而 `allow_origins=["*"]` 与 credentials=True 是非法组合,浏览器会直接拒绝。
4. **安全提醒**:身份证是敏感数据。`CORS_ORIGINS=*` 且未配置任何 token 时,局域网内任何一个网页的 JS 都能悄悄调用你的接口读数据。**生产必须二选一**:配 `API_TOKENS`,或把 `CORS_ORIGINS` 收紧到具体前端地址。
5. **生产更推荐同源**:用 Nginx / fnOS 反向代理把前端和 API 放在同一域名下(`/api` 转发到 8000),浏览器视为同源,连 CORS 都不需要,也少一层预检开销。
## 如果内存还是吃紧(降级路径)
按代价从低到高:
1. `MAX_SIDE=960` + `DET_LIMIT_SIDE_LEN=736`:身份证文字在 960px 下仍有 ~25px 高,识别基本无损,通常再省 100–150MB
2. 换 PP-OCR **mobile** 版识别模型(`ch_PP-OCRv4_mobile_rec`),体积和内存都小于 server 版,身份证这种清晰印刷体精度损失很小
3. 加 swap(2–4G)作为兜底——会拖慢速度,但能防 OOM 被杀。**这是 2G 机器强烈建议做的**:
```bash
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
```
4. 极端情况换 Tesseract + `chi_sim`:内存可压到 200MB 内,但中文证件识别率明显下降,不推荐
**并发上不去时**:低配下不要加 worker,改成前面挂 Nginx 限流排队。1 核的现实吞吐大约 **1–2 QPS**,超过就会堆积。要扛量就横向多开几个容器 + 前面 LB,而不是在单机上加进程。
## 准确率与调优
- 清晰扫描件 / 规整拍照:字段级 95%+;模糊、反光、强倾斜、复印件会明显下降。
- 掉得最多的通常是**住址**(长文本)和**名字里的生僻字**。建议对 `id_number_valid=false` 的结果走人工复核,不要直接采信。
- 拍照场景强烈建议前端先做一次**方向校正 + 边缘裁剪**,比在服务端补救有效得多。
- 想加"先检测身份证区域再 OCR"(防复杂背景),可接 YOLOv8n 做前置裁剪,挂在 `_load_image` 之后即可——**1C/2G 下不建议加**,会再吃 100MB 内存和一大截 CPU。
## 合规与安全(重要)
身份证号属于**敏感个人信息**,受《个人信息保护法》《数据安全法》约束:
1. 这个方案的价值就在**数据不出你自己的机器**。别为了省事换成公有云 API,那等于把 PII 交给第三方。
2. 服务**不落盘、不缓存**图片,`raw_text` 含明文,生产环境建议要么关掉、要么 `mask=true`。
3. 日志里**不要打印身份证号和姓名**。当前实现只打加载信息。
4. 对外必须 HTTPS + 鉴权 + 限流;已做两层大小防护(content-length 预检 + 惰性尺寸检查),`MAX_IMAGE_BYTES=5MB`。
5. 收集前应有用户授权与隐私政策,明确保留期限——最安全的做法是识别完即丢弃。
## 目录
```
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 配置:
```dockerfile
ARG BASE_IMAGE=docker.m.daocloud.io/library/python:3.11-slim
FROM ${BASE_IMAGE}
```
切换源只需在 `.env` 里设 `BASE_IMAGE`,构建前先探活:
```bash
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` 则换下一个。全部不通就走离线导入兜底:
```bash
# 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 存在兼容问题。