[해커네컷] 2. '찍을래' 기능 추가(1/2)
왜 추가하게 되었나
끝났다고 생각했던 해커네컷을 다시 꺼내면서 해커톤의 기억이 떠올랐다. 행복했던 기억이었다. 이전의 코드를 그대로 재활용하는 게 아니라 몇가지 기능을 추가하고 싶었다. 그 중 하나가 '찍을래' 기능이다.
'찍을래' 기능
'찍을래' 기능은 사진 부스를 설치해서 직접 사진을 찍을 수 있는 기능이다. '찍을래'라는 기능의 이름은 아직 가제이다. slack 메시지로 '찍을래'라고 말하면 사진 부스를 예약할 수 있고, 사진 부스에서 촬영한 사진은 slack 메시지로 전달된다. 일단은 부스에서 사진을 촬영하고&&그 사진을 slack 채널에 업로드해주는 기능까지만 구현하려고 한다. 금전적으로 허용된다면 라즈베리파이를 사용하고, 허용되지 않는다면 공기계를 사용할 예정이다.
전체 기능의 구조가 다음과 같이 변경되는 셈이다.

'찍을래' 화면 UI 설계
1. 메인페이지

2. 사진촬영

3. 완료 화면

구현해야 할 함수
1. gogo_live(){
show UI
}
When '촬영하기' pressed:
2. function gogo_snap(){
show left_time while count_down(3s)
snap()
save_snapped_photo()
show_snap_succeed_ui()
}
3. function send_photo_to_channel{
send()
}
1. HTML에서 버튼을 누르면 slack에서 메시지가 전달되는 기능
일단 웹으로 쏴 둔 UI와 slack을 연결하는 게 먼저일 것 같았다. 이걸 통신이라고 볼 수 있을진 모르겠지만,,, 아무튼 통신에 관련된 부분이다. 구현할 기능은 다음과 같다.
1. '메시지 보내기' 버튼이 있는 html 띄우기
2. 버튼을 누르면 server에 메시지 보내달라고 요청
3. server에서 channel id를 미리 알고 있다고 치고 메시지 보내기
1. '메시지 보내기' 버튼이 있는 html 띄우기
<body>
<button onclick="location.href='/gogo_message'">찍을래</button>
</body>
2. 버튼을 누르면 server에 메시지 보내달라고 요청
channel id와 보낼 메시지를 받아서 해당 채널에 메시지를 보내주는 함수. 버튼이 클릭되면 이 함수를 실행하도록 하면 된다.
def send_message_to_channel(message, channel_id):
slack_client = WebClient(token=config.bot_token)
slack_client.chat_postMessage(
channel=channel_id,
text=message
)
3. server측에서 channel에 메시지 보내기
@app.route('/gogo_message')
def gogo_message():
send_message_to_channel('가보자고', channel_id)
return render_template('web.html')

2. 웹상에서 카메라로 사진 찍고 local에 저장하는 기능
1. 웹에 카메라 띄우기
2. 3초마다 한 번씩 찍어서 저장하기
3. 4개 찍으면 함수 종료, 4개 사진이 저장된 경로를 배열에 담아서 return.
1. 웹에 카메라 띄우기
생각보다 정말... 고생했다. opencv로 카메라 모듈을 이용해 사진을 찍어서, 이걸 실시간으로 웹에 전송하려고 했기 때문이다. 사실 그럴 필요가 없다는 걸 다섯시간 정도 삽질한 후에 깨달았다. 내가 필요한 건 그냥 사진이다. 이 화면을 미리보기로 띄워주는 건 말그대로 가공할 필요 없는 미리보기 화면에 불과하기 때문이다.
🔨삽질1🔨 기존에 쓰려고 했던 코드
def gogo_picture():
cap = cv.VideoCapture(0)
if not cap.isOpened():
print("error")
exit()
while True:
ret, img = cap.read()
cv.imshow('PC_camera', img)
time.sleep(15)
img_captured = cv.imwrite(f"{time.time()}.png", img)
return img_captured
방향을 바꿨다. python으로 사진을 byte단위로 전송하는 대신 js를 써서 그냥 화면에 띄워주고, 이 사진을 flask server에 전송해 주면 그만이다.
사이트를 참고해서 해당 코드를 따왔다. 어이없을 정도로 간단하게 끝났다. 저녁에 먹은 짜장면이 빛을 발하는 순간이었다. (출처: https://www.kirupa.com/html5/accessing_your_webcam_in_html5.htm)
<body>
<div id="container">
<video autoplay="true" id="videoElement">
</video>
</div>
<script>
var video = document.querySelector("#videoElement");
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: true })
.then(function (stream) {
video.srcObject = stream;
})
.catch(function (err0r) {
console.log("Something went wrong!");
});
}
else{
console.log("NONO")
}
</script>

