5
1

YOLOv8モデルを使って物体検出アプリケーションをFLASKで作成してみた

Last updated at Posted at 2024-03-02

[Python]初心者がYOLOv8モデルを使って物体検出/セグメンテーション/画像分類アプリケーションをFLASKで作成してみた


はじめに

こんにちは。リスキリングを目的にプログラミングを学びなおしている元プログラマー(現なんちゃってプロジェクトマネージャー)の @Nuts12 です。
現在私はIT企業に勤めながら、Aidemy というプログラミングスクールで夜な夜なPythonと機械学習に取り組んでいます。 本投稿はAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しているブログ記事の第二弾となります。 今回は色々と試行錯誤を繰り返しながら学習を進めることとなりました。失敗談を含んだ奮闘記となっておりますので、これから物体検出にチャレンジされる皆さんの少しでもご参考になれば幸いです。

開発環境について

以下、今回の開発環境になります。
Operating System: Windows 11 Home 64-bit
System Model: Surface Pro 6
Processor: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz (8 CPUs), ~2.1GHz
Memory: 16384MB RAM
Python: Python 3.9.13
開発ツール: VSCode

使用したパッケージ、モジュール一覧
from results_set import set_result, set_result2
from flask import Flask, render_template, url_for, request, redirect, session, flash
import numpy as np
import shutil
import datetime
import cv2
import os
from dotenv import load_dotenv
from datetime import timedelta
from ultralytics import YOLO
import gc

パッケージバージョン
Flask==3.0.2
numpy==1.26.3
opencv-python==4.9.0.80
python-dotenv==1.0.1
ultralytics==8.1.20

学習テーマの選定

今回の学習テーマについて、前回修了したデータ分析講座と今回取り組んだAIアプリ開発講座で学んだ内容を踏まえて、折角なら実践的且つ開発案件として多く取り扱われているテーマで成果物を作成することを目標にしたいと、漠然としたイメージを持っていました。

それが何なのかネットを漁ってみても答えが出ず手詰まりになってしまったので、Aidemyのチューターさんにご相談したところ、今ホットなキーワードとして、「生成AI」「自然言語処理」「物体検出」 等をご教示いただきました。 確かにChatGPT等、「生成AI」 の分野は今後も需要は伸びていくであろうと安直な気持ちでまずは 「生成AI」 の分野で何かできないか調査を開始することにしました。

生成AI(OpenAI)の検討

生成AIと言えばやっぱりChatGPTということで、PythonからChatGPTを使う方法をまずは調査しました。少し調べるだけでOpenAIというChatGPTのAPIがあることがわかります。
そこでopenaiライブラリをインストールし、早速コードを記述して実行して見ました。

openai sample
from openai import OpenAI

API_Key = 'ここに自分のAPI_Keyを記載'
GPT_MODEL = 'gpt-3.5-turbo'

def Ask_ChatGPT(message):
    
    # クライアント
    client = OpenAI(api_key=API_Key)
    
    # ユーザーの質問に対して回答生成
    completion = client.chat.completions.create(
                     model    = "gpt-3.5-turbo",     # モデルを選択
                     messages = [{
                                "role":"user",       # ロール
                                "content":message,   # メッセージ 
                                }],

                     max_tokens  = 1024,             # 生成する文章の最大単語数
                     n           = 1,                # いくつの返答を生成するか
                     stop        = None,             # 指定した単語が出現した場合、文章生成を打ち切る
                     temperature = 0,                # 出力する単語のランダム性(0から2の範囲) 0であれば毎回返答内容固定
                     stream      = False,            # 生成した回答を段階的に出力するか否か
        )
    
    # 回答部分抽出
    response = completion.choices[0].message.content   
    return response

message = "日本で2番目に高い山は?"

# ChatGPT起動
res = Ask_ChatGPT(message)

# 出力
print(res)

なにやらエラーが出力ています。

openai sample 出力結果
openai.RateLimitError: Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}

エラーメッセージの中にbillingという文字が見えたのでいやな予感がしつつ、ネットで検索すると、やはりOpenAIを使用するには課金が必要であることがわかりました。

トークン数による従量課金且つ低コストで使用出来るようですが、趣味でやっている内は少しでもお金ががかかるとなると少し腰が引けてしまいます。

なお、初期登録から3か月はお試し期間があるようです。しかし私の場合なぜか去年の3月に一度登録を行っていることが履歴から見て取れたのでお試し期間も使えないことが分かりました。(全く記憶にない自分が少し怖いですが。。)

この時点で、「生成AI」 をテーマにした成果物作成を早くも断念することとしました。

学習テーマを「物体検出」に決定

「生成AI」 を断念したので残ったキーワードは 「自然言語処理」「物体検出」 の2つです。「自然言語処理」 はこれまで私が学習してきた範疇から少し外れてしまうので次の機会に取っておくとして、今回は消去法で 「物体検出」 をテーマとして成果物を作成する方針で決定としました。

改めて物体検出(物体検知)とは何かを調べてみます。簡単に説明すると、「画像や動画から物体を検知(検出)する技術です。 人間は、見ている画像からモノの位置とそれが何であるかという判断を即座に行えますが、それをコンピューターで実現する方法が物体検知です」ということが分かります。

これまで学習してきたCNN(畳み込みニューラル ネットワーク)の活用や、RNN(リカレント ニューラルネットワーク)といった新しい技術も利用できそうでチャレンジとしてはいい感じです。

