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

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

MENU

pythonで動画に字幕を付ける方法

現在オンライン講義Courseraを利用(聴講)して1から勉強中なのですが、、
Courseraは日本語の講義が全くない!!
一応字幕機能もあるのですが、、、
 ・動画欄の下に字幕文章欄が付いている形式なので動画を見ながら受講しにくい
 ・そもそも字幕も日本語対応しているものが少ない
といった問題が。。。

自分は英語が苦手なのでどうしたものかと悩んだ結果、
”自分で日本語字幕を動画につける!!”
こととしました。

今回はこちらの字幕付与方法についてまとめておきたいと思います。
※字幕を英語⇒日本語に変換する方法はまた後程まとめます!!



■前提

Courseraでは下記ファイル形式がダウンロードできます。
動画:.mp4
字幕:.vtt

そこで今回はmp4ファイル形式の動画にvtt形式の字幕データを付与していきたいと思います!!
さらに用いる言語はpython
理由は、英語⇒日本語に変換する際にスクレイピングを用いて変換が行いたかったためです。

後、OSはUbuntu20.04を用います。
※こちらに関しては理由はないですw

■.vtt形式って?

自分の技術力が浅く、.vtt形式のファイルがなんなのか知らなかったので調べてみました。

【vttファイルとは?】
vttはWebビデオテキストトラックファイルのこと。
Web Video Text Tracksの略でテキストデータファイルです。
字幕やキャプション、説明、章、メタデータなどのWebビデオに関する情報が含まれています。

■動画に字幕を付けてみる

vttファイルを用いてmp4に字幕を付ける際、htmlを用いれば簡単に字幕を付けることが出来るようです。
コードはこちら。

<video controls autoplay src="video.mp4">
 <track default src="track.vtt">
</video>

ただ、、こちらをローカル環境で動作させると

'file:' URLs are treated as unique security origins.

といったエラーになってしまい、字幕動画が再生できませんでした。。
ローカルセキュリティポリシー設定を変更すれば字幕再生できそうだったのですが、、、
https://dev.classmethod.jp/articles/chrome-localfile-security/

設定変えるのもな、と思ったのとそもそもブラウザ立ち上げるのが、、と思ったので
字幕付きの動画を生成させることにしました!!

字幕付き動画再生を行うにあたり、
"ffmpeg"
を用います。
ffmpegがインストールされていない方はこちらのコマンドでインストールしてみてください。

sudo apt-get install ffmpeg

ffmpegを用いた字幕動画生成を行う場合、こちらのコマンドを実行すればOKです。

ffmpeg -i 動画ファイル名 -i 字幕ファイル.vtt -map 0:v -map 0:a -map 1 -metadata:s:s:0 language=jpn -c:v copy -c:a copy -c:s srt 出力動画.mkv

pythonでコマンドを実行するためにはsubprocessを用いればよいので、

cmdline = "ffmpeg -i 動画ファイル名 -i 字幕ファイル.vtt -map 0:v -map 0:a -map 1 -metadata:s:s:0 language=jpn -c:v copy -c:a copy -c:s srt 出力動画.mkv"
res = subprocess.call(cmdline, shell=True)

とすればOKです。

こちらを実行して 、
出力動画.mkvが生成されること
生成された 出力動画.mkvを再生すると字幕が付与されていることを確認して下さい。

さらに、、、
引数で動画ファイル、字幕ファイルを指定して字幕動画を作成するためのコードはこちら。

import subprocess
import sys
import os

args = sys.argv

if args[1].endswith('.mp4') and args[2].endswith('.vtt'):
    cmdline = "ffmpeg -i " + args[1] + " -i " + args[2] + " -map 0:v -map 0:a -map 1 -metadata:s:s:0 language=jpn -c:v copy -c:a copy -c:s srt " + "output.mkv"
    subprocess.call(cmdline, shell=True)

こちらのコードを実行する際に、

python exec.py 動画ファイル.mp4 字幕ファイル.vtt

とすることで指定した動画ファイル・字幕ファイルを元に字幕動画ファイルの作成が行えます。

mkvファイルとは?

先ほどのコードを実行すると字幕動画ファイルが生成されるのですが、mkv拡張子の動画ファイルが生成されます。
このmkvファイルについて解説しておきます。

mkvファイルとは?】
動画と付随する音声や字幕などを収録することができるファイル形式の一つです。
様々な種類のメディアデータを格納する「Matroska」(マトリョーシカ)フォーマットの動画向け仕様で、標準のファイル拡張子は「.mkv」です。
テレビの録画や映画の録画に適したファイル形式のようです。
さらに、複数の言語字幕一括で保存でき、高画質な動画再生が可能という特徴も持っているファイル形式のようです。
https://e-words.jp/w/MKV%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.html

■最後に

今回は動画への字幕付与をした字幕動画を生成する方法についてまとめました。
次は英語⇒日本語に字幕を変換していきたいと思います!!



html+javascriptでxmlファイルをjsonデータへjsonデータをxmlファイルに変換

前回まででjsonファイルの読み込みやcsvファイルの読み込み、
csvjsonデータの変換を行ってきました。
elsammit-beginnerblg.hatenablog.com
elsammit-beginnerblg.hatenablog.com

