Flask 이미지 분류 - Flask imiji bunlyu

이글은 www.analyticsvidhya.com의 내용임. (링크)

원문에서는 PyTorch를 사용하지만, 여기서는 Tensorflow에 맞게 수정 정리하였다.

Deploy an Image Classification Model Using Flask

Overview

  • Flask 소개
  • 이미지 분류모델 구축
  • Flask를 사용하여 모델 배포

Introduction

    이미지 분류는 소셜미디어의 건강 기능일 때 중요한 요소이다. 어떤 태그에 기초하여 컨텐츠를 분류하는 것은 다양한 법률과 규정을 대신한다. 이는 특정 대상의 집합으로부터 컨텐츠를 숨기기 위해 중요하다.

    인스타그램의 이미지를 보면 이미지 몇몇은 "Sensitive Content"로 표시된다. 인도주의에 대한 위협, 테러 또는 폭력적인 이미지는 일반적으로 "Sensitive Content"로 분류된다. 인스타그램은 어떻게 이미지를 분류하는지는 항상 궁금했다. 이 호기심이 이미지 분류 절차를 이해하기 위한 답을 찾도록 했다.

    대부분의 이미지는 인스타그램이 배포한 이미지 분류 모델로 탐지된다. 또한 커뮤니티 기반 피드백도 있다. 이는 이미지 분류에 대한 중요한 사용 사례이다. 이글에서는 이미지의 분류를 감지하기 위한 이미지 분류보델을 배포해 본다.


Table of Contents

  1. 모델 배포(Deployment)란?
  2. Flask란?
  3. Flask 설치
  4. 문제 상태(Statement) 이해하기
  5. 사전훈련 이미지 분류모델 설정
  6. 이미지 스크래퍼(Scraper) 구축
  7. 웹페이지 생성
  8. Flask 프로젝트 설정
  9. 배포된 모델 작업

모델 배포(Deployment)란?

    전형적인 ML(Machine Learning)과 DL(Deep Learning) 프로젝트에서 보통 문제 상태를 정의하고 데이터를 수집하고 준비하고 모델을 구축한다.

    모델을 성공적으로 구축하고 훈련한 이후, 이를 최종사용자가 사용할 수 있기를 바란다. 따라서 최종 사용자가 이용할 수 있도록 모델을 배포(deploy)해야 한다. 모델 배포는 ML과 DL 프로젝트의 최종 단계중 하나이다.

    이글에서는 분류 모델을 구축하고 Flask를 사용하여 배포하는 법을 알아본다.


Flask란?

    Flask는 파이썬으로 구현한 웹 어플리케이션 프레임워크이다. 이는 프로토콜 관리, 쓰레드 관리 등과 같은 자세한 것에 대한 걱정 없이 어플리케이션을 웹 개발자가 더 쉽게 작성할 수 있게 만들어진 여러개의 모듈이 있다.

    Flask는 웹 어플리케이션 개발을 위한 다양한 선택을 할 수 있고 웹 어플리케이션 구축을 위해 필수적인 도구와 라이브러리를 제공한다.


Flask 설치

    다음 명령으로 설치한다

>>Linux 

sudo apt-get install python3-flask


>>Anaconda conda install flask

문제 상태(Statement) 이해하기

    문제 상태를 이야기해 보자. (아래 그림과 같은) 텍스트 박스를 포함한 웹페이지를 만들고 URL을 입력한다. 여기서 작업은 URL로부터의 모든 이미지를 긁어오는 것(Scrape)하는 것이다. 각 이미지에 대해 이미지 분류모델을 사용하여 분류하고 웹페이지에 표시한다.

Flask 이미지 분류 - Flask imiji bunlyu

    다음은 단대단(end-to-end)모델의 작업흐름(workflow)이다.

