1. Aws와 Node-red 연결하기

2. 라즈베리파이 publish -> Node-red subcribe 예제 (Switch)

3. Node-red publish -> 라즈베리파이 subcribe 예제 (LED)

 

 

 


1. Aws 와 Node-red 연결하기

 

Node.js 를 설치합니다

 

설치 후 Nodejs command prompt 를 실행합니다

 

node-red 명령어를 입력하여 node-red 를 실행합니다

node-red

 

실행 후 크롬 창에서 http://127.0.0.1:1880/ 에 접속합니다

 

 

 

다음과 같은 node-red 창이 나옵니다

 

 

아래 블록들을 다음과 같이 배치합니다

파란색 동그라미는 수정사항이 있다는 표시이고 빨간색 세모는 연결이 안 됐거나 문제가 있는 표시입니다

 

더블클릭하여 해당 노드 설정에 들어갈 수 있습니다

 

 

Inject : 

입력입니다

테스트라는 string을 보내도록 설정합니다

 

 

 

 

mqtt out:

토픽은 test, Qos는 0, 보존은 하지 않는다 로 설정한 후 빨간 네모 펜 표시로 들어갑니다

 

원하는 이름을 지정하고  서버는 본인의 엔드포인트를 붙여 넣기, 포트는 8883으로 기입하고 또다시 빨간 네모 펜 표시로 들어갑니다

 

 

라즈베리파이에서 인증서와 프라이빗 키, CA인증서를 pc로 가져온 후 파일 버튼을 눌러 해당 파일을 지정합니다

 

전부 변경을 누르면 완료

 

 

 

mqtt in:

 

서버는 mqtt out에서 만든 서버가 그대로 저장되어 있기 때문에 화살표 버튼으로 가져올 수 있습니다

 

 

토픽은 test, QoS는 0으로 지정합니다

 

 

 

 

 

이제 모든 설정을 완료하였으므로 우측 상단의 배포하기를 누릅니다

 

초록 불과 함께 접속됨이 보인다면 성공입니다

 

 

우측의 벌레 모양을 누르면 디버그를 확인할 수 있으며 inject 옆의 파란 버튼을 누르면 활성화됩니다

 

디버그 창에서 "테스트"가 제대로 발행되고 구독하였음을 알 수 있습니다

 

 

 

 

같은 test라는 이름으로 구독한다면 Aws 의 mqtt 클라이언트 테스트 화면과 라즈베리파이의 subscribe.py 를 실행했을 때도 구독이 되었음을 볼 수 있을 것입니다

 

 

 

 

 

2. 라즈베리파이 publish -> Node-red subcribe 예제 (Switch)

 

mqtt in 와 text 를 가져와 다음과 같이 배치합니다

 

mqtt in:

다음과 같이 설정합니다

서버는 위에서 만든 서버를 그대로 가져옵니다

 

 

text:

아래와 같이 설정하고 Group을 추가합니다

 

dashboard group의 설정입니다

 

 

같은 Tab을 가진 노드끼리는 같은 dashboard 상에서 표시되고

같은 Name을 가진 노드끼리는 dashboard 상에서 같은 묶음으로 표시됩니다

 

 

완료하였으면 변경을 누르고

 

배포하기를 누릅니다

 

 

이제 라즈베리파이의 스위치와 케이블이 잘 연결이 되었는지 확인한 후

 

라즈베리파이에서 발행하는 코드를 생성합니다

import time, json, ssl
import paho.mqtt.client as mqtt
import RPi.GPIO as GPIO


# aws set
ENDPOINT = "본인의 엔드포인트"
THING_NAME = 'raspi'
CERTPATH =  "/home/pi/aws/raspi.cert.pem" # cert파일 경로
KEYPATH = "/home/pi/aws/raspi.private.key" # key 파일 경로
CAROOTPATH = "/home/pi/aws/root-CA.crt" # RootCaPem 파일 경로
TOPIC = 'input' #Topic명

# GPIO set
GPIO.setmode(GPIO.BCM)
SW1 = 22
SW2 = 23
SW3 = 24
SW4 = 25

"""
GPIO.setmode(GPIO.BOARD)
SW1 = 15
SW2 = 16
SW3 = 18
SW4 = 22
"""
GPIO.setup(SW1, GPIO.IN)
GPIO.setup(SW2, GPIO.IN)
GPIO.setup(SW3, GPIO.IN)
GPIO.setup(SW4, GPIO.IN)


def on_connect(mqttc, obj, flags, rc):
  if rc == 0: # 연결 성공
    print('connected!!')

# SW 컨트롤에 따라 다른 값 발행하는 함수
def GPIO_Input_SW():
  if GPIO.input(SW1) == False:
      comment = "1 Button push !!"
  elif GPIO.input(SW2) == False:
      comment = "2 Button push !!"
  elif GPIO.input(SW3) == False:
      comment = "3 Button push !!"
  elif GPIO.input(SW4) == False:
      comment = "4 Button push !!"
  else:
      comment = None
      
  if comment is not None:
      payload = json.dumps(comment) #메시지 포맷
      mqtt_client.publish(TOPIC, payload, qos=1) #메시지 발행
      print("SW {}".format(comment))
      time.sleep(0.5)
  
    
