colab과 opencv+yolov4을 이용한 고양이 짤 생성기

서론

필자는 고양이를 좋아한다.

하지만 무슨무슨 법을 준수하지 않는 유저가 많아 고양이를 많이 보고싶어도 그럴 수 없다.

어떻게하면 편하게 실시간으로 업로드되는 고양이 짤을 계속 볼 수 있을까??
라는 단순한 생각이 커지고 커져 여기까지 이르렀다.

파이썬과 머신러닝을 하나도 모르는 필자가 어떻게 만들었는지 그 일련의 과정을 서술하겠다.

아이디어의 출발

Socket.io를 들어가면 첫째로 반기는 것이 트윗을 실시간으로 emit하는 component다.

socket io tweet component

아하! 실시간 데이터는 트위터에서 가져오면 되겠다!

한번 Socket io의 깃헙의 twitter Example을 봐보자.

tweet ex dependency

아하! node-tweet-stream이라는 모듈을 추가로 사용하면 되는구나!

node-tweet-stream의 깃헙을 들어가서 사용법을 한번 봐보자.

node-tweet-stream

사용하기 위해선 Twitter API Token과 사용제한이 있는 것 같다.

API Token을 얻는법은 정리가 잘 되어있는 다른 분의 블로그 링크를 소개한다.
http://hleecaster.com/twitter-api-developer/

필자가 신청할 때 바로 승인되어서 토큰을 얻을 수 있었다.

트윗을 얻어보자

서버는 node.js, express를 이용했다.
먼저 npm으로 express, socket io, node-tweet-stream을 설치하자.

그 후 코드를 작성하고 node server.js 를 입력해 서버를 실행하자.

npm install --save express socket.io node-tweet-stream

// server.js
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 8080;

app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});

const Twitter = require('node-tweet-stream');
const twitter = new Twitter({
  consumer_key: "TWITTER API TOKEN",
  consumer_secret: "TWITTER API TOKEN",
  token: "TWITTER API TOKEN",
  token_secret: "TWITTER API TOKEN"
});

twitter.track('cat');
//고양이를 tracking

twitter.on('tweet', tweet => {
  io.emit('tweet', tweet)
});

twitter.on('error', err => {
  console.error(err);
});

http.listen(port, function(){
  console.log('listening on *:' + port);
});

index.html은 이와같이 작성한다.

<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <title>Cat</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  </head>
  <body></body>
  
  <script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
  <script>
    const socket = io();
    socket.on('tweet', function(tweet){
      console.log(tweet);
    });
  </script>
</html>

localhost:8080으로 접속해서 F12를 눌러 console탭으로 이동하여 무슨 결과를 출력하는지 확인해보자.

chrome console log

사진이나 비디오등 사용자가 업로드한 미디어는 extended_entities.media에서 확인할 수 있다.

media는 array타입이므로 각각 요소를 순회하여 media[n].type이 “photo”인지 검사하고 부합할 때 media[n].media_url을 src로하는 img 태그를 계속 추가해 나가면 될 듯 싶다!

1차 구현

// server.js
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 8080;

app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});

const Twitter = require('node-tweet-stream');
const twitter = new Twitter({
  consumer_key: "TWITTER API TOKEN",
  consumer_secret: "TWITTER API TOKEN",
  token: "TWITTER API TOKEN",
  token_secret: "TWITTER API TOKEN"
});

twitter.track('cat');

twitter.on('tweet', tweet => {
  if(tweet.extended_entities === undefined) return;
  let medias = tweet.extended_entities.media;

  medias.forEach(media => {
    if(media.type === 'photo') 
      io.emit('tweet', media.media_url);
  });
});

twitter.on('error', err => {
  console.error(err);
});

http.listen(port, function(){
  console.log('listening on *:' + port);
});

index.html은 이와같이 작성한다.