今回は最後で、xmlファイルを読み込みjsonデータに変換したり、jsonデータをxmlファイルに変換させてみたいと思います。
jsonxmlの変換はこちらを用いれば簡単にできそうですが、、、
http://goessner.net/download/prj/jsonxml/

ライセンスがGPLでしたので、、あえて自分で作ることにしました!!



■前提について

今回の変換ですが、様々なパターンでの変換を考えていることもあり、
内部処理ではjsonデータにして扱うことにしました。
※理由はキーとvalueが分割して扱うことが出来るのでやりやすそうだったから。

このため、今回のコードも
csvファイルを一旦jsonデータに変換してからjsonファイルとして出力したり、
jsonファイルから読み出したjsonデータをcsvファイルとして出力したりしております。

また今回取り扱うデータについても、

{
    "type":"man",
    "name":"ニック",
    "age":20
}

といった形式とします。
また、xml形式ですが下記とします。

<?xml version="1.0" encoding="UTF-8"?>
<list> 
    <item> 
        <type>man</type> 
        <name>ニック</name> 
        <age>20</age> 
    </item> 
    <item> 
        <type>man</type> 
        <name>Bob</name> 
        <age>30</age> 
    </item> 
    <item> 
        <type>woman</type> 
        <name>Alice</name> 
        <age>12</age> 
    </item> 
</list> 

xmlファイルからデータを読み出してjsonデータに変換

ではまずxmlファイルから読み出したデータからjsonデータに変換します。
xmlファイルからのデータ読み出しですが、jsonファイルの読み出しと同じコードで実現可能なため割愛します。
コードをチェックしたい方はこちらをご確認ください。
elsammit-beginnerblg.hatenablog.com

xmlファイルからjsonデータへ変換するコードはこちら。

function xmlTojson(xmlArray){
    let parser = new DOMParser();
    let doc = parser.parseFromString(xmlArray, "application/xml");
    let nl = doc.getElementsByTagName("item");
    let matches = nl.length;

    let jsonData = [];
    for (let i = 0; i < matches; i++ ) {
        let e = nl.item(i);
        let youso = [];
        for(let j = 0;j < Math.floor(e.childNodes.length/2);j++){
            let type = e.getElementsByTagName(e.childNodes[1+j*2].nodeName);
            youso.push(type);
        }
        let buf = {type:youso[0].item(0).textContent, japan:youso[1].item(0).textContent, us:youso[2].item(0).textContent};
        jsonData.push(buf);
    }
    return jsonData;
}

渡されたxmlファイルのデータをparseします。

    let parser = new DOMParser();
    let doc = parser.parseFromString(xmlArray, "application/xml");

parseされたデータからitemタグを抽出します。

let nl = doc.getElementsByTagName("item");

そして、itemタグ配下のtype、name、ageを取得しjsonData配列に逐次追加。

    let jsonData = [];
    for (let i = 0; i < matches; i++ ) {
        let e = nl.item(i);
        let youso = [];
        for(let j = 0;j < Math.floor(e.childNodes.length/2);j++){
            let type = e.getElementsByTagName(e.childNodes[1+j*2].nodeName);
            youso.push(type);
        }
        let buf = {type:youso[0].item(0).textContent, name:youso[1].item(0).textContent, age:youso[2].item(0).textContent};
        jsonData.push(buf);
    }

youso配列にノードの要素を次々に格納していきます。
childNodesにはノードの要素名とテキストが順番に格納されているため、
1つ飛ばしで要素を取得していっております。

e.childNodes[1+j*2].nodeName

最後に要素名毎のデータを、getElementsByTagName関数で取得しています。
getElementsByTagName関数やchildNodes関数については下記をご覧ください。
https://developer.mozilla.org/ja/docs/Web/API/Node/childNodes
https://developer.mozilla.org/ja/docs/Web/API/Document/getElementsByTagName

jsonデータをxmlファイルに変換・出力

では次にjsonデータをxmlファイルに変換・出力してみます。
コードはこちら。

function WriteXmlFile(){
    let writeString = "";
    let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
    writeString += '<?xml version="1.0" encoding="UTF-8"?> \n';
    writeString += '<list> \n';
    for(let i = 0; i < huga.length; i++){
        writeString += '    <item> \n';
        writeString += '        <type>' + huga[i].type + '</type> \n';
        writeString += '        <name>' + huga[i].name+ '</name> \n';
        writeString += '        <age>' + huga[i].age+ '</age> \n';
        writeString += '    </item> \n';
    }
    writeString += '</list> \n';
    let blob = new Blob([bom, writeString],{type:"application/xml"});
    let link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = '作ったファイル.xml';
    link.click();
}

こちらは結構単純です。
jsonデータを分割して文字列として次々に追加してくのみです!!
最後に、

    let blob = new Blob([bom, writeString],{type:"application/xml"});
    let link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = '作ったファイル.xml';
    link.click();

により変換した文字列をxmlファイルに書き込みダウンロード処理を実行して完了になります。

■最後に

今回はxmljsonの変換やファイル読み出し、書き込みを行ってみました。
ファイル変換を行ってみると各データ毎のparse方法や文字列の扱い方が分かるのでとても勉強になりました。