try:    
  mqtt_client = mqtt.Client(client_id=THING_NAME)
  mqtt_client.on_connect = on_connect
  mqtt_client.tls_set(CAROOTPATH, certfile= CERTPATH, keyfile=KEYPATH, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
  mqtt_client.connect(ENDPOINT, port=8883)
  mqtt_client.loop_start()
  
  while True: 
      GPIO_Input_SW()
      time.sleep(0.01)

except KeyboardInterrupt:
  pass
  
mqtt_client.disconnect()
GPIO.cleanup()
print(end='\n')

그리고 코드를 실행합니다

connected! 가 출력되었다면 발행에 성공한 것입니다

 

 

 

대시보드로 들어갑니다

 

대시보드 창으로 들어가 보면 다음과 같고

SW1~SW4에 따라 다음과 같은 메시지를 반환합니다

 

리눅스 실행화면은 다음과 같습니다

 

 

 

 

 

3. Node-red publish -> 라즈베리파이 subcribe 예제 (LED)

 

이번에는 왼쪽의 노드 중에 mqtt out 1개와 switch 3개 를 가져와 배치합니다

 

mqtt out:

토픽을 led로 지정하고 서버는 위에서 사용한 서버를 가져옵니다

 

switch:

Label 은 node-red 상에서 표시되는 이름입니다

 

 

On/Off Payload는 스위치가 on/off 되었을 때 출력하는 값입니다

형식은 JSON으로 지정하고 JSON 형식의 코드를 작성합니다

 

 

{
	"color":"red",
	"onoff":"1"
}

 

 

Gourp 설정입니다

 

Red switch 설정을 완료하였습니다

Green과 Blue 도 똑같이 진행하되 JSON 부분만 Green, Blue로 변경하시면 됩니다

 

 

 

전부 완료하신 후 배포하기를 누릅니다

 

 

 

배포 후 대시보드 창에 들어가면

3개의 스위치가 활성화되어 있음을 볼 수 있습니다

 

 

라즈베리파이에서 led의 스위치 케이블을 모두 연결한 후 코드를 생성합니다

 

import time, json, ssl
import paho.mqtt.client as mqtt
import RPi.GPIO as GPIO

# aws set
ENDPOINT = "본인의 엔드포인트"
THING_NAME = 'raspi'
CERTPATH =  "/home/pi/aws/raspi.cert.pem" # cert파일 경로
KEYPATH = "/home/pi/aws/raspi.private.key" # key 파일 경로
CAROOTPATH = "/home/pi/aws/root-CA.crt" # RootCaPem 파일 경로
TOPIC = 'led' #Topic명
recvData = ""

# GPIO set
GPIO.setmode(GPIO.BCM)
Led_Red = 4
Led_Green = 5
Led_Blue = 6

"""
GPIO.setmode(GPIO.BOARD)
Led_Red = 7
Led_Green = 29
Led_Blue = 31
"""

GPIO.setup(Led_Red, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(Led_Blue, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(Led_Green, GPIO.OUT, initial=GPIO.LOW)


# GPIO 값에 따른 출력 함수
def GPIO_Led_onoff(Led, light):
  if Led == 'Red':
    Led_color = Led_Red
  elif Led == 'Green':
    Led_color = Led_Green
  elif Led == 'Blue':
    Led_color = Led_Blue
  
  if light == 1:
    print_light = "On"
  else:
    print_light = "Off"

  GPIO.output(Led_color, light)
  print("{} Led {} !!".format(Led, print_light))


# 메세지를 받았을때
def on_message(mqttc, obj, msg):
  time.sleep(0.1)
  recvData = str(msg.payload.decode("utf-8"))
  
  try:
    # json data로 전환 및 데이터 반환
    jsonData = json.loads(recvData)
    Led = str(jsonData["color"])
    light = int(jsonData["onoff"])
    # 데이터에 따른 GPIO 동작 함수
    GPIO_Led_onoff(Led, light)
  except :
    pass


def on_connect(mqttc, obj, flags, rc):
  if rc == 0: # 연결 성공
    print('connected')
    

def on_subscribe(mqttc, obj, mid, granted_qos):
  print("Subscribed: "+TOPIC)

  
try: 
  mqtt_client = mqtt.Client(client_id=THING_NAME)
  mqtt_client.on_message = on_message
  mqtt_client.on_connect = on_connect
  mqtt_client.on_subscribe = on_subscribe

  mqtt_client.tls_set(CAROOTPATH, certfile= CERTPATH, keyfile=KEYPATH, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
  mqtt_client.connect(ENDPOINT, port=8883)
  mqtt_client.subscribe(TOPIC)
  mqtt_client.loop_start()

  while True:
      time.sleep(1)

except KeyboardInterrupt:
    pass
  
mqtt_client.disconnect()
GPIO.cleanup()
print(end='\n')

on_message() 함수에서 받은 JSON data에서 color 값과 onoff 값을 가져옵니다

 

가져온 값에 따라서 GPIO_Led_onoff() 함수에서 GPIO를 출력하고 print 합니다

 

 

 

 

라즈베리파이에서 코드를 실행합니다

위와 같은 출력이 나온다면 잘 구독이 되었습니다

 

 

 

이제 대시보드의 스위치를 작동해봅니다

 

 

 

 

라즈베리파이

 

Node-red

리눅스 실행화면

 

 

 

 

 

 

 

 

 

 

이렇게 라즈베리파이에서 AWS IoT Core, mqtt, Node-red를 이용하여 연결시켜 보았습니다

이제 라즈베리파이에 어떤 센서를 연결하여도 값을 받을 수 있을 것이고 조금 더 응용한다면 스마트폰에서도 값을 받는 것도 가능할 것 같습니다

 

 

1. 라즈베리파이에 aws 연결하기

2. 라즈베리파이 publish -> Aws subcribe 예제

3. Aws publish -> 라즈베리파이 subcribe 예제

 

 


1. 라즈베리파이에 aws 연결하기

 

 

먼저 AWS에서 AWS IoT Core에 들어갑니다 (AWS 회원가입 필수)

 

Connect - Get started 에 들어가 시작하기를 누릅니다

 

 

라즈베리파이에 연결하기 위해 Linux, Python 으로 선택합니다

 

 

사물 등록을 위해 원하는 이름을 지정합니다

 

 

사물이 생성되었다는 메시지와 함께 위 창이 나왔습니다

 

연결 키트를 다운로드하고 라즈베리파이 안에 넣습니다

MobaXterm을 사용하신다면 드래그 앤 드롭만으로 쉽게 넣을 수 있습니다

 

이 연결 키트 안에 인증서와 프라이빗 키 등이 전부 들어있습니다

 

라즈베리파이 상에서 연결 키트를 넣은 폴더로 들어가 명령어를 차례대로 실행합니다

 

unzip connect_device_package.zip
chmod +x start.sh
./start.sh

 

위와 같은  {"message": "Hello World!", "sequence": 0} 형태가 나오면 성공입니다

 

 

메시지를 라즈베리파이에 전송해 볼 수 있습니다

잘 전송이 되었네요

 

 

보안 - 인증서 로 들어가면 생성한 인증서를 볼 수 있습니다

 

인증서 안으로 들어가 보면

정책과 사물이 전부 연결되어 있음을 볼 수 있습니다

 

여기서 알 수 있듯이 사물 - 인증서 그리고 인증서 - 정책 으로 연결되어 있습니다

 

만약 연결 키트로 하지 않으신다면 각각 생성 후 연결해 주어야 합니다

 

 

 

 

다음은 정책 설정을 해야 합니다

정책은 aws에 들어오는 디바이스 중 접근을 거부하거나 허용하는 권한을 정합니다

보안 - 정책 을 통해 정책으로 들어가 줍니다

그리고 본인의 정책으로 들어갑니다

 

활성 버전 편집, JSON 으로 들어가 코드를 수정해줍니다

이 코드는 모든 접근을 허용한다는 코드입니다

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:*",
      "Resource": "*"
    }
  ]
}

수정 후 하단의 새 버전으로 저장을 누릅니다

 

 

방금 설정한 버전 2를 활성으로 설정합니다

 

 

 

 

 

2. 라즈베리파이 publish -> Aws subcribe 예제

 

mqtt 사용을 위해 라즈베리파이에 paho-mqtt 설치

pip install paho-mqtt

 

 

설정에서 엔드포인트를 가져와 복사합니다

 

 

테스트 - 주제 구독 에서

test라는 주제를 구독합니다

 

 

라즈베리파이에서 다음과 같은 코드를 생성하고 publish.py 로 저장합니다

이때 엔드포인트는 위에서 저장한 엔드포인트를 넣으며 파일 경로는 절대 경로를 넣어줍니다

import time, json, ssl
import paho.mqtt.client as mqtt

ENDPOINT = "본인의 엔드포인트"
THING_NAME = 'raspi'
CERTPATH =  "/home/pi/aws/raspi.cert.pem" # cert파일 경로
KEYPATH = "/home/pi/aws/raspi.private.key" # key 파일 경로
CAROOTPATH = "/home/pi/aws/root-CA.crt" # RootCaPem 파일 경로
TOPIC = 'test' #주제


def on_connect(mqttc, obj, flags, rc):
  if rc == 0: # 연결 성공
    print('connected!!')

try: 
  mqtt_client = mqtt.Client(client_id=THING_NAME)
  mqtt_client.on_connect = on_connect
  mqtt_client.tls_set(CAROOTPATH, certfile= CERTPATH, keyfile=KEYPATH, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
  mqtt_client.connect(ENDPOINT, port=8883)
  mqtt_client.loop_start()
  
  i=0
  while True: 
    payload = json.dumps({'action': i*0.1}) #메시지 포맷
    mqtt_client.publish('test', payload, qos=1) #메시지 발행
    i=i+1 
    time.sleep(2)

except KeyboardInterrupt:
    pass
  
mqtt_client.disconnect()
print(end='\n')

그리고 다음 명령어로 파이썬 코드를 실행해 줍니다

python publish.py

라즈베리파이에서 connected!! 가 출력되며

 

MQTT 테스트 클라이언트에서 다음과 같이 2초에 0.1 씩 늘어나는 숫자를 받고 있다면 성공입니다

 

 

 

 

3. Aws publish -> 라즈베리파이 subcribe 예제

 

이번에는 라즈베리파이에서 subscribe.py 라는 이름의 코드를 생성합니다

import time, json, ssl
import paho.mqtt.client as mqtt

ENDPOINT = "본인의 엔드포인트"
THING_NAME = 'raspi'
CERTPATH =  "/home/pi/aws/raspi.cert.pem" # cert파일 경로
KEYPATH = "/home/pi/aws/raspi.private.key" # key 파일 경로
CAROOTPATH = "/home/pi/aws/root-CA.crt" # RootCaPem 파일 경로
TOPIC = 'test' #주제

def on_connect(mqttc, obj, flags, rc):
  if rc == 0: # 연결 성공
    print('connected')
    
def on_message(mqttc, obj, msg):
  print(msg.topic+":"+str(msg.payload))

def on_subscribe(mqttc, obj, mid, granted_qos):
  print("Subscribed: "+TOPIC)

try:
  mqtt_client = mqtt.Client(client_id=THING_NAME)
  mqtt_client.on_message = on_message
  mqtt_client.on_connect = on_connect
  mqtt_client.on_subscribe = on_subscribe
  
  mqtt_client.tls_set(CAROOTPATH, certfile= CERTPATH, keyfile=KEYPATH, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
  mqtt_client.connect(ENDPOINT, port=8883)
  mqtt_client.subscribe(TOPIC)
  mqtt_client.loop_start()
  while(1):
      time.sleep(2)

except KeyboardInterrupt:
    pass
  
mqtt_client.disconnect()
print(end='\n')

똑같이 다음 명령어로 실행합니다

python subscribe.py

위와 같은 출력이 나오고

 

 

MQTT 테스트 클라이언트로 들어갑니다

 

주제 게시로 들어가 주제 이름을 test 라 하고 게시합니다

 

 

라즈베리파이 상에서 위와 같이 나오면 성공입니다

 

 

준비물 :

raspberry pi

sd 카드 (저는 16GB로 진행했습니다)

sd 카드 리더기

 

준비 프로그램 : 

Raspberry Pi Imager

Angry IP Scanner 또는 개인 공유기 관리자 페이지

MobaXterm

VNC Viewer

 

 

 

 

 

목차

 

1. SD 카드에 Image write

2. 라즈베리파이 IP 확인

3. 라즈베리파이 연결

4. 라즈베리파이 원격연결

 

pc에 와이파이가 연결된 상태로 진행하시는 게 편합니다

 

 


1. SD 카드에 Image write

 

Operating System : Raspberry Pi OS (other) - Raspberry Pi OS FULL(32-BIT)

 

Storage : 삽입 한 SD 카드 선택

 

전부 선택하셨으면 다음과 같습니다

 

 

 

 

여기서 우측 하단의 설정 버튼을 누르시거나 윈도우 + shift + x를 누르시면 설정 창이 나타납니다

 

 

 

 

여기서  세 가지 를 체크해 줍니다

 

Enable SSH - ssh를 활성화합니다

다른 글에서 ssh 파일을 넣는 방법과 같습니다

 

Set username and password - 유저 이름과 비밀번호를 설정합니다.

유저 이름은 기본으로 pi로 설정되어있습니다

 

Configure wifi - pc가 와이파이가 연결되어있다면 자동으로 와이파이와 비밀번호가 입력되어 있으니 체크만 하시면 됩니다 이때 와이파이는 2.4G로 잡습니다

다른 글에서 wpa_supplicant.conf 파일을 넣는 방법과 같습니다

 

 

 

완료했다면 WRITE 버튼을 누르고 기다린 후

끝났다면 SD 카드를 라즈베리파이에 삽입한 후 전원을 켭니다

 

 

 

 

 

2. 라즈베리파이 IP 확인

 

Angry IP Scanner를 켭니다

위와 같은 현재 pc가 연결되어 있는 ip의 0~255의 range 가 자동 설정되며

 

혹시라도 설정이 안 되시면

cmd 창에서 ipconfig 명령어를 입력하셔서

현재 pc 가 연결되어 있는 ip를 볼 수 있습니다

 

 

 

이제 Angry IP Scanner의 start 버튼을 누르면 ip scan 이 진행되고 연결된 ip를을 보여줍니다

115번에 pc가 연결되어 있는 것이 보이고

139번이 라즈베리파이가 연결되어 있는 것이 보입니다

 

그러므로 저의 라즈베리파이의 ip는 192.xxx.xxx.139 네요

 

 

 

 

 

3. 라즈베리파이 연결

 

다음은 MobaXterm을 실행합니다

Session - SSH로 들어가서 Remote host에 본인의 라즈베리파이의 ip를 입력합니다

 

로그인 창이 나오면 성공입니다

처음에 설정한 아이디와 비밀번호를 입력합니다

(비밀번호는 입력 시 화면에 표시되지 않습니다!)

 

 

올바르게 입력했다면 비밀번호를 저장할 건지 묻는 창이 나온 후 접속이 완료됩니다

 

 

Putty를 사용할 수도 있지만 좌측의 GUI 가 편해서 MobaXterm을 선택하였습니다

드래그 앤 드롭으로 windows에서의 파일을 옮기기도 쉬워서 리눅스 환경이 아직 어렵다면 MobaXterm 이 좋지 않을까 싶네요

 

 

 

4. 라즈베리파이 원격 연결

 

순서대로 다음 명령어를 실행합니다

 

sudo apt-get update
sudo apt-get upgrade

 

 

sudo raspi-config

라즈베리의 환경설정에 진입했습니다

 

방향키로 위아래로 움직일 수 있고

Tab 키로 <Select>로 움직일 수 있습니다

 

3 Interface Options로 진입하여 SSH와 VNC Server를 enable 합니다

 

 

Tab - Finish로 설정을 빠져나옵니다

 

 

다음 명령어를 순서대로 입력합니다

sudo apt-get install tightvncserver
vncserver -geometry 1280x1024

 

문제가 없으면 password를 입력하라는 문구가 뜹니다

입력하면 한번 더 입력하라고 합니다

 

 

 

 

 

 

이제 VNC Viewer를 엽니다

VNC Server 주소를 넣으라는 창에 라즈베리파이의 ip를 입력합니다

 

 

 

위와 같은 창이 뜨는데 라즈베리파이의 아이디와 비밀번호를 입력하시면 됩니다

 

 

 

 

여기까지 모니터/키보드 없이 라즈베리파이 설치하는 방법이었습니다

 

사용한 버전

  • OpenCV = 3.4.2

 

Opencv 에서는 object tracking 을 위하여 제공하는 Tracking API 가 존재합니다

 

 

https://docs.opencv.org/3.4/d9/df8/group__tracking.html

 

OpenCV: Tracking API

Long-term optical tracking API Long-term optical tracking is an important issue for many computer vision applications in real world scenario. The development in this area is very fragmented and this API is an unique interface useful for plug several algori

docs.opencv.org

 

먼저 원하는 trakcer 를 지정합니다

(OpenCV 버전에 따라 가능한 tracker가 달라질 수 있습니다)

 

  • tracker  = cv2.TrackerBoosting_create()
  • tracker  = cv2.TrackerMIL_create()
  • tracker  = cv2.TrackerKCF_create()
  • tracker  = cv2.TrackerTLD_create()
  • tracker  = cv2.TrackerMedianFlow_create()
  • tracker  = cv2.TrackerGOTURN_create()
  • tracker  = cv2.TrackerCSRT_create()
  • tracker  = cv2.TrackerMOSSE_create()

 

 

GOTURN 은 딥러닝 기반의 tracker 로 'goturn.caffemodel' 와 ''goturn.protxt' 파일이 필요합니다

https://github.com/opencv/opencv_extra/tree/c4219d5eb3105ed8e634278fad312a1a8d2c182d/testdata/tracking

 

 

 

traker 를 초기화 시켜줍니다

 

  • traker.init(img, bbox)

img : 이미지

bbox : tracking할 객체의 바운딩 박스로 (x, y, w, h) 의 형태로 입력합니다

 

 

 

그리고 다음 프레임에서 부터 update 를 통해 bbox를 갱신시켜줍니다

  • status, bbox = cv2.traker.update(img)

img : 이미지

status : tracking 성공여부

bbox : tracking한 바운딩 박스 (tracking에 실패해도 반환은 함)

 

 

 

우선 동영상 하나와 간단한 코드를 가지고 tracking을 시도해 보겠습니다

import cv2

video_src = "tracking.mp4"

#webcam = cv2.VideoCapture(1)
webcam = cv2.VideoCapture(video_src)
tracker = cv2.TrackerMIL_create()

_, img = webcam.read()
# 추적 할 객체 지정
bbox = cv2.selectROI('Tracking', img, False)
tracker.init(img, bbox)


while True:
    timer = cv2.getTickCount()

    _, img = webcam.read()
    status, bbox = tracker.update(img)
	
    # 추적 성공
    if status:
        x, y, w, h = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])
        cv2.rectangle(img, (x, y), ((x + w), (y + h)), (0, 255, 0), 3, 1)
        cv2.putText(img, 'tracking', (70, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
    # 추적 실패
    else:
        cv2.putText(img, 'lost', (70, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

    fps = cv2.getTickFrequency()/(cv2.getTickCount()-timer)
    cv2.putText(img, 'fps : '+str(int(fps)), (70,50), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)

    cv2.imshow('Tracking', img)

    if cv2.waitKey(1) & 0xff == ord('q'):
        break

 

 

tracker 는 MIL를 사용하였고 

 

 

 

이번엔 모든 tracker를 적용해 보고 확인해 봅시다.

이 때 한번이라도 추적에 실패한 tracker들은 다음 프레임부터는 추적하지 않도록 하였습니다

 

import cv2
import numpy as np

video_src = "tracking.mp4"

#webcam = cv2.VideoCapture(1)
webcam = cv2.VideoCapture(video_src)

# trackers 지정
trackerName = ['Boosting', 'MIL', 'KCF', 'TLD', 'MedianFlow', 'GOTURN', 'CSRT', 'MOSSE']
trackers = [
            cv2.TrackerBoosting_create(),
            cv2.TrackerMIL_create(),
            cv2.TrackerKCF_create(),
            cv2.TrackerTLD_create(),
            cv2.TrackerMedianFlow_create(),
            cv2.TrackerGOTURN_create(),
            cv2.TrackerCSRT_create(),
            cv2.TrackerMOSSE_create()
            ]


status, img = webcam.read()
bbox = cv2.selectROI('Tracking', img, False)

# trackers 초기화
t = 0
color = []
for tracker in trackers:
    tracker.init(img, bbox)
    colors = np.random.uniform(0, 255, size=(1, 3))
    color.append(colors[0].tolist())
    t += 1


def drawBox(img, bbox, line, color):
    x, y, w, h = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])
    cv2.rectangle(img, (x, y), ((x+w), (y+h)), color, 3, 1)
    cv2.putText(img, trackerName[t], line, cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)

tracker_num = list(range(8))

while True:
    status, img = webcam.read()
    for t in tracker_num:
        status, bbox = trackers[t].update(img)
        # 추적 성공한 tracker
        if status:
            drawBox(img, bbox, (20, 30*t+20), color[t])
        # 추적 실패한 tracker
        else:
            tracker_num.remove(t)

    cv2.imshow('Tracking', img)

    if cv2.waitKey(1) & 0xff == ord('q'):
        break

 

 

 

이 영상에서 우리는 경험적으로

Bossting, MIL, CSRT 가 좋은 성능을 보여줌을 알 수 있습니다

(물론 다른 상황에서는 다른 tracker가 더 좋은 성능을 보일 수 도 있습니다)

 

 

 

 

그리고 객체 추적에서 하나 더 중요하게 여겨지는것은 속도 입니다

위의 코드에도 있듯이 fps 를 계산 하여 출력하였는데 일반적으로 우리가 real time 으로 느끼는 fps는 30이상이여야 한다고 합니다

 

 

현재 cpu만 사용하고 있는 제 PC 기준으로 같은 영상으로 각각의 tracker의 fps를 확인해보면

 

 

Boosting : 30~40

MIL : 10~20

KCF : 130~150

TLD : 15~20

MedianFlow : 170~220

GOTURN : 15~20

CSRT : 20~40

MOSSE : 500~600

 

 

결과적으로 CSRT가 가장 안정적이면서 적당한 속도를 내는것 같습니다

 

사용한 버전

  • OpenCV = 4.5.2

 

OpenCV에서 제공하는 cv2.getPerspectiveTransform(), cv2.warpPerspective() 를 이용하여

원근 변환 (Perspective Transfrom)을 살펴보고 webcam을 이용한 간단한 스캔 프로그램을 만들어 보겠습니다

 

자주쓰는 스캔어플인 Adobe Scan과 같은 프로세스로 진행해보려 합니다

 

 

 

원근 변환 (Perspective Transfrom)

원근 변환은 테두리가 되는 꼭지점 4점을 기준으로 이미지를 변화하는 변환인데

같은 기하학전 변환인 아핀 변환과 다르게 원근 변환은 이미지를 들어 올리거나 기울이는 것처럼 보이는 원근감에 변화를 주는 변환입니다

 

import cv2
import numpy as np

img = cv2.imread('milk.jpg')

h, w = img.shape[:2] # (700, 525)
pt1 = np.float32([[0, 0], [0, h], [w, 0], [w, h]])
pt2 = np.float32([[100, 100], [100, h-100], [w-20, 20], [w-20, h-20]])

mtrx = cv2.getPerspectiveTransform(pt1, pt2)
result = cv2.warpPerspective(img, mtrx, (w, h))

add = cv2.hconcat([img, result])
cv2.imshow('concat', add)
cv2.waitKey(0)
cv2.destroyAllWindows()

cv2.getPerspectiveTransform() 이 변환 전, 후의 좌표를 원근 변환에 필요한 행렬로 변화시켜주고

cv2.warpPerspective() 에서 변화시킨 행렬로 이미지를 변환시켜줍니다

 

그 결과 다음과 같은 원근 변환된 이미지를 얻을 수 있습니다

 

 

 

 

 

webcam과 원근 변환을 이용한 스캔

 

진행하려는 프로세스는 다음과 같습니다

 

카메라로 비췄을때 이미지처리를 통해 문서로 추정되는 네 점의 좌표를 가져옵니다

만약 좌표를 찾지 못한하면 수동으로 좌표를 가져옵니다

 

그 후 네점의 원근 변환을 이용하여 문서를 스캔합니다

 

또한 pytesseract를 이용하여 문서의 글씨를 읽어보기까지 해보려고 합니다

 

import cv2
import imutils
import numpy as np
import pytesseract
import math

pytesseract.pytesseract.tesseract_cmd = r'C:/Program Files/Tesseract-OCR/tesseract.exe'

scan_cnt = np.zeros((4, 2), dtype=np.float32)
DOT = False
DOTSIZE = 15

scan_cnt : 좌표를 넣을 빈 numpy array

DOT : 좌표의 존재 유무에 따른 BOOL

DOTSIZE : 좌표 점의 크기

 

 

def ImageRead(image):
    SIZE = 1000
    image2 = image.copy()
    h, w = image.shape[:2]
    if w or h >= SIZE:
        if h > w:
            image = imutils.resize(image2, height=SIZE, inter=cv2.INTER_AREA)
        else:
            image = imutils.resize(image2, width=SIZE, inter=cv2.INTER_AREA)
    return image

불러들인 이미지의 크기를 최대 1000으로 조절합니다

 

 

def ImageBinarization(image):
    image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    image_gray = cv2.GaussianBlur(image_gray, (5, 5), 5)
    image_bin = cv2.adaptiveThreshold(
        image_gray,
        maxValue=255.0,
        adaptiveMethod=cv2.ADAPTIVE_THRESH_MEAN_C,
        thresholdType=cv2.THRESH_BINARY_INV,
        blockSize=19,
        C=1,
    )

    return image_bin

문서를 찾기 위해 전처리를 진행합니다

grayscale, blur, threshold 순으로 진행하였습니다

 

 

def ImageContours(image, imageb):
    global scan_cnt, DOT

    image_rectangle = image.copy()
    height, width = imageb.shape
    AREA = height * width / 10

    canny = cv2.Canny(imageb, 100, 255)
    (contours, _) = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    biggest_cnt = np.array([])
    max_area = 0
    for cnt in contours:
        approx = cv2.approxPolyDP(cnt, cv2.arcLength(cnt, True) * 0.02, True)
        vtc = len(approx)
        area_cnt = cv2.contourArea(cnt)

        if area_cnt > AREA and vtc == 4:
            if area_cnt > max_area:
                biggest_cnt = approx
                max_area = area_cnt

    # detected rectangle contour
    if biggest_cnt.size > 0:	# 8
        scan_cnt = np.reshape(biggest_cnt, (4, 2))
        DOT = True

    # undetected rectangle contour
    else:
        no_cnt = ((5, 5), (width - 5, 5), (width - 5, height - 5), (5, height - 5))
        scan_cnt = np.reshape(no_cnt, (4, 2))
        DOT = False

    for cnt in scan_cnt:
        cv2.line(image_rectangle, tuple(cnt), tuple(cnt), (0, 255, 0), DOTSIZE)

    return image_rectangle

cv2.Canny() 를 이용하여 가장자리만 검출한 후 cv2.findcontour() 를 이용하여 contour들의 정보를 가져옵니다

 

for 문에서

cv2.approxPolyDP() 로 contour를 근사 시키고 cv2.contourArea() 로 contour의 넓이를 구합니다

근사 시킨 contour의 꼭짓점이 4개이고 넓이가 일정 넓이 이상인 contour만,

그중에서도 가장 넓이가 큰 contour만 biggest_cnt에 저장합니다

 

그렇게 biggest_cnt가 존재한다면 빈 array였던 scan_cnt에 저장하고

존재하지 않는다면 scan_cnt에 가장자리 네 좌표를 저장합니다

 

 

def OpenWebcam(webcam):
    global img

    if not webcam.isOpened():
        print("Could not open webcam")
        exit()

    dot_count = 0
    while webcam.isOpened():

        status, frame = webcam.read()

        if not status:
            break

        img = ImageRead(frame)
        img2 = img.copy()
        img3 = ImageBinarization(img2)
        img4 = ImageContours(img2, img3)

        cv2.imshow('add', img4)

        # detected 4 point
        if DOT is True:
            dot_count += 1
        elif DOT is False:
            dot_count = 0

        if dot_count >= 20:
            break

        # press "Q" to stop
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    webcam.release()
    cv2.destroyAllWindows()

    return img4

webcam을 열고 가져온 이미지로 위의 과정을 진행합니다

좌표를 구하는 과정

종료하는 방법은 두 가지인데

하나는 DOT == True인 상태에서 20 프레임이 지나거나

수동으로 q를 누르는 방법입니다

 

종료가 되면 그때의 image를 가져오고 scan_cnt의 좌표에 점이 찍혀 출력됩니다

 

 

catch = False
pts_cnt = -1
def onMouse(event, x, y, flags, param):
    global scan_cnt, catch, pts_cnt
    image = img.copy()

    if event == cv2.EVENT_LBUTTONDOWN:
        c = 0
        for cnt in scan_cnt:
            distance = math.sqrt(math.pow(x-cnt[0], 2)+math.pow(y-cnt[1], 2))
            if distance < DOTSIZE:
                pts_cnt = c
                scan_cnt[pts_cnt] = [x, y]
                catch = True
            c += 1

        for i in range(4):
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i]), (0, 255, 0), DOTSIZE)
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i-1]), (0, 255, 0), 1)
        cv2.imshow('area', image)

    elif event == cv2.EVENT_MOUSEMOVE:
        if catch is True:
            scan_cnt[pts_cnt] = [x, y]
            for i in range(4):
                cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i]), (0, 255, 0), DOTSIZE)
                cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i-1]), (0, 255, 0), 1)
            cv2.imshow('area', image)

    elif event == cv2.EVENT_LBUTTONUP:
        if catch is True:
            catch = False
            scan_cnt[pts_cnt] = [x, y]

        for i in range(4):
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i]), (0, 255, 0), DOTSIZE)
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i-1]), (0, 255, 0), 1)
        cv2.imshow('area', image)