また、Aidemy のAIアプリ開発講座では、Flask を使用したWebアプリケーション作成方法も学習しているので、単に物体検知モデルを構築するだけではなく、それをWebアプリケーションに落とし込むところまでを今回の成果物にしたいと漠然としたイメージを持ちながら次のステップに進みます。

物体検出モデル構築のステップ

物体検出とは言っても、これまでと同じ教師あり学習と同様以下のステップで進められる想定で作業に着手していきます。

  1. 画像データセットの取得
  2. 物体検出モデルの選定
  3. 画像データセットの前処理
  4. 訓練データ・テストデータの設定
  5. 訓練データによる学習と検証データによる評価

画像データセットの取得

Scrapingによるデータ取得

まずはインプットとなるデータの入手方法を検討しました。
Scrapingを使ってインターネットからダウンロードすることができないか調査をしてみたところ、icrawler というライブラリを使って画像をキーワード検索した結果枚数指定でダウンロードできるようです。

早速コードを記載して画像ダウンロードを試してみます。クローラーモデルは BingImageCrawler だけでなく GoogleImageCrawler も使用しています。
なお、とりあえずの検索キーワードは cat です。

icrawler 画像ダウンロード
# 必要な機能の読込
from icrawler.builtin import GoogleImageCrawler
from icrawler.builtin import BingImageCrawler

# 画像のダウンロード
google_crawler = GoogleImageCrawler(downloader_threads=4,
    storage={'root_dir': 'googledata'})
google_crawler.crawl(keyword='cat', max_num=1000)

bing_crawler = BingImageCrawler(downloader_threads=4,
                                storage={'root_dir': 'bingdata'})
bing_crawler.crawl(keyword='cat', filters=None, offset=0, max_num=1000)

こんな感じで簡単に猫の画像を2000枚程度入手できました。(ダウンロード画像が取得できない等一部エラーが発生するためmax_numで指定した枚数を必ずしも取得することはできません)

000001[1].jpg

こんな数行で大量データを取得できるなんですごい! と感心しながら、でも欲しいのは物体検知用の画像サンプルであることを思い直し、キーワードを何となくそれっぽい object detection sample に変更してみて再度プログラムを実行して見る。

結果、その時に取得できたのは以下のような物体検知後の画像ファイルばかりとなりました。どうも物体検知用として期待する画像をScrapingを使って取得するのは簡単ではなさそうと分かり、他の入手方法を検討することとしました。

000014.jpg

公式サイトからのデータセット取得

物体検出に使えそうなデータセットなんて、このご時世絶対どこかからかダウンロードできるはずとChatGPT様にお伺いを立ててみる。するとメジャーな公開ダウンロードサイトとして以下の3つがあることが分かる。

  1. COCO
  2. PASCAL VOC
  3. Open Images

以下はそれぞれの説明。

COCO(Common Objects in Context)データセットは、物体検出や画像分類などのコンピュータビジョンタスクのために広く使用される大規模な画像データセットです。COCOデータセットは、Microsoftが主導して作成されました。

PASCAL VOC(Visual Object Classes)データセットは、物体検出やセグメンテーションなどのコンピュータビジョンタスクのための非常に有名なデータセットの1つです。このデータセットは、物体検出、セグメンテーション、およびクラス分類の3つの主要なタスクに焦点を当てています。

Open Imagesデータセットは、Googleが公開している大規模な画像データセットです。このデータセットは、多様なカテゴリの画像と対応するラベル(アノテーション)を含んでいます。Open Imagesデータセットは、様々なコンピュータビジョンのタスクに利用されますが、特に物体検出や画像分類のためのデータセットとして広く知られています。

Open Images

とりあえずGoogle様が提供しているデータセットなら汎用的に利用できるだろうということでまずはOpen Imageのサイトにアクセスしてみる。

そこで目にしたのが、The full set of 9,178,275 images. の文字。いやいやそれは流石に多すぎるのでは? 貧弱なノートパソコンではフルセットのダウンロードは無理なので部分的にダウンロードできないかホームページを確認してみる。

If you only need a certain subset of these images and you'd rather avoid downloading the full 1.9M images, we provide a Python script that downloads images from CVDF.
Download the file downloader.py (open and press Ctrl + S), or directly run:
wget https://raw.githubusercontent.com/openimages/dataset/master/downloader.py
Create a text file containing all the image IDs that you're interested in downloading. It can come from filtering the annotations with certain classes, those annotated with a certain type of annotations (e.g., MIAP). Each line should follow the format $SPLIT/$IMAGE_ID, where \$SPLIT is either "train", "test", "validation", or "challenge2018"; and \$IMAGE_ID is the image ID that uniquely identifies the image. A sample file could be:
train/f9e0434389a1d4dd
train/1a007563ebc18664
test/ea8bfd4e765304db
Run the following script, making sure you have the dependencies installed:
python downloader.py \$IMAGE\_ LIST\_ FILE --download_folder=$DOWNLOAD_FOLDER --num_processes=5
For help, run:
python downloader.py -h

wgetdownload.py をダウンロードして、コマンドラインを打ち込めばうまくいきそう。

でもその前に wget をダウンロードしなければ、ということでネットから cygwin という Windows上にLinuxなどのUNIX系OSの環境を再現するソフトウェアパッケージをインストールすれば、 wget も同時にインストールすることを確認します。

ということで、 cygwin をネットからダウンロードし、wget を指定してインストールを実行。 最後に環境変数 PATHcygwin のインストールディレクトリ配下の bin フォルダを追加して準備完了です。