もう少し整理したらソースコード公開してみようかな??



html+javascriptでcsvファイルをjsonデータへjsonデータをcsvファイルに変換

先日、javascritptでjsonファイルを読み出したり、書き出す処理についてまとめました。
elsammit-beginnerblg.hatenablog.com

こちらのファイル読み込み・出力を行うにあたり、
jsoncsv
jsonxml
xmlcsv
と変換できるツール的なものが作成できれば面白そうだな。
と思い作成を進めております。

今回は、
jsoncsv
のパターンである、jsonファイル⇒csvファイルに変換したりcsvファイル⇒jsonファイルに変換する方法についてまとめたいと思います。



■前提について

今回の変換ですが、様々なパターンでの変換を考えていることもあり、
内部処理ではjsonデータにして扱うことにしました。
※理由はキーとvalueが分割して扱うことが出来るのでやりやすそうだったから。

このため、今回のコードも
csvファイルを一旦jsonデータに変換してからjsonファイルとして出力したり、
jsonファイルから読み出したjsonデータをcsvファイルとして出力したりしております。

また今回取り扱うデータについても、

{
    "type":"man",
    "name":"ニック",
    "age":20
}

といった形式とします。
またcsvファイルの形式ですが下記の通り、
1行目にキー名を定義、
2行目以降をvalue
としております。
f:id:Elsammit:20210620214920p:plain

csvファイルからデータを読み出してjsonデータに変換

ではまずはcsvファイルから読み出したcsv形式データからjsonデータに変換していきます。
csvファイルの読み出しですが、jsonファイルの読み出しと同じコードを利用できるため割愛します。
※詳細をチェックしたい方はこちらをご確認ください。
elsammit-beginnerblg.hatenablog.com

コードはこちら。

function csv2json(csvArray){
    let jsonArray = [];

    let RowArray = csvArray.split('\n');
    let items = RowArray[0].split(',');
    for(let i = 1; i < RowArray.length; i++){
        let cellArray = RowArray[i].split(',');
        let line = new Object();
        for(let j = 0; j < items.length; j++){
            line[items[j]] = cellArray[j];
        }
        jsonArray.push(a_line);
    }
    return jsonArray;
}

引数としてcsvファイルから読み出したデータ群を渡します。
今回のcsvファイル(データ)の一行目はキー名として定義しているので、

    let RowArray = csvArray.split('\n');
    let items = RowArray[0].split(',');

としてitems内にキー名を配列として格納いたしました。
要するに、
items[0]:type
items[1]:name
items[2]:age
がそれぞれ格納されているわけです。

次に各valueを取り出します。
RowArray に各行ごとの文字列が格納されているので、
1行ごとにカンマ区切りで文字列を分割し、それを変数に格納すればよいです。

    for(let i = 1; i < RowArray.length; i++){
        let cellArray = RowArray[i].split(',');
        let line = new Object();
        for(let j = 0; j < items.length; j++){
            line[items[j]] = cellArray[j];
        }
        jsonArray.push(a_line);
    }

カンマ区切りのデータを分割して格納する変数はcellArray になります。
cellArrayによりカンマ区切りで分割されたデータをキーに合わせて格納しているのがlineになります。
lineに格納する際にjsonデータの形に加工しているわけです。
最後にjsonArrayという、jsonデータ配列にプッシュしていきjsonデータ群を返しております。

長々書いてしまいましたが、やっていることは至極シンプルで
csv形式のデータを行分割
・行分割したデータをカンマ区切りで分割
・分割したデータをjsonデータに格納
・返却用の変数にjsonデータをpush
を行っているのみです。

jsonデータをcsvファイルに変換・出力

では次にjsonデータをcsvファイルとして出力してみます。
jsonデータをcsv形式に変換するコードはこちら。

function json2csv(json) {
    let ret= Object.keys(json[1]).join(',') + "\n";

    ret += json.map(function(d){
        return Object.keys(d).map(function(key) {
            return d[key];
        }).join(',');
    }).join('\n');

    return ret;
}

まずcsvファイルの1行目はキー名となるので、

 let ret= Object.keys(json[1]).join(',') + "\n";

といったようにキー名をカンマ区切りでret変数に文字列として格納します。

そしてvalueは、

    ret += json.map(function(d){
        return Object.keys(d).map(function(key) {
            return d[key];
        }).join(',');
    }).join('\n');

というように各キー名にあったvalueを取り出してカンマ区切りの文字列に変換⇒ret変数に格納を繰り返しております。

csv形式のデータをcsvファイルに変換するコードですが、
こちらも先日のjsonファイル出力時と同様のコードになるため、詳細は省きますが、、、
コードとしてこちらのようになります。

function WriteToCSV(){
    let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
    let hugastring = json2csv(huga);
    let blob = new Blob([bom, hugastring],{type:"text/plan"});
    let link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = '作ったファイル.csv';
    link.click();
}

■最後に

今回はcsvファイルをjsonデータに変換したり、jsonデータをcsv形式のデータに変換しファイル出力するまでの流れを記載しました。
次回はjsonデータ⇔xmlデータの変換についてまとめようかな??
ツールについても引き続き作成頑張っていきます!!



