Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

sqlite3でDBを操作してみる(C++編) ~~続き~~

先日C++sql文を利用使用してDB操作する方法をまとめましたが、
データ挿入まででデータ閲覧までは記事に載せきれませんでした。
elsammit-beginnerblg.hatenablog.com

今回はデータ閲覧方法についてまとめていきたいと思います!!
f:id:Elsammit:20210529125301j:plain



■sqlite3操作(データベースからデータを取得)

では引き続きデータ取得方法をまとめていきます。

まずは前回作成したデータベースをおさらいしておきます。
今回も前回と同様にこちらのデータベースを用いていきます。
f:id:Elsammit:20210529115441p:plain

データベースからデータを取得するコードはこちら。

#include <sqlite3.h>
#include <stdio.h>
#include <cstring>
#include <iostream>
#include <vector>

typedef struct{
    int id;
    std::string name;
    std::string category;
    int cost;
    int size;
    int weight;
} dbcolumn;

int callBackFunc(void* param, int col_cnt, char** row_txt, char** col_name){
    dbcolumn m_dbcolumn;
    m_dbcolumn.id = atoi(row_txt[0]);
    char* name = row_txt[1];
    char* category = row_txt[2];
    m_dbcolumn.name = std::string((char*)name);
    m_dbcolumn.category = std::string((char*)category);
    m_dbcolumn.cost = atoi(row_txt[3]);
    m_dbcolumn.size = atoi(row_txt[4]);
    m_dbcolumn.weight = atoi(row_txt[5]);
    
    ((std::vector<dbcolumn>*)param)->push_back(m_dbcolumn);
    
    return 0;
}

int main(void){
    sqlite3 *db = NULL;
    char* err = NULL;
    int count = 0;
    std::vector<dbcolumn> m_dbList;

    int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
    }
    char sql2[30] = "select * from huga;";
    ret = sqlite3_exec(db, sql2, callBackFunc, (void*)(&m_dbList), &err);
    if(ret != SQLITE_OK){
        printf("DB Read Error \n");
        return -1;
    }

    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

例のごとくsql文の実行にはsqlite3_exec関数を用いています。
データ取得にあたり、データを格納する変数はdbcolumn構造体に格納しております。
データを格納する際のコールバック関数はこちらになっており、
各レコード毎に構造体変数に値を格納しています。

int callBackFunc(void* param, int col_cnt, char** row_txt, char** col_name){
    dbcolumn m_dbcolumn;
    m_dbcolumn.id = atoi(row_txt[0]);
    char* name = row_txt[1];
    char* category = row_txt[2];
    m_dbcolumn.name = std::string((char*)name);
    m_dbcolumn.category = std::string((char*)category);
    m_dbcolumn.cost = atoi(row_txt[3]);
    m_dbcolumn.size = atoi(row_txt[4]);
    m_dbcolumn.weight = atoi(row_txt[5]);
    
    ((std::vector<dbcolumn>*)param)->push_back(m_dbcolumn);
    
    return 0;
}

sqlite3_exec関数のコールバックですが、各行の読み込み毎にコールされます。
すなわち、select * fromでデータを取得した場合、
1行目読み込んだらコールバック関数コール、
2行目読み込んだらコールバック関数コール、
・・・
といったような制御になっております。

コールバック関数に与えている引数ですがそれぞれ、
param:コールバックコールに渡す引数(nullでも可)
col_cnt:行数.
row_txt:行毎のデータ.
col_name:列名.
となります。

こちらを実行するとデータの読み取りも行えるかと思います。

■prepared_statementを使用してデータを取得する

上記のようにsqlite3_exec関数を用いればデータの取得は可能です。
が、、、
繰り返しsql文を実行する場合にはprepared_statementを用いた方がパフォーマンスが向上するようです。
どうやら、sqlite3_execは毎回内部的にprepared_statementを生成・実行しているようで繰り返し処理の場合には無駄な処理が入ってしまいパフォーマンスが落ちてしまうよう。

ということで、prepared_statementを使用した場合についてもまとめておきます。
コードはこちら。

#include <sqlite3.h>
#include <stdio.h>
#include <cstring>
#include <iostream>
#include <vector>

typedef struct{
    int id;
    std::string name;
    std::string category;
    int cost;
    int size;
    int weight;
} dbcolumn;

int main(void){
    sqlite3 *db = NULL;
    char* err = NULL;
    int count = 0;
    sqlite3_stmt *stmt = NULL;
    std::vector<dbcolumn> m_dbList;

    int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
    }
    char sql2[30] = "select * from huga;";
    sqlite3_prepare_v2(db, sql2, strlen(sql2), &stmt, NULL);

    sqlite3_reset(stmt);

    int r = 0;
    dbcolumn m_dbcolumn;
    while (SQLITE_ROW == (r=sqlite3_step(stmt))){

        // 数値は列数に対応し、各行に対するどの列を読み出すか指定が可能
        m_dbcolumn.id = sqlite3_column_int(stmt, 0);                    // sqlite3_column_intを使用すればint型で定義したデータが読み込める.
        const unsigned char* name = sqlite3_column_text(stmt, 1);       // sqlite3_column_textを使用すればtext型で定義したデータが読み込める
        const unsigned char* category = sqlite3_column_text(stmt, 2);
        m_dbcolumn.name = std::string((char*)name);
        m_dbcolumn.category = std::string((char*)category);
        m_dbcolumn.cost = sqlite3_column_int(stmt, 3);
        m_dbcolumn.size = sqlite3_column_int(stmt, 4);
        m_dbcolumn.weight = sqlite3_column_int(stmt, 5);

        m_dbList.push_back(m_dbcolumn);
    }
    if (SQLITE_DONE!=r){
        printf("Read Error \n");
        return -1;
    }
 sqlite3_finalize(stmt );
    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

