AI 탐구노트

Headshot Tracking 따라하기 - 1편 본문

DIY 테스트

Headshot Tracking 따라하기 - 1편

42morrow 2024. 9. 29. 22:50

 

재미난 것 발견!

 

작년 쯤에 유튜브를 보다가 재미난 장난감을 만든 분의 영상과 코드(깃헙)을 발견했습니다.

 

사진 : 안면감지/트래킹에 따라 서보모터를 조정하는 예시

 

 

서보모터를 이용해 Pan-Tilt를 할 수 있는 장비를 만들고 (실제 알리에서 판매하고 있음),

카메라 영상에서 인공지능 안면감지를 이용해 얼굴, 특히 미간을 트래킹하도록 한 것입니다. 

영상을 보면 아무리 피해도 이 스나이퍼는 놓치질 않습니다. 

 

비슷한 영상을 유튜브 상에서 많이 볼 수 있는데 이 경우처럼 단순 트래킹하고 

트래킹된 지점을 화면 상에 표시하는 것 외에 

아이들 장난감을 이용해 발사하는 것까지 구현해 둔 사례도 있죠. 

 

어디에 써먹을 수 있나?

 

실제로 이런 기술이 적용된 사례는 많습니다.

홈 카메라나 화상회의 카메라에 화자의 얼굴을 따라가면 비춰주는 사례도 있고 (상용화된 제품도 있음),

도둑 침입을 감지하고 이에 대한 조명 투사를 하는 솔루션도 있죠.

 

개인적으로는 선풍기에 작은 카메라 장치 하나 달고 사람이 있는 각도만 회전하도록 하는 제품도 가능하다고 봅니다.

물론 워낙 선풍기 자체의 가격이 높지 않아 이런 장치가 가성비가 있으려면 아주아주 저렴하게 제작 가능해야겠죠.

 

만들어 보자~

 

현재 이런 제품은 예시처럼 알리에서 저렴하게 구입할 수 있는 장치들로 구성이 가능합니다.

저도 위 영상에서 사용하고 있는 팬-틸팅 세트를 1만원 이하에 구매할 수 있었습니다. (SG90 또는 SG92 등으로 검색하면 나옴)

그리고, 아두이노보드가 없어서 이 또한 쿠팡에 올라온 아두이노 UNO 호환보드(7천원 이하)를 구입했습니다. 

 

구조는 어떻게 될까?

 

연결된 github 상에는 서보모터를 아두이노에서 어떻게 연결하는지에 대한 이미지만 있고 전체 장비의 구성은 없습니다. 

Python을 이용한 2개의 코드 파일과 안면감지에 사용되는 json 파일 하나만 있습니다. 

안면 감지하는 코드와 안면 트래킹을 하면서 아두이노에 신호를 줘서 서보모터를 컨트롤 하는 코드로 구성되어 있는거죠.

 

그럼 추정할 수 있는 구조는 아래와 같겠죠.

카메라(USB도 무방) - PC - 아두이노 보드 - 팬틸팅 장비(서보 모터 2개) 

 

 

물론 PC 대신 라즈베리파이를 쓴다면 'PC-아두이노 보드' 조합을 대체할 수도 있을 겁니다.

구성은 단순해져서 좋은데 개발 편의성 측면은 PC가 더 낫죠.

쉽게 구성해서 테스트 해 보는 것이 우선이라 위의 구성으로 갑니다.

PC 쪽 코드는 공개되어 있어 동작에는 문제가 없을 것으로 예상됩니다. 

 

 

 

우여곡절 : 다른 장비 (NodeMCU)로 시도해보다

 

아두이노 보드는 아니지만 예전에 장난감 만들려고 구입해 둔 NodeMCU (ESP8266 기반 보드)가 있다는 것이 기억났습니다.

자체로 아두이노와 같이 디지털 신호를 받을 수 있는 핀을 제공하고 아두이노 IDE도 사용할 수 있으니 문제가 없겠다 싶었죠.

 

그런데...

왠 일인지 PC (우분투, 윈도우)에서 USB 연결 시 장치 인식이 안 되더군요.

한 대도 아니고 두 대의 장비가 동일한 증상을 보였죠.

윈도우 10과 예전 버전 우분투에서는 연결했었던 거고, 두 장비 증상이 같으니 이건 보드 자체의 문제는 아닌 것 같고...

 