마우스 컨트롤입니다

image에 scan_cnt의 좌표가 점 찍 혀 있는데

점을 원하는 곳으로 옮겨 scan_cnt를 조정할 수 있습니다

 

def TextRead(image):
    lang = 'eng+kor'
    config = ' '
    chars = pytesseract.image_to_string(image, lang=lang, config=config)

    result_chars = ''
    for c in chars:
        if ord('a') <= ord(c) <= ord('z') \
                or ord('A') <= ord(c) <= ord('Z') \
                or c.isdigit() or ord(c) == ord(' ') or ord(c) == ord(',')\
                or ord('가') <= ord(c) <= ord('핳'):
            result_chars += c
    print(result_chars)

pytesseract 로 글을 print 하는 함수입니다

언어는 eng, kor 두 가지이고 a~z, A~Z, 가~핳, 숫자, ' ', ', '를 인식합니다

 

 

def ImageScan():
    sm = scan_cnt.sum(axis=1)
    di = np.diff(scan_cnt, axis=1)

    topL = scan_cnt[np.argmin(sm)]
    bottomR = scan_cnt[np.argmax(sm)]
    topR = scan_cnt[np.argmin(di)]
    bottomL = scan_cnt[np.argmax(di)]

    # 변환 전 4개 좌표
    pts1 = np.float32([topL, topR, bottomR, bottomL])

    w1 = abs(bottomR[0] - bottomL[0])
    w2 = abs(topR[0] - topL[0])
    h1 = abs(topR[1] - bottomR[1])
    h2 = abs(topL[1] - bottomL[1])
    width = max([w1, w2])
    height = max([h1, h2])

    # 변환 후 4개 좌표
    pts2 = np.float32([[0, 0], [width - 1, 0],
                       [width - 1, height - 1], [0, height - 1]])

    # 변환 행렬 계산
    mtrx = cv2.getPerspectiveTransform(pts1, pts2)
    # 원근 변환 적용
    img_scan = cv2.warpPerspective(img, mtrx, (width, height))
    cv2.imshow('result', img_scan)

    img_scan_gray = cv2.cvtColor(img_scan, cv2.COLOR_BGR2GRAY)
    TextRead(img_scan_gray)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

