ラズパイ4 でOpenCVのハフ(HougH)変換を使って、秒針を読む

概要

 先回、ラズパイ4に pypylon をインストールし、Basler社カメラを使える様にしましたが、今回は近所の電気屋さんで比較的リーズナブルに購入できるwebカメラを使ってみます。
 また、OpenCVのハフ(Hough)変換という処理を使って、水平方向のスマホ時計の位置・傾きを検出し、秒針を読み取る検討をしました。奥行方向移動は今回未検討です。

結果

 次動画の通り 左上に秒針読取結果を水色表示しています。途中で時計を水平移動しますが、位置・傾きを補正し結果表示します。細くはいろいろ課題がありますが、とりあえず動いてくれました。

【 スマホストップウォッチの秒針を読む 】

処理フロー

 処理フローは次の通りです。
 ① ハフ変換(円)で時計部と中心座標を検出(黄緑色)
 ② ハフ変換(直線)で秒針(ピンク)検出
   検出直線で円中心に最も近いものを秒針と判定
 ③ 秒針の始点を円中心に近い側として傾き計算
 ④ ハフ変換(直線)でスマホ上端(水色)検出
   円中心からの距離と傾き(10度以内)で判定
 ⑤ 秒針傾きとスマホ傾きから、経過時間(秒)計算

  

 OpenCVのサンプルプログラムでは、ハフ変換(直線)前にCanny法でエッジ検出しているので同じ様にしています。参考までに処理結果は次の様になりました。

  

プログラム

 今回のテストしたプログラムです。

import cv2
import math
import numpy as np
import time


x3 = 0              # 秒針中心座標初期化(x)
y3 = 0              # 秒針中心座標初期化(y)
sec_trs=0           # 経過時間
ang_pin=0           # 秒針角度
ang_hsi=0           # 補正角度
pic_cnt=0           # 画像保存枚数

capture = cv2.VideoCapture(0)
if capture.isOpened() is False:
  raise IOError

