勉強しないとな~blog

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

OpenCVやってみる - 50. 点数集計処理組み合わせ

今回は、今まで作ったGUIに、点数集計処理を組み合わせていきます。

処理組み合わせ

去年の点数集計処理と、今回のGUIを組み合わせるだけです。

前回はtkinter.Frameを継承したアプリを一度作り、その後で関数をオーバーライドしたりしましたが、今回は最終的な形のものをまとめて作ります。

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'

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

        self.cap = cv2.VideoCapture()
        self.svm = load_svm(svm_data)
        self.templates = load_templates(template_data)

        self.master.title('Harupan App')
        self.master.geometry('500x400')

        #### 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, textvariable=self.t_ip)
        self.entry_port = tk.Entry(self, textvariable=self.t_port)
        self.entry_ip.grid(row=0, column=0)
        self.entry_port.grid(row=1, column=0)

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

        #### Image canvas ####
        self.canvas_image = tk.Canvas(self, bg='white')
        self.canvas_image.grid(row=2,column=0, columnspan=2)
        self.update()
        self.w, self.h = self.canvas_image.winfo_width(), self.canvas_image.winfo_height()
        print(f'Canvas size: {self.w},{self.h}')
        self.disp_img = None

        self.master.bind('<KeyPress>', self.key_handler)
        self.master.protocol('WM_DELETE_WINDOW', self.cleanup_app)

        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):
        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 cap_process(self):
        while self.run_flag:
            if not self.q_connect.empty():
                url = self.q_connect.get()
                if url == None:
                    self.cap.release()
                    print('Camera closed')
                elif self.cap.open(url):
                    print('Camera opened')
                else:
                    print('Camera open failed')
            elif self.cap.isOpened():
                ret, img = self.cap.read()
                if not ret:
                    print('Can''t receive frame')
                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
            score, img2 = calc_harupan(img, self.templates, self.svm)
            score_text = str(score) + ' points'
            score_area = np.zeros((50, img2.shape[1], 3), 'uint8')
            score_area = cv2.putText(score_area, score_text, (0,45), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,0), 3)
            img2 = np.vstack((img2, score_area))
            if not self.q_img2.full():
                self.q_img2.put((True, img2))
    
    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()


    def key_handler(self, e):
        print(e.keysym)
root = tk.Tk()
app = harupan_gui(root)
app.mainloop()
Canvas size: 382,269
Number of threads: 8
   <_MainThread(MainThread, started 20972)>
   <Thread(Thread-2, started daemon 10616)>
   <Heartbeat(Thread-3, started daemon 6208)>
   <HistorySavingThread(IPythonHistorySavingThread, started 18536)>
   <ParentPollerWindows(Thread-1, started daemon 17772)>
   <Thread(thread1, started 5064)>
   <Thread(thread2, started 22324)>
   <Thread(thread3, started 17676)>
F7
BackSpace
1
4
Camera opened
Camera closed
Number of threads: 5
   <_MainThread(MainThread, started 20972)>
   <Thread(Thread-2, started daemon 10616)>
   <Heartbeat(Thread-3, started daemon 6208)>
   <HistorySavingThread(IPythonHistorySavingThread, started 18536)>
   <ParentPollerWindows(Thread-1, started daemon 17772)>

キー入力のうち、F7はキャプチャツールの録画開始キー、あとはIPアドレスが変わったのでその入力。

更新レートは2~3秒と、あまりよろしくない。

調整した点がいくつか。

  • ウィンドウサイズを500x300 → 500x400に変更
    • 表示画像の下側が切れてしまっていたため
    • 固定サイズではなくうまく表示する方法があればいいですが、これは後で再検討で。
  • 点数計算処理は、新しいスレッド処理として追加
    • 最初は画像取得のスレッド内で実施しましたが、そうすると、カメラの動きに対して追従性が悪い。
      cv2.VideoCaptureの中のバッファに画像データがある程度溜め込まれるためかと推定。
    • 画像取得のスレッドは、ひたすらread()関数を呼んで、前のフレームの点数計算処理が終わっていなければ画像を捨てる、という仕事としました。
    • これにより、処理対象の画像が、リアルタイムに取得したものになり、追従性が改善しました。
  • update_image()関数のループの中で、q_img2.get()を呼ぶ前の、q_img2が空でないかどうか(q_img2.empty()Falseかどうか)のチェックを削除
    • 前回やったところでは、この確認をしないと、アプリ終了時にupdate_image()関数のスレッドがq_imgのデータ待ちのまま終了できなかったので。
    • ただ、今回これで動かしてみると、点数集計処理が全然終わらない…
    • おそらく、このスレッドがq_img2.empty()を確認するだけの処理を繰り返して、CPU時間がそちらに取られてしまったのかと。
    • これを外すと、きちんと点数集計処理が完了するようになりました。
    • ただし、アプリ終了時にスレッドを落とせるよう、q_img, q_img2にダミーデータを入れる対応をしています。
  • アプリ終了時の後処理を定義して、Tk().protocol()で登録

ここまで

GUIで春のパン祭り点数集計ができるようになりました。

使ってみて、GUIでもう少し改善したいところがあるので、次回それをやっておきたいなと。

あとはexe化とGitHubでの公開。

OpenCVやってみる - 49. スレッドで並列処理

前回の続き。

今回は、春のパン祭りシール点数集計の処理を組み合わせていく前に、アプリ内処理の並列化をします。

スレッドで並列処理

前回はGUI上でのキー入力でカメラ画像更新をしましたが、実際は操作なしで更新するようにしたいところ。

ということで、処理をいくつかに分けて、それぞれ並列処理で別々に動かすことで、画像の自動更新を実現します。

GUIのクラス化

並列化する前に、少しコードを整理しておきたい。

