Commit ·
9ae9767
1
Parent(s): 2f535b3
Upload app from GitHub
Browse files- Insightface_Model/Models/buffalo_s/1k3d68.onnx +3 -0
- Insightface_Model/Models/buffalo_s/2d106det.onnx +3 -0
- Insightface_Model/Models/buffalo_s/det_500m.onnx +3 -0
- Insightface_Model/Models/buffalo_s/genderage.onnx +3 -0
- Insightface_Model/Models/buffalo_s/w600k_mbf.onnx +3 -0
- configure.sh +30 -0
- face_rec.py +239 -0
- home.py +131 -0
- main.sh +2 -0
- pages/1_real_time_prediction.py +292 -0
- pages/2_registration_form.py +197 -0
- pages/3_report.py +402 -0
- simulated_logs.txt +0 -0
- upload_logs.py +19 -0
Insightface_Model/Models/buffalo_s/1k3d68.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:df5c06b8a0c12e422b2ed8947b8869faa4105387f199c477af038aa01f9a45cc
|
| 3 |
+
size 143607619
|
Insightface_Model/Models/buffalo_s/2d106det.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f001b856447c413801ef5c42091ed0cd516fcd21f2d6b79635b1e733a7109dbf
|
| 3 |
+
size 5030888
|
Insightface_Model/Models/buffalo_s/det_500m.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5e4447f50245bbd7966bd6c0fa52938c61474a04ec7def48753668a9d8b4ea3a
|
| 3 |
+
size 2524817
|
Insightface_Model/Models/buffalo_s/genderage.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4fde69b1c810857b88c64a335084f1c3fe8f01246c9a191b48c7bb756d6652fb
|
| 3 |
+
size 1322532
|
Insightface_Model/Models/buffalo_s/w600k_mbf.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9cc6e4a75f0e2bf0b1aed94578f144d15175f357bdc05e815e5c4a02b319eb4f
|
| 3 |
+
size 13616099
|
configure.sh
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
echo "
|
| 2 |
+
<VirtualHost *:80>
|
| 3 |
+
ServerName <domain or ip address>
|
| 4 |
+
Redirect / https://<domain or ip address>
|
| 5 |
+
</VirtualHost>
|
| 6 |
+
|
| 7 |
+
<VirtualHost *:443>
|
| 8 |
+
|
| 9 |
+
ServerName <domain or ip address>
|
| 10 |
+
SSLEngine on
|
| 11 |
+
SSLProxyEngine On
|
| 12 |
+
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
|
| 13 |
+
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
|
| 14 |
+
|
| 15 |
+
ProxyRequests Off
|
| 16 |
+
ProxyPreserveHost On
|
| 17 |
+
#AllowEncodedSlashes NoDecode
|
| 18 |
+
<Proxy *>
|
| 19 |
+
Order deny,allow
|
| 20 |
+
Allow from all
|
| 21 |
+
</Proxy>
|
| 22 |
+
|
| 23 |
+
ProxyPass /_stcore ws://localhost:8501/_stcore
|
| 24 |
+
ProxyPassReverse /_stcore ws://localhost:8501/_stcore
|
| 25 |
+
|
| 26 |
+
# The order is important here
|
| 27 |
+
ProxyPass / http://localhost:8501/
|
| 28 |
+
ProxyPassReverse / http://localhost:8501/
|
| 29 |
+
|
| 30 |
+
</VirtualHost>" > /etc/apache2/sites-available/deploy_attendance_app.conf
|
face_rec.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import cv2
|
| 4 |
+
|
| 5 |
+
import redis
|
| 6 |
+
|
| 7 |
+
from insightface.app import FaceAnalysis
|
| 8 |
+
from sklearn.metrics import pairwise
|
| 9 |
+
|
| 10 |
+
import time
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
hostname = 'redis-18916.c83.us-east-1-2.ec2.redns.redis-cloud.com'
|
| 15 |
+
port = 18916
|
| 16 |
+
password = 'vTig9W0x2XtLCyR3MDFQFyZjSMSId6Gr'
|
| 17 |
+
r = redis.StrictRedis(host=hostname,
|
| 18 |
+
port=port,
|
| 19 |
+
password=password)
|
| 20 |
+
# extracting data from redis database
|
| 21 |
+
def retrive_data(name):
|
| 22 |
+
retrive_dict = r.hgetall(name)
|
| 23 |
+
retrive_series = pd.Series(retrive_dict)
|
| 24 |
+
retrive_series = retrive_series.apply(lambda x:np.frombuffer(x,dtype=np.float32))
|
| 25 |
+
index = retrive_series.index
|
| 26 |
+
index = list(map(lambda x: x.decode(),index))
|
| 27 |
+
retrive_series.index = index
|
| 28 |
+
retrive_df = retrive_series.to_frame().reset_index()
|
| 29 |
+
retrive_df.columns = ['name_role','Facial Feature']
|
| 30 |
+
retrive_df[['Name','Role']] = retrive_df['name_role'].apply(lambda x : x.split('@')).apply(pd.Series)
|
| 31 |
+
return retrive_df[['Name','Role','Facial Feature']]
|
| 32 |
+
|
| 33 |
+
# Configure face analysis
|
| 34 |
+
face_app = FaceAnalysis(name='buffalo_s',
|
| 35 |
+
root='insightface_model',
|
| 36 |
+
providers=['CPUExecutionProvider'])
|
| 37 |
+
face_app.prepare(ctx_id=0,det_size=(640,640),det_thresh=0.5)
|
| 38 |
+
|
| 39 |
+
# def ml_search_algorithm(dataframe,feature_column,test_vector,name_role=['Name','Role'],thresh=0.5):
|
| 40 |
+
|
| 41 |
+
# # cosine similarity based search algorithm
|
| 42 |
+
# # feature_column --> column name that contains features(embeddings) in dataframe
|
| 43 |
+
|
| 44 |
+
# dataframe = dataframe.copy()
|
| 45 |
+
|
| 46 |
+
# X_list = dataframe[feature_column].tolist()
|
| 47 |
+
# x = np.asarray(X_list)
|
| 48 |
+
|
| 49 |
+
# # Debugging step: print shape of each embedding
|
| 50 |
+
# for idx, item in enumerate(X_list):
|
| 51 |
+
# print(f"Item {idx} shape: {np.array(item).shape}")
|
| 52 |
+
|
| 53 |
+
# try:
|
| 54 |
+
# x = np.asarray(X_list)
|
| 55 |
+
# except ValueError as e:
|
| 56 |
+
# print("Error converting X_list to array:", e)
|
| 57 |
+
# # Handle inconsistent shapes here, for example, by filtering out invalid items
|
| 58 |
+
# return 'Unknown', 'Unknown'
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# similar = pairwise.cosine_similarity(x,test_vector.reshape(1,-1))
|
| 62 |
+
# similar_arr = np.array(similar).flatten()
|
| 63 |
+
# dataframe['cosine'] = similar_arr
|
| 64 |
+
|
| 65 |
+
# data_filter = dataframe.query(f'cosine >= {thresh}')
|
| 66 |
+
# if(len(data_filter) > 0):
|
| 67 |
+
# data_filter.reset_index(drop=True,inplace=True)
|
| 68 |
+
# argmax = data_filter['cosine'].argmax()
|
| 69 |
+
# person_name,person_role = data_filter.loc[argmax][name_role]
|
| 70 |
+
# else:
|
| 71 |
+
# person_name = 'Unknown'
|
| 72 |
+
# person_role = 'Unknown'
|
| 73 |
+
|
| 74 |
+
# return person_name,person_role
|
| 75 |
+
|
| 76 |
+
def ml_search_algorithm(dataframe, feature_column, test_vector, name_role=['Name', 'Role'], thresh=0.5):
|
| 77 |
+
# cosine similarity based search algorithm
|
| 78 |
+
dataframe = dataframe.copy()
|
| 79 |
+
X_list = dataframe[feature_column].tolist()
|
| 80 |
+
|
| 81 |
+
# Check if all embeddings have the same shape as the test vector
|
| 82 |
+
valid_indices = []
|
| 83 |
+
valid_embeddings = []
|
| 84 |
+
|
| 85 |
+
# Get test vector shape (usually 512 for InsightFace)
|
| 86 |
+
test_dim = test_vector.shape[0]
|
| 87 |
+
|
| 88 |
+
# Filter out embeddings with inconsistent shapes
|
| 89 |
+
for idx, item in enumerate(X_list):
|
| 90 |
+
try:
|
| 91 |
+
item_array = np.array(item)
|
| 92 |
+
if item_array.shape[0] == test_dim:
|
| 93 |
+
valid_indices.append(idx)
|
| 94 |
+
valid_embeddings.append(item_array)
|
| 95 |
+
except:
|
| 96 |
+
print(f"Skipping item {idx} due to shape issue")
|
| 97 |
+
|
| 98 |
+
if len(valid_embeddings) == 0:
|
| 99 |
+
print("No valid embeddings found in database")
|
| 100 |
+
return 'Unknown', 'Unknown'
|
| 101 |
+
|
| 102 |
+
# Create a new array with only valid embeddings
|
| 103 |
+
x = np.vstack(valid_embeddings)
|
| 104 |
+
|
| 105 |
+
# Create a new filtered dataframe
|
| 106 |
+
filtered_df = dataframe.iloc[valid_indices].copy()
|
| 107 |
+
|
| 108 |
+
# Calculate similarity
|
| 109 |
+
similar = pairwise.cosine_similarity(x, test_vector.reshape(1, -1))
|
| 110 |
+
similar_arr = np.array(similar).flatten()
|
| 111 |
+
|
| 112 |
+
# Add similarity scores to filtered dataframe
|
| 113 |
+
filtered_df['cosine'] = similar_arr
|
| 114 |
+
|
| 115 |
+
# Filter by threshold
|
| 116 |
+
data_filter = filtered_df.query(f'cosine >= {thresh}')
|
| 117 |
+
|
| 118 |
+
if len(data_filter) > 0:
|
| 119 |
+
data_filter.reset_index(drop=True, inplace=True)
|
| 120 |
+
argmax = data_filter['cosine'].argmax()
|
| 121 |
+
person_name, person_role = data_filter.loc[argmax][name_role]
|
| 122 |
+
else:
|
| 123 |
+
person_name = 'Unknown'
|
| 124 |
+
person_role = 'Unknown'
|
| 125 |
+
|
| 126 |
+
return person_name, person_role
|
| 127 |
+
class RealTimePred:
|
| 128 |
+
def __init__(self):
|
| 129 |
+
self.logs = dict(name=[],role=[],current_time=[])
|
| 130 |
+
|
| 131 |
+
def reset_dict(self):
|
| 132 |
+
self.logs = dict(name=[],role=[],current_time=[])
|
| 133 |
+
|
| 134 |
+
def saveLogs_redis(self):
|
| 135 |
+
dataframe = pd.DataFrame(self.logs)
|
| 136 |
+
|
| 137 |
+
dataframe.drop_duplicates('name',inplace=True)
|
| 138 |
+
|
| 139 |
+
name_list = dataframe['name'].tolist()
|
| 140 |
+
role_list = dataframe['role'].tolist()
|
| 141 |
+
ctime_list = dataframe['current_time'].tolist()
|
| 142 |
+
encoded_data = []
|
| 143 |
+
|
| 144 |
+
for name,role,ctime in zip(name_list,role_list,ctime_list):
|
| 145 |
+
if name != 'Unknown':
|
| 146 |
+
concat_string = f'{name}@{role}@{ctime}'
|
| 147 |
+
encoded_data.append(concat_string)
|
| 148 |
+
|
| 149 |
+
if len(encoded_data) > 0:
|
| 150 |
+
r.lpush('attendance:logs',*encoded_data)
|
| 151 |
+
|
| 152 |
+
self.reset_dict()
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def face_prediction(self,test_image,dataframe,feature_column,name_role=['Name','Role'],thresh=0.5):
|
| 156 |
+
current_time = str(datetime.now().strftime('%Y-%m-%d %H:%M'))
|
| 157 |
+
results = face_app.get(test_image)
|
| 158 |
+
test_copy = test_image.copy()
|
| 159 |
+
for res in results:
|
| 160 |
+
x1,y1,x2,y2 = res['bbox'].astype(int)
|
| 161 |
+
embeddings = res['embedding']
|
| 162 |
+
|
| 163 |
+
person_name,person_role = ml_search_algorithm(dataframe,'Facial Feature',
|
| 164 |
+
test_vector=embeddings,
|
| 165 |
+
name_role=name_role,
|
| 166 |
+
thresh=thresh)
|
| 167 |
+
|
| 168 |
+
if person_name=='Unknown':
|
| 169 |
+
color = (0,0,255)
|
| 170 |
+
else:
|
| 171 |
+
color = (0,255,0)
|
| 172 |
+
cv2.rectangle(test_copy,(x1,y1),(x2,y2),color,2)
|
| 173 |
+
text_gen = person_name
|
| 174 |
+
cv2.putText(test_copy,text_gen,(x1,y1),cv2.FONT_HERSHEY_DUPLEX,0.7,color,2)
|
| 175 |
+
cv2.putText(test_copy,current_time,(x1,y2+20),cv2.FONT_HERSHEY_DUPLEX,0.7,color,2)
|
| 176 |
+
|
| 177 |
+
# save info in logs dict
|
| 178 |
+
self.logs['name'].append(person_name)
|
| 179 |
+
self.logs['role'].append(person_role)
|
| 180 |
+
self.logs['current_time'].append(current_time)
|
| 181 |
+
|
| 182 |
+
return test_copy
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# Registration Form
|
| 187 |
+
class RegistrationForm:
|
| 188 |
+
def __init__(self):
|
| 189 |
+
self.sample = 0
|
| 190 |
+
|
| 191 |
+
def reset(self):
|
| 192 |
+
self.sample = 0
|
| 193 |
+
|
| 194 |
+
def get_embedding(self,frame):
|
| 195 |
+
# get results from insightface model
|
| 196 |
+
results = face_app.get(frame,max_num=1)
|
| 197 |
+
embeddings = None
|
| 198 |
+
for res in results:
|
| 199 |
+
self.sample = self.sample + 1
|
| 200 |
+
x1,y1,x2,y2 = res['bbox'].astype(int)
|
| 201 |
+
cv2.rectangle(frame,(x1,y1),(x2,y2),(0,255,0),2)
|
| 202 |
+
|
| 203 |
+
# put text samples info
|
| 204 |
+
text = f"samples : {self.sample}"
|
| 205 |
+
cv2.putText(frame,text,(x1,y1),cv2.FONT_HERSHEY_DUPLEX,0.6,(0,255,0),2)
|
| 206 |
+
|
| 207 |
+
embeddings = res['embedding']
|
| 208 |
+
|
| 209 |
+
return frame,embeddings
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def save_data_in_redis_db(self,name,role):
|
| 213 |
+
if name is not None:
|
| 214 |
+
if name.strip() != '':
|
| 215 |
+
key = f'{name}@{role}'
|
| 216 |
+
else:
|
| 217 |
+
return 'name_false'
|
| 218 |
+
else:
|
| 219 |
+
return 'name_false'
|
| 220 |
+
|
| 221 |
+
if 'face_embedding.txt' not in os.listdir():
|
| 222 |
+
return 'file_false'
|
| 223 |
+
# step 1 : load "face_embedding.txt"
|
| 224 |
+
x_array = np.loadtxt('face_embedding.txt',dtype=np.float32) # flatten array
|
| 225 |
+
|
| 226 |
+
# step 2 : convert into array (proper shape)
|
| 227 |
+
received_samples = int(x_array.size/512)
|
| 228 |
+
x_array = x_array.reshape(received_samples,512)
|
| 229 |
+
|
| 230 |
+
# step 3 : cal. mean embeddings
|
| 231 |
+
x_mean = x_array.mean(axis=0)
|
| 232 |
+
x_mean_bytes = x_mean.tobytes()
|
| 233 |
+
|
| 234 |
+
# step 4 : save this into redis database (redis hashes)
|
| 235 |
+
r.hset(name='academy:register',key=key,value=x_mean_bytes)
|
| 236 |
+
|
| 237 |
+
os.remove('face_embedding.txt')
|
| 238 |
+
self.reset()
|
| 239 |
+
return True
|
home.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import time
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import base64
|
| 5 |
+
|
| 6 |
+
# Page configuration
|
| 7 |
+
st.set_page_config(
|
| 8 |
+
page_title="AI Attendance System",
|
| 9 |
+
page_icon="👁️",
|
| 10 |
+
layout="wide",
|
| 11 |
+
initial_sidebar_state="expanded"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
# Custom CSS for better appearance
|
| 15 |
+
st.markdown("""
|
| 16 |
+
<style>
|
| 17 |
+
.main-header {
|
| 18 |
+
font-size: 2.5rem;
|
| 19 |
+
color: #1E88E5;
|
| 20 |
+
text-align: center;
|
| 21 |
+
margin-bottom: 1rem;
|
| 22 |
+
font-weight: 700;
|
| 23 |
+
}
|
| 24 |
+
.sub-header {
|
| 25 |
+
font-size: 1.5rem;
|
| 26 |
+
color: #424242;
|
| 27 |
+
margin-top: 2rem;
|
| 28 |
+
font-weight: 600;
|
| 29 |
+
}
|
| 30 |
+
.card {
|
| 31 |
+
border-radius: 5px;
|
| 32 |
+
padding: 20px;
|
| 33 |
+
background-color: #f8f9fa;
|
| 34 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 35 |
+
margin-bottom: 20px;
|
| 36 |
+
}
|
| 37 |
+
.success-box {
|
| 38 |
+
background-color: #d4edda;
|
| 39 |
+
color: #155724;
|
| 40 |
+
padding: 15px;
|
| 41 |
+
border-radius: 5px;
|
| 42 |
+
margin-bottom: 20px;
|
| 43 |
+
}
|
| 44 |
+
.feature-icon {
|
| 45 |
+
font-size: 1.8rem;
|
| 46 |
+
margin-right: 10px;
|
| 47 |
+
}
|
| 48 |
+
.footer {
|
| 49 |
+
text-align: center;
|
| 50 |
+
margin-top: 50px;
|
| 51 |
+
padding: 20px;
|
| 52 |
+
color: #6c757d;
|
| 53 |
+
border-top: 1px solid #e9ecef;
|
| 54 |
+
}
|
| 55 |
+
</style>
|
| 56 |
+
""", unsafe_allow_html=True)
|
| 57 |
+
|
| 58 |
+
# Header with logo
|
| 59 |
+
col1, col2, col3 = st.columns([1, 3, 1])
|
| 60 |
+
with col2:
|
| 61 |
+
st.markdown("<h1 class='main-header'>🔷 AI-Powered Attendance System</h1>", unsafe_allow_html=True)
|
| 62 |
+
st.markdown("<p style='text-align: center; font-size: 1.2rem;'>Smart Recognition • Secure • Efficient</p>", unsafe_allow_html=True)
|
| 63 |
+
|
| 64 |
+
# Create two columns for content
|
| 65 |
+
left_col, right_col = st.columns([2, 1])
|
| 66 |
+
|
| 67 |
+
with left_col:
|
| 68 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 69 |
+
with st.spinner('🔄 Loading Models and Connecting to Database...'):
|
| 70 |
+
try:
|
| 71 |
+
import face_rec
|
| 72 |
+
time.sleep(2) # Simulating loading time
|
| 73 |
+
st.markdown("<div class='success-box'><b>✅ Face Recognition Model:</b> Loaded Successfully</div>", unsafe_allow_html=True)
|
| 74 |
+
st.markdown("<div class='success-box'><b>✅ Database Connection:</b> Connected to Redis DB</div>", unsafe_allow_html=True)
|
| 75 |
+
except Exception as e:
|
| 76 |
+
st.error(f"Error loading system components: {e}")
|
| 77 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 78 |
+
|
| 79 |
+
# System features
|
| 80 |
+
st.markdown("<h2 class='sub-header'>System Features</h2>", unsafe_allow_html=True)
|
| 81 |
+
|
| 82 |
+
features = [
|
| 83 |
+
("👁️ Real-time Face Recognition", "Instant identification of registered users using advanced ML models"),
|
| 84 |
+
("⏱️ Automatic Attendance Logging", "Timestamps recorded automatically with entry and exit times"),
|
| 85 |
+
("🔐 Role-based Access Management", "Different access levels for students and teachers"),
|
| 86 |
+
("📊 Comprehensive Reports", "Generate detailed attendance reports with various filters"),
|
| 87 |
+
("⚡ High Performance", "Optimized for speed even with multiple simultaneous recognitions")
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
for icon_title, description in features:
|
| 91 |
+
st.markdown(f"""
|
| 92 |
+
<div style='display: flex; align-items: center; margin-bottom: 15px;'>
|
| 93 |
+
<div class='feature-icon'>{icon_title.split()[0]}</div>
|
| 94 |
+
<div>
|
| 95 |
+
<b>{" ".join(icon_title.split()[1:])}</b><br>
|
| 96 |
+
<span style='color: #6c757d; font-size: 0.9rem;'>{description}</span>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
""", unsafe_allow_html=True)
|
| 100 |
+
|
| 101 |
+
with right_col:
|
| 102 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 103 |
+
st.markdown("<h3 style='text-align: center;'>Quick Navigation</h3>", unsafe_allow_html=True)
|
| 104 |
+
|
| 105 |
+
# Quick action buttons
|
| 106 |
+
if st.button("📝 Register New User", use_container_width=True):
|
| 107 |
+
st.switch_page("pages/2_registration_form.py")
|
| 108 |
+
|
| 109 |
+
if st.button("🟢 Start Attendance Tracking", use_container_width=True):
|
| 110 |
+
st.switch_page("pages/1_real_time_prediction.py")
|
| 111 |
+
|
| 112 |
+
if st.button("📊 View Attendance Reports", use_container_width=True):
|
| 113 |
+
st.switch_page("pages/3_report.py")
|
| 114 |
+
|
| 115 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 116 |
+
|
| 117 |
+
# System status
|
| 118 |
+
st.markdown("<div class='card' style='margin-top: 20px;'>", unsafe_allow_html=True)
|
| 119 |
+
st.markdown("<h3 style='text-align: center;'>System Status</h3>", unsafe_allow_html=True)
|
| 120 |
+
|
| 121 |
+
# Display some system metrics
|
| 122 |
+
col1, col2 = st.columns(2)
|
| 123 |
+
with col1:
|
| 124 |
+
st.metric(label="System Status", value="Online", delta="Active")
|
| 125 |
+
with col2:
|
| 126 |
+
st.metric(label="Database Status", value="Connected", delta="Active")
|
| 127 |
+
|
| 128 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 129 |
+
|
| 130 |
+
# Footer
|
| 131 |
+
st.markdown("<div class='footer'>AI-Powered Attendance System • © 2025</div>", unsafe_allow_html=True)
|
main.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
bash configure.sh
|
| 2 |
+
streamlit run home.py
|
pages/1_real_time_prediction.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from home import face_rec
|
| 3 |
+
from streamlit_webrtc import webrtc_streamer
|
| 4 |
+
import av
|
| 5 |
+
import time
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
# Page configuration
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="Live Attendance | AI Attendance",
|
| 12 |
+
page_icon="🟢",
|
| 13 |
+
layout="wide"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Custom CSS
|
| 17 |
+
st.markdown("""
|
| 18 |
+
<style>
|
| 19 |
+
.main-header {
|
| 20 |
+
font-size: 2.2rem;
|
| 21 |
+
color: #4CAF50;
|
| 22 |
+
text-align: center;
|
| 23 |
+
margin-bottom: 1.5rem;
|
| 24 |
+
font-weight: 700;
|
| 25 |
+
}
|
| 26 |
+
.card {
|
| 27 |
+
border-radius: 8px;
|
| 28 |
+
padding: 25px;
|
| 29 |
+
background-color: #f8f9fa;
|
| 30 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
}
|
| 33 |
+
.webcam-container {
|
| 34 |
+
border: 2px solid #4CAF50;
|
| 35 |
+
border-radius: 8px;
|
| 36 |
+
padding: 15px;
|
| 37 |
+
background-color: #f1f8e9;
|
| 38 |
+
}
|
| 39 |
+
.status-indicator {
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
margin-bottom: 10px;
|
| 43 |
+
}
|
| 44 |
+
.status-dot {
|
| 45 |
+
width: 12px;
|
| 46 |
+
height: 12px;
|
| 47 |
+
border-radius: 50%;
|
| 48 |
+
margin-right: 8px;
|
| 49 |
+
}
|
| 50 |
+
.active {
|
| 51 |
+
background-color: #4CAF50;
|
| 52 |
+
box-shadow: 0 0 8px #4CAF50;
|
| 53 |
+
animation: pulse 2s infinite;
|
| 54 |
+
}
|
| 55 |
+
@keyframes pulse {
|
| 56 |
+
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); }
|
| 57 |
+
70% { box-shadow: 0 0 0 10px rgba(76, 175, 80, 0); }
|
| 58 |
+
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
|
| 59 |
+
}
|
| 60 |
+
.inactive {
|
| 61 |
+
background-color: #9e9e9e;
|
| 62 |
+
}
|
| 63 |
+
.section-title {
|
| 64 |
+
font-size: 1.3rem;
|
| 65 |
+
font-weight: 600;
|
| 66 |
+
color: #424242;
|
| 67 |
+
margin-bottom: 15px;
|
| 68 |
+
padding-bottom: 8px;
|
| 69 |
+
border-bottom: 2px solid #e0e0e0;
|
| 70 |
+
}
|
| 71 |
+
.attendance-table {
|
| 72 |
+
font-size: 0.9rem;
|
| 73 |
+
}
|
| 74 |
+
.footer {
|
| 75 |
+
text-align: center;
|
| 76 |
+
margin-top: 30px;
|
| 77 |
+
padding: 20px;
|
| 78 |
+
color: #6c757d;
|
| 79 |
+
border-top: 1px solid #e9ecef;
|
| 80 |
+
}
|
| 81 |
+
.stats-box {
|
| 82 |
+
background-color: #f8f9fa;
|
| 83 |
+
border-radius: 8px;
|
| 84 |
+
padding: 15px;
|
| 85 |
+
text-align: center;
|
| 86 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
| 87 |
+
}
|
| 88 |
+
.stats-number {
|
| 89 |
+
font-size: 1.8rem;
|
| 90 |
+
font-weight: bold;
|
| 91 |
+
color: #4CAF50;
|
| 92 |
+
}
|
| 93 |
+
.stats-label {
|
| 94 |
+
color: #757575;
|
| 95 |
+
font-size: 0.9rem;
|
| 96 |
+
}
|
| 97 |
+
</style>
|
| 98 |
+
""", unsafe_allow_html=True)
|
| 99 |
+
|
| 100 |
+
# Header
|
| 101 |
+
st.markdown("<h1 class='main-header'>🟢 Live Attendance Tracking</h1>", unsafe_allow_html=True)
|
| 102 |
+
|
| 103 |
+
# System status indicators
|
| 104 |
+
col1, col2, col3 = st.columns(3)
|
| 105 |
+
with col1:
|
| 106 |
+
st.markdown("""
|
| 107 |
+
<div class="status-indicator">
|
| 108 |
+
<div class="status-dot active"></div>
|
| 109 |
+
<div>Recognition System: <b>Active</b></div>
|
| 110 |
+
</div>
|
| 111 |
+
""", unsafe_allow_html=True)
|
| 112 |
+
with col2:
|
| 113 |
+
st.markdown("""
|
| 114 |
+
<div class="status-indicator">
|
| 115 |
+
<div class="status-dot active"></div>
|
| 116 |
+
<div>Database Connection: <b>Active</b></div>
|
| 117 |
+
</div>
|
| 118 |
+
""", unsafe_allow_html=True)
|
| 119 |
+
with col3:
|
| 120 |
+
st.markdown("""
|
| 121 |
+
<div class="status-indicator">
|
| 122 |
+
<div class="status-dot active"></div>
|
| 123 |
+
<div>Auto-Logging: <b>Enabled</b></div>
|
| 124 |
+
</div>
|
| 125 |
+
""", unsafe_allow_html=True)
|
| 126 |
+
|
| 127 |
+
# Main content
|
| 128 |
+
left_col, right_col = st.columns([3, 2])
|
| 129 |
+
|
| 130 |
+
with left_col:
|
| 131 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 132 |
+
st.markdown("<h2 class='section-title'>📹 Live Recognition Feed</h2>", unsafe_allow_html=True)
|
| 133 |
+
|
| 134 |
+
# Retrieve registered data
|
| 135 |
+
with st.spinner('Retrieving data from Redis database...'):
|
| 136 |
+
try:
|
| 137 |
+
redis_face_db = face_rec.retrive_data(name='academy:register')
|
| 138 |
+
if not redis_face_db.empty:
|
| 139 |
+
st.success('✅ User data retrieved successfully!')
|
| 140 |
+
else:
|
| 141 |
+
st.warning('⚠️ No registered users found in the database.')
|
| 142 |
+
except Exception as e:
|
| 143 |
+
st.error(f"Error retrieving data: {e}")
|
| 144 |
+
redis_face_db = pd.DataFrame()
|
| 145 |
+
|
| 146 |
+
# Configuration options
|
| 147 |
+
with st.expander("Recognition Settings"):
|
| 148 |
+
wait_time = st.slider("Log Update Interval (seconds)", 10, 120, 30)
|
| 149 |
+
confidence_threshold = st.slider("Recognition Confidence Threshold", 0.3, 0.9, 0.5, 0.05)
|
| 150 |
+
|
| 151 |
+
# Initialize real-time prediction
|
| 152 |
+
set_time = time.time()
|
| 153 |
+
realtime_pred = face_rec.RealTimePred()
|
| 154 |
+
|
| 155 |
+
# Video frame callback function
|
| 156 |
+
def video_frame_callback(frame):
|
| 157 |
+
global set_time
|
| 158 |
+
img = frame.to_ndarray(format="bgr24")
|
| 159 |
+
|
| 160 |
+
# Perform face prediction
|
| 161 |
+
pred_img = realtime_pred.face_prediction(
|
| 162 |
+
img,
|
| 163 |
+
redis_face_db,
|
| 164 |
+
'Facial Feature',
|
| 165 |
+
['Name', 'Role'],
|
| 166 |
+
thresh=confidence_threshold
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
# Check if it's time to save logs
|
| 170 |
+
time_now = time.time()
|
| 171 |
+
diff_time = time_now - set_time
|
| 172 |
+
if diff_time >= wait_time:
|
| 173 |
+
realtime_pred.saveLogs_redis()
|
| 174 |
+
set_time = time.time() # Reset timer
|
| 175 |
+
|
| 176 |
+
return av.VideoFrame.from_ndarray(pred_img, format="bgr24")
|
| 177 |
+
|
| 178 |
+
# Webcam feed with face recognition
|
| 179 |
+
st.markdown("<div class='webcam-container'>", unsafe_allow_html=True)
|
| 180 |
+
webrtc_streamer(
|
| 181 |
+
key="realTimePrediction",
|
| 182 |
+
video_frame_callback=video_frame_callback,
|
| 183 |
+
rtc_configuration={"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]},
|
| 184 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 185 |
+
)
|
| 186 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 187 |
+
|
| 188 |
+
# Instructions
|
| 189 |
+
st.info("""
|
| 190 |
+
**Instructions:**
|
| 191 |
+
1. Stand in front of the camera
|
| 192 |
+
2. Wait for the system to recognize your face
|
| 193 |
+
3. Your attendance will be logged automatically
|
| 194 |
+
4. The system records entry and exit times
|
| 195 |
+
""")
|
| 196 |
+
|
| 197 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 198 |
+
|
| 199 |
+
with right_col:
|
| 200 |
+
# User database card
|
| 201 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 202 |
+
st.markdown("<h2 class='section-title'>👥 Registered Users</h2>", unsafe_allow_html=True)
|
| 203 |
+
|
| 204 |
+
# Display registered users
|
| 205 |
+
if not redis_face_db.empty:
|
| 206 |
+
st.dataframe(
|
| 207 |
+
redis_face_db[['Name', 'Role']].sort_values('Name'),
|
| 208 |
+
use_container_width=True,
|
| 209 |
+
hide_index=True,
|
| 210 |
+
height=200
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# Display statistics
|
| 214 |
+
total_users = len(redis_face_db)
|
| 215 |
+
students = len(redis_face_db[redis_face_db['Role'] == 'Student'])
|
| 216 |
+
teachers = len(redis_face_db[redis_face_db['Role'] == 'Teacher'])
|
| 217 |
+
|
| 218 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 219 |
+
stats_cols = st.columns(3)
|
| 220 |
+
with stats_cols[0]:
|
| 221 |
+
st.markdown(f"""
|
| 222 |
+
<div class="stats-box">
|
| 223 |
+
<div class="stats-number">{total_users}</div>
|
| 224 |
+
<div class="stats-label">Total Users</div>
|
| 225 |
+
</div>
|
| 226 |
+
""", unsafe_allow_html=True)
|
| 227 |
+
with stats_cols[1]:
|
| 228 |
+
st.markdown(f"""
|
| 229 |
+
<div class="stats-box">
|
| 230 |
+
<div class="stats-number">{students}</div>
|
| 231 |
+
<div class="stats-label">Students</div>
|
| 232 |
+
</div>
|
| 233 |
+
""", unsafe_allow_html=True)
|
| 234 |
+
with stats_cols[2]:
|
| 235 |
+
st.markdown(f"""
|
| 236 |
+
<div class="stats-box">
|
| 237 |
+
<div class="stats-number">{teachers}</div>
|
| 238 |
+
<div class="stats-label">Teachers</div>
|
| 239 |
+
</div>
|
| 240 |
+
""", unsafe_allow_html=True)
|
| 241 |
+
else:
|
| 242 |
+
st.warning("No registered users found in the database.")
|
| 243 |
+
|
| 244 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 245 |
+
|
| 246 |
+
# Recent activity card
|
| 247 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 248 |
+
st.markdown("<h2 class='section-title'>🕒 Recent Activity</h2>", unsafe_allow_html=True)
|
| 249 |
+
|
| 250 |
+
# Load and display recent logs
|
| 251 |
+
try:
|
| 252 |
+
logs_list = face_rec.r.lrange('attendance:logs', 0, 9) # Get last 10 logs
|
| 253 |
+
|
| 254 |
+
if logs_list:
|
| 255 |
+
# Convert bytes to string and create dataframe
|
| 256 |
+
logs_string = [log.decode('utf-8').split('@') for log in logs_list]
|
| 257 |
+
logs_df = pd.DataFrame(logs_string, columns=['Name', 'Role', 'Timestamp'])
|
| 258 |
+
|
| 259 |
+
# Format timestamp
|
| 260 |
+
logs_df['Timestamp'] = pd.to_datetime(logs_df['Timestamp'],format='ISO8601')
|
| 261 |
+
logs_df['Time'] = logs_df['Timestamp'].dt.strftime('%H:%M:%S')
|
| 262 |
+
logs_df['Date'] = logs_df['Timestamp'].dt.strftime('%Y-%m-%d')
|
| 263 |
+
|
| 264 |
+
# Display recent logs
|
| 265 |
+
st.dataframe(
|
| 266 |
+
logs_df[['Name', 'Role', 'Time', 'Date']],
|
| 267 |
+
use_container_width=True,
|
| 268 |
+
hide_index=True
|
| 269 |
+
)
|
| 270 |
+
else:
|
| 271 |
+
st.info("No recent activity logged.")
|
| 272 |
+
except Exception as e:
|
| 273 |
+
st.error(f"Error loading logs: {e}")
|
| 274 |
+
|
| 275 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 276 |
+
|
| 277 |
+
# Quick actions
|
| 278 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 279 |
+
st.markdown("<h2 class='section-title'>⚡ Quick Actions</h2>", unsafe_allow_html=True)
|
| 280 |
+
|
| 281 |
+
col1, col2 = st.columns(2)
|
| 282 |
+
with col1:
|
| 283 |
+
if st.button("📊 View Reports", use_container_width=True):
|
| 284 |
+
st.switch_page("pages/report.py")
|
| 285 |
+
with col2:
|
| 286 |
+
if st.button("📝 Add New User", use_container_width=True):
|
| 287 |
+
st.switch_page("pages/page1.py")
|
| 288 |
+
|
| 289 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 290 |
+
|
| 291 |
+
# Footer
|
| 292 |
+
st.markdown("<div class='footer'>AI-Powered Attendance System • © 2025</div>", unsafe_allow_html=True)
|
pages/2_registration_form.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import face_rec
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
from streamlit_webrtc import webrtc_streamer
|
| 6 |
+
import av
|
| 7 |
+
import time
|
| 8 |
+
|
| 9 |
+
# Page configuration
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="User Registration | AI Attendance",
|
| 12 |
+
page_icon="📝",
|
| 13 |
+
layout="wide"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Custom CSS
|
| 17 |
+
st.markdown("""
|
| 18 |
+
<style>
|
| 19 |
+
.main-header {
|
| 20 |
+
font-size: 2.2rem;
|
| 21 |
+
color: #1E88E5;
|
| 22 |
+
text-align: center;
|
| 23 |
+
margin-bottom: 1.5rem;
|
| 24 |
+
font-weight: 700;
|
| 25 |
+
}
|
| 26 |
+
.card {
|
| 27 |
+
border-radius: 8px;
|
| 28 |
+
padding: 25px;
|
| 29 |
+
background-color: #f8f9fa;
|
| 30 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
}
|
| 33 |
+
.instructions {
|
| 34 |
+
background-color: #e8f4f8;
|
| 35 |
+
border-left: 4px solid #1E88E5;
|
| 36 |
+
padding: 15px;
|
| 37 |
+
margin-bottom: 20px;
|
| 38 |
+
border-radius: 4px;
|
| 39 |
+
}
|
| 40 |
+
.submit-button {
|
| 41 |
+
background-color: #4CAF50;
|
| 42 |
+
color: white;
|
| 43 |
+
padding: 12px 24px;
|
| 44 |
+
font-size: 16px;
|
| 45 |
+
border-radius: 4px;
|
| 46 |
+
border: none;
|
| 47 |
+
cursor: pointer;
|
| 48 |
+
width: 100%;
|
| 49 |
+
}
|
| 50 |
+
.input-label {
|
| 51 |
+
font-weight: 600;
|
| 52 |
+
color: #424242;
|
| 53 |
+
margin-bottom: 8px;
|
| 54 |
+
}
|
| 55 |
+
.webcam-container {
|
| 56 |
+
border: 2px dashed #1E88E5;
|
| 57 |
+
border-radius: 8px;
|
| 58 |
+
padding: 10px;
|
| 59 |
+
margin-top: 10px;
|
| 60 |
+
}
|
| 61 |
+
.step-counter {
|
| 62 |
+
background-color: #1E88E5;
|
| 63 |
+
color: white;
|
| 64 |
+
border-radius: 50%;
|
| 65 |
+
width: 25px;
|
| 66 |
+
height: 25px;
|
| 67 |
+
display: inline-flex;
|
| 68 |
+
align-items: center;
|
| 69 |
+
justify-content: center;
|
| 70 |
+
margin-right: 10px;
|
| 71 |
+
}
|
| 72 |
+
.step-title {
|
| 73 |
+
font-size: 1.2rem;
|
| 74 |
+
font-weight: 600;
|
| 75 |
+
color: #1E88E5;
|
| 76 |
+
}
|
| 77 |
+
.footer {
|
| 78 |
+
text-align: center;
|
| 79 |
+
margin-top: 30px;
|
| 80 |
+
padding: 20px;
|
| 81 |
+
color: #6c757d;
|
| 82 |
+
border-top: 1px solid #e9ecef;
|
| 83 |
+
}
|
| 84 |
+
</style>
|
| 85 |
+
""", unsafe_allow_html=True)
|
| 86 |
+
|
| 87 |
+
# Header
|
| 88 |
+
st.markdown("<h1 class='main-header'>📝 User Registration Form</h1>", unsafe_allow_html=True)
|
| 89 |
+
|
| 90 |
+
# Progress bar for registration process
|
| 91 |
+
step = 1
|
| 92 |
+
steps = ["Enter Information", "Capture Face", "Submit Registration"]
|
| 93 |
+
progress = (step / len(steps))
|
| 94 |
+
|
| 95 |
+
st.progress(progress, text=f"Step {step} of {len(steps)}: {steps[step-1]}")
|
| 96 |
+
|
| 97 |
+
# Main content in columns
|
| 98 |
+
left_col, right_col = st.columns([3, 2])
|
| 99 |
+
|
| 100 |
+
with left_col:
|
| 101 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 102 |
+
|
| 103 |
+
# Instructions
|
| 104 |
+
st.markdown("<div class='instructions'>", unsafe_allow_html=True)
|
| 105 |
+
st.markdown("""
|
| 106 |
+
🔹 Please complete all fields below
|
| 107 |
+
🔹 Face capture requires good lighting
|
| 108 |
+
🔹 Look directly at the camera
|
| 109 |
+
🔹 Remove glasses and face coverings
|
| 110 |
+
""")
|
| 111 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 112 |
+
|
| 113 |
+
# Initialize registration form
|
| 114 |
+
registration_form = face_rec.RegistrationForm()
|
| 115 |
+
|
| 116 |
+
# Step 1: Collect person name and role
|
| 117 |
+
st.markdown("<div class='step-title'><span class='step-counter'>1</span> Personal Information</div>", unsafe_allow_html=True)
|
| 118 |
+
st.markdown("<p class='input-label'>Full Name</p>", unsafe_allow_html=True)
|
| 119 |
+
person_name = st.text_input(label="", placeholder="Enter your first and last name", label_visibility="collapsed")
|
| 120 |
+
|
| 121 |
+
st.markdown("<p class='input-label'>Select Role</p>", unsafe_allow_html=True)
|
| 122 |
+
role = st.selectbox(label="", options=("Student", "Teacher"), label_visibility="collapsed")
|
| 123 |
+
|
| 124 |
+
# Additional information (optional)
|
| 125 |
+
with st.expander("Additional Information (Optional)"):
|
| 126 |
+
st.text_input("Email Address")
|
| 127 |
+
st.text_input("ID Number")
|
| 128 |
+
col1, col2 = st.columns(2)
|
| 129 |
+
with col1:
|
| 130 |
+
st.date_input("Date of Birth")
|
| 131 |
+
with col2:
|
| 132 |
+
st.selectbox("Department", ["Computer Science", "Engineering", "Business", "Arts", "Science"])
|
| 133 |
+
|
| 134 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 135 |
+
|
| 136 |
+
with right_col:
|
| 137 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 138 |
+
|
| 139 |
+
# Step 2: Collect facial embedding of that person
|
| 140 |
+
st.markdown("<div class='step-title'><span class='step-counter'>2</span> Face Capture</div>", unsafe_allow_html=True)
|
| 141 |
+
st.write("Please position your face in the frame and wait for the system to capture your facial features.")
|
| 142 |
+
|
| 143 |
+
# Face capture status indicators
|
| 144 |
+
status_placeholder = st.empty()
|
| 145 |
+
|
| 146 |
+
# Custom face capture function
|
| 147 |
+
def video_callback_func(frame):
|
| 148 |
+
img = frame.to_ndarray(format='bgr24')
|
| 149 |
+
reg_img, embedding = registration_form.get_embedding(img)
|
| 150 |
+
|
| 151 |
+
# Save embedding if available
|
| 152 |
+
if embedding is not None:
|
| 153 |
+
with open('face_embedding.txt', mode='ab') as f:
|
| 154 |
+
np.savetxt(f, embedding)
|
| 155 |
+
status_placeholder.success("✅ Face captured successfully!")
|
| 156 |
+
|
| 157 |
+
return av.VideoFrame.from_ndarray(reg_img, format='bgr24')
|
| 158 |
+
|
| 159 |
+
# Webcam feed
|
| 160 |
+
st.markdown("<div class='webcam-container'>", unsafe_allow_html=True)
|
| 161 |
+
webrtc_streamer(
|
| 162 |
+
key='registration',
|
| 163 |
+
video_frame_callback=video_callback_func,
|
| 164 |
+
rtc_configuration={"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]},
|
| 165 |
+
media_stream_constraints={"video": True, "audio": False},
|
| 166 |
+
)
|
| 167 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 168 |
+
|
| 169 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 170 |
+
|
| 171 |
+
# Step 3: Submit button and save the data
|
| 172 |
+
st.markdown("<div class='card'>", unsafe_allow_html=True)
|
| 173 |
+
st.markdown("<div class='step-title'><span class='step-counter'>3</span> Complete Registration</div>", unsafe_allow_html=True)
|
| 174 |
+
|
| 175 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 176 |
+
with col2:
|
| 177 |
+
if st.button("Register User", key="submit_button", use_container_width=True):
|
| 178 |
+
with st.spinner("Processing registration..."):
|
| 179 |
+
time.sleep(1) # Simulate processing
|
| 180 |
+
return_val = registration_form.save_data_in_redis_db(person_name, role)
|
| 181 |
+
|
| 182 |
+
if return_val == True:
|
| 183 |
+
st.success(f"✅ {person_name} registered successfully!")
|
| 184 |
+
st.balloons()
|
| 185 |
+
# Add a timer for redirecting
|
| 186 |
+
st.write("Redirecting to home page in 5 seconds...")
|
| 187 |
+
time.sleep(5)
|
| 188 |
+
st.switch_page("home.py")
|
| 189 |
+
elif return_val == 'name_false':
|
| 190 |
+
st.error("❌ Please enter a valid name. Name cannot be empty or contain only spaces.")
|
| 191 |
+
elif return_val == 'file_false':
|
| 192 |
+
st.error("❌ Face embedding data not found. Please refresh the page and try again.")
|
| 193 |
+
|
| 194 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 195 |
+
|
| 196 |
+
# Footer
|
| 197 |
+
st.markdown("<div class='footer'>AI-Powered Attendance System • © 2025</div>", unsafe_allow_html=True)
|
pages/3_report.py
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import datetime
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
from home import face_rec
|
| 6 |
+
import time
|
| 7 |
+
|
| 8 |
+
# Page configuration
|
| 9 |
+
st.set_page_config(
|
| 10 |
+
page_title="Attendance Dashboard",
|
| 11 |
+
layout="wide",
|
| 12 |
+
initial_sidebar_state="expanded"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# Custom CSS for better styling
|
| 16 |
+
st.markdown("""
|
| 17 |
+
<style>
|
| 18 |
+
.main-header {
|
| 19 |
+
font-size: 2.5rem;
|
| 20 |
+
color: #1E3A8A;
|
| 21 |
+
margin-bottom: 1rem;
|
| 22 |
+
text-align: center;
|
| 23 |
+
padding: 1rem;
|
| 24 |
+
border-bottom: 2px solid #E5E7EB;
|
| 25 |
+
}
|
| 26 |
+
.sub-header {
|
| 27 |
+
font-size: 1.8rem;
|
| 28 |
+
color: #1E3A8A;
|
| 29 |
+
margin-top: 1rem;
|
| 30 |
+
}
|
| 31 |
+
.card {
|
| 32 |
+
background-color: #F9FAFB;
|
| 33 |
+
border-radius: 10px;
|
| 34 |
+
padding: 1.5rem;
|
| 35 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 36 |
+
margin-bottom: 1rem;
|
| 37 |
+
}
|
| 38 |
+
.metric-card {
|
| 39 |
+
background-color: #EFF6FF;
|
| 40 |
+
border-radius: 10px;
|
| 41 |
+
padding: 1rem;
|
| 42 |
+
text-align: center;
|
| 43 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
| 44 |
+
}
|
| 45 |
+
.metric-value {
|
| 46 |
+
font-size: 2rem;
|
| 47 |
+
font-weight: bold;
|
| 48 |
+
color: #1E40AF;
|
| 49 |
+
}
|
| 50 |
+
.metric-label {
|
| 51 |
+
font-size: 1rem;
|
| 52 |
+
color: #6B7280;
|
| 53 |
+
}
|
| 54 |
+
.stTabs [data-baseweb="tab-list"] {
|
| 55 |
+
gap: 10px;
|
| 56 |
+
}
|
| 57 |
+
.stTabs [data-baseweb="tab"] {
|
| 58 |
+
border-radius: 4px 4px 0px 0px;
|
| 59 |
+
padding: 10px 16px;
|
| 60 |
+
background-color: #F3F4F6;
|
| 61 |
+
}
|
| 62 |
+
.stTabs [aria-selected="true"] {
|
| 63 |
+
background-color: #DBEAFE;
|
| 64 |
+
border-bottom: 2px solid #2563EB;
|
| 65 |
+
}
|
| 66 |
+
.dataframe {
|
| 67 |
+
font-size: 14px;
|
| 68 |
+
}
|
| 69 |
+
.refresh-btn {
|
| 70 |
+
background-color: #3B82F6;
|
| 71 |
+
color: white;
|
| 72 |
+
font-weight: bold;
|
| 73 |
+
border-radius: 6px;
|
| 74 |
+
padding: 8px 16px;
|
| 75 |
+
}
|
| 76 |
+
.filter-section {
|
| 77 |
+
background-color: #F9FAFB;
|
| 78 |
+
padding: 15px;
|
| 79 |
+
border-radius: 10px;
|
| 80 |
+
margin-bottom: 20px;
|
| 81 |
+
}
|
| 82 |
+
</style>
|
| 83 |
+
""", unsafe_allow_html=True)
|
| 84 |
+
|
| 85 |
+
# Header section
|
| 86 |
+
st.markdown('<h1 class="main-header">Attendance Management Dashboard</h1>', unsafe_allow_html=True)
|
| 87 |
+
|
| 88 |
+
# Function to load logs
|
| 89 |
+
def load_logs(name, end=-1):
|
| 90 |
+
with st.spinner('Loading attendance logs...'):
|
| 91 |
+
logs_list = face_rec.r.lrange(name, start=0, end=end)
|
| 92 |
+
return logs_list
|
| 93 |
+
|
| 94 |
+
# Function to process logs into DataFrame
|
| 95 |
+
def process_logs(logs_list):
|
| 96 |
+
# Convert bytes to string
|
| 97 |
+
logs_list_string = [log.decode('utf-8') for log in logs_list]
|
| 98 |
+
|
| 99 |
+
# Split string by @ and create nested list
|
| 100 |
+
logs_nested_list = [log.split('@') for log in logs_list_string]
|
| 101 |
+
|
| 102 |
+
# Convert nested list into dataframe
|
| 103 |
+
logs_df = pd.DataFrame(logs_nested_list, columns=['Name', 'Role', 'Timestamp'])
|
| 104 |
+
|
| 105 |
+
# Time based analysis
|
| 106 |
+
logs_df['Timestamp'] = pd.to_datetime(logs_df['Timestamp'], format='mixed')
|
| 107 |
+
logs_df['Date'] = logs_df['Timestamp'].dt.date
|
| 108 |
+
|
| 109 |
+
# Calculate Intime and outtime
|
| 110 |
+
report_df = logs_df.groupby(by=['Date', 'Name', 'Role']).agg(
|
| 111 |
+
In_time=pd.NamedAgg('Timestamp', 'min'),
|
| 112 |
+
Out_time=pd.NamedAgg('Timestamp', 'max')
|
| 113 |
+
).reset_index()
|
| 114 |
+
|
| 115 |
+
report_df['In_time'] = pd.to_datetime(report_df['In_time'])
|
| 116 |
+
report_df['Out_time'] = pd.to_datetime(report_df['Out_time'])
|
| 117 |
+
report_df['duration'] = report_df['Out_time'] - report_df['In_time']
|
| 118 |
+
|
| 119 |
+
# Mark attendance status
|
| 120 |
+
all_dates = report_df['Date'].unique()
|
| 121 |
+
name_role = report_df[['Name', 'Role']].drop_duplicates().values.tolist()
|
| 122 |
+
date_name_role_zip = []
|
| 123 |
+
for dt in all_dates:
|
| 124 |
+
for name, role in name_role:
|
| 125 |
+
date_name_role_zip.append([dt, name, role])
|
| 126 |
+
|
| 127 |
+
full_df = pd.DataFrame(date_name_role_zip, columns=['Date', 'Name', 'Role'])
|
| 128 |
+
full_df = pd.merge(full_df, report_df, how='left', on=['Date', 'Name', 'Role'])
|
| 129 |
+
full_df['Duration_seconds'] = full_df['duration'].dt.total_seconds()
|
| 130 |
+
full_df['Duration_hours'] = full_df['Duration_seconds'] / (60 * 60)
|
| 131 |
+
|
| 132 |
+
def status_marker(x):
|
| 133 |
+
if pd.isna(x):
|
| 134 |
+
return 'Absent'
|
| 135 |
+
elif x >= 0 and x < 1:
|
| 136 |
+
return 'Absent (Less than 1 hour)'
|
| 137 |
+
elif x >= 1 and x < 4:
|
| 138 |
+
return 'Half Day (less than 4 hours)'
|
| 139 |
+
elif x >= 4 and x < 6:
|
| 140 |
+
return 'Half Day'
|
| 141 |
+
elif x >= 6:
|
| 142 |
+
return "Present"
|
| 143 |
+
|
| 144 |
+
full_df['Status'] = full_df['Duration_hours'].apply(status_marker)
|
| 145 |
+
return logs_df, full_df
|
| 146 |
+
|
| 147 |
+
# Create tabs with more visual appeal
|
| 148 |
+
tab1, tab2, tab3 = st.tabs(["📊 Dashboard", "📋 Attendance Records", "🔍 Search & Filter"])
|
| 149 |
+
|
| 150 |
+
# Tab 1: Dashboard
|
| 151 |
+
with tab1:
|
| 152 |
+
st.markdown('<div class="card">', unsafe_allow_html=True)
|
| 153 |
+
st.markdown('<h2 class="sub-header">Registered Personnel</h2>', unsafe_allow_html=True)
|
| 154 |
+
|
| 155 |
+
col1, col2 = st.columns([1, 3])
|
| 156 |
+
|
| 157 |
+
with col1:
|
| 158 |
+
if st.button('Refresh Data', key='refresh_data', help="Load the latest data from database"):
|
| 159 |
+
with st.spinner('Retrieving data from database...'):
|
| 160 |
+
redis_face_db = face_rec.retrive_data(name='academy:register')
|
| 161 |
+
total_registered = len(redis_face_db)
|
| 162 |
+
st.success(f"Successfully loaded {total_registered} records")
|
| 163 |
+
|
| 164 |
+
with col2:
|
| 165 |
+
try:
|
| 166 |
+
redis_face_db = face_rec.retrive_data(name='academy:register')
|
| 167 |
+
st.dataframe(redis_face_db[['Name', 'Role']], use_container_width=True)
|
| 168 |
+
|
| 169 |
+
# Count by role for visualization
|
| 170 |
+
role_counts = redis_face_db['Role'].value_counts().reset_index()
|
| 171 |
+
role_counts.columns = ['Role', 'Count']
|
| 172 |
+
|
| 173 |
+
fig = px.pie(role_counts, values='Count', names='Role',
|
| 174 |
+
title='Distribution by Role',
|
| 175 |
+
color_discrete_sequence=px.colors.qualitative.Pastel)
|
| 176 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 177 |
+
except Exception as e:
|
| 178 |
+
st.info("Click 'Refresh Data' to load registered personnel")
|
| 179 |
+
|
| 180 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 181 |
+
|
| 182 |
+
# Recent attendance logs
|
| 183 |
+
st.markdown('<div class="card">', unsafe_allow_html=True)
|
| 184 |
+
st.markdown('<h2 class="sub-header">Recent Attendance Activity</h2>', unsafe_allow_html=True)
|
| 185 |
+
|
| 186 |
+
if st.button('Load Recent Activity', key='load_recent'):
|
| 187 |
+
logs_list = load_logs(name='attendance:logs', end=10)
|
| 188 |
+
if logs_list:
|
| 189 |
+
st.success(f"Showing {len(logs_list)} recent logs")
|
| 190 |
+
logs_list_string = [log.decode('utf-8') for log in logs_list]
|
| 191 |
+
|
| 192 |
+
# Create a more structured view of logs
|
| 193 |
+
for i, log in enumerate(logs_list_string):
|
| 194 |
+
parts = log.split('@')
|
| 195 |
+
if len(parts) >= 3:
|
| 196 |
+
name, role, timestamp = parts[0], parts[1], parts[2]
|
| 197 |
+
col1, col2, col3 = st.columns([1, 1, 2])
|
| 198 |
+
with col1:
|
| 199 |
+
st.write(f"**{name}**")
|
| 200 |
+
with col2:
|
| 201 |
+
st.write(f"{role}")
|
| 202 |
+
with col3:
|
| 203 |
+
st.write(f"{timestamp}")
|
| 204 |
+
st.divider()
|
| 205 |
+
else:
|
| 206 |
+
st.info("No recent logs found")
|
| 207 |
+
|
| 208 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 209 |
+
|
| 210 |
+
# Tab 2: Attendance Records
|
| 211 |
+
with tab2:
|
| 212 |
+
st.markdown('<div class="card">', unsafe_allow_html=True)
|
| 213 |
+
st.markdown('<h2 class="sub-header">Complete Attendance Report</h2>', unsafe_allow_html=True)
|
| 214 |
+
|
| 215 |
+
if st.button('Generate Full Report', key='gen_report'):
|
| 216 |
+
with st.spinner("Processing attendance data..."):
|
| 217 |
+
# Load and process logs
|
| 218 |
+
logs_list = load_logs(name='attendance:logs')
|
| 219 |
+
if logs_list:
|
| 220 |
+
logs_df, full_df = process_logs(logs_list)
|
| 221 |
+
|
| 222 |
+
# Summary metrics
|
| 223 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 224 |
+
with col1:
|
| 225 |
+
st.markdown('<div class="metric-card">', unsafe_allow_html=True)
|
| 226 |
+
st.markdown(f'<div class="metric-value">{len(full_df["Date"].unique())}</div>', unsafe_allow_html=True)
|
| 227 |
+
st.markdown('<div class="metric-label">Total Days</div>', unsafe_allow_html=True)
|
| 228 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 229 |
+
|
| 230 |
+
with col2:
|
| 231 |
+
st.markdown('<div class="metric-card">', unsafe_allow_html=True)
|
| 232 |
+
st.markdown(f'<div class="metric-value">{len(full_df["Name"].unique())}</div>', unsafe_allow_html=True)
|
| 233 |
+
st.markdown('<div class="metric-label">Total Personnel</div>', unsafe_allow_html=True)
|
| 234 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 235 |
+
|
| 236 |
+
with col3:
|
| 237 |
+
present_count = len(full_df[full_df['Status'] == 'Present'])
|
| 238 |
+
st.markdown('<div class="metric-card">', unsafe_allow_html=True)
|
| 239 |
+
st.markdown(f'<div class="metric-value">{present_count}</div>', unsafe_allow_html=True)
|
| 240 |
+
st.markdown('<div class="metric-label">Present Records</div>', unsafe_allow_html=True)
|
| 241 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 242 |
+
|
| 243 |
+
with col4:
|
| 244 |
+
absent_count = len(full_df[full_df['Status'] == 'Absent'])
|
| 245 |
+
st.markdown('<div class="metric-card">', unsafe_allow_html=True)
|
| 246 |
+
st.markdown(f'<div class="metric-value">{absent_count}</div>', unsafe_allow_html=True)
|
| 247 |
+
st.markdown('<div class="metric-label">Absent Records</div>', unsafe_allow_html=True)
|
| 248 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 249 |
+
|
| 250 |
+
# Status distribution visualization
|
| 251 |
+
status_counts = full_df['Status'].value_counts().reset_index()
|
| 252 |
+
status_counts.columns = ['Status', 'Count']
|
| 253 |
+
|
| 254 |
+
fig = px.bar(status_counts, x='Status', y='Count',
|
| 255 |
+
title='Attendance Status Distribution',
|
| 256 |
+
color='Status',
|
| 257 |
+
color_discrete_sequence=px.colors.qualitative.Bold)
|
| 258 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 259 |
+
|
| 260 |
+
# Display the complete report table
|
| 261 |
+
st.subheader("Detailed Attendance Records")
|
| 262 |
+
|
| 263 |
+
# Format columns for better display
|
| 264 |
+
display_df = full_df.copy()
|
| 265 |
+
display_df['Date'] = pd.to_datetime(display_df['Date']).dt.strftime('%Y-%m-%d')
|
| 266 |
+
display_df['In_time'] = pd.to_datetime(display_df['In_time']).dt.strftime('%H:%M:%S')
|
| 267 |
+
display_df['Out_time'] = pd.to_datetime(display_df['Out_time']).dt.strftime('%H:%M:%S')
|
| 268 |
+
display_df['Duration_hours'] = display_df['Duration_hours'].round(2)
|
| 269 |
+
|
| 270 |
+
# Choose columns to display
|
| 271 |
+
display_cols = ['Date', 'Name', 'Role', 'In_time', 'Out_time', 'Duration_hours', 'Status']
|
| 272 |
+
st.dataframe(display_df[display_cols], use_container_width=True)
|
| 273 |
+
|
| 274 |
+
# Option to download the report
|
| 275 |
+
csv = display_df[display_cols].to_csv(index=False).encode('utf-8')
|
| 276 |
+
st.download_button(
|
| 277 |
+
label="Download Report as CSV",
|
| 278 |
+
data=csv,
|
| 279 |
+
file_name=f"attendance_report_{datetime.datetime.now().strftime('%Y%m%d')}.csv",
|
| 280 |
+
mime="text/csv",
|
| 281 |
+
)
|
| 282 |
+
else:
|
| 283 |
+
st.warning("No attendance logs found. Make sure the system is recording attendance.")
|
| 284 |
+
else:
|
| 285 |
+
st.info("Click 'Generate Full Report' to process and display attendance data")
|
| 286 |
+
|
| 287 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 288 |
+
|
| 289 |
+
# Tab 3: Search & Filter
|
| 290 |
+
with tab3:
|
| 291 |
+
st.markdown('<div class="card">', unsafe_allow_html=True)
|
| 292 |
+
st.markdown('<h2 class="sub-header">Search Records</h2>', unsafe_allow_html=True)
|
| 293 |
+
|
| 294 |
+
# Load data first before showing filters
|
| 295 |
+
load_data_btn = st.button("Load Data for Filtering", key="load_filter_data")
|
| 296 |
+
|
| 297 |
+
if load_data_btn:
|
| 298 |
+
with st.spinner("Loading data..."):
|
| 299 |
+
logs_list = load_logs(name='attendance:logs')
|
| 300 |
+
if logs_list:
|
| 301 |
+
logs_df, full_df = process_logs(logs_list)
|
| 302 |
+
st.session_state['full_df'] = full_df
|
| 303 |
+
st.success("Data loaded successfully! You can now filter the records.")
|
| 304 |
+
else:
|
| 305 |
+
st.warning("No attendance logs found to filter.")
|
| 306 |
+
|
| 307 |
+
if 'full_df' in st.session_state:
|
| 308 |
+
full_df = st.session_state['full_df']
|
| 309 |
+
|
| 310 |
+
st.markdown('<div class="filter-section">', unsafe_allow_html=True)
|
| 311 |
+
col1, col2 = st.columns(2)
|
| 312 |
+
|
| 313 |
+
with col1:
|
| 314 |
+
# Date filter
|
| 315 |
+
date_options = sorted([str(date) for date in full_df['Date'].unique()])
|
| 316 |
+
date_in = st.selectbox('Select Date', ['ALL'] + date_options, index=0)
|
| 317 |
+
|
| 318 |
+
# Name filter
|
| 319 |
+
name_list = sorted(full_df['Name'].unique().tolist())
|
| 320 |
+
name_in = st.selectbox('Select Name', ['ALL'] + name_list)
|
| 321 |
+
|
| 322 |
+
with col2:
|
| 323 |
+
# Role filter
|
| 324 |
+
role_list = sorted(full_df['Role'].unique().tolist())
|
| 325 |
+
role_in = st.selectbox('Select Role', ['ALL'] + role_list)
|
| 326 |
+
|
| 327 |
+
# Status filter
|
| 328 |
+
status_list = sorted(full_df['Status'].unique().tolist())
|
| 329 |
+
status_in = st.multiselect('Select Status', ['ALL'] + status_list, default=['Present'])
|
| 330 |
+
|
| 331 |
+
# Duration filter with slider
|
| 332 |
+
duration_in = st.slider('Filter by minimum duration in hours', 0, 12, 0)
|
| 333 |
+
|
| 334 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 335 |
+
|
| 336 |
+
# Apply filters
|
| 337 |
+
if st.button('Apply Filters', key='apply_filters'):
|
| 338 |
+
with st.spinner("Filtering records..."):
|
| 339 |
+
# Convert date column to string for comparison
|
| 340 |
+
filter_df = full_df.copy()
|
| 341 |
+
filter_df['Date'] = filter_df['Date'].astype(str)
|
| 342 |
+
|
| 343 |
+
# Apply each filter
|
| 344 |
+
if date_in != 'ALL':
|
| 345 |
+
filter_df = filter_df[filter_df['Date'] == date_in]
|
| 346 |
+
|
| 347 |
+
if name_in != 'ALL':
|
| 348 |
+
filter_df = filter_df[filter_df['Name'] == name_in]
|
| 349 |
+
|
| 350 |
+
if role_in != 'ALL':
|
| 351 |
+
filter_df = filter_df[filter_df['Role'] == role_in]
|
| 352 |
+
|
| 353 |
+
if duration_in > 0:
|
| 354 |
+
filter_df = filter_df[filter_df['Duration_hours'] > duration_in]
|
| 355 |
+
|
| 356 |
+
if 'ALL' not in status_in and len(status_in) > 0:
|
| 357 |
+
filter_df = filter_df[filter_df['Status'].isin(status_in)]
|
| 358 |
+
|
| 359 |
+
if len(filter_df) > 0:
|
| 360 |
+
# Format display columns
|
| 361 |
+
display_df = filter_df.copy()
|
| 362 |
+
display_df['Date'] = pd.to_datetime(display_df['Date']).dt.strftime('%Y-%m-%d') if 'datetime' in str(type(display_df['Date'].iloc[0])) else display_df['Date']
|
| 363 |
+
if not pd.isna(display_df['In_time'].iloc[0]):
|
| 364 |
+
display_df['In_time'] = pd.to_datetime(display_df['In_time']).dt.strftime('%H:%M:%S')
|
| 365 |
+
display_df['Out_time'] = pd.to_datetime(display_df['Out_time']).dt.strftime('%H:%M:%S')
|
| 366 |
+
display_df['Duration_hours'] = display_df['Duration_hours'].round(2)
|
| 367 |
+
|
| 368 |
+
display_cols = ['Date', 'Name', 'Role', 'In_time', 'Out_time', 'Duration_hours', 'Status']
|
| 369 |
+
st.dataframe(display_df[display_cols], use_container_width=True)
|
| 370 |
+
|
| 371 |
+
# Option to download filtered results
|
| 372 |
+
csv = display_df[display_cols].to_csv(index=False).encode('utf-8')
|
| 373 |
+
st.download_button(
|
| 374 |
+
label="Download Filtered Results",
|
| 375 |
+
data=csv,
|
| 376 |
+
file_name=f"filtered_attendance_{datetime.datetime.now().strftime('%Y%m%d')}.csv",
|
| 377 |
+
mime="text/csv",
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
# Visual summary of filtered results
|
| 381 |
+
if len(display_df) > 1:
|
| 382 |
+
status_filtered = display_df['Status'].value_counts().reset_index()
|
| 383 |
+
status_filtered.columns = ['Status', 'Count']
|
| 384 |
+
|
| 385 |
+
fig = px.pie(status_filtered, values='Count', names='Status',
|
| 386 |
+
title='Status Distribution in Filtered Results',
|
| 387 |
+
hole=0.4,
|
| 388 |
+
color_discrete_sequence=px.colors.qualitative.Pastel)
|
| 389 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 390 |
+
else:
|
| 391 |
+
st.warning("No records match your filter criteria.")
|
| 392 |
+
else:
|
| 393 |
+
st.info("Please click 'Load Data for Filtering' first to enable search functionality.")
|
| 394 |
+
|
| 395 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 396 |
+
|
| 397 |
+
# Footer
|
| 398 |
+
st.markdown("""
|
| 399 |
+
<div style="text-align: center; margin-top: 30px; padding: 20px; background-color: #F3F4F6; border-radius: 10px;">
|
| 400 |
+
<p style="color: #6B7280; font-size: 14px;">Attendance Management System © 2025</p>
|
| 401 |
+
</div>
|
| 402 |
+
""", unsafe_allow_html=True)
|
simulated_logs.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
upload_logs.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import redis
|
| 2 |
+
|
| 3 |
+
# Connect to Redis Client
|
| 4 |
+
hostname = 'redis-18916.c83.us-east-1-2.ec2.redns.redis-cloud.com'
|
| 5 |
+
portnumber = 18916
|
| 6 |
+
password = 'vTig9W0x2XtLCyR3MDFQFyZjSMSId6Gr'
|
| 7 |
+
|
| 8 |
+
r = redis.StrictRedis(host=hostname,
|
| 9 |
+
port=portnumber,
|
| 10 |
+
password=password)
|
| 11 |
+
|
| 12 |
+
# Simulated Logs
|
| 13 |
+
with open('simulated_logs.txt', 'r') as f:
|
| 14 |
+
logs_text = f.read()
|
| 15 |
+
|
| 16 |
+
encoded_logs = logs_text.split('\n')
|
| 17 |
+
|
| 18 |
+
# Push into Redis database
|
| 19 |
+
r.lpush('attendance:logs', *encoded_logs)
|