기획 기사 기획 기사

'오징어 게임'의 '영희'를 만들어 보자!

글. 조선해양공학과 3 백지원 편집. 컴퓨터공학부 3 심성원
지난해 넷플릭스 드라마 '오징어 게임'이 전 세계적으로 엄청난 인기를 끌었는데요. 한국식 유머 코드와 사회 비판, 그리고 어린 시절 온국민이 한 번씩은 해 보았던 추억의 놀이라는 요소가 적절히 조합되어 전 세계인의 흥미를 사로잡았다고 합니다. '오징어 게임'에는 '무궁화 꽃이 피었습니다'를 비롯해 달고나 만들기, 구슬치기, 줄다리기 등 추억의 놀이가 여럿 등장합니다. 주인공은 극 중에서 순서대로 게임을 통과하며 우승에 다가가게 되지요.
넷플릭스 드라마 '오징어 게임'의 영희 로봇

그중 첫 번째 게임인 '무궁화 꽃이 피었습니다' 게임에서는 술래 역할로 거대한 로봇 '영희'가 등장해서 카메라 센서로 참가자들의 움직임을 감지하고 탈락 여부를 판정합니다. ('오징어 게임'은 청소년 관람 불가 등급 콘텐츠라서, 독자 여러분을 위해 자세한 내용은 생략하겠습니다. 성인이 된 후에 관람해 주세요!) 언뜻 생각해 보면 수많은 참가자들의 움직임을 하나하나 확인하는 것은 쉽지 않을 것 같아 보입니다. 그런데 이 알고리즘은 여러분도 만들 수 있을 정도로 간단하다는 사실, 알고 계셨나요? 이번 [어때요 코딩 정말 쉽죠] 코너에서는 파이썬 라이브러리인 OpenCV를 활용해 카메라 센서로 움직임을 감지해내는 알고리즘을 코딩하는 방법을 소개합니다! 파이썬을 기존에 잘 몰랐던 분도 쉽게 이해할 수 있으니, 잘 따라와 주세요!

(기사 하단에 전체 소스 코드를 올려 놓았으니 복사해서 함께 보기를 추천합니다.)

OpenCV 라이브러리 준비하기
영희 알고리즘을 만들기 위해서는 먼저 파이썬 라이브러리인 OpenCV를 설치해야 합니다. 여기서 '라이브러리'란 많은 책이 담겨 있는 도서관처럼, 이미 다른 사람들이 만들어 놓은 함수와 기능을 담아 놓은 집합소를 뜻합니다. 라이브러리를 활용하면 코드의 세세한 부분을 전부 작성하지 않더라도 다른 사람들이 만들어 놓은 코드를 활용해서 훨씬 간편하게 코딩을 할 수 있지요. OpenCV는 Open source Computer Vision의 약자로, 카메라 센서를 활용한 영상 처리에 특화된 라이브러리입니다. 우리의 목표는 컴퓨터나 노트북에 달린 카메라 센서를 이용해 움직임을 감지하는 것이니까 OpenCV를 사용해야겠죠?

파이썬 코딩을 위해서는 기본적으로 비주얼 스튜디오 코드(VS Code)나 파이참(PyCharm)과 같은 코드 편집 프로그램에서 파이썬을 설치하면 됩니다. 첫 번째로 할 작업은 우리가 이 프로젝트에서 OpenCV 라이브러리를 사용해 코딩하겠다는 명령어를 넣어 주는 것입니다. 이런 작업을 '라이브러리를 선언한다'고 하고, import라는 명령어를 사용합니다.

OpenCV 라이브러리와 VS Code의 로고

움직임 감지 알고리즘에서 앞으로 카메라를 통해 입력받고 다루게 될 시각 정보는 영상 정보입니다. 영상 정보는 매 순간 프레임을 구성하는 모든 픽셀의 색상 정보를 행렬의 형태로 모아 다루게 되는데요. 따라서 행렬 계산을 위한 라이브러리도 추가로 선언해 주어야 합니다. OpenCV와 행렬 계산 라이브러리는 각각 cv2와 numpy라는 이름으로 불러올 수 있는데요, numpy의 경우 글자수를 줄여 np라는 이름으로 불러와 줍니다.

여기까지 하셨다면, 파이썬으로 움직임 감지 알고리즘을 만들기 위한 준비는 다 끝났다고 볼 수 있습니다. 이제 구체적인 코드를 작성해 볼까요?