今まで見てきたTkinter参考では、tkinterのクラスを継承してアプリケーションを作っていることがありました。
そのほうがまとまりがよさそう。

tkinter.Tkを継承するパターン、tkinter.Frameを継承するバターンとありましたが、 OpenCV+tkinterをやっていたサイトではtkinter.Frameを継承していたので、そっちにしておきます。

【Python/tkinter】OpenCVのカメラ動画をCanvasに表示する | イメージングソリューション

その他参考にしたページ。

【Python】Tkinterのテンプレート - Qiita

TkinterでGUI 後書きと補足 | Python学習講座

import tkinter as tk
import cv2
import numpy as np
from PIL import Image, ImageOps, ImageTk
class harupan_gui(tk.Frame):
    TEXT_CONNECT = 'Connect   '
    TEXT_DISCONNECT = 'Disconnect'

    def __init__(self, master=None):
        super().__init__(master)
        self.pack()

        self.cap = cv2.VideoCapture()

        self.master.title('Harupan App')
        self.master.geometry('500x300')

        #### 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, textvariable=self.t_ip)
        self.entry_port = tk.Entry(self, textvariable=self.t_port)
        self.entry_ip.grid(row=0, column=0)
        self.entry_port.grid(row=1, column=0)

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

        #### Image canvas ####
        self.canvas_image = tk.Canvas(self, bg='white')
        self.canvas_image.grid(row=2,column=0)
        self.update()
        self.w, self.h = self.canvas_image.winfo_width(), self.canvas_image.winfo_height()
        print(f'Canvas size: {self.w},{self.h}')
        self.disp_img = None

        master.bind('<KeyPress>', self.key_handler)

    def event_connect(self, e):
        if(self.t_connect.get() == self.TEXT_CONNECT):
            ret = self.cap.open(f'http://{self.t_ip.get()}:{self.t_port.get()}/video')
            if ret:
                print('Camera opened')
                self.t_connect.set(self.TEXT_DISCONNECT)
            else:
                print('Camera open failed')
        else:
            self.cap.release()
            print('Camera closed')
            self.t_connect.set(self.TEXT_CONNECT)

    #### Capture and display camera image on canvas ####
    def update_image(self):
        ret, img = self.cap.read()
        if not ret:
            print('Can\'t capture image')
            return
        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)
        # canvas_image.create_image(0,0,image=img, anchor=tkinter.NW)
        print('updated')

    def key_handler(self, e):
        print(e.keysym)
        if e.keysym == 'space':
            self.update_image()
root = tk.Tk()
app = harupan_gui(master=root)
app.mainloop()
Canvas size: 382,269
Camera opened
space
updated
Win_L
Shift_L
Camera closed

キー入力の最後のほうのWin_L, Shift_Lは、スクリーンショットをとったときのキー入力です。

なんとなくコードのまとまりが見えてきたかな。。。

GUIと画像取得部分の分離 (スレッド化)

上で作ったharupan_guiの処理を分割したいと思います。
分割のしかたとしては、

  • GUIの受け付け(接続情報入力、接続・切断ボタン)
  • カメラ接続・切断の実施
  • 定期的な画像取得
  • GUIの画像更新

※後述しますが、2つ目と3つ目はまとめないとうまくいかなかったので、実装は1つにまとめました。

という感じになります。
このように分割して並列処理することで、特にGUI上でのきっかけがなくても勝手に画像が更新されるようになります。

実装は以下のようにしました。

  • マルチスレッドを使用、2つのスレッドを立てる (※GUIのメイン処理Tk.mainloopはメインのスレッドで動かすので、計3つの並列処理になる)
    • cv2.VideoCaptureからの画像取得(および接続・切断)
    • 取得した画像のtkinter.Canvasへの反映
  • スレッド間はQueue.queueでデータをやり取りする
    • 画像データ (画像取得スレッド → 画像反映スレッド)
    • 接続・切断要求 (GUIメイン処理(ボタンクリックイベント) → 画像取得スレッド)
  • どちらのスレッドも、アプリ起動中はずっと回り続ける処理になるが、アプリ終了時に終了できるよう、フラグを見て回るようにしておく
    • フラグ不要のやり方があればよかったが…

上で作ったharupan_guiクラスを継承して、関数をオーバーライドする形で実装します。

参考サイト。

threading --- スレッドベースの並列処理 — Python 3.7.16 ドキュメント

queue --- 同期キュークラス — Python 3.11.2 ドキュメント

Python入門 (3) -マルチスレッド|npaka|note

Pythonのスレッド間の協調作業にはQueueを使う - Qiita

[解決!Python]クラスを継承するには:解決!Python - @IT

その他はまったポイントは以下の通り。

  • Tk.mainloopは別スレッド化できない
  • デバッグ時、立ち上げたスレッドを落としきれないことがあった
    • その場合、Jupyterカーネルの再起動と上のセルの再実行が必要、なかなか面倒だった
    • threading.active_count()threading.enumerate()で状況確認可能
    • 下記コードでは、立ち上げたスレッドを終了できたことが確認できるよう、表示を追加
  • VideoCaptureの接続・切断と画像読み出しは同じスレッドでやらないとうまくいかない
    • 最初は別スレッドにしていましたが、タイミングによっては切断後に読み出しが行われることがあり
    • その場合、例外が発生してスレッドが落ちてしまう
    • 1つのスレッドにして、接続・切断要求がある場合は画像読み出しを行わないように実装した
