円の検出アプリを作った

Python

はじめに

大学のアルバイトでリアルタイム撮影をしながら、円の画像認識をする必要があったのでGUI操作可能なプログラムを書きました。
カメラによる動画の取得、画像認識、GUI、csvへの保存などちょっと内容は多いです。
こんな感じのアプリとなっています。

 

構成

・カメラによる動画の取得と画像認識

・csvへの保存

・GUI

となっています。

 

カメラによる動画の取得と画像認識

import cv2
import sys
from csv import writer
from time import time

delay = 1
window_name = "frame"

###ビデオキャプチャ###
cap = cv2.VideoCapture(0) 

if not cap.isOpened():
    sys.exit()


###動画再生しながら画像認識###
while True:
    ret, frame = cap.read()           
    # フレームを取得ができたらret = True,できなかったらFalse
    if ret:
        gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
        circle = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp = 1, minDist=20,param1 = 100, param2 = 60,minRadius=0,maxRadius=0)
        if circle is not None:
            for i in circle:
                for j in range(len(i)):
                    x,y,r = i[j]
                    frame = cv2.circle(frame, (int(x), int(y)), int(r), (255, 0, 0), 3)
        cv2.imshow(window_name,frame)

    # フレームが取得できない場合はループを抜ける
        if cv2.waitKey(delay) & 0xFF == ord("q"):
            break
    else:
        cap.set(cv2.CAP_PROP_POS_FRAMES,0)
cap = cv2.VideoCapture(0)でカメラを指定してあげます。

ここで引数は0にするとPC内蔵カメラになります。(内蔵カメラがない場合は外付けカメラ)

ret, frame = cap.read()
でカメラの画像を取り込んでいます。
retは画像が取れていたかどうか(bool値になっています。)
frameは画像そのもの
frameに画像が入るので、もしちゃんと画像が所得できていたら(if ret == True)
円検出のためにframe を二値化処理してあげて(frame ←cv2.cvtcolor)
hughcircles関数でハフ変換を用いた円検出を行います。
円検出された円はfor文を使って全部frameに上書きしています。
また、このプログラムはキーボードの”q”で抜けられるようになっています。

取った円の情報をcsvに保存する

import cv2
import sys
from csv import writer
from time import time

delay = 1
window_name = "frame"
csvfile = "test.csv"

###ビデオキャプチャ###
cap = cv2.VideoCapture(0) 

if not cap.isOpened():
    sys.exit()

###csv用意###
xyr = []

for i in range(10):
    lis = [f"x{i+1}",f"y{i+1}",f"r{i+1}"]
    xyr.extend(lis)

columns_name = ["time"]
columns_name.extend(xyr)
print(columns_name)

with open(csvfile,"a",newline='') as f:
            writer_object = writer(f)
            writer_object.writerow(columns_name)
            f.close()

###動画再生しながらデータの保存###
st = time()
while True:
    ret, frame = cap.read()                                         # フレームを取得

    second = time()-st

    if ret:
        gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
        circle = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp = 1, minDist=20,param1 = 100, param2 = 60,minRadius=0,maxRadius=0)
        if circle is not None:
            for i in circle:
                for j in range(len(i)):
                    x,y,r = i[j]
                    frame = cv2.circle(frame, (int(x), int(y)), int(r), (255, 0, 0), 3)
        cv2.imshow(window_name,frame)

        with open(csvfile,"a",newline='') as f:
            writer_object = writer(f)
            writer_object.writerow([second]+i.flatten().tolist())
            f.close()

    # フレームが取得できない場合はループを抜ける
        if cv2.waitKey(delay) & 0xFF == ord("q"):
            break
    else:
        cap.set(cv2.CAP_PROP_POS_FRAMES,0)


 
# 動画オブジェクト解放
cv2.destroyWindow("frame")

先ほどの円の検出に加えてwith文を使ってcsvファイルに保存し続けるようにしています。

###csv用意###
xyr = []

for i in range(10):
    lis = [f"x{i+1}",f"y{i+1}",f"r{i+1}"]
    xyr.extend(lis)

columns_name = ["time"]
columns_name.extend(xyr)
print(columns_name)

with open(csvfile,"a",newline='') as f:
            writer_object = writer(f)
            writer_object.writerow(columns_name)
            f.close()

ここでcsvファイルのヘッダー情報を生成しています。
その後,stに開始時間を保存しておいて都度、計測時の絶対時間を取って開始時間と差分を取ることで相対時間を得ています。
その後、測定ごとにwith open()分を用いてcsv fileを開いて書き込み続けています。

GUI化

