2021.08.09 - [프로젝트/2021 한이음 OpenCV를 이용한 얼굴인식 기반 도어락 제작 프로젝트] - 라즈베리파이와 원격 서버 간의 통신을 위한 서버 연결
2021.06.23 - [프로젝트/2021 한이음 OpenCV를 이용한 얼굴인식 기반 도어락 제작 프로젝트] - 라즈베리파이 카메라 모듈 사용 오류 해결
길고 긴 프로젝트 활동이 끝났다.
최종 시안까지 마치고, 코드 분석 및 그동안의 활동을 리뷰하면서 포스팅을 하려고 한다!
필요한 재료 및 소프트웨어 클라우드 구상 :
- 라즈베리파이 키트(초음파, 온도 센서 모듈 포함) + 해당 프로젝트의 경우 터치 스크린도 포함
- 네이버 클라우드 서버
- 도어락, 도어락 제어용 아두이노
- 문, 문틀 용 나무 자재
프로젝트 목적
: 라즈베리파이로 동작, 얼굴 인식으로 잠금을 해제할 수 있는 도어락 만들기
세부 조건 및 동작 설명
1) 라즈베리파이의 카메라 모듈에서 사람의 얼굴을 인식
2) 사용자 등록(웹에서 구현)이 된 얼굴이라면 온도와 출입자의 정보를 웹 서버로 전송
3) 웹에서는 회원가입 및 로그인, 등록된 사용자의 정보와 출입 로그, 원격 도어락 해제의 기능이 있다.
구현 과정
1. 얼굴 인식 API 가져오기
Python 머신러닝으로 얼굴을 분석해 얼굴 인식을 수행, OpenCV를 활용해서
데이터셋과 대조해 누구인지 출력하는 오픈소스를 사용
https://thecodingnote.tistory.com/8
라즈베리파이 서버에서 cmake, dlib, face_recognition 모듈을 설치한다.
(라파 사양 탓인지 모듈 설치에서 굉장히 오랜 시간이 걸렸다.)
import face_recognition
import cv2
import numpy as np
# This is a demo of running face recognition on live video from your webcam. It's a little more complicated than the
# other example, but it includes some basic performance tweaks to make things run a lot faster:
# 1. Process each video frame at 1/4 resolution (though still display it at full resolution)
# 2. Only detect faces in every other frame of video.
# PLEASE NOTE: This example requires OpenCV (the `cv2` library) to be installed only to read from your webcam.
# OpenCV is *not* required to use the face_recognition library. It's only required if you want to run this
# specific demo. If you have trouble installing it, try any of the other demos that don't require it instead.
# Get a reference to webcam #0 (the default one)
video_capture = cv2.VideoCapture(0)
# Load a sample picture and learn how to recognize it.
obama_image = face_recognition.load_image_file("obama.jpg")
obama_face_encoding = face_recognition.face_encodings(obama_image)[0]
# Load a second sample picture and learn how to recognize it.
biden_image = face_recognition.load_image_file("biden.jpg")
biden_face_encoding = face_recognition.face_encodings(biden_image)[0]
# Create arrays of known face encodings and their names
known_face_encodings = [
obama_face_encoding,
biden_face_encoding
]
known_face_names = [
"Barack Obama",
"Joe Biden"
]
# Initialize some variables
face_locations = []
face_encodings = []
face_names = []
process_this_frame = True
while True:
# Grab a single frame of video
ret, frame = video_capture.read()
# Resize frame of video to 1/4 size for faster face recognition processing
small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
# Convert the image from BGR color (which OpenCV uses) to RGB color (which face_recognition uses)
rgb_small_frame = small_frame[:, :, ::-1]
# Only process every other frame of video to save time
if process_this_frame:
# Find all the faces and face encodings in the current frame of video
face_locations = face_recognition.face_locations(rgb_small_frame)
face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations)
face_names = []
for face_encoding in face_encodings:
# See if the face is a match for the known face(s)
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
name = "Unknown"
# # If a match was found in known_face_encodings, just use the first one.
# if True in matches:
# first_match_index = matches.index(True)
# name = known_face_names[first_match_index]
# Or instead, use the known face with the smallest distance to the new face
face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = known_face_names[best_match_index]
face_names.append(name)
process_this_frame = not process_this_frame
# Display the results
for (top, right, bottom, left), name in zip(face_locations, face_names):
# Scale back up face locations since the frame we detected in was scaled to 1/4 size
top *= 4
right *= 4
bottom *= 4
left *= 4
# Draw a box around the face
cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)
# Draw a label with a name below the face
cv2.rectangle(frame, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED)
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (left + 6, bottom - 6), font, 1.0, (255, 255, 255), 1)
# Display the resulting image
cv2.imshow('Video', frame)
# Hit 'q' on the keyboard to quit!
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# Release handle to the webcam
video_capture.release()
cv2.destroyAllWindows()
라즈베리파이에 카메라 모듈 설치 후 저장된 이미지를 인코딩 된 상태로 변수에 저장,
비디오에서 읽어온 이미지 변수(frame)을 resize한 뒤(opencv 모듈 활용) small_frame 변수에 저장하였다.
OpenCV는 RGB가 아닌 BGR 방식을 사용하기 때문에
rgb_small_frame = small_frame[:, :, ::-1]
다음 코드로 재정렬, 새로운 변수에 넣는다.
오픈소스라 자세한 설명은 위를 참고하도록 하고,
이 얼굴 인식 API를 라즈베리파이 서버에서 실행하는 것까지 테스트를 완료한다.
2. 라즈베리파이 개발 환경 설치하기
라즈베리파이 공식 홈페이지에서 라즈베리파이 OS를 다운로드
해당 프로젝트에서는 Raspbain OS를 설치하였다.
다운로드한 OS파일을 SD 카드에 write해서 라파에 설치!
https://upcake.tistory.com/327
참고하기
카메라 모듈 사용에서 오류가 떠 해결
https://mingyum119.tistory.com/79?category=925797
로컬과 같은 과정으로, 라즈베리파이에 부착된 카메라 센서 모듈로 얼굴 인식 테스트에 성공
3. 원격 서버 구축 및 소켓 통신으로 서버와 라즈베리파이 간의 데이터 전송
한이음에서 지원해준 클라우드 서비스로, 네이버 클라우스 서버를 구축하였다.
뭣 모르고 리눅스 계열의 우분투가 아닌 윈도우 서버를 구축했는데
1) 포트 포워딩이 힘듦
2) 현재 프로세스의 정보를 복제하는 'fork'를 할 수 없다.
위 두가지 이유로 개발하면서 두고두고 후회했다.
리눅스 서버 구축하는 것을 추천 ;_;
포트를 지정해서 VPC와 Subnet을 생성하였다.
리눅스의 포트포워딩과 비교하면 갱장히 어려웠던 과정 ..
자세한 서버 구축 관련 포스팅은 추후 업로드 예정 .. !
다음은 소켓 통신
https://mingyum119.tistory.com/97
서버와 클라이언트 간의 데이터 통신은 오픈소스를 활용하였다.
<클라이언트에서 보낸 메세지를 서버에서 수신하여 콘솔에 출력하는 예제>
import socket
# 접속할 서버 주소입니다. 여기에서는 루프백(loopback) 인터페이스 주소 즉 localhost를 사용합니다.
HOST = ''
# 클라이언트 접속을 대기하는 포트 번호입니다.
PORT = 31004
# 소켓 객체를 생성합니다.
# 주소 체계(address family)로 IPv4, 소켓 타입으로 TCP 사용합니다.
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("step1")
# 포트 사용중이라 연결할 수 없다는
# WinError 10048 에러 해결를 위해 필요합니다.
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
print("step2")
# bind 함수는 소켓을 특정 네트워크 인터페이스와 포트 번호에 연결하는데 사용됩니다.
# HOST는 hostname, ip address, 빈 문자열 ""이 될 수 있습니다.
# 빈 문자열이면 모든 네트워크 인터페이스로부터의 접속을 허용합니다.
# PORT는 1-65535 사이의 숫자를 사용할 수 있습니다.
server_socket.bind((HOST, PORT))
print("step3")
# 서버가 클라이언트의 접속을 허용하도록 합니다.
server_socket.listen()
print("sever ready")
# accept 함수에서 대기하다가 클라이언트가 접속하면 새로운 소켓을 리턴합니다.
client_socket, addr = server_socket.accept()
# 접속한 클라이언트의 주소입니다.
print('Connected by', addr)
# 무한루프를 돌면서
while True:
# 클라이언트가 보낸 메시지를 수신하기 위해 대기합니다.
data = client_socket.recv(1024)
# 빈 문자열을 수신하면 루프를 중지합니다.
if not data:
break
# 수신받은 문자열을 출력합니다.
print('Received from', addr, data.decode())
# 받은 문자열을 다시 클라이언트로 전송해줍니다.(에코)
client_socket.sendall(data)
# 소켓을 닫습니다.
client_socket.close()
server_socket.close()
원격 서버에 저장된 Server.py 코드
import socket
# 서버의 주소입니다. hostname 또는 ip address를 사용할 수 있습니다.
HOST = '원격 서버의 공인 IP'
# 서버에서 지정해 놓은 포트 번호입니다.
PORT = 31004
# 소켓 객체를 생성합니다.
# 주소 체계(address family)로 IPv4, 소켓 타입으로 TCP 사용합니다.
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 지정한 HOST와 PORT를 사용하여 서버에 접속합니다.
client_socket.connect((HOST, PORT))
# 메시지를 전송합니다.
client_socket.sendall('안녕'.encode())
# 메시지를 수신합니다.
data = client_socket.recv(1024)
print('Received', repr(data.decode()))
# 소켓을 닫습니다.
client_socket.close()
라즈베리파이에 저장된 Client.py 코드
포트는 Client와 Server가 동일하게만 지정하면 된다고 하였음
이제 수신 정보만 잘 변형해서 라즈베리파이에 저장되는 정보(출입자 이름, 온도)를 적절히 렌더링 해 넘기면 된다
우리 프로젝트의 데이터 흐름 설계는 다음과 같다
0. 웹 서버에서 라즈베리파이로 휴대폰에 있는 사진을 전송 후 라즈베리파이에서 인코딩된 벡터값을 라즈베리파이 서버에 저장
1. 아두이노에서 if(distance < 30) 안에서 물체가 도어락 앞에 왔다는 신호(온도 값을 json 파일로 전송)를 라즈베리파이에 보내줌 (text파일 형태로 전송)
2. 라파에서 이 신호를 받으면
1) text파일로 전송받은 파일을 convertTxtToJson.py에서 Json 파일로 변환한 다음 JSON 리스트에 저장
2) 비디오 촬영, 촬영 시 인코딩된 값인 비디오 촬영 결괏값과 기존에 등록한 사용자 사진(인코딩된 값)의 벡터값과 비교해 일치하는 사진이 있는지 확인 -> blank.py에서 이를 처리
3. 일치하는 사진이 없으면 실패, 일치하는 사진이 있으면 라즈베리파이에서 아두이노로 True 신호값 전송해 아두이노에서 도어락 잠금해제
- 라파에서 아두이노로 이 True 신호값 보내는 시점에서 현재시간 값과 일치하는 사용자명을 따로 저장 -> JSON 파일에 key, value 추가
4. 라파에서 아두이노로 보낸 신호값에 의해 도어락 잠금이 해제되고, 웹 서버는 라파로부터 JSON파일을 소켓으로 전송받아 저장
-> JSON 파일에는 도어락 해제 시간(time), 인식된 사용자 명(name), 체온값(temperature)이 최종적으로 저장(더 필요한 정보가 있나?)
6. 이 JSON 파일을 웹 서버에서 출력
+ JSON파일은 하나의 리스트에 처리해서 프론트 단에서 for문을 돌려 웹에서 표시
소켓 코드를 고쳐서, 웹 서버에서 사용자가 이름과 본인 사진을 넣어 저장하면
데이터가 라즈베리파이로 이동되어, 출입 시 사용되도록 한다.
이렇게 photo 디렉터리에서 이름과 encoding 정보(사진)를 넣는 txt 파일에
소켓에서 receive한 data를 넣으면, 웹 서버에 데이터가 send될 때마다 txt파일이 갱신된다.
commu.py에 app.py에서 fileUpload를 하여 POST 방식으로 img와 username을 받아온다.
Client인 commu.py에 'add' 시그널 전송
(app.py와 commu.py는 같은 원격 서버에서 동작하며, 라즈베리파이와 연결하는 중간 매체로 사용된다. )
commu.py에서 data 변수에 인코딩 된 사진 정보를 receive하고, 받아온 name과 data변수를 라파로 전송한다.
사용자 정보 삭제도 비슷한 맥락으로 진행
아두이노 코드는 내가 잘 몰라서 Skip 🥲
이렇게 소켓 통신을 이용해 웹 서버에 저장된 사진과 사용자 이름을 라즈베리파이에 전송하였다.
+ 사용자 정보 저장 방식 (회원가입 시 입력 값 처리 방식)
userList.json의 key값이 사용자의 Id가 되고, Value값이 List형태로 이루어진 Password와 Email이 된다.
4. 등록된 사진 정보(인코딩 된 상태)와 카메라의 얼굴을 대조해 사용자 이름 출력하기
기존 얼굴 인식 코드를 수정해 encoding.txt 파일의 값을 고대로 가져와 사용하였다.
if ppid == 0:
# receive Success or Fail and name from camera module
signal, name = camera.face(temperature)
# if success
if(signal == 1):
line = line[:-1] + f', "name":"{name}"' + line[-1] + "\n"
# open door
pin3.write(1)
sleep(1)
pin3.write(0)
# record data
# output_file.write(line)
# send Temperature.json
print('send Temperature information to server')
print(line.encode("UTF-8"))
# solve codec error
client_socket2.send(line.encode("UTF-8"))
else:
print('Time Over. Please retry')
exit(0)
# if parent process
else:
# wait for child process's die
os.wait()
sleep(3)
camera 모듈에서 face 함수를 가져와 리턴된 값을 List 형태로 signal, name에 받는다.
signal == 1이면 얼굴인식이 성공된 경우이므로 Door open 수행
5. 인식된 출입자 데이터를 라즈베리파이에서 웹 서버로 전송하기(출입자 온도, 이름, 출입 시간)
먼저 온도 데이터를 라즈베리파이 서버에서 원격 서버로 보낸다.
commu2.py에서 data변수에 정보를 receive하고 온도, 사용자 이름, 출입 시간을 리스트 형태로 만들어
Temperature.txt에 write한다.
import socket as sc
import datetime
HOST_3 = '0.0.0.0'
PORT_3 = 31005
path = 'C:\\Users\\Administrator\\Desktop\\final\\Temperature.txt'
socket_3 = sc.socket(sc.AF_INET, sc.SOCK_STREAM)
socket_3.setsockopt(sc.SOL_SOCKET, sc.SO_REUSEADDR, 1)
socket_3.bind((HOST_3, PORT_3))
socket_3.listen()
client, addr = socket_3.accept()
print(1)
while True:
print(2)
datas = client.recv(100)
datas = datas.decode('utf-8')
if datas[0] == "{":
times = datetime.datetime.today()
times = str(times)
times = times[:19]
datas = datas[:datas.find("[")+5] + datas[datas.find("]") :-2] + ", \"times\":" + times + "}\n"
with open(path, 'a') as Temperature_file:
Temperature_file.write(datas)
느낀 점
오류 해결로 80%의 시간을 보낸 기억이 난다. 처음으로 하드웨어를 만져보고 통신을 건드려보아서, 웹 개발분야 이외의 개발 분야를 맛볼 수 있었던 경험이었다. 미숙했던 만큼 팀원들에게 많이 의지했었고, 본질에 접근하지 못하고 구글링으로 문제의 주위에 겉도는 경험을 하며 '문제 해결력'이라는 단어의 정의를 실감할 수 있는 계기가 되었다. 가령 소스코드에서 예상이 가는 문제 코드의 사이 사이에 print문을 넣어서 코드의 버그를 해결한다던가, 구현 과정의 A부터 Z까지 차근차근 짚어가며 오류 가능성을 모색해보는 자세를 배웠다. 좋은 팀원을 만나 좋은 프로젝트를 진행해, 다른 프로젝트를 여러개 수행하면서 혼자서는 결코 해낼 수 없었을 결과물을 낼 수 있었다. 다음에 한이음을 또 한다면, 다른 프로젝트를 제하고 올인원해서 좀 더 많은 것을 얻어가고 싶다.
'Other > 기록' 카테고리의 다른 글
[회고] 봄날은 온다, 2023년 하반기 회고 😎 (1) | 2023.12.30 |
---|---|
[세미나] 프로젝트 관리 방법론 및 포스트 모템 (1) | 2023.07.11 |
[후기] DND 9기 백엔드 개발자 합격 후기 (0) | 2023.07.03 |
[회고] 2023년 상반기 회고 👶 그리고 하반기 목표 (8) | 2023.06.30 |
[후기] 2023 IUPC (인하대학교 프로그래밍 경진대회) 출전 후기 (0) | 2023.05.26 |