움직임 감지 프로그램을 코딩해 보자!
파이썬 프로그래밍은 특정 기능을 수행하도록 하는 함수를 선언하고 실행하는 일의 연속이라고 할 수 있습니다. 다양한 명령어를 조합해서 원하는 결과가 나오도록 하는 함수를 선언하고, 그렇게 선언한 함수를 실행하는 것이지요. 움직임 감지 프로그램 역시 움직임 감지 기능을 수행하는 함수를 선언한 후, 이를 실행하는 방식으로 이루어집니다.1

우리의 목표가 될 메인 함수를 '오징어 게임'의 제목을 따서 이 함수를 squidgame이라고 이름지어 볼까요? 참고로 함수의 이름은 younghee, mugunghwa 등 어느 것으로 해도 상관없습니다. 함수 선언에는 def 명령어를 이용합니다.

다음으로 함수가 동작하면서 사용하게 될 데이터를 저장할 변수를 설정해 줍니다. 앞으로 우리는 카메라에서 받아 온 정보를 계속해서 사용해야 하므로, 정보를 video라는 이름의 변수에 저장해서 변수의 이름을 데이터로 불러오는 용도로 사용하는 것이지요. 이때 OpenCV 라이브러리의 VideoCapture라는 메서드2를 사용해서 카메라에서 전송되는 정보를 video라는 변수에 바로 할당하면 간편하게 변수를 선언할 수 있습니다. video 변수에 cv2 라이브러리의 VideoCapture 메서드를 호출해 할당하고, 이때의 동작 유형은 0으로 설정합니다.

VideoCapture를 포함해 파이썬에서 메서드는 괄호 안에 매개변수로 어떤 값을 넣는지에 따라 메서드의 동작 방식이 결정되는데요. 이때 동작 방식으로 입력한 '0'은 입력 카메라가 여럿인 경우를 대비하여 동작의 대상으로 첫번째 카메라를 지정하는 역할을 합니다.

다음으로 카메라를 통해 받는 데이터의 크기를 우리가 정한 크기만큼 사용하기 위해 너비와 높이를 지정해 주어야 하는데, 이때는 video 변수에 set 메서드를 불러와 사용합니다.3 데이터의 크기를 정하지 않을 경우 에러가 발생할 수도 있기 때문에 잊지 말고 지정해 주세요.

카메라를 통해 정보를 받는 데까지는 성공했는데 이제 어떻게 해야 할까요? 다음 단계로 들어가기 전에 프로그램의 구조를 그려 봐야 할 것 같습니다. 우선 우리가 인식하고자 하는 '움직임'이라는 건 어떻게 정의할 수 있을까요?

영상 정보를 입력 받는다는 것은 '프레임'이라는 정지한 시각 정보가 연속적으로 들어온다는 것을 의미합니다. 그러니 만약 연속한 두 프레임의 시각 정보가 서로 다르면 '움직임'이 발생했다고 볼 수 있겠죠. 다시 말해 우리는 앞 프레임과 뒤 프레임의 시각 정보를 서로 비교해야 한다는 것입니다. 앞 프레임을 Head, 뒤 프레임을 Tail이라는 이름의 변수로 각각 지정해 비교하면 되겠죠? 첫번째와 두번째 프레임을 비교한 다음엔, 두번째 프레임을 Head로 바꾸고 다시 세번째 프레임을 Tail로 바꾸어 비교합니다. 두번째, 세번째를 비교하고 나면 세번째와 네번째 프레임을 비교하고… 이런 식으로 계속해서 반복해 비교하면 되겠네요.

물론 카메라에 오류가 발생해서 아예 프레임 정보를 받아올 수 없는 상황도 생각해야 합니다. 오류가 발생했는데 프로그램이 계속 실행되면 안 되니까, 이런 경우에는 프로그램을 중단하는 명령어를 삽입해야 합니다.

이를 파이썬 코드로 구현하는 과정은 다음과 같습니다. 우선 조건문 if를 활용해서 영상 정보가 들어오는 경우에만 코드를 실행하도록 합니다. isOpened 메서드를 활용해 영상을 정상적으로 입력 받았다는 사실을 확인했다면, 이어지는 코드를 실행해 read 메서드를 활용해 Head 변수에 첫번째 프레임의 정보를 받아 오고, 아니라면 break 명령어로 프로그램을 멈추어 줍니다.

그리고 스위치처럼 True와 false 값만을 갖는 변수 ret을 활용해 'Head 정보 입력 여부'를 나타내도록 하고, 만약 Head가 정상적으로 입력되었다면 다음 프레임을 같은 방식으로 Tail 변수에 저장해 줍니다. 마지막으로, 우리가 받아 온 영상 정보를 확인하기 위해 처리 과정을 출력해 주는 창을 생성해야 하는데요. 이때 출력할 대상으로 Head의 정보를 복사한 color라는 변수를 새로 생성하고, 이 color 변수를 라이브러리의 imshow 메서드를 활용해 생성한 'Color' 창에서 실시간으로 확인할 수 있도록 해 줍니다. 실제 파이썬 코드는 아래와 같습니다.