こちらのsqlite3_prepare_v2関数を用いることでprepared_statementを生成しクエリの実行が出来ます。

sqlite3_prepare_v2(db, sql2, strlen(sql2), &stmt, NULL);

sqlite3_prepare関数もあるのですが、レガシーな関数であり現在は使用することをお勧めされていないようです。
http://www.sqlite.org/c3ref/prepare.html

prepared_statementは1回実行するたびに初期化する必要があります。
sqlite3_reset()関数を用いることでprepared_statementを実行可能な状態にリセットさせることが出来ます。

取得したデータを閲覧している関数がこちらです。

    while (SQLITE_ROW == (r=sqlite3_step(stmt))){
        // 数値は列数に対応し、各行に対するどの列を読み出すか指定が可能
        m_dbcolumn.id = sqlite3_column_int(stmt, 0);                    // sqlite3_column_intを使用すればint型で定義したデータが読み込める.
        const unsigned char* name = sqlite3_column_text(stmt, 1);       // sqlite3_column_textを使用すればtext型で定義したデータが読み込める
        const unsigned char* category = sqlite3_column_text(stmt, 2);
        m_dbcolumn.name = std::string((char*)name);
        m_dbcolumn.category = std::string((char*)category);
        m_dbcolumn.cost = sqlite3_column_int(stmt, 3);
        m_dbcolumn.size = sqlite3_column_int(stmt, 4);
        m_dbcolumn.weight = sqlite3_column_int(stmt, 5);

        m_dbList.push_back(m_dbcolumn);
    }

sqlite_step関数でSQL文の結果から1行ずつ読み取ります。
読み取ったデータですが、
int型のデータの場合には、sqlite3_column_int関数
text型のデータの場合には、sqlite3_column_text関数
を用いるようです。

またsqlite3_column_int関数、sqlite3_column_text関数の第2引数には取得したい列番号を指定すればよいです。

最後にsqlite3_finalize関数を用いて生成したprepared_statementを削除して完了です。

こちらのコードを実行しても同様にデータ取得できるかと思います。
prepared_statementを利用した場合、sqlite3_exec関数と比較してどの程度パフォーマンス向上するのかは分かりませんでした。。。
もう少し調べてみたいと思います!!

■最後に

今回はデータベースからのデータ読み出しですが、
sqlite3_exec関数を用いたケースとprepared_statementを利用したケースの2つを載せました。
一般的にsqlite3_exec関数はCREATE_TABLEなど初回のみ実行のケース。
select * fromなど複数回実行するケースはprepared_statementを用いるようです。

prepared_statementは初期化やリセット・解放する変数が1つ増えるため注意が必要なのが困りものですね。。。



sqlite3でDBを操作してみる(C++編)

以前sqlについて簡単にまとめました。
今までnodejsやpython、go言語などでsqlを用いてDBの操作したことがあったのですが、
CやC++sqlを利用したことがなかったので今回使ってみました!!
elsammit-beginnerblg.hatenablog.com

ちょっとばかしクセのある使い方だったので備忘録として残しておこうと思います。
f:id:Elsammit:20210529125301j:plain



■環境構築

今回用いる環境ですが"Ubuntu 20.04"を用いました。

C言語C++でsqlite3 を用いる場合には、

sqlite3.h

が必要になるのですが、標準ではこららのライブラリが未インストールになります。
そこでこちらのコマンドを実行してライブラリのインストールを行います。

$ sudo apt-get install sqlite3
$ sudo apt-get install libsqlite3-dev

これで環境構築は完了です。

また今回はデータベースとして、
hoge.db
を用意し、こちらにテーブルの作成やテーブル内にデータを挿入したりしたいと思います。
データベースですが、

sqlite3 hoge.db

とコマンド実行頂ければ生成可能です。

■sqlite3操作(データベースを開く)

では実装していきます。
まずは先ほど作成したデータベースをオープンしてみたいと思います。
コードはこちら。

#include <sqlite3.h>
#include <stdio.h>

int main(void){
    sqlite3 *db = NULL;
    int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
        return -1;
    }
    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

まずsqlite3型のポインタを生成します。
この変数ですが、データベースハンドルと呼ばれ、以降データベースを操作する際の核となる変数です。
こちらのハンドルを生成しデータベースと紐づけるがsqlite3_open関数になります。
sqlite3_open関数は
 ・第1引数:データベースファイルパス
 ・第2引数:データベースハンドル
になります。

オープンしたデータベースはクローズすることが必要なので、

sqlite3_close(db);

を最後に実行しています。

ではこちらを実行していきたいと思います。
ビルドを実行するにあたり、libsqlite3.soのリンクが必要になります。
例えばこちらのようにビルドを実行すればOKです。

$ g++ -o execute sql_test.cpp -lsqlite3

無事ビルドが完了したら生成物を実行してみてください。
finishという文字列がコンソール上に表示されればOKです。

■sqlite3操作(テーブルを作成する)

次にテーブルを作成していきます。
今回作成するテーブルはこちらとしました(理由はないです)。
f:id:Elsammit:20210529115441p:plain

こちらのテーブルを生成するコードですがこちらになります。

