文書の過去の版を表示しています。


第3章 スターターコード【中級編 ライン判定(通過判定)】

この章では、

  • エリア検知からラインをまたいだ検知への応用
  • WEB UIを利用することで画像から座標情報をペイントツールで目視で読み取って転記するという面倒な作業の廃止
  • カメラ画像座標から見取り図画像座標への切り替え

がテーマとなります。
WEBUIについてはカスタマイズ対象としませんが、興味がある人が調べられるように簡単に説明します。

WEB UIを利用した時の解析プログラム

import sys
import os
import json
import math
from datetime import datetime, timedelta, timezone
import csv
import glob
import numpy as np

def intersect(p1, p2, p3, p4):
    tc1 = (p1[0] - p2[0]) * (p3[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p3[0])
    tc2 = (p1[0] - p2[0]) * (p4[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p4[0])
    td1 = (p3[0] - p4[0]) * (p1[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p1[0])
    td2 = (p3[0] - p4[0]) * (p2[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p2[0])
    return tc1*tc2<0 and td1*td2<0

if __name__ == '__main__':
    
    dirpath = 'data'
    line_def_file = os.environ['HOME']+'/drawui/sketchlines.json'
    
    JST = timezone(timedelta(hours=+9), 'JST')
    print(sys.argv[1]+"を集計しています・・")
    file_list = sorted(glob.glob('./'+dirpath+'/'+sys.argv[1]+'/raw_'+sys.argv[1]+'*.json'))
    print("対象ファイル一覧")
    for filename in file_list:
        print(filename)
    
    json_open = open(line_def_file, 'r')
    result_json_file = open('result_sketchlines_'+sys.argv[1]+'.json', 'w')
    linedef = json.load(json_open)
    resultarr = {}
    for lineidx, line in enumerate(linedef['lines']):
        linename='lineID'+str(lineidx)
        print(linename)
        resultarr[linename]={}
        
    print("集計・・")
    for lineidx, line in enumerate(linedef['lines']):
        linename='lineID'+str(lineidx)
        print(linename)
        polygon = []
        print(line)
        for point in line['points']:
            polygon.append([point['x'],point['y']])

        for filename in file_list:
            print(linename+"_"+filename)
            if os.path.getsize(filename) == 0:
                continue
                
            raw_json_open = open(filename, 'r')
            raw_json = json.load(raw_json_open)
            
            for frame in raw_json:
                jptime = datetime.fromtimestamp(frame['frame_time'],JST)
                people = frame['objects']
                for person in people:
                    
                    #print(linename+'_'+person['tracking_id']+'_'+str(judgenum))
                    personpos = [person["floor"]["x"],person["floor"]["y"]]
                    if person['tracking_id'] in resultarr[linename]:
                        if intersect(personpos,resultarr[linename][person['tracking_id']]['prev_pos'],polygon[0],polygon[1]):
                            resultarr[linename][person['tracking_id']]['passing_judge'] = True
                            resultarr[linename][person['tracking_id']]['passing_time'] = jptime.isoformat()
                        resultarr[linename][person['tracking_id']]['end_time'] = frame['frame_time']
                        resultarr[linename][person['tracking_id']]['end_time_jp'] = jptime.isoformat()
                    else:
                        resultarr[linename][person['tracking_id']]={'line':linename,'start_time_jp':jptime.isoformat(),'end_time_jp':jptime.isoformat(),'start_time':frame['frame_time'],'end_time':frame['frame_time'],'stream_id':frame['stream_id'],'passing_judge':False,'passing_time':0,'prev_pos':personpos}

    json.dump(resultarr,result_json_file)
    
    for linename, records in resultarr.items():
        print(linename)
        f = open("sketchlines_"+sys.argv[1]+"_"+linename+".csv", 'w', encoding='utf-8', newline='')
        write = csv.writer(f)
        write.writerow(["stream_id","linename","tracking_id","start_time_jp","end_time_jp","passing_judge","passing_time"])
        for tracking_id, record in records.items():
            write.writerow([record['stream_id'],linename,tracking_id,record['start_time_jp'],record['end_time_jp'],record['passing_judge'],record['passing_time']])
        
        f.close()      
二章で作成したプログラムからの改変となっているので、基本的な構造は同じになります。

線情報はUIから都度更新される下記のものを参照しています

{
    "lines": [
        {
            "points": [
                {"x": 131, "y": 134}, 
                {"x": 260, "y": 128}
            ]
        }, 
        {
            "points": [
                {"x": 59, "y": 226}, 
                {"x": 130, "y": 100}
            ]
        }
    ]
}

WEB UIを使う前に、まず前章と同じように座標値を手で調べる前提でラインを通過した時の検知を作りましょう 通過については下記のように例えば一つ前の11秒00のフレームと現在の11秒10のフレームとの人物位置をそれぞれA(p1)、B(p2)、検知線の開始・終了位置C(p3)、D(p4)の線が交差しているかどうかを判定することになります。

これらについては一つまとまった判定になるため、下記のように4点与えてあげれば通過・非通過を1,0で応答してくれる関数を別途定義します。

def intersect(p1, p2, p3, p4):
    tc1 = (p1[0] - p2[0]) * (p3[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p3[0])
    tc2 = (p1[0] - p2[0]) * (p4[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p4[0])
    td1 = (p3[0] - p4[0]) * (p1[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p1[0])
    td2 = (p3[0] - p4[0]) * (p2[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p2[0])
    return tc1*tc2<0 and td1*td2<0

また、前章のエリア判定から変わっているポイントとして

  • ライン判定になっているので関連する変数をすべて変更
  • 1つ前の座標を結果配列で保持(prev_pos項目)しておいて、簡単にintersect関数に引き渡せるようにしている
  • 通過時間を記録する
  • CSVに出力する内容を変更

となっています。
これらを反映したプログラムがこちらです。

import sys
import os
import json
import math
from datetime import datetime, timedelta, timezone
import csv
import glob
import numpy as np

def intersect(p1, p2, p3, p4):
    tc1 = (p1[0] - p2[0]) * (p3[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p3[0])
    tc2 = (p1[0] - p2[0]) * (p4[1] - p1[1]) + (p1[1] - p2[1]) * (p1[0] - p4[0])
    td1 = (p3[0] - p4[0]) * (p1[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p1[0])
    td2 = (p3[0] - p4[0]) * (p2[1] - p3[1]) + (p3[1] - p4[1]) * (p3[0] - p2[0])
    return tc1*tc2<0 and td1*td2<0

if __name__ == '__main__':
    
    dirpath = 'data'
    duration = 5
    line_def_file = '03lines.json'
    
    JST = timezone(timedelta(hours=+9), 'JST')
    print(sys.argv[1]+"を集計しています・・")
    file_list = sorted(glob.glob('./'+dirpath+'/'+sys.argv[1]+'/raw_'+sys.argv[1]+'*.json'))
    print("対象ファイル一覧")
    for filename in file_list:
        print(filename)
    
    json_open = open(line_def_file, 'r')
    result_json_file = open('result_'+line_def_file[:len(line_def_file)-5]+'_'+sys.argv[1]+'.json', 'w')
    linedef = json.load(json_open)
    resultarr = {}
    for lineidx, line in enumerate(linedef['lines']):
        linename='lineID'+str(lineidx)
        print(linename)
        resultarr[linename]={}
        
    print("集計・・")
    for lineidx, line in enumerate(linedef['lines']):
        linename='lineID'+str(lineidx)
        print(linename)
        polygon = []
        print(line)
        for point in line['points']:
            polygon.append([point['x'],point['y']])

        for filename in file_list:
            print(linename+"_"+filename)
            if os.path.getsize(filename) == 0:
                continue
                
            raw_json_open = open(filename, 'r')
            raw_json = json.load(raw_json_open)
            
            for frame in raw_json:
                jptime = datetime.fromtimestamp(frame['frame_time'],JST)
                people = frame['objects']
                for person in people:
                    
                    #print(linename+'_'+person['tracking_id']+'_'+str(judgenum))
                    personpos = [(person["x0"]+person["x1"])/2,person["y1"]]
                    if person['tracking_id'] in resultarr[linename]:
                        if intersect(personpos,resultarr[linename][person['tracking_id']]['prev_pos'],polygon[0],polygon[1]):
                            resultarr[linename][person['tracking_id']]['passing_judge'] = True
                            resultarr[linename][person['tracking_id']]['passing_time'] = jptime.isoformat()
                        resultarr[linename][person['tracking_id']]['end_time'] = frame['frame_time']
                        resultarr[linename][person['tracking_id']]['end_time_jp'] = jptime.isoformat()
                    else:
                        resultarr[linename][person['tracking_id']]={'line':linename,'start_time_jp':jptime.isoformat(),'end_time_jp':jptime.isoformat(),'start_time':frame['frame_time'],'end_time':frame['frame_time'],'stream_id':frame['stream_id'],'passing_judge':False,'passing_time':0,'prev_pos':personpos}

    json.dump(resultarr,result_json_file)
    
    for linename, records in resultarr.items():
        print(linename)
        f = open(line_def_file[:len(line_def_file)-5]+"_"+sys.argv[1]+"_"+linename+".csv", 'w', encoding='utf-8', newline='')
        write = csv.writer(f)
        write.writerow(["stream_id","linename","tracking_id","start_time_jp","end_time_jp","passing_judge","passing_time"])
        for tracking_id, record in records.items():
            write.writerow([record['stream_id'],linename,tracking_id,record['start_time_jp'],record['end_time_jp'],record['passing_judge'],record['passing_time']])
        
        f.close()    

さて、前項でライン判定まではできるようになったので、WEBUIを利用してみましょう
今まではtutorialフォルダのみで作業していましたが、次は

cd ~/drawui

でフォルダを切り替えましょう
ここではフロントエンド(WEB画面で表示される部分のプログラム)とバックエンド(フロントエンドのプログラムから指示されてデータを保存したり出力したりする部分のプログラム)両方を兼ね備えており、

./start.sh

を押してしばらく待てば
http://解析端末のIP/


などの画面にアクセスすることができます。
この画面では

  1. ナビゲーションページ
  2. カメラから静止画を取得して、その上に複数の多角形を設定できるページ
  3. カメラから静止画を取得して、その上に複数の線分を設定できるページ
  4. カメラからリアルタイム映像を取得して、その上に複数の多角形を設定できるページ
  5. カメラからリアルタイム映像を取得して、その上に複数の線分を設定できるページ
  6. SCORER People Trackerで保存した見取り図を取得して、その上に複数の多角形を設定できるページ
  7. SCORER People Trackerで保存した見取り図を取得して、その上に複数の線分を設定できるページ

が用意されています。
エリアや線分を追加してsaveボタンをおすことでJSONファイルに結果が保存され、次回表示時にもその結果が表示されます
2と4、3と5は同じ設定値を参照していますので、どちらかで更新すればもう片方に反映します。
データの出力先はdrawuiフォルダ内の

camareas.json
camlines.json
sketchareas.json
sketchlines.json

となっており、JSON形式は前章までで利用していたものと同じになります。
利用を終了する場合はTerminal上でCtrl+Cでサーバーが止まります。
スターターコードの完成の項でデータのつなぎ込みをしてみましょう

drawuiで利用しているのはReactという最近のWEB開発ではデファクトスタンダード(事実上の業界標準)となりつつある技術を利用しています。
昔のイメージのHTMLファイルをページごとに作成して表示というものではなく、1つのindex.htmlに複雑なjavascriptを追加することで、スマホのアプリのような動きのある自由な表現が可能になる仕組みです。 そのため、解説しているサイトや本は日本でも多数でてきています。
公式ページも日本語で公開されており、技術に興味がある人は
https://ja.reactjs.org/docs/getting-started.html
等から学び始めることがよいでしょう。

ただし今回はReact自体が分からなくても、どのプログラムファイルをどういじれば何が変わるかという点だけでピンポイントで解説していこうと思いますので、本チュートリアルを進めるうえではReactの学習は必須ではありません。

WEBサーバーについては今回FlaskというPythonで簡単にWEBサーバーを構築できる仕組みを採用します。
Flaskでは

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return "Hello World!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=False, port=80)
だけで

python app.py

を実行するとhttp://解析端末のIP/でアクセスができ、白い画面に「Hello World!」と表示されWEBページが見れることを確認できます。
今回はHello World!ではなく、Reactで作ったコードにアクセスできるように設定します。スターターコードの

app = Flask(__name__, static_folder="./build/static", template_folder="./build")
(中略)
@app.route("/")
def index():
    return render_template("index.html")
@app.errorhandler(404)
def not_found(e):
    return render_template("index.html")

がその設定になり、トップページと、何もないページにアクセスした場合強制的にトップページを表示するという設定となります。Reactを使う上ではこういうものだと思ってもらえれば大丈夫です。

なお、このFlaskでのWEBページ公開方法は開発用となっており、インターネットで大量にアクセスしてくるような大規模な本番用WEBページには非推奨です。ですが、SCORER Edge上で少数の人間がアクセスしてくる分には全く問題ありません。

描画画面でエリアや線分を設定したあと、そのデータを保存したり、ページ表示時に読み込んだりする部分のプログラム(API)についての解説ですが、今回は特殊な方法を使っているため、WEBサーバー公開と同じserver.py内で処理が行われています。 一般的にはWEBサーバーとAPI部分は別となっているのですが、

  • tutorial_3.1641272799.txt.gz
  • 最終更新: 2022/01/04 14:06
  • by satoshi