feat: 人员识别与管理模块 - MediaPipe人脸检测、人脸识别、人员库管理

This commit is contained in:
2026-04-16 18:47:15 +08:00
parent 32f98ce4f3
commit 55c6ad88c8
7 changed files with 769 additions and 45 deletions
+29
View File
@@ -165,6 +165,35 @@ async def analyze_unanalyzed():
return {"results": results}
@app.get("/api/persons")
async def get_persons():
"""获取人员列表"""
from person_manager import person_manager
return {"persons": person_manager.get_persons_list(), "stats": person_manager.get_stats()}
@app.delete("/api/persons/{person_id}")
async def delete_person(person_id: str):
"""删除人员"""
from person_manager import person_manager
if person_id in person_manager.persons:
del person_manager.persons[person_id]
person_manager._save_persons_db()
return {"success": True}
raise HTTPException(status_code=404, detail="人员不存在")
@app.post("/api/persons/{person_id}/rename")
async def rename_person(person_id: str, name: str):
"""重命名人员"""
from person_manager import person_manager
if person_id in person_manager.persons:
person_manager.persons[person_id]['name'] = name
person_manager._save_persons_db()
return {"success": True, "name": name}
raise HTTPException(status_code=404, detail="人员不存在")
# ============== 图片 API ==============
@app.get("/api/images")
+90
View File
@@ -405,6 +405,96 @@ function closeSettingsModal() {
document.getElementById('settings-modal').classList.remove('active');
}
// Persons Management
function openPersonsModal() {
loadPersonsList();
document.getElementById('persons-modal').classList.add('active');
}
function closePersonsModal() {
document.getElementById('persons-modal').classList.remove('active');
}
function loadPersonsList() {
fetch(API_BASE + '/api/persons')
.then(function(res) { return res.json(); })
.then(function(data) {
var statsDiv = document.getElementById('persons-stats');
var listDiv = document.getElementById('persons-list');
// 统计信息
statsDiv.innerHTML = '<div class="stats-summary">' +
'<span>Total: ' + data.stats.total_persons + '</span>' +
'<span>Detected: ' + data.stats.total_detections + '</span>' +
'<span>Known: ' + data.stats.known_persons_detected + '</span>' +
'<span>New: ' + data.stats.new_persons_added + '</span>' +
'</div>';
// 人员列表
if (data.persons.length === 0) {
listDiv.innerHTML = '<p style="color:#888;text-align:center;">No persons recorded yet</p>';
return;
}
listDiv.innerHTML = '';
data.persons.forEach(function(person) {
var item = document.createElement('div');
item.className = 'person-item';
var firstSeen = new Date(person.first_seen).toLocaleDateString();
var lastSeen = new Date(person.last_seen).toLocaleDateString();
item.innerHTML = '<div class="person-info">' +
'<span class="person-name">' + person.name + '</span>' +
'<span class="person-id">' + person.person_id + '</span>' +
'</div>' +
'<div class="person-stats">' +
'<span>Visits: ' + person.visit_count + '</span>' +
'<span>First: ' + firstSeen + '</span>' +
'<span>Last: ' + lastSeen + '</span>' +
'</div>' +
'<div class="person-actions">' +
'<button onclick="renamePerson(\'' + person.person_id + '\')" class="btn-small">Rename</button>' +
'<button onclick="deletePerson(\'' + person.person_id + '\')" class="btn-small btn-danger">Delete</button>' +
'</div>';
listDiv.appendChild(item);
});
})
.catch(function(e) { console.error('Load persons failed:', e); });
}
function renamePerson(personId) {
var newName = prompt('Enter new name:');
if (!newName) return;
fetch(API_BASE + '/api/persons/' + personId + '/rename?name=' + encodeURIComponent(newName), {
method: 'POST'
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Renamed to ' + newName, 1500);
loadPersonsList();
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function deletePerson(personId) {
if (!confirm('Delete this person?')) return;
fetch(API_BASE + '/api/persons/' + personId, {method: 'DELETE'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Person deleted', 1500);
loadPersonsList();
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function loadSettingsForm() {
fetch(API_BASE + '/api/config')
.then(function(res) { return res.json(); })
+21
View File
@@ -29,6 +29,11 @@
</div>
</div>
<div class="panel-section">
<h3>👥 人员库</h3>
<button onclick="openPersonsModal()" class="btn-settings">查看人员库</button>
</div>
<div class="panel-section">
<h3>⚙️ 系统设置</h3>
<button onclick="openSettingsModal()" class="btn-settings">🔧 设置参数</button>
@@ -155,6 +160,22 @@
</div>
</div>
<!-- 人员库模态框 -->
<div class="modal" id="persons-modal" onclick="closeModalOnBackground(event, 'persons-modal')">
<div class="modal-content settings-modal-content" onclick="event.stopPropagation()">
<span class="modal-close" onclick="closePersonsModal()">×</span>
<h2>👥 人员库管理</h2>
<div class="persons-stats" id="persons-stats">
<!-- 统计信息 -->
</div>
<div class="persons-list" id="persons-list">
<!-- 人员列表 -->
</div>
</div>
</div>
<!-- 图片详情模态框 -->
<div class="modal" id="image-modal" onclick="closeModalOnBackground(event, 'image-modal')">
<div class="modal-content" onclick="event.stopPropagation()">
+67
View File
@@ -582,6 +582,73 @@ button {
}
/* 响应式 */
.settings-actions {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: center;
}
/* 人员库样式 */
.persons-stats {
margin-bottom: 20px;
}
.stats-summary {
display: flex;
gap: 20px;
justify-content: center;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.stats-summary span {
color: #555;
}
.persons-list {
max-height: 400px;
overflow-y: auto;
}
.person-item {
background: #f9f9f9;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid #2196F3;
}
.person-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.person-name {
font-weight: bold;
font-size: 16px;
}
.person-id {
color: #888;
font-size: 12px;
}
.person-stats {
display: flex;
gap: 15px;
color: #555;
font-size: 13px;
margin-bottom: 10px;
}
.person-actions {
display: flex;
gap: 10px;
}
@media (max-width: 768px) {
.control-panel {
flex-direction: column;