참고) 윈도우는 NodeMCU 보드에서 사용하는 CH340 드라이버를 사전에 설치해야 하고 리눅스에서는 기본 인식이 되어야 합니다.

 

윈도우 11에서는 장치 관리자에서 '포트/COM'을 항목이 나오지 않았고,

우분투에서도 lsusb에 아예 연결된 USB 장치 목록이 나오지 않았습니다. 

 

이런저런 시도를 해 봤지만 결국 문제 해결을 하지 못했고, 눈물을 머금고 쿠팡에서 아두이노를 주문을 할 수 밖에 없었습니다. 

 

 

장비 수령

 

아두니오 호환 보드

 

주문했던 물품을 받아보니 '아두이노 우노 R3 CH340 호환 보드' 였습니다. 

정품보다 훨신 저렴한 중국제 호환보드인데 뭐 기능 등은 차이가 없을테니 걱정은 없습니다. 

 

아두이노 장비를 수령하고 즉시 연결해 보니 아무런 문제없이 아두이노 연결이 확인됩니다. 역시... 

아래 항목 가운데 'Device 022: ID: la86 QinHeung Electronics CH340 serial converter'라고 되어 있는 부분이 그것입니다. 

흠... 진짜 NodeMCU 보드의 문제인가... -_-;

$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 05e3:0610 Genesys Logic, Inc. Hub
Bus 001 Device 003: ID 05e3:0610 Genesys Logic, Inc. Hub
Bus 001 Device 004: ID 32e6:d412 Web Camera Web Camera
Bus 001 Device 005: ID 0483:5222 STMicroelectronics 108EC-S
Bus 001 Device 006: ID 18f8:0f97 [Maxxter] Optical Gaming Mouse [Xtrem]
Bus 001 Device 010: ID 0bda:8771 Realtek Semiconductor Corp. Bluetooth Radio
Bus 001 Device 022: ID 1a86:7523 QinHeng Electronics CH340 serial converter
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 002 Device 002: ID 05e3:0612 Genesys Logic, Inc. Hub

 

 

장비 정상 작동 확인

 

장치 연결은 확인했지만, 실제로 제대로 동작하는지 확인은 필요합니다.

Python에서 이용하기 전에 먼저 Arduino IDE에서 확인해 보기로 했습니다.

 

보드 및 연결 포트 확인

 

 

Board는 'Arduino Uno'로 선택했고 port는 잡힌 '/dev/ttyUSB0'로 선택했습니다. 

 

 

서보모터 동작 테스트 

 

우선 서보모터 하나를 아두이노 보드에 다음과 같이 연결합니다. 

  • 신호 (노란색) : 디지털 9번
  • 전원 : 5V 
  • 접지 : GND

둘 이상을 연결하기 위해서는 나머지는 동일하게 하고 신호 부분은 다른 핀 (예:10번)으로 설정하면 됩니다.

아두이노 UNO에서 PWM (Pulse Width Modulation: 펄스 폭 변조) 용 디지털 핀은 3,5,6,9,10,11 번 핀이라고 합니다. 

 

 

 

참고글) 아두이노 PWM(펄스 폭 변조) 쉽게 이해하기

 

 

아두이노 스케치 업로드

 

아두이노 보드에 기본 동작할 스케치(Sketch) 업로드를 진행합니다.

이번에 사용할 것은 'StandardFirmata' 로 python에서는 PyFirmata나 PyMata4 등의 패키지를 이용해 명령을 전달할 겁니다. 

 

참고)

Sketch라는 것은 아두이노 보드가 하드웨어와 소프트웨어 간의 명령을 처리하는 방식을 정의하는 프로그램입니다.

이를 통해서 python에서 명령을 주고 아두이노 컨트롤러가 받아서 이를 처리하게 되는거죠.

 

Firmata 스케치는 아두이노와 외부 프로그램 간의 통신 기능을 제공하는 역할을 합니다. 

Firmata는 일종의 아두이노 펌웨어로 아두이노의 핀 상태를 제어하거나 센서 데이터를 읽을 수 있게 해 주죠.

 