<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <title>Cat</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  </head>
  <body></body>
  
  <script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script>
  <script>
    const socket = io();
    socket.on('tweet', function(tweet){
      let target = document.querySelector("body");
      let p = document.createElement('img');
      
      p.setAttribute("src", tweet);
      target.appendChild(p);
    });
  </script>
</html>

cat

고양이들이 순조롭게 나타나는 듯 싶다.

wtf

하지만 이게 왠걸??
중간 중간마다 수상한 짤이 나타난다!!

node-tweet-stream은 track한 키워드에 대한 모든 트윗을 가져오기 때문에 이와같은 일이 일어나는 것이다!


필자는 깊은 고민에 빠졌다…..

어떻게하면 이 수상한 짤을 필터링하고 고양이만 가져올 수 있을까?

그럼 사진에서 고양이가 존재하는지 판별할 수 있는 도구가 필요해. 즉 이미지 속에 존재하는 것을 탐지해야한다는 소리!

필자는 머신러닝 기술 중 이미지 비전을 떠올렸지만 앞이 캄캄해졌다.

필자는 머신러닝은 커녕 파이썬도 몰랐기 때문이다!
그래도 대 오픈소스 시대의 희망을 믿고 필자는 미지의 세계로 발을 내딛었다.

머신러닝 입문

머신러닝을 이용하려면 GPU의 힘을 사용해야한다.
하지만 가난한 학부생은 힘은 물론 돈이 없기때문에 과금을 해야하는 것을 지양하고싶다.

그렇게 무료 머신러닝, 무료 이미지 비전을 검색한 결과

Google Colaboratory, OpenCV, yolo v4 darknet라는 키워드들을 알게 되었다.

Google Colaboratory(이하 Colab)는 브라우저 상 Python 코드를 실행하고 머신러닝, 데이터 분석에 적합한 무료 호스팅 Jupyter Notebook 서비스다. 물론 GPU도 무료로 사용 가능하다. (단, 리소스 제한이 존재함)

OpenCV는 컴퓨터 비전을 목적으로 하는 오픈소스 라이브러리다.

Darknet은 딥러닝 모델을 돌려볼 수 있는 오픈소스 신경망 프레임워크다.

Yolo v4 객체인식 딥러닝 모델이다.

따라서 Colab에 OpenCV, Darknet, Yolov4를 설치하고 Python을 이용해 만들면 될 것 같다.

무작정 따라하기

필자는 아무것도 모르기 때문에 [참조1]을 통해 실습을 진행해 보았다.

참조1: https://wiserloner.tistory.com/1181

change

먼저 Colab에 접속해 새 노트를 만든다.
새 노트를 만들고 수정->노트설정을 클릭해서 GPU로 변경한다.

test1

노트 빈공간 상단에 커서를 위치하고 +코드를 누르고 나온 칸에 코드를 이와같이 작성한다. (칸 상단, 하단에 커서를 두면 새로운 코드나 텍스트를 추가할 수 있다.)

!git clone https://github.com/AlexeyAB/darknet
%cd darknet
!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!sed -i 's/CUDNN_HALF=0/CUDNN_HALF=1/' Makefile
!sed -i 's/LIBSO=0/LIBSO=1/' Makefile
!make
!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
!./darknet detector test cfg/coco.data cfg/yolov4.cfg yolov4.weights data/person.jpg

순차적으로 작성하고 위에서부터 차례로 실행하자.

  1. 깃헙에 존재하는 yolov4+darknet을 clone
  2. Makefile의 설정을 변경하고 make
  3. yolov4의 가중치를 저장
  4. 잘 실행되는지 테스트

res1

결과출력의 마지막이 위와같이 나오면 성공이다.
CLI상에서는 사진을 출력할 수 없기 때문에 이러한 에러가 발생한다.

그렇다고 사진을 아예 못보는건 아니다.
이제 진짜 결과 사진을 출력하기 위해 Python코드를 작성해보자.

참조2: https://hanryang1125.tistory.com/9