프로젝트 작업 흐름 설정

  1. 모델 구축

    : 이미지 분류를 위해 사전훈련된 Densenet 121을 사용한다. 텐서플로의 라이브러리로 제공되며, 여기서는 설계에서부터 높은 정밀도의 분류 모델 구축에 초점을 맞추지 않지만 모델을 배포하는 방법과 웹 인터페이스로 이를 사용하는 것을 본다.

  2. 이미지 스크래퍼 생성

    : requests와 BeautifulSoup라이브러리로 웹 스크래퍼를 생성한다. 이는 URL로부터 모든 이미지를 다운로드하고 예측할 수 있도록 저장한다.

  3. 웹 템플릿 설계

    : 사용자가 URL을 입력하고 계산된 결과를 받을 수 있는 사용자 인터페이스를 설계한다.

  4. 이미지 분류와 결과 전달

    : 사용자로부터 요청이 있으면, 이미지를 분류하기 위해 모델을 사용하고 결과를 사용자에게 되돌려준다.

    아래 그림은 위 단계를 나타낸 것이다

Flask 이미지 분류 - Flask imiji bunlyu

사전훈련 이미지 분류모델 설정

    이미지 분류를 위해 사전훈련된 Densenet 121 모델을 사용한다.

    필요한 라이브러리를 임포트하고 텐서플로에서 사전훈련된 densenet 121 모델을 사용하고 테스트 이미지를 [다운로드]하여 dog.jpg로 저장한다.

    우선 필요한 라이브러리를 임포트한다.

>> get_prediction.py

import json
import io
import glob
from PIL import Image
import tensorflow as tf
import numpy as np

    이미지를 전처리하기 위한 함수를 정의한다. densenet 121인 입력크기인 224 X 224로 맞춘다.

>> get_prediction.py

def tranform_image(image_bytes):
    image = Image.open(io.BytesIO(image_bytes))

    image = np.array(image)
    image = image[:, :, 0:3]
    image = tf.image.convert_image_dtype(image, tf.float32)
    image = tf.image.resize_with_crop_or_pad(image, 224, 224)

    return image

    모델을 통해 분류하는 함수를 정의한다.

>> get_prediction.py

def get_category(model, imagenet_class_mapping, image_path):
    with open(image_path, 'rb') as file:
        image_bytes = file.read()

    transformed_image = tranform_image(image_bytes)
    outputs = model.predict(transformed_image.numpy()[np.newaxis, :])

    predicted_idx = str(np.argmax(outputs))

    return imagenet_class_mapping[predicted_idx]

    모델을 로드하고 샘플 이미지로 테스트한다.


model = tf.keras.applications.DenseNet121(
    include_top=True, weights='imagenet', input_tensor=None, input_shape=None,
    pooling=None, classes=1000
)

imagenet_class_mapping = json.load(open('imagenet_class_index.json'))

result = get_category(model, imagenet_class_mapping, 'dog.jpg')

    예측결과를 분류명과 매핑하기 위해 [imagenet_class_index.json]파일을 다운로드한 후 사용하였다.

    아래는 실행 결과와 샘플 이미지이다.


['n02089867', 'Walker_hound']
>> dog.jpg
Flask 이미지 분류 - Flask imiji bunlyu

['n11939491', 'daisy']
 >> sunflower.jpg
Flask 이미지 분류 - Flask imiji bunlyu

    [get_prediction.py 전체소스 다운로드]


이미지 스크래퍼(Scraper) 구축

    사용자가 입력한 URL로부터 이미지(여기서는 jpeg만을 대상으로 한다.)를 다운받기 위한 웹 스크랩퍼를 구축한다. 이를 위해 BeautifulSoup라이브러리를 사용한다.

    필요한 라이브러리를 임포트한다.

>> get_images.py

import requests
from bs4 import BeautifulSoup
import os
import time

    사용자가 URL을 입력하면 해당 URL의 이미지를 저장하기 위한 디렉토리를 생성하고 이미지를 저장한다. 이를 위해 스크래핑한 이미지를 저장하기 위한 디렉토리 경로를 반환하는 get_path함수를 정의한다.

>> get_images.py

def get_path(url):
    return "static/URL_" + str(url.replace("/", "_").replace(":", ""))

    입력받은 URL에서 이미지를 다운로드 받아 저장하기 위한 get_images함수를 정의한다. 이 함수는 get_path 함수로 저장할 디렉토리를 생성하고 URL의 소스코드를 요청한다. 이 소스코드에서 "img" 태그로 이미지를 추출한다.

    이 글에서는 jpeg 포멧만을 대상으로 하며, 이미지 파일명은 counter 이름 저장한다.

    이 함수를 실행하려면, static/ 서브디렉토리가 생서되어 있어야 한다.