なお、Windows環境であれば、コントロールパネルから、システム環境変数を編集⇒詳細設定タブ⇒環境変数でシステム環境変数(ユーザー共通の環境変数)を修正することが出来ます。

downloader.pywget で取得して早速コマンドを実行してみます。

downloader.pyの実行
>python downloader.py train/f9e0434389a1d4dd 
FileNotFoundError: [Errno 2] No such file or directory: 'train/f9e0434389a1d4dd'

train/f9e0434389a1d4ddが無い? この文字列は IMAGE_ID のサンプルでは無かったのでしょうか? downloader.pyの中にも、このIDの記載があるのにちょっとよく分かりません。

引数として明示的に image_list='train/f9e0434389a1d4dd' を指定しても結果は同じでした。その他に巨大なデータセットを部分的にダウンロードする術が分からず、敢無く Open Image からのデータセットダウンロードを諦めることとしました。

COCO

ということで、次に訪れたのがCOCOです。

coco2017のデータセットなら訓蓮画像、検証画像、アノテーションファイルの一通りのセットをダウンロードできそうです。ちなみにアノテーション (annotation) とは、テキストや音声、画像などのさまざまな形式のデータに、タグやメタタグと呼ばれる情報を付与する作業のことです。この情報によって画像の教師あり学習を行うことが出来ます。
因みに私は今回の学習の中で初めてアノテーションの存在を知りました。。

Images
2017 Train images [118K/18GB]
2017 Val images [5K/1GB]
2017 Train/Val annotations [241MB]

でも、このサイトでもそう簡単にはダウンロードさせてくれないようです。ダウンロードページでRecommendされている以下の手順どおり実行しても BucketNotFoundException が発生してしまいます。一体、何が悪いのか見当がつきません。

Our data is hosted on Google Cloud Platform (GCP). gsutil provides tools for efficiently accessing this data. You do not need a GCP account to use gsutil. Instructions for downloading the data are as follows:

(1) Install gsutil via:curl https://sdk.cloud.google.com | bash
(2) Make local dir:mkdir val2017
(3) Synchronize via:gsutil -m rsync gs://images.cocodataset.org/val2017 val2017
The splits are available for download via rsync are: train2014, val2014, test2014, test2015, train2017, val2017, test2017, unlabeled2017. Simply replace 'val2017' with the split you wish to download and repeat steps (2)-(3).
Finally, you can also download all the annotation zip files via:

(4) Get annotations:gsutil -m rsync gs://images.cocodataset.org/annotations [localdir]
The download is multi-threaded, you can control other options of the download as well (see gsutil rsync). Please do not contact us with help installing gsutil (we note only that you do not need to run gcloud init).

gsutilの実行結果
>gsutil -m rsync gs://images.cocodataset.org/val2017 val2017
BucketNotFoundException: 404 gs://images.cocodataset.org bucket does not exist.

そこでまたネットの力を借りてダウンロードする方法を検索してみます。

Open Image の時にインストールした wget コマンドでもダウンロードできるようです。とりあえず実行させてみます。

wgetの実行
> wget http://images.cocodataset.org/zips/train2017.zip
wget : ストリームが長すぎます。
発生場所 行:1 文字:1
+ wget http://images.cocodataset.org/zips/train2017.zip
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Invoke-WebRequest], IOException
    + FullyQualifiedErrorId : System.IO.IOException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

散々待たされた挙句、またもやエラーが発生。今度はストリームが長すぎるとのこと。もう心が折れそうです。

そこで最後の頼みの綱であるChatGPT様にPythonのコードでダウンロードできないか伺ってみる。返ってきた答えを少し修正したのが以下のコード。

COCOデータセットダウンロード
import os
import requests
import zipfile

# COCOデータセットのURLとファイル名
train_images_url = 'http://images.cocodataset.org/zips/train2017.zip'
val_images_url = 'http://images.cocodataset.org/zips/val2017.zip'
annotations_url = 'http://images.cocodataset.org/annotations/annotations_trainval2017.zip'

train_images_zip = 'train2017.zip'
val_images_zip = 'val2017.zip'
annotations_zip = 'annotations_trainval2017.zip'

# ダウンロード関数
def download_and_extract(url, zip_file):
    if not os.path.exists(zip_file):
        print(f'Downloading {url}...')
        r = requests.get(url, stream=True)
        with open(zip_file, 'wb') as f:
            for chunk in r.iter_content(chunk_size=1024):
                if chunk:
                    f.write(chunk)
        print(f'{zip_file} downloaded.')
    else:
        print(f'{zip_file} already exists.')

    # ZIPファイルを展開
    print(f'Extracting {zip_file}...')
    with zipfile.ZipFile(zip_file, 'r') as zip_ref:
        zip_ref.extractall('.')
    print(f'{zip_file} extracted.')

# ダウンロードと展開
download_and_extract(train_images_url, train_images_zip)
download_and_extract(val_images_url, val_images_zip)
download_and_extract(annotations_url, annotations_zip)

今度は事前にファイルサイズも確認する。

COCOデータセットサイズ確認
import requests

url = "http://images.cocodataset.org/zips/train2017.zip"

response = requests.head(url)
if 'Content-Length' in response.headers:
    file_size_bytes = int(response.headers['Content-Length'])
    print(f"ファイルのサイズ: {file_size_bytes} バイト")
else:
    print("ファイルサイズを取得できませんでした。")
ファイルのサイズ: 19336861798 バイト

結果は約17GB。ちゃんとデータを読み込んでくれてそう。そこで前述の coco2017 データセットダウンロードを実行。時間がかかりそうなので一晩寝て待つことにしました。

