开发了一套基于虹软人脸识别SDK的会议签到系统

       会议签到老是人工找单位,找名字的签字太麻烦,会议后统计也是啰嗦,一个单位一个单位的算,而且会议要求什么人来,结果还随便找个人代替,会议的实效性大大降低,公司大了,部门多,事也多,本身就是为了赚钱的生意,开会讲究的就是高效、有效,偏偏连这点事都管不好?发挥下IT部门的优势,下手整一套人脸识别的会议签到系统。

        除了上面的需求,还要考虑不同终端的跨平台需求,先想想用什么方法解决,有人可能会问,为啥不用那些成熟的会议签到系统,网上很多,手机PC都能签到,多方便,不好意思,公司虽然不大,但是对安全保密要求还是比较严格的,尤其是组织架构和个人信息这方面管的那叫一个宽,否则我也不会在之前专门写了单机运行的人脸识别考勤和就餐扣费系统了。废话了半天,直接上实际内容吧。

       程序总体完成度95%了,就剩下一个密码登录的管理,对我来说写不写无所谓,有需要的可以自己去写写控制一下权限就好,内部网络或者单机运行,就我个人来说,没必要那么折腾。

完成后的界面先展示一下:

当天的会议会有明显标识。

模板文件部分代码:

{% extends "base.html" %}

{% block content %}
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
    <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
        <h2 class="text-xl font-bold text-gray-800">会议签到列表</h2>
        <div class="text-sm text-gray-600">
            选择会议进行签到
        </div>
    </div>

    <div class="overflow-x-auto">
        <table class="min-w-full divide-y divide-gray-200">
            <thead class="bg-gray-50">
                <tr>
                    <th scope="col"
                        class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
                    <th scope="col"
                        class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">会议名称</th>
                    <th scope="col"
                        class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">开始时间</th>
                    <th scope="col"
                        class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">结束时间</th>
                    <th scope="col"
                        class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">地点</th>
                    <th scope="col"
                        class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">参会人数</th>
                    <th scope="col"
                        class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">已签到</th>
                    <th scope="col"
                        class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
                </tr>
            </thead>
            <tbody class="bg-white divide-y divide-gray-200">
                {% if meetings and meetings|length > 0 %}
                {% for meeting in meetings %}
                <!-- 当天会议高亮显示 -->
                <tr {% if meeting.is_today %}class="bg-yellow-50 border-l-4 border-yellow-400" {% endif %}>
                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                        {{ meeting.id }}
                        {% if meeting.is_today %}
                        <span
                            class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
                            今天
                        </span>
                        {% endif %}
                    </td>
                    <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
                        {{ meeting.name }}
                    </td>
                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ meeting.start_time.replace('T', '
                        ') }}</td>
                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ meeting.end_time.replace('T', ' ')
                        }}</td>
                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ meeting.location or '-' }}</td>
                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ meeting.participant_count }}</td>
                    <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ meeting.checkin_count }}</td>
                    <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
                        <a href="{{ url_for('meeting_checkin', meeting_id=meeting.id) }}"
                            class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition duration-150 ease-in-out">
                            <i class="fa fa-id-card mr-1"></i> 进入签到
                        </a>
                    </td>
                </tr>
                {% endfor %}
                {% else %}
                <tr>
                    <td colspan="8" class="px-6 py-10 text-center text-sm text-gray-500">
                        <div class="flex flex-col items-center">
                            <i class="fa fa-calendar-o text-gray-300 text-4xl mb-3"></i>
                            <p>暂无会议数据</p>
                        </div>
                    </td>
                </tr>
                {% endif %}
            </tbody>
        </table>
    </div>
</div>
{% endblock %}

对应路由代码:

@app.route('/public_meeting_list')
def public_meeting_list():
    """公开的会议列表页面,仅用于签到"""
    conn = sqlite3.connect(app.config['DATABASE_FILE'])
    c = conn.cursor()
    # 按开始时间倒序排列,最新的会议在前面
    c.execute('SELECT * FROM meetings ORDER BY start_time DESC')
    meetings = c.fetchall()
    meeting_list = []
    # 获取当前日期(仅年月日部分)
    current_date = datetime.now().strftime('%Y-%m-%d')
    for meeting in meetings:
        participant_count = 0
        if meeting[5]:
            participant_ids = meeting[5].split(',')
            participant_count = len(participant_ids) if (participant_ids and participant_ids[0]) else 0
        c.execute('SELECT COUNT(*) FROM checkin_records WHERE meeting_id=?', (meeting[0],))
        checkin_count = c.fetchone()[0]
        # 提取会议开始日期(仅年月日)
        meeting_date = meeting[2].split(' ')[0] if ' ' in meeting[2] else meeting[2].split('T')[0]
        # 判断是否为当天会议
        is_today = (meeting_date == current_date)
        meeting_list.append({
            'id': meeting[0],
            'name': meeting[1],
            'start_time': meeting[2],
            'end_time': meeting[3],
            'location': meeting[4],
            'participant_count': participant_count,
            'checkin_count': checkin_count,
            'is_today': is_today
        })
    conn.close()
    return render_template('public_meeting_list.html', meetings=meeting_list)

默认进入公开的会议列表界面,程序代码可修改默认页面。

# 路由定义
# @app.route('/')
# def index():
#     return redirect(url_for('department_list'))  #部门列表

@app.route('/')
def index():
    # 重定向到新增的公开会议列表页面
    return redirect(url_for('public_meeting_list'))

进入签到页面:

界面一目了然,我就不废话介绍了。

识别成功和识别失败以及重复签到都会有相应的提示,已签到的人员会在列表中显示姓名部门和签到时间。

模板文件代码:
 

{% extends "base.html" %}