>> get_images.py

def get_images(url):
    path = get_path(url)
    try:
        os.mkdir(path)
    except:
        pass

    response = requests.request("GET", url, headers=headers)
    data = BeautifulSoup(response.text, 'html.parser')
    images = data.find_all('img', src=True)
    print('Number of Images :', len(images))

    # select src tag
    image_src = [x['src'] for x in images]

    # select only jp format images
    image_src = [x for x in image_src if x.endswith('.jpeg')]
    image_count = 1
    for image in image_src:
        print(image)
        image_file_name = path+'/'+str(image_count)+'.jpeg'
        print(image_file_name)
        with open(image_file_name, 'wb') as f:
            res = requests.get(image)
            f.write(res.content)
        image_count = image_count + 1

get_images('https://medium.com/@allanishac/9-wild-animals-that-would-make-a-much-better-president-than-donald-trump-b41f960bb171')

    다운로드 결과는 아래와 같다.

Flask 이미지 분류 - Flask imiji bunlyu

    [get_images.py 전체소스 다운로드]


웹페이지 생성

    웹 서비스를 위해 아래와 같이 "home.html""image_class.html" 두개의 웹페이지를 생성하자.

  1. "home.html"

    : 사용자가 URL을 입력할 수 있는 텍스트 박스를 가진 기본 페이지

  2. "image_class.html"

    : 이미지를 분류하는 페이지

1. home.html

Flask 이미지 분류 - Flask imiji bunlyu

    위 이미지와 같이 검색 컨텐이너에서 데이터를 수집하기 위해 home.html파일을 아래와 같이 작성한다.

Flask 이미지 분류 - Flask imiji bunlyu

    이렇게 함으로써 백엔드(backend) 코드는 "search"라는 이름으로 어떠한 데이터를 받아들일 수 있다.

    [home.html 전체소스 다운로드]

2. image_class.html

    결과를 계산하는 동안 다른 페이지는 아래처럼 결과를 보여준다. 이 "image_class.html"페이지는 매번 요청시마다 업데이트 되고 웹페이지에서 아래 정보를 볼 수 있게 한다.

  1. 이미지 분류
  2. 이미지
  3. 모든 이미지 분류에서 빈도수
    Flask 이미지 분류 - Flask imiji bunlyu

    [image_class.html 전체 샘플소스 다운로드]

    실제로는 generate_html.py에서 동적으로 image_class.html을 생성한다.

>> generate_html.py

# define function to add the image in the html file with the class name
def get_picture_html(path, tag):
    image_html = """<p> {tag_name} </p> <picture> <img src= "../{path_name}"  height="300" width="400"> </picture>"""
    return image_html.format(tag_name=tag, path_name=path)

# define function to add the list element in the html file
def get_count_html(category, count):
    count_html = """<li> {category_name} : {count_} </li>"""
    return count_html.format(category_name = category, count_ = count)

# function to calculate the value count
def get_value_count(image_class_dict):
    count_dic = {}
    for category in image_class_dict.values():
        if category in count_dic.keys():
            count_dic[category] = count_dic[category]+1
        else:
            count_dic[category] = 1
    return count_dic

# function to generate the html file from image_class dictionary
# keys will be the path of the images and values will be the class associated to it.
def generate_html(image_class_dict):
    picture_html = ""
    count_html = ""

    # loop through the keys and add image to the html file
    for image in image_class_dict.keys():
        picture_html += get_picture_html(path=image, tag= image_class_dict[image])

    value_counts = get_value_count(image_class_dict)

    # loop through the value_counts and add a count of class to the html file
    for value in value_counts.keys():
        count_html += get_count_html(value, value_counts[value])

    [generate_html.py 전체소스 다운로드]

    이제 각각의 조각을 조합한 Flask 프로젝트를 설정하자.


Flask 프로젝트 설정

    프로젝트와 관련된 다음 작업을 완료했다.

  1. 이미지 분류 모델 : get_prediction.py
  2. 이미지 스크랩퍼 : get_images.py
  3. URL을 입력받고 결과를 보여줄 웹 페이지 : home.html

    Flask 프로젝트를 구성하기 위해 각각의 모듈을 아래와 같은 구조로 만든다.