こちらを参考にしながら機能を追加していきました。GUIに関しては参考サイトが丁寧に説明されているためそちらを参考にしてください。

import tkinter as tk
from tkinter import ttk
import cv2
import PIL.Image, PIL.ImageTk
from tkinter import font
from csv import writer
from time import time

class Application(tk.Frame):
    def __init__(self,master, video_source=0):
        super().__init__(master)


        #GUIアプリの大きさ
        self.master.geometry("700x1000")
        #GUIアプリのタイトル
        self.master.title("Tkinter with Video Streaming and Capture")

        # ---------------------------------------------------------
        # 設定の初期化
        # ---------------------------------------------------------
        
        #csv fileの名前
        self.csvfile = "circle_detection.csv"

        #初期時間
        self.init_time = time()

        #flag管理
        self.circle_detection_flag = False
        self.save_flag = False


        #fitting parameter の初期値
        self.mdist = 20
        self.par1 = 100
        self.par2 = 60
    

        ###csv用意###
        xyr = []

        for i in range(10):
            lis = [f"x{i+1}",f"y{i+1}",f"r{i+1}"]
            xyr.extend(lis)

        columns_name = ["time"]
        columns_name.extend(xyr)

        with open(self.csvfile,"w",newline='') as f:
                    writer_object = writer(f)
                    writer_object.writerow(columns_name)
                    f.close()
        #############

        # ---------------------------------------------------------
        # フォントの設定
        # ---------------------------------------------------------
        self.font_frame = font.Font( family="Meiryo UI", size=15, weight="normal" )
        self.font_btn_big = font.Font( family="Meiryo UI", size=15, weight="bold" )
        self.font_btn_small = font.Font( family="Meiryo UI", size=12, weight="bold" )

        self.font_lbl_bigger = font.Font( family="Meiryo UI", size=45, weight="bold" )
        self.font_lbl_big = font.Font( family="Meiryo UI", size=20, weight="bold" )
        self.font_lbl_middle = font.Font( family="Meiryo UI", size=15, weight="bold" )
        self.font_lbl_small = font.Font( family="Meiryo UI", size=12, weight="normal" )

        # ---------------------------------------------------------
        # カメラを開く
        # ---------------------------------------------------------

        self.vcap = cv2.VideoCapture( video_source )
        self.width = self.vcap.get( cv2.CAP_PROP_FRAME_WIDTH )
        self.height = self.vcap.get( cv2.CAP_PROP_FRAME_HEIGHT )

        # ---------------------------------------------------------
        # GUIアプリ
        # ---------------------------------------------------------
        #GUIアプリの実行
        self.create_widgets()
        self.delay = 15 #[mili seconds]
        self.update()


    def create_widgets(self):

        # ---------------------------------------------------------
        # 動画部の設定
        # ---------------------------------------------------------
        self.frame_cam = tk.LabelFrame(self.master, text = 'Camera', font=self.font_frame)
        self.frame_cam.place(x = 10, y = 10)
        self.frame_cam.configure(width = self.width+30, height = self.height+50)
        self.frame_cam.grid_propagate(0)
        self.canvas1 = tk.Canvas(self.frame_cam)
        self.canvas1.configure( width= self.width, height=self.height)
        self.canvas1.grid(column= 0, row=0,padx = 10, pady=10)

        # ---------------------------------------------------------
        # コントロールパネル1の設定
        # ---------------------------------------------------------
        self.frame_btn = tk.LabelFrame( self.master, text='Control', font=self.font_frame )
        self.frame_btn.place( x=10, y=550 )
        self.frame_btn.configure( width=self.width + 30, height=100 )
        self.frame_btn.grid_propagate( 0 )

        #circle detectionボタン
        self.btn_snapshot = tk.Button( self.frame_btn, text='circle detection', font=self.font_btn_big)
        self.btn_snapshot.configure(width = 12, height = 1, command=self.press_circle_detection)
        self.btn_snapshot.grid(column=0, row=0, padx=30, pady= 10)

        #Closeボタン
        self.btn_close = tk.Button( self.frame_btn, text='Close', font=self.font_btn_big )
        self.btn_close.configure( width= 12, height=1, command=self.press_close_button )
        self.btn_close.grid( column=1, row=0, padx=20, pady=10 )

        #Seveボタン
        self.btn_close = tk.Button( self.frame_btn, text='save', font=self.font_btn_big )
        self.btn_close.configure( width= 12, height=1, command=self.press_save_flag )
        self.btn_close.grid( column=2, row=0, padx=20, pady=10 )

        # ---------------------------------------------------------
        # コントロールパネル2の設定
        # ---------------------------------------------------------
        self.frame_param = tk.LabelFrame( self.master, text='Control', font=self.font_frame )
        self.frame_param.place( x=10, y=650 )
        self.frame_param.configure( width=self.width + 30, height=100 )
        self.frame_param.grid_propagate( 0 )
        
        #min Dist部
        self.min_Dist_label = tk.Label(self.frame_param, text="min_Dist", font=self.font_frame )
        self.min_Dist_label.grid(column=0, row=0, padx=10, pady= 10)

        self.minDist_number = tk.DoubleVar()
        self.minDist_number.set(self.mdist)
        self.minDist_var = ttk.Entry(self.frame_param,textvariable=self.minDist_number,width=5)
        self.minDist_var.grid(column=1, row=0, padx=10, pady= 10)

        #param1部
        self.param1_label = tk.Label(self.frame_param, text="param1", font=self.font_frame )
        self.param1_label.grid(column=2, row=0, padx=10, pady= 10)

        self.param1_number = tk.DoubleVar()
        self.param1_number.set(self.par1)
        self.param1_var = ttk.Entry(self.frame_param,textvariable=self.param1_number,width=5)
        self.param1_var.grid(column=3, row=0, padx=10, pady= 10)

       #param2部
        self.param2_label = tk.Label(self.frame_param, text="param2", font=self.font_frame )
        self.param2_label.grid(column=4, row=0, padx=10, pady= 10)

        self.param2_number = tk.DoubleVar()
        self.param2_number.set(self.par2)
        self.param2_var = ttk.Entry(self.frame_param,textvariable=self.param2_number,width=5)
        self.param2_var.grid(column=5, row=0, padx=10, pady= 10)

        #changeボタン
        self.btn_change = tk.Button(self.frame_param, text='change', font=self.font_btn_big )
        self.btn_change.configure( width= 12, height=1, command=self.press_change )
        self.btn_change.grid( column=6, row=0, padx=10, pady=10 )

    def update(self):
        #動画を撮る
        ret, frame = self.vcap.read()

        #今の時間を所得
        self.second = time()-self.init_time

        if ret:
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

            # cirile_detection flagが立っていたら画像検出開始
            if self.circle_detection_flag:
                gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
                circle = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp = 1, minDist=self.mdist,param1 = self.par1, param2 = self.par2,minRadius=0,maxRadius=0)
                if circle is not None:
                    for i in circle:
                        for j in range(len(i)):
                            x,y,r = i[j]
                            frame = cv2.circle(frame, (int(x), int(y)), int(r), (255, 0, 0), 3)

                    # save_flagが立っていたら保存開始
                    if self.save_flag:
                        with open(self.csvfile,"a",newline='') as f:
                            writer_object = writer(f)
                            writer_object.writerow([self.second]+i.flatten().tolist())
                            f.close()
            
            #動画再生
            self.photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(frame))

            #self.photo -> Canvas
            self.canvas1.create_image(0,0, image= self.photo, anchor = tk.NW)
            self.master.after(self.delay, self.update)
        else:
            cap.set(cv2.CAP_PROP_POS_FRAMES,0)
        

    #closeボタンを押したときの処理を定義
    def press_close_button(self):
        self.master.destroy()
        self.vcap.release()

    #circle_detectionボタンを押したときの処理を定義
    def press_circle_detection(self):
        self.circle_detection_flag = not self.circle_detection_flag

    #saveボタンを押したときの処理を定義
    def press_save_flag(self):
        self.save_flag = not self.save_flag

    #changeボタンを押したときの処理を定義
    def press_change(self):
        ###fitting parameter###
        self.mdist = self.minDist_number.get()
        self.par1 = self.param1_number.get()
        self.par2 = self.param2_number.get()

#プログラムを実行
def main():
    root = tk.Tk()
    app = Application(master=root)#Inherit
    app.mainloop()

if __name__ == "__main__":
    main()

ボタン:
circle_detection→円検知をスタート、ストップ
close→カメラの終了
save→”test.csv”というファイルを生成してそれに円の半径、x座標、y座標(r,x,y)情報を保存

パラメーター調整:
min Dist→円同士の離れている最低の距離、2つの円があったときにこの値(pixel)より大きく離れている場合にお互いの円を検出
param1→Canny エッジ検出の閾値の上限値 (下限値は上限値の 1/2 が推奨)
param2→ 円検出の閾値 (小さいほど検出される円の数が少なくなり、大きいほど多くの円が検出されます)

といった風になっています。

コメント

タイトルとURLをコピーしました