html+javascriptでフロントエンドにてjsonファイルの読み込み・書き出し

前回まで動画や画像処理について備忘録まとめてきましたが、
今回は打って変わってjavascriptでのjsonデータやjsonファイルの取り扱いについてまとめていきたいと思います。

ちょうどjsonファイルを取り扱ってデータ管理する必要があったのですが、
アプリを作成すると配布するのが面倒だったのであえてhtml+javascriptでやってみることにしました!!



■使用するjsonデータ形式

今回使用するjsonデータはこちらとします。
※こちらのjsonデータは必要に応じて置き換えてみてください。

{
    "type":"man",
    "name":"ニック",
    "age":20
}

jsonファイルを読み込む

それでは早速jsonファイルを読み込む処理を作成していきます。
htmlのコードはこちらのようになります。

<!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
            <meta  content="text/html; charset=UTF-8">
            <script type="text/javascript" src="jsonRead.js"></script>
        </head>
        <body>
            <input id="file1" type="file" onChange="fileChanged(this)" multiple>
        </body>
    </html>

ここで、javascriptファイル名をjsonRead.jsとしました。
実施していることは単純で、

<input id="file1" type="file" onChange="fileChanged(this)" multiple>

にてinputでファイル選択欄を作成し、ファイルが選択されたらfileChanged(this)をコールといった処理があるだけです。

ただファイルを読み込むだけではつまらないので読み込んだファイルをリストとして出力できるように、inputタグの下に、

<table border="1" id="table1">
     <tr>
         <th>性別</th>
         <th>名前</th>
         <th>年齢</th>
       </tr>
</table>

を追記しておきます。

次にjavascriptでの処理です。
コードはこちら。

let reader = new FileReader();

let huga = [];      // 管理するデータリスト
let fileCount = 1;