Flask 이미지 분류 - Flask imiji bunlyu

Note : 위 디렉토리의 static디렉토리에 이미지를 templates에 html 파일을 저장해야한다. Flask는 해당 이름만을 검색한다. 만약 이를 변경하면 오류가 발생한다.

Flask Application 실행

    Flask 어플리케이션은 home.html 파일 먼저 랜더링 하여 자용자로부터 이미지 분류를 위한 요청을 받을 준비를 한다. 사용자가 URL을 입력하면 Flask는 POST를 감지하고 get_image_class 함수를 호출한다.

    get_image_class 함수는 다음과 같은 단계로 진행된다.

  1. 입력받은 URL로부터 이미지를 다운로드하고 저잫하기 위한 요청을 한다. : get_images
  2. 이미지 분류를 진행하고 사전(dictionary)형태로 결과를 반환하는 get_prediction.py파일에 저장된 이미지 디렉토리 경로를 전달한다.
  3. 마지막으로 반환된 사전을 generate_html.py파일에 전달하여 결과 페이지를 생성한다.
Flask 이미지 분류 - Flask imiji bunlyu
>> get_class.py

# importing the required libaries
from flask import Flask, render_template, request, redirect, url_for
from get_images import get_images, get_path, get_directory
from get_prediction import get_prediction
from generate_html import generate_html
from tensorflow as tf
import json

app = Flask(__name__)

# mapping
imagenet_class_mapping = json.load(open('imagenet_class_index.json'))

# use the pre-trained model
model = tf.keras.applications.DenseNet121(
        include_top=True, weights='imagenet', input_tensor=None, input_shape=None,
        pooling=None, classes=1000)

# define the function to get the images from the url and predicted the class
def get_image_class(path): 
    # get images from the URL and store it in a given path
    get_images(path)
    # predict the image class of the images with provided directory
    path = get_path(path)
    images_with_tags = get_prediction(model, imagenet_class_mapping, path)
    # generate html file to render once we predict the classes
    generate_html(images_with_tags)

    여기까지 사용자에게 결과를 제공하기 위한 준비를 마쳤다. success 함수를 호출한 후 image_class.html 파일을 랜더링한다.

>> get_class.py

# by deafult render the "home.html"    
@app.route('/')
def home():
    return render_template('home.html')

@app.route('/', methods=['POST', 'GET'])
def get_data():
    if request.method == 'POST':
        user = request.form['search']
        # if search button hit, call the function get_image_class 
        get_image_class(user)
        #render the image_class.html
        return redirect(url_for('success', name=get_directory(user)))


@app.route('/success/')
def success(name):
    return render_template('image_class.html')


if __name__ == '__main__' :
    app.run(debug=True)

    [get_class.py 전체소스 다운로드]

URL의 모든 이미지에 대한 예측하기

    지금까지 각각의 이미지에 대해 개별적으로 예측을 진행했다. 여러 이미지를 포함하는 새로운 파라미터인 directory_pathget_category에 추가하여 이를 개선한다.


# get class of all the images present in the directory 
def get_category(model, imagenet_class_mapping, image_path):
    with open(image_path, 'rb') as file:
        image_bytes = file.read()

    transformed_image = tranform_image(image_bytes)
    outputs = model.predict(transformed_image.numpy()[np.newaxis, :])

    predicted_idx = str(np.argmax(outputs))

    return imagenet_class_mapping[predicted_idx]


# It will create a dictionary of the image path and the predicted class
# we will use that dictionary to generate the html file. 
def get_prediction(model, imagenet_class_mapping, path_to_directory):
    files = glob.glob(path_to_directory+'/*')
    image_with_tags={}
    for image_file in files:
        image_with_tags[image_file] = get_category(model, imagenet_class_mapping, image_file)[1]

    return image_with_tags

배포된 모델 작업

    이제 get_class.py를 실행하면 Flask 서버가 localhost:5000로 시작된다.

Flask 이미지 분류 - Flask imiji bunlyu