본문 바로가기
코딩과 알고리즘

파도(21) - 동영상 사람인식 & HTML 캔버스 연동

※ '파도'는 크레이의 '파이스크립트 도전기'의 줄임말입니다.

지난 게시글에서 연재되는 글입니다 => https://itadventure.tistory.com/562

 

파도(20) - 동영상 얼굴인식

※ '파도'는 크레이의 파이스크립트 도전기의 줄임말입니다. 지난 게시글에서 연재되는 글입니다 => https://itadventure.tistory.com/561 파도(19) - 파이스크립트 동영상 재생! 근데 넘 느려요 ㅎ.. ※ '파

itadventure.tistory.com


지난 게시글에서는 cv2 모듈을 이용하여 동영상 안에 있는 한 모델분 얼굴을 인식,
자동추적하는 부분을 다뤄 보았는데요.

이 번에는 사람들이 서 있는 모습이나 걸어가는 모습을 추적하는 몸체 인식 부분을 다뤄보도록 하겠습니다.

다만 이번에는 방법을 좀 달리 해볼텐데요.
영상 이미지를 보여주는 방법을 다르게 적용해보도록 할 겁니다.
어떻게요? 바로 HTML캔버스라는 녀석을 통해서 말이지요.

 

HTML캔버스가 뭔가요?

 

HTML 캔버스란 웹브라우저에서의 제약된 HTML 규칙을 벗어나
자유로운 그래픽을 표현할 수 있는 공간(CANVAS)을 제공해주는 태그입니다.
이 것을 이용하면 웹브라우저만으로 3차원 가상공간을 표현할 수도 있다는 점에서
웹브라우저의 놀라운 진보가 아닐 수 없지요.
(캔바스의 놀라움을 확인해보시려면 아래 크레이의 게시글을 참조해 주세요)
https://itadventure.tistory.com/61?category=728056 : 3차원 웹 23번째 시간, 해가 뜨고 해가 지고

 

오늘 다룰 내용은 3차원 가상공간과는 아무 관계 없지만,
HTML캔버스 기술은 이미지 처리에도 아주 특화된 기능을 가지고 있어서 데이터의 형태만 잘 맞춰주면
파이썬에서 생성된 영상프레임을 바로 HTML 캔버스에 보여줄 수 있습니다.
그렇기 때문에, 지난 게시글에서 다루었던 matplotlib 를 사용한 방법보다 속도가 빠르다는 장점이 있습니다만 이번 예제는 해상도가 꽤 높아서 그렇게 빨라진 부분을 체감하지는 못하는 것 같습니다 :)

 

전체 소오~스

 

그럼 오늘의 전체 소스 공개하고, 들어가보도록 하겠습니다 :)
아래 소스를 웹서버에 넣고 실행만 해주시면 되는데요.
실행 결과는 크레이의 홈페이지에서도 바로 확인 가능합니다.
http://dreamplan7.cafe24.com/pyscript/pycam-3.html

<!DOCTYPE html>
<html> 
  <head> 
    <link 
      rel="stylesheet" 
      href="https://pyscript.net/alpha/pyscript.css" 
    /> 
    <script 
      defer 
      src="https://pyscript.net/alpha/pyscript.js"
    ></script> 
    <py-env>
    - matplotlib
    - opencv-python
    - paths:
      - http://dreamplan7.cafe24.com/pyscript/pixabay-6387.mp4
      - http://dreamplan7.cafe24.com/pyscript/haarcascade_lowerbody.xml
      - http://dreamplan7.cafe24.com/pyscript/haarcascade_upperbody.xml
      - http://dreamplan7.cafe24.com/pyscript/haarcascade_fullbody.xml
    </py-env>
    <py-config>
      - autoclose_loader: true
      - runtimes:
        -
          src: "https://cdn.jsdelivr.net/pyodide/dev/full/pyodide.js"
          name: pyodide-0.20
          lang: python
    </py-config>
  </head>
  <body> 
    <link rel="stylesheet" href="pytable.css"/>
    프레임번호 : <span id="frameNo"></span>
    <canvas id="view" width=1280 height=720></canvas>