Firmata 스케치의 역할을 정리해보면 다음과 같습니다. 

  • 명령 수신 및 실행 : USB로 전달된 명령을 수신하고 하고 이에 따라 핀을 제어하거나 데이터를 반환합니다.
  • 다양한 언어에서 제어 가능 : 표준화된 프로토콜을 이용하기 때문입니다.
  • 외부 로직 사용 : 아두이노 자체에서 로직 처리를 하지 않고 외부에서 로직을 처리하고 아두이노에 명령을 전달할 수 있습니다.

 

 

 

 

Firmata 스케치 동작 확인 

 

아두이노 IDE에서 'Example > Servo > Sweep'을 선택해서 아두이노에 업로드를 합니다.

서보모터가 제대로 동작하는 것을 확인할 수 있었습니다.

여기까지 되면 1.USB 연결 (정상), 2.아두이노 보드 인식(정상), 3.신호 전달 및 처리(정상)을 확인하는 것입니다. 

 

 

 

이제 Python 코드에서 가능한지 확인을 해야 합니다.

 

Python 에서 돌려보기

 

시련 - 1

다음은 Python 코드를 이용해 테스트를 진행한 것입니다. 

pyfirmata 패키지를 이용하는 방식인데, 어찌보면 가장 간단한 코드라 할 수 있습니다. 

import time
from pyfirmata import Arduino, util, SERVO

# Connect to the Arduino board
board = Arduino('/dev/ttyUSB0')  # Adjust port if necessary

# Define the pin for the servo
servo_pin_1 = 9

# Set the pin mode to SERVO
board.digital[servo_pin_1].mode = SERVO

# Servo control function
def set_servo_angle(pin, angle):
    # print(f"Setting servo at pin {pin} to angle {angle}")
    board.digital[pin].write(angle)  # Write the angle directly for servo control
    time.sleep(0.1)

try:
    while True:
        for i in range(0,90):
            set_servo_angle(servo_pin_1, i)
        for i in range(90,1,-1):
            set_servo_angle(servo_pin_1, i)


except KeyboardInterrupt:
    print("Program stopped")

finally:
    board.exit()  # Close the connection

 

 

동작은 되는데 몇 가지 문제가 있었습니다.

 

1.움직임을 지정한 각도가 90도인데, 이보다 훨~씬 많이 돌아갑니다. 

2.프로그램을 Vscode상에서 종료해도 아두이노는 동작을 합니다. 즉, board.exit()이 걸리지 않은거죠.

 

1번에 대해서는 도대체 이게 뭔 일인가...

고민을 하다가 찾은 것은 구입한 서보모터가 360 degree라고 적혀 있었다는  것이었습니다. 

 

참고글) 서보모터에는 두 종류가 있는데 알고 계시나요?

 