{% block content %}
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
    <div class="mb-6 p-4 
        {% if meeting_status == 'not_started' %}bg-yellow-50 border-l-4 border-yellow-400{% endif %}
        {% if meeting_status == 'ongoing' %}bg-green-50 border-l-4 border-green-400{% endif %}
        {% if meeting_status == 'ended' %}bg-red-50 border-l-4 border-red-400{% endif %}">
        <div class="flex">
            <div class="flex-shrink-0">
                {% if meeting_status == 'not_started' %}
                <i class="fa fa-calendar-o text-yellow-500"></i>
                {% elif meeting_status == 'ongoing' %}
                <i class="fa fa-calendar-check-o text-green-500"></i>
                {% else %}
                <i class="fa fa-calendar-times-o text-red-500"></i>
                {% endif %}
            </div>
            <div class="ml-3">
                <p class="text-sm {% if meeting_status == 'not_started' %}text-yellow-700{% endif %}
                                 {% if meeting_status == 'ongoing' %}text-green-700{% endif %}
                                 {% if meeting_status == 'ended' %}text-red-700{% endif %}">
                    {{ status_text }}
                </p>
            </div>
        </div>
    </div>

    <!-- 会议基本信息(突出日期) -->
    <div class="flex justify-between items-center mb-6">
        <h2 class="text-xl font-bold text-gray-800">会议签到 - {{ meeting[1] }}</h2>
        <div class="text-sm text-gray-600">
            <p><strong>会议日期:</strong> {{ meeting_date }}</p>
            <p><strong>时间范围:</strong> {{ meeting[2].split('T')[-1] }} 至 {{ meeting[3].split('T')[-1] }}</p>
            <p><strong>地点:</strong> {{ meeting[4] }}</p>
        </div>
    </div>

    <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <!-- 左侧:人脸识别区域 -->
        <div class="lg:col-span-2">
            <div class="bg-gray-100 rounded-lg p-4 mb-4">
                <h3 class="text-lg font-medium text-gray-800 mb-3">摄像头自动签到</h3>

                <div class="relative bg-gray-800 rounded-lg overflow-hidden" style="height: 600px;">
                    <!-- 视频预览区域 -->
                    <video id="video" class="w-full h-full object-cover" {% if meeting_status !='ongoing' %}disabled{%
                        endif %} autoplay muted playsinline></video>

                    <!-- 人脸框引导(仅当天显示) -->
                    {% if meeting_status == 'ongoing' %}
                    <div class="face-guide"></div>
                    {% endif %}

                    <!-- 状态提示 -->
                    <div id="status-indicator"
                        class="absolute bottom-4 left-4 bg-black bg-opacity-70 text-white px-4 py-2 rounded-lg text-sm">
                        {% if meeting_status == 'ongoing' %}
                        正在准备摄像头...
                        {% elif meeting_status == 'not_started' %}
                        会议尚未开始,当天可签到
                        {% else %}
                        会议已结束,无法签到
                        {% endif %}
                    </div>

                    <!-- 加载状态遮罩 -->
                    <div id="loading-overlay"
                        class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
                        <div class="text-white text-center">
                            <i class="fa fa-circle-o-notch fa-spin fa-3x mb-2"></i>
                            <p>正在识别...</p>
                        </div>
                    </div>

                    <!-- 非当天显示遮罩 -->
                    {% if meeting_status != 'ongoing' %}
                    <div class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
                        <div class="text-white text-center">
                            <i class="fa fa-ban fa-3x mb-2"></i>
                            <p>{{ status_text }}</p>
                        </div>
                    </div>
                    {% endif %}
                </div>

                <div class="text-center text-sm text-gray-600 mt-2">
                    <p>摄像头将自动检测并识别人脸进行签到,无需手动操作</p>
                </div>
            </div>
        </div>

        <!-- 右侧:签到结果和已签到列表 -->
        <div class="lg:col-span-1">
            <div class="bg-gray-100 rounded-lg p-4 mb-6">
                <h3 class="text-lg font-medium text-gray-800 mb-3">签到结果</h3>

                <div id="result-container"
                    class="bg-white rounded-lg p-4 border border-gray-200 h-[230px] overflow-y-auto">
                    {% if meeting_status != 'ongoing' %}
                    <div class="text-center text-gray-500 h-full flex flex-col items-center justify-center">
                        <i class="fa fa-info-circle text-4xl mb-3"></i>
                        <p>{{ status_text }}</p>
                    </div>
                    {% else %}
                    <div class="text-center text-gray-500 h-full flex flex-col items-center justify-center">
                        <i class="fa fa-clock-o text-4xl mb-3"></i>
                        <p>等待签到...</p>
                        <p class="text-xs mt-2">请将面部对准摄像头</p>
                    </div>
                    {% endif %}
                </div>
            </div>

            <!-- 已签到人员列表 -->
            <div class="bg-gray-100 rounded-lg p-4">
                <h3 class="text-lg font-medium text-gray-800 mb-3">已签到人员</h3>

                <div id="checkin-list-container"
                    class="bg-white rounded-lg p-4 border border-gray-200 h-[300px] overflow-y-auto">
                    <div id="checkin-list-loading" class="text-center text-gray-500 py-6">
                        <i class="fa fa-circle-o-notch fa-spin mr-2"></i>
                        <span>加载签到记录中...</span>
                    </div>
                    <div id="checkin-list-empty" class="text-center text-gray-500 py-6 hidden">
                        <i class="fa fa-inbox text-2xl mb-2"></i>
                        <p>暂无签到记录</p>
                    </div>
                    <table id="checkin-list" class="min-w-full divide-y divide-gray-200 hidden">
                        <thead>
                            <tr>
                                <th
                                    class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    部门</th>
                                <th
                                    class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    姓名</th>
                                <th
                                    class="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                    签到时间</th>
                            </tr>
                        </thead>
                        <tbody id="checkin-list-body" class="divide-y divide-gray-200">
                            <!-- 签到记录将通过JS动态添加 -->
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

<style>
    /* 添加人脸框引导样式 */
    .face-guide {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 200px;
        height: 200px;
        border: 2px solid rgba(0, 255, 0, 0.5);
        border-radius: 10px;
        pointer-events: none;
    }

    /* 添加通知样式 */
    .notification {
        position: fixed;
        top: 60px;
        right: 60px;
        padding: 12px 20px;
        border-radius: 4px;
        color: white;
        font-size: 14px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        z-index: 1000;
        opacity: 0;
        transition: opacity 0.3s ease;
    }

    .notification.success {
        background-color: #10b981;
    }

    .notification.error {
        background-color: #ef4444;
    }

    .notification.show {
        opacity: 1;
    }

    /* 添加表格样式 */
    #checkin-list td {
        padding: 6px 4px;
        text-sm;
    }

    #checkin-list tr:nth-child(even) {
        background-color: #f9fafb;
    }
</style>