import queue
import threading
class harupan_gui_pp(harupan_gui):
    def __init__(self, master=None, img_queue_size=3):
        super().__init__(master)
        self.q_connect = queue.Queue(maxsize=0)
        self.q_img = 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.thread1.start()
        self.thread2.start()
        print(f'Number of threads: {threading.active_count()}')
        for th in threading.enumerate():
            print('  ', th)

    def event_connect(self, e):
        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:
            if not self.q_img.empty():
                img = self.q_img.get()
                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)
                # print('updated')

    def cap_process(self):
        while self.run_flag:
            if not self.q_connect.empty():
                print(self.q_connect.qsize())
                url = self.q_connect.get()
                if url == None:
                    self.cap.release()
                    print('Camera closed')
                else:
                    if self.cap.open(url):
                        print('Camera opened')
                    else:
                        print('Camera open failed')
            elif self.cap.isOpened():
                ret, img = self.cap.read()
                if ret and not self.q_img.full():
                    self.q_img.put(img)

    def run_app(self):
        self.master.mainloop()
        self.run_flag = False
        self.thread1.join(timeout=10)
        self.thread2.join(timeout=10)
        
        print(f'Number of threads: {threading.active_count()}')
        for th in threading.enumerate():
            print('  ', th)

    def key_handler(self, e):
        print(e.keysym)

root = tk.Tk()
app = harupan_gui_pp(root)
app.run_app()
Canvas size: 382,269
Number of threads: 7
   <_MainThread(MainThread, started 13660)>
   <Thread(Thread-2, started daemon 8116)>
   <Heartbeat(Thread-3, started daemon 27768)>
   <HistorySavingThread(IPythonHistorySavingThread, started 19020)>
   <ParentPollerWindows(Thread-1, started daemon 25108)>
   <Thread(thread1, started 23272)>
   <Thread(thread2, started 12632)>
F7
1
Camera opened
1
Camera closed
Number of threads: 5
   <_MainThread(MainThread, started 13660)>
   <Thread(Thread-2, started daemon 8116)>
   <Heartbeat(Thread-3, started daemon 27768)>
   <HistorySavingThread(IPythonHistorySavingThread, started 19020)>
   <ParentPollerWindows(Thread-1, started daemon 25108)>

カメラ接続・切断と、画像表示ができました。
画像表示は、自動で更新し続けました。ただ、ちらつきが出てしまっています。 生成したスレッドもきちんと終了できています。

ここまで

今回はここで区切りにしておきます。

今回は少し寄り道になってしまいましたが、次こそ春のパン祭りシール点数集計との組み合わせをしたいと思います。

OpenCVやってみる - 48. GUIでカメラ画像表示

前の記事通り、GUIでのカメラ画像表示をやってみます。

前回と前々回の内容の組み合わせ+αです。

カメラ画像表示

前回、前々回の内容に加えて、OpenCV画像→PIL画像の変換があるぐらいです。

下記参照。

【Python/tkinter】OpenCVのカメラ動画をCanvasに表示する | イメージングソリューション

after()での待ちを入れていますが、いまいち理解できていないので、まずはキーボード入力で更新するようにします。

キー入力受付

と、そのためにはTkinterでキー入力受付が必要。
下記参照で。

Tkinter: イベントを検出する(クリック・キー入力・マウス移動)

キーの値はevent.keysymで取得。

tkinter超入門【第45回 キー入力イベント】 | ITよろず雑記帳

import tkinter
import cv2
from PIL import Image, ImageTk, ImageOps
def key_handler(e):
    print(e.keycode, e.keysym)

root = tkinter.Tk()
root.title('Key event test')
root.bind('<KeyPress>', key_handler)

frame = tkinter.Frame(root)

label = tkinter.Label(frame, text='Input text')
t = tkinter.StringVar()
entry =tkinter.Entry(frame, textvariable=t)

frame.pack()
label.pack()
entry.pack()

root.mainloop()

65 a
66 b
67 c
68 d
69 e
70 f
13 Return
27 Escape

最後にEnterキー、Escキーを押しています。
keycodeは基本的にはASCIIコード通りになっているのか。
環境にもよるかな?

今度こそカメラ画像表示

DroidCamアプリのIPアドレス、ポート番号は入力できるようにしておきます。
また、接続ボタンも用意します。

StringVar()は、ウィジェット変数で、ユーザーが入力した値をリアルタイムで反映してくれるものだそう。

Tkinterで使われるWidget変数とは?StringVarを中心に解説!? | 「モノづくりから始まるエンジニア」

今回は、スペースキーで画像更新にしましたが、テキストボックスとの併用はあんまりよくないかも。

  • テキストボックスにスペース入力をしたいだけでも画像更新されてしまう
  • テキストボックスをアクティブにしたときに、画像更新のスペースキーが入力になってしまう

このへんはGUI設計のときに要検討。

cap = cv2.VideoCapture()

root = tkinter.Tk()
root.title('Display DroidCam Image')
root.geometry('500x300')

# frame = tkinter.Frame(root, padx=10, pady=10)
frame = tkinter.Frame(root)
frame.pack()

#### Entries for connection information ####
t_ip = tkinter.StringVar(value='192.168.1.7')
t_port = tkinter.StringVar(value='4747')
entry_ip = tkinter.Entry(frame, textvariable=t_ip)
entry_port = tkinter.Entry(frame, textvariable=t_port)
entry_ip.grid(row=0, column=0)
entry_port.grid(row=1, column=0)

#### Connect button ####
text_connect = 'Connect   '
text_disconnect = 'Disconnect'
t_connect = tkinter.StringVar(value=text_connect)

def event_connect(e):
    if(t_connect.get() == text_connect):
        ret = cap.open(f'http://{t_ip.get()}:{t_port.get()}/video')
        if ret:
            print('Camera opened')
            t_connect.set(text_disconnect)
        else:
            print('Camera open failed')
    else:
        cap.release()
        print('Camera closed')
        t_connect.set(text_connect)

button_connect = tkinter.Button(frame, textvariable=t_connect)
button_connect.bind('<Button-1>', event_connect)
button_connect.grid(row=0, column=1)

