勉強しないとな~blog

ちゃんと勉強せねば…な電気設計エンジニアです。

OpenCVやってみる - 57. Optical Flow

OpenCVのOptical Flowを試してみる

画像からの動き検出をやってみたいと思っています。

OpenCVの公式チュートリアルの中で、Optical Flowが取り上げられていて、これでできそうなので、やってみます。

OpenCV: Optical Flow

対象データ

同じデータを使っても面白くないので、別のデータで。
MOTChallengeの中のMOT16のデータを使ってみたいと思います。

https://motchallenge.net/data/MOT16/

色々と動画データがありますが、特に密なOptical Flowを計算するときは背景が動いていない(カメラが固定されている)ほうが効果が分かりやすそうなので、そういうデータを選びます。

  • MOT16-09
  • MOT16-04
  • MOT16-02
  • MOT16-08
  • MOT16-03
  • MOT16-01

今回は、この中のMOT16-09を使ってみます。

動画データは上記リンクからダウンロード。

Optical Flow計算実施

チュートリアルのコード通りでできると思いますが、途中の状態も見ながらやってみようと思います。

まずはLucas-Kanadeの手法から。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img_path = 'C:/work/MOT16/train/MOT16-09/img1/%06d.jpg'
cap = cv2.VideoCapture(img_path)
print(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
print(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
1920.0
1080.0
def plot_image(img):
    img0 = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    plt.imshow(img0), plt.xticks([]), plt.yticks([])
    plt.show()
    print('MOT16 - https://motchallenge.net/data/MOT16/')
ret, old_frame = cap.read()
plot_image(old_frame)

MOT16 - https://motchallenge.net/data/MOT16/

↑ MOTChallengeの動画はCreative Commons Attribution-NonCommercial-ShareAlike 3.0 Licenseなので、とりあえずいちいちリンクを貼っておきます。
これでいいのかな…

Creative Commons — Attribution-NonCommercial-ShareAlike 3.0 Unported — CC BY-NC-SA 3.0

ブログに画像を入れたい方必見!「クリエイティブ・コモンズ」の意味と画像の探し方を徹底解説! | インターネットビジネスラボ

クリエイティブ・コモンズ・ライセンスとは | クリエイティブ・コモンズ・ジャパン

cv2.VideoCaptureでは、上記の通り連番画像ファイルの読み込みが可能。

OpenCV: cv::VideoCapture Class Reference

以下は下準備。

# params for ShiTomasi corner detection
feature_params = dict( maxCorners = 100,
                       qualityLevel = 0.3,
                       minDistance = 7,
                       blockSize = 7 )
# Parameters for lucas kanade optical flow
lk_params = dict( winSize  = (15, 15),
                  maxLevel = 2,
                  criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

# Create some random colors
color = np.random.randint(0, 255, (100, 3))

以下は、cv2.goodFeaturesToTrack()関数で特徴点を検出しています。

old_gray = cv2.cvtColor(old_frame, cv2.COLOR_RGB2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
print(type(p0))
print(p0.shape)
print(p0[:5,:,:])
print(p0.dtype)
<class 'numpy.ndarray'>
(100, 1, 2)
[[[1437.  179.]]

 [[1421.  174.]]

 [[1305.  144.]]

 [[1390.  137.]]

 [[ 401.   39.]]]
float32

maxCorners引数を100にしているので、100点出てきています。

img_with_corners = old_frame.copy()
for p in p0[:,0,:]:
    cv2.circle(img_with_corners, center=(int(p[0]), int(p[1])), radius=10, color=(0,255,0), thickness=3)

plot_image(img_with_corners)

MOT16 - https://motchallenge.net/data/MOT16/

人じゃないところに特徴点検出してしまっている…
背景がガチャガチャしているとあまり良くないか。

チュートリアルのコード続きです。
ただし、結果を動画データに保存できるようにコード追加しています。

codec = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter('MOT16-09-LK.mp4', codec, 30.0, (1920,1080))
# Create a mask image for drawing purposes
mask = np.zeros_like(old_frame)
while(1):
    ret, frame = cap.read()
    if not ret:
        print('No frames grabbed!')
        break
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # calculate optical flow
    p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
    # Select good points
    if p1 is not None:
        good_new = p1[st==1]
        good_old = p0[st==1]
    # draw the tracks
    for i, (new, old) in enumerate(zip(good_new, good_old)):
        a, b = new.ravel()
        c, d = old.ravel()
        mask = cv2.line(mask, (int(a), int(b)), (int(c), int(d)), color[i].tolist(), 2)
        frame = cv2.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1)
    img = cv2.add(frame, mask)
    writer.write(img)
    img = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
    cv2.imshow('frame', img)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break
    # Now update the previous frame and previous points
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1, 1, 2)

cv2.destroyAllWindows()
writer.release()
No frames grabbed!

それらしい動きが取得できているよう。
動画データも残せました。(一部のみ切り出して下記に示しています。)

MOT16 - https://motchallenge.net/data/MOT16/

処理は結構重たく、上記のコードの実行時間は46.6(s)でした。(動画自体は18秒。PC性能は低い。)

全部の軌跡を残しているので、後半のほうはかなりぐちゃぐちゃ。
とりあえずオプティカルフローの雰囲気はこんな感じということで。

ある程度のフレーム数、同じ点を追尾してくれていますが、軌跡が途中で飛んだりすることもあります。

チュートリアルのコードでは、cv2.calcOpticalFlowPyrLK()で推定した次の特徴点位置を繰り返し同じ関数に与えて、軌跡の計算をしているので、次々と新しい人物が現れてくるような今回のデータは不適切だったかも。

ときどき特徴点検出をやり直したほうが良かったかも。

今回はそこまで深堀しませんが。

密なオプティカルフロー

チュートリアルのもう一つの項目「密なオプティカルフロー」(Dense Optical Flow)をやってみます。
データは同じMOT16のものを使用。

実施

まずは動画データを先頭に巻き戻し。

cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
True

前と同様に、チュートリアルのコードに動画保存を追加。

ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(frame1)
hsv[..., 1] = 255

codec = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter('MOT16-09-FB.mp4', codec, 30.0, (1920,1080*2))

while(1):
    ret, frame2 = cap.read()
    if not ret:
        print('No frames grabbed!')
        break
    next = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
    flow = cv2.calcOpticalFlowFarneback(prvs, next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
    mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
    hsv[..., 0] = ang*180/np.pi/2
    hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
    bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
    frame2 = np.vstack((frame2, bgr))
    writer.write(frame2)
    frame2 = cv2.resize(frame2, None, fx=0.25, fy=0.25, interpolation=cv2.INTER_AREA)
    cv2.imshow('frame2', frame2)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break
    prvs = next
cv2.destroyAllWindows()
writer.release()
No frames grabbed!

これもなんとなく動きが得られているような。

  • エッジ部分で特に動きが見られる。(黒っぽい服、ズボンだと顕著)
  • 動きの方向も区別できている(左→右: 赤い色、右→左: 青っぽい色、すれ違いが発生しているところは特に見どころ)

MOT16 - https://motchallenge.net/data/MOT16/

処理はより重たく、上記のコードの実行時間は6m23.9(s)でした。(動画自体は18秒。PC性能は低い。)

以上

今回はここまで。
きちんと評価はできていませんが、Optical Flowを体感しました。

オブジェクトトラッキングアルゴリズムに興味を持っているので、今後関係しそうなものを見ていきたいと思います。

OpenCVやってみる - 56. GitHubでexe公開

今回で春のパン祭り点数集計のアプリも仕上げです。
GitHubのrelease機能を使って、exeを公開してみます。

下記を参考にしましたが、基本的にはGitHubのガイドに従うだけです。

【GitHub】exeファイルを配布する方法 - しまぞうブログ

改めてGitHubリポジトリのリンクを貼っておきます。

github.com

手順

参考サイトでは、既存のtagを使っていましたが、今回はRelease機能上でtagも一緒に新規作成しました。

  1. GitHubリポジトリトップで、"Create a new release"をクリック。
    スクリーンショットは取り忘れました(´-`)

  2. "Choose a tag"のところをクリック。

  3. tag名のところに、今回のexeのバージョン名を入力して、"Create new tag"をクリック。

  4. "Choose a tag"のところに、バージョン名のタグが表示されます。
    あとはタイトル、説明文を入力して、exeをアップロードします。
    最後に"Publish release"をクリックすると、公開されます。

結果

  • GitHubリポジトリトップの右側の"Releases"に、今回のリリース版が表示されていて、

  • そこをクリックすると内容が表示されます。
    ここからexeがダウンロードできます。

ひとまず完了

これで一区切りです。

直したいところもまだありますが、気が向いたらで。

春のパン祭りももうすぐ終わりなので、もう需要はないかな…

OpenCVやってみる - 55. exe化変更、リポジトリ調整

今回は、exeをGitHubに上げる前に、いくつか調整しておきます。

変更点は、GitHubリポジトリに反映しています。

github.com


exe化の変更

  • Pyinstaller生成物フォルダを変更
    pyinstallerフォルダを作って、その中にbuildとdistのフォルダを生成するようにしました。
  • アプリのアイコン追加
    --iconのオプションで指定可能。
    お絵描きソフト(よく使っているdrawio)で適当なアイコンを作り、iconフォルダを作ってそこに配置しました。
    pngデータでいけました。

フォルダ構成

(アプリルート)  
├─ harupan.py
├─ data
│   ├─ harupan_svm_220412.dat
│   └─ templates2021.json
├─ pyinstaller
│   ├─ build
│   │   └─ harupan
│   │        └─ 中間生成物色々
│   ├─ dist
│   │   └─ harupan.exe (`--onefile`オプション指定時)
│   └─ harupan.spec
├─ icon
│   └─ harupan_icon4.png
...

Pyinstaller実行時の引数は以下のようになります。

pyinstaller harupan.py --distpath pyinstaller/dist --workpath pyinstaller/build --specpath pyinstaller --add-data ..\data\harupan_svm_220412.dat;data --add-data ..\data\templates2021.json;data --noconsole --onefile --icon ..\icon\harupan_icon4.png

生成されたexeの見た目。


その他変更

PowerShellスクリプト

まず、PowerShellでAnacondaを使う場合は、下記サイトに書かれている手順が必要でした。
【Python】PowerShellでAnacondaを使えるようにする | Hisuiblog

作成したPowerShellスクリプト(harupan_pyinstaller.ps1)の中身は以下の通りです。

Param([String]$Arg1 = "dist")
pyinstaller harupan.py --distpath pyinstaller/$Arg1 --workpath pyinstaller/build --specpath pyinstaller --add-data "..\data\harupan_svm_220412.dat;data" --add-data "..\data\templates2021.json;data" --noconsole --onefile --icon ..\icon\harupan_icon4.png

引数でexe生成フォルダ名を指定できるようにしました。

PowerShellでのスクリプト引数の使い方参考:
PowerShell で引数を受け取る | マイクロソフ党ブログ

requirements.txt生成

Python環境の再現用に、requirements.txtを生成しました。
pip freeze > requirements.txtでできそうでしたが、"@ file:"というのが出てしまうことがあり、この場合、環境再現ができませんでした。

生成されたrequirements.txt :

altgraph==0.17.3
certifi @ file:///C:/b/abs_85o_6fm0se/croot/certifi_1671487778835/work/certifi
distlib==0.3.6
filelock==3.12.0
importlib-metadata==6.5.0
numpy==1.21.6
opencv-python==4.7.0.72
ordered-set==4.1.0
pefile==2023.2.7
Pillow==9.5.0
platformdirs==3.2.0
pyinstaller==5.10.0
pyinstaller-hooks-contrib==2023.2
pywin32-ctypes==0.2.0
typing_extensions==4.5.0
virtualenv==20.22.0
virtualenv-clone==0.5.7
wincertstore==0.2
zipp==3.15.0
zstandard==0.20.0

下記サイト参考:
pip freezeで上手く出力されない場合(@ file:~~となってしまう場合) - Qiita

このサイトに書かれているように、pip list --format=freeze > requirements.txtならうまくいきました。

生成されたrequirements.txt :

altgraph==0.17.3
certifi==2022.12.7
distlib==0.3.6
filelock==3.12.0
importlib-metadata==6.5.0
numpy==1.21.6
opencv-python==4.7.0.72
ordered-set==4.1.0
pefile==2023.2.7
Pillow==9.5.0
pip==22.3.1
platformdirs==3.2.0
pyinstaller==5.10.0
pyinstaller-hooks-contrib==2023.2
pywin32-ctypes==0.2.0
setuptools==67.7.1
typing_extensions==4.5.0
virtualenv==20.22.0
virtualenv-clone==0.5.7
wheel==0.38.4
wincertstore==0.2
zipp==3.15.0
zstandard==0.20.0

下記の手順で環境再構築ができます。

conda create -n test python=3.7
conda activate test
pip install -r requirements.txt


ついで

はてなブログでコードブロックを表示するとき、横に長い行が折り返して表示されていたのが気になったので、 スクロールできるように、デザインCSSを変更してみました。

はてなブログでソースコードのブロックに横スクロールバーを表示する方法 - yk5656 diary


以上

次こそGitHubにexeを置いて完了のはず…

OpenCVやってみる - 54. Nuitkaでexe化

今回は、前回に引き続きexe化をやります。 前はPyinstallerを使いましたが、今回はNuitkaを試してみます。

conda環境作成

前回用意したconda環境はかなり最小限のものでしたが、Pyinstallerがどうしても余分になってしまいます。
なので、環境を再作成します。

前と同じコマンドとなります。
実行結果は省略します。

conda create -n harupan_nuitka python=3.7
conda activate harupan_nuitka
pip install opencv-python Pillow

Nuitkaインストール

pipでインストールできます。

参考サイト
NuitkaでPythonプログラムを配布してみよう | インフォメーション・ディベロプメント

pip install nuitka

実行結果

(harupan_nuitka) C:\Users\a\work\OpenCV\harupan\harupan_data>pip install nuitka
Collecting nuitka
  Downloading Nuitka-1.5.6.tar.gz (4.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.2/4.2 MB 6.3 MB/s eta 0:00:00
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Preparing metadata (pyproject.toml) ... done
Collecting zstandard>=0.15
  Downloading zstandard-0.20.0-cp37-cp37m-win_amd64.whl (644 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 644.5/644.5 kB 5.8 MB/s eta 0:00:00
Collecting ordered-set>=4.1.0
  Downloading ordered_set-4.1.0-py3-none-any.whl (7.6 kB)
Building wheels for collected packages: nuitka
  Building wheel for nuitka (pyproject.toml) ... done
  Created wheel for nuitka: filename=Nuitka-1.5.6-cp37-cp37m-win_amd64.whl size=2874116 sha256=ae2e48989ca850acd83b60a16242cd3fd7fb351a8f123dc3a6e7647a4128cef7
  Stored in directory: c:\users\a\appdata\local\pip\cache\wheels\1a\93\6b\a0109964271ae49d17bf0845da846148a392cf697099275638
Successfully built nuitka
Installing collected packages: zstandard, ordered-set, nuitka
Successfully installed nuitka-1.5.6 ordered-set-4.1.0 zstandard-0.20.0

(harupan_nuitka) 

Nuitka実行

Nuitkaの使い方は、下記サイトで特に詳しく説明されています。

Nuitkaを使ってスクリプトをバイナリ化してみよう - PythonOsaka

今回は、下記コマンドでexe生成しました。

nuitka harupan.py --standalone --enable-plugin=tk-inter --include-data-dir=data=data --output-dir=nuitka --disable-console
  • --standalone
    生成したexeを実行するPCにPython環境がなくても実行できるようになります。
  • --enable-plugin=tk-inter
    tkinterを使う場合は、このオプションが必要になります。
    numpyも--enable-pluginのオプションを付ける必要がある、と書かれているサイトもありましたが、今回はなしで大丈夫でした。
  • --include-data-dir=data=data
    今回は外部のデータファイルを含みますが、そのフォルダを指定します。
    フォルダ内の全ファイルが配布用フォルダにコピーされます。
    オプション内で=が2回現れていますが、1つ目の後がコピー元、2つ目の後がコピー先になります。
  • --output-dir
    ビルド生成物フォルダと、exeを含む配布用フォルダの生成先を指定します。
  • --disable-console
    exe実行時、コンソールが出現しないようになります。

なお、--onefileオプションを付けると、Pyinstallerのときと同様、1つのexeファイルに集約されるとのことですが、やってみたところ、exe生成に失敗してしまいました…
今回は諦めにします。

実行結果(これまた長いですが、今後の参考として、全部掲載しちゃいます。)

(harupan_nuitka) C:\Users\a\work\OpenCV\harupan\harupan_data>nuitka harupan.py --standalone --enable-plugin=tk-inter --include-data-dir=data=data --output-dir=nuitka --disable-console 
Nuitka-Options:INFO: Used command line options: harupan.py --standalone --enable-plugin=tk-inter --include-data-dir=data=data --output-dir=nuitka --disable-console
Nuitka:INFO: Starting Python compilation with Nuitka '1.5.6' on Python '3.7' commercial grade 'not installed'.
Nuitka-Plugins:INFO: tk-inter: Injecting pre-module load code for module 'tkinter':
Nuitka-Plugins:INFO: tk-inter:     Need to make sure we set environment variables for TCL.
Nuitka:INFO: Completed Python level compilation and optimization.                
Nuitka:INFO: Generating source code for C backend compiler.
Nuitka:INFO: Running data composer tool for optimal constant value handling.              
Nuitka:INFO: Running C compilation via Scons.
Nuitka-Scons:INFO: Too old gcc 'C:\Program Files\mingw-w64\x86_64-8.1.0-win32-seh-rt_v6-rev0\mingw64\bin\gcc.exe' ((8, 1, 0) < (11, 2)) ignored!
Nuitka will use gcc from MinGW64 of winlibs to compile on Windows.

Is it OK to download and put it in 'C:\Users\a\AppData\Local\Nuitka\Nuitka\Cache\downloads\gcc\x86_64\11.3.0-14.0.3-10.0.0-msvcrt-r3'.

Fully automatic, cached. Proceed and download? [Yes]/No :
Nuitka:INFO: Downloading 'https://github.com/brechtsanders/winlibs_mingw/releases/download/11.3.0-14.0.3-10.0.0-msvcrt-r3/winlibs-x86_64-posix-seh-gcc-11.3.0-llvm-14.0.3-mingw-w64msvcrt-10.0.0-r3.zip'.
Nuitka:INFO: Extracting to 'C:\Users\a\AppData\Local\Nuitka\Nuitka\Cache\downloads\gcc\x86_64\11.3.0-14.0.3-10.0.0-msvcrt-r3\mingw64\bin\gcc.exe'
Nuitka-Scons:INFO: Backend C compiler: gcc (gcc).
Nuitka will make use of ccache to speed up repeated compilation.

Is it OK to download and put it in 'C:\Users\a\AppData\Local\Nuitka\Nuitka\Cache\downloads\ccache\v4.6'.

Fully automatic, cached. Proceed and download? [Yes]/No :
Nuitka:INFO: Downloading 'https://github.com/ccache/ccache/releases/download/v4.6/ccache-4.6-windows-32.zip'.
Nuitka:INFO: Extracting to 'C:\Users\a\AppData\Local\Nuitka\Nuitka\Cache\downloads\ccache\v4.6\ccache.exe'
Nuitka-Scons:INFO: Slow C compilation detected, used 60s so far, scalability problem.
Nuitka-Scons:INFO: Running '"C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\ccache\\v4.6\\ccache.exe" "C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\gcc\\x86_64\\11.3.0-14.0.3-10.0.0-msvcrt-r3\\mingw64\\bin\\gcc.exe" -o "module.PIL.Image.o" -c -std=c11 -fvisibility=hidden -fwrapv -pipe -fpartial-inlining -ftrack-macro-expansion=0 -Wno-deprecated-declarations -fno-var-tracking -Wno-misleading-indentation -fcompare-debug-second -O3 -D_WIN32_WINNT=0x0501 -D__NUITKA_NO_ASSERT__ -D_NUITKA_WINMAIN_ENTRY_POINT -D_NUITKA_STANDALONE -DMS_WIN64 -D_NUITKA_CONSTANTS_FROM_RESOURCE -D_NUITKA_FROZEN=155 -D_NUITKA_EXE -IC:\\Users\\a\\anaconda3\\envs\\harupan_nuitka\\include -I. -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\include -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\static_src "module.PIL.Image.c"' took 78.11 seconds
Nuitka-Scons:INFO: Slow C compilation detected, used 60s so far, scalability problem.
Nuitka-Scons:INFO: Running '"C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\ccache\\v4.6\\ccache.exe" "C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\gcc\\x86_64\\11.3.0-14.0.3-10.0.0-msvcrt-r3\\mingw64\\bin\\gcc.exe" -o "module.PIL.TiffImagePlugin.o" -c -std=c11 -fvisibility=hidden -fwrapv -pipe -fpartial-inlining -ftrack-macro-expansion=0 -Wno-deprecated-declarations -fno-var-tracking -Wno-misleading-indentation -fcompare-debug-second -O3 -D_WIN32_WINNT=0x0501 -D__NUITKA_NO_ASSERT__ -D_NUITKA_WINMAIN_ENTRY_POINT -D_NUITKA_STANDALONE -DMS_WIN64 -D_NUITKA_CONSTANTS_FROM_RESOURCE -D_NUITKA_FROZEN=155 -D_NUITKA_EXE -IC:\\Users\\a\\anaconda3\\envs\\harupan_nuitka\\include -I. -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\include -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\static_src "module.PIL.TiffImagePlugin.c"' took 81.07 seconds
Nuitka-Scons:INFO: Slow C compilation detected, used 60s so far, scalability problem.
Nuitka-Scons:INFO: Running '"C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\ccache\\v4.6\\ccache.exe" "C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\gcc\\x86_64\\11.3.0-14.0.3-10.0.0-msvcrt-r3\\mingw64\\bin\\gcc.exe" -o "module.numpy.lib.npyio.o" -c -std=c11 -fvisibility=hidden -fwrapv -pipe -fpartial-inlining -ftrack-macro-expansion=0 -Wno-deprecated-declarations -fno-var-tracking -Wno-misleading-indentation -fcompare-debug-second -O3 -D_WIN32_WINNT=0x0501 -D__NUITKA_NO_ASSERT__ -D_NUITKA_WINMAIN_ENTRY_POINT -D_NUITKA_STANDALONE -DMS_WIN64 -D_NUITKA_CONSTANTS_FROM_RESOURCE -D_NUITKA_FROZEN=155 -D_NUITKA_EXE -IC:\\Users\\a\\anaconda3\\envs\\harupan_nuitka\\include -I. -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\include -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\static_src "module.numpy.lib.npyio.c"' took 81.81 seconds
Nuitka-Scons:INFO: Slow C compilation detected, used 60s so far, scalability problem.
Nuitka-Scons:INFO: Running '"C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\ccache\\v4.6\\ccache.exe" "C:\\Users\\a\\AppData\\Local\\Nuitka\\Nuitka\\Cache\\downloads\\gcc\\x86_64\\11.3.0-14.0.3-10.0.0-msvcrt-r3\\mingw64\\bin\\gcc.exe" -o "module.numpy.ma.core.o" -c -std=c11 -fvisibility=hidden -fwrapv -pipe -fpartial-inlining -ftrack-macro-expansion=0 -Wno-deprecated-declarations -fno-var-tracking -Wno-misleading-indentation -fcompare-debug-second -O3 -D_WIN32_WINNT=0x0501 -D__NUITKA_NO_ASSERT__ -D_NUITKA_WINMAIN_ENTRY_POINT -D_NUITKA_STANDALONE -DMS_WIN64 -D_NUITKA_CONSTANTS_FROM_RESOURCE -D_NUITKA_FROZEN=155 -D_NUITKA_EXE -IC:\\Users\\a\\anaconda3\\envs\\harupan_nuitka\\include -I. -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\include -IC:\\Users\\a\\ANACON~1\\envs\\HARUPA~1\\lib\\SITE-P~1\\nuitka\\build\\static_src "module.numpy.ma.core.c"' took 96.54 seconds
Nuitka-Scons:INFO: Backend linking program with 176 files (no progress information available).
Nuitka-Scons:INFO: Compiled 704 C files using ccache.
Nuitka-Scons:INFO: Cached C files (using ccache) with result 'cache miss': 176
Nuitka-Options:INFO: Included data file 'data\harupan_svm.dat' due to specified data dir 'data' on command line.
Nuitka-Options:INFO: Included data file 'data\harupan_svm_220412.dat' due to specified data dir 'data' on command line.       
Nuitka-Options:INFO: Included data file 'data\templates2019.json' due to specified data dir 'data' on command line.
Nuitka-Options:INFO: Included data file 'data\templates2019_220412.json' due to specified data dir 'data' on command line.    
Nuitka-Options:INFO: Included data file 'data\templates2020.json' due to specified data dir 'data' on command line.
Nuitka-Options:INFO: Included data file 'data\templates2020_220412.json' due to specified data dir 'data' on command line.    
Nuitka-Options:INFO: Included data file 'data\templates2021.json' due to specified data dir 'data' on command line.
Nuitka-Options:INFO: Included data file 'data\templates2021_220412.json' due to specified data dir 'data' on command line.    
Nuitka-Plugins:INFO: tk-inter: Included 87 data files due to Tk needed for tkinter usage.
Nuitka-Plugins:INFO: tk-inter: Included 830 data files due to Tcl needed for tkinter usage.
Nuitka-Plugins:INFO: dll-files: Found 1 file DLLs from cv2 installation.
Nuitka-Plugins:WARNING: dll-files: DLL configuration by filename code for 'numpy' did not give a result. Either conditions are
Nuitka-Plugins:WARNING: missing, or this version of the module needs treatment added.
Nuitka-Plugins:INFO: dll-files: Found 1 file DLLs from numpy installation.
Nuitka will make use of Dependency Walker (https://dependencywalker.com) tool
to analyze the dependencies of Python extension modules.

Is it OK to download and put it in 'C:\Users\a\AppData\Local\Nuitka\Nuitka\Cache\downloads\depends\x86_64'.

Fully automatic, cached. Proceed and download? [Yes]/No :
Nuitka:INFO: Downloading 'https://dependencywalker.com/depends22_x64.zip'.
Nuitka:INFO: Extracting to 'C:\Users\a\AppData\Local\Nuitka\Nuitka\Cache\downloads\depends\x86_64\depends.exe'
Nuitka:INFO: Keeping build directory 'nuitka\harupan.build'.
Nuitka:INFO: Successfully created 'nuitka\harupan.dist\harupan.exe'.

(harupan_nuitka) C:\Users\a\work\OpenCV\harupan\harupan_data> 

生成フォルダ構成

(アプリルート)  
└─ nuitka
     ├─ harupan.build
     │   └─ ビルド生成物色々
     └─ harupan.dist    ... フォルダサイズ 209MB !
          ├─ harupan.exe
          ├─ data
          │   ├─ harupan_svm_220412.dat
          │   ├─ templates2021.json
          │   └─ ...
          └─ その他exe実行に必要なファイル、フォルダ
  • 途中、gcc、ccache、Dependency Walkerをダウンロードしていいかの確認が入りました。
    C:\Users\(ユーザ名)\AppData\Local\Nuitkaの下にダウンロードしたものが置かれていました。
    この後何度かexe生成をやったところでは、これらが再利用されているようでした。
  • 上記ダウンロードを含めて、10分ほどかかりました。
    再実行したときは、4分ほどの所要時間でした。少し待ち時間が出る感じです。
  • 生成されたフォルダサイズを見ると、Pyinstallerのときより大きくなっていました。
    • Pyinstaller: 157MB
    • Nuitka: 209MB

exe実行

harupan.distフォルダ内のharupan.exeをダブルクリックで実行。

動きました。

ただ、処理速度が速くなることを期待しましたが、特にそんなことはなく。

ここまで

今回はこんなところで。
Nuitkaを使ってみましたが、期待したような処理速度向上はなく、サイズも大きくなり、単一exe生成も失敗してしまったので、Pyinstallerで生成したexeを使うことにします。

このexeをGitHubで公開して一区切りかな。

OpenCVやってみる - 53. Pyinstallerでexe化

予定通り、春のパン祭り点数集計アプリのexe化をやってみます。

今回は基本的にはJupyterを使わない記事です。

スクリプト更新

今まで、tkinterを使ったGUIはJupyter上で試していただけなので、スクリプトに追加します。

スクリプトは、GitHubに上げました。
今までprivateリポジトリとして用意していましたが、今回ライセンスファイル(MITライセンス)も追加したうえでpublicにしてみました。

よかったらご覧ください。

GitHub - hubnoki/harupan: 春のパン祭り自動計算

以下は実行結果。コマンドプロンプトからの実行です。

(py37cv2) C:\work\OpenCV\harupan\harupan_data>python harupan.py
Canvas size: 490,314
Number of threads: 4
   <_MainThread(MainThread, started 27100)>
   <Thread(thread1, started 3120)>
   <Thread(thread2, started 17288)>
   <Thread(thread3, started 25248)>
Camera opened
Camera closed
Number of threads: 1
   <_MainThread(MainThread, started 27100)>

(py37cv2) C:\work\OpenCV\harupan\harupan_data>

結果は当然特に変わらず。

exe化

Pythonスクリプトをexe化する方法はいくつかあるようでした。

PyInstallerでPythonをexe化!ライブラリの比較、注意点も解説 | プログラミングを学ぶならトレノキャンプ(TRAINOCAMP)

Pyinstallerが標準的ぽいのでこれを使いたいですが、Nuitkaというのを見ると、実行ファイルの実行速度が速くなることがある、ということで、こちらも試してみたいと思います。

処理時間表示

コードに点数計算の処理時間を出すようにしておきました。

↓ 処理時間表示用のLabel追加

#### Calculation time ####
self.t_calc_time = tk.StringVar(value='     ms')
self.label_calc_time = tk.Label(self.frame_result, font=('Consolas', 10), textvariable=self.t_calc_time)
...
self.label_calc_time.grid(row=0, column=2, padx=(5,0))

↓ 処理時間計測

t = time.time()
score, img2 = calc_harupan(img, self.templates, self.svm)
t = time.time() - t
if not self.q_img2.full():
    self.t_calc_result.set(f'{score:2.1f} points')
    self.t_calc_time.set(f'{int(t*1000):4d} ms')

処理時間は出せました。
かなり遅いことが分かります。3秒近くかかっています。

conda環境再作成

続いてPyinstallerでのexe化を実施ですが、実際にやってみたところうまくいかず、
condaの環境を作り直しました。

結果的に、うまくいかなかったのはデータファイルの指定をしていなかったからでしたが、 環境を作り直して最小限のパッケージだけ含めることで、実行ファイルのサイズがだいぶ小さくなりました。

↓ conda環境作成

(py37cv2) C:\work\OpenCV\harupan\harupan_data>conda create -n harupan python=3.7
Collecting package metadata (current_repodata.json): done
Solving environment: done


==> WARNING: A newer version of conda exists. <==
  current version: 4.8.2
  latest version: 23.3.1

Please update conda by running

    $ conda update -n base -c defaults conda



## Package Plan ##

  environment location: C:\Users\a\anaconda3\envs\harupan

  added / updated specs:
    - python=3.7


The following packages will be downloaded:

    package                    |            build
    ---------------------------|-----------------
    ca-certificates-2023.01.10 |       haa95532_0         121 KB
    certifi-2022.12.7          |   py37haa95532_0         149 KB
    openssl-1.1.1t             |       h2bbff1b_0         5.5 MB
    pip-22.3.1                 |   py37haa95532_0         2.7 MB
    python-3.7.16              |       h6244533_0        17.2 MB
    setuptools-65.6.3          |   py37haa95532_0         1.1 MB
    sqlite-3.41.1              |       h2bbff1b_0         897 KB
    vc-14.2                    |       h21ff451_1           8 KB
    vs2015_runtime-14.27.29016 |       h5e58377_2        1007 KB
    wheel-0.38.4               |   py37haa95532_0          82 KB
    wincertstore-0.2           |   py37haa95532_2          15 KB
    ------------------------------------------------------------
                                           Total:        28.8 MB

The following NEW packages will be INSTALLED:

  ca-certificates    pkgs/main/win-64::ca-certificates-2023.01.10-haa95532_0
  certifi            pkgs/main/win-64::certifi-2022.12.7-py37haa95532_0
  openssl            pkgs/main/win-64::openssl-1.1.1t-h2bbff1b_0
  pip                pkgs/main/win-64::pip-22.3.1-py37haa95532_0
  python             pkgs/main/win-64::python-3.7.16-h6244533_0
  setuptools         pkgs/main/win-64::setuptools-65.6.3-py37haa95532_0
  sqlite             pkgs/main/win-64::sqlite-3.41.1-h2bbff1b_0
  vc                 pkgs/main/win-64::vc-14.2-h21ff451_1
  vs2015_runtime     pkgs/main/win-64::vs2015_runtime-14.27.29016-h5e58377_2
  wheel              pkgs/main/win-64::wheel-0.38.4-py37haa95532_0
  wincertstore       pkgs/main/win-64::wincertstore-0.2-py37haa95532_2


Proceed ([y]/n)?


Downloading and Extracting Packages
wincertstore-0.2     | 15 KB     | ################################################################################## | 100%  
pip-22.3.1           | 2.7 MB    | ################################################################################## | 100%  
openssl-1.1.1t       | 5.5 MB    | ################################################################################## | 100%  
sqlite-3.41.1        | 897 KB    | ################################################################################## | 100%  
ca-certificates-2023 | 121 KB    | ################################################################################## | 100%  
certifi-2022.12.7    | 149 KB    | ################################################################################## | 100%  
python-3.7.16        | 17.2 MB   | ################################################################################## | 100%  
vs2015_runtime-14.27 | 1007 KB   | ################################################################################## | 100%  
vc-14.2              | 8 KB      | ################################################################################## | 100%  
wheel-0.38.4         | 82 KB     | ################################################################################## | 100%  
setuptools-65.6.3    | 1.1 MB    | ################################################################################## | 100%  
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
#
# To activate this environment, use
#
#     $ conda activate harupan
#
# To deactivate an active environment, use
#
#     $ conda deactivate


(py37cv2) C:\work\OpenCV\harupan\harupan_data>conda activate harupan

(harupan) C:\work\OpenCV\harupan\harupan_data>conda list
# packages in environment at C:\Users\a\anaconda3\envs\harupan:
#
# Name                    Version                   Build  Channel
ca-certificates           2023.01.10           haa95532_0
certifi                   2022.12.7        py37haa95532_0
openssl                   1.1.1t               h2bbff1b_0
pip                       22.3.1           py37haa95532_0
python                    3.7.16               h6244533_0
setuptools                65.6.3           py37haa95532_0
sqlite                    3.41.1               h2bbff1b_0
vc                        14.2                 h21ff451_1
vs2015_runtime            14.27.29016          h5e58377_2
wheel                     0.38.4           py37haa95532_0
wincertstore              0.2              py37haa95532_2

↓ 必要なパッケージインストール(opencv-python, Pillow)

(harupan) C:\work\OpenCV\harupan\harupan_data>pip install opencv-python
Collecting opencv-python
  Using cached opencv_python-4.7.0.72-cp37-abi3-win_amd64.whl (38.2 MB)
Collecting numpy>=1.17.0
  Downloading numpy-1.21.6-cp37-cp37m-win_amd64.whl (14.0 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 14.0/14.0 MB 5.5 MB/s eta 0:00:00
Installing collected packages: numpy, opencv-python
Successfully installed numpy-1.21.6 opencv-python-4.7.0.72

(harupan) C:\work\OpenCV\harupan\harupan_data>pip install Pillow
Collecting Pillow
  Downloading Pillow-9.5.0-cp37-cp37m-win_amd64.whl (2.5 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.5/2.5 MB 5.8 MB/s eta 0:00:00
Installing collected packages: Pillow
Successfully installed Pillow-9.5.0

Pythonスクリプト動作確認

(harupan) C:\work\OpenCV\harupan\harupan_data>python harupan.py  
Canvas size: 490,314
Number of threads: 4
   <_MainThread(MainThread, started 29612)>
   <Thread(thread1, started 28132)>
   <Thread(thread2, started 6116)>
   <Thread(thread3, started 19860)>
Camera opened
harupan.py:257: DeprecationWarning: `np.float` is a deprecated alias for the builtin `float`. To silence this warning, use `float` by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use `np.float64` here.
Deprecated in NumPy 1.20; for more details and guidance: https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations     
  return 1.0 - np.float(np.count_nonzero(xor_img)) / (img1.shape[0]*img2.shape[1])
Camera closed
Number of threads: 1
   <_MainThread(MainThread, started 29612)>

↓ Pyinstallerインストール

(harupan) C:\work\OpenCV\harupan\harupan_data>pip install pyinstaller
Collecting pyinstaller
  Downloading pyinstaller-5.10.0-py3-none-win_amd64.whl (1.3 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.3/1.3 MB 5.4 MB/s eta 0:00:00
Collecting pefile>=2022.5.30
  Using cached pefile-2023.2.7-py3-none-any.whl (71 kB)
Collecting importlib-metadata>=1.4
  Downloading importlib_metadata-6.3.0-py3-none-any.whl (22 kB)
Collecting altgraph
  Using cached altgraph-0.17.3-py2.py3-none-any.whl (21 kB)
Collecting pywin32-ctypes>=0.2.0
  Downloading pywin32_ctypes-0.2.0-py2.py3-none-any.whl (28 kB)
Collecting pyinstaller-hooks-contrib>=2021.4
  Using cached pyinstaller_hooks_contrib-2023.2-py2.py3-none-any.whl (261 kB)
Requirement already satisfied: setuptools>=42.0.0 in c:\users\a\anaconda3\envs\harupan\lib\site-packages (from pyinstaller) (65.6.3)
Collecting zipp>=0.5
  Downloading zipp-3.15.0-py3-none-any.whl (6.8 kB)
Collecting typing-extensions>=3.6.4
  Downloading typing_extensions-4.5.0-py3-none-any.whl (27 kB)
Installing collected packages: pywin32-ctypes, altgraph, zipp, typing-extensions, pyinstaller-hooks-contrib, pefile, importlib-metadata, pyinstaller
Successfully installed altgraph-0.17.3 importlib-metadata-6.3.0 pefile-2023.2.7 pyinstaller-5.10.0 pyinstaller-hooks-contrib-2023.2 pywin32-ctypes-0.2.0 typing-extensions-4.5.0 zipp-3.15.0

(harupan) C:\work\OpenCV\harupan\harupan_data>pip list
Package                   Version
------------------------- ---------
altgraph                  0.17.3
certifi                   2022.12.7
importlib-metadata        6.3.0
numpy                     1.21.6
opencv-python             4.7.0.72
pefile                    2023.2.7
Pillow                    9.5.0
pip                       22.3.1
pyinstaller               5.10.0
pyinstaller-hooks-contrib 2023.2
pywin32-ctypes            0.2.0
setuptools                65.6.3
typing_extensions         4.5.0
wheel                     0.38.4
wincertstore              0.2
zipp                      3.15.0

(harupan) C:\work\OpenCV\harupan\harupan_data>

Pyinstallerでexe化

今回のアプリは、SVMと点数文字テンプレートのデータファイルを使っています。
そのため、以下の変更等が必要でした。

  • Pyinstaller実行時に--add-dataオプションでデータファイルを指定 → exe生成時にデータファイルがコピーされる
  • データファイルをexeと同じパスの下に置くことができなかったので、dataフォルダを作ってその中に配置、対応してPythonスクリプトを変更

変更内容

  • ファイル配置変更
(アプリルート)  
├─ harupan.py  
├─ harupan_svm_220412.dat  
├─ templates2021.json

(アプリルート)  
├─ harupan.py  
├─ data  
│   ├─ harupan_svm_220412.dat  
│   └─ templates2021.json
...
    svm_data='harupan_svm_220412.dat'
    template_data='templates2021.json'
...

...
    svm_data='data/harupan_svm_220412.dat'
    template_data='data/templates2021.json'
...

Pyinstaller実行

Pyinstallerは以下のようにコマンドプロンプトから実行。
Windows形式のパスでの記述になります。

--add-dataオプションの使い方は以下参照。

Windowsでは、

データファイルのパス;exeでの配置ディレクトリ(exeファイルからの相対パス)

の形で指定します。

【超簡単】Python プログラムを pyinstaller で EXE化しよう | 趣味や仕事に役立つ初心者DIYプログラミング入門

Python pyinstallerで実行ファイル化(exe化) - IT技術で仕事を減らしたい!

pyinstaller harupan.py --add-data data\harupan_svm_220412.dat;data --add-data data\templates2021.json;data

実行結果。とりあえず全部載せちゃいます。長くなりますがご容赦。

(harupan) C:\work\OpenCV\harupan\harupan_data>pyinstaller harupan.py --add-data data\harupan_svm_220412.dat;data --add-data data\templates2021.json;data                           
588 INFO: PyInstaller: 5.10.0
588 INFO: Python: 3.7.16 (conda)
588 INFO: Platform: Windows-10-10.0.22621-SP0
590 INFO: wrote C:\work\OpenCV\harupan\harupan_data\harupan.spec
594 INFO: UPX is not available.
603 INFO: Extending PYTHONPATH with paths
['C:\\Users\\a\\work\\OpenCV\\harupan\\harupan_data']
939 INFO: Appending 'datas' from .spec
940 INFO: checking Analysis
940 INFO: Building Analysis because Analysis-00.toc is non existent
941 INFO: Initializing module dependency graph...
944 INFO: Caching module graph hooks...
970 INFO: Analyzing base_library.zip ...
3186 INFO: Loading module hook 'hook-heapq.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
3285 INFO: Loading module hook 'hook-encodings.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
4229 INFO: Loading module hook 'hook-pickle.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
5813 INFO: Caching module dependency graph...
5967 INFO: running Analysis Analysis-00.toc
5974 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
  required by C:\Users\a\anaconda3\envs\harupan\python.exe
6283 INFO: Analyzing C:\work\OpenCV\harupan\harupan_data\harupan.py
6323 INFO: Loading module hook 'hook-cv2.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\_pyinstaller_hooks_contrib\\hooks\\stdhooks'...
7411 INFO: Loading module hook 'hook-numpy.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
7514 WARNING: Conda distribution 'numpy', dependency of 'numpy', was not found. If you installed this distribution with pip then you may ignore this warning.
8097 INFO: Loading module hook 'hook-difflib.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
8215 INFO: Loading module hook 'hook-platform.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
8476 INFO: Loading module hook 'hook-sysconfig.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
9666 INFO: Loading module hook 'hook-PIL.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
9740 INFO: Loading module hook 'hook-PIL.Image.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
10829 INFO: Loading module hook 'hook-PIL.ImageFilter.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
10887 INFO: Processing module hooks...
10966 WARNING: Hidden import "six" not found!
11201 INFO: Loading module hook 'hook-PIL.SpiderImagePlugin.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
11243 INFO: Loading module hook 'hook-_tkinter.py' from 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks'...
11245 INFO: checking Tree
11245 INFO: Building Tree because Tree-00.toc is non existent
11245 INFO: Building Tree Tree-00.toc
11302 INFO: checking Tree
11303 INFO: Building Tree because Tree-01.toc is non existent
11303 INFO: Building Tree Tree-01.toc
11310 INFO: checking Tree
11310 INFO: Building Tree because Tree-02.toc is non existent
11311 INFO: Building Tree Tree-02.toc
11342 INFO: Looking for ctypes DLLs
11348 INFO: Analyzing run-time hooks ...
11352 INFO: Including run-time hook 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py'
11356 INFO: Including run-time hook 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py'
11360 INFO: Including run-time hook 'C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py'
11388 INFO: Looking for dynamic libraries
13132 INFO: Looking for eggs
13132 INFO: Using Python library C:\Users\a\anaconda3\envs\harupan\python37.dll
13133 INFO: Found binding redirects:
[]
13142 INFO: Warnings written to C:\work\OpenCV\harupan\harupan_data\build\harupan\warn-harupan.txt
13208 INFO: Graph cross-reference written to C:\work\OpenCV\harupan\harupan_data\build\harupan\xref-harupan.html
13279 INFO: checking PYZ
13280 INFO: Building PYZ because PYZ-00.toc is non existent
13280 INFO: Building PYZ (ZlibArchive) C:\work\OpenCV\harupan\harupan_data\build\harupan\PYZ-00.pyz
13940 INFO: Building PYZ (ZlibArchive) C:\work\OpenCV\harupan\harupan_data\build\harupan\PYZ-00.pyz completed successfully.
13954 INFO: checking PKG
13955 INFO: Building PKG because PKG-00.toc is non existent
13955 INFO: Building PKG (CArchive) harupan.pkg
13988 INFO: Building PKG (CArchive) harupan.pkg completed successfully.
13990 INFO: Bootloader C:\Users\a\anaconda3\envs\harupan\lib\site-packages\PyInstaller\bootloader\Windows-64bit-intel\run.exe
13991 INFO: checking EXE
13991 INFO: Building EXE because EXE-00.toc is non existent
13991 INFO: Building EXE from EXE-00.toc
13992 INFO: Copying bootloader EXE to C:\work\OpenCV\harupan\harupan_data\build\harupan\harupan.exe.notanexecutable
14044 INFO: Copying icon to EXE
14046 INFO: Copying icons from ['C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico']
14079 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes
14080 INFO: Writing RT_ICON 1 resource with 3752 bytes
14080 INFO: Writing RT_ICON 2 resource with 2216 bytes
14080 INFO: Writing RT_ICON 3 resource with 1384 bytes
14081 INFO: Writing RT_ICON 4 resource with 37019 bytes
14081 INFO: Writing RT_ICON 5 resource with 9640 bytes
14081 INFO: Writing RT_ICON 6 resource with 4264 bytes
14082 INFO: Writing RT_ICON 7 resource with 1128 bytes
14087 INFO: Copying 0 resources to EXE
14087 INFO: Embedding manifest in EXE
14088 INFO: Updating manifest in C:\work\OpenCV\harupan\harupan_data\build\harupan\harupan.exe.notanexecutable
14120 INFO: Updating resource type 24 name 1 language 0
14124 INFO: Appending PKG archive to EXE
14132 INFO: Fixing EXE headers
14293 INFO: Building EXE from EXE-00.toc completed successfully.
14298 INFO: checking COLLECT
14299 INFO: Building COLLECT because COLLECT-00.toc is non existent
14299 INFO: Building COLLECT COLLECT-00.toc
17707 INFO: Building COLLECT COLLECT-00.toc completed successfully.

(harupan) C:\work\OpenCV\harupan\harupan_data>

以下のように生成物ができました。
distフォルダ内のhaupan.exeをダブルクリックすると、アプリが起動しました。
コマンドプロンプトも一緒に起動していて、print()でのコンソール出力が表示されています。

(アプリルート)  
├─ build
│   └─ harupan
│        └─ 中間生成物色々
└─ dist
     └─ harupan    ... フォルダサイズ 157MB !
          ├─ harupan.exe
          ├─ data
          │   ├─ harupan_svm_220412.dat  
          │   └─ templates2021.json
          └─ その他exe実行に必要なファイル、フォルダ

その他オプション指定

  • --noconsoleで、コマンドプロンプト起動なしにする
  • --onefileで1つのexeファイルにまとめる
    ただし、今回はデータファイルも含むので、下記に記載のような対応が必要。

PyInstallerで実行ファイルにリソースを埋め込み - Qiita

######################################################
# main
######################################################
def resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)

def main():
    root = tk.Tk()
    svm_data = resource_path('data/harupan_svm_220412.dat')
    template_data = resource_path('data/templates2021.json')
    app = harupan_gui(master=root, img_queue_size=1, svm_data=svm_data, template_data=template_data)
    app.mainloop()

Pyinstaller実行結果。
前の生成結果を残すために、--distpathオプションで出力先をdist2に変更しています。

(harupan) C:\work\OpenCV\harupan\harupan_data>pyinstaller harupan.py --add-data data\harupan_svm_220412.dat;data --add-data data\templates2021.json;data --noconsole --onefile --distpath dist2 
1414 INFO: PyInstaller: 5.10.0
1415 INFO: Python: 3.7.16 (conda)
1416 INFO: Platform: Windows-10-10.0.22621-SP0
1418 INFO: wrote C:\work\OpenCV\harupan\harupan_data\harupan.spec
1426 INFO: UPX is not available.
1443 INFO: Extending PYTHONPATH with paths
['C:\\Users\\a\\work\\OpenCV\\harupan\\harupan_data']
2211 INFO: Appending 'datas' from .spec
2212 INFO: checking Analysis
2365 INFO: checking PYZ
2406 INFO: checking PKG
2411 INFO: Building because toc changed
2412 INFO: Building PKG (CArchive) harupan.pkg
23543 INFO: Building PKG (CArchive) harupan.pkg completed successfully.
23572 INFO: Bootloader C:\Users\a\anaconda3\envs\harupan\lib\site-packages\PyInstaller\bootloader\Windows-64bit-intel\runw.exe
23572 INFO: checking EXE
23585 INFO: Rebuilding EXE-00.toc because harupan.exe missing
23585 INFO: Building EXE from EXE-00.toc
23586 INFO: Copying bootloader EXE to C:\work\OpenCV\harupan\harupan_data\dist2\harupan.exe.notanexecutable       
23634 INFO: Copying icon to EXE
23637 INFO: Copying icons from ['C:\\Users\\a\\anaconda3\\envs\\harupan\\lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-windowed.ico']
23670 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes
23670 INFO: Writing RT_ICON 1 resource with 3752 bytes
23670 INFO: Writing RT_ICON 2 resource with 2216 bytes
23670 INFO: Writing RT_ICON 3 resource with 1384 bytes
23670 INFO: Writing RT_ICON 4 resource with 38188 bytes
23671 INFO: Writing RT_ICON 5 resource with 9640 bytes
23671 INFO: Writing RT_ICON 6 resource with 4264 bytes
23671 INFO: Writing RT_ICON 7 resource with 1128 bytes
23675 INFO: Copying 0 resources to EXE
23675 INFO: Embedding manifest in EXE
23677 INFO: Updating manifest in C:\work\OpenCV\harupan\harupan_data\dist2\harupan.exe.notanexecutable
23714 INFO: Updating resource type 24 name 1 language 0
23720 INFO: Appending PKG archive to EXE
23779 INFO: Fixing EXE headers
24343 INFO: Building EXE from EXE-00.toc completed successfully.

(harupan) C:\work\OpenCV\harupan\harupan_data>
(アプリルート)  
└─ dist2
     └─ harupan    ... フォルダサイズ 56.7MB
          └─ harupan.exe

生成物がexeファイルだけになりました。
また、全体のデータサイズも小さくなりました。

ただ、exeをダブルクリックした後実際に起動するまでが少し長くなりました。
圧縮ファイルを展開したりで時間がかかるのか?

処理時間再確認

作ったexeで、実際に点数計算処理をやってみました。

が、特にexe化したからといって処理が速くなったりはしませんでした。

ここまで

今回はここまでにします。

次回は、Nuitkaでのexe化をやってみたいと思います。

OpenCVやってみる - 52. GUI調整

今回は、春のパン祭り点数集計GUIの微調整をしていきます。

調整項目

主に調整したいのは、

  • 画像更新停止ボタン追加
    ある程度点数認識できたら間違っている部分を目視で確認、点数修正、という使い方を想定しているので。
  • ウィンドウサイズに応じた表示サイズ調整
    目視確認しやすいウィンドウサイズに調整したいので。

の2点。

あと細かい調整。

実行結果

コードが長いので、先に結果のgifを出します。

コード

今回は変更したコード全体と実行結果だけ掲載します。詳細の解説は省略で…

from harupan_data.harupan import *
import tkinter as tk
import cv2
from PIL import Image, ImageOps, ImageTk

import queue
import threading
class harupan_gui(tk.Frame):
    TEXT_CONNECT = 'Connect   '
    TEXT_DISCONNECT = 'Disconnect'
    TEXT_STOP = 'Stop  '
    TEXT_RESUME = 'Resume'

    def __init__(self, master=None, img_queue_size=1, svm_data='harupan_data/harupan_svm_220412.dat', template_data='harupan_data/templates2021.json'):
        super().__init__(master)

        self.cap = cv2.VideoCapture()
        self.open_params = (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 10000, cv2.CAP_PROP_READ_TIMEOUT_MSEC, 5000)
        self.svm = load_svm(svm_data)
        self.templates = load_templates(template_data)

        #### Main window settings ####
        self.master.title('Harupan App')
        self.master.geometry('500x400')
        self.master.protocol('WM_DELETE_WINDOW', self.cleanup_app)
        self.master.bind('<Configure>', self.update_canvas_size)

        #### Sub frames ####
        self.frame_connection = tk.Frame(self)
        self.frame_log = tk.Frame(self.frame_connection, width=120, height=30)
        self.frame_log.propagate(False)
        self.frame_canvas = tk.Frame(self)
        self.frame_canvas.config(relief='ridge', bd=5)
        self.frame_result = tk.Frame(self)

        #### Entries for connection information ####
        self.t_ip = tk.StringVar(value='192.168.1.7')
        self.t_port = tk.StringVar(value='4747')
        self.entry_ip = tk.Entry(self.frame_connection, textvariable=self.t_ip)
        self.entry_port = tk.Entry(self.frame_connection, textvariable=self.t_port)

        #### Connect button ####
        self.t_connect = tk.StringVar(value=self.TEXT_CONNECT)
        self.button_connect = tk.Button(self.frame_connection, textvariable=self.t_connect)
        self.button_connect.bind('<Button-1>', self.event_connect)

        #### Connection log ####
        self.t_log = tk.StringVar()
        self.label_log = tk.Label(self.frame_log, textvariable=self.t_log)

        #### Image canvas ####
        self.canvas_image = tk.Canvas(self.frame_canvas, bg='white')
        self.disp_img = None

        #### Label for calculation result ####
        self.t_calc_result = tk.StringVar(value=' 0 points')
        self.label_points = tk.Label(self.frame_result, bg='black', fg='green', font=('Consolas', 20), textvariable=self.t_calc_result)

        #### Stop button ####
        self.t_stop = tk.StringVar(value=self.TEXT_STOP)
        self.button_stop = tk.Button(self.frame_result, textvariable=self.t_stop)
        self.button_stop.bind('<Button-1>', self.event_stop_button)

        #### Place widgets ####
        self.pack(expand=True, fill='both')

        self.frame_connection.pack()
        self.frame_canvas.pack(expand=True, fill='both')
        self.frame_result.pack()

        self.entry_ip.grid(row=0, column=0)
        self.entry_port.grid(row=1, column=0)
        self.button_connect.grid(row=0, column=1, rowspan=2, padx=(5,0))
        self.frame_log.grid(row=0, column=2, rowspan=2, padx=(5,0))
        self.label_log.pack(fill='both')

        self.canvas_image.pack(expand=True, fill='both')

        self.label_points.grid(row=0, column=0)
        self.button_stop.grid(row=0, column=1, padx=(5,0))

        self.frame_canvas.update()
        self.w, self.h = self.canvas_image.winfo_width(), self.canvas_image.winfo_height()
        print(f'Canvas size: {self.w},{self.h}')

        #### Start internal threads ####
        self.q_connect = queue.Queue(maxsize=0)
        self.q_img = queue.Queue(maxsize=1)
        self.q_img2 = queue.Queue(maxsize=img_queue_size)
        self.run_flag = True
        self.thread1 = threading.Thread(target=self.update_image, name='thread1')
        self.thread2 = threading.Thread(target=self.cap_process, name='thread2')
        self.thread3 = threading.Thread(target=self.calc_process, name='thread3')
        self.thread1.start()
        self.thread2.start()
        self.thread3.start()
        print(f'Number of threads: {threading.active_count()}')
        for th in threading.enumerate():
            print('  ', th)

    def event_connect(self, e):
        self.t_log.set('')
        if(self.t_connect.get() == self.TEXT_CONNECT):
            url = f'http://{self.t_ip.get()}:{self.t_port.get()}/video'
            self.q_connect.put(url)
            self.t_connect.set(self.TEXT_DISCONNECT)
        else:
            self.q_connect.put(None)
            self.t_connect.set(self.TEXT_CONNECT)

    def update_image(self):
        while self.run_flag:
            val, img = self.q_img2.get()
            if not val:
                continue
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            img = Image.fromarray(img)
            img = ImageOps.pad(img, (self.w,self.h))
            self.disp_img = ImageTk.PhotoImage(image=img)
            self.canvas_image.create_image(self.w/2,self.h/2,image=self.disp_img)

    def _print_log(self, mes):
        print(mes)
        self.t_log.set(mes)

    def cap_process(self):
        while self.run_flag:
            if not self.q_connect.empty():
                url = self.q_connect.get()
                if url == None:
                    self.cap.release()
                    self._print_log('Camera closed')
                elif self.cap.open(url, cv2.CAP_FFMPEG, self.open_params):
                    self._print_log('Camera opened')
                else:
                    self._print_log('Camera open failed')
                    self.t_connect.set(self.TEXT_CONNECT)
            elif self.cap.isOpened():
                ret, img = self.cap.read()
                if not ret:
                    self._print_log('Can\'t receive frame')
                    self.cap.release()
                    self.t_connect.set(self.TEXT_CONNECT)
                elif not self.q_img.full():
                    self.q_img.put((True, img))
    
    def calc_process(self):
        while self.run_flag:
            val, img = self.q_img.get()
            if not val:
                self.q_img2.put((False, None))
                continue
            if self.t_stop.get() == self.TEXT_STOP:
                score, img2 = calc_harupan(img, self.templates, self.svm)
                self.t_calc_result.set(f'{score:2.1f} points')
                if not self.q_img2.full():
                    self.q_img2.put((True, img2))

    def event_stop_button(self, e):
        s = self.TEXT_RESUME if self.t_stop.get() == self.TEXT_STOP else self.TEXT_STOP
        self.t_stop.set(s)

    def update_canvas_size(self, e):
        self.w, self.h = self.canvas_image.winfo_width(), self.canvas_image.winfo_height()

    def cleanup_app(self):
        self.run_flag = False

        # Put dummy data to finish thread1(update_image()), thread3(calc_process())
        if self.q_img.empty():
            self.q_img.put((False, None))
        if self.q_img2.empty():
            self.q_img2.put((False, None))

        self.thread1.join(timeout=10)
        self.thread2.join(timeout=10)
        self.thread3.join(timeout=10)

        print(f'Number of threads: {threading.active_count()}')
        for th in threading.enumerate():
            print('  ', th)

        if self.cap.isOpened():
            self.cap.release()

        self.master.destroy()

root = tk.Tk()
app = harupan_gui(root)
app.mainloop()
Canvas size: 490,314
Number of threads: 8
   <_MainThread(MainThread, started 23564)>
   <Thread(Thread-2, started daemon 14256)>
   <Heartbeat(Thread-3, started daemon 25136)>
   <HistorySavingThread(IPythonHistorySavingThread, started 19276)>
   <ParentPollerWindows(Thread-1, started daemon 5052)>
   <Thread(thread1, started 22068)>
   <Thread(thread2, started 16572)>
   <Thread(thread3, started 21688)>
Camera opened
Camera closed
Number of threads: 5
   <_MainThread(MainThread, started 23564)>
   <Thread(Thread-2, started daemon 14256)>
   <Heartbeat(Thread-3, started daemon 25136)>
   <HistorySavingThread(IPythonHistorySavingThread, started 19276)>
   <ParentPollerWindows(Thread-1, started daemon 5052)>

tkinterウィジェットの配置メモ。

以上

まだGUIで直したいところは色々ありますが、きりがないのでひとまずこれでフィックスで。

次回はexe化になると思われます。

その他参考

コラム - ゼロから歩くPythonの道 | 第21回 tkinterでフォントと文字サイズ、色を変更する方法|CTC教育サービス 研修/トレーニング

【Python/Tkinter】Label(ラベル)の使い方:文字フォント・サイズ・色・配置の設定 | OFFICE54

OpenCVやってみる - 51. VideoCaptureタイムアウト設定

春のパン祭り点数集計GUIの調整をしています。

調整項目の一つとして、VideoCaptureの接続タイムアウト設定がありました。

iPhoneに入れたDroidCamアプリ経由で画像を取得する場合、割り当てられているIPアドレスが場合によって変わるので、IP設定を間違えて接続しようとしてしまうことがあり。

そうすると、前のコードでは、1分近く接続待ちしてしまうことがありました。
その間アプリを落とすこともできないと。

これを解決すべく、接続のタイムアウト設定のしかたを調べてみましたが、これで1記事にしておきます。

なかなかいい情報が見つからず、大変だったので…

結論

以下のコードで、VideoCaptureのオープン時にパラメータを設定すれば、接続タイムアウト時間を設定できるようでした。

ただし、なぜか最大5秒ぐらいまでしか設定できない…

import cv2
cap = cv2.VideoCapture()
timeout_ms = 2000
cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, timeout_ms))
False

上記では、DroidCamを起動しない状態でVideoCaptureのオープンを試みています。
結果、設定した時間でタイムアウトしています。

説明

open()関数の引数は、

  • 第2引数: apiPreferenceで、どのバックエンドを使いたいかを指定します。
    今回はFFmpegとしていますが、デフォルトでFFmpegにはなるようでした。ただし、
    • タイムアウト設定のためには第3引数の指定が必要
    • いずれもオプション引数にはなっていない

    ということから、この引数を入れておく必要があるようです。

  • 第3引数: paramsで、プロパティ名と設定値のペアを与えます。複数プロパティの設定も可能。

となっています。

OpenCV: cv::VideoCapture Class Reference - open

プロパティは、以下にリストアップされています。

OpenCV: Flags for video I/O - VideoCaptureProperties

今回使ったのはCAP_PROP_OPEN_TIMEOUT_MSECで、以下のように説明があります。

  • video captureオープン時のタイムアウト時間(ms単位)
  • FFmpegとGstreamerバックエンドでのみ有効
  • open-only : open()関数かVideoCaptureのコンストラクタでのみ設定可能(他のプロパティはだいたいset()で随時設定可能)

下記は、VideoCaptureのコンストラクタで設定した例です。

cap = cv2.VideoCapture('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, timeout_ms))
print(cap)
print(cap.isOpened())
< cv2.VideoCapture 000001C956F6A7B0>
False

時間測定

実際にどれぐらいのタイムアウト時間になっているかの確認です。

Jupyterでは%time%timeitのマジックコマンドで簡単に時間測定できます。

Jupyter Notebookでセルの実行時間をはかるなら%%timeを使おうって話 - EnsekiTT Blog

タイムアウト設定しない場合と、いくつかの設定値で設定した場合で確認しました。

%time cap.open('http://192.168.1.13:4747/video')
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 1000))
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 2000))
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 3000))
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 4000))
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000))
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 6000))
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 7000))
%time cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 10000))
Wall time: 13.5 s
Wall time: 1.09 s
Wall time: 2.05 s
Wall time: 3.06 s
Wall time: 4.05 s
Wall time: 5.04 s
Wall time: 5.56 s
Wall time: 5.46 s
Wall time: 5.6 s





False

なぜかタイムアウト時間が5秒ちょっとで頭打ちになってしまう。

特に設定しないと、13秒になっています。
前回のGUIでやった感じだと、もっと時間がかかってた気がしますが…

読み出しのタイムアウト

同様に、CAP_PROP_READ_TIMEOUT_MSECプロパティで、read()タイムアウトも設定してみます。

一旦DroidCamを立ち上げた状態でVideoCaptureをオープンしてread()を繰り返し実行、その後にDroidCamを落とすことで、read()ができなくなる状況を作ります。

また、%timeだと、全部のread()の実行時間が表示されて分かりにくくなるので、timeitモジュールを使って時間計測し、最後のread()の時間だけ表示します。

timeitで実行時間計測 | Python Snippets

import timeit

params = (cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 2000, cv2.CAP_PROP_READ_TIMEOUT_MSEC, 2000)
cap.open('http://192.168.1.13:4747/video', cv2.CAP_FFMPEG, params)
ret = True
t = 0

def cap_read():
    global ret
    if cap.isOpened():
        ret, img = cap.read()
    else:
        print('VideoCapture is closed')

while ret:
    t = timeit.timeit('cap_read()', globals=globals(), number=1)

print(f'Last read() time: {t:.2f} sec')
Last read() time: 0.08 sec

2秒のつもりでタイムアウト設定しましたが、ずっと短い時間でread()が終わってしまいました…

タイムアウト設定なしだと、

cap.open('http://192.168.1.13:4747/video')

while ret:
    t = timeit.timeit('cap_read()', globals=globals(), number=1)

print(f'Last read() time: {t:.2f} sec')
Last read() time: 0.08 sec

特にタイムアウト設定した場合と変わりません。

これではタイムアウト発生条件にならないのか?

以上

今回はここまで。

次回は、今回のタイムアウト設定も含めて、春のパン祭り点数集計GUIの調整をやっていきます。

補足

VideoCaptureのプロパティは、OpenCVのバージョンによってときどき変わるようでした。

今回の環境では、4.70を使っています。

cv2.__version__
'4.7.0'

その他参考