이미지 띄우기 성공! 이제 이걸 서버로 보내주기만 하면 된다
🔨삽질2🔨JS에서 사진 찍어서 server로 보내기
이게 쉬울 거라고 생각했는데 생각보다 어려웠다. 파이썬으로는 사진 찍는 건 쉬운데 실시간으로 웹에 띄우기가 어렵고, js로는 실시간으로 웹에 띄우는 건 쉬운데 사진 찍어서 저장하기가 어렵다. 어떤 걸 선택하는 게 좋을까...
🔨삽질3🔨버튼 누르면 사진 찍어서 저장하기
애초에 굳이 프론트단에서 사진을 찍을 필요가 있나? js가 띄우기를 잘하고 python이 찍기를 잘하면 각자 재능을 발휘할 기회를 주면 안 되는 건가?
'사진찍기' 버튼을 누르면 아예 server에서 사진을 찍어서 저장해 보기로 했다. 일단 호출되면 사진을 찍어주는 API를 만들어 봐야겠다.
🔨삽질4🔨opencv에서 사진 초록색으로 저장되는 현상 해결하기
capture = cv2.VideoCapture(0)
capture.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
while True:
ret, frame = capture.read()
cv2.imwrite('photoooo.jpg', frame)
break
위와 같이 코드를 짰다.

저장된 사진이다. 올여름 납량특집으로 개봉해도 이상하지 않을 것 같다.
cv2.imshow("VideoFrame", frame)
time.sleep(4)
혹시 이미지를 화면에 꼭 띄워야 하나 싶어서 해당 코드를 추가했다. 4초가 지난 후에 사진을 찍는 걸로 했다. 결과는 똑같았다. 아마 frame이 다 짜여지기 전에 그냥 sleep을 박아버렸기 때문인 것 같다.
def snap_picture_after(sec):
camera = cv2.VideoCapture(0)
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1080)
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 725)
time_to_end = int(time.time())+sec
result_img_src = f'./snapped_pics/{time.time()}.jpg'
while True:
ret, frame = camera.read()
now_time = int(time.time())
if time_to_end <= now_time:
cv2.imwrite(result_img_src, frame)
break
camera.release()
cv2.destroyAllWindows()
return result_img_src
성공했다. 프레임이 다 짜여지기 전에 사진을 찍은 게 문제였다. 뭔가 얼레벌레긴 하지만... 일단 카메라를 켜고 현재 시간으로부터 3초가 지나면 이미지를 저장하고 while문을 끝내는 걸로 했다.

성공!
🔨삽질4🔨 카메라 하나로 미리보기 영상을 제공하는 동시에 사진찍기
@app.route('/gogo_snap')
def gogo_picture():
img_src = snap_picture_after(3)
return img_src
api를 만들어서 버튼을 누르면 3초 후에 사진이 찍히는 함수를 실행하도록 했다.

미친듯이 오류를 뿜어냈다. 아무래도 카메라 하나로 웹에 미리보기 화면을 띄워주고 있는데 동시에 그걸로 사진까지 찍으려고 해서 그런 것 같았다. 해결방법을 생각해 봤다. 세 가지 정도를 떠올렸다.
1. 화면에서 비디오 미리보기를 중지하고 gogo_snap request하기
2. 카메라는 미리보기용으로 쓰고 사진은 현재 화면 캡쳐해서 사진만 떼오기
3. 그냥 카메라 두 개 달기...
<button onclick="location.href='/silhum'">silhum</button>
@app.route('/silhum')
def elsedfsad():
return "<script>location.href='/gogo_snap'</script>"
다이렉트로 사진 찍는 함수를 호출하는 대신 다른 페이지로 넘어가서(카메라를 끄기 위함), 그 페이지에서 사진 찍는 함수를 호출하도록 했더니 해결됐다. 카메라 두 개 안 달아도 돼서 다행이다^^,,,