update logic

This commit is contained in:
Joseph 2025-04-25 15:31:43 +07:00
parent 19202f9656
commit 5685810508
6 changed files with 169 additions and 50 deletions

89
main.py
View File

@ -8,7 +8,7 @@ import datetime
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
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 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"),
{"email": email}
).fetchone()
if existing:
db.close()
raise HTTPException(status_code=400, detail="Email đã được đăng ký.")
# Save image
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()
try:
if existing:
# Email exists, just add new encoding
student_id = existing[0]
db.execute(
text("INSERT INTO students (name, email, encoding) VALUES (:name, :email, :encoding)"),
{"name": name, "email": email, "encoding": encoding_bytes}
text("INSERT INTO student_encodings (student_id, encoding) VALUES (:student_id, :encoding)"),
{"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()
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]
students = db.query(Student).all()
for student in students:
known_encoding = np.frombuffer(student.encoding)
# Get all encodings with student info
encodings = db.execute(
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)
if result[0]:
now = datetime.datetime.now()
recent_check = db.query(CheckInLog).filter(
CheckInLog.student_id == student.id,
CheckInLog.time > now - datetime.timedelta(minutes=5)
).first()
recent_check = db.execute(
text("""
SELECT id FROM checkin_logs
WHERE student_id = :student_id
AND time > :time_threshold
"""),
{
"student_id": encoding.id,
"time_threshold": now - datetime.timedelta(minutes=5)
}
).fetchone()
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.add(log)
db.execute(
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()
return {"message": f"Check-in successful for {student.name}"}
return {"message": f"Check-in successful for {encoding.name}"}
return {"message": "No match found."}
@app.get("/logs")
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 = []
for log in logs:
result.append({
"name": log.student.name,
"name": log.name,
"time": log.time.strftime("%Y-%m-%d %H:%M:%S"),
"camera_id": log.camera_id
})

View File

@ -6,16 +6,24 @@ import datetime
class Student(Base):
__tablename__ = "students"
__tablename__ = "students"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
email = Column(String(100), nullable=False, unique=True, index=True)
encoding = Column(LargeBinary, nullable=False)
__table_args__ = (UniqueConstraint('email', name='uq_student_email'),)
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):
__tablename__ = "checkin_logs"

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -23,7 +23,7 @@
video {
width: 100%;
max-width: 400px;
max-width: 1200px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: block;
@ -86,6 +86,19 @@
display: flex;
justify-content: center;
margin-top: 20px;
gap: 10px;
}
#auto-checkin {
background-color: #f39c12;
}
#auto-checkin:hover {
background-color: #d35400;
}
#auto-checkin.active {
background-color: #e74c3c;
}
.logs-container {
@ -136,14 +149,16 @@
.main-layout {
display: flex;
gap: 20px;
max-width: 1600px;
margin: 0 auto;
}
.left-panel {
flex: 1;
flex: 6;
}
.right-panel {
flex: 1;
flex: 5;
}
@media (max-width: 768px) {
@ -176,7 +191,6 @@
position: fixed;
top: 20px;
right: 20px;
background-color: #f87171; /* red-400 */
color: white;
padding: 12px 20px;
border-radius: 8px;
@ -187,6 +201,14 @@
animation: slideIn 0.3s ease-out;
}
.alert-success {
background-color: #34d399; /* green-400 */
}
.alert-error {
background-color: #f87171; /* red-400 */
}
@keyframes slideIn {
from {
opacity: 0;
@ -204,7 +226,10 @@
<div class="left-panel">
<div class="container">
<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>
<div class="form-group">
@ -217,6 +242,7 @@
<div class="button-group">
<button id="register">📥 Đăng ký khuôn mặt</button>
<button id="checkin">✅ Điểm danh</button>
<button id="auto-checkin">🔄 Tự động điểm danh</button>
</div>
<div class="shortcut-hint">
Nhấn phím Space để điểm danh nhanh
@ -246,7 +272,7 @@
</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>
const video = document.getElementById('video');
@ -262,9 +288,9 @@
showAlert("Không mở được camera: " + err);
});
function showAlert(text) {
function showAlert(text, status = 'error') {
const alertDiv = document.createElement('div');
alertDiv.className = 'custom-alert';
alertDiv.className = `custom-alert alert-${status}`;
alertDiv.innerText = text;
document.body.appendChild(alertDiv);
@ -279,7 +305,7 @@
// Hàm chụp ảnh từ video và gửi đến API
function sendImage(url, extraData = {}) {
context.drawImage(video, 0, 0, 400, 300);
context.drawImage(video, 0, 0, 1200, 900);
canvas.toBlob((blob) => {
const formData = new FormData();
formData.append("file", blob, "frame.jpg");
@ -294,12 +320,13 @@
})
.then(res => res.json())
.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")) {
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");
}
@ -307,19 +334,52 @@
document.getElementById('register').addEventListener('click', () => {
const name = document.getElementById('name').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 });
});
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
document.getElementById('checkin').addEventListener('click', () => {
if (autoCheckinInterval) {
toggleAutoCheckin(); // Tắt tự động nếu đang bật
}
sendImage("/checkin", { camera_id: "webcam" });
});
// Sự kiện: Nhấn phím Space để điểm danh
document.addEventListener('keydown', (event) => {
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" });
}
});

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