#include <sqlite3.h>
#include <stdio.h>

int main(void){
    sqlite3 *db = NULL;
    char* err = NULL;
    
int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
        return -1;
    }

    ret = sqlite3_exec(db, "create table huga(id INTEGER, name TEXT, category TEXT, cost INTEGER, size INTEGER, weight INTEGER);", NULL, NULL, &err);
    if(ret != SQLITE_OK){
        printf("Execution Err \n");
        sqlite3_close(db);
        sqlite3_free(err);
        return -1;
    }

    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

テーブル作成にあたりsqlite3_exec関数を用いました。
sqlite3_exec関数ですが、引数として与えられたデータベースハンドルに対して、引数として与えているsql文が実行できる関数になります。

sqlite3_exec関数の引数ですが、こちらの通りになっております。

sqlite3_exec(データベースハンドル, sql文, コールバック関数, コールバック関数に渡す引数, エラーメッセージ);

今回コールバック関数を使用する必要がないので、NULLにしております。

こちらを実行した後に下記を実行すると、
データベース内にテーブルが作成されていることが確認できるかと思います。

$sqlite3 hoge.db
sqlite> .table

■sqlite3操作(データを挿入)

ではでは続いて作成したテーブルにデータを挿入してみます。
と言ってもこちらは先ほどのテーブル作成と同様にsqlite3_exec関数を用いればOKです。

#include <sqlite3.h>
#include <stdio.h>

int main(void){
    sqlite3 *db = NULL;
    char* err = NULL;
    
    int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
        return -1;
    }

    char insertMsg[100] ="insert into huga values(1, 'AAA', 'aaa', 10, 100, 111);" ;
    ret = sqlite3_exec(db, insertMsg, NULL, NULL, &err);

    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

■sqlite3操作(テーブル有無チェック)

ここで、データ挿入の前にテーブルの有無をチェックしたい場合があるかと思います。
その場合にはこちらのようなコードにすればOKです。

#include <sqlite3.h>
#include <stdio.h>
#include <iostream>

int CallBack_CheckTable(void* param, int col_cnt, char** row_txt, char** col_name){
    *(int*)param = atoi(row_txt[0]);
    return 0;
}

int main(void){
    sqlite3 *db = NULL;
    char* err = NULL;
    int count = 0;
    
    int ret = sqlite3_open("./hoge.db", &db);
    if(ret != SQLITE_OK){
        printf("FILE Open Error \n");
        return -1;
    }

    char sql[100] = "select count(*) from sqlite_master  where type='table' and name='huga';";
    ret = sqlite3_exec(db, sql, CallBack_CheckTable, (void*)(&count), &err);

    if(count <= 0){
        ret = sqlite3_exec(db, "create table huga(id INTEGER, name TEXT, category TEXT, cost INTEGER, size INTEGER, weight INTEGER);", NULL, NULL, &err);
        if(ret != SQLITE_OK){
            printf("Execution Err \n");
            sqlite3_close(db);
            sqlite3_free(err);
            return -1;
        }
    }

    char insertMsg[100] ="insert into huga values(1, 'AAA', 'aaa', 10, 100, 111);" ;
    ret = sqlite3_exec(db, insertMsg, NULL, NULL, &err);

    sqlite3_close(db);
    printf("Finish \n");
    return 0;
}

急に長くなってしまいましたね。。。
今までsqlite3_exec関数のコールバック関数を利用していなかったのですが、今回はsql文により得られた結果を用いて判定が必要だったので利用しました。

まずsql文のおさらいですが、

select count(*) from sqlite_master  where type='table' and name='huga';

を実行するとnameで与えたテーブル名の個数がカウントされます。
こちらを用いれば、テーブルがなければ0が返ってきますし、テーブルがあれば1が返ってくる形になります。

こちらのsql文をsqlite3_exec関数で実行します。
そして実行した結果テーブル有無をチェックするためにコールバック関数CallBack_CheckTableを用意しました。
テーブル有無の結果を格納するためにコールバック側で利用する変数としてcountをセットしました。

  int count = 0;
 char sql[100] = "select count(*) from sqlite_master  where type='table' and name='huga';";
 ret = sqlite3_exec(db, sql, CallBack_CheckTable, (void*)(&count), &err);

コールバック関数CallBack_CheckTableはこちらになります。

int CallBack_CheckTable(void* param, int col_cnt, char** row_txt, char** col_name){
    *(int*)param = atoi(row_txt[0]);
    return 0;
}

コールバック関数の引数ですがあらかじめ決められており、

int コールバック関数名(void* sqlite3_execの第4引数, int 列数, char** 行内容, char** 列名);

になります。
今回1行・1列目にテーブル有無数が格納されております。
このため、テーブルが存在すれば引数として与えていたparamに1を、なければ0を入れております。

ここまでがテーブル有無チェックでした。
そしてここからテーブルがなければテーブルの作成を行っていくのですが、
こちらは下記の通り、結果を見て未作成であれば先ほどのテーブル作成を実行すればOKです。

    if(count <= 0){
        ret = sqlite3_exec(db, "create table huga(id INTEGER, name TEXT, category TEXT, cost INTEGER, size INTEGER, weight INTEGER);", NULL, NULL, &err);
        if(ret != SQLITE_OK){
            printf("Execution Err \n");
            sqlite3_close(db);
            sqlite3_free(err);
            return -1;
        }
    }

■最後に