이제 두 프레임 간의 차이를 비교하는 과정이 남았습니다. 그런데 여기서 잠깐! 우리가 Head와 Tail로 받아들인 정보는 컬러 영상 이미지입니다. 디지털 환경에서 컬러 이미지는 이미지를 구성하는 각 픽셀의 컬러 정보를 BRG(Blue, Red, Green), 즉 파랑, 빨강, 초록의 삼원색으로 나누어 그 세기를 0부터 255까지의 값으로 저장하는데요. 이때 삼원색의 세기 값을 각각 모두 비교하다 보면 프로그램의 계산량이 너무 많아지는 문제가 있습니다. 이를 방지하기 위해 흑백으로 영상을 전환해 주는 OpenCV 라이브러리의 메서드, cvtColor를 이용해 Head와 Tail변수에서 BRG 세 요소로 표현된 값을 흑백(Grayscale), 즉 0부터 255까지의 단일한 밝기 값으로 바꾸어 주어야 합니다.

자, 그럼 흑백으로 변환한 두 프레임을 비교해서 움직임을 검출해 볼까요? 우선 프레임별로 대응하는 같은 위치의 픽셀을 비교하기 위해서는 밝기 값이 얼마나 바뀌었는지, 즉 '밝기 값의 차이'가 얼마나 되는지를 측정하면 될 것 같습니다. 이를 위해 absdiff 메서드를 사용해 특정 픽셀의 밝기 값의 차를 계산하고, 절댓값으로 표현한 후 diff라는 변수에 할당해 줍니다. 이때 흑백으로 변환한 이미지와 밝기 차이 정보 역시 imshow 메서드를 활용해 창으로 확인할 수 있도록 하면 좋겠지요?

그런데 밝기 값의 차이는 0에서 255까지 다양한 값을 가질 수 있고, 조명의 상태에 따라 시시각각으로 미세하게 변할 수 있기에 어느 정도를 '움직인' 것으로 보아야 할지 모호하다는 문제가 있습니다. 실제로 '무궁화 꽃이 피었습니다' 놀이를 할 때에도 어쩔 수 없이 숨을 쉬는 것이나, 아주 조금 움직인 것까지 움직였다고 하지는 않습니다. 그래서 임의로 100이라는 문턱 값(threshold)를 설정하고, 이 차이가 100을 넘으면 모두 255(흰색)로, 100 미만이면 모두 0(검은색)으로 변환해 diff_binary 변수에 저장하는 작업을 한 번 더 수행해 줍니다. 이렇게 하면 처음의 컬러 영상 정보가 '움직임이 포착된 하얀색 픽셀'과 '움직이지 않은 검은색 픽셀' 두 가지로만 나누어지게 되고, 이 역시 imshow 메서드로 확인할 수 있습니다.

아까 영상 크기를 640x480 픽셀로 지정했던 것, 기억나시나요? 지금까지 우리가 다룬 변수에는 640x480개, 즉 30만 여 개의 픽셀의 밝기 값이 저장되어 있습니다. 그러니 여기에서 '움직인' 것으로 볼 수 있는 픽셀의 수, 즉 하얀색 픽셀의 수를 세면 얼마나 움직였는지를 파악할 수 있겠지요. 이를 위해 countNonZero 메서드를 활용해 diff_binary 변수에서 흰색 픽셀의 개수를 세고, 이를 white_pixel 이라는 변수에 저장해 줍니다. 결과적으로 이 white_pixel의 값이 특정 기준치, 예를 들면 150을 넘긴다면 움직임이 감지된 것으로 볼 수 있습니다.

움직임을 감지하는 과정

그리고 첫번째 프레임, 두번째 프레임, 세번째 프레임으로 넘어가며 연속적으로 움직임을 포착할 수 있게 하기 위해서 기존의 Tail은 Head로 교체하고, 새로운 Tail 정보를 받도록 해 줍니다. 물론 무한정 프로그램을 실행할 수는 없으니 Esc 키를 누르면 프로그램이 종료되도록 break 명령어도 넣어주어야겠지요.

여기서 움직임이 감지된 것을 우리가 확인할 수 있도록 하기 위해서는 한 가지 작업이 더 필요한데요, 바로 움직임이 감지된 순간 화면이 빨갛게 물들게 한다거나, 경고음을 낸다거나, '탈락'이라는 글씨를 출력한다거나 하는 함수를 만들어 호출하는 것입니다. 저는 화면이 빨갛게 물들게 하는 방법을 선택해 보았는데요. 처음에 squidgame 함수를 선언했을 때와 마찬가지로 이번에는 frame이라는 변수를 입력받아 변화된 결과물을 내보내는 red_frame 이라는 함수를 선언해 볼까요?