OpenCV에서 yolov4를 사용하려면 4.4.0 이상 버전을 사용해야한다. Colab상에 설치되어있는 OpenCV는 버전이 낮기 때문에 먼저 upgrade를 진행해야한다.

pip install --upgrade opencv-python

설치를 진행하고 나면 RESTART RUNTIME버튼을 눌러주자.
만일 나오지 않는다면 위 메뉴의 런타임->런타임 다시 시작을 눌러주자.

런타임을 다시 시작할때 진행한 항목이 모조리 초기화 될 수 있는데 초기화 되었다면 다시 위의 항목부터 진행해주자.

그리고 아래와 같은 코드를 작성하자

# app.py
import cv2
import numpy as np
from google.colab.patches import cv2_imshow

classes = []
with open("/content/darknet/cfg/coco.names", "r") as f:
    classes = [line.strip() for line in f.readlines()]

def yolo(frame, size, score_threshold, nms_threshold):
    net = cv2.dnn.readNetFromDarknet("/content/darknet/cfg/yolov4.cfg", "/content/darknet/yolov4.weights")
    net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
    net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
    layer_names = net.getLayerNames()
    output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()]

    # 클래스의 갯수만큼 랜덤 RGB 배열을 생성
    colors = np.random.uniform(0, 255, size=(len(classes), 3))

    # 이미지의 높이, 너비, 채널 받아오기
    height, width, channels = frame.shape

    # 네트워크에 넣기 위한 전처리
    blob = cv2.dnn.blobFromImage(frame, 0.00392, (size, size), (0, 0, 0), True, crop=False)

    # 전처리된 blob 네트워크에 입력
    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.1:
                # 탐지된 객체의 너비, 높이 및 중앙 좌표값 찾기
                center_x = int(detection[0] * width)
                center_y = int(detection[1] * height)
                w = int(detection[2] * width)
                h = int(detection[3] * height)

                # 객체의 사각형 테두리 중 좌상단 좌표값 찾기
                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)

    # 후보 박스(x, y, width, height)와 confidence(상자가 물체일 확률) 출력
    print(f"boxes: {boxes}")
    print(f"confidences: {confidences}")

    # Non Maximum Suppression (겹쳐있는 박스 중 confidence 가 가장 높은 박스를 선택)
    indexes = cv2.dnn.NMSBoxes(boxes, confidences, score_threshold=score_threshold, nms_threshold=nms_threshold)
    
    # 후보 박스 중 선택된 박스의 인덱스 출력
    print(f"indexes: ", end='')
    for index in indexes:
        print(index, end=' ')
    print("\n\n============================== classes ==============================")

    for i in range(len(boxes)):
        if i in indexes:
            x, y, w, h = boxes[i]
            class_name = classes[class_ids[i]]
            label = f"{class_name} {confidences[i]:.2f}"
            color = colors[class_ids[i]]

            # 사각형 테두리 그리기 및 텍스트 쓰기
            cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
            cv2.rectangle(frame, (x - 1, y), (x + len(class_name) * 13 + 65, y - 25), color, -1)
            cv2.putText(frame, label, (x, y - 8), cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0, 0, 0), 2)
            
            # 탐지된 객체의 정보 출력
            print(f"[{class_name}({i})] conf: {confidences[i]} / x: {x} / y: {y} / width: {w} / height: {h}")

    return frame
image = "/content/darknet/data/person.jpg"
frame = cv2.imread(image)

# 입력 사이즈 리스트 (Yolo 에서 사용되는 네크워크 입력 이미지 사이즈)
size_list = [320, 416, 608]

frame = yolo(frame=frame, size=size_list[2], score_threshold=0.4, nms_threshold=0.4)
cv2_imshow(frame)
cv2.waitKey(0)
cv2.destroyAllWindows()

res2

코드를 실행하고 위와같은 결과가 나오면 정상이다!
darknet/data 디렉토리에 다른 이미지도 있으니 한번 테스트 해보길 바란다.
jpg가 아닌 확장자는 오류가 발생한다.