<py-script>
import cv2
from js import document, setInterval, \
  setTimeout, ImageData, Uint8ClampedArray, \
  CanvasRenderingContext2D as Context2d
from pyodide.ffi import create_proxy, to_js
import asyncio
import gc
import numpy as np

상체인식 = cv2.CascadeClassifier('haarcascade_upperbody.xml')
하체인식 = cv2.CascadeClassifier('haarcascade_lowerbody.xml')
몸체인식 = cv2.CascadeClassifier('haarcascade_fullbody.xml')

캡쳐 = cv2.VideoCapture('pixabay-6387.mp4')

프레임번호 = 0
canvas = document.getElementById("view")
ctx = canvas.getContext("2d")

def draw_image(ctx, x, y, image):
  data = Uint8ClampedArray.new(to_js(image.tobytes()))
  height, width, _ = image.shape
  image_data = ImageData.new(data, width, height)
  ctx.putImageData(image_data, 0, 0, x, y, width, height)

def ShowFrame():
  global 캡쳐, 그래프, cv2, 프레임번호, document, 상체인식, 하체인식, 몸체인식
  캡쳐.set(cv2.CAP_PROP_POS_FRAMES, 프레임번호)
  ret, frame = 캡쳐.read() # 두 개의 값을 반환하므로 두 변수 지정
  
  reverse = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
  판독된상체들 = 상체인식.detectMultiScale(reverse, maxSize = (100,100))
  판독된하체들 = 하체인식.detectMultiScale(reverse, maxSize = (100,100))
  판독된몸체들 = 몸체인식.detectMultiScale(reverse, maxSize = (120,220))
  
  for (x, y, w, h) in 판독된상체들:
    cv2.rectangle(reverse, (x, y), (x + w, y + h), (0, 255, 0, 255), 3)
  for (x, y, w, h) in 판독된하체들:
    cv2.rectangle(reverse, (x, y), (x + w, y + h), (0, 0, 255, 255), 3)
  for (x, y, w, h) in 판독된몸체들:
    cv2.rectangle(reverse, (x, y), (x + w, y + h), (255, 0, 0, 255), 1)
  
  draw_image(ctx, 0, 0, reverse)
  document.getElementById('frameNo').innerHTML=프레임번호
  프레임번호=프레임번호+10
  gc.collect()
  setTimeout(ShowFrameProxy, 0)

ShowFrameProxy = create_proxy(ShowFrame)
setTimeout(ShowFrameProxy, 0)

</py-script> 
  </body> 
</html>

 

실행화면.


여러사람들이 서있거나 걷고 있는 영상에서 사람들의 상체와 하체, 그리고 전신을 인식해서 사각형으로 보여주고 있는데요. 사람들이 옹기 종기 모여 있는 부분 또는 구도상 카메라에서 비스듬하게 서있는 등의 영역은 인식하지 못하기도 합니다.
좀 아쉬운 부분이지만 그래도 이 정도면 괜찮네~ 소리가 나오더라구요 :)

그럼 이제부터 소스에 새로이 적용된 부분을 설명드리도록 하겠습니다.

 

몸체 인식 훈련 XML 데이터

 

지난 시간에 얼굴을 인식하는 haarcascade_frontalface_alt2.xml  이라는 훈련 데이터를 사용했었는데요.
이 XML 파일은 얼굴만 인식합니다.

opencv 모듈에서는 위의 XML 외에도 몸체를 인식하는 XML 파일이 있는데요.
상체, 하체, 전신 3가지가 있습니다.
이번 예제에서는 좀 욕심을 내서 3가지를 모두 인식하도록 할텐데요.

이를 위해서 <py-env> 태그를 아래와 같이 수정하였습니다.