<script>
    // 页面加载完成后执行
    document.addEventListener('DOMContentLoaded', function () {
        const video = document.getElementById('video');
        const statusIndicator = document.getElementById('status-indicator');
        const resultContainer = document.getElementById('result-container');
        const loadingOverlay = document.getElementById('loading-overlay');
        const meetingId = "{{ meeting[0] }}";
        const meetingStatus = "{{ meeting_status }}";

        // 自动识别相关变量
        let recognitionInterval;
        let lastCheckinTime = 0;
        const MIN_CHECKIN_INTERVAL = 2000; // 2秒内不重复签到

        // 非会议当天:关闭摄像头和识别
        if (meetingStatus !== 'ongoing') {
            // 停止可能存在的摄像头流
            if (video.srcObject) {
                video.srcObject.getTracks().forEach(track => track.stop());
            }
        } else {
            // 会议当天:初始化摄像头
            initCamera();
        }

        // 页面加载时获取签到记录
        fetchCheckinRecords();

        // 定时刷新签到记录(每30秒)
        setInterval(fetchCheckinRecords, 30000);

        // 添加通知显示功能
        function showNotification(message, type = 'success') {
            // 创建通知元素
            const notification = document.createElement('div');
            notification.className = `notification ${type} show`;
            notification.textContent = message;

            // 添加到页面
            document.body.appendChild(notification);

            // 3秒后移除
            setTimeout(() => {
                notification.classList.remove('show');
                setTimeout(() => {
                    document.body.removeChild(notification);
                }, 300);
            }, 3000);
        }

        // 初始化摄像头
        function initCamera() {
            // 检查浏览器是否支持getUserMedia
            if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
                // 获取摄像头流
                navigator.mediaDevices.getUserMedia({
                    video: {
                        facingMode: 'user',
                        width: { ideal: 1280 },
                        height: { ideal: 720 }
                    }
                })
                    .then(function (stream) {
                        video.srcObject = stream;
                        statusIndicator.textContent = "摄像头已就绪,将自动识别人脸...";

                        // 开始自动识别
                        startAutoRecognition();
                    })
                    .catch(function (error) {
                        console.error('摄像头访问失败:', error);
                        statusIndicator.textContent = "无法访问摄像头,请检查设备";
                        statusIndicator.classList.add('bg-red-600');

                        // 显示错误信息
                        resultContainer.innerHTML = `
                        <div class="text-center text-gray-500">
                            <i class="fa fa-exclamation-triangle text-4xl mb-3"></i>
                            <p>无法访问摄像头</p>
                            <p class="text-xs mt-2">请检查摄像头是否正常工作</p>
                        </div>
                    `;
                    });
            } else {
                // 不支持getUserMedia
                statusIndicator.textContent = "浏览器不支持摄像头功能";
                statusIndicator.classList.add('bg-red-600');

                resultContainer.innerHTML = `
                    <div class="text-center text-gray-500">
                        <i class="fa fa-exclamation-triangle text-4xl mb-3"></i>
                        <p>您的浏览器不支持摄像头功能</p>
                        <p class="text-xs mt-2">请更换现代浏览器重试</p>
                    </div>
                `;
            }
        }

        // 开始自动识别
        function startAutoRecognition() {
            // 每2秒尝试识别一次
            recognitionInterval = setInterval(() => {
                const now = Date.now();
                // 检查是否在冷却期内
                if (now - lastCheckinTime < MIN_CHECKIN_INTERVAL) {
                    return;
                }

                // 进行识别
                captureAndRecognize();
            }, 2000);
        }

        // 捕获并识别
        function captureAndRecognize() {
            // 创建Canvas并绘制当前视频帧
            const canvas = document.createElement('canvas');
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            canvas.getContext('2d').drawImage(video, 0, 0);

            // 将Canvas内容转换为Blob
            canvas.toBlob(function (blob) {
                // 创建FormData并添加图像
                const formData = new FormData();
                formData.append('file', blob, 'face.jpg');

                // 发送请求
                sendFaceImage(formData, true);
            }, 'image/jpeg');
        }

        // 发送人脸图像进行识别
        function sendFaceImage(formData, isAuto) {
            // 显示加载状态
            loadingOverlay.classList.remove('hidden');
            if (isAuto) {
                statusIndicator.textContent = "正在识别...";
            }

            // 1. 校验文件是否存在
            const file = formData.get('file');
            if (!file) {
                loadingOverlay.classList.add('hidden');
                showNotification('无法获取图像数据', 'error');
                return;
            }

            // 2. 校验文件大小(限制在5MB以内)
            if (file.size > 5 * 1024 * 1024) {
                loadingOverlay.classList.add('hidden');
                showNotification('图像过大,请重试', 'error');
                return;
            }

            // 3. 发送请求并处理响应
            fetch(`/meeting/checkin/${meetingId}`, {
                method: 'POST',
                body: formData
            })
                .then(response => {
                    // 解析后端返回的JSON(无论状态码是否为200)
                    return response.json().then(data => {
                        return { status: response.status, data: data };
                    }).catch(() => {
                        // 若后端返回非JSON(如HTML错误页),直接提示
                        return { status: response.status, data: { message: `服务器返回无效数据(状态码:${response.status})` } };
                    });
                })
                .then(({ status, data }) => {
                    loadingOverlay.classList.add('hidden');
                    if (isAuto) {
                        statusIndicator.textContent = "摄像头已就绪,将自动识别人脸...";
                    }

                    // 处理不同状态码
                    if (status === 200 && data.success) {
                        // 签到成功
                        showSuccessResult(data.data);
                        showNotification('签到成功');
                        lastCheckinTime = Date.now();
                        if (isAuto) {
                            clearInterval(recognitionInterval);
                            setTimeout(() => startAutoRecognition(), MIN_CHECKIN_INTERVAL);
                        }
                    } else {
                        // 显示后端返回的具体错误原因
                        const errorMsg = data.message || `请求失败(状态码:${status})`;
                        showNotification(errorMsg, 'error');

                        // 在结果区域显示详细错误
                        resultContainer.innerHTML = `
                <div class="text-center text-red-500">
                    <i class="fa fa-exclamation-circle text-2xl mb-2"></i>
                    <p class="text-sm">${errorMsg}</p>
                    <p class="text-xs mt-1">请检查后重试</p>
                </div>
            `;
                    }
                })
                .catch(error => {
                    // 网络错误等异常
                    loadingOverlay.classList.add('hidden');
                    const errorMsg = `网络异常:${error.message}`;
                    showNotification(errorMsg, 'error');
                    resultContainer.innerHTML = `
            <div class="text-center text-red-500">
                <i class="fa fa-wifi text-2xl mb-2"></i>
                <p class="text-sm">${errorMsg}</p>
                <p class="text-xs mt-1">请检查网络连接</p>
            </div>
        `;
                });
        }

        // 显示成功结果
        function showSuccessResult(data) {
            resultContainer.innerHTML = `
                <div class="text-center text-green-600 mb-4">
                    <i class="fa fa-check-circle text-4xl mb-2"></i>
                    <p class="text-lg font-medium">签到成功</p>
                </div>
                <div class="space-y-3">
                    <div class="flex justify-between items-center">
                        <span class="text-gray-600">姓名:</span>
                        <span class="font-medium">${data.name}</span>
                    </div>
                    <div class="flex justify-between items-center">
                        <span class="text-gray-600">部门:</span>
                        <span class="font-medium">${data.department}</span>
                    </div>
                    <div class="flex justify-between items-center">
                        <span class="text-gray-600">签到时间:</span>
                        <span class="font-medium">${data.checkin_time}</span>
                    </div>
                    <div class="flex justify-between items-center">
                        <span class="text-gray-600">匹配度:</span>
                        <span class="font-medium">${Math.round(data.similarity * 100)}%</span>
                    </div>
                </div>
            `;

            // 刷新签到列表
            fetchCheckinRecords();
        }

        // 显示已签到结果
        function showAlreadyCheckedInResult(data) {
            resultContainer.innerHTML = `
                <div class="text-center text-yellow-600 mb-4">
                    <i class="fa fa-info-circle text-4xl mb-2"></i>
                    <p class="text-lg font-medium">已签到</p>
                </div>
                <div class="space-y-3">
                    <div class="flex justify-between items-center">
                        <span class="text-gray-600">姓名:</span>
                        <span class="font-medium">${data.name}</span>
                    </div>
                    <div class="flex justify-between items-center">
                        <span class="text-gray-600">部门:</span>
                        <span class="font-medium">${data.department}</span>
                    </div>
                    <div class="text-center text-sm text-yellow-500 mt-3">
                        该用户已经完成签到
                    </div>
                </div>
            `;
        }

        // 显示未找到结果
        function showNotFoundResult(debugInfo) {
            let debugHtml = '';
            if (debugInfo) {
                debugHtml = `
                    <div class="mt-3 text-xs text-gray-500">
                        <p>调试信息:</p>
                        <p>最高匹配度: ${(debugInfo.highest_similarity * 100).toFixed(2)}%</p>
                        <p>识别阈值: ${(debugInfo.threshold * 100).toFixed(2)}%</p>
                    </div>
                `;
            }

            resultContainer.innerHTML = `
                <div class="text-center text-red-500">
                    <i class="fa fa-user-times text-4xl mb-3"></i>
                    <p class="text-lg font-medium">未找到匹配人员</p>
                    <p class="text-sm mt-2">请确保您是本次会议的参会人员</p>
                    <p class="text-sm">或联系管理员录入您的人脸信息</p>
                    ${debugHtml}
                </div>
            `;
        }

        // 显示未检测到人脸结果
        function showNoFaceResult() {
            resultContainer.innerHTML = `
                <div class="text-center text-red-500">
                    <i class="fa fa-exclamation-triangle text-4xl mb-3"></i>
                    <p class="text-lg font-medium">未检测到人脸</p>
                    <p class="text-sm mt-2">请确保面部清晰可见</p>
                    <p class="text-sm">并确保面部光线充足、无遮挡</p>
                </div>
            `;
        }

        // 获取签到记录
        function fetchCheckinRecords() {
            fetch(`/meeting/checkin_records/${meetingId}`)
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTP error! Status: ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => {
                    const loadingEl = document.getElementById('checkin-list-loading');
                    const emptyEl = document.getElementById('checkin-list-empty');
                    const listEl = document.getElementById('checkin-list');
                    const bodyEl = document.getElementById('checkin-list-body');

                    // 隐藏加载状态
                    loadingEl.classList.add('hidden');

                    if (data.length === 0) {
                        // 显示空状态
                        emptyEl.classList.remove('hidden');
                        listEl.classList.add('hidden');
                    } else {
                        // 显示列表并填充数据
                        emptyEl.classList.add('hidden');
                        listEl.classList.remove('hidden');

                        // 清空现有内容
                        bodyEl.innerHTML = '';

                        // 按签到时间排序(最新的在前)
                        data.sort((a, b) => new Date(b.checkin_time) - new Date(a.checkin_time));

                        // 添加记录
                        data.forEach(record => {
                            const row = document.createElement('tr');
                            row.innerHTML = `
                                <td>${record.department}</td>
                                <td>${record.name}</td>
                                <td>${formatDateTime(record.checkin_time)}</td>
                            `;
                            bodyEl.appendChild(row);
                        });
                    }
                })
                .catch(error => {
                    console.error('获取签到记录失败:', error);
                    document.getElementById('checkin-list-loading').innerHTML = `
                        <span class="text-red-500">加载失败,请刷新页面重试</span>
                    `;
                });
        }

        // 格式化日期时间
        function formatDateTime(dateTimeStr) {
            const date = new Date(dateTimeStr);
            return date.toLocaleString('zh-CN', {
                year: 'numeric',
                month: '2-digit',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit',
                second: '2-digit'
            }).replace(',', ' ');
        }

        // 页面卸载时清理资源
        window.addEventListener('beforeunload', function () {
            if (recognitionInterval) {
                clearInterval(recognitionInterval);
            }
            if (video.srcObject) {
                video.srcObject.getTracks().forEach(track => track.stop());
            }
        });
    });