위 사이트의 설명에 따르면 아두이노에서 사용하는 서보모터 유형은 다음과 같이 2가지가 있다고 합니다. 

  • 표준형 (Standard servo motor)
    좌우 90도(180도 이내만 회전. 감시센서 용도로 사용. 대신 0~180으로 해놔도 대략적인 각도(5~170도)로 맞음
  • 연속형 (Continuous rotation servo motor)
    360도 무한 회전 방식. 바퀴 회전 용도도 사용. 제가 구입한 것! T^T 

 

하드웨어 쪽은 문외한이라... 뭔가 각도가 더 넓으면 좋겠지 싶어서 선택한 것이었는데... 그게 아니었나봅니다. 

 

조금 더 조사를 해 보니 이렇게도 나뉘었습니다. (이건 그냥 참고로)

  • 아날로그 서보모터 : 전류의 양으로 모터의 회전 각을 조절
  • 디지털 서보모터 : 전류가 흐르는 시간을 조절해 회전 각을 조절

 

시련 - 2

 

연속형의 작동 방식에서 디지털 핀으로 전달하는 인자값이 angle이 아니라는 것도 알게 됐습니다. 

엥? 그냥 각도 넣으면 각도대로 움직이는게 아니었어? 

움직이는 시간이라고 되어 있는데 실제 입력한 값은 또 그 정도는 아닌데... 

 

write(value) 함수에서 value의 값이 표준형과 연속형에서 다음과 같이 차이가 납니다.

  • 표준형 : 회전시키려는 각도
  • 연속형 : 0이 정방향 최고속도, 180이 역방향 최고속도, 90은 정지

 

예를 들어 연속형의 경우, 커맨드를 0, 180, 90 중에서 value값으로 설정하고

회전 시간을 time.sleep(rotation_time)으로 따로 설정하도록 해야 한다는 얘기로 이해 했습니다. 

 

그렇다면 회전 시간으로 돌아가는 각도를 역으로 계산하는 로직이 필요하다는건데... 

다행히도 이 부분까지 알려주신 분이 있었습니다. (링크)

이 분에 따르면 SG90 서보모터의 경우, 0.12초에 60도 회전을 한다고 합니다. 

하지만, 제것은 0.08초 정도를 해야 비슷하게 맞춰졌습니다. 그래서, 60도 회전 시간을 factor로 해서 0.08로 맞춰서 해 봅니다. 

 

import time
from pyfirmata import Arduino, util, SERVO
from pynput import keyboard

# Connect to the Arduino board
board = Arduino('/dev/ttyUSB0')  

# Define the pin for the servo
servo_pin_1 = 9

# Set the pin mode to SERVO
board.digital[servo_pin_1].mode = SERVO

# Variable to keep track of whether the program should stop
stop_program = False

# Servo control function for continuous rotation servo
def set_servo_speed(pin, speed):
    board.digital[pin].write(speed)
    time.sleep(0.1)

# Function to control rotation for a specific angle
def rotate_for_angle(pin, speed, angle):
    adjusted_rotation_time_per_60_deg = 0.08
    rotation_time = (angle / 60) * adjusted_rotation_time_per_60_deg  
    set_servo_speed(pin, speed)
    time.sleep(rotation_time)  # Rotate for the calculated time
    set_servo_speed(pin, 90)  # Stop the servo after rotating

# Function to handle key press events
def on_press(key):
    global stop_program
    try:
        if key.char == 'q':  # If 'q' is pressed, set stop_program to True
            stop_program = True
            return False  # Stop listener
    except AttributeError:
        pass

# Start listening for key presses
listener = keyboard.Listener(on_press=on_press)
listener.start()

try:
    while not stop_program:
        # Rotate 180 degrees (estimated with adjusted time)
        print("Rotating clockwise for 180 degrees...")
        rotate_for_angle(servo_pin_1, 180, 180)  # Rotate 180 degrees in clockwise direction

        time.sleep(1)

        # Rotate -180 degrees (estimated with adjusted time)
        print("Rotating counterclockwise for 180 degrees...")
        rotate_for_angle(servo_pin_1, 0, 180)  # Rotate 180 degrees in counterclockwise direction

        time.sleep(1)

except KeyboardInterrupt:
    print("Program interrupted with keyboard")

finally:
    # Stop the servo when the program ends
    set_servo_speed(servo_pin_1, 90)  # Set to neutral (stop)
    board.exit()  # Close the connection
    print("Servo stopped and board connection closed.")

 

 

이제 완성되었으니 실제 구동을 해 봅니다.

대략 180도를 돌았다가 다시 반대로 회전했다가 하는 과정을 거치네요.

하지만, 각도가 정확하진 않으며 돌때마다 약간씩 차이가 나는 것 같긴 합니다만 그냥 진행합니다.  

이걸 눈대중으로 맞춰야 한다면 저는 자신이 없습니다. 눈도 침침한데... ^^; 

 

서보모터 2개 연결

 

영상에서 본 기능을 구현하기 위해서는 서보모터 2개를 연결하고 진행해야 합니다.

 

나머지 서보모터 하나를 다음과 같이 연결합니다. 

  • 신호 (노란색) : 디지털 10번
  • 전원 : 5V 
  • 접지 : GND

 

동일한 코드로 30도만 상하로 움직이기 했더니 일단 동작은 됩니다.

휴... 이제 하드웨어 쪽은 겨우 작동을 시킬 수 있게 되었습니다. 

 

문제는 앞서 언급한 것처럼 각도 조절이 생각했던 것만큼 정확하게 되지 않는다는 것인데... 

각도를 입력으로 받는 표준형 서보모터가 있으면 그걸로 해 봤으면 좋겠는데 그러질 못하네요.

그래도 혹시나 해서 알리에 주문을 넣어두긴 했습니다. 하지만 오기까진 한참 걸리겠죠? ^^; 

일단 불완전한 상태로라도 그대로 진행해 볼 생각입니다. 

 

다음 편에는 안면감지, 트래킹 코드와 하드웨어 컨트롤하는 코드를 결합하는 부분을 테스트해 보고 소개하겠습니다.