Colab을 사용해서 OpenCV + Yolov4를 이용해보았다.
대 오픈소스 시대의 감사함을 느낀 필자는 또 다른 고민에 빠졌다…..

객체탐지는 했다 이거야 하지만 어떻게 이 객체탐지 application과 짤 생성기 application끼리 통신하지??

짤을 생성하기 전에 얻어온 meadi_url에 존재하는 이미지를 객체탐지 application에 보내서 고양이가 짤에 존재하는지 판별해주는 API를 만들면 될 것 같다!

일이 점점 커져가는 걸 느낀 필자는 두려움을 느끼며 한발 더 내딛었다.

API 만들기

Colab이 Python기반이기 때문에 Python에 관련된 API 프레임워크를 사용하고 싶다.
그리고 필자는 파이썬을 전혀 모르기 때문에 심플한 사용법이면 좋겠다.

그렇게 FastAPI를 알게 되었다!

Colab with FastAPI를 검색하니 이와같은 답변을 보게 되었다.
https://stackoverflow.com/a/63833779

대충 Colab은 외부에서 접근을 불허하기 때문에 ngrok를 사용해서 우회로 들어가야하고 API통신은 그대로 FastAPI + Uvicorn을 이용하면 된다는 내용이다.

그럼 FastAPI + Uvicorn + ngrok를 써보자!

무작정 따라하기

먼저 필요한 것들을 설치하자.

pip install fastapi uvicorn pyngrok nest-asyncio

설치하고 아래와 같은 코드를 작성하고 실행하자.

# api.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=['*'],
    allow_headers=['*'],
)

@app.get('/')
async def root():
    return {'hello': 'world'}

import nest_asyncio
from pyngrok import ngrok
import uvicorn

ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000)

res3

res4

콘솔에 나온 public url의 항목을 눌러 접속한다.
접속하고 위와 같은 결과가 나오면 정상!


이제 node server와 통신하는 코드를 작성해보자

추가 할 모듈은 node-fetch이다.

npm install node-fetch --save

// server.js
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 8080;

app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});

const fetch = require('node-fetch');
const run = async () => {
  let url = "PUBLIC URL";
  // PUBLIC URL을 기입하세요!
  const response = await fetch(url);
  const json = await response.json();
  console.log(json);
}


http.listen(port, function(){
  console.log('listening on *:' + port);
});

run()

run 함수 안 url변수에 Colab에서 실행시킨 FastAPI의 ngrok public url을 String형태로 기입한다.
Endpoint는 / 이다.

res5

위와같은 실행결과가 나오면 정상적으로 상호 통신이 완료된 것이다.
Colab상 터미널에서도 log가 존재할 것이다.


우리가 해야할 것은 node서버에서 url을 던지고 Colab FastAPI에서 url을 받아야 되는 것이다.

차근차근 url을 던지고 받는 것부터 구현해보자.

# api.py
from urllib.parse import unquote
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=['*'],
    allow_headers=['*'],
)

@app.get('/')
async def root(request: Request):
    url = str(request.query_params)
    
    if not url: return {'cat': 'false'}
    url = unquote(url)
    print(url);
    return {'cat': 'false'}

import nest_asyncio
from pyngrok import ngrok
import uvicorn

ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000)
// server.js
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 8080;

app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});

const fetch = require('node-fetch');
const run = async (src) => {
  let url = "PUBLIC URL/?";
  // PUBLIC URL을 기입하세요!
  // 뒤에 쿼리스트링을 나타내는 ? 는 꼭 존재해야합니다!
  // 따라서 형식은 http://foo.com/? 이런식 입니다.
  url += src;
  const response = await fetch(url);
  const json = await response.json();
  
  console.log(json);
}