</script>
{% endblock %}

对应路由代码:

 

@app.route('/meeting/checkin/<int:meeting_id>', methods=['GET', 'POST'])
def meeting_checkin(meeting_id):
    conn = sqlite3.connect(app.config['DATABASE_FILE'])
    c = conn.cursor()
    
    # 1. 检查会议是否存在
    c.execute('SELECT * FROM meetings WHERE id=?', (meeting_id,))
    meeting = c.fetchone()
    if not meeting:
        conn.close()
        return jsonify({"success": False, "message": "会议不存在"}), 404
    
    # 2. 解析会议日期(仅提取年月日,忽略时间)
    from datetime import datetime
    meeting_start_str = meeting[2].replace('T', ' ')
    meeting_end_str = meeting[3].replace('T', ' ')
    
    # 关键修改:仅提取日期部分(年月日)
    meeting_date = datetime.strptime(meeting_start_str.split(' ')[0], "%Y-%m-%d").date()  # 会议当天日期
    current_date = datetime.now().date()  # 当前日期(仅年月日)
    
    # 3. 定义会议状态(供前端展示)
    if current_date < meeting_date:
        meeting_status = "not_started"  # 未到会议当天
        status_text = f"会议时间为 {meeting_date.strftime('%Y-%m-%d')}[还未到时间不可签到,会议当天可签到]"
    elif current_date > meeting_date:
        meeting_status = "ended"  # 已过会议当天
        status_text = f"会议当天为 {meeting_date.strftime('%Y-%m-%d')}[会议已过期,不可签到]"
    else:
        meeting_status = "ongoing"  # 当天会议
        status_text = f"今天是会议当天({meeting_date.strftime('%Y-%m-%d')})[可正常签到]"
    
    # 4. 处理POST请求(签到操作)时检查日期
    if request.method == 'POST':
        # 仅在会议当天允许签到
        if current_date < meeting_date:
            conn.close()
            return jsonify({
                "success": False, 
                "message": f"未到会议签到日期,仅允许在会议当天签到\n会议日期: {meeting_date.strftime('%Y-%m-%d')}",
                "error_type": "MEETING_NOT_STARTED"
            }), 403
            
        if current_date > meeting_date:
            conn.close()
            return jsonify({
                "success": False, 
                "message": f"会议签到日期已过,禁止签到\n会议日期: {meeting_date.strftime('%Y-%m-%d')}",
                "error_type": "MEETING_ENDED"
            }), 403

    # 初始化参会人员ID列表
    participant_ids = []
    if meeting[5]:  # 检查参会人员字段是否存在
        try:
            participant_ids = [int(id.strip()) for id in meeting[5].split(',') if id.strip()]
        except ValueError:
            # 处理无效ID格式
            participant_ids = []
    
    if request.method == 'POST':
        # 2. 检查文件是否上传
        if 'file' not in request.files:
            conn.close()
            return jsonify({"success": False, "message": "未获取到图像数据"}), 400
        
        file = request.files['file']
        if file.filename == '':
            conn.close()
            return jsonify({"success": False, "message": "请确保摄像头正常工作"}), 400
        
        # 3. 检查文件类型
        allowed_ext = {'png', 'jpg', 'jpeg'}
        file_ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
        if file_ext not in allowed_ext:
            conn.close()
            return jsonify({"success": False, "message": f"不支持的文件类型({file_ext}),仅允许PNG/JPG"}), 400
        
        # 4. 读取并验证图片数据
        try:
            image_data = file.read()
            if len(image_data) < 1024:  # 小于1KB的文件可能损坏
                conn.close()
                return jsonify({"success": False, "message": "图片文件损坏或为空"}), 400
        except Exception as e:
            conn.close()
            return jsonify({"success": False, "message": f"读取图片失败:{str(e)}"}), 400
        
        # 5. 检查SDK是否可用
        if not arcsoft_sdk or not arcsoft_sdk.is_initialized:
            conn.close()
            return jsonify({
                "success": False, 
                "message": "人脸SDK未初始化,无法完成签到",
                "error_type": "SDK_NOT_INITIALIZED"
            }), 500
        
        # 6. 检测人脸
        faces, img, error = detect_faces(image_data)
        if error:
            conn.close()
            return jsonify({
                "success": False, 
                "message": f"人脸检测失败:{error}",
                "error_type": "FACE_DETECTION_FAILED"
            }), 400
        
        if not faces:
            conn.close()
            return jsonify({
                "success": False, 
                "message": "未检测到人脸,请确保图片中有人脸且光线充足",
                "error_type": "NO_FACE_DETECTED"
            }), 400
        
        if len(faces) > 1:
            conn.close()
            return jsonify({
                "success": False, 
                "message": f"检测到{len(faces)}个人脸,请确保画面中只有一个人",
                "error_type": "MULTIPLE_FACES_DETECTED"
            }), 400
        
        # 7. 提取人脸特征
        feature, error = extract_face_feature(image_data)
        if error:
            conn.close()
            return jsonify({
                "success": False, 
                "message": f"提取人脸特征失败:{error}",
                "error_type": "FEATURE_EXTRACTION_FAILED"
            }), 400
        
        # 8. 检查会议参与人员
        if not participant_ids:
            conn.close()
            return jsonify({
                "success": False, 
                "message": "该会议未设置参会人员,无法签到",
                "error_type": "NO_PARTICIPANTS_SET"
            }), 400
        
        # 9. 比对人脸特征
        c.execute('SELECT id, employee_id, name, face_feature FROM employees WHERE id IN ({})'.format(','.join(['?']*len(participant_ids))), participant_ids)
        employees = c.fetchall()
        
        best_match = None
        highest_similarity = 0.0
        for emp in employees:
            emp_id, emp_no, emp_name, emp_feature = emp
            if not emp_feature:
                continue  # 跳过未录入人脸的参会人员
            
            similarity, err = compare_features(feature, emp_feature)
            if err:
                continue  # 比对失败则跳过
            
            if similarity > highest_similarity:
                highest_similarity = similarity
                best_match = (emp_id, emp_no, emp_name)
        
        # 10. 验证比对结果
        threshold = 0.8  # 匹配阈值
        if not best_match or highest_similarity < threshold:
            conn.close()
            return jsonify({
                "success": False, 
                "message": f"未匹配到参会人员(最高相似度:{highest_similarity:.2f},阈值:{threshold})",
                "error_type": "NO_MATCH_FOUND",
                "debug_info": {
                    "highest_similarity": highest_similarity,
                    "threshold": threshold,
                    "participant_count": len(participant_ids)
                }
            }), 400
        
        # 11. 检查是否已签到
        emp_id, emp_no, emp_name = best_match
        c.execute('SELECT id FROM checkin_records WHERE employee_id=? AND meeting_id=?', (emp_id, meeting_id))
        if c.fetchone():
            conn.close()
            return jsonify({
                "success": False, 
                "message": f"参会人员「{emp_name}」已签到,无需重复操作",
                "error_type": "ALREADY_CHECKED_IN",
                "data": {
                    "employee_id": emp_no,
                    "name": emp_name
                }
            }), 400
        
        # 12. 记录签到信息
        checkin_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        c.execute('''INSERT INTO checkin_records 
                     (employee_id, meeting_id, checkin_time, checkin_type, status, similarity) 
                     VALUES (?, ?, ?, ?, ?, ?)''', 
                 (emp_id, meeting_id, checkin_time, "face", "success", highest_similarity))
        conn.commit()
        
        # 13. 返回成功结果
        conn.close()
        return jsonify({
            "success": True,
            "message": "签到成功",
            "data": {
                "employee_id": emp_no,
                "name": emp_name,
                "department": get_department_name(emp_id),
                "checkin_time": checkin_time,
                "similarity": highest_similarity
            }
        }), 200
    
    # GET请求渲染页面
    # 安全处理无参会人员的情况
    if participant_ids:
        query = '''SELECT e.id, e.employee_id, e.name, e.face_image, cr.checkin_time 
                   FROM employees e 
                   LEFT JOIN checkin_records cr ON e.id = cr.employee_id AND cr.meeting_id = ?
                   WHERE e.id IN ({})'''.format(','.join(['?']*len(participant_ids)))
        params = [meeting_id] + participant_ids
    else:
        query = '''SELECT e.id, e.employee_id, e.name, e.face_image, cr.checkin_time 
                   FROM employees e 
                   LEFT JOIN checkin_records cr ON e.id = cr.employee_id AND cr.meeting_id = ?
                   WHERE 1=0'''
        params = [meeting_id]
    
    c.execute(query, params)
    participants = c.fetchall()
    c.execute('SELECT id, name FROM departments')
    departments = {row[0]: row[1] for row in c.fetchall()}
    conn.close()
    
    return render_template('meeting_checkin.html', 
                          meeting=meeting, 
                          participants=participants,
                          departments=departments,
                          meeting_status=meeting_status,  # 传递状态(未开始/当天/已结束)
                          status_text=status_text,  # 状态提示文本
                          meeting_date=meeting_date.strftime('%Y-%m-%d'))  # 会议日期

