AI 탐구노트

Headshot Tracking 따라하기 - 2편 본문

DIY 테스트

Headshot Tracking 따라하기 - 2편

42morrow 2024. 10. 14. 18:57

 

 

이전 작업 정리

지난 1편에서는 우분투와 아두이노를 연결하고 서보모터를 컨트롤하는 것까지 해 봤습니다.

 

Headshot Tracking 따라해 보기 - 1편

재미난 것 발견! 작년 쯤에 유튜브를 보다가 재미난 장난감을 만든 분의 영상을 발견했습니다.    서보모터를 이용해 Pan-Tilt를 할 수 있는 장비를 만들고 (실제 알리에서 판매하고 있음),카메

42morrow.tistory.com

 

그리고 최근 표준형 서보모터(180도)가 알리에서 도착했고 그것의 동작도 확인했었죠.

 

서보모터 (SG90 스탠다드) 테스트

예전 글에서 Head Tracking 하는 테스트를 하던 중 가지고 있던 서보모터가 연속형(360도)이라 각도 조절이 어려웠다는 얘기를 한 적이 있습니다.  Headshot Tracking 따라해 보기 - 1편재미난 것 발견! 

42morrow.tistory.com

 

 

서버 모터 교체 후 동작 확인

이제 연속형 서보모터를 표준형 서보모터로 변경하고 테스트를 진행해 보겠습니다. 표준형과 연속형은 형태는 동일하므로 교체 작업 외에는 할 것이 없습니다. 2개의 서보모터가 각각 다른 디지털핀을 통해 신호를 수신하고 동작하게 되었으니 이제 AI쪽과 연결하는 부분이 남았습니다. 

 

 

드디어 다음 단계로...

 

이상으로 하드웨어 측면은 준비가 된 것을 확인했으니 지금부터는 안면감지 후 트래킹하는 부분을 테스트 해 보겠습니다. 참고로 github repo에 공개된 코드는 facedetection.py, facetracking.py 2개의 파일로 되어 있으며 여기서는 face tracking 쪽만 다룹니다. 일단은 기존 코드를 이용해서 동작을 시켜보고, 제가 생각하기에 개선이 필요한 부분을 수정해서 진행해 보는 걸로 하겠습니다. 

 

 

공개 코드를 보면 다음과 같은 기능으로 구현된 것을 알 수 있습니다. 

  • PC (또는 준하는 장비로 예상)의 웹캠을 이용해서 영상 수집한 후
  • 영상에서의 안면감지는 cvzone의 FaceDetection 모듈 이용해서
  • 감지된 얼굴의 바운딩박스의 좌표를 추출하고 
  • 카메라 화면 비율과 서보모터의 움직임 반경을 비율 계산해서 
  • Pyfirmata를 이용해 python 코드로
  • 표준형(180도) 서보모터를 동작시켜 타게팅된 방향으로 팬/틸팅 진행

 

원본 코드 가운데 아주 일부는 제 컴퓨터 환경에 맞추느라 수정되었습니다. 대략적인 설명은 달았는데 어려운 것은 없을 겁니다.

import cv2
from cvzone.FaceDetectionModule import FaceDetector
from pyfirmata import Arduino, util, SERVO
import numpy as np

# 카메라 영상 속성 설정 및 획득
cap = cv2.VideoCapture(0)
ws, hs = 1280, 720
cap.set(3, ws)
cap.set(4, hs)

if not cap.isOpened():
    print("Camera couldn't Access!!!")
    exit()


# 아두이노 보드 지정
board = Arduino("/dev/ttyArduino")

# 서보모터가 사용할 디지털 핀 지정
servo_pinX = board.get_pin('d:9:s') #pin 9 Arduino
servo_pinY = board.get_pin('d:10:s') #pin 10 Arduino

# initial servo position
servoPos = [90, 90] 

# 안면 감지기
detector = FaceDetector()