const Twitter = require('node-tweet-stream');
const twitter = new Twitter({
  consumer_key: "TWITTER API KEY",
  consumer_secret: "TWITTER API KEY",
  token: "TWITTER API KEY",
  token_secret: "TWITTER API KEY"
});

twitter.track('cat');

twitter.on('tweet', tweet => {
  if(tweet.extended_entities === undefined) return;
  let medias = tweet.extended_entities.media;

  medias.forEach(media => {
    if(media.type === 'photo') 
    {
      run(media.media_url);
    }
  });
});

twitter.on('error', err => {
  console.error(err);
});

http.listen(port, function(){
  console.log('listening on *:' + port);
});

jsres

apires

이번엔 쿼리스트링을 이용해 URL을 전달하기 때문에 PUBLIC_URL 뒤에 /?를 반드시 붙여놔야 한다.
따라서 형식은 http://foo.com/? 이런 식이 되겠다.

위와 같이 api단에 url이 전달되고 node server에 test json이 전달 되는 것을 확인할 수 있다.

print 한 url뒤에 = 기호가 붙어있는데 이를 제거하고 사용하면 된다.


이제 우리는 아래와 같은 endpoint를 구성하면 된다는 것을 쉽게 유추할 수 있다.

@app.get('/')
async def root(request: Request):
    url = str(request.query_params)
    
    if not url: return {'cat': 'false'}
    url = unquote(url)
    
    ret = areYouCat(url)

    if(ret == True): return {'cat': 'true'}
    return {'cat': 'false'}

areYouCat 구현하기

앞서 무작정 따라해본 예제에서 필요없는 출력들을 다 배제하고 고양이가 존재하는지 안하는지만 알아보자.

# areYouCat.py
import cv2
import numpy as np
import urllib.request

classes = []
with open("/content/darknet/cfg/coco.names", "r") as f:
    classes = [line.strip() for line in f.readlines()]

def yolo(frame, size, score_threshold, nms_threshold):
    net = cv2.dnn.readNetFromDarknet("/content/darknet/cfg/yolov4.cfg", "/content/darknet/yolov4.weights")
    net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
    net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)

    layer_names = net.getLayerNames()
    output_layers = [layer_names[i[0] - 1] for i in net.getUnconnectedOutLayers()]
    # 이미지의 높이, 너비, 채널 받아오기
    height, width, channels = frame.shape

    # 네트워크에 넣기 위한 전처리
    blob = cv2.dnn.blobFromImage(frame, 0.00392, (size, size), (0, 0, 0), True, crop=False)

    # 전처리된 blob 네트워크에 입력
    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.1:
                # 탐지된 객체의 너비, 높이 및 중앙 좌표값 찾기
                center_x = int(detection[0] * width)
                center_y = int(detection[1] * height)
                w = int(detection[2] * width)
                h = int(detection[3] * height)

                # 객체의 사각형 테두리 중 좌상단 좌표값 찾기
                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)

    # Non Maximum Suppression (겹쳐있는 박스 중 confidence 가 가장 높은 박스를 선택)
    indexes = cv2.dnn.NMSBoxes(boxes, confidences, score_threshold=score_threshold, nms_threshold=nms_threshold)
    
    # 최종 결과에서 고양이가 있는지 확인하면 된다
    for idx in indexes:
        class_name = classes[class_ids[idx[0]]]
        if(class_name == "cat"): return True
    return False

def areYouCat(src=None):
    # 이미지 경로
    if(src == None): return False
    src = src[:-1]

    if ".jpg" not in src: 
        return False

    # 이미지 읽어오기
    url_response = urllib.request.urlopen(src)
    img_array = np.array(bytearray(url_response.read()), dtype=np.uint8)
    img = cv2.imdecode(img_array, -1)

    # 입력 사이즈 리스트 (Yolo 에서 사용되는 네크워크 입력 이미지 사이즈)
    size_list = [320, 416, 608]

    ret = yolo(frame=img, size=size_list[1], score_threshold=0.4, nms_threshold=0.4)
    return ret