部门管理界面:

员工管理界面:

人像的上传通过员工管理进行补充,当然也特别写了两个功能,分别是批量导入,方便初始化的时候通过导入人像自动添加员工,可以按照部门批量导入各个部门的人员,当然,照片文件命名时要通过人名+扩展名的方式。

第二个功能就是可以现场拍照提取人脸特征增加员工。

会议管理界面:

除了创建会议,编辑会议和签到,删除之外,还有一个记录功能,记录就是直观的统计和计算参会情况。

       起始整个系统的核心还是人脸识别这个功能,我目前找到可以离线使用的而且识别准确率高,支持活体检测的也就是虹软的这个最好用,不是做广告,也没收好处费,是真好用。有兴趣的可以去虹软看看,免费的100个终端激活名额,我用的是3.0版本的,基于C++的SDK。

先看看虹软SDK部分的调用。

我用的是python+Flask+Arcsoft SDK3.0,看代码了解怎么调用的C++ 64位的dll。

首先是虹软SDK的常量的定义,这个找虹软开发者网站上看帮助和接入说明。

# 虹软SDK常量定义
# 颜色格式
ASVL_PAF_RGB24_B8G8R8 = 513  # RGB分量交织,按B, G, R字节序排布
ASVL_PAF_GRAY = 1793         # 8-bit灰度图
ASVL_PAF_YUYV = 257          # YUYV格式
ASVL_PAF_I420 = 2049         # I420格式
ASVL_PAF_NV21 = 2305         # NV21格式
ASVL_PAF_NV12 = 2049         # NV12格式
ASVL_PAF_DEPTH_U16 = 8193    # 深度图格式
# 检测模式
ASF_DETECT_MODE_VIDEO = 0x00000000  # 视频模式(连续帧检测)
ASF_DETECT_MODE_IMAGE = 0xFFFFFFFF  # 图像模式(静态图检测)
# 人脸检测方向
ASF_OP_0_ONLY = 0x1         # 仅检测0度方向人脸
# 功能掩码
ASF_FACE_DETECT = 0x00000001        # 人脸检测
ASF_FACERECOGNITION = 0x00000004    # 人脸特征提取
# 比对模型
ASF_LIFE_PHOTO = 0x1        # 生活照比对模型(推荐阈值0.80)
# 成功返回值
MOK = 0