scan_cnt의 좌표를 가지고 원근 변환을 진행합니다

원근 변환을 진행한 img_scan이 출력되고 pytesseract가 img_scan의 글을 읽어옵니다

Fig Amyris,Sandalwood,NUSK 로 읽었네요

 

def main():
    img_webcam = OpenWebcam(cv2.VideoCapture(1))
    cv2.imshow('area', img_webcam)
    cv2.setMouseCallback('area', onMouse)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    ImageScan()


main()

메인 함수입니다

 

pytesseract를 안 쓰시는 분들이라면

TextRead() 함수와

import pytesseract, pytesseract.pytesseract.tesseract_cmd = '경로' 만 지우시면 됩니다.

 

 

 

전체 코드

더보기

 

import cv2
import imutils
import numpy as np
import pytesseract
import math

pytesseract.pytesseract.tesseract_cmd = r'C:/Program Files/Tesseract-OCR/tesseract.exe'

scan_cnt = np.zeros((4, 2), dtype=np.float32)
DOT = False
DOTSIZE = 15


def ImageRead(image):
    SIZE = 1000
    image2 = image.copy()
    h, w = image.shape[:2]
    if w or h >= SIZE:
        if h > w:
            image = imutils.resize(image2, height=SIZE, inter=cv2.INTER_AREA)
        else:
            image = imutils.resize(image2, width=SIZE, inter=cv2.INTER_AREA)
    return image