# api.py
from urllib.parse import unquote
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=['*'],
    allow_headers=['*'],
)

@app.get('/')
async def root(request: Request):
    url = str(request.query_params)
    
    if not url: return {'cat': 'false'}
    url = unquote(url)
    
    ret = areYouCat(url)

    if(ret == True): return {'cat': 'true'}
    return {'cat': 'false'}

import nest_asyncio
from pyngrok import ngrok
import uvicorn

ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000)
// server.js
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 8080;

app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});

const fetch = require('node-fetch');
const run = async (src) => {
  let url = "PUBLIC URL/?";
  // PUBLIC URL을 기입하세요!
  // 뒤에 쿼리스트링을 나타내는 ? 는 꼭 존재해야합니다!
  // 따라서 형식은 http://foo.com/? 이런식 입니다.
  url += src;
  const response = await fetch(url);
  const json = await response.json();
  
  if(json['cat'] === 'false') return false;

  return true;
}

const Twitter = require('node-tweet-stream');
const twitter = new Twitter({
  consumer_key: "TWITTER API KEY",
  consumer_secret: "TWITTER API KEY",
  token: "TWITTER API KEY",
  token_secret: "TWITTER API KEY"
});

twitter.track('cat');

twitter.on('tweet', tweet => {
  if(tweet.extended_entities === undefined) return;
  let medias = tweet.extended_entities.media;

  medias.forEach(media => {
    if(media.type === 'photo') 
    {
      run(media.media_url).then((ret => {
        if(true === ret) 
          io.emit('tweet', media.media_url);
      })).catch((err)=>{console.log(err)})
    }
  });
});

twitter.on('error', err => {
  console.error(err);
});

http.listen(port, function(){
  console.log('listening on *:' + port);
});

res6

이로써 고양이 짤을 자동생성하는 여정이 끝났다.

심화과정

앞으로의 과정은 필자가 프로젝트 진행중에 겪은 이슈와 해결방안을 서술할 것이다.

마지막엔 필자의 소스를 소개 할 것이다.

Colab 환경 초기화

말 그대로 Colab에 설치했던 모든 요소가 깔끔히 초기화 된다.

처음부터 다시 설치, 설정해야하는 번거로움과 당혹스러움을 겪게된다.

후술할 OpenCV 컴파일을 하고 초기화를 맞으면?? 생각하기도 싫다..

해결 방안

바로 Google Drive와 연동하는 것이다.
Colab도 Google이기 때문에 Drive연동이 매우 간단하다.
자주 쓰는 darknet을 drive에 저장해놓고 사용하면 된다.

%cd ..
from google.colab import drive
drive.mount('/content/gdrive')
!ln -s /content/gdrive/My\ Drive/ /mydrive
!mkdir /mydrive/yolov4
%cd /mydrive/yolov4

이게 다다!
첫 명령을 실행하면 API TOKEN을 얻을 수 있는 페이지가 나오므로 잘 따라가 토큰을 얻고 입력하면 된다.

이제 내 작업 폴더는 내 드라이브 내의 yolov4가 되고 이곳에 설치를 하면된다!

!git clone https://github.com/AlexeyAB/darknet
%cd darknet/
!sed -i 's/OPENCV=0/OPENCV=1/' Makefile
!sed -i 's/GPU=0/GPU=1/' Makefile
!sed -i 's/CUDNN=0/CUDNN=1/' Makefile
!sed -i 's/CUDNN_HALF=0/CUDNN_HALF=1/' Makefile
!sed -i 's/LIBSO=0/LIBSO=1/' Makefile
!make
!wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights

그러면 소스에 있는 절대경로들은 /content/darknet/이 아닌 /mydrive/yolov4/darknet/이 되는점을 유의해야 한다.

GPU를 사용중이 아닙니다