# 虹软SDK错误码映射表 - 新增错误码81925的映射
ERROR_CODE_MAPPING = {
    0: "操作成功",  # 新增:0表示成功
    81925: "图像尺寸不合法或格式不兼容(确保宽度为4的倍数,高度为2的倍数)",  # 新增错误码映射
    # 激活相关错误
    0x20000: "激活参数错误",
    0x20001: "SDK已激活(激活状态正常)",
    0x20002: "SDK已激活,无需重复激活",
    0x20003: "部分功能模块已激活",
    0x20004: "离线激活文件已存在",
    0x20005: "激活失败,网络连接错误",
    0x20006: "激活失败,服务器错误",
    0x20007: "激活失败,授权文件不存在",
    0x20008: "激活失败,授权文件已损坏",
    0x20009: "激活失败,授权文件已过期",
    0x2000A: "激活失败,授权文件无效",
    0x2000B: "激活失败,授权文件不匹配",
    0x2000C: "激活失败,授权次数不足",
    0x2000D: "激活失败,设备码不匹配",
    0x2000E: "激活失败,SDK版本不匹配",
    0x2000F: "激活失败,功能不支持",
    
    # 其他错误码
    0x30001: "授权文件不存在",
    0x30002: "授权文件无效",
    0x30003: "授权已过期",
    0x30004: "授权次数不足",
    0x30005: "引擎不匹配",
    0x30006: "内存不足",
    0x30007: "参数错误",
    0x30008: "版本不支持",
    0x30009: "引擎已初始化",
    0x3000A: "引擎未初始化",
    0x3000B: "不支持的操作",
    0x3000C: "功能未授权",
    0x3000D: "系统繁忙",
    0x40001: "图像数据为空",
    0x40002: "图像格式不支持",
    0x40003: "图像尺寸不合法",
    0x40004: "未检测到人脸",
    0x40005: "人脸检测失败",
    0x50001: "人脸信息为空",
    0x50002: "特征提取失败",
    0x50003: "特征数据为空",
    0x50004: "特征数据无效",
    0x60001: "特征1为空",
    0x60002: "特征2为空",
    0x60003: "特征比对失败",
    0x60004: "比对模型不支持",
    0x70001: "无效的句柄",
    0x70002: "内存分配失败",
    0x70003: "参数为空",
    0x70004: "操作失败",
    0x70005: "功能未实现",
    0x70006: "SDK未初始化",
    0x70007: "SDK已释放",
    0x70008: "不支持当前平台",
    0x70009: "时间戳错误",
    0x7000A: "文件操作失败",
    0x7000B: "文件格式错误",
    0x7000C: "设备错误",
    0x7000D: "许可证错误",
    0x7000E: "版本不兼容",
    0x7000F: "功能未授权",
}

虹软SDK封装类

这是人脸识别的核心,包括了SDK的激活、初始化、人脸检测、特征提取和比对等关键功能。