def ImageBinarization(image):
    image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    image_gray = cv2.GaussianBlur(image_gray, (5, 5), 5)
    image_bin = cv2.adaptiveThreshold(
        image_gray,
        maxValue=255.0,
        adaptiveMethod=cv2.ADAPTIVE_THRESH_MEAN_C,
        thresholdType=cv2.THRESH_BINARY_INV,
        blockSize=19,
        C=1,
    )

    return image_bin


def ImageContours(image, imageb):
    global scan_cnt, DOT

    image_rectangle = image.copy()
    height, width = imageb.shape
    AREA = height * width / 10

    canny = cv2.Canny(imageb, 100, 255)
    (contours, _) = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    biggest_cnt = np.array([])
    max_area = 0
    for cnt in contours:
        approx = cv2.approxPolyDP(cnt, cv2.arcLength(cnt, True) * 0.02, True)
        vtc = len(approx)
        area_cnt = cv2.contourArea(cnt)

        if area_cnt > AREA and vtc == 4:
            if area_cnt > max_area:
                biggest_cnt = approx
                max_area = area_cnt

    # detected rectangle contour
    if biggest_cnt.size > 0:
        scan_cnt = np.reshape(biggest_cnt, (4, 2))
        DOT = True

    # undetected rectangle contour
    else:
        no_cnt = ((5, 5), (width - 5, 5), (width - 5, height - 5), (5, height - 5))
        scan_cnt = np.reshape(no_cnt, (4, 2))
        DOT = False

    for cnt in scan_cnt:
        cv2.line(image_rectangle, tuple(cnt), tuple(cnt), (0, 255, 0), DOTSIZE)

    return image_rectangle


def OpenWebcam(webcam):
    global img

    if not webcam.isOpened():
        print("Could not open webcam")
        exit()

    dot_count = 0
    while webcam.isOpened():

        status, frame = webcam.read()

        if not status:
            break

        img = ImageRead(frame)
        img2 = img.copy()
        img3 = ImageBinarization(img2)
        img4 = ImageContours(img2, img3)

        cv2.imshow('add', img4)

        # detected 4 point
        if DOT is True:
            dot_count += 1
        elif DOT is False:
            dot_count = 0

        if dot_count >= 20:
            break

        # press "Q" to stop
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    webcam.release()
    cv2.destroyAllWindows()

    return img4


catch = False
pts_cnt = -1
def onMouse(event, x, y, flags, param):
    global scan_cnt, catch, pts_cnt
    image = img.copy()

    if event == cv2.EVENT_LBUTTONDOWN:
        c = 0
        for cnt in scan_cnt:
            distance = math.sqrt(math.pow(x-cnt[0], 2)+math.pow(y-cnt[1], 2))
            if distance < DOTSIZE:
                pts_cnt = c
                scan_cnt[pts_cnt] = [x, y]
                catch = True
            c += 1

        for i in range(4):
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i]), (0, 255, 0), DOTSIZE)
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i-1]), (0, 255, 0), 1)
        cv2.imshow('area', image)

    elif event == cv2.EVENT_MOUSEMOVE:
        if catch is True:
            scan_cnt[pts_cnt] = [x, y]
            for i in range(4):
                cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i]), (0, 255, 0), DOTSIZE)
                cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i-1]), (0, 255, 0), 1)
            cv2.imshow('area', image)

    elif event == cv2.EVENT_LBUTTONUP:
        if catch is True:
            catch = False
            scan_cnt[pts_cnt] = [x, y]

        for i in range(4):
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i]), (0, 255, 0), DOTSIZE)
            cv2.line(image, tuple(scan_cnt[i]), tuple(scan_cnt[i-1]), (0, 255, 0), 1)
        cv2.imshow('area', image)


def TextRead(image):
    lang = 'eng+kor'
    config = ' '
    chars = pytesseract.image_to_string(image, lang=lang, config=config)

    result_chars = ''
    for c in chars:
        if ord('a') <= ord(c) <= ord('z') \
                or ord('A') <= ord(c) <= ord('Z') \
                or c.isdigit() or ord(c) == ord(' ') or ord(c) == ord(',')\
                or ord('가') <= ord(c) <= ord('핳'):
            result_chars += c
    print(result_chars)


def ImageScan():
    sm = scan_cnt.sum(axis=1)
    di = np.diff(scan_cnt, axis=1)

    topL = scan_cnt[np.argmin(sm)]
    bottomR = scan_cnt[np.argmax(sm)]
    topR = scan_cnt[np.argmin(di)]
    bottomL = scan_cnt[np.argmax(di)]

    # 변환 전 4개 좌표
    pts1 = np.float32([topL, topR, bottomR, bottomL])

    w1 = abs(bottomR[0] - bottomL[0])
    w2 = abs(topR[0] - topL[0])
    h1 = abs(topR[1] - bottomR[1])
    h2 = abs(topL[1] - bottomL[1])
    width = max([w1, w2])
    height = max([h1, h2])

    # 변환 후 4개 좌표
    pts2 = np.float32([[0, 0], [width - 1, 0],
                       [width - 1, height - 1], [0, height - 1]])

    # 변환 행렬 계산
    mtrx = cv2.getPerspectiveTransform(pts1, pts2)
    # 원근 변환 적용
    img_scan = cv2.warpPerspective(img, mtrx, (width, height))
    cv2.imshow('result', img_scan)

    img_scan_gray = cv2.cvtColor(img_scan, cv2.COLOR_BGR2GRAY)
    TextRead(img_scan_gray)

    cv2.waitKey(0)
    cv2.destroyAllWindows()