#### Image canvas ####
canvas_image = tkinter.Canvas(frame, bg='white')
canvas_image.grid(row=2,column=0)
frame.update()
w, h = canvas_image.winfo_width(), canvas_image.winfo_height()
print(f'Canvas size: {w},{h}')
disp_img = None

#### Capture and display camera image on canvas ####
def update_image():
    global disp_img

    ret, img = cap.read()
    if not ret:
        print('Can\'t capture image')
        return
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = Image.fromarray(img)
    img = ImageOps.pad(img, (w,h))
    disp_img = ImageTk.PhotoImage(image=img)
    canvas_image.create_image(w/2,h/2,image=disp_img)
    # canvas_image.create_image(0,0,image=img, anchor=tkinter.NW)
    print('updated')

def key_handler(e):
    # print(e.keysym)
    if e.keysym == 'space':
        update_image()

root.bind('<KeyPress>', key_handler)

root.mainloop()
Canvas size: 382,269
Camera opened
Camera closed
Camera opened
updated
updated
updated
updated
Camera closed

ちょっとはまってしまいましたが、なんとかできた。

Canvas.create_image()自体では画面の更新はしておらず、mainloopに戻ってから実際の更新をしているよう。
また、Canvas.create_image()では画像への参照を登録しているのみと思われます。

ということで、mainloop内からアクセスできる変数になっている必要があると。
で、グローバル変数を使いました。
グローバル変数を使う関数の中でglobal (変数名)を書いておかないといけないのも要注意。

【Python】tkinterのmainloopについて解説 | だえうホームページ

after()についても解説があり、結局理解することになりました。

ここまで

とりあえず今回やることはできました。
次回は春のパン祭りスクリプトとのつなぎになります。

OpenCVやってみる - 47. PythonでGUI

前回の続きです。
春のパン祭りアプリをexe化する前に、PythonGUIを作れるようにしておきます。

今回の記事はJupyter notebookでやった内容の貼り付けです。

Tkinter試し

PythonGUI作成するのに、いくつか良く使われるライブラリがあるようですが、今回はTkinterを使ってみます。Pythonインストール時に標準で付いてくるそう。

まずは簡単なGUIを動かしてみる。
以下を参考にしました。

https://python.keicode.com/advanced/tkinter.php

import tkinter

まずはテスト。

tkinter._test()

こんな画面が出ました。

"Click me!"したらカッコが増えた。

バージョン確認も。

tkinter.Tcl().eval('info patchlevel')
'8.6.9'

GUI基本(テキスト表示、文字入力、ボタン)

だいたい下記の通りに。
1点だけ、このサイトではfrom tkinter import *としていますが、今回はこれはなしにしておきます。

Tkinter で GUI を作る基本 - Tkinter による GUI プログラミング - Python 入門

from tkinter import ttk
root = tkinter.Tk()
root.title('My first app')
''
frame1 = ttk.Frame(root, padding=16)
label1 = ttk.Label(frame1, text='Your name')
t = tkinter.StringVar()
entry1 = ttk.Entry(frame1, textvariable=t)
button1 = ttk.Button(frame1, text='OK', command=lambda: print('Hello %s' % t.get()))
frame1.pack()
label1.pack(side=tkinter.LEFT)
entry1.pack(side=tkinter.LEFT)
button1.pack(side=tkinter.LEFT)
root.mainloop()
Hello tekitou tarou

できました。
ウィンドウのxボタンを押すまでは、Jupyterのセルが実行中になっていました。

少しだけ試し

また、アプリケーションのルート要素は、アプリケーションを終了したら消えるよう。 なので、作り直します。

tkinter.Frame()ではpaddingのオプションはなく、代わりにpadxpadyがあるようでした。

root = tkinter.Tk()
root.title('My second app')

frame1 = tkinter.Frame(root, padx=30, pady=20)
label1 = tkinter.Label(frame1, text='Your name')
t = tkinter.StringVar()
entry1 = tkinter.Entry(frame1, textvariable=t)
button1 = tkinter.Button(frame1, text='OK', command=lambda: print('Hello %s' % t.get()))

frame1.pack()
label1.pack(side=tkinter.LEFT)
entry1.pack(side=tkinter.LEFT)
button1.pack(side=tkinter.LEFT)

root.mainloop()
Hello tarou tekitou

これだけだといまいち違いが分からないな…

とりあえずこれで基本的なところは押さえられたかなと。

画像表示

Labelでも画像表示できるようでしたが、 試し画像としてjpeg画像を使おうとしたら、対応はしていないようでした。

調べたところ、CanvasウィジェットPILライブラリを使った方法は見つかったので、それでやってみます。

【Python】Tkinterによる画像表示をわかりやすく解説 | ジコログ

【Python/tkinter】OpenCVのカメラ動画をCanvasに表示する | イメージングソリューション

ここまではpack()を使ったウィジェット配置をしていましたが、他にgrid()place()を使う方法もあるとのこと。上記サンプルでは、grid()を使っていました。ここでもgrid()を使ってみました。

【tkinter】grid()を使ったWidgetの配置の方法 - どん底から這い上がるまでの記録

【tkinter】pack()を使ったWidgetの配置の方法 - どん底から這い上がるまでの記録

【tkinter】place()を使ったWidgetの配置の方法 - どん底から這い上がるまでの記録

from PIL import Image, ImageTk, ImageOps
root = tkinter.Tk()
root.title('Image')
root.geometry('400x300')

frame = ttk.Frame(root)
frame.pack()

label_txt = ttk.Label(frame, text='Displaying image')
label_txt.grid(row=0,column=0)

canvas = tkinter.Canvas(frame)
canvas.grid(row=1,column=0)

frame.update() # Needed to get canvas size
w = canvas.winfo_width()
h = canvas.winfo_height()
print(w,h)