class ArcSoftSDK:
    def __init__(self, app_id, sdk_key):
        """初始化虹软SDK并激活"""
        self.app_id = app_id
        self.sdk_key = sdk_key
        self.engine = None
        self.is_initialized = False
        self.lib_face = None
        self.lib_engine = None
        
        # 记录SDK操作日志
        self._log("开始初始化虹软SDK...")
        
        try:
            # 根据操作系统加载SDK
            self._load_sdk_libraries()
            
            # 获取SDK版本信息
            self._get_sdk_version()
            
            # 检查是否已存在ArcFace64.dat文件
            if os.path.exists("ArcFace64.dat"):
                self._log("检测到ArcFace64.dat文件,跳过激活过程")
            else:
                # 激活SDK
                self._activate_sdk()
            
            # 初始化引擎
            self._init_engine()
            
            self.is_initialized = True
            self._log("虹软SDK初始化成功")
            
        except Exception as e:
            self._log(f"虹软SDK初始化失败: {str(e)}", level="ERROR")
            raise
    
    def _load_sdk_libraries(self):
        """加载SDK动态链接库"""
        self._log("开始加载SDK动态链接库...")
        
        system = platform.system()
        # 检查系统位数(64位系统需要64位SDK)
        if platform.architecture()[0] != "64bit":
            raise Exception("虹软SDK需要64位操作系统支持")
            
        if system == "Windows":
            lib_face = os.path.join("lib", "libarcsoft_face.dll")
            lib_engine = os.path.join("lib", "libarcsoft_face_engine.dll")
        elif system == "Linux":
            lib_face = os.path.join("lib", "libarcsoft_face.so")
            lib_engine = os.path.join("lib", "libarcsoft_face_engine.so")
        elif system == "Darwin":
            lib_face = os.path.join("lib", "libarcsoft_face.dylib")
            lib_engine = os.path.join("lib", "libarcsoft_face_engine.dylib")
        else:
            raise Exception(f"不支持的操作系统: {system}")

        # 检查SDK文件
        for lib in [lib_face, lib_engine]:
            if not os.path.exists(lib):
                raise Exception(f"SDK文件不存在: {lib}")

        # 加载SDK库
        try:
            self.lib_face = ctypes.CDLL(lib_face)
            self.lib_engine = ctypes.CDLL(lib_engine)
            self._log("SDK动态链接库加载成功")
        except Exception as e:
            self._log(f"SDK动态链接库加载失败: {str(e)}", level="ERROR")
            raise
    
    def _get_sdk_version(self):
        """获取SDK版本信息"""
        try:
            # 虹软SDK的版本获取函数需要正确的参数定义
            self.lib_engine.ASFGetVersion.argtypes = [c_void_p]
            self.lib_engine.ASFGetVersion.restype = c_char_p
            
            # 对于未初始化的引擎,可以传入NULL指针尝试获取基础版本
            version_str = self.lib_engine.ASFGetVersion(None)
            if version_str:
                self._log(f"虹软SDK版本: {version_str.decode('utf-8')}")
            else:
                self._log("获取SDK版本信息成功,但返回为空")
        except Exception as e:
            self._log(f"获取SDK版本失败: {str(e)}", level="WARNING")
            # 版本获取失败不影响主功能,仅作为警告
    
    def _activate_sdk(self):
        """激活SDK"""
        self._log("开始SDK激活流程...")
        
        # 定义激活函数
        self.lib_engine.ASFOnlineActivation.argtypes = [c_char_p, c_char_p]
        self.lib_engine.ASFOnlineActivation.restype = c_int

        # 执行激活
        res = self.lib_engine.ASFOnlineActivation(
            self.app_id.encode('utf-8'),
            self.sdk_key.encode('utf-8')
        )
        
        # 处理激活结果
        if res == MOK:
            self._log("SDK激活成功")
        elif res in (0x20001, 0x20002):
            # 已激活状态
            self._log(f"SDK已激活(状态码: {res})")
        else:
            error_msg = ERROR_CODE_MAPPING.get(res, f"未知错误(错误码: {res})")
            # 针对90114等未知错误的特殊处理
            if res == 90114:
                error_msg += ",可能是重复激活或网络问题导致"
            raise Exception(f"SDK激活失败: {error_msg}")
    
    def _init_engine(self):
        """初始化引擎"""
        self._log("开始引擎初始化...")
        
        # 定义结构体
        class MRECT(ctypes.Structure):
            _fields_ = [
                ("left", ctypes.c_int),
                ("top", ctypes.c_int),
                ("right", ctypes.c_int),
                ("bottom", ctypes.c_int)
            ]

        class ASF_SingleFaceInfo(ctypes.Structure):
            _fields_ = [
                ("faceRect", MRECT),
                ("faceOrient", ctypes.c_int)
            ]

        class ASF_MultiFaceInfo(ctypes.Structure):
            _fields_ = [
                ("faceRect", POINTER(MRECT)),
                ("faceOrient", POINTER(ctypes.c_int)),
                ("faceNum", ctypes.c_int),
                ("faceID", POINTER(ctypes.c_int))  # VIDEO模式有效
            ]

        class ASF_FaceFeature(ctypes.Structure):
            _fields_ = [
                ("feature", POINTER(c_byte)),
                ("featureSize", ctypes.c_int)
            ]

        self.MRECT = MRECT
        self.ASF_SingleFaceInfo = ASF_SingleFaceInfo
        self.ASF_MultiFaceInfo = ASF_MultiFaceInfo
        self.ASF_FaceFeature = ASF_FaceFeature

        # 定义初始化函数
        self.lib_engine.ASFInitEngine.argtypes = [
            ctypes.c_int,          # detectMode
            ctypes.c_int,          # detectFaceOrientPriority
            ctypes.c_int,          # detectFaceScaleVal
            ctypes.c_int,          # detectFaceMaxNum
            ctypes.c_int,          # combinedMask
            POINTER(c_void_p)      # hEngine
        ]
        self.lib_engine.ASFInitEngine.restype = ctypes.c_int

        # 初始化参数 - 关键修改:将检测模式改为图像模式
        detect_mode = ASF_DETECT_MODE_IMAGE  # 图像模式(静态图检测)
        orient_priority = ASF_OP_0_ONLY      # 仅检测0度方向人脸
        min_face_scale = 32                  # 最小人脸比例
        max_face_num = 5                     # 最大检测人脸数
        combined_mask = ASF_FACE_DETECT | ASF_FACERECOGNITION  # 需要的功能
        
        self._log(f"初始化参数: 检测模式={detect_mode}, 人脸方向={orient_priority}, 最小人脸比例={min_face_scale}, 最大人脸数={max_face_num}, 功能掩码={combined_mask}")

        # 执行初始化
        self.engine = c_void_p()
        res = self.lib_engine.ASFInitEngine(
            detect_mode,
            orient_priority,
            min_face_scale,
            max_face_num,
            combined_mask,
            byref(self.engine)
        )
        
        # 处理初始化结果
        if res == MOK:
            self._log("引擎初始化成功")
        else:
            error_msg = ERROR_CODE_MAPPING.get(res, f"未知错误(错误码: {res})")
            raise Exception(f"引擎初始化失败: {error_msg}")
    
    def detect_faces(self, image):
        """检测人脸"""
        if not self.is_initialized or not self.engine:
            return None, "虹软SDK未初始化"
        
        try:
            # 转换图像为RGB格式
            image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            height, width = image_rgb.shape[:2]
            
            # 检查图像尺寸是否有效
            if width <= 0 or height <= 0:
                return None, "无效的图像尺寸"
                
            img_data = image_rgb.ctypes.data_as(POINTER(c_uint8))

            # 准备输出参数
            multi_face_info = self.ASF_MultiFaceInfo()

            # 调用检测函数
            self.lib_engine.ASFDetectFaces.argtypes = [
                c_void_p,                  # hEngine
                ctypes.c_int,              # width
                ctypes.c_int,              # height
                ctypes.c_int,              # format
                POINTER(c_uint8),          # imgData
                POINTER(self.ASF_MultiFaceInfo), # detectedFaces
                ctypes.c_int               # detectModel
            ]
            
            # 使用SDK常量定义检测模型
            ASF_DETECT_MODEL_RGB = 0x1
            res = self.lib_engine.ASFDetectFaces(
                self.engine,
                width,
                height,
                ASVL_PAF_RGB24_B8G8R8,  # 颜色格式
                img_data,
                byref(multi_face_info),
                ASF_DETECT_MODEL_RGB
            )

            # 处理返回结果
            if res != MOK:
                error_msg = ERROR_CODE_MAPPING.get(res, f"未知错误(错误码: {res})")
                return None, f"人脸检测失败: {error_msg}"
                
            if multi_face_info.faceNum <= 0:
                return None, "未检测到人脸,请确保面部清晰可见"

            # 提取第一个人脸
            face_rect = multi_face_info.faceRect[0]
            face_info = (
                face_rect.left,
                face_rect.top,
                face_rect.right - face_rect.left,
                face_rect.bottom - face_rect.top
            )
            return face_info, None
        
        except Exception as e:
            return None, f"人脸检测异常: {str(e)}"
    
    def extract_feature(self, image):
        """提取人脸特征"""
        if not self.is_initialized or not self.engine:
            return None, "虹软SDK未初始化"
        
        try:
            # 先检测人脸
            image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
            height, width = image_rgb.shape[:2]
            img_data = image_rgb.ctypes.data_as(POINTER(c_uint8))

            # 准备输出参数
            multi_face_info = self.ASF_MultiFaceInfo()

            # 调用检测函数
            res = self.lib_engine.ASFDetectFaces(
                self.engine,
                width, height,
                ASVL_PAF_RGB24_B8G8R8,
                img_data,
                byref(multi_face_info),
                0x1
            )
            
            if res != MOK:
                error_msg = ERROR_CODE_MAPPING.get(res, f"未知错误(错误码: {res})")
                return None, f"人脸检测失败: {error_msg}"
                
            if multi_face_info.faceNum <= 0:
                return None, "未检测到人脸"

            # 准备单人脸信息
            single_face = self.ASF_SingleFaceInfo()
            single_face.faceRect = multi_face_info.faceRect[0]
            single_face.faceOrient = multi_face_info.faceOrient[0]

            # 提取特征
            self.lib_engine.ASFFaceFeatureExtract.argtypes = [
                c_void_p,                  # hEngine
                ctypes.c_int,              # width
                ctypes.c_int,              # height
                ctypes.c_int,              # format
                POINTER(c_uint8),          # imgData
                POINTER(self.ASF_SingleFaceInfo), # faceInfo
                POINTER(self.ASF_FaceFeature)   # feature
            ]
            
            face_feature = self.ASF_FaceFeature()
            res = self.lib_engine.ASFFaceFeatureExtract(
                self.engine,
                width, height,
                ASVL_PAF_RGB24_B8G8R8,
                img_data,
                byref(single_face),
                byref(face_feature)
            )

            if res != MOK or face_feature.featureSize <= 0:
                error_msg = ERROR_CODE_MAPPING.get(res, f"未知错误(错误码: {res})")
                return None, f"特征提取失败: {error_msg}"

            # 复制特征数据
            feature_data = ctypes.string_at(face_feature.feature, face_feature.featureSize)
            return feature_data, None
        
        except Exception as e:
            return None, f"特征提取异常: {str(e)}"
    
    def compare_features(self, feature1, feature2):
        """比对人脸特征"""
        if not self.is_initialized or not self.engine:
            return 0.0, "虹软SDK未初始化"
        
        try:
            if not feature1 or not feature2:
                return 0.0, "特征数据为空"

            # 构建特征结构体1
            feat1 = self.ASF_FaceFeature()
            feat1.feature = ctypes.cast(ctypes.create_string_buffer(feature1), POINTER(c_byte))
            feat1.featureSize = len(feature1)

            # 构建特征结构体2
            feat2 = self.ASF_FaceFeature()
            feat2.feature = ctypes.cast(ctypes.create_string_buffer(feature2), POINTER(c_byte))
            feat2.featureSize = len(feature2)

            # 比对
            self.lib_engine.ASFFaceFeatureCompare.argtypes = [
                c_void_p,                  # hEngine
                POINTER(self.ASF_FaceFeature),  # feature1
                POINTER(self.ASF_FaceFeature),  # feature2
                POINTER(c_float),          # confidenceLevel
                ctypes.c_int               # compareModel
            ]
            
            confidence = c_float(0.0)
            res = self.lib_engine.ASFFaceFeatureCompare(
                self.engine,
                byref(feat1),
                byref(feat2),
                byref(confidence),
                ASF_LIFE_PHOTO  # 生活照比对模型
            )

            if res != MOK:
                error_msg = ERROR_CODE_MAPPING.get(res, f"未知错误(错误码: {res})")
                return 0.0, f"特征比对失败: {error_msg}"
            
            return confidence.value, None
        
        except Exception as e:
            return 0.0, f"特征比对异常: {str(e)}"
    
    def _log(self, message, level="INFO"):
        """记录SDK操作日志"""
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{timestamp}] [ArcSoftSDK] [{level}] {message}")
    
    def __del__(self):
        """销毁引擎,确保资源释放"""
        if self.engine and self.is_initialized and self.lib_engine:
            self._log("尝试释放引擎资源...")
            try:
                self.lib_engine.ASFUninitEngine.argtypes = [c_void_p]
                self.lib_engine.ASFUninitEngine.restype = c_int
                
                res = self.lib_engine.ASFUninitEngine(self.engine)
                if res == MOK:
                    self._log("引擎已成功释放")
                else:
                    error_msg = ERROR_CODE_MAPPING.get(res, f"未知错误(错误码: {res})")
                    self._log(f"引擎释放失败: {error_msg}", level="ERROR")
            except Exception as e:
                self._log(f"引擎释放异常: {str(e)}", level="ERROR")
            
            self.is_initialized = False