<py-env>
- matplotlib
- opencv-python
- paths:
  - http://dreamplan7.cafe24.com/pyscript/pixabay-6387.mp4
  - http://dreamplan7.cafe24.com/pyscript/haarcascade_lowerbody.xml
  - http://dreamplan7.cafe24.com/pyscript/haarcascade_upperbody.xml
  - http://dreamplan7.cafe24.com/pyscript/haarcascade_fullbody.xml
</py-env>


haarcascade_lowerbody.xml 파일이 하체 인식용 훈련 데이터이고,
haarcascade_upperbody.xml 파일은 상체 인식용 훈련 데이터이고,
haarcascade_fullbody.xml
파일은 전신 인식용 훈련 데이터인 것이지요.

그 외에 pixabay-6387.mp4 파일은 사람들이 걸어가는 영상이 담긴 동영상 파일인데,
픽사베이에서 오픈된 영상이라 문제없이 사용할 수 있는 파일입니다.

원본 URL : https://pixabay.com/ko/videos/사람들-상업-가게-바쁘다-6387/

이 파일을 통해서 각 부위를 판별할 수 있는 모델을 먼저 정의한 다음에,


상체인식 = cv2.CascadeClassifier('haarcascade_upperbody.xml')
하체인식 = cv2.CascadeClassifier('haarcascade_lowerbody.xml')
몸체인식 = cv2.CascadeClassifier('haarcascade_fullbody.xml')


 

영상을 읽기 시작합니다. 지난번과 동일하지만 이번에는 0프레임부터 시작하도록 하였습니다.


캡쳐 = cv2.VideoCapture('pixabay-6387.mp4')
프레임번호 = 0


 

캔바스의 출현

 

이 번에는 뭐니뭐니해도 캔바스 기술을 적용한 것이 특별한데요.

먼저 HTML 소스에 캔버스 태그가 정의되어야 합니다.
아래 태그가 바로 그 것입니다. 캔바스 크기는 자유롭게 변경이 가능한데요
여기서는 간단하게 영상의 크기와 똑같이 맞춰주었습니다.


<canvas id="view" width=1280 height=720></canvas>


이어서 파이스크립트 태그 <py-script> 내에서 캔버스 기술을 사용하기 위해
아래 모듈의 선언이 필요합니다.


from js import document, setInterval, \
  setTimeout, ImageData, Uint8ClampedArray, \
  CanvasRenderingContext2D as Context2d
from pyodide.ffi import create_proxy, to_js


ImageData 모듈는 캔버스용 이미지를 생성할 때 사용되고,
Uint8ClampedArray 모듈과 to_js 모듈은 파이썬에서 캔바스로 이미지 데이터를 넘길 때 사용됩니다.
파이썬의 이미지 형태와 캔바스의 이미지 형태가 다르기 때문에 이 2개의 모듈을 이용해서 변형해 주는 것이지요.

이윽고 파이썬에서 캔버스를 정의하는 부분이 바로 아래의 코드입니다.


canvas = document.getElementById("view")
ctx = canvas.getContext("2d")


동영상의 프레임 이미지는 2차원 평면의 작동 화면을 보여주는 기능이기 때문에
캔바스의 2D 컨텍스트 기능을 활용하여야 합니다.

canvas.getContext("2d") 가 바로 그것이지요.

이 컨텍스트를 가져와 ctx 라는 변수에 받아았기 때문에
이제 ctx 를 가지고 캔바스를 마음껏 조정할 수 있는 것입니다.

이어서 2개의 함수 정의가 보이실 텐데요.
이 부분은 잠시 후에 살펴보겠습니다.


def draw_image(ctx, x, y, image):
      :

def ShowFrame():
      :


 

타이머 이벤트에 연결


이제 파이썬 함수를 자바스크립트에서 연결할 수 있도록 정의한 다음,
자바스크립트의 타이머 이벤트에 연결해줍니다. 이제 웹페이지가 로딩이 끝나면 자동으로
ShowFrame 파이썬 함수가 실행될 것입니다.


ShowFrameProxy = create_proxy(ShowFrame)
setTimeout(ShowFrameProxy, 0)


 

