Showing
2 changed files
with
21 additions
and
269 deletions
... | @@ -166,7 +166,7 @@ def verify(): | ... | @@ -166,7 +166,7 @@ def verify(): |
166 | def tempAttendance(): | 166 | def tempAttendance(): |
167 | attendance_db = pymysql.connect(read_default_file="./DB.cnf") | 167 | attendance_db = pymysql.connect(read_default_file="./DB.cnf") |
168 | cursor = attendance_db.cursor(pymysql.cursors.DictCursor) | 168 | cursor = attendance_db.cursor(pymysql.cursors.DictCursor) |
169 | - sql = "SELECT ls.student_id, s.student_name, sa.status FROM lecture_students AS ls LEFT JOIN student_attendance AS sa ON ls.student_id = sa.student_id LEFT JOIN student AS s ON ls.student_id = s.student_id;" | 169 | + sql = "SELECT ls.student_id as student_id, s.student_name as student_name, ifnull(sa.status, 'absent') as status FROM lecture_students AS ls LEFT JOIN student_attendance AS sa ON ls.student_id = sa.student_id LEFT JOIN student AS s ON ls.student_id = s.student_id;" |
170 | mycursor.execute(sql) | 170 | mycursor.execute(sql) |
171 | data = mycursor.fetchall() | 171 | data = mycursor.fetchall() |
172 | return render_template('attendance.html', output_data = data) | 172 | return render_template('attendance.html', output_data = data) | ... | ... |
1 | -<!doctype html> | 1 | +<!DOCTYPE html> |
2 | <html> | 2 | <html> |
3 | <head> | 3 | <head> |
4 | <meta charset="utf-8"> | 4 | <meta charset="utf-8"> |
5 | <meta name="viewport" content="width=device-width, initial-scale=1"> | 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
6 | -<title>Web Attendance System Register</title> | 6 | +<title>Attendance</title> |
7 | -<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css"> | 7 | +<table> |
8 | -<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nanum+Gothic:400,700,800&subset=korean"> | 8 | + <thead> |
9 | -<style>body,h1,h2,h3,h4,h5,p {font-family: "Nanum+Gothic", sans-serif}</style> | 9 | + <tr> |
10 | -<style> | 10 | + <th>학번</th> |
11 | -#container { | 11 | + <th>이름</th> |
12 | - margin: 15px auto; | 12 | + <th>출결</th> |
13 | -} | 13 | + </tr> |
14 | -#videoInput { | 14 | + </thead> |
15 | - background-color: #666; | 15 | + <tbody> |
16 | -} | 16 | + {% for row in output_data %} |
17 | -#canvasOutput { | 17 | + <tr> |
18 | - background-color: #666; | 18 | + <td>{{row['student_id']}}</td> |
19 | -} | 19 | + <td>{{row['student_name']}}</td> |
20 | -#student_id { | 20 | + <td>{{row['status']}}</td> |
21 | - margin-top: 10px; | 21 | + </tr> |
22 | - margin-bottom: 5px; | 22 | + {% endfor %} |
23 | -} | 23 | + </tbody> |
24 | -#student_name { | 24 | +</table> |
25 | - margin-bottom: 10px; | ||
26 | -} | ||
27 | -</style> | ||
28 | -<script type='text/javascript' src="{{url_for('static', filename='js/opencv.js')}}"></script> | ||
29 | -<script type='text/javascript' src="{{url_for('static', filename='js/utils.js')}}"></script> | ||
30 | -<script type='text/javascript' src="https://code.jquery.com/jquery-1.12.4.min.js"></script> | ||
31 | -<script type='text/javascript'> | ||
32 | -var b64encoded = ''; | ||
33 | -var streaming = false; | ||
34 | -function init() | ||
35 | -{ | ||
36 | - let video = document.getElementById('videoInput'); | ||
37 | - let container = document.getElementById('container'); | ||
38 | - let canvasOutput = document.getElementById("canvasOutput"); | ||
39 | - if (navigator.mediaDevices.getUserMedia){ | ||
40 | - navigator.mediaDevices.getUserMedia({ video: true }) | ||
41 | - .then(function (stream) { | ||
42 | - video.srcObject = stream; | ||
43 | - video.addEventListener('canplay', () => { | ||
44 | - var screenWidth = $(document).width(); | ||
45 | - var screenHeight = $(document).height(); | ||
46 | - var headerHeight = $('#header').height(); | ||
47 | - var inputformHeight = $('#inputForm').height(); | ||
48 | - var ratio = 1.0; | ||
49 | - video.width = video.videoWidth; | ||
50 | - video.height = video.videoHeight; | ||
51 | - if (video.width > screenWidth || headerHeight + video.height + inputformHeight > screenHeight){ | ||
52 | - ratio = Math.min(screenWidth / (video.width * 1.0), screenHeight / ((headerHeight + video.height + inputformHeight) * 1.0)); | ||
53 | - } | ||
54 | - container.style.width = Math.round(video.width * ratio) + 'px'; | ||
55 | - container.style.height = Math.round(video.height * ratio) + 'px'; | ||
56 | - canvasOutput.width = Math.round(video.width * ratio); | ||
57 | - canvasOutput.height = Math.round(video.height * ratio); | ||
58 | - load_cascade(); | ||
59 | - }); | ||
60 | - }).catch(function (err0r) { | ||
61 | - console.log("Something went wrong!"); | ||
62 | - streaming = false; | ||
63 | - }); | ||
64 | - } | ||
65 | -} | ||
66 | - | ||
67 | -function load_cascade() | ||
68 | -{ | ||
69 | - let faceCascadeFile = 'haarcascade_frontalface_default.xml' | ||
70 | - let faceCascadeURL = 'static/js/haarcascade_frontalface_default.xml' | ||
71 | - let utils = new Utils('errorMessage'); | ||
72 | - utils.createFileFromUrl(faceCascadeFile, faceCascadeURL, () => { | ||
73 | - main(); | ||
74 | - }); | ||
75 | -} | ||
76 | - | ||
77 | -function main() | ||
78 | -{ | ||
79 | - let video = document.getElementById("videoInput"); | ||
80 | - let canvasOutput = document.getElementById("canvasOutput"); | ||
81 | - let canvasContext = canvasOutput.getContext('2d'); | ||
82 | - let src = new cv.Mat(video.height, video.width, cv.CV_8UC4); | ||
83 | - let dst = new cv.Mat(video.height, video.width, cv.CV_8UC4); | ||
84 | - let msize = new cv.Size(video.width / 4, video.height / 4); | ||
85 | - let dsize = new cv.Size(canvasOutput.width, canvasOutput.height); | ||
86 | - let cap = new cv.VideoCapture(video); | ||
87 | - let faces = new cv.RectVector(); | ||
88 | - let classifier = new cv.CascadeClassifier(); | ||
89 | - | ||
90 | - class Tracker{ | ||
91 | - constructor(){ | ||
92 | - this.arr = new Array(); | ||
93 | - } | ||
94 | - register = function(x, y, width, height) { | ||
95 | - var x_center = (x + width) / 2; | ||
96 | - var y_center = (y + height) / 2; | ||
97 | - var now = Date.now() | ||
98 | - this.arr = this.arr.filter(ent => now - ent.time < 300); | ||
99 | - for (const prop in this.arr){ | ||
100 | - var prop_x_center = (this.arr[prop].x + this.arr[prop].width) / 2; | ||
101 | - var prop_y_center = (this.arr[prop].y + this.arr[prop].height) / 2; | ||
102 | - if (Math.abs(x_center - prop_x_center) < 10 && Math.abs(y_center - prop_y_center) < 10){ | ||
103 | - this.arr[prop].x = x; | ||
104 | - this.arr[prop].y = y; | ||
105 | - this.arr[prop].width = width; | ||
106 | - this.arr[prop].height = height; | ||
107 | - this.arr[prop].time = now; | ||
108 | - return this.arr[prop].init_time; | ||
109 | - } | ||
110 | - } | ||
111 | - var ent = {x: x, y: y, width: width, height: height, time: now, init_time: now} | ||
112 | - this.arr.push(ent) | ||
113 | - return now; | ||
114 | - } | ||
115 | - }; | ||
116 | - | ||
117 | - var tracker = new Tracker(); | ||
118 | - classifier.load('haarcascade_frontalface_default.xml'); | ||
119 | - const FPS = 30; | ||
120 | - function processVideo() { | ||
121 | - try { | ||
122 | - if (!streaming) { | ||
123 | - // clean and stop. | ||
124 | - src.delete(); | ||
125 | - dst.delete(); | ||
126 | - faces.delete(); | ||
127 | - classifier.delete(); | ||
128 | - return; | ||
129 | - } | ||
130 | - let begin = Date.now(); | ||
131 | - // start processing. | ||
132 | - cap.read(src); | ||
133 | - cv.flip(src, src, 1); | ||
134 | - src.copyTo(dst); | ||
135 | - // detect faces. | ||
136 | - classifier.detectMultiScale(dst, faces, 1.1, 5, 0, msize); | ||
137 | - // draw faces. | ||
138 | - for (let i = 0; i < faces.size(); ++i) { | ||
139 | - let face = faces.get(i); | ||
140 | - let point1 = new cv.Point(face.x, face.y); | ||
141 | - let point2 = new cv.Point(face.x + face.width, face.y + face.height); | ||
142 | - cv.rectangle(dst, point1, point2, [255, 0, 0, 255], 8); | ||
143 | - let cropped = new cv.Mat(); | ||
144 | - let margin_x = 0; | ||
145 | - let margin_y = 0; | ||
146 | - if (face.width > face.height) | ||
147 | - { | ||
148 | - margin_y = (face.width - face.height) / 2; | ||
149 | - } | ||
150 | - else | ||
151 | - { | ||
152 | - margin_x = (face.height - face.width) / 2; | ||
153 | - } | ||
154 | - Math.max(face.width, face.height) | ||
155 | - Math.min(face.width, face.height) | ||
156 | - let rect = new cv.Rect(Math.max(face.x-margin_x, 0), Math.max(face.y-margin_y, 0), Math.min(face.width+margin_x, src.cols), Math.min(face.height+margin_y, src.rows)); | ||
157 | - cropped = src.roi(rect); | ||
158 | - let tempCanvas = document.createElement("canvas"); | ||
159 | - cv.imshow(tempCanvas,cropped); | ||
160 | - if (Date.now() - tracker.register(face.x, face.y, face.width, face.height) > 1000){ | ||
161 | - // 1초동안 인식되면 사진 촬영 종료하고 등록 버튼 활성화 | ||
162 | - cv.rectangle(dst, point1, point2, [0, 255, 0, 255], 8); | ||
163 | - b64encoded = tempCanvas.toDataURL("image/jpeg", 0.6); | ||
164 | - toggle_streaming(); | ||
165 | - change_notice("촬영 완료! 정보를 등록해주세요<br>제대로 촬영되지 않은 경우 버튼을 눌러 다시 촬영할 수 있습니다."); | ||
166 | - activate_sender(); | ||
167 | - } | ||
168 | - } | ||
169 | - // to do resize preview | ||
170 | - cv.resize(dst, dst, dsize, 0, 0, cv.INTER_AREA); | ||
171 | - cv.imshow('canvasOutput', dst); | ||
172 | - // schedule the next one. | ||
173 | - let delay = 1000/FPS - (Date.now() - begin); | ||
174 | - setTimeout(processVideo, delay); | ||
175 | - } catch (err) { | ||
176 | - console.log(err); | ||
177 | - } | ||
178 | - } | ||
179 | - setTimeout(processVideo, 0); | ||
180 | -} | ||
181 | - | ||
182 | -function activate_sender() | ||
183 | -{ | ||
184 | - let sender = document.getElementById("sender"); | ||
185 | - sender.disabled = false; | ||
186 | -} | ||
187 | - | ||
188 | -function toggle_streaming() | ||
189 | -{ | ||
190 | - let streamButton = document.getElementById("streamButton"); | ||
191 | - streaming = !streaming; | ||
192 | - if (streaming){ | ||
193 | - streamButton.value = "촬영중지"; | ||
194 | - change_notice("얼굴이 인식되면 얼굴을 촬영합니다"); | ||
195 | - } | ||
196 | - else{ | ||
197 | - streamButton.value = "촬영시작"; | ||
198 | - change_notice("촬영 시작 버튼을 누르면 얼굴을 촬영합니다"); | ||
199 | - } | ||
200 | - main(); | ||
201 | -} | ||
202 | - | ||
203 | -function change_notice(text) | ||
204 | -{ | ||
205 | - let notice = document.getElementById("notice"); | ||
206 | - notice.innerHTML = text; | ||
207 | -} | ||
208 | - | ||
209 | -function submit() | ||
210 | -{ | ||
211 | - let student_id = document.getElementById('student_id').value; | ||
212 | - let student_name = document.getElementById('student_name').value; | ||
213 | - if (b64encoded === '') | ||
214 | - { | ||
215 | - alert("얼굴을 먼저 촬영해주세요"); | ||
216 | - return; | ||
217 | - } | ||
218 | - if (!(student_id.length && student_name.length)) | ||
219 | - { | ||
220 | - alert("학번과 이름을 입력해주세요"); | ||
221 | - return; | ||
222 | - } | ||
223 | - b64encoded = b64encoded.replace('data:image/jpeg;base64,', '') | ||
224 | - $.ajax({ | ||
225 | - type: "POST", | ||
226 | - url: "/register", | ||
227 | - dataType: "json", | ||
228 | - data: {'image':b64encoded, 'student_id':student_id, 'student_name':student_name}, | ||
229 | - success: function(data){ | ||
230 | - if (data.status == "success"){ | ||
231 | - alert("등록 성공"); | ||
232 | - } | ||
233 | - else if (data.status == "already"){ | ||
234 | - alert("등록 실패: 이미 등록된 학번입니다. 학번을 변경해주세요."); | ||
235 | - } | ||
236 | - } | ||
237 | - }) | ||
238 | -} | ||
239 | - | ||
240 | -</script> | ||
241 | -</head> | ||
242 | -<body onload="cv['onRuntimeInitialized']=()=>{init();};" class="w3-light-grey"> | ||
243 | -<!-- w3-content defines a container for fixed size centered content, | ||
244 | -and is wrapped around the whole page content, except for the footer in this example --> | ||
245 | - <div class="w3-content"> | ||
246 | - <!-- Header --> | ||
247 | - <header id="header" class="w3-container w3-center"> | ||
248 | - <h1><b>얼굴 등록</b></h1> | ||
249 | - <p>Made by <span class="w3-tag">정해갑</span></p> | ||
250 | - </header> | ||
251 | - | ||
252 | - <div class="w3-row", style='text-align:center'> | ||
253 | - <h2 id="notice">촬영 시작 버튼을 누르면 얼굴을 촬영합니다</h2> | ||
254 | - <input id="streamButton" type="button" onclick="toggle_streaming()" value="활영시작"> | ||
255 | - <div id="container"> | ||
256 | - <video autoplay="true" id="videoInput" style="display: none; object-fit:cover;"></video> | ||
257 | - <canvas id="canvasOutput">/canvas> | ||
258 | - </div> | ||
259 | - <div id="inputForm"> | ||
260 | - <strong>얼굴 이미지는 서버에 저장되지 않습니다</strong><br> | ||
261 | - <strong>(복원 불가능한 512차원 벡터로 변환됩니다)</strong><br> | ||
262 | - <strong>학번과 이름은 임의로 입력해주세요</strong><br> | ||
263 | - <strong>예)1234/홍길동 등</strong><br> | ||
264 | - 학번: <input type="text" id="student_id"><br> | ||
265 | - 이름: <input type="text" id="student_name"><br> | ||
266 | - <input id="sender" type="button" onclick="submit()" value="등록" disabled> | ||
267 | - </div> | ||
268 | - </div> | ||
269 | - </div> | ||
270 | -</body> | ||
271 | -</html> | ||
272 | - | ||
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
-
Please register or login to post a comment