Showing
2 changed files
with
281 additions
and
0 deletions
| ... | @@ -162,6 +162,15 @@ def verify(): | ... | @@ -162,6 +162,15 @@ def verify(): |
| 162 | attendance_db.close() | 162 | attendance_db.close() |
| 163 | return send | 163 | return send |
| 164 | 164 | ||
| 165 | +@app.route('/attendance.html') | ||
| 166 | +def tempAttendance(): | ||
| 167 | + attendance_db = pymysql.connect(read_default_file="./DB.cnf") | ||
| 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;" | ||
| 170 | + mycursor.execute(sql) | ||
| 171 | + data = mycursor.fetchall() | ||
| 172 | + return render_template('attendance.html', output_data = data) | ||
| 173 | + | ||
| 165 | @app.route('/robots.txt') | 174 | @app.route('/robots.txt') |
| 166 | def robots(): | 175 | def robots(): |
| 167 | return send_from_directory(app.static_folder, request.path[1:]) | 176 | return send_from_directory(app.static_folder, request.path[1:]) | ... | ... |
flask/templates/attendance.html
0 → 100644
| 1 | +<!doctype html> | ||
| 2 | +<html> | ||
| 3 | +<head> | ||
| 4 | +<meta charset="utf-8"> | ||
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 6 | +<title>Web Attendance System Register</title> | ||
| 7 | +<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css"> | ||
| 8 | +<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Nanum+Gothic:400,700,800&subset=korean"> | ||
| 9 | +<style>body,h1,h2,h3,h4,h5,p {font-family: "Nanum+Gothic", sans-serif}</style> | ||
| 10 | +<style> | ||
| 11 | +#container { | ||
| 12 | + margin: 15px auto; | ||
| 13 | +} | ||
| 14 | +#videoInput { | ||
| 15 | + background-color: #666; | ||
| 16 | +} | ||
| 17 | +#canvasOutput { | ||
| 18 | + background-color: #666; | ||
| 19 | +} | ||
| 20 | +#student_id { | ||
| 21 | + margin-top: 10px; | ||
| 22 | + margin-bottom: 5px; | ||
| 23 | +} | ||
| 24 | +#student_name { | ||
| 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 | + |
-
Please register or login to post a comment