今回はC++でのsql文を用いたDB操作をまとめてみました。
本当はselect * from文の利用方法についてもまとめたいと思ったのですが、、、
ここまで長くなってしまったのでまた後日まとめたいと思います!!



xfreeでPHP・MySQLサーバー導入手順 & VScodeでftpサーバー同期方法

Webアプリや作品を公開するのにサーバが欲しいな!!と思っていたのですが、
遊びでサーバをわざわざ契約するのもな。。。と悩んでおりました。。。

そんな時、TwitterでWeb作成やWeb開発されている方が「xfree」をお勧めしており、
どんなものか??
と調べてみたところ制限はありますが無料で使用できるとのこと!!
自分が欲しいレベルではあったので早速使用してみることにしました。



■xfreeとは?

エックスサーバーが提供する無料のサーバ環境です。
https://www.xfree.ne.jp/

機能は3つ用意されておりそれぞれ、
 ・HTMLサーバー機能
 ・PHPMySQLサーバー機能
 ・WordPressサーバー機能
となります。

エックスサーバーが運営していることもあり、安定性が高く無料レンタルサーバーの中では早めの部類にあたるようです。
一方で、、、
サポートっ非対応、SSL非対応、容量が1GB~2GBとかなり少ない
といったデメリットもあります。

まぁ、お試しに使う分には十分なのですが本格的に運用するならエックスサーバーの本家を利用してね!ってことですね!!
自分はお試しで使う分で十分なのでまずはxfreeを登録。

動的なWebブラウザ・アプリを作ってみたいのと、PHPも使ってみたかったので、
PHPMySQLサーバー機能
を選択しました。

■xfreeでのPHPMySQLサーバー機能を利用するまでの手順

まずは機能を利用するまでのアカウント作成や登録の手順です。
といっても、、、
無料レンタルサーバー新規お申し込み
を選択すると確認用メールアドレスを記載する欄が出るので
メールアドレス入力⇒送信
を行うと、アカウント登録用のURLが送られてくるので必要事項を入力しアカウントを登録。

その後、登録したアカウントでログインすると、ログイン画面で先ほどの3つの機能が選択できるので、
利用したい機能に対して「利用を開始する」を選択
これで利用できる状態になるかと思います。

アカウント登録は一般的な流れですし、ログイン後の手順もワンクリックなので細かい記載は割愛しますが、
詳細はこちらにまとまっておりますのでご参考に。
https://www.xfree.ne.jp/manual/man_order_user.php

PHPMySQLサーバー機能を使ってみる

では次にPHPMySQLサーバー機能の使い方を簡単にまとめておきます。

まずドメイン名ですが、
サーバー利用開始時時点ですでにドメイン名が1つ登録されております。
登録済みドメイン名ですが、
ドメイン設定」
で確認可能です。

次にxfreeサーバーとのファイルダウンロードやアップロードですが、FTPでのやり取りが用意されております。
FTPアカウントですが、
FTPアカウント設定」
を押下いただくとアカウント名が表示されます。

こちらのアカウント名を利用して、
http://アカウント名
Webブラウザにアクセスしてみてください。
こちらのような画面が表示されるかと思います。
f:id:Elsammit:20210525221919j:plain

こちらのファイル群ですが、先ほどのFTPアカウント設定から、
WebFTP欄のログインから入ることが出来ます。
ログインするとファイルマネージャー画面が立ち上がるかと思います。

こちらでファイル編集やローカルで編集したファイルのアップロードが行えるのですが、、、
正直作業しづらい。。
ということで普段コードを書くのに利用しているvscodeを用いてftpサーバーとの同期を試みることにしました。

VSCodeftpサーバーとの同期手順

ftpサーバーとの同期を行うにあたり、拡張機能である
SFTP
を用います。

拡張機能タブを選択の上、検索欄に"sftp"を入力すると
SFTPがトップに出るかと思いますのでこちらをインストールします。
f:id:Elsammit:20210525222950p:plain

次に同期を行うftpサーバの情報を登録します。
同期をとりたいフォルダをvscode上で開いた状態で、
表示タブ⇒コマンド パレットを選択 or Ctrl + Shift + pを押下
すると入力windowが表示されるので、
sftp:config
を入力⇒Enterキー押下します。

すると該当フォルダに
sftp.json
ファイルが生成されているかと思います。
こちらに必要事項を記載ください。

【name】
識別名になります。

【hostname】
先ほどのxfree管理パネルにてドメイン設定を選択すると記載されている"ドメイン名"を記載下さい。

【protocol】
"ftp"と記載。

【username】
先ほどのxfree管理パネルにて、FTPアカウント設定を選択すると表示される
"アカウント名"
を記載ください。

【password】
FTPのパスワードですが、先ほどのxfree管理パネルにて
FTPアカウント設定⇒編集を選択すると、
パスワード入力欄が出るので登録すると設定したパスワードに変更されます。
そして先ほど登録したパスワードをsftp.jsonに書き込めばOKです。

これで設定が完了です。

ここで、VSCode上でフォルダを選択せずにコマンド パレット上で、
sftp:config
を入力すると下記エラー画面(と言いますかフォルダを選択してくださいメッセージ)が表示されますので、
同期させたいフォルダをVSCode上で開いておいてください。

SFTP expects to work at a folder.

これで同期を行うための設定が完了しました。
では試しにxfreeサーバー上からファイル一式をダウンロードしてみます。
先ほどのコマンド パレット上で、

sync Remote -> Local

と入力もしくは選択を行います。
すると、先ほどのnameで設定した名前が選択項目に登場するので選択。