if w > 1 and h > 1:
    img = Image.open('harupan_190428_1.jpg')
    img = ImageOps.pad(img, (w,h))
    img = ImageTk.PhotoImage(image=img)

    canvas.create_image(w/2,h/2, image=img)

root.mainloop()
382 269

なんとか画像が出ました。
参考サイト通りにやらなかったのもあって、結構苦労した…

ここまで

ということで今回はここまで。
次回はカメラ画像をGUIで表示してみます。

OpenCVやってみる - 46. カメラ変更

久し振りの更新です。

今年も春のパン祭りが始まり、シールも早速集まってきた(もらった)ので、去年のシール点数自動集計を改めて触ってみました。

いくつか本体の処理以外でやったことがあるので、記事にしておきます。

今回は、Jupyter notebookのエクスポートではなく、普通にはてなブログ上で書いた記事です。

内容

  • カメラの変更
    去年はPC内蔵のカメラを使っていましたが、モニタ側に向いているインカメしかないので、シール台紙を映しながら結果を確認するのがやりづらく。
    iPhoneWebカメラ化する方法があるようだったので、これを試しました。
  • exe化
    Pythonスクリプトとしてしか作っていなかったので、読んでいる人には使ってもらいにくかったかなと。
    Pythonスクリプトのexe化もできるようで、こちらも試しました。
    ついでに、GtHubでの公開も~

カメラの変更

私はiPhoneを使っています。
いくつか調べて、DroidCamというアプリを使う方法をやってみました。

qiita.com

EpocCamというアプリもやってみましたが、OpenCVからの接続がうまくいかなかったので没で。

iPhone SEを高画質WEBカメラにしてzoomと接続する方法|sugarcamera

手順。

  1. iPhoneにDroidCamをインストール

  2. iPhone(家のWiFiに接続した状態)でDroidCamを立ち上げ
    アプリの画面で、IPアドレスとポート番号を確認

    画面下のほう、広告がでかでかと出ていますが…

  3. ブラウザから確認: ブラウザのアドレスバーに"http://(IPアドレス):(ポート番号)/video" ・・・今回は"http://192.168.1.7:4747/video"

    ちゃんとiPhoneカメラの画像が見えました。
    ちなみに、iPhoneを横向きにして映しています。これは設定次第かな?

  4. OpenCV-Pythonから確認: 下記コードを実施。

import cv2
cap = cv2.VideoCapture()
cap.open('http://192.168.1.7:4747/video')
True
while True:
    ret, frame = cap.read()
    if not ret:
        print('Can\'t receive frame')
        cv2.destroyAllWindows()
        break
    else:
        cv2.imshow('Image', frame)
        k = cv2.waitKey(1)
        if k == ord('e'):
            cv2.destroyAllWindows()
            break
            
print(frame.shape)
(480, 640, 3)
cap.release()
cap.isOpened()
False

画像はVGAサイズになっているようです。

確認した画像は下の通り。
ちゃんと見られました。

一旦ここまで

ここで一区切りにします。
次回は、Pythonスクリプトのexe化をしたいところですが、その前にPythonでのGUIもやる必要がありそうなので、そちらをやります。

DTMやってみたい - 1. やりたいこと

時間が空いちゃいましたが、ブログ更新しておこう。

やりたいこと