// ファイル選択画面にてファイルを選択した際の割り込み.
function fileChanged(input){
   huga = [];                  // 初期化.  
   for(let i = 0; i < input.files.length; i++){
        reader.readAsText(input.files[i], 'UTF-8');
        reader.onload = () =>{
            //console.log(reader.result);
            huga = JSON.parse(reader.result);
            
            let table = document.getElementById('table1'); 
            
           while (table.rows.length > 1){
                table.deleteRow(1);
            }
            
            //テーブルへのjsonデータ書き込み
            for(let j = 0;j < huga.length;j++){
                let row = table.insertRow(-1);
                let cell1 = row.insertCell(0);
                let cell2 = row.insertCell(1);
                let cell3 = row.insertCell(2);

                let checkBox = '<input type="radio" name="selectBtn" value="select'+j+'">'
                cell1.innerHTML = "<input type='text' id='type" + j + "' onChange='ChangeText(" + (j*10+1) + ")' value='" + huga[j].type + "'>"
                cell2.innerHTML = "<input type='text' id='name" + j + "' onChange='ChangeText(" + (j*10+2) + ")' value='" + huga[j].name+ "'>"
                cell3.innerHTML = "<input type='text' id='age" + j + "' onChange='ChangeText(" + (j*10+3) + ")' value='" + huga[j].age + "'>"
            }
        }

htmlと比較して少し長めですね。
htmlのコードの際にもご紹介しましたが、
実施していることとしては、fileChanged関数がファイル選択時に実行される処理になります。
こちらの関数がコールされると、

 for(let i = 0; i < input.files.length; i++){
        reader.readAsText(input.files[i], 'UTF-8');
        reader.onload = () =>{
            //console.log(reader.result);
            huga = JSON.parse(reader.result);

にて読み込んだファイル内のデータを読み出し、テキストデータとしてreader.resultに格納されます。
このreader.resultをJSON.parseによりjsonデータに変換、huga変数に格納しています。

JSON.parse以降の処理はテーブルへのデータ書き込みの処理にあたります。

var table = document.getElementById('table1'); 

にてtable1のidを持つ要素をtable変数に格納し、

for(let j = 0;j < huga.length;j++){
     let row = table.insertRow(-1);
     let cell1 = row.insertCell(0);
     let cell2 = row.insertCell(1);
     let cell3 = row.insertCell(2);

     let checkBox = '<input type="radio" name="selectBtn" value="select'+j+'">'
     cell1.innerHTML = "<input type='text' id='type" + j + "' onChange='ChangeText(" + (j*10+1) + ")' value='" + huga[j].type + "'>"
     cell2.innerHTML = "<input type='text' id='name" + j + "' onChange='ChangeText(" + (j*10+2) + ")' value='" + huga[j].name + "'>"
     cell3.innerHTML = "<input type='text' id='age" + j + "' onChange='ChangeText(" + (j*10+3) + ")' value='" + huga[j].age + "'>"
}

によりテーブルへjsonデータ分だけ書き込みを行っております。
ここで、テーブルのセル内が編集可能となるようにinputタグを用いてjsonデータの書き込みを行っております。

jsonデータをjsonファイルとして出力する

では次にjsonファイルを出力させてみたいと思います。
まずはhtmlです。
といってもこちらは、

<button type="button" onClick="WriteToFile()" >ファイル書き込み(json)</button>

をbody内に記載するのみです。
こちらはボタンタグでクリック時にWriteToFile関数がコールされる処理ですね。

そして、javascriptですがこちらのコードとなります。

function WriteToFile(){
    let hugastring = JSON.stringify(huga);
    let blob = new Blob([hugastring],{type:"text/plan"});
    let link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = '作ったファイル.json';
    link.click();
}

まず、

 let hugastring = JSON.stringify(huga);

でhugaデータを文字列データ(テキストデータ)に変換し、

let blob = new Blob([hugastring],{type:"text/plan"});

にて先ほどのテキストデータをオブジェクトに変換。
そして、

    let link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = '作ったファイル.json';
    link.click();

によってjsonファイルとしてダウンロードという形で出力させております。

■おまけ

先ほどのテーブルに対して追加したい場合にはこちらのようなコードをhtml、javascriptに追加すればOKです。
こちらのコードで先ほどのファイル出力を行うと追加したデータも合体して出力されます。
【html】

<label>タイプ:</label>
<input type="text" id="type">
<br/>
<label>名前:</label>
<input type="text" id="name">
<br/>
<label>年齢:</label>
<input type="text" id="age">
<br/>
<button type="button" onClick="ClickFunc()" >追加</button>
<br/>

javascript

function ClickFunc(){
    let type = document.getElementById('type').value;
    let name= document.getElementById('name').value; 
    let age = document.getElementById('age').value; 
    let data = {type:type, name:name, age:age}
    huga.push(data);
    
    var table = document.getElementById('table1'); 
    var row = table.insertRow(-1); 
    var cell1 = row.insertCell(0);
    var cell2 = row.insertCell(1);
    var cell3 = row.insertCell(2);

    cell1.innerHTML = "<input type='text' value='" + type + "'>"
    cell2.innerHTML = "<input type='text' value='" + name + "'>"
    cell3.innerHTML = "<input type='text' value='" + age + "'>"
}

■最後に

今回はjavascriptにてjsonファイルを読み込んでjsonデータとして取り扱い、jsonファイルとして出力する方法をまとめました。
分かれば案外簡単に処理できるな、と感じました。
せっかくなので、csvxmlなど他のファイルも試してみようかな??



OpenCVでのマウスによるエリア指定方法

今回はOpenCVでマウスイベントを取得する方法とマウスクリックした位置を取得してエリア指定する方法をまとめていきたいと思います。
マウス操作で領域切り出しを実施してみたかったので、知れて良かった!!



■環境

 ・OS:WIndows10
 ・プラットフォーム:anaconda
 ・言語:Python

■マウスイベント取得

まずはOpenCVにてマウスイベントを取得する方法です。
コードはこちら。

import cv2

def onMouse(event, x, y, flag, params):
    a, b = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click " + str(a))
    
    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click " + str(b))

if __name__ == '__main__':
    img = cv2.imread("ファイル名")
    size = (752, 1008)
    
    img = cv2.resize(img, size)
    wname = "MouseEvent"
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, onMouse, [1, 2])

    cv2.imshow(wname, img)
    while 1:
        if cv2.waitKey(10) == ord('q'):
            break
            
    cv2.destroyAllWindows()

やっていることですが、、、
マウス割り込み時の関数 onMouseを定義し、

def onMouse(event, x, y, flag, params):
    a, b = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click " + str(a))
    
    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click " + str(b))

このonMouseをマウスイベントとしてセットしていきます。

cv2.setMouseCallback(wname, onMouse, [1, 2])

第1引数にimshowで表示するwindow名、
第2引数にコールバック関数、
第3引数にセットした関数に渡す引数になります。

ここで、onMouse関数の引数であるx,yですがクリックした座標が格納されます。
このためこちらのように関数を作成すると、左クリックするごとに画像上に赤丸が付与されるようにすることもできます。

def onMouse(event, x, y, flag, params):
    wname, img = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click ")
        cv2.circle(img, (x, y), 3, (0, 0, 255), 3)
        cv2.imshow(wname, img)

    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click ")

動作させてみるとこんな感じです。
f:id:Elsammit:20210610225853g:plain

■選択した領域で囲う

では今度は先ほどの点を結んで領域として囲んでみます。
コードはこちらになります。

import cv2

class PointList():
    def __init__(self, npoints):
        self.ptlist = []
        self.pos = 0
    
    def add(self, x, y):
        self.ptlist.append([x, y])
        self.pos += 1
        return True

def onMouse(event, x, y, flag, params):
    wname, img, ptlist = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click ")
        ptlist.add(x, y)
        cv2.circle(img, (x, y), 3, (0, 0, 255), 3)
        cv2.imshow(wname, img)

    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click ")
        for i in range(ptlist.pos):
                    cv2.line(img, (ptlist.ptlist[i][0], ptlist.ptlist[i][1]),
                         (ptlist.ptlist[(i+1)%ptlist.pos][0], ptlist.ptlist[(i+1)%ptlist.pos][1]), (0, 0, 255), 3)
        cv2.imshow(wname, img)

if __name__ == '__main__':
    img = cv2.imread("IMG_0967.JPG")
    size = (752, 1008)
    
    img = cv2.resize(img, size)
    wname = "MouseEvent"
    ptlist = PointList(0)
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, onMouse, [wname, img, ptlist])

    cv2.imshow(wname, img)
    while 1:
        if cv2.waitKey(10) == ord('q'):
            break
            
    cv2.destroyAllWindows()

先ほどと異なる点として、
まずクリックした位置を覚えておくクラス、
PointListクラスを定義。

class PointList():
    def __init__(self, npoints):
        self.ptlist = []
        self.pos = 0
    
    def add(self, x, y):
        self.ptlist.append([x, y])
        self.pos += 1
        return True

そして左クリック毎に先ほどのクラス定義したオブジェクトptlistへ
クリックしたxy座標の値を記録しています。

ptlist.add(x, y)

最後に、右クリック時に記録していたplistからx,y座標を読み出し、線を引くことで領域指定が出来るようになります。

for i in range(ptlist.pos):
            cv2.line(img, (ptlist.ptlist[i][0], ptlist.ptlist[i][1]),
                (ptlist.ptlist[(i+1)%ptlist.pos][0], ptlist.ptlist[(i+1)%ptlist.pos][1]), (0, 0, 255), 3)

さらに、右クリック時の処理に、

arr_ld = np.array(ptlist.ptlist)
rect = cv2.boundingRect(arr_ld)
x,y,w,h = rect
croped = img[y:y+h, x:x+w].copy()

pts = arr_ld - arr_ld.min(axis=0)
mask = np.zeros(croped.shape[:2], np.uint8)
cv2.drawContours(mask, [pts], -1, (255, 255, 255), -1, cv2.LINE_AA)
dst = cv2.bitwise_and(croped, croped, mask=mask)

img = cv2.fillConvexPoly(img, np.array(arr_ld, 'int32'), color=(255, 255, 255))

img_gray = cv2.cvtColor(dst, cv2.COLOR_BGR2GRAY)
ret, img_thresh = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY)
cv2.imshow("wname", img_thresh)
cv2.imshow(wname, img)

を追加すると囲んだ領域に白くマスクを付けつつ、別画像として表示させることが可能になります。
動作としてはこんな感じ。
f:id:Elsammit:20210610231812g:plain

■最終的なコード

囲んだ領域に白くマスクを付けつつ、別画像として表示させる最終的なコードはこちらになります。

import cv2
import numpy as np

class PointList():
    def __init__(self, npoints):
        self.ptlist = []
        self.pos = 0
    
    def add(self, x, y):
        self.ptlist.append([x, y])
        self.pos += 1
        return True

def onMouse(event, x, y, flag, params):
    wname, img, ptlist = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click ")
        ptlist.add(x, y)
        cv2.circle(img, (x, y), 3, (0, 0, 255), 3)
        cv2.imshow(wname, img)

    if event == cv2.EVENT_RBUTTONDOWN:
        if(ptlist.pos >= 3): 
            print("Right button Click ")
            for i in range(ptlist.pos):
                cv2.line(img, (ptlist.ptlist[i][0], ptlist.ptlist[i][1]),
                    (ptlist.ptlist[(i+1)%ptlist.pos][0], ptlist.ptlist[(i+1)%ptlist.pos][1]), (0, 0, 255), 3)
            arr_ld = np.array(ptlist.ptlist)
            rect = cv2.boundingRect(arr_ld)
            x,y,w,h = rect
            croped = img[y:y+h, x:x+w].copy()

            pts = arr_ld - arr_ld.min(axis=0)
            mask = np.zeros(croped.shape[:2], np.uint8)
            cv2.drawContours(mask, [pts], -1, (255, 255, 255), -1, cv2.LINE_AA)
            dst = cv2.bitwise_and(croped, croped, mask=mask)

            img = cv2.fillConvexPoly(img, np.array(arr_ld, 'int32'), color=(255, 255, 255))

            cv2.imshow("wname", dst)
            cv2.imshow(wname, img)
        else:
            print("NG")

if __name__ == '__main__':
    img = cv2.imread("IMG_0967.JPG")
    size = (752, 1008)
    
    img = cv2.resize(img, size)
    wname = "MouseEvent"
    ptlist = PointList(0)
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, onMouse, [wname, img, ptlist])

    cv2.imshow(wname, img)
    while 1:
        if cv2.waitKey(10) == ord('q'):
            break
            
    cv2.destroyAllWindows()