するとダウンロードが始まり、しばらくするとファイル一式がローカルにダウンロードされるかと思います。
逆にファイルをアップロードしたい場合には、

sync Local -> Remote

と入力もしくは選択を行えばOKです。

■最後に

今回はxfreeの初期設定やCSCodeでftpサーバー同期方法についてまとめてみました。
これからxfreeも使い倒していきたいと思います!!
後、PHPも初なので楽しんでいきたい!!



Yoloによる物体検知と動画再生Webアプリを組み合わせてみる

前回、Yoloを用いて画像に対する物体検知を行いました。
elsammit-beginnerblg.hatenablog.com

今回はこちらのYoloによる物体検知と以前作成したWebアプリを組み合わせ、動画再生中に物体検知するWebアプリを作成していきたいと思います。



■条件

すでに動画再生アプリが作成されておりかつ、以前ご紹介したyolov3-tf2がインストール済みであることが条件です。

未実装、未インストールの場合にはこちらをご参考に作成、インストールしてください。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com

■環境

 ・OS:Windows 10
 ・ディストリビューション:anaconda

■試しにyolov3-tf2で動画再生時の物体検知を行ってみる

まずはyolov3-tf2単体で動画再生を行い、実力を確認してみたいと思います。
今回用いる動画はこちらになります。
f:id:Elsammit:20210519220641g:plain

yolov3-tf2での動画再生しながら物体検出の方法(コマンド)はこちらになります。
【yolov3】

python detect_video.py --video path_to_file.mp4

【yolov3-tiny】

python detect_video.py --video path_to_file.mp4 --weights ./checkpoints/yolov3-tiny.tf --tiny

ではそれぞれこちらのコマンドを用いて動画再生してみます。
まずはyolov3から試してみます。
【yolov3】
f:id:Elsammit:20210519221323g:plain

左上に各フレーム毎の処理時間が記載されているのですが、、、
概ね300ms
ちょっと遅いですね。
元の映像と比較してもカクカクです。。。
一方で検出性能は抜群!!ほぼ全ての人に対して検出が行えております。

続いてyolov3-tiny。
【yolov3-tiny】
f:id:Elsammit:20210519221804g:plain

こちらは打って変わって検出精度はまばらですね。。。
一方で各フレームでの処理時間は60msh程度とかなり処理速度が早いです。

カクカク動くのが嫌だったので、Webアプリにはyolov3-tinyを使用することにしました。
以降はyolov3-tinyを用いた動画再生アプリを作成していきます。

■Webアプリ作成(フォルダ構成)

ではWebアプリ作成に移ります。
まずはフォルダ構成です。
こちらの通りになっております。
赤字が前回と比較して新規追加ファイルになります。
f:id:Elsammit:20210519223251p:plain

追加しているのはYolov3での学習済みデータになります。
先ほど使用したyolov3-tf2フォルダ内のdata、checkpointsをコピーして持ってきます。

■Webアプリ作成(コード修正)

では実際にコード修正を進めていきます。
前回作成したファイルから修正するファイルはcamera.py、base_camera.pyになります。
まずはcamera.pyから。
コードはこちら。

import time
from base_camera import BaseCamera
import cv2
import time
import cv2
import tensorflow as tf
from yolov3_tf2.models import (
    YoloV3, YoloV3Tiny
)
from yolov3_tf2.dataset import transform_images
from yolov3_tf2.utils import draw_outputs

class Camera(BaseCamera):
    counter = 0
    speed = 1
    stop = True
    rewindFlg = False
    progress = 0
    MoviePath = '動画ファイルパス'
    cap = cv2.VideoCapture(MoviePath)

    if (cap.isOpened()== False):
        print("File Open Error")
    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

    @staticmethod
    def frames():
        physical_devices = tf.config.experimental.list_physical_devices('GPU')
        for physical_device in physical_devices:
            tf.config.experimental.set_memory_growth(physical_device, True)

        yolo = YoloV3Tiny(classes=80)
        yolo.load_weights("./checkpoints/yolov3-tiny.tf")
        print('weights loaded')
        class_names = [c.strip() for c in open('./data/coco.names').readlines()]
        print('classes loaded')

        times = []

        while True:
            if Camera.stop == True:
                continue

            ret, frame2 = Camera.cap.read()
            
            if ret == True:
                Camera.counter = Camera.cap.get(cv2.CAP_PROP_POS_FRAMES)
                Camera.progress = int(Camera.counter / Camera.frame_count * 100)

                if Camera.speed != 0:
                    SpdNum = Camera.speed*30
                        
                    if Camera.rewindFlg==True:
                        Camera.counter-=SpdNum
                        if Camera.counter < 0:
                            Camera.counter = 0
                        Camera.cap.set(cv2.CAP_PROP_POS_FRAMES, Camera.counter)
                    else:
                        Camera.counter+=SpdNum
                        Camera.cap.set(cv2.CAP_PROP_POS_FRAMES, Camera.counter)
                else:
                    time.sleep(1/30)
            
                img = cv2.resize(frame2,(640,480))
                img_in = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                img_in = tf.expand_dims(img_in, 0)
                img_in = transform_images(img_in, 416)

                t1 = time.time()
                boxes, scores, classes, nums = yolo.predict(img_in)
                t2 = time.time()
                times.append(t2-t1)
                times = times[-20:]
                img = draw_outputs(img, (boxes, scores, classes, nums), class_names)
                img = cv2.putText(img, "Time: {:.2f}ms".format(sum(times)/len(times)*1000), (0, 30),
                          cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0, 0, 255), 2)

                yield cv2.imencode('.png', img)[1].tobytes()
            else:
                Camera.counter=0
                Camera.cap.set(cv2.CAP_PROP_POS_FRAMES, Camera.counter)