前のテーマが終わったので次何しようかと。
で、DTMやりたいなと。

  • エレクトーンやってます(就職してから始めたのでそんなぐらいのレベル(´・ω・`)、今は時間が取れてなくてやってない)が、エレクトーン自体は持ってない。
  • 練習は基本電子ピアノで、メトロノーム機能とちょこっと音色があるぐらい。
  • GarageBandで伴奏データ打ち込んでみたことはある、やっぱりドラムベースあると違う。
  • ヤマハのシンセ(MX61)を随分前に買ったが、全く使いこなせてない。でもちょっといじってみた感じ、面白そうな予感。

という感じの背景があり、

  • DTMやってみたい!
    • エレクトーン練習用の伴奏データをちゃんと作れるようになりたい
    • エレクトーンの曲でなくても、伴奏データ作って楽譜も起こして弾いてみたい
    • シンセも使いこなしたい
    • 曲のアレンジぐらいできるようになるのでは?
    • エレピが好きなんですが、適当なコード進行とかフレーズとか作れるようになるのでは?

と思ってます。

やっていかないといけなさそうなところを書き出してみる。

  • 音楽理論
    • コード進行等理解した方が耳コピもしやすそう
  • 耳コピ
    • 一回やってみたが、なかなか辛い… やりやすいやり方があるのでは?
  • DTMソフトの使い方
    • シンセに付属してたものもありますが、ちゃんと使っておらず。まずはこれを使うか。
    • 無償のソフトでいいのないかな?
    • ちゃんと何か購入するのもありか。
  • シンセの使い方
    • 一応取説見てちょっといじったが、音を重ねる、スプリットする、プリセットのリズムパターンを鳴らすぐらい。
    • エレクトーンぐらいのことができるようになりたい。
    • 音楽のセンスも必要かも。
  • 鍵盤の練習(´・ω・`)
    • へたっぴなので…

というわけで、まず音楽理論やってみたいと思います。

以上

次回は音楽理論をざっとネットで調べたところを書いてみます。

OpenCVやってみる - 45. 高速化処理で再確認(最終回のはず)

今回は、前回高速化した処理で、PCカメラ画像のリアルタイム処理をやってみます。
今度こそ最終回!

準備

修正したスクリプトの読み込み、テンプレートデータとSVMデータの読み込み。

from harupan_data.harupan import *

svm = load_svm('harupan_data/harupan_svm_220412.dat')
templates2021= load_templates('harupan_data/templates2021.json')

リアルタイム処理

これは前々回と全く同じです。

def realtime_harupan():
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        print('Cannot open camera')
        return
    else:
        print('Camera opened')
    while True:
        ret, frame = cap.read()
        if not ret:
            print('Can''t receive frame')
            cv2.destroyAllWindows()
            return
        else:
            score, result_img = calc_harupan(frame, templates2021, svm)
            score_text = str(score) + ' points'
            score_area = np.zeros((50, result_img.shape[1], 3), 'uint8')
            score_area = cv2.putText(score_area, score_text, (0,45), cv2.FONT_HERSHEY_SIMPLEX, 1, (255,255,0), 3)
            score_img = np.vstack((result_img, score_area))
            cv2.imshow('Result', score_img)
            k = cv2.waitKey(1)
            if k == ord('p'):
                cv2.waitKey(0)
            elif k == ord('e'):
                cap.release()
                return
realtime_harupan()
Camera opened
cv2.destroyAllWindows()

今回も、結果をGIFで残したので、これを載せてみます。

↓前回のGIFも載せます。

やっぱり処理が速くなっています。
精度も特に悪くなったりはしていない。

特に処理時間は計っていませんでしたが、GIFの編集アプリで見ると、フレームごとの間隔が見られました。

↓今回

↓前回

ちょっと見えづらいかもしれませんが、下のほうにフレームの間隔の時間が記載されています。
前回はフレーム間2秒程度、今回は1秒程度になっています。

また別の春のパン祭りシール台紙を。

これの前回版は、

という感じで、どちらも結構遅い。
何が違うのか?

処理時間は、

↓今回

↓前回

という感じで、大差ない?むしろ少し遅くなった?という感じ。

以上

今度こそ以上にします。
またテーマを見つけて、OpenCVや画像処理をいじっていきたい。

おまけ

最終版のスクリプトを以下に掲載しておきます。


######################################################
# Importing libraries
######################################################
from ctypes import resize
import cv2
import numpy as np
from matplotlib import pyplot as plt
import math
import copy
import random
import json

######################################################
# Detecting contours
######################################################
def reduce_resolution(img, res_th=800):
    h, w, chs = img.shape
    if h > res_th or w > res_th:
        k = float(res_th)/h if w > h else float(res_th)/w
    else:
        k = 1.0
    rtn_img = cv2.resize(img, None, fx=k, fy=k, interpolation=cv2.INTER_AREA)
    return rtn_img

def harupan_binarize(img, sat_th=100):
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # Convert hue value (rotation, mask by saturation)
    hsv[:,:,0] = np.where(hsv[:,:,0] < 50, hsv[:,:,0]+180, hsv[:,:,0])
    hsv[:,:,0] = np.where(hsv[:,:,1] < sat_th, 0, hsv[:,:,0])
    # Thresholding with cv2.inRange()
    binary_img = cv2.inRange(hsv[:,:,0], 135, 190)
    return binary_img

def detect_candidate_contours(image, res_th=800, sat_th=100):
    img = reduce_resolution(image, res_th)
    binimg = harupan_binarize(img, sat_th)
    # Retrieve all points on the contours (cv2.CHAIN_APPROX_NONE)
    contours, hierarchy = cv2.findContours(binimg, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
    if len(contours) == 0:
        return contours, img
    # Pick up contours that have no parents
    indices = [i for i,hier in enumerate(hierarchy[0,:,:]) if hier[3] == -1]
    # Pick up contours that reside in above contours
    indices = [i for i,hier in enumerate(hierarchy[0,:,:]) if (hier[3] in indices) and (hier[2] == -1) ]
    contours = [contours[i] for i in indices]
    contours = [ctr for ctr in contours if cv2.contourArea(ctr) > float(res_th)*float(res_th)/4000]
    return contours, img

# image: Entire image containing multiple contours
# contours: Contours contained in "image" (Retrieved by cv2.findContours(), the origin is same as "image")
def refine_contours(image, contours):
    subctrs = []
    subimgs = []
    binimgs = []
    thresholds = []
    n_ctrs = []
    for ctr in contours:
        img, _ = create_contour_area_image(image, ctr)
        # Thresholding using G value in BGR format
        thresh, binimg = cv2.threshold(img[:,:,1], 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        # Add black region around thresholded image, to detect contours correctly
        binimg = cv2.copyMakeBorder(binimg, 2,2,2,2, cv2.BORDER_CONSTANT, 0)
        ctrs2, _ = cv2.findContours(binimg, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
        max_len = 0
        for ctr2 in ctrs2:
            if max_len <= ctr2.shape[0]:
                max_ctr = ctr2
                max_len = ctr2.shape[0]
        subctrs += [max_ctr]
        subimgs += [img]
        binimgs += [binimg]
        thresholds += [thresh]
        n_ctrs += [len(ctrs2)]
    debug_info = (binimgs, thresholds, n_ctrs)
    return subctrs, subimgs, debug_info

######################################################
# Auxiliary functions
######################################################
def create_contour_area_image(img, ctr):
    x,y,w,h = cv2.boundingRect(ctr)
    rtn_img = img[y:y+h,x:x+w,:].copy()
    rtn_ctr = ctr.copy()
    origin = np.array([x,y])
    for c in rtn_ctr:
        c[0,:] -= origin
    return rtn_img, rtn_ctr

# ctr: Should be output of create_contour_area_image() (Origin of points is the origin of bounding box)
# img_shape: Optional, tuple of (image_height, image_width), if omitted, calculated from ctr
def create_solid_contour(ctr, img_shape=(int(0),int(0))):
    if img_shape == (int(0),int(0)):
        _,_,w,h = cv2.boundingRect(ctr)
    else:
        h,w = img_shape
    img = np.zeros((h,w), 'uint8')
    img = cv2.drawContours(img, [ctr], -1, 255, -1)
    return img

# ctr: Should be output of create_contour_area_image() (Origin of points is the origin of bounding box)
def create_upright_solid_contour(ctr):
    ctr2 = ctr.copy()
    (cx,cy),(w,h),angle = cv2.minAreaRect(ctr2)
    M = cv2.getRotationMatrix2D((cx,cy), angle, 1)
    for i in range(ctr2.shape[0]):
        ctr2[i,0,:] = ( M @ np.array([ctr2[i,0,0], ctr2[i,0,1], 1]) ).astype('int')
    rect = cv2.boundingRect(ctr2)
    img = np.zeros((rect[3],rect[2]), 'uint8')
    ctr2 -= rect[0:2]
    M[:,2] -= rect[0:2]
    img = cv2.drawContours(img, [ctr2], -1, 255,-1)
    return img, M, ctr2


######################################################
# Dataset classes
######################################################
class contour_dataset:
    def __init__(self, ctr):
        self.ctr = ctr.copy()
        self.rrect = cv2.minAreaRect(ctr)
        self.box = cv2.boxPoints(self.rrect)
        self.solid = create_solid_contour(ctr)
        n = 100
        if n >= ctr.shape[0]:
            self.pts = np.array([p for p in ctr[:,0,:]])
        else:            
            r = n / ctr.shape[0]
            self.pts = np.zeros((100,2), 'int')
            pts = []
            for i in range(ctr.shape[0]):
                f = math.modf(i*r)[0] 
                if (f <= r/2) or (f > 1.0 - r/2):
                    pts += [ctr[i,0,:]]
            self.pts = np.array(pts)

class template_dataset:
    def __init__(self, ctr, num, selected_idx=[0]):
        self.ctr = ctr.copy()
        self.num = num
        self.rrect = cv2.minAreaRect(ctr)
        self.box = cv2.boxPoints(self.rrect)
        if num == 0:
            self.solid,_,_ = create_upright_solid_contour(ctr)
        else:
            self.solid = create_solid_contour(ctr)
        self.pts = np.array([ctr[idx,0,:] for idx in selected_idx])


######################################################
# ICP
######################################################
# pts: list of 2D points, or ndarray of shape (n,2)
# query: 2D point to find nearest neighbor
def find_nearest_neighbor(pts, query):
    min_distance_sq = float('inf')
    min_idx = 0
    for i, p in enumerate(pts):
        d = np.dot(query - p, query - p)
        if(d < min_distance_sq):
            min_distance_sq = d
            min_idx = i
    return min_idx, np.sqrt(min_distance_sq)

# src, dst: ndarray, shape is (n,2) (n: number of points)
def estimate_affine_2d(src, dst):
    n = min(src.shape[0], dst.shape[0])
    x = dst[0:n].flatten()
    A = np.zeros((2*n,6))
    for i in range(n):
        A[i*2,0] = src[i,0]
        A[i*2,1] = src[i,1]
        A[i*2,2] = 1
        A[i*2+1,3] = src[i,0]
        A[i*2+1,4] = src[i,1]
        A[i*2+1,5] = 1
    M = np.linalg.inv(A.T @ A) @ A.T @ x
    return M.reshape([2,3])

# Find optimum affine matrix using ICP algorithm
# src_pts: ndarray, shape is (n_s,2) (n_s: number of points)
# dst_pts: ndarray, shape is (n_d,2) (n_d: number of points, n_d should be larger or equal to n_s)
# initial_matrix: ndarray, shape is (2,3)
def icp(src_pts, dst_pts, max_iter=20, initial_matrix=np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])):
    search_range = 0.25
    return icp_sub(src_pts, dst_pts, max_iter=max_iter, initial_matrix=initial_matrix, search_range=search_range)

# Find optimum affine matrix using ICP algorithm
# src_pts: ndarray, shape is (n_s,2) (n_s: number of points)
# dst_pts: ndarray, shape is (n_d,2) (n_d: number of points, n_d should be larger or equal to n_s)
# initial_matrix: ndarray, shape is (2,3)
# search_range: float number, 0.0 ~ 1.0, the range to search nearest neighbor, 1.0 -> Search in all dst_pts
def icp_sub(src_pts, dst_pts, max_iter=20, initial_matrix=np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]), search_range=0.5):
    default_affine_matrix = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
    n_dst = dst_pts.shape[0]
    n_src = src_pts.shape[0]
    if n_dst < n_src:
        # print("icp: Insufficient destination points")
        return default_affine_matrix, False
    if initial_matrix.shape != (2,3):
        print("icp: Illegal shape of initial_matrix")
        return default_affine_matrix, False
    n_search = int(n_dst*search_range)
    M = initial_matrix
    # Store indices of the nearest neighbor point of dst_pts to the converted point of src_pts
    nn_idx = []
    converged = False
    for i in range(max_iter):
        nn_idx_tmp = []
        dst_pts_list = [p for p in dst_pts]
        idx_list = list(range(0,dst_pts.shape[0]))
        first_pt = True
        for p in src_pts:
            # Convert source point with current conversion matrix
            p2 = M @ np.array([p[0], p[1], 1])
            if first_pt:
                # First point should be searched in all destination points
                idx, _ = find_nearest_neighbor(dst_pts_list, p2)
                first_pt = False
            else:
                # Search nearest neighbor point in specified range around the last point
                n = int(min(n_search/2, len(idx_list)/2))
                s = max(len(idx_list) + last_idx - n, 0)
                e = min(len(idx_list) + last_idx + n, 3*len(idx_list))
                pts = (dst_pts_list + dst_pts_list + dst_pts_list)[s:e]
                idx, _ = find_nearest_neighbor(pts, p2)
                # The index acquired above is counted from 's', so actual index must be recovered
                idx = (idx + s) % len(idx_list)
            nn_idx_tmp += [idx_list[idx]]
            last_idx = idx
            del dst_pts_list[idx]
            del idx_list[idx]
        if nn_idx != [] and nn_idx == nn_idx_tmp:
            converged = True
            break
        dst_pts2 = np.zeros_like(src_pts)
        for j,idx in enumerate(nn_idx_tmp):
            dst_pts2[j,:] = dst_pts[idx,:]
        M = estimate_affine_2d(src_pts, dst_pts2)
        nn_idx = nn_idx_tmp
    return M, converged


