update logic
This commit is contained in:
parent
19202f9656
commit
5685810508
89
main.py
89
main.py
|
|
@ -8,7 +8,7 @@ import datetime
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from database import SessionLocal, engine
|
from database import SessionLocal, engine
|
||||||
from models import Base, Student, CheckInLog
|
from models import Base, Student, CheckInLog, StudentEncoding
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
|
@ -39,9 +39,6 @@ async def register_face(name: str = Form(...),email: str = Form(...),file: Uploa
|
||||||
text("SELECT id FROM students WHERE email = :email"),
|
text("SELECT id FROM students WHERE email = :email"),
|
||||||
{"email": email}
|
{"email": email}
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if existing:
|
|
||||||
db.close()
|
|
||||||
raise HTTPException(status_code=400, detail="Email đã được đăng ký.")
|
|
||||||
|
|
||||||
# Save image
|
# Save image
|
||||||
image_data = await file.read()
|
image_data = await file.read()
|
||||||
|
|
@ -60,9 +57,31 @@ async def register_face(name: str = Form(...),email: str = Form(...),file: Uploa
|
||||||
encoding_bytes = encodings[0].tobytes()
|
encoding_bytes = encodings[0].tobytes()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if existing:
|
||||||
|
# Email exists, just add new encoding
|
||||||
|
student_id = existing[0]
|
||||||
db.execute(
|
db.execute(
|
||||||
text("INSERT INTO students (name, email, encoding) VALUES (:name, :email, :encoding)"),
|
text("INSERT INTO student_encodings (student_id, encoding) VALUES (:student_id, :encoding)"),
|
||||||
{"name": name, "email": email, "encoding": encoding_bytes}
|
{"student_id": student_id, "encoding": encoding_bytes}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return {"message": "Đã thêm encoding mới cho sinh viên."}
|
||||||
|
else:
|
||||||
|
# Email doesn't exist, create new student
|
||||||
|
db.execute(
|
||||||
|
text("INSERT INTO students (name, email) VALUES (:name, :email)"),
|
||||||
|
{"name": name, "email": email}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Get the last inserted id
|
||||||
|
result = db.execute(text("SELECT LAST_INSERT_ID()")).fetchone()
|
||||||
|
student_id = result[0]
|
||||||
|
|
||||||
|
# Insert encoding
|
||||||
|
db.execute(
|
||||||
|
text("INSERT INTO student_encodings (student_id, encoding) VALUES (:student_id, :encoding)"),
|
||||||
|
{"student_id": student_id, "encoding": encoding_bytes}
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Đăng ký thành công."}
|
return {"message": "Đăng ký thành công."}
|
||||||
|
|
@ -87,34 +106,66 @@ async def checkin(file: UploadFile = File(...), camera_id: str = Form("cam1"), d
|
||||||
|
|
||||||
unknown_encoding = unknown_encodings[0]
|
unknown_encoding = unknown_encodings[0]
|
||||||
|
|
||||||
students = db.query(Student).all()
|
# Get all encodings with student info
|
||||||
for student in students:
|
encodings = db.execute(
|
||||||
known_encoding = np.frombuffer(student.encoding)
|
text("""
|
||||||
|
SELECT s.id, s.name, se.encoding
|
||||||
|
FROM student_encodings se
|
||||||
|
JOIN students s ON s.id = se.student_id
|
||||||
|
""")
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for encoding in encodings:
|
||||||
|
known_encoding = np.frombuffer(encoding.encoding)
|
||||||
result = face_recognition.compare_faces([known_encoding], unknown_encoding, tolerance=0.5)
|
result = face_recognition.compare_faces([known_encoding], unknown_encoding, tolerance=0.5)
|
||||||
if result[0]:
|
if result[0]:
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
recent_check = db.query(CheckInLog).filter(
|
recent_check = db.execute(
|
||||||
CheckInLog.student_id == student.id,
|
text("""
|
||||||
CheckInLog.time > now - datetime.timedelta(minutes=5)
|
SELECT id FROM checkin_logs
|
||||||
).first()
|
WHERE student_id = :student_id
|
||||||
|
AND time > :time_threshold
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"student_id": encoding.id,
|
||||||
|
"time_threshold": now - datetime.timedelta(minutes=5)
|
||||||
|
}
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
if recent_check:
|
if recent_check:
|
||||||
return {"message": f"{student.name} already checked in recently."}
|
return {"message": f"{encoding.name} already checked in recently."}
|
||||||
|
|
||||||
log = CheckInLog(student_id=student.id, time=now, camera_id=camera_id)
|
db.execute(
|
||||||
db.add(log)
|
text("""
|
||||||
|
INSERT INTO checkin_logs (student_id, time, camera_id)
|
||||||
|
VALUES (:student_id, :time, :camera_id)
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"student_id": encoding.id,
|
||||||
|
"time": now,
|
||||||
|
"camera_id": camera_id
|
||||||
|
}
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": f"Check-in successful for {student.name}"}
|
return {"message": f"Check-in successful for {encoding.name}"}
|
||||||
|
|
||||||
return {"message": "No match found."}
|
return {"message": "No match found."}
|
||||||
|
|
||||||
@app.get("/logs")
|
@app.get("/logs")
|
||||||
def get_logs(db: Session = Depends(get_db)):
|
def get_logs(db: Session = Depends(get_db)):
|
||||||
logs = db.query(CheckInLog).all()
|
logs = db.execute(
|
||||||
|
text("""
|
||||||
|
SELECT s.name, cl.time, cl.camera_id
|
||||||
|
FROM checkin_logs cl
|
||||||
|
JOIN students s ON cl.student_id = s.id
|
||||||
|
ORDER BY cl.time DESC
|
||||||
|
""")
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for log in logs:
|
for log in logs:
|
||||||
result.append({
|
result.append({
|
||||||
"name": log.student.name,
|
"name": log.name,
|
||||||
"time": log.time.strftime("%Y-%m-%d %H:%M:%S"),
|
"time": log.time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"camera_id": log.camera_id
|
"camera_id": log.camera_id
|
||||||
})
|
})
|
||||||
|
|
|
||||||
14
models.py
14
models.py
|
|
@ -6,16 +6,24 @@ import datetime
|
||||||
class Student(Base):
|
class Student(Base):
|
||||||
__tablename__ = "students"
|
__tablename__ = "students"
|
||||||
|
|
||||||
__tablename__ = "students"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String(100), nullable=False)
|
name = Column(String(100), nullable=False)
|
||||||
email = Column(String(100), nullable=False, unique=True, index=True)
|
email = Column(String(100), nullable=False, unique=True, index=True)
|
||||||
encoding = Column(LargeBinary, nullable=False)
|
|
||||||
|
|
||||||
__table_args__ = (UniqueConstraint('email', name='uq_student_email'),)
|
__table_args__ = (UniqueConstraint('email', name='uq_student_email'),)
|
||||||
|
|
||||||
checkins = relationship("CheckInLog", back_populates="student")
|
checkins = relationship("CheckInLog", back_populates="student")
|
||||||
|
encodings = relationship("StudentEncoding", back_populates="student")
|
||||||
|
|
||||||
|
class StudentEncoding(Base):
|
||||||
|
__tablename__ = "student_encodings"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
student_id = Column(Integer, ForeignKey("students.id"))
|
||||||
|
encoding = Column(LargeBinary, nullable=False)
|
||||||
|
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
student = relationship("Student", back_populates="encodings")
|
||||||
|
|
||||||
class CheckInLog(Base):
|
class CheckInLog(Base):
|
||||||
__tablename__ = "checkin_logs"
|
__tablename__ = "checkin_logs"
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
video {
|
video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 1200px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -86,6 +86,19 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auto-checkin {
|
||||||
|
background-color: #f39c12;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auto-checkin:hover {
|
||||||
|
background-color: #d35400;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auto-checkin.active {
|
||||||
|
background-color: #e74c3c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-container {
|
.logs-container {
|
||||||
|
|
@ -136,14 +149,16 @@
|
||||||
.main-layout {
|
.main-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-panel {
|
.left-panel {
|
||||||
flex: 1;
|
flex: 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-panel {
|
.right-panel {
|
||||||
flex: 1;
|
flex: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
@ -176,7 +191,6 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background-color: #f87171; /* red-400 */
|
|
||||||
color: white;
|
color: white;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
@ -187,6 +201,14 @@
|
||||||
animation: slideIn 0.3s ease-out;
|
animation: slideIn 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: #34d399; /* green-400 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: #f87171; /* red-400 */
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -204,7 +226,10 @@
|
||||||
<div class="left-panel">
|
<div class="left-panel">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>📸 Face Camera</h2>
|
<h2>📸 Face Camera</h2>
|
||||||
<video id="video" autoplay></video>
|
<div class="video-container" style="position: relative; display: flex; justify-content: center; align-items: center;">
|
||||||
|
<video id="video" autoplay style="width: 100%;"></video>
|
||||||
|
<img src="http://127.0.0.1:8000/static/face-removebg-preview.png" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 60%; height: auto; pointer-events: none; opacity: 0.5;" alt="Hướng dẫn nhận diện khuôn mặt">
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -217,6 +242,7 @@
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button id="register">📥 Đăng ký khuôn mặt</button>
|
<button id="register">📥 Đăng ký khuôn mặt</button>
|
||||||
<button id="checkin">✅ Điểm danh</button>
|
<button id="checkin">✅ Điểm danh</button>
|
||||||
|
<button id="auto-checkin">🔄 Tự động điểm danh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="shortcut-hint">
|
<div class="shortcut-hint">
|
||||||
Nhấn phím Space để điểm danh nhanh
|
Nhấn phím Space để điểm danh nhanh
|
||||||
|
|
@ -246,7 +272,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<canvas id="canvas" width="400" height="300" style="display:none;"></canvas>
|
<canvas id="canvas" width="1200" height="900" style="display:none;"></canvas>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const video = document.getElementById('video');
|
const video = document.getElementById('video');
|
||||||
|
|
@ -262,9 +288,9 @@
|
||||||
showAlert("Không mở được camera: " + err);
|
showAlert("Không mở được camera: " + err);
|
||||||
});
|
});
|
||||||
|
|
||||||
function showAlert(text) {
|
function showAlert(text, status = 'error') {
|
||||||
const alertDiv = document.createElement('div');
|
const alertDiv = document.createElement('div');
|
||||||
alertDiv.className = 'custom-alert';
|
alertDiv.className = `custom-alert alert-${status}`;
|
||||||
alertDiv.innerText = text;
|
alertDiv.innerText = text;
|
||||||
document.body.appendChild(alertDiv);
|
document.body.appendChild(alertDiv);
|
||||||
|
|
||||||
|
|
@ -279,7 +305,7 @@
|
||||||
|
|
||||||
// Hàm chụp ảnh từ video và gửi đến API
|
// Hàm chụp ảnh từ video và gửi đến API
|
||||||
function sendImage(url, extraData = {}) {
|
function sendImage(url, extraData = {}) {
|
||||||
context.drawImage(video, 0, 0, 400, 300);
|
context.drawImage(video, 0, 0, 1200, 900);
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", blob, "frame.jpg");
|
formData.append("file", blob, "frame.jpg");
|
||||||
|
|
@ -294,12 +320,13 @@
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
showAlert(data.message || JSON.stringify(data));
|
const status = data.message && data.message.includes("successful") ? 'success' : 'error';
|
||||||
|
showAlert(data.message || JSON.stringify(data), status);
|
||||||
if (url === "/checkin" && data.message && data.message.includes("successful")) {
|
if (url === "/checkin" && data.message && data.message.includes("successful")) {
|
||||||
loadLogs(); // Tải lại logs sau khi check-in thành công
|
loadLogs(); // Tải lại logs sau khi check-in thành công
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => showAlert("Lỗi gửi ảnh: " + err));
|
.catch(err => showAlert("Lỗi gửi ảnh: " + err, 'error'));
|
||||||
}, "image/jpeg");
|
}, "image/jpeg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,19 +334,52 @@
|
||||||
document.getElementById('register').addEventListener('click', () => {
|
document.getElementById('register').addEventListener('click', () => {
|
||||||
const name = document.getElementById('name').value.trim();
|
const name = document.getElementById('name').value.trim();
|
||||||
const email = document.getElementById('email').value.trim();
|
const email = document.getElementById('email').value.trim();
|
||||||
if (!name || !email) return showAlert("Vui lòng nhập cả tên và email.");
|
if (!name || !email) {
|
||||||
|
showAlert("Vui lòng nhập cả tên và email.", 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
sendImage("/register", { name, email });
|
sendImage("/register", { name, email });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let autoCheckinInterval = null;
|
||||||
|
const autoCheckinButton = document.getElementById('auto-checkin');
|
||||||
|
|
||||||
|
// Hàm bật/tắt tự động điểm danh
|
||||||
|
function toggleAutoCheckin() {
|
||||||
|
if (autoCheckinInterval) {
|
||||||
|
// Tắt tự động
|
||||||
|
clearInterval(autoCheckinInterval);
|
||||||
|
autoCheckinInterval = null;
|
||||||
|
autoCheckinButton.classList.remove('active');
|
||||||
|
autoCheckinButton.textContent = '🔄 Tự động điểm danh';
|
||||||
|
} else {
|
||||||
|
// Bật tự động
|
||||||
|
autoCheckinInterval = setInterval(() => {
|
||||||
|
sendImage("/checkin", { camera_id: "webcam" });
|
||||||
|
}, 1000); // Gửi mỗi giây
|
||||||
|
autoCheckinButton.classList.add('active');
|
||||||
|
autoCheckinButton.textContent = '⏹️ Dừng tự động';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sự kiện: Bật/tắt tự động điểm danh
|
||||||
|
autoCheckinButton.addEventListener('click', toggleAutoCheckin);
|
||||||
|
|
||||||
// Sự kiện: Check-in
|
// Sự kiện: Check-in
|
||||||
document.getElementById('checkin').addEventListener('click', () => {
|
document.getElementById('checkin').addEventListener('click', () => {
|
||||||
|
if (autoCheckinInterval) {
|
||||||
|
toggleAutoCheckin(); // Tắt tự động nếu đang bật
|
||||||
|
}
|
||||||
sendImage("/checkin", { camera_id: "webcam" });
|
sendImage("/checkin", { camera_id: "webcam" });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sự kiện: Nhấn phím Space để điểm danh
|
// Sự kiện: Nhấn phím Space để điểm danh
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
if (event.code === 'Space' || event.keyCode === 32) {
|
if (event.code === 'Space' || event.keyCode === 32) {
|
||||||
event.preventDefault(); // Ngăn chặn hành vi mặc định của phím Space
|
event.preventDefault();
|
||||||
|
if (autoCheckinInterval) {
|
||||||
|
toggleAutoCheckin(); // Tắt tự động nếu đang bật
|
||||||
|
}
|
||||||
sendImage("/checkin", { camera_id: "webcam" });
|
sendImage("/checkin", { camera_id: "webcam" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
Loading…
Reference in New Issue