# 카메라 영상 획득 및 감지, 트래킹 진행
while True:
    success, img = cap.read()
    img, bboxs = detector.findFaces(img, draw=False)

    if bboxs:
        # 안면 바운딩 박스 중심 좌표 추출
        fx, fy = bboxs[0]["center"][0], bboxs[0]["center"][1]
		pos = [fx, fy]
        
		# 영상 화면 대비 서보 반경 계산
        servoX = np.interp(fx, [0, ws], [0, 180])
        servoY = np.interp(fy, [0, hs], [0, 180])
        
		# 서보모터 움직임 범위 제어 (0도 < 범위 < 180도)
        if servoX < 0:
            servoX = 0
        elif servoX > 180:
            servoX = 180
        if servoY < 0:
            servoY = 0
        elif servoY > 180:
            servoY = 180

		# 최종 서보 회전 각도 확정 (X축, Y축)
		servoPos[0] = servoX
        servoPos[1] = servoY

		# 감지된 안면 상에 과녁표시 그리기
        cv2.circle(img, (fx, fy), 80, (0, 0, 255), 2)
        cv2.putText(img, str(pos), (fx+15, fy-15), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2 )
        cv2.line(img, (0, fy), (ws, fy), (0, 0, 0), 2)  # x line
        cv2.line(img, (fx, hs), (fx, 0), (0, 0, 0), 2)  # y line
        cv2.circle(img, (fx, fy), 15, (0, 0, 255), cv2.FILLED)
        cv2.putText(img, "TARGET LOCKED", (850, 50), cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 255), 3 )

    else:
        cv2.putText(img, "NO TARGET", (880, 50), cv2.FONT_HERSHEY_PLAIN, 3, (0, 0, 255), 3)
        cv2.circle(img, (640, 360), 80, (0, 0, 255), 2)
        cv2.circle(img, (640, 360), 15, (0, 0, 255), cv2.FILLED)
        cv2.line(img, (0, 360), (ws, 360), (0, 0, 0), 2)  # x line
        cv2.line(img, (640, hs), (640, 0), (0, 0, 0), 2)  # y line

	# 화면 왼쪽 상단에 서보모터 회전 각도 표시 
    cv2.putText(img, f'Servo X: {int(servoPos[0])} deg', (50, 50), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)
    cv2.putText(img, f'Servo Y: {int(servoPos[1])} deg', (50, 100), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)

	# 서보모터 회전 실행
    servo_pinX.write(servoPos[0])
    servo_pinY.write(servoPos[1])

	# 영상 화면 출력
    cv2.imshow("Image", img)
    cv2.waitKey(1)

 

 

테스트 진행

 

 

위 코드를 이용해서 일단 돌려 봅니다. 오호... 생각보다 움직임은 활발하게 나옵니다. 

 

그런데, 몇 가지 문제가 있습니다.

1.웹캠 영상 방향과는 반대로 움직입니다. 

2.y축을 담당하는 서보모터의 움직임 범위가 깔끔하지 않습니다.

 

첫번째, 화면에 보이는 영상 방향은 실제 서비스가 아니라 제가 화면을 보면서 움직임까지 하다보니 생기는 현상이므로 이슈는 아닙니다. 다만, 테스트 할 때 확인하기 불편하다는 것이라 잠시 좌우 반전을 시켜두기 위해 아래의 코드를 추가합니다. 

    img = cv2.flip(img,1) # 영상 좌우반전 (테스트 목적)

 

 

두번째, 서보모터의 움직임 범위가 현재는 0~180도로 되어 있고, 이 각도가 화면 크기에 매칭되어 있는데, 실제 카메라의 FoV(Field of View, 카메라 화각)에 맞춰 조정되는 것이 필요할 것 같아 숫자 조정을 해 봤습니다. 

# 카메라 FOV 설정 (가지고 있는 웹캠의 FOV를 안다면 그걸 적용하면 됩니다. 저는 몰라서 대충... -_-;)
horizontal_fov = 70  # 수평 FOV (예: 70도)
vertical_fov = 50    # 수직 FOV (예: 50도)


# 카메라 FOV를 기반으로 서보모터 각도 변환
servoX = np.interp(fx, [0, ws], [90 - horizontal_fov / 2, 90 + horizontal_fov / 2])
servoY = np.interp(fy, [0, hs], [90 - vertical_fov / 2, 90 + vertical_fov / 2])

 

 

두 항목이 적용된 코드는 다음과 같습니다.

 

import cv2
from cvzone.FaceDetectionModule import FaceDetector
from pyfirmata import Arduino, util, SERVO
import numpy as np

# 카메라 영상 속성 설정 및 획득
cap = cv2.VideoCapture(0)
ws, hs = 1280, 720
cap.set(3, ws)
cap.set(4, hs)

if not cap.isOpened():
    print("Camera couldn't Access!!!")
    exit()

# 아두이노 보드 지정
board = Arduino("/dev/ttyArduino")

# 서보모터가 사용할 디지털 핀 지정
servo_pinX = board.get_pin('d:9:s') #pin 9 Arduino
servo_pinY = board.get_pin('d:10:s') #pin 10 Arduino

# initial servo position
servoPos = [90, 90] 

# 안면 감지기
detector = FaceDetector()

# 카메라 FOV 설정 (가지고 있는 웹캠의 FOV를 안다면 그걸 적용하면 됩니다. 저는 몰라서 대충... -_-;)
horizontal_fov = 70  # 수평 FOV (예: 70도)
vertical_fov = 50    # 수직 FOV (예: 50도)