######################################################
# Calculating similarity and determining the number
######################################################
def binary_image_similarity(img1, img2):
    if img1.shape != img2.shape:
        print('binary_image_similarity: Different image size')
        return 0.0
    xor_img = cv2.bitwise_xor(img1, img2)
    return 1.0 - np.float(np.count_nonzero(xor_img)) / (img1.shape[0]*img2.shape[1])

# src, dst: contour_dataset or template_dataset (holding member variables box, solid)
def get_transform_by_rotated_rectangle(src, dst):
    # Rotated patterns are created when starting index is slided
    dst_box2 = np.vstack([dst.box, dst.box])
    max_similarity = 0.0
    max_converted_img = np.zeros((dst.solid.shape[1], dst.solid.shape[0]), 'uint8')
    for i in range(4):
        M = cv2.getAffineTransform(src.box[0:3], dst_box2[i:i+3])
        converted_img = cv2.warpAffine(src.solid, M, dsize=(dst.solid.shape[1], dst.solid.shape[0]), flags=cv2.INTER_NEAREST)
        similarity = binary_image_similarity(converted_img, dst.solid)
        if similarity > max_similarity:
            M_rtn = M
            max_similarity = similarity
            max_converted_img = converted_img
    return M_rtn, max_similarity, max_converted_img

