2 Commits

Author SHA1 Message Date
9e3308db12 fix: 修复人员库保存人脸图片路径缺失问题 2026-04-20 12:25:41 +08:00
69b57b1904 feat: 人员库弹框显示人员图片 2026-04-19 16:43:25 +08:00
13 changed files with 76 additions and 5 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/events.db Normal file

Binary file not shown.

View File

@@ -537,7 +537,7 @@ class PersonManager:
if save_new_person and confirmed_change:
for person in identified_persons:
if person['is_new'] and len(self.persons) < self.config['max_persons']:
# 保存人脸特征
# 保存人脸特征和人脸图片
x, y, w, h = person['bbox']
face_region = image[y:y+int(h*0.4), x:x+w]
@@ -548,8 +548,8 @@ class PersonManager:
person['person_id'] = person_id
person['name'] = f"Person #{len(self.persons) + 1}"
# 保存到人员库
self.add_new_person_with_encoding(person_id, encoding, person['name'])
# 保存到人员库(包括人脸图片)
self.add_new_person_with_encoding(person_id, encoding, person['name'], face_region)
# 清空缓冲区,更新状态
self.confirmation_buffer = {key: self.confirmation_buffer[key]}
@@ -580,13 +580,14 @@ class PersonManager:
'detection_source': 'yolo'
}
def add_new_person_with_encoding(self, person_id, encoding, name=None):
def add_new_person_with_encoding(self, person_id, encoding, name=None, face_image=None):
"""保存新人员到库(已有 encoding
Args:
person_id: 人员ID
encoding: 特征向量
name: 名称
face_image: 人脸图片(可选,用于保存人脸图片)
Returns:
dict: 人员信息
@@ -594,10 +595,18 @@ class PersonManager:
if name is None:
name = person_id
# 保存人脸图片
face_path = ''
if face_image is not None and face_image.size > 0:
face_path = str(self.faces_dir / f"{person_id}.jpg")
cv2.imwrite(face_path, face_image)
print(f"[PersonManager] Face image saved: {face_path}")
person_data = {
'person_id': person_id,
'name': name,
'face_encoding': encoding.tolist() if isinstance(encoding, np.ndarray) else encoding,
'face_path': face_path,
'first_seen': datetime.datetime.now().isoformat(),
'last_seen': datetime.datetime.now().isoformat(),
'visit_count': 1
@@ -620,6 +629,7 @@ class PersonManager:
'visit_count': p['visit_count'],
'first_seen': p['first_seen'], # 已经精确到秒
'last_seen': p['last_seen'], # 已经精确到秒
'face_path': p.get('face_path', ''), # 人脸图片路径
}
for p in self.persons.values()
]

Binary file not shown.

View File

@@ -195,6 +195,25 @@ async def rename_person(person_id: str, name: str):
raise HTTPException(status_code=404, detail="人员不存在")
@app.get("/api/persons/{person_id}/face")
async def get_person_face(person_id: str):
"""获取人员人脸图片"""
from person_manager import person_manager
from pathlib import Path
if person_id in person_manager.persons:
face_path = person_manager.persons[person_id].get('face_path', '')
if face_path and Path(face_path).exists():
return FileResponse(face_path)
# 检查 faces_dir 中的默认图片
face_file = person_manager.faces_dir / f"{person_id}.jpg"
if face_file.exists():
return FileResponse(str(face_file))
raise HTTPException(status_code=404, detail="人员图片不存在")
@app.get("/api/stats/daily")
async def get_daily_stats(date: str = None):
"""获取每日统计数据"""

View File

@@ -630,7 +630,15 @@ function loadPersonsList() {
var firstSeen = new Date(person.first_seen).toLocaleString();
var lastSeen = new Date(person.last_seen).toLocaleString();
item.innerHTML = '<div class="person-info">' +
// 构建人员图片URL
var faceImgHtml = '<div class="person-face-placeholder">No Image</div>';
if (person.face_path) {
faceImgHtml = '<img class="person-face-img" src="/api/persons/' + person.person_id + '/face" alt="' + person.name + '" onerror="this.parentElement.innerHTML=\'<div class=&quot;person-face-placeholder&quot;>No Image</div>\'">';
}
item.innerHTML = '<div class="person-face">' + faceImgHtml + '</div>' +
'<div class="person-content">' +
'<div class="person-info">' +
'<span class="person-name">' + person.name + '</span>' +
'<span class="person-id">' + person.person_id + '</span>' +
'</div>' +
@@ -642,6 +650,7 @@ function loadPersonsList() {
'<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>' +
'</div>';
listDiv.appendChild(item);

View File

@@ -751,6 +751,38 @@ button {
margin-bottom: 8px;
border-radius: 8px;
border-left: 4px solid #2196F3;
display: flex;
gap: 12px;
align-items: flex-start;
}
.person-face {
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
background: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.person-face-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.person-face-placeholder {
color: #888;
font-size: 12px;
text-align: center;
}
.person-content {
flex: 1;
min-width: 0;
}
.person-info {
@@ -771,6 +803,7 @@ button {
.person-stats {
display: flex;
flex-wrap: wrap;
gap: 10px;
color: #555;
font-size: 12px;