百万级人脸库毫秒检索、跨摄像头轨迹追踪,这几个词凑在一起,乍看像是个安防大脑级别的项目。但当你把 Java 后端、IoT 设备接入和 AI 推理链路真正串起来做一遍,会发现最难的从来不是某个模型有多新,而是整条数据流水线上每个环节能不能在高压下不脱节。
这篇文章记录的是我最近一次完整落地的项目复盘:业务场景是一个商业园区,需要把常驻人员、商户员工、临时访客和少量重点防控对象统一管起来,底库规模逼近百万级。核心要求有两个——任意摄像头抓拍到一张人脸,要在 1 秒内给出身份结果;同一身份在不同摄像头之间出现时,系统能自动拼接出完整活动轨迹。项目组主力是 Java 工程师,摄像头是海康、大华混用的存量设备,AI 部分用 Python 生态实现。我尽量把从选型到实战的全过程写清楚,包括那些只有踩进去才看得见的坑,希望对正在做类似事情的团队有帮助。
1. 项目背景与整体架构:三个技术栈到底怎么分工
1.1 需求拆解:百万级、毫秒级、跨摄像头分别是什么含义
先说清楚几个关键词的真实含义,因为业务方提需求和开发理解之间往往存在巨大偏差。
"百万级人脸库"指的是底库中注册的人脸底图数量级在百万上下。我们项目里按一个人 1~5 张底图估算,实际档案数大约 30 万,底图总量约 85 万,最终按 100 万的规模做设计和压测。这个量级对存储压力不大,但对检索链路的设计非常关键。
"毫秒检索"不是指抓拍后整个业务链路 1 秒内完成,而是特指底库向量比对这个环节要控制在毫秒级。我们给业务方交付的体验指标是:抓拍事件从产生到身份结果回写,全链路 500ms 以内,其中纯底库检索控制在 10~30ms。余量要留出来,因为网络传输、AI 推理、消息队列都各有延迟。
"跨摄像头轨迹追踪"比普通的人脸识别难一个维度。单摄像头识别只是回答"这个人是谁",跨摄像头追踪还要回答"这个人刚才从哪来、现在到哪去、经过哪些点位"。它要求系统把所有抓拍事件在时空维度上关联起来,这本质上是一个在线聚合问题,不像底库检索那样有现成组件可以直接用。
需求拆解到这一步,团队内部基本达成共识:这不是一个纯 AI 项目,而是一个重工程的数据管道项目。
1.2 选型逻辑:为什么是 Java、IoT、AI 三件事一起做
网上很多文章喜欢把 Java、IoT、AI 这几个词堆在一起当噱头,但实际上它们在这个项目里的分工非常清晰。
Java 负责的是"业务大脑":人员档案、设备管理、用户权限、轨迹查询接口、告警推送、事务性操作。选 Java 不是因为它的算法生态好,而是因为它有足够成熟的微服务基础设施、连接池和运维工具链,团队维护成本低。 Spring Boot 加 Spring Cloud Alibaba 的组合在这个场景下完全够用。
IoT 负责的是"数据源头":摄像头接入、流媒体拉取、设备发现、心跳监控、断流重连。这一层最容易被低估。很多团队的误区是把摄像头当成一个简单的 RTSP 地址,开发一个 FFmpeg 命令就直接拉流,结果上线后设备离线、码流卡顿、断流不自愈,问题全堆在这里。IoT 接入层的稳定性,决定了上游 AI 和业务层有没有数据可用。
AI 负责的是"感知能力":人脸检测、关键点对齐、特征提取、行人再识别。训练模型是 AI 工程师的活,但落地时更多精力要花在推理服务的接口设计、GPU 资源分配、模型迭代发布上。
一句话总结:Java 是骨架,IoT 是血管,AI 是感官。三样东西缺一个,整个链路就跑不通。
1.3 全链路组件地图:先看清数据从哪来到哪去
在动手编码之前,我强烈建议团队先画一张"数据流转图"。不需要很精确,但要把每个环节的输入输出定义清楚:
| 链路层级 | 组件 | 核心职责 | 选型参考 |
|---|---|---|---|
| 设备层 | 网络摄像头、抓拍机 | 采集视频流、上报设备状态 | 存量海康/大华设备,RTSP/ONVIF |
| 接入层 | 流媒体网关、设备接入服务 | 统一拉流、协议适配、设备鉴权 | ZLMediaKit / SRS,Netty |
| 消息管道 | 消息队列 | 转发电事件、设备状态、告警 | Kafka + MQTT 双链路 |
| AI 推理层 | 检测/识别/ReID 服务 | 抽帧、人脸检测、特征提取 | Python + PyTorch/ONNX Runtime |
| 检索引擎 | 向量索引服务 | 百万底库的快速比对 | FAISS 单机索引,预留 Milvus |
| 业务层 | Java 微服务 | 档案管理、轨迹聚合、告警、API | Spring Boot / Spring Cloud |
| 存储层 | 各类数据库 | 元数据、热轨迹、历史记录 | MySQL + Redis + MongoDB |
这张表里最容易忽略的是消息管道的角色。抓拍事件不是同步请求,摄像头拉着流,AI 推理完把结构化特征打给 Kafka,业务层再消费,这样做的好处是削峰填谷。园区高峰期的人流量波动很大,如果所有环节都是同步调用,任何一环抖动都会直接放大到用户端。
2. AI 推理链路:从视频帧到结构化特征
2.1 抓拍触发与抽帧策略:别让无效计算吃掉 GPU
很多人第一次做人脸识别项目,上来就在每路摄像头上定时抽帧,每 200ms 抽一帧送去做检测。这个思路在小规模测试时没问题,但上了 30 路以上摄像头就会出事——大量画面是静止的走廊、停车场、空房间,这些帧都在白白消耗 GPU 算力。
我们最终的抽帧策略分了三层:
- 设备端优先:海康、大华的摄像头自带移动侦测和人脸抓拍能力,尽量开启设备的动态检测,只在画面中有目标移动时才产生抓拍事件。
- 流媒体端兜底:对于不支持智能事件的旧设备,在流媒体网关侧做轻量帧差分,连续几帧画面变化超过阈值才把帧推到 AI Server。
- 推理服务端过滤:AI Server 接到的帧再跑一次快速人脸检测,没有人脸的帧直接丢弃,不进入后续特征提取。
三层下来,实际进入特征提取的帧大概只有原始流量的 8%~15%,GPU 利用率反而上去了,因为每一帧都是有效计算。
2.2 人脸检测、对齐与质量过滤:检索之前最容易被忽略的关口
特征提取模型再好,如果输入的人脸图是糊的、偏的、尺寸太小的,底库检索再快也没有意义。所以我们把"质量过滤"放在特征提取之前。
检测阶段我们用的是 RetinaFace 系模型,在 GPU 上速度和精度平衡得比较好。最近也有团队用 YOLOv8-face,效果也不错。检测到了人脸之后,用 5 个关键点(左眼、右眼、鼻尖、左嘴角、右嘴角)做仿射变换,把脸对齐到 112x112 的标准输入尺寸,这一步对特征提取效果影响极大。
过滤规则上,几个经验值可以参考:
- 人脸像素宽度小于 40px 直接丢掉,这个尺寸下特征提取已经基本不可靠。
- 模糊度分数高于阈值直接丢掉。Opencv 的 Laplacian 方差可以当简单的模糊指标用,进阶做法是训练一个小模型打分。
- 左右偏转角超过 30 度、俯仰角超过 25 度的脸要谨慎处理,极端侧面脸即使识别出来,置信度也偏低。
质量过滤的收益是隐性的。表面上你只是丢掉了一些"难样本",但实际上你保住了底库检索的精度,也大幅减少了无效比对带来的 CPU/GPU 开销。
2.3 特征向量与相似度:512 维的 Embedding 是怎么工作的
人脸识别的核心思想是把一张人脸图像压缩成一个固定维度的向量,让同一个人的不同照片在向量空间中距离很近,不同人的照片距离很远。我们用 ArcFace 系模型输出 512 维的 float 向量,推理时得到的就是这张脸的"数学指纹"。
为什么是 512 维而不是 128 维或 2048 维?这是一个精度和工程成本的平衡。128 维在人脸大规模底库下区分度不足,容易产生更多跨身份误配;2048 维的精度收益有限,却让内存占用和检索计算量直接翻了四倍。百万人脸底库,512 维 float 向量全量加载大约是 2GB 内存,这是单机内存可以轻松承接的量级。
模型输出的向量经过 L2 归一化,也就是模长变成 1。归一化之后,两个向量的余弦相似度就等于它们的点积,这个性质让向量检索组件可以直接用内积距离来近似余弦相似度,不需要每次实时计算模长。
相似度阈值是一个必须实测调优的东西。不同模型、不同训练数据、不同底库人种分布,阈值都不一样。项目里我们最初沿用其他人分享的 0.6 阈值,结果误识率偏高,后来用验证集画 ROC 曲线重新标定,把阈值调到 0.68 才达到业务预期。
2.4 Java 与 AI Server 的桥接:选对调用方式能省一半心
模型本身是 Python 生态,但业务系统是 Java,两者之间怎么通信是个绕不开的问题。我们对比过三种方案:
- gRPC:Java 端通过 grpc-java 调用 Python 推理服务。性能好、接口约束清晰、支持流式传输,但需要写 proto 文件并生成代码。
- HTTP + JSON:实现最简单,但 JSON 序列化和反序列化开销大,高并发下延迟和 CPU 消耗都不理想。
- Java 内嵌模型:用 TensorFlow Java API 或者 ONNX Runtime Java 直接在 Java 进程内推理,省掉了跨进程网络开销,但模型部署和 GPU 显存管理会很别扭,且模型迭代涉及 Java 侧发版。
我们最终选了 gRPC。原因很实际:AI Server 单独部署在 GPU 机器上,Java 业务服务可以水平扩展,两边独立发版互不干扰。gRPC 连接池复用之后,一次特征提取的网络开销在几毫秒量级,比 HTTP 方案稳定一个数量级。
3. 百万级人脸库的毫秒检索落地
3.1 为什么暴力遍历在百万级底库上行不通
先算一笔账。100 万张底图,每张提取 512 维 float 特征,那就是 100 万 x 512 次浮点乘加。用 Java 循环做暴力比对,单次查询大约需要 5 亿次浮点计算。就算你的机器每秒能跑 10 亿次浮点运算,一次查询也要 500ms 起步,这还只是一个请求;当并发请求上来,CPU 直接打满,服务立刻不可用。
还有内存问题。100 万条 float[512] 的对象如果放在 Java 堆上,每个对象有对象头和数组长度等额外开销,实际占用会膨胀到 4~6 倍,2GB 的数据轻松变成 8~10GB 堆内存,GC 必然爆炸。所以百万级召回,向量索引和堆外内存管理几乎是必须的,不能靠硬算。
3.2 向量索引选型:FAISS、Milvus、Elasticsearch 怎么选
主流的向量检索引擎有三个方向,各自对应不同团队的技术底色:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| FAISS | 单机内存索引性能极强,部署轻量,Meta 维护 | 无内置分布式和高可用,动态更新要自己处理 | 百万量级、单机可扛、更新频率低 |
| Milvus | 分布式向量数据库,内置动态更新、标量过滤、数据持久化 | 组件多、运维重,小规模项目属于杀鸡用牛刀 | 千万级以上、需要多节点扩展 |
| Elasticsearch | 已有 ES 的团队接入成本低,支持向量字段与传统过滤组合 | 检索性能比 FAISS 差一个量级,内存开销大 | 检索过滤条件复杂、不想引入新组件 |
我们最终选了 FAISS。理由很朴素:底库规模 100 万,加索引全量在内存里也就 2GB 出头;更新频率是"日常小批量增量 + 每天一次全量重建",不是在线实时高频变更;单机 64GB 内存的机器完全够用,不用为这个量级引入一套分布式系统。
如果你团队已经重度使用 ES,也可以考虑 ES 的 dense_vector 加 HNSW 索引。代价是延迟会高一些,但对几百万级规模仍然可用。
3.3 FAISS 的索引结构与 Java 桥接
FAISS 索引类型很多,实际落地我们重点对比了 IndexIVFFlat 和 IndexHNSW。
IndexIVFFlat 的思路是先把底库向量聚类分成 N 个桶(nlist),查询时只搜索距离最近的 K 个桶(nprobe),而不是全量遍历。它的内存开销和原始向量一致,构建速度快,代价是 nprobe 设置过小时会丢失精度。我们最终用 nlist=1024,nprobe=32,在 100 万底库上的召回率能达到 95% 以上。
IndexHNSW 是图结构索引,检索延迟低、召回率高,但构建时间较长,参数多,调优需要更多经验。单机百万级场景两者都能胜任,看团队偏好。
FAISS 是 C++ 内核,Python 接口最成熟。Java 直接用 JNI 调 C++ 库维护成本太高,我们的做法是把 FAISS 包成一个独立的检索服务,对外提供 gRPC 接口,Java 侧只负责传特征向量、收 topK 结果。核心代码示意:
import faiss import numpy as np d = 512 # 特征维度 index = faiss.index_factory(d, "IVF1024,Flat") # 训练聚类中心 index.train(np.random.rand(200000, d).astype("float32")) # 添加底库向量,id 用业务主键 index.add_with_ids(feat_np.astype("float32"), ids) # 查询:返回 top20 的相似度和对应 id D, I = index.search(query_vec.astype("float32"), k=20)Java 侧把这段逻辑封装成服务后,一次检索的 RPC 耗时基本在 5ms 以内,加上 FAISS 本身的计算时间,单次 top20 检索整体 10~20ms,完全满足毫秒级的要求。
3.4 底库动态更新:增量、删除与索引重建的工程处理
FAISS 的 IndexIVFFlat 训练好之后,追加新向量很容易,直接 add_with_ids 就行。但要注意两个隐患:
第一,聚类中心是训练时确定的,如果线上增量数据的分布和训练集差异很大,新增向量可能归错桶,导致检索召回率下降。我们的对策是每天凌晨低峰期用全量底库重建一次索引,训练过程大约 2~3 分钟,可接受。
第二,删除操作比较麻烦。FAISS 的 remove_ids 可以删,但删除会造成索引内部布局稀疏化,反复增删之后性能会劣化。工程上更稳妥的方案是:底库索引本身不做物理删除,只在 Java 侧维护一个"已删除 ID 集合",检索拿到候选结果后,再过滤掉已删除 ID。这样索引保持稳定,业务层承担过滤责任,逻辑还更清晰。
索引重建期间不能让检索服务停下,我们的做法是双缓冲。一个 hot 索引对外服务,另一个 cold 索引在后台重建,构建完成后原子切换。切换动作对调用方完全透明,不会出现查询抖动。
3.5 实测性能:百万人脸库到底能跑多快
压测环境是一台 32 核 CPU 的物理机,内存 64GB,FAISS 服务独占。底库 100 万向量,512 维,IndexIVFFlat 参数为 nlist=1024、nprobe=32:
| 场景 | 单次检索耗时 | 说明 |
|---|---|---|
| 单请求 top20 | 5~12ms | 加上网络与 gRPC 开销约 15ms |
| 并发 50 QPS | P99 18ms,P95 12ms | 检索服务无明显瓶颈 |
| 并发 200 QPS | P99 35ms | CPU 占用约 60%,仍稳定 |
| 全链路含推理 | 180~350ms | 含拉流、推理、检索、轨迹聚合 |
这个结果说明:百万级底库在单机 FAISS 上完全可以撑住毫秒检索。真正的性能瓶颈在整条链路,而不是 FAISS 本身。
4. 跨摄像头轨迹追踪:从"认得出人"到"拼得出路"
4.1 单摄像头跟踪和跨摄像头追踪的差别
很多开始接触这个领域的人会把跨摄像头追踪理解成"多目标跟踪(MOT)",但两者是不同的层次。
单摄像头 MOT 解决的是"同一画面里,这个框一直跟着那个人走"的问题。它依赖帧间 IoU、运动模型、外观特征,输出的是"摄像头内部 ID",比如 Camera-01-Track-001。这个 ID 一旦目标离开画面就作废了,换一个摄像头全部重新编号。
跨摄像头追踪要解决的是"这个人出现在摄像头 1 之后,几分钟后出现在摄像头 3,再过几分钟出现在摄像头 7"的串联问题。只靠单摄 MOT 无法做到,因为不同摄像头之间的外观差异非常大,光照、角度、分辨率完全不一样。这需要 ReID(行人再识别)能力的参与:不是看目标在相邻帧之间运动是否连续,而是看两段出现在不同画面的目标,外观特征是否匹配同一身份。
我们项目的做法是"人脸为主、人体为辅"。正脸清晰时优先人脸识别,人脸太小或角度不好时,用人体的全局特征做补充关联。
4.2 跨摄像头轨迹接力的完整流程
一次真实的跨摄像头接力,在系统内部是这么流转的:
- 摄像头 A 抓拍到一张清晰正脸,AI Server 提取特征后送到底库检索,命中身份 ID = P10001。
- 结果写入 Redis,以 P10001 为 key,记录"摄像头 A、时间 T1、位置坐标、抓拍图 URL"。
- 摄像头 B 在几分钟后抓拍到同一身份的疑似目标,但人脸分值没有直接超过硬阈值,只达到了"候选"级别。
- 轨迹聚合服务把候选身份拿去查询 Redis 中该身份最近 30 分钟内的轨迹点,发现有摄像头 A 的记录,且两者在拓扑和时间上满足通行条件。
- 系统把摄像头 B 的这次抓拍也归入 P10001 的轨迹,并计算相似度打分,确认关联。
这个流程的关键不是某一次识别是否命中,而是通过"识别 + 时空校验"的配合,把单独置信度不够的抓拍事件也串起来,形成完整的移动路线。
4.3 时空约束:防止张三认成李四的兜底手段
ReID 和人脸特征相似度都做不到 100% 准。实际跑下来,纯特征匹配会出现不少"看起来像但实际不是同一人"的情况。要控制误关联,必须在空间和时间上加约束。
我们的约束规则有三条:
- 时间窗口约束:同一摄像头相邻两次出现同一身份的时间差,一般不超过目标离开视野的合理时间;跨摄像头的出现时间差,不能超过两台摄像头之间步行或车行所需时间的 1.5 倍。
- 空间拓扑约束:摄像头在园区内的位置关系形成一张图。两台摄像头物理上相邻且有路径连通,才允许出现跨镜头跳变;如果两栋楼之间没有连接通道,轨迹不能出现直接跳转。
- 速度约束:跨镜头时间差和空间距离换算出来的移动速度,超过正常人的步行速度上限(比如 3m/s)时,匹配直接废弃。
实际项目里,加时空约束之前,轨迹关联的误报率在 10% 左右;加完之后降到了 1% 以内。这比换更强模型性价比高得多,而且逻辑清晰、可解释。
4.4 轨迹存储与回放
轨迹数据是典型的时间序列写入、按身份聚合查询。我们选择了双层存储:
热轨迹存 Redis:每个 person_id 维护一个按时间排序的 ZSET,记录最近 30 分钟的抓拍点位。查询"这个人现在在哪"时毫秒级返回。冷轨迹落 MongoDB:每 5 分钟把 Redis 中的数据归档一次,按 person_id、时间范围建索引,支持历史轨迹回放。
回放接口的核心逻辑很简单:输入 person_id 和时间范围,查询出所有抓拍记录,按时间升序排列,把摄像头 ID、位置坐标、抓拍图拼成一条可播放的轨迹。真正费功夫的是处理"中间断点"——比如目标经过了一个没有摄像头的区域,轨迹会有一段空缺。我们处理方式是不做猜测补全,只在界面上用虚线标注"该路段无监控覆盖",把诚实展示给用户比伪装完整更重要。
5. IoT 设备接入与数据管道:摄像头上云的第一公里
5.1 摄像头接入协议的取舍:ONVIF、RTSP、GB28181 各管一摊
存量园区摄像头品牌混杂,协议能力参差不齐。我们没有追求用一套协议通吃,而是按管理维度做了分工:
| 协议 | 主要用途 | 我们怎么用 |
|---|---|---|
| ONVIF | 设备发现、能力协商、参数配置 | 新设备上线时自动发现、获取拉流地址、配置画质 |
| RTSP | 实时视频流拉取 | 实际取帧通道,走流媒体网关统一拉流 |
| GB/T 28181 | 国标设备目录、注册管理、平台对接 | 对存量国标设备做目录同步和状态管理 |
这套组合的好处是:设备初始化用 ONVIF 自动完成,流传输统一走 RTSP,国标存量设备通过 28181 协议接入平台目录,三方互补。如果只用 RTSP 裸拉流,设备管理会非常累;如果只走国标,又没有现成的流媒体处理能力。
5.2 流媒体与抽帧管道:带宽和算力怎么平衡
流媒体网关是我们的视频总线,选型用了 ZLMediaKit,部署简单、协议支持全。摄像头推送 RTSP 流到网关,网关负责转封装,AI Server 从网关按需取帧,而不是每路摄像头都直连推理服务。
带宽得算清楚。假设一路 1080p 视频码率 4Mbps,如果 30 路摄像头 7x24 小时全量拉流,光上行带宽就接近 120Mbps,很多园区办公室网络根本扛不住。我们最终的策略是"按需拉流":设备侧发现画面有移动才上报事件,流媒体网关再即时拉取对应摄像头的高清流,平时只保持低码率的预览流或直接不拉流。这样平均带宽降到 20%~30%,但事件响应仍然及时。
5.3 设备心跳、断流重连和补偿机制
设备在线率和链路稳定性是 IoT 层的核心指标。我们在这里踩过不少坑,几个关键点是:
- 心跳机制:摄像头通过 MQTT 每 30 秒上报一次心跳,超过 3 个心跳周期未上报则标记离线,触发运维告警。
- 断流重连:RTSP 拉流断开后,必须有退避重连策略。直接每 1 秒重连一次会把流媒体网关打崩,正确做法是 1s、2s、4s、8s 指数退避,最多到 60s 封顶。
- 事件补偿:AI Server 来不及处理导致消息积压时,不能丢弃事件。Kafka 的持久化在这里起了大作用,服务恢复后可以从 offset 继续消费,抓拍识别的结果不会丢。
5.4 消息链路选型:Kafka 和 MQTT 的分工
很多物联网项目把 MQTT 和 Kafka 对立起来,其实它们适合不同的消息类型。我们的做法是双链路并行:
MQTT 负责设备控制面:心跳、上下线通知、设备配置下发。消息量小、实时性要求高,MQTT 的发布订阅模型天然适配。Kafka 负责数据面:抓拍事件、识别结果、轨迹数据。消息量大、需要持久化与回放,Kafka 的高吞吐和 offset 管理正好满足。
两套消息系统划清边界之后,数据管道变得异常清爽。
6. 性能调优与实战中踩过的坑
6.1 Java 堆内存装不下百万特征向量:GC 之痛
项目早期有一个严重的性能问题:团队想省掉独立的检索引擎,直接用 Java 内置一个 ConcurrentHashMap<Long, float[]> 存特征向量,再写循环做余弦比对。结果启动后 Java 堆内存直接飙到 10GB,Full GC 频繁,接口 P99 超过 1 秒,完全无法用。
后来查原因,问题不在 HashMap,而在 float[] 的对象开销。512 个 float 是 2KB,但每个数组对象在 JVM 里还有对象头、对齐填充等额外空间,再加上 HashMap 的 Node 节点和扩容开销,实际膨胀远超出预期。这个教训说明:像百万级特征向量这种数据,应该交给堆外内存或者 C++ 侧管理。我们用 FAISS 独立服务后,Java 堆内存被彻底解放,2GB 数据完全由 C++ 进程管理,Java 侧只处理业务元数据和过滤逻辑,GC 问题销声匿迹。
6.2 索引重建不能影响在线服务
前面提到每天凌晨要重建索引,但最初实现时太粗暴:停服重建,重建完再启动。结果每天早上 3 点到 4 点之间,检索接口是断的,自助查询和夜间告警全部失效,运维被投诉了好几次。
改成双缓冲索引后,问题解决。具体做法是维护两个 FAISS 索引对象,一个对外服务,一个在后台构建。构建完成调用原子切换,让新的索引对象上线,老的释放。这个模式在写多读少、需要平滑升级的场景里非常好用,不只是 FAISS,很多内存态数据结构的更新都可以用。
6.3 相似度阈值不是拍脑袋定的
人脸识别的阈值直接影响误识率和漏识率,两者此消彼长。阈值设太高,真实匹配会被漏掉;阈值设太低,陌生人会被误认成库里的人。业务方的要求是"宁可漏报,不可错报",这个约束直接把阈值往高处推。
我们标定流程是:从底库中抽 1 万条真实档案,构造 10 万对正负样本对,跑一遍检索得到全部相似度分布,画出 ROC 曲线,取误识率十万分之一对应的阈值。实测下来,最终值落在 0.70 左右,和最初拍脑袋定的 0.6 差了整整 0.1。这个差距在百万底库上意味着每天多出几十次错误命中。
6.4 敏感数据的最小化处理不能省
人脸数据属于强敏感信息,商用项目必须把合规意识放进架构里。我们项目里做了几件事:人脸特征向量和原始照片分开存储,检索服务只保留向量和业务 ID;原始抓拍图走对象存储加密存储,按业务需求设置保留周期,到期自动清理;轨迹数据不保留超过业务要求的时间窗口;摄像头覆盖区域设置醒目提示标识,访客数据单独授权管理。这些不是麻烦事,而是这个领域的基本盘。疏忽任何一环,项目上线后都可能面临巨大风险,甚至直接导致项目停摆。
回看这个项目,真正的门槛不在单个 AI 模型的精度,也不在 Java 服务有多复杂,而在于把 Java、IoT、AI 三方的数据规范、接口约定、异常处理节奏对齐。个人经验是:如果团队之前没做过类似的链路,千万别一上来就挑战百万底库,先用 1 万条底图把"摄像头拉流 → AI 推理 → 向量检索 → 轨迹聚合"这条最小闭环跑通,性能问题后面再逐步优化。核心链路通了,剩下的所有问题都只是工程问题。