[OpenCV]円の検出はハフ変換より最小外接円がいいぞ
(2026/01/02 デモの映像を追加し、ボール検出用のコードを更新)
赤いボールを検出しようとした話。
巷ではハフ変換を使った方法がある。
これ説明するのは難しいけど、この辺がわかりやすいかな。↓↓
でも、ハフ変換は処理が重い上に精度が悪い。なーんかもたつくし、できたとしても検出された円の大きさが安定しない。
なんだかなぁと思っていると、経験ある友達が「最小外接円を使った方がいい」と教えてくれた。
実際使ってみたところ、はるかに精度いい(厳密には違うが)しスムーズだった。
「精度がいい」というよりは、円を検出してるわけじゃないから認識が簡単になっていて、精度よく検出できているように感じる、と言った方が正確かな。
デモとして、ハフ変換と最小外接円それぞれで赤いボールの検出の比較をしてみた。
まずハフ変換の方。滅多に円が検出されない上に、検出されても円のサイズがめちゃめちゃに暴れる。
円の大きさで距離を推定したい場合なんかは致命的になる。
続いて最小外接円を使った方式。
基本的にずっとボールが適切に検出されているし、大きさもほぼ狂わない。ボールのブレにも追随できている。
悪くないでしょう?
もちろんハフ変換が最適な場合もあると思うけど、今回は赤いボール1つだけ検出できればよかったから、最小外接円方式で十分快適だった。
検証に使ったコード例を下に置いておく。(検証当時のOpenCVのバージョンは4.12.0)
例えば下記をdetect_red_ball.pyと保存して"python detect_red_ball.py -m -c"とすると、カメラからの映像を元に最小外接円方式で赤い円検出を行う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | # -*- coding: utf-8 -*- import argparse import numpy as np import cv2 def getCircle_mec(oc): MIN_RADIUS = 25 # 輪郭抽出 contours_info = cv2.findContours(oc, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # OpenCV 3 -> (image, contours, hierarchy), OpenCV 4 -> (contours, hierarchy) if len(contours_info) == 2: contours, hierarchy = contours_info else: img, contours, hierarchy = contours_info print("{} contours.".format(len(contours))) if len(contours) > 0: # 一番大きい赤色領域を指定する # contours may be a tuple in some OpenCV builds, so convert/sort into a list contours = sorted(contours, key=cv2.contourArea, reverse=True) cnt = contours[0] # 最小外接円を用いて円を検出する (x, y), radius = cv2.minEnclosingCircle(cnt) center = (int(x), int(y)) radius = int(radius) # 円が小さすぎたら円を検出していないとみなす if radius < MIN_RADIUS: return None else: return center, radius else: return None def getCircle_hough(frame, oc): # フレーム画像とマスク画像の共通の領域のグレースケールを抽出する。(3つ目のチャネルがグレースケール) extracted = cv2.split(cv2.bitwise_and(frame, frame, mask=oc)) #ハフ変換を適用し、映像内の円を探す circles = cv2.HoughCircles( extracted[2], cv2.HOUGH_GRADIENT, dp = 1, minDist = 50, param1 = 180, param2 = 25, minRadius = 20, maxRadius = 250 ) if circles is None: return None else: # circles の形状は (1, N, 3) または (N,3) の場合があるため整形 circles = np.squeeze(circles) if circles.ndim == 1: # 単一の円だけ返ってきた場合の対応 x, y, r = circles else: # 最も半径が大きい円を選択 idx = int(np.argmax(circles[:, 2])) x, y, r = circles[idx] return (int(x), int(y)), int(r) if __name__ == '__main__': # ”-h"はハフ変換オプション用。"-h"でヘルプの表示はしない parser = argparse.ArgumentParser(description='Detect circles from camera or a video file (plays while processing).', add_help=False) # 代わりに"--help"を導入 parser.add_argument('--help', action='help', help='show this help message and exit') # 映像ソース選択(カメラか動画か) group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-c', '--camera', action='store_true', help='Use camera as input') group.add_argument('-v', '--video', help='Path to the video file to process') # 円検出方式選択(ハフ変換か最小外接円か) method_group = parser.add_mutually_exclusive_group(required=True) method_group.add_argument('-m', '--mec', action='store_true', help='Use contour-based minimum enclosing circle method') method_group.add_argument('-h', '--hough', action='store_true', help='Use HoughCircles based method') args = parser.parse_args() # 検出に使用するHSV 範囲 lower_hsv = np.array([1, 149, 167]) upper_hsv = np.array([179, 255, 255]) if args.camera: cap = cv2.VideoCapture(0) if not cap.isOpened(): print('Failed to open camera') cap.release() exit(1) window_name = 'Circle Detect - camera' else: cap = cv2.VideoCapture(args.video) if not cap.isOpened(): print('Failed to open video:', args.video) cap.release() exit(1) window_name = f'Circle Detect - {args.video}' while True: # 赤色の円を抽出する ret, frame = cap.read() if not ret or frame is None: # EOFまたは読み取りエラー print('End of input or failed to read frame') break # HSVによる画像情報に変換 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # ガウシアンぼかしを適用して、認識精度を上げる blur = cv2.GaussianBlur(hsv, (9, 9), 0) # 指定した色範囲のみを抽出する color = cv2.inRange(blur, lower_hsv, upper_hsv) # オープニング・クロージングによるノイズ除去 element8 = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], np.uint8) oc = cv2.morphologyEx(color, cv2.MORPH_OPEN, element8) oc = cv2.morphologyEx(oc, cv2.MORPH_CLOSE, element8) # 検出メソッドを選択 if args.mec: detected_circle = getCircle_mec(oc) else: detected_circle = getCircle_hough(frame, oc) if detected_circle is not None: # 見つかった円の上に青い円を描画 # detected_circle[0]:中心座標、detected_circle[1]:半径 cv2.circle(frame, detected_circle[0], detected_circle[1], (255, 0, 0), 2) print(detected_circle[1]) else: print("No circle detected") # 検出結果とともに映像を表示 cv2.imshow(window_name, frame) # 最小の待ち時間(1ms)でループを回す。'q'で終了 if cv2.waitKey(1) & 0xFF == ord('q'): break # 終了時に映像ソースを解放 cap.release() cv2.destroyAllWindows() |
みんな、最小外接円、使おうぜ!

この記事へのコメントはこちら