분명 필자는 참조링크들을 따라 GPU를 사용하는 OpenCV와 Darknet을 설치했는데 고양이 판독을 진행하면 GPU 사용중이 아닙니다. 라고 경고가 뜨는 것이다!

해결 방안

OpenCV를 GPU사용 버전으로 컴파일해서 라이브러리를 얻으면 된다.

참조3: https://ichi.pro/ko/colab-eseo-gpuwa-hamkke-opencvleul-sayonghaneun-bangbeob-eun-mueos-ibnikka-41065314358367
참조4: https://hanryang1125.tistory.com/18

필자는 혹시 몰라서 Colab의 /content폴더에서 GPU런타임을 켜놓고 Make를 진행했다.

%cd /content
!git clone https://github.com/opencv/opencv
!git clone https://github.com/opencv/opencv_contrib
!mkdir /content/build
%cd /content/build
!cmake -DOPENCV_EXTRA_MODULES_PATH=/content/opencv_contrib/modules  -DBUILD_SHARED_LIBS=OFF  -DBUILD_TESTS=OFF  -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF -DWITH_OPENEXR=OFF -DWITH_CUDA=ON -DWITH_CUBLAS=ON -DWITH_CUDNN=ON -DOPENCV_DNN_CUDA=ON /content/opencv
!make -j8 install

Colab 무료 인스턴스 기준으로 2시간 20분 정도 걸려서 컴파일을 완료했다.

!mkdir /mydrive/yolov4/cv2_gpu/python3
!cp  /content/build/lib/python3/cv2.cpython-37m-x86_64-linux-gnu.so   "/mydrive/yolov4/cv2_gpu/python3"

컴파일을 완료하고 필요한 결과파일을 꼭!! 드라이브에 저장해주자.

2021/07 기준으로 파이썬 버전은 3.7.11이고
우리가 필요한파일은 /content/build/lib/python3/cv2.cpython-37m-x86_64-linux-gnu.so 이다.

파이썬 버전이 변경되면 37m이 36m, 38m으로 변경될 수 도있다.

!pip uninstall opencv-python -y
!cp /mydrive/yolov4/cv2_gpu/cv2.cpython-37m-x86_64-linux-gnu.so /usr/local/lib/python3.7/dist-packages/

위 명령을 사용해서 현 opencv-python을 지우고 컴파일한 라이브러리를 복사하자.
명령이 완료되면 런타임을 다시 시작하고 올바르게 됐는지 확인하자.

import cv2

print(cv2.__version__)
print(cv2.cuda.getCudaEnabledDeviceCount())

2021/07 기준으로

4.5.3-dev  
1

이라고 나오면 완벽하다.

CORS ERROR, INTERNAL SERVER ERROR

결과물을 기다리다가 console탭은 물론 network탭이 빨갛게 물든 경우가 있다.

CORS가 없대나 뭐래나.. 이는 ngrok때문인데 현재 우리는 비회원 자격으로 ngrok를 사용하고 있기 때문에 사용량에 제한이 있다.
일정 사용량을 넘어가면 임대받은 주소가 터져서 사용할 수 없게 되는 것이다.

Internal Server Error의 경우 몇가지 경우가 있는데 첫째로 넘겨준 주소가 잘못 된 경우 (여기서 png확장자는 되지않는다는 것을 알았다) 두번째는 request가 너무 많아서 응답이 지연되는 경우 셋째로 areYouCat이 정의되지 않은 경우

필자가 겪은 상황은 두번째 상황이 많았고 이는 concurrency에 제한을 걸어서 request가 몰리는 것을 예방 할 수 있다.

세번째 상황은 필자는 api코드와 areYouCat코드를 분리해 놨는데
api서버를 실행하기 전 areYouCat코드를 한번 실행 버튼을 눌러주면 해결된다.

해결 방안

ngrok에 회원가입하고 AuthToken을 얻고 이를 사용하면 된다.

uvicorn Setting에 limit_concurrency 항목을 설정해 주면 된다.