次の朝、恐る恐る実行結果を見てみると取り敢えず正常終了はしてそうです。データも確認してみるとしっかりデータがダウンロードされていました!

なお、訓練画像は118,287枚(約18.2GB)ありました。これだけあれば色々試すことが出来そうです。ようやく次のステップに進めます。

(参考)YouTubeからの動画ダウンロード

試しに動画ダウンロードも試してみたので備忘を兼ねて記載しておきます。

Pythonを使って簡単なコードでYouTube動画がダウンロードできるんですね。
試しに後述の YOLOv8 モデルを使ってダウンロードした動画を物体検出してみましたが、ちゃんと検出してくれてました。ただ、貧弱なノートパソコンを使っているので実行時間は結構かかりました。

同じような環境で時間が無い方は Google Colab でGPUを使うのがよろしいかと。

動画で物体検出
from ultralytics import YOLO

# Load a model
model = YOLO('yolov8n.pt')

# Predict the model
model.predict('./YOASOBI.mp4', save=True, conf=0.5, exist_ok=True)

なお、物体検出実行後の動画はWindowsだとデフォルトで .avi形式で出力される模様。.mp4形式で出力する方法について調べてみましたが、YOLO のライブラリに手を加えないと難しそうなので断念しました。

物体検出モデルの選定

次は物体検出に使うモデルの選定に移ります。ChatGPT様にお伺いを立てるとメジャーなモデルとして3つ回答がありました。以下はその概要です。

YOLO (You Only Look Once):

提案者: Joseph Redmon, Santosh Divvala, Ross Girshick, Ali Farhadi
概要: 2016年に提案された、リアルタイム物体検出のためのモデルです。画像全体を一度に処理し、グリッドセルに分割してそれぞれのセルが物体を検出するか否かを予測します。
特徴: 高速でリアルタイムの処理が可能でありながら、比較的単純なネットワーク構造を持っています。

Faster R-CNN(ファスターR-CNN):

提案者: Shaoqing Ren, Kaiming He, Ross Girshick, Jian Sun
概要: 2015年に提案された、物体検出の分野で革新的なモデルです。Region Proposal Network(RPN)と呼ばれるネットワークを使用して領域提案を行い、それを使用して物体の境界ボックスを回帰させます。
特徴: 高い精度と高速な推論速度が両立しており、多くの物体検出のタスクで広く使用されています。

SSD (Single Shot MultiBox Detector):

提案者: Wei Liu, Dragomir Anguelov, Dumitru Erhan, Christian Szegedy, Scott Reed, Cheng-Yang Fu, Alexander C. Berg
概要: 2016年に提案されたモデルで、YOLOと同様にリアルタイム物体検出を目指しています。畳み込みネットワークを使用して画像内の複数の位置に対して異なるスケールとアスペクト比で領域提案を生成します。
特徴: 複数のスケールでの検出が可能であり、YOLOよりも小さい物体の検出に優れています。

実際にネットで検索をしてみると上記3モデルの内YOLOのキーワードが上位にヒットすることが多く、情報も比較的新しいものが多かったため、初学者の自分はYOLOを採用することとし、使用するバージョンは最新であろうYOLOv8モデルとする方針にしました。

YOLOv8の動作検証

YOLOv8 がどんなものなのか簡単な動作検証を行ってみます。 YOLOv8 で用意されているのは物体検出も含め5つのモデルがありますのでそれぞれを使って画像を解析してみます。

YOLOv8動作検証
from ultralytics import YOLO

# Load a model
model = YOLO('yolov8n.pt')