우리가 보는 컬러 영상이 특정 조건을 만족하면 빨간색으로 반투명하게 물들도록 하기 위해서는 먼저 컬러 영상에 덮일 빨간색 이미지를 red_image 변수로 만들어야 합니다. 이를 위해 새롭게 red_image 변수를 선언할 때 먼저 numpy 라이브러리의 full 메서드를 호출해서 가로 640픽셀, 세로 480픽셀의 3차원 행렬을 생성하고, 빨간색을 표현하기 위해 Blue, Green, Red 색상값을 의미하는 매개변수 자리에 각각 0, 0, 255를 입력해 줍니다. 한편 빨간 이미지가 원래의 컬러 영상을 전부 가리지 않고 반투명하게 덮도록 하는 것이 목표이므로, 함수를 호출할 때 입력받을 frame 영상과 방금 생성한 red_image를 cv2의 addWeighted 메서드를 활용해 혼합한 후, 그 결과를 다시 frame으로 내보내도록 합니다.4

마지막으로 조건문 if를 활용해서 움직임이 감지된 순간, 즉 white_pixel의 값이 기준치 150을 넘긴 순간 red_frame 함수가 실행되도록 하면, 짜잔! '오징어 게임'의 '무궁화 꽃이 피었습니다' 게임이 만들어집니다. 마지막에 squidgame 함수를 호출해 주면 게임이 정상적으로 실행되는지 확인할 수 있습니다.

파이썬 코드와 실제로 실행했을 때의 화면은 다음과 같습니다.

프로그램 실행 장면. 왼쪽 위 - 움직임이 감지되어 Color 창이 빨갛게 물든 모습. / 왼쪽 아래 - 흑백으로 변환한 영상 정보. / 오른쪽 위 - 두 프레임 간의 밝기 차이(움직임) / 오른쪽 아래 - 움직임이 발생했는지의 여부를 검정색(밝기 0)과 하얀색(밝기 0)으로만 나타낸 것

1 이런 방식으로 코딩을 하는 것을 객체 지향 프로그래밍이라고 합니다. 다양한 변수를 정의하고 계산을 수행하는 과정을 함수라는 하나의 객체, 즉 패키지로 묶어 다루면 여러 가지 이점이 있습니다.
2 메서드는 함수와 비슷하게 기능하며 우리가 가공하고자 하는 데이터(객체)에 작업을 하도록 하는 명령어를 의미합니다.
3 특정 변수에 메서드를 직접 지정하여 실행할 때 파이썬에서는 온점(.)을 사용합니다. set 메서드는 video 변수와 함께 동작하는 메서드이므로 video.set()과 같은 형태로 묶어 호출하게 됩니다.
4 이때 red_image(frame)에서 frame은 특정 변수를 의미하는 것이 아니라, 함수와 함께 입력받을 변수를 임시로 표시한 것입니다. frame이라는 변수에 특별히 고정적인 의미가 있는 것은 아니며, 수많은 변수의 역할을 구분해 들어갈 자리를 임시로 지정해 놓은 것일 뿐이므로 나중에 함수를 실제로 사용하는 변수(color)와 함께 호출할 때는 red_image(color)와 같이 호출하면 됩니다.

자, 어떠셨나요? OpenCV 라이브러리를 활용하니 생각보다 간단하게 프로그램을 코딩할 수 있었지요? 이처럼 라이브러리는 전 세계의 수많은 사람들이 이미 만들어 놓은 좋은 함수와 메서드를 모아놓은 것이기 때문에, 사용법만 익히면 고등학생 여러분도 충분히 좋은 프로그램을 만들 수 있답니다. 물론 나중에 코딩 실력을 기르고, 훌륭한 개발자가 된다면 언젠가는 여러분이 직접 라이브러리를 만드는 일에 참여할 수도 있겠지요? 이번 '오징어 게임'에서의 영희와 같은 움직임 감지 프로그램을 직접 만들어본 경험이 여러분께 공학도의 꿈으로 한발짝 더 다가가게 하는 계기가 되었기를 희망합니다. 그럼 다음에 또 만나요!

* 전체 소스 코드
그림출처
그림 1. https://www.mk.co.kr/news/world/view/2021/10/953277/
그림 2. https://code.visualstudio.com/, https://opencv.org/
그림 3, 4. 공대상상 자체제작 이미지