基本的にはyolov3-tf2フォルダ内のdetect_video.pyの処理を流用しています。
下記が物体検出のコードとなります。

img = cv2.resize(frame2,(640,480))
img_in = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_in = tf.expand_dims(img_in, 0)
img_in = transform_images(img_in, 416)

t1 = time.time()
boxes, scores, classes, nums = yolo.predict(img_in)
t2 = time.time()
times.append(t2-t1)
times = times[-20:]
img = draw_outputs(img, (boxes, scores, classes, nums), class_names)
img = cv2.putText(img, "Time: {:.2f}ms".format(sum(times)/len(times)*1000), (0, 30),
          cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0, 0, 255), 2)

こちらのyolo.predictを行うために学習済みデータが必要になります。
学習済みデータを読み出すために、

physical_devices = tf.config.experimental.list_physical_devices('GPU')
for physical_device in physical_devices:
      tf.config.experimental.set_memory_growth(physical_device, True)

yolo = YoloV3Tiny(classes=80)
yolo.load_weights("./checkpoints/yolov3-tiny.tf")

class_names = [c.strip() for c in open('./data/coco.names').readlines()]

を追加いたしました。

次にbase_camera.py。
といってもこちらはタイムアウト処理が入っているのですが、
今回追記した処理の初期設定に時間を要している間にタイムアウトが発生してしまい動かなくなってしまいます。
そこで、タイムアウト処理を伸ばすことにしました。
コードはこちら。

if time.time() - BaseCamera.last_access > 100:
    frames_iterator.close()
    print('Stopping camera thread due to inactivity.')
    break

これで物体検知付き動画再生アプリができました。

■Webアプリを動作させてみる

では、こちらのアプリを動作させてみたいと思います。
動作させた結果はこちら。
f:id:Elsammit:20210519225255g:plain

うん!!うまく動画再生Webアプリとして動作させることが出来ました!!
検出時間は大体60msなので、検出速度も上々。
Webアプリとして動かすことが出来て満足。

■最後に

今回は動画再生Webアプリを作成しました。
cv2.VideoCapture(MoviePath)のMoviePathをカメラ番号に置き換えれば、カメラからの映像を通して物体検知が行えます。
良かったら試してみてください。
私も試してみましたが、処理速度は十分で検知精度はそこそこ、といった感じでしたw

一応コードはこちらに格納していますので、よろしかったら覗いてみてください。
https://github.com/Elsammit/Flask-VideoApplication



Raspberry PiにYOLOをインストールし物体検知してみる

今回はラズパイでYoloを使用して物体検知をしてみたいと思います!!

結構環境構築に手こずったところがあるので、こちらも備忘録として残して置ければと思います。



■環境構築手順

今回下記tensorflow2-yolo-v3を利用して物体検出を実施いたします。
https://github.com/zzh8829/yolov3-tf2.githttps://github.com/cedrickchee/tensorflow2-yolo-v3

こちらを利用するにあたり、tensorflow2.1以上が必要になります。
そこでまずはtensorflow2.1をインストールしていきたいと思います。

念のため必要なファイルをインストール。

sudo apt-get install python3-protobuf python3-termcolor python3-yaml python3-pydot python3-pyasn1 python3-pyasn1-modules python3-rsa python3-markdown python3-cachetools python3-future python3-dill python3-tqdm python3-pil python3-pip python3-wheel python3-setuptools python3-matplotlib python3-h5py python3-scipy python3-grpcio python3-requests-oauthlib python3-werkzeug

そして実際にtensorflow2.1以上をインストールしてみます!!
下記の通りpipを用いてインストール出来ればよかったのですが、、、
上手くインストールできず苦戦しました。。。
まずこちらのコマンドを実行してみたところ、

pip3 install tensorflow==2.3.0

こちらのようなエラーになり、

ERROR: Could not find a version that satisfies the requirement tensorflow==2.3.0
ERROR: No matching distribution found for tensorflow==2.3.0

ということで自分でtensorflowを主導でダウンロード・インストールすることにしました。
手順はこちら。

wget "https://raw.githubusercontent.com/PINTO0309/Tensorflow-bin/master/tensorflow-2.3.0-cp37-none-linux_armv7l_download.sh"
sudo chmod +x tensorflow-2.3.0-cp37-none-linux_armv7l_download.sh
./tensorflow-2.3.0-cp37-none-linux_armv7l_download.sh
sudo pip3 uninstall tensorflow
export PIP_DEFAULT_TIMEOUT=1000
pip3 install tensorflow-2.3.0-cp37-none-linux_armv7l.whl

ここで、tensorflowのインストールに時間がかかってしまいデフォルト設定のままですとタイムアウトエラーが発生してしまいました。
そこで、

export PIP_DEFAULT_TIMEOUT=1000

をインストール前に実行しております。

もし上記でインストール出来ない場合、下記のように管理者権限でインストールを試してみてください。

sudo -H pip3 install tensorflow-2.3.0-cp37-none-linux_armv7l.whl

【補足】
もしかしたらこちらでもインストール出来るかもしれません。

python3 -m pip install tensorflow-hub tensorflow-datasets https://github.com/lhelontra/tensorflow-on-arm/releases/download/v2.3.0/tensorflow-2.3.0-cp37-none-linux_aarch64.whl