def main():
    img_webcam = OpenWebcam(cv2.VideoCapture(1, cv2.CAP_MSMF))
    cv2.imshow('area', img_webcam)
    cv2.setMouseCallback('area', onMouse)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    ImageScan()


main()

 


참조

https://minimin2.tistory.com/135

 

'컴퓨터비전' 카테고리의 다른 글

OpenCV tracker 를 활용한 object tracking  (1) 2022.01.12
OpenCV에서 YOLO 가중치 가져오기  (0) 2021.09.06

 

사용한 버전

  • OpenCV = 4.5.2

기본적으로 OpenCV 3.3 이상 필요하며 YOLOv4를 사용하시려면 OpenCV 4.4.0 이상의 버전이 필요합니다

 

 

import cv2
import numpy as np

필요한 라이브러리를 import 합니다

 

net = cv2.dnn.readNet("yolov3.weights", "yolov3.cfg")
classes = []
with open("coco.names", "r") as f:
    classes = [line.strip() for line in f.readlines()]
layer_names = net.getLayerNames()
output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()]

yolov3의 weight파일, cfg파일, names파일이 필요한데 아래 링크에서 다운받습니다

 

https://pjreddie.com/darknet/yolo/

 

YOLO: Real-Time Object Detection

YOLO: Real-Time Object Detection You only look once (YOLO) is a state-of-the-art, real-time object detection system. On a Pascal Titan X it processes images at 30 FPS and has a mAP of 57.9% on COCO test-dev. Comparison to Other Detectors YOLOv3 is extremel

pjreddie.com

https://github.com/pjreddie/darknet/blob/master/cfg/yolov3.cfg

 

GitHub - pjreddie/darknet: Convolutional Neural Networks

Convolutional Neural Networks. Contribute to pjreddie/darknet development by creating an account on GitHub.

github.com

https://github.com/pjreddie/darknet/blob/master/data/coco.names

 

GitHub - pjreddie/darknet: Convolutional Neural Networks

Convolutional Neural Networks. Contribute to pjreddie/darknet development by creating an account on GitHub.

github.com

 

본인이 만든 custom data가 있으신 분들은 각각의 파일을 그대로 넣으시면 됩니다

 

 

class_color = []
for c in classes:
    colors = np.random.uniform(0, 255, size=(1, 3))
    class_color.append(colors[0].tolist())

같은 클래스끼리는 같은 색으로 맞추기 위해 추가하였습니다

 

img = cv2.imread('dog.jpg')
height, width, channels = img.shape

blob = cv2.dnn.blobFromImage(img, 0.00392, (416, 416), (0, 0, 0), True, crop=False)
net.setInput(blob)
outs = net.forward(output_layers)

원하는 이미지를 불러오고 사이즈를 조정합니다

 

원문에서는 320, 416, 608 세 가지 사이즈를 제시했는데

사이즈가 크면 성능은 좋지만 속도가 느리고 사이즈가 작으면 성능은 안 좋아지지만 속도가 빨라집니다

(그렇다고 무조건적으로 사이즈가 커지면 성능이 좋아지는건 아닙니다)

 

custom data를 이용하시는 분들은 학습시킨 사이즈를 그대로 이용하는 것이 가장 좋습니다

 

 

class_ids = []
confidences = []
boxes = []
for out in outs:
    for detection in out:
        scores = detection[5:]
        class_id = np.argmax(scores)
        confidence = scores[class_id]
        if confidence > 0.1:
            # Object detected
            center_x = int(detection[0] * width)
            center_y = int(detection[1] * height)
            w = int(detection[2] * width)
            h = int(detection[3] * height)
            # Rectangle coordinates
            x = int(center_x - w / 2)
            y = int(center_y - h / 2)
            boxes.append([x, y, w, h])
            confidences.append(float(confidence))
            class_ids.append(class_id)

 

필자의 세 가지 class를 구분하는 custom data를 기준으로 다음의 결과가 나온 이미지를

detection을 print 해보면 다음과 같은 결과가 나옵니다

[0.38140836 0.26383564 0.354637   0.38738284 0.9840158   0.9838726    0.             0.            ]
[0.38810647 0.26002026 0.36215368 0.36065686 0.9994475  0.99936306  0.             0.            ]
[0.77718025 0.60074013 0.2545407  0.29360077 0.992965    0.               0.9929514  0.            ]
[0.77864075 0.59860307 0.22294061 0.27640662 0.8993639  0.               0.8993448  0.            ]
[0.26834533 0.66779995 0.34188515 0.39219028 0.9753665  0.               0.             0.9753509 ]
[0.7684378  0.60176474 0.22183374 0.29401144 0.7363867  0.                0.7361567  0.            ]
[0.7797308  0.60492533 0.23948666 0.2746269  0.99026895 0.               0.9902018  0.            ]

 

이를 바탕으로 detection은 각각 bounding box의

[center_x, center_y, width, height, ????, class0_confidence, class1_confidence, class2_confidence, ...]

라고 추측할 수 있습니다

(detection[4] 의 값은 confidence와 비슷한 거 같은데... 확실하지 않네요)

 

YOLO custom data를 만들어 보셨다면 traing data의

[class_id, center_x, center_y, width, height]와 매우 유사함을 볼 수 있습니다

 

그렇다면 이번 블록은 정확도가 0.1 이상인 boundung box들을 가져오고

이를 다시 이미지의 width, height에 맞게 되돌려 놓는 작업이라 생각하시면 됩니다

 

 

그런데 위의 결과에서 7개의 boundingbox가 저장되었는데 왜 3개밖에 안 나왔을까요

 

indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)

이유는 여기 있습니다

NMS(Non Maximum Suppression)라고 하는데

임계값 이상의 범위가 중복되는 boundingbox들은 같은 물체를 판별했다고 판단해  confidence가 낮은 box를 제거합니다

 

위의 코드는 0.5 이하의 confidence 가지거나 다른 box와 0.4 이상 중복되는 box를 제거합니다

(yolov3의 default obj_thresh 값이 0.5, default nms_thresh 값이 0.4 )

 

실제로 위의 코드를

indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.99)로 바꾼다면

7개의 bounding box가 모두 나옴을 볼 수 있습니다

 

font = cv2.FONT_HERSHEY_PLAIN
for i in range(len(boxes)):
    if i in indexes:
        x, y, w, h = boxes[i]
        label = str(classes[class_ids[i]])
        color = class_color[class_ids[i]]
        con = ' ' +str(round(confidences[i], 2))
        cv2.rectangle(img, (x, y), (x + w, y + h), color, 2)
        cv2.putText(img, label + con, (x, y - 6), font, 1.5, color, 2)
cv2.imshow("Predicted", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

그렇게 indexs로 저장한 데이터들을 가지고 불러온 이미지에 boundingbox를 그리면 다음과 같은 결과를 볼 수 있습니다

 

그러나 OpenCV로 실행할 때 darknet에서와 같은 성능을 보장하지는 않는 것 같습니다

좌:opencv 우:darknet

우선 confidence 가 다르고

좌:opencv 우:darknet

같은 yolov3을 가지고 obj_thresh 0.5 nms_thresh 0.6 으로 실행을 하였을 때 겹쳐있는 말에 대해서 서로 다른 결과가 나왔음을 볼 수 있습니다

 

 

전체코드

import cv2
import numpy as np


net = cv2.dnn.readNet("yolov3.weights", "yolov3.cfg")
classes = []
with open("coco.names", "r") as f:
    classes = [line.strip() for line in f.readlines()]
layer_names = net.getLayerNames()
output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()]

class_color = []
for c in classes:
    colors = np.random.uniform(0, 255, size=(1, 3))
    class_color.append(colors[0].tolist())

img = cv2.imread('dog.jpg')
height, width, channels = img.shape

blob = cv2.dnn.blobFromImage(img, 0.00392, (160, 160), (0, 0, 0), True, crop=False)
net.setInput(blob)
outs = net.forward(output_layers)