def get_similarity_with_template(target_data, template_data, sim_th_high=0.95, sim_th_low=0.7):
    _,(w1,h1), _ = target_data.rrect
    _,(w2,h2), _ = template_data.rrect
    r = w1/h1 if w1 < h1 else h1/w1
    r = r * h2/w2 if w2 < h2 else r * w2/h2
    M, sim_init, _ = get_transform_by_rotated_rectangle(template_data, target_data)
    if sim_init > sim_th_high or sim_init < sim_th_low or r > 1.4 or r < 0.7:
        dsize = (template_data.solid.shape[1], template_data.solid.shape[0])
        flags = cv2.INTER_NEAREST|cv2.WARP_INVERSE_MAP
        converted_img = cv2.warpAffine(target_data.solid, M, dsize=dsize, flags=flags)
        return sim_init, converted_img
    M, _ = icp(template_data.pts, target_data.pts, initial_matrix=M)
    Minv = cv2.invertAffineTransform(M)
    converted_ctr = np.zeros_like(target_data.ctr)
    for i in range(target_data.ctr.shape[0]):
        converted_ctr[i,0,:] = (Minv[:,0:2] @ target_data.ctr[i,0,:]) + Minv[:,2]
    converted_img = create_solid_contour(converted_ctr, img_shape=template_data.solid.shape)
    val = binary_image_similarity(converted_img, template_data.solid)
    return val, converted_img

def get_similarity_with_template_zero(target_data, template_data):
    dsize = (template_data.solid.shape[1], template_data.solid.shape[0])
    converted_img = cv2.resize(target_data.solid, dsize=dsize, interpolation=cv2.INTER_NEAREST)
    val = binary_image_similarity(converted_img, template_data.solid)
    return val, converted_img

def get_similarities(target, templates):
    similarities = []
    converted_imgs = []
    for tmpl in templates:
        if tmpl.num == 0:
            sim,converted_img = get_similarity_with_template_zero(target, tmpl)
        else:
            sim,converted_img = get_similarity_with_template(target, tmpl)
        similarities += [sim]
        converted_imgs += [converted_img]
    return similarities, converted_imgs

def calc_harupan(img, templates, svm):
    ctrs, resized_img = detect_candidate_contours(img, sat_th=50)
    # print('Number of candidates: ', len(ctrs))
    if len(ctrs) == 0:
        return 0.0, resized_img
    subctrs, _, _ = refine_contours(resized_img, ctrs)
    subctr_datasets = [contour_dataset(ctr) for ctr in subctrs]
    ########
    #### Simple code
    similarities = [get_similarities(d, templates)[0] for d in subctr_datasets]
    #### Code printing progress
    # similarities = []
    # for i,d in enumerate(subctr_datasets):
    #     print(i, end=' ')
    #     similarities += [get_similarities(d, templates)[0]]
    # print('')
    ########
    _, result = svm.predict(np.array(similarities, 'float32'))
    result = result.astype('int')
    score = 0.0
    texts = {0:'0', 1:'1', 2:'2', 3:'3', 5:'.5'}
    font = cv2.FONT_HERSHEY_SIMPLEX
    for res, ctr in zip(result, ctrs):
        if res[0] == 5:
            score += 0.5
        elif res[0] != -1:
            score += res[0]
        
        # Annotating recognized numbers for confirmation
        if res[0] != -1:
            resized_img = cv2.drawContours(resized_img, [ctr], -1, (0,255,0), 3)
            x,y,_,_ = cv2.boundingRect(ctr)
            resized_img = cv2.putText(resized_img, texts[res[0]], (x,y), font, 1, (230,230,0), 5)
    return score, resized_img

######################################################
# Loading template data and SVM model
######################################################
def load_svm(filename):
    return cv2.ml.SVM_load(filename)

def load_templates(filename):
    with open(filename, mode='r') as f:
        load_data = json.load(f)
        templates_rtn = []
        for d in load_data:
            templates_rtn += [template_dataset(np.array(d['ctr']), d['num'], d['pts'])]
    return templates_rtn