# 课前拍照打卡系统总流程与接口规范 ## 1. 文档目的 本文档用于说明课前拍照打卡系统的整体实现流程、模块拆分、模块内部职责、模块之间的接口规范、数据结构、错误码、数据库读写约定和后续扩展方向。 当前阶段系统按单机板端方案设计,优先实现两个基础能力: 1. 大图人头数统计:输入课堂大图,检测人头或人脸位置,输出人数统计结果。 2. 单张人脸匹配:输入单张清晰人脸,和本地已注册人脸库匹配,输出识别结果。 完整课堂大图多人脸识别打卡属于后续扩展功能。当前文档会预留接口,但不会把第一阶段目标复杂化。 ## 2. 设计原则 1. 单机优先:程序、模型、配置、数据库都部署在 IMX6ULL_ALPHA 板端。 2. 先命令行,后界面:第一阶段使用命令行运行,方便调试、答辩和移植。 3. 模块解耦:摄像头、模型推理、后处理、数据库、业务流程分离。 4. 模型可替换:接口层不绑定具体模型,ncnn 和 TFLite 通过适配层切换。 5. 数据可追溯:每次检测、匹配都保存输入图片路径、结果、阈值、模型版本和时间。 6. 板端友好:避免重复加载模型,避免保存过多大文件,避免不必要的内存拷贝。 ## 3. 默认技术决策 当前先采用以下默认方案,后续如果实测性能不满足,再做调整。 | 项目 | 默认方案 | 原因 | | --- | --- | --- | | 开发语言 | C++17 | 适合嵌入式 Linux,便于调用 OpenCV、SQLite、ncnn | | 构建系统 | CMake | 方便 PC 编译和交叉编译 | | 图像处理 | OpenCV | 摄像头采集、图片读写、resize、画框方便 | | 推理框架 | ncnn 优先,TFLite 预留 | ncnn 对 ARM CPU 友好,部署文件简单 | | 本地数据库 | SQLite | 单机部署简单,不需要数据库服务 | | 配置格式 | JSON | 结构清晰,便于维护 | | 第一阶段运行方式 | 命令行工具 | 降低 UI 干扰,先跑通核心功能 | | 时间格式 | 本地时间字符串 `YYYY-MM-DD HH:MM:SS` | 便于查看数据库记录 | | 图片路径 | 相对项目运行目录保存 | 方便板端迁移 | ## 4. 系统总体架构 ```text +-----------------------------+ | attendance_app | | command line entry point | +--------------+--------------+ | v +-----------------------------+ | AppContext | | config / db / model paths | +--+-----------+-----------+--+ | | | +-----------+ | +----------------+ v v v +---------------+ +---------------+ +----------------+ | CameraCapture | | HeadCounter | | FaceMatchFlow | | V4L2/OpenCV | | large image | | register/match | +-------+-------+ +-------+-------+ +--------+-------+ | | | v v v +---------------+ +---------------+ +----------------+ | ImageUtils | | HeadDetector | | FaceDetector | | read/save | | model infer | | single face | | draw/resize | +-------+-------+ +--------+-------+ +---------------+ | | v v +---------------+ +----------------+ | Postprocess | | FaceAligner | | decode / NMS | | crop / align | +-------+-------+ +--------+-------+ | | v v +---------------+ +----------------+ | AttendanceDB |<-----------| FeatureExtract | | SQLite write | | embedding | +---------------+ +--------+-------+ | v +----------------+ | FaceMatcher | | cosine search | +----------------+ ``` ## 5. 程序运行模式 主程序 `attendance_app` 负责解析命令行参数,并根据 `--mode` 调用不同业务流程。 | 模式 | 功能 | 当前阶段 | | --- | --- | --- | | `capture` | 摄像头拍照并保存图片 | 可选实现 | | `head_count` | 大图人头数统计 | 必须实现 | | `register_face` | 注册学生人脸特征 | 必须实现 | | `face_match` | 单张人脸匹配 | 必须实现 | | `init_db` | 初始化数据库 | 必须实现 | | `full_attendance` | 大图多人脸识别打卡 | 后续扩展 | ### 5.1 命令行规范 ```bash ./attendance_app --mode init_db ./attendance_app --mode capture --output data/images/capture_001.jpg ./attendance_app --mode head_count --image data/images/classroom.jpg ./attendance_app --mode register_face --student-no 20240001 --name zhangsan --class-name class01 --image data/faces/zhangsan.jpg ./attendance_app --mode face_match --image data/images/test_face.jpg ``` ### 5.2 通用命令行参数 | 参数 | 必填 | 说明 | | --- | --- | --- | | `--mode` | 是 | 运行模式 | | `--config` | 否 | 主配置文件路径,默认 `config/app_config.json` | | `--image` | 部分模式必填 | 输入图片路径 | | `--output` | 否 | 输出图片路径 | | `--save-result-image` | 否 | 是否保存带框结果图 | | `--verbose` | 否 | 输出详细日志 | ## 6. 总流程设计 ## 6.1 程序启动流程 所有模式共用启动流程。 ```text main() -> parse command line -> load app_config.json -> load camera_config.json -> load model_config.json -> create AppContext -> init log module -> ensure data directories exist -> open SQLite database -> dispatch by mode ``` ### 6.1.1 启动阶段检查项 | 检查项 | 失败处理 | | --- | --- | | 配置文件存在且可解析 | 返回 `CONFIG_ERROR` | | 数据目录可写 | 返回 `FILE_IO_ERROR` | | 数据库可打开 | 返回 `DATABASE_ERROR` | | 模型文件存在 | 进入对应模式时返回 `MODEL_LOAD_ERROR` | | 输入图片存在 | 返回 `INVALID_ARGUMENT` 或 `FILE_IO_ERROR` | ## 6.2 大图人头数统计流程 ### 6.2.1 流程说明 ```text 输入图片路径或摄像头采集 -> 读取原图 cv::Mat -> 检查图片是否为空 -> resize / letterbox 到模型输入尺寸 -> 模型推理 -> 解码模型输出 -> 置信度过滤 -> NMS 去重 -> 坐标映射回原图 -> 统计有效目标数量 -> 绘制检测框 -> 保存结果图片 -> 写入 head_count_records -> 输出 JSON 或终端结果 ``` ### 6.2.2 顺序调用关系 ```text HeadCountFlow -> ImageUtils::ReadImage() -> HeadCounter::Count() -> HeadDetector::Detect() -> HeadPreprocessor::Preprocess() -> ModelEngine::Forward() -> DetectPostprocess::Decode() -> DetectPostprocess::Nms() -> ImageUtils::DrawDetections() -> AttendanceDB::InsertHeadCountRecord() ``` ### 6.2.3 输入 | 字段 | 类型 | 说明 | | --- | --- | --- | | `image_path` | string | 待检测课堂大图 | | `save_result_image` | bool | 是否保存带框图片 | | `result_image_path` | string | 结果图片路径,可为空 | | `confidence_threshold` | float | 检测置信度阈值 | | `nms_threshold` | float | NMS 阈值 | ### 6.2.4 输出 | 字段 | 类型 | 说明 | | --- | --- | --- | | `head_count` | int | 最终统计人数 | | `detections` | array | 检测框列表 | | `result_image_path` | string | 带框图片路径 | | `elapsed_ms` | int | 处理耗时 | | `record_id` | int64 | 数据库记录 ID | ## 6.3 单张人脸注册流程 ### 6.3.1 流程说明 ```text 输入学生信息和人脸图片 -> 检查 student_no 是否已存在 -> 读取图片 -> 人脸检测 -> 如果多张人脸,选择最大人脸 -> 人脸关键点定位 -> 人脸对齐为固定尺寸 -> 特征提取 -> 特征向量 L2 归一化 -> 保存学生信息 -> 保存人脸图片和特征向量 -> 输出注册结果 ``` ### 6.3.2 顺序调用关系 ```text FaceRegisterFlow -> FaceRepository::FindStudentByNo() -> ImageUtils::ReadImage() -> FaceDetector::Detect() -> FaceAligner::Align() -> FaceFeatureExtractor::Extract() -> FaceMatcher::Normalize() -> FaceRepository::InsertStudent() -> FaceRepository::InsertFaceTemplate() ``` ### 6.3.3 输入 | 字段 | 类型 | 说明 | | --- | --- | --- | | `student_no` | string | 学号,唯一 | | `name` | string | 姓名 | | `class_name` | string | 班级 | | `image_path` | string | 注册人脸图片 | | `overwrite` | bool | 学号已存在时是否覆盖 | ### 6.3.4 输出 | 字段 | 类型 | 说明 | | --- | --- | --- | | `student_id` | int64 | 数据库学生 ID | | `template_id` | int64 | 人脸模板 ID | | `face_image_path` | string | 保存的人脸图片路径 | | `feature_dim` | int | 特征维度 | | `message` | string | 注册说明 | ## 6.4 单张人脸匹配流程 ### 6.4.1 流程说明 ```text 输入测试人脸图片 -> 读取图片 -> 人脸检测 -> 如果多张人脸,选择最大人脸 -> 人脸对齐 -> 特征提取 -> 特征向量归一化 -> 从数据库读取所有注册特征 -> 逐个计算余弦相似度 -> 选择最高分 -> 根据阈值判断 matched / unknown -> 写入 face_match_records -> 输出匹配结果 ``` ### 6.4.2 顺序调用关系 ```text FaceMatchFlow -> ImageUtils::ReadImage() -> FaceDetector::Detect() -> FaceAligner::Align() -> FaceFeatureExtractor::Extract() -> FaceRepository::LoadAllTemplates() -> FaceMatcher::Match() -> FaceRepository::InsertFaceMatchRecord() ``` ### 6.4.3 输入 | 字段 | 类型 | 说明 | | --- | --- | --- | | `image_path` | string | 待匹配人脸图片 | | `threshold` | float | 匹配阈值 | | `save_aligned_face` | bool | 是否保存对齐后人脸 | ### 6.4.4 输出 | 字段 | 类型 | 说明 | | --- | --- | --- | | `matched` | bool | 是否匹配成功 | | `student_id` | int64 | 匹配到的学生 ID | | `student_no` | string | 学号 | | `name` | string | 姓名 | | `score` | float | 最高相似度 | | `threshold` | float | 使用阈值 | | `status` | string | `matched`、`unknown`、`failed` | | `record_id` | int64 | 识别记录 ID | ## 6.5 后续完整打卡流程 完整流程在第一阶段之后实现。 ```text 课堂大图拍照 -> 大图人脸检测 -> 裁剪每个人脸 -> 对每个人脸做对齐和特征提取 -> 对每个人脸做数据库匹配 -> 同一学生结果去重 -> 生成一次课堂 session -> 保存每个学生的到课状态 -> 保存未知人脸和低置信度人脸 ``` 为避免第一阶段过度复杂,当前仅预留 `full_attendance` 模式和数据库扩展方向。 ## 7. 模块详细设计 ## 7.1 App 模块 ### 7.1.1 职责 | 文件 | 职责 | | --- | --- | | `include/app/app_config.h` | 配置结构定义 | | `src/app/app_config.cpp` | JSON 配置读取、默认值填充 | | `include/app/app_context.h` | 全局运行上下文 | | `src/app/app_context.cpp` | 初始化目录、数据库、公共资源 | | `src/main.cpp` | 命令行入口和模式分发 | ### 7.1.2 AppContext 内容 ```cpp struct AppContext { AppConfig app_config; CameraConfig camera_config; ModelConfig model_config; std::shared_ptr db; std::shared_ptr logger; }; ``` ### 7.1.3 约束 1. `AppContext` 只保存公共依赖,不直接实现业务逻辑。 2. 模型对象由具体业务模块懒加载或在流程开始时加载。 3. 数据库连接在程序启动时打开,程序退出时关闭。 ## 7.2 Config 模块 ### 7.2.1 app_config.json ```json { "data_root": "data", "database_path": "data/database/attendance.db", "log_path": "data/logs/attendance.log", "save_result_image": true, "timezone": "Asia/Shanghai" } ``` ### 7.2.2 camera_config.json ```json { "device": "/dev/video0", "width": 1280, "height": 720, "fps": 15, "format": "MJPEG", "warmup_frames": 5 } ``` ### 7.2.3 model_config.json ```json { "engine": "ncnn", "head_detect": { "param": "models/head_detect/head_detect.param", "bin": "models/head_detect/head_detect.bin", "input_width": 416, "input_height": 416, "confidence_threshold": 0.4, "nms_threshold": 0.45, "model_version": "head_detect_v1" }, "face_detect": { "param": "models/face_recognition/face_detect.param", "bin": "models/face_recognition/face_detect.bin", "input_width": 320, "input_height": 320, "confidence_threshold": 0.6, "nms_threshold": 0.4, "model_version": "face_detect_v1" }, "face_feature": { "param": "models/face_recognition/face_feature.param", "bin": "models/face_recognition/face_feature.bin", "input_width": 112, "input_height": 112, "feature_dim": 128, "match_threshold": 0.75, "model_version": "face_feature_v1" } } ``` ### 7.2.4 配置读取规则 1. 配置缺失时使用默认值,但模型路径、数据库路径不能缺失。 2. 命令行参数优先级高于配置文件。 3. 阈值必须在 `[0, 1]` 范围内,否则返回 `CONFIG_ERROR`。 4. 文件路径统一使用 `/`,在 Windows 调试时由 C++ 标准库兼容处理。 ## 7.3 Camera 模块 ### 7.3.1 职责 1. 打开摄像头设备。 2. 设置分辨率、帧率、格式。 3. 采集一帧图片。 4. 保存拍照图片。 5. 出错时返回明确错误码。 ### 7.3.2 接口定义 ```cpp struct CameraConfig { std::string device; int width; int height; int fps; std::string format; int warmup_frames; }; struct CaptureResult { cv::Mat image; std::string saved_path; int width; int height; std::string captured_at; }; class CameraCapture { public: Status Open(const CameraConfig& config); Result CaptureOne(const std::string& save_path); void Close(); }; ``` ### 7.3.3 实现约定 1. PC 调试阶段可使用 OpenCV `VideoCapture`。 2. 板端如果 OpenCV 摄像头采集不稳定,再切换为 V4L2 实现。 3. 摄像头打开后先丢弃 `warmup_frames` 帧,降低曝光不稳定影响。 4. 保存图片默认使用 JPEG,质量参数建议 `85`。 ## 7.4 ImageUtils 模块 ### 7.4.1 职责 1. 图片读取和保存。 2. 图片尺寸检查。 3. resize 和 letterbox。 4. 检测框绘制。 5. 图片路径生成。 ### 7.4.2 接口定义 ```cpp struct LetterboxInfo { float scale; int pad_x; int pad_y; int resized_width; int resized_height; }; Result ReadImage(const std::string& path); Status SaveImage(const std::string& path, const cv::Mat& image); Result ResizeWithLetterbox( const cv::Mat& image, int target_width, int target_height, LetterboxInfo* info ); cv::Rect MapBoxToOriginal( const cv::Rect2f& model_box, const LetterboxInfo& info, int original_width, int original_height ); void DrawDetections(cv::Mat* image, const std::vector& detections); ``` ### 7.4.3 图片坐标约定 1. 原点在左上角。 2. `x` 向右增大,`y` 向下增大。 3. 检测框格式默认使用 `x, y, width, height`。 4. 数据库存储整数坐标,模型内部可使用浮点坐标。 ## 7.5 ModelEngine 模块 ### 7.5.1 职责 为 ncnn 或 TFLite 提供统一推理接口,让业务模块不直接依赖具体推理框架。 ### 7.5.2 接口定义 ```cpp struct Tensor { std::vector shape; std::vector data; }; struct ModelInput { std::string name; Tensor tensor; }; struct ModelOutput { std::string name; Tensor tensor; }; class ModelEngine { public: virtual ~ModelEngine() = default; virtual Status Load(const ModelLoadOptions& options) = 0; virtual Result> Forward( const std::vector& inputs ) = 0; }; ``` ### 7.5.3 第一阶段简化方案 如果时间紧,可以先不抽象 `ModelEngine`,直接在 `HeadDetector`、`FaceDetector`、`FaceFeatureExtractor` 中调用 ncnn。 但头文件接口仍然建议保持稳定,避免后续切换模型时影响业务流程。 ## 7.6 HeadCount 模块 ### 7.6.1 文件职责 | 文件 | 职责 | | --- | --- | | `include/head_count/head_detector.h` | 人头检测模型接口 | | `src/head_count/head_detector.cpp` | 模型加载、预处理、推理 | | `include/head_count/detect_postprocess.h` | 检测后处理接口 | | `src/head_count/detect_postprocess.cpp` | 输出解码、阈值过滤、NMS | | `include/head_count/head_counter.h` | 人头统计业务接口 | | `src/head_count/head_counter.cpp` | 串联检测、统计、保存结果 | ### 7.6.2 核心数据结构 ```cpp struct Detection { int class_id; std::string class_name; float score; float x; float y; float width; float height; }; struct HeadCountRequest { std::string image_path; bool save_result_image; std::string result_image_path; float confidence_threshold; float nms_threshold; }; struct HeadCountResult { int head_count; std::vector detections; std::string image_path; std::string result_image_path; int elapsed_ms; int64_t record_id; }; ``` ### 7.6.3 HeadDetector 接口 ```cpp class HeadDetector { public: Status Load(const HeadDetectConfig& config); Result> Detect(const cv::Mat& image); }; ``` ### 7.6.4 HeadCounter 接口 ```cpp class HeadCounter { public: HeadCounter(HeadDetector* detector, AttendanceDB* db); Result Count(const HeadCountRequest& request); }; ``` ### 7.6.5 后处理规则 1. 先按置信度过滤。 2. 再按类别过滤,只保留 `head` 或 `face` 类别。 3. 执行 NMS。 4. 将检测框映射回原图。 5. 裁剪超出图片边界的坐标。 6. 最终 `head_count = detections.size()`。 ### 7.6.6 NMS 接口 ```cpp std::vector Nms( const std::vector& input, float iou_threshold ); ``` ## 7.7 FaceMatch 模块 ### 7.7.1 文件职责 | 文件 | 职责 | | --- | --- | | `include/face_match/face_detector.h` | 单张图片人脸检测接口 | | `src/face_match/face_detector.cpp` | 人脸检测模型推理 | | `include/face_match/face_aligner.h` | 人脸对齐接口 | | `src/face_match/face_aligner.cpp` | 关键点对齐和裁剪 | | `include/face_match/face_feature_extractor.h` | 特征提取接口 | | `src/face_match/face_feature_extractor.cpp` | 人脸特征模型推理 | | `include/face_match/face_matcher.h` | 特征匹配接口 | | `src/face_match/face_matcher.cpp` | 余弦相似度匹配 | ### 7.7.2 核心数据结构 ```cpp struct FaceLandmarks { cv::Point2f left_eye; cv::Point2f right_eye; cv::Point2f nose; cv::Point2f left_mouth; cv::Point2f right_mouth; }; struct FaceDetection { float score; cv::Rect2f box; FaceLandmarks landmarks; }; struct FaceFeature { std::vector values; int dim; std::string model_version; }; struct Student { int64_t id; std::string student_no; std::string name; std::string class_name; }; struct FaceTemplate { int64_t id; Student student; FaceFeature feature; std::string face_image_path; }; ``` ### 7.7.3 FaceDetector 接口 ```cpp class FaceDetector { public: Status Load(const FaceDetectConfig& config); Result> Detect(const cv::Mat& image); }; ``` ### 7.7.4 FaceAligner 接口 ```cpp class FaceAligner { public: Result Align( const cv::Mat& image, const FaceDetection& face, int output_width, int output_height ); }; ``` ### 7.7.5 FaceFeatureExtractor 接口 ```cpp class FaceFeatureExtractor { public: Status Load(const FaceFeatureConfig& config); Result Extract(const cv::Mat& aligned_face); }; ``` ### 7.7.6 FaceMatcher 接口 ```cpp struct FaceMatchCandidate { int64_t student_id; std::string student_no; std::string name; float score; }; struct FaceMatchRequest { std::string image_path; float threshold; bool save_aligned_face; }; struct FaceMatchResult { bool matched; std::string status; FaceMatchCandidate best; float threshold; std::string input_image_path; std::string aligned_face_path; int elapsed_ms; int64_t record_id; }; class FaceMatcher { public: static void Normalize(FaceFeature* feature); static float CosineSimilarity( const std::vector& a, const std::vector& b ); Result Match( const FaceFeature& query, const std::vector& templates, float threshold ); }; ``` ### 7.7.7 多人脸处理规则 单张人脸匹配功能原则上要求输入图片只有一个人脸。若检测到多张人脸,第一阶段默认选择面积最大的人脸。 后续可以增加策略: 1. 选择置信度最高的人脸。 2. 选择离图片中心最近的人脸。 3. 返回错误,要求重新拍摄。 当前推荐:注册和单张匹配都选择面积最大人脸,并在日志中记录检测到的人脸数量。 ## 7.8 Database 模块 ### 7.8.1 文件职责 | 文件 | 职责 | | --- | --- | | `include/database/attendance_db.h` | 数据库连接和表初始化 | | `src/database/attendance_db.cpp` | SQLite 执行 SQL、事务封装 | | `include/database/face_repository.h` | 学生、人脸模板、识别记录的数据访问 | | `src/database/face_repository.cpp` | 业务数据读写实现 | ### 7.8.2 AttendanceDB 接口 ```cpp class AttendanceDB { public: Status Open(const std::string& db_path); Status InitSchema(); Status Execute(const std::string& sql); sqlite3* RawHandle(); void Close(); }; ``` ### 7.8.3 FaceRepository 接口 ```cpp class FaceRepository { public: explicit FaceRepository(AttendanceDB* db); Result InsertStudent(const Student& student); Result FindStudentByNo(const std::string& student_no); Result InsertFaceTemplate( int64_t student_id, const std::string& image_path, const FaceFeature& feature ); Result> LoadAllTemplates(); Result InsertFaceMatchRecord(const FaceMatchResult& result); Result InsertHeadCountRecord(const HeadCountResult& result); }; ``` ### 7.8.4 事务约定 1. 注册学生时,`students` 和 `face_templates` 必须在同一事务内写入。 2. 写入检测记录时,如果结果图片保存失败,不写数据库记录。 3. 数据库写失败时,业务流程返回失败,但不删除原始输入图片。 4. 特征向量以 float32 二进制 BLOB 保存。 ## 8. 通用接口规范 ## 8.1 命名空间 所有业务代码建议放入统一命名空间: ```cpp namespace attendance { // ... } ``` ## 8.2 返回值规范 推荐使用 `Status` 和 `Result` 统一表达成功、失败和错误信息。 ```cpp enum class StatusCode { OK = 0, INVALID_ARGUMENT, CONFIG_ERROR, FILE_IO_ERROR, CAMERA_ERROR, MODEL_LOAD_ERROR, MODEL_INFER_ERROR, IMAGE_PROCESS_ERROR, FACE_NOT_FOUND, MULTIPLE_FACES_FOUND, FEATURE_ERROR, DATABASE_ERROR, NOT_FOUND, UNKNOWN_ERROR }; struct Status { StatusCode code; std::string message; bool ok() const { return code == StatusCode::OK; } }; template struct Result { Status status; T value; bool ok() const { return status.ok(); } }; ``` ### 8.2.1 错误码说明 | 错误码 | 说明 | | --- | --- | | `OK` | 成功 | | `INVALID_ARGUMENT` | 命令行参数或函数入参不合法 | | `CONFIG_ERROR` | 配置文件缺失、格式错误、字段非法 | | `FILE_IO_ERROR` | 图片、模型、目录读写失败 | | `CAMERA_ERROR` | 摄像头打开或采集失败 | | `MODEL_LOAD_ERROR` | 模型文件加载失败 | | `MODEL_INFER_ERROR` | 模型推理失败 | | `IMAGE_PROCESS_ERROR` | 图片 resize、转换、裁剪失败 | | `FACE_NOT_FOUND` | 未检测到人脸 | | `MULTIPLE_FACES_FOUND` | 检测到多张人脸且当前策略不允许 | | `FEATURE_ERROR` | 特征维度错误或特征提取失败 | | `DATABASE_ERROR` | SQLite 执行失败 | | `NOT_FOUND` | 数据不存在 | | `UNKNOWN_ERROR` | 未分类错误 | ## 8.3 日志规范 日志格式: ```text [2026-06-04 09:00:00] [INFO] [HeadCounter] image=data/images/classroom.jpg head_count=36 elapsed_ms=2530 ``` 日志级别: | 级别 | 使用场景 | | --- | --- | | `DEBUG` | 模型输入尺寸、检测框数量、阈值等调试信息 | | `INFO` | 主要流程开始和结束 | | `WARN` | 非致命问题,例如检测到多张人脸但选择最大人脸 | | `ERROR` | 流程失败,例如模型加载失败、数据库写失败 | ## 8.4 JSON 输出规范 命令行工具建议支持 `--json` 参数,便于后续 UI 或脚本调用。 ### 8.4.1 head_count 输出 ```json { "status": "ok", "mode": "head_count", "image_path": "data/images/classroom.jpg", "result_image_path": "data/results/classroom_detected.jpg", "head_count": 36, "detections": [ { "class_id": 0, "class_name": "head", "score": 0.91, "x": 120, "y": 85, "width": 32, "height": 36 } ], "elapsed_ms": 2530, "record_id": 1 } ``` ### 8.4.2 face_match 输出 ```json { "status": "matched", "mode": "face_match", "input_image_path": "data/images/test_face.jpg", "matched": true, "student_id": 1, "student_no": "20240001", "name": "zhangsan", "score": 0.82, "threshold": 0.75, "elapsed_ms": 840, "record_id": 12 } ``` ### 8.4.3 失败输出 ```json { "status": "error", "code": "FACE_NOT_FOUND", "message": "no face detected in image", "mode": "face_match" } ``` ## 9. 数据库设计 ## 9.1 students ```sql CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL UNIQUE, name TEXT NOT NULL, class_name TEXT, created_at TEXT NOT NULL, updated_at TEXT ); ``` ## 9.2 face_templates ```sql CREATE TABLE IF NOT EXISTS face_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, face_image_path TEXT NOT NULL, feature BLOB NOT NULL, feature_dim INTEGER NOT NULL, model_version TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY(student_id) REFERENCES students(id) ); ``` ## 9.3 head_count_records ```sql CREATE TABLE IF NOT EXISTS head_count_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_path TEXT NOT NULL, result_image_path TEXT, head_count INTEGER NOT NULL, detections_json TEXT NOT NULL, confidence_threshold REAL NOT NULL, nms_threshold REAL NOT NULL, model_version TEXT NOT NULL, elapsed_ms INTEGER, created_at TEXT NOT NULL ); ``` ## 9.4 face_match_records ```sql CREATE TABLE IF NOT EXISTS face_match_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, input_image_path TEXT NOT NULL, aligned_face_path TEXT, matched_student_id INTEGER, score REAL, threshold REAL NOT NULL, status TEXT NOT NULL, model_version TEXT NOT NULL, elapsed_ms INTEGER, created_at TEXT NOT NULL, FOREIGN KEY(matched_student_id) REFERENCES students(id) ); ``` ## 9.5 后续扩展表 完整课堂打卡时建议增加: ```sql CREATE TABLE IF NOT EXISTS attendance_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, class_name TEXT, course_name TEXT, image_path TEXT NOT NULL, result_image_path TEXT, expected_count INTEGER, detected_count INTEGER, recognized_count INTEGER, unknown_count INTEGER, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS attendance_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER NOT NULL, student_id INTEGER, face_image_path TEXT, bbox_json TEXT, score REAL, status TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY(session_id) REFERENCES attendance_sessions(id), FOREIGN KEY(student_id) REFERENCES students(id) ); ``` ## 10. 文件和目录规范 ## 10.1 目录用途 | 目录 | 用途 | | --- | --- | | `config/` | 配置文件 | | `models/head_detect/` | 大图人头检测模型 | | `models/face_recognition/` | 人脸检测和特征模型 | | `data/images/` | 原始图片 | | `data/faces/` | 注册人脸、对齐人脸、裁剪人脸 | | `data/results/` | 带框结果图 | | `data/database/` | SQLite 数据库 | | `data/logs/` | 日志文件 | | `include/` | 头文件 | | `src/` | 源码 | | `tools/` | 独立工具程序 | | `tests/` | 单元测试 | | `docs/` | 设计文档 | ## 10.2 文件命名 拍照图片: ```text data/images/capture_YYYYMMDD_HHMMSS.jpg ``` 人头统计结果图: ```text data/results/head_count_YYYYMMDD_HHMMSS.jpg ``` 注册人脸图: ```text data/faces/register_{student_no}_YYYYMMDD_HHMMSS.jpg ``` 单张匹配对齐图: ```text data/faces/match_aligned_YYYYMMDD_HHMMSS.jpg ``` ## 11. 模块间数据流 ## 11.1 head_count 数据流 | 阶段 | 输入 | 输出 | | --- | --- | --- | | 图片读取 | `image_path` | `cv::Mat original` | | 预处理 | `original` | `model_input`, `LetterboxInfo` | | 推理 | `model_input` | raw tensors | | 后处理 | raw tensors, `LetterboxInfo` | `vector` | | 统计 | `vector` | `head_count` | | 画图 | `original`, detections | result image | | 入库 | result object | record ID | ## 11.2 face_match 数据流 | 阶段 | 输入 | 输出 | | --- | --- | --- | | 图片读取 | `image_path` | `cv::Mat image` | | 人脸检测 | `image` | `vector` | | 人脸选择 | detections | `FaceDetection selected` | | 对齐 | image, selected | `cv::Mat aligned_face` | | 特征提取 | aligned_face | `FaceFeature query` | | 模板读取 | database | `vector` | | 匹配 | query, templates | `FaceMatchResult` | | 入库 | result | record ID | ## 12. 性能和资源约束 IMX6ULL_ALPHA 板端性能有限,代码实现时需要遵守以下约束: 1. 模型只加载一次,不能每张图片重新加载。 2. 尽量使用较小输入尺寸,例如 `320x320`、`416x416`。 3. 大图检测优先使用 letterbox,不直接拉伸变形。 4. 数据库特征模板加载后可以缓存,注册新人后再刷新缓存。 5. 图片保存默认 JPEG,避免保存 BMP/PNG 大文件。 6. 日志不要频繁写大量检测框细节,调试时再打开 `DEBUG`。 7. 避免在循环中频繁创建大 `cv::Mat`,可复用缓冲区。 8. 第一阶段采用单线程顺序流程,确保稳定后再考虑多线程。 ## 13. 测试计划 ## 13.1 单元测试 | 测试文件 | 测试内容 | | --- | --- | | `tests/test_head_postprocess.cpp` | NMS、坐标映射、置信度过滤 | | `tests/test_face_similarity.cpp` | 余弦相似度、特征归一化 | | `tests/test_config.cpp` | 配置读取和默认值 | | `tests/test_database.cpp` | 数据库初始化、插入、查询 | ## 13.2 PC 端集成测试 1. 使用本地图片测试 `head_count`。 2. 注册 5 名学生。 3. 用同一学生不同图片测试 `face_match`。 4. 用未注册人员图片测试 `unknown`。 5. 删除模型文件,确认返回 `MODEL_LOAD_ERROR`。 6. 输入不存在图片,确认返回 `FILE_IO_ERROR`。 ## 13.3 板端测试 1. 摄像头能正常拍照并保存。 2. `head_count` 能在板端完成一次推理。 3. `face_match` 能在板端完成一次推理。 4. 连续运行 20 次不崩溃。 5. 断电重启后数据库仍可读取。 6. 存储空间不足时能返回明确错误。 ## 14. 第一阶段验收标准 ## 14.1 大图人头数统计 1. 能读取本地课堂大图。 2. 能输出检测到的人头数量。 3. 能保存带框结果图。 4. 能将检测记录写入 SQLite。 5. 命令行能输出文本结果和 JSON 结果。 ## 14.2 单张人脸匹配 1. 能注册至少 5 名学生。 2. 能保存学生信息和特征向量。 3. 能识别已注册学生。 4. 能将未注册人员识别为 `unknown`。 5. 能保存识别记录。 ## 15. 待确认决策项 以下问题建议在开始编码核心模型模块前确认。 | 编号 | 决策项 | 推荐默认值 | 影响 | | --- | --- | --- | --- | | D1 | 推理框架选 ncnn 还是 TFLite | ncnn | 影响模型转换和 C++ 调用方式 | | D2 | 大图统计检测对象是人头还是人脸 | 人头优先,脸部可备选 | 人头对低头、侧脸更稳;人脸方便后续识别 | | D3 | 摄像头类型 | 先 USB 摄像头 | OpenCV 采集更容易调试 | | D4 | 人脸特征维度 | 按模型确定,优先 128 或 512 | 影响数据库 BLOB 大小和匹配速度 | | D5 | 注册时一人保存几个模板 | 第一阶段 1 个 | 多模板准确率更高,但管理更复杂 | | D6 | 多人脸输入单张匹配时如何处理 | 选择最大人脸 | 简单可演示,但可能误选背景人脸 | | D7 | 匹配阈值 | 初始 0.75 | 需要通过测试集调参 | | D8 | 是否加入 LCD/Web 界面 | 第一阶段不加 | 先保证算法和数据链路稳定 | | D9 | 是否做完整大图多人脸识别 | 第二阶段做 | 第一阶段先验证两个基础能力 | ## 16. 建议实施顺序 1. 建立基础 CMake 工程。 2. 实现 `Status`、`Result`、日志、时间工具。 3. 实现配置读取。 4. 实现 SQLite 初始化和基础表结构。 5. 实现图片读写、画框、letterbox、坐标映射。 6. 实现 `head_count` 的后处理和假数据测试。 7. 接入真实人头检测模型。 8. 实现人脸特征相似度和数据库模板读写。 9. 接入人脸检测、人脸对齐、特征提取模型。 10. 打通 `register_face`。 11. 打通 `face_match`。 12. 在 PC 端完整测试。 13. 交叉编译部署到 IMX6ULL_ALPHA。 14. 根据板端耗时和准确率调整模型和阈值。 ## 17. 后续扩展接口预留 完整课堂打卡可以新增以下业务接口。 ```cpp struct AttendanceSessionRequest { std::string image_path; std::string class_name; std::string course_name; bool save_result_image; }; struct AttendanceRecord { int64_t student_id; std::string student_no; std::string name; Detection face_box; float score; std::string status; std::string face_image_path; }; struct AttendanceSessionResult { int64_t session_id; int detected_count; int recognized_count; int unknown_count; std::vector records; std::string image_path; std::string result_image_path; }; class AttendanceFlow { public: Result Run( const AttendanceSessionRequest& request ); }; ``` 后续完整打卡流程可以复用第一阶段已经实现的: 1. 图片读取和保存。 2. 人脸或人头检测。 3. 人脸对齐。 4. 特征提取。 5. 特征匹配。 6. SQLite 入库。 ## 18. 风险点和规避方案 | 风险 | 表现 | 规避方案 | | --- | --- | --- | | 板端推理慢 | 一张图处理十几秒以上 | 降低输入尺寸、换更小模型、只做演示级检测 | | 后排人脸太小 | 漏检严重 | 使用更高分辨率拍照、调整摄像头位置、使用人头检测模型 | | 光照差 | 检测和识别不稳定 | 固定摄像头曝光,增加补光,加入模糊/亮度检查 | | 阈值不合适 | 误识别或大量 unknown | 用真实班级图片调参 | | 数据库特征模型版本混乱 | 新旧特征不可比 | `face_templates` 必须保存 `model_version` | | 中文路径或姓名乱码 | 数据显示异常 | 源码和数据库统一 UTF-8 | | 摄像头驱动不稳定 | 拍照失败 | 先用 USB 摄像头,必要时改 V4L2 | ## 19. 当前推荐结论 第一阶段按以下路线推进最稳: 1. 使用 C++17 + OpenCV + SQLite + ncnn。 2. 先实现命令行程序,不做界面。 3. 大图人头数统计和单张人脸匹配分开实现。 4. 人头统计先只输出人数,不做人脸身份识别。 5. 单张人脸匹配先要求输入清晰单人脸。 6. 所有结果都保存到 SQLite,便于答辩展示和后续扩展。