[OpenCV] 円を検出する

## はじめに
Hough変換による円の検出は、コンピュータビジョンにおける一般的な処理の一つです。この方法は、画像中の形状（この場合は円）を特定するために使用されます。

## Hough変換とは
Hough変換は、様々なサイズや位置にある円を効果的に検出することができるため、工業検査、画像分析、コンピュータビジョンの研究など多くの分野で広く使用されています。


## HoughCircles関数の使用

```python:
# HoughCircles関数を使用して円を検出
circles = cv2.HoughCircles(img, 
                           cv2.HOUGH_GRADIENT, 
                           dp=1, 
                           minDist=20, 
                           param1=50, 
                           param2=30, 
                           minRadius=0, 
                           maxRadius=0)

# circles変数をfloatからintに変換
circles = np.uint16(np.around(circles))

```

### 引数の説明
* cv2.HOUGH_GRADIENT: 検出方法。現在はこの方法のみが実装されています。
* dp: 画像解像度に対する累積器解像度の比率。dp=1 は同じ解像度。通常0.8~1.2です。
* minDist: 検出された円の中心同士の最小距離。単位はピクセルです。最初は10にしたほうがいいです。
* param1: Cannyエッジ検出器の高い閾値（低い閾値はこの値の半分に設定されます）。最初は100にしてください。
* param2: Hough変換の累積器の閾値。この値が小さいほど、より多くの円が検出されますが、誤検出の可能性も高まります。最初は30にしてください。
* minRadius: 検出される円の最小半径。単位はピクセルです。
* maxRadius: 検出される円の最大半径。単位はピクセルです。

実際の検出の時には、まず、以下の引数を固定にしてください。
* cv2.HOUGH_GRADIENT
* dp=1
* param1 = 100
* param2 = 30
特に、param1とparam2で頑張るよりは、他のパラメータを調整したほうがいいです。

* minDist = 10 、これは検出される円が複数になりますが、それをフィルターする時に有効です。
* 検出される円の直径が分かる前提で、`minRadius < 検出したい円の直径 < maxRadius`
の関係であることを覚え、この引数を調整してください。



## 実際の処理

以下の順序で処理を行います。

1. 画像を読み取り、Gray Scaleに変換
1. Gasussian Blur処理でノイズを除去
1. Canny Edge 検出を利用
1. Hough変換で円を検出

下記の機械部品の写真があります。外径と内径を検出してみます。
![live-img-2024-01-09-17-44-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/9b4f1ff8-cebe-d187-c09f-f46527804d02.png)


```python:

import cv2
import numpy as np

def empty(x):
    pass

cv2.namedWindow('Parameters')
cv2.resizeWindow('Parameters', width=1000, height=400)
cv2.createTrackbar('k_size_set', 'Parameters', 1, 10, empty) #3
cv2.createTrackbar('canny_1st', 'Parameters', 90, 500, empty) #80
cv2.createTrackbar('canny_2nd', 'Parameters', 60, 500, empty) #120
cv2.createTrackbar('minDist_set', 'Parameters', 100, 200, empty)
cv2.createTrackbar('param1_set', 'Parameters', 100, 300, empty) #100
cv2.createTrackbar('param2_set', 'Parameters', 30, 300, empty) #30
cv2.createTrackbar('minRadius_set', 'Parameters',510, 1000, empty) #250
cv2.createTrackbar('maxRadius_set', 'Parameters', 0, 1000, empty ) #500

#inner (minR, maxR) = (172,278)
#outer (minR, maxR) = (490,570)


img_src = cv2.imread(
    "./00.Hough_Transform/Images/live-img-2024-01-09-17-50-49.png", 1)


# Circle Detection Process
# 1. Color -> GrayScale
# 2. Gaussian Blur :: parameter -> kernel
# 2.5. Canny Edge Detection
# 3. Houhgh Transform


while True :
    
    #make a copy
    img_dst = img_src.copy()
    
    
    # 1. Color -> GrayScale    
    img_gray = cv2.cvtColor(img_src, cv2.COLOR_BGR2GRAY)
    
    #2. Gaussian blur
    kernel = cv2.getTrackbarPos("k_size_set", "Parameters")
    kernel = (kernel * 2) + 1
    img_blur = cv2.GaussianBlur(img_gray, (kernel, kernel), None)
    
    #2.5.Canny Edge Detection
    thres1_val = cv2.getTrackbarPos('canny_1st', 'Parameters')
    thres2_val = cv2.getTrackbarPos('canny_2nd', 'Parameters')
    img_edge = cv2.Canny(img_blur, threshold1=thres1_val, threshold2=thres2_val)
    
    # 3. Houhgh Transform
    
    circles = cv2.HoughCircles(img_edge, cv2.HOUGH_GRADIENT,
                               dp=1,
                               minDist=cv2.getTrackbarPos('minDist_set', 'Parameters'),
                               param1=cv2.getTrackbarPos('param1_set', 'Parameters'),
                               param2=cv2.getTrackbarPos('param2_set', 'Parameters'),
                               minRadius=cv2.getTrackbarPos('minRadius_set', 'Parameters'),
                               maxRadius=cv2.getTrackbarPos('maxRadius_set', 'Parameters'),
                               )
    
    try:
        circles = np.uint16(np.around(circles))

        for circle in circles[0, :]:
            # 円周を描画する
            cv2.circle(img_dst, (circle[0], circle[1]), circle[2], (0, 165, 255), 5)
            print('radius')
            print(circle[2])

            # 中心点を描画する
            cv2.circle(img_dst, (circle[0], circle[1]), 2, (0, 0, 255), 3)
            print('center')
            print(circle[0], circle[1])
            
        # 4. Plotting
        cv2.imshow('result', img_dst)
        
    except:
        pass

    # qを押すと止まる。
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()

```
### 検出結果
#### パラメータ調整前
下記のグラフは途中結果も含まれています。
オリジナル画像ーGray Scale画像
Canny Edge画像-Hough変換画像の順です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/7b530edf-c0fa-bf5f-62d4-869391388705.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/3e54f21c-e5bd-ea74-526f-66865aa5d81e.png)
上記のDefaultパラメータでは、複数の円が検出されました。

#### パラメータ調整後
部品の外径が約540ピクセル前後であることが分かるため、minRadiusとmax_Radiusを如何に設定します。

、`minRadius(**490**) < 検出したい円の直径(=~540) < maxRadius (**570**)`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/7127a199-46f3-060b-0e71-0ff6cc68ad03.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/4d1623f6-f02f-e7cc-8962-1b43860fb0fc.png)


#### 内径の検出
同様に内径を検出してみます。
部品の外径が約230ピクセル前後であることが分かるため、minRadiusとmax_Radiusを如何に設定します。

、`minRadius(**170**) < 検出したい円の直径(=~230) < maxRadius (**280**)`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/580f305b-f124-ee18-ea06-0199de3b15df.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/0eec18ce-8722-5d8f-6bd5-eca59227e0c4.png)

## 追加
Streamlitでアプリを作成してみました。

https://detectcircle.streamlit.app/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/34f48022-94f7-ff9c-88a7-d2676bf7bc3c.png)

#参考文献
https://shikaku-mafia.com/cv2-houghcircles/

https://qiita.com/kotai2003/items/662c33c15915f2a8517e