# Predict the model
model.predict('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, show_boxes=True, exist_ok=True)

# Load a model
model = YOLO("yolov8n-seg.pt")

# Predict the model
model.predict('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, show_boxes=True, exist_ok=True)

# Load a model
model = YOLO("yolov8n-pose.pt")

# Predict the model
model.predict('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, show_boxes=True, exist_ok=True)

# Load a model
model = YOLO("yolov8n-obb.pt")

# Predict the model
model.predict('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, show_boxes=True, exist_ok=True)

# Load a model
model = YOLO("yolov8n-cls.pt")

# Predict the model
model.predict('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, show_boxes=True, exist_ok=True)

実行すると自動的に .pt形式の学習モデルファイルがダウンロードされます。このモデルファイルは、精度や解析対象のデータサイズによってファイルが選択できるようになっていて、それらの値が高いほどモデルファイルのサイズも処理にかかる時間も増加していきます。

また、predict関数のパラメータにsave=Trueを選択することで、解析後のデータを保存することが出来ます。モデルによって解析後データが保存されるデフォルトフォルダが決まっています。

なお、exist_ok=True オプションを付けないと、同じモデルを使ったときに勝手に別名の保存フォルダが作成されていきます。

モデルの一つである YOLOv8-pose モデルは姿勢検出といって、人の姿勢を線で推定することが出来る面白機能なのですが、今回は物体検出を目的としているので、作成するアプリケーションでは使用しないこととします。

また、YOLOv8-obb モデルは配向物体検出といって、説明には「物体検出よりもさらに一歩進んで画像内の物体をより正確に見つけるために余分な角度を導入する」と記載がありますが正直使い道がよくわかりません。

よって、この後作成する物体検出アプリケーションとしては、YOLOv8 (物体検出)、YOLOv8-seg (セグメンテーション)、YOLOv8-cls(画像分類)の3つのモデルを使用することを検討しながら進めていきます。

YOLOv8 モデル一覧

Model Filenames Task Output Image Folder
YOLOv8 yolov8n.pt 
yolov8s.pt 
yolov8m.pt 
yolov8l.pt 
yolov8x.pt
Detection .\runs\detect\predict
YOLOv8-seg yolov8n-seg.pt
yolov8s-seg.pt
yolov8m-seg.pt
yolov8l-seg.pt
yolov8x-seg.pt
Instance Segmentation .\runs\segment\predict
YOLOv8-cls yolov8n-cls.pt
yolov8s-cls.pt
yolov8m-cls.pt
yolov8l-cls.pt
yolov8x-cls.pt
Classification .\runs\classify\predict
YOLOv8-pose yolov8n-pose.pt
yolov8s-pose.pt
yolov8m-pose.pt
yolov8l-pose.pt
yolov8x-pose.pt
yolov8x-pose-p6.pt
Pose/Keypoints .\runs\pose\predict
YOLOv8-obb yolov8n-obb.pt
yolov8s-obb.pt
yolov8m-obb.pt
yolov8l-obb.pt
yolov8x-obb.pt
Oriented Detection .\runs\obb\predict

YOLOv8のResults確認

YOLOv8 (物体検出)、YOLOv8-seg (セグメンテーション)、YOLOv8-cls(画像分類)の3つのモデルについては、画像解析時に戻される Resultsオブジェクト の中身を事前に確認しておきます。以下、手当たり次第にResultsオブジェクトの中身を確認したコードです。

resultsの内容確認
from ultralytics import YOLO
import numpy as np

def print_result(results):

    print('results:', results)

    for result in results:
        boxes = result.boxes  # Boxes object for bounding box outputs
        masks = result.masks  # Masks object for segmentation masks outputs
        keypoints = result.keypoints  # Keypoints object for pose outputs
        probs = result.probs  # Probs object for classification outputs
        print('result:', result)
        print('to numpy:', result.numpy())
        print('boxex:', boxes)
        print('boxex: class{}, conf{}'.format( boxes.cls, boxes.conf))
        print('masks:', masks)
        print('keypoints:', keypoints)
        print('probs:', probs)
        print('prot:', result.plot())
        # result.show()  # display to screen
        # result.save(filename='result.jpg')  # save to disk
        
def print_result2(results):

    print('results:', results)

    for result in results:
        probs = result.probs  # Probs object for classification outputs
        print('result:', result)
        print('to numpy:', result.numpy())
        print('probs:{}, conf{}'.format( probs.top5, probs.top5conf.numpy()))
        print(type(probs.top5conf))
        print('prot:', result.plot())
        # result.show()  # display to screen
        # result.save(filename='result.jpg')  # save to disk

# Load a model
model = YOLO('yolov8n.pt')
results = model('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, exist_ok=True)
print('物体検出')
print_result(results)

# Load a model
model = YOLO("yolov8n-seg.pt")
results = model('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, exist_ok=True)
print('セグメンテーション')
print_result(results)

# Load a model
model = YOLO("yolov8n-cls.pt")
results = model('https://ultralytics.com/images/bus.jpg', save=True, conf=0.5, exist_ok=True)
print('画像分類')
print_result2(results)

出力された内容から、検出された物体の座標情報らしき情報と、クラス(物体)、そしてそのその確率(精度)が取得できそうです。

なお、クラスの 0 は人、5 はバスになります。 また、物体検出とセグメンテーションは80クラス(後述のcoco128.yamlに定義されているものと同じ)、画像分類は1000クラス定義されていることが分かります。

resultsの内容抜粋
xywh: tensor([[739.8075, 633.0853, 139.3708, 486.8358],
        [145.1526, 650.0331, 192.0326, 508.1233],
        [283.6209, 634.8312, 120.8660, 454.4848],
        [402.7171, 474.2406, 800.6133, 492.7648]])
xywhn: tensor([[0.9133, 0.5862, 0.1721, 0.4508],
        [0.1792, 0.6019, 0.2371, 0.4705],
        [0.3501, 0.5878, 0.1492, 0.4208],
        [0.4972, 0.4391, 0.9884, 0.4563]])
xyxy: tensor([[670.1221, 389.6674, 809.4929, 876.5032],
        [ 49.1363, 395.9715, 241.1689, 904.0948],
        [223.1879, 407.5888, 344.0539, 862.0737],
        [  2.4104, 227.8582, 803.0237, 720.6230]])
xyxyn: tensor([[0.8273, 0.3608, 0.9994, 0.8116],
        [0.0607, 0.3666, 0.2977, 0.8371],
        [0.2755, 0.3774, 0.4248, 0.7982],
        [0.0030, 0.2110, 0.9914, 0.6672]])
boxex: classtensor([0., 0., 0., 5.]), conftensor([0.8875, 0.8518, 0.8420, 0.8066])

(参考)YOLOv8の動作検証

YOLOv8 なら、たった3行のコードでパソコンのカメラを使った物体検出も簡単にできます。パラメータの 0 はパソコンのカメラ番号です。

カメラを使った物体検出
### YOLOv8の動作検証
from ultralytics import YOLO

model = YOLO("yolov8n.pt")
model(0 , show=True) 

ちゃんと自分のことを人と認識してくれているようでちょっと嬉しいです。

画像データセットの前処理

ダウンロードされる学習モデルをそのまま使っても十分精度が出るのではないかと思いつつも、一応、YOLOv8 で物体検出モデルの学習を試みます。
その為に事前に COCO ホームページからダウンロードしていた coco2017 データセット用の巨大な1本に纏まっている.json形式のアノテーションファイルを YOLOv8 で読み取れる画像別の.txt形式に変換が必要となります。

.json形式から.txt形式への変換にはYOLOのホームページに掲載されているconvert_coco 関数を使うことができます。

アノテーションファイルをcoco形式(.json)からYOLO形式(.txt)に変換
from ultralytics.data.converter import convert_coco

convert_coco(labels_dir='.\annotations\instances_val2017')

coco128/coco8データセットの発見

アノテーションファイルを YOLOv8 で読み取れる.txt形式に変換したはよいものの、何せ画像枚数が118,287枚あるのでそのまま学習させるのは如何なものかと思いながら、YOLO のホームページを見ていると coco128 というデータセットが存在するような記載があったので、調べてみたらultralyticsGitHubにありました。元々は yolov5 のテスト用に準備されたデータセットだったみたいですね。coco128.yaml の一番下の行にはデータセットのURLが記載されています。

coco128 データセットURL
https://ultralytics.com/assets/coco128.zip

ダウンロードしてみると画像は128枚(だから coco128 なんですね)。データは訓練画像しかないようですがアノテーションファイルは初めから YOLOv8 で読み取れるの.txt形式になっています。
初めからこういうデータが欲しかった。(今までの苦労はいったい。。)

さらにYOLO のホームページを調べると coco8 というデータセットも準備されていることを発見。ついでにダウンロードしておきました。

coco8 なので画像データは8枚しかありませんが、ちゃんと訓練用とテスト用にデータが分かれており、.txt形式のアノテーションファイルも準備されています。使えそうなのでこちらもダウンロードしておきます。

coco8 データセットURL
https://ultralytics.com/assets/coco8.zip

訓練データ・テストデータの設定

非力なPCでは実践的な物体検出モデルの学習は出来ないと諦めていたので学習の流れだけを確認することにします。

ということでこれまでデータセットをいくつかダウンロードしましたが、8枚の画像ながら訓練とテストに初めから分かれている coco8 データセットを使うことにします。

coco8.zip をそのまま解凍したフォルダ構成を、適当なフォルダにコピーしました。フォルダ構成は以下のようになっています。

labelsの配下がアノテーションファイルの格納場所になっており、ファイル名で画像ファイルとの紐づけがなされています。

coco8データセットフォルダ構成
COCO8
│  LICENSE
│  README.md
│
├─images
│  ├─train
│  │      000000000009.jpg
│  │      000000000025.jpg
│  │      000000000030.jpg
│  │      000000000034.jpg
│  │
│  └─val
│          000000000036.jpg
│          000000000042.jpg
│          000000000049.jpg
│          000000000061.jpg
│
└─labels
    ├─train
    │      000000000009.txt
    │      000000000025.txt
    │      000000000030.txt
    │      000000000034.txt
    │
    └─val
            000000000036.txt
            000000000042.txt
            000000000049.txt
            000000000061.txt

訓練データによる学習と検証データによる評価

準備が整ったので、YOLO ホームページに掲載されているサンプルソースを参考にして学習をしてみます。最初は様子見でepoches数を10に設定して訓練実行。batchサイズは-1を指定することで最適化してくれます。

YOLOv物体検出学習
from ultralytics import YOLO

# Load a pretrained YOLO model (recommended for training)
model = YOLO('yolov8n.pt')

results = model.train(data='coco8.yaml', epochs=10, batch=-1)

# Evaluate the model's performance on the validation set
results = model.val()

このデータモデルを使っても、そこそこ時間がかかります。やはりGPU搭載の高機能PCじゃないと実践的な機械学習は難しいですね。
以下は実行結果です。物体検出モデルの評価指標としてmAPが使われています。mAPは100%が完全一致で、50%以上が正解とみなせる一つの敷居値となります。

学習と検証の結果
      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       1/10         0G     0.9024      2.245      1.236         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.893      0.522      0.721      0.509

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       2/10         0G     0.9851       2.38      1.432         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.905      0.527      0.735      0.509

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       3/10         0G      1.179      3.081      1.399         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.906      0.531      0.726      0.505

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       4/10         0G     0.9213      2.265       1.24         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.907      0.534      0.715      0.505

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       5/10         0G     0.7841      1.859      1.188         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.908      0.539       0.73      0.514

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       6/10         0G     0.7662      1.688      1.225         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.909      0.541      0.749      0.535

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       7/10         0G     0.9255      2.006      1.323         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17       0.91      0.543       0.75      0.535

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       8/10         0G      1.081      2.177      1.478         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.913       0.55      0.748      0.538

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       9/10         0G      1.001      1.492      1.238         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.913       0.55      0.747      0.538

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
      10/10         0G     0.8826      1.584      1.246         13        640: 100%|██
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9 
                   all          4         17      0.915       0.55       0.75      0.549

10 epochs completed in 0.012 hours.
Optimizer stripped from runs\detect\train33\weights\last.pt, 6.5MB
Optimizer stripped from runs\detect\train33\weights\best.pt, 6.5MB

Validating runs\detect\train33\weights\best.pt...
Ultralytics YOLOv8.1.20 🚀 Python-3.9.13 torch-2.2.0+cpu CPU (Intel Core(TM) i7-8650U 1.90GHz)
Model summary (fused): 168 layers, 3151904 parameters, 0 gradients, 8.7 GFLOPs
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-9
                   all          4         17      0.916       0.55       0.75      0.545
                person          4         10      0.972        0.3      0.485      0.219
                   dog          4          1          1          0      0.497      0.348
                 horse          4          2      0.964          1      0.995      0.698
              elephant          4          2          1          0      0.531      0.114
              umbrella          4          1      0.762          1      0.995      0.995
          potted plant          4          1      0.796          1      0.995      0.895
5
                person          4         10      0.972        0.3      0.485      0.219
                   dog          4          1          1          0      0.497      0.348
                 horse          4          2      0.964          1      0.995      0.698
              elephant          4          2          1          0      0.531      0.114
              umbrella          4          1      0.762          1      0.995      0.995
          potted plant          4          1      0.796          1      0.995      0.895
Speed: 0.6ms preprocess, 155.4ms inference, 0.0ms loss, 7.8ms postprocess per image
Results saved to runs\detect\train332

訓練画像数が4枚しかないので、何とも言えないですがmAPの数値は学習数と正の相関関係がありそうです。また、訓練が終わると勝手に、最適な学習モデル(best.pt)と最終的な学習モデル(last.pt)を保存してくれます。

また、学習時のデータも画像やcsvファイルの形式でデフォルト ./runs/detect/trainXX 配下に作成されます。

2024-03-03 024244.png

model.val関数は、自動的に best.pt を使って評価を実行してくれるようです。
また、BOX(P は 精度(Precision)、Rは再現率(Recall)です。人のmAPが50%を切っているのが気になりますが、学習件数の問題ということであまり気にしないことにします。なお、best.pt を使って、転移学習を行うことも可能です。簡単ですね~。

こちらも学習時と同様に、評価時のデータがデフォルト ./runs/detect/trainXXX 配下(ただし学習時とは違うフォルダ)に作成されます。

2024-03-03 024749.png

物探検出アプリケーションの作成

物体検出の学習に一旦区切りをつけて、アウトプットとなるアプリケーション作成を設計していきます。

ここで、YOLOv8 を使って単純に画像をインプットとして、物体検出、セグメンテーション、画像分類の3種類の検出結果が表示させるアプリを Flask で作れたらそれっぽいものができるんっではないかと安直な考えを思いつきます。

他にいいアイデアもなく、このタイミングで YOLOv8 機能を使ったアプリ作成を具体的に目指すことになりました。

画面設計(HTML作成)

先ずはWebデザインを行います。 HTML作成に当たり Bootstrap v5.3 というHTMLのフレームワークを取り入れてみることにしました。 これの何がすごいかというと、用意されているテンプレートのstylesheetを使うだけで、簡単に見栄えがよくて、レスポンシブなHTMLが作成できるとのことです。

早速使ってみましたが、初学者には少しとっつきづらかったですね。どこまで Bootstrap の定義を採用すればいいのかその判断が悩ましいです。

極力 Bootstrap のhtmlサンプルやcssを使うようにしていましたが、headerやfooterのサイズ、フォントの位置など、細かいところで思うように表示されない箇所が多く、結局、最後の方は力業でどうにかしてしまいました。

おそらく、Bootstrap の有識者の方がみたら余計なコードがたくさん記載されていると思われるでしょうが、これも経験ということでご勘弁ください。(お気づきの点あればコメントいただけると幸いです)

アプリ設計(Flask)

Flask でアプリといっても、設計上WEBの画面は一枚だけ、そこにインプットとなる画像と、YOLOv8 で検出された画像を表示させるだけ、なんならその検出したクラスも表示させるだけななので、そんな難しくは無いだろうとこの時は甘く見てました。

ネットで調べたら同じような事考えている人は大勢いて、参考となるアプリを簡単に探すことができました。
そこで今の時代Pythonを学んでいる人が世の中に大勢いることを再認識するのでした。まだまだ自分も精進が必要です!

以下、今回大いに参考にさせていただいたYouTube動画になります。

テンプレートエンジン jinjaに触れる

HTML上でPythonのコードを記述するのに、テンプレートエンジンとなる jinja というパッケージを使います。だいぶ前に開発経験のあるjavaでいうところのjsp(Java Server Page)みたいなものだと認識しながらコーディングしてました。

ただし、Pythonのコードが完全につかえる訳ではなく、何度も失敗しながら試行錯誤することになりました。以下、1枚のHTMLテンプレートを作成しただけで躓いてしまった点を備忘を兼ねて列挙しておきます。

  1. jinjaの中で変数の値を変更することが出来ない
  2. zip関数が使えない
  3. len関数が使えない
  4. format関数が使えない

これらはHTMLのテーブルを jinja を使って動的に作成するときに直面した課題です。Python側からリスト型変数を渡して、jinja の中でループを使って動的にテーブルデータを作成しようとしたのですが、jinja 側でリスト型変数をうまく処理することが出来ず、最終的には全てのパラメータをばらしてjinja 側に渡すようにして課題を解決していきました。

また、今回はHTMLのテーブルデータのカラムが3つ、内1つがindexだったので、jinjaloop.index というループ文中で使える変数を用いてどうにかなったのですが、それ以上のカラム数のテーブルを動的に作るときはどうするのでしょうかね??

HTMLテーブルデータを動的に作成(抜粋)
                    <!-- 出力結果 -->
                    {% if exit %}
                    <h3>{{ "出力結果" }}</h3>

                    <!-- リスト出力 -->
                    <table class="table table-striped" border="1">
                        <thead>
                            <tr>
                                <th scope="col">{{ "#" }}</th>
                                <th scope="col">{{ "クラス" }}</th>
                                <th scope="col">{{ table_clm }}</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for a, b in table_data.items() %}
                            <tr>
                                <th scope="row">{{ loop.index }}</th>
                                <td>{{ a }}</td>
                                <td>{{ b }}</td>
                            </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                    {% endif %}

GitHub登録とRender登録

一通りコーディングとテストが完了したので早速 GitHub にアップして、Render にアプリケーションをデプロイします。プログラムの中で環境変数 SECRET_KEY を参照しているので、忘れずにRenderEnvironment に登録を行います。

2024-03-02 193952.png

デプロイが無事完了したので、いよいよ本番稼働確認です。URLにアクセスすると初期画面が表示されます。

成功か!? と思った矢先にエラーが発生します。エラーの内容をみるとどうやら使用メモリが上限の500MBを超えてしまったとのこと。 その後何回か動かしてみますが、1,2度実行すると同じエラーが発生してしまいます。

アプリが落ちても勝手に再起動されるみたいなので、ほったらかしにしていたら Render より直々に警告メールをもらってしまいました。フリーでサーバーを使わせてもらっているので、さすがに無視するわけにはいかず、少しソースに手を加えることにします。

Renderからの警告メール


Render

Web Service flask_yolov8_app exceeded its memory limit

An instance of your Web Service flask_yolov8_app exceeded its memory limit, which triggered an automatic restart. While restarting, the instance was temporarily unavailable.

This might have been caused by:

  • A memory leak in your application
  • A spike in incoming traffic
  • An undersized instance type for your use case

Recommended actions

Check your service's logs and metrics to help identify why the instance exceeded its memory limit.

  • If you find a memory leak, patch the leak and redeploy your service.
  • If you experienced a traffic spike, consider scaling your service to help handle future spikes.
  • Otherwise, your service might require more memory for your use case. To increase memory, upgrade your instance type.

If you take no action, your service might exceed its memory limit again, resulting in additional interruptions.

If you can't resolve this issue, please reach out to support@render.com.

Best,
The Render team


You can change notification settings for this service, along with default notification settings for your account/team.

ソースの修正(メモリ管理)

メモリを使っている箇所はあたりがついています。YOLOv8 の3つのモデルです。

アプリケーション起動時に物体検出、セグメンテーション、画像分類の3つのモデルを作成し、リクエストを受け付けたタイミングで画像変換を行っていましたが、初期稼働時にモデルファイルをダウンロードしたら一旦モデルを削除する変更を行います。

そして、リクエストを受け付けたタイミングでモデルを再作成し、画像変換後、リクエストを戻す前に、モデルを削除、且つガベージコレクションを明示的に実施しメモリを解放するように変更します。

レスポンスは勿論悪くなりますが、致し方ないところです。

メモリ対策①モデルの初期化と削除
# モデル初期化(モデルファイルダウンロード)
YOLO('models/yolov8n.pt')
YOLO('models/yolov8n-seg.pt')
YOLO('models/yolov8n-cls.pt')

# メモリ対策(モデルファイルダウンロード後メモリ開放)
gc.collect()
メモリ対策②モデルはリクエストの都度作成し画像変換後削除
    # モデル初期化
    model = YOLO('models/yolov8n.pt')
    results = model(img_path, save=True, exist_ok=True, project=result_dir)
    del model
メモリ対策③リクエスト返却直前にガベージコレクションを実施
    # # メモリ対策 (入力画像オブジェクトも念のため削除)
    del img
    gc.collect()

    # 画像出力
    return render_template('index.html',
                           exit=True,
                           title=title,
                           content=img_path,
                           content2=result_path,
                           table_clm=table_clm,
                           table_data=dic )

本対応後、再度アプリにアクセスしてみましたが今度は何度動かしてもアプリが落ちることはありませんでした。本対応がうまく機能したようです。これで当初予定していた物体検出アプリケーション作成は完了となります!!

GitHubRender アプリケーションのURLを以下に掲載しておきます。

今後の学習課題とまとめ

本稿ではPythonを使った物体検出方法を実践すべく、データセットの準備と加工、YOLOv8 モデルを使った物体検出方法とその学習方法、および Flask を使ったWebアプリケーションの作成まで、実際に手を動かしてきた手順と課題について取り上げました。

調べながら手を動かしWebアプリケーションを作成するという当初の目的を達成することが出来たのは、大変貴重な経験になったと実感しています。

一方で新しい課題(目標?)も出来ましたので、備忘を兼ねて以下に列挙しておきます。

  1. オリジナル画像とアノテーションを使った新しい物体検出クラスの学習
  2. 物体検出モデルのチューニング手順の実践
  3. Bootstrapを用いたWebデザインスキルとテクニックの習得
  4. Flaskアプリケーションを使ったより複雑なWebサイト構築スキルの習得
  5. これまで学んできたPythonのデータハンドリング手法やディープラーニングの総復習(特にOpenCVとCNN)
  6. 開発環境の見直し(Google Colabの導入検討等)※機械学習するには今の環境は非力すぎる。。
  7. Pythonでここだけは絶対的な自信があるという領域を1つ作ること

前回記事でも言及しましたが、知識の定着や学習意欲を継続させる為、今後は学習した内容を少しずつでもいいので Qiita のブログで発信していく、Kaggle のコンペティションに参加する、GitHub に成果物をアップしていく、等々、情報発信や成果物作成に比重を置いて学習を継続できればと思います。

冒頭でお伝えしたとおり本稿はAidemyの修了レポートを兼ねているため長文となってしまいましたが、最後までお付き合いいただき有難うございました。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1