PRD：全离线生成 `face_gallery.db`（每人 1 条 centroid，Windows+Python+ONNX）

   1. 背景与目标
   •  在 Windows 电脑上离线处理“注册照片”，为每个人生成 1 条 512D 
      人脸特征（centroid），写入 SQLite 数据库 face_gallery.db。
   •  RK3588 运行时 ai_face_recog 以 gallery.backend=sqlite
      读取该库，实现识别（无需改线上阈值/配置逻辑）。

   ──────────────────────────────────────────

   2. 范围（In/Out）

   In Scope
   •  数据集扫描（按人目录）
   •  人脸检测（含 5 点关键点）+ 对齐到 112×112
   •  ArcFace/MobileFaceNet ONNX 推理生成 512D embedding
   •  每人多图聚合为 centroid（L2 normalize 后平均，再 L2 normalize）
   •  生成 SQLite：person、embedding 两表
   •  生成构建报告（统计、异常、可选相似度抽检）

   Out of Scope
   •  在线注册/写库接口
   •  RKNN 推理/设备端工具
   •  人脸库增量热更新（只生成 db 文件）

   ──────────────────────────────────────────

   3. 用户与使用方式

   目标用户
   •  开发/测试人员（在 Windows 上离线准备人脸库）

   使用方式（CLI）

   提供脚本：build_gallery.py
   示例：

   bash
     python build_gallery.py ^
       --dataset "D:\faces\dataset" ^
       --db_out "D:\faces\face_gallery.db" ^
       --det_model "D:\models\face_det.onnx" ^
       --recog_model "D:\models\mobilefacenet_arcface_bs1.onnx" ^
       --expected_dim 512 ^
       --max_imgs_per_person 10 ^
       --pick_face largest ^
       --min_face_size 80 ^
       --fail_on_empty true

   ──────────────────────────────────────────

   4. 输入数据规范

   目录结构

     dataset/
       张三/
         001.jpg
         002.jpg
       李四/
         a.png
         b.png

   •  文件夹名作为 person.name（UTF-8）
   •  每人建议 3~10 张图（允许混合：标准大头照 + 摄像头位照片）

   图片支持格式
   •  jpg/jpeg/png/bmp（由 OpenCV 读取）

   ──────────────────────────────────────────

   5. 模型与前处理规范（必须严格一致）

   识别模型（已确定）
   •  输入：float32 [1,3,112,112]，输出：float32 [1,512]
   •  输入图像为 对齐后的 112×112 RGB
   •  归一化：(x - 127.5) / 128.0，其中 x 为 uint8 0..255
   •  HWC→CHW→NCHW

   检测模型（你不要求我管来源，但脚本必须满足）
   •  det ONNX 推理输出至少包含：bbox + 5 landmarks
   •  必须明确 landmark 顺序（开发在配置/代码中固定）

   对齐目标点（112×112，必须固定为以下值）
   •  (38.2946, 51.6963)
   •  (73.5318, 51.5014)
   •  (56.0252, 71.7366)
   •  (41.5493, 92.3655)
   •  (70.7299, 92.2041)

   对齐方法：
   •  基于 5 点求相似变换（Similarity / affine partial）→ warpAffine 到
      112×112，双线性插值

   ──────────────────────────────────────────

   6. 特征生成与聚合（每人 1 条 centroid）

   对每张有效图片：
   1. 检测 → 选择人脸（默认：最大脸）
   2. 5 点对齐 → 112×112 RGB
   3. 预处理 + ONNX 推理 → 512D embedding
   4. L2 normalize（单样本）

   对每个人：
   •  取最多 max_imgs_per_person 张有效样本 embedding（不足也可）
   •  centroid 计算：
     •  centroid = mean(emb_i)（对已归一化 emb 做均值）
     •  centroid = L2_normalize(centroid)
   •  只写入 1 条 embedding（centroid）

   ──────────────────────────────────────────

   7. SQLite 数据库规范（必须与现有插件兼容）

   文件
   •  输出文件名：face_gallery.db（路径由 --db_out 指定）

   表结构（最小要求）

   sql
     CREATE TABLE IF NOT EXISTS person (
       id   INTEGER PRIMARY KEY,
       name TEXT NOT NULL UNIQUE
     );

     CREATE TABLE IF NOT EXISTS embedding (
       person_id INTEGER NOT NULL,
       emb       BLOB NOT NULL,
       FOREIGN KEY(person_id) REFERENCES person(id)
     );

     CREATE INDEX IF NOT EXISTS idx_embedding_person_id ON embedding(person_id);

   写入规则
   •  person.name = 文件夹名
   •  embedding.emb = centroid 向量的 float32 原始字节（BLOB）
     •  长度必须为 expected_dim * 4（512→2048 bytes）
   •  每个 person 仅 1 行 embedding

   ──────────────────────────────────────────

   8. 质量控制与异常处理

   过滤规则（可配置）
   •  min_face_size：bbox 宽或高小于阈值则丢弃
   •  检测不到脸：记录为失败样本
   •  多脸：默认选最大脸；可配置 --pick_face（largest/first/highest_score）

   容错策略（可配置）
   •  单人所有图片都失败：该 person 不写入库，并在报告中列出
   •  --fail_on_empty true：若最终 person 数为 0 或 embedding 数为 0，则脚本返回非
      0 退出码

   ──────────────────────────────────────────

   9. 输出报告（必须）

   脚本运行结束输出：
   •  总人数、成功入库人数、总图片数、成功样本数、失败样本数
   •  每人使用的样本数量
   •  失败原因统计（no_face / small_face / align_fail / infer_fail 等）
   •  数据库自检：
     •  COUNT(person)、COUNT(embedding)
     •  随机抽查 N 条 length(emb)==2048

   （可选增强）打印每人 centroid 的 L2 norm（应接近 1.0）

   ──────────────────────────────────────────

   10. 依赖与运行环境
   •  Windows 10/11
   •  Python 3.9+
   •  依赖包：
     •  numpy
     •  opencv-python
     •  onnxruntime
   •  sqlite3：使用 Python 标准库

   ──────────────────────────────────────────

   11. 验收标准（可直接验）
   1. 生成的 face_gallery.db 能被你当前 ai_face_recog 加载，设备日志出现：gallery 
      loaded: n=<入库人数> dim=512
   2. SQLite 校验：
     •  SELECT COUNT(*) FROM person; == 入库人数
     •  SELECT COUNT(*) FROM embedding; == 入库人数
     •  SELECT length(emb) FROM embedding LIMIT 5; 全为 2048
   3. 实测：对至少 3 人，每人 3 张注册图 + 1
      条现场图，识别能输出对应姓名（阈值用现有配置不强制修改）

   ──────────────────────────────────────────

   12. 与设备端配置的对接要求（提醒开发）
   •  gallery.backend="sqlite"
   •  gallery.path 指向拷贝后的 face_gallery.db
   •  gallery.expected_dim=512
   •  threshold.margin 保持现有值即可（因为每人只有 1 条 centroid，不会出现“同人占
      top2 导致 margin 过小”的问题）