# 카메라 영상 획득 및 감지, 트래킹 진행
while True:
    success, img = cap.read()
    img = cv2.flip(img,1)  # 영상 좌우반전 (테스트 목적)
    img, bboxs = detector.findFaces(img, draw=False)

    if bboxs:
        # 안면 바운딩 박스 중심 좌표 추출
        fx, fy = bboxs[0]["center"][0], bboxs[0]["center"][1]
        pos = [fx, fy]
        
        # 카메라 FOV를 기반으로 서보모터 각도 변환
        servoX = np.interp(fx, [0, ws], [90 - horizontal_fov / 2, 90 + horizontal_fov / 2])
        servoY = np.interp(fy, [0, hs], [90 - vertical_fov / 2, 90 + vertical_fov / 2])
        
        # 서보모터 움직임 범위 제어 (0도 < 범위 < 180도)
        servoX = np.clip(servoX, 0, 180)
        servoY = np.clip(servoY, 0, 180)

        # 최종 서보 회전 각도 확정 (X축, Y축)
        servoPos[0] = servoX
        servoPos[1] = servoY

        # 감지된 안면 상에 과녁표시 그리기
        cv2.circle(img, (fx, fy), 80, (0, 0, 255), 2)
        cv2.putText(img, str(pos), (fx+15, fy-15), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)
        cv2.line(img, (0, fy), (ws, fy), (0, 0, 0), 2)  # x line
        cv2.line(img, (fx, hs), (fx, 0), (0, 0, 0), 2)  # y line
        cv2.circle(img, (fx, fy), 15, (0, 0, 255), cv2.FILLED)
        cv2.putText(img, "TARGET LOCKED", (850, 50), cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 255), 3)

    else:
        cv2.putText(img, "NO TARGET", (880, 50), cv2.FONT_HERSHEY_PLAIN, 3, (0, 0, 255), 3)
        cv2.circle(img, (640, 360), 80, (0, 0, 255), 2)
        cv2.circle(img, (640, 360), 15, (0, 0, 255), cv2.FILLED)
        cv2.line(img, (0, 360), (ws, 360), (0, 0, 0), 2)  # x line
        cv2.line(img, (640, hs), (640, 0), (0, 0, 0), 2)  # y line

    # 화면 왼쪽 상단에 서보모터 회전 각도 표시
    cv2.putText(img, f'Servo X: {int(servoPos[0])} deg', (50, 50), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)
    cv2.putText(img, f'Servo Y: {int(servoPos[1])} deg', (50, 100), cv2.FONT_HERSHEY_PLAIN, 2, (255, 0, 0), 2)

    # 서보모터 회전 실행
    servo_pinX.write(servoPos[0])
    servo_pinY.write(servoPos[1])

    # 영상 화면 출력
    cv2.imshow("Image", img)
    cv2.waitKey(1)

 

 

결과 확인

일단 얼굴 감지는 cvzone의 모듈 성능이라 정확도나 추론 속도에서 약간 아쉬운 점은 있습니다. 그렇더라도 너무 빠르게 움직이지만 않는다면 잘 잡아내고 있습니다. 그리고, 서보모터가 장착된 터렛도 얼굴을 추적하며 잘 움직입니다. 

 

사진 : 카메라 인식으로 안면 추적하고 이에 맞춰 터렛이 움직이는 데모 예시

 

 

원래는 영상을 찍어봤는데 그것까지는 아닌 것 같아 사진으로만 대신합니다. 

 

지금까지 진행한 내용들을 정리하면 다음과 같습니다.

 

1.서보모터 (표준형, 180도)를 이용한 터렛을 제작합니다. -> 상용 제품 이용 (SG90 및 브라켓)

2.아두이노를 이용해 서보모터를 제어하고, 이를 python pyfirmata를 이용해 컨트롤 합니다.

3.카메라 FOV를 감안해 서보모터의 움직임을 캘리브레이션 합니다.

4.cvzone 모듈을 이용한 안면 감지를 하고 감지된 얼굴을 따라 터렛이 움직이도록 제어합니다.

 

 

응용은 다양하게 가능할 것 같습니다. 예전에 언급한 바 있듯이 사람을 있는 범위만 움직이는 선풍기라던가, 야생동물이나 침입자 추적 등의 다양한 분야에 활용 가능하겠죠. 아무튼 재미있는 주제였고 잘 마무리되었다고 생각합니다.

 

다음 번에는 이를 활용하는 뭔가를 하나 만들어 보도록 하겠습니다. 가능하면 PC가 아닌 임베디드 보드이면 더 좋겠죠.