最後に、yolov3-tf2opencvも用いるので、

pip3 install opencv-python

を実行。
これで環境構築完了です。

■tensorflow2-yolo-v3を利用して物体検知してみる

環境構築も完了したので実際に物体検知していきたいと思います!!

まずは学習データをダウンロード。

wget https://pjreddie.com/media/files/yolov3.weights -O data/yolov3.weights
wget https://pjreddie.com/media/files/yolov3-tiny.weights -O data/yolov3-tiny.weights

yolov3とyolov3-tinyという簡易版の両方を使用したかったので両方ダウンロードしてみました。
実際には片方のみで問題ないです。

ではこちらの学習データを適用してみたいと思います。
コマンドはこちら。
【yolov3】

python3 convert.py --weights ./data/yolov3.weights --output ./checkpoints/yolov3.tf

【yolov3-tiny】

python3 convert.py --weights ./data/yolov3-tiny.weights --output ./checkpoints/yolov3-tiny.tf --tiny

少し時間がかかるので、しばし待つ。
下記のようなメッセージが出力されればOKです。

I0516 10:10:52.379993 3069450960 convert.py:28] weights saved

ここまで長った。。。
では、実際に物体検知してみます!!
コマンドはこちら。
【yolov3】

python3 detect.py --image ./data/meme.jpg

【yolov3-tiny】

python3 detect.py --weights ./checkpoints/yolov3-tiny.tf --tiny --image  ./data/meme.jpg

もしかしたらスワップ領域を拡張しないと動かない可能性があります。
スワップ領域を拡張するために、/etc/dphys-swapfileファイルを下記の通り変更してみてください。

CONF_SWAPSIZE=1024

■実行結果をまとめてみる

では実行結果についてまとめていきたいと思います。
まずは各動作での物体検知精度です。
各検出結果の画像はこちらの通り。
【yolov3】
f:id:Elsammit:20210516103153j:plain

【yolov3-tiny】
f:id:Elsammit:20210516103130j:plain

yolov3-tinyの方が簡易版だけあって少し精度が悪いですね。
ただ、どちらもある程度の物体検出は行えておりますね。

次に実行時間。
【yolov3】
 21.9秒

【yolov3-tiny】
 4.2秒

圧倒的にyolov3-tinyの方が早いですね。
ただどちらもリアルタイム性はあまりなさそうですね。。。

■最後に

今回はYOLOで物体検知を試してみました。
今度は動画で試してみようかな??
動画になるとある程度速度が求められるので、yolov3-tinyで実施ですね。
yolov3の21秒は、動画にしてはちょっと、、なので。



Flaskで静的ファイル(javascript、css)のファイル更新で反映されない

先日作成した動画再生アプリ作成しました。
elsammit-beginnerblg.hatenablog.com

この際Flaskを用いたのですが、javascriptcssを変更しても表示上変更されずに苦労したので、対策を備忘録として残しておこうと思います。
f:id:Elsammit:20210510221324p:plain



■現象

javascriptcssを変更しても表示上変化しない。
ブラウザキャッシュが原因で、ブラウザキャッシュをクリアすれば変更後の状態が反映される。
ファイル変更後の動作確認時毎回ブラウザキャッシュしなければならなくなるのでとても面倒。

■対処方法

どうやらHTML内でcssをリンクする際に、

style.css?v=12

のように更新日時やバージョンを付け加えればキャッシュがクリアされ、
変更が反映されるようです。

しかし、、、
いちいち手動で設定するのが面倒だな。。。
と思い調べてみたところ、下記を発見!!

内容としてはファイル更新した旨を示す情報をファイルに埋め込むことで対応するようです。
具体的には下記のようなコードをFlaskのコードに埋め込めばOKのよう。

@app.context_processor
def override_url_for():
    return dict(url_for=dated_url_for)

def dated_url_for(endpoint, **values):
    if endpoint == 'static':
        filename = values.get('filename', None)
        if filename:
            file_path = os.path.join(app.root_path,
                                     endpoint, filename)
            values['q'] = int(os.stat(file_path).st_mtime)
    return url_for(endpoint, **values)

早速入れ込んで動作検証実施。
確かにこちらのコードで問題なくcssjavascriptの変更に追従して表示も変わっていたことを確認!!

無事に解決。

■最後に

今回は短いですが以上になります。
Flaskだとjavascriptcssが静的ファイルとして扱われるのでこのような仕様なのでしょうか??
少し面倒な仕様だな、と感じてしまった。。



flaskで動画再生Webアプリを作ってみる

先日OpenCVを用いた動画再生や逆再生、早送り等を実装してみました。
elsammit-beginnerblg.hatenablog.com

今回はFlaskを用いてこれら逆再生や早送りなどが実装されたWebアプリについてまとめていきたいと思います!!



■完成形

完成したアプリはこちらのようになります。
f:id:Elsammit:20210509191521g:plain

■最終コード

最終的なコードはこちらに格納いたしましたのでご参考まで。
https://github.com/Elsammit/Flask-VideoApplication

■フォルダ構成

フォルダ構成は下記になります。
f:id:Elsammit:20210509180839p:plain

■Flaskでの動画表示

ではまずはFlaskで動画再生表示について実装していきたいと思います。
こちらのコードですが、下記を参考に作成しました。
https://github.com/miguelgrinberg/flask-video-streaming
FastAPIでOpenCV Streaming - Qiita