동영상 프레임 무한반복!

 

ShowFrame 함수에서는 동영상의 하나의 프레임을 보여주는 기능을 수행하는데요.
아래와 같은 구조로 되어 있어 사실은 무한 반복합니다. 함수가 끝날 무렵 다시 setTimeout 이벤트가 실행이 되어 다시 ShowFrame 함수가 호출되는 것이지요.


def ShowFrame():
     :
  setTimeout(ShowFrameProxy, 0)


 

이번에는 RGB~A! 에이?!


ShowFrame() 함수에서 동영상의 프레임을 읽어 오는 부분은 지난 내용과 별반 다를게 없으나,
색상의 배열 순서를 처리하는 방법에 있어 바뀐 부분이 있습니다.

동영상에서 뽑아낸 하나의 점을 표현하는 색상배열 순서는
파랑색, 녹색, 빨강색 순이라고 했던 것 기억하시나요?
그리고 이를 이미지로 표현하기 위해서는 빨강, 녹색, 파랑색 순으로 변경해야 하는 부분을요.

하지만 HTML 캔버스에서는 한가지가 더 붙습니다.
바로 투명도(Alpha)이지요.

그래서 캔버스의 색상배열 순서는 빨강, 녹색, 파랑색, 투명도입니다.
IT용어로는 RGBA 라고 하지요. ( Red, Green, Blue, Alpha )

이미지를 반전해주는 소스가 아래와 같이 변경되었습니다.


reverse = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)


 

몸체 인식

 

이제 영상의 사람들의 몸체를 인식하는 코드가 실행되는데요.
상체, 하체, 몸체 모든 영역을 인식합니다.


판독된상체들 = 상체인식.detectMultiScale(reverse, maxSize = (100,100))
판독된하체들 = 하체인식.detectMultiScale(reverse, maxSize = (100,100))
판독된몸체들 = 몸체인식.detectMultiScale(reverse, maxSize = (120,220))


여기서 maxSize 는 인식할 수 있는 최대 사각형의 크기(픽셀 단위)를 의미합니다.
이는 잘못된 인식을 막기 위한 하나의 수단인데요.
사람의 몸체 크기가 어느 정도 제한된 것이 확실한 경우 사용합니다.
이런 제약을 주지 않으면 아래와 같이 큰 사각형을 사람 몸체로 인식하기도 하지요.


그 밖에도 최소 크기를 정할 수 있는 minSize도 있으며 여러가지 옵션들이 있으니 관심 있는 분은 아래 URL에서 관련 정보를 확인해 주세요. ( 영문 )

https://docs.opencv.org/3.4/d1/de5/classcv_1_1CascadeClassifier.html#aaf8181cb63968136476ec4204ffca498

이제 인식된 몸체를 화면에 사각형으로 그려주어야 겠지요?
아래 코드가 그 역활을 수행합니다.


  for (x, y, w, h) in 판독된상체들:
    cv2.rectangle(reverse, (x, y), (x + w, y + h), (0, 255, 0, 255), 3)
  for (x, y, w, h) in 판독된하체들:
    cv2.rectangle(reverse, (x, y), (x + w, y + h), (0, 0, 255, 255), 3)
  for (x, y, w, h) in 판독된몸체들:
    cv2.rectangle(reverse, (x, y), (x + w, y + h), (255, 0, 0, 255), 1)



그리고 나서 최종 화면을 사용자의 웹브러우저에 보여주어야 할 텐데요.
바로 이 때 캔바스를 이용합니다. 아래 함수가 실행되면서 제어권은 draw_image 함수로 넘어가는데요.


draw_image(ctx, 0, 0, reverse)


draw_image 함수는 아래와 같이 짧은 코드로 구성되어 있지만 꽤 많은 일을 합니다.
아래 내용은 꽤 복잡할 수 있으니 복잡하시다면 그냥 함수만 복사해 사용하시기를 권해드립니다.


