Files
Docker/idcard-api
2026-10-18 08:55:43 +08:00
..
ow
2026-10-18 08:55:43 +08:00
ow
2026-10-18 08:55:43 +08:00
ow
2026-10-18 07:40:42 +08:00
ow
2026-10-18 07:40:42 +08:00
ow
2026-10-18 08:55:43 +08:00
ow
2026-10-18 07:40:42 +08:00
ow
2026-10-18 07:40:42 +08:00
ow
2026-10-18 08:55:43 +08:00
ow
2026-10-18 08:55:43 +08:00
ow
2026-10-18 07:40:42 +08:00

身份证识别 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,这是最便宜的保险:

快速开始

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 即成功

构建卡住/失败的两种原因和对策:

  1. pip 慢:Docker Hub 基础镜像可在 fnOS「设置 → Docker → 镜像仓库」配轩辕等加速源;pip 慢就在 Dockerfile 的 pip install 后加 -i https://pypi.tuna.tsinghua.edu.cn/simple。
  2. 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

几个要点:

  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 机器强烈建议做的:
    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 配置:

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 存在兼容问题。