■最後に

マウスでエリア指定して画像を切り出せるのは使い方によっては便利だな!!と思いました。
特に、Yoloなどの物体識別やOCRはもろに画像サイズが効いてくるので、エリアを指定し必要部分のみ抽出できるのはいいですね。



連番画像を動画化してコマ撮り映像を作成してみた

先日こんな記事を発見!!
note.com

さっそく妻に
"コマ撮りしてみないか?"
と誘ってみたところ、
"昔やってみたけどうまく行かなかった。"
"ただ興味はあるから協力してくれたらやる!!"
とのこと。

こんなご時世なのでお外に出づらいこともあり、
休日のお家時間として作業を行うことにしました!!

と言っても、、、
・妻:コマ撮り画像を作成
・私:画像を動画化
の分担となり、ほとんど私は何もしなかったのですがw。

動画作成後日談は別ブログに載せるとして、、、
こちらでは連番画像を動画化した時のコードを載せておきたいと思います!!

下記画像はコマ撮り映像に使用した画像です!!
f:id:Elsammit:20210607214617j:plain



■環境

 ・OS:windows10
 ・言語:python
 ・プラットフォーム:anaconda 

■連番画像の作成

動画化に入る前に連番画像になるように画像の名前をリネームしていきたいと思います。

今回使用した画像は写真撮影時間が名前に入っていたのですでに連番画像として扱おうと思えば扱えたのですが、
ちょっと分かりにくかったので番号名に変換しました。
その時のコードがこちら。