def draw_image(ctx, x, y, image):
  data = Uint8ClampedArray.new(to_js(image.tobytes()))
  height, width, _ = image.shape
  image_data = ImageData.new(data, width, height)
  ctx.putImageData(image_data, 0, 0, x, y, width, height)


먼저 파이썬용 이미지를 자바스크립트용 이미지로 변경해 줍니다.


data = Uint8ClampedArray.new(to_js(image.tobytes()))


상세 원리를 설명드리기에는 전산학에 대한 이해가 필요하기 때문에,
최소한의 이해원리만 설명드리자면 아래와 같습니다.

제일 안쪽의 image.tobytes() 는 이미지 데이터를 파이스크립트가 좋아하는 배열 형태에서

[[[127 127 127 255] [127 127 127 255] ...


컴퓨턱가 좋아하는 '바이트'라는 배열로 변경해 줍니다.

b'\x7f\x7f\x7f\xff\x7f\x7f...

그리고 다시 to_js() 모듈이 이 바이트 배열을
자바스크립트가 좋아하는 '자바스크립트 배열'로 바꿔주는 것이지요.

127,127,127,255,127,127,127,255,...

이 데이터를 HTML캔바스에서 사용하기 위해서는 다시 두 번의 변환 절차를 거쳐야 하는데요.
첫번째가 바로 Uint8ClampedArray.new() 입니다.

이 코드까지 거치고 나면 비로서 연속된 픽셀 데이터가 일렬로 쭈욱 나열되는데요.
이 픽셀 데이터에는 아직 가로 크기, 세로 크기에 해당하는 해상도라는 부분이 없습니다.

그래서 두번째 변환을 거치는데요.
바로 이 때 해상도를 적용합니다.


height, width, _ = image.shape
image_data = ImageData.new(data, width, height)


image 는 자바스크립트로 변환하기 이전 파이썬 이미지인데요.
배열로 구성되어 있고 1280x720 해상도에 4가지 요소(RGBA)로 구성되어 있는 3차원 배열입니다.
그래서 image.shape 는 (720, 1280, 4) 라는 값을 가지고 있는데요. ( 해상도 순서 정반대 주의! )

아래 코드가 실행되면, image.shape 의 순서에 따라,
height 변수에는 720, width 변수에는 1280, _ (언더바) 변수에는 4라는 값이 입력됩니다.


height, width, _ = image.shape


"언더바 ( _ )도 변수가 될 수 있나요?"
네, 언더바도 변수가 될 수 있습니다.
여기서는 사용하지 않는 변수이기 때문에 이름 짓기 고심될 때 아주 유용하지요 :)

그리고 최종적으로 아래 코드가 실행되면 HTML캔바스에서 사용할 수 있는
이미지 데이터가 완성되는 것입니다.


image_data = ImageData.new(data, width, height)


이 이미지는 완성은 했지만 아직 캔버스에 그림을 그려주지는 않았지요.
아래 코드는 캔바스에 최종적으로 그림을 그려주는 코드입니다.


ctx.putImageData(image_data, 0, 0, x, y, width, height)


위의 과정이 계속 반복이 되면서 동영상이 재생되는 것이지요.
동영상 재생이 끝날 때까지 말이지요 :)

 

마무~리

 

이번 시간에는 opencv를 이용하여 동영상에서 사람 몸체 중 상체, 하체, 전신 영역을 인식하고 사각형으로 각 영역을 표시하는 부분과, 이렇게 알아낸 부분을 HTML 캔버스에 표시하는 방법에 대해 알아보았으며 특히 파이썬 배열 => 자바스크립트 배열 => HTML캔버스 이미지 로 변환하는 기술을 활용해 보았습니다.

아무쪼록 필요하신 분에게 도움이 되시기를 바라며 오늘도 이만 글을 맺습니다 :)
오늘도 관심과 사랑으로 방문해주시는 모든 분들께 감사 꾸벅!

내용이 마음에 드셨다면 공감 클릭~
구독자분의 댓글은 사랑입니다 :)