项目实战:从零搭建智能门禁系统
人脸检测 + 识别 + 权限管理 + 日志记录全流程。
说实话,做嵌入式这么多年,我接过不少“智能门禁”的需求。有的客户拿个树莓派就想上,有的非要上云端识别。但真正要在 Jetson 上落地一个可用的门禁系统,你得把整个链路打通——从摄像头抓图,到人脸检测,再到特征比对、权限判断,最后还得把谁进谁出记下来。
这一章,我就带你手搭一套完整的门禁系统。咱们不玩虚的,直接上代码。
1. 系统架构概览
先看整体逻辑。我习惯把门禁系统拆成四个模块:
- 采集层:USB 摄像头或 CSI 摄像头,负责抓取视频帧
- 推理层:Jetson 上的 TensorRT 加速,跑人脸检测 + 特征提取
- 业务层:权限判断、白名单管理、开门信号输出
- 记录层:日志写入本地文件或 SQLite 数据库
嗯,这里要注意:千万别把识别和检测混在一个模型里做。我见过有人用一个大模型同时干两件事,结果 Jetson Nano 上跑不到 5fps。分开做,检测用轻量模型,识别用特征向量比对,这样效率高得多。
核心原则:检测模型负责“找到人脸”,识别模型负责“认出是谁”。两个模型串行跑,但用不同的推理引擎实例。
2. 硬件选型与连接
我这次用的是 Jetson Orin Nano,配一个 USB 摄像头(1080p 30fps)。为什么不用 CSI?因为 USB 摄像头调试方便,换镜头也容易。你想想看,如果现场安装时发现焦距不对,USB 摄像头拧下来换一个就行,CSI 就得拆排线。
| 组件 | 型号 | 备注 |
|---|---|---|
| 主控 | Jetson Orin Nano 8GB | JetPack 6.0 |
| 摄像头 | Logitech C920 | USB 2.0,支持 1080p |
| 继电器模块 | 1路 5V 低电平触发 | 控制电锁 |
| 存储 | NVMe SSD 256GB | 存放模型和日志 |
小提示:继电器模块的 GPIO 引脚记得接上拉电阻。我曾经因为没接上拉,导致 Jetson 启动时继电器误触发,门锁直接弹开了……嗯,那场面挺尴尬的。
3. 人脸检测模块
检测这块,我选的是 RetinaFace 的 TensorRT 版本。为什么不用 MTCNN?因为 MTCNN 在 Jetson 上跑三阶段级联,延迟不稳定。RetinaFace 单阶段搞定,配合 TensorRT 能跑到 30ms 以内。
代码实现如下:
import cv2
import numpy as np
import tensorrt as trt
import pycuda.driver as cuda
class FaceDetector:
def __init__(self, engine_path):
# 加载 TensorRT 引擎
self.logger = trt.Logger(trt.Logger.WARNING)
with open(engine_path, 'rb') as f:
runtime = trt.Runtime(self.logger)
self.engine = runtime.deserialize_cuda_engine(f.read())
self.context = self.engine.create_execution_context()
# 分配 GPU 内存
self.inputs, self.outputs, self.bindings = [], [], []
for binding in self.engine:
size = trt.volume(self.engine.get_binding_shape(binding))
dtype = trt.nptype(self.engine.get_binding_dtype(binding))
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
self.bindings.append(int(device_mem))
if self.engine.binding_is_input(binding):
self.inputs.append({'host': host_mem, 'device': device_mem})
else:
self.outputs.append({'host': host_mem, 'device': device_mem})
self.stream = cuda.Stream()
def detect(self, image):
# 预处理:resize 到 640x640,归一化
img = cv2.resize(image, (640, 640))
img = img.astype(np.float32) / 255.0
img = img.transpose(2, 0, 1) # HWC -> CHW
img = np.expand_dims(img, axis=0)
# 拷贝到 GPU
cuda.memcpy_htod_async(self.inputs[0]['device'], img, self.stream)
# 推理
self.context.execute_async_v2(bindings=self.bindings, stream_handle=self.stream.handle)
# 取回结果
cuda.memcpy_dtoh_async(self.outputs[0]['host'], self.outputs[0]['device'], self.stream)
self.stream.synchronize()
# 后处理:解析检测框
output = self.outputs[0]['host']
boxes = self.parse_output(output)
return boxes
def parse_output(self, output):
# 简化版后处理,实际需要 NMS
boxes = []
for i in range(0, len(output), 6):
conf = output[i+4]
if conf > 0.5:
x1, y1, x2, y2 = output[i:i+4]
boxes.append([x1, y1, x2, y2, conf])
return boxes
注意:TensorRT 引擎文件是跟 Jetson 的 CUDA 版本绑定的。你在 Orin 上生成的 .engine 文件,拿到 Xavier 上跑不了。反过来也一样。所以模型转换这一步,必须在目标设备上做。
4. 人脸识别与特征提取
检测到人脸之后,下一步是提取特征向量。我用的是 ArcFace 模型,输出 512 维的 float 向量。说白了,就是把一张人脸压缩成一个“数字指纹”。
比对的时候,计算当前人脸向量和白名单库中所有向量的余弦相似度。超过阈值(我一般设 0.65)就算匹配成功。
import numpy as np
from scipy.spatial.distance import cosine
class FaceRecognizer:
def __init__(self, engine_path, whitelist_path):
# 加载 TensorRT 引擎(类似检测模块)
self.engine = self.load_engine(engine_path)
# 加载白名单特征库
self.whitelist = np.load(whitelist_path, allow_pickle=True).item()
# self.whitelist 结构: { "name": feature_vector }
def extract_feature(self, face_img):
# 预处理:对齐、归一化
aligned = self.align_face(face_img)
# 推理得到特征向量
feature = self.infer(aligned)
# L2 归一化
feature = feature / np.linalg.norm(feature)
return feature
def match(self, feature):
best_name = "unknown"
best_score = 0.0
for name, ref_feat in self.whitelist.items():
sim = 1 - cosine(feature, ref_feat)
if sim > best_score:
best_score = sim
best_name = name
if best_score < 0.65:
return "unknown", best_score
return best_name, best_score
关键点:特征向量一定要做 L2 归一化。不做归一化的话,不同光照条件下同一个人的特征距离会很大。我刚开始做的时候没注意,白天能识别,晚上就认不出了。归一化之后,光照鲁棒性明显提升。
5. 权限管理与开门逻辑
权限管理其实不复杂。白名单里每个人可以带一个权限等级:
- admin:全天可进
- staff:工作日 8:00-20:00 可进
- visitor:需管理员远程审批
判断逻辑:
import datetime
def check_access(person_name, level):
now = datetime.datetime.now()
if level == "admin":
return True
elif level == "staff":
if now.weekday() < 5 and 8 <= now.hour < 20:
return True
else:
return False
elif level == "visitor":
# 这里可以触发远程审批流程
return False
return False
开门信号通过 GPIO 控制继电器。Jetson 的 GPIO 用 jetson-gpio 库操作:
import RPi.GPIO as GPIO # 兼容 Jetson
RELAY_PIN = 18
GPIO.setmode(GPIO.BCM)
GPIO.setup(RELAY_PIN, GPIO.OUT)
GPIO.output(RELAY_PIN, GPIO.HIGH) # 默认高电平,锁住
def open_door():
GPIO.output(RELAY_PIN, GPIO.LOW) # 低电平触发
time.sleep(2) # 保持开门 2 秒
GPIO.output(RELAY_PIN, GPIO.HIGH) # 重新锁住
安全提醒:千万不要把继电器直接接 Jetson 的 5V 引脚。Jetson 的 GPIO 是 3.3V 逻辑电平,继电器模块需要 5V 驱动。中间加一个电平转换模块,或者用光耦隔离。我曾经偷懒直接接,结果烧了一个 GPIO 口……
6. 日志记录模块
日志记录我用 SQLite,轻量、零配置。每条日志包含:时间、人名、是否通过、相似度分数。
import sqlite3
import datetime
class AccessLogger:
def __init__(self, db_path="access.db"):
self.conn = sqlite3.connect(db_path)
self.cursor = self.conn.cursor()
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS access_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT,
person_name TEXT,
access_granted INTEGER,
confidence REAL
)
''')
self.conn.commit()
def log(self, name, granted, confidence):
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.cursor.execute(
"INSERT INTO access_log (timestamp, person_name, access_granted, confidence) VALUES (?, ?, ?, ?)",
(now, name, 1 if granted else 0, confidence)
)
self.conn.commit()
def query_recent(self, limit=10):
self.cursor.execute("SELECT * FROM access_log ORDER BY id DESC LIMIT ?", (limit,))
return self.cursor.fetchall()
小技巧:日志文件最好放在 SSD 上,不要放 SD 卡。SD 卡的写入寿命有限,频繁写日志容易坏。我有一块 Jetson Nano 的 SD 卡就是被日志写挂的。
7. 主循环:把所有模块串起来
最后,写一个主循环把上面所有模块串起来。嗯,这里要注意帧率控制——检测和识别不能每帧都做,否则 Jetson 会过热降频。
def main():
cap = cv2.VideoCapture(0)
detector = FaceDetector("retinaface.engine")
recognizer = FaceRecognizer("arcface.engine", "whitelist.npy")
logger = AccessLogger()
frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
# 每 3 帧做一次检测
if frame_count % 3 == 0:
boxes = detector.detect(frame)
for box in boxes:
x1, y1, x2, y2, conf = box
face = frame[y1:y2, x1:x2]
if face.size == 0:
continue
# 提取特征
feature = recognizer.extract_feature(face)
name, score = recognizer.match(feature)
# 权限判断
if name != "unknown":
level = whitelist.get(name, {}).get("level", "visitor")
granted = check_access(name, level)
if granted:
open_door()
logger.log(name, granted, score)
else:
logger.log("unknown", False, score)
frame_count += 1
# 显示画面(调试用)
cv2.imshow("Access Control", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
性能数据:在 Jetson Orin Nano 上,检测 + 识别单次耗时约 45ms(检测 30ms + 识别 15ms)。跳帧处理后,整体系统可以稳定跑在 10fps 左右,完全满足门禁场景需求。
8. 部署与测试
部署时我建议先做离线测试:用录好的视频文件代替摄像头,验证识别准确率和权限逻辑。没问题了再接真实摄像头。
测试用例:
- 已注册人员正常通行 → 应开门
- 已注册人员非工作时间 → 应拒绝
- 未注册人员 → 应拒绝并记录
- 多人同时出现在画面 → 只识别最靠近的那个人
这些用例跑通了,系统基本就稳了。
好了,这一章的内容就到这里。从摄像头采集到日志记录,整个链路你应该已经清楚了。代码都在上面,拿回去改改就能用。如果你在实际部署中遇到什么问题,欢迎交流。
公众号:蓝海资料掘金营,微信deep3321