另一个关键,照片(图片)和人脸的相关处理函数。

图片预处理函数 - 确保尺寸合规,因为虹软SDK对图片有明确的尺寸比例要求。

def preprocess_image(image, format_type=ASVL_PAF_RGB24_B8G8R8):
    """
    按照SDK要求预处理图像,确保尺寸符合要求
    
    Args:
        image: 原始图像
        format_type: 图像格式类型
        
    Returns:
        处理后的图像
    """
    height, width = image.shape[:2]
    
    # 1. 确保宽度是4的倍数(虹软SDK严格要求)
    new_width = width - (width % 4)
    if new_width < 4:  # 确保最小宽度不小于4
        new_width = 4
    
    # 2. 根据格式类型处理高度(确保是2的倍数)
    new_height = height
    if format_type in [ASVL_PAF_YUYV, ASVL_PAF_I420, ASVL_PAF_NV21, ASVL_PAF_NV12, ASVL_PAF_RGB24_B8G8R8]:
        # 这些格式要求高度是2的倍数
        new_height = height - (height % 2)
        if new_height < 2:  # 确保最小高度不小于2
            new_height = 2
    
    # 3. 如果尺寸有变化则裁剪图像(从中心裁剪)
    if new_width != width or new_height != height:
        start_x = (width - new_width) // 2
        start_y = (height - new_height) // 2
        image = image[start_y:start_y+new_height, start_x:start_x+new_width]
    
    return image
# 人脸处理函数
def detect_faces(image_data):
    if not arcsoft_sdk or not arcsoft_sdk.is_initialized:
        return [], None, "虹软SDK未初始化"
    try:
        nparr = np.frombuffer(image_data, np.uint8)
        img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
        if img is None:
            return [], None, "图像解码失败"
        
        # 预处理图像
        img = preprocess_image(img)
        
        face_info, err = arcsoft_sdk.detect_faces(img)
        if not face_info:
            return [], img, err
        return [face_info], img, None
    except Exception as e:
        return [], None, f"人脸检测失败: {str(e)}"

def extract_face_feature(image_data):
    if not arcsoft_sdk or not arcsoft_sdk.is_initialized:
        return None, "虹软SDK未初始化"
    try:
        nparr = np.frombuffer(image_data, np.uint8)
        img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
        if img is None:
            return None, "图像解码失败"
        
        # 预处理图像
        img = preprocess_image(img)
        
        feature, err = arcsoft_sdk.extract_feature(img)
        if not feature:
            return None, err
        return feature, None
    except Exception as e:
        return None, f"特征提取失败: {str(e)}"

def compare_features(feature1, feature2):
    if not arcsoft_sdk or not arcsoft_sdk.is_initialized:
        return 0.0, "虹软SDK未初始化"
    try:
        if not feature1 or not feature2:
            return 0.0, "特征数据为空"
        
        similarity, err = arcsoft_sdk.compare_features(feature1, feature2)
        return similarity, err
    except Exception as e:
        return 0.0, f"特征比对失败: {str(e)}"

说说数据库部分,我采用的是本地的sqlite数据库,简单易用。

数据库初始化代码

def init_db():
    db_exists = os.path.exists(app.config['DATABASE_FILE'])
    conn = sqlite3.connect(app.config['DATABASE_FILE'])
    c = conn.cursor()
    
    # 创建部门表
    c.execute('''CREATE TABLE IF NOT EXISTS departments
                 (id INTEGER PRIMARY KEY AUTOINCREMENT,
                 name TEXT NOT NULL,
                 parent_id INTEGER,
                 FOREIGN KEY (parent_id) REFERENCES departments(id))''')
    
    # 创建员工表
    c.execute('''CREATE TABLE IF NOT EXISTS employees
                 (id INTEGER PRIMARY KEY AUTOINCREMENT,
                 employee_id TEXT NOT NULL UNIQUE,
                 name TEXT NOT NULL,
                 department_id INTEGER NOT NULL,
                 position TEXT,
                 face_feature BLOB,
                 face_image TEXT,
                 FOREIGN KEY (department_id) REFERENCES departments(id))''')
    
    # 创建会议表
    c.execute('''CREATE TABLE IF NOT EXISTS meetings
                 (id INTEGER PRIMARY KEY AUTOINCREMENT,
                 name TEXT NOT NULL,
                 start_time DATETIME NOT NULL,
                 end_time DATETIME NOT NULL,
                 location TEXT,
                 participant_employees TEXT)''')
    
    # 创建签到记录表
    c.execute('''CREATE TABLE IF NOT EXISTS checkin_records
                 (id INTEGER PRIMARY KEY AUTOINCREMENT,
                 employee_id INTEGER NOT NULL,
                 meeting_id INTEGER NOT NULL,
                 checkin_time DATETIME DEFAULT CURRENT_TIMESTAMP,
                 checkin_type TEXT,
                 status TEXT,
                 similarity REAL,
                 FOREIGN KEY (employee_id) REFERENCES employees(id),
                 FOREIGN KEY (meeting_id) REFERENCES meetings(id))''')
    
    # 兼容旧数据库
    try:
        c.execute('ALTER TABLE meetings ADD COLUMN participant_employees TEXT')
    except sqlite3.OperationalError:
        pass
    
    conn.commit()
    conn.close()
    if not db_exists:
        print(f"数据库文件 {app.config['DATABASE_FILE']} 已创建并初始化")

最后还有一点就是虹软SDK初始化app_id和sdk_key

# 初始化虹软SDK
    try:
        APP_ID = "你自己申请的id"
        SDK_KEY = "你下载的对应key"
        
        print(f"尝试初始化虹软SDK - APP_ID: {APP_ID[:5]}...{APP_ID[-5:]}, SDK_KEY: {SDK_KEY[:5]}...{SDK_KEY[-5:]}")
        arcsoft_sdk = ArcSoftSDK(APP_ID, SDK_KEY)
        print("虹软SDK初始化成功")
    except Exception as e:
        print(f"虹软SDK初始化失败: {str(e)}")
        arcsoft_sdk = None

程序目录结构如下:

用pyinstaller封装了一下。

大家觉得这个系统怎么样?欢迎讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半熟的皮皮虾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值