# 身份证识别 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": "", "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 ` | 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 存在兼容问题。