class_ids = []
confidences = []
boxes = []
for out in outs:
    for detection in out:
        scores = detection[5:]
        class_id = np.argmax(scores)
        confidence = scores[class_id]
        if confidence > 0.05:
            # Object detected
            center_x = int(detection[0] * width)
            center_y = int(detection[1] * height)
            w = int(detection[2] * width)
            h = int(detection[3] * height)
            # Rectangle coordinates
            x = int(center_x - w / 2)
            y = int(center_y - h / 2)
            boxes.append([x, y, w, h])
            confidences.append(float(confidence))
            class_ids.append(class_id)

indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)

font = cv2.FONT_HERSHEY_PLAIN
for i in range(len(boxes)):
    if i in indexes:
        x, y, w, h = boxes[i]
        label = str(classes[class_ids[i]])
        color = class_color[class_ids[i]]
        con = ' ' +str(round(confidences[i], 2))
        cv2.rectangle(img, (x, y), (x + w, y + h), color, 2)
        cv2.putText(img, label + con, (x, y - 6), font, 1.5, color, 2)
cv2.imshow("Predicted", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

 

웹캠으로 이용하고 싶다면?

더보기
import cv2
import numpy as np


net = cv2.dnn.readNet("yolov3.weights", "yolov3.cfg")
classes = []

with open("coco.names", "r") as f:
    classes = [line.strip() for line in f.readlines()]
layer_names = net.getLayerNames()
output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()]

class_color = []
for c in classes:
    colors = np.random.uniform(0, 255, size=(1, 3))
    class_color.append(colors[0].tolist())
    

# open webcam
webcam = cv2.VideoCapture(0)
status, frame = webcam.read()
 
if status:
    height, width, channels = frame.shape
else:
    print("Could not open webcam")
    exit()

while webcam.isOpened():
    timer = cv2.getTickCount()
    
    # read frame from webcam 
    status, frame = webcam.read()
    
    if not status:
        break

        
    # 화면 display
    blob = cv2.dnn.blobFromImage(frame, 0.00392, (416, 416), (0, 0, 0), True, crop=False)
    net.setInput(blob)
    outs = net.forward(output_layers)

    class_ids = []
    confidences = []
    boxes = []
    for out in outs:
        for detection in out:
            scores = detection[5:]
            class_id = np.argmax(scores)
            confidence = scores[class_id]
            if confidence > 0.05:
                # Object detected
                center_x = int(detection[0] * width)
                center_y = int(detection[1] * height)
                w = int(detection[2] * width)
                h = int(detection[3] * height)
                # Rectangle coordinates
                x = int(center_x - w / 2)
                y = int(center_y - h / 2)
                boxes.append([x, y, w, h, center_x, center_y])
                confidences.append(float(confidence))
                class_ids.append(class_id)

    indexes = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)

    for i in range(len(boxes)):
        if i in indexes:
            x, y, w, h, cx, cy = boxes[i]
            label = str(classes[class_ids[i]])
            color = class_color[class_ids[i]]
            cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
            cv2.putText(frame, label, (x, y + 20), cv2.FONT_HERSHEY_PLAIN, 2, color, 2)
            cv2.line(frame, (cx, cy), (cx, cy), (0, 0, 255), 5)

    fps = cv2.getTickFrequency()/(cv2.getTickCount()-timer)
    cv2.putText(frame, 'fps : '+str(int(fps)), (70,50), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,0,255), 2)
    cv2.imshow('result', frame)
 
    
    # press "Q" to stop
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break


webcam.release()
cv2.destroyAllWindows()

 

 


참조

https://bong-sik.tistory.com/16

https://pysource.com/2019/06/27/yolo-object-detection-using-opencv-with-python/

https://ctkim.tistory.com/98

https://blueberry-kyu.tistory.com/11

 

3. 학습

 

학습을 하기 위해 필요한 파일은 3가지입니다

1. DATA 파일

2. cfg 파일

3. weights 파일

 

 

 

 

 

DATA 파일의 내부는

 

classes= 3 (클래스 개수)
train=custom/train.txt (학습 데이터 경로가 들어있는 텍스트 파일)
valid=custom/test.txt (검증 데이터 경로가 들어있는 텍스트 파일)
names=custom/custom.names (클래스 명이 적혀있는 파일)
backup=custom/backup/ (학습 시 백업 데이터가 저장되는 경로)

 

와 같이 이루어져 있는데

그중 train, test, names 파일을 만들어줘야 합니다

 

먼저 이전 포스트에서 만들어둔 YOLO_Formatted 폴더를 원하는 이름으로 바꾼 후 darknet-master\build\darknet\x64 안에 넣어줍니다

 

그 폴더 안에서 다음 코드를 실행해 줍니다

 

import os
import glob

classes = []

def createFolders(root):
    try:
        os.mkdir(root + '/custom')
        os.mkdir(root + '/custom' + '/backup')
    except:
        return
    return


def classesList(root):
    try:
        for file in os.listdir(root):
            if os.path.isfile(os.path.join(root, file)) == False:
                classes.append(file)
    except:
        return
    return

def createNames(root, file):
    try:
        fileName = open(root + '/' + file, "w")
        for name in classes:
            fileName.write(name + '\n')
    except:
        return
    return

def YOLOdataset(wd, root, ratio):
    try:
        fileTrain = open(root + '/train.txt', "w")
        fileValid = open(root + '/valid.txt', "w")

        for index in classes:
            labelDir = wd + '/' + index
            labelList = glob.glob(os.path.join(labelDir, '*.txt'))
            imageList = glob.glob(os.path.join(labelDir, '*.jpg'))
            count_train = int(float(len(labelList)) * ratio)
            count = 0

            for label in labelList:
                name, extension = os.path.splitext(label)
                imagelabel = name + '.jpg'
                if imagelabel not in imageList:
                    print('false : '+imagelabel)
                    continue
                if count < count_train:
                    fileTrain.write(imagelabel + '\n')
                    print('train : '+imagelabel)
                else:
                    fileValid.write(imagelabel + '\n')
                    print('valid : '+imagelabel)
                count = count + 1
    except:
        return
    return

def YOLOdatafile(root):
    try:
        file = open(root + '/detector.data', "w")
        file.write('classes=' + str(len(classes)) + '\n')
        file.write('train=custom/train.txt' + '\n')
        file.write('valid=custom/valid.txt' + '\n')
        file.write('names=custom/custom.names' + '\n')
        file.write('backup=custom/backup/' + '\n')
    except:
        return
    return


wd = os.getcwd()
x64 = os.path.dirname(wd)
createFolders(x64)
classesList(wd)

Dir = x64 + '/custom'
createNames(Dir, "custom.names")
YOLOdataset(wd, Dir, 0.8)
YOLOdatafile(Dir)

 

실행하셨으면 darknet-master\build\darknet\x64 폴더에 custom 폴더가 생성되고 custom폴 더안에 backup 폴더, names 파일 data 파일, train.txt, test.txt가 생성되어 있을 것이고 각각의 파일들의 상태는 다음과 같습니다

custom 폴더
names 파일

 

DATA 파일
train.txt
test.txt

 

 

 

 

cfg파일

 

먼저 오리지널 cfg파일을 다운로드합니다

https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4-tiny.cfg

 

저는 yolov4-tiny.cfg을 사용하였습니다

cfg 파일을 메모장으로 열고 저희가 사용하는 custom data에 맞게 수정해 줍니다

 

 

batch = 64 [기본값 64]

subdivisions = 4 [기본 값 8]

 

한 번에 batch만큼 처리하고 batch를 subdivisions만큼 나누어 처리합니다

batch는 클수록 subdivisions는 작을수록 빠르지만 많은 GPU memory가 필요합니다

 

train 시 out of memory 가 뜨면 subdivisions를 올려봅시다 (8, 16...)

 

 

height = 416

width = 416

 

32의 배수여야 하며 416과 608을 많이 사용합니다

 

 

max_batches = 6200 [클래스 개수 * 2000 + 200??]

 

iterations

2000 대신 3000, 4000을 곱하기도 합니다

 

+200 하시는 경우가 많은데 이유는 잘 모르겠네요..??

 

 

steps = 4800, 5400 [max_batches * 0.8, max_batches * 0.9]

 

해당 step에 도달 시 learning rate를 scale 만큼 재조정합니다

 

 

classes = 3 [클래스 개수]

 

yolo 레이어에 있습니다

yolov4-tiny 기준 총 2개 (yolov4는 3개)

 

 

filters = 24 [(클래스 개수 + 5) * 3]

 

yolo 레이어 바로 위인 convolutional 레이어의 filters를 수정합니다

yolov4-tiny 기준 총 2개 (yolov4는 3개)

 

 

anchors = 기본 값 사용

 

darknet.exe detector calc_anchors DATA파일 -num_of_clusters 9 -width 416 -height 416을 실행하여 계산된 값 (anchors.txt가 생성됩니다)을 입력합니다

 

default로 설정돼있는 값을 이용해도 큰 차이는 없는 듯합니다

 

 

random = 0 [or 1]

 

1로 설정한다면 height, width의 값을 다른 여러 값으로 바꾸면서 진행합니다