filepath_list = sorted(glob.glob('Photos/*'))
shutil.rmtree("./copyed")
os.mkdir("./copyed")
j = 0
for filename in filepath_list:
    #os.rename(filename, "copyed\photo"+str(j)+".jpg")    #ファイルのリネームの場合
    shutil.copyfile(filename, "copyed\photo"+str(j)+".jpg") #別フォルダにコピーする場合
    j+=1 

まず、

filepath_list = sorted(glob.glob('Photos/*'))

でフォルダ内のソートを行い、

shutil.rmtree("./copyed")
os.mkdir("./copyed")

で名前変換後にコピーするフォルダを再作成。
実施当初はリネームのみしていたのですが、
動画作成時の妻の要望が多かったためこちらのコードにしました。

最後に、

for filename in filepath_list:
    #os.rename(filename, "copyed\photo"+str(j)+".jpg")    #ファイルのリネームの場合
    shutil.copyfile(filename, "copyed\photo"+str(j)+".jpg") #別フォルダにコピーする場合
    j+=1 

でファイル毎に名前をphoto0.jpgといった名前にリネームないしは別フォルダにコピーしています。

ファイルをリネームするだけか別フォルダにコピーするかはお好みで。

■連番画像から動画化

では本題の連番画像から動画にしてみたいと思います。
と言ってもコードは結構単純なので載せてしまいます。

for i in range(0, j):
    filepath = "copyed\photo"+str(i)+".jpg"
    _img = cv2.imread(filepath)

    if _img is None:
        print("can't read")
        break
    
    img = cv2.resize(_img, (1616,1080))

    video.write(img)

video.release()

まず、

fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
video = cv2.VideoWriter("video.mp4",fourcc,3.5, (1616,1080))

 filepath = "copyed\photo"+str(i)+".jpg"
 _img = cv2.imread(filepath)
 if _img is None:
     print("can't read")
     break

で各ファイル毎にimreadで画像を読み出します。
画像名を連続番号に変更しているため単純なfor文で読み出しが可能になっています。

もし、画像が読み出せなかったら、
"can't read"
と出力し処理を抜けています。

そして、

video.write(img)

にて画像を1フレームとしてvideowriterに書き込んでいます。

videowriterですが、

fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
video = cv2.VideoWriter("video.mp4",fourcc,3.5, (1616,1080))

として、3.5fpsのmp4動画に変換しています。
3.5fpsの理由ですが、、、
コマ撮り動画を見ながら調整しました。
4fpsだと早いし3fpsだと遅いとのこと。
まぁ、こちらは環境や好みで変更ください。

■おまけ

画像の保存時間でソート、連番画像を作成する場合にはこちらのコードで行えます。

#files = os.listdir("Photos/")
xs = []
j = 0
for root, dir, files in os.walk("copyed/"):
    for f in files:
        path = os.path.join(root, f)
        xs.append((os.path.getmtime(path), path))

    for mtime, path in sorted(xs):
        name = os.path.basename(path)
        t = datetime.datetime.fromtimestamp(mtime)
        print(t, name)
        #os.rename('Photos\\' +name, "Photos\photo"+str(j)+".jpg")
        j+=1

■全体コード

コード全体はこちらのようになりました。

import sys
import cv2
import os
import glob
import re
import time
import datetime
import shutil

fourcc = cv2.VideoWriter_fourcc('m','p','4','v')
video = cv2.VideoWriter("video.mp4",fourcc,3.5, (1616,1080))

if not video.isOpened():
    print("can't be opened")
    sys.exit()

#保存時間でソート・リネームする場合はこちらを利用.
'''
#files = os.listdir("Photos/")
xs = []
j = 0
for root, dir, files in os.walk("copyed/"):
    for f in files:
        path = os.path.join(root, f)
        xs.append((os.path.getmtime(path), path))

    for mtime, path in sorted(xs):
        name = os.path.basename(path)
        t = datetime.datetime.fromtimestamp(mtime)
        print(t, name)
        #os.rename('Photos\\' +name, "Photos\photo"+str(j)+".jpg")
        j+=1
'''

#ファイル名でのソートはこちらを利用.
filepath_list = sorted(glob.glob('Photos/*'))
shutil.rmtree("./copyed")
os.mkdir("./copyed")
j = 0
for filename in filepath_list:
    #os.rename(filename, "copyed\photo"+str(j)+".jpg")
    shutil.copyfile(filename, "copyed\photo"+str(j)+".jpg")
    j+=1 