# api.py
import nest_asyncio
from pyngrok import ngrok
import uvicorn

ngrok.set_auth_token("YOUR AUTH TOKEN!")

ngrok_tunnel = ngrok.connect(8000)

print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000, limit_concurrency=25)

한국 고양이만 가져오기

한국 고양이들만 보고 싶은 기분이 들었다.
이를 달성하기 위해선 server.js의 일부를 고쳐야한다.

//twitter.track('cat');
twitter.track('고양이');
twitter.language('ko');

일본 고양이는 이렇게 하면 된다.

//twitter.track('cat');
//twitter.track('고양이');
//twitter.language('ko');
twitter.track('ねこ');
twitter.track('ネコ');
twitter.track('');
twitter.language('ja');

중복되는 사진들

결론부터 얘기하면 중복되는건 어쩔 수 없다.
node-tweet-stream은 RT되는 것도 다 받아오기 때문에 지금 RT가 핫하게 되는 고양이 사진들은 중복되기 마련이다.

비동기 areYouCat

주의! 필자는 파이썬에 대해 무지하기 때문에 이 목차에선 기술적 오류가 존재할 수 있음!!

파이썬 asyncio

파이썬에서 비동기, coroutine을 사용하려면 asyncio를 사용해야한다.

asyncio를 사용하려면 파이썬의 버전은 3.7이상 이어야 한다.

# 새로 asyncio를 import
import asyncio

async def yolo(frame, size, score_threshold, nms_threshold):
  # 중략..
  
async def areYouCat(src=None):
  # 중략..
  task = asyncio.create_task(yolo(frame=img, size=size_list[1], score_threshold=0.4, nms_threshold=0.4))
    ret = await task
    return ret

@app.get('/')
async def root(request: Request):
    url = str(request.query_params)
    
    if not url: return {'cat': 'false'}
    url = unquote(url)

    task = asyncio.create_task(areYouCat(url))
    ret = await task
    # coroutine의 핵심

    if(ret == True): return {'cat': 'true'}
    return {'cat': 'false'}

# 중략..

코루틴을 실행하기 위해선 async 함수를 asyncio.create_task()로 wrapping 해야한다.
areYouCat을 async함수로 만들려면 def앞에 async키워드를 추가하면 끝이다.
실제로 연산하는 함수는 yolo이므로 yolo도 코루틴으로 만들기위해 async 키워드를 추가해주자.

결과값을 await해서 받아오고 처리한다.

로그 찍히는 걸 보면 확실히 동기 소스, 비동기 소스 간 차이가 있는데
실행시간에서 드라마틱한 차이는 없다.

500개 이미지 분석(단순 트윗된 500개의 이미지이다)

테스팅 환경이 잘 꾸려지면 모를까 API limit도 신경써야하고 실시간 트윗량에 좌지우지 되므로 절대 Async가 빠르다라곤 단언 할 수 없다.
애초에 맞게 짰는지도 모르겠다

필자의 소스

https://github.com/sleepyjun/CatML
여기서 확인할 수 있다.

Jupyter Notebook 파일을 받아서 Colab에 불러오면 된다.
위에서 설명을 따라 순차적으로 실행하고
API코드 내 ngrok AUTH TOKEN을 기입하면 된다.

동기 코드는 MAIN 실행버튼 클릭 -> FastAPI 실행
비동기 코드는 Async Main 실행버튼 클릭 -> Async FastAPI 실행
해서 API를 실행시키고 public_url을 얻어오면 된다.

client 안 index.html과 server 안 package.json, server.js를 같은 디렉토리에 두고
server.js에 얻어온 public_url과 자신의 Twitter API TOKEN을 기입하고
npm install 명령 후 node server.js 를 실행하고 <localhost:8080> 에 접속하면 된다.

cv2.cpython-37m-x86_64-linux-gnu.so 필자가 컴파일해서 사용하는 라이브러리 파일을 게시해놓았다.