# ループ処理
while(True):
  try:
    ret, frame = capture.read()
    if ret is False:
      raise IOError

    img_gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)       # グレイ変換
    edges = cv2.Canny(img_gray,10,220,apertureSize=3)       # エッジ検出

    # ハフ変換で時計部円検出 (半径等条件設定により、時計部のみ検出する様にしている)
    circles=cv2.HoughCircles(img_gray,cv2.HOUGH_GRADIENT,1,30,param1=70,param2=30,minRadius=80,maxRadius=110)
    cnt_cntr=0
    if circles is not None:
      for cx,cy,r in circles.squeeze(axis=0).astype(int):
        cv2.circle(frame,(cx,cy),r,(0,255,0),2)
        cv2.circle(frame,(cx,cy),1,(0,0,255),3)
        x3_tmp = cx
        y3_tmp = cy
        cnt_cntr = cnt_cntr + 1

    if cnt_cntr==1:
      x3 = x3_tmp
      y3 = y3_tmp

    if x3 > 0 and y3 > 0 :
      # ハフ変換で直線抽出   
      lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180, threshold=90,minLineLength=20,maxLineGap=5)
      ang_hsi =0
      
      if lines is not None:  
        for line in lines:
          x1 , y1 , x2 , y2 = line[0]                       # 検出直線始点・終点座標取得

          # 円中心と検出した直線の距離計算
          u = np.array([ x2-x1 , y2-y1 ])
          v = np.array([ x3-x1 , y3-y1 ])
          D_CTR = math.floor(abs(np.cross(u,v) / np.linalg.norm(u))) 
          
          D_P1 = math.sqrt((x1-x3)**2+(y1-y3)**2)           # 円中心と直線始点の距離計算
          D_P2 = math.sqrt((x2-x3)**2+(y2-y3)**2)           # 円中心と直線終点の距離計算

          # 距離が短い点を始点として角度計算(ラジアン)
          if D_P1 > D_P2 and D_CTR < 10:
            rslt_rad = math.atan2( y1-y2 , x1-x2 )
          else :
            rslt_rad = math.atan2( y2-y1 , x2-x1 ) 
            
          rslt_ang = math.degrees(rslt_rad)                 # ラジアンから角度変換

          # 0秒=0度として、角度変換
          if rslt_ang > -90 :
            rslt_ang_360 = rslt_ang + 90
          else :
            rslt_ang_360 = rslt_ang + 450

          dsp_ang = str(math.floor(rslt_ang_360))
  
          if D_CTR < 10:
            # 円中心からの距離が10以下 → 秒針と判定
            cv2.putText(frame,dsp_ang,(x2,y2),cv2.FONT_HERSHEY_PLAIN,2,(255,0,255),1,cv2.LINE_AA)
            cv2.line(frame,(x1,y1),(x2,y2),(255,0,255),3)   # 秒針描画(ピンク)
            ang_pin = rslt_ang_360
      
          else:
            # 秒針以外に検出した直線
            if rslt_ang < 15 and rslt_ang > -15 and D_CTR > 150 and D_CTR < 220 and y1 < y3 and y2 < y3 :
              # スクリーン上側の水平方向直線(傾き10度以内 + 他条件)
              cv2.putText(frame,str(math.floor(rslt_ang))+",D" + str(D_CTR),(x1,y1+30),cv2.FONT_HERSHEY_PLAIN,1,(255,125,0),1,cv2.LINE_AA)
              cv2.line(frame,(x1,y1),(x2,y2),(255,255,0),3) # スクリーン上側水平方向直線(水色)
              ang_hsi = rslt_ang                            # 補正角度記憶
              
            else:
              # その他の検出した直線
              #cv2.putText(frame,str(math.floor(rslt_ang))+",D" + str(D_CTR),(x1,y1-10),cv2.FONT_HERSHEY_PLAIN,1,(0,0,255),1,cv2.LINE_AA)
              cv2.line(frame,(x1,y1),(x2,y2),(0,0,255),2)   # 赤色直線
           
        
  except KeyboardInterrupt:
    cv2.putText(frame,"Error",(25,25),cv2.FONT_HERSHEY_PLAIN,2,(0,0,255),1,cv2.LINE_AA)

  # 角度から秒に変換(秒針傾きと補正角度考慮)
  sec_trs = math.floor( 60 * (ang_pin - ang_hsi) / 360 )
  if sec_trs<=0:
    sec_trs = 60+sec_trs
  if sec_trs>=60:
    sec_trs = sec_trs-60  

  # 経過時間・時計部中心座標等、表示追加
  cv2.putText(frame,"TIME : "+str(sec_trs)+"sec",(15,45),cv2.FONT_HERSHEY_PLAIN,3,(255,255,0),1,cv2.LINE_AA)
  cv2.putText(frame,"( Center Of Circle : "+str(x3)+" , "+str(y3) + " )",(350,25),cv2.FONT_HERSHEY_PLAIN,1,(0,255,255),1,cv2.LINE_AA)

  edges_rgb=cv2.cvtColor(edges,cv2.COLOR_GRAY2RGB)        # 画像合成用に変換
  cv2.putText(edges_rgb,"Canny Edge Detection",(15,45),cv2.FONT_HERSHEY_PLAIN,3,(0,0,255),1,cv2.LINE_AA)

  img_mrg=cv2.hconcat([edges_rgb,frame])                  # 画像合成
  cv2.imshow( 'img_mrg' , img_mrg )

  cv2.imshow('frame',frame)
  #cv2.imshow('edges',edges_rgb)

  time.sleep(0.09)

  # ESCで処理中断
  k = cv2.waitKey(1)
  if k==27:
    break

capture.release()
cv2.destroyAllWindows()

まとめ

 照明・背景色・移動範囲等をある程度限定していますが、時計移動に追従して、秒針を解析することが出来ました。ただ、連続して動かすと、誤認識等もあるので今後の課題として認識しておきます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です