ここで動画表示について解説しても良いのですが、、、
こちらの記事がかなり詳しく記載されており、ここで書くのは冗長な気がするので割愛します。

実施している流れとしては、
・フロントエンドから動画再生要求
・バックエンドにてOpenCVのVideoCaptureで動画データのフレームを取得
・取得したフレームをバイト列に変換の上フロントエンドに返す
・フロントエンドにて受け取ったバイト列情報から動画を再生
となります。
マルチアクセスにも対応させるためにフレームの取得・操作をスレッドで行っております。

■動画早送り・巻き戻し機能追加(バックエンド)

では先ほどの動画表示から早送り・巻き戻しなどの機能を追加していきます。
まずはバックエンドから。

変更するファイルはcamera.py、main.pyの2つです。
それぞれ、
・main.py:フロントエンドとのIF追加
・camera.py:フロントエンドからの指示に応じて早送り・巻き戻し動作実行
になります。

まずはman.pyから。
追加コードはこちらになります。

@app.route('/start', methods=["POST"])
def start_movie():
    print("start")
    Camera.rewindFlg = False
    Camera.stop = False
    Camera.speed = 0
    return Response("OK", 200)

@app.route('/pause', methods=["POST"])
def pause_movie():
    print("pause")
    Camera.stop = True
    return Response("OK", 200)

@app.route('/stop', methods=["POST"])
def stop_movie():
    print("stop")
    Camera.cap = cv2.VideoCapture(Camera.MoviePath)
    Camera.stop = False
    video_feed()

@app.route('/speed', methods=["POST"])
def changeSpeed():
    if Camera.rewindFlg == True:
        Camera.speed = 1
        Camera.rewindFlg = False
    else:
        Camera.speed = Camera.speed % 3 + 1
    Camera.stop = False
    print("speed:"+str(Camera.speed))
    return Response("OK", 200)

@app.route('/rewind', methods=["POST"])
def rewind_movie():
    if Camera.rewindFlg == False:
        Camera.speed = 1
        Camera.rewindFlg = True
    else:
        Camera.speed = Camera.speed % 3 + 1
    Camera.stop = False
    print("rewind:"+str(Camera.rewindFlg))
    return Response("OK", 200)

@app.route('/progress', methods=["POST"])
def get_progress():
    return Response(str(Camera.progress), 200)

実施していることは、各動作に応じてpost requestを受け取り、受け取った内容に応じてフラグ管理を行っているのみになります。
管理するフラグですが、
・一時停止判定用フラグ:stop
・巻き戻し判定用フラグ:rewindFlg
・早送り時の速度:speed
の3種類です。
speedは3段階で切り替えられるようにいたしました。
合わせて再生バーを表示させたかったので、逐次呼び出すようのprogressも用意いたしました。


次にcamera.pyです。
変更部はこちら。

    def frames():
        while True:
            #↓ここから
            if Camera.stop == True:
                continue

            ret, frame2 = Camera.cap.read()
            frame = cv2.resize(frame2,(WIDTH,HEIGHT))
            
            if ret == True:
                Camera.counter = Camera.cap.get(cv2.CAP_PROP_POS_FRAMES)
                Camera.progress = int(Camera.counter / Camera.frame_count * 100)

                if Camera.speed != 0:
                    SpdNum = Camera.speed*30
                        
                    if Camera.rewindFlg==True:
                        Camera.counter-=SpdNum
                        if Camera.counter < 0:
                            Camera.counter = 0
                        Camera.cap.set(cv2.CAP_PROP_POS_FRAMES, Camera.counter)
                    else:
                        Camera.counter+=SpdNum
                        Camera.cap.set(cv2.CAP_PROP_POS_FRAMES, Camera.counter)
                else:
                    time.sleep(1/30)
            else:
                Camera.counter=0
            #↑ここまで
            yield cv2.imencode('.png', frame)[1].tobytes()

実施していることは、先ほどのフラグに応じて一時停止させたり動画の速度を速めたりしております。
早送り・巻き戻しの方法についてはこちらにまとめておりますので、よろしければこちらもどうぞ。
elsammit-beginnerblg.hatenablog.com

■動画早送り・巻き戻し機能追加(フロントエンド)

次にフロントエンドです。
こちらはjavascriptcss、htmlそれぞれ変更を加えております。
今回はjavascriptの一部のみ抜粋させていただきます。

まずflaskにてhtmlからjavascriptのコードを呼び出す際にはこちらのように、
scriptタグからstatic配下のファイルパスを指定すればOKです。

<script src="{{url_for('static', filename='js/script.js')}}"></script>

そして、script.jsにはPostリクエストを行うためのAjaxを記載いたしました。
スタート、ストップ等の動作に応じてそれぞれ定義しているのですが、
各関数はほぼ同じになっております。
一例として早送り押下時のコードを下記に載せます。

$( function() {
    $('#Speed').on('click',
    function() {
        var hostUrl= '/speed';
        $.ajax({
            url: hostUrl,
            type:'POST',
            timeout:3000,
        }).done(function(data) {
                          console.log("ok");
        }).fail(function(XMLHttpRequest, textStatus, errorThrown) {
                         console.log("error");
        })
    });
} );

内容としては、Speedと定義したidに対してボタン押下された際、
hostUrlで定義したパスに対してPOSTリクエストを行うのみになります。

■最後に

今見てて思ったのですが、なんかもっと効率よく書ける気がしてきました。。
改善版を再度上げられたらな!と思います。