Compare commits
2 Commits
e75425ac14
...
v1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e3308db12 | |||
| 69b57b1904 |
BIN
__pycache__/analyzer.cpython-310.pyc
Normal file
BIN
__pycache__/analyzer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/camera.cpython-310.pyc
Normal file
BIN
__pycache__/camera.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-310.pyc
Normal file
BIN
__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/database.cpython-310.pyc
Normal file
BIN
__pycache__/database.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/local_analyzer.cpython-310.pyc
Normal file
BIN
__pycache__/local_analyzer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/person_manager.cpython-310.pyc
Normal file
BIN
__pycache__/person_manager.cpython-310.pyc
Normal file
Binary file not shown.
BIN
__pycache__/scheduler.cpython-310.pyc
Normal file
BIN
__pycache__/scheduler.cpython-310.pyc
Normal file
Binary file not shown.
BIN
data/events.db
Normal file
BIN
data/events.db
Normal file
Binary file not shown.
@@ -537,7 +537,7 @@ class PersonManager:
|
|||||||
if save_new_person and confirmed_change:
|
if save_new_person and confirmed_change:
|
||||||
for person in identified_persons:
|
for person in identified_persons:
|
||||||
if person['is_new'] and len(self.persons) < self.config['max_persons']:
|
if person['is_new'] and len(self.persons) < self.config['max_persons']:
|
||||||
# 保存人脸特征
|
# 保存人脸特征和人脸图片
|
||||||
x, y, w, h = person['bbox']
|
x, y, w, h = person['bbox']
|
||||||
face_region = image[y:y+int(h*0.4), x:x+w]
|
face_region = image[y:y+int(h*0.4), x:x+w]
|
||||||
|
|
||||||
@@ -548,8 +548,8 @@ class PersonManager:
|
|||||||
person['person_id'] = person_id
|
person['person_id'] = person_id
|
||||||
person['name'] = f"Person #{len(self.persons) + 1}"
|
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]}
|
self.confirmation_buffer = {key: self.confirmation_buffer[key]}
|
||||||
@@ -580,13 +580,14 @@ class PersonManager:
|
|||||||
'detection_source': 'yolo'
|
'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)
|
"""保存新人员到库(已有 encoding)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
person_id: 人员ID
|
person_id: 人员ID
|
||||||
encoding: 特征向量
|
encoding: 特征向量
|
||||||
name: 名称
|
name: 名称
|
||||||
|
face_image: 人脸图片(可选,用于保存人脸图片)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: 人员信息
|
dict: 人员信息
|
||||||
@@ -594,10 +595,18 @@ class PersonManager:
|
|||||||
if name is None:
|
if name is None:
|
||||||
name = person_id
|
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_data = {
|
||||||
'person_id': person_id,
|
'person_id': person_id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'face_encoding': encoding.tolist() if isinstance(encoding, np.ndarray) else encoding,
|
'face_encoding': encoding.tolist() if isinstance(encoding, np.ndarray) else encoding,
|
||||||
|
'face_path': face_path,
|
||||||
'first_seen': datetime.datetime.now().isoformat(),
|
'first_seen': datetime.datetime.now().isoformat(),
|
||||||
'last_seen': datetime.datetime.now().isoformat(),
|
'last_seen': datetime.datetime.now().isoformat(),
|
||||||
'visit_count': 1
|
'visit_count': 1
|
||||||
@@ -620,6 +629,7 @@ class PersonManager:
|
|||||||
'visit_count': p['visit_count'],
|
'visit_count': p['visit_count'],
|
||||||
'first_seen': p['first_seen'], # 已经精确到秒
|
'first_seen': p['first_seen'], # 已经精确到秒
|
||||||
'last_seen': p['last_seen'], # 已经精确到秒
|
'last_seen': p['last_seen'], # 已经精确到秒
|
||||||
|
'face_path': p.get('face_path', ''), # 人脸图片路径
|
||||||
}
|
}
|
||||||
for p in self.persons.values()
|
for p in self.persons.values()
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
web/__pycache__/app.cpython-310.pyc
Normal file
BIN
web/__pycache__/app.cpython-310.pyc
Normal file
Binary file not shown.
19
web/app.py
19
web/app.py
@@ -195,6 +195,25 @@ async def rename_person(person_id: str, name: str):
|
|||||||
raise HTTPException(status_code=404, detail="人员不存在")
|
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")
|
@app.get("/api/stats/daily")
|
||||||
async def get_daily_stats(date: str = None):
|
async def get_daily_stats(date: str = None):
|
||||||
"""获取每日统计数据"""
|
"""获取每日统计数据"""
|
||||||
|
|||||||
@@ -630,7 +630,15 @@ function loadPersonsList() {
|
|||||||
var firstSeen = new Date(person.first_seen).toLocaleString();
|
var firstSeen = new Date(person.first_seen).toLocaleString();
|
||||||
var lastSeen = new Date(person.last_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="person-face-placeholder">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-name">' + person.name + '</span>' +
|
||||||
'<span class="person-id">' + person.person_id + '</span>' +
|
'<span class="person-id">' + person.person_id + '</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
@@ -642,6 +650,7 @@ function loadPersonsList() {
|
|||||||
'<div class="person-actions">' +
|
'<div class="person-actions">' +
|
||||||
'<button onclick="renamePerson(\'' + person.person_id + '\')" class="btn-small">Rename</button>' +
|
'<button onclick="renamePerson(\'' + person.person_id + '\')" class="btn-small">Rename</button>' +
|
||||||
'<button onclick="deletePerson(\'' + person.person_id + '\')" class="btn-small btn-danger">Delete</button>' +
|
'<button onclick="deletePerson(\'' + person.person_id + '\')" class="btn-small btn-danger">Delete</button>' +
|
||||||
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
listDiv.appendChild(item);
|
listDiv.appendChild(item);
|
||||||
|
|||||||
@@ -751,6 +751,38 @@ button {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border-left: 4px solid #2196F3;
|
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 {
|
.person-info {
|
||||||
@@ -771,6 +803,7 @@ button {
|
|||||||
|
|
||||||
.person-stats {
|
.person-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user