for i in range(0, j):
    filepath = "copyed\photo"+str(i)+".jpg"
    _img = cv2.imread(filepath)
    if _img is None:
        print("can't read")
        break
    
    img = cv2.resize(_img, (1616,1080))

    video.write(img)

video.release()

■最後に

画像を連番画像となるように名前をリネームし動画化するまでを行いました。
コマ撮り映像はこちらのようになりました。
よろしかったら見てみてください!!
www.youtube.com

今後も続ける予定なようなので、動画にするコードもアプリとして使用できるようにしようかな?



Flask 動画アップロード方法

前回、Flaskでの動画再生アプリや、
elsammit-beginnerblg.hatenablog.com

Yoloで物体検知が行える動画再生アプリを作成しました。
elsammit-beginnerblg.hatenablog.com

今回はFlaskでの動画アップロード機能を追加して、自分の持っている動画をWeb上で物体検知させて遊んでみたいと思います。



■前提

今回は前回の
"Yoloによる物体検知と動画再生Webアプリを組み合わせてみる"
elsammit-beginnerblg.hatenablog.com

からアップロード機能を実装していきたいと思います。
もし環境やFlaskによる動画再生アプリ作成方法を確認したい場合にはこちらをご覧ください。

■アップロード機能追加(フロントエンド)

ではまずはフロントエンドについてまとめておきたいと思います。
と言ってもフロントエンドはformを用いてpost送信を行うためのコードを書くだけ。
htmlファイルを変更するだけでOKです。
追加するコードはこちら。

<form method = post enctype = multipart/form-data action = "/upload">
      <p><input type=file name = file>
      <input type = submit value = Upload>
</form>

formタグにて送信方法をpostにセットし、ファイル選択用のinputタグとサーバへの送信用のタグを記載しているのみ。
とても簡単!!

今回は実施していませんが、レイアウトにこだわる場合にはcssファイルも変更してみてください。

■サーバサイド

ではメインであるサーバサイド(Flask)を変更していきたいと思います。
まずはコードを載せます。

ALLOWED_EXTENSIONS = set(['mp4', 'wmv', 'avi', 'gif'])
UPLOAD_FOLDER = './uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

def allwed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

#動画取得
@app.route('/upload', methods=["POST"])
def get_test():
    if 'file' not in request.files: # ファイルがなかった場合
        print('ファイルがありません')
        return redirect('/')
    file = request.files['file']    # データの取り出し
    if file.filename == '':         # ファイル名がなかった場合
        print('ファイルがありません')
        return redirect("/")
    if file and allwed_file(file.filename):
        filename = secure_filename(file.filename)   # 危険な文字を削除(サニタイズ処理)
        # ファイルの保存し保存したファイルから動画読み出し.
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        Camera.cap = cv2.VideoCapture(UPLOAD_FOLDER+"/"+filename)
        return redirect("/")                        # アップロード後のページに転送
    else:
        print("not movie file")
        return redirect("/")

まず、

@app.route('/upload', methods=["POST"])

ですが、、、ここは今までと同じなので割愛。

request変数にフロントより動画データファイルが渡されるので、

    if 'file' not in request.files: # ファイルがなかった場合
        print('ファイルがありません')
        return redirect('/')
    file = request.files['file']    # データの取り出し

のコードでファイルの有無をチェックしつつ、データを取り出します。

次に、

 if file and allwed_file(file.filename):
        filename = secure_filename(file.filename)   # 危険な文字を削除(サニタイズ処理)

ですが、ファイルが許可されたファイル拡張子であるか?
そして、許可されたファイル拡張子であってもファイル名が悪意のある名前でないか?
をチェックしています。
allwed_file関数ですが、

def allwed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

としました。

悪意のあるファイル名かをチェックする、secure_filename関数ですが、
Flask公式もアップロード機能を追加する際には載せておくことを推奨しているようです。
Uploading Files — Flask Documentation (2.0.x)

下記のような記載があり、ユーザ入力を信じるな。
悪意のあるユーザを想定してどんなファイルでもアップロードさせるのは危険。
と記載されていますね。
気を付けます。
Now the problem is that there is that principle called “never trust user input”. This is also true for the filename of an uploaded file.
All submitted form data can be forged, and filenames can be dangerous. For the moment just remember: always use that function to secure a filename before storing it directly on the filesystem.

最後に、

file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        Camera.cap = cv2.VideoCapture(UPLOAD_FOLDER+"/"+filename)
        return redirect("/")                        # アップロード後のページに転送

にてサーバの所定位置にアップロードされた動画を格納し、
そこから動画をVideoCaptureしていきます。

■動かしてみる

動かすといっても以前動かした内容に動画アップロード機能が追加されただけなので、
キャプチャのみ載せます。
今回のコードを追加して実行するとこちらのように動画の上にファイルアップロード用のボタンがセットされます。
f:id:Elsammit:20210603225439p:plain

■最後に

今回は様々な動画に対して物体検知が行えるように動画アップロード機能を追加してみました。
全体のコードですが、こちらに格納していますのでよろしければ見ていってください!!
https://github.com/Elsammit/Flask-VideoApplication.git

動画は結果が目で見て分かるのでやっていて楽しいですね!!
もう少し色々な処理を試してみたいな。