(저는 608 사이즈에서 out of memory가 발생해서 안 했습니다)

 

 

 

전부 완료했다면 원본 cfg와 헷갈리지 않게 파일명을 바꾸고 저장합니다

 

 

 

 

 

 

weight 파일

 

https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.conv.29

yolov4-tiny.conv.29을 다운로드합니다

 

 

 

 

 

 

진짜 학습하기

 

이제 다 왔습니다

cmd 상에서 build\darknet\x64에 진입 후 다음 명령어를 실행해 줍니다

 

darknet.exe detector train custom/detector.data custom/yolov4-tiny-custom.cfg custom/yolov4-tiny.conv.29

(darknet.exe detector train DATA파일경로 cfg파일경로 weight파일경로)

 

 

~~

~~

~~

~~

 

 

 

 

4. 결과 확인

 

학습이 완료되었으면 map를 확인해봅시다

darknet.exe detector map custom/detector.data custom/yolov4-tiny-custom.cfg custom/backup/yolov4-tiny-custom_best.weights

(darknet.exe detector map DATA파일경로 cfg파일경로 학습된weight파일경로)

 

 

이번엔 실제 사진으로 test 해봅시다

darknet.exe detector test custom/detector.data custom/yolov4-tiny-custom.cfg custom/backup/yolov4-tiny-custom_best.weights test/test01.jpg

(darknet.exe detector test DATA파일경로 cfg파일경로 학습된weight파일경로 사진파일경로)

 

동영상 파일을 확인하고 싶다면

darknet.exe detector demo DATA파일경로 cfg파일경로 학습된weight파일경로 동영상파일경로

(-out_filename 파일명.avi 으로 저장 가능)

 

웹캠으로 확인하고 싶다면

darknet.exe detector demo DATA파일경로 cfg파일경로 학습된weight파일경로

 

 

 


참조

https://github.com/kiyoshiiriemon/yolov4_darknet
https://webnautes.tistory.com/1423
http://daddynkidsmakers.blogspot.com/2020/05/yolo.html
https://naloblog.tistory.com/69
https://wingnim.tistory.com/56
https://ultrakid.tistory.com/17
https://velog.io/@springkim/YOLOv2
https://www.ccoderun.ca/programming/2020-09-25_Darknet_FAQ/

 

 

'딥러닝 > YOLO' 카테고리의 다른 글

custom data를 이용한 YOLO 학습 (1/2)  (0) 2021.08.17
Windows10 darknet 설치하기  (0) 2021.08.14

https://github.com/AlexeyAB/darknet

 

GitHub - AlexeyAB/darknet: YOLOv4 / Scaled-YOLOv4 / YOLO - Neural Networks for Object Detection (Windows and Linux version of Da

YOLOv4 / Scaled-YOLOv4 / YOLO - Neural Networks for Object Detection (Windows and Linux version of Darknet ) - GitHub - AlexeyAB/darknet: YOLOv4 / Scaled-YOLOv4 / YOLO - Neural Networks for Object ...

github.com

 

 

저는 Keras에서 만들때 쓰기도 했고 졸업작품에서도 쓰기위해 캔, 종이컵, 페트병 이미지를 수집하였습니다

 

yolov4-tiny 로 진행하였음을 미리 알려드립니다

 

사용한 버전

  • Python = 3.7.7
  • OpenCV = 4.5.1

 

1. 데이터 수집

 

먼저 데이터 수집입니다 대표적인 방법으로는 2가지가 있는데

 

- kaggle 등에서 dataset 을 구하기

다른 사람이 미리 만들어둔 dataset을 이용하는 방법입니다

힘들게 구하러 다닐 필요도 없는게 장점이지만 내가 원하는 dataset 찾기가 힘들죠

 

- 이미지 크롤링 하기

프로그램을 이용하거나 파이썬 코드로 크롤링을 하는 방법입니다

손으로 하는것 보다 빠르게 학습 데이터를 모을수 있습니다 다만 모은 데이터를 한번 정리는 해야겠죠

프로그램을 이용하는건 크롬의 Fatkun 같은 어플을 이용하시면 되고

파이썬 코드는 구글에 검색하면 매우 많이 있으니 스킵 하겠습니다

 

 

 

그리고 클래스별로 모든 사진을 512*512 정사각형 형태로 resize합니다

(512*512 가 아니여도 너무 작지만 않으면 상관없습니다)

(resize 하기전에 이름이 한글이거나 특수기호가 있을수도 있기때문에 이름을 전부 바꾸고 진행합시다)

 

 

다음 파이썬 코드를 사진들이 있는 폴더에서 실행시켜 줍니다

name은 원하는 이름으로

그럼 resize 라는 폴더만에 resize된 사진들이 저장 됩니다

import cv2
import numpy as np
import os

SIZE = 512
wd = os.getcwd()
os.mkdir(wd + '/resize')
save_root = wd +'/resize/'
name = 'paper'

def ImageList(root):
    img_list = []
    
    for file in os.listdir(root):
        try:
            img = cv2.imread(os.path.join(root, file))
            img_list.append((img, file))
        except:
            return
    return img_list


img_list = ImageList(wd)
  
for i, img in enumerate(img_list, start=1):
	i1 = str(i)
	try:
		img1 = cv2.resize(img, (SIZE,SIZE), cv2.INTER_LINEAR)
		cv2.imwrite(save_root + name +' (' + i1.zfill(4) + ').jpg', img1)
	except:
		continue

 

이렇게 클래스별로 2000장 씩

총 6000장의 이미지 파일을 준비했습니다

 

 

 

2. 라벨링

 

라벨링 방법에는 BBox Label Tool, LabelMe, Labelbox 등이 있고

제가 소개할 방법은 BBox Label Tool입니다

 

원본 BBox Label Tool을 이용할 수 도 있지만 여러 클래스를 동시에 라벨링 할 수 있고 변환까지 시켜주는 코드가 있길래 이 코드로 진행해 보겠습니다

https://github.com/andrewhu/Multiclass-BBox-Label-Tool

 

GitHub - andrewhu/Multiclass-BBox-Label-Tool: Multi-class bounding box labeling tool

Multi-class bounding box labeling tool. Contribute to andrewhu/Multiclass-BBox-Label-Tool development by creating an account on GitHub.

github.com

 

 

 

 

github에서 다운을 받은후 폴더를 열어보면  classes 라는 이름의 텍스트 파일이 있습니다

이 텍스트 파일을 본인이 원하는 클래스로 바꾸어 줍니다

그리고 Images, Labels 폴더 안에 클래스 별로 폴더를 하나씩 생성합니다

Images 폴더안에 있는 각 폴더에 해당 이미지들은 전부 넣습니다

 

※ 여기서 폴더는 라벨링 편하게 하기위해 임의로 나눈 것으로 can 폴더안에 paper 이미지가 하나 들어가있거나 동시에 들어있는 이미지가 있어도 상관없습니다

 

 

전부 완료하면 다음과 같은 형태가 되어있습니다

 

Images

- can

-- can사진들

- paper

-- paper 사진들

- plastic

-- plastic 사진들

Labels

- can

- paper

- plastic

classes.txt

main.py

convert.py

..

 

 

이제 main.py를 실행해 줍니다

image Dir에서 디렉토리를 선택하고 Class로 해당 라벨을 선택 후 사각형을 그려줍니다

A, D 버튼으로 라벨이 저장 되면서 해당 디렉토리의 이전, 다음 사진으로 넘어가게 됩니다,

(마지막 사진일 경우에도 D 한번 눌러야 합니다!)

 

 

 

 

 

이 작업이 상당히 오래걸립니다...  하다보면 눈이 엄청 아프니 중간중간 쉬면서 합시다!!

 

 

 

 

 

 

그렇게 작업을 모두 진행하면

Labels 폴더안에 Images 폴더의 사진과 똑같은 이름의 .txt 파일들이 생성된것을 볼수 있습니다

파일을 열어보면 다음과 같은 형태가 보이는데

 

 

이제 convert.py를 실행합니다

그럼 YOLO_Formatted 폴더가 생성되며 이미지와 텍스트 파일이 모두 옮겨지는데 텍스트 파일을 열어보면

 

좌상단 꼭지점 x, 좌상단 꼭지점 y, 우하단 꼭지점 x, 우하단 꼭지점 y, 이미지의 width, 이미지의 height, 클래스 명

의 형태였던 텍스트 파일이

 

클래스 (0, 1, 2),  박스 중앙 x, 박스 중앙 y, 박스 width, 박스 height

의 형태로 변환 된것을 볼 수 있습니다

 

 

https://blueberry-kyu.tistory.com/12

 

'딥러닝 > YOLO' 카테고리의 다른 글

custom data를 이용한 YOLO 학습 (2/2)  (0) 2021.08.21
Windows10 darknet 설치하